@lotics/ui 45.10.0 → 46.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
@@ -15,189 +15,122 @@ CURRENT major only — upgrading an app across majors is `MIGRATION.md`.
15
15
  | Doc | Read it for |
16
16
  |---|---|
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` + `loticsResolve()` — the pre-bundle list and the whole `resolve` block a custom-code app's `vite.config.ts` imports rather than hand-carries, dev-link included). Read before building any screen; reuse first. |
18
- | [docs/data_entry.md](./docs/data_entry.md) | **a picker over a select field keeps the field's colours** (`optionPicker` — the hand-rolled `map` to `{value,label}` drops `color`, so a value reads coloured in a register and grey in the editor); a choice that GATES a form is usually one not to ask (two doors to one room is not a chooser; show the dominant path, offer the other beside its submit, and give a recovery link a quieter treatment than an alternative way in); Which editing pattern for which job — inline edit, fieldset forms, choosing a CHOICE control by option count (and why "no selection" is a PLACEHOLDER, never an option valued `""` — the picker reads that as nothing chosen and renders down to its chevron), browser-autofill suppression (search controls only), find-or-create (`Combobox`), line items, handoffs, phased records, billing, tags, dispositions, attachments (`InlineFiles` for a record ROW — list + one CTA, `multiple` decides add-vs-replace; the `FilesEditor` COMPOUND for a whole section — 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, whether a multiline value keeps its fixed reserve or `autoGrow`s (who decides the length — the field, or whoever is typing); the swap rule for a NEW editor (same string ⇒ one element, different string ⇒ swap) worked through its two live cases — `InlineMarkdown` holds the SAME WYSIWYG the product's knowledge docs use (`MarkdownEditor`, ProseMirror on web, raw text on native) mounted AT REST, so focusing it moves nothing and the prose stays selectable — the case that shows a swap can be made to look stable but never to be it, while a URL takes `InlineTextInput`'s `link` TREATMENT because marking it changes its ink and not its characters, the commit-on-blur vs action-press ordering law (the kit gates the press — `pending_commits`). |
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 — it turns on WHO supplied the values (machine → a gate: a diff when something is being replaced, a full editable preview when records are being created from a document; human-typed → save-direct + the `ResultHeader` receipt), findings, provenance, confidence; **after the run** — a stored record that fills up from several writers (a person, a chat agent, an extraction, an automation) and remembers none of them: an unwritten value must not render like a written one, a machine's prose and a person's must not share a treatment, model markdown goes in `variant="embedded"`, and the read path must project every field the write paths set; 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, **Stop** while streaming), **stopping** (`cancel` stops the run, `abort` only stops listening — so closing a dialog must `cancel` or it keeps billing); **review surfaces compose from atoms** — `DiffValue` (a changed value, droppable in any cell/row/total), `DiffMark` (what happened to the row — ONE circular disc, every surface, its kind derived from before/after rather than a boolean "did this change" — an empty `before` is `added`, never `changed`), `useChangeSet` (accept/reject/undo bookkeeping, no layout) — see [MIGRATION.md](./MIGRATION.md) for the `ChangeReview` family they replace |
20
- | [docs/composition.md](./docs/composition.md) | The design-language contract — **the form comes before the treatment** (name what the subject IS — register, rail, board, timeline, deviation queue — before reaching for parts; a subject flattened into rows is well-typed and says nothing, and reuse-first is about not duplicating primitives, not about a screen's shape; **a hierarchy in the DATA does not oblige one in the UI** — who MAINTAINS the structure decides, and a set the reader scans is a register whatever shape the data has), canvas + content column, heading altitude (incl. eyebrow vs group lead — a label is one or the other, and a section heading's own row never wraps so a control label that repeats the title can squeeze the title+description into a narrow ribbon at phone width), banded cards, register vs inset rows (incl. the register laws a row centres its cells by: every cell a FIXED height, a pressable cell on the shared hover token, a column sized by what it carries), the button ladder and **what underlined text may mean** (it GOES somewhere or REVEALS something — never mutates; blue leaves the surface, muted stays on it, and the one in-prose disclosure exception is scoped there), 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), **the register's rhythm** (no hairline between rows — their own height and the hover wash separate them; the register's one line is the band capping the columns, because that is the break whitespace cannot state. Density is `TableRow`'s `minHeight`, never a mode), **a register that TRIAGES** (`TableGroup` — titled bands ordered by what needs the reader now, grouped by what implies a different ACTION rather than by a category already in a column, and what the screen can CLASSIFY kept as a separate list from what it RENDERS, so a value omitted on purpose does not resurface under *Unclassified* and one added later does not vanish), **the register's own craft** (a column header treated as CHROME a step lighter than row metadata, sentence case in the table header too, and a supporting line one RUNG BELOW the line it supports rather than a fixed size — plus the half that rule was missing: every SUBJECT on one row starts at the same rung, the subject leads by WEIGHT and its identity mark rather than by size, and whether the supporting line drops a rung is decided by what else separates the pair; plus the two ways a `columns` array and a rendered row disagree — a partial `priority` annotation, and a conditional column whose cell was not hidden with it), **identity marks** (a mark that looks the same on every row carries nothing — `Avatar` derives its hue from the name and takes circle/square for person vs organization; the palette is ordered by contrast, not spectrum, and never paints a person red or amber; a column of faces must not draw an UNBOUNDED audience as faces, and an overlapping stack separates with translucent INK rather than the surface colour, which a row's wash keeps changing out from under), **where the accent goes** ("which of these" — a selected row, an active filter; never DATA and never the focus ring, while a page-scale position marker like an active TAB takes `primary`, the ink the kit already spends on "this is on"; and selection must not share a wash with hover), **theming is an app-level ESCAPE HATCH, not a product surface** (`applyLoticsTheme({ … })` once at boot, before `mount`, writing the `--lotics-*` roles on the document root; nothing in the platform writes a theme, and `apps.theme.color` is the launcher icon only — the five `var()` roles plus the three font rungs ARE the whole mechanism, and wrapping a component cannot replace them because RN-Web writes `fontFamily` per element and background/border do not inherit), color discipline, **a fact band is read at a GLANCE and a glance is WIDE** (header facts paired into columns rather than stacked one per row — the tell is a screen reported as plain whose treatment is fine, spending its height on facts and its width on nothing; plus a record owes the reader the SIZE of its subject, summed from what the page already holds), **typography** (the ramp is DATA in `type_ramp`, one table both platforms derive from; leading and tracking are baked per rung and a hand-set `letterSpacing` is always a second copy of the curve), whitespace, **where a commit sits** (with its fields, never an overlay's footer — but a full-page editor is not a dialog, so a scrolling document keeps its commit pinned), and how to TEST an overlay component (a `Popover`-backed surface never mounts under jsdom). Selection is never the brand — a neutral ground on a row, a dark doubled edge on a chip or card, with `accent_wash` kept for attention; a lookup key is not a column (the search matches it); `Table` sheds and stacks rather than scrolling horizontally, because narrowing is what asks whether a column earns its width. |
21
- | [docs/reviewing.md](./docs/reviewing.md) | **Reviewing a screen you built** — the other docs say what good looks like; this one says how to find out whether you achieved it, because "it looks fine" is the same claim as "it should work" made with the same evidence. Measure, never eyeball: render it, extract computed values, let the table show the defect. Two GATES that outrank every treatment question (**SUBTRACT** — what question does this element answer, and count FACTS not elements; **RIGHT CONTROL** — name the species before treating it), then 11 probes each naming what to COLLECT and the SIGNATURE in the numbers: type inventory (range, singletons, the label/value shrink war), ground+border+radius across siblings, what each RULE separates, gap RATIOS, the state diff (paint may change, geometry may not), alignment drift (incl. a centred child that moves while its container measures identical), the surface walk (open it, press it, read the settled string), the DATA probe (what is really in the table — incl. grepping rendered text for internal ids), FIRST PAINT (an entry screen showing zero inputs is gated by something, usually a mode chooser), the ABSENCE pass that catches "bland", and microcopy. Plus the extraction snippets, what measurement cannot see, and how to act on a finding (fix the CLASS, and never resolve one with "the docs say X"). |
18
+ | [docs/data_entry.md](./docs/data_entry.md) | Which editing pattern for which job — inline edit, fieldset forms, choosing a CHOICE control by option count, find-or-create (`Combobox`), line items, handoffs, phased records, billing, tags, dispositions, attachments (`InlineFiles` for a record ROW, the `FilesEditor` COMPOUND for a whole section, plus the three-way file INTAKE), stage gates, and the commit-on-blur vs action-press ordering law. |
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 — it turns on WHO supplied the values; findings, provenance, confidence; **after the run** — a stored record that fills up from several writers; the whole run in a dialog, **stopping**; **review surfaces compose from atoms** — `DiffValue`, `DiffMark`, `useChangeSet`. |
20
+ | [docs/composition.md](./docs/composition.md) | The design-language contract — **the form comes before the treatment** (name what the subject IS before reaching for parts), canvas + content column, heading altitude, register vs inset rows, the button ladder, master-detail `Drawer`, the register's rhythm, craft and TRIAGE bands, where the accent goes, theming as an app-level ESCAPE HATCH, color discipline, typography, and where a commit sits. |
21
+ | [docs/reviewing.md](./docs/reviewing.md) | **Reviewing a screen you built** — the other docs say what good looks like; this one says how to find out whether you achieved it. GATES that outrank every treatment question, then 11 probes each naming what to COLLECT and the SIGNATURE in the numbers. Plus the extraction snippets, what measurement cannot see, and how to act on a finding. |
22
22
  | [docs/testing.md](./docs/testing.md) | Driving the kit in a browser — the three anatomies where the a11y tree says one thing and a driver must do another: a `PressDoor` row whose named button always intercepts pointer events (by design), portalled overlays that render at the top of the DOM, and custom pointer drag that `dragTo` cannot move. |
23
23
  | [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) and the ACTIVITY shape — a communications feed where the row's label is the GIST and the body varies by medium, one anatomy rather than a row type per kind. |
24
24
 
25
25
  ## Iron rules
26
26
 
27
27
  - **Reuse first; the catalog can lag `src/`.** Before hand-rolling ANY capability, `ls src/` and
28
- grep for a match a component in `src/` missing from the catalog is a doc bug to fix, not a
29
- license to hand-roll.
28
+ grep it as well as reading the catalog.
29
+ [catalog.md §"How to read a component's API"](./docs/catalog.md).
30
30
  - **One canonical component per data role** (member → `MemberChip`, SEVERAL of them →
31
31
  `AvatarGroup`, who-is-this → `MemberPeek`/`MemberProfileCard`, select → `OptionBadge`,
32
- files → `FilePreview` family, …) —
33
- the catalog's Reach-by-role outranks neighboring code.
32
+ files → `FilePreview` family, …) — the catalog's Reach-by-role outranks neighboring code.
33
+ [catalog.md §"Reach by role"](./docs/catalog.md).
34
34
  - **Responsive layout measures the CONTAINER, not the screen.** `useContainerSize()` reports the
35
- nearest `SizeBoundary`; wrap one around any region whose width stops tracking its parent's, and
36
- `Dialog`/`Drawer`/popover bodies already are boundaries so their contents get the panel
37
- rather than the region they were opened from. The overlay's OWN presentation is not one of
38
- these questions and is not yours to answer: `PopoverContent` reads the screen itself and takes
39
- no prop, because it fills the viewport whatever box opened it — handing it a container width is
40
- how a narrow column on a wide display opened a full-bleed phone sheet.
41
- `useScreenSize()` answers a question about the DEVICE — is there a keyboard
42
- worth a shortcut hint, are these touch targets — and reaching for it to decide SPACE is how a
43
- panel a few hundred pixels wide lays its contents out for the whole display. (A component that
44
- needs a number rather than a bucket — how many columns fit — measures its own box; `Table`
45
- does exactly that.) → [catalog.md §Utility hooks](./docs/catalog.md).
35
+ nearest `SizeBoundary`; wrap one around any region whose width stops tracking its parent's.
36
+ `useScreenSize()` answers a question about the DEVICE.
37
+ [catalog.md §"Utility hooks & plumbing"](./docs/catalog.md).
46
38
  - **No user-facing string is a literal in `src/` when a locale slice already names it.** Every
47
- string a component renders resolves prop → `useLoticsLocale()` slice → the pack's English
48
- default. Write it as an allowlist, not a hunt: a literal typechecks, renders, and passes a
49
- render test in the language it happens to be written in the kit shipped a Vietnamese ⓘ label
50
- to every tenant that way, invisible to a whole suite of vi-pack assertions. Assert a wired
51
- string under the OTHER language. [composition.md §Microcopy](./docs/composition.md).
52
- - **Annotate every droppable table column, or none.** A partial `priority` is the trap: an
53
- unannotated column sorts above the whole hand-written range, so one annotation makes every
54
- other column drop first, and one unannotated column among annotated ones goes first however
55
- important it is. **A conditionally hidden column is the same family**: the `columns` entry and
56
- the row's `TableCell` are two edits, dropping only the first leaves the body one cell longer
57
- than the header and shifts every column after it under its neighbour's title — derive both from
58
- one value so they cannot be edited apart. →
59
- [composition.md §The register's own craft](./docs/composition.md).
39
+ string a component renders resolves prop → `useLoticsLocale()` slice → the English pack.
40
+ → [composition.md §"Localization a rendered string is WIRED"](./docs/composition.md).
41
+ - **Annotate every droppable table column, or none.** A partial `priority` is the trap.
42
+ **A conditionally hidden column is the same family**: the `columns` entry and the row's
43
+ `TableCell` are two edits derive both from one value so they cannot be edited apart.
44
+ [composition.md §"The register's own craft"](./docs/composition.md).
60
45
  - **A section's ADD rides its heading row, right edge** — Add files, Add fee, New line: a
61
- `primary` `Button` beside `SectionHeadingTitle`, rendered empty or full. Under the rows it
62
- extends, the verb MOVES with the row count and vanishes off-screen on a long list; a heading is
63
- the one place it doesn't. → [composition.md §The add-placement law](./docs/composition.md).
46
+ `primary` `Button` beside `SectionHeadingTitle`, rendered empty or full.
47
+ [composition.md §"The add-placement law"](./docs/composition.md).
64
48
  - **A section heading's RUNG is the surface's answer, never the call site's.** There is ONE
65
49
  `SectionHeadingTitle`; it renders `##` xl on a page and `####` md inside a pane, because the
