@lotics/ui 46.2.0 → 46.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 +38 -1
- package/MIGRATION.md +87 -0
- package/docs/ai_patterns.md +11 -0
- package/docs/catalog.md +217 -21
- package/docs/composition.md +74 -6
- package/docs/data_entry.md +56 -2
- package/docs/reviewing.md +34 -0
- package/docs/templates.md +93 -30
- package/docs/testing.md +6 -0
- package/examples/tpl_board.tsx +257 -0
- package/examples/tpl_money.tsx +1027 -0
- package/package.json +261 -258
- package/src/accordion.tsx +7 -1
- package/src/alert.css +0 -1
- package/src/alert.tsx +8 -0
- package/src/axis_label_indices.ts +84 -0
- package/src/bar_chart.tsx +137 -16
- package/src/board.tsx +611 -0
- package/src/card.tsx +7 -1
- package/src/charge_lines.tsx +373 -0
- package/src/chip_group.tsx +57 -1
- package/src/dialog.tsx +46 -24
- package/src/drawer.tsx +21 -2
- package/src/file_gallery_modal.tsx +3 -0
- package/src/file_row.tsx +98 -5
- package/src/icon.tsx +6 -0
- package/src/inline_edit.tsx +54 -10
- package/src/inline_number_input.tsx +5 -1
- package/src/inline_text_input.tsx +1 -1
- package/src/line_chart.tsx +2 -2
- package/src/locale.tsx +26 -1
- package/src/matrix.tsx +23 -8
- package/src/modal.tsx +23 -3
- package/src/overlay_layer.ts +65 -0
- package/src/page_content.tsx +8 -22
- package/src/page_header.tsx +60 -11
- package/src/popover.tsx +29 -5
- package/src/reference_field.tsx +36 -13
- package/src/skip_link.tsx +2 -1
- package/src/stacked_bar_chart.tsx +31 -1
- package/src/table.tsx +6 -1
- package/src/tabs.tsx +1 -1
- package/src/text.tsx +21 -0
- package/src/tooltip.tsx +2 -1
- package/src/use_change_set.ts +66 -17
- package/src/use_scroll_seam.ts +79 -0
- package/examples/tpl_report.tsx +0 -410
- package/examples/tpl_statements.tsx +0 -221
- package/src/line_chart_labels.ts +0 -32
package/AGENTS.md
CHANGED
|
@@ -18,7 +18,7 @@ CURRENT major only — upgrading an app across majors is `MIGRATION.md`.
|
|
|
18
18
|
| [docs/data_entry.md](./docs/data_entry.md) | Which editing pattern for which job — inline edit, fieldset forms, choosing a CHOICE control by option count, find-or-create (`Combobox`), line items, handoffs, phased records, billing, tags, dispositions, attachments (`InlineFiles` for a record ROW, the `FilesEditor` COMPOUND for a whole section, plus the three-way file INTAKE), stage gates, and the commit-on-blur vs action-press ordering law. |
|
|
19
19
|
| [docs/ai_patterns.md](./docs/ai_patterns.md) | AI acts, the human stays in charge — composer, live run feed (`AgentRun`), the one law's split — it turns on WHO supplied the values; findings, provenance, confidence; **after the run** — a stored record that fills up from several writers; the whole run in a dialog, **stopping**; **review surfaces compose from atoms** — `DiffValue`, `DiffMark`, `useChangeSet`. |
|
|
20
20
|
| [docs/composition.md](./docs/composition.md) | The design-language contract — **the form comes before the treatment** (name what the subject IS before reaching for parts), canvas + content column, heading altitude, register vs inset rows, the button ladder, master-detail `Drawer`, the register's rhythm, craft and TRIAGE bands, where the accent goes, theming as an app-level ESCAPE HATCH, color discipline, typography, **one fact one surface** (a chooser over the thing it chooses is a SELECTOR, not a second card grid; a control that only swaps one number for another is replaced by showing both), and where a commit sits. |
|
|
21
|
-
| [docs/reviewing.md](./docs/reviewing.md) | **Reviewing a screen you built** — the other docs say what good looks like; this one says how to find out whether you achieved it. GATES that outrank every treatment question, then 11 probes each naming what to COLLECT and the SIGNATURE in the numbers. Plus the extraction snippets, what measurement cannot see, and how to act on a finding. |
|
|
21
|
+
| [docs/reviewing.md](./docs/reviewing.md) | **Reviewing a screen you built — and the answer when one "looks bad"** — the other docs say what good looks like; this one says how to find out whether you achieved it. Run it BEFORE the deploy: a typecheck, a lint, a test and `app check` are all blind to how a screen reads. GATES that outrank every treatment question, then 11 probes each naming what to COLLECT and the SIGNATURE in the numbers. Plus the extraction snippets, what measurement cannot see, and how to act on a finding. |
|
|
22
22
|
| [docs/testing.md](./docs/testing.md) | Driving the kit in a browser — the three anatomies where the a11y tree says one thing and a driver must do another: a `PressDoor` row whose named button always intercepts pointer events (by design), portalled overlays that render at the top of the DOM, and custom pointer drag that `dragTo` cannot move. |
|
|
23
23
|
| [docs/templates.md](./docs/templates.md) | The map of `examples/tpl_*.tsx` — what shape each template solves and which to start from (copy + adapt, never import) — plus the record-surface composition rules (pipeline order, static shape, decision budget) and the ACTIVITY shape — a communications feed where the row's label is the GIST and the body varies by medium, one anatomy rather than a row type per kind. |
|
|
24
24
|
|
|
@@ -75,6 +75,30 @@ CURRENT major only — upgrading an app across majors is `MIGRATION.md`.
|
|
|
75
75
|
- **The kit's fonts/colors/icons ARE the design system** — never a custom font, icon set, or
|
|
76
76
|
hand-picked palette shade; color is `solid`/`tint`/`ramp` with ONE accent per screen.
|
|
77
77
|
→ [composition.md §"Color discipline"](./docs/composition.md).
|
|
78
|
+
- **An overlay paints in the order it was OPENED, and a scroller that swaps content opens the new
|
|
79
|
+
content at the top.** Compose overlays wherever you like — a `Dialog` written beside the
|
|
80
|
+
`Drawer` whose row opens it works, both ways round, and a `Dialog` opened from an open `Popover`
|
|
81
|
+
paints over its panel. An overlay of your OWN gets that for free by rendering its react-native
|
|
82
|
+
`Modal` **only while open** (`{open && <Modal visible …>}`); mounted while closed it takes its
|
|
83
|
+
body-level slot on the app's first paint and is then covered by anything opened later —
|
|
84
|
+
rendering and announcing perfectly, dead to every press. The z-index rungs beside the overlays
|
|
85
|
+
are published in `overlay_layer`, so never hand-pick a number to clear a Lotics overlay.
|
|
86
|
+
`DrawerScrollArea` / `DialogScrollArea` / `ModalBody` take a `scrollKey` and the routed
|
|
87
|
+
`Popover` takes the same seam off its route, so a swapped-in record opens at the top and the
|
|
88
|
+
reader returns to their place on the way back.
|
|
89
|
+
→ [composition.md §"Canvas & content column"](./docs/composition.md)
|
|
90
|
+
→ [catalog.md §"Overlays & navigation"](./docs/catalog.md)
|
|
91
|
+
- **ONE VALUE, ONE RENDERING, inside one card — the chart draws the entity its table draws.**
|
|
92
|
+
A value's filter renders it the way its cell does, and the same rule reaches the chart beside
|
|
93
|
+
the table: `StackedBarRow` and `BarChartItem` take a `leading` slot for the entity's own mark.
|
|
94
|
+
A LEGEND row is the exception — its subject is a category, and the swatch is that identity.
|
|
95
|
+
→ [composition.md §"A register that TRIAGES"](./docs/composition.md).
|
|
96
|
+
- **A row says which side HOLDS; `numberOfLines` handles the side that gives way.** The clamp
|
|
97
|
+
brings its own `minWidth: 0` + `flexShrink: 1` (a clamp that cannot shrink cannot clamp), so
|
|
98
|
+
what a call site still has to state is `flexShrink: 0` on the figure or control that must stay
|
|
99
|
+
whole. Same law on a categorical axis: the LABELS thin to fit, the bars never do.
|
|
100
|
+
→ [composition.md §"Typography"](./docs/composition.md),
|
|
101
|
+
[catalog.md §"Numbers & charts"](./docs/catalog.md).
|
|
78
102
|
- **Hand-typed type is off-system — and it always lands too small.** Every run of language is
|
|
79
103
|
`<Text size= weight= color=>` on the fixed rungs. **Grep the diff: `fontSize:` / `lineHeight:` /
|
|
80
104
|
`letterSpacing:` outside `src/` is a bug.** → [composition.md §"Typography"](./docs/composition.md).
|
|
@@ -90,6 +114,19 @@ CURRENT major only — upgrading an app across majors is `MIGRATION.md`.
|
|
|
90
114
|
(the tab stop, name, focus ring), never `PressableHighlight`, which wraps its children in the
|
|
91
115
|
button. `Table`/`TableRow` already does this.
|
|
92
116
|
→ [composition.md §"Row actions are always-visible siblings"](./docs/composition.md).
|
|
117
|
+
- **Piles you MOVE records between are a `Board`; bands of rows are not.** `Board` +
|
|
118
|
+
`BoardColumn` + `BoardCard` (`@lotics/ui/board`) — columns are one field's values, and a
|
|
119
|
+
card's `moves` drive a KEYBOARD menu first, with pointer drag layered on the same list.
|
|
120
|
+
`tpl_task_board` is a `DataGrid` of ROWS despite its name; a view built on it and labelled
|
|
121
|
+
Kanban is mislabelled.
|
|
122
|
+
→ [catalog.md §"Records in PILES you move them between"](./docs/catalog.md),
|
|
123
|
+
[templates.md §"`Board` vs `tpl_task_board`"](./docs/templates.md).
|
|
124
|
+
- **Money you are PRICING is `ChargeLines`; money you are READING is `Ledger`.** A charge is ONE
|
|
125
|
+
line — `quantity × unitPrice` reading across it and ENDING at the amount, never a derived figure
|
|
126
|
+
hung underneath — and the band's total lands on that same amount column. Below its fork width
|
|
127
|
+
the row takes two lines and the arithmetic RIGHT-ALIGNS to the band, so the amounts keep the
|
|
128
|
+
total's edge. → [catalog.md §"Money you are PRICING"](./docs/catalog.md),
|
|
129
|
+
[templates.md §"`tpl_money`"](./docs/templates.md).
|
|
93
130
|
- **A fact every row needs is a COLUMN; a fact one row needs expands in place.** `TableRow`'s
|
|
94
131
|
`detail` + `expanded` reveal the detail beneath the row; keep the drawer for a heavy form.
|
|
95
132
|
→ [composition.md §"No dead rows"](./docs/composition.md),
|
package/MIGRATION.md
CHANGED
|
@@ -4,6 +4,93 @@ Breaking changes, newest first — normally per major, plus the rare minor that
|
|
|
4
4
|
anyway (recorded under its exact version). The current contract lives in `AGENTS.md` + `docs/`;
|
|
5
5
|
this file exists only to move an app from one release to the next.
|
|
6
6
|
|
|
7
|
+
## 46.3.0
|
|
8
|
+
|
|
9
|
+
**A `Text` with `numberOfLines` now declares `flexShrink: 1` + `minWidth: 0` for you.**
|
|
10
|
+
A clamp that cannot shrink cannot clamp: on web a clamped `Text` is `white-space: nowrap`, so
|
|
11
|
+
`min-width: auto` floors it at the WHOLE string; on native a `Text` is `flexShrink: 0`. Either way
|
|
12
|
+
two clamped values on one row laid out at intrinsic width and ran off the frame. Both declarations
|
|
13
|
+
now come with the clamp, and both are no-ops until a row actually overflows.
|
|
14
|
+
|
|
15
|
+
Nothing's API changed, but **a layout default moved under every clamped `Text` in your app** — this
|
|
16
|
+
is the one 46.3.0 change to re-check by looking. Two shapes are worth a pass:
|
|
17
|
+
|
|
18
|
+
- A flex ROW where a clamped `Text` was the side that HELD and an unclamped sibling gave way. Both
|
|
19
|
+
can shrink now, so the wrong one may truncate.
|
|
20
|
+
- A flex COLUMN with a bounded height (a fixed-height card, a `maxHeight` panel). `flexShrink` acts
|
|
21
|
+
on the MAIN axis, so a clamped `Text` that used to hold its height can now be compressed.
|
|
22
|
+
|
|
23
|
+
The pair sits BEFORE your own `style`, so the site that must stay whole says so:
|
|
24
|
+
|
|
25
|
+
```diff
|
|
26
|
+
-<Text numberOfLines={1}>{amount}</Text>
|
|
27
|
+
+<Text numberOfLines={1} style={{ flexShrink: 0 }}>{amount}</Text>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
That is how a figure keeps winning over its label. `minWidth` is overridden the same way
|
|
31
|
+
(`style={{ minWidth: "auto" }}`) on the rare surface that wants the old min-content floor back.
|
|
32
|
+
|
|
33
|
+
**An overlay of your OWN must render its react-native `Modal` only while it is OPEN.**
|
|
34
|
+
`Dialog`, `Drawer`, `Modal` and `FileGalleryModal` now do — nothing in their API changed — and a
|
|
35
|
+
hand-rolled overlay outside the kit has to do the same, or it loses to them:
|
|
36
|
+
|
|
37
|
+
```diff
|
|
38
|
+
-<Modal visible={open} onRequestClose={close} transparent>
|
|
39
|
+
- …
|
|
40
|
+
-</Modal>
|
|
41
|
+
+{open && (
|
|
42
|
+
+ <Modal visible onRequestClose={close} transparent>
|
|
43
|
+
+ …
|
|
44
|
+
+ </Modal>
|
|
45
|
+
+)}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
react-native-web appends a `Modal`'s body-level `<div>` on its FIRST render and never re-orders
|
|
49
|
+
it, so an overlay mounted while closed takes its slot on the app's first paint and is then covered
|
|
50
|
+
by any overlay opened later — while rendering, reading and announcing perfectly, with only its
|
|
51
|
+
presses dead. Nothing is lost by the change: react-native-web renders a closed `Modal`'s children
|
|
52
|
+
as `null` already, so only the empty portal div was being held. A takeover with
|
|
53
|
+
`animationType="fade"` keeps its fade IN and now closes immediately, as `FileGalleryModal` always
|
|
54
|
+
has; state that must survive a close belongs outside the `Modal` element either way.
|
|
55
|
+
|
|
56
|
+
**The overlay z-index rungs are now published constants.** The numbers are unchanged for `Tooltip`
|
|
57
|
+
and `Alert` (`10000`) and for a toast (`10001`); **the skip link moved `10001` → `10002`**, off the
|
|
58
|
+
rung it was sharing with a toast. Read the rung instead of naming a number:
|
|
59
|
+
|
|
60
|
+
```diff
|
|
61
|
+
-import { View } from "react-native";
|
|
62
|
+
+import { NOTIFICATION_Z } from "@lotics/ui/overlay_layer";
|
|
63
|
+
…
|
|
64
|
+
- zIndex: 10001,
|
|
65
|
+
+ zIndex: NOTIFICATION_Z,
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The full table is in `docs/catalog.md` § Overlays & navigation: `OVERLAY_Z` (every overlay, and
|
|
69
|
+
`Popover`), `OVERLAY_Z_ABOVE` (tooltip/alert), `NOTIFICATION_Z` (a toast), `SKIP_LINK_Z`.
|
|
70
|
+
|
|
71
|
+
**`PageContent`'s title band is now a `PageHeader`**, so the two stop drifting. `title`,
|
|
72
|
+
`titleRight` and `description` are unchanged, and `PageHeader`'s `title` became optional — but the
|
|
73
|
+
band's rhythm is now `PageHeader`'s everywhere: a description sits **8px** under the title (it was
|
|
74
|
+
flush in `PageHeader`) and the band clears **16px** before the content (it was 24 in
|
|
75
|
+
`PageContent`). Nothing to change; worth one look at a page shell you care about.
|
|
76
|
+
|
|
77
|
+
**`useChangeSet`'s per-proposal default is a MAP, not a predicate.** `initial: "accepted"` /
|
|
78
|
+
`"pending"` are unchanged.
|
|
79
|
+
|
|
80
|
+
```diff
|
|
81
|
+
-useChangeSet(ids, { initial: (id) => (overwrites.has(id) ? "rejected" : "accepted") })
|
|
82
|
+
+const initial = useMemo(
|
|
83
|
+
+ () => new Map(rows.filter((r) => r.overwrites).map((r) => [r.id, "rejected" as const])),
|
|
84
|
+
+ [rows],
|
|
85
|
+
+);
|
|
86
|
+
+useChangeSet(ids, { initial })
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
An id the map does not name arrives `accepted`, as before. A value rather than a function is what
|
|
90
|
+
lets the default be an ordinary dependency: `status` and the group arrays derive from it together,
|
|
91
|
+
so `status` changes identity whenever a decision does — which a screen that memoizes its rows on
|
|
92
|
+
`review.status` needs, and which a predicate held out of the dependency lists could not give.
|
|
93
|
+
|
|
7
94
|
## 46.0.0
|
|
8
95
|
|
|
9
96
|
**`AgentRun` / `AgentProgress` drop `stepsLabel`, and the `agentRun` locale slice drops `steps`.**
|
package/docs/ai_patterns.md
CHANGED
|
@@ -379,6 +379,17 @@ alignment and rhythm.
|
|
|
379
379
|
`keptCount`/`total`/`settled`. `initial` defaults to **`accepted`** so an operator drops
|
|
380
380
|
exceptions instead of approving eight identical lines; pass `pending` when each change
|
|
381
381
|
genuinely deserves its own verdict, and gate the commit on `settled`.
|
|
382
|
+
- **A set that mixes KINDS defaults per kind: `initial` also takes a `Map<id, decision>`.** A
|
|
383
|
+
review usually holds both — filling a blank should arrive `accepted`, overwriting a value a
|
|
384
|
+
human already set should arrive `rejected`, so the destructive half is opt-in — and one
|
|
385
|
+
global default puts one of the two on the wrong footing. An id the map does not name arrives
|
|
386
|
+
`accepted`. Build it with a `useMemo` over the proposals your screen already holds, which is
|
|
387
|
+
what keeps the hook ignorant of what a proposal IS; a VALUE (rather than a predicate written
|
|
388
|
+
inline) is what lets `status` and the group arrays derive from it together, so `status`
|
|
389
|
+
changes identity the moment a decision moves and a screen memoizing its rows on it re-runs.
|
|
390
|
+
Deciding those rows by calling `reject()` from the result handler instead writes overrides
|
|
391
|
+
nobody made, and `undo` then hands the row back **accepted** — the destructive change
|
|
392
|
+
silently in, from the control that means "undo".
|
|
382
393
|
|
|
383
394
|
### The shapes, and what each one is
|
|
384
395
|
|
package/docs/catalog.md
CHANGED
|
@@ -151,8 +151,8 @@ reader must read WHOLE rather than recognise at a glance: a payment term, an add
|
|
|
151
151
|
Above 1 the resting box RESERVES that many lines — the full editor height, not the height this
|
|
152
152
|
particular value needs — so clicking in moves neither the field nor anything below it; Enter
|
|
153
153
|
then inserts a newline and the value commits on blur.
|
|
154
|
-
**`autoGrow`** makes that budget a MINIMUM and fits the value instead
|
|
155
|
-
[data_entry.md §Open prose grows](./data_entry.md). **`link`** marks a URL value with
|
|
154
|
+
**`autoGrow`** makes that budget a MINIMUM and fits the value instead, and a NOTE field always
|
|
155
|
+
takes it — see [data_entry.md §Open prose grows](./data_entry.md). **`link`** marks a URL value with
|
|
156
156
|
the navigation ink as a TREATMENT on the same single element — never a swapped-in
|
|
157
157
|
`TextLink`, which would reintroduce the sub-pixel jump this field stays one
|
|
158
158
|
`<input>` to avoid; pressing still edits, so pair it with an `Open` `InlineButton`
|
|
@@ -182,6 +182,33 @@ field rows lives in
|
|
|
182
182
|
`DetailTable` + `DetailRow`; the record's identity band is `RecordSummary`; its money
|
|
183
183
|
summary is `Ledger`.
|
|
184
184
|
|
|
185
|
+
### Money you are PRICING — the charge band
|
|
186
|
+
|
|
187
|
+
`ChargeLines` + `ChargeLine` (`@lotics/ui/charge_lines`) is the WRITE side of a record's money:
|
|
188
|
+
what it charges, priced per line, closed by its own total. `Ledger` is the READ side of the same
|
|
189
|
+
subject and the two are not interchangeable — a ledger has nothing to type into, and this has no
|
|
190
|
+
notion of adjustments or of three sides.
|
|
191
|
+
|
|
192
|
+
- **A charge is ONE line.** The arithmetic reads across it — `quantity × unitPrice`, ending at the
|
|
193
|
+
amount. Hanging the derived figure on a second line doubles every row's height and leaves the
|
|
194
|
+
answer in no column, so the total below closes nothing.
|
|
195
|
+
- **One money column.** Quantity, unit price and amount all right-align and the total lands on the
|
|
196
|
+
amount column, so the band adds up by eye. The typed figures right-align through
|
|
197
|
+
`InlineNumberInput`'s `align`; without it a typed price and a derived one drift apart, which is
|
|
198
|
+
probe 6 in [reviewing.md](./reviewing.md).
|
|
199
|
+
- **The total is the sum of what is on screen.** Never a figure computed off a different array: a
|
|
200
|
+
total that is not the sum of something visible is the one figure a reader cannot check.
|
|
201
|
+
- **The row's verb sits in a fixed slot**, so the money column does not shift between a row that
|
|
202
|
+
can be removed and one that cannot. `locked` turns the editors into values for a settled charge.
|
|
203
|
+
- **Below its fork width** the name takes its own line and the arithmetic sits under it — a
|
|
204
|
+
two-line row ON PURPOSE, measured on the band's own container because it lives in drawers as
|
|
205
|
+
often as on pages. The arithmetic RIGHT-ALIGNS to the band there: laid out from the left it
|
|
206
|
+
would land the amounts wherever the fixed slot widths happen to end, which coincides with the
|
|
207
|
+
closing total only when the container is exactly as wide as the row. The row's money verb moves
|
|
208
|
+
up to the name line in the fork, because the arithmetic line has no width to reserve for it.
|
|
209
|
+
- **The field names are locale-wired** — `LoticsLocale.chargeLines` gives the quantity, unit-price
|
|
210
|
+
and amount editors their screen-reader names, each qualified by the charge it belongs to.
|
|
211
|
+
|
|
185
212
|
### Tasks, to-dos, phased work — ONE compound, two axes
|
|
186
213
|
|
|
187
214
|
Anything a reader marks is `checklist` — `Checklist` + `ChecklistGroup` + `ChecklistItem`. Two
|
|
@@ -217,7 +244,9 @@ same record, so it follows automatically; for a surface whose SUBJECT is tasks s
|
|
|
217
244
|
|
|
218
245
|
### Tabular data — pick by SCALE + intent
|
|
219
246
|
|
|
220
|
-
Two columnar shapes, and the choice is about data size
|
|
247
|
+
Two columnar shapes, and the choice is about data size. (A third shape divides the same records
|
|
248
|
+
into **piles you move them between** — that is the board, below, and it is picked by INTENT, not
|
|
249
|
+
by size.)
|
|
221
250
|
|
|
222
251
|
- **High-volume register** (thousands+ you BROWSE) — `Table` (columns defined once; sortable
|
|
223
252
|
headers via `SortHeader`; paired with `Pagination`) + rows that either open a `Drawer` to
|
|
@@ -253,6 +282,56 @@ Two columnar shapes, and the choice is about data size:
|
|
|
253
282
|
grouping + pagination/infinite don't compose; use the register instead. Worked example:
|
|
254
283
|
[`tpl_task_board`](../examples/tpl_task_board.tsx) (a `CheckCircle` leading).
|
|
255
284
|
|
|
285
|
+
### Records in PILES you move them between — the column board
|
|
286
|
+
|
|
287
|
+
`Board` + `BoardColumn` + `BoardCard` (`@lotics/ui/board`). Columns are the values of ONE
|
|
288
|
+
field; cards are records; the act the shape exists for is sending a card to another column.
|
|
289
|
+
Reach for it when the reader ADVANCES work and the pile sizes are themselves the report — not
|
|
290
|
+
when they compare a row's measures (`DataGrid`) or browse thousands (`Table`).
|
|
291
|
+
|
|
292
|
+
Worked example: [`tpl_board`](../examples/tpl_board.tsx).
|
|
293
|
+
|
|
294
|
+
- **Order WITHIN a column is deliberately not offered.** The board moves a card BETWEEN piles;
|
|
295
|
+
it does not let you drag one above another in the same pile. Ordering that survives a reload
|
|
296
|
+
needs a persisted rank field on the record, and without one the reader arranges a pile, leaves,
|
|
297
|
+
and finds it rearranged — which is worse than never offering it. A pile is sorted by a column
|
|
298
|
+
the data already carries (a deadline, a priority), stated once. An earlier implementation shipped
|
|
299
|
+
an insert-at-index affordance with no rank behind it; it is gone.
|
|
300
|
+
|
|
301
|
+
- **A column states its identity and its SIZE separately.** `heading` is a node — the component
|
|
302
|
+
the value's data role owns (`OptionBadge` for a select, `MemberChip` for a person) — and
|
|
303
|
+
`count` is a PROP. Formatted into the heading the number becomes part of the column's NAME.
|
|
304
|
+
- **The move is keyboard-first.** A card's `moves` (`{ key, label }[]`) name its destinations,
|
|
305
|
+
and the card renders a focusable control whose menu items are those labels. Pointer drag is
|
|
306
|
+
layered on the SAME list: the grip drags, the drop resolves to the column under the pointer,
|
|
307
|
+
and a destination the menu would refuse is refused there too. Omit `moves` and the card
|
|
308
|
+
carries no move control — the honest rendering of a card that cannot go anywhere.
|
|
309
|
+
- **The board owns its horizontal scroll**, so the page body never scrolls sideways and the
|
|
310
|
+
shape survives 375. **Columns grow and the page scrolls** — a bounded board height is a number
|
|
311
|
+
nobody can derive, which is how `height: 560` gets hand-picked into an app.
|
|
312
|
+
- **A card's controls sit in the CARD's top-right corner, and they are always visible.** Pinned
|
|
313
|
+
to the card rather than carried by the title row, because a title that wraps to two lines
|
|
314
|
+
otherwise pushes them down and a control that moves with the length of the text beside it is one
|
|
315
|
+
the reader hunts for on every card. Always visible rather than revealed on hover: hover-reveal is
|
|
316
|
+
the exception a DENSE register earns, where forty controls at rest drown the rows — a board is
|
|
317
|
+
already sparse, and hiding the one act the shape exists for leaves a pointer user with a card
|
|
318
|
+
that looks inert. (Revealing on focus only is the same bug wearing a keyboard: the pointer, which
|
|
319
|
+
is how most people drive a board, never triggers it.)
|
|
320
|
+
- **The card face states the record; the MOVE MENU states the destinations.** A badge on the card
|
|
321
|
+
repeating the column it already sits in prints one fact twice. The same badge in the menu is new
|
|
322
|
+
information — it names where the card would GO, in that column's own vocabulary — so the menu
|
|
323
|
+
takes each destination's heading node and cannot drift from the pile it points at.
|
|
324
|
+
- **A card darkens under the pointer and its geometry does not move.** The wash is the card's own,
|
|
325
|
+
one neutral step; a border appearing on hover adds an edge to a surface already full of them.
|
|
326
|
+
- **A drop must not open the record.** The browser ends a drag with a press on the row the grip
|
|
327
|
+
sits in, and that row is a door — so without a guard every drag dumps the reader into a detail
|
|
328
|
+
surface they never asked for. Suppress the press for the tick after a drop. The case that
|
|
329
|
+
reproduces it is a SHORT drag, where the release lands back inside the card it started in.
|
|
330
|
+
- `BOARD_COLUMN_WIDTH` is the kit's column width. Do not re-pick it per app.
|
|
331
|
+
- Not the same thing as [`tpl_task_board`](../examples/tpl_task_board.tsx), whose name says
|
|
332
|
+
columns and whose body is a `DataGrid` of ROWS — see
|
|
333
|
+
[templates.md §"`Board` vs `tpl_task_board`"](./templates.md).
|
|
334
|
+
|
|
256
335
|
### Numbers & charts
|
|
257
336
|
|
|
258
337
|
`KPIStrip` (the dashboard stat band), `SummaryLine` (the light inline register/list summary
|
|
@@ -443,6 +522,10 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
443
522
|
numbers), `weight`, `color`, alignment, tabular numerals. **`leading="tight"`** sets the line
|
|
444
523
|
box for a STACKED PAIR rather than for prose — a subject over its supporting line, a value over
|
|
445
524
|
its annotation; see composition.md, which also covers the `gap` that goes with it.
|
|
525
|
+
**`numberOfLines`** clamps, and brings the `minWidth: 0` + `flexShrink: 1` that makes clamping
|
|
526
|
+
possible on a flex row — a clamp that cannot shrink cannot clamp. Both are no-ops until the row
|
|
527
|
+
overflows, and a `style` on the call site still wins, so a value that must never give way says
|
|
528
|
+
`flexShrink: 0` and keeps holding over its own label.
|
|
446
529
|
- **`eyebrow`** — `Eyebrow`: the small quiet label above or beside a VALUE (`xs` muted medium) —
|
|
447
530
|
a metric caption, a field name in a cell, an artifact tag. Takes `color` (for a VERDICT word
|
|
448
531
|
like "Mismatch") and `align`, and deliberately takes no `size`, `weight` or `transform`:
|
|
@@ -702,16 +785,21 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
702
785
|
useSeparator` inserts and what menus/popovers put between option groups.
|
|
703
786
|
- **`container`** — `Container`: centers content at a max width (`ContainerSize` sm|md|lg,
|
|
704
787
|
`CONTAINER_SIZES`).
|
|
705
|
-
- **`page_header`** — `PageHeader`: the page's title band
|
|
788
|
+
- **`page_header`** — `PageHeader`: the page's title band, and the ONE place that band is
|
|
789
|
+
defined — `PageContent`'s `title`/`titleRight`/`description` render one of these rather than a
|
|
790
|
+
second copy of the row. `actions` puts page-level CTAs on
|
|
706
791
|
the title row (right-aligned) and `trailing` puts a control immediately AFTER the title;
|
|
707
792
|
`left`/`right` form a separate nav row above. Split `trailing` from `actions` by what the
|
|
708
793
|
control acts on: `actions` do something to the page's CONTENT (create, sort, export),
|
|
709
794
|
`trailing` changes what is AROUND it (a side-panel toggle, a view switch). A panel toggle
|
|
710
795
|
filed under `actions` reads as a peer of "create one of these". Under a title too long for
|
|
711
796
|
the row the TITLE gives way and wraps while `trailing`
|
|
712
|
-
and `actions` keep their width.
|
|
797
|
+
and `actions` keep their width. **The band owns the space under itself** — 16px, whether it
|
|
798
|
+
is rendered on a hand-rolled screen or by `PageContent`; a shell adds nothing after it, or
|
|
799
|
+
the same band closes on two different rhythms depending on which shell a screen picked.
|
|
713
800
|
- **`page_content`** — `PageContent` + `PAGE_SIZES`: the page's padded, width-capped content
|
|
714
|
-
region — a centred column with optional `title`/`titleRight`/`description
|
|
801
|
+
region — a centred column with optional `title`/`titleRight`/`description` (which render a
|
|
802
|
+
`PageHeader`, so the band's row law and rhythm have one home), `header`/`footer`
|
|
715
803
|
slots and `fullscreen`. **Reach for it before hand-rolling a screen shell.** Its side
|
|
716
804
|
padding is `pagePad` (`@lotics/ui/spacing`), so a screen that
|
|
717
805
|
genuinely cannot use it — one with a rail, a side panel, or a scroller it must hold a ref to —
|
|
@@ -1145,7 +1233,9 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1145
1233
|
a list too small for its own row.
|
|
1146
1234
|
Pass `actions` UNCONDITIONALLY and `disabled` the verb when it has nothing to act on: a slot
|
|
1147
1235
|
that appears once the value is non-empty resizes the field as the user types.
|
|
1148
|
-
- **`inline_text_input`**, **`inline_number_input`** (`format` for currency/units
|
|
1236
|
+
- **`inline_text_input`**, **`inline_number_input`** (`format` for currency/units, `align="right"`
|
|
1237
|
+
to put the RESTING figure on its slot's far edge so a typed money column shares one edge —
|
|
1238
|
+
data_entry.md §Billing),
|
|
1149
1239
|
**`inline_select`**, **`inline_member_select`**, **`inline_date_picker`**
|
|
1150
1240
|
(`format="datetime"`, `optionalTime`; keyboard focus opens the TYPED segmented `DateField`
|
|
1151
1241
|
— locale field order, separator advances, Alt+ArrowDown floats the calendar; click still
|
|
@@ -1293,6 +1383,20 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1293
1383
|
line box, so the band is the same height either way and a loading record's header does not
|
|
1294
1384
|
move when the number lands. Still ONE number: a node is a stand-in for it, not a licence to
|
|
1295
1385
|
put a control in the slot.
|
|
1386
|
+
- **`charge_lines`** — `ChargeLines` + `ChargeLine`: the editable money band — what a record
|
|
1387
|
+
CHARGES, priced line by line, closed by its own total. The write side of `Ledger`'s subject,
|
|
1388
|
+
and not interchangeable with it: a ledger has nothing to type into and this has no notion of
|
|
1389
|
+
adjustments. A charge is ONE line — `quantity`/`unitPrice` derive the amount and the amount is
|
|
1390
|
+
never typed; leave both out and the `amount` itself becomes the editor (`amountActions` puts a
|
|
1391
|
+
list price one tap away, `unitPriceActions` a rate). Verbs sit BESIDE the field, never inside
|
|
1392
|
+
it, and the row's own verb (`action`, usually removal) sits in a slot held open on every row —
|
|
1393
|
+
both so the money column cannot move between rows. `extra` carries a second control belonging
|
|
1394
|
+
to the charge (how it was paid); `warning` names a problem on the line that has it; `locked`
|
|
1395
|
+
turns the editors into values for a settled band; `empty` speaks for a record nobody has priced
|
|
1396
|
+
yet. `formatMoney` is the BAND's, so two lines cannot disagree about a currency, and the `total`
|
|
1397
|
+
you pass must be the sum of the lines on screen. Below its fork width the row takes two lines
|
|
1398
|
+
with the arithmetic right-aligned to the band — see § "Money you are PRICING" for the full
|
|
1399
|
+
rules. Worked example: [`tpl_money`](../examples/tpl_money.tsx).
|
|
1296
1400
|
- **`ledger`** — `Ledger` + `LedgerGroup` + `LedgerBasis` + `LedgerRow` + `LedgerTotal` — the
|
|
1297
1401
|
record-level money list: every figure on ONE right-aligned tabular column, `peek` turns a row
|
|
1298
1402
|
into a pressable door floating its particulars in an anchored popover (put links INSIDE the
|
|
@@ -1505,6 +1609,13 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1505
1609
|
- **`data_grid`** — `DataGrid` + `gridRowStyle`: the inline-managed grouped table (see
|
|
1506
1610
|
[Tabular data](#tabular-data--pick-by-scale--intent)); `labels` localizes the sort-header
|
|
1507
1611
|
a11y via `SortHeaderLabels`.
|
|
1612
|
+
- **`board`** — `Board` + `BoardColumn` + `BoardCard` + `BoardMove` + `BOARD_COLUMN_WIDTH`:
|
|
1613
|
+
the column board — piles you MOVE records between (see
|
|
1614
|
+
[Records in PILES](#records-in-piles-you-move-them-between--the-column-board)). The board
|
|
1615
|
+
owns the horizontal scroll; a column takes a `heading` node, a `columnKey` and a `count`
|
|
1616
|
+
prop; a card takes `moves` + `onMove`, which drive the keyboard menu AND the pointer drop.
|
|
1617
|
+
Strings resolve through the `board` locale slice (`open` / `move` / `moveTo` / `empty`),
|
|
1618
|
+
overridable per card with `openLabel` / `moveLabel` and per column with `emptyMessage`.
|
|
1508
1619
|
- **`pagination`** — `Pagination` (+ `PaginationLabels`): the register's pager — a range and two
|
|
1509
1620
|
arrows, no page index. ONE per register, ABOVE the rows on the `SummaryLine` row; its range is
|
|
1510
1621
|
the register's count (pair with `Table` `counted`), and `onPageChange` owes a scroll reset the
|
|
@@ -1586,15 +1697,56 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1586
1697
|
|
|
1587
1698
|
### Overlays & navigation
|
|
1588
1699
|
|
|
1700
|
+
**Every overlay paints in the order it was OPENED**, so mount them wherever composition wants — a
|
|
1701
|
+
`Dialog` written beside the `Drawer` whose row opens it is the ordinary shape and it works; a
|
|
1702
|
+
drawer opened from a dialog covers the dialog, which is the same rule the other way round. There
|
|
1703
|
+
is no layer prop and nothing at the call site has to know the tree order. It covers every
|
|
1704
|
+
body-level surface the kit has, `Popover` included: a `Dialog` or a file-preview `Modal` opened
|
|
1705
|
+
from an open popover paints OVER the panel that opened it. `Alert` and `Tooltip` stay above every
|
|
1706
|
+
overlay at any depth.
|
|
1707
|
+
|
|
1708
|
+
**The one rule an overlay of your OWN has to follow: render the react-native `Modal` element only
|
|
1709
|
+
while it is open** (`{open && <Modal visible …>}`, or an early `return null`). react-native-web
|
|
1710
|
+
appends a `Modal`'s body-level `<div>` on its FIRST render and never re-orders it, so an overlay
|
|
1711
|
+
mounted while closed has already taken its slot before anything opened — and once two overlays tie
|
|
1712
|
+
at the same z-index, that slot is what decides. The symptom is nasty precisely because it is not
|
|
1713
|
+
visual: the covered surface renders, reads and announces perfectly, and only its presses do
|
|
1714
|
+
nothing. Nothing is lost by not mounting it — react-native-web renders a closed `Modal`'s children
|
|
1715
|
+
as `null` anyway, so only the empty portal div was ever being held, and anything that must survive
|
|
1716
|
+
a close (a router, a context, form state) belongs outside the `Modal` element regardless.
|
|
1717
|
+
|
|
1718
|
+
**The rungs beside the overlays are a published contract** (`overlay_layer`), because a surface
|
|
1719
|
+
OUTSIDE the kit that has to clear a Lotics overlay cannot do it with a hand-picked literal:
|
|
1720
|
+
|
|
1721
|
+
| z-index | what sits there |
|
|
1722
|
+
|---|---|
|
|
1723
|
+
| `9999` | every overlay, and `Popover` — `OVERLAY_Z` |
|
|
1724
|
+
| `10000` | `Tooltip`, `Alert` — `OVERLAY_Z_ABOVE` |
|
|
1725
|
+
| `10001` | a transient notification / toast — `NOTIFICATION_Z` |
|
|
1726
|
+
| `10002` | the skip link — `SKIP_LINK_Z` |
|
|
1727
|
+
|
|
1728
|
+
Overlays all share one rung on purpose: DOM order settles the tie and DOM order is open order.
|
|
1729
|
+
`Popover` is on it too — it is no `Modal`, but it portals into the nearest `PortalHost`, so one
|
|
1730
|
+
opened inside an overlay is already inside that overlay's stacking context and a page-level one is
|
|
1731
|
+
a child of the app root, which every overlay's body-level box follows.
|
|
1732
|
+
|
|
1589
1733
|
- **`portal`** — `PortalHost` + `Portal`: the floating-content mount point; the app root
|
|
1590
1734
|
needs ONE `PortalHost` or `Dialog`/`Popover`/`Tooltip`/`Alert`/`OptionList` cannot render.
|
|
1591
1735
|
- **`dialog`** — `Dialog` (+ `DialogHeader`/`DialogHeaderTitle`/`DialogFooter`, `useDialog`,
|
|
1592
1736
|
`useDialogNavigation`): the centered card over a scrim; BAKES a screen router in
|
|
1593
|
-
(`<Dialog><Screen route="">…` — see `screen_router`).
|
|
1737
|
+
(`<Dialog><Screen route="">…` — see `screen_router`). `DialogScrollArea` takes the same
|
|
1738
|
+
**`scrollKey`** seam as `DrawerScrollArea` for a pane that swaps its body in place; a dialog
|
|
1739
|
+
navigating between `Screen`s does not need it, because a stacked screen stays mounted and keeps
|
|
1740
|
+
its own offset.
|
|
1594
1741
|
- **`drawer`** — `Drawer` + `DrawerScrollArea` + `DrawerFooter`: the docked side panel with scrim; the
|
|
1595
1742
|
register row's edit surface. ONE standard width (600px) — pass `width` only for a genuinely
|
|
1596
1743
|
exceptional panel; full-width on small screens. `DrawerScrollArea` is the guttered content
|
|
1597
|
-
region — `DialogScrollArea`'s counterpart — so never hand-pad a drawer body.
|
|
1744
|
+
region — `DialogScrollArea`'s counterpart — so never hand-pad a drawer body. Give it a
|
|
1745
|
+
**`scrollKey`** (the open content's identity — `scrollKey={openChild?.id}`, where the undefined
|
|
1746
|
+
leg names the root content rather than opting out) on a drawer that SWAPS its body in place, so
|
|
1747
|
+
the new content opens at the top and the previous one's offset returns on the way back — the seam
|
|
1748
|
+
`DialogScrollArea` and `ModalBody` take too, and the one thing a React `key` on the scroller
|
|
1749
|
+
cannot buy, since that resets the parent's place along with the child's. It always
|
|
1598
1750
|
scrolls: a padded box around a `flex:1` scroller insets that scroller's viewport, ending the
|
|
1599
1751
|
list short of the panel with the last row clipped. Two bodies skip it and take the `Drawer`'s
|
|
1600
1752
|
bare slot: a FULL-BLEED one (a band spanning the panel, a record screen with its own gutters),
|
|
@@ -1605,7 +1757,8 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1605
1757
|
edge-to-edge takeover: an OPAQUE surface that COVERS THE WHOLE SCREEN, so unlike Dialog
|
|
1606
1758
|
(centered card WITH scrim) and Drawer (docked panel WITH scrim) there is nothing behind it
|
|
1607
1759
|
to dim — NO scrim, NO backdrop. Lays children as a flex column: a pinned ModalHeader
|
|
1608
|
-
(eyebrow/title + an actions slot + close), a flex:1 scrolling ModalBody
|
|
1760
|
+
(eyebrow/title + an actions slot + close), a flex:1 scrolling ModalBody (which takes the
|
|
1761
|
+
**`scrollKey`** seam — a wizard stepping between steps swaps its body in place), a pinned
|
|
1609
1762
|
ModalFooter (the commit bar, same chrome as DialogFooter/DrawerFooter). Reach for it for a
|
|
1610
1763
|
focused capture / multi-step wizard / a console the user steps INTO; pick Dialog when the
|
|
1611
1764
|
surface is a card the user can see context around.
|
|
@@ -1631,11 +1784,16 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1631
1784
|
The built-in scroller is **full-bleed horizontally** and re-insets its content by the same
|
|
1632
1785
|
amount, so a scrolling body reaches the panel's real edges while the text stays on the
|
|
1633
1786
|
same column the header and footer use. Nothing to opt into; `disableBodyScroll` consumers
|
|
1634
|
-
(which own their scroll) are untouched.
|
|
1787
|
+
(which own their scroll) are untouched. It also carries the **scroll seam** every scroller
|
|
1788
|
+
that swaps its body in place takes — keyed on the routed popover's current route, with no prop
|
|
1789
|
+
at call site, and inert in a popover that has no sub-screens.
|
|
1635
1790
|
- **`popover_nav`** — `usePopoverNav` + `PopoverScreen` + `PopoverNavHeader` — the popover's
|
|
1636
1791
|
built-in mini-router: EVERY `Popover` provides the nav context (`navigate(route)` pushes,
|
|
1637
1792
|
`goBack`, `currentRoute`, `canGoBack`; resets on close), `PopoverScreen route=""` is the
|
|
1638
|
-
root and screens render conditionally (unmounted when inactive
|
|
1793
|
+
root and screens render conditionally (unmounted when inactive, so a screen's own state does
|
|
1794
|
+
not survive the round trip — the panel's SCROLL does: the screens share `PopoverContent`'s one
|
|
1795
|
+
scroller, which takes the ROUTE as its seam key, so a sub-screen opens at the top with its
|
|
1796
|
+
`PopoverNavHeader` in view and the root list's offset returns on the way back),
|
|
1639
1797
|
`PopoverNavHeader` is the title row whose back chevron auto-appears while `canGoBack`
|
|
1640
1798
|
(`right` slot, `backLabel`). For a multi-screen menu inside ONE popover; route PATTERNS,
|
|
1641
1799
|
`params`, and stacked-alive screens are `screen_router`'s job.
|
|
@@ -1661,6 +1819,11 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1661
1819
|
content) — it is what `SectionHeadingTitle.info`/`CardHeaderTitle.info` render.
|
|
1662
1820
|
- **`alert`** — `Alert` (+ `AlertButton`/`AlertOptions`): the blocking confirm —
|
|
1663
1821
|
`Alert.alert(title, message, [{cancel}, {destructive}])`.
|
|
1822
|
+
- **`overlay_layer`** — `OVERLAY_Z` / `OVERLAY_Z_ABOVE` / `NOTIFICATION_Z` / `SKIP_LINK_Z`: the
|
|
1823
|
+
ONE paint-order table for everything that reaches `document.body` (the rungs are in the table
|
|
1824
|
+
above), and the file that states why open order works. Constants only — there is no layer to
|
|
1825
|
+
render and nothing to claim; an overlay of your own gets its place by mounting its react-native
|
|
1826
|
+
`Modal` only while it is open. Never write one of these numbers as a literal.
|
|
1664
1827
|
- **`overlay_scope`** — `isOverlayScopeActive` / `pushOverlayScope` / `useOverlayScope`: the
|
|
1665
1828
|
module-level open-overlay counter every overlay primitive reports into; the host's
|
|
1666
1829
|
shortcut registry reads it synchronously to floor page-level shortcuts while any overlay
|
|
@@ -1715,11 +1878,24 @@ component rather than showing it at zero.
|
|
|
1715
1878
|
it carries SHAPE (rising, spiky, flat) and no readable values, which is exactly what a
|
|
1716
1879
|
register wants. Reach for a real chart the moment someone needs to read a value off it.
|
|
1717
1880
|
- **`bar_chart`** / **`line_chart`** / **`pie_chart`** — `BarChart` / `LineChart` /
|
|
1718
|
-
`PieChart`: the canonical SVG chart set (no recharts).
|
|
1719
|
-
as the track fits and thins the rest
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1881
|
+
`PieChart`: the canonical SVG chart set (no recharts). **A categorical axis prints as many
|
|
1882
|
+
labels as the track fits and thins the rest** — `LineChart` and `BarChart`'s vertical
|
|
1883
|
+
orientation both, because it is the axis's rule and not one chart's. It is anchored on the LAST
|
|
1884
|
+
position: the newest reading is the one a reader looks up, and anchoring there is what keeps the
|
|
1885
|
+
spacing uniform; the first position is labelled only when it clears the same distance, so a
|
|
1886
|
+
series whose length does not divide evenly drops its opening label rather than crowding the one
|
|
1887
|
+
beside it. **The BARS never thin** — how many bars a card carries is a question about the data,
|
|
1888
|
+
never about how wide a label happens to be, and an unlabelled bar keeps its slot so the row
|
|
1889
|
+
above stays in line. **What thins is the whole annotation, label AND value together**, because a
|
|
1890
|
+
bar's name and its figure are one reading: a number under an unnamed bar is a quantity of nothing
|
|
1891
|
+
a reader can name, and the surviving annotation is widened across the room its dropped neighbours
|
|
1892
|
+
vacated — room the figure needs as much as the name does. So a DENSE vertical `BarChart` (more
|
|
1893
|
+
bars than `track ÷ 50px`: a 30-day daily series in a 600px card keeps roughly ten) shows the
|
|
1894
|
+
figures only for the bars it labels. When every bar's number has to be readable, that is a
|
|
1895
|
+
`Table` or the horizontal orientation, which gives each entity its own row and its own value
|
|
1896
|
+
column. `BarChart`'s horizontal orientation is one row per entity: **`labelWidth`**
|
|
1897
|
+
sizes the fixed label column (default 80, which fits a date and not a company name), and each
|
|
1898
|
+
item's **`leading`** slot carries that entity's own mark ahead of its name.
|
|
1723
1899
|
- **`progress_bar`** — `ProgressBar`: the determinate meter; `compact` = ONE row, track + a
|
|
1724
1900
|
plain sm tabular count beside it.
|
|
1725
1901
|
- **`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
|
|
@@ -1741,7 +1917,10 @@ component rather than showing it at zero.
|
|
|
1741
1917
|
cost make-up per product line. `series` (key/label/colour, one hue family per dimension)
|
|
1742
1918
|
drives a legend that is on by default; each `StackedBarRow` takes `label` + optional `meta`,
|
|
1743
1919
|
a pre-formatted headline `value` with its `valueTone`, and a `caption` sentence, so colour is
|
|
1744
|
-
never the only channel.
|
|
1920
|
+
never the only channel. **`leading`** is the entity's own mark (`BrandMark`, `Avatar`, a status
|
|
1921
|
+
dot), centred on the label's line box — a row's segment colours belong to the MEASURE, so unlike
|
|
1922
|
+
a legend row this one carries no identity until you give it one, and a chart beside a table over
|
|
1923
|
+
the same entities has to draw them the same way (composition.md § the register's own craft). Between `stacked_progress_bar` (one whole, own track) and `breakdown`
|
|
1745
1924
|
(one whole + ranked share rows beneath it): reach here the moment there are SEVERAL wholes to
|
|
1746
1925
|
compare.
|
|
1747
1926
|
- **`waterfall_chart`** — `WaterfallChart`: the BRIDGE — an opening level, the signed steps
|
|
@@ -1780,9 +1959,15 @@ component rather than showing it at zero.
|
|
|
1780
1959
|
cells, pressable, the value IN the cell) + `Matrix.Totals` (row + column + grand) +
|
|
1781
1960
|
`Matrix.Legend`. Pick over `Heatmap` when the NUMBER and totals matter. Figures are
|
|
1782
1961
|
RIGHT-aligned — they are `tabular`, and centring them defeats the aligned column — and row
|
|
1783
|
-
labels carry default ink, because a row's label is its identity, not chrome.
|
|
1962
|
+
labels carry default ink, because a row's label is its identity, not chrome. **The totals column
|
|
1963
|
+
takes one flex share like a data column**, because a total is the longest figure in its row: on a
|
|
1964
|
+
fixed width it is the FIRST thing to clip, and a clamped figure is whole in the DOM, so nothing
|
|
1965
|
+
but a rendered screen sees it. `MATRIX_COL_GAP` is
|
|
1784
1966
|
exported for a caller budgeting
|
|
1785
|
-
its own column widths.
|
|
1967
|
+
its own column widths. **A money matrix states its unit once** — in the card's
|
|
1968
|
+
`CardHeaderMeta`, cells bare (`formatCompactNumber`, not `formatMoney`), per
|
|
1969
|
+
[composition.md](./composition.md) §"A column states its unit ONCE": one currency glyph per cell
|
|
1970
|
+
is the same word fifty times and it is what pushes the totals column past its share.
|
|
1786
1971
|
- **`matrix_totals`** — the data layer for `Matrix`: `matrixTotals` (the cross-tab
|
|
1787
1972
|
aggregation) + `MatrixAxisItem` / `MatrixCellRef` / `MatrixTotalsResult`; React-free, so a
|
|
1788
1973
|
KPI can be driven off the same numbers the grid shows.
|
|
@@ -1993,6 +2178,10 @@ component rather than showing it at zero.
|
|
|
1993
2178
|
NODE** as well as a string, because a second line can itself be carrying a change (a file
|
|
1994
2179
|
being RECLASSIFIED is the same bytes filed differently — a `DiffValue` on the property); a
|
|
1995
2180
|
node renders outside the muted single-line `Text`, which would clip it and fight its colour.
|
|
2181
|
+
It also takes the **field annotations** (`description` / `warning` / `error`) — an expected
|
|
2182
|
+
document is a value the reader supplies, so a row whose document FAILS says so on the row, in
|
|
2183
|
+
the same block `DetailRow` renders; without it a fault could only be demoted into a callout
|
|
2184
|
+
about the whole section, which says something different.
|
|
1996
2185
|
Both `sm` variants share ONE beat (48px min + 6px vertical padding).
|
|
1997
2186
|
- **`file_rows`** — `FileRows`: batteries-included file list — row press → built-in gallery
|
|
1998
2187
|
+ a ⋯ Download/Open-external/Remove menu; composes `FileRow` + `ActionMenu` +
|
|
@@ -2205,7 +2394,14 @@ component rather than showing it at zero.
|
|
|
2205
2394
|
`ids` is normally a memo over query rows — a new array every render, which is the
|
|
2206
2395
|
`Maximum update depth exceeded` loop). `initial` defaults to `accepted` so the operator drops
|
|
2207
2396
|
exceptions instead of approving eight identical lines; pass `pending` when each change deserves
|
|
2208
|
-
its own verdict and gate the commit on `settled`.
|
|
2397
|
+
its own verdict and gate the commit on `settled`. **`initial` also takes a
|
|
2398
|
+
`ReadonlyMap<id, decision>`**, for a set that mixes kinds with different safe defaults —
|
|
2399
|
+
filling a blank arrives `accepted`, overwriting a value a human already set arrives `rejected`,
|
|
2400
|
+
so the destructive half is opt-in; an id the map does not name arrives `accepted`. Build it in a
|
|
2401
|
+
`useMemo` over your own rows: it is a VALUE, so `status` and the groups derive from it together
|
|
2402
|
+
and `status` changes identity whenever a decision does — which is what a screen memoizing its
|
|
2403
|
+
rows on `status` needs. Do not reach for `reject()` in the result handler instead: that writes
|
|
2404
|
+
overrides the operator never made, so `undo` hands the row back `accepted`.
|
|
2209
2405
|
- **`clarify`** — `Clarify` + `ClarifyOption`: the agent asks back — a borderless block
|
|
2210
2406
|
(the question text + a `ChoiceList`, no card wrapper; an optional muted `eyebrow` sits tight above
|
|
2211
2407
|
the question — e.g. a wizard's "1 / 3"). `ClarifyOption` requires
|