@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,50 @@
1
+ "use client";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { BinaryIcon, BracesIcon, CalendarClockIcon, CalendarIcon, CircleHelpIcon, ClockIcon, FingerprintIcon, HashIcon, KeyRoundIcon, Link2Icon, SigmaIcon, ToggleLeftIcon, TypeIcon, } from "lucide-react";
4
+ import { cn } from "../lib/cn.js";
5
+ import { useUI } from "../provider/UIProvider.js";
6
+ import { asBinary } from "./grid-model.js";
7
+ const TYPE_ICON = {
8
+ integer: HashIcon,
9
+ number: SigmaIcon,
10
+ text: TypeIcon,
11
+ boolean: ToggleLeftIcon,
12
+ datetime: CalendarClockIcon,
13
+ date: CalendarIcon,
14
+ time: ClockIcon,
15
+ json: BracesIcon,
16
+ binary: BinaryIcon,
17
+ uuid: FingerprintIcon,
18
+ other: CircleHelpIcon,
19
+ };
20
+ /** A column type's glyph, named by the type. Identity, not status: it takes no colour. */
21
+ export function ColumnTypeIcon({ type, className }) {
22
+ const { messages } = useUI();
23
+ const Icon = TYPE_ICON[type];
24
+ return (_jsx("span", { role: "img", "aria-label": messages.workbench.gridType(type), className: cn("inline-flex shrink-0", className), children: _jsx(Icon, { "aria-hidden": true, className: "size-3" }) }));
25
+ }
26
+ /** A column's primary and foreign key marks, each named. */
27
+ export function ColumnKeyIcons({ column }) {
28
+ const { messages } = useUI();
29
+ const t = messages.workbench;
30
+ return (_jsxs(_Fragment, { children: [column.primaryKey && (_jsx("span", { role: "img", "aria-label": t.gridPrimaryKey, className: "inline-flex shrink-0", children: _jsx(KeyRoundIcon, { "aria-hidden": true, className: "size-3" }) })), column.foreignKey && (_jsx("span", { role: "img", "aria-label": t.gridForeignKey(column.references ?? null), className: "inline-flex shrink-0", children: _jsx(Link2Icon, { "aria-hidden": true, className: "size-3" }) }))] }));
31
+ }
32
+ /** How one value reads in a cell: NULL, empty text and binary are marked so they never pass for data. */
33
+ export function GridCellValue({ value, className }) {
34
+ const { messages } = useUI();
35
+ const t = messages.workbench;
36
+ if (value === null || value === undefined) {
37
+ return (_jsx("span", { className: cn("text-muted-foreground italic select-none", className), children: t.gridNull }));
38
+ }
39
+ const binary = asBinary(value);
40
+ if (binary) {
41
+ return (_jsxs("span", { className: cn("truncate text-muted-foreground italic", className), children: ["<", t.gridBinary(binary.size), ">"] }));
42
+ }
43
+ if (value === "") {
44
+ return (_jsxs("span", { className: cn("text-muted-foreground italic select-none", className), children: ["<", t.gridEmptyText, ">"] }));
45
+ }
46
+ if (typeof value === "object") {
47
+ return (_jsx("span", { className: cn("truncate text-muted-foreground", className), children: JSON.stringify(value) }));
48
+ }
49
+ return _jsx("span", { className: cn("truncate", className), children: String(value) });
50
+ }
@@ -0,0 +1,31 @@
1
+ import * as React from "react";
2
+ import { type GridColumnMeta, type GridRow } from "./grid-model.js";
3
+ export interface CellDetailDialogProps {
4
+ open: boolean;
5
+ onOpenChange: (open: boolean) => void;
6
+ column: string;
7
+ value: unknown;
8
+ onCopy?: (text: string) => void;
9
+ }
10
+ /**
11
+ * One cell's whole value: JSON pretty-printed (also text that holds JSON),
12
+ * bytes as their size with a download, everything copyable.
13
+ */
14
+ export declare function CellDetailDialog({ open, onOpenChange, column, value, onCopy, }: CellDetailDialogProps): React.JSX.Element;
15
+ export interface RowDetailDrawerProps {
16
+ open: boolean;
17
+ onOpenChange: (open: boolean) => void;
18
+ row: GridRow;
19
+ columns: readonly GridColumnMeta[];
20
+ /** 1-based position in the view. */
21
+ rowNumber: number;
22
+ rowTotal: number;
23
+ /** Previous (-1) or next (1) row, without closing. */
24
+ onNavigate: (delta: -1 | 1) => void;
25
+ onCopy?: (text: string) => void;
26
+ }
27
+ /**
28
+ * One row, read top to bottom: thirty columns do not fit across a screen.
29
+ * Previous and next walk the view without closing, to compare rows.
30
+ */
31
+ export declare function RowDetailDrawer({ open, onOpenChange, row, columns, rowNumber, rowTotal, onNavigate, onCopy, }: RowDetailDrawerProps): React.JSX.Element;
@@ -0,0 +1,78 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { CheckIcon, ChevronDownIcon, ChevronUpIcon, CopyIcon, DownloadIcon } from "lucide-react";
4
+ import * as React from "react";
5
+ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "../overlays/Dialog.js";
6
+ import { Drawer, DrawerContent, DrawerDescription, DrawerHeader, DrawerTitle, } from "../overlays/Drawer.js";
7
+ import { Button } from "../primitives/Button.js";
8
+ import { useUI } from "../provider/UIProvider.js";
9
+ import { downloadBase64 } from "./download.js";
10
+ import { ColumnKeyIcons, ColumnTypeIcon, GridCellValue } from "./GridCell.js";
11
+ import { asBinary } from "./grid-model.js";
12
+ /** Pretty-prints JSON, or text that holds JSON; null for anything else. */
13
+ function prettyJson(value) {
14
+ if (typeof value === "object" && value !== null)
15
+ return JSON.stringify(value, null, 2);
16
+ if (typeof value !== "string")
17
+ return null;
18
+ const trimmed = value.trim();
19
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("["))
20
+ return null;
21
+ try {
22
+ return JSON.stringify(JSON.parse(trimmed), null, 2);
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
28
+ /** A copy button that says "Copied" for a moment. Reports through `onCopied`. */
29
+ function CopyButton({ text, label, onCopied, }) {
30
+ const { messages } = useUI();
31
+ const [copied, setCopied] = React.useState(false);
32
+ React.useEffect(() => {
33
+ if (!copied)
34
+ return;
35
+ const timer = window.setTimeout(() => setCopied(false), 1500);
36
+ return () => window.clearTimeout(timer);
37
+ }, [copied]);
38
+ return (_jsxs(Button, { variant: "ghost", size: "xs", onClick: () => {
39
+ void navigator.clipboard?.writeText(text).then(() => {
40
+ setCopied(true);
41
+ onCopied?.(text);
42
+ }, () => { });
43
+ }, children: [copied ? _jsx(CheckIcon, { "aria-hidden": true }) : _jsx(CopyIcon, { "aria-hidden": true }), copied ? messages.workbench.gridCopied : label] }));
44
+ }
45
+ /**
46
+ * One cell's whole value: JSON pretty-printed (also text that holds JSON),
47
+ * bytes as their size with a download, everything copyable.
48
+ */
49
+ export function CellDetailDialog({ open, onOpenChange, column, value, onCopy, }) {
50
+ const { messages } = useUI();
51
+ const t = messages.workbench;
52
+ const binary = asBinary(value);
53
+ const body = value === null || value === undefined
54
+ ? t.gridNull
55
+ : binary
56
+ ? binary.base64
57
+ : (prettyJson(value) ?? String(value));
58
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "sm:max-w-2xl", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { className: "font-mono text-sm", children: column }), _jsx(DialogDescription, { className: "text-xs", children: binary
59
+ ? t.gridBytes(binary.size, binary.truncated === true)
60
+ : t.gridCharacters(body.length) })] }), _jsxs("div", { className: "flex justify-end gap-1", children: [binary && binary.base64 !== "" && (_jsxs(Button, { variant: "ghost", size: "xs", onClick: () => downloadBase64(binary.base64, `${column}.bin`), children: [_jsx(DownloadIcon, { "aria-hidden": true }), t.gridDownload] })), _jsx(CopyButton, { text: body, label: t.gridCopyValue, onCopied: onCopy })] }), _jsx("pre", {
61
+ // A long value scrolls: focusable so a keyboard can reach and scroll it.
62
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
63
+ tabIndex: 0, "aria-label": t.contentsOf(column), className: "max-h-96 overflow-auto rounded-lg border bg-muted/40 p-3 font-mono text-xs break-all whitespace-pre-wrap outline-none focus-visible:ring-2 focus-visible:ring-ring/50", children: body })] }) }));
64
+ }
65
+ /**
66
+ * One row, read top to bottom: thirty columns do not fit across a screen.
67
+ * Previous and next walk the view without closing, to compare rows.
68
+ */
69
+ export function RowDetailDrawer({ open, onOpenChange, row, columns, rowNumber, rowTotal, onNavigate, onCopy, }) {
70
+ const { messages } = useUI();
71
+ const t = messages.workbench;
72
+ return (_jsx(Drawer, { open: open, onOpenChange: onOpenChange, children: _jsxs(DrawerContent, { side: "right", className: "gap-0 sm:max-w-md", children: [_jsxs(DrawerHeader, { className: "border-b", children: [_jsx(DrawerTitle, { className: "text-sm", children: t.gridRowOf(rowNumber, rowTotal) }), _jsx(DrawerDescription, { className: "text-xs", children: t.gridColumns(columns.length) }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Button, { variant: "ghost", size: "icon-xs", "aria-label": t.gridPreviousRow, disabled: rowNumber <= 1, onClick: () => onNavigate(-1), children: _jsx(ChevronUpIcon, { "aria-hidden": true }) }), _jsx(Button, { variant: "ghost", size: "icon-xs", "aria-label": t.gridNextRow, disabled: rowNumber >= rowTotal, onClick: () => onNavigate(1), children: _jsx(ChevronDownIcon, { "aria-hidden": true }) }), _jsx(CopyButton, { text: JSON.stringify(row, null, 2), label: t.gridCopyRow, onCopied: onCopy })] })] }), _jsx("dl", { className: "flex-1 space-y-3 overflow-auto px-4 py-3", children: columns.map((column) => {
73
+ const value = row[column.name];
74
+ const json = prettyJson(value);
75
+ const binary = asBinary(value);
76
+ return (_jsxs("div", { className: "space-y-1", children: [_jsxs("dt", { className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [_jsx(ColumnTypeIcon, { type: column.type }), _jsx("span", { className: "min-w-0 truncate font-medium text-foreground", children: column.name }), _jsx(ColumnKeyIcons, { column: column }), _jsxs("span", { className: "ml-auto shrink-0", children: [column.dataType ?? t.gridType(column.type), column.nullable === false ? `, ${t.gridNotNull}` : ""] })] }), _jsx("dd", { className: "text-xs", children: binary ? (_jsxs("span", { className: "flex items-center gap-2", children: [_jsx(GridCellValue, { value: value }), binary.base64 !== "" && (_jsxs(Button, { variant: "ghost", size: "xs", onClick: () => downloadBase64(binary.base64, `${column.name}.bin`), children: [_jsx(DownloadIcon, { "aria-hidden": true }), t.gridDownload] }))] })) : json !== null || (typeof value === "string" && value.length > 120) ? (_jsx("pre", { className: "max-h-48 overflow-auto rounded-lg border bg-muted/40 p-2 font-mono break-all whitespace-pre-wrap", children: json ?? String(value) })) : (_jsx(GridCellValue, { value: value, className: "font-mono break-all" })) })] }, column.name));
77
+ }) })] }) }));
78
+ }
@@ -0,0 +1,49 @@
1
+ import type * as React from "react";
2
+ export type MenuEntry = {
3
+ type?: "item";
4
+ id: string;
5
+ label: string;
6
+ /** Its chord, shown beside it (`"Mod+N"`). Binding it is `useShortcuts`' job. */
7
+ shortcut?: string;
8
+ icon?: React.ReactNode;
9
+ disabled?: boolean;
10
+ destructive?: boolean;
11
+ onSelect: () => void;
12
+ } | {
13
+ type: "checkbox";
14
+ id: string;
15
+ label: string;
16
+ checked: boolean;
17
+ shortcut?: string;
18
+ disabled?: boolean;
19
+ onCheckedChange: (checked: boolean) => void;
20
+ } | {
21
+ type: "label";
22
+ id: string;
23
+ label: string;
24
+ } | {
25
+ type: "separator";
26
+ id: string;
27
+ };
28
+ export interface WorkbenchMenu {
29
+ id: string;
30
+ /** The word in the bar ("File"). */
31
+ label: string;
32
+ items: readonly MenuEntry[];
33
+ }
34
+ export interface MenuBarProps {
35
+ menus: readonly WorkbenchMenu[];
36
+ /** Before the menus: the product mark, a home link. */
37
+ start?: React.ReactNode;
38
+ /** Right-aligned: the product name, a connection, the person. */
39
+ end?: React.ReactNode;
40
+ /** Draw shortcuts with Mac glyphs. Detected from the platform when left out. */
41
+ mac?: boolean;
42
+ className?: string;
43
+ }
44
+ /**
45
+ * The menu bar across the top of an IDE layout, from data: each menu a list
46
+ * of commands, toggles, labels and separators. It is the ARIA menubar
47
+ * (Menubar primitive): one tab stop, arrows between and within menus.
48
+ */
49
+ export declare function MenuBar({ menus, start, end, mac, className }: MenuBarProps): React.JSX.Element;
@@ -0,0 +1,25 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { cn } from "../lib/cn.js";
4
+ import { Menubar, MenubarCheckboxItem, MenubarContent, MenubarItem, MenubarLabel, MenubarMenu, MenubarSeparator, MenubarShortcut, MenubarTrigger, } from "../primitives/Menubar.js";
5
+ import { useUI } from "../provider/UIProvider.js";
6
+ import { formatShortcut, isMacPlatform } from "./keys.js";
7
+ /**
8
+ * The menu bar across the top of an IDE layout, from data: each menu a list
9
+ * of commands, toggles, labels and separators. It is the ARIA menubar
10
+ * (Menubar primitive): one tab stop, arrows between and within menus.
11
+ */
12
+ export function MenuBar({ menus, start, end, mac, className }) {
13
+ const { messages } = useUI();
14
+ const isMac = mac ?? isMacPlatform();
15
+ const shortcut = (keys) => keys ? _jsx(MenubarShortcut, { children: formatShortcut(keys, isMac) }) : null;
16
+ return (_jsxs("div", { "data-slot": "workbench-menu-bar", className: cn("flex h-8 shrink-0 items-center gap-1 border-b bg-surface-1 px-1 text-foreground", className), children: [start, _jsx(Menubar, { "aria-label": messages.workbench.menuBar, children: menus.map((menu) => (_jsxs(MenubarMenu, { value: menu.id, children: [_jsx(MenubarTrigger, { children: menu.label }), _jsx(MenubarContent, { children: menu.items.map((entry) => {
17
+ if (entry.type === "separator")
18
+ return _jsx(MenubarSeparator, {}, entry.id);
19
+ if (entry.type === "label")
20
+ return _jsx(MenubarLabel, { children: entry.label }, entry.id);
21
+ if (entry.type === "checkbox")
22
+ return (_jsxs(MenubarCheckboxItem, { checked: entry.checked, disabled: entry.disabled, onCheckedChange: (checked) => entry.onCheckedChange(checked === true), children: [entry.label, shortcut(entry.shortcut)] }, entry.id));
23
+ return (_jsxs(MenubarItem, { disabled: entry.disabled, variant: entry.destructive ? "destructive" : "default", onSelect: entry.onSelect, children: [entry.icon && (_jsx("span", { "aria-hidden": true, className: "flex", children: entry.icon })), entry.label, shortcut(entry.shortcut)] }, entry.id));
24
+ }) })] }, menu.id))) }), end && (_jsx("div", { className: "ml-auto flex items-center gap-2 pr-2 text-xs text-muted-foreground", children: end }))] }));
25
+ }
@@ -0,0 +1,50 @@
1
+ import * as React from "react";
2
+ /** A size bound: a percentage of the group (`20`) or pixels (`"200px"`). */
3
+ export type PaneSize = number | `${number}px`;
4
+ export interface PaneProps {
5
+ /** Stable within the group: the stored size's key. */
6
+ id: string;
7
+ /** What the pane is, for its resize handle's name ("Resize Explorer"). */
8
+ label: string;
9
+ /** Percent of the group at first paint. Panes without one share what is left. */
10
+ defaultSize?: number;
11
+ minSize?: PaneSize;
12
+ maxSize?: PaneSize;
13
+ /**
14
+ * Hidden but still mounted, so its state (an unsaved draft, a scroll
15
+ * position) survives; its neighbours take the room.
16
+ */
17
+ collapsed?: boolean;
18
+ className?: string;
19
+ children?: React.ReactNode;
20
+ }
21
+ /**
22
+ * One pane of a `PaneGroup`. Outside a group it is a plain block; inside
23
+ * one, the group lays it out and puts a resize handle between it and the
24
+ * next visible pane.
25
+ */
26
+ export declare function Pane({ className, children }: PaneProps): React.JSX.Element;
27
+ export interface PaneGroupProps {
28
+ /** `horizontal` puts panes side by side; `vertical` stacks them. */
29
+ orientation?: "horizontal" | "vertical";
30
+ /** Remember sizes per person under this key (localStorage). */
31
+ storageKey?: string;
32
+ /** Sizes changed (a drag ended, or a keyboard step): percent per pane id. */
33
+ onSizesChange?: (sizes: Record<string, number>) => void;
34
+ className?: string;
35
+ /** `Pane`s; anything else is ignored. */
36
+ children: React.ReactNode;
37
+ }
38
+ type Sizes = Record<string, number>;
39
+ /** Every pane's size, from the stored sizes over the defaults. */
40
+ export declare function resolvePaneSizes(panes: readonly Pick<PaneProps, "id" | "defaultSize">[], stored?: Sizes): Sizes;
41
+ /**
42
+ * Resizable panes with the ARIA window-splitter pattern: each handle is a
43
+ * focusable `separator` with its value as the pane's share of the group;
44
+ * the arrow keys step it, Home and End take it to its bounds, and the
45
+ * pointer drags it. Sizes are flex weights, so a collapsed pane's room goes
46
+ * to its neighbours and comes back when it reopens. With `storageKey` they
47
+ * are remembered per person.
48
+ */
49
+ export declare function PaneGroup({ orientation, storageKey, onSizesChange, className, children, }: PaneGroupProps): React.JSX.Element;
50
+ export {};
@@ -0,0 +1,146 @@
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 { readLocal, subscribeLocal, writeLocal } from "../lib/local-store.js";
6
+ import { useUI } from "../provider/UIProvider.js";
7
+ /**
8
+ * One pane of a `PaneGroup`. Outside a group it is a plain block; inside
9
+ * one, the group lays it out and puts a resize handle between it and the
10
+ * next visible pane.
11
+ */
12
+ export function Pane({ className, children }) {
13
+ return _jsx("div", { className: className, children: children });
14
+ }
15
+ function bound(size, total, fallback) {
16
+ if (size === undefined)
17
+ return fallback;
18
+ if (typeof size === "number")
19
+ return size;
20
+ return total > 0 ? (parseFloat(size) / total) * 100 : fallback;
21
+ }
22
+ function parseStored(value) {
23
+ if (!value)
24
+ return {};
25
+ try {
26
+ const parsed = JSON.parse(value);
27
+ if (!parsed || typeof parsed !== "object")
28
+ return {};
29
+ return Object.fromEntries(Object.entries(parsed).filter(([, v]) => typeof v === "number" && v > 0));
30
+ }
31
+ catch {
32
+ return {};
33
+ }
34
+ }
35
+ /** Every pane's size, from the stored sizes over the defaults. */
36
+ export function resolvePaneSizes(panes, stored = {}) {
37
+ const given = panes.reduce((sum, p) => sum + (p.defaultSize ?? 0), 0);
38
+ const missing = panes.filter((p) => p.defaultSize === undefined).length;
39
+ const share = missing > 0 ? Math.max(10, (100 - given) / missing) : 0;
40
+ return Object.fromEntries(panes.map((p) => [p.id, stored[p.id] ?? p.defaultSize ?? share]));
41
+ }
42
+ const STEP = 5;
43
+ /**
44
+ * Resizable panes with the ARIA window-splitter pattern: each handle is a
45
+ * focusable `separator` with its value as the pane's share of the group;
46
+ * the arrow keys step it, Home and End take it to its bounds, and the
47
+ * pointer drags it. Sizes are flex weights, so a collapsed pane's room goes
48
+ * to its neighbours and comes back when it reopens. With `storageKey` they
49
+ * are remembered per person.
50
+ */
51
+ export function PaneGroup({ orientation = "horizontal", storageKey, onSizesChange, className, children, }) {
52
+ const { messages } = useUI();
53
+ const groupRef = React.useRef(null);
54
+ const baseId = React.useId();
55
+ const horizontal = orientation === "horizontal";
56
+ const panes = React.Children.toArray(children).filter((child) => React.isValidElement(child) && child.type === Pane);
57
+ const specs = panes.map((p) => p.props);
58
+ const stored = React.useSyncExternalStore(subscribeLocal, () => (storageKey ? readLocal(storageKey) : null), () => null);
59
+ const [local, setLocal] = React.useState({});
60
+ const sizes = { ...resolvePaneSizes(specs, parseStored(stored)), ...local };
61
+ const visible = specs.filter((p) => !p.collapsed);
62
+ const total = visible.reduce((sum, p) => sum + (sizes[p.id] ?? 0), 0) || 1;
63
+ const commit = (next) => {
64
+ const all = { ...sizes, ...next };
65
+ setLocal(all);
66
+ if (storageKey)
67
+ writeLocal(storageKey, JSON.stringify(all));
68
+ onSizesChange?.(all);
69
+ };
70
+ /** The pair's new sizes when the first takes `want` (weight), within both panes' bounds. */
71
+ const resize = (a, b, want, px) => {
72
+ const sa = sizes[a.id] ?? 0;
73
+ const sb = sizes[b.id] ?? 0;
74
+ const pair = sa + sb;
75
+ const w = (pct) => (pct / 100) * total;
76
+ const lo = Math.max(w(bound(a.minSize, px, 0)), pair - w(bound(b.maxSize, px, 100)), 0);
77
+ const hi = Math.min(w(bound(a.maxSize, px, 100)), pair - w(bound(b.minSize, px, 0)), pair);
78
+ const next = Math.min(Math.max(want, lo), Math.max(lo, hi));
79
+ return { [a.id]: next, [b.id]: pair - next };
80
+ };
81
+ const groupPx = () => {
82
+ const rect = groupRef.current?.getBoundingClientRect();
83
+ return rect ? (horizontal ? rect.width : rect.height) : 0;
84
+ };
85
+ const startDrag = (a, b) => (event) => {
86
+ if (event.button !== 0)
87
+ return;
88
+ event.preventDefault();
89
+ const handle = event.currentTarget;
90
+ handle.setPointerCapture?.(event.pointerId);
91
+ handle.focus();
92
+ const start = horizontal ? event.clientX : event.clientY;
93
+ const px = groupPx();
94
+ const from = sizes[a.id] ?? 0;
95
+ let latest = {};
96
+ const onMove = (e) => {
97
+ const delta = (horizontal ? e.clientX : e.clientY) - start;
98
+ latest = resize(a, b, from + (px > 0 ? (delta / px) * total : 0), px);
99
+ setLocal((current) => ({ ...current, ...latest }));
100
+ };
101
+ const onUp = () => {
102
+ handle.removeEventListener("pointermove", onMove);
103
+ handle.removeEventListener("pointerup", onUp);
104
+ handle.removeEventListener("pointercancel", onUp);
105
+ if (Object.keys(latest).length > 0)
106
+ commit(latest);
107
+ };
108
+ handle.addEventListener("pointermove", onMove);
109
+ handle.addEventListener("pointerup", onUp);
110
+ handle.addEventListener("pointercancel", onUp);
111
+ };
112
+ const onHandleKey = (a, b) => (event) => {
113
+ const px = groupPx();
114
+ const step = (STEP / 100) * total;
115
+ const now = sizes[a.id] ?? 0;
116
+ const back = horizontal ? "ArrowLeft" : "ArrowUp";
117
+ const forward = horizontal ? "ArrowRight" : "ArrowDown";
118
+ let want = null;
119
+ if (event.key === back)
120
+ want = now - step;
121
+ else if (event.key === forward)
122
+ want = now + step;
123
+ else if (event.key === "Home")
124
+ want = 0;
125
+ else if (event.key === "End")
126
+ want = Number.POSITIVE_INFINITY;
127
+ if (want === null)
128
+ return;
129
+ event.preventDefault();
130
+ commit(resize(a, b, want, px));
131
+ };
132
+ return (_jsx("div", { ref: groupRef, "data-slot": "pane-group", "data-orientation": orientation, className: cn("flex h-full min-h-0 w-full min-w-0", !horizontal && "flex-col", className), children: panes.map((pane) => {
133
+ const p = pane.props;
134
+ const i = visible.indexOf(p);
135
+ const next = i >= 0 ? visible[i + 1] : undefined;
136
+ const share = ((sizes[p.id] ?? 0) / total) * 100;
137
+ const px = (v) => typeof v === "string" && v.endsWith("px") ? v : undefined;
138
+ return (_jsxs(React.Fragment, { children: [_jsx("div", { id: `${baseId}-${p.id}`, "data-slot": "pane", hidden: p.collapsed, className: cn("min-h-0 min-w-0 overflow-hidden", p.className), style: {
139
+ flex: `${sizes[p.id] ?? 0} 1 0px`,
140
+ [horizontal ? "minWidth" : "minHeight"]: px(p.minSize),
141
+ [horizontal ? "maxWidth" : "maxHeight"]: px(p.maxSize),
142
+ }, children: p.children }), next && (_jsx("div", { role: "separator", tabIndex: 0, "aria-orientation": horizontal ? "vertical" : "horizontal", "aria-controls": `${baseId}-${p.id}`, "aria-label": messages.workbench.resize(p.label), "aria-valuenow": Math.round(share), "aria-valuemin": 0, "aria-valuemax": 100, "data-slot": "pane-handle", className: cn("relative shrink-0 bg-border outline-hidden transition-colors select-none hover:bg-ring focus-visible:bg-ring active:bg-ring", "after:absolute after:content-['']", horizontal
143
+ ? "w-px cursor-col-resize after:-inset-x-1 after:inset-y-0"
144
+ : "h-px cursor-row-resize after:inset-x-0 after:-inset-y-1"), onPointerDown: startDrag(p, next), onKeyDown: onHandleKey(p, next) }))] }, p.id));
145
+ }) }));
146
+ }
@@ -0,0 +1,20 @@
1
+ import type { PendingCounts } from "./pending.js";
2
+ export interface PendingChangesBarProps {
3
+ counts: PendingCounts;
4
+ onCommit: () => void;
5
+ onDiscard: () => void;
6
+ committing?: boolean;
7
+ /** The last commit's failure: nothing was written, the changes are still pending. */
8
+ error?: string | null;
9
+ /** 1-based row the failure belongs to, when the host reported one. */
10
+ errorRow?: number | null;
11
+ /** A note under the counts ("No primary key: rows can be added, not changed"). */
12
+ note?: string | null;
13
+ className?: string;
14
+ }
15
+ /**
16
+ * What is waiting to be written, with Commit and Discard. Hidden while
17
+ * nothing is pending and no error stands. The warn tone: pending is a state
18
+ * to notice, not a failure; a failed commit is the error tone.
19
+ */
20
+ export declare function PendingChangesBar({ counts, onCommit, onDiscard, committing, error, errorRow, note, className, }: PendingChangesBarProps): import("react").JSX.Element | null;
@@ -0,0 +1,19 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { CheckIcon, LoaderCircleIcon, TriangleAlertIcon, XIcon } from "lucide-react";
4
+ import { cn } from "../lib/cn.js";
5
+ import { Button } from "../primitives/Button.js";
6
+ import { useUI } from "../provider/UIProvider.js";
7
+ import { PILL_TONE } from "../status/tones.js";
8
+ /**
9
+ * What is waiting to be written, with Commit and Discard. Hidden while
10
+ * nothing is pending and no error stands. The warn tone: pending is a state
11
+ * to notice, not a failure; a failed commit is the error tone.
12
+ */
13
+ export function PendingChangesBar({ counts, onCommit, onDiscard, committing = false, error, errorRow, note, className, }) {
14
+ const { messages } = useUI();
15
+ const t = messages.workbench;
16
+ if (counts.total === 0 && !error)
17
+ return null;
18
+ return (_jsxs("section", { "aria-label": t.gridPendingChanges, "data-slot": "pending-changes-bar", className: cn("shrink-0", className), children: [counts.total > 0 && (_jsxs("div", { className: cn("flex flex-wrap items-center gap-x-3 gap-y-1 border-x-0 border-t-0 border-b px-3 py-1.5 text-xs", PILL_TONE.warn), children: [_jsx(TriangleAlertIcon, { "aria-hidden": true, className: "size-3.5 shrink-0" }), _jsx("span", { className: "font-medium", role: "status", children: t.gridPendingSummary(counts.inserts, counts.updates, counts.deletes) }), note && _jsx("span", { children: note }), _jsxs("span", { className: "ml-auto flex items-center gap-1", children: [_jsxs(Button, { variant: "ghost", size: "xs", onClick: onDiscard, disabled: committing, children: [_jsx(XIcon, { "aria-hidden": true }), t.gridDiscard] }), _jsxs(Button, { size: "xs", onClick: onCommit, disabled: committing, children: [committing ? (_jsx(LoaderCircleIcon, { "aria-hidden": true, className: "animate-spin motion-reduce:animate-none" })) : (_jsx(CheckIcon, { "aria-hidden": true })), committing ? t.gridCommitting : t.gridCommit] })] })] })), error && (_jsx("p", { role: "alert", className: cn("border-x-0 border-t-0 border-b px-3 py-1.5 text-xs", PILL_TONE.error), children: t.gridCommitFailed(error, errorRow ?? null) }))] }));
19
+ }
@@ -0,0 +1,50 @@
1
+ import * as React from "react";
2
+ export interface ShortcutDefinition {
3
+ /** Stable id: the help dialog's key, and how a palette entry names its shortcut. */
4
+ id: string;
5
+ /** The chord: `"Mod+K"`, `"Mod+Shift+A"`, `"F2"`. */
6
+ keys: string;
7
+ /** What it does, for the help dialog. */
8
+ label: string;
9
+ /** The help dialog's heading for it ("General", "Editor"). */
10
+ group?: string;
11
+ run: (event: KeyboardEvent) => void;
12
+ /** False keeps it listed but inert. Defaults to true. */
13
+ enabled?: boolean;
14
+ /**
15
+ * Fire while the focus is in a text field. Chords with Ctrl, Command or Alt
16
+ * fire there anyway; a bare key (F2, Delete) does not unless this is set.
17
+ */
18
+ inEditable?: boolean;
19
+ /** Listed in the help dialog. Defaults to true. */
20
+ listed?: boolean;
21
+ }
22
+ /** What the help dialog shows: a definition without its handler. */
23
+ export type ShortcutEntry = Pick<ShortcutDefinition, "id" | "keys" | "label" | "group">;
24
+ /**
25
+ * Binds keyboard shortcuts while the component is mounted, and lists them in
26
+ * `KeyboardShortcutsDialog`. Handlers may change every render; the chords
27
+ * re-register only when an id, chord or label changes.
28
+ */
29
+ export declare function useShortcuts(definitions: ShortcutDefinition[]): void;
30
+ /** Every shortcut bound on the page right now, for a help dialog or a palette. */
31
+ export declare function useRegisteredShortcuts(): ShortcutEntry[];
32
+ export interface ShortcutKeysProps extends React.ComponentProps<"span"> {
33
+ /** The chord, `"Mod+K"`. */
34
+ keys: string;
35
+ /** Draw Mac glyphs. Detected from the platform when left out. */
36
+ mac?: boolean;
37
+ }
38
+ /** A chord as key caps. The caps are drawn; the spoken name joins the keys with "+". */
39
+ export declare function ShortcutKeys({ keys, mac, className, ...props }: ShortcutKeysProps): React.JSX.Element;
40
+ export interface KeyboardShortcutsDialogProps {
41
+ open: boolean;
42
+ onOpenChange: (open: boolean) => void;
43
+ /** The shortcuts to list. Defaults to every one registered on the page. */
44
+ shortcuts?: readonly ShortcutEntry[];
45
+ mac?: boolean;
46
+ /** Groups without a name land under this heading. Defaults to "General". */
47
+ defaultGroup?: string;
48
+ }
49
+ /** The help overlay: every shortcut, grouped, with its key caps. */
50
+ export declare function KeyboardShortcutsDialog({ open, onOpenChange, shortcuts, mac, defaultGroup, }: KeyboardShortcutsDialogProps): React.JSX.Element;
@@ -0,0 +1,109 @@
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 { Dialog, DialogContent, DialogDescription, DialogTitle } from "../overlays/Dialog.js";
6
+ import { useUI } from "../provider/UIProvider.js";
7
+ import { isMacPlatform, matchesShortcut, parseShortcut, shortcutKeys } from "./keys.js";
8
+ /**
9
+ * The page's shortcut registry: one keydown listener on the document for
10
+ * every `useShortcuts` call. The most recently mounted binding of a chord
11
+ * wins, so a focused editor can take over a chord the shell also binds.
12
+ */
13
+ const registry = {
14
+ entries: [],
15
+ version: 0,
16
+ listeners: new Set(),
17
+ snapshot: [],
18
+ };
19
+ function changed() {
20
+ registry.version += 1;
21
+ const seen = new Set();
22
+ registry.snapshot = registry.entries
23
+ .flatMap((e) => e.ref.current)
24
+ .filter((d) => d.listed !== false && !seen.has(d.id) && seen.add(d.id))
25
+ .map(({ id, keys, label, group }) => ({ id, keys, label, group }));
26
+ for (const listener of registry.listeners)
27
+ listener();
28
+ }
29
+ function isEditable(target) {
30
+ if (!(target instanceof HTMLElement))
31
+ return false;
32
+ return target.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(target.tagName);
33
+ }
34
+ function onKeyDown(event) {
35
+ if (event.defaultPrevented)
36
+ return;
37
+ const mac = isMacPlatform();
38
+ const editable = isEditable(event.target);
39
+ for (let i = registry.entries.length - 1; i >= 0; i -= 1) {
40
+ for (const def of registry.entries[i]?.ref.current ?? []) {
41
+ if (def.enabled === false)
42
+ continue;
43
+ const chord = parseShortcut(def.keys);
44
+ if (editable && !def.inEditable && !(chord.mod || chord.ctrl || chord.meta || chord.alt)) {
45
+ continue;
46
+ }
47
+ if (matchesShortcut(event, chord, mac)) {
48
+ event.preventDefault();
49
+ def.run(event);
50
+ return;
51
+ }
52
+ }
53
+ }
54
+ }
55
+ /**
56
+ * Binds keyboard shortcuts while the component is mounted, and lists them in
57
+ * `KeyboardShortcutsDialog`. Handlers may change every render; the chords
58
+ * re-register only when an id, chord or label changes.
59
+ */
60
+ export function useShortcuts(definitions) {
61
+ const ref = React.useRef(definitions);
62
+ const signature = definitions
63
+ .map((d) => `${d.id}\u0000${d.keys}\u0000${d.label}\u0000${d.group ?? ""}\u0000${d.listed}`)
64
+ .join("\u0001");
65
+ React.useLayoutEffect(() => {
66
+ ref.current = definitions;
67
+ });
68
+ React.useEffect(() => {
69
+ const entry = { ref };
70
+ registry.entries.push(entry);
71
+ if (registry.entries.length === 1)
72
+ document.addEventListener("keydown", onKeyDown);
73
+ changed();
74
+ return () => {
75
+ registry.entries = registry.entries.filter((e) => e !== entry);
76
+ if (registry.entries.length === 0)
77
+ document.removeEventListener("keydown", onKeyDown);
78
+ changed();
79
+ };
80
+ }, [signature]);
81
+ }
82
+ const subscribe = (listener) => {
83
+ registry.listeners.add(listener);
84
+ return () => registry.listeners.delete(listener);
85
+ };
86
+ const getSnapshot = () => registry.snapshot;
87
+ const EMPTY = [];
88
+ /** Every shortcut bound on the page right now, for a help dialog or a palette. */
89
+ export function useRegisteredShortcuts() {
90
+ return React.useSyncExternalStore(subscribe, getSnapshot, () => EMPTY);
91
+ }
92
+ /** A chord as key caps. The caps are drawn; the spoken name joins the keys with "+". */
93
+ export function ShortcutKeys({ keys, mac, className, ...props }) {
94
+ const { messages } = useUI();
95
+ const parts = shortcutKeys(keys, mac ?? isMacPlatform());
96
+ return (_jsx("span", { className: cn("inline-flex items-center gap-0.5", className), ...props, children: parts.map((part, i) => (_jsxs(React.Fragment, { children: [i > 0 && _jsx("span", { className: "sr-only", children: messages.workbench.keyJoin }), _jsx("kbd", { className: "min-w-5 rounded-lg border bg-muted px-1 py-0.5 text-center font-mono text-2xs text-muted-foreground", children: part })] }, `${part}-${i}`))) }));
97
+ }
98
+ /** The help overlay: every shortcut, grouped, with its key caps. */
99
+ export function KeyboardShortcutsDialog({ open, onOpenChange, shortcuts, mac, defaultGroup = "General", }) {
100
+ const { messages } = useUI();
101
+ const registered = useRegisteredShortcuts();
102
+ const list = shortcuts ?? registered;
103
+ const groups = new Map();
104
+ for (const s of list) {
105
+ const key = s.group ?? defaultGroup;
106
+ groups.set(key, [...(groups.get(key) ?? []), s]);
107
+ }
108
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "max-w-md gap-3 p-4", children: [_jsx(DialogTitle, { className: "text-sm font-medium", children: messages.workbench.keyboardShortcuts }), _jsx(DialogDescription, { className: "sr-only", children: messages.workbench.keyboardShortcutsDescription }), _jsx("div", { className: "max-h-[60vh] overflow-y-auto", children: [...groups].map(([group, entries]) => (_jsxs("section", { "aria-label": group, className: "mb-3 last:mb-0", children: [_jsx("h3", { className: "px-1 py-1 text-2xs font-semibold tracking-wider text-muted-foreground uppercase", children: group }), _jsx("dl", { children: entries.map((s) => (_jsxs("div", { className: "flex items-center justify-between gap-4 rounded-lg px-1 py-1 text-sm", children: [_jsx("dt", { children: s.label }), _jsx("dd", { children: _jsx(ShortcutKeys, { keys: s.keys, mac: mac }) })] }, s.id))) })] }, group))) })] }) }));
109
+ }