@lotics/ui 29.2.0 → 30.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +23 -1
- package/MIGRATION.md +23 -0
- package/docs/ai_patterns.md +34 -3
- package/docs/catalog.md +7 -1
- package/docs/composition.md +80 -5
- package/docs/data_entry.md +13 -0
- package/examples/tpl_item_list.tsx +39 -6
- package/examples/tpl_record.tsx +187 -6
- package/package.json +2 -1
- package/src/agent_run.tsx +8 -1
- package/src/agent_run_pane.tsx +57 -12
- package/src/choice_list.tsx +6 -3
- package/src/clarify.tsx +6 -2
- package/src/clarify_wizard.tsx +4 -2
- package/src/control_surface.ts +25 -0
- package/src/dialog.tsx +47 -8
- package/src/file_intake.ts +17 -2
- package/src/files_editor.tsx +1 -1
- package/src/floating_action_bar.tsx +33 -4
- package/src/locale.tsx +3 -0
- package/src/mime.ts +76 -0
- package/src/pressable_row.tsx +13 -9
- package/src/table_fit.ts +16 -4
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 — 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 |
|
|
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, **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
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,28 @@ 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
|
+
- **ONE LEFT EDGE — a row that paints a wash BLEEDS it, and never insets its content.**
|
|
93
|
+
`marginHorizontal: -ROW_WASH_BLEED` absorbed by an equal `paddingHorizontal` is net zero, so
|
|
94
|
+
a row's content sits on its container's own edge — level with the headings, labels and prose
|
|
95
|
+
beside it — while the hover/selection wash still breathes by growing into the parent's
|
|
96
|
+
padding. `PressableRow` (all variants), `Table`, `ChoiceList`, `ListItem` and `FileRow`
|
|
97
|
+
already do this, so a screen composed from the kit aligns with **no per-site padding at all**.
|
|
98
|
+
The moment one component insets instead, every neighbour has to be hand-padded by the same
|
|
99
|
+
amount to catch up, and that compensation spreads to each new surface built beside it — which
|
|
100
|
+
is how a `SummaryLine`, a `Clarify` question and a bare table's footer each acquired a
|
|
101
|
+
padding they should never have needed. **An indent is legible only when something VISIBLE
|
|
102
|
+
occupies it** — a checkbox, an ordinal, a diff mark. An empty indent is a defect.
|
|
103
|
+
- **The one exception is a self-contained OVERLAY.** A menu or a selector's popover
|
|
104
|
+
(`MenuListItem`, `OptionList`) has nothing outside itself to align with, so its rows keep
|
|
105
|
+
plain padding. Bleeding there would push rows past the popover's own edge.
|
|
106
|
+
- **A slot holding another component's part takes THAT component's constant**, never a
|
|
107
|
+
hand-picked number that happens to look close: `AgentRun`'s dot column was 18 while the
|
|
108
|
+
`Marker` it holds is `NODE` (24), so a 24px dot centred in an 18px column put the whole run
|
|
109
|
+
rail 3px left of every other band in the dialog. Import the owner's constant.
|
|
110
|
+
- Inside a `Dialog`, every band reads `dialogGutter(small)` — header, scroll area, footer and
|
|
111
|
+
any pane a caller drops in. A hand-copied `small ? 16 : 24` is a fifth copy of a number
|
|
112
|
+
that has to agree with four others.
|
|
113
|
+
→ [composition.md](./docs/composition.md).
|
|
92
114
|
- **Documents get a LIST; images get a GRID.** The discriminator is what identifies the file to
|
|
93
115
|
the person looking for it. A PDF, a Word doc and a spreadsheet are identified by their NAME —
|
|
94
116
|
their thumbnails are a grey page with grey lines on it, identical to every other grey page —
|
package/MIGRATION.md
CHANGED
|
@@ -4,6 +4,29 @@ 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
|
+
## 30.0.0 — a washed row bleeds; content sits on the container's edge
|
|
8
|
+
|
|
9
|
+
No API changed. Every register, table and choice list **moves its content 20px
|
|
10
|
+
left** onto its container's own edge, because `PressableRow` (all three
|
|
11
|
+
variants), `ChoiceList` and `Table` now bleed their hover/selection wash outward
|
|
12
|
+
instead of insetting content to make room for it. `ROW_WASH_BLEED` (8) in
|
|
13
|
+
`control_surface` names the rule.
|
|
14
|
+
|
|
15
|
+
**What you may have to remove.** If a surface of yours was hand-padded to line
|
|
16
|
+
up with a register or a table, that compensation is now a double-correction and
|
|
17
|
+
will sit 20px out. The shapes to look for:
|
|
18
|
+
|
|
19
|
+
| Compensator | What to do |
|
|
20
|
+
|---|---|
|
|
21
|
+
| A `SummaryLine`, caption or toolbar wrapped in `paddingHorizontal: 20` to match the rows below it | Drop the padding |
|
|
22
|
+
| `CardFooter` used as a **bare table's** footer (no `Card` around it) | Use a `Divider` + a plain row; `CardFooter` carries a *card's* 20px interior gutter |
|
|
23
|
+
| A heading or question hand-padded 8 to match a `ChoiceList`/`Clarify` block | Drop the padding |
|
|
24
|
+
| Anything reading `ROW_GUTTER` for its own spacing | It is 0 now — it means "the table's content gutter", and the wash no longer lives in it |
|
|
25
|
+
|
|
26
|
+
Tables also fit slightly MORE: the fit math stops reserving 40px of horizontal
|
|
27
|
+
padding, so a column that used to drop at a given width may now survive. Nothing
|
|
28
|
+
that was visible becomes hidden — the change only ever un-drops.
|
|
29
|
+
|
|
7
30
|
## 29.0.0 — review surfaces compose from atoms
|
|
8
31
|
|
|
9
32
|
`ChangeReview` and its family are DELETED, replaced by three layout-free pieces. The
|
package/docs/ai_patterns.md
CHANGED
|
@@ -133,6 +133,30 @@ transition — never leave a live button waiting on a network round-trip:
|
|
|
133
133
|
`useWorkflow` calls are NOT deduped (rapid distinct calls are legitimate), so a workflow CTA
|
|
134
134
|
must disable itself (`loading`) while pending or a double-click writes twice.
|
|
135
135
|
|
|
136
|
+
## Stopping — the exit is part of the trigger
|
|
137
|
+
|
|
138
|
+
The press being irreversible is a property of the BUTTON, not of the run. Every streaming surface
|
|
139
|
+
owes the operator a way out, because the reason to leave is usually visible in the first few
|
|
140
|
+
steps: it read the wrong document, it is grouping the wrong way, the brief was wrong. Without an
|
|
141
|
+
exit they pay for a result they already know they will discard.
|
|
142
|
+
|
|
143
|
+
- **`cancel`, never `abort`.** `useAgentRun` exposes both and they are not synonyms. `abort` stops
|
|
144
|
+
LISTENING — the run keeps executing server-side and lands in the session history. `cancel` stops
|
|
145
|
+
the run itself. A user-facing Stop wires to `cancel`; `abort` is for unmount.
|
|
146
|
+
- **Closing a dialog must CANCEL.** This is the one that leaks silently. The hook aborts on
|
|
147
|
+
unmount by design — right for a page you navigate away from and will come back to, wrong for a
|
|
148
|
+
dialog just dismissed, where the result is abandoned and every remaining token is spent on
|
|
149
|
+
nobody. Unmount is not cancellation; the close handler calls `cancel` itself.
|
|
150
|
+
- **Stop lands where the run STARTED**, not on a dead transcript — the intake, with the files, the
|
|
151
|
+
brief and the answers still there, because the operator stopped in order to change one of them.
|
|
152
|
+
It is the same destination the parked wizard's Cancel uses; only the verb differs, and it
|
|
153
|
+
differs truthfully — you STOP something running, you CANCEL something waiting.
|
|
154
|
+
- **The control is PINNED, in the footer.** A feed that follows its newest part scrolls a control
|
|
155
|
+
placed inside it away from the reader — exactly while the run is doing the thing they want to
|
|
156
|
+
stop. `AgentRunActions` renders Stop for you; a surface composing its own footers states it
|
|
157
|
+
directly ([`tpl_item_list`](../examples/tpl_item_list.tsx) and
|
|
158
|
+
[`tpl_record`](../examples/tpl_record.tsx) respectively).
|
|
159
|
+
|
|
136
160
|
## Show the work — `AgentRun`
|
|
137
161
|
|
|
138
162
|
`AgentRun` (`@lotics/ui/agent_run`) is a live feed of the agent's work as a TIMELINE. The prop is
|
|
@@ -424,8 +448,8 @@ Reach for it whenever an agent run happens inside a dialog — which is nearly a
|
|
|
424
448
|
const run = useAgentRun("intake"); // @lotics/app-sdk
|
|
425
449
|
<AgentRunScope> {/* wraps the Dialog, like ClarifyWizardScope */}
|
|
426
450
|
<Dialog …>
|
|
427
|
-
<AgentRunPane run={run} onCancel={
|
|
428
|
-
<DialogFooter><AgentRunActions run={run} /></DialogFooter>
|
|
451
|
+
<AgentRunPane run={run} onCancel={back} /> {/* content */}
|
|
452
|
+
<DialogFooter><AgentRunActions run={run} onStop={back} /></DialogFooter> {/* actions */}
|
|
429
453
|
</Dialog>
|
|
430
454
|
</AgentRunScope>
|
|
431
455
|
```
|
|
@@ -438,8 +462,15 @@ Submit, leaving a live `awaiting_input` run readable and unanswerable until it e
|
|
|
438
462
|
also swaps `FollowScroll` for `DialogScrollArea` on the park, so the question opens at the top
|
|
439
463
|
rather than wherever the feed was scrolled to.
|
|
440
464
|
|
|
465
|
+
**Mount `AgentRunActions` wherever the run drives the surface — never gated on one phase.** It
|
|
466
|
+
holds the wizard's verbs while a question is up, **Stop** while the run streams, and nothing once
|
|
467
|
+
it settles; the RUN decides, which is the whole point of handing it the run. A host that names a
|
|
468
|
+
single phase out here duplicates that decision and gets it wrong the moment the bar learns a new
|
|
469
|
+
one — which is precisely how two streaming phases ended up with an empty footer and no way to
|
|
470
|
+
stop.
|
|
471
|
+
|
|
441
472
|
**`run` is a SHAPE, not an import** (`AgentRunLike`: `status`, `parts`, `pendingChoice`,
|
|
442
|
-
`answerChoice`, `error`; `AgentRunQuestion` names what `pendingChoice` carries, so a template or
|
|
473
|
+
`answerChoice`, `cancel`, `error`; `AgentRunQuestion` names what `pendingChoice` carries, so a template or
|
|
443
474
|
test declares one directly — an option is a label + description and the answer's value IS the
|
|
444
475
|
label, which is the `ask_user_choice` wire shape, so nothing maps between two question types). `useAgentRun()` satisfies it structurally — `@lotics/ui` never depends on
|
|
445
476
|
`@lotics/app-sdk` — which also means a template or test can hand it a plain object and exercise the
|
package/docs/catalog.md
CHANGED
|
@@ -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
|
package/docs/composition.md
CHANGED
|
@@ -122,6 +122,31 @@ without scanning. The heading row is the section's control line — it already c
|
|
|
122
122
|
and the view controls — so the add belongs on it, in the same spot whether the section holds
|
|
123
123
|
nought or forty.
|
|
124
124
|
|
|
125
|
+
**Where there IS no heading row, the add has nowhere else to be, so it rides the surface's own
|
|
126
|
+
bar** — an attachment popover, a dialog body, any self-contained widget. That is not an exception
|
|
127
|
+
to the law so much as the same reasoning: the rule exists so the verb sits in ONE place a reader
|
|
128
|
+
can return to, and on a surface with no heading the bar IS that place. `FilesEditorUpload` is for
|
|
129
|
+
exactly this case; inside a section the host puts its own `Button` on the heading instead, because
|
|
130
|
+
`FilesEditor` provides the context and the heading is outside it — and a file widget rendering a
|
|
131
|
+
`SectionHeading` would own something the section owns. Worked both ways in `tpl_record` (Files,
|
|
132
|
+
Photos) and `tpl_task_board` (the attachment popover).
|
|
133
|
+
|
|
134
|
+
**A header CHECKBOX is legible only when it heads a COLUMN.** A table's select-all works because
|
|
135
|
+
it sits at the same x as every row's box beneath it — the column is what turns a 24px tick into
|
|
136
|
+
"all of these". A grid has no such column, so the same control in a section heading heads
|
|
137
|
+
nothing: it is a transparent 24×24 at radius 6 standing between 40px grounded buttons at radius
|
|
138
|
+
10, off their baseline, differing on every axis at once, and shape is read before meaning. **A
|
|
139
|
+
grid's select-all is a BUTTON** ("Select all" / "Deselect all"), which is what
|
|
140
|
+
`FilesEditorSelectAll` ships. Nothing is lost by giving up the tri-state: `indeterminate` says
|
|
141
|
+
"some", and the `FloatingActionBar` beside it already says WHICH some as an exact count — the
|
|
142
|
+
glyph was approximating a number that was on screen.
|
|
143
|
+
|
|
144
|
+
While a mode is ON, the heading row holds only that mode's controls; the section's own ADD stands
|
|
145
|
+
down until it is off. Otherwise a `primary` add sits in a row that is entirely about the
|
|
146
|
+
selection, competing with the promoted act in the action bar — two accents on one screen. At rest
|
|
147
|
+
the add returns to the right edge where the law puts it, and drag/paste never stopped working, so
|
|
148
|
+
no path actually closes.
|
|
149
|
+
|
|
125
150
|
- **Weight is the section's**, not a hedge: a section's add is the act that section offers, so
|
|
126
151
|
`primary`. Don't drop to `secondary` because the list is full — an add that changes weight
|
|
127
152
|
with row count is the fault, not the cure.
|
|
@@ -306,9 +331,50 @@ Divider-separated.
|
|
|
306
331
|
|
|
307
332
|
| `PressableRow` variant | Shape | Use |
|
|
308
333
|
|---|---|---|
|
|
309
|
-
| `register` (default) | rounded (radius 10) full-width hover/open/`marked` wash
|
|
310
|
-
| `bleed` (legacy) |
|
|
311
|
-
| `inset` | rounded (radius 8)
|
|
334
|
+
| `register` (default) | rounded (radius 10) full-width hover/open/`marked` wash | THE record list |
|
|
335
|
+
| `bleed` (legacy) | square wash, `Divider`-separated | only an edge-to-edge data grid that genuinely wants hard rules |
|
|
336
|
+
| `inset` | rounded (radius 8) | grouped lists in a padded container (Accordion drill-downs) |
|
|
337
|
+
|
|
338
|
+
**A washed row BLEEDS its wash; it never insets its content.** All three variants
|
|
339
|
+
carry `marginHorizontal: -ROW_WASH_BLEED` absorbed by an equal `paddingHorizontal`
|
|
340
|
+
— net zero for layout — so a row's content sits on its container's own edge,
|
|
341
|
+
level with the headings, labels and prose around it, while the wash still gets
|
|
342
|
+
breathing room by growing into the parent's padding. The parent must afford
|
|
343
|
+
`ROW_WASH_BLEED` (8px); every kit container clears it (`Drawer` 20, `Dialog` 24,
|
|
344
|
+
the page canvas 28).
|
|
345
|
+
|
|
346
|
+
**A bar of verbs is TWO groups, not a row of peers.** `FloatingActionBar` separates its own
|
|
347
|
+
STATE (the count and Clear — what is picked, and how to un-pick it) from the ACTS
|
|
348
|
+
(`children`) with the section boundary getting the most air (20) and each group sitting on the
|
|
349
|
+
action gap (8). Spaced uniformly, four controls read as four equal things, and the emphasis
|
|
350
|
+
ladder between them — muted for a dismiss, `danger` for a destructive act, `primary` for the
|
|
351
|
+
promoted one — stops reading as a ladder and starts reading as arbitrary variety. The colours
|
|
352
|
+
are not the defect when this happens; the spacing is. Group first, and the ladder is then read
|
|
353
|
+
WITHIN the acts, where it means something.
|
|
354
|
+
|
|
355
|
+
The grouping pays a second time at PHONE width, which is why it is structure and not styling. A
|
|
356
|
+
count plus an escape plus three verbs is ~520px of content — wider than a 375 screen — so the
|
|
357
|
+
bar wraps; because `state` and `acts` are each a nowrap row, the ONLY break available is the
|
|
358
|
+
boundary between them. The line splits exactly where the meaning does (what is picked / what to
|
|
359
|
+
do with it) instead of orphaning one verb from the ladder it belongs to. A row of five peers has
|
|
360
|
+
no such seam and breaks wherever it runs out of room.
|
|
361
|
+
|
|
362
|
+
**Two exceptions, both real.** A self-contained OVERLAY — a menu, a selector's popover
|
|
363
|
+
(`MenuListItem`, `OptionList`) — has nothing outside itself to align with, so its rows keep
|
|
364
|
+
plain padding; bleeding there would push them past the popover's own edge. And a slot that
|
|
365
|
+
holds another component's PART takes that component's constant, never a hand-picked number:
|
|
366
|
+
`AgentRun`'s dot column was 18 while the `Marker` it holds is `NODE` (24), so a 24px dot
|
|
367
|
+
centred in an 18px column put the whole run rail 3px outside the surface's edge — every row
|
|
368
|
+
wrong by the same amount, so it read as internally consistent and nothing flagged it.
|
|
369
|
+
|
|
370
|
+
The register used to pad 20 WITHOUT the negative margin, and the cost was not
|
|
371
|
+
the 20px — it was that everything nearby had to be hand-padded by the same 20 to
|
|
372
|
+
line up with it. A `Table`'s header band, its static rows and its expanded detail
|
|
373
|
+
all inherited the number; a `SummaryLine` above the register got it at the call
|
|
374
|
+
site; a bare table's footer borrowed `CardFooter` and picked up a *card's*
|
|
375
|
+
interior gutter. **An indent is legible only when something VISIBLE occupies it**
|
|
376
|
+
— a checkbox, an ordinal, a diff mark. An empty one is a defect, and the
|
|
377
|
+
compensation it forces spreads to every new surface that sits beside it.
|
|
312
378
|
|
|
313
379
|
The `Divider` BETWEEN rows is the resting separation; the wash is the hover state. `selected`
|
|
314
380
|
shows a zinc-100 highlight; `marked` (a bulk-ticked row) a resting blue-50 tint.
|
|
@@ -419,7 +485,7 @@ and the "detail" of a list belongs in a `Drawer`/sheet, not a second column):
|
|
|
419
485
|
band, plus a bottom-pinned `<FloatingActionBar count label onClear>` carrying the bulk
|
|
420
486
|
action(s) while ≥ 1 row is ticked (it renders nothing at count 0); a row that can't take the
|
|
421
487
|
action has no usable checkbox. Bar weight gradient, left → right: the built-in Clear is muted
|
|
422
|
-
(the quietest act), destructive bulk = `danger
|
|
488
|
+
(the quietest act), destructive bulk = `danger` + icon, secondary bulk = `secondary`
|
|
423
489
|
+ icon, ONE primary CTA last — never repeat the count in a CTA title; the bar's label already
|
|
424
490
|
carries it.
|
|
425
491
|
|
|
@@ -504,7 +570,16 @@ All view controls are 40px tall (`CONTROL_HEIGHT`), `sm` labels, in ONE wrapping
|
|
|
504
570
|
action `primary`; it sits at a different altitude than the form's one terminal commit.)
|
|
505
571
|
- **Button labels carry no trailing ellipsis** ("Assign", not "Assign…"). **Button color is
|
|
506
572
|
VALENCE/RISK, never category**: the ladder `muted < secondary < primary` is the emphasis axis;
|
|
507
|
-
`danger` (and its quieter `danger-secondary`) marks destructive — that's the whole axis.
|
|
573
|
+
`danger` (and its quieter `danger-secondary`) marks destructive — that's the whole axis.
|
|
574
|
+
**Which of the two is decided by the COMPANY the button keeps, never by how destructive the
|
|
575
|
+
act is.** Among other BUTTONS — a bar, a form's action row, a toolbar — use `danger`: it
|
|
576
|
+
carries a ground (red-100) exactly where `secondary` carries zinc-100, so every act in the row
|
|
577
|
+
keeps ONE shape and only the ink moves; the ladder is then legible as a ladder. Drop a
|
|
578
|
+
groundless `danger-secondary` in there and the destructive verb is the one control with no box
|
|
579
|
+
— a shape difference, which a reader parses before colour and reads as an accident, and the
|
|
580
|
+
row stops looking designed (this is what "four buttons with different shapes" is, every time).
|
|
581
|
+
Among TEXT — a row's trailing verb, a `Callout`'s action, a dialog's tertiary — use
|
|
582
|
+
`danger-secondary`, because there a filled box is the thing that looks pasted on. A
|
|
508
583
|
fill-less `Button` (`danger-secondary`, `muted`, un-colored) shows no box, so its optical edge is
|
|
509
584
|
its INK, inset by its padding: **never align one to a column of TEXT** — it reads as indented
|
|
510
585
|
against every label starting on the true edge. The answer is to move the act, not to invent a
|
package/docs/data_entry.md
CHANGED
|
@@ -581,6 +581,19 @@ intake paths, all wired to ONE handler:
|
|
|
581
581
|
modal dialog with no wrapper). Scope it with `enabled` ("while this record/dialog is open");
|
|
582
582
|
a paste with no acceptable file is left alone, so Ctrl+V in a text field still pastes text.
|
|
583
583
|
|
|
584
|
+
**What `accept` is matched against, and the one thing a paste cannot do.** A pattern is an
|
|
585
|
+
extension (`.pdf`), a wildcard type (`image/*`) or an exact MIME. A file is matched on
|
|
586
|
+
`resolveMimeType(file.type, file.name)` — its own declared type when that type says something,
|
|
587
|
+
and what its EXTENSION says when it doesn't. That fallback is not a nicety: a file copied in
|
|
588
|
+
Finder/Explorer and pasted arrives with real bytes and a real filename but an uninformative
|
|
589
|
+
type — `""`, or just as often `application/octet-stream`, the same non-answer wearing the shape
|
|
590
|
+
of one. Judging it on that reads silence as "not a PDF", the file is discarded, and since an
|
|
591
|
+
unacceptable paste is deliberately left alone, the user sees nothing happen at all. Matching on
|
|
592
|
+
the resolved type also means the surface agrees with what the server will store. Separately,
|
|
593
|
+
some clipboards carry only a file *reference* (a path/URL, no bytes): there is nothing to
|
|
594
|
+
upload and no API that can go read it, so those pastes cannot work by any means — dragging the
|
|
595
|
+
file is the path that always does.
|
|
596
|
+
|
|
584
597
|
**Paste routing (focus, then stack).** When two file targets are enabled on the same layer,
|
|
585
598
|
the paste goes to the TOP-MOST one whose `region` contains focus (`document.activeElement`) —
|
|
586
599
|
so two peer file sections each win while the user is working in them, never "last-mounted
|
|
@@ -7,7 +7,6 @@ import { ActionMenu, type ActionMenuItem } from "@lotics/ui/action_menu";
|
|
|
7
7
|
import { Alert } from "@lotics/ui/alert";
|
|
8
8
|
import { Badge } from "@lotics/ui/badge";
|
|
9
9
|
import { Button } from "@lotics/ui/button";
|
|
10
|
-
import { CardFooter } from "@lotics/ui/card";
|
|
11
10
|
import { CheckboxInput } from "@lotics/ui/checkbox_input";
|
|
12
11
|
import { DetailRow, DetailTable } from "@lotics/ui/detail_row";
|
|
13
12
|
import { Divider } from "@lotics/ui/divider";
|
|
@@ -571,6 +570,11 @@ function EnterDataDialog({ open, onOpenChange, seedDocs, onCreate, onCreateMany
|
|
|
571
570
|
);
|
|
572
571
|
setPhase("running");
|
|
573
572
|
},
|
|
573
|
+
// A REAL app passes `useAgentRun().cancel` — which stops the run on the
|
|
574
|
+
// SERVER, not just locally. Here the run IS the reveal timer below, and that
|
|
575
|
+
// timer is gated on `phase`, so leaving the streaming phase is what stops
|
|
576
|
+
// it; there is nothing else for this mock to tear down.
|
|
577
|
+
cancel: () => {},
|
|
574
578
|
};
|
|
575
579
|
useEffect(() => {
|
|
576
580
|
if (phase !== "analyze" && phase !== "running") return;
|
|
@@ -624,7 +628,17 @@ function EnterDataDialog({ open, onOpenChange, seedDocs, onCreate, onCreateMany
|
|
|
624
628
|
setDienThoai("");
|
|
625
629
|
setPhi(null);
|
|
626
630
|
};
|
|
627
|
-
|
|
631
|
+
/**
|
|
632
|
+
* Closing CANCELS — it does not merely stop watching.
|
|
633
|
+
*
|
|
634
|
+
* `useAgentRun` aborts on unmount, and its `abort` is deliberately
|
|
635
|
+
* local-only: the run keeps executing server-side so it survives navigating
|
|
636
|
+
* away and lands in the session history. That is right for a page you will
|
|
637
|
+
* come back to, and wrong for a dialog you just dismissed — the operator has
|
|
638
|
+
* abandoned the result and every remaining token is spent on nobody. So the
|
|
639
|
+
* close path calls `cancel` explicitly. Unmount is not cancellation.
|
|
640
|
+
*/
|
|
641
|
+
const close = () => { run.cancel(); onOpenChange(false); reset(); };
|
|
628
642
|
|
|
629
643
|
/**
|
|
630
644
|
* THE COMMIT. It writes what is on screen — the edited drafts, minus the
|
|
@@ -955,9 +969,22 @@ function EnterDataDialog({ open, onOpenChange, seedDocs, onCreate, onCreateMany
|
|
|
955
969
|
<Button title="Back" color="secondary" onPress={() => { setKhach(""); setDienThoai(""); setPhi(null); setPhase("intake"); }} />
|
|
956
970
|
<Button title="Create record" color="primary" disabled={khach.trim() === ""} onPress={createManual} />
|
|
957
971
|
</DialogFooter>
|
|
958
|
-
) : phase === "clarify" ? (
|
|
972
|
+
) : phase === "analyze" || phase === "clarify" || phase === "running" ? (
|
|
959
973
|
<DialogFooter>
|
|
960
|
-
|
|
974
|
+
{/* EVERY run-driven phase, not just `clarify`. This branch used to
|
|
975
|
+
name that one phase, because `AgentRunActions` only ever held the
|
|
976
|
+
wizard and there was nothing to show while the feed streamed —
|
|
977
|
+
so the two streaming phases had an empty footer and no way to
|
|
978
|
+
stop. The component's contract is that the HOST mounts it
|
|
979
|
+
wherever the run drives the surface and the RUN decides what it
|
|
980
|
+
holds; gating it by phase out here duplicates that decision and
|
|
981
|
+
gets it wrong the moment the run learns a new one.
|
|
982
|
+
|
|
983
|
+
`onStop` is the SAME handler the pane's `onCancel` gets: stopping
|
|
984
|
+
a streaming run and abandoning a parked question both put the
|
|
985
|
+
operator back at the intake they started from, with their files
|
|
986
|
+
and their answers still there to retry from. */}
|
|
987
|
+
<AgentRunActions run={run} onStop={() => setPhase("intake")} />
|
|
961
988
|
</DialogFooter>
|
|
962
989
|
) : phase === "preview" ? (
|
|
963
990
|
<DialogFooter>
|
|
@@ -1252,7 +1279,13 @@ export function TplItemList() {
|
|
|
1252
1279
|
<EmptyState message="No matching records" hint="Try another keyword or switch status" />
|
|
1253
1280
|
</>
|
|
1254
1281
|
) : null}
|
|
1255
|
-
|
|
1282
|
+
{/* NOT `CardFooter` — that is a Card PART, and its 20px is the card's
|
|
1283
|
+
interior gutter. There is no card here, so it put the totals 20px
|
|
1284
|
+
right of the register's own rows and of the toolbar above them.
|
|
1285
|
+
A bare table's footer is a divider and a row; borrowing furniture
|
|
1286
|
+
from a container that is not there is what put it out of true. */}
|
|
1287
|
+
<Divider />
|
|
1288
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 12, paddingVertical: 12 }}>
|
|
1256
1289
|
<Text size="sm" color="muted" tabular style={{ flex: 1 }}>
|
|
1257
1290
|
{`${filtered.length} records, ${formatMoney(tongPhi)} in fees`}
|
|
1258
1291
|
</Text>
|
|
@@ -1264,7 +1297,7 @@ export function TplItemList() {
|
|
|
1264
1297
|
total={filtered.length}
|
|
1265
1298
|
onPageChange={setPage}
|
|
1266
1299
|
/>
|
|
1267
|
-
</
|
|
1300
|
+
</View>
|
|
1268
1301
|
</View>
|
|
1269
1302
|
</View>
|
|
1270
1303
|
</ScrollView>
|