@lotics/ui 23.1.1 → 24.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.
package/AGENTS.md CHANGED
@@ -17,7 +17,7 @@ CURRENT major only — upgrading an app across majors is `MIGRATION.md`.
17
17
  | [docs/catalog.md](./docs/catalog.md) | **The complete inventory** — Reach-by-role (each data role → the ONE canonical component) + every `@lotics/ui/<module>` entry point (incl. `@lotics/ui/vite`'s `loticsOptimizeDeps` for a custom-code app's `vite.config.ts`). Read before building any screen; reuse first. |
18
18
  | [docs/data_entry.md](./docs/data_entry.md) | Which editing pattern for which job — inline edit, fieldset forms, browser-autofill suppression (search controls only), find-or-create (`Combobox`), line items, handoffs, phased records, billing, tags, dispositions, attachments (the `FilesEditor` COMPOUND — root owns selection/gallery/confirm, you compose the bar, a HOST verb reads `useFilesEditorSelection` — plus the three-way file INTAKE: CTA + `FileDropTarget` + `usePasteFiles`), stage gates, the commit-on-blur vs action-press ordering law (the kit gates the press — `pending_commits`). |
19
19
  | [docs/ai_patterns.md](./docs/ai_patterns.md) | AI acts, the human stays in charge — composer, live run feed (`AgentRun`), the one law's split (modify → review-before-apply; create → save-direct + the `ResultHeader` receipt), findings, provenance, confidence; the UI half of the SDK's [ai doc](../app-sdk/docs/ai.md)., the whole run in a dialog (`AgentRunScope`/`AgentRunPane`/`AgentRunActions` — a parked question REPLACES the feed, actions in the footer) |
20
- | [docs/composition.md](./docs/composition.md) | The design-language contract — canvas + content column, heading altitude (incl. eyebrow vs group lead — a label is one or the other), banded cards, register vs inset rows, master-detail `Drawer`, view controls, color discipline, typography, whitespace, and how to TEST an overlay component (a `Popover`-backed surface never mounts under jsdom). |
20
+ | [docs/composition.md](./docs/composition.md) | The design-language contract — canvas + content column, heading altitude (incl. eyebrow vs group lead — a label is one or the other), banded cards, register vs inset rows, master-detail `Drawer` on a LIST screen vs a child collection's row EXPANDING inside a record, view controls, RECORD EXTENT (one page, sections scrolled to and never routed to), color discipline, typography, whitespace, and how to TEST an overlay component (a `Popover`-backed surface never mounts under jsdom). |
21
21
  | [docs/templates.md](./docs/templates.md) | The map of `examples/tpl_*.tsx` — what shape each template solves and which to start from (copy + adapt, never import) — plus the record-surface composition rules (pipeline order, static shape, decision budget). |
22
22
 
23
23
  ## Iron rules
@@ -39,6 +39,13 @@ CURRENT major only — upgrading an app across majors is `MIGRATION.md`.
39
39
  never a second chip. A chip beside a name reads as its PEER (a colored one reads louder),
40
40
  inverting the hierarchy the row is scanned by. →
41
41
  [composition.md §`Badge` is for STATUS only](./docs/composition.md).
42
+ - **A countdown ANNOTATES its date field — never a trailing badge, and never hand-rolled.**
43
+ How far off a date is, what to call that number, and how loud it reads are three CONTRACTS:
44
+ invent them per screen and two registers disagree about when "urgent" starts and phrase "2
45
+ days late" four ways. Reach for `@lotics/ui/deadline` and spread `deadlineAnnotation` onto the
46
+ `DetailRow` — the three annotation slots carry the three urgency levels exactly. A binary
47
+ `error={overdue ? … }` is the tell: it makes a date due tomorrow read like one due in three
48
+ months. → [catalog.md §Dates & times](./docs/catalog.md).
42
49
  - **The kit's fonts/colors/icons ARE the design system** — never a custom font, icon set, or
43
50
  hand-picked palette shade; color is `solid`/`tint`/`ramp` with ONE accent per screen.
44
51
  - **Hand-typed type is off-system — and it always lands too small.** Every run of language is
@@ -54,6 +61,11 @@ CURRENT major only — upgrading an app across majors is `MIGRATION.md`.
54
61
  (a CTA, ⋯, a checkbox, a `Link`) is a role-less `PressableRow` + a **`PressDoor`** sibling
55
62
  (the tab stop, name, focus ring), never `PressableHighlight`, which wraps its children in the