66
- container publishes the altitude — `DrawerScrollArea` / `DialogScrollArea` / `ModalBody` /
67
- `PopoverContent` do it for you, and a pane you build yourself declares it with
68
- `HeadingAltitudeContext`. Do not look for a per-surface heading variant and do not hand-pick a
69
- size: a `##` inside a drawer is 24px over the drawer's own 20px title.
70
- → [composition.md §The heading ramp](./docs/composition.md).
50
+ container publishes the altitude.
51
+ [composition.md §"The heading ramp"](./docs/composition.md).
71
52
  - **`EmptyState` carries NO verb, a FAILED read is not an empty one, and a BAND is not a region.**
72
53
  Four region states, picked by what the region can ASSERT: `Skeleton`/`Loading` in flight →
73
54
  **`ErrorState`** (`message`/`detail`/`onRetry`) on failure → `EmptyState` (succeeded, found
74
- nothing) → `CompletionState` on done. An alert glyph inside an empty state claims the read
75
- succeeded when nothing is known. How much quiet it reserves is a density axis — `compact` for
76
- one band of a record (left-aligned, and glyph-less because an inline mark beside `sm` text
77
- cannot be both legible and cap-aligned in this icon set), the default for a whole region.
78
- → [catalog.md §Status / feedback](./docs/catalog.md),
79
- [composition.md §Empty states](./docs/composition.md).
55
+ nothing) → `CompletionState` on done.
56
+ [catalog.md §"Status / feedback"](./docs/catalog.md),
57
+ [composition.md §"Empty states"](./docs/composition.md).
80
58
  - **Progress: ONE compound, TWO axes — and both are about meaning.** A run of markable rows is
