@lotics/ui 3.6.0 → 4.1.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.
Files changed (65) hide show
  1. package/AGENTS.md +352 -0
  2. package/examples/app_orders.tsx +405 -0
  3. package/examples/tpl_allocate.tsx +120 -0
  4. package/examples/tpl_approvals.tsx +375 -0
  5. package/examples/tpl_attendance.tsx +355 -0
  6. package/examples/tpl_batch.tsx +234 -0
  7. package/examples/tpl_billing.tsx +344 -0
  8. package/examples/tpl_calendar.tsx +288 -0
  9. package/examples/tpl_callsheet.tsx +481 -0
  10. package/examples/tpl_convert.tsx +490 -0
  11. package/examples/tpl_crm_desk.tsx +541 -0
  12. package/examples/tpl_dashboard.tsx +554 -0
  13. package/examples/tpl_detail.tsx +232 -0
  14. package/examples/tpl_directory.tsx +263 -0
  15. package/examples/tpl_dispatch.tsx +289 -0
  16. package/examples/tpl_dossier.tsx +431 -0
  17. package/examples/tpl_intake.tsx +206 -0
  18. package/examples/tpl_inventory.tsx +299 -0
  19. package/examples/tpl_order.tsx +483 -0
  20. package/examples/tpl_pick.tsx +240 -0
  21. package/examples/tpl_quick.tsx +210 -0
  22. package/examples/tpl_reconcile.tsx +275 -0
  23. package/examples/tpl_record.tsx +301 -0
  24. package/examples/tpl_record_plain.tsx +154 -0
  25. package/examples/tpl_rollup.tsx +300 -0
  26. package/examples/tpl_run.tsx +235 -0
  27. package/examples/tpl_settings.tsx +178 -0
  28. package/examples/tpl_shifts.tsx +421 -0
  29. package/examples/tpl_stock.tsx +387 -0
  30. package/examples/tpl_timeline.tsx +244 -0
  31. package/examples/tpl_tower.tsx +356 -0
  32. package/examples/tpl_wizard.tsx +223 -0
  33. package/package.json +12 -2
  34. package/src/bar_chart.tsx +5 -0
  35. package/src/combobox.tsx +33 -8
  36. package/src/control_surface.ts +8 -0
  37. package/src/form_date_picker.tsx +2 -0
  38. package/src/form_picker.tsx +1 -0
  39. package/src/form_switch.tsx +1 -0
  40. package/src/form_text_input.tsx +2 -0
  41. package/src/icon.tsx +2 -0
  42. package/src/icon_button.tsx +5 -2
  43. package/src/index.css +6 -3
  44. package/src/inline_date_picker.tsx +111 -0
  45. package/src/inline_edit.tsx +238 -0
  46. package/src/inline_number_input.tsx +70 -0
  47. package/src/inline_select.tsx +92 -0
  48. package/src/inline_text_input.tsx +71 -0
  49. package/src/inline_time_picker.tsx +64 -0
  50. package/src/line_chart.tsx +4 -0
  51. package/src/link.tsx +32 -0
  52. package/src/list_item.tsx +5 -0
  53. package/src/number_input.tsx +12 -1
  54. package/src/page_content.tsx +5 -0
  55. package/src/picker.tsx +4 -1
  56. package/src/popover.tsx +10 -1
  57. package/src/pressable_row.tsx +4 -1
  58. package/src/radio_picker.tsx +3 -1
  59. package/src/section_heading.tsx +43 -29
  60. package/src/segmented_control.tsx +3 -2
  61. package/src/tabs.tsx +4 -2
  62. package/src/tag_input.tsx +202 -0
  63. package/src/text.tsx +1 -1
  64. package/src/time_picker.tsx +15 -3
  65. package/src/tooltip.tsx +19 -0
