@lotics/ui 8.0.0 → 10.0.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 (58) hide show
  1. package/AGENTS.md +177 -70
  2. package/examples/tpl_allocate.tsx +2 -2
  3. package/examples/tpl_attendance.tsx +2 -2
  4. package/examples/tpl_calendar.tsx +1 -1
  5. package/examples/tpl_dashboard.tsx +1 -1
  6. package/examples/tpl_item_list.tsx +1015 -124
  7. package/examples/tpl_pick.tsx +3 -3
  8. package/examples/tpl_pivot.tsx +1 -1
  9. package/examples/tpl_record.tsx +1354 -0
  10. package/examples/tpl_report.tsx +7 -7
  11. package/examples/tpl_rollup.tsx +6 -6
  12. package/examples/tpl_shifts.tsx +2 -2
  13. package/examples/tpl_statements.tsx +221 -0
  14. package/examples/tpl_stock.tsx +7 -7
  15. package/examples/tpl_task_board.tsx +16 -13
  16. package/examples/tpl_tasks.tsx +15 -28
  17. package/examples/tpl_tower.tsx +2 -2
  18. package/package.json +8 -7
  19. package/src/capture_row.tsx +59 -0
  20. package/src/checklist.tsx +104 -0
  21. package/src/chip.tsx +12 -3
  22. package/src/detail_row.tsx +137 -10
  23. package/src/inline_date_picker.tsx +8 -3
  24. package/src/inline_edit.tsx +40 -10
  25. package/src/inline_member_select.tsx +3 -0
  26. package/src/inline_number_input.tsx +5 -2
  27. package/src/inline_select.tsx +8 -3
  28. package/src/inline_tag_select.tsx +140 -0
  29. package/src/inline_text_input.tsx +5 -2
  30. package/src/inline_time_picker.tsx +5 -2
  31. package/src/ledger.tsx +220 -0
  32. package/src/locale.tsx +25 -0
  33. package/src/page_header.tsx +0 -2
  34. package/src/popover_nav.tsx +40 -0
  35. package/src/progress_bar.tsx +32 -1
  36. package/src/record_summary.tsx +101 -0
  37. package/src/section_heading.tsx +16 -8
  38. package/src/suggestion_chip.tsx +47 -0
  39. package/src/trend_footer.tsx +3 -1
  40. package/src/use_screen_size.ts +1 -1
  41. package/src/use_section_nav.test.ts +69 -0
  42. package/src/use_section_nav.ts +59 -0
  43. package/examples/tpl_billing.tsx +0 -344
  44. package/examples/tpl_detail.tsx +0 -232
  45. package/examples/tpl_directory.tsx +0 -260
  46. package/examples/tpl_intake.tsx +0 -206
  47. package/examples/tpl_order.tsx +0 -482
  48. package/examples/tpl_quick.tsx +0 -211
  49. package/examples/tpl_record_plain.tsx +0 -259
  50. package/examples/tpl_settings.tsx +0 -178
  51. package/examples/tpl_timeline.tsx +0 -244
  52. package/examples/tpl_wizard.tsx +0 -223
  53. package/src/animation_horizontal_slide.tsx +0 -75
  54. package/src/form_time_picker.tsx +0 -22
  55. package/src/highlighted_text.tsx +0 -92
  56. package/src/menu_title.tsx +0 -15
  57. package/src/pager_view.tsx +0 -167
  58. package/src/popover_header.tsx +0 -38
package/AGENTS.md CHANGED
@@ -12,6 +12,10 @@ typeahead, async search, virtualization, and a11y the primitive already ships.
12
12
  `PortalHost` at the app root.
13
13
  - RN-Web only: `View`/`ScrollView` from `react-native`, the `Text` primitive (no raw
14
14
  `div`/`span`, no raw `fontSize`/`fontWeight`); styles are RN objects.
15
+ - Fonts: the app entry imports the Inter sheet ONCE — `import "@lotics/ui/fonts.css"` (400/500/600,
16
+ served by absolute URL so it resolves on every origin an app runs from) — or every `Text` falls
17
+ back to system fonts. File picking: `pickFiles` (`@lotics/ui/file_picker`) opens the browser
18
+ picker and resolves the chosen `File[]` — the imperative half behind every Add-file CTA.
15
19
  - i18n: most components are string-free. For the rest, set the language ONCE at the root with
16
20
  **`LoticsLocaleProvider`** (`@lotics/ui/locale`, the sibling of `LoticsThemeProvider`) — wrap
17
21
  `<LoticsLocaleProvider locale={vi}>` and every wired component picks up the pack, no per-instance
@@ -75,12 +79,12 @@ Pick by capability, not by name. (→ the source file for the API.)
75
79
  `InlineNumberInput` · `InlineSelect` · `InlineMemberSelect` · `InlineDatePicker` ·
76
80
  `InlineTimePicker`; a READ-ONLY field in that same column uses `InlineStatic` (matches the
77
81
  editor box exactly, no input chrome, so it aligns pixel-for-pixel) (see §Data entry).
