@calliopelabs/ui 0.14.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/README.md +235 -1
  2. package/dist/agent/AgentChat.d.ts +8 -1
  3. package/dist/agent/AgentChat.js +43 -2
  4. package/dist/agent/WorkflowRenderers.d.ts +61 -0
  5. package/dist/agent/WorkflowRenderers.js +102 -0
  6. package/dist/agent/controller.d.ts +19 -0
  7. package/dist/agent/controller.js +33 -0
  8. package/dist/agent/copilotkit/CopilotKitAgentProvider.js +2 -2
  9. package/dist/agent/index.d.ts +4 -2
  10. package/dist/agent/index.js +3 -1
  11. package/dist/agent/mock.d.ts +26 -4
  12. package/dist/agent/mock.js +68 -37
  13. package/dist/agent/renderers.d.ts +49 -0
  14. package/dist/agent/renderers.js +27 -0
  15. package/dist/appearance/brands.d.ts +17 -0
  16. package/dist/appearance/brands.js +14 -0
  17. package/dist/chat/MessageBubble.js +1 -1
  18. package/dist/chat/MessageList.d.ts +9 -2
  19. package/dist/chat/MessageList.js +11 -8
  20. package/dist/css/appearance.js +1 -1
  21. package/dist/css/calliope.css +5 -0
  22. package/dist/css/tokens.css +5 -0
  23. package/dist/index.d.ts +9 -1
  24. package/dist/index.js +8 -1
  25. package/dist/primitives/Badge.d.ts +14 -0
  26. package/dist/primitives/Badge.js +25 -0
  27. package/dist/primitives/ContextMenu.d.ts +18 -0
  28. package/dist/primitives/ContextMenu.js +34 -0
  29. package/dist/primitives/Menubar.d.ts +21 -0
  30. package/dist/primitives/Menubar.js +43 -0
  31. package/dist/primitives/ScrollArea.d.ts +16 -0
  32. package/dist/primitives/ScrollArea.js +11 -0
  33. package/dist/primitives/Toast.d.ts +37 -0
  34. package/dist/primitives/Toast.js +43 -0
  35. package/dist/primitives/Tooltip.d.ts +16 -0
  36. package/dist/primitives/Tooltip.js +26 -0
  37. package/dist/provider/messages.d.ts +217 -0
  38. package/dist/provider/messages.js +238 -0
  39. package/dist/shell/RailPanel.js +1 -1
  40. package/dist/theme/css-layer.js +2 -1
  41. package/dist/theme/tokens.generated.d.ts +3 -0
  42. package/dist/theme/tokens.generated.js +3 -0
  43. package/dist/workbench/ActivityBar.d.ts +37 -0
  44. package/dist/workbench/ActivityBar.js +57 -0
  45. package/dist/workbench/BottomPanel.d.ts +30 -0
  46. package/dist/workbench/BottomPanel.js +20 -0
  47. package/dist/workbench/CodeEditor.d.ts +48 -0
  48. package/dist/workbench/CodeEditor.js +153 -0
  49. package/dist/workbench/CommandPalette.d.ts +49 -0
  50. package/dist/workbench/CommandPalette.js +114 -0
  51. package/dist/workbench/DataChart.d.ts +45 -0
  52. package/dist/workbench/DataChart.js +146 -0
  53. package/dist/workbench/DataGrid.d.ts +89 -0
  54. package/dist/workbench/DataGrid.js +590 -0
  55. package/dist/workbench/DataProfile.d.ts +17 -0
  56. package/dist/workbench/DataProfile.js +51 -0
  57. package/dist/workbench/EditorTabs.d.ts +62 -0
  58. package/dist/workbench/EditorTabs.js +115 -0
  59. package/dist/workbench/FileBrowser.d.ts +62 -0
  60. package/dist/workbench/FileBrowser.js +117 -0
  61. package/dist/workbench/FileGrid.d.ts +13 -0
  62. package/dist/workbench/FileGrid.js +114 -0
  63. package/dist/workbench/FileIcon.d.ts +10 -0
  64. package/dist/workbench/FileIcon.js +24 -0
  65. package/dist/workbench/FileList.d.ts +35 -0
  66. package/dist/workbench/FileList.js +160 -0
  67. package/dist/workbench/FilePreview.d.ts +66 -0
  68. package/dist/workbench/FilePreview.js +108 -0
  69. package/dist/workbench/FileTree.d.ts +37 -0
  70. package/dist/workbench/FileTree.js +218 -0
  71. package/dist/workbench/GridCell.d.ts +19 -0
  72. package/dist/workbench/GridCell.js +50 -0
  73. package/dist/workbench/GridDetails.d.ts +31 -0
  74. package/dist/workbench/GridDetails.js +78 -0
  75. package/dist/workbench/MenuBar.d.ts +49 -0
  76. package/dist/workbench/MenuBar.js +25 -0
  77. package/dist/workbench/PaneGroup.d.ts +50 -0
  78. package/dist/workbench/PaneGroup.js +146 -0
  79. package/dist/workbench/PendingChangesBar.d.ts +20 -0
  80. package/dist/workbench/PendingChangesBar.js +19 -0
  81. package/dist/workbench/Shortcuts.d.ts +50 -0
  82. package/dist/workbench/Shortcuts.js +109 -0
  83. package/dist/workbench/SqlEditor.d.ts +21 -0
  84. package/dist/workbench/SqlEditor.js +79 -0
  85. package/dist/workbench/StatusBar.d.ts +28 -0
  86. package/dist/workbench/StatusBar.js +26 -0
  87. package/dist/workbench/TruncationBanner.d.ts +11 -0
  88. package/dist/workbench/TruncationBanner.js +12 -0
  89. package/dist/workbench/UploadDropzone.d.ts +52 -0
  90. package/dist/workbench/UploadDropzone.js +115 -0
  91. package/dist/workbench/WorkbenchShell.d.ts +36 -0
  92. package/dist/workbench/WorkbenchShell.js +20 -0
  93. package/dist/workbench/dnd.d.ts +17 -0
  94. package/dist/workbench/dnd.js +77 -0
  95. package/dist/workbench/download.d.ts +11 -0
  96. package/dist/workbench/download.js +28 -0
  97. package/dist/workbench/editor-theme.d.ts +42 -0
  98. package/dist/workbench/editor-theme.js +113 -0
  99. package/dist/workbench/grid-format.d.ts +59 -0
  100. package/dist/workbench/grid-format.js +169 -0
  101. package/dist/workbench/grid-model.d.ts +75 -0
  102. package/dist/workbench/grid-model.js +259 -0
  103. package/dist/workbench/grid-selection.d.ts +69 -0
  104. package/dist/workbench/grid-selection.js +183 -0
  105. package/dist/workbench/grid-stats.d.ts +58 -0
  106. package/dist/workbench/grid-stats.js +185 -0
  107. package/dist/workbench/index.d.ts +44 -0
  108. package/dist/workbench/index.js +47 -0
  109. package/dist/workbench/keys.d.ts +23 -0
  110. package/dist/workbench/keys.js +105 -0
  111. package/dist/workbench/model.d.ts +72 -0
  112. package/dist/workbench/model.js +214 -0
  113. package/dist/workbench/palette.d.ts +17 -0
  114. package/dist/workbench/palette.js +45 -0
  115. package/dist/workbench/pending.d.ts +92 -0
  116. package/dist/workbench/pending.js +207 -0
  117. package/dist/workbench/selection.d.ts +16 -0
  118. package/dist/workbench/selection.js +46 -0
  119. package/dist/workbench/sql-completion.d.ts +107 -0
  120. package/dist/workbench/sql-completion.js +475 -0
  121. package/dist/workbench/typeahead.d.ts +15 -0
  122. package/dist/workbench/typeahead.js +22 -0
  123. package/dist/workflow/WorkflowRuntimeControls.d.ts +21 -0
  124. package/dist/workflow/WorkflowRuntimeControls.js +54 -0
  125. package/dist/workflow/runtime-controls.d.ts +29 -0
  126. package/dist/workflow/runtime-controls.js +10 -0
  127. package/package.json +17 -2
