logisheets-core 1.9.0 → 1.10.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.
@@ -0,0 +1,68 @@
1
+ /** The kinds of canvas events forwarded to a craft. */
2
+ export type CraftCanvasEventType = 'mousedown' | 'mousemove' | 'mouseup' | 'click' | 'dblclick' | 'contextmenu' | 'wheel' | 'keydown' | 'keyup';
3
+ /**
4
+ * A serialized, realm-safe snapshot of a canvas DOM event, plus the resolved
5
+ * cell under the pointer. Passed to the craft's handler. It is a plain object
6
+ * (not the live DOM event) so it crosses the iframe boundary cleanly; the craft
7
+ * cannot mutate host event state — it only returns a decision.
8
+ */
9
+ export interface CraftCanvasEvent {
10
+ type: CraftCanvasEventType;
11
+ /** Viewport coordinates (mouse/wheel only; 0 for keyboard). */
12
+ clientX: number;
13
+ clientY: number;
14
+ /** Coordinates relative to the data canvas's top-left (mouse/wheel only). */
15
+ offsetX: number;
16
+ offsetY: number;
17
+ /** Which view the event came from ('main' or a secondary view id). */
18
+ viewId: string;
19
+ /** The sheet shown in that view. */
20
+ sheetIdx: number;
21
+ /** Cell under the pointer, or null when outside the data area / keyboard. */
22
+ row: number | null;
23
+ col: number | null;
24
+ button: number;
25
+ buttons: number;
26
+ deltaX: number;
27
+ deltaY: number;
28
+ key: string;
29
+ code: string;
30
+ altKey: boolean;
31
+ ctrlKey: boolean;
32
+ metaKey: boolean;
33
+ shiftKey: boolean;
34
+ /** True when the event repeats (held key). */
35
+ repeat: boolean;
36
+ }
37
+ /**
38
+ * What a craft returns from its handler. `true` (or `{handled: true}`) tells
39
+ * the host to consume the event — the engine never sees it. `false`, `void`, or
40
+ * `{handled: false}` lets it pass through to the engine unchanged.
41
+ */
42
+ export type CraftInputDecision = boolean | {
43
+ handled: boolean;
44
+ } | void;
45
+ export type CraftInputHandler = (e: CraftCanvasEvent) => CraftInputDecision;
46
+ /** Host: mark the active craft (its id, i.e. its iframe src), or null. */
47
+ export declare function setActiveCraft(craftId: string | null): void;
48
+ /** The currently-active craft id, or null. */
49
+ export declare function getActiveCraft(): string | null;
50
+ /**
51
+ * Craft (via injection): register a canvas-input handler. Returns a disposer.
52
+ * Re-registering replaces the previous handler for that craft.
53
+ */
54
+ export declare function registerCraftInputHandler(craftId: string, handler: CraftInputHandler): () => void;
55
+ /**
56
+ * Whether an active craft has a handler ready. The interceptor checks this
57
+ * first so it does no work (no event serialization) unless a craft is actually
58
+ * listening — important since this runs on every mousemove.
59
+ */
60
+ export declare function hasActiveCraftInputHandler(): boolean;
61
+ /**
62
+ * Host: deliver an event to the active craft and get its decision. Never
63
+ * throws — a craft handler that throws is treated as "not handled" so a buggy
64
+ * craft can't wedge the spreadsheet.
65
+ */
66
+ export declare function dispatchCraftCanvasEvent(evt: CraftCanvasEvent): {
67
+ handled: boolean;
68
+ };
@@ -0,0 +1,71 @@
1
+ // Canvas input routing for the ACTIVE craft.
2
+ //
3
+ // When a craft is active (its panel is open and it is the selected craft), the
4
+ // host lets it see mouse/keyboard events happening on the spreadsheet canvas
5
+ // BEFORE the engine does, and the craft decides — synchronously — whether the
6
+ // engine should still handle each one. This is the seam that lets a craft
7
+ // implement its own canvas tool (custom selection, drawing, drag gestures)
8
+ // without forking the engine.
9
+ //
10
+ // The craft registers a handler through the injected `window.onCanvasInput`
11
+ // (see the craft panel). The host (the per-view interceptor) calls
12
+ // `dispatchCraftCanvasEvent` from a capture-phase DOM listener; if the active
13
+ // craft's handler returns "handled", the host stops the event from reaching
14
+ // the engine. The call is synchronous end-to-end — the iframe is same-origin,
15
+ // so there is no postMessage hop — which is what makes a real pass-through
16
+ // decision possible mid-dispatch.
17
+ // Which craft is active right now (panel open + selected). null = none, so the
18
+ // interceptor is a no-op and the engine behaves normally.
19
+ let activeCraftId = null;
20
+ // Per-craft handlers. A craft's handler stays registered while its iframe is
21
+ // alive; only the active craft's handler is ever invoked.
22
+ const handlers = new Map();
23
+ /** Host: mark the active craft (its id, i.e. its iframe src), or null. */
24
+ export function setActiveCraft(craftId) {
25
+ activeCraftId = craftId;
26
+ }
27
+ /** The currently-active craft id, or null. */
28
+ export function getActiveCraft() {
29
+ return activeCraftId;
30
+ }
31
+ /**
32
+ * Craft (via injection): register a canvas-input handler. Returns a disposer.
33
+ * Re-registering replaces the previous handler for that craft.
34
+ */
35
+ export function registerCraftInputHandler(craftId, handler) {
36
+ handlers.set(craftId, handler);
37
+ return () => {
38
+ if (handlers.get(craftId) === handler)
39
+ handlers.delete(craftId);
40
+ };
41
+ }
42
+ /**
43
+ * Whether an active craft has a handler ready. The interceptor checks this
44
+ * first so it does no work (no event serialization) unless a craft is actually
45
+ * listening — important since this runs on every mousemove.
46
+ */
47
+ export function hasActiveCraftInputHandler() {
48
+ return activeCraftId !== null && handlers.has(activeCraftId);
49
+ }
50
+ /**
51
+ * Host: deliver an event to the active craft and get its decision. Never
52
+ * throws — a craft handler that throws is treated as "not handled" so a buggy
53
+ * craft can't wedge the spreadsheet.
54
+ */
55
+ export function dispatchCraftCanvasEvent(evt) {
56
+ if (activeCraftId === null)
57
+ return { handled: false };
58
+ const handler = handlers.get(activeCraftId);
59
+ if (!handler)
60
+ return { handled: false };
61
+ try {
62
+ const r = handler(evt);
63
+ const handled = r === true || (typeof r === 'object' && r !== null && r.handled === true);
64
+ return { handled };
65
+ }
66
+ catch (e) {
67
+ // eslint-disable-next-line no-console
68
+ console.error('[craft-events] handler threw', e);
69
+ return { handled: false };
70
+ }
71
+ }
@@ -1,4 +1,6 @@
1
1
  export * from './runtime.js';
