@lotics/ui 44.13.0 → 45.2.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 +9 -2
- package/MIGRATION.md +61 -5
- package/docs/ai_patterns.md +2 -3
- package/docs/catalog.md +35 -14
- package/docs/composition.md +143 -42
- package/docs/data_entry.md +17 -6
- package/docs/reviewing.md +24 -12
- package/docs/templates.md +1 -1
- package/package.json +2 -1
- package/src/accordion.tsx +2 -2
- package/src/avatar_group.tsx +1 -1
- package/src/avatar_size.ts +2 -3
- package/src/card_select_item.tsx +7 -6
- package/src/color_tokens.ts +22 -6
- package/src/combobox.tsx +11 -1
- package/src/control_surface.ts +22 -16
- package/src/date_stamp.tsx +1 -3
- package/src/detail_row.tsx +4 -5
- package/src/dialog.tsx +13 -6
- package/src/drawer.tsx +18 -4
- package/src/filter_chip.tsx +1 -2
- package/src/finding.tsx +24 -7
- package/src/heading_altitude.ts +65 -0
- package/src/icon.tsx +8 -0
- package/src/inline_edit.tsx +14 -3
- package/src/inline_markdown.tsx +118 -0
- package/src/inline_text_input.tsx +31 -7
- package/src/ledger.tsx +3 -3
- package/src/markdown.web.tsx +3 -6
- package/src/matrix.tsx +1 -1
- package/src/modal.tsx +10 -1
- package/src/popover.tsx +151 -142
- package/src/pressable_row.tsx +24 -15
- package/src/reference_field.tsx +11 -9
- package/src/section_heading.tsx +57 -71
- package/src/select.tsx +17 -0
- package/src/stepper.tsx +2 -3
- package/src/summary.tsx +3 -3
- package/src/table.tsx +4 -1
- package/src/text_utils.ts +3 -4
- package/src/timeline.tsx +3 -4
package/AGENTS.md
CHANGED
|
@@ -15,9 +15,9 @@ 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) | **a picker over a select field keeps the field's colours** (`optionPicker` — the hand-rolled `map` to `{value,label}` drops `color`, so a value reads coloured in a register and grey in the editor); a choice that GATES a form is usually one not to ask (two doors to one room is not a chooser; show the dominant path, offer the other beside its submit, and give a recovery link a quieter treatment than an alternative way in); Which editing pattern for which job — inline edit, fieldset forms, choosing a CHOICE control by option count (and why "no selection" is a PLACEHOLDER, never an option valued `""` — the picker reads that as nothing chosen and renders down to its chevron), 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`). |
|
|
18
|
+
| [docs/data_entry.md](./docs/data_entry.md) | **a picker over a select field keeps the field's colours** (`optionPicker` — the hand-rolled `map` to `{value,label}` drops `color`, so a value reads coloured in a register and grey in the editor); a choice that GATES a form is usually one not to ask (two doors to one room is not a chooser; show the dominant path, offer the other beside its submit, and give a recovery link a quieter treatment than an alternative way in); Which editing pattern for which job — inline edit, fieldset forms, choosing a CHOICE control by option count (and why "no selection" is a PLACEHOLDER, never an option valued `""` — the picker reads that as nothing chosen and renders down to its chevron), 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 swap rule for a NEW editor (same string ⇒ one element, different string ⇒ swap) worked through its two live cases — `InlineMarkdown` renders prose at rest and refuses to collapse when opened, while a URL takes `InlineTextInput`'s `link` TREATMENT because marking it changes its ink and not its characters, the commit-on-blur vs action-press ordering law (the kit gates the press — `pending_commits`). |
|
|
19
19
|
| [docs/ai_patterns.md](./docs/ai_patterns.md) | AI acts, the human stays in charge — composer, live run feed (`AgentRun`), the one law's split — it turns on WHO supplied the values (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, its kind derived from before/after rather than a boolean "did this change" — an empty `before` is `added`, never `changed`), `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 — **the form comes before the treatment** (name what the subject IS — register, rail, board, timeline, deviation queue — before reaching for parts; a subject flattened into rows is well-typed and says nothing, and reuse-first is about not duplicating primitives, not about a screen's shape; **a hierarchy in the DATA does not oblige one in the UI** — who MAINTAINS the structure decides, and a set the reader scans is a register whatever shape the data has), canvas + content column, heading altitude (incl. eyebrow vs group lead — a label is one or the other, and a section heading's own row never wraps so a control label that repeats the title can squeeze the title+description into a narrow ribbon at phone width), 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), **the register's rhythm** (no hairline between rows — their own height and the hover wash separate them; the register's one line is the band capping the columns, because that is the break whitespace cannot state. Density is `TableRow`'s `minHeight`, never a mode), **a register that TRIAGES** (`TableGroup` — titled bands ordered by what needs the reader now, grouped by what implies a different ACTION rather than by a category already in a column, and what the screen can CLASSIFY kept as a separate list from what it RENDERS, so a value omitted on purpose does not resurface under *Unclassified* and one added later does not vanish), **the register's own craft** (a column header treated as CHROME a step lighter than row metadata, sentence case in the table header too, and a supporting line one RUNG BELOW the line it supports rather than a fixed size — plus the half that rule was missing: every SUBJECT on one row starts at the same rung, the subject leads by WEIGHT and its identity mark rather than by size, and whether the supporting line drops a rung is decided by what else separates the pair; plus the two ways a `columns` array and a rendered row disagree — a partial `priority` annotation, and a conditional column whose cell was not hidden with it), **identity marks** (a mark that looks the same on every row carries nothing — `Avatar` derives its hue from the name and takes circle/square for person vs organization; the palette is ordered by contrast, not spectrum, and never paints a person red or amber; a column of faces must not draw an UNBOUNDED audience as faces, and an overlapping stack separates with translucent INK rather than the surface colour, which a row's wash keeps changing out from under), **where the accent goes** ("which of these" — a selected row, an active filter; never DATA and never the focus ring, while a page-scale position marker like an active TAB takes `primary`, the ink the kit already spends on "this is on"; and selection must not share a wash with hover), **theming is an app-level ESCAPE HATCH, not a product surface** (`applyLoticsTheme({ … })` once at boot, before `mount`, writing the `--lotics-*` roles on the document root; nothing in the platform writes a theme, and `apps.theme.color` is the launcher icon only — the five `var()` roles plus the three font rungs ARE the whole mechanism, and wrapping a component cannot replace them because RN-Web writes `fontFamily` per element and background/border do not inherit), color discipline, **typography** (the ramp is DATA in `type_ramp`, one table both platforms derive from; leading and tracking are baked per rung and a hand-set `letterSpacing` is always a second copy of the curve), whitespace, **where a commit sits** (with its fields, never an overlay's footer — but a full-page editor is not a dialog, so a scrolling document keeps its commit pinned), and how to TEST an overlay component (a `Popover`-backed surface never mounts under jsdom). |
|
|
20
|
+
| [docs/composition.md](./docs/composition.md) | The design-language contract — **the form comes before the treatment** (name what the subject IS — register, rail, board, timeline, deviation queue — before reaching for parts; a subject flattened into rows is well-typed and says nothing, and reuse-first is about not duplicating primitives, not about a screen's shape; **a hierarchy in the DATA does not oblige one in the UI** — who MAINTAINS the structure decides, and a set the reader scans is a register whatever shape the data has), canvas + content column, heading altitude (incl. eyebrow vs group lead — a label is one or the other, and a section heading's own row never wraps so a control label that repeats the title can squeeze the title+description into a narrow ribbon at phone width), 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), **the register's rhythm** (no hairline between rows — their own height and the hover wash separate them; the register's one line is the band capping the columns, because that is the break whitespace cannot state. Density is `TableRow`'s `minHeight`, never a mode), **a register that TRIAGES** (`TableGroup` — titled bands ordered by what needs the reader now, grouped by what implies a different ACTION rather than by a category already in a column, and what the screen can CLASSIFY kept as a separate list from what it RENDERS, so a value omitted on purpose does not resurface under *Unclassified* and one added later does not vanish), **the register's own craft** (a column header treated as CHROME a step lighter than row metadata, sentence case in the table header too, and a supporting line one RUNG BELOW the line it supports rather than a fixed size — plus the half that rule was missing: every SUBJECT on one row starts at the same rung, the subject leads by WEIGHT and its identity mark rather than by size, and whether the supporting line drops a rung is decided by what else separates the pair; plus the two ways a `columns` array and a rendered row disagree — a partial `priority` annotation, and a conditional column whose cell was not hidden with it), **identity marks** (a mark that looks the same on every row carries nothing — `Avatar` derives its hue from the name and takes circle/square for person vs organization; the palette is ordered by contrast, not spectrum, and never paints a person red or amber; a column of faces must not draw an UNBOUNDED audience as faces, and an overlapping stack separates with translucent INK rather than the surface colour, which a row's wash keeps changing out from under), **where the accent goes** ("which of these" — a selected row, an active filter; never DATA and never the focus ring, while a page-scale position marker like an active TAB takes `primary`, the ink the kit already spends on "this is on"; and selection must not share a wash with hover), **theming is an app-level ESCAPE HATCH, not a product surface** (`applyLoticsTheme({ … })` once at boot, before `mount`, writing the `--lotics-*` roles on the document root; nothing in the platform writes a theme, and `apps.theme.color` is the launcher icon only — the five `var()` roles plus the three font rungs ARE the whole mechanism, and wrapping a component cannot replace them because RN-Web writes `fontFamily` per element and background/border do not inherit), color discipline, **a fact band is read at a GLANCE and a glance is WIDE** (header facts paired into columns rather than stacked one per row — the tell is a screen reported as plain whose treatment is fine, spending its height on facts and its width on nothing; plus a record owes the reader the SIZE of its subject, summed from what the page already holds), **typography** (the ramp is DATA in `type_ramp`, one table both platforms derive from; leading and tracking are baked per rung and a hand-set `letterSpacing` is always a second copy of the curve), whitespace, **where a commit sits** (with its fields, never an overlay's footer — but a full-page editor is not a dialog, so a scrolling document keeps its commit pinned), and how to TEST an overlay component (a `Popover`-backed surface never mounts under jsdom). Selection is never the brand — a neutral ground on a row, a dark doubled edge on a chip or card, with `accent_wash` kept for attention; a lookup key is not a column (the search matches it); `Table` sheds and stacks rather than scrolling horizontally, because narrowing is what asks whether a column earns its width. |
|
|
21
21
|
| [docs/reviewing.md](./docs/reviewing.md) | **Reviewing a screen you built** — the other docs say what good looks like; this one says how to find out whether you achieved it, because "it looks fine" is the same claim as "it should work" made with the same evidence. Measure, never eyeball: render it, extract computed values, let the table show the defect. Two GATES that outrank every treatment question (**SUBTRACT** — what question does this element answer, and count FACTS not elements; **RIGHT CONTROL** — name the species before treating it), then 11 probes each naming what to COLLECT and the SIGNATURE in the numbers: type inventory (range, singletons, the label/value shrink war), ground+border+radius across siblings, what each RULE separates, gap RATIOS, the state diff (paint may change, geometry may not), alignment drift (incl. a centred child that moves while its container measures identical), the surface walk (open it, press it, read the settled string), the DATA probe (what is really in the table — incl. grepping rendered text for internal ids), FIRST PAINT (an entry screen showing zero inputs is gated by something, usually a mode chooser), the ABSENCE pass that catches "bland", and microcopy. Plus the extraction snippets, what measurement cannot see, and how to act on a finding (fix the CLASS, and never resolve one with "the docs say X"). |
|
|
22
22
|
| [docs/testing.md](./docs/testing.md) | Driving the kit in a browser — the three anatomies where the a11y tree says one thing and a driver must do another: a `PressDoor` row whose named button always intercepts pointer events (by design), portalled overlays that render at the top of the DOM, and custom pointer drag that `dragTo` cannot move. |
|
|
23
23
|
| [docs/templates.md](./docs/templates.md) | The map of `examples/tpl_*.tsx` — what shape each template solves and which to start from (copy + adapt, never import) — plus the record-surface composition rules (pipeline order, static shape, decision budget) and the ACTIVITY shape — a communications feed where the row's label is the GIST and the body varies by medium, one anatomy rather than a row type per kind. |
|
|
@@ -61,6 +61,13 @@ CURRENT major only — upgrading an app across majors is `MIGRATION.md`.
|
|
|
61
61
|
`primary` `Button` beside `SectionHeadingTitle`, rendered empty or full. Under the rows it
|
|
62
62
|
extends, the verb MOVES with the row count and vanishes off-screen on a long list; a heading is
|
|
63
63
|
the one place it doesn't. → [composition.md §The add-placement law](./docs/composition.md).
|
|
64
|
+
- **A section heading's RUNG is the surface's answer, never the call site's.** There is ONE
|
|
65
|
+
`SectionHeadingTitle`; it renders `##` xl on a page and `####` md inside a pane, because the
|
|
66
|
+
container publishes the altitude — `DrawerScrollArea` / `DialogScrollArea` / `ModalBody` /
|
|
67
|
+
`PopoverContent` do it for you, and a pane you build yourself declares it with
|
|
68
|
+
`HeadingAltitudeContext`. Do not look for a per-surface heading variant and do not hand-pick a
|
|
69
|
+
size: a `##` inside a drawer is 24px over the drawer's own 20px title.
|
|
70
|
+
→ [composition.md §The heading ramp](./docs/composition.md).
|
|
64
71
|
- **`EmptyState` carries NO verb, and a FAILED read is not an empty one.** Four region states,
|
|
65
72
|
picked by what the region can ASSERT: `Skeleton`/`Loading` in flight → **`ErrorState`**
|
|
66
73
|
(`message`/`detail`/`onRetry`) on failure → `EmptyState` (succeeded, found nothing) →
|
package/MIGRATION.md
CHANGED
|
@@ -14,6 +14,63 @@ usage produced an interactive control nested in a `button`: invalid ARIA, and on
|
|
|
14
14
|
owners, since ticking the box also opened the file. A slot the docs tell you to put a control in
|
|
15
15
|
cannot be inside the control.
|
|
16
16
|
|
|
17
|
+
## 45.0.0 — a section heading stops shouting over the panel that holds it
|
|
18
|
+
|
|
19
|
+
*A MAJOR, because an export was removed.* Every consumer pinning `^44.x` is excluded by design:
|
|
20
|
+
the removal below breaks a build, and the altitude change silently re-ranks headings in any pane
|
|
21
|
+
that already composes its body in a content region. Neither should arrive with a routine install —
|
|
22
|
+
an app takes this release when someone is looking at it.
|
|
23
|
+
|
|
24
|
+
**`DialogSectionHeadingTitle` is REMOVED. Use `SectionHeadingTitle` everywhere — it now takes the
|
|
25
|
+
`####` rung by itself inside an overlay.** Replace the import and the tag; every slot
|
|
26
|
+
(`icon`, `description`, `info`, `level`) is unchanged, and the rendered output inside a dialog is
|
|
27
|
+
identical — measured, 16px/600/aria-level 4 before and after.
|
|
28
|
+
|
|
29
|
+
A `Drawer`'s title is `lg`, as is a `Dialog`'s and a `Modal`'s. A `SectionHeadingTitle` inside one
|
|
30
|
+
rendered `xl` — **24px under a 20px bar**, measured, and the same in every consuming app. The kit's
|
|
31
|
+
answer was a second component, which only moved the question to the call site: the caller still had
|
|
32
|
+
to know what was holding the heading, and callers reached for the plain one. Nothing covered the
|
|
33
|
+
drawer at all.
|
|
34
|
+
|
|
35
|
+
So the CONTAINER answers instead. `DrawerScrollArea`, `DialogScrollArea`, `ModalBody` and
|
|
36
|
+
`PopoverContent` publish the surface's heading altitude, and one `SectionHeadingTitle` renders `##`
|
|
37
|
+
xl on a page and `####` md inside them. **Nothing to pass, and nothing to change in an app that was
|
|
38
|
+
already composing pane content in those regions** — the fix arrives with the upgrade.
|
|
39
|
+
|
|
40
|
+
Three things worth knowing:
|
|
41
|
+
|
|
42
|
+
- **A whole PAGE surface docked in an overlay is unaffected, deliberately.** The master-detail
|
|
43
|
+
register puts a record screen in a drawer; that screen brings its own `#` identity band, carries
|
|
44
|
+
its own gutters, and so sits in the container's BARE slot rather than the scroll area — which is
|
|
45
|
+
why the altitude is published by the scroll area rather than by `Drawer`. Its sections stay `##`
|
|
46
|
+
and its `###` subsections stay under them. A pane body that hand-rolls the gutter instead of
|
|
47
|
+
using the scroll area also keeps `##`; wrap it, as the gutter contract already asks, and it steps.
|
|
48
|
+
- **A pane you build yourself declares its own altitude** — new export
|
|
49
|
+
`@lotics/ui/heading_altitude`. A docked side panel is not an overlay and uses none of those
|
|
50
|
+
regions, but its headings must not shout across 400px either: wrap its content in
|
|
51
|
+
`<HeadingAltitudeContext.Provider value="panel">`.
|
|
52
|
+
- **In a pane, `####` is the FLOOR.** `SubsectionHeadingTitle` is `###` at every altitude and would
|
|
53
|
+
out-rank the section holding it. A named group inside a pane's section is
|
|
54
|
+
`Text size="sm" weight="semibold"`.
|
|
55
|
+
|
|
56
|
+
**`Finding`'s source is VISIBLY a link when `onOpenSource` is set.** It was a `PressableHighlight`
|
|
57
|
+
with `accessibilityRole="link"` wrapping muted caption text — reachable by keyboard, announced to a
|
|
58
|
+
screen reader, and indistinguishable from an inert caption to everyone else until they hovered it.
|
|
59
|
+
With a handler it now renders default-colour `medium` underlined; without one it is unchanged. No
|
|
60
|
+
API change. If a host was passing `onOpenSource` for sources it did not actually want to advertise,
|
|
61
|
+
pass it only for the ones it can open.
|
|
62
|
+
|
|
63
|
+
**`Combobox` stops suppressing `getOptionDescription` too.** 44.9.3 removed
|
|
64
|
+
`description={content ? undefined : desc}` from `OptionList` and left the identical line in
|
|
65
|
+
`Combobox` — the instance fixed, not the class. `optionPicker` sets `renderOptionContent`
|
|
66
|
+
whenever a badge is requested, so a Combobox caller passing both still silently lost the
|
|
67
|
+
subtitle. Found by an audit reading the source rather than the pixels; no app hits it today.
|
|
68
|
+
|
|
69
|
+
**`Matrix`'s corner label matches its column headers.** 44.10.0 moved the column headers to
|
|
70
|
+
`sm` / `muted` / `medium` and left the corner at `xs`/regular — so a label that names an axis
|
|
71
|
+
sat a rung AND a weight below its own peers, measured live as 12px/400 beside 14px/500. Same
|
|
72
|
+
structural role, same treatment.
|
|
73
|
+
|
|
17
74
|
## 44.9.3 — an option's description stops being erased, and a pivot's headers rejoin the ramp
|
|
18
75
|
|
|
19
76
|
**`OptionList` no longer suppresses `getOptionDescription` when `renderOptionContent` is set.**
|
|
@@ -1739,16 +1796,15 @@ const ref = useRef<SectionHandlers>(null);
|
|
|
1739
1796
|
// AFTER
|
|
1740
1797
|
<Accordion expanded={open} onToggle={setOpen}>
|
|
1741
1798
|
<AccordionHeader>
|
|
1742
|
-
<
|
|
1799
|
+
<SectionHeadingTitle icon="eye-off">Hidden fields</SectionHeadingTitle>
|
|
1743
1800
|
</AccordionHeader>
|
|
1744
1801
|
<AccordionContent>{body}</AccordionContent>
|
|
1745
1802
|
</Accordion>
|
|
1746
1803
|
```
|
|
1747
1804
|
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
now carries the same slots.
|
|
1805
|
+
Compose the heading rung exactly as for a static section — `SectionHeadingTitle` (`##` on a page,
|
|
1806
|
+
`####` inside an overlay, decided by the surface) or `SubsectionHeadingTitle` (`###`). Every rung
|
|
1807
|
+
carries the same slots, so a disclosure header loses no affordance.
|
|
1752
1808
|
|
|
1753
1809
|
## v19 from 18.x
|
|
1754
1810
|
|
package/docs/ai_patterns.md
CHANGED
|
@@ -553,9 +553,8 @@ the person's conclusion on the page's own ground; that contrast IS the signal.
|
|
|
553
553
|
|
|
554
554
|
**Model prose arrives with its own type ladder — take it away at the door.** A model asked for
|
|
555
555
|
a summary emits `##` headings and heavy `**bold**` knowing nothing of what surrounds it, and
|
|
556
|
-
document-scale markdown hands those an `h1` equal to your section heading
|
|
557
|
-
|
|
558
|
-
least-trustworthy text on the screen was also the loudest. Render every embedded value with
|
|
556
|
+
document-scale markdown hands those an `h1` equal to your section heading — so the
|
|
557
|
+
least-trustworthy text on the screen ends up the loudest. Render every embedded value with
|
|
559
558
|
`<Markdown variant="embedded">`.
|
|
560
559
|
|
|
561
560
|
And one that is not about treatment at all: **check that the read path projects every field the
|
package/docs/catalog.md
CHANGED
|
@@ -154,9 +154,17 @@ then inserts a newline and the value commits on blur. Both halves matter: a fiel
|
|
|
154
154
|
goes multiline once focused hides the text from the reader who never clicks, and one that
|
|
155
155
|
reserves less than it opens to shoves the rest of the form down the page on every click.
|
|
156
156
|
**`autoGrow`** makes that budget a MINIMUM and fits the value instead — see
|
|
157
|
-
[data_entry.md §Open prose grows](./data_entry.md)
|
|
157
|
+
[data_entry.md §Open prose grows](./data_entry.md). **`link`** marks a URL value with
|
|
158
|
+
the navigation ink as a TREATMENT on the same single element — never a swapped-in
|
|
159
|
+
`TextLink`, which would reintroduce the sub-pixel jump this field stays one
|
|
160
|
+
`<input>` to avoid; pressing still edits, so pair it with an `Open` `InlineButton`
|
|
161
|
+
in `actions`),
|
|
158
162
|
`InlineNumberInput`, `InlineSelect` (single or
|
|
159
|
-
`multi`), `InlineMemberSelect`, `InlineDatePicker`, `InlineTimePicker
|
|
163
|
+
`multi`), `InlineMemberSelect`, `InlineDatePicker`, `InlineTimePicker`,
|
|
164
|
+
**`InlineMarkdown`** (markdown RENDERED at rest, raw source while editing — the
|
|
165
|
+
member that swaps because its two states show different strings; its editor never
|
|
166
|
+
opens shorter than the view it replaced, so pressing a long note cannot collapse
|
|
167
|
+
the box, and **`minLines`** sets the floor for a short one); a
|
|
160
168
|
READ-ONLY field in that same column uses `InlineStatic` (matches the editor box exactly, no
|
|
161
169
|
input chrome, so it aligns pixel-for-pixel). A stack of labelled field rows lives in
|
|
162
170
|
`DetailTable` + `DetailRow`; the record's identity band is `RecordSummary`; its money
|
|
@@ -281,8 +289,6 @@ to drill).
|
|
|
281
289
|
gap-spaced region + `SectionHeading`/`SectionHeadingTitle`/`SectionHeadingMeta`; no body
|
|
282
290
|
component — children are the body), `Subsection` (+ `SubsectionHeading`/
|
|
283
291
|
`SubsectionHeadingTitle` — the named group INSIDE a section, `###` lg-semibold title),
|
|
284
|
-
`DialogSectionHeadingTitle` (`####` md-semibold — the rung for a surface inside a DIALOG, whose
|
|
285
|
-
`DialogHeaderTitle` already occupies lg; composes with the Subsection layout),
|
|
286
292
|
`SectionStack` (the flat page's content column — owns the fixed 56px beat + hairline between
|
|
287
293
|
top-level blocks), `SubsectionStack` (a section's `Subsection` groups — fixed 32px beat,
|
|
288
294
|
space-only while the groups are SHORT, `divided` once they run long), `SectionCard`, `PageHeader` /
|
|
@@ -661,12 +667,18 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
661
667
|
`warning`/`error` render as an ARIA `alert`) — wrapping form fields in an alert is wrong.
|
|
662
668
|
The law: **content + fields → `Inset`; a message → `Callout`.**
|
|
663
669
|
- **`section_heading`** — `Section`, `SectionHeading`, `SectionHeadingTitle`,
|
|
664
|
-
`SectionHeadingMeta`, `Subsection`, `SubsectionHeading`, `SubsectionHeadingTitle
|
|
665
|
-
`DialogSectionHeadingTitle` — the
|
|
670
|
+
`SectionHeadingMeta`, `Subsection`, `SubsectionHeading`, `SubsectionHeadingTitle` — the
|
|
666
671
|
card-less twin of the Card family, compound, owns no margin; spacing via the Section gap
|
|
667
|
-
(16, fixed), no body component. `SectionHeadingTitle` is
|
|
668
|
-
|
|
669
|
-
|
|
672
|
+
(16, fixed), no body component. `SectionHeadingTitle` is the SECTION rung + `info` for an ⓘ
|
|
673
|
+
provenance popover after the title, same as `CardHeaderTitle.info` (`weight="medium"` is the
|
|
674
|
+
one opt-down). **Which rung it renders is the SURFACE's answer, not a prop:** `##` xl-semibold
|
|
675
|
+
on a page, `####` md-semibold inside an overlay's content region (`DrawerScrollArea` /
|
|
676
|
+
`DialogScrollArea` / `ModalBody` / `PopoverContent`), where the container's own title already
|
|
677
|
+
occupies lg. The call site is identical on both, which is the whole point — there is no
|
|
678
|
+
per-surface variant to pick, so it cannot be picked wrong. A whole PAGE surface docked in an
|
|
679
|
+
overlay (a record screen in a master-detail drawer) carries its own gutters and so sits in the
|
|
680
|
+
container's BARE slot, keeping the page rungs; nesting overlays never compounds.
|
|
681
|
+
`SubsectionHeadingTitle` is the `###` lg-semibold level-3
|
|
670
682
|
title of a named group inside a section (same `info` ⓘ AND `description` slots as the section
|
|
671
683
|
title) — heading-row siblings ride its right edge. Its `description` is what a files
|
|
672
684
|
SUBSECTION uses to name its drag/paste paths: a `FileDropTarget`-wrapped region shows no
|
|
@@ -674,11 +686,20 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
674
686
|
target) carries that line at THIS rung, not the section above it. **A section's ADD is one of those siblings** — a
|
|
675
687
|
`primary` `Button` beside the title, rendered whether the collection is empty or full, never
|
|
676
688
|
under the rows it extends and never repeated in the `EmptyState`
|
|
677
|
-
(composition.md § The add-placement law).
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
689
|
+
(composition.md § The add-placement law). An overlay surface loses only the type size, never an
|
|
690
|
+
affordance — `icon`, `description` and `info` are the same slots at both altitudes. **`description`
|
|
691
|
+
is on every rung** — the ramp's slots do not vary by level; only the type size does. Inside an
|
|
692
|
+
overlay `####` is the ramp's FLOOR: `SubsectionHeadingTitle` has one altitude and would out-rank
|
|
693
|
+
the section holding it, so a named group there is `Text size="sm" weight="semibold"`. The heading
|
|
694
|
+
ramp is FIXED: `#` xxl / `##` xl / `###` lg / `####` md, no size props.
|
|
695
|
+
- **`heading_altitude`** — `HeadingAltitudeContext`, `useHeadingAltitude`, `HeadingAltitude`
|
|
696
|
+
(`"page" | "panel"`): WHICH SURFACE a heading is standing on, which is what selects the section
|
|
697
|
+
rung. The kit's content regions publish it, so an ordinary screen never imports this. Reach for
|
|
698
|
+
it when you build your OWN pane — a docked side panel, a split view, any self-contained region
|
|
699
|
+
whose own chrome caps at `lg` and which therefore uses none of those regions: wrap its content
|
|
700
|
+
in `<HeadingAltitudeContext.Provider value="panel">` and every heading inside is right. Scoping
|
|
701
|
+
the rule to the panes the KIT renders would make it an allowlist short by however many you
|
|
702
|
+
build. Flat, never a depth counter — a popover inside a drawer is one rung down, not two.
|
|
682
703
|
- **`section_stack`** — `SectionStack`, `SubsectionStack` — stacks that own the
|
|
683
704
|
between-block law, skipping null children: `SectionStack` = a fixed 56px beat + a hairline
|
|
684
705
|
`Divider` between top-level blocks; `SubsectionStack` = a fixed 32px beat, space-only while
|
package/docs/composition.md
CHANGED
|
@@ -45,8 +45,8 @@ subject into rows.
|
|
|
45
45
|
and never hand-roll one: the three surfaces disagree on the number (24 / responsive / 20) and
|
|
46
46
|
only the surface knows which it is.
|
|
47
47
|
- **Never wrap a scroller in a padded box.** The box insets the scroller's VIEWPORT, so the list
|
|
48
|
-
ends short of the surface with dead space beneath it and its last row clipped
|
|
49
|
-
|
|
48
|
+
ends short of the surface with dead space beneath it and its last row clipped. A pinned
|
|
49
|
+
heading above a scrolling list therefore takes the
|
|
50
50
|
overlay's BARE slot and puts the gutter on the pinned part and on the scroller's
|
|
51
51
|
`contentContainerStyle`. Same for a full-bleed band, which wants no gutter at all.
|
|
52
52
|
- **Content column**: one centered column — `{ width: "100%", maxWidth, alignSelf: "center" }` —
|
|
@@ -88,10 +88,38 @@ restyle a heading level per-page.
|
|
|
88
88
|
| Rank | `Text` size | Construct |
|
|
89
89
|
|---|---|---|
|
|
90
90
|
| `#` | `xxl` semibold | Page title — `PageHeader` / `RecordSummary` title |
|
|
91
|
-
| `##` | `xl` semibold | Section title — `SectionHeadingTitle
|
|
91
|
+
| `##` | `xl` semibold | Section title on a PAGE — `SectionHeadingTitle` |
|
|
92
92
|
| `###` | `lg` semibold | Subsection title — `SubsectionHeadingTitle`, always |
|
|
93
|
+
| `####` | `md` semibold | Section title in a PANE — the SAME `SectionHeadingTitle` |
|
|
93
94
|
| body | `sm` | Everything else |
|
|
94
95
|
|
|
96
|
+
**Which rung a section title takes is the SURFACE's answer, never the call site's.** A `Drawer`,
|
|
97
|
+
`Dialog` or `Modal` caps its own chrome title at `lg`, so a `##` inside one renders 24px over a
|
|
98
|
+
20px bar — a heading shouting over the pane that holds it. The CONTAINER publishes the altitude
|
|
99
|
+
and one `SectionHeadingTitle` answers to it: `##` on a page, `####` inside `DrawerScrollArea` /
|
|
100
|
+
`DialogScrollArea` / `ModalBody` / `PopoverContent`. Write the same heading either way; there is
|
|
101
|
+
no per-surface variant to remember, which is the point — a caller who has to know what is holding
|
|
102
|
+
their heading will sooner or later be wrong about it.
|
|
103
|
+
|
|
104
|
+
Three consequences worth knowing before you compose:
|
|
105
|
+
|
|
106
|
+
- **A whole PAGE surface docked in an overlay keeps the page rung.** The master-detail register
|
|
107
|
+
puts a full record screen in a drawer; that screen brings its own `#` identity band, carries
|
|
108
|
+
its own gutters, and therefore sits in the container's BARE slot rather than the scroll area —
|
|
109
|
+
which is exactly why the altitude is published by the scroll area. Its sections stay `##` and
|
|
110
|
+
its subsections stay `###`. Nothing to pass; put an ordinary pane body in the scroll area and
|
|
111
|
+
a page surface in the bare slot, as the gutter contract already asks.
|
|
112
|
+
- **A pane you build YOURSELF declares its own altitude.** A docked side panel is not an overlay
|
|
113
|
+
and uses none of those regions, but its headings must not shout across 400px either — wrap its
|
|
114
|
+
content in `<HeadingAltitudeContext.Provider value="panel">` (`@lotics/ui/heading_altitude`).
|
|
115
|
+
A rule scoped to the containers the kit happens to render is an allowlist short by however many
|
|
116
|
+
panes the app builds.
|
|
117
|
+
- **In a pane, `####` is the FLOOR.** `SubsectionHeadingTitle` has one altitude (`###` lg) and
|
|
118
|
+
would out-rank the section holding it, so a named group inside a pane's section is
|
|
119
|
+
`Text size="sm" weight="semibold"` — a pane is one surface, not a document.
|
|
120
|
+
|
|
121
|
+
Nesting never compounds: a popover opened inside a drawer is one rung down, not two.
|
|
122
|
+
|
|
95
123
|
- **Page band** — the screen's TYPE ANCHOR, and only secondarily a label. A register's own type
|
|
96
124
|
runs 12–16px, so a page without a band tops out around a **1.33x** range between its largest and
|
|
97
125
|
smallest text, and that reads flat no matter how much colour is on it — scale is the one
|
|
@@ -128,9 +156,8 @@ restyle a heading level per-page.
|
|
|
128
156
|
below. **The row is `flexDirection: "row"` and never wraps**, so a long control label doesn't
|
|
129
157
|
drop to its own line at phone width — it holds its own width and the title's `flex: 1` gives
|
|
130
158
|
up whatever is left, taking the `description` down with it. A label that repeats the heading
|
|
131
|
-
verbatim is the usual way this gets long enough to bite:
|
|
132
|
-
|
|
133
|
-
duplicated onto the button squeezed both title and description into a 107px ribbon wrapping
|
|
159
|
+
verbatim is the usual way this gets long enough to bite: duplicating the title onto the button
|
|
160
|
+
squeezes both title and description into a narrow ribbon wrapping
|
|
134
161
|
one or two words per line for eleven lines total. The button-label law already asks for
|
|
135
162
|
2–4 words; a heading-row control is the case where skipping it has a measurable width cost,
|
|
136
163
|
not just a wordier button. A COUNT earns that slot only when what it counts is not fully on screen — collapsed,
|
|
@@ -144,14 +171,14 @@ restyle a heading level per-page.
|
|
|
144
171
|
slots carry each section's count and CTA); a `Text size="sm" weight="semibold"` stand-in is only
|
|
145
172
|
for small sub-groups. Do NOT wrap each section in its own `CardHeader`, do NOT hand-roll
|
|
146
173
|
sm-semibold lead lines on a flat page, and NEVER use a bare eyebrow as a section title.
|
|
147
|
-
- **
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
`DialogHeaderTitle`
|
|
151
|
-
a multi-section surface otherwise ties with, or is outranked by,
|
|
152
|
-
with the SUBSECTION layout (`Subsection` › `SubsectionHeading` ›
|
|
153
|
-
|
|
154
|
-
stays ABSOLUTE — this is one more fixed
|
|
174
|
+
- **Pane section title** — the FOURTH rung, taken automatically by `SectionHeadingTitle`
|
|
175
|
+
inside a pane's content region. Same construct, same `icon`/`description`/`info` slots;
|
|
176
|
+
only the type size steps down, and nothing at the call site says so. It exists because
|
|
177
|
+
`DialogHeaderTitle` and a `Drawer`'s title are **lg** — the rung `SubsectionHeadingTitle`
|
|
178
|
+
occupies — so a panel holding a multi-section surface otherwise ties with, or is outranked by,
|
|
179
|
+
its own content. Compose it with the SUBSECTION layout (`Subsection` › `SubsectionHeading` ›
|
|
180
|
+
this, in a `SubsectionStack`): the 12px body gap and 32px beat are already right for a panel.
|
|
181
|
+
The ladder stays ABSOLUTE — this is one more fixed rung, never a `size`/`compact` knob.
|
|
155
182
|
- **Subsection title** — the level BELOW a section on a long record surface: `Subsection` ›
|
|
156
183
|
`SubsectionHeading` › `SubsectionHeadingTitle` (`###` — lg semibold, with the same `info` ⓘ
|
|
157
184
|
and `description` slots as `SectionHeadingTitle`; siblings — a `Badge`, a
|
|
@@ -221,7 +248,7 @@ nought or forty.
|
|
|
221
248
|
**When the row runs out of width, the VERB wraps — the title never gives way.** The title names
|
|
222
249
|
the section; the verb is the thing that can move. Getting this backwards is easy to ship because
|
|
223
250
|
RN-Web resolves `flex: 1` to `flex-basis: 0%` with `min-width: 0`, so a title beside a button that
|
|
224
|
-
holds its intrinsic width is the half that collapses
|
|
251
|
+
holds its intrinsic width is the half that collapses, and at phone width the heading breaks one
|
|
225
252
|
word per line beside an untouched button. `SectionHeading` and `SubsectionHeading` wrap and floor
|
|
226
253
|
their title column for this; do not add a second `flex: 1` spacer beside a title that already has
|
|
227
254
|
one, which halves its share again and is how a badge came to land on top of a section title.
|
|
@@ -490,14 +517,38 @@ below the pressed element — the composable `Accordion` family (`AccordionHeade
|
|
|
490
517
|
rhythm, so lists mix expandable + static rows. The body is **flush** with the header's left edge
|
|
491
518
|
(aligns with the title — like a `Section` body, no indent, no tinted well). `AccordionHeader` is a
|
|
492
519
|
layout slot: `AccordionTitle`/`AccordionMeta` is the compact list-row heading, but a **collapsible
|
|
493
|
-
SECTION** is this same family with the heading rung composed inside it — `SectionHeadingTitle`
|
|
494
|
-
`SubsectionHeadingTitle`
|
|
495
|
-
`
|
|
520
|
+
SECTION** is this same family with the heading rung composed inside it — `SectionHeadingTitle` or
|
|
521
|
+
`SubsectionHeadingTitle` (+ `SectionHeadingMeta`) in the `AccordionHeader`, the body in
|
|
522
|
+
`AccordionContent`; inside a pane the section title steps to `####` on its own, so a
|
|
523
|
+
collapsible section composes identically on both surfaces. There is no separate collapsible-section
|
|
496
524
|
component: one heading family and one disclosure across static + collapsible. The disclosure
|
|
497
525
|
header IS the section heading — name it for what it hides, never a generic bucket ("Advanced",
|
|
498
526
|
"More", "Options") wrapped around one named section: that stacks two titles of the same rank and
|
|
499
527
|
leaves the reader no reason to open the chevron.
|
|
500
528
|
|
|
529
|
+
### A control's resting treatment promises what pressing it does
|
|
530
|
+
|
|
531
|
+
Both halves of that fail in practice, and they fail in opposite directions.
|
|
532
|
+
|
|
533
|
+
**A control that looks like text.** A press target whose resting state is a muted caption is
|
|
534
|
+
announced to a screen reader, revealed on hover, and invisible to everyone else — including the
|
|
535
|
+
reader who navigates by eye, which is most of them. Hover is not an affordance; it is a
|
|
536
|
+
confirmation of one. If pressing it navigates, it wears navigation ink at rest: default colour,
|
|
537
|
+
`medium`, underlined.
|
|
538
|
+
|
|
539
|
+
**Text that looks like a control.** The mirror mistake, and the more annoying one, because the
|
|
540
|
+
reader presses and nothing happens — or something happens that was not what the ink promised. A
|
|
541
|
+
figure that opens a PEEK is not navigating and must not wear the ink of something that leaves;
|
|
542
|
+
identity marks and captions that go nowhere stay `muted`, regular, unruled.
|
|
543
|
+
|
|
544
|
+
So the two states of one element are treated DIFFERENTLY on purpose, and the difference is the
|
|
545
|
+
whole point. Rendering them the same is not neutrality — it is the promise being made to nobody, or
|
|
546
|
+
to everybody, and both are wrong. When a component takes an optional handler, its resting
|
|
547
|
+
appearance changes with the handler, never only its `accessibilityRole`.
|
|
548
|
+
|
|
549
|
+
The failure is invisible to a test that asserts the role, which is why it survives: the role is
|
|
550
|
+
what the element IS, and this rule is about what it LOOKS LIKE.
|
|
551
|
+
|
|
501
552
|
## No dead rows — registers & tables
|
|
502
553
|
|
|
503
554
|
**The identifier leads.** A register's FIRST column is the thing the reader came to find the row
|
|
@@ -1320,8 +1371,8 @@ necessarily wrong (which pile against what specifically), but it is a new duplic
|
|
|
1320
1371
|
|
|
1321
1372
|
## The register's own craft — what "it looks like a spreadsheet" actually is
|
|
1322
1373
|
|
|
1323
|
-
Three defects produce that report, none of them about colour, and all three
|
|
1324
|
-
|
|
1374
|
+
Three defects produce that report, none of them about colour, and all three survive a design
|
|
1375
|
+
audit. (The fourth was drawing a line between every row;
|
|
1325
1376
|
that one is settled above — a register has one line, and it is the band.)
|
|
1326
1377
|
|
|
1327
1378
|
**A column header is a LABEL at the body size** — `sm` / `muted` / `medium`, matching the values
|
|
@@ -1344,6 +1395,28 @@ which is the loudest possible version of the thing the rule exists to stop. Remo
|
|
|
1344
1395
|
without replacing the separation is what left the header looking adrift — the ink step above is
|
|
1345
1396
|
the replacement, and the two changes only work together.
|
|
1346
1397
|
|
|
1398
|
+
**A LOOKUP KEY IS NOT A COLUMN — and this is where "cramped" usually comes from.** Ask of each
|
|
1399
|
+
column whether its value is COMPARED down the page or MATCHED one at a time. A reference,
|
|
1400
|
+
identifier or code that a reader arrives already holding — quoted on the phone, printed on a
|
|
1401
|
+
document, pasted from an email — is matched, and the question it answers is "which record is this
|
|
1402
|
+
one", which the search field answers exactly and without scanning. A column answers it by spending
|
|
1403
|
+
its width on every row forever, and takes that width from the columns that ARE compared. Measured
|
|
1404
|
+
on a register whose reference columns held 350px and truncated nothing while the two columns
|
|
1405
|
+
carrying the facts a reader scans lost 224px and 84px of real values: cutting the references
|
|
1406
|
+
halved the truncation and returned the width to where the eye actually goes. Put them in the
|
|
1407
|
+
search haystack, on the record, and in the row's expanded detail if they must be visible — never
|
|
1408
|
+
in a column, unless the value is genuinely compared (a sequence a reader reads for gaps, a code
|
|
1409
|
+
whose PREFIX groups the rows).
|
|
1410
|
+
|
|
1411
|
+
**`Table` does not scroll horizontally, and that is the feature.** When the columns do not fit, it
|
|
1412
|
+
sheds by `priority` and then stacks — it never offers a pan. A horizontal scroller is a WIDTH fix
|
|
1413
|
+
for a COUNT problem: it makes room for one more column without ever asking whether the column
|
|
1414
|
+
earns a row's width, and the columns it hides off-screen are hidden from the author too, so the
|
|
1415
|
+
register quietly grows past what anyone reads. The narrowing IS the question "which of these does
|
|
1416
|
+
the reader compare", asked at the only time it gets answered. A dataset that genuinely wants pan —
|
|
1417
|
+
every column compared against its neighbours — is a different shape: a quantity across two
|
|
1418
|
+
dimensions is `Matrix`, a managed grid is `DataGrid`.
|
|
1419
|
+
|
|
1347
1420
|
**Annotate every droppable column, or none.** `priority` decides what a narrow container sheds
|
|
1348
1421
|
(lowest number is last to go, and column 0 — the identity — never drops). A PARTIAL annotation is
|
|
1349
1422
|
the trap: an unannotated column defaults above the whole hand-written range, so one annotated
|
|
@@ -1378,9 +1451,9 @@ pair on the screen and divide. A spread in the RATIOS is the finding, not a spre
|
|
|
1378
1451
|
**Every SUBJECT on one row starts at the same rung — a register's is `sm`, and a value in any
|
|
1379
1452
|
other column is `sm` too.** "One rung below" is only half a rule: it constrains the STEP and says
|
|
1380
1453
|
nothing about where the pair starts, so a subject placed a rung high stays compliant while dragging
|
|
1381
|
-
its whole column above the rest.
|
|
1382
|
-
|
|
1383
|
-
|
|
1454
|
+
its whole column above the rest. Two columns can each be correctly "one rung apart" and still be a
|
|
1455
|
+
whole rung out of step with each other — and then the higher one shouts while everything else
|
|
1456
|
+
recedes.
|
|
1384
1457
|
|
|
1385
1458
|
**The subject leads by WEIGHT and by its identity mark, not by size.** `medium` against `regular`,
|
|
1386
1459
|
plus the avatar or tile that answers "which row is this", is enough. Money is the case that most
|
|
@@ -1397,9 +1470,8 @@ the size step is a second, and a second is only worth its cost where nothing els
|
|
|
1397
1470
|
body size. This matters most where that line is the DISAMBIGUATOR (which person, at which
|
|
1398
1471
|
company): `xs` demotes content the reader needs, and 12px is where stacked diacritics crowd.
|
|
1399
1472
|
- Both lines are regular weight and colour is the only thing between them — the supporting line
|
|
1400
|
-
drops to **`xs`**.
|
|
1401
|
-
|
|
1402
|
-
pair.
|
|
1473
|
+
drops to **`xs`**. Without the step the two stop reading as a value and its annotation and start
|
|
1474
|
+
reading as two peers.
|
|
1403
1475
|
|
|
1404
1476
|
Deciding from the CONTENT instead — prose gets `sm`, reference data gets `xs` — fails on a line
|
|
1405
1477
|
that holds both. An account name beside a contract number is one line, and it takes ONE rung: the
|
|
@@ -1415,8 +1487,7 @@ is the smallest rung's own rule extended up the ladder rather than a second opin
|
|
|
1415
1487
|
Set it on BOTH lines where both are body rungs; where the supporting line is `xs` it is already
|
|
1416
1488
|
tight, so only the subject carries the prop. Then the stack's own `gap` goes to **0** — each tight
|
|
1417
1489
|
line box already contributes 3px above and below, giving 6px between the two, and a `gap` on top
|
|
1418
|
-
is a third source of space between lines that are meant to read as one.
|
|
1419
|
-
identity stack sits at exactly the mark's 40, and every pair on the screen carries the same 6px.
|
|
1490
|
+
is a third source of space between lines that are meant to read as one.
|
|
1420
1491
|
|
|
1421
1492
|
Running text keeps prose leading. The default is for paragraphs, and `.ui-markdown` renders at it.
|
|
1422
1493
|
|
|
@@ -1424,8 +1495,7 @@ Running text keeps prose leading. The default is for paragraphs, and `.ui-markdo
|
|
|
1424
1495
|
sits beside — a 24px `CopyButton` on an 18px `xs` line — so a flex row containing both is sized by
|
|
1425
1496
|
the CONTROL, and `alignItems: "center"` then spends the difference above the text. The line's text
|
|
1426
1497
|
starts lower, and the pair reads with a wider gap than the identical pair one column over that
|
|
1427
|
-
happens to carry no control
|
|
1428
|
-
figure beside it, from one `gap: 2` shared by both.
|
|
1498
|
+
happens to carry no control — one shared `gap` reading as two different ones.
|
|
1429
1499
|
|
|
1430
1500
|
It is invisible in review because every number is defensible — the gap is right, the rungs are
|
|
1431
1501
|
right, the control is the documented size — and the defect is the interaction between them. It is
|
|
@@ -1528,10 +1598,10 @@ colour cannot substitute for. A register with no page band tops out near 1.33×.
|
|
|
1528
1598
|
|
|
1529
1599
|
## Where the accent goes — "where you are", and nothing else
|
|
1530
1600
|
|
|
1531
|
-
`colors.accent` paints the
|
|
1532
|
-
|
|
1533
|
-
valence all come from a palette family, so an accent can never overwrite a meaning —
|
|
1534
|
-
paints the focus ring.
|
|
1601
|
+
`colors.accent` paints an active FILTER — the one place a brand hue answers "where the reader is"
|
|
1602
|
+
without also answering "what is this control doing". It never paints DATA — a status, a series and
|
|
1603
|
+
a valence all come from a palette family, so an accent can never overwrite a meaning — it never
|
|
1604
|
+
paints the focus ring, and it never paints a SELECTION (below).
|
|
1535
1605
|
|
|
1536
1606
|
**A page-scale position marker takes `primary`, not `accent`** — an active tab's underline is the
|
|
1537
1607
|
case. `primary` is already the ink the kit spends on "this is on": a checkbox's fill, a switch's
|
|
@@ -1544,14 +1614,27 @@ nobody chose (see § Color discipline), three pixels from a near-black button.
|
|
|
1544
1614
|
The line between the two: `accent` marks WHICH OF THESE (a row among rows, a filter among
|
|
1545
1615
|
filters), `primary` marks THIS IS ON (a control's state, the section you are in).
|
|
1546
1616
|
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1617
|
+
**SELECTION IS NEVER THE BRAND — it is a neutral GROUND on a row, and a dark EDGE on a control.**
|
|
1618
|
+
A register row wears `zinc-100`; a chip and a card wear a `zinc-900` border doubled by a boxShadow,
|
|
1619
|
+
keeping the ground every other chip and card has. The two altitudes deliberately differ: a ground
|
|
1620
|
+
is the row's own language, so a control that borrows it says the same thing twice, and a control
|
|
1621
|
+
has a border to say it with while a row does not.
|
|
1622
|
+
|
|
1623
|
+
The reason to hold that line is that it was crossed twice, in both directions, and each crossing
|
|
1624
|
+
looked reasonable. Painting selections through `accent_wash` shipped a blue nobody had chosen onto
|
|
1625
|
+
the busiest surface in the product; making the token neutral to fix that turned a themed app's drop
|
|
1626
|
+
target grey and left a token named for the brand that resolved to zinc. Selections carry their own
|
|
1627
|
+
LITERALS now, so "is this selected" never depends on whether a tenant set an accent, and
|
|
1628
|
+
`accent_wash` goes back to meaning what it says: the brand's tint, spent on ATTENTION — a region
|
|
1629
|
+
lighting up under a dragged file — never on which record is open.
|
|
1630
|
+
|
|
1631
|
+
**Selection and hover must not share a ground.** They did, and the register's one piece of
|
|
1552
1632
|
persistent state was erased by a transient one: the row whose record is open looked exactly like
|
|
1553
|
-
the row under the pointer.
|
|
1554
|
-
|
|
1633
|
+
the row under the pointer. One neutral step separates them — **hover zinc-50, selected zinc-100,
|
|
1634
|
+
pressed zinc-200**, three rungs of the ramp every other pressable already uses to say the same
|
|
1635
|
+
three things. A control marked by an EDGE sidesteps the collision entirely, which is the other
|
|
1636
|
+
reason chips are marked that way: the edge survives the hover wash instead of being replaced by
|
|
1637
|
+
it.
|
|
1555
1638
|
|
|
1556
1639
|
### Theming is an app-level ESCAPE HATCH, not a product surface
|
|
1557
1640
|
|
|
@@ -1666,7 +1749,7 @@ happens to render 133 times is one badge.
|
|
|
1666
1749
|
|
|
1667
1750
|
Prefer `dot` when the column is secondary or the row already carries a coloured mark. Reach for
|
|
1668
1751
|
`tonal` when the status column is what the register is SCANNED by — the tints are the palette's
|
|
1669
|
-
50-step against its 900-step ink
|
|
1752
|
+
50-step against its 900-step ink, comfortably clear of the AA floor, and
|
|
1670
1753
|
a column of them is the difference between a register that reads as organised and one that reads
|
|
1671
1754
|
as a grey spreadsheet. That difference is the commonest "it looks bland" report, and it has a
|
|
1672
1755
|
real answer that costs no meaning: make the colour the screen ALREADY earns more present, rather
|
|
@@ -1681,6 +1764,24 @@ text. Pass it at the call site.
|
|
|
1681
1764
|
|
|
1682
1765
|
A `Badge` is never a metric value.
|
|
1683
1766
|
|
|
1767
|
+
### A fact band is read at a GLANCE, and a glance is WIDE
|
|
1768
|
+
|
|
1769
|
+
A record's header facts — owner, direction, mode, terms — are a band a reader takes in at once,
|
|
1770
|
+
not a form they fill down. Stacked one per row at the page's own rhythm, a handful of one-word
|
|
1771
|
+
values can spend a quarter of the first screen while the viewport's right half sits empty. Pair
|
|
1772
|
+
them into columns above the width where a label and its value still fit.
|
|
1773
|
+
|
|
1774
|
+
The tell is a screen reported as **plain** whose treatment is fine. Then the treatment was never
|
|
1775
|
+
the problem, and another pass at it will not help: the page is spending its height on facts and
|
|
1776
|
+
its width on nothing.
|
|
1777
|
+
|
|
1778
|
+
**A record page also owes the reader the SIZE of its subject.** Where the data is quantities — how
|
|
1779
|
+
many lines, how heavy, worth what — a page that holds them only inside sections makes a reader open
|
|
1780
|
+
two of them to learn how big the thing in front of them is. Sum what the page has already loaded
|
|
1781
|
+
into one band under the identity, and print no total that is not the sum of something visible
|
|
1782
|
+
further down. Drop an empty total rather than rendering a zero: "0 kg" asserts a measurement nobody
|
|
1783
|
+
took.
|
|
1784
|
+
|
|
1684
1785
|
## Typography
|
|
1685
1786
|
|
|
1686
1787
|
**A `size` rung scales the WHOLE component, not just its figure.** A composite with a graphic and
|
package/docs/data_entry.md
CHANGED
|
@@ -131,6 +131,17 @@ FORMATTED and the editor shows something else (`1,250 kg` → `1250`, `31/07/202
|
|
|
131
131
|
field), the string changes anyway, so those editors do swap and should. **The rule for a new
|
|
132
132
|
editor: same string in both states ⇒ one element; different string ⇒ swap.**
|
|
133
133
|
|
|
134
|
+
Two worked cases sit on either side of it, and they look alike until you apply the rule.
|
|
135
|
+
**`InlineMarkdown` swaps**: `**bold**` and **bold** are different strings, so the quarter pixel is
|
|
136
|
+
invisible and rendering prose at rest is worth having. A swap editor is supposed to leave the box
|
|
137
|
+
where it was, which markdown cannot do exactly — so it does the half that matters and refuses to
|
|
138
|
+
COLLAPSE: the resting height becomes the editor's floor, because growing is tolerable where
|
|
139
|
+
shrinking pulls the text out from under the reader and jumps everything below it.
|
|
140
|
+
**A LINK does not swap**: marking a URL changes its ink, not its characters, so `InlineTextInput`
|
|
141
|
+
takes a **`link`** treatment on its single `<input>` rather than swapping in a `TextLink`. Pressing
|
|
142
|
+
the field still edits — an `Open` `InlineButton` in `actions` is how you reach the destination,
|
|
143
|
+
which is where a verb about the value belongs.
|
|
144
|
+
|
|
134
145
|
**Commit-on-blur would race the press that caused it — the kit closes that race for you.**
|
|
135
146
|
Pressing an action while an inline field is focused starts TWO things in ONE gesture: the
|
|
136
147
|
field's write (the blur, on mousedown) and the button's handler (on mouseup). A handler that
|
|
@@ -220,9 +231,10 @@ One per type:
|
|
|
220
231
|
field.
|
|
221
232
|
|
|
222
233
|
They split across two shared shells. The same-height **typed** editors (`InlineTextInput`,
|
|
223
|
-
`InlineNumberInput`) render **`InlineEditFrame`** driven by the
|
|
224
|
-
**`useInlineEdit`** hook (the draft buffer, async save, and — for the
|
|
225
|
-
toggle) — pass a custom input to that pair to join the family.
|
|
234
|
+
`InlineNumberInput`, `InlineMarkdown`) render **`InlineEditFrame`** driven by the
|
|
235
|
+
**`useInlineEdit`** hook (the draft buffer, async save, and — for the ones that swap — the view⇄edit
|
|
236
|
+
toggle) — pass a custom input to that pair to join the family. The frame's `display` takes a NODE as
|
|
237
|
+
well as a string, which is what lets a swapping editor rest as something other than text. On the frame, **`editing` means an
|
|
226
238
|
input is MOUNTED** (which element renders, and whether the surface drops its resting padding to
|
|
227
239
|
host one); an always-mounted editor passes it constant and passes the real state as **`editOpen`**,
|
|
228
240
|
which the ✓/✕ verbs read — they assert "there is an edit to accept or reject", so on a resting
|
|
@@ -263,9 +275,8 @@ verified on a wide screen is not verified.
|
|
|
263
275
|
`autoGrow` makes the budget a minimum and fits the value. It costs nothing the fixed reserve was
|
|
264
276
|
protecting, and the reason is worth stating because it is easy to assume otherwise: an
|
|
265
277
|
`InlineTextInput` is ONE input in both states, never a resting view swapped for an editor, so a
|
|
266
|
-
grown box is the same height focused as at rest
|
|
267
|
-
|
|
268
|
-
impossible. Growing on a control that DOES swap (number, date, time — their resting display is
|
|
278
|
+
grown box is the same height focused as at rest. The reserve was guarding against a jump the
|
|
279
|
+
one-element design had already made impossible. Growing on a control that DOES swap (number, date, time — their resting display is
|
|
269
280
|
formatted) would reintroduce it, which is why the prop lives here and not on those.
|
|
270
281
|
|
|
271
282
|
Two places to keep the reserve anyway: a value that can be **arbitrarily long** (growth has no
|