@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
Maintainers
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-uiReact 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@>=17Entry 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 gridQuick 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 setexpandAll: false(collapsed by default) anddrilledMembers(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 = trueto block it). - Value sort:
enableValueSorting+valueSortSettings(rowSortOrder/columnSortOrder, optionalheaderText/headerDelimiterto 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 = trueto block it),resizeStop. - Hyperlink cells:
hyperlinkSettings.memberTypeis'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 DOMNode(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, defaulttrue): aFieldsstrip 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 intoRows,ColumnsorValuesto add it (dropping inValuesdefaults it toSum); drag betweenRowsandColumnsto move it; a chip's×removes it.Valueschips are removable but not draggable (re-grouping a measure into a dimension is out of this phase's scope). Event:onFieldDropped(args.cancel = trueto block a move),fieldRemove. - Member filter: click a field chip's ▼ icon (in the grouping bar or the
Filtersreadout) to open a checklist of that field's distinct values, backed byMultiSelectand pre-checked to what's currently included;Applynarrows the pivot,Canceldiscards. Set directly viadataSourceSettings.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 anAggregatesubmenu of the 12 base aggregate types (hover — like any menu, it opens on hover, not click — then pick one). Event:contextMenuClick.Drillthrough/CalculatedFieldcontext 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'} /> {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
; popupdiv.e-daterangepicker.e-popup > .e-date-range-container (.e-range-header, .e-calendar-container > .e-left-calendar/.e-right-calendar withe-start-date/e-end-date/e-range-hovercells), .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
