@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.
- package/README.md +235 -1
- package/dist/agent/AgentChat.d.ts +8 -1
- package/dist/agent/AgentChat.js +43 -2
- package/dist/agent/WorkflowRenderers.d.ts +61 -0
- package/dist/agent/WorkflowRenderers.js +102 -0
- package/dist/agent/controller.d.ts +19 -0
- package/dist/agent/controller.js +33 -0
- package/dist/agent/copilotkit/CopilotKitAgentProvider.js +2 -2
- package/dist/agent/index.d.ts +4 -2
- package/dist/agent/index.js +3 -1
- package/dist/agent/mock.d.ts +26 -4
- package/dist/agent/mock.js +68 -37
- package/dist/agent/renderers.d.ts +49 -0
- package/dist/agent/renderers.js +27 -0
- package/dist/appearance/brands.d.ts +17 -0
- package/dist/appearance/brands.js +14 -0
- package/dist/chat/MessageBubble.js +1 -1
- package/dist/chat/MessageList.d.ts +9 -2
- package/dist/chat/MessageList.js +11 -8
- package/dist/css/appearance.js +1 -1
- package/dist/css/calliope.css +5 -0
- package/dist/css/tokens.css +5 -0
- package/dist/index.d.ts +9 -1
- package/dist/index.js +8 -1
- package/dist/primitives/Badge.d.ts +14 -0
- package/dist/primitives/Badge.js +25 -0
- package/dist/primitives/ContextMenu.d.ts +18 -0
- package/dist/primitives/ContextMenu.js +34 -0
- package/dist/primitives/Menubar.d.ts +21 -0
- package/dist/primitives/Menubar.js +43 -0
- package/dist/primitives/ScrollArea.d.ts +16 -0
- package/dist/primitives/ScrollArea.js +11 -0
- package/dist/primitives/Toast.d.ts +37 -0
- package/dist/primitives/Toast.js +43 -0
- package/dist/primitives/Tooltip.d.ts +16 -0
- package/dist/primitives/Tooltip.js +26 -0
- package/dist/provider/messages.d.ts +217 -0
- package/dist/provider/messages.js +238 -0
- package/dist/shell/RailPanel.js +1 -1
- package/dist/theme/css-layer.js +2 -1
- package/dist/theme/tokens.generated.d.ts +3 -0
- package/dist/theme/tokens.generated.js +3 -0
- package/dist/workbench/ActivityBar.d.ts +37 -0
- package/dist/workbench/ActivityBar.js +57 -0
- package/dist/workbench/BottomPanel.d.ts +30 -0
- package/dist/workbench/BottomPanel.js +20 -0
- package/dist/workbench/CodeEditor.d.ts +48 -0
- package/dist/workbench/CodeEditor.js +153 -0
- package/dist/workbench/CommandPalette.d.ts +49 -0
- package/dist/workbench/CommandPalette.js +114 -0
- package/dist/workbench/DataChart.d.ts +45 -0
- package/dist/workbench/DataChart.js +146 -0
- package/dist/workbench/DataGrid.d.ts +89 -0
- package/dist/workbench/DataGrid.js +590 -0
- package/dist/workbench/DataProfile.d.ts +17 -0
- package/dist/workbench/DataProfile.js +51 -0
- package/dist/workbench/EditorTabs.d.ts +62 -0
- package/dist/workbench/EditorTabs.js +115 -0
- package/dist/workbench/FileBrowser.d.ts +62 -0
- package/dist/workbench/FileBrowser.js +117 -0
- package/dist/workbench/FileGrid.d.ts +13 -0
- package/dist/workbench/FileGrid.js +114 -0
- package/dist/workbench/FileIcon.d.ts +10 -0
- package/dist/workbench/FileIcon.js +24 -0
- package/dist/workbench/FileList.d.ts +35 -0
- package/dist/workbench/FileList.js +160 -0
- package/dist/workbench/FilePreview.d.ts +66 -0
- package/dist/workbench/FilePreview.js +108 -0
- package/dist/workbench/FileTree.d.ts +37 -0
- package/dist/workbench/FileTree.js +218 -0
- package/dist/workbench/GridCell.d.ts +19 -0
- package/dist/workbench/GridCell.js +50 -0
- package/dist/workbench/GridDetails.d.ts +31 -0
- package/dist/workbench/GridDetails.js +78 -0
- package/dist/workbench/MenuBar.d.ts +49 -0
- package/dist/workbench/MenuBar.js +25 -0
- package/dist/workbench/PaneGroup.d.ts +50 -0
- package/dist/workbench/PaneGroup.js +146 -0
- package/dist/workbench/PendingChangesBar.d.ts +20 -0
- package/dist/workbench/PendingChangesBar.js +19 -0
- package/dist/workbench/Shortcuts.d.ts +50 -0
- package/dist/workbench/Shortcuts.js +109 -0
- package/dist/workbench/SqlEditor.d.ts +21 -0
- package/dist/workbench/SqlEditor.js +79 -0
- package/dist/workbench/StatusBar.d.ts +28 -0
- package/dist/workbench/StatusBar.js +26 -0
- package/dist/workbench/TruncationBanner.d.ts +11 -0
- package/dist/workbench/TruncationBanner.js +12 -0
- package/dist/workbench/UploadDropzone.d.ts +52 -0
- package/dist/workbench/UploadDropzone.js +115 -0
- package/dist/workbench/WorkbenchShell.d.ts +36 -0
- package/dist/workbench/WorkbenchShell.js +20 -0
- package/dist/workbench/dnd.d.ts +17 -0
- package/dist/workbench/dnd.js +77 -0
- package/dist/workbench/download.d.ts +11 -0
- package/dist/workbench/download.js +28 -0
- package/dist/workbench/editor-theme.d.ts +42 -0
- package/dist/workbench/editor-theme.js +113 -0
- package/dist/workbench/grid-format.d.ts +59 -0
- package/dist/workbench/grid-format.js +169 -0
- package/dist/workbench/grid-model.d.ts +75 -0
- package/dist/workbench/grid-model.js +259 -0
- package/dist/workbench/grid-selection.d.ts +69 -0
- package/dist/workbench/grid-selection.js +183 -0
- package/dist/workbench/grid-stats.d.ts +58 -0
- package/dist/workbench/grid-stats.js +185 -0
- package/dist/workbench/index.d.ts +44 -0
- package/dist/workbench/index.js +47 -0
- package/dist/workbench/keys.d.ts +23 -0
- package/dist/workbench/keys.js +105 -0
- package/dist/workbench/model.d.ts +72 -0
- package/dist/workbench/model.js +214 -0
- package/dist/workbench/palette.d.ts +17 -0
- package/dist/workbench/palette.js +45 -0
- package/dist/workbench/pending.d.ts +92 -0
- package/dist/workbench/pending.js +207 -0
- package/dist/workbench/selection.d.ts +16 -0
- package/dist/workbench/selection.js +46 -0
- package/dist/workbench/sql-completion.d.ts +107 -0
- package/dist/workbench/sql-completion.js +475 -0
- package/dist/workbench/typeahead.d.ts +15 -0
- package/dist/workbench/typeahead.js +22 -0
- package/dist/workflow/WorkflowRuntimeControls.d.ts +21 -0
- package/dist/workflow/WorkflowRuntimeControls.js +54 -0
- package/dist/workflow/runtime-controls.d.ts +29 -0
- package/dist/workflow/runtime-controls.js +10 -0
- 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
|
+
}
|