@lotics/ui 41.1.0 → 41.4.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 +4 -4
- package/MIGRATION.md +100 -0
- package/docs/ai_patterns.md +36 -0
- package/docs/catalog.md +52 -11
- package/docs/composition.md +89 -11
- package/docs/data_entry.md +63 -0
- package/docs/templates.md +120 -9
- package/examples/tpl_record.tsx +763 -9
- package/package.json +3 -1
- package/src/inline_text_input.tsx +33 -2
- package/src/markdown.css +43 -0
- package/src/markdown.tsx +7 -1
- package/src/markdown.web.tsx +59 -3
- package/src/markdown_types.ts +21 -0
- package/src/member_chip.tsx +12 -2
- package/src/text_disclosure.tsx +70 -0
- package/src/text_link.tsx +13 -3
- package/src/timeline.tsx +194 -34
package/AGENTS.md
CHANGED
|
@@ -15,10 +15,10 @@ CURRENT major only — upgrading an app across majors is `MIGRATION.md`.
|
|
|
15
15
|
| Doc | Read it for |
|
|
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
|
-
| [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, 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; the UI half of the SDK's [ai doc](../app-sdk/docs/ai.md)., the whole run in a dialog (`AgentRunScope`/`AgentRunPane`/`AgentRunActions` — a parked question REPLACES the feed, actions in the footer, **Stop** while streaming), **stopping** (`cancel` stops the run, `abort` only stops listening — so closing a dialog must `cancel` or it keeps billing); **review surfaces compose from atoms** — `DiffValue` (a changed value, droppable in any cell/row/total), `DiffMark` (what happened to the row — ONE circular disc, every surface), `useChangeSet` (accept/reject/undo bookkeeping, no layout) — see [MIGRATION.md](./MIGRATION.md) for the `ChangeReview` family they replace |
|
|
20
|
-
| [docs/composition.md](./docs/composition.md) | The design-language contract — canvas + content column, heading altitude (incl. eyebrow vs group lead — a label is one or the other), banded cards, register vs inset rows (incl. the register laws a row centres its cells by: every cell a FIXED height, a pressable cell on the shared hover token, a column sized by what it carries), 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
|
-
| [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). |
|
|
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)., the whole run in a dialog (`AgentRunScope`/`AgentRunPane`/`AgentRunActions` — a parked question REPLACES the feed, actions in the footer, **Stop** while streaming), **stopping** (`cancel` stops the run, `abort` only stops listening — so closing a dialog must `cancel` or it keeps billing); **review surfaces compose from atoms** — `DiffValue` (a changed value, droppable in any cell/row/total), `DiffMark` (what happened to the row — ONE circular disc, every surface), `useChangeSet` (accept/reject/undo bookkeeping, no layout) — see [MIGRATION.md](./MIGRATION.md) for the `ChangeReview` family they replace |
|
|
20
|
+
| [docs/composition.md](./docs/composition.md) | The design-language contract — canvas + content column, heading altitude (incl. eyebrow vs group lead — a label is one or the other), banded cards, register vs inset rows (incl. the register laws a row centres its cells by: every cell a FIXED height, a pressable cell on the shared hover token, a column sized by what it carries), the button ladder and **what underlined text may mean** (it GOES somewhere or REVEALS something — never mutates; blue leaves the surface, muted stays on it, and the one in-prose disclosure exception is scoped there), master-detail `Drawer` on a LIST screen vs a child collection's row EXPANDING inside a record, view controls, RECORD EXTENT (one page, sections scrolled to and never routed to), color discipline, typography, whitespace, and how to TEST an overlay component (a `Popover`-backed surface never mounts under jsdom). |
|
|
21
|
+
| [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. |
|
|
22
22
|
|
|
23
23
|
## Iron rules
|
|
24
24
|
|
package/MIGRATION.md
CHANGED
|
@@ -4,6 +4,106 @@ 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
|
+
## 41.4.0 — embedded markdown is demoted in the OUTLINE, not only on the type ladder
|
|
8
|
+
|
|
9
|
+
`<Markdown variant="embedded">` sized its headings down and left them as the tags the author
|
|
10
|
+
wrote, so an `h2` inside a record value still landed in heading navigation as a PEER of the
|
|
11
|
+
page's own sections. Measured on a record surface, the outline read:
|
|
12
|
+
|
|
13
|
+
> Activity · "Tóm tắt cuộc họp (AI) — …" · "Bài học …" · Details · Next action
|
|
14
|
+
|
|
15
|
+
— a model's call-summary title between two real sections, contributed from inside ONE row of a
|
|
16
|
+
feed that can hold twenty more. Nothing visual finds this, and fixing the size is what makes it
|
|
17
|
+
invisible: the defect stops looking wrong at the moment it stops being measurable.
|
|
18
|
+
|
|
19
|
+
Embedded headings now render `h4`/`h5`/`h6` (h1→h4, h2→h5, h3 and deeper→h6). They stay
|
|
20
|
+
headings, so a reader can still navigate inside a long summary; they simply can no longer reach
|
|
21
|
+
a section's level. **Nothing to migrate** — document-scale markdown is untouched, and the
|
|
22
|
+
embedded type scale is unchanged.
|
|
23
|
+
|
|
24
|
+
## 41.3.0 — `Timeline` rows, embedded markdown, and a disclosure that is not a link
|
|
25
|
+
|
|
26
|
+
Nothing here removes an API or breaks a type. Two of the changes alter what your rows render on
|
|
27
|
+
sight; the rest is additive.
|
|
28
|
+
|
|
29
|
+
### `Timeline` bounds its own text, and `right` leaves the press target
|
|
30
|
+
|
|
31
|
+
**The label clamps to two lines** (the description too). It was unbounded, which is invisible
|
|
32
|
+
until a caller puts user-authored prose in it: measured on a real interaction log, one
|
|
33
|
+
700-character summary drew a **180px label** — nine lines for one entry — and the disc, which
|
|
34
|
+
centred on the label row, ended up 80px below the first line it was meant to sit beside.
|
|
35
|
+
|
|
36
|
+
If your label can run long, put the full text in `details` as well. The row is then a
|
|
37
|
+
truncation and the detail its source, which is the drill-down every expandable register row
|
|
38
|
+
already makes:
|
|
39
|
+
|
|
40
|
+
```diff
|
|
41
|
+
label: entry.summary,
|
|
42
|
+
+ details: <Text size="sm">{entry.summary}</Text>,
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**`right` renders beside the press target, not inside it.** On a row with `details` the whole
|
|
46
|
+
row was a `<button>`, so an interactive `right` — a row-action menu, an `IconButton` — nested a
|
|
47
|
+
button inside a button: invalid HTML, and one click reaching two handlers. It is now a sibling,
|
|
48
|
+
and the chevron closes the pressable region, so the order reads `label … ⌄ right`. A
|
|
49
|
+
non-interactive `right` (a duration, a `Badge`) moves 14px right and is otherwise unaffected.
|
|
50
|
+
The hover wash now stops where the press target does, which is the point: a wash that ran under
|
|
51
|
+
a row action made that action invisible exactly when the pointer reached it.
|
|
52
|
+
|
|
53
|
+
**Everything in the band aligns to the label's FIRST LINE, not to the label block** — the disc,
|
|
54
|
+
the chevron and whatever you pass as `right`. Centring and first-line alignment are the same
|
|
55
|
+
answer until a label wraps and then they are 10px apart, so making two-line labels ordinary made
|
|
56
|
+
the old behaviour wrong. **A single-line row is pixel-identical** (measured: 40px band, zero
|
|
57
|
+
drift on all three ornaments); only wrapped rows change, and they change to correct.
|
|
58
|
+
|
|
59
|
+
**`TimelineItem` takes `placeholder`.** It marks a label the SURFACE supplied rather than one the
|
|
60
|
+
record holds, and draws it in the ink an unfilled field uses. A feed fed by automations and
|
|
61
|
+
extractions routinely receives an entry with no prose in it, and `label: string` forces the
|
|
62
|
+
caller to invent some — which then renders in body ink, so "nobody has written this up" and
|
|
63
|
+
"this is what happened" look identical.
|
|
64
|
+
|
|
65
|
+
```diff
|
|
66
|
+
- label: entry.gist || `${entry.direction} ${entry.kind}`,
|
|
67
|
+
+ label: entry.gist || "Not written up yet",
|
|
68
|
+
+ placeholder: !entry.gist,
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### `Markdown` takes `variant`
|
|
72
|
+
|
|
73
|
+
`"document"` (the default, and what every current caller keeps) is markdown that IS the surface
|
|
74
|
+
— its `h1` is the page's `h1`. `"embedded"` is markdown that is a VALUE on a page someone else
|
|
75
|
+
designed, and collapses every heading to the body's own size.
|
|
76
|
+
|
|
77
|
+
```diff
|
|
78
|
+
- <Markdown>{record.aiSummary}</Markdown>
|
|
79
|
+
+ <Markdown variant="embedded">{record.aiSummary}</Markdown>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Reach for it whenever the text was not written by whoever built the screen — now the common
|
|
83
|
+
case, because the writer is routinely a model. Measured on a real feed, a machine summary at
|
|
84
|
+
document scale drew 18px/600 headings, one rung under the section heading containing it.
|
|
85
|
+
|
|
86
|
+
`MarkdownProps` is reached from `@lotics/ui/markdown_types`, the same way `MediaPlayerProps` is;
|
|
87
|
+
the component entry points do not re-export it.
|
|
88
|
+
|
|
89
|
+
### `TextDisclosure`, and what underlined text is allowed to mean
|
|
90
|
+
|
|
91
|
+
New component: muted underlined text that REVEALS adjacent content in place — a transcript under
|
|
92
|
+
its player, a long value under its summary. It exists because the rule used to be "underlined
|
|
93
|
+
text is the NAVIGATION affordance and nothing else", and following that produced worse screens:
|
|
94
|
+
the quiet control it left you (`Button color="muted"`) measures transparent, borderless and
|
|
95
|
+
undecorated at rest, so its affordance arrives only on hover.
|
|
96
|
+
|
|
97
|
+
The rule is now scoped rather than absolute, and the INK carries the difference:
|
|
98
|
+
|
|
99
|
+
| | |
|
|
100
|
+
|---|---|
|
|
101
|
+
| blue + `role="link"` (`Link`) | leaves this surface |
|
|
102
|
+
| muted underline (`TextDisclosure`) | reveals more of it, in place |
|
|
103
|
+
|
|
104
|
+
Anything that MUTATES still carries a control surface — `Button` in chrome, `InlineButton` on a
|
|
105
|
+
field. Nothing to migrate: existing `Link`/`TextLink` usage is unaffected.
|
|
106
|
+
|
|
7
107
|
## 41.1.0 — `FileRows` no longer captions every row with the file type
|
|
8
108
|
|
|
9
109
|
`meta` defaulted to the file-type label, so a row read `contract.pdf` with `PDF` under it. The
|
package/docs/ai_patterns.md
CHANGED
|
@@ -503,6 +503,42 @@ locator after the label ("page 2", a record code). Kinds carry recognizable glyp
|
|
|
503
503
|
make each chip pressable (hover wash + an open glyph); the host does the navigation. An empty
|
|
504
504
|
list renders nothing.
|
|
505
505
|
|
|
506
|
+
## After the run — a record that FILLS UP from more than one writer
|
|
507
|
+
|
|
508
|
+
`Sources` and `Confidence` above belong to a live answer, where the run is on screen and the
|
|
509
|
+
reader is deciding about it. The harder surface is the one they open a week later: a stored
|
|
510
|
+
record whose fields were written by a person, by a chat agent, by an extraction off an upload,
|
|
511
|
+
and by an automation that fired when a file landed — and which carries no memory of which was
|
|
512
|
+
which. Increasingly that is the normal shape, because the app's own form is the LEAST used way
|
|
513
|
+
in. Three rules, each of which a screen built for a single writer gets wrong for free.
|
|
514
|
+
|
|
515
|
+
**An unwritten value must not render like a written one.** A row assembled by an automation
|
|
516
|
+
arrives with its enums set and its prose empty, and the natural repair — composing a label out
|
|
517
|
+
of the enums that ARE set — produces a real-looking sentence in body ink. "Nobody has written
|
|
518
|
+
this up" and "this is what happened" then look identical, which is the one distinction the
|
|
519
|
+
reader most needs. Say the value is missing, in the ink an unfilled field already uses
|
|
520
|
+
(`Timeline`'s `placeholder`; the same idea wherever a component takes a `label: string` and
|
|
521
|
+
therefore cannot express absence — that type is the trap, so give it a way to say so).
|
|
522
|
+
|
|
523
|
+
**A machine's prose and a person's must not share a treatment.** They are different claims and
|
|
524
|
+
they earn different trust, so rendering them alike teaches the reader to skim both at one
|
|
525
|
+
speed. Carry the difference with SHAPE, not weight: name the author and the evidence on the
|
|
526
|
+
line above ("Written by AI from the recording" — the source alone is not authorship), and drop
|
|
527
|
+
the machine's text into a recessed `Inset`, which says it was not written on this page. Leave
|
|
528
|
+
the person's conclusion on the page's own ground; that contrast IS the signal.
|
|
529
|
+
|
|
530
|
+
**Model prose arrives with its own type ladder — take it away at the door.** A model asked for
|
|
531
|
+
a summary emits `##` headings and heavy `**bold**` knowing nothing of what surrounds it, and
|
|
532
|
+
document-scale markdown hands those an `h1` equal to your section heading: measured on a real
|
|
533
|
+
feed, machine content rendered 18px/600 headings over 44 medium-weight runs, so the
|
|
534
|
+
least-trustworthy text on the screen was also the loudest. Render every embedded value with
|
|
535
|
+
`<Markdown variant="embedded">`.
|
|
536
|
+
|
|
537
|
+
And one that is not about treatment at all: **check that the read path projects every field the
|
|
538
|
+
write paths set.** A field written by an intake and never projected by the query behind the
|
|
539
|
+
screen is invisible in a way no visual pass can catch — the surface renders perfectly, and the
|
|
540
|
+
evidence is discarded at render. Diff the two lists whenever a surface has more than one writer.
|
|
541
|
+
|
|
506
542
|
## Confidence — `Confidence`
|
|
507
543
|
|
|
508
544
|
Confidence IS a callout (`@lotics/ui/confidence`) — the ONLY form it takes: the three-tick
|
package/docs/catalog.md
CHANGED
|
@@ -38,16 +38,18 @@ emphasis/risk), `IconButton` (icon-ONLY — the circular affordance; an icon-onl
|
|
|
38
38
|
type error, so reach here. `size` `lg` 40px / `md` 28px / `sm` 24px — md/sm keep a 40px touch
|
|
39
39
|
target via hitSlop — the SAME `color` palette as Button plus `none`/`white`, plus `loading` and `elevated` (white
|
|
40
40
|
fill + border + shadow, for a button sitting ON imagery — a tile's remove ✕, an overlay's
|
|
41
|
-
retry); needs `accessibilityLabel`/`tooltip`), `TextLink` (underlined text that
|
|
42
|
-
an
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
retry); needs `accessibilityLabel`/`tooltip`), `TextLink` (underlined text that NAVIGATES with
|
|
42
|
+
an `href` — a real web anchor — or, without one, a plain underlined marker you drop in your own
|
|
43
|
+
pressable like a table cell; colour via `color`. It does not act: it has no `onPress`),
|
|
44
|
+
`TextDisclosure` (the reveal counterpart — muted underlined text that unfolds adjacent content
|
|
45
|
+
in place), `Chip` (dismissible facet chip), `CopyButton`
|
|
45
46
|
(put a value on the clipboard — rides the value it copies, confirms only a real write).
|
|
46
47
|
A button is
|
|
47
48
|
never a raw `Pressable`. For a link OUT (a URL / record / document) use `Link` — fixed
|
|
48
49
|
underline+blue + `role="link"`, the destination signal (`onPress` only — the consumer wires
|
|
49
50
|
the opener, e.g. the app SDK's `openExternal`); `TextLink` is the neutral,
|
|
50
|
-
colour-configurable underlined
|
|
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.
|
|
51
53
|
|
|
52
54
|
### Pick from a list
|
|
53
55
|
|
|
@@ -128,7 +130,9 @@ Above 1 the resting box RESERVES that many lines — the full editor height, not
|
|
|
128
130
|
particular value needs — so clicking in moves neither the field nor anything below it; Enter
|
|
129
131
|
then inserts a newline and the value commits on blur. Both halves matter: a field that only
|
|
130
132
|
goes multiline once focused hides the text from the reader who never clicks, and one that
|
|
131
|
-
reserves less than it opens to shoves the rest of the form down the page on every click
|
|
133
|
+
reserves less than it opens to shoves the rest of the form down the page on every click.
|
|
134
|
+
**`autoGrow`** makes that budget a MINIMUM and fits the value instead — see
|
|
135
|
+
[data_entry.md §Open prose grows](./data_entry.md)),
|
|
132
136
|
`InlineNumberInput`, `InlineSelect` (single or
|
|
133
137
|
`multi`), `InlineMemberSelect`, `InlineDatePicker`, `InlineTimePicker`; a
|
|
134
138
|
READ-ONLY field in that same column uses `InlineStatic` (matches the editor box exactly, no
|
|
@@ -403,7 +407,16 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
403
407
|
numbers), `weight`, `color`, alignment, tabular numerals.
|
|
404
408
|
- **`markdown`** — `Markdown`: the single canonical markdown renderer for chat, apps, and
|
|
405
409
|
`AgentRun`; rich GFM markdown on web with copyable tables, plain-text on
|
|
406
|
-
native; takes a markdown `children` string.
|
|
410
|
+
native; takes a markdown `children` string. **`variant`** decides whose type
|
|
411
|
+
ladder the headings belong to: `"document"` (default) where the markdown IS the
|
|
412
|
+
surface, `"embedded"` where it is a VALUE on a page someone else designed — a
|
|
413
|
+
field on a record, a summary in a feed row. Reach for `"embedded"` whenever the
|
|
414
|
+
text was not written by whoever built the screen, which is now the common case
|
|
415
|
+
because the writer is routinely a model; at document scale an `h1` it emits is
|
|
416
|
+
the same rung as your `SectionHeadingTitle`. Embedded demotes the heading LEVEL as well as
|
|
417
|
+
the scale (h1→h4, h2→h5, h3+→h6): sizing alone leaves a model's `##` announcing as a PEER of
|
|
418
|
+
your own sections in heading navigation, which no visual check can see.
|
|
419
|
+
- **`markdown_types`** — `MarkdownProps`; types only.
|
|
407
420
|
- **`markdown.css`** — import once for the web markdown styling.
|
|
408
421
|
- **`format_date`** — `formatDate` / `parseDate` / `toISODate` + `DateFormatStyle`.
|
|
409
422
|
- **`format_money`** — `formatMoney` / `formatCompactNumber`.
|
|
@@ -488,7 +501,10 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
488
501
|
image-less entities — groups, organizations. A person → `Avatar`/`MemberChip`.
|
|
489
502
|
- **`wave_avatar`** — `WaveAvatar`: decorative animated waveform avatar (voice/audit
|
|
490
503
|
history); animates on web, renders a static fallback on native.
|
|
491
|
-
- **`member_chip`** — `MemberChip`: avatar + name; the universal person render.
|
|
504
|
+
- **`member_chip`** — `MemberChip`: avatar + name; the universal person render. `size` scales
|
|
505
|
+
BOTH — `sm` gives a 24px avatar and a 12px name, every larger rung keeps the name at body
|
|
506
|
+
size. Reach for `sm` when the chip sits inside a sentence or a dense row, where a body-size
|
|
507
|
+
name would collide with the 12px text around it.
|
|
492
508
|
|
|
493
509
|
### Layout & surfaces
|
|
494
510
|
|
|
@@ -632,8 +648,20 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
632
648
|
with no `href` it stays NEUTRAL: plain underlined text to wrap in your own pressable,
|
|
633
649
|
or a MARKER that a value leads somewhere (a record reference whose press opens a peek,
|
|
634
650
|
not a trip). `color` overrides either way; inherits every `Text` prop. It does NOT
|
|
635
|
-
act:
|
|
636
|
-
|
|
651
|
+
act: anything that MUTATES carries a control surface (`Button`/`InlineButton`).
|
|
652
|
+
- **`text_disclosure`** — `TextDisclosure`: the counterpart to `TextLink` — muted
|
|
653
|
+
underlined text that REVEALS adjacent content in place (a transcript under its
|
|
654
|
+
player, a long value under its summary). Underlined text either goes somewhere or
|
|
655
|
+
reveals something, and the INK says which: blue + `role="link"` leaves the surface,
|
|
656
|
+
muted stays on it. Takes `expanded`/`onToggle` and a `label` NOUN — it supplies the
|
|
657
|
+
verb, so two call sites cannot end up with "Show" and "View". Reach for it instead
|
|
658
|
+
of the two things that look right and are not: `Button color="muted"` measures
|
|
659
|
+
transparent, borderless and undecorated at rest, so its affordance arrives only on
|
|
660
|
+
hover (invisible to keyboard and touch, and against a text column it reads as a
|
|
661
|
+
label); `Accordion` is a list-ROW disclosure, heavy furniture for a verb nested
|
|
662
|
+
inside a row that already has a chevron. Announces `role="button"` + `aria-expanded`
|
|
663
|
+
— reaching for `Link` instead tells a screen-reader user they are leaving a page
|
|
664
|
+
that is about to unfold under them.
|
|
637
665
|
- **`chip`** — `Chip`: the generic pill — pressable when `onPress` (announces as a button;
|
|
638
666
|
pass `accessibilityLabel` when children aren't self-describing text) + an
|
|
639
667
|
absolutely-positioned dismiss ✕ sibling when `onDismiss` (its name = `dismissTooltip` ??
|
|
@@ -1395,7 +1423,20 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1395
1423
|
`pxPerDay`) + `GanttTask`/`GanttLabels` types. **Limitation:** labels are not
|
|
1396
1424
|
provider-wired — pass `labels` (`DEFAULT_GANTT_LABELS` is English).
|
|
1397
1425
|
- **`timeline`** — `Timeline`: a heterogeneous event LOG — per-row icon + expandable
|
|
1398
|
-
details; models the past, NOT progress.
|
|
1426
|
+
details; models the past, NOT progress. Three things it decides for you, because a row
|
|
1427
|
+
cannot be trusted to a caller's data: the **label clamps to two lines** (and the
|
|
1428
|
+
description likewise) — a row is a scan target, and an unclamped one lets a single
|
|
1429
|
+
user-authored note set the section's height, so put the full text in `details` too;
|
|
1430
|
+
**`placeholder`** marks a label the SURFACE supplied rather than one the record
|
|
1431
|
+
holds, drawing it in the unfilled-field ink — reach for it on any feed that fills
|
|
1432
|
+
from more than one direction, where an entry routinely arrives with no words in it;
|
|
1433
|
+
**the disc, the chevron and `right` all sit on the label's FIRST LINE**, so a wrapped
|
|
1434
|
+
row lines up with an unwrapped one (the worked reference for the rule in
|
|
1435
|
+
composition.md §registers); and **`right` renders beside the press target, not inside
|
|
1436
|
+
it**, so a control there is valid HTML and keeps its own click — though on a row that
|
|
1437
|
+
EXPANDS, verbs belong in the BODY and `right` is better kept for a fact (a duration, a
|
|
1438
|
+
badge). A control sharing the label row with the press target is what produced the nesting,
|
|
1439
|
+
the first-line box and the chevron-pairing problems in the first place.
|
|
1399
1440
|
- **`checklist`** — `Checklist` + `ChecklistGroup` + `ChecklistItem` + `ChecklistNote` /
|
|
1400
1441
|
`ChecklistActions` / `ChecklistField`: THE run of tickable rows, at any
|
|
1401
1442
|
altitude. It replaced a `task` family and a `pipeline` family that had converged on one
|
package/docs/composition.md
CHANGED
|
@@ -417,6 +417,31 @@ truncation, so you are buying a baseline with clipped text. Worth it for an iden
|
|
|
417
417
|
contact; think harder for a cell whose job is prose, where the honest answer may be to move that
|
|
418
418
|
content off the register entirely.
|
|
419
419
|
|
|
420
|
+
**This is not a table rule — it is a rule about furniture centred on text.** Anywhere a fixed
|
|
421
|
+
ornament sits beside a text block and centres on it — a `Timeline`'s disc, a marker, an avatar,
|
|
422
|
+
a rail glyph — the ornament's position is a function of the text's height, so an unbounded text
|
|
423
|
+
block moves it. A feed is where this actually bites, because a feed is the one row primitive
|
|
424
|
+
whose text is written by the USER: measured on a real interaction log, one 700-character summary
|
|
425
|
+
drew a 180px label and put the disc 80px below the line it names. Bound the text; the row keeps
|
|
426
|
+
its beat and the ornament keeps its line. Where the full value still has to be readable, the row
|
|
427
|
+
EXPANDS to it — a truncation and its source, not a second copy.
|
|
428
|
+
|
|
429
|
+
**Bounding the text is half the fix; the other half is aligning to the LINE, not the block.** A
|
|
430
|
+
clamp turns an 80px drift into a 10px one — it does not remove it, because a two-line label
|
|
431
|
+
still centres its ornaments half a line low while the one-line rows beside it do not. So pin
|
|
432
|
+
every ornament in the band to the first line's centre, from ONE constant the row derives (see
|
|
433
|
+
`FIRST_LINE_CENTRE` in `timeline.tsx`, the reference): the mark, the affordance, and whatever a
|
|
434
|
+
caller passes as trailing content all read it, and a wrapped row lines up with an unwrapped one.
|
|
435
|
+
Two consequences worth stating, because both look like unrelated tidying: a height FLOOR above
|
|
436
|
+
the content is what created the slack being distributed, so deriving the touch target from
|
|
437
|
+
`padding + line` instead removes the drift and the second source of truth in one move; and an
|
|
438
|
+
ornament OUTSIDE the row's padding needs its own box, since it does not inherit that offset.
|
|
439
|
+
|
|
440
|
+
The way it gets in is a SLOT. A component that takes a caller's string or node cannot assume
|
|
441
|
+
anything about it, and every fixture the component was built against is tidy, so the defect is
|
|
442
|
+
invisible until it ships. Bound the slot in the component — not in a prop the next caller can
|
|
443
|
+
re-open.
|
|
444
|
+
|
|
420
445
|
Two corollaries, both counter-intuitive enough to state:
|
|
421
446
|
|
|
422
447
|
- **Shrinking a NEIGHBOURING cell does not fix it.** The spread is a function of the varying
|
|
@@ -482,6 +507,24 @@ over it). `PressableHighlight` is the wrong base for such a row — it IS a butt
|
|
|
482
507
|
children, so any nested control lands inside it; it stays for surfaces whose content is
|
|
483
508
|
non-interactive by construction. `TableRow` already composes `PressDoor`.
|
|
484
509
|
|
|
510
|
+
**A trailing slot is not a PEER of the row's own affordance — space them apart.** A disclosure
|
|
511
|
+
chevron belongs to the row's button; the slot beside it does not, and if the two are spaced like
|
|
512
|
+
siblings the eye reads one cluster. Measured on a feed row: 10px from the label to the chevron
|
|
513
|
+
and 12px from the chevron to the action menu, so the only thing saying they were different
|
|
514
|
+
targets was a 4px break in the hover wash — invisible to a reader who never hovers. Put clearly
|
|
515
|
+
more space BETWEEN the groups than inside one (2:1 is enough) and the pair separates without a
|
|
516
|
+
divider, a weight change or any new element. It is the ordinary grouping rule — one uniform gap
|
|
517
|
+
makes every child a peer — applied to a row's right edge, where it is easy to miss because both
|
|
518
|
+
items are small grey glyphs sitting on the same line.
|
|
519
|
+
|
|
520
|
+
**"Non-interactive by construction" excludes any SLOT.** A trailing `right` / `actions` / `meta`
|
|
521
|
+
prop takes whatever the caller passes, so a row that wraps one in its own press surface is a
|
|
522
|
+
nested button waiting for the first caller to pass an `ActionMenu` — which is the natural thing
|
|
523
|
+
to pass. The browser says so out loud (`<button> cannot contain a nested <button>`) and nothing
|
|
524
|
+
else does: it renders, it measures, it typechecks, and the click quietly runs two handlers.
|
|
525
|
+
Render the slot as a SIBLING of the press target (`Timeline` is the reference), which also stops
|
|
526
|
+
the row's hover wash sliding under the action at the moment the pointer reaches it.
|
|
527
|
+
|
|
485
528
|
Actions stay **visible** — a hover-only action is invisible to keyboard and touch users. The ONLY
|
|
486
529
|
exception is a DENSE tree/register where a persistent per-row action would clutter: reveal on
|
|
487
530
|
hover **OR focus-within**, keeping the action in the DOM + tab order and gating only its opacity,
|
|
@@ -661,13 +704,34 @@ by what KIND of thing they are, so an avatar takes the same number rather than a
|
|
|
661
704
|
already puts it. Chrome has TWO rungs: `Button` (a 40px control with a surface) →
|
|
662
705
|
`InlineButton` (28px, filled, on a field's own surface).
|
|
663
706
|
|
|
664
|
-
**AN ACT CARRIES A CONTROL;
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
707
|
+
**AN ACT CARRIES A CONTROL; UNDERLINED TEXT REVEALS.** `Link`/`TextLink` are the underlined
|
|
708
|
+
pair; everything that MUTATES is a `Button` or an `InlineButton`. A surface-less act was tried
|
|
709
|
+
as a general third rung and removed: it made underline mean anything at all, so the affordance
|
|
710
|
+
stopped answering the one question a reader has — does this take me away, or does it do
|
|
711
|
+
something here. It also competed with the `Button` colour ladder, since `muted` already IS the
|
|
712
|
+
quiet rung.
|
|
713
|
+
|
|
714
|
+
**The ONE exception, and it is narrow: an IN-PROSE DISCLOSURE.** A show/hide for adjacent
|
|
715
|
+
content — a transcript under its player, a long value under its summary — has nowhere else to
|
|
716
|
+
go. It cannot move to chrome, because it belongs to the thing above it; and the two controls
|
|
717
|
+
that look right in chrome are both wrong against a text column. `Button color="muted"` measures
|
|
718
|
+
transparent, borderless and undecorated AT REST, with its wash arriving only on hover: that is
|
|
719
|
+
a hover-only affordance, banned above, and it reads as a stray label between two content
|
|
720
|
+
blocks. `Accordion` fixes the affordance and overcorrects — it is a LIST-ROW primitive, a
|
|
721
|
+
full-width header with its own chevron, and nesting one inside a row that already has a chevron
|
|
722
|
+
is list furniture borrowed for a one-off verb.
|
|
723
|
+
|
|
724
|
+
So underline is permitted here, and the INK keeps it unambiguous:
|
|
725
|
+
|
|
726
|
+
| | |
|
|
727
|
+
|---|---|
|
|
728
|
+
| blue + `role="link"` (`Link`) | leaves this surface |
|
|
729
|
+
| muted underline in a caller's `Pressable` (`TextLink`, no `href`) | reveals more of it, in place |
|
|
730
|
+
|
|
731
|
+
Both say "show me more"; only one takes you away, which is the question the affordance existed
|
|
732
|
+
to answer. It stays scoped to REVEAL: a press that saves, sends, deletes or opens an overlay
|
|
733
|
+
carries a control surface, always. Pick by what the press DOES,
|
|
734
|
+
and let the surface say which kind it is. Never a `muted` text action that MUTATES: it mutes
|
|
671
735
|
the one thing carrying the act. No
|
|
672
736
|
"success"/green button. Decision UIs put positive/negative color on the STATUS (dot) and verdict
|
|
673
737
|
(colored `Text`), not the buttons.
|
|
@@ -903,10 +967,11 @@ selection washes).
|
|
|
903
967
|
(rose vs orange) stays on `Badge`s.
|
|
904
968
|
|
|
905
969
|
Links use `Link`/`TextLink` (blue-600), never `solid("blue")` — and blue-600 means NAVIGATION
|
|
906
|
-
specifically, not "interactive" generally. Underlined text is
|
|
907
|
-
|
|
908
|
-
when it has an `href`; as a passive marker on
|
|
909
|
-
|
|
970
|
+
specifically, not "interactive" generally. Underlined text is for going somewhere or REVEALING
|
|
971
|
+
something, never for mutating (see the button ladder above, which scopes the one in-prose
|
|
972
|
+
disclosure exception). `TextLink` takes the blue only when it has an `href`; as a passive marker on
|
|
973
|
+
a value, or as a muted in-place disclosure, it stays neutral — because a press that opens a PEEK,
|
|
974
|
+
or unfolds a transcript, is not a trip.
|
|
910
975
|
|
|
911
976
|
## `Badge` is for STATUS only — everything else is text
|
|
912
977
|
|
|
@@ -932,6 +997,19 @@ tonal. A `Badge` is never a metric value.
|
|
|
932
997
|
|
|
933
998
|
## Typography
|
|
934
999
|
|
|
1000
|
+
**A `size` rung scales the WHOLE component, not just its figure.** A composite with a graphic and
|
|
1001
|
+
a label — a chip, a badge with a caption, a tile — has one `size` prop and the reader expects it
|
|
1002
|
+
to mean one thing. Scaling only the graphic makes the small rung half-small, and the defect shows
|
|
1003
|
+
up nowhere near the component: it appears when the chip is dropped INSIDE a sentence, where a
|
|
1004
|
+
12px label runs straight into its 14px name and the two read as different type systems meeting
|
|
1005
|
+
mid-phrase. Their line boxes differ too (16 against 20), so no `alignItems: "center"` can put
|
|
1006
|
+
them on a shared baseline — the symptom is usually reported as "the alignment is off".
|
|
1007
|
+
|
|
1008
|
+
The check is cheap and worth running whenever a component takes a size: render the small rung and
|
|
1009
|
+
measure the TEXT inside it, not the box. If the text did not move, the prop is only half wired.
|
|
1010
|
+
|
|
1011
|
+
|
|
1012
|
+
|
|
935
1013
|
Only the `Text` primitive (`size` / `weight` / `color` / `transform` / `tabular` / `level` for
|
|
936
1014
|
semantic heading rank). Uppercase tracking is built into `transform="uppercase"` — never hand-set
|
|
937
1015
|
`letterSpacing`.
|
package/docs/data_entry.md
CHANGED
|
@@ -203,6 +203,41 @@ cell (noise on a dense board, and a pressable nested in the trigger is invalid D
|
|
|
203
203
|
(the app workflow must ACCEPT null on that input — a `select`/`date`/`member` field clears on
|
|
204
204
|
null). `InlineSelect multi` needs no `onClear` — an empty set is already a valid `onSave`.
|
|
205
205
|
|
|
206
|
+
### Open prose GROWS; a value with a known shape keeps its reserve
|
|
207
|
+
|
|
208
|
+
`numberOfLines` reserves a fixed budget so the box is the same height resting and editing. That
|
|
209
|
+
is right for a value whose shape the FIELD decides — a two-line address, a payment term, a
|
|
210
|
+
clause — where the reserve keeps the page still and an overlong value is itself a signal that
|
|
211
|
+
something belongs elsewhere.
|
|
212
|
+
|
|
213
|
+
It is wrong wherever the length is the AUTHOR's choice, and it fails in the worst available way.
|
|
214
|
+
The box is drawn at the budget and the value simply exceeds it: no ellipsis, no clamp, no
|
|
215
|
+
scrollbar, nothing to scroll. Measured on a record note, 285 characters drew **76px of a 116px
|
|
216
|
+
value** — two full lines gone with nothing on screen admitting it. A truncation mark would have
|
|
217
|
+
been kinder; this is silent. So the test is not "is this field long" but **"who decides the
|
|
218
|
+
length"** — the field, or whoever is typing.
|
|
219
|
+
|
|
220
|
+
**And it gets worse as the surface narrows**, which is the opposite of the intuition that a
|
|
221
|
+
desktop check is the demanding one. A budget is a count of LINES and the wrap point moves: the
|
|
222
|
+
same value measured 60px hidden at 1280 and **96px hidden at 375**, because the narrower column
|
|
223
|
+
wrapped it into more lines against the same three-line box. A fixed reserve verified on a wide
|
|
224
|
+
screen is not verified.
|
|
225
|
+
|
|
226
|
+
`autoGrow` makes the budget a minimum and fits the value. It costs nothing the fixed reserve was
|
|
227
|
+
protecting, and the reason is worth stating because it is easy to assume otherwise: an
|
|
228
|
+
`InlineTextInput` is ONE input in both states, never a resting view swapped for an editor, so a
|
|
229
|
+
grown box is the same height focused as at rest — measured identical to the pixel, box and
|
|
230
|
+
origin. The reserve was guarding against a jump the one-element design had already made
|
|
231
|
+
impossible. Growing on a control that DOES swap (number, date, time — their resting display is
|
|
232
|
+
formatted) would reintroduce it, which is why the prop lives here and not on those.
|
|
233
|
+
|
|
234
|
+
Two places to keep the reserve anyway: a value that can be **arbitrarily long** (growth has no
|
|
235
|
+
ceiling — 406 characters is already 238px, so a field that might hold a document wants the budget,
|
|
236
|
+
and overflowing it is the signal the value belongs elsewhere), and a **grid cell** (`variant="bare"`
|
|
237
|
+
in a `DataGrid`), where a cell varying with its data breaks the row rhythm the grid promises. Grow
|
|
238
|
+
on a record surface; reserve in a table. Worked both ways in `dev/pages/inline_edit.tsx`
|
|
239
|
+
§ Multiline — the same value in a reserved field and a grown one, side by side.
|
|
240
|
+
|
|
206
241
|
### The row stack — `DetailTable` + `DetailRow`
|
|
207
242
|
|
|
208
243
|
A STACK of rows lives in a `DetailTable` (label, value laid out like a TABLE: `labelWidth` /
|
|
@@ -291,6 +326,34 @@ row that wants it. See the "General" section of `tpl_record`, which also reads t
|
|
|
291
326
|
full record surface. Not every field is a same-height swap — a tag field, a status, or an
|
|
292
327
|
attachment grid edit in place too (below).
|
|
293
328
|
|
|
329
|
+
## When the editor is a MODE instead of a resident field
|
|
330
|
+
|
|
331
|
+
The section above is the default and holds almost everywhere: the value IS the editor, nothing
|
|
332
|
+
moves, and there is no verb to find. It has exactly one exception, and it is decided by a
|
|
333
|
+
question with a factual answer — **does this value already appear somewhere else on the same
|
|
334
|
+
surface?**
|
|
335
|
+
|
|
336
|
+
On a FEED, it does. A row's identity line is the value, and the body opens under it, so a
|
|
337
|
+
resident editor renders the same sentence twice, stacked and identical. Quieting the frame does
|
|
338
|
+
not help: a sentence appearing twice reads as a mistake at any weight. So the editor becomes a
|
|
339
|
+
mode, reached by an **Edit** verb in the row's trailing slot beside its other actions.
|
|
340
|
+
|
|
341
|
+
**Except when the value is EMPTY, and that half matters more.** There is nothing to duplicate,
|
|
342
|
+
and a surface fed by automations and extractions receives entries with no words in them
|
|
343
|
+
routinely — a recording lands, the row says it has not been written up, and the empty field is
|
|
344
|
+
the invitation to write it. Withhold it there and the feed can RECEIVE an entry it gives the
|
|
345
|
+
reader no way to finish. **An empty value shows its field; a filled one waits to be asked.**
|
|
346
|
+
|
|
347
|
+
Two things follow that are easy to get wrong in the other direction:
|
|
348
|
+
|
|
349
|
+
- **Name the verb for the words, not the row.** "Edit", not "Edit entry" — on a feed the entry
|
|
350
|
+
is a record of something that happened, and only the prose a PERSON wrote is editable at all.
|
|
351
|
+
Derived values (a header's participants), verbatim ones (a transcript), artifacts (media,
|
|
352
|
+
files) and a model's output (a summary — you re-run it, you do not hand-edit it) are all
|
|
353
|
+
read-only, and a verb naming the whole entry claims otherwise.
|
|
354
|
+
- **One row in edit mode at a time.** Two half-finished sentences on one feed leave the reader
|
|
355
|
+
unable to say which one the next keystroke lands in.
|
|
356
|
+
|
|
294
357
|
## Fieldset form — fields lay out on a RESPONSIVE two-column grid
|
|
295
358
|
|
|
296
359
|
Never hard-code columns, never 3-up. A fieldset is `flexDirection:row, flexWrap:wrap,
|