npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

tenilla

v0.3.6

Published

A lightweight DOM manipulation framework

Readme

Tenilla

npm version npm downloads License TypeScript ESM

A lightweight browser DOM manipulation framework.

Tenilla manipulates the real DOM directly through a fluent chainable API — no virtual DOM, no reactive layer, no runtime scheduler. It gives you full control over the DOM while keeping your code clean and concise.

Features

  • Fluent DOM creationh(), hAlias(), svg(), mathml() with full TypeScript tag name type inference
  • Chainable prototype extensions.on(), .attr(), .child(), .css(), .tap() on any DOM node
  • Built-in UI components — 18 components: Modal, Pagination, Tooltip, SmartForm, TabPanel, DatePicker, TimePicker, DateTimePicker, Tree, TreePanel, Grid, Button, StringInput, NumberInput, TextArea, BooleanInput, Select, CheckboxGroup, RadioGroup
  • Zero runtime dependencies — core and components depend only on each other
  • Pure ESM with tree-shaking support — outputs .mjs artifacts with .d.mts type declarations
  • Import on demand — each component has its own sub-path and accompanying CSS

Installation

# Core DOM utilities (umbrella package)
npm install tenilla

# Or install core directly
npm install @tenilla/core

# Component library (requires @tenilla/core as a peer dependency)
npm install @tenilla/components

Note: Tenilla is distributed as ESM. Your project should use "type": "module" or a compatible bundler.

Quick Start

import { hAlias } from 'tenilla';

const [div, button, span] = hAlias('div,button,span');

document.body.append(
  div('container')
    .child(
      span('greeting', 'Hello, Tenilla!'),
      button('btn', 'Click me')
        .attr('type', 'button')
        .css({ padding: '8px 16px', cursor: 'pointer' })
        .on('click', () => alert('It works!')),
    ),
);

Core API (tenilla / @tenilla/core)

Element Creation

import { h, hAlias, div, option, checkbox } from 'tenilla';

// Create elements with h()
const el = h('section', 'card');            // <section class="card">
const withChild = h('div', 'wrapper', h('p', '', 'Hello'));

// hAlias — batch create typed creators from a comma-separated string
const [header, main, footer] = hAlias('header,main,footer');

// Built-in shortcut creators
const d = div('my-div');                    // equivalent to h('div', 'my-div')
const opt = option('val', 'Label', true);   // <option value="val" selected>Label</option>
const cb = checkbox('cb-class', false);     // <input type="checkbox" class="cb-class">

SVG & MathML

import { svg, svgAlias, mathml, mathMLAlias } from 'tenilla';

const circle = svg('circle', { cx: '50', cy: '50', r: '40' });
const [rect, path] = svgAlias('rect,path');

const mi = mathml('mi', {}); // MathML element

Chainable DOM Extensions

Tenilla extends native DOM prototypes for a fluent chainable API:

| Method | Target | Description | | ------------------------------- | --------- | ---------------------------------------------------------------------------- | | .on(type, listener, options?) | Node | Chainable addEventListener with typed event map | | .tap(fn) | Node | Execute a side-effect callback, returns this | | .attr(name, value) | Element | Chainable setAttribute; undefined \| null \| false removes the attribute | | .attrs(record) | Element | Batch set attributes; false values are skipped | | .child(...nodes) | Element | Chainable append | | .class(className, toggle?) | Element | Chainable classList.toggle | | .classes(classNames) | Element | Set className | | .styleText(text) | Element | Set style.cssText | | .styles(styleObj) | Element | Object.assign to style | | .styleProp(name, value) | Element | Set a single CSS property | | .styleProps(record) | Element | Batch set CSS properties |

div('card')
  .attr('role', 'article')
  .attrs({ 'data-id': '42', hidden: false })
  .styles({ display: 'flex', gap: '12px' })
  .child(h('h2', 'title', 'Card Title'))
  .on('click', (e) => console.log(e));

Event Bus

import { SimpleEvent } from 'tenilla';

interface Events {
  change: [value: number];
  reset: [];
}

const bus = new SimpleEvent<Events>();

const handler = (v: number) => console.log(v);
bus.on('change', handler);
bus.emit('change', 42);
bus.off('change', handler);

Date Utilities

import { _formatDate, _formatTime, _formatDateTime, _isSameDay, _pad } from 'tenilla';

_formatDate(new Date());      // '2026-08-02'
_formatTime(14, 30);         // '14:30'
_pad(5);                      // '05'
_isSameDay(dateA, dateB);     // boolean

Component Base Classes