78
- - **Tasks / to-dos**there is NO Task component (a task row varies too much app-to-app to abstract
79
- well a Reminders row and a Linear column-grid share almost nothing). Compose the row directly:
80
- `CheckCircle` (the completion ring fills with a spring, distinct from the square `Checkbox`) + an
81
- inline-editable title (`InlineTextInput` with `struck` for done) + whatever meta cells you add
82
- (`OptionBadge` dot tags, `InlineDatePicker`, `InlineMemberSelect`). The one reusable primitive is
83
- `CheckCircle`; the rest is composition — see `tpl_tasks` (quick list) and `tpl_task_board` (columns).
82
+ - **Tasks / to-dos — pick by ALTITUDE.** A task-management PAGE (many tasks, grouping, filters,
83
+ expandable rows) is COMPOSITION there is NO Task component (a Reminders row and a Linear
84
+ column-grid share almost nothing): `CheckCircle` + a struck `InlineTextInput` + your meta cells;
85
+ see `tpl_tasks` (quick list) and `tpl_task_board` (columns). A record's own small CHECKLIST
86
+ (the 5–8 tasks living in a drawer/section) is the `Checklist`/`ChecklistRow` compound +
87
+ `SuggestionChip` commons + `CaptureRow` — see `tpl_record`/`tpl_item_list`.
84
88
  - **Tabular data — pick by SCALE + intent.** Two columnar shapes, and the choice is about data size:
85
89
  - **High-volume register** (thousands+ you BROWSE) — `Table` (columns defined once; sortable
86
90
  headers via `SortHeader`; paired with `Pagination`) + read-only rows that open a `Drawer` to edit.
@@ -100,7 +104,9 @@ Pick by capability, not by name. (→ the source file for the API.)
100
104
  - **Numbers & charts** — `KPIStrip` (the dashboard stat band) · `SummaryLine` (the light inline
101
105
  register/list summary — below the toolbar, from the filtered rows) · `KPICard` / `Metric` (headline figures), `TrendChip`
102
106
  (delta), `Sparkline`, `BarChart` / `LineChart` / `PieChart` (the canonical SVG set — no
103
- recharts), `RingGauge`, `ProgressBar` / `StackedProgressBar` / `StepProgress`, `Breakdown`
107
+ recharts), `RingGauge`, `ProgressBar` (its `compact` prop = ONE row, track + a plain sm
108
+ tabular count beside it — the cell/heading/peek-trigger meter; a caption floating above a
109
+ tiny bar reads misaligned) / `StackedProgressBar` / `StepProgress`, `Breakdown`
104
110
  (a stacked bar + ranked share rows, pressable to drill; `maxRows` folds the long tail behind a
105
111
  "Show N more" toggle — `labels` to localize — so several facet cards align to one height in a row),
106
112
  `Funnel` (a CONVERSION funnel — ordered stages as bars that NARROW; the step conversion rate is the
@@ -180,15 +186,15 @@ This is the most common thing to get right. Match the JOB to the pattern:
180
186
  | You're capturing… | Reach for | Why |
181
187
  |---|---|---|
182
188
  | an EXISTING record's fields | **Inline edit** (`Inline*`) | edit in place, no form mode |
183
- | a brand-NEW record (a form) | **fieldset form** (`FormField` + the 2-col grid) | structured entry + a Save |
189
+ | 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 |
184
190
  | a RELATED record (pick or make) | **find-or-create** (`Combobox allowCustom`) | one control covers both |
185
191
  | REPEATING rows you build & revise | **line items** (create→preview→edit) | add / edit / remove, live totals |
186
- | CHARGES that bill onto documents | **billing** (`tpl_billing`) | the invoice document is the unit |
192
+ | CHARGES that bill onto documents | **billing** (`tpl_record` Billing section) | the invoice document is the unit |
193
+ | 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 |
187
194
  | a multi-value TAG field | **`Select multi`** (`renderSelected` → `Chip`) | chips composed, not a separate control |
188
195
  | ONE choice from a small visible set | **`ChipGroup` pills** (or `RadioPicker`) | required single-select, one tap, every option visible |
189
196
  | a STATUS with terminal outcomes | **disposition** (open → resolve → revise) | guides the decision |
190
197
  | FILES | **attachment field** (dropzone + grid + gallery) | add / preview / delete |
191
- | many entries FAST | **quick capture** (one row, Enter to add) | repeat-entry speed |
192
198
  | a state TRANSITION mid-flow | **stage gate** (popover / dialog by weight) | right-sized friction |
193
199
 
194
200
  ### Inline edit — the preferred way to edit an existing record
@@ -200,19 +206,33 @@ reflow, the whole point), and it commits on blur (Enter saves, Escape reverts) o
200
206
  (`format` for currency/units) · `InlineSelect` (plain options OR `renderOptionContent`; floats an
201
207
  `OptionList` in a popover so the row never grows; the RESTING value renders like its option
202
208
  (`renderOptionContent` by default; `renderSelected` overrides) — a colored `OptionBadge`, not just a label) · `InlineMemberSelect` (a `MemberChip` at rest →
203
- member picker; the inline twin of `MemberSelect`) · `InlineDatePicker` (`format="datetime"` for
209
+ member picker; the inline twin of `MemberSelect`; worked example: `tpl_record`'s "Sales owner" fact) · `InlineDatePicker` (`format="datetime"` for
204
210
  always-on time; `optionalTime` to let the user ADD/REMOVE a time — the value's own shape, date vs
205
- datetime, is the source of truth) · `InlineTimePicker` — all
211
+ datetime, is the source of truth) · `InlineTimePicker` · `InlineTagSelect` (the MULTI member
212
+ a tag SET in the inline vocabulary: selected tags render as badges inside the standard chip,
213
+ clicking floats a multi `OptionList` (checkbox rows), CLOSING commits the new set in one
214
+ `onSave` — never a borderless `Select` posing as an inline field) — all
206
215
  on `useInlineEdit` + `InlineEditView` (custom inputs join via those). `onSave` is async: the
