@lotics/ui 45.10.0 → 46.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +70 -137
- package/MIGRATION.md +27 -0
- package/docs/ai_patterns.md +166 -160
- package/docs/catalog.md +223 -287
- package/docs/composition.md +394 -518
- package/docs/data_entry.md +109 -155
- package/docs/reviewing.md +39 -55
- package/docs/templates.md +459 -423
- package/docs/testing.md +3 -7
- package/package.json +1 -1
- package/src/agent_progress.tsx +5 -4
- package/src/agent_run.tsx +218 -44
- package/src/agent_run_pane.tsx +5 -0
- package/src/agent_transform.ts +34 -0
- package/src/locale.tsx +3 -7
- package/src/pressable_row.tsx +7 -5
- package/src/table.tsx +15 -14
package/docs/catalog.md
CHANGED
|
@@ -3,13 +3,23 @@
|
|
|
3
3
|
The complete inventory of the kit, in two passes: **Reach by role** maps each data role /
|
|
4
4
|
job (actions, members, selects, dates, files, money, status, …) to the ONE canonical
|
|
5
5
|
component, and the **Full import inventory** lists every `@lotics/ui/<module>` entry point
|
|
6
|
-
with its purpose. Read this before building any screen — reuse first
|
|
7
|
-
|
|
6
|
+
with its purpose. Read this before building any screen — reuse first, and never hand-roll
|
|
7
|
+
something listed here. Absence is the weaker signal: this file is a hand-written index of
|
|
8
|
+
`src/`, not `src/` itself, so an entry can be missing from a kit that ships the component —
|
|
9
|
+
the first bullet below is what to do about that. The
|
|
8
10
|
composition patterns (data entry, AI review, templates) live in the sibling area docs
|
|
9
11
|
indexed in [AGENTS.md](../AGENTS.md).
|
|
10
12
|
|
|
11
13
|
## How to read a component's API
|
|
12
14
|
|
|
15
|
+
- **Reuse first — and this catalog can LAG `src/`.** The kit's inventory is `src/`; this file
|
|
16
|
+
is a hand-written index of it, so an export lands in `src/` and in `package.json`'s `exports`
|
|
17
|
+
in one edit and gets its entry here in another. Before hand-rolling ANY capability, list and
|
|
18
|
+
grep the shipped sources — `ls node_modules/@lotics/ui/src/` — as well as reading this file.
|
|
19
|
+
A component in `src/` and missing here is a DOC BUG: from inside the kit it is fixed in the
|
|
20
|
+
same change, and from an app it is a reason to import the module anyway, never a licence to
|
|
21
|
+
hand-roll a second copy of what the kit already ships. Only the converse is absolute —
|
|
22
|
+
everything listed here exists, and what is listed here is never hand-rolled.
|
|
13
23
|
- **Compose with React Native primitives**: `View` / `ScrollView` from `react-native` (never
|
|
14
24
|
`div`/`span`), style with RN style objects (not CSS), and render every string through the `Text`
|
|
15
25
|
primitive — the kit renders on web **and** native.
|
|
@@ -47,9 +57,7 @@ in place), `Chip` (dismissible facet chip), `CopyButton`
|
|
|
47
57
|
A button is
|
|
48
58
|
never a raw `Pressable`. For a link OUT (a URL / record / document) use `Link` — fixed
|
|
49
59
|
underline+blue + `role="link"`, the destination signal (`onPress` only — the consumer wires
|
|
50
|
-
the opener, e.g. the app SDK's `openExternal`)
|
|
51
|
-
colour-configurable underlined marker. Underlined text GOES somewhere or REVEALS something and
|
|
52
|
-
the ink says which — anything that MUTATES carries a control surface instead.
|
|
60
|
+
the opener, e.g. the app SDK's `openExternal`).
|
|
53
61
|
|
|
54
62
|
### Pick from a list
|
|
55
63
|
|
|
@@ -80,28 +88,23 @@ renders these per option.)
|
|
|
80
88
|
|
|
81
89
|
**To answer "who IS this?" — `MemberPeek`**, the same chip made a door onto a
|
|
82
90
|
`MemberProfileCard`: a 72px avatar, what they sign in as, their role, their teams and when they
|
|
83
|
-
joined. The
|
|
84
|
-
|
|
91
|
+
joined. The trigger takes `Peek`'s `variant="control"` there, so the chip seats in the 40px
|
|
92
|
+
band instead of under-filling it — there is no `marker` prop. Feed it
|
|
85
93
|
a member (an app's `ResolvedMember` already carries `groups`); `role` arrives ALREADY
|
|
86
|
-
TRANSLATED, never a raw enum
|
|
94
|
+
TRANSLATED, never a raw enum. Use `MemberProfileCard`
|
|
87
95
|
alone wherever the card is not behind a press (a profile header, a drawer).
|
|
88
96
|
|
|
89
97
|
**Where a peek must NOT go**, both cases being one rule — *two destinations behind one
|
|
90
|
-
object*: a PICKER OPTION
|
|
91
|
-
|
|
92
|
-
PRESSES (the row opens the record; a chip inside it would open something else four pixels
|
|
93
|
-
away). Those keep the plain `MemberChip`. A peek belongs where nothing else on the line
|
|
98
|
+
object*: a PICKER OPTION, and a ROW THAT ALREADY PRESSES. Those keep the plain `MemberChip`. A
|
|
99
|
+
peek belongs where nothing else on the line
|
|
94
100
|
presses — a person named in prose, a static header, a detail row, a log whose rows do not
|
|
95
101
|
navigate. **Judge the container, not its look:** a `DataGrid` group band names a person and
|
|
96
102
|
reads inert, but it is a button that collapses the group, so a peek there nests a button in a
|
|
97
|
-
button. A fully interactive screen often has NO valid spot
|
|
98
|
-
names a person three times (collapsing band, inline editor, picker option) and correctly
|
|
99
|
-
peeks on none of them.
|
|
103
|
+
button. A fully interactive screen often has NO valid spot.
|
|
100
104
|
|
|
101
105
|
**`groups` distinguishes absent from empty, and the card renders the difference**: omitted
|
|
102
106
|
drops the row (this caller was never told — a public-app response, an older server), `[]`
|
|
103
|
-
keeps it and says "None".
|
|
104
|
-
on no team.
|
|
107
|
+
keeps it and says "None".
|
|
105
108
|
|
|
106
109
|
**Size is a RUNG, never a pixel count** — `sm | md | lg | xl` = 24 / 28 / 40 / 72
|
|
107
110
|
(`@lotics/ui/avatar_size`, shared by `Avatar`, `MemberChip` and `GroupAvatar`). Default `md`.
|
|
@@ -111,17 +114,14 @@ Pick by density:
|
|
|
111
114
|
|
|
112
115
|
**Both middle rungs quote `control_surface`, and which one you want depends on whether the
|
|
113
116
|
avatar is IN a control or IS one.** `md` is `CONTROL_CONTENT_HEIGHT` (28) — the largest rung a
|
|
114
|
-
40px control band seats without growing
|
|
115
|
-
already used to sit inside a field. An avatar is the tallest thing in whatever row holds it, so
|
|
116
|
-
a rung that overshoots doesn't look slightly big, it RESIZES the control. Put nothing taller
|
|
117
|
+
40px control band seats without growing. Put nothing taller
|
|
117
118
|
than `md` in a control. `lg` is `CONTROL_HEIGHT` (40) — the avatar that stands in a band on its
|
|
118
|
-
own
|
|
119
|
-
belongs in a row that sets its own height.
|
|
119
|
+
own. Anything bigger belongs in a row that sets its own height.
|
|
120
120
|
|
|
121
121
|
**`sm` shows ONE initial; every larger rung shows two** — two letters are not legible at 24px.
|
|
122
|
-
That is the practical reason not to reach for `sm` by reflex:
|
|
123
|
-
|
|
124
|
-
|
|
122
|
+
That is the practical reason not to reach for `sm` by reflex: a register that must identify
|
|
123
|
+
someone wants `md`. Initials are the first and **last** word of the name — "Vũ Thị Lan" → VL,
|
|
124
|
+
"Mary Jane
|
|
125
125
|
Watson" → MW — because a Vietnamese name's first two words are the family and middle names
|
|
126
126
|
that a whole office shares.
|
|
127
127
|
|
|
@@ -150,9 +150,7 @@ The `Inline*` family: `InlineTextInput` (**`numberOfLines`** — the line budget
|
|
|
150
150
|
reader must read WHOLE rather than recognise at a glance: a payment term, an address, a clause.
|
|
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
|
-
then inserts a newline and the value commits on blur.
|
|
154
|
-
goes multiline once focused hides the text from the reader who never clicks, and one that
|
|
155
|
-
reserves less than it opens to shoves the rest of the form down the page on every click.
|
|
153
|
+
then inserts a newline and the value commits on blur.
|
|
156
154
|
**`autoGrow`** makes that budget a MINIMUM and fits the value instead — see
|
|
157
155
|
[data_entry.md §Open prose grows](./data_entry.md). **`link`** marks a URL value with
|
|
158
156
|
the navigation ink as a TREATMENT on the same single element — never a swapped-in
|
|
@@ -167,20 +165,16 @@ is mounted at rest and merely gains a draft on focus, so focusing moves nothing
|
|
|
167
165
|
the prose stays SELECTABLE rather than sitting under a button. Its toolbar is off for
|
|
168
166
|
that reason — a band that appears with the edit pushes everything under it down — and it
|
|
169
167
|
passes its **`variant`** THROUGH to the editor, which is what actually draws the box:
|
|
170
|
-
the frame paints no surface here
|
|
171
|
-
every markdown field wore the document sheet, 4px right of the plain-text field above it.
|
|
168
|
+
the frame paints no surface here.
|
|
172
169
|
**`numberOfLines`** is the reserve, as everywhere else in the family); a
|
|
173
170
|
READ-ONLY field in that same column uses `InlineStatic` (matches the editor box exactly, no
|
|
174
171
|
input chrome, so it aligns pixel-for-pixel). **`MarkdownEditor`** is that editor standalone — a WYSIWYG whose value stays a plain
|
|
175
|
-
markdown string, with a formatting toolbar as a SIBLING of the field
|
|
176
|
-
encloses is the document; the controls that act on it sit outside it). Web is
|
|
172
|
+
markdown string, with a formatting toolbar as a SIBLING of the field. Web is
|
|
177
173
|
ProseMirror via `@lotics/markdown-editor`; native falls back to raw-text editing, and
|
|
178
174
|
the platform-neutral `MarkdownEditorProps` is what stops the two drifting. **`variant`**
|
|
179
175
|
is `"document"` (the default soft sheet) or the two FIELD states, `"framed"` / `"bare"`,
|
|
180
176
|
DERIVED from `InlineEditVariant` so they cannot drift from the rest of the family. A field
|
|
181
|
-
takes the control's corner and the control's text inset instead of the sheet's.
|
|
182
|
-
is NAMED rather than left as absence: an optional prop whose omission is a distinct third
|
|
183
|
-
mode reads exactly backwards to anyone who knows `TextInputField.variant`. Wrap it in a
|
|
177
|
+
takes the control's corner and the control's text inset instead of the sheet's. Wrap it in a
|
|
184
178
|
label with **`FormMarkdownEditor`**. Pass **`accessibilityLabel`** whenever the visible
|
|
185
179
|
label is a sibling rather than a wrapping `FormField` — a `DetailRow`, an inline
|
|
186
180
|
editor's frame — or the editable surface is an unnamed text box. A stack of labelled
|
|
@@ -198,13 +192,9 @@ axes carry every shape it takes, and BOTH are decisions about meaning, not looks
|
|
|
198
192
|
have.
|
|
199
193
|
- **`mark`** — does ticking mean DONE or CHOSEN? `check` is a ring, `select` is the square box.
|
|
200
194
|
Reach for the square whenever the row is PICKED rather than finished — which forms to produce,
|
|
201
|
-
which records to export.
|
|
202
|
-
prevent: a reader who has learned that a filled ring means "done" reads a ticked pick-list as
|
|
203
|
-
work already carried out, and nothing on screen corrects them. (The shapes are 24px either way
|
|
195
|
+
which records to export. (The shapes are 24px either way
|
|
204
196
|
and share one gutter, so a surface swaps between them without moving anything.)
|
|
205
197
|
|
|
206
|
-
A record's Progress and a desk's to-do list are the same component two booleans apart.
|
|
207
|
-
|
|
208
198
|
**A row can carry a DECIDED outcome instead of a tick — `ChecklistItem.verdict` (`"pass"` /
|
|
209
199
|
`"fail"`).** This is a per-ROW axis, not a list-level one: a QC gate, a reconciliation match,
|
|
210
200
|
any row whose mark means an inspection SETTLED rather than work getting done. `fail` renders in
|
|
@@ -216,21 +206,12 @@ no-op (withheld internally), because neither of the toggle-capable marks (`Check
|
|
|
216
206
|
pass/fail should drive that from its own control, then set `verdict` from the result, never
|
|
217
207
|
reach for `done`+`onToggle` on the same row a verdict already settled.
|
|
218
208
|
|
|
219
|
-
**A row carries its TITLE and its own stamp, and nothing else.** A checklist REPORTS: it does not
|
|
220
|
-
collect, and it does not PREVIEW. A field some section owns — an assignee, an address, a portal
|
|
221
|
-
login — is NAMED as a gap by a `ChecklistNote` whose `action` jumps to that section, never edited
|
|
222
|
-
in the row (two edit surfaces for one fact) and never rendered as a value (an avatar or badge is
|
|
223
|
-
a copy that has to be kept in step). The exceptions are narrow and stated: `ChecklistItem.trailing`
|
|
224
|
-
for the row's OWN stamp (a tick writes today, so a run filled in after the fact needs the
|
|
225
|
-
correction in reach), and `ChecklistField` for a value with NO other home.
|
|
226
|
-
|
|
227
209
|
**A list you scan DOWN columns — the same four values compared across twenty rows — is a
|
|
228
210
|
`Table`.** Grouping, filters and a per-row register belong there.
|
|
229
211
|
|
|
230
212
|
**A record's work state is a connected checklist** ([`tpl_record`](../examples/tpl_record.tsx)
|
|
231
|
-
§ Progress), where the desks are `ChecklistGroup`s and the milestones are rows. A
|
|
232
|
-
|
|
233
|
-
put the CTA a section away from the state that gated it. A register's workspace drawer renders the
|
|
213
|
+
§ Progress), where the desks are `ChecklistGroup`s and the milestones are rows. A register's
|
|
214
|
+
workspace drawer renders the
|
|
234
215
|
same record, so it follows automatically; for a surface whose SUBJECT is tasks see
|
|
235
216
|
[`tpl_task_board`](../examples/tpl_task_board.tsx).
|
|
236
217
|
|
|
@@ -259,16 +240,15 @@ Two columnar shapes, and the choice is about data size:
|
|
|
259
240
|
heading, so digits line up under their label; **`DataGridColumn.footer` gives that column its
|
|
260
241
|
cell in the group's footer row** (a subtotal, a resolved measure) — the grid lays it out in
|
|
261
242
|
the column's own box, so a total can never drift from what it sums and no consumer rebuilds
|
|
243
|
+
the row. `renderGroupFooter` remains for content that does NOT sit in a column (a per-group
|
|
244
|
+
add row). The grid takes exactly its container's width — no outdent to fight, so it needs no
|
|
245
|
+
scroller unless YOU add one.
|
|
262
246
|
**`DataGrid` SHEDS NOTHING at narrow widths, and that is deliberate** — every column is a
|
|
263
247
|
measure you came to compare, unlike `Table`'s register of objects where the row is an index
|
|
264
248
|
and the record holds the rest. So it has no `priority` and no stacked mode: fixed columns keep
|
|
265
249
|
their widths, the flexible column holds a `FLEX_MIN_WIDTH` floor, and past that the grid
|
|
266
|
-
OVERFLOWS. A narrow container must scroll it horizontally
|
|
267
|
-
|
|
268
|
-
rendered rows of unlabelled numbers, silently. If you want columns to drop, you want `Table`.
|
|
269
|
-
the row. `renderGroupFooter` remains for content that does NOT sit in a column (a per-group
|
|
270
|
-
add row). The grid takes exactly its container's width — no outdent to fight, so it needs no
|
|
271
|
-
scroller unless YOU add one. It renders ALL rows (no
|
|
250
|
+
OVERFLOWS. A narrow container must scroll it horizontally. If you want columns to drop, you
|
|
251
|
+
want `Table`. It renders ALL rows (no
|
|
272
252
|
virtualization), so it's only for sets small enough to hold in view — at 10k+ it lags, and
|
|
273
253
|
grouping + pagination/infinite don't compose; use the register instead. Worked example:
|
|
274
254
|
[`tpl_task_board`](../examples/tpl_task_board.tsx) (a `CheckCircle` leading).
|
|
@@ -279,10 +259,9 @@ Two columnar shapes, and the choice is about data size:
|
|
|
279
259
|
— below the toolbar, from the filtered rows), `KPICard` / `Metric` (headline figures),
|
|
280
260
|
`TrendChip` (delta), `Sparkline`, `BarChart` / `LineChart` / `PieChart` (the canonical SVG
|
|
281
261
|
set — no recharts), `ProgressRing`, `ProgressBar` (its `compact` prop = ONE row, track + a
|
|
282
|
-
plain sm tabular count beside it — the cell/heading/peek-trigger meter
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
tells the reader they are exactly at the limit. Numbers format in the reader's locale, so
|
|
262
|
+
plain sm tabular count beside it — the cell/heading/peek-trigger meter. **The track clamps at
|
|
263
|
+
100%, the caption does not** — over its max it reads `2,100 / 2,000 (105%)`. Numbers format in the
|
|
264
|
+
reader's locale, so
|
|
286
265
|
never hand-format the value you pass in — when display precision differs from the true value
|
|
287
266
|
(whole credits off a fractional balance), `formatValue` reshapes the caption text and leaves
|
|
288
267
|
the fill and the percentage exact) / `StackedProgressBar` (ONE whole split across segments,
|
|
@@ -294,12 +273,12 @@ stacked bar + ranked share rows, pressable to drill; `maxRows` folds the long ta
|
|
|
294
273
|
"Show N more" toggle — `labels` to localize — so several facet cards align to one height in
|
|
295
274
|
a row), `Funnel` (a CONVERSION funnel — ordered stages as bars that NARROW; the step
|
|
296
275
|
conversion rate is the HEADLINE (a bold aligned row across the top, the first stage = the
|
|
297
|
-
100% baseline), the count is the supporting figure below — the
|
|
298
|
-
|
|
276
|
+
100% baseline), the count is the supporting figure below — never pin the rate to the fill height.
|
|
277
|
+
`orientation` vertical columns |
|
|
299
278
|
horizontal bars; pass `onSelect`+`selectedKey` to make the bars press-to-drill (the selected
|
|
300
279
|
stays solid, others dim — the caller renders the records). The subset/drop-off sibling of
|
|
301
280
|
`StackedProgressBar` — nested cohorts that shrink "calls → connected → won", NOT a whole
|
|
302
|
-
split across stages
|
|
281
|
+
split across stages), `StatusGrid` + `StatusLegend`, `Heatmap`
|
|
303
282
|
(density: colour-only, "where does it cluster"), `Matrix` (the PIVOT cross-tab: the NUMBER
|
|
304
283
|
in each cell — optionally a heat wash behind it — plus row/column/grand totals; press a cell
|
|
305
284
|
to drill).
|
|
@@ -329,11 +308,8 @@ action/badge/unit), `ActionMenu` (⋯), `FloatingActionBar` (bulk-select bar).
|
|
|
329
308
|
|
|
330
309
|
A DESIGNED row (your own pile, card, or panel) that presses open AND carries its own
|
|
331
310
|
controls is `PressableRow` + **`PressDoor`** — never `PressableHighlight`, which IS a
|
|
332
|
-
button and wraps its children (a button must not contain interactive descendants).
|
|
333
|
-
|
|
334
|
-
accessible name, and focus ring; lift the content above it with `zIndex: 1`. `Table`/
|
|
335
|
-
`TableRow` does exactly this internally, so a columnar register needs no assembly —
|
|
336
|
-
reach for `PressDoor` only outside it.
|
|
311
|
+
button and wraps its children (a button must not contain interactive descendants). `Table`/
|
|
312
|
+
`TableRow` does exactly this internally, so a columnar register needs no assembly.
|
|
337
313
|
|
|
338
314
|
### Filters & view controls
|
|
339
315
|
|
|
@@ -366,11 +342,9 @@ into a list *and* a grid is the mistake — it gives the reader two places to lo
|
|
|
366
342
|
thing. The minority is not stranded by this: a non-image tile in a grid renders a `DocumentCard`
|
|
367
343
|
printing its filename, and a photo in a list keeps its full name plus a meta line and is ONE tap
|
|
368
344
|
from the picture, because a `FileRows` row press opens the built-in gallery. (The badge is
|
|
369
|
-
MIME-driven, so that row shows a generic image mark, not the shot itself
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
that arrived on the record, and the hand-over photos taken against it — not because one set was
|
|
373
|
-
split by type.
|
|
345
|
+
MIME-driven, so that row shows a generic image mark, not the shot itself.) A screen showing
|
|
346
|
+
BOTH surfaces (`tpl_record`) does so because it holds two distinct SETS the reader asks
|
|
347
|
+
different questions of, not because one set was split by type.
|
|
374
348
|
|
|
375
349
|
`FilesEditor` (THE attachment surface: an upload-aware grid whose bar you COMPOSE — the root
|
|
376
350
|
owns selection + gallery + confirmed remove, the bar pieces and any HOST verb go below it via
|
|
@@ -392,8 +366,7 @@ action / remove; `onPress` makes the whole row a pressable door, `trailing` stay
|
|
|
392
366
|
independently-pressable sibling; for checklists & readable lists. **`size`** — `sm` (default)
|
|
393
367
|
is the compact attachment line for many files scanned as a list; **`md` is the document-desk
|
|
394
368
|
row** (taller badge, `ListItem` height) for the FEW rows that ARE the section's subject — an
|
|
395
|
-
expected-document checklist, a slot a button fills
|
|
396
|
-
appendix to the page rather than its point), `FileBadge` (the two-tone
|
|
369
|
+
expected-document checklist, a slot a button fills), `FileBadge` (the two-tone
|
|
397
370
|
type mark), `FilePreview` / `FileGalleryModal`, `ImageGallery`; picking is `pickFiles`
|
|
398
371
|
(`@lotics/ui/file_picker` — opens the browser picker and resolves the chosen `File[]`, the
|
|
399
372
|
imperative half behind every Add-file CTA); for gated CRUD compose locally with
|
|
@@ -426,20 +399,14 @@ that same line, over the `readings` stacked ONE PER LINE — each `source value`
|
|
|
426
399
|
joining them (a middot claims a relation it will not name, and is the one mark a screen reader
|
|
427
400
|
drops), both kept on
|
|
428
401
|
screen with NEITHER marked as the wrong one, since a finding reports that two sources disagree
|
|
429
|
-
and which is right is the reader's call. `detail` is the optional consequence.
|
|
430
|
-
a stack gets severity down the left edge and magnitude down the right. The readings ARE the
|
|
402
|
+
and which is right is the reader's call. `detail` is the optional consequence. The readings ARE the
|
|
431
403
|
provenance, so there are no `Sources` chips under it repeating the same names. The children slot
|
|
432
404
|
composes ANY visual result — a compact `Table` for per-line detail (danger color on the offending cells),
|
|
433
405
|
`ProgressBar` for consumption-toward-a-cliff (free time, credit), dot `Badge`s for a
|
|
434
406
|
present/missing checklist, `Confidence` for judgment calls. Display-only — it informs the
|
|
435
407
|
verdict the host records; `finding` locale slice). `ApprovalPrompt` (the surface that ANSWERS
|
|
436
|
-
an agent's approval-gated action
|
|
437
|
-
|
|
438
|
-
optional `children` input summary; slot it where the composer sits while the run is parked;
|
|
439
|
-
`approvalPrompt` locale slice), `MessageActions` (THE single shared per-message chrome bar for a
|
|
440
|
-
chat transcript — copy with owned copied-feedback, regenerate, edit, and the branch/version
|
|
441
|
-
pager as an internal part; each action renders only when its handler is given, so the assistant
|
|
442
|
-
bar and the user bar are one component with different handlers; `messageActions` locale slice).
|
|
408
|
+
an agent's approval-gated action), `MessageActions` (THE single shared per-message chrome bar
|
|
409
|
+
for a chat transcript).
|
|
443
410
|
|
|
444
411
|
---
|
|
445
412
|
|
|
@@ -452,15 +419,14 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
452
419
|
|
|
453
420
|
- **`vite`** — `loticsOptimizeDeps`: the canonical `optimizeDeps.include` list a custom-code
|
|
454
421
|
app's `vite.config.ts` must pre-bundle for `lotics app dev` to render (RN-ecosystem + markdown
|
|
455
|
-
CJS-interop; dev-only — the prod rollup build resolves the interop without it).
|
|
456
|
-
|
|
457
|
-
the scaffold's `vite.config.ts` imports it instead of hardcoding the list:
|
|
422
|
+
CJS-interop; dev-only — the prod rollup build resolves the interop without it). The
|
|
423
|
+
scaffold's `vite.config.ts` imports it instead of hardcoding the list:
|
|
458
424
|
`import { loticsOptimizeDeps } from "@lotics/ui/vite"` → `optimizeDeps: { include: loticsOptimizeDeps }`.
|
|
459
425
|
To add app-specific entries, spread: `include: [...loticsOptimizeDeps, "my-dep"]`. A Node-loadable
|
|
460
426
|
`.mjs` leaf (a Vite config can't import a `.ts` from `node_modules`), zero-import by contract.
|
|
461
427
|
- **`vite`** — `loticsResolve()`: the whole `resolve` block, for the same reason and on the same
|
|
462
|
-
terms — `resolve: loticsResolve()`.
|
|
463
|
-
|
|
428
|
+
terms — `resolve: loticsResolve()`. The entries: the `react-native` → `react-native-web` alias
|
|
429
|
+
(**load-bearing** — without it the RN
|
|
464
430
|
primitives have no web implementation and the app does not build), `.web.tsx`-first `extensions`
|
|
465
431
|
(per-target variants win; `.mjs`/`.mts` kept for parity with Vite's default resolver), `dedupe`
|
|
466
432
|
for React + RN-Web (the kit is consumed across many subpaths, and a second React copy in one
|
|
@@ -468,8 +434,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
468
434
|
read at call time so it lasts exactly one command. To add your own alias, spread:
|
|
469
435
|
`const base = loticsResolve(); resolve: { ...base, alias: [...base.alias, entry] }`.
|
|
470
436
|
`lotics app codegen` writes the matching `paths` into the app's `.lotics/tsconfig.link.json`,
|
|
471
|
-
so `tsc`, vitest, eslint and the editor resolve the same kit Vite does
|
|
472
|
-
Vite-only trick that leaves every other tool checking the published package.
|
|
437
|
+
so `tsc`, vitest, eslint and the editor resolve the same kit Vite does.
|
|
473
438
|
|
|
474
439
|
### Text & formatting
|
|
475
440
|
|
|
@@ -495,18 +460,18 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
495
460
|
`Icon` rather than rendering the wrong company's logo. Not for the app's
|
|
496
461
|
own glyphs; that is `icon`. `x` is the one mark whose brand colour is BLACK, so it is the one
|
|
497
462
|
that needs `muted` on a dark ground; the rest carry their own hue either way. **Not a product
|
|
498
|
-
logo**: a monochrome stand-in for one reads as a different thing
|
|
499
|
-
|
|
500
|
-
|
|
463
|
+
logo**: a monochrome stand-in for one reads as a different thing, the kit ships no vendor
|
|
464
|
+
artwork, and official artwork is not uniformly inlinable anyway — pass your own image to
|
|
465
|
+
`SwitchButton`'s `leading` slot instead.
|
|
501
466
|
- **`markdown`** — `Markdown`: the single canonical markdown renderer for chat, apps, and
|
|
502
467
|
`AgentRun`; rich GFM markdown on web with copyable tables, plain-text on
|
|
503
468
|
native; takes a markdown `children` string. **`variant`** decides whose type
|
|
504
469
|
ladder the headings belong to: `"document"` (default) where the markdown IS the
|
|
505
470
|
surface, `"embedded"` where it is a VALUE on a page someone else designed — a
|
|
506
471
|
field on a record, a summary in a feed row. Reach for `"embedded"` whenever the
|
|
507
|
-
text was not written by whoever built the screen
|
|
508
|
-
|
|
509
|
-
|
|
472
|
+
text was not written by whoever built the screen; at document scale an `h1` it
|
|
473
|
+
emits is the same rung as your `SectionHeadingTitle`. Embedded demotes the heading LEVEL as well
|
|
474
|
+
as
|
|
510
475
|
the scale (h1→h4, h2→h5, h3+→h6): sizing alone leaves a model's `##` announcing as a PEER of
|
|
511
476
|
your own sections in heading navigation, which no visual check can see.
|
|
512
477
|
- **`markdown_types`** — `MarkdownProps`; types only.
|
|
@@ -518,10 +483,8 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
518
483
|
`getInputLineHeight` — the 16px-mobile/14px-desktop input contract that stops Safari iOS
|
|
519
484
|
auto-zoom; only for hand-rolled raw inputs. It carries the **ink** as well as the size,
|
|
520
485
|
because omitting `color` on a native `<input>` does not inherit — it falls to the UA
|
|
521
|
-
default, pure black, against the `zinc-900` every resting value beside it uses.
|
|
522
|
-
|
|
523
|
-
change re-weights every stroke's antialiasing), which is a bug no box measurement can
|
|
524
|
-
see. Hand-rolling a raw input means taking this style, not just its font size.
|
|
486
|
+
default, pure black, against the `zinc-900` every resting value beside it uses.
|
|
487
|
+
Hand-rolling a raw input means taking this style, not just its font size.
|
|
525
488
|
- **`font_family`** — the three Inter stacks (`fontFamilyRegular` / `fontFamilyMedium` /
|
|
526
489
|
`fontFamilySemiBold`). Weight in this kit is a FAMILY, not a `font-weight` axis: each rung is
|
|
527
490
|
a separate file, which is why `Text`'s `weight` prop swaps the family rather than a number.
|
|
@@ -531,14 +494,13 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
531
494
|
the ONLY themeable typeface. There is deliberately no body-face role: body weight is three
|
|
532
495
|
coupled families, `text.css` hand-tunes tracking for Inter's glyphs and for Vietnamese
|
|
533
496
|
diacritics at 12px, and `font-feature-settings: "cv11","ss01","ss03"` are Inter's own
|
|
534
|
-
alternates.
|
|
535
|
-
subtly miscalibrated. Changing the body face is a kit change, done once for everybody.
|
|
497
|
+
alternates. Changing the body face is a kit change, done once for everybody.
|
|
536
498
|
- **`type_ramp`** — the type ramp as DATA: `TYPE_RAMP_MOBILE` / `TYPE_RAMP_DESKTOP` (size +
|
|
537
499
|
leading per rung, the 768px breakpoint between them), `TYPE_TRACKING` (em per rung, negative
|
|
538
500
|
throughout and steepening as the type grows), `TypeRung` / `TypeRungName` (`TextSize` IS this
|
|
539
501
|
set), and `trackingPx` for platforms with no em unit. `Text` renders through two mechanisms —
|
|
540
502
|
a `data-text-size` attribute matched by `text.css` on web, a `StyleSheet` on native — and this
|
|
541
|
-
is what both derive from, so the two cannot disagree.
|
|
503
|
+
is what both derive from, so the two cannot disagree. You
|
|
542
504
|
almost never need to import it, because `Text size=` is the way to ask for a rung. Reach for
|
|
543
505
|
it only when styling something `Text` cannot wrap (a raw `TextInput`, a canvas, a chart axis)
|
|
544
506
|
and the result has to sit at a rung exactly.
|
|
@@ -575,15 +537,13 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
575
537
|
- **`spacing`** — the `SPACE` scale + `SpaceToken`, and **`pagePad(small)`** — the page's
|
|
576
538
|
side gutter (`SPACE.md` on a phone, `SPACE.xl` wider). `PageContent` applies it; a screen
|
|
577
539
|
that cannot use that shell — one with a rail, a side panel or its own scroller — imports it
|
|
578
|
-
rather than picking a number
|
|
579
|
-
seen twice and must not shift sideways between them. It is also COUPLED to `ROW_WASH_BLEED`:
|
|
540
|
+
rather than picking a number. It is also COUPLED to `ROW_WASH_BLEED`:
|
|
580
541
|
a register row bleeds its wash outward to `pagePad − ROW_WASH_BLEED`, so the two are read
|
|
581
542
|
together and neither belongs to the app.
|
|
582
543
|
- **`control_surface`** — `CONTROL_HEIGHT` (40), `CONTROL_CONTENT_HEIGHT` (28 — the tallest
|
|
583
544
|
FIXED-height node a 40px band seats, 6 per side of air; what `InlineButton` and a `md`
|
|
584
545
|
`IconButton` already used, now named so an avatar or any other child lands on it too. Text
|
|
585
|
-
never needs it because `minHeight` governs a single line,
|
|
586
|
-
could each pick a different inset and only disagree once an avatar went in),
|
|
546
|
+
never needs it because `minHeight` governs a single line),
|
|
587
547
|
`CONTROL_PADDING_V` (5 — derived from the pair, BORDER INCLUDED in the 6 per side; every 40px
|
|
588
548
|
surface pads by it instead of a number of its own), `CONTROL_RADIUS` (10), `CONTROL_TEXT_INSET`
|
|
589
549
|
(9 — how far a control insets its OWN text: 1px border + 8px padding; anything that must line
|
|
@@ -631,8 +591,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
631
591
|
so a prop declared on one side typechecks everywhere and silently does nothing in the browser.
|
|
632
592
|
- **`label_tone`** — `labelTone(label)`: one stable `ColorName` per free-text label (a tag, a
|
|
633
593
|
topic, a category), for `Badge`. `avatarTone`'s sibling and the same discipline: pure and
|
|
634
|
-
NEVER STORED
|
|
635
|
-
colour has to be chosen; `red` and `amber` excluded, because a palette that can paint an
|
|
594
|
+
NEVER STORED; `red` and `amber` excluded, because a palette that can paint an
|
|
636
595
|
arbitrary noun red makes every red on the screen ambiguous; hue-interleaved so two labels in
|
|
637
596
|
adjacent buckets still look different. Case- and space-folded, matching whatever normalizes the
|
|
638
597
|
set — two spellings of one label must not be two colours. Returns a NAME (not a hex like
|
|
@@ -650,14 +609,14 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
650
609
|
one cell. Takes `{ id, name, kind: "person" | "group", image? }`, shows `max` of them (default
|
|
651
610
|
3) and makes the rest a `+N` that is the LAST MARK IN THE STACK, not loose text beside it.
|
|
652
611
|
`size` is the shared avatar scale with the same `md` default as `Avatar`/`MemberChip`, and the
|
|
653
|
-
marks, the overlap and the count's type all derive from it — no fixed pixels
|
|
654
|
-
|
|
612
|
+
marks, the overlap and the count's type all derive from it — no fixed pixels. Three rules it
|
|
613
|
+
exists to carry: the caller passes only principals it
|
|
655
614
|
can NAME, so the overflow never counts a mark the reader sees is missing; a group is ONE mark
|
|
656
615
|
bearing the group's name, never its members' faces (expanding it states a membership the reader
|
|
657
616
|
may not be entitled to); and the separating hairline is **translucent ink, never the surface
|
|
658
617
|
colour** — a register row washes to `zinc[50]` on hover, `accent_wash` when open and
|
|
659
618
|
`zinc[200]` while pressed, so a ring painted the background is a halo in three of the four
|
|
660
|
-
states, while a dark edge darkens any ground
|
|
619
|
+
states, while a dark edge darkens any ground. Marks
|
|
661
620
|
`announce` because they stand alone here. INERT — its home is a register row, and a row that
|
|
662
621
|
already presses must not hold a second destination (see `member_peek`). A single person →
|
|
663
622
|
`MemberChip`; a single group → `GroupAvatar`.
|
|
@@ -672,8 +631,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
672
631
|
- **`member_profile_card`** — `MemberProfileCard`: WHO IS THIS — `xl` avatar, name, sign-in
|
|
673
632
|
identity, role, join date and groups, plus one optional `action`. Every row is conditional, so a
|
|
674
633
|
card told nothing is a header and nothing else. `role` comes in pre-translated (vocabulary only
|
|
675
|
-
the product can name); `joined` comes in as an ISO DATE and the card formats it to month + year
|
|
676
|
-
(a date has a canonical form the kit owns, so two hosts formatting it themselves would drift);
|
|
634
|
+
the product can name); `joined` comes in as an ISO DATE and the card formats it to month + year;
|
|
677
635
|
`groups` distinguishes absent (no row) from `[]` ("None"). `inactive` draws the departed state as
|
|
678
636
|
a `Badge` — a lifecycle state, not another muted line.
|
|
679
637
|
- **`member_peek`** — `MemberPeek`: `MemberChip` + `Peek` + the card above, with the trigger's
|
|
@@ -700,8 +658,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
700
658
|
one opt-down). **Which rung it renders is the SURFACE's answer, not a prop:** `##` xl-semibold
|
|
701
659
|
on a page, `####` md-semibold inside an overlay's content region (`DrawerScrollArea` /
|
|
702
660
|
`DialogScrollArea` / `ModalBody` / `PopoverContent`), where the container's own title already
|
|
703
|
-
occupies lg.
|
|
704
|
-
per-surface variant to pick, so it cannot be picked wrong. A whole PAGE surface docked in an
|
|
661
|
+
occupies lg. A whole PAGE surface docked in an
|
|
705
662
|
overlay (a record screen in a master-detail drawer) carries its own gutters and so sits in the
|
|
706
663
|
container's BARE slot, keeping the page rungs; nesting overlays never compounds.
|
|
707
664
|
`SubsectionHeadingTitle` is the `###` lg-semibold level-3
|
|
@@ -713,8 +670,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
713
670
|
`primary` `Button` beside the title, rendered whether the collection is empty or full, never
|
|
714
671
|
under the rows it extends and never repeated in the `EmptyState`
|
|
715
672
|
(composition.md § The add-placement law). An overlay surface loses only the type size, never an
|
|
716
|
-
affordance — `icon`, `description` and `info` are the same slots at both altitudes.
|
|
717
|
-
is on every rung** — the ramp's slots do not vary by level; only the type size does. Inside an
|
|
673
|
+
affordance — `icon`, `description` and `info` are the same slots at both altitudes. Inside an
|
|
718
674
|
overlay `####` is the ramp's FLOOR: `SubsectionHeadingTitle` has one altitude and would out-rank
|
|
719
675
|
the section holding it, so a named group there is `Text size="sm" weight="semibold"`. The heading
|
|
720
676
|
ramp is FIXED: `#` xxl / `##` xl / `###` lg / `####` md, no size props.
|
|
@@ -723,9 +679,8 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
723
679
|
rung. The kit's content regions publish it, so an ordinary screen never imports this. Reach for
|
|
724
680
|
it when you build your OWN pane — a docked side panel, a split view, any self-contained region
|
|
725
681
|
whose own chrome caps at `lg` and which therefore uses none of those regions: wrap its content
|
|
726
|
-
in `<HeadingAltitudeContext.Provider value="panel">` and every heading inside is right.
|
|
727
|
-
|
|
728
|
-
build. Flat, never a depth counter — a popover inside a drawer is one rung down, not two.
|
|
682
|
+
in `<HeadingAltitudeContext.Provider value="panel">` and every heading inside is right.
|
|
683
|
+
Flat, never a depth counter — a popover inside a drawer is one rung down, not two.
|
|
729
684
|
- **`section_stack`** — `SectionStack`, `SubsectionStack` — stacks that own the
|
|
730
685
|
between-block law, skipping null children: `SectionStack` = a fixed 56px beat + a hairline
|
|
731
686
|
`Divider` between top-level blocks; `SubsectionStack` = a fixed 32px beat, space-only while
|
|
@@ -752,16 +707,13 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
752
707
|
`left`/`right` form a separate nav row above. Split `trailing` from `actions` by what the
|
|
753
708
|
control acts on: `actions` do something to the page's CONTENT (create, sort, export),
|
|
754
709
|
`trailing` changes what is AROUND it (a side-panel toggle, a view switch). A panel toggle
|
|
755
|
-
filed under `actions` reads as a peer of "create one of these".
|
|
756
|
-
|
|
757
|
-
and
|
|
758
|
-
and `actions` keep their width — a wrapped title is merely taller, a control squeezed
|
|
759
|
-
below its own icon is broken.
|
|
710
|
+
filed under `actions` reads as a peer of "create one of these". Under a title too long for
|
|
711
|
+
the row the TITLE gives way and wraps while `trailing`
|
|
712
|
+
and `actions` keep their width.
|
|
760
713
|
- **`page_content`** — `PageContent` + `PAGE_SIZES`: the page's padded, width-capped content
|
|
761
714
|
region — a centred column with optional `title`/`titleRight`/`description`, `header`/`footer`
|
|
762
|
-
slots and `fullscreen`. **Reach for it before hand-rolling a screen shell
|
|
763
|
-
|
|
764
|
-
up on different gutters. Its side padding is `pagePad` (`@lotics/ui/spacing`), so a screen that
|
|
715
|
+
slots and `fullscreen`. **Reach for it before hand-rolling a screen shell.** Its side
|
|
716
|
+
padding is `pagePad` (`@lotics/ui/spacing`), so a screen that
|
|
765
717
|
genuinely cannot use it — one with a rail, a side panel, or a scroller it must hold a ref to —
|
|
766
718
|
still lands on the same edge by importing that token. It OWNS its `ScrollView` and exposes no
|
|
767
719
|
scroll props, which is the real limit on adoption.
|
|
@@ -808,21 +760,20 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
808
760
|
- **`comments_button`** — `CommentsButton`: THE discussion affordance for a listed record — the
|
|
809
761
|
count and a bubble as ONE control, opening the thread. Reach for it wherever a record is listed
|
|
810
762
|
and its conversation would otherwise be invisible until opened (a register row, a card, a
|
|
811
|
-
compact header); it is `CopyButton`'s peer
|
|
763
|
+
compact header); it is `CopyButton`'s peer and
|
|
812
764
|
shares its surface, scale and focus ring, so a row carrying both reads as one grammar. The count
|
|
813
765
|
is INSIDE the target, never loose text beside an `IconButton`: the number is what the reader
|
|
814
766
|
aims at, and splitting them leaves half the affordance dead under the pointer. FULL INK, unlike
|
|
815
|
-
the muted values around it —
|
|
816
|
-
|
|
767
|
+
the muted values around it — the one thing on the row that can be UNREAD. `subject` reaches the
|
|
768
|
+
accessible
|
|
817
769
|
NAME only ("3 comments on Northwind Packaging" is a destination; "3 comments" on the fortieth row
|
|
818
770
|
is not) — never the visible label, which would spend row width restating whose record it is. The
|
|
819
771
|
pack's `commentsButton.withSubject` builds that phrase, so the word ORDER is the pack's to choose
|
|
820
|
-
and the join is a WORD
|
|
821
|
-
|
|
822
|
-
**Render it only when `count > 0`** — a zero on every quiet row is a column of noise
|
|
823
|
-
|
|
824
|
-
thread (`tpl_item_list` opens the record's drawer on its comments section)
|
|
825
|
-
reader cannot act on states that a conversation exists and leaves them to find it.
|
|
772
|
+
and the join is a WORD — a screen reader drops punctuation along with the relation it was
|
|
773
|
+
carrying.
|
|
774
|
+
**Render it only when `count > 0`** — a zero on every quiet row is a column of noise. Pair it
|
|
775
|
+
with a jump that LANDS on the
|
|
776
|
+
thread (`tpl_item_list` opens the record's drawer on its comments section).
|
|
826
777
|
- **`copy_button`** — `CopyButton`: THE copy control — `value` onto the clipboard, glyph
|
|
827
778
|
flips to a check and the tooltip to the pack's "Copied" for ~2s. The confirmation follows
|
|
828
779
|
a REAL write: an unreachable clipboard throws and the button stays at rest rather than
|
|
@@ -844,8 +795,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
844
795
|
`nav.back` — "Back" / "Quay lại"). Bare it is the 40px chevron-left disc; pass
|
|
845
796
|
**`label`** to name where back GOES ("Danh sách") — the SAME disc with the destination
|
|
846
797
|
beside it, ONE pressable over disc + words, never an icon button beside unclickable
|
|
847
|
-
prose. It takes no size or colour
|
|
848
|
-
one disc so they cannot drift.
|
|
798
|
+
prose. It takes no size or colour.
|
|
849
799
|
**A back affordance INSIDE a header row is not this** — beside a drawer's title a 40px
|
|
850
800
|
disc outweighs the name it belongs to, so that one is a small `IconButton` (see
|
|
851
801
|
`screen_router` below). The distinction is prominence: this HEADS a surface, that
|
|
@@ -893,8 +843,8 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
893
843
|
- **`floating_action_bar`** — `FloatingActionBar`: the floating bulk-select action bar.
|
|
894
844
|
- **`pressable_row`** — `PressableRow`: THE register row — full-width hover/open wash,
|
|
895
845
|
`selected` (the open record) vs `marked` (ticked in bulk-select), forwards `ref` for
|
|
896
|
-
popover anchoring. **Its text is SELECTABLE**:
|
|
897
|
-
|
|
846
|
+
popover anchoring. **Its text is SELECTABLE**: dragging across a name and copying it
|
|
847
|
+
works, and a release that selected text
|
|
898
848
|
does not open the record (see `press_selection`). Furniture that must not smear over —
|
|
899
849
|
`Badge`, `Avatar`, `Button` — opts out for itself.
|
|
900
850
|
The guard behind it is internal (`press_selection`): only a real, non-empty selection
|
|
@@ -952,21 +902,19 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
952
902
|
the section's add (that lives on the heading row, where it does not move — § The add-placement
|
|
953
903
|
law in composition.md), a no-results empty is HINT-only since the filters that emptied it carry
|
|
954
904
|
their own clear, and a FAILED read is `ErrorState`, not this. **`compact`** for one BAND of a
|
|
955
|
-
record — left-aligned, glyph-less, 56px against the default's 178
|
|
956
|
-
|
|
957
|
-
whole region, where nothing else is in view to say so. → composition.md § Empty states.
|
|
905
|
+
record — left-aligned, glyph-less, 56px against the default's 178; the centred default speaks
|
|
906
|
+
for a whole region. → composition.md § Empty states.
|
|
958
907
|
- **`error_state`** — `ErrorState`: the region-scale FAILED read — `message` + optional `detail`
|
|
959
908
|
+ `onRetry` (the kit renders the button and words it from the locale pack, so "try again" reads
|
|
960
909
|
the same everywhere). The fourth of the region states: `Skeleton`/`Loading` in flight → this on
|
|
961
|
-
failure → `EmptyState` on nothing → `CompletionState` on done.
|
|
962
|
-
asserts the read succeeded and found nothing, when nothing is known. For a failure INSIDE a
|
|
910
|
+
failure → `EmptyState` on nothing → `CompletionState` on done. For a failure INSIDE a
|
|
963
911
|
flow (a form that won't save) use `Callout tone="error"`; this is for a region with no content
|
|
964
912
|
to show, where a tinted strip leaves the area collapsed.
|
|
965
913
|
- **`completion_state`** — `CompletionState`: the "all done" terminal state.
|
|
966
914
|
- **`skeleton`** — `Skeleton`: loading placeholder blocks. **Mirror the layout that is
|
|
967
915
|
arriving** — same row count, same column widths, same heights. A skeleton of a different
|
|
968
|
-
shape makes the content jump when it lands
|
|
969
|
-
|
|
916
|
+
shape makes the content jump when it lands. First of the four region states; the other three are
|
|
917
|
+
under `error_state`.
|
|
970
918
|
- **`loading`** — `Loading`: the centered indeterminate loading state (composes
|
|
971
919
|
`DotsIndicator`).
|
|
972
920
|
- **`activity_indicator`** — `ActivityIndicator`: the bare spinner.
|
|
@@ -984,8 +932,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
984
932
|
also home of the shared `PickerOption` type. Its empty row does one of two jobs and they
|
|
985
933
|
are not interchangeable: `includeEmptyOption` makes empty a **choice** the reader picks to
|
|
986
934
|
clear the field, so it is NAMED ("None", from the locale pack; override per instance with
|
|
987
|
-
`emptyOptionLabel`) and keeps that name after a selection
|
|
988
|
-
something is chosen is missing exactly when it is the only route back to empty. Without
|
|
935
|
+
`emptyOptionLabel`) and keeps that name after a selection. Without
|
|
989
936
|
that flag the row is a **placeholder**: it carries `placeholder` and leaves on selection.
|
|
990
937
|
Don't reach for `placeholder` to name a choice — a hint and a value read differently.
|
|
991
938
|
Inside a `FormField` (so inside `FormPicker`) it takes its NAME from the visible label
|
|
@@ -996,8 +943,8 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
996
943
|
`renderSelected(item, { remove })` + `searchable` + `allowCustom` — the tag field is just
|
|
997
944
|
a multi Select; opens `OptionList`. **Set `searchPlaceholder` whenever `allowCustom` is on**:
|
|
998
945
|
the create row only exists once there is a query, so on an untyped menu the placeholder is the
|
|
999
|
-
only always-visible place that can say a new value is allowed
|
|
1000
|
-
|
|
946
|
+
only always-visible place that can say a new value is allowed. `customOptionLabel` names what
|
|
947
|
+
the create row will
|
|
1001
948
|
do, with the value in it. Its trigger is `role="combobox"` (a `<div>`), never
|
|
1002
949
|
`role="button"`: the chip box legitimately puts a remove button INSIDE the trigger, and a
|
|
1003
950
|
real `<button>` may not contain one. **Any trigger that renders caller-supplied content
|
|
@@ -1010,13 +957,12 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1010
957
|
opens — single/multi, optional internal search, create row, keyboard + native-`<select>`
|
|
1011
958
|
typeahead; host it directly in a `Popover`/`Dialog` for a command palette. A chosen row is
|
|
1012
959
|
`selected`: the kit's `zinc.100` resting highlight AND `aria-selected`, one weight above the
|
|
1013
|
-
`zinc.50` the keyboard/hover row takes
|
|
960
|
+
`zinc.50` the keyboard/hover row takes.
|
|
1014
961
|
It opens SCROLLED to its seat (the selected row in `search.mode:"none"`, else the first real
|
|
1015
|
-
option), which only shows on a list longer than its own box
|
|
1016
|
-
96-row list opening on your value and opening at the top.
|
|
962
|
+
option), which only shows on a list longer than its own box.
|
|
1017
963
|
`indeterminateValues` (multi) draws a MIXED tick on values only SOME of the things being
|
|
1018
|
-
edited carry — the shape for labelling a selection
|
|
1019
|
-
|
|
964
|
+
edited carry — the shape for labelling a selection. Presentational and one-directional: a mixed
|
|
965
|
+
row is not in `value`, so
|
|
1020
966
|
pressing it selects (the caller applies to all), and it announces itself in the row label
|
|
1021
967
|
because `role="option"` has no ARIA state for a third value.
|
|
1022
968
|
- **`combobox`** — COMPOUND single-select editable search: `Combobox` root +
|
|
@@ -1060,6 +1006,17 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1060
1006
|
field with an empty slot reserves nothing, so its text starts and ends where it always did.
|
|
1061
1007
|
- **`number_input`** — `NumberInput`: numeric input; `format` for currency/units.
|
|
1062
1008
|
- **`search_input`** — `SearchInput`: the search box for toolbars/filters.
|
|
1009
|
+
- **`markdown_editor`** — `MarkdownEditor`: the standalone WYSIWYG whose value stays a plain
|
|
1010
|
+
markdown string, with the formatting toolbar as a SIBLING of the field. `variant` is
|
|
1011
|
+
`"document"` (the default soft sheet) or the two FIELD states `"framed"` / `"bare"`, DERIVED
|
|
1012
|
+
from `InlineEditVariant`; the editor draws its own surface, so a host frame never paints one
|
|
1013
|
+
under it. Web is ProseMirror via `@lotics/markdown-editor`, native falls back to raw-text
|
|
1014
|
+
editing — see § Edit a record's fields in place for the whole family and its `numberOfLines`
|
|
1015
|
+
reserve. **`markdown_editor_props`** is the platform-neutral `MarkdownEditorProps` /
|
|
1016
|
+
`MarkdownEditorVariant` that keeps the two implementations from drifting; **`markdown_editor.css`**
|
|
1017
|
+
is the web sheet, imported once at the app entry.
|
|
1018
|
+
- **`form_markdown_editor`** — `FormMarkdownEditor`: `FormField` wrapping a `MarkdownEditor` —
|
|
1019
|
+
the labeled prose field, for a dialog or settings form.
|
|
1063
1020
|
- **`form_field`** — `FormField` + `useFormField`: label / description / warning / error /
|
|
1064
1021
|
`optional` marker wrapper (the three field-annotation tones — muted / amber / danger — all
|
|
1065
1022
|
wired into the input's describedby); the fieldset grid cell (`half`/`full` widths — see the
|
|
@@ -1083,7 +1040,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1083
1040
|
switch (`accessibilityRole="switch"`, the inner Switch read-only). The settings-panel/menu
|
|
1084
1041
|
row toggle. The leading slot takes EITHER `icon` (an `IconName`, the app's own glyph language)
|
|
1085
1042
|
or `leading` (any node); `icon` wins if both are passed. A row naming an outside product passes
|
|
1086
|
-
its logo through `leading
|
|
1043
|
+
its logo through `leading`.
|
|
1087
1044
|
- **`use_form`** — `useForm`: THE batch draft-form state hook — `values` = `initialValues` +
|
|
1088
1045
|
an edits overlay (a revalidation refreshes untouched fields, no sync effect), `validate`
|
|
1089
1046
|
(sync/async, gates submit, editing clears the field's error), `onSubmit(values, helpers)`
|
|
@@ -1114,7 +1071,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1114
1071
|
- **`date_filter`** — `DateFilter`: the date+time period filter panel — presets
|
|
1115
1072
|
(`PresetId`), calendar, optional time segments; the body `DateRangeFilterField` opens.
|
|
1116
1073
|
**Every preset SETS a range** — the list holds no mode and no null case, so a hand-picked
|
|
1117
|
-
range selects nothing
|
|
1074
|
+
range selects nothing. The
|
|
1118
1075
|
presets are a `listbox` of `MenuButton` options: selection is the kit's `zinc.100` row
|
|
1119
1076
|
highlight plus `aria-selected`, never a filled primary treatment, because the row reports
|
|
1120
1077
|
state rather than offering an action. Sidebar at viewports of 768 and up, strip below —
|
|
@@ -1150,13 +1107,11 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1150
1107
|
`DeadlineThresholds`, `Deadline`). Three contracts a screen must not invent for itself — how
|
|
1151
1108
|
far off a date is, what to CALL that number, and how loud it reads. `daysUntil` collapses both
|
|
1152
1109
|
ends to midnight because the vocabulary is day-granular: rounding the raw gap lands on the
|
|
1153
|
-
wrong day whenever now and the deadline straddle noon
|
|
1154
|
-
one at 02:00 today reads "1 day overdue" 12 hours in). Wording comes from the `deadline` locale
|
|
1110
|
+
wrong day whenever now and the deadline straddle noon. Wording comes from the `deadline` locale
|
|
1155
1111
|
slice; the 1-day/3-day tone lines are freight defaults and move per domain via
|
|
1156
1112
|
`DeadlineThresholds`. **`deadlineAnnotation` spreads onto the `DetailRow` holding the date** —
|
|
1157
1113
|
a deadline is a property OF that field, and the three annotation slots carry the three urgency
|
|
1158
|
-
levels exactly (`error` / `warning` / `description`)
|
|
1159
|
-
speaking a vocabulary the rest of the field grid does not. `nearestDeadline` returns an OVERDUE
|
|
1114
|
+
levels exactly (`error` / `warning` / `description`). `nearestDeadline` returns an OVERDUE
|
|
1160
1115
|
milestone FIRST and expects the caller to pass only OPEN ones — done-ness is not knowable from
|
|
1161
1116
|
a date, and skipping past a missed cut-off is how a record silently stops reporting the failure.
|
|
1162
1117
|
Worked example: `tpl_record`'s Due rows.
|
|
@@ -1204,8 +1159,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1204
1159
|
same shape `Select` gives: return `<Chip onDismiss={remove}>` for a removable tag, or a plain
|
|
1205
1160
|
badge that ignores it — a dense grid column is the case for the badge, since the ✕ costs more
|
|
1206
1161
|
width there than the reopen it saves. `remove` detaches that one tag whether the list is open or
|
|
1207
|
-
shut, and never opens the list
|
|
1208
|
-
trigger needs nothing to hold the press back. Single has no `remove`: unsetting one value is `onClear` and its
|
|
1162
|
+
shut, and never opens the list. Single has no `remove`: unsetting one value is `onClear` and its
|
|
1209
1163
|
Clear row. Both modes take
|
|
1210
1164
|
`allowCustom` (a create-a-tag/option row) + `searchable` + **`customOptionPlacement`** (default
|
|
1211
1165
|
`"bottom"`, right for a tag field; pass `"top"` for a find-or-create REFERENCE picker over a long
|
|
@@ -1234,9 +1188,14 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1234
1188
|
surface already promises and, down a column, draws the grid twice.
|
|
1235
1189
|
|
|
1236
1190
|
It is an axis of weight, not of use: both hover and open identically. There is no `*Cell` family —
|
|
1237
|
-
a data-grid cell is `<InlineSelect variant="bare" …/>`, not a `SelectCell
|
|
1238
|
-
USE, and duplicating the picker stack to flip two style properties, are both things the kit forbids).
|
|
1191
|
+
a data-grid cell is `<InlineSelect variant="bare" …/>`, not a `SelectCell`.
|
|
1239
1192
|
Hand-roll a pressable cell to match `bare`, never a background wash.
|
|
1193
|
+
- **`inline_markdown`** — `InlineMarkdown`: the PROSE member of the Inline\* family — a
|
|
1194
|
+
`MarkdownEditor` on the inline-field box metrics, `onSave` per field. It is the one member
|
|
1195
|
+
that does NOT swap: the editor is mounted at rest and merely gains a draft on focus, so
|
|
1196
|
+
focusing moves nothing and the prose stays SELECTABLE instead of sitting under a button. Its
|
|
1197
|
+
toolbar is off for the same reason, and `variant` passes THROUGH to the editor, which draws
|
|
1198
|
+
the box. Full note under § Edit a record's fields in place.
|
|
1240
1199
|
- **`inline_files`** — `InlineFiles`: the FILES member of the Inline\* family — a record's
|
|
1241
1200
|
attachments as a `DetailRow` value. A `FileRows` LIST (press a row to preview full-screen, ⋯
|
|
1242
1201
|
to remove) with ONE CTA under it; no selection mode, no bulk bar, no upload queue — reach for
|
|
@@ -1244,8 +1203,8 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1244
1203
|
is the FIELD's cardinality and it changes the verb**: many → the CTA reads `addMoreLabel` and
|
|
1245
1204
|
the host APPENDS in `onAdd`; one → it reads `replaceLabel` and the host SETS, because "add
|
|
1246
1205
|
another" is not an act a single-file field can perform and offering it is how a record ends up
|
|
1247
|
-
holding two of something the schema says there is one of
|
|
1248
|
-
|
|
1206
|
+
holding two of something the schema says there is one of. `blockedReason` renders a sentence
|
|
1207
|
+
instead of the CTA for a
|
|
1249
1208
|
field with nothing to attach TO yet — never a disabled button, which says only that you may
|
|
1250
1209
|
not. A LIST and not switchable: documents are identified by NAME, images by CONTENT, so a
|
|
1251
1210
|
photo field wants `FileGrid`/`FilesEditor` instead — that is a judgment about the data, so it
|
|
@@ -1295,8 +1254,8 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1295
1254
|
unit (though "kg"/"$" belong IN the value via `InlineNumberInput format`) — composes into a
|
|
1296
1255
|
row inside the value cell, costing only the row that wants it. The LABEL WRAPS inside its column and
|
|
1297
1256
|
is never clipped — a fixed `labelWidth` would otherwise ellipsize every long field name
|
|
1298
|
-
("Registered business address")
|
|
1299
|
-
|
|
1257
|
+
("Registered business address"); a wrapped label's FIRST line stays level with the value's first
|
|
1258
|
+
control line while a
|
|
1300
1259
|
one-line label still centers on it (both modes, no prop). The FIELD-ANNOTATION vocabulary (same
|
|
1301
1260
|
names + meanings as `FormField`), always EXPLICIT — a row never hides guidance behind an ⓘ:
|
|
1302
1261
|
**`description`** = a fact / persistent guidance (muted), under the VALUE (stacked mode
|
|
@@ -1339,10 +1298,9 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1339
1298
|
into a pressable door floating its particulars in an anchored popover (put links INSIDE the
|
|
1340
1299
|
peek — never a button in a button; `reference` is the trailing-link alternative for static
|
|
1341
1300
|
rows), `LedgerTotal` = the divider-set emphasized close with `zeroLabel` for settled. **`meta` is a
|
|
1342
|
-
neutral QUALIFIER** (a date, a method) at caption weight — never a problem or a state:
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
that can FIX it, and in a statement the arithmetic has usually said it already.
|
|
1301
|
+
neutral QUALIFIER** (a date, a method) at caption weight — never a problem or a state: a
|
|
1302
|
+
caption is the wrong weight for something wanting action. A problem goes on the row
|
|
1303
|
+
that can FIX it.
|
|
1346
1304
|
**Pick the shape by what the statement IS**, because there are two and they read differently:
|
|
1347
1305
|
- **`charges → total`** — `LedgerGroup`s, each **closed by its own sum** ("Charges" vs
|
|
1348
1306
|
"Received"), giving three ascending rungs: row `sm/regular`, subtotal `sm/medium`, total
|
|
@@ -1368,10 +1326,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1368
1326
|
whose lookup link sits INSIDE the popover, a flat `reference` on the row with nothing to
|
|
1369
1327
|
expand, `meta` + `success` tone on money coming back, and a one-row `Adjustments` group that
|
|
1370
1328
|
takes no `total`. `Received` is derived from the charges carrying a payment METHOD rather than
|
|
1371
|
-
a second flag to keep in step
|
|
1372
|
-
trust by summing exactly what its label claims. `LedgerBasis` is demoed on the gallery's
|
|
1373
|
-
Charts page instead — a delivery order has no base to compute from, and inventing one to
|
|
1374
|
-
place a component is how invented needs start.
|
|
1329
|
+
a second flag to keep in step. `LedgerBasis` is demoed on the gallery's Charts page instead.
|
|
1375
1330
|
- **`running_ledger`** — `RunningLedger`: a CHRONOLOGICAL, self-totalling ledger — a stock
|
|
1376
1331
|
kardex, a statement of account — where every row carries the balance AS OF that row. Not
|
|
1377
1332
|
`Ledger`: that compound closes a fixed set of charge/receipt groups into one total and has no
|
|
@@ -1379,23 +1334,17 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1379
1334
|
`rows` (date, label, a SIGNED `amount` — positive in/debit, negative out/credit, one field
|
|
1380
1335
|
rather than paired columns so the same shape serves a stock quantity and a money statement),
|
|
1381
1336
|
each rendered with its running balance **computed by the component**, never accepted as a
|
|
1382
|
-
caller-supplied number
|
|
1383
|
-
accumulates, and a caller-supplied one is the one kind of drift no reader can catch by looking.
|
|
1337
|
+
caller-supplied number.
|
|
1384
1338
|
`format` is ONE formatter for the whole ledger (`formatMoney`, or a quantity formatter with a
|
|
1385
1339
|
unit suffix) — every row shares one unit, which is what lets the column state it once.
|
|
1386
1340
|
`criticalAtOrBelow` reads a row's balance in danger tone once it reaches a caller-declared
|
|
1387
1341
|
floor (a stock-out, a negative balance) — **the ONLY tone this component owns**, evaluated
|
|
1388
1342
|
independently on EVERY balance in the column: each row, the opening anchor (so an already-critical
|
|
1389
1343
|
opening reads danger even with zero rows following it), and the closing line. A row's `amount`
|
|
1390
|
-
is never auto-toned by its arithmetic sign
|
|
1391
|
-
as an IN one
|
|
1344
|
+
is never auto-toned by its arithmetic sign — an OUT movement is routine business as often
|
|
1345
|
+
as an IN one. Closes with a `Divider`-set current-balance line at `Ledger`'s own
|
|
1392
1346
|
`LedgerTotal` weight, `closingLabel` overriding the locale default ("Current balance") for a
|
|
1393
|
-
domain the pack cannot own generically ("Tồn kho hiện tại").
|
|
1394
|
-
— a stock kardex or an AR statement of account is a genuinely different shape from every
|
|
1395
|
-
existing template (the 3-statement accrual grammar in `tpl_statements`, the fleet-register
|
|
1396
|
-
drawer in `tpl_stock`), so forcing it into either would be a mismatch rather than a worked
|
|
1397
|
-
example; proven against real, live deployed consumers instead (a warehouse and a receivables
|
|
1398
|
-
app) until a template of its own earns its place.
|
|
1347
|
+
domain the pack cannot own generically ("Tồn kho hiện tại").
|
|
1399
1348
|
- **`reference_field`** — `ReferenceField`: a reference to ANOTHER RECORD, rendered as a
|
|
1400
1349
|
FIELD VALUE — the kit's inline-editor surface (so a pointer sits in the value column
|
|
1401
1350
|
like the editors above and below it), whose press opens a PEEK of that record's facts.
|
|
@@ -1408,23 +1357,21 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1408
1357
|
VALUE floor (200), not the control floor, because that column holds names as well as editors.
|
|
1409
1358
|
The verbs live INSIDE the
|
|
1410
1359
|
peek, never on the field, and they are ALL `Button`s at one altitude — a peek is
|
|
1411
|
-
dialog-scale and a dialog's verbs are buttons
|
|
1412
|
-
|
|
1360
|
+
dialog-scale and a dialog's verbs are buttons. The row splits by WHAT EACH TOUCHES, and the
|
|
1361
|
+
spacer is that
|
|
1413
1362
|
seam: **left, the LINK** — `Change` (`onChange`, `secondary`) points the field at a different
|
|
1414
1363
|
record, named for the intent and deliberately not `danger` since a recoverable correction must
|
|
1415
1364
|
not wear the colour reserved for destruction; `Clear` (`onClear`, NO fill — the least-reached
|
|
1416
1365
|
verb of the four) leaves it empty. The draft's footer follows the same rule: `Cancel` carries
|
|
1417
1366
|
no fill beside a filled `Save`, so the only filled button in either mode is the one that acts. **Both are REQUIRED**, so the footer has one shape
|
|
1418
|
-
everywhere
|
|
1419
|
-
Change and then declining to pick reaches the same empty state. **Right, the RECORD it points at** — `Edit` (`primary`, the one filled-dark rung,
|
|
1367
|
+
everywhere. **Right, the RECORD it points at** — `Edit` (`primary`, the one filled-dark rung,
|
|
1420
1368
|
handed to `Save` when the draft opens) and `Open` (`secondary`, rightmost, OPTIONAL, the
|
|
1421
|
-
departure — not prioritized
|
|
1422
|
-
|
|
1369
|
+
departure — not prioritized). Every verb is optional and the footer renders only when it HAS
|
|
1370
|
+
one, so an
|
|
1423
1371
|
edit-only reference draws no rule under nothing.
|
|
1424
1372
|
**Both unset — they differ only in FOLLOW-THROUGH.** `Clear` is the plain detach (nothing
|
|
1425
|
-
focused); `Change` is the same write plus the caller's focus handoff.
|
|
1426
|
-
|
|
1427
|
-
it" from "unset it, I'm about to pick another". Whether empty is VALID is the row's business — its
|
|
1373
|
+
focused); `Change` is the same write plus the caller's focus handoff. Whether empty is VALID is
|
|
1374
|
+
the row's business — its
|
|
1428
1375
|
`warning`, its validation — never the peek's.
|
|
1429
1376
|
**Hand the picker focus after `Change`, never after `Clear`.** Both land on your empty state.
|
|
1430
1377
|
Change said "wrong one", so open the picker focused (`Combobox`'s `autoFocus` both focuses AND
|
|
@@ -1470,11 +1417,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1470
1417
|
Omitting `onOpen` renders a plain, non-interactive reference — never a press target wired to
|
|
1471
1418
|
nothing, which is worse than no affordance. Composes as a `DetailRow`'s value for a single
|
|
1472
1419
|
relationship, or repeats plainly (no per-row label) in a list for several — the surrounding
|
|
1473
|
-
label or heading is the caller's, this renders one reference.
|
|
1474
|
-
yet, for the same reason as `running_ledger` above: what this component is FOR — a reference
|
|
1475
|
-
into a genuinely separate deployed app — has no natural home inside any single-app template in
|
|
1476
|
-
this repo without inventing a fictional sibling app to point at; proven against real, live
|
|
1477
|
-
deployed consumers instead.
|
|
1420
|
+
label or heading is the caller's, this renders one reference.
|
|
1478
1421
|
- **`peek`** — `Peek`: drill-down for a reference — press a name/id where it appears and get its
|
|
1479
1422
|
details in an anchored popover, without leaving the screen; keep the content a summary with ONE
|
|
1480
1423
|
action to the full record. **`variant` picks the register the TRIGGER sits in, and getting it
|
|
@@ -1502,9 +1445,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1502
1445
|
**The row OWNS its `title`/`description` pair, so do not hand-roll one beside it.** Both
|
|
1503
1446
|
lines are `sm`, `leading="tight"`, no gap between them — the stacked-pair law in
|
|
1504
1447
|
[composition.md](./composition.md), applied by the component rather than restated at each
|
|
1505
|
-
call site.
|
|
1506
|
-
between the lines, against the 6px the rule prescribes), and the height was sized around
|
|
1507
|
-
that mistake at 64, which left a correctly-sized `md` mark adrift in it; the row is 56 now.
|
|
1448
|
+
call site. The row is 56.
|
|
1508
1449
|
A `description` passed as a NODE keeps its own leading — the component only sets it on the
|
|
1509
1450
|
string branch, because forcing a line box on somebody else's subtree re-spaces content that
|
|
1510
1451
|
may not be a line of text at all.
|
|
@@ -1528,14 +1469,10 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1528
1469
|
default.** A `SummaryLine`/`KPIStrip` figure above the register is a claim that ONE column is
|
|
1529
1470
|
the reason to scan this page; left unset, `priority` defaults to column order and sheds
|
|
1530
1471
|
right-to-left, so that exact column is the first thing table_fit drops the moment the
|
|
1531
|
-
container narrows
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
register answers "which one"; the moment it cannot answer "and what about it" the reader is
|
|
1536
|
-
sent to a drawer, loses the surrounding rows, and pays a navigation to come back — which is
|
|
1537
|
-
what makes scanning twenty records unaffordable. Reach for it when the detail is read or
|
|
1538
|
-
light-edit; a heavy form still deserves its own surface. `expanded` is CONTROLLED — how many
|
|
1472
|
+
container narrows.
|
|
1473
|
+
**`detail` + `expanded` reveal a row's detail BENEATH it instead of navigating away.**
|
|
1474
|
+
Reach for it when the detail is read or light-edit; a heavy form still deserves its own surface.
|
|
1475
|
+
`expanded` is CONTROLLED — how many
|
|
1539
1476
|
may be open at once is the caller's rule, so the component does not pick one. The detail
|
|
1540
1477
|
renders as a SIBLING of the press surface (a control inside it would otherwise be swallowed
|
|
1541
1478
|
by the row's own toggle), the row shows the open wash while it is out, and the door announces
|
|
@@ -1543,8 +1480,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1543
1480
|
to expand.
|
|
1544
1481
|
**`Table.count` + `TableRow.ordinal` make it a COUNTED register** — a narrow gutter left of
|
|
1545
1482
|
`leading` carrying each row's position, headed by the total. Reach for it on any register
|
|
1546
|
-
long enough to scroll
|
|
1547
|
-
without counting rows, and it is what a reader coming from a spreadsheet (STT) expects to
|
|
1483
|
+
long enough to scroll — it is what a reader coming from a spreadsheet (STT) expects to
|
|
1548
1484
|
find. `count` is the single switch — omit it and no gutter is reserved, so an uncounted
|
|
1549
1485
|
register keeps every pixel it had. It is CHROME, not a column: it never sorts, never drops,
|
|
1550
1486
|
and is excluded from the column band, but it IS reserved space the fit accounts for.
|
|
@@ -1564,8 +1500,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1564
1500
|
`SortHeaderLabels`: the sortable column header and the sort-state helpers `Table`/
|
|
1565
1501
|
`DataGrid` consumers drive. `sortBy` copies rather than mutates, and compares strings with
|
|
1566
1502
|
`localeCompare` (case- and accent-folded, `numeric`) rather than `<`/`>` — code-point order
|
|
1567
|
-
exiles every accented letter past the whole unaccented alphabet
|
|
1568
|
-
reads correctly until its first Đ and looks fine to a reviewer forever. `getValue` returns a
|
|
1503
|
+
exiles every accented letter past the whole unaccented alphabet. `getValue` returns a
|
|
1569
1504
|
NUMBER for anything ordered numerically (a timestamp, a count).
|
|
1570
1505
|
- **`data_grid`** — `DataGrid` + `gridRowStyle`: the inline-managed grouped table (see
|
|
1571
1506
|
[Tabular data](#tabular-data--pick-by-scale--intent)); `labels` localizes the sort-header
|
|
@@ -1597,9 +1532,9 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1597
1532
|
`Summary.Header` / `Summary.Total` / `Summary.Metric` / `Summary.Distribution` /
|
|
1598
1533
|
`Summary.Facts`, composed as children. **Declare the buckets ONCE on the root and every part
|
|
1599
1534
|
derives from them** — the headline is their sum (there is no `total` prop to go stale), the
|
|
1600
|
-
segments and the legend swatches are the buckets' own colours
|
|
1601
|
-
|
|
1602
|
-
|
|
1535
|
+
segments and the legend swatches are the buckets' own colours: a `segments` array passed
|
|
1536
|
+
beside an `items` array beside a `total` drifts three ways, invisibly. A `SummaryBucket`
|
|
1537
|
+
must PARTITION the set; an aggregate over the same
|
|
1603
1538
|
rows that does not (a condition like "overdue" beside "draft / sent / paid", counted again
|
|
1604
1539
|
from a different question) goes to `Summary.Facts` as `extra`, where it renders with NO swatch
|
|
1605
1540
|
— the visual difference says it is not a slice of the bar. Ordering is the caller's and is
|
|
@@ -1635,7 +1570,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1635
1570
|
springing to a filled check when done, distinct from the square checkbox; the
|
|
1636
1571
|
task/to-do/checklist control. `partial` half-fills it, for the row that is underway but not
|
|
1637
1572
|
finished (a task at "doing", a step whose children are half ticked) — a binary ring reports
|
|
1638
|
-
that row as untouched
|
|
1573
|
+
that row as untouched. The CLICK stays binary
|
|
1639
1574
|
(`onChange(done)`) so the ring means one thing everywhere; `partial` is reached through
|
|
1640
1575
|
whatever NAMES it — a status cell, or children ticking off — never by cycling the ring. Keep
|
|
1641
1576
|
it monochrome and let colour live in the status cell.
|
|
@@ -1684,9 +1619,8 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1684
1619
|
never dismiss it — clicks, scrolls, and Escape belong to that layer until it closes. In a
|
|
1685
1620
|
NARROW VIEWPORT (`small`, under 768) it presents instead as a modal bottom sheet (scrim, close
|
|
1686
1621
|
button, slide-up) — decided by `PopoverContent` itself off the screen size, with nothing to pass
|
|
1687
|
-
and no way to answer it differently:
|
|
1688
|
-
|
|
1689
|
-
the device — a few-hundred-pixel side panel on a desktop gets the sheet too.
|
|
1622
|
+
and no way to answer it differently: the VIEWPORT, never the container the trigger sits in,
|
|
1623
|
+
and never the device — a few-hundred-pixel side panel on a desktop gets the sheet too.
|
|
1690
1624
|
`PopoverContent` already insets its
|
|
1691
1625
|
body 12px — put content directly in it, NEVER add your own padding `View` (that double-pads);
|
|
1692
1626
|
title/actions go in `PopoverHeader` / `PopoverFooter`. **A fixed width is the `width` PROP,
|
|
@@ -1695,8 +1629,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1695
1629
|
then stretches the panel and leaves the content adrift in a box wider than itself. A popover
|
|
1696
1630
|
with no bands never sees this.
|
|
1697
1631
|
The built-in scroller is **full-bleed horizontally** and re-insets its content by the same
|
|
1698
|
-
amount, so a scrolling body reaches the panel's real edges
|
|
1699
|
-
panel edge rather than under an invisible margin 12px inside it — while the text stays on the
|
|
1632
|
+
amount, so a scrolling body reaches the panel's real edges while the text stays on the
|
|
1700
1633
|
same column the header and footer use. Nothing to opt into; `disableBodyScroll` consumers
|
|
1701
1634
|
(which own their scroll) are untouched.
|
|
1702
1635
|
- **`popover_nav`** — `usePopoverNav` + `PopoverScreen` + `PopoverNavHeader` — the popover's
|
|
@@ -1739,8 +1672,8 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1739
1672
|
section wrappers must be DIRECT children of the ScrollView content. The highlight and the
|
|
1740
1673
|
jump both re-measure the live DOM on web, so content loading in above a section never
|
|
1741
1674
|
strands them. Reaching the END of the scroll activates the LAST key regardless of offsets —
|
|
1742
|
-
a final section shorter than the viewport never scrolls its top past the trigger line
|
|
1743
|
-
|
|
1675
|
+
a final section shorter than the viewport never scrolls its top past the trigger line.
|
|
1676
|
+
On NARROW containers
|
|
1744
1677
|
the rail becomes a PINNED bar naming the CURRENT section that opens a full-page
|
|
1745
1678
|
section-picker `Modal` — never a horizontal tab strip. Worked example:
|
|
1746
1679
|
[`tpl_record`](../examples/tpl_record.tsx).
|
|
@@ -1751,11 +1684,10 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1751
1684
|
### Numbers & charts
|
|
1752
1685
|
|
|
1753
1686
|
**These are where a work screen gets its visual interest, and it is the only kind that survives
|
|
1754
|
-
contact with a real dataset.**
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
sentences. → [composition.md](./composition.md) §"Character comes from the DATA".
|
|
1687
|
+
contact with a real dataset.** Each of these is information first, so none of it is the
|
|
1688
|
+
decoration that gets cut in review. A screen reported as flat or boring is usually a screen with
|
|
1689
|
+
numbers in it rendered as sentences.
|
|
1690
|
+
→ [composition.md](./composition.md) §"Character comes from the DATA".
|
|
1759
1691
|
|
|
1760
1692
|
Two failure modes to hold against them. A figure nobody compares to anything is not a metric — it
|
|
1761
1693
|
is a number with a large font, and the size promises a significance it cannot deliver. And a
|
|
@@ -1767,7 +1699,7 @@ component rather than showing it at zero.
|
|
|
1767
1699
|
(ⓘ), so the reader can find out what "on-time %" counts without leaving the screen.
|
|
1768
1700
|
- **`kpi_strip`** — `KPIStrip`: the boxed dashboard stat band. Every item has to answer a
|
|
1769
1701
|
question the surface below it cannot — a strip restating totals the table already carries is
|
|
1770
|
-
the trim test failing
|
|
1702
|
+
the trim test failing.
|
|
1771
1703
|
- **`metric`** — `Metric`: a bare headline figure (`format`
|
|
1772
1704
|
currency|number|percentage|none, `tone`, `size` sm|md|lg|hero). Bare = no card, no label of its
|
|
1773
1705
|
own, so whatever contains it has to name it. Always `tabular` — it is set that way here, and it
|
|
@@ -1781,8 +1713,7 @@ component rather than showing it at zero.
|
|
|
1781
1713
|
Direction words via the `trendFooter` locale slice; goes in `SectionCard footer`.
|
|
1782
1714
|
- **`sparkline`** — `Sparkline`: the inline mini trend line. Sized for a ROW or a card corner:
|
|
1783
1715
|
it carries SHAPE (rising, spiky, flat) and no readable values, which is exactly what a
|
|
1784
|
-
register wants
|
|
1785
|
-
without stopping. Reach for a real chart the moment someone needs to read a value off it.
|
|
1716
|
+
register wants. Reach for a real chart the moment someone needs to read a value off it.
|
|
1786
1717
|
- **`bar_chart`** / **`line_chart`** / **`pie_chart`** — `BarChart` / `LineChart` /
|
|
1787
1718
|
`PieChart`: the canonical SVG chart set (no recharts). `LineChart` prints as many x labels
|
|
1788
1719
|
as the track fits and thins the rest, anchored on the LAST point — the newest reading is the
|
|
@@ -1822,8 +1753,7 @@ component rather than showing it at zero.
|
|
|
1822
1753
|
horizontal rows (take rows for a narrow container or long step names; a 70px column and a
|
|
1823
1754
|
rotated label is not an answer). The span always includes zero, so a bridge that closes at a
|
|
1824
1755
|
loss hangs below the baseline instead of floating on a scale that exaggerates every step.
|
|
1825
|
-
`waterfallLayout` is the pure geometry, exported and tested on its own
|
|
1826
|
-
drawing if the floating bars land where the arithmetic says, and no screenshot confirms that.
|
|
1756
|
+
`waterfallLayout` is the pure geometry, exported and tested on its own.
|
|
1827
1757
|
Pick it over `ledger` when the reader is scanning for SHAPE (a dashboard), and `ledger` when
|
|
1828
1758
|
the figures must be read exactly and reconciled line by line (a record). Over `bar_chart`
|
|
1829
1759
|
whenever the bars are steps in ONE arithmetic — five columns all starting at zero state a
|
|
@@ -1849,17 +1779,17 @@ component rather than showing it at zero.
|
|
|
1849
1779
|
`Matrix.Header` (corner + axis labels) + `Matrix.Grid` (`display` number|heat|both — the
|
|
1850
1780
|
cells, pressable, the value IN the cell) + `Matrix.Totals` (row + column + grand) +
|
|
1851
1781
|
`Matrix.Legend`. Pick over `Heatmap` when the NUMBER and totals matter. Figures are
|
|
1852
|
-
RIGHT-aligned — they are `tabular`, and centring them defeats the aligned column
|
|
1853
|
-
|
|
1854
|
-
|
|
1782
|
+
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. `MATRIX_COL_GAP` is
|
|
1784
|
+
exported for a caller budgeting
|
|
1855
1785
|
its own column widths.
|
|
1856
1786
|
- **`matrix_totals`** — the data layer for `Matrix`: `matrixTotals` (the cross-tab
|
|
1857
1787
|
aggregation) + `MatrixAxisItem` / `MatrixCellRef` / `MatrixTotalsResult`; React-free, so a
|
|
1858
1788
|
KPI can be driven off the same numbers the grid shows.
|
|
1859
1789
|
- **`highlight_segments`** — `splitHighlightSegments`: cuts a string into plain and
|
|
1860
|
-
highlighted runs for `Composer`'s `highlightRanges`. React-free
|
|
1861
|
-
|
|
1862
|
-
|
|
1790
|
+
highlighted runs for `Composer`'s `highlightRanges`. React-free: it feeds a mirror sitting
|
|
1791
|
+
behind a real text field, so a boundary off by one paints the tint adrift while everything
|
|
1792
|
+
still looks like a working composer. Overlapping,
|
|
1863
1793
|
unsorted and out-of-bounds ranges all normalize; no input drops a character.
|
|
1864
1794
|
- **`legend_item`** — `LegendItem`: one swatch + label of a chart legend.
|
|
1865
1795
|
- **`remainder_meter`** — `RemainderMeter`: allocated-vs-remaining meter
|
|
@@ -1869,7 +1799,7 @@ component rather than showing it at zero.
|
|
|
1869
1799
|
whole model is one source distributed one way, and a caller with a second relationship to show
|
|
1870
1800
|
reaches for a second instance before reaching for a wider one. Extract a true 3-column strip
|
|
1871
1801
|
only once a SECOND real caller needs the exact same 3-way shape (the kit's own
|
|
1872
|
-
extract-on-the-second-hand-roll rule)
|
|
1802
|
+
extract-on-the-second-hand-roll rule).
|
|
1873
1803
|
- **`allocation_row`** — `AllocationRow`: one target's row in an allocation surface (pairs
|
|
1874
1804
|
with `RemainderMeter`).
|
|
1875
1805
|
|
|
@@ -1899,19 +1829,16 @@ component rather than showing it at zero.
|
|
|
1899
1829
|
composition.md §registers); and **`right` renders beside the press target, not inside
|
|
1900
1830
|
it**, so a control there is valid HTML and keeps its own click — though on a row that
|
|
1901
1831
|
EXPANDS, verbs belong in the BODY and `right` is better kept for a fact (a duration, a
|
|
1902
|
-
badge).
|
|
1903
|
-
the first-line box and the chevron-pairing problems in the first place.
|
|
1832
|
+
badge).
|
|
1904
1833
|
- **`checklist`** — `Checklist` + `ChecklistGroup` + `ChecklistItem` + `ChecklistNote` /
|
|
1905
1834
|
`ChecklistActions` / `ChecklistField`: THE run of tickable rows, at any
|
|
1906
1835
|
altitude — a ring, a title, a value beside it, a muted note, a detail block, an optional
|
|
1907
1836
|
grouping level. **`connected` is the LINE joining the rings, and it is the whole
|
|
1908
|
-
difference
|
|
1909
|
-
sequence), off for a SET picked in any order, where a line claims an order the work does not
|
|
1910
|
-
have. Rows are ticked ONE AT A TIME; where the run is a sequence, derive position from the
|
|
1837
|
+
difference between a SEQUENCE and a set picked in any order.** Rows are ticked ONE AT A TIME; where the run is a sequence, derive position from the
|
|
1911
1838
|
highest stamp so one click marks everything below it — a parent that ticked its children wrote
|
|
1912
1839
|
what the reader never said (today's date onto milestones that happened on unrecorded days),
|
|
1913
|
-
and a rung passed without a recorded day shows a BLANK date
|
|
1914
|
-
|
|
1840
|
+
and a rung passed without a recorded day shows a BLANK date. **`ChecklistGroup` is a phase — a
|
|
1841
|
+
name, and at most a way to fold its rows away**: no
|
|
1915
1842
|
COMPLETION control, because a phase
|
|
1916
1843
|
owns no completion of its own and a ring there is a control that never responds (one identical
|
|
1917
1844
|
to the pressable rings beneath it invites the press it refuses); and no BODY, because a
|
|
@@ -1971,7 +1898,7 @@ component rather than showing it at zero.
|
|
|
1971
1898
|
`accept`, `label`/`hint`/`dropLabel`, `height`); click falls back to a picker, and while
|
|
1972
1899
|
mounted it ALSO takes a paste (Ctrl/Cmd+V) via its inner `FileDropTarget` `paste`, scoped to
|
|
1973
1900
|
the well's region. `label`/`dropLabel`/`hint` default from the locale's `fileDropzone` slice
|
|
1974
|
-
(the hint
|
|
1901
|
+
(the hint names the paste) — pass them only to override.
|
|
1975
1902
|
- **`file_drop_target`** — `FileDropTarget`: makes ANY region accept a file drag-drop —
|
|
1976
1903
|
`{ onFiles, accept?, multiple?, paste?, disabled?, children, style? }` wrapped around a
|
|
1977
1904
|
section, a card, a whole panel, so nobody hunts for a dropzone. While a drag hovers, the
|
|
@@ -2008,8 +1935,8 @@ component rather than showing it at zero.
|
|
|
2008
1935
|
needs, so withholding `onAdd`/`onRemove` IS the read-only shape — there is no `readOnly`
|
|
2009
1936
|
mode. **A HOST verb is a plain `Button`** reading **`useFilesEditorSelection()`**
|
|
2010
1937
|
(`{selected, selectedIds, files, selectMode, clear, exit}`) — an AI read over the picked
|
|
2011
|
-
papers, "send to the broker", a ZIP: acts the kit has never heard of
|
|
2012
|
-
|
|
1938
|
+
papers, "send to the broker", a ZIP: acts the kit has never heard of. **No children means no
|
|
1939
|
+
bar** (a grid that only
|
|
2013
1940
|
previews). Bar words resolve prop → `LoticsLocale.filesEditor`; in-flight tile words →
|
|
2014
1941
|
`LoticsLocale.fileUpload`. **No empty state:** with zero files it is a bare bar, so pair it
|
|
2015
1942
|
with a `FileDropzone` for the well a records screen opens on. Use `FileGrid`/`FileRows` bare
|
|
@@ -2038,8 +1965,8 @@ component rather than showing it at zero.
|
|
|
2038
1965
|
- **`file_thumbnail`** — **`diff`** puts a `DiffKind` corner mark on a tile (top-LEFT, the one
|
|
2039
1966
|
corner remove/selection/uploading do not claim) and fades a `removed` one. The mark rides a
|
|
2040
1967
|
white RING: a tile's ground is an arbitrary photo, and a pale tint over a pale scan is a
|
|
2041
|
-
smudge. A `removed` tile fades its BODY only — the mark stays at full strength
|
|
2042
|
-
|
|
1968
|
+
smudge. A `removed` tile fades its BODY only — the mark stays at full strength. Ignored at
|
|
1969
|
+
the ROW rung: a corner disc on a row-sized tile is the tile, and such a tile lives
|
|
2043
1970
|
inside a row that carries the change instead. A grid says MEMBERSHIP well (arriving, leaving) and PAIRING not at
|
|
2044
1971
|
all — nowhere on a 96px tile to name what a replacement supersedes — so a set where things
|
|
2045
1972
|
are being REPLACED wants `FileRow`. Also: `FileThumbnail` + `DisplayFile` + `THUMBNAIL_SIZE` /
|
|
@@ -2071,7 +1998,7 @@ component rather than showing it at zero.
|
|
|
2071
1998
|
+ a ⋯ Download/Open-external/Remove menu; composes `FileRow` + `ActionMenu` +
|
|
2072
1999
|
`FileGalleryModal`; localized via the `gallery` locale slice. **`meta` has NO default**: the
|
|
2073
2000
|
badge states the type as colour and as a label, and the filename ends in the extension, so a
|
|
2074
|
-
type caption
|
|
2001
|
+
type caption is a fourth statement of one fact costing a line of row height. Pass a second
|
|
2075
2002
|
line only when it carries something the NAME does not — what an invoice covers, who uploaded
|
|
2076
2003
|
it, when it landed.
|
|
2077
2004
|
- **`file_badge`** — `FileBadge`: the two-tone file-type mark (PDF / XLSX / DOCX / video /
|
|
@@ -2101,8 +2028,7 @@ component rather than showing it at zero.
|
|
|
2101
2028
|
absolutely inside a positioning wrapper the component owns) because a `<video>` is a
|
|
2102
2029
|
replaced element: once metadata loads it reports the source's intrinsic size, and any
|
|
2103
2030
|
ancestor sized by its content grows to match — with RN-web's `flexShrink: 0` default,
|
|
2104
|
-
nothing pushes back
|
|
2105
|
-
the moment it loaded. **Do not add your own fullscreen/expand control**: the native
|
|
2031
|
+
nothing pushes back. **Do not add your own fullscreen/expand control**: the native
|
|
2106
2032
|
transport already carries fullscreen, picture-in-picture and download, and a second
|
|
2107
2033
|
affordance beside them is both redundant and a claim about where it leads. A source it
|
|
2108
2034
|
cannot load is surfaced, never silent — a media element that fails paints an EMPTY BOX and
|
|
@@ -2167,13 +2093,27 @@ component rather than showing it at zero.
|
|
|
2167
2093
|
state }`); `renderToolOutput(call)` replaces a step's default Output panel (the one render escape
|
|
2168
2094
|
hatch — input/error untouched). A per-tool failure shows amber with the reason in its expanded
|
|
2169
2095
|
Error panel; a run-level breaking `error` (outside `parts`) renders as a terminal danger row, with
|
|
2170
|
-
an optional `onRetry` Button under it.
|
|
2171
|
-
|
|
2096
|
+
an optional `onRetry` Button under it. While it runs the whole feed is ONE row and NO prose: the
|
|
2097
|
+
row names the call in flight ("Searching records") and says "Thinking…" whenever the
|
|
2098
|
+
model is writing rather than calling. The agent's between-call narration is never shown — the one
|
|
2099
|
+
message needing a response is a bulk-mutation confirmation, and the agent waits there, so the run
|
|
2100
|
+
stops and that text becomes the answer. A reply with no tool calls is all answer and still streams.
|
|
2101
|
+
At settle the work folds to "{last action}" with the answer below. Expanding is the one
|
|
2102
|
+
place live and settled differ: opened mid-run the timeline is capped and self-pinning (220px),
|
|
2103
|
+
opened settled it is uncapped. Stays fully open while a call is parked, when a SETTLED run ended on
|
|
2104
|
+
a tool call, and when the work is already one row. `collapseProcess={false}` hands the job back to
|
|
2105
|
+
the caller (a surface that already frames the run — it then owns the `FollowScroll` too);
|
|
2106
|
+
`summarizeRun(steps)` renames the row.
|
|
2107
|
+
Fixed chrome (Thinking / Input / Output /
|
|
2108
|
+
Error / awaiting / Retry) localizes via the `agentRun` locale slice. Needs `ai` as an optional type-only peer. AI
|
|
2172
2109
|
work ONLY — deterministic work (template fill, an export) shows a `loading` state on its trigger
|
|
2173
2110
|
and delivers at once; dressing it as an agent run is theater.
|
|
2174
2111
|
- **`agent_transform`** — internal (no component): the one place that folds an ai-sdk `UIMessage`'s
|
|
2175
2112
|
`parts` into the render timeline (`AgentUIPart`, `toSegments`, `lastRunningStep`), shared by
|
|
2176
|
-
`AgentRun` and `AgentProgress`.
|
|
2113
|
+
`AgentRun` and `AgentProgress`. Also `splitTimeline` → `AgentTimeline` (`{ process, result,
|
|
2114
|
+
steps }`): the run's work/answer split, cut POSITIONALLY at the last tool call because a
|
|
2115
|
+
model emits its plan, its asides and its report all as `text`. The sole file permitted to
|
|
2116
|
+
`import type` from `ai`.
|
|
2177
2117
|
- **`agent_progress`** — `AgentProgress`: `AgentRun`'s compact, floating, expandable form (same
|
|
2178
2118
|
`parts` prop) — a composer's working state.
|
|
2179
2119
|
- **`approval_prompt`** — `ApprovalPrompt`: the surface that ANSWERS an agent's approval-gated
|
|
@@ -2201,10 +2141,9 @@ component rather than showing it at zero.
|
|
|
2201
2141
|
- **`confidence`** — `Confidence` + `ConfidenceLevel` + `levelFromScore`: confidence IS a
|
|
2202
2142
|
callout — the meter + level phrase head the block, the REQUIRED `children` carry the basis
|
|
2203
2143
|
(what was checked, what failed); the box stays white with a hairline border at every
|
|
2204
|
-
level — the meter is the only colored element. There is NO standalone meter — a level without
|
|
2205
|
-
be rendered;
|
|
2206
|
-
|
|
2207
|
-
high/med/low; localized via the provider.
|
|
2144
|
+
level — the meter is the only colored element. There is NO standalone meter — a level without
|
|
2145
|
+
its reasons cannot be rendered; rank / badges / severity carry the standing instead. Also:
|
|
2146
|
+
calibrated high/med/low; localized via the provider.
|
|
2208
2147
|
- **`diff_value`** — **`DiffValue`, THE atom of a review surface**: a value that changed,
|
|
2209
2148
|
rendering `before` struck above/before `after`. Owns no container, no context, no decision —
|
|
2210
2149
|
so it drops into a table cell, a `DetailRow`, a total, a chip or a sentence and inherits that
|
|
@@ -2212,16 +2151,15 @@ component rather than showing it at zero.
|
|
|
2212
2151
|
a chip for a link being repointed — and a node `before` is struck by a DRAWN rule rather than
|
|
2213
2152
|
by text decoration, because `line-through` on a `Text` does not cross a `View` child (the old
|
|
2214
2153
|
value would otherwise render at full strength beside its replacement with nothing saying it is
|
|
2215
|
-
the one going away
|
|
2216
|
-
genuinely is not text: an avatar carries identity a string cannot
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
the column's own size, not a miniature badge. Worked in `dev/pages/ai.tsx` § every shape.
|
|
2154
|
+
the one going away). Reach for a node when the value
|
|
2155
|
+
genuinely is not text: an avatar carries identity a string cannot. A node belongs where it
|
|
2156
|
+
fits at FULL size — a
|
|
2157
|
+
form row, a card. Shrinking a composite into a table cell takes its own type off the scale,
|
|
2158
|
+
so a file in a CELL is a filename at the column's own size, not a miniature badge. Worked in
|
|
2159
|
+
`dev/pages/ai.tsx` § every shape.
|
|
2222
2160
|
**`before === after` collapses to ONE plain value** —
|
|
2223
2161
|
agreement is not a change, and a document confirming what the record already says is the
|
|
2224
|
-
GOOD case
|
|
2162
|
+
GOOD case. **`delta`** (a host-formatted string, the
|
|
2225
2163
|
same contract as `Finding.delta`) prints HOW FAR it moved, because
|
|
2226
2164
|
`1.600.000 → 1.481.481` otherwise makes the reader subtract to find out whether a
|
|
2227
2165
|
correction is trivial or alarming. `layout` stacked (a column of figures) | inline (prose, dense
|
|
@@ -2229,9 +2167,9 @@ component rather than showing it at zero.
|
|
|
2229
2167
|
for an addition, `after` for a removal. BOTH values stay on screen: the old one is the only
|
|
2230
2168
|
evidence the reader has that the correction is the right SIZE.
|
|
2231
2169
|
**`placeholder` is YOUR string — translate it.** The component renders no word of its own
|
|
2232
|
-
(its only fallbacks are `—` and empty), so nothing here reaches the locale packs.
|
|
2233
|
-
|
|
2234
|
-
|
|
2170
|
+
(its only fallbacks are `—` and empty), so nothing here reaches the locale packs.
|
|
2171
|
+
`DiffMark` beside it DOES ship localized words, but the two are different sentences even when
|
|
2172
|
+
English collides them — the mark says what happened to
|
|
2235
2173
|
the ROW ("Removed" / "Xóa"), the placeholder says what is in the FIELD now, which reads "đã
|
|
2236
2174
|
xóa" or "bỏ trống", never the bare verb. The same prop also carries "Pick a candidate below"
|
|
2237
2175
|
and "Not on the document". Pull it from your own catalog.
|
|
@@ -2240,9 +2178,8 @@ component rather than showing it at zero.
|
|
|
2240
2178
|
accessible word (never colour alone — "added" vs "removed" by hue is invisible to a reader who
|
|
2241
2179
|
cannot separate them, and that is the distinction a review turns on). **ONE treatment
|
|
2242
2180
|
everywhere: a filled 22px DISC**, 14px glyph, 700 ink on a 100 ground. A form marked with bare
|
|
2243
|
-
glyphs beside a table marked with filled shapes reads as two systems saying the same thing
|
|
2244
|
-
|
|
2245
|
-
principled the reason. The disc also gives the glyph its own ground, which is what lets one
|
|
2181
|
+
glyphs beside a table marked with filled shapes reads as two systems saying the same thing.
|
|
2182
|
+
The disc also gives the glyph its own ground, which is what lets one
|
|
2246
2183
|
mark sit on a white row, a tinted cell, or a photograph. **`unchanged` renders as an empty
|
|
2247
2184
|
disc-sized HOLE** — silence is the honest rendering of "nothing happened", it keeps the
|
|
2248
2185
|
column's width so nothing shifts left, and it announces nothing (eleven "Unchanged"es is the
|
|
@@ -2253,8 +2190,7 @@ component rather than showing it at zero.
|
|
|
2253
2190
|
an `Inline*` editor draws (40px band, 8px horizontal padding inside a 1px transparent border),
|
|
2254
2191
|
minus the editor. A record's value column mixes editors with things that are not editors, and
|
|
2255
2192
|
anything rendered raw beside them starts at the cell edge, 20px tall, at the top — so the
|
|
2256
|
-
column quietly acquires TWO left edges and two baselines
|
|
2257
|
-
down to meet a control) agrees with one kind of row and not the other. `InlineStatic` is the
|
|
2193
|
+
column quietly acquires TWO left edges and two baselines. `InlineStatic` is the
|
|
2258
2194
|
STRING case and composes it; reach for the slot when the value is a node, which a review
|
|
2259
2195
|
surface always is. An ANNOTATED row needs the grid even with no editor in sight, because the
|
|
2260
2196
|
annotation stack is inset to a control's words — data_entry.md §The editability affordance
|