@lotics/ui 12.1.2 → 13.7.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 +5 -0
- package/docs/catalog.md +73 -32
- package/docs/composition.md +61 -24
- package/docs/data_entry.md +19 -17
- package/docs/templates.md +71 -76
- package/examples/tpl_item_list.tsx +6 -6
- package/examples/tpl_record.tsx +131 -188
- package/examples/tpl_task_board.tsx +20 -22
- package/package.json +3 -2
- package/src/badge.tsx +26 -10
- package/src/checklist.tsx +1 -1
- package/src/detail_row.tsx +24 -8
- package/src/form_date_picker.tsx +2 -1
- package/src/form_field.tsx +23 -3
- package/src/form_picker.tsx +2 -1
- package/src/form_switch.tsx +16 -2
- package/src/form_text_input.tsx +4 -4
- package/src/inline_date_picker.tsx +19 -9
- package/src/inline_edit.tsx +43 -24
- package/src/inline_member_select.tsx +43 -10
- package/src/inline_number_input.tsx +4 -4
- package/src/inline_select.tsx +193 -87
- package/src/inline_text_input.tsx +4 -4
- package/src/inline_time_picker.tsx +4 -4
- package/src/inset.tsx +38 -0
- package/src/linked_record_box.tsx +102 -0
- package/src/number_input.tsx +1 -1
- package/src/radio_picker.tsx +18 -28
- package/src/text_input_field.tsx +3 -3
- package/examples/tpl_tasks.tsx +0 -456
- package/src/inline_tag_select.tsx +0 -140
package/AGENTS.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# @lotics/ui — the UI reference index
|
|
2
2
|
|
|
3
|
+
> **Upgrading to v13 from 12.x.** `RadioPicker`'s **`direction`** prop was removed — the
|
|
4
|
+
> picker is always the stacked column of full-width choices. A former `direction="row"`
|
|
5
|
+
> usage just drops the prop. Additively, `DetailRow` and `FormField` gained a **`warning`**
|
|
6
|
+
> field annotation (amber, the third tone between `description` and `error`).
|
|
7
|
+
|
|
3
8
|
> **Upgrading to v12 from 11.x.** Two exports were removed: **`CommentComposer`** (compose
|
|
4
9
|
> comments with THE kit `Composer` — attach via `actionsButton`/`files`; `CommentList` is
|
|
5
10
|
> unchanged) and **`DetailRow.info`** (field annotations are explicit — use the new
|
package/docs/catalog.md
CHANGED
|
@@ -95,8 +95,8 @@ Batch draft-form state → `useForm`.
|
|
|
95
95
|
|
|
96
96
|
### Edit a record's fields in place
|
|
97
97
|
|
|
98
|
-
The `Inline*` family: `InlineTextInput` · `InlineNumberInput` · `InlineSelect`
|
|
99
|
-
`
|
|
98
|
+
The `Inline*` family: `InlineTextInput` · `InlineNumberInput` · `InlineSelect` (single or
|
|
99
|
+
`multi`) · `InlineMemberSelect` · `InlineDatePicker` · `InlineTimePicker`; a
|
|
100
100
|
READ-ONLY field in that same column uses `InlineStatic` (matches the editor box exactly, no
|
|
101
101
|
input chrome, so it aligns pixel-for-pixel). A stack of labelled field rows lives in
|
|
102
102
|
`DetailTable` + `DetailRow`; the record's identity band is `RecordSummary`; its money
|
|
@@ -105,10 +105,9 @@ summary is `Ledger`.
|
|
|
105
105
|
### Tasks / to-dos — pick by ALTITUDE
|
|
106
106
|
|
|
107
107
|
A task-management PAGE (many tasks, grouping, filters, expandable rows) is COMPOSITION —
|
|
108
|
-
there is NO Task component
|
|
109
|
-
`
|
|
110
|
-
[`
|
|
111
|
-
[`tpl_task_board`](../examples/tpl_task_board.tsx) (columns). A record's own small CHECKLIST
|
|
108
|
+
there is NO Task component: `CheckCircle` + a struck `InlineTextInput` + your meta cells
|
|
109
|
+
(`InlineDatePicker`/`InlineMemberSelect`/`InlineSelect`, all `variant="cell"`); see `tpl_record`'s
|
|
110
|
+
Tasks section (the canonical task pattern) and [`tpl_task_board`](../examples/tpl_task_board.tsx) (columns). A record's own small CHECKLIST
|
|
112
111
|
(the 5–8 tasks living in a drawer/section) is the `Checklist`/`ChecklistRow` compound +
|
|
113
112
|
`SuggestionChip` commons + `CaptureRow` — see [`tpl_record`](../examples/tpl_record.tsx) /
|
|
114
113
|
[`tpl_item_list`](../examples/tpl_item_list.tsx).
|
|
@@ -342,10 +341,15 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
342
341
|
|
|
343
342
|
- **`card`** — `Card` · `CardHeader` · `CardHeaderTitle` (+ `info` ⓘ popover) ·
|
|
344
343
|
`CardHeaderMeta` · `CardBody` · `CardFooter`.
|
|
344
|
+
- **`inset`** — `Inset`: a tinted, recessed content surface (zinc-50 fill · 10 radius · 14
|
|
345
|
+
pad · 12 gap) — the "well" INSIDE a Card/Section for a grouped sub-form, an inline fill
|
|
346
|
+
editor, or a nested block. **NOT a `Callout`:** a Callout is a status BAND (and
|
|
347
|
+
`warning`/`error` render as an ARIA `alert`) — wrapping form fields in an alert is wrong.
|
|
348
|
+
The law: **content + fields → `Inset`; a message → `Callout`.**
|
|
345
349
|
- **`section_heading`** — `Section` · `SectionHeading` · `SectionHeadingTitle` ·
|
|
346
350
|
`SectionHeadingMeta` · `Subsection` · `SubsectionHeading` · `SubsectionHeadingTitle` — the
|
|
347
351
|
card-less twin of the Card family, compound, owns no margin; spacing via the Section gap
|
|
348
|
-
(
|
|
352
|
+
(16, fixed), no body component. `SectionHeadingTitle` is ALWAYS `##` (xl semibold;
|
|
349
353
|
`weight="medium"` opt-down only) + `info` for an ⓘ provenance popover after the title,
|
|
350
354
|
same as `CardHeaderTitle.info`. `SubsectionHeadingTitle` is the `###` lg-semibold level-3
|
|
351
355
|
title of a named group inside a section (same `info` ⓘ affordance as the section title) —
|
|
@@ -356,10 +360,11 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
356
360
|
`SectionHandlers.expand/collapse`). **Warning:** two modules export a `Section` — the
|
|
357
361
|
layout grammar's card-less region is the one in `section_heading`; import from
|
|
358
362
|
`@lotics/ui/section` only when you specifically want this collapsible titled block.
|
|
359
|
-
- **`section_stack`** — `SectionStack` · `SubsectionStack` —
|
|
360
|
-
between-block law: a fixed beat + hairline
|
|
361
|
-
|
|
362
|
-
|
|
363
|
+
- **`section_stack`** — `SectionStack` · `SubsectionStack` — stacks that own the
|
|
364
|
+
between-block law, skipping null children: `SectionStack` = a fixed 56px beat + a hairline
|
|
365
|
+
`Divider` between top-level blocks; `SubsectionStack` = a fixed 32px beat, space-only (no
|
|
366
|
+
hairline — titles carry the grouping, `divided` an opt-in for a rare headingless stack);
|
|
367
|
+
stop hand-rolling gap + `<Divider />` pairs.
|
|
363
368
|
- **`section_card`** — `SectionCard`: a titled card with a one-line description and an
|
|
364
369
|
optional hairline-set `footer` (where `TrendFooter` and source notes go).
|
|
365
370
|
- **`stack`** — `Stack`: gap-spaced row/column; `useSeparator` inserts `Separator`s between
|
|
@@ -434,6 +439,15 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
434
439
|
- **`pressable_row`** — `PressableRow`: THE register row — full-width hover/open wash,
|
|
435
440
|
`selected` (the open record) vs `marked` (ticked in bulk-select), forwards `ref` for
|
|
436
441
|
popover anchoring.
|
|
442
|
+
- **`linked_record_box`** — `LinkedRecordBox`: a bordered box scoping ANOTHER record's data
|
|
443
|
+
(`icon` · `name` · `subtitle` · vertical `facts`), the WHOLE box a keyboard door into its
|
|
444
|
+
detail (`onOpen`/`doorLabel`); verbs ride an `actions` slot in a hairline-fenced footer at the
|
|
445
|
+
bottom (destructive LEFT, go-to RIGHT — the divider draws itself when `actions` is present, so
|
|
446
|
+
don't hand-add one). Use it wherever a record points at another (a shipment's customer, an invoice's
|
|
447
|
+
party, a case's sibling). It **enforces the a11y contract** copy-pasting got wrong: a
|
|
448
|
+
container with interactive descendants is never `role="button"` (invalid HTML) — an internal
|
|
449
|
+
empty door sibling carries the tab stop / name / focus ring, verbs lift above via `zIndex`.
|
|
450
|
+
The reassign picker / empty state that swaps in for the box is the consumer's.
|
|
437
451
|
- **`pressable_highlight`** — `PressableHighlight`: the hover-wash + keyboard-focus-ring
|
|
438
452
|
`Pressable` under `MenuButton`/`Switcher`/custom pressable surfaces; its style-fn/children
|
|
439
453
|
receive `hovered` + `focusVisible`.
|
|
@@ -453,12 +467,16 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
453
467
|
|
|
454
468
|
### Badges, status & feedback
|
|
455
469
|
|
|
456
|
-
- **`badge`** — `Badge`:
|
|
457
|
-
the
|
|
470
|
+
- **`badge`** — `Badge`: a STATUS indicator (lifecycle state / risk / quality cue), the ONLY
|
|
471
|
+
thing it's for. **Default `variant="dot"`** (pill-less colored dot + label, the light indicator);
|
|
472
|
+
`variant="tonal"` is the heavy filled pill — opt in sparingly for the ONE prominent status of a
|
|
473
|
+
surface. **NOT for a type / category / attribute / count — that's inline `Text`, never a Badge**
|
|
474
|
+
(see composition.md "Badge is for status only"). A bare `<Badge>` is `dot`.
|
|
458
475
|
- **`status_badge`** — `StatusBadge`: an enabled/disabled pulse badge (`enabled` + `label`).
|
|
459
476
|
- **`option_badge`** — `OptionBadge`: a select value as its configured colored badge.
|
|
460
477
|
- **`callout`** — `Callout` · `CalloutTitle` · `CalloutText` · `CalloutActions` (`tone`
|
|
461
|
-
info|success|warning|error|neutral): inline status band
|
|
478
|
+
info|success|warning|error|neutral): inline status band — a MESSAGE (`warning`/`error`
|
|
479
|
+
are ARIA `alert`s). For a form / nested content surface use **`Inset`**, never a Callout.
|
|
462
480
|
- **`empty_state`** — `EmptyState`: centered placeholder for an empty list/filter result —
|
|
463
481
|
`message` + `hint`, optional `icon` anchor and `action` CTA.
|
|
464
482
|
- **`completion_state`** — `CompletionState`: the "all done" terminal state.
|
|
@@ -506,13 +524,14 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
506
524
|
the tint; override via `style` only when a surface genuinely wants otherwise.
|
|
507
525
|
- **`number_input`** — `NumberInput`: numeric input; `format` for currency/units.
|
|
508
526
|
- **`search_input`** — `SearchInput`: the search box for toolbars/filters.
|
|
509
|
-
- **`form_field`** — `FormField` + `useFormField`: label / description / error /
|
|
510
|
-
marker wrapper
|
|
511
|
-
|
|
527
|
+
- **`form_field`** — `FormField` + `useFormField`: label / description / warning / error /
|
|
528
|
+
`optional` marker wrapper (the three field-annotation tones — muted / amber / danger — all
|
|
529
|
+
wired into the input's describedby); the fieldset grid cell (`half`/`full` widths — see the
|
|
530
|
+
data-entry patterns doc indexed in [AGENTS.md](../AGENTS.md)).
|
|
512
531
|
- **`form_text_input`** — `FormTextInput`: `FormField` wrapping a `TextInputField`; the
|
|
513
532
|
one-line labeled text field. Controls without a Form\* twin just wrap in `FormField`.
|
|
514
533
|
- **`form_switch`** — `FormSwitch`: the FormField-labeled twin of `Switch` — toggle +
|
|
515
|
-
clickable `label` (pressing it flips the value) + optional `description`/`error`; the
|
|
534
|
+
clickable `label` (pressing it flips the value) + optional `description`/`warning`/`error`; the
|
|
516
535
|
settings-form boolean row. A bare toggle in a cell/toolbar → `Switch`; a full-row menu
|
|
517
536
|
toggle → `SwitchButton`.
|
|
518
537
|
- **`form_picker`** — `FormPicker`: `FormField` wrapping a `Picker` — one labeled native
|
|
@@ -558,20 +577,41 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
558
577
|
INSIDE the control and inline error, commit on blur (Enter saves, Escape reverts) or
|
|
559
578
|
`controls="buttons"`; KEYBOARD focus on the closed view opens edit mode with the input
|
|
560
579
|
focused (type → Tab → type — see the data-entry keyboard contract), pointer focus never
|
|
561
|
-
does;
|
|
562
|
-
|
|
580
|
+
does; **`variant: "form" | "cell"`** is THE axis that separates a form field from a data-grid
|
|
581
|
+
cell (see below).
|
|
563
582
|
- **`inline_text_input`**, **`inline_number_input`** (`format` for currency/units),
|
|
564
583
|
**`inline_select`**, **`inline_member_select`**, **`inline_date_picker`**
|
|
565
584
|
(`format="datetime"`, `optionalTime`; keyboard focus opens the TYPED segmented `DateField`
|
|
566
585
|
— locale field order, separator advances, Alt+ArrowDown floats the calendar; click still
|
|
567
|
-
opens the calendar popover), **`inline_time_picker`** — the
|
|
568
|
-
|
|
586
|
+
opens the calendar popover), **`inline_time_picker`** — the inline field editors (a form field by
|
|
587
|
+
default, a grid cell with `variant="cell"` — see the split below);
|
|
588
|
+
`InlineSelect`/`InlineMemberSelect` render the resting value like its option —
|
|
569
589
|
`renderOptionContent` by default, `renderSelected` to override — a chip/badge at rest, not
|
|
570
|
-
just text.
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
590
|
+
just text. **`InlineSelect` is single OR multi** — pass `multi` for a tag SET (`value: T[]`,
|
|
591
|
+
commits the new set on popover-CLOSE; selected tags render as badges via `renderSelected`),
|
|
592
|
+
mirroring `Select`'s `multi` axis; there is NO separate tag component. Both modes take
|
|
593
|
+
`allowCustom` (a create-a-tag/option row) + `searchable`. **`InlineMemberSelect` takes `avatarOnly`**
|
|
594
|
+
— the resting display is the bare AVATAR (no name/chevron, a dashed "add" ghost when unset) for a
|
|
595
|
+
DENSE row where the name won't fit (a task-row assignee); the dropdown rows still show avatar + name.
|
|
596
|
+
`InlineDatePicker` takes a **`tone`**
|
|
597
|
+
(`TextColor`) that colours the resting date by
|
|
598
|
+
semantic state — overdue red, soon-due amber (the caller owns the rule, e.g. a task's `dueTone`);
|
|
599
|
+
the value text IS the signal, no separate dot.
|
|
600
|
+
**The form/cell split — one `variant`, not a second family.** Every `Inline*` editor is BOTH a
|
|
601
|
+
form field and a data-grid cell; `variant` picks which:
|
|
602
|
+
- **`variant="form"`** (default) — a zinc-50 chip at rest + a hover-BORDER. THE editability
|
|
603
|
+
affordance on a surface that MIXES editable and static values (a `DetailTable` / record field).
|
|
604
|
+
- **`variant="cell"`** — transparent at rest + a background-tint WASH on hover (`zinc-100` /
|
|
605
|
+
`zinc-200` press — the same `PressableHighlight` language as the row/cell beside it), and it reads
|
|
606
|
+
VALUE-FIRST: the date drops its resting calendar glyph, a select drops its chevron (the column
|
|
607
|
+
header + the uniformly-editable grid are the affordance; the saving spinner still shows). For a
|
|
608
|
+
DENSE, uniformly-editable surface — a register/`DataGrid` column, a task row (worked example:
|
|
609
|
+
`tpl_task_board`'s columns, `tpl_record`'s Tasks section).
|
|
610
|
+
|
|
611
|
+
There is NO separate `*Cell` component family — a data-grid cell is `<InlineSelect variant="cell" …/>`,
|
|
612
|
+
not a `SelectCell`. (Naming a component for a USE, and duplicating the picker stack to flip two
|
|
613
|
+
style properties, are both things the kit forbids.) A custom pressable cell you hand-roll should
|
|
614
|
+
match the `variant="cell"` language: the `PressableHighlight` wash, never a border.
|
|
575
615
|
- **`inline_static`** — `InlineStatic`: a READ-ONLY value matching the Inline\* box metrics
|
|
576
616
|
EXACTLY (height, padding, 1px transparent border) so a non-editable field — a computed
|
|
577
617
|
total, a system ID, a synced/locked value — aligns pixel-for-pixel in the same column;
|
|
@@ -583,15 +623,16 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
583
623
|
edit; optional `trailing` slot renders a right-side action/badge after the value (units
|
|
584
624
|
belong IN the value via `InlineNumberInput format`). The FIELD-ANNOTATION vocabulary (same
|
|
585
625
|
names + meanings as `FormField`), always EXPLICIT — a row never hides guidance behind an ⓘ:
|
|
586
|
-
**`description`** = persistent guidance, under the VALUE (stacked mode
|
|
587
|
-
order label · description · control); **`
|
|
588
|
-
|
|
589
|
-
|
|
626
|
+
**`description`** = a fact / persistent guidance (muted), under the VALUE (stacked mode
|
|
627
|
+
mirrors the form order label · description · control); **`warning`** = a consequence to weigh
|
|
628
|
+
before acting (amber, announced); **`error`** = field-level failure under the value, danger +
|
|
629
|
+
alert semantics (the `Inline*` editors already render their own transient save errors — don't
|
|
630
|
+
wire both); invalid/cross-field STATE = a co-located `Callout`; **`flat`**
|
|
590
631
|
on a flat-text value row (`InlineStatic`, plain `Text`) tucks the annotation up by the
|
|
591
632
|
control band's slack so its gap matches a chip row's.
|
|
592
633
|
`DetailTable`: the compound parent of
|
|
593
634
|
a row STACK — `labelWidth` (default 130) / `trailingWidth` / `minHeight` (default 40, the
|
|
594
|
-
inline-control grid) declared ONCE + the
|
|
635
|
+
inline-control grid) declared ONCE + the 8px row gap; with `trailingWidth` every row
|
|
595
636
|
reserves the trailing column so value cells share one width and trailing items align at
|
|
596
637
|
one x, like a table. RESPONSIVE with no prop: it measures its own container (onLayout, not
|
|
597
638
|
the viewport — works inside a Drawer; the unmeasured first frame renders opacity-0 so the
|
package/docs/composition.md
CHANGED
|
@@ -370,6 +370,11 @@ All view controls are 40px tall (`CONTROL_HEIGHT`), `sm` labels, in ONE wrapping
|
|
|
370
370
|
- **Inline status**: `<Callout tone="info|success|warning|error|neutral">` — compound (like
|
|
371
371
|
Card): compose `CalloutTitle`/`CalloutText`/`CalloutActions` inside. `Callout` is INLINE;
|
|
372
372
|
`Alert` is the blocking modal; `Badge` is a one-word pill.
|
|
373
|
+
- **Nested content / a sub-form goes in an `Inset`, NEVER a `Callout`.** A Callout is a
|
|
374
|
+
MESSAGE, and `warning`/`error` render as an ARIA `alert` — wrapping form fields in an alert
|
|
375
|
+
makes a screen reader announce the whole form as one time-sensitive alert. `<Inset>` is the
|
|
376
|
+
tinted "well" for a grouped sub-form / inline fill editor / nested block: **fields + content
|
|
377
|
+
→ `Inset`; a message → `Callout`.**
|
|
373
378
|
- **One primary action per surface.** Everything else secondary/muted; destructive = `danger`
|
|
374
379
|
styling + an explicit label. (A grouped BUILDER section — line items — MAY give its own Add
|
|
375
380
|
action `primary`; it sits at a different altitude than the form's one terminal commit.)
|
|
@@ -407,29 +412,50 @@ gate's SCOPE, once** — never prose beside the button, never revealed only on p
|
|
|
407
412
|
|
|
408
413
|
## Field annotations & linked records
|
|
409
414
|
|
|
410
|
-
- **The field-annotation vocabulary**
|
|
411
|
-
always EXPLICIT
|
|
412
|
-
the VALUE on a horizontal row — the label
|
|
413
|
-
the form order label · description · control
|
|
414
|
-
|
|
415
|
-
|
|
415
|
+
- **The field-annotation vocabulary** — THREE tones, same names + meanings on `FormField` and
|
|
416
|
+
`DetailRow`, always EXPLICIT (never a hidden ⓘ on rows): **`description`** (info, muted) = a
|
|
417
|
+
content/domain fact or persistent guidance, under the VALUE on a horizontal row — the label
|
|
418
|
+
column has no room for prose; stacked mode wears the form order label · description · control;
|
|
419
|
+
**`warning`** (amber) = a consequence the user should weigh before acting, announced on
|
|
420
|
+
appearance; **`error`** (danger + alert semantics) = an invalid state under the value (the
|
|
421
|
+
`Inline*` editors already render their own transient save errors); cross-field / record-level
|
|
422
|
+
STATE = a co-located `Callout`. Every tone is USEFUL AND TERSE — a short clause stating the
|
|
423
|
+
fact or consequence, never widget mechanics, never a second sentence; `warning` marks a
|
|
424
|
+
consequence, not decoration. Rows share ONE
|
|
416
425
|
alignment law: the row top-aligns and label · control · trailing each center within the
|
|
417
426
|
first control line — a tall value block never drags the label. A FLAT value row
|
|
418
427
|
(`InlineStatic`, plain `Text`) sets **`DetailRow flat`**: the annotation tucks up by the
|
|
419
428
|
control band's slack so the perceived gap under the text equals the gap under a chip —
|
|
420
429
|
the band's invisible bottom half must never read as a hole above the description.
|
|
421
430
|
- **A LINKED record never renders as field rows** — another record's data must not read as
|
|
422
|
-
fields on THIS one.
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
431
|
+
fields on THIS one. Use **`LinkedRecordBox`** (`tpl_record`'s Customer/sibling are the worked
|
|
432
|
+
examples): a BORDERED, fully pressable box (glyph anchor · identity · facts stacked
|
|
433
|
+
vertically) whose press opens the record's detail drawer; the verbs ride its `actions` slot
|
|
434
|
+
in a hairline-fenced footer INSIDE the box — destructive LEFT in danger, go-to RIGHT. One verb
|
|
435
|
+
set across every box of a group (a party wall's five links all read "Gỡ", not a mix of
|
|
436
|
+
"Đổi"/"Gỡ"): with a clearable link, changing = remove → the empty-state picker re-prompts, so
|
|
437
|
+
the destructive verb covers both required anchors and optional adds. Editing the linked record happens on
|
|
438
|
+
ITS page, never here.
|
|
439
|
+
**The empty / attach state is a find-or-create `Combobox`, never a bare picker** (`tpl_record`'s
|
|
440
|
+
Customer is the worked example): rows are `renderOptionContent`-rich, NOT a wall of bare names —
|
|
441
|
+
the premium shape is a **glyph tile (28·radius 7·zinc-100) · the name over a muted metadata line**
|
|
442
|
+
that carries the type + identifying fact as TEXT (`type · code · city`), joined by "·" and free to
|
|
443
|
+
wrap the row taller. A type/attribute is **NOT a status, so it is text, never a `Badge`** (see
|
|
444
|
+
Badge discipline below). The row should read like the `LinkedRecordBox` it becomes — reuse the same
|
|
445
|
+
kind-glyph. (The kit suppresses `renderOptionContent` on the `allowCustom` "Create …" row so it keeps its plain + look.)
|
|
446
|
+
Plus `allowCustom` + `customOptionPlacement:"top"` + `customOptionLabel={q => 'Create "…"'}`. In
|
|
447
|
+
`onValueChange`, an existing pick attaches; the custom row opens a create **`Dialog` prefilled with
|
|
448
|
+
the typed query** (mint the record + link it) — so a party absent from the registry is created
|
|
449
|
+
WITHOUT leaving the record. `customOptionPlacement:"top"` keeps the create row visible while the
|
|
450
|
+
keyboard highlight stays on the first MATCH, so Enter on a partial never makes a duplicate. A
|
|
451
|
+
linked record seeded by a workflow (not user-attached) skips the picker — show the box or an
|
|
452
|
+
`EmptyState`, no create affordance. The component OWNS the press ANATOMY (the error-prone part hand-rolling
|
|
453
|
+
got wrong): the box is a `PressableRow` (role-less, non-focusable surface — hover wash spans
|
|
454
|
+
the nested verbs) with the keyboard DOOR — an EMPTY absolutely-positioned `role="button"`
|
|
455
|
+
sibling carrying the tab stop, the accessible name and the focus ring — beneath the content,
|
|
456
|
+
the interior verbs lifted above it via `zIndex: 1`. NEVER `role="button"` on a container with
|
|
457
|
+
interactive descendants: a button must not contain a button (invalid HTML, a hydration error,
|
|
458
|
+
and AT double-announcement).
|
|
433
459
|
|
|
434
460
|
## Color discipline — solid / tint / ramp + ONE accent
|
|
435
461
|
|
|
@@ -452,16 +478,27 @@ selection washes).
|
|
|
452
478
|
(emerald-700) — all AA on white. Map a dynamic `ColorName` to a token at the call site; hue nuance
|
|
453
479
|
(rose vs orange) stays on `Badge`s. Links use `Link`/`TextLink` (blue-600), never `solid("blue")`.
|
|
454
480
|
|
|
455
|
-
##
|
|
481
|
+
## `Badge` is for STATUS only — everything else is text
|
|
456
482
|
|
|
457
|
-
|
|
483
|
+
A `Badge` means STATE that must read at a GLANCE — a lifecycle status, a risk level, a quality
|
|
484
|
+
cue. That is the ONLY thing it is for; keep the vocabulary scarce so a colored chip always MEANS
|
|
485
|
+
status. **A type / category / attribute / count is NOT a status — render it as inline `Text` (a
|
|
486
|
+
muted metadata line, free to wrap the row taller), NEVER a Badge.** A party's type, a document
|
|
487
|
+
kind, a city, "3 files" → text. If you're reaching for a Badge in a row or an option list "to
|
|
488
|
+
show more info," STOP — that's text. (The bug that set this rule: tonal type-pills sprinkled
|
|
489
|
+
through combobox option rows.)
|
|
458
490
|
|
|
459
|
-
|
|
460
|
-
2. **`Badge variant="dot"`** — a categorical STATE in a scannable column / legend.
|
|
461
|
-
3. **`Badge`** (tonal pill, the default) — a record's PROMINENT status: a drawer/detail header.
|
|
491
|
+
When it IS a status, match weight to prominence — lightest → heaviest:
|
|
462
492
|
|
|
463
|
-
|
|
464
|
-
|
|
493
|
+
1. **Colored `Text`** — a verdict WORD in a header (the valence tokens above).
|
|
494
|
+
2. **`Badge variant="dot"` (the DEFAULT)** — a colored dot + label; a categorical STATE in a
|
|
495
|
+
scannable column / legend / inline. Reach for this by default; a bare `<Badge>` is `dot`.
|
|
496
|
+
3. **`Badge variant="tonal"`** — a filled pill, the HEAVY exception: the ONE prominent status of
|
|
497
|
+
a surface (a drawer/detail header). Opt IN explicitly and sparingly; one per view, never
|
|
498
|
+
sprinkled through rows.
|
|
499
|
+
|
|
500
|
+
A register's dense rows read lighter — the row's primary status is `dot`, its drawer twin may go
|
|
501
|
+
tonal. A `Badge` is never a metric value.
|
|
465
502
|
|
|
466
503
|
## Typography
|
|
467
504
|
|
package/docs/data_entry.md
CHANGED
|
@@ -13,13 +13,14 @@ in [the templates](./templates.md) (`examples/tpl_*.tsx`).
|
|
|
13
13
|
| You're capturing… | Reach for | Why |
|
|
14
14
|
|---|---|---|
|
|
15
15
|
| an EXISTING record's fields | [**Inline edit**](#inline-edit--the-preferred-way-to-edit-an-existing-record) (`Inline*`) | edit in place, no form mode |
|
|
16
|
+
| a value in a DENSE ROW / register `DataGrid` cell (a task's due · assignee · status) | the SAME `Inline*` editor in **`variant="cell"`** | transparent at rest + a background-tint wash on hover (not the form's chip + border), value-first (the date drops its calendar glyph) — one editor per value type, `variant` picks form vs grid; there is no separate `*Cell` family |
|
|
16
17
|
| a brand-NEW record | **create-then-refine** (`tpl_record`: "New" is ONE CLICK → a fresh Draft; everything refines in place on the record surface) | nobody fills 5 sections in one sitting; the surface is the editor |
|
|
17
18
|
| a RELATED record (pick or make) | [**find-or-create**](#find-or-create--the-combobox-family-is-the-control) (`Combobox allowCustom`) | one control covers both |
|
|
18
19
|
| REPEATING rows you build & revise | [**line items**](#line-items--create--preview--edit-a-composition-not-a-primitive) (create→preview→edit) | add / edit / remove, live totals |
|
|
19
20
|
| CHARGES that bill onto documents | [**billing**](#billing--the-invoice-document-is-the-unit) (`tpl_record` Billing section) | the invoice document is the unit |
|
|
20
21
|
| a record's FEE/charge SUMMARY | [**`Ledger`**](#fee-summary--ledger) (worked example: `tpl_item_list` drawer) | grouped money lines, one emphasized total — no bars/charts |
|
|
21
22
|
| a multi-value TAG field | [**`Select multi`**](#tag--multi-value-field--select-multi) (`renderSelected` → `Chip`) | chips composed, not a separate control |
|
|
22
|
-
| ONE choice from a small visible set | **`ChipGroup` pills** (or `RadioPicker` — column when options carry descriptions
|
|
23
|
+
| ONE choice from a small visible set | **`ChipGroup` pills** (or `RadioPicker` — a stacked column of full-width choices, right when options carry descriptions) | required single-select, one tap, every option visible |
|
|
23
24
|
| ONE pick from a BIG registry (ports, projects…) | on a RECORD surface: **`InlineSelect searchable`** (search lives in the popover); on a form/attach surface: **`Combobox`** with `value` | an inline row stays an inline editor — a persistent search input breaks the surface's grammar |
|
|
24
25
|
| a YES/NO on a record | **`CheckboxInput`** as the `DetailRow` value | a boolean needs no editor mode |
|
|
25
26
|
| a STATUS with terminal outcomes | [**disposition**](#disposition--lifecycle-status-is-asymmetric-by-phase) (open → resolve → revise) | guides the decision |
|
|
@@ -71,7 +72,7 @@ calendar popover stays the pointer path (click the resting value, as ever) — i
|
|
|
71
72
|
the only path. A PARTIAL entry never commits and never clears the stored value: the field
|
|
72
73
|
stays in edit mode showing the `datePicker.invalidDate` inline error until fixed or Escaped.
|
|
73
74
|
|
|
74
|
-
The POPOVER editors (`InlineSelect` / `InlineMemberSelect`
|
|
75
|
+
The POPOVER editors (`InlineSelect` / `InlineMemberSelect`) deliberately
|
|
75
76
|
do NOT auto-open on focus — a popup opening on Tab arrival traps traversal; per the WAI-ARIA
|
|
76
77
|
combobox contract they open on Enter/Space from the keyboard.
|
|
77
78
|
|
|
@@ -94,16 +95,16 @@ One per type:
|
|
|
94
95
|
ADD/REMOVE a time — the value's own shape, date vs datetime, is the source of truth
|
|
95
96
|
(`optionalTime` is ignored when `format="datetime"`).
|
|
96
97
|
- **`InlineTimePicker`** — a time-only value.
|
|
97
|
-
- **`
|
|
98
|
-
render as badges inside the standard chip, clicking floats
|
|
99
|
-
CLOSING commits the new set in one `onSave` — never a borderless `Select` posing as an inline
|
|
98
|
+
- **`InlineSelect multi`** — the MULTI mode of `InlineSelect` (there's no separate tag component):
|
|
99
|
+
a tag SET (`value: T[]`), selected tags render as badges inside the standard chip, clicking floats
|
|
100
|
+
a multi `OptionList` (checkbox rows), CLOSING commits the new set in one `onSave` — never a borderless `Select` posing as an inline
|
|
100
101
|
field.
|
|
101
102
|
|
|
102
103
|
They split across two shared shells. The same-height **input-swap** editors (`InlineTextInput`,
|
|
103
104
|
`InlineNumberInput`, `InlineTimePicker`) render **`InlineEditFrame`** driven by the
|
|
104
105
|
**`useInlineEdit`** hook (the view⇄edit toggle, draft buffer, and async save) — swap a custom
|
|
105
106
|
input into that pair to join the family. The **popover** editors (`InlineSelect`,
|
|
106
|
-
`InlineMemberSelect`, `InlineDatePicker
|
|
107
|
+
`InlineMemberSelect`, `InlineDatePicker`) instead compose **`InlineEditView`**
|
|
107
108
|
(the resting box, wired as the popover trigger) with their OWN `useState` for open/draft/saving/
|
|
108
109
|
error — a pick, not a keystroke, is the edit, so `useInlineEdit` doesn't fit. `onSave` is async
|
|
109
110
|
either way: the saving spinner sits INSIDE the control at its right edge (never a sibling — that
|
|
@@ -118,7 +119,7 @@ list's active-highlight and doubles the footer hairline), and never a persistent
|
|
|
118
119
|
cell (noise on a dense board, and a pressable nested in the trigger is invalid DOM). `onSave`'s
|
|
119
120
|
`next` stays non-null — a caller opts in per field; the clear fires `onClear`, which writes `null`
|
|
120
121
|
(the app workflow must ACCEPT null on that input — a `select`/`date`/`member` field clears on
|
|
121
|
-
null). `
|
|
122
|
+
null). `InlineSelect multi` needs no `onClear` — an empty set is already a valid `onSave`.
|
|
122
123
|
|
|
123
124
|
### The row stack — `DetailTable` + `DetailRow`
|
|
124
125
|
|
|
@@ -127,19 +128,20 @@ A STACK of rows lives in a `DetailTable` (label · value · trailing laid out li
|
|
|
127
128
|
zinc-50 chips need) holding `DetailRow`s — set `trailingWidth` when ANY row carries a trailing
|
|
128
129
|
action/badge, so EVERY row reserves the column and one row's `Copy` button never makes its editor
|
|
129
130
|
narrower than its neighbours'. Rows share ONE alignment law: the row top-aligns and label ·
|
|
130
|
-
control · trailing each center within the first control line — annotations (
|
|
131
|
-
the value
|
|
132
|
-
grow the row downward without dragging the label. The full annotation
|
|
133
|
-
composition.md §Field annotations.
|
|
131
|
+
control · trailing each center within the first control line — annotations (the three tones
|
|
132
|
+
under the value: `description` muted · `warning` amber · `error` danger, each indented to the
|
|
133
|
+
chip's 8px text inset) grow the row downward without dragging the label. The full annotation
|
|
134
|
+
vocabulary lives in composition.md §Field annotations.
|
|
134
135
|
|
|
135
136
|
### The editability affordance
|
|
136
137
|
|
|
137
|
-
|
|
138
|
-
without hovering
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
is the
|
|
138
|
+
A `variant="form"` editor (the default) AT REST sits on a zinc-50 chip — THE editability affordance:
|
|
139
|
+
users see what's editable without hovering, and it hovers via a BORDER (an input's edge). Keep it
|
|
140
|
+
wherever editable and static values MIX (a `DetailTable` / record field). `variant="cell"` opts out
|
|
141
|
+
of the chip — transparent at rest + a background-tint WASH on hover — for DENSE, uniformly-editable
|
|
142
|
+
surfaces (a register/`DataGrid` column, a task row where EVERY cell edits: the chip repeated
|
|
143
|
+
everywhere is noise, and the wash matches the row/cell it sits in). A `disabled` editor rests FLAT
|
|
144
|
+
automatically — the chip is the editability promise, and an inert field must not make it.
|
|
143
145
|
|
|
144
146
|
A READ-ONLY value in the same column — a computed total, a system ID, a synced/locked field — is
|
|
145
147
|
**`InlineStatic`**: it copies the editor box metrics exactly (height, padding, 1px transparent
|