@calliopelabs/ui 0.14.0 → 0.18.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 (127) hide show
  1. package/README.md +235 -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 +9 -1
  24. package/dist/index.js +8 -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/Menubar.d.ts +21 -0
  30. package/dist/primitives/Menubar.js +43 -0
  31. package/dist/primitives/ScrollArea.d.ts +16 -0
  32. package/dist/primitives/ScrollArea.js +11 -0
  33. package/dist/primitives/Toast.d.ts +37 -0
  34. package/dist/primitives/Toast.js +43 -0
  35. package/dist/primitives/Tooltip.d.ts +16 -0
  36. package/dist/primitives/Tooltip.js +26 -0
  37. package/dist/provider/messages.d.ts +217 -0
  38. package/dist/provider/messages.js +238 -0
  39. package/dist/shell/RailPanel.js +1 -1
  40. package/dist/theme/css-layer.js +2 -1
  41. package/dist/theme/tokens.generated.d.ts +3 -0
  42. package/dist/theme/tokens.generated.js +3 -0
  43. package/dist/workbench/ActivityBar.d.ts +37 -0
  44. package/dist/workbench/ActivityBar.js +57 -0
  45. package/dist/workbench/BottomPanel.d.ts +30 -0
  46. package/dist/workbench/BottomPanel.js +20 -0
  47. package/dist/workbench/CodeEditor.d.ts +48 -0
  48. package/dist/workbench/CodeEditor.js +153 -0
  49. package/dist/workbench/CommandPalette.d.ts +49 -0
  50. package/dist/workbench/CommandPalette.js +114 -0
  51. package/dist/workbench/DataChart.d.ts +45 -0
  52. package/dist/workbench/DataChart.js +146 -0
  53. package/dist/workbench/DataGrid.d.ts +89 -0
  54. package/dist/workbench/DataGrid.js +590 -0
  55. package/dist/workbench/DataProfile.d.ts +17 -0
  56. package/dist/workbench/DataProfile.js +51 -0
  57. package/dist/workbench/EditorTabs.d.ts +62 -0
  58. package/dist/workbench/EditorTabs.js +115 -0
  59. package/dist/workbench/FileBrowser.d.ts +62 -0
  60. package/dist/workbench/FileBrowser.js +117 -0
  61. package/dist/workbench/FileGrid.d.ts +13 -0
  62. package/dist/workbench/FileGrid.js +114 -0
  63. package/dist/workbench/FileIcon.d.ts +10 -0
  64. package/dist/workbench/FileIcon.js +24 -0
  65. package/dist/workbench/FileList.d.ts +35 -0
  66. package/dist/workbench/FileList.js +160 -0
  67. package/dist/workbench/FilePreview.d.ts +66 -0
  68. package/dist/workbench/FilePreview.js +108 -0
  69. package/dist/workbench/FileTree.d.ts +37 -0
  70. package/dist/workbench/FileTree.js +218 -0
  71. package/dist/workbench/GridCell.d.ts +19 -0
  72. package/dist/workbench/GridCell.js +50 -0
  73. package/dist/workbench/GridDetails.d.ts +31 -0
  74. package/dist/workbench/GridDetails.js +78 -0
  75. package/dist/workbench/MenuBar.d.ts +49 -0
  76. package/dist/workbench/MenuBar.js +25 -0
  77. package/dist/workbench/PaneGroup.d.ts +50 -0
  78. package/dist/workbench/PaneGroup.js +146 -0
  79. package/dist/workbench/PendingChangesBar.d.ts +20 -0
  80. package/dist/workbench/PendingChangesBar.js +19 -0
  81. package/dist/workbench/Shortcuts.d.ts +50 -0
  82. package/dist/workbench/Shortcuts.js +109 -0
  83. package/dist/workbench/SqlEditor.d.ts +21 -0
  84. package/dist/workbench/SqlEditor.js +79 -0
  85. package/dist/workbench/StatusBar.d.ts +28 -0
  86. package/dist/workbench/StatusBar.js +26 -0
  87. package/dist/workbench/TruncationBanner.d.ts +11 -0
  88. package/dist/workbench/TruncationBanner.js +12 -0
  89. package/dist/workbench/UploadDropzone.d.ts +52 -0
  90. package/dist/workbench/UploadDropzone.js +115 -0
  91. package/dist/workbench/WorkbenchShell.d.ts +36 -0
  92. package/dist/workbench/WorkbenchShell.js +20 -0
  93. package/dist/workbench/dnd.d.ts +17 -0
  94. package/dist/workbench/dnd.js +77 -0
  95. package/dist/workbench/download.d.ts +11 -0
  96. package/dist/workbench/download.js +28 -0
  97. package/dist/workbench/editor-theme.d.ts +42 -0
  98. package/dist/workbench/editor-theme.js +113 -0
  99. package/dist/workbench/grid-format.d.ts +59 -0
  100. package/dist/workbench/grid-format.js +169 -0
  101. package/dist/workbench/grid-model.d.ts +75 -0
  102. package/dist/workbench/grid-model.js +259 -0
  103. package/dist/workbench/grid-selection.d.ts +69 -0
  104. package/dist/workbench/grid-selection.js +183 -0
  105. package/dist/workbench/grid-stats.d.ts +58 -0
  106. package/dist/workbench/grid-stats.js +185 -0
  107. package/dist/workbench/index.d.ts +44 -0
  108. package/dist/workbench/index.js +47 -0
  109. package/dist/workbench/keys.d.ts +23 -0
  110. package/dist/workbench/keys.js +105 -0
  111. package/dist/workbench/model.d.ts +72 -0
  112. package/dist/workbench/model.js +214 -0
  113. package/dist/workbench/palette.d.ts +17 -0
  114. package/dist/workbench/palette.js +45 -0
  115. package/dist/workbench/pending.d.ts +92 -0
  116. package/dist/workbench/pending.js +207 -0
  117. package/dist/workbench/selection.d.ts +16 -0
  118. package/dist/workbench/selection.js +46 -0
  119. package/dist/workbench/sql-completion.d.ts +107 -0
  120. package/dist/workbench/sql-completion.js +475 -0
  121. package/dist/workbench/typeahead.d.ts +15 -0
  122. package/dist/workbench/typeahead.js +22 -0
  123. package/dist/workflow/WorkflowRuntimeControls.d.ts +21 -0
  124. package/dist/workflow/WorkflowRuntimeControls.js +54 -0
  125. package/dist/workflow/runtime-controls.d.ts +29 -0
  126. package/dist/workflow/runtime-controls.js +10 -0
  127. package/package.json +17 -2
