logisheets-core 1.15.0 → 1.16.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/README.md CHANGED
@@ -35,8 +35,8 @@ declare const client: Client // supplied by the host (browser worker or Node)
35
35
  const ops = new WorkbookOps(client)
36
36
 
37
37
  await ops.inputCell(0, 0, 0, 'Hello') // sheet 0, cell A1
38
- await ops.createSheet(1, 'Summary')
39
- await ops.setSheetColor(0, '#4472C4')
38
+ await ops.createSheet('Summary', 1)
39
+ await ops.setSheetColor(0, 'FF4472C4') // ARGB hex, no '#'
40
40
  ```
41
41
 
42
42
  `WorkbookOps` covers cell input (including block cells and cell images), sheet
@@ -60,7 +60,8 @@ export function dispatchCraftCanvasEvent(evt) {
60
60
  return { handled: false };
61
61
  try {
62
62
  const r = handler(evt);
63
- const handled = r === true || (typeof r === 'object' && r !== null && r.handled === true);
63
+ const handled = r === true ||
64
+ (typeof r === 'object' && r !== null && r.handled === true);
64
65
  return { handled };
65
66
  }
66
67
  catch (e) {
@@ -1,10 +1,13 @@
1
+ /** Where a craft's code lives. Resolved by a host's `CraftRegistry`. */
1
2
  export interface CraftManifest {
2
3
  /**
3
- * The path of the runtime JS file
4
+ * Locator of the headless runtime JS, interpreted by the registry (a URL
5
+ * for logisheets-runtime's HttpCraftRegistry). Empty means the craft has
6
+ * no runtime and a headless host skips it.
4
7
  */
5
8
  readonly rtJs: string;
6
9
  /**
7
- * The path of the html url which is used in web
10
+ * URL of the craft's browser UI page (loaded in the craft panel iframe).
8
11
  */
9
12
  readonly html: string;
10
13
  }
@@ -31,7 +31,16 @@ export type MaybePromise<T> = T | Promise<T>;
31
31
  * onResponse the response has been produced and is about to be returned
32
32
  */
33
33
  export interface CraftRuntime<S extends CraftState = CraftState, W = unknown> {
34
+ /**
35
+ * Rehydrate from the state the craft saved. Returning an `ErrorMessage`
36
+ * makes the host skip this craft. `s` is the parsed state object; the
37
+ * host passes the SAME object to every later hook.
38
+ */
34
39
  onLoad: (s: S, wb: W) => MaybePromise<Result<void>>;
40
+ /**
41
+ * Write the request's inputs into the workbook. Return an `ErrorMessage`
42
+ * (or throw) to object; the host then rejects the request.
43
+ */
35
44
  onRequest: (req: JsonRpcRequest, s: S, wb: W) => MaybePromise<Result<void>>;
36
45
  /**
37
46
  * Called once an RPC request's inputs have been written into the workbook
@@ -42,5 +51,10 @@ export interface CraftRuntime<S extends CraftState = CraftState, W = unknown> {
42
51
  * input. Optional: a craft with no validation needs simply omits it.
43
52
  */
44
53
  onValidate?: (s: S, wb: W) => MaybePromise<Result<readonly Violation[]>>;
54
+ /**
55
+ * Fill `resp` (typically `resp.result`) from the workbook. It is mutated
56
+ * in place: the host returns the same object. Return an `ErrorMessage`
57
+ * (or throw) to turn the reply into an error.
58
+ */
45
59
  onResponse: (resp: JsonRpcResponse, s: S, wb: W) => MaybePromise<Result<void>>;
46
60
  }
@@ -1,5 +1,17 @@
1
+ /** Push a craft's serialized state (replaces the previous one). Called from
2
+ * the iframe-injected `setCraftState`; `craftId` is captured per-iframe by
3
+ * the host. An empty `craftId` is ignored. Not written to the engine until
4
+ * the host next saves the file. */
1
5
  export declare function setCraftState(craftId: string, json: string): void;
6
+ /** A craft's last-known state (e.g. the one rehydrated from the loaded
7
+ * workbook), or undefined if the craft never stored anything. */
2
8
  export declare function getCraftState(craftId: string): string | undefined;
9
+ /** Forget a craft's state; the next save omits it. */
3
10
  export declare function clearCraftState(craftId: string): void;
11
+ /** Snapshot every craft's state, for the host to fold into the AppData
12
+ * envelope's `craftStates` on save. */
4
13
  export declare function getPersistentCraftStates(): Record<string, string>;
14
+ /** Rehydrate from a previously-persisted snapshot. Replaces the current
15
+ * contents, so a freshly-loaded workbook doesn't inherit state from a prior
16
+ * one; non-string entries are dropped. Pass `undefined` to reset. */
5
17
  export declare function loadPersistentCraftStates(data: unknown): void;
@@ -12,31 +12,35 @@
12
12
  // undo/redo Status (it rides AppData, a side channel on the workbook), so
13
13
  // writing it never pollutes edit history.
14
14
  const craftStates = new Map();
15
- // Push a craft's serialized state. Called from the iframe-injected
16
- // `setCraftState`; `craftId` is captured per-iframe by the host.
15
+ /** Push a craft's serialized state (replaces the previous one). Called from
16
+ * the iframe-injected `setCraftState`; `craftId` is captured per-iframe by
17
+ * the host. An empty `craftId` is ignored. Not written to the engine until
18
+ * the host next saves the file. */
17
19
  export function setCraftState(craftId, json) {
18
20
  if (!craftId)
19
21
  return;
20
22
  craftStates.set(craftId, json);
21
23
  }
22
- // Read a craft's last-known state (e.g. the one rehydrated from the loaded
23
- // workbook). Returns undefined if the craft never stored anything.
24
+ /** A craft's last-known state (e.g. the one rehydrated from the loaded
25
+ * workbook), or undefined if the craft never stored anything. */
24
26
  export function getCraftState(craftId) {
25
27
  return craftStates.get(craftId);
26
28
  }
29
+ /** Forget a craft's state; the next save omits it. */
27
30
  export function clearCraftState(craftId) {
28
31
  craftStates.delete(craftId);
29
32
  }
30
- // Snapshot every craft's state for persistence into the AppData envelope.
33
+ /** Snapshot every craft's state, for the host to fold into the AppData
34
+ * envelope's `craftStates` on save. */
31
35
  export function getPersistentCraftStates() {
32
36
  const out = {};
33
37
  for (const [craftId, json] of craftStates)
34
38
  out[craftId] = json;
35
39
  return out;
36
40
  }
37
- // Rehydrate the store from a previously-persisted snapshot. Replaces the
38
- // current contents so a freshly-loaded workbook doesn't inherit stale state
39
- // from a prior one.
41
+ /** Rehydrate from a previously-persisted snapshot. Replaces the current
42
+ * contents, so a freshly-loaded workbook doesn't inherit state from a prior
43
+ * one; non-string entries are dropped. Pass `undefined` to reset. */
40
44
  export function loadPersistentCraftStates(data) {
41
45
  craftStates.clear();
42
46
  if (!data || typeof data !== 'object')
@@ -7,11 +7,16 @@ export interface RadioBinding {
7
7
  col: number;
8
8
  value: string;
9
9
  }
10
+ /** Listen for ANY change to the interaction store (every widget kind, not
11
+ * just radios). Returns an unsubscribe function. */
10
12
  export declare function subscribeRadioBindings(listener: () => void): () => void;
11
13
  export declare function registerRadioBinding(binding: RadioBinding): void;
12
14
  export declare function unregisterRadioBinding(binding: Pick<RadioBinding, 'groupId' | 'blockId' | 'row' | 'col'>): void;
15
+ /** Remove a group's radio bindings and selection, or every group's when
16
+ * `groupId` is omitted. */
13
17
  export declare function clearRadioBindings(groupId?: string): void;
14
18
  export declare function getRadioBindings(): readonly RadioBinding[];
19
+ /** The `value` of the group's selected binding, if any. */
15
20
  export declare function getRadioSelection(groupId: string): string | undefined;
16
21
  export declare function setRadioSelection(groupId: string, value: string): void;
17
22
  export interface MultiSelectBinding {
@@ -27,9 +32,13 @@ export declare function registerMultiSelectBinding(binding: MultiSelectBinding):
27
32
  export declare function unregisterMultiSelectBinding(binding: Pick<MultiSelectBinding, 'groupId' | 'blockId' | 'row' | 'col'>): void;
28
33
  export declare function clearMultiSelectBindings(groupId?: string): void;
29
34
  export declare function getMultiSelectBindings(): readonly MultiSelectBinding[];
35
+ /** Cap a group's selections (negative clamps to 0). Until set, a group has
36
+ * no cap when toggling, though {@link getMultiSelectMax} reports 0. */
30
37
  export declare function setMultiSelectMax(groupId: string, max: number): void;
31
38
  export declare function getMultiSelectMax(groupId: string): number;
32
39
  export declare function getMultiSelectSelections(groupId: string): string[];
40
+ /** Select `value` if unselected and under the cap, deselect it if
41
+ * selected. A select past the cap is silently ignored. */
33
42
  export declare function toggleMultiSelectValue(groupId: string, value: string): void;
34
43
  export interface PointAllocatorBinding {
35
44
  type: 'pointAllocator';
@@ -49,6 +58,8 @@ export declare function registerPointAllocator(binding: PointAllocatorBinding):
49
58
  export declare function unregisterPointAllocator(binding: Pick<PointAllocatorBinding, 'groupId' | 'blockId' | 'row' | 'col'>): void;
50
59
  export declare function clearPointAllocators(groupId?: string): void;
51
60
  export declare function getPointAllocatorBindings(): readonly PointAllocatorBinding[];
61
+ /** Set a group's point budget (negative clamps to 0). Shrinking it does not
62
+ * take back points already allocated, so `remaining` can go negative. */
52
63
  export declare function setPointPool(groupId: string, total: number): void;
53
64
  export declare function getPointPool(groupId: string): {
54
65
  total: number;
@@ -73,6 +84,8 @@ export declare function registerNumberSlider(binding: NumberSliderBinding): void
73
84
  export declare function unregisterNumberSlider(binding: Pick<NumberSliderBinding, 'groupId' | 'blockId' | 'row' | 'col'>): void;
74
85
  export declare function clearNumberSliders(groupId?: string): void;
75
86
  export declare function getNumberSliderBindings(): readonly NumberSliderBinding[];
87
+ /** Add `delta` points to one cell (negative removes), clamped so the group
88
+ * never exceeds its pool and a cell never goes below 0. */
76
89
  export declare function adjustPointAllocation(binding: Pick<PointAllocatorBinding, 'groupId' | 'blockId' | 'row' | 'col'>, delta: number): void;
77
90
  export interface PercentAllocatorBinding {
78
91
  type: 'percentAllocator';
@@ -135,6 +148,10 @@ interface PersistedShape {
135
148
  numberSliderBindings: NumberSliderBinding[];
136
149
  percentBindings: PercentAllocatorBinding[];
137
150
  }
151
+ /** Snapshot every binding and user selection, for the host to fold into the
152
+ * AppData envelope's `craftInteractions` on save. */
138
153
  export declare function getPersistentInteractions(): PersistedShape;
154
+ /** Replace the whole store from a snapshot (as written by
155
+ * {@link getPersistentInteractions}); malformed entries are dropped. */
139
156
  export declare function loadPersistentInteractions(data: unknown): void;
140
157
  export {};
@@ -1,3 +1,23 @@
1
+ // Craft interactions — host-drawn widgets bound to block cells.
2
+ //
3
+ // A craft registers bindings (radio, multi-select, point allocator, number
4
+ // slider, percent allocator); the host draws an overlay on each bound cell and
5
+ // records what the user picks. The browser injects these functions onto the
6
+ // craft iframe (`window.registerRadio`, `window.getRadioSelection`, ...; see
7
+ // the app's craft-interaction layer), and the craft reads results back on
8
+ // demand. Nothing here writes to the workbook.
9
+ //
10
+ // Conventions for every binding:
11
+ // - `row` / `col` are BLOCK-relative, counted from the block's master cell,
12
+ // so a binding follows its block when the block moves.
13
+ // - A binding is keyed by (groupId, blockId, row, col); registering the same
14
+ // key again replaces it. `sheetIdx` is NOT part of the key, so the same
15
+ // key on two sheets collides.
16
+ // - State is one module-level store per process; subscribers
17
+ // ({@link subscribeRadioBindings}) hear about every change of every kind.
18
+ // - Radio / multi-select / point selections live HERE, not in cells, and
19
+ // persist through {@link getPersistentInteractions} on save. Slider and
20
+ // percent values live in the cells themselves.
1
21
  const radioBindings = new Map();
2
22
  const groupSelections = new Map();
3
23
  const storeListeners = new Set();
@@ -8,6 +28,8 @@ function notifyStore() {
8
28
  for (const l of storeListeners)
9
29
  l();
10
30
  }
31
+ /** Listen for ANY change to the interaction store (every widget kind, not
32
+ * just radios). Returns an unsubscribe function. */
11
33
  export function subscribeRadioBindings(listener) {
12
34
  storeListeners.add(listener);
13
35
  return () => {
@@ -22,6 +44,8 @@ export function unregisterRadioBinding(binding) {
22
44
  radioBindings.delete(bindingKey(binding));
23
45
  notifyStore();
24
46
  }
47
+ /** Remove a group's radio bindings and selection, or every group's when
48
+ * `groupId` is omitted. */
25
49
  export function clearRadioBindings(groupId) {
26
50
  if (groupId === undefined) {
27
51
  radioBindings.clear();
@@ -39,6 +63,7 @@ export function clearRadioBindings(groupId) {
39
63
  export function getRadioBindings() {
40
64
  return Array.from(radioBindings.values());
41
65
  }
66
+ /** The `value` of the group's selected binding, if any. */
42
67
  export function getRadioSelection(groupId) {
43
68
  return groupSelections.get(groupId);
44
69
  }
@@ -79,6 +104,8 @@ export function clearMultiSelectBindings(groupId) {
79
104
  export function getMultiSelectBindings() {
80
105
  return Array.from(multiSelectBindings.values());
81
106
  }
107
+ /** Cap a group's selections (negative clamps to 0). Until set, a group has
108
+ * no cap when toggling, though {@link getMultiSelectMax} reports 0. */
82
109
  export function setMultiSelectMax(groupId, max) {
83
110
  multiSelectMax.set(groupId, Math.max(0, max));
84
111
  notifyStore();
@@ -89,6 +116,8 @@ export function getMultiSelectMax(groupId) {
89
116
  export function getMultiSelectSelections(groupId) {
90
117
  return Array.from(multiSelectSelections.get(groupId) ?? []);
91
118
  }
119
+ /** Select `value` if unselected and under the cap, deselect it if
120
+ * selected. A select past the cap is silently ignored. */
92
121
  export function toggleMultiSelectValue(groupId, value) {
93
122
  let set = multiSelectSelections.get(groupId);
94
123
  if (!set) {
@@ -151,6 +180,8 @@ export function clearPointAllocators(groupId) {
151
180
  export function getPointAllocatorBindings() {
152
181
  return Array.from(pointBindings.values());
153
182
  }
183
+ /** Set a group's point budget (negative clamps to 0). Shrinking it does not
184
+ * take back points already allocated, so `remaining` can go negative. */
154
185
  export function setPointPool(groupId, total) {
155
186
  pointPoolTotals.set(groupId, Math.max(0, total));
156
187
  notifyStore();
@@ -203,6 +234,8 @@ export function clearNumberSliders(groupId) {
203
234
  export function getNumberSliderBindings() {
204
235
  return Array.from(numberSliderBindings.values());
205
236
  }
237
+ /** Add `delta` points to one cell (negative removes), clamped so the group
238
+ * never exceeds its pool and a cell never goes below 0. */
206
239
  export function adjustPointAllocation(binding, delta) {
207
240
  const { groupId, blockId, row, col } = binding;
208
241
  let m = pointAllocations.get(groupId);
@@ -328,6 +361,8 @@ export function redistributePercent(current, targetIdx, deltaPct) {
328
361
  }
329
362
  return next;
330
363
  }
364
+ /** Snapshot every binding and user selection, for the host to fold into the
365
+ * AppData envelope's `craftInteractions` on save. */
331
366
  export function getPersistentInteractions() {
332
367
  const out = {
333
368
  radioBindings: Array.from(radioBindings.values()),
@@ -361,6 +396,8 @@ export function getPersistentInteractions() {
361
396
  }
362
397
  return out;
363
398
  }
399
+ /** Replace the whole store from a snapshot (as written by
400
+ * {@link getPersistentInteractions}); malformed entries are dropped. */
364
401
  export function loadPersistentInteractions(data) {
365
402
  // Always clear first, then repopulate from whatever the workbook carried.
366
403
  // Passing undefined/null (a workbook with no interaction state) therefore
@@ -1,23 +1,58 @@
1
1
  import type { Alignment, Payload, StPatternType, StBorderStyle, SelectedData } from 'logisheets-web';
2
+ /** A partial font change: only the fields present are applied. */
2
3
  export interface FontStyle {
3
4
  bold?: boolean;
5
+ /** `true` sets a single underline, `false` removes it. */
4
6
  underline?: boolean;
5
7
  italic?: boolean;
8
+ /**
9
+ * 8-hex-digit ARGB with NO `#`, e.g. `'FF1976D2'`. Passed to the engine
10
+ * unchanged: a `#RRGGBB` or 6-digit value yields no color and silently
11
+ * does not render. `''` is ignored rather than clearing the color.
12
+ */
6
13
  color?: string;
