@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 +12 -28
- package/MIGRATION.md +66 -0
- package/docs/ai_patterns.md +38 -16
- package/docs/catalog.md +71 -14
- package/docs/data_entry.md +53 -2
- package/docs/templates.md +15 -2
- package/examples/tpl_item_list.tsx +146 -48
- package/examples/tpl_record.tsx +37 -12
- package/package.json +13 -1
- package/src/agent_progress.tsx +11 -7
- package/src/agent_run.tsx +114 -43
- package/src/agent_transform.test.ts +96 -0
- package/src/agent_transform.ts +17 -5
- package/src/approval_prompt.tsx +70 -0
- package/src/dom_region.ts +14 -0
- package/src/dom_region.web.ts +13 -0
- package/src/file_drop_target.tsx +16 -0
- package/src/file_drop_target.web.tsx +118 -0
- package/src/file_dropzone.tsx +51 -96
- package/src/file_intake.test.ts +124 -0
- package/src/file_intake.ts +148 -0
- package/src/locale.tsx +21 -8
- package/src/message_actions.tsx +111 -0
- package/src/use_paste_files.ts +14 -0
- package/src/use_paste_files.web.ts +101 -0
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.
|
package/docs/ai_patterns.md
CHANGED
|
@@ -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
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
**
|
|
159
|
-
|
|
160
|
-
|
|
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 `
|
|
167
|
-
`
|
|
168
|
-
step's label, else "Working…" / "Done" /
|
|
169
|
-
string, so it no longer tracks the running
|
|
170
|
-
`Composer`'s compact geometry, so the composer morphs
|
|
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), `
|
|
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`,
|
|
902
|
-
`label`/`hint`/`dropLabel`, `height`); click falls back to a picker
|
|
903
|
-
|
|
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
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
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
|
package/docs/data_entry.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|