@lotics/ui 22.3.0 → 23.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 (70) hide show
  1. package/AGENTS.md +9 -2
  2. package/MIGRATION.md +142 -2
  3. package/docs/ai_patterns.md +22 -22
  4. package/docs/catalog.md +131 -99
  5. package/docs/composition.md +134 -42
  6. package/docs/data_entry.md +67 -31
  7. package/docs/templates.md +86 -68
  8. package/examples/tpl_allocate.tsx +5 -5
  9. package/examples/tpl_attendance.tsx +2 -2
  10. package/examples/tpl_calendar.tsx +6 -6
  11. package/examples/tpl_dashboard.tsx +9 -9
  12. package/examples/tpl_dieline.tsx +2 -2
  13. package/examples/tpl_item_list.tsx +64 -37
  14. package/examples/tpl_lookup.tsx +5 -5
  15. package/examples/tpl_pick.tsx +6 -6
  16. package/examples/tpl_pivot.tsx +3 -3
  17. package/examples/tpl_record.tsx +919 -611
  18. package/examples/tpl_report.tsx +2 -2
  19. package/examples/tpl_rollup.tsx +5 -5
  20. package/examples/tpl_shifts.tsx +5 -5
  21. package/examples/tpl_statements.tsx +7 -7
  22. package/examples/tpl_stock.tsx +2 -2
  23. package/examples/tpl_task_board.tsx +42 -26
  24. package/examples/tpl_tower.tsx +6 -6
  25. package/package.json +4 -3
  26. package/src/agent_run.tsx +4 -4
  27. package/src/breakdown.tsx +1 -1
  28. package/src/calendar/calendar_view.tsx +1 -1
  29. package/src/change_review.tsx +9 -8
  30. package/src/chip_group.tsx +12 -2
  31. package/src/choice_list.tsx +2 -2
  32. package/src/confidence.tsx +2 -2
  33. package/src/data_grid.tsx +1 -1
  34. package/src/detail_row.tsx +50 -58
  35. package/src/file_dropzone.tsx +1 -1
  36. package/src/file_gallery_modal.tsx +3 -3
  37. package/src/file_row.tsx +29 -4
  38. package/src/file_rows.tsx +1 -1
  39. package/src/finding.tsx +4 -4
  40. package/src/form_field.tsx +1 -1
  41. package/src/format_date.ts +2 -2
  42. package/src/heatmap.tsx +1 -1
  43. package/src/inline_button.tsx +84 -0
  44. package/src/inline_date_picker.tsx +17 -10
  45. package/src/inline_edit.tsx +341 -60
  46. package/src/inline_member_select.tsx +8 -3
  47. package/src/inline_number_input.tsx +11 -4
  48. package/src/inline_select.tsx +26 -13
  49. package/src/inline_text_input.tsx +27 -6
  50. package/src/inline_time_picker.tsx +10 -4
  51. package/src/input_metrics.ts +17 -0
  52. package/src/ledger.tsx +2 -2
  53. package/src/locale.tsx +7 -3
  54. package/src/matrix.tsx +1 -1
  55. package/src/number_input.tsx +18 -7
  56. package/src/pipeline.tsx +1 -1
  57. package/src/popover.tsx +1 -1
  58. package/src/press_door.tsx +1 -1
  59. package/src/pressable_highlight.tsx +1 -1
  60. package/src/progress_bar.tsx +3 -3
  61. package/src/record_summary.tsx +2 -2
  62. package/src/result_header.tsx +2 -2
  63. package/src/sequence.tsx +170 -0
  64. package/src/share_or_download.ts +2 -2
  65. package/src/step_progress.tsx +7 -5
  66. package/src/stepper.tsx +1 -1
  67. package/src/task.tsx +6 -6
  68. package/src/text_input_field.tsx +21 -4
  69. package/src/text_utils.ts +7 -1
  70. package/src/linked_record_box.tsx +0 -157
package/docs/catalog.md CHANGED
@@ -20,7 +20,7 @@ indexed in [AGENTS.md](../AGENTS.md).
20
20
  - **If the closest component lacks a capability, extend it** (a platform change every app
21
21
  inherits), never inline a one-off `View`/`Text` rebuild — that forfeits the typeahead,
22
22
  async search, virtualization, and a11y the primitive already ships.
23
- - Floating content (`Dialog` · `Popover` · `Tooltip` · `Alert` · `OptionList`) needs a
23
+ - Floating content (`Dialog`, `Popover`, `Tooltip`, `Alert`, `OptionList`) needs a
24
24
  `PortalHost` (`@lotics/ui/portal`) at the app root.
25
25
  - Worked examples referenced below (`tpl_record`, `tpl_item_list`, …) ship in
26
26
  [`../examples/`](../examples/).
@@ -95,8 +95,15 @@ Batch draft-form state → `useForm`.
95
95
 
96
96
  ### Edit a record's fields in place
97
97
 
98
- The `Inline*` family: `InlineTextInput` · `InlineNumberInput` · `InlineSelect` (single or
99
- `multi`) · `InlineMemberSelect` · `InlineDatePicker` · `InlineTimePicker`; a
98
+ The `Inline*` family: `InlineTextInput` (**`numberOfLines`** the line budget for a value a
99
+ reader must read WHOLE rather than recognise at a glance: a payment term, an address, a clause.
100
+ Above 1 the resting box RESERVES that many lines — the full editor height, not the height this
101
+ particular value needs — so clicking in moves neither the field nor anything below it; Enter
102
+ then inserts a newline and the value commits on blur. Both halves matter: a field that only
103
+ goes multiline once focused hides the text from the reader who never clicks, and one that
104
+ reserves less than it opens to shoves the rest of the form down the page on every click),
105
+ `InlineNumberInput`, `InlineSelect` (single or
106
+ `multi`), `InlineMemberSelect`, `InlineDatePicker`, `InlineTimePicker`; a
100
107
  READ-ONLY field in that same column uses `InlineStatic` (matches the editor box exactly, no
101
108
  input chrome, so it aligns pixel-for-pixel). A stack of labelled field rows lives in