81
59
  `checklist` (`Checklist` + `ChecklistGroup` + `ChecklistItem`, over `stepper`). **`connected`**
82
- asks *is this a sequence* on for one record walking positions, off for N items handled in any
83
- order, where a line claims an order the work does not have. **`mark`** asks *does ticking mean
84
- DONE or CHOSEN* — a ring for completion, the square box for picking (which forms to produce,
85
- which records to export). Spend the ring on a choice and a reader who learned that a filled
86
- ring means "done" reads a ticked pick-list as work already carried out. A GROUP is a phase — a name, optionally a
87
- Show/Hide disclosure, and no body: it owns no COMPLETION, and a ring that never responds invites
88
- a press it refuses. Rows tick ONE at a time; derive position from the highest stamp and one click marks
89
- everything below it without inventing dates. A checklist REPORTS — a field owned by a section is
90
- named as a gap with a `ChecklistNote action` that jumps there, never edited or previewed in the
91
- row. A per-ROW third axis, `ChecklistItem.verdict` (`"pass"`/`"fail"`), is a DECIDED outcome — a
92
- QC gate, a reconciliation match — never toggled; see `docs/catalog.md` for why it withholds
93
- `onToggle`. Worked in `examples/tpl_record.tsx` § Progress. See `docs/catalog.md`.
60
+ asks *is this a sequence*. **`mark`** asks *does ticking mean DONE or CHOSEN*.
61
+ Worked in `examples/tpl_record.tsx` § Progress.
62
+ [catalog.md §"Tasks, to-dos, phased work"](./docs/catalog.md),
63
+ [catalog.md §"Tasks & checklists"](./docs/catalog.md).
94
64
  - **`Badge` = STATUS only; supporting detail is the muted second line.** A type / category /
