@weasel-js/labkit 0.8.0 → 1.0.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 (71) hide show
  1. package/README.md +11 -3
  2. package/dist/_dts/{DrawCommand-DACv9lhe.d.ts → DrawCommand-BwRCZYuo.d.ts} +72 -6
  3. package/dist/_dts/{useExperimentState-eSvpwV_P.d.ts → useExperimentState-CJn2hHzd.d.ts} +6 -4
  4. package/dist/{chunk-SSMHPMDV.js → chunk-53XSBIUK.js} +12 -11
  5. package/dist/chunk-53XSBIUK.js.map +1 -0
  6. package/dist/{chunk-FBZCQXI2.js → chunk-FP5LYDVX.js} +1229 -1227
  7. package/dist/chunk-FP5LYDVX.js.map +1 -0
  8. package/dist/{chunk-OHGEOTCV.js → chunk-HNXVLKBG.js} +3 -3
  9. package/dist/{chunk-OHGEOTCV.js.map → chunk-HNXVLKBG.js.map} +1 -1
  10. package/dist/{chunk-6V7ROR7G.js → chunk-XVBXYKFJ.js} +1168 -557
  11. package/dist/chunk-XVBXYKFJ.js.map +1 -0
  12. package/dist/index.d.ts +37 -13
  13. package/dist/index.js +776 -27
  14. package/dist/index.js.map +1 -1
  15. package/dist/passthrough/weasel-canvas.d.ts +1 -1
  16. package/dist/passthrough/weasel-canvas.js +1 -1
  17. package/dist/passthrough/weasel-ui.d.ts +97 -69
  18. package/dist/passthrough/weasel-ui.js +2 -2
  19. package/dist/state/index.d.ts +2 -2
  20. package/dist/state/index.js +2 -2
  21. package/dist/styles.css +228 -287
  22. package/dist/ui/layers/index.js +3 -3
  23. package/package.json +4 -6
  24. package/src/canvas/CanvasStack.less +1 -1
  25. package/src/controls/ControlPanel.less +12 -12
  26. package/src/dragdrop/DragGhost.less +2 -2
  27. package/src/dragdrop/Palette.less +6 -6
  28. package/src/index.ts +2 -0
  29. package/src/lab/Lab.test.tsx +20 -15
  30. package/src/lab/Lab.tsx +33 -32
  31. package/src/lab/LabContext.ts +3 -3
  32. package/src/lab/LabShell.less +14 -14
  33. package/src/lab/LabShell.test.tsx +7 -20
  34. package/src/lab/LabShell.tsx +22 -13
  35. package/src/lab/WorkspaceGrid.less +1 -1
  36. package/src/lab/WorkspaceGrid.stories.tsx +4 -4
  37. package/src/lab/index.ts +1 -1
  38. package/src/lab/useSystemMode.ts +30 -0
  39. package/src/layers/LayerList.less +9 -9
  40. package/src/primitives/FpsMeter.less +5 -5
  41. package/src/primitives/ScaleIndicator.less +7 -7
  42. package/src/primitives/Sidebar.less +12 -12
  43. package/src/primitives/StatusBar.less +10 -10
  44. package/src/primitives/Toolbar.less +16 -16
  45. package/src/state/store.test.ts +17 -9
  46. package/src/state/store.ts +13 -10
  47. package/src/state/types.ts +5 -2
  48. package/src/theme/Interstellar.stories.less +20 -20
  49. package/src/theme/Interstellar.stories.tsx +44 -59
  50. package/src/theme/base.less +26 -26
  51. package/src/theme/interstellar.test.ts +30 -0
  52. package/src/theme/interstellar.tokens.json +57 -0
  53. package/src/theme/interstellar.ts +9 -0
  54. package/src/ui/layers/LayerStack.less +21 -21
  55. package/src/ui/properties/CurveField.less +5 -5
  56. package/src/ui/properties/EffectCard.tsx +2 -2
  57. package/src/ui/properties/PropertyGroup.less +6 -6
  58. package/src/ui/properties/PropertyPanel.less +70 -70
  59. package/src/ui/properties/SpeechBalloonPanels.stories.tsx +1 -1
  60. package/src/ui/properties/storyLayouts.tsx +1 -1
  61. package/src/workspace/Workspace.less +14 -14
  62. package/src/workspace/Workspace.stories.tsx +2 -2
  63. package/src/workspace/Workspace.test.tsx +2 -2
  64. package/dist/chunk-6V7ROR7G.js.map +0 -1
  65. package/dist/chunk-FBZCQXI2.js.map +0 -1
  66. package/dist/chunk-SSMHPMDV.js.map +0 -1
  67. package/dist/theme-interstellar.css +0 -25
  68. package/dist/theme-light.css +0 -13
  69. package/src/theme/interstellar.less +0 -34
  70. package/src/theme/light.less +0 -14
  71. package/src/theme/tokens.less +0 -85