102
109
  `DetailTable` + `DetailRow`; the record's identity band is `RecordSummary`; its money
@@ -151,13 +158,13 @@ Two columnar shapes, and the choice is about data size:
151
158
 
152
159
  ### Numbers & charts
153
160
 
154
- `KPIStrip` (the dashboard stat band) · `SummaryLine` (the light inline register/list summary
155
- — below the toolbar, from the filtered rows) · `KPICard` / `Metric` (headline figures),
161
+ `KPIStrip` (the dashboard stat band), `SummaryLine` (the light inline register/list summary
162
+ — below the toolbar, from the filtered rows), `KPICard` / `Metric` (headline figures),
156
163
  `TrendChip` (delta), `Sparkline`, `BarChart` / `LineChart` / `PieChart` (the canonical SVG
157
164
  set — no recharts), `RingGauge`, `ProgressBar` (its `compact` prop = ONE row, track + a
158
165
  plain sm tabular count beside it — the cell/heading/peek-trigger meter; a caption floating
159
166
  above a tiny bar reads misaligned. **The track clamps at 100%, the caption does not** — over
160
- its max it reads `2,100 / 2,000 · 105%`, because a meter that says "100%" when you are over
167
+ its max it reads `2,100 / 2,000 (105%)`, because a meter that says "100%" when you are over
161
168
  tells the reader they are exactly at the limit. Numbers format in the reader's locale, so
162
169
  never hand-format the value you pass in — when display precision differs from the true value
163
170
  (whole credits off a fractional balance), `formatValue` reshapes the caption text and leaves
@@ -206,8 +213,8 @@ controls is `PressableRow` + **`PressDoor`** — never `PressableHighlight`, whi
206
213
  button and wraps its children (a button must not contain interactive descendants). The
207
214
  door is an empty absolutely-positioned SIBLING of the content carrying the tab stop,
208
215
  accessible name, and focus ring; lift the content above it with `zIndex: 1`. `Table`/
209
- `TableRow` and `LinkedRecordBox` do exactly this internally, so a columnar register needs
210
- no assembly — reach for `PressDoor` only outside them.
216
+ `TableRow` does exactly this internally, so a columnar register needs no assembly —
217
+ reach for `PressDoor` only outside it.
211
218
 
212
219
  ### Filters & view controls
213
220
 
@@ -246,7 +253,11 @@ scoped to that region's focus; the region-wrapper next to `FileDropzone`'s dedic
246
253
  "files land without hunting for a dropzone", `FileRow` (a horizontal file/document LINE —
247
254
  badge-or-placeholder + name + meta + a composable `trailing` slot for a status badge /
248
255
  action / remove; `onPress` makes the whole row a pressable door, `trailing` stays an
249
- independently-pressable sibling; for checklists & readable lists), `FileBadge` (the two-tone
256
+ independently-pressable sibling; for checklists & readable lists. **`size`** — `sm` (default)
257
+ is the compact attachment line for many files scanned as a list; **`md` is the document-desk
258
+ row** (taller badge, `ListItem` height) for the FEW rows that ARE the section's subject — an
259
+ expected-document checklist, a slot a button fills. A two-row section at `sm` reads as an
260
+ appendix to the page rather than its point), `FileBadge` (the two-tone
250
261
  type mark), `FilePreview` / `FileGalleryModal`, `ImageGallery`; picking is `pickFiles`
251
262
  (`@lotics/ui/file_picker` — opens the browser picker and resolves the chosen `File[]`, the
252
263
  imperative half behind every Add-file CTA); for gated CRUD compose locally with
@@ -255,7 +266,7 @@ patterns doc indexed in [AGENTS.md](../AGENTS.md)).
255
266
 
256
267
  ### Specialized work surfaces
257
268
 
