Kit catalog¶
24 components, grouped by how they hold state. The gallery (/ui-kit in the demo umbrella) shows every one live, and the browser e2e drives them all.
Pure slots — presentation only¶
No state; the parent projects content.
| Component | Notes |
|---|---|
<Card> |
title / actions slots + default body |
<Field> |
label / help / error around a control — also the substrate declui generates onto |
<PageHeader> |
title + actions slot |
<Stat> |
a KPI tile |
<EmptyState> |
icon + message + action |
<Avatar> |
initials or image; sizes via forwarded class |
<Breadcrumb> |
links with CSS separators |
<Tooltip> |
CSS hover tooltip |
Attribute forwarding applies to every component: class on the host merges into the child's root; style/id/title/role/tabindex/aria-*/data-* are forwarded. A mistyped prop (anything else) fails closed.
Controlled — the parent owns the flag¶
Externally-triggered overlays: the opener lives elsewhere on the page, so the parent holds the Local and the wiring (:open prop + @close emit).
| Component | Contract |
|---|---|
<Dialog> |
:open + @close; footer content via slots |
<Drawer> |
side panel; same contract |
<Toast> |
transient notice; pairs well with :autohide |
<button @click="confirm_open = True" class="ui-btn ui-btn--primary">Delete…</button>
<Dialog :open="confirm_open" @close="close_confirm">
<template slot="title">Delete 3 issues?</template>
<button class="ui-btn ui-btn--danger" @click="do_delete()">Delete</button>
</Dialog>
Uncontrolled — the component owns its state¶
Self-triggered widgets: drop them in with zero parent wiring; each placement is monomorphized into independent state.
| Component | Notes |
|---|---|
<Collapsible> |
an accordion row; N placements, N independent open flags |
<Popover> |
trigger + panel, outside-click close |
<Switch> |
on/off toggle |
<TagInput> |
add/remove chips over a Local[list] (the client :each reconciler) |
<Sortable> |
pointer drag-reorder via :sortable |
Widget primitives — the optional runtime family¶
Complex widgets whose presentation state (open option, sort column, page) nothing else reads. They ship as a runtime in widgets.js; your data and selection stay in Locals the app reads. Each is a deliberate, labelled runtime stand-in. Use the component — this table is the canonical reference; the :binder column is the kit's internal wiring, not something you author directly.
| Component (the API) | Binder (internal) | State the app owns |
|---|---|---|
<Tabs> |
:tabs |
— (bar generated from data-tab panels) |
<DataTable> |
:table="rows" |
a Local[list]; header-click sorting |
<DataGrid> |
:datagrid="rows" + :datagridsel="sel" |
rows Local[list] + selection Local[set] (survives sort/filter/page; tri-state select-all; aria-sort headers) |
<Combobox> |
:combobox |
Local[str] — searchable single-select, full keyboard + ARIA listbox |
<ComboboxMulti> |
:combobox (multi) |
Local[set] — chips, Backspace removes last |
<Menu> |
:menu |
— (roving focus, Escape-to-trigger; each item keeps its compiled @click) |
<CommandPalette> |
:palette |
— (Ctrl/⌘-K overlay, fuzzy filter, activates the item's compiled @click) |
DataGrid columns¶
<DataGrid> (and <DataTable>) read their columns from the <th data-field> cells in the head slot; each <th> carries optional data-* options. Hand-author them, or let declui's screens=("table",) generate them from a typed model.
| Attribute | Effect |
|---|---|
data-field="key" |
required — the row-object key this column reads |
data-sort="off" |
make the column unsortable (sortable by default; shift-click a header for a secondary key) |
data-sort-type="numeric" \| "date" |
sort on the raw value numerically / chronologically (default: natural text) |
data-format="money" \| "date" \| "datetime" \| "num" \| "percent" \| "bool" |
a display formatter (closed registry; an unknown key throws) — the raw value stays for sort/filter |
data-filter="text" |
render a per-column filter input in a filter row (ANDs with the global box) |
data-align="right" \| "center" |
cell alignment |
data-hidden |
start the column hidden (the toolbar's Columns picker can restore it) |
data-pin |
freeze the column to the left under horizontal scroll (also toggleable from the Columns menu) |
<DataGrid>'s toolbar adds a global filter, a Columns menu (visibility + pin toggles), and a page-size selector + a "Showing X-Y of Z" line. On the headers: drag the right edge to resize, drag the left grip to reorder, click to sort. Data and selection stay in the app's Locals; cells are textContent-only (never HTML). <DataTable> is the minimal sortable subset (no toolbar).
The column contract fails closed — at the lint gate
situ check validates every <th> in your tree, so a column mistake fails the gate rather than the page. It catches the three the runtime cannot see, because a <th> the engine never reads is a <th> it cannot reject:
- a knob on a
<th>with nodata-field— the engine selectsthead th[data-field], so the column and every knob on it is silently dropped; - a typo'd attribute name (
data-formatt="num") — an unknowndata-*is simply never read; - a value on a presence-only knob:
data-hiddenanddata-pinare tested withhasAttribute, sodata-pin="false"pins the column. Write the bare attribute or omit it.
data-sort is likewise closed to "off" — the engine tests !== "off", so data-sort="of" would silently mean sortable. Unknown values on any knob are rejected by both the lint check (when literal) and the runtime (always). Generating columns from a typed model? declui's table screen emits this contract from Field(...), checked against the same closed sets by the type checker.
Native-wrapped¶
| Component | Notes |
|---|---|
<Select> |
a real <select class="ui-select"> with :bind forwarding — keyboard and screen-reader behavior for free, zero runtime. The accessible default for a fixed option set; <Combobox> is the searchable upgrade. |
Class-contract variants (no component needed)¶
Buttons (ui-btn, --primary/--danger/--ghost, sizes, loading), inputs (ui-input, invalid state), ui-badge--*, ui-alert--*, tables + pagination, ui-progress, ui-skeleton, ui-kbd. Write the class; there is nothing to instantiate.
Slots & props — at a glance¶
The exact contract of each component, to place it without opening the source. + body = the default (unnamed) slot; binders and the state-mode rationale are in the family sections above. Every component also forwards class + the passthrough attributes.
| Component | Props / emits | Named slots (+ body) |
|---|---|---|
<Avatar> |
— | + body (initials) |
<Breadcrumb> |
— | + body (links) |
<Card> |
— | title, actions, + body |
<PageHeader> |
— | title, description, actions |
<Stat> |
— | label, delta, + body (value) |
<EmptyState> |
— | media, title, description, action |
<Field> |
— | label, help, + body (the control) |
<Tooltip> |
— | tip, + body (trigger) |
<Dialog> |
open: Prop[bool], close: Emit[None] |
title, footer, + body |
<Drawer> |
open: Prop[bool], close: Emit[None] |
title, + body |
<Toast> |
open: Prop[bool], close: Emit[None] |
+ body |
<Collapsible> |
— (owns its open flag) | title, + body |
<Popover> |
— (owns its open flag) | trigger, + body (panel) |
<Menu> |
— | trigger, + body (items) |
<Switch> |
— (owns on: Local[bool]) |
+ body (label) |
<TagInput> |
— (owns tags: Local[list]) |
— |
<Sortable> |
— (owns items: Local[list]; v1) |
— |
<Tabs> |
— (runtime-only; panels via data-tab) |
+ body (panels) |
<DataTable> |
rows: Prop[list] |
head |
<DataGrid> |
rows: Prop[list], selection: Prop[set] |
head |
<Combobox> |
value: Prop[str] |
+ body (options) |
<ComboboxMulti> |
value: Prop[set] |
+ body (options) |
<CommandPalette> |
— | + body (items) |
<Select> |
value: Prop[str] |
+ body (<option>s) |