@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.
- package/AGENTS.md +41 -1100
- package/docs/ai_patterns.md +374 -0
- package/docs/catalog.md +904 -0
- package/docs/composition.md +439 -0
- package/docs/data_entry.md +398 -0
- package/docs/templates.md +357 -0
- package/package.json +3 -2
package/AGENTS.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @lotics/ui — the UI reference
|
|
1
|
+
# @lotics/ui — the UI reference index
|
|
2
2
|
|
|
3
3
|
> **Upgrading to v11 from ≤10.x — the type scale was re-slotted.** The display sizes gained the
|
|
4
4
|
> missing `##` step: `xl` is now 22/24 (section title), `xxl` is 28/32 (page/record title — the
|
|
@@ -8,1102 +8,43 @@
|
|
|
8
8
|
> ALWAYS `##` (22/24), and disabled `Button`s fade their label to the zinc-400 disabled ink on
|
|
9
9
|
> every variant.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
and `elevated` (white fill + border + shadow, for a button sitting ON imagery — a tile's remove ✕,
|
|
52
|
-
an overlay's retry); needs `accessibilityLabel`/`tooltip`), `TextLink` (underlined text that's
|
|
53
|
-
OPTIONALLY an action (`onPress`) or a link (`href`) — or, with neither, plain underlined text you
|
|
54
|
-
drop in your own pressable like a table cell; colour via `color`; the go-to for Clear / Select all /
|
|
55
|
-
inline links), `Chip` (dismissible facet chip). A button is never a raw `Pressable`. For a link OUT
|
|
56
|
-
(a URL / record / document) use `Link` — fixed underline+blue + `role="link"`, the destination
|
|
57
|
-
signal; `TextLink` is the neutral, colour-configurable underlined link/action.
|
|
58
|
-
- **Pick from a list** — `Picker` (native `<select>`, plain label-only single, native typeahead)
|
|
59
|
-
or `Select` (the rich one — custom-rendered options, single/multi, select-all). Search-as-you-type
|
|
60
|
-
/ async / create-new → `Combobox`. A selectable card row → `CardSelectItem`. For a SELECT-FIELD
|
|
61
|
-
picker, render each option as its colored chip on `Select`
|
|
62
|
-
(`renderOptionContent={(o) => <OptionBadge value={o} />}`). `Select` opens the shared
|
|
63
|
-
`OptionList` body; `Combobox` is COMPOUND (`ComboboxInput` + `ComboboxContent`) over the same
|
|
64
|
-
`useOptionList` engine; `Picker` is the only one that's a native dropdown.
|
|
65
|
-
- **Pick member(s)** — `MemberSelect` (a `Select` that renders each option as a `MemberChip`,
|
|
66
|
-
single or multi) — the ready member picker; pass it the roster (`members={useMembers().members}`).
|
|
67
|
-
Don't re-wire `Select` + `renderOptionContent` + a directory by hand. For an assignee FILTER that
|
|
68
|
-
also matches unassigned records, pass `unassignedLabel` → it prepends a muted "unassigned" option
|
|
69
|
-
(value `MEMBER_UNASSIGNED`), so you never hand-roll a mixed member+none `Select`. To edit a `select_member`
|
|
70
|
-
field IN PLACE (chip at rest → picker on click) use its inline-edit twin `InlineMemberSelect`.
|
|
71
|
-
- **A person / member (display)** — `MemberChip` (avatar + name + optional secondary line) — the
|
|
72
|
-
ONE way to show a member inline: a picker option, an assignee, a `select_member` value. Pure:
|
|
73
|
-
resolve the member from your directory (`useMembers`) and pass `name` / `image`; never hand-roll
|
|
74
|
-
`Avatar` + `Text`. (The product's `MemberBadge` is just `memberId → MemberChip`; `MemberSelect`
|
|
75
|
-
renders these per option.)
|
|
76
|
-
- **A select-field value** — `OptionBadge` (a stored `select` value as its CONFIGURED colored
|
|
77
|
-
badge) — never hand-map option-key → color. Feed it a resolved option (`useFieldOptions` for a
|
|
78
|
-
picker option, or `byKey(readSelect(cell)[0]?.key)` for a stored value); multi-select wraps to
|
|
79
|
-
one badge each; a missing/unknown color token degrades to neutral.
|
|
80
|
-
- **Tags / multi-value chip box** — `Select multi` whose `renderSelected` returns a removable
|
|
81
|
-
`<Chip onDismiss={remove}>` (with `searchable` + `allowCustom`). The chip box is COMPOSED, not a
|
|
82
|
-
separate control — there's no `display` mode; `renderSelected` is the seam (see §Data entry).
|
|
83
|
-
- **Text & form** — `TextInputField`, `NumberInput`, `SearchInput`; wrap with `FormField`;
|
|
84
|
-
`Checkbox`, `Switch`, `RadioPicker`; dates via `DatePicker` / `DateRangeFilterField`, times
|
|
85
|
-
via `TimePicker`.
|
|
86
|
-
- **Edit a record's fields in place** — the `Inline*` family: `InlineTextInput` ·
|
|
87
|
-
`InlineNumberInput` · `InlineSelect` · `InlineMemberSelect` · `InlineDatePicker` ·
|
|
88
|
-
`InlineTimePicker`; a READ-ONLY field in that same column uses `InlineStatic` (matches the
|
|
89
|
-
editor box exactly, no input chrome, so it aligns pixel-for-pixel) (see §Data entry).
|
|
90
|
-
- **Tasks / to-dos — pick by ALTITUDE.** A task-management PAGE (many tasks, grouping, filters,
|
|
91
|
-
expandable rows) is COMPOSITION — there is NO Task component (a Reminders row and a Linear
|
|
92
|
-
column-grid share almost nothing): `CheckCircle` + a struck `InlineTextInput` + your meta cells;
|
|
93
|
-
see `tpl_tasks` (quick list) and `tpl_task_board` (columns). A record's own small CHECKLIST
|
|
94
|
-
(the 5–8 tasks living in a drawer/section) is the `Checklist`/`ChecklistRow` compound +
|
|
95
|
-
`SuggestionChip` commons + `CaptureRow` — see `tpl_record`/`tpl_item_list`.
|
|
96
|
-
- **Tabular data — pick by SCALE + intent.** Two columnar shapes, and the choice is about data size:
|
|
97
|
-
- **High-volume register** (thousands+ you BROWSE) — `Table` (columns defined once; sortable
|
|
98
|
-
headers via `SortHeader`; paired with `Pagination`) + read-only rows that open a `Drawer` to edit.
|
|
99
|
-
It scales by PAGING — renders one page, never the whole set — so you FILTER + search, you don't
|
|
100
|
-
group. Worked example: `tpl_item_list`. Never an HTML `<table>` or a `.map` of rows.
|
|
101
|
-
- **Inline-managed grouped table** (MODERATE — hundreds, low-thousands — you MANAGE in view) — the
|
|
102
|
-
`DataGrid` primitive: a grouped, sortable grid whose cells are LIVE inline editors (ANY field — a
|
|
103
|
-
column is `{ key, label, width?, sortable?, cell: (item) => ReactNode }`, so `InlineMemberSelect` /
|
|
104
|
-
`InlineDatePicker` / `InlineNumberInput` / `InlineSelect` / a borderless `Select multi` for tags / colour-dot `OptionBadge`).
|
|
105
|
-
`DataGrid` owns the sortable header + collapsible grouped sections + aligned rows + an optional
|
|
106
|
-
per-row `leading` slot (a `CheckCircle`); YOU own the data, the sort/group/filter/collapse STATE
|
|
107
|
-
(`cycleSort` + `sortBy`, a `collapsed` Set), the toolbar (`SearchInput` + `FilterChip`s), and the
|
|
108
|
-
per-group add row (`renderGroupFooter`, aligned with the exported `gridRowStyle` + the column
|
|
109
|
-
widths). It renders ALL rows (no virtualization), so it's only for sets small enough to hold in
|
|
110
|
-
view — at 10k+ it lags, and grouping + pagination/infinite don't compose; use the register instead.
|
|
111
|
-
Worked example: `tpl_task_board` (a `CheckCircle` leading).
|
|
112
|
-
- **Numbers & charts** — `KPIStrip` (the dashboard stat band) · `SummaryLine` (the light inline
|
|
113
|
-
register/list summary — below the toolbar, from the filtered rows) · `KPICard` / `Metric` (headline figures), `TrendChip`
|
|
114
|
-
(delta), `Sparkline`, `BarChart` / `LineChart` / `PieChart` (the canonical SVG set — no
|
|
115
|
-
recharts), `RingGauge`, `ProgressBar` (its `compact` prop = ONE row, track + a plain sm
|
|
116
|
-
tabular count beside it — the cell/heading/peek-trigger meter; a caption floating above a
|
|
117
|
-
tiny bar reads misaligned) / `StackedProgressBar` / `StepProgress`, `Breakdown`
|
|
118
|
-
(a stacked bar + ranked share rows, pressable to drill; `maxRows` folds the long tail behind a
|
|
119
|
-
"Show N more" toggle — `labels` to localize — so several facet cards align to one height in a row),
|
|
120
|
-
`Funnel` (a CONVERSION funnel — ordered stages as bars that NARROW; the step conversion rate is the
|
|
121
|
-
HEADLINE (a bold aligned row across the top, the first stage = the 100% baseline), the count is the
|
|
122
|
-
supporting figure below — the Amplitude/Mixpanel convention, never pin the rate to the fill height.
|
|
123
|
-
`orientation` vertical columns | horizontal bars; pass `onSelect`+`selectedKey` to make the bars
|
|
124
|
-
press-to-drill (the selected stays solid, others dim — the caller renders the records). The
|
|
125
|
-
subset/drop-off sibling of `StackedProgressBar` — nested cohorts that shrink "calls → connected →
|
|
126
|
-
won", NOT a whole split across stages — that's `StackedProgressBar`),
|
|
127
|
-
`StatusGrid` + `StatusLegend`, `Heatmap` (density: colour-only, "where does it cluster"),
|
|
128
|
-
`Matrix` (the PIVOT cross-tab: the NUMBER in each cell — optionally a heat wash behind it — plus
|
|
129
|
-
row/column/grand totals; press a cell to drill).
|
|
130
|
-
- **Surfaces & layout** — `Card` (+ `CardHeader`/`CardHeaderTitle`/`CardHeaderMeta`/`CardBody`/
|
|
131
|
-
`CardFooter`), `Section` (the card-less twin of `Card`: bare gap-spaced region + `SectionHeading`/
|
|
132
|
-
`SectionHeadingTitle`/`SectionHeadingMeta`; no body component — children are the body;
|
|
133
|
-
), `Subsection` (+ `SubsectionHeading`/
|
|
134
|
-
`SubsectionHeadingTitle` — the named group INSIDE a section, `###` lg-semibold title),
|
|
135
|
-
`SectionStack` (the flat page's content column — owns the fixed 56px beat + hairline between
|
|
136
|
-
top-level blocks), `SubsectionStack` (the same law one step tighter — fixed 24px beat +
|
|
137
|
-
hairline between a section's `Subsection` groups), `SectionCard`,
|
|
138
|
-
`PageHeader` /
|
|
139
|
-
`PageContent`, `Stack`, `Spacer`, `Divider`, `Accordion`, `Tabs`, `SegmentedControl`, `Stepper`,
|
|
140
|
-
`DangerZone` (the destructive section — delete/archive — set apart at the bottom of a record/settings surface).
|
|
141
|
-
- **Rows & registers** — `PressableRow` (THE register row; forwards its `ref`, so wrapping it in a `PopoverTrigger` anchors a row-triggered peek Popover — without the ref the trigger is unmeasurable and the popover renders off-screen), `ListItem`, `MenuButton`,
|
|
142
|
-
`MenuListItem`, `DetailRow` (label+value for drawers/peeks; optional `trailing` slot for a
|
|
143
|
-
right-side action/badge/unit), `ActionMenu` (⋯), `FloatingActionBar` (bulk-select bar).
|
|
144
|
-
- **Filters & view controls** — `SearchInput`, `ChipGroup`, `FilterChip` (+ `RangeSlider`,
|
|
145
|
-
`Counter`), `Chip`.
|
|
146
|
-
- **Overlays** — `Dialog` (centered card over a scrim), `Modal` (+ `ModalHeader`/`ModalBody`/
|
|
147
|
-
`ModalFooter` — a full-bleed, edge-to-edge takeover with NO scrim), `Drawer` (+ `DrawerFooter`),
|
|
148
|
-
`Popover`, `Tooltip`, `OptionList` (the searchable list body — host it in a `Popover`/`Dialog` for
|
|
149
|
-
a command palette), `Alert` (the blocking confirm).
|
|
150
|
-
- **Status / feedback** — `Badge` / `StatusBadge`, `Callout` (inline status), `EmptyState`,
|
|
151
|
-
`CompletionState`, `ActivityIndicator` / `Loading`, `Skeleton`.
|
|
152
|
-
- **Files** — `FilesEditor` (THE all-in-one attachment field: an upload-aware grid + a toolbar below
|
|
153
|
-
it that swaps into a batch SELECT mode, full-screen preview, download/share, and Alert-confirmed
|
|
154
|
-
remove — the host only owns `files` + wires `onAdd`/`onRemove`; mirrors the frontend
|
|
155
|
-
`cell_files_editor`. Reach for this first for "manage a record's attachments"), `FileDropzone`,
|
|
156
|
-
`FileRows` (batteries-included file LIST: tap a row → built-in
|
|
157
|
-
full-screen gallery, with a per-row trailing ⋯ menu = Download · Open-external · Remove; the default "here are some files"
|
|
158
|
-
surface), `FileGrid` (the upload-aware grid: completed files + a live upload
|
|
159
|
-
queue in one surface — `FilesEditor` is this + the toolbar; reach for `FileGrid` bare when you own
|
|
160
|
-
the chrome), `FileThumbnail` / `FileThumbnailGrid`
|
|
161
|
-
(square tiles, display-only), `UploadingThumbnail` (the single in-flight tile FileGrid renders —
|
|
162
|
-
reach for it only when hand-rolling a non-grid upload layout), `FileRow` (a horizontal file/document
|
|
163
|
-
LINE — badge-or-placeholder + name + meta + a composable `trailing` slot for a status badge / action
|
|
164
|
-
/ remove; `onPress` makes the whole row a pressable door, `trailing` stays an independently-pressable
|
|
165
|
-
sibling; for checklists & readable lists), `FileBadge` (the two-tone type mark), `FilePreview` /
|
|
166
|
-
`FileGalleryModal`, `ImageGallery`; for gated CRUD compose locally with `useSelectionMode` +
|
|
167
|
-
`shareOrDownloadFiles` + `rotateImageToBlob` (see §Data entry → Attachments).
|
|
168
|
-
- **Specialized work surfaces** — `ScanField` (scan/verify), `Stepper` (a guided run / progress
|
|
169
|
-
sequence — done · current · upcoming, horizontal OR vertical), `RemainderMeter` + `AllocationRow`
|
|
170
|
-
(allocation), `Timeline` (a heterogeneous event LOG — icons + expandable details, not progress),
|
|
171
|
-
`Calendar`, `Gantt`, `comments_thread`.
|
|
172
|
-
- **AI surfaces** — `Composer` (the adaptive command/chat composer — a compact pill when empty that
|
|
173
|
-
expands for long text + attachments; the surface that triggers agent work),
|
|
174
|
-
`AgentRun` (the live streaming work feed) + `AgentProgress` (its compact, floating, expandable
|
|
175
|
-
form — a composer's "working" state) + `Confidence`;
|
|
176
|
-
**`ChangeReview` — THE one review-before-apply surface, a COMPOUND family** (frame: `ChangeReview`
|
|
177
|
-
· `ChangeReviewHeader` · `ChangeReviewActions`; sections: `Change` · `ChangeLabel` ·
|
|
178
|
-
`ChangeSummary` · `ChangeReasoning`; the grammar: `ChangeFields` + `ChangeField` ·
|
|
179
|
-
`ChangeRecord` · `ChangeBand` + `ChangeValueInput`): adds, updates, removals, conflicts, whole
|
|
180
|
-
records, display-only findings are all compositions — see §AI workflows for the laws;
|
|
181
|
-
`Clarify` (the agent asks back — selectable `ChoiceList` options),
|
|
182
|
-
`Sources` (provenance chips for AI output — at review scale, `label={null}` slots the chips at a
|
|
183
|
-
section's bottom), `Finding` (one ranked insight from an AI check — localized severity word ·
|
|
184
|
-
title · detail · `Sources` chips · a `children` slot; **`FindingComparison`** is the
|
|
185
|
-
expected-vs-actual body: each disagreeing side a labeled row, the DELTA emphasized under a
|
|
186
|
-
hairline (localized "Difference") — quantities, totals, dates; a plain `metric` prop remains for
|
|
187
|
-
one-number findings. The children slot composes ANY visual result — a compact `Table` for
|
|
188
|
-
per-line detail (danger color on the offending cells), `ProgressBar` for
|
|
189
|
-
consumption-toward-a-cliff (free time, credit), dot `Badge`s for a present/missing checklist,
|
|
190
|
-
`Confidence` for judgment calls — the AI-page demos show each. Display-only — it informs the verdict the host records; `finding` slice).
|
|
191
|
-
See §AI workflows.
|
|
192
|
-
|
|
193
|
-
---
|
|
194
|
-
|
|
195
|
-
## Data entry — which pattern for which job
|
|
196
|
-
|
|
197
|
-
This is the most common thing to get right. Match the JOB to the pattern:
|
|
198
|
-
|
|
199
|
-
| You're capturing… | Reach for | Why |
|
|
200
|
-
|---|---|---|
|
|
201
|
-
| an EXISTING record's fields | **Inline edit** (`Inline*`) | edit in place, no form mode |
|
|
202
|
-
| a brand-NEW record | **create-then-refine** (`tpl_record`: "New" is ONE CLICK → a fresh Draft; everything refines in place on the record surface) | nobody fills 5 sections in one sitting; the surface is the editor |
|
|
203
|
-
| a RELATED record (pick or make) | **find-or-create** (`Combobox allowCustom`) | one control covers both |
|
|
204
|
-
| REPEATING rows you build & revise | **line items** (create→preview→edit) | add / edit / remove, live totals |
|
|
205
|
-
| CHARGES that bill onto documents | **billing** (`tpl_record` Billing section) | the invoice document is the unit |
|
|
206
|
-
| a record's FEE/charge SUMMARY | **`Ledger`** (worked example: `tpl_item_list` drawer) | LedgerGroup (label + sum) → LedgerRow (label · meta · ONE right tabular money column; `peek` makes the row a door to its PARTICULARS in an anchored popover — references live inside the peek; `reference` = a trailing link on a non-peek row) → LedgerTotal (divider-set emphasized close, `zeroLabel` for the settled state). Pair with a Record-payment POPOVER that appends a receipt. No bars/charts |
|
|
207
|
-
| a multi-value TAG field | **`Select multi`** (`renderSelected` → `Chip`) | chips composed, not a separate control |
|
|
208
|
-
| ONE choice from a small visible set | **`ChipGroup` pills** (or `RadioPicker`) | required single-select, one tap, every option visible |
|
|
209
|
-
| a STATUS with terminal outcomes | **disposition** (open → resolve → revise) | guides the decision |
|
|
210
|
-
| FILES | **attachment field** (dropzone + grid + gallery) | add / preview / delete |
|
|
211
|
-
| a state TRANSITION mid-flow | **stage gate** (popover / dialog by weight) | right-sized friction |
|
|
212
|
-
|
|
213
|
-
### Inline edit — the preferred way to edit an existing record
|
|
214
|
-
When the whole record is editable (a detail/record screen, dense settings), don't wrap it in a
|
|
215
|
-
form mode or a preview↔edit card — make each VALUE inline-editable: it reads as plain text,
|
|
216
|
-
hover reveals its border (it's a field — no grey tint, no pencil that shifts), click swaps the input in **at the same height** (zero
|
|
217
|
-
reflow, the whole point), and it commits on blur (Enter saves, Escape reverts) or via
|
|
218
|
-
`controls="buttons"` (✓ primary / ✕). One per type — `InlineTextInput` · `InlineNumberInput`
|
|
219
|
-
(`format` for currency/units) · `InlineSelect` (plain options OR `renderOptionContent`; floats an
|
|
220
|
-
`OptionList` in a popover so the row never grows; the RESTING value renders like its option
|
|
221
|
-
(`renderOptionContent` by default; `renderSelected` overrides) — a colored `OptionBadge`, not just a label) · `InlineMemberSelect` (a `MemberChip` at rest →
|
|
222
|
-
member picker; the inline twin of `MemberSelect`; worked example: `tpl_record`'s "Sales owner" fact) · `InlineDatePicker` (`format="datetime"` for
|
|
223
|
-
always-on time; `optionalTime` to let the user ADD/REMOVE a time — the value's own shape, date vs
|
|
224
|
-
datetime, is the source of truth) · `InlineTimePicker` · `InlineTagSelect` (the MULTI member —
|
|
225
|
-
a tag SET in the inline vocabulary: selected tags render as badges inside the standard chip,
|
|
226
|
-
clicking floats a multi `OptionList` (checkbox rows), CLOSING commits the new set in one
|
|
227
|
-
`onSave` — never a borderless `Select` posing as an inline field) — all
|
|
228
|
-
on `useInlineEdit` + `InlineEditView` (custom inputs join via those). `onSave` is async: the
|
|
229
|
-
saving spinner sits INSIDE the control (never a sibling — that reflows); an error shows inline
|
|
230
|
-
without losing the edit. To let a value be UNSET (a diff-write CLEAR — unassign, remove a due
|
|
231
|
-
date, drop a select), pass **`onClear`** to `InlineSelect` / `InlineMemberSelect` /
|
|
232
|
-
`InlineDatePicker`. It surfaces through each popover's OWN clear affordance — `InlineSelect` renders
|
|
233
|
-
a compact left-aligned "Clear" `Button` below the `OptionList` (only while a value is set);
|
|
234
|
-
`InlineDatePicker` reuses the calendar's own footer "Clear" button. NOT a bolted-on sibling row: that
|
|
235
|
-
can't reach the option list's active-highlight (so the last-hovered option stays lit) and would
|
|
236
|
-
double the footer hairline;
|
|
237
|
-
and never a persistent ✕ on the resting cell (noise on a dense board, and a pressable nested in the
|
|
238
|
-
view's button trigger is invalid DOM). `onSave`'s `next` stays non-null so a caller opts in per field;
|
|
239
|
-
the clear fires `onClear`, which writes `null` (the app workflow must ACCEPT null on that input — a
|
|
240
|
-
`select`/`date`/`member` field clears on null). `InlineTagSelect` (multi) needs no `onClear` — an
|
|
241
|
-
empty set is already a valid `onSave`. A STACK of rows lives in a `DetailTable` (label ·
|
|
242
|
-
value · trailing laid out like a TABLE: `labelWidth` / `trailingWidth` /
|
|
243
|
-
`minHeight` set ONCE on the parent, plus the 6px row gap the zinc-50 chips
|
|
244
|
-
need) holding `DetailRow`s — set `trailingWidth` when ANY row carries a
|
|
245
|
-
trailing action/badge, so EVERY row reserves the column and one row's
|
|
246
|
-
`Copy` button never makes its editor narrower than its neighbours'. An editor AT REST sits
|
|
247
|
-
on a zinc-50 chip — THE editability affordance: users see what's editable without hovering; the
|
|
248
|
-
picker-opening editors (`InlineSelect`/`InlineDatePicker`/`InlineTimePicker`) additionally carry a
|
|
249
|
-
rest glyph (chevron / calendar / clock) marking them tappable pickers without hover — the
|
|
250
|
-
touch-critical cue plain text/number fields don't get.
|
|
251
|
-
`background="transparent"` opts a field out of the chip — for DENSE, uniformly-editable
|
|
252
|
-
surfaces (a task list/board where EVERY cell edits: the chip repeated everywhere is noise and
|
|
253
|
-
distinguishes nothing; hover/focus still reveal the input). Keep the chip wherever editable and
|
|
254
|
-
static values MIX. A `disabled` editor rests FLAT automatically — the chip is the editability
|
|
255
|
-
promise, and an inert field must not make it. A
|
|
256
|
-
READ-ONLY value in the same column — a computed total, a system ID, a synced/locked field — is
|
|
257
|
-
`InlineStatic`: it copies the editor box metrics exactly (height, padding, 1px transparent border)
|
|
258
|
-
but stays FLAT and non-interactive, so editable (chip) vs read-only (flat) is legible at a glance
|
|
259
|
-
and the static value never reads as a disabled input. To hang a right-side
|
|
260
|
-
action/badge off a row, use `DetailRow`'s `trailing` slot (NOT for units — "kg"/"₫" belong IN the value via `InlineNumberInput format`) — see the "Details"
|
|
261
|
-
`DetailTable` of `tpl_record`, which also reads top→bottom as a full record surface
|
|
262
|
-
(header → fields → `DangerZone`). Not every field is a same-height swap — a tag field, a status, or an attachment grid
|
|
263
|
-
edit in place too (below).
|
|
264
|
-
|
|
265
|
-
### Fieldset form — fields lay out on a RESPONSIVE two-column grid
|
|
266
|
-
Never hard-code columns, never 3-up. A fieldset is `flexDirection:row, flexWrap:wrap,
|
|
267
|
-
columnGap:16`; each `FormField` declares an intrinsic width — `half` (`flexGrow:1,
|
|
268
|
-
flexBasis:240`) or `full` (`flexGrow:1, flexBasis:"100%"`) — so two halves sit side-by-side on a
|
|
269
|
-
wide card and stack on a narrow one with zero media queries. Pair short, related fields as halves
|
|
270
|
-
(phone/email, qty/price); give long or singular values the full row (legal name, address, notes).
|
|
271
|
-
Past two columns the label→field link breaks — MANY inputs means GROUPING into labeled fieldsets
|
|
272
|
-
(each its own 2-up grid, `Divider` between), never a third column. `FormDatePicker`/`FormPicker`
|
|
273
|
-
wrap their OWN `FormField` (and omit `style`) — for a grid cell use a bare `FormField style={half}`
|
|
274
|
-
wrapping `DatePicker`/`Picker`.
|
|
275
|
-
|
|
276
|
-
### Find-or-create — the `Combobox` family IS the control
|
|
277
|
-
`Combobox` is COMPOUND: a root holds the DATA + behaviour (the shared `useOptionList` engine —
|
|
278
|
-
`options`, `value`, `onValueChange`, `onSearchChange`, `allowCustom`, the per-row
|
|
279
|
-
`getOptionDescription`/`renderOptionContent` so they're typed against the option `data`), and the
|
|
280
|
-
parts render the CHROME, composed as children — never a render-prop pile:
|
|
281
|
-
|
|
282
|
-
```tsx
|
|
283
|
-
<Combobox options={hits} value={sel} onSearchChange={setQ} onValueChange={pick} allowCustom
|
|
284
|
-
customOptionLabel={(q) => `Create "${q}"`} getOptionDescription={(o) => o.data?.code}>
|
|
285
|
-
<ComboboxInput icon="search" clearable onClear={deselect} placeholder="Find or create…" />
|
|
286
|
-
<ComboboxContent recentsLabel="Recent">
|
|
287
|
-
<ComboboxEmpty>No match — type a name to create one</ComboboxEmpty>
|
|
288
|
-
<ComboboxFooter>{validity}</ComboboxFooter>
|
|
289
|
-
</ComboboxContent>
|
|
290
|
-
</Combobox>
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
It's a SELECT by default (`ComboboxInput` with NO `icon` → a trailing chevron); opt INTO the
|
|
294
|
-
search-box look with `ComboboxInput icon="search"` only when typing-to-search is primary (a
|
|
295
|
-
large/remote set). For a select that the user can RESET, pass `clearable` + `onClear` (deselect):
|
|
296
|
-
the chevron shows while the field is empty and a clear ✕ replaces it once there's a value/query;
|
|
297
|
-
pressing ✕ clears the text and reopens the full-browse list, so `onClear` lands back on the whole
|
|
298
|
-
set (on focus an empty field already browses everything — `recentOptions ?? all`). `allowCustom` appends a "Create …" row (`customOptionLabel`) when the query
|
|
299
|
-
matches no option, emitting the typed text as the value — so a value not in the known set means
|
|
300
|
-
CREATE. Wire that branch to a create overlay (an anchored popover for a 1–2 field gate, a modal
|
|
301
|
-
`Dialog` for 3+) that builds the new record and attaches it; existing matches attach directly.
|
|
302
|
-
A record-CREATION dialog stays a MINIMAL gate and really creates — the row lands and its own
|
|
303
|
-
workspace opens for refinement (an EMPTY checklist, commons as ghosts). NEVER a creation
|
|
304
|
-
wizard: create-then-refine puts complexity on the record surface, not in front of it. The create
|
|
305
|
-
row sits BELOW matches by default (the keyboard highlight lands on the first MATCH, so Enter on a
|
|
306
|
-
partial picks it, never a duplicate); pass `customOptionPlacement="top"` to pin it above. Use
|
|
307
|
-
`reflectSelection={false}` and render the attached record below as a card with a Change action.
|
|
308
|
-
For input-level status that must stay visible while results scroll (validity feedback on the typed
|
|
309
|
-
value, a result count, a secondary "create" affordance), drop a `ComboboxFooter` into the content —
|
|
310
|
-
a pinned row below the listbox, OUTSIDE keyboard option-nav; call `useCombobox().close()` from a
|
|
311
|
-
footer action that hands off elsewhere (so the popover dismisses cleanly instead of floating over
|
|
312
|
-
what you navigated to). A `ComboboxEmpty` child gives the no-match state richer content than the
|
|
313
|
-
`ComboboxContent emptyText` string.
|
|
314
|
-
|
|
315
|
-
### Line items — create → preview → edit (a composition, not a primitive)
|
|
316
|
-
For a list of records you build then revise (invoice rows, repair lines, config entries), each
|
|
317
|
-
item is a `Card` toggling between a read-only PREVIEW (a stack of `DetailRow`s) and an EDIT form
|
|
318
|
-
(the inputs), via a local `editing` flag + Edit/Save/Cancel in the `CardFooter`. A freshly-added
|
|
319
|
-
item opens in edit; Save collapses it to the preview; Edit reopens it. **Edit is cancelable** —
|
|
320
|
-
snapshot the item on Edit so Cancel reverts, or DISCARDS a freshly-added one. Duplicate sits left;
|
|
321
|
-
the destructive **Delete is `danger-secondary`**; Cancel + the Save/Edit toggle sit right. Every
|
|
322
|
-
screen composes its own (~15 lines) so the preview rows + form fit the data.
|
|
323
|
-
|
|
324
|
-
### Handoff — a stage transition, never a message
|
|
325
|
-
When work crosses departments (sales → operations → accounting), the HANDOFF is the RECORD
|
|
326
|
-
changing desks — never an inbox, a notification, or a copied task. Two types:
|
|
327
|
-
**(A) Same entity** → a STAGE field on the shared record: sections carry OWNER dot tags, and
|
|
328
|
-
the handoff is MANAGED AS TASKS — each desk's checklist on the record (the task-row vocabulary:
|
|
329
|
-
sibling `CheckCircle` + struck transparent `InlineTextInput` + per-task `InlineMemberSelect`
|
|
330
|
-
assignees + the `CaptureRow` add-affordance — never a decorative progress strip). A NEW record
|
|
331
|
-
starts with an EMPTY checklist — tasks truly vary. The commons split in two: MANDATORY tasks are
|
|
332
|
-
seeded by the app (a workflow on create, per record type) — no human types them; common-but-
|
|
333
|
-
OPTIONAL tasks appear as `SuggestionChip`s under the list — a PILL, never a row,
|
|
334
|
-
so a suggestion can't be mistaken for a task (tap = materialize, ✕ = dismiss for this record;
|
|
335
|
-
already-present labels filter out; suggestions never count in done/total and pause while a
|
|
336
|
-
filter narrows the view) — the same suggestion grammar as the
|
|
337
|
-
billing Standard pill. Real rows carry a ⋯ `menu` (an `ActionMenu`; Delete lives BEHIND it, danger-styled and last — never a bare ✕ a stray tap can hit), completing the checklist's CRUD. Open
|
|
338
|
-
tasks INFORM
|
|
339
|
-
the handoff, they NEVER block it: the CTA stays enabled, the count warns, and open tasks carry
|
|
340
|
-
over. The handoff CTA opens a DIALOG for the receiving desk (assignee `MemberSelect` + an
|
|
341
|
-
optional note; the open-task warning inside); confirming closes the drawer — the record left
|
|
342
|
-
this register. Tasks PEEK from the register: the done/total column is a pressable
|
|
343
|
-
compact-`ProgressBar` trigger whose popover holds the checklist on FIXED columns — every row
|
|
344
|
-
ring · struck `InlineTextInput` title (flex) · quick-reassign `InlineMemberSelect` (140) — no
|
|
345
|
-
expandable rows (tags/files depth is the Task list template's lesson, not the peek's); the
|
|
346
|
-
popover body is `PopoverContent`'s own ScrollView (`disableBodyScroll` is ONLY for children
|
|
347
|
-
that manage their own scroll, like `OptionList`). The DRAWER carries a real Tasks SECTION in
|
|
348
|
-
the Record template's shape (heading + the compact meter + the same checklist) — one shared
|
|
349
|
-
task state per record feeds the column, the peek, the section, and the handoff dialog;
|
|
350
|
-
a section owned by a later desk sits visible but GATED ("Billing opens when the record reaches
|
|
351
|
-
Accounting"). Registers scope by stage — the receiving department's register IS its inbox. The
|
|
352
|
-
sender's sections stay editable after handoff (the gate transfers RESPONSIBILITY, not access —
|
|
353
|
-
locking is app IAM, not template grammar). Worked example: `tpl_record`.
|
|
354
|
-
**(B) Different entity** (a deal → a shipment) → SPAWN-AND-LINK: the upstream record's terminal
|
|
355
|
-
gate CREATES the downstream record on the shared spine, linked both ways (a linked-record row
|
|
356
|
-
each side); usually TWO apps — the upstream closes its own lifecycle, the downstream starts
|
|
357
|
-
fresh in its department's app. Never merge the two lifecycles into one record.
|
|
358
|
-
|
|
359
|
-
### Sequential phases on one record + the outline rail
|
|
360
|
-
When a record's work happens in ORDERED PHASES (gate in → gate out; receive → dispatch), the
|
|
361
|
-
phases are NOT tabs or a segmented control — hiding the other phase loses the context the
|
|
362
|
-
current one needs and buries why its gate is blocked. Both phase sections sit on ONE page in
|
|
363
|
-
chronological order; each phase owns ITS OWN fee rows + phase total (the invoice-band idea one
|
|
364
|
-
level up — money never reads as one pot); each closes with a SEQUENTIAL confirm gate that names
|
|
365
|
-
its blockers ("Confirm gate in first."). The header status chip carries the phase. A LONG record
|
|
366
|
-
surface pairs with a LEFT OUTLINE RAIL — `MenuButton` items + `useSectionNav` (scroll-spy: jump
|
|
367
|
-
to a section, the highlight follows the scroll); on narrow containers the rail becomes a
|
|
368
|
-
PINNED bar naming the current section that opens a full-page section-picker `Modal`. A per-phase
|
|
369
|
-
dot on a rail item carries its confirmed state. A direction that is a TYPE (one record per gate
|
|
370
|
-
EVENT) is instead a discriminator chosen ONCE at creation — a `SegmentedControl` in the create
|
|
371
|
-
step, never a toggle on the record.
|
|
372
|
-
|
|
373
|
-
### Billing — the invoice DOCUMENT is the unit (`tpl_record` Billing section)
|
|
374
|
-
When charges get grouped into issuable documents (an e-invoice, a bill) and then collected, don't
|
|
375
|
-
split the screen into "enter fees here, issue there" — that smears one job across two places. Make
|
|
376
|
-
**each invoice a FLAT hairline-set band that holds its own editable charge lines** (amount input +
|
|
377
|
-
payment method), its **live total**, its **status badge** (nothing-to-bill · draft · issued + ref),
|
|
378
|
-
and its **issue action** in its closing row — no card chrome; Data-capture templates are flat. A charge never lives apart from the document it bills on. Issuing is gated
|
|
379
|
-
**inline, never a dead end**: when a prerequisite is missing (a bill-to tax ID, a method on a
|
|
380
|
-
charged line) the issue button disables with one muted line saying what's needed; the payment-method
|
|
381
|
-
picker turns required the instant a line carries an amount. Issuing a real e-invoice is irreversible
|
|
382
|
-
→ confirm in a `Dialog` (stage gate). A grand-total **receipt** validates first — surface the EXACT
|
|
383
|
-
missing methods (`Alert.alert` listing each) rather than a vague "incomplete." A refundable
|
|
384
|
-
**deposit** is its own card and its own receipt — never folded into the total due. Composition over
|
|
385
|
-
`Card` + `NumberInput` + `Picker` + `Badge`; no new primitive.
|
|
386
|
-
|
|
387
|
-
### Tag / multi-value field — `Select multi`
|
|
388
|
-
A tag field's resting state should be a tidy CHIP BOX — and that's just a multi `Select` whose
|
|
389
|
-
`renderSelected` returns a removable `<Chip onDismiss={remove}>`. There's NO separate component and no
|
|
390
|
-
`display` mode: `renderSelected(item, { remove })` composes the anchor — render a `Chip` with `remove`
|
|
391
|
-
for the ✕, or a plain `OptionBadge`/`MemberChip`/custom pill that ignores it; `renderOptionContent`
|
|
392
|
-
renders the menu rows. `searchable` adds the filter field, `allowCustom` the create row. Borderless for
|
|
393
|
-
a grid cell? Pass a `style`, never a `variant`. `Combobox` is the SINGLE-value sibling — for a search
|
|
394
|
-
that emits one pick at a time and renders the selection elsewhere, use `reflectSelection={false}`
|
|
395
|
-
(see `tpl_*`).
|
|
396
|
-
|
|
397
|
-
### Disposition — lifecycle status is ASYMMETRIC by phase
|
|
398
|
-
When a status is an OPEN default plus terminal OUTCOMES the user decides (Lead → Closed/Lost, Draft
|
|
399
|
-
→ Confirmed/Cancelled, Open → Approved/Rejected), a 3-way segmented/dropdown is wrong — it treats a
|
|
400
|
-
lifecycle as flat peers and guides neither the decision nor the revision. Split by phase: while
|
|
401
|
-
OPEN, surface the outcomes as ACTIONS with valence (`Mark closed` = `primary`, `Mark lost` =
|
|
402
|
-
`danger-secondary`; the open state a quiet dot `Badge`). Coloring the buttons is right here — a
|
|
403
|
-
SINGLE record-level decision is not the approvals-queue "wall of loud buttons." Once RESOLVED, show
|
|
404
|
-
the colored STATE (`Badge variant="dot"` — emerald positive / red negative / blue open) with a
|
|
405
|
-
quiet, REVERSIBLE **Change**: a popover STATE SWITCHER (each state a colored dot + label, current
|
|
406
|
-
marked + disabled), not a generic text menu. A composition (Badge + Button + Popover/MenuButton).
|
|
407
|
-
|
|
408
|
-
### Attachments — a full add / preview / DELETE field
|
|
409
|
-
**Default: `<FilesEditor files onAdd onRemove>`** — it bundles the grid + a toolbar (Upload · Select ·
|
|
410
|
-
Download all) + a batch SELECT mode + the gallery + confirmed-remove; the host only owns `files` and
|
|
411
|
-
wires `onAdd` (picked → its upload) / `onRemove`. The destructive per-tile ✕ shows only in SELECT mode
|
|
412
|
-
(the default view is a clean preview — no stray-tap deletes); pass `selectTileRemove={false}` to drop
|
|
413
|
-
that ✕ entirely so select mode deletes ONLY via Select → Menu → Delete (the batch gating flow — how the
|
|
414
|
-
cell popover's `cell_files_editor` does it). The built-in gallery is Download +
|
|
415
|
-
inline preview (no "open in new tab" — it's redundant once everything previews inline). In a
|
|
416
|
-
height-bounded container (a popover/drawer) pass `gridMaxHeight` so the grid SCROLLS and the toolbar
|
|
417
|
-
pins below it; omit it in free-flow layouts (a form field) where the grid grows. Reach for the
|
|
418
|
-
lower-level pieces below only when you need custom chrome:
|
|
419
|
-
Capture with `<FileDropzone onFiles accept label hint dropLabel height>` (drag-over lights the
|
|
420
|
-
accent; click falls back to a picker); display what landed with **`<FileGrid files uploads>`** ABOVE
|
|
421
|
-
the dropzone (existing files are the content; the dropzone sinks to the bottom as the "add more"
|
|
422
|
-
affordance — only the empty state leads with it). `FileGrid` is the upload-aware grid: `files` are
|
|
423
|
-
the saved/completed `DisplayFile`s, `uploads` is the LIVE add-queue (`FileUpload[]`) — it interleaves
|
|
424
|
-
both and renders each in-flight tile itself — a LABELED status overlay (uploading spinner · "Retrying"
|
|
425
|
-
· "Paused" · "Upload failed" + a retry button · "Can't upload" for a dead/empty file) — so you never
|
|
426
|
-
hand-map an upload to a `FileThumbnail`. Localize the labels with `labels.upload` (an
|
|
427
|
-
`UploadStatusLabels`; the Lotics `@lotics/ui-internal` adapter already injects vi/en). Make it CRUDable
|
|
428
|
-
by wiring its callbacks: `onFilePress`
|
|
429
|
-
→ set a `number|null` index that drives `<FileGalleryModal files activeIndex onIndexChange>` — a
|
|
430
|
-
FULL-SCREEN viewer with a toolbar (filename · counter · actions — download, optional
|
|
431
|
-
`onOpenExternal`/`onRemove` · close-✕), prev/next, ESC, and rotate. The toolbar is RESPONSIVE: on a
|
|
432
|
-
phone (`useScreenSize().small`) the actions collapse into a `⋯` `ActionMenu` so the close-✕ never
|
|
433
|
-
overflows off-screen (its popover portals inside the modal's own `PortalHost` — a raw `<Modal>` lacks
|
|
434
|
-
one, which is why an earlier inline-less popover rendered behind the overlay). The 90° rotate controls
|
|
435
|
-
for images are NOT in the toolbar — they FLOAT as a pill overlaid on the image (every screen size).
|
|
436
|
-
`onDisplayRemove`/`onUploadRemove`
|
|
437
|
-
→ drop the file / cancel the upload (the grid renders a ✕ on each tile automatically); `onRetry` /
|
|
438
|
-
`onRetryAll` for failed uploads. In a sandboxed custom-code app the toolbar's "open in new tab" can't
|
|
439
|
-
pop a window — pass `onOpenExternal` wired to the SDK's `openExternal` (omit it elsewhere and the
|
|
440
|
-
action hides). PDF/Word/Excel/CSV all render INLINE here (pdf.js / `@lotics/docx` / `@lotics/xlsx`,
|
|
441
|
-
lazy) — no native `<iframe>` viewer, so they work inside the cross-origin app iframe. Keep
|
|
442
|
-
label+grid+dropzone on a `gap` (a bare `CardBody` has none). By default `FileGrid` tiles FILL the
|
|
443
|
-
container width — a uniform size (≥ `minItemWidth`, default 96) so full rows span the width and reflow
|
|
444
|
-
as it changes; a short last row keeps that size and left-aligns (no stretched tiles, no empty cells).
|
|
445
|
-
Pass `columns` for a fixed column count, or `itemSize` for exact fixed-size tiles. For a dense strip
|
|
446
|
-
in a FIXED-width container that must NOT wrap, pass `singleRow` — it fits as many tiles as the width
|
|
447
|
-
allows and collapses the rest into a clickable "+N" overflow tile (`onOverflowPress(hiddenCount)`,
|
|
448
|
-
which the host wires — e.g. open the gallery at the first hidden file). This is the responsive overflow
|
|
449
|
-
pattern (avatar stack / Finder); prefer it over a hardcoded `maxVisible`, which can't adapt to width.
|
|
450
|
-
Under the hood it composes `FileThumbnail` (the completed tile — right surface per
|
|
451
|
-
MIME: image thumbnail · a doc tile with the `FileBadge` centered + a single-line filename · media
|
|
452
|
-
card; also takes `isTemplate` to overlay a TMPL marker) and `UploadingThumbnail` (the in-flight tile);
|
|
453
|
-
reach for either directly only when hand-rolling a NON-grid layout. **When the filename must be
|
|
454
|
-
readable, use `FileRow`** — a horizontal LINE (`FileBadge` or a `placeholder` + the FULL name + a meta
|
|
455
|
-
line + a composable `trailing` slot). `onPress` makes the whole row a pressable door (open the file);
|
|
456
|
-
`trailing` (a status `Badge`, an action, a remove ✕) stays an independently-pressable sibling. For
|
|
457
|
-
attachment lists / message files / document checklists where a square tile truncates the name. Never hand-build a drop well, a file grid, the upload tiles, or a preview
|
|
458
|
-
modal. In a custom-code APP, don't hand-roll the upload+preview state either — `@lotics/app-sdk`
|
|
459
|
-
`useAttachments()` gives the chat's instant-preview lifecycle (local object-URL now, `file_id` on
|
|
460
|
-
complete); map each `AttachedFile` to a `FileUpload` (ready → `{ status: "complete", id, file: {…} }`,
|
|
461
|
-
else `{ status, id, filename, mimeType: mime_type, previewUrl: preview_url }`) and feed `FileGrid`'s
|
|
462
|
-
`uploads` — `onUploadRemove={remove}`.
|
|
463
|
-
|
|
464
|
-
**Gated file CRUD is a PATTERN, composed locally — not a sealed kit component.** When a file field must
|
|
465
|
-
GUARD deletion (operator-facing, accidental delete is a real risk), DON'T use `FileGrid` (its per-tile
|
|
466
|
-
✕ is exactly the stray-tap risk you're guarding against) — compose it in the app from the kit pieces;
|
|
467
|
-
don't reach for a one-size widget (the action set + layout vary per app — the host frontend's
|
|
468
|
-
`files_editor` and each app compose their own). The recipe: `FileDropzone` (add) + `FileThumbnailGrid
|
|
469
|
-
files selectedIds onFilePress` (pass `selectedIds` ONLY in select mode) + `FileGalleryModal` (view +
|
|
470
|
-
rotate + persist) + a `Dialog` confirm, all driven by **`useSelectionMode()`** — the reusable LOGIC. The
|
|
471
|
-
gating rule: **NO per-thumbnail ✕** — delete is **Select → ⋯ menu → Delete → confirm**, so a stray tap
|
|
472
|
-
never removes a file. Three reusable primitives back it (the logic is shared; the action bar + layout +
|
|
473
|
-
copy stay local):
|
|
474
|
-
- **`useSelectionMode()`** (`@lotics/ui/use_selection_mode`) — `{ active, selected, enter, exit, toggle,
|
|
475
|
-
toggleAll }`, an agnostic multi-select state machine (string ids; pairs with the grid's `selectedIds`).
|
|
476
|
-
- **`shareOrDownloadFiles(files, { title, credentials })`** (`@lotics/ui/share_or_download`) — `navigator.share({ files })`
|
|
477
|
-
(mobile → Save to gallery / send to an app), else individual `downloadFileFromUrl` — **never a ZIP**.
|
|
478
|
-
It shares the BYTES (fetches each URL → `File`), so URL expiry afterward is moot; the share path needs the
|
|
479
|
-
host iframe to grant `allow="web-share"` (else it falls back to download). Best for FAST urls (presigned
|
|
480
|
-
R2). **For SLOW urls (auth-gated proxy with a server round-trip), split it:** `prepareShareFiles(files,
|
|
481
|
-
{credentials})` fetches the `File[]` BEFORE the gesture (on menu-open), then `shareFiles(File[])` runs
|
|
482
|
-
`navigator.share` synchronously on the tap — **iOS Safari rejects `share()` if a slow fetch burns the
|
|
483
|
-
tap's transient activation**, which silently lands in the download fallback. `prepareShareFiles` throws on
|
|
484
|
-
a fetch failure (log it, don't swallow); `shareFiles` returns `"shared"|"cancelled"|"unsupported"`.
|
|
485
|
-
- **`rotateImageToBlob(url, degrees)`** (`@lotics/ui/rotate_image`) — canvas-bake a 90° rotation into a
|
|
486
|
-
NEW blob for re-upload (`useImageRotation` is view-only; this is how a rotation is persisted). Pair with
|
|
487
|
-
`FileGalleryModal`'s `onPersistRotation`/`persisting` (the ✓ shown on a rotated image).
|
|
488
|
-
|
|
489
|
-
### Stage gates — tiered by weight
|
|
490
|
-
A transition that needs NO input is one click. 1–3 quick fields → a POPOVER FORM anchored to its
|
|
491
|
-
action button (title + one-line stake + `FormField`s; confirm in `PopoverFooter`). A
|
|
492
|
-
destructive/exception path (anything touching money or locks) → a `Dialog` (consequence in prose,
|
|
493
|
-
danger confirm + cancel in `DialogFooter`) or an `Alert.alert(title, message, [{cancel},
|
|
494
|
-
{destructive}])` confirm. Never an inline expanding panel for a gate — it shifts layout and loses
|
|
495
|
-
the "this is a gate" framing.
|
|
496
|
-
|
|
497
|
-
---
|
|
498
|
-
|
|
499
|
-
## AI workflows — AI proposes, the human decides
|
|
500
|
-
|
|
501
|
-
The AI surfaces share ONE law: the agent never commits — it **proposes**, the human
|
|
502
|
-
accepts / edits / dismisses, the deterministic app applies. The agent owns judgment
|
|
503
|
-
(recognition, estimation, intent→parameters); the app owns geometry, math, and the write.
|
|
504
|
-
Compose the surfaces as a loop, and reach for the right one by job:
|
|
505
|
-
|
|
506
|
-
**Which AI surface — decide by the OUTCOME's shape, not the task's topic.** Three shapes:
|
|
507
|
-
|
|
508
|
-
1. **Field writes** (extract, match, rank, classify → the record changes) → **in-app agent**
|
|
509
|
-
(`useAgentRun`) + **`ChangeReview`**. The commits must obey the diff law (Keep/Drop per
|
|
510
|
-
change, app-workflow writes, bounded app authority), and the operator wants one button +
|
|
511
|
-
a review — never a prompt box.
|
|
512
|
-
2. **Evidence for an in-app decision** (cross-check, audit, tie-out → nothing is written; the
|
|
513
|
-
operator acts on what was found) → **in-app agent** + **`Finding`**. Still bounded and
|
|
514
|
-
prompt-free (an optional instructions brief at most): the brief is fixed, the output is
|
|
515
|
-
structured display-only findings read IN the record's context, and there is no conversation
|
|
516
|
-
to have — the loop closes when the operator acts in the app. Chat would add prompting and
|
|
517
|
-
tear the findings away from the record they judge.
|
|
518
|
-
3. **A file or an open-ended answer** (edit this document, draft from context, explain) →
|
|
519
|
-
**hand off to the chat agent** (`askAi` in `@lotics/app-sdk`). The loop is multi-turn with
|
|
520
|
-
no output schema, judged by looking — and the chat harness already owns it: preview beside
|
|
521
|
-
the thread, version chains, branching, session memory. An in-app "edit chat" would
|
|
522
|
-
re-implement all of that inside every app.
|
|
523
|
-
|
|
524
|
-
The Document desk's Use-AI fork IS this table as UI: Extract data (1) · Cross-check (2) ·
|
|
525
|
-
Edit with AI (3) — one entry point, three outcome shapes.
|
|
526
|
-
|
|
527
|
-
- **Command / compose** — `Composer`, the adaptive command surface. Empty + minimal (no `footerRight`
|
|
528
|
-
model picker, no `pills`, no `files`), it's a COMPACT PILL: an optional `actionsButton` (attach) left,
|
|
529
|
-
the input, a send arrow right (Enter sends, Shift+Enter for a newline) — the same geometry
|
|
530
|
-
`AgentProgress` morphs into. Type past one line, or add files, and it EXPANDS: the input lifts onto
|
|
531
|
-
its own row, the `files` slot (`FileThumbnail`s, each `onRemove`-able) and `pills` stack above, and
|
|
532
|
-
the buttons drop to a footer row (the expansion latches until the text is cleared). For a run that
|
|
533
|
-
needs a FILE + a prompt together (attach a photo, review/remove it, add a note, THEN send), fill
|
|
534
|
-
`actionsButton` + the `files` slot and set `sendDisabled` so Send fires with an attachment and/or
|
|
535
|
-
text (`onSend(text)` — the host holds the uploaded file id). **Never auto-run an agent on attach** —
|
|
536
|
-
attaching HOLDS the file for review; the human presses Send. The canvas (`tpl_dieline`) uses it this
|
|
537
|
-
way; while running the host swaps `Composer` for `AgentProgress` (same pill geometry, so it reads as
|
|
538
|
-
a morph).
|
|
539
|
-
- **Show the work** — `AgentRun`: a live feed of the agent's work as a TIMELINE. The prop is an
|
|
540
|
-
ORDERED `items` array — `{type:"text"}` prose (rendered as `Markdown`, a trailing ▍ while live),
|
|
541
|
-
`{type:"reasoning"}` thinking, and `{type:"step"}` tool/step calls, in the order they happened (a
|
|
542
|
-
real run is think → text → a burst of calls → more text, NOT all text on top of a flat step list).
|
|
543
|
-
**Progressive disclosure** — the feed shows the label + state; the detail is revealed on demand:
|
|
544
|
-
`reasoning` renders COLLAPSED (a muted "Thinking" row, press to reveal the Markdown), and a `step`'s
|
|
545
|
-
`input`/`output` are hidden in the row but open in a press-to-reveal **peek** (auto-built Input /
|
|
546
|
-
Output `JsonPanel`s; an `error` status + `errorText` paint the row red and show the reason there).
|
|
547
|
-
Pass an explicit `peek` node to render that reveal yourself (it overrides the auto panel); `detail`
|
|
548
|
-
is OPTIONAL — a short human summary (a count), never invented prose or raw I/O (that's `input`/`output`).
|
|
549
|
-
Consecutive `step` items fold into ONE activity group: while the agent is mid-tools the tail group is
|
|
550
|
-
a SINGLE pulsing row whose label swaps in place as each call fires (no growing stack of dots); once
|
|
551
|
-
prose resumes the group settles into a persistent "{final action} · {n} steps" HEADER (a `complete`
|
|
552
|
-
terminal dot — distinct from the filled `done` step dots) that STAYS PUT and rolls the calls out
|
|
553
|
-
BELOW it on press. The run ALWAYS ends on the agent's text (no global terminal node). A `step` with
|
|
554
|
-
`kind:"tool"` carries the RAW tool name, resolved via a built-in map + an optional `labelForTool`
|
|
555
|
-
override (localize THERE — the kit stays English); `stepsLabel` localizes the "{n} steps" suffix.
|
|
556
|
-
Fed natively by `@lotics/app-sdk` `useAgentRun().items` (reasoning + per-tool I/O + state come for
|
|
557
|
-
free — no hand-assembly). Transparent work, NEVER a bare spinner. On a canvas/composer app reach for
|
|
558
|
-
`AgentProgress` — `AgentRun` collapsed into a floating pill (avatar + current step) that EXPANDS on
|
|
559
|
-
press; the composer morphs into it while running, and reveals again when done.
|
|
560
|
-
- **Review before apply — ONE surface, the compound `ChangeReview` family.** Every "the agent
|
|
561
|
-
proposes → the human accepts / edits / dismisses → nothing auto-applies" surface composes from
|
|
562
|
-
twelve pieces in three groups. **Frame**: `ChangeReview` (context provider + stack; hairline
|
|
563
|
-
dividers between `Change` sections — and `ChangeFields` rules between fields) ·
|
|
564
|
-
`ChangeReviewHeader` (the SECTION heading — md semibold; keep it lean: never repeat the record
|
|
565
|
-
the dialog is already about) · `ChangeReviewActions` (the commit bar — pin it in the
|
|
566
|
-
`DialogFooter`/`DrawerFooter`; **Keep all** bottom-left presses every pending decision — pass
|
|
567
|
-
`onAcceptAll` when field state lives in the host; the "N of M kept" counter reads HERE beside it,
|
|
568
|
-
never in the header; Apply disables at 0 kept or via `applyDisabled`). **Sections**: `Change`
|
|
569
|
-
(heading + free body + optional verbs + collapse — omit both callbacks for display-only) ·
|
|
570
|
-
`ChangeLabel` · `ChangeSummary` (the collapsed row's content) · `ChangeReasoning` (the agent's
|
|
571
|
-
quiet hairline aside — only when not self-explanatory). **The grammar**:
|
|
572
|
-
- **`ChangeFields` + `ChangeField`** — THE field unit. `ChangeFields` is the OPEN form (the
|
|
573
|
-
section IS the record — an extract dialog's one order: no card chrome). Each `ChangeField`:
|
|
574
|
-
label (sm, muted — the DetailRow convention) · the `−` band when replacing (`before`) · the value — `ChangeValueInput`
|
|
575
|
-
by default (the green `+` band; pressing it edits IN THE BAND — a borderless input with
|
|
576
|
-
identical type metrics, so nothing shifts; `unit="pcs"` fixes the suffix outside the editable
|
|
577
|
-
core — type the number, never the unit) or any input · conflict `candidates` as full-width decision rows + the localized
|
|
578
|
-
"Type another value" third option (`custom*` props; the outcome band is read-only via
|
|
579
|
-
`valueReadOnly` — the decision comes from picking, never from editing the band; gate with
|
|
580
|
-
`keepDisabled`) · row `reasoning` LAST · per-field **Keep/Drop** bottom-right
|
|
581
|
-
(`status`/`onKeep`/`onDrop`/`onUndo`). A pure REMOVAL is `before` with an empty value — the
|
|
582
|
-
`−` band alone. A decided field collapses to the compact one-row card (mark · label →
|
|
583
|
-
`summary` · Undo).
|
|
584
|
-
- **`ChangeRecord`** — THE item card for SETS of records (order lines): `id` (it registers in
|
|
585
|
-
the review context like a `Change`), `tone` add/edit/remove, the tinted header band with the
|
|
586
|
-
localized op word (Add/Edit/Delete · Thêm/Sửa/Xóa) + title, body = its `ChangeField`s, and
|
|
587
|
-
**the verb level follows the decision level**: add/remove carry ONE card Keep/Drop (fields
|
|
588
|
-
none — editable parts, one write); edit carries NO card verbs (only its changed fields, each
|
|
589
|
-
deciding for itself — dropping a field narrows the update diff). Collapses via `summary`.
|
|
590
|
-
- **`ChangeBand`** — the raw diff band (aligned `+`/`−` marker column, light emerald/red, dark
|
|
591
|
-
text) every piece builds from; reach for it directly for custom strokes.
|
|
592
|
-
**The scaling boundary (known, deliberate)**: `ChangeFields` + `ChangeRecord` cards cover one
|
|
593
|
-
record through ~10; a BULK review (30+ imported rows — a bank statement, an Excel import) needs a
|
|
594
|
-
dense form that does not exist yet — design it against the first real migration, not
|
|
595
|
-
speculatively.
|
|
596
|
-
**The laws**: ONE verb pair everywhere — Keep/Drop, localized; never rename per shape (Apply is
|
|
597
|
-
the only outcome-named button). Candidate rows carry the VALUE only; provenance = `Sources` at
|
|
598
|
-
the VERY bottom of the review (after every change; or omit it — lean beats decorated). The HOST owns every decision in plain `useState`; the family owns the
|
|
599
|
-
mechanics (collapse + Undo, the counter, Keep-all, apply gating). Never apply a field with no
|
|
600
|
-
value — gate unresolved conflicts. Editing IS the review.
|
|
601
|
-
- **Ask back** — `Clarify`: when the agent is unsure, it asks a question with quick-reply options
|
|
602
|
-
and PAUSES, instead of guessing wrong. Human-in-the-loop input mid-run.
|
|
603
|
-
- **Provenance** — `Sources`: openable chips saying where the output came FROM (records, a
|
|
604
|
-
document + page, a table), under any answer / summary / extracted value. Makes AI output
|
|
605
|
-
verifiable — show it on anything produced from data the agent read.
|
|
606
|
-
- **Session, not chat** — the outputs accrue as a history the user can CLEAR ("New session"); the
|
|
607
|
-
APP owns the evolving state, each run is a discrete task. (The agent may re-read the session for
|
|
608
|
-
"make it a bit less", but it's a run LOG, not a conversation transcript — that's why it's not a
|
|
609
|
-
chat.)
|
|
610
|
-
|
|
611
|
-
Ten shapes prove the range. The first four PRODUCE — pick by whether the work is a living artifact,
|
|
612
|
-
a conversation, a single pass, or generated prose:
|
|
613
|
-
- **Canvas** (`tpl_dieline`) — the page IS the design on a pannable/zoomable surface: it stays
|
|
614
|
-
CENTRED at every zoom (a floating zoom pill bottom-left), the FLOATING composer at the bottom (a
|
|
615
|
-
`Composer`: attach a photo → review/remove it → add a note → send, with `sendDisabled` allowing a
|
|
616
|
-
file-and/or-text submit — NOT auto-run on attach) morphs into `AgentProgress` while running, and a pinned PARAMS PANEL (a label/value field-card
|
|
617
|
-
in live-edit mode, carrying the single Download; minimizes to a pill) floats centre-right. Panel, zoom pill and composer
|
|
618
|
-
float ON TOP — they never shift the design. (Floating layers use a `pointerEvents:"none"` wrapper
|
|
619
|
-
with the interactive child set `"auto"`; RN-Web ignores `"box-none"` in style, so a full-width
|
|
620
|
-
wrapper would otherwise eat clicks on the canvas behind it.) Change the design by prompt ("5 mm
|
|
621
|
-
taller") OR by editing a param directly — either re-flows it in place. For a design/document the
|
|
622
|
-
user shapes over time.
|
|
623
|
-
|
|
624
|
-
The next six output STRUCTURE the first four can't — when the answer is a panel of facts, a queue of
|
|
625
|
-
decisions, or a ranked set, don't cram it into chat prose. Each pairs a template with a primitive:
|
|
626
|
-
- **Answer desk** (`tpl_lookup`) — describe the goods on the LEFT → the agent RANKS the
|
|
627
|
-
matching codes (NEAREST MATCHES, the top one Recommended) → pick one and its STRUCTURED answer pins on
|
|
628
|
-
the RIGHT (a verdict header + an exact breakdown + the policies + sources). Input → matches → pick,
|
|
629
|
-
NOT a single confident verdict: classification is ambiguous, so the alternatives are first-class and
|
|
630
|
-
picking a different one changes the duty; refining the description re-ranks. For look-up-and-explain:
|
|
631
|
-
tariff/HS, fee lookup, policy Q&A, a spec/compliance desk. (NOT a chat with the answer in a bubble.)
|
|
632
|
-
|
|
633
|
-
- **Document desk** (`tpl_documents` — THE go-to for document-driven records) — the record's files
|
|
634
|
-
block feeds ONE "Use AI" entry that FORKS into the two document tasks, each a specialized run with
|
|
635
|
-
a task-pure result: **Extract** (files read → fields already matching fold into one quiet line →
|
|
636
|
-
every add / update / conflict a `ChangeField` (struck before · bordered input · candidate chips),
|
|
637
|
-
the record's current value a first-class choice — plus proposed new lines as record-body `Change`s
|
|
638
|
-
→ one outcome-named `ChangeReviewActions` commit) and
|
|
639
|
-
**Cross-check** (documents compared against the record and each other → ranked `Finding`s —
|
|
640
|
-
severity · title · the prominent metric · sources — separated by hairlines; the findings ARE
|
|
641
|
-
the outcome the human acts on — no phantom "record verdict" write, a persisted check-status
|
|
642
|
-
goes stale on the next edit). The fork carries an OPTIONAL instructions field — the user steers what the agent
|
|
643
|
-
checks or extracts, so `Finding` serves ANY file-based AI request, not just the stock
|
|
644
|
-
cross-check. Every file list in the flow opens the full-page `FileGalleryModal`. Plus **Create documents**: a readiness checklist (unchecked by
|
|
645
|
-
default, missing inputs called out) → generate → the files land back on the record. Never merge the
|
|
646
|
-
two AI tasks into one mixed output — the fork is the design.
|
|
647
|
-
- **Triage** — an inbox the agent classified + routed is a `ChangeReview` of `Change`s (body: the
|
|
648
|
-
item + the agent's call; verbs Accept / Dismiss; Accept-all covers the high-confidence sweep).
|
|
649
|
-
- **Compare / ranked pick** — one `ChangeField` whose `candidates` carry the ranked options
|
|
650
|
-
(`description` = the score/reason, `source` where it came from); the human picks one. Quotes,
|
|
651
|
-
carriers, suppliers, plans.
|
|
652
|
-
|
|
653
|
-
The AI vocabulary has **no purple accent and no gimmick glyphs** (no sparkles) — but it is NOT
|
|
654
|
-
monochrome: **colour is used where it carries meaning, not for decoration.** What the violet sparkle
|
|
655
|
-
used to carry now reads structurally — **provenance** is an uppercase microlabel naming the artifact
|
|
656
|
-
(`PROPOSED` · `MATCH` · `MISMATCH` · `SUGGESTED EDIT` · `QUESTION`), and the agent's **reasoning** is
|
|
657
|
-
a left-ruled margin note (a hairline rule + muted text), quoted apart from the facts and the human's
|
|
658
|
-
controls — while **status/severity/diffs use functional colour** the way the rest of the kit does:
|
|
659
|
-
- **Confidence** — a 3-tick meter + the full phrase ("High confidence" / "Medium" / "Low"), emerald /
|
|
660
|
-
amber / zinc by level; `labels` to translate (one phrase per level).
|
|
661
|
-
- **Stepper / AgentRun nodes** — progress dots on a spine: `current` a **pulsing accent ring** (white
|
|
662
|
-
centre), `done` a filled accent dot + **white check**, `upcoming` a faint **grey** ring, the terminal
|
|
663
|
-
`complete` a **blackish ring + black check**, `warning` **amber**. Default accent is neutral ink
|
|
664
|
-
(`color` themes it); `AgentRun` keeps the neutral default — an active group's tail pulses (`current`),
|
|
665
|
-
a settled group is a `done`/`warning` row, and there is no global terminal node (the run ends on text).
|
|
666
|
-
- **ChangeBand** — the removed value on the light **red** band with the `−` marker, the incoming
|
|
667
|
-
value on the light **emerald** band with `+` (the GitHub-diff idiom, markers in one aligned
|
|
668
|
-
column); everything else in a review stays neutral — a decided row reads a single emerald check,
|
|
669
|
-
an add/remove record card wears the quiet tone wash (50 body · 100 header · 200 border). Colour
|
|
670
|
-
marks the change, never the chrome.
|
|
671
|
-
|
|
672
|
-
Card chrome (borders, microlabels) stays neutral — colour marks the *state*, never the container.
|
|
673
|
-
The **composer keeps its icons**: `Composer` compact is a single-row pill with an optional circular
|
|
674
|
-
attach `actionsButton` + a circular send `Button`; expanded it adds a `FileThumbnail` attachment row
|
|
675
|
-
(each `onRemove`-able) above the input; `AgentProgress` is the `WaveAvatar` pill. "No icons"
|
|
676
|
-
was only ever about the review surfaces' sparkles/severity glyphs, not functional affordances.
|
|
677
|
-
|
|
678
|
-
---
|
|
679
|
-
|
|
680
|
-
## Composition grammar
|
|
681
|
-
|
|
682
|
-
- **Canvas**: full-bleed `colors.zinc[50]` ScrollView; content column `maxWidth` 880–1040,
|
|
683
|
-
centered, `padding: 28`. Cards float on the canvas — never white-on-white.
|
|
684
|
-
- **Heading vocabulary — ONE construct per altitude, no drift.**
|
|
685
|
-
· **Page band**: one per screen — `Text size="xxl" weight="semibold"` title (`#`) + one `sm muted`
|
|
686
|
-
subtitle that says what the screen decides. Right side: the screen's ONE primary action and/or
|
|
687
|
-
a period filter — never a summary Badge (those belong to the KPI strip).
|
|
688
|
-
· **Card header** (a card's own title band, or separate banded cards): `CardHeader` +
|
|
689
|
-
`CardHeaderTitle` (`sm semibold`; `info` when the title alone doesn't define the numbers).
|
|
690
|
-
· **Section title** — ONE construct: `Section` > `SectionHeading` > `SectionHeadingTitle`
|
|
691
|
-
(lg semibold + optional muted `description`/`info`), everything left-aligned at the column
|
|
692
|
-
edge. The page column is a `SectionStack` — it owns the between-section law (`gap: 32` +
|
|
693
|
-
a bare hairline separating one section from the next); the `Divider` NEVER goes directly
|
|
694
|
-
under the heading, which orphans the title from its own content. Inside a DRAWER, a full
|
|
695
|
-
multi-section WORKSPACE uses the real
|
|
696
|
-
`SectionHeading` (lg) — its meta + CTA slots carry each section's count and action; a
|
|
697
|
-
`Text size="sm" weight="semibold"` stand-in is only for small sub-groups. Do NOT wrap each section in its own `CardHeader`, do NOT
|
|
698
|
-
hand-roll sm-semibold lead lines on a flat page. NEVER a bare eyebrow as a section title.
|
|
699
|
-
· **The heading ramp is FIXED — the markdown ladder, no size knobs.** `#` = xxl
|
|
700
|
-
(`RecordSummary` / `PageHeader` title) → `##` = xl (`SectionHeadingTitle`, always) →
|
|
701
|
-
`###` = lg (`SubsectionHeadingTitle`, always) → body sm. One outline on every surface;
|
|
702
|
-
never restyle a heading level per-page.
|
|
703
|
-
· **Subsection title** — the level BELOW a section on a long record surface: `Subsection` >
|
|
704
|
-
`SubsectionHeading` > `SubsectionHeadingTitle` (`###` lg semibold; siblings — a `Badge`, a
|
|
705
|
-
`SectionHeadingMeta`, an action — ride the heading row's right edge). Sibling subsections
|
|
706
|
-
stack in a `SubsectionStack` (24 + hairline between groups — no margins, no hand-rolled
|
|
707
|
-
dividers); a headingless `Subsection` is the section's lead group. Group leads INSIDE a
|
|
708
|
-
subsection's rows are `md medium` at most. Do NOT hand-roll `Text weight="semibold"` group
|
|
709
|
-
leads inside a section, and never promote a subsection to its own section-level heading
|
|
710
|
-
just to separate it.
|
|
711
|
-
· **Eyebrow / label** (`<Text size="xs" color="muted" weight="medium">` — **sentence case, NEVER
|
|
712
|
-
`transform="uppercase"`**): a small quiet label above or beside content — an artifact tag
|
|
713
|
-
("Proposed", "Question", "Suggested edit"), a field name, a metric caption, a minor one-line
|
|
714
|
-
label. **All-caps is banned — it reads as shouting and the redundant uppercasing of every little
|
|
715
|
-
label is the #1 thing that makes a surface feel templated.** Sentence case + medium weight, full
|
|
716
|
-
stop. (A COLORED status word — a verdict like "Mismatch" / "Resolved" — keeps the same xs/medium
|
|
717
|
-
shape with a status `color`, still not uppercase.) This applies to every hand-written label; don't
|
|
718
|
-
reach for a wrapper component either, just write the `Text`.
|
|
719
|
-
· **Gate header**: a `Dialog` uses `DialogHeaderTitle`; a popover form uses `Text size="sm"
|
|
720
|
-
weight="semibold"` + an optional `xs muted` subtitle.
|
|
721
|
-
- **Time-constrained data gets a period filter** in the header band — `DateRangeFilterField`, never
|
|
722
|
-
a static period badge. Every period-dependent number MUST follow the selection. Pass `includeTime`
|
|
723
|
-
when the time-of-day matters: the trigger previews the chosen time (locale-aware 24h/12h) and the
|
|
724
|
-
hour/minute selects label themselves from the `labels` prop (`hour`/`minute`/`dayPeriod`).
|
|
725
|
-
- **Keyboard & focus — use `tabIndex`, never `focusable`.** RN-Web's `Pressable` silently ignores
|
|
726
|
-
`focusable` (it writes its own `tabIndex`), so set a pressable control's tab-stop status with
|
|
727
|
-
`tabIndex={0 | -1}` (`focusable` only works on a plain `View`/`TextInput`). Roving widgets
|
|
728
|
-
(Tabs/SegmentedControl/RadioPicker) keep ONE stop at `0`, the rest `-1`. And NEVER let a FOCUSED
|
|
729
|
-
control unmount — a conditional pointer affordance that vanishes on use (e.g. an "apply suggested
|
|
730
|
-
value" pill shown only while a field is empty) must be `tabIndex={-1}`, or the browser drops focus
|
|
731
|
-
to `<body>` and the next Tab jumps to the page's first focusable. Full rules + the keyboard test:
|
|
732
|
-
`docs/accessibility.md` → Focus & tab order.
|
|
733
|
-
- **Focus rings are per-component, never global.** There is NO global focus CSS — `index.css` only
|
|
734
|
-
resets the native outline. Every interactive control paints its OWN ring; nothing rings unless it
|
|
735
|
-
opts in. Three ways, pick by base:
|
|
736
|
-
- **`FocusRingPressable`** (`@lotics/ui/focus_ring_pressable`) — a `Pressable` that rings on keyboard
|
|
737
|
-
focus, WITHOUT the hover wash; the base for any control whose hover affordance is its BORDER not a wash
|
|
738
|
-
(inputs/selects, the inline editors, cells, tiles, nav buttons, menu options). Forwards all
|
|
739
|
-
`PressableProps`; its state-fn `style` exposes `hovered`/`focusVisible` so the control paints its own
|
|
740
|
-
border-hover. Reach for this before hand-rolling a Pressable + the hook.
|
|
741
|
-
- **`PressableHighlight`** — set the opt-in **`focusRing`** prop (or read `state.focusVisible` from
|
|
742
|
-
its style-fn / children for a bespoke treatment, e.g. `CardSelectItem` which rings on hover/press/focus).
|
|
743
|
-
- **`useFocusRing`** (`@lotics/ui/use_focus_ring`) — the underlying hook for inputs / custom surfaces.
|
|
744
|
-
Returns `{ focusVisible, focused, focusProps }`; spread `focusProps`, apply
|
|
745
|
-
`focusVisible && { boxShadow: FOCUS_RING }` (comma-join any existing box-shadow). `useFocusRing({ always: true })`
|
|
746
|
-
for text-like inputs/selects (ring on ANY focus, as browsers do for typing-capable fields); plain
|
|
747
|
-
controls omit it (keyboard-only).
|
|
748
|
-
The ring is the shared **`FOCUS_RING`** token (`@lotics/ui/control_surface`, `0 0 0 2px zinc-900`). A
|
|
749
|
-
mouse-opened popover trigger (Select/Combobox/InlineSelect/InlineDatePicker) wears the SAME token on
|
|
750
|
-
its open state so it reads identically to a keyboard-focused control. **Coverage is a contract:** EVERY
|
|
751
|
-
interactive control must ring (a focusable control with no focus treatment is a bug — the per-Pressable
|
|
752
|
-
audit is the gate); a surface that shouldn't ring is a non-control → `tabIndex={-1}`, not a missing ring.
|
|
753
|
-
- **Hover intensifies a control's OWN resting signature — never a foreign affordance. Pick the base by WHAT THE SURFACE IS; never a raw `Pressable`.** The one rule behind every interactive control:
|
|
754
|
-
- **Fields** (type into / pick from) — `TextInputField`, `Picker`, `Select`, `Combobox`'s input,
|
|
755
|
-
`NumberInput`, `DateField`/`TimePicker`, `DateRangeFilterField`, the inline editors
|
|
756
|
-
(`InlineSelect`/`InlineDatePicker` via `InlineEditView`) — signature is a BORDER → hover DARKENS it to
|
|
757
|
-
`HOVER_BORDER` (a borderless in-cell field REVEALS one). Build on **`FocusRingPressable`** (ring + a11y
|
|
758
|
-
+ a `state.hovered`/`focusVisible` style-fn, NO wash); layer the hover-border AFTER `style` so it wins
|
|
759
|
-
over the caller's resting edge; put the open ring (`FOCUS_RING`) on the open state. **A field NEVER
|
|
760
|
-
greys its content.**
|
|
761
|
-
- **Actions** — `Button`, `IconButton` — signature is a FILL → hover DARKENS the fill (own colour logic).
|
|
762
|
-
Never a border, never a wash.
|
|
763
|
-
- **Pills / toggles** — `Chip`/`ChipGroup`/`FilterChip` (via `chipSurfaceStyle`), `SegmentedControl`,
|
|
764
|
-
`Tabs`, `Switcher`, `RadioPicker` — signature is a pill SURFACE → hover WASHES it (white→zinc-100);
|
|
765
|
-
selected adds a ring/fill.
|
|
766
|
-
- **Surfaces** — `PressableRow`, `MenuButton`, list/menu items, `CardSelectItem`, `Card`, `Accordion` —
|
|
767
|
-
signature is a row/card SURFACE → hover WASHES it. Build on **`PressableHighlight`** — the grey wash IS
|
|
768
|
-
the affordance (its whole job; it's in the name).
|
|
769
|
-
- The bug this prevents: a FIELD built on `PressableHighlight` inherits the wash AND its own border →
|
|
770
|
-
it greys *and* animates its edge while its siblings only border-hover (`Select`/`DateRangeFilterField`/
|
|
771
|
-
the inline editors did exactly this pre-7.x). Reach for `PressableHighlight` on a bordered/input control
|
|
772
|
-
and you've made it.
|
|
773
|
-
- **Cards are banded — and composable** (all from `@lotics/ui/card`):
|
|
774
|
-
```tsx
|
|
775
|
-
<Card style={{ padding: 0 }}>
|
|
776
|
-
<CardHeader><CardHeaderTitle info="what this shows">Title</CardHeaderTitle><CardHeaderMeta>12</CardHeaderMeta></CardHeader>
|
|
777
|
-
<CardBody>…</CardBody> {/* repeat with <Divider/> between */}
|
|
778
|
-
<CardFooter>hint(flex:1) + actions</CardFooter>
|
|
779
|
-
</Card>
|
|
780
|
-
```
|
|
781
|
-
Never nest cards; never hand-compose a title or footer band. `info` is standard, not garnish.
|
|
782
|
-
- **Flat work execution & data capture vs carded monitoring.** A `Card` is ALWAYS a bordered,
|
|
783
|
-
lifted container — there is no "flat card". MONITORING / dashboard screens group with cards (and
|
|
784
|
-
the `KPIStrip` band). WORK-EXECUTION screens — registers, lists, worklists — AND **data-capture /
|
|
785
|
-
record screens (forms, wizards, billing, settings, inline records) are FLAT**: content sits
|
|
786
|
-
DIRECTLY on the white canvas with no Card wrapper; separation comes from the row wash, gaps,
|
|
787
|
-
`SectionHeading` + `Divider` rhythm. Reach for a Card only
|
|
788
|
-
where a screen holds a DISTINCT region that must be told apart from another; a single register or
|
|
789
|
-
list filling the screen needs none. ONE carve-out inside a flat form: **repeating SUB-OBJECTS
|
|
790
|
-
(line items) are each their own small Card** (`padding: 16`), so discrete items
|
|
791
|
-
read as discrete objects; hairlines between them blur into one run-on list.
|
|
792
|
-
- **Summaries — two altitudes, never mixed.** A DASHBOARD opens with the boxed stat band
|
|
793
|
-
`<KPIStrip items={[{label, value, trend?, caption?, tone?, info?}…]}>` (a `Card` of `KPICard`
|
|
794
|
-
columns). A REGISTER/LIST (work-execution) screen instead carries a LIGHT inline
|
|
795
|
-
`<SummaryLine items={[{label, value, tone?, info?}…]}>` BELOW its toolbar — a small `Metric` + a
|
|
796
|
-
muted label per item, recomputed from the FILTERED rows so it always describes WHAT'S SHOWN. Both
|
|
797
|
-
are INFORMATIONAL — they never filter or navigate (that's the tabs/chips' job); if there are no
|
|
798
|
-
cross-cutting numbers, drop them. Do NOT put a `KPIStrip` on a register page or a `SummaryLine` on
|
|
799
|
-
a dashboard. Card stat rails use `KPICard`.
|
|
800
|
-
- **Numbers**: free-standing numerals `<Text tabular>`. Money: `formatMoney(n)` from
|
|
801
|
-
`@lotics/ui/format_money` (`compact` for strip captions; it emits a non-breaking space before `₫` —
|
|
802
|
-
normalize `\s` in test asserts). Dates: `formatDate(value, opts)` from
|
|
803
|
-
`@lotics/ui/format_date` — the date-VALUE formatter. `format` (date STYLE) ∈ `date` (22/05/2026) ·
|
|
804
|
-
`medium` (22 thg 5, 2026) · `long` (22 tháng 5, 2026) · `dayMonth` (22 thg 5) · `monthYear`
|
|
805
|
-
(Tháng 5 2026); **`time: true` is orthogonal** — prepends the 24h time to ANY style
|
|
806
|
-
(`14:30 22/05/2026`, `14:30 22 tháng 5, 2026`); `compact` drops the year on `date`. It's `Intl` +
|
|
807
|
-
the product's conventions (stable `/`, naive-ISO parse, 24h time-first, `emptyLabel`) — never
|
|
808
|
-
hand-roll a date via `Intl`/`padStart` for value display. An order the presets lack (e.g.
|
|
809
|
-
time-first `HH:mm dd/MM/yyyy` in a doc builder): `parseDate` + assemble the missing order —
|
|
810
|
-
never a regex re-implementation. (Exempt: a component's own internal
|
|
811
|
-
chrome — a calendar's header/weekday/a11y labels, a gantt axis — renders its own set.)
|
|
812
|
-
- **Every number is a door** (except the KPI strip). A component that summarizes records leads to
|
|
813
|
-
the records behind it when pressed — switch to the filtered list, expand in place, or navigate.
|
|
814
|
-
Expansion happens IMMEDIATELY below the pressed element — the composable `Accordion` family
|
|
815
|
-
(`AccordionHeader`/`AccordionTitle`/`AccordionMeta`/`AccordionContent`); header-only = a plain row
|
|
816
|
-
of identical rhythm, so lists mix expandable + static rows. The body is **flush** with the
|
|
817
|
-
header's left edge (aligns with the title — like a `Section` body, no indent). `AccordionHeader`
|
|
818
|
-
is a layout slot: `AccordionTitle`/`AccordionMeta` is the compact list-row heading, but for a
|
|
819
|
-
**collapsible Section** (configurable font/level/description + count) compose the SAME
|
|
820
|
-
`SectionHeadingTitle`/`SectionHeadingMeta` inside it — one heading family across static + collapsible.
|
|
821
|
-
- **No dead rows.** Every listed record is actionable. PRIMARY entity rows press-open the workspace
|
|
822
|
-
`Drawer` (sequenced); read-only drill-downs expand via `Accordion` or glance via `Peek`; every
|
|
823
|
-
other row gets an `ActionMenu` (⋯ → MenuButton items: destructive last + **confirmed** via
|
|
824
|
-
`Alert`). The whole surface is the door: register rows are `PressableRow` (full-bleed wash incl.
|
|
825
|
-
nested controls), Divider-separated:
|
|
826
|
-
· **Register (default, `variant="register"`)** — the rounded full-width hover/open/`marked` wash
|
|
827
|
-
spans the whole row (incl. nested controls); content sits on the 20px gutter so a `Table` header
|
|
828
|
-
+ cells align. The Divider BETWEEN rows is the resting separation. The COLUMNAR register is
|
|
829
|
-
`Table`/`TableRow`/`TableCell` (columns once → header + widths can't drift; ONE hairline under the
|
|
830
|
-
header; the rows below it Divider-separated; a sortable column shows its `SortHeader` sort glyph
|
|
831
|
-
ALWAYS so it reads as sortable). A `TableRow` with no `onPress` renders a STATIC read-only row (no
|
|
832
|
-
hover wash / pointer cursor) so `Table` also serves read-only tabular data (a fee breakdown, a spec
|
|
833
|
-
sheet), not just interactive registers. A pressable `TableRow` REQUIRES `accessibilityLabel`
|
|
834
|
-
("Open …") — its keyboard door is an empty overlay with no content to derive a name from. Make it SELECTABLE with the `Table` `leading` gutter + `selectAll`
|
|
835
|
-
slot — a `CheckboxInput` per `TableRow` (`leading`) + a select-all in the header band, the ticked
|
|
836
|
-
rows `marked`, paired with a `FloatingActionBar`; the selection state is the **`useSelection()`**
|
|
837
|
-
hook (`selected`/`count`/`toggle`/`setAll`/`allSelected`/`indeterminate`/`clear` — gating which
|
|
838
|
-
rows CAN be ticked stays with you: pass only the selectable ids to `setAll`/`allSelected`/
|
|
839
|
-
`indeterminate`). `tpl_item_list` is the reference. Paginate OUTSIDE (slice + `Pagination` in the
|
|
840
|
-
`CardFooter`).
|
|
841
|
-
· **`variant="bleed"`** (legacy) — px-20 square, `Divider`-separated; only for an edge-to-edge data
|
|
842
|
-
grid that genuinely wants hard rules.
|
|
843
|
-
· **`variant="inset"`** — grouped lists in a padded container (Accordion drill-downs).
|
|
844
|
-
Right-hand columns align only if every trailing element is FIXED-width (give each trailing action
|
|
845
|
-
a fixed `width`, so amount/status columns don't jitter).
|
|
846
|
-
- **Row actions are ALWAYS-VISIBLE siblings, never hover-revealed, never nested.** A pressable row =
|
|
847
|
-
a role-less surface (the hover/press wash, via DOM `mouseenter` so it covers the whole row) + an
|
|
848
|
-
accessible "Open …" door (a button) + trailing actions (⋯, remove ✕, edit) as SIBLINGS of
|
|
849
|
-
the door — a button must not contain another button, so the action is independently pressable and
|
|
850
|
-
the ✕ isn't swallowed by the row press (`FileRow` and `PressableRow` are the references). The door
|
|
851
|
-
wraps the row body ONLY when that body is non-interactive by construction (`FileRow`'s name/meta);
|
|
852
|
-
where the body is arbitrary app content — `TableRow`'s cells, which legitimately carry a `Link` or
|
|
853
|
-
a popover trigger — the door is an EMPTY absolutely-positioned sibling under the cells (tab stop +
|
|
854
|
-
name + focus ring; mouse rides the surface), so a control in a cell never nests inside it. Actions
|
|
855
|
-
stay **visible** — a hover-only action is invisible to keyboard and touch users. The ONLY exception
|
|
856
|
-
is a DENSE tree/register where a persistent per-row action would clutter (e.g. the DB sidebar):
|
|
857
|
-
reveal on hover **OR focus-within**, keeping the action in the DOM + tab order and gating only its
|
|
858
|
-
opacity, so it's still keyboard-reachable. Do that locally — there is no shared hover-reveal
|
|
859
|
-
primitive (the old `HoverAction` made the keyboard-inaccessible anti-pattern easy and was removed).
|
|
860
|
-
- **Master-detail = list + workspace `Drawer` with sequencing.** Pressing a PRIMARY entity row
|
|
861
|
-
opens the record workspace in a `Drawer` with `onPrev`/`onNext`/`position` ("3/24") over the
|
|
862
|
-
visible ordering (←/→ built in). Key the drawer body by record id so per-record state resets on
|
|
863
|
-
step. Facts are `DetailRow`s; the commit bar is a `DrawerFooter`. The open row shows a selected
|
|
864
|
-
highlight (and `marked` — a resting blue tint — for a bulk-ticked row). `Peek` is ONLY for
|
|
865
|
-
secondary references, never the primary row press.
|
|
866
|
-
- **Two work shapes, no side panels.** A work screen is ONE of two shapes — never a persistent
|
|
867
|
-
side-by-side split (it dies on a phone, and the "detail" of a list belongs in a `Drawer`/sheet,
|
|
868
|
-
not a second column). (1) **Work each** — a flat register/list; press a row → its workspace
|
|
869
|
-
`Drawer` (above). (2) **Act on many** — a flat list with a leading `CheckboxInput` per row + a
|
|
870
|
-
select-all `CardHeader` band, plus a bottom-pinned `<FloatingActionBar count label onClear>`
|
|
871
|
-
carrying the bulk action(s) while ≥1 row is ticked (`tpl_item_list`); a row
|
|
872
|
-
that can't take the action has NO checkbox. Bar weight gradient, left → right: the built-in
|
|
873
|
-
Clear is muted (the quietest act), destructive bulk = `danger-secondary` + icon, secondary
|
|
874
|
-
bulk = `secondary` + icon, ONE primary CTA last — never repeat the count in a CTA title; the bar's label already carries it. When a region genuinely must sit beside the list (a
|
|
875
|
-
source/remainder summary, a capacity picker) make it a TOP summary header or move it into a
|
|
876
|
-
Popover/sheet at the decision point — not a standing column.
|
|
877
|
-
- **One control radius** — every interactive control (`Button`, the inputs/selects/pickers,
|
|
878
|
-
`SearchInput`, `MenuButton`, `Tabs`, the `SegmentedControl` track, `ChipGroup`/`FilterChip`/
|
|
879
|
-
`Chip`) wears `CONTROL_RADIUS` (10, from `control_surface.ts`) — never a literal. Containers
|
|
880
|
-
are deliberately larger (`Card` 16, the review card 12). An element NESTED inside a padded control
|
|
881
|
-
(a segmented thumb, a combobox chip, an ✕ on a chip) takes `CONTROL_RADIUS − padding` so its corner
|
|
882
|
-
stays CONCENTRIC with the parent — set it equal and the inner corner bulges past the outer curve.
|
|
883
|
-
Round-by-function controls (avatars, status dots, switches, sliders, progress bars, `IconButton`)
|
|
884
|
-
stay full (999); a small square toggle (`Checkbox`, 24px) keeps a proportional small radius (6 —
|
|
885
|
-
the same fraction of its box that 10 is of a 40px control), not the band radius. No capsules —
|
|
886
|
-
`Button` has no pill `shape`.
|
|
887
|
-
- **One view-control vocabulary** (all 40px, `sm` labels, in one wrapping band):
|
|
888
|
-
· `SearchInput` is THE search box — a white `TextInputField` preset (the shared `CONTROL_RADIUS`
|
|
889
|
-
like every other input, a thin zinc-300 border, the leading search glyph + a clear ✕). It reads as search by its
|
|
890
|
-
ICON + white fill, not a distinct shape, so a toolbar of controls stays one consistent band.
|
|
891
|
-
Never a bare `TextInputField` + search icon.
|
|
892
|
-
· `ChipGroup` is THE one-of-N lens (≤ ~10 options the user flips between; include "All"; counts in
|
|
893
|
-
the label). Filters ONE list to a SUBSET — including by process stage / lifecycle state.
|
|
894
|
-
· `FilterChip` is THE secondary-dimension filter — a compact chip that opens a composed popover
|
|
895
|
-
editor (`OptionList multi` / `RangeSlider` / `Counter` / date range). Single-select closes on
|
|
896
|
-
pick via the `{({close}) => …}` render prop.
|
|
897
|
-
· `ColumnFilter` is the TYPED column filter — give it a `FilterableColumn` (`text` / `number` /
|
|
898
|
-
`select`) + a controlled `ColumnFilterValue` and it renders the right editor (contains / range /
|
|
899
|
-
multi-select) inside a `FilterChip`, with `columnFilterToConditions` mapping the value to query
|
|
900
|
-
predicates. Reach for it — ONE compact chip per dimension — when a register filters on SEVERAL
|
|
901
|
-
columns (origin, destination, carrier, type…). Never hand-roll a row of bespoke chips/filters,
|
|
902
|
-
and never use a permanently-expanded `ChipGroup` for a secondary dimension.
|
|
903
|
-
· `Combobox` is the SEARCHABLE picker — reach
|
|
904
|
-
for it over `ColumnFilter` when the value is ONE of a large/growing set you type to narrow (a
|
|
905
|
-
port, a customer) rather than a short fixed list. Pass `recentOptions` (the same option list) so
|
|
906
|
-
a click opens the full list, then typing filters. The **From → To route picker** is two of them
|
|
907
|
-
(`icon="map-pin"`, `clearable`) with an `arrow-right` between — flight-search style.
|
|
908
|
-
· `Tabs` switch between VIEWS/sections (different content/layout) — never a subset of one list. A tab earns a `status` dot (optional `TabOption.status`) only when its area needs attention (a blocker / missing item) — the resting state has none.
|
|
909
|
-
`SegmentedControl` chooses a MODE/PARAMETER of the SAME view (2–4 peers, no panel swap).
|
|
910
|
-
· **Layout**: search + secondary filters LEFT, the primary CTA RIGHT, in ONE band. A `ChipGroup`
|
|
911
|
-
one-of-N lens is fine ONLY when the band has no `SearchInput`; once a search is present, the
|
|
912
|
-
status filter becomes a `Select`/`FilterChip` dropdown — never a row of pills competing with the
|
|
913
|
-
search.
|
|
914
|
-
- **Footer actions align RIGHT.** `PopoverFooter`/`DialogFooter`/`DrawerFooter` default
|
|
915
|
-
`align="end"` — commit at the right edge, secondary to its left. Never left-flow a Save/Submit.
|
|
916
|
-
- **Empty result**: `<EmptyState message hint? icon? action?>` — never a bare muted Text.
|
|
917
|
-
- **Inline status**: `<Callout tone="info|success|warning|error|neutral">` — compound (like Card):
|
|
918
|
-
compose `CalloutTitle`/`CalloutText`/`CalloutActions` inside. `Callout` is INLINE; `Alert` is the
|
|
919
|
-
blocking modal; `Badge` is a one-word pill.
|
|
920
|
-
- **One primary action per surface.** Everything else secondary/muted; destructive = `danger`
|
|
921
|
-
styling + explicit label. (A grouped BUILDER section — line items — MAY give its own Add action
|
|
922
|
-
`primary`; it sits at a different altitude than the form's one terminal commit.)
|
|
923
|
-
- **Button labels carry no trailing ellipsis** ("Assign", not "Assign…"). **Button color is
|
|
924
|
-
VALENCE/RISK, never category**: the ladder `muted < secondary < primary` is the emphasis axis,
|
|
925
|
-
`danger` marks destructive — that's the whole axis. No "success"/green. Decision UIs put
|
|
926
|
-
positive/negative color on the STATUS (dot) and verdict (colored `Text`), not the buttons.
|
|
927
|
-
- **Color discipline** — every status / data-viz / accent color comes from a NAMED helper, never a
|
|
928
|
-
hand-picked `colors.<family>[shade]`: `solid(name)` = the 500 (dots, series, meter accents);
|
|
929
|
-
`tint(name, α)` = a wash; `ramp(name, count)` = N shades of ONE family for a coherent dimension.
|
|
930
|
-
A status is ONE family NAME (`{ color: "emerald" }`) and every weight derives from it. One accent
|
|
931
|
-
per screen purpose (blue=pipeline, emerald=money, red=danger, amber=waiting). Direct `colors.*` is
|
|
932
|
-
reserved for NEUTRALS (`zinc`, `border`, `white`, the `*[50]` selection washes).
|
|
933
|
-
**TEXT never wears `solid()`** — the 500 is a FILL shade (dots, series). A status word/number uses
|
|
934
|
-
`Text`'s valence tokens: `color="danger"` (red-900) / `"warning"` (amber-700) / `"success"`
|
|
935
|
-
(emerald-700) — AA on white. Map a dynamic `ColorName` to a token at the call site; hue nuance
|
|
936
|
-
(rose vs orange) stays on `Badge`s. Links use `Link`/`TextLink` (blue-600), never `solid("blue")`.
|
|
937
|
-
- **Status indicators have a WEIGHT — match to prominence, never to a metric.** colored `Text` (a
|
|
938
|
-
verdict WORD in a header) < `Badge variant="dot"` (a categorical STATE in a scannable column /
|
|
939
|
-
legend) < `Badge` tonal pill (a record's PROMINENT status, a drawer/detail header). A register's
|
|
940
|
-
dense rows read lighter — the row's primary status is `variant="dot"`, its drawer twin tonal. A
|
|
941
|
-
`Badge` is never a metric value.
|
|
942
|
-
- **Typography**: only the `Text` primitive (size/weight/color/transform). Uppercase tracking is
|
|
943
|
-
built in.
|
|
944
|
-
- **Touch & whitespace**: anything pressable is ≥ 40px tall (8px min gap between pressables). 16px
|
|
945
|
-
between cards, 24–28 canvas padding, 16–20 inside bands, 10–12 between content lines. Density
|
|
946
|
-
comes from alignment + hierarchy, not cramming.
|
|
947
|
-
|
|
948
|
-
---
|
|
949
|
-
|
|
950
|
-
## Worked examples (templates)
|
|
951
|
-
|
|
952
|
-
Full worked-example screens ship in the package — **read the source at
|
|
953
|
-
`node_modules/@lotics/ui/examples/<name>.tsx`** (pure `@lotics/ui` + mock data, each a usable
|
|
954
|
-
recipe for a screen JOB; copy and adapt). Pick by the job:
|
|
955
|
-
|
|
956
|
-
- **Analytics** — `tpl_dashboard` (KPIStrip + trends + attention list) · `tpl_stock`
|
|
957
|
-
(10k-record faceted funnel) · `tpl_tower` (live `StatusGrid` wallboard) · `tpl_pivot` (the
|
|
958
|
-
`Matrix` cross-tab desk: one dimension × another, number + heat per cell + row/col/grand totals,
|
|
959
|
-
press a cell to drill the list behind it) · `tpl_rollup` (hierarchical totals).
|
|
960
|
-
- **Work** — every screen is FLAT (no Card) and ONE of two shapes: a register/list you
|
|
961
|
-
WORK EACH of (press a row → workspace `Drawer`) or a list you ACT ON MANY of (tick rows →
|
|
962
|
-
`FloatingActionBar`) — never a side-by-side panel. `tpl_item_list` (THE canonical register and the
|
|
963
|
-
consolidated work-list: search + a status `Select` + facet `FilterChip`s LEFT, the New CTA RIGHT in
|
|
964
|
-
one toolbar row, a `SummaryLine` below, then a sortable, Divider-separated `Table` where every row
|
|
965
|
-
carries a leading checkbox; a select-all band, footer `Pagination`, and a `FloatingActionBar` for
|
|
966
|
-
the bulk action over the ticked rows; a per-row PRINT button (the trailing action column) is the
|
|
967
|
-
row's primary action and the ⋯ its overflow; a row that can't take the bulk action gets a DISABLED
|
|
968
|
-
checkbox (here an "Awaiting docs" record — the same gating as a blocked run line) — a row press opens
|
|
969
|
-
the PRODUCTION workspace `Drawer`: a Details `DetailTable` showing every inline field type
|
|
970
|
-
(text · `InlineMemberSelect` · dates incl. optional-time · dot-select · money number · the
|
|
971
|
-
`InlineTagSelect` tag field · `InlineStatic` + System badge), a LINKED CASES section (the
|
|
972
|
-
customer's other records as `ListItem`s — the aligned row compound (title + description at
|
|
973
|
-
the column edge, its own 8px outdent; badge · fee · chevron right) — pressing one PUSHES an
|
|
974
|
-
EDITABLE workspace for that record inside the drawer via the hosted `ScreenRouter`; the
|
|
975
|
-
drawer HEADER swaps to back + the pushed id, back pops with scroll preserved), a Files section with its
|
|
976
|
-
OWN Add CTA (`FileRows` CRUD via `pickFiles` + an EXPECTED document as a ghost `FileRow
|
|
977
|
-
placeholder` with a Request action), the Payment `Ledger` (peekable fee rows + the Record-payment popover), an ACTIVITY section (the CRM touch-log shape: OUTCOME PILLS — `ChipGroup`, optional, tap-to-deselect — over a MULTILINE note (the note IS the record and the one requirement), an always-visible optional Follow-up `InlineDatePicker`, an "Attach files" secondary (`pickFiles` → removable `FileThumbnailGrid` in the composer), the PRIMARY Log saying WHY when disabled; below, a labelled "History" `Timeline` of outcome-typed items, newest first — an entry's attachments ride its expandable `details` as a thumbnail grid whose press opens the shared `FileGalleryModal`), and a closing `DangerZone`. Detail rows show trailing CTAs where earned (Phone → Call, Due → Today) — the case where `trailingWidth` IS set. This ONE template subsumes the old approvals / dispatch / batch / run
|
|
978
|
-
screens: register, per-row action, gated selection, and act-on-many in one) · `tpl_pick` (guided `ScanField` run — a single-focus task column with
|
|
979
|
-
the pick path collapsed into an `Accordion` below) · `tpl_allocate` (`RemainderMeter` split — a
|
|
980
|
-
source/remainder summary header over the invoice list + Apply) ·
|
|
981
|
-
`tpl_record` (THE record surface, create-then-refine: "New" = a small
|
|
982
|
-
dialog with the identity essentials + find-or-create → lands as a Draft; the surface IS the
|
|
983
|
-
editor — `RecordSummary` header over its key-fact `DetailTable` + a Details `DetailTable`
|
|
984
|
-
(one shared column grid, auto-stacking when narrow), a CUSTOMER section (TWO states, no
|
|
985
|
-
swap mode: attached = the read-only-first card — Edit/Done live in the SECTION HEADING and
|
|
986
|
-
swap the fields to inline chips, where a trailing Fetch fills contact + city from the
|
|
987
|
-
tax-ID registry, un-gating billing; Remove (danger) sits low and detaches the link → the
|
|
988
|
-
find-or-create search, whose custom row opens the create Dialog rendering the SAME
|
|
989
|
-
inline-chip table + Fetch), a FILES section (dropzone add, gallery preview, delete),
|
|
990
|
-
and a BILLING
|
|
991
|
-
section OWNED BY Accounting (gated until the record reaches that stage) in the INLINE vocabulary (each invoice a hairline-set band: charge amounts as chips
|
|
992
|
-
with the list price as ghost placeholder + a one-tap "Standard …" suggestion pill while
|
|
993
|
-
unset, "How paid…" select chips, NO per-band totals — the collect band owns the number; the
|
|
994
|
-
band's action row sits BOTTOM-RIGHT as hint → status dot → Issue, the hint naming exactly
|
|
995
|
-
what blocks (fees → method → the customer gate); an ISSUED invoice keeps its lines editable
|
|
996
|
-
and offers Re-issue — a new lookup code replaces the old, confirmed in the same Dialog;
|
|
997
|
-
validated receipt; separate refundable deposit) — and
|
|
998
|
-
the lifecycle is a HANDOFF CHAIN (Sales → Operations → Accounting → Closed): a TASKS section
|
|
999
|
-
speaking the FULL Task-list grammar — a clearable Group-by `FilterChip` (Desk/Assignee/Status)
|
|
1000
|
-
+ Assignee/Status filter chips derive the groups (empty groups drop; desk heads keep their
|
|
1001
|
-
owner dots, assignee heads are `MemberChip`s), the add-a-task capture at the TOP landing on
|
|
1002
|
-
the current desk, rows with per-task `InlineMemberSelect` assignees — EVERY desk's rows edit
|
|
1003
|
-
(the stage gates the handoff and Billing, never task editing; planning ahead on a later desk
|
|
1004
|
-
is normal work) — OWNER dot tags per section, per-stage handoff CTAs NEVER blocked (open
|
|
1005
|
-
tasks warn + carry over); "New" is ONE CLICK → a fresh record at Sales; ALSO the settings shape: Preferences switch rows + `DangerZone`; a LEFT OUTLINE RAIL (`MenuButton` + `useSectionNav`)
|
|
1006
|
-
jumps between the sections. Absorbed the
|
|
1007
|
-
old order-form, inline-record, intake, settings, billing, and quick-capture templates) ·
|
|
1008
|
-
`tpl_tasks` (the QUICK LIST — Apple-Reminders shape for personal lists: `CheckCircle` rows you tick +
|
|
1009
|
-
expand in place to edit (`Inline*`, stable row — the meta summary never shifts on expand), colour-dot
|
|
1010
|
-
tags, an attachments field (`FileThumbnailGrid` → tap a tile to the gallery for download + confirmed
|
|
1011
|
-
remove + an `Add attachment` `pickFiles` button), search + a clearable group-by (✕ → ungrouped) + tag
|
|
1012
|
-
filter chips, and a primary Draft-from-notes CTA `Composer → AgentRun → ChangeReview`) ·
|
|
1013
|
-
`tpl_task_board` (the COLUMNS shape: a search · group-by · filter toolbar over a grouped, sortable
|
|
1014
|
-
grid of inline-editable cells — assignee/due/status/tags set directly, a files cell (thumbnail glance →
|
|
1015
|
-
popover grid → gallery), a **dynamic action column** (a per-row underlined action LINK driven by each
|
|
1016
|
-
task's `action` descriptor — attach/approve/open, each wired in a real app to its OWN workflow: one
|
|
1017
|
-
board, a different action per row; the link's pressable fills the cell to match the inline editors),
|
|
1018
|
-
a trailing **`ActionMenu`** (the ⋯ overflow — a `danger` Remove + confirm; CUSTOM pressable
|
|
1019
|
-
cells (files, the action link) hover with the control-surface BORDER reveal (`HOVER_BORDER`),
|
|
1020
|
-
the same language as the inline-editor cells beside them — never their own background wash
|
|
1021
|
-
today, more row actions later), per-group add rows that pre-set the group's field, and the same
|
|
1022
|
-
Draft-from-notes CTA). Both `tpl_tasks` and `tpl_task_board`: filters/search left, primary CTA right, one
|
|
1023
|
-
row; group-by holds only real dimensions, ✕ clears to ungrouped (no "Nothing" option). **`tpl_task_board` is the inline-managed
|
|
1024
|
-
grouped table — MODERATE data you manage in view (group/sort/filter/edit-in-place, renders all
|
|
1025
|
-
rows). For thousands+ you BROWSE, that's `tpl_item_list`'s paginated register, not this.** It builds
|
|
1026
|
-
on the `DataGrid` primitive (the inline-managed grouped-table shape); it owns
|
|
1027
|
-
only its data, toolbar, columns and per-group add. There is NO Task component — a task ROW varies
|
|
1028
|
-
too much to bake into one, so rows compose `CheckCircle` + `InlineTextInput` + inline-editor cells
|
|
1029
|
-
as **sibling** controls in a plain `View`. NEVER wrap the whole row in a `Pressable accessibilityRole="button"`
|
|
1030
|
-
(for tap-to-expand) with those controls nested inside: RNW renders the row AND each control as a `<button>`,
|
|
1031
|
-
so you get `<button>`s inside a `<button>` — invalid DOM + the inner controls drop out of the keyboard tab
|
|
1032
|
-
order. Give "expand" its own affordance (a trailing `Pressable` over the meta/chevron), as `tpl_tasks` does.
|
|
1033
|
-
- **Finance** — `tpl_statements` (the income statement, balance sheet, and cash flow statement
|
|
1034
|
-
in ONE statement grammar: right-aligned column captions over fixed money columns, items
|
|
1035
|
-
indented under group headers, a hairline rule above every subtotal, the grand total
|
|
1036
|
-
DOUBLE-RULED, negatives in accounting parentheses, per-cell currency-free (the meta line says
|
|
1037
|
-
VND once), no bars/charts. The three statements TIE — net income → retained earnings, closing
|
|
1038
|
-
cash = the balance sheet's cash, the loan repayment moves the debt line — keep mock books
|
|
1039
|
-
reconciled or the template teaches the wrong thing) · `tpl_report` (scope-first lookup report —
|
|
1040
|
-
a header period + one-of-N dimension search drives KPIs → aligned facets w/ a `maxRows`
|
|
1041
|
-
long-tail toggle → paginated register → export).
|
|
1042
|
-
- **Scheduling** — `tpl_calendar` · `tpl_attendance` · `tpl_shifts`.
|
|
1043
|
-
- **Agents** — *produce*: `tpl_dieline` (the design CANVAS: photo → stream → the dieline
|
|
1044
|
-
reveals CENTRED on a pannable/zoomable surface, the floating composer morphing into `AgentProgress`,
|
|
1045
|
-
a pinned live-edit params panel centre-right; prompt OR edit a param to iterate) ·
|
|
1046
|
-
*Structure*: `tpl_lookup` (the ANSWER desk: converse left,
|
|
1047
|
-
a pinned verdict panel right; a follow-up refines it) ·
|
|
1048
|
-
`tpl_documents` (the DOCUMENT DESK — files on a record → the Use-AI fork: extract = the open
|
|
1049
|
-
`ChangeFields` review (adds/updates/removal/conflict) + proposed order lines as `ChangeRecord`
|
|
1050
|
-
cards, Keep-all + one outcome-named Apply; cross-check = display-only `Change` findings → a
|
|
1051
|
-
recorded verdict; plus the Create-documents readiness checklist).
|
|
1052
|
-
|
|
1053
|
-
---
|
|
1054
|
-
|
|
1055
|
-
## Full component inventory (import as `@lotics/ui/<module>`)
|
|
1056
|
-
|
|
1057
|
-
text ·
|
|
1058
|
-
markdown (Markdown — the single canonical markdown renderer for chat, apps, and `AgentRun`; rich on web via react-markdown/remark-gfm with copyable tables, plain-text on native; takes a markdown `children` string; import `@lotics/ui/markdown.css` once for styling) ·
|
|
1059
|
-
card (Card · CardHeader · CardHeaderTitle · CardHeaderMeta · CardBody · CardFooter) ·
|
|
1060
|
-
section_heading (Section · SectionHeading · SectionHeadingTitle · SectionHeadingMeta · Subsection · SubsectionHeading · SubsectionHeadingTitle — the card-less twin of the Card family, compound, owns no margin; spacing via the Section gap (12, fixed), no body component. `SectionHeadingTitle` is ALWAYS `##` (xl semibold; `weight="medium"` opt-down only) + `info` for an ⓘ provenance popover after the title, same as `CardHeaderTitle.info`. `SubsectionHeadingTitle` is the `###` lg-semibold level-3 title of a named group inside a section — heading-row siblings ride its right edge; the heading ramp is FIXED: # xxl / ## xl / ### lg, no size props) ·
|
|
1061
|
-
section_stack (SectionStack · SubsectionStack — divided stacks that own the between-block law: a fixed beat + hairline Divider BETWEEN blocks, skipping null children (56 for the flat page's Sections, 24 for a section's Subsections); stop hand-rolling gap + `<Divider />` pairs) · badge ·
|
|
1062
|
-
option_badge (OptionBadge — a select value as its configured colored badge) ·
|
|
1063
|
-
member_chip (MemberChip — avatar + name; the universal person render) ·
|
|
1064
|
-
member_select (MemberSelect — a Picker of MemberChip options; the member picker) ·
|
|
1065
|
-
status_badge · button · icon_button · link · text_link (TextLink — underlined text that's optionally an `onPress` action or an `href` link, or plain underlined text to wrap in your own pressable; the neutral counterpart to the fixed-blue `Link`) · chip (Chip — the generic pill: pressable when `onPress` (announces as a button; pass `accessibilityLabel` when children aren't self-describing text) + an absolutely-positioned dismiss ✕ sibling when `onDismiss` (its name = `dismissTooltip` ?? the `chip.remove` locale slice). Suggestion pills → `SuggestionChip`) · tabs (Tabs — switch between content sections; WAI-ARIA tablist + roving tabindex; each TabOption takes an optional `status` ColorName → a small attention dot before its label, for a tab whose area needs work) · segmented_control ·
|
|
1066
|
-
picker (native `<select>`, plain label-only single) · select (Select — rich/custom-rendered, single/multi, select-all, chips via `renderSelected` + `searchable` + `allowCustom` — the tag field is just a multi Select; opens `OptionList`) · option_list (OptionList — the ONE shared searchable listbox body every selector opens: single/multi, optional internal search, create row, keyboard + native-`<select>` typeahead; host it directly in a `Popover`/`Dialog` for a command palette) · combobox (COMPOUND single-select editable search: `Combobox` root + `ComboboxInput` + `ComboboxContent`, optional `ComboboxEmpty`/`ComboboxFooter`, `useCombobox()`; over the shared `useOptionList` engine; browses on focus; no `multi` — multi-value chips → `Select multi`) ·
|
|
1067
|
-
text_input_field · number_input · search_input · form_field · checkbox · checkbox_input · switch ·
|
|
1068
|
-
radio_picker · counter · range_slider · date_picker · date_range_filter_field · date_calendar (Calendar — the bare month grid: `mode="single"` or `"range"` ({start,end} — two months side by side on desktop), month/year pickers + arrows, localized weekday/month names via BCP-47 `locale`, `firstDayOfWeek` (default Monday), `ref.navigateToMonth`. The engine DatePicker/DateFilter wrap in field chrome — reach for it bare only when the calendar lives permanently on the surface, not behind a field) · form_switch (FormSwitch — the FormField-labeled twin of `Switch`: toggle + clickable `label` (pressing it flips the value) + optional `description`/`error`; the settings-form boolean row. A bare toggle in a cell/toolbar → `Switch`; a full-row menu toggle → `SwitchButton`) · form_text_input (FormTextInput — `FormField` (label / description / error / optional) wrapping a `TextInputField`; the one-line labeled text field. Controls without a Form* twin just wrap in `FormField`) · switch_button (SwitchButton — the full-ROW toggle: a PressableHighlight row (optional icon + medium title left, `Switch` pinned right) where the whole row IS the switch (`accessibilityRole="switch"`, the inner Switch read-only). The settings-panel/menu row toggle; vs `Switch` (bare control) and `FormSwitch` (form stack)) · use_form (useForm — THE batch draft-form state hook: `values` = `initialValues` + an edits overlay (a revalidation refreshes untouched fields, no sync effect), `validate` (sync/async, gates submit, editing clears the field's error), `onSubmit(values, helpers)` with a re-entry-guarded `submitting`, and `changes`/`changed` — the touched-fields diff that feeds DIFF-writes (send only edited fields); `setFieldValue` (curried or direct) + `setValues`/`setFieldError`/`reset`. Pairs with the Form* family for dialog/settings forms — the draft-validate-COMMIT-together twin of the self-persisting Inline* editors) · time_picker ·
|
|
1069
|
-
inline_text_input · inline_number_input · inline_select · inline_member_select · inline_date_picker ·
|
|
1070
|
-
inline_time_picker (the Inline* family — per-field editors on `inline_edit`'s `useInlineEdit` +
|
|
1071
|
-
`InlineEditView`; `InlineSelect`/`InlineMemberSelect` render the resting value like its option — `renderOptionContent` by default, `renderSelected` to override — a chip/badge at rest, not just text) ·
|
|
1072
|
-
inline_static (InlineStatic — a READ-ONLY value matching the Inline* box metrics EXACTLY (height, padding, 1px transparent border) so a non-editable field — a computed total, a system ID, a synced/locked value — aligns pixel-for-pixel in the same column; non-interactive, NOT a disabled input; `muted`/`tabular`/`align="right"` for a number column, `weight="medium"` to emphasise a total among plain rows) ·
|
|
1073
|
-
record_summary (RecordSummary — the identity band of a record detail/drawer: ONE row — `title` xl semibold tabular · `subtitle` sm muted · `status` Badge slot · optional `metric` {label,value,tone,note} pinned right, the band's ONE accent. The record's FIELDS never live in the header: compose them as `DetailTable`s in the sections below, the header's key facts as their own `DetailTable` right under the band on the same labelWidth/trailingWidth — `tpl_record` does this. Replaces hand-rolled record headers (mixed scales, several competing figures, color noise)) ·
|
|
1074
|
-
list · list_item · menu_button · menu_list_item · detail_row (DetailTable + DetailRow — the record field grid. DetailRow: label+value row for drawer/peek detail; in FORM mode (`labelWidth` set) the value column FILLS the row so a stack of inline editors all span the same width + none jumps wider on edit; optional `trailing` slot renders a right-side action/badge after the value (units belong IN the value via `InlineNumberInput format`). DetailTable: the compound parent of a row STACK — `labelWidth`/`trailingWidth`/`minHeight` (default 40, the inline-control grid) declared ONCE + the 6px row gap; with `trailingWidth` every row reserves the trailing column so value cells share one width and trailing items align at one x, like a table. RESPONSIVE with no prop: it measures its own container (onLayout, not the viewport — works inside a Drawer; the unmeasured first frame renders opacity-0 so the first PAINT is already in the right mode — no reshuffle as a drawer opens) and when the columns would crush the value cell it STACKS every row (the label above a full-width value row IN THE FORMFIELD LABEL GRAMMAR — medium, default ink — so narrow record surfaces and forms read as one vocabulary; trailing beside the value, gap 14. The components stay separate: FormField = draft controls validated + committed together; inline editors self-persist); raise `minValueWidth` (default 160) when a cell holds MORE than one editor (an amount+method pair) so the table stacks earlier. Two tables on one page share one grid by repeating the same labelWidth/trailingWidth. Worked example: `tpl_record`) · ledger (Ledger + LedgerGroup + LedgerRow + LedgerTotal — the record-level money list: charge/receipt GROUPS with sums in their headers, every figure on ONE right-aligned tabular column (all lines share the 8px inset), `peek` turns a row into a pressable door floating its particulars in an anchored popover (put links INSIDE the peek — never a button in a button; `reference` is the trailing-link alternative for static rows), LedgerTotal = the divider-set emphasized close with `zeroLabel` for settled. The financial-statement grammar at record density; worked example: `tpl_item_list` drawer) · danger_zone (DangerZone — the destructive section: a soft danger-tinted frame (`tint`/`solid`, never raw hex) + a danger heading + a description + a destructive action slot (children, e.g. a `danger` Button); sits APART at the bottom of a record/settings surface) · pressable_row ·
|
|
1075
|
-
check_circle (CheckCircle — the completion ring: an empty ring that springs to a filled check when done, distinct from the square checkbox; the task/to-do/checklist toggle) · checklist (Checklist + ChecklistRow — the record-scoped checklist COMPOUND: it owns GEOMETRY only (row minHeight 32, gap 12, ring/title alignment, ONE `trailingWidth` so assignee cells column-align) while content stays composed — `control` takes the CheckCircle (omit onChange = read-only ring), children the struck transparent InlineTextInput, `trailing` an InlineMemberSelect, `menu` the row's ⋯ options ({items: ActionMenuItem[], accessibilityLabel}) — Delete lives BEHIND the menu, danger-styled and last, never a bare ✕ (omit on read-only rows). NARROW surfaces (a drawer/peek checklist) put the editors on the `meta` line instead of `trailing` — the second line indents past the ring so the TITLE keeps the full width and stays readable; wide surfaces use `trailing`; never both. SUGGESTIONS are never rows: offer the commons as `SuggestionChip`s under the list (tap = materialize, ✕ = dismiss; a pill can't be mistaken for a task). Close the list with `CaptureRow`. There is deliberately NO monolithic Task component — richer task-management rows (tpl_tasks expand affordances, board cards) compose their own anatomy directly) · suggestion_chip (SuggestionChip — the dismissible SUGGESTION pill: an item the record could have but doesn't yet (a common task, an expected line) as a `Chip` whose press MATERIALIZES it (plus glyph + label, one tuned anatomy) and whose ✕ refuses it; filter out labels already present; suggestions never count in totals. Chrome via the `suggestionChip` locale slice) · capture_row (CaptureRow — the add-an-item row closing an editable list: dashed empty ring + borderless input on the item-title inset + a primary Save that appears on type; Enter commits too. Controlled: `value`/`onChangeText`/`onSubmit`. One shape wherever a list grows in place — task checklists, simple item lists) · action_menu ·
|
|
1076
|
-
floating_action_bar · filter_chip · column_filter (ColumnFilter — the typed per-column filter pill +
|
|
1077
|
-
columnFilterToConditions; for a register filtering on several columns) · chip_group · search_input ·
|
|
1078
|
-
sort_header · table · data_grid (DataGrid — the inline-managed grouped table: a grouped, sortable grid of LIVE inline-editor cells (`columns[].cell` → ANY field) + optional per-row `leading` (a CheckCircle) + `renderGroupFooter` (per-group add, align with the exported `gridRowStyle`) + `labels` (localize the sort-header a11y via `SortHeaderLabels`). Owns header/sections/rows; consumer owns data + sort/group/filter/collapse state + toolbar. Renders ALL rows — MODERATE data; 10k+ → the paginated `Table` register. Example: `tpl_task_board`) · pagination · accordion · stepper (Stepper + Step — done/current/upcoming/warning/complete progress on a track (horizontal) or spine (vertical); compound `<Step status>children` OR data `steps[]`+`current`; **navigable** via `Step.onPress` (both orientations — the whole step is the tap target) + `active` to wash the selected one, so it doubles as a section/phase switcher; the guided-run / agent-feed primitive — subsumes the old StepList) ·
|
|
1079
|
-
step_progress · timeline (heterogeneous event LOG — per-row icon + expandable details, models the past; NOT progress) · drawer (+ DrawerFooter) · dialog · modal (Modal + ModalHeader + ModalBody + ModalFooter — the full-bleed, edge-to-edge takeover: an OPAQUE surface that COVERS THE WHOLE SCREEN, so unlike Dialog (centered card WITH scrim) and Drawer (docked panel WITH scrim) there is nothing behind it to dim — NO scrim, NO backdrop. Lays children as a flex column: a pinned ModalHeader (eyebrow/title + an actions slot + close), a flex:1 scrolling ModalBody, a pinned ModalFooter (the commit bar, same chrome as DialogFooter/DrawerFooter). Reach for it for a focused capture / multi-step wizard / a console the user steps INTO, where surrounding chrome is a distraction; pick Dialog when the surface is a card the user can see context around) · screen_router (ScreenRouter + Screen + useScreenRouter — the SCREENS compound: a flat navigation stack (`navigate("/case/:id")` pushes, `goBack` pops, `canGoBack`, route `params`; stacked screens stay mounted `display:none` so scroll survives the round trip). Dialog BAKES a router in (`<Dialog><Screen route="">…`); ANY other container hosts the standalone `<ScreenRouter>` — and it wraps AROUND the container so the CHROME can read the stack: a Drawer drilling into a LINKED record swaps its header to a BACK IconButton + the pushed record's id while `canGoBack` (sequence ◀ ▶ hides — stepping the root from inside a linked record disorients), and the pushed `<Screen route="/case/:id">` is a REAL editable workspace with its own footer CTAs. Key the router by record id so stepping ◀ ▶ resets the stack. Worked example: `tpl_item_list` drawer) · popover (Popover + PopoverTrigger + PopoverContent — **NON-MODAL: the anchored popover has NO blocking overlay, so the rest of the page stays interactive; clicking another control both dismisses this popover AND activates that control in one click; clicking outside, scrolling an ancestor, or Escape dismisses. Only `small` (bottom sheet) is modal (scrim). PopoverContent already insets its body 12px; put content directly in it, NEVER add your own padding View (that double-pads). Title/actions via PopoverHeader / PopoverFooter**) · popover_nav (usePopoverNav + PopoverScreen + PopoverNavHeader — the popover's built-in mini-router: EVERY `Popover` provides the nav context (`navigate(route)` pushes, `goBack`, `currentRoute`, `canGoBack`; resets on close), `PopoverScreen route=""` is the root and screens render conditionally (unmounted when inactive — no scroll preservation), `PopoverNavHeader` is the title row whose back chevron auto-appears while `canGoBack` (`right` slot, `backLabel`). For a multi-screen menu inside ONE popover (an avatar/settings menu drilling into a sub-panel); route PATTERNS, `params`, and stacked-alive screens are `screen_router`'s job. Distinct from `Popover`'s plain `PopoverHeader` children container) · tooltip ·
|
|
1080
|
-
alert · peek · empty_state · completion_state · callout (Callout · CalloutTitle ·
|
|
1081
|
-
CalloutText · CalloutActions) · kpi_card · kpi_strip · summary_line (SummaryLine — the light inline summary of a register/list's FILTERED view, sits below the toolbar; NOT the boxed dashboard `kpi_strip` band) · metric · trend_chip · sparkline ·
|
|
1082
|
-
bar_chart · line_chart · pie_chart · ring_gauge · progress_bar · stacked_progress_bar · breakdown ·
|
|
1083
|
-
funnel (Funnel — conversion funnel: narrowing bars + the step rate as a bold aligned headline row, count below; `orientation` vertical|horizontal, `onSelect`/`selectedKey` press-to-drill; the subset/drop-off sibling of stacked_progress_bar's whole-split) ·
|
|
1084
|
-
status_grid (StatusGrid + StatusLegend) · heatmap (density cross-tab — colour-only, no numbers) · matrix (Matrix — the PIVOT cross-tab: band-compound `Matrix` root + `Matrix.Header` (corner + axis labels) + `Matrix.Grid` (`display` number|heat|both — the cells, pressable, the value IN the cell) + `Matrix.Totals` (row + column + grand) + `Matrix.Legend`; the data layer `matrixTotals`/`MatrixAxisItem`/`MatrixCellRef` import from `@lotics/ui/matrix_totals`. Pick over `Heatmap` when the NUMBER and totals matter, not just where it clusters) · legend_item · remainder_meter · allocation_row ·
|
|
1085
|
-
scan_field · file_dropzone · files_editor (FilesEditor — THE all-in-one attachment field: FileGrid + a toolbar (Upload primary · Select · Download all) that swaps into a batch SELECT mode (Select all · a Menu of Download/Share/Delete · Done; the per-tile ✕ is select-mode-only, never in the default view; `selectTileRemove={false}` drops even that so delete is menu-only) + built-in gallery (Download + inline preview; no "open in new tab") + Alert-confirmed remove; host wires `files` + `onAdd`/`onRemove` (+ optional `uploads`, `onShareSelected`, `readOnly`, `selectTileRemove`, `labels`, `galleryLabels`, `gridMaxHeight` — cap the grid height so it scrolls and the toolbar pins, for a popover/drawer); mirrors the frontend cell_files_editor. Use FileGrid/FileRows bare only when you own the chrome) · file_grid (FileGrid — the upload-aware grid: completed files + a live upload queue in one surface; FileUpload/PendingUpload types; the add-files default) · uploading_thumbnail (UploadingThumbnail — the single in-flight upload tile FileGrid renders; reach for it only when hand-rolling a non-grid upload layout) · file_thumbnail · file_thumbnail_grid · file_row · file_rows (FileRows — batteries-included file list: row press → built-in gallery + a ⋯ Download/Open-external/Remove menu; composes FileRow + ActionMenu + FileGalleryModal) · file_preview (FilePreview — the universal inline preview: image/PDF/video/audio + Word via `@lotics/docx` + Excel/CSV via `@lotics/xlsx`; the heavy engines (pdf.js · `@lotics/docx` · `@lotics/xlsx`) are LAZY (dynamic-imported, ~free until a doc of that type is opened) and SHIP AS `@lotics/ui` deps (7.14.0+) — custom-code apps get PDF/Word/Excel preview with ZERO extra install. Renders to canvas/DOM, never a nested iframe — works in the sandboxed app iframe) ·
|
|
1086
|
-
app_icon (AppIcon — the app's launcher tile: a brand-gradient square from a `themeColor` palette token (unknown → neutral zinc) holding any Lucide icon by runtime name via DynamicIcon; `size` sm|md. One render wherever an app shows — launcher, list, picker, settings) · dynamic_icon (DynamicIcon — any Lucide icon by RUNTIME name (kebab-case string), for user/config-chosen icons a compile-time `IconName` can't express; web lazy-loads per icon, unknown name → blank. Decorative — the enclosing control carries the accessible name. A fixed, code-chosen glyph → `Icon`) · group_avatar (GroupAvatar — the first letter of `name` in a zinc rounded square (`size`, default 40); the avatar for image-less entities — groups, organizations. A person → `Avatar`/`MemberChip`) · rotatable_image (RotatableImage — an image rotating in 90° steps that REFITS: at a quarter turn the box lays out in SWAPPED dimensions then rotates into place, so a rotated landscape fills the frame; the VIEW half of image rotation — `rotate_image` bakes the pixels for re-upload; FilePreview/ImageGallery compose it) · file_preview_types (PreviewLabels / FilePreviewProps / GalleryLabels — the shared label + prop contracts of the file-preview family; types only) · file_gallery_modal · image_gallery · use_selection (useSelection — always-on multi-select state for a register/list: the `selected` Set + `toggle`/`setAll`/`allSelected`/`indeterminate`/`count`/`clear`; selectability gating stays with the caller. The checkbox-always-visible counterpart to `use_selection_mode`) · use_selection_mode · use_section_nav (useSectionNav — scroll-spy for a LONG record surface with a left outline rail: keys in page order → {scrollRef, onScroll, register(key)→onLayout, jumpTo(key), activeKey}; rail items are `MenuButton`s (selected={activeKey===key}); section wrappers must be DIRECT children of the ScrollView content. On NARROW containers the rail becomes a PINNED bar naming the CURRENT section (the spy keeps it honest) that opens a full-page section-picker `Modal` (`MenuButton` list; Escape / the close control dismiss) — never a horizontal tab strip the thumb has to scroll. Worked example: `tpl_record`) · share_or_download · rotate_image · avatar · skeleton · activity_indicator · loading · divider ·
|
|
1087
|
-
spacer · stack · section_card · page_header · page_content · calendar (calendar/index.ts) · gantt ·
|
|
1088
|
-
comments_thread · agent_run (live streaming work feed) · agent_progress (its compact floating
|
|
1089
|
-
expandable form — a composer's working state) · composer (Composer — the adaptive command/chat
|
|
1090
|
-
composer: a compact pill that expands for long text + attachments) · confidence (calibrated high/med/low) ·
|
|
1091
|
-
change_review (the COMPOUND review family — frame: ChangeReview provider/stack · ChangeReviewHeader
|
|
1092
|
-
(auto kept-counter over decidable entries) · ChangeReviewActions (the commit bar in the
|
|
1093
|
-
DialogFooter/DrawerFooter: Keep-all bottom-left (onAcceptAll for host-held field state) + Apply
|
|
1094
|
-
gating); sections: Change (host-owned status, labeled verbs, collapses to its ChangeSummary + Undo;
|
|
1095
|
-
no callbacks = display-only) · ChangeLabel · ChangeSummary · ChangeReasoning (the quiet why);
|
|
1096
|
-
grammar: ChangeFields (the open record form) + ChangeField (THE field: − band · value · candidates
|
|
1097
|
-
+ type-another-value · reasoning · per-field Keep/Drop · collapse) · ChangeRecord (THE item card:
|
|
1098
|
-
registers like a Change; tone wash + localized op word; verb level follows the decision level) ·
|
|
1099
|
-
ChangeBand (the raw ± band) · ChangeValueInput (the diff-at-rest editor); the `changeReview`
|
|
1100
|
-
locale slice) ·
|
|
1101
|
-
clarify (the agent asks back, via ChoiceList) · choice_list (ChoiceList — selectable answer options,
|
|
1102
|
-
the agent's quick-reply surface) · sources (provenance chips, per-kind glyphs) · finding (Finding — one ranked AI-check insight: severity word · title · detail · Sources ·
|
|
1103
|
-
children slot — with FindingComparison, the expected-vs-actual body: labeled sides + emphasized
|
|
1104
|
-
delta; the `finding` locale slice) ·
|
|
1105
|
-
format_money · format_date · colors (solid · tint · ramp · ColorName ·
|
|
1106
|
-
isColorName · asColorName — coerce a stored option/status token to a ColorName, neutral fallback) ·
|
|
1107
|
-
auto_sizer (AutoSizer — measures its own box via onLayout and renders the render-prop child only once `{width,height}` exist; `autoSizeWidthOnly`/`autoSizeHeightOnly`, `onResize`. For content needing pixel dimensions before first paint — a canvas, a virtualized grid) · separator (Separator — a `Divider` pre-wrapped in vertical padding (`padding: SpacerSize`, default 8): the between-groups rule WITH breathing room — what `Stack useSeparator` inserts and what menus/popovers put between option groups; `Divider` is the bare hairline) · back_button (BackButton — the chevron-left `IconButton` (lg, secondary) heading a screen/panel: `onPress` + translated `accessibilityLabel` (default "Back"); the one go-back glyph — don't hand-roll it) · info_popover (InfoPopover — the ⓘ button opening a 280px popover of explanatory `text`; the middle ground between Tooltip (short hover label) and composing Popover (rich content) — it is what `SectionHeadingTitle.info`/`CardHeaderTitle.info` render) · shortcut_badge (ShortcutBadge — the keycap hint pill: a zinc-50 badge rendering a shortcut from a raw string or `ShortcutDescriptor` (⌘B on Mac, Ctrl+B elsewhere); null on small screens. `TextInputField shortcut` renders it built-in) · trend_footer (TrendFooter — the "Up X% vs last period" caption under a chart card: signed `value` (0 = flat), `periodLabel`, optional `detail`; `goodDirection="down"` flips green/red for metrics where down is good; SKIP it when there's no comparator. Direction words via the `trendFooter` locale slice; goes in `SectionCard footer`) · dots_indicator (DotsIndicator — three looping bouncing dots (`size`/`color`); the indeterminate "working/typing" pulse `Loading` composes; use bare beside a caption while an agent thinks. Determinate work → ProgressBar) · scroll_to_bottom (ScrollToBottom — the floating jump-to-latest circle button for a chat/feed; ONLY the affordance — the caller owns positioning, visibility, and the actual scroll) · animation_fade_in (AnimationFadeIn — the mount transition: children fade (+ optional `translateY` rise) into place on first render, once; the entrance polish Accordion/Timeline/Stepper/AgentRun rows use) · landmark (Landmark — the semantic region wrapper: `kind` banner|navigation|main|complementary|contentinfo|region maps to the matching HTML element on web for screen-reader landmark navigation, `accessibilityRole` on native; `accessibilityLabel` required for `region`) · skip_link (SkipLink — the a11y bypass link: parked off-screen until keyboard focus slides it in; `href="#targetId"` jumps past repeated nav into the main region. One per app shell, FIRST in the tree; web-only) · use_async_fn (useAsyncFn — wrap an async function into a manual-trigger mutation: `[run, {loading, data, error}]`, unmount-safe, the error lands in state AND rethrows; the pending-state engine for a submit/download/upload action) · use_hover (useHover — pointer-hover state `{hovered, hoverProps}` for raw inputs/DOM controls that lack a hovered style state; native stays false. Pressable-based controls use the built-in `hovered` state instead) · use_auto_grow_height (useAutoGrowHeight — the grows-with-content textarea engine ({minLines,maxLines} → container height + `scrollEnabled` at the cap); powers Composer and multiline TextInputField — reach for it only when hand-rolling a growing input) · overlay_scope (isOverlayScopeActive / useOverlayScope — the module-level open-overlay counter every overlay primitive reports into; the host's shortcut registry reads it synchronously to floor page-level shortcuts while any overlay is open. Lives in the primitives — never call it from a screen) · route_matching (pure `:param` route-pattern utilities — routeMatches / parseRouteParams / findBestPattern (exact beats parameterized); the matching core under ScreenRouter/Dialog) · text_utils (text/typography plumbing: `getTextColor` (the TextColor→hex map incl. the AA-cleared valence set), the Inter `fontFamily*` stacks, and `getInputTextStyle`/`getInputLineHeight` — the 16px-mobile/14px-desktop input contract that stops Safari iOS auto-zoom; only for hand-rolled raw inputs) · use_focus_ring (useFocusRing — keyboard-aware focus state for painting a control's own ring; see Focus rings) ·
|
|
1108
|
-
focus_ring_pressable (FocusRingPressable — a Pressable that rings on keyboard focus; the raw-control default) ·
|
|
1109
|
-
control_surface (CONTROL_HEIGHT · CONTROL_RADIUS · FOCUS_RING · chipSurfaceStyle — the shared control-surface tokens).
|
|
11
|
+
The component kit for Lotics custom-code apps and the product frontend: React-Native-Web
|
|
12
|
+
primitives (renders on web **and** native), data-entry patterns, AI surfaces, a composition
|
|
13
|
+
grammar, and worked-example screens. It pairs with **`@lotics/app-sdk`** (data + RPC — read
|
|
14
|
+
`node_modules/@lotics/app-sdk/AGENTS.md`): the SDK fetches/mutates, this kit draws.
|
|
15
|
+
|
|
16
|
+
This file is the index. The comprehensive references live in **`docs/`** — read the owning area
|
|
17
|
+
doc before building any screen, **never from memory**. Exact props are the shipped sources
|
|
18
|
+
(`src/<name>.tsx`); full worked screens are `examples/tpl_*.tsx`.
|
|
19
|
+
|
|
20
|
+
## The area references
|
|
21
|
+
|
|
22
|
+
| Doc | Read it for |
|
|
23
|
+
|---|---|
|
|
24
|
+
| [docs/catalog.md](./docs/catalog.md) | **The complete inventory** — Reach-by-role (each data role → the ONE canonical component) + every `@lotics/ui/<module>` entry point. Read before building any screen; reuse first. |
|
|
25
|
+
| [docs/data_entry.md](./docs/data_entry.md) | Which editing pattern for which job — inline edit, fieldset forms, find-or-create (`Combobox`), line items, handoffs, phased records, billing, tags, dispositions, attachments, stage gates. |
|
|
26
|
+
| [docs/ai_patterns.md](./docs/ai_patterns.md) | AI proposes, the human decides — composer, live run feed (`AgentRun`), review-before-apply, findings, provenance, confidence; the UI half of the SDK's [ai doc](../app-sdk/docs/ai.md). |
|
|
27
|
+
| [docs/composition.md](./docs/composition.md) | The design-language contract — canvas + content column, heading altitude, banded cards, register vs inset rows, master-detail `Drawer`, view controls, color discipline, typography, whitespace. |
|
|
28
|
+
| [docs/templates.md](./docs/templates.md) | The map of `examples/tpl_*.tsx` — what shape each template solves and which to start from (copy + adapt, never import). |
|
|
29
|
+
|
|
30
|
+
## Iron rules
|
|
31
|
+
|
|
32
|
+
- **Reuse first; the catalog can lag `src/`.** Before hand-rolling ANY capability, `ls src/` and
|
|
33
|
+
grep for a match — a component in `src/` missing from the catalog is a doc bug to fix, not a
|
|
34
|
+
license to hand-roll.
|
|
35
|
+
- **One canonical component per data role** (member → `MemberChip`, select → `OptionBadge`,
|
|
36
|
+
files → `FilePreview` family, …) — the catalog's Reach-by-role outranks neighboring code.
|
|
37
|
+
- **The kit's fonts/colors/icons ARE the design system** — never a custom font, icon set, or
|
|
38
|
+
hand-picked palette shade; color is `solid`/`tint`/`ramp` with ONE accent per screen.
|
|
39
|
+
- **Pure primitives only** — no i18n, analytics, or domain types in `src/` (pass `labels`,
|
|
40
|
+
callbacks); Lotics-coupled UI belongs in `@lotics/ui-internal`.
|
|
41
|
+
- **Every state designed** — skeleton (mirroring layout), empty, error; no layout shift.
|
|
42
|
+
|
|
43
|
+
## Keeping this reference current
|
|
44
|
+
|
|
45
|
+
These docs publish to npm and are read from `node_modules` by authors who cannot see this repo.
|
|
46
|
+
**Codify every new component, pattern, or rule into its OWNING area doc + the index above in the
|
|
47
|
+
same change, and bump the package version** — a `src/` export absent from
|
|
48
|
+
[docs/catalog.md](./docs/catalog.md) WILL be hand-rolled by the next author. New shapes that prove
|
|
49
|
+
themselves graduate into `src/` + an `examples/tpl_*` + a catalog entry, so every app inherits
|
|
50
|
+
them. Each fact lives in exactly one doc — link, never duplicate.
|