2
2
  export * from './state.js';
3
+ export * from './storage.js';
4
+ export * from './events.js';
3
5
  export * from './manifest.js';
4
6
  export * from './rpc.js';
@@ -1,4 +1,6 @@
1
1
  export * from './runtime.js';
2
2
  export * from './state.js';
3
+ export * from './storage.js';
4
+ export * from './events.js';
3
5
  export * from './manifest.js';
4
6
  export * from './rpc.js';
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The per-craft handle injected onto the craft iframe's `window` as
3
+ * `window.craftStorage`. All keys/values are scoped to the owning craft; a
4
+ * craft can neither see nor touch another craft's namespace.
5
+ */
6
+ export interface CraftStorage {
7
+ /** Read a value, or `null` if the key was never set. */
8
+ get(key: string): Promise<string | null>;
9
+ /** Write a value, overwriting any previous one. */
10
+ set(key: string, value: string): Promise<void>;
11
+ /** Delete a single key. A no-op if it doesn't exist. */
12
+ remove(key: string): Promise<void>;
13
+ /** List every key this craft has stored. */
14
+ keys(): Promise<string[]>;
15
+ /** Delete every key in THIS craft's namespace only. */
16
+ clear(): Promise<void>;
17
+ }
18
+ /**
19
+ * The platform-specific store the host injects at startup. It is the same
20
+ * shape as {@link CraftStorage} but takes an explicit `craftId`, so a single
21
+ * backend instance serves every craft while keeping their namespaces isolated.
22
+ * Concrete implementations (localStorage, Tauri app-data) live host-side; core
23
+ * only defines the seam, mirroring how the engine Client is injected.
24
+ */
25
+ export interface CraftStorageBackend {
26
+ get(craftId: string, key: string): Promise<string | null>;
27
+ set(craftId: string, key: string, value: string): Promise<void>;
28
+ remove(craftId: string, key: string): Promise<void>;
29
+ keys(craftId: string): Promise<string[]>;
30
+ clear(craftId: string): Promise<void>;
31
+ }
32
+ /**
33
+ * Install the concrete backend for this host. Called once at app startup
34
+ * (e.g. localStorage in the browser, a Tauri command bridge on the desktop).
35
+ * Until this runs, craft storage transparently uses an in-memory fallback.
36
+ */
37
+ export declare function setCraftStorageBackend(backend: CraftStorageBackend): void;
38
+ /**
39
+ * Build the per-craft {@link CraftStorage} handle the host injects as
40
+ * `window.craftStorage`. It closes over `craftId` and forwards to whichever
41
+ * backend is active at call time, so swapping the backend affects every craft
42
+ * uniformly and the craft can never reach outside its own namespace.
43
+ */
44
+ export declare function makeCraftStorage(craftId: string): CraftStorage;
@@ -0,0 +1,73 @@
1
+ // Device-scoped, per-craft key/value storage.
2
+ //
3
+ // This is the sibling of craft STATE (see ./state.ts) but with the opposite
4
+ // scope. Craft state rides the workbook's AppData, so it travels inside the
5
+ // .xlsx and is per-document. Craft storage instead lives on the DEVICE — the
6
+ // browser origin's localStorage on the web, and the app-data directory on the
7
+ // desktop — so it persists across workbooks and never leaves the machine.
8
+ //
9
+ // A craft (running in its iframe) reaches storage through the injected
10
+ // `window.craftStorage` object, which the host binds to that craft's id. Every
11
+ // operation is async because the desktop backend talks to the native side over
12
+ // IPC; the web backend wraps synchronous localStorage in resolved promises so
13
+ // both platforms present the same interface.
14
+ //
15
+ // Values are opaque strings — the craft owns its own schema, exactly as with
16
+ // craft state. The store is plaintext on both platforms, so crafts must not use
17
+ // it for secrets.
18
+ // In-memory fallback used until a host injects a real backend, and on hosts
19
+ // with no persistent store (SSR, a Node runtime, or a browser in private mode
20
+ // where localStorage throws). Data lives only for the session.
21
+ class MemoryCraftStorageBackend {
22
+ constructor() {
23
+ this.crafts = new Map();
24
+ }
25
+ ns(craftId) {
26
+ let m = this.crafts.get(craftId);
27
+ if (!m) {
28
+ m = new Map();
29
+ this.crafts.set(craftId, m);
30
+ }
31
+ return m;
32
+ }
33
+ async get(craftId, key) {
34
+ const v = this.ns(craftId).get(key);
35
+ return v === undefined ? null : v;
36
+ }
37
+ async set(craftId, key, value) {
38
+ this.ns(craftId).set(key, value);
39
+ }
40
+ async remove(craftId, key) {
41
+ this.ns(craftId).delete(key);
42
+ }
43
+ async keys(craftId) {
44
+ return [...this.ns(craftId).keys()];
45
+ }
46
+ async clear(craftId) {
47
+ this.crafts.delete(craftId);
48
+ }
49
+ }
50
+ let activeBackend = new MemoryCraftStorageBackend();
51
+ /**
52
+ * Install the concrete backend for this host. Called once at app startup
53
+ * (e.g. localStorage in the browser, a Tauri command bridge on the desktop).
54
+ * Until this runs, craft storage transparently uses an in-memory fallback.
55
+ */
56
+ export function setCraftStorageBackend(backend) {
57
+ activeBackend = backend;
58
+ }
59
+ /**
60
+ * Build the per-craft {@link CraftStorage} handle the host injects as
61
+ * `window.craftStorage`. It closes over `craftId` and forwards to whichever
62
+ * backend is active at call time, so swapping the backend affects every craft
63
+ * uniformly and the craft can never reach outside its own namespace.
64
+ */
65
+ export function makeCraftStorage(craftId) {
66
+ return {
67
+ get: (key) => activeBackend.get(craftId, key),
68
+ set: (key, value) => activeBackend.set(craftId, key, value),
69
+ remove: (key) => activeBackend.remove(craftId, key),
70
+ keys: () => activeBackend.keys(craftId),
71
+ clear: () => activeBackend.clear(craftId),
72
+ };
73
+ }
@@ -1,6 +1,6 @@
1
1
  import type { Value } from 'logisheets-web';
2
2
  import type { Violation } from '../validation/index.js';
3
- export type FieldTypeEnum = 'enum' | 'multiSelect' | 'datetime' | 'boolean' | 'string' | 'number' | 'image' | 'fieldRef' | 'multiSelectRef';
3
+ export type FieldTypeEnum = 'unspecified' | 'enum' | 'multiSelect' | 'datetime' | 'boolean' | 'string' | 'number' | 'image' | 'fieldRef' | 'multiSelectRef';
4
4
  export interface EnumValue {
5
5
  id: string;
6
6
  label: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "logisheets-core",
3
- "version": "1.9.0",
3
+ "version": "1.10.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",
@@ -44,11 +44,12 @@
44
44
  "author": "Jeremy He",
45
45
  "license": "MIT",
46
46
  "peerDependencies": {
47
- "logisheets-web": "^1.9.0"
47
+ "logisheets-web": "^1.10.0"
48
48
  },
49
49
  "devDependencies": {
50
+ "@types/node": "^18",
50
51
  "logisheets-web": "workspace:*",
51
- "typescript": "^5.5.0",
52
+ "typescript": "^6.0.0",
52
53
  "vitest": "3.2.6"
53
54
  }
54
55
  }