95
65
  attribute / count is not a status — it belongs under its identity as `size="xs" color="muted"`,
96
- never a second chip. A chip beside a name reads as its PEER (a colored one reads louder),
97
- inverting the hierarchy the row is scanned by.
98
- [composition.md §`Badge` is for STATUS only](./docs/composition.md).
66
+ never a second chip.
67
+ [composition.md §"`Badge` is for STATUS only"](./docs/composition.md).
99
68
  - **A countdown ANNOTATES its date field — never a trailing badge, and never hand-rolled.**
100
- How far off a date is, what to call that number, and how loud it reads are three CONTRACTS:
101
- invent them per screen and two registers disagree about when "urgent" starts and phrase "2
102
- days late" four ways. Reach for `@lotics/ui/deadline` and spread `deadlineAnnotation` onto the
103
- `DetailRow` the three annotation slots carry the three urgency levels exactly. A binary
104
- `error={overdue ? }` is the tell: it makes a date due tomorrow read like one due in three
105
- months. → [catalog.md §Dates & times](./docs/catalog.md).
106
- - **A third-party channel takes `BrandMark`, our own glyphs take `Icon`.** `@lotics/ui/brand_mark`
107
- draws Facebook / Zalo / LinkedIn / X / Google Meet as solid single-colour silhouettes — a reader
108
- scanning a feed recognises the logo before the word. The names are an allowlist, so a channel with
109
- no mark (a phone call, an event, a website) fails to typecheck and falls back to `Icon` instead of
110
- borrowing another company's logo. A product's official full-colour logo is NOT one of these: the
111
- kit ships no vendor artwork, so a row that names a product passes its own image into
112
- `SwitchButton`'s `leading` slot. → [catalog.md §Text & formatting](./docs/catalog.md).
69
+ Reach for `@lotics/ui/deadline` and spread `deadlineAnnotation` onto the
70
+ `DetailRow`. [catalog.md §"Dates & times"](./docs/catalog.md).
71
+ - **A third-party channel takes `BrandMark`, our own glyphs take `Icon`.** The names are an
72
+ allowlist, so a channel with no mark (a phone call, an event, a website) fails to typecheck
73
+ and falls back to `Icon` instead of borrowing another company's logo.
74
+ → [catalog.md §"Text & formatting"](./docs/catalog.md).
113
75
  - **The kit's fonts/colors/icons ARE the design system** — never a custom font, icon set, or
