@lotics/ui 7.12.1 → 7.13.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 +25 -15
- package/package.json +1 -1
- package/src/agent_run.tsx +94 -29
- package/src/files_editor.tsx +13 -3
- package/src/table.tsx +24 -4
package/AGENTS.md
CHANGED
|
@@ -302,7 +302,9 @@ marked + disabled), not a generic text menu. A composition (Badge + Button + Pop
|
|
|
302
302
|
**Default: `<FilesEditor files onAdd onRemove>`** — it bundles the grid + a toolbar (Upload · Select ·
|
|
303
303
|
Download all) + a batch SELECT mode + the gallery + confirmed-remove; the host only owns `files` and
|
|
304
304
|
wires `onAdd` (picked → its upload) / `onRemove`. The destructive per-tile ✕ shows only in SELECT mode
|
|
305
|
-
(the default view is a clean preview — no stray-tap deletes);
|
|
305
|
+
(the default view is a clean preview — no stray-tap deletes); pass `selectTileRemove={false}` to drop
|
|
306
|
+
that ✕ entirely so select mode deletes ONLY via Select → Menu → Delete (the batch gating flow — how the
|
|
307
|
+
cell popover's `cell_files_editor` does it). The built-in gallery is Download +
|
|
306
308
|
inline preview (no "open in new tab" — it's redundant once everything previews inline). In a
|
|
307
309
|
height-bounded container (a popover/drawer) pass `gridMaxHeight` so the grid SCROLLS and the toolbar
|
|
308
310
|
pins below it; omit it in free-flow layouts (a form field) where the grid grows. Reach for the
|
|
@@ -413,20 +415,26 @@ Compose the surfaces as a loop, and reach for the right one by job:
|
|
|
413
415
|
way; while running the host swaps `Composer` for `AgentProgress` (same pill geometry, so it reads as
|
|
414
416
|
a morph).
|
|
415
417
|
- **Show the work** — `AgentRun`: a live feed of the agent's work as a TIMELINE. The prop is an
|
|
416
|
-
ORDERED `items` array — `{type:"text"}` prose (rendered as `Markdown`, a trailing ▍ while live)
|
|
417
|
-
|
|
418
|
-
text
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
418
|
+
ORDERED `items` array — `{type:"text"}` prose (rendered as `Markdown`, a trailing ▍ while live),
|
|
419
|
+
`{type:"reasoning"}` thinking, and `{type:"step"}` tool/step calls, in the order they happened (a
|
|
420
|
+
real run is think → text → a burst of calls → more text, NOT all text on top of a flat step list).
|
|
421
|
+
**Progressive disclosure** — the feed shows the label + state; the detail is revealed on demand:
|
|
422
|
+
`reasoning` renders COLLAPSED (a muted "Thinking" row, press to reveal the Markdown), and a `step`'s
|
|
423
|
+
`input`/`output` are hidden in the row but open in a press-to-reveal **peek** (auto-built Input /
|
|
424
|
+
Output `JsonPanel`s; an `error` status + `errorText` paint the row red and show the reason there).
|
|
425
|
+
Pass an explicit `peek` node to render that reveal yourself (it overrides the auto panel); `detail`
|
|
426
|
+
is OPTIONAL — a short human summary (a count), never invented prose or raw I/O (that's `input`/`output`).
|
|
427
|
+
Consecutive `step` items fold into ONE activity group: while the agent is mid-tools the tail group is
|
|
428
|
+
a SINGLE pulsing row whose label swaps in place as each call fires (no growing stack of dots); once
|
|
429
|
+
prose resumes the group settles into a persistent "{final action} · {n} steps" HEADER (a `complete`
|
|
430
|
+
terminal dot — distinct from the filled `done` step dots) that STAYS PUT and rolls the calls out
|
|
431
|
+
BELOW it on press. The run ALWAYS ends on the agent's text (no global terminal node). A `step` with
|
|
425
432
|
`kind:"tool"` carries the RAW tool name, resolved via a built-in map + an optional `labelForTool`
|
|
426
433
|
override (localize THERE — the kit stays English); `stepsLabel` localizes the "{n} steps" suffix.
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
434
|
+
Fed natively by `@lotics/app-sdk` `useAgentRun().items` (reasoning + per-tool I/O + state come for
|
|
435
|
+
free — no hand-assembly). Transparent work, NEVER a bare spinner. On a canvas/composer app reach for
|
|
436
|
+
`AgentProgress` — `AgentRun` collapsed into a floating pill (avatar + current step) that EXPANDS on
|
|
437
|
+
press; the composer morphs into it while running, and reveals again when done.
|
|
430
438
|
- **Review before apply — ONE card + the batch engine, the BODY is compositional.** Every "the agent
|
|
431
439
|
proposes → the human accepts / edits / dismisses → nothing auto-applies" surface wears the same
|
|
432
440
|
`reviewCardStyle` + an optional `Confidence`. There are two primitives:
|
|
@@ -669,7 +677,9 @@ was only ever about the review surfaces' sparkles/severity glyphs, not functiona
|
|
|
669
677
|
+ cells align. The Divider BETWEEN rows is the resting separation. The COLUMNAR register is
|
|
670
678
|
`Table`/`TableRow`/`TableCell` (columns once → header + widths can't drift; ONE hairline under the
|
|
671
679
|
header; the rows below it Divider-separated; a sortable column shows its `SortHeader` sort glyph
|
|
672
|
-
ALWAYS so it reads as sortable).
|
|
680
|
+
ALWAYS so it reads as sortable). A `TableRow` with no `onPress` renders a STATIC read-only row (no
|
|
681
|
+
hover wash / pointer cursor) so `Table` also serves read-only tabular data (a fee breakdown, a spec
|
|
682
|
+
sheet), not just interactive registers. Make it SELECTABLE with the `Table` `leading` gutter + `selectAll`
|
|
673
683
|
slot — a `CheckboxInput` per `TableRow` (`leading`) + a select-all in the header band, the ticked
|
|
674
684
|
rows `marked`, paired with a `FloatingActionBar`; the selection state is the **`useSelection()`**
|
|
675
685
|
hook (`selected`/`count`/`toggle`/`setAll`/`allSelected`/`indeterminate`/`clear` — gating which
|
|
@@ -878,7 +888,7 @@ CalloutText · CalloutActions) · kpi_card · kpi_strip · summary_line (Summary
|
|
|
878
888
|
bar_chart · line_chart · pie_chart · ring_gauge · progress_bar · stacked_progress_bar · breakdown ·
|
|
879
889
|
funnel (Funnel — conversion funnel: narrowing bars + the step rate as a bold aligned headline row, count below; `orientation` vertical|horizontal, `onSelect`/`selectedKey` press-to-drill; the subset/drop-off sibling of stacked_progress_bar's whole-split) ·
|
|
880
890
|
status_grid (StatusGrid + StatusLegend) · heatmap (density cross-tab — colour-only, no numbers) · matrix (Matrix — the PIVOT cross-tab: band-compound `Matrix` root + `Matrix.Header` (corner + axis labels) + `Matrix.Grid` (`display` number|heat|both — the cells, pressable, the value IN the cell) + `Matrix.Totals` (row + column + grand) + `Matrix.Legend`; the data layer `matrixTotals`/`MatrixAxisItem`/`MatrixCellRef` import from `@lotics/ui/matrix_totals`. Pick over `Heatmap` when the NUMBER and totals matter, not just where it clusters) · legend_item · remainder_meter · allocation_row ·
|
|
881
|
-
scan_field · file_dropzone · files_editor (FilesEditor — THE all-in-one attachment field: FileGrid + a toolbar (Upload primary · Select · Download all) that swaps into a batch SELECT mode (Select all · a Menu of Download/Share/Delete · Done; the per-tile ✕ is select-mode-only, never in the default view) + built-in gallery (Download + inline preview; no "open in new tab") + Alert-confirmed remove; host wires `files` + `onAdd`/`onRemove` (+ optional `uploads`, `onShareSelected`, `readOnly`, `labels`, `galleryLabels`, `gridMaxHeight` — cap the grid height so it scrolls and the toolbar pins, for a popover/drawer); mirrors the frontend cell_files_editor. Use FileGrid/FileRows bare only when you own the chrome) · file_grid (FileGrid — the upload-aware grid: completed files + a live upload queue in one surface; FileUpload/PendingUpload types; the add-files default) · uploading_thumbnail (UploadingThumbnail — the single in-flight upload tile FileGrid renders; reach for it only when hand-rolling a non-grid upload layout) · file_thumbnail · file_thumbnail_grid · file_row · file_rows (FileRows — batteries-included file list: row press → built-in gallery + a ⋯ Download/Open-external/Remove menu; composes FileRow + ActionMenu + FileGalleryModal) · file_preview (FilePreview — the universal inline preview: image/PDF/video/audio + Word via `@lotics/docx` + Excel/CSV via `@lotics/xlsx`; the heavy engines are LAZY (dynamic-imported, ~free until a doc of that type is opened) so custom-code APPS can use it too — they just add `@lotics/docx` + `@lotics/xlsx` to their deps. Renders to canvas/DOM, never a nested iframe — works in the sandboxed app iframe) ·
|
|
891
|
+
scan_field · file_dropzone · files_editor (FilesEditor — THE all-in-one attachment field: FileGrid + a toolbar (Upload primary · Select · Download all) that swaps into a batch SELECT mode (Select all · a Menu of Download/Share/Delete · Done; the per-tile ✕ is select-mode-only, never in the default view; `selectTileRemove={false}` drops even that so delete is menu-only) + built-in gallery (Download + inline preview; no "open in new tab") + Alert-confirmed remove; host wires `files` + `onAdd`/`onRemove` (+ optional `uploads`, `onShareSelected`, `readOnly`, `selectTileRemove`, `labels`, `galleryLabels`, `gridMaxHeight` — cap the grid height so it scrolls and the toolbar pins, for a popover/drawer); mirrors the frontend cell_files_editor. Use FileGrid/FileRows bare only when you own the chrome) · file_grid (FileGrid — the upload-aware grid: completed files + a live upload queue in one surface; FileUpload/PendingUpload types; the add-files default) · uploading_thumbnail (UploadingThumbnail — the single in-flight upload tile FileGrid renders; reach for it only when hand-rolling a non-grid upload layout) · file_thumbnail · file_thumbnail_grid · file_row · file_rows (FileRows — batteries-included file list: row press → built-in gallery + a ⋯ Download/Open-external/Remove menu; composes FileRow + ActionMenu + FileGalleryModal) · file_preview (FilePreview — the universal inline preview: image/PDF/video/audio + Word via `@lotics/docx` + Excel/CSV via `@lotics/xlsx`; the heavy engines are LAZY (dynamic-imported, ~free until a doc of that type is opened) so custom-code APPS can use it too — they just add `@lotics/docx` + `@lotics/xlsx` to their deps. Renders to canvas/DOM, never a nested iframe — works in the sandboxed app iframe) ·
|
|
882
892
|
file_gallery_modal · image_gallery · use_selection (useSelection — always-on multi-select state for a register/list: the `selected` Set + `toggle`/`setAll`/`allSelected`/`indeterminate`/`count`/`clear`; selectability gating stays with the caller. The checkbox-always-visible counterpart to `use_selection_mode`) · use_selection_mode · share_or_download · rotate_image · avatar · skeleton · activity_indicator · loading · divider ·
|
|
883
893
|
spacer · stack · section_card · page_header · page_content · calendar (calendar/index.ts) · gantt ·
|
|
884
894
|
comments_thread · agent_run (live streaming work feed) · agent_progress (its compact floating
|
package/package.json
CHANGED
package/src/agent_run.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import { colors } from "./colors";
|
|
|
4
4
|
import { Text } from "./text";
|
|
5
5
|
import { Icon, type IconName } from "./icon";
|
|
6
6
|
import { Markdown } from "./markdown";
|
|
7
|
+
import { JsonPanel, stringifyData } from "./json_panel";
|
|
7
8
|
import { Peek } from "./peek";
|
|
8
9
|
import { PressableHighlight } from "./pressable_highlight";
|
|
9
10
|
import { Marker, type StepStatus } from "./stepper";
|
|
@@ -18,25 +19,35 @@ export interface AgentRunStep {
|
|
|
18
19
|
* it to a human label via the default map / `labelForTool`. A `step` carries
|
|
19
20
|
* an already-human label. */
|
|
20
21
|
label: string;
|
|
21
|
-
/** A note under the label — should be REAL tool I/O (an arg, a count, a result
|
|
22
|
-
* summary), never invented prose. Muted. */
|
|
23
|
-
detail?: string;
|
|
24
22
|
status: AgentStepStatus;
|
|
25
23
|
kind?: "step" | "tool";
|
|
26
|
-
/**
|
|
27
|
-
*
|
|
24
|
+
/** The tool call's input (arguments) and result. When present (and no explicit
|
|
25
|
+
* `peek`), the feed reveals them ON DEMAND — a press-to-open peek with an Input /
|
|
26
|
+
* Output panel — instead of cluttering the row; the row itself shows only the
|
|
27
|
+
* label + state. */
|
|
28
|
+
input?: unknown;
|
|
29
|
+
output?: unknown;
|
|
30
|
+
/** The tool failure message (shown in the peek when `status` is `"error"`). */
|
|
31
|
+
errorText?: string;
|
|
32
|
+
/** A note under the label — a short, human summary (a count, a result summary),
|
|
33
|
+
* never invented prose. Muted. For raw tool I/O prefer `input`/`output`. */
|
|
34
|
+
detail?: string;
|
|
35
|
+
/** Custom content for the press-to-open peek. Overrides the auto-built Input /
|
|
36
|
+
* Output panel — pass this to render the reveal yourself. */
|
|
28
37
|
peek?: ReactNode;
|
|
29
38
|
}
|
|
30
39
|
|
|
31
40
|
/**
|
|
32
|
-
* The ordered transcript of a run, in the order it happened:
|
|
33
|
-
*
|
|
34
|
-
* (`step`). A real run is a timeline — text, a
|
|
35
|
-
*
|
|
36
|
-
* step list. Consecutive `step` items collapse into ONE activity
|
|
41
|
+
* The ordered transcript of a run, in the order it happened: the agent's answer
|
|
42
|
+
* prose (`text`), its thinking (`reasoning`, shown collapsed / revealed on demand),
|
|
43
|
+
* and the tools it runs between prose (`step`). A real run is a timeline — text, a
|
|
44
|
+
* burst of tool calls, more text — so the feed is an ordered array, NOT a text
|
|
45
|
+
* block over a flat step list. Consecutive `step` items collapse into ONE activity
|
|
46
|
+
* group; a `step`'s `input`/`output` reveal in a peek on press.
|
|
37
47
|
*/
|
|
38
48
|
export type AgentRunItem =
|
|
39
49
|
| { type: "text"; id: string; text: string }
|
|
50
|
+
| { type: "reasoning"; id: string; text: string }
|
|
40
51
|
| ({ type: "step" } & AgentRunStep);
|
|
41
52
|
|
|
42
53
|
export interface AgentRunProps {
|
|
@@ -101,18 +112,19 @@ function stepLabel(s: AgentRunStep, labelForTool?: (toolName: string) => string
|
|
|
101
112
|
return s.kind === "tool" ? resolveToolMeta(s.label, labelForTool).label : s.label;
|
|
102
113
|
}
|
|
103
114
|
|
|
104
|
-
// A run is a timeline of segments: prose, then a group of consecutive
|
|
105
|
-
// calls, then more prose. Folding the flat item list into segments is what
|
|
106
|
-
// the feed interleave instead of forcing all text to the top.
|
|
115
|
+
// A run is a timeline of segments: prose, thinking, then a group of consecutive
|
|
116
|
+
// tool calls, then more prose. Folding the flat item list into segments is what
|
|
117
|
+
// lets the feed interleave instead of forcing all text to the top.
|
|
107
118
|
type Segment =
|
|
108
119
|
| { kind: "text"; id: string; text: string }
|
|
120
|
+
| { kind: "reasoning"; id: string; text: string }
|
|
109
121
|
| { kind: "group"; id: string; steps: AgentRunStep[] };
|
|
110
122
|
|
|
111
123
|
function toSegments(items: AgentRunItem[]): Segment[] {
|
|
112
124
|
const segments: Segment[] = [];
|
|
113
125
|
for (const item of items) {
|
|
114
|
-
if (item.type === "text") {
|
|
115
|
-
segments.push({ kind:
|
|
126
|
+
if (item.type === "text" || item.type === "reasoning") {
|
|
127
|
+
segments.push({ kind: item.type, id: item.id, text: item.text });
|
|
116
128
|
continue;
|
|
117
129
|
}
|
|
118
130
|
const { type: _t, ...step } = item;
|
|
@@ -158,6 +170,9 @@ export function AgentRun(props: AgentRunProps) {
|
|
|
158
170
|
</View>
|
|
159
171
|
);
|
|
160
172
|
}
|
|
173
|
+
if (seg.kind === "reasoning") {
|
|
174
|
+
return <ReasoningDisclosure key={seg.id} text={seg.text} streaming={streaming && i === lastIndex} expanded={!!expanded[seg.id]} onToggle={() => toggle(seg.id)} />;
|
|
175
|
+
}
|
|
161
176
|
return (
|
|
162
177
|
<ToolGroup
|
|
163
178
|
key={seg.id}
|
|
@@ -218,6 +233,66 @@ function StepBody({ label, detail, status }: { label: string; detail?: string; s
|
|
|
218
233
|
);
|
|
219
234
|
}
|
|
220
235
|
|
|
236
|
+
// The press-to-open reveal for a tool step: the caller's `peek` wins; else Input /
|
|
237
|
+
// Output / Error panels auto-built from the carried I/O; else nothing (no reveal).
|
|
238
|
+
// Raw I/O stays OUT of the row — only shown here, on demand.
|
|
239
|
+
function stepPeek(s: AgentRunStep): ReactNode {
|
|
240
|
+
if (s.peek) return s.peek;
|
|
241
|
+
const hasInput = s.input !== undefined;
|
|
242
|
+
const hasOutput = s.output !== undefined;
|
|
243
|
+
if (!hasInput && !hasOutput && !s.errorText) return null;
|
|
244
|
+
return (
|
|
245
|
+
<View style={{ gap: 8 }}>
|
|
246
|
+
{hasInput ? <JsonPanel title="Input" value={stringifyData(s.input)} /> : null}
|
|
247
|
+
{s.errorText ? (
|
|
248
|
+
<JsonPanel title="Error" value={s.errorText} />
|
|
249
|
+
) : hasOutput ? (
|
|
250
|
+
<JsonPanel title="Output" value={stringifyData(s.output)} />
|
|
251
|
+
) : null}
|
|
252
|
+
</View>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// A step's body, wrapped in a Peek when there's something to reveal (I/O or a
|
|
257
|
+
// caller peek), plain otherwise.
|
|
258
|
+
function StepContent({ s, label }: { s: AgentRunStep; label: string }) {
|
|
259
|
+
const peek = stepPeek(s);
|
|
260
|
+
const body = <StepBody label={label} detail={s.detail} status={s.status} />;
|
|
261
|
+
return peek ? (
|
|
262
|
+
<Peek accessibilityLabel={label} content={peek}>
|
|
263
|
+
{body}
|
|
264
|
+
</Peek>
|
|
265
|
+
) : (
|
|
266
|
+
body
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Thinking — a muted, COLLAPSED "Thinking" disclosure (revealed on press), so the
|
|
271
|
+
// agent's reasoning never crowds the answer. Pulses while it's still streaming.
|
|
272
|
+
function ReasoningDisclosure(props: { text: string; streaming?: boolean; expanded: boolean; onToggle: () => void }) {
|
|
273
|
+
const { text, streaming, expanded, onToggle } = props;
|
|
274
|
+
return (
|
|
275
|
+
<View style={styles.group}>
|
|
276
|
+
<PressableHighlight focusRing onPress={onToggle} accessibilityRole="button" accessibilityLabel="Thinking" style={styles.row}>
|
|
277
|
+
<View style={styles.dotCol}>
|
|
278
|
+
<Marker status={streaming ? "current" : "done"} color={colors.zinc[400]} live={!!streaming} />
|
|
279
|
+
</View>
|
|
280
|
+
<View style={styles.rowBody}>
|
|
281
|
+
<Text size="sm" color="muted">
|
|
282
|
+
{streaming ? "Thinking…" : "Thinking"}
|
|
283
|
+
</Text>
|
|
284
|
+
</View>
|
|
285
|
+
{chevron(expanded ? "up" : "down")}
|
|
286
|
+
</PressableHighlight>
|
|
287
|
+
{expanded ? (
|
|
288
|
+
<View style={styles.reasoning}>
|
|
289
|
+
<Markdown>{text}</Markdown>
|
|
290
|
+
</View>
|
|
291
|
+
) : null}
|
|
292
|
+
</View>
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
221
296
|
function SummaryLabel({ label, count, stepsLabel }: { label: string; count: number; stepsLabel: (n: number) => string }) {
|
|
222
297
|
return (
|
|
223
298
|
<Text size="sm" weight="medium" numberOfLines={1}>
|
|
@@ -269,13 +344,7 @@ function ToolGroup(props: {
|
|
|
269
344
|
return (
|
|
270
345
|
<View style={styles.group}>
|
|
271
346
|
<ActivityRow markerStatus={final.status === "error" ? "warning" : "done"}>
|
|
272
|
-
{final
|
|
273
|
-
<Peek accessibilityLabel={resolve(final)} content={final.peek}>
|
|
274
|
-
<StepBody label={resolve(final)} detail={final.detail} status={final.status} />
|
|
275
|
-
</Peek>
|
|
276
|
-
) : (
|
|
277
|
-
<StepBody label={resolve(final)} detail={final.detail} status={final.status} />
|
|
278
|
-
)}
|
|
347
|
+
<StepContent s={final} label={resolve(final)} />
|
|
279
348
|
</ActivityRow>
|
|
280
349
|
</View>
|
|
281
350
|
);
|
|
@@ -300,13 +369,7 @@ function ToolGroup(props: {
|
|
|
300
369
|
? steps.map((s) => (
|
|
301
370
|
<AnimationFadeIn key={s.id} translateY={4}>
|
|
302
371
|
<ActivityRow markerStatus={s.status === "error" ? "warning" : "done"}>
|
|
303
|
-
{s
|
|
304
|
-
<Peek accessibilityLabel={resolve(s)} content={s.peek}>
|
|
305
|
-
<StepBody label={resolve(s)} detail={s.detail} status={s.status} />
|
|
306
|
-
</Peek>
|
|
307
|
-
) : (
|
|
308
|
-
<StepBody label={resolve(s)} detail={s.detail} status={s.status} />
|
|
309
|
-
)}
|
|
372
|
+
<StepContent s={s} label={resolve(s)} />
|
|
310
373
|
</ActivityRow>
|
|
311
374
|
</AnimationFadeIn>
|
|
312
375
|
))
|
|
@@ -328,4 +391,6 @@ const styles = StyleSheet.create({
|
|
|
328
391
|
},
|
|
329
392
|
dotCol: { width: 18, alignItems: "center" },
|
|
330
393
|
rowBody: { flex: 1 },
|
|
394
|
+
// Reasoning body aligns under the "Thinking" label (past the dot column + gap).
|
|
395
|
+
reasoning: { paddingLeft: 36, paddingRight: 8, paddingBottom: 4 },
|
|
331
396
|
});
|
package/src/files_editor.tsx
CHANGED
|
@@ -64,6 +64,12 @@ export interface FilesEditorProps {
|
|
|
64
64
|
onDownloadZipSelected?: (files: DisplayFile[]) => void;
|
|
65
65
|
/** Remove a completed file. Omit (or `readOnly`) for a view-only surface. */
|
|
66
66
|
onRemove?: (id: string) => void;
|
|
67
|
+
/** Show the per-tile ✕ during SELECT mode (default `true`). The default
|
|
68
|
+
* (non-select) view NEVER shows a ✕ regardless — this governs select mode
|
|
69
|
+
* only. Set `false` when batch Select → Menu → Delete is the sole delete
|
|
70
|
+
* path (e.g. a cell popover), so a tile in select mode toggles selection and
|
|
71
|
+
* nothing else. */
|
|
72
|
+
selectTileRemove?: boolean;
|
|
67
73
|
/** Cancel an in-flight upload / retry a failed one (when using `uploads`). */
|
|
68
74
|
onUploadRemove?: (id: string) => void;
|
|
69
75
|
onRetry?: (id: string) => void;
|
|
@@ -112,6 +118,7 @@ export function FilesEditor(props: FilesEditorProps) {
|
|
|
112
118
|
onUpload,
|
|
113
119
|
onDownloadZipSelected,
|
|
114
120
|
onRemove,
|
|
121
|
+
selectTileRemove = true,
|
|
115
122
|
onUploadRemove,
|
|
116
123
|
onRetry,
|
|
117
124
|
onRetryAll,
|
|
@@ -163,9 +170,12 @@ export function FilesEditor(props: FilesEditorProps) {
|
|
|
163
170
|
selectedIds={selectMode ? selected : undefined}
|
|
164
171
|
onToggleSelect={selectMode ? toggle : undefined}
|
|
165
172
|
onFilePress={selectMode ? undefined : (f) => setGalleryIndex(files.findIndex((x) => x.id === f.id))}
|
|
166
|
-
// The per-tile ✕ is a SELECT-mode affordance
|
|
167
|
-
// preview surface so a stray tap can't drop a
|
|
168
|
-
|
|
173
|
+
// The per-tile ✕ is a SELECT-mode affordance (opt-out via `selectTileRemove`)
|
|
174
|
+
// — the default view stays a clean preview surface so a stray tap can't drop a
|
|
175
|
+
// file, and a `selectTileRemove={false}` surface deletes only via the menu.
|
|
176
|
+
onDisplayRemove={
|
|
177
|
+
!readOnly && selectMode && selectTileRemove && onRemove ? (id) => confirmRemove(() => onRemove(id)) : undefined
|
|
178
|
+
}
|
|
169
179
|
onUploadRemove={onUploadRemove}
|
|
170
180
|
onRetry={onRetry}
|
|
171
181
|
onRetryAll={onRetryAll}
|
package/src/table.tsx
CHANGED
|
@@ -144,11 +144,25 @@ export function TableRow(props: TableRowProps) {
|
|
|
144
144
|
);
|
|
145
145
|
const { focusVisible, focusProps } = useFocusRing();
|
|
146
146
|
|
|
147
|
+
// Read-only row (no `onPress`): a static, non-interactive row — no hover wash,
|
|
148
|
+
// no pointer cursor, no focusable door — so a `Table` can present read-only
|
|
149
|
+
// tabular data (a fee breakdown, a spec sheet) without implying the rows open
|
|
150
|
+
// something. An interactive row (with `onPress`) keeps the full register surface.
|
|
151
|
+
if (!onPress) {
|
|
152
|
+
return (
|
|
153
|
+
<View style={styles.staticRow}>
|
|
154
|
+
{ctx.leading > 0 ? <View style={{ width: ctx.leading }}>{leading}</View> : null}
|
|
155
|
+
<View style={[styles.door, { minHeight }]}>{cells}</View>
|
|
156
|
+
{ctx.trailing > 0 ? <View style={{ width: ctx.trailing, alignItems: "flex-end" }}>{trailing}</View> : null}
|
|
157
|
+
</View>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
147
161
|
return (
|
|
148
|
-
<PressableRow onPress={onPress
|
|
162
|
+
<PressableRow onPress={onPress} selected={selected} marked={marked} style={styles.row}>
|
|
149
163
|
{ctx.leading > 0 ? <View style={{ width: ctx.leading }}>{leading}</View> : null}
|
|
150
164
|
<Pressable
|
|
151
|
-
accessibilityRole=
|
|
165
|
+
accessibilityRole="button"
|
|
152
166
|
accessibilityLabel={accessibilityLabel}
|
|
153
167
|
onPress={onPress}
|
|
154
168
|
{...focusProps}
|
|
@@ -173,8 +187,6 @@ export function TableCell(props: TableCellProps) {
|
|
|
173
187
|
return <View style={_column ? colStyle(_column) : undefined}>{children}</View>;
|
|
174
188
|
}
|
|
175
189
|
|
|
176
|
-
const noop = () => {};
|
|
177
|
-
|
|
178
190
|
const styles = StyleSheet.create({
|
|
179
191
|
// A hairline under the column header anchors the columns; the rows below it are
|
|
180
192
|
// Divider-separated.
|
|
@@ -194,6 +206,14 @@ const styles = StyleSheet.create({
|
|
|
194
206
|
row: {
|
|
195
207
|
gap: 14,
|
|
196
208
|
},
|
|
209
|
+
// Read-only row (a `TableRow` with no `onPress`): the register row's gutter +
|
|
210
|
+
// layout so cells still align with the header, minus the hover/press surface.
|
|
211
|
+
staticRow: {
|
|
212
|
+
flexDirection: "row",
|
|
213
|
+
alignItems: "center",
|
|
214
|
+
gap: 14,
|
|
215
|
+
paddingHorizontal: 20,
|
|
216
|
+
},
|
|
197
217
|
door: {
|
|
198
218
|
flex: 1,
|
|
199
219
|
flexDirection: "row",
|