@lotics/ui 14.1.0 → 14.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 CHANGED
@@ -56,6 +56,11 @@ doc before building any screen, **never from memory**. Exact props are the shipp
56
56
  hand-picked palette shade; color is `solid`/`tint`/`ramp` with ONE accent per screen.
57
57
  - **Pure primitives only** — no i18n, analytics, or domain types in `src/` (pass `labels`,
58
58
  callbacks); Lotics-coupled UI belongs in `@lotics/ui-internal`.
59
+ - **A button never contains a control** — a row that presses open AND carries its own controls
60
+ (a CTA, ⋯, a checkbox, a `Link`) is a role-less `PressableRow` + a **`PressDoor`** sibling
61
+ (the tab stop, name, focus ring), never `PressableHighlight`, which wraps its children in the
62
+ button. `Table`/`TableRow` and `LinkedRecordBox` already do this. →
63
+ [composition.md](./docs/composition.md).
59
64
  - **Every state designed** — skeleton (mirroring layout), empty, error; no layout shift.
60
65
  - **Copy states content, not mechanics** — a `description` names the section's content or one
61
66
  domain rule; widget behavior ("edits in place, saves on its own") and selling points never
@@ -39,7 +39,7 @@ Three shapes:
39
39
 
40
40
  The Document desk's Use-AI fork IS this table as UI: Extract data (1) · Cross-check (2) ·
41
41
  Edit with AI (3) — one entry point, three outcome shapes
42
- ([`tpl_documents`](../examples/tpl_documents.tsx)).
42
+ ([`tpl_record`](../examples/tpl_record.tsx)'s Documents section).
43
43
 
44
44
  ## Command / compose — `Composer`
45
45
 
@@ -138,16 +138,20 @@ pass it as the `error` prop and it renders as a terminal danger row under the tr
138
138
 
139
139
  **In a bounded container, wrap it in `FollowScroll`.** An `AgentRun` streaming inside a dialog,
140
140
  drawer, or fixed-height panel grows BELOW the fold — a plain scroll container doesn't follow. Wrap
141
- the feed: `<FollowScroll style={{ maxHeight: … }}><AgentRun … /></FollowScroll>` — pinned to the
142
- newest content while streaming, unpinned when the human scrolls up to read (`AgentProgress`'s
143
- expanded panel already does this). A full-page inverted list (chat) follows by construction and
144
- doesn't need it.
141
+ the feed: `<FollowScroll style={{ maxHeight: … }}><AgentRun … /></FollowScroll>` — the same
142
+ inverted-list mechanism as a chat message list, packaged as a wrapper: the follow happens at
143
+ LAYOUT level (each growth paints already pinned no scroll-after-paint flash), scrolling up
144
+ releases it, and the feed stays at the newest content when the run settles (`AgentProgress`'s
145
+ expanded panel does this). When settling REPLACES the feed with different content (a results
146
+ pane), swap `FollowScroll` for a plain scroller on the phase flip — the remount opens the new
147
+ content at the top; a transcript that persists just stays in `FollowScroll`. A full-page chat
148
+ list is already inverted and doesn't need the wrapper.
145
149
 
146
150
  Fed natively by `@lotics/app-sdk`'s `useAgentRun().parts` (reasoning + per-tool I/O + state come
147
151
  for free — no hand-assembly): `<AgentRun parts={run.parts} state={…} error={run.error} />` — see
148
152
  [the SDK doc](../../app-sdk/docs/ai.md) for the hook. Requires `ai` as an optional (type-only) peer.
149
153
  **Reasoning only shows when the model produces it** — the declared model must support thinking
150
- (Sonnet 4.6 / Opus 4.8, not Haiku); the renderer shows every reasoning part it's handed.
154
+ (Sonnet / Opus, not Haiku); the renderer shows every reasoning part it's handed.
151
155
 
152
156
  **Limitation:** the collapsed reasoning row's "Thinking" label and the expanded detail's
