@calliopelabs/ui 0.14.0 → 0.16.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.
Files changed (64) hide show
  1. package/README.md +142 -1
  2. package/dist/agent/AgentChat.d.ts +8 -1
  3. package/dist/agent/AgentChat.js +43 -2
  4. package/dist/agent/WorkflowRenderers.d.ts +61 -0
  5. package/dist/agent/WorkflowRenderers.js +102 -0
  6. package/dist/agent/controller.d.ts +19 -0
  7. package/dist/agent/controller.js +33 -0
  8. package/dist/agent/copilotkit/CopilotKitAgentProvider.js +2 -2
  9. package/dist/agent/index.d.ts +4 -2
  10. package/dist/agent/index.js +3 -1
  11. package/dist/agent/mock.d.ts +26 -4
  12. package/dist/agent/mock.js +68 -37
  13. package/dist/agent/renderers.d.ts +49 -0
  14. package/dist/agent/renderers.js +27 -0
  15. package/dist/appearance/brands.d.ts +17 -0
  16. package/dist/appearance/brands.js +14 -0
  17. package/dist/chat/MessageBubble.js +1 -1
  18. package/dist/chat/MessageList.d.ts +9 -2
  19. package/dist/chat/MessageList.js +11 -8
  20. package/dist/css/appearance.js +1 -1
  21. package/dist/css/calliope.css +5 -0
  22. package/dist/css/tokens.css +5 -0
  23. package/dist/index.d.ts +6 -1
  24. package/dist/index.js +6 -1
  25. package/dist/primitives/Badge.d.ts +14 -0
  26. package/dist/primitives/Badge.js +25 -0
  27. package/dist/primitives/ContextMenu.d.ts +18 -0
  28. package/dist/primitives/ContextMenu.js +34 -0
  29. package/dist/primitives/ScrollArea.d.ts +16 -0
  30. package/dist/primitives/ScrollArea.js +11 -0
  31. package/dist/primitives/Toast.d.ts +37 -0
  32. package/dist/primitives/Toast.js +43 -0
  33. package/dist/primitives/Tooltip.d.ts +16 -0
  34. package/dist/primitives/Tooltip.js +26 -0
  35. package/dist/provider/messages.d.ts +69 -0
  36. package/dist/provider/messages.js +60 -0
  37. package/dist/theme/css-layer.js +2 -1
  38. package/dist/theme/tokens.generated.d.ts +3 -0
  39. package/dist/theme/tokens.generated.js +3 -0
  40. package/dist/workbench/FileBrowser.d.ts +62 -0
  41. package/dist/workbench/FileBrowser.js +117 -0
  42. package/dist/workbench/FileGrid.d.ts +13 -0
  43. package/dist/workbench/FileGrid.js +114 -0
  44. package/dist/workbench/FileIcon.d.ts +10 -0
  45. package/dist/workbench/FileIcon.js +24 -0
  46. package/dist/workbench/FileList.d.ts +35 -0
  47. package/dist/workbench/FileList.js +160 -0
  48. package/dist/workbench/FilePreview.d.ts +66 -0
  49. package/dist/workbench/FilePreview.js +108 -0
  50. package/dist/workbench/FileTree.d.ts +37 -0
  51. package/dist/workbench/FileTree.js +218 -0
  52. package/dist/workbench/UploadDropzone.d.ts +52 -0
  53. package/dist/workbench/UploadDropzone.js +115 -0
  54. package/dist/workbench/dnd.d.ts +17 -0
  55. package/dist/workbench/dnd.js +77 -0
  56. package/dist/workbench/index.d.ts +16 -0
  57. package/dist/workbench/index.js +16 -0
  58. package/dist/workbench/model.d.ts +72 -0
  59. package/dist/workbench/model.js +214 -0
  60. package/dist/workbench/selection.d.ts +16 -0
  61. package/dist/workbench/selection.js +46 -0
  62. package/dist/workbench/typeahead.d.ts +15 -0
  63. package/dist/workbench/typeahead.js +22 -0
  64. package/package.json +5 -1
