@lotics/ui 41.1.0 → 41.3.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 +83 -0
- package/docs/ai_patterns.md +36 -0
- package/docs/catalog.md +50 -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 +4 -2
- 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,89 @@ 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.3.0 — `Timeline` rows, embedded markdown, and a disclosure that is not a link
|
|
8
|
+
|
|
9
|
+
Nothing here removes an API or breaks a type. Two of the changes alter what your rows render on
|
|
10
|
+
sight; the rest is additive.
|
|
11
|
+
|
|
12
|
+
### `Timeline` bounds its own text, and `right` leaves the press target
|
|
13
|
+
|
|
14
|
+
**The label clamps to two lines** (the description too). It was unbounded, which is invisible
|
|
15
|
+
until a caller puts user-authored prose in it: measured on a real interaction log, one
|
|
16
|
+
700-character summary drew a **180px label** — nine lines for one entry — and the disc, which
|
|
17
|
+
centred on the label row, ended up 80px below the first line it was meant to sit beside.
|
|
18
|
+
|
|
19
|
+
If your label can run long, put the full text in `details` as well. The row is then a
|
|
20
|
+
truncation and the detail its source, which is the drill-down every expandable register row
|
|
21
|
+
already makes:
|
|
22
|
+
|
|
23
|
+
```diff
|
|
24
|
+
label: entry.summary,
|
|
25
|
+
+ details: <Text size="sm">{entry.summary}</Text>,
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**`right` renders beside the press target, not inside it.** On a row with `details` the whole
|
|
29
|
+
row was a `<button>`, so an interactive `right` — a row-action menu, an `IconButton` — nested a
|
|
30
|
+
button inside a button: invalid HTML, and one click reaching two handlers. It is now a sibling,
|
|
31
|
+
and the chevron closes the pressable region, so the order reads `label … ⌄ right`. A
|
|
32
|
+
non-interactive `right` (a duration, a `Badge`) moves 14px right and is otherwise unaffected.
|
|
33
|
+
The hover wash now stops where the press target does, which is the point: a wash that ran under
|
|
34
|
+
a row action made that action invisible exactly when the pointer reached it.
|
|
35
|
+
|
|
36
|
+
**Everything in the band aligns to the label's FIRST LINE, not to the label block** — the disc,
|
|
37
|
+
the chevron and whatever you pass as `right`. Centring and first-line alignment are the same
|
|
38
|
+
answer until a label wraps and then they are 10px apart, so making two-line labels ordinary made
|
|
39
|
+
the old behaviour wrong. **A single-line row is pixel-identical** (measured: 40px band, zero
|
|
40
|
+
drift on all three ornaments); only wrapped rows change, and they change to correct.
|
|
41
|
+
|
|
42
|
+
**`TimelineItem` takes `placeholder`.** It marks a label the SURFACE supplied rather than one the
|
|
43
|
+
record holds, and draws it in the ink an unfilled field uses. A feed fed by automations and
|
|
44
|
+
extractions routinely receives an entry with no prose in it, and `label: string` forces the
|
|
45
|
+
caller to invent some — which then renders in body ink, so "nobody has written this up" and
|
|
46
|
+
"this is what happened" look identical.
|
|
47
|
+
|
|
48
|
+
```diff
|
|
49
|
+
- label: entry.gist || `${entry.direction} ${entry.kind}`,
|
|
50
|
+
+ label: entry.gist || "Not written up yet",
|
|
51
|
+
+ placeholder: !entry.gist,
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### `Markdown` takes `variant`
|
|
55
|
+
|
|
56
|
+
`"document"` (the default, and what every current caller keeps) is markdown that IS the surface
|
|
57
|
+
— its `h1` is the page's `h1`. `"embedded"` is markdown that is a VALUE on a page someone else
|
|
58
|
+
designed, and collapses every heading to the body's own size.
|
|
59
|
+
|
|
60
|
+
```diff
|
|
61
|
+
- <Markdown>{record.aiSummary}</Markdown>
|
|
62
|
+
+ <Markdown variant="embedded">{record.aiSummary}</Markdown>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Reach for it whenever the text was not written by whoever built the screen — now the common
|
|
66
|
+
case, because the writer is routinely a model. Measured on a real feed, a machine summary at
|
|
67
|
+
document scale drew 18px/600 headings, one rung under the section heading containing it.
|
|
68
|
+
|
|
69
|
+
`MarkdownProps` is reached from `@lotics/ui/markdown_types`, the same way `MediaPlayerProps` is;
|
|
70
|
+
the component entry points do not re-export it.
|
|
71
|
+
|
|
72
|
+
### `TextDisclosure`, and what underlined text is allowed to mean
|
|
73
|
+
|
|
74
|
+
New component: muted underlined text that REVEALS adjacent content in place — a transcript under
|
|
75
|
+
its player, a long value under its summary. It exists because the rule used to be "underlined
|
|
76
|
+
text is the NAVIGATION affordance and nothing else", and following that produced worse screens:
|
|
77
|
+
the quiet control it left you (`Button color="muted"`) measures transparent, borderless and
|
|
78
|
+
undecorated at rest, so its affordance arrives only on hover.
|
|
79
|
+
|
|
80
|
+
The rule is now scoped rather than absolute, and the INK carries the difference:
|
|
81
|
+
|
|
82
|
+
| | |
|
|
83
|
+
|---|---|
|
|
84
|
+
| blue + `role="link"` (`Link`) | leaves this surface |
|
|
85
|
+
| muted underline (`TextDisclosure`) | reveals more of it, in place |
|
|
86
|
+
|
|
87
|
+
Anything that MUTATES still carries a control surface — `Button` in chrome, `InlineButton` on a
|
|
88
|
+
field. Nothing to migrate: existing `Link`/`TextLink` usage is unaffected.
|
|
89
|
+
|
|
7
90
|
## 41.1.0 — `FileRows` no longer captions every row with the file type
|
|
8
91
|
|
|
9
92
|
`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,14 @@ 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`.
|
|
417
|
+
- **`markdown_types`** — `MarkdownProps`; types only.
|
|
407
418
|
- **`markdown.css`** — import once for the web markdown styling.
|
|
408
419
|
- **`format_date`** — `formatDate` / `parseDate` / `toISODate` + `DateFormatStyle`.
|
|
409
420
|
- **`format_money`** — `formatMoney` / `formatCompactNumber`.
|
|
@@ -488,7 +499,10 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
488
499
|
image-less entities — groups, organizations. A person → `Avatar`/`MemberChip`.
|
|
489
500
|
- **`wave_avatar`** — `WaveAvatar`: decorative animated waveform avatar (voice/audit
|
|
490
501
|
history); animates on web, renders a static fallback on native.
|
|
491
|
-
- **`member_chip`** — `MemberChip`: avatar + name; the universal person render.
|
|
502
|
+
- **`member_chip`** — `MemberChip`: avatar + name; the universal person render. `size` scales
|
|
503
|
+
BOTH — `sm` gives a 24px avatar and a 12px name, every larger rung keeps the name at body
|
|
504
|
+
size. Reach for `sm` when the chip sits inside a sentence or a dense row, where a body-size
|
|
505
|
+
name would collide with the 12px text around it.
|
|
492
506
|
|
|
493
507
|
### Layout & surfaces
|
|
494
508
|
|
|
@@ -632,8 +646,20 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
632
646
|
with no `href` it stays NEUTRAL: plain underlined text to wrap in your own pressable,
|
|
633
647
|
or a MARKER that a value leads somewhere (a record reference whose press opens a peek,
|
|
634
648
|
not a trip). `color` overrides either way; inherits every `Text` prop. It does NOT
|
|
635
|
-
act:
|
|
636
|
-
|
|
649
|
+
act: anything that MUTATES carries a control surface (`Button`/`InlineButton`).
|
|
650
|
+
- **`text_disclosure`** — `TextDisclosure`: the counterpart to `TextLink` — muted
|
|
651
|
+
underlined text that REVEALS adjacent content in place (a transcript under its
|
|
652
|
+
player, a long value under its summary). Underlined text either goes somewhere or
|
|
653
|
+
reveals something, and the INK says which: blue + `role="link"` leaves the surface,
|
|
654
|
+
muted stays on it. Takes `expanded`/`onToggle` and a `label` NOUN — it supplies the
|
|
655
|
+
verb, so two call sites cannot end up with "Show" and "View". Reach for it instead
|
|
656
|
+
of the two things that look right and are not: `Button color="muted"` measures
|
|
657
|
+
transparent, borderless and undecorated at rest, so its affordance arrives only on
|
|
658
|
+
hover (invisible to keyboard and touch, and against a text column it reads as a
|
|
659
|
+
label); `Accordion` is a list-ROW disclosure, heavy furniture for a verb nested
|
|
660
|
+
inside a row that already has a chevron. Announces `role="button"` + `aria-expanded`
|
|
661
|
+
— reaching for `Link` instead tells a screen-reader user they are leaving a page
|
|
662
|
+
that is about to unfold under them.
|
|
637
663
|
- **`chip`** — `Chip`: the generic pill — pressable when `onPress` (announces as a button;
|
|
638
664
|
pass `accessibilityLabel` when children aren't self-describing text) + an
|
|
639
665
|
absolutely-positioned dismiss ✕ sibling when `onDismiss` (its name = `dismissTooltip` ??
|
|
@@ -1395,7 +1421,20 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1395
1421
|
`pxPerDay`) + `GanttTask`/`GanttLabels` types. **Limitation:** labels are not
|
|
1396
1422
|
provider-wired — pass `labels` (`DEFAULT_GANTT_LABELS` is English).
|
|
1397
1423
|
- **`timeline`** — `Timeline`: a heterogeneous event LOG — per-row icon + expandable
|
|
1398
|
-
details; models the past, NOT progress.
|
|
1424
|
+
details; models the past, NOT progress. Three things it decides for you, because a row
|
|
1425
|
+
cannot be trusted to a caller's data: the **label clamps to two lines** (and the
|
|
1426
|
+
description likewise) — a row is a scan target, and an unclamped one lets a single
|
|
1427
|
+
user-authored note set the section's height, so put the full text in `details` too;
|
|
1428
|
+
**`placeholder`** marks a label the SURFACE supplied rather than one the record
|
|
1429
|
+
holds, drawing it in the unfilled-field ink — reach for it on any feed that fills
|
|
1430
|
+
from more than one direction, where an entry routinely arrives with no words in it;
|
|
1431
|
+
**the disc, the chevron and `right` all sit on the label's FIRST LINE**, so a wrapped
|
|
1432
|
+
row lines up with an unwrapped one (the worked reference for the rule in
|
|
1433
|
+
composition.md §registers); and **`right` renders beside the press target, not inside
|
|
1434
|
+
it**, so a control there is valid HTML and keeps its own click — though on a row that
|
|
1435
|
+
EXPANDS, verbs belong in the BODY and `right` is better kept for a fact (a duration, a
|
|
1436
|
+
badge). A control sharing the label row with the press target is what produced the nesting,
|
|
1437
|
+
the first-line box and the chevron-pairing problems in the first place.
|
|
1399
1438
|
- **`checklist`** — `Checklist` + `ChecklistGroup` + `ChecklistItem` + `ChecklistNote` /
|
|
1400
1439
|
`ChecklistActions` / `ChecklistField`: THE run of tickable rows, at any
|
|
1401
1440
|
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,
|
package/docs/templates.md
CHANGED
|
@@ -241,11 +241,10 @@ screens — register, per-row action, gated selection, and act-on-many in one. T
|
|
|
241
241
|
- **Linked records** still push an editable screen for that record inside the drawer via the
|
|
242
242
|
hosted `ScreenRouter` — while `canGoBack` the drawer header swaps to a back button + the
|
|
243
243
|
pushed id and the ◀ ▶ sequencer hides; back pops with scroll preserved.
|
|
244
|
-
-
|
|
244
|
+
- One thing this template no longer demonstrates, so look elsewhere: the drawer does NOT
|
|
245
245
|
write back to its row (settling a ledger used to flip the register's paid state live — the
|
|
246
|
-
drawer's money is the record's now)
|
|
247
|
-
|
|
248
|
-
the record surface has no counterpart for.
|
|
246
|
+
drawer's money is the record's now). The communications log lives on the record surface as
|
|
247
|
+
[`tpl_record`](../examples/tpl_record.tsx) § Activity.
|
|
249
248
|
- **The "Enter data" INTAKE FORK** (the New CTA) — AI FIRST, the form as fallback, one phased
|
|
250
249
|
dialog. The whole register page is ALSO a `<FileDropTarget … paste>` (the whole-surface intake
|
|
251
250
|
standard): a file dropped or Ctrl/Cmd+V pasted ANYWHERE on the register opens this same dialog
|
|
@@ -457,11 +456,123 @@ billing, and quick-capture templates. Top → bottom:
|
|
|
457
456
|
a label-left capture form (auto-stamped date/receipt-no, record-prefilled fields, the
|
|
458
457
|
right input each) whose CTA and produced `FileRow` ride an empty-label `DetailRow` — the
|
|
459
458
|
form-action alignment law (data_entry.md), worked.
|
|
460
|
-
- **
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
459
|
+
- **Activity — what was SAID with the other party**, newest first, directly under Progress: the
|
|
460
|
+
two answer a reader's first two questions in order, where the record stands and what passed
|
|
461
|
+
between us to get it there. It is NOT an audit trail and NOT the handoff — see the next bullet
|
|
462
|
+
for the line, which is what keeps this section from becoming the change log Progress refuses.
|
|
463
|
+
|
|
464
|
+
**The row's LABEL is the GIST** — what came of the exchange, in the reader's own words. The
|
|
465
|
+
medium and the direction are metadata on the muted supporting line beneath it. Building it the
|
|
466
|
+
other way round is the natural mistake and it makes the feed unreadable: every row renders as a
|
|
467
|
+
pair of enum values ("Outbound Meeting"), the substance sits behind a chevron, and a column of
|
|
468
|
+
near-identical labels defeats the scan the feed exists for. Measured on a real one, that shape
|
|
469
|
+
spent 504px of chrome on 70px of content.
|
|
470
|
+
|
|
471
|
+
**ONE anatomy, a body that VARIES.** An entry can be a call, a recorded demo, an email, a
|
|
472
|
+
message or a note typed between meetings — and those differ in what they CARRY, not in what
|
|
473
|
+
they are: all of them answer what-came-of-it, which way, over what, and when. So the four
|
|
474
|
+
invariants are the row, and the body is a SET OF BLOCKS rather than a shape per kind. A shape
|
|
475
|
+
per kind writes the same block five times and lets the copies drift, and the sixth kind — the
|
|
476
|
+
one nobody has thought of — needs a sixth. **Every block is optional and any combination is
|
|
477
|
+
legal**, because what an entry carries is independent of what it is: a call may arrive as a
|
|
478
|
+
recording alone, gain a transcript minutes later and a summary after that.
|
|
479
|
+
|
|
480
|
+
The blocks, in the order they render (`ActivityBody` in `tpl_record` is the worked one):
|
|
481
|
+
|
|
482
|
+
| Block | What it is | How it renders |
|
|
483
|
+
|---|---|---|
|
|
484
|
+
| Participants | ONLY what cannot be derived — a third party who joined, an email's From/To. The record is already the counterparty and the row already says who logged it, so naming the obvious attendees is padding | muted line |
|
|
485
|
+
| Subject | the one thing an email has that nothing else does | `sm` medium |
|
|
486
|
+
| Media | the exchange itself, recorded | `MediaPlayer` — **give it a box**, it fills its parent and collapses to nothing without one |
|
|
487
|
+
| Transcript | VERBATIM and long; the source a summary was made from | a muted underlined `TextLink` in your own `Pressable`, revealing PLAIN text — see the disclosure exception in composition.md §"Commit & feedback surfaces" |
|
|
488
|
+
| Prose | what a PERSON wrote — a note, a counterparty's email | `Markdown variant="embedded"` on the page's own ground |
|
|
489
|
+
| AI prose | what a MODEL wrote | ONE short label INSIDE the `Inset` ("AI summary") — floating above it, a 12px muted fragment over a tinted box reads as an orphan. Name the evidence only where it is NOT on screen: over a player the reader can see, "from the recording" is four words restating a video. Let the label replace the block's own `## Heading` rather than sit above it |
|
|
490
|
+
| Files | what came with it | documents → `FileRow`s (a name identifies them); images → `FileThumbnailGrid` (content does) |
|
|
491
|
+
| Source | where it happened, if openable | `Link` |
|
|
492
|
+
| Author | who filed it | `MemberChip` for a person; `WaveAvatar` + name for an automation |
|
|
493
|
+
|
|
494
|
+
**The verbs — Edit and Delete — live at the BOTTOM of the open body, not in the row's trailing
|
|
495
|
+
slot.** Three defects came out of putting a control on the label row (a button nested in the
|
|
496
|
+
row's own button, a fixed-height box to hold it on the first line, and a gap close enough to
|
|
497
|
+
the chevron to read as one cluster) and every one of them exists only because something
|
|
498
|
+
interactive shared the row. In the body there is nothing to share with, the verbs sit beside
|
|
499
|
+
the content they act on, and the expansion the reader already chose IS the look-before-you-
|
|
500
|
+
delete step — which makes "two clicks" the feature rather than the cost. Provenance sits left
|
|
501
|
+
on that footer, the verbs right; they are two groups, so space pushes them apart rather than a
|
|
502
|
+
uniform gap making them peers.
|
|
503
|
+
|
|
504
|
+
Delete belongs there DESPITE the feed being a record — because of what a multi-writer feed
|
|
505
|
+
actually produces. An extraction filing against the wrong contact and an automation
|
|
506
|
+
double-filing a recording are not history, and a feed with no way to remove them accumulates
|
|
507
|
+
garbage it also asks you to trust. Name what leaves with the entry in the confirmation: a
|
|
508
|
+
count somewhere else is the part nobody expects.
|
|
509
|
+
|
|
510
|
+
Only the GIST and the note are editable — everything else is derived, verbatim, an artifact or
|
|
511
|
+
a model's output. The editor is a MODE behind an **Edit** verb rather than a resident field,
|
|
512
|
+
because the gist is already the row's label and a resident one renders it twice; an EMPTY gist
|
|
513
|
+
shows its field regardless, because that is the write-up path for an entry an automation filed.
|
|
514
|
+
The rule and its two corollaries are in data_entry.md §"When the editor is a MODE instead of a
|
|
515
|
+
resident field".
|
|
516
|
+
|
|
517
|
+
**A block earns its place by carrying what nothing else on the row does.** The failure is not
|
|
518
|
+
a missing block, it is three blocks saying one thing: a participants line, a rep's note and a
|
|
519
|
+
machine summary all reporting that the spreadsheet owner was absent. When the summary is good
|
|
520
|
+
the rep usually adds nothing, and an entry with no note is the normal case rather than an
|
|
521
|
+
incomplete one. Cut on sight: a label above a block that already labels itself, a fact the
|
|
522
|
+
record or the row already states, and any block whose content the reader can see rendered
|
|
523
|
+
directly above it.
|
|
524
|
+
|
|
525
|
+
**Two of those rows are the whole point of the section on a modern record.** A feed fills from
|
|
526
|
+
more than one direction — a person, a chat agent, an extraction off an upload, an automation
|
|
527
|
+
firing on a file — so the surface has to say which values nobody wrote and which a machine did.
|
|
528
|
+
An entry with no prose renders `placeholder` (see `Timeline`), never a phrase assembled from
|
|
529
|
+
the enums; and a machine's text is separated from a person's by SHAPE — an attribution line
|
|
530
|
+
plus a recessed ground — never by a weight nudge. The reasoning and the failure modes are in
|
|
531
|
+
ai_patterns.md §After the run.
|
|
532
|
+
|
|
533
|
+
**The transcript folds, and it reveals in place.** It is the source a summary was made from,
|
|
534
|
+
read rarely and in full when at all — and the reason anyone opens it is to check a claim the
|
|
535
|
+
summary made, so a dialog is wrong: it takes the claim off screen at the moment they want to
|
|
536
|
+
compare. Reveal it whole rather than into a scroll box; a scroller inside a scrolling drawer
|
|
537
|
+
traps the wheel, and a reader who pressed it asked for the length.
|
|
538
|
+
|
|
539
|
+
**The toggle is underlined text, and that is a deliberate exception.** Three controls were
|
|
540
|
+
tried. `Button color="muted"` measures transparent, borderless and undecorated at rest — the
|
|
541
|
+
wash arrives only on hover, so the affordance is hover-only (banned in §"Commit & feedback surfaces") and it
|
|
542
|
+
reads as a stray label between two content blocks. `Accordion` fixes the affordance and
|
|
543
|
+
overcorrects: it is a LIST-ROW primitive, a full-width header with its own chevron, nested
|
|
544
|
+
inside a row that already has one. What is left is a muted underlined `TextLink` in your own
|
|
545
|
+
`Pressable` — visible at rest, on the column's own left edge, and carrying no more weight than
|
|
546
|
+
the words. The ink keeps underline honest: blue leaves the surface, muted stays on it.
|
|
547
|
+
|
|
548
|
+
**Reveal it PLAIN, with no tinted well.** The toggle directly above already says what the text
|
|
549
|
+
is; a panel would put two identical recessed boxes on one row meaning two different things — a
|
|
550
|
+
verbatim record and a machine's reading of it. Keep the `Inset` for the machine's.
|
|
551
|
+
|
|
552
|
+
**The capture sits AT the feed**, as a `Composer` — compact until typed into. Logging an entry
|
|
553
|
+
is this section's input, not a distinct AREA of the record, and a section of its own costs a
|
|
554
|
+
rail entry, which is a claim that there is somewhere else to go.
|
|
555
|
+
|
|
556
|
+
**The tail FOLDS, it does not page.** A record's feed is read newest-first and the rest is
|
|
557
|
+
reference; an unbounded one makes every section below it unreachable by scroll. Show the recent
|
|
558
|
+
few with a toggle naming how many are behind it — a register pages, a record's feed folds.
|
|
559
|
+
|
|
560
|
+
The toggle is a `TextDisclosure`, the same control as the transcript's, because it is the same
|
|
561
|
+
act: it reveals more of what is already here. A `Button color="muted"` is the reach that feels
|
|
562
|
+
right and is not — measured, it is transparent, borderless and undecorated at rest, so the
|
|
563
|
+
affordance only arrives on hover and it reads as a stray line under the feed. It sits on the
|
|
564
|
+
SECTION's left edge, not the rows': the rows are inset by the disc rail, and at the fold's line
|
|
565
|
+
the rail has ended, so nothing visible occupies that indent. Same for any other fold on the
|
|
566
|
+
surface — a document set's "show the rest" is this control too.
|
|
567
|
+
- **No Handoff section.** Progress owns it: it says where the record SITS and carries the act
|
|
568
|
+
that moves it, so a second section repeating the handoff was one more place to look for one
|
|
569
|
+
answer. What that costs is explicit: the sibling record a handoff creates has no home on this
|
|
570
|
+
surface. It belongs on the Progress stage that produced it if an app needs it back.
|
|
571
|
+
|
|
572
|
+
**Activity is not that trail, and the line is the SUBJECT.** Activity carries what a person
|
|
573
|
+
said or sent to a counterparty; the handoff trail carries where the record moved and which
|
|
574
|
+
field changed. Put desk transitions in Activity and it becomes the audit log this template
|
|
575
|
+
declined to build — a feed of everything, scanned for nothing.
|
|
465
576
|
|
|
466
577
|
Recall is the undo: confirmed, it withdraws the sibling, clears the custody stamp that
|
|
467
578
|
handoff wrote, and returns the record to the desk BEFORE it — one step, never a jump to the
|