@rowsncolumns/rnc-engine 0.10.132

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 (37) hide show
  1. package/LICENSE.md +100 -0
  2. package/README.md +83 -0
  3. package/dist/esm/command-handlers.d.ts +169 -0
  4. package/dist/esm/command-handlers.d.ts.map +1 -0
  5. package/dist/esm/command-handlers.js +404 -0
  6. package/dist/esm/fold.d.ts +140 -0
  7. package/dist/esm/fold.d.ts.map +1 -0
  8. package/dist/esm/fold.js +539 -0
  9. package/dist/esm/index.d.ts +620 -0
  10. package/dist/esm/index.d.ts.map +1 -0
  11. package/dist/esm/index.js +425 -0
  12. package/dist/esm/io.d.ts +119 -0
  13. package/dist/esm/io.d.ts.map +1 -0
  14. package/dist/esm/io.js +318 -0
  15. package/dist/esm/keys.d.ts +20 -0
  16. package/dist/esm/keys.d.ts.map +1 -0
  17. package/dist/esm/keys.js +41 -0
  18. package/dist/esm/move-rescope.d.ts +18 -0
  19. package/dist/esm/move-rescope.d.ts.map +1 -0
  20. package/dist/esm/move-rescope.js +38 -0
  21. package/dist/esm/use-engine-core.d.ts +293 -0
  22. package/dist/esm/use-engine-core.d.ts.map +1 -0
  23. package/dist/esm/use-engine-core.js +2279 -0
  24. package/dist/esm/use-engine-pivot-editor.d.ts +78 -0
  25. package/dist/esm/use-engine-pivot-editor.d.ts.map +1 -0
  26. package/dist/esm/use-engine-pivot-editor.js +252 -0
  27. package/dist/esm/use-spreadsheet-engine.d.ts +7 -0
  28. package/dist/esm/use-spreadsheet-engine.d.ts.map +1 -0
  29. package/dist/esm/use-spreadsheet-engine.js +23 -0
  30. package/dist/esm/use-spreadsheet-ui.d.ts +39 -0
  31. package/dist/esm/use-spreadsheet-ui.d.ts.map +1 -0
  32. package/dist/esm/use-spreadsheet-ui.js +78 -0
  33. package/dist/esm/wasm/rnc_wasm.d.ts +394 -0
  34. package/dist/esm/wasm/rnc_wasm.js +1455 -0
  35. package/dist/esm/wasm/rnc_wasm_bg.wasm +0 -0
  36. package/dist/esm/wasm/rnc_wasm_bg.wasm.d.ts +81 -0
  37. package/package.json +51 -0