package/AGENTS.md ADDED
@@ -0,0 +1,352 @@
1
+ # @lotics/ui — the UI reference
2
+
3
+ A React-Native-Web component kit (renders on web **and** native). This file is the design
4
+ law + the "reach for what" guide. The **exact API** of any component is its source, which
5
+ ships with the package: read `node_modules/@lotics/ui/src/<name>.tsx`. **Never guess a prop —
6
+ open the file.** If the closest component lacks a capability, **extend it** (a platform change
7
+ every app inherits), never inline a one-off `View`/`Text` rebuild — that forfeits the
8
+ typeahead, async search, virtualization, and a11y the primitive already ships.
9
+
10
+ - Import per module: `import { Combobox } from "@lotics/ui/combobox"`.
11
+ - Floating content (`Dialog` · `Popover` · `Tooltip` · `Alert` · `CommandMenu`) needs a
12
+ `PortalHost` at the app root.
13
+ - RN-Web only: `View`/`ScrollView` from `react-native`, the `Text` primitive (no raw
14
+ `div`/`span`, no raw `fontSize`/`fontWeight`); styles are RN objects.
15
+ - i18n: most components are string-free; the few that emit their own user-facing strings take
16
+ a `labels` prop (`DateRangeFilterField`, `Pagination`, `SortHeader`/`Table.sortLabels`) —
17
+ localize there.
18
+
19
+ ---
20
+
21
+ ## Reach by role — what exists
22
+
23
+ Pick by capability, not by name. (→ the source file for the API.)
24
+
25
+ - **Actions** — `Button` (labelled; `color` = emphasis/risk, `shape` pill|rounded),
26
+ `IconButton` (icon-only, needs `accessibilityLabel`/`tooltip`), `LinkButton` (quiet ghost
27
+ action — Clear / Select all), `PillButton` (dismissible facet chip). A button is never a raw
28
+ `Pressable`. For a link OUT (a URL / record / document) use `Link` — underline+blue, the
29
+ destination signal; the opposite of `LinkButton` (a quiet in-app action, never underlined).
30
+ - **Pick from a list** — `Picker` (known options, native typeahead, single/multi/custom-render,
31
+ no search box). Search-as-you-type / async / create-new → `Combobox`. A selectable card row →
32
+ `CardSelectItem`.
33
+ - **Tags / multi-select chip box** — `TagInput` (chips + an Add-popover checklist + create), NOT
34
+ `Combobox multi` (see §Data entry).
35
+ - **Text & form** — `TextInputField`, `NumberInput`, `SearchInput`; wrap with `FormField`;
36
+ `Checkbox`, `Switch`, `RadioPicker`; dates via `DatePicker` / `DateRangeFilterField`, times
37
+ via `TimePicker`.
38
+ - **Edit a record's fields in place** — the `Inline*` family: `InlineTextInput` ·
39
+ `InlineNumberInput` · `InlineSelect` · `InlineDatePicker` · `InlineTimePicker` (see §Data entry).
40
+ - **Tabular data** — `Table` (columns defined once; sortable headers via `SortHeader`; paired
41
+ with `Pagination`). Never an HTML `<table>` or a `.map` of rows.
42
+ - **Numbers & charts** — `KPIStrip` / `KPICard` / `Metric` (headline figures), `TrendChip`
43
+ (delta), `Sparkline`, `BarChart` / `LineChart` / `PieChart` (the canonical SVG set — no
44
+ recharts), `RingGauge`, `ProgressBar` / `StackedProgressBar` / `StepProgress`, `Breakdown`,
45
+ `StatusGrid` + `StatusLegend`, `Heatmap`.
46
+ - **Surfaces & layout** — `Card` (+ `CardHeader`/`CardHeaderTitle`/`CardHeaderMeta`/`CardBody`/
47
+ `CardFooter`), `SectionCard`, `SectionHeading` (+ `SectionHeadingTitle`), `PageHeader` /
48
+ `PageContent`, `Stack`, `Spacer`, `Divider`, `Accordion`, `Tabs`, `SegmentedControl`, `Stepper`.
49
+ - **Rows & registers** — `PressableRow` (THE register row), `ListItem`, `MenuButton`,
50
+ `MenuListItem`, `DetailRow` (label+value for drawers/peeks), `ActionMenu` (⋯), `FloatingActionBar`
51
+ (bulk-select bar).
52
+ - **Filters & view controls** — `SearchInput`, `ChipGroup`, `FilterPill` (+ `RangeSlider`,
53
+ `Counter`), `PillButton`.
54
+ - **Overlays** — `Dialog`, `Drawer` (+ `DrawerFooter`), `Popover`, `Tooltip`, `CommandMenu`,
55
+ `Alert` (the blocking confirm).
56
+ - **Status / feedback** — `Badge` / `StatusBadge`, `Callout` (inline status), `EmptyState`,
57
+ `CompletionState`, `ActivityIndicator` / `Loading`, `Skeleton`.
58
+ - **Files** — `FileDropzone`, `FileThumbnail` / `FileThumbnailGrid`, `FilePreview` /
59
+ `FileGalleryModal`, `ImageGallery` (see §Data entry → Attachments).
60
+ - **Specialized work surfaces** — `ScanField` (scan/verify), `StepList` (guided run),
61
+ `RemainderMeter` + `AllocationRow` (allocation), `Timeline`, `Calendar`, `Gantt`,
62
+ `comments_thread`.
63
+
64
+ ---
65
+
66
+ ## Data entry — which pattern for which job
67
+
68
+ This is the most common thing to get right. Match the JOB to the pattern:
69
+
70
+ | You're capturing… | Reach for | Why |
71
+ |---|---|---|
72
+ | an EXISTING record's fields | **Inline edit** (`Inline*`) | edit in place, no form mode |
73
+ | a brand-NEW record (a form) | **fieldset form** (`FormField` + the 2-col grid) | structured entry + a Save |
74
+ | a RELATED record (pick or make) | **find-or-create** (`Combobox allowCustom`) | one control covers both |
75
+ | REPEATING rows you build & revise | **line items** (create→preview→edit) | add / edit / remove, live totals |
76
+ | CHARGES that bill onto documents | **billing** (`tpl_billing`) | the invoice document is the unit |
77
+ | a multi-value TAG field | **`TagInput`** | a chip box, not a search input |
78
+ | a STATUS with terminal outcomes | **disposition** (open → resolve → revise) | guides the decision |
79
+ | FILES | **attachment field** (dropzone + grid + gallery) | add / preview / delete |
80
+ | many entries FAST | **quick capture** (one row, Enter to add) | repeat-entry speed |
81
+ | a state TRANSITION mid-flow | **stage gate** (popover / dialog by weight) | right-sized friction |
82
+
83
+ ### Inline edit — the preferred way to edit an existing record
84
+ When the whole record is editable (a detail/record screen, dense settings), don't wrap it in a
85
+ form mode or a preview↔edit card — make each VALUE inline-editable: it reads as plain text,
86
+ hover tints it (no pencil — that shifts), click swaps the input in **at the same height** (zero
87
+ reflow, the whole point), and it commits on blur (Enter saves, Escape reverts) or via
88
+ `controls="buttons"` (✓ primary / ✕). One per type — `InlineTextInput` · `InlineNumberInput`
89
+ (`format` for currency/units) · `InlineSelect` (plain options OR `renderOptionContent`; floats a
90
+ `Picker` menu in a popover so the row never grows) · `InlineDatePicker` · `InlineTimePicker` — all
91
+ on `useInlineEdit` + `InlineEditView` (custom inputs join via those). `onSave` is async: the
92
+ saving spinner sits INSIDE the control (never a sibling — that reflows); an error shows inline
93
+ without losing the edit. Pair with `DetailRow` (label left, inline value right). Not every field
94
+ is a same-height swap — a tag field, a status, or an attachment grid edit in place too (below).
95
+
96
+ ### Fieldset form — fields lay out on a RESPONSIVE two-column grid
97
+ Never hard-code columns, never 3-up. A fieldset is `flexDirection:row, flexWrap:wrap,
98
+ columnGap:16`; each `FormField` declares an intrinsic width — `half` (`flexGrow:1,
99
+ flexBasis:240`) or `full` (`flexGrow:1, flexBasis:"100%"`) — so two halves sit side-by-side on a
100
+ wide card and stack on a narrow one with zero media queries. Pair short, related fields as halves
101
+ (phone/email, qty/price); give long or singular values the full row (legal name, address, notes).
102
+ Past two columns the label→field link breaks — MANY inputs means GROUPING into labeled fieldsets
103
+ (each its own 2-up grid, `Divider` between), never a third column. `FormDatePicker`/`FormPicker`
104
+ wrap their OWN `FormField` (and omit `style`) — for a grid cell use a bare `FormField style={half}`
105
+ wrapping `DatePicker`/`Picker`.
106
+
107
+ ### Find-or-create — `Combobox` IS the control
108
+ `Combobox` is a SELECT by default (no leading icon, a trailing chevron); opt INTO the search-box
109
+ look with `icon="search"` only when typing-to-search is primary (a large/remote set).
110
+ `allowCustom` appends a "Create …" row (`customOptionLabel`) when the query matches no option,
111
+ emitting the typed text as the value — so a value not in the known set means CREATE. Wire that
112
+ branch to a create overlay (a modal `Dialog` for 4+ fields, an anchored popover for 1–3) that
113
+ builds the new record and attaches it; existing matches attach directly. The create row sits
114
+ BELOW matches by default (the keyboard highlight lands on the first MATCH, so Enter on a partial
115
+ picks it, never a duplicate); pass `customOptionPlacement="top"` to pin it above. Use
116
+ `reflectSelection={false}` and render the attached record below as a card with a Change action.
117
+
118
+ ### Line items — create → preview → edit (a composition, not a primitive)
119
+ For a list of records you build then revise (invoice rows, repair lines, config entries), each
120
+ item is a `Card` toggling between a read-only PREVIEW (a stack of `DetailRow`s) and an EDIT form
121
+ (the inputs), via a local `editing` flag + Edit/Save/Cancel in the `CardFooter`. A freshly-added
122
+ item opens in edit; Save collapses it to the preview; Edit reopens it. **Edit is cancelable** —
123
+ snapshot the item on Edit so Cancel reverts, or DISCARDS a freshly-added one. Duplicate sits left;
124
+ the destructive **Delete is `danger-secondary`**; Cancel + the Save/Edit toggle sit right. Every
125
+ screen composes its own (~15 lines) so the preview rows + form fit the data.
126
+
127
+ ### Billing & invoicing — the invoice DOCUMENT is the unit (`tpl_billing`)
128
+ When charges get grouped into issuable documents (an e-invoice, a bill) and then collected, don't
129
+ split the screen into "enter fees here, issue there" — that smears one job across two places. Make
130
+ **each invoice a `Card` that holds its own editable charge lines** (amount input + payment method),
131
+ its **live total**, its **status badge** (nothing-to-bill · draft · issued + ref), and its **issue
132
+ action** in the footer. A charge never lives apart from the document it bills on. Issuing is gated
133
+ **inline, never a dead end**: when a prerequisite is missing (a bill-to tax ID, a method on a
134
+ charged line) the issue button disables with one muted line saying what's needed; the payment-method
135
+ picker turns required the instant a line carries an amount. Issuing a real e-invoice is irreversible
136
+ → confirm in a `Dialog` (stage gate). A grand-total **receipt** validates first — surface the EXACT
137
+ missing methods (`Alert.alert` listing each) rather than a vague "incomplete." A refundable
138
+ **deposit** is its own card and its own receipt — never folded into the total due. Composition over
139
+ `Card` + `NumberInput` + `Picker` + `Badge`; no new primitive.
140
+
141
+ ### Tag field — `TagInput`, NOT `Combobox multi`
142
+ A tag field's resting state should be a tidy CHIP BOX: `TagInput` is a bordered box of removable
143
+ chips + an Add affordance; searching/creating happens in a POPOVER (a searchable, checkable list +
144
+ an optional `allowCreate` row), never an inline token input. `Combobox multi` squeezes a
145
+ `TextInput` among the chips — right for a SEARCH field that accumulates picks, wrong for a chip box
146
+ you occasionally add to. One owner per shape: chips-you-edit → `TagInput`; search-that-accumulates
147
+ → `Combobox multi`.
148
+
149
+ ### Disposition — lifecycle status is ASYMMETRIC by phase
150
+ When a status is an OPEN default plus terminal OUTCOMES the user decides (Lead → Closed/Lost, Draft
151
+ → Confirmed/Cancelled, Open → Approved/Rejected), a 3-way segmented/dropdown is wrong — it treats a
152
+ lifecycle as flat peers and guides neither the decision nor the revision. Split by phase: while
153
+ OPEN, surface the outcomes as ACTIONS with valence (`Mark closed` = `primary`, `Mark lost` =
154
+ `danger-secondary`; the open state a quiet dot `Badge`). Coloring the buttons is right here — a
155
+ SINGLE record-level decision is not the approvals-queue "wall of loud buttons." Once RESOLVED, show
156
+ the colored STATE (`Badge variant="dot"` — emerald positive / red negative / blue open) with a
157
+ quiet, REVERSIBLE **Change**: a popover STATE SWITCHER (each state a colored dot + label, current
158
+ marked + disabled), not a generic text menu. A composition (Badge + Button + Popover/MenuButton).
159
+
160
+ ### Attachments — a full add / preview / DELETE field
161
+ Capture with `<FileDropzone onFiles accept label hint dropLabel height>` (drag-over lights the
162
+ accent; click falls back to a picker); display what landed with `<FileThumbnailGrid files>` ABOVE
163
+ the dropzone (existing files are the content; the dropzone sinks to the bottom as the "add more"
164
+ affordance — only the empty state leads with it). Make it CRUDable by wiring the grid's two
165
+ callbacks: `onFilePress` → set a `number|null` index that drives `<FileGalleryModal files
166
+ activeIndex onIndexChange>` (built-in prev/next/ESC/rotate); `onRemove` → drop the file from state
167
+ (the grid renders a ✕ on each thumbnail automatically). Keep label+grid+dropzone on a `gap` (a bare
168
+ `CardBody` has none). `FileThumbnail` shows the right surface per MIME (image thumbnail · doc card ·
169
+ media card). Never hand-build a drop well, a file row grid, or a preview modal.
170
+
171
+ ### Quick capture — the SPEED surface
172
+ For fast repeat entry (logging activity, expenses), one compact capture row (a `SegmentedControl`
173
+ mode, a `Combobox`-as-select, a `Counter`, an Enter-to-submit `TextInputField`); each entry commits
174
+ on its own into an undoable log below, the field keeps focus and the prior choices carry over. No
175
+ screen-wide Save.
176
+
177
+ ### Stage gates — tiered by weight
178
+ A transition that needs NO input is one click. 1–3 quick fields → a POPOVER FORM anchored to its
179
+ action button (title + one-line stake + `FormField`s; confirm in `PopoverFooter`). A
180
+ destructive/exception path (anything touching money or locks) → a `Dialog` (consequence in prose,
181
+ danger confirm + cancel in `DialogFooter`) or an `Alert.alert(title, message, [{cancel},
182
+ {destructive}])` confirm. Never an inline expanding panel for a gate — it shifts layout and loses
183
+ the "this is a gate" framing.
184
+
185
+ ---
186
+
187
+ ## Composition grammar
188
+
189
+ - **Canvas**: full-bleed `colors.zinc[50]` ScrollView; content column `maxWidth` 880–1040,
190
+ centered, `padding: 28`. Cards float on the canvas — never white-on-white.
191
+ - **Heading vocabulary — ONE construct per altitude, no drift.**
192
+ · **Page band**: one per screen — `Text size="xl" weight="semibold"` title + one `sm muted`
193
+ subtitle that says what the screen decides. Right side: the screen's ONE primary action and/or
194
+ a period filter — never a summary Badge (those belong to the KPI strip).
195
+ · **Card header** (a card's own title band, or separate banded cards): `CardHeader` +
196
+ `CardHeaderTitle` (`sm semibold`; `info` when the title alone doesn't define the numbers).
197
+ · **Section title** (a substantial titled block — a form, a timeline, a field group — INSIDE one
198
+ multi-section card or drawer): a `Text size="sm" weight="semibold"` at the TOP of the section's
199
+ content, sections separated by ONE `Divider`. Do NOT wrap each section in its own `CardHeader`
200
+ (its auto-divider double-stacks hairlines around every title). A card-less full page uses
201
+ `SectionHeading` + `SectionHeadingTitle`. NEVER a bare eyebrow standing in for a section title.
202
+ · **Eyebrow** (`<Text size="xs" color="muted" transform="uppercase">`, WEIGHTLESS): ONLY a
203
+ column header or a truly minor one-line label — not a section that owns real content. Don't add
204
+ `weight`. (A COLORED status word like "Blocked" — `weight="semibold"` + `color="danger"` — is a
205
+ different element, the verdict word, and keeps its weight.)
206
+ · **Gate header**: a `Dialog` uses `DialogHeaderTitle`; a popover form uses `Text size="sm"
207
+ weight="semibold"` + an optional `xs muted` subtitle.
208
+ - **Time-constrained data gets a period filter** in the header band — `DateRangeFilterField`, never
209
+ a static period badge. Every period-dependent number MUST follow the selection.
210
+ - **Keyboard & focus — use `tabIndex`, never `focusable`.** RN-Web's `Pressable` silently ignores
211
+ `focusable` (it writes its own `tabIndex`), so set a pressable control's tab-stop status with
212
+ `tabIndex={0 | -1}` (`focusable` only works on a plain `View`/`TextInput`). Roving widgets
213
+ (Tabs/SegmentedControl/RadioPicker) keep ONE stop at `0`, the rest `-1`. And NEVER let a FOCUSED
214
+ control unmount — a conditional pointer affordance that vanishes on use (e.g. an "apply suggested
215
+ value" pill shown only while a field is empty) must be `tabIndex={-1}`, or the browser drops focus
216
+ to `<body>` and the next Tab jumps to the page's first focusable. A mouse-opened popover trigger
217
+ (Picker/Combobox/InlineSelect/InlineDatePicker) gets no `:focus-visible` ring, so it wears
218
+ **`ACTIVE_RING`** (`control_surface.ts`, a `0 0 0 2px zinc-900` box-shadow mirroring the focus
219
+ outline) on its open state — reuse that token, don't hand-roll a thin 1px edge. Full rules + the
220
+ keyboard test: `docs/accessibility.md` → Focus & tab order.
221
+ - **Cards are banded — and composable** (all from `@lotics/ui/card`):
222
+ ```tsx
223
+ <Card style={{ padding: 0 }}>
224
+ <CardHeader><CardHeaderTitle info="what this shows">Title</CardHeaderTitle><CardHeaderMeta>12</CardHeaderMeta></CardHeader>
225
+ <CardBody>…</CardBody> {/* repeat with <Divider/> between */}
226
+ <CardFooter>hint(flex:1) + actions</CardFooter>
227
+ </Card>
228
+ ```
229
+ Never nest cards; never hand-compose a title or footer band. `info` is standard, not garnish.
230
+ - **The stat band**: `<KPIStrip items={[{label, value, trend?, caption?, tone?, info?}…]}>` is the
231
+ ONE way to open a screen with numbers. KPI strips are INFORMATIONAL — they never filter or
232
+ navigate (that's the tabs/chips' job), and they carry cross-cutting health numbers the tabs
233
+ can't show; if there are none, drop the strip. Card stat rails use `KPICard`.
234
+ - **Numbers**: free-standing numerals `<Text tabular>`. Money: `formatMoney(n)` from
235
+ `@lotics/ui/format_money` (`compact` for strip captions). Dates: `formatDate(value, opts)` from
236
+ `@lotics/ui/format_date` (`format:"datetime"`, `compact` → dd/MM). Never hand-roll grouping/₫ or
237
+ dd/MM.
238
+ - **Every number is a door** (except the KPI strip). A component that summarizes records leads to
239
+ the records behind it when pressed — switch to the filtered list, expand in place, or navigate.
240
+ Expansion happens IMMEDIATELY below the pressed element — the composable `Accordion` family
241
+ (`AccordionHeader`/`AccordionTitle`/`AccordionMeta`/`AccordionContent`); header-only = a plain row
242
+ of identical rhythm, so lists mix expandable + static rows.
243
+ - **No dead rows.** Every listed record is actionable. PRIMARY entity rows press-open the workspace
244
+ `Drawer` (sequenced); read-only drill-downs expand via `Accordion` or glance via `Peek`; every
245
+ other row gets an `ActionMenu` (⋯ → MenuButton items: destructive last + **confirmed** via
246
+ `Alert`). The whole surface is the door: register rows are `PressableRow` (full-bleed wash incl.
247
+ nested controls). Two row patterns, never un-separated:
248
+ · **Register (default)** — px-20 square rows, wash to the card edges, separated by full-bleed
249
+ `Divider`s (`{i > 0 ? <Divider/> : null}`), NO wrapping padding. A COLUMNAR register is
250
+ `Table`/`TableRow`/`TableCell` (columns defined once → header + widths can't drift); paginate
251
+ OUTSIDE (slice + `Pagination` in the `CardFooter`).
252
+ · **Inset contained list** — grouped/gap-separated lists: `PressableRow variant="inset"` in a
253
+ padded container, NO `Divider`s (the gap + rounded hover separate them).
254
+ Right-hand columns align only if every trailing element is FIXED-width (give each trailing action
255
+ a fixed `width`, so amount/status columns don't jitter).
256
+ - **Master-detail = list + workspace `Drawer` with sequencing.** Pressing a PRIMARY entity row
257
+ opens the record workspace in a `Drawer` with `onPrev`/`onNext`/`position` ("3/24") over the
258
+ visible ordering (←/→ built in). Key the drawer body by record id so per-record state resets on
259
+ step. Facts are `DetailRow`s; the commit bar is a `DrawerFooter`. The open row shows a selected
260
+ highlight (and `marked` — a resting blue tint — for a bulk-ticked row). `Peek` is ONLY for
261
+ secondary references, never the primary row press.
262
+ - **One view-control vocabulary** (all 40px, `sm` labels, in one wrapping band):
263
+ · `SearchInput` is THE search box — a rounded white pill with a thin zinc-300 border. Never a
264
+ bare `TextInputField` + search icon.
265
+ · `ChipGroup` is THE one-of-N lens (≤ ~10 options the user flips between; include "All"; counts in
266
+ the label). Filters ONE list to a SUBSET — including by process stage / lifecycle state.
267
+ · `FilterPill` is THE secondary-dimension filter — a compact pill that opens a composed popover
268
+ editor (`PickerMenu multi` / `RangeSlider` / `Counter` / date range). Single-select closes on
269
+ pick via the `{({close}) => …}` render prop.
270
+ · `Tabs` switch between VIEWS/sections (different content/layout) — never a subset of one list.
271
+ `SegmentedControl` chooses a MODE/PARAMETER of the SAME view (2–4 peers, no panel swap).
272
+ - **Footer actions align RIGHT.** `PopoverFooter`/`DialogFooter`/`DrawerFooter` default
273
+ `align="end"` — commit at the right edge, secondary to its left. Never left-flow a Save/Submit.
274
+ - **Empty result**: `<EmptyState message hint? icon? action?>` — never a bare muted Text.
275
+ - **Inline status**: `<Callout tone="info|success|warning|error|neutral">` — compound (like Card):
276
+ compose `CalloutTitle`/`CalloutText`/`CalloutActions` inside. `Callout` is INLINE; `Alert` is the
277
+ blocking modal; `Badge` is a one-word pill.
278
+ - **One primary action per surface.** Everything else secondary/muted; destructive = `danger`
279
+ styling + explicit label. (A grouped BUILDER section — line items — MAY give its own Add action
280
+ `primary`; it sits at a different altitude than the form's one terminal commit.)
281
+ - **Button labels carry no trailing ellipsis** ("Assign", not "Assign…"). **Button color is
282
+ VALENCE/RISK, never category**: the ladder `muted < secondary < primary` is the emphasis axis,
283
+ `danger` marks destructive — that's the whole axis. No "success"/green. Decision UIs put
284
+ positive/negative color on the STATUS (dot) and verdict (colored `Text`), not the buttons.
285
+ - **Color discipline** — every status / data-viz / accent color comes from a NAMED helper, never a
286
+ hand-picked `colors.<family>[shade]`: `solid(name)` = the 500 (dots, series, meter accents);
287
+ `tint(name, α)` = a wash; `ramp(name, count)` = N shades of ONE family for a coherent dimension.
288
+ A status is ONE family NAME (`{ color: "emerald" }`) and every weight derives from it. One accent
289
+ per screen purpose (blue=pipeline, emerald=money, red=danger, amber=waiting). Direct `colors.*` is
290
+ reserved for NEUTRALS (`zinc`, `border`, `white`, the `*[50]` selection washes).
291
+ - **Status indicators have a WEIGHT — match to prominence, never to a metric.** colored `Text` (a
292
+ verdict WORD in a header) < `Badge variant="dot"` (a categorical STATE in a scannable column /
293
+ legend) < `Badge` tonal pill (a record's PROMINENT status, a drawer/detail header). A register's
294
+ dense rows read lighter — the row's primary status is `variant="dot"`, its drawer twin tonal. A
295
+ `Badge` is never a metric value.
296
+ - **Typography**: only the `Text` primitive (size/weight/color/transform). Uppercase tracking is
297
+ built in.
298
+ - **Touch & whitespace**: anything pressable is ≥ 40px tall (8px min gap between pressables). 16px
299
+ between cards, 24–28 canvas padding, 16–20 inside bands, 10–12 between content lines. Density
300
+ comes from alignment + hierarchy, not cramming.
301
+
302
+ ---
303
+
304
+ ## Worked examples (templates)
305
+
306
+ Full worked-example screens ship in the package — **read the source at
307
+ `node_modules/@lotics/ui/examples/<name>.tsx`** (pure `@lotics/ui` + mock data, each a usable
308
+ recipe for a screen JOB; copy and adapt). Pick by the job:
309
+
310
+ - **Monitor & decide** — `tpl_dashboard` (KPIStrip + trends + attention list) · `tpl_stock`
311
+ (10k-record faceted funnel) · `tpl_tower` (live `StatusGrid` wallboard) · `tpl_rollup`
312
+ (hierarchical totals).
313
+ - **Work execution** — `app_orders` (stage-gated work queue) · `tpl_crm_desk` (assign & work +
314
+ `FloatingActionBar`) · `tpl_callsheet` (call console) · `tpl_convert` (staged conversion) ·
315
+ `tpl_inventory` (threshold actions) · `tpl_dossier` (high-volume paginated register) ·
316
+ `tpl_approvals` (verdicts: bulk + over-policy `Dialog`) · `tpl_reconcile` (pair matching) ·
317
+ `tpl_dispatch` (capacity allocation) · `tpl_batch` (compose from parts) · `tpl_pick` (guided
318
+ `ScanField` run) · `tpl_allocate` (`RemainderMeter` split) · `tpl_run` (preview → resolve → post).
319
+ - **Data capture** — `tpl_intake` (multi-fieldset form + attachments) · `tpl_order` (the
320
+ transactional form: find-or-create + line items + attachment CRUD + 2-col grid) · `tpl_billing`
321
+ (charges→invoice→collect: the invoice DOCUMENT is the unit — editable charge lines + method,
322
+ live total, status, inline-gated issue; validated receipt; separate deposit) · `tpl_quick`
323
+ (quick-capture log) · `tpl_wizard` (`Stepper` form + review) · `tpl_record` (inline edit + Stage
324
+ disposition + `TagInput` + Documents grid; `tpl_record_plain` = card-less).
325
+ - **Records & lookup** — `tpl_detail` (full record + tabs) · `tpl_directory` (searchable register) ·
326
+ `tpl_timeline` (audit feed).
327
+ - **Planning & time** — `tpl_calendar` · `tpl_attendance` · `tpl_shifts`.
328
+ - **Administration** — `tpl_settings`.
329
+
330
+ ---
331
+
332
+ ## Full component inventory (import as `@lotics/ui/<module>`)
333
+
334
+ text · card (Card · CardHeader · CardHeaderTitle · CardHeaderMeta · CardBody · CardFooter) ·
335
+ section_heading (SectionHeading · SectionHeadingTitle — compound, owns no margin) · badge ·
336
+ status_badge · button · icon_button · link · link_button · pill_button · tabs · segmented_control ·
337
+ picker · combobox · tag_input (TagInput — chip box + Add-popover; for tags, not Combobox multi) ·
338
+ text_input_field · number_input · search_input · form_field · checkbox · checkbox_input · switch ·
339
+ radio_picker · counter · range_slider · date_picker · date_range_filter_field · time_picker ·
340
+ inline_text_input · inline_number_input · inline_select · inline_date_picker · inline_time_picker
341
+ (the Inline* family — per-field editors on `inline_edit`'s `useInlineEdit` + `InlineEditView`) ·
342
+ list · list_item · menu_button · menu_list_item · detail_row · pressable_row · action_menu ·
343
+ floating_action_bar · filter_pill · sort_header · table · pagination · accordion · stepper ·
344
+ step_progress · step_list · timeline · drawer (+ DrawerFooter) · dialog · popover · tooltip ·
345
+ command_menu · alert · peek · empty_state · completion_state · callout (Callout · CalloutTitle ·
346
+ CalloutText · CalloutActions) · kpi_card · kpi_strip · metric · trend_chip · sparkline ·
347
+ bar_chart · line_chart · pie_chart · ring_gauge · progress_bar · stacked_progress_bar · breakdown ·
348
+ status_grid (StatusGrid + StatusLegend) · heatmap · legend_item · remainder_meter · allocation_row ·
349
+ scan_field · file_dropzone · file_thumbnail · file_thumbnail_grid · file_preview ·
350
+ file_gallery_modal · image_gallery · avatar · skeleton · activity_indicator · loading · divider ·
351
+ spacer · stack · section_card · page_header · page_content · calendar (calendar/index.ts) · gantt ·
352
+ comments_thread · format_money · format_date · colors (solid · tint · ramp · ColorName).