56
63
  button. `Table`/`TableRow` already does this. → [composition.md](./docs/composition.md).
64
+ - **A fact every row needs is a COLUMN; a fact one row needs expands in place.** Sending the
65
+ reader to a drawer for either loses the surrounding rows and charges a navigation to come
66
+ back — which is what makes scanning twenty records unaffordable. `TableRow`'s `detail` +
67
+ `expanded` reveal the detail beneath the row; keep the drawer for a heavy form.
68
+ → [catalog.md](./docs/catalog.md).
57
69
  - **A reference to another record is a FIELD** — it wears the inline editor's own resting
58
70
  surface, carries an **`InlineButton`** Open inside it, and PEEKS its facts on press (detach
59
71
  lives in the peek). Never its own Section, never a card, never the other record's fields as
package/MIGRATION.md CHANGED
@@ -4,6 +4,43 @@ Breaking changes, newest first — normally per major, plus the rare minor that
4
4
  anyway (recorded under its exact version). The current contract lives in `AGENTS.md` + `docs/`;
5
5
  this file exists only to move an app from one release to the next.
6
6
 
7
+ ## 24.0.0 — `TextLink` navigates, `TextButton` acts
8
+
9
+ ```tsx
10
+ <TextLink onPress={selectAll}>Select all</TextLink> // BEFORE
11
+ <TextButton onPress={selectAll}>Select all</TextButton> // AFTER — same look, role=button
12
+ ```
13
+
14
+ `TextLink` no longer takes `onPress`. It had three modes behind one name — `href` made it a link,
15
+ `onPress` made it `role="button"`, neither made it plain underlined text — so one export was three
16
+ components, and the button mode was nearly all of the use, including `OptionList` and `FilterChip`
17
+ inside this kit.
18
+
19
+ **Pick by what the press DOES, not by appearance.** Both are UNDERLINED — that is what marks a
20
+ surface-less control interactive, and it is not optional (position is a learned convention, hover is
21
+ absent on touch). What separates them is INK: blue means NAVIGATION, so `Link`/`TextLink` are blue
22
+ and go somewhere while `TextButton` is zinc and acts. They differ in `role` too, which is what a
23
+ screen reader announces and what keeps open-in-new-tab off a control that opens nothing.
24
+
25
+ `TextButtonColor` is `default | danger` only — deliberately not `TextColor`. A `muted` text action
26
+ would mute the one thing carrying the act's weight; use `Button color="muted"` when an act must be
27
+ that quiet. `disabled` greys to zinc-400, drops the press and the hover wash, and announces
28
+ `aria-disabled`. ONE size, no `icon`/`href`/`decoration`.
29
+
30
+ Migration is mechanical: every `TextLink` with `onPress` becomes `TextButton`; every `TextLink` with
31
+ `href` or with neither prop is unchanged. It is NOT visible — a migrated action keeps the same
32
+ underlined neutral ink it had. Passing `size`, `icon`, or a `TextColor` other than `danger` is now a
33
+ type error; drop them (`Text`'s default size is already `sm`, so the common `size="sm"` was
34
+ redundant), and reach for `Button` when a verb needs a glyph or a real hit target.
35
+
36
+ It also sharpens an existing rule: blue-600 means NAVIGATION specifically, not "interactive"
37
+ generally — the underline carries interactive, so an ACTION stays zinc.
38
+
39
+ A destructive text-weight act is `TextButton color="danger"`, which lands its ink on a text column
40
+ by construction and keeps a 40px target via `hitSlop`. Do NOT reach for a fill-less `Button`
41
+ (`danger-secondary`, `muted`) there: with no box, its optical edge is its ink, inset by its own
42
+ padding, so it reads as indented against the labels it should line up with.
43
+
7
44
  ## 23.0.0 — the `Inline*` editors are WHITE, and `variant` is renamed
8
45
 
