@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,49 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface PaletteItem {
|
|
3
|
+
/** Stable: the recent list and the option's key. */
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
/** The heading it sits under ("Editor", "Tables", "Open tabs"). */
|
|
7
|
+
group?: string;
|
|
8
|
+
/** A decorative icon (`aria-hidden`). */
|
|
9
|
+
icon?: React.ReactNode;
|
|
10
|
+
/** Extra words the search matches. */
|
|
11
|
+
keywords?: string;
|
|
12
|
+
/** A second, quieter line: a path, a server. */
|
|
13
|
+
description?: string;
|
|
14
|
+
/** Its chord, drawn as key caps: `"Mod+N"`. */
|
|
15
|
+
shortcut?: string;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
/** Leave the palette open after it runs (an entry that switches the palette's own mode). */
|
|
18
|
+
keepOpen?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface CommandPaletteProps {
|
|
21
|
+
open: boolean;
|
|
22
|
+
onOpenChange: (open: boolean) => void;
|
|
23
|
+
items: readonly PaletteItem[];
|
|
24
|
+
/** Run an entry. The palette has already closed unless the entry keeps it open. */
|
|
25
|
+
onSelect: (item: PaletteItem) => void;
|
|
26
|
+
/** The search text. Controlled when given, so a host can switch lists on a prefix ("@", ">"). */
|
|
27
|
+
query?: string;
|
|
28
|
+
onQueryChange?: (query: string) => void;
|
|
29
|
+
/** Rank and filter `items` by the query. False when the host already did. Defaults to true. */
|
|
30
|
+
filter?: boolean;
|
|
31
|
+
/** Ids run lately, shown first under "Recent" while the query is empty. */
|
|
32
|
+
recent?: readonly string[];
|
|
33
|
+
/** The host is still building `items` (an object index): shown under the list. */
|
|
34
|
+
loading?: boolean;
|
|
35
|
+
placeholder?: string;
|
|
36
|
+
/** Copy for no match. */
|
|
37
|
+
empty?: string;
|
|
38
|
+
/** A key on the highlighted entry that the palette does not handle (Delete to close a tab). */
|
|
39
|
+
onItemKeyDown?: (item: PaletteItem, event: React.KeyboardEvent<HTMLInputElement>) => void;
|
|
40
|
+
mac?: boolean;
|
|
41
|
+
className?: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The command palette: a modal search over commands, objects and open tabs,
|
|
45
|
+
* with the ARIA combobox pattern (the input owns a listbox; Up and Down move
|
|
46
|
+
* the highlight, Enter runs it, Escape closes). The host supplies the items
|
|
47
|
+
* and runs them; focus returns where it was when the palette closes.
|
|
48
|
+
*/
|
|
49
|
+
export declare function CommandPalette({ open, onOpenChange, items, onSelect, query: queryProp, onQueryChange, filter, recent, loading, placeholder, empty, onItemKeyDown, mac, className, }: CommandPaletteProps): React.JSX.Element;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { SearchIcon } from "lucide-react";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { cn } from "../lib/cn.js";
|
|
6
|
+
import { Dialog, DialogContent, DialogTitle } from "../overlays/Dialog.js";
|
|
7
|
+
import { useUI } from "../provider/UIProvider.js";
|
|
8
|
+
import { rankItems } from "./palette.js";
|
|
9
|
+
import { ShortcutKeys } from "./Shortcuts.js";
|
|
10
|
+
function sections(items, query, filter, recent, recentLabel) {
|
|
11
|
+
const list = filter ? rankItems(items, query) : [...items];
|
|
12
|
+
const out = [];
|
|
13
|
+
let rest = list;
|
|
14
|
+
if (query.trim() === "" && recent && recent.length > 0) {
|
|
15
|
+
const byId = new Map(list.map((i) => [i.id, i]));
|
|
16
|
+
const firsts = recent.flatMap((id) => byId.get(id) ?? []);
|
|
17
|
+
if (firsts.length > 0) {
|
|
18
|
+
out.push({ heading: recentLabel, items: firsts });
|
|
19
|
+
const taken = new Set(firsts.map((i) => i.id));
|
|
20
|
+
rest = list.filter((i) => !taken.has(i.id));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const groups = new Map();
|
|
24
|
+
for (const item of rest)
|
|
25
|
+
groups.set(item.group, [...(groups.get(item.group) ?? []), item]);
|
|
26
|
+
for (const [heading, groupItems] of groups)
|
|
27
|
+
out.push({ heading, items: groupItems });
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The command palette: a modal search over commands, objects and open tabs,
|
|
32
|
+
* with the ARIA combobox pattern (the input owns a listbox; Up and Down move
|
|
33
|
+
* the highlight, Enter runs it, Escape closes). The host supplies the items
|
|
34
|
+
* and runs them; focus returns where it was when the palette closes.
|
|
35
|
+
*/
|
|
36
|
+
export function CommandPalette({ open, onOpenChange, items, onSelect, query: queryProp, onQueryChange, filter = true, recent, loading = false, placeholder, empty, onItemKeyDown, mac, className, }) {
|
|
37
|
+
const { messages } = useUI();
|
|
38
|
+
const m = messages.workbench;
|
|
39
|
+
const [ownQuery, setOwnQuery] = React.useState("");
|
|
40
|
+
const query = queryProp ?? ownQuery;
|
|
41
|
+
const setQuery = (next) => {
|
|
42
|
+
if (queryProp === undefined)
|
|
43
|
+
setOwnQuery(next);
|
|
44
|
+
onQueryChange?.(next);
|
|
45
|
+
};
|
|
46
|
+
// The highlight belongs to one query: typing puts it back on the first match.
|
|
47
|
+
const [highlight, setHighlight] = React.useState({ query: "", index: 0 });
|
|
48
|
+
const active = highlight.query === query ? highlight.index : 0;
|
|
49
|
+
const setActive = (index) => setHighlight({ query, index });
|
|
50
|
+
const inputRef = React.useRef(null);
|
|
51
|
+
const listRef = React.useRef(null);
|
|
52
|
+
const restoreRef = React.useRef(null);
|
|
53
|
+
const baseId = React.useId();
|
|
54
|
+
const shown = sections(items, query, filter, recent, m.recent);
|
|
55
|
+
const flat = shown.flatMap((s) => s.items);
|
|
56
|
+
const current = flat[Math.min(active, flat.length - 1)];
|
|
57
|
+
const optionId = (index) => `${baseId}-option-${index}`;
|
|
58
|
+
React.useEffect(() => {
|
|
59
|
+
if (!open)
|
|
60
|
+
return;
|
|
61
|
+
const el = listRef.current?.querySelector('[aria-selected="true"]');
|
|
62
|
+
el?.scrollIntoView?.({ block: "nearest" });
|
|
63
|
+
}, [active, open]);
|
|
64
|
+
const close = () => {
|
|
65
|
+
onOpenChange(false);
|
|
66
|
+
if (queryProp === undefined)
|
|
67
|
+
setOwnQuery("");
|
|
68
|
+
};
|
|
69
|
+
const run = (item) => {
|
|
70
|
+
if (!item || item.disabled)
|
|
71
|
+
return;
|
|
72
|
+
if (!item.keepOpen)
|
|
73
|
+
close();
|
|
74
|
+
onSelect(item);
|
|
75
|
+
};
|
|
76
|
+
const move = (delta) => {
|
|
77
|
+
if (flat.length === 0)
|
|
78
|
+
return;
|
|
79
|
+
setActive((Math.min(active, flat.length - 1) + delta + flat.length) % flat.length);
|
|
80
|
+
};
|
|
81
|
+
const onKeyDown = (event) => {
|
|
82
|
+
if (event.key === "ArrowDown") {
|
|
83
|
+
event.preventDefault();
|
|
84
|
+
move(1);
|
|
85
|
+
}
|
|
86
|
+
else if (event.key === "ArrowUp") {
|
|
87
|
+
event.preventDefault();
|
|
88
|
+
move(-1);
|
|
89
|
+
}
|
|
90
|
+
else if (event.key === "Enter") {
|
|
91
|
+
event.preventDefault();
|
|
92
|
+
run(current);
|
|
93
|
+
}
|
|
94
|
+
else if (current) {
|
|
95
|
+
onItemKeyDown?.(current, event);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
return (_jsx(Dialog, { open: open, onOpenChange: (next) => (next ? onOpenChange(true) : close()), children: _jsxs(DialogContent, { hideCloseButton: true, "aria-describedby": undefined, className: cn("top-24 max-w-xl translate-y-0 gap-0 overflow-hidden p-0 sm:max-w-xl", className), onOpenAutoFocus: (event) => {
|
|
99
|
+
restoreRef.current = document.activeElement;
|
|
100
|
+
event.preventDefault();
|
|
101
|
+
inputRef.current?.focus();
|
|
102
|
+
}, onCloseAutoFocus: (event) => {
|
|
103
|
+
event.preventDefault();
|
|
104
|
+
if (restoreRef.current instanceof HTMLElement)
|
|
105
|
+
restoreRef.current.focus();
|
|
106
|
+
}, children: [_jsx(DialogTitle, { className: "sr-only", children: m.commandPalette }), _jsxs("div", { className: "flex items-center gap-2 border-b px-3", children: [_jsx(SearchIcon, { "aria-hidden": true, className: "size-4 shrink-0 text-muted-foreground" }), _jsx("input", { ref: inputRef, role: "combobox", "aria-expanded": flat.length > 0, "aria-controls": `${baseId}-list`, "aria-autocomplete": "list", "aria-activedescendant": current ? optionId(flat.indexOf(current)) : undefined, "aria-label": placeholder ?? m.searchCommands, placeholder: placeholder ?? m.searchCommands, className: "h-11 w-full bg-transparent text-sm outline-hidden placeholder:text-muted-foreground", value: query, onChange: (event) => setQuery(event.target.value), onKeyDown: onKeyDown })] }), _jsx("div", { ref: listRef, id: `${baseId}-list`, role: "listbox", hidden: flat.length === 0, "aria-label": m.commandPalette, "aria-busy": loading || undefined, className: "max-h-[min(24rem,60vh)] overflow-y-auto p-1", children: shown.map((section, s) => {
|
|
107
|
+
const headingId = `${baseId}-group-${s}`;
|
|
108
|
+
return (_jsxs("div", { role: "group", "aria-labelledby": section.heading ? headingId : undefined, children: [section.heading && (_jsx("div", { id: headingId, role: "presentation", className: "px-2 pt-2 pb-1 text-2xs font-semibold tracking-wider text-muted-foreground uppercase", children: section.heading })), section.items.map((item) => {
|
|
109
|
+
const i = flat.indexOf(item);
|
|
110
|
+
const selected = item === current;
|
|
111
|
+
return (_jsxs("div", { id: optionId(i), role: "option", tabIndex: -1, "aria-selected": selected, "aria-disabled": item.disabled || undefined, className: cn("flex cursor-pointer items-center gap-2 rounded-lg px-2 py-1.5 text-sm", selected && "bg-accent text-accent-foreground", item.disabled && "cursor-default opacity-50"), onPointerMove: () => i !== active && setActive(i), onClick: () => run(item), children: [item.icon && (_jsx("span", { "aria-hidden": true, className: "flex shrink-0 text-muted-foreground [&_svg]:size-4", children: item.icon })), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "block truncate", children: item.label }), item.description && (_jsxs(_Fragment, { children: [" ", _jsx("span", { className: "block truncate text-xs text-muted-foreground", children: item.description })] }))] }), item.shortcut && _jsx(ShortcutKeys, { keys: item.shortcut, mac: mac })] }, `${section.heading ?? ""}:${item.id}`));
|
|
112
|
+
})] }, section.heading ?? `section-${s}`));
|
|
113
|
+
}) }), (loading || flat.length === 0) && (_jsx("div", { role: "status", className: "px-3 py-4 text-center text-sm text-muted-foreground", children: loading ? m.commandsLoading : (empty ?? m.noCommands) }))] }) }));
|
|
114
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { type GridColumn, type GridRow } from "./grid-model.js";
|
|
3
|
+
/**
|
|
4
|
+
* The parts of Recharts the chart draws with. The host hands over the
|
|
5
|
+
* module (`loadCharts={() => import("recharts")}`), so the package never
|
|
6
|
+
* names Recharts in an import: an app without it builds, and one with it
|
|
7
|
+
* loads it only when a chart opens.
|
|
8
|
+
*/
|
|
9
|
+
export interface ChartLibrary {
|
|
10
|
+
ResponsiveContainer: React.ElementType;
|
|
11
|
+
BarChart: React.ElementType;
|
|
12
|
+
Bar: React.ElementType;
|
|
13
|
+
LineChart: React.ElementType;
|
|
14
|
+
Line: React.ElementType;
|
|
15
|
+
AreaChart: React.ElementType;
|
|
16
|
+
Area: React.ElementType;
|
|
17
|
+
ScatterChart: React.ElementType;
|
|
18
|
+
Scatter: React.ElementType;
|
|
19
|
+
PieChart: React.ElementType;
|
|
20
|
+
Pie: React.ElementType;
|
|
21
|
+
Cell: React.ElementType;
|
|
22
|
+
XAxis: React.ElementType;
|
|
23
|
+
YAxis: React.ElementType;
|
|
24
|
+
CartesianGrid: React.ElementType;
|
|
25
|
+
Tooltip: React.ElementType;
|
|
26
|
+
Legend: React.ElementType;
|
|
27
|
+
}
|
|
28
|
+
export type ChartKind = "bar" | "line" | "area" | "scatter" | "pie";
|
|
29
|
+
export interface DataChartProps {
|
|
30
|
+
columns: readonly (string | GridColumn)[];
|
|
31
|
+
rows: readonly GridRow[];
|
|
32
|
+
/** Resolves to Recharts (an optional peer): `() => import("recharts")`. Called once, when the chart mounts. */
|
|
33
|
+
loadCharts: () => Promise<ChartLibrary>;
|
|
34
|
+
/** A note that the rows themselves are capped. */
|
|
35
|
+
truncated?: string | null;
|
|
36
|
+
/** Rows plotted at most. */
|
|
37
|
+
rowCap?: number;
|
|
38
|
+
className?: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* A quick chart of a result: bar, line, area, scatter or pie, an X column
|
|
42
|
+
* and one or more Y columns, coloured by the theme's chart series. Recharts
|
|
43
|
+
* is the host's to supply (`loadCharts`); without it nothing is loaded.
|
|
44
|
+
*/
|
|
45
|
+
export declare function DataChart(props: DataChartProps): React.JSX.Element;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { ActivityIcon, ChartColumnIcon, ChartPieIcon, CircleIcon, TrendingUpIcon, } from "lucide-react";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { cn } from "../lib/cn.js";
|
|
6
|
+
import { Segmented } from "../primitives/Segmented.js";
|
|
7
|
+
import { Select } from "../primitives/Select.js";
|
|
8
|
+
import { useUI } from "../provider/UIProvider.js";
|
|
9
|
+
import { EmptyState } from "../states/EmptyState.js";
|
|
10
|
+
import { ErrorState } from "../states/ErrorState.js";
|
|
11
|
+
import { LoadingState } from "../states/LoadingState.js";
|
|
12
|
+
import { columnSignature } from "./grid-model.js";
|
|
13
|
+
import { TruncationBanner } from "./TruncationBanner.js";
|
|
14
|
+
/** Recharts puts every point in the DOM, so the chart plots a prefix. */
|
|
15
|
+
const CHART_ROW_CAP = 500;
|
|
16
|
+
/** The five chart series, from the theme: they follow the ground and the accent. */
|
|
17
|
+
const SERIES = [
|
|
18
|
+
"var(--chart-1)",
|
|
19
|
+
"var(--chart-2)",
|
|
20
|
+
"var(--chart-3)",
|
|
21
|
+
"var(--chart-4)",
|
|
22
|
+
"var(--chart-5)",
|
|
23
|
+
];
|
|
24
|
+
const KIND_ICON = {
|
|
25
|
+
bar: ChartColumnIcon,
|
|
26
|
+
line: TrendingUpIcon,
|
|
27
|
+
area: ActivityIcon,
|
|
28
|
+
scatter: CircleIcon,
|
|
29
|
+
pie: ChartPieIcon,
|
|
30
|
+
};
|
|
31
|
+
function isNumeric(value) {
|
|
32
|
+
if (typeof value === "number")
|
|
33
|
+
return Number.isFinite(value);
|
|
34
|
+
return typeof value === "string" && value.trim() !== "" && Number.isFinite(Number(value));
|
|
35
|
+
}
|
|
36
|
+
/** Columns mostly numeric in a sample of their first values, and the rest. */
|
|
37
|
+
function classify(names, rows) {
|
|
38
|
+
const numeric = [];
|
|
39
|
+
const categorical = [];
|
|
40
|
+
for (const name of names) {
|
|
41
|
+
const sample = [];
|
|
42
|
+
for (const row of rows) {
|
|
43
|
+
const v = row[name];
|
|
44
|
+
if (v !== null && v !== undefined)
|
|
45
|
+
sample.push(v);
|
|
46
|
+
if (sample.length >= 20)
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
const share = sample.length > 0 ? sample.filter(isNumeric).length / sample.length : 0;
|
|
50
|
+
(share > 0.7 ? numeric : categorical).push(name);
|
|
51
|
+
}
|
|
52
|
+
return { numeric, categorical };
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* A quick chart of a result: bar, line, area, scatter or pie, an X column
|
|
56
|
+
* and one or more Y columns, coloured by the theme's chart series. Recharts
|
|
57
|
+
* is the host's to supply (`loadCharts`); without it nothing is loaded.
|
|
58
|
+
*/
|
|
59
|
+
export function DataChart(props) {
|
|
60
|
+
return _jsx(ChartSurface, { ...props }, columnSignature(props.columns));
|
|
61
|
+
}
|
|
62
|
+
function ChartSurface({ columns, rows, loadCharts, truncated, rowCap = CHART_ROW_CAP, className, }) {
|
|
63
|
+
const { messages } = useUI();
|
|
64
|
+
const t = messages.workbench;
|
|
65
|
+
const names = React.useMemo(() => columns.map((c) => (typeof c === "string" ? c : c.name)), [columns]);
|
|
66
|
+
const { numeric, categorical } = React.useMemo(() => classify(names, rows), [names, rows]);
|
|
67
|
+
const [kind, setKind] = React.useState("bar");
|
|
68
|
+
const [x, setX] = React.useState(() => categorical[0] ?? names[0] ?? "");
|
|
69
|
+
const [ys, setYs] = React.useState(() => {
|
|
70
|
+
const first = numeric.find((n) => n !== (categorical[0] ?? names[0])) ?? names[1];
|
|
71
|
+
return first ? [first] : [];
|
|
72
|
+
});
|
|
73
|
+
const [lib, setLib] = React.useState(null);
|
|
74
|
+
const [failed, setFailed] = React.useState(null);
|
|
75
|
+
const [attempt, setAttempt] = React.useState(0);
|
|
76
|
+
const loader = React.useRef(loadCharts);
|
|
77
|
+
React.useEffect(() => {
|
|
78
|
+
let live = true;
|
|
79
|
+
loader.current().then((module) => live && setLib(module), (reason) => live && setFailed(reason instanceof Error ? reason.message : String(reason)));
|
|
80
|
+
return () => {
|
|
81
|
+
live = false;
|
|
82
|
+
};
|
|
83
|
+
}, [attempt]);
|
|
84
|
+
const data = React.useMemo(() => rows.slice(0, rowCap).map((row) => {
|
|
85
|
+
const entry = { [x]: row[x] };
|
|
86
|
+
for (const y of ys) {
|
|
87
|
+
const v = row[y];
|
|
88
|
+
entry[y] = typeof v === "number" ? v : Number(v) || 0;
|
|
89
|
+
}
|
|
90
|
+
return entry;
|
|
91
|
+
}), [rows, rowCap, x, ys]);
|
|
92
|
+
if (rows.length === 0)
|
|
93
|
+
return _jsx(EmptyState, { title: t.chartNoData, className: className });
|
|
94
|
+
const notes = [truncated, rows.length > rowCap ? t.chartCap(rowCap, rows.length) : null].filter(Boolean);
|
|
95
|
+
const tick = { fontSize: 11, fill: "var(--muted-foreground)" };
|
|
96
|
+
const tooltip = {
|
|
97
|
+
contentStyle: {
|
|
98
|
+
backgroundColor: "var(--popover)",
|
|
99
|
+
border: "1px solid var(--border)",
|
|
100
|
+
borderRadius: "var(--radius)",
|
|
101
|
+
color: "var(--popover-foreground)",
|
|
102
|
+
fontSize: 12,
|
|
103
|
+
},
|
|
104
|
+
itemStyle: { color: "var(--popover-foreground)" },
|
|
105
|
+
labelStyle: { color: "var(--muted-foreground)" },
|
|
106
|
+
cursor: { fill: "var(--muted)", stroke: "var(--border)" },
|
|
107
|
+
};
|
|
108
|
+
const legend = { wrapperStyle: { fontSize: 12, color: "var(--muted-foreground)" } };
|
|
109
|
+
let chart;
|
|
110
|
+
if (failed) {
|
|
111
|
+
chart = (_jsx(ErrorState, { title: t.chartLoadFailed, error: failed, onRetry: () => {
|
|
112
|
+
setFailed(null);
|
|
113
|
+
setAttempt((n) => n + 1);
|
|
114
|
+
}, retryLabel: t.retry }));
|
|
115
|
+
}
|
|
116
|
+
else if (!lib) {
|
|
117
|
+
chart = _jsx(LoadingState, { label: t.chartLoading, variant: "rows", rows: 4 });
|
|
118
|
+
}
|
|
119
|
+
else if (ys.length === 0) {
|
|
120
|
+
chart = _jsx(EmptyState, { title: t.chartPickY });
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
const L = lib;
|
|
124
|
+
const axes = (_jsxs(_Fragment, { children: [_jsx(L.CartesianGrid, { strokeDasharray: "3 3", stroke: "var(--border)" }), _jsx(L.XAxis, { dataKey: x, tick: tick, tickLine: false, stroke: "var(--border)" }), _jsx(L.YAxis, { tick: tick, tickLine: false, stroke: "var(--border)" }), _jsx(L.Tooltip, { ...tooltip }), ys.length > 1 && _jsx(L.Legend, { ...legend })] }));
|
|
125
|
+
const series = (i) => SERIES[i % SERIES.length];
|
|
126
|
+
chart = (_jsx(L.ResponsiveContainer, { width: "100%", height: "100%", children: kind === "bar" ? (_jsxs(L.BarChart, { data: data, children: [axes, ys.map((y, i) => (_jsx(L.Bar, { dataKey: y, fill: series(i), radius: [2, 2, 0, 0], isAnimationActive: false }, y)))] })) : kind === "line" ? (_jsxs(L.LineChart, { data: data, children: [axes, ys.map((y, i) => (_jsx(L.Line, { dataKey: y, stroke: series(i), strokeWidth: 2, dot: { r: 2, fill: series(i) }, isAnimationActive: false }, y)))] })) : kind === "area" ? (_jsxs(L.AreaChart, { data: data, children: [axes, ys.map((y, i) => (_jsx(L.Area, { dataKey: y, stroke: series(i), fill: series(i), fillOpacity: 0.2, strokeWidth: 2, isAnimationActive: false }, y)))] })) : kind === "scatter" ? (_jsxs(L.ScatterChart, { children: [_jsx(L.CartesianGrid, { strokeDasharray: "3 3", stroke: "var(--border)" }), _jsx(L.XAxis, { dataKey: x, name: x, type: "number", tick: tick, tickLine: false, stroke: "var(--border)" }), _jsx(L.YAxis, { dataKey: ys[0], name: ys[0], tick: tick, tickLine: false, stroke: "var(--border)" }), _jsx(L.Tooltip, { ...tooltip }), _jsx(L.Scatter, { data: data, fill: series(0), isAnimationActive: false })] })) : (_jsxs(L.PieChart, { children: [_jsx(L.Tooltip, { ...tooltip }), _jsx(L.Legend, { ...legend }), _jsx(L.Pie, { data: data.map((d) => ({
|
|
127
|
+
name: String(d[x] ?? ""),
|
|
128
|
+
value: Number(d[ys[0] ?? ""]) || 0,
|
|
129
|
+
})), dataKey: "value", nameKey: "name", outerRadius: "80%", stroke: "var(--background)", isAnimationActive: false, children: data.map((_, i) => (_jsx(L.Cell, { fill: series(i) }, i))) })] })) }));
|
|
130
|
+
}
|
|
131
|
+
return (_jsxs("section", { "aria-label": t.chartLabel, "data-slot": "data-chart", className: cn("flex min-h-0 flex-col", className), children: [notes.length > 0 && _jsx(TruncationBanner, { children: notes.join(" ") }), _jsxs("div", { className: "flex shrink-0 flex-wrap items-center gap-3 border-b bg-muted/40 px-3 py-1.5 text-xs", children: [_jsx(Segmented, { label: t.chartType, size: "sm", value: kind, onValueChange: setKind, options: ["bar", "line", "area", "scatter", "pie"].map((k) => ({
|
|
132
|
+
value: k,
|
|
133
|
+
label: t.chartKind(k),
|
|
134
|
+
icon: KIND_ICON[k],
|
|
135
|
+
})) }), _jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx("span", { className: "text-muted-foreground", "aria-hidden": true, children: t.chartX }), _jsx(Select, { size: "sm", "aria-label": t.chartX, value: x, onValueChange: (next) => {
|
|
136
|
+
setX(next);
|
|
137
|
+
setYs((prev) => prev.filter((y) => y !== next));
|
|
138
|
+
}, options: names.map((n) => ({ value: n, label: n })) })] }), _jsxs("fieldset", { className: "flex min-w-0 flex-wrap items-center gap-1", children: [_jsx("legend", { className: "sr-only", children: t.chartY }), _jsx("span", { className: "mr-0.5 text-muted-foreground", "aria-hidden": true, children: t.chartY }), names
|
|
139
|
+
.filter((n) => n !== x)
|
|
140
|
+
.map((name) => {
|
|
141
|
+
const on = ys.includes(name);
|
|
142
|
+
return (_jsxs("button", { type: "button", "aria-pressed": on, className: cn("flex items-center gap-1 rounded-full border px-2 py-0.5 outline-none focus-visible:ring-2 focus-visible:ring-ring/50", on
|
|
143
|
+
? "border-foreground/30 text-foreground"
|
|
144
|
+
: "text-muted-foreground hover:text-foreground"), onClick: () => setYs((prev) => (on ? prev.filter((y) => y !== name) : [...prev, name])), children: [on && (_jsx("span", { "aria-hidden": true, className: "size-2 rounded-full", style: { backgroundColor: SERIES[ys.indexOf(name) % SERIES.length] } })), name] }, name));
|
|
145
|
+
})] })] }), _jsx("div", { className: "min-h-64 flex-1 p-4", children: chart })] }));
|
|
146
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { type CellMatrix, type CopyFormat, type ExportFile, type ExportFormat } from "./grid-format.js";
|
|
3
|
+
import { type GridColumn, type GridRow } from "./grid-model.js";
|
|
4
|
+
import { type ChangeSet, type PendingChanges } from "./pending.js";
|
|
5
|
+
/** A format the host adds to the export menu (a workbook); the host writes the file. */
|
|
6
|
+
export interface GridExportOption {
|
|
7
|
+
id: string;
|
|
8
|
+
label: string;
|
|
9
|
+
}
|
|
10
|
+
/** An export the person asked for. `file` is the library's text for its own formats, null for a host's. */
|
|
11
|
+
export interface GridExportRequest {
|
|
12
|
+
format: ExportFormat | (string & {});
|
|
13
|
+
matrix: CellMatrix;
|
|
14
|
+
file: ExportFile | null;
|
|
15
|
+
}
|
|
16
|
+
/** A copy the grid put on the clipboard. */
|
|
17
|
+
export interface GridCopyEvent {
|
|
18
|
+
format: CopyFormat;
|
|
19
|
+
text: string;
|
|
20
|
+
/** Cells copied: rows times columns. */
|
|
21
|
+
cells: number;
|
|
22
|
+
}
|
|
23
|
+
/** A commit's failure, as the host rejects `onCommit` with it. `key` is the row key in the change set. */
|
|
24
|
+
export interface GridCommitError {
|
|
25
|
+
message: string;
|
|
26
|
+
key?: number;
|
|
27
|
+
}
|
|
28
|
+
export interface DataGridProps {
|
|
29
|
+
/** Names, or descriptions with a type, keys and nullability. Undeclared types are sniffed from the rows. */
|
|
30
|
+
columns: readonly (string | GridColumn)[];
|
|
31
|
+
rows: readonly GridRow[];
|
|
32
|
+
/** The grid's accessible name. */
|
|
33
|
+
label?: string;
|
|
34
|
+
loading?: boolean;
|
|
35
|
+
error?: string | {
|
|
36
|
+
message: string;
|
|
37
|
+
} | null;
|
|
38
|
+
onRetry?: () => void;
|
|
39
|
+
/** The server's total when it capped the rows; with `hasMore`, the banner says more exist. */
|
|
40
|
+
totalRows?: number | null;
|
|
41
|
+
hasMore?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Remembers column widths per person (the library's local store). The
|
|
44
|
+
* grid also starts clean (selection, sort, filter, hidden columns) when it
|
|
45
|
+
* changes, or when the columns do.
|
|
46
|
+
*/
|
|
47
|
+
storageKey?: string;
|
|
48
|
+
/** The table for "Copy as INSERT" and the SQL export. */
|
|
49
|
+
tableName?: string;
|
|
50
|
+
/** The exported file's name without extension. */
|
|
51
|
+
exportName?: string;
|
|
52
|
+
/** After a copy reached the clipboard: the host may say so. */
|
|
53
|
+
onCopy?: (event: GridCopyEvent) => void;
|
|
54
|
+
/**
|
|
55
|
+
* Takes over export: the grid hands every request here instead of
|
|
56
|
+
* downloading (`downloadExport(request.file)` downloads one). Required for
|
|
57
|
+
* `exportFormats`.
|
|
58
|
+
*/
|
|
59
|
+
onExport?: (request: GridExportRequest) => void;
|
|
60
|
+
/** Formats the host writes itself (xlsx), after the library's own. */
|
|
61
|
+
exportFormats?: readonly GridExportOption[];
|
|
62
|
+
/**
|
|
63
|
+
* Cells edit in place (Enter, F2, double click or typing), rows add,
|
|
64
|
+
* duplicate and delete, and the changes wait in a bar to be committed.
|
|
65
|
+
* Existing rows change only when a column is a `primaryKey`.
|
|
66
|
+
*/
|
|
67
|
+
editable?: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Writes the changes. Resolve and the grid clears them; reject (with a
|
|
70
|
+
* `GridCommitError` or an Error) and they stay, with the message shown.
|
|
71
|
+
*/
|
|
72
|
+
onCommit?: (changes: ChangeSet) => void | Promise<void>;
|
|
73
|
+
/** Controlled pending changes; leave out to let the grid keep them. */
|
|
74
|
+
pending?: PendingChanges;
|
|
75
|
+
onPendingChange?: (pending: PendingChanges) => void;
|
|
76
|
+
/** Host controls at the end of the toolbar (an edit-mode switch, a refresh). */
|
|
77
|
+
toolbar?: React.ReactNode;
|
|
78
|
+
className?: string;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* A data grid for query results and table data: rows and columns both
|
|
82
|
+
* virtualized (50,000 rows by 30 columns is a few hundred nodes), the
|
|
83
|
+
* WAI-ARIA grid pattern for the keyboard, cell, row and column selection,
|
|
84
|
+
* sort (NULLs last), filter, hidden and resizable columns, copy and export
|
|
85
|
+
* in six text formats, selection statistics, cell and row detail, and
|
|
86
|
+
* optional editing with pending changes the host commits. Host-driven: the
|
|
87
|
+
* rows come in, changes go out through callbacks, nothing fetches.
|
|
88
|
+
*/
|
|
89
|
+
export declare function DataGrid(props: DataGridProps): React.JSX.Element;
|