14
+ /** Point size. `0` is ignored. */
7
15
  size?: number;
8
16
  strike?: boolean;
9
17
  /** Font family name, e.g. "Arial", "Times New Roman", "Microsoft YaHei". */
10
18
  name?: string;
11
19
  }
20
+ /**
21
+ * Font payloads for the selection: one `cellStyleUpdate` per cell of a cell
22
+ * range, or one `lineStyleUpdate` for a row/column selection. `[]` when
23
+ * nothing is selected.
24
+ */
12
25
  export declare function generateFontPayload(sheetIdx: number, data: SelectedData, update: FontStyle): readonly Payload[];
26
+ /** Alignment payloads for the selection (same shape rules as
27
+ * {@link generateFontPayload}). */
13
28
  export declare function generateAlgnmentPayload(sheetIdx: number, data: SelectedData, alignment: Alignment): readonly Payload[];
14
29
  export declare function generateWrapTextPayload(sheetIdx: number, data: SelectedData, wrapText: boolean): readonly Payload[];
15
30
  export declare function generateNumFmtPayload(sheetIdx: number, data: SelectedData, numFmt: string): readonly Payload[];
31
+ /**
32
+ * Pattern-fill payloads for the selection. Unlike font and border colors,
33
+ * these take hex strings (`'#RRGGBB'`, `'RRGGBB'` or `'AARRGGBB'`, alpha
34
+ * dropped) and convert them to the engine's `{red, green, blue}` 0-255
35
+ * objects. Omitted / empty colors leave that part of the fill unchanged.
36
+ */
16
37
  export declare function generatePatternFillPayload(sheetIdx: number, data: SelectedData, fgColor?: string, bgColor?: string, pattern?: StPatternType): readonly Payload[];