153
157
  "Input"/"Output"/"Error" panel titles are currently English-only (not on the locale provider and
@@ -171,7 +175,7 @@ when done.
171
175
 
172
176
  Every "the agent proposes → the human accepts / edits / dismisses → nothing auto-applies" surface
173
177
  composes from twelve pieces in three groups (all in `@lotics/ui/change_review`; worked example:
174
- [`tpl_documents`](../examples/tpl_documents.tsx)).
178
+ [`tpl_record`](../examples/tpl_record.tsx)'s Use-AI extract flow).
175
179
 
176
180
  **Frame**
177
181
 
@@ -278,7 +282,9 @@ Back / Next / Cancel / Submit (Next/Submit primary), the step position an eyebro
278
282
  question — no progress bar, and no eyebrow at all for a single question. Each step is a `Clarify`; advance only once the
279
283
  current question is answered; `onSubmit` gives one `{ value, custom }` per question (aligned by index,
280
284
  `custom` true when the answer came from "Other…"), `onCancel` dismisses. All navigation chrome is
281
- locale-wired (the `clarify` slice). The multi-question form of the ask-back.
285
+ locale-wired (the `clarify` slice). The multi-question form of the ask-back. Worked example:
286
+ [`tpl_item_list`](../examples/tpl_item_list.tsx)'s intake fork (the wizard between the analyze
287
+ and import runs).
282
288
 
283
289
  ## Provenance — `Sources`
284
290
 
@@ -362,7 +368,7 @@ ranked set, don't cram it into chat prose:
362
368
  so the alternatives are first-class and picking a different one changes the outcome; refining
363
369
  the description re-ranks. For look-up-and-explain: tariff/classification codes, fee lookup,
364
370
  policy Q&A, a spec/compliance desk. (NOT a chat with the answer in a bubble.)