258
- `ScanField` (scan/verify), `Stepper` (a guided run / progress sequence — done · current ·
269
+ `ScanField` (scan/verify), `Stepper` (a guided run / progress sequence — done, current,
259
270
  upcoming, horizontal OR vertical), `RemainderMeter` + `AllocationRow` (allocation),
260
271
  `Timeline` (a heterogeneous event LOG — icons + expandable details, not progress),
261
272
  `Calendar` (the `calendar` module's views), `Gantt`, `comments_thread`.
@@ -266,15 +277,15 @@ upcoming, horizontal OR vertical), `RemainderMeter` + `AllocationRow` (allocatio
266
277
  long text + attachments; the surface that triggers agent work), `AgentRun` (the live
267
278
  streaming work feed) + `AgentProgress` (its compact, floating, expandable form — a
268
279
  composer's "working" state) + `Confidence`; **`ChangeReview` — THE one review-before-apply
269
- surface, a COMPOUND family** (frame: `ChangeReview` · `ChangeReviewHeader` ·
270
- `ChangeReviewActions`; sections: `Change` · `ChangeLabel` · `ChangeSummary` ·
271
- `ChangeReasoning`; the grammar: `ChangeFields` + `ChangeField` · `ChangeRecord` ·
280
+ surface, a COMPOUND family** (frame: `ChangeReview`, `ChangeReviewHeader`,
281
+ `ChangeReviewActions`; sections: `Change`, `ChangeLabel`, `ChangeSummary`,
282
+ `ChangeReasoning`; the grammar: `ChangeFields` + `ChangeField`, `ChangeRecord`,
272
283
  `ChangeBand` + `ChangeValueInput`): adds, updates, removals, conflicts, whole records,
273
284
  display-only findings are all compositions — see the AI-patterns doc indexed in
274
285
  [AGENTS.md](../AGENTS.md) for the laws; `Clarify` (the agent asks back — selectable
275
286
  `ChoiceList` options), `Sources` (provenance chips for AI output — at review scale,
276
287
  `label={null}` slots the chips at a section's bottom), `Finding` (one ranked insight from an
277
- AI check — localized severity word · title · detail · `Sources` chips · a `children` slot;
288
+ AI check — localized severity word, title, detail, `Sources` chips, a `children` slot;
278
289
  **`FindingComparison`** is the expected-vs-actual body: each disagreeing side a labeled row,
279
290
  the DELTA emphasized under a hairline (localized "Difference") — quantities, totals, dates;
280
291
  a plain `metric` prop remains for one-number findings. The children slot composes ANY visual
@@ -336,18 +347,18 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
336
347
  weekday/month names, and `formatDate` display all follow the provider with no per-instance
337
348
  `locale` prop — an explicit `locale` still overrides. **Limitation:** the calendar/gantt
338
349
  views and the comment labels are not yet provider-wired — pass their `labels` props directly.
339
- - **`colors`** — the palette + `withAlpha` · `solid` · `tint` · `ramp` · `ColorName` ·
340
- `isColorName` · `asColorName` (coerce a stored option/status token to a `ColorName`,
350
+ - **`colors`** — the palette + `withAlpha`, `solid`, `tint`, `ramp`, `ColorName`,
351
+ `isColorName`, `asColorName` (coerce a stored option/status token to a `ColorName`,
341
352
  neutral fallback).
342
353
  - **`tokens`** — design tokens: re-exports `colors` plus `space`/`type`/`weight`/`radius`
343
354
  scales and `getCssVariables()` — an OPT-IN serializer to `--lotics-*` CSS variables for
344
355
  hand-rolled plain DOM/CSS (nothing injects them automatically; `@lotics/ui` components
345
356
  don't need them).
346
357
  - **`spacing`** — the `SPACE` scale + `SpaceToken`.
347
- - **`control_surface`** — `CONTROL_HEIGHT` (40) · `CONTROL_RADIUS` (10) · `CONTROL_TEXT_INSET`
358
+ - **`control_surface`** — `CONTROL_HEIGHT` (40), `CONTROL_RADIUS` (10), `CONTROL_TEXT_INSET`
348
359
  (9 — how far a control insets its OWN text: 1px border + 8px padding; anything that must line
349
- up with a control's WORDS rather than its box carries it, and `TASK_TEXT_INSET` IS it) ·
350
- `FOCUS_RING` · `HOVER_BORDER` · `CONTROL_TRANSITION` · `chipSurfaceStyle` — the shared
360
+ up with a control's WORDS rather than its box carries it, and `TASK_TEXT_INSET` IS it),
361
+ `FOCUS_RING`, `HOVER_BORDER`, `CONTROL_TRANSITION`, `chipSurfaceStyle` — the shared
351
362
  control-surface tokens.
352
363
  - **`fonts.css`** — the Inter sheet (400/500/600, served by absolute URL so it resolves on
353
364
  every origin an app runs from); the app entry imports it ONCE or every `Text` falls back
@@ -380,15 +391,15 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
380
391
 
381
392
  ### Layout & surfaces
382
393
 
383
- - **`card`** — `Card` · `CardHeader` · `CardHeaderTitle` (+ `info` ⓘ popover) ·
384
- `CardHeaderMeta` · `CardBody` · `CardFooter`.
385
- - **`inset`** — `Inset`: a tinted, recessed content surface (zinc-50 fill · 10 radius · 14
386
- pad · 12 gap) — the "well" INSIDE a Card/Section for a grouped sub-form, an inline fill
394
+ - **`card`** — `Card`, `CardHeader`, `CardHeaderTitle` (+ `info` ⓘ popover),
395
+ `CardHeaderMeta`, `CardBody`, `CardFooter`.
396
+ - **`inset`** — `Inset`: a tinted, recessed content surface (zinc-50 fill, 10 radius, 14
397
+ pad, 12 gap) — the "well" INSIDE a Card/Section for a grouped sub-form, an inline fill
387
398
  editor, or a nested block. **NOT a `Callout`:** a Callout is a status BAND (and
388
399
  `warning`/`error` render as an ARIA `alert`) — wrapping form fields in an alert is wrong.
389
400
  The law: **content + fields → `Inset`; a message → `Callout`.**
390
- - **`section_heading`** — `Section` · `SectionHeading` · `SectionHeadingTitle` ·
391
- `SectionHeadingMeta` · `Subsection` · `SubsectionHeading` · `SubsectionHeadingTitle` ·
401
+ - **`section_heading`** — `Section`, `SectionHeading`, `SectionHeadingTitle`,
402
+ `SectionHeadingMeta`, `Subsection`, `SubsectionHeading`, `SubsectionHeadingTitle`,
392
403
  `DialogSectionHeadingTitle` — the
393
404
  card-less twin of the Card family, compound, owns no margin; spacing via the Section gap
394
405
  (16, fixed), no body component. `SectionHeadingTitle` is ALWAYS `##` (xl semibold;
@@ -399,7 +410,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
399
410
  md-semibold rung with the SAME `icon`/`description`/`info` slots as the section title, so a
400
411
  dialog surface loses only the type size, never an affordance. The heading ramp is FIXED:
401
412
  `#` xxl / `##` xl / `###` lg / `####` md, no size props.
402
- - **`section_stack`** — `SectionStack` · `SubsectionStack` — stacks that own the
413
+ - **`section_stack`** — `SectionStack`, `SubsectionStack` — stacks that own the
403
414
  between-block law, skipping null children: `SectionStack` = a fixed 56px beat + a hairline
404
415
  `Divider` between top-level blocks; `SubsectionStack` = a fixed 32px beat, space-only while
405
416
  the groups are SHORT (the titles carry the grouping, hairlines stay at the section level) and
@@ -467,7 +478,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
467
478
  the `chip.remove` locale slice). Suggestion pills → `SuggestionChip`.
468
479
  - **`action_menu`** — `ActionMenu`: the ⋯ overflow menu (`ActionMenuItem[]`; danger items
469
480
  last).
470
- - **`menu_button`** — `MenuButton`: the menu/rail row (icon · title · `right` slot;
481
+ - **`menu_button`** — `MenuButton`: the menu/rail row (icon, title, `right` slot;
471
482
  `focused`/`danger`; `role` menuitem|button|option) — popover menus, outline rails, section
472
483
  pickers. Its resting highlight has TWO meanings and they are not interchangeable:
473
484
  **`selected`** is listbox SELECTION (emitted as `aria-selected`, and only under
@@ -490,27 +501,8 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
490
501
  own controls. Pair it with `PressableRow` (surface takes the mouse + the wash, door takes
491
502
  the keyboard, nested controls keep their own presses); the parent owns the positioning
492
503
  context and the content lifts above the door with `zIndex: 1`. `radius` (default 10)
493
- matches the surface it spans. `TableRow` / `LinkedRecordBox` are the in-kit consumers
494
- reach for it when designing a row those don't cover.
495
- - **`linked_record_box`** — `LinkedRecordBox`: a bordered box scoping ANOTHER record's data
496
- (`icon` · `name` · `subtitle` · vertical `facts`); verbs ride an `actions` slot in a
497
- hairline-fenced footer at the bottom (destructive LEFT, go-to RIGHT — the divider draws itself
498
- when `actions` is present, so don't hand-add one). Use it wherever a record points at another (a
499
- shipment's customer, an invoice's party, a case's sibling). **`onOpen` picks the variant, and
500
- they are exclusive at the type level**: WITH it (+ the required `doorLabel`) the whole box is a
501
- keyboard door into the record's detail and `facts` values are TEXT (`""` → "—"); WITHOUT it the
502
- box is a STATIC card — no door, no tab stop, no pointer, nothing announced as a button — and a
503
- `facts` value may be a NODE (an inline editor; a node prints as authored, no "—"). A node fact
504
- gets the CONTROL BAND on both cells — its label centres on the control instead of printing at
505
- the band's top — while a string fact keeps its bare text line, so a long value that wraps still
506
- tops out level with its label. Reach for the
507
- static one when the linked record has NO page of its own: nothing to open, so the box is where
508
- its values are read and edited, and the only interactive parts are `actions` + the fact nodes.
509
- Under a door, `actions` is the ONLY place a control may live (a fact editor there would compete
510
- with the press). The door variant **enforces the a11y contract** copy-pasting got wrong: a
511
- container with interactive descendants is never `role="button"` (invalid HTML) — an internal
512
- empty door sibling carries the tab stop / name / focus ring, verbs lift above via `zIndex`.
513
- The reassign picker / empty state that swaps in for the box is the consumer's.
504
+ matches the surface it spans. `TableRow` is the in-kit consumer reach for it when
505
+ designing a pressable surface it doesn't cover.
514
506
  - **`pressable_highlight`** — `PressableHighlight`: the hover-wash + keyboard-focus-ring
515
507
  `Pressable` under `MenuButton`/`Switcher`/custom pressable surfaces; its style-fn/children
516
508
  receive `hovered` + `focusVisible`.
@@ -526,7 +518,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
526
518
  shortcut from a raw string or `ShortcutDescriptor` (⌘B on Mac, Ctrl+B elsewhere); null on
527
519
  small screens. `TextInputField shortcut` renders it built-in, while the field is EMPTY —
528
520
  it hints at reaching the field, so a value replaces it (with the clear ✕, when `clearable`).
529
- - **`keyboard`** — `isMac` · `ShortcutDescriptor` · `formatShortcut` (platform-aware
521
+ - **`keyboard`** — `isMac`, `ShortcutDescriptor`, `formatShortcut` (platform-aware
530
522
  shortcut formatting behind `ShortcutBadge`).
531
523
 
532
524
  ### Badges, status & feedback
@@ -538,7 +530,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
538
530
  (see composition.md "Badge is for status only"). A bare `<Badge>` is `dot`.
539
531
  - **`status_badge`** — `StatusBadge`: an enabled/disabled pulse badge (`enabled` + `label`).
540
532
  - **`option_badge`** — `OptionBadge`: a select value as its configured colored badge.
541
- - **`callout`** — `Callout` · `CalloutTitle` · `CalloutText` · `CalloutActions` (`tone`
533
+ - **`callout`** — `Callout`, `CalloutTitle`, `CalloutText`, `CalloutActions` (`tone`
542
534
  info|success|warning|error|neutral): inline status band — a MESSAGE (`warning`/`error`
543
535
  are ARIA `alert`s). For a form / nested content surface use **`Inset`**, never a Callout.
544
536
  - **`empty_state`** — `EmptyState`: centered placeholder for an empty list/filter result —
@@ -664,17 +656,30 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
664
656
  INSIDE the control and inline error, commit on blur (Enter saves, Escape reverts) or
665
657
  `controls="buttons"`; KEYBOARD focus on the closed view opens edit mode with the input
666
658
  focused (type → Tab → type — see the data-entry keyboard contract), pointer focus never
667
- does; **`variant: "form" | "cell"`** is THE axis that separates a form field from a data-grid
668
- cell (see below). Every commit registers in **`pending_commits`**, which
659
+ does; **`variant: "framed" | "bare"`** sets how much of the field's frame shows AT REST
660
+ (see below). Every commit registers in **`pending_commits`**, which
669
661
  `Button`/`IconButton` wait on via **`use_gated_press`** so an action pressed in the same
670
662
  gesture as the blur cannot read the record before the edit lands (data_entry.md § Inline
671
663
  edit) — automatic, nothing to pass.
664
+ **`actions` puts VERBS on the field's surface** (an `InlineButton` — Copy, Open, Today), and
665
+ EVERY `Inline*` editor takes it. `trailing` is decoration only (a chevron, a spinner): it
666
+ renders INSIDE the press target, so a button there would be a button in a button.
667
+ With `actions` the field renders through ONE shell that owns the surface in BOTH modes — so
668
+ the verbs never move when the editor swaps to its input, the control inside goes `seamless`
669
+ (drawing no second box), hover is tracked on the box (react-native-web hands a parent's hover
670
+ to the innermost pressable, so a control-tracked border drops out as the pointer crosses a
671
+ verb), and the focus ring paints on the whole FIELD via focus-within, not on the value region.
672
+ A popover-backed editor also anchors its overlay to the shell (`anchorRef`) rather than to the
673
+ control — with verbs the control is narrower than the field, and an inherited width would open
674
+ a list too small for its own row.
675
+ Pass `actions` UNCONDITIONALLY and `disabled` the verb when it has nothing to act on: a slot
676
+ that appears once the value is non-empty resizes the field as the user types.
672
677
  - **`inline_text_input`**, **`inline_number_input`** (`format` for currency/units),
673
678
  **`inline_select`**, **`inline_member_select`**, **`inline_date_picker`**
674
679
  (`format="datetime"`, `optionalTime`; keyboard focus opens the TYPED segmented `DateField`
675
680
  — locale field order, separator advances, Alt+ArrowDown floats the calendar; click still
676
681
  opens the calendar popover), **`inline_time_picker`** — the inline field editors (a form field by
677
- default, a grid cell with `variant="cell"` — see the split below);
682
+ the same editor everywhere, `variant="bare"` on a dense grid — see the surface note below);
678
683
  `InlineSelect`/`InlineMemberSelect` render the resting value like its option —
679
684
  `renderOptionContent` by default, `renderSelected` to override — a chip/badge at rest, not
680
685
  just text. **`InlineSelect` is single OR multi** — pass `multi` for a tag SET (`value: T[]`,
@@ -687,38 +692,64 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
687
692
  (`TextColor`) that colours the resting date by
688
693
  semantic state — overdue red, soon-due amber (the caller owns the rule, e.g. a task's `dueTone`);
689
694
  the value text IS the signal, no separate dot.
690
- **The form/cell split one `variant`, not a second family.** Every `Inline*` editor is BOTH a
691
- form field and a data-grid cell; `variant` picks which:
692
- - **`variant="form"`** (default) — a zinc-50 chip at rest + a hover-BORDER. THE editability
693
- affordance on a surface that MIXES editable and static values (a `DetailTable` / record field).
694
- - **`variant="cell"`** transparent at rest + a background-tint WASH on hover (`zinc-100` /
695
- `zinc-200` press the same `PressableHighlight` language as the row/cell beside it), and it reads
696
- VALUE-FIRST: the date drops its resting calendar glyph, a select drops its chevron (the column
697
- header + the uniformly-editable grid are the affordance; the saving spinner still shows). For a
698
- DENSE, uniformly-editable surface a register/`DataGrid` column, a task row (worked example:
699
- `tpl_task_board`'s columns, `tpl_item_list`'s rows).
700
-
701
- There is NO separate `*Cell` component family a data-grid cell is `<InlineSelect variant="cell" …/>`,
702
- not a `SelectCell`. (Naming a component for a USE, and duplicating the picker stack to flip two
703
- style properties, are both things the kit forbids.) A custom pressable cell you hand-roll should
704
- match the `variant="cell"` language: the `PressableHighlight` wash, never a border.
695
+ **ONE surface, ONE hover language; `variant` sets its resting WEIGHT.** An editor wears THE pill
696
+ surface white, 1px border, the same one `Chip`, `ChipGroup` and a secondary `Button` wear — so
697
+ every control a row can hold reads as one family. Open adds the 2px ring; disabled rests flat and
698
+ borderless, promising no press. HOVER differs by variant, because hover must be a visible CHANGE
699
+ rather than a deeper line: `framed` deepens its border AND tints (zinc-200 → zinc-400 alone is a
700
+ shade shift on 1px), `bare` has the border ARRIVE (loud by itself — and a tint there would make a
701
+ hovered cell lighter than the row washing beneath it).
702
+ - **`variant="framed"`** (default) the frame is already showing. Required wherever editable and
703
+ static values MIX (a record's `DetailTable`): there it is the only thing saying what you can change.
704
+ - **`variant="bare"`** — nothing at rest, the SAME border on hover. For a surface where EVERY value
705
+ edits (a register/`DataGrid` column, a task row), where a per-field frame states what the whole
706
+ surface already promises and, down a column, draws the grid twice.
707
+
708
+ It is an axis of weight, not of use: both hover and open identically. There is no `*Cell` family
709
+ a data-grid cell is `<InlineSelect variant="bare" …/>`, not a `SelectCell` (naming a component for a
710
+ USE, and duplicating the picker stack to flip two style properties, are both things the kit forbids).
711
+ Hand-roll a pressable cell to match `bare`, never a background wash.
705
712
  - **`inline_static`** — `InlineStatic`: a READ-ONLY value matching the Inline\* box metrics
706
713
  EXACTLY (height, padding, 1px transparent border) so a non-editable field — a computed
707
714
  total, a system ID, a synced/locked value — aligns pixel-for-pixel in the same column;
708
715
  non-interactive, NOT a disabled input; `muted`/`tabular`/`align="right"` for a number
709
716
  column, `weight="medium"` to emphasise a total among plain rows.
717
+ - **`sequence`** — `Sequence` + `SequenceItem` (+ `SEQUENCE_INSET`): an ORDERED list whose ORDER
718
+ IS THE DATA — a route's stops, an approval chain, a set of legs — drawn as a connected rail so
719
+ the sequence reads without a label saying "first"/"then". Reach for it when a thing has a
720
+ VARIABLE number of positions: one field per position (`Origin` / `Transfer point` /
721
+ `Destination`) cannot hold a second middle entry and cannot say the entries are ordered.
722
+ `SequenceItem` takes the content, an optional `role` (DERIVE it from index — a stored role lies
723
+ the moment the list is reordered) and `onMoveUp`/`onMoveDown`/`onRemove`; the controls render
724
+ even where they cannot act so the right edge never shifts between items. Reorder is BUTTONS,
725
+ not a drag handle — dragging is invisible to the keyboard and fiddly on a phone, and a
726
+ three-to-six position list does not need it. Indent anything that follows the list (an "Add"
727
+ link) by `SEQUENCE_INSET` so it lands on the rail's column. Distinct from `Timeline` (an
728
+ activity FEED), `Stepper` (a wizard's fixed positions) and `Pipeline` (ONE record walking
729
+ stages that own their controls) — this is the list a user EDITS.
730
+ - **`inline_button`** — `InlineButton`: a verb that sits INSIDE an inline field's surface
731
+ (the `Open` on a reference, a `Copy` on a value worth copying) rather than in the row's
732
+ trailing column — put it inside when the act is ABOUT THE VALUE, so it travels with what it
733
+ acts on and the trailing column stays free for row-level verbs. FILLED (`Button secondary`'s own
734
+ zinc-100) on the field's white surface — the contrast is a RELATIONSHIP, not a colour: invert it
735
+ if the resting field surface ever changes, or the verb dissolves into the value. 28px, radius 8,
736
+ no border (the field already has one) and no shadow (depth belongs to the primary action alone). **It stops propagation** — that is the contract: the
737
+ surface underneath is itself pressable, so without it one press fires both, silently. Always
738
+ a WORD, never icon-only.
710
739
  - **`detail_row`** — `DetailTable` + `DetailRow` — the record field grid. `DetailRow`:
711
740
  label+value row for drawer/peek detail; in FORM mode (`labelWidth` set) the value column
712
741
  FILLS the row so a stack of inline editors all span the same width + none jumps wider on
713
- edit; optional `trailing` slot renders a right-side action/badge after the value (units
714
- belong IN the value via `InlineNumberInput format`). The LABEL WRAPS inside its column and
742
+ edit. There is NO trailing slot: a verb about the VALUE goes on the field (`InlineButton` in
743
+ the editor's `actions`), and anything else that belongs beside the value — a status `Badge`, a
744
+ unit (though "kg"/"$" belong IN the value via `InlineNumberInput format`) — composes into a
745
+ row inside the value cell, costing only the row that wants it. The LABEL WRAPS inside its column and
715
746
  is never clipped — a fixed `labelWidth` would otherwise ellipsize every long field name
716
747
  ("Registered business address"), and a name the reader can't finish is worse than a taller
717
748
  row; a wrapped label's FIRST line stays level with the value's first control line while a
718
749
  one-line label still centers on it (both modes, no prop). The FIELD-ANNOTATION vocabulary (same
719
750
  names + meanings as `FormField`), always EXPLICIT — a row never hides guidance behind an ⓘ:
720
751
  **`description`** = a fact / persistent guidance (muted), under the VALUE (stacked mode
721
- mirrors the form order label · description · control); **`warning`** = a consequence to weigh
752
+ mirrors the form order label, description, control); **`warning`** = a consequence to weigh
722
753
  before acting (amber, announced); **`error`** = field-level failure under the value, danger +
723
754
  alert semantics (the `Inline*` editors already render their own transient save errors — don't
724
755
  wire both); invalid/cross-field STATE = a co-located `Callout`; **`flat`**
@@ -729,19 +760,20 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
729
760
  on two surfaces.
730
761
  `DetailTable`: the compound parent of
731
762
  a row STACK — `labelWidth` (default `DETAIL_LABEL_WIDTH`, 130 — THE kit's label column, and
732
- a `TaskList`'s default too) / `trailingWidth` / `minHeight` (default 40, the
733
- inline-control grid) declared ONCE + the 8px row gap; with `trailingWidth` every row
734
- reserves the trailing column so value cells share one width and trailing items align at
735
- one x, like a table. RESPONSIVE with no prop: it measures its own container (onLayout, not
763
+ a `TaskList`'s default too) / `minHeight` (default 40, the inline-control grid) declared
764
+ ONCE + the `SPACE.md` (16) row gap 8 was right while a field was a tint, but a stack of
765
+ BORDERED rows that close together fuses into one block. TWO columns, no third: the value
766
+ FILLS what the label leaves, so nothing one row does can narrow its neighbours' editors. RESPONSIVE with no prop: it
767
+ measures its own container (onLayout, not
736
768
  the viewport — works inside a Drawer; the unmeasured first frame renders opacity-0 so the
737
769
  first PAINT is already in the right mode) and when the columns would crush the value cell
738
770
  it STACKS every row (the label above a full-width value row in the FormField label
739
- grammar; trailing beside the value); raise `minValueWidth` (default 160) when a cell holds
771
+ grammar); raise `minValueWidth` (default 160) when a cell holds
740
772
  MORE than one editor so the table stacks earlier. Two tables on one page share one grid by
741
- repeating the same labelWidth/trailingWidth. Worked example:
773
+ repeating the same labelWidth. Worked example:
742
774
  [`tpl_record`](../examples/tpl_record.tsx).
743
775
  - **`record_summary`** — `RecordSummary`: the identity band of a record detail/drawer — ONE
744
- row: `title` xxl semibold tabular · `subtitle` sm muted · `status` Badge slot · optional
776
+ row: `title` xxl semibold tabular, `subtitle` sm muted, `status` Badge slot, optional
745
777
  `metric` {label,value,tone,note} pinned right, the band's ONE accent. The record's FIELDS
746
778
  never live in the header: compose them as `DetailTable`s in the sections below. Replaces
747
779
  hand-rolled record headers (mixed scales, several competing figures, color noise).
@@ -819,7 +851,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
819
851
 
820
852
  `TaskStatus` takes the `CheckCircle` (omit `onChange` for a read-only ring; a PICKER list
821
853
  puts a `CheckboxInput` here and sets `controlWidth={24}`). `TaskTitle` takes the
822
- `variant="cell"` `InlineTextInput` — or the title TEXT itself as a string child
854
+ `InlineTextInput variant="bare"` — or the title TEXT itself as a string child
823
855
  (`<TaskTitle struck={done}>{label}</TaskTitle>`), which is the READ-ONLY form: the compound
824
856
  applies the cell inset and the row's band, so a plain `<Text>` + a hand-rolled
825
857
  `TASK_TEXT_INSET` is never needed and a long title that wraps keeps its first line beside the
@@ -831,7 +863,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
831
863
  gutter on the first line (Delete lives BEHIND it, danger-styled and last, never a bare ✕).
832
864
  `TaskDetail` is a FREE-FORM block under the row on the title's text edge — a chart, a table, a
833
865
  form with its own submit — rendered only while open. The module also exports
834
- **`TASK_TEXT_INSET`** (9), the inset a `variant="cell"` control puts on its own text: every
866
+ **`TASK_TEXT_INSET`** (9), the inset an inline control puts on its own text: every
835
867
  slot above already applies it, so reach for it ONLY when a custom title NODE (not a cell
836
868
  control) has to land on the same text edge.
837
869
 
@@ -844,7 +876,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
844
876
  step inward per level. Declare `actionWidth={0}` when NO row in the list carries actions.
845
877
 
846
878
  **THE ROW CARRIES THE TITLE; THE FIELDS THE USER CAN SET ARE SUB-ROWS BENEATH IT.** One task's
847
- own fields hang under it as **`TaskSubRow`** (`label` · the control · `description` /
879
+ own fields hang under it as **`TaskSubRow`** (`label`, the control, `description` /
848
880
  `warning` / `error`), indented ONE step — the same step a nested `TaskList` takes, because
849
881
  belonging is expressed by indentation and there is only one device for it. Label and value sit
850
882
  ADJACENT so the eye pairs them. **The label column is the LIST's, not the row's** — one
@@ -855,7 +887,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
855
887
  vocabulary with longer field names staggered nearly every row). The value takes the slack from
856
888
  a readable minimum — on a phone or in a narrow drawer it drops onto its own line under the
857
889
  label rather than ellipsizing beside it. Pass a form-variant `Inline*` editor (the default),
858
- not `variant="cell"`: a cell variant belongs to a grid.
890
+ `variant="bare"`: a dense grid drops the resting frame.
859
891
 
860
892
  **A TASK'S FIELD ANNOTATES EXACTLY LIKE A RECORD'S FIELD.** `description` (persistent
861
893
  guidance), `warning` (a consequence to weigh — amber, announced) and `error` (field-level
@@ -1096,8 +1128,8 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
1096
1128
  `FileGrid`; the host wires `files` + `onAdd`/`onRemove` (+ optional `uploads`,
1097
1129
  `selectTileRemove`, `labels`, `galleryLabels`, `gridMaxHeight` — cap the grid height so it
1098
1130
  scrolls and the bar pins, for a popover/drawer). **What you can DO to the files is composed
1099
- below it**, from `FilesEditorBar` + `FilesEditorUpload` · `FilesEditorSelect` ·
1100
- `FilesEditorSelectAll` · `FilesEditorDownload` · `FilesEditorRemove` (+
1131
+ below it**, from `FilesEditorBar` + `FilesEditorUpload`, `FilesEditorSelect`,
1132
+ `FilesEditorSelectAll`, `FilesEditorDownload`, `FilesEditorRemove` (+
1101
1133
  `FilesEditorBarSpacer` to push the rest right). Each renders nothing without the handler it
1102
1134
  needs, so withholding `onAdd`/`onRemove` IS the read-only shape — there is no `readOnly`
1103
1135
  mode. **A HOST verb is a plain `Button`** reading **`useFilesEditorSelection()`**
@@ -1124,8 +1156,8 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
1124
1156
  (`FileGrid` resolves them from `LoticsLocale.fileUpload` on your behalf).
1125
1157
  - **`file_thumbnail`** — `FileThumbnail` + `DisplayFile` + `THUMBNAIL_SIZE` /
1126
1158
  `COMPACT_THUMBNAIL_SIZE` + `getMediaIcon`: the completed tile — the right surface per
1127
- MIME: image thumbnail · a doc tile with the `FileBadge` centered + a single-line filename
1128
- · media card; `isTemplate` overlays a TMPL marker. The tile's accessible name is the
1159
+ MIME: image thumbnail, a doc tile with the `FileBadge` centered + a single-line filename
1160
+ , media card; `isTemplate` overlays a TMPL marker. The tile's accessible name is the
1129
1161
  filename; pass `accessibilityLabel` to say what pressing it DOES instead. The per-surface
1130
1162
  pieces are exported for a hand-rolled layout: `DocumentBadge` (a bare pressable badge —
1131
1163
  its `size` is the square SLOT side, like every other tile here, and the badge is fitted to
@@ -1146,15 +1178,15 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
1146
1178
  the mark is TALLER than it is wide (26 × 32 by default), so a square slot takes the
1147
1179
  fitted width, not the slot side (that conversion is what `DocumentBadge` does).
1148
1180
  - **`file_preview`** — `FilePreview`: the universal inline preview — image/PDF/video/audio +
1149
- Word via `@lotics/docx` + Excel/CSV via `@lotics/xlsx`; the heavy engines (pdf.js ·
1150
- `@lotics/docx` · `@lotics/xlsx`) are LAZY (dynamic-imported, ~free until a doc of that
1181
+ Word via `@lotics/docx` + Excel/CSV via `@lotics/xlsx`; the heavy engines (pdf.js,
1182
+ `@lotics/docx`, `@lotics/xlsx`) are LAZY (dynamic-imported, ~free until a doc of that
1151
1183
  type is opened) and SHIP AS `@lotics/ui` deps — custom-code apps get PDF/Word/Excel
1152
1184
  preview with ZERO extra install. Renders to canvas/DOM, never a nested iframe — works in
1153
1185
  the sandboxed app iframe.
1154
1186
  - **`file_preview_types`** — `PreviewLabels` / `FilePreviewProps` / `GalleryLabels` — the
1155
1187
  shared label + prop contracts of the file-preview family; types only.
1156
1188
  - **`file_gallery_modal`** — `FileGalleryModal`: the FULL-SCREEN viewer — toolbar (filename
1157
- · counter · download, optional `onOpenExternal`/`onRemove` · close-✕), prev/next, ESC,
1189
+ , counter, download, optional `onOpenExternal`/`onRemove`, close-✕), prev/next, ESC,
1158
1190
  rotate (the 90° controls FLOAT as a pill on the image); on a phone the actions collapse
1159
1191
  into a ⋯ `ActionMenu`. `onPersistRotation`/`persisting` wire a rotation save. Driven by
1160
1192
  `files` + `activeIndex` + `onIndexChange`.
@@ -1236,16 +1268,16 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
1236
1268
  be rendered; where a compact meter used to decorate a ranked row or proposal card, rank /
1237
1269
  badges / severity carry the standing instead. Also: calibrated
1238
1270
  high/med/low; localized via the provider.
1239
- - **`change_review`** — the COMPOUND review family — frame: `ChangeReview` provider/stack ·
1240
- `ChangeReviewHeader` (auto kept-counter over decidable entries) · `ChangeReviewActions`
1271
+ - **`change_review`** — the COMPOUND review family — frame: `ChangeReview` provider/stack,
1272
+ `ChangeReviewHeader` (auto kept-counter over decidable entries), `ChangeReviewActions`
1241
1273
  (the commit bar in the DialogFooter/DrawerFooter: Keep-all bottom-left (`onAcceptAll` for
1242
1274
  host-held field state) + Apply gating); sections: `Change` (host-owned status, labeled
1243
- verbs, collapses to its `ChangeSummary` + Undo; no callbacks = display-only) ·
1244
- `ChangeLabel` · `ChangeSummary` · `ChangeReasoning` (the quiet why); grammar:
1245
- `ChangeFields` (the open record form) + `ChangeField` (THE field: − band · value ·
1246
- candidates + type-another-value · reasoning · per-field Keep/Drop · collapse) ·
1275
+ verbs, collapses to its `ChangeSummary` + Undo; no callbacks = display-only),
1276
+ `ChangeLabel`, `ChangeSummary`, `ChangeReasoning` (the quiet why); grammar:
1277
+ `ChangeFields` (the open record form) + `ChangeField` (THE field: − band, value,
1278
+ candidates + type-another-value, reasoning, per-field Keep/Drop, collapse),
1247
1279
  `ChangeRecord` (THE item card: registers like a Change; tone wash + localized op word;
1248
- verb level follows the decision level) · `ChangeBand` (the raw ± band) ·
1280
+ verb level follows the decision level), `ChangeBand` (the raw ± band),
1249
1281
  `ChangeValueInput` (the diff-at-rest editor); the `changeReview` locale slice.
1250
1282
  - **`clarify`** — `Clarify` + `ClarifyOption`: the agent asks back — a borderless block
1251
1283
  (the question text + a `ChoiceList`, no card wrapper; an optional muted `eyebrow` sits tight above
@@ -1280,17 +1312,17 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
1280
1312
  - **`sources`** — `Sources` + `SourceRef`/`SourceKind` (record | document | table | web |
1281
1313
  knowledge): provenance chips, per-kind glyphs.
1282
1314
  - **`finding`** — `Finding` + `FindingComparison` + `FindingSeverity`/`FindingLabels`: one
1283
- ranked AI-check insight — severity word · title · detail · `Sources` · children slot; the
1315
+ ranked AI-check insight — severity word, title, detail, `Sources`, children slot; the
1284
1316
  expected-vs-actual body with the emphasized delta; the `finding` locale slice.
1285
1317
  - **`result_header`** — `ResultHeader` (+ `ResultTone`): the save-direct RECEIPT's outcome
1286
1318
  strip, on the page grid — tone mark (`ok`/`attention`/`error`/`skipped`) inline on the
1287
- title row · outcome-first title · right-slot `action` (the open-record jump / a retry) ·
1319
+ title row, outcome-first title, right-slot `action` (the open-record jump / a retry),
1288
1320
  ONE line under (the honest `Confidence`, or the failure reason). The receipt composes:
1289
1321
  header → the receipt lines (a `DetailTable` of spread `DetailRow`s — label left, value at
1290
1322
  the right edge) → a `Confidence` callout naming the exact values that failed their
1291
1323
  deterministic checks (or stating what passed). NOTHING in a
1292
1324
  receipt edits — the RECORD is the edit surface, one press away. SEVERAL records → an
1293
- attention-first register of `ListItem` rows (tone mark · title · figures · needs-checking
1325
+ attention-first register of `ListItem` rows (tone mark, title, figures, needs-checking
1294
1326
  count) pressing straight through to the records. See ai_patterns §the one law.
1295
1327
  - **`comments_thread`** — `CommentList` + the
1296
1328
  `ThreadComment`/`ThreadMember`/`ThreadFile` types: the record comments thread.