@calliopelabs/ui 0.16.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 (71) hide show
  1. package/README.md +94 -1
  2. package/dist/index.d.ts +3 -0
  3. package/dist/index.js +2 -0
  4. package/dist/primitives/Menubar.d.ts +21 -0
  5. package/dist/primitives/Menubar.js +43 -0
  6. package/dist/provider/messages.d.ts +148 -0
  7. package/dist/provider/messages.js +178 -0
  8. package/dist/shell/RailPanel.js +1 -1
  9. package/dist/workbench/ActivityBar.d.ts +37 -0
  10. package/dist/workbench/ActivityBar.js +57 -0
  11. package/dist/workbench/BottomPanel.d.ts +30 -0
  12. package/dist/workbench/BottomPanel.js +20 -0
  13. package/dist/workbench/CodeEditor.d.ts +48 -0
  14. package/dist/workbench/CodeEditor.js +153 -0
  15. package/dist/workbench/CommandPalette.d.ts +49 -0
  16. package/dist/workbench/CommandPalette.js +114 -0
  17. package/dist/workbench/DataChart.d.ts +45 -0
  18. package/dist/workbench/DataChart.js +146 -0
  19. package/dist/workbench/DataGrid.d.ts +89 -0
  20. package/dist/workbench/DataGrid.js +590 -0
  21. package/dist/workbench/DataProfile.d.ts +17 -0
  22. package/dist/workbench/DataProfile.js +51 -0
  23. package/dist/workbench/EditorTabs.d.ts +62 -0
  24. package/dist/workbench/EditorTabs.js +115 -0
  25. package/dist/workbench/GridCell.d.ts +19 -0
  26. package/dist/workbench/GridCell.js +50 -0
  27. package/dist/workbench/GridDetails.d.ts +31 -0
  28. package/dist/workbench/GridDetails.js +78 -0
  29. package/dist/workbench/MenuBar.d.ts +49 -0
  30. package/dist/workbench/MenuBar.js +25 -0
  31. package/dist/workbench/PaneGroup.d.ts +50 -0
  32. package/dist/workbench/PaneGroup.js +146 -0
  33. package/dist/workbench/PendingChangesBar.d.ts +20 -0
  34. package/dist/workbench/PendingChangesBar.js +19 -0
  35. package/dist/workbench/Shortcuts.d.ts +50 -0
  36. package/dist/workbench/Shortcuts.js +109 -0
  37. package/dist/workbench/SqlEditor.d.ts +21 -0
  38. package/dist/workbench/SqlEditor.js +79 -0
  39. package/dist/workbench/StatusBar.d.ts +28 -0
  40. package/dist/workbench/StatusBar.js +26 -0
  41. package/dist/workbench/TruncationBanner.d.ts +11 -0
  42. package/dist/workbench/TruncationBanner.js +12 -0
  43. package/dist/workbench/WorkbenchShell.d.ts +36 -0
  44. package/dist/workbench/WorkbenchShell.js +20 -0
  45. package/dist/workbench/download.d.ts +11 -0
  46. package/dist/workbench/download.js +28 -0
  47. package/dist/workbench/editor-theme.d.ts +42 -0
  48. package/dist/workbench/editor-theme.js +113 -0
  49. package/dist/workbench/grid-format.d.ts +59 -0
  50. package/dist/workbench/grid-format.js +169 -0
  51. package/dist/workbench/grid-model.d.ts +75 -0
  52. package/dist/workbench/grid-model.js +259 -0
  53. package/dist/workbench/grid-selection.d.ts +69 -0
  54. package/dist/workbench/grid-selection.js +183 -0
  55. package/dist/workbench/grid-stats.d.ts +58 -0
  56. package/dist/workbench/grid-stats.js +185 -0
  57. package/dist/workbench/index.d.ts +28 -0
  58. package/dist/workbench/index.js +31 -0
  59. package/dist/workbench/keys.d.ts +23 -0
  60. package/dist/workbench/keys.js +105 -0
  61. package/dist/workbench/palette.d.ts +17 -0
  62. package/dist/workbench/palette.js +45 -0
  63. package/dist/workbench/pending.d.ts +92 -0
  64. package/dist/workbench/pending.js +207 -0
  65. package/dist/workbench/sql-completion.d.ts +107 -0
  66. package/dist/workbench/sql-completion.js +475 -0
  67. package/dist/workflow/WorkflowRuntimeControls.d.ts +21 -0
  68. package/dist/workflow/WorkflowRuntimeControls.js +54 -0
  69. package/dist/workflow/runtime-controls.d.ts +29 -0
  70. package/dist/workflow/runtime-controls.js +10 -0
  71. package/package.json +13 -2
@@ -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
+ }
@@ -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,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,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
+ }