@lotics/ui 28.3.1 → 29.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +12 -1
- package/MIGRATION.md +26 -0
- package/docs/ai_patterns.md +158 -111
- package/docs/catalog.md +100 -25
- package/docs/data_entry.md +13 -0
- package/docs/templates.md +31 -12
- package/examples/tpl_item_list.tsx +201 -28
- package/examples/tpl_record.tsx +390 -123
- package/examples/tpl_task_board.tsx +45 -29
- package/package.json +7 -3
- package/src/agent_run.tsx +1 -1
- package/src/clarify_wizard.tsx +1 -1
- package/src/diff_mark.tsx +110 -0
- package/src/diff_value.tsx +232 -0
- package/src/file_badge.tsx +2 -0
- package/src/file_intake.ts +17 -2
- package/src/file_row.tsx +58 -6
- package/src/file_thumbnail.tsx +113 -30
- package/src/file_thumbnail_grid.tsx +13 -1
- package/src/finding.tsx +2 -3
- package/src/inline_slot.tsx +58 -0
- package/src/inline_static.tsx +5 -23
- package/src/locale.tsx +18 -46
- package/src/mime.ts +76 -0
- package/src/result_header.tsx +1 -1
- package/src/use_change_set.ts +115 -0
- package/src/change_review.tsx +0 -800
package/AGENTS.md
CHANGED
|
@@ -16,7 +16,7 @@ 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) | Which editing pattern for which job — inline edit, fieldset forms, browser-autofill suppression (search controls only), find-or-create (`Combobox`), line items, handoffs, phased records, billing, tags, dispositions, attachments (the `FilesEditor` COMPOUND — root owns selection/gallery/confirm, you compose the bar, a HOST verb reads `useFilesEditorSelection` — plus the three-way file INTAKE: CTA + `FileDropTarget` + `usePasteFiles`), stage gates, the commit-on-blur vs action-press ordering law (the kit gates the press — `pending_commits`). |
|
|
19
|
-
| [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 (
|
|
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; 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); **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
20
|
| [docs/composition.md](./docs/composition.md) | The design-language contract — canvas + content column, heading altitude (incl. eyebrow vs group lead — a label is one or the other), banded cards, register vs inset rows, master-detail `Drawer` on a LIST screen vs a child collection's row EXPANDING inside a record, view controls, RECORD EXTENT (one page, sections scrolled to and never routed to), color discipline, typography, whitespace, and how to TEST an overlay component (a `Popover`-backed surface never mounts under jsdom). |
|
|
21
21
|
| [docs/templates.md](./docs/templates.md) | The map of `examples/tpl_*.tsx` — what shape each template solves and which to start from (copy + adapt, never import) — plus the record-surface composition rules (pipeline order, static shape, decision budget). |
|
|
22
22
|
|
|
@@ -89,6 +89,17 @@ CURRENT major only — upgrading an app across majors is `MIGRATION.md`.
|
|
|
89
89
|
back — which is what makes scanning twenty records unaffordable. `TableRow`'s `detail` +
|
|
90
90
|
`expanded` reveal the detail beneath the row; keep the drawer for a heavy form.
|
|
91
91
|
→ [catalog.md](./docs/catalog.md).
|
|
92
|
+
- **Documents get a LIST; images get a GRID.** The discriminator is what identifies the file to
|
|
93
|
+
the person looking for it. A PDF, a Word doc and a spreadsheet are identified by their NAME —
|
|
94
|
+
their thumbnails are a grey page with grey lines on it, identical to every other grey page —
|
|
95
|
+
so a grid of them is a wall of the same tile and the reader has to read a truncated caption
|
|
96
|
+
anyway. Use `FileRow` / `FileRows`, where the full name is the row and a second line can
|
|
97
|
+
carry a date, a size, a status, a diff. Photos, scans and drawings are identified by their
|
|
98
|
+
CONTENT — the filename is `IMG_4471.jpg` and tells nobody anything — so use
|
|
99
|
+
`FileThumbnailGrid`, where the picture is the identity and many fit on one screen. A mixed
|
|
100
|
+
set follows its majority; an app whose attachments are overwhelmingly paperwork should not
|
|
101
|
+
pay a grid's wasted space for the occasional photo.
|
|
102
|
+
→ [catalog.md](./docs/catalog.md).
|
|
92
103
|
- **A reference to another record is a FIELD** — it wears the inline editor's own resting
|
|
93
104
|
surface and PEEKS its facts on press; EVERY verb lives in the peek's footer as a `Button`,
|
|
94
105
|
none on the field — LEFT the link (Change and Clear, both standard), RIGHT the record (Edit's
|
package/MIGRATION.md
CHANGED
|
@@ -4,6 +4,32 @@ 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
|
+
## 29.0.0 — review surfaces compose from atoms
|
|
8
|
+
|
|
9
|
+
`ChangeReview` and its family are DELETED, replaced by three layout-free pieces. The
|
|
10
|
+
`change_review` export and the `changeReview` locale slice are gone. Apps pinned to 28.x are
|
|
11
|
+
unaffected until they upgrade.
|
|
12
|
+
|
|
13
|
+
**Why.** `ChangeReview` was a REQUIRED context root — `useReviewCtx` threw outside it — so a
|
|
14
|
+
change could not be dropped into a screen that already existed; you adopted the whole container
|
|
15
|
+
or nothing. And the family owned LAYOUT, not just semantics (a divided stack, a ruled field
|
|
16
|
+
container, a label-plus-bands block), so a change could only ever *look like* a stack of
|
|
17
|
+
labelled field diffs. Every surface needing a diff somewhere else — inside a ledger row, beside
|
|
18
|
+
a total, in a cell — rebuilt it by hand, and several apps did.
|
|
19
|
+
|
|
20
|
+
**What to reach for instead**
|
|
21
|
+
|
|
22
|
+
| Was | Now |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `ChangeField` `before`/`value` bands | **`DiffValue`** — anywhere a value renders |
|
|
25
|
+
| `ChangeRecord` op wash, add/remove framing | **`DiffMark`** on whatever row you already draw |
|
|
26
|
+
| `ChangeReview` provider + `Change` status + `ChangeReviewHeader` counter | **`useChangeSet(ids)`** — the same bookkeeping, no container |
|
|
27
|
+
| `ChangeReviewActions` | your own `Button`s, reading `review.keptCount` / `review.settled` |
|
|
28
|
+
| `ChangeFields`, `ChangeLabel`, `ChangeSummary`, `ChangeReasoning`, `ChangeBand`, `ChangeValueInput` | ordinary `DetailTable` / `Text` / inline inputs — they were layout, not contract |
|
|
29
|
+
|
|
30
|
+
The opinionated stack is not gone, it is demoted: build it in your own screen from the atoms.
|
|
31
|
+
Every app needed a slightly different shape, which is why one container could not serve them.
|
|
32
|
+
|
|
7
33
|
## 28.2.0 — `SubsectionHeadingTitle` takes a `description`
|
|
8
34
|
|
|
9
35
|
Additive; nothing to change. The heading ramp's three rungs are meant to carry the SAME slots
|
package/docs/ai_patterns.md
CHANGED
|
@@ -13,12 +13,23 @@ file). Every component here is in the [catalog](./catalog.md) inventory.
|
|
|
13
13
|
|
|
14
14
|
The AI surfaces share ONE law: the agent never commits **invisibly**. The agent owns judgment
|
|
15
15
|
(recognition, estimation, intent→parameters); the app owns geometry, math, and the write. How
|
|
16
|
-
the human stays in charge follows
|
|
16
|
+
the human stays in charge follows **who supplied the values**, not whether a before-state
|
|
17
|
+
exists. A diff is one way to show a person what a machine decided; it is not the only reason
|
|
18
|
+
they need to see it. Values a HUMAN typed need no gate — they already read them, on the way in.
|
|
19
|
+
Values a MACHINE read off a document need one, and a create is not exempt just because there is
|
|
20
|
+
nothing to compare against: a receipt saying "Created 3 cases" is the first moment the operator
|
|
21
|
+
could have caught a misread reference, and by then it is in the data.
|
|
17
22
|
|
|
18
23
|
- **Modifying an existing record** — a diff exists, and a wrong value can clobber a right one →
|
|
19
|
-
review BEFORE apply:
|
|
20
|
-
- **Creating new records** (intake, import, drafting rows) —
|
|
21
|
-
|
|
24
|
+
review BEFORE apply: `DiffValue` + `DiffMark` + `useChangeSet`, composed into the screen; nothing auto-applies.
|
|
25
|
+
- **Creating new records from a DOCUMENT** (intake, import, drafting rows) — no before-state,
|
|
26
|
+
so nothing to diff, but the values are still proposed rather than typed → **a full EDITABLE
|
|
27
|
+
preview of every record before the write**, not a summary of it. Lay the records out as they
|
|
28
|
+
will be stored — every field, editable in place, nothing behind a "3 fields" count — and
|
|
29
|
+
commit once. There is no diff to mark, so the whole record is the review; `DiffMark` still
|
|
30
|
+
earns its place to say which records are new versus matched onto something existing.
|
|
31
|
+
- **Creating new records a human TYPED** (the manual create form) — they read every value on
|
|
32
|
+
the way in → **apply directly, then end on the RECEIPT**
|
|
22
33
|
(`@lotics/ui/result_header`) — the receipt states the outcome and ROUTES; it is never a
|
|
23
34
|
record editor. Run DETERMINISTIC checks over what the save STORED (a checksum, a count
|
|
24
35
|
reconciliation — binary math, never a fabricated score). ONE record: `ResultHeader` (tone,
|
|
@@ -52,7 +63,7 @@ for the right one by job.
|
|
|
52
63
|
Three shapes:
|
|
53
64
|
|
|
54
65
|
1. **Field writes** (extract, match, rank, classify → records change) → **in-app agent**
|
|
55
|
-
(`useAgentRun`), then split by the one law: modifying an existing record →
|
|
66
|
+
(`useAgentRun`), then split by the one law: modifying an existing record → **a review surface you compose**
|
|
56
67
|
(Keep/Drop per change, app-workflow writes, bounded app authority); creating new records →
|
|
57
68
|
**save direct + the receipt** (outcome, verdicts, and a route to the record).
|
|
58
69
|
Either way one button, never a prompt box.
|
|
@@ -241,107 +252,143 @@ into it while running and reveals again when done.
|
|
|
241
252
|
**Limitation:** the pill's expand/collapse accessibility labels ("Show the agent's steps" /
|
|
242
253
|
"Hide the agent's steps") are currently hardcoded English.
|
|
243
254
|
|
|
244
|
-
## Review before apply —
|
|
245
|
-
|
|
246
|
-
**When to reach for it — the one law's boundary.**
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
the
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
255
|
+
## Review before apply — TWO atoms and a hook, composed into your screen
|
|
256
|
+
|
|
257
|
+
**When to reach for it — the one law's boundary.** A review gates values the MACHINE
|
|
258
|
+
supplied, whether or not they replace anything. Modifying an existing record is the obvious
|
|
259
|
+
case and takes the full diff. Creating records FROM A DOCUMENT takes the same gate in a
|
|
260
|
+
different shape — no diff, because nothing is being replaced, but the whole record laid out
|
|
261
|
+
and editable before the write (`examples/tpl_item_list.tsx`). Only values a HUMAN TYPED skip
|
|
262
|
+
it: they read them on the way in, so the manual form saves direct and ends on the receipt.
|
|
263
|
+
|
|
264
|
+
**There is no review CONTAINER, deliberately.** A review is not a place you go; it is something
|
|
265
|
+
that happens to values already on screen. So the diff renders wherever the value renders — a
|
|
266
|
+
table cell, a `DetailRow`, a total, a chip, a sentence — and inherits that container's width,
|
|
267
|
+
alignment and rhythm. A compound family with a provider at its root can only produce the one
|
|
268
|
+
shape its root draws, and every surface needing a diff somewhere else rebuilds it by hand.
|
|
269
|
+
|
|
270
|
+
### The three pieces
|
|
271
|
+
|
|
272
|
+
- **`DiffValue`** (`@lotics/ui/diff_value`) — a value that changed. `before` struck, `after`
|
|
273
|
+
emphasised. `layout="stacked"` (default) for a column of figures, `"inline"` for prose and
|
|
274
|
+
dense rows; `align`/`tabular` for money so the struck value and its replacement line up;
|
|
275
|
+
`tone` when the change itself is the bad news. **The three ops are the same component:**
|
|
276
|
+
an ADD omits `before`, a REMOVAL omits `after` (the struck value IS the change), an UPDATE
|
|
277
|
+
passes both, and **equal values collapse to one** — agreement is not a change, and an invoice
|
|
278
|
+
confirming the amount that was claimed is the GOOD case, not a diff of a value against
|
|
279
|
+
itself. **Both values stay on screen** on a real change (the old one is the only evidence the
|
|
280
|
+
correction is the right SIZE), and **`delta`** prints the movement itself — "+250 pcs",
|
|
281
|
+
"−118.519 ₫ VAT" — so nobody subtracts in their head.
|
|
282
|
+
|
|
283
|
+
**The struck value is muted, never red.** An old value is not an ERROR, and red is this
|
|
284
|
+
system's error colour — every routine update would read as a problem, and a table of eight
|
|
285
|
+
changed rows would carry sixteen coloured figures. Red-against-green is also the one pair a
|
|
286
|
+
colour-blind reader cannot separate, and it is the distinction a review turns on. The
|
|
287
|
+
strikethrough carries "going away" with no colour at all; weight, tone and (stacked) size
|
|
288
|
+
carry the rest.
|
|
289
|
+
- **`DiffMark`** (`@lotics/ui/diff_mark`) — what happened to the row holding the value, for the
|
|
290
|
+
cases no single value carries: `added` / `changed` / `removed` / `unchanged`. Glyph plus an
|
|
291
|
+
accessible word, never colour alone. `unchanged` draws a quiet dot, because most rows in a
|
|
292
|
+
real review are untouched.
|
|
293
|
+
|
|
294
|
+
**ONE treatment, every surface: a filled 22px disc.** The form, the line table, the files
|
|
295
|
+
list and the thumbnail grid all draw the same mark, because a reader parses shape before
|
|
296
|
+
meaning and a form marked with bare glyphs beside a table marked with filled shapes reads as
|
|
297
|
+
two systems saying the same thing. `unchanged` renders as an empty disc-sized HOLE: an
|
|
298
|
+
operator opening a job of twelve fee lines to see where a dropped invoice landed should find
|
|
299
|
+
the one filled shape in a column of blanks without reading anything or separating two hues.
|
|
300
|
+
The hole keeps the column's width and announces nothing.
|
|
301
|
+
- **`useChangeSet(ids, { initial })`** (`@lotics/ui/use_change_set`) — the bookkeeping:
|
|
302
|
+
`status`/`accept`/`reject`/`undo`/`acceptAll`/`reset`, `accepted`/`rejected`/`pending`,
|
|
303
|
+
`keptCount`/`total`/`settled`. `initial` defaults to **`accepted`** so an operator drops
|
|
304
|
+
exceptions instead of approving eight identical lines; pass `pending` when each change
|
|
305
|
+
genuinely deserves its own verdict, and gate the commit on `settled`.
|
|
306
|
+
|
|
307
|
+
### The shapes, and what each one is
|
|
308
|
+
|
|
309
|
+
- **A record's fields** — a `DetailTable` of rows, each a label plus a `DiffValue`, with
|
|
310
|
+
Keep/Drop on the row and a collapsed state once decided. That row is ~20 lines and it belongs
|
|
311
|
+
to your screen: `examples/tpl_record.tsx` and `dev/pages/ai.tsx` each write their own and they
|
|
312
|
+
read differently, which is the system working.
|
|
313
|
+
- **A record SET** — a `Table`, one row per proposed record, `DiffMark` leading and `DiffValue`
|
|
314
|
+
inside the figure cells. A set of ledger lines reads like the ledger it is about.
|
|
315
|
+
- **A conflict** — two documents disagree, so the value is the read-only OUTCOME of a pick:
|
|
316
|
+
a `DiffValue` showing the placeholder until resolved, over `CardSelectItem` candidates (each
|
|
317
|
+
naming its source document) and a type-another-value option. **Never pre-select**, and gate
|
|
318
|
+
Keep until one is chosen — a pre-selected candidate is a decision the system made and
|
|
319
|
+
attributed to the person.
|
|
320
|
+
- **An ATTACHMENT** — a dropped document's proposal is often not a value at all: it is WHICH
|
|
321
|
+
LINE the file belongs to. That decision is as wrong-able as a misread figure and gets the
|
|
322
|
+
same treatment — the file rendered on the row it lands on, inside a `DiffValue` whose sides
|
|
323
|
+
are NODES (a `FileBadge` plus its name). A file replacing one already filed shows both, and
|
|
324
|
+
the superseded one is struck by a DRAWN rule: `line-through` set on a `Text` does not cross
|
|
325
|
+
a `View` child, so without it the old scan renders at full strength beside its replacement
|
|
326
|
+
with nothing saying which is going away. Worked in `examples/tpl_record.tsx`.
|
|
327
|
+
- **A DOCUMENT SET** — a files section is not a field. It is a set of objects with identity,
|
|
328
|
+
so what changes is MEMBERSHIP plus per-document properties, and the kinds do not share a
|
|
329
|
+
treatment. **Added** and **removed** are membership: the row's existence and its mark are
|
|
330
|
+
the whole change. **Superseded** is one slot whose bytes changed — the row names the new
|
|
331
|
+
file and its second line names the one it displaces. **Reclassified** is the one a review
|
|
332
|
+
surface gets wrong: the bytes are untouched and only the FILING moved, so the `DiffValue`
|
|
333
|
+
belongs on the property — a mark with no visible diff reads as though the file itself
|
|
334
|
+
changed. `FileRow` takes the mark in `leading` (so the names still form a column) and a node
|
|
335
|
+
`meta` (so the second line can carry a diff).
|
|
336
|
+
|
|
337
|
+
**In a GRID** the same set reads differently again, and that is a property of the shape
|
|
338
|
+
rather than a gap: a grid is scanned whole, so it carries membership (this is arriving, that
|
|
339
|
+
is leaving — `FileThumbnailGrid diffs`, a corner mark plus a fade) at a glance and carries
|
|
340
|
+
pairing not at all. Pick the surface for the change; do not shrink the change to fit a shape.
|
|
341
|
+
|
|
342
|
+
**A file badge has a discriminator too.** Where the file is the row's SUBJECT — a `FileRow`,
|
|
343
|
+
a tile — the badge earns its space and the row is built around it. Where the file is one
|
|
344
|
+
VALUE among several columns, render the filename as a value at the column's own text size:
|
|
345
|
+
a badge shrunk to fit a table cell takes its label off the type scale entirely (5px, at the
|
|
346
|
+
size a cell allows), and sizing it back up makes it set the row's height for a fact the
|
|
347
|
+
extension already carries.
|
|
348
|
+
|
|
349
|
+
**The strikethrough has a discriminator.** Strike the superseded file when it shares a CELL
|
|
350
|
+
with its replacement, adjacent — a table's document column, where the pairing is what the
|
|
351
|
+
cell means. Do NOT strike it on a files-section line, where the two sit on different rows:
|
|
352
|
+
there the strike says "going away" without saying what replaces it, so a word names the
|
|
353
|
+
relation and the chip only identifies the file. Both shapes are worked in `dev/pages/ai.tsx`.
|
|
354
|
+
- **A record being CREATED from a document** — no before-state, so no diff: the whole record
|
|
355
|
+
is the review. Lay every field out as it will be stored, editable in place, and mark only
|
|
356
|
+
the fact that IS relational (matched onto something existing, or new alongside it). One
|
|
357
|
+
commit named for the outcome. Worked in `examples/tpl_item_list.tsx`.
|
|
358
|
+
- **Display-only** — a finding that informs but has nothing to decide simply is not in the
|
|
359
|
+
change set. There is no "display-only" mode to remember.
|
|
360
|
+
|
|
361
|
+
### The laws that survive the container
|
|
362
|
+
|
|
363
|
+
- **Nothing auto-applies.** The commit is one explicit press, named for its outcome
|
|
364
|
+
("Update record (3)", "Add 4 tasks") — never "Apply" alone, never "OK".
|
|
365
|
+
- **Editing IS the review.** A value the operator can correct in place beats accept-or-reject
|
|
366
|
+
on a value they cannot touch: a wrong proposal you can fix costs a keystroke, one you can
|
|
367
|
+
only reject costs the whole re-entry.
|
|
368
|
+
- **Gate the commit on what will actually be written**, not on the proposal count — disable at
|
|
369
|
+
zero kept.
|
|
370
|
+
- **The mark rides the LABEL, and the value sits in an `InlineSlot`.** Both are alignment
|
|
371
|
+
before they are semantics. A mark beside the VALUE indents every row it marks by its own
|
|
372
|
+
width plus a gap, so an unmarked row needs a spacer of exactly the glyph's width to keep up —
|
|
373
|
+
and they drift (17 against 13 shipped that way). The label column is a fixed width with one
|
|
374
|
+
left edge, so a mark placed there aligns down the page for free. Give the slot a fixed HEIGHT
|
|
375
|
+
as well as width: it is an inline-flex box inside `DetailRow`'s label `Text`, so an empty one
|
|
376
|
+
baselines differently from one holding a glyph. On the value side, a form mixes 40px editors
|
|
377
|
+
with 20px flat diffs; without the slot the column has two left edges and two baselines, and
|
|
378
|
+
the label agrees with one kind of row and not the other.
|
|
379
|
+
- **The reason goes above the verdict.** A `why` passed as `DetailRow`'s `description` renders
|
|
380
|
+
under the whole cell — which is under the Keep/Drop buttons. A justification printed after
|
|
381
|
+
the controls that act on it is one nobody reads. Render it inside the value block.
|
|
382
|
+
- **Show the WHOLE thing, not just the parts that moved.** The operator's question is rarely
|
|
383
|
+
"what did it find" — it is "is this right, and where does it sit". A surface showing only
|
|
384
|
+
the changed rows answers the first and leaves the second to a second screen. Render the
|
|
385
|
+
record entire and let `DiffValue` collapse what agrees (equal values render as one plain
|
|
386
|
+
value, so the same component tree serves a record with one change and a record with twenty).
|
|
387
|
+
Emphasis, not omission, is what makes the changes findable.
|
|
388
|
+
- **The fixture must not flatter.** A review demo whose every row carries a change proves
|
|
389
|
+
nothing about the screen that matters — the one where a document touched two lines of
|
|
390
|
+
fourteen. Build the sparse case and check the marks are still findable in it.
|
|
391
|
+
- **Provenance belongs at the bottom of the section** (`Sources`), never crowded into a heading.
|
|
345
392
|
|
|
346
393
|
## Ask back — `Clarify` / `ClarifyWizard`
|
|
347
394
|
|
|
@@ -449,7 +496,7 @@ sources. Severity → colour: `critical` red, `warning` amber, `info` zinc ("Not
|
|
|
449
496
|
emerald ("On track"); words come from the `finding` locale slice. **Display-only**: a finding
|
|
450
497
|
informs the action the human takes in the app; it decides nothing itself — no phantom "record
|
|
451
498
|
verdict" write (a persisted check-status goes stale on the next edit). Stack several most-severe
|
|
452
|
-
first —
|
|
499
|
+
first — render each as its own section separated by a `Divider` (a finding that only informs
|
|
453
500
|
apply). `FindingComparison` is the expected-vs-actual body: each disagreeing side a labeled row
|
|
454
501
|
(source, value), a hairline, then the DELTA emphasized — compose it as the finding's children
|
|
455
502
|
for any one-value-disagrees insight (quantities, totals, dates).
|
|
@@ -496,9 +543,9 @@ ranked set, don't cram it into chat prose:
|
|
|
496
543
|
document-driven records) — the record's files block feeds ONE "Use AI" entry that FORKS into
|
|
497
544
|
the two document tasks, each a specialized run with a task-pure result: **Extract** (files
|
|
498
545
|
read → fields already matching fold into one quiet line → every add / update / conflict a
|
|
499
|
-
`
|
|
546
|
+
a `DiffValue` per field (the struck current value, the proposed one, candidate rows), the record's current value
|
|
500
547
|
a first-class choice — plus proposed new lines as record-body `Change`s → one outcome-named
|
|
501
|
-
|
|
548
|
+
an explicit outcome-named commit) and **Cross-check** (documents compared against the record and
|
|
502
549
|
each other → ranked `Finding`s — severity, title, the prominent metric, sources — separated
|
|
503
550
|
by hairlines; the findings ARE the outcome the human acts on). The fork carries an OPTIONAL
|
|
504
551
|
instructions field — the user steers what the agent checks or extracts, so `Finding` serves ANY
|
|
@@ -518,10 +565,10 @@ ranked set, don't cram it into chat prose:
|
|
|
518
565
|
can do it in ONE run — every app agent carries `ask_user_choice`, so the run PARKS on the
|
|
519
566
|
agent's own question and `useAgentRun().pendingChoice`/`answerChoice` drive the same
|
|
520
567
|
`ClarifyWizard` (see the SDK's [ai doc](../../app-sdk/docs/ai.md)).
|
|
521
|
-
- **Triage** — an inbox the agent classified + routed is a `
|
|
568
|
+
- **Triage** — an inbox the agent classified + routed is a `useChangeSet` over rows (body: the
|
|
522
569
|
item + the agent's call; the standard Keep/Drop verbs; Keep-all covers the high-confidence
|
|
523
570
|
sweep). Leads, tickets, documents, emails.
|
|
524
|
-
- **Compare / ranked pick** — one `
|
|
571
|
+
- **Compare / ranked pick** — one `DiffValue` over `CardSelectItem` rows carrying the ranked options; the
|
|
525
572
|
human picks one. Quotes, carriers, suppliers, plans. (Fold the score/reason into each
|
|
526
573
|
candidate's value string — see the candidate-row limitation above.)
|
|
527
574
|
|
package/docs/catalog.md
CHANGED
|
@@ -301,13 +301,13 @@ upcoming, horizontal OR vertical), `RemainderMeter` + `AllocationRow` (allocatio
|
|
|
301
301
|
`Composer` (the adaptive command/chat composer — a compact pill when empty that expands for
|
|
302
302
|
long text + attachments; the surface that triggers agent work), `AgentRun` (the live
|
|
303
303
|
streaming work feed) + `AgentProgress` (its compact, floating, expandable form — a
|
|
304
|
-
composer's "working" state) + `Confidence`;
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
`
|
|
309
|
-
|
|
310
|
-
[AGENTS.md](../AGENTS.md) for the laws; `Clarify` (the agent asks back — selectable
|
|
304
|
+
composer's "working" state) + `Confidence`; **review-before-apply composes from two atoms
|
|
305
|
+
plus a hook** — `DiffValue` (a value that changed: before struck beside/above after; drops
|
|
306
|
+
into a table cell, a `DetailRow`, a total, a sentence), `DiffMark` (what happened to the row:
|
|
307
|
+
added/changed/removed/unchanged) and `useChangeSet` (accept/reject/undo/counter, zero
|
|
308
|
+
layout). An add is `after` alone, a removal is `before` alone, an update is both, a conflict
|
|
309
|
+
is a `DiffValue` over `CardSelectItem` candidates — and the ROW around any of them belongs to
|
|
310
|
+
your screen. See the AI-patterns doc indexed in [AGENTS.md](../AGENTS.md) for the laws; `Clarify` (the agent asks back — selectable
|
|
311
311
|
`ChoiceList` options), `Sources` (provenance chips for AI output — at review scale,
|
|
312
312
|
`label={null}` slots the chips at a section's bottom), `Finding` (one ranked insight from an
|
|
313
313
|
AI check — localized severity word, title, detail, `Sources` chips, a `children` slot;
|
|
@@ -1328,7 +1328,13 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1328
1328
|
|
|
1329
1329
|
- **`mime`** — pure MIME predicates: `isImageMimeType` / `isVideoMimeType` /
|
|
1330
1330
|
`isAudioMimeType` / `isPdfMimeType` / `isExcelMimeType` / `isCsvMimeType` /
|
|
1331
|
-
`isDocxMimeType` / `isPreviewableMimeType`.
|
|
1331
|
+
`isDocxMimeType` / `isPreviewableMimeType`. Plus
|
|
1332
|
+
**`resolveMimeType(declaredType, filename)`** — what to TREAT a file as. An
|
|
1333
|
+
honestly-declared type is returned untouched; an *uninformative* one (`""`,
|
|
1334
|
+
`application/octet-stream` and friends — what an OS clipboard or drag hands
|
|
1335
|
+
over) is resolved from the filename extension instead, falling back to the
|
|
1336
|
+
declaration when the extension is unknown too. It returns the same answer the
|
|
1337
|
+
server stores, so a surface that filters on it agrees with what gets persisted.
|
|
1332
1338
|
- **`download`** — `downloadFileFromUrl(url, filename, { credentials? })`: fetch+blob+anchor
|
|
1333
1339
|
download that works inside sandboxed iframes (where `window.open` is silently dropped);
|
|
1334
1340
|
`credentials` defaults to `same-origin` — pass `"include"` only for auth-gated same-site
|
|
@@ -1397,7 +1403,14 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1397
1403
|
the single in-flight upload tile `FileGrid` renders; reach for it only when hand-rolling a
|
|
1398
1404
|
non-grid upload layout — it is i18n-free by design, so a bare call site owes it `labels`
|
|
1399
1405
|
(`FileGrid` resolves them from `LoticsLocale.fileUpload` on your behalf).
|
|
1400
|
-
- **`file_thumbnail`** —
|
|
1406
|
+
- **`file_thumbnail`** — **`diff`** puts a `DiffKind` corner mark on a tile (top-LEFT, the one
|
|
1407
|
+
corner remove/selection/uploading do not claim) and fades a `removed` one. The mark rides a
|
|
1408
|
+
white RING: a tile's ground is an arbitrary photo, and a pale tint over a pale scan is a
|
|
1409
|
+
smudge. A `removed` tile fades its BODY only — the mark stays at full strength, since a faded
|
|
1410
|
+
mark is the smudge the ring exists to prevent. Ignored below 32px: a 22px disc on a compact
|
|
1411
|
+
tile is the tile, and a compact strip lives inside a row that carries the change instead. A grid says MEMBERSHIP well (arriving, leaving) and PAIRING not at
|
|
1412
|
+
all — nowhere on a 96px tile to name what a replacement supersedes — so a set where things
|
|
1413
|
+
are being REPLACED wants `FileRow`. Also: `FileThumbnail` + `DisplayFile` + `THUMBNAIL_SIZE` /
|
|
1401
1414
|
`COMPACT_THUMBNAIL_SIZE` + `getMediaIcon`: the completed tile — the right surface per
|
|
1402
1415
|
MIME: image thumbnail, a doc tile with the `FileBadge` centered + a single-line filename
|
|
1403
1416
|
, media card; `isTemplate` overlays a TMPL marker. The tile's accessible name is the
|
|
@@ -1405,14 +1418,26 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1405
1418
|
pieces are exported for a hand-rolled layout: `DocumentBadge` (a bare pressable badge —
|
|
1406
1419
|
its `size` is the square SLOT side, like every other tile here, and the badge is fitted to
|
|
1407
1420
|
it), `DocumentCard`, `MediaCard`, `RemoveButton`.
|
|
1408
|
-
- **`file_thumbnail_grid`** —
|
|
1421
|
+
- **`file_thumbnail_grid`** — **`diffs`** (a `ReadonlyMap<string, DiffKind>`, keyed like
|
|
1422
|
+
`selectedIds`) marks each tile, so a grid under review is the SAME grid with one more map
|
|
1423
|
+
passed in rather than a different component the operator has never seen.
|
|
1424
|
+
`FileThumbnailGrid` (+ the generic `ThumbnailGrid`):
|
|
1409
1425
|
display-only square-tile grid; `selectedIds` + `onFilePress` pair with
|
|
1410
1426
|
`useSelectionMode()` for gated select-mode CRUD.
|
|
1411
1427
|
- **`file_row`** — `FileRow`: a horizontal file/document LINE — `FileBadge` or a
|
|
1412
|
-
`placeholder` + the FULL name + a meta line +
|
|
1413
|
-
makes the whole row a pressable door, `trailing` stays an independently-pressable
|
|
1414
|
-
for attachment lists / message files / document checklists where a square tile
|
|
1415
|
-
the name.
|
|
1428
|
+
`placeholder` + the FULL name + a meta line + composable `leading` / `trailing` slots;
|
|
1429
|
+
`onPress` makes the whole row a pressable door, `trailing` stays an independently-pressable
|
|
1430
|
+
sibling; for attachment lists / message files / document checklists where a square tile
|
|
1431
|
+
truncates the name. **`leading`** sits BEFORE the badge — a `DiffMark` for a document set
|
|
1432
|
+
under review, a select checkbox, a status dot; outside the badge rather than drawn on it, so
|
|
1433
|
+
it lines up down the list instead of moving with the badge's `sm`/`md` size. **`meta` takes a
|
|
1434
|
+
NODE** as well as a string, because a second line can itself be carrying a change (a file
|
|
1435
|
+
being RECLASSIFIED is the same bytes filed differently — a `DiffValue` on the property); a
|
|
1436
|
+
node renders outside the muted single-line `Text`, which would clip it and fight its colour.
|
|
1437
|
+
Both `sm` variants share ONE beat (48px min + 6px vertical padding): the static row used to
|
|
1438
|
+
have neither, so it came out 37px against the pressable's 49 — whether a row was a door
|
|
1439
|
+
decided how tall it was — and 37 sat under every other row beat in the kit (`DetailRow` 40,
|
|
1440
|
+
`TableRow` 52, this component's own `md` 56), which is what "squeezed" literally was.
|
|
1416
1441
|
- **`file_rows`** — `FileRows`: batteries-included file list — row press → built-in gallery
|
|
1417
1442
|
+ a ⋯ Download/Open-external/Remove menu; composes `FileRow` + `ActionMenu` +
|
|
1418
1443
|
`FileGalleryModal`; localized via the `gallery` locale slice.
|
|
@@ -1514,17 +1539,67 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1514
1539
|
be rendered; where a compact meter used to decorate a ranked row or proposal card, rank /
|
|
1515
1540
|
badges / severity carry the standing instead. Also: calibrated
|
|
1516
1541
|
high/med/low; localized via the provider.
|
|
1517
|
-
- **`
|
|
1518
|
-
`
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
`
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1542
|
+
- **`diff_value`** — **`DiffValue`, THE atom of a review surface**: a value that changed,
|
|
1543
|
+
rendering `before` struck above/before `after`. Owns no container, no context, no decision —
|
|
1544
|
+
so it drops into a table cell, a `DetailRow`, a total, a chip or a sentence and inherits that
|
|
1545
|
+
container's width and alignment. Either side may be a NODE — a `MemberChip` for a reassignment,
|
|
1546
|
+
a chip for a link being repointed — and a node `before` is struck by a DRAWN rule rather than
|
|
1547
|
+
by text decoration, because `line-through` on a `Text` does not cross a `View` child (the old
|
|
1548
|
+
value would otherwise render at full strength beside its replacement with nothing saying it is
|
|
1549
|
+
the one going away — type-checked, rendered, and wrong). Reach for a node when the value
|
|
1550
|
+
genuinely is not text: an avatar carries identity a string cannot, and a review that swapped
|
|
1551
|
+
the record's own chip for a name would show the operator a surface they have never seen at the
|
|
1552
|
+
one moment they are asked to check something. A node belongs where it fits at FULL size — a
|
|
1553
|
+
form row, a card. Shrinking a composite into a table cell takes its own type off the scale
|
|
1554
|
+
(a file badge sized to a cell rendered its label at 5px), so a file in a CELL is a filename at
|
|
1555
|
+
the column's own size, not a miniature badge. Worked in `dev/pages/ai.tsx` § every shape.
|
|
1556
|
+
**`before === after` collapses to ONE plain value** —
|
|
1557
|
+
agreement is not a change, and a document confirming what the record already says is the
|
|
1558
|
+
GOOD case, not a diff of a value against itself. **`delta`** (a host-formatted string, the
|
|
1559
|
+
same contract as `FindingComparison.delta`) prints HOW FAR it moved, because
|
|
1560
|
+
`1.600.000 → 1.481.481` otherwise makes the reader subtract to find out whether a
|
|
1561
|
+
correction is trivial or alarming. `layout` stacked (a column of figures) | inline (prose, dense
|
|
1562
|
+
rows); `align`/`tabular` for money; `tone` for whether the change is good news. Omit `before`
|
|
1563
|
+
for an addition, `after` for a removal. BOTH values stay on screen: the old one is the only
|
|
1564
|
+
evidence the reader has that the correction is the right SIZE.
|
|
1565
|
+
**`placeholder` is YOUR string — translate it.** The component renders no word of its own
|
|
1566
|
+
(its only fallbacks are `—` and empty), so nothing here reaches the locale packs. That is
|
|
1567
|
+
deliberate rather than an omission: `DiffMark` beside it DOES ship localized words, but the
|
|
1568
|
+
two are different sentences even when English collides them — the mark says what happened to
|
|
1569
|
+
the ROW ("Removed" / "Xóa"), the placeholder says what is in the FIELD now, which reads "đã
|
|
1570
|
+
xóa" or "bỏ trống", never the bare verb. The same prop also carries "Pick a candidate below"
|
|
1571
|
+
and "Not on the document". Pull it from your own catalog.
|
|
1572
|
+
- **`diff_mark`** — **`DiffMark`**: what happened to the row holding the value —
|
|
1573
|
+
`added`/`changed`/`removed`/`unchanged`, for the cases no single value carries. Glyph + an
|
|
1574
|
+
accessible word (never colour alone — "added" vs "removed" by hue is invisible to a reader who
|
|
1575
|
+
cannot separate them, and that is the distinction a review turns on). **ONE treatment
|
|
1576
|
+
everywhere: a filled 22px DISC**, 14px glyph, 700 ink on a 100 ground. It briefly had two — a
|
|
1577
|
+
bare glyph for dense surfaces, a filled shape for sparse ones — and a form marked with bare
|
|
1578
|
+
glyphs beside a table marked with filled shapes read as two systems saying the same thing; a
|
|
1579
|
+
reader parses shape before meaning, so an unexplained difference reads as an accident however
|
|
1580
|
+
principled the reason. The disc also gives the glyph its own ground, which is what lets one
|
|
1581
|
+
mark sit on a white row, a tinted cell, or a photograph. **`unchanged` renders as an empty
|
|
1582
|
+
disc-sized HOLE** — silence is the honest rendering of "nothing happened", it keeps the
|
|
1583
|
+
column's width so nothing shifts left, and it announces nothing (eleven "Unchanged"es is the
|
|
1584
|
+
same noise to a screen reader that a dot is to an eye). `showLabel` puts the word OUTSIDE the
|
|
1585
|
+
disc; text crammed inside stops it being a circle, and the shape is what makes it findable
|
|
1586
|
+
without reading. Leads a table row, sits in a card corner, precedes a field label.
|
|
1587
|
+
- **`inline_slot`** — **`InlineSlot`**: any node placed on the INLINE-CONTROL grid — the box
|
|
1588
|
+
an `Inline*` editor draws (40px band, 8px horizontal padding inside a 1px transparent border),
|
|
1589
|
+
minus the editor. A record's value column mixes editors with things that are not editors, and
|
|
1590
|
+
anything rendered raw beside them starts at the cell edge, 20px tall, at the top — so the
|
|
1591
|
+
column quietly acquires TWO left edges and two baselines, and `DetailRow`'s label (which pads
|
|
1592
|
+
down to meet a control) agrees with one kind of row and not the other. `InlineStatic` is the
|
|
1593
|
+
STRING case and composes it; reach for the slot when the value is a node, which a review
|
|
1594
|
+
surface always is. NOT for a table cell — a `Table` sets its own row rhythm.
|
|
1595
|
+
- **`use_change_set`** — **`useChangeSet(ids, { initial })`**: the accept/reject/undo
|
|
1596
|
+
bookkeeping as a HOOK, zero layout — `status`/`accept`/`reject`/`undo`/`acceptAll`/`reset`,
|
|
1597
|
+
`accepted`/`rejected`/`pending`, `keptCount`/`total`/`settled`. Decisions are stored as
|
|
1598
|
+
OVERRIDES against a default, never seeded from `ids` into state (seeding needs an effect, and
|
|
1599
|
+
`ids` is normally a memo over query rows — a new array every render, which is the
|
|
1600
|
+
`Maximum update depth exceeded` loop). `initial` defaults to `accepted` so the operator drops
|
|
1601
|
+
exceptions instead of approving eight identical lines; pass `pending` when each change deserves
|
|
1602
|
+
its own verdict and gate the commit on `settled`.
|
|
1528
1603
|
- **`clarify`** — `Clarify` + `ClarifyOption`: the agent asks back — a borderless block
|
|
1529
1604
|
(the question text + a `ChoiceList`, no card wrapper; an optional muted `eyebrow` sits tight above
|
|
1530
1605
|
the question — e.g. a wizard's "1 / 3"). `ClarifyOption` requires
|