package/README.md CHANGED
@@ -43,14 +43,22 @@ function MyLab() {
43
43
  }
44
44
  ```
45
45
 
46
- ## Theme
46
+ ## Theming
47
47
 
48
- Theme follows the OS by default. To force a theme:
48
+ labkit ships one theme, `interstellar` a cosmic dark and a warm parchment
49
+ light — as a value, not a stylesheet:
49
50
 
50
51
  ```tsx
51
- <LabShell title="..." theme="dark">...</LabShell>
52
+ import { interstellarTheme } from '@weasel-js/labkit';
53
+ import { ThemeProvider } from '@weasel-js/theme/react';
54
+
55
+ <ThemeProvider theme={interstellarTheme} mode="dark">…</ThemeProvider>
52
56
  ```
53
57
 
58
+ `<Lab>` and `<LabShell>` do this for you; `mode` is `"auto"` (follow the OS),
59
+ `"light"` or `"dark"`. Only `styles.css` needs importing — the token values
60
+ arrive through the provider.
61
+
54
62
  ## Development
55
63
 
56
64
  ```bash
@@ -603,11 +603,18 @@ interface TextureHandle {
603
603
  * equivalent to `{ fill: 'solid', color: '#abc' }`. Pattern paints must set
604
604
  * `fill: 'pattern'` explicitly.
605
605
  *
606
- * The `'pattern'` variant's payload is a `TextureHandle` (registered via
607
- * `registerTexture()`). The kit-level factory `createTilePattern` (and the
608
- * `patterns-builtin` catalog: `hatch`, `crosshatch`, `dots`, `chunks`)
609
- * produces these handles by rendering a tile to an `OffscreenCanvas` and
610
- * registering the resulting `ImageBitmap` as a GL texture.
606
+ * The `'pattern'` variant's payload is either a `TilePatternSpec` plain
607
+ * data naming one of the built-in tiles, which survives serialization — or a
608
+ * `TextureHandle` for a tile the consumer built itself via
609
+ * `createTilePattern()`. A handle is a session-scoped registry key, so a
610
+ * paint carrying one cannot be persisted or exported; prefer the spec.
611
+ *
612
+ * `units` on a pattern names the space the tile's origin and scale live in,
613
+ * not the space of any geometry (a pattern has none). Under `'bounds'` the
614
+ * tile anchors to the painted node's box, so the pattern travels with the
615
+ * node and a resize reveals more tiles rather than stretching them;
616
+ * `fillInPoseFrame` resolves that to a `'local'` paint with an explicit
617
+ * `origin` before the renderer sees it.
611
618
  */
