@calliopelabs/ui 0.13.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 (115) hide show
  1. package/README.md +192 -4
  2. package/dist/agent/AgentChat.d.ts +63 -0
  3. package/dist/agent/AgentChat.js +117 -0
  4. package/dist/agent/AgentLauncher.d.ts +25 -0
  5. package/dist/agent/AgentLauncher.js +25 -0
  6. package/dist/agent/AgentProvider.d.ts +82 -0
  7. package/dist/agent/AgentProvider.js +176 -0
  8. package/dist/agent/CalliopeAgent.d.ts +43 -0
  9. package/dist/agent/CalliopeAgent.js +111 -0
  10. package/dist/agent/WorkflowRenderers.d.ts +61 -0
  11. package/dist/agent/WorkflowRenderers.js +102 -0
  12. package/dist/agent/controller.d.ts +167 -0
  13. package/dist/agent/controller.js +496 -0
  14. package/dist/agent/copilotkit/CopilotKitAgentProvider.d.ts +41 -0
  15. package/dist/agent/copilotkit/CopilotKitAgentProvider.js +145 -0
  16. package/dist/agent/copilotkit/index.d.ts +6 -0
  17. package/dist/agent/copilotkit/index.js +6 -0
  18. package/dist/agent/engine.d.ts +41 -0
  19. package/dist/agent/engine.js +93 -0
  20. package/dist/agent/index.d.ts +17 -0
  21. package/dist/agent/index.js +17 -0
  22. package/dist/agent/mock.d.ts +56 -0
  23. package/dist/agent/mock.js +143 -0
  24. package/dist/agent/renderers.d.ts +49 -0
  25. package/dist/agent/renderers.js +27 -0
  26. package/dist/agent/surface.d.ts +30 -0
  27. package/dist/agent/surface.js +90 -0
  28. package/dist/agent/tool.d.ts +98 -0
  29. package/dist/agent/tool.js +81 -0
  30. package/dist/appearance/brands.d.ts +31 -12
  31. package/dist/appearance/brands.js +32 -51
  32. package/dist/chat/AgentProgress.d.ts +19 -0
  33. package/dist/chat/AgentProgress.js +34 -0
  34. package/dist/chat/Composer.d.ts +31 -0
  35. package/dist/chat/Composer.js +44 -0
  36. package/dist/chat/MessageBubble.d.ts +23 -0
  37. package/dist/chat/MessageBubble.js +26 -0
  38. package/dist/chat/MessageList.d.ts +43 -0
  39. package/dist/chat/MessageList.js +71 -0
  40. package/dist/chat/ModelPicker.d.ts +17 -0
  41. package/dist/chat/ModelPicker.js +13 -0
  42. package/dist/chat/ToolCallCard.d.ts +27 -0
  43. package/dist/chat/ToolCallCard.js +52 -0
  44. package/dist/chat/model.d.ts +123 -0
  45. package/dist/chat/model.js +337 -0
  46. package/dist/chat/patch.d.ts +12 -0
  47. package/dist/chat/patch.js +149 -0
  48. package/dist/chat/useAgUiRun.d.ts +52 -0
  49. package/dist/chat/useAgUiRun.js +137 -0
  50. package/dist/css/appearance.js +1 -1
  51. package/dist/css/calliope.css +5 -0
  52. package/dist/css/tokens.css +5 -0
  53. package/dist/index.d.ts +15 -1
  54. package/dist/index.js +16 -1
  55. package/dist/primitives/Badge.d.ts +14 -0
  56. package/dist/primitives/Badge.js +25 -0
  57. package/dist/primitives/Button.d.ts +1 -1
  58. package/dist/primitives/ContextMenu.d.ts +18 -0
  59. package/dist/primitives/ContextMenu.js +34 -0
  60. package/dist/primitives/ScrollArea.d.ts +16 -0
  61. package/dist/primitives/ScrollArea.js +11 -0
  62. package/dist/primitives/Toast.d.ts +37 -0
  63. package/dist/primitives/Toast.js +43 -0
  64. package/dist/primitives/Tooltip.d.ts +16 -0
  65. package/dist/primitives/Tooltip.js +26 -0
  66. package/dist/provider/messages.d.ts +135 -0
  67. package/dist/provider/messages.js +131 -0
  68. package/dist/theme/css-layer.js +2 -1
  69. package/dist/theme/mappings/palette.d.ts +52 -0
  70. package/dist/theme/mappings/palette.js +208 -0
  71. package/dist/theme/mappings/render.d.ts +9 -0
  72. package/dist/theme/mappings/render.js +520 -0
  73. package/dist/theme/mappings/targets.d.ts +43 -0
  74. package/dist/theme/mappings/targets.js +222 -0
  75. package/dist/theme/tokens.d.ts +10 -9
  76. package/dist/theme/tokens.generated.d.ts +356 -0
  77. package/dist/theme/tokens.generated.js +360 -0
  78. package/dist/theme/tokens.js +20 -264
  79. package/dist/tokens/README.jupyterlab.md +125 -0
  80. package/dist/tokens/README.md +15 -0
  81. package/dist/tokens/README.native.md +29 -0
  82. package/dist/tokens/README.plain.md +84 -0
  83. package/dist/tokens/README.python.md +33 -0
  84. package/dist/tokens/README.vscode.md +114 -0
  85. package/dist/tokens/jupyterlab.css +1710 -0
  86. package/dist/tokens/plain.css +930 -0
  87. package/dist/tokens/tokens.native.d.ts +85 -0
  88. package/dist/tokens/tokens.native.js +773 -0
  89. package/dist/tokens/tokens.py +1043 -0
  90. package/dist/tokens/vscode.css +1138 -0
  91. package/dist/workbench/FileBrowser.d.ts +62 -0
  92. package/dist/workbench/FileBrowser.js +117 -0
  93. package/dist/workbench/FileGrid.d.ts +13 -0
  94. package/dist/workbench/FileGrid.js +114 -0
  95. package/dist/workbench/FileIcon.d.ts +10 -0
  96. package/dist/workbench/FileIcon.js +24 -0
  97. package/dist/workbench/FileList.d.ts +35 -0
  98. package/dist/workbench/FileList.js +160 -0
  99. package/dist/workbench/FilePreview.d.ts +66 -0
  100. package/dist/workbench/FilePreview.js +108 -0
  101. package/dist/workbench/FileTree.d.ts +37 -0
  102. package/dist/workbench/FileTree.js +218 -0
  103. package/dist/workbench/UploadDropzone.d.ts +52 -0
  104. package/dist/workbench/UploadDropzone.js +115 -0
  105. package/dist/workbench/dnd.d.ts +17 -0
  106. package/dist/workbench/dnd.js +77 -0
  107. package/dist/workbench/index.d.ts +16 -0
  108. package/dist/workbench/index.js +16 -0
  109. package/dist/workbench/model.d.ts +72 -0
  110. package/dist/workbench/model.js +214 -0
  111. package/dist/workbench/selection.d.ts +16 -0
  112. package/dist/workbench/selection.js +46 -0
  113. package/dist/workbench/typeahead.d.ts +15 -0
  114. package/dist/workbench/typeahead.js +22 -0
  115. package/package.json +39 -2
