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

@nalashaacontrols/nalashaa-ui

v1.0.0

Published

Nalashaa in-house UI controls: data grid, tree view, list box, week view, inputs, dropdown, date/time/date-range pickers, dialog, tooltip, accordion, checkbox, switch, button. Vanilla JS core, optional React adapters, zero runtime dependencies, Syncfusion

Readme

@nalashaacontrols/nalashaa-ui

Nalashaa's in-house UI controls for EHR applications: data grid, tree view, list box, week view, splitter, text inputs, dropdown, date/time and date-range pickers, dialog, tooltip, accordion, checkbox, switch and button. Vanilla JavaScript core, optional React adapters, zero runtime dependencies, Syncfusion-compatible API and CSS class names so an existing app can switch by changing one import line.

Status: 1.0.0, first release. npm run build produces the full dist/ (16 components, ESM + CJS + types + CSS); npm test runs 160 tests. Release steps are in .claude/plan/03-publishing.md.


Components

| Component | Vanilla class | React component | Subpath | | --- | --- | --- | --- | | Data grid | UniversalGrid + feature modules | GridComponent, ColumnsDirective, ColumnDirective, Inject, PagerComponent | data-grid | | Button | Button | ButtonComponent | button | | CheckBox | CheckBox | CheckBoxComponent | checkbox | | Switch | Switch | SwitchComponent | switch | | TextBox | TextBox, NumericTextBox, MaskedTextBox | TextBoxComponent, TextAreaComponent, NumericTextBoxComponent, MaskedTextBoxComponent | textbox | | DropDownList | DropDownList | DropDownListComponent | dropdown | | DatePicker | DatePicker, DateTimePicker | DatePickerComponent, DateTimePickerComponent | datepicker | | TimePicker | TimePicker | TimePickerComponent | timepicker | | Dialog | Dialog | DialogComponent | dialog | | Tooltip | Tooltip | TooltipComponent | tooltip | | Accordion | Accordion | AccordionComponent, AccordionItemsDirective, AccordionItemDirective | accordion | | TreeView | TreeView | TreeViewComponent | treeview | | ListBox | ListBox | ListBoxComponent (+ CheckBoxSelection no-op) | listbox | | DateRangePicker | DateRangePicker | DateRangePickerComponent | daterangepicker | | WeekView | WeekView | WeekViewComponent | weekview | | Timeline | Timeline | TimelineComponent | timeline | | ProviderView | ProviderView | ProviderViewComponent | providerview | | Splitter | Splitter | SplitterComponent, PanesDirective, PaneDirective | splitter | | Uploader | Uploader | UploaderComponent | uploader | | DashboardLayout | DashboardLayout | DashboardLayoutComponent, PanelsDirective, PanelDirective | dashboardlayout | | Menu | Menu | MenuComponent | menu | | ComboBox | ComboBox | ComboBoxComponent | combobox | | ColorPicker | ColorPicker | ColorPickerComponent | colorpicker | | Pivot Table | PivotTable, PivotEngine | PivotViewComponent | pivot-table | | ContextMenu | ContextMenu (subclasses Menu) | ContextMenuComponent | contextmenu | | MultiSelect | MultiSelect (subclasses ComboBox) | MultiSelectComponent | multiselect |

Six more folders exist under src/components/ but are not part of the package yet (no entry in scripts/build.mjs, not in dist/, not importable): colorpicker, dropdownbutton, radiobutton, pager, sidebar and documenteditor. They are shown on the dev page (examples/basic/) for evaluation only. See "Adding a component" to promote one.


Installation

The package is private on registry.npmjs.org under the @nalashaacontrols scope. Log in once with an account that belongs to the org, then install:

npm login
npm install @nalashaacontrols/nalashaa-ui

React is an optional peer dependency. The root entry and the /react entries need it; @nalashaacontrols/nalashaa-ui/core and the /<name> entries do not.

npm install react@>=17 react-dom@>=17

Entry points

One import is enough. The package root exports every vanilla class and every React component together:

import { GridComponent, ColumnsDirective, ColumnDirective, Inject, Sort, Page,
         ButtonComponent, CheckBoxComponent, DatePickerComponent, DialogComponent,
         UniversalGrid, Button } from '@nalashaacontrols/nalashaa-ui';

Styles are injected automatically: a component adds its own <style> tag the first time it is constructed, so there is nothing else to import. The root import is the recommended one — the ESM build is code-split and has no import-time side effects (sideEffects lists only CSS), so a bundler ships only the components you actually use. The narrower entries exist for apps that want them:

| Import | What you get | | --- | --- | | @nalashaacontrols/nalashaa-ui | everything: every vanilla class + every React component (needs react) | | @nalashaacontrols/nalashaa-ui/core | every vanilla class only (no React needed) | | @nalashaacontrols/nalashaa-ui/react | every React component only | | @nalashaacontrols/nalashaa-ui/styles | one CSS file for all components (optional, see below) | | @nalashaacontrols/nalashaa-ui/<name> | one component's vanilla class(es) | | @nalashaacontrols/nalashaa-ui/<name>/react | one component's React adapter | | @nalashaacontrols/nalashaa-ui/<name>/styles | one component's CSS (optional) |

<name> is the subpath from the table above. ESM and CommonJS builds are both shipped (.esm.js / .cjs). Types are included; no @types package is needed.

Styles

No CSS import is required. Each component injects its styles into document.head the first time one is constructed (for React, on first mount) as <style id="nalashaa-ui-style-<name>">, once per component, and does nothing during server-side rendering. Importing alone injects nothing, which is what keeps the root import tree-shakeable.

If you prefer to control the CSS yourself (strict Content-Security-Policy without style-src 'unsafe-inline', or a global stylesheet pipeline), turn the injection off before the first import and load the file instead:

globalThis.NALASHAA_UI_DISABLE_STYLE_INJECTION = true;
import '@nalashaacontrols/nalashaa-ui/styles';          // all components
// or
import '@nalashaacontrols/nalashaa-ui/data-grid/styles'; // just the grid

Quick start

Vanilla JavaScript

import { Button } from '@nalashaacontrols/nalashaa-ui';

const btn = new Button(document.querySelector('#save'), {
  content: 'Save',
  isPrimary: true,
  iconCss: 'e-icons e-save',
});

btn.setProperties({ disabled: true });
btn.destroy();

Every class follows the same lifecycle: new Class(element, options) and destroy(), with defaults available as Class.defaults. Classes whose options can change after construction also expose setProperties(partial): Button, Switch, DatePicker/DateTimePicker, TimePicker, DateRangePicker, TreeView, ListBox, WeekView and UniversalGrid. The others (CheckBox, the text boxes, DropDownList, Dialog, Tooltip, Accordion) are configured at construction and driven through their methods; the React adapters handle prop changes for you either way. Vanilla callbacks use on* names (onChange, onInput, onOpen, onClose …); the React adapters accept Syncfusion's change, input, open, close props and map them for you. The dev page examples/basic/ (npm run dev) shows every component with live event logs. WeekView, Timeline, the data grid and DocumentEditor need the whole viewport, so picking one in the left list replaces the content area with it, full height and width (#weekview / #timeline / #providerview / #grid / #documenteditor link straight in). Each also has a standalone page that runs the same demo module.

React

import { ButtonComponent } from '@nalashaacontrols/nalashaa-ui';

export function SaveButton({ onSave }) {
  return (
    <ButtonComponent isPrimary iconCss="e-icons e-save" onClick={onSave}>
      Save
    </ButtonComponent>
  );
}

React components are forwardRef wrappers. The ref gives you the host DOM element (or the core instance where Syncfusion does the same), and props are mirrored to the core after mount.

Migrating from Syncfusion

Replace the package name in imports. Component names, prop names and the e- CSS classes are kept.

