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