365
- - **Document desk** ([`tpl_documents`](../examples/tpl_documents.tsx) — THE go-to for
371
+ - **Document desk** (in [`tpl_record`](../examples/tpl_record.tsx) — THE go-to for
366
372
  document-driven records) — the record's files block feeds ONE "Use AI" entry that FORKS into
367
373
  the two document tasks, each a specialized run with a task-pure result: **Extract** (files
368
374
  read → fields already matching fold into one quiet line → every add / update / conflict a
@@ -376,6 +382,15 @@ ranked set, don't cram it into chat prose:
376
382
  full-page `FileGalleryModal`. Plus **Create documents**: a readiness checklist (unchecked by
377
383
  default, missing inputs called out) → generate → the files land back on the record. Never merge
378
384
  the two AI tasks into one mixed output — the fork is the design.
385
+ - **Intake desk** (in [`tpl_item_list`](../examples/tpl_item_list.tsx)'s "Enter data" fork) — the
386
+ register's New CTA leads with AI: a `FileDropzone` hero → a short ANALYZE run reads the files →
387
+ `ClarifyWizard` asks the ambiguities the analysis surfaced → the IMPORT run drafts records →
388
+ `ChangeRecord` cards + one Apply land the rows; the manual VARIANTS sit below the dropzone as
389
+ `CardSelectItem`s, each opening the classic create-then-refine form. The template mocks the
390
+ clarify step as TWO app-orchestrated phases (analyze-run → wizard → import-run); a real app
391
+ can do it in ONE run — every app agent carries `ask_user_choice`, so the run PARKS on the
392
+ agent's own question and `useAgentRun().pendingChoice`/`answerChoice` drive the same
393
+ `ClarifyWizard` (see the SDK's [ai doc](../../app-sdk/docs/ai.md)).
379
394
  - **Triage** — an inbox the agent classified + routed is a `ChangeReview` of `Change`s (body: the
380
395
  item + the agent's call; the standard Keep/Drop verbs; Keep-all covers the high-confidence
381
396
  sweep). Leads, tickets, documents, emails.
package/docs/catalog.md CHANGED
@@ -179,10 +179,19 @@ of a record/settings surface).
179
179
 
180
180
  `PressableRow` (THE register row; forwards its `ref`, so wrapping it in a `PopoverTrigger`
181
181
  anchors a row-triggered peek Popover — without the ref the trigger is unmeasurable and the
182
- popover renders off-screen), `ListItem`, `MenuButton`, `MenuListItem`, `DetailRow`
182
+ popover renders off-screen), `PressDoor` (the keyboard door that pairs with it — see
183
+ below), `ListItem`, `MenuButton`, `MenuListItem`, `DetailRow`
183
184
  (label+value for drawers/peeks; optional `trailing` slot for a right-side
184
185
  action/badge/unit), `ActionMenu` (⋯), `FloatingActionBar` (bulk-select bar).
185
186
 
187
+ A DESIGNED row (your own pile, card, or panel) that presses open AND carries its own
188
+ controls is `PressableRow` + **`PressDoor`** — never `PressableHighlight`, which IS a
189
+ button and wraps its children (a button must not contain interactive descendants). The
190
+ door is an empty absolutely-positioned SIBLING of the content carrying the tab stop,
191
+ accessible name, and focus ring; lift the content above it with `zIndex: 1`. `Table`/
192
+ `TableRow` and `LinkedRecordBox` do exactly this internally, so a columnar register needs
193
+ no assembly — reach for `PressDoor` only outside them.
194
+
186
195
  ### Filters & view controls
187
196
 
188
197
  `SearchInput`, `ChipGroup`, `FilterChip` (+ `RangeSlider`, `Counter`), `Chip`,
@@ -212,8 +221,9 @@ trailing ⋯ menu = Download · Open-external · Remove; the default "here are s
212
221
  surface), `FileGrid` (the upload-aware grid: completed files + a live upload queue in one
213
222
  surface — `FilesEditor` is this + the toolbar; reach for `FileGrid` bare when you own the
214
223
  chrome), `FileThumbnail` / `FileThumbnailGrid` (square tiles; `onPress`/`onFilePress` makes each
215
- tile a pressable door — e.g. tap-to-preview — carrying an accessible button role + filename
216
- label; `selectedIds` for a selection overlay),
224
+ tile a pressable door — e.g. tap-to-preview — carrying an accessible button role + the
225
+ filename as its name, overridable per tile with `accessibilityLabel` when what the press
226
+ DOES reads better than a raw filename; `selectedIds` for a selection overlay),
217
227
  `UploadingThumbnail` (the single in-flight tile FileGrid renders — reach for it only when
218
228
  hand-rolling a non-grid upload layout), `FileRow` (a horizontal file/document LINE —
219
229
  badge-or-placeholder + name + meta + a composable `trailing` slot for a status badge /
@@ -443,6 +453,14 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
443
453
  - **`pressable_row`** — `PressableRow`: THE register row — full-width hover/open wash,
444
454
  `selected` (the open record) vs `marked` (ticked in bulk-select), forwards `ref` for
445
455
  popover anchoring.
456
+ - **`press_door`** — `PressDoor`: THE keyboard door — an EMPTY absolutely-positioned
457
+ `Pressable` sibling that carries the `role="button"` tab stop, the (REQUIRED)
458
+ `accessibilityLabel`, and the focus ring for a pressable surface whose content holds its
459
+ own controls. Pair it with `PressableRow` (surface takes the mouse + the wash, door takes
460
+ the keyboard, nested controls keep their own presses); the parent owns the positioning
461
+ context and the content lifts above the door with `zIndex: 1`. `radius` (default 10)
462
+ matches the surface it spans. `TableRow` / `LinkedRecordBox` are the in-kit consumers —
463
+ reach for it when designing a row those don't cover.
446
464
  - **`linked_record_box`** — `LinkedRecordBox`: a bordered box scoping ANOTHER record's data
447
465
  (`icon` · `name` · `subtitle` · vertical `facts`), the WHOLE box a keyboard door into its
448
466
  detail (`onOpen`/`doorLabel`); verbs ride an `actions` slot in a hairline-fenced footer at the
@@ -906,7 +924,11 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
906
924
  - **`file_thumbnail`** — `FileThumbnail` + `DisplayFile` + `THUMBNAIL_SIZE` /
907
925
  `COMPACT_THUMBNAIL_SIZE` + `getMediaIcon`: the completed tile — the right surface per
908
926
  MIME: image thumbnail · a doc tile with the `FileBadge` centered + a single-line filename
909
- · media card; `isTemplate` overlays a TMPL marker.
927
+ · media card; `isTemplate` overlays a TMPL marker. The tile's accessible name is the
928
+ filename; pass `accessibilityLabel` to say what pressing it DOES instead. The per-surface
929
+ pieces are exported for a hand-rolled layout: `DocumentBadge` (a bare pressable badge —
930
+ its `size` is the square SLOT side, like every other tile here, and the badge is fitted to
931
+ it), `DocumentCard`, `MediaCard`, `RemoveButton`.
910
932
  - **`file_thumbnail_grid`** — `FileThumbnailGrid` (+ the generic `ThumbnailGrid`):
911
933
  display-only square-tile grid; `selectedIds` + `onFilePress` pair with
912
934
  `useSelectionMode()` for gated select-mode CRUD.
@@ -919,7 +941,9 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
919
941
  + a ⋯ Download/Open-external/Remove menu; composes `FileRow` + `ActionMenu` +
920
942
  `FileGalleryModal`; localized via the `gallery` locale slice.
921
943
  - **`file_badge`** — `FileBadge`: the two-tone file-type mark (PDF / XLSX / DOCX / video /
922
- audio …) driven by MIME.
944
+ audio …) driven by MIME. Its `size` is the badge's WIDTH and everything scales off it —
945
+ the mark is TALLER than it is wide (26 × 32 by default), so a square slot takes the
946
+ fitted width, not the slot side (that conversion is what `DocumentBadge` does).
923
947
  - **`file_preview`** — `FilePreview`: the universal inline preview — image/PDF/video/audio +