@@ -0,0 +1,78 @@
1
+ import type { PivotTable, PivotValue, SheetRange } from "@rowsncolumns/spreadsheet";
2
+ /**
3
+ * Bridges `@rowsncolumns/pivot`'s `<PivotEditor>` (the pivot settings sidebar) to the RNC
4
+ * engine's pivot commands — the reusable version of the spec-transform pattern previously
5
+ * duplicated by each engine host.
6
+ *
7
+ * The legacy editor is driven by `usePivot`, which is DuckDB-coupled: it computes pivot
8
+ * results in JS and writes them into a full `sheetData`. The engine instead computes pivots
9
+ * wasm-side from the `PivotTable` spec, and `create-pivot-table` / `update-pivot-table`
10
+ * render the output grid immediately. So every editor callback here is a PURE SPEC TRANSFORM
11
+ * over the current `PivotTable` (looked up by `pivotId` in the fold's `pivotTables` map),
12
+ * persisted via `onUpdatePivotTable` — the engine recomputes and re-renders in the same
13
+ * undoable commit.
14
+ *
15
+ * Each row/column/value field carries both `field` (the source column header name) and a
16
+ * `sourceColumnOffset` resolved from the source range's header row, so the engine can map the
17
+ * field to its source column regardless of which key it reads. `getGridValues` reads the
18
+ * source range straight from the engine (the same `{ headers, rows }` shape `usePivot` builds
19
+ * for the editor's field picker).
20
+ *
21
+ * Usage (the returned object spreads directly onto `<PivotEditor>`):
22
+ * ```tsx
23
+ * const pivotEditor = useEnginePivotEditor({
24
+ * pivotTables: state.pivotTables,
25
+ * getEffectiveValue: state.getEffectiveValue,
26
+ * onUpdatePivotTable: state.onUpdatePivotTable,
27
+ * onRefreshPivot: state.onRefreshPivot,
28
+ * });
29
+ * <PivotEditor pivotTable={active} {...pivotEditor} onRequestClose={close} />
30
+ * ```
31
+ */
32
+ /** Structural stand-in for `@rowsncolumns/pivot`'s `PivotField` (this package deliberately
33
+ * does not depend on the pivot package — the shape is what the editor's callbacks pass). */
34
+ export type EnginePivotField = {
35
+ field: string;
36
+ displayName?: string;
37
+ aggFunc?: PivotValue["aggFunc"];
38
+ };
39
+ /** The `columns` sentinel that interleaves value-field headers as a column level. */
40
+ export declare const VALUES_PIVOT_FIELD = "__values__";
41
+ export interface UseEnginePivotEditorParams {
42
+ /** The fold's pivot spec map (`state.pivotTables` from `useSpreadsheetEngine`). */
43
+ pivotTables: Map<string | number, PivotTable>;
44
+ /** Full-document effective-value reader (`state.getEffectiveValue`) — drives the editor's
45
+ * field picker and `sourceColumnOffset` resolution (off-window-safe). */
46
+ getEffectiveValue: (sheetId: number, rowIndex: number, columnIndex: number) => unknown;
47
+ /** `state.onUpdatePivotTable` — persists a transformed spec; the engine re-renders. */
48
+ onUpdatePivotTable: (pivotId: PivotTable["pivotId"], pivotTable: PivotTable) => void;
49
+ /** `state.onRefreshPivot` — re-runs the pivot against live source data without a spec
50
+ * change. Optional: when omitted, refresh falls back to re-emitting the current spec
51
+ * (`update-pivot-table` recomputes too). */
52
+ onRefreshPivot?: (pivotId: PivotTable["pivotId"]) => void;
53
+ }
54
+ type GridValues = {
55
+ headers: (string | number | boolean)[];
56
+ rows: (string | number | boolean)[][];
57
+ };
58
+ export declare function useEnginePivotEditor({ pivotTables, getEffectiveValue, onUpdatePivotTable, onRefreshPivot, }: UseEnginePivotEditorParams): {
59
+ getGridValues: (range: SheetRange) => GridValues;
60
+ addRowPivot: (pivotId: PivotTable["pivotId"], field: EnginePivotField) => Promise<void>;
61
+ addColumnPivot: (pivotId: PivotTable["pivotId"], field: EnginePivotField) => Promise<void>;
62
+ addValue: (pivotId: PivotTable["pivotId"], field: EnginePivotField) => Promise<void>;
63
+ removePivotField: (pivotId: PivotTable["pivotId"], field: string, area: "rows" | "columns" | "values") => Promise<void>;
64
+ changeFieldOrder: (pivotId: PivotTable["pivotId"], area: "rows" | "columns" | "values", fromIndex: number, toIndex: number) => Promise<void>;
65
+ setAggregationFunction: (pivotId: PivotTable["pivotId"], field: string, aggFunc: EnginePivotField["aggFunc"]) => Promise<void>;
66
+ setShowAs: (pivotId: PivotTable["pivotId"], field: string, showAs: NonNullable<PivotValue["showAs"]>) => Promise<void>;
67
+ addFilter: (pivotId: PivotTable["pivotId"], field: string, filterValue: unknown) => Promise<void>;
68
+ removeFilter: (pivotId: PivotTable["pivotId"], field: string) => Promise<void>;
69
+ sortPivotField: (pivotId: PivotTable["pivotId"], field: string, direction: "asc" | "desc") => Promise<void>;
70
+ removeSortPivotField: (pivotId: PivotTable["pivotId"], field: string) => Promise<void>;
71
+ toggleSubtotals: (pivotId: PivotTable["pivotId"], show: boolean) => Promise<void>;
72
+ toggleRowGrandTotals: (pivotId: PivotTable["pivotId"], show: boolean) => Promise<void>;
73
+ toggleColumnGrandTotals: (pivotId: PivotTable["pivotId"], show: boolean) => Promise<void>;
74
+ updatePivotSourceRange: (pivotId: PivotTable["pivotId"], sourceRange: SheetRange) => Promise<void>;
75
+ refreshPivot: (pivotId: PivotTable["pivotId"]) => Promise<void>;
76
+ };
77
+ export {};
78
+ //# sourceMappingURL=use-engine-pivot-editor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-engine-pivot-editor.d.ts","sourceRoot":"","sources":["../../src/use-engine-pivot-editor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,UAAU,EACV,UAAU,EACV,UAAU,EACX,MAAM,2BAA2B,CAAC;AAGnC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH;4FAC4F;AAC5F,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,UAAU,CAAC,SAAS,CAAC,CAAC;CACjC,CAAC;AAEF,qFAAqF;AACrF,eAAO,MAAM,kBAAkB,eAAe,CAAC;AAK/C,MAAM,WAAW,0BAA0B;IACzC,mFAAmF;IACnF,WAAW,EAAE,GAAG,CAAC,MAAM,GAAG,MAAM,EAAE,UAAU,CAAC,CAAC;IAC9C;6EACyE;IACzE,iBAAiB,EAAE,CACjB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,KAChB,OAAO,CAAC;IACb,uFAAuF;IACvF,kBAAkB,EAAE,CAClB,OAAO,EAAE,UAAU,CAAC,SAAS,CAAC,EAC9B,UAAU,EAAE,UAAU,KACnB,IAAI,CAAC;IACV;;gDAE4C;IAC5C,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,UAAU,CAAC,SAAS,CAAC,KAAK,IAAI,CAAC;CAC3D;AAED,KAAK,UAAU,GAAG;IAChB,OAAO,EAAE,CAAC,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,EAAE,CAAC;IACvC,IAAI,EAAE,CAAC,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,EAAE,EAAE,CAAC;CACvC,CAAC;AAEF,wBAAgB,oBAAoB,CAAC,EACnC,WAAW,EACX,iBAAiB,EACjB,kBAAkB,EAClB,cAAc,GACf,EAAE,0BAA0B;2BAGjB,UAAU,KAAG,UAAU;2BAiDf,UAAU,CAAC,SAAS,CAAC,SAAS,gBAAgB;8BAc9C,UAAU,CAAC,SAAS,CAAC,SAAS,gBAAgB;wBA0B9C,UAAU,CAAC,SAAS,CAAC,SAAS,gBAAgB;gCAgBnD,UAAU,CAAC,SAAS,CAAC,SACvB,MAAM,QACP,MAAM,GAAG,SAAS,GAAG,QAAQ;gCAiB1B,UAAU,CAAC,SAAS,CAAC,QACxB,MAAM,GAAG,SAAS,GAAG,QAAQ,aACxB,MAAM,WACR,MAAM;sCAcN,UAAU,CAAC,SAAS,CAAC,SACvB,MAAM,WACJ,gBAAgB,CAAC,SAAS,CAAC;yBAc3B,UAAU,CAAC,SAAS,CAAC,SACvB,MAAM,UACL,WAAW,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;yBAa3B,UAAU,CAAC,SAAS,CAAC,SAAS,MAAM,eAAe,OAAO;4BAY1D,UAAU,CAAC,SAAS,CAAC,SAAS,MAAM;8BAYzC,UAAU,CAAC,SAAS,CAAC,SACvB,MAAM,aACF,KAAK,GAAG,MAAM;oCAcX,UAAU,CAAC,SAAS,CAAC,SAAS,MAAM;+BAYpC,UAAU,CAAC,SAAS,CAAC,QAAQ,OAAO;oCASpC,UAAU,CAAC,SAAS,CAAC,QAAQ,OAAO;uCASpC,UAAU,CAAC,SAAS,CAAC,QAAQ,OAAO;sCASpC,UAAU,CAAC,SAAS,CAAC,eAAe,UAAU;4BAgC9C,UAAU,CAAC,SAAS,CAAC;EAoDxC"}
@@ -0,0 +1,252 @@
1
+ import { useCallback, useMemo } from "react";
2
+ /** The `columns` sentinel that interleaves value-field headers as a column level. */
3
+ export const VALUES_PIVOT_FIELD = "__values__";
4
+ const isValuesField = (field) => field === VALUES_PIVOT_FIELD || field?.toLowerCase() === "values";
5
+ export function useEnginePivotEditor({ pivotTables, getEffectiveValue, onUpdatePivotTable, onRefreshPivot, }) {
6
+ // Read a range straight from the engine — first row is the header, the rest are data rows.
7
+ const getGridValues = useCallback((range) => {
8
+ const headers = [];
9
+ const rows = [];
10
+ for (let r = range.startRowIndex; r <= range.endRowIndex; r++) {
11
+ const row = [];
12
+ for (let c = range.startColumnIndex; c <= range.endColumnIndex; c++) {
13
+ const value = getEffectiveValue(range.sheetId, r, c);
14
+ row.push(typeof value === "string" ||
15
+ typeof value === "number" ||
16
+ typeof value === "boolean"
17
+ ? value
18
+ : "");
19
+ }
20
+ if (r === range.startRowIndex)
21
+ headers.push(...row);
22
+ else
23
+ rows.push(row);
24
+ }
25
+ return { headers, rows };
26
+ }, [getEffectiveValue]);
27
+ // The source column index (0-based, relative to `source.startColumnIndex`) whose header
28
+ // matches `field`.
29
+ const resolveSourceColumnOffset = useCallback((spec, field) => {
30
+ const source = spec.source;
31
+ if (!source)
32
+ return undefined;
33
+ const { headers } = getGridValues(source);
34
+ const offset = headers.findIndex((h) => String(h) === field);
35
+ return offset >= 0 ? offset : undefined;
36
+ }, [getGridValues]);
37
+ const getSpec = useCallback((pivotId) => pivotTables.get(pivotId), [pivotTables]);
38
+ const update = useCallback((pivotId, next) => onUpdatePivotTable(pivotId, next), [onUpdatePivotTable]);
39
+ const addRowPivot = useCallback(async (pivotId, field) => {
40
+ const spec = getSpec(pivotId);
41
+ if (!spec || spec.rows.some((g) => g.field === field.field))
42
+ return;
43
+ const group = {
44
+ field: field.field,
45
+ displayName: field.displayName ?? field.field,
46
+ sourceColumnOffset: resolveSourceColumnOffset(spec, field.field),
47
+ };
48
+ update(pivotId, { ...spec, rows: [...spec.rows, group] });
49
+ }, [getSpec, resolveSourceColumnOffset, update]);
50
+ const addColumnPivot = useCallback(async (pivotId, field) => {
51
+ const spec = getSpec(pivotId);
52
+ if (!spec)
53
+ return;
54
+ // The "Values" sentinel places the value-field headers as a column level; there can
55
+ // only be one, and it conventionally leads (PivotManager.addColumnPivot).
56
+ if (isValuesField(field.field)) {
57
+ if (spec.columns.some((g) => isValuesField(g.field)))
58
+ return;
59
+ const sentinel = {
60
+ field: VALUES_PIVOT_FIELD,
61
+ displayName: "Values",
62
+ };
63
+ update(pivotId, { ...spec, columns: [sentinel, ...spec.columns] });
64
+ return;
65
+ }
66
+ if (spec.columns.some((g) => g.field === field.field))
67
+ return;
68
+ const group = {
69
+ field: field.field,
70
+ displayName: field.displayName ?? field.field,
71
+ sourceColumnOffset: resolveSourceColumnOffset(spec, field.field),
72
+ };
73
+ update(pivotId, { ...spec, columns: [...spec.columns, group] });
74
+ }, [getSpec, resolveSourceColumnOffset, update]);
75
+ const addValue = useCallback(async (pivotId, field) => {
76
+ const spec = getSpec(pivotId);
77
+ if (!spec || spec.values.some((v) => v.field === field.field))
78
+ return;
79
+ const value = {
80
+ field: field.field,
81
+ displayName: field.displayName ?? field.field,
82
+ aggFunc: field.aggFunc ?? "sum",
83
+ sourceColumnOffset: resolveSourceColumnOffset(spec, field.field),
84
+ };
85
+ update(pivotId, { ...spec, values: [...spec.values, value] });
86
+ }, [getSpec, resolveSourceColumnOffset, update]);
87
+ const removePivotField = useCallback(async (pivotId, field, area) => {
88
+ const spec = getSpec(pivotId);
89
+ if (!spec)
90
+ return;
91
+ const next = area === "columns" && isValuesField(field)
92
+ ? spec.columns.filter((g) => !isValuesField(g.field))
93
+ : spec[area].filter((item) => item.field !== field);
94
+ update(pivotId, { ...spec, [area]: next });
95
+ }, [getSpec, update]);
96
+ const changeFieldOrder = useCallback(async (pivotId, area, fromIndex, toIndex) => {
97
+ const spec = getSpec(pivotId);
98
+ if (!spec)
99
+ return;
100
+ const arr = [...spec[area]];
101
+ const [moved] = arr.splice(fromIndex, 1);
102
+ arr.splice(toIndex, 0, moved);
103
+ update(pivotId, { ...spec, [area]: arr });
104
+ }, [getSpec, update]);
105
+ const setAggregationFunction = useCallback(async (pivotId, field, aggFunc) => {
106
+ const spec = getSpec(pivotId);
107
+ if (!spec || !aggFunc)
108
+ return;
109
+ const values = spec.values.map((v) => v.field === field ? { ...v, aggFunc } : v);
110
+ update(pivotId, { ...spec, values });
111
+ }, [getSpec, update]);
112
+ const setShowAs = useCallback(async (pivotId, field, showAs) => {
113
+ const spec = getSpec(pivotId);
114
+ if (!spec)
115
+ return;
116
+ const values = spec.values.map((v) => v.field === field ? { ...v, showAs } : v);
117
+ update(pivotId, { ...spec, values });
118
+ }, [getSpec, update]);
119
+ const addFilter = useCallback(async (pivotId, field, filterValue) => {
120
+ const spec = getSpec(pivotId);
121
+ if (!spec)
122
+ return;
123
+ update(pivotId, {
124
+ ...spec,
125
+ filters: { ...spec.filters, [field]: filterValue },
126
+ });
127
+ }, [getSpec, update]);
128
+ const removeFilter = useCallback(async (pivotId, field) => {
129
+ const spec = getSpec(pivotId);
130
+ if (!spec)
131
+ return;
132
+ const filters = { ...spec.filters };
133
+ delete filters[field];
134
+ update(pivotId, { ...spec, filters });
135
+ }, [getSpec, update]);
136
+ const sortPivotField = useCallback(async (pivotId, field, direction) => {
137
+ const spec = getSpec(pivotId);
138
+ if (!spec)
139
+ return;
140
+ const sortModel = [...(spec.sortModel ?? [])];
141
+ const idx = sortModel.findIndex((s) => s.field === field);
142
+ if (idx >= 0)
143
+ sortModel[idx] = { field, sort: direction };
144
+ else
145
+ sortModel.push({ field, sort: direction });
146
+ update(pivotId, { ...spec, sortModel });
147
+ }, [getSpec, update]);
148
+ const removeSortPivotField = useCallback(async (pivotId, field) => {
149
+ const spec = getSpec(pivotId);
150
+ if (!spec)
151
+ return;
152
+ update(pivotId, {
153
+ ...spec,
154
+ sortModel: (spec.sortModel ?? []).filter((s) => s.field !== field),
155
+ });
156
+ }, [getSpec, update]);
157
+ const toggleSubtotals = useCallback(async (pivotId, show) => {
158
+ const spec = getSpec(pivotId);
159
+ if (!spec)
160
+ return;
161
+ update(pivotId, { ...spec, showSubtotals: show });
162
+ }, [getSpec, update]);
163
+ const toggleRowGrandTotals = useCallback(async (pivotId, show) => {
164
+ const spec = getSpec(pivotId);
165
+ if (!spec)
166
+ return;
167
+ update(pivotId, { ...spec, showRowGrandTotals: show });
168
+ }, [getSpec, update]);
169
+ const toggleColumnGrandTotals = useCallback(async (pivotId, show) => {
170
+ const spec = getSpec(pivotId);
171
+ if (!spec)
172
+ return;
173
+ update(pivotId, { ...spec, showColumnGrandTotals: show });
174
+ }, [getSpec, update]);
175
+ const updatePivotSourceRange = useCallback(async (pivotId, sourceRange) => {
176
+ const spec = getSpec(pivotId);
177
+ if (!spec)
178
+ return;
179
+ // `sourceColumnOffset` is relative to the source range, so a new source can shift every
180
+ // field's column — re-resolve each row/column/value offset against the new source's
181
+ // header row by field name (the sentinel has no source column).
182
+ const { headers } = getGridValues(sourceRange);
183
+ const offsetFor = (field) => {
184
+ if (isValuesField(field))
185
+ return undefined;
186
+ const i = headers.findIndex((h) => String(h) === field);
187
+ return i >= 0 ? i : undefined;
188
+ };
189
+ update(pivotId, {
190
+ ...spec,
191
+ source: sourceRange,
192
+ rows: spec.rows.map((g) => ({ ...g, sourceColumnOffset: offsetFor(g.field) })),
193
+ columns: spec.columns.map((g) => ({
194
+ ...g,
195
+ sourceColumnOffset: offsetFor(g.field),
196
+ })),
197
+ values: spec.values.map((v) => ({
198
+ ...v,
199
+ sourceColumnOffset: offsetFor(v.field),
200
+ })),
201
+ });
202
+ }, [getSpec, getGridValues, update]);
203
+ // Real refresh: re-run the pivot against live source data (no spec change). Falls back to
204
+ // re-emitting the spec when the host didn't wire `onRefreshPivot`.
205
+ const refreshPivot = useCallback(async (pivotId) => {
206
+ if (onRefreshPivot) {
207
+ onRefreshPivot(pivotId);
208
+ return;
209
+ }
210
+ const spec = getSpec(pivotId);
211
+ if (!spec)
212
+ return;
213
+ update(pivotId, { ...spec });
214
+ }, [onRefreshPivot, getSpec, update]);
215
+ return useMemo(() => ({
216
+ getGridValues,
217
+ addRowPivot,
218
+ addColumnPivot,
219
+ addValue,
220
+ removePivotField,
221
+ changeFieldOrder,
222
+ setAggregationFunction,
223
+ setShowAs,
224
+ addFilter,
225
+ removeFilter,
226
+ sortPivotField,
227
+ removeSortPivotField,
228
+ toggleSubtotals,
229
+ toggleRowGrandTotals,
230
+ toggleColumnGrandTotals,
231
+ updatePivotSourceRange,
232
+ refreshPivot,
233
+ }), [
234
+ getGridValues,
235
+ addRowPivot,
236
+ addColumnPivot,
237
+ addValue,
238
+ removePivotField,
239
+ changeFieldOrder,
240
+ setAggregationFunction,
241
+ setShowAs,
242
+ addFilter,
243
+ removeFilter,
244
+ sortPivotField,
245
+ removeSortPivotField,
246
+ toggleSubtotals,
247
+ toggleRowGrandTotals,
248
+ toggleColumnGrandTotals,
249
+ updatePivotSourceRange,
250
+ refreshPivot,
251
+ ]);
252
+ }
@@ -0,0 +1,7 @@
1
+ import { type EngineSpreadsheet, type UseEngineSpreadsheetOptions } from "./use-engine-core";
2
+ import { type SpreadsheetUI } from "./use-spreadsheet-ui";
3
+ /** The composed surface: the engine core (data/calc/commands/accessors) + the chrome (dialogs + host
4
+ * intents). This — not `useEngineCore` alone — is the drop-in replacement for `useSpreadsheetState`. */
5
+ export type SpreadsheetEngine = EngineSpreadsheet & SpreadsheetUI;
6
+ export declare function useSpreadsheetEngine(options: UseEngineSpreadsheetOptions): SpreadsheetEngine;
7
+ //# sourceMappingURL=use-spreadsheet-engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-spreadsheet-engine.d.ts","sourceRoot":"","sources":["../../src/use-spreadsheet-engine.ts"],"names":[],"mappings":"AAWA,OAAO,EACL,KAAK,iBAAiB,EAEtB,KAAK,2BAA2B,EACjC,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,KAAK,aAAa,EAAoB,MAAM,sBAAsB,CAAC;AAE5E;wGACwG;AACxG,MAAM,MAAM,iBAAiB,GAAG,iBAAiB,GAAG,aAAa,CAAC;AAalE,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,2BAA2B,GACnC,iBAAiB,CA6BnB"}
@@ -0,0 +1,23 @@
1
+ import { useCallback, useMemo } from "react";
2
+ import { useEngineCore, } from "./use-engine-core";
3
+ import { useSpreadsheetUI } from "./use-spreadsheet-ui";
4
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
5
+ const _assertDropInParity = true;
6
+ export function useSpreadsheetEngine(options) {
7
+ const core = useEngineCore(options);
8
+ const ui = useSpreadsheetUI();
9
+ // Insert → Checkbox / Dropdown pass a pre-built rule (checkbox with `showEditUI: false`). The
10
+ // chrome opener alone would drop it and only show the dialog — create the rule through the engine
11
+ // first, then open the dialog unless suppressed, mirroring the JS host's
12
+ // `useOnRequestDataValidation` (create immediately; dialog is optional edit UI on top).
13
+ const onRequestDataValidation = useCallback((sheetId, activeCell, selections, rule, showEditUI = true) => {
14
+ if (rule) {
15
+ core.onCreateDataValidationRule?.(rule);
16
+ }
17
+ if (showEditUI) {
18
+ ui.onRequestDataValidation(sheetId, activeCell, selections, rule, showEditUI);
19
+ }
20
+ }, [core, ui]);
21
+ // `ui` last: its dialog-openers + host-intent defaults complete the engine core's surface.
22
+ return useMemo(() => ({ ...core, ...ui, onRequestDataValidation }), [core, ui, onRequestDataValidation]);
23
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * `useSpreadsheetUI` — the backend-agnostic **chrome** layer: which dialog is open, the target it's
3
+ * editing, and the host-owned UI intents (clipboard, citations, comments, drag-drop, paint-format,
4
+ * link/file insertion). It owns NO document data and runs NO calc, so it composes equally with either
5
+ * data hook ({@link useEngineCore} / `useSpreadsheetState`). The `onRequest*` handlers are the seam
6
+ * CanvasGrid calls ("the user asked to edit this table / open the CF dialog / …"); here they flip
7
+ * dialog state. The remaining handlers are inert defaults the host app overrides (it owns the real
8
+ * clipboard / citation / comment behavior). The app renders its dialog components from
9
+ * `dialogs`/`dialogContext` and submits the result through the data hook's commands (e.g.
10
+ * `onUpdateTable`). Self-contained (plain `useState`) so it carries no jotai/`spreadsheet-state`
11
+ * dependency and can move to its own package unchanged.
12
+ */
13
+ import type { useSpreadsheetState } from "@rowsncolumns/spreadsheet-state";
14
+ /** The reference hook's return type — used to type every chrome handler EXACTLY as `useSpreadsheetState`
15
+ * exposes it (so they're drop-in for CanvasGrid's props). Erased at runtime. */
16
+ type SS = ReturnType<typeof useSpreadsheetState>;
17
+ /** The dialogs the chrome can open. */
18
+ export type DialogName = "editTable" | "editEmbed" | "editSlicer" | "deleteSheet" | "namedRange" | "resize" | "formatCells" | "conditionalFormat" | "insertImage" | "insertLink" | "dataValidation" | "note";
19
+ /** The host/chrome slice of `useSpreadsheetState`: dialog-openers (`onRequest*`, `onInsertNote`) plus
20
+ * the few intents the HOST genuinely owns (file drag/drop/insert). Picked from `SS` so signatures stay
21
+ * exact. Everything that reads or mutates engine data — paint format, citations, comments, link follow,
22
+ * CF preview — moved to {@link useEngineCore}; the chrome is backend-agnostic UI only. */
23
+ export type ChromeParityStubs = Pick<SS, "onRequestConditionalFormat" | "onRequestDataValidation" | "onRequestDefineNamedRange" | "onRequestDeleteSheet" | "onRequestEditEmbed" | "onRequestEditSlicer" | "onRequestEditTable" | "onRequestFormatCells" | "onRequestInsertImage" | "onRequestInsertLink" | "onRequestResize" | "onRequestUpdateNamedRange" | "onInsertNote" | "onDragOver" | "onDrop" | "onInsertFile">;
24
+ /** Chrome state + the dialog-opener handlers + host-owned UI intents. Spread into
25
+ * {@link useSpreadsheetEngine}'s return, where these override the engine core's omitted surface. */
26
+ export interface SpreadsheetUI extends ChromeParityStubs {
27
+ /** Which dialogs are currently open. */
28
+ dialogs: Partial<Record<DialogName, boolean>>;
29
+ /** The target each open dialog is editing (the table / embed / sheetId / cell+selection it was
30
+ * opened for) — the app reads this to populate the dialog. */
31
+ dialogContext: Partial<Record<DialogName, unknown>>;
32
+ /** Open a dialog, stashing the value it should edit. */
33
+ openDialog: (name: DialogName, context?: unknown) => void;
34
+ /** Close a dialog. */
35
+ closeDialog: (name: DialogName) => void;
36
+ }
37
+ export declare function useSpreadsheetUI(): SpreadsheetUI;
38
+ export {};
39
+ //# sourceMappingURL=use-spreadsheet-ui.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-spreadsheet-ui.d.ts","sourceRoot":"","sources":["../../src/use-spreadsheet-ui.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;AAG3E;gFACgF;AAChF,KAAK,EAAE,GAAG,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEjD,uCAAuC;AACvC,MAAM,MAAM,UAAU,GAClB,WAAW,GACX,WAAW,GACX,YAAY,GACZ,aAAa,GACb,YAAY,GACZ,QAAQ,GACR,aAAa,GACb,mBAAmB,GACnB,aAAa,GACb,YAAY,GACZ,gBAAgB,GAChB,MAAM,CAAC;AAEX;;;0FAG0F;AAC1F,MAAM,MAAM,iBAAiB,GAAG,IAAI,CAClC,EAAE,EACA,4BAA4B,GAC5B,yBAAyB,GACzB,2BAA2B,GAC3B,sBAAsB,GACtB,oBAAoB,GACpB,qBAAqB,GACrB,oBAAoB,GACpB,sBAAsB,GACtB,sBAAsB,GACtB,qBAAqB,GACrB,iBAAiB,GACjB,2BAA2B,GAC3B,cAAc,GACd,YAAY,GACZ,QAAQ,GACR,cAAc,CACjB,CAAC;AAYF;oGACoG;AACpG,MAAM,WAAW,aAAc,SAAQ,iBAAiB;IACtD,wCAAwC;IACxC,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,CAAC;IAC9C;kEAC8D;IAC9D,aAAa,EAAE,OAAO,CAAC,MAAM,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,CAAC;IACpD,wDAAwD;IACxD,UAAU,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IAC1D,sBAAsB;IACtB,WAAW,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,CAAC;CACzC;AAED,wBAAgB,gBAAgB,IAAI,aAAa,CA+HhD"}
@@ -0,0 +1,78 @@
1
+ import { useCallback, useMemo, useState } from "react";
2
+ // A rejected promise (`Promise<never>` → assignable anywhere) for the async intents the HOST owns —
3
+ // paste / file-drop. An honest signal if a drop-in caller ever invokes one without wiring it.
4
+ const hostOwned = (op) => Promise.reject(new Error(`${op} is host-owned in the chrome layer`));
5
+ // Inert defaults for the file-drag/drop intents the host app owns. Module-level → stable identity.
6
+ const onDragOver = () => { };
7
+ const onDrop = () => Promise.resolve();
8
+ const onInsertFile = () => hostOwned("onInsertFile");
9
+ export function useSpreadsheetUI() {
10
+ const [dialogs, setDialogs] = useState({});
11
+ const [dialogContext, setDialogContext] = useState({});
12
+ const openDialog = useCallback((name, context) => {
13
+ setDialogs((prev) => ({ ...prev, [name]: true }));
14
+ setDialogContext((prev) => ({ ...prev, [name]: context }));
15
+ }, []);
16
+ const closeDialog = useCallback((name) => {
17
+ setDialogs((prev) => ({ ...prev, [name]: false }));
18
+ }, []);
19
+ // Each opener stashes the value the dialog edits, then opens it. Arg shapes come from `SS[K]`, so
20
+ // they match exactly what CanvasGrid passes.
21
+ const onRequestEditTable = useCallback((table) => openDialog("editTable", table), [openDialog]);
22
+ const onRequestEditEmbed = useCallback((embed) => openDialog("editEmbed", embed), [openDialog]);
23
+ const onRequestEditSlicer = useCallback((slicer) => openDialog("editSlicer", slicer), [openDialog]);
24
+ const onRequestDeleteSheet = useCallback((sheetId) => openDialog("deleteSheet", sheetId), [openDialog]);
25
+ const onRequestDefineNamedRange = useCallback((sheetId, activeCell, selections) => openDialog("namedRange", { sheetId, activeCell, selections }), [openDialog]);
26
+ const onRequestUpdateNamedRange = useCallback((sheetId, namedRange) => openDialog("namedRange", { sheetId, namedRange }), [openDialog]);
27
+ const onRequestResize = useCallback((sheetId, indexes, axis) => openDialog("resize", { sheetId, indexes, axis }), [openDialog]);
28
+ const onRequestFormatCells = useCallback(() => openDialog("formatCells"), [openDialog]);
29
+ const onRequestConditionalFormat = useCallback(() => openDialog("conditionalFormat"), [openDialog]);
30
+ const onRequestInsertImage = useCallback((sheetId, activeCell, insertOverCells) => openDialog("insertImage", { sheetId, activeCell, insertOverCells }), [openDialog]);
31
+ const onRequestInsertLink = useCallback((sheetId, activeCell, selections) => openDialog("insertLink", { sheetId, activeCell, selections }), [openDialog]);
32
+ const onRequestDataValidation = useCallback((sheetId, activeCell, selections) => openDialog("dataValidation", { sheetId, activeCell, selections }), [openDialog]);
33
+ // Inserting a note opens the note editor (UI state) — the note text is written via the engine's
34
+ // `update-note` command once the user submits. Mirrors `useInsertNote` (a dialog opener).
35
+ const onInsertNote = useCallback((sheetId, cell, selections) => openDialog("note", { sheetId, cell, selections }), [openDialog]);
36
+ return useMemo(() => ({
37
+ dialogs,
38
+ dialogContext,
39
+ openDialog,
40
+ closeDialog,
41
+ // dialog-openers (real)
42
+ onRequestEditTable,
43
+ onRequestEditEmbed,
44
+ onRequestEditSlicer,
45
+ onRequestDeleteSheet,
46
+ onRequestDefineNamedRange,
47
+ onRequestUpdateNamedRange,
48
+ onRequestResize,
49
+ onRequestFormatCells,
50
+ onRequestConditionalFormat,
51
+ onRequestInsertImage,
52
+ onRequestInsertLink,
53
+ onRequestDataValidation,
54
+ onInsertNote,
55
+ // file drag/drop/insert — genuinely host-owned (inert defaults the app overrides)
56
+ onDragOver,
57
+ onDrop,
58
+ onInsertFile,
59
+ }), [
60
+ dialogs,
61
+ dialogContext,
62
+ openDialog,
63
+ closeDialog,
64
+ onRequestEditTable,
65
+ onRequestEditEmbed,
66
+ onRequestEditSlicer,
67
+ onRequestDeleteSheet,
68
+ onRequestDefineNamedRange,
69
+ onRequestUpdateNamedRange,
70
+ onRequestResize,
71
+ onRequestFormatCells,
72
+ onRequestConditionalFormat,
73
+ onRequestInsertImage,
74
+ onRequestInsertLink,
75
+ onRequestDataValidation,
76
+ onInsertNote,
77
+ ]);
78
+ }