@lotics/ui 14.3.1 → 15.1.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
@@ -1,31 +1,5 @@
1
1
  # @lotics/ui — the UI reference index
2
2
 
3
- > **Upgrading to v13 from 12.x.** `RadioPicker`'s **`direction`** prop was removed — the
4
- > picker is always the stacked column of full-width choices. A former `direction="row"`
5
- > usage just drops the prop. Additively, `DetailRow` and `FormField` gained a **`warning`**
6
- > field annotation (amber, the third tone between `description` and `error`).
7
-
8
- > **Upgrading to v12 from 11.x.** Two exports were removed: **`CommentComposer`** (compose
9
- > comments with THE kit `Composer` — attach via `actionsButton`/`files`; `CommentList` is
10
- > unchanged) and **`DetailRow.info`** (field annotations are explicit — use the new
11
- > **`DetailRow.description`** under the value and **`DetailRow.error`**, the `FormField`
12
- > vocabulary; heading/card `info` ⓘ props are untouched). Visual behavior changes:
13
- > `SubsectionStack` is SPACE-ONLY (32px beat, no hairlines — titles carry the grouping),
14
- > `Section` heading gap 12→16, `DetailTable` row gap 6→8, `DetailRow` rows top-align with
15
- > label/control/trailing centered on the first control line, `Timeline` grew to 32px
16
- > discs / 16px icons / 40px minimum rows, `TextInputField` paints a WHITE surface on any
17
- > background (it was transparent — identical on the white canvas, visible on tinted
18
- > panels), and the inline editors' resting chip gained 6px vertical padding (identical for
19
- > one-line values; multi-line resting content now breathes).
20
-
21
- > **Upgrading to v11 from ≤10.x — the type scale was re-slotted.** The display sizes gained the
22
- > missing `##` step: `xl` is now 22/24 (section title), `xxl` is 28/32 (page/record title — the
23
- > OLD `xl` look), and the new `xxxl` is 32/48 (hero numbers — the OLD `xxl` look). Migrate an
24
- > app with two renames IN THIS ORDER: first `size="xxl"` → `size="xxxl"`, then `size="xl"` →
25
- > `size="xxl"` — every existing usage keeps its exact look. Also: `SectionHeadingTitle` is now
26
- > ALWAYS `##` (22/24), and disabled `Button`s fade their label to the zinc-400 disabled ink on
27
- > every variant EXCEPT `primary`, which keeps inverted ink over its washed surface.
28
-
29
3
  The component kit for Lotics custom-code apps and the product frontend: React-Native-Web
30
4
  primitives (renders on web **and** native), data-entry patterns, AI surfaces, a composition
