@lotics/ui 44.7.1 → 44.8.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 +11 -1
- package/MIGRATION.md +110 -5
- package/docs/catalog.md +35 -7
- package/docs/composition.md +91 -5
- package/docs/data_entry.md +60 -6
- package/docs/reviewing.md +36 -3
- package/examples/tpl_item_list.tsx +8 -4
- package/package.json +2 -1
- package/src/accordion.tsx +9 -1
- package/src/agent_run_pane.tsx +7 -5
- package/src/card.tsx +7 -1
- package/src/card_select_item.tsx +15 -2
- package/src/control_surface.ts +18 -4
- package/src/data_grid.tsx +9 -1
- package/src/date_range_filter_field.tsx +9 -2
- package/src/date_stamp.tsx +8 -2
- package/src/detail_row.tsx +17 -12
- package/src/empty_state.tsx +9 -2
- package/src/error_state.tsx +9 -2
- package/src/field_annotations.tsx +25 -2
- package/src/finding.tsx +19 -10
- package/src/format_money.ts +16 -2
- package/src/inline_edit.tsx +29 -9
- package/src/inline_select.tsx +8 -1
- package/src/inline_text_input.tsx +6 -7
- package/src/locale.tsx +24 -1
- package/src/number_input.tsx +8 -1
- package/src/reference_field.tsx +9 -2
- package/src/section_heading.tsx +17 -5
- package/src/select.tsx +16 -16
- package/src/sequence.tsx +41 -25
- package/src/step_progress.tsx +6 -4
- package/src/table_fit.ts +18 -8
- package/src/tabs.tsx +19 -0
- package/src/text_input_field.tsx +35 -1
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` + `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) | **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); 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
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
|
-
| [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), **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), **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), **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), **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, **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, 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 — **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), 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), **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), **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), **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), **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, **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, and how to TEST an overlay component (a `Popover`-backed surface never mounts under jsdom). |
|
|
21
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 10 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), 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"). |
|
|
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. |
|
|
@@ -42,6 +42,16 @@ CURRENT major only — upgrading an app across majors is `MIGRATION.md`.
|
|
|
42
42
|
panel a few hundred pixels wide lays its contents out for the whole display. (A component that
|
|
43
43
|
needs a number rather than a bucket — how many columns fit — measures its own box; `Table`
|
|
44
44
|
does exactly that.) → [catalog.md §Utility hooks](./docs/catalog.md).
|
|
45
|
+
- **No user-facing string is a literal in `src/` when a locale slice already names it.** Every
|
|
46
|
+
string a component renders resolves prop → `useLoticsLocale()` slice → the pack's English
|
|
47
|
+
default. Write it as an allowlist, not a hunt: a literal typechecks, renders, and passes a
|
|
48
|
+
render test in the language it happens to be written in — the kit shipped a Vietnamese ⓘ label
|
|
49
|
+
to every tenant that way, invisible to a whole suite of vi-pack assertions. Assert a wired
|
|
50
|
+
string under the OTHER language. → [composition.md §Microcopy](./docs/composition.md).
|
|
51
|
+
- **Annotate every droppable table column, or none.** A partial `priority` is the trap: an
|
|
52
|
+
unannotated column sorts above the whole hand-written range, so one annotation makes every
|
|
53
|
+
other column drop first, and one unannotated column among annotated ones goes first however
|
|
54
|
+
important it is. → [composition.md §The register's own craft](./docs/composition.md).
|
|
45
55
|
- **A section's ADD rides its heading row, right edge** — Add files, Add fee, New line: a
|
|
46
56
|
`primary` `Button` beside `SectionHeadingTitle`, rendered empty or full. Under the rows it
|
|
47
57
|
extends, the verb MOVES with the row count and vanishes off-screen on a long list; a heading is
|
package/MIGRATION.md
CHANGED
|
@@ -4,6 +4,112 @@ 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
|
+
## 44.8.0 — a value column gets its own floor, and eight defaults stop fighting their own docs
|
|
8
|
+
|
|
9
|
+
No API is removed. Three of these change what an existing screen renders, so read them even
|
|
10
|
+
though nothing will fail to compile.
|
|
11
|
+
|
|
12
|
+
**`StepProgress`'s built-in caption is locale-wired, and `LoticsLocale` gains a `stepProgress`
|
|
13
|
+
slice.** A hand-built pack must supply it (both shipped packs already do):
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
stepProgress: {
|
|
17
|
+
complete: (total) => `Hoàn tất (${total}/${total})`,
|
|
18
|
+
stage: (name, index, total) => `${name} (${index}/${total})`,
|
|
19
|
+
none: (total) => `0/${total}`,
|
|
20
|
+
position: (index, total) => `${index} trên ${total}`,
|
|
21
|
+
},
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The caption over named stages was built as `Complete (7/7)` / `0/7` in the component, and the
|
|
25
|
+
screen-reader fallback as `4 of 7` — hardcoded English inside a component the kit requires to
|
|
26
|
+
resolve prop → pack → English default. A Vietnamese board read `Complete (7/7)` in its own
|
|
27
|
+
register. Same defect class as the `fileRow`/`sources`/`counter` leak below, and it hid the same
|
|
28
|
+
way: every state renders correctly under `en`, because English is what the bug produces.
|
|
29
|
+
|
|
30
|
+
Each entry is a FUNCTION rather than a word to concatenate, for the reason the earlier entry
|
|
31
|
+
gives — where the number sits in the sentence is a property of the language, and `position`
|
|
32
|
+
is `4 of 7` against `4 trên 7`. Passing `label` still overrides the caption entirely; nothing
|
|
33
|
+
that already passed one changes.
|
|
34
|
+
|
|
35
|
+
**`Finding` stacks its `readings` one per line and drops the ` · ` between them.** No prop
|
|
36
|
+
changes; a two-reading finding is now two lines instead of one.
|
|
37
|
+
|
|
38
|
+
The separator failed twice over. `docs/reviewing.md` bans ` · ` outright — it claims a
|
|
39
|
+
relationship while refusing to name it, and it is the loudest templated-metadata tell there is —
|
|
40
|
+
so an app reaching for the kit's own ranked-insight component failed the kit's own microcopy
|
|
41
|
+
probe with nothing it could do about that. And a middot is the one mark a screen reader drops,
|
|
42
|
+
so the character carrying "these two disagree" never survived being read aloud: exactly the
|
|
43
|
+
defect `CommentsButton` was fixed for. It wrapped badly too — inline + `flexWrap` could start a
|
|
44
|
+
phone-width line WITH the separator, or split a source from its value across lines.
|
|
45
|
+
|
|
46
|
+
Stacked, each `source value` pair is a row at every width and needs no separator to relate them.
|
|
47
|
+
Pass `readings` shorter values, or move a long one to `detail`, if the extra line costs you
|
|
48
|
+
somewhere tight.
|
|
49
|
+
|
|
50
|
+
**A `SequenceItem` with no handlers renders no control column.** Reorder/remove controls still
|
|
51
|
+
render disabled where they cannot act — first item, last item, a row that can be removed but not
|
|
52
|
+
moved — because that is what holds the right edge steady between items that are editable. What
|
|
53
|
+
changed is the item that declares NO editing at all: it used to get three permanently dead
|
|
54
|
+
buttons, which is a dead affordance by `reviewing.md`'s own gates, and a read-only sequence (legs
|
|
55
|
+
already run, a route as recorded) is a common shape rather than an edge case. A list where some
|
|
56
|
+
rows are fully locked and others are editable will now step in the right margin; give the locked
|
|
57
|
+
rows a handler if that matters more than the dead controls did.
|
|
58
|
+
|
|
59
|
+
**`DateStamp` takes `compact`.** Additive, default `false`. It forwards the option `formatDate`
|
|
60
|
+
already had, dropping the year (`08/09`) so the stamp can agree with a neighbouring column the
|
|
61
|
+
caller formats itself. The format stays in `formatDate` — this is a passthrough, not a second
|
|
62
|
+
implementation.
|
|
63
|
+
|
|
64
|
+
**A `DetailTable` stacks earlier, because a value column's floor is no longer a control's.**
|
|
65
|
+
`minValueWidth` defaulted to `MIN_CONTROL_WIDTH` (160) — the width below which a CONTROL stops
|
|
66
|
+
being operable, which is a different question from the width below which a VALUE stops being
|
|
67
|
+
readable. A value column holds company names and addresses. At 150/160 the stack threshold was
|
|
68
|
+
334, so a 375px screen kept two columns and handed the value ~170px: a name needing 221 got 104.
|
|
69
|
+
The new `MIN_VALUE_WIDTH` (200) is the default; pass `minValueWidth` explicitly to pin the old
|
|
70
|
+
behaviour on a table whose values really are short.
|
|
71
|
+
|
|
72
|
+
**An unannotated table column's drop priority is `columns.length + index`, not `index`.** An
|
|
73
|
+
unannotated register still sheds right-to-left — that is unchanged and covered by a test — but a
|
|
74
|
+
MIXED table now behaves the way its author meant. Annotating your most important column
|
|
75
|
+
`priority: 1` used to TIE it with the unannotated column at index 1, and the right-to-left
|
|
76
|
+
tie-break then dropped yours first: the amount column, the fact the register exists for,
|
|
77
|
+
disappearing at narrow widths on a screen that reviewed as correctly annotated. **Annotate every
|
|
78
|
+
droppable column or none** — a partial annotation is what the old default punished.
|
|
79
|
+
|
|
80
|
+
**A selected `CardSelectItem` carries a GROUND**, not the same ring as hover. It was
|
|
81
|
+
`selected || hovered || pressed || focusVisible` → one style, so the card you had CHOSEN was
|
|
82
|
+
pixel-identical to the card under the pointer. Selection is now `accent_wash` + an accent edge,
|
|
83
|
+
the same "you are here" a selected table row and an active chip carry. This is the fix 43.5.0
|
|
84
|
+
made for `Table`'s selected row, one release late.
|
|
85
|
+
|
|
86
|
+
**`DataGrid`'s flexible column has a floor** (`FLEX_MIN_WIDTH`, 120) instead of `minWidth: 0`.
|
|
87
|
+
This grid sheds no columns by design — every column is a measure you came to compare — so the
|
|
88
|
+
fixed columns held their widths and the flexible identity column absorbed the whole squeeze,
|
|
89
|
+
all the way to zero: a narrow container rendered rows of unlabelled money, silently. Past the
|
|
90
|
+
floor the grid now overflows, and a narrow container needs to scroll it.
|
|
91
|
+
|
|
92
|
+
**`AgentRunLike.error` is OPTIONAL.** It was declared required while its own docblock promised
|
|
93
|
+
it accepted an SDK run's optional `error` — and an optional property is not assignable to a
|
|
94
|
+
required one however wide the required one's type, so the three-line usage printed in
|
|
95
|
+
`ai_patterns.md` did not compile. **If you worked around it** with a `useMemo` reshape or a cast
|
|
96
|
+
(`{ ...run, error: run.error ?? null }`), delete that: hand the SDK's run straight over.
|
|
97
|
+
|
|
98
|
+
**`Select` takes the `D` data generic** its siblings already had, so `optionPicker(...)` spreads
|
|
99
|
+
into it without a cast. Additive.
|
|
100
|
+
|
|
101
|
+
Also additive, nothing to change: `MIN_VALUE_WIDTH` and `FLEX_MIN_WIDTH` are exported (the
|
|
102
|
+
latter via a new `./table_fit` entry point, which also exports `ROW_HEIGHT` and `COLUMN_GAP` so
|
|
103
|
+
a skeleton can be built from the register's own constants); `formatMoney` takes
|
|
104
|
+
`maxFractionDigits` for a genuine per-unit price (`Intl`'s currency style floors at the
|
|
105
|
+
currency's minor unit, so an LCL rate of 0,019 US$/kg printed as `0,02`); `InlineSelect`
|
|
106
|
+
forwards `getOptionDescription` to the two-line option row `OptionList` already renders;
|
|
107
|
+
`NumberInput` takes a `placeholder`; `EmptyState` and `ErrorState` centre their TEXT and not
|
|
108
|
+
only their box (they swap into one another as a read settles, so only one of the two centred
|
|
109
|
+
would have shown up as the copy jumping alignment at the moment a region fails); a
|
|
110
|
+
heading row WRAPS rather than crushing its title; `Accordion`'s meta yields before its title;
|
|
111
|
+
`CardHeaderTitle` reads the ⓘ's name from the locale pack; and `ReferenceField`'s peek footer
|
|
112
|
+
gives its clear the same resting surface as the verb beside it.
|
|
7
113
|
## 44.5.0 — `Pagination` drops the page index
|
|
8
114
|
|
|
9
115
|
The pager rendered a range AND a page index — `16–30 of 45` beside `Page 2 of 3`. Both answer
|
|
@@ -94,11 +200,10 @@ No API is removed and nothing throws, but four DEFAULTS moved, so every register
|
|
|
94
200
|
in your app renders differently on upgrade. Each is reversible at the call site if you need the
|
|
95
201
|
old look while you catch up.
|
|
96
202
|
|
|
97
|
-
**Table column headers are sentence case, not all-caps
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
`muted
|
|
101
|
-
case alone leaves it reading as a stray label rather than as the band naming the columns.
|
|
203
|
+
**Table column headers are sentence case, not all-caps.** `Table` and `SortHeader` were
|
|
204
|
+
uppercasing every column label, which the heading ramp bans and names the #1 templated tell. What
|
|
205
|
+
keeps the band from reading as a stray label once the case is gone is WEIGHT: the header is
|
|
206
|
+
`sm muted medium`, and a row's supporting line is `sm muted` regular, so the two never collide.
|
|
102
207
|
|
|
103
208
|
**Row separators are lighter than the header rule.** The band capping a table keeps
|
|
104
209
|
`colors.border`; the hairlines between rows are `zinc-100`. Every rule used to be `border`, which
|
package/docs/catalog.md
CHANGED
|
@@ -222,6 +222,13 @@ Two columnar shapes, and the choice is about data size:
|
|
|
222
222
|
heading, so digits line up under their label; **`DataGridColumn.footer` gives that column its
|
|
223
223
|
cell in the group's footer row** (a subtotal, a resolved measure) — the grid lays it out in
|
|
224
224
|
the column's own box, so a total can never drift from what it sums and no consumer rebuilds
|
|
225
|
+
**`DataGrid` SHEDS NOTHING at narrow widths, and that is deliberate** — every column is a
|
|
226
|
+
measure you came to compare, unlike `Table`'s register of objects where the row is an index
|
|
227
|
+
and the record holds the rest. So it has no `priority` and no stacked mode: fixed columns keep
|
|
228
|
+
their widths, the flexible column holds a `FLEX_MIN_WIDTH` floor, and past that the grid
|
|
229
|
+
OVERFLOWS. A narrow container must scroll it horizontally; the floor exists because without
|
|
230
|
+
one the flexible identity column absorbed the entire squeeze down to zero and the grid
|
|
231
|
+
rendered rows of unlabelled numbers, silently. If you want columns to drop, you want `Table`.
|
|
225
232
|
the row. `renderGroupFooter` remains for content that does NOT sit in a column (a per-group
|
|
226
233
|
add row). The grid takes exactly its container's width — no outdent to fight, so it needs no
|
|
227
234
|
scroller unless YOU add one. It renders ALL rows (no
|
|
@@ -376,7 +383,9 @@ your screen. See the AI-patterns doc indexed in [AGENTS.md](../AGENTS.md) for th
|
|
|
376
383
|
`ChoiceList` options), `Sources` (provenance chips for AI output — at review scale,
|
|
377
384
|
`label={null}` slots the chips at a section's bottom), `Finding` (one ranked insight from an
|
|
378
385
|
AI check, laid out HORIZONTALLY: a severity dot, the title, and the `delta` right-aligned on
|
|
379
|
-
that same line, over
|
|
386
|
+
that same line, over the `readings` stacked ONE PER LINE — each `source value`, no separator
|
|
387
|
+
joining them (a middot claims a relation it will not name, and is the one mark a screen reader
|
|
388
|
+
drops), both kept on
|
|
380
389
|
screen with NEITHER marked as the wrong one, since a finding reports that two sources disagree
|
|
381
390
|
and which is right is the reader's call. `detail` is the optional consequence. A reader scanning
|
|
382
391
|
a stack gets severity down the left edge and magnitude down the right. The readings ARE the
|
|
@@ -542,6 +551,11 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
542
551
|
one interaction it signals; the padding still comes off the 8-grid, the ring is only
|
|
543
552
|
the floor), `HOVER_BORDER`, `CONTROL_TRANSITION`, `chipSurfaceStyle` — the shared
|
|
544
553
|
control-surface tokens.
|
|
554
|
+
- **`table_fit`** — the register's shared geometry, importable so a skeleton or a
|
|
555
|
+
hand-built band derives from the same numbers `Table` lays out with: `ROW_HEIGHT` (72),
|
|
556
|
+
`COLUMN_GAP`, `ROW_GUTTER`, `FLEX_MIN_WIDTH` (120 — what a flexible column needs to stay
|
|
557
|
+
usable), plus `computeTableFit` itself. `MIN_VALUE_WIDTH` (200 — a record VALUE column's
|
|
558
|
+
floor, deliberately above a control's `MIN_CONTROL_WIDTH`) lives in `control_surface`.
|
|
545
559
|
- **`fonts.css`** — the Inter sheet (400/500/600, served by absolute URL so it resolves on
|
|
546
560
|
every origin an app runs from); the app entry imports it ONCE or every `Text` falls back
|
|
547
561
|
to system fonts.
|
|
@@ -1150,7 +1164,9 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1150
1164
|
`Destination`) cannot hold a second middle entry and cannot say the entries are ordered.
|
|
1151
1165
|
`SequenceItem` takes the content, an optional `role` (DERIVE it from index — a stored role lies
|
|
1152
1166
|
the moment the list is reordered) and `onMoveUp`/`onMoveDown`/`onRemove`; the controls render
|
|
1153
|
-
even where they cannot act so the right edge never shifts between items
|
|
1167
|
+
even where they cannot act so the right edge never shifts between items — but an item passing
|
|
1168
|
+
NO handler at all renders no control column, so a read-only sequence (legs already run, a route
|
|
1169
|
+
as recorded) is a first-class shape rather than three dead buttons per row. Reorder is BUTTONS,
|
|
1154
1170
|
not a drag handle — dragging is invisible to the keyboard and fiddly on a phone, and a
|
|
1155
1171
|
three-to-six position list does not need it. Indent anything that follows the list (an "Add"
|
|
1156
1172
|
link) by `SEQUENCE_INSET` so it lands on the rail's column. Distinct from `Timeline` (an
|
|
@@ -1182,8 +1198,9 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1182
1198
|
before acting (amber, announced); **`error`** = field-level failure under the value, danger +
|
|
1183
1199
|
alert semantics (the `Inline*` editors already render their own transient save errors — don't
|
|
1184
1200
|
wire both); invalid/cross-field STATE = a co-located `Callout`; **`flat`**
|
|
1185
|
-
on a
|
|
1186
|
-
|
|
1201
|
+
on a value sitting OFF the inline-control grid (a raw node — NOT `InlineStatic` / `InlineSlot`,
|
|
1202
|
+
which are on it) aligns the annotation to it on both axes: drops the control text inset and
|
|
1203
|
+
tucks up by the band's slack.
|
|
1187
1204
|
The three annotations are ONE anatomy shared with a checklist row's `ChecklistNote` — same
|
|
1188
1205
|
order, same alert semantics — because a record's field and a task's field are the same thing
|
|
1189
1206
|
on two surfaces.
|
|
@@ -1257,7 +1274,8 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1257
1274
|
panel narrower than the control that opened it reads as belonging to something else. So the
|
|
1258
1275
|
field truncates its name to one line and the peek's header WRAPS the whole of it: the column
|
|
1259
1276
|
stays scannable, the full value is one press away. Between the floor and `labelWidth +
|
|
1260
|
-
|
|
1277
|
+
MIN_VALUE_WIDTH + 24` a `DetailTable` stacks, which is the right grammar at that width — the
|
|
1278
|
+
VALUE floor (200), not the control floor, because that column holds names as well as editors.
|
|
1261
1279
|
The verbs live INSIDE the
|
|
1262
1280
|
peek, never on the field, and they are ALL `Button`s at one altitude — a peek is
|
|
1263
1281
|
dialog-scale and a dialog's verbs are buttons, so mixing weights only made the reader rank
|
|
@@ -1374,6 +1392,9 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1374
1392
|
position (a group band, a totals line) and the gutter stays blank so the cells beside it stay
|
|
1375
1393
|
in line. A position is NEVER an identity: sort or filter and row 12 is a different record —
|
|
1376
1394
|
a stable handle is a column, and it leads the cells. Worked screen: `tpl_item_list`.
|
|
1395
|
+
- **`tabs`** — a tab may state its band's size with `TabOption.count`, and needs attention with
|
|
1396
|
+
`TabOption.status` (a `ColorName` dot). Pressing the tab IS the filter, so the count is also the
|
|
1397
|
+
reason to press it. → [composition.md](./composition.md) §"A partitioning control states its band's size".
|
|
1377
1398
|
- **`table`** rhythm — rows are separated by their own height and the hover wash, never by a hairline; the register's one line is the band capping the columns. 72px rows, overridable per row with `TableRow`'s `minHeight` when a surface genuinely needs density. There is no `spacing` prop: a table of VALUES rather than of objects is a different component (`Matrix`, `DataGrid`). → [composition.md](./composition.md) §"The register's rhythm".
|
|
1378
1399
|
- **`table` › `TableGroup`** — a titled BAND of rows inside a `Table` (`label`, `count?`, `color?` for a single valence dot). Turns a register that presents records into one that triages them: group by what implies a different ACTION, order the bands by what needs the reader now, and let air rather than a rule separate them. → [composition.md](./composition.md) §"A register that TRIAGES".
|
|
1379
1400
|
- **`sort_header`** — `SortHeader` + `SortState`/`SortDir` + `cycleSort` + `sortBy` +
|
|
@@ -1435,6 +1456,9 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1435
1456
|
empty — and a ladder of fields is a form with invisible borders. Reach for `InlineDatePicker`
|
|
1436
1457
|
when a person is expected to ENTER the date, and this when the system already knows it.
|
|
1437
1458
|
Empty renders a thin rule, not a placeholder; read-only (omit `onChange`) renders text alone.
|
|
1459
|
+
`compact` drops the year (`08/09`) — the stamp shares a row with dates the caller formats, and
|
|
1460
|
+
the option goes straight through to `formatDate`, which stays the one place deciding what a
|
|
1461
|
+
date looks like.
|
|
1438
1462
|
`onChange` fires on CLOSE, not on each pick — one commit per visit, and a dismissed panel
|
|
1439
1463
|
still writes whatever was picked in it, which is what makes a single tap enough.
|
|
1440
1464
|
Worked screen: `examples/tpl_record.tsx` § Progress.
|
|
@@ -1606,7 +1630,9 @@ component rather than showing it at zero.
|
|
|
1606
1630
|
status mix on one bar).
|
|
1607
1631
|
- **`step_progress`** — `StepProgress`: N-of-M dots/segments progress. A real `progressbar`
|
|
1608
1632
|
(valuenow/min/max) because its segments are decoration over ONE quantity — the opposite
|
|
1609
|
-
case from `stepper`, whose steps carry content a `progressbar` would hide.
|
|
1633
|
+
case from `stepper`, whose steps carry content a `progressbar` would hide. Pass the stage
|
|
1634
|
+
NAMES and it captions itself from the `stepProgress` locale slice ("Hạ bãi (2/4)"); `label`
|
|
1635
|
+
overrides, `captionTone="danger"` is the documented stalled-stage slot.
|
|
1610
1636
|
- **`breakdown`** — `Breakdown`: a stacked bar + ranked share rows, pressable to drill;
|
|
1611
1637
|
`maxRows` folds the long tail behind a localized "Show N more" toggle
|
|
1612
1638
|
(`BreakdownLabels`).
|
|
@@ -2019,7 +2045,9 @@ component rather than showing it at zero.
|
|
|
2019
2045
|
column quietly acquires TWO left edges and two baselines, and `DetailRow`'s label (which pads
|
|
2020
2046
|
down to meet a control) agrees with one kind of row and not the other. `InlineStatic` is the
|
|
2021
2047
|
STRING case and composes it; reach for the slot when the value is a node, which a review
|
|
2022
|
-
surface always is.
|
|
2048
|
+
surface always is. An ANNOTATED row needs the grid even with no editor in sight, because the
|
|
2049
|
+
annotation stack is inset to a control's words — data_entry.md §The editability affordance
|
|
2050
|
+
has the per-table test. NOT for a table cell — a `Table` sets its own row rhythm.
|
|
2023
2051
|
- **`use_change_set`** — **`useChangeSet(ids, { initial })`**: the accept/reject/undo
|
|
2024
2052
|
bookkeeping as a HOOK, zero layout — `status`/`accept`/`reject`/`undo`/`acceptAll`/`reset`,
|
|
2025
2053
|
`accepted`/`rejected`/`pending`, `keptCount`/`total`/`settled`. Decisions are stored as
|
package/docs/composition.md
CHANGED
|
@@ -8,6 +8,23 @@ full worked-example screens ship as source in `examples/tpl_*.tsx` (picked by jo
|
|
|
8
8
|
[templates](./templates.md)). Every rule here is enforced by the primitives' defaults; fighting
|
|
9
9
|
them produces off-system UI.
|
|
10
10
|
|
|
11
|
+
## The form comes before the treatment
|
|
12
|
+
|
|
13
|
+
**Name what the subject IS, then reach for parts.** A set of records is a register; a thing moving
|
|
14
|
+
through ordered places is a rail; work assigned to finite resources over time is a board; a
|
|
15
|
+
sequence of events is a timeline; a population whose point is its deviant minority is a queue
|
|
16
|
+
sorted by deviation. Getting this wrong cannot be recovered later — a register of objects that
|
|
17
|
+
should have been a rail is well-typed, well-spaced, correctly aligned and still says nothing about
|
|
18
|
+
order or progress, because a status column reports one word and leaves the reader to know which
|
|
19
|
+
word comes after which.
|
|
20
|
+
|
|
21
|
+
The pull toward a register is structural, not a lapse in taste: `Table`, `DetailRow`,
|
|
22
|
+
`SummaryLine` and `Badge` are the most reachable things here, and they compose into something
|
|
23
|
+
defensible for almost any data. That is exactly why the question has to be asked out loud. **Reuse
|
|
24
|
+
first is about not duplicating primitives** — a one-off composition needs no new component, so the
|
|
25
|
+
kit lacking a device is a reason to compose one from what it has, not a reason to flatten the
|
|
26
|
+
subject into rows.
|
|
27
|
+
|
|
11
28
|
## Canvas & content column
|
|
12
29
|
|
|
13
30
|
- **Canvas**: a full-bleed `colors.white` ScrollView with `padding: 28`. Cards separate from the
|
|
@@ -183,6 +200,14 @@ without scanning. The heading row is the section's control line — it already c
|
|
|
183
200
|
and the view controls — so the add belongs on it, in the same spot whether the section holds
|
|
184
201
|
nought or forty.
|
|
185
202
|
|
|
203
|
+
**When the row runs out of width, the VERB wraps — the title never gives way.** The title names
|
|
204
|
+
the section; the verb is the thing that can move. Getting this backwards is easy to ship because
|
|
205
|
+
RN-Web resolves `flex: 1` to `flex-basis: 0%` with `min-width: 0`, so a title beside a button that
|
|
206
|
+
holds its intrinsic width is the half that collapses — measured at 375 as a heading broken one
|
|
207
|
+
word per line beside an untouched button. `SectionHeading` and `SubsectionHeading` wrap and floor
|
|
208
|
+
their title column for this; do not add a second `flex: 1` spacer beside a title that already has
|
|
209
|
+
one, which halves its share again and is how a badge came to land on top of a section title.
|
|
210
|
+
|
|
186
211
|
**Where there IS no heading row, the add has nowhere else to be, so it rides the surface's own
|
|
187
212
|
bar** — an attachment popover, a dialog body, any self-contained widget. That is not an exception
|
|
188
213
|
to the law so much as the same reasoning: the rule exists so the verb sits in ONE place a reader
|
|
@@ -217,6 +242,18 @@ no path actually closes.
|
|
|
217
242
|
- **Only the ADD.** A verb that acts on a SELECTION (delete, export, run) belongs to the
|
|
218
243
|
selection — `FloatingActionBar` — and a verb about ONE row stays on the row.
|
|
219
244
|
|
|
245
|
+
## A partitioning control states its band's size as a PROP
|
|
246
|
+
|
|
247
|
+
`TableGroup` takes `count`; `Tabs` takes it per option. Neither takes a label with the number
|
|
248
|
+
formatted into it, and that is the whole rule: `"Trên tàu (4)"` is a second copy of a figure the
|
|
249
|
+
screen already computes, and it is the copy that goes stale — the band re-counts on every filter
|
|
250
|
+
and the string does not.
|
|
251
|
+
|
|
252
|
+
It also decides where the count may live. A register whose bands already state their sizes must
|
|
253
|
+
not carry a strip of chips above it repeating the same three numbers; that is one fact twice, and
|
|
254
|
+
the reader learns to skip the strip. And a count is a reason to PRESS — a control that states how
|
|
255
|
+
many rows are behind it and does not filter to them is stating a fact it refuses to act on.
|
|
256
|
+
|
|
220
257
|
## Period filters for time-constrained data
|
|
221
258
|
|
|
222
259
|
Time-constrained data gets a **`DateRangeFilterField`** in the header band — never a static period
|
|
@@ -312,7 +349,11 @@ compete instead of reading as different things. Selection changes what the box I
|
|
|
312
349
|
hover changes how intensely it says it.
|
|
313
350
|
|
|
314
351
|
For fields: layer the hover-border AFTER `style` so it wins over the caller's resting edge, put the
|
|
315
|
-
open ring (`FOCUS_RING`) on the open state, and **a field NEVER greys its content**.
|
|
352
|
+
open ring (`FOCUS_RING`) on the open state, and **a field NEVER greys its content**. That ordering
|
|
353
|
+
is load-bearing rather than tidy: a caller's resting `borderColor` is exactly how a frameless
|
|
354
|
+
variant gets expressed, and layered the other way it erases the hover edge that is such a field's
|
|
355
|
+
ONLY affordance — a control that then draws nothing in either state and is simply never pressed.
|
|
356
|
+
Give the field a variant PROP for its resting frame instead of taking one through `style`. The bug this
|
|
316
357
|
grammar prevents: a FIELD built on `PressableHighlight` inherits the wash AND its own border — it
|
|
317
358
|
greys *and* animates its edge while its siblings only border-hover. Reach for `PressableHighlight`
|
|
318
359
|
on a bordered/input control and you've made it.
|
|
@@ -416,6 +457,12 @@ a dashboard. Card stat rails use `KPICard`.
|
|
|
416
457
|
form round-trip never expands them to a fabricated `"2026-05-01"`. Comparisons and sorting treat
|
|
417
458
|
a partial as its period start (`"2026" < "2026-05" < "2026-05-01"`).
|
|
418
459
|
|
|
460
|
+
**A per-unit price is not a total, and the currency's minor unit is the wrong floor for it.**
|
|
461
|
+
`Intl`'s currency style rounds to two decimals for USD, so a rate of 0,019 per kg prints as
|
|
462
|
+
`0,02` — a 5% error rendered as fact, with nothing on the screen saying it was rounded. Pass
|
|
463
|
+
`maxFractionDigits` on a UNIT price; leave it off for a total, where two decimals is the contract.
|
|
464
|
+
Never hand-roll the grouping or the symbol to get the digits back.
|
|
465
|
+
|
|
419
466
|
## Every number is a door
|
|
420
467
|
|
|
421
468
|
Except the KPI strip: a component that summarizes records leads to the records behind it when
|
|
@@ -712,6 +759,21 @@ action goes to the referenced record's own page; a local `Drawer` that re-shows
|
|
|
712
759
|
just showed makes "Open" mean *read that again, wider*, and leaves the real record unreachable
|
|
713
760
|
from the only control that promised it.
|
|
714
761
|
|
|
762
|
+
**A record surface answers *which record is this* first — and the block that does it is SHORT.**
|
|
763
|
+
Identity is the handful of facts someone would say aloud to name this record (its number and its
|
|
764
|
+
counterparty, usually two); everything else is an ATTRIBUTE and belongs below the answer the
|
|
765
|
+
reader opened the record for. Get the size wrong and the position becomes unwinnable: eight
|
|
766
|
+
labelled header fields push the ledger under the fold, and moving the block to the bottom to fix
|
|
767
|
+
that leaves a reader who cannot tell whose record they are in. Both orders measure perfectly, so
|
|
768
|
+
neither is caught by any check that reads the rendered screen. **When a leading block buries the
|
|
769
|
+
answer, shrink it — do not move it**; frequency of EDIT is not the axis, because a rarely-edited
|
|
770
|
+
fact can still be the one that names the thing.
|
|
771
|
+
|
|
772
|
+
"The chrome already carries identity, so the body need not lead with it" is a real argument and a
|
|
773
|
+
falsifiable one — verify it FACT BY FACT before relying on it. A summary strip showing the record
|
|
774
|
+
number and its totals carries half an identity; the counterparty is missing, and half is what
|
|
775
|
+
makes the claim feel true while the reader still cannot name what they are looking at.
|
|
776
|
+
|
|
715
777
|
**All of the above is the LIST SCREEN.** A CHILD COLLECTION inside a record page — fees, lines,
|
|
716
778
|
stops, containers — does the opposite: the row **EXPANDS** (`TableRow` `detail` + `expanded`),
|
|
717
779
|
never docks a drawer. A drawer's whole job is to supply a context the reader is missing, and on a
|
|
@@ -928,10 +990,13 @@ gate's SCOPE, once** — never prose beside the button, never revealed only on p
|
|
|
928
990
|
alignment law: the row top-aligns and label, control, trailing each center within the
|
|
929
991
|
first control line — a tall value block never drags the label, and a LABEL too long for its
|
|
930
992
|
column WRAPS (never clipped, never abbreviated to fit) with its first line still level with
|
|
931
|
-
the value. A
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
the
|
|
993
|
+
the value. A value that sits OFF the
|
|
994
|
+
inline-control grid — a raw node, not an `InlineStatic` / `InlineSlot` — sets **`DetailRow
|
|
995
|
+
flat`**, which aligns its annotations on both axes: they drop the control's text inset so the
|
|
996
|
+
description starts on the value's own left edge, and tuck up by the band's slack so the gap
|
|
997
|
+
under the text matches the gap under a chip. Miss it and the row has three left edges and a
|
|
998
|
+
hole; pass it on a value that IS on the grid and you create both. Which one a static value
|
|
999
|
+
should be is decided per table — see data_entry.md §The editability affordance.
|
|
935
1000
|
- **A verb about a VALUE goes ON the value — an `InlineButton` in the field's `actions`.** Copy
|
|
936
1001
|
this reference, call this number, open this record: the act names the value, so it travels
|
|
937
1002
|
with what it acts on, and a reader never has to pair a control sitting at the page's right
|
|
@@ -1243,6 +1308,15 @@ which is the loudest possible version of the thing the rule exists to stop. Remo
|
|
|
1243
1308
|
without replacing the separation is what left the header looking adrift — the ink step above is
|
|
1244
1309
|
the replacement, and the two changes only work together.
|
|
1245
1310
|
|
|
1311
|
+
**Annotate every droppable column, or none.** `priority` decides what a narrow container sheds
|
|
1312
|
+
(lowest number is last to go, and column 0 — the identity — never drops). A PARTIAL annotation is
|
|
1313
|
+
the trap: an unannotated column defaults above the whole hand-written range, so one annotated
|
|
1314
|
+
column silently makes every other column drop before it, and one unannotated column among
|
|
1315
|
+
annotated ones is the first to go however important it is. Both read as correct in review. The
|
|
1316
|
+
question to answer per register is "what is the last column standing", and the answer is almost
|
|
1317
|
+
always the figure the register exists to compare — so give that one the lowest number and work
|
|
1318
|
+
outwards.
|
|
1319
|
+
|
|
1246
1320
|
**A row's SUBJECT is `medium`, never semibold.** Semibold is the heading ladder's weight — `#`,
|
|
1247
1321
|
`##`, `###` are all semibold — so a register whose row subjects are semibold renders its content
|
|
1248
1322
|
at heading weight once per row, in the same font CUT as the page title (`Inter_600SemiBold` is a
|
|
@@ -1495,6 +1569,12 @@ disclosure exception). `TextLink` takes the blue only when it has an `href`; as
|
|
|
1495
1569
|
a value, or as a muted in-place disclosure, it stays neutral — because a press that opens a PEEK,
|
|
1496
1570
|
or unfolds a transcript, is not a trip.
|
|
1497
1571
|
|
|
1572
|
+
**Selection is a GROUND, and hover is a ring — at every altitude.** A row, a card, a chip, a
|
|
1573
|
+
tile: the thing you have CHOSEN carries the accent wash, and the thing under the pointer carries
|
|
1574
|
+
the ring. Collapsing them into one "active" style makes the selected item pixel-identical to the
|
|
1575
|
+
hovered one, so "which did I pick" is answerable only by moving the mouse away. The tell in code
|
|
1576
|
+
is a single boolean OR-ing selection together with hover, press and focus.
|
|
1577
|
+
|
|
1498
1578
|
## `Badge` is for STATUS only — everything else is text
|
|
1499
1579
|
|
|
1500
1580
|
A `Badge` means STATE that must read at a GLANCE — a lifecycle status, a risk level, a quality
|
|
@@ -1610,6 +1690,12 @@ Anything pressable is ≥ 40px tall (8px minimum gap between pressables). 16px b
|
|
|
1610
1690
|
canvas padding, 16–20 inside bands, 10–12 between content lines. Density comes from alignment +
|
|
1611
1691
|
hierarchy, not cramming.
|
|
1612
1692
|
|
|
1693
|
+
**A control's height comes from `CONTROL_HEIGHT`, never from padding tuned against a leading.**
|
|
1694
|
+
Padding that happens to sum to the band height is a second owner of that number, and it drifts the
|
|
1695
|
+
moment the type ramp moves — which the ramp is allowed to do. The signature is one control
|
|
1696
|
+
standing a few pixels proud of every sibling in a filter bar, correct on the day it was written
|
|
1697
|
+
and wrong after a release that touched no control at all.
|
|
1698
|
+
|
|
1613
1699
|
**The trim test — does this element RESTATE something already on the screen?** Run it over every
|
|
1614
1700
|
screen before shipping: a per-row proportion bar when the percentage is already on the row; the
|
|
1615
1701
|
percentage itself when the column holds four values and a total; a state line that only says
|
package/docs/data_entry.md
CHANGED
|
@@ -66,6 +66,13 @@ Controlled inputs make derived state track what is on screen, and read shorter t
|
|
|
66
66
|
they replace — four `DetailRow` + `Inline*` pairs whose `onSave` exists only to `setState` collapse
|
|
67
67
|
to four fields. Worked example: `tpl_record`'s New-customer dialog.
|
|
68
68
|
|
|
69
|
+
**The discriminator, since the kit's own `tpl_item_list` fills preview cards with `Inline*`
|
|
70
|
+
editors:** what decides is whether anything on the surface READS the draft before the commit. A
|
|
71
|
+
dialog whose Save is gated on the values being typed, or whose visible totals derive from them,
|
|
72
|
+
takes controlled `FormField`s — that is this rule. A REVIEW surface whose gate is decided by
|
|
73
|
+
something else (which cards are kept, which candidate is picked) may keep inline editors, because
|
|
74
|
+
nothing is arguing with the box under the cursor. Neither shape is a create form's stand-in.
|
|
75
|
+
|
|
69
76
|
**But do not then derive the ERROR per keystroke** — the mistake controlled inputs invite, and the
|
|
70
77
|
opposite failure to the one above. Validity and the error MESSAGE are different questions. Validity
|
|
71
78
|
gates the commit and is checked continuously; the message claims the reader did something wrong, so
|
|
@@ -313,19 +320,66 @@ where it must stay strictly inside its box — that is `framed`. A DISABLED `fra
|
|
|
313
320
|
box: it drops the border and fill so it cannot promise a press, but it stays in line with the enabled
|
|
314
321
|
fields beside it.
|
|
315
322
|
|
|
323
|
+
**`bare` puts its whole affordance in the hover edge, so nothing may outrank that edge.** Pass the
|
|
324
|
+
variant as a PROP; never express it as a resting `borderColor: "transparent"` in `style`, which is
|
|
325
|
+
applied after the hover rule and erases the edge one line after it is computed. The failure is
|
|
326
|
+
silent in every direction that usually catches things — the field keeps its role, its label, its
|
|
327
|
+
focus ring, its metrics and its save — and what breaks is only that nobody presses it, because a
|
|
328
|
+
control drawing nothing at rest and nothing on hover is text. The kit's own `TextInputField` had
|
|
329
|
+
it, so a `bare` inline editor in a register column went un-pressed rather than mis-styled.
|
|
330
|
+
|
|
331
|
+
**The cursor is the other half of that promise, and it belongs to the CONTROL, not to the page.**
|
|
332
|
+
An inline editor takes the pointer like every other control. It once forced the arrow back, on the
|
|
333
|
+
reasoning that an editor is an input rather than a button — right that it is not a button, wrong
|
|
334
|
+
about the replacement: the arrow is what inert prose gets, so the one value on the row that was
|
|
335
|
+
the reader's to change was the one that looked least touchable. The I-beam is not the alternative
|
|
336
|
+
either; a resting editor sets `userSelect: "none"` so a drag edits rather than selects, and an
|
|
337
|
+
I-beam would promise a caret that is not there. A field is disabled is the only case that reverts
|
|
338
|
+
to the arrow, for the same reason it rests bare: an inert value must promise nothing.
|
|
339
|
+
|
|
316
340
|
A READ-ONLY value in the same column — a computed total, a system ID, a synced/locked field — is
|
|
317
341
|
**`InlineStatic`**: it copies the editor box metrics exactly (height, padding, 1px transparent
|
|
318
342
|
border) but stays FLAT and non-interactive, so editable (chip) vs read-only (flat) is legible at a
|
|
319
343
|
glance and the static value never reads as a disabled input.
|
|
320
344
|
|
|
321
|
-
**
|
|
322
|
-
`INLINE_CONTROL_HEIGHT` — worth paying to hold a column straight
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
345
|
+
**In a grid that MIXES the two, or one that ANNOTATES.** "Copies the editor box metrics" means it
|
|
346
|
+
hard-sets `INLINE_CONTROL_HEIGHT` — worth paying to hold a column straight, waste where there is
|
|
347
|
+
no column to hold. Two different things can make the column, and missing the second is what put
|
|
348
|
+
three left edges on a review dialog:
|
|
349
|
+
|
|
350
|
+
- **An editor beside it.** The obvious case: a static value rendered raw beside a real editor
|
|
351
|
+
starts at the cell edge, 20px tall, at the top, while the editor's text starts 9px in and
|
|
352
|
+
centres in a 40px band.
|
|
353
|
+
- **A value column's floor is not a control's floor.** `minValueWidth` decides when the table
|
|
354
|
+
gives up two columns and stacks, and it defaults to `MIN_VALUE_WIDTH` (200) rather than a
|
|
355
|
+
control's `MIN_CONTROL_WIDTH` (160), because the column holds company names and addresses, not
|
|
356
|
+
only editors. The narrow-width failure is silent in both halves: the table technically fits, so
|
|
357
|
+
it does not stack, and every value is then truncated past half while the label beside it measures
|
|
358
|
+
perfectly. Raise it further where a cell holds MORE than one editor; lower it only where the
|
|
359
|
+
values genuinely are short.
|
|
360
|
+
|
|
361
|
+
**A `description` / `warning` / `error` under it.** `FieldAnnotations` insets its stack by
|
|
362
|
+
`CONTROL_TEXT_INSET` so it lines up with a CONTROL's words — so an annotated row whose value is
|
|
363
|
+
raw puts the value at the cell edge and its own description 9px inside it. Nothing else on the
|
|
364
|
+
screen is wrong, the drift is small enough to read as sloppiness rather than structure, and it
|
|
365
|
+
appears only on the rows that happen to carry a description, so a column slides in and out of
|
|
366
|
+
alignment down its own length.
|
|
367
|
+
|
|
368
|
+
Decide it per TABLE, never per row: one annotated static row puts every static value in that table
|
|
369
|
+
on the grid, or the table keeps two row heights for no reason a reader can see.
|
|
370
|
+
|
|
371
|
+
In an all-static, un-annotated grid (a peek's facts, a summary popover) the grid buys nothing and
|
|
372
|
+
costs 40px of band for a 20px value — and the row is the MAX of that box and the table's own band,
|
|
373
|
+
so neither fix works alone: use plain `Text` for the value AND pass `DetailTable` a `minHeight`
|
|
374
|
+
(28, `DetailRow`'s own default). Same test as `labelWidth` — a `DetailTable`'s defaults are the
|
|
327
375
|
inline-control grid's; a grid that isn't one overrides both.
|
|
328
376
|
|
|
377
|
+
`DetailRow flat` is the ESCAPE from the grid, not a synonym for read-only: it says "this value is
|
|
378
|
+
a raw node sitting off the control band", and it corrects the annotations on both axes for it —
|
|
379
|
+
dropping the text inset and tucking the block up by the band's slack. Never pass it with
|
|
380
|
+
`InlineStatic` or `InlineSlot`, which are ON the grid: there it re-introduces the 9px drift it
|
|
381
|
+
exists to remove and pulls the description 4px too tight.
|
|
382
|
+
|
|
329
383
|
### Choosing a CHOICE control — by option count, not by taste
|
|
330
384
|
|
|
331
385
|
A field whose value is one-of-N has three answers, and the wrong one is what makes a record
|
package/docs/reviewing.md
CHANGED
|
@@ -45,10 +45,35 @@ the same direction. The kit measures the CONTAINER, not the screen — see AGENT
|
|
|
45
45
|
|
|
46
46
|
---
|
|
47
47
|
|
|
48
|
-
##
|
|
48
|
+
## Three gates, before any probe
|
|
49
49
|
|
|
50
|
-
A
|
|
51
|
-
|
|
50
|
+
A screen in the wrong FORM measures clean in every direction, a perfectly-treated element that
|
|
51
|
+
should not exist still measures clean, and a wrong control cannot be treated into a right one.
|
|
52
|
+
All three are cheap and all three outrank everything below.
|
|
53
|
+
|
|
54
|
+
### 0. RIGHT FORM
|
|
55
|
+
|
|
56
|
+
**Name what the subject IS before composing anything.** A set of records is a register. A thing
|
|
57
|
+
moving through ordered places is a rail. Work assigned to finite resources over time is a board.
|
|
58
|
+
A sequence of events is a timeline. A population whose point is its deviant minority is a queue
|
|
59
|
+
sorted by deviation. A quantity across two dimensions is a matrix.
|
|
60
|
+
|
|
61
|
+
This gate exists because **every probe below reads a screen that is already a register of fields**
|
|
62
|
+
and measures its TREATMENT. So a screen that should have been a board or a rail passes the type
|
|
63
|
+
inventory, the ground tally, the gaps, the rules and the alignment — all of them — and still reads
|
|
64
|
+
as templated. No amount of treatment fixes it, and nothing else here can see it.
|
|
65
|
+
|
|
66
|
+
*The tell that it was skipped:* every screen in the product is a register with a summary line over
|
|
67
|
+
it. That is what reuse-first produces when nothing asks the question — the catalog's most reachable
|
|
68
|
+
parts are `Table`, `DetailRow`, `SummaryLine` and `Badge`, so a subject whose natural shape is not
|
|
69
|
+
a table gets flattened into one and no probe fires. **Reuse-first is about not duplicating
|
|
70
|
+
PRIMITIVES, not about the shape of a screen**: a one-off composition needs no new component, so
|
|
71
|
+
"the catalog has no journey rail" is an argument for composing one out of `Stepper`, not for
|
|
72
|
+
rendering a status column and moving on.
|
|
73
|
+
|
|
74
|
+
*And check the form against what the data can FILL.* A rail with no stamped stops, a calendar with
|
|
75
|
+
no durations, a map with no coordinates — each renders an empty device that measures perfectly.
|
|
76
|
+
→ probe 8.
|
|
52
77
|
|
|
53
78
|
### 1. SUBTRACT
|
|
54
79
|
|
|
@@ -244,6 +269,14 @@ A resting register is the cheapest thing to screenshot and the least likely to h
|
|
|
244
269
|
|
|
245
270
|
- **Open what the row opens** and measure where the actionable field LANDS. An UNBOUNDED section
|
|
246
271
|
(a feed, a thread) above a bounded one buries it, and the burial deepens with use.
|
|
272
|
+
- **Name the record before you read it.** Collect the y of each fact that IDENTIFIES what is open
|
|
273
|
+
(its number, its counterparty) and compare against the viewport; any of them first answered
|
|
274
|
+
below the fold is the finding. Then measure the other direction — the top of the block answering
|
|
275
|
+
what the reader OPENED this for, and what sits above it. A leading block that pushes that answer
|
|
276
|
+
under the fold is too LONG, not mis-positioned: moving it down trades a buried ledger for a
|
|
277
|
+
reader who cannot tell whose record they are in, and both orders measure clean on their own, so
|
|
278
|
+
nothing that reads a resting screen catches either. Re-measure at 375, where a block that cleared
|
|
279
|
+
the fold at 1280 buries the answer. → [composition.md](./composition.md) §"Master-detail".
|
|
247
280
|
- **Press every control** and look at the state you land in. Commonest miss: a value rendering as
|
|
248
281
|
a coloured chip in a register and as bare grey text in its own EDITOR.
|
|
249
282
|
- **TAB through it, then diff what a keyboard reaches against what LOOKS pressable.** A pointer
|