924
948
  Word via `@lotics/docx` + Excel/CSV via `@lotics/xlsx`; the heavy engines (pdf.js ·
925
949
  `@lotics/docx` · `@lotics/xlsx`) are LAZY (dynamic-imported, ~free until a doc of that
@@ -977,10 +1001,13 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
977
1001
  - **`agent_progress`** — `AgentProgress`: `AgentRun`'s compact, floating, expandable form (same
978
1002
  `parts` prop) — a composer's working state.
979
1003
  - **`follow_scroll`** — `FollowScroll`: a scroll container that FOLLOWS its growing content —
980
- pinned to the bottom as children stream in; scrolling up unpins (read history), scrolling back
981
- re-pins. Wrap a streaming `AgentRun` with it in any bounded container (a dialog, a drawer,
982
- a panel put the height bound on `style`); `AgentProgress`'s expanded panel uses it. Chat
983
- doesn't need it (an inverted message list follows by construction).
1004
+ the chat message list's inverted mechanism packaged as a wrapper (single-cell inverted list),
1005
+ so the follow happens at LAYOUT level: each growth paints already pinned (no flash), scrolling
1006
+ up releases it, and short content still reads top-down. Wrap a streaming `AgentRun` with it in
1007
+ any bounded container (a dialog, a drawer, a panel — put the height bound on `style`);
1008
+ `AgentProgress`'s expanded panel uses it. Inherits the inverted pattern's quirks (scrollbar
1009
+ thumb runs opposite; keyboard paging inverted) — same as chat. A full-page chat list is
1010
+ already inverted and doesn't need it.
984
1011
  - **`confidence`** — `Confidence` + `ConfidenceLevel` + `levelFromScore`: calibrated
985
1012
  high/med/low; localized via the provider.
986
1013
  - **`change_review`** — the COMPOUND review family — frame: `ChangeReview` provider/stack ·
@@ -287,6 +287,14 @@ body is arbitrary app content — `TableRow`'s cells, which legitimately carry a
287
287
  trigger — the door is an EMPTY absolutely-positioned sibling under the cells (tab stop + name +
288
288
  focus ring; the mouse rides the surface), so a control in a cell never nests inside it.
289
289
 