612
619
  type FillStyle = {
613
620
  fill?: 'solid';
@@ -615,7 +622,12 @@ type FillStyle = {
615
622
  opacity?: number;
616
623
  } | {
617
624
  fill: 'pattern';
618
- pattern: TextureHandle;
625
+ pattern: TextureHandle | TilePatternSpec;
626
+ units?: GradientUnits;
627
+ origin?: {
628
+ x: number;
629
+ y: number;
630
+ };
619
631
  opacity?: number;
620
632
  } | {
621
633
  fill: 'linear-gradient';
@@ -628,6 +640,7 @@ type FillStyle = {
628
640
  y: number;
629
641
  };
630
642
  stops: GradStop[];
643
+ units?: GradientUnits;
631
644
  opacity?: number;
632
645
  } | {
633
646
  fill: 'radial-gradient';
@@ -637,6 +650,7 @@ type FillStyle = {
637
650
  };
638
651
  radius: number;
639
652
  stops: GradStop[];
653
+ units?: GradientUnits;
640
654
  opacity?: number;
641
655
  } | {
642
656
  fill: 'conic-gradient';
@@ -646,8 +660,56 @@ type FillStyle = {
646
660
  };
647
661
  angle: number;
648
662
  stops: GradStop[];
663
+ units?: GradientUnits;
649
664
  opacity?: number;
650
665
  };
666
+ /**
667
+ * Which coordinate space a gradient's geometry (`from`/`to`, `center`,
668
+ * `radius`) is expressed in. SVG's `gradientUnits`, plus an option for
669
+ * paints that are themselves viewport furniture.
670
+ *
671
+ * - `'bounds'`: fractions of the painted node's bounding box, `0..1` on each
672
+ * axis — SVG `objectBoundingBox`. Resolved by the node painter, before the
673
+ * renderer sees it, so the paint follows the node through moves, resizes
674
+ * and rotation. What a gradient on a scene node wants.
675
+ * - `'local'`: the coordinate space the geometry was handed to the renderer
676
+ * in — the enclosing group's frame. For draw commands a consumer builds
677
+ * itself, where "the coordinates I just wrote" is the useful frame.
678
+ * - `'world'`: scene coordinates. The paint stays put under pan and zoom
679
+ * while the geometry moves through it. SVG `userSpaceOnUse`. Requires the
680
+ * renderer to have been handed a view matrix; falls back to `'screen'`
681
+ * when it has not.
682
+ * - `'screen'`: CSS pixels of the drawing surface. For overlays and
683
+ * viewport-fixed washes that should not move with the content at all.
684
+ *
685
+ * Defaults to `'screen'`, which is the behavior every gradient had before
686
+ * this field existed.
687
+ */
688
+ type GradientUnits = 'bounds' | 'local' | 'world' | 'screen';
689
+ /**
690
+ * A built-in tile, described as plain data. The serializable half of the
691
+ * pattern paint: `resolvePatternSpec()` turns one into a `TextureHandle`
692
+ * at paint time, memoized so identical specs share a texture.
693
+ *
694
+ * `size` is the tile's edge length, and doubles as its extent in paint
695
+ * space — a bigger `size` rasterizes a bigger tile rather than magnifying
696
+ * a small one, which is why there is no separate scale field.
697
+ */
698
+ interface TilePatternSpec {
699
+ tile: 'hatch' | 'crosshatch' | 'dots' | 'chunks';
700
+ color: string;
701
+ /** `chunks` only — omit for a transparent tile background. */
702
+ bg?: string;
703
+ size?: number;
704
+ /** `hatch` / `crosshatch`. */
705
+ lineWidth?: number;
706
+ /** `dots`. */
707
+ radius?: number;
708
+ /** `chunks`. */
709
+ density?: number;
710
+ chunkSize?: number;
711
+ seed?: number;
712
+ }
651
713
  /** A single color stop within a gradient. `offset` is in 0..1. */
652
714
  interface GradStop {
653
715
  offset: number;
@@ -932,6 +994,10 @@ interface ImageDrawCommand {
932
994
  w: number;
933
995
  h: number;
934
996
  opacity?: number;
997
+ /** Magnification filter. `'linear'` (default) smooths; `'nearest'` shows
998
+ * device pixels as hard squares — required by anything magnifying a
999
+ * framebuffer readback, where blur destroys the point of the readback. */
1000
+ sampling?: 'linear' | 'nearest';
935
1001
  }
936
1002
  /**
937
1003
  * Custom shader draw command. The renderer generates a quad over `bounds`
@@ -29,10 +29,12 @@ interface SavedSnapshot {
29
29
  state: unknown;
30
30
  savedAt: number;
31
31
  }
32
+ /** `auto` follows the OS; the other two are an explicit choice. */
33
+ type LabMode = 'auto' | 'light' | 'dark';
32
34
  interface LabStoreState {
33
35
  workspaces: WorkspaceRecord[];
34
36
  savedSnapshots: SavedSnapshot[];
35
- theme: 'light' | 'interstellar' | 'auto';
37
+ mode: LabMode;
36
38
  }
37
39
  interface StorageAdapter {
38
40
  read(key: string): string | null;
@@ -48,7 +50,7 @@ interface ExperimentStateHandle<TS, TC> {
48
50
  interface CreateLabStoreOptions {
49
51
  storageKey: string;
50
52
  storage: StorageAdapter;
51
- initialTheme?: 'light' | 'interstellar' | 'auto';
53
+ initialMode?: LabMode;
52
54
  }
53
55
  type InstrumentSerializers = Record<string, {
54
56
  serialize?: (state: unknown) => unknown;
@@ -73,7 +75,7 @@ interface LabStoreActions {
73
75
  loadSnapshot: (snapshotId: string, workspaceId: string) => void;
74
76
  deleteSnapshot: (snapshotId: string) => void;
75
77
  listSnapshots: (workspaceId?: string) => SavedSnapshot[];
76
- setTheme: (theme: 'light' | 'interstellar' | 'auto') => void;
78
+ setMode: (mode: LabMode) => void;
77
79
  }
78
80
  type LabStore = StoreApi<LabStoreState & LabStoreActions> & {
79
81
  registerSerializers: (s: InstrumentSerializers) => void;
@@ -99,4 +101,4 @@ declare function useWorkspaceId(): string;
99
101
  declare function useExperimentState<TS = unknown, TC = unknown>(): ExperimentStateHandle<TS, TC>;
100
102
 
101
103
  export { LabStoreContext as b, LabStoreProvider as c, WorkspaceIdContext as f, WorkspaceIdProvider as g, createLabStore as h, createMemoryAdapter as i, useExperimentState as j, useLabStore as k, localStorageAdapter as l, useWorkspaceId as m, noneAdapter as n, sessionStorageAdapter as s, urlHashAdapter as u };
102
- export type { CreateLabStoreOptions as C, ExperimentStateHandle as E, InstrumentSerializers as I, LabStore as L, StorageAdapter as S, UndoStack as U, WorkspaceRecord as W, LabStoreActions as a, LabStoreState as d, SavedSnapshot as e };
104
+ export type { CreateLabStoreOptions as C, ExperimentStateHandle as E, InstrumentSerializers as I, LabStore as L, StorageAdapter as S, UndoStack as U, WorkspaceRecord as W, LabStoreActions as a, LabStoreState as d, SavedSnapshot as e, LabMode as o };
@@ -183,17 +183,18 @@ function createLabStore(options) {
183
183
  hydratedSnapshots = [];
184
184
  }
185
185
  }
186
- const themeRaw = options.storage.read(labStorageKey(options.storageKey, "theme"));
187
- let hydratedTheme;
188
- if (themeRaw === "light" || themeRaw === "interstellar" || themeRaw === "auto") {
189
- hydratedTheme = themeRaw;
186
+ const modeRaw = options.storage.read(labStorageKey(options.storageKey, "theme"));
187
+ const stored = modeRaw === "interstellar" ? "dark" : modeRaw;
188
+ let hydratedMode;
189
+ if (stored === "light" || stored === "dark" || stored === "auto") {
190
+ hydratedMode = stored;
190
191
  } else {
191
- hydratedTheme = options.initialTheme ?? "auto";
192
+ hydratedMode = options.initialMode ?? "auto";
192
193
  }
193
194
  const store = createStore()((set, get) => ({
194
195
  workspaces: hydratedWorkspaces,
195
196
  savedSnapshots: hydratedSnapshots,
196
- theme: hydratedTheme,
197
+ mode: hydratedMode,
197
198
  addWorkspace: (record) => {
198
199
  set((s) => ({
199
200
  workspaces: [...s.workspaces, { ...record, undoStack: emptyUndoStack() }]
@@ -300,8 +301,8 @@ function createLabStore(options) {
300
301
  const filtered = workspaceId ? all.filter((sn) => sn.workspaceId === workspaceId) : all;
301
302
  return [...filtered].sort((a, b) => b.savedAt - a.savedAt);
302
303
  },
303
- setTheme: (theme) => {
304
- set({ theme });
304
+ setMode: (mode) => {
305
+ set({ mode });
305
306
  scheduleFlush();
306
307
  }
307
308
  }));
@@ -317,7 +318,7 @@ function createLabStore(options) {
317
318
  labStorageKey(options.storageKey, "saves"),
318
319
  JSON.stringify(s.savedSnapshots)
319
320
  );
320
- options.storage.write(labStorageKey(options.storageKey, "theme"), s.theme);
321
+ options.storage.write(labStorageKey(options.storageKey, "theme"), s.mode);
321
322
  flushTimer = null;
322
323
  }, 300);
323
324
  }
@@ -346,5 +347,5 @@ function useExperimentState() {
346
347
  }
347
348
 
348
349
  export { LabStoreContext, LabStoreProvider, WorkspaceIdContext, WorkspaceIdProvider, createLabStore, createMemoryAdapter, decodeUrlHash, deserializeWorkspaces, emptyUndoStack, encodeUrlHash, labStorageKey, localStorageAdapter, noneAdapter, serializeWorkspaces, sessionStorageAdapter, urlHashAdapter, useExperimentState, useLabStore, useWorkspaceId };
349
- //# sourceMappingURL=chunk-SSMHPMDV.js.map
350
- //# sourceMappingURL=chunk-SSMHPMDV.js.map
350
+ //# sourceMappingURL=chunk-53XSBIUK.js.map
351
+ //# sourceMappingURL=chunk-53XSBIUK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/state/helpers.ts","../src/state/adapters.ts","../src/state/context.tsx","../src/state/store.ts","../src/state/useExperimentState.ts"],"names":["useContext","useStore"],"mappings":"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","file":"chunk-53XSBIUK.js","sourcesContent":["import type { InstrumentSerializers, UndoStack, WorkspaceRecord } from './types';\n\nexport function labStorageKey(\n storageKey: string,\n bucket: 'workspaces' | 'saves' | 'theme',\n): string {\n return `lk:${storageKey}:${bucket}`;\n}\n\nexport function encodeUrlHash(value: string): string {\n return btoa(encodeURIComponent(value));\n}\n\nexport function decodeUrlHash(hash: string): string | null {\n if (!hash) return null;\n try {\n return decodeURIComponent(atob(hash));\n } catch {\n return null;\n }\n}\n\nexport function emptyUndoStack(): UndoStack {\n return { past: [], future: [] };\n}\n\ntype SerializedRecord = Omit<WorkspaceRecord, 'undoStack'>;\n\nexport function serializeWorkspaces(\n workspaces: WorkspaceRecord[],\n serializers: InstrumentSerializers,\n): string {\n const records: SerializedRecord[] = workspaces.map(({ undoStack: _undo, ...w }) => {\n const s = serializers[w.instrumentName];\n return {\n ...w,\n state: s?.serialize ? s.serialize(w.state) : w.state,\n };\n });\n return JSON.stringify(records);\n}\n\nexport function deserializeWorkspaces(\n raw: string,\n deserializers: InstrumentSerializers,\n): WorkspaceRecord[] {\n try {\n const records = JSON.parse(raw) as SerializedRecord[];\n return records.map((r) => {\n const d = deserializers[r.instrumentName];\n return {\n ...r,\n state: d?.deserialize ? d.deserialize(r.state) : r.state,\n undoStack: emptyUndoStack(),\n };\n });\n } catch {\n console.warn('[labkit] deserializeWorkspaces: failed to parse, returning empty list');\n return [];\n }\n}\n","import { decodeUrlHash, encodeUrlHash } from './helpers';\nimport type { StorageAdapter } from './types';\n\nexport const localStorageAdapter: StorageAdapter = {\n read: (key) => {\n try {\n return localStorage.getItem(key);\n } catch {\n return null;\n }\n },\n write: (key, value) => {\n try {\n localStorage.setItem(key, value);\n } catch (e) {\n console.warn('[labkit] localStorage write failed:', e);\n }\n },\n delete: (key) => {\n try {\n localStorage.removeItem(key);\n } catch {\n /* ignore */\n }\n },\n};\n\nexport const sessionStorageAdapter: StorageAdapter = {\n read: (key) => {\n try {\n return sessionStorage.getItem(key);\n } catch {\n return null;\n }\n },\n write: (key, value) => {\n try {\n sessionStorage.setItem(key, value);\n } catch (e) {\n console.warn('[labkit] sessionStorage write failed:', e);\n }\n },\n delete: (key) => {\n try {\n sessionStorage.removeItem(key);\n } catch {\n /* ignore */\n }\n },\n};\n\nconst URL_HASH_GUARD = typeof window !== 'undefined';\n\nfunction readHashMap(): Record<string, string> {\n if (!URL_HASH_GUARD) return {};\n const raw = decodeUrlHash(window.location.hash.replace(/^#/, ''));\n if (!raw) return {};\n try {\n return JSON.parse(raw) as Record<string, string>;\n } catch {\n return {};\n }\n}\n\nfunction writeHashMap(map: Record<string, string>): void {\n if (!URL_HASH_GUARD) return;\n const encoded = encodeUrlHash(JSON.stringify(map));\n window.history.replaceState(null, '', `#${encoded}`);\n}\n\nexport const urlHashAdapter: StorageAdapter = {\n read: (key) => readHashMap()[key] ?? null,\n write: (key, value) => {\n const map = readHashMap();\n map[key] = value;\n writeHashMap(map);\n },\n delete: (key) => {\n const map = readHashMap();\n delete map[key];\n writeHashMap(map);\n },\n};\n\nexport function createMemoryAdapter(): StorageAdapter {\n const store = new Map<string, string>();\n return {\n read: (key) => store.get(key) ?? null,\n write: (key, value) => {\n store.set(key, value);\n },\n delete: (key) => {\n store.delete(key);\n },\n };\n}\n\nexport const noneAdapter: StorageAdapter = {\n read: () => null,\n write: () => {},\n delete: () => {},\n};\n","import { createContext, type ReactElement, type ReactNode, useContext } from 'react';\nimport { useStore } from 'zustand/react';\nimport type { LabStore } from './store';\nimport type { LabStoreState } from './types';\n\ntype LabStoreCtx = { store: LabStore } | null;\n\nexport const LabStoreContext = createContext<LabStoreCtx>(null);\n\nexport function LabStoreProvider({\n store,\n children,\n}: {\n store: LabStore;\n children: ReactNode;\n}): ReactElement {\n return <LabStoreContext.Provider value={{ store }}>{children}</LabStoreContext.Provider>;\n}\n\nexport function useLabStore(): LabStoreState & ReturnType<LabStore['getState']> {\n const ctx = useContext(LabStoreContext);\n if (!ctx) throw new Error('[labkit] useLabStore must be used inside <LabStoreProvider>');\n return useStore(ctx.store);\n}\n\nexport const WorkspaceIdContext = createContext<string | null>(null);\n\nexport function WorkspaceIdProvider({\n workspaceId,\n children,\n}: {\n workspaceId: string;\n children: ReactNode;\n}): ReactElement {\n return <WorkspaceIdContext.Provider value={workspaceId}>{children}</WorkspaceIdContext.Provider>;\n}\n\nexport function useWorkspaceId(): string {\n const id = useContext(WorkspaceIdContext);\n if (!id) throw new Error('[labkit] useWorkspaceId must be used inside <WorkspaceIdProvider>');\n return id;\n}\n","import { createStore, type StoreApi } from 'zustand/vanilla';\nimport {\n deserializeWorkspaces,\n emptyUndoStack,\n labStorageKey,\n serializeWorkspaces,\n} from './helpers';\nimport type {\n CreateLabStoreOptions,\n InstrumentSerializers,\n LabMode,\n LabStoreState,\n SavedSnapshot,\n WorkspaceRecord,\n} from './types';\n\nexport interface LabStoreActions {\n addWorkspace: (record: Omit<WorkspaceRecord, 'undoStack'>) => void;\n removeWorkspace: (id: string) => void;\n updateWorkspaceState: <TS>(id: string, next: TS | ((prev: TS) => TS)) => void;\n updateWorkspaceConfig: <TC>(id: string, key: keyof TC, value: TC[keyof TC]) => void;\n updateWorkspaceView: (id: string, view: WorkspaceRecord['view']) => void;\n updateWorkspaceUndoStack: (\n id: string,\n next:\n | WorkspaceRecord['undoStack']\n | ((prev: WorkspaceRecord['undoStack']) => WorkspaceRecord['undoStack']),\n ) => void;\n setWorkspaceInstrument: (id: string, instrumentName: string) => void;\n saveSnapshot: (workspaceId: string, name: string) => void;\n loadSnapshot: (snapshotId: string, workspaceId: string) => void;\n deleteSnapshot: (snapshotId: string) => void;\n listSnapshots: (workspaceId?: string) => SavedSnapshot[];\n setMode: (mode: LabMode) => void;\n}\n\nexport type LabStore = StoreApi<LabStoreState & LabStoreActions> & {\n registerSerializers: (s: InstrumentSerializers) => void;\n};\n\nexport function createLabStore(options: CreateLabStoreOptions): LabStore {\n let serializers: InstrumentSerializers = {};\n let flushTimer: ReturnType<typeof setTimeout> | null = null;\n\n const workspacesRaw = options.storage.read(labStorageKey(options.storageKey, 'workspaces'));\n let hydratedWorkspaces: WorkspaceRecord[] = [];\n if (workspacesRaw) {\n hydratedWorkspaces = deserializeWorkspaces(workspacesRaw, serializers);\n }\n\n const savesRaw = options.storage.read(labStorageKey(options.storageKey, 'saves'));\n let hydratedSnapshots: SavedSnapshot[] = [];\n if (savesRaw) {\n try {\n hydratedSnapshots = JSON.parse(savesRaw) as SavedSnapshot[];\n } catch {\n console.warn('[labkit] failed to parse saved snapshots, starting empty');\n hydratedSnapshots = [];\n }\n }\n\n const modeRaw = options.storage.read(labStorageKey(options.storageKey, 'theme'));\n // `interstellar` was the dark mode's name back when it was a theme.\n const stored = modeRaw === 'interstellar' ? 'dark' : modeRaw;\n let hydratedMode: LabMode;\n if (stored === 'light' || stored === 'dark' || stored === 'auto') {\n hydratedMode = stored;\n } else {\n hydratedMode = options.initialMode ?? 'auto';\n }\n\n const store = createStore<LabStoreState & LabStoreActions>()((set, get) => ({\n workspaces: hydratedWorkspaces,\n savedSnapshots: hydratedSnapshots,\n mode: hydratedMode,\n\n addWorkspace: (record) => {\n set((s) => ({\n workspaces: [...s.workspaces, { ...record, undoStack: emptyUndoStack() }],\n }));\n scheduleFlush();\n },\n\n removeWorkspace: (id) => {\n set((s) => ({ workspaces: s.workspaces.filter((w) => w.id !== id) }));\n scheduleFlush();\n },\n\n updateWorkspaceState: (id, next) => {\n set((s) => ({\n workspaces: s.workspaces.map((w) => {\n if (w.id !== id) return w;\n const nextState =\n typeof next === 'function' ? (next as (prev: unknown) => unknown)(w.state) : next;\n return { ...w, state: nextState };\n }),\n }));\n scheduleFlush();\n },\n\n updateWorkspaceConfig: (id, key, value) => {\n set((s) => ({\n workspaces: s.workspaces.map((w) => {\n if (w.id !== id) return w;\n return {\n ...w,\n config: { ...(w.config as Record<string, unknown>), [key as string]: value },\n };\n }),\n }));\n scheduleFlush();\n },\n\n updateWorkspaceView: (id, view) => {\n set((s) => ({\n workspaces: s.workspaces.map((w) => (w.id === id ? { ...w, view } : w)),\n }));\n scheduleFlush();\n },\n\n updateWorkspaceUndoStack: (id, next) => {\n set((s) => ({\n workspaces: s.workspaces.map((w) => {\n if (w.id !== id) return w;\n const undoStack =\n typeof next === 'function'\n ? (next as (prev: WorkspaceRecord['undoStack']) => WorkspaceRecord['undoStack'])(\n w.undoStack,\n )\n : next;\n return { ...w, undoStack };\n }),\n }));\n },\n\n setWorkspaceInstrument: (id, instrumentName) => {\n set((s) => ({\n workspaces: s.workspaces.map((w) => (w.id === id ? { ...w, instrumentName } : w)),\n }));\n scheduleFlush();\n },\n\n saveSnapshot: (workspaceId, name) => {\n const workspace = get().workspaces.find((w) => w.id === workspaceId);\n if (!workspace) return;\n const reg = serializers[workspace.instrumentName];\n const serializedState = reg?.serialize\n ? reg.serialize(workspace.state)\n : structuredClone(workspace.state);\n const clonedConfig = structuredClone(workspace.config);\n const lastAt = get().savedSnapshots.reduce((m, sn) => (sn.savedAt > m ? sn.savedAt : m), 0);\n const savedAt = Math.max(Date.now(), lastAt + 1);\n const snapshot: SavedSnapshot = {\n id: crypto.randomUUID(),\n name,\n workspaceId,\n instrumentName: workspace.instrumentName,\n config: clonedConfig,\n state: serializedState,\n savedAt,\n };\n set((s) => ({ savedSnapshots: [...s.savedSnapshots, snapshot] }));\n scheduleFlush();\n },\n\n loadSnapshot: (snapshotId, workspaceId) => {\n const snapshot = get().savedSnapshots.find((sn) => sn.id === snapshotId);\n if (!snapshot) return;\n const workspace = get().workspaces.find((w) => w.id === workspaceId);\n if (!workspace) return;\n if (snapshot.instrumentName !== workspace.instrumentName) {\n console.warn(\n `[labkit] loadSnapshot: instrument mismatch (snapshot=${snapshot.instrumentName}, workspace=${workspace.instrumentName}); refusing to load`,\n );\n return;\n }\n const reg = serializers[snapshot.instrumentName];\n const restoredState = reg?.deserialize ? reg.deserialize(snapshot.state) : snapshot.state;\n set((s) => ({\n workspaces: s.workspaces.map((w) =>\n w.id === workspaceId ? { ...w, state: restoredState, config: snapshot.config } : w,\n ),\n }));\n scheduleFlush();\n },\n\n deleteSnapshot: (snapshotId) => {\n set((s) => ({\n savedSnapshots: s.savedSnapshots.filter((sn) => sn.id !== snapshotId),\n }));\n scheduleFlush();\n },\n\n listSnapshots: (workspaceId) => {\n const all = get().savedSnapshots;\n const filtered = workspaceId ? all.filter((sn) => sn.workspaceId === workspaceId) : all;\n return [...filtered].sort((a, b) => b.savedAt - a.savedAt);\n },\n\n setMode: (mode) => {\n set({ mode });\n scheduleFlush();\n },\n }));\n\n function scheduleFlush(): void {\n if (flushTimer) clearTimeout(flushTimer);\n flushTimer = setTimeout(() => {\n const s = store.getState();\n options.storage.write(\n labStorageKey(options.storageKey, 'workspaces'),\n serializeWorkspaces(s.workspaces, serializers),\n );\n options.storage.write(\n labStorageKey(options.storageKey, 'saves'),\n JSON.stringify(s.savedSnapshots),\n );\n options.storage.write(labStorageKey(options.storageKey, 'theme'), s.mode);\n flushTimer = null;\n }, 300);\n }\n\n return Object.assign(store, {\n registerSerializers(s: InstrumentSerializers) {\n serializers = s;\n },\n });\n}\n","import { useContext } from 'react';\nimport { useStore } from 'zustand/react';\nimport { LabStoreContext, WorkspaceIdContext } from './context';\nimport type { ExperimentStateHandle } from './types';\n\nexport function useExperimentState<TS = unknown, TC = unknown>(): ExperimentStateHandle<TS, TC> {\n const ctx = useContext(LabStoreContext);\n if (!ctx) throw new Error('[labkit] useExperimentState must be used inside <LabStoreProvider>');\n\n const workspaceId = useContext(WorkspaceIdContext);\n if (!workspaceId)\n throw new Error('[labkit] useExperimentState must be used inside <WorkspaceIdProvider>');\n\n const record = useStore(ctx.store, (s) => s.workspaces.find((w) => w.id === workspaceId));\n\n if (!record) throw new Error(`[labkit] No workspace found with id \"${workspaceId}\"`);\n\n const updateWorkspaceState = useStore(ctx.store, (s) => s.updateWorkspaceState);\n const updateWorkspaceConfig = useStore(ctx.store, (s) => s.updateWorkspaceConfig);\n\n return {\n state: record.state as TS,\n config: record.config as TC,\n setState: (next) =>\n updateWorkspaceState(workspaceId, next as Parameters<typeof updateWorkspaceState>[1]),\n setConfig: (key, value) => updateWorkspaceConfig(workspaceId, key as never, value as never),\n };\n}\n"]}