logisheets-core 1.1.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 (63) hide show
  1. package/dist/craft-interactions/index.d.ts +140 -0
  2. package/dist/craft-interactions/index.js +462 -0
  3. package/dist/field/index.d.ts +51 -0
  4. package/dist/field/index.js +70 -0
  5. package/dist/format/index.d.ts +21 -0
  6. package/dist/format/index.js +454 -0
  7. package/dist/index.d.ts +12 -0
  8. package/dist/index.js +17 -0
  9. package/dist/ops/index.d.ts +155 -0
  10. package/dist/ops/index.js +335 -0
  11. package/dist/permissions/index.d.ts +32 -0
  12. package/dist/permissions/index.js +74 -0
  13. package/dist/port.d.ts +9 -0
  14. package/dist/port.js +14 -0
  15. package/dist/selection/index.d.ts +1 -0
  16. package/dist/selection/index.js +1 -0
  17. package/dist/selection/model.d.ts +10 -0
  18. package/dist/selection/model.js +38 -0
  19. package/dist/strings/case.d.ts +2 -0
  20. package/dist/strings/case.js +6 -0
  21. package/dist/strings/char-code.d.ts +422 -0
  22. package/dist/strings/char-code.js +424 -0
  23. package/dist/strings/common_length.d.ts +4 -0
  24. package/dist/strings/common_length.js +28 -0
  25. package/dist/strings/contain.d.ts +4 -0
  26. package/dist/strings/contain.js +60 -0
  27. package/dist/strings/index.d.ts +5 -0
  28. package/dist/strings/index.js +5 -0
  29. package/dist/strings/judges.d.ts +3 -0
  30. package/dist/strings/judges.js +38 -0
  31. package/dist/strings/surrogate.d.ts +16 -0
  32. package/dist/strings/surrogate.js +30 -0
  33. package/dist/structured/index.d.ts +41 -0
  34. package/dist/structured/index.js +50 -0
  35. package/dist/transaction/index.d.ts +8 -0
  36. package/dist/transaction/index.js +11 -0
  37. package/dist/type-guard/index.d.ts +3 -0
  38. package/dist/type-guard/index.js +3 -0
  39. package/dist/type-guard/propterty.d.ts +1 -0
  40. package/dist/type-guard/propterty.js +3 -0
  41. package/dist/type-guard/string.d.ts +1 -0
  42. package/dist/type-guard/string.js +3 -0
  43. package/dist/type-guard/u8.d.ts +1 -0
  44. package/dist/type-guard/u8.js +3 -0
  45. package/dist/utils/a1notation.d.ts +18 -0
  46. package/dist/utils/a1notation.js +76 -0
  47. package/dist/utils/array.d.ts +2 -0
  48. package/dist/utils/array.js +12 -0
  49. package/dist/utils/clone.d.ts +2 -0
  50. package/dist/utils/clone.js +25 -0
  51. package/dist/utils/const.d.ts +7 -0
  52. package/dist/utils/const.js +8 -0
  53. package/dist/utils/equal.d.ts +1 -0
  54. package/dist/utils/equal.js +11 -0
  55. package/dist/utils/index.d.ts +6 -0
  56. package/dist/utils/index.js +6 -0
  57. package/dist/utils/uuid.d.ts +1 -0
  58. package/dist/utils/uuid.js +4 -0
  59. package/dist/validation/index.d.ts +34 -0
  60. package/dist/validation/index.js +60 -0
  61. package/dist/value/index.d.ts +9 -0
  62. package/dist/value/index.js +49 -0
  63. package/package.json +54 -0
