@calliopelabs/ui 0.16.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +94 -1
  2. package/dist/index.d.ts +3 -0
  3. package/dist/index.js +2 -0
  4. package/dist/primitives/Menubar.d.ts +21 -0
  5. package/dist/primitives/Menubar.js +43 -0
  6. package/dist/provider/messages.d.ts +148 -0
  7. package/dist/provider/messages.js +178 -0
  8. package/dist/shell/RailPanel.js +1 -1
  9. package/dist/workbench/ActivityBar.d.ts +37 -0
  10. package/dist/workbench/ActivityBar.js +57 -0
  11. package/dist/workbench/BottomPanel.d.ts +30 -0
  12. package/dist/workbench/BottomPanel.js +20 -0
  13. package/dist/workbench/CodeEditor.d.ts +48 -0
  14. package/dist/workbench/CodeEditor.js +153 -0
  15. package/dist/workbench/CommandPalette.d.ts +49 -0
  16. package/dist/workbench/CommandPalette.js +114 -0
  17. package/dist/workbench/DataChart.d.ts +45 -0
  18. package/dist/workbench/DataChart.js +146 -0
  19. package/dist/workbench/DataGrid.d.ts +89 -0
  20. package/dist/workbench/DataGrid.js +590 -0
  21. package/dist/workbench/DataProfile.d.ts +17 -0
  22. package/dist/workbench/DataProfile.js +51 -0
  23. package/dist/workbench/EditorTabs.d.ts +62 -0
  24. package/dist/workbench/EditorTabs.js +115 -0
  25. package/dist/workbench/GridCell.d.ts +19 -0
  26. package/dist/workbench/GridCell.js +50 -0
  27. package/dist/workbench/GridDetails.d.ts +31 -0
  28. package/dist/workbench/GridDetails.js +78 -0
  29. package/dist/workbench/MenuBar.d.ts +49 -0
  30. package/dist/workbench/MenuBar.js +25 -0
  31. package/dist/workbench/PaneGroup.d.ts +50 -0
  32. package/dist/workbench/PaneGroup.js +146 -0
  33. package/dist/workbench/PendingChangesBar.d.ts +20 -0
  34. package/dist/workbench/PendingChangesBar.js +19 -0
  35. package/dist/workbench/Shortcuts.d.ts +50 -0
  36. package/dist/workbench/Shortcuts.js +109 -0
  37. package/dist/workbench/SqlEditor.d.ts +21 -0
  38. package/dist/workbench/SqlEditor.js +79 -0
  39. package/dist/workbench/StatusBar.d.ts +28 -0
  40. package/dist/workbench/StatusBar.js +26 -0
  41. package/dist/workbench/TruncationBanner.d.ts +11 -0
  42. package/dist/workbench/TruncationBanner.js +12 -0
  43. package/dist/workbench/WorkbenchShell.d.ts +36 -0
  44. package/dist/workbench/WorkbenchShell.js +20 -0
  45. package/dist/workbench/download.d.ts +11 -0
  46. package/dist/workbench/download.js +28 -0
  47. package/dist/workbench/editor-theme.d.ts +42 -0
  48. package/dist/workbench/editor-theme.js +113 -0
  49. package/dist/workbench/grid-format.d.ts +59 -0
  50. package/dist/workbench/grid-format.js +169 -0
  51. package/dist/workbench/grid-model.d.ts +75 -0
  52. package/dist/workbench/grid-model.js +259 -0
  53. package/dist/workbench/grid-selection.d.ts +69 -0
  54. package/dist/workbench/grid-selection.js +183 -0
  55. package/dist/workbench/grid-stats.d.ts +58 -0
  56. package/dist/workbench/grid-stats.js +185 -0
  57. package/dist/workbench/index.d.ts +28 -0
  58. package/dist/workbench/index.js +31 -0
  59. package/dist/workbench/keys.d.ts +23 -0
  60. package/dist/workbench/keys.js +105 -0
  61. package/dist/workbench/palette.d.ts +17 -0
  62. package/dist/workbench/palette.js +45 -0
  63. package/dist/workbench/pending.d.ts +92 -0
  64. package/dist/workbench/pending.js +207 -0
  65. package/dist/workbench/sql-completion.d.ts +107 -0
  66. package/dist/workbench/sql-completion.js +475 -0
  67. package/dist/workflow/WorkflowRuntimeControls.d.ts +21 -0
  68. package/dist/workflow/WorkflowRuntimeControls.js +54 -0
  69. package/dist/workflow/runtime-controls.d.ts +29 -0
  70. package/dist/workflow/runtime-controls.js +10 -0
  71. package/package.json +13 -2