@@ -0,0 +1,590 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useVirtualizer } from "@tanstack/react-virtual";
4
+ import { ArrowDownIcon, ArrowUpIcon, ChevronDownIcon, ChevronsUpDownIcon, CopyIcon, DownloadIcon, EllipsisVerticalIcon, EyeIcon, EyeOffIcon, FunnelIcon, Maximize2Icon, PlusIcon, RotateCcwIcon, SearchIcon, Trash2Icon, XIcon, } from "lucide-react";
5
+ import * as React from "react";
6
+ import { cn } from "../lib/cn.js";
7
+ import { readLocal, subscribeLocal, writeLocal } from "../lib/local-store.js";
8
+ import { Button } from "../primitives/Button.js";
9
+ import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuLabel, ContextMenuSeparator, ContextMenuShortcut, ContextMenuTrigger, } from "../primitives/ContextMenu.js";
10
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "../primitives/DropdownMenu.js";
11
+ import { useUI } from "../provider/UIProvider.js";
12
+ import { ErrorState } from "../states/ErrorState.js";
13
+ import { LoadingState } from "../states/LoadingState.js";
14
+ import { downloadExport } from "./download.js";
15
+ import { ColumnKeyIcons, ColumnTypeIcon, GridCellValue } from "./GridCell.js";
16
+ import { CellDetailDialog, RowDetailDrawer } from "./GridDetails.js";
17
+ import { EXPORT_FORMATS, exportMatrix, formatMatrix, selectionMatrix, } from "./grid-format.js";
18
+ import { buildColumnMeta, cellText, columnSignature, isNumericType, viewRowIndexes, } from "./grid-model.js";
19
+ import { EMPTY_SELECTION, extendRowSelection, extendSelection, isColumnSelected, isRowSelected, isSelected, moveActive, selectAll, selectCell, selectColumn, selectRow, selectionBounds, selectionSize, toggleCell, } from "./grid-selection.js";
20
+ import { computeStats, formatStatValue, numericSummary } from "./grid-stats.js";
21
+ import { PendingChangesBar } from "./PendingChangesBar.js";
22
+ import { NO_CHANGES, addRow, deleteRows, editCell, insertedKeys, isCellChanged, parseCellInput, pendingCounts, pendingValue, revertRow, toChangeSet, } from "./pending.js";
23
+ import { formatShortcut } from "./keys.js";
24
+ import { TruncationBanner } from "./TruncationBanner.js";
25
+ const ROW_H = 28;
26
+ const HEADER_H = 32;
27
+ const GUTTER_W = 64;
28
+ const DEFAULT_COL_W = 160;
29
+ const MIN_COL_W = 60;
30
+ const MAX_COL_W = 800;
31
+ const RESIZE_STEP = 16;
32
+ /** Rows sampled when fitting a column to its content. */
33
+ const AUTOFIT_SAMPLE = 500;
34
+ const WIDTHS_PREFIX = "calliope-ui:grid-widths:";
35
+ function parseWidths(raw) {
36
+ if (!raw)
37
+ return {};
38
+ try {
39
+ const parsed = JSON.parse(raw);
40
+ if (!parsed || typeof parsed !== "object")
41
+ return {};
42
+ const out = {};
43
+ for (const [name, width] of Object.entries(parsed)) {
44
+ if (typeof width === "number" && Number.isFinite(width))
45
+ out[name] = width;
46
+ }
47
+ return out;
48
+ }
49
+ catch {
50
+ return {};
51
+ }
52
+ }
53
+ const clampWidth = (w) => Math.min(MAX_COL_W, Math.max(MIN_COL_W, Math.round(w)));
54
+ /**
55
+ * A data grid for query results and table data: rows and columns both
56
+ * virtualized (50,000 rows by 30 columns is a few hundred nodes), the
57
+ * WAI-ARIA grid pattern for the keyboard, cell, row and column selection,
58
+ * sort (NULLs last), filter, hidden and resizable columns, copy and export
59
+ * in six text formats, selection statistics, cell and row detail, and
60
+ * optional editing with pending changes the host commits. Host-driven: the
61
+ * rows come in, changes go out through callbacks, nothing fetches.
62
+ */
63
+ export function DataGrid(props) {
64
+ const key = `${props.storageKey ?? ""}\u0000${columnSignature(props.columns)}`;
65
+ return _jsx(GridSurface, { ...props }, key);
66
+ }
67
+ function GridSurface({ columns: columnsIn, rows, label, loading = false, error, onRetry, totalRows, hasMore = false, storageKey, tableName, exportName, onCopy, onExport, exportFormats, editable = false, onCommit, pending: pendingIn, onPendingChange, toolbar, className, }) {
68
+ const { messages } = useUI();
69
+ const t = messages.workbench;
70
+ const gridId = React.useId();
71
+ const scrollRef = React.useRef(null);
72
+ const [sort, setSort] = React.useState(null);
73
+ const [filterInput, setFilterInput] = React.useState("");
74
+ const [filter, setFilter] = React.useState("");
75
+ const [hidden, setHidden] = React.useState([]);
76
+ const [selection, setSelection] = React.useState(EMPTY_SELECTION);
77
+ const [scrollLeft, setScrollLeft] = React.useState(0);
78
+ const [cellDetail, setCellDetail] = React.useState(null);
79
+ const [rowDetail, setRowDetail] = React.useState(null);
80
+ const [editing, setEditing] = React.useState(null);
81
+ const [ownPending, setOwnPending] = React.useState(NO_CHANGES);
82
+ const [committing, setCommitting] = React.useState(false);
83
+ const [commitError, setCommitError] = React.useState(null);
84
+ const pending = pendingIn ?? ownPending;
85
+ const setPending = (next) => {
86
+ if (pendingIn === undefined)
87
+ setOwnPending(next);
88
+ onPendingChange?.(next);
89
+ };
90
+ // Widths: the stored ones, with a drag in progress on top.
91
+ const storeKey = storageKey ? WIDTHS_PREFIX + storageKey : null;
92
+ const stored = React.useSyncExternalStore(subscribeLocal, () => (storeKey ? readLocal(storeKey) : null), () => null);
93
+ const [localWidths, setLocalWidths] = React.useState({});
94
+ const widths = React.useMemo(() => ({ ...parseWidths(stored), ...localWidths }), [stored, localWidths]);
95
+ const saveWidth = (name, width) => {
96
+ const next = { ...widths, [name]: clampWidth(width) };
97
+ setLocalWidths(next);
98
+ if (storeKey)
99
+ writeLocal(storeKey, JSON.stringify(next));
100
+ };
101
+ const meta = React.useMemo(() => buildColumnMeta(columnsIn, rows), [columnsIn, rows]);
102
+ const columns = React.useMemo(() => meta.filter((c) => !hidden.includes(c.name)), [meta, hidden]);
103
+ const names = React.useMemo(() => columns.map((c) => c.name), [columns]);
104
+ const primaryKeys = React.useMemo(() => meta.filter((c) => c.primaryKey).map((c) => c.name), [meta]);
105
+ React.useEffect(() => {
106
+ if (filterInput === filter)
107
+ return;
108
+ // Scanning every cell of a 50k-row result per keystroke is the one thing that can make it slow.
109
+ const timer = window.setTimeout(() => setFilter(filterInput), 150);
110
+ return () => window.clearTimeout(timer);
111
+ }, [filterInput, filter]);
112
+ /** Row keys in view order: source rows filtered and sorted, then rows inserted while editing. */
113
+ const viewKeys = React.useMemo(() => {
114
+ const keys = viewRowIndexes(rows, names, filter, sort);
115
+ return editable ? keys.concat(insertedKeys(pending)) : keys;
116
+ }, [rows, names, filter, sort, editable, pending]);
117
+ const rowCount = viewKeys.length;
118
+ const colCount = columns.length;
119
+ const widthOf = React.useCallback((name) => widths[name] ?? DEFAULT_COL_W, [widths]);
120
+ const totalWidth = columns.reduce((sum, c) => sum + widthOf(c.name), 0);
121
+ // eslint-disable-next-line react-hooks/incompatible-library -- the virtualizer is read during render by design
122
+ const rowVirtualizer = useVirtualizer({
123
+ count: rowCount,
124
+ getScrollElement: () => scrollRef.current,
125
+ estimateSize: () => ROW_H,
126
+ overscan: 12,
127
+ paddingStart: HEADER_H,
128
+ });
129
+ const columnVirtualizer = useVirtualizer({
130
+ horizontal: true,
131
+ count: colCount,
132
+ getScrollElement: () => scrollRef.current,
133
+ estimateSize: (index) => widthOf(columns[index]?.name ?? ""),
134
+ overscan: 3,
135
+ paddingStart: GUTTER_W,
136
+ });
137
+ React.useEffect(() => {
138
+ columnVirtualizer.measure();
139
+ }, [columnVirtualizer, widths, columns]);
140
+ const sourceOf = React.useCallback((key) => (key >= 0 ? rows[key] : undefined), [rows]);
141
+ const valueAt = React.useCallback((row, col) => {
142
+ const key = viewKeys[row];
143
+ const column = names[col];
144
+ if (key === undefined || column === undefined)
145
+ return undefined;
146
+ return pendingValue(pending, key, column, sourceOf(key));
147
+ }, [viewKeys, names, pending, sourceOf]);
148
+ const rowAt = (row) => {
149
+ const key = viewKeys[row];
150
+ if (key === undefined)
151
+ return {};
152
+ return pending.rows.get(key)?.values ?? sourceOf(key) ?? {};
153
+ };
154
+ function canEdit(key) {
155
+ if (!editable || key === undefined)
156
+ return false;
157
+ const state = pending.rows.get(key)?.state;
158
+ if (state === "deleted")
159
+ return false;
160
+ return state === "added" || primaryKeys.length > 0;
161
+ }
162
+ function reveal(cell) {
163
+ rowVirtualizer.scrollToIndex(cell.row, { align: "auto" });
164
+ columnVirtualizer.scrollToIndex(cell.col, { align: "auto" });
165
+ }
166
+ function focusGrid() {
167
+ scrollRef.current?.focus({ preventScroll: true });
168
+ }
169
+ const copyMatrix = () => {
170
+ if (selection.ranges.length > 0)
171
+ return selectionMatrix(selection, names, valueAt);
172
+ return {
173
+ headers: names,
174
+ rows: viewKeys.map((_, row) => names.map((_, col) => valueAt(row, col))),
175
+ };
176
+ };
177
+ function copyAs(format) {
178
+ const matrix = copyMatrix();
179
+ if (matrix.rows.length === 0)
180
+ return;
181
+ const text = formatMatrix(format, matrix, { tableName });
182
+ const cells = matrix.rows.length * matrix.headers.length;
183
+ void navigator.clipboard?.writeText(text).then(() => onCopy?.({ format, text, cells }), () => { });
184
+ }
185
+ function runExport(format) {
186
+ const matrix = {
187
+ headers: names,
188
+ rows: viewKeys.map((_, row) => names.map((_, col) => valueAt(row, col))),
189
+ };
190
+ const own = EXPORT_FORMATS.includes(format);
191
+ const file = own
192
+ ? exportMatrix(format, matrix, { tableName, baseName: exportName })
193
+ : null;
194
+ if (onExport)
195
+ onExport({ format, matrix, file });
196
+ else if (file)
197
+ downloadExport(file);
198
+ }
199
+ function startEdit(cell, seed) {
200
+ const key = viewKeys[cell.row];
201
+ if (key === undefined || !canEdit(key) || !names[cell.col])
202
+ return false;
203
+ reveal(cell);
204
+ setEditing({ key, col: cell.col, seed });
205
+ return true;
206
+ }
207
+ function finishEdit(text, move) {
208
+ if (!editing)
209
+ return;
210
+ const column = columns[editing.col];
211
+ if (text !== null && column) {
212
+ const original = pendingValue(pending, editing.key, column.name, sourceOf(editing.key));
213
+ const value = parseCellInput(text, original, column.type);
214
+ if (!Object.is(value, original)) {
215
+ setPending(editCell(pending, editing.key, column.name, value, sourceOf(editing.key)));
216
+ }
217
+ }
218
+ setEditing(null);
219
+ const active = selection.active;
220
+ if (move && active) {
221
+ const next = moveActive(selection, { row: active.row + move.row, col: active.col + move.col }, rowCount, colCount, false);
222
+ setSelection(next);
223
+ if (next.active)
224
+ reveal(next.active);
225
+ }
226
+ focusGrid();
227
+ }
228
+ /** Rows covered by the selection (or the active row), as keys. */
229
+ function selectedKeys() {
230
+ const bounds = selectionBounds(selection);
231
+ if (!bounds)
232
+ return [];
233
+ const keys = [];
234
+ for (let row = bounds.minRow; row <= bounds.maxRow; row++) {
235
+ const key = viewKeys[row];
236
+ if (key !== undefined)
237
+ keys.push(key);
238
+ }
239
+ return keys;
240
+ }
241
+ const deletable = (editable ? selectedKeys() : []).filter((key) => pending.rows.get(key)?.state === "added" || (primaryKeys.length > 0 && key >= 0));
242
+ function addNewRow(from) {
243
+ const { changes } = addRow(pending, meta.map((c) => c.name), from);
244
+ setPending(changes);
245
+ // The new row is last in view.
246
+ const cell = { row: rowCount, col: 0 };
247
+ setSelection(selectCell(cell));
248
+ requestAnimationFrame(() => reveal(cell));
249
+ }
250
+ async function commit() {
251
+ if (!onCommit || committing)
252
+ return;
253
+ const changeSet = toChangeSet(pending, { columns: meta.map((c) => c.name), primaryKeys });
254
+ setCommitting(true);
255
+ setCommitError(null);
256
+ try {
257
+ await onCommit(changeSet);
258
+ setPending(NO_CHANGES);
259
+ }
260
+ catch (reason) {
261
+ const failure = reason;
262
+ const message = typeof failure?.message === "string" && failure.message !== ""
263
+ ? failure.message
264
+ : String(reason);
265
+ const row = typeof failure?.key === "number" ? viewKeys.indexOf(failure.key) : -1;
266
+ setCommitError({ message, row: row >= 0 ? row + 1 : null });
267
+ }
268
+ finally {
269
+ setCommitting(false);
270
+ }
271
+ }
272
+ function handleKeyDown(event) {
273
+ // Keys typed into a header control or the cell editor are theirs.
274
+ if (event.target !== event.currentTarget)
275
+ return;
276
+ if (rowCount === 0 || colCount === 0)
277
+ return;
278
+ const mod = event.metaKey || event.ctrlKey;
279
+ const active = selection.active ?? { row: 0, col: 0 };
280
+ const viewport = scrollRef.current?.clientHeight ?? ROW_H * 20;
281
+ const page = Math.max(1, Math.floor((viewport - HEADER_H) / ROW_H) - 1);
282
+ const go = (row, col) => {
283
+ event.preventDefault();
284
+ // The first move lands on the first cell rather than skipping it.
285
+ const to = selection.active ? { row, col } : { row: 0, col: 0 };
286
+ const next = moveActive(selection, to, rowCount, colCount, event.shiftKey);
287
+ setSelection(next);
288
+ if (next.active)
289
+ reveal(next.active);
290
+ };
291
+ switch (event.key) {
292
+ case "ArrowDown":
293
+ return go(active.row + 1, active.col);
294
+ case "ArrowUp":
295
+ return go(active.row - 1, active.col);
296
+ case "ArrowRight":
297
+ return go(active.row, active.col + 1);
298
+ case "ArrowLeft":
299
+ return go(active.row, active.col - 1);
300
+ case "PageDown":
301
+ return go(active.row + page, active.col);
302
+ case "PageUp":
303
+ return go(active.row - page, active.col);
304
+ case "Home":
305
+ return go(mod ? 0 : active.row, 0);
306
+ case "End":
307
+ return go(mod ? rowCount - 1 : active.row, colCount - 1);
308
+ case "Enter":
309
+ event.preventDefault();
310
+ if (event.shiftKey)
311
+ setRowDetail(active.row);
312
+ else if (!startEdit(active))
313
+ setCellDetail(active);
314
+ return;
315
+ case "F2":
316
+ event.preventDefault();
317
+ startEdit(active);
318
+ return;
319
+ case "Escape":
320
+ if (selection.ranges.length === 0)
321
+ return;
322
+ event.preventDefault();
323
+ setSelection(EMPTY_SELECTION);
324
+ return;
325
+ default:
326
+ break;
327
+ }
328
+ if (mod && (event.key === "a" || event.key === "A")) {
329
+ event.preventDefault();
330
+ setSelection(selectAll(rowCount, colCount));
331
+ return;
332
+ }
333
+ if (mod && (event.key === "c" || event.key === "C")) {
334
+ event.preventDefault();
335
+ copyAs(event.shiftKey ? "tsv-headers" : "tsv");
336
+ return;
337
+ }
338
+ // Typing a character starts an edit with it, as a spreadsheet does.
339
+ if (!mod && !event.altKey && event.key.length === 1 && editable && selection.active) {
340
+ if (startEdit(selection.active, event.key))
341
+ event.preventDefault();
342
+ }
343
+ }
344
+ function handleCellMouseDown(event, cell) {
345
+ if (event.button !== 0)
346
+ return;
347
+ event.preventDefault();
348
+ focusGrid();
349
+ if (event.shiftKey)
350
+ setSelection((prev) => extendSelection(prev, cell));
351
+ else if (event.metaKey || event.ctrlKey)
352
+ setSelection((prev) => toggleCell(prev, cell));
353
+ else
354
+ setSelection(selectCell(cell));
355
+ }
356
+ function autoFit(name) {
357
+ let longest = name.length;
358
+ const sample = Math.min(viewKeys.length, AUTOFIT_SAMPLE);
359
+ const col = names.indexOf(name);
360
+ for (let row = 0; row < sample; row++) {
361
+ longest = Math.max(longest, cellText(valueAt(row, col)).length);
362
+ }
363
+ // Monospace text-xs is a shade over 7px a character; 48px is the padding and the controls.
364
+ saveWidth(name, longest * 7.3 + 48);
365
+ }
366
+ const selectedCount = selectionSize(selection);
367
+ const activeColumn = selection.active ? names[selection.active.col] : undefined;
368
+ const activeValue = selection.active
369
+ ? cellText(valueAt(selection.active.row, selection.active.col))
370
+ : undefined;
371
+ const footer = React.useMemo(() => {
372
+ const bounds = selectionBounds(selection);
373
+ if (!bounds)
374
+ return null;
375
+ const wholeColumn = rowCount > 0 &&
376
+ bounds.minCol === bounds.maxCol &&
377
+ bounds.minRow === 0 &&
378
+ bounds.maxRow === rowCount - 1;
379
+ if (wholeColumn) {
380
+ const column = names[bounds.minCol];
381
+ if (column !== undefined) {
382
+ return {
383
+ kind: "column",
384
+ name: column,
385
+ stats: computeStats(viewKeys.map((_, row) => valueAt(row, bounds.minCol))),
386
+ };
387
+ }
388
+ }
389
+ const values = [];
390
+ for (let row = bounds.minRow; row <= bounds.maxRow; row++) {
391
+ for (let col = bounds.minCol; col <= bounds.maxCol; col++) {
392
+ if (isSelected(selection, row, col))
393
+ values.push(valueAt(row, col));
394
+ }
395
+ }
396
+ return { kind: "selection", summary: numericSummary(values) };
397
+ }, [selection, rowCount, names, viewKeys, valueAt]);
398
+ const counts = pendingCounts(pending);
399
+ const truncated = rows.length > 0 && totalRows != null && totalRows > rows.length
400
+ ? t.gridTruncated(rows.length, totalRows)
401
+ : rows.length > 0 && hasMore
402
+ ? t.gridTruncated(rows.length, null)
403
+ : null;
404
+ const errorMessage = typeof error === "string" ? error : error?.message;
405
+ const virtualRows = rowVirtualizer.getVirtualItems();
406
+ const virtualColumns = columnVirtualizer.getVirtualItems();
407
+ const activeId = selection.active &&
408
+ virtualRows.some((v) => v.index === selection.active?.row) &&
409
+ virtualColumns.some((v) => v.index === selection.active?.col)
410
+ ? `${gridId}-${selection.active.row}-${selection.active.col}`
411
+ : undefined;
412
+ const extraFormats = onExport ? (exportFormats ?? []) : [];
413
+ const detailCell = cellDetail && names[cellDetail.col] !== undefined ? cellDetail : null;
414
+ const detailRow = rowDetail !== null && rowDetail < rowCount ? rowDetail : null;
415
+ let body;
416
+ if (error) {
417
+ body = (_jsx(ErrorState, { title: t.gridLoadFailed, error: errorMessage || t.gridLoadFailed, onRetry: onRetry, retryLabel: t.retry }));
418
+ }
419
+ else if (loading && rows.length === 0) {
420
+ body = _jsx(LoadingState, { label: t.gridLoading, rows: 8 });
421
+ }
422
+ else {
423
+ body = (_jsxs(ContextMenu, { children: [_jsx(ContextMenuTrigger, { asChild: true, children: _jsxs("div", { ref: scrollRef, role: "grid", tabIndex: 0, "aria-label": label ?? t.gridLabel, "aria-rowcount": rowCount + 1, "aria-colcount": colCount + 1, "aria-multiselectable": true, "aria-busy": loading || undefined, "aria-activedescendant": activeId, onKeyDown: handleKeyDown, onScroll: (event) => {
424
+ const left = event.currentTarget.scrollLeft;
425
+ setScrollLeft((prev) => (prev === left ? prev : left));
426
+ }, className: "relative min-h-0 flex-1 overflow-auto font-mono text-xs outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-inset", children: [_jsxs("div", { role: "rowgroup", className: "relative", style: {
427
+ width: GUTTER_W + totalWidth,
428
+ height: Math.max(rowVirtualizer.getTotalSize(), HEADER_H),
429
+ }, children: [_jsxs("div", { role: "row", "aria-rowindex": 1, className: "sticky top-0 z-20 bg-muted", style: { height: HEADER_H, width: GUTTER_W + totalWidth }, children: [_jsx("div", { role: "columnheader", "aria-colindex": 1, className: "absolute top-0 left-0 z-10 flex h-full border-r border-b bg-muted", style: { width: GUTTER_W, transform: `translateX(${scrollLeft}px)` }, children: _jsx("button", { type: "button", "aria-label": t.gridSelectAll, className: "flex flex-1 items-center justify-center text-muted-foreground outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50", onClick: () => {
430
+ setSelection(selectAll(rowCount, colCount));
431
+ focusGrid();
432
+ }, children: "#" }) }), virtualColumns.map((vc) => {
433
+ const column = columns[vc.index];
434
+ if (!column)
435
+ return null;
436
+ return (_jsx(HeaderCell, { column: column, index: vc.index, left: vc.start, width: vc.size, sort: sort?.column === column.name ? sort.direction : null, selected: isColumnSelected(selection, vc.index, rowCount), hiddenCount: hidden.length, activeValue: activeColumn === column.name ? activeValue : undefined, onSort: (direction) => setSort(direction ? { column: column.name, direction } : null), onSelect: (additive) => {
437
+ setSelection((prev) => selectColumn(prev, vc.index, rowCount, additive));
438
+ focusGrid();
439
+ }, onResize: (width) => saveWidth(column.name, width), onResizing: (width) => setLocalWidths((prev) => ({ ...prev, [column.name]: clampWidth(width) })), onAutoFit: () => autoFit(column.name), onHide: () => {
440
+ setHidden((prev) => [...prev, column.name]);
441
+ setSelection(EMPTY_SELECTION);
442
+ }, onShowAll: () => setHidden([]), onFilterByValue: (value) => {
443
+ setFilterInput(value);
444
+ setFilter(value);
445
+ }, onCopyName: () => void navigator.clipboard?.writeText(column.name) }, column.name));
446
+ })] }), virtualRows.map((vr) => {
447
+ const key = viewKeys[vr.index];
448
+ if (key === undefined)
449
+ return null;
450
+ const state = pending.rows.get(key)?.state;
451
+ const rowSelected = isRowSelected(selection, vr.index, colCount);
452
+ const failed = commitError?.row === vr.index + 1;
453
+ return (_jsxs("div", { role: "row", "aria-rowindex": vr.index + 2, "aria-selected": rowSelected, "data-state": state, className: cn("absolute top-0 left-0", vr.index % 2 === 1 && "bg-muted/30", state === "deleted" && "text-muted-foreground", failed && "ring-1 ring-danger ring-inset"), style: {
454
+ height: vr.size,
455
+ width: GUTTER_W + totalWidth,
456
+ transform: `translateY(${vr.start}px)`,
457
+ }, children: [_jsxs("div", { role: "rowheader", "aria-colindex": 1, className: cn("absolute top-0 left-0 z-10 flex h-full cursor-pointer items-center justify-end gap-1 border-r border-b bg-muted px-2 text-muted-foreground tabular-nums select-none", state && "border-l-2", state === "added" && "border-l-success", state === "modified" && "border-l-warning", state === "deleted" && "border-l-danger", rowSelected && "bg-primary/15 text-foreground"), style: { width: GUTTER_W, transform: `translateX(${scrollLeft}px)` }, onMouseDown: (event) => {
458
+ if (event.button !== 0)
459
+ return;
460
+ event.preventDefault();
461
+ if (event.shiftKey)
462
+ setSelection((prev) => extendRowSelection(prev, vr.index, colCount));
463
+ else
464
+ setSelection((prev) => selectRow(prev, vr.index, colCount, event.metaKey || event.ctrlKey));
465
+ focusGrid();
466
+ }, children: [key < 0 ? "+" : (vr.index + 1).toLocaleString(), state && _jsx("span", { className: "sr-only", children: ` ${t.gridRowState(state)}` })] }), virtualColumns.map((vc) => {
467
+ const column = columns[vc.index];
468
+ if (!column)
469
+ return null;
470
+ const value = pendingValue(pending, key, column.name, sourceOf(key));
471
+ const cell = { row: vr.index, col: vc.index };
472
+ const selected = isSelected(selection, cell.row, cell.col);
473
+ const active = selection.active?.row === cell.row && selection.active.col === cell.col;
474
+ const isEditing = editing?.key === key && editing.col === vc.index;
475
+ const changed = isCellChanged(pending, key, column.name);
476
+ return (_jsx("div", { id: `${gridId}-${cell.row}-${cell.col}`, role: "gridcell", "aria-colindex": vc.index + 2, "aria-selected": selected, "aria-readonly": editable ? !canEdit(key) : undefined, title: isEditing ? undefined : cellText(value), className: cn("absolute top-0 flex h-full items-center overflow-hidden border-r border-b px-2", isNumericType(column.type) || typeof value === "number"
477
+ ? "justify-end tabular-nums"
478
+ : "justify-start", state === "deleted" && "line-through", changed && "bg-warning/15", state === "added" && "bg-success/10", selected && "bg-primary/15", active && "ring-2 ring-ring ring-inset", isEditing && "p-0"), style: { left: vc.start, width: vc.size }, onMouseDown: (event) => {
479
+ if (!isEditing)
480
+ handleCellMouseDown(event, cell);
481
+ }, onDoubleClick: () => {
482
+ if (!startEdit(cell))
483
+ setCellDetail(cell);
484
+ }, onContextMenu: () => {
485
+ if (!isSelected(selection, cell.row, cell.col))
486
+ setSelection(selectCell(cell));
487
+ }, children: isEditing ? (_jsx(CellEditor, { label: t.gridEditCell(column.name), initial: editing.seed ?? cellText(value), numeric: isNumericType(column.type), onDone: finishEdit })) : (_jsx(GridCellValue, { value: value })) }, column.name));
488
+ })] }, key));
489
+ })] }), rowCount === 0 && (_jsx("p", { className: "absolute inset-x-0 py-8 text-center font-sans text-sm text-muted-foreground", style: { top: HEADER_H }, children: rows.length === 0 ? t.gridNoRows : t.gridNoMatches }))] }) }), _jsxs(ContextMenuContent, { className: "w-60", children: [_jsxs(ContextMenuItem, { onSelect: () => copyAs("tsv"), children: [_jsx(CopyIcon, { "aria-hidden": true }), t.gridCopy, _jsx(ContextMenuShortcut, { children: formatShortcut("Mod+C") })] }), _jsxs(ContextMenuItem, { onSelect: () => copyAs("tsv-headers"), children: [_jsx(CopyIcon, { "aria-hidden": true }), t.gridCopyWithHeaders, _jsx(ContextMenuShortcut, { children: formatShortcut("Mod+Shift+C") })] }), _jsx(ContextMenuLabel, { children: t.gridCopyAs }), ["csv", "json", "insert", "markdown"].map((format) => (_jsx(ContextMenuItem, { onSelect: () => copyAs(format), children: t.gridCopyFormat(format) }, format))), _jsx(ContextMenuSeparator, {}), _jsxs(ContextMenuItem, { disabled: selection.active === null, onSelect: () => selection.active && setRowDetail(selection.active.row), children: [_jsx(Maximize2Icon, { "aria-hidden": true }), t.gridRowDetail, _jsx(ContextMenuShortcut, { children: formatShortcut("Shift+Enter") })] }), _jsxs(ContextMenuItem, { disabled: selection.active === null, onSelect: () => selection.active && setCellDetail(selection.active), children: [_jsx(Maximize2Icon, { "aria-hidden": true }), t.gridCellDetail] }), editable && (_jsxs(_Fragment, { children: [_jsx(ContextMenuSeparator, {}), _jsxs(ContextMenuItem, { onSelect: () => addNewRow(), children: [_jsx(PlusIcon, { "aria-hidden": true }), t.gridAddRow] }), _jsxs(ContextMenuItem, { disabled: selection.active === null, onSelect: () => selection.active && addNewRow(rowAt(selection.active.row)), children: [_jsx(CopyIcon, { "aria-hidden": true }), t.gridDuplicateRow] }), _jsxs(ContextMenuItem, { variant: "destructive", disabled: deletable.length === 0, onSelect: () => setPending(deleteRows(pending, deletable, sourceOf)), children: [_jsx(Trash2Icon, { "aria-hidden": true }), t.gridDeleteRows(deletable.length)] }), _jsxs(ContextMenuItem, { disabled: !selection.active || !pending.rows.has(viewKeys[selection.active.row] ?? NaN), onSelect: () => {
490
+ const key = selection.active ? viewKeys[selection.active.row] : undefined;
491
+ if (key !== undefined)
492
+ setPending(revertRow(pending, key));
493
+ }, children: [_jsx(RotateCcwIcon, { "aria-hidden": true }), t.gridRevertRow] })] })), _jsx(ContextMenuSeparator, {}), _jsxs(ContextMenuItem, { onSelect: () => setSelection(EMPTY_SELECTION), children: [t.gridClearSelection, _jsx(ContextMenuShortcut, { children: formatShortcut("Escape") })] })] })] }));
494
+ }
495
+ return (_jsxs("div", { "data-slot": "data-grid", className: cn("flex min-h-0 min-w-0 flex-col bg-background text-sm", className), children: [_jsxs("div", { className: "flex min-h-9 shrink-0 flex-wrap items-center gap-2 border-b bg-muted/40 px-2 py-1 text-xs", children: [_jsxs("span", { className: "text-muted-foreground tabular-nums", children: [t.gridRows(rowCount, rows.length + (editable ? insertedKeys(pending).length : 0)), ", ", t.gridColumns(colCount)] }), _jsxs("label", { className: "flex h-7 max-w-xs min-w-32 flex-1 items-center gap-1 rounded-lg border bg-background px-2 focus-within:ring-2 focus-within:ring-ring/50", children: [_jsx(SearchIcon, { "aria-hidden": true, className: "size-3 shrink-0 text-muted-foreground" }), _jsx("span", { className: "sr-only", children: t.gridFilter }), _jsx("input", { type: "search", value: filterInput, onChange: (event) => setFilterInput(event.target.value), placeholder: t.gridFilter, className: "w-full min-w-0 bg-transparent outline-none placeholder:text-muted-foreground" }), filterInput && (_jsx("button", { type: "button", "aria-label": t.gridClearFilter, className: "text-muted-foreground hover:text-foreground", onClick: () => {
496
+ setFilterInput("");
497
+ setFilter("");
498
+ }, children: _jsx(XIcon, { "aria-hidden": true, className: "size-3" }) }))] }), hidden.length > 0 && (_jsxs(Button, { variant: "ghost", size: "xs", onClick: () => setHidden([]), children: [_jsx(EyeIcon, { "aria-hidden": true }), t.gridHidden(hidden.length)] })), _jsxs("span", { className: "ml-auto flex items-center gap-1", children: [editable && (_jsxs(_Fragment, { children: [_jsxs(Button, { variant: "ghost", size: "xs", onClick: () => addNewRow(), children: [_jsx(PlusIcon, { "aria-hidden": true }), t.gridAddRow] }), _jsxs(Button, { variant: "ghost", size: "xs", disabled: deletable.length === 0, onClick: () => setPending(deleteRows(pending, deletable, sourceOf)), children: [_jsx(Trash2Icon, { "aria-hidden": true }), t.gridDeleteRows(Math.max(deletable.length, 1))] })] })), _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", size: "xs", disabled: rowCount === 0, children: [_jsx(DownloadIcon, { "aria-hidden": true }), t.gridExport, _jsx(ChevronDownIcon, { "aria-hidden": true })] }) }), _jsxs(DropdownMenuContent, { align: "end", className: "w-56", children: [_jsx(DropdownMenuLabel, { className: "text-xs font-normal text-muted-foreground", children: t.gridExportScope(rowCount, colCount) }), _jsx(DropdownMenuSeparator, {}), EXPORT_FORMATS.map((format) => (_jsx(DropdownMenuItem, { onSelect: () => runExport(format), children: t.gridExportFormat(format) }, format))), extraFormats.length > 0 && _jsx(DropdownMenuSeparator, {}), extraFormats.map((format) => (_jsx(DropdownMenuItem, { onSelect: () => runExport(format.id), children: format.label }, format.id)))] })] }), toolbar] })] }), truncated && _jsx(TruncationBanner, { children: truncated }), editable && (_jsx(PendingChangesBar, { counts: counts, committing: committing, error: commitError?.message, errorRow: commitError?.row, note: primaryKeys.length === 0 ? t.gridNoPrimaryKey : null, onCommit: () => void commit(), onDiscard: () => {
499
+ setPending(NO_CHANGES);
500
+ setCommitError(null);
501
+ } })), body, footer && (_jsxs("section", { "aria-label": t.gridStatistics, className: "flex min-h-8 shrink-0 flex-wrap items-center gap-x-4 gap-y-1 border-t bg-muted/40 px-3 py-1 text-xs text-muted-foreground", children: [footer.kind === "column" ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "font-medium text-foreground", children: footer.name }), _jsx(Stat, { label: t.gridStat("rows"), value: footer.stats.count.toLocaleString() }), _jsx(Stat, { label: t.gridStat("nulls"), value: footer.stats.nulls.toLocaleString() }), _jsx(Stat, { label: t.gridStat("distinct"), value: footer.stats.distinct.toLocaleString() }), footer.stats.min !== undefined && (_jsx(Stat, { label: t.gridStat("min"), value: formatStatValue(footer.stats.min) })), footer.stats.max !== undefined && (_jsx(Stat, { label: t.gridStat("max"), value: formatStatValue(footer.stats.max) })), footer.stats.numeric && (_jsxs(_Fragment, { children: [_jsx(Stat, { label: t.gridStat("avg"), value: formatStatValue(footer.stats.avg) }), _jsx(Stat, { label: t.gridStat("sum"), value: formatStatValue(footer.stats.sum) })] }))] })) : (_jsxs(_Fragment, { children: [_jsx("span", { className: "text-foreground tabular-nums", children: t.gridSelected(selectedCount) }), footer.summary ? (_jsxs(_Fragment, { children: [_jsx(Stat, { label: t.gridStat("sum"), value: formatStatValue(footer.summary.sum) }), _jsx(Stat, { label: t.gridStat("avg"), value: formatStatValue(footer.summary.avg) }), _jsx(Stat, { label: t.gridStat("min"), value: formatStatValue(footer.summary.min) }), _jsx(Stat, { label: t.gridStat("max"), value: formatStatValue(footer.summary.max) })] })) : (_jsx("span", { children: t.gridNoNumeric }))] })), _jsx(Button, { variant: "ghost", size: "icon-xs", className: "ml-auto", "aria-label": t.gridClearSelection, onClick: () => setSelection(EMPTY_SELECTION), children: _jsx(XIcon, { "aria-hidden": true }) })] })), detailCell && (_jsx(CellDetailDialog, { open: true, onOpenChange: (open) => !open && setCellDetail(null), column: names[detailCell.col] ?? "", value: valueAt(detailCell.row, detailCell.col) })), detailRow !== null && (_jsx(RowDetailDrawer, { open: true, onOpenChange: (open) => !open && setRowDetail(null), row: rowAt(detailRow), columns: columns, rowNumber: detailRow + 1, rowTotal: rowCount, onNavigate: (delta) => setRowDetail((prev) => prev === null ? prev : Math.min(Math.max(prev + delta, 0), rowCount - 1)) }))] }));
502
+ }
503
+ function Stat({ label, value }) {
504
+ return (_jsxs("span", { children: [label, ": ", _jsx("span", { className: "text-foreground tabular-nums", children: value })] }));
505
+ }
506
+ /** The input a cell edits in. Enter keeps and moves down, Tab keeps and moves across, Escape cancels. */
507
+ function CellEditor({ label, initial, numeric, onDone, }) {
508
+ const [text, setText] = React.useState(initial);
509
+ const done = React.useRef(false);
510
+ const finish = (value, move) => {
511
+ if (done.current)
512
+ return;
513
+ done.current = true;
514
+ onDone(value, move);
515
+ };
516
+ return (_jsx("input", {
517
+ // The editor exists because the person asked to edit: it takes focus.
518
+ // eslint-disable-next-line jsx-a11y/no-autofocus
519
+ autoFocus: true, "aria-label": label, value: text, inputMode: numeric ? "decimal" : undefined, onFocus: (event) => {
520
+ if (text === initial && initial.length > 1)
521
+ event.currentTarget.select();
522
+ }, onChange: (event) => setText(event.target.value), onKeyDown: (event) => {
523
+ if (event.key === "Enter") {
524
+ event.preventDefault();
525
+ finish(text, { row: event.shiftKey ? -1 : 1, col: 0 });
526
+ }
527
+ else if (event.key === "Escape") {
528
+ event.preventDefault();
529
+ finish(null, null);
530
+ }
531
+ else if (event.key === "Tab") {
532
+ event.preventDefault();
533
+ finish(text, { row: 0, col: event.shiftKey ? -1 : 1 });
534
+ }
535
+ }, onBlur: () => finish(text, null), className: "size-full bg-background px-2 font-mono text-xs ring-2 ring-ring outline-none ring-inset" }));
536
+ }
537
+ /**
538
+ * One column header: what the column is, how it sorts, and what can be done
539
+ * to it. The name selects the column; sorting is its own button, so a click
540
+ * never does both.
541
+ */
542
+ function HeaderCell({ column, index, left, width, sort, selected, hiddenCount, activeValue, onSort, onSelect, onResize, onResizing, onAutoFit, onHide, onShowAll, onFilterByValue, onCopyName, }) {
543
+ const { messages } = useUI();
544
+ const t = messages.workbench;
545
+ const typeLabel = column.dataType ?? t.gridType(column.type);
546
+ function startDrag(event) {
547
+ if (event.button !== 0)
548
+ return;
549
+ event.preventDefault();
550
+ event.stopPropagation();
551
+ const handle = event.currentTarget;
552
+ handle.setPointerCapture(event.pointerId);
553
+ const startX = event.clientX;
554
+ let latest = width;
555
+ const move = (e) => {
556
+ latest = clampWidth(width + e.clientX - startX);
557
+ onResizing(latest);
558
+ };
559
+ const up = () => {
560
+ handle.removeEventListener("pointermove", move);
561
+ handle.removeEventListener("pointerup", up);
562
+ handle.removeEventListener("pointercancel", up);
563
+ onResize(latest);
564
+ };
565
+ handle.addEventListener("pointermove", move);
566
+ handle.addEventListener("pointerup", up);
567
+ handle.addEventListener("pointercancel", up);
568
+ }
569
+ return (_jsxs("div", { role: "columnheader", "aria-colindex": index + 2, "aria-sort": sort === "asc" ? "ascending" : sort === "desc" ? "descending" : undefined, "aria-selected": selected, className: cn("group absolute top-0 flex h-full items-center gap-1 overflow-hidden border-r border-b pr-2 pl-2 font-sans font-medium", selected ? "bg-primary/15 text-foreground" : "text-muted-foreground"), style: { left, width }, children: [_jsx(ColumnTypeIcon, { type: column.type }), _jsx("button", { type: "button", className: "min-w-0 flex-1 truncate rounded-sm text-left outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50", title: `${column.name} (${typeLabel}${column.nullable === false ? `, ${t.gridNotNull}` : ""})${column.references ? ` → ${column.references}` : ""}`, onClick: (event) => onSelect(event.metaKey || event.ctrlKey), onDoubleClick: onAutoFit, children: column.name }), column.nullable === true && (_jsx("span", { className: "shrink-0 text-muted-foreground", title: t.gridNullable, "aria-hidden": true, children: "?" })), _jsx(ColumnKeyIcons, { column: column }), _jsx("button", { type: "button", "aria-label": t.gridSortBy(column.name), className: cn("shrink-0 rounded-sm outline-none hover:text-foreground focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-ring/50", sort ? "text-foreground" : "opacity-40 group-hover:opacity-100"), onClick: () => onSort(sort === "asc" ? "desc" : sort === "desc" ? null : "asc"), children: sort === "asc" ? (_jsx(ArrowUpIcon, { "aria-hidden": true, className: "size-3" })) : sort === "desc" ? (_jsx(ArrowDownIcon, { "aria-hidden": true, className: "size-3" })) : (_jsx(ChevronsUpDownIcon, { "aria-hidden": true, className: "size-3" })) }), _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { "aria-label": t.gridColumnMenu(column.name), className: "shrink-0 rounded-sm opacity-40 outline-none group-hover:opacity-100 hover:text-foreground focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-ring/50 aria-expanded:opacity-100", children: _jsx(EllipsisVerticalIcon, { "aria-hidden": true, className: "size-3" }) }), _jsxs(DropdownMenuContent, { className: "w-56", children: [_jsxs(DropdownMenuLabel, { className: "flex items-center gap-1.5 text-xs", children: [_jsx("span", { className: "truncate font-mono", children: column.name }), _jsx("span", { className: "ml-auto font-normal text-muted-foreground", children: typeLabel })] }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onSelect: () => onSort("asc"), children: [_jsx(ArrowUpIcon, { "aria-hidden": true }), t.gridSortAscending] }), _jsxs(DropdownMenuItem, { onSelect: () => onSort("desc"), children: [_jsx(ArrowDownIcon, { "aria-hidden": true }), t.gridSortDescending] }), sort && (_jsxs(DropdownMenuItem, { onSelect: () => onSort(null), children: [_jsx(XIcon, { "aria-hidden": true }), t.gridClearSort] })), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { disabled: activeValue === undefined, onSelect: () => activeValue !== undefined && onFilterByValue(activeValue), children: [_jsx(FunnelIcon, { "aria-hidden": true }), _jsx("span", { className: "truncate", children: t.gridFilterByValue(activeValue ?? null) })] }), _jsxs(DropdownMenuItem, { onSelect: onHide, children: [_jsx(EyeOffIcon, { "aria-hidden": true }), t.gridHideColumn] }), hiddenCount > 0 && (_jsxs(DropdownMenuItem, { onSelect: onShowAll, children: [_jsx(EyeIcon, { "aria-hidden": true }), t.gridShowAllColumns(hiddenCount)] })), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onSelect: onCopyName, children: [_jsx(CopyIcon, { "aria-hidden": true }), t.gridCopyColumnName] })] })] }), _jsx("div", { role: "separator", tabIndex: 0, "aria-orientation": "vertical", "aria-label": t.gridResizeColumn(column.name), "aria-valuenow": width, "aria-valuemin": MIN_COL_W, "aria-valuemax": MAX_COL_W, onPointerDown: startDrag, onDoubleClick: onAutoFit, onKeyDown: (event) => {
570
+ const step = event.shiftKey ? RESIZE_STEP * 4 : RESIZE_STEP;
571
+ const next = event.key === "ArrowRight"
572
+ ? width + step
573
+ : event.key === "ArrowLeft"
574
+ ? width - step
575
+ : event.key === "Home"
576
+ ? MIN_COL_W
577
+ : event.key === "End"
578
+ ? MAX_COL_W
579
+ : event.key === "Enter"
580
+ ? null
581
+ : undefined;
582
+ if (next === undefined)
583
+ return;
584
+ event.preventDefault();
585
+ if (next === null)
586
+ onAutoFit();
587
+ else
588
+ onResize(next);
589
+ }, className: "absolute top-0 right-0 h-full w-1.5 cursor-col-resize touch-none outline-none select-none hover:bg-primary/50 focus-visible:bg-ring" })] }));
590
+ }
@@ -0,0 +1,17 @@
1
+ import * as React from "react";
2
+ import { type GridColumn, type GridRow } from "./grid-model.js";
3
+ export interface DataProfileProps {
4
+ columns: readonly (string | GridColumn)[];
5
+ rows: readonly GridRow[];
6
+ /** A note that the rows themselves are capped (the grid's truncation sentence). */
7
+ truncated?: string | null;
8
+ /** Rows profiled at most; the rest are left out and the banner says so. */
9
+ rowCap?: number;
10
+ className?: string;
11
+ }
12
+ /**
13
+ * A card per column: completeness, distinct values, range, mean and median
14
+ * for numbers, and the most frequent values. Honest about its sample: a
15
+ * profile of the fetched rows is not a profile of the table.
16
+ */
17
+ export declare function DataProfile(props: DataProfileProps): React.JSX.Element;