207
216
  saving spinner sits INSIDE the control (never a sibling — that reflows); an error shows inline
208
- without losing the edit. Pair with `DetailRow` (label left, inline value right). A READ-ONLY value
209
- in the same column a computed total, a system ID, a synced/locked field — is `InlineStatic`: it
210
- copies the editor box exactly (height, padding, 1px transparent border) but is non-interactive, so
211
- it sits FLUSH with the editors instead of reading as a disabled input. To hang a right-side
212
- action/badge/unit off a row, use `DetailRow`'s `trailing` slot (wrap the editor in a `flex:1` View
213
- so it takes the slack and the trailing pins right) see the "Row anatomy" section of
214
- `tpl_record_plain`, which also reads top→bottom as a full drawer record (header fields
215
- `DangerZone`). Not every field is a same-height swapa tag field, a status, or an attachment grid
217
+ without losing the edit. A STACK of rows lives in a `DetailTable` (label ·
218
+ value · trailing laid out like a TABLE: `labelWidth` / `trailingWidth` /
219
+ `minHeight` set ONCE on the parent, plus the 6px row gap the zinc-50 chips
220
+ need) holding `DetailRow`s set `trailingWidth` when ANY row carries a
221
+ trailing action/badge, so EVERY row reserves the column and one row's
222
+ `Copy` button never makes its editor narrower than its neighbours'. An editor AT REST sits
223
+ on a zinc-50 chip THE editability affordance: users see what's editable without hovering.
224
+ `background="transparent"` opts a field out of the chipfor DENSE, uniformly-editable
225
+ surfaces (a task list/board where EVERY cell edits: the chip repeated everywhere is noise and
226
+ distinguishes nothing; hover/focus still reveal the input). Keep the chip wherever editable and
227
+ static values MIX. A `disabled` editor rests FLAT automatically — the chip is the editability
228
+ promise, and an inert field must not make it. A
229
+ READ-ONLY value in the same column — a computed total, a system ID, a synced/locked field — is
230
+ `InlineStatic`: it copies the editor box metrics exactly (height, padding, 1px transparent border)
231
+ but stays FLAT and non-interactive, so editable (chip) vs read-only (flat) is legible at a glance
232
+ and the static value never reads as a disabled input. To hang a right-side
233
+ action/badge off a row, use `DetailRow`'s `trailing` slot (NOT for units — "kg"/"₫" belong IN the value via `InlineNumberInput format`) — see the "Details"
234
+ `DetailTable` of `tpl_record`, which also reads top→bottom as a full record surface
235
+ (header → fields → `DangerZone`). Not every field is a same-height swap — a tag field, a status, or an attachment grid
216
236
  edit in place too (below).
217
237
 
218
238
  ### Fieldset form — fields lay out on a RESPONSIVE two-column grid
@@ -250,8 +270,11 @@ the chevron shows while the field is empty and a clear ✕ replaces it once ther
250
270
  pressing ✕ clears the text and reopens the full-browse list, so `onClear` lands back on the whole
251
271
  set (on focus an empty field already browses everything — `recentOptions ?? all`). `allowCustom` appends a "Create …" row (`customOptionLabel`) when the query
252
272
  matches no option, emitting the typed text as the value — so a value not in the known set means
253
- CREATE. Wire that branch to a create overlay (a modal `Dialog` for 4+ fields, an anchored popover
254
- for 1–3) that builds the new record and attaches it; existing matches attach directly. The create
273
+ CREATE. Wire that branch to a create overlay (an anchored popover for a 1–2 field gate, a modal
274
+ `Dialog` for 3+) that builds the new record and attaches it; existing matches attach directly.
275
+ A record-CREATION dialog stays a MINIMAL gate and really creates — the row lands and its own
276
+ workspace opens for refinement (an EMPTY checklist, commons as ghosts). NEVER a creation
277
+ wizard: create-then-refine puts complexity on the record surface, not in front of it. The create
255
278
  row sits BELOW matches by default (the keyboard highlight lands on the first MATCH, so Enter on a
256
279
  partial picks it, never a duplicate); pass `customOptionPlacement="top"` to pin it above. Use
257
280
  `reflectSelection={false}` and render the attached record below as a card with a Change action.
@@ -271,12 +294,61 @@ snapshot the item on Edit so Cancel reverts, or DISCARDS a freshly-added one. Du
271
294
  the destructive **Delete is `danger-secondary`**; Cancel + the Save/Edit toggle sit right. Every
272
295
  screen composes its own (~15 lines) so the preview rows + form fit the data.
273
296
 