114
76
  hand-picked palette shade; color is `solid`/`tint`/`ramp` with ONE accent per screen.
77
+ → [composition.md §"Color discipline"](./docs/composition.md).
115
78
  - **Hand-typed type is off-system — and it always lands too small.** Every run of language is
116
- `<Text size= weight= color=>` on the fixed rungs; a screen reaching for `fontSize` /
117
- `lineHeight` / `letterSpacing` has left the ladder, and the sizes it invents come out UNDER
118
- `xs` (12) and `sm` (14) — which is what "looks small and cheap" literally is. `Text` is also
119
- the only way to get the letter-spacing curve and Inter's `cv11 ss01 ss03` alternates, the
120
- "designed" tells you cannot type by hand. **Grep the diff: `fontSize:` / `lineHeight:` /
121
- `letterSpacing:` outside `src/` is a bug.** → [composition.md §Typography](./docs/composition.md).
79
+ `<Text size= weight= color=>` on the fixed rungs. **Grep the diff: `fontSize:` / `lineHeight:` /
80
+ `letterSpacing:` outside `src/` is a bug.** [composition.md §"Typography"](./docs/composition.md).
122
81
  - **A label is an `Eyebrow` or a `GroupLead`, and the difference is WHAT IT LABELS.** A value
123
82
  gets `<Eyebrow>` (`xs` muted medium, dense surfaces, never uppercase); a BLOCK someone acts on