import { TenillaComponent, TenillaInput } from 'tenilla';

// All components extend TenillaComponent, providing element and remove()
abstract class TenillaComponent {
  get element(): HTMLElement;
  abstract remove(): void;
}

// Input components extend TenillaInput, adding name, value, disabled, onChange
abstract class TenillaInput extends TenillaComponent {
  abstract name: string;
  abstract get value(): any;
  abstract set value(v: any);
  abstract get disabled(): boolean;
  abstract set disabled(v: boolean);
}

Components (@tenilla/components)

Each component is imported on demand via its sub-path:

import { Modal } from '@tenilla/components/Modal';
import '@tenilla/components/Modal.css';

Form Input Components

Basic input components, usable independently of SmartForm:

import { StringInput } from '@tenilla/components/StringInput';
import { NumberInput } from '@tenilla/components/NumberInput';
import { TextArea } from '@tenilla/components/TextArea';
import { BooleanInput } from '@tenilla/components/BooleanInput';

const name = new StringInput({ label: 'Username', placeholder: 'Enter...', onChange: v => console.log(v) });
const age = new NumberInput({ label: 'Age', value: 18, onChange: v => console.log(v) });
const bio = new TextArea({ label: 'Bio', placeholder: 'Describe...', onChange: v => console.log(v) });
const active = new BooleanInput({ label: 'Active', value: true, onChange: v => console.log(v) });

Selection Components

import { Select } from '@tenilla/components/Select';
import { CheckboxGroup } from '@tenilla/components/CheckboxGroup';
import { RadioGroup } from '@tenilla/components/RadioGroup';

const sel = new Select({ name: 'fruit', label: 'Select Fruit', value: 'apple', options: [...] });
sel.setOptions([...]);           // Replace options list
sel.setDisabled('durian', true); // Disable a single option
sel.disabled = true;             // Disable the entire control

const cg = new CheckboxGroup({ name: 'features', label: 'Select Features', value: ['grid'], options: [...] });
cg.checkAll();                   // Select all
cg.clear();                      // Clear all
cg.setOptions([...]);            // Replace options list

const rg = new RadioGroup({ name: 'theme', label: 'Theme', value: 'auto', options: [...] });

SmartForm

Schema-driven form builder with support for cross-field validation and Grid layout:

import { SmartForm } from '@tenilla/components/SmartForm';
import '@tenilla/components/SmartForm.css';

const form = new SmartForm([
  { row: [
    { name: 'title', label: 'Title', type: 'string', colspan: 8, placeholder: 'Enter title...',
      validator: (value: string) => {
        if (!value || value.trim().length === 0) {
          return '标题不能为空';
        }
        // Cross-field validation: high-priority items need longer titles
        const priority = form.get('priority');
        if (priority >= 8 && value.length < 10) {
          return '高优先级项目标题至少需要10个字符';
        }
        return true;
      },
    },
    { name: 'priority', label: 'Priority', type: 'number', colspan: 4, value: 3,
      validator: (value: number) => {
        if (value < 1 || value > 10) {
          return '优先级必须在1-10之间';
        }
        // Cross-field validation: Pattern articles can't be too low priority
        const category = form.get('category');
        if (category === 'pattern' && value < 5) {
          return 'Pattern类文章优先级至少为5';
        }
        return undefined;
      },
    },
  ]},
  { row: [
    { name: 'category', label: 'Category', type: 'select', colspan: 6,
      options: [
        { label: 'Guide', value: 'guide' },
        { label: 'Demo', value: 'demo' },
        { label: 'Pattern', value: 'pattern' },
      ],
    },
    { name: 'published', label: 'Published', type: 'boolean', colspan: 6, value: true },
  ]},
  { row: [
    { name: 'publishDate', label: 'Publish Date', type: 'date', colspan: 6,
      validator: (value: Date | null) => {
        if (!value) return '发布日期不能为空';
        return true;
      },
    },
    { name: 'deadline', label: 'Deadline', type: 'datetime', colspan: 6,
      validator: (value: Date | null) => {
        if (!value) return '截止日期不能为空';
        const publishDate = form.get('publishDate');
        if (publishDate && value <= new Date(publishDate)) {
          return '截止日期必须晚于发布日期';
        }
        return true;
      },
    },
  ]},
]);

document.getElementById('form-host')!.appendChild(form.element);

// Get form values
const values = form.value; // Fully typed based on schema

// Validate entire form
const validationResult = form.validate();
if (validationResult === true) {
  console.log('Form is valid!', values);
} else {
  console.error('Validation failed:', validationResult);
}

