@bendyline/squisq-grid-react 2.11.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bendyline LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/NOTICE.md ADDED
@@ -0,0 +1,16 @@
1
+ # Third-Party Notices for @bendyline/squisq-grid-react
2
+
3
+ This notice applies to the `@bendyline/squisq-grid-react` npm package.
4
+ Squisq-authored code is licensed under the MIT license in `LICENSE`.
5
+ Third-party components remain under their respective license terms.
6
+
7
+ ## Runtime and peer dependencies
8
+
9
+ | Package | Version | License | Repository |
10
+ | ----------------------- | -------------------- | ------- | ----------------------------------- |
11
+ | @tanstack/react-virtual | 3.14.10 | MIT | https://github.com/TanStack/virtual |
12
+ | react _(peer)_ | ^18.0.0 \|\| ^19.0.0 | MIT | https://github.com/facebook/react |
13
+ | react-dom _(peer)_ | ^18.0.0 \|\| ^19.0.0 | MIT | https://github.com/facebook/react |
14
+
15
+ Copyright and complete license texts for these dependencies are included in
16
+ their respective npm distributions and source repositories.
package/README.md ADDED
@@ -0,0 +1,23 @@
1
+ # @bendyline/squisq-grid-react
2
+
3
+ Virtualized data grid for squisq data sidecars: an in-house columnar store
4
+ behind a Web Worker (`TableStoreClient`, implementing core's
5
+ `TableQueryProvider`), plus a TanStack-Virtual React renderer (`DataGrid`)
6
+ with spreadsheet-style sort/filter/selection/clipboard and journaled cell
7
+ editing.
8
+
9
+ The Tiptap mount lives in `@bendyline/squisq-editor-react` (the data-card
10
+ widget lazy-imports this package); the grid itself has no editor
11
+ dependencies and works in any React 18/19 host.
12
+
13
+ ```tsx
14
+ import { DataGrid, TableStoreClient, EditJournal } from '@bendyline/squisq-grid-react';
15
+ import '@bendyline/squisq-grid-react/styles';
16
+
17
+ const provider = new TableStoreClient({ headers, cells });
18
+ <DataGrid provider={provider} journal={new EditJournal()} view={{ sort: [], filter: [] }} />;
19
+ ```
20
+
21
+ Theming: every color resolves through `--squisq-grid-*` CSS custom
22
+ properties with literal fallbacks; hosts re-bind them to their own palette
23
+ (editor-react aliases them onto its chrome tokens).
@@ -0,0 +1,8 @@
1
+ THIRD-PARTY LICENSES FOR @bendyline/squisq-grid-react
2
+
3
+ Generated from the actual esbuild input graph. Package-local license, copying,
4
+ and notice files are reproduced verbatim. When an npm tarball omits its
5
+ repository license, the pinned upstream copy is vendored and identified below.
6
+
7
+ COMPONENTS
8
+
@@ -0,0 +1,376 @@
1
+ import { TableCellValue, TableColumnKind, TableQueryProvider, TableSchema, TableViewState, TableViewResult, TableRowsPage, TableCellEdit, TableEditResult, TableDistinctResult } from '@bendyline/squisq/table';
2
+ import { ReactElement } from 'react';
3
+
4
+ /**
5
+ * Columnar ingestion: a format-neutral cell grid → typed columns the worker
6
+ * kernel operates on. grid-react never parses files itself — the editor
7
+ * adapter feeds an `IngestTable` built from the lazy formats readers (CSV
8
+ * strings with a typing pass; XLSX typed `XlsxCell.value`s).
9
+ *
10
+ * Layout per column kind:
11
+ * - `number` → `Float64Array data` + `Uint8Array valid` (an explicit blank
12
+ * mask — NaN-as-sentinel is rejected because a coercion bug could
13
+ * legitimately produce NaN, and masks make blank semantics testable)
14
+ * - `string`/`date` → DICTIONARY-encoded: `Int32Array codes` (−1 = blank) +
15
+ * `string[] dict`. Chosen because (1) the code array is transferable, so
16
+ * the bulk of the bytes move to the worker rather than being cloned;
17
+ * (2) locale-aware sort is one `rank` array over unique values instead of
18
+ * per-row collation; (3) `contains` lowercasing happens once per dict
19
+ * entry. High-cardinality columns degrade gracefully (dict length = row
20
+ * count — no worse than `string[]`).
21
+ * - `boolean` → `Uint8Array data` + `Uint8Array valid`
22
+ *
23
+ * Column typing follows the SHARED rule in `@bendyline/squisq/table`
24
+ * (`isNumericCellText`: all non-blank cells numeric, no leading-zero
25
+ * strings) so kernel results stay parity-testable against
26
+ * `applyTableViewState`.
27
+ */
28
+
29
+ /** A format-neutral cell: `null` = blank. */
30
+ type IngestCell = TableCellValue;
31
+ interface IngestColumnHint {
32
+ /** Force a column kind (XLSX adapters know; CSV lets inference decide). */
33
+ kind?: TableColumnKind;
34
+ }
35
+ interface IngestTable {
36
+ headers: string[];
37
+ /** Row-major cells; ragged rows are padded with blanks. */
38
+ cells: IngestCell[][];
39
+ /** Optional per-column hints, index-aligned with `headers`. */
40
+ hints?: (IngestColumnHint | undefined)[];
41
+ }
42
+ interface NumberColumn {
43
+ kind: 'number';
44
+ name: string;
45
+ data: Float64Array;
46
+ valid: Uint8Array;
47
+ }
48
+ interface DictColumn {
49
+ kind: 'string' | 'date';
50
+ name: string;
51
+ /** Dictionary code per row; −1 = blank. */
52
+ codes: Int32Array;
53
+ dict: string[];
54
+ }
55
+ interface BooleanColumn {
56
+ kind: 'boolean';
57
+ name: string;
58
+ data: Uint8Array;
59
+ valid: Uint8Array;
60
+ }
61
+ type StoreColumn = NumberColumn | DictColumn | BooleanColumn;
62
+ interface ColumnarTable {
63
+ columns: StoreColumn[];
64
+ rowCount: number;
65
+ }
66
+ /** Build typed columns from a neutral cell grid. */
67
+ declare function buildColumnarTable(table: IngestTable): ColumnarTable;
68
+ /** Read one cell back out of typed columns (`null` = blank). */
69
+ declare function columnCellValue(column: StoreColumn, row: number): TableCellValue;
70
+
71
+ /**
72
+ * The table kernel — sort/filter/window/edit over transferred typed columns.
73
+ *
74
+ * SHIPPING CONSTRAINT: this function's SOURCE is the worker. It is embedded
75
+ * via `tableKernel.toString()` into a Blob URL (`buildKernelSource`), the
76
+ * same zero-asset pattern as the teleprompter's PCM worklet — editor
77
+ * packages ship no runtime asset files, and URL-based workers would force
78
+ * bundler configuration onto every host. Therefore:
79
+ *
80
+ * - the function must reference NOTHING from module scope (all helpers are
81
+ * nested; only web-worker globals like `Intl` are used);
82
+ * - tsup must never minify this package (mangled `toString()` output);
83
+ * both properties are pinned by tests in `kernel.test.ts`.
84
+ *
85
+ * Semantics mirror `applyTableViewState` in `@bendyline/squisq/table`
86
+ * (numeric compare on number columns, collator-ranked dictionary compare on
87
+ * text, blanks last regardless of direction, stable sort with source-index
88
+ * tie-break, filter-then-sort) — parity is a tested property, not an
89
+ * aspiration. Type/collation rules are COPIED here by necessity (zero
90
+ * imports); the parity suite is what keeps the copies honest.
91
+ *
92
+ * Edits mutate columns in place and NEVER re-permute: a row teleporting out
93
+ * from under the caret mid-entry is hostile. `applyEdits` reports
94
+ * `staleView` when an edited cell touched an active sort/filter column so
95
+ * the UI can offer an explicit refresh.
96
+ */
97
+ interface KernelColumnPayload {
98
+ name: string;
99
+ kind: 'number' | 'string' | 'date' | 'boolean';
100
+ /** number/boolean: values; string/date: dictionary codes (−1 = blank). */
101
+ data: Float64Array | Int32Array | Uint8Array;
102
+ valid?: Uint8Array;
103
+ dict?: string[];
104
+ }
105
+ interface KernelSortTerm {
106
+ col: number;
107
+ dir: 'asc' | 'desc';
108
+ }
109
+ interface KernelFilterClause {
110
+ col: number;
111
+ op: '=' | '!=' | '>' | '<' | '>=' | '<=' | '~' | '!~' | '^~' | '$~';
112
+ value: string;
113
+ /** Case-sensitive text matching (the grammar's `*` modifier). */
114
+ caseSensitive?: boolean;
115
+ }
116
+ interface KernelCellEdit {
117
+ rowId: number;
118
+ col: number;
119
+ value: number | string | boolean | null;
120
+ }
121
+ type KernelRequest = {
122
+ type: 'init';
123
+ seq: number;
124
+ columns: KernelColumnPayload[];
125
+ rowCount: number;
126
+ } | {
127
+ type: 'setView';
128
+ seq: number;
129
+ sort: KernelSortTerm[];
130
+ filter: KernelFilterClause[];
131
+ } | {
132
+ type: 'rows';
133
+ seq: number;
134
+ start: number;
135
+ count: number;
136
+ } | {
137
+ type: 'applyEdits';
138
+ seq: number;
139
+ edits: KernelCellEdit[];
140
+ } | {
141
+ type: 'distinct';
142
+ seq: number;
143
+ col: number;
144
+ limit: number;
145
+ } | {
146
+ type: 'dispose';
147
+ };
148
+ type KernelCell = number | string | boolean | null;
149
+ type KernelResponse = {
150
+ type: 'ready';
151
+ seq: number;
152
+ } | {
153
+ type: 'viewResult';
154
+ seq: number;
155
+ viewRowCount: number;
156
+ } | {
157
+ type: 'rowsResult';
158
+ seq: number;
159
+ start: number;
160
+ rowIds: number[];
161
+ cells: KernelCell[][];
162
+ } | {
163
+ type: 'editResult';
164
+ seq: number;
165
+ staleView: boolean;
166
+ } | {
167
+ type: 'distinctResult';
168
+ seq: number;
169
+ values: string[];
170
+ totalDistinct: number;
171
+ hasBlank: boolean;
172
+ } | {
173
+ type: 'error';
174
+ seq: number;
175
+ message: string;
176
+ };
177
+ /** The structural slice of a worker global scope the kernel needs. */
178
+ interface KernelScope {
179
+ onmessage: ((event: {
180
+ data: KernelRequest;
181
+ }) => void) | null;
182
+ postMessage(message: KernelResponse): void;
183
+ }
184
+ declare function tableKernel(scope: KernelScope): void;
185
+ /** The worker source: the kernel applied to the worker's own global scope. */
186
+ declare function buildKernelSource(): string;
187
+
188
+ /**
189
+ * TableStoreClient — the in-house columnar store behind the
190
+ * `TableQueryProvider` contract. Compute (filter/sort/window/edit) runs in
191
+ * the kernel; the kernel runs in a Blob-URL Web Worker when available and
192
+ * falls back to an in-process host otherwise (SSR, tests — the fallback is
193
+ * also the protocol-parity proof, since both paths execute the SAME
194
+ * `tableKernel` source).
195
+ *
196
+ * Transfer note: typed-array halves of the columns are TRANSFERRED to the
197
+ * worker (zero-copy); dictionaries are structured-cloned. After `init` the
198
+ * client's own column buffers are detached — the client keeps only the
199
+ * schema; the ingest source stays with the adapter (it owns save).
200
+ */
201
+
202
+ interface KernelHost {
203
+ post(message: KernelRequest, transfer?: Transferable[]): void;
204
+ onResponse: (message: KernelResponse) => void;
205
+ terminate(): void;
206
+ }
207
+ /** In-process host: drives `tableKernel` directly (SSR/tests/parity). */
208
+ declare class LocalKernelHost implements KernelHost {
209
+ onResponse: (message: KernelResponse) => void;
210
+ private readonly scope;
211
+ constructor();
212
+ post(message: KernelRequest): void;
213
+ terminate(): void;
214
+ }
215
+ interface TableStoreClientOptions {
216
+ /** Force the in-process host (tests, SSR probes). */
217
+ forceLocal?: boolean;
218
+ }
219
+ declare class TableStoreClient implements TableQueryProvider {
220
+ private readonly host;
221
+ private readonly schema;
222
+ private seq;
223
+ private viewRowCount;
224
+ private readonly pending;
225
+ private ready;
226
+ constructor(table: IngestTable, options?: TableStoreClientOptions);
227
+ private request;
228
+ describe(): Promise<TableSchema>;
229
+ setView(view: TableViewState): Promise<TableViewResult>;
230
+ rows(start: number, count: number): Promise<TableRowsPage>;
231
+ applyEdits(edits: TableCellEdit[]): Promise<TableEditResult>;
232
+ distinct(col: number, limit: number): Promise<TableDistinctResult>;
233
+ /** Row count under the current view (post-filter). */
234
+ get currentViewRowCount(): number;
235
+ dispose(): void;
236
+ }
237
+
238
+ /**
239
+ * EditJournal — unsaved cell edits, keyed by immutable source row id.
240
+ *
241
+ * Lives on the MAIN thread beside the store client. Row identity is the
242
+ * source row index assigned at ingest, so an entry is unambiguous under any
243
+ * sort/filter permutation and unaffected by edits changing sort keys —
244
+ * identity was never derived from values.
245
+ *
246
+ * Batches are the undo unit (one commit = one batch). The journal is
247
+ * additionally cached module-level by `(cacheKey)` — conventionally
248
+ * `${path}@${mediaRevision}` — so a widget unmount/remount (ProseMirror
249
+ * re-decoration) does not lose unsaved edits; a revision bump (save or
250
+ * re-upload) naturally misses the cache and starts clean.
251
+ */
252
+
253
+ interface JournalEntry {
254
+ rowId: number;
255
+ col: number;
256
+ prev: TableCellValue;
257
+ next: TableCellValue;
258
+ }
259
+ declare class EditJournal {
260
+ /** Latest value per cell, keyed `rowId:col`. */
261
+ private readonly latest;
262
+ private readonly undoStack;
263
+ private readonly redoStack;
264
+ get dirtyCount(): number;
265
+ get canUndo(): boolean;
266
+ get canRedo(): boolean;
267
+ isDirty(rowId: number, col: number): boolean;
268
+ /** Record one committed batch of edits. Clears the redo stack. */
269
+ commit(entries: JournalEntry[]): void;
270
+ /** Pop the latest batch; returns the INVERSE edits to apply to the store. */
271
+ undo(): TableCellEdit[];
272
+ /** Re-apply the most recently undone batch. */
273
+ redo(): TableCellEdit[];
274
+ /** Net outstanding edits (what a Save must persist). */
275
+ entries(): JournalEntry[];
276
+ clear(): void;
277
+ private applyLatest;
278
+ private rebuildLatest;
279
+ }
280
+ /**
281
+ * Get (or create) the journal for a cache key. Any entry for the same path
282
+ * at a DIFFERENT revision is discarded — a revision bump means the bytes
283
+ * changed underneath the edits.
284
+ */
285
+ declare function journalFor(path: string, revision: number): EditJournal;
286
+ declare function discardJournal(path: string): void;
287
+
288
+ /**
289
+ * DataGrid — the virtualized grid over a `TableQueryProvider`.
290
+ *
291
+ * Rendering model: TanStack Virtual drives ROW virtualization only (at the
292
+ * ~20-column design target, column virtualization buys nothing and costs
293
+ * sticky-header/selection complexity); columns are a CSS grid template from
294
+ * the width map. The grid owns a bounded scroll container
295
+ * (`overscroll-behavior: contain`) — it never scrolls the document.
296
+ *
297
+ * Interaction contract (kept in sync with the plan spec):
298
+ * - selection: rectangular `{anchor, focus}` in view coordinates; roving
299
+ * tabIndex on the focused cell (real focus — the cell editor needs it);
300
+ * - keyboard: arrows move, Shift extends, Ctrl/Cmd jumps to data edges,
301
+ * PageUp/Down by viewport, Home/End row-wise, Ctrl/Cmd+Home/End
302
+ * grid-wise, Enter commits+moves down, Tab commits+moves right, F2 or
303
+ * typing opens the editor, Escape cancels, Ctrl/Cmd+A selects all,
304
+ * Ctrl/Cmd+Z / +Shift+Z drive the edit journal;
305
+ * - clipboard: `copy` writes TSV (`text/plain`) + `<table>` (`text/html`)
306
+ * from the selection prefetch cache (capped; truncation is announced);
307
+ * - edits: committed per cell through `provider.applyEdits` + the journal;
308
+ * the view is NEVER auto re-sorted after an edit — `staleView` drives an
309
+ * explicit refresh affordance instead;
310
+ * - sort headers cycle asc → desc → none; Shift+click appends a term.
311
+ *
312
+ * Theming: every color resolves through `--squisq-grid-*` tokens with
313
+ * literal fallbacks (see styles/grid.css) so the grid works standalone;
314
+ * editor-react aliases the tokens onto its chrome palette.
315
+ */
316
+
317
+ interface DataGridProps {
318
+ provider: TableQueryProvider;
319
+ /** Present = editable; the journal records batches for undo + save. */
320
+ journal?: EditJournal;
321
+ view: TableViewState;
322
+ onViewChange?: (view: TableViewState) => void;
323
+ /** False = view state is session-only ("not saved to document" hint). */
324
+ viewPersisted?: boolean;
325
+ onSave?: () => void | Promise<void>;
326
+ saving?: boolean;
327
+ height?: number;
328
+ /** Reason editing is unavailable (e.g. parquet sidecars). */
329
+ readOnlyReason?: string;
330
+ /**
331
+ * Per-cell lock predicate (source row id + column). A locked cell renders
332
+ * with a lock affordance and refuses edits while its neighbors stay
333
+ * editable — how XLSX keeps formula/date cells safe from value patching.
334
+ */
335
+ isCellLocked?: (rowId: number, col: number) => boolean;
336
+ /** Tooltip/announcement for locked cells. */
337
+ lockedReason?: string;
338
+ /**
339
+ * Formula editing, when the host has a calculation engine. A cell whose
340
+ * `getFormula` returns text edits as `=formula`; any committed draft
341
+ * starting with `=` routes through `commitFormula`, whose returned
342
+ * `updates` (the edited cell plus recalculated dependents) are applied to
343
+ * the provider and the visible cache.
344
+ */
345
+ formulaSupport?: FormulaSupport;
346
+ /**
347
+ * Notified after each committed VALUE edit. A host mirroring edits into a
348
+ * calculation engine may RETURN the recalculated dependent updates, which
349
+ * the grid applies to the provider and the visible cache — this is what
350
+ * makes formula cells recalc live when a plain value changes.
351
+ */
352
+ onCellEdited?: (edit: TableCellEdit) => void | TableCellEdit[] | Promise<void | TableCellEdit[]>;
353
+ /** Unsaved formula edits (kept outside the value journal) for the save bar. */
354
+ extraDirtyCount?: number;
355
+ /**
356
+ * Discard handler for those formula edits (runs beside journal discard).
357
+ * The host reverts its engine + provider; the grid then refetches.
358
+ */
359
+ onDiscardExtra?: () => void | Promise<void>;
360
+ className?: string;
361
+ }
362
+ interface FormulaCommitResult {
363
+ ok: boolean;
364
+ /** User-facing message when `ok` is false (parse error, budget, …). */
365
+ error?: string;
366
+ /** Value updates to apply: the edited cell + recalculated dependents. */
367
+ updates?: TableCellEdit[];
368
+ }
369
+ interface FormulaSupport {
370
+ /** Formula source (no `=`) for a cell, or undefined for plain values. */
371
+ getFormula(rowId: number, col: number): string | undefined;
372
+ commitFormula(rowId: number, col: number, formula: string): Promise<FormulaCommitResult>;
373
+ }
374
+ declare function DataGrid({ provider, journal, view, onViewChange, viewPersisted, onSave, saving, height, readOnlyReason, isCellLocked, lockedReason, formulaSupport, onCellEdited, extraDirtyCount, onDiscardExtra, className, }: DataGridProps): ReactElement;
375
+
376
+ export { type ColumnarTable, DataGrid, type DataGridProps, EditJournal, type FormulaCommitResult, type FormulaSupport, type IngestCell, type IngestColumnHint, type IngestTable, type JournalEntry, type KernelCellEdit, type KernelColumnPayload, type KernelRequest, type KernelResponse, type KernelScope, LocalKernelHost, type StoreColumn, TableStoreClient, type TableStoreClientOptions, buildColumnarTable, buildKernelSource, columnCellValue, discardJournal, journalFor, tableKernel };