124
83
  gets `<GroupLead>` (`sm` medium, page ink). Neither takes `size`/`weight`/`transform` — the
125
- omitted props are the rule. Hand-writing the `Text` yourself is how an act-on-it title ends up
126
- whispering at 12px, which is also where stacked diacritics crowd.
127
- [composition.md §Eyebrow vs group lead](./docs/composition.md).
84
+ omitted props are the rule.
85
+ [composition.md §"The heading ramp"](./docs/composition.md).
128
86
  - **Pure primitives only** — no i18n, analytics, or domain types in `src/` (pass `labels`,
129
87
  callbacks); Lotics-coupled UI belongs in `@lotics/ui-internal`.
130
88
  - **A button never contains a control** — a row that presses open AND carries its own controls
131
89
  (a CTA, ⋯, a checkbox, a `Link`) is a role-less `PressableRow` + a **`PressDoor`** sibling
132
90
  (the tab stop, name, focus ring), never `PressableHighlight`, which wraps its children in the
133
- button. `Table`/`TableRow` already does this. → [composition.md](./docs/composition.md).
134
- - **A fact every row needs is a COLUMN; a fact one row needs expands in place.** Sending the
135
- reader to a drawer for either loses the surrounding rows and charges a navigation to come
136
- back which is what makes scanning twenty records unaffordable. `TableRow`'s `detail` +
137
- `expanded` reveal the detail beneath the row; keep the drawer for a heavy form.
138
- [catalog.md](./docs/catalog.md).
91
+ button. `Table`/`TableRow` already does this.
92
+ [composition.md §"Row actions are always-visible siblings"](./docs/composition.md).
93
+ - **A fact every row needs is a COLUMN; a fact one row needs expands in place.** `TableRow`'s
94
+ `detail` + `expanded` reveal the detail beneath the row; keep the drawer for a heavy form.
95
+ [composition.md §"No dead rows"](./docs/composition.md),
96
+ [catalog.md §"Lists, tables & registers"](./docs/catalog.md).
139
97
  - **ONE LEFT EDGE — a row that paints a wash BLEEDS it, and never insets its content.**
140
98
  `marginHorizontal: -ROW_WASH_BLEED` absorbed by an equal `paddingHorizontal` is net zero, so
