@lotics/ui 43.3.0 → 43.3.2
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 +4 -9
- package/docs/ai_patterns.md +11 -18
- package/docs/catalog.md +14 -25
- package/docs/composition.md +14 -16
- package/docs/data_entry.md +6 -22
- package/docs/templates.md +11 -29
- package/package.json +1 -1
- package/src/detail_row.tsx +10 -1
package/AGENTS.md
CHANGED
|
@@ -16,7 +16,7 @@ CURRENT major only — upgrading an app across majors is `MIGRATION.md`.
|
|
|
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
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 (`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 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)
|
|
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), `useChangeSet` (accept/reject/undo bookkeeping, no layout) — see [MIGRATION.md](./MIGRATION.md) for the `ChangeReview` family they replace |
|
|
20
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 (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), color discipline, typography, whitespace, and how to TEST an overlay component (a `Popover`-backed surface never mounts under jsdom). |
|
|
21
21
|
| [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. |
|
|
22
22
|
| [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. |
|
|
@@ -55,8 +55,7 @@ CURRENT major only — upgrading an app across majors is `MIGRATION.md`.
|
|
|
55
55
|
order, where a line claims an order the work does not have. **`mark`** asks *does ticking mean
|
|
56
56
|
DONE or CHOSEN* — a ring for completion, the square box for picking (which forms to produce,
|
|
57
57
|
which records to export). Spend the ring on a choice and a reader who learned that a filled
|
|
58
|
-
ring means "done" reads a ticked pick-list as work already carried out.
|
|
59
|
-
task/pipeline families that had converged on one anatomy. A GROUP is a phase — a name, optionally a
|
|
58
|
+
ring means "done" reads a ticked pick-list as work already carried out. A GROUP is a phase — a name, optionally a
|
|
60
59
|
Show/Hide disclosure, and no body: it owns no COMPLETION, and a ring that never responds invites
|
|
61
60
|
a press it refuses. Rows tick ONE at a time; derive position from the highest stamp and one click marks
|
|
62
61
|
everything below it without inventing dates. A checklist REPORTS — a field owned by a section is
|
|
@@ -101,17 +100,13 @@ CURRENT major only — upgrading an app across majors is `MIGRATION.md`.
|
|
|
101
100
|
padding. `PressableRow` (all variants), `Table`, `ChoiceList`, `ListItem` and `FileRow`
|
|
102
101
|
already do this, so a screen composed from the kit aligns with **no per-site padding at all**.
|
|
103
102
|
The moment one component insets instead, every neighbour has to be hand-padded by the same
|
|
104
|
-
amount to catch up, and that compensation spreads to each new surface built beside it
|
|
105
|
-
is how a `SummaryLine`, a `Clarify` question and a bare table's footer each acquired a
|
|
106
|
-
padding they should never have needed. **An indent is legible only when something VISIBLE
|
|
103
|
+
amount to catch up, and that compensation spreads to each new surface built beside it. **An indent is legible only when something VISIBLE
|
|
107
104
|
occupies it** — a checkbox, an ordinal, a diff mark. An empty indent is a defect.
|
|
108
105
|
- **The one exception is a self-contained OVERLAY.** A menu or a selector's popover
|
|
109
106
|
(`MenuListItem`, `OptionList`) has nothing outside itself to align with, so its rows keep
|
|
110
107
|
plain padding. Bleeding there would push rows past the popover's own edge.
|
|
111
108
|
- **A slot holding another component's part takes THAT component's constant**, never a
|
|
112
|
-
hand-picked number that happens to look close
|
|
113
|
-
`Marker` it holds is `NODE` (24), so a 24px dot centred in an 18px column put the whole run
|
|
114
|
-
rail 3px left of every other band in the dialog. Import the owner's constant.
|
|
109
|
+
hand-picked number that happens to look close. Import the owner's constant.
|
|
115
110
|
- Inside a `Dialog`, every band reads `dialogGutter(small)` — header, scroll area, footer and
|
|
116
111
|
any pane a caller drops in. A hand-copied `small ? 16 : 24` is a fifth copy of a number
|
|
117
112
|
that has to agree with four others.
|
package/docs/ai_patterns.md
CHANGED
|
@@ -124,8 +124,7 @@ transition — never leave a live button waiting on a network round-trip:
|
|
|
124
124
|
|
|
125
125
|
- **Same-tick feedback.** On press, synchronously close the dialog / swap to the run surface
|
|
126
126
|
(`AgentRun` with its streaming state) so there is nothing left to click. A button that stays
|
|
127
|
-
pressable while an attach/start request round-trips invites double-fires
|
|
128
|
-
production as stacked paid runs.
|
|
127
|
+
pressable while an attach/start request round-trips invites double-fires.
|
|
129
128
|
- **Single-flight in app code too.** Guard the handler with a ref (state alone races the
|
|
130
129
|
re-render): first press wins, later presses no-op until the flow settles.
|
|
131
130
|
- **The SDK backstops runs, not workflows.** `useAgentRun().run()` is single-flight by default
|
|
@@ -242,14 +241,10 @@ feed**; the starting row IS the placeholder, consistent and localized.
|
|
|
242
241
|
(Sonnet / Opus, not Haiku); the renderer shows every reasoning part it's handed.
|
|
243
242
|
|
|
244
243
|
**Localization.** The feed's fixed chrome — the "Thinking" label, the "Input"/"Output"/"Error"
|
|
245
|
-
panel titles, the "awaiting" annotation,
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
contradicting itself. The "{n} steps" suffix is locale-backed too (`agentRun.steps`). `labelForCall`
|
|
250
|
-
and `stepsLabel` remain as per-call-site OVERRIDES — for phrasing a row by what
|
|
251
|
-
it targets — not as the only route to a translation. A tool name in neither the locale map nor
|
|
252
|
-
`labelForCall` falls back to its prettified form.
|
|
244
|
+
panel titles, the "awaiting" annotation, the "Retry" action, the tool labels (`agentRun.tools`,
|
|
245
|
+
keyed by raw tool name) and the "{n} steps" suffix (`agentRun.steps`) — resolves through the
|
|
246
|
+
`LoticsLocale` `agentRun` slice (translate once at the provider; a `vi` app gets them for free);
|
|
247
|
+
`labelForCall` and `stepsLabel` remain as per-call-site OVERRIDES.
|
|
253
248
|
|
|
254
249
|
**Answering an awaiting call — `ApprovalPrompt`.** The `awaiting` row inside `AgentRun` is
|
|
255
250
|
READ-ONLY — it only NAMES that a call is parked on a human decision. The surface that ANSWERS it
|
|
@@ -466,8 +461,7 @@ rather than wherever the feed was scrolled to.
|
|
|
466
461
|
holds the wizard's verbs while a question is up, **Stop** while the run streams, and nothing once
|
|
467
462
|
it settles; the RUN decides, which is the whole point of handing it the run. A host that names a
|
|
468
463
|
single phase out here duplicates that decision and gets it wrong the moment the bar learns a new
|
|
469
|
-
one
|
|
470
|
-
stop.
|
|
464
|
+
one.
|
|
471
465
|
|
|
472
466
|
**`run` is a SHAPE, not an import** (`AgentRunLike`: `status`, `parts`, `pendingChoice`,
|
|
473
467
|
`answerChoice`, `cancel`, `error`; `AgentRunQuestion` names what `pendingChoice` carries, so a template or
|
|
@@ -610,8 +604,8 @@ ranked set, don't cram it into chat prose:
|
|
|
610
604
|
document-driven records) — the record's files block feeds ONE "Use AI" entry that FORKS into
|
|
611
605
|
the two document tasks, each a specialized run with a task-pure result: **Extract** (files
|
|
612
606
|
read → fields already matching fold into one quiet line → every add / update / conflict a
|
|
613
|
-
|
|
614
|
-
a first-class choice — plus proposed new lines as record-body `Change`s →
|
|
607
|
+
`DiffValue` per field (the struck current value, the proposed one, candidate rows), the record's current value
|
|
608
|
+
a first-class choice — plus proposed new lines as record-body `Change`s →
|
|
615
609
|
an explicit outcome-named commit) and **Cross-check** (documents compared against the record and
|
|
616
610
|
each other → ranked `Finding`s — severity, title, the prominent metric, sources — separated
|
|
617
611
|
by hairlines; the findings ARE the outcome the human acts on). The fork carries an OPTIONAL
|
|
@@ -642,12 +636,11 @@ ranked set, don't cram it into chat prose:
|
|
|
642
636
|
## The visual vocabulary — functional colour, no gimmicks
|
|
643
637
|
|
|
644
638
|
The AI vocabulary has **no purple accent and no gimmick glyphs** (no sparkles) — but it is NOT
|
|
645
|
-
monochrome: **colour is used where it carries meaning, not for decoration.**
|
|
646
|
-
sparkle used to carry now reads structurally — **provenance** is a quiet sentence-case microlabel
|
|
639
|
+
monochrome: **colour is used where it carries meaning, not for decoration.** **Provenance** is a quiet sentence-case microlabel
|
|
647
640
|
naming the artifact ("Proposed", "Match", "Mismatch", "Suggested edit", "Question" — xs,
|
|
648
|
-
muted, medium; all-caps is banned kit-wide),
|
|
641
|
+
muted, medium; all-caps is banned kit-wide), the agent's **reasoning** is a left-ruled margin
|
|
649
642
|
note (a hairline rule + muted text — `ChangeReasoning`), quoted apart from the facts and the
|
|
650
|
-
human's controls
|
|
643
|
+
human's controls, and **status/severity/diffs use functional colour** the way the rest of the
|
|
651
644
|
kit does:
|
|
652
645
|
|
|
653
646
|
- **Confidence** — the 3-tick meter + the full phrase, emerald / amber / zinc by level.
|
package/docs/catalog.md
CHANGED
|
@@ -80,8 +80,7 @@ renders these per option.)
|
|
|
80
80
|
|
|
81
81
|
**Size is a RUNG, never a pixel count** — `sm | md | lg | xl` = 24 / 28 / 40 / 72
|
|
82
82
|
(`@lotics/ui/avatar_size`, shared by `Avatar`, `MemberChip` and `GroupAvatar`). Default `md`.
|
|
83
|
-
|
|
84
|
-
the product and its apps, so one person looked different on every screen. Pick by density:
|
|
83
|
+
Pick by density:
|
|
85
84
|
`sm` for a dense row or inline cell, `md` for a control band, register or list (the default),
|
|
86
85
|
`lg` for a prominent row or the account menu, `xl` for a profile's own header.
|
|
87
86
|
|
|
@@ -89,9 +88,7 @@ the product and its apps, so one person looked different on every screen. Pick b
|
|
|
89
88
|
avatar is IN a control or IS one.** `md` is `CONTROL_CONTENT_HEIGHT` (28) — the largest rung a
|
|
90
89
|
40px control band seats without growing, and the height `InlineButton` and a `md` `IconButton`
|
|
91
90
|
already used to sit inside a field. An avatar is the tallest thing in whatever row holds it, so
|
|
92
|
-
a rung that overshoots doesn't look slightly big, it RESIZES the control
|
|
93
|
-
member chip measured 46 in a `Select` trigger, 50 in an inline editor and 54 in an `InlineSlot`,
|
|
94
|
-
because each surface had picked its own inset with no number to pick it from. Put nothing taller
|
|
91
|
+
a rung that overshoots doesn't look slightly big, it RESIZES the control. Put nothing taller
|
|
95
92
|
than `md` in a control. `lg` is `CONTROL_HEIGHT` (40) — the avatar that stands in a band on its
|
|
96
93
|
own, an account menu beside a row of icon buttons, so it measures what they do. Anything bigger
|
|
97
94
|
belongs in a row that sets its own height.
|
|
@@ -155,8 +152,7 @@ axes carry every shape it takes, and BOTH are decisions about meaning, not looks
|
|
|
155
152
|
work already carried out, and nothing on screen corrects them. (The shapes are 24px either way
|
|
156
153
|
and share one gutter, so a surface swaps between them without moving anything.)
|
|
157
154
|
|
|
158
|
-
A record's Progress and a desk's to-do list are the same component two booleans apart
|
|
159
|
-
two families until the anatomy proved identical.
|
|
155
|
+
A record's Progress and a desk's to-do list are the same component two booleans apart.
|
|
160
156
|
|
|
161
157
|
**A row carries its TITLE and its own stamp, and nothing else.** A checklist REPORTS: it does not
|
|
162
158
|
collect, and it does not PREVIEW. A field some section owns — an assignee, an address, a portal
|
|
@@ -1068,7 +1064,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1068
1064
|
`DetailTable`: the compound parent of
|
|
1069
1065
|
a row STACK — `labelWidth` (default `DETAIL_LABEL_WIDTH`, 130 — THE kit's label column, and
|
|
1070
1066
|
the record surface's) / `minHeight` (default 40, the inline-control grid) declared
|
|
1071
|
-
ONCE + the `SPACE.md` (16) row gap —
|
|
1067
|
+
ONCE + the `SPACE.md` (16) row gap — a stack of
|
|
1072
1068
|
BORDERED rows that close together fuses into one block. TWO columns, no third: the value
|
|
1073
1069
|
FILLS what the label leaves, so nothing one row does can narrow its neighbours' editors. RESPONSIVE with no prop: it
|
|
1074
1070
|
measures its own container (onLayout, not
|
|
@@ -1078,7 +1074,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1078
1074
|
grammar); raise `minValueWidth` (default 160) when a cell holds
|
|
1079
1075
|
MORE than one editor so the table stacks earlier. Two tables on one page share one grid by
|
|
1080
1076
|
repeating the same labelWidth. Worked example:
|
|
1081
|
-
[`tpl_record`](../examples/tpl_record.tsx).
|
|
1077
|
+
[`tpl_record`](../examples/tpl_record.tsx).
|
|
1082
1078
|
- **`record_summary`** — `RecordSummary`: the identity band of a record detail/drawer — ONE
|
|
1083
1079
|
row: `title` xxl semibold tabular, `subtitle` sm muted, `status` Badge slot, optional
|
|
1084
1080
|
`metric` {label,value,tone,note} pinned right, the band's ONE accent. The record's FIELDS
|
|
@@ -1175,8 +1171,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1175
1171
|
and Save writes the pre-action values back over what it just fetched.
|
|
1176
1172
|
The peek uses the popover's OWN anatomy: `PopoverHeader` for the identity, a scrolling body,
|
|
1177
1173
|
and a **pinned `PopoverFooter`** for the verbs. A long block therefore scrolls with its actions
|
|
1178
|
-
still reachable
|
|
1179
|
-
own Save.
|
|
1174
|
+
still reachable.
|
|
1180
1175
|
`Edit` swaps the SAME grid's value cells for inputs — a DRAFT, so nothing commits until `Save`,
|
|
1181
1176
|
which fires `onSave` with **only the facts that CHANGED** (never a snapshot, so a lock or
|
|
1182
1177
|
`before_update` sees the real edit). This is what lets a peek hold editors at all: a
|
|
@@ -1365,7 +1360,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1365
1360
|
never `style`**: `style` reaches only the BODY, and the panel is `max-content`, so a width set
|
|
1366
1361
|
there governs the scroller while the header and footer sit outside it — the first long title
|
|
1367
1362
|
then stretches the panel and leaves the content adrift in a box wider than itself. A popover
|
|
1368
|
-
with no bands never sees this
|
|
1363
|
+
with no bands never sees this.
|
|
1369
1364
|
The built-in scroller is **full-bleed horizontally** and re-insets its content by the same
|
|
1370
1365
|
amount, so a scrolling body reaches the panel's real edges — its content slides under the
|
|
1371
1366
|
panel edge rather than under an invisible margin 12px inside it — while the text stays on the
|
|
@@ -1437,8 +1432,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1437
1432
|
`PieChart`: the canonical SVG chart set (no recharts).
|
|
1438
1433
|
- **`progress_bar`** — `ProgressBar`: the determinate meter; `compact` = ONE row, track + a
|
|
1439
1434
|
plain sm tabular count beside it.
|
|
1440
|
-
- **`progress_ring`** — `ProgressRing`: the same meter in a circle, and the ONLY circular one
|
|
1441
|
-
(it replaced `RingGauge`). Same API shape as the bar — a real `value`/`max` rather than a
|
|
1435
|
+
- **`progress_ring`** — `ProgressRing`: the same meter in a circle, and the ONLY circular one. Same API shape as the bar — a real `value`/`max` rather than a
|
|
1442
1436
|
pre-computed percentage, optional `label`/`caption`, and a `format` that turns the centre
|
|
1443
1437
|
figure off. `format="none"` with no label leaves the arc bare at any size: that is the
|
|
1444
1438
|
TRIGGER form, for a control row where a figure would be unreadable and a bar would not
|
|
@@ -1505,10 +1499,9 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1505
1499
|
the first-line box and the chevron-pairing problems in the first place.
|
|
1506
1500
|
- **`checklist`** — `Checklist` + `ChecklistGroup` + `ChecklistItem` + `ChecklistNote` /
|
|
1507
1501
|
`ChecklistActions` / `ChecklistField`: THE run of tickable rows, at any
|
|
1508
|
-
altitude
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
line, and it is the whole difference**: on for ONE record walking positions (it asserts
|
|
1502
|
+
altitude — a ring, a title, a value beside it, a muted note, a detail block, an optional
|
|
1503
|
+
grouping level. **`connected` is the LINE joining the rings, and it is the whole
|
|
1504
|
+
difference**: on for ONE record walking positions (it asserts
|
|
1512
1505
|
sequence), off for a SET picked in any order, where a line claims an order the work does not
|
|
1513
1506
|
have. Rows are ticked ONE AT A TIME; where the run is a sequence, derive position from the
|
|
1514
1507
|
highest stamp so one click marks everything below it — a parent that ticked its children wrote
|
|
@@ -1662,10 +1655,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1662
1655
|
NODE** as well as a string, because a second line can itself be carrying a change (a file
|
|
1663
1656
|
being RECLASSIFIED is the same bytes filed differently — a `DiffValue` on the property); a
|
|
1664
1657
|
node renders outside the muted single-line `Text`, which would clip it and fight its colour.
|
|
1665
|
-
Both `sm` variants share ONE beat (48px min + 6px vertical padding)
|
|
1666
|
-
have neither, so it came out 37px against the pressable's 49 — whether a row was a door
|
|
1667
|
-
decided how tall it was — and 37 sat under every other row beat in the kit (`DetailRow` 40,
|
|
1668
|
-
`TableRow` 52, this component's own `md` 56), which is what "squeezed" literally was.
|
|
1658
|
+
Both `sm` variants share ONE beat (48px min + 6px vertical padding).
|
|
1669
1659
|
- **`file_rows`** — `FileRows`: batteries-included file list — row press → built-in gallery
|
|
1670
1660
|
+ a ⋯ Download/Open-external/Remove menu; composes `FileRow` + `ActionMenu` +
|
|
1671
1661
|
`FileGalleryModal`; localized via the `gallery` locale slice. **`meta` has NO default**: the
|
|
@@ -1838,9 +1828,8 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1838
1828
|
`added`/`changed`/`removed`/`unchanged`, for the cases no single value carries. Glyph + an
|
|
1839
1829
|
accessible word (never colour alone — "added" vs "removed" by hue is invisible to a reader who
|
|
1840
1830
|
cannot separate them, and that is the distinction a review turns on). **ONE treatment
|
|
1841
|
-
everywhere: a filled 22px DISC**, 14px glyph, 700 ink on a 100 ground.
|
|
1842
|
-
|
|
1843
|
-
glyphs beside a table marked with filled shapes read as two systems saying the same thing; a
|
|
1831
|
+
everywhere: a filled 22px DISC**, 14px glyph, 700 ink on a 100 ground. A form marked with bare
|
|
1832
|
+
glyphs beside a table marked with filled shapes reads as two systems saying the same thing; a
|
|
1844
1833
|
reader parses shape before meaning, so an unexplained difference reads as an accident however
|
|
1845
1834
|
principled the reason. The disc also gives the glyph its own ground, which is what lets one
|
|
1846
1835
|
mark sit on a white row, a tinted cell, or a photograph. **`unchanged` renders as an empty
|
package/docs/composition.md
CHANGED
|
@@ -380,12 +380,7 @@ holds another component's PART takes that component's constant, never a hand-pic
|
|
|
380
380
|
centred in an 18px column put the whole run rail 3px outside the surface's edge — every row
|
|
381
381
|
wrong by the same amount, so it read as internally consistent and nothing flagged it.
|
|
382
382
|
|
|
383
|
-
|
|
384
|
-
the 20px — it was that everything nearby had to be hand-padded by the same 20 to
|
|
385
|
-
line up with it. A `Table`'s header band, its static rows and its expanded detail
|
|
386
|
-
all inherited the number; a `SummaryLine` above the register got it at the call
|
|
387
|
-
site; a bare table's footer borrowed `CardFooter` and picked up a *card's*
|
|
388
|
-
interior gutter. **An indent is legible only when something VISIBLE occupies it**
|
|
383
|
+
**An indent is legible only when something VISIBLE occupies it**
|
|
389
384
|
— a checkbox, an ordinal, a diff mark. An empty one is a defect, and the
|
|
390
385
|
compensation it forces spreads to every new surface that sits beside it.
|
|
391
386
|
|
|
@@ -425,8 +420,7 @@ content off the register entirely.
|
|
|
425
420
|
ornament sits beside a text block and centres on it — a `Timeline`'s disc, a marker, an avatar,
|
|
426
421
|
a rail glyph — the ornament's position is a function of the text's height, so an unbounded text
|
|
427
422
|
block moves it. A feed is where this actually bites, because a feed is the one row primitive
|
|
428
|
-
whose text is written by the USER
|
|
429
|
-
drew a 180px label and put the disc 80px below the line it names. Bound the text; the row keeps
|
|
423
|
+
whose text is written by the USER. Bound the text; the row keeps
|
|
430
424
|
its beat and the ornament keeps its line. Where the full value still has to be readable, the row
|
|
431
425
|
EXPANDS to it — a truncation and its source, not a second copy.
|
|
432
426
|
|
|
@@ -530,9 +524,7 @@ non-interactive by construction. `TableRow` already composes `PressDoor`.
|
|
|
530
524
|
|
|
531
525
|
**A trailing slot is not a PEER of the row's own affordance — space them apart.** A disclosure
|
|
532
526
|
chevron belongs to the row's button; the slot beside it does not, and if the two are spaced like
|
|
533
|
-
siblings the eye reads one cluster.
|
|
534
|
-
and 12px from the chevron to the action menu, so the only thing saying they were different
|
|
535
|
-
targets was a 4px break in the hover wash — invisible to a reader who never hovers. Put clearly
|
|
527
|
+
siblings the eye reads one cluster. Put clearly
|
|
536
528
|
more space BETWEEN the groups than inside one (2:1 is enough) and the pair separates without a
|
|
537
529
|
divider, a weight change or any new element. It is the ordinary grouping rule — one uniform gap
|
|
538
530
|
makes every child a peer — applied to a row's right edge, where it is easy to miss because both
|
|
@@ -550,7 +542,7 @@ Actions stay **visible** — a hover-only action is invisible to keyboard and to
|
|
|
550
542
|
exception is a DENSE tree/register where a persistent per-row action would clutter: reveal on
|
|
551
543
|
hover **OR focus-within**, keeping the action in the DOM + tab order and gating only its opacity,
|
|
552
544
|
so it's still keyboard-reachable. Do that locally — there is deliberately no shared hover-reveal
|
|
553
|
-
primitive
|
|
545
|
+
primitive.
|
|
554
546
|
|
|
555
547
|
**A row is a button wrapped around DATA, so its text stays SELECTABLE and a drag is not a press.**
|
|
556
548
|
The surface carries no blanket `userSelect: "none"` — that made a customer's own name and number
|
|
@@ -892,6 +884,15 @@ gate's SCOPE, once** — never prose beside the button, never revealed only on p
|
|
|
892
884
|
the row is filled. In a label-left grid, ONE FIELD PER ROW is not a failure to compact; it is
|
|
893
885
|
the only shape that keeps the column a reader scans. Both attempts are recorded here so nobody
|
|
894
886
|
re-runs them.
|
|
887
|
+
**A spread `DetailRow` gives its VALUE the squeeze, never its label.** The two share one flex
|
|
888
|
+
row, so whichever of them cannot shrink forces the other to. A flex child defaults to
|
|
889
|
+
`min-width: auto`, which floors the value at its longest word — and the label, having no floor
|
|
890
|
+
of its own, absorbs everything left: on a narrow register a two-word label collapsed to a 4px
|
|
891
|
+
column 200px tall, one character per line. The `minWidth: 0` on the value is what stops it, and
|
|
892
|
+
it is load-bearing rather than housekeeping. **The probe generalises past this component**:
|
|
893
|
+
wherever a fixed-ish label shares a row with a `flex: 1` value, measure the LABEL's width and
|
|
894
|
+
height across rows whose values differ in length — a label whose box changes shape from row to
|
|
895
|
+
row is the signature, and it is invisible at any width where every value happens to fit.
|
|
895
896
|
The other caveat: that research studies COMPLETION forms, where the failure mode is a skipped
|
|
896
897
|
required field, while a record page is read-mostly.
|
|
897
898
|
A SUMMARY belongs to the RECORD, not to a section. The highlights-panel precedent (a strip of
|
|
@@ -1033,10 +1034,7 @@ When it IS a status, match weight to prominence — lightest → heaviest:
|
|
|
1033
1034
|
a surface (a drawer/detail header). Opt IN explicitly and sparingly; one per view, never
|
|
1034
1035
|
sprinkled through rows.
|
|
1035
1036
|
|
|
1036
|
-
**`tonal` IS allowed on a register's ONE primary status column
|
|
1037
|
-
paragraph used to get wrong.** It read "a register's dense rows read lighter, the row's primary
|
|
1038
|
-
status is `dot`" while `badge.tsx` named "a register's primary Status column" as a legitimate
|
|
1039
|
-
tonal use — two docs, opposite answers, so the choice fell to whoever read which. What "one per
|
|
1037
|
+
**`tonal` IS allowed on a register's ONE primary status column.** What "one per
|
|
1040
1038
|
view" governs is the number of badge KINDS on a surface, not the number of rows: badging the
|
|
1041
1039
|
stage AND the type AND the city is the clutter the rule exists to stop; one status column that
|
|
1042
1040
|
happens to render 133 times is one badge.
|
package/docs/data_entry.md
CHANGED
|
@@ -86,11 +86,7 @@ whole point), and it commits on blur (Enter saves, Escape reverts) or via `contr
|
|
|
86
86
|
|
|
87
87
|
**`numberOfLines` declares what ENTER means — wrapping does not.** A field left at one line holds a
|
|
88
88
|
single-line value that may WRAP when it runs long (a title, a summary), and Enter finishes it.
|
|
89
|
-
Declaring 2+ lines declares PROSE: Enter is a paragraph break and blur is what saves.
|
|
90
|
-
to be one flag, so `autoGrow` — a prop about fitting the value — silently turned Enter into a
|
|
91
|
-
newline, and a one-line summary field that grew became a field you could only leave by clicking
|
|
92
|
-
away. Nothing at the call site said so, which is what made it worth separating rather than
|
|
93
|
-
documenting.
|
|
89
|
+
Declaring 2+ lines declares PROSE: Enter is a paragraph break and blur is what saves.
|
|
94
90
|
|
|
95
91
|
**How "nothing moves" is achieved differs by editor, and the difference is a rule.** A field whose
|
|
96
92
|
resting value is the RAW string (`InlineTextInput`) is ONE `<input>` in both states — it never
|
|
@@ -225,16 +221,12 @@ something belongs elsewhere.
|
|
|
225
221
|
|
|
226
222
|
It is wrong wherever the length is the AUTHOR's choice, and it fails in the worst available way.
|
|
227
223
|
The box is drawn at the budget and the value simply exceeds it: no ellipsis, no clamp, no
|
|
228
|
-
scrollbar, nothing to scroll.
|
|
229
|
-
value** — two full lines gone with nothing on screen admitting it. A truncation mark would have
|
|
230
|
-
been kinder; this is silent. So the test is not "is this field long" but **"who decides the
|
|
224
|
+
scrollbar, nothing to scroll — a truncation mark would have been kinder; this is silent. So the test is not "is this field long" but **"who decides the
|
|
231
225
|
length"** — the field, or whoever is typing.
|
|
232
226
|
|
|
233
|
-
**And it gets worse as the surface narrows
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
wrapped it into more lines against the same three-line box. A fixed reserve verified on a wide
|
|
237
|
-
screen is not verified.
|
|
227
|
+
**And it gets worse as the surface narrows**: a budget is a count of LINES and the wrap point
|
|
228
|
+
moves, so the narrower column wraps the same value into more hidden lines. A fixed reserve
|
|
229
|
+
verified on a wide screen is not verified.
|
|
238
230
|
|
|
239
231
|
`autoGrow` makes the budget a minimum and fits the value. It costs nothing the fixed reserve was
|
|
240
232
|
protecting, and the reason is worth stating because it is easy to assume otherwise: an
|
|
@@ -490,8 +482,7 @@ closes the list as its add-affordance.
|
|
|
490
482
|
|
|
491
483
|
**`connected` decides which shape you are building.** ON for one record walking positions — the
|
|
492
484
|
line asserts sequence, which is what a desk handoff IS. OFF for N items ticked in any order, where
|
|
493
|
-
a line claims an order the work does not have. It is the same compound either way
|
|
494
|
-
families until the anatomy turned out to be identical.
|
|
485
|
+
a line claims an order the work does not have. It is the same compound either way.
|
|
495
486
|
|
|
496
487
|
**A GROUP is a name and nothing else.** No control: a phase owns no completion of its own, so a
|
|
497
488
|
ring there reports without responding — and one identical to the pressable rings beneath it
|
|
@@ -660,13 +651,6 @@ A record field whose value is documents. `<DetailRow label="Scan"><InlineFiles
|
|
|
660
651
|
— a `FileRows` list (press a row to preview it full-screen, ⋯ to remove) with one CTA under it,
|
|
661
652
|
sitting on the inline-control grid via `InlineSlot`.
|
|
662
653
|
|
|
663
|
-
It exists because every other field type shipped BOTH halves — `Select`/`InlineSelect`,
|
|
664
|
-
`TextInputField`/`InlineTextInput` — and files shipped only the section-scale one. So a record
|
|
665
|
-
field holding documents got hand-rolled per app as `FileRows` + `pickFiles` + a `Button`, and
|
|
666
|
-
each copy re-derived the same decisions. The one they missed is `InlineSlot`: raw children in a
|
|
667
|
-
value column start at the cell edge and 20px tall, beside editors that are 40px with a 9px text
|
|
668
|
-
inset, so the column acquires two left edges and two baselines and stops reading as a column.
|
|
669
|
-
|
|
670
654
|
**Cardinality is a property of the FIELD, and it changes the verb.**
|
|
671
655
|
|
|
672
656
|
| `multiple` | picker | CTA once something is attached | the host's `onAdd` must |
|
package/docs/templates.md
CHANGED
|
@@ -235,15 +235,12 @@ screens — register, per-row action, gated selection, and act-on-many in one. T
|
|
|
235
235
|
ANCHORED to its bar button rather than a full-page `Modal` that would escape the drawer.
|
|
236
236
|
**The discussion follows the same measure**: docked beside the record where there is room for
|
|
237
237
|
its column, and a SECTION under General where there is not — which a drawer always is. It is
|
|
238
|
-
never behind a control.
|
|
239
|
-
body-SWAP inside a drawer; both made the reader know a control existed, press it, and give up
|
|
240
|
-
the record to read a note, which is where an unread note goes to be missed.
|
|
238
|
+
never behind a control.
|
|
241
239
|
- **Linked records** still push an editable screen for that record inside the drawer via the
|
|
242
240
|
hosted `ScreenRouter` — while `canGoBack` the drawer header swaps to a back button + the
|
|
243
241
|
pushed id and the ◀ ▶ sequencer hides; back pops with scroll preserved.
|
|
244
|
-
-
|
|
245
|
-
|
|
246
|
-
drawer's money is the record's now). The communications log lives on the record surface as
|
|
242
|
+
- The drawer does NOT write back to its row — the drawer's money is the record's. The
|
|
243
|
+
communications log lives on the record surface as
|
|
247
244
|
[`tpl_record`](../examples/tpl_record.tsx) § Activity.
|
|
248
245
|
- **The "Enter data" INTAKE FORK** (the New CTA) — AI FIRST, the form as fallback, one phased
|
|
249
246
|
dialog. The whole register page is ALSO a `<FileDropTarget … paste>` (the whole-surface intake
|
|
@@ -338,15 +335,11 @@ billing, and quick-capture templates. Top → bottom:
|
|
|
338
335
|
reader has after what it IS, ahead of its files, its route and its money, which are all
|
|
339
336
|
reference. The desk handoff as the ordered positions it is: a `Checklist connected` where the
|
|
340
337
|
desks are `ChecklistGroup`s (Sales → Operations → Accounting) and the milestones they produce
|
|
341
|
-
are the rows.
|
|
342
|
-
a ring that never responds invites the press it refuses) and no body (a condition hung off a
|
|
343
|
-
heading leaves the reader working out which row it was about). The row that OWES the work
|
|
338
|
+
are the rows. The row that OWES the work
|
|
344
339
|
carries all three: how long it has been owed (`meta`), its condition (`ChecklistNote`, e.g.
|
|
345
340
|
past the due date — with an `action` jumping to the section that owns a missing field rather
|
|
346
341
|
than editing it here) and the single act that leaves the desk (`ChecklistActions` → the handoff
|
|
347
|
-
dialog).
|
|
348
|
-
everything below it without writing dates nobody recorded; a rung it passes shows a BLANK date,
|
|
349
|
-
which is the honest answer to "when" and stays one click from being filled. `meta` is prose
|
|
342
|
+
dialog). `meta` is prose
|
|
350
343
|
the reader can't set, never the date that the stage's own field already shows. See § The
|
|
351
344
|
handoff shape for the rules; for a record that genuinely needs a CHECKLIST, the task-list
|
|
352
345
|
grammar is worked in `tpl_item_list`.
|
|
@@ -464,15 +457,12 @@ billing, and quick-capture templates. Top → bottom:
|
|
|
464
457
|
**The GIST is what the feed is scanned by** — what came of the exchange, in the reader's own
|
|
465
458
|
words. Building it the other way round is the natural mistake and it makes the feed unreadable:
|
|
466
459
|
every entry renders as a pair of enum values ("Outbound Meeting"), the substance sits behind a
|
|
467
|
-
chevron, and a column of near-identical labels defeats the scan the feed exists for.
|
|
468
|
-
a real one, that shape spent 504px of chrome on 70px of content.
|
|
460
|
+
chevron, and a column of near-identical labels defeats the scan the feed exists for.
|
|
469
461
|
|
|
470
462
|
**An entry is a COMMENT, not a `Timeline` row.** `Timeline` renders its label inside the row's
|
|
471
463
|
press target, which is right for derived read-only text ("Stage changed to Won") and wrong for
|
|
472
464
|
the one thing on an entry a person WROTE — text inside a button cannot be edited where it sits.
|
|
473
|
-
|
|
474
|
-
un-clamp, the body's second copy of the gist came out, and Edit ended up a scroll from the
|
|
475
|
-
sentence it edits) and none were independent. So the gist is a RESIDENT field and the derived
|
|
465
|
+
So the gist is a RESIDENT field and the derived
|
|
476
466
|
facts sit above it on a muted `xs` **byline** — which way it went, over what, when. Order does
|
|
477
467
|
not decide the scan; weight does. The byline is small and grey, the prose is `sm` on the page's
|
|
478
468
|
own ground, and the eye lands on the substance even though it reads second.
|
|
@@ -604,9 +594,7 @@ billing, and quick-capture templates. Top → bottom:
|
|
|
604
594
|
|
|
605
595
|
**The blocks SHOW; they are not folded behind a disclosure.** They are what the entry IS, and a
|
|
606
596
|
control that hides them makes the reader click to discover what the entry already says is there
|
|
607
|
-
— on every entry, forever.
|
|
608
|
-
bury the next entry", which measurement did not support: an audio player is 54px and a file tile
|
|
609
|
-
row is 96, so hiding one behind a 20px disclosure buys 34px and costs a click. A video is the
|
|
597
|
+
— on every entry, forever. A video is the
|
|
610
598
|
honest counter-case at 358px and it still shows — it is the most informative thing on a recorded
|
|
611
599
|
call, the feed folds its own tail, and the byline and gist stay at the top of every entry, so the
|
|
612
600
|
scan survives a tall one.
|
|
@@ -639,12 +627,8 @@ billing, and quick-capture templates. Top → bottom:
|
|
|
639
627
|
compare. Reveal it whole rather than into a scroll box; a scroller inside a scrolling drawer
|
|
640
628
|
traps the wheel, and a reader who pressed it asked for the length.
|
|
641
629
|
|
|
642
|
-
**The toggle is underlined text, and that is a deliberate exception
|
|
643
|
-
|
|
644
|
-
wash arrives only on hover, so the affordance is hover-only (banned in §"Commit & feedback surfaces") and it
|
|
645
|
-
reads as a stray label between two content blocks. `Accordion` fixes the affordance and
|
|
646
|
-
overcorrects: it is a LIST-ROW primitive, a full-width header with its own chevron, nested
|
|
647
|
-
inside a row that already has one. What is left is a muted underlined `TextLink` in your own
|
|
630
|
+
**The toggle is underlined text, and that is a deliberate exception** (the in-prose disclosure,
|
|
631
|
+
composition.md §"Commit & feedback surfaces"): a muted underlined `TextLink` in your own
|
|
648
632
|
`Pressable` — visible at rest, on the column's own left edge, and carrying no more weight than
|
|
649
633
|
the words. The ink keeps underline honest: blue leaves the surface, muted stays on it.
|
|
650
634
|
|
|
@@ -661,9 +645,7 @@ billing, and quick-capture templates. Top → bottom:
|
|
|
661
645
|
few with a toggle naming how many are behind it — a register pages, a record's feed folds.
|
|
662
646
|
|
|
663
647
|
The toggle is a `TextDisclosure`, the same control as the transcript's, because it is the same
|
|
664
|
-
act: it reveals more of what is already here.
|
|
665
|
-
right and is not — measured, it is transparent, borderless and undecorated at rest, so the
|
|
666
|
-
affordance only arrives on hover and it reads as a stray line under the feed. It sits on the
|
|
648
|
+
act: it reveals more of what is already here. It sits on the
|
|
667
649
|
SECTION's left edge, not the rows': the rows are inset by the disc rail, and at the fold's line
|
|
668
650
|
the rail has ended, so nothing visible occupies that indent. Same for any other fold on the
|
|
669
651
|
surface — a document set's "show the rest" is this control too.
|
package/package.json
CHANGED
package/src/detail_row.tsx
CHANGED
|
@@ -252,7 +252,16 @@ const styles = StyleSheet.create({
|
|
|
252
252
|
// is the one that gives way, because it is the one that is long. Both stay
|
|
253
253
|
// shrinkable on purpose: a long LABEL against a short value has to wrap too,
|
|
254
254
|
// and pinning the label unshrinkable would just overflow the row the other way.
|
|
255
|
-
|
|
255
|
+
// Spread mode's label. It does NOT shrink, and that is the whole rule: both
|
|
256
|
+
// items in this row are floorless under React Native Web (which defaults flex
|
|
257
|
+
// children to `min-width: 0`, unlike the web's `auto`), so with shrink on both
|
|
258
|
+
// the flex algorithm splits the deficit and the LABEL loses — measured on a
|
|
259
|
+
// narrow register, a two-word column name collapsed to a 4px box 200px tall,
|
|
260
|
+
// one character per line, while its neighbour with a short value measured a
|
|
261
|
+
// clean 251x20. A label the reader cannot finish makes the row meaningless,
|
|
262
|
+
// and the value beside it wraps for free (`spreadValue` already carries the
|
|
263
|
+
// `minWidth: 0` that lets it), so the value is the one that gives way.
|
|
264
|
+
flexLabel: { flexGrow: 1, flexShrink: 0, flexBasis: "auto" },
|
|
256
265
|
// `center` is what makes a WRAPPED label align sanely, and it does both jobs
|
|
257
266
|
// with no measurement: a one-line label is shorter than the control band, so
|
|
258
267
|
// it centers on the control line (the alignment law); a wrapped label is as
|