17
38
  export type BatchUpdateType = 'all' | 'top' | 'bottom' | 'left' | 'right' | 'horizontal' | 'vertical' | 'outer' | 'inner' | 'clear';
39
+ /** Which edges of the selection to set, and how. */
18
40
  export interface BorderBatchUpdate {
41
+ /** Which edges: one side, all/outer/inner, or the inner horizontal or
42
+ * vertical lines. `clear` currently generates no payloads at all. */
19
43
  batch: BatchUpdateType;
44
+ /** 8-hex-digit ARGB, e.g. `'FF000000'`. A leading `#` is stripped and
45
+ * the value upper-cased, but a 6-digit value is NOT expanded and will
46
+ * not render. */
20
47
  color?: string;
21
48
  borderType?: StBorderStyle;
22
49
  }
50
+ /**
51
+ * Border payloads for the selection. Setting an edge also clears the facing
52
+ * edge of the neighbouring cell (its bottom for a top edge, and so on), so
53
+ * two conflicting borders never share one grid line.
54
+ *
55
+ * Mutates `update.color` in place (strips `#`, upper-cases); pass a copy if
56
+ * the caller keeps the object.
57
+ */
23
58
  export declare function generateBorderPayloads(sheetIdx: number, data: SelectedData, update: BorderBatchUpdate): readonly Payload[];