- import { GridComponent, ColumnsDirective, ColumnDirective, Inject, Sort, Page } from '@syncfusion/ej2-react-grids';
+ import { GridComponent, ColumnsDirective, ColumnDirective, Inject, Sort, Page } from '@nalashaacontrols/nalashaa-ui';

Data grid

Features are opt-in modules, mirroring Syncfusion's <Inject services={[…]} />. In vanilla usage only the modules you pass are bundled; the React GridComponent loads all modules and accepts <Inject> for Syncfusion source compatibility.

Available modules (long name / short name): SortModule/Sort, PageModule/Page, SelectionModule/Selection, FreezeModule/Freeze, ResizeModule/Resize, ReorderModule/Reorder, GroupModule/Group, DetailRowModule/DetailRow, ContextMenuModule/ContextMenu, ExcelExportModule/ExcelExport, AdaptiveModule/Adaptive, KeyboardModule/Keyboard, FilterModule/Filter, VirtualModule/Virtual. ALL_MODULES is the full list.

Vanilla

import { UniversalGrid, Sort, Page, Selection, Freeze } from '@nalashaacontrols/nalashaa-ui';

const grid = new UniversalGrid(document.querySelector('#grid'), {
  modules: [Sort, Page, Selection, Freeze],
  dataSource: rows,
  primaryKey: 'id',
  columns: [
    { field: 'id',   headerText: 'ID',   width: 80, isPrimaryKey: true },
    { field: 'name', headerText: 'Name', allowSorting: true },
    { field: 'due',  headerText: 'Due',  type: 'date', format: 'M/d/yyyy' },
  ],
  allowSorting: true,
  allowPaging: true,
  pageSettings: { pageSize: 25, pageSizes: [10, 25, 50, 'All'] },
  height: '100%',
  rowSelected: e => console.log(e.data),
});

grid.setDataSource(newRows);   // replace data
grid.showColumns(['due']);
grid.destroy();

Server-side paging and sorting: set dataSource to { result, count } and handle dataStateChange, exactly as with Syncfusion's DataStateChangeEventArgs.

React

import {
  GridComponent, ColumnsDirective, ColumnDirective, Inject,
  Sort, Page, Selection, Group,
} from '@nalashaacontrols/nalashaa-ui';

export function PatientsGrid({ rows }) {
  return (
    <GridComponent
      dataSource={rows}
      primaryKey="id"
      allowSorting
      allowPaging
      allowGrouping
      pageSettings={{ pageSize: 25 }}
      selectionSettings={{ type: 'Multiple', checkboxOnly: true }}
      rowSelected={e => console.log(e.data)}
      height="100%"
    >
      <ColumnsDirective>
        <ColumnDirective type="checkbox" width="40" />
        <ColumnDirective field="id"   headerText="ID"   width="80" isPrimaryKey />
        <ColumnDirective field="name" headerText="Name" />
        <ColumnDirective field="dob"  headerText="DOB"  type="date" format="M/d/yyyy" />
        <ColumnDirective headerText="Actions" template={row => <button>Edit</button>} />
      </ColumnsDirective>
      <Inject services={[Sort, Page, Selection, Group]} />
    </GridComponent>
  );
}

Column template and detailTemplate accept React elements; they are rendered through portals into the grid's cells. The grid ref exposes getSelectedRecords(), refresh(), setDataSource() and the other UniversalGrid methods (see GridHandle in the types).

Supported grid events: rowSelected, rowDeselected, rowSelecting, rowDataBound, dataBound, actionBegin, actionComplete, recordClick, recordDoubleClick, contextMenuOpen, contextMenuClick, dataStateChange.


Pivot Table