@@ -0,0 +1,155 @@
1
+ import type { Payload, ActionEffect, SelectedData, Alignment, StPatternType, Value } from 'logisheets-web';
2
+ import type { Client } from '../port.js';
3
+ import { type ValidationRule, type Violation } from '../validation/index.js';
4
+ import { type FieldColumn } from '../field/index.js';
5
+ import { type FontStyle, type BorderBatchUpdate } from '../format/index.js';
6
+ /**
7
+ * Tells WorkbookOps whether to mark transactions temp (speculative). The
8
+ * browser injects its global temp-mode toggle; the Node runtime leaves it at
9
+ * the default (always committed).
10
+ */
11
+ export type TempModeProvider = () => boolean;
12
+ /** One field of a form-backed block, resolved by the host. */
13
+ export interface FormBlockField {
14
+ /** Display name. */
15
+ name: string;
16
+ /** Engine render id, allocated by the host's FieldManager. */
17
+ renderId: string;
18
+ /** Per-field value-formula template (#FIELD("X") / #KEY); '' if free-form. */
19
+ valueFormula?: string;
20
+ /** Whether the field renders via a host-drawn (DIY) overlay. */
21
+ diyRender: boolean;
22
+ /** Number format applied to the field's render info. */
23
+ numFmt?: string;
24
+ }
25
+ /**
26
+ * High-level workbook operations bound to one engine {@link Client}.
27
+ * Construct one per workbook and share it across the host.
28
+ */
29
+ export declare class WorkbookOps {
30
+ private readonly client;
31
+ private readonly tempMode;
32
+ /** Monotonic id for the throwaway ephemeral cells used by evalFormula. */
33
+ private ephemeralSeq;
34
+ constructor(client: Client, tempMode?: TempModeProvider);
35
+ /**
36
+ * Build a transaction at the host's current temp-mode, send it, and return
37
+ * the engine's effect. Throws on an engine ErrorMessage so callers can use
38
+ * normal try/catch instead of inspecting a union.
39
+ */
40
+ private apply;
41
+ /** Write a value or formula into a cell. */
42
+ inputCell(sheetIdx: number, row: number, col: number, content: string, undoable?: boolean): Promise<ActionEffect>;
43
+ /** Write a value into a cell addressed within a block's coordinate space. */
44
+ inputBlockCell(sheetIdx: number, blockId: number, row: number, col: number, input: string, undoable?: boolean): Promise<ActionEffect>;
45
+ /**
46
+ * Write several block cells in one transaction. Used when an interaction
47
+ * changes a group of related cells at once (e.g. redistributing a percent
48
+ * allocation across a pool).
49
+ */
50
+ inputBlockCells(inputs: ReadonlyArray<{
51
+ sheetIdx: number;
52
+ blockId: number;
53
+ row: number;
54
+ col: number;
55
+ input: string;
56
+ }>, undoable?: boolean): Promise<ActionEffect>;
57
+ /**
58
+ * Set a cell's content to an image URL, optionally widening the column
59
+ * and/or heightening the row to fit — all in one transaction. The host
60
+ * computes the target `colWidth`/`rowHeight` (in engine units) and passes
61
+ * them only when an adjustment is actually needed.
62
+ */
63
+ setCellImage(sheetIdx: number, row: number, col: number, url: string, opts?: {
64
+ colWidth?: number;
65
+ rowHeight?: number;
66
+ }): Promise<ActionEffect>;
67
+ /** Create a new sheet named `name` at index `idx`. */
68
+ createSheet(name: string, idx: number): Promise<ActionEffect>;
69
+ /** Rename a sheet. */
70
+ renameSheet(oldName: string, newName: string): Promise<ActionEffect>;
71
+ /** Delete the sheet at index `idx`. */
72
+ deleteSheet(idx: number): Promise<ActionEffect>;
73
+ /** Set a sheet tab's color (ARGB string; empty clears it). */
74
+ setSheetColor(idx: number, color: string): Promise<ActionEffect>;
75
+ /** Insert `cnt` rows into a block, starting at block-row `start`. */
76
+ insertRowsInBlock(sheetIdx: number, blockId: number, start: number, cnt?: number): Promise<ActionEffect>;
77
+ /** Delete a block. */
78
+ removeBlock(sheetIdx: number, blockId: number): Promise<ActionEffect>;
79
+ /** Apply font styling (bold/italic/underline/strike/color/size). */
80
+ setFont(sheetIdx: number, data: SelectedData, update: FontStyle): Promise<void>;
81
+ /** Apply horizontal/vertical alignment. */
82
+ setAlignment(sheetIdx: number, data: SelectedData, alignment: Alignment): Promise<void>;
83
+ /** Toggle wrap-text. */
84
+ setWrapText(sheetIdx: number, data: SelectedData, wrapText: boolean): Promise<void>;
85
+ /** Apply a number format. */
86
+ setNumFmt(sheetIdx: number, data: SelectedData, numFmt: string): Promise<void>;
87
+ /** Apply a pattern fill (foreground/background color + pattern). */
88
+ setPatternFill(sheetIdx: number, data: SelectedData, opts: {
89
+ fgColor?: string;
90
+ bgColor?: string;
91
+ pattern?: StPatternType;
92
+ }): Promise<void>;
93
+ /** Apply borders to the selection per the batch directive. */
94
+ setBorder(sheetIdx: number, data: SelectedData, update: BorderBatchUpdate): Promise<void>;
95
+ /** Apply a generated payload list, skipping the round-trip when empty. */
96
+ private applyGenerated;
97
+ /**
98
+ * Create a form-backed block: the `createBlock` + `bindFormSchema` +
99
+ * per-field `upsertFieldRenderInfo` payloads, in one transaction.
100
+ *
101
+ * The host resolves each field first (type → FieldInfo, validation
102
+ * formulas, and the engine render id from its FieldManager), then hands
103
+ * the flattened list here. Field/formula composition and FieldManager
104
+ * registration stay in the host because they touch engine-side render
105
+ * state that isn't part of the Client seam.
106
+ */
107
+ createFormBlock(opts: {
108
+ sheetIdx: number;
109
+ blockId: number;
110
+ masterRow: number;
111
+ masterCol: number;
112
+ refName: string;
113
+ keyIdx: number;
114
+ fields: readonly FormBlockField[];
115
+ }): Promise<void>;
116
+ /**
117
+ * Apply a caller-built payload list as one transaction (at the host's
118
+ * temp-mode). Escape hatch for operations whose payload construction still
119
+ * lives in the host — e.g. the toolbar's format/border generators. Prefer
120
+ * a named method above when one exists; this exists so no caller has to
121
+ * reach past WorkbookOps to `client.handleTransaction` directly.
122
+ */
123
+ applyPayloads(payloads: readonly Payload[], undoable?: boolean): Promise<ActionEffect>;
124
+ /** Commit the workbook's temp (speculative) branch into the main branch. */
125
+ commitTempStatus(): Promise<void>;
126
+ /** Discard the workbook's temp (speculative) branch. */
127
+ cleanupTempStatus(): Promise<void>;
128
+ /**
129
+ * Establish (or refresh) the validation rule for a cell.
130
+ *
131
+ * A validation rule is an Excel formula (no leading `=`) that should
132
+ * evaluate to a boolean. We park it in the cell's *shadow* cell so the
133
+ * engine evaluates it reactively; the host reads the shadow value back and
134
+ * renders the result (see logisheets-core's `interpretValidation`).
135
+ *
136
+ * Lifted verbatim out of the browser's ValidationCell component so the
137
+ * Node runtime gets the same operation.
138
+ */
139
+ setValidationRule(sheetIdx: number, row: number, col: number, formula: string): Promise<void>;
140
+ /**
141
+ * Evaluate an Excel formula (no leading `=`) in a sheet and return its
142
+ * Value. Parks the formula in a throwaway ephemeral cell, reads the result
143
+ * back, and leaves no committed change — the same mechanism the browser
144
+ * uses for shadow cells, here for one-shot evaluation.
145
+ */
146
+ evalFormula(sheetIdx: number, formula: string): Promise<Value>;
147
+ /**
148
+ * Evaluate formula-based validation rules and return the violating cells.
149
+ * Headless batch check — the per-cell browser path uses setValidationRule +
150
+ * interpretValidation instead.
151
+ */
152
+ checkValidations(rules: readonly ValidationRule[]): Promise<Violation[]>;
153
+ /** Check required / unique / membership field constraints. */
154
+ checkFieldConstraints(columns: readonly FieldColumn[]): Promise<Violation[]>;
155
+ }
@@ -0,0 +1,335 @@
1
+ // WorkbookOps — the operation layer.
2
+ //
3
+ // This is the single home for high-level, engine-neutral workbook
4
+ // *operations* (input a cell, add a sheet, insert a block row, set a
5
+ // validation rule, ...). Each operation is an async method that does the full
6
+ // orchestration — resolve ids, build payloads, send the transaction, surface
7
+ // errors — on top of the injected {@link Client} seam (see ../port).
8
+ //
9
+ // Both hosts are thin shells over this layer:
10
+ // - browser app -> injects logisheets-engine's worker-backed client
11
+ // - node runtime -> injects an async client built over the Node WASM engine
12
+ //
13
+ // Operations are written async against the full Client. The browser client is
14
+ // already async; the Node runtime adapts its synchronous handle() into an
15
+ // async Client, so this one implementation runs unchanged on both.
16
+ //
17
+ // UI side effects (refocus, toasts, event buses, closing dialogs) stay in the
18
+ // host — only the engine-facing operation lives here.
19
+ import { makeTransaction } from '../transaction/index.js';
20
+ import { checkValidations as checkValidationsPure, } from '../validation/index.js';
21
+ import { checkFieldConstraints as checkFieldConstraintsPure, } from '../field/index.js';
22
+ import { generateFontPayload, generateAlgnmentPayload, generateWrapTextPayload, generateNumFmtPayload, generatePatternFillPayload, generateBorderPayloads, } from '../format/index.js';
23
+ function isErrorMessage(v) {
24
+ return (typeof v === 'object' &&
25
+ v !== null &&
26
+ 'msg' in v);
27
+ }
28
+ /**
29
+ * High-level workbook operations bound to one engine {@link Client}.
30
+ * Construct one per workbook and share it across the host.
31
+ */
32
+ export class WorkbookOps {
33
+ constructor(client, tempMode = () => false) {
34
+ this.client = client;
35
+ this.tempMode = tempMode;
36
+ /** Monotonic id for the throwaway ephemeral cells used by evalFormula. */
37
+ this.ephemeralSeq = 1;
38
+ }
39
+ /**
40
+ * Build a transaction at the host's current temp-mode, send it, and return
41
+ * the engine's effect. Throws on an engine ErrorMessage so callers can use
42
+ * normal try/catch instead of inspecting a union.
43
+ */
44
+ async apply(payloads, undoable) {
45
+ const transaction = makeTransaction(payloads, undoable, this.tempMode());
46
+ const res = await this.client.handleTransaction({ transaction });
47
+ if (isErrorMessage(res)) {
48
+ throw new Error('Transaction failed: ' + res.msg);
49
+ }
50
+ return res;
51
+ }
52
+ // ---- cell / block input --------------------------------------------
53
+ /** Write a value or formula into a cell. */
54
+ inputCell(sheetIdx, row, col, content, undoable = true) {
55
+ return this.apply([{ type: 'cellInput', value: { sheetIdx, row, col, content } }], undoable);
56
+ }
57
+ /** Write a value into a cell addressed within a block's coordinate space. */
58
+ inputBlockCell(sheetIdx, blockId, row, col, input, undoable = true) {
59
+ return this.apply([
60
+ {
61
+ type: 'blockInput',
62
+ value: { sheetIdx, blockId, row, col, input },
63
+ },
64
+ ], undoable);
65
+ }
66
+ /**
67
+ * Write several block cells in one transaction. Used when an interaction
68
+ * changes a group of related cells at once (e.g. redistributing a percent
69
+ * allocation across a pool).
70
+ */
71
+ inputBlockCells(inputs, undoable = true) {
72
+ return this.apply(inputs.map((i) => ({ type: 'blockInput', value: i })), undoable);
73
+ }
74
+ /**
75
+ * Set a cell's content to an image URL, optionally widening the column
76
+ * and/or heightening the row to fit — all in one transaction. The host
77
+ * computes the target `colWidth`/`rowHeight` (in engine units) and passes
78
+ * them only when an adjustment is actually needed.
79
+ */
80
+ setCellImage(sheetIdx, row, col, url, opts) {
81
+ const payloads = [
82
+ { type: 'cellInput', value: { sheetIdx, row, col, content: url } },
83
+ ];
84
+ if (opts?.colWidth !== undefined) {
85
+ payloads.push({
86
+ type: 'setColWidth',
87
+ value: { sheetIdx, col, width: opts.colWidth },
88
+ });
89
+ }
90
+ if (opts?.rowHeight !== undefined) {
91
+ payloads.push({
92
+ type: 'setRowHeight',
93
+ value: { sheetIdx, row, height: opts.rowHeight },
94
+ });
95
+ }
96
+ return this.apply(payloads, true);
97
+ }
98
+ // ---- sheets ---------------------------------------------------------
99
+ /** Create a new sheet named `name` at index `idx`. */
100
+ createSheet(name, idx) {
101
+ return this.apply([{ type: 'createSheet', value: { idx, newName: name } }], true);
102
+ }
103
+ /** Rename a sheet. */
104
+ renameSheet(oldName, newName) {
105
+ return this.apply([{ type: 'sheetRename', value: { oldName, newName } }], true);
106
+ }
107
+ /** Delete the sheet at index `idx`. */
108
+ deleteSheet(idx) {
109
+ return this.apply([{ type: 'deleteSheet', value: { idx } }], true);
110
+ }
111
+ /** Set a sheet tab's color (ARGB string; empty clears it). */
112
+ setSheetColor(idx, color) {
113
+ return this.apply([{ type: 'setSheetColor', value: { idx, color } }], true);
114
+ }
115
+ // ---- blocks ---------------------------------------------------------
116
+ /** Insert `cnt` rows into a block, starting at block-row `start`. */
117
+ insertRowsInBlock(sheetIdx, blockId, start, cnt = 1) {
118
+ return this.apply([
119
+ {
120
+ type: 'insertRowsInBlock',
121
+ value: { sheetIdx, blockId, start, cnt },
122
+ },
123
+ ], true);
124
+ }
125
+ /** Delete a block. */
126
+ removeBlock(sheetIdx, blockId) {
127
+ return this.apply([{ type: 'removeBlock', value: { sheetIdx, id: blockId } }], true);
128
+ }
129
+ // ---- formatting -----------------------------------------------------
130
+ //
131
+ // Each method turns the current sheet + selection into style-update
132
+ // payloads (logic in ../format) and applies them. The host supplies the
133
+ // sheet index (a view concern) and the selection.
134
+ /** Apply font styling (bold/italic/underline/strike/color/size). */
135
+ async setFont(sheetIdx, data, update) {
136
+ await this.applyGenerated(generateFontPayload(sheetIdx, data, update));
137
+ }
138
+ /** Apply horizontal/vertical alignment. */
139
+ async setAlignment(sheetIdx, data, alignment) {
140
+ await this.applyGenerated(generateAlgnmentPayload(sheetIdx, data, alignment));
141
+ }
142
+ /** Toggle wrap-text. */
143
+ async setWrapText(sheetIdx, data, wrapText) {
144
+ await this.applyGenerated(generateWrapTextPayload(sheetIdx, data, wrapText));
145
+ }
146
+ /** Apply a number format. */
147
+ async setNumFmt(sheetIdx, data, numFmt) {
148
+ await this.applyGenerated(generateNumFmtPayload(sheetIdx, data, numFmt));
149
+ }
150
+ /** Apply a pattern fill (foreground/background color + pattern). */
151
+ async setPatternFill(sheetIdx, data, opts) {
152
+ await this.applyGenerated(generatePatternFillPayload(sheetIdx, data, opts.fgColor, opts.bgColor, opts.pattern));
153
+ }
154
+ /** Apply borders to the selection per the batch directive. */
155
+ async setBorder(sheetIdx, data, update) {
156
+ await this.applyGenerated(generateBorderPayloads(sheetIdx, data, update));
157
+ }
158
+ /** Apply a generated payload list, skipping the round-trip when empty. */
159
+ async applyGenerated(payloads) {
160
+ if (payloads.length === 0)
161
+ return;
162
+ await this.apply(payloads, true);
163
+ }
164
+ // ---- structured blocks ---------------------------------------------
165
+ /**
166
+ * Create a form-backed block: the `createBlock` + `bindFormSchema` +
167
+ * per-field `upsertFieldRenderInfo` payloads, in one transaction.
168
+ *
169
+ * The host resolves each field first (type → FieldInfo, validation
170
+ * formulas, and the engine render id from its FieldManager), then hands
171
+ * the flattened list here. Field/formula composition and FieldManager
172
+ * registration stay in the host because they touch engine-side render
173
+ * state that isn't part of the Client seam.
174
+ */
175
+ async createFormBlock(opts) {
176
+ const { sheetIdx, blockId, masterRow, masterCol, refName, keyIdx, fields, } = opts;
177
+ const payloads = [
178
+ {
179
+ type: 'createBlock',
180
+ value: {
181
+ sheetIdx,
182
+ id: blockId,
183
+ masterRow,
184
+ masterCol,
185
+ rowCnt: 1,
186
+ colCnt: fields.length,
187
+ },
188
+ },
189
+ {
190
+ type: 'bindFormSchema',
191
+ value: {
192
+ refName,
193
+ sheetIdx,
194
+ blockId,
195
+ fieldFrom: 0,
196
+ row: true,
197
+ keyIdx: keyIdx < 0 ? 0 : keyIdx,
198
+ fields: fields.map((f) => f.name),
199
+ renderIds: fields.map((f) => f.renderId),
200
+ fieldFormulas: fields.map((f) => f.valueFormula ?? ''),
201
+ validationFormulas: [],
202
+ editabilityFormulas: [],
203
+ },
204
+ },
205
+ ...fields.map((f) => ({
206
+ type: 'upsertFieldRenderInfo',
207
+ value: {
208
+ renderId: f.renderId,
209
+ diyRender: f.diyRender,
210
+ styleUpdate: { setNumFmt: f.numFmt ?? '' },
211
+ },
212
+ })),
213
+ ];
214
+ await this.apply(payloads, true);
215
+ }
216
+ // ---- generic / temp-branch -----------------------------------------
217
+ /**
218
+ * Apply a caller-built payload list as one transaction (at the host's
219
+ * temp-mode). Escape hatch for operations whose payload construction still
220
+ * lives in the host — e.g. the toolbar's format/border generators. Prefer
221
+ * a named method above when one exists; this exists so no caller has to
222
+ * reach past WorkbookOps to `client.handleTransaction` directly.
223
+ */
224
+ applyPayloads(payloads, undoable = true) {
225
+ return this.apply(payloads, undoable);
226
+ }
227
+ /** Commit the workbook's temp (speculative) branch into the main branch. */
228
+ async commitTempStatus() {
229
+ const res = await this.client.commitTempStatus();
230
+ if (isErrorMessage(res)) {
231
+ throw new Error('Failed to commit temp status: ' + res.msg);
232
+ }
233
+ }
234
+ /** Discard the workbook's temp (speculative) branch. */
235
+ async cleanupTempStatus() {
236
+ const res = await this.client.cleanupTempStatus();
237
+ if (isErrorMessage(res)) {
238
+ throw new Error('Failed to clean up temp status: ' + res.msg);
239
+ }
240
+ }
241
+ // ---- validation -----------------------------------------------------
242
+ /**
243
+ * Establish (or refresh) the validation rule for a cell.
244
+ *
245
+ * A validation rule is an Excel formula (no leading `=`) that should
246
+ * evaluate to a boolean. We park it in the cell's *shadow* cell so the
247
+ * engine evaluates it reactively; the host reads the shadow value back and
248
+ * renders the result (see logisheets-core's `interpretValidation`).
249
+ *
250
+ * Lifted verbatim out of the browser's ValidationCell component so the
251
+ * Node runtime gets the same operation.
252
+ */
253
+ async setValidationRule(sheetIdx, row, col, formula) {
254
+ const shadow = await this.client.getShadowCellId({
255
+ sheetIdx,
256
+ rowIdx: row,
257
+ colIdx: col,
258
+ });
259
+ if (isErrorMessage(shadow)) {
260
+ throw new Error('Failed to get shadow cell id: ' + shadow.msg);
261
+ }
262
+ await this.apply([
263
+ {
264
+ type: 'ephemeralCellInput',
265
+ value: {
266
+ id: shadow.cellId.value,
267
+ sheetIdx,
268
+ content: `=${formula}`,
269
+ },
270
+ },
271
+ ], false);
272
+ }
273
+ /**
274
+ * Evaluate an Excel formula (no leading `=`) in a sheet and return its
275
+ * Value. Parks the formula in a throwaway ephemeral cell, reads the result
276
+ * back, and leaves no committed change — the same mechanism the browser
277
+ * uses for shadow cells, here for one-shot evaluation.
278
+ */
279
+ async evalFormula(sheetIdx, formula) {
280
+ const id = this.ephemeralSeq++;
281
+ await this.apply([
282
+ {
283
+ type: 'ephemeralCellInput',
284
+ value: { id, sheetIdx, content: '=' + formula },
285
+ },
286
+ ], false);
287
+ const sheetId = await this.client.getSheetId({ sheetIdx });
288
+ if (isErrorMessage(sheetId)) {
289
+ throw new Error('Failed to resolve sheet id: ' + sheetId.msg);
290
+ }
291
+ const infos = await this.client.batchGetCellInfoById({
292
+ ids: [{ sheetId, cellId: { type: 'ephemeralCell', value: id } }],
293
+ });
294
+ if (isErrorMessage(infos)) {
295
+ throw new Error('Failed to read ephemeral cell: ' + infos.msg);
296
+ }
297
+ return infos[0].value;
298
+ }
299
+ /**
300
+ * Evaluate formula-based validation rules and return the violating cells.
301
+ * Headless batch check — the per-cell browser path uses setValidationRule +
302
+ * interpretValidation instead.
303
+ */
304
+ async checkValidations(rules) {
305
+ const values = new Map();
306
+ for (const rule of rules) {
307
+ values.set(rule.formula, await this.evalFormula(rule.sheetIdx, rule.formula));
308
+ }
309
+ return checkValidationsPure(rules, (_sheetIdx, formula) => values.get(formula));
310
+ }
311
+ /** Check required / unique / membership field constraints. */
312
+ async checkFieldConstraints(columns) {
313
+ const values = new Map();
314
+ for (const { cells } of columns) {
315
+ for (const c of cells) {
316
+ const key = `${c.sheetIdx}:${c.row}:${c.col}`;
317
+ if (values.has(key))
318
+ continue;
319
+ const v = await this.client.getValue({
320
+ sheetIdx: c.sheetIdx,
321
+ row: c.row,
322
+ col: c.col,
323
+ });
324
+ if (isErrorMessage(v)) {
325
+ throw new Error('Failed to read cell value: ' + v.msg);
326
+ }
327
+ values.set(key, v);
328
+ }
329
+ }
330
+ return checkFieldConstraintsPure(columns, (sheetIdx, row, col) => {
331
+ const v = values.get(`${sheetIdx}:${row}:${col}`);
332
+ return (v ?? 'empty');
333
+ });
334
+ }
335
+ }
@@ -0,0 +1,32 @@
1
+ declare class CallerRegistry {
2
+ private _entries;
3
+ private _blockOwners;
4
+ private _fieldPositions;
5
+ getUserUuid(): string;
6
+ getCraftUuid(craftId: string): string;
7
+ isUser(uuid: string | undefined): boolean;
8
+ registerBlockOwner(sheetIdx: number, blockId: number, callerUuid: string): void;
9
+ getBlockOwner(sheetIdx: number, blockId: number): string | undefined;
10
+ /**
11
+ * Register the field at a specific block-relative position.
12
+ * `axis = 'col'` — column-oriented form (one field per column);
13
+ * `axis = 'row'` — row-oriented form (one field per row).
14
+ */
15
+ registerFieldPosition(sheetIdx: number, blockId: number, axis: 'col' | 'row', offset: number, renderId: string): void;
16
+ getFieldRenderId(sheetIdx: number, blockId: number, blockRow: number, blockCol: number): string | undefined;
17
+ private _getOrAssign;
18
+ }
19
+ export declare const callerRegistry: CallerRegistry;
20
+ /** Minimal shape needed to decide static editability — a subset of the
21
+ * engine's FieldInfo. `userEditable` may be a boolean or a formula string. */
22
+ export interface FieldEditableInfo {
23
+ userEditable?: boolean | string;
24
+ }
25
+ /**
26
+ * Whether a field permits user edits based on its static declaration alone
27
+ * (ignoring any dynamic formula). `userEditable === false` blocks; `true`,
28
+ * `undefined`, or a formula string permits (the formula is enforced
29
+ * downstream via a shadow value).
30
+ */
31
+ export declare function isFieldUserEditable(fi: FieldEditableInfo | undefined): boolean;
32
+ export {};
@@ -0,0 +1,74 @@
1
+ // Permissions — caller identity registry + the pure editability predicate.
2
+ //
3
+ // The CallerRegistry is plain in-process state (no engine, no UI): it maps
4
+ // caller identities to uuids, blocks to owners, and block-relative field
5
+ // positions to renderIds. It's the shared foundation the App's permission glue
6
+ // (patch.ts, field-editable.ts) builds on — moving it here makes it usable from
7
+ // the Node runtime too.
8
+ //
9
+ // The engine-integration parts (monkey-patching the WorkbookClient, toast
10
+ // feedback, resolving FieldInfo via the live engine) stay in the App: they are
11
+ // UI/runtime glue, not portable logic.
12
+ import { simpleUuid } from '../utils/index.js';
13
+ const USER_KEY = '__user__';
14
+ class CallerRegistry {
15
+ constructor() {
16
+ this._entries = new Map();
17
+ this._blockOwners = new Map();
18
+ // (sheetIdx, blockId, block-relative col) → field renderId. Populated
19
+ // when patch.ts observes a bindFormSchema payload. Lets the cellInput
20
+ // validator look up the FieldInfo for any block cell to enforce
21
+ // FieldInfo.userEditable.
22
+ this._fieldPositions = new Map();
23
+ }
24
+ getUserUuid() {
25
+ return this._getOrAssign(USER_KEY);
26
+ }
27
+ getCraftUuid(craftId) {
28
+ if (craftId === USER_KEY) {
29
+ throw new Error(`invalid craftId: ${craftId}`);
30
+ }
31
+ return this._getOrAssign(craftId);
32
+ }
33
+ isUser(uuid) {
34
+ return uuid === this._entries.get(USER_KEY);
35
+ }
36
+ registerBlockOwner(sheetIdx, blockId, callerUuid) {
37
+ this._blockOwners.set(`${sheetIdx}-${blockId}`, callerUuid);
38
+ }
39
+ getBlockOwner(sheetIdx, blockId) {
40
+ return this._blockOwners.get(`${sheetIdx}-${blockId}`);
41
+ }
42
+ /**
43
+ * Register the field at a specific block-relative position.
44
+ * `axis = 'col'` — column-oriented form (one field per column);
45
+ * `axis = 'row'` — row-oriented form (one field per row).
46
+ */
47
+ registerFieldPosition(sheetIdx, blockId, axis, offset, renderId) {
48
+ this._fieldPositions.set(`${sheetIdx}-${blockId}-${axis}-${offset}`, renderId);
49
+ }
50
+ getFieldRenderId(sheetIdx, blockId, blockRow, blockCol) {
51
+ // Try column-oriented form first, then row-oriented. A block
52
+ // shouldn't be bound to both — first hit wins.
53
+ return (this._fieldPositions.get(`${sheetIdx}-${blockId}-col-${blockCol}`) ??
54
+ this._fieldPositions.get(`${sheetIdx}-${blockId}-row-${blockRow}`));
55
+ }
56
+ _getOrAssign(key) {
57
+ const existing = this._entries.get(key);
58
+ if (existing)
59
+ return existing;
60
+ const uuid = simpleUuid();
61
+ this._entries.set(key, uuid);
62
+ return uuid;
63
+ }
64
+ }
65
+ export const callerRegistry = new CallerRegistry();
66
+ /**
67
+ * Whether a field permits user edits based on its static declaration alone
68
+ * (ignoring any dynamic formula). `userEditable === false` blocks; `true`,
69
+ * `undefined`, or a formula string permits (the formula is enforced
70
+ * downstream via a shadow value).
71
+ */
72
+ export function isFieldUserEditable(fi) {
73
+ return fi?.userEditable !== false;
74
+ }
package/dist/port.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ import type { Client } from 'logisheets-web';
2
+ export type { Client };
3
+ /**
4
+ * Anything in logisheets-core that needs to reach the engine takes one of
5
+ * these instead of importing a concrete client. Tests pass a mock.
6
+ */
7
+ export interface HasClient {
8
+ readonly client: Client;
9
+ }
package/dist/port.js ADDED
@@ -0,0 +1,14 @@
1
+ // The dependency seam.
2
+ //
3
+ // logisheets-core is pure logic and must not bundle a backend. It talks to
4
+ // the engine ONLY through the `Client` interface, which it imports as a TYPE
5
+ // from logisheets-web. `import type` is erased at compile time, so this file
6
+ // produces NO runtime dependency on logisheets-web (or on logisheets-node).
7
+ //
8
+ // The concrete Client is INJECTED by the host:
9
+ // - browser app -> logisheets-engine's worker-backed WorkbookClient
10
+ // - node runtime -> logisheets' synchronous handle()-based client
11
+ //
12
+ // Both implement the same `Client` interface (node's source is a copy of
13
+ // web's), so logisheets-core never needs to know which one it got.
14
+ export {};
@@ -0,0 +1 @@
1
+ export * from './model.js';
@@ -0,0 +1 @@
1
+ export * from './model.js';
@@ -0,0 +1,10 @@
1
+ export declare class SelectionModel<T> {
2
+ #private;
3
+ readonly multi: boolean;
4
+ readonly data: readonly T[];
5
+ constructor(multi?: boolean, data?: readonly T[]);
6
+ get selected(): readonly T[];
7
+ toggle(data: T): void;
8
+ select(data: T): void;
9
+ deSelect(data: T): void;
10
+ }
@@ -0,0 +1,38 @@
1
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
2
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
3
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
4
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
5
+ };
6
+ var _SelectionModel_selected;
7
+ export class SelectionModel {
8
+ constructor(multi = false, data = []) {
9
+ this.multi = multi;
10
+ this.data = data;
11
+ _SelectionModel_selected.set(this, new Map());
12
+ data.forEach((d) => {
13
+ __classPrivateFieldGet(this, _SelectionModel_selected, "f").set(d, true);
14
+ });
15
+ }
16
+ get selected() {
17
+ const selecteds = [];
18
+ __classPrivateFieldGet(this, _SelectionModel_selected, "f").forEach((checked, d) => {
19
+ if (!checked)
20
+ return;
21
+ selecteds.push(d);
22
+ });
23
+ return selecteds;
24
+ }
25
+ toggle(data) {
26
+ if (__classPrivateFieldGet(this, _SelectionModel_selected, "f").has(data))
27
+ __classPrivateFieldGet(this, _SelectionModel_selected, "f").set(data, true);
28
+ else
29
+ __classPrivateFieldGet(this, _SelectionModel_selected, "f").set(data, false);
30
+ }
31
+ select(data) {
32
+ __classPrivateFieldGet(this, _SelectionModel_selected, "f").set(data, true);
33
+ }
34
+ deSelect(data) {
35
+ __classPrivateFieldGet(this, _SelectionModel_selected, "f").set(data, false);
36
+ }
37
+ }
38
+ _SelectionModel_selected = new WeakMap();