@lotics/ui 44.8.0 → 44.9.1
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 -3
- package/MIGRATION.md +27 -0
- package/docs/ai_patterns.md +20 -0
- package/docs/catalog.md +73 -4
- package/docs/composition.md +9 -1
- package/docs/reviewing.md +8 -0
- package/package.json +3 -1
- package/src/checklist.tsx +23 -3
- package/src/ledger.tsx +13 -3
- package/src/locale.tsx +11 -0
- package/src/matrix.tsx +17 -12
- package/src/related_record_row.tsx +80 -0
- package/src/running_ledger.tsx +196 -0
- package/src/stepper.tsx +26 -2
- package/src/table_fit.ts +15 -5
package/AGENTS.md
CHANGED
|
@@ -16,8 +16,8 @@ CURRENT major only — upgrading an app across majors is `MIGRATION.md`.
|
|
|
16
16
|
|---|---|
|
|
17
17
|
| [docs/catalog.md](./docs/catalog.md) | **The complete inventory** — Reach-by-role (each data role → the ONE canonical component) + every `@lotics/ui/<module>` entry point (incl. `@lotics/ui/vite`'s `loticsOptimizeDeps` + `loticsResolve()` — the pre-bundle list and the whole `resolve` block a custom-code app's `vite.config.ts` imports rather than hand-carries, dev-link included). Read before building any screen; reuse first. |
|
|
18
18
|
| [docs/data_entry.md](./docs/data_entry.md) | **a picker over a select field keeps the field's colours** (`optionPicker` — the hand-rolled `map` to `{value,label}` drops `color`, so a value reads coloured in a register and grey in the editor); Which editing pattern for which job — inline edit, fieldset forms, browser-autofill suppression (search controls only), find-or-create (`Combobox`), line items, handoffs, phased records, billing, tags, dispositions, attachments (`InlineFiles` for a record ROW — list + one CTA, `multiple` decides add-vs-replace; the `FilesEditor` COMPOUND for a whole section — root owns selection/gallery/confirm, you compose the bar, a HOST verb reads `useFilesEditorSelection` — plus the three-way file INTAKE: CTA + `FileDropTarget` + `usePasteFiles`), stage gates, whether a multiline value keeps its fixed reserve or `autoGrow`s (who decides the length — the field, or whoever is typing), the commit-on-blur vs action-press ordering law (the kit gates the press — `pending_commits`). |
|
|
19
|
-
| [docs/ai_patterns.md](./docs/ai_patterns.md) | AI acts, the human stays in charge — composer, live run feed (`AgentRun`), the one law's split — it turns on WHO supplied the values (machine → a gate: a diff when something is being replaced, a full editable preview when records are being created from a document; human-typed → save-direct + the `ResultHeader` receipt), findings, provenance, confidence; **after the run** — a stored record that fills up from several writers (a person, a chat agent, an extraction, an automation) and remembers none of them: an unwritten value must not render like a written one, a machine's prose and a person's must not share a treatment, model markdown goes in `variant="embedded"`, and the read path must project every field the write paths set; the UI half of the SDK's [ai doc](../app-sdk/docs/ai.md), the whole run in a dialog (`AgentRunScope`/`AgentRunPane`/`AgentRunActions` — a parked question REPLACES the feed, actions in the footer, **Stop** while streaming), **stopping** (`cancel` stops the run, `abort` only stops listening — so closing a dialog must `cancel` or it keeps billing); **review surfaces compose from atoms** — `DiffValue` (a changed value, droppable in any cell/row/total), `DiffMark` (what happened to the row — ONE circular disc, every surface), `useChangeSet` (accept/reject/undo bookkeeping, no layout) — see [MIGRATION.md](./MIGRATION.md) for the `ChangeReview` family they replace |
|
|
20
|
-
| [docs/composition.md](./docs/composition.md) | The design-language contract — **the form comes before the treatment** (name what the subject IS — register, rail, board, timeline, deviation queue — before reaching for parts; a subject flattened into rows is well-typed and says nothing, and reuse-first is about not duplicating primitives, not about a screen's shape), canvas + content column, heading altitude (incl. eyebrow vs group lead — a label is one or the other), banded cards, register vs inset rows (incl. the register laws a row centres its cells by: every cell a FIXED height, a pressable cell on the shared hover token, a column sized by what it carries), the button ladder and **what underlined text may mean** (it GOES somewhere or REVEALS something — never mutates; blue leaves the surface, muted stays on it, and the one in-prose disclosure exception is scoped there), master-detail `Drawer` on a LIST screen vs a child collection's row EXPANDING inside a record, view controls, RECORD EXTENT (one page, sections scrolled to and never routed to), **the register's rhythm** (no hairline between rows — their own height and the hover wash separate them; the register's one line is the band capping the columns, because that is the break whitespace cannot state. Density is `TableRow`'s `minHeight`, never a mode), **a register that TRIAGES** (`TableGroup` — titled bands ordered by what needs the reader now, grouped by what implies a different ACTION rather than by a category already in a column), **the register's own craft** (a column header treated as CHROME a step lighter than row metadata, sentence case in the table header too, and a supporting line one RUNG BELOW the line it supports rather than a fixed size — plus the half that rule was missing: every SUBJECT on one row starts at the same rung, the subject leads by WEIGHT and its identity mark rather than by size, and whether the supporting line drops a rung is decided by what else separates the pair), **identity marks** (a mark that looks the same on every row carries nothing — `Avatar` derives its hue from the name and takes circle/square for person vs organization; the palette is ordered by contrast, not spectrum, and never paints a person red or amber), **where the accent goes** ("which of these" — a selected row, an active filter; never DATA and never the focus ring, while a page-scale position marker like an active TAB takes `primary`, the ink the kit already spends on "this is on"; and selection must not share a wash with hover), **theming is an app-level ESCAPE HATCH, not a product surface** (`applyLoticsTheme({ … })` once at boot, before `mount`, writing the `--lotics-*` roles on the document root; nothing in the platform writes a theme, and `apps.theme.color` is the launcher icon only — the five `var()` roles plus the three font rungs ARE the whole mechanism, and wrapping a component cannot replace them because RN-Web writes `fontFamily` per element and background/border do not inherit), color discipline, **typography** (the ramp is DATA in `type_ramp`, one table both platforms derive from; leading and tracking are baked per rung and a hand-set `letterSpacing` is always a second copy of the curve), whitespace, and how to TEST an overlay component (a `Popover`-backed surface never mounts under jsdom). |
|
|
19
|
+
| [docs/ai_patterns.md](./docs/ai_patterns.md) | AI acts, the human stays in charge — composer, live run feed (`AgentRun`), the one law's split — it turns on WHO supplied the values (machine → a gate: a diff when something is being replaced, a full editable preview when records are being created from a document; human-typed → save-direct + the `ResultHeader` receipt), findings, provenance, confidence; **after the run** — a stored record that fills up from several writers (a person, a chat agent, an extraction, an automation) and remembers none of them: an unwritten value must not render like a written one, a machine's prose and a person's must not share a treatment, model markdown goes in `variant="embedded"`, and the read path must project every field the write paths set; the UI half of the SDK's [ai doc](../app-sdk/docs/ai.md), the whole run in a dialog (`AgentRunScope`/`AgentRunPane`/`AgentRunActions` — a parked question REPLACES the feed, actions in the footer, **Stop** while streaming), **stopping** (`cancel` stops the run, `abort` only stops listening — so closing a dialog must `cancel` or it keeps billing); **review surfaces compose from atoms** — `DiffValue` (a changed value, droppable in any cell/row/total), `DiffMark` (what happened to the row — ONE circular disc, every surface, its kind derived from before/after rather than a boolean "did this change" — an empty `before` is `added`, never `changed`), `useChangeSet` (accept/reject/undo bookkeeping, no layout) — see [MIGRATION.md](./MIGRATION.md) for the `ChangeReview` family they replace |
|
|
20
|
+
| [docs/composition.md](./docs/composition.md) | The design-language contract — **the form comes before the treatment** (name what the subject IS — register, rail, board, timeline, deviation queue — before reaching for parts; a subject flattened into rows is well-typed and says nothing, and reuse-first is about not duplicating primitives, not about a screen's shape), canvas + content column, heading altitude (incl. eyebrow vs group lead — a label is one or the other, and a section heading's own row never wraps so a control label that repeats the title can squeeze the title+description into a narrow ribbon at phone width), banded cards, register vs inset rows (incl. the register laws a row centres its cells by: every cell a FIXED height, a pressable cell on the shared hover token, a column sized by what it carries), the button ladder and **what underlined text may mean** (it GOES somewhere or REVEALS something — never mutates; blue leaves the surface, muted stays on it, and the one in-prose disclosure exception is scoped there), master-detail `Drawer` on a LIST screen vs a child collection's row EXPANDING inside a record, view controls, RECORD EXTENT (one page, sections scrolled to and never routed to), **the register's rhythm** (no hairline between rows — their own height and the hover wash separate them; the register's one line is the band capping the columns, because that is the break whitespace cannot state. Density is `TableRow`'s `minHeight`, never a mode), **a register that TRIAGES** (`TableGroup` — titled bands ordered by what needs the reader now, grouped by what implies a different ACTION rather than by a category already in a column), **the register's own craft** (a column header treated as CHROME a step lighter than row metadata, sentence case in the table header too, and a supporting line one RUNG BELOW the line it supports rather than a fixed size — plus the half that rule was missing: every SUBJECT on one row starts at the same rung, the subject leads by WEIGHT and its identity mark rather than by size, and whether the supporting line drops a rung is decided by what else separates the pair), **identity marks** (a mark that looks the same on every row carries nothing — `Avatar` derives its hue from the name and takes circle/square for person vs organization; the palette is ordered by contrast, not spectrum, and never paints a person red or amber), **where the accent goes** ("which of these" — a selected row, an active filter; never DATA and never the focus ring, while a page-scale position marker like an active TAB takes `primary`, the ink the kit already spends on "this is on"; and selection must not share a wash with hover), **theming is an app-level ESCAPE HATCH, not a product surface** (`applyLoticsTheme({ … })` once at boot, before `mount`, writing the `--lotics-*` roles on the document root; nothing in the platform writes a theme, and `apps.theme.color` is the launcher icon only — the five `var()` roles plus the three font rungs ARE the whole mechanism, and wrapping a component cannot replace them because RN-Web writes `fontFamily` per element and background/border do not inherit), color discipline, **typography** (the ramp is DATA in `type_ramp`, one table both platforms derive from; leading and tracking are baked per rung and a hand-set `letterSpacing` is always a second copy of the curve), whitespace, and how to TEST an overlay component (a `Popover`-backed surface never mounts under jsdom). |
|
|
21
21
|
| [docs/reviewing.md](./docs/reviewing.md) | **Reviewing a screen you built** — the other docs say what good looks like; this one says how to find out whether you achieved it, because "it looks fine" is the same claim as "it should work" made with the same evidence. Measure, never eyeball: render it, extract computed values, let the table show the defect. Two GATES that outrank every treatment question (**SUBTRACT** — what question does this element answer, and count FACTS not elements; **RIGHT CONTROL** — name the species before treating it), then 10 probes each naming what to COLLECT and the SIGNATURE in the numbers: type inventory (range, singletons, the label/value shrink war), ground+border+radius across siblings, what each RULE separates, gap RATIOS, the state diff (paint may change, geometry may not), alignment drift (incl. a centred child that moves while its container measures identical), the surface walk (open it, press it, read the settled string), the DATA probe (what is really in the table — incl. grepping rendered text for internal ids), the ABSENCE pass that catches "bland", and microcopy. Plus the extraction snippets, what measurement cannot see, and how to act on a finding (fix the CLASS, and never resolve one with "the docs say X"). |
|
|
22
22
|
| [docs/testing.md](./docs/testing.md) | Driving the kit in a browser — the three anatomies where the a11y tree says one thing and a driver must do another: a `PressDoor` row whose named button always intercepts pointer events (by design), portalled overlays that render at the top of the DOM, and custom pointer drag that `dragTo` cannot move. |
|
|
23
23
|
| [docs/templates.md](./docs/templates.md) | The map of `examples/tpl_*.tsx` — what shape each template solves and which to start from (copy + adapt, never import) — plus the record-surface composition rules (pipeline order, static shape, decision budget) and the ACTIVITY shape — a communications feed where the row's label is the GIST and the body varies by medium, one anatomy rather than a row type per kind. |
|
|
@@ -72,7 +72,9 @@ CURRENT major only — upgrading an app across majors is `MIGRATION.md`.
|
|
|
72
72
|
a press it refuses. Rows tick ONE at a time; derive position from the highest stamp and one click marks
|
|
73
73
|
everything below it without inventing dates. A checklist REPORTS — a field owned by a section is
|
|
74
74
|
named as a gap with a `ChecklistNote action` that jumps there, never edited or previewed in the
|
|
75
|
-
row.
|
|
75
|
+
row. A per-ROW third axis, `ChecklistItem.verdict` (`"pass"`/`"fail"`), is a DECIDED outcome — a
|
|
76
|
+
QC gate, a reconciliation match — never toggled; see `docs/catalog.md` for why it withholds
|
|
77
|
+
`onToggle`. Worked in `examples/tpl_record.tsx` § Progress. See `docs/catalog.md`.
|
|
76
78
|
- **`Badge` = STATUS only; supporting detail is the muted second line.** A type / category /
|
|
77
79
|
attribute / count is not a status — it belongs under its identity as `size="xs" color="muted"`,
|
|
78
80
|
never a second chip. A chip beside a name reads as its PEER (a colored one reads louder),
|
package/MIGRATION.md
CHANGED
|
@@ -4,6 +4,33 @@ 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
|
+
## 44.9.1 — a pivot's figures align, and a ledger row stops losing its own name
|
|
8
|
+
|
|
9
|
+
No API is removed and nothing fails to compile. Three renders change.
|
|
10
|
+
|
|
11
|
+
**`Matrix` right-aligns its figures instead of centring them.** Every value, column
|
|
12
|
+
header and total was `align="center"` while the cells also set `tabular` — and tabular
|
|
13
|
+
figures exist so digits line up in a column, which centring throws away. Measured on a
|
|
14
|
+
real pivot: centred, 24 figures painted across **14 distinct right edges**; right-aligned,
|
|
15
|
+
**12** — exactly one per column. No `align` prop came with this: a matrix is one measure
|
|
16
|
+
across two dimensions, so its cells cannot want different alignments, and a prop for a
|
|
17
|
+
centred case nobody has is surface without a consumer.
|
|
18
|
+
|
|
19
|
+
**`Matrix` row labels carry default ink, not `muted`.** A pivot whose rows are entities
|
|
20
|
+
rendered every entity name a step quieter than the numbers about it, inverting how the
|
|
21
|
+
grid is read — you find the row by its name, then scan across.
|
|
22
|
+
|
|
23
|
+
**`MATRIX_COL_GAP` is exported.** It was the literal `3` in four places, so a caller
|
|
24
|
+
sizing its own column budget had to hard-code it and could drift from what the grid draws.
|
|
25
|
+
|
|
26
|
+
**A `LedgerRow`'s `meta` yields before its label, and by 3×.** Only the label carried a
|
|
27
|
+
`flexShrink`, and RN-web resolves the unstyled caption to `1` as well — so under pressure
|
|
28
|
+
both shrank proportionally, and because a caption is usually the longer string, the
|
|
29
|
+
SUBORDINATE text ended up wider than the row's identity. Measured in a 180px row: before,
|
|
30
|
+
the label kept 31px of the 77 it needs while the caption kept 41 of 103; after, the label
|
|
31
|
+
keeps 55 and the caption 16. Any caller that dropped `meta` at small widths to dodge this
|
|
32
|
+
can stop.
|
|
33
|
+
|
|
7
34
|
## 44.8.0 — a value column gets its own floor, and eight defaults stop fighting their own docs
|
|
8
35
|
|
|
9
36
|
No API is removed. Three of these change what an existing screen renders, so read them even
|
package/docs/ai_patterns.md
CHANGED
|
@@ -318,6 +318,16 @@ shape its root draws, and every surface needing a diff somewhere else rebuilds i
|
|
|
318
318
|
operator opening a job of twelve fee lines to see where a dropped invoice landed should find
|
|
319
319
|
the one filled shape in a column of blanks without reading anything or separating two hues.
|
|
320
320
|
The hole keeps the column's width and announces nothing.
|
|
321
|
+
|
|
322
|
+
**Derive the kind from the same before/after the row's `DiffValue` carries — never from a
|
|
323
|
+
boolean "did this differ".** A field with no prior value (`before` empty or absent) that a run
|
|
324
|
+
now proposes a value for is an ADD, not a CHANGE, and a local review row that only asks
|
|
325
|
+
"does `draft !== before`" answers yes for both and always renders `changed` — the amber pencil
|
|
326
|
+
on a value that was never there to begin with, which reads as the AI having second-guessed a
|
|
327
|
+
figure it actually just filled in for the first time. This bites hardest on the record that
|
|
328
|
+
looks most ordinary: the first proposal any fresh record ever gets has an empty `before` on
|
|
329
|
+
every field, so every mark on that one run is wrong until the kind is derived per field
|
|
330
|
+
(`before === after` → no mark; `before === ""` → `added`; otherwise → `changed`).
|
|
321
331
|
- **`useChangeSet(ids, { initial })`** (`@lotics/ui/use_change_set`) — the bookkeeping:
|
|
322
332
|
`status`/`accept`/`reject`/`undo`/`acceptAll`/`reset`, `accepted`/`rejected`/`pending`,
|
|
323
333
|
`keptCount`/`total`/`settled`. `initial` defaults to **`accepted`** so an operator drops
|
|
@@ -387,6 +397,16 @@ shape its root draws, and every surface needing a diff somewhere else rebuilds i
|
|
|
387
397
|
only reject costs the whole re-entry.
|
|
388
398
|
- **Gate the commit on what will actually be written**, not on the proposal count — disable at
|
|
389
399
|
zero kept.
|
|
400
|
+
- **A decided row's Keep/Drop must be reachable from BOTH directions when `initial: "accepted"`.**
|
|
401
|
+
`undo` returns a row to its DEFAULT, not to `"pending"` — so on the common default
|
|
402
|
+
(`initial: "accepted"`, the operator drops the exceptions rather than re-approving every
|
|
403
|
+
identical field) a row starts already decided and Undo is a no-op with nothing to reach:
|
|
404
|
+
there is no path back to a `pending`-style Keep/Drop pair, and the row is stuck accepted
|
|
405
|
+
forever. The collapsed-to-Undo anatomy (value + one "Undo" button, swapping to Keep/Drop only
|
|
406
|
+
while `pending`) is correct ONLY under `initial: "pending"`, where every row genuinely starts
|
|
407
|
+
undecided. Under `initial: "accepted"`, render Keep/Drop as an always-visible TOGGLE instead —
|
|
408
|
+
both buttons present, the one matching the row's current status disabled, the other live —
|
|
409
|
+
so either direction is one press away regardless of where the row started.
|
|
390
410
|
- **The mark rides the LABEL, and the value sits in an `InlineSlot`.** Both are alignment
|
|
391
411
|
before they are semantics. A mark beside the VALUE indents every row it marks by its own
|
|
392
412
|
width plus a gap, so an unmarked row needs a spacer of exactly the glyph's width to keep up —
|
package/docs/catalog.md
CHANGED
|
@@ -179,6 +179,17 @@ axes carry every shape it takes, and BOTH are decisions about meaning, not looks
|
|
|
179
179
|
|
|
180
180
|
A record's Progress and a desk's to-do list are the same component two booleans apart.
|
|
181
181
|
|
|
182
|
+
**A row can carry a DECIDED outcome instead of a tick — `ChecklistItem.verdict` (`"pass"` /
|
|
183
|
+
`"fail"`).** This is a per-ROW axis, not a list-level one: a QC gate, a reconciliation match,
|
|
184
|
+
any row whose mark means an inspection SETTLED rather than work getting done. `fail` renders in
|
|
185
|
+
red with an X (`StepStatus`'s `fail`, shared with `Stepper`/`AgentRun` via the same `Marker`) —
|
|
186
|
+
distinct from `warning`, which means "still open, needs a look," where `fail` means the answer is
|
|
187
|
+
already no. A verdict is reported, not toggled: passing `onToggle` alongside `verdict` is a
|
|
188
|
+
no-op (withheld internally), because neither of the toggle-capable marks (`CheckboxInput`,
|
|
189
|
+
`CheckCircle`) has a fail-shaped state — a caller with an inspector who taps to CONFIRM a
|
|
190
|
+
pass/fail should drive that from its own control, then set `verdict` from the result, never
|
|
191
|
+
reach for `done`+`onToggle` on the same row a verdict already settled.
|
|
192
|
+
|
|
182
193
|
**A row carries its TITLE and its own stamp, and nothing else.** A checklist REPORTS: it does not
|
|
183
194
|
collect, and it does not PREVIEW. A field some section owns — an assignee, an address, a portal
|
|
184
195
|
login — is NAMED as a gap by a `ChecklistNote` whose `action` jumps to that section, never edited
|
|
@@ -1266,6 +1277,30 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1266
1277
|
trust by summing exactly what its label claims. `LedgerBasis` is demoed on the gallery's
|
|
1267
1278
|
Charts page instead — a delivery order has no base to compute from, and inventing one to
|
|
1268
1279
|
place a component is how invented needs start.
|
|
1280
|
+
- **`running_ledger`** — `RunningLedger`: a CHRONOLOGICAL, self-totalling ledger — a stock
|
|
1281
|
+
kardex, a statement of account — where every row carries the balance AS OF that row. Not
|
|
1282
|
+
`Ledger`: that compound closes a fixed set of charge/receipt groups into one total and has no
|
|
1283
|
+
chronological concept at all. `opening` (label + optional date + the anchor balance) then
|
|
1284
|
+
`rows` (date, label, a SIGNED `amount` — positive in/debit, negative out/credit, one field
|
|
1285
|
+
rather than paired columns so the same shape serves a stock quantity and a money statement),
|
|
1286
|
+
each rendered with its running balance **computed by the component**, never accepted as a
|
|
1287
|
+
caller-supplied number — a running balance is exactly the figure that must equal what it
|
|
1288
|
+
accumulates, and a caller-supplied one is the one kind of drift no reader can catch by looking.
|
|
1289
|
+
`format` is ONE formatter for the whole ledger (`formatMoney`, or a quantity formatter with a
|
|
1290
|
+
unit suffix) — every row shares one unit, which is what lets the column state it once.
|
|
1291
|
+
`criticalAtOrBelow` reads a row's balance in danger tone once it reaches a caller-declared
|
|
1292
|
+
floor (a stock-out, a negative balance) — **the ONLY tone this component owns**, evaluated
|
|
1293
|
+
independently on EVERY balance in the column: each row, the opening anchor (so an already-critical
|
|
1294
|
+
opening reads danger even with zero rows following it), and the closing line. A row's `amount`
|
|
1295
|
+
is never auto-toned by its arithmetic sign, because an OUT movement is routine business as often
|
|
1296
|
+
as an IN one and colouring every negative red claims a meaning the number alone does not carry. Closes with a `Divider`-set current-balance line at `Ledger`'s own
|
|
1297
|
+
`LedgerTotal` weight, `closingLabel` overriding the locale default ("Current balance") for a
|
|
1298
|
+
domain the pack cannot own generically ("Tồn kho hiện tại"). No `examples/` worked example yet
|
|
1299
|
+
— a stock kardex or an AR statement of account is a genuinely different shape from every
|
|
1300
|
+
existing template (the 3-statement accrual grammar in `tpl_statements`, the fleet-register
|
|
1301
|
+
drawer in `tpl_stock`), so forcing it into either would be a mismatch rather than a worked
|
|
1302
|
+
example; proven against real, live deployed consumers instead (a warehouse and a receivables
|
|
1303
|
+
app) until a template of its own earns its place.
|
|
1269
1304
|
- **`reference_field`** — `ReferenceField`: a reference to ANOTHER RECORD, rendered as a
|
|
1270
1305
|
FIELD VALUE — the kit's inline-editor surface (so a pointer sits in the value column
|
|
1271
1306
|
like the editors above and below it), whose press opens a PEEK of that record's facts.
|
|
@@ -1329,6 +1364,22 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1329
1364
|
reference is a FIELD, not a section — one pointer to one other row never earns a heading
|
|
1330
1365
|
and a rail entry. Worked example: [`tpl_record`](../examples/tpl_record.tsx) § General
|
|
1331
1366
|
(customer) and its Transport parties.
|
|
1367
|
+
- **`related_record_row`** — `RelatedRecordRow`: a pointer to a record in a DIFFERENT app's
|
|
1368
|
+
table — never `ReferenceField`, which peeks an in-WORKSPACE link this app's own table holds
|
|
1369
|
+
(a facts draft, Change/Clear). A suite of apps that deploys one bundle per department has no
|
|
1370
|
+
in-app route to a sibling's record and no local fields to peek, so this only ever reports:
|
|
1371
|
+
`kind` (an `Eyebrow` naming what the reference IS — "Lệnh sản xuất"), `code` (the record's own
|
|
1372
|
+
human id), an optional `status` (pass a `Badge`), and `onOpen` — the HOST's job, typically
|
|
1373
|
+
`openExternal(url)` from `@lotics/app-sdk` into the sibling app's own deployed URL, since only
|
|
1374
|
+
the calling app's author knows that app's subdomain and its route shape for a record page.
|
|
1375
|
+
Omitting `onOpen` renders a plain, non-interactive reference — never a press target wired to
|
|
1376
|
+
nothing, which is worse than no affordance. Composes as a `DetailRow`'s value for a single
|
|
1377
|
+
relationship, or repeats plainly (no per-row label) in a list for several — the surrounding
|
|
1378
|
+
label or heading is the caller's, this renders one reference. No `examples/` worked example
|
|
1379
|
+
yet, for the same reason as `running_ledger` above: what this component is FOR — a reference
|
|
1380
|
+
into a genuinely separate deployed app — has no natural home inside any single-app template in
|
|
1381
|
+
this repo without inventing a fictional sibling app to point at; proven against real, live
|
|
1382
|
+
deployed consumers instead.
|
|
1332
1383
|
- **`peek`** — `Peek`: drill-down for a reference — press a name/id where it appears and get its
|
|
1333
1384
|
details in an anchored popover, without leaving the screen; keep the content a summary with ONE
|
|
1334
1385
|
action to the full record. **`variant` picks the register the TRIGGER sits in, and getting it
|
|
@@ -1356,8 +1407,9 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1356
1407
|
- **`table`** — `Table` + `TableRow` + `TableCell`: the paginated high-volume register
|
|
1357
1408
|
(columns defined once, `sortLabels` localizable; rows are `PressableRow`-based).
|
|
1358
1409
|
CONTAINER-RESPONSIVE with no prop (measures itself, like `Breakdown`/`DetailTable`): when
|
|
1359
|
-
the width can't fit every column it hides columns by `TableColumn.priority` (
|
|
1360
|
-
|
|
1410
|
+
the width can't fit every column it hides columns by `TableColumn.priority` (**`1` = highest,
|
|
1411
|
+
as in P1/P2/P3** — the smallest number is the last to go; default = column order, rightmost
|
|
1412
|
+
first; column 0 — the identity — never drops), and
|
|
1361
1413
|
below the two-column floor every row STACKS: the identity cell (column 0) HEADS the pile
|
|
1362
1414
|
unlabelled — its column name would only restate the value — and each REMAINING cell renders
|
|
1363
1415
|
as a `DetailRow`-spread line (muted label left, value at the right edge — one vocabulary with
|
|
@@ -1368,6 +1420,13 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1368
1420
|
On pressable rows dropped values stay one tap away — the row opens the record; a READ-ONLY
|
|
1369
1421
|
register (rows without `onPress`) has no door, so give the columns it can't afford to lose
|
|
1370
1422
|
a low `priority` (e.g. `priority: 1` — outlives its right-side neighbours).
|
|
1423
|
+
**Set `priority` from what the page already told the reader matters, never leave it at the
|
|
1424
|
+
default.** A `SummaryLine`/`KPIStrip` figure above the register is a claim that ONE column is
|
|
1425
|
+
the reason to scan this page; left unset, `priority` defaults to column order and sheds
|
|
1426
|
+
right-to-left, so that exact column is the first thing table_fit drops the moment the
|
|
1427
|
+
container narrows — the banner keeps promising a number the row beneath it can no longer
|
|
1428
|
+
show. The fix is one line per column, not a rewrite: rank by what the summary already flagged
|
|
1429
|
+
as the point, not by where the column happens to sit in the header row.
|
|
1371
1430
|
**`detail` + `expanded` reveal a row's detail BENEATH it instead of navigating away.** A
|
|
1372
1431
|
register answers "which one"; the moment it cannot answer "and what about it" the reader is
|
|
1373
1432
|
sent to a drawer, loses the surrounding rows, and pays a navigation to come back — which is
|
|
@@ -1647,7 +1706,11 @@ component rather than showing it at zero.
|
|
|
1647
1706
|
- **`matrix`** — `Matrix`: the PIVOT cross-tab — band-compound `Matrix` root +
|
|
1648
1707
|
`Matrix.Header` (corner + axis labels) + `Matrix.Grid` (`display` number|heat|both — the
|
|
1649
1708
|
cells, pressable, the value IN the cell) + `Matrix.Totals` (row + column + grand) +
|
|
1650
|
-
`Matrix.Legend`. Pick over `Heatmap` when the NUMBER and totals matter.
|
|
1709
|
+
`Matrix.Legend`. Pick over `Heatmap` when the NUMBER and totals matter. Figures are
|
|
1710
|
+
RIGHT-aligned — they are `tabular`, and centring them defeats the aligned column that is
|
|
1711
|
+
the reason to read a pivot at all — and row labels carry default ink, because a row's
|
|
1712
|
+
label is its identity, not chrome. `MATRIX_COL_GAP` is exported for a caller budgeting
|
|
1713
|
+
its own column widths.
|
|
1651
1714
|
- **`matrix_totals`** — the data layer for `Matrix`: `matrixTotals` (the cross-tab
|
|
1652
1715
|
aggregation) + `MatrixAxisItem` / `MatrixCellRef` / `MatrixTotalsResult`; React-free, so a
|
|
1653
1716
|
KPI can be driven off the same numbers the grid shows.
|
|
@@ -1658,7 +1721,13 @@ component rather than showing it at zero.
|
|
|
1658
1721
|
unsorted and out-of-bounds ranges all normalize; no input drops a character.
|
|
1659
1722
|
- **`legend_item`** — `LegendItem`: one swatch + label of a chart legend.
|
|
1660
1723
|
- **`remainder_meter`** — `RemainderMeter`: allocated-vs-remaining meter
|
|
1661
|
-
(`RemainderMeterLabels` localized via the provider).
|
|
1724
|
+
(`RemainderMeterLabels` localized via the provider). **A THREE-way comparison (ordered vs.
|
|
1725
|
+
received vs. invoiced, say) is TWO stacked meters against the same `total`** — one per pairwise
|
|
1726
|
+
relationship (ordered-vs-received, ordered-vs-invoiced) — not a new component: the primitive's
|
|
1727
|
+
whole model is one source distributed one way, and a caller with a second relationship to show
|
|
1728
|
+
reaches for a second instance before reaching for a wider one. Extract a true 3-column strip
|
|
1729
|
+
only once a SECOND real caller needs the exact same 3-way shape (the kit's own
|
|
1730
|
+
extract-on-the-second-hand-roll rule) — one confirmed need is not yet a pattern.
|
|
1662
1731
|
- **`allocation_row`** — `AllocationRow`: one target's row in an allocation surface (pairs
|
|
1663
1732
|
with `RemainderMeter`).
|
|
1664
1733
|
|
package/docs/composition.md
CHANGED
|
@@ -115,7 +115,15 @@ restyle a heading level per-page.
|
|
|
115
115
|
(`##` — xl semibold, optional muted `description` and `info` popover), everything left-aligned
|
|
116
116
|
at the column edge; `SectionHeadingMeta`, any VIEW control, and the section's own ADD ride the
|
|
117
117
|
heading row's right edge (the title's `flex: 1` pushes them) — see the add-placement law
|
|
118
|
-
below.
|
|
118
|
+
below. **The row is `flexDirection: "row"` and never wraps**, so a long control label doesn't
|
|
119
|
+
drop to its own line at phone width — it holds its own width and the title's `flex: 1` gives
|
|
120
|
+
up whatever is left, taking the `description` down with it. A label that repeats the heading
|
|
121
|
+
verbatim is the usual way this gets long enough to bite: measured on a real section, an
|
|
122
|
+
11-character title-plus-button pair left the title a comfortable column, and the SAME text
|
|
123
|
+
duplicated onto the button squeezed both title and description into a 107px ribbon wrapping
|
|
124
|
+
one or two words per line for eleven lines total. The button-label law already asks for
|
|
125
|
+
2–4 words; a heading-row control is the case where skipping it has a measurable width cost,
|
|
126
|
+
not just a wordier button. A COUNT earns that slot only when what it counts is not fully on screen — collapsed,
|
|
119
127
|
paged, or long enough that the reader would have to scan to know. Over a list or thread
|
|
120
128
|
rendered whole directly beneath its own heading it restates what is already in view, and the
|
|
121
129
|
heading reads busier for it. The page column is a **`SectionStack`** — it owns the
|
package/docs/reviewing.md
CHANGED
|
@@ -308,6 +308,14 @@ A resting register is the cheapest thing to screenshot and the least likely to h
|
|
|
308
308
|
no option colours). The fix is often config or a write path, not CSS.
|
|
309
309
|
- **The obvious column may be the empty one.** Reading only the field the composer writes ships a
|
|
310
310
|
feature empty on every row and correct in code review.
|
|
311
|
+
- **List every field a write path SETS and check each one has a render on the record's own read
|
|
312
|
+
surface.** A screen with no visual defect can still be hiding real stored data: a workflow or
|
|
313
|
+
form commits a field, the record page never grew a row for it, and nothing about the page
|
|
314
|
+
LOOKS wrong — there is no empty box, no broken layout, just an absence with nothing marking
|
|
315
|
+
it. Pull the field list from the write path (a workflow's `set`, a mutation's payload), not
|
|
316
|
+
from the screen, then confirm each one against the record's own DetailRow/Text tree; where one
|
|
317
|
+
is missing, query the record directly to prove the value is really sitting there unseen before
|
|
318
|
+
calling it a fix. A screenshot cannot catch this — it only shows what already renders.
|
|
311
319
|
- **A fixture can fail in the generous direction too**: an image tile pointed at a dead URL renders
|
|
312
320
|
nothing, so the chip designed to survive a photo was never tested against one.
|
|
313
321
|
- **Grep rendered text for internal identifiers — first probe, every audit** —
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lotics/ui",
|
|
3
|
-
"version": "44.
|
|
3
|
+
"version": "44.9.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./vite": {
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
"./use_selection": "./src/use_selection.ts",
|
|
58
58
|
"./use_section_nav": "./src/use_section_nav.ts",
|
|
59
59
|
"./ledger": "./src/ledger.tsx",
|
|
60
|
+
"./running_ledger": "./src/running_ledger.tsx",
|
|
60
61
|
"./use_selection_mode": "./src/use_selection_mode.ts",
|
|
61
62
|
"./file_thumbnail_grid": "./src/file_thumbnail_grid.tsx",
|
|
62
63
|
"./uploading_thumbnail": "./src/uploading_thumbnail.tsx",
|
|
@@ -208,6 +209,7 @@
|
|
|
208
209
|
"./counter": "./src/counter.tsx",
|
|
209
210
|
"./link": "./src/link.tsx",
|
|
210
211
|
"./reference_field": "./src/reference_field.tsx",
|
|
212
|
+
"./related_record_row": "./src/related_record_row.tsx",
|
|
211
213
|
"./text_disclosure": "./src/text_disclosure.tsx",
|
|
212
214
|
"./text_link": "./src/text_link.tsx",
|
|
213
215
|
"./sort_header": "./src/sort_header.tsx",
|
package/src/checklist.tsx
CHANGED
|
@@ -209,6 +209,15 @@ export interface ChecklistItemProps extends StepPositional {
|
|
|
209
209
|
accessibilityLabel?: string;
|
|
210
210
|
/** Ticked. */
|
|
211
211
|
done?: boolean;
|
|
212
|
+
/**
|
|
213
|
+
* A DECIDED outcome, not a completion tick — a QC pass/fail, a match that
|
|
214
|
+
* held or didn't. Takes over the row's mark from `done`/`current` (a
|
|
215
|
+
* verdict is reached by definition) and renders `fail` in red with an X, so
|
|
216
|
+
* a scan of the run tells "settled" from "settled, and the answer is no" —
|
|
217
|
+
* ticking every row and separately noting a rejection in prose is the
|
|
218
|
+
* failure this exists to replace.
|
|
219
|
+
*/
|
|
220
|
+
verdict?: "pass" | "fail";
|
|
212
221
|
/** The row the reader is waiting on — medium weight, so a run full of ticks
|
|
213
222
|
* still says which one is owed. */
|
|
214
223
|
current?: boolean;
|
|
@@ -241,6 +250,7 @@ export function ChecklistItem({
|
|
|
241
250
|
title,
|
|
242
251
|
accessibilityLabel,
|
|
243
252
|
done,
|
|
253
|
+
verdict,
|
|
244
254
|
current,
|
|
245
255
|
onToggle,
|
|
246
256
|
trailing,
|
|
@@ -249,10 +259,18 @@ export function ChecklistItem({
|
|
|
249
259
|
children,
|
|
250
260
|
...positional
|
|
251
261
|
}: ChecklistItemProps) {
|
|
262
|
+
const settled = done || verdict != null;
|
|
252
263
|
return (
|
|
253
264
|
<Step
|
|
254
|
-
status={done ? "done" : current ? "current" : "upcoming"}
|
|
255
|
-
|
|
265
|
+
status={verdict === "fail" ? "fail" : verdict === "pass" ? "done" : done ? "done" : current ? "current" : "upcoming"}
|
|
266
|
+
// A verdict is DECIDED elsewhere (an inspection, an AI check) and
|
|
267
|
+
// reported here, not toggled — passing `onToggle` through would put a
|
|
268
|
+
// completion checkbox on a row whose mark already means something else,
|
|
269
|
+
// and neither `CheckCircle` nor `CheckboxInput` (the interactive marks)
|
|
270
|
+
// know a `fail` state exists, so it would silently render as "not done"
|
|
271
|
+
// instead. The non-interactive `Marker` does, so this keeps a verdict
|
|
272
|
+
// row on that path.
|
|
273
|
+
onToggle={verdict != null ? undefined : onToggle}
|
|
256
274
|
accessibilityLabel={accessibilityLabel ?? title}
|
|
257
275
|
{...positional}
|
|
258
276
|
>
|
|
@@ -262,7 +280,9 @@ export function ChecklistItem({
|
|
|
262
280
|
WRAPS, so a long title drops its stamp to the next line rather than
|
|
263
281
|
crushing it. */}
|
|
264
282
|
<View style={{ flexDirection: "row", alignItems: "baseline", gap: 10, flexWrap: "wrap" }}>
|
|
265
|
-
|
|
283
|
+
{/* A fail stays in default ink, never muted — muting reads as
|
|
284
|
+
"settled, move on" and a rejection is the opposite of that. */}
|
|
285
|
+
<Text size="sm" weight={current ? "medium" : "regular"} color={verdict === "fail" ? "danger" : settled ? "muted" : "default"}>
|
|
266
286
|
{title}
|
|
267
287
|
</Text>
|
|
268
288
|
{trailing}
|
package/src/ledger.tsx
CHANGED
|
@@ -44,8 +44,11 @@ function useLedger(): LedgerContextValue {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/** Signed display: negatives render as "− <abs>" (a receipt against charges).
|
|
47
|
-
* `n + 0` normalizes -0 (a zero receipts sum passed negated) to plain 0.
|
|
48
|
-
|
|
47
|
+
* `n + 0` normalizes -0 (a zero receipts sum passed negated) to plain 0.
|
|
48
|
+
* Exported for `RunningLedger`, which shares this exact glyph rule for every
|
|
49
|
+
* money/quantity figure it renders — one definition, so the two components'
|
|
50
|
+
* output can never drift into two different negative-number conventions. */
|
|
51
|
+
export function signed(format: (n: number) => string, n: number) {
|
|
49
52
|
return n < 0 ? `− ${format(-n)}` : format(n + 0);
|
|
50
53
|
}
|
|
51
54
|
|
|
@@ -234,7 +237,7 @@ export function LedgerRow(props: LedgerRowProps) {
|
|
|
234
237
|
{label}
|
|
235
238
|
</Text>
|
|
236
239
|
{meta ? (
|
|
237
|
-
<Text size="xs" color="muted" numberOfLines={1}>
|
|
240
|
+
<Text size="xs" color="muted" numberOfLines={1} style={styles.yield}>
|
|
238
241
|
{meta}
|
|
239
242
|
</Text>
|
|
240
243
|
) : null}
|
|
@@ -354,6 +357,13 @@ const styles = StyleSheet.create({
|
|
|
354
357
|
rowHovered: { backgroundColor: colors.zinc[50] },
|
|
355
358
|
grow: { flexGrow: 1, flexShrink: 1 },
|
|
356
359
|
shrink: { flexShrink: 1 },
|
|
360
|
+
// The META yields BEFORE the label, and by more. Only the label carried a
|
|
361
|
+
// shrink, so under width pressure the row'''s IDENTITY was the one thing that
|
|
362
|
+
// gave way while its subordinate qualifier held full width — a narrow ledger
|
|
363
|
+
// read "Tổng thu (chư…" beside an intact "03/06, Cash". A caption qualifies a
|
|
364
|
+
// number; the label names it, and the name is what a reader needs when the
|
|
365
|
+
// row is too tight for both.
|
|
366
|
+
yield: { flexShrink: 3 },
|
|
357
367
|
total: { gap: 6 },
|
|
358
368
|
// The BASIS row, the only one whose two sides can differ in height: its label may
|
|
359
369
|
// carry a stacked `meta` while the figure stays one line. Centring then measures the
|
package/src/locale.tsx
CHANGED
|
@@ -74,6 +74,9 @@ export interface LoticsLocale {
|
|
|
74
74
|
clarify: { otherPlaceholder: string; back: string; next: string; cancel: string; submit: string };
|
|
75
75
|
/** `Ledger`: the screen-reader name of a peekable row. */
|
|
76
76
|
ledger: { rowDetails: (label: string) => string };
|
|
77
|
+
/** `RunningLedger`: the closing line's label ("Số dư hiện tại", "Tồn kho
|
|
78
|
+
* hiện tại") — the ledger's one derived, always-current figure. */
|
|
79
|
+
runningLedger: { currentBalance: string };
|
|
77
80
|
/** `Step`/`ChecklistItem`'s toggleable MARKER, when the caller names no
|
|
78
81
|
* label of its own. A pipeline stage always passes its title, so this is the
|
|
79
82
|
* bare-`Step` fallback. */
|
|
@@ -207,6 +210,10 @@ export interface LoticsLocale {
|
|
|
207
210
|
* a SOURCE with different words. */
|
|
208
211
|
fileRow: { open: (name: string) => string };
|
|
209
212
|
sources: { open: (label: string) => string; heading: string };
|
|
213
|
+
/** `RelatedRecordRow`: the accessible name for its press target, when the
|
|
214
|
+
* caller has not overridden it — "<kind> <code>", e.g. "Kế hoạch sản xuất
|
|
215
|
+
* KH-2026-0001". */
|
|
216
|
+
relatedRecordRow: { open: (kind: string, code: string) => string };
|
|
210
217
|
/** `Counter`: the two steppers, named for the value they move. */
|
|
211
218
|
counter: { decrease: (label: string) => string; increase: (label: string) => string };
|
|
212
219
|
/** `AllocationRow`: the a11y name of the amount input (naming its destination),
|
|
@@ -303,6 +310,7 @@ export const en: LoticsLocale = {
|
|
|
303
310
|
inline: { saveError: "Couldn't save. Try again.", save: "Save", cancel: "Cancel" },
|
|
304
311
|
clarify: { otherPlaceholder: "Or type your own answer…", back: "Back", next: "Next", cancel: "Cancel", submit: "Submit" },
|
|
305
312
|
ledger: { rowDetails: (label) => `${label} details` },
|
|
313
|
+
runningLedger: { currentBalance: "Current balance" },
|
|
306
314
|
stepper: { complete: "Complete step", progress: "Progress" },
|
|
307
315
|
stepProgress: {
|
|
308
316
|
complete: (total) => `Complete (${total}/${total})`,
|
|
@@ -409,6 +417,7 @@ export const en: LoticsLocale = {
|
|
|
409
417
|
infoPopover: { more: "More information", about: (label) => `About ${label}` },
|
|
410
418
|
fileRow: { open: (name) => `Open ${name}` },
|
|
411
419
|
sources: { open: (label) => `Open ${label}`, heading: "Sources" },
|
|
420
|
+
relatedRecordRow: { open: (kind, code) => `Open ${kind} ${code}` },
|
|
412
421
|
counter: { decrease: (label) => `Decrease ${label}`, increase: (label) => `Increase ${label}` },
|
|
413
422
|
allocationRow: {
|
|
414
423
|
allocateTo: (label) => `Allocate to ${label}`,
|
|
@@ -488,6 +497,7 @@ export const vi: LoticsLocale = {
|
|
|
488
497
|
inline: { saveError: "Không lưu được. Thử lại.", save: "Lưu", cancel: "Hủy" },
|
|
489
498
|
clarify: { otherPlaceholder: "Hoặc nhập câu trả lời khác…", back: "Quay lại", next: "Tiếp", cancel: "Hủy", submit: "Gửi" },
|
|
490
499
|
ledger: { rowDetails: (label) => `Chi tiết ${label}` },
|
|
500
|
+
runningLedger: { currentBalance: "Số dư hiện tại" },
|
|
491
501
|
stepper: { complete: "Hoàn thành bước", progress: "Tiến trình" },
|
|
492
502
|
stepProgress: {
|
|
493
503
|
complete: (total) => `Hoàn tất (${total}/${total})`,
|
|
@@ -594,6 +604,7 @@ export const vi: LoticsLocale = {
|
|
|
594
604
|
infoPopover: { more: "Thông tin thêm", about: (label) => `Thông tin về ${label}` },
|
|
595
605
|
fileRow: { open: (name) => `Mở ${name}` },
|
|
596
606
|
sources: { open: (label) => `Mở ${label}`, heading: "Nguồn" },
|
|
607
|
+
relatedRecordRow: { open: (kind, code) => `Mở ${kind} ${code}` },
|
|
597
608
|
counter: { decrease: (label) => `Giảm ${label}`, increase: (label) => `Tăng ${label}` },
|
|
598
609
|
allocationRow: {
|
|
599
610
|
allocateTo: (label) => `Phân bổ vào ${label}`,
|
package/src/matrix.tsx
CHANGED
|
@@ -25,6 +25,11 @@ interface MatrixContextValue {
|
|
|
25
25
|
totalColWidth: number;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/** Gap between a matrix's columns. Exported because a caller sizing its own
|
|
29
|
+
* column budget needs the same number the grid renders with — hard-coding it
|
|
30
|
+
* is how a width budget and the layout drift apart. */
|
|
31
|
+
export const MATRIX_COL_GAP = 3;
|
|
32
|
+
|
|
28
33
|
const MatrixContext = createContext<MatrixContextValue | null>(null);
|
|
29
34
|
|
|
30
35
|
function useMatrix(): MatrixContextValue {
|
|
@@ -138,12 +143,12 @@ function MatrixHeader({ corner, totalLabel }: MatrixHeaderProps) {
|
|
|
138
143
|
)}
|
|
139
144
|
</View>
|
|
140
145
|
{cols.map((c) => (
|
|
141
|
-
<Text key={c.key} size="xs" color="muted" align="
|
|
146
|
+
<Text key={c.key} size="xs" color="muted" align="right" numberOfLines={1} style={styles.colCell}>
|
|
142
147
|
{c.label}
|
|
143
148
|
</Text>
|
|
144
149
|
))}
|
|
145
150
|
{hasTotals ? (
|
|
146
|
-
<Text size="xs" color="muted" weight="medium" align="
|
|
151
|
+
<Text size="xs" color="muted" weight="medium" align="right" numberOfLines={1} style={[styles.totalCol, { width: totalColWidth }]}>
|
|
147
152
|
{resolvedTotalLabel}
|
|
148
153
|
</Text>
|
|
149
154
|
) : null}
|
|
@@ -163,14 +168,14 @@ function MatrixGrid({ display = "both" }: MatrixGridProps) {
|
|
|
163
168
|
<View style={styles.grid}>
|
|
164
169
|
{rows.map((r) => (
|
|
165
170
|
<View key={r.key} style={styles.row}>
|
|
166
|
-
<Text size="sm"
|
|
171
|
+
<Text size="sm" numberOfLines={1} style={[styles.rowLabel, { width: rowLabelWidth }]}>
|
|
167
172
|
{r.label}
|
|
168
173
|
</Text>
|
|
169
174
|
{cols.map((c) => (
|
|
170
175
|
<MatrixCell key={c.key} ctx={ctx} display={display} row={r} col={c} value={value(r.key, c.key)} />
|
|
171
176
|
))}
|
|
172
177
|
{hasTotals ? (
|
|
173
|
-
<Text size="sm" weight="semibold" tabular align="
|
|
178
|
+
<Text size="sm" weight="semibold" tabular align="right" numberOfLines={1} style={[styles.totalCol, { width: totalColWidth }]}>
|
|
174
179
|
{formatValue(rowTotals.get(r.key) ?? 0)}
|
|
175
180
|
</Text>
|
|
176
181
|
) : null}
|
|
@@ -206,7 +211,7 @@ function MatrixCell({ ctx, display, row, col, value }: MatrixCellProps) {
|
|
|
206
211
|
const label = `${row.label}, ${col.label}: ${formatValue(value)}`;
|
|
207
212
|
|
|
208
213
|
const content = showNumber ? (
|
|
209
|
-
<Text size="sm" weight="medium" tabular align="
|
|
214
|
+
<Text size="sm" weight="medium" tabular align="right" color={value === 0 ? "zinc-500" : "default"}>
|
|
210
215
|
{value === 0 ? "" : formatValue(value)}
|
|
211
216
|
</Text>
|
|
212
217
|
) : null;
|
|
@@ -254,11 +259,11 @@ function MatrixTotals({ label }: MatrixTotalsProps) {
|
|
|
254
259
|
{label ?? locale.matrix.total}
|
|
255
260
|
</Text>
|
|
256
261
|
{cols.map((c) => (
|
|
257
|
-
<Text key={c.key} size="sm" weight="semibold" tabular align="
|
|
262
|
+
<Text key={c.key} size="sm" weight="semibold" tabular align="right" numberOfLines={1} style={styles.colCell}>
|
|
258
263
|
{formatValue(colTotals.get(c.key) ?? 0)}
|
|
259
264
|
</Text>
|
|
260
265
|
))}
|
|
261
|
-
<Text size="sm" weight="semibold" tabular align="
|
|
266
|
+
<Text size="sm" weight="semibold" tabular align="right" numberOfLines={1} style={[styles.totalCol, { width: totalColWidth }]}>
|
|
262
267
|
{formatValue(grandTotal)}
|
|
263
268
|
</Text>
|
|
264
269
|
</View>
|
|
@@ -295,21 +300,21 @@ Matrix.Legend = MatrixLegend;
|
|
|
295
300
|
|
|
296
301
|
const styles = StyleSheet.create({
|
|
297
302
|
container: {
|
|
298
|
-
gap:
|
|
303
|
+
gap: MATRIX_COL_GAP,
|
|
299
304
|
},
|
|
300
305
|
headRow: {
|
|
301
306
|
flexDirection: "row",
|
|
302
307
|
alignItems: "center",
|
|
303
|
-
gap:
|
|
308
|
+
gap: MATRIX_COL_GAP,
|
|
304
309
|
paddingBottom: 2,
|
|
305
310
|
},
|
|
306
311
|
grid: {
|
|
307
|
-
gap:
|
|
312
|
+
gap: MATRIX_COL_GAP,
|
|
308
313
|
},
|
|
309
314
|
row: {
|
|
310
315
|
flexDirection: "row",
|
|
311
316
|
alignItems: "center",
|
|
312
|
-
gap:
|
|
317
|
+
gap: MATRIX_COL_GAP,
|
|
313
318
|
},
|
|
314
319
|
rowLabel: {
|
|
315
320
|
paddingRight: 8,
|
|
@@ -333,7 +338,7 @@ const styles = StyleSheet.create({
|
|
|
333
338
|
totalsRow: {
|
|
334
339
|
flexDirection: "row",
|
|
335
340
|
alignItems: "center",
|
|
336
|
-
gap:
|
|
341
|
+
gap: MATRIX_COL_GAP,
|
|
337
342
|
marginTop: 3,
|
|
338
343
|
paddingTop: 8,
|
|
339
344
|
borderTopWidth: 1,
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import { Text } from "./text";
|
|
4
|
+
import { Eyebrow } from "./eyebrow";
|
|
5
|
+
import { Icon } from "./icon";
|
|
6
|
+
import { colors } from "./colors";
|
|
7
|
+
import { PressableHighlight } from "./pressable_highlight";
|
|
8
|
+
import { useLoticsLocale } from "./locale";
|
|
9
|
+
|
|
10
|
+
export interface RelatedRecordRowProps {
|
|
11
|
+
/** What KIND of record this is — the table/app it lives in ("Kế hoạch sản
|
|
12
|
+
* xuất", "Lệnh sản xuất"). Rendered as an `Eyebrow` naming the value below
|
|
13
|
+
* it, the same job an `Eyebrow` does in a cell or a metric caption. */
|
|
14
|
+
kind: string;
|
|
15
|
+
/** The record's own human code ("KH-2026-0001"). */
|
|
16
|
+
code: string;
|
|
17
|
+
/** The record's status, rendered as-is beside the code — pass a `Badge`.
|
|
18
|
+
* Omit for a reference with no lifecycle state worth carrying here. */
|
|
19
|
+
status?: ReactNode;
|
|
20
|
+
/**
|
|
21
|
+
* Opens the record. THE HOST performs the navigation — typically
|
|
22
|
+
* `openExternal(url)` from `@lotics/app-sdk` into a sibling app's own
|
|
23
|
+
* deployed URL, since a suite of apps like this one is several SEPARATELY
|
|
24
|
+
* DEPLOYED bundles with no shared router; only the calling app knows the
|
|
25
|
+
* target app's subdomain and its own route shape for a record page. Omit
|
|
26
|
+
* for a report-only reference with nowhere to send the reader (a linked
|
|
27
|
+
* record whose app cannot be reached, or a same-app case better served by
|
|
28
|
+
* an in-app navigate instead of this component at all).
|
|
29
|
+
*/
|
|
30
|
+
onOpen?: () => void;
|
|
31
|
+
/** Accessible name for the row when `onOpen` is set. Defaults to
|
|
32
|
+
* "<kind> <code>" — override when that reads ambiguously (the same kind
|
|
33
|
+
* repeated down a list of several). */
|
|
34
|
+
accessibilityLabel?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A pointer to ONE record living in a DIFFERENT app's table — never a route
|
|
39
|
+
* inside this bundle, because a document-desk suite deploys one app per
|
|
40
|
+
* department with no in-app path to a sibling's record. Composes as a
|
|
41
|
+
* `DetailRow`'s value for a single relationship ("Kế hoạch sản xuất" →
|
|
42
|
+
* one linked plan) or repeats plainly in a list for several (an order's
|
|
43
|
+
* three downstream production plans) — the surrounding label/heading is the
|
|
44
|
+
* caller's choice, this renders one reference.
|
|
45
|
+
*
|
|
46
|
+
* NOT `ReferenceField`: that component edits an in-workspace link (peek,
|
|
47
|
+
* Change, Clear, an editable facts draft) for a reference the CURRENT app's
|
|
48
|
+
* own table holds. This one only ever reports — the target record's fields
|
|
49
|
+
* live in a table another app's bundle renders, so there is no draft to
|
|
50
|
+
* open and no picker to reassign from here.
|
|
51
|
+
*/
|
|
52
|
+
export function RelatedRecordRow(props: RelatedRecordRowProps) {
|
|
53
|
+
const { kind, code, status, onOpen, accessibilityLabel } = props;
|
|
54
|
+
const locale = useLoticsLocale();
|
|
55
|
+
const content = (
|
|
56
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
|
57
|
+
<View style={{ flex: 1, minWidth: 0, gap: 2 }}>
|
|
58
|
+
<Eyebrow numberOfLines={1}>{kind}</Eyebrow>
|
|
59
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
|
60
|
+
<Text size="sm" numberOfLines={1}>
|
|
61
|
+
{code}
|
|
62
|
+
</Text>
|
|
63
|
+
{status}
|
|
64
|
+
</View>
|
|
65
|
+
</View>
|
|
66
|
+
{onOpen ? <Icon name="external-link" size={14} color={colors.zinc[400]} /> : null}
|
|
67
|
+
</View>
|
|
68
|
+
);
|
|
69
|
+
if (!onOpen) return content;
|
|
70
|
+
return (
|
|
71
|
+
<PressableHighlight
|
|
72
|
+
focusRing
|
|
73
|
+
onPress={onOpen}
|
|
74
|
+
accessibilityLabel={accessibilityLabel ?? locale.relatedRecordRow.open(kind, code)}
|
|
75
|
+
style={{ paddingVertical: 4 }}
|
|
76
|
+
>
|
|
77
|
+
{content}
|
|
78
|
+
</PressableHighlight>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { StyleSheet, View } from "react-native";
|
|
2
|
+
import { Divider } from "./divider";
|
|
3
|
+
import { Link } from "./link";
|
|
4
|
+
import { signed } from "./ledger";
|
|
5
|
+
import { Text } from "./text";
|
|
6
|
+
import { formatDate } from "./format_date";
|
|
7
|
+
import { useLoticsLocale } from "./locale";
|
|
8
|
+
|
|
9
|
+
export interface RunningLedgerOpening {
|
|
10
|
+
/** What the anchor IS ("Tồn đầu kỳ", "Số dư đầu kỳ"). */
|
|
11
|
+
label: string;
|
|
12
|
+
/** ISO date, when the anchor has one — a stock count date, a statement's
|
|
13
|
+
* period start. Omit for a balance with no dated origin. */
|
|
14
|
+
date?: string;
|
|
15
|
+
/** The balance BEFORE the first row. */
|
|
16
|
+
balance: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface RunningLedgerRow {
|
|
20
|
+
id: string;
|
|
21
|
+
/** ISO date. Rows render in the order given — this component does not sort
|
|
22
|
+
* them, so the caller's own query order IS the chronology on screen. */
|
|
23
|
+
date: string;
|
|
24
|
+
/** What happened ("Nhập kho theo PO-2026-0001", "Thu tiền hóa đơn HD-014"). */
|
|
25
|
+
label: string;
|
|
26
|
+
/** The movement. Signed: positive is IN/debit, negative is OUT/credit — ONE
|
|
27
|
+
* field rather than paired in/out columns, so the same row shape serves a
|
|
28
|
+
* stock quantity and a money statement without either caller picking the
|
|
29
|
+
* other's vocabulary. */
|
|
30
|
+
amount: number;
|
|
31
|
+
/** A trailing reference link (the source PO, the invoice this receipt was
|
|
32
|
+
* matched against). */
|
|
33
|
+
reference?: { label: string; onPress: () => void };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface RunningLedgerProps {
|
|
37
|
+
opening: RunningLedgerOpening;
|
|
38
|
+
/** Chronological rows — oldest first. Each row's running balance is this
|
|
39
|
+
* component's own DERIVED value (`opening.balance` plus every `amount` up
|
|
40
|
+
* to and including that row), never a value the caller supplies: a running
|
|
41
|
+
* balance is exactly the row that MUST equal what it accumulates, and a
|
|
42
|
+
* caller-supplied figure is the one shape of drift no reader can catch by
|
|
43
|
+
* looking. */
|
|
44
|
+
rows: RunningLedgerRow[];
|
|
45
|
+
/** Formats every figure on the balance/amount column — `formatMoney` for a
|
|
46
|
+
* statement, or a quantity formatter (`(n) => \`${n.toLocaleString()} kg\`
|
|
47
|
+
* `) for a stock ledger. ONE formatter for the whole ledger: every row here
|
|
48
|
+
* shares one unit, which is what lets the column state it once instead of
|
|
49
|
+
* per row. */
|
|
50
|
+
format: (n: number) => string;
|
|
51
|
+
/** A row whose running balance lands AT OR BELOW this reads in danger tone
|
|
52
|
+
* — a stock-out, an account gone negative. Omit for a ledger with no
|
|
53
|
+
* critical floor (most money statements; a stock ledger typically passes
|
|
54
|
+
* 0). */
|
|
55
|
+
criticalAtOrBelow?: number;
|
|
56
|
+
/**
|
|
57
|
+
* The closing line's label. Default resolves from the locale pack
|
|
58
|
+
* ("Current balance"), which fits a money statement but not every domain
|
|
59
|
+
* this serves both of on day one — a stock kardex wants "Tồn kho hiện
|
|
60
|
+
* tại", specific vocabulary the locale pack cannot own for every future
|
|
61
|
+
* caller. Override per instance the same way `Sources.label` does.
|
|
62
|
+
*/
|
|
63
|
+
closingLabel?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* A chronological, self-totalling ledger — a stock kardex, a statement of
|
|
68
|
+
* account — where every row carries the balance AS OF that row. NOT `Ledger`:
|
|
69
|
+
* that component closes a fixed set of charge/receipt GROUPS into one total
|
|
70
|
+
* and has no chronological concept at all; reaching for it here would mean
|
|
71
|
+
* inventing a "group" for what is actually a time series and losing the
|
|
72
|
+
* running balance a kardex or a statement is FOR. Composes a `Divider`-closed
|
|
73
|
+
* current-balance line the same way `Ledger`'s own `LedgerTotal` does, so a
|
|
74
|
+
* page mixing this with a `Ledger` elsewhere still reads as one design
|
|
75
|
+
* language.
|
|
76
|
+
*
|
|
77
|
+
* The balance column is DERIVED — see `rows`' own doc. A platform whose
|
|
78
|
+
* rollup engine cannot produce an ordered cumulative aggregate (no per-row
|
|
79
|
+
* stored balance exists for a stock movement today) is exactly the case this
|
|
80
|
+
* is for: opening balance + a plain list of signed deltas needs no schema
|
|
81
|
+
* change on either side to render correctly.
|
|
82
|
+
*/
|
|
83
|
+
export function RunningLedger(props: RunningLedgerProps) {
|
|
84
|
+
const { opening, rows, format, criticalAtOrBelow, closingLabel } = props;
|
|
85
|
+
const locale = useLoticsLocale();
|
|
86
|
+
let running = opening.balance;
|
|
87
|
+
const computed = rows.map((r) => {
|
|
88
|
+
running += r.amount;
|
|
89
|
+
return { row: r, balance: running };
|
|
90
|
+
});
|
|
91
|
+
const closing = computed.length > 0 ? computed[computed.length - 1].balance : opening.balance;
|
|
92
|
+
const closingCritical = criticalAtOrBelow != null && closing <= criticalAtOrBelow;
|
|
93
|
+
// Evaluated independently of `closingCritical` (and of whether there are
|
|
94
|
+
// any rows at all) — an opening balance that already sits at/below the
|
|
95
|
+
// floor must read as critical on ITS OWN row; the closing line below only
|
|
96
|
+
// renders when there is at least one row, so it can never be the sole
|
|
97
|
+
// carrier of this signal.
|
|
98
|
+
const openingCritical = criticalAtOrBelow != null && opening.balance <= criticalAtOrBelow;
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<View style={styles.ledger}>
|
|
102
|
+
<View style={styles.openingRow}>
|
|
103
|
+
<View style={styles.grow}>
|
|
104
|
+
<Text size="sm" weight="medium">
|
|
105
|
+
{opening.label}
|
|
106
|
+
</Text>
|
|
107
|
+
{opening.date ? (
|
|
108
|
+
<Text size="xs" color="muted">
|
|
109
|
+
{formatDate(opening.date)}
|
|
110
|
+
</Text>
|
|
111
|
+
) : null}
|
|
112
|
+
</View>
|
|
113
|
+
<Text size="sm" weight="medium" tabular color={openingCritical ? "danger" : undefined}>
|
|
114
|
+
{signed(format, opening.balance)}
|
|
115
|
+
</Text>
|
|
116
|
+
</View>
|
|
117
|
+
{computed.map(({ row, balance }) => {
|
|
118
|
+
const critical = criticalAtOrBelow != null && balance <= criticalAtOrBelow;
|
|
119
|
+
return (
|
|
120
|
+
<View key={row.id} style={styles.row}>
|
|
121
|
+
<Text size="xs" color="muted" numberOfLines={1} style={styles.date}>
|
|
122
|
+
{formatDate(row.date)}
|
|
123
|
+
</Text>
|
|
124
|
+
<Text size="sm" numberOfLines={1} style={styles.shrink}>
|
|
125
|
+
{row.label}
|
|
126
|
+
</Text>
|
|
127
|
+
{row.reference ? (
|
|
128
|
+
<Link size="xs" onPress={row.reference.onPress} accessibilityLabel={row.reference.label}>
|
|
129
|
+
{row.reference.label}
|
|
130
|
+
</Link>
|
|
131
|
+
) : null}
|
|
132
|
+
<View style={styles.grow} />
|
|
133
|
+
{/* Neutral, never auto-toned by sign: an OUT movement is routine
|
|
134
|
+
business (a stock issue to a filled order, a payment applied)
|
|
135
|
+
as often as an IN one, and colouring every negative red would
|
|
136
|
+
claim a meaning the number alone does not carry — the same
|
|
137
|
+
restraint `LedgerRow`'s own `tone` leaves to its caller. The
|
|
138
|
+
one signal this component DOES own is `criticalAtOrBelow`,
|
|
139
|
+
which is a real domain rule the caller opted into, not a
|
|
140
|
+
guess from the arithmetic sign. */}
|
|
141
|
+
<Text size="sm" tabular style={styles.amount}>
|
|
142
|
+
{signed(format, row.amount)}
|
|
143
|
+
</Text>
|
|
144
|
+
<Text size="sm" weight="medium" tabular color={critical ? "danger" : undefined} style={styles.balance}>
|
|
145
|
+
{signed(format, balance)}
|
|
146
|
+
</Text>
|
|
147
|
+
</View>
|
|
148
|
+
);
|
|
149
|
+
})}
|
|
150
|
+
{computed.length > 0 ? (
|
|
151
|
+
<View style={styles.total}>
|
|
152
|
+
<Divider />
|
|
153
|
+
<View style={styles.row}>
|
|
154
|
+
<Text size="md" weight="semibold" style={styles.grow}>
|
|
155
|
+
{closingLabel ?? locale.runningLedger.currentBalance}
|
|
156
|
+
</Text>
|
|
157
|
+
<Text size="md" weight="semibold" tabular color={closingCritical ? "danger" : undefined}>
|
|
158
|
+
{signed(format, closing)}
|
|
159
|
+
</Text>
|
|
160
|
+
</View>
|
|
161
|
+
</View>
|
|
162
|
+
) : null}
|
|
163
|
+
</View>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const styles = StyleSheet.create({
|
|
168
|
+
// Mirrors `Ledger`'s own outdent + row rhythm so the two read as one design
|
|
169
|
+
// language wherever a page uses both.
|
|
170
|
+
ledger: { gap: 0, marginHorizontal: -8 },
|
|
171
|
+
row: {
|
|
172
|
+
flexDirection: "row",
|
|
173
|
+
alignItems: "center",
|
|
174
|
+
gap: 10,
|
|
175
|
+
minHeight: 28,
|
|
176
|
+
paddingHorizontal: 8,
|
|
177
|
+
},
|
|
178
|
+
openingRow: {
|
|
179
|
+
flexDirection: "row",
|
|
180
|
+
alignItems: "baseline",
|
|
181
|
+
gap: 10,
|
|
182
|
+
minHeight: 28,
|
|
183
|
+
paddingHorizontal: 8,
|
|
184
|
+
marginBottom: 4,
|
|
185
|
+
},
|
|
186
|
+
grow: { flexGrow: 1, flexShrink: 1 },
|
|
187
|
+
shrink: { flexShrink: 1 },
|
|
188
|
+
date: { width: 72 },
|
|
189
|
+
// Fixed widths so the amount and balance columns each keep ONE right edge
|
|
190
|
+
// down the whole run, whatever an individual figure's digit count — the
|
|
191
|
+
// same reasoning `Ledger`'s single money column applies to two columns
|
|
192
|
+
// instead of one.
|
|
193
|
+
amount: { minWidth: 90, textAlign: "right" },
|
|
194
|
+
balance: { minWidth: 100, textAlign: "right" },
|
|
195
|
+
total: { gap: 6, marginTop: 6 },
|
|
196
|
+
});
|
package/src/stepper.tsx
CHANGED
|
@@ -23,8 +23,13 @@ import { NODE, STEP_HEAD_TEXT_LINE, STEP_MARKER_GAP, STEP_ROW_INSET, STEP_ROW_PA
|
|
|
23
23
|
// A node's place in a sequence. `upcoming` = not reached (greyish); `current` =
|
|
24
24
|
// where we are (ring + white centre, pulses when live); `done` = passed (filled);
|
|
25
25
|
// `complete` = the terminal "finished" marker (filled + check); `warning` = an
|
|
26
|
-
// issue at a reached step
|
|
27
|
-
|
|
26
|
+
// issue at a reached step; `fail` = a reached step whose OUTCOME is negative —
|
|
27
|
+
// a verdict, not an issue still open. Distinct from `warning`: amber asks the
|
|
28
|
+
// reader to look, red tells them the answer is already no (a QC reject, a
|
|
29
|
+
// failed match) — collapsing the two into one amber "something's off" loses
|
|
30
|
+
// the one bit that actually matters, whether the row is still actionable or
|
|
31
|
+
// already decided.
|
|
32
|
+
export type StepStatus = "upcoming" | "current" | "done" | "warning" | "fail" | "complete";
|
|
28
33
|
export type StepOrientation = "horizontal" | "vertical";
|
|
29
34
|
|
|
30
35
|
const reached = (s: StepStatus) => s !== "upcoming";
|
|
@@ -302,6 +307,14 @@ export function Step(props: StepProps) {
|
|
|
302
307
|
{onToggle && mark === "box" ? (
|
|
303
308
|
// SELECTION: the square, in the ring's own box so the column and the
|
|
304
309
|
// spine keep their rhythm whichever mark a surface uses.
|
|
310
|
+
// `fail` deliberately reads as unchecked here, not done: neither
|
|
311
|
+
// mark below has a fail-shaped state, and forcing one to `done`
|
|
312
|
+
// would draw a rejected step in the same ink as a passed one —
|
|
313
|
+
// the one distinction a verdict exists to keep. A verdict is
|
|
314
|
+
// reported, not toggled, so a real caller never reaches this
|
|
315
|
+
// combination (`ChecklistItem` withholds `onToggle` once
|
|
316
|
+
// `verdict` is set); this stays correct for anyone driving
|
|
317
|
+
// `Step` directly.
|
|
305
318
|
<View style={{ width: NODE, height: NODE, alignItems: "center", justifyContent: "center" }}>
|
|
306
319
|
<CheckboxInput
|
|
307
320
|
checked={status === "done" || status === "complete"}
|
|
@@ -376,6 +389,17 @@ export function Marker({ status, color, live }: { status: StepStatus; color: str
|
|
|
376
389
|
</View>
|
|
377
390
|
);
|
|
378
391
|
}
|
|
392
|
+
if (status === "fail") {
|
|
393
|
+
// A decided negative: filled in the same solid-red the kit uses for danger
|
|
394
|
+
// everywhere else (deadline.ts's own tone→hue mapping), with a white X —
|
|
395
|
+
// the "done" disc's shape, wrong-answer color, so a scanning eye reads
|
|
396
|
+
// both as "reached and settled" and only the color says which way.
|
|
397
|
+
return (
|
|
398
|
+
<View style={[styles.disc, { backgroundColor: solid("red") }]}>
|
|
399
|
+
<Icon name="x" size={10} color={colors.white} />
|
|
400
|
+
</View>
|
|
401
|
+
);
|
|
402
|
+
}
|
|
379
403
|
if (status === "current") {
|
|
380
404
|
return (
|
|
381
405
|
<View style={styles.discWrap}>
|
package/src/table_fit.ts
CHANGED
|
@@ -20,12 +20,22 @@ export interface TableFitColumn {
|
|
|
20
20
|
key: string;
|
|
21
21
|
/** Fixed width in px; omit for a flexible column. */
|
|
22
22
|
width?: number;
|
|
23
|
-
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
/** This column's claim on space, **`1` = highest**, the way P1/P2/P3 rank a
|
|
24
|
+
* bug: the column you least want to lose gets the SMALLEST number. Said the
|
|
25
|
+
* other way round it is the same rule — the larger the number, the sooner the
|
|
26
|
+
* column drops, and ties drop right-to-left. The FIRST column is the row's
|
|
27
|
+
* identity and never drops.
|
|
26
28
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
+
* Stated rank-first deliberately. "Higher drops first" is accurate and was
|
|
30
|
+
* all this said, but a reader who takes `priority` to mean "bigger is more
|
|
31
|
+
* important" inverts it and gives their most important column the LARGEST
|
|
32
|
+
* number — which is then the first column the fit sheds. That failure is
|
|
33
|
+
* invisible at desk width and shows only on a phone, so it reviews as
|
|
34
|
+
* correct. It has been read backwards once already.
|
|
35
|
+
*
|
|
36
|
+
* An unannotated column defaults to `columns.length + index`, which sits
|
|
37
|
+
* above the small ranks a real annotation uses rather than interleaved with
|
|
38
|
+
* them, so an
|
|
29
39
|
* unannotated register still sheds right-to-left AND an explicit priority is
|
|
30
40
|
* strictly safer than none. Defaulting to the bare index made annotation
|
|
31
41
|
* actively harmful: marking your most important column `priority: 1` TIED it
|