@lotics/ui 23.1.1 → 24.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +13 -1
- package/MIGRATION.md +37 -0
- package/docs/catalog.md +69 -14
- package/docs/composition.md +63 -3
- package/docs/data_entry.md +19 -1
- package/docs/templates.md +24 -28
- package/examples/tpl_item_list.tsx +19 -768
- package/examples/tpl_record.tsx +515 -415
- package/examples/tpl_task_board.tsx +2 -2
- package/package.json +5 -2
- package/src/breakdown.tsx +3 -1
- package/src/choice_list.tsx +3 -1
- package/src/date_calendar.tsx +3 -1
- package/src/deadline.ts +157 -0
- package/src/filter_chip.tsx +2 -2
- package/src/funnel.tsx +3 -1
- package/src/heatmap.tsx +3 -1
- package/src/locale.tsx +16 -0
- package/src/matrix.tsx +3 -1
- package/src/option_list.tsx +3 -3
- package/src/press_door.tsx +12 -1
- package/src/reference_field.tsx +174 -0
- package/src/status_grid.tsx +3 -1
- package/src/table.tsx +56 -4
- package/src/text_button.tsx +139 -0
- package/src/text_link.tsx +24 -14
package/AGENTS.md
CHANGED
|
@@ -17,7 +17,7 @@ CURRENT major only — upgrading an app across majors is `MIGRATION.md`.
|
|
|
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` for a custom-code app's `vite.config.ts`). Read before building any screen; reuse first. |
|
|
18
18
|
| [docs/data_entry.md](./docs/data_entry.md) | Which editing pattern for which job — inline edit, fieldset forms, browser-autofill suppression (search controls only), find-or-create (`Combobox`), line items, handoffs, phased records, billing, tags, dispositions, attachments (the `FilesEditor` COMPOUND — 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
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 (modify → review-before-apply; create → 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) |
|
|
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, master-detail `Drawer
|
|
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, master-detail `Drawer` on a LIST screen vs a child collection's row EXPANDING inside a record, view controls, RECORD EXTENT (one page, sections scrolled to and never routed to), color discipline, typography, whitespace, and how to TEST an overlay component (a `Popover`-backed surface never mounts under jsdom). |
|
|
21
21
|
| [docs/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). |
|
|
22
22
|
|
|
23
23
|
## Iron rules
|
|
@@ -39,6 +39,13 @@ CURRENT major only — upgrading an app across majors is `MIGRATION.md`.
|
|
|
39
39
|
never a second chip. A chip beside a name reads as its PEER (a colored one reads louder),
|
|
40
40
|
inverting the hierarchy the row is scanned by. →
|
|
41
41
|
[composition.md §`Badge` is for STATUS only](./docs/composition.md).
|
|
42
|
+
- **A countdown ANNOTATES its date field — never a trailing badge, and never hand-rolled.**
|
|
43
|
+
How far off a date is, what to call that number, and how loud it reads are three CONTRACTS:
|
|
44
|
+
invent them per screen and two registers disagree about when "urgent" starts and phrase "2
|
|
45
|
+
days late" four ways. Reach for `@lotics/ui/deadline` and spread `deadlineAnnotation` onto the
|
|
46
|
+
`DetailRow` — the three annotation slots carry the three urgency levels exactly. A binary
|
|
47
|
+
`error={overdue ? … }` is the tell: it makes a date due tomorrow read like one due in three
|
|
48
|
+
months. → [catalog.md §Dates & times](./docs/catalog.md).
|
|
42
49
|
- **The kit's fonts/colors/icons ARE the design system** — never a custom font, icon set, or
|
|
43
50
|
hand-picked palette shade; color is `solid`/`tint`/`ramp` with ONE accent per screen.
|
|
44
51
|
- **Hand-typed type is off-system — and it always lands too small.** Every run of language is
|
|
@@ -54,6 +61,11 @@ CURRENT major only — upgrading an app across majors is `MIGRATION.md`.
|
|
|
54
61
|
(a CTA, ⋯, a checkbox, a `Link`) is a role-less `PressableRow` + a **`PressDoor`** sibling
|
|
55
62
|
(the tab stop, name, focus ring), never `PressableHighlight`, which wraps its children in the
|
|
56
63
|
button. `Table`/`TableRow` already does this. → [composition.md](./docs/composition.md).
|
|
64
|
+
- **A fact every row needs is a COLUMN; a fact one row needs expands in place.** Sending the
|
|
65
|
+
reader to a drawer for either loses the surrounding rows and charges a navigation to come
|
|
66
|
+
back — which is what makes scanning twenty records unaffordable. `TableRow`'s `detail` +
|
|
67
|
+
`expanded` reveal the detail beneath the row; keep the drawer for a heavy form.
|
|
68
|
+
→ [catalog.md](./docs/catalog.md).
|
|
57
69
|
- **A reference to another record is a FIELD** — it wears the inline editor's own resting
|
|
58
70
|
surface, carries an **`InlineButton`** Open inside it, and PEEKS its facts on press (detach
|
|
59
71
|
lives in the peek). Never its own Section, never a card, never the other record's fields as
|
package/MIGRATION.md
CHANGED
|
@@ -4,6 +4,43 @@ 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
|
+
## 24.0.0 — `TextLink` navigates, `TextButton` acts
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
<TextLink onPress={selectAll}>Select all</TextLink> // BEFORE
|
|
11
|
+
<TextButton onPress={selectAll}>Select all</TextButton> // AFTER — same look, role=button
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`TextLink` no longer takes `onPress`. It had three modes behind one name — `href` made it a link,
|
|
15
|
+
`onPress` made it `role="button"`, neither made it plain underlined text — so one export was three
|
|
16
|
+
components, and the button mode was nearly all of the use, including `OptionList` and `FilterChip`
|
|
17
|
+
inside this kit.
|
|
18
|
+
|
|
19
|
+
**Pick by what the press DOES, not by appearance.** Both are UNDERLINED — that is what marks a
|
|
20
|
+
surface-less control interactive, and it is not optional (position is a learned convention, hover is
|
|
21
|
+
absent on touch). What separates them is INK: blue means NAVIGATION, so `Link`/`TextLink` are blue
|
|
22
|
+
and go somewhere while `TextButton` is zinc and acts. They differ in `role` too, which is what a
|
|
23
|
+
screen reader announces and what keeps open-in-new-tab off a control that opens nothing.
|
|
24
|
+
|
|
25
|
+
`TextButtonColor` is `default | danger` only — deliberately not `TextColor`. A `muted` text action
|
|
26
|
+
would mute the one thing carrying the act's weight; use `Button color="muted"` when an act must be
|
|
27
|
+
that quiet. `disabled` greys to zinc-400, drops the press and the hover wash, and announces
|
|
28
|
+
`aria-disabled`. ONE size, no `icon`/`href`/`decoration`.
|
|
29
|
+
|
|
30
|
+
Migration is mechanical: every `TextLink` with `onPress` becomes `TextButton`; every `TextLink` with
|
|
31
|
+
`href` or with neither prop is unchanged. It is NOT visible — a migrated action keeps the same
|
|
32
|
+
underlined neutral ink it had. Passing `size`, `icon`, or a `TextColor` other than `danger` is now a
|
|
33
|
+
type error; drop them (`Text`'s default size is already `sm`, so the common `size="sm"` was
|
|
34
|
+
redundant), and reach for `Button` when a verb needs a glyph or a real hit target.
|
|
35
|
+
|
|
36
|
+
It also sharpens an existing rule: blue-600 means NAVIGATION specifically, not "interactive"
|
|
37
|
+
generally — the underline carries interactive, so an ACTION stays zinc.
|
|
38
|
+
|
|
39
|
+
A destructive text-weight act is `TextButton color="danger"`, which lands its ink on a text column
|
|
40
|
+
by construction and keeps a 40px target via `hitSlop`. Do NOT reach for a fill-less `Button`
|
|
41
|
+
(`danger-secondary`, `muted`) there: with no box, its optical edge is its ink, inset by its own
|
|
42
|
+
padding, so it reads as indented against the labels it should line up with.
|
|
43
|
+
|
|
7
44
|
## 23.0.0 — the `Inline*` editors are WHITE, and `variant` is renamed
|
|
8
45
|
|
|
9
46
|
```tsx
|
package/docs/catalog.md
CHANGED
|
@@ -116,21 +116,26 @@ the `Task` compound owns the row — `TaskStatus` (a `CheckCircle`), `TaskTitle`
|
|
|
116
116
|
`InlineTextInput`), `TaskActions`. **The row carries the TITLE; the fields a user can SET hang
|
|
117
117
|
under it as `TaskSubRow`s** (`InlineDatePicker`/`InlineMemberSelect`/`InlineSelect`, each with
|
|
118
118
|
its NAME beside it), never a `DetailTable` in a `TaskDetail`, which declares a grid of its own.
|
|
119
|
-
|
|
120
|
-
because a sub-row reflows instead of being authored per surface; add `SuggestionChip`
|
|
121
|
-
`CaptureRow` for the
|
|
122
|
-
compared across twenty rows — is a `Table`, not a `TaskList`.**
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
[`
|
|
119
|
+
One composition serves a standalone task surface at any density — a flat list or a grouped desk
|
|
120
|
+
board — because a sub-row reflows instead of being authored per surface; add `SuggestionChip`
|
|
121
|
+
commons + `CaptureRow` for the capture case. **A list you scan DOWN columns — the same four values
|
|
122
|
+
compared across twenty rows — is a `Table`, not a `TaskList`.**
|
|
123
|
+
|
|
124
|
+
**A RECORD never carries a checklist.** Its work state is `Pipeline`
|
|
125
|
+
([`tpl_record`](../examples/tpl_record.tsx) § Progress): a reached stage owns the facts it stamps,
|
|
126
|
+
the LIVE stage alone carries its condition and the one act that leaves it. A per-desk checklist
|
|
127
|
+
described the work but never said where the record SAT, and it put the CTA a section away from the
|
|
128
|
+
state that gated it — so `tpl_record` replaced one with the pipeline, and a register's workspace
|
|
129
|
+
drawer follows the same shape. `TaskList` is for a surface whose SUBJECT is tasks; see
|
|
130
|
+
[`tpl_task_board`](../examples/tpl_task_board.tsx).
|
|
126
131
|
|
|
127
132
|
### Tabular data — pick by SCALE + intent
|
|
128
133
|
|
|
129
134
|
Two columnar shapes, and the choice is about data size:
|
|
130
135
|
|
|
131
136
|
- **High-volume register** (thousands+ you BROWSE) — `Table` (columns defined once; sortable
|
|
132
|
-
headers via `SortHeader`; paired with `Pagination`) +
|
|
133
|
-
|
|
137
|
+
headers via `SortHeader`; paired with `Pagination`) + rows that either open a `Drawer` to
|
|
138
|
+
edit or expand in place (`detail` + `expanded`) when the detail is read or light-edit. It scales by PAGING — renders one page, never the whole set — so you FILTER +
|
|
134
139
|
search, you don't group. Worked example: [`tpl_item_list`](../examples/tpl_item_list.tsx).
|
|
135
140
|
Never an HTML `<table>` or a `.map` of rows. The register adapts to its container on its
|
|
136
141
|
own — hides columns by `priority`, stacks rows below the two-column floor (see the `table`
|
|
@@ -456,7 +461,9 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
456
461
|
### Actions, links & menus
|
|
457
462
|
|
|
458
463
|
- **`button`** — `Button`: the labelled action; `title` MANDATORY (it is the accessible
|
|
459
|
-
name), `color` = emphasis/risk.
|
|
464
|
+
name), `color` = emphasis/risk. A fill-less `Button` shows no box, so its optical
|
|
465
|
+
edge is its INK, inset by its padding — don't align one to a column of TEXT; that
|
|
466
|
+
is `TextButton`'s job.
|
|
460
467
|
- **`icon_button`** — `IconButton`: the icon-only circular action (see
|
|
461
468
|
[Actions](#actions)).
|
|
462
469
|
- **`back_button`** — `BackButton`: the one go-back control — don't hand-roll it (an
|
|
@@ -468,10 +475,23 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
468
475
|
never an icon button beside unclickable prose.
|
|
469
476
|
- **`link`** — `Link`: the EXTERNAL hyperlink — fixed underline+blue + `role="link"`;
|
|
470
477
|
`onPress` only (the consumer wires the opener).
|
|
471
|
-
- **`text_link`** — `TextLink`: underlined text that
|
|
472
|
-
|
|
473
|
-
underlined text to wrap in your own pressable
|
|
474
|
-
|
|
478
|
+
- **`text_link`** — `TextLink`: underlined text that NAVIGATES — `href` renders a real
|
|
479
|
+
`<a>` on web (middle-click / open-in-new-tab work) and takes the blue navigation ink;
|
|
480
|
+
with no `href` it stays NEUTRAL: plain underlined text to wrap in your own pressable,
|
|
481
|
+
or a MARKER that a value leads somewhere (a record reference whose press opens a peek,
|
|
482
|
+
not a trip). `color` overrides either way; inherits every `Text` prop. It does NOT
|
|
483
|
+
act — that's `TextButton`.
|
|
484
|
+
- **`text_button`** — `TextButton`: a low-chrome ACTION at text weight ("Select all",
|
|
485
|
+
"Clear", "Add stop") — the third rung of chrome after `Button` (40px surface) and
|
|
486
|
+
`InlineButton` (28px, filled, on a field's surface). **Underlined, in neutral ink**:
|
|
487
|
+
underline marks interactive, blue marks navigation — so `Link` goes somewhere and
|
|
488
|
+
this acts. Choose by what the press DOES. The underline is not optional (position is
|
|
489
|
+
a learned convention, hover is absent on touch). `TextButtonColor` is
|
|
490
|
+
`default | danger` only — `muted` would mute the thing carrying the act (use
|
|
491
|
+
`Button color="muted"`). `onPress` REQUIRED; `disabled` greys to zinc-400, drops the
|
|
492
|
+
press + wash, announces `aria-disabled`. Hover wash, focus ring and a 40px target
|
|
493
|
+
(32px box + `hitSlop`) come from `PressableHighlight` — a pressable `Text` has
|
|
494
|
+
none of them. ONE size, no icon (a verb needing a glyph is a `Button`).
|
|
475
495
|
- **`chip`** — `Chip`: the generic pill — pressable when `onPress` (announces as a button;
|
|
476
496
|
pass `accessibilityLabel` when children aren't self-describing text) + an
|
|
477
497
|
absolutely-positioned dismiss ✕ sibling when `onDismiss` (its name = `dismissTooltip` ??
|
|
@@ -644,6 +664,21 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
644
664
|
**12- vs 24-hour derived from `locale`** — never from the browser, which is why this is not
|
|
645
665
|
an `<input type="time">`. The value is always canonical 24-hour `"HH:mm"`; `locale` changes
|
|
646
666
|
only what is displayed and typed.
|
|
667
|
+
- **`deadline`** — the COUNTDOWN vocabulary, RN-free and pure: `daysUntil`, `countdownLabel`,
|
|
668
|
+
`deadlineTone` / `deadlineColor`, `deadlineAnnotation`, `nearestDeadline` (+ `DeadlineLabels`,
|
|
669
|
+
`DeadlineThresholds`, `Deadline`). Three contracts a screen must not invent for itself — how
|
|
670
|
+
far off a date is, what to CALL that number, and how loud it reads. `daysUntil` collapses both
|
|
671
|
+
ends to midnight because the vocabulary is day-granular: rounding the raw gap lands on the
|
|
672
|
+
wrong day whenever now and the deadline straddle noon (a 02:00 cut-off tomorrow reads "today";
|
|
673
|
+
one at 02:00 today reads "1 day overdue" 12 hours in). Wording comes from the `deadline` locale
|
|
674
|
+
slice; the 1-day/3-day tone lines are freight defaults and move per domain via
|
|
675
|
+
`DeadlineThresholds`. **`deadlineAnnotation` spreads onto the `DetailRow` holding the date** —
|
|
676
|
+
a deadline is a property OF that field, and the three annotation slots carry the three urgency
|
|
677
|
+
levels exactly (`error` / `warning` / `description`), so it never becomes a trailing badge
|
|
678
|
+
speaking a vocabulary the rest of the field grid does not. `nearestDeadline` returns an OVERDUE
|
|
679
|
+
milestone FIRST and expects the caller to pass only OPEN ones — done-ness is not knowable from
|
|
680
|
+
a date, and skipping past a missed cut-off is how a record silently stops reporting the failure.
|
|
681
|
+
Worked example: `tpl_record`'s Due rows.
|
|
647
682
|
|
|
648
683
|
### Inline editing & record surfaces
|
|
649
684
|
|
|
@@ -785,6 +820,16 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
785
820
|
divider-set emphasized close with `zeroLabel` for settled. The financial-statement grammar
|
|
786
821
|
at record density; worked example: [`tpl_item_list`](../examples/tpl_item_list.tsx)
|
|
787
822
|
drawer.
|
|
823
|
+
- **`reference_field`** — `ReferenceField`: a reference to ANOTHER RECORD, rendered as a
|
|
824
|
+
FIELD VALUE — the kit's inline-editor surface (so a pointer sits in the value column
|
|
825
|
+
like the editors above and below it), whose press opens a PEEK of that record's facts.
|
|
826
|
+
`name` + optional `code` + `facts` (label/value pairs) + `onOpen`/`openLabel` and an
|
|
827
|
+
optional `onRemove`. The verbs live INSIDE the peek, never on the field: `Open` goes to
|
|
828
|
+
the referenced record's own page, `Remove` detaches. The value is a `TextLink` marker
|
|
829
|
+
(underlined, neutral) because the press opens a summary rather than navigating. A
|
|
830
|
+
reference is a FIELD, not a section — one pointer to one other row never earns a heading
|
|
831
|
+
and a rail entry. Worked example: [`tpl_record`](../examples/tpl_record.tsx) § General
|
|
832
|
+
(customer) and its Transport parties.
|
|
788
833
|
- **`peek`** — `Peek`: drill-down for inline references — press a name/id where it appears
|
|
789
834
|
and get its details in an anchored popover, without leaving the screen; keep the content a
|
|
790
835
|
summary with ONE action to the full record.
|
|
@@ -807,6 +852,16 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
807
852
|
On pressable rows dropped values stay one tap away — the row opens the record; a READ-ONLY
|
|
808
853
|
register (rows without `onPress`) has no door, so give the columns it can't afford to lose
|
|
809
854
|
a low `priority` (e.g. `priority: 1` — outlives its right-side neighbours).
|
|
855
|
+
**`detail` + `expanded` reveal a row's detail BENEATH it instead of navigating away.** A
|
|
856
|
+
register answers "which one"; the moment it cannot answer "and what about it" the reader is
|
|
857
|
+
sent to a drawer, loses the surrounding rows, and pays a navigation to come back — which is
|
|
858
|
+
what makes scanning twenty records unaffordable. Reach for it when the detail is read or
|
|
859
|
+
light-edit; a heavy form still deserves its own surface. `expanded` is CONTROLLED — how many
|
|
860
|
+
may be open at once is the caller's rule, so the component does not pick one. The detail
|
|
861
|
+
renders as a SIBLING of the press surface (a control inside it would otherwise be swallowed
|
|
862
|
+
by the row's own toggle), the row shows the open wash while it is out, and the door announces
|
|
863
|
+
`aria-expanded`. Prefer the columns first: a fact every row needs is a column, not a reason
|
|
864
|
+
to expand.
|
|
810
865
|
- **`sort_header`** — `SortHeader` + `SortState`/`SortDir` + `cycleSort` + `sortBy` +
|
|
811
866
|
`SortHeaderLabels`: the sortable column header and the sort-state helpers `Table`/
|
|
812
867
|
`DataGrid` consumers drive.
|
package/docs/composition.md
CHANGED
|
@@ -335,7 +335,29 @@ record id so per-record state resets on step. Facts are `DetailRow`s; the commit
|
|
|
335
335
|
after the scroll body; actions sit right, a leading `<Text style={{ flex: 1 }}>` hint pushes them
|
|
336
336
|
there). The open row shows a `selected` highlight (and `marked` for a bulk-ticked row). `Peek` is
|
|
337
337
|
ONLY for secondary references — a glanceable summary popover on an inline reference (a customer
|
|
338
|
-
name, a record id) with ONE action to the full record — never the primary row press.
|
|
338
|
+
name, a record id) with ONE action to the full record — never the primary row press. That ONE
|
|
339
|
+
action goes to the referenced record's own page; a local `Drawer` that re-shows the facts the peek
|
|
340
|
+
just showed makes "Open" mean *read that again, wider*, and leaves the real record unreachable
|
|
341
|
+
from the only control that promised it.
|
|
342
|
+
|
|
343
|
+
**All of the above is the LIST SCREEN.** A CHILD COLLECTION inside a record page — fees, lines,
|
|
344
|
+
stops, containers — does the opposite: the row **EXPANDS** (`TableRow` `detail` + `expanded`),
|
|
345
|
+
never docks a drawer. A drawer's whole job is to supply a context the reader is missing, and on a
|
|
346
|
+
record page they are already in it: docking one charges a navigation to see nine fields, takes the
|
|
347
|
+
sibling rows away while they read, and charges again to come back. `expanded` is CONTROLLED, so
|
|
348
|
+
pick the rule — for a ledger it is ONE at a time, because two open details push the next row off
|
|
349
|
+
screen and a register that cannot be scanned has stopped being one. The boundary is the detail's
|
|
350
|
+
weight, not its field count: read or light-edit (independent inline commits) expands, a real form
|
|
351
|
+
with a submit and cross-field validation still earns its own surface.
|
|
352
|
+
|
|
353
|
+
The expansion carries **EVERY** field, including the ones already in columns. Column cells are
|
|
354
|
+
read-only, and `table_fit` tier 2 drops droppable columns on the explicit promise that a door
|
|
355
|
+
reaches the dropped values — the expansion IS that door, so anything it omits is unreachable
|
|
356
|
+
rather than merely inconvenient. `Table` does not expose its fit result, so this cannot be
|
|
357
|
+
conditional. This is also the answer to "just add more columns": the fit budget is the MEASURED
|
|
358
|
+
container, so past it the register drops columns silently instead of overflowing — widening does
|
|
359
|
+
not reveal the extra fields, it hides them without saying so. And free text (a note) has no column
|
|
360
|
+
width at any measure.
|
|
339
361
|
|
|
340
362
|
## Two work shapes, no side panels
|
|
341
363
|
|
|
@@ -422,7 +444,21 @@ All view controls are 40px tall (`CONTROL_HEIGHT`), `sm` labels, in ONE wrapping
|
|
|
422
444
|
action `primary`; it sits at a different altitude than the form's one terminal commit.)
|
|
423
445
|
- **Button labels carry no trailing ellipsis** ("Assign", not "Assign…"). **Button color is
|
|
424
446
|
VALENCE/RISK, never category**: the ladder `muted < secondary < primary` is the emphasis axis;
|
|
425
|
-
`danger` (and its quieter `danger-secondary`) marks destructive — that's the whole axis.
|
|
447
|
+
`danger` (and its quieter `danger-secondary`) marks destructive — that's the whole axis. A
|
|
448
|
+
fill-less `Button` (`danger-secondary`, `muted`, un-colored) shows no box, so its optical edge is
|
|
449
|
+
its INK, inset by its padding: **never align one to a column of TEXT** — it reads as indented
|
|
450
|
+
against every label starting on the true edge, and reaching for the filled tier to fix that buys
|
|
451
|
+
alignment with prominence, on what is usually the rarest act on the surface. A text-weight act is
|
|
452
|
+
`TextButton`, which lands its ink on the column by construction. Chrome has three rungs: `Button` (a 40px control with a
|
|
453
|
+
surface) → `InlineButton` (28px, filled, on a field's own surface) → `TextButton` (no surface,
|
|
454
|
+
sitting in a line of text). **UNDERLINE MARKS INTERACTIVE, BLUE MARKS NAVIGATION** — so
|
|
455
|
+
`Link`/`TextLink` are blue + underline and GO somewhere, `TextButton` is zinc + underline and
|
|
456
|
+
ACTS. Pick by what the press DOES. The underline is not optional on a surface-less control:
|
|
457
|
+
POSITION is a learned convention rather than an affordance (invisible to a first-time reader) and
|
|
458
|
+
hover is not one either (absent on touch, revealed only once you're already there). A tint is the
|
|
459
|
+
other way to say it — the Material/HIG answer — but blue is spoken for here and a second accent
|
|
460
|
+
for "actionable" would collide with one-accent-per-purpose. Never a `muted` text action: it mutes
|
|
461
|
+
the one thing carrying the act. No
|
|
426
462
|
"success"/green button. Decision UIs put positive/negative color on the STATUS (dot) and verdict
|
|
427
463
|
(colored `Text`), not the buttons.
|
|
428
464
|
- **Create buttons carry NO `+` icon** — "Add fee", not "+ Add fee". A create is `primary` and
|
|
@@ -493,6 +529,24 @@ gate's SCOPE, once** — never prose beside the button, never revealed only on p
|
|
|
493
529
|
no rail entry — it is not a place you navigate to. Narrow, it collapses onto the same bar the
|
|
494
530
|
sections use, on the RIGHT: sections are where you ARE, the discussion is what is being SAID,
|
|
495
531
|
and the two poles keep the sides they hold on a wide screen.
|
|
532
|
+
- **A record is ONE PAGE. A section is a place you SCROLL to, never a route you swap to.** The
|
|
533
|
+
rule above already assumes it — a section earns a POSITION in a top-to-bottom read, and the
|
|
534
|
+
discussion is docked precisely because a thing "parked at one position is unreachable from
|
|
535
|
+
every other". Routing the sections makes that true of every one of them. It is tempting on a
|
|
536
|
+
long record and it does not survive contact: each fix it needs rebuilds the whole-record view
|
|
537
|
+
in miniature — a dot to say which hidden section just changed, a page-band action to
|
|
538
|
+
re-surface an affordance some section had swallowed, an "everything" route that IS this page.
|
|
539
|
+
It also HIDES what it does not carry: a whole-record drop target advertised only inside the
|
|
540
|
+
files section does not exist from the eight destinations that are not it. And it charges
|
|
541
|
+
permanently — browser find reaches only the open section, two sections can never be read
|
|
542
|
+
together, and every cross-section check costs a click each way. A long record is a DENSITY
|
|
543
|
+
problem, and density is cuttable (a choice that took four rows, a group that restated the
|
|
544
|
+
title): **cut the sections, keep the page.** The rail stays — `useSectionNav` is a scroll-spy
|
|
545
|
+
and a jump, and it carries a `Badge variant="dot"` on any section holding a condition, which
|
|
546
|
+
is how the page says WHERE without a banner at the top saying it for one section at a time.
|
|
547
|
+
A cross-section TASK is not an argument for routing either: it belongs in an overlay that
|
|
548
|
+
spans the record (an extract review lists every proposed field, whatever section owns it),
|
|
549
|
+
which the rail then never has to serve.
|
|
496
550
|
- **A VARIABLE-LENGTH, ORDERED fact is a `Sequence`, not a row of fields.** Before reaching for
|
|
497
551
|
any layout lever, check the section is not carrying a MODELLING error: `Origin` /
|
|
498
552
|
`Transfer point` / `Destination` looks like three fields and is actually one route — it cannot
|
|
@@ -618,7 +672,13 @@ selection washes).
|
|
|
618
672
|
**TEXT never wears `solid()`** — the 500 is a FILL shade (dots, series). A status word/number uses
|
|
619
673
|
`Text`'s valence tokens: `color="danger"` (red-900) / `"warning"` (amber-700) / `"success"`
|
|
620
674
|
(emerald-700) — all AA on white. Map a dynamic `ColorName` to a token at the call site; hue nuance
|
|
621
|
-
(rose vs orange) stays on `Badge`s.
|
|
675
|
+
(rose vs orange) stays on `Badge`s.
|
|
676
|
+
|
|
677
|
+
Links use `Link`/`TextLink` (blue-600), never `solid("blue")` — and blue-600 means NAVIGATION
|
|
678
|
+
specifically, not "interactive" generally: the underline is what marks a surface-less control
|
|
679
|
+
interactive (see the button ladder above), so a `TextButton` ACTS and stays zinc. `TextLink` takes
|
|
680
|
+
the blue only when it has an `href`; as a passive marker on a value it stays neutral, because a
|
|
681
|
+
press that opens a PEEK is not a trip.
|
|
622
682
|
|
|
623
683
|
## `Badge` is for STATUS only — everything else is text
|
|
624
684
|
|
package/docs/data_entry.md
CHANGED
|
@@ -191,6 +191,14 @@ A READ-ONLY value in the same column — a computed total, a system ID, a synced
|
|
|
191
191
|
border) but stays FLAT and non-interactive, so editable (chip) vs read-only (flat) is legible at a
|
|
192
192
|
glance and the static value never reads as a disabled input.
|
|
193
193
|
|
|
194
|
+
**Only in a grid that MIXES the two.** "Copies the editor box metrics" means it hard-sets
|
|
195
|
+
`INLINE_CONTROL_HEIGHT` — worth paying to hold a column straight beside real editors, pure waste
|
|
196
|
+
where there are none. In an ALL-static grid (a peek's facts, a summary popover) it reserves a 40px
|
|
197
|
+
control band for a 20px value, and the row is the MAX of that box and the table's own band, so
|
|
198
|
+
neither fix works alone: use plain `Text` for the value AND pass `DetailTable` a `minHeight` (28,
|
|
199
|
+
`DetailRow`'s own default). Same test as `labelWidth` — a `DetailTable`'s defaults are the
|
|
200
|
+
inline-control grid's; a grid that isn't one overrides both.
|
|
201
|
+
|
|
194
202
|
### Choosing a CHOICE control — by option count, not by taste
|
|
195
203
|
|
|
196
204
|
A field whose value is one-of-N has three answers, and the wrong one is what makes a record
|
|
@@ -216,7 +224,17 @@ on a reference, `Call` on a phone): the act names the value, so it travels with
|
|
|
216
224
|
instead of being paired back to a control at the page's right edge. Pass it UNCONDITIONALLY and
|
|
217
225
|
`disabled` it when there is nothing to act on — a verb that appears once the value is non-empty
|
|
218
226
|
resizes the field as the user types, which is the one thing an inline editor promises never to
|
|
219
|
-
do.
|
|
227
|
+
do. **"About the value" is the whole test, and DEPARTURE fails it:** `Open`, on a field holding a
|
|
228
|
+
reference to another record, is not an act on that value — it is leaving for a different record —
|
|
229
|
+
and putting it on the surface hides two destinations behind one press area (the box opens a
|
|
230
|
+
summary, the verb 8px away navigates). A reference's `Open` and `Remove` live inside its PEEK,
|
|
231
|
+
which is what `LedgerRow` already enforces by IGNORING `reference` while `peek` is set. What marks
|
|
232
|
+
the field as a reference instead of typed text is then the VALUE, not a verb: `TextLink` with
|
|
233
|
+
neither `href` nor `onPress` — the kit's underline, non-interactive, so it sits inside the field's
|
|
234
|
+
own press target without nesting a control. Not a `Badge` (STATUS only), and not `Link`'s fixed
|
|
235
|
+
blue + `role="link"`, which promises navigation a peek does not perform.
|
|
236
|
+
|
|
237
|
+
Anything that is not a verb — a status `Badge`, a unit ("kg"/"$", which belong IN the value
|
|
220
238
|
via `InlineNumberInput format`) — composes into a row inside the VALUE cell, so it costs only the
|
|
221
239
|
row that wants it. See the "General" section of `tpl_record`, which also reads top→bottom as a
|
|
222
240
|
full record surface. Not every field is a same-height swap — a tag field, a status, or an
|
package/docs/templates.md
CHANGED
|
@@ -196,33 +196,27 @@ screens — register, per-row action, gated selection, and act-on-many in one. T
|
|
|
196
196
|
only](./composition.md)). A per-row action `Button` (here Print) is the row's primary action in
|
|
197
197
|
the trailing column, `⋯` its overflow. A select-all band, footer totals + `Pagination`, and a
|
|
198
198
|
`FloatingActionBar` carrying the bulk action while rows are ticked.
|
|
199
|
-
- **A row press opens the PRODUCTION workspace `Drawer
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
the
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
- **
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
an always-visible optional follow-up `InlineDatePicker`, an "Attach files" secondary
|
|
221
|
-
(`pickFiles` → removable `FileThumbnailGrid` in the composer), and the primary Log button
|
|
222
|
-
saying WHY when disabled. Below, a labelled History `Timeline` of outcome-typed entries,
|
|
223
|
-
newest first — an entry's attachments ride its expandable details as a thumbnail grid
|
|
224
|
-
whose press opens the shared `FileGalleryModal`.
|
|
225
|
-
- A closing `DangerZone`.
|
|
199
|
+
- **A row press opens the PRODUCTION workspace `Drawer`** — which RENDERS
|
|
200
|
+
[`tpl_record`](../examples/tpl_record.tsx) in `chrome="drawer"`, not a copy of it. Same
|
|
201
|
+
sections, same order, same rules, because it is the same component: `<TplRecord
|
|
202
|
+
chrome="drawer" code={row.id} />`. A hand-built second record surface is exactly how the two
|
|
203
|
+
drifted apart — a checklist on one side and a `Pipeline` on the other, a reference as a
|
|
204
|
+
section here and a field there — so the register owns the REGISTER and nothing else.
|
|
205
|
+
- What the chrome flag changes is only what a nested surface must not do: comments become the
|
|
206
|
+
LAST SECTION of the drawer's own scroller (a `Drawer` has no second gutter to dock them
|
|
207
|
+
into), and the back button is left to the Drawer's header. Everything else follows from
|
|
208
|
+
width — the layout measures its CONTAINER via `onLayout`, never the window, so the outline
|
|
209
|
+
rail, the docked discussion and the mirrored gutters collapse on their own, and the section
|
|
210
|
+
picker is a popover ANCHORED to its bar button rather than a full-page `Modal` that would
|
|
211
|
+
escape the drawer.
|
|
212
|
+
- **Linked records** still push an editable screen for that record inside the drawer via the
|
|
213
|
+
hosted `ScreenRouter` — while `canGoBack` the drawer header swaps to a back button + the
|
|
214
|
+
pushed id and the ◀ ▶ sequencer hides; back pops with scroll preserved.
|
|
215
|
+
- Two things this template no longer demonstrates, so look elsewhere: the drawer does NOT
|
|
216
|
+
write back to its row (settling a ledger used to flip the register's paid state live — the
|
|
217
|
+
drawer's money is the record's now), and the CRM call-log shape (outcome pills over a
|
|
218
|
+
multiline note, an outcome-typed history `Timeline`) went with the Activity section, which
|
|
219
|
+
the record surface has no counterpart for.
|
|
226
220
|
- **The "Enter data" INTAKE FORK** (the New CTA) — AI FIRST, the form as fallback, one phased
|
|
227
221
|
dialog. The whole register page is ALSO a `<FileDropTarget … paste>` (the whole-surface intake
|
|
228
222
|
standard): a file dropped or Ctrl/Cmd+V pasted ANYWHERE on the register opens this same dialog
|
|
@@ -416,7 +410,9 @@ billing, and quick-capture templates. Top → bottom:
|
|
|
416
410
|
map** (every section including Danger zone; subsections never join it) — headed by the
|
|
417
411
|
bare circular `BackButton`; it scroll-spies the active section (`current={activeKey === key}`
|
|
418
412
|
on each item — navigation, not selection), and on narrow containers becomes the pinned
|
|
419
|
-
current-section bar opening a section-picker
|
|
413
|
+
current-section bar opening a section-picker POPOVER, anchored to that bar's own button, whose
|
|
414
|
+
items carry the same `current`. Anchored, not a `Modal`: a modal escapes its container, so the
|
|
415
|
+
same surface hosted in a drawer would take the whole window over the register behind it.
|
|
420
416
|
- **The page layout is the DOCS layout — the reading column is CENTRED IN THE VIEWPORT, and
|
|
421
417
|
the rail sits in its left gutter.** The `ScrollView` is FULL-WIDTH — **never cap it at
|
|
422
418
|
`CONTENT_MAX`, or the scroll container becomes the column and the gutters go wheel-dead**
|