274
- ### Billing & invoicing the invoice DOCUMENT is the unit (`tpl_billing`)
297
+ ### Handoffa stage transition, never a message
298
+ When work crosses departments (sales → operations → accounting), the HANDOFF is the RECORD
299
+ changing desks — never an inbox, a notification, or a copied task. Two types:
300
+ **(A) Same entity** → a STAGE field on the shared record: sections carry OWNER dot tags, and
301
+ the handoff is MANAGED AS TASKS — each desk's checklist on the record (the task-row vocabulary:
302
+ sibling `CheckCircle` + struck transparent `InlineTextInput` + per-task `InlineMemberSelect`
303
+ assignees + the `CaptureRow` add-affordance — never a decorative progress strip). A NEW record
304
+ starts with an EMPTY checklist — tasks truly vary. The commons split in two: MANDATORY tasks are
305
+ seeded by the app (a workflow on create, per record type) — no human types them; common-but-
306
+ OPTIONAL tasks appear as `SuggestionChip`s under the list — a PILL, never a row,
307
+ so a suggestion can't be mistaken for a task (tap = materialize, ✕ = dismiss for this record;
308
+ already-present labels filter out; suggestions never count in done/total and pause while a
309
+ filter narrows the view) — the same suggestion grammar as the
310
+ 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
311
+ tasks INFORM
312
+ the handoff, they NEVER block it: the CTA stays enabled, the count warns, and open tasks carry
313
+ over. The handoff CTA opens a DIALOG for the receiving desk (assignee `MemberSelect` + an
314
+ optional note; the open-task warning inside); confirming closes the drawer — the record left
315
+ this register. Tasks PEEK from the register: the done/total column is a pressable
316
+ compact-`ProgressBar` trigger whose popover holds the checklist on FIXED columns — every row
317
+ ring · struck `InlineTextInput` title (flex) · quick-reassign `InlineMemberSelect` (140) — no
318
+ expandable rows (tags/files depth is the Task list template's lesson, not the peek's); the
319
+ popover body is `PopoverContent`'s own ScrollView (`disableBodyScroll` is ONLY for children
320
+ that manage their own scroll, like `OptionList`). The DRAWER carries a real Tasks SECTION in
321
+ the Record template's shape (heading + the compact meter + the same checklist) — one shared
322
+ task state per record feeds the column, the peek, the section, and the handoff dialog;
323
+ a section owned by a later desk sits visible but GATED ("Billing opens when the record reaches
324
+ Accounting"). Registers scope by stage — the receiving department's register IS its inbox. The
325
+ sender's sections stay editable after handoff (the gate transfers RESPONSIBILITY, not access —
326
+ locking is app IAM, not template grammar). Worked example: `tpl_record`.
327
+ **(B) Different entity** (a deal → a shipment) → SPAWN-AND-LINK: the upstream record's terminal
328
+ gate CREATES the downstream record on the shared spine, linked both ways (a linked-record row
329
+ each side); usually TWO apps — the upstream closes its own lifecycle, the downstream starts
330
+ fresh in its department's app. Never merge the two lifecycles into one record.
331
+
332
+ ### Sequential phases on one record + the outline rail
333
+ When a record's work happens in ORDERED PHASES (gate in → gate out; receive → dispatch), the
334
+ phases are NOT tabs or a segmented control — hiding the other phase loses the context the
335
+ current one needs and buries why its gate is blocked. Both phase sections sit on ONE page in
336
+ chronological order; each phase owns ITS OWN fee rows + phase total (the invoice-band idea one
337
+ level up — money never reads as one pot); each closes with a SEQUENTIAL confirm gate that names
338
+ its blockers ("Confirm gate in first."). The header status chip carries the phase. A LONG record
339
+ surface pairs with a LEFT OUTLINE RAIL — `MenuButton` items + `useSectionNav` (scroll-spy: jump
340
+ to a section, the highlight follows the scroll); on narrow containers the rail becomes a
341
+ PINNED bar naming the current section that opens a full-page section-picker `Modal`. A per-phase
342
+ dot on a rail item carries its confirmed state. A direction that is a TYPE (one record per gate
343
+ EVENT) is instead a discriminator chosen ONCE at creation — a `SegmentedControl` in the create
344
+ step, never a toggle on the record.
345
+
346
+ ### Billing — the invoice DOCUMENT is the unit (`tpl_record` Billing section)
275
347
  When charges get grouped into issuable documents (an e-invoice, a bill) and then collected, don't
276
348
  split the screen into "enter fees here, issue there" — that smears one job across two places. Make
277
- **each invoice a `Card` that holds its own editable charge lines** (amount input + payment method),
278
- its **live total**, its **status badge** (nothing-to-bill · draft · issued + ref), and its **issue
279
- action** in the footer. A charge never lives apart from the document it bills on. Issuing is gated
349
+ **each invoice a FLAT hairline-set band that holds its own editable charge lines** (amount input +
350
+ payment method), its **live total**, its **status badge** (nothing-to-bill · draft · issued + ref),
351
+ 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
280
352
  **inline, never a dead end**: when a prerequisite is missing (a bill-to tax ID, a method on a
281
353
  charged line) the issue button disables with one muted line saying what's needed; the payment-method
282
354
  picker turns required the instant a line carries an amount. Issuing a real e-invoice is irreversible
@@ -387,12 +459,6 @@ copy stay local):
387
459
  NEW blob for re-upload (`useImageRotation` is view-only; this is how a rotation is persisted). Pair with
388
460
  `FileGalleryModal`'s `onPersistRotation`/`persisting` (the ✓ shown on a rotated image).
389
461
 
390
- ### Quick capture — the SPEED surface
391
- For fast repeat entry (logging activity, expenses), one compact capture row (a `SegmentedControl`
392
- mode, a `Combobox`-as-select, a `Counter`, an Enter-to-submit `TextInputField`); each entry commits
393
- on its own into an undoable log below, the field keeps focus and the prior choices carry over. No
394
- screen-wide Save.
395
-
396
462
  ### Stage gates — tiered by weight
397
463
  A transition that needs NO input is one click. 1–3 quick fields → a POPOVER FORM anchored to its
398
464
  action button (title + one-line stake + `FormField`s; confirm in `PopoverFooter`). A