9
46
  ```tsx
package/docs/catalog.md CHANGED
@@ -116,21 +116,26 @@ the `Task` compound owns the row — `TaskStatus` (a `CheckCircle`), `TaskTitle`
116
116
  `InlineTextInput`), `TaskActions`. **The row carries the TITLE; the fields a user can SET hang
117
117
  under it as `TaskSubRow`s** (`InlineDatePicker`/`InlineMemberSelect`/`InlineSelect`, each with
118
118
  its NAME beside it), never a `DetailTable` in a `TaskDetail`, which declares a grid of its own.
119
- The SAME composition serves a record's 5–8-task drawer checklist and a grouped desk board,
120
- because a sub-row reflows instead of being authored per surface; add `SuggestionChip` commons +
121
- `CaptureRow` for the checklist case. **A list you scan DOWN columns — the same four values
122
- compared across twenty rows — is a `Table`, not a `TaskList`.** And a record's own PROGRESS is
123
- not a checklist at all — that's `Pipeline` ([`tpl_record`](../examples/tpl_record.tsx) § Progress).
124
- See [`tpl_item_list`](../examples/tpl_item_list.tsx) for the checklist grammar, and
125
- [`tpl_task_board`](../examples/tpl_task_board.tsx) for columns.
119
+ One composition serves a standalone task surface at any density — a flat list or a grouped desk
120
+ board — because a sub-row reflows instead of being authored per surface; add `SuggestionChip`
121
+ commons + `CaptureRow` for the capture case. **A list you scan DOWN columns — the same four values
122
+ compared across twenty rows — is a `Table`, not a `TaskList`.**
123
+
124
+ **A RECORD never carries a checklist.** Its work state is `Pipeline`
125
+ ([`tpl_record`](../examples/tpl_record.tsx) § Progress): a reached stage owns the facts it stamps,
126
+ the LIVE stage alone carries its condition and the one act that leaves it. A per-desk checklist
127
+ described the work but never said where the record SAT, and it put the CTA a section away from the
128
+ state that gated it — so `tpl_record` replaced one with the pipeline, and a register's workspace
129
+ drawer follows the same shape. `TaskList` is for a surface whose SUBJECT is tasks; see
130
+ [`tpl_task_board`](../examples/tpl_task_board.tsx).
126
131
 
127
132
  ### Tabular data — pick by SCALE + intent
128
133
 
129
134
  Two columnar shapes, and the choice is about data size:
130
135
 
131
136
  - **High-volume register** (thousands+ you BROWSE) — `Table` (columns defined once; sortable
132
- headers via `SortHeader`; paired with `Pagination`) + read-only rows that open a `Drawer`
133
- to edit. It scales by PAGING — renders one page, never the whole set — so you FILTER +
137
+ headers via `SortHeader`; paired with `Pagination`) + rows that either open a `Drawer` to
138
+ edit or expand in place (`detail` + `expanded`) when the detail is read or light-edit. It scales by PAGING — renders one page, never the whole set — so you FILTER +
134
139
  search, you don't group. Worked example: [`tpl_item_list`](../examples/tpl_item_list.tsx).
135
140
  Never an HTML `<table>` or a `.map` of rows. The register adapts to its container on its
136
141
  own — hides columns by `priority`, stacks rows below the two-column floor (see the `table`
@@ -456,7 +461,9 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
456
461
  ### Actions, links & menus
457
462
 
458
463
  - **`button`** — `Button`: the labelled action; `title` MANDATORY (it is the accessible
459
- name), `color` = emphasis/risk.
464
+ name), `color` = emphasis/risk. A fill-less `Button` shows no box, so its optical
465
+ edge is its INK, inset by its padding — don't align one to a column of TEXT; that
466
+ is `TextButton`'s job.
460
467
  - **`icon_button`** — `IconButton`: the icon-only circular action (see
461
468
  [Actions](#actions)).
462
469
  - **`back_button`** — `BackButton`: the one go-back control — don't hand-roll it (an
@@ -468,10 +475,23 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
468
475
  never an icon button beside unclickable prose.
469
476
  - **`link`** — `Link`: the EXTERNAL hyperlink — fixed underline+blue + `role="link"`;
470
477
  `onPress` only (the consumer wires the opener).
471
- - **`text_link`** — `TextLink`: underlined text that's optionally an `onPress` action or an
472
- `href` link (a real `<a>` on web middle-click / open-in-new-tab work), or plain
473
- underlined text to wrap in your own pressable; inherits every `Text` prop; the neutral
474
- counterpart to the fixed-blue `Link`.
478
+ - **`text_link`** — `TextLink`: underlined text that NAVIGATES `href` renders a real
479
+ `<a>` on web (middle-click / open-in-new-tab work) and takes the blue navigation ink;
480
+ with no `href` it stays NEUTRAL: plain underlined text to wrap in your own pressable,
481
+ or a MARKER that a value leads somewhere (a record reference whose press opens a peek,
482
+ not a trip). `color` overrides either way; inherits every `Text` prop. It does NOT
483
+ act — that's `TextButton`.
484
+ - **`text_button`** — `TextButton`: a low-chrome ACTION at text weight ("Select all",
485
+ "Clear", "Add stop") — the third rung of chrome after `Button` (40px surface) and
486
+ `InlineButton` (28px, filled, on a field's surface). **Underlined, in neutral ink**:
487
+ underline marks interactive, blue marks navigation — so `Link` goes somewhere and
488
+ this acts. Choose by what the press DOES. The underline is not optional (position is
489
+ a learned convention, hover is absent on touch). `TextButtonColor` is
490
+ `default | danger` only — `muted` would mute the thing carrying the act (use
491
+ `Button color="muted"`). `onPress` REQUIRED; `disabled` greys to zinc-400, drops the
492
+ press + wash, announces `aria-disabled`. Hover wash, focus ring and a 40px target
493
+ (32px box + `hitSlop`) come from `PressableHighlight` — a pressable `Text` has
494
+ none of them. ONE size, no icon (a verb needing a glyph is a `Button`).
475
495
  - **`chip`** — `Chip`: the generic pill — pressable when `onPress` (announces as a button;
476
496
  pass `accessibilityLabel` when children aren't self-describing text) + an
477
497
  absolutely-positioned dismiss ✕ sibling when `onDismiss` (its name = `dismissTooltip` ??
@@ -644,6 +664,21 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
644
664
  **12- vs 24-hour derived from `locale`** — never from the browser, which is why this is not
645
665
  an `<input type="time">`. The value is always canonical 24-hour `"HH:mm"`; `locale` changes
646
666
  only what is displayed and typed.
667
+ - **`deadline`** — the COUNTDOWN vocabulary, RN-free and pure: `daysUntil`, `countdownLabel`,
668
+ `deadlineTone` / `deadlineColor`, `deadlineAnnotation`, `nearestDeadline` (+ `DeadlineLabels`,
669
+ `DeadlineThresholds`, `Deadline`). Three contracts a screen must not invent for itself — how
670
+ far off a date is, what to CALL that number, and how loud it reads. `daysUntil` collapses both
671
+ ends to midnight because the vocabulary is day-granular: rounding the raw gap lands on the
672
+ wrong day whenever now and the deadline straddle noon (a 02:00 cut-off tomorrow reads "today";
673
+ one at 02:00 today reads "1 day overdue" 12 hours in). Wording comes from the `deadline` locale
674
+ slice; the 1-day/3-day tone lines are freight defaults and move per domain via
675
+ `DeadlineThresholds`. **`deadlineAnnotation` spreads onto the `DetailRow` holding the date** —
676
+ a deadline is a property OF that field, and the three annotation slots carry the three urgency
677
+ levels exactly (`error` / `warning` / `description`), so it never becomes a trailing badge
678
+ speaking a vocabulary the rest of the field grid does not. `nearestDeadline` returns an OVERDUE
679
+ milestone FIRST and expects the caller to pass only OPEN ones — done-ness is not knowable from
680
+ a date, and skipping past a missed cut-off is how a record silently stops reporting the failure.
681
+ Worked example: `tpl_record`'s Due rows.
647
682
 
648
683
  ### Inline editing & record surfaces
649
684
 
@@ -785,6 +820,16 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
785
820
  divider-set emphasized close with `zeroLabel` for settled. The financial-statement grammar
786
821
  at record density; worked example: [`tpl_item_list`](../examples/tpl_item_list.tsx)
787
822
  drawer.
823
+ - **`reference_field`** — `ReferenceField`: a reference to ANOTHER RECORD, rendered as a
824
+ FIELD VALUE — the kit's inline-editor surface (so a pointer sits in the value column
825
+ like the editors above and below it), whose press opens a PEEK of that record's facts.
826
+ `name` + optional `code` + `facts` (label/value pairs) + `onOpen`/`openLabel` and an
827
+ optional `onRemove`. The verbs live INSIDE the peek, never on the field: `Open` goes to
828
+ the referenced record's own page, `Remove` detaches. The value is a `TextLink` marker
829
+ (underlined, neutral) because the press opens a summary rather than navigating. A
830
+ reference is a FIELD, not a section — one pointer to one other row never earns a heading
831
+ and a rail entry. Worked example: [`tpl_record`](../examples/tpl_record.tsx) § General
832
+ (customer) and its Transport parties.
788
833
  - **`peek`** — `Peek`: drill-down for inline references — press a name/id where it appears
789
834
  and get its details in an anchored popover, without leaving the screen; keep the content a
790
835
  summary with ONE action to the full record.
@@ -807,6 +852,16 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
807
852
  On pressable rows dropped values stay one tap away — the row opens the record; a READ-ONLY
808
853
  register (rows without `onPress`) has no door, so give the columns it can't afford to lose
809
854
  a low `priority` (e.g. `priority: 1` — outlives its right-side neighbours).
855
+ **`detail` + `expanded` reveal a row's detail BENEATH it instead of navigating away.** A
856
+ register answers "which one"; the moment it cannot answer "and what about it" the reader is
857
+ sent to a drawer, loses the surrounding rows, and pays a navigation to come back — which is
858
+ what makes scanning twenty records unaffordable. Reach for it when the detail is read or
859
+ light-edit; a heavy form still deserves its own surface. `expanded` is CONTROLLED — how many
860
+ may be open at once is the caller's rule, so the component does not pick one. The detail
861
+ renders as a SIBLING of the press surface (a control inside it would otherwise be swallowed
862
+ by the row's own toggle), the row shows the open wash while it is out, and the door announces
863
+ `aria-expanded`. Prefer the columns first: a fact every row needs is a column, not a reason
864
+ to expand.
810
865
  - **`sort_header`** — `SortHeader` + `SortState`/`SortDir` + `cycleSort` + `sortBy` +
811
866
  `SortHeaderLabels`: the sortable column header and the sort-state helpers `Table`/
812
867
  `DataGrid` consumers drive.
@@ -335,7 +335,29 @@ record id so per-record state resets on step. Facts are `DetailRow`s; the commit
335
335
  after the scroll body; actions sit right, a leading `<Text style={{ flex: 1 }}>` hint pushes them
336
336
  there). The open row shows a `selected` highlight (and `marked` for a bulk-ticked row). `Peek` is
337
337
  ONLY for secondary references — a glanceable summary popover on an inline reference (a customer
338
- name, a record id) with ONE action to the full record — never the primary row press.
338
+ name, a record id) with ONE action to the full record — never the primary row press. That ONE
339
+ action goes to the referenced record's own page; a local `Drawer` that re-shows the facts the peek
340
+ just showed makes "Open" mean *read that again, wider*, and leaves the real record unreachable
341
+ from the only control that promised it.
342
+
343
+ **All of the above is the LIST SCREEN.** A CHILD COLLECTION inside a record page — fees, lines,
344
+ stops, containers — does the opposite: the row **EXPANDS** (`TableRow` `detail` + `expanded`),
345
+ never docks a drawer. A drawer's whole job is to supply a context the reader is missing, and on a
346
+ record page they are already in it: docking one charges a navigation to see nine fields, takes the
347
+ sibling rows away while they read, and charges again to come back. `expanded` is CONTROLLED, so
348
+ pick the rule — for a ledger it is ONE at a time, because two open details push the next row off
349
+ screen and a register that cannot be scanned has stopped being one. The boundary is the detail's
350
+ weight, not its field count: read or light-edit (independent inline commits) expands, a real form
351
+ with a submit and cross-field validation still earns its own surface.
352
+
353
+ The expansion carries **EVERY** field, including the ones already in columns. Column cells are
354
+ read-only, and `table_fit` tier 2 drops droppable columns on the explicit promise that a door
355
+ reaches the dropped values — the expansion IS that door, so anything it omits is unreachable
356
+ rather than merely inconvenient. `Table` does not expose its fit result, so this cannot be
357
+ conditional. This is also the answer to "just add more columns": the fit budget is the MEASURED
358
+ container, so past it the register drops columns silently instead of overflowing — widening does
359
+ not reveal the extra fields, it hides them without saying so. And free text (a note) has no column
360
+ width at any measure.
339
361
 
340
362
  ## Two work shapes, no side panels
341
363
 
@@ -422,7 +444,21 @@ All view controls are 40px tall (`CONTROL_HEIGHT`), `sm` labels, in ONE wrapping
422
444
  action `primary`; it sits at a different altitude than the form's one terminal commit.)
423
445
  - **Button labels carry no trailing ellipsis** ("Assign", not "Assign…"). **Button color is
424
446
  VALENCE/RISK, never category**: the ladder `muted < secondary < primary` is the emphasis axis;
425
- `danger` (and its quieter `danger-secondary`) marks destructive — that's the whole axis. No
447
+ `danger` (and its quieter `danger-secondary`) marks destructive — that's the whole axis. A
448
+ fill-less `Button` (`danger-secondary`, `muted`, un-colored) shows no box, so its optical edge is
449
+ its INK, inset by its padding: **never align one to a column of TEXT** — it reads as indented
450
+ against every label starting on the true edge, and reaching for the filled tier to fix that buys
451
+ alignment with prominence, on what is usually the rarest act on the surface. A text-weight act is
452
+ `TextButton`, which lands its ink on the column by construction. Chrome has three rungs: `Button` (a 40px control with a
453
+ surface) → `InlineButton` (28px, filled, on a field's own surface) → `TextButton` (no surface,
454
+ sitting in a line of text). **UNDERLINE MARKS INTERACTIVE, BLUE MARKS NAVIGATION** — so
455
+ `Link`/`TextLink` are blue + underline and GO somewhere, `TextButton` is zinc + underline and
456
+ ACTS. Pick by what the press DOES. The underline is not optional on a surface-less control:
457
+ POSITION is a learned convention rather than an affordance (invisible to a first-time reader) and
458
+ hover is not one either (absent on touch, revealed only once you're already there). A tint is the
459
+ other way to say it — the Material/HIG answer — but blue is spoken for here and a second accent
460
+ for "actionable" would collide with one-accent-per-purpose. Never a `muted` text action: it mutes
461
+ the one thing carrying the act. No
426
462
  "success"/green button. Decision UIs put positive/negative color on the STATUS (dot) and verdict
427
463
  (colored `Text`), not the buttons.
428
464
  - **Create buttons carry NO `+` icon** — "Add fee", not "+ Add fee". A create is `primary` and
@@ -493,6 +529,24 @@ gate's SCOPE, once** — never prose beside the button, never revealed only on p
493
529
  no rail entry — it is not a place you navigate to. Narrow, it collapses onto the same bar the
494
530
  sections use, on the RIGHT: sections are where you ARE, the discussion is what is being SAID,
495
531
  and the two poles keep the sides they hold on a wide screen.
532
+ - **A record is ONE PAGE. A section is a place you SCROLL to, never a route you swap to.** The
533
+ rule above already assumes it — a section earns a POSITION in a top-to-bottom read, and the
534
+ discussion is docked precisely because a thing "parked at one position is unreachable from
535
+ every other". Routing the sections makes that true of every one of them. It is tempting on a
536
+ long record and it does not survive contact: each fix it needs rebuilds the whole-record view
537
+ in miniature — a dot to say which hidden section just changed, a page-band action to
538
+ re-surface an affordance some section had swallowed, an "everything" route that IS this page.
539
+ It also HIDES what it does not carry: a whole-record drop target advertised only inside the
540
+ files section does not exist from the eight destinations that are not it. And it charges
541
+ permanently — browser find reaches only the open section, two sections can never be read
542
+ together, and every cross-section check costs a click each way. A long record is a DENSITY
543
+ problem, and density is cuttable (a choice that took four rows, a group that restated the
544
+ title): **cut the sections, keep the page.** The rail stays — `useSectionNav` is a scroll-spy
545
+ and a jump, and it carries a `Badge variant="dot"` on any section holding a condition, which
546
+ is how the page says WHERE without a banner at the top saying it for one section at a time.
547
+ A cross-section TASK is not an argument for routing either: it belongs in an overlay that
548
+ spans the record (an extract review lists every proposed field, whatever section owns it),
549
+ which the rail then never has to serve.
496
550
  - **A VARIABLE-LENGTH, ORDERED fact is a `Sequence`, not a row of fields.** Before reaching for
497
551
  any layout lever, check the section is not carrying a MODELLING error: `Origin` /
498
552
  `Transfer point` / `Destination` looks like three fields and is actually one route — it cannot
@@ -618,7 +672,13 @@ selection washes).
618
672
  **TEXT never wears `solid()`** — the 500 is a FILL shade (dots, series). A status word/number uses
619
673
  `Text`'s valence tokens: `color="danger"` (red-900) / `"warning"` (amber-700) / `"success"`
620
674
  (emerald-700) — all AA on white. Map a dynamic `ColorName` to a token at the call site; hue nuance
621
- (rose vs orange) stays on `Badge`s. Links use `Link`/`TextLink` (blue-600), never `solid("blue")`.
675
+ (rose vs orange) stays on `Badge`s.
676
+
677
+ Links use `Link`/`TextLink` (blue-600), never `solid("blue")` — and blue-600 means NAVIGATION
678
+ specifically, not "interactive" generally: the underline is what marks a surface-less control
679
+ interactive (see the button ladder above), so a `TextButton` ACTS and stays zinc. `TextLink` takes
680
+ the blue only when it has an `href`; as a passive marker on a value it stays neutral, because a
681
+ press that opens a PEEK is not a trip.
622
682
 
623
683
  ## `Badge` is for STATUS only — everything else is text
624
684
 
@@ -191,6 +191,14 @@ A READ-ONLY value in the same column — a computed total, a system ID, a synced
191
191
  border) but stays FLAT and non-interactive, so editable (chip) vs read-only (flat) is legible at a
192
192
  glance and the static value never reads as a disabled input.
193
193
 
194
+ **Only in a grid that MIXES the two.** "Copies the editor box metrics" means it hard-sets
195
+ `INLINE_CONTROL_HEIGHT` — worth paying to hold a column straight beside real editors, pure waste
196
+ where there are none. In an ALL-static grid (a peek's facts, a summary popover) it reserves a 40px
197
+ control band for a 20px value, and the row is the MAX of that box and the table's own band, so
198
+ neither fix works alone: use plain `Text` for the value AND pass `DetailTable` a `minHeight` (28,
199
+ `DetailRow`'s own default). Same test as `labelWidth` — a `DetailTable`'s defaults are the
200
+ inline-control grid's; a grid that isn't one overrides both.
201
+
194
202
  ### Choosing a CHOICE control — by option count, not by taste
195
203
 
196
204
  A field whose value is one-of-N has three answers, and the wrong one is what makes a record
@@ -216,7 +224,17 @@ on a reference, `Call` on a phone): the act names the value, so it travels with
216
224
  instead of being paired back to a control at the page's right edge. Pass it UNCONDITIONALLY and
217
225
  `disabled` it when there is nothing to act on — a verb that appears once the value is non-empty
218
226
  resizes the field as the user types, which is the one thing an inline editor promises never to
219
- do. Anything that is not a verb a status `Badge`, a unit ("kg"/"$", which belong IN the value
227
+ do. **"About the value" is the whole test, and DEPARTURE fails it:** `Open`, on a field holding a
228
+ reference to another record, is not an act on that value — it is leaving for a different record —
229
+ and putting it on the surface hides two destinations behind one press area (the box opens a
230
+ summary, the verb 8px away navigates). A reference's `Open` and `Remove` live inside its PEEK,
231
+ which is what `LedgerRow` already enforces by IGNORING `reference` while `peek` is set. What marks
232
+ the field as a reference instead of typed text is then the VALUE, not a verb: `TextLink` with
233
+ neither `href` nor `onPress` — the kit's underline, non-interactive, so it sits inside the field's
234
+ own press target without nesting a control. Not a `Badge` (STATUS only), and not `Link`'s fixed
235
+ blue + `role="link"`, which promises navigation a peek does not perform.
236
+
237
+ Anything that is not a verb — a status `Badge`, a unit ("kg"/"$", which belong IN the value
220
238
  via `InlineNumberInput format`) — composes into a row inside the VALUE cell, so it costs only the
221
239
  row that wants it. See the "General" section of `tpl_record`, which also reads top→bottom as a
222
240
  full record surface. Not every field is a same-height swap — a tag field, a status, or an
package/docs/templates.md CHANGED
@@ -196,33 +196,27 @@ screens — register, per-row action, gated selection, and act-on-many in one. T
196
196
  only](./composition.md)). A per-row action `Button` (here Print) is the row's primary action in
197
197
  the trailing column, `⋯` its overflow. A select-all band, footer totals + `Pagination`, and a
198
198
  `FloatingActionBar` carrying the bulk action while rows are ticked.
199
- - **A row press opens the PRODUCTION workspace `Drawer`**, which demonstrates the full
200
- record-workspace anatomy section by section:
201
- - **Details `DetailTable`** showing every inline field type text, `InlineMemberSelect`,
202
- dates incl. optional time, dot-select, money number, an `InlineSelect multi` tag field,
203
- and `InlineStatic` + a System badge for read-only values. It is also the worked example of
204
- where a VERB goes: Phone Call and Due Today are both `InlineButton`s in the field's
205
- `actions` (on the surface, disabled while empty) the date editor included, since a field
206
- anchors its overlay to its own box; the System badge, not being a verb, sits beside its value.
207
- - **Linked records** the customer's other records as `ListItem`s (title + description at
208
- the column edge, badge, amount, chevron); pressing one PUSHES an **editable** workspace
209
- for that record inside the drawer via the hosted `ScreenRouter` while `canGoBack` the
210
- drawer header swaps to a back button + the pushed id and the sequencer hides; back
211
- pops with scroll preserved.
212
- - **Files** with its own Add CTA `FileRows` CRUD via `pickFiles`, plus an EXPECTED
213
- document as a ghost `FileRow placeholder` with a Request action. The section is wrapped in
214
- a `<FileDropTarget paste>`, so a drag anywhere onto it or a Ctrl/Cmd+V lands on the same
215
- `addFiles` handler; a `SectionHeadingTitle description` names the three ways in.
216
- - **Payment** the `Ledger` (charge/received groups with sums, peekable fee rows,
217
- `LedgerTotal`) + a Record-payment popover that appends a receipt.
218
- - **Activity** the CRM touch-log shape: optional outcome pills (`ChipGroup`,
219
- tap-to-deselect) over a MULTILINE note (the note IS the record and the one requirement),
220
- an always-visible optional follow-up `InlineDatePicker`, an "Attach files" secondary
221
- (`pickFiles` → removable `FileThumbnailGrid` in the composer), and the primary Log button
222
- saying WHY when disabled. Below, a labelled History `Timeline` of outcome-typed entries,
223
- newest first — an entry's attachments ride its expandable details as a thumbnail grid
224
- whose press opens the shared `FileGalleryModal`.
225
- - A closing `DangerZone`.
199
+ - **A row press opens the PRODUCTION workspace `Drawer`** which RENDERS
200
+ [`tpl_record`](../examples/tpl_record.tsx) in `chrome="drawer"`, not a copy of it. Same
201
+ sections, same order, same rules, because it is the same component: `<TplRecord
202
+ chrome="drawer" code={row.id} />`. A hand-built second record surface is exactly how the two
203
+ drifted apart a checklist on one side and a `Pipeline` on the other, a reference as a
204
+ section here and a field there so the register owns the REGISTER and nothing else.
205
+ - What the chrome flag changes is only what a nested surface must not do: comments become the
206
+ LAST SECTION of the drawer's own scroller (a `Drawer` has no second gutter to dock them
207
+ into), and the back button is left to the Drawer's header. Everything else follows from
208
+ width — the layout measures its CONTAINER via `onLayout`, never the window, so the outline
209
+ rail, the docked discussion and the mirrored gutters collapse on their own, and the section
210
+ picker is a popover ANCHORED to its bar button rather than a full-page `Modal` that would
211
+ escape the drawer.
212
+ - **Linked records** still push an editable screen for that record inside the drawer via the
213
+ hosted `ScreenRouter` while `canGoBack` the drawer header swaps to a back button + the
214
+ pushed id and the sequencer hides; back pops with scroll preserved.
215
+ - Two things this template no longer demonstrates, so look elsewhere: the drawer does NOT
216
+ write back to its row (settling a ledger used to flip the register's paid state live — the
217
+ drawer's money is the record's now), and the CRM call-log shape (outcome pills over a
218
+ multiline note, an outcome-typed history `Timeline`) went with the Activity section, which
219
+ the record surface has no counterpart for.
226
220
  - **The "Enter data" INTAKE FORK** (the New CTA) — AI FIRST, the form as fallback, one phased
227
221
  dialog. The whole register page is ALSO a `<FileDropTarget … paste>` (the whole-surface intake
228
222
  standard): a file dropped or Ctrl/Cmd+V pasted ANYWHERE on the register opens this same dialog
@@ -416,7 +410,9 @@ billing, and quick-capture templates. Top → bottom:
416
410
  map** (every section including Danger zone; subsections never join it) — headed by the
417
411
  bare circular `BackButton`; it scroll-spies the active section (`current={activeKey === key}`
418
412
  on each item — navigation, not selection), and on narrow containers becomes the pinned
419
- current-section bar opening a section-picker `Modal` whose items carry the same `current`.
413
+ current-section bar opening a section-picker POPOVER, anchored to that bar's own button, whose
414
+ items carry the same `current`. Anchored, not a `Modal`: a modal escapes its container, so the
415
+ same surface hosted in a drawer would take the whole window over the register behind it.
420
416
  - **The page layout is the DOCS layout — the reading column is CENTRED IN THE VIEWPORT, and
421
417
  the rail sits in its left gutter.** The `ScrollView` is FULL-WIDTH — **never cap it at
422
418
  `CONTENT_MAX`, or the scroll container becomes the column and the gutters go wheel-dead**