141
- a row's content sits on its container's own edge level with the headings, labels and prose
142
- beside it — while the hover/selection wash still breathes by growing into the parent's
143
- padding. `PressableRow` (all variants), `Table`, `ChoiceList`, `ListItem` and `FileRow`
144
- already do this, so a screen composed from the kit aligns with **no per-site padding at all**.
145
- The moment one component insets instead, every neighbour has to be hand-padded by the same
146
- amount to catch up, and that compensation spreads to each new surface built beside it. **An indent is legible only when something VISIBLE
147
- occupies it** — a checkbox, an ordinal, a diff mark. An empty indent is a defect.
148
- - **The one exception is a self-contained OVERLAY.** A menu or a selector's popover
149
- (`MenuListItem`, `OptionList`) has nothing outside itself to align with, so its rows keep
150
- plain padding. Bleeding there would push rows past the popover's own edge.
151
- - **A slot holding another component's part takes THAT component's constant**, never a
152
- hand-picked number that happens to look close. Import the owner's constant.
153
- - Inside a `Dialog`, every band reads `dialogGutter(small)` — header, scroll area, footer and
154
- any pane a caller drops in. A hand-copied `small ? 16 : 24` is a fifth copy of a number
155
- that has to agree with four others.
156
- → [composition.md](./docs/composition.md).
99
+ a row's content sits on its container's own edge. **An indent is legible only when something
100
+ VISIBLE occupies it**a checkbox, an ordinal, a diff mark. An empty indent is a defect.
101
+ - Inside a `Dialog` that edge is `useDialogGutter()` — no argument, read and never re-derived.
102
+ [composition.md §"Canvas & content column"](./docs/composition.md),
103
+ [composition.md §"No dead rows"](./docs/composition.md).
157
104
  - **Documents get a LIST; images get a GRID.** The discriminator is what identifies the file to
158
- the person looking for it. A PDF, a Word doc and a spreadsheet are identified by their NAME —
159
- their thumbnails are a grey page with grey lines on it, identical to every other grey page —
160
- so a grid of them is a wall of the same tile and the reader has to read a truncated caption
161
- anyway. Use `FileRow` / `FileRows`, where the full name is the row and a second line can
162
- carry a date, a size, a status, a diff. Photos, scans and drawings are identified by their
163
- CONTENT — the filename is `IMG_4471.jpg` and tells nobody anything — so use
164
- `FileThumbnailGrid`, where the picture is the identity and many fit on one screen. **A mixed
165
- set follows its MAJORITY, and that one surface holds the whole set — never split a pile by
166
- file type** — the cost lands on the minority by design, not on the reader, who would otherwise
167
- get two places to look for one thing.
168
- → [catalog.md](./docs/catalog.md).
105
+ the person looking for it. Use `FileRow` / `FileRows`, where the full name is the row. Photos,
106
+ scans and drawings are identified by their CONTENT so use `FileThumbnailGrid`, where the
107
+ picture is the identity. **A mixed set follows its MAJORITY, and that one surface holds the
108
+ whole set never split a pile by file type.**
109
+ [catalog.md §"Files"](./docs/catalog.md).
169
110
  - **A reference to another record is a FIELD** — it wears the inline editor's own resting
170
111
  surface and PEEKS its facts on press; EVERY verb lives in the peek's footer as a `Button`,
171
- none on the field LEFT the link (Change and Clear, both standard), RIGHT the record (Edit's
172
- draft, and the optional Open). Change is `secondary`, never `danger`: it is a correction. After
173
- Change hand the picker focus, after Clear never. Never its own
174
- Section, never a card, never the other record's fields as rows on this one. **A peek can
175
- hold editors only as a DRAFT** — `Edit` → inputs → `Save` (which sends only what CHANGED),
176
- with the open draft pinning the popover so a stray click cannot discard typing; a
177
- self-persisting inline editor may never go in one, because there blur and dismiss are the
178
- same gesture.
179
- → [composition.md](./docs/composition.md), [catalog.md](./docs/catalog.md).
112
+ none on the field. Never its own Section, never a card, never the other record's fields as
113
+ rows on this one.
114
+ [composition.md §"Field annotations & linked records"](./docs/composition.md), [catalog.md](./docs/catalog.md).
180
115
  - **A verb about a VALUE goes on the value** — an `InlineButton` in the field's `actions`