@@ -528,7 +594,6 @@ a conversation, a single pass, or generated prose:
528
594
  taller") OR by editing a param directly — either re-flows it in place. For a design/document the
529
595
  user shapes over time.
530
596
 
531
-
532
597
  The next six output STRUCTURE the first four can't — when the answer is a panel of facts, a queue of
533
598
  decisions, or a ranked set, don't cram it into chat prose. Each pairs a template with a primitive:
534
599
  - **Answer desk** (`tpl_lookup`) — describe the goods on the LEFT → the agent RANKS the
@@ -595,11 +660,14 @@ was only ever about the review surfaces' sparkles/severity glyphs, not functiona
595
660
  a period filter — never a summary Badge (those belong to the KPI strip).
596
661
  · **Card header** (a card's own title band, or separate banded cards): `CardHeader` +
597
662
  `CardHeaderTitle` (`sm semibold`; `info` when the title alone doesn't define the numbers).
598
- · **Section title** (a substantial titled block a form, a timeline, a field group — INSIDE one
599
- multi-section card or drawer): a `Text size="sm" weight="semibold"` at the TOP of the section's
600
- content, sections separated by ONE `Divider`. Do NOT wrap each section in its own `CardHeader`
601
- (its auto-divider double-stacks hairlines around every title). A card-less full page uses
602
- `SectionHeading` + `SectionHeadingTitle`. NEVER a bare eyebrow standing in for a section title.
663
+ · **Section title** — ONE construct: `Section` > `SectionHeading` > `SectionHeadingTitle`
664
+ (lg semibold + optional muted `description`/`info`), everything left-aligned at the column
665
+ edge. The `Divider` goes BETWEEN sections (page column `gap: 32`, a bare hairline separating
666
+ one section from the next) NEVER directly under the heading, which orphans the title from
667
+ its own content. Inside a DRAWER, a full multi-section WORKSPACE uses the real
668
+ `SectionHeading` (lg) — its meta + CTA slots carry each section's count and action; a
669
+ `Text size="sm" weight="semibold"` stand-in is only for small sub-groups. Do NOT wrap each section in its own `CardHeader`, do NOT
670
+ hand-roll sm-semibold lead lines on a flat page. NEVER a bare eyebrow as a section title.
603
671
  · **Eyebrow / label** (`<Text size="xs" color="muted" weight="medium">` — **sentence case, NEVER
604
672
  `transform="uppercase"`**): a small quiet label above or beside content — an artifact tag
605
673
  ("Proposed", "Question", "Suggested edit"), a field name, a metric caption, a minor one-line
@@ -671,13 +739,16 @@ was only ever about the review surfaces' sparkles/severity glyphs, not functiona
671
739
  </Card>
672
740
  ```
673
741
  Never nest cards; never hand-compose a title or footer band. `info` is standard, not garnish.
674
- - **Flat work execution vs carded monitoring.** A `Card` is ALWAYS a bordered, lifted container —
675
- there is no "flat card". MONITORING / dashboard screens group with cards (and the `KPIStrip`
676
- band). WORK-EXECUTION screens — registers, lists, the worklists an operator acts on — are FLAT:
677
- the register/list sits DIRECTLY on the white canvas with no Card wrapper; separation comes from
678
- the row wash, gaps, and `Divider`/`CardHeader`/`CardFooter` bands used BARE. Reach for a Card only
742
+ - **Flat work execution & data capture vs carded monitoring.** A `Card` is ALWAYS a bordered,
743
+ lifted container — there is no "flat card". MONITORING / dashboard screens group with cards (and
744
+ the `KPIStrip` band). WORK-EXECUTION screens — registers, lists, worklists AND **data-capture /
745
+ record screens (forms, wizards, billing, settings, inline records) are FLAT**: content sits
746
+ DIRECTLY on the white canvas with no Card wrapper; separation comes from the row wash, gaps,
747
+ `SectionHeading` + `Divider` rhythm. Reach for a Card only
679
748
  where a screen holds a DISTINCT region that must be told apart from another; a single register or
680
- list filling the screen needs none.
749
+ list filling the screen needs none. ONE carve-out inside a flat form: **repeating SUB-OBJECTS
750
+ (line items) are each their own small Card** (`padding: 16`), so discrete items
751
+ read as discrete objects; hairlines between them blur into one run-on list.
681
752
  - **Summaries — two altitudes, never mixed.** A DASHBOARD opens with the boxed stat band
682
753
  `<KPIStrip items={[{label, value, trend?, caption?, tone?, info?}…]}>` (a `Card` of `KPICard`
683
754
  columns). A REGISTER/LIST (work-execution) screen instead carries a LIGHT inline
@@ -842,13 +913,11 @@ Full worked-example screens ship in the package — **read the source at
842
913
  `node_modules/@lotics/ui/examples/<name>.tsx`** (pure `@lotics/ui` + mock data, each a usable
843
914
  recipe for a screen JOB; copy and adapt). Pick by the job:
844
915
 
845
- - **Monitor & decide** — `tpl_dashboard` (KPIStrip + trends + attention list) · `tpl_stock`
846
- (10k-record faceted funnel) · `tpl_report` (scope-first lookup report a header period +
847
- one-of-N dimension search drives KPIs → aligned facets w/ a `maxRows` long-tail toggle →
848
- paginated register → export) · `tpl_tower` (live `StatusGrid` wallboard) · `tpl_pivot` (the
916
+ - **Analytics** — `tpl_dashboard` (KPIStrip + trends + attention list) · `tpl_stock`
917
+ (10k-record faceted funnel) · `tpl_tower` (live `StatusGrid` wallboard) · `tpl_pivot` (the
849
918
  `Matrix` cross-tab desk: one dimension × another, number + heat per cell + row/col/grand totals,
850
919
  press a cell to drill the list behind it) · `tpl_rollup` (hierarchical totals).
851
- - **Work execution** — every screen is FLAT (no Card) and ONE of two shapes: a register/list you
920
+ - **Work** — every screen is FLAT (no Card) and ONE of two shapes: a register/list you
852
921
  WORK EACH of (press a row → workspace `Drawer`) or a list you ACT ON MANY of (tick rows →
853
922
  `FloatingActionBar`) — never a side-by-side panel. `tpl_item_list` (THE canonical register and the
854
923
  consolidated work-list: search + a status `Select` + facet `FilterChip`s LEFT, the New CTA RIGHT in
@@ -856,11 +925,46 @@ recipe for a screen JOB; copy and adapt). Pick by the job:
856
925
  carries a leading checkbox; a select-all band, footer `Pagination`, and a `FloatingActionBar` for
857
926
  the bulk action over the ticked rows; a per-row PRINT button (the trailing action column) is the
858
927
  row's primary action and the ⋯ its overflow; a row that can't take the bulk action gets a DISABLED
859
- checkbox (here an "Awaiting docs" case — the same gating as a blocked run line) — a row press opens
860
- the workspace `Drawer`. This ONE template subsumes the old approvals / dispatch / batch / run
928
+ checkbox (here an "Awaiting docs" record — the same gating as a blocked run line) — a row press opens
929
+ the PRODUCTION workspace `Drawer`: a Details `DetailTable` showing every inline field type
930
+ (text · `InlineMemberSelect` · dates incl. optional-time · dot-select · money number · the
931
+ `InlineTagSelect` tag field · `InlineStatic` + System badge), a LINKED CASES section (the
932
+ customer's other records as `ListItem`s — the aligned row compound (title + description at
933
+ the column edge, its own 8px outdent; badge · fee · chevron right) — pressing one PUSHES an
934
+ EDITABLE workspace for that record inside the drawer via the hosted `ScreenRouter`; the
935
+ drawer HEADER swaps to back + the pushed id, back pops with scroll preserved), a Files section with its
936
+ OWN Add CTA (`FileRows` CRUD via `pickFiles` + an EXPECTED document as a ghost `FileRow
937
+ 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
861
938
  screens: register, per-row action, gated selection, and act-on-many in one) · `tpl_pick` (guided `ScanField` run — a single-focus task column with
862
939
  the pick path collapsed into an `Accordion` below) · `tpl_allocate` (`RemainderMeter` split — a
863
940
  source/remainder summary header over the invoice list + Apply) ·
941
+ `tpl_record` (THE record surface, create-then-refine: "New" = a small
942
+ dialog with the identity essentials + find-or-create → lands as a Draft; the surface IS the
943
+ editor — `RecordSummary` header over its key-fact `DetailTable` + a Details `DetailTable`
944
+ (one shared column grid, auto-stacking when narrow), a CUSTOMER section (TWO states, no
945
+ swap mode: attached = the read-only-first card — Edit/Done live in the SECTION HEADING and
946
+ swap the fields to inline chips, where a trailing Fetch fills contact + city from the
947
+ tax-ID registry, un-gating billing; Remove (danger) sits low and detaches the link → the
948
+ find-or-create search, whose custom row opens the create Dialog rendering the SAME
949
+ inline-chip table + Fetch), a FILES section (dropzone add, gallery preview, delete),
950
+ and a BILLING
951
+ 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
952
+ with the list price as ghost placeholder + a one-tap "Standard …" suggestion pill while
953
+ unset, "How paid…" select chips, NO per-band totals — the collect band owns the number; the
954
+ band's action row sits BOTTOM-RIGHT as hint → status dot → Issue, the hint naming exactly
955
+ what blocks (fees → method → the customer gate); an ISSUED invoice keeps its lines editable
956
+ and offers Re-issue — a new lookup code replaces the old, confirmed in the same Dialog;
957
+ validated receipt; separate refundable deposit) — and
958
+ the lifecycle is a HANDOFF CHAIN (Sales → Operations → Accounting → Closed): a TASKS section
959
+ speaking the FULL Task-list grammar — a clearable Group-by `FilterChip` (Desk/Assignee/Status)
960
+ + Assignee/Status filter chips derive the groups (empty groups drop; desk heads keep their
961
+ owner dots, assignee heads are `MemberChip`s), the add-a-task capture at the TOP landing on
962
+ the current desk, rows with per-task `InlineMemberSelect` assignees — EVERY desk's rows edit
963
+ (the stage gates the handoff and Billing, never task editing; planning ahead on a later desk
964
+ is normal work) — OWNER dot tags per section, per-stage handoff CTAs NEVER blocked (open
965
+ 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`)
966
+ jumps between the sections. Absorbed the
967
+ old order-form, inline-record, intake, settings, billing, and quick-capture templates) ·
864
968
  `tpl_tasks` (the QUICK LIST — Apple-Reminders shape for personal lists: `CheckCircle` rows you tick +
865
969
  expand in place to edit (`Inline*`, stable row — the meta summary never shifts on expand), colour-dot
866
970
  tags, an attachments field (`FileThumbnailGrid` → tap a tile to the gallery for download + confirmed
@@ -871,7 +975,9 @@ recipe for a screen JOB; copy and adapt). Pick by the job:
871
975
  popover grid → gallery), a **dynamic action column** (a per-row underlined action LINK driven by each
872
976
  task's `action` descriptor — attach/approve/open, each wired in a real app to its OWN workflow: one
873
977
  board, a different action per row; the link's pressable fills the cell to match the inline editors),
874
- a trailing **`ActionMenu`** (the ⋯ overflow — a `danger` Remove + confirm
978
+ a trailing **`ActionMenu`** (the ⋯ overflow — a `danger` Remove + confirm; CUSTOM pressable
979
+ cells (files, the action link) hover with the control-surface BORDER reveal (`HOVER_BORDER`),
980
+ the same language as the inline-editor cells beside them — never their own background wash
875
981
  today, more row actions later), per-group add rows that pre-set the group's field, and the same
876
982
  Draft-from-notes CTA). Both `tpl_tasks` and `tpl_task_board`: filters/search left, primary CTA right, one
877
983
  row; group-by holds only real dimensions, ✕ clears to ungrouped (no "Nothing" option). **`tpl_task_board` is the inline-managed
@@ -884,17 +990,17 @@ recipe for a screen JOB; copy and adapt). Pick by the job:
884
990
  (for tap-to-expand) with those controls nested inside: RNW renders the row AND each control as a `<button>`,
885
991
  so you get `<button>`s inside a `<button>` — invalid DOM + the inner controls drop out of the keyboard tab
886
992
  order. Give "expand" its own affordance (a trailing `Pressable` over the meta/chevron), as `tpl_tasks` does.
887
- - **Data capture** — `tpl_intake` (multi-fieldset form + attachments) · `tpl_order` (the
888
- transactional form: find-or-create + line items + attachment CRUD + 2-col grid) · `tpl_billing`
889
- (charges→invoice→collect: the invoice DOCUMENT is the unit editable charge lines + method,
890
- live total, status, inline-gated issue; validated receipt; separate deposit) · `tpl_quick`
891
- (quick-capture log) · `tpl_wizard` (`Stepper` form + review) · `tpl_record_plain` (inline edit + Stage
892
- disposition + `Select` tags + Documents grid; `tpl_record_plain` = card-less).
893
- - **Records & lookup** `tpl_detail` (full record + tabs) · `tpl_directory` (searchable register) ·
894
- `tpl_timeline` (audit feed).
895
- - **Planning & time** `tpl_calendar` · `tpl_attendance` · `tpl_shifts`.
896
- - **Administration** — `tpl_settings`.
897
- - **AI workflows** — *produce*: `tpl_dieline` (the design CANVAS: photo → stream → the dieline
993
+ - **Finance** — `tpl_statements` (the income statement, balance sheet, and cash flow statement
994
+ in ONE statement grammar: right-aligned column captions over fixed money columns, items
995
+ indented under group headers, a hairline rule above every subtotal, the grand total
996
+ DOUBLE-RULED, negatives in accounting parentheses, per-cell currency-free (the meta line says
997
+ VND once), no bars/charts. The three statements TIE net income retained earnings, closing
998
+ cash = the balance sheet's cash, the loan repayment moves the debt line — keep mock books
999
+ reconciled or the template teaches the wrong thing) · `tpl_report` (scope-first lookup report —
1000
+ a header period + one-of-N dimension search drives KPIs → aligned facets w/ a `maxRows`
1001
+ long-tail toggle paginated register export).
1002
+ - **Scheduling** — `tpl_calendar` · `tpl_attendance` · `tpl_shifts`.
1003
+ - **Agents** — *produce*: `tpl_dieline` (the design CANVAS: photo → stream → the dieline
898
1004
  reveals CENTRED on a pannable/zoomable surface, the floating composer morphing into `AgentProgress`,
899
1005
  a pinned live-edit params panel centre-right; prompt OR edit a param to iterate) ·
900
1006
  *Structure*: `tpl_lookup` (the ANSWER desk: converse left,
@@ -915,27 +1021,28 @@ section_heading (Section · SectionHeading · SectionHeadingTitle · SectionHead
915
1021
  option_badge (OptionBadge — a select value as its configured colored badge) ·
916
1022
  member_chip (MemberChip — avatar + name; the universal person render) ·
917
1023
  member_select (MemberSelect — a Picker of MemberChip options; the member picker) ·
918
- 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 · 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 ·
1024
+ 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 ·
919
1025
  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`) ·
920
1026
  text_input_field · number_input · search_input · form_field · checkbox · checkbox_input · switch ·
921
- radio_picker · counter · range_slider · date_picker · date_range_filter_field · time_picker ·
1027
+ 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 ·
922
1028
  inline_text_input · inline_number_input · inline_select · inline_member_select · inline_date_picker ·
923
1029
  inline_time_picker (the Inline* family — per-field editors on `inline_edit`'s `useInlineEdit` +
924
1030
  `InlineEditView`; `InlineSelect`/`InlineMemberSelect` render the resting value like its option — `renderOptionContent` by default, `renderSelected` to override — a chip/badge at rest, not just text) ·
925
1031
  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) ·
