@lotics/ui 43.2.0 → 43.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -9
- package/MIGRATION.md +44 -0
- package/docs/ai_patterns.md +11 -18
- package/docs/catalog.md +20 -28
- package/docs/composition.md +5 -16
- package/docs/data_entry.md +54 -38
- package/docs/templates.md +133 -48
- package/examples/tpl_record.tsx +349 -272
- package/package.json +1 -1
- package/src/inline_edit.tsx +34 -1
- package/src/inline_text_input.tsx +19 -4
- package/src/timeline.tsx +16 -5
package/AGENTS.md
CHANGED
|
@@ -16,7 +16,7 @@ CURRENT major only — upgrading an app across majors is `MIGRATION.md`.
|
|
|
16
16
|
|---|---|
|
|
17
17
|
| [docs/catalog.md](./docs/catalog.md) | **The complete inventory** — Reach-by-role (each data role → the ONE canonical component) + every `@lotics/ui/<module>` entry point (incl. `@lotics/ui/vite`'s `loticsOptimizeDeps` + `loticsResolve()` — the pre-bundle list and the whole `resolve` block a custom-code app's `vite.config.ts` imports rather than hand-carries, dev-link included). Read before building any screen; reuse first. |
|
|
18
18
|
| [docs/data_entry.md](./docs/data_entry.md) | Which editing pattern for which job — inline edit, fieldset forms, browser-autofill suppression (search controls only), find-or-create (`Combobox`), line items, handoffs, phased records, billing, tags, dispositions, attachments (`InlineFiles` for a record ROW — list + one CTA, `multiple` decides add-vs-replace; the `FilesEditor` COMPOUND for a whole section — root owns selection/gallery/confirm, you compose the bar, a HOST verb reads `useFilesEditorSelection` — plus the three-way file INTAKE: CTA + `FileDropTarget` + `usePasteFiles`), stage gates, whether a multiline value keeps its fixed reserve or `autoGrow`s (who decides the length — the field, or whoever is typing), the commit-on-blur vs action-press ordering law (the kit gates the press — `pending_commits`). |
|
|
19
|
-
| [docs/ai_patterns.md](./docs/ai_patterns.md) | AI acts, the human stays in charge — composer, live run feed (`AgentRun`), the one law's split — it turns on WHO supplied the values (machine → a gate: a diff when something is being replaced, a full editable preview when records are being created from a document; human-typed → save-direct + the `ResultHeader` receipt), findings, provenance, confidence; **after the run** — a stored record that fills up from several writers (a person, a chat agent, an extraction, an automation) and remembers none of them: an unwritten value must not render like a written one, a machine's prose and a person's must not share a treatment, model markdown goes in `variant="embedded"`, and the read path must project every field the write paths set; the UI half of the SDK's [ai doc](../app-sdk/docs/ai.md)
|
|
19
|
+
| [docs/ai_patterns.md](./docs/ai_patterns.md) | AI acts, the human stays in charge — composer, live run feed (`AgentRun`), the one law's split — it turns on WHO supplied the values (machine → a gate: a diff when something is being replaced, a full editable preview when records are being created from a document; human-typed → save-direct + the `ResultHeader` receipt), findings, provenance, confidence; **after the run** — a stored record that fills up from several writers (a person, a chat agent, an extraction, an automation) and remembers none of them: an unwritten value must not render like a written one, a machine's prose and a person's must not share a treatment, model markdown goes in `variant="embedded"`, and the read path must project every field the write paths set; the UI half of the SDK's [ai doc](../app-sdk/docs/ai.md), the whole run in a dialog (`AgentRunScope`/`AgentRunPane`/`AgentRunActions` — a parked question REPLACES the feed, actions in the footer, **Stop** while streaming), **stopping** (`cancel` stops the run, `abort` only stops listening — so closing a dialog must `cancel` or it keeps billing); **review surfaces compose from atoms** — `DiffValue` (a changed value, droppable in any cell/row/total), `DiffMark` (what happened to the row — ONE circular disc, every surface), `useChangeSet` (accept/reject/undo bookkeeping, no layout) — see [MIGRATION.md](./MIGRATION.md) for the `ChangeReview` family they replace |
|
|
20
20
|
| [docs/composition.md](./docs/composition.md) | The design-language contract — canvas + content column, heading altitude (incl. eyebrow vs group lead — a label is one or the other), banded cards, register vs inset rows (incl. the register laws a row centres its cells by: every cell a FIXED height, a pressable cell on the shared hover token, a column sized by what it carries), the button ladder and **what underlined text may mean** (it GOES somewhere or REVEALS something — never mutates; blue leaves the surface, muted stays on it, and the one in-prose disclosure exception is scoped there), master-detail `Drawer` on a LIST screen vs a child collection's row EXPANDING inside a record, view controls, RECORD EXTENT (one page, sections scrolled to and never routed to), color discipline, typography, whitespace, and how to TEST an overlay component (a `Popover`-backed surface never mounts under jsdom). |
|
|
21
21
|
| [docs/testing.md](./docs/testing.md) | Driving the kit in a browser — the three anatomies where the a11y tree says one thing and a driver must do another: a `PressDoor` row whose named button always intercepts pointer events (by design), portalled overlays that render at the top of the DOM, and custom pointer drag that `dragTo` cannot move. |
|
|
22
22
|
| [docs/templates.md](./docs/templates.md) | The map of `examples/tpl_*.tsx` — what shape each template solves and which to start from (copy + adapt, never import) — plus the record-surface composition rules (pipeline order, static shape, decision budget) and the ACTIVITY shape — a communications feed where the row's label is the GIST and the body varies by medium, one anatomy rather than a row type per kind. |
|
|
@@ -55,8 +55,7 @@ CURRENT major only — upgrading an app across majors is `MIGRATION.md`.
|
|
|
55
55
|
order, where a line claims an order the work does not have. **`mark`** asks *does ticking mean
|
|
56
56
|
DONE or CHOSEN* — a ring for completion, the square box for picking (which forms to produce,
|
|
57
57
|
which records to export). Spend the ring on a choice and a reader who learned that a filled
|
|
58
|
-
ring means "done" reads a ticked pick-list as work already carried out.
|
|
59
|
-
task/pipeline families that had converged on one anatomy. A GROUP is a phase — a name, optionally a
|
|
58
|
+
ring means "done" reads a ticked pick-list as work already carried out. A GROUP is a phase — a name, optionally a
|
|
60
59
|
Show/Hide disclosure, and no body: it owns no COMPLETION, and a ring that never responds invites
|
|
61
60
|
a press it refuses. Rows tick ONE at a time; derive position from the highest stamp and one click marks
|
|
62
61
|
everything below it without inventing dates. A checklist REPORTS — a field owned by a section is
|
|
@@ -101,17 +100,13 @@ CURRENT major only — upgrading an app across majors is `MIGRATION.md`.
|
|
|
101
100
|
padding. `PressableRow` (all variants), `Table`, `ChoiceList`, `ListItem` and `FileRow`
|
|
102
101
|
already do this, so a screen composed from the kit aligns with **no per-site padding at all**.
|
|
103
102
|
The moment one component insets instead, every neighbour has to be hand-padded by the same
|
|
104
|
-
amount to catch up, and that compensation spreads to each new surface built beside it
|
|
105
|
-
is how a `SummaryLine`, a `Clarify` question and a bare table's footer each acquired a
|
|
106
|
-
padding they should never have needed. **An indent is legible only when something VISIBLE
|
|
103
|
+
amount to catch up, and that compensation spreads to each new surface built beside it. **An indent is legible only when something VISIBLE
|
|
107
104
|
occupies it** — a checkbox, an ordinal, a diff mark. An empty indent is a defect.
|
|
108
105
|
- **The one exception is a self-contained OVERLAY.** A menu or a selector's popover
|
|
109
106
|
(`MenuListItem`, `OptionList`) has nothing outside itself to align with, so its rows keep
|
|
110
107
|
plain padding. Bleeding there would push rows past the popover's own edge.
|
|
111
108
|
- **A slot holding another component's part takes THAT component's constant**, never a
|
|
112
|
-
hand-picked number that happens to look close
|
|
113
|
-
`Marker` it holds is `NODE` (24), so a 24px dot centred in an 18px column put the whole run
|
|
114
|
-
rail 3px left of every other band in the dialog. Import the owner's constant.
|
|
109
|
+
hand-picked number that happens to look close. Import the owner's constant.
|
|
115
110
|
- Inside a `Dialog`, every band reads `dialogGutter(small)` — header, scroll area, footer and
|
|
116
111
|
any pane a caller drops in. A hand-copied `small ? 16 : 24` is a fifth copy of a number
|
|
117
112
|
that has to agree with four others.
|
package/MIGRATION.md
CHANGED
|
@@ -4,6 +4,37 @@ Breaking changes, newest first — normally per major, plus the rare minor that
|
|
|
4
4
|
anyway (recorded under its exact version). The current contract lives in `AGENTS.md` + `docs/`;
|
|
5
5
|
this file exists only to move an app from one release to the next.
|
|
6
6
|
|
|
7
|
+
## 43.3.0 — a `bare` inline field sits on the column, and Enter finishes a one-line one
|
|
8
|
+
|
|
9
|
+
Two fixes to `Inline*`, both of them behaviour a caller cannot see in a prop name.
|
|
10
|
+
|
|
11
|
+
**A `bare` field now aligns its TEXT, not its box.** The field surface carries 8px
|
|
12
|
+
of horizontal padding, which is invisible at rest on a variant that draws no
|
|
13
|
+
border — so every bare value rendered 8px right of whatever named it. In a
|
|
14
|
+
`DataGrid` that meant each column's values sat off its own header. The inset is
|
|
15
|
+
now bled back out with a negative LEFT margin: glyphs land on the container's
|
|
16
|
+
edge, the hover/focus border overhangs into the gutter, and hover moves nothing.
|
|
17
|
+
|
|
18
|
+
Left only, deliberately. A matching right bleed aligns nothing on left-aligned
|
|
19
|
+
text and pushes the frame past the edge its neighbours stop at, which shows the
|
|
20
|
+
moment the border paints. **If you compensated with your own negative margin,
|
|
21
|
+
remove it** — you will now be 8px out the other way. A field needs ~8px of slack
|
|
22
|
+
on its left to hover into; in a zero-gap column, adjacent borders will touch. A
|
|
23
|
+
DISABLED `framed` field is unaffected: it drops its border and fill so it cannot
|
|
24
|
+
promise a press, but it stays in line with the enabled fields beside it.
|
|
25
|
+
|
|
26
|
+
**`numberOfLines` now decides what ENTER does — wrapping no longer does.** The two
|
|
27
|
+
were one flag, so `autoGrow` — a prop about fitting the value — silently turned
|
|
28
|
+
Enter from "save" into "insert a newline". A one-line summary field that grew when
|
|
29
|
+
it ran long became a field you could only leave by clicking away, and nothing at
|
|
30
|
+
the call site said so.
|
|
31
|
+
|
|
32
|
+
A field left at one line (or `numberOfLines={1}`) holds a single-line value that
|
|
33
|
+
may WRAP, and Enter commits it. Declaring 2+ lines declares PROSE: Enter is a
|
|
34
|
+
paragraph break and blur saves, exactly as before. **If you wrote
|
|
35
|
+
`numberOfLines={1} autoGrow` and relied on Enter inserting a newline, raise the
|
|
36
|
+
line budget** — that is now what says so.
|
|
37
|
+
|
|
7
38
|
## 43.0.0 — a time is PICKED, and there is nothing to type
|
|
8
39
|
|
|
9
40
|
`TimePicker` is no longer a segmented text field. It is the value on a pressable
|
|
@@ -78,6 +109,19 @@ its reasoning are in `docs/composition.md` § The pointer cursor. If a component
|
|
|
78
109
|
needs the pointer, set `CURSOR_ACTION` in its style — and if it is built on the bare
|
|
79
110
|
react-native `Pressable`, note that the pointer is what you get by DEFAULT, from
|
|
80
111
|
react-native-web, unless you set `CURSOR_DEFAULT`.
|
|
112
|
+
## 42.5.0 — an expanded `Timeline` row shows its whole label
|
|
113
|
+
|
|
114
|
+
The label was clamped to two lines whether the row was open or shut. Expanding a
|
|
115
|
+
row is a request to see more of what is there, so a label that stays truncated
|
|
116
|
+
through it leaves the caller exactly one way to make its own label readable:
|
|
117
|
+
repeat it inside `details`. Then the reader meets the same sentence twice — once
|
|
118
|
+
cut off, once whole — which reads as a rendering bug rather than as disclosure.
|
|
119
|
+
|
|
120
|
+
It now clamps while collapsed only. **If you duplicated a label into `details`
|
|
121
|
+
to work around this, delete the copy**; the row will show it in full on its own.
|
|
122
|
+
Nothing else changes: a collapsed row is identical, and a row whose label already
|
|
123
|
+
fitted in two lines is identical open or shut.
|
|
124
|
+
|
|
81
125
|
## 42.4.0 — `Picker` is named by its `FormField`, and its way back to empty has a name
|
|
82
126
|
|
|
83
127
|
Two fixes to the same control, both about a thing on screen that the control did
|
package/docs/ai_patterns.md
CHANGED
|
@@ -124,8 +124,7 @@ transition — never leave a live button waiting on a network round-trip:
|
|
|
124
124
|
|
|
125
125
|
- **Same-tick feedback.** On press, synchronously close the dialog / swap to the run surface
|
|
126
126
|
(`AgentRun` with its streaming state) so there is nothing left to click. A button that stays
|
|
127
|
-
pressable while an attach/start request round-trips invites double-fires
|
|
128
|
-
production as stacked paid runs.
|
|
127
|
+
pressable while an attach/start request round-trips invites double-fires.
|
|
129
128
|
- **Single-flight in app code too.** Guard the handler with a ref (state alone races the
|
|
130
129
|
re-render): first press wins, later presses no-op until the flow settles.
|
|
131
130
|
- **The SDK backstops runs, not workflows.** `useAgentRun().run()` is single-flight by default
|
|
@@ -242,14 +241,10 @@ feed**; the starting row IS the placeholder, consistent and localized.
|
|
|
242
241
|
(Sonnet / Opus, not Haiku); the renderer shows every reasoning part it's handed.
|
|
243
242
|
|
|
244
243
|
**Localization.** The feed's fixed chrome — the "Thinking" label, the "Input"/"Output"/"Error"
|
|
245
|
-
panel titles, the "awaiting" annotation,
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
contradicting itself. The "{n} steps" suffix is locale-backed too (`agentRun.steps`). `labelForCall`
|
|
250
|
-
and `stepsLabel` remain as per-call-site OVERRIDES — for phrasing a row by what
|
|
251
|
-
it targets — not as the only route to a translation. A tool name in neither the locale map nor
|
|
252
|
-
`labelForCall` falls back to its prettified form.
|
|
244
|
+
panel titles, the "awaiting" annotation, the "Retry" action, the tool labels (`agentRun.tools`,
|
|
245
|
+
keyed by raw tool name) and the "{n} steps" suffix (`agentRun.steps`) — resolves through the
|
|
246
|
+
`LoticsLocale` `agentRun` slice (translate once at the provider; a `vi` app gets them for free);
|
|
247
|
+
`labelForCall` and `stepsLabel` remain as per-call-site OVERRIDES.
|
|
253
248
|
|
|
254
249
|
**Answering an awaiting call — `ApprovalPrompt`.** The `awaiting` row inside `AgentRun` is
|
|
255
250
|
READ-ONLY — it only NAMES that a call is parked on a human decision. The surface that ANSWERS it
|
|
@@ -466,8 +461,7 @@ rather than wherever the feed was scrolled to.
|
|
|
466
461
|
holds the wizard's verbs while a question is up, **Stop** while the run streams, and nothing once
|
|
467
462
|
it settles; the RUN decides, which is the whole point of handing it the run. A host that names a
|
|
468
463
|
single phase out here duplicates that decision and gets it wrong the moment the bar learns a new
|
|
469
|
-
one
|
|
470
|
-
stop.
|
|
464
|
+
one.
|
|
471
465
|
|
|
472
466
|
**`run` is a SHAPE, not an import** (`AgentRunLike`: `status`, `parts`, `pendingChoice`,
|
|
473
467
|
`answerChoice`, `cancel`, `error`; `AgentRunQuestion` names what `pendingChoice` carries, so a template or
|
|
@@ -610,8 +604,8 @@ ranked set, don't cram it into chat prose:
|
|
|
610
604
|
document-driven records) — the record's files block feeds ONE "Use AI" entry that FORKS into
|
|
611
605
|
the two document tasks, each a specialized run with a task-pure result: **Extract** (files
|
|
612
606
|
read → fields already matching fold into one quiet line → every add / update / conflict a
|
|
613
|
-
|
|
614
|
-
a first-class choice — plus proposed new lines as record-body `Change`s →
|
|
607
|
+
`DiffValue` per field (the struck current value, the proposed one, candidate rows), the record's current value
|
|
608
|
+
a first-class choice — plus proposed new lines as record-body `Change`s →
|
|
615
609
|
an explicit outcome-named commit) and **Cross-check** (documents compared against the record and
|
|
616
610
|
each other → ranked `Finding`s — severity, title, the prominent metric, sources — separated
|
|
617
611
|
by hairlines; the findings ARE the outcome the human acts on). The fork carries an OPTIONAL
|
|
@@ -642,12 +636,11 @@ ranked set, don't cram it into chat prose:
|
|
|
642
636
|
## The visual vocabulary — functional colour, no gimmicks
|
|
643
637
|
|
|
644
638
|
The AI vocabulary has **no purple accent and no gimmick glyphs** (no sparkles) — but it is NOT
|
|
645
|
-
monochrome: **colour is used where it carries meaning, not for decoration.**
|
|
646
|
-
sparkle used to carry now reads structurally — **provenance** is a quiet sentence-case microlabel
|
|
639
|
+
monochrome: **colour is used where it carries meaning, not for decoration.** **Provenance** is a quiet sentence-case microlabel
|
|
647
640
|
naming the artifact ("Proposed", "Match", "Mismatch", "Suggested edit", "Question" — xs,
|
|
648
|
-
muted, medium; all-caps is banned kit-wide),
|
|
641
|
+
muted, medium; all-caps is banned kit-wide), the agent's **reasoning** is a left-ruled margin
|
|
649
642
|
note (a hairline rule + muted text — `ChangeReasoning`), quoted apart from the facts and the
|
|
650
|
-
human's controls
|
|
643
|
+
human's controls, and **status/severity/diffs use functional colour** the way the rest of the
|
|
651
644
|
kit does:
|
|
652
645
|
|
|
653
646
|
- **Confidence** — the 3-tick meter + the full phrase, emerald / amber / zinc by level.
|
package/docs/catalog.md
CHANGED
|
@@ -80,8 +80,7 @@ renders these per option.)
|
|
|
80
80
|
|
|
81
81
|
**Size is a RUNG, never a pixel count** — `sm | md | lg | xl` = 24 / 28 / 40 / 72
|
|
82
82
|
(`@lotics/ui/avatar_size`, shared by `Avatar`, `MemberChip` and `GroupAvatar`). Default `md`.
|
|
83
|
-
|
|
84
|
-
the product and its apps, so one person looked different on every screen. Pick by density:
|
|
83
|
+
Pick by density:
|
|
85
84
|
`sm` for a dense row or inline cell, `md` for a control band, register or list (the default),
|
|
86
85
|
`lg` for a prominent row or the account menu, `xl` for a profile's own header.
|
|
87
86
|
|
|
@@ -89,9 +88,7 @@ the product and its apps, so one person looked different on every screen. Pick b
|
|
|
89
88
|
avatar is IN a control or IS one.** `md` is `CONTROL_CONTENT_HEIGHT` (28) — the largest rung a
|
|
90
89
|
40px control band seats without growing, and the height `InlineButton` and a `md` `IconButton`
|
|
91
90
|
already used to sit inside a field. An avatar is the tallest thing in whatever row holds it, so
|
|
92
|
-
a rung that overshoots doesn't look slightly big, it RESIZES the control
|
|
93
|
-
member chip measured 46 in a `Select` trigger, 50 in an inline editor and 54 in an `InlineSlot`,
|
|
94
|
-
because each surface had picked its own inset with no number to pick it from. Put nothing taller
|
|
91
|
+
a rung that overshoots doesn't look slightly big, it RESIZES the control. Put nothing taller
|
|
95
92
|
than `md` in a control. `lg` is `CONTROL_HEIGHT` (40) — the avatar that stands in a band on its
|
|
96
93
|
own, an account menu beside a row of icon buttons, so it measures what they do. Anything bigger
|
|
97
94
|
belongs in a row that sets its own height.
|
|
@@ -155,8 +152,7 @@ axes carry every shape it takes, and BOTH are decisions about meaning, not looks
|
|
|
155
152
|
work already carried out, and nothing on screen corrects them. (The shapes are 24px either way
|
|
156
153
|
and share one gutter, so a surface swaps between them without moving anything.)
|
|
157
154
|
|
|
158
|
-
A record's Progress and a desk's to-do list are the same component two booleans apart
|
|
159
|
-
two families until the anatomy proved identical.
|
|
155
|
+
A record's Progress and a desk's to-do list are the same component two booleans apart.
|
|
160
156
|
|
|
161
157
|
**A row carries its TITLE and its own stamp, and nothing else.** A checklist REPORTS: it does not
|
|
162
158
|
collect, and it does not PREVIEW. A field some section owns — an assignee, an address, a portal
|
|
@@ -1068,7 +1064,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1068
1064
|
`DetailTable`: the compound parent of
|
|
1069
1065
|
a row STACK — `labelWidth` (default `DETAIL_LABEL_WIDTH`, 130 — THE kit's label column, and
|
|
1070
1066
|
the record surface's) / `minHeight` (default 40, the inline-control grid) declared
|
|
1071
|
-
ONCE + the `SPACE.md` (16) row gap —
|
|
1067
|
+
ONCE + the `SPACE.md` (16) row gap — a stack of
|
|
1072
1068
|
BORDERED rows that close together fuses into one block. TWO columns, no third: the value
|
|
1073
1069
|
FILLS what the label leaves, so nothing one row does can narrow its neighbours' editors. RESPONSIVE with no prop: it
|
|
1074
1070
|
measures its own container (onLayout, not
|
|
@@ -1078,7 +1074,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1078
1074
|
grammar); raise `minValueWidth` (default 160) when a cell holds
|
|
1079
1075
|
MORE than one editor so the table stacks earlier. Two tables on one page share one grid by
|
|
1080
1076
|
repeating the same labelWidth. Worked example:
|
|
1081
|
-
[`tpl_record`](../examples/tpl_record.tsx).
|
|
1077
|
+
[`tpl_record`](../examples/tpl_record.tsx).
|
|
1082
1078
|
- **`record_summary`** — `RecordSummary`: the identity band of a record detail/drawer — ONE
|
|
1083
1079
|
row: `title` xxl semibold tabular, `subtitle` sm muted, `status` Badge slot, optional
|
|
1084
1080
|
`metric` {label,value,tone,note} pinned right, the band's ONE accent. The record's FIELDS
|
|
@@ -1175,8 +1171,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1175
1171
|
and Save writes the pre-action values back over what it just fetched.
|
|
1176
1172
|
The peek uses the popover's OWN anatomy: `PopoverHeader` for the identity, a scrolling body,
|
|
1177
1173
|
and a **pinned `PopoverFooter`** for the verbs. A long block therefore scrolls with its actions
|
|
1178
|
-
still reachable
|
|
1179
|
-
own Save.
|
|
1174
|
+
still reachable.
|
|
1180
1175
|
`Edit` swaps the SAME grid's value cells for inputs — a DRAFT, so nothing commits until `Save`,
|
|
1181
1176
|
which fires `onSave` with **only the facts that CHANGED** (never a snapshot, so a lock or
|
|
1182
1177
|
`before_update` sees the real edit). This is what lets a peek hold editors at all: a
|
|
@@ -1365,7 +1360,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1365
1360
|
never `style`**: `style` reaches only the BODY, and the panel is `max-content`, so a width set
|
|
1366
1361
|
there governs the scroller while the header and footer sit outside it — the first long title
|
|
1367
1362
|
then stretches the panel and leaves the content adrift in a box wider than itself. A popover
|
|
1368
|
-
with no bands never sees this
|
|
1363
|
+
with no bands never sees this.
|
|
1369
1364
|
The built-in scroller is **full-bleed horizontally** and re-insets its content by the same
|
|
1370
1365
|
amount, so a scrolling body reaches the panel's real edges — its content slides under the
|
|
1371
1366
|
panel edge rather than under an invisible margin 12px inside it — while the text stays on the
|
|
@@ -1437,8 +1432,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1437
1432
|
`PieChart`: the canonical SVG chart set (no recharts).
|
|
1438
1433
|
- **`progress_bar`** — `ProgressBar`: the determinate meter; `compact` = ONE row, track + a
|
|
1439
1434
|
plain sm tabular count beside it.
|
|
1440
|
-
- **`progress_ring`** — `ProgressRing`: the same meter in a circle, and the ONLY circular one
|
|
1441
|
-
(it replaced `RingGauge`). Same API shape as the bar — a real `value`/`max` rather than a
|
|
1435
|
+
- **`progress_ring`** — `ProgressRing`: the same meter in a circle, and the ONLY circular one. Same API shape as the bar — a real `value`/`max` rather than a
|
|
1442
1436
|
pre-computed percentage, optional `label`/`caption`, and a `format` that turns the centre
|
|
1443
1437
|
figure off. `format="none"` with no label leaves the arc bare at any size: that is the
|
|
1444
1438
|
TRIGGER form, for a control row where a figure would be unreadable and a bar would not
|
|
@@ -1487,9 +1481,12 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1487
1481
|
provider-wired — pass `labels` (`DEFAULT_GANTT_LABELS` is English).
|
|
1488
1482
|
- **`timeline`** — `Timeline`: a heterogeneous event LOG — per-row icon + expandable
|
|
1489
1483
|
details; models the past, NOT progress. Three things it decides for you, because a row
|
|
1490
|
-
cannot be trusted to a caller's data: the **label clamps to two lines
|
|
1491
|
-
description
|
|
1492
|
-
user-authored note set the section's height
|
|
1484
|
+
cannot be trusted to a caller's data: the **label clamps to two lines WHILE COLLAPSED**
|
|
1485
|
+
(the description always) — a row is a scan target, and an unclamped one lets a single
|
|
1486
|
+
user-authored note set the section's height. Opening the row un-clamps it, so **never
|
|
1487
|
+
repeat the label inside `details`**: the fact belongs to one element, and a copy means
|
|
1488
|
+
the reader meets the same sentence twice, cut off and then whole. `details` carries
|
|
1489
|
+
what the label does not — the attachments, the machine's reading, the provenance;
|
|
1493
1490
|
**`placeholder`** marks a label the SURFACE supplied rather than one the record
|
|
1494
1491
|
holds, drawing it in the unfilled-field ink — reach for it on any feed that fills
|
|
1495
1492
|
from more than one direction, where an entry routinely arrives with no words in it;
|
|
@@ -1502,10 +1499,9 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1502
1499
|
the first-line box and the chevron-pairing problems in the first place.
|
|
1503
1500
|
- **`checklist`** — `Checklist` + `ChecklistGroup` + `ChecklistItem` + `ChecklistNote` /
|
|
1504
1501
|
`ChecklistActions` / `ChecklistField`: THE run of tickable rows, at any
|
|
1505
|
-
altitude
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
line, and it is the whole difference**: on for ONE record walking positions (it asserts
|
|
1502
|
+
altitude — a ring, a title, a value beside it, a muted note, a detail block, an optional
|
|
1503
|
+
grouping level. **`connected` is the LINE joining the rings, and it is the whole
|
|
1504
|
+
difference**: on for ONE record walking positions (it asserts
|
|
1509
1505
|
sequence), off for a SET picked in any order, where a line claims an order the work does not
|
|
1510
1506
|
have. Rows are ticked ONE AT A TIME; where the run is a sequence, derive position from the
|
|
1511
1507
|
highest stamp so one click marks everything below it — a parent that ticked its children wrote
|
|
@@ -1659,10 +1655,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1659
1655
|
NODE** as well as a string, because a second line can itself be carrying a change (a file
|
|
1660
1656
|
being RECLASSIFIED is the same bytes filed differently — a `DiffValue` on the property); a
|
|
1661
1657
|
node renders outside the muted single-line `Text`, which would clip it and fight its colour.
|
|
1662
|
-
Both `sm` variants share ONE beat (48px min + 6px vertical padding)
|
|
1663
|
-
have neither, so it came out 37px against the pressable's 49 — whether a row was a door
|
|
1664
|
-
decided how tall it was — and 37 sat under every other row beat in the kit (`DetailRow` 40,
|
|
1665
|
-
`TableRow` 52, this component's own `md` 56), which is what "squeezed" literally was.
|
|
1658
|
+
Both `sm` variants share ONE beat (48px min + 6px vertical padding).
|
|
1666
1659
|
- **`file_rows`** — `FileRows`: batteries-included file list — row press → built-in gallery
|
|
1667
1660
|
+ a ⋯ Download/Open-external/Remove menu; composes `FileRow` + `ActionMenu` +
|
|
1668
1661
|
`FileGalleryModal`; localized via the `gallery` locale slice. **`meta` has NO default**: the
|
|
@@ -1835,9 +1828,8 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1835
1828
|
`added`/`changed`/`removed`/`unchanged`, for the cases no single value carries. Glyph + an
|
|
1836
1829
|
accessible word (never colour alone — "added" vs "removed" by hue is invisible to a reader who
|
|
1837
1830
|
cannot separate them, and that is the distinction a review turns on). **ONE treatment
|
|
1838
|
-
everywhere: a filled 22px DISC**, 14px glyph, 700 ink on a 100 ground.
|
|
1839
|
-
|
|
1840
|
-
glyphs beside a table marked with filled shapes read as two systems saying the same thing; a
|
|
1831
|
+
everywhere: a filled 22px DISC**, 14px glyph, 700 ink on a 100 ground. A form marked with bare
|
|
1832
|
+
glyphs beside a table marked with filled shapes reads as two systems saying the same thing; a
|
|
1841
1833
|
reader parses shape before meaning, so an unexplained difference reads as an accident however
|
|
1842
1834
|
principled the reason. The disc also gives the glyph its own ground, which is what lets one
|
|
1843
1835
|
mark sit on a white row, a tinted cell, or a photograph. **`unchanged` renders as an empty
|
package/docs/composition.md
CHANGED
|
@@ -380,12 +380,7 @@ holds another component's PART takes that component's constant, never a hand-pic
|
|
|
380
380
|
centred in an 18px column put the whole run rail 3px outside the surface's edge — every row
|
|
381
381
|
wrong by the same amount, so it read as internally consistent and nothing flagged it.
|
|
382
382
|
|
|
383
|
-
|
|
384
|
-
the 20px — it was that everything nearby had to be hand-padded by the same 20 to
|
|
385
|
-
line up with it. A `Table`'s header band, its static rows and its expanded detail
|
|
386
|
-
all inherited the number; a `SummaryLine` above the register got it at the call
|
|
387
|
-
site; a bare table's footer borrowed `CardFooter` and picked up a *card's*
|
|
388
|
-
interior gutter. **An indent is legible only when something VISIBLE occupies it**
|
|
383
|
+
**An indent is legible only when something VISIBLE occupies it**
|
|
389
384
|
— a checkbox, an ordinal, a diff mark. An empty one is a defect, and the
|
|
390
385
|
compensation it forces spreads to every new surface that sits beside it.
|
|
391
386
|
|
|
@@ -425,8 +420,7 @@ content off the register entirely.
|
|
|
425
420
|
ornament sits beside a text block and centres on it — a `Timeline`'s disc, a marker, an avatar,
|
|
426
421
|
a rail glyph — the ornament's position is a function of the text's height, so an unbounded text
|
|
427
422
|
block moves it. A feed is where this actually bites, because a feed is the one row primitive
|
|
428
|
-
whose text is written by the USER
|
|
429
|
-
drew a 180px label and put the disc 80px below the line it names. Bound the text; the row keeps
|
|
423
|
+
whose text is written by the USER. Bound the text; the row keeps
|
|
430
424
|
its beat and the ornament keeps its line. Where the full value still has to be readable, the row
|
|
431
425
|
EXPANDS to it — a truncation and its source, not a second copy.
|
|
432
426
|
|
|
@@ -530,9 +524,7 @@ non-interactive by construction. `TableRow` already composes `PressDoor`.
|
|
|
530
524
|
|
|
531
525
|
**A trailing slot is not a PEER of the row's own affordance — space them apart.** A disclosure
|
|
532
526
|
chevron belongs to the row's button; the slot beside it does not, and if the two are spaced like
|
|
533
|
-
siblings the eye reads one cluster.
|
|
534
|
-
and 12px from the chevron to the action menu, so the only thing saying they were different
|
|
535
|
-
targets was a 4px break in the hover wash — invisible to a reader who never hovers. Put clearly
|
|
527
|
+
siblings the eye reads one cluster. Put clearly
|
|
536
528
|
more space BETWEEN the groups than inside one (2:1 is enough) and the pair separates without a
|
|
537
529
|
divider, a weight change or any new element. It is the ordinary grouping rule — one uniform gap
|
|
538
530
|
makes every child a peer — applied to a row's right edge, where it is easy to miss because both
|
|
@@ -550,7 +542,7 @@ Actions stay **visible** — a hover-only action is invisible to keyboard and to
|
|
|
550
542
|
exception is a DENSE tree/register where a persistent per-row action would clutter: reveal on
|
|
551
543
|
hover **OR focus-within**, keeping the action in the DOM + tab order and gating only its opacity,
|
|
552
544
|
so it's still keyboard-reachable. Do that locally — there is deliberately no shared hover-reveal
|
|
553
|
-
primitive
|
|
545
|
+
primitive.
|
|
554
546
|
|
|
555
547
|
**A row is a button wrapped around DATA, so its text stays SELECTABLE and a drag is not a press.**
|
|
556
548
|
The surface carries no blanket `userSelect: "none"` — that made a customer's own name and number
|
|
@@ -1033,10 +1025,7 @@ When it IS a status, match weight to prominence — lightest → heaviest:
|
|
|
1033
1025
|
a surface (a drawer/detail header). Opt IN explicitly and sparingly; one per view, never
|
|
1034
1026
|
sprinkled through rows.
|
|
1035
1027
|
|
|
1036
|
-
**`tonal` IS allowed on a register's ONE primary status column
|
|
1037
|
-
paragraph used to get wrong.** It read "a register's dense rows read lighter, the row's primary
|
|
1038
|
-
status is `dot`" while `badge.tsx` named "a register's primary Status column" as a legitimate
|
|
1039
|
-
tonal use — two docs, opposite answers, so the choice fell to whoever read which. What "one per
|
|
1028
|
+
**`tonal` IS allowed on a register's ONE primary status column.** What "one per
|
|
1040
1029
|
view" governs is the number of badge KINDS on a surface, not the number of rows: badging the
|
|
1041
1030
|
stage AND the type AND the city is the clutter the rule exists to stop; one status column that
|
|
1042
1031
|
happens to render 133 times is one badge.
|
package/docs/data_entry.md
CHANGED
|
@@ -84,6 +84,10 @@ layout), click OR keyboard focus opens it for editing **with nothing moving** (z
|
|
|
84
84
|
whole point), and it commits on blur (Enter saves, Escape reverts) or via `controls="buttons"`
|
|
85
85
|
(✓ / ✕).
|
|
86
86
|
|
|
87
|
+
**`numberOfLines` declares what ENTER means — wrapping does not.** A field left at one line holds a
|
|
88
|
+
single-line value that may WRAP when it runs long (a title, a summary), and Enter finishes it.
|
|
89
|
+
Declaring 2+ lines declares PROSE: Enter is a paragraph break and blur is what saves.
|
|
90
|
+
|
|
87
91
|
**How "nothing moves" is achieved differs by editor, and the difference is a rule.** A field whose
|
|
88
92
|
resting value is the RAW string (`InlineTextInput`) is ONE `<input>` in both states — it never
|
|
89
93
|
swaps elements, and `editing` is a style state driven by focus. Two elements cannot be made to
|
|
@@ -217,16 +221,12 @@ something belongs elsewhere.
|
|
|
217
221
|
|
|
218
222
|
It is wrong wherever the length is the AUTHOR's choice, and it fails in the worst available way.
|
|
219
223
|
The box is drawn at the budget and the value simply exceeds it: no ellipsis, no clamp, no
|
|
220
|
-
scrollbar, nothing to scroll.
|
|
221
|
-
value** — two full lines gone with nothing on screen admitting it. A truncation mark would have
|
|
222
|
-
been kinder; this is silent. So the test is not "is this field long" but **"who decides the
|
|
224
|
+
scrollbar, nothing to scroll — a truncation mark would have been kinder; this is silent. So the test is not "is this field long" but **"who decides the
|
|
223
225
|
length"** — the field, or whoever is typing.
|
|
224
226
|
|
|
225
|
-
**And it gets worse as the surface narrows
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
wrapped it into more lines against the same three-line box. A fixed reserve verified on a wide
|
|
229
|
-
screen is not verified.
|
|
227
|
+
**And it gets worse as the surface narrows**: a budget is a count of LINES and the wrap point
|
|
228
|
+
moves, so the narrower column wraps the same value into more hidden lines. A fixed reserve
|
|
229
|
+
verified on a wide screen is not verified.
|
|
230
230
|
|
|
231
231
|
`autoGrow` makes the budget a minimum and fits the value. It costs nothing the fixed reserve was
|
|
232
232
|
protecting, and the reason is worth stating because it is easy to assume otherwise: an
|
|
@@ -277,6 +277,17 @@ already promises and, repeated down a column, draws the grid a second time. Both
|
|
|
277
277
|
identically, so this changes what a field looks like RESTING, never what it does. A hand-rolled
|
|
278
278
|
pressable cell should match `bare`: nothing at rest, the border on hover — never a background wash.
|
|
279
279
|
|
|
280
|
+
**A `bare` field aligns its TEXT, not its box.** Because it looks like text at rest, it sits on the
|
|
281
|
+
column like text: the frame's 8px inset is pulled back out with a negative LEFT margin, so the glyphs
|
|
282
|
+
land on the container's edge. Without that, every bare value rendered 8px right of the header naming
|
|
283
|
+
it. The bleed is one-sided on purpose — a matching right bleed aligns nothing on left-aligned text and
|
|
284
|
+
only pushes the frame past the edge its neighbours stop at, which shows the moment the border paints.
|
|
285
|
+
Two consequences for callers: a `bare` field needs ~8px of slack on its LEFT to hover into (in a
|
|
286
|
+
zero-gap column the borders of adjacent cells will touch), and it is the wrong variant for a surface
|
|
287
|
+
where it must stay strictly inside its box — that is `framed`. A DISABLED `framed` field keeps the
|
|
288
|
+
box: it drops the border and fill so it cannot promise a press, but it stays in line with the enabled
|
|
289
|
+
fields beside it.
|
|
290
|
+
|
|
280
291
|
A READ-ONLY value in the same column — a computed total, a system ID, a synced/locked field — is
|
|
281
292
|
**`InlineStatic`**: it copies the editor box metrics exactly (height, padding, 1px transparent
|
|
282
293
|
border) but stays FLAT and non-interactive, so editable (chip) vs read-only (flat) is legible at a
|
|
@@ -331,33 +342,46 @@ row that wants it. See the "General" section of `tpl_record`, which also reads t
|
|
|
331
342
|
full record surface. Not every field is a same-height swap — a tag field, a status, or an
|
|
332
343
|
attachment grid edit in place too (below).
|
|
333
344
|
|
|
334
|
-
##
|
|
345
|
+
## A value rendered TWICE is the bug — not the resident editor
|
|
335
346
|
|
|
336
|
-
The section above is the default and holds
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
surface?**
|
|
347
|
+
The section above is the default and holds everywhere. There is one thing that appears to
|
|
348
|
+
contradict it, and the contradiction is worth naming because the wrong resolution is the
|
|
349
|
+
tempting one: **a value that already appears elsewhere on the same surface.**
|
|
340
350
|
|
|
341
|
-
|
|
342
|
-
resident editor
|
|
343
|
-
|
|
344
|
-
mode
|
|
351
|
+
A feed used to be the example. Its row LABEL was the gist, the body opened underneath, and a
|
|
352
|
+
resident editor rendered the same sentence twice — stacked and identical, which reads as a
|
|
353
|
+
mistake at any weight, so quieting the frame does not help. The fix looked like demoting the
|
|
354
|
+
editor to a mode behind an **Edit** verb, and that is a workaround: it keeps the duplication and
|
|
355
|
+
buys silence by hiding one copy behind a click. What it costs shows up immediately after —
|
|
356
|
+
the verb has to live somewhere (a button nested in the row's own button, or a footer a scroll
|
|
357
|
+
away from the sentence it edits), and every repair from there is a repair to the arrangement,
|
|
358
|
+
not to the surface.
|
|
345
359
|
|
|
346
|
-
**
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
360
|
+
**Render it once, and the question dissolves.** Text inside a press target cannot be edited where
|
|
361
|
+
it sits, so a value a person WROTE does not belong in a row's label: the label is for derived,
|
|
362
|
+
read-only text ("Stage changed to Won"). Move the prose out and it is an ordinary resident field
|
|
363
|
+
with no mode, no verb, and one copy — `tpl_record`'s activity feed is the worked example, and the
|
|
364
|
+
byline above it carries the derived facts that used to fight it for the row.
|
|
365
|
+
|
|
366
|
+
So: if a resident editor would duplicate a value, **delete the duplicate**. Make the editor a mode
|
|
367
|
+
only when the second appearance is genuinely load-bearing and cannot move.
|
|
351
368
|
|
|
352
369
|
Two things follow that are easy to get wrong in the other direction:
|
|
353
370
|
|
|
354
|
-
- **
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
- **
|
|
360
|
-
|
|
371
|
+
- **Only the prose a PERSON wrote is editable.** Derived values (a header's participants),
|
|
372
|
+
verbatim ones (a transcript), artifacts (media, files) and a model's output (a summary — you
|
|
373
|
+
re-run it, you do not hand-edit it) are all read-only. If a surface does end up with an edit
|
|
374
|
+
verb, name it for the words rather than the row: "Edit", not "Edit entry", which claims the
|
|
375
|
+
whole entry is yours to rewrite.
|
|
376
|
+
- **An EMPTY value still shows its field, and shows it FRAMED.** A surface fed by automations
|
|
377
|
+
receives entries with no words in them routinely — a recording lands and nobody has written it
|
|
378
|
+
up — so withholding the field lets a feed RECEIVE an entry it gives the reader no way to
|
|
379
|
+
finish. Empty is also the state where `bare` fails: grey placeholder text on an invisible box
|
|
380
|
+
does not read as somewhere you can type. Follow the value — `variant={value ? "bare" : "framed"}`
|
|
381
|
+
— so a written-up entry reads as prose and an empty one is recognisable as an input from across
|
|
382
|
+
the page. A button that reveals a field is the wrong trade: `InlineTextInput` has no
|
|
383
|
+
`autoFocus` (permanently mounted, every field on a record would fight for focus on load), so
|
|
384
|
+
the revealed field cannot take the caret and the reader pays a second click for less clarity.
|
|
361
385
|
|
|
362
386
|
## Fieldset form — fields lay out on a RESPONSIVE two-column grid
|
|
363
387
|
|
|
@@ -458,8 +482,7 @@ closes the list as its add-affordance.
|
|
|
458
482
|
|
|
459
483
|
**`connected` decides which shape you are building.** ON for one record walking positions — the
|
|
460
484
|
line asserts sequence, which is what a desk handoff IS. OFF for N items ticked in any order, where
|
|
461
|
-
a line claims an order the work does not have. It is the same compound either way
|
|
462
|
-
families until the anatomy turned out to be identical.
|
|
485
|
+
a line claims an order the work does not have. It is the same compound either way.
|
|
463
486
|
|
|
464
487
|
**A GROUP is a name and nothing else.** No control: a phase owns no completion of its own, so a
|
|
465
488
|
ring there reports without responding — and one identical to the pressable rings beneath it
|
|
@@ -628,13 +651,6 @@ A record field whose value is documents. `<DetailRow label="Scan"><InlineFiles
|
|
|
628
651
|
— a `FileRows` list (press a row to preview it full-screen, ⋯ to remove) with one CTA under it,
|
|
629
652
|
sitting on the inline-control grid via `InlineSlot`.
|
|
630
653
|
|
|
631
|
-
It exists because every other field type shipped BOTH halves — `Select`/`InlineSelect`,
|
|
632
|
-
`TextInputField`/`InlineTextInput` — and files shipped only the section-scale one. So a record
|
|
633
|
-
field holding documents got hand-rolled per app as `FileRows` + `pickFiles` + a `Button`, and
|
|
634
|
-
each copy re-derived the same decisions. The one they missed is `InlineSlot`: raw children in a
|
|
635
|
-
value column start at the cell edge and 20px tall, beside editors that are 40px with a 9px text
|
|
636
|
-
inset, so the column acquires two left edges and two baselines and stops reading as a column.
|
|
637
|
-
|
|
638
654
|
**Cardinality is a property of the FIELD, and it changes the verb.**
|
|
639
655
|
|
|
640
656
|
| `multiple` | picker | CTA once something is attached | the host's `onAdd` must |
|