181
- (Copy, Open) or a **`CopyButton`** in a register cell, never the row's `trailing`: a control at
182
- the page's right edge has to be paired back to the field it belongs to, and one row's action
183
- makes EVERY row reserve the column. → [composition.md](./docs/composition.md).
116
+ (Copy, Open) or a **`CopyButton`** in a register cell, never the row's `trailing`.
117
+ [composition.md §"Field annotations & linked records"](./docs/composition.md).
184
118
  - **A register row's text is SELECTABLE and a drag is not a press** — the row is a button wrapped
185
119
  around DATA, so dragging across a name and copying it works, and the release that ended the
186
120
  selection does not also open the record. Reach for `CopyButton` only where ONE value is copied
187
- often enough to earn a control. → [composition.md](./docs/composition.md).
121
+ often enough to earn a control.
122
+ → [composition.md §"Row actions are always-visible siblings"](./docs/composition.md).
188
123
  - **Files land THREE ways on any surface that holds them** — the section's Add CTA
189
124
  (`pickFiles`), and a drag OR paste, both from ONE **`<FileDropTarget … paste>`** wrapping the
190
- region a Files section, OR a WHOLE surface (a record's section stack, a register page), so a
191
- drop/paste anywhere on it routes to that surface's intake (its `paste` scopes Ctrl/Cmd+V to the
192
- region's focus; `usePasteFiles` is the bare hook for a region-less modal), all on ONE handler.
193
- Paste routes to the top-most FOCUSED region, else the top of the stack — two peer file sections
194
- each win when focus is in them. Nobody should hunt for a dropzone; a `FileDropzone` well is the
195
- empty state, not the only way in. Since a wrapped region shows no zone, name the paths with a
196
- muted heading description ("Drag, paste, or click to add files"). → [data_entry.md](./docs/data_entry.md).
125
+ region. Nobody should hunt for a dropzone; a `FileDropzone` well is the empty state, not the
126
+ only way in.
127
+ [data_entry.md §"Intake files land THREE ways"](./docs/data_entry.md).
197
128
  - **Every state designed** — skeleton (mirroring layout), empty, error; no layout shift.
129
+ → [catalog.md §"Badges, status & feedback"](./docs/catalog.md).
198
130
  - **Copy states content, not mechanics** — a `description` names the section's content or one
199
131
  domain rule; widget behavior ("edits in place, saves on its own") and selling points never
200
- appear. Instructions live in empty states / form-field hints. → [composition.md §Microcopy](./docs/composition.md).
132
+ appear. Instructions live in empty states / form-field hints.
133
+ → [composition.md §"Microcopy"](./docs/composition.md).
201
134
 
202
135
  ## Keeping this reference current
203
136
 
package/MIGRATION.md CHANGED
@@ -4,6 +4,33 @@ 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
+ ## 46.0.0
8
+
9
+ **`AgentRun` / `AgentProgress` drop `stepsLabel`, and the `agentRun` locale slice drops `steps`.**
10
+ The run row no longer prints a step count — not on the folded run row, not on a settled
11
+ `ToolGroup` header. The tally was a number a reader could act on in no way: while the run is live
12
+ the label already changes on every call, so it added no motion the row did not have, and once
13
+ settled it sized work nobody had asked to size. The chevron carries "there is more inside".
14
+
15
+ Delete the prop and the pack key; both are now type errors.
16
+
17
+ ```diff
18
+ -<AgentRun parts={parts} stepsLabel={(n) => plural(n, { one: "# step", other: "# steps" })} />
19
+ +<AgentRun parts={parts} />
20
+ ```
21
+
22
+ ```diff
23
+ agentRun: {
24
+ tools: { … },
25
+ - steps: (n) => `${n} bước`,
26
+ },
27
+ ```
28
+
29
+ A row's accessible name is now the label alone (`"Searching records"`), where it was
30
+ `"Searching records — 2 steps"` — update any test that queried it by the old string. Two rows can
31
+ now share a name once the work is expanded (the run row and the step it was named for), so prefer
32
+ `getAllByLabelText(...)[0]` over `getByLabelText` there.
33
+
7
34
  ## 45.9.0
8
35
 
9
36
  **`BrandMark` no longer carries product logos, and `SwitchButton` takes `leading` instead of