@lotics/ui 26.4.1 → 27.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/MIGRATION.md +70 -0
- package/docs/catalog.md +64 -25
- package/docs/composition.md +17 -15
- package/docs/templates.md +30 -20
- package/examples/tpl_item_list.tsx +5 -6
- package/examples/tpl_record.tsx +159 -15
- package/examples/tpl_task_board.tsx +1 -2
- package/package.json +1 -2
- package/src/filter_chip.tsx +2 -2
- package/src/ledger.tsx +164 -27
- package/src/option_list.tsx +8 -5
- package/src/reference_field.tsx +25 -11
- package/src/select.tsx +21 -6
- package/src/summary_line.tsx +14 -0
- package/src/text_input_field.tsx +8 -1
- package/src/text_link.tsx +9 -5
- package/src/text_button.tsx +0 -139
package/MIGRATION.md
CHANGED
|
@@ -4,6 +4,76 @@ 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
|
+
## 27.4.0 — a `Select` trigger announces as `combobox`, not `button`
|
|
8
|
+
|
|
9
|
+
No API change, but the DOM and the accessibility tree changed, so **a test or script that
|
|
10
|
+
finds a Select by its button role must be updated**:
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
getByRole("button", { name: "Tags" }) // BEFORE — a <button> element
|
|
14
|
+
getByRole("combobox", { name: "Tags" }) // AFTER — a <div role="combobox">
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The trigger had to stop being a `<button>` element: the sanctioned chip box
|
|
18
|
+
(`renderSelected` → `<Chip onDismiss>`) puts a remove button inside the trigger, and
|
|
19
|
+
`<button>`'s content model forbids interactive descendants — so every multi Select with
|
|
20
|
+
chips rendered invalid HTML and threw a React error on mount. `combobox` is also the role
|
|
21
|
+
the APG pattern this control already follows calls for. Focus, Enter/Space, pointer open,
|
|
22
|
+
Escape, and chip removal are unchanged.
|
|
23
|
+
|
|
24
|
+
Note for `searchable`: `OptionList`'s filter input is a combobox too, so scope such a query
|
|
25
|
+
to the trigger (or use `.first()`) rather than assuming one match per Select.
|
|
26
|
+
|
|
27
|
+
## 27.2.0 — a `LedgerGroup`'s sum moved below its rows
|
|
28
|
+
|
|
29
|
+
No API change; the rendering moved. A summed group used to print `LABEL … sum` as an eyebrow
|
|
30
|
+
ABOVE its rows and now closes them: the rows come first, then `Label sum` at `sm/medium`,
|
|
31
|
+
with no eyebrow (the label travels with the sum instead of being said twice).
|
|
32
|
+
|
|
33
|
+
**If you mix summed and unsummed groups in one `Ledger`, that now looks inconsistent** — an
|
|
34
|
+
unsummed group still captions from above, so the group names land in two different places.
|
|
35
|
+
This is the one case that wants an edit: give every group a `total`, and let a lone row stand
|
|
36
|
+
bare rather than grouping it (a one-row group's sum only restates the row). Omitting `total`
|
|
37
|
+
remains fully supported for a statement whose groups all name categories without summing them.
|
|
38
|
+
|
|
39
|
+
## 27.0.0 — `TextButton` is REMOVED; an act carries a control surface
|
|
40
|
+
|
|
41
|
+
**`@lotics/ui/text_button` is gone, with no replacement component.** It was the wrong
|
|
42
|
+
abstraction, and 25.0.0 had already retreated from it inside `ReferenceField`.
|
|
43
|
+
|
|
44
|
+
It existed to solve one real problem: a fill-less `Button`'s optical edge is its INK, inset by its
|
|
45
|
+
padding, so it reads as indented against a column of labels. But paying for that with a third rung
|
|
46
|
+
cost more than it bought — **underline came to mean two things at once**, separable only by ink
|
|
47
|
+
(blue navigates, zinc acts), so the affordance stopped answering the only question a reader has:
|
|
48
|
+
does this take me away, or does it do something here. And it duplicated the `Button` colour ladder,
|
|
49
|
+
where `muted` already IS the quiet rung.
|
|
50
|
+
|
|
51
|
+
**The new law: an ACT carries a control surface; only NAVIGATION is underlined text.** Chrome has
|
|
52
|
+
two rungs — `Button` (40px, a surface) and `InlineButton` (28px, filled, on a field's own surface).
|
|
53
|
+
`Link`/`TextLink` stay underlined and go somewhere.
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
<TextButton onPress={selectAll}>Select all</TextButton> // BEFORE
|
|
57
|
+
<Button title="Select all" color="muted" onPress={selectAll} /> // AFTER
|
|
58
|
+
|
|
59
|
+
<TextButton color="danger" onPress={del}>Remove fee</TextButton> // BEFORE
|
|
60
|
+
<Button title="Remove fee" color="danger" onPress={del} /> // AFTER — rule 9's convention
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Per call site, pick by WHERE the act sits, not by how quiet you want it to look:**
|
|
64
|
+
|
|
65
|
+
- In **chrome** — a `PopoverFooter`, a heading row, a select-all band — use `Button color="muted"`.
|
|
66
|
+
Nothing there establishes a text edge, so the padding inset costs nothing. (`FilterChip`'s Clear
|
|
67
|
+
and `OptionList`'s select-all/deselect-all moved this way inside the kit.)
|
|
68
|
+
- **Destructive** — `Button color="danger"`, bottom-left after the entity's fields, per composition
|
|
69
|
+
rule 9. That is the page-wide convention a text-weight verb was quietly opting out of.
|
|
70
|
+
- **An add** — `Button` below the list it extends, per rule 8.
|
|
71
|
+
- Still tempted to align a quiet verb to a column of TEXT? **Move the act, don't restyle it.** That
|
|
72
|
+
pull is what produced the rung in the first place.
|
|
73
|
+
|
|
74
|
+
`TextLink` is unchanged. Its `onPress`-free forms — an `href` link, or a passive underlined marker
|
|
75
|
+
on a value whose press opens a peek — were never this component's job and keep working.
|
|
76
|
+
|
|
7
77
|
## 26.0.0 — a section's ADD moves to its heading row; `EmptyState` loses its verb
|
|
8
78
|
|
|
9
79
|
**`EmptyState.action` is REMOVED — no replacement on that component.** Two different things
|
package/docs/catalog.md
CHANGED
|
@@ -467,8 +467,9 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
467
467
|
|
|
468
468
|
- **`button`** — `Button`: the labelled action; `title` MANDATORY (it is the accessible
|
|
469
469
|
name), `color` = emphasis/risk. A fill-less `Button` shows no box, so its optical
|
|
470
|
-
edge is its INK, inset by its padding — don't align one to a column of TEXT
|
|
471
|
-
|
|
470
|
+
edge is its INK, inset by its padding — don't align one to a column of TEXT. There is
|
|
471
|
+
no text-weight rung to reach for: move the act into chrome (a `PopoverFooter`, a
|
|
472
|
+
heading row) where no text edge exists to betray, or give it the filled tier.
|
|
472
473
|
- **`icon_button`** — `IconButton`: the icon-only circular action (see
|
|
473
474
|
[Actions](#actions)).
|
|
474
475
|
- **`back_button`** — `BackButton`: the one go-back control — don't hand-roll it (an
|
|
@@ -485,18 +486,8 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
485
486
|
with no `href` it stays NEUTRAL: plain underlined text to wrap in your own pressable,
|
|
486
487
|
or a MARKER that a value leads somewhere (a record reference whose press opens a peek,
|
|
487
488
|
not a trip). `color` overrides either way; inherits every `Text` prop. It does NOT
|
|
488
|
-
act
|
|
489
|
-
|
|
490
|
-
"Clear", "Add stop") — the third rung of chrome after `Button` (40px surface) and
|
|
491
|
-
`InlineButton` (28px, filled, on a field's surface). **Underlined, in neutral ink**:
|
|
492
|
-
underline marks interactive, blue marks navigation — so `Link` goes somewhere and
|
|
493
|
-
this acts. Choose by what the press DOES. The underline is not optional (position is
|
|
494
|
-
a learned convention, hover is absent on touch). `TextButtonColor` is
|
|
495
|
-
`default | danger` only — `muted` would mute the thing carrying the act (use
|
|
496
|
-
`Button color="muted"`). `onPress` REQUIRED; `disabled` greys to zinc-400, drops the
|
|
497
|
-
press + wash, announces `aria-disabled`. Hover wash, focus ring and a 40px target
|
|
498
|
-
(32px box + `hitSlop`) come from `PressableHighlight` — a pressable `Text` has
|
|
499
|
-
none of them. ONE size, no icon (a verb needing a glyph is a `Button`).
|
|
489
|
+
act: an act carries a control surface (`Button`/`InlineButton`), and underlined text
|
|
490
|
+
is the NAVIGATION affordance only.
|
|
500
491
|
- **`chip`** — `Chip`: the generic pill — pressable when `onPress` (announces as a button;
|
|
501
492
|
pass `accessibilityLabel` when children aren't self-describing text) + an
|
|
502
493
|
absolutely-positioned dismiss ✕ sibling when `onDismiss` (its name = `dismissTooltip` ??
|
|
@@ -588,7 +579,14 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
588
579
|
also home of the shared `PickerOption` type.
|
|
589
580
|
- **`select`** — `Select`: rich/custom-rendered, single/multi, select-all, chips via
|
|
590
581
|
`renderSelected(item, { remove })` + `searchable` + `allowCustom` — the tag field is just
|
|
591
|
-
a multi Select; opens `OptionList`.
|
|
582
|
+
a multi Select; opens `OptionList`. Its trigger is `role="combobox"` (a `<div>`), never
|
|
583
|
+
`role="button"`: the chip box legitimately puts a remove button INSIDE the trigger, and a
|
|
584
|
+
real `<button>` may not contain one. **Any trigger that renders caller-supplied content
|
|
585
|
+
must not take `accessibilityRole="button"`** — react-native-web picks the element from the
|
|
586
|
+
role, and `<button>`'s content model forbids interactive descendants, so the first caller
|
|
587
|
+
to pass a chip, link, or menu makes the markup invalid. Either take a non-button role, or
|
|
588
|
+
keep the verbs OUTSIDE the pressable as siblings (`Chip`'s absolute ✕, `InlineEditView`'s
|
|
589
|
+
shell-owned `actions`).
|
|
592
590
|
- **`option_list`** — `OptionList`: the ONE shared searchable listbox body every selector
|
|
593
591
|
opens — single/multi, optional internal search, create row, keyboard + native-`<select>`
|
|
594
592
|
typeahead; host it directly in a `Popover`/`Dialog` for a command palette.
|
|
@@ -838,18 +836,54 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
838
836
|
`metric` {label,value,tone,note} pinned right, the band's ONE accent. The record's FIELDS
|
|
839
837
|
never live in the header: compose them as `DetailTable`s in the sections below. Replaces
|
|
840
838
|
hand-rolled record headers (mixed scales, several competing figures, color noise).
|
|
841
|
-
- **`ledger`** — `Ledger` + `LedgerGroup` + `LedgerRow` + `LedgerTotal` — the
|
|
842
|
-
money list:
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
839
|
+
- **`ledger`** — `Ledger` + `LedgerGroup` + `LedgerBasis` + `LedgerRow` + `LedgerTotal` — the
|
|
840
|
+
record-level money list: every figure on ONE right-aligned tabular column, `peek` turns a row
|
|
841
|
+
into a pressable door floating its particulars in an anchored popover (put links INSIDE the
|
|
842
|
+
peek — never a button in a button; `reference` is the trailing-link alternative for static
|
|
843
|
+
rows), `LedgerTotal` = the divider-set emphasized close with `zeroLabel` for settled. **`meta` is a
|
|
844
|
+
neutral QUALIFIER** (a date, a method) at caption weight — never a problem or a state: one
|
|
845
|
+
row's meta being a fact and another's a complaint is what makes a column read inconsistent,
|
|
846
|
+
and a caption is the wrong weight for something wanting action. A problem goes on the row
|
|
847
|
+
that can FIX it, and in a statement the arithmetic has usually said it already.
|
|
848
|
+
**Pick the shape by what the statement IS**, because there are two and they read differently:
|
|
849
|
+
- **`charges → total`** — `LedgerGroup`s, each **closed by its own sum** ("Charges" vs
|
|
850
|
+
"Received"), giving three ascending rungs: row `sm/regular`, subtotal `sm/medium`, total
|
|
851
|
+
`md/semibold` over its rule. A summed group's label travels WITH its sum and takes no
|
|
852
|
+
eyebrow, so its rows are met before its name and must carry the side themselves, by opposite
|
|
853
|
+
SIGNS. Two same-signed groups are two statements, not two sides; give them two `Ledger`s.
|
|
854
|
+
A `total` must equal the rows it closes — derive the rows and the sum from ONE list, never
|
|
855
|
+
from two predicates that can drift apart — and it earns its place only by adding what the
|
|
856
|
+
rows don't already say, which a one-row group's sum doesn't. **Omitting `total` is a shape,
|
|
857
|
+
not a degradation**: the label becomes a caption ABOVE the rows, for a group that names a
|
|
858
|
+
category rather than summing a side. **Don't mix the two in one `Ledger`** — the label would
|
|
859
|
+
sit above some groups and below others, reading as an accident; if any group earns a sum,
|
|
860
|
+
give every group one and let a lone row stand bare instead of grouping it.
|
|
861
|
+
- **`basis → derived → total`** — duty off a customs value, commission off gross, interest
|
|
862
|
+
off principal. The base is **`LedgerBasis`** (label + value + optional `meta` for how it was
|
|
863
|
+
arrived at), which takes the third treatment on the money column and sets its `Divider`
|
|
864
|
+
BELOW it, since a premise separates itself from what it feeds. Do not render a base as a
|
|
865
|
+
`LedgerRow` — that claims it is a peer you could add to the rows under it — and do not wrap
|
|
866
|
+
it in a one-row group. With one side there is no group at all: the levies are bare
|
|
867
|
+
`LedgerRow`s between the basis and the total.
|
|
868
|
+
Worked example: [`tpl_record`](../examples/tpl_record.tsx) § Billing — a three-sided closing
|
|
869
|
+
statement (billed, less credited, less received) exercising every row state: a `peek` door
|
|
870
|
+
whose lookup link sits INSIDE the popover, a flat `reference` on the row with nothing to
|
|
871
|
+
expand, `meta` + `success` tone on money coming back, and a one-row `Adjustments` group that
|
|
872
|
+
takes no `total`. `Received` is derived from the charges carrying a payment METHOD rather than
|
|
873
|
+
a second flag to keep in step, and the refundable deposit stays OUT because a ledger earns
|
|
874
|
+
trust by summing exactly what its label claims. `LedgerBasis` is demoed on the gallery's
|
|
875
|
+
Charts page instead — a delivery order has no base to compute from, and inventing one to
|
|
876
|
+
place a component is how invented needs start.
|
|
849
877
|
- **`reference_field`** — `ReferenceField`: a reference to ANOTHER RECORD, rendered as a
|
|
850
878
|
FIELD VALUE — the kit's inline-editor surface (so a pointer sits in the value column
|
|
851
879
|
like the editors above and below it), whose press opens a PEEK of that record's facts.
|
|
852
|
-
`name` + optional `code` + `facts`, plus the verb callbacks below. The
|
|
880
|
+
`name` + optional `code` + `facts`, plus the verb callbacks below. The peek takes THE FIELD'S
|
|
881
|
+
WIDTH (`inheritTriggerWidth`, as `InlineSelect` does), floored at `MIN_CONTROL_WIDTH` — a
|
|
882
|
+
panel narrower than the control that opened it reads as belonging to something else. So the
|
|
883
|
+
field truncates its name to one line and the peek's header WRAPS the whole of it: the column
|
|
884
|
+
stays scannable, the full value is one press away. Between the floor and `labelWidth +
|
|
885
|
+
MIN_CONTROL_WIDTH + 24` a `DetailTable` stacks, which is the right grammar at that width.
|
|
886
|
+
The verbs live INSIDE the
|
|
853
887
|
peek, never on the field, and they are ALL `Button`s at one altitude — a peek is
|
|
854
888
|
dialog-scale and a dialog's verbs are buttons, so mixing weights only made the reader rank
|
|
855
889
|
four acts that are four acts. The row splits by WHAT EACH TOUCHES, and the spacer is that
|
|
@@ -948,7 +982,12 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
948
982
|
- **`filter_chip`** — `FilterChip` + `selectSummary`: the toolbar filter pill hosting a
|
|
949
983
|
facet (options, a `Slider range`, a `Counter`).
|
|
950
984
|
- **`summary_line`** — `SummaryLine`: the light inline summary of a register/list's FILTERED
|
|
951
|
-
view, sits below the toolbar; NOT the boxed dashboard `kpi_strip` band.
|
|
985
|
+
view, sits below the toolbar; NOT the boxed dashboard `kpi_strip` band. Every item is an
|
|
986
|
+
AGGREGATE over the rows in view (a count, a sum, a fill), and the strip goes with the set it
|
|
987
|
+
summarizes — including a record's child list (a consol's houses, an order's lines). **Never on
|
|
988
|
+
a record's identity band reporting that record's own state**: derived verdicts on one record
|
|
989
|
+
("documents incomplete", "2 fields need checking") are the checklist a record must not carry
|
|
990
|
+
(§`pipeline`) and restate at a distance what the sections below state in place.
|
|
952
991
|
- **`use_selection`** — `useSelection`: always-on multi-select state for a register/list —
|
|
953
992
|
the `selected` Set + `toggle`/`setAll`/`allSelected`/`indeterminate`/`count`/`clear`;
|
|
954
993
|
selectability gating stays with the caller. The checkbox-always-visible counterpart to
|
package/docs/composition.md
CHANGED
|
@@ -471,17 +471,19 @@ All view controls are 40px tall (`CONTROL_HEIGHT`), `sm` labels, in ONE wrapping
|
|
|
471
471
|
`danger` (and its quieter `danger-secondary`) marks destructive — that's the whole axis. A
|
|
472
472
|
fill-less `Button` (`danger-secondary`, `muted`, un-colored) shows no box, so its optical edge is
|
|
473
473
|
its INK, inset by its padding: **never align one to a column of TEXT** — it reads as indented
|
|
474
|
-
against every label starting on the true edge
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
474
|
+
against every label starting on the true edge. The answer is to move the act, not to invent a
|
|
475
|
+
rung for it: a quiet verb belongs in CHROME (a `PopoverFooter`, a heading row, a select-all
|
|
476
|
+
band) where nothing establishes a text edge to betray, and a destructive one belongs where rule 9
|
|
477
|
+
already puts it. Chrome has TWO rungs: `Button` (a 40px control with a surface) →
|
|
478
|
+
`InlineButton` (28px, filled, on a field's own surface).
|
|
479
|
+
|
|
480
|
+
**AN ACT CARRIES A CONTROL; ONLY NAVIGATION IS UNDERLINED TEXT.** `Link`/`TextLink` are the
|
|
481
|
+
underlined pair and they GO somewhere; everything that acts is a `Button` or an `InlineButton`.
|
|
482
|
+
A surface-less ACT was tried as a third rung and removed: it made underline mean two things at
|
|
483
|
+
once, separable only by ink, so the affordance stopped answering the one question a reader has —
|
|
484
|
+
does this take me away, or does it do something here. It also competed with the `Button` colour
|
|
485
|
+
ladder for the same job, since `muted` already IS the quiet rung. Pick by what the press DOES,
|
|
486
|
+
and let the surface say which kind it is. Never a `muted` text action: it mutes
|
|
485
487
|
the one thing carrying the act. No
|
|
486
488
|
"success"/green button. Decision UIs put positive/negative color on the STATUS (dot) and verdict
|
|
487
489
|
(colored `Text`), not the buttons.
|
|
@@ -711,10 +713,10 @@ selection washes).
|
|
|
711
713
|
(rose vs orange) stays on `Badge`s.
|
|
712
714
|
|
|
713
715
|
Links use `Link`/`TextLink` (blue-600), never `solid("blue")` — and blue-600 means NAVIGATION
|
|
714
|
-
specifically, not "interactive" generally
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
716
|
+
specifically, not "interactive" generally. Underlined text is the NAVIGATION affordance and nothing
|
|
717
|
+
else (see the button ladder above): an act carries a control surface. `TextLink` takes the blue only
|
|
718
|
+
when it has an `href`; as a passive marker on a value it stays neutral, because a press that opens a
|
|
719
|
+
PEEK is not a trip.
|
|
718
720
|
|
|
719
721
|
## `Badge` is for STATUS only — everything else is text
|
|
720
722
|
|
package/docs/templates.md
CHANGED
|
@@ -42,8 +42,7 @@ the package index is [../AGENTS.md](../AGENTS.md).
|
|
|
42
42
|
| A guided sequence of physical tasks (scan, confirm, next) | `tpl_pick` |
|
|
43
43
|
| Splitting one source amount across many targets | `tpl_allocate` |
|
|
44
44
|
| A record's create/edit surface — also the settings shape | `tpl_record` |
|
|
45
|
-
| A
|
|
46
|
-
| A team task board (inline-managed grouped table) | `tpl_task_board` |
|
|
45
|
+
| A surface whose SUBJECT is tasks (the canonical task-list grammar) | `tpl_task_board` |
|
|
47
46
|
| One record HANDED between desks — stages that each own their controls | `tpl_record` (its Progress section) |
|
|
48
47
|
| Financial statements | `tpl_statements` |
|
|
49
48
|
| A scoped lookup report with export | `tpl_report` |
|
|
@@ -336,25 +335,36 @@ billing, and quick-capture templates. Top → bottom:
|
|
|
336
335
|
`DrawerFooter`. "Add fee" rides the section heading's right edge — the one place it does not
|
|
337
336
|
move as rows arrive — and is create-then-refine: a blank fee opens straight in the drawer. The
|
|
338
337
|
empty state carries no button of its own, because that one is already on screen.
|
|
339
|
-
- **Billing —
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
338
|
+
- **Billing — INLINE bands, closed by a STATEMENT.** Each invoice is a `Subsection` costing ONE
|
|
339
|
+
line of chrome (its name, its total, its issued ref) over its charge rows in a `DetailTable`;
|
|
340
|
+
the charge editors carry ghost list prices with a one-tap "Standard …" fill and a "How paid…"
|
|
341
|
+
select. Three shapes were tried. Inline bands with a heading rung, a band callout and an
|
|
342
|
+
action row each made the CHROME three times the content; a `Table` + right-docked `Drawer`
|
|
343
|
+
then hid four charge lines behind three expansions, which trades a real fault for a worse one
|
|
344
|
+
— you can no longer see what you are paying for. So the lines all stay and the chrome
|
|
345
|
+
shrank, and a per-invoice problem rides the CHARGE ROW that has it (`warning` on the line
|
|
346
|
+
missing its method), which is smaller than a callout and more precise: it names the offender
|
|
347
|
+
instead of counting offenders.
|
|
348
|
+
The section then CLOSES with a `Ledger` — the money grammar's worked example, and the answer
|
|
349
|
+
the bands cannot give: three invoice totals down the page are three facts the reader has to
|
|
350
|
+
add up, and "is anything still owed" appeared nowhere. THREE sides — `Invoiced`, a one-row
|
|
351
|
+
`Adjustments` credit, and `Received` with its rows negative and `success`-toned — so the total
|
|
352
|
+
states arithmetic rather than a difference, over a `LedgerTotal` carrying `zeroLabel="Paid in
|
|
353
|
+
full"`. Every row state is on screen: a `peek` door on the multi-line invoice (its lookup link
|
|
354
|
+
INSIDE the popover, since a button cannot hold a button), a flat `reference` on the single-line
|
|
355
|
+
one, `meta` qualifying how money came back. `Received` is DERIVED from the charges that carry a
|
|
356
|
+
payment method — not a second flag to keep in step — and the refundable deposit stays OUT,
|
|
357
|
+
because a ledger earns trust by summing exactly what its label claims. The fixture is
|
|
358
|
+
deliberately uneven (an unpriced charge, one issued invoice, a one-row group) because a tidy
|
|
359
|
+
one verifies nothing.
|
|
348
360
|
THE ACTION-GATING LAW is still the worked example: a not-ready Issue is DISABLED and the
|
|
349
|
-
reason is a co-located `Callout` at the gate's SCOPE, once — an invoice's own inconsistency
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
Issuing gates on record premises only — never silently on stage.
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
total is the one that will be billed even when the press was held for a charge cell's write
|
|
357
|
-
(data_entry.md § Inline edit).
|
|
361
|
+
reason is a co-located `Callout` at the gate's SCOPE, once — an invoice's own inconsistency (a
|
|
362
|
+
charged line with no method) on that line, a broken record premise (no customer / invalid tax
|
|
363
|
+
ID) at the section top; self-evident empties stay silent, and NEVER prose beside a CTA.
|
|
364
|
+
Issuing gates on record premises only — never silently on stage. It is also the worked example
|
|
365
|
+
of **an irreversible action taking an ID, not a captured object** — the confirm re-resolves the
|
|
366
|
+
invoice from its key, so the quoted total is the one that will be billed even when the press
|
|
367
|
+
was held for a charge cell's write (data_entry.md § Inline edit).
|
|
358
368
|
- **Document set — the OUTPUT desk, the last WORK section** (the composition rules' output
|
|
359
369
|
law worked on the record surface: the top is intake, the bottom produces on demand).
|
|
360
370
|
Forms group PER PARTY on the `SubsectionStack` beat — each party a `SubsectionHeading` +
|
|
@@ -159,7 +159,7 @@ const ASSIGNEES = [...new Set(HO_SO.map((r) => r.phuTrach))].map((n) => ({ label
|
|
|
159
159
|
// Columns defined ONCE — Table renders the header from these and each
|
|
160
160
|
// TableCell takes its width/align by position; the ⋯ is the trailing gutter.
|
|
161
161
|
// `priority` = the MOBILE contract: when the container can't fit every column
|
|
162
|
-
// the highest numbers drop first (Received →
|
|
162
|
+
// the highest numbers drop first (Received → Status), the identity +
|
|
163
163
|
// Customer + Fee survive longest, and at the register floor rows STACK
|
|
164
164
|
// (label-over-value) so nothing is lost — the drawer carries full detail
|
|
165
165
|
// either way.
|
|
@@ -715,8 +715,8 @@ function EnterDataDialog({ open, onOpenChange, seedDocs, onCreate, onCreateMany
|
|
|
715
715
|
<View style={{ paddingHorizontal: 24, paddingBottom: 12, gap: 12 }}>
|
|
716
716
|
<Text size="xs" color="muted">
|
|
717
717
|
{variant === "export"
|
|
718
|
-
? "The export case starts in Processing — details
|
|
719
|
-
: "The import case starts in Processing — details
|
|
718
|
+
? "The export case starts in Processing — details and payment live on the record itself."
|
|
719
|
+
: "The import case starts in Processing — details and payment live on the record itself."}
|
|
720
720
|
</Text>
|
|
721
721
|
<FormField label="Customer">
|
|
722
722
|
{khach === "" ? (
|
|
@@ -779,9 +779,8 @@ export function TplItemList() {
|
|
|
779
779
|
const [search, setSearch] = useState("");
|
|
780
780
|
const [page, setPage] = useState(0);
|
|
781
781
|
const [openMa, setOpenMa] = useState<string | null>(null);
|
|
782
|
-
//
|
|
783
|
-
// drawer
|
|
784
|
-
// settling the ledger in the drawer flips the row's Unpaid/Print/menu live.
|
|
782
|
+
// Paid state is LIFTED, not owned by the drawer: settling the ledger inside the
|
|
783
|
+
// drawer flips the row's Unpaid/Print/menu live, because both read one source.
|
|
785
784
|
const [paidMap, setPaidMap] = useState<Record<string, boolean>>({});
|
|
786
785
|
const daThuOf = (r: HoSo) => paidMap[r.ma] ?? r.daThu;
|
|
787
786
|
// The register rows are state: the create dialog PREPENDS a real row and
|