926
- list · list_item · menu_button · menu_list_item · detail_row (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/unit after the value) · 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 ·
927
- 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. Compose task rows directly, no Task component) · action_menu ·
1032
+ 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)) ·
1033
+ 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 ·
1034
+ 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 ·
928
1035
  floating_action_bar · filter_chip · column_filter (ColumnFilter — the typed per-column filter pill +
929
1036
  columnFilterToConditions; for a register filtering on several columns) · chip_group · search_input ·
930
1037
  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) ·
931
- 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) · popover · tooltip ·
1038
+ 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_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 ·
932
1039
  alert · peek · empty_state · completion_state · callout (Callout · CalloutTitle ·
933
1040
  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 ·
934
1041
  bar_chart · line_chart · pie_chart · ring_gauge · progress_bar · stacked_progress_bar · breakdown ·
935
1042
  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) ·
936
1043
  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 ·
937
1044
  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) ·
938
- 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 · share_or_download · rotate_image · avatar · skeleton · activity_indicator · loading · divider ·
1045
+ 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 ·
939
1046
  spacer · stack · section_card · page_header · page_content · calendar (calendar/index.ts) · gantt ·
940
1047
  comments_thread · agent_run (live streaming work feed) · agent_progress (its compact floating
941
1048
  expandable form — a composer's working state) · composer (Composer — the adaptive command/chat
@@ -956,6 +1063,6 @@ children slot — with FindingComparison, the expected-vs-actual body: labeled s
956
1063
  delta; the `finding` locale slice) ·
