@lotics/ui 22.2.0 → 23.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 +9 -2
- package/MIGRATION.md +142 -2
- package/docs/ai_patterns.md +27 -25
- package/docs/catalog.md +119 -98
- package/docs/composition.md +134 -42
- package/docs/data_entry.md +67 -31
- package/docs/templates.md +86 -68
- package/examples/tpl_allocate.tsx +5 -5
- package/examples/tpl_attendance.tsx +2 -2
- package/examples/tpl_calendar.tsx +6 -6
- package/examples/tpl_dashboard.tsx +9 -9
- package/examples/tpl_dieline.tsx +2 -2
- package/examples/tpl_item_list.tsx +64 -37
- package/examples/tpl_lookup.tsx +5 -5
- package/examples/tpl_pick.tsx +6 -6
- package/examples/tpl_pivot.tsx +3 -3
- package/examples/tpl_record.tsx +919 -611
- package/examples/tpl_report.tsx +2 -2
- package/examples/tpl_rollup.tsx +5 -5
- package/examples/tpl_shifts.tsx +5 -5
- package/examples/tpl_statements.tsx +7 -7
- package/examples/tpl_stock.tsx +2 -2
- package/examples/tpl_task_board.tsx +42 -26
- package/examples/tpl_tower.tsx +6 -6
- package/package.json +3 -2
- package/src/agent_run.tsx +40 -25
- package/src/breakdown.tsx +1 -1
- package/src/calendar/calendar_view.tsx +1 -1
- package/src/change_review.tsx +9 -8
- package/src/chip_group.tsx +12 -2
- package/src/choice_list.tsx +2 -2
- package/src/confidence.tsx +2 -2
- package/src/data_grid.tsx +1 -1
- package/src/detail_row.tsx +50 -58
- package/src/file_dropzone.tsx +1 -1
- package/src/file_gallery_modal.tsx +3 -3
- package/src/file_rows.tsx +1 -1
- package/src/finding.tsx +4 -4
- package/src/form_field.tsx +1 -1
- package/src/format_date.ts +2 -2
- package/src/heatmap.tsx +1 -1
- package/src/inline_button.tsx +84 -0
- package/src/inline_date_picker.tsx +17 -10
- package/src/inline_edit.tsx +298 -59
- package/src/inline_member_select.tsx +8 -3
- package/src/inline_number_input.tsx +11 -4
- package/src/inline_select.tsx +26 -13
- package/src/inline_text_input.tsx +12 -4
- package/src/inline_time_picker.tsx +10 -4
- package/src/ledger.tsx +2 -2
- package/src/locale.tsx +7 -3
- package/src/matrix.tsx +1 -1
- package/src/number_input.tsx +18 -7
- package/src/pipeline.tsx +1 -1
- package/src/popover.tsx +1 -1
- package/src/press_door.tsx +1 -1
- package/src/pressable_highlight.tsx +1 -1
- package/src/progress_bar.tsx +3 -3
- package/src/record_summary.tsx +2 -2
- package/src/result_header.tsx +2 -2
- package/src/sequence.tsx +170 -0
- package/src/share_or_download.ts +2 -2
- package/src/step_progress.tsx +7 -5
- package/src/stepper.tsx +1 -1
- package/src/task.tsx +6 -6
- package/src/text_input_field.tsx +19 -1
- package/src/text_utils.ts +1 -1
- package/src/linked_record_box.tsx +0 -157
package/docs/composition.md
CHANGED
|
@@ -44,8 +44,9 @@ restyle a heading level per-page.
|
|
|
44
44
|
numbers) + optional `CardHeaderMeta` (a count/unit/period, xs muted tabular).
|
|
45
45
|
- **Section title** — ONE construct: `Section` › `SectionHeading` › `SectionHeadingTitle`
|
|
46
46
|
(`##` — xl semibold, optional muted `description` and `info` popover), everything left-aligned
|
|
47
|
-
at the column edge; `SectionHeadingMeta` and
|
|
48
|
-
title's `flex: 1` pushes them)
|
|
47
|
+
at the column edge; `SectionHeadingMeta` and any VIEW control ride the heading row's right edge
|
|
48
|
+
(the title's `flex: 1` pushes them) — but a verb that CHANGES the content does not: see the
|
|
49
|
+
action-placement law below. The page column is a **`SectionStack`** — it owns the
|
|
49
50
|
between-section law (a fixed 56px beat + a bare hairline separating one section from the NEXT;
|
|
50
51
|
null/false children are skipped so a conditional section never leaves a stray hairline). The
|
|
51
52
|
`Divider` NEVER goes directly under a heading — that orphans the title from its own content.
|
|
@@ -317,7 +318,7 @@ card): `<PressableRow onPress>` + `<PressDoor onPress accessibilityLabel="Open
|
|
|
317
318
|
content on `zIndex: 1` (the door hit-tests above in-flow siblings, so the content must lift back
|
|
318
319
|
over it). `PressableHighlight` is the wrong base for such a row — it IS a button and wraps its
|
|
319
320
|
children, so any nested control lands inside it; it stays for surfaces whose content is
|
|
320
|
-
non-interactive by construction. `TableRow`
|
|
321
|
+
non-interactive by construction. `TableRow` already composes `PressDoor`.
|
|
321
322
|
|
|
322
323
|
Actions stay **visible** — a hover-only action is invisible to keyboard and touch users. The ONLY
|
|
323
324
|
exception is a DENSE tree/register where a persistent per-row action would clutter: reveal on
|
|
@@ -424,11 +425,10 @@ All view controls are 40px tall (`CONTROL_HEIGHT`), `sm` labels, in ONE wrapping
|
|
|
424
425
|
`danger` (and its quieter `danger-secondary`) marks destructive — that's the whole axis. No
|
|
425
426
|
"success"/green button. Decision UIs put positive/negative color on the STATUS (dot) and verdict
|
|
426
427
|
(colored `Text`), not the buttons.
|
|
427
|
-
- **Create buttons carry NO `+` icon** — "Add fee", not "+ Add fee".
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
its glyph.)
|
|
428
|
+
- **Create buttons carry NO `+` icon** — "Add fee", not "+ Add fee". A create is `primary` and
|
|
429
|
+
sits BELOW the list it extends, never in the section heading — see the verb-placement law for
|
|
430
|
+
why, and for the one-verb-one-weight rule that keeps it primary in the `EmptyState` too. (The
|
|
431
|
+
icon-only `+` `IconButton` — a composer's attach — is a different thing and keeps its glyph.)
|
|
432
432
|
|
|
433
433
|
## The action-gating law — disabled + the reason as a co-located Callout
|
|
434
434
|
|
|
@@ -456,57 +456,148 @@ gate's SCOPE, once** — never prose beside the button, never revealed only on p
|
|
|
456
456
|
- **The field-annotation vocabulary** — THREE tones, same names + meanings on `FormField` and
|
|
457
457
|
`DetailRow`, always EXPLICIT (never a hidden ⓘ on rows): **`description`** (info, muted) = a
|
|
458
458
|
content/domain fact or persistent guidance, under the VALUE on a horizontal row — the label
|
|
459
|
-
column has no room for prose; stacked mode wears the form order label
|
|
459
|
+
column has no room for prose; stacked mode wears the form order label, description, control;
|
|
460
460
|
**`warning`** (amber) = a consequence the user should weigh before acting, announced on
|
|
461
461
|
appearance; **`error`** (danger + alert semantics) = an invalid state under the value (the
|
|
462
462
|
`Inline*` editors already render their own transient save errors); cross-field / record-level
|
|
463
463
|
STATE = a co-located `Callout`. Every tone is USEFUL AND TERSE — a short clause stating the
|
|
464
464
|
fact or consequence, never widget mechanics, never a second sentence; `warning` marks a
|
|
465
465
|
consequence, not decoration. Rows share ONE
|
|
466
|
-
alignment law: the row top-aligns and label
|
|
466
|
+
alignment law: the row top-aligns and label, control, trailing each center within the
|
|
467
467
|
first control line — a tall value block never drags the label, and a LABEL too long for its
|
|
468
468
|
column WRAPS (never clipped, never abbreviated to fit) with its first line still level with
|
|
469
469
|
the value. A FLAT value row
|
|
470
470
|
(`InlineStatic`, plain `Text`) sets **`DetailRow flat`**: the annotation tucks up by the
|
|
471
471
|
control band's slack so the perceived gap under the text equals the gap under a chip —
|
|
472
472
|
the band's invisible bottom half must never read as a hole above the description.
|
|
473
|
-
- **A
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
473
|
+
- **A verb about a VALUE goes ON the value — an `InlineButton` in the field's `actions`.** Copy
|
|
474
|
+
this reference, call this number, open this record: the act names the value, so it travels
|
|
475
|
+
with what it acts on, and a reader never has to pair a control sitting at the page's right
|
|
476
|
+
edge with the field it belongs to. `DetailRow` has NO trailing slot, because a reserved
|
|
477
|
+
column is paid by every row for the benefit of one — it was declared on the TABLE, so a
|
|
478
|
+
single `Copy` made every value cell stop short of an edge nothing occupied. EVERY `Inline*`
|
|
479
|
+
editor takes `actions`, popover-backed ones included — a field exposes its outer box
|
|
480
|
+
separately from the control inside it, so a select's list and a date's calendar still open at
|
|
481
|
+
the field's own width. Anything that is not a verb — a status `Badge`, a unit — composes into
|
|
482
|
+
the value cell instead.
|
|
483
|
+
`actions` renders in BOTH view and edit mode, and it must be passed UNCONDITIONALLY —
|
|
484
|
+
`disabled` the verb when it has nothing to act on. A slot that appears once the value is
|
|
485
|
+
non-empty changes the field's width as the user types, which is the one thing an inline
|
|
486
|
+
editor promises never to do.
|
|
487
|
+
- **What is about the WHOLE record is a PANEL, not a section.** A section takes a position in a
|
|
488
|
+
top-to-bottom reading order, which is a claim that it belongs at that point in the read. A
|
|
489
|
+
record's DISCUSSION does not: a note is written about anything on the page and is wanted while
|
|
490
|
+
you work anywhere on it, so parked at one position it is unreachable from every other — exactly
|
|
491
|
+
when it matters. Dock it in the page's right gutter (the one the outline rail's balance already
|
|
492
|
+
reserved), pinned, scrolling inside itself so a long thread never lengthens the record. Give it
|
|
493
|
+
no rail entry — it is not a place you navigate to. Narrow, it collapses onto the same bar the
|
|
494
|
+
sections use, on the RIGHT: sections are where you ARE, the discussion is what is being SAID,
|
|
495
|
+
and the two poles keep the sides they hold on a wide screen.
|
|
496
|
+
- **A VARIABLE-LENGTH, ORDERED fact is a `Sequence`, not a row of fields.** Before reaching for
|
|
497
|
+
any layout lever, check the section is not carrying a MODELLING error: `Origin` /
|
|
498
|
+
`Transfer point` / `Destination` looks like three fields and is actually one route — it cannot
|
|
499
|
+
hold a second transfer, and three identical rows say nothing about which comes first. The same
|
|
500
|
+
goes for an approval chain, a set of legs, a list of signatories. Rendered as a `Sequence` it
|
|
501
|
+
gains the one thing the grid could never give it — visible ORDER — and stops competing with the
|
|
502
|
+
fields around it, because it is no longer pretending to be one.
|
|
503
|
+
Nothing about spacing or disclosure fixes a section holding the wrong SHAPE, and a section that
|
|
504
|
+
looks "unattended" is usually this rather than a density problem.
|
|
505
|
+
- **A VERB GOES WHERE ITS EFFECT LANDS.** One law places every action on a record surface, and
|
|
506
|
+
it is the same one that decides `InlineButton` vs a trailing column:
|
|
507
|
+
|
|
508
|
+
| the act changes… | it lives… |
|
|
509
|
+
|---|---|
|
|
510
|
+
| a VALUE (copy it, open what it points at) | ON the value — `InlineButton` in the editor's `actions` |
|
|
511
|
+
| a ROW (remove it, move it) | ON the row — an `ActionMenu`, or the row's own controls |
|
|
512
|
+
| the LIST (one more of them) | BELOW the list, left-aligned, `primary` |
|
|
513
|
+
| the RECORD (issue, print, hand off) | its own action row at the section's END |
|
|
514
|
+
| the record's EXISTENCE | the `DangerZone`, last on the page |
|
|
515
|
+
|
|
516
|
+
The heading row is therefore identity + meta + view controls, and holds no content verb. An
|
|
517
|
+
**Add in the heading sits above the thing it extends**, at a different altitude from the rows
|
|
518
|
+
it affects, competing with the title for the one line that names the section — while the row it
|
|
519
|
+
creates appears at the BOTTOM, nowhere near the button that made it. Put it under the last row,
|
|
520
|
+
on the list's own left edge, and the button is where its result will be.
|
|
521
|
+
**An add is `primary`, in the `EmptyState` too.** It is the act its section offers, so it
|
|
522
|
+
carries that section's weight; a lone `secondary` button reads as though the real action were
|
|
523
|
+
somewhere else. The rule that matters is that ONE VERB KEEPS ONE WEIGHT — the same add must
|
|
524
|
+
not be primary on an empty list and secondary on a full one.
|
|
525
|
+
A record surface therefore carries several primaries, one per section, and that is correct:
|
|
526
|
+
"one primary per SCREEN" governs the page BAND, not a stack of independent sections each with
|
|
527
|
+
its own single act. The cost is real and worth knowing — the more sections offer an act, the
|
|
528
|
+
less any one of them stands out — so the answer to a loud page is FEWER SECTIONS WITH ACTS,
|
|
529
|
+
never a quieter button.
|
|
530
|
+
- **A DENSE section is compacted by a SUMMARY above it, by ROW HEIGHT, and by grouping fields
|
|
531
|
+
that are ONE ENTITY — never by a two-column layout.** The evidence is unusually clear here, so
|
|
532
|
+
do not re-derive it: Baymard finds an overall two-column form layout "generally problematic and
|
|
533
|
+
never recommended" (empty columns pull attention in several directions; fields get skipped), and
|
|
534
|
+
NN/g measures single-column completion materially faster. What IS sanctioned is 2–3 inputs on
|
|
535
|
+
ONE line **when they logically belong to a single coherent entity** — their examples are
|
|
536
|
+
day/month/year, first/middle/last name, city/state/ZIP. A route (origin, via, destination) is
|
|
537
|
+
that; two unrelated short fields sharing a line to save height is not.
|
|
538
|
+
**But that exception belongs to the LABEL-ABOVE form grammar, not to a `DetailTable`.** Every
|
|
539
|
+
example of it (day/month/year, city/state/ZIP) sits in a form where each input already owns a
|
|
540
|
+
label on its own line. A `DetailRow` puts the label in a LEFT COLUMN, so a grouped row can only
|
|
541
|
+
name its parts by adding a sub-label line no other row has — which drops its controls off the
|
|
542
|
+
baseline every other row shares — and naming them with placeholders loses the names the moment
|
|
543
|
+
the row is filled. In a label-left grid, ONE FIELD PER ROW is not a failure to compact; it is
|
|
544
|
+
the only shape that keeps the column a reader scans. Both attempts are recorded here so nobody
|
|
545
|
+
re-runs them.
|
|
546
|
+
The other caveat: that research studies COMPLETION forms, where the failure mode is a skipped
|
|
547
|
+
required field, while a record page is read-mostly.
|
|
548
|
+
A SUMMARY belongs to the RECORD, not to a section. The highlights-panel precedent (a strip of
|
|
549
|
+
key facts above the detail) is a PAGE-level device, where what it summarises is a scroll or a
|
|
550
|
+
tab away; repeated per section it sits inches above the very rows it repeats, so it reads as
|
|
551
|
+
duplication and adds a third block of chrome — section title, description, strip — before any
|
|
552
|
+
data. Put the few facts someone would ask for by phone in the record's own header, once.
|
|
553
|
+
A read-only view behind an Edit button is the last resort: it compacts by DROPPING empty
|
|
554
|
+
fields — and an empty row is information, the difference between "this record has no vehicle"
|
|
555
|
+
and "this section has no vehicle field" — and it costs a MODE, against the law that the surface
|
|
556
|
+
IS the editor.
|
|
557
|
+
- **A REFERENCE to another record is a FIELD, not a section and not a card.** The kit already
|
|
558
|
+
draws every other reference as a token — a person is a `MemberChip`, a file a `FileBadge`, a
|
|
559
|
+
select option an `OptionBadge` — and a record is no different. It renders as a VALUE wearing
|
|
560
|
+
the **inline editor's own resting surface** (the 40px band, the white bordered chip, the radius), so
|
|
561
|
+
a pointer at another record sits in the value column exactly like the editors above and below
|
|
562
|
+
it. Put BOTH the name and its code INSIDE it — one entity reads as one object, and a muted
|
|
563
|
+
code beside a link reads as an orphan.
|
|
564
|
+
**THREE acts, placed by how often they happen and what they cost to undo**: pressing the
|
|
565
|
+
surface PEEKS the facts (free, so it gets the biggest target); an **`InlineButton`** Open sits
|
|
566
|
+
INSIDE on the right (the common act, so it stays in sight and stays labelled — a press target
|
|
567
|
+
you discover by hovering is not an affordance); and DETACH lives in the peek's footer
|
|
568
|
+
(destructive and rare, and it is the answer to the question the peek asks — "is this the right
|
|
569
|
+
one?" — so the check and the correction are one gesture). A detach link repeated down a column
|
|
570
|
+
of references is noise in the scan path for an act most readers never perform.
|
|
571
|
+
The interior verb REQUIRES the anatomy a container-with-controls always requires: a role-less,
|
|
572
|
+
non-focusable surface (a button must never contain a button), an empty `PressDoor` sibling
|
|
573
|
+
carrying the tab stop / name / focus ring, and the verb lifted above it on `zIndex`. Track
|
|
574
|
+
hover on the container itself, or react-native-web releases it to the innermost pressable and
|
|
575
|
+
the border flickers out as the pointer crosses the verb.
|
|
576
|
+
**Never promote it to a Section.** A section claims a distinct AREA of the record and buys an
|
|
577
|
+
outline-rail entry; one pointer at one other row never earns that. What tempts the promotion is
|
|
578
|
+
the machinery around it — and each piece is just part of the field: the search is its EMPTY
|
|
579
|
+
state, the create dialog is a modal, the validity problem is the row's `warning`.
|
|
580
|
+
**Never render the other record's fields as rows on this one** either — the reference carries
|
|
581
|
+
IDENTITY; any fact THIS record actually uses (a tax id that gates invoicing) is this record's
|
|
582
|
+
own field or the row's annotation, and everything else belongs on the record you open. When
|
|
583
|
+
the referenced record has NO surface of its own, it has no identity to reference: its values
|
|
584
|
+
simply ARE this record's fields — put them in a `Subsection`, with no box around them.
|
|
488
585
|
**The empty / attach state is a find-or-create `Combobox`, never a bare picker** (`tpl_record`'s
|
|
489
586
|
Customer is the worked example): rows are `renderOptionContent`-rich, NOT a wall of bare names —
|
|
490
|
-
the premium shape is a **glyph tile (28
|
|
491
|
-
that carries the type + identifying fact as TEXT (`type
|
|
492
|
-
wrap the row taller. A type/attribute is **NOT a status, so it is text, never a `Badge`** (see
|
|
493
|
-
Badge discipline below). The
|
|
494
|
-
kind-glyph. (The kit suppresses `renderOptionContent` on the `allowCustom` "Create …" row so it keeps its plain + look.)
|
|
587
|
+
the premium shape is a **glyph tile (28, radius 7, zinc-100) beside the name, over a muted metadata
|
|
588
|
+
line** that carries the type + identifying fact as TEXT (`type, code, city`) — COMMA-joined, never
|
|
589
|
+
a middot (see Microcopy), and free to wrap the row taller. A type/attribute is **NOT a status, so it is text, never a `Badge`** (see
|
|
590
|
+
Badge discipline below). (The kit suppresses `renderOptionContent` on the `allowCustom` "Create …" row so it keeps its plain + look.)
|
|
495
591
|
Plus `allowCustom` + `customOptionPlacement:"top"` + `customOptionLabel={q => 'Create "…"'}`. In
|
|
496
592
|
`onValueChange`, an existing pick attaches; the custom row opens a create **`Dialog` prefilled with
|
|
497
593
|
the typed query** (mint the record + link it) — so a party absent from the registry is created
|
|
498
594
|
WITHOUT leaving the record. `customOptionPlacement:"top"` keeps the create row visible while the
|
|
499
595
|
keyboard highlight stays on the first MATCH, so Enter on a partial never makes a duplicate. A
|
|
500
|
-
linked record seeded by a workflow (not user-attached) skips the picker — show the
|
|
501
|
-
`EmptyState`, no create affordance.
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
the interior verbs lifted above it via `zIndex: 1`. NEVER `role="button"` on a container with
|
|
506
|
-
interactive descendants: a button must not contain a button (invalid HTML, a hydration error,
|
|
507
|
-
and AT double-announcement). That fenced, lifted footer is the ONLY place a control may sit
|
|
508
|
-
inside a door box — a control in the facts column would compete with the press for the same
|
|
509
|
-
click, which is why a node fact typechecks only on the doorless variant.
|
|
596
|
+
linked record seeded by a workflow (not user-attached) skips the picker — show the chip or an
|
|
597
|
+
`EmptyState`, no create affordance.
|
|
598
|
+
**One verb set across every reference of a group** (a party wall's five links all read "Gỡ", not
|
|
599
|
+
a mix of "Đổi"/"Gỡ"): with a clearable link, changing = remove → the empty-state picker
|
|
600
|
+
re-prompts, so the destructive verb covers both required anchors and optional adds.
|
|
510
601
|
|
|
511
602
|
## Color discipline — solid / tint / ramp + ONE accent
|
|
512
603
|
|
|
@@ -652,7 +743,7 @@ that praises it is marketing.
|
|
|
652
743
|
|
|
653
744
|
### Error messages — what happened → what it means → what to do
|
|
654
745
|
|
|
655
|
-
The consensus anatomy (GOV.UK
|
|
746
|
+
The consensus anatomy (GOV.UK, NN/g, Microsoft): state what happened, what it means for the
|
|
656
747
|
user's data, and the one next step. Field errors go INLINE (`FormField error=`); a page-level
|
|
657
748
|
failure is a `Callout tone="error"` (title = what happened, text = what to do, an action when
|
|
658
749
|
one exists).
|
|
@@ -663,7 +754,8 @@ one exists).
|
|
|
663
754
|
- Empty required field → instruct ("Enter the hours you work a week"); constraint violation →
|
|
664
755
|
describe the constraint ("First name must be 35 characters or less").
|
|
665
756
|
- Apology is reserved for genuine system failure — never for validation.
|
|
666
|
-
- BAD: `Error: invalid input (0x643).`
|
|
757
|
+
- BAD: `Error: invalid input (0x643).`
|
|
758
|
+
GOOD: `Không tải được chứng từ lên — tệp vẫn còn trên
|
|
667
759
|
máy bạn. Thử lại; nếu vẫn lỗi, kiểm tra kết nối mạng.`
|
|
668
760
|
|
|
669
761
|
### Empty states — confirm, orient, one action
|
package/docs/data_entry.md
CHANGED
|
@@ -13,14 +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
|
|
16
|
+
| a value in a DENSE ROW / register `DataGrid` cell (a task's due, assignee, status) | the SAME `Inline*` editor in **`variant="bare"`** | one editor per value type; `bare` drops the resting frame (the grid already promises every cell edits) and fades the same border in on hover — there is no separate `*Cell` family |
|
|
17
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 |
|
|
18
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 |
|
|
19
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 |
|
|
20
20
|
| CHARGES that bill onto documents | [**billing**](#billing--the-invoice-document-is-the-unit) (`tpl_record` Billing section) | the invoice document is the unit |
|
|
21
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 |
|
|
22
22
|
| a multi-value TAG field | [**`Select multi`**](#tag--multi-value-field--select-multi) (`renderSelected` → `Chip`) | chips composed, not a separate control |
|
|
23
|
-
| ONE choice from a small visible set | **`ChipGroup` pills**
|
|
23
|
+
| ONE choice from a small visible set | [**`ChipGroup` pills**](#choosing-a-choice-control--by-option-count-not-by-taste) — more options or long labels take `InlineSelect`; `RadioPicker` only where the choice IS the task | one tap, every option visible, and the row stays ONE band |
|
|
24
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 |
|
|
25
25
|
| a YES/NO on a record | **`CheckboxInput`** as the `DetailRow` value | a boolean needs no editor mode |
|
|
26
26
|
| a STATUS with terminal outcomes | [**disposition**](#disposition--lifecycle-status-is-asymmetric-by-phase) (open → resolve → revise) | guides the decision |
|
|
@@ -154,13 +154,12 @@ null). `InlineSelect multi` needs no `onClear` — an empty set is already a val
|
|
|
154
154
|
|
|
155
155
|
### The row stack — `DetailTable` + `DetailRow`
|
|
156
156
|
|
|
157
|
-
A STACK of rows lives in a `DetailTable` (label
|
|
158
|
-
`
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
under the value: `description` muted · `warning` amber · `error` danger, each indented to the
|
|
157
|
+
A STACK of rows lives in a `DetailTable` (label, value laid out like a TABLE: `labelWidth` /
|
|
158
|
+
`minHeight` set ONCE on the parent, plus the `SPACE.md` row gap bordered fields need — a border
|
|
159
|
+
is a harder edge than the old tint, so rows 8px apart fused) holding `DetailRow`s. There are TWO columns and no third: the value FILLS whatever the label leaves, so
|
|
160
|
+
nothing one row does can narrow its neighbours' editors. Rows share ONE alignment law: the row
|
|
161
|
+
top-aligns and label, control each center within the first control line — annotations (the three tones
|
|
162
|
+
under the value: `description` muted, `warning` amber, `error` danger, each indented to the
|
|
164
163
|
chip's 8px text inset) grow the row downward without dragging the label. The full annotation
|
|
165
164
|
vocabulary lives in composition.md §Field annotations. A long LABEL wraps inside its column
|
|
166
165
|
instead of clipping — never shorten a field name to fit the label width, and never reach for a
|
|
@@ -168,23 +167,60 @@ tooltip to recover the tail; the wrapped label's first line stays level with the
|
|
|
168
167
|
|
|
169
168
|
### The editability affordance
|
|
170
169
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
170
|
+
An editor AT REST wears THE pill surface — WHITE with a 1px border, the same one `Chip`,
|
|
171
|
+
`ChipGroup` and a secondary `Button` wear. That is THE editability affordance: a read-only
|
|
172
|
+
`InlineStatic` stays flat and borderless, so users see what is editable without hovering, and a
|
|
173
|
+
bordered box beside bare text is a plainer signal than a tint. Hover marks the field you are about to act
|
|
174
|
+
on with a CHANGE you can see, which is why it differs by variant: a `framed` field deepens its
|
|
175
|
+
border AND tints (border alone would be zinc-200 → zinc-400, a shade shift on a 1px line), while a
|
|
176
|
+
`bare` field has the border ARRIVE out of nothing, which is loud enough by itself — and a tint
|
|
177
|
+
there would make a hovered cell lighter than the row washing under it. Open adds the 2px ring. A `disabled` editor rests FLAT and borderless
|
|
178
|
+
automatically — the surface is the editability promise, and an inert field must not make it.
|
|
179
|
+
|
|
180
|
+
**`variant` decides how much of that frame shows AT REST — an axis of WEIGHT, not of use.**
|
|
181
|
+
`"framed"` (default) keeps the surface visible: required wherever editable and static values MIX,
|
|
182
|
+
because the frame is the only thing saying which values you can change. `"bare"` shows nothing at
|
|
183
|
+
rest and fades the SAME border in on hover — for a surface where EVERY value is editable (a
|
|
184
|
+
register/`DataGrid` column, a task row), where a per-field frame states what the whole surface
|
|
185
|
+
already promises and, repeated down a column, draws the grid a second time. Both hover and open
|
|
186
|
+
identically, so this changes what a field looks like RESTING, never what it does. A hand-rolled
|
|
187
|
+
pressable cell should match `bare`: nothing at rest, the border on hover — never a background wash.
|
|
178
188
|
|
|
179
189
|
A READ-ONLY value in the same column — a computed total, a system ID, a synced/locked field — is
|
|
180
190
|
**`InlineStatic`**: it copies the editor box metrics exactly (height, padding, 1px transparent
|
|
181
191
|
border) but stays FLAT and non-interactive, so editable (chip) vs read-only (flat) is legible at a
|
|
182
192
|
glance and the static value never reads as a disabled input.
|
|
183
193
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
194
|
+
### Choosing a CHOICE control — by option count, not by taste
|
|
195
|
+
|
|
196
|
+
A field whose value is one-of-N has three answers, and the wrong one is what makes a record
|
|
197
|
+
surface look cluttered — not the number of rows. A row that is four rows TALL shouts over every
|
|
198
|
+
neighbour, and variation in row WEIGHT reads as noise long before row COUNT does.
|
|
199
|
+
|
|
200
|
+
| The choice | Reach for | Why |
|
|
201
|
+
|---|---|---|
|
|
202
|
+
| 2–3 SHORT labels, switched often | **`ChipGroup`** | every option visible, ONE press to switch; the pills size to their words and wrap, so the row stays one band |
|
|
203
|
+
| More options, or long labels, or the value is set once and read after | **`InlineSelect`** | scales to any count; costs two presses (open, pick) and hides the alternatives until you do — put each option's gloss in `renderOptionContent`, where it is needed while CHOOSING rather than on every later read |
|
|
204
|
+
| The choice IS the task — a wizard step, a short focused form | **`RadioPicker`** | full-width choices with their descriptions permanently visible. In a grid of many fields this is ~4 rows of height for one value; never use it as a row in a record's field table |
|
|
205
|
+
|
|
206
|
+
**`SegmentedControl` is NOT a field control.** It belongs to the view-control vocabulary (a mode
|
|
207
|
+
or parameter of the SAME view — see the composition grammar), and it fails as a value editor for
|
|
208
|
+
two concrete reasons: it is a bounded TRACK, so in a value column that fills the row it either
|
|
209
|
+
stretches absurdly wide for two options or needs a hand-picked width; and its track+thumb is a
|
|
210
|
+
different visual species from the chip surface every other editor on the page wears. A `ChipGroup`
|
|
211
|
+
is the same `chipSurfaceStyle` as the controls around it, sizes to its content, and grows by
|
|
212
|
+
wrapping instead of squeezing.
|
|
213
|
+
|
|
214
|
+
A VERB about the value rides the FIELD — an **`InlineButton`** in an editor's `actions` (`Copy`
|
|
215
|
+
on a reference, `Call` on a phone): the act names the value, so it travels with what it acts on
|
|
216
|
+
instead of being paired back to a control at the page's right edge. Pass it UNCONDITIONALLY and
|
|
217
|
+
`disabled` it when there is nothing to act on — a verb that appears once the value is non-empty
|
|
218
|
+
resizes the field as the user types, which is the one thing an inline editor promises never to
|
|
219
|
+
do. Anything that is not a verb — a status `Badge`, a unit ("kg"/"$", which belong IN the value
|
|
220
|
+
via `InlineNumberInput format`) — composes into a row inside the VALUE cell, so it costs only the
|
|
221
|
+
row that wants it. See the "General" section of `tpl_record`, which also reads top→bottom as a
|
|
222
|
+
full record surface. Not every field is a same-height swap — a tag field, a status, or an
|
|
223
|
+
attachment grid edit in place too (below).
|
|
188
224
|
|
|
189
225
|
## Fieldset form — fields lay out on a RESPONSIVE two-column grid
|
|
190
226
|
|
|
@@ -280,7 +316,7 @@ changing desks — never an inbox, a notification, or a copied task. Two types:
|
|
|
280
316
|
handoff is MANAGED AS TASKS — each desk's checklist on the record. The **`Task`** compound owns
|
|
281
317
|
the list geometry (the control column, the indent, and `density` — 44px targets by default) —
|
|
282
318
|
compose it, never hand-roll the row. A **`TaskItem`** carries a `TaskStatus` (a `CheckCircle` — a
|
|
283
|
-
read-only ring when it has no `onChange`), a `TaskTitle` (
|
|
319
|
+
read-only ring when it has no `onChange`), a `TaskTitle` (an `InlineTextInput`,
|
|
284
320
|
or the title TEXT as a string child — `<TaskTitle struck={done}>{label}</TaskTitle>` is the
|
|
285
321
|
read-only form, and the compound owns its inset and band, so never hand-roll a `Text` title) and
|
|
286
322
|
a `TaskActions` ⋯ menu — never a decorative progress strip. The fields the desk SETS (the
|
|
@@ -303,7 +339,7 @@ about one FIELD is not a caption either: it is that sub-row's own `description`
|
|
|
303
339
|
|
|
304
340
|
**THE ROW CARRIES THE TITLE; THE FIELDS THE USER CAN SET ARE SUB-ROWS BENEATH IT.** A field
|
|
305
341
|
belonging to ONE task — its due date, its assignee, a portal login, a reference number — hangs
|
|
306
|
-
under it as a **`TaskSubRow`** (`label
|
|
342
|
+
under it as a **`TaskSubRow`** (`label`, the control, `description` / `warning` / `error`),
|
|
307
343
|
indented ONE step, with the NAME beside its VALUE so the eye pairs them. Belonging is expressed by
|
|
308
344
|
that indentation, which is the same step a nested `TaskList` takes — one device, at every depth.
|
|
309
345
|
|
|
@@ -315,7 +351,7 @@ own words is not a column: it lined up the labels of a task whose names happened
|
|
|
315
351
|
staggered every other one, and a vocabulary with longer field names than English staggered nearly
|
|
316
352
|
all of them. Raise it once, on the list, for a vocabulary that genuinely needs more. The value
|
|
317
353
|
takes the slack from a readable minimum, dropping onto its own line under the label on a surface
|
|
318
|
-
too narrow to seat both. Use the
|
|
354
|
+
too narrow to seat both. Use the `Inline*` editor —
|
|
319
355
|
belongs to a grid.
|
|
320
356
|
|
|
321
357
|
**A TASK'S FIELD ANNOTATES EXACTLY LIKE A RECORD'S FIELD.** `TaskSubRow` carries the same
|
|
@@ -390,8 +426,8 @@ open tasks carry over. The handoff CTA opens a DIALOG for the receiving desk (as
|
|
|
390
426
|
the record left this register.
|
|
391
427
|
|
|
392
428
|
Tasks PEEK from the register: the done/total column is a pressable compact-`ProgressBar` trigger
|
|
393
|
-
whose popover holds the same `Task` list — every `TaskItem`'s ring `TaskStatus
|
|
394
|
-
`InlineTextInput` `TaskTitle` (flex)
|
|
429
|
+
whose popover holds the same `Task` list — every `TaskItem`'s ring `TaskStatus`, struck
|
|
430
|
+
`InlineTextInput` `TaskTitle` (flex), a quick-reassign `InlineMemberSelect` in a `TaskSubRow`
|
|
395
431
|
under it — no expandable rows (tags/files depth is
|
|
396
432
|
the Task list template's lesson, not the peek's); the popover body is `PopoverContent`'s own
|
|
397
433
|
ScrollView (`disableBodyScroll` is ONLY for children that manage their own scroll, like
|
|
@@ -431,7 +467,7 @@ create step, never a toggle on the record.
|
|
|
431
467
|
When charges get grouped into issuable documents (an e-invoice, a bill) and then collected, don't
|
|
432
468
|
split the screen into "enter fees here, issue there" — that smears one job across two places.
|
|
433
469
|
Make **each invoice a FLAT hairline-set band that holds its own editable charge lines** (amount
|
|
434
|
-
input + payment method), its **live total**, its **status badge** (nothing-to-bill
|
|
470
|
+
input + payment method), its **live total**, its **status badge** (nothing-to-bill, draft,
|
|
435
471
|
issued + ref), and its **issue action** in its closing row — no card chrome; data-capture
|
|
436
472
|
templates are flat (see [the composition grammar](./composition.md)). A charge never lives apart
|
|
437
473
|
from the document it bills on. Issuing is gated **inline, never a dead end**: when a prerequisite
|
|
@@ -446,7 +482,7 @@ receipt — never folded into the total due. Composition over `Card` + `NumberIn
|
|
|
446
482
|
## Fee summary — `Ledger`
|
|
447
483
|
|
|
448
484
|
For a record's fee/charge SUMMARY (read-heavy, one payment action), reach for the `Ledger`
|
|
449
|
-
compound: **`LedgerGroup`** (label + group sum) → **`LedgerRow`** (label
|
|
485
|
+
compound: **`LedgerGroup`** (label + group sum) → **`LedgerRow`** (label, `meta`, ONE right
|
|
450
486
|
tabular money column; `peek` makes the row a door to its PARTICULARS in an anchored popover —
|
|
451
487
|
references live INSIDE the peek, since a row is never a button holding another button (`reference`
|
|
452
488
|
is ignored while `peek` is set); `reference` = a trailing link on a non-peek row — an issued
|
|
@@ -597,8 +633,8 @@ more" affordance — only the empty state leads with it).
|
|
|
597
633
|
|
|
598
634
|
`FileGrid` is the upload-aware grid: `files` are the saved/completed `DisplayFile`s, `uploads` is
|
|
599
635
|
the LIVE add-queue (`FileUpload[]`) — it interleaves both and renders each in-flight tile itself —
|
|
600
|
-
a LABELED status overlay (uploading spinner
|
|
601
|
-
button
|
|
636
|
+
a LABELED status overlay (uploading spinner, "Retrying", "Paused", "Upload failed" + a retry
|
|
637
|
+
button, "Can't upload" for a dead/empty file) — so you never hand-map an upload to a
|
|
602
638
|
`FileThumbnail`. Those words resolve **prop → `LoticsLocale.fileUpload` → nothing** — set the
|
|
603
639
|
pack once at the root and a stalled or dead upload speaks the app's language everywhere,
|
|
604
640
|
including inside `FilesEditor`, which has no per-instance way to reach them. `labels.upload` /
|
|
@@ -606,7 +642,7 @@ including inside `FilesEditor`, which has no per-instance way to reach them. `la
|
|
|
606
642
|
|
|
607
643
|
Make it CRUDable by wiring its callbacks: `onFilePress` → set a `number|null` index that drives
|
|
608
644
|
`<FileGalleryModal files activeIndex onIndexChange>` — a FULL-SCREEN viewer with a toolbar
|
|
609
|
-
(filename
|
|
645
|
+
(filename, counter, actions — download, optional `onOpenExternal`/`onRemove`, close-✕),
|
|
610
646
|
prev/next, ESC, and rotate. The toolbar is RESPONSIVE: on a phone (`useScreenSize().small`) the
|
|
611
647
|
actions collapse into a `⋯` `ActionMenu` so the close-✕ never overflows off-screen (its popover
|
|
612
648
|
portals inside the modal's own `PortalHost` — a raw `<Modal>` lacks one, which is why an inline
|
|
@@ -631,7 +667,7 @@ gallery at the first hidden file). This is the responsive overflow pattern (avat
|
|
|
631
667
|
file-manager); prefer it over a hardcoded `maxVisible`, which can't adapt to width.
|
|
632
668
|
|
|
633
669
|
Under the hood it composes `FileThumbnail` (the completed tile — right surface per MIME: image
|
|
634
|
-
thumbnail
|
|
670
|
+
thumbnail, a doc tile with the `FileBadge` centered + a single-line filename, media card; also
|
|
635
671
|
takes `isTemplate` to overlay a TMPL marker) and `UploadingThumbnail` (the in-flight tile); reach
|
|
636
672
|
for either directly only when hand-rolling a NON-grid layout. **When the filename must be
|
|
637
673
|
readable, use `FileRow`** — a horizontal LINE (`FileBadge` or a `placeholder` + the FULL name + a
|