Phases 1–3 of 7 — an aggregation table (rows/columns/values axes, sub-totals, grand totals, Compact/Tabular layout, number/date formatting, all 23 AggregateTypes except CalculatedField) plus interaction (drill down/up, per-field member sort, a single-level value sort, cell/row/column selection, column resize, hyperlink-styled cells, cellTemplate and tooltips, keyboard navigation) plus a grouping bar (drag fields between Rows/Columns/ Values), member (checklist) filtering, and a right-click context menu (Expand/Collapse, change a value's aggregate). Still no 24-condition label/value/date/number filters, date/ number/custom grouping, toolbar, field list dialog, virtualization/paging, export or chart — see .claude/plan/17-pivottable.md for the remaining phases and what each one unblocks.

import { PivotTable } from '@nalashaacontrols/nalashaa-ui';

const pivot = new PivotTable(document.querySelector('#pivot'), {
  dataSourceSettings: {
    dataSource: orders,
    rows: [{ name: 'Region' }, { name: 'Country' }],
    columns: [{ name: 'Category' }],
    values: [{ name: 'Sales', type: 'Sum', format: { format: 'C', currency: 'USD' } }],
    showRowSubTotals: true,
    showGrandTotals: true,
  },
  dataBound: () => console.log(pivot.getEngine().pivotValues),
});

pivot.updateDataSource(newOrders);
pivot.destroy();
import { PivotViewComponent } from '@nalashaacontrols/nalashaa-ui';

export function SalesPivot({ orders }) {
  return (
    <PivotViewComponent
      dataSourceSettings={{
        dataSource: orders,
        rows: [{ name: 'Region' }],
        columns: [{ name: 'Category' }],
        values: [{ name: 'Sales', type: 'PercentageOfColumnTotal' }],
      }}
      queryCellInfo={(a) => console.log(a.data.formattedText)}
    />
  );
}

All 23 AggregateTypes: Sum, Product, Count, DistinctCount, Median, Min, Max, Avg, PopulationStDev, SampleStDev, PopulationVar, SampleVar (computed directly from each cell's records); Index, PercentageOfGrandTotal, PercentageOfColumnTotal, PercentageOfRowTotal, PercentageOfParentRowTotal, PercentageOfParentColumnTotal, PercentageOfParentTotal, RunningTotals, DifferenceFrom, PercentageOfDifferenceFrom (derived from a base aggregate — baseAggregateType, default Sum — plus the relevant grand/row/column/parent totals); CalculatedField is not implemented until the phase 5 formula engine lands.

Events: created, destroyed, load, enginePopulating, enginePopulated, dataBound, queryCellInfo, headerCellInfo, aggregateCellInfo. Methods: refresh(), refreshData(), updateDataSource(dataSource), getEngine(), getPersistData().

Interaction (phase 2)

<PivotViewComponent
  dataSourceSettings={{
    dataSource: orders,
    rows: [{ name: 'Region' }, { name: 'Country' }],
    columns: [{ name: 'Category' }],
    values: [{ name: 'Sales', type: 'Sum' }],
    expandAll: true,              // drill state — collapse a node with drilledMembers
    sortSettings: [{ name: 'Region', order: 'Descending' }],
    enableValueSorting: true,     // single-level value sort — see the plan doc's gap note
    valueSortSettings: { rowSortOrder: 'Descending' },
  }}
  selectionSettings={{ mode: 'Cell' }}   // 'Cell' | 'Row' | 'Column' | 'Both'
  hyperlinkSettings={{ enable: true, memberType: 'Value' }}
  showTooltip
  cellTemplate={({ data }) => `${data.formattedText} ✓`}   // rendered as text, never HTML
  drill={(a) => console.log('drilled', a.path)}
  onHeadersSort={(a) => console.log(a.fieldName, a.order)}
  cellSelected={(a) => console.log('selected', a.rowIndex, a.colIndex)}
  hyperlinkCellClick={(a) => console.log('clicked', a.data.formattedText)}
  resizeStop={(a) => console.log('column', a.column, 'is now', a.width, 'px')}
/>
  • Drill down/up: click the +/− icon on a collapsible header cell, or set expandAll: false (collapsed by default) and drilledMembers (an array of { path: [{ field, value }] }, our own reconstruction of Syncfusion's drill-state shape — no source to verify the exact wire format against). Event: drill.
  • Member sort: click the small sort icon next to any header's caption, or set sortSettings: [{ name, order }]. Event: onHeadersSort (args.cancel = true to block it).
  • Value sort: enableValueSorting + valueSortSettings (rowSortOrder/ columnSortOrder, optional headerText/headerDelimiter to pick which column/row to sort by — defaults to the grand total). Phase 2 scope: sorts each parent group's own children; it does not yet reorder parent blocks themselves in a multi-level axis (a documented gap, see the plan doc).
  • Selection: click a value cell (Cell/Both) or a row/column header (Row/Column/ Both). Single-selection only in phase 2 (no ctrl/shift multi-select or range yet). Events: cellSelecting (cancelable), cellSelected, cellDeselected, rowSelected, rowDeselected. Methods: applyRowSelection(rowIndex), applyColumnSelection(colIndex), clearSelection().
  • Column resize: drag the handle on a value column's right edge. Events: resizing (args.cancel = true to block it), resizeStop.
  • Hyperlink cells: hyperlinkSettings.memberType is 'Value', 'Header' or 'Both'. Rendered as a styled <span>, not a real <a href> (no navigation to prevent). Event: hyperlinkCellClick.
  • cellTemplate: return a string (rendered as text) or a DOM Node (appended as-is) — never raw HTML, so an untrusted template string can't inject markup.
  • Keyboard: arrow keys move focus between value cells; Enter/Space selects the focused cell, or toggles a focused drill/sort icon.

Grouping bar, filtering & context menu (phase 3)

Needed ContextMenu and MultiSelect (both above) as prerequisites — built specifically to power this phase's filter checklist and right-click menu.

<PivotViewComponent
  dataSourceSettings={{
    dataSource: orders,
    rows: [{ name: 'Region' }],
    columns: [{ name: 'Category' }],
    values: [{ name: 'Sales', type: 'Sum' }],
    filters: [{ name: 'Region', type: 'Include', items: ['East', 'West'] }],
  }}
  showGroupingBar   // default true
  onFieldDropped={(a) => console.log(a.fieldName, a.fromAxis, '→', a.toAxis)}
  fieldRemove={(a) => console.log('removed', a.fieldName, 'from', a.axis)}
  contextMenuClick={(a) => console.log(a.fieldName, '→', a.aggregateType)}
/>
  • Grouping bar (showGroupingBar, default true): a Fields strip lists every data source column not already in rows/columns/values — there is no field-list dialog yet (phase 4), so this strip stands in for it. Drag a field chip into Rows, Columns or Values to add it (dropping in Values defaults it to Sum); drag between Rows and Columns to move it; a chip's × removes it. Values chips are removable but not draggable (re-grouping a measure into a dimension is out of this phase's scope). Event: onFieldDropped (args.cancel = true to block a move), fieldRemove.
  • Member filter: click a field chip's ▼ icon (in the grouping bar or the Filters readout) to open a checklist of that field's distinct values, backed by MultiSelect and pre-checked to what's currently included; Apply narrows the pivot, Cancel discards. Set directly via dataSourceSettings.filters: [{ name, type: 'Include' | 'Exclude', items }]. This is member (checklist) filtering only — the 24-condition Label/Value/Date/Number filters from the captured Syncfusion spec are not implemented.
  • Context menu: right-click a collapsible header for Expand/Collapse (same action as its +/− icon); right-click a value cell for an Aggregate submenu of the 12 base aggregate types (hover — like any menu, it opens on hover, not click — then pick one). Event: contextMenuClick. Drillthrough/CalculatedField context items are not implemented (later phases).

Inputs

TextBox, NumericTextBox, MaskedTextBox

import {
  TextBoxComponent, TextAreaComponent, NumericTextBoxComponent, MaskedTextBoxComponent,
} from '@nalashaacontrols/nalashaa-ui';

<TextBoxComponent placeholder="First name" floatLabelType="Auto" showClearButton
                  value={name} input={e => setName(e.value)} />
<TextAreaComponent rows={4} placeholder="Notes" />
<NumericTextBoxComponent value={weight} min={0} max={500} step={0.5} format="n1"
                         change={e => setWeight(e.value)} />
<MaskedTextBoxComponent mask="000-000-0000" promptChar="_" placeholder="Phone" />

Common options: value, placeholder, cssClass, floatLabelType ('Never' | 'Always' | 'Auto'), enabled, readonly, showClearButton, width, htmlAttributes. Numeric adds min, max, step, format (n2, c2, p0 …), decimals, showSpinButton, strictMode, validateDecimalOnType. Masked adds mask and promptChar.

CheckBox

import { CheckBoxComponent } from '@nalashaacontrols/nalashaa-ui';
<CheckBoxComponent label="Active" checked={active} change={e => setActive(e.checked)} />

Options: checked, indeterminate, label, labelPosition ('Before' | 'After'), name, value, cssClass, disabled.

Switch

import { SwitchComponent } from '@nalashaacontrols/nalashaa-ui';
<SwitchComponent checked={hipaaAllowed} change={e => setHipaaAllowed(e.checked)} />

// captions inside the track, and a veto before the state flips
<SwitchComponent checked={active} onLabel="ON" offLabel="OFF"
                 beforeChange={e => { e.cancel = !confirmed; }}
                 change={e => setActive(e.checked)} />

Options: checked, onLabel, offLabel, name, value, id, cssClass, className, disabled, enableRtl. Events: change, beforeChange (set args.cancel = true to keep the current state), created. The ref exposes checked, element, toggle() and focusIn(). id lands on the hidden input, not the wrapper — as in Syncfusion — so an external <label for="…"> can drive the switch. Every option can be changed after construction on the vanilla core with setProperties(partial); the React adapter routes prop changes through it, so nothing remounts.

Renders the same DOM as Syncfusion's switch — div.e-switch-wrapper > input.e-switch + span.e-switch-inner (span.e-switch-on + span.e-switch-off) + span.e-switch-handle, with e-switch-active on the inner and handle when checked — so existing app-level .e-switch-* overrides keep applying. Passing checked makes it controlled: a click flips it immediately, and re-rendering with the old value pulls it back. Omit checked for an uncontrolled switch. Wrapping the switch in a <label> is safe: the click the label forwards to the hidden input is what toggles, so the caption and the track each toggle exactly once.

TreeView

import { TreeViewComponent } from '@nalashaacontrols/nalashaa-ui';

const rows = [
  { id: 'clinical', name: 'Clinical', hasKids: true, open: true },
  { id: 'enc', name: 'Encounters', parent: 'clinical' },
  { id: 'billing', name: 'Billing', hasKids: true },
];

<TreeViewComponent
  fields={{ dataSource: rows, id: 'id', text: 'name', parentID: 'parent',
            hasChildren: 'hasKids', expanded: 'open' }}
  expandOn="Click"
  selectedNodes={[selectedId]}
  nodeClicked={(e) => setSelectedId(e.node.getAttribute('data-uid'))}
/>

fields.dataSource takes either flat rows linked by parentID or hierarchical rows nested under child; the other fields entries name the columns (id, text, parentID, child, hasChildren, expanded, selected, isChecked, iconCss, imageUrl, tooltip, htmlAttributes, navigateUrl, selectable).

Options: fields, cssClass, className, expandOn ('Auto' | 'Click' | 'DblClick' | 'None' — the expander icon always works), fullRowSelect, allowMultiSelection (ctrl-click toggles, shift-click selects a range, Ctrl+A selects all visible), allowEditing (double click, F2 or beginEdit() turn the text into an input; Enter/blur saves, Escape cancels), allowTextWrap, showCheckBox, autoCheck (checks the subtree and puts ancestors in the mixed state), checkOnClick, checkDisabledChildren, selectedNodes, checkedNodes, expandedNodes, sortOrder, disabled, enableRtl, nodeTemplate, animation ({ expand: { duration, easing }, collapse: { … } }, default 400ms linear — a user toggle slides the child list, expandAll/collapseAll are instant). Events: nodeClicked, nodeSelecting, nodeSelected, nodeExpanding, nodeExpanded, nodeCollapsing, nodeCollapsed, nodeChecking, nodeChecked, nodeEditing, nodeEdited, keyPress, drawNode, dataSourceChanged, dataBound, created, destroyed — the *ing ones and keyPress take args.cancel = true. Every node event gets args.node (the li[data-uid] element) and args.nodeData.

The ref exposes expandAll(ids?, level?), collapseAll(ids?, level?), checkAll(), uncheckAll(), selectNodes(), disableNodes(), enableNodes(), getDisabledNodes(), ensureVisible(), addNodes(), removeNodes(), moveNodes(ids, target, index), beginEdit(), updateNode(), refreshNode(), getTreeData(), refresh(), getNode(), getAllCheckedNodes(), selectedNodes, checkedNodes, expandedNodes and element. Every ids argument takes node ids or li elements.

Coverage against Syncfusion's TreeView documentation: data binding (local hierarchical and flat), check boxes, multiple selection, node editing, node template, accessibility roles/keys, expand/collapse animation and the full method set above are in. Not in: drag and drop (allowDragAndDrop and its four events), remote data through DataManager (fields.query/tableName), lazy loadOnDemand rendering (every level renders up front), enablePersistence, locale, enableHtmlSanitizer.

nodeTemplate receives the row and returns React content, rendered into the node through a portal — so hooks, context and event handlers inside a template work like anywhere else in your tree:

<TreeViewComponent
  fields={fields}
  nodeTemplate={(data) => (
    <span title={data.name}>
      <i className={data.hasKids ? 'fa fa-folder' : 'fa fa-file'} />&nbsp;{data.name}
    </span>
  )}
/>

Renders Syncfusion's tree DOM — div.e-treeview.e-fullrow-wrap > ul.e-list-parent > li.e-list-item.e-level-N[data-uid] > div.e-fullrow + div.e-text-content (div.e-icons + span.e-list-text), with e-active, e-hover, e-node-focus and e-node-collapsed on the li — so existing app-level .e-treeview * overrides keep applying. Keyboard: arrows move and expand/collapse, Home/End jump, Enter selects, Space toggles a checkbox.

The vanilla core takes the same options with on* callback names (onNodeClicked, onNodeExpanded, …) and a nodeTemplate that returns an element or a trusted HTML string:

import { TreeView } from '@nalashaacontrols/nalashaa-ui';
const tree = new TreeView(document.querySelector('#chart'), { fields, expandOn: 'Click' });
tree.expandAll();
tree.setProperties({ selectedNodes: ['enc'] });

ListBox

import { ListBoxComponent } from '@nalashaacontrols/nalashaa-ui';

const payers = [{ text: 'Aetna', value: 'aetna' }, { text: 'Cigna', value: 'cigna' }];

<ListBoxComponent
  dataSource={payers}
  fields={{ text: 'text', value: 'value' }}
  value={allowed}
  selectionSettings={{ showCheckbox: true, showSelectAll: true }}
  change={(e) => setAllowed(e.value)}
/>

// dual list box: the toolbar moves items to/from the list named by `scope`
<ListBoxComponent id="available" dataSource={left} scope="#chosen"
                  toolbarSettings={{ items: ['moveUp', 'moveDown', 'moveTo', 'moveFrom', 'moveAllTo', 'moveAllFrom'] }} />
<ListBoxComponent id="chosen" dataSource={right} />

Options: dataSource (objects or primitives), fields (text, value, groupBy, iconCss, htmlAttributes), value (controlled, in data order), selectionSettings (mode: 'Single' | 'Multiple', showCheckbox, showSelectAll, checkboxPosition), toolbarSettings (items, position), scope, allowDragAndDrop, allowFiltering, filterBarPlaceholder, ignoreCase, ignoreAccent, height, enabled, sortOrder, maximumSelectionLength, itemTemplate (React content through a portal, or a ${field} string), noRecordsTemplate, cssClass/className, enableRtl. Events: change (value, items, elements), select (cancellable), beforeItemRender, filtering (updateData for your own data), actionBegin (cancellable) / actionComplete (eventName, items) for toolbar moves, dragStart, drag, beforeDrop (cancellable), drop, dataBound, created, destroyed.

The ref exposes value, dataSource, getDataByValue(), getDataByValues(), getDataList(), getSortedList(), enableItems(), selectItems(), selectAll(), addItems(), removeItems(), moveUp(), moveDown(), moveTop(), moveBottom(), moveTo(), moveAllTo(), moveFrom(), moveAllFrom(), refresh() and element. ListBoxComponent.Inject(CheckBoxSelection) is accepted as a no-op — checkbox selection is built in — so code written against Syncfusion keeps compiling.

Multiple mode without checkboxes behaves like a native list: click selects one, Ctrl-click toggles, Shift-click selects a range; ↑↓ select as they move, Ctrl moves focus only, Space toggles, Ctrl+A selects all, Ctrl+Shift+↑↓ reorders and Ctrl+←/→ moves to/from the partner when a toolbar is present. Renders Syncfusion's list-box DOM (div.e-listbox-wrapper > ul.e-list-parent > li.e-list-item[data-value], .e-selectall-parent, .e-filter-parent, .e-listboxtool-wrapper > .e-listbox-tool) with its 40px rows, so app-level .e-listbox-wrapper overrides keep applying. Not included: remote DataManager binding, groupTemplate, persistence, touch drag.

ColorPicker

import { ColorPickerComponent } from '@nalashaacontrols/nalashaa-ui';

<ColorPickerComponent value="#1565c0" mode="Palette" modeSwitcher
  change={(args) => setColour(args.currentValue.hex)} />

A split button that opens a colour palette or an HSV picker. Options: value (#rrggbb), mode ('Palette' default, 'Picker'), modeSwitcher, showButtons (default true — false commits as soon as a tile is picked), disabled, cssClass/className (the reference's e-hide-value hides the swatch). change carries { value, currentValue: { hex, rgba }, previousValue }, and the ref exposes value, showPopup() and hidePopup().

Renders Syncfusion's DOM — span.e-colorpicker-wrapper > input.e-colorpicker + div.e-split-btn-wrapper, the popup as div.e-colorpicker.e-popup > div.e-container.e-color-palette with span.e-tile[data-hex] — so existing overrides keep applying. Not included: custom presets, a working opacity channel, the beforeTileRender / open / close events and tile keyboard navigation; see .claude/plan/14-colorpicker.md.

import { ColorPicker } from '@nalashaacontrols/nalashaa-ui';
const picker = new ColorPicker(document.querySelector('#swatch'), {
  value: '#1565c0',
  onChange: (args) => save(args.value),
});

DropDownList

import { DropDownListComponent } from '@nalashaacontrols/nalashaa-ui';
<DropDownListComponent
  dataSource={[{ text: 'Male', value: 'M' }, { text: 'Female', value: 'F' }]}
  fields={{ text: 'text', value: 'value' }}
  value={gender}
  placeholder="Gender"
  allowFiltering
  change={e => setGender(e.value)}
/>

Options: dataSource, fields, value, placeholder, floatLabelType, enabled, showClearButton, width, popupHeight, popupWidth, itemTemplate, valueTemplate, allowFiltering, sortOrder, noRecordsTemplate.

ComboBox

import { ComboBoxComponent } from '@nalashaacontrols/nalashaa-ui';

<ComboBoxComponent
  dataSource={['Alabama', 'Alaska', 'Arizona']}
  placeholder="Select a state"
  allowFiltering
  allowCustom
  change={(args) => setState(args.value)}
/>

A dropdown you can type into: the list filters as you type and, with allowCustom (on by default), a value that matches nothing is kept as typed. dataSource takes a primitive array or objects addressed through fields.

Options: dataSource, fields ({ text, value }), value, placeholder, floatLabelType ('Never' | 'Auto' | 'Always'), allowFiltering, filterType ('StartsWith' default, 'Contains', 'EndsWith'), allowCustom, autofill, showClearButton, readonly, enabled, width, popupHeight, popupWidth (a length or 'auto'), itemTemplate, valueTemplate, noRecordsTemplate, cssClass/className.

Events: change ({ value, itemData, previousItemData, isInteracted, … }), select (cancellable), filtering (cancellable, carries the typed text), customValueSpecifier (rewrite args.item.value or cancel to refuse a typed value), focus, blur, open, close. The ref exposes element, value, text, itemData, showPopup(), hidePopup(), focusIn() and focusOut().

Committing follows the reference: Enter, Tab or blur turn the typed text into a value — an exact match selects that item, anything else becomes a custom value (or reverts when allowCustom is off) — and Escape restores the selected item's text.

Renders Syncfusion's DOM — span.e-input-group.e-control-wrapper.e-ddl > select.e-ddl-hidden + input.e-control.e-combobox + span.e-clear-icon + span.e-input-group-icon.e-ddl-icon, with the popup as div#<id>_popup.e-ddl.e-popup > div.e-content.e-dropdownbase > ul.e-list-parent.e-ul > li.e-list-item — so existing .e-ddl * overrides keep applying. It builds on this package's DropDownList, exactly as the reference builds on its own. Not included: remote data (DataManager), virtual scrolling, grouping, header/footer templates; see .claude/plan/13-combobox.md.

The vanilla core takes the same options with on* callback names:

import { ComboBox } from '@nalashaacontrols/nalashaa-ui';
const combo = new ComboBox(document.querySelector('#state'), {
  dataSource: ['Alabama', 'Alaska'],
  allowFiltering: true,
  onChange: (args) => save(args.value),
});
combo.showPopup();

MultiSelect

Subclasses ComboBox (as ComboBox subclasses DropDownList): the popup, filtering and keyboard table are inherited; value widens to an array, selections render as chips, and each popup item gets a checkbox. Built for the pivot table's phase 3 member filter — see .claude/plan/17-pivottable.md.

import { MultiSelectComponent } from '@nalashaacontrols/nalashaa-ui';

<MultiSelectComponent
  dataSource={['Alabama', 'Alaska', 'Arizona']}
  value={['Alaska']}
  allowFiltering
  change={(args) => save(args.value)}
/>

Only mode: 'Box' (chips) is implemented; allowCustom is off by default and unsupported if turned on (see the core's class doc — a non-matching typed value becoming a scalar would break the array invariant). Events: change, select, removing (a chip's × button), filtering, focus, blur. Methods: showPopup(), hidePopup(), focusIn(), focusOut(), setValue(values), getValue().

DateRangePicker

import { DateRangePickerComponent } from '@nalashaacontrols/nalashaa-ui';

<DateRangePickerComponent
  placeholder="Select a range"
  floatLabelType="Auto"
  startDate={start}
  endDate={end}
  format="M/d/yyyy"
  strictMode
  change={(e) => setRange([e.startDate, e.endDate])}
/>

// presets and limits
<DateRangePickerComponent
  presets={[{ label: 'Last 7 Days', start: weekAgo, end: today }, { label: 'This Month', start: first, end: last }]}
  min={yearStart} max={today} minDays={3} maxDays={30}
/>

The popup is Syncfusion's: a start/end header with the day count, two adjacent month calendars (click a title for the year and decade views), an optional preset list with a "Custom Range" item, and Apply / Cancel. Click a day for the start, a later day for the end (hovering previews the band), an earlier day to restart; Apply commits and fires change; presets apply at once; typing start - end in the input commits on change/Enter.

Options: startDate/endDate or value ([start, end] or {start, end}, controlled by content), min, max, minDays, maxDays, strictMode (out-of-range typed dates clamp to min/max, anything else reverts; without it the input gets e-error and the value becomes null), format (d dd M MM MMM MMMM yy yyyy EEE EEEE and 'quoted' literals), separator, placeholder, floatLabelType, cssClass/className, width, zIndex, enabled/disabled, readonly, allowEdit, showClearButton, firstDayOfWeek, weekNumber, presets, openOnFocus, start (initial Month/Year/Decade view), htmlAttributes, labels (header/button texts). Events: change and select (value, startDate, endDate, daySpan, text, element, event, isInteracted), open/close (cancellable), cleared, navigated, renderDayCell (set isDisabled), focus, blur, created, destroyed. The ref exposes value, startDate, endDate, element, show(), hide(), clear(), focusIn(), focusOut(), getSelectedRange(), currentView(), navigateTo().

Renders Syncfusion's DOM (`span.e-input-group.e-date-range-wrapper > input.e-daterangepicker

  • .e-range-icon; popup div.e-daterangepicker.e-popup > .e-date-range-container (.e-range-header, .e-calendar-container > .e-left-calendar/.e-right-calendar with e-start-date/e-end-date/e-range-hover cells), .e-presets, .e-footer) with its bootstrap4 metrics. Not included: locale`/CLDR formats, Islamic calendar, persistence, the mobile full-screen mode.

DatePicker, DateTimePicker, TimePicker

import { DatePickerComponent, DateTimePickerComponent, TimePickerComponent } from '@nalashaacontrols/nalashaa-ui';

<DatePickerComponent value={dob} format="M/d/yyyy" max={new Date()} change={e => setDob(e.value)} />
<DateTimePickerComponent value={appt} step={15} />
<TimePickerComponent value={time} format="hh:mm a" step={30} min={open} max={close} />

Options: value, placeholder, format, min, max, enabled, readOnly, showClearButton, strictMode, width, floatLabelType. DatePicker adds showTodayButton; TimePicker and DateTimePicker add step (minutes). Vanilla instances expose show(), hide(), clear().


WeekView

import { WeekViewComponent } from '@nalashaacontrols/nalashaa-ui';

<WeekViewComponent
  date={selectedDate}                 // any day of the week to show; Monday-first by default
  startHour="08:00" endHour="18:00" slotMinutes={15}
  headerLabel="EST"
  events={appointments}               // [{ id, start, end, title, color, textColor, tooltip }]
  fields={{ start: 'startTime', end: 'endTime', title: 'patientName' }}
  unavailable={(slotStart) => isOutsideWorkHours(slotStart)}
  blocks={[{ start: lunchStart, end: lunchEnd, description: 'Lunch', color: '#efff00' }]}
  eventTemplate={(a) => <><StatusIcon status={a.status} /> {a.patientName}</>}
  slotDoubleClick={(e) => openAdd(e.start)}
  addClick={(e) => openAdd(e.start)}
  moreClick={(e) => openSidePanel(e.events)}
  eventClick={(e) => openCard(e.event)}
  eventDoubleClick={(e) => openEdit(e.event)}
  eventContextMenu={(e) => showMenu(e.domEvent, e.event)}
  eventDrop={(e) => reschedule(e.event.id, e.start)}
/>

A simple 7-day time grid, extracted from the EHR scheduler's week view: a header row with a gutter label and one cell per day (today highlighted, per-day event count badge), and a scrollable body with a 12-hour time column and one column per day cut into slots. Events are bucketed into the slot containing their start; a slot with events shows a scrollable list of rows plus + and "view all N" buttons (red when more than maxVisibleEvents), an empty slot shows a hover +. The component only draws and reports — every interaction is a callback and the host owns the data, so a drop fires eventDrop with the new start and you pass the updated events back.

Options: date, firstDayOfWeek (default Monday), startHour/endHour ('HH:mm' or minutes), slotMinutes (15), slotHeight (88px), gutterWidth (110px), rowHeight (20px), maxVisibleEvents (5), headerLabel, events, fields, blocks, unavailable, allowDragAndDrop, showAddButton, showEventCount, eventTemplate and dayHeaderTemplate (React content through portals; the header one receives { date, count, isToday } and replaces the default name/date/badge), dayDateFormat, height, cssClass/className, labels. Events: slotClick, slotDoubleClick, slotContextMenu, addClick, moreClick, eventClick (deferred so a double-click cancels it), eventDoubleClick, eventContextMenu, eventRendered, dragStart, eventDrop, created, destroyed. The ref exposes getWeekDates(), scrollTo(minutes | Date), slotAt(x, y), refresh(), date, events and element. No toolbar, day/month views, resizing or recurring events — the host renders navigation around it.

Timeline

import { TimelineComponent } from '@nalashaacontrols/nalashaa-ui';

<TimelineComponent
  date={selectedDate}                 // the day to show
  orientation="horizontal"            // or "vertical" — providers as columns (Provider view)
  startHour="08:00" endHour="18:00" slotMinutes={15}
  headerLabel="EST"
  resources={providers}               // one row each: [{ id, text, ... }]
  resourceFields={{ id: 'id', text: 'fullName' }}
  events={appointments}               // [{ id, resourceId, start, end, title, color, textColor, tooltip }]
  fields={{ start: 'startTime', end: 'endTime', title: 'patientName', resourceId: 'providerId' }}
  unavailable={(slotStart, provider) => isOutsideWorkHours(provider, slotStart)}
  blocks={[{ resourceId: 'p2', start: lunchStart, end: lunchEnd, description: 'Lunch', color: '#efff00' }]}
  resourceTemplate={(p) => <ProviderHeader provider={p} />}
  eventTemplate={(a) => <><StatusIcon status={a.status} /> {a.patientName}</>}
  slotDoubleClick={(e) => openAdd(e.start, e.resourceId)}
  moreClick={(e) => openSidePanel(e.events)}
  eventClick={(e) => openCard(e.event)}
  eventDrop={(e) => reschedule(e.event.id, e.start, e.resourceId)}
/>

A resource-grouped day view with two layouts, set by orientation: 'horizontal' (default) puts resources on the rows and time across the columns — the EHR scheduler's Timeline view, Syncfusion's TimelineDay. 'vertical' puts resources on the columns and time down the rows — its Provider view, Syncfusion's Day view grouped by resource. Everything below holds for both; only the axis the metrics apply to changes. One scroll container holds everything: the time header stays pinned while the grid scrolls sideways, the resource column while it scrolls down (sticky, no scroll-syncing). Each appointment is drawn as a bar placed by its own start and sized by its duration, so a 1:10–1:35 PM appointment crosses the 1:30 PM column; one that only overlaps the visible hours is clipped at the edge. Overlapping appointments stack in lanes, capped by maxVisibleEvents and the row height — whatever does not fit is counted on the count button of the slot it starts in (hover-revealed, red when it overflows). Bars are placed as a share of the visible range, so they stay exact however wide the cells grow. An empty slot shows a hover +. A block paints every slot it covers for that resource, each labelled with the blocked hours ("12:00 PM – 1:00 PM", dated when the block runs into another day) so the range reads wherever you scroll; the first covered slot also carries the description and the block's colours. Dropping an event on another row reports that row, so moving an appointment to another provider is one callback. Like WeekView, the component only draws and reports — the host owns the data.

Options: date, startHour/endHour ('HH:mm' or minutes), slotMinutes (15), orientation ('horizontal'), slotWidth (200px) and rowHeight (100px) for the horizontal layout, slotHeight (60px) and timeWidth (110px) for the vertical one, resourceWidth (250px), headerHeight (40px), stretch (true — cells grow to fill a host bigger than the grid on both axes, so those sizes are minimums and four providers in a tall host use its full height; set false to pin them exactly), eventHeight (20px), maxVisibleEvents (5 — stacked lanes per row), headerLabel, resources, resourceFields, events, fields (adds resourceId), blocks (a block without resourceId covers every row), unavailable(slotStart, resource), allowDragAndDrop, showAddButton, showEventCount, eventTemplate / resourceTemplate / timeHeaderTemplate / slotTemplate (React content through portals — slotTemplate adds content to every slot cell, e.g. an availability strip), height, cssClass/className, labels. Events: slotClick, slotDoubleClick, slotContextMenu, addClick, moreClick, eventClick (deferred so a double-click cancels it), eventDoubleClick, eventContextMenu, eventRendered, dragStart, eventDrop (carries resource / resourceId alongside start), created, destroyed. The ref exposes getDate(), getSlots(), scrollTo(minutes | Date), scrollToResource(id), slotAt(x, y), refresh(), date, events, resources and element. TimelineWeek/Month, event resizing and virtual resource scrolling are not implemented — see .claude/plan/15-timeline.md.

ProviderView

import { ProviderViewComponent } from '@nalashaacontrols/nalashaa-ui';

<ProviderViewComponent
  date={selectedDate}
  startHour="07:00" endHour="19:00" slotMinutes={15}
  headerLabel="EST"
  resources={providers}               // one COLUMN each
  resourceFields={{ id: 'id', text: 'fullName' }}
  events={appointments}
  fields={{ start: 'startTime', end: 'endTime', title: 'patientName', resourceId: 'providerId' }}
  blocks={blockDays}
  unavailable={(slotStart, provider) => isOutsideWorkHours(provider, slotStart)}
  resourceTemplate={(p) => <ProviderHeader provider={p} />}
  eventTemplate={(a) => <AppointmentCard appt={a} />}
  slotTemplate={(s) => <Availability provider={s.resource} start={s.start} />}
  slotDoubleClick={(e) => openAdd(e.start, e.resourceId)}
  eventDrop={(e) => reschedule(e.event.id, e.start, e.resourceId)}
/>

The same day grid as Timeline, turned: resources are columns and time runs down the rows — a provider day view (Syncfusion's Day view grouped by resource). It subclasses Timeline, so the data shape, every callback and argument, block painting, unavailable slots, lanes, clipping and drag-and-drop are identical; overlapping appointments split the provider's column side by side instead of stacking down a row. orientation is not a prop here — that is what the component is. Metrics: resourceWidth (250px per column), slotHeight (60px per row), timeWidth (110px gutter), headerHeight (64px, for a photo and a name) — and with stretch on (the default) the provider columns and the slot rows grow to fill the host, so a two-provider day uses the whole width and a short day uses the whole height. Everything else, the ref API included, is Timeline's.

Uploader

import { UploaderComponent } from '@nalashaacontrols/nalashaa-ui';

<UploaderComponent
  autoUpload={false}
  multiple={false}
  allowedExtensions=".pdf,.docx,.jpg"
  maxFileSize={10 * 1024 * 1024}
  selected={(args) => {
    args.cancel = true;                       // keep the file, skip the built-in upload
    readFile(args.filesData[0].rawFile);
  }}
/>

File selection with drag and drop, validation, a file list and — when you give it a saveUrl — the upload itself. The component's host element is the <input type="file">, as in Syncfusion, so ref.element is the input and the browse button is ref.element.closest('.e-file-select-wrap').querySelector('button').

Options: asyncSettings (saveUrl, removeUrl, chunkSize, retryCount, retryAfterDelay), multiple (default true), autoUpload (default true — false shows the Upload / Clear bar), enabled, showFileList, allowedExtensions ('.png,.jpg', also set as the input's accept), minFileSize, maxFileSize (default 30000000), dropArea (element or selector; defaults to the uploader's own wrapper), directoryUpload, sequentialUpload (one request in flight at a time), buttons (browse/upload/clear texts), locale (any of the status texts), template (render a row yourself), cssClass/className, enableRtl, htmlAttributes.

Events, with Syncfusion's argument shapes: created, selected (cancellable; set isModified + modifiedFilesData to change what is added), fileListRendering, beforeUpload (its customFormData is appended to every request), uploading, progress, success, failure, beforeRemove, removing, clearing, change, actionComplete, canceling, pausing, resuming, chunkUploading, chunkSuccess, chunkFailure. The ref exposes element, uploadWrapper, filesData, uploaderName, getFilesData(), upload(), remove(), clearAll(), cancel(), pause(), resume() and retry().

statusCode on a FileInfo is Syncfusion's vocabulary and is not in the order you would guess: '0' failed (validation or upload), '1' ready, '2' uploaded, '3' in progress, '4' paused, '5' cancelled. A file that fails validation still appears in the list, marked e-validation-fails, and is never uploaded.

With a chunkSize the file is sent in slices carrying Syncfusion's chunk metadata, a failed slice is retried retryCount times, and pause() / resume() become meaningful:

<UploaderComponent
  asyncSettings={{ saveUrl: '/api/files', removeUrl: '/api/files/remove', chunkSize: 512 * 1024 }}
  success={(args) => refresh()}
/>

Renders Syncfusion's DOM — div.e-upload.e-control-wrapper > div.e-file-select-wrap > button + span.e-file-select + span.e-file-drop, then ul.e-upload-files > li.e-upload-file-list with e-file-container, e-file-name, e-file-type, e-file-size, e-file-status, e-upload-progress-wrap and the e-file-remove-btn / e-file-abort-btn / e-file-delete-btn / e-file-reload-btn / e-file-pause-btn / e-file-play-btn icons — so existing .e-upload * overrides keep applying. Not included: preloaded files, enablePersistence, form-submission mode, the spinner shown while a remove is in flight, and arrow-key navigation across rows; see .claude/plan/10-uploader.md.

The vanilla core takes the same options with on* callback names:

import { Uploader } from '@nalashaacontrols/nalashaa-ui';
const uploader = new Uploader(document.querySelector('#files'), {
  autoUpload: false,
  asyncSettings: { saveUrl: '/api/files' },
  onSelected: (args) => console.log(args.filesData.map((f) => f.name)),
});
uploader.upload();

Layout and overlays

Splitter

import { SplitterComponent, PanesDirective, PaneDirective } from '@nalashaacontrols/nalashaa-ui';

<SplitterComponent height="100%" separatorSize={4}>
  <PanesDirective>
    <PaneDirective size="240px" min="160px" max="420px" collapsible>
      <PatientList />
    </PaneDirective>
    <PaneDirective>
      <Chart />
    </PaneDirective>
    <PaneDirective size="30%" collapsible collapsed>
      <Inspector />
    </PaneDirective>
  </PanesDirective>
</SplitterComponent>

Resizable, collapsible split panes, horizontal or vertical, nestable. Drag a separator to resize; hover one to reveal its collapse arrows. Pane content is this element's children, or a content function returning React content — both render through a portal, so hooks and context work normally.

Options: orientation ('Horizontal' | 'Vertical'), height, width, separatorSize (default 1), paneSettings (an alternative to the directives), cssClass/className, enabled, enableRtl, enableReversePanes, enableHtmlSanitizer. Per pane: size ('240px', '30%' or a number), min, max, resizable (default true — a bar resizes only when the panes on both sides allow it), collapsible, collapsed, content, cssClass.

Events: resizeStart (set args.cancel = true to refuse the drag), resizing, resizeStop, beforeCollapse / beforeExpand (both cancellable), collapsed, expanded, created. Every collapse event reports args.index as [target, neighbour] and args.pane as the matching elements, so a handler can tell which pane moved. The ref exposes element, allPanes, allBars, collapse(index), expand(index), addPane(props, index), removePane(index) and refresh().

Sizing follows Syncfusion exactly: a pane's size becomes its flex-basis, so a percentage still reflows with the container; sized panes get e-static-pane and stop growing; panes without a size share the remainder. If every pane declares a size, the last one's is dropped so a flexible pane always absorbs the separators and any rounding.

Keyboard: a bar takes focus on click, then the axis arrow keys move the boundary by separatorSize pixels (1% for a percentage-sized pair) and Enter toggles a collapsible pane.

Renders Syncfusion's DOM — div.e-splitter.e-splitter-horizontal > div.e-pane.e-pane-horizontal.e-scrollable and div.e-split-bar.e-split-bar-horizontal > button.e-navigate-arrow + div.e-resize-handler + button.e-navigate-arrow, with e-static-pane, e-collapsed/e-pane-hidden, e-expanded, e-resizable-split-bar, e-last-bar, e-split-bar-hover and e-split-bar-active — so existing app-level .e-splitter * overrides keep applying. Not included: enablePersistence, beforeSanitizeHtml, data-* attribute configuration. Three reference defects are deliberately not reproduced (the split bar is not aria-hidden, aria-valuenow tracks the real size, and expanded does not fire when beforeExpand was cancelled); see .claude/plan/09-splitter.md.

The vanilla core takes the same options with on* callback names:

import { Splitter } from '@nalashaacontrols/nalashaa-ui';
const splitter = new Splitter(document.querySelector('#layout'), {
  paneSettings: [{ size: '240px', min: '160px', collapsible: true }, {}],
  onResizeStop: (args) => save(args.paneSize[0]),
});
splitter.collapse(0);

DashboardLayout

import { DashboardLayoutComponent, PanelsDirective, PanelDirective } from '@nalashaacontrols/nalashaa-ui';

<DashboardLayoutComponent
  columns={6}
  cellSpacing={[10, 10]}
  cellAspectRatio={100 / 60}
  allowResizing
  draggableHandle=".e-panel-header"
  change={(args) => save(args.changedPanels)}
>
  <PanelsDirective>
    <PanelDirective id="appointments" row={0} col={0} sizeX={2} sizeY={2} header={<span>Appointments</span>}>
      <AppointmentSummary />
    </PanelDirective>
    <PanelDirective id="claims" row={0} col={2} sizeX={2} sizeY={1} header={<span>Claims</span>}>
      <ClaimsWidget />
    </PanelDirective>
  </PanelsDirective>
</DashboardLayoutComponent>

A grid of panels the user can drag and resize. Panels live in cells, not pixels: a panel is sizeX columns wide and sizeY rows tall at row / col, and the cell size follows the container — cellWidth = (parentWidth - (columns - 1) × cellSpacing[0]) / columns, and cellHeight = cellWidth / cellAspectRatio. Panel content is the directive's children, a content prop (React content, markup, an element, or a function returning one) or the same through header; React content renders through a portal, so hooks and context work normally.

Options: columns (default 1), cellSpacing (default [5, 5]), cellAspectRatio (default 1), allowDragging (default true), allowResizing (default false), allowPushing (default true — panels in the way are pushed down; false refuses the move), allowFloating (default true — panels are pulled up into free rows after every change), showGridLines, draggableHandle (a selector inside the panel; put e-drag-restrict on anything that must not start a drag), resizableHandles (default ['e-south-east'], any of the eight directions), mediaQuery (default 'max-width: 600px', below which the layout stacks into one column and interaction is off), panels (an alternative to the directives), cssClass/className, enableRtl, enableHtmlSanitizer.

Per panel: id (assigned layout_<n> when omitted), row, col, sizeX, sizeY, minSizeX, minSizeY, maxSizeX, maxSizeY, zIndex, enabled, cssClass, header, content.

Events: change ({ changedPanels, isInteracted, addedPanels, removedPanels } — also fires for API calls, with isInteracted false), dragStart (set args.cancel = true to refuse), drag, dragStop (args.panels is everything that moved), resizeStart, resize, resizeStop, created, destroyed. The ref exposes element, panels, getCellInstance(id), addPanel(), updatePanel(), removePanel(id), removeAll(), movePanel(id, row, col), resizePanel(id, sizeX, sizeY), serialize() and refresh() — serialize() returns the id, position and sizes of every panel, ready to persist.

Renders Syncfusion's DOM — div.e-dashboardlayout[role=list] > div.e-panel[role=listitem] > div.e-panel-container > div.e-panel-header + div.e-panel-content, with data-row / data-col / data-sizex / data-sizey on each panel, span.e-resize.e-dl-icon handles, div.e-holder while dragging and table.e-dashboard-gridline-table for the grid lines — so existing .e-dashboardlayout * overrides keep applying. Not included: panels declared as inline markup, the reference's sideways-adjustment and swap heuristics (a collided panel is always pushed down), enablePersistence; see .claude/plan/11-dashboardlayout.md.

The vanilla core takes the same options with on* callback names:

import { DashboardLayout } from '@nalashaacontrols/nalashaa-ui';
const dashboard = new DashboardLayout(document.querySelector('#dashboard'), {
  columns: 6,
  cellSpacing: [10, 10],
  allowResizing: true,
  panels: [{ id: 'one', sizeX: 2, sizeY: 2, header: '<div>Appointments</div>', content: '<div>128 today</div>' }],
  onChange: (args) => localStorage.setItem('dashboard', JSON.stringify(dashboard.serialize())),
});
dashboard.addPanel({ sizeX: 2, sizeY: 1, content: '<div>new</div>' });

Menu

import { MenuComponent } from '@nalashaacontrols/nalashaa-ui';

const items = [
  { text: 'File', iconCss: 'fa fa-file', items: [
    { text: 'New' }, { text: 'Open' }, { separator: true }, { text: 'Exit', id: 'exit' },
  ] },
  { text: 'Edit', items: [{ text: 'Undo' }, { text: 'Clipboard', items: [{ text: 'Cut' }, { text: 'Copy' }] }] },
  { text: 'Docs', url: '/docs' },
];

<MenuComponent items={items} select={(args) => run(args.item.text)} />

A menu bar with nested pop-up submenus, horizontal or vertical. Items are data, not children: a parent carries items, a separator draws a divider, a url renders an anchor, and flat data with parentId is nested for you. Root submenus open on hover unless showItemOnClick is set; a submenu inside an open popup always follows the pointer.

Options: items, fields (rename any key: itemId, text, parentId, iconCss, url, separator, children), orientation ('Horizontal' | 'Vertical'), showItemOnClick, hoverDelay, cssClass/className (added to the wrapper and to every popup), enableRtl, enableHtmlSanitizer, animationSettings (effect 'SlideDown' | 'FadeIn' | 'ZoomIn' | 'None', duration, easing).

Events: select ({ element, item, event }), beforeItemRender, beforeOpen — cancellable, and setting args.top / args.left places the popup yourself — onOpen, beforeClose (cancellable), onClose, created. The ref exposes element, items, getItem(id), close(), enableItems(texts, enable, isUniqueId), showItems(), hideItems(), removeItems(), insertAfter(items, text) and insertBefore(items, text) — pass isUniqueId: true to address items by id instead of text.

Colours: the bar reads in the body text colour (#212529) with a grey hover, not the bootstrap4 theme's link blue; every box metric still matches the reference exactly. The colour layer is zero-specificity (:where()), so a host stylesheet — including an app that still loads Syncfusion's menu CSS — overrides it.

Keyboard: the arrows walk the bar and the popups (ArrowDown on the bar, ArrowRight in a popup opens and focuses the first item), Home/End jump, Enter selects and Escape steps back out; separators, hidden and disabled items are skipped.

Renders Syncfusion's DOM — div.e-menu-wrapper > ul.e-menu.e-menu-parent[role=menubar] > li.e-menu-item with span.e-menu-icon, span.e-icons.e-caret, e-blankicon, e-separator, e-navigable + a.e-menu-url > div.e-anchor-wrap, and each popup as div.e-menu-popup#<liId>-ej2menu-<menuId>-popup > ul.e-menu-parent.e-ul[role=menu] on the body — so existing .e-menu-wrapper * overrides keep applying. Not included: hamburgerMode / title / target, enableScrolling's navigation arrows, item templates; see .claude/plan/12-menu.md.

The vanilla core takes the same options with on* callback names:

import { Menu } from '@nalashaacontrols/nalashaa-ui';
const menu = new Menu(document.querySelector('#menu'), {
  items,
  showItemOnClick: true,
  onSelect: (args) => run(args.item.text),
});
menu.enableItems(['Exit'], false);

ContextMenu

Subclasses Menu: a hidden root list that opens at the cursor on a native contextmenu event inside target, and closes on an outside click or Escape. Built for the pivot table's phase 3 right-click menu — see .claude/plan/17-pivottable.md.

import { ContextMenuComponent } from '@nalashaacontrols/nalashaa-ui';

<ContextMenuComponent
  target=".e-grid"
  items={[{ text: 'Expand' }, { text: 'Collapse' }, { separator: true }, { text: 'Remove', id: 'remove' }]}
  select={(args) => run(args.item.text)}
/>

filter (a (target) => boolean prop) can veto opening for a specific right-clicked element. The ref exposes open(x, y, target?, items?) — passing items swaps the menu's contents before it opens, for a right-click menu that differs by what was clicked (e.g. a pivot cell's row header vs. its value cells) — plus close() and isOpen.

Dialog

import { DialogComponent } from '@nalashaacontrols/nalashaa-ui';
<DialogComponent
  header="Discharge patient"
  visible={open}
  isModal
  showCloseIcon
  width="480px"
  closeOnEscape
  buttons={[
    { buttonModel: { content: 'Cancel' }, click: () => setOpen(false) },
    { buttonModel: { content: 'Confirm', isPrimary: true }, click: confirm },
  ]}
  close={() => setOpen(false)}
>
  <p>Are you sure?</p>
</DialogComponent>

Options: header, content, visible, isModal, showCloseIcon, width, height, minHeight, cssClass, zIndex, target, position ({ X, Y }), buttons, allowDragging, closeOnEscape, animationSettings, enableResize.

Tooltip

import { TooltipComponent } from '@nalashaacontrols/nalashaa-ui';
<TooltipComponent content="Last updated 2 min ago" position="TopCenter" opensOn="Hover">
  <span>Vitals</span>
</TooltipComponent>

Options: content, target (selector for delegated tooltips), position, opensOn ('Auto' | 'Hover' | 'Click' | 'Focus' | 'Custom'), isSticky, showTipPointer, openDelay, closeDelay, offsetX, offsetY, mouseTrail, windowCollision, container, width, height, cssClass.

Accordion

import { AccordionComponent, AccordionItemsDirective, AccordionItemDirective } from '@nalashaacontrols/nalashaa-ui';

<AccordionComponent expandMode="Single">
  <AccordionItemsDirective>
    <AccordionItemDirective header="Allergies" content={() => <AllergyList />} expanded />
    <AccordionItemDirective header="Medications" content={() => <MedList />} />
  </AccordionItemsDirective>
</AccordionComponent>

Options: items, expandMode ('Single' | 'Multiple'), cssClass, width, height, events expanding, expanded, clicked.


Theming

All components use the e- class prefix (e-control, e-btn, e-grid, e-input-group …). Existing Syncfusion overrides in your app keep applying. Pass `cssCl