@@ -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;
@@ -0,0 +1,218 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { ChevronRightIcon, RotateCwIcon } from "lucide-react";
4
+ import * as React from "react";
5
+ import { cn } from "../lib/cn.js";
6
+ import { ContextMenu, ContextMenuContent, ContextMenuTrigger } from "../primitives/ContextMenu.js";
7
+ import { Button } from "../primitives/Button.js";
8
+ import { Skeleton } from "../primitives/Skeleton.js";
9
+ import { useUI } from "../provider/UIProvider.js";
10
+ import { startFileDrag, useFileDrop } from "./dnd.js";
11
+ import { FileIcon } from "./FileIcon.js";
12
+ import { parentPath } from "./model.js";
13
+ import { typeahead } from "./typeahead.js";
14
+ /**
15
+ * A lazy file tree on the WAI-ARIA tree pattern: one tab stop, arrows move
16
+ * and open, Home and End jump, typing a letter finds the next name. The
17
+ * host owns the data: it answers `onLoad` with a branch per directory, so
18
+ * the tree fetches nothing and works over any backend.
19
+ */
20
+ export function FileTree({ label, root, branches = {}, onLoad, onRetry, expanded: expandedProp, defaultExpanded, onExpandedChange, selectedPath = null, onSelect, showFiles = false, onMove, contextMenu, className, }) {
21
+ const { messages } = useUI();
22
+ const t = messages.workbench;
23
+ const [ownExpanded, setOwnExpanded] = React.useState(() => new Set(defaultExpanded ?? []));
24
+ const expanded = expandedProp ?? ownExpanded;
25
+ const [focusKey, setFocusKey] = React.useState(null);
26
+ const [menuItem, setMenuItem] = React.useState(null);
27
+ const nodes = React.useRef(new Map());
28
+ const pendingFocus = React.useRef(null);
29
+ const requested = React.useRef(new Set());
30
+ const search = React.useRef({ text: "", at: 0 });
31
+ const rootDrop = useFileDrop("", onMove);
32
+ const shown = React.useCallback((items) => showFiles ? items : items.filter((item) => item.kind === "directory"), [showFiles]);
33
+ const rows = React.useMemo(() => {
34
+ const out = [];
35
+ const walk = (items, level) => {
36
+ const list = shown(items);
37
+ list.forEach((item, i) => {
38
+ const branch = branches[item.path];
39
+ const leaf = item.kind !== "directory" ||
40
+ (branch?.state === "ready" && shown(branch.items).length === 0);
41
+ const open = !leaf && expanded.has(item.path);
42
+ out.push({
43
+ type: "item",
44
+ key: item.path,
45
+ item,
46
+ level,
47
+ size: list.length,
48
+ pos: i + 1,
49
+ open,
50
+ leaf,
51
+ });
52
+ if (!open)
53
+ return;
54
+ if (branch?.state === "ready")
55
+ walk(branch.items, level + 1);
56
+ else if (branch?.state === "error")
57
+ out.push({
58
+ type: "error",
59
+ key: `error:${item.path}`,
60
+ path: item.path,
61
+ level: level + 1,
62
+ message: branch.message,
63
+ });
64
+ else
65
+ out.push({
66
+ type: "loading",
67
+ key: `loading:${item.path}`,
68
+ path: item.path,
69
+ level: level + 1,
70
+ });
71
+ });
72
+ };
73
+ if (root.state === "ready")
74
+ walk(root.items, 1);
75
+ return out;
76
+ }, [root, branches, expanded, shown]);
77
+ // Ask for each open directory's children once; a branch arriving clears the request.
78
+ React.useEffect(() => {
79
+ for (const path of requested.current)
80
+ if (branches[path])
81
+ requested.current.delete(path);
82
+ for (const row of rows) {
83
+ if (row.type !== "loading" || branches[row.path] || requested.current.has(row.path))
84
+ continue;
85
+ requested.current.add(row.path);
86
+ onLoad?.(row.path);
87
+ }
88
+ }, [rows, branches, onLoad]);
89
+ React.useLayoutEffect(() => {
90
+ if (pendingFocus.current === null)
91
+ return;
92
+ nodes.current.get(pendingFocus.current)?.focus();
93
+ pendingFocus.current = null;
94
+ });
95
+ const setExpanded = (next) => {
96
+ if (!expandedProp)
97
+ setOwnExpanded(next);
98
+ onExpandedChange?.(next);
99
+ };
100
+ const toggle = (path, open) => {
101
+ if (expanded.has(path) === open)
102
+ return;
103
+ const next = new Set(expanded);
104
+ if (open)
105
+ next.add(path);
106
+ else
107
+ next.delete(path);
108
+ setExpanded(next);
109
+ };
110
+ const tabKey = (focusKey !== null && rows.some((r) => r.key === focusKey) && focusKey) ||
111
+ (selectedPath !== null && rows.some((r) => r.key === selectedPath) && selectedPath) ||
112
+ rows[0]?.key;
113
+ const focusRow = (key) => {
114
+ if (key === undefined)
115
+ return;
116
+ setFocusKey(key);
117
+ pendingFocus.current = key;
118
+ };
119
+ const activate = (row) => {
120
+ if (row.type === "error")
121
+ onRetry?.(row.path);
122
+ else if (row.type === "item") {
123
+ onSelect?.(row.item);
124
+ if (row.item.kind === "directory" && !row.leaf)
125
+ toggle(row.item.path, true);
126
+ }
127
+ };
128
+ const onKeyDown = (event) => {
129
+ const index = rows.findIndex((r) => r.key === tabKey);
130
+ const row = rows[index];
131
+ if (!row)
132
+ return;
133
+ const handled = () => {
134
+ event.preventDefault();
135
+ event.stopPropagation();
136
+ };
137
+ switch (event.key) {
138
+ case "ArrowDown":
139
+ handled();
140
+ focusRow(rows[Math.min(rows.length - 1, index + 1)]?.key);
141
+ return;
142
+ case "ArrowUp":
143
+ handled();
144
+ focusRow(rows[Math.max(0, index - 1)]?.key);
145
+ return;
146
+ case "Home":
147
+ handled();
148
+ focusRow(rows[0]?.key);
149
+ return;
150
+ case "End":
151
+ handled();
152
+ focusRow(rows[rows.length - 1]?.key);
153
+ return;
154
+ case "ArrowRight":
155
+ handled();
156
+ if (row.type !== "item" || row.leaf)
157
+ return;
158
+ if (!row.open)
159
+ toggle(row.item.path, true);
160
+ else
161
+ focusRow(rows[index + 1]?.key);
162
+ return;
163
+ case "ArrowLeft": {
164
+ handled();
165
+ if (row.type === "item" && row.open) {
166
+ toggle(row.item.path, false);
167
+ return;
168
+ }
169
+ const parent = row.type === "item" ? parentPath(row.item.path) : row.path;
170
+ if (parent !== "" && rows.some((r) => r.key === parent))
171
+ focusRow(parent);
172
+ return;
173
+ }
174
+ case "Enter":
175
+ case " ":
176
+ handled();
177
+ activate(row);
178
+ return;
179
+ default: {
180
+ const names = rows.map((r) => (r.type === "item" ? r.item.name : ""));
181
+ const next = typeahead(search.current, event, names, index);
182
+ if (next !== null) {
183
+ handled();
184
+ focusRow(rows[next]?.key);
185
+ }
186
+ }
187
+ }
188
+ };
189
+ if (root.state !== "ready" || rows.length === 0) {
190
+ return (_jsx("div", { className: cn("p-2 text-sm text-muted-foreground", className), ...rootDrop.props, children: root.state === "loading" ? (_jsx("div", { role: "status", "aria-label": t.loading, className: "flex flex-col gap-2 p-1", children: Array.from({ length: 5 }, (_, i) => (_jsx(Skeleton, { className: cn("h-4", i % 2 ? "w-2/3" : "w-4/5") }, i))) })) : root.state === "error" ? (_jsxs("div", { role: "alert", className: "flex flex-col items-start gap-2 p-1", children: [_jsx("p", { className: "[overflow-wrap:anywhere]", children: root.message || t.loadFailed }), onRetry && (_jsx(Button, { size: "sm", variant: "outline", onClick: () => onRetry(""), children: t.retry }))] })) : (_jsx("p", { className: "p-1", children: showFiles ? t.empty : t.noFolders })) }));
191
+ }
192
+ const tree = (_jsx("ul", { role: "tree", "aria-label": label ?? t.folders, onKeyDown: onKeyDown, ...rootDrop.props, className: cn("flex min-h-full flex-col gap-px p-1 text-sm outline-none", rootDrop.over && "bg-primary/5", className), children: rows.map((row) => row.type === "item" ? (_jsx(TreeNode, { row: row, nodeRef: (el) => {
193
+ if (el)
194
+ nodes.current.set(row.key, el);
195
+ else
196
+ nodes.current.delete(row.key);
197
+ }, tabbable: row.key === tabKey, selected: row.item.path === selectedPath, onFocus: () => setFocusKey(row.key), onActivate: () => activate(row), onToggle: () => toggle(row.item.path, !row.open), onContextMenu: () => setMenuItem(row.item), onMove: onMove, dragLabel: row.item.name }, row.key)) : (_jsx("li", { ref: (el) => {
198
+ if (el)
199
+ nodes.current.set(row.key, el);
200
+ else
201
+ nodes.current.delete(row.key);
202
+ }, role: "treeitem", "aria-level": row.level, "aria-selected": false, "aria-disabled": row.type === "loading" || undefined, "aria-busy": row.type === "loading" || undefined, tabIndex: row.key === tabKey ? 0 : -1, onFocus: () => setFocusKey(row.key), onClick: () => activate(row), style: { paddingInlineStart: `${row.level * 0.75 + 0.75}rem` }, className: cn("flex h-7 items-center gap-1.5 rounded-lg pr-2 text-xs text-muted-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring/50", row.type === "error" && "cursor-pointer text-danger-fg hover:bg-accent"), children: row.type === "loading" ? (t.loading) : (_jsxs(_Fragment, { children: [_jsx(RotateCwIcon, { "aria-hidden": true, className: "size-3" }), _jsx("span", { className: "truncate", children: t.retryFolder })] })) }, row.key))) }));
203
+ if (!contextMenu)
204
+ return tree;
205
+ return (_jsxs(ContextMenu, { onOpenChange: (open) => {
206
+ if (!open)
207
+ setMenuItem(null);
208
+ }, children: [_jsx(ContextMenuTrigger, { asChild: true, children: tree }), menuItem && _jsx(ContextMenuContent, { children: contextMenu(menuItem) })] }));
209
+ }
210
+ function TreeNode({ row, nodeRef, tabbable, selected, onFocus, onActivate, onToggle, onContextMenu, onMove, dragLabel, }) {
211
+ const { item, level, size, pos, open, leaf } = row;
212
+ const drop = useFileDrop(item.kind === "directory" ? item.path : null, onMove);
213
+ return (_jsxs("li", { ref: nodeRef, role: "treeitem", "aria-level": level, "aria-setsize": size, "aria-posinset": pos, "aria-expanded": leaf ? undefined : open, "aria-selected": selected, tabIndex: tabbable ? 0 : -1, draggable: Boolean(onMove), onDragStart: onMove ? (event) => startFileDrag(event, [item.path], dragLabel) : undefined, ...drop.props, onFocus: onFocus, onClick: onActivate, onContextMenu: onContextMenu, style: { paddingInlineStart: `${(level - 1) * 0.75 + 0.25}rem` }, className: cn("flex h-7 shrink-0 cursor-default items-center gap-1 rounded-lg pr-2 outline-none select-none hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring/50", selected && "bg-accent font-medium text-accent-foreground", drop.over && "bg-primary/10 ring-2 ring-primary"), children: [_jsx("span", { "aria-hidden": true, onClick: (event) => {
214
+ event.stopPropagation();
215
+ if (!leaf)
216
+ onToggle();
217
+ }, className: cn("inline-flex size-4 shrink-0 items-center justify-center rounded text-muted-foreground", leaf && "invisible"), children: _jsx(ChevronRightIcon, { className: cn("size-3 transition-transform", open && "rotate-90") }) }), _jsx(FileIcon, { name: item.name, kind: item.kind, open: open }), _jsx("span", { className: "truncate", children: item.name })] }));
218
+ }
@@ -0,0 +1,19 @@
1
+ import type * as React from "react";
2
+ import { type ColumnType, type GridColumnMeta } from "./grid-model.js";
3
+ export interface ColumnTypeIconProps {
4
+ type: ColumnType;
5
+ className?: string;
6
+ }
7
+ /** A column type's glyph, named by the type. Identity, not status: it takes no colour. */
8
+ export declare function ColumnTypeIcon({ type, className }: ColumnTypeIconProps): React.JSX.Element;
9
+ export interface ColumnKeyIconsProps {
10
+ column: Pick<GridColumnMeta, "primaryKey" | "foreignKey" | "references">;
11
+ }
12
+ /** A column's primary and foreign key marks, each named. */
13
+ export declare function ColumnKeyIcons({ column }: ColumnKeyIconsProps): React.JSX.Element;
14
+ export interface GridCellValueProps {
15
+ value: unknown;
16
+ className?: string;
17
+ }
18
+ /** How one value reads in a cell: NULL, empty text and binary are marked so they never pass for data. */
19
+ export declare function GridCellValue({ value, className }: GridCellValueProps): React.JSX.Element;