@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,11 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { ScrollArea as ScrollAreaPrimitive } from "radix-ui";
4
+ import { cn } from "../lib/cn.js";
5
+ /** A scrolling box with thin, token-coloured bars that look the same on every platform. */
6
+ export function ScrollArea({ className, viewportClassName, children, label, orientation = "vertical", ...props }) {
7
+ return (_jsxs(ScrollAreaPrimitive.Root, { "data-slot": "scroll-area", className: cn("relative overflow-hidden", className), ...props, children: [_jsx(ScrollAreaPrimitive.Viewport, { "data-slot": "scroll-area-viewport", className: cn("size-full rounded-[inherit] outline-none focus-visible:ring-2 focus-visible:ring-ring/50", viewportClassName), ...(label ? { role: "region", "aria-label": label, tabIndex: 0 } : {}), children: children }), orientation !== "horizontal" && _jsx(ScrollBar, { orientation: "vertical" }), orientation !== "vertical" && _jsx(ScrollBar, { orientation: "horizontal" }), _jsx(ScrollAreaPrimitive.Corner, {})] }));
8
+ }
9
+ export function ScrollBar({ className, orientation = "vertical", ...props }) {
10
+ return (_jsx(ScrollAreaPrimitive.ScrollAreaScrollbar, { "data-slot": "scroll-area-scrollbar", orientation: orientation, className: cn("flex touch-none p-px transition-colors select-none", orientation === "vertical" && "h-full w-2 border-l border-l-transparent", orientation === "horizontal" && "h-2 flex-col border-t border-t-transparent", className), ...props, children: _jsx(ScrollAreaPrimitive.ScrollAreaThumb, { "data-slot": "scroll-area-thumb", className: "relative flex-1 rounded-full bg-border hover:bg-muted-foreground/60" }) }));
11
+ }
@@ -0,0 +1,37 @@
1
+ import * as React from "react";
2
+ import type { StatusTone } from "../status/tones.js";
3
+ export interface ToastOptions {
4
+ /** Replaces the toast with the same id (an upload's progress, then its result). */
5
+ id?: string;
6
+ title: string;
7
+ description?: string;
8
+ /** A status tone for the dot beside the title. `error` toasts are announced assertively. */
9
+ tone?: StatusTone;
10
+ /** One follow-up action ("Undo", "View"). `altText` says how to reach it without the toast. */
11
+ action?: {
12
+ label: string;
13
+ altText: string;
14
+ onClick: () => void;
15
+ };
16
+ /** Milliseconds before it closes itself. Defaults to the provider's (5s); `Infinity` keeps it. */
17
+ duration?: number;
18
+ }
19
+ export interface ToastApi {
20
+ /** Shows a toast and returns its id. */
21
+ toast: (options: ToastOptions) => string;
22
+ dismiss: (id: string) => void;
23
+ }
24
+ export interface ToastProviderProps {
25
+ children: React.ReactNode;
26
+ /** Defaults to 5000. */
27
+ duration?: number;
28
+ className?: string;
29
+ }
30
+ /**
31
+ * Transient notices, bottom right, on Radix Toast: they pause on hover and
32
+ * focus, F8 jumps to them, and swiping or Escape closes one. Wrap the app
33
+ * once and call `useToast().toast(...)` anywhere under it.
34
+ */
35
+ export declare function ToastProvider({ children, duration, className }: ToastProviderProps): React.JSX.Element;
36
+ /** The toast API of the nearest ToastProvider. Throws outside one, so a missing provider is loud. */
37
+ export declare function useToast(): ToastApi;
@@ -0,0 +1,43 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { XIcon } from "lucide-react";
4
+ import { Toast as ToastPrimitive } from "radix-ui";
5
+ import * as React from "react";
6
+ import { cn } from "../lib/cn.js";
7
+ import { useUI } from "../provider/UIProvider.js";
8
+ import { StatusDot } from "../status/StatusDot.js";
9
+ const ToastContext = React.createContext(null);
10
+ /**
11
+ * Transient notices, bottom right, on Radix Toast: they pause on hover and
12
+ * focus, F8 jumps to them, and swiping or Escape closes one. Wrap the app
13
+ * once and call `useToast().toast(...)` anywhere under it.
14
+ */
15
+ export function ToastProvider({ children, duration = 5000, className }) {
16
+ const { messages } = useUI();
17
+ const [toasts, setToasts] = React.useState([]);
18
+ const counter = React.useRef(0);
19
+ const api = React.useMemo(() => ({
20
+ toast: (options) => {
21
+ const id = options.id ?? `toast-${++counter.current}`;
22
+ setToasts((list) => {
23
+ const entry = { ...options, id };
24
+ return list.some((t) => t.id === id)
25
+ ? list.map((t) => (t.id === id ? entry : t))
26
+ : [...list, entry];
27
+ });
28
+ return id;
29
+ },
30
+ dismiss: (id) => setToasts((list) => list.filter((t) => t.id !== id)),
31
+ }), []);
32
+ return (_jsx(ToastContext.Provider, { value: api, children: _jsxs(ToastPrimitive.Provider, { duration: duration, label: messages.toast.region, children: [children, toasts.map((t) => (_jsxs(ToastPrimitive.Root, { "data-slot": "toast", "data-tone": t.tone, open: true, duration: t.duration, type: t.tone === "error" ? "foreground" : "background", onOpenChange: (open) => {
33
+ if (!open)
34
+ api.dismiss(t.id);
35
+ }, className: "pointer-events-auto relative flex w-full items-start gap-2.5 rounded-lg bg-popover p-3 pr-9 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 data-[state=open]:animate-cui-fade-in data-[swipe=end]:translate-x-(--radix-toast-swipe-end-x) data-[swipe=move]:translate-x-(--radix-toast-swipe-move-x)", children: [t.tone && _jsx(StatusDot, { tone: t.tone, className: "mt-1.5" }), _jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [_jsx(ToastPrimitive.Title, { className: "font-medium [overflow-wrap:anywhere]", children: t.title }), t.description && (_jsx(ToastPrimitive.Description, { className: "[overflow-wrap:anywhere] text-muted-foreground", children: t.description })), t.action && (_jsx(ToastPrimitive.Action, { altText: t.action.altText, onClick: t.action.onClick, className: "mt-1 w-fit rounded-lg text-sm font-medium text-primary underline-offset-4 outline-none hover:underline focus-visible:ring-2 focus-visible:ring-ring/50", children: t.action.label }))] }), _jsx(ToastPrimitive.Close, { "aria-label": messages.toast.close, className: "absolute top-2 right-2 inline-flex size-6 items-center justify-center rounded-lg text-muted-foreground outline-none hover:bg-accent hover:text-accent-foreground focus-visible:ring-2 focus-visible:ring-ring/50", children: _jsx(XIcon, { className: "size-3.5", "aria-hidden": true }) })] }, t.id))), _jsx(ToastPrimitive.Viewport, { "data-slot": "toast-viewport", className: cn("fixed right-0 bottom-0 z-50 m-0 flex w-full max-w-sm list-none flex-col gap-2 p-4 outline-none", className) })] }) }));
36
+ }
37
+ /** The toast API of the nearest ToastProvider. Throws outside one, so a missing provider is loud. */
38
+ export function useToast() {
39
+ const api = React.useContext(ToastContext);
40
+ if (!api)
41
+ throw new Error("useToast needs a ToastProvider above it");
42
+ return api;
43
+ }
@@ -0,0 +1,16 @@
1
+ import { Tooltip as TooltipPrimitive } from "radix-ui";
2
+ import type * as React from "react";
3
+ /**
4
+ * Shares one open delay across the tooltips under it. A Tooltip outside a
5
+ * provider makes its own, so the provider is optional; an app with many
6
+ * icon buttons wraps its shell once.
7
+ */
8
+ export declare function TooltipProvider({ delayDuration, ...props }: React.ComponentProps<typeof TooltipPrimitive.Provider>): React.JSX.Element;
9
+ /**
10
+ * A short hint on hover and keyboard focus. It names nothing: an icon-only
11
+ * control still needs its own `aria-label`; the tooltip repeats it for
12
+ * sighted pointer users.
13
+ */
14
+ export declare function Tooltip(props: React.ComponentProps<typeof TooltipPrimitive.Root>): React.JSX.Element;
15
+ export declare function TooltipTrigger(props: React.ComponentProps<typeof TooltipPrimitive.Trigger>): React.JSX.Element;
16
+ export declare function TooltipContent({ className, sideOffset, children, ...props }: React.ComponentProps<typeof TooltipPrimitive.Content>): React.JSX.Element;
@@ -0,0 +1,26 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { Tooltip as TooltipPrimitive } from "radix-ui";
4
+ import { cn } from "../lib/cn.js";
5
+ /**
6
+ * Shares one open delay across the tooltips under it. A Tooltip outside a
7
+ * provider makes its own, so the provider is optional; an app with many
8
+ * icon buttons wraps its shell once.
9
+ */
10
+ export function TooltipProvider({ delayDuration = 300, ...props }) {
11
+ return (_jsx(TooltipPrimitive.Provider, { "data-slot": "tooltip-provider", delayDuration: delayDuration, ...props }));
12
+ }
13
+ /**
14
+ * A short hint on hover and keyboard focus. It names nothing: an icon-only
15
+ * control still needs its own `aria-label`; the tooltip repeats it for
16
+ * sighted pointer users.
17
+ */
18
+ export function Tooltip(props) {
19
+ return (_jsx(TooltipProvider, { children: _jsx(TooltipPrimitive.Root, { "data-slot": "tooltip", ...props }) }));
20
+ }
21
+ export function TooltipTrigger(props) {
22
+ return _jsx(TooltipPrimitive.Trigger, { "data-slot": "tooltip-trigger", ...props });
23
+ }
24
+ export function TooltipContent({ className, sideOffset = 4, children, ...props }) {
25
+ return (_jsx(TooltipPrimitive.Portal, { children: _jsx(TooltipPrimitive.Content, { "data-slot": "tooltip-content", sideOffset: sideOffset, className: cn("z-50 w-fit max-w-xs origin-(--radix-tooltip-content-transform-origin) rounded-lg bg-foreground px-2 py-1 text-xs text-balance text-background data-[state=delayed-open]:animate-cui-fade-in", className), ...props, children: children }) }));
26
+ }
@@ -1,5 +1,6 @@
1
1
  import type { ProductTheme } from "../appearance/product.js";