Key Features:

  • Cross-field validation: Use form.get('fieldName') in any validator to access other field values
  • Grid layout: Each field uses colspan (1-12) to control width in 12-column grid
  • Type-safe: Form value type is automatically inferred from schema
  • Rich field types: string, number, boolean, textarea, select, filter-select, checkboxes, radios, date, time, datetime

Modal

Dialog-based modal component (with <div> backdrop fallback).

import { Modal, FormModal } from '@tenilla/components/Modal';
import '@tenilla/components/Modal.css';

const modal = new Modal({
  title: 'Confirm Action',
  body: someElement,
  size: 'lg',             // 'sm' | 'lg' | 'xl' | ''
  backdrop: true,
  keyboard: true,
  confirmText: 'Confirm',
  cancelText: 'Cancel',
  showCancel: true,
  onConfirm: () => { /* return false to prevent closing */ },
  onHidden: () => console.log('closed'),
});

modal.show();
modal.setBody(newBody);
modal.hide();
modal.remove();

// Static convenience methods
const confirmed: boolean = await Modal.confirm({ title: 'Delete?', body: 'This action cannot be undone.' });
await Modal.alert({ title: 'Notice', body: 'Operation completed.' });

// FormModal — specialized subclass for form workflows
const fm = new FormModal<MyData>({ /* ... */ });
fm.setData(initialData);
const data = fm.getData();

Pagination

import { Pagination } from '@tenilla/components/Pagination';
import '@tenilla/components/Pagination.css';

const pager = new Pagination({
  element: document.getElementById('pager')!,
  currentPage: 1,
  totalItems: 200,
  pageSize: 20,
  showSizer: true,
  sizeOpts: [10, 20, 50],
  maxVisiblePages: 5,
  onChange: (page) => console.log('page', page),
  onSizeChange: (size) => console.log('size', size),
});

pager.changePage(3);
pager.update({ totalItems: 500 });
pager.remove();

Tooltip

Directional hover tooltip with Bootstrap-compatible variants.

import { Tooltip } from '@tenilla/components/Tooltip';
import '@tenilla/components/Tooltip.css';

const tip = new Tooltip(hostElement, 'Tooltip text', {
  direction: 'top',       // 'top' | 'bottom' | 'left' | 'right'
  variant: 'info',        // 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info' | 'light' | 'dark'
  delay: 200,
  customClass: 'my-tip',
});

tip.setContent('Updated text');
tip.setDirection('bottom');
tip.setVariant('danger');
tip.remove();

TabPanel

Tab panel with top/left tab positions and theming.

import { TabPanel } from '@tenilla/components/TabPanel';
import '@tenilla/components/TabPanel.css';

const tabs = new TabPanel({
  position: 'top',          // 'top' | 'left'
  theme: 'primary',         // 'default' | 'primary' | 'success' | 'warning' | 'danger'
  size: 'normal',           // 'small' | 'normal' | 'large'
  bordered: true,
  activeId: 'tab-1',
  onChange: (id) => console.log('active:', id),
});

tabs.add({ id: 'tab-1', title: 'Overview', body: overviewEl });
tabs.add({ id: 'tab-2', title: 'Settings', body: settingsEl, closable: true });
tabs.setActive('tab-2');
tabs.setDisabled('tab-1', true);
tabs.setVisible('tab-2', false);
tabs.remove('tab-2');
tabs.clear();
tabs.remove();

Grid

12-column grid system.

import { container, row, col } from '@tenilla/components/Grid';
import '@tenilla/components/Grid.css';

const layout = container().child(
  row().child(
    col(6).child(h('div', 'card', 'Left Column')),
    col(6).child(h('div', 'card', 'Right Column')),
  ),
);

Button

import { btn } from '@tenilla/components/Button';
import '@tenilla/components/Button.css';

const b = btn('primary', 'Click Me');
// Variants: 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info' | 'light' | 'dark'

Date & Time Pickers

import { DatePicker } from '@tenilla/components/DatePicker';
import { TimePicker } from '@tenilla/components/TimePicker';
import { DateTimePicker } from '@tenilla/components/DateTimePicker';

// Date picker
const dp = new DatePicker({ value: '2026-08-02', placeholder: 'Select date', onChange: d => console.log(d) });
dp.setValue(new Date());

// Time picker
const tp = new TimePicker({ value: { hour: 14, minute: 30 }, format: '24h', minuteStep: 5, onChange: d => console.log(d) });

// Date-time picker
const dtp = new DateTimePicker({ value: new Date(), placeholder: 'Select date & time', onChange: d => console.log(d) });