957
1064
  format_money · format_date · colors (solid · tint · ramp · ColorName ·
958
1065
  isColorName · asColorName — coerce a stored option/status token to a ColorName, neutral fallback) ·
959
- use_focus_ring (useFocusRing — keyboard-aware focus state for painting a control's own ring; see Focus rings) ·
1066
+ 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) ·
960
1067
  focus_ring_pressable (FocusRingPressable — a Pressable that rings on keyboard focus; the raw-control default) ·
961
1068
  control_surface (CONTROL_HEIGHT · CONTROL_RADIUS · FOCUS_RING · chipSurfaceStyle — the shared control-surface tokens).
@@ -75,7 +75,7 @@ export function TplAllocate() {
75
75
  <View>
76
76
  <View style={{ padding: 20, flexDirection: "row", alignItems: "flex-start", gap: 24, flexWrap: "wrap" }}>
77
77
  <View style={{ gap: 2, flexGrow: 1, flexBasis: 220 }}>
78
- <Text size="xs" color="muted" transform="uppercase">Payment received</Text>
78
+ <Text size="xs" color="muted" weight="medium">Payment received</Text>
79
79
  <Text size="xxl" weight="semibold" tabular>{formatMoney(PAYMENT.amount)}</Text>
80
80
  <Text size="sm" color="muted">{`${PAYMENT.from} · ref ${PAYMENT.ref} · ${PAYMENT.date}`}</Text>
81
81
  </View>
@@ -92,7 +92,7 @@ export function TplAllocate() {
92
92
  {/* the targets — the open invoices, each absorbing part of the payment */}
93
93
  <View>
94
94
  <View style={{ paddingHorizontal: 20, paddingVertical: 12 }}>
95
- <Text size="xs" color="muted" transform="uppercase">{`Open invoices · ${INVOICES.length}`}</Text>
95
+ <Text size="xs" color="muted" weight="medium">{`Open invoices · ${INVOICES.length}`}</Text>
96
96
  </View>
97
97
  <Divider />
98
98
  {INVOICES.map((inv, i) => (
@@ -222,7 +222,7 @@ function NhanVienWorkspace({ nv }: { nv: NhanVien }) {
222
222
  <View style={{ gap: 10 }}>
223
223
  <View style={{ flexDirection: "row", alignItems: "center" }}>
224
224
  <View style={{ flex: 1 }}>
225
- <Text size="xs" color="muted" transform="uppercase">This week</Text>
225
+ <Text size="xs" color="muted" weight="medium">This week</Text>
226
226
  </View>
227
227
  <Text size="sm" weight="medium" tabular>{nv.tongTuan}</Text>
228
228
  </View>
@@ -254,7 +254,7 @@ export function TplAttendance() {
254
254
  const onSort = (key: string) => setSort(cycleSort(sort, key));
255
255
 
256
256
  const dem = (tt: TrangThai) => NHAN_VIEN.filter((nv) => nv.trangThai === tt).length;
257
- const eyebrow = (label: string) => <Text size="xs" color="muted" transform="uppercase">{label}</Text>;
257
+ const eyebrow = (label: string) => <Text size="xs" color="muted" weight="medium">{label}</Text>;
258
258
 
259
259
  // The Today roster, ordered by the active sort (Hours sorts numerically).
260
260
  const sorted = sortBy(NHAN_VIEN, sort, (nv, key) =>
@@ -19,7 +19,7 @@ import {
19
19
  } from "@lotics/ui/calendar";
20
20
 
21
21
  // ─────────────────────────────────────────────────────────────────────────────
22
- // Template · Calendar & planning — the delivery/schedule desk. One real
22
+ // Template · Calendar — the delivery/schedule desk. One real
23
23
  // CalendarView (week grid) over the current week's deliveries / reconciliations /
24
24
  // customer visits, then a "Today" agenda card listing today's three slots.
25
25
  //
@@ -36,7 +36,7 @@ import { Funnel } from "@lotics/ui/funnel";
36
36
  import { formatMoney } from "@lotics/ui/format_money";
37
37
 
38
38
  // ─────────────────────────────────────────────────────────────────────────────
39
- // Template · Executive dashboard — the executive operations dashboard. One
39
+ // Template · Dashboard — the executive operations dashboard. One
40
40
  // screen that answers "how is this month going, where is it stuck, who pays
41
41
  // the bills".
42
42
  //