290
+ That empty door is the **`PressDoor`** primitive (`@lotics/ui/press_door`) — don't hand-roll it
291
+ when you design a row `Table` doesn't cover (a mobile pile with a per-row control, a bespoke
292
+ card): `<PressableRow onPress>` + `<PressDoor onPress accessibilityLabel="Open …" />` + your
293
+ content on `zIndex: 1` (the door hit-tests above in-flow siblings, so the content must lift back
294
+ over it). `PressableHighlight` is the wrong base for such a row — it IS a button and wraps its
295
+ children, so any nested control lands inside it; it stays for surfaces whose content is
296
+ non-interactive by construction. `TableRow` and `LinkedRecordBox` already compose `PressDoor`.
297
+
290
298
  Actions stay **visible** — a hover-only action is invisible to keyboard and touch users. The ONLY
291
299
  exception is a DENSE tree/register where a persistent per-row action would clutter: reveal on
292
300
  hover **OR focus-within**, keeping the action in the DOM + tab order and gating only its opacity,
package/docs/templates.md CHANGED
@@ -50,7 +50,7 @@ the package index is [../AGENTS.md](../AGENTS.md).
50
50
  | Shift signup + staffing | `tpl_shifts` |
51
51
  | AI produces a visual artifact on a canvas | `tpl_dieline` |
52
52
  | AI ranks answers — look-up-and-explain | `tpl_lookup` |
53
- | AI over a record's documents (extract / cross-check / generate) | `tpl_documents` |
53
+ | AI over a record's documents (extract / cross-check / generate) | `tpl_record` — its Documents + Document set sections |
54
54
 
55
55
  ## Composition rules — how a record surface is assembled
56
56
 
@@ -204,6 +204,17 @@ screens — register, per-row action, gated selection, and act-on-many in one. T
204
204
  newest first — an entry's attachments ride its expandable details as a thumbnail grid
205
205
  whose press opens the shared `FileGalleryModal`.
206
206
  - A closing `DangerZone`.
207
+ - **The "Enter data" INTAKE FORK** (the New CTA) — AI FIRST, the form as fallback, one phased
208
+ dialog: a `FileDropzone` hero (drop the customer's files) → a short ANALYZE stream
209
+ (`AgentRun` in a `FollowScroll`) reads them → `ClarifyWizard` asks the ambiguities the
210
+ analysis surfaced (described options, one custom-answer slot) → the IMPORT stream drafts the
211
+ records → `ChangeRecord` cards to Keep/Drop (fee editable in place) + one outcome-named
212
+ Apply that really lands the rows in the register (no drawer on a bulk landing). BELOW the
213
+ dropzone, "or enter manually" → the record VARIANTS as `CardSelectItem`s, each opening the
214
+ same THREE-field create-then-refine form (a single manual create still opens its drawer).
215
+ The template mocks the clarify step as two app-orchestrated phases; a real app can do it in
216
+ ONE run — every app agent carries `ask_user_choice`, so the run parks on the agent's own
217
+ question and `useAgentRun().pendingChoice`/`answerChoice` drive the same `ClarifyWizard`.
207
218
 
208
219
  ### `tpl_pick` — the guided queue
209
220
 
@@ -266,15 +277,13 @@ billing, and quick-capture templates. Top → bottom:
266
277
  (dashed add-ghost when unset), sized to the list's `trailingWidth` so the title never overlaps.
267
278
  (Contrast the General section's dates/assignee, the same editors in the default `variant="form"`
268
279
  — a zinc-50 chip + hover-border + the full member chip.)
269
- - **Documents** — the Agents "Document desk" pattern (shared with `tpl_documents` change
270
- it in BOTH): the register `Table` (search · Add files) whose selection feeds the
271
- `FloatingActionBar` → ONE "Use AI" fork (extract / cross-check / edit-with-AI). Every
272
- row's leading visual is a `FileThumbnail` in ONE square 32px slot — an image file fills
273
- it as a real thumbnail, a document centers its badge in it — never a bare `FileBadge`
274
- (mixed footprints misalign the identity column). ONE
275
- sanctioned divergence: GENERATION — on the record page this desk holds what ARRIVES and
276
- Create moves to the Document set OUTPUT section below; `tpl_documents` (no output
277
- pipeline on its page) keeps the toolbar "Create documents" dialog.
280
+ - **Documents** — the Agents "Document desk" pattern (this template is its worked example
281
+ see the Agents chapter below): the register `Table` (search · Add files) whose selection
282
+ feeds the `FloatingActionBar` → ONE "Use AI" fork (extract / cross-check / edit-with-AI).
283
+ Every row's leading visual is a `FileThumbnail` in ONE square 32px slot — an image file
284
+ fills it as a real thumbnail, a document centers its badge in it — never a bare `FileBadge`
285
+ (mixed footprints misalign the identity column). The desk holds what ARRIVES; generation
286
+ lives in the Document set OUTPUT section below.
278
287
  - **Customer — the LINKED-RECORD reference** via **`LinkedRecordBox`**: another record's data
279
288
  NEVER renders as field rows on this one — a BORDERED, fully pressable box (glyph anchor ·
280
289
  identity · facts stacked VERTICALLY) whose press opens the detail drawer; the verbs ride the
@@ -460,8 +469,9 @@ double-shift days flagged). Accepting and assigning move the board live.
460
469
  ## Agents