@@ -0,0 +1,10 @@
1
+ import { type FileKind } from "./model.js";
2
+ export interface FileIconProps {
3
+ name: string;
4
+ kind: FileKind;
5
+ /** An open directory (an expanded tree node). */
6
+ open?: boolean;
7
+ className?: string;
8
+ }
9
+ /** A file-type glyph from the name's extension; directories in the accent. Decorative. */
10
+ export declare function FileIcon({ name, kind, open, className }: FileIconProps): import("react").JSX.Element;
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { FileArchiveIcon, FileCodeIcon, FileIcon as LucideFileIcon, FileImageIcon, FileJsonIcon, FileSpreadsheetIcon, FileTextIcon, FolderIcon, FolderOpenIcon, } from "lucide-react";
3
+ import { cn } from "../lib/cn.js";
4
+ import { fileExtension } from "./model.js";
5
+ const BY_EXTENSION = {};
6
+ const group = (icon, extensions) => {
7
+ for (const ext of extensions.split(" "))
8
+ BY_EXTENSION[ext] = icon;
9
+ };
10
+ group(FileCodeIcon, "py js jsx ts tsx mjs cjs rs go java kt rb sh bash zsh sql html css scss c h cpp hpp cs php swift lua r tf");
11
+ group(FileJsonIcon, "json jsonl ipynb");
12
+ group(FileSpreadsheetIcon, "csv tsv parquet xlsx xls ods");
13
+ group(FileImageIcon, "png jpg jpeg gif svg webp avif bmp ico");
14
+ group(FileTextIcon, "md markdown txt log yaml yml toml ini cfg conf rst pdf doc docx");
15
+ group(FileArchiveIcon, "zip tar gz tgz bz2 xz 7z rar");
16
+ /** A file-type glyph from the name's extension; directories in the accent. Decorative. */
17
+ export function FileIcon({ name, kind, open = false, className }) {
18
+ if (kind === "directory") {
19
+ const Folder = open ? FolderOpenIcon : FolderIcon;
20
+ return _jsx(Folder, { "aria-hidden": true, className: cn("size-4 shrink-0 text-primary", className) });
21
+ }
22
+ const Glyph = BY_EXTENSION[fileExtension(name)] ?? LucideFileIcon;
23
+ return _jsx(Glyph, { "aria-hidden": true, className: cn("size-4 shrink-0 text-muted-foreground", className) });
24
+ }
@@ -0,0 +1,35 @@
1
+ import * as React from "react";
2
+ import { type MoveHandler } from "./dnd.js";
3
+ import { type FileItem, type FileSort } from "./model.js";
4
+ export interface FileViewProps {
5
+ /** Names the view for screen readers ("Files in reports"). */
6
+ label: string;
7
+ /** In display order (`sortFiles`). */
8
+ items: readonly FileItem[];
9
+ selection: ReadonlySet<string>;
10
+ onSelectionChange: (next: Set<string>) => void;
11
+ /** Double-click or Enter. */
12
+ onOpen: (item: FileItem) => void;
13
+ /** Delete, or Cmd/Ctrl+Backspace, on the selection. */
14
+ onDelete?: (items: FileItem[]) => void;
15
+ /** F2 on one item. */
16
+ onRename?: (item: FileItem) => void;
17
+ /** Drag items onto directories. */
18
+ onMove?: MoveHandler;
19
+ className?: string;
20
+ }
21
+ export interface FileListProps extends FileViewProps {
22
+ sort?: FileSort;
23
+ /** Present, the headers are sort buttons. */
24
+ onSortChange?: (sort: FileSort) => void;
25
+ /** How a modified time reads. Defaults to the locale's medium date and short time. */
26
+ formatDate?: (value: string | Date) => string;
27
+ }
28
+ /**
29
+ * A directory as a sortable table, on the WAI-ARIA grid pattern: one tab
30
+ * stop, arrows move between cells, selection follows the focus (Ctrl or Cmd
31
+ * with an arrow moves without selecting, Shift extends), Home and End, Page
32
+ * Up and Down, Enter opens, Cmd/Ctrl+A selects all, Escape clears, and
33
+ * typing finds a name. Rows drag onto folders.
34
+ */
35
+ export declare function FileList({ label, items, selection, onSelectionChange, onOpen, onDelete, onRename, onMove, sort, onSortChange, formatDate, className, }: FileListProps): React.JSX.Element;
@@ -0,0 +1,160 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { ArrowDownIcon, ArrowUpIcon } from "lucide-react";
4
+ import * as React from "react";
5
+ import { cn } from "../lib/cn.js";
6
+ import { useUI } from "../provider/UIProvider.js";
7
+ import { startFileDrag, useFileDrop } from "./dnd.js";
8
+ import { FileIcon } from "./FileIcon.js";
9
+ import { formatBytes } from "./model.js";
10
+ import { modeOf, useSelectionModel } from "./selection.js";
11
+ import { typeahead } from "./typeahead.js";
12
+ const COLUMNS = ["name", "size", "modified"];
13
+ const WIDTH = {
14
+ name: "min-w-0 flex-1",
15
+ size: "w-20 shrink-0 justify-end text-right",
16
+ modified: "hidden w-44 shrink-0 @lg:flex",
17
+ };
18
+ const defaultFormat = (value) => {
19
+ const date = value instanceof Date ? value : new Date(value);
20
+ return Number.isNaN(date.getTime())
21
+ ? ""
22
+ : date.toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" });
23
+ };
24
+ /**
25
+ * A directory as a sortable table, on the WAI-ARIA grid pattern: one tab
26
+ * stop, arrows move between cells, selection follows the focus (Ctrl or Cmd
27
+ * with an arrow moves without selecting, Shift extends), Home and End, Page
28
+ * Up and Down, Enter opens, Cmd/Ctrl+A selects all, Escape clears, and
29
+ * typing finds a name. Rows drag onto folders.
30
+ */
31
+ export function FileList({ label, items, selection, onSelectionChange, onOpen, onDelete, onRename, onMove, sort, onSortChange, formatDate = defaultFormat, className, }) {
32
+ const { messages } = useUI();
33
+ const t = messages.workbench;
34
+ const order = React.useMemo(() => items.map((i) => i.path), [items]);
35
+ const model = useSelectionModel(order, selection, onSelectionChange);
36
+ // Focus: row -1 is the header; columns index COLUMNS.
37
+ const [focus, setFocus] = React.useState({ row: 0, col: 0 });
38
+ const cells = React.useRef(new Map());
39
+ const pending = React.useRef(false);
40
+ const search = React.useRef({ text: "", at: 0 });
41
+ const headerRow = onSortChange ? -1 : 0;
42
+ const row = Math.min(Math.max(focus.row, headerRow), items.length - 1);
43
+ const col = focus.col;
44
+ React.useLayoutEffect(() => {
45
+ if (!pending.current)
46
+ return;
47
+ pending.current = false;
48
+ cells.current.get(`${row}:${col}`)?.focus();
49
+ });
50
+ const move = (next) => {
51
+ pending.current = true;
52
+ setFocus({
53
+ row: Math.min(Math.max(next.row, headerRow), items.length - 1),
54
+ col: Math.min(Math.max(next.col, 0), COLUMNS.length - 1),
55
+ });
56
+ };
57
+ const selected = () => items.filter((i) => selection.has(i.path));
58
+ const onKeyDown = (event) => {
59
+ const meta = event.metaKey || event.ctrlKey;
60
+ const go = (target) => {
61
+ event.preventDefault();
62
+ const clamped = Math.min(Math.max(target, headerRow), items.length - 1);
63
+ move({ row: clamped, col });
64
+ const item = items[clamped];
65
+ if (item && !meta)
66
+ model.select(item.path, event.shiftKey ? "range" : "replace");
67
+ };
68
+ switch (event.key) {
69
+ case "ArrowDown":
70
+ return go(row + 1);
71
+ case "ArrowUp":
72
+ return go(row - 1);
73
+ case "PageDown":
74
+ return go(row + 10);
75
+ case "PageUp":
76
+ return go(row - 10);
77
+ case "ArrowRight":
78
+ event.preventDefault();
79
+ return move({ row, col: col + 1 });
80
+ case "ArrowLeft":
81
+ event.preventDefault();
82
+ return move({ row, col: col - 1 });
83
+ case "Home":
84
+ if (meta)
85
+ return go(0);
86
+ event.preventDefault();
87
+ return move({ row, col: 0 });
88
+ case "End":
89
+ if (meta)
90
+ return go(items.length - 1);
91
+ event.preventDefault();
92
+ return move({ row, col: COLUMNS.length - 1 });
93
+ }
94
+ if (row < 0)
95
+ return;
96
+ const item = items[row];
97
+ if (!item)
98
+ return;
99
+ if (event.key === "Enter") {
100
+ event.preventDefault();
101
+ onOpen(item);
102
+ }
103
+ else if (event.key === " ") {
104
+ event.preventDefault();
105
+ model.select(item.path, meta ? "toggle" : event.shiftKey ? "range" : "replace");
106
+ }
107
+ else if (meta && event.key.toLowerCase() === "a") {
108
+ event.preventDefault();
109
+ model.selectAll();
110
+ }
111
+ else if (event.key === "Escape" && selection.size > 0) {
112
+ event.preventDefault();
113
+ model.clear();
114
+ }
115
+ else if (onDelete && (event.key === "Delete" || (meta && event.key === "Backspace"))) {
116
+ const targets = selected();
117
+ if (targets.length) {
118
+ event.preventDefault();
119
+ onDelete(targets);
120
+ }
121
+ }
122
+ else if (onRename && event.key === "F2") {
123
+ event.preventDefault();
124
+ onRename(item);
125
+ }
126
+ else {
127
+ const next = typeahead(search.current, event, items.map((i) => i.name), row);
128
+ if (next !== null)
129
+ go(next);
130
+ }
131
+ };
132
+ const cellRef = (r, c) => (el) => {
133
+ if (el)
134
+ cells.current.set(`${r}:${c}`, el);
135
+ else
136
+ cells.current.delete(`${r}:${c}`);
137
+ };
138
+ const tabIndex = (r, c) => (r === row && c === col ? 0 : -1);
139
+ const header = { name: t.name, size: t.size, modified: t.modified };
140
+ return (_jsxs("div", { role: "grid", "aria-label": label, "aria-multiselectable": true, "aria-rowcount": items.length + 1, onKeyDown: onKeyDown, className: cn("@container flex min-w-0 flex-col text-sm", className), children: [_jsx("div", { role: "row", "aria-rowindex": 1, className: "sticky top-0 z-10 flex items-center gap-2 border-b bg-muted/60 px-3 text-xs font-medium text-muted-foreground backdrop-blur-sm", children: COLUMNS.map((key, c) => {
141
+ const active = sort?.key === key;
142
+ const ariaSort = active ? (sort.dir === "asc" ? "ascending" : "descending") : undefined;
143
+ return (_jsx("div", { role: "columnheader", "aria-sort": ariaSort, className: cn("flex h-8 items-center", WIDTH[key]), children: onSortChange ? (_jsxs("button", { type: "button", ref: cellRef(-1, c), tabIndex: tabIndex(-1, c), onFocus: () => setFocus({ row: -1, col: c }), onClick: () => onSortChange({
144
+ key,
145
+ dir: active && sort.dir === "asc" ? "desc" : "asc",
146
+ }), className: "inline-flex items-center gap-1 rounded-lg outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50", children: [header[key], active &&
147
+ (sort.dir === "asc" ? (_jsx(ArrowUpIcon, { "aria-hidden": true, className: "size-3" })) : (_jsx(ArrowDownIcon, { "aria-hidden": true, className: "size-3" })))] })) : (header[key]) }, key));
148
+ }) }), items.map((item, r) => (_jsx(FileRow, { item: item, index: r, selected: selection.has(item.path), onMove: onMove, dragPaths: selection.has(item.path) ? order.filter((p) => selection.has(p)) : [item.path], dragLabel: (n) => (n === 1 ? item.name : t.itemCount(n)), onClick: (event) => {
149
+ setFocus({ row: r, col });
150
+ model.select(item.path, modeOf(event));
151
+ }, onDoubleClick: () => onOpen(item), onContextMenu: () => {
152
+ // Right-clicking outside the selection selects the item, as file managers do.
153
+ if (!selection.has(item.path))
154
+ model.select(item.path, "replace");
155
+ }, children: COLUMNS.map((key, c) => (_jsx("div", { role: "gridcell", ref: cellRef(r, c), tabIndex: tabIndex(r, c), onFocus: () => setFocus({ row: r, col: c }), className: cn("flex h-8 items-center gap-2 rounded-lg outline-none focus-visible:ring-2 focus-visible:ring-ring/50", WIDTH[key], key !== "name" && "text-xs text-muted-foreground"), children: key === "name" ? (_jsxs(_Fragment, { children: [_jsx(FileIcon, { name: item.name, kind: item.kind }), _jsx("span", { className: "truncate", children: item.name })] })) : key === "size" ? (item.kind === "file" && item.size != null ? (formatBytes(item.size)) : (_jsx("span", { "aria-label": t.noSize, children: "\u2013" }))) : item.modified ? (_jsx("span", { className: "truncate", children: formatDate(item.modified) })) : null }, key))) }, item.path)))] }));
156
+ }
157
+ function FileRow({ item, index, selected, onMove, dragPaths, dragLabel, onClick, onDoubleClick, onContextMenu, children, }) {
158
+ const drop = useFileDrop(item.kind === "directory" ? item.path : null, onMove);
159
+ return (_jsx("div", { role: "row", "data-path": item.path, "aria-rowindex": index + 2, "aria-selected": selected, draggable: Boolean(onMove), onDragStart: onMove ? (event) => startFileDrag(event, dragPaths, dragLabel(dragPaths.length)) : undefined, ...drop.props, onClick: onClick, onDoubleClick: onDoubleClick, onContextMenu: onContextMenu, className: cn("flex cursor-default items-center gap-2 px-3 select-none hover:bg-accent/60", selected && "bg-accent text-accent-foreground hover:bg-accent", drop.over && "bg-primary/10 ring-2 ring-primary ring-inset"), children: children }));
160
+ }
@@ -0,0 +1,66 @@
1
+ import * as React from "react";
2
+ /** What the host read from the file. `previewKindFor(name, mime)` picks the kind. */
3
+ export type FilePreviewContent = {
4
+ kind: "text";
5
+ text: string;
6
+ } | {
7
+ kind: "markdown";
8
+ text: string;
9
+ }
10
+ /** Pretty-printed when it parses; shown as written otherwise (JSON Lines). */
11
+ | {
12
+ kind: "json";
13
+ text: string;
14
+ }
15
+ /** Parsed here; the first row is the header unless `header` is false. `delimiter` "\t" for TSV. */
16
+ | {
17
+ kind: "csv";
18
+ text: string;
19
+ delimiter?: string;
20
+ header?: boolean;
21
+ }
22
+ /** Rows the server already parsed (a Parquet or Excel sample). */
23
+ | {
24
+ kind: "table";
25
+ columns: readonly string[];
26
+ rows: readonly Record<string, unknown>[];
27
+ } | {
28
+ kind: "image";
29
+ src: string;
30
+ alt?: string;
31
+ }
32
+ /** Nothing to show: the type and size only. */
33
+ | {
34
+ kind: "binary";
35
+ };
36
+ export interface FilePreviewProps {
37
+ /** The file's name, the panel's title. */
38
+ name: string;
39
+ size?: number | null;
40
+ mimeType?: string | null;
41
+ /** Undefined while loading. */
42
+ content?: FilePreviewContent;
43
+ error?: string | {
44
+ message: string;
45
+ } | null;
46
+ onRetry?: () => void;
47
+ /** Part of the file only: the rows or bytes shown, and the whole when known. */
48
+ truncated?: boolean | {
49
+ shown: number;
50
+ total?: number | null;
51
+ };
52
+ /** Draws markdown. The package ships no markdown parser; without one the source shows as text. */
53
+ renderMarkdown?: (text: string) => React.ReactNode;
54
+ /** Shows a close button. */
55
+ onClose?: () => void;
56
+ /** Buttons in the header: download, open. */
57
+ actions?: React.ReactNode;
58
+ className?: string;
59
+ }
60
+ /**
61
+ * A file's contents in a side panel: CSV and tables in the library's
62
+ * DataTable (search, sort, paging), JSON pretty-printed, images, markdown
63
+ * through the host's renderer, and text. It reads nothing itself: the host
64
+ * fetches the bytes and hands over the content.
65
+ */
66
+ export declare function FilePreview({ name, size, mimeType, content, error, onRetry, truncated, renderMarkdown, onClose, actions, className, }: FilePreviewProps): React.JSX.Element;
@@ -0,0 +1,108 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { FileQuestionIcon, XIcon } from "lucide-react";
4
+ import * as React from "react";
5
+ import { DataTable } from "../data/DataTable.js";
6
+ import { useLocalTable } from "../data/use-local-table.js";
7
+ import { cn } from "../lib/cn.js";
8
+ import { Button } from "../primitives/Button.js";
9
+ import { useUI } from "../provider/UIProvider.js";
10
+ import { EmptyState } from "../states/EmptyState.js";
11
+ import { ErrorState } from "../states/ErrorState.js";
12
+ import { LoadingState } from "../states/LoadingState.js";
13
+ import { FileIcon } from "./FileIcon.js";
14
+ import { formatBytes, parseDelimited } from "./model.js";
15
+ function TablePreview({ name, columns, rows, }) {
16
+ const { messages } = useUI();
17
+ const t = messages.workbench;
18
+ const controller = useLocalTable(rows, {
19
+ searchText: (row) => row.cells.join(" "),
20
+ pageSize: 50,
21
+ sortBy: Object.fromEntries(columns.map((_, i) => [String(i), (row) => row.cells[i] ?? ""])),
22
+ });
23
+ const tableColumns = columns.map((header, i) => ({
24
+ id: String(i),
25
+ header: header || t.column(i + 1),
26
+ sortKey: String(i),
27
+ cellClassName: "max-w-64 truncate",
28
+ cell: (row) => row.cells[i] ?? "",
29
+ }));
30
+ return (_jsx(DataTable, { label: name, controller: controller, columns: tableColumns, getRowId: (row) => row.id, empty: { title: t.noRows }, density: "compact" }));
31
+ }
32
+ function stringify(value) {
33
+ if (value == null)
34
+ return "";
35
+ return typeof value === "object" ? JSON.stringify(value) : String(value);
36
+ }
37
+ /**
38
+ * A file's contents in a side panel: CSV and tables in the library's
39
+ * DataTable (search, sort, paging), JSON pretty-printed, images, markdown
40
+ * through the host's renderer, and text. It reads nothing itself: the host
41
+ * fetches the bytes and hands over the content.
42
+ */
43
+ export function FilePreview({ name, size, mimeType, content, error, onRetry, truncated, renderMarkdown, onClose, actions, className, }) {
44
+ const { messages } = useUI();
45
+ const t = messages.workbench;
46
+ const table = React.useMemo(() => {
47
+ if (content?.kind === "csv") {
48
+ const parsed = parseDelimited(content.text, content.delimiter);
49
+ const header = content.header !== false;
50
+ const width = Math.max(0, ...parsed.map((r) => r.length));
51
+ const columns = header ? (parsed[0] ?? []) : Array.from({ length: width }, () => "");
52
+ const body = header ? parsed.slice(1) : parsed;
53
+ return { columns, rows: body.map((cells, i) => ({ id: String(i), cells })) };
54
+ }
55
+ if (content?.kind === "table")
56
+ return {
57
+ columns: content.columns,
58
+ rows: content.rows.map((row, i) => ({
59
+ id: String(i),
60
+ cells: content.columns.map((c) => stringify(row[c])),
61
+ })),
62
+ };
63
+ return null;
64
+ }, [content]);
65
+ const json = React.useMemo(() => {
66
+ if (content?.kind !== "json")
67
+ return null;
68
+ try {
69
+ return JSON.stringify(JSON.parse(content.text), null, 2);
70
+ }
71
+ catch {
72
+ return content.text;
73
+ }
74
+ }, [content]);
75
+ const errorMessage = typeof error === "string" ? error : error?.message;
76
+ let body;
77
+ if (error) {
78
+ body = (_jsx(ErrorState, { title: t.previewFailed, error: errorMessage || t.previewFailed, onRetry: onRetry, retryLabel: t.retry }));
79
+ }
80
+ else if (!content) {
81
+ body = _jsx(LoadingState, { label: t.previewLoading, rows: 4 });
82
+ }
83
+ else if (table) {
84
+ body = _jsx(TablePreview, { name: name, columns: table.columns, rows: table.rows });
85
+ }
86
+ else if (content.kind === "image") {
87
+ body = (_jsx("img", { src: content.src, alt: content.alt ?? name, className: "mx-auto max-h-full max-w-full rounded-lg object-contain" }));
88
+ }
89
+ else if (content.kind === "markdown" && renderMarkdown) {
90
+ body = _jsx("div", { className: "text-sm", children: renderMarkdown(content.text) });
91
+ }
92
+ else if (content.kind === "binary") {
93
+ body = (_jsx(EmptyState, { icon: _jsx(FileQuestionIcon, { "aria-hidden": true, className: "size-6" }), title: t.noPreview, description: [mimeType, size != null ? formatBytes(size) : null]
94
+ .filter(Boolean)
95
+ .join(" · ") }));
96
+ }
97
+ else {
98
+ body = (_jsx("pre", {
99
+ // Long text scrolls the panel: focusable so a keyboard can reach and scroll it.
100
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
101
+ tabIndex: 0, "aria-label": t.contentsOf(name), className: "rounded-lg font-mono text-xs leading-relaxed break-words whitespace-pre-wrap outline-none focus-visible:ring-2 focus-visible:ring-ring/50", children: json ?? (content.kind === "text" || content.kind === "markdown" ? content.text : "") }));
102
+ }
103
+ const partial = truncated &&
104
+ (typeof truncated === "object"
105
+ ? t.showing(truncated.shown, truncated.total ?? null)
106
+ : t.partial);
107
+ return (_jsxs("section", { "aria-label": t.previewOf(name), "data-slot": "file-preview", className: cn("flex min-h-0 min-w-0 flex-col bg-background", className), children: [_jsxs("header", { className: "flex min-h-11 items-center gap-2 border-b px-3 py-1.5", children: [_jsx(FileIcon, { name: name, kind: "file" }), _jsx("h2", { className: "min-w-0 flex-1 truncate text-sm font-medium", title: name, children: name }), size != null && (_jsx("span", { className: "shrink-0 text-xs text-muted-foreground", children: formatBytes(size) })), actions, onClose && (_jsx(Button, { variant: "ghost", size: "icon", className: "size-7", "aria-label": t.closePreview, onClick: onClose, children: _jsx(XIcon, { "aria-hidden": true }) }))] }), _jsx("div", { className: "min-h-0 flex-1 overflow-auto p-3", children: body }), partial && _jsx("p", { className: "border-t px-3 py-1.5 text-xs text-muted-foreground", children: partial })] }));
108
+ }
@@ -0,0 +1,37 @@
1
+ import * as React from "react";
2
+ import { type MoveHandler } from "./dnd.js";
3
+ import { type FileBranch, type FileItem } from "./model.js";
4
+ export interface FileTreeProps {
5
+ /** Names the tree. Defaults to "Folders". */
6
+ label?: string;
7
+ /** The top level. */
8
+ root: FileBranch;
9
+ /** Each loaded directory's children, by path. A directory missing here has not been asked for. */
10
+ branches?: Readonly<Record<string, FileBranch>>;
11
+ /** A directory opened with no branch yet: fetch it and put a `loading`, then `ready`, branch in `branches`. */
12
+ onLoad?: (path: string) => void;
13
+ /** Re-fetch after an error. `""` is the root. */
14
+ onRetry?: (path: string) => void;
15
+ /** Open directories. Controlled when given. */
16
+ expanded?: ReadonlySet<string>;
17
+ defaultExpanded?: Iterable<string>;
18
+ onExpandedChange?: (expanded: Set<string>) => void;
19
+ /** The current directory, drawn as selected. */
20
+ selectedPath?: string | null;
21
+ /** A node chosen by click, Enter or Space. */
22
+ onSelect?: (item: FileItem) => void;
23
+ /** Files as well as directories. Off by default: a folder tree. */
24
+ showFiles?: boolean;
25
+ /** Drag and drop between nodes; directories (and the tree's background, the root) are targets. */
26
+ onMove?: MoveHandler;
27
+ /** A node's context-menu items (`ContextMenuItem`s). */
28
+ contextMenu?: (item: FileItem) => React.ReactNode;
29
+ className?: string;
30
+ }
31
+ /**
32
+ * A lazy file tree on the WAI-ARIA tree pattern: one tab stop, arrows move
33
+ * and open, Home and End jump, typing a letter finds the next name. The
34
+ * host owns the data: it answers `onLoad` with a branch per directory, so
35
+ * the tree fetches nothing and works over any backend.
36
+ */
37
+ export declare function FileTree({ label, root, branches, onLoad, onRetry, expanded: expandedProp, defaultExpanded, onExpandedChange, selectedPath, onSelect, showFiles, onMove, contextMenu, className, }: FileTreeProps): React.JSX.Element;