@@ -0,0 +1,259 @@
1
+ /**
2
+ * The data grid's model, as pure data: what a column is, how a value reads as
3
+ * text, how rows sort (NULLs last) and filter. No React, no DOM, so a Server
4
+ * Component or a worker can import it. Extracted from Loadr's grid (shared
5
+ * with Chat Studio).
6
+ */
7
+ export const COLUMN_TYPES = [
8
+ "integer",
9
+ "number",
10
+ "text",
11
+ "boolean",
12
+ "datetime",
13
+ "date",
14
+ "time",
15
+ "json",
16
+ "binary",
17
+ "uuid",
18
+ "other",
19
+ ];
20
+ const COLUMN_TYPE_SET = new Set(COLUMN_TYPES);
21
+ /** Types whose values are right-aligned and get numeric stats. */
22
+ export function isNumericType(type) {
23
+ return type === "integer" || type === "number";
24
+ }
25
+ // Driver spellings, checked as substrings once parameters and wrappers are
26
+ // stripped. Order matters: "interval" contains "int", "datetime" contains
27
+ // "date" and "time", almost every string type mentions "char" or "text".
28
+ const SPELLINGS = [
29
+ ["interval", "other"],
30
+ ["timedelta", "other"],
31
+ ["binary_double", "number"],
32
+ ["binary_float", "number"],
33
+ ["uuid", "uuid"],
34
+ ["uniqueidentifier", "uuid"],
35
+ ["guid", "uuid"],
36
+ ["bytea", "binary"],
37
+ ["varbinary", "binary"],
38
+ ["binary", "binary"],
39
+ ["blob", "binary"],
40
+ ["bytes", "binary"],
41
+ ["image", "binary"],
42
+ ["raw", "binary"],
43
+ ["jsonb", "json"],
44
+ ["json", "json"],
45
+ ["variant", "json"],
46
+ ["struct", "json"],
47
+ ["array", "json"],
48
+ ["object", "json"],
49
+ ["map", "json"],
50
+ ["xml", "json"],
51
+ ["timestamptz", "datetime"],
52
+ ["timestamp", "datetime"],
53
+ ["datetime", "datetime"],
54
+ ["date", "date"],
55
+ ["timetz", "time"],
56
+ ["time", "time"],
57
+ ["boolean", "boolean"],
58
+ ["bool", "boolean"],
59
+ ["bit", "boolean"],
60
+ ["decimal", "number"],
61
+ ["numeric", "number"],
62
+ ["float", "number"],
63
+ ["double", "number"],
64
+ ["real", "number"],
65
+ ["money", "number"],
66
+ ["number", "number"],
67
+ ["integer", "integer"],
68
+ ["bigint", "integer"],
69
+ ["smallint", "integer"],
70
+ ["tinyint", "integer"],
71
+ ["mediumint", "integer"],
72
+ ["serial", "integer"],
73
+ ["int", "integer"],
74
+ ["long", "integer"],
75
+ ["short", "integer"],
76
+ ["byte", "integer"],
77
+ ["varchar", "text"],
78
+ ["char", "text"],
79
+ ["text", "text"],
80
+ ["clob", "text"],
81
+ ["string", "text"],
82
+ ["enum", "text"],
83
+ ["name", "text"],
84
+ ];
85
+ // ClickHouse wraps the real type.
86
+ const WRAPPERS = /^(nullable|lowcardinality)\((.*)\)$/;
87
+ /** Any spelling of a column type onto the vocabulary; unknown spellings are "other". */
88
+ export function normalizeColumnType(raw) {
89
+ if (!raw)
90
+ return "other";
91
+ let name = raw.trim().toLowerCase();
92
+ if (COLUMN_TYPE_SET.has(name))
93
+ return name;
94
+ for (let i = 0; i < 3; i++) {
95
+ const wrapped = WRAPPERS.exec(name);
96
+ if (!wrapped)
97
+ break;
98
+ name = (wrapped[2] ?? "").trim();
99
+ }
100
+ name = name
101
+ .replace(/\[[^\]]*\]/g, "")
102
+ .replace(/\([^)]*\)/g, "")
103
+ .replace(/\s+(with|without)\s+time\s+zone/, "")
104
+ .replace(/\s+unsigned|\s+signed|\s+zerofill/g, "")
105
+ .trim();
106
+ if (COLUMN_TYPE_SET.has(name))
107
+ return name;
108
+ for (const [spelling, type] of SPELLINGS) {
109
+ if (name.includes(spelling))
110
+ return type;
111
+ }
112
+ return "other";
113
+ }
114
+ const UUID_TEXT = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
115
+ const DATETIME_TEXT = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}/;
116
+ const DATE_TEXT = /^\d{4}-\d{2}-\d{2}$/;
117
+ const TIME_TEXT = /^\d{2}:\d{2}(:\d{2})?/;
118
+ const SNIFF_SAMPLE = 20;
119
+ /** A guess at a column's type from its values, for a result that declares none. */
120
+ export function sniffColumnType(values) {
121
+ let seen = null;
122
+ let checked = 0;
123
+ for (const value of values) {
124
+ if (value === null || value === undefined)
125
+ continue;
126
+ if (checked >= SNIFF_SAMPLE)
127
+ break;
128
+ checked++;
129
+ let kind;
130
+ if (typeof value === "boolean")
131
+ kind = "boolean";
132
+ else if (typeof value === "number")
133
+ kind = Number.isInteger(value) ? "integer" : "number";
134
+ else if (typeof value === "object")
135
+ kind = asBinary(value) ? "binary" : "json";
136
+ else if (typeof value === "string") {
137
+ if (UUID_TEXT.test(value))
138
+ kind = "uuid";
139
+ else if (DATETIME_TEXT.test(value))
140
+ kind = "datetime";
141
+ else if (DATE_TEXT.test(value))
142
+ kind = "date";
143
+ else if (TIME_TEXT.test(value))
144
+ kind = "time";
145
+ else
146
+ kind = "text";
147
+ }
148
+ else
149
+ kind = "other";
150
+ if (seen === null)
151
+ seen = kind;
152
+ else if (seen !== kind) {
153
+ if (isNumericType(seen) && isNumericType(kind))
154
+ seen = "number";
155
+ else
156
+ return "other";
157
+ }
158
+ }
159
+ return seen ?? "other";
160
+ }
161
+ /** Columns as the host gave them (names or descriptions), resolved; undeclared types are sniffed. */
162
+ export function buildColumnMeta(columns, rows = []) {
163
+ return columns.map((column) => {
164
+ const c = typeof column === "string" ? { name: column } : column;
165
+ const type = c.type
166
+ ? normalizeColumnType(c.type)
167
+ : sniffColumnType(rows.slice(0, SNIFF_SAMPLE * 4).map((row) => row[c.name]));
168
+ return {
169
+ name: c.name,
170
+ type,
171
+ dataType: c.dataType,
172
+ nullable: c.nullable,
173
+ primaryKey: c.primaryKey === true,
174
+ foreignKey: c.foreignKey === true,
175
+ references: c.references,
176
+ };
177
+ });
178
+ }
179
+ /** A fingerprint of a result's shape: a grid keyed on it starts clean for different columns. */
180
+ export function columnSignature(columns) {
181
+ return columns
182
+ .map((c) => (typeof c === "string" ? `${c}:?` : `${c.name}:${c.type ?? "?"}`))
183
+ .join("\u0000");
184
+ }
185
+ export function asBinary(value) {
186
+ if (value === null || typeof value !== "object")
187
+ return null;
188
+ const marker = value;
189
+ if (marker.__binary__ !== true)
190
+ return null;
191
+ return {
192
+ __binary__: true,
193
+ base64: typeof marker.base64 === "string" ? marker.base64 : "",
194
+ size: typeof marker.size === "number" ? marker.size : 0,
195
+ truncated: marker.truncated === true,
196
+ };
197
+ }
198
+ /**
199
+ * How a value reads in a text format. NULL is an empty field, not the word,
200
+ * so a pasted column lands in a spreadsheet empty.
201
+ */
202
+ export function cellText(value) {
203
+ if (value === null || value === undefined)
204
+ return "";
205
+ const binary = asBinary(value);
206
+ if (binary)
207
+ return `<binary ${binary.size} bytes>`;
208
+ if (typeof value === "object")
209
+ return JSON.stringify(value);
210
+ return String(value);
211
+ }
212
+ /** Ascending order of two cells; NULL is neither, the caller puts it last. */
213
+ function compareNonNull(a, b) {
214
+ if (typeof a === "number" && typeof b === "number")
215
+ return a - b;
216
+ if (typeof a === "boolean" && typeof b === "boolean")
217
+ return Number(a) - Number(b);
218
+ const as = typeof a === "object" ? JSON.stringify(a) : String(a);
219
+ const bs = typeof b === "object" ? JSON.stringify(b) : String(b);
220
+ return as < bs ? -1 : as > bs ? 1 : 0;
221
+ }
222
+ /** Compares two cells in a direction; NULLs sort last in both. */
223
+ export function compareCells(a, b, direction = "asc") {
224
+ const aNull = a === null || a === undefined;
225
+ const bNull = b === null || b === undefined;
226
+ if (aNull || bNull)
227
+ return aNull && bNull ? 0 : aNull ? 1 : -1;
228
+ return direction === "asc" ? compareNonNull(a, b) : -compareNonNull(a, b);
229
+ }
230
+ /**
231
+ * The source indexes of the rows in view, in view order: those with a cell
232
+ * in `columns` containing `filter` (case-insensitive), sorted by `sort`
233
+ * (stable, NULLs last).
234
+ */
235
+ export function viewRowIndexes(rows, columns, filter, sort) {
236
+ const needle = filter.trim().toLowerCase();
237
+ const indexes = [];
238
+ for (let i = 0; i < rows.length; i++) {
239
+ if (needle === "") {
240
+ indexes.push(i);
241
+ continue;
242
+ }
243
+ const row = rows[i];
244
+ for (const column of columns) {
245
+ const value = row?.[column];
246
+ if (value === null || value === undefined)
247
+ continue;
248
+ if (cellText(value).toLowerCase().includes(needle)) {
249
+ indexes.push(i);
250
+ break;
251
+ }
252
+ }
253
+ }
254
+ if (sort) {
255
+ const { column, direction } = sort;
256
+ indexes.sort((a, b) => compareCells(rows[a]?.[column], rows[b]?.[column], direction) || a - b);
257
+ }
258
+ return indexes;
259
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * The grid's selection model, as pure data (from Loadr's grid).
3
+ *
4
+ * Coordinates are *view* coordinates: `row` indexes the visible (filtered and
5
+ * sorted) row order, `col` indexes the visible columns. The grid maps them back
6
+ * to source rows when it copies or edits. Keeping the model out of the
7
+ * component is what lets 50k-row selection behaviour be tested without a DOM.
8
+ */
9
+ export interface CellRef {
10
+ row: number;
11
+ col: number;
12
+ }
13
+ /** A rectangle, anchored where the drag or shift-click started. */
14
+ export interface SelectionRange {
15
+ anchor: CellRef;
16
+ focus: CellRef;
17
+ }
18
+ export interface SelectionState {
19
+ /** Additive ranges, in the order the user made them. */
20
+ ranges: SelectionRange[];
21
+ /** The cell the keyboard acts on. Null when nothing is selected. */
22
+ active: CellRef | null;
23
+ }
24
+ export declare const EMPTY_SELECTION: SelectionState;
25
+ export interface Bounds {
26
+ minRow: number;
27
+ maxRow: number;
28
+ minCol: number;
29
+ maxCol: number;
30
+ }
31
+ /** Is this cell inside any range? */
32
+ export declare function isSelected(state: SelectionState, row: number, col: number): boolean;
33
+ /** Total cells covered, counting an overlap once. */
34
+ export declare function selectionSize(state: SelectionState): number;
35
+ /** The bounding box of everything selected, or null when nothing is. */
36
+ export declare function selectionBounds(state: SelectionState): Bounds | null;
37
+ /** Plain click: one cell, everything else dropped. */
38
+ export declare function selectCell(cell: CellRef): SelectionState;
39
+ /**
40
+ * Shift-click or shift-arrow: stretch the newest range from its anchor.
41
+ *
42
+ * With nothing selected yet this behaves like a plain click, which is what
43
+ * every spreadsheet does when you shift-click into an empty grid.
44
+ */
45
+ export declare function extendSelection(state: SelectionState, cell: CellRef): SelectionState;
46
+ /**
47
+ * Cmd/Ctrl-click: add the cell as its own range, or remove it if it was
48
+ * already the whole of one. Clicking inside a bigger range leaves that range
49
+ * alone and just moves the active cell, which is how Excel behaves.
50
+ */
51
+ export declare function toggleCell(state: SelectionState, cell: CellRef): SelectionState;
52
+ /** Column header click. Additive keeps the existing ranges (cmd-click). */
53
+ export declare function selectColumn(state: SelectionState, col: number, rowCount: number, additive?: boolean): SelectionState;
54
+ /** Row-number click. */
55
+ export declare function selectRow(state: SelectionState, row: number, colCount: number, additive?: boolean): SelectionState;
56
+ /** Shift-click on a row number: every row between the anchor row and this one. */
57
+ export declare function extendRowSelection(state: SelectionState, row: number, colCount: number): SelectionState;
58
+ export declare function selectAll(rowCount: number, colCount: number): SelectionState;
59
+ /** True when every row of `col` is inside the selection. */
60
+ export declare function isColumnSelected(state: SelectionState, col: number, rowCount: number): boolean;
61
+ /** True when every column of `row` is inside the selection. */
62
+ export declare function isRowSelected(state: SelectionState, row: number, colCount: number): boolean;
63
+ /**
64
+ * Move the active cell, clamped to the grid.
65
+ *
66
+ * `extend` stretches the current range instead of starting a new one, which is
67
+ * what shift-arrow does.
68
+ */
69
+ export declare function moveActive(state: SelectionState, to: CellRef, rowCount: number, colCount: number, extend: boolean): SelectionState;
@@ -0,0 +1,183 @@
1
+ /**
2
+ * The grid's selection model, as pure data (from Loadr's grid).
3
+ *
4
+ * Coordinates are *view* coordinates: `row` indexes the visible (filtered and
5
+ * sorted) row order, `col` indexes the visible columns. The grid maps them back
6
+ * to source rows when it copies or edits. Keeping the model out of the
7
+ * component is what lets 50k-row selection behaviour be tested without a DOM.
8
+ */
9
+ export const EMPTY_SELECTION = { ranges: [], active: null };
10
+ function rangeBounds(range) {
11
+ return {
12
+ minRow: Math.min(range.anchor.row, range.focus.row),
13
+ maxRow: Math.max(range.anchor.row, range.focus.row),
14
+ minCol: Math.min(range.anchor.col, range.focus.col),
15
+ maxCol: Math.max(range.anchor.col, range.focus.col),
16
+ };
17
+ }
18
+ function contains(range, row, col) {
19
+ const b = rangeBounds(range);
20
+ return row >= b.minRow && row <= b.maxRow && col >= b.minCol && col <= b.maxCol;
21
+ }
22
+ /** Is this cell inside any range? */
23
+ export function isSelected(state, row, col) {
24
+ for (const range of state.ranges) {
25
+ if (contains(range, row, col))
26
+ return true;
27
+ }
28
+ return false;
29
+ }
30
+ /** Total cells covered, counting an overlap once. */
31
+ export function selectionSize(state) {
32
+ if (state.ranges.length === 0)
33
+ return 0;
34
+ const [only] = state.ranges;
35
+ if (state.ranges.length === 1 && only) {
36
+ const b = rangeBounds(only);
37
+ return (b.maxRow - b.minRow + 1) * (b.maxCol - b.minCol + 1);
38
+ }
39
+ const seen = new Set();
40
+ for (const range of state.ranges) {
41
+ const b = rangeBounds(range);
42
+ for (let r = b.minRow; r <= b.maxRow; r++) {
43
+ for (let c = b.minCol; c <= b.maxCol; c++)
44
+ seen.add(`${r}:${c}`);
45
+ }
46
+ }
47
+ return seen.size;
48
+ }
49
+ /** The bounding box of everything selected, or null when nothing is. */
50
+ export function selectionBounds(state) {
51
+ if (state.ranges.length === 0)
52
+ return null;
53
+ let out = null;
54
+ for (const range of state.ranges) {
55
+ const b = rangeBounds(range);
56
+ out = out
57
+ ? {
58
+ minRow: Math.min(out.minRow, b.minRow),
59
+ maxRow: Math.max(out.maxRow, b.maxRow),
60
+ minCol: Math.min(out.minCol, b.minCol),
61
+ maxCol: Math.max(out.maxCol, b.maxCol),
62
+ }
63
+ : b;
64
+ }
65
+ return out;
66
+ }
67
+ /** Plain click: one cell, everything else dropped. */
68
+ export function selectCell(cell) {
69
+ return { ranges: [{ anchor: cell, focus: cell }], active: cell };
70
+ }
71
+ /**
72
+ * Shift-click or shift-arrow: stretch the newest range from its anchor.
73
+ *
74
+ * With nothing selected yet this behaves like a plain click, which is what
75
+ * every spreadsheet does when you shift-click into an empty grid.
76
+ */
77
+ export function extendSelection(state, cell) {
78
+ if (state.ranges.length === 0)
79
+ return selectCell(cell);
80
+ const ranges = state.ranges.slice();
81
+ const last = ranges[ranges.length - 1];
82
+ if (!last)
83
+ return selectCell(cell);
84
+ ranges[ranges.length - 1] = { anchor: last.anchor, focus: cell };
85
+ return { ranges, active: cell };
86
+ }
87
+ /**
88
+ * Cmd/Ctrl-click: add the cell as its own range, or remove it if it was
89
+ * already the whole of one. Clicking inside a bigger range leaves that range
90
+ * alone and just moves the active cell, which is how Excel behaves.
91
+ */
92
+ export function toggleCell(state, cell) {
93
+ const exact = state.ranges.findIndex((r) => r.anchor.row === cell.row &&
94
+ r.anchor.col === cell.col &&
95
+ r.focus.row === cell.row &&
96
+ r.focus.col === cell.col);
97
+ if (exact >= 0) {
98
+ const ranges = state.ranges.filter((_, i) => i !== exact);
99
+ return { ranges, active: ranges[ranges.length - 1]?.focus ?? null };
100
+ }
101
+ return { ranges: [...state.ranges, { anchor: cell, focus: cell }], active: cell };
102
+ }
103
+ /** Column header click. Additive keeps the existing ranges (cmd-click). */
104
+ export function selectColumn(state, col, rowCount, additive = false) {
105
+ if (rowCount === 0)
106
+ return additive ? state : EMPTY_SELECTION;
107
+ const range = {
108
+ anchor: { row: 0, col },
109
+ focus: { row: rowCount - 1, col },
110
+ };
111
+ return {
112
+ ranges: additive ? [...state.ranges, range] : [range],
113
+ active: { row: 0, col },
114
+ };
115
+ }
116
+ /** Row-number click. */
117
+ export function selectRow(state, row, colCount, additive = false) {
118
+ if (colCount === 0)
119
+ return additive ? state : EMPTY_SELECTION;
120
+ const range = {
121
+ anchor: { row, col: 0 },
122
+ focus: { row, col: colCount - 1 },
123
+ };
124
+ return {
125
+ ranges: additive ? [...state.ranges, range] : [range],
126
+ active: { row, col: 0 },
127
+ };
128
+ }
129
+ /** Shift-click on a row number: every row between the anchor row and this one. */
130
+ export function extendRowSelection(state, row, colCount) {
131
+ if (state.ranges.length === 0)
132
+ return selectRow(state, row, colCount);
133
+ const ranges = state.ranges.slice();
134
+ const last = ranges[ranges.length - 1];
135
+ if (!last)
136
+ return selectRow(state, row, colCount);
137
+ ranges[ranges.length - 1] = {
138
+ anchor: { row: last.anchor.row, col: 0 },
139
+ focus: { row, col: Math.max(colCount - 1, 0) },
140
+ };
141
+ return { ranges, active: { row, col: 0 } };
142
+ }
143
+ export function selectAll(rowCount, colCount) {
144
+ if (rowCount === 0 || colCount === 0)
145
+ return EMPTY_SELECTION;
146
+ return {
147
+ ranges: [{ anchor: { row: 0, col: 0 }, focus: { row: rowCount - 1, col: colCount - 1 } }],
148
+ active: { row: 0, col: 0 },
149
+ };
150
+ }
151
+ /** True when every row of `col` is inside the selection. */
152
+ export function isColumnSelected(state, col, rowCount) {
153
+ if (rowCount === 0)
154
+ return false;
155
+ return state.ranges.some((r) => {
156
+ const b = rangeBounds(r);
157
+ return b.minCol <= col && b.maxCol >= col && b.minRow === 0 && b.maxRow >= rowCount - 1;
158
+ });
159
+ }
160
+ /** True when every column of `row` is inside the selection. */
161
+ export function isRowSelected(state, row, colCount) {
162
+ if (colCount === 0)
163
+ return false;
164
+ return state.ranges.some((r) => {
165
+ const b = rangeBounds(r);
166
+ return b.minRow <= row && b.maxRow >= row && b.minCol === 0 && b.maxCol >= colCount - 1;
167
+ });
168
+ }
169
+ /**
170
+ * Move the active cell, clamped to the grid.
171
+ *
172
+ * `extend` stretches the current range instead of starting a new one, which is
173
+ * what shift-arrow does.
174
+ */
175
+ export function moveActive(state, to, rowCount, colCount, extend) {
176
+ if (rowCount === 0 || colCount === 0)
177
+ return EMPTY_SELECTION;
178
+ const cell = {
179
+ row: Math.min(Math.max(to.row, 0), rowCount - 1),
180
+ col: Math.min(Math.max(to.col, 0), colCount - 1),
181
+ };
182
+ return extend ? extendSelection(state, cell) : selectCell(cell);
183
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Column and selection statistics, and the column profile (from Loadr's grid
3
+ * and profile view). Pure.
4
+ *
5
+ * One pass, no sorting of the whole column: at 50k rows the grid computes these
6
+ * on a header menu opening or a selection change, so the cost has to stay linear
7
+ * and allocation-light.
8
+ */
9
+ export interface ColumnStats {
10
+ /** Values looked at, including NULLs. */
11
+ count: number;
12
+ nulls: number;
13
+ distinct: number;
14
+ min?: number | string;
15
+ max?: number | string;
16
+ sum?: number;
17
+ avg?: number;
18
+ /** True when every non-NULL value was a number. */
19
+ numeric: boolean;
20
+ }
21
+ export declare function computeStats(values: readonly unknown[]): ColumnStats;
22
+ export interface NumericSummary {
23
+ count: number;
24
+ sum: number;
25
+ avg: number;
26
+ min: number;
27
+ max: number;
28
+ }
29
+ /**
30
+ * Sum/avg over whatever in the selection parses as a number.
31
+ *
32
+ * Unlike `computeStats` this does coerce numeric strings, because a selection
33
+ * spans columns and a numeric column arriving as text should still add up.
34
+ */
35
+ export declare function numericSummary(values: readonly unknown[]): NumericSummary | null;
36
+ export declare function formatStatValue(value: number | string | undefined): string;
37
+ /** What the profile infers a column holds. */
38
+ export type ProfileKind = "numeric" | "text" | "boolean" | "date" | "null";
39
+ /** One column of a profile. */
40
+ export interface ColumnProfile {
41
+ name: string;
42
+ kind: ProfileKind;
43
+ count: number;
44
+ nulls: number;
45
+ distinct: number;
46
+ min?: number | string;
47
+ max?: number | string;
48
+ mean?: number;
49
+ median?: number;
50
+ /** The most frequent values, most frequent first, as a share of the non-NULL values. */
51
+ top: {
52
+ value: string;
53
+ count: number;
54
+ share: number;
55
+ }[];
56
+ }
57
+ /** A column's profile: counts, range, mean and median (numeric), and the top values. */
58
+ export declare function profileColumn(name: string, values: readonly unknown[], topCount?: number): ColumnProfile;