@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,21 @@
1
+ import * as React from "react";
2
+ import { type CodeEditorProps } from "./CodeEditor.js";
3
+ import { type SqlSchema, type SqlSchemaRequest } from "./sql-completion.js";
4
+ export interface SqlEditorProps extends Omit<CodeEditorProps, "language"> {
5
+ /** What completion knows: tables, their columns, schemas, other databases. */
6
+ schema?: SqlSchema;
7
+ /**
8
+ * Completion reached something the schema lacks (a table's columns, another
9
+ * database's tables). Load it and pass a new `schema`; each request is made
10
+ * once per schema.
11
+ */
12
+ onSchemaRequest?: (request: SqlSchemaRequest) => void;
13
+ }
14
+ /**
15
+ * A SQL editor: `CodeEditor` with schema-aware completion (tables after FROM
16
+ * and JOIN, columns after SELECT and WHERE, `alias.` resolved against the
17
+ * statement under the cursor, `schema.` and `db.` qualifiers, window-function
18
+ * snippets). The schema is the host's; completion never waits for it, and
19
+ * asks for what it lacks through `onSchemaRequest`.
20
+ */
21
+ export declare function SqlEditor({ schema, onSchemaRequest, onMount, ...props }: SqlEditorProps): React.JSX.Element;
@@ -0,0 +1,79 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { CodeEditor, } from "./CodeEditor.js";
5
+ import { sqlCompletions, } from "./sql-completion.js";
6
+ const requestKey = (r) => r.kind === "columns" ? `columns:${r.schema ?? ""}.${r.table}` : `database:${r.database}`;
7
+ function completionKind(monaco, kind) {
8
+ const K = monaco.languages.CompletionItemKind;
9
+ return {
10
+ keyword: K.Keyword,
11
+ function: K.Function,
12
+ snippet: K.Snippet,
13
+ table: K.Struct,
14
+ column: K.Field,
15
+ schema: K.Module,
16
+ database: K.Module,
17
+ }[kind];
18
+ }
19
+ /**
20
+ * A SQL editor: `CodeEditor` with schema-aware completion (tables after FROM
21
+ * and JOIN, columns after SELECT and WHERE, `alias.` resolved against the
22
+ * statement under the cursor, `schema.` and `db.` qualifiers, window-function
23
+ * snippets). The schema is the host's; completion never waits for it, and
24
+ * asks for what it lacks through `onSchemaRequest`.
25
+ */
26
+ export function SqlEditor({ schema, onSchemaRequest, onMount, ...props }) {
27
+ const latest = React.useRef({ schema, onSchemaRequest, onMount });
28
+ const asked = React.useRef(new Set());
29
+ React.useLayoutEffect(() => {
30
+ if (latest.current.schema !== schema)
31
+ asked.current.clear();
32
+ latest.current = { schema, onSchemaRequest, onMount };
33
+ });
34
+ const mount = React.useCallback((editor, monaco) => {
35
+ // Providers are per language, page-wide: answer only for this editor's model.
36
+ const provider = monaco.languages.registerCompletionItemProvider("sql", {
37
+ triggerCharacters: ["."],
38
+ provideCompletionItems(model, position) {
39
+ if (model !== editor.getModel())
40
+ return { suggestions: [] };
41
+ const { items, requests } = sqlCompletions(model.getValue(), model.getOffsetAt(position), latest.current.schema ?? { tables: [] });
42
+ for (const request of requests) {
43
+ const key = requestKey(request);
44
+ if (asked.current.has(key))
45
+ continue;
46
+ asked.current.add(key);
47
+ latest.current.onSchemaRequest?.(request);
48
+ }
49
+ const word = model.getWordUntilPosition(position);
50
+ const range = {
51
+ startLineNumber: position.lineNumber,
52
+ startColumn: word.startColumn,
53
+ endLineNumber: position.lineNumber,
54
+ endColumn: word.endColumn,
55
+ };
56
+ return {
57
+ suggestions: items.map((item) => ({
58
+ label: item.label,
59
+ kind: completionKind(monaco, item.kind),
60
+ detail: item.detail,
61
+ insertText: item.insertText,
62
+ insertTextRules: item.snippet
63
+ ? monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet
64
+ : undefined,
65
+ sortText: item.sortText,
66
+ range,
67
+ })),
68
+ };
69
+ },
70
+ });
71
+ const userCleanup = latest.current.onMount?.(editor, monaco);
72
+ return () => {
73
+ provider.dispose();
74
+ if (typeof userCleanup === "function")
75
+ userCleanup();
76
+ };
77
+ }, []);
78
+ return _jsx(CodeEditor, { ...props, language: "sql", onMount: mount });
79
+ }
@@ -0,0 +1,28 @@
1
+ import type * as React from "react";
2
+ /**
3
+ * The bar's tone. Neutral is the chrome; the others paint it with a status
4
+ * colour, so "which environment am I pointed at" shows before it is asked
5
+ * (a production connection in the error tone).
6
+ */
7
+ export type StatusBarTone = "neutral" | "ok" | "warn" | "error" | "info";
8
+ export interface StatusBarProps {
9
+ /** Left: connection, policy, running tasks. */
10
+ start?: React.ReactNode;
11
+ /** Right: readiness, appearance toggle, version. */
12
+ end?: React.ReactNode;
13
+ tone?: StatusBarTone;
14
+ className?: string;
15
+ }
16
+ /** The thin bar across the bottom of an IDE layout: a labelled region of `StatusBarItem`s. */
17
+ export declare function StatusBar({ start, end, tone, className }: StatusBarProps): React.JSX.Element;
18
+ export interface StatusBarItemProps {
19
+ icon?: React.ReactNode;
20
+ children?: React.ReactNode;
21
+ /** Makes it a button. */
22
+ onClick?: () => void;
23
+ /** Hover text; also the name of an icon-only button. */
24
+ title?: string;
25
+ className?: string;
26
+ }
27
+ /** One entry of the status bar: text with an icon, or a button when it acts. */
28
+ export declare function StatusBarItem({ icon, children, onClick, title, className }: StatusBarItemProps): React.JSX.Element;
@@ -0,0 +1,26 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { cn } from "../lib/cn.js";
4
+ import { useUI } from "../provider/UIProvider.js";
5
+ import { PILL_TONE } from "../status/tones.js";
6
+ const TONE = {
7
+ neutral: "bg-surface-1 text-muted-foreground",
8
+ ok: PILL_TONE.ok,
9
+ warn: PILL_TONE.warn,
10
+ error: PILL_TONE.error,
11
+ info: PILL_TONE.info,
12
+ };
13
+ /** The thin bar across the bottom of an IDE layout: a labelled region of `StatusBarItem`s. */
14
+ export function StatusBar({ start, end, tone = "neutral", className }) {
15
+ const { messages } = useUI();
16
+ return (_jsxs("section", { "aria-label": messages.workbench.statusBar, "data-tone": tone, className: cn("flex h-6 shrink-0 items-center justify-between gap-3 border-t px-2 text-xs select-none", TONE[tone], className), children: [_jsx("div", { className: "flex min-w-0 items-center gap-3", children: start }), _jsx("div", { className: "flex min-w-0 items-center gap-3", children: end })] }));
17
+ }
18
+ /** One entry of the status bar: text with an icon, or a button when it acts. */
19
+ export function StatusBarItem({ icon, children, onClick, title, className }) {
20
+ const body = (_jsxs(_Fragment, { children: [icon && (_jsx("span", { "aria-hidden": true, className: "flex [&_svg]:size-3", children: icon })), children] }));
21
+ const base = cn("flex min-w-0 items-center gap-1 truncate", className);
22
+ if (onClick) {
23
+ return (_jsx("button", { type: "button", title: title, "aria-label": children ? undefined : title, onClick: onClick, className: cn(base, "rounded-lg px-1 outline-hidden hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"), children: body }));
24
+ }
25
+ return (_jsx("span", { title: title, className: base, children: body }));
26
+ }
@@ -0,0 +1,11 @@
1
+ import type * as React from "react";
2
+ export interface TruncationBannerProps {
3
+ /** One sentence naming the cap ("Showing 1,000 of 48,211 rows"). */
4
+ children: React.ReactNode;
5
+ className?: string;
6
+ }
7
+ /**
8
+ * One strip, one sentence: this view is not showing everything. The warn
9
+ * tone: a capped result is expected, not a failure, and must not read as one.
10
+ */
11
+ export declare function TruncationBanner({ children, className }: TruncationBannerProps): React.JSX.Element;
@@ -0,0 +1,12 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { InfoIcon } from "lucide-react";
4
+ import { cn } from "../lib/cn.js";
5
+ import { PILL_TONE } from "../status/tones.js";
6
+ /**
7
+ * One strip, one sentence: this view is not showing everything. The warn
8
+ * tone: a capped result is expected, not a failure, and must not read as one.
9
+ */
10
+ export function TruncationBanner({ children, className }) {
11
+ return (_jsxs("p", { role: "status", "data-slot": "truncation-banner", className: cn("flex shrink-0 items-center gap-2 border-x-0 border-t-0 border-b px-3 py-1 text-xs", PILL_TONE.warn, className), children: [_jsx(InfoIcon, { "aria-hidden": true, className: "size-3 shrink-0" }), _jsx("span", { className: "min-w-0", children: children })] }));
12
+ }
@@ -0,0 +1,52 @@
1
+ import * as React from "react";
2
+ import { type UploadState } from "./model.js";
3
+ /** One upload as the host tracks it. */
4
+ export interface UploadItem {
5
+ id: string;
6
+ name: string;
7
+ size?: number | null;
8
+ /** 0 to 100; null or absent while the size of the work is unknown. */
9
+ progress?: number | null;
10
+ state: UploadState;
11
+ /** Why it failed, for `error`. */
12
+ error?: string;
13
+ }
14
+ export interface UploadRejection {
15
+ file: File;
16
+ reason: "too-large" | "type";
17
+ }
18
+ export interface UploadDropzoneProps {
19
+ /** Files dropped or picked that pass `accept` and `maxSize`. The host uploads them. */
20
+ onFiles: (files: File[]) => void;
21
+ /** Files refused by `accept` or `maxSize`. */
22
+ onReject?: (rejections: UploadRejection[]) => void;
23
+ /** As the file input's `accept`: extensions (`.csv`) and MIME types (`image/*`), comma-separated. */
24
+ accept?: string;
25
+ /** Bytes. */
26
+ maxSize?: number;
27
+ multiple?: boolean;
28
+ disabled?: boolean;
29
+ /** The uploads to show, with their progress. */
30
+ uploads?: readonly UploadItem[];
31
+ /** Cancel a queued or running upload. */
32
+ onCancel?: (id: string) => void;
33
+ /** Clear a finished, failed or cancelled upload from the list. */
34
+ onDismiss?: (id: string) => void;
35
+ /**
36
+ * The area to drop onto (a whole file browser): the dropzone wraps it,
37
+ * draws an overlay while files hover, and floats the progress in a
38
+ * corner. Without children it draws its own box with a browse button.
39
+ */
40
+ children?: React.ReactNode;
41
+ /** The box's prompt. Defaults to "Drop files here". */
42
+ label?: string;
43
+ className?: string;
44
+ }
45
+ /**
46
+ * Drop files from the desktop, or pick them, and see each upload's
47
+ * progress. Host-driven: it hands the files to `onFiles` and draws the
48
+ * `uploads` the host reports, so any transport (chunked, presigned, a
49
+ * mutation) fits. Drags between the browser's own views are not files and
50
+ * pass through.
51
+ */
52
+ export declare function UploadDropzone({ onFiles, onReject, accept, maxSize, multiple, disabled, uploads, onCancel, onDismiss, children, label, className, }: UploadDropzoneProps): React.JSX.Element;
@@ -0,0 +1,115 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { CheckIcon, UploadIcon, XIcon } from "lucide-react";
4
+ import * as React from "react";
5
+ import { cn } from "../lib/cn.js";
6
+ import { Button } from "../primitives/Button.js";
7
+ import { useUI } from "../provider/UIProvider.js";
8
+ import { formatBytes } from "./model.js";
9
+ function accepts(file, accept) {
10
+ if (!accept)
11
+ return true;
12
+ const name = file.name.toLowerCase();
13
+ const type = file.type.toLowerCase();
14
+ return accept
15
+ .split(",")
16
+ .map((part) => part.trim().toLowerCase())
17
+ .filter(Boolean)
18
+ .some((rule) => rule.startsWith(".")
19
+ ? name.endsWith(rule)
20
+ : rule.endsWith("/*")
21
+ ? type.startsWith(rule.slice(0, -1))
22
+ : type === rule);
23
+ }
24
+ const hasFiles = (event) => event.dataTransfer.types.includes("Files");
25
+ /**
26
+ * Drop files from the desktop, or pick them, and see each upload's
27
+ * progress. Host-driven: it hands the files to `onFiles` and draws the
28
+ * `uploads` the host reports, so any transport (chunked, presigned, a
29
+ * mutation) fits. Drags between the browser's own views are not files and
30
+ * pass through.
31
+ */
32
+ export function UploadDropzone({ onFiles, onReject, accept, maxSize, multiple = true, disabled = false, uploads = [], onCancel, onDismiss, children, label, className, }) {
33
+ const { messages } = useUI();
34
+ const t = messages.workbench;
35
+ const input = React.useRef(null);
36
+ const depth = React.useRef(0);
37
+ const [over, setOver] = React.useState(false);
38
+ const overlay = children !== undefined;
39
+ const take = (list) => {
40
+ if (!list || disabled)
41
+ return;
42
+ const files = Array.from(list).slice(0, multiple ? undefined : 1);
43
+ const ok = [];
44
+ const rejected = [];
45
+ for (const file of files) {
46
+ if (!accepts(file, accept))
47
+ rejected.push({ file, reason: "type" });
48
+ else if (maxSize !== undefined && file.size > maxSize)
49
+ rejected.push({ file, reason: "too-large" });
50
+ else
51
+ ok.push(file);
52
+ }
53
+ if (ok.length)
54
+ onFiles(ok);
55
+ if (rejected.length)
56
+ onReject?.(rejected);
57
+ };
58
+ const dropProps = disabled
59
+ ? {}
60
+ : {
61
+ onDragEnter: (event) => {
62
+ if (!hasFiles(event))
63
+ return;
64
+ event.preventDefault();
65
+ depth.current++;
66
+ setOver(true);
67
+ },
68
+ onDragOver: (event) => {
69
+ if (!hasFiles(event))
70
+ return;
71
+ event.preventDefault();
72
+ event.dataTransfer.dropEffect = "copy";
73
+ },
74
+ onDragLeave: (event) => {
75
+ if (!hasFiles(event))
76
+ return;
77
+ depth.current = Math.max(0, depth.current - 1);
78
+ if (depth.current === 0)
79
+ setOver(false);
80
+ },
81
+ onDrop: (event) => {
82
+ if (!hasFiles(event))
83
+ return;
84
+ event.preventDefault();
85
+ depth.current = 0;
86
+ setOver(false);
87
+ take(event.dataTransfer.files);
88
+ },
89
+ };
90
+ const picker = (_jsx("input", { ref: input, type: "file", hidden: true, tabIndex: -1, accept: accept, multiple: multiple, onChange: (event) => {
91
+ take(event.target.files);
92
+ event.target.value = "";
93
+ } }));
94
+ const progress = uploads.length > 0 && (_jsx(UploadList, { uploads: uploads, onCancel: onCancel, onDismiss: onDismiss, className: overlay ? "absolute right-3 bottom-3 z-20 w-72 shadow-md" : "mt-3" }));
95
+ if (overlay) {
96
+ return (_jsxs("div", { "data-slot": "upload-dropzone", ...dropProps, className: cn("relative flex min-h-0 flex-col", className), children: [children, picker, over && (_jsx("div", { "aria-hidden": true, className: "pointer-events-none absolute inset-0 z-30 flex items-center justify-center rounded-lg border-2 border-dashed border-primary bg-primary/10", children: _jsxs("span", { className: "flex flex-col items-center gap-2 rounded-lg bg-background/80 px-4 py-3 font-medium text-primary", children: [_jsx(UploadIcon, { "aria-hidden": true, className: "size-6" }), t.dropToUpload] }) })), progress] }));
97
+ }
98
+ return (_jsxs("div", { "data-slot": "upload-dropzone", className: className, children: [_jsxs("div", { ...dropProps, "aria-disabled": disabled || undefined, className: cn("flex flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed px-6 py-8 text-center text-sm text-muted-foreground transition-colors", over && "border-primary bg-primary/10 text-primary", disabled && "opacity-50"), children: [_jsx(UploadIcon, { "aria-hidden": true, className: "size-6" }), _jsx("p", { className: "font-medium text-foreground", children: label ?? t.dropHere }), _jsx(Button, { type: "button", size: "sm", variant: "outline", disabled: disabled, onClick: () => input.current?.click(), children: t.browse }), (accept || maxSize !== undefined) && (_jsx("p", { className: "text-xs", children: [accept, maxSize !== undefined && t.maxSize(formatBytes(maxSize))]
99
+ .filter(Boolean)
100
+ .join(" · ") })), picker] }), progress] }));
101
+ }
102
+ function UploadList({ uploads, onCancel, onDismiss, className, }) {
103
+ const { messages } = useUI();
104
+ const t = messages.workbench;
105
+ const running = uploads.filter((u) => u.state === "queued" || u.state === "uploading").length;
106
+ return (_jsxs("section", { "aria-label": t.uploads, className: cn("rounded-lg border bg-popover p-3 text-popover-foreground", className), children: [_jsx("h3", { className: "mb-2 text-xs font-medium", "aria-live": "polite", children: running > 0 ? t.uploading(running) : t.uploadsDone }), _jsx("ul", { className: "flex max-h-60 flex-col gap-2 overflow-auto", children: uploads.map((u) => {
107
+ const active = u.state === "queued" || u.state === "uploading";
108
+ const pct = u.state === "done" ? 100 : Math.round(Math.min(100, Math.max(0, u.progress ?? 0)));
109
+ return (_jsxs("li", { "data-state": u.state, className: "flex flex-col gap-1 text-xs", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [_jsx("span", { className: "min-w-0 flex-1 truncate", title: u.name, children: u.name }), _jsx("span", { className: cn("shrink-0 tabular-nums", u.state === "error" ? "text-danger-fg" : "text-muted-foreground"), children: u.state === "uploading" && u.progress != null
110
+ ? `${pct}%`
111
+ : t.uploadState(u.state) }), u.state === "done" && (_jsx(CheckIcon, { "aria-hidden": true, className: "size-3.5 text-success-fg" })), active && onCancel && (_jsx("button", { type: "button", "aria-label": t.cancelUpload(u.name), onClick: () => onCancel(u.id), className: "inline-flex size-5 shrink-0 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, { "aria-hidden": true, className: "size-3" }) })), !active && onDismiss && (_jsx("button", { type: "button", "aria-label": t.dismissUpload(u.name), onClick: () => onDismiss(u.id), className: "inline-flex size-5 shrink-0 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, { "aria-hidden": true, className: "size-3" }) }))] }), active && (_jsx("div", { role: "progressbar", "aria-label": u.name, "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": u.progress != null ? pct : undefined, className: "h-1 overflow-hidden rounded-full bg-muted", children: _jsx("div", { className: cn("h-full rounded-full bg-primary transition-[width]", u.state === "uploading" &&
112
+ u.progress == null &&
113
+ "w-1/3 animate-pulse motion-reduce:animate-none"), style: u.progress != null || u.state === "queued" ? { width: `${pct}%` } : undefined }) })), u.state === "error" && u.error && (_jsx("p", { className: "[overflow-wrap:anywhere] text-danger-fg", children: u.error }))] }, u.id));
114
+ }) })] }));
115
+ }
@@ -0,0 +1,36 @@
1
+ import type * as React from "react";
2
+ export interface WorkbenchShellProps {
3
+ /** Across the top: a `MenuBar`. */
4
+ menuBar?: React.ReactNode;
5
+ /** Down the left: an `ActivityBar`. */
6
+ activityBar?: React.ReactNode;
7
+ /** The side bar the activity bar switches (explorer, files, history). */
8
+ sidebar?: React.ReactNode;
9
+ /** Defaults to true when there is a sidebar. Closed, it stays mounted. */
10
+ sidebarOpen?: boolean;
11
+ /** The editor area: an `EditorGroup`, or several in a `PaneGroup`. */
12
+ children: React.ReactNode;
13
+ /** Under the editors: a `BottomPanel`. */
14
+ bottomPanel?: React.ReactNode;
15
+ bottomPanelOpen?: boolean;
16
+ /** The panel fills the editor area; the editors stay mounted underneath. */
17
+ bottomPanelMaximized?: boolean;
18
+ /** Right of the editors: an assistant, an inspector. */
19
+ aside?: React.ReactNode;
20
+ asideOpen?: boolean;
21
+ /** Along the bottom: a `StatusBar`. */
22
+ statusBar?: React.ReactNode;
23
+ /** Remember pane sizes per person under keys starting with this. */
24
+ storageKey?: string;
25
+ /** Overlays that belong to the shell: the command palette, the shortcuts dialog. */
26
+ overlays?: React.ReactNode;
27
+ className?: string;
28
+ }
29
+ /**
30
+ * The IDE layout: menu bar, activity bar, a resizable side bar, the editor
31
+ * area over a bottom panel, an optional right-hand panel, and the status
32
+ * bar. Every region is host content and every toggle is a prop, so it holds
33
+ * no state but the pane sizes. Hidden regions stay mounted, so an open
34
+ * page keeps its state when the panels around it change.
35
+ */
36
+ export declare function WorkbenchShell({ menuBar, activityBar, sidebar, sidebarOpen, children, bottomPanel, bottomPanelOpen, bottomPanelMaximized, aside, asideOpen, statusBar, storageKey, overlays, className, }: WorkbenchShellProps): React.JSX.Element;
@@ -0,0 +1,20 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { cn } from "../lib/cn.js";
4
+ import { TooltipProvider } from "../primitives/Tooltip.js";
5
+ import { useUI } from "../provider/UIProvider.js";
6
+ import { Pane, PaneGroup } from "./PaneGroup.js";
7
+ /**
8
+ * The IDE layout: menu bar, activity bar, a resizable side bar, the editor
9
+ * area over a bottom panel, an optional right-hand panel, and the status
10
+ * bar. Every region is host content and every toggle is a prop, so it holds
11
+ * no state but the pane sizes. Hidden regions stay mounted, so an open
12
+ * page keeps its state when the panels around it change.
13
+ */
14
+ export function WorkbenchShell({ menuBar, activityBar, sidebar, sidebarOpen = true, children, bottomPanel, bottomPanelOpen = false, bottomPanelMaximized = false, aside, asideOpen = false, statusBar, storageKey, overlays, className, }) {
15
+ const { messages } = useUI();
16
+ const m = messages.workbench;
17
+ const key = (part) => (storageKey ? `${storageKey}:${part}` : undefined);
18
+ const hasBottom = bottomPanel !== undefined && bottomPanel !== null;
19
+ return (_jsx(TooltipProvider, { children: _jsxs("div", { "data-slot": "workbench-shell", className: cn("flex h-full min-h-0 flex-col overflow-hidden bg-background text-foreground", className), children: [menuBar, _jsxs("div", { className: "flex min-h-0 flex-1", children: [activityBar, _jsxs(PaneGroup, { orientation: "horizontal", storageKey: key("main"), className: "flex-1", children: [sidebar !== undefined && sidebar !== null && (_jsx(Pane, { id: "workbench-sidebar", label: m.sideBar, defaultSize: 22, minSize: "180px", maxSize: 45, collapsed: !sidebarOpen, children: sidebar })), _jsx(Pane, { id: "workbench-center", label: m.editor, defaultSize: 78, minSize: "300px", children: _jsxs(PaneGroup, { orientation: "vertical", storageKey: key("center"), children: [_jsx(Pane, { id: "workbench-editor", label: m.editor, defaultSize: 70, minSize: "80px", collapsed: hasBottom && bottomPanelOpen && bottomPanelMaximized, children: children }), hasBottom && (_jsx(Pane, { id: "workbench-panel", label: m.panel, defaultSize: 30, minSize: "80px", collapsed: !bottomPanelOpen, children: bottomPanel }))] }) }), aside !== undefined && aside !== null && (_jsx(Pane, { id: "workbench-aside", label: m.secondarySideBar, defaultSize: 28, minSize: "240px", maxSize: 50, collapsed: !asideOpen, children: aside }))] })] }), statusBar, overlays] }) }));
20
+ }
@@ -0,0 +1,17 @@
1
+ import * as React from "react";
2
+ /** The data type of a drag between the browser's own views. Uploads are `Files` and never match. */
3
+ export declare const FILE_DRAG_TYPE = "application/x-calliope-files";
4
+ export type MoveHandler = (sources: string[], target: string, options: {
5
+ copy: boolean;
6
+ }) => void;
7
+ /** Starts an internal drag of `paths`, with a small label as its image. */
8
+ export declare function startFileDrag(event: React.DragEvent, paths: readonly string[], label: string): void;
9
+ /**
10
+ * Makes an element a drop target for the directory `target`. Holding Alt
11
+ * (Option) copies instead of moving. The sources are only readable on drop,
12
+ * so a hover cannot tell a no-op; `canDrop` filters it out then.
13
+ */
14
+ export declare function useFileDrop(target: string | null, onMove: MoveHandler | undefined): {
15
+ over: boolean;
16
+ props: React.HTMLAttributes<HTMLElement>;
17
+ };
@@ -0,0 +1,77 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import { canDrop } from "./model.js";
4
+ /** The data type of a drag between the browser's own views. Uploads are `Files` and never match. */
5
+ export const FILE_DRAG_TYPE = "application/x-calliope-files";
6
+ /** Starts an internal drag of `paths`, with a small label as its image. */
7
+ export function startFileDrag(event, paths, label) {
8
+ event.dataTransfer.setData(FILE_DRAG_TYPE, JSON.stringify(paths));
9
+ event.dataTransfer.effectAllowed = "copyMove";
10
+ const ghost = document.createElement("div");
11
+ ghost.className =
12
+ "fixed -top-96 left-0 rounded-lg bg-popover px-2 py-1 text-xs text-popover-foreground shadow-md ring-1 ring-foreground/10";
13
+ ghost.textContent = label;
14
+ document.body.appendChild(ghost);
15
+ event.dataTransfer.setDragImage(ghost, 0, 0);
16
+ requestAnimationFrame(() => ghost.remove());
17
+ }
18
+ function readPaths(event) {
19
+ try {
20
+ const parsed = JSON.parse(event.dataTransfer.getData(FILE_DRAG_TYPE));
21
+ return Array.isArray(parsed) && parsed.every((p) => typeof p === "string") ? parsed : null;
22
+ }
23
+ catch {
24
+ return null;
25
+ }
26
+ }
27
+ /**
28
+ * Makes an element a drop target for the directory `target`. Holding Alt
29
+ * (Option) copies instead of moving. The sources are only readable on drop,
30
+ * so a hover cannot tell a no-op; `canDrop` filters it out then.
31
+ */
32
+ export function useFileDrop(target, onMove) {
33
+ const [over, setOver] = React.useState(false);
34
+ const depth = React.useRef(0);
35
+ const enabled = target !== null && Boolean(onMove);
36
+ const accepts = (event) => enabled && event.dataTransfer.types.includes(FILE_DRAG_TYPE);
37
+ const props = enabled
38
+ ? {
39
+ onDragEnter: (event) => {
40
+ if (!accepts(event))
41
+ return;
42
+ event.preventDefault();
43
+ event.stopPropagation();
44
+ depth.current++;
45
+ setOver(true);
46
+ },
47
+ onDragOver: (event) => {
48
+ if (!accepts(event))
49
+ return;
50
+ event.preventDefault();
51
+ event.stopPropagation();
52
+ event.dataTransfer.dropEffect = event.altKey ? "copy" : "move";
53
+ },
54
+ onDragLeave: (event) => {
55
+ if (!accepts(event))
56
+ return;
57
+ event.stopPropagation();
58
+ depth.current = Math.max(0, depth.current - 1);
59
+ if (depth.current === 0)
60
+ setOver(false);
61
+ },
62
+ onDrop: (event) => {
63
+ if (!accepts(event))
64
+ return;
65
+ event.preventDefault();
66
+ event.stopPropagation();
67
+ depth.current = 0;
68
+ setOver(false);
69
+ const sources = readPaths(event);
70
+ const copy = Boolean(event.altKey);
71
+ if (sources && target !== null && canDrop(sources, target, copy))
72
+ onMove?.(sources, target, { copy });
73
+ },
74
+ }
75
+ : {};
76
+ return { over, props };
77
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Saving a file the grid produced. Browser-only when called; nothing runs on
3
+ * import, so the module stays importable on a server.
4
+ */
5
+ import { type ExportFile } from "./grid-format.js";
6
+ /** Hands a Blob to the browser as a download. */
7
+ export declare function downloadBlob(blob: Blob, filename: string): void;
8
+ /** Downloads an export the grid built (`onExport` hosts call this for the formats they do not handle). */
9
+ export declare function downloadExport(file: ExportFile): void;
10
+ /** Decodes a base64 binary cell and downloads it. */
11
+ export declare function downloadBase64(base64: string, filename: string): void;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Saving a file the grid produced. Browser-only when called; nothing runs on
3
+ * import, so the module stays importable on a server.
4
+ */
5
+ import { exportBlob } from "./grid-format.js";
6
+ /** Hands a Blob to the browser as a download. */
7
+ export function downloadBlob(blob, filename) {
8
+ const url = URL.createObjectURL(blob);
9
+ const link = document.createElement("a");
10
+ link.href = url;
11
+ link.download = filename;
12
+ document.body.appendChild(link);
13
+ link.click();
14
+ link.remove();
15
+ URL.revokeObjectURL(url);
16
+ }
17
+ /** Downloads an export the grid built (`onExport` hosts call this for the formats they do not handle). */
18
+ export function downloadExport(file) {
19
+ downloadBlob(exportBlob(file), file.filename);
20
+ }
21
+ /** Decodes a base64 binary cell and downloads it. */
22
+ export function downloadBase64(base64, filename) {
23
+ const binary = atob(base64);
24
+ const bytes = new Uint8Array(binary.length);
25
+ for (let i = 0; i < binary.length; i++)
26
+ bytes[i] = binary.charCodeAt(i);
27
+ downloadBlob(new Blob([bytes], { type: "application/octet-stream" }), filename);
28
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The code editor's colours, from the library's tokens: the ground, the
3
+ * text, the accent for the cursor and selection, and the chart series for
4
+ * syntax (a categorical palette that already holds contrast on every
5
+ * ground). Pure apart from `cssColorToHex`, which needs a canvas.
6
+ */
7
+ /** Monaco's theme shape, kept structural so this module needs no Monaco types. */
8
+ export interface EditorThemeData {
9
+ base: "vs" | "vs-dark";
10
+ inherit: boolean;
11
+ rules: {
12
+ token: string;
13
+ foreground?: string;
14
+ fontStyle?: string;
15
+ }[];
16
+ colors: Record<string, string>;
17
+ }
18
+ /** The tokens the theme reads, as `#rrggbb`. */
19
+ export interface EditorPalette {
20
+ background: string;
21
+ foreground: string;
22
+ muted: string;
23
+ border: string;
24
+ popover: string;
25
+ accent: string;
26
+ primary: string;
27
+ ring: string;
28
+ danger: string;
29
+ chart: [string, string, string, string, string];
30
+ }
31
+ /**
32
+ * Any CSS colour (oklch, hex, a named colour) as `#rrggbb`, by painting one
33
+ * pixel. Null where there is no canvas (a server, jsdom) or the value does
34
+ * not parse.
35
+ */
36
+ export declare function cssColorToHex(value: string): string | null;
37
+ /** The palette under `element`, or null when a token cannot be read. */
38
+ export declare function readEditorPalette(element: Element, toHex?: (value: string) => string | null): EditorPalette | null;
39
+ /** Relative luminance of `#rrggbb`, 0 to 1. */
40
+ export declare function luminance(hex: string): number;
41
+ /** Monaco theme data for a palette. Dark or light follows the ground. */
42
+ export declare function editorTheme(p: EditorPalette): EditorThemeData;