2
2
  import type { ChatRole, ChatRunStatus, ChatToolCallStatus } from "../chat/model.js";
3
+ import type { UploadState } from "../workbench/model.js";
3
4
  import type { WorkflowQuestionState } from "../workflow/question.js";
4
5
  import type { WorkflowAction, WorkflowAvailability, WorkflowScope, WorkflowStatus } from "../workflow/model.js";
5
6
  import type { ArtifactStatus, GateStatus, ReviewDecision, ReviewVerdict, RunStageStatus } from "../workflow/run.js";
@@ -103,6 +104,66 @@ export interface Messages {
103
104
  dialog: {
104
105
  close: string;
105
106
  };
107
+ workbench: {
108
+ /** The file browser's name. */
109
+ files: string;
110
+ /** The folder tree's name. */
111
+ folders: string;
112
+ /** The root directory in the breadcrumb. */
113
+ root: string;
114
+ /** The breadcrumb's landmark name. */
115
+ location: string;
116
+ /** A listing's accessible name: "Files in reports". */
117
+ filesIn: (folder: string) => string;
118
+ view: string;
119
+ listView: string;
120
+ gridView: string;
121
+ name: string;
122
+ size: string;
123
+ modified: string;
124
+ /** Read for a folder's or an unknown size's dash. */
125
+ noSize: string;
126
+ loading: string;
127
+ loadFailed: string;
128
+ retry: string;
129
+ /** A tree folder whose children failed to load; activating it retries. */
130
+ retryFolder: string;
131
+ empty: string;
132
+ emptyDescription: string;
133
+ noFolders: string;
134
+ itemCount: (count: number) => string;
135
+ selected: (count: number) => string;
136
+ /** The overlay while files from the desktop hover over the browser. */
137
+ dropToUpload: string;
138
+ dropHere: string;
139
+ browse: string;
140
+ maxSize: (size: string) => string;
141
+ /** The upload list's name. */
142
+ uploads: string;
143
+ uploading: (count: number) => string;
144
+ uploadsDone: string;
145
+ uploadState: (state: UploadState) => string;
146
+ cancelUpload: (name: string) => string;
147
+ dismissUpload: (name: string) => string;
148
+ previewOf: (name: string) => string;
149
+ previewLoading: string;
150
+ previewFailed: string;
151
+ noPreview: string;
152
+ closePreview: string;
153
+ /** A text preview's scrolling region. */
154
+ contentsOf: (name: string) => string;
155
+ /** A truncated preview: "Showing 100 of 4,210 rows". */
156
+ showing: (shown: number, total: number | null) => string;
157
+ partial: string;
158
+ /** A CSV column with no header. */
159
+ column: (index: number) => string;
160
+ noRows: string;
161
+ };
162
+ toast: {
163
+ /** The notifications region; Radix fills `{hotkey}` with the shortcut that focuses it. */
164
+ region: string;
165
+ close: string;
166
+ };
106
167
  confirm: {
107
168
  confirm: string;
108
169
  cancel: string;
@@ -615,6 +676,14 @@ export interface Messages {
615
676
  waitingOnYou: string;
616
677
  /** The shortcut as people read it, from the key list ("⌘/", "Ctrl+/"). */
617
678
  shortcut: (keys: string) => string;
679
+ /** A question from the agent that is not a tool call. */
680
+ interrupt: string;
681
+ decline: string;
682
+ back: string;
683
+ /** Answers an artifact's form review. */
684
+ sendReview: string;
685
+ /** The shared state's section. */
686
+ sharedState: string;
618
687
  };
619
688
  }
620
689
  export declare const defaultMessages: Messages;
@@ -236,6 +236,61 @@ export const defaultMessages = {
236
236
  dialog: {
237
237
  close: "Close",
238
238
  },
239
+ workbench: {
240
+ files: "Files",
241
+ folders: "Folders",
242
+ root: "Home",
243
+ location: "Location",
244
+ filesIn: (folder) => `Files in ${folder}`,
245
+ view: "View",
246
+ listView: "List",
247
+ gridView: "Grid",
248
+ name: "Name",
249
+ size: "Size",
250
+ modified: "Modified",
251
+ noSize: "No size",
252
+ loading: "Loading files",
253
+ loadFailed: "Could not load this folder",
254
+ retry: "Retry",
255
+ retryFolder: "Could not load. Retry",
256
+ empty: "This folder is empty",
257
+ emptyDescription: "Drop files here to upload them.",
258
+ noFolders: "No folders",
259
+ itemCount: (count) => `${count.toLocaleString()} ${count === 1 ? "item" : "items"}`,
260
+ selected: (count) => `${count.toLocaleString()} selected`,
261
+ dropToUpload: "Drop files to upload",
262
+ dropHere: "Drop files here",
263
+ browse: "Choose files",
264
+ maxSize: (size) => `Up to ${size} each`,
265
+ uploads: "Uploads",
266
+ uploading: (count) => `Uploading ${count.toLocaleString()} ${count === 1 ? "file" : "files"}`,
267
+ uploadsDone: "Uploads finished",
268
+ uploadState: (state) => ({
269
+ queued: "Waiting",
270
+ uploading: "Uploading",
271
+ done: "Done",
272
+ error: "Failed",
273
+ canceled: "Cancelled",
274
+ })[state],
275
+ cancelUpload: (name) => `Cancel upload of ${name}`,
276
+ dismissUpload: (name) => `Clear ${name}`,
277
+ previewOf: (name) => `Preview of ${name}`,
278
+ previewLoading: "Loading preview",
279
+ previewFailed: "Could not preview this file",
280
+ noPreview: "No preview for this file",
281
+ closePreview: "Close preview",
282
+ contentsOf: (name) => `Contents of ${name}`,
283
+ showing: (shown, total) => total === null
284
+ ? `Showing the first ${shown.toLocaleString()}`
285
+ : `Showing ${shown.toLocaleString()} of ${total.toLocaleString()}`,
286
+ partial: "Showing the start of the file",
287
+ column: (index) => `Column ${index}`,
288
+ noRows: "No rows",
289
+ },
290
+ toast: {
291
+ region: "Notifications ({hotkey})",
292
+ close: "Dismiss notification",
293
+ },
239
294
  confirm: {
240
295
  confirm: "Confirm",
241
296
  cancel: "Cancel",
@@ -851,6 +906,11 @@ export const defaultMessages = {
851
906
  emptyDescription: "Ask about what is on this page, or pick a starting point.",
852
907
  waitingOnYou: "Answer the card above to continue.",
853
908
  shortcut: (keys) => `Shortcut ${keys}`,
909
+ interrupt: "The agent is waiting for you",
910
+ decline: "Decline",
911
+ back: "Back",
912
+ sendReview: "Send review",
913
+ sharedState: "Shared state",
854
914
  },
855
915
  };
856
916
  export function mergeMessages(overrides) {
@@ -1,5 +1,5 @@
1
1
  import { BOOT_SOURCE, bootConfig } from "../appearance/boot.js";
2
- import { astroliftBrand, chatStudioBrand, cykickBrand, loadrBrand, workbenchBrand, zentinelleBrand, } from "../appearance/brands.js";
2
+ import { astroliftBrand, chatStudioBrand, cykickBrand, deepDataAgentBrand, loadrBrand, workbenchBrand, zentinelleBrand, } from "../appearance/brands.js";
3
3
  import { CORNERS, defineProduct } from "../appearance/product.js";
4
4
  import { accentDeclarations, block, renderThemeCss } from "./css.js";
5
5
  /**
@@ -16,6 +16,7 @@ export const CSS_LAYER_PRODUCTS = [
16
16
  chatStudioBrand,
17
17
  loadrBrand,
18
18
  cykickBrand,
19
+ deepDataAgentBrand,
19
20
  ].map((config) => defineProduct(config));
20
21
  const TOKENS_HEADER = `/*
21
22
  * @calliopelabs/ui CSS layer: tokens.
@@ -349,5 +349,8 @@ export declare const TOKENS: {
349
349
  readonly onLight: "#ffffff";
350
350
  };
351
351
  };
352
+ readonly "deep-data-agent": {
353
+ readonly sidebar: "#1a150c";
354
+ };
352
355
  };
353
356
  };
@@ -352,6 +352,9 @@ export const TOKENS = {
352
352
  "light": "#0e7490",
353
353
  "onLight": "#ffffff"
354
354
  }
355
+ },
356
+ "deep-data-agent": {
357
+ "sidebar": "#1a150c"
355
358
  }
356
359
  }
357
360
  };
@@ -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
+ }