@@ -18,6 +18,9 @@ function selectedCellRange(v) {
18
18
  function selectedLines(v) {
19
19
  return v.data?.ty === 'line' ? v.data.d : undefined;
20
20
  }
21
+ // Fills are the one place the engine takes an `{red, green, blue}` object
22
+ // rather than an ARGB string, so the fill generator converts here. Accepts
23
+ // `#RRGGBB`, `RRGGBB` or 8-digit ARGB (alpha dropped).
21
24
  function hexToColor(hex) {
22
25
  let h = hex.startsWith('#') ? hex.slice(1) : hex;
23
26
  // ARGB format (8 chars): skip the first 2 alpha chars
@@ -35,6 +38,11 @@ function cellStyle(sheetIdx, row, col, ty) {
35
38
  function lineStyle(sheetIdx, from, to, row, ty) {
36
39
  return { type: 'lineStyleUpdate', value: { sheetIdx, from, to, row, ty } };
37
40
  }
41
+ /**
42
+ * Font payloads for the selection: one `cellStyleUpdate` per cell of a cell
43
+ * range, or one `lineStyleUpdate` for a row/column selection. `[]` when
44
+ * nothing is selected.
45
+ */
38
46
  export function generateFontPayload(sheetIdx, data, update) {
39
47
  if (!data.data)
40
48
  return [];
@@ -79,6 +87,8 @@ export function generateFontPayload(sheetIdx, data, update) {
79
87
  lineTy.setFontName = update.name;
80
88
  return [lineStyle(sheetIdx, d.start, d.end, d.type === 'row', lineTy)];
81
89
  }
90
+ /** Alignment payloads for the selection (same shape rules as
91
+ * {@link generateFontPayload}). */
82
92
  export function generateAlgnmentPayload(sheetIdx, data, alignment) {
83
93
  return generateForSelection(sheetIdx, data, { setAlignment: alignment });
84
94
  }
@@ -88,6 +98,12 @@ export function generateWrapTextPayload(sheetIdx, data, wrapText) {
88
98
  export function generateNumFmtPayload(sheetIdx, data, numFmt) {
89
99
  return generateForSelection(sheetIdx, data, { setNumFmt: numFmt });
90
100
  }
101
+ /**
102
+ * Pattern-fill payloads for the selection. Unlike font and border colors,
103
+ * these take hex strings (`'#RRGGBB'`, `'RRGGBB'` or `'AARRGGBB'`, alpha
104
+ * dropped) and convert them to the engine's `{red, green, blue}` 0-255
105
+ * objects. Omitted / empty colors leave that part of the fill unchanged.
106
+ */
91
107
  export function generatePatternFillPayload(sheetIdx, data, fgColor, bgColor, pattern) {
92
108
  const fill = {};
93
109
  if (fgColor)
@@ -113,6 +129,14 @@ function generateForSelection(sheetIdx, data, ty) {
113
129
  const d = data.data.d;
114
130
  return [lineStyle(sheetIdx, d.start, d.end, d.type === 'row', ty)];
115
131
  }
132
+ /**
133
+ * Border payloads for the selection. Setting an edge also clears the facing
134
+ * edge of the neighbouring cell (its bottom for a top edge, and so on), so
135
+ * two conflicting borders never share one grid line.
136
+ *
137
+ * Mutates `update.color` in place (strips `#`, upper-cases); pass a copy if
138
+ * the caller keeps the object.
139
+ */
116
140
  export function generateBorderPayloads(sheetIdx, data, update) {
117
141
  if (update.color) {
118
142
  if (update.color.startsWith('#'))
package/dist/index.js CHANGED
@@ -2,7 +2,25 @@
2
2
  //
3
3
  // Runs unchanged in the browser app and in a Node runtime. It depends on
4
4
  // logisheets-web for TYPES only (see ./port); the concrete engine Client is
5
- // injected by the host.
5
+ // injected by the host: logisheets-engine's worker client in the browser,
6
+ // logisheets-runtime's `handle()` proxy on Node. ESM only.
7
+ //
8
+ // Main exports:
9
+ // - WorkbookOps (./ops): the high-level operation layer. Every engine-facing
10
+ // operation a host offers (cell input, sheets, blocks, form schemas,
11
+ // analysis blocks, pivots, formatting, validation) lives here once.
12
+ // - format generators (./format): pure selection -> style payload builders.
13
+ // - craft contracts (./craft): CraftRuntime (the headless craft hooks),
14
+ // craft state (per-document, rides AppData), craft storage (per-device),
15
+ // canvas-input routing, the JSON-RPC wire types.
16
+ // - craft interactions (./craft-interactions): host-drawn widgets (radio,
17
+ // multi-select, allocators, sliders) crafts bind to block cells.
18
+ // - validation, field authoring model, Value helpers, permissions registry,
19
+ // string / A1 / type-guard utilities.
20
+ //
21
+ // Module-level stores (craft state, storage backend, interactions, active
22
+ // craft, callerRegistry) are process singletons: one workbook per host
23
+ // process is assumed. A Node host serving several workbooks shares them.
6
24
  export * from './port.js';
7
25
  export * from './ops/index.js';
8
26
  export * from './format/index.js';
@@ -218,6 +218,23 @@ export interface PivotRefresh {
218
218
  /**
219
219
  * High-level workbook operations bound to one engine {@link Client}.
220
220
  * Construct one per workbook and share it across the host.
221
+ *
222
+ * Indexes are 0-based; `sheetIdx` is the tab position. Block-relative
223
+ * `row`/`col` count from the block's master (top-left) cell.
224
+ *
225
+ * Failure contract, read before relying on a write:
226
+ * - An operation THROWS (rejects) when the engine answers a call with an
227
+ * `ErrorMessage`, and when this layer refuses its inputs (documented per
228
+ * method).
229
+ * - An operation does NOT throw when the engine refuses the edit itself.
230
+ * The engine reports that as an `ActionEffect` with
231
+ * `status.type === 'err'` (reason in `errorMessage`) and applies nothing.
232
+ * Methods returning `Promise<ActionEffect>` hand that effect back: check
233
+ * `status`. Methods returning `Promise<void>` or a summary DROP it, so a
234
+ * refused transaction looks like success there.
235
+ *
236
+ * Every transaction is built at the injected {@link TempModeProvider}'s
237
+ * current value, so in the browser's temp mode these writes are speculative.
221
238
  */
222
239
  export declare class WorkbookOps {
223
240
  private readonly client;
@@ -228,7 +245,9 @@ export declare class WorkbookOps {
228
245
  /**
229
246
  * Build a transaction at the host's current temp-mode, send it, and return
230
247
  * the engine's effect. Throws on an engine ErrorMessage so callers can use
231
- * normal try/catch instead of inspecting a union.
248
+ * normal try/catch instead of inspecting a union. An engine REJECTION is
249
+ * not an ErrorMessage: it comes back as the effect with `status.type ===
250
+ * 'err'` and is returned, not thrown.
232
251
  */
233
252
  private apply;
234
253
  /** Write a value or formula into a cell. */
@@ -263,7 +282,11 @@ export declare class WorkbookOps {
263
282
  renameSheet(oldName: string, newName: string): Promise<ActionEffect>;
264
283
  /** Delete the sheet at index `idx`. */
265
284
  deleteSheet(idx: number): Promise<ActionEffect>;
266
- /** Set a sheet tab's color (ARGB string; empty clears it). */
285
+ /**
286
+ * Set a sheet tab's color: 8-hex ARGB with no `#` (e.g. `'FF4472C4'`),
287
+ * or `''` to clear. Stored verbatim and written to the file's `tabColor`;
288
+ * nothing validates or normalises it.
289
+ */
267
290
  setSheetColor(idx: number, color: string): Promise<ActionEffect>;
268
291
  /** Insert `cnt` rows into a block, starting at block-row `start`. */
269
292
  insertRowsInBlock(sheetIdx: number, blockId: number, start: number, cnt?: number): Promise<ActionEffect>;
@@ -298,7 +321,8 @@ export declare class WorkbookOps {
298
321
  * re-`bindFormSchema` is needed.
299
322
  */
300
323
  moveBlockLine(sheetIdx: number, blockId: number, from: number, to: number, isRow?: boolean, undoable?: boolean): Promise<ActionEffect>;
301
- /** Apply font styling (bold/italic/underline/strike/color/size). */
324
+ /** Apply font styling (bold/italic/underline/strike/color/size). The
325
+ * color is 8-hex ARGB with no `#`, see {@link FontStyle}. */
302
326
  setFont(sheetIdx: number, data: SelectedData, update: FontStyle): Promise<void>;
303
327
  /** Apply horizontal/vertical alignment. */
304
328
  setAlignment(sheetIdx: number, data: SelectedData, alignment: Alignment): Promise<void>;
@@ -306,13 +330,16 @@ export declare class WorkbookOps {
306
330
  setWrapText(sheetIdx: number, data: SelectedData, wrapText: boolean): Promise<void>;
307
331
  /** Apply a number format. */
308
332
  setNumFmt(sheetIdx: number, data: SelectedData, numFmt: string): Promise<void>;
309
- /** Apply a pattern fill (foreground/background color + pattern). */
333
+ /** Apply a pattern fill (foreground/background color + pattern). Colors
334
+ * are hex strings here (`'#RRGGBB'`, `'RRGGBB'` or `'AARRGGBB'`), turned
335
+ * into the engine's `{red, green, blue}` objects; alpha is dropped. */
310
336
  setPatternFill(sheetIdx: number, data: SelectedData, opts: {
311
337
  fgColor?: string;
312
338
  bgColor?: string;
313
339
  pattern?: StPatternType;
314
340
  }): Promise<void>;
315
- /** Apply borders to the selection per the batch directive. */
341
+ /** Apply borders to the selection per the batch directive. The color is
342
+ * 8-hex ARGB (a leading `#` is stripped), see {@link BorderBatchUpdate}. */
316
343
  setBorder(sheetIdx: number, data: SelectedData, update: BorderBatchUpdate): Promise<void>;
317
344
  /** Apply a generated payload list, skipping the round-trip when empty. */
318
345
  private applyGenerated;
@@ -708,6 +735,11 @@ export declare class WorkbookOps {
708
735
  * Value. Parks the formula in a throwaway ephemeral cell, reads the result
709
736
  * back, and leaves no committed change — the same mechanism the browser
710
737
  * uses for shadow cells, here for one-shot evaluation.
738
+ *
739
+ * Each call takes a NEW ephemeral id (from 1, per WorkbookOps instance)
740
+ * and never removes the cell, so the cells accumulate, and keep
741
+ * recomputing, for the life of the loaded workbook. A rejected write is not
742
+ * detected: the read-back then yields whatever the slot holds.
711
743
  */
712
744
  evalFormula(sheetIdx: number, formula: string): Promise<Value>;
713
745
  /**
package/dist/ops/index.js CHANGED
@@ -146,6 +146,23 @@ function toSchemaFieldSpec(f) {
146
146
  /**
147
147
  * High-level workbook operations bound to one engine {@link Client}.
148
148
  * Construct one per workbook and share it across the host.
149
+ *
150
+ * Indexes are 0-based; `sheetIdx` is the tab position. Block-relative
151
+ * `row`/`col` count from the block's master (top-left) cell.
152
+ *
153
+ * Failure contract, read before relying on a write:
154
+ * - An operation THROWS (rejects) when the engine answers a call with an
155
+ * `ErrorMessage`, and when this layer refuses its inputs (documented per
156
+ * method).
157
+ * - An operation does NOT throw when the engine refuses the edit itself.
158
+ * The engine reports that as an `ActionEffect` with
159
+ * `status.type === 'err'` (reason in `errorMessage`) and applies nothing.
160
+ * Methods returning `Promise<ActionEffect>` hand that effect back: check
161
+ * `status`. Methods returning `Promise<void>` or a summary DROP it, so a
162
+ * refused transaction looks like success there.
163
+ *
164
+ * Every transaction is built at the injected {@link TempModeProvider}'s
165
+ * current value, so in the browser's temp mode these writes are speculative.
149
166
  */
150
167
  export class WorkbookOps {
151
168
  constructor(client, tempMode = () => false) {
@@ -157,7 +174,9 @@ export class WorkbookOps {
157
174
  /**
158
175
  * Build a transaction at the host's current temp-mode, send it, and return
159
176
  * the engine's effect. Throws on an engine ErrorMessage so callers can use
160
- * normal try/catch instead of inspecting a union.
177
+ * normal try/catch instead of inspecting a union. An engine REJECTION is
178
+ * not an ErrorMessage: it comes back as the effect with `status.type ===
179
+ * 'err'` and is returned, not thrown.
161
180
  */
162
181
  async apply(payloads, undoable) {
163
182
  const transaction = makeTransaction(payloads, undoable, this.tempMode());
@@ -226,7 +245,11 @@ export class WorkbookOps {
226
245
  deleteSheet(idx) {
227
246
  return this.apply([{ type: 'deleteSheet', value: { idx } }], true);
228
247
  }
229
- /** Set a sheet tab's color (ARGB string; empty clears it). */
248
+ /**
249
+ * Set a sheet tab's color: 8-hex ARGB with no `#` (e.g. `'FF4472C4'`),
250
+ * or `''` to clear. Stored verbatim and written to the file's `tabColor`;
251
+ * nothing validates or normalises it.
252
+ */
230
253
  setSheetColor(idx, color) {
231
254
  return this.apply([{ type: 'setSheetColor', value: { idx, color } }], true);
232
255
  }
@@ -313,7 +336,8 @@ export class WorkbookOps {
313
336
  // Each method turns the current sheet + selection into style-update
314
337
  // payloads (logic in ../format) and applies them. The host supplies the
315
338
  // sheet index (a view concern) and the selection.
316
- /** Apply font styling (bold/italic/underline/strike/color/size). */
339
+ /** Apply font styling (bold/italic/underline/strike/color/size). The
340
+ * color is 8-hex ARGB with no `#`, see {@link FontStyle}. */
317
341
  async setFont(sheetIdx, data, update) {
318
342
  await this.applyGenerated(generateFontPayload(sheetIdx, data, update));
319
343
  }
@@ -329,11 +353,14 @@ export class WorkbookOps {
329
353
  async setNumFmt(sheetIdx, data, numFmt) {
330
354
  await this.applyGenerated(generateNumFmtPayload(sheetIdx, data, numFmt));
331
355
  }
332
- /** Apply a pattern fill (foreground/background color + pattern). */
356
+ /** Apply a pattern fill (foreground/background color + pattern). Colors
357
+ * are hex strings here (`'#RRGGBB'`, `'RRGGBB'` or `'AARRGGBB'`), turned
358
+ * into the engine's `{red, green, blue}` objects; alpha is dropped. */
333
359
  async setPatternFill(sheetIdx, data, opts) {
334
360
  await this.applyGenerated(generatePatternFillPayload(sheetIdx, data, opts.fgColor, opts.bgColor, opts.pattern));
335
361
  }
336
- /** Apply borders to the selection per the batch directive. */
362
+ /** Apply borders to the selection per the batch directive. The color is
363
+ * 8-hex ARGB (a leading `#` is stripped), see {@link BorderBatchUpdate}. */
337
364
  async setBorder(sheetIdx, data, update) {
338
365
  await this.applyGenerated(generateBorderPayloads(sheetIdx, data, update));
339
366
  }
@@ -1367,6 +1394,11 @@ export class WorkbookOps {
1367
1394
  * Value. Parks the formula in a throwaway ephemeral cell, reads the result
1368
1395
  * back, and leaves no committed change — the same mechanism the browser
1369
1396
  * uses for shadow cells, here for one-shot evaluation.
1397
+ *
1398
+ * Each call takes a NEW ephemeral id (from 1, per WorkbookOps instance)
1399
+ * and never removes the cell, so the cells accumulate, and keep
1400
+ * recomputing, for the life of the loaded workbook. A rejected write is not
1401
+ * detected: the read-back then yields whatever the slot holds.
1370
1402
  */
1371
1403
  async evalFormula(sheetIdx, formula) {
1372
1404
  const id = this.ephemeralSeq++;
@@ -1,8 +1,17 @@
1
+ /**
2
+ * Session-scoped caller identities. The user and each craft get a random uuid
3
+ * on first ask; a host tags each write with it and later maps it back (via
4
+ * {@link resolveActor}) to the actor the engine's `mayModifyBlock` expects.
5
+ * Nothing here persists: uuids and owner records are gone on reload, which
6
+ * is why block ownership that must survive lives on the engine's block.
7
+ */
1
8
  declare class CallerRegistry {
2
9
  private _entries;
3
10
  private _blockOwners;
4
11
  private _fieldPositions;
12
+ /** The user's uuid for this session. */
5
13
  getUserUuid(): string;
14
+ /** A craft's uuid for this session. Throws for the reserved user key. */
6
15
  getCraftUuid(craftId: string): string;
7
16
  isUser(uuid: string | undefined): boolean;
8
17
  /**
@@ -21,6 +30,8 @@ declare class CallerRegistry {
21
30
  type: 'craft';
22
31
  craftId: string;
23
32
  } | undefined;
33
+ /** Record which caller created a block. In-memory only; keyed by
34
+ * `sheetIdx`, so it goes stale when sheets move. */
24
35
  registerBlockOwner(sheetIdx: number, blockId: number, callerUuid: string): void;
25
36
  getBlockOwner(sheetIdx: number, blockId: number): string | undefined;
26
37
  /**
@@ -32,5 +43,6 @@ declare class CallerRegistry {
32
43
  getFieldRenderId(sheetIdx: number, blockId: number, blockRow: number, blockCol: number): string | undefined;
33
44
  private _getOrAssign;
34
45
  }
46
+ /** The process-wide registry. */
35
47
  export declare const callerRegistry: CallerRegistry;
36
48
  export {};
@@ -1,4 +1,4 @@
1
- // Permissions — caller identity registry + the pure editability predicate.
1
+ // Permissions — the caller identity registry.
2
2
  //
3
3
  // The CallerRegistry is plain in-process state (no engine, no UI): it maps
4
4
  // caller identities to uuids, blocks to owners, and block-relative field
@@ -11,19 +11,28 @@
11
11
  // UI/runtime glue, not portable logic.
12
12
  import { simpleUuid } from '../utils/index.js';
13
13
  const USER_KEY = '__user__';
14
+ /**
15
+ * Session-scoped caller identities. The user and each craft get a random uuid
16
+ * on first ask; a host tags each write with it and later maps it back (via
17
+ * {@link resolveActor}) to the actor the engine's `mayModifyBlock` expects.
18
+ * Nothing here persists: uuids and owner records are gone on reload, which
19
+ * is why block ownership that must survive lives on the engine's block.
20
+ */
14
21
  class CallerRegistry {
15
22
  constructor() {
16
23
  this._entries = new Map();
17
24
  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.
25
+ // (sheetIdx, blockId, axis, block-relative offset) → field renderId.
26
+ // Populated when the app's patch.ts observes a bindFormSchema payload.
27
+ // It used to back the host-side `userEditable` check; that rule now lives
28
+ // on the engine schema, and nothing outside tests reads this map.
22
29
  this._fieldPositions = new Map();
23
30
  }
31
+ /** The user's uuid for this session. */
24
32
  getUserUuid() {
25
33
  return this._getOrAssign(USER_KEY);
26
34
  }
35
+ /** A craft's uuid for this session. Throws for the reserved user key. */
27
36
  getCraftUuid(craftId) {
28
37
  if (craftId === USER_KEY) {
29
38
  throw new Error(`invalid craftId: ${craftId}`);
@@ -55,6 +64,8 @@ class CallerRegistry {
55
64
  }
56
65
  return undefined;
57
66
  }
67
+ /** Record which caller created a block. In-memory only; keyed by
68
+ * `sheetIdx`, so it goes stale when sheets move. */
58
69
  registerBlockOwner(sheetIdx, blockId, callerUuid) {
59
70
  this._blockOwners.set(`${sheetIdx}-${blockId}`, callerUuid);
60
71
  }
@@ -84,6 +95,7 @@ class CallerRegistry {
84
95
  return uuid;
85
96
  }
86
97
  }
98
+ /** The process-wide registry. */
87
99
  export const callerRegistry = new CallerRegistry();
88
100
  // `FieldEditableInfo` and `isFieldUserEditable` used to live here, reading a tri-state `userEditable`
89
101
  // boolean off the host's own field store. The field's write policy is declared
package/dist/port.js CHANGED
@@ -7,8 +7,10 @@
7
7
  //
8
8
  // The concrete Client is INJECTED by the host:
9
9
  // - browser app -> logisheets-engine's worker-backed WorkbookClient
10
- // - node runtime -> logisheets' synchronous handle()-based client
10
+ // - node runtime -> logisheets-runtime's Proxy over the Node WASM handle(),
11
+ // with each synchronous result wrapped in a Promise
11
12
  //
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.
13
+ // Both implement the same `Client` interface, so logisheets-core never needs
14
+ // to know which one it got. (The Node proxy leaves the `register*` callback
15
+ // members unimplemented; core code must not depend on them.)
14
16
  export {};
@@ -1,4 +1,11 @@
1
+ /** Whether `value` is a cell (`B3`) or range (`A1:C9`) reference,
2
+ * case-insensitive. No `$`, no sheet prefix. */
1
3
  export declare function isA1notation(value: unknown): boolean;
4
+ /**
5
+ * Parse a cell or range reference into 0-based indexes: `cs`/`rs` are the
6
+ * start column/row, `ce`/`re` the end (present for a range only).
7
+ * `undefined` when the text is not in {@link isA1notation}'s form.
8
+ */
2
9
  export declare function parseA1notation(value: string): {
3
10
  cs: number;
4
11
  rs: number;
@@ -3,12 +3,18 @@ import { upperCase } from '../strings/index.js';
3
3
  import { isString } from '../type-guard/index.js';
4
4
  const a1notationReg = /^(?<cs>[A-Z]+)(?<rs>\d+)(:(?<ce>[A-Z]+)(?<re>\d+))?$/i;
5
5
  const notationReq = /^[A-Z]/;
6
+ /** Whether `value` is a cell (`B3`) or range (`A1:C9`) reference,
7
+ * case-insensitive. No `$`, no sheet prefix. */
6
8
  export function isA1notation(value) {
7
9
  if (!isString(value))
8
10
  return false;
9
11
  return a1notationReg.test(upperCase(value));
10
12
  }
11
- // zero-based
13
+ /**
14
+ * Parse a cell or range reference into 0-based indexes: `cs`/`rs` are the
15
+ * start column/row, `ce`/`re` the end (present for a range only).
16
+ * `undefined` when the text is not in {@link isA1notation}'s form.
17
+ */
12
18
  export function parseA1notation(value) {
13
19
  const result = upperCase(value).match(a1notationReg);
14
20
  if (!result?.groups)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "logisheets-core",
3
- "version": "1.15.0",
3
+ "version": "1.16.0",
4
4
  "description": "UI-free LogiSheets logic — runs in the browser or on Node. Depends on logisheets-web only for types; the engine Client is injected by the host.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -63,7 +63,7 @@
63
63
  },
64
64
  "homepage": "https://github.com/logisky/LogiSheets",
65
65
  "peerDependencies": {
66
- "logisheets-web": "^1.15.0"
66
+ "logisheets-web": "^1.16.0"
67
67
  },
68
68
  "devDependencies": {
69
69
  "@types/node": "^18",