461
470
 
462
471
  Screens where AI does the work and the human reviews. `tpl_dieline` *produces* an artifact;
463
- `tpl_lookup` and `tpl_documents` *structure* information for a decision. The AI component
464
- contracts and the propose→review→apply loop live in [ai_patterns.md](./ai_patterns.md).
472
+ `tpl_lookup` *structures* information for a decision; the document desk (in `tpl_record`)
473
+ does both over a record's files. The AI component contracts and the propose→review→apply
474
+ loop live in [ai_patterns.md](./ai_patterns.md).
465
475
 
466
476
  ### `tpl_dieline` — the design canvas
467
477
 
@@ -484,12 +494,15 @@ rows, policy `Callout`s, `Confidence`, and `Sources`. The flow is input → matc
484
494
  a single confident verdict: classification is ambiguous, so the alternatives are first-class.
485
495
  Refine the description in a follow-up and the ranking updates.
486
496
 
487
- ### `tpl_documents`the document desk
497
+ ### The document desk in `tpl_record`
488
498
 
489
- THE worked example for everything document-driven on a record. A record's files block feeds
490
- ONE **"Use AI"** entry the one AI entry point, never per-row AI buttons (the per-row `⋯`
499
+ THE worked example for everything document-driven on a record lives on the record template
500
+ (its Documents sectionno standalone template). A record's files block feeds ONE
501
+ **"Use AI"** entry — the one AI entry point, never per-row AI buttons (the per-row `⋯`
491
502
  menu holds only Rename/Download). Inside its dialog the run FORKS into three tasks as
492
- `CardSelectItem`s (select → a confirming footer CTA, never one-click):
503
+ `CardSelectItem`s (select → a confirming footer CTA, never one-click), and the streaming
504
+ phase runs inside a `FollowScroll` (swapped back to `DialogScrollArea` on the phase flip,
505
+ so review opens at the top):
493
506
 
494
507
  - **Extract** (read the documents, fill the record) — the open `ChangeFields` review: ONE
495
508
  `Change` section per record whose body stacks a `ChangeField` per proposed value (label ·
@@ -502,10 +515,9 @@ menu holds only Rename/Download). Inside its dialog the run FORKS into three tas
502
515
  - **Edit with AI** — the `askAi` handoff: dialogue-shaped file iteration happens in the
503
516
  platform chat (the documents attached to a fresh thread), not in the app.
504
517
 
505
- Separately, a **Create documents** action opens its own dialog: a readiness checklist
506
- (hand-composed rows per-document missing inputs open a fill screen, saved values recompute
507
- readiness) generation runs as a visible `AgentRun`, never a teleport → generated files stay
508
- PENDING (previewable) until the user commits them onto the record.
518
+ GENERATION is separate and deterministic the record's Document set OUTPUT section
519
+ (readiness checklist template fill with a `loading` trigger, never `AgentRun` theater;
520
+ the set appears at once).
509
521
 
510
522
  All mock — `useState` plus a timer streams the `AgentRun`; a real app drives the same
511
523
  components from its live agent run.