Tree

Collapsible tree control.

import { Tree } from '@tenilla/components/Tree';
import '@tenilla/components/Tree.css';

const tree = new Tree({
  data: [
    { id: '1', label: 'Node 1' },
    { id: '2', label: 'Node 2', expanded: true, children: [
      { id: '2-1', label: 'Child 1' },
    ]},
    { id: '3', label: 'Disabled Node', disabled: true },
  ],
  indent: '24px',           // Indent per level
  togglePosition: 'right',  // Arrow position: 'left' | 'right'
  onChange: (id, oldId) => console.log('selected', id),
  onToggle: (id, expanded) => console.log(id, expanded ? 'expanded' : 'collapsed'),
});

tree.expandAll();
tree.collapseAll();
tree.add({ id: 'new', label: 'New Node' });
tree.add({ id: 'child', label: 'Child Node' }, 'parent-id');
tree.remove('3');
tree.remove();

TreePanel

Layout component with a left Tree navigation and a right content area; body uses a lazy-loading function.

import { TreePanel } from '@tenilla/components/TreePanel';
import '@tenilla/components/TreePanel.css';

interface TreePanelData {
  id?: string | number | symbol;
  title?: string | number | symbol;
  body: () => HTMLElement;
  children?: TreePanelData[];
  disabled?: boolean;
  expanded?: boolean;
}

const panel = new TreePanel({
  data: [
    { title: 'Section 1', body: () => div('', 'Content 1') },
    { title: 'Section 2', body: () => div('', 'Content 2') },
  ],
  indent: '20px',
  togglePosition: 'left',  // or 'right'
  activeId: 'Section 1',
  onChange: (id) => console.log('selected', id),
});

panel.value = 'Section 2'; // Switch to a given item
panel.remove();

Package Structure

tenilla/                          # Monorepo root (pnpm workspace)
├── packages/
│   ├── tenilla/                  # Umbrella package — re-exports @tenilla/core
│   ├── core/                     # @tenilla/core — DOM utilities, prototype extensions, event bus, SVG/MathML
│   ├── components/               # @tenilla/components — UI component library (18 components)
│   └── document/                 # @tenilla/document — Vite-powered documentation site (private)
├── scripts/                      # Build and release automation
└── types/                        # Ambient type declarations

| Package | npm | Description | | --------------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------- | | tenilla | tenilla | Umbrella package (re-exports @tenilla/core) | | @tenilla/core | @tenilla/core | Core DOM utilities and prototype extensions | | @tenilla/components | @tenilla/components | Built-in UI components (requires @tenilla/core as peer dependency) |

Full Component List (18)

| Component | Sub-path | Description | | ------------------ | ------------------------------------ | ------------------------------------------------------- | | Modal | @tenilla/components/Modal | Modal dialog (includes FormModal, static confirm/alert) | | Pagination | @tenilla/components/Pagination | Pagination control | | Tooltip | @tenilla/components/Tooltip | Directional hover tooltip | | SmartForm | @tenilla/components/SmartForm | Schema-driven form builder | | TabPanel | @tenilla/components/TabPanel | Tab panel | | DatePicker | @tenilla/components/DatePicker | Date picker | | TimePicker | @tenilla/components/TimePicker | Time picker | | DateTimePicker | @tenilla/components/DateTimePicker | Date-time picker | | Grid | @tenilla/components/Grid | 12-column grid system | | Button | @tenilla/components/Button | Button | | StringInput | @tenilla/components/StringInput | Text input | | NumberInput | @tenilla/components/NumberInput | Number input | | TextArea | @tenilla/components/TextArea | Multi-line textarea | | BooleanInput | @tenilla/components/BooleanInput | Toggle (checkbox) | | Select | @tenilla/components/Select | Dropdown selector | | CheckboxGroup | @tenilla/components/CheckboxGroup | Multi-select group | | RadioGroup | @tenilla/components/RadioGroup | Radio group | | Tree | @tenilla/components/Tree | Tree control | | TreePanel | @tenilla/components/TreePanel | Left tree nav + right content area |

Development

# Install dependencies
pnpm install

# Run documentation site locally
pnpm dev

# Lint
pnpm lint

# Build all packages
pnpm build

# Release (patch version)
pnpm pub

# Release (minor / major version)
pnpm pubminor
pnpm pubmajor

Build Requirements

  • Node.js 18+
  • pnpm 9+
  • TypeScript 6.0+ (for consumers using types)

License

MIT — Copyright (c) 2026 kasukabe tsumugi