@@ -0,0 +1,62 @@
1
+ import * as React from "react";
2
+ import { type MoveHandler } from "./dnd.js";
3
+ import { type FileTreeProps } from "./FileTree.js";
4
+ import { type FileItem, type FileSort, type FileView } from "./model.js";
5
+ export interface FileBrowserProps {
6
+ /** The directory on screen; "" is the root. */
7
+ path: string;
8
+ /** Its entries, in any order (the browser sorts them). Undefined while loading. */
9
+ items?: readonly FileItem[];
10
+ /** The listing failed: shown in place of the entries, with `onRetry`. */
11
+ error?: string | {
12
+ message: string;
13
+ } | null;
14
+ onRetry?: () => void;
15
+ /** Go to a directory: a breadcrumb, a tree node, or opening a folder. */
16
+ onNavigate: (path: string) => void;
17
+ /** Open a file (double-click or Enter). Folders navigate instead. */
18
+ onOpen?: (item: FileItem) => void;
19
+ /** Names the browser. Defaults to "Files". */
20
+ label?: string;
21
+ /** The root's name in the breadcrumb. Defaults to "Home". */
22
+ rootLabel?: string;
23
+ view?: FileView;
24
+ defaultView?: FileView;
25
+ onViewChange?: (view: FileView) => void;
26
+ sort?: FileSort;
27
+ defaultSort?: FileSort;
28
+ onSortChange?: (sort: FileSort) => void;
29
+ /** The selected paths. Controlled when given. */
30
+ selection?: ReadonlySet<string>;
31
+ onSelectionChange?: (selection: Set<string>) => void;
32
+ onMove?: MoveHandler;
33
+ onDelete?: (items: FileItem[]) => void;
34
+ onRename?: (item: FileItem) => void;
35
+ /**
36
+ * Context-menu items (`ContextMenuItem`s) for the items right-clicked (the
37
+ * selection), or for the folder itself when `targets` is empty.
38
+ */
39
+ contextMenu?: (targets: FileItem[]) => React.ReactNode;
40
+ /** A folder tree beside the listing; the browser wires its selection, moves and navigation. */
41
+ tree?: Omit<FileTreeProps, "selectedPath" | "onSelect" | "onMove" | "contextMenu">;
42
+ /** Controls beside the view switch: new folder, upload, search. */
43
+ toolbar?: React.ReactNode;
44
+ /** A thumbnail for grid tiles. */
45
+ renderThumbnail?: (item: FileItem) => React.ReactNode;
46
+ formatDate?: (value: string | Date) => string;
47
+ /** Copy for an empty folder. */
48
+ empty?: {
49
+ title: string;
50
+ description?: string;
51
+ };
52
+ className?: string;
53
+ }
54
+ /**
55
+ * The file browser: breadcrumb, view switch, an optional folder tree, and
56
+ * the listing as a table (FileList) or tiles (FileGrid), with sorting,
57
+ * selection, keyboard navigation, a context menu and drag-and-drop moves.
58
+ * It is driven by the host: data comes in as props and every change goes
59
+ * out through a callback, so it fetches nothing and runs over any store.
60
+ * Uncontrolled view, sort and selection work out of the box.
61
+ */
62
+ export declare function FileBrowser({ path, items, error, onRetry, onNavigate, onOpen, label, rootLabel, view: viewProp, defaultView, onViewChange, sort: sortProp, defaultSort, onSortChange, selection: selectionProp, onSelectionChange, onMove, onDelete, onRename, contextMenu, tree, toolbar, renderThumbnail, formatDate, empty, className, }: FileBrowserProps): React.JSX.Element;
@@ -0,0 +1,117 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { ChevronRightIcon, FolderOpenIcon, LayoutGridIcon, ListIcon } 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 { Segmented } from "../primitives/Segmented.js";
8
+ import { useUI } from "../provider/UIProvider.js";
9
+ import { EmptyState } from "../states/EmptyState.js";
10
+ import { ErrorState } from "../states/ErrorState.js";
11
+ import { LoadingState } from "../states/LoadingState.js";
12
+ import { useFileDrop } from "./dnd.js";
13
+ import { FileGrid } from "./FileGrid.js";
14
+ import { FileList } from "./FileList.js";
15
+ import { FileTree } from "./FileTree.js";
16
+ import { pathSegments, sortFiles } from "./model.js";
17
+ /**
18
+ * The file browser: breadcrumb, view switch, an optional folder tree, and
19
+ * the listing as a table (FileList) or tiles (FileGrid), with sorting,
20
+ * selection, keyboard navigation, a context menu and drag-and-drop moves.
21
+ * It is driven by the host: data comes in as props and every change goes
22
+ * out through a callback, so it fetches nothing and runs over any store.
23
+ * Uncontrolled view, sort and selection work out of the box.
24
+ */
25
+ export function FileBrowser({ path, items, error, onRetry, onNavigate, onOpen, label, rootLabel, view: viewProp, defaultView = "list", onViewChange, sort: sortProp, defaultSort = { key: "name", dir: "asc" }, onSortChange, selection: selectionProp, onSelectionChange, onMove, onDelete, onRename, contextMenu, tree, toolbar, renderThumbnail, formatDate, empty, className, }) {
26
+ const { messages } = useUI();
27
+ const t = messages.workbench;
28
+ const [ownView, setOwnView] = React.useState(defaultView);
29
+ const [ownSort, setOwnSort] = React.useState(defaultSort);
30
+ const [ownSelection, setOwnSelection] = React.useState(() => new Set());
31
+ const [menuTargets, setMenuTargets] = React.useState([]);
32
+ const view = viewProp ?? ownView;
33
+ const sort = sortProp ?? ownSort;
34
+ const selection = selectionProp ?? ownSelection;
35
+ const name = label ?? t.files;
36
+ const setView = (next) => {
37
+ setOwnView(next);
38
+ onViewChange?.(next);
39
+ };
40
+ const setSort = (next) => {
41
+ setOwnSort(next);
42
+ onSortChange?.(next);
43
+ };
44
+ const setSelection = React.useCallback((next) => {
45
+ setOwnSelection(next);
46
+ onSelectionChange?.(next);
47
+ }, [onSelectionChange]);
48
+ // A new folder starts with nothing selected (a controlled selection is the host's to clear).
49
+ const [shownPath, setShownPath] = React.useState(path);
50
+ if (shownPath !== path) {
51
+ setShownPath(path);
52
+ setOwnSelection(new Set());
53
+ }
54
+ const sorted = React.useMemo(() => (items ? sortFiles(items, sort) : []), [items, sort]);
55
+ const open = (item) => item.kind === "directory" ? onNavigate(item.path) : onOpen?.(item);
56
+ const segments = pathSegments(path);
57
+ const here = segments[segments.length - 1]?.name ?? rootLabel ?? t.root;
58
+ const viewProps = {
59
+ label: t.filesIn(here),
60
+ items: sorted,
61
+ selection,
62
+ onSelectionChange: setSelection,
63
+ onOpen: open,
64
+ onDelete,
65
+ onRename,
66
+ onMove,
67
+ };
68
+ const errorMessage = typeof error === "string" ? error : error?.message;
69
+ let body;
70
+ if (error) {
71
+ body = (_jsx(ErrorState, { title: t.loadFailed, error: errorMessage || t.loadFailed, onRetry: onRetry, retryLabel: t.retry }));
72
+ }
73
+ else if (!items) {
74
+ body = _jsx(LoadingState, { label: t.loading, rows: 6 });
75
+ }
76
+ else if (sorted.length === 0) {
77
+ body = (_jsx(EmptyState, { icon: _jsx(FolderOpenIcon, { "aria-hidden": true, className: "size-6" }), title: empty?.title ?? t.empty, description: empty?.description ?? (onMove ? t.emptyDescription : undefined) }));
78
+ }
79
+ else if (view === "grid") {
80
+ body = _jsx(FileGrid, { ...viewProps, renderThumbnail: renderThumbnail });
81
+ }
82
+ else {
83
+ body = _jsx(FileList, { ...viewProps, sort: sort, onSortChange: setSort, formatDate: formatDate });
84
+ }
85
+ const folderDrop = useFileDrop(items ? path : null, onMove);
86
+ const pane = (_jsx("div", { ...folderDrop.props, onContextMenu: (event) => {
87
+ if (!contextMenu)
88
+ return;
89
+ // The menu is for the item under the pointer (with the rest of the
90
+ // selection when it is part of it), or the folder on empty space.
91
+ const path = event.target.closest("[data-path]")?.dataset.path;
92
+ const item = sorted.find((i) => i.path === path);
93
+ const targets = !item
94
+ ? []
95
+ : selection.has(item.path)
96
+ ? sorted.filter((i) => selection.has(i.path))
97
+ : [item];
98
+ if (!items || error || contextMenu(targets) == null) {
99
+ event.preventDefault();
100
+ return;
101
+ }
102
+ setMenuTargets(targets);
103
+ }, className: cn("min-h-0 min-w-0 flex-1 overflow-auto", folderDrop.over && "bg-primary/5 ring-2 ring-primary ring-inset"), children: body }));
104
+ return (_jsxs("section", { "aria-label": name, "data-slot": "file-browser", className: cn("@container flex min-h-0 flex-col bg-background text-sm", className), children: [_jsxs("div", { className: "flex min-h-11 flex-wrap items-center gap-2 border-b px-3 py-1.5", children: [_jsx(PathBar, { path: path, rootLabel: rootLabel ?? t.root, onNavigate: onNavigate, onMove: onMove }), _jsxs("div", { className: "ml-auto flex items-center gap-1.5", children: [toolbar, _jsx(Segmented, { label: t.view, size: "sm", iconOnly: true, value: view, onValueChange: setView, options: [
105
+ { value: "list", label: t.listView, icon: ListIcon },
106
+ { value: "grid", label: t.gridView, icon: LayoutGridIcon },
107
+ ] })] })] }), _jsxs("div", { className: "flex min-h-0 flex-1", children: [tree && (_jsx("div", { className: "hidden w-56 shrink-0 overflow-auto border-r @2xl:block", children: _jsx(FileTree, { ...tree, selectedPath: path, onSelect: (item) => item.kind === "directory" ? onNavigate(item.path) : onOpen?.(item), onMove: onMove }) })), contextMenu ? (_jsxs(ContextMenu, { children: [_jsx(ContextMenuTrigger, { asChild: true, children: pane }), _jsx(ContextMenuContent, { children: contextMenu(menuTargets) })] })) : (pane)] }), _jsxs("div", { role: "status", className: "flex h-7 items-center gap-3 border-t px-3 text-xs text-muted-foreground", children: [items && !error && _jsx("span", { children: t.itemCount(sorted.length) }), selection.size > 0 && _jsx("span", { children: t.selected(selection.size) })] })] }));
108
+ }
109
+ function PathBar({ path, rootLabel, onNavigate, onMove, }) {
110
+ const { messages } = useUI();
111
+ const crumbs = [{ name: rootLabel, path: "" }, ...pathSegments(path)];
112
+ return (_jsx("nav", { "aria-label": messages.workbench.location, className: "min-w-0 flex-1", children: _jsx("ol", { className: "flex min-w-0 items-center gap-0.5", children: crumbs.map((crumb, i) => (_jsx(Crumb, { name: crumb.name, path: crumb.path, first: i === 0, current: i === crumbs.length - 1, onNavigate: onNavigate, onMove: onMove }, crumb.path || "/"))) }) }));
113
+ }
114
+ function Crumb({ name, path, first, current, onNavigate, onMove, }) {
115
+ const drop = useFileDrop(current ? null : path, onMove);
116
+ return (_jsxs("li", { className: cn("flex min-w-0 items-center gap-0.5", current ? "shrink" : "shrink-[2]"), children: [!first && _jsx(ChevronRightIcon, { "aria-hidden": true, className: "size-3 shrink-0 text-muted-foreground" }), _jsx("button", { type: "button", "aria-current": current ? "location" : undefined, onClick: () => onNavigate(path), ...drop.props, className: cn("min-w-0 truncate rounded-lg px-1.5 py-0.5 outline-none hover:bg-accent hover:text-accent-foreground focus-visible:ring-2 focus-visible:ring-ring/50", current ? "font-medium text-foreground" : "text-muted-foreground", drop.over && "bg-primary/10 ring-2 ring-primary"), children: name })] }));
117
+ }
@@ -0,0 +1,13 @@
1
+ import * as React from "react";
2
+ import type { FileItem } from "./model.js";
3
+ import type { FileViewProps } from "./FileList.js";
4
+ export interface FileGridProps extends FileViewProps {
5
+ /** A thumbnail in place of the type icon (an image's own preview). */
6
+ renderThumbnail?: (item: FileItem) => React.ReactNode;
7
+ }
8
+ /**
9
+ * A directory as tiles: a multi-select listbox laid out in rows, so the
10
+ * arrows move in two dimensions (up and down by a row of tiles). The same
11
+ * selection, opening, typeahead and drag rules as FileList.
12
+ */
13
+ export declare function FileGrid({ label, items, selection, onSelectionChange, onOpen, onDelete, onRename, onMove, renderThumbnail, className, }: FileGridProps): React.JSX.Element;
@@ -0,0 +1,114 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { cn } from "../lib/cn.js";
5
+ import { useUI } from "../provider/UIProvider.js";
6
+ import { startFileDrag, useFileDrop } from "./dnd.js";
7
+ import { FileIcon } from "./FileIcon.js";
8
+ import { modeOf, useSelectionModel } from "./selection.js";
9
+ import { typeahead } from "./typeahead.js";
10
+ /**
11
+ * A directory as tiles: a multi-select listbox laid out in rows, so the
12
+ * arrows move in two dimensions (up and down by a row of tiles). The same
13
+ * selection, opening, typeahead and drag rules as FileList.
14
+ */
15
+ export function FileGrid({ label, items, selection, onSelectionChange, onOpen, onDelete, onRename, onMove, renderThumbnail, className, }) {
16
+ const { messages } = useUI();
17
+ const t = messages.workbench;
18
+ const order = React.useMemo(() => items.map((i) => i.path), [items]);
19
+ const model = useSelectionModel(order, selection, onSelectionChange);
20
+ const [focusIndex, setFocusIndex] = React.useState(0);
21
+ const index = Math.min(focusIndex, items.length - 1);
22
+ const list = React.useRef(null);
23
+ const tiles = React.useRef(new Map());
24
+ const pending = React.useRef(false);
25
+ const search = React.useRef({ text: "", at: 0 });
26
+ React.useLayoutEffect(() => {
27
+ if (!pending.current)
28
+ return;
29
+ pending.current = false;
30
+ tiles.current.get(index)?.focus();
31
+ });
32
+ const columns = () => {
33
+ const template = list.current ? getComputedStyle(list.current).gridTemplateColumns : "";
34
+ return Math.max(1, template.split(" ").filter(Boolean).length);
35
+ };
36
+ const onKeyDown = (event) => {
37
+ const meta = event.metaKey || event.ctrlKey;
38
+ const go = (target) => {
39
+ event.preventDefault();
40
+ const next = Math.min(Math.max(target, 0), items.length - 1);
41
+ pending.current = true;
42
+ setFocusIndex(next);
43
+ const item = items[next];
44
+ if (item && !meta)
45
+ model.select(item.path, event.shiftKey ? "range" : "replace");
46
+ };
47
+ switch (event.key) {
48
+ case "ArrowRight":
49
+ return go(index + 1);
50
+ case "ArrowLeft":
51
+ return go(index - 1);
52
+ case "ArrowDown":
53
+ return go(index + columns());
54
+ case "ArrowUp":
55
+ return go(index - columns());
56
+ case "Home":
57
+ return go(0);
58
+ case "End":
59
+ return go(items.length - 1);
60
+ }
61
+ const item = items[index];
62
+ if (!item)
63
+ return;
64
+ if (event.key === "Enter") {
65
+ event.preventDefault();
66
+ onOpen(item);
67
+ }
68
+ else if (event.key === " ") {
69
+ event.preventDefault();
70
+ model.select(item.path, meta ? "toggle" : event.shiftKey ? "range" : "replace");
71
+ }
72
+ else if (meta && event.key.toLowerCase() === "a") {
73
+ event.preventDefault();
74
+ model.selectAll();
75
+ }
76
+ else if (event.key === "Escape" && selection.size > 0) {
77
+ event.preventDefault();
78
+ model.clear();
79
+ }
80
+ else if (onDelete && (event.key === "Delete" || (meta && event.key === "Backspace"))) {
81
+ const targets = items.filter((i) => selection.has(i.path));
82
+ if (targets.length) {
83
+ event.preventDefault();
84
+ onDelete(targets);
85
+ }
86
+ }
87
+ else if (onRename && event.key === "F2") {
88
+ event.preventDefault();
89
+ onRename(item);
90
+ }
91
+ else {
92
+ const next = typeahead(search.current, event, items.map((i) => i.name), index);
93
+ if (next !== null)
94
+ go(next);
95
+ }
96
+ };
97
+ return (_jsx("div", { ref: list, role: "listbox", "aria-label": label, "aria-multiselectable": true, onKeyDown: onKeyDown, className: cn("grid grid-cols-[repeat(auto-fill,minmax(6.5rem,1fr))] content-start gap-1 p-2", className), children: items.map((item, i) => (_jsx(Tile, { item: item, selected: selection.has(item.path), tabbable: i === index, tileRef: (el) => {
98
+ if (el)
99
+ tiles.current.set(i, el);
100
+ else
101
+ tiles.current.delete(i);
102
+ }, onFocus: () => setFocusIndex(i), onMove: onMove, dragPaths: selection.has(item.path) ? order.filter((p) => selection.has(p)) : [item.path], dragLabel: (n) => (n === 1 ? item.name : t.itemCount(n)), thumbnail: renderThumbnail?.(item), onClick: (event) => {
103
+ setFocusIndex(i);
104
+ model.select(item.path, modeOf(event));
105
+ }, onDoubleClick: () => onOpen(item), onContextMenu: () => {
106
+ // Right-clicking outside the selection selects the item, as file managers do.
107
+ if (!selection.has(item.path))
108
+ model.select(item.path, "replace");
109
+ } }, item.path))) }));
110
+ }
111
+ function Tile({ item, selected, tabbable, tileRef, onFocus, onMove, dragPaths, dragLabel, thumbnail, onClick, onDoubleClick, onContextMenu, }) {
112
+ const drop = useFileDrop(item.kind === "directory" ? item.path : null, onMove);
113
+ return (_jsxs("div", { ref: tileRef, role: "option", "data-path": item.path, "aria-selected": selected, tabIndex: tabbable ? 0 : -1, title: item.name, draggable: Boolean(onMove), onDragStart: onMove ? (event) => startFileDrag(event, dragPaths, dragLabel(dragPaths.length)) : undefined, ...drop.props, onFocus: onFocus, onClick: onClick, onDoubleClick: onDoubleClick, onContextMenu: onContextMenu, className: cn("flex cursor-default flex-col items-center gap-1.5 rounded-lg p-2 text-center outline-none select-none hover:bg-accent/60 focus-visible:ring-2 focus-visible:ring-ring/50", selected && "bg-accent text-accent-foreground hover:bg-accent", drop.over && "bg-primary/10 ring-2 ring-primary"), children: [_jsx("span", { className: "flex size-12 items-center justify-center overflow-hidden rounded-lg", children: thumbnail ?? _jsx(FileIcon, { name: item.name, kind: item.kind, className: "size-8" }) }), _jsx("span", { className: "line-clamp-2 w-full text-xs [overflow-wrap:anywhere]", children: item.name })] }));
114
+ }
@@ -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;