@lotics/ui 22.2.0 → 23.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 (68) hide show
  1. package/AGENTS.md +9 -2
  2. package/MIGRATION.md +142 -2
  3. package/docs/ai_patterns.md +27 -25
  4. package/docs/catalog.md +119 -98
  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 +3 -2
  26. package/src/agent_run.tsx +40 -25
  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_rows.tsx +1 -1
  38. package/src/finding.tsx +4 -4
  39. package/src/form_field.tsx +1 -1
  40. package/src/format_date.ts +2 -2
  41. package/src/heatmap.tsx +1 -1
  42. package/src/inline_button.tsx +84 -0
  43. package/src/inline_date_picker.tsx +17 -10
  44. package/src/inline_edit.tsx +298 -59
  45. package/src/inline_member_select.tsx +8 -3
  46. package/src/inline_number_input.tsx +11 -4
  47. package/src/inline_select.tsx +26 -13
  48. package/src/inline_text_input.tsx +12 -4
  49. package/src/inline_time_picker.tsx +10 -4
  50. package/src/ledger.tsx +2 -2
  51. package/src/locale.tsx +7 -3
  52. package/src/matrix.tsx +1 -1
  53. package/src/number_input.tsx +18 -7
  54. package/src/pipeline.tsx +1 -1
  55. package/src/popover.tsx +1 -1
  56. package/src/press_door.tsx +1 -1
  57. package/src/pressable_highlight.tsx +1 -1
  58. package/src/progress_bar.tsx +3 -3
  59. package/src/record_summary.tsx +2 -2
  60. package/src/result_header.tsx +2 -2
  61. package/src/sequence.tsx +170 -0
  62. package/src/share_or_download.ts +2 -2
  63. package/src/step_progress.tsx +7 -5
  64. package/src/stepper.tsx +1 -1
  65. package/src/task.tsx +6 -6
  66. package/src/text_input_field.tsx +19 -1
  67. package/src/text_utils.ts +1 -1
  68. 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,8 @@ 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`, `InlineNumberInput`, `InlineSelect` (single or
99
+ `multi`), `InlineMemberSelect`, `InlineDatePicker`, `InlineTimePicker`; a
100
100
  READ-ONLY field in that same column uses `InlineStatic` (matches the editor box exactly, no
101
101
  input chrome, so it aligns pixel-for-pixel). A stack of labelled field rows lives in
102
102
  `DetailTable` + `DetailRow`; the record's identity band is `RecordSummary`; its money
@@ -151,13 +151,13 @@ Two columnar shapes, and the choice is about data size:
151
151
 
152
152
  ### Numbers & charts
153
153
 
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),
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),
156
156
  `TrendChip` (delta), `Sparkline`, `BarChart` / `LineChart` / `PieChart` (the canonical SVG
157
157
  set — no recharts), `RingGauge`, `ProgressBar` (its `compact` prop = ONE row, track + a
158
158
  plain sm tabular count beside it — the cell/heading/peek-trigger meter; a caption floating
159
159
  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
160
+ its max it reads `2,100 / 2,000 (105%)`, because a meter that says "100%" when you are over
161
161
  tells the reader they are exactly at the limit. Numbers format in the reader's locale, so
162
162
  never hand-format the value you pass in — when display precision differs from the true value
163
163
  (whole credits off a fractional balance), `formatValue` reshapes the caption text and leaves
@@ -206,8 +206,8 @@ controls is `PressableRow` + **`PressDoor`** — never `PressableHighlight`, whi
206
206
  button and wraps its children (a button must not contain interactive descendants). The
207
207
  door is an empty absolutely-positioned SIBLING of the content carrying the tab stop,
208
208
  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.
209
+ `TableRow` does exactly this internally, so a columnar register needs no assembly —
210
+ reach for `PressDoor` only outside it.
211
211
 
212
212
  ### Filters & view controls
213
213
 
@@ -255,7 +255,7 @@ patterns doc indexed in [AGENTS.md](../AGENTS.md)).
255
255
 
256
256
  ### Specialized work surfaces
257
257
 
258
- `ScanField` (scan/verify), `Stepper` (a guided run / progress sequence — done · current ·
258
+ `ScanField` (scan/verify), `Stepper` (a guided run / progress sequence — done, current,
259
259
  upcoming, horizontal OR vertical), `RemainderMeter` + `AllocationRow` (allocation),
260
260
  `Timeline` (a heterogeneous event LOG — icons + expandable details, not progress),
261
261
  `Calendar` (the `calendar` module's views), `Gantt`, `comments_thread`.
@@ -266,15 +266,15 @@ upcoming, horizontal OR vertical), `RemainderMeter` + `AllocationRow` (allocatio
266
266
  long text + attachments; the surface that triggers agent work), `AgentRun` (the live
267
267
  streaming work feed) + `AgentProgress` (its compact, floating, expandable form — a
268
268
  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` ·