31
5
  grammar, and worked-example screens. It pairs with **`@lotics/app-sdk`** (data + RPC — read
@@ -33,14 +7,15 @@ grammar, and worked-example screens. It pairs with **`@lotics/app-sdk`** (data +
33
7
 
34
8
  This file is the index. The comprehensive references live in **`docs/`** — read the owning area
35
9
  doc before building any screen, **never from memory**. Exact props are the shipped sources
36
- (`src/<name>.tsx`); full worked screens are `examples/tpl_*.tsx`.
10
+ (`src/<name>.tsx`); full worked screens are `examples/tpl_*.tsx`. Everything here describes the
11
+ CURRENT major only — upgrading an app across majors is `MIGRATION.md`.
37
12
 
38
13
  ## The area references
39
14
 
40
15
  | Doc | Read it for |
41
16
  |---|---|
42
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` for a custom-code app's `vite.config.ts`). Read before building any screen; reuse first. |
43
- | [docs/data_entry.md](./docs/data_entry.md) | Which editing pattern for which job — inline edit, fieldset forms, find-or-create (`Combobox`), line items, handoffs, phased records, billing, tags, dispositions, attachments, stage gates. |
18
+ | [docs/data_entry.md](./docs/data_entry.md) | Which editing pattern for which job — inline edit, fieldset forms, find-or-create (`Combobox`), line items, handoffs, phased records, billing, tags, dispositions, attachments (incl. the three-way file INTAKE: CTA + `FileDropTarget` + `usePasteFiles`), stage gates. |
44
19
  | [docs/ai_patterns.md](./docs/ai_patterns.md) | AI proposes, the human decides — composer, live run feed (`AgentRun`), review-before-apply, findings, provenance, confidence; the UI half of the SDK's [ai doc](../app-sdk/docs/ai.md). |
45
20
  | [docs/composition.md](./docs/composition.md) | The design-language contract — canvas + content column, heading altitude, banded cards, register vs inset rows, master-detail `Drawer`, view controls, color discipline, typography, whitespace. |
46
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). |
@@ -61,6 +36,15 @@ doc before building any screen, **never from memory**. Exact props are the shipp
61
36
  (the tab stop, name, focus ring), never `PressableHighlight`, which wraps its children in the
62
37
  button. `Table`/`TableRow` and `LinkedRecordBox` already do this. →
63
38
  [composition.md](./docs/composition.md).
39
+ - **Files land THREE ways on any surface that holds them** — the section's Add CTA
40
+ (`pickFiles`), and a drag OR paste, both from ONE **`<FileDropTarget … paste>`** wrapping the
41
+ region — a Files section, OR a WHOLE surface (a record's section stack, a register page), so a
42
+ drop/paste anywhere on it routes to that surface's intake (its `paste` scopes Ctrl/Cmd+V to the
43
+ region's focus; `usePasteFiles` is the bare hook for a region-less modal), all on ONE handler.
44
+ Paste routes to the top-most FOCUSED region, else the top of the stack — two peer file sections
45
+ each win when focus is in them. Nobody should hunt for a dropzone; a `FileDropzone` well is the
46
+ empty state, not the only way in. Since a wrapped region shows no zone, name the paths with a
47
+ muted heading description ("Drag, paste, or click to add files"). → [data_entry.md](./docs/data_entry.md).
64
48
  - **Every state designed** — skeleton (mirroring layout), empty, error; no layout shift.
65
49
  - **Copy states content, not mechanics** — a `description` names the section's content or one
66
50
  domain rule; widget behavior ("edits in place, saves on its own") and selling points never
package/MIGRATION.md ADDED
@@ -0,0 +1,66 @@
1
+ # @lotics/ui — migration notes
2
+
3
+ Breaking changes, newest first — normally per major, plus the rare minor that broke a type
4
+ anyway (recorded under its exact version). The current contract lives in `AGENTS.md` + `docs/`;
5
+ this file exists only to move an app from one release to the next.
6
+
7
+ ## v15 from 14.x
8
+
9
+ `AgentRun`/`AgentProgress`'s **`labelForTool`** prop was replaced by **`labelForCall`** — the
10
+ callback now receives the whole call (`AgentToolCall = { toolName, input, state }`) instead of
11
+ just the name, so labels can vary by input and by the new **`awaiting`** step state (ai's
12
+ `approval-requested`, rendered as an amber parked row). Additively: `renderToolOutput` (replace
13
+ an expanded step's Output panel), `onRetry` (a retry button under the terminal error row), and
14
+ the new `ApprovalPrompt` + `MessageActions` components with their locale slices.
15
+
16
+ ## v14 from 13.x
17
+
18
+ The ai-sdk `UIMessage.parts` shape IS the `AgentRun` contract — the bespoke transcript type is
19
+ gone. `AgentRun`/`AgentProgress`'s **`items: AgentRunItem[]`** was replaced by **`parts:
20
+ readonly AgentUIPart[]`**, and the **`AgentRunItem`** / **`AgentRunStep`** / **`AgentStepStatus`**
21
+ exports were deleted. An app feeds it straight from the SDK: `useAgentRun().items` →
22
+ **`.parts`** (`@lotics/app-sdk` ≥ 0.54.0, whose SSE reducer now emits ai `dynamic-tool` / `text`
23
+ / `reasoning` parts); chat passes `message.parts`. Hand-built demo transcripts are rewritten as
24
+ parts. Additively: a run-level **`error`** prop for the BREAKING failure that lives OUTSIDE
25
+ `parts` (a killed stream / API error) — a per-tool failure stays in its own `output-error` part
26
+ — and tool I/O now expands IN PLACE on press instead of opening a `Peek` popover.
27
+
28
+ ## 13.9.0 — a BREAKING type change in a MINOR
29
+
30
+ **`ClarifyOption.description` went optional → REQUIRED** (every answer explains itself). It
31
+ shipped in a minor, which is why it surprises an app on `^13`: a `<Clarify options={…}>` whose
32
+ options carry only `label` stops typechecking on any 13.9.0+ install. Fix by writing the
33
+ one-line description each option always needed. Same release: `Clarify`'s hardcoded "Question"
34
+ eyebrow was dropped (the question text stays), and `allowCustom` ("Other…" free text) +
35
+ `ClarifyWizard` were added.
36
+
37
+ ## v13 from 12.x
38
+
39
+ `RadioPicker`'s **`direction`** prop was removed — the picker is always the stacked column of
40
+ full-width choices. A former `direction="row"` usage just drops the prop. Additively, `DetailRow`
41
+ and `FormField` gained a **`warning`** field annotation (amber, the third tone between
42
+ `description` and `error`).
43
+
44
+ ## v12 from 11.x
45
+
46
+ Two exports were removed: **`CommentComposer`** (compose comments with THE kit `Composer` —
47
+ attach via `actionsButton`/`files`; `CommentList` is unchanged) and **`DetailRow.info`** (field
48
+ annotations are explicit — use the new **`DetailRow.description`** under the value and
49
+ **`DetailRow.error`**, the `FormField` vocabulary; heading/card `info` ⓘ props are untouched).
50
+ Visual behavior changes: `SubsectionStack` is SPACE-ONLY (32px beat, no hairlines — titles carry
51
+ the grouping), `Section` heading gap 12→16, `DetailTable` row gap 6→8, `DetailRow` rows top-align
52
+ with label/control/trailing centered on the first control line, `Timeline` grew to 32px discs /
53
+ 16px icons / 40px minimum rows, `TextInputField` paints a WHITE surface on any background (it was
54
+ transparent — identical on the white canvas, visible on tinted panels), and the inline editors'
55
+ resting chip gained 6px vertical padding (identical for one-line values; multi-line resting
56
+ content now breathes).
57
+
58
+ ## v11 from ≤10.x
59
+
60
+ The type scale was re-slotted — the display sizes gained the missing `##` step: `xl` is now 22/24
61
+ (section title), `xxl` is 28/32 (page/record title — the OLD `xl` look), and the new `xxxl` is
62
+ 32/48 (hero numbers — the OLD `xxl` look). Migrate an app with two renames IN THIS ORDER: first
63
+ `size="xxl"` → `size="xxxl"`, then `size="xl"` → `size="xxl"` — every existing usage keeps its
64
+ exact look. Also: `SectionHeadingTitle` is now ALWAYS `##` (22/24), and disabled `Button`s fade
65
+ their label to the zinc-400 disabled ink on every variant EXCEPT `primary`, which keeps inverted
66
+ ink over its washed surface.
@@ -111,7 +111,10 @@ text, NOT all text on top of a flat step list. Content parts with no feed repres
111
111
  tool part's `input`/`output` are hidden in the row and **expand in place** on press — auto-built
112
112
  Input / Output `JsonPanel`s roll out under the row (the same disclosure pattern as "Thinking",
113
113
  never a popover). A tool in an `output-error` state flows like any other call — the dot goes
114
- amber and its `errorText` shows in the expanded Error panel.
114
+ amber and its `errorText` shows in the expanded Error panel. To render a tool's OUTPUT yourself
115
+ (an image preview, a table) pass **`renderToolOutput`** — the ONLY render escape hatch: it fires
116
+ when an output-carrying step is expanded, and a non-`undefined` return replaces the default
117
+ Output panel; `undefined` keeps it. Input + error rendering are untouched.
115
118
 
116
119
  **Activity grouping.** Consecutive tool parts fold into ONE group. While the agent is mid-tools
117
120
  the tail group is a SINGLE pulsing row whose label swaps in place as each call fires (no growing
@@ -123,18 +126,24 @@ no global terminal node.
123
126
 
124
127
  **Tool labels.** A tool part carries the RAW tool name (`update_records` from `tool-update_records`,
125
128
  or a `dynamic-tool`'s `toolName`), resolved via a built-in map of the platform record/document
126
- tools + an optional `labelForTool` override (localize THERE — the kit stays English); unknown
127
- names fall back to a prettified form. `stepsLabel` localizes the "{n} steps" suffix. The tool
128
- `state` collapses ai's lifecycle to running / done / error (an approval-pending state reads as
129
- running a read-only feed has no approval control). The whole-run `state` prop
130
- (`"streaming" | "done" | "error"`) defaults to `streaming` while any tool is running, else `done`.
129
+ tools + an optional **`labelForCall`** override (localize THERE — the kit stays English). It gets
130
+ the whole call `{ toolName, input, state }` (`AgentToolCall`) so a caller can phrase by what the
131
+ call targets or by whether it's awaiting; return `undefined` to fall back. Unknown names fall back
132
+ to a prettified form. `stepsLabel` localizes the "{n} steps" suffix. The tool `state` folds ai's
133
+ 7-state lifecycle to **running / awaiting / done / error**: `approval-requested` `awaiting` (the
134
+ call is parked on a human decision — an amber dot + an "awaiting" annotation, and it NEVER pulses,
135
+ because it isn't working); `approval-responded` → running; `output-denied` → error carrying the
136
+ denial reason. The whole-run `state` prop (`"streaming" | "done" | "error"`) defaults to `streaming`
137
+ while any tool is running, else `done` (an awaiting call is not running, so it settles).
131
138
 
132
139
  **Errors — two kinds, both surfaced, neither shouting.** An INTERMEDIARY tool failure rides in
133
140
  that tool's `output-error` part and flows like a normal call — the amber dot marks it (a settled
134
141
  group's header goes amber too), the reason waits in the expanded Error panel; real runs retry and
135
142
  move on, so the feed doesn't dramatize it. A BREAKING error that terminates the whole run lives
136
143
  OUTSIDE `parts` (chat persists it in the message's `errors`; `useAgentRun().error` carries it) —
137
- pass it as the `error` prop and it renders as a terminal danger row under the transcript.
144
+ pass it as the `error` prop and it renders as a terminal danger row under the transcript. Pass
145
+ **`onRetry`** alongside it and a Retry action (a secondary `Button`) rides under that row so the
146
+ operator can re-fire the run; omit it and the error row renders exactly as before.
138
147
 
139
148
  **In a bounded container, wrap it in `FollowScroll`.** An `AgentRun` streaming inside a dialog,
140
149
  drawer, or fixed-height panel grows BELOW the fold — a plain scroll container doesn't follow. Wrap
@@ -155,20 +164,33 @@ install; the imports are type-only (purity-enforced), so no `ai` runtime ever en
155
164
  **Reasoning only shows when the model produces it** — the declared model must support thinking
156
165
  (Sonnet / Opus, not Haiku); the renderer shows every reasoning part it's handed.
157
166
 
158
- **Limitation:** the collapsed reasoning row's "Thinking" label and the expanded detail's
159
- "Input"/"Output"/"Error" panel titles are currently English-only (not on the locale provider and
160
- not prop-overridable); `labelForTool`/`stepsLabel` localize everything else in the feed.
167
+ **Localization.** The feed's fixed chrome — the "Thinking" label, the "Input"/"Output"/"Error"
168
+ panel titles, the "awaiting" annotation, and the "Retry" action — resolves through the
169
+ `LoticsLocale` `agentRun` slice (translate once at the provider; a `vi` app gets them for free).
170
+ It is provider-localized, not per-instance prop-overridable; `labelForCall`/`stepsLabel` localize
171
+ the tool labels and the "{n} steps" suffix per call site.
172
+
173
+ **Answering an awaiting call — `ApprovalPrompt`.** The `awaiting` row inside `AgentRun` is
174
+ READ-ONLY — it only NAMES that a call is parked on a human decision. The surface that ANSWERS it
175
+ is `ApprovalPrompt` (`@lotics/ui/approval_prompt`): a warning icon + `message` (per-instance
176
+ override, else the `approvalPrompt` slice default) on a bordered card, with **Deny** (secondary)
177
+ + **Approve** (primary) right-aligned and an optional `children` slot for an input summary between
178
+ them — `onApprove`/`onDeny` fire the SDK's approve/deny (the ai-sdk approval vocabulary). Slot it
179
+ where the composer normally sits while the run is parked, so the operator reads the parked row in
180
+ the feed and answers it below; on decision the run resumes (`approval-responded` → running) or the
181
+ call settles denied (`output-denied` → error). Its strings resolve prop → the `approvalPrompt`
182
+ locale slice → English.
161
183
 
162
184
  ### `AgentProgress` — the floating pill
163
185
 
164
186
  On a canvas/composer app reach for `AgentProgress` (`@lotics/ui/agent_progress`) — `AgentRun`
165
187
  collapsed into a floating pill (an animated `WaveAvatar` + the current step's label) that EXPANDS
166
- on press to the full feed in a capped scroll panel. It takes the same `items`/`state`/
167
- `labelForTool`/`stepsLabel`, plus `label` (override the collapsed text defaults to the running
168
- step's label, else "Working…" / "Done" / "Stopped"; pass it to localize — note it's one static
169
- string, so it no longer tracks the running step) and `defaultExpanded`. Its pill matches
170
- `Composer`'s compact geometry, so the composer morphs into it while running and reveals again
171
- when done.
188
+ on press to the full feed in a capped scroll panel. It takes the same `parts`/`state`/`error`, and
189
+ forwards `labelForCall`/`renderToolOutput`/`onRetry`/`stepsLabel` to the expanded feed, plus `label`
190
+ (override the collapsed text — defaults to the running step's label, else "Working…" / "Done" /
191
+ "Stopped"; pass it to localize — note it's one static string, so it no longer tracks the running
192
+ step) and `defaultExpanded`. Its pill matches `Composer`'s compact geometry, so the composer morphs
193
+ into it while running and reveals again when done.
172
194
 
173
195
  **Limitation:** the pill's expand/collapse accessibility labels ("Show the agent's steps" /
174
196
  "Hide the agent's steps") are currently hardcoded English.
package/docs/catalog.md CHANGED
@@ -225,7 +225,11 @@ tile a pressable door — e.g. tap-to-preview — carrying an accessible button
225
225
  filename as its name, overridable per tile with `accessibilityLabel` when what the press
226
226
  DOES reads better than a raw filename; `selectedIds` for a selection overlay),
227
227
  `UploadingThumbnail` (the single in-flight tile FileGrid renders — reach for it only when
228
- hand-rolling a non-grid upload layout), `FileRow` (a horizontal file/document LINE
228
+ hand-rolling a non-grid upload layout), `FileDropTarget` (wrap ANY region a record's Files
229
+ section, a card, a panel — so a file dragged onto it lands; `paste` also takes Ctrl/Cmd+V
230
+ scoped to that region's focus; the region-wrapper next to `FileDropzone`'s dedicated well) +
231
+ `usePasteFiles` (the bare clipboard hook underneath, for a region-less surface) — the halves of
232
+ "files land without hunting for a dropzone", `FileRow` (a horizontal file/document LINE —
229
233
  badge-or-placeholder + name + meta + a composable `trailing` slot for a status badge /
230
234
  action / remove; `onPress` makes the whole row a pressable door, `trailing` stays an
231
235
  independently-pressable sibling; for checklists & readable lists), `FileBadge` (the two-tone
@@ -263,7 +267,14 @@ a plain `metric` prop remains for one-number findings. The children slot compose
263
267
  result — a compact `Table` for per-line detail (danger color on the offending cells),
264
268
  `ProgressBar` for consumption-toward-a-cliff (free time, credit), dot `Badge`s for a
265
269
  present/missing checklist, `Confidence` for judgment calls. Display-only — it informs the
266
- verdict the host records; `finding` locale slice).
270
+ verdict the host records; `finding` locale slice). `ApprovalPrompt` (the surface that ANSWERS
271
+ an agent's approval-gated action — the interactive counterpart to `AgentRun`'s read-only amber
272
+ `awaiting` row: a warning icon + message on a bordered card, Deny/Approve right-aligned, an
273
+ optional `children` input summary; slot it where the composer sits while the run is parked;
274
+ `approvalPrompt` locale slice), `MessageActions` (THE single shared per-message chrome bar for a
275
+ chat transcript — copy with owned copied-feedback, regenerate, edit, and the branch/version
276
+ pager as an internal part; each action renders only when its handler is given, so the assistant
277
+ bar and the user bar are one component with different handlers; `messageActions` locale slice).
267
278
 
268
279
  ---
269
280
 
@@ -898,9 +909,35 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
898
909
  - **`file_picker`** — `pickFiles({ accept?, multiple? }) → Promise<File[]>`: opens the
899
910
  browser file dialog imperatively — the trigger half behind every Add-file CTA; cancel
900
911
  resolves `[]` on modern engines.
901
- - **`file_dropzone`** — `FileDropzone`: the drag-drop capture well (`onFiles`, `accept`,
902
- `label`/`hint`/`dropLabel`, `height`); click falls back to a picker. `label`/`dropLabel`
903
- default from the locale's `fileDropzone` slice pass them only to override.
912
+ - **`file_dropzone`** — `FileDropzone`: the dedicated drag-drop capture well (`onFiles`,
913
+ `accept`, `label`/`hint`/`dropLabel`, `height`); click falls back to a picker, and while
914
+ mounted it ALSO takes a paste (Ctrl/Cmd+V) via its inner `FileDropTarget` `paste`, scoped to
915
+ the well's region. `label`/`dropLabel`/`hint` default from the locale's `fileDropzone` slice
916
+ (the hint now names the paste) — pass them only to override.
917
+ - **`file_drop_target`** — `FileDropTarget`: makes ANY region accept a file drag-drop —
918
+ `{ onFiles, accept?, multiple?, paste?, disabled?, children, style? }` wrapped around a
919
+ section, a card, a whole panel, so nobody hunts for a dropzone. While a drag hovers, the
920
+ region wears the kit's accent ring + wash (a box-shadow — nothing reflows); pass a FUNCTION
921
+ child (`(dragging) => …`) to draw the drag state yourself and the target paints nothing (how
922
+ `FileDropzone` keeps its well styling). **`paste`** ALSO takes Ctrl/Cmd+V, wiring
923
+ `usePasteFiles` scoped to THIS region's focus — so two peer file sections each win when
924
+ focus is in them, instead of the last-mounted one grabbing every paste. Native renders the
925
+ children unchanged.
926
+ - **`use_paste_files`** — `usePasteFiles({ onFiles, accept?, multiple?, enabled?, region? })`:
927
+ the clipboard half of intake — Ctrl/Cmd+V adds a copied file or a screenshot to the open
928
+ surface (most reach it via `FileDropTarget`'s `paste`; use the bare hook for a region-less
929
+ surface like a modal dialog). Scope it with `enabled` ("while this dialog/record is open"); a
930
+ paste carrying no acceptable file is left alone, so typing Ctrl+V in a text field still pastes
931
+ text. **Routing (`region`):** among all enabled sinks, the paste goes to the TOP-MOST whose
932
+ `region` contains `document.activeElement`; with none focused it falls back to the top of the
933
+ stack (the last-mounted surface — right for a dialog over a screen). A sink with no `region`
934
+ is a pure stack participant, so a single existing consumer is unchanged. Native is a no-op.
935
+ - **`file_intake`** — the shared, RN-free intake core + the contracts above:
936
+ `matchesAccept(file, accept)`, `filesFromTransfer(dataTransfer, { accept?, multiple? })`
937
+ (one extractor for BOTH `drop` and `paste`, with the clipboard-`items` fallback a pasted
938
+ screenshot needs), `selectPasteSink(entries)` (the pure focus-then-stack routing rule the
939
+ `.web` sink applies), and the types `FileIntakeFilter` / `FileTransferLike` / `RegionRef` /
940
+ `PasteSinkEntry` / `UsePasteFilesOptions` / `FileDropTargetProps`.
904
941
  - **`files_editor`** — `FilesEditor` — THE all-in-one attachment field: `FileGrid` + a
905
942
  toolbar (Upload primary · Select · Download all) that swaps into a batch SELECT mode
906
943
  (Select all · a Menu of Download/Share/Delete · Done; the per-tile ✕ is select-mode-only,
@@ -986,20 +1023,40 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
986
1023
  - **`composer`** — `Composer`: the adaptive command/chat composer — a compact pill that
987
1024
  expands for long text + attachments (`pills` + attachment slots, controlled or
988
1025
  uncontrolled `value`, `onSend`/`onStop`, `sendDisabled` override).
989
- - **`agent_run`** — `AgentRun` + `resolveToolMeta`: the live streaming work feed. Its prop is the
990
- agent message's ai-sdk `parts` (`UIMessagePart[]` — text, reasoning, and tool parts), fed straight
991
- from `useAgentRun().parts`; renders prose, a collapsed "Thinking" disclosure, and grouped tool
992
- steps whose I/O expands in place on press (no popover). A per-tool failure shows amber with the
993
- reason in its expanded Error panel; a run-level
994
- breaking `error` (outside `parts`) renders as a terminal danger row. Needs `ai` as an optional
995
- type-only peer. AI work ONLY
996
- deterministic work (template fill, an export) shows a `loading` state on its trigger and delivers
997
- at once; dressing it as an agent run is theater.
1026
+ - **`agent_run`** — `AgentRun` + `resolveToolMeta` + `AgentToolCall` + `AgentToolOutput` (the
1027
+ `renderToolOutput` call shape `{ toolName, input, output }`): the live streaming work feed.
1028
+ Its prop is the agent message's ai-sdk `parts` (`UIMessagePart[]` — text, reasoning, and tool
1029
+ parts), fed straight from `useAgentRun().parts`; renders prose, a collapsed "Thinking" disclosure,
1030
+ and grouped tool steps whose I/O expands in place on press (no popover). A tool `state` folds ai's
1031
+ 7-state lifecycle to running / **awaiting** / done / error an `awaiting` call (parked on a human
1032
+ approval) wears an amber dot + an "awaiting" annotation and never pulses. `labelForCall(call)`
1033
+ localizes/overrides a step's label from the whole call (`AgentToolCall` = `{ toolName, input,
1034
+ state }`); `renderToolOutput(call)` replaces a step's default Output panel (the one render escape
1035
+ hatch — input/error untouched). A per-tool failure shows amber with the reason in its expanded
1036
+ Error panel; a run-level breaking `error` (outside `parts`) renders as a terminal danger row, with
1037
+ an optional `onRetry` Button under it. Fixed chrome (Thinking / Input / Output / Error / awaiting /
1038
+ Retry) localizes via the `agentRun` locale slice. Needs `ai` as an optional type-only peer. AI
1039
+ work ONLY — deterministic work (template fill, an export) shows a `loading` state on its trigger
1040
+ and delivers at once; dressing it as an agent run is theater.
998
1041
  - **`agent_transform`** — internal (no component): the one place that folds an ai-sdk `UIMessage`'s
999
1042
  `parts` into the render timeline (`AgentUIPart`, `toSegments`, `lastRunningStep`), shared by
1000
1043
  `AgentRun` and `AgentProgress`. The sole file permitted to `import type` from `ai`.
1001
1044
  - **`agent_progress`** — `AgentProgress`: `AgentRun`'s compact, floating, expandable form (same
1002
1045
  `parts` prop) — a composer's working state.
1046
+ - **`approval_prompt`** — `ApprovalPrompt`: the surface that ANSWERS an agent's approval-gated
1047
+ action — the interactive counterpart to `AgentRun`'s read-only amber `awaiting` row. A warning
1048
+ icon + `message` (per-instance override; else the slice default) on a bordered card, `onDeny`
1049
+ (secondary) + `onApprove` (primary) right-aligned, an optional `children` slot for an input
1050
+ summary between them. Approve/deny is the ai-sdk approval vocabulary. Slot it where the composer
1051
+ sits while the run is parked; the `approvalPrompt` locale slice.
1052
+ - **`message_actions`** — `MessageActions` + `MessageActionsBranch`: THE single shared per-message
1053
+ action bar for a chat transcript — `onCopy` (the bar OWNS the copied feedback: the icon flips to
1054
+ a check + the tooltip to "Copied" for ~2s, timer cleared on unmount; the handler only does the
1055
+ clipboard write), `onRegenerate`, `onEdit`, and `branch` (the sibling-version pager, absorbed as
1056
+ an internal part — renders when `total > 1`). Each action renders ONLY when its handler is given,
1057
+ so an assistant bar (copy + regenerate) and a user bar (copy + edit) are the same component with
1058
+ a different handler set. The bar hugs its content — the parent aligns it (left under an assistant
1059
+ turn, right under a user bubble); the `messageActions` locale slice.
1003
1060
  - **`follow_scroll`** — `FollowScroll`: a scroll container that FOLLOWS its growing content —
1004
1061
  the chat message list's inverted mechanism packaged as a wrapper (single-cell inverted list),
1005
1062
  so the follow happens at LAYOUT level: each growth paints already pinned (no flash), scrolling
@@ -24,7 +24,7 @@ in [the templates](./templates.md) (`examples/tpl_*.tsx`).
24
24
  | ONE pick from a BIG registry (ports, projects…) | on a RECORD surface: **`InlineSelect searchable`** (search lives in the popover); on a form/attach surface: **`Combobox`** with `value` | an inline row stays an inline editor — a persistent search input breaks the surface's grammar |
25
25
  | a YES/NO on a record | **`CheckboxInput`** as the `DetailRow` value | a boolean needs no editor mode |
26
26
  | a STATUS with terminal outcomes | [**disposition**](#disposition--lifecycle-status-is-asymmetric-by-phase) (open → resolve → revise) | guides the decision |
27
- | FILES | [**attachment field**](#attachments--a-full-add--preview--delete-field) (dropzone + grid + gallery) | add / preview / delete |
27
+ | FILES | [**attachment field**](#attachments--a-full-add--preview--delete-field) (dropzone + grid + gallery; intake = CTA + drop-anywhere + paste) | add / preview / delete |
28
28
  | a state TRANSITION mid-flow | [**stage gate**](#stage-gates--tiered-by-weight) (popover / dialog by weight) | right-sized friction |
29
29
 
30
30
  **The right-input-per-field law.** A field gets the control its SHAPE wants, never a default
@@ -340,6 +340,56 @@ Popover/MenuButton).
340
340
 
341
341
  ## Attachments — a full add / preview / DELETE field
342
342
 
343
+ ### Intake — files land THREE ways, on every surface that holds files
344
+
345
+ A user should never hunt for a dropzone. Any surface that holds files carries all three
346
+ intake paths, all wired to ONE handler:
347
+
348
+ 1. **The section's own CTA** — `pickFiles` behind an Add-file button (the explicit path).
349
+ 2. **Drop anywhere on the region + paste** — wrap the region in
350
+ **`<FileDropTarget onFiles accept multiple paste disabled style>`**. It makes ARBITRARY
351
+ children a drop region: drag a file over any part of it and the region wears the accent ring
352
+ + wash (a box-shadow, so nothing reflows), drop and `onFiles` fires. Add **`paste`** and the
353
+ SAME region also takes Ctrl/Cmd+V (a screenshot or a copied file) — so the region carries
354
+ both non-explicit paths from one wrapper. This is the region-wrapper; `FileDropzone` stays
355
+ the dedicated empty-state WELL (and is itself a `FileDropTarget` with `paste` — a function
356
+ child, `(dragging) => …`, lets a child own the drag visual and the target paint nothing).
357
+ **The region can be a WHOLE SURFACE, not just a Files section.** Wrap a record's entire
358
+ section stack (or a register's whole page) in ONE `FileDropTarget` and a drop/paste ANYWHERE
359
+ on it routes to that surface's intake — the user never has to find the Files section. Give
360
+ `flex:1` (+ `minWidth:0` inside a flex row) so the wrapper is layout-transparent, and
361
+ `disabled` while an intake dialog is open so a paste can't start a second one. When the whole
362
+ surface is the target, the inner section drops its OWN `FileDropTarget` (redundant — one drop
363
+ would otherwise double it); its affordance line stays.
364
+ 3. **The bare paste hook** — **`usePasteFiles({ onFiles, accept, multiple, enabled, region })`**
365
+ is what `paste` wires internally; reach for it directly only for a region-LESS surface (a
366
+ modal dialog with no wrapper). Scope it with `enabled` ("while this record/dialog is open");
367
+ a paste with no acceptable file is left alone, so Ctrl+V in a text field still pastes text.
368
+
369
+ **Paste routing (focus, then stack).** When two file targets are enabled on the same layer,
370
+ the paste goes to the TOP-MOST one whose `region` contains focus (`document.activeElement`) —
371
+ so two peer file sections each win while the user is working in them, never "last-mounted
372
+ silently grabs everything." With none focused it falls back to the top of the stack (the
373
+ last-mounted surface — the right default for a dialog stacked over a screen). A `usePasteFiles`
374
+ with no `region` is a pure stack participant, so a single existing consumer is byte-for-byte
375
+ unchanged. `FileDropTarget paste` passes its own region, so you get focus routing for free.
376
+
377
+ Both drop and paste are web behaviors with native no-op siblings — wrap/call them
378
+ unconditionally on a shared screen. The two templates are the worked examples of the
379
+ WHOLE-SURFACE shape: `examples/tpl_record.tsx` wraps its ENTIRE section stack in one
380
+ `<FileDropTarget … paste disabled={aiOpen}>` → a drop/paste anywhere on the record opens the
381
+ `intakeFiles` fork dialog; `examples/tpl_item_list.tsx` wraps its whole register page → a
382
+ drop/paste anywhere opens the "Enter data" intake PRE-SEEDED with the dropped files (the
383
+ register also keeps a section-scoped `FileDropTarget` on the record drawer's Files section, so
384
+ both scopes are shown). Each is one `intakeFiles`/`addFiles`/seed handler behind the wrapper
385
+ plus the Add/Enter CTA.
386
+
387
+ **Make the two invisible paths discoverable.** A `FileDropTarget`-wrapped section shows no
388
+ dropzone, so drag + paste are invisible unless you SAY so. Every files section carries a muted
389
+ affordance line under its heading — **`<SectionHeadingTitle description="Drag, paste, or click
390
+ to add files">`** (both templates do this). A `FileDropzone`'s own hint already names the paste
391
+ (its default `fileDropzone.hint` is "or click, or paste (⌘V)").
392
+
343
393
  ### Default: `FilesEditor`
344
394
 
345
395
  **`<FilesEditor files onAdd onRemove>`** bundles the upload-aware grid + a toolbar below it
@@ -368,7 +418,8 @@ Reach for the lower-level pieces below only when you need custom chrome.
368
418
  ### The pieces — `FileDropzone`, `FileGrid`, `FileGalleryModal`, `FileRow`
369
419
 
370
420
  Capture with `<FileDropzone onFiles accept label hint dropLabel height>` (drag-over lights the
371
- accent; click falls back to a picker); display what landed with **`<FileGrid files uploads>`**
421
+ accent; click falls back to a picker; a mounted zone is also the paste sink, so Ctrl/Cmd+V
422
+ works inside an open upload dialog); display what landed with **`<FileGrid files uploads>`**
372
423
  ABOVE the dropzone (existing files are the content; the dropzone sinks to the bottom as the "add
373
424
  more" affordance — only the empty state leads with it).
374
425
 
package/docs/templates.md CHANGED
@@ -193,7 +193,9 @@ screens — register, per-row action, gated selection, and act-on-many in one. T
193
193
  drawer header swaps to a back button + the pushed id and the ◀ ▶ sequencer hides; back
194
194
  pops with scroll preserved.
195
195
  - **Files** with its own Add CTA — `FileRows` CRUD via `pickFiles`, plus an EXPECTED
196
- document as a ghost `FileRow placeholder` with a Request action.
196
+ document as a ghost `FileRow placeholder` with a Request action. The section is wrapped in
197
+ a `<FileDropTarget … paste>`, so a drag anywhere onto it or a Ctrl/Cmd+V lands on the same
198
+ `addFiles` handler; a `SectionHeadingTitle description` names the three ways in.
197
199
  - **Payment** — the `Ledger` (charge/received groups with sums, peekable fee rows,
198
200
  `LedgerTotal`) + a Record-payment popover that appends a receipt.
199
201
  - **Activity** — the CRM touch-log shape: optional outcome pills (`ChipGroup`,
@@ -205,7 +207,11 @@ screens — register, per-row action, gated selection, and act-on-many in one. T
205
207
  whose press opens the shared `FileGalleryModal`.
206
208
  - A closing `DangerZone`.
207
209
  - **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
210
+ dialog. The whole register page is ALSO a `<FileDropTarget paste>` (the whole-surface intake
211
+ standard): a file dropped or Ctrl/Cmd+V pasted ANYWHERE on the register opens this same dialog
212
+ PRE-SEEDED with the dropped files (list-first — the paid "Read with AI" stays a deliberate
213
+ button). The dialog is CONTROLLED (`open`/`onOpenChange` + `seedNames`) so both the button and
214
+ the top-level drop drive it. The dialog itself: a `FileDropzone` hero (drop the customer's files) → a short ANALYZE stream
209
215
  (`AgentRun` in a `FollowScroll`) reads them → `ClarifyWizard` asks the ambiguities the
210
216
  analysis surfaced (described options, one custom-answer slot) → the IMPORT stream drafts the
211
217
  records → `ChangeRecord` cards to Keep/Drop (fee editable in place) + one outcome-named
@@ -280,6 +286,13 @@ billing, and quick-capture templates. Top → bottom:
280
286
  - **Documents** — the Agents "Document desk" pattern (this template is its worked example —
281
287
  see the Agents chapter below): the register `Table` (search · Add files) whose selection
282
288
  feeds the `FloatingActionBar` → ONE "Use AI" fork (extract / cross-check / edit-with-AI).
289
+ The template is the worked example of the THREE-way, WHOLE-SURFACE file intake: the Add-files
290
+ CTA plus a `<FileDropTarget … paste disabled={aiOpen}>` wrapping the ENTIRE section stack (not
291
+ just Documents) — a drag or Ctrl/Cmd+V ANYWHERE on the record (`disabled` while the intake
292
+ dialog runs so a paste can't start a second one) — all landing in ONE `intakeFiles` fork
293
+ dialog. The Documents section therefore carries NO drop target of its own (the whole-record
294
+ one covers it), only its Add-files CTA and a muted `SectionHeadingTitle description` ("Drag,
295
+ paste, or click to add files") that keeps the otherwise-invisible paths discoverable.
283
296
  Every row's leading visual is a `FileThumbnail` in ONE square 32px slot — an image file
284
297
  fills it as a real thumbnail, a document centers its badge in it — never a bare `FileBadge`
285
298
  (mixed footprints misalign the identity column). The desk holds what ARRIVES; generation