@lotics/ui 11.4.0 → 11.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,920 @@
1
+ # @lotics/ui — component catalog
2
+
3
+ The complete inventory of the kit, in two passes: **Reach by role** maps each data role /
4
+ job (actions, members, selects, dates, files, money, status, …) to the ONE canonical
5
+ component, and the **Full import inventory** lists every `@lotics/ui/<module>` entry point
6
+ with its purpose. Read this before building any screen — reuse first; a component absent
7
+ from this catalog does not exist, and one present here must never be hand-rolled. The
8
+ composition patterns (data entry, AI review, templates) live in the sibling area docs
9
+ indexed in [AGENTS.md](../AGENTS.md).
10
+
11
+ ## How to read a component's API
12
+
13
+ - **Compose with React Native primitives**: `View` / `ScrollView` from `react-native` (never
14
+ `div`/`span`), style with RN style objects (not CSS), and render every string through the `Text`
15
+ primitive — the kit renders on web **and** native.
16
+ - **Import per module**: `import { Combobox, ComboboxInput, ComboboxContent } from "@lotics/ui/combobox"`.
17
+ There is no barrel — every module below is its own entry point.
18
+ - **The exact API is the source, which ships with the package**: read
19
+ `node_modules/@lotics/ui/src/<module>.tsx`. **Never guess a prop — open the file.**
20
+ - **If the closest component lacks a capability, extend it** (a platform change every app
21
+ inherits), never inline a one-off `View`/`Text` rebuild — that forfeits the typeahead,
22
+ async search, virtualization, and a11y the primitive already ships.
23
+ - Floating content (`Dialog` · `Popover` · `Tooltip` · `Alert` · `OptionList`) needs a
24
+ `PortalHost` (`@lotics/ui/portal`) at the app root.
25
+ - Worked examples referenced below (`tpl_record`, `tpl_item_list`, …) ship in
26
+ [`../examples/`](../examples/).
27
+
28
+ ---
29
+
30
+ ## Reach by role — what exists
31
+
32
+ Pick by capability, not by name. (→ the source file for the API.)
33
+
34
+ ### Actions
35
+
36
+ `Button` (labelled; `title` is MANDATORY — the type blocks a title-less Button; `color` =
37
+ emphasis/risk), `IconButton` (icon-ONLY — the circular affordance; an icon-only Button is a
38
+ type error, so reach here. `size` `lg` 40px / `md` 28px / `sm` 24px — md/sm keep a 40px touch
39
+ target via hitSlop — the SAME `color` palette as Button, plus `loading` and `elevated` (white
40
+ fill + border + shadow, for a button sitting ON imagery — a tile's remove ✕, an overlay's
41
+ retry); needs `accessibilityLabel`/`tooltip`), `TextLink` (underlined text that's OPTIONALLY
42
+ an action (`onPress`) or a link (`href` — a real web anchor) — or, with neither, plain
43
+ underlined text you drop in your own pressable like a table cell; colour via `color`; the
44
+ go-to for Clear / Select all / inline links), `Chip` (dismissible facet chip). A button is
45
+ never a raw `Pressable`. For a link OUT (a URL / record / document) use `Link` — fixed
46
+ underline+blue + `role="link"`, the destination signal (`onPress` only — the consumer wires
47
+ the opener, e.g. the app SDK's `openExternal`); `TextLink` is the neutral,
48
+ colour-configurable underlined link/action.
49
+
50
+ ### Pick from a list
51
+
52
+ `Picker` (native `<select>`, plain label-only single, native typeahead) or `Select` (the
53
+ rich one — custom-rendered options, single/multi, select-all). Search-as-you-type / async /
54
+ create-new → `Combobox`. A selectable card row → `CardSelectItem`. For a SELECT-FIELD
55
+ picker, render each option as its colored chip on `Select`
56
+ (`renderOptionContent={(o) => <OptionBadge value={o} />}`). `Select` opens the shared
57
+ `OptionList` body; `Combobox` is COMPOUND (`ComboboxInput` + `ComboboxContent`) over the
58
+ same option-list engine; `Picker` is the only one that's a native dropdown.
59
+
60
+ ### Pick member(s)
61
+
62
+ `MemberSelect` (a `Select` that renders each option as a `MemberChip`, single or multi) —
63
+ the ready member picker; pass it the roster (`members={useMembers().members}`). Don't
64
+ re-wire `Select` + `renderOptionContent` + a directory by hand. For an assignee FILTER that
65
+ also matches unassigned records, pass `unassignedLabel` → it prepends a muted "unassigned"
66
+ option (value `MEMBER_UNASSIGNED`), so you never hand-roll a mixed member+none `Select`. To
67
+ edit a `select_member` field IN PLACE (chip at rest → picker on click) use its inline-edit
68
+ twin `InlineMemberSelect`.
69
+
70
+ ### A person / member (display)
71
+
72
+ `MemberChip` (avatar + name + optional secondary line) — the ONE way to show a member
73
+ inline: a picker option, an assignee, a `select_member` value. Pure: resolve the member from
74
+ your directory and pass `name` / `image`; never hand-roll `Avatar` + `Text`. (`MemberSelect`
75
+ renders these per option.)
76
+
77
+ ### A select-field value
78
+
79
+ `OptionBadge` (a stored `select` value as its CONFIGURED colored badge) — never hand-map
80
+ option-key → color. Feed it a resolved option; multi-select wraps to one badge each; a
81
+ missing/unknown color token degrades to neutral.
82
+
83
+ ### Tags / multi-value chip box
84
+
85
+ `Select multi` whose `renderSelected` returns a removable `<Chip onDismiss={remove}>` (with
86
+ `searchable` + `allowCustom`). The chip box is COMPOSED, not a separate control — there's no
87
+ `display` mode; `renderSelected` is the seam (see the data-entry patterns doc indexed in
88
+ [AGENTS.md](../AGENTS.md)).
89
+
90
+ ### Text & form
91
+
92
+ `TextInputField`, `NumberInput`, `SearchInput`; wrap with `FormField`; `Checkbox`, `Switch`,
93
+ `RadioPicker`; dates via `DatePicker` / `DateRangeFilterField`, times via `TimePicker`.
94
+ Batch draft-form state → `useForm`.
95
+
96
+ ### Edit a record's fields in place
97
+
98
+ The `Inline*` family: `InlineTextInput` · `InlineNumberInput` · `InlineSelect` ·
99
+ `InlineMemberSelect` · `InlineTagSelect` · `InlineDatePicker` · `InlineTimePicker`; a
100
+ READ-ONLY field in that same column uses `InlineStatic` (matches the editor box exactly, no
101
+ input chrome, so it aligns pixel-for-pixel). A stack of labelled field rows lives in
102
+ `DetailTable` + `DetailRow`; the record's identity band is `RecordSummary`; its money
103
+ summary is `Ledger`.
104
+
105
+ ### Tasks / to-dos — pick by ALTITUDE
106
+
107
+ A task-management PAGE (many tasks, grouping, filters, expandable rows) is COMPOSITION —
108
+ there is NO Task component (a Reminders row and a Linear column-grid share almost nothing):
109
+ `CheckCircle` + a struck `InlineTextInput` + your meta cells; see
110
+ [`tpl_tasks`](../examples/tpl_tasks.tsx) (quick list) and
111
+ [`tpl_task_board`](../examples/tpl_task_board.tsx) (columns). A record's own small CHECKLIST
112
+ (the 5–8 tasks living in a drawer/section) is the `Checklist`/`ChecklistRow` compound +
113
+ `SuggestionChip` commons + `CaptureRow` — see [`tpl_record`](../examples/tpl_record.tsx) /
114
+ [`tpl_item_list`](../examples/tpl_item_list.tsx).
115
+
116
+ ### Tabular data — pick by SCALE + intent
117
+
118
+ Two columnar shapes, and the choice is about data size:
119
+
120
+ - **High-volume register** (thousands+ you BROWSE) — `Table` (columns defined once; sortable
121
+ headers via `SortHeader`; paired with `Pagination`) + read-only rows that open a `Drawer`
122
+ to edit. It scales by PAGING — renders one page, never the whole set — so you FILTER +
123
+ search, you don't group. Worked example: [`tpl_item_list`](../examples/tpl_item_list.tsx).
124
+ Never an HTML `<table>` or a `.map` of rows. The register adapts to its container on its
125
+ own — hides columns by `priority`, stacks rows below the two-column floor (see the `table`
126
+ inventory entry) — so a narrow screen or panel needs NO branching; compose a hand-designed
127
+ card pile (`PressableRow` + your own hierarchy) over `useScreenSize` only when a screen
128
+ deserves a better mobile shape than the automatic stack.
129
+ - **Inline-managed grouped table** (MODERATE — hundreds, low-thousands — you MANAGE in view)
130
+ — the `DataGrid` primitive: a grouped, sortable grid whose cells are LIVE inline editors
131
+ (ANY field — a column is `{ key, label, width?, sortable?, cell: (item) => ReactNode }`,
132
+ so `InlineMemberSelect` / `InlineDatePicker` / `InlineNumberInput` / `InlineSelect` / a
133
+ borderless `Select multi` for tags / colour-dot `OptionBadge`). `DataGrid` owns the
134
+ sortable header + collapsible grouped sections + aligned rows + an optional per-row
135
+ `leading` slot (a `CheckCircle`); YOU own the data, the sort/group/filter/collapse STATE
136
+ (`cycleSort` + `sortBy` from `@lotics/ui/sort_header`, a `collapsed` Set), the toolbar
137
+ (`SearchInput` + `FilterChip`s), and the per-group add row (`renderGroupFooter`, aligned
138
+ with the exported `gridRowStyle` + the column widths). It renders ALL rows (no
139
+ virtualization), so it's only for sets small enough to hold in view — at 10k+ it lags, and
140
+ grouping + pagination/infinite don't compose; use the register instead. Worked example:
141
+ [`tpl_task_board`](../examples/tpl_task_board.tsx) (a `CheckCircle` leading).
142
+
143
+ ### Numbers & charts
144
+
145
+ `KPIStrip` (the dashboard stat band) · `SummaryLine` (the light inline register/list summary
146
+ — below the toolbar, from the filtered rows) · `KPICard` / `Metric` (headline figures),
147
+ `TrendChip` (delta), `Sparkline`, `BarChart` / `LineChart` / `PieChart` (the canonical SVG
148
+ set — no recharts), `RingGauge`, `ProgressBar` (its `compact` prop = ONE row, track + a
149
+ plain sm tabular count beside it — the cell/heading/peek-trigger meter; a caption floating
150
+ above a tiny bar reads misaligned) / `StackedProgressBar` / `StepProgress`, `Breakdown` (a
151
+ stacked bar + ranked share rows, pressable to drill; `maxRows` folds the long tail behind a
152
+ "Show N more" toggle — `labels` to localize — so several facet cards align to one height in
153
+ a row), `Funnel` (a CONVERSION funnel — ordered stages as bars that NARROW; the step
154
+ conversion rate is the HEADLINE (a bold aligned row across the top, the first stage = the
155
+ 100% baseline), the count is the supporting figure below — the Amplitude/Mixpanel
156
+ convention, never pin the rate to the fill height. `orientation` vertical columns |
157
+ horizontal bars; pass `onSelect`+`selectedKey` to make the bars press-to-drill (the selected
158
+ stays solid, others dim — the caller renders the records). The subset/drop-off sibling of
159
+ `StackedProgressBar` — nested cohorts that shrink "calls → connected → won", NOT a whole
160
+ split across stages — that's `StackedProgressBar`), `StatusGrid` + `StatusLegend`, `Heatmap`
161
+ (density: colour-only, "where does it cluster"), `Matrix` (the PIVOT cross-tab: the NUMBER
162
+ in each cell — optionally a heat wash behind it — plus row/column/grand totals; press a cell
163
+ to drill).
164
+
165
+ ### Surfaces & layout
166
+
167
+ `Card` (+ `CardHeader`/`CardHeaderTitle`/`CardHeaderMeta`/`CardBody`/`CardFooter`),
168
+ `Section` (from `@lotics/ui/section_heading` — the card-less twin of `Card`: bare
169
+ gap-spaced region + `SectionHeading`/`SectionHeadingTitle`/`SectionHeadingMeta`; no body
170
+ component — children are the body), `Subsection` (+ `SubsectionHeading`/
171
+ `SubsectionHeadingTitle` — the named group INSIDE a section, `###` lg-semibold title),
172
+ `SectionStack` (the flat page's content column — owns the fixed 56px beat + hairline between
173
+ top-level blocks), `SubsectionStack` (the same law one step tighter — fixed 24px beat +
174
+ hairline between a section's `Subsection` groups), `SectionCard`, `PageHeader` /
175
+ `PageContent`, `Stack`, `Spacer`, `Divider`, `Accordion`, `Tabs`, `SegmentedControl`,
176
+ `Stepper`, `DangerZone` (the destructive section — delete/archive — set apart at the bottom
177
+ of a record/settings surface).
178
+
179
+ ### Rows & registers
180
+
181
+ `PressableRow` (THE register row; forwards its `ref`, so wrapping it in a `PopoverTrigger`
182
+ anchors a row-triggered peek Popover — without the ref the trigger is unmeasurable and the
183
+ popover renders off-screen), `ListItem`, `MenuButton`, `MenuListItem`, `DetailRow`
184
+ (label+value for drawers/peeks; optional `trailing` slot for a right-side
185
+ action/badge/unit), `ActionMenu` (⋯), `FloatingActionBar` (bulk-select bar).
186
+
187
+ ### Filters & view controls
188
+
189
+ `SearchInput`, `ChipGroup`, `FilterChip` (+ `RangeSlider`, `Counter`), `Chip`,
190
+ `ColumnFilter` (the typed per-column filter pill + `columnFilterToConditions`).
191
+
192
+ ### Overlays
193
+
194
+ `Dialog` (centered card over a scrim), `Modal` (+ `ModalHeader`/`ModalBody`/`ModalFooter` —
195
+ a full-bleed, edge-to-edge takeover with NO scrim), `Drawer` (+ `DrawerFooter`), `Popover`,
196
+ `Tooltip`, `OptionList` (the searchable list body — host it in a `Popover`/`Dialog` for a
197
+ command palette), `Alert` (the blocking confirm), `Peek` (drill-down popover on an inline
198
+ reference), `InfoPopover` (the ⓘ explainer).
199
+
200
+ ### Status / feedback
201
+
202
+ `Badge` / `StatusBadge`, `Callout` (inline status), `EmptyState`, `CompletionState`,
203
+ `ActivityIndicator` / `Loading`, `Skeleton`.
204
+
205
+ ### Files
206
+
207
+ `FilesEditor` (THE all-in-one attachment field: an upload-aware grid + a toolbar below it
208
+ that swaps into a batch SELECT mode, full-screen preview, download/share, and
209
+ Alert-confirmed remove — the host only owns `files` + wires `onAdd`/`onRemove`. Reach for
210
+ this first for "manage a record's attachments"), `FileDropzone`, `FileRows`
211
+ (batteries-included file LIST: tap a row → built-in full-screen gallery, with a per-row
212
+ trailing ⋯ menu = Download · Open-external · Remove; the default "here are some files"
213
+ surface), `FileGrid` (the upload-aware grid: completed files + a live upload queue in one
214
+ surface — `FilesEditor` is this + the toolbar; reach for `FileGrid` bare when you own the
215
+ chrome), `FileThumbnail` / `FileThumbnailGrid` (square tiles, display-only),
216
+ `UploadingThumbnail` (the single in-flight tile FileGrid renders — reach for it only when
217
+ hand-rolling a non-grid upload layout), `FileRow` (a horizontal file/document LINE —
218
+ badge-or-placeholder + name + meta + a composable `trailing` slot for a status badge /
219
+ action / remove; `onPress` makes the whole row a pressable door, `trailing` stays an
220
+ independently-pressable sibling; for checklists & readable lists), `FileBadge` (the two-tone
221
+ type mark), `FilePreview` / `FileGalleryModal`, `ImageGallery`; picking is `pickFiles`
222
+ (`@lotics/ui/file_picker` — opens the browser picker and resolves the chosen `File[]`, the
223
+ imperative half behind every Add-file CTA); for gated CRUD compose locally with
224
+ `useSelectionMode` + `shareOrDownloadFiles` + `rotateImageToBlob` (see the data-entry
225
+ patterns doc indexed in [AGENTS.md](../AGENTS.md)).
226
+
227
+ ### Specialized work surfaces
228
+
229
+ `ScanField` (scan/verify), `Stepper` (a guided run / progress sequence — done · current ·
230
+ upcoming, horizontal OR vertical), `RemainderMeter` + `AllocationRow` (allocation),
231
+ `Timeline` (a heterogeneous event LOG — icons + expandable details, not progress),
232
+ `Calendar` (the `calendar` module's views), `Gantt`, `comments_thread`.
233
+
234
+ ### AI surfaces
235
+
236
+ `Composer` (the adaptive command/chat composer — a compact pill when empty that expands for
237
+ long text + attachments; the surface that triggers agent work), `AgentRun` (the live
238
+ streaming work feed) + `AgentProgress` (its compact, floating, expandable form — a
239
+ composer's "working" state) + `Confidence`; **`ChangeReview` — THE one review-before-apply
240
+ surface, a COMPOUND family** (frame: `ChangeReview` · `ChangeReviewHeader` ·
241
+ `ChangeReviewActions`; sections: `Change` · `ChangeLabel` · `ChangeSummary` ·
242
+ `ChangeReasoning`; the grammar: `ChangeFields` + `ChangeField` · `ChangeRecord` ·
243
+ `ChangeBand` + `ChangeValueInput`): adds, updates, removals, conflicts, whole records,
244
+ display-only findings are all compositions — see the AI-patterns doc indexed in
245
+ [AGENTS.md](../AGENTS.md) for the laws; `Clarify` (the agent asks back — selectable
246
+ `ChoiceList` options), `Sources` (provenance chips for AI output — at review scale,
247
+ `label={null}` slots the chips at a section's bottom), `Finding` (one ranked insight from an
248
+ AI check — localized severity word · title · detail · `Sources` chips · a `children` slot;
249
+ **`FindingComparison`** is the expected-vs-actual body: each disagreeing side a labeled row,
250
+ the DELTA emphasized under a hairline (localized "Difference") — quantities, totals, dates;
251
+ a plain `metric` prop remains for one-number findings. The children slot composes ANY visual
252
+ result — a compact `Table` for per-line detail (danger color on the offending cells),
253
+ `ProgressBar` for consumption-toward-a-cliff (free time, credit), dot `Badge`s for a
254
+ present/missing checklist, `Confidence` for judgment calls. Display-only — it informs the
255
+ verdict the host records; `finding` locale slice).
256
+
257
+ ---
258
+
259
+ ## Full import inventory
260
+
261
+ Every published entry point, grouped by area. Import as `@lotics/ui/<module>`; the module's
262
+ source (`src/<module>.tsx`/`.ts`) is the API reference.
263
+
264
+ ### Text & formatting
265
+
266
+ - **`text`** — the `Text` primitive (never raw `div`/`span`/`fontSize`): `size` `xs`–`xxxl`
267
+ (the heading ramp: `xl` = `##` section title, `xxl` = `#` page/record title, `xxxl` = hero
268
+ numbers), `weight`, `color`, alignment, tabular numerals.
269
+ - **`markdown`** — `Markdown`: the single canonical markdown renderer for chat, apps, and
270
+ `AgentRun`; rich on web via react-markdown/remark-gfm with copyable tables, plain-text on
271
+ native; takes a markdown `children` string.
272
+ - **`markdown.css`** — import once for the web markdown styling.
273
+ - **`format_date`** — `formatDate` / `parseDate` / `toISODate` + `DateFormatStyle`.
274
+ - **`format_money`** — `formatMoney` / `formatCompactNumber`.
275
+ - **`text_utils`** — text/typography plumbing: `getTextColor` (the TextColor→hex map incl.
276
+ the AA-cleared valence set), the Inter `fontFamily*` stacks, and `getInputTextStyle` /
277
+ `getInputLineHeight` — the 16px-mobile/14px-desktop input contract that stops Safari iOS
278
+ auto-zoom; only for hand-rolled raw inputs.
279
+
280
+ ### Theming, locale & tokens
281
+
282
+ - **`theme`** — `LoticsThemeProvider` / `useLoticsTheme` + `DEFAULT_ACCENT`: the single
283
+ brand accent (OKLCH blue by default) that chart fills, hero CTAs, and focus rings read.
284
+ - **`locale`** — `LoticsLocaleProvider` / `useLoticsLocale` + the shipped `en` (default) and
285
+ `vi` packs. Set the language ONCE at the root; wired components resolve **prop → provider
286
+ locale → English default**. **Limitation:** the calendar/gantt views and the comment
287
+ labels are not yet provider-wired — pass their `labels` props directly.
288
+ - **`colors`** — the palette + `withAlpha` · `solid` · `tint` · `ramp` · `ColorName` ·
289
+ `isColorName` · `asColorName` (coerce a stored option/status token to a `ColorName`,
290
+ neutral fallback).
291
+ - **`tokens`** — design tokens: re-exports `colors` plus `space`/`type`/`weight`/`radius`
292
+ scales and `getCssVariables()` — an OPT-IN serializer to `--lotics-*` CSS variables for
293
+ hand-rolled plain DOM/CSS (nothing injects them automatically; `@lotics/ui` components
294
+ don't need them).
295
+ - **`spacing`** — the `SPACE` scale + `SpaceToken`.
296
+ - **`control_surface`** — `CONTROL_HEIGHT` (40) · `CONTROL_RADIUS` (10) · `FOCUS_RING` ·
297
+ `HOVER_BORDER` · `CONTROL_TRANSITION` · `chipSurfaceStyle` — the shared control-surface
298
+ tokens.
299
+ - **`fonts.css`** — the Inter sheet (400/500/600, served by absolute URL so it resolves on
300
+ every origin an app runs from); the app entry imports it ONCE or every `Text` falls back
301
+ to system fonts.
302
+ - **`index.css`** — CSS custom properties (`--font-size-*`, `--input-font-size`, …) for
303
+ hand-rolled DOM surfaces; components don't require it.
304
+
305
+ ### Icons & identity
306
+
307
+ - **`icon`** — `Icon` + `IconName`: the curated lucide set for a fixed, code-chosen glyph.
308
+ The set is deliberately partial — a missing glyph is added to the kit (deep import + map
309
+ entry), never assumed to exist.
310
+ - **`dynamic_icon`** — `DynamicIcon`: any Lucide icon by RUNTIME name (kebab-case string),
311
+ for user/config-chosen icons a compile-time `IconName` can't express; web lazy-loads per
312
+ icon, unknown name → blank. Decorative — the enclosing control carries the accessible
313
+ name. A fixed, code-chosen glyph → `Icon`.
314
+ - **`app_icon`** — `AppIcon`: the app's launcher tile — a brand-gradient square from a
315
+ `themeColor` palette token (unknown → neutral zinc) holding any Lucide icon by runtime
316
+ name via `DynamicIcon`; `size` sm|md. One render wherever an app shows — launcher, list,
317
+ picker, settings.
318
+ - **`avatar`** — `Avatar`: image-or-initial person avatar (`source`/`name`/`size`;
319
+ `announce` when standalone — decorative by default because the name text usually sits
320
+ beside it).
321
+ - **`group_avatar`** — `GroupAvatar`: the first letter of `name` in a zinc rounded square
322
+ (`size`, default 40); the avatar for image-less entities — groups, organizations. A person
323
+ → `Avatar`/`MemberChip`.
324
+ - **`wave_avatar`** — `WaveAvatar`: decorative animated waveform avatar (voice/audit
325
+ history); animates on web, renders a static fallback on native.
326
+ - **`member_chip`** — `MemberChip`: avatar + name; the universal person render.
327
+
328
+ ### Layout & surfaces
329
+
330
+ - **`card`** — `Card` · `CardHeader` · `CardHeaderTitle` (+ `info` ⓘ popover) ·
331
+ `CardHeaderMeta` · `CardBody` · `CardFooter`.
332
+ - **`section_heading`** — `Section` · `SectionHeading` · `SectionHeadingTitle` ·
333
+ `SectionHeadingMeta` · `Subsection` · `SubsectionHeading` · `SubsectionHeadingTitle` — the
334
+ card-less twin of the Card family, compound, owns no margin; spacing via the Section gap
335
+ (12, fixed), no body component. `SectionHeadingTitle` is ALWAYS `##` (xl semibold;
336
+ `weight="medium"` opt-down only) + `info` for an ⓘ provenance popover after the title,
337
+ same as `CardHeaderTitle.info`. `SubsectionHeadingTitle` is the `###` lg-semibold level-3
338
+ title of a named group inside a section — heading-row siblings ride its right edge; the
339
+ heading ramp is FIXED: `#` xxl / `##` xl / `###` lg, no size props.
340
+ - **`section`** — a SECOND, standalone `Section`: a self-contained titled block
341
+ (`title`/`description`/`collapsible`/`icon`/`titleRight`, imperative
342
+ `SectionHandlers.expand/collapse`). **Warning:** two modules export a `Section` — the
343
+ layout grammar's card-less region is the one in `section_heading`; import from
344
+ `@lotics/ui/section` only when you specifically want this collapsible titled block.
345
+ - **`section_stack`** — `SectionStack` · `SubsectionStack` — divided stacks that own the
346
+ between-block law: a fixed beat + hairline `Divider` BETWEEN blocks, skipping null
347
+ children (56 for the flat page's Sections, 24 for a section's Subsections); stop
348
+ hand-rolling gap + `<Divider />` pairs.
349
+ - **`section_card`** — `SectionCard`: a titled card with a one-line description and an
350
+ optional hairline-set `footer` (where `TrendFooter` and source notes go).
351
+ - **`stack`** — `Stack`: gap-spaced row/column; `useSeparator` inserts `Separator`s between
352
+ children.
353
+ - **`spacer`** — `Spacer` + `SpacerSize` (the fixed size scale).
354
+ - **`divider`** — `Divider`: the bare hairline.
355
+ - **`separator`** — `Separator`: a `Divider` pre-wrapped in vertical padding (`padding:
356
+ SpacerSize`, default 8) — the between-groups rule WITH breathing room, what `Stack
357
+ useSeparator` inserts and what menus/popovers put between option groups.
358
+ - **`container`** — `Container`: centers content at a max width (`ContainerSize` sm|md|lg,
359
+ `CONTAINER_SIZES`).
360
+ - **`page_header`** — `PageHeader`: the page's title band.
361
+ - **`page_content`** — `PageContent` + `PAGE_SIZES`: the page's padded, width-capped content
362
+ region.
363
+ - **`accordion`** — `Accordion` + `AccordionHeader`/`AccordionTitle`/`AccordionMeta`:
364
+ expandable section rows.
365
+ - **`tabs`** — `Tabs`: switch between content sections; WAI-ARIA tablist + roving tabindex;
366
+ each `TabOption` takes an optional `status` `ColorName` → a small attention dot before its
367
+ label, for a tab whose area needs work.
368
+ - **`segmented_control`** — `SegmentedControl`: the exclusive small-set switch.
369
+ - **`danger_zone`** — `DangerZone`: the destructive section — a soft danger-tinted frame
370
+ (the kit's `tint`/`solid`, never raw hex) + a danger heading + a description + a
371
+ destructive action slot (children, e.g. a `danger` Button); sits APART at the bottom of a
372
+ record/settings surface.
373
+ - **`landmark`** — `Landmark`: the semantic region wrapper — `kind`
374
+ banner|navigation|main|complementary|contentinfo|region maps to the matching HTML element
375
+ on web for screen-reader landmark navigation, `accessibilityRole` on native;
376
+ `accessibilityLabel` required for `region`.
377
+ - **`skip_link`** — `SkipLink`: the a11y bypass link — parked off-screen until keyboard
378
+ focus slides it in; `href="#targetId"` jumps past repeated nav into the main region. One
379
+ per app shell, FIRST in the tree; web-only.
380
+ - **`auto_sizer`** — `AutoSizer`: measures its own box via onLayout and renders the
381
+ render-prop child only once `{width,height}` exist; `autoSizeWidthOnly` /
382
+ `autoSizeHeightOnly`, `onResize`. For content needing pixel dimensions before first paint
383
+ — a canvas, a virtualized grid.
384
+
385
+ ### Actions, links & menus
386
+
387
+ - **`button`** — `Button`: the labelled action; `title` MANDATORY (it is the accessible
388
+ name), `color` = emphasis/risk.
389
+ - **`icon_button`** — `IconButton`: the icon-only circular action (see
390
+ [Actions](#actions)).
391
+ - **`back_button`** — `BackButton`: the chevron-left `IconButton` (lg, secondary) heading a
392
+ screen/panel: `onPress` + translated `accessibilityLabel` (default "Back"); the one
393
+ go-back glyph — don't hand-roll it.
394
+ - **`link`** — `Link`: the EXTERNAL hyperlink — fixed underline+blue + `role="link"`;
395
+ `onPress` only (the consumer wires the opener).
396
+ - **`text_link`** — `TextLink`: underlined text that's optionally an `onPress` action or an
397
+ `href` link (a real `<a>` on web — middle-click / open-in-new-tab work), or plain
398
+ underlined text to wrap in your own pressable; inherits every `Text` prop; the neutral
399
+ counterpart to the fixed-blue `Link`.
400
+ - **`chip`** — `Chip`: the generic pill — pressable when `onPress` (announces as a button;
401
+ pass `accessibilityLabel` when children aren't self-describing text) + an
402
+ absolutely-positioned dismiss ✕ sibling when `onDismiss` (its name = `dismissTooltip` ??
403
+ the `chip.remove` locale slice). Suggestion pills → `SuggestionChip`.
404
+ - **`action_menu`** — `ActionMenu`: the ⋯ overflow menu (`ActionMenuItem[]`; danger items
405
+ last).
406
+ - **`menu_button`** — `MenuButton`: the menu/rail row (icon · title · `right` slot;
407
+ `selected`/`focused`/`danger`; `role` menuitem|button|option) — popover menus, outline
408
+ rails, section pickers.
409
+ - **`menu_list_item`** — `MenuListItem`: the richer listbox/menu row (title + description +
410
+ `right`; `selected` vs roving `focused` via `aria-activedescendant`).
411
+ - **`floating_action_bar`** — `FloatingActionBar`: the floating bulk-select action bar.
412
+ - **`pressable_row`** — `PressableRow`: THE register row — full-width hover/open wash,
413
+ `selected` (the open record) vs `marked` (ticked in bulk-select), forwards `ref` for
414
+ popover anchoring.
415
+ - **`pressable_highlight`** — `PressableHighlight`: the hover-wash + keyboard-focus-ring
416
+ `Pressable` under `MenuButton`/`Switcher`/custom pressable surfaces; its style-fn/children
417
+ receive `hovered` + `focusVisible`.
418
+ - **`focus_ring_pressable`** — `FocusRingPressable`: a Pressable that rings on keyboard
419
+ focus; the raw-control default.
420
+ - **`card_select_item`** — `CardSelectItem`: a bordered, card-shaped button; `selected` =
421
+ persistent ring (reads as `aria-pressed`) — the org-picker / entity-switcher item.
422
+ - **`switcher`** — `Switcher`: current-item trigger opening a popover of `MenuButton` items
423
+ (`SwitcherItem { id, label }`, `currentId`, `onSelect`) — the compact entity/workspace
424
+ switcher.
425
+ - **`count`** — `Count`: a small circular count bubble (`color` highlight|muted|red).
426
+ - **`shortcut_badge`** — `ShortcutBadge`: the keycap hint pill — a zinc-50 badge rendering a
427
+ shortcut from a raw string or `ShortcutDescriptor` (⌘B on Mac, Ctrl+B elsewhere); null on
428
+ small screens. `TextInputField shortcut` renders it built-in.
429
+ - **`keyboard`** — `isMac` · `ShortcutDescriptor` · `formatShortcut` (platform-aware
430
+ shortcut formatting behind `ShortcutBadge`).
431
+
432
+ ### Badges, status & feedback
433
+
434
+ - **`badge`** — `Badge`: the tonal pill; `variant="dot"` = pill-less colored dot + label,
435
+ the LIGHT state indicator.
436
+ - **`status_badge`** — `StatusBadge`: an enabled/disabled pulse badge (`enabled` + `label`).
437
+ - **`option_badge`** — `OptionBadge`: a select value as its configured colored badge.
438
+ - **`callout`** — `Callout` · `CalloutTitle` · `CalloutText` · `CalloutActions` (`tone`
439
+ info|success|warning|error|neutral): inline status band.
440
+ - **`empty_state`** — `EmptyState`: centered placeholder for an empty list/filter result —
441
+ `message` + `hint`, optional `icon` anchor and `action` CTA.
442
+ - **`completion_state`** — `CompletionState`: the "all done" terminal state.
443
+ - **`skeleton`** — `Skeleton`: loading placeholder blocks.
444
+ - **`loading`** — `Loading`: the centered indeterminate loading state (composes
445
+ `DotsIndicator`).
446
+ - **`activity_indicator`** — `ActivityIndicator`: the bare spinner.
447
+ - **`dots_indicator`** — `DotsIndicator`: three looping bouncing dots (`size`/`color`); the
448
+ indeterminate "working/typing" pulse `Loading` composes; use bare beside a caption while
449
+ an agent thinks. Determinate work → `ProgressBar`.
450
+ - **`animation_fade_in`** — `AnimationFadeIn`: the mount transition — children fade (+
451
+ optional `translateY` rise) into place on first render, once; the entrance polish
452
+ Accordion/Timeline/Stepper/AgentRun rows use.
453
+
454
+ ### Pickers & selection controls
455
+
456
+ - **`picker`** — `Picker`: native `<select>`, plain label-only single, native typeahead;
457
+ also home of the shared `PickerOption` type.
458
+ - **`select`** — `Select`: rich/custom-rendered, single/multi, select-all, chips via
459
+ `renderSelected(item, { remove })` + `searchable` + `allowCustom` — the tag field is just
460
+ a multi Select; opens `OptionList`.
461
+ - **`option_list`** — `OptionList`: the ONE shared searchable listbox body every selector
462
+ opens — single/multi, optional internal search, create row, keyboard + native-`<select>`
463
+ typeahead; host it directly in a `Popover`/`Dialog` for a command palette.
464
+ - **`combobox`** — COMPOUND single-select editable search: `Combobox` root +
465
+ `ComboboxInput` + `ComboboxContent`, optional `ComboboxEmpty`/`ComboboxFooter`,
466
+ `useCombobox()`; over the shared option-list engine; browses on focus; no `multi` —
467
+ multi-value chips → `Select multi`.
468
+ - **`member_select`** — `MemberSelect` + `MEMBER_UNASSIGNED`: the member picker (a `Select`
469
+ of `MemberChip` options; `unassignedLabel` prepends the unassigned option).
470
+ - **`radio_picker`** — `RadioPicker`: labelled radio group for a small visible single
471
+ choice.
472
+ - **`chip_group`** — `ChipGroup`: pill single-select over a small visible set.
473
+ - **`checkbox`** — `Checkbox`: the bare square check control.
474
+ - **`checkbox_input`** — `CheckboxInput`: checkbox + label as one labelled control.
475
+ - **`switch`** — `Switch`: the bare toggle (a cell/toolbar boolean).
476
+ - **`counter`** — `Counter`: increment/decrement numeric stepper (filter facet).
477
+ - **`range_slider`** — `RangeSlider` + `rangeSummary`: min–max range control (filter facet).
478
+
479
+ ### Form & text inputs
480
+
481
+ - **`text_input_field`** — `TextInputField`: the standard text input (multiline grows via
482
+ the auto-grow engine; `shortcut` renders a `ShortcutBadge`).
483
+ - **`number_input`** — `NumberInput`: numeric input; `format` for currency/units.
484
+ - **`search_input`** — `SearchInput`: the search box for toolbars/filters.
485
+ - **`form_field`** — `FormField` + `useFormField`: label / description / error / `optional`
486
+ marker wrapper; the fieldset grid cell (`half`/`full` widths — see the data-entry patterns
487
+ doc indexed in [AGENTS.md](../AGENTS.md)).
488
+ - **`form_text_input`** — `FormTextInput`: `FormField` wrapping a `TextInputField`; the
489
+ one-line labeled text field. Controls without a Form\* twin just wrap in `FormField`.
490
+ - **`form_switch`** — `FormSwitch`: the FormField-labeled twin of `Switch` — toggle +
491
+ clickable `label` (pressing it flips the value) + optional `description`/`error`; the
492
+ settings-form boolean row. A bare toggle in a cell/toolbar → `Switch`; a full-row menu
493
+ toggle → `SwitchButton`.
494
+ - **`form_picker`** — `FormPicker`: `FormField` wrapping a `Picker` — one labeled native
495
+ select.
496
+ - **`form_date_picker`** — `FormDatePicker`: `FormField` wrapping a `DatePicker` — one
497
+ labeled date field. (Omits `style`; for a grid cell use a bare `FormField style={half}`
498
+ around `DatePicker`.)
499
+ - **`switch_button`** — `SwitchButton`: the full-ROW toggle — a `PressableHighlight` row
500
+ (optional icon + medium title left, `Switch` pinned right) where the whole row IS the
501
+ switch (`accessibilityRole="switch"`, the inner Switch read-only). The settings-panel/menu
502
+ row toggle.
503
+ - **`use_form`** — `useForm`: THE batch draft-form state hook — `values` = `initialValues` +
504
+ an edits overlay (a revalidation refreshes untouched fields, no sync effect), `validate`
505
+ (sync/async, gates submit, editing clears the field's error), `onSubmit(values, helpers)`
506
+ with a re-entry-guarded `submitting`, and `changes`/`changed` — the touched-fields diff
507
+ that feeds DIFF-writes (send only edited fields); `setFieldValue` (curried or direct) +
508
+ `setValues`/`setFieldError`/`reset`. Pairs with the Form\* family for dialog/settings
509
+ forms — the draft-validate-COMMIT-together twin of the self-persisting Inline\* editors.
510
+ - **`scan_field`** — `ScanField`: the scan/verify input (`ScanStatus` idle|match|mismatch).
511
+
512
+ ### Dates & times
513
+
514
+ - **`date_picker`** — `DatePicker` (+ `DatePickerPanel`, `DatePickerLabels`): the field-form
515
+ date (and datetime) picker.
516
+ - **`date_calendar`** — `Calendar`: the bare month grid — `mode="single"` or `"range"`
517
+ (`{start,end}` — two months side by side on desktop), month/year pickers + arrows,
518
+ localized weekday/month names via BCP-47 `locale`, `firstDayOfWeek` (default Monday),
519
+ `ref.navigateToMonth`. The engine `DatePicker`/`DateFilter` wrap in field chrome — reach
520
+ for it bare only when the calendar lives permanently on the surface, not behind a field.
521
+ - **`date_filter`** — `DateFilter`: the single/range date+time filter panel — presets
522
+ (`PresetId`), calendar, optional time segments; the body `DateRangeFilterField` opens.
523
+ - **`date_range_filter_field`** — `DateRangeFilterField`: the register's period filter field
524
+ (presets + footer + time-segment a11y; localized via the `dateRange` locale slice).
525
+ - **`time_picker`** — `TimePicker`: the time-of-day input.
526
+
527
+ ### Inline editing & record surfaces
528
+
529
+ - **`inline_edit`** — `useInlineEdit` + `InlineEditView` / `InlineEditFrame` +
530
+ `INLINE_CONTROL_HEIGHT` (40) + `inlineValueTextStyle`: the engine custom inline editors
531
+ join through — a view ⇄ edit toggle, a draft buffer, async `onSave` with the spinner
532
+ INSIDE the control and inline error, commit on blur (Enter saves, Escape reverts) or
533
+ `controls="buttons"`; `background="tint" | "transparent"` (the zinc-50 editability chip vs
534
+ flat for dense uniformly-editable surfaces).
535
+ - **`inline_text_input`**, **`inline_number_input`** (`format` for currency/units),
536
+ **`inline_select`**, **`inline_member_select`**, **`inline_date_picker`**
537
+ (`format="datetime"`, `optionalTime`), **`inline_time_picker`** — the Inline\* per-field
538
+ editors; `InlineSelect`/`InlineMemberSelect` render the resting value like its option —
539
+ `renderOptionContent` by default, `renderSelected` to override — a chip/badge at rest, not
540
+ just text.
541
+ - **`inline_tag_select`** — `InlineTagSelect`: the MULTI inline editor — a tag SET in the
542
+ inline vocabulary: selected tags render as badges inside the standard chip (`renderTag`),
543
+ clicking floats a multi `OptionList` (checkbox rows), CLOSING commits the new set in one
544
+ `onSave` — never a borderless `Select` posing as an inline field.
545
+ - **`inline_static`** — `InlineStatic`: a READ-ONLY value matching the Inline\* box metrics
546
+ EXACTLY (height, padding, 1px transparent border) so a non-editable field — a computed
547
+ total, a system ID, a synced/locked value — aligns pixel-for-pixel in the same column;
548
+ non-interactive, NOT a disabled input; `muted`/`tabular`/`align="right"` for a number
549
+ column, `weight="medium"` to emphasise a total among plain rows.
550
+ - **`detail_row`** — `DetailTable` + `DetailRow` — the record field grid. `DetailRow`:
551
+ label+value row for drawer/peek detail; in FORM mode (`labelWidth` set) the value column
552
+ FILLS the row so a stack of inline editors all span the same width + none jumps wider on
553
+ edit; optional `trailing` slot renders a right-side action/badge after the value (units
554
+ belong IN the value via `InlineNumberInput format`). `DetailTable`: the compound parent of
555
+ a row STACK — `labelWidth` (default 130) / `trailingWidth` / `minHeight` (default 40, the
556
+ inline-control grid) declared ONCE + the 6px row gap; with `trailingWidth` every row
557
+ reserves the trailing column so value cells share one width and trailing items align at
558
+ one x, like a table. RESPONSIVE with no prop: it measures its own container (onLayout, not
559
+ the viewport — works inside a Drawer; the unmeasured first frame renders opacity-0 so the
560
+ first PAINT is already in the right mode) and when the columns would crush the value cell
561
+ it STACKS every row (the label above a full-width value row in the FormField label
562
+ grammar; trailing beside the value); raise `minValueWidth` (default 160) when a cell holds
563
+ MORE than one editor so the table stacks earlier. Two tables on one page share one grid by
564
+ repeating the same labelWidth/trailingWidth. Worked example:
565
+ [`tpl_record`](../examples/tpl_record.tsx).
566
+ - **`record_summary`** — `RecordSummary`: the identity band of a record detail/drawer — ONE
567
+ row: `title` xl semibold tabular · `subtitle` sm muted · `status` Badge slot · optional
568
+ `metric` {label,value,tone,note} pinned right, the band's ONE accent. The record's FIELDS
569
+ never live in the header: compose them as `DetailTable`s in the sections below. Replaces
570
+ hand-rolled record headers (mixed scales, several competing figures, color noise).
571
+ - **`ledger`** — `Ledger` + `LedgerGroup` + `LedgerRow` + `LedgerTotal` — the record-level
572
+ money list: charge/receipt GROUPS with sums in their headers, every figure on ONE
573
+ right-aligned tabular column, `peek` turns a row into a pressable door floating its
574
+ particulars in an anchored popover (put links INSIDE the peek — never a button in a
575
+ button; `reference` is the trailing-link alternative for static rows), `LedgerTotal` = the
576
+ divider-set emphasized close with `zeroLabel` for settled. The financial-statement grammar
577
+ at record density; worked example: [`tpl_item_list`](../examples/tpl_item_list.tsx)
578
+ drawer.
579
+ - **`peek`** — `Peek`: drill-down for inline references — press a name/id where it appears
580
+ and get its details in an anchored popover, without leaving the screen; keep the content a
581
+ summary with ONE action to the full record.
582
+
583
+ ### Lists, tables & registers
584
+
585
+ - **`list`** — `List`: children separated by hairline `Divider`s.
586
+ - **`list_item`** — `ListItem`: the plain list row.
587
+ - **`table`** — `Table` + `TableRow` + `TableCell`: the paginated high-volume register
588
+ (columns defined once, `sortLabels` localizable; rows are `PressableRow`-based).
589
+ CONTAINER-RESPONSIVE with no prop (measures itself, like `Breakdown`/`DetailTable`): when
590
+ the width can't fit every column it hides columns by `TableColumn.priority` (higher drops
591
+ first; default = column order, rightmost first; column 0 — the identity — never drops), and
592
+ below the two-column floor every row STACKS: each cell renders as a `DetailRow`-spread
593
+ line (muted label left, value at the right edge — one vocabulary with the drawer's detail
594
+ rows), the lines sitting on the identity column's text edge, never under the checkbox;
595
+ the header band gives way, so sorting is a wide-container affordance. Rows carry TWO right-side slots: `action`
596
+ (the primary CTA `Button` — register: in the trailing gutter; stacked: closes the content,
597
+ right-aligned) and `trailing` (the ⋯ overflow — stays beside the identity on the top line).
598
+ On pressable rows dropped values stay one tap away — the row opens the record; a READ-ONLY
599
+ register (rows without `onPress`) has no door, so give the columns it can't afford to lose
600
+ a low `priority` (e.g. `priority: 1` — outlives its right-side neighbours).
601
+ - **`sort_header`** — `SortHeader` + `SortState`/`SortDir` + `cycleSort` + `sortBy` +
602
+ `SortHeaderLabels`: the sortable column header and the sort-state helpers `Table`/
603
+ `DataGrid` consumers drive.
604
+ - **`data_grid`** — `DataGrid` + `gridRowStyle`: the inline-managed grouped table (see
605
+ [Tabular data](#tabular-data--pick-by-scale--intent)); `labels` localizes the sort-header
606
+ a11y via `SortHeaderLabels`.
607
+ - **`pagination`** — `Pagination` (+ `PaginationLabels`): the register's pager.
608
+ - **`column_filter`** — `ColumnFilter` + `columnFilterToConditions` +
609
+ `isColumnFilterActive` + `columnFilterSummary`: the typed per-column filter pill; for a
610
+ register filtering on several columns.
611
+ - **`filter_chip`** — `FilterChip` + `selectSummary`: the toolbar filter pill hosting a
612
+ facet (options, a `RangeSlider`, a `Counter`).
613
+ - **`summary_line`** — `SummaryLine`: the light inline summary of a register/list's FILTERED
614
+ view, sits below the toolbar; NOT the boxed dashboard `kpi_strip` band.
615
+ - **`use_selection`** — `useSelection`: always-on multi-select state for a register/list —
616
+ the `selected` Set + `toggle`/`setAll`/`allSelected`/`indeterminate`/`count`/`clear`;
617
+ selectability gating stays with the caller. The checkbox-always-visible counterpart to
618
+ `use_selection_mode`.
619
+ - **`use_selection_mode`** — `useSelectionMode`: `{ active, selected, enter, exit, toggle,
620
+ toggleAll }` — an agnostic enter/exit multi-select state machine (string ids; pairs with
621
+ `FileThumbnailGrid selectedIds` and gated-CRUD flows).
622
+
623
+ ### Tasks & checklists
624
+
625
+ - **`check_circle`** — `CheckCircle`: the completion ring — an empty ring that springs to a
626
+ filled check when done, distinct from the square checkbox; the task/to-do/checklist
627
+ toggle.
628
+ - **`checklist`** — `Checklist` + `ChecklistRow` — the record-scoped checklist COMPOUND: it
629
+ owns GEOMETRY only (row minHeight 32, gap 12, ring/title alignment, ONE `trailingWidth` so
630
+ assignee cells column-align) while content stays composed — `control` takes the
631
+ `CheckCircle` (omit onChange = read-only ring), children the struck transparent
632
+ `InlineTextInput`, `trailing` an `InlineMemberSelect`, `menu` the row's ⋯ options
633
+ (`{items: ActionMenuItem[], accessibilityLabel}`) — Delete lives BEHIND the menu,
634
+ danger-styled and last, never a bare ✕ (omit on read-only rows). NARROW surfaces (a
635
+ drawer/peek checklist) put the editors on the `meta` line instead of `trailing` — the
636
+ second line indents past the ring so the TITLE keeps the full width; wide surfaces use
637
+ `trailing`; never both. SUGGESTIONS are never rows: offer the commons as `SuggestionChip`s
638
+ under the list. Close the list with `CaptureRow`. There is deliberately NO monolithic Task
639
+ component — richer task-management rows compose their own anatomy directly.
640
+ - **`suggestion_chip`** — `SuggestionChip`: the dismissible SUGGESTION pill — an item the
641
+ record could have but doesn't yet (a common task, an expected line) as a `Chip` whose
642
+ press MATERIALIZES it (plus glyph + label, one tuned anatomy) and whose ✕ refuses it;
643
+ filter out labels already present; suggestions never count in totals. Chrome via the
644
+ `suggestionChip` locale slice.
645
+ - **`capture_row`** — `CaptureRow`: the add-an-item row closing an editable list — dashed
646
+ empty ring + borderless input on the item-title inset + a primary Save that appears on
647
+ type; Enter commits too. Controlled: `value`/`onChangeText`/`onSubmit`. One shape wherever
648
+ a list grows in place — task checklists, simple item lists.
649
+
650
+ ### Overlays & navigation
651
+
652
+ - **`portal`** — `PortalHost` + `Portal`: the floating-content mount point; the app root
653
+ needs ONE `PortalHost` or `Dialog`/`Popover`/`Tooltip`/`Alert`/`OptionList` cannot render.
654
+ - **`dialog`** — `Dialog` (+ `DialogHeader`/`DialogHeaderTitle`/`DialogFooter`, `useDialog`,
655
+ `useDialogNavigation`): the centered card over a scrim; BAKES a screen router in
656
+ (`<Dialog><Screen route="">…` — see `screen_router`).
657
+ - **`drawer`** — `Drawer` + `DrawerFooter`: the docked side panel with scrim; the register
658
+ row's edit surface.
659
+ - **`modal`** — `Modal` + `ModalHeader` + `ModalBody` + `ModalFooter` — the full-bleed,
660
+ edge-to-edge takeover: an OPAQUE surface that COVERS THE WHOLE SCREEN, so unlike Dialog
661
+ (centered card WITH scrim) and Drawer (docked panel WITH scrim) there is nothing behind it
662
+ to dim — NO scrim, NO backdrop. Lays children as a flex column: a pinned ModalHeader
663
+ (eyebrow/title + an actions slot + close), a flex:1 scrolling ModalBody, a pinned
664
+ ModalFooter (the commit bar, same chrome as DialogFooter/DrawerFooter). Reach for it for a
665
+ focused capture / multi-step wizard / a console the user steps INTO; pick Dialog when the
666
+ surface is a card the user can see context around.
667
+ - **`popover`** — `Popover` + `PopoverTrigger` + `PopoverContent` (+ `PopoverHeader`,
668
+ `PopoverFooter`; `side`/`align`; `disableBodyScroll` ONLY for children that manage their
669
+ own scroll, like `OptionList`): the anchored floating surface. **NON-MODAL**: the anchored
670
+ popover has NO blocking overlay — the rest of the page stays interactive, and clicking another
671
+ control both dismisses this popover AND activates that control in one click; clicking outside,
672
+ scrolling an ancestor, or Escape dismisses. Only the `small` (bottom-sheet) presentation is
673
+ modal (scrim). `PopoverContent` already insets its body 12px — put content directly in it,
674
+ NEVER add your own padding `View` (that double-pads); title/actions go in `PopoverHeader` /
675
+ `PopoverFooter`.
676
+ - **`popover_nav`** — `usePopoverNav` + `PopoverScreen` + `PopoverNavHeader` — the popover's
677
+ built-in mini-router: EVERY `Popover` provides the nav context (`navigate(route)` pushes,
678
+ `goBack`, `currentRoute`, `canGoBack`; resets on close), `PopoverScreen route=""` is the
679
+ root and screens render conditionally (unmounted when inactive — no scroll preservation),
680
+ `PopoverNavHeader` is the title row whose back chevron auto-appears while `canGoBack`
681
+ (`right` slot, `backLabel`). For a multi-screen menu inside ONE popover; route PATTERNS,
682
+ `params`, and stacked-alive screens are `screen_router`'s job.
683
+ - **`screen_router`** — `ScreenRouter` + `Screen` + `useScreenRouter` — the SCREENS
684
+ compound: a flat navigation stack (`navigate("/case/:id")` pushes, `goBack` pops,
685
+ `canGoBack`, route `params`; stacked screens stay mounted `display:none` so scroll
686
+ survives the round trip). Dialog bakes a router in; ANY other container hosts the
687
+ standalone `<ScreenRouter>` — and it wraps AROUND the container so the CHROME can read the
688
+ stack (a Drawer drilling into a LINKED record swaps its header to a BACK `IconButton`
689
+ while `canGoBack`). Key the router by record id so stepping between records resets the
690
+ stack. Worked example: [`tpl_item_list`](../examples/tpl_item_list.tsx) drawer.
691
+ - **`route_matching`** — pure `:param` route-pattern utilities — `routeMatches` /
692
+ `parseRouteParams` / `findBestPattern` (exact beats parameterized) / `shouldRouteMatch`;
693
+ the matching core under `ScreenRouter`/`Dialog`.
694
+ - **`tooltip`** — `Tooltip` (+ `TooltipTrigger`, `TooltipProvider`, `useTooltip`): the short
695
+ hover label.
696
+ - **`info_popover`** — `InfoPopover`: the ⓘ button opening a 280px popover of explanatory
697
+ `text`; the middle ground between Tooltip (short hover label) and composing Popover (rich
698
+ content) — it is what `SectionHeadingTitle.info`/`CardHeaderTitle.info` render.
699
+ - **`alert`** — `Alert` (+ `AlertButton`/`AlertOptions`): the blocking confirm —
700
+ `Alert.alert(title, message, [{cancel}, {destructive}])`.
701
+ - **`overlay_scope`** — `isOverlayScopeActive` / `pushOverlayScope` / `useOverlayScope`: the
702
+ module-level open-overlay counter every overlay primitive reports into; the host's
703
+ shortcut registry reads it synchronously to floor page-level shortcuts while any overlay
704
+ is open. Lives in the primitives — never call it from a screen.
705
+ - **`use_section_nav`** — `useSectionNav`: scroll-spy for a LONG record surface with a left
706
+ outline rail — keys in page order → `{scrollRef, onScroll, register(key)→onLayout,
707
+ jumpTo(key), activeKey}`; rail items are `MenuButton`s (`selected={activeKey===key}`);
708
+ section wrappers must be DIRECT children of the ScrollView content. On NARROW containers
709
+ the rail becomes a PINNED bar naming the CURRENT section that opens a full-page
710
+ section-picker `Modal` — never a horizontal tab strip. Worked example:
711
+ [`tpl_record`](../examples/tpl_record.tsx).
712
+ - **`scroll_to_bottom`** — `ScrollToBottom`: the floating jump-to-latest circle button for a
713
+ chat/feed; ONLY the affordance — the caller owns positioning, visibility, and the actual
714
+ scroll.
715
+
716
+ ### Numbers & charts
717
+
718
+ - **`kpi_card`** — `KPICard`: one headline metric in a card (trend chip slot, sub-caption,
719
+ ⓘ definition).
720
+ - **`kpi_strip`** — `KPIStrip`: the boxed dashboard stat band.
721
+ - **`metric`** — `Metric`: a bare headline figure (`format`
722
+ currency|number|percentage|none, `tone`, `size` sm|md|lg|hero).
723
+ - **`trend_chip`** — `TrendChip`: the signed ±% delta chip.
724
+ - **`trend_footer`** — `TrendFooter`: the "Up X% vs last period" caption under a chart card
725
+ — signed `value` (0 = flat), `periodLabel`, optional `detail`; `goodDirection="down"`
726
+ flips green/red for metrics where down is good; SKIP it when there's no comparator.
727
+ Direction words via the `trendFooter` locale slice; goes in `SectionCard footer`.
728
+ - **`sparkline`** — `Sparkline`: the inline mini trend line.
729
+ - **`bar_chart`** / **`line_chart`** / **`pie_chart`** — `BarChart` / `LineChart` /
730
+ `PieChart`: the canonical SVG chart set (no recharts).
731
+ - **`ring_gauge`** — `RingGauge`: single-fraction circular gauge.
732
+ - **`progress_bar`** — `ProgressBar`: the determinate meter; `compact` = ONE row, track + a
733
+ plain sm tabular count beside it.
734
+ - **`stacked_progress_bar`** — `StackedProgressBar`: one whole split across segments (a
735
+ status mix on one bar).
736
+ - **`step_progress`** — `StepProgress`: N-of-M dots/segments progress.
737
+ - **`breakdown`** — `Breakdown`: a stacked bar + ranked share rows, pressable to drill;
738
+ `maxRows` folds the long tail behind a localized "Show N more" toggle
739
+ (`BreakdownLabels`).
740
+ - **`funnel`** — `Funnel`: conversion funnel — narrowing bars + the step rate as a bold
741
+ aligned headline row, count below; `orientation` vertical|horizontal,
742
+ `onSelect`/`selectedKey` press-to-drill; the subset/drop-off sibling of
743
+ `stacked_progress_bar`'s whole-split.
744
+ - **`status_grid`** — `StatusGrid` + `StatusLegend`: the per-slot state grid (occupancy,
745
+ availability) and its legend.
746
+ - **`heatmap`** — `Heatmap`: density cross-tab — colour-only, no numbers; "where does it
747
+ cluster".
748
+ - **`matrix`** — `Matrix`: the PIVOT cross-tab — band-compound `Matrix` root +
749
+ `Matrix.Header` (corner + axis labels) + `Matrix.Grid` (`display` number|heat|both — the
750
+ cells, pressable, the value IN the cell) + `Matrix.Totals` (row + column + grand) +
751
+ `Matrix.Legend`. Pick over `Heatmap` when the NUMBER and totals matter.
752
+ - **`matrix_totals`** — the data layer for `Matrix`: `matrixTotals` (the cross-tab
753
+ aggregation) + `MatrixAxisItem` / `MatrixCellRef` / `MatrixTotalsResult`; React-free, so a
754
+ KPI can be driven off the same numbers the grid shows.
755
+ - **`legend_item`** — `LegendItem`: one swatch + label of a chart legend.
756
+ - **`remainder_meter`** — `RemainderMeter`: allocated-vs-remaining meter
757
+ (`RemainderMeterLabels` localized via the provider).
758
+ - **`allocation_row`** — `AllocationRow`: one target's row in an allocation surface (pairs
759
+ with `RemainderMeter`).
760
+
761
+ ### Scheduling & time
762
+
763
+ - **`calendar`** — the calendar views: `CalendarView` (the composed month/week surface),
764
+ `TimeGridView`, `MonthView`, plus the layout/date helpers (`layoutDayColumns`,
765
+ `packEventLanes`, `addDays`, `startOfWeek`, …) and `CalendarEvent`/`CalendarLabels` types.
766
+ **Limitation:** labels are not provider-wired — pass `labels`
767
+ (`DEFAULT_CALENDAR_LABELS` is English).
768
+ - **`gantt`** — `GanttView` + scale helpers (`barGeometry`, `axisRange`, `buildTicks`,
769
+ `pxPerDay`) + `GanttTask`/`GanttLabels` types. **Limitation:** labels are not
770
+ provider-wired — pass `labels` (`DEFAULT_GANTT_LABELS` is English).
771
+ - **`timeline`** — `Timeline`: a heterogeneous event LOG — per-row icon + expandable
772
+ details; models the past, NOT progress.
773
+ - **`stepper`** — `Stepper` + `Step` — done/current/upcoming/warning/complete progress on a
774
+ track (horizontal) or spine (vertical); compound `<Step status>children` OR data
775
+ `steps[]`+`current`; **navigable** via `Step.onPress` (both orientations — the whole step
776
+ is the tap target) + `active` to wash the selected one, so it doubles as a section/phase
777
+ switcher; the guided-run / agent-feed primitive.
778
+
779
+ ### Files
780
+
781
+ - **`mime`** — pure MIME predicates: `isImageMimeType` / `isVideoMimeType` /
782
+ `isAudioMimeType` / `isPdfMimeType` / `isExcelMimeType` / `isCsvMimeType` /
783
+ `isDocxMimeType` / `isPreviewableMimeType`.
784
+ - **`download`** — `downloadFileFromUrl(url, filename, { credentials? })`: fetch+blob+anchor
785
+ download that works inside sandboxed iframes (where `window.open` is silently dropped);
786
+ `credentials` defaults to `same-origin` — pass `"include"` only for auth-gated same-site
787
+ proxy URLs, never for presigned URLs.
788
+ - **`file_picker`** — `pickFiles({ accept?, multiple? }) → Promise<File[]>`: opens the
789
+ browser file dialog imperatively — the trigger half behind every Add-file CTA; cancel
790
+ resolves `[]` on modern engines.
791
+ - **`file_dropzone`** — `FileDropzone`: the drag-drop capture well (`onFiles`, `accept`,
792
+ `label`/`hint`/`dropLabel`, `height`); click falls back to a picker.
793
+ - **`files_editor`** — `FilesEditor` — THE all-in-one attachment field: `FileGrid` + a
794
+ toolbar (Upload primary · Select · Download all) that swaps into a batch SELECT mode
795
+ (Select all · a Menu of Download/Share/Delete · Done; the per-tile ✕ is select-mode-only,
796
+ never in the default view; `selectTileRemove={false}` drops even that so delete is
797
+ menu-only) + built-in gallery (Download + inline preview; no "open in new tab") +
798
+ Alert-confirmed remove; host wires `files` + `onAdd`/`onRemove` (+ optional `uploads`,
799
+ `onShareSelected`, `readOnly`, `selectTileRemove`, `labels`, `galleryLabels`,
800
+ `gridMaxHeight` — cap the grid height so it scrolls and the toolbar pins, for a
801
+ popover/drawer). Use `FileGrid`/`FileRows` bare only when you own the chrome.
802
+ - **`file_grid`** — `FileGrid` + the `FileUpload`/`PendingUpload` types: the upload-aware
803
+ grid — `files` are the saved/completed `DisplayFile`s, `uploads` is the LIVE add-queue; it
804
+ interleaves both and renders each in-flight tile itself with a labeled status overlay.
805
+ Tiles FILL the container width at a uniform size (≥ `minItemWidth`, default 96); pass
806
+ `columns` for a fixed count, `itemSize` for exact tiles, or `singleRow` to fit one row and
807
+ collapse the rest into a clickable "+N" overflow tile (`onOverflowPress(hiddenCount)`).
808
+ CRUD via `onFilePress` / `onDisplayRemove` / `onUploadRemove` / `onRetry` / `onRetryAll`;
809
+ localize with `labels.upload` (an `UploadStatusLabels`).
810
+ - **`uploading_thumbnail`** — `UploadingThumbnail` + `UploadStatus`/`UploadStatusLabels`:
811
+ the single in-flight upload tile `FileGrid` renders; reach for it only when hand-rolling a
812
+ non-grid upload layout.
813
+ - **`file_thumbnail`** — `FileThumbnail` + `DisplayFile` + `THUMBNAIL_SIZE` /
814
+ `COMPACT_THUMBNAIL_SIZE` + `getMediaIcon`: the completed tile — the right surface per
815
+ MIME: image thumbnail · a doc tile with the `FileBadge` centered + a single-line filename
816
+ · media card; `isTemplate` overlays a TMPL marker.
817
+ - **`file_thumbnail_grid`** — `FileThumbnailGrid` (+ the generic `ThumbnailGrid`):
818
+ display-only square-tile grid; `selectedIds` + `onFilePress` pair with
819
+ `useSelectionMode()` for gated select-mode CRUD.
820
+ - **`file_row`** — `FileRow`: a horizontal file/document LINE — `FileBadge` or a
821
+ `placeholder` + the FULL name + a meta line + a composable `trailing` slot; `onPress`
822
+ makes the whole row a pressable door, `trailing` stays an independently-pressable sibling;
823
+ for attachment lists / message files / document checklists where a square tile truncates
824
+ the name.
825
+ - **`file_rows`** — `FileRows`: batteries-included file list — row press → built-in gallery
826
+ + a ⋯ Download/Open-external/Remove menu; composes `FileRow` + `ActionMenu` +
827
+ `FileGalleryModal`; localized via the `gallery` locale slice.
828
+ - **`file_badge`** — `FileBadge`: the two-tone file-type mark (PDF / XLSX / DOCX / video /
829
+ audio …) driven by MIME.
830
+ - **`file_preview`** — `FilePreview`: the universal inline preview — image/PDF/video/audio +
831
+ Word via `@lotics/docx` + Excel/CSV via `@lotics/xlsx`; the heavy engines (pdf.js ·
832
+ `@lotics/docx` · `@lotics/xlsx`) are LAZY (dynamic-imported, ~free until a doc of that
833
+ type is opened) and SHIP AS `@lotics/ui` deps — custom-code apps get PDF/Word/Excel
834
+ preview with ZERO extra install. Renders to canvas/DOM, never a nested iframe — works in
835
+ the sandboxed app iframe.
836
+ - **`file_preview_types`** — `PreviewLabels` / `FilePreviewProps` / `GalleryLabels` — the
837
+ shared label + prop contracts of the file-preview family; types only.
838
+ - **`file_gallery_modal`** — `FileGalleryModal`: the FULL-SCREEN viewer — toolbar (filename
839
+ · counter · download, optional `onOpenExternal`/`onRemove` · close-✕), prev/next, ESC,
840
+ rotate (the 90° controls FLOAT as a pill on the image); on a phone the actions collapse
841
+ into a ⋯ `ActionMenu`. `onPersistRotation`/`persisting` wire a rotation save. Driven by
842
+ `files` + `activeIndex` + `onIndexChange`.
843
+ - **`image_gallery`** — `ImageGallery`: main image + thumbnail strip (`thumbnailPosition`
844
+ bottom|right, `mainAspectRatio`, `rotatable`) with a built-in zoom modal.
845
+ - **`rotatable_image`** — `RotatableImage`: an image rotating in 90° steps that REFITS — at
846
+ a quarter turn the box lays out in SWAPPED dimensions then rotates into place, so a
847
+ rotated landscape fills the frame; the VIEW half of image rotation
848
+ (`FilePreview`/`ImageGallery` compose it).
849
+ - **`rotate_image`** — `rotateImageToBlob(url, degrees)`: canvas-bake a 90° rotation into a
850
+ NEW blob for re-upload (`useImageRotation` is view-only; this is how a rotation is
851
+ persisted). Pair with `FileGalleryModal`'s `onPersistRotation`/`persisting`.
852
+ - **`use_image_rotation`** — `useImageRotation`: per-file VIEW rotation state (degrees keyed
853
+ by file id, in memory) shared between an inline gallery and its fullscreen modal;
854
+ view-only — a render transform, never a mutation of the stored file.
855
+ - **`share_or_download`** — `shareOrDownloadFiles(files, { title, credentials })`:
856
+ `navigator.share({ files })` (mobile → Save to gallery / send to an app), else individual
857
+ `downloadFileFromUrl` — **never a ZIP**. It shares the BYTES (fetches each URL → `File`),
858
+ so URL expiry afterward is moot; the share path needs the host iframe to grant
859
+ `allow="web-share"` (else it falls back to download). Best for FAST urls (presigned).
860
+ **For SLOW urls (auth-gated proxy with a server round-trip), split it:**
861
+ `prepareShareFiles(files, {credentials})` fetches the `File[]` BEFORE the gesture (on
862
+ menu-open), then `shareFiles(File[])` runs `navigator.share` synchronously on the tap —
863
+ **iOS Safari rejects `share()` if a slow fetch burns the tap's transient activation**,
864
+ which silently lands in the download fallback. `prepareShareFiles` throws on a fetch
865
+ failure (log it, don't swallow); `shareFiles` returns `"shared"|"cancelled"|"unsupported"`.
866
+
867
+ ### AI & collaboration
868
+
869
+ - **`composer`** — `Composer`: the adaptive command/chat composer — a compact pill that
870
+ expands for long text + attachments (`pills` + attachment slots, controlled or
871
+ uncontrolled `value`, `onSend`/`onStop`, `sendDisabled` override).
872
+ - **`agent_run`** — `AgentRun` + `AgentRunStep`/`AgentRunItem` + `resolveToolMeta`: the live
873
+ streaming work feed.
874
+ - **`agent_progress`** — `AgentProgress`: its compact, floating, expandable form — a
875
+ composer's working state.
876
+ - **`confidence`** — `Confidence` + `ConfidenceLevel` + `levelFromScore`: calibrated
877
+ high/med/low; localized via the provider.
878
+ - **`change_review`** — the COMPOUND review family — frame: `ChangeReview` provider/stack ·
879
+ `ChangeReviewHeader` (auto kept-counter over decidable entries) · `ChangeReviewActions`
880
+ (the commit bar in the DialogFooter/DrawerFooter: Keep-all bottom-left (`onAcceptAll` for
881
+ host-held field state) + Apply gating); sections: `Change` (host-owned status, labeled
882
+ verbs, collapses to its `ChangeSummary` + Undo; no callbacks = display-only) ·
883
+ `ChangeLabel` · `ChangeSummary` · `ChangeReasoning` (the quiet why); grammar:
884
+ `ChangeFields` (the open record form) + `ChangeField` (THE field: − band · value ·
885
+ candidates + type-another-value · reasoning · per-field Keep/Drop · collapse) ·
886
+ `ChangeRecord` (THE item card: registers like a Change; tone wash + localized op word;
887
+ verb level follows the decision level) · `ChangeBand` (the raw ± band) ·
888
+ `ChangeValueInput` (the diff-at-rest editor); the `changeReview` locale slice.
889
+ - **`clarify`** — `Clarify`: the agent asks back, via `ChoiceList`.
890
+ - **`choice_list`** — `ChoiceList` + `ChoiceOption`: selectable answer options, the agent's
891
+ quick-reply surface.
892
+ - **`sources`** — `Sources` + `SourceRef`/`SourceKind` (record | document | table | web |
893
+ knowledge): provenance chips, per-kind glyphs.
894
+ - **`finding`** — `Finding` + `FindingComparison` + `FindingSeverity`/`FindingLabels`: one
895
+ ranked AI-check insight — severity word · title · detail · `Sources` · children slot; the
896
+ expected-vs-actual body with the emphasized delta; the `finding` locale slice.
897
+ - **`comments_thread`** — `CommentList` + `CommentComposer` + the
898
+ `ThreadComment`/`ThreadMember`/`ThreadFile` types: the record comments thread.
899
+ **Limitation:** labels are not provider-wired — pass `CommentListLabels`.
900
+
901
+ ### Utility hooks & plumbing
902
+
903
+ - **`use_screen_size`** — `useScreenSize` / `getScreenSize` / `calculateScreenSize`:
904
+ `{ small, medium, large }` width-breakpoint booleans (small < 768 ≤ medium < 1728 ≤
905
+ large); tracks width changes only, so the mobile soft keyboard (a height-only change)
906
+ never re-renders.
907
+ - **`use_async_fn`** — `useAsyncFn`: wrap an async function into a manual-trigger mutation —
908
+ `[run, {loading, data, error}]`, unmount-safe, the error lands in state AND rethrows; the
909
+ pending-state engine for a submit/download/upload action.
910
+ - **`use_hover`** — `useHover`: pointer-hover state `{hovered, hoverProps}` for raw
911
+ inputs/DOM controls that lack a hovered style state; native stays false. Pressable-based
912
+ controls use the built-in `hovered` state instead.
913
+ - **`use_auto_grow_height`** — `useAutoGrowHeight`: the grows-with-content textarea engine
914
+ (`{minLines, maxLines}` → container height + `scrollEnabled` at the cap); powers
915
+ `Composer` and multiline `TextInputField` — reach for it only when hand-rolling a growing
916
+ input.
917
+ - **`use_focus_ring`** — `useFocusRing`: keyboard-aware focus state for painting a control's
918
+ own ring.
919
+ - **`json_panel`** — `JsonPanel` (`{title, value}`) + `stringifyData`: a labeled monospace
920
+ panel for raw/JSON payloads (debug & developer surfaces).