269
+ surface, a COMPOUND family** (frame: `ChangeReview`, `ChangeReviewHeader`,
270
+ `ChangeReviewActions`; sections: `Change`, `ChangeLabel`, `ChangeSummary`,
271
+ `ChangeReasoning`; the grammar: `ChangeFields` + `ChangeField`, `ChangeRecord`,
272
272
  `ChangeBand` + `ChangeValueInput`): adds, updates, removals, conflicts, whole records,
273
273
  display-only findings are all compositions — see the AI-patterns doc indexed in
274
274
  [AGENTS.md](../AGENTS.md) for the laws; `Clarify` (the agent asks back — selectable
275
275
  `ChoiceList` options), `Sources` (provenance chips for AI output — at review scale,
276
276
  `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;
277
+ AI check — localized severity word, title, detail, `Sources` chips, a `children` slot;
278
278
  **`FindingComparison`** is the expected-vs-actual body: each disagreeing side a labeled row,
279
279
  the DELTA emphasized under a hairline (localized "Difference") — quantities, totals, dates;
280
280
  a plain `metric` prop remains for one-number findings. The children slot composes ANY visual
@@ -336,18 +336,18 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
336
336
  weekday/month names, and `formatDate` display all follow the provider with no per-instance
337
337
  `locale` prop — an explicit `locale` still overrides. **Limitation:** the calendar/gantt
338
338
  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`,
339
+ - **`colors`** — the palette + `withAlpha`, `solid`, `tint`, `ramp`, `ColorName`,
340
+ `isColorName`, `asColorName` (coerce a stored option/status token to a `ColorName`,
341
341
  neutral fallback).
342
342
  - **`tokens`** — design tokens: re-exports `colors` plus `space`/`type`/`weight`/`radius`
343
343
  scales and `getCssVariables()` — an OPT-IN serializer to `--lotics-*` CSS variables for
344
344
  hand-rolled plain DOM/CSS (nothing injects them automatically; `@lotics/ui` components
345
345
  don't need them).
346
346
  - **`spacing`** — the `SPACE` scale + `SpaceToken`.
347
- - **`control_surface`** — `CONTROL_HEIGHT` (40) · `CONTROL_RADIUS` (10) · `CONTROL_TEXT_INSET`
347
+ - **`control_surface`** — `CONTROL_HEIGHT` (40), `CONTROL_RADIUS` (10), `CONTROL_TEXT_INSET`
348
348
  (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
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
351
351
  control-surface tokens.
352
352
  - **`fonts.css`** — the Inter sheet (400/500/600, served by absolute URL so it resolves on
353
353
  every origin an app runs from); the app entry imports it ONCE or every `Text` falls back
@@ -380,15 +380,15 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
380
380
 
381
381
  ### Layout & surfaces
382
382
 
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
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
387
387
  editor, or a nested block. **NOT a `Callout`:** a Callout is a status BAND (and
388
388
  `warning`/`error` render as an ARIA `alert`) — wrapping form fields in an alert is wrong.
389
389
  The law: **content + fields → `Inset`; a message → `Callout`.**
390
- - **`section_heading`** — `Section` · `SectionHeading` · `SectionHeadingTitle` ·
391
- `SectionHeadingMeta` · `Subsection` · `SubsectionHeading` · `SubsectionHeadingTitle` ·
390
+ - **`section_heading`** — `Section`, `SectionHeading`, `SectionHeadingTitle`,
391
+ `SectionHeadingMeta`, `Subsection`, `SubsectionHeading`, `SubsectionHeadingTitle`,
392
392
  `DialogSectionHeadingTitle` — the
393
393
  card-less twin of the Card family, compound, owns no margin; spacing via the Section gap
394
394
  (16, fixed), no body component. `SectionHeadingTitle` is ALWAYS `##` (xl semibold;
@@ -399,7 +399,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
399
399
  md-semibold rung with the SAME `icon`/`description`/`info` slots as the section title, so a
400
400
  dialog surface loses only the type size, never an affordance. The heading ramp is FIXED:
401
401
  `#` xxl / `##` xl / `###` lg / `####` md, no size props.
402
- - **`section_stack`** — `SectionStack` · `SubsectionStack` — stacks that own the
402
+ - **`section_stack`** — `SectionStack`, `SubsectionStack` — stacks that own the
403
403
  between-block law, skipping null children: `SectionStack` = a fixed 56px beat + a hairline
404
404
  `Divider` between top-level blocks; `SubsectionStack` = a fixed 32px beat, space-only while
405
405
  the groups are SHORT (the titles carry the grouping, hairlines stay at the section level) and
@@ -467,7 +467,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
467
467
  the `chip.remove` locale slice). Suggestion pills → `SuggestionChip`.
468
468
  - **`action_menu`** — `ActionMenu`: the ⋯ overflow menu (`ActionMenuItem[]`; danger items
469
469
  last).
470
- - **`menu_button`** — `MenuButton`: the menu/rail row (icon · title · `right` slot;
470
+ - **`menu_button`** — `MenuButton`: the menu/rail row (icon, title, `right` slot;
471
471
  `focused`/`danger`; `role` menuitem|button|option) — popover menus, outline rails, section
472
472
  pickers. Its resting highlight has TWO meanings and they are not interchangeable:
473
473
  **`selected`** is listbox SELECTION (emitted as `aria-selected`, and only under
@@ -490,27 +490,8 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
490
490
  own controls. Pair it with `PressableRow` (surface takes the mouse + the wash, door takes
491
491
  the keyboard, nested controls keep their own presses); the parent owns the positioning
492
492
  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.
493
+ matches the surface it spans. `TableRow` is the in-kit consumer reach for it when
494
+ designing a pressable surface it doesn't cover.
514
495
  - **`pressable_highlight`** — `PressableHighlight`: the hover-wash + keyboard-focus-ring
515
496
  `Pressable` under `MenuButton`/`Switcher`/custom pressable surfaces; its style-fn/children
516
497
  receive `hovered` + `focusVisible`.
@@ -526,7 +507,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
526
507
  shortcut from a raw string or `ShortcutDescriptor` (⌘B on Mac, Ctrl+B elsewhere); null on
527
508
  small screens. `TextInputField shortcut` renders it built-in, while the field is EMPTY —
528
509
  it hints at reaching the field, so a value replaces it (with the clear ✕, when `clearable`).
529
- - **`keyboard`** — `isMac` · `ShortcutDescriptor` · `formatShortcut` (platform-aware
510
+ - **`keyboard`** — `isMac`, `ShortcutDescriptor`, `formatShortcut` (platform-aware
530
511
  shortcut formatting behind `ShortcutBadge`).
531
512
 
532
513
  ### Badges, status & feedback
@@ -538,7 +519,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
538
519
  (see composition.md "Badge is for status only"). A bare `<Badge>` is `dot`.
539
520
  - **`status_badge`** — `StatusBadge`: an enabled/disabled pulse badge (`enabled` + `label`).
540
521
  - **`option_badge`** — `OptionBadge`: a select value as its configured colored badge.
541
- - **`callout`** — `Callout` · `CalloutTitle` · `CalloutText` · `CalloutActions` (`tone`
522
+ - **`callout`** — `Callout`, `CalloutTitle`, `CalloutText`, `CalloutActions` (`tone`
542
523
  info|success|warning|error|neutral): inline status band — a MESSAGE (`warning`/`error`
543
524
  are ARIA `alert`s). For a form / nested content surface use **`Inset`**, never a Callout.
544
525
  - **`empty_state`** — `EmptyState`: centered placeholder for an empty list/filter result —
@@ -664,17 +645,30 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
664
645
  INSIDE the control and inline error, commit on blur (Enter saves, Escape reverts) or
665
646
  `controls="buttons"`; KEYBOARD focus on the closed view opens edit mode with the input
666
647
  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
648
+ does; **`variant: "framed" | "bare"`** sets how much of the field's frame shows AT REST
649
+ (see below). Every commit registers in **`pending_commits`**, which
669
650
  `Button`/`IconButton` wait on via **`use_gated_press`** so an action pressed in the same
670
651
  gesture as the blur cannot read the record before the edit lands (data_entry.md § Inline
671
652
  edit) — automatic, nothing to pass.
653
+ **`actions` puts VERBS on the field's surface** (an `InlineButton` — Copy, Open, Today), and
654
+ EVERY `Inline*` editor takes it. `trailing` is decoration only (a chevron, a spinner): it
655
+ renders INSIDE the press target, so a button there would be a button in a button.
656
+ With `actions` the field renders through ONE shell that owns the surface in BOTH modes — so
657
+ the verbs never move when the editor swaps to its input, the control inside goes `seamless`
658
+ (drawing no second box), hover is tracked on the box (react-native-web hands a parent's hover
659
+ to the innermost pressable, so a control-tracked border drops out as the pointer crosses a
660
+ verb), and the focus ring paints on the whole FIELD via focus-within, not on the value region.
661
+ A popover-backed editor also anchors its overlay to the shell (`anchorRef`) rather than to the
662
+ control — with verbs the control is narrower than the field, and an inherited width would open
663
+ a list too small for its own row.
664
+ Pass `actions` UNCONDITIONALLY and `disabled` the verb when it has nothing to act on: a slot
665
+ that appears once the value is non-empty resizes the field as the user types.
672
666
  - **`inline_text_input`**, **`inline_number_input`** (`format` for currency/units),
673
667
  **`inline_select`**, **`inline_member_select`**, **`inline_date_picker`**
674
668
  (`format="datetime"`, `optionalTime`; keyboard focus opens the TYPED segmented `DateField`
675
669
  — locale field order, separator advances, Alt+ArrowDown floats the calendar; click still
676
670
  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);
671
+ the same editor everywhere, `variant="bare"` on a dense grid — see the surface note below);
678
672
  `InlineSelect`/`InlineMemberSelect` render the resting value like its option —
679
673
  `renderOptionContent` by default, `renderSelected` to override — a chip/badge at rest, not
680
674
  just text. **`InlineSelect` is single OR multi** — pass `multi` for a tag SET (`value: T[]`,
@@ -687,38 +681,64 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
687
681
  (`TextColor`) that colours the resting date by
688
682
  semantic state — overdue red, soon-due amber (the caller owns the rule, e.g. a task's `dueTone`);
689
683
  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.
684
+ **ONE surface, ONE hover language; `variant` sets its resting WEIGHT.** An editor wears THE pill
685
+ surface white, 1px border, the same one `Chip`, `ChipGroup` and a secondary `Button` wear — so
686
+ every control a row can hold reads as one family. Open adds the 2px ring; disabled rests flat and
687
+ borderless, promising no press. HOVER differs by variant, because hover must be a visible CHANGE
688
+ rather than a deeper line: `framed` deepens its border AND tints (zinc-200 → zinc-400 alone is a
689
+ shade shift on 1px), `bare` has the border ARRIVE (loud by itself — and a tint there would make a
690
+ hovered cell lighter than the row washing beneath it).
691
+ - **`variant="framed"`** (default) the frame is already showing. Required wherever editable and
692
+ static values MIX (a record's `DetailTable`): there it is the only thing saying what you can change.
693
+ - **`variant="bare"`** — nothing at rest, the SAME border on hover. For a surface where EVERY value
694
+ edits (a register/`DataGrid` column, a task row), where a per-field frame states what the whole
695
+ surface already promises and, down a column, draws the grid twice.
696
+
697
+ It is an axis of weight, not of use: both hover and open identically. There is no `*Cell` family
698
+ a data-grid cell is `<InlineSelect variant="bare" …/>`, not a `SelectCell` (naming a component for a
699
+ USE, and duplicating the picker stack to flip two style properties, are both things the kit forbids).
700
+ Hand-roll a pressable cell to match `bare`, never a background wash.
705
701
  - **`inline_static`** — `InlineStatic`: a READ-ONLY value matching the Inline\* box metrics
706
702
  EXACTLY (height, padding, 1px transparent border) so a non-editable field — a computed
707
703
  total, a system ID, a synced/locked value — aligns pixel-for-pixel in the same column;
708
704
  non-interactive, NOT a disabled input; `muted`/`tabular`/`align="right"` for a number
709
705
  column, `weight="medium"` to emphasise a total among plain rows.
706
+ - **`sequence`** — `Sequence` + `SequenceItem` (+ `SEQUENCE_INSET`): an ORDERED list whose ORDER
707
+ IS THE DATA — a route's stops, an approval chain, a set of legs — drawn as a connected rail so
708
+ the sequence reads without a label saying "first"/"then". Reach for it when a thing has a
709
+ VARIABLE number of positions: one field per position (`Origin` / `Transfer point` /
710
+ `Destination`) cannot hold a second middle entry and cannot say the entries are ordered.
711
+ `SequenceItem` takes the content, an optional `role` (DERIVE it from index — a stored role lies
712
+ the moment the list is reordered) and `onMoveUp`/`onMoveDown`/`onRemove`; the controls render
713
+ even where they cannot act so the right edge never shifts between items. Reorder is BUTTONS,
714
+ not a drag handle — dragging is invisible to the keyboard and fiddly on a phone, and a
715
+ three-to-six position list does not need it. Indent anything that follows the list (an "Add"
716
+ link) by `SEQUENCE_INSET` so it lands on the rail's column. Distinct from `Timeline` (an
717
+ activity FEED), `Stepper` (a wizard's fixed positions) and `Pipeline` (ONE record walking
718
+ stages that own their controls) — this is the list a user EDITS.
719
+ - **`inline_button`** — `InlineButton`: a verb that sits INSIDE an inline field's surface
720
+ (the `Open` on a reference, a `Copy` on a value worth copying) rather than in the row's
721
+ trailing column — put it inside when the act is ABOUT THE VALUE, so it travels with what it
722
+ acts on and the trailing column stays free for row-level verbs. FILLED (`Button secondary`'s own
723
+ zinc-100) on the field's white surface — the contrast is a RELATIONSHIP, not a colour: invert it
724
+ if the resting field surface ever changes, or the verb dissolves into the value. 28px, radius 8,
725
+ 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
726
+ surface underneath is itself pressable, so without it one press fires both, silently. Always
727
+ a WORD, never icon-only.
710
728
  - **`detail_row`** — `DetailTable` + `DetailRow` — the record field grid. `DetailRow`:
711
729
  label+value row for drawer/peek detail; in FORM mode (`labelWidth` set) the value column
712
730
  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
731
+ edit. There is NO trailing slot: a verb about the VALUE goes on the field (`InlineButton` in
732
+ the editor's `actions`), and anything else that belongs beside the value — a status `Badge`, a
733
+ unit (though "kg"/"$" belong IN the value via `InlineNumberInput format`) — composes into a
734
+ row inside the value cell, costing only the row that wants it. The LABEL WRAPS inside its column and
715
735
  is never clipped — a fixed `labelWidth` would otherwise ellipsize every long field name
716
736
  ("Registered business address"), and a name the reader can't finish is worse than a taller
717
737
  row; a wrapped label's FIRST line stays level with the value's first control line while a
718
738
  one-line label still centers on it (both modes, no prop). The FIELD-ANNOTATION vocabulary (same
719
739
  names + meanings as `FormField`), always EXPLICIT — a row never hides guidance behind an ⓘ:
720
740
  **`description`** = a fact / persistent guidance (muted), under the VALUE (stacked mode
721
- mirrors the form order label · description · control); **`warning`** = a consequence to weigh
741
+ mirrors the form order label, description, control); **`warning`** = a consequence to weigh
722
742
  before acting (amber, announced); **`error`** = field-level failure under the value, danger +
723
743
  alert semantics (the `Inline*` editors already render their own transient save errors — don't
724
744
  wire both); invalid/cross-field STATE = a co-located `Callout`; **`flat`**
@@ -729,19 +749,20 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
729
749
  on two surfaces.
730
750
  `DetailTable`: the compound parent of
731
751
  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
752
+ a `TaskList`'s default too) / `minHeight` (default 40, the inline-control grid) declared
753
+ ONCE + the `SPACE.md` (16) row gap 8 was right while a field was a tint, but a stack of
754
+ BORDERED rows that close together fuses into one block. TWO columns, no third: the value
755
+ FILLS what the label leaves, so nothing one row does can narrow its neighbours' editors. RESPONSIVE with no prop: it
756
+ measures its own container (onLayout, not
736
757
  the viewport — works inside a Drawer; the unmeasured first frame renders opacity-0 so the
737
758
  first PAINT is already in the right mode) and when the columns would crush the value cell
738
759
  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
760
+ grammar); raise `minValueWidth` (default 160) when a cell holds
740
761
  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:
762
+ repeating the same labelWidth. Worked example:
742
763
  [`tpl_record`](../examples/tpl_record.tsx).
743
764
  - **`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
765
+ row: `title` xxl semibold tabular, `subtitle` sm muted, `status` Badge slot, optional
745
766
  `metric` {label,value,tone,note} pinned right, the band's ONE accent. The record's FIELDS
746
767
  never live in the header: compose them as `DetailTable`s in the sections below. Replaces
747
768
  hand-rolled record headers (mixed scales, several competing figures, color noise).
@@ -819,7 +840,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
819
840
 
820
841
  `TaskStatus` takes the `CheckCircle` (omit `onChange` for a read-only ring; a PICKER list
821
842
  puts a `CheckboxInput` here and sets `controlWidth={24}`). `TaskTitle` takes the
822
- `variant="cell"` `InlineTextInput` — or the title TEXT itself as a string child
843
+ `InlineTextInput variant="bare"` — or the title TEXT itself as a string child
823
844
  (`<TaskTitle struck={done}>{label}</TaskTitle>`), which is the READ-ONLY form: the compound
824
845
  applies the cell inset and the row's band, so a plain `<Text>` + a hand-rolled
825
846
  `TASK_TEXT_INSET` is never needed and a long title that wraps keeps its first line beside the
@@ -831,7 +852,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
831
852
  gutter on the first line (Delete lives BEHIND it, danger-styled and last, never a bare ✕).
832
853
  `TaskDetail` is a FREE-FORM block under the row on the title's text edge — a chart, a table, a
833
854
  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
855
+ **`TASK_TEXT_INSET`** (9), the inset an inline control puts on its own text: every
835
856
  slot above already applies it, so reach for it ONLY when a custom title NODE (not a cell
836
857
  control) has to land on the same text edge.
837
858
 
@@ -844,7 +865,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
844
865
  step inward per level. Declare `actionWidth={0}` when NO row in the list carries actions.
845
866
 
846
867
  **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` /
868
+ own fields hang under it as **`TaskSubRow`** (`label`, the control, `description` /
848
869
  `warning` / `error`), indented ONE step — the same step a nested `TaskList` takes, because
849
870
  belonging is expressed by indentation and there is only one device for it. Label and value sit
850
871
  ADJACENT so the eye pairs them. **The label column is the LIST's, not the row's** — one
@@ -855,7 +876,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
855
876
  vocabulary with longer field names staggered nearly every row). The value takes the slack from
856
877
  a readable minimum — on a phone or in a narrow drawer it drops onto its own line under the
857
878
  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.
879
+ `variant="bare"`: a dense grid drops the resting frame.
859
880
 
860
881
  **A TASK'S FIELD ANNOTATES EXACTLY LIKE A RECORD'S FIELD.** `description` (persistent
861
882
  guidance), `warning` (a consequence to weigh — amber, announced) and `error` (field-level
@@ -1096,8 +1117,8 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
1096
1117
  `FileGrid`; the host wires `files` + `onAdd`/`onRemove` (+ optional `uploads`,
1097
1118
  `selectTileRemove`, `labels`, `galleryLabels`, `gridMaxHeight` — cap the grid height so it
1098
1119
  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` (+
1120
+ below it**, from `FilesEditorBar` + `FilesEditorUpload`, `FilesEditorSelect`,
1121
+ `FilesEditorSelectAll`, `FilesEditorDownload`, `FilesEditorRemove` (+
1101
1122
  `FilesEditorBarSpacer` to push the rest right). Each renders nothing without the handler it
1102
1123
  needs, so withholding `onAdd`/`onRemove` IS the read-only shape — there is no `readOnly`
1103
1124
  mode. **A HOST verb is a plain `Button`** reading **`useFilesEditorSelection()`**
@@ -1124,8 +1145,8 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
1124
1145
  (`FileGrid` resolves them from `LoticsLocale.fileUpload` on your behalf).
1125
1146
  - **`file_thumbnail`** — `FileThumbnail` + `DisplayFile` + `THUMBNAIL_SIZE` /
1126
1147
  `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
1148
+ MIME: image thumbnail, a doc tile with the `FileBadge` centered + a single-line filename
1149
+ , media card; `isTemplate` overlays a TMPL marker. The tile's accessible name is the
1129
1150
  filename; pass `accessibilityLabel` to say what pressing it DOES instead. The per-surface
1130
1151
  pieces are exported for a hand-rolled layout: `DocumentBadge` (a bare pressable badge —
1131
1152
  its `size` is the square SLOT side, like every other tile here, and the badge is fitted to
@@ -1146,15 +1167,15 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
1146
1167
  the mark is TALLER than it is wide (26 × 32 by default), so a square slot takes the
1147
1168
  fitted width, not the slot side (that conversion is what `DocumentBadge` does).
1148
1169
  - **`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
1170
+ Word via `@lotics/docx` + Excel/CSV via `@lotics/xlsx`; the heavy engines (pdf.js,
1171
+ `@lotics/docx`, `@lotics/xlsx`) are LAZY (dynamic-imported, ~free until a doc of that
1151
1172
  type is opened) and SHIP AS `@lotics/ui` deps — custom-code apps get PDF/Word/Excel
1152
1173
  preview with ZERO extra install. Renders to canvas/DOM, never a nested iframe — works in
1153
1174
  the sandboxed app iframe.
1154
1175
  - **`file_preview_types`** — `PreviewLabels` / `FilePreviewProps` / `GalleryLabels` — the
1155
1176
  shared label + prop contracts of the file-preview family; types only.
1156
1177
  - **`file_gallery_modal`** — `FileGalleryModal`: the FULL-SCREEN viewer — toolbar (filename
1157
- · counter · download, optional `onOpenExternal`/`onRemove` · close-✕), prev/next, ESC,
1178
+ , counter, download, optional `onOpenExternal`/`onRemove`, close-✕), prev/next, ESC,
1158
1179
  rotate (the 90° controls FLOAT as a pill on the image); on a phone the actions collapse
1159
1180
  into a ⋯ `ActionMenu`. `onPersistRotation`/`persisting` wire a rotation save. Driven by
1160
1181
  `files` + `activeIndex` + `onIndexChange`.
@@ -1236,16 +1257,16 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
1236
1257
  be rendered; where a compact meter used to decorate a ranked row or proposal card, rank /
1237
1258
  badges / severity carry the standing instead. Also: calibrated
1238
1259
  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`
1260
+ - **`change_review`** — the COMPOUND review family — frame: `ChangeReview` provider/stack,
1261
+ `ChangeReviewHeader` (auto kept-counter over decidable entries), `ChangeReviewActions`
1241
1262
  (the commit bar in the DialogFooter/DrawerFooter: Keep-all bottom-left (`onAcceptAll` for
1242
1263
  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) ·
1264
+ verbs, collapses to its `ChangeSummary` + Undo; no callbacks = display-only),
1265
+ `ChangeLabel`, `ChangeSummary`, `ChangeReasoning` (the quiet why); grammar:
1266
+ `ChangeFields` (the open record form) + `ChangeField` (THE field: − band, value,
1267
+ candidates + type-another-value, reasoning, per-field Keep/Drop, collapse),
1247
1268
  `ChangeRecord` (THE item card: registers like a Change; tone wash + localized op word;
1248
- verb level follows the decision level) · `ChangeBand` (the raw ± band) ·
1269
+ verb level follows the decision level), `ChangeBand` (the raw ± band),
1249
1270
  `ChangeValueInput` (the diff-at-rest editor); the `changeReview` locale slice.
1250
1271
  - **`clarify`** — `Clarify` + `ClarifyOption`: the agent asks back — a borderless block
1251
1272
  (the question text + a `ChoiceList`, no card wrapper; an optional muted `eyebrow` sits tight above
@@ -1280,17 +1301,17 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
1280
1301
  - **`sources`** — `Sources` + `SourceRef`/`SourceKind` (record | document | table | web |
1281
1302
  knowledge): provenance chips, per-kind glyphs.
1282
1303
  - **`finding`** — `Finding` + `FindingComparison` + `FindingSeverity`/`FindingLabels`: one
1283
- ranked AI-check insight — severity word · title · detail · `Sources` · children slot; the
1304
+ ranked AI-check insight — severity word, title, detail, `Sources`, children slot; the
1284
1305
  expected-vs-actual body with the emphasized delta; the `finding` locale slice.
1285
1306
  - **`result_header`** — `ResultHeader` (+ `ResultTone`): the save-direct RECEIPT's outcome
1286
1307
  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) ·
1308
+ title row, outcome-first title, right-slot `action` (the open-record jump / a retry),
1288
1309
  ONE line under (the honest `Confidence`, or the failure reason). The receipt composes:
1289
1310
  header → the receipt lines (a `DetailTable` of spread `DetailRow`s — label left, value at
1290
1311
  the right edge) → a `Confidence` callout naming the exact values that failed their
1291
1312
  deterministic checks (or stating what passed). NOTHING in a
1292
1313
  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
1314
+ attention-first register of `ListItem` rows (tone mark, title, figures, needs-checking
1294
1315
  count) pressing straight through to the records. See ai_patterns §the one law.
1295
1316
  - **`comments_thread`** — `CommentList` + the
1296
1317
  `ThreadComment`/`ThreadMember`/`ThreadFile` types: the record comments thread.