@weasel-js/labkit 1.4.4 → 1.5.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 (219) hide show
  1. package/README.md +13 -2
  2. package/dist/_dts/{CanvasStackContext-kTEZvEgE.d.ts → CanvasStackContext-CqkxkFJW.d.ts} +1 -1
  3. package/dist/_dts/ToggleBar.d-DbHoWKIA.d.ts +116 -0
  4. package/dist/_dts/{frac-z7ker2Vx.d.ts → frac-8R6UmlvN.d.ts} +253 -31
  5. package/dist/_dts/index-B8uu2ba7.d.ts +237 -0
  6. package/dist/_dts/{index-Blv9uQzu.d.ts → index-retnAut7.d.ts} +18 -10
  7. package/dist/_dts/{types-B9_zrHmb.d.ts → types-Ca2LCnPq.d.ts} +65 -2
  8. package/dist/_dts/usePanZoom-nS798rOF.d.ts +160 -0
  9. package/dist/_dts/useTrialState-jcx_TdU6.d.ts +212 -0
  10. package/dist/canvas/index.d.ts +17 -128
  11. package/dist/canvas/index.js +4 -2
  12. package/dist/chrome/index.d.ts +4 -92
  13. package/dist/chrome/index.js +9 -5
  14. package/dist/chunk-67SJLMC7.js +704 -0
  15. package/dist/chunk-67SJLMC7.js.map +1 -0
  16. package/dist/chunk-6ZDGOZQV.js +310 -0
  17. package/dist/chunk-6ZDGOZQV.js.map +1 -0
  18. package/dist/{chunk-MOM3GOVY.js → chunk-EKIICY6X.js} +111 -25
  19. package/dist/chunk-EKIICY6X.js.map +1 -0
  20. package/dist/{chunk-CTRKTLYZ.js → chunk-H6ZAOWNE.js} +11 -11
  21. package/dist/chunk-H6ZAOWNE.js.map +1 -0
  22. package/dist/{chunk-SMHP6XZ4.js → chunk-KJALCDWE.js} +9 -7
  23. package/dist/chunk-KJALCDWE.js.map +1 -0
  24. package/dist/{chunk-BKFVHKJH.js → chunk-MUKOW3TC.js} +3 -3
  25. package/dist/{chunk-BKFVHKJH.js.map → chunk-MUKOW3TC.js.map} +1 -1
  26. package/dist/chunk-NDRYLVOW.js +15 -0
  27. package/dist/chunk-NDRYLVOW.js.map +1 -0
  28. package/dist/{chunk-64ZCN3DA.js → chunk-NS64DXMN.js} +226 -159
  29. package/dist/chunk-NS64DXMN.js.map +1 -0
  30. package/dist/{chunk-I6JCVE24.js → chunk-PGETSEDK.js} +135 -13
  31. package/dist/chunk-PGETSEDK.js.map +1 -0
  32. package/dist/{chunk-FQMJUVHJ.js → chunk-QLSV2N3G.js} +32 -37
  33. package/dist/chunk-QLSV2N3G.js.map +1 -0
  34. package/dist/chunk-RWNDDKOH.js +682 -0
  35. package/dist/chunk-RWNDDKOH.js.map +1 -0
  36. package/dist/chunk-S2SHZRD7.js +7681 -0
  37. package/dist/chunk-S2SHZRD7.js.map +1 -0
  38. package/dist/{chunk-U3IYHIAE.js → chunk-TQBBYTAF.js} +19 -82
  39. package/dist/chunk-TQBBYTAF.js.map +1 -0
  40. package/dist/chunk-UU3NJO6N.js +72 -0
  41. package/dist/chunk-UU3NJO6N.js.map +1 -0
  42. package/dist/chunk-WIG6XHJ7.js +35 -0
  43. package/dist/chunk-WIG6XHJ7.js.map +1 -0
  44. package/dist/{chunk-TJ7QY3OC.js → chunk-ZPH5WTWF.js} +21 -15
  45. package/dist/chunk-ZPH5WTWF.js.map +1 -0
  46. package/dist/config/index.d.ts +232 -0
  47. package/dist/config/index.js +6 -0
  48. package/dist/config/index.js.map +1 -0
  49. package/dist/controls/index.d.ts +2 -3
  50. package/dist/controls/index.js +3 -2
  51. package/dist/dragdrop/index.d.ts +2 -4
  52. package/dist/index.d.ts +110 -381
  53. package/dist/index.js +691 -817
  54. package/dist/index.js.map +1 -1
  55. package/dist/layers/index.d.ts +3 -5
  56. package/dist/layers/index.js +2 -2
  57. package/dist/loupe/index.d.ts +6 -8
  58. package/dist/loupe/index.js +2 -2
  59. package/dist/passthrough/weasel-ui.d.ts +164 -122
  60. package/dist/passthrough/weasel-ui.js +1 -1
  61. package/dist/primitives/index.d.ts +53 -6
  62. package/dist/primitives/index.js +6 -4
  63. package/dist/state/index.d.ts +12 -7
  64. package/dist/state/index.js +16 -13
  65. package/dist/state/index.js.map +1 -1
  66. package/dist/styles.css +84 -50
  67. package/dist/surface/index.d.ts +44 -7
  68. package/dist/surface/index.js +2 -1
  69. package/dist/ui/layers/index.js +1 -1
  70. package/dist/undo/index.d.ts +2 -4
  71. package/package.json +15 -8
  72. package/src/annotations/AnnotationOverlay.tsx +53 -5
  73. package/src/annotations/Annotations.overlay.test.tsx +85 -6
  74. package/src/annotations/drawOne.test.ts +1 -1
  75. package/src/annotations/drawOne.ts +2 -2
  76. package/src/annotations/paint.test.ts +2 -2
  77. package/src/annotations/paint.ts +1 -1
  78. package/src/annotations/preload.ts +6 -0
  79. package/src/annotations/store.test.ts +18 -0
  80. package/src/annotations/store.ts +5 -4
  81. package/src/annotations/svgNodes.test.ts +2 -2
  82. package/src/annotations/toolMap.ts +3 -3
  83. package/src/annotations/types.ts +6 -3
  84. package/src/canvas/CameraWheelContext.ts +12 -0
  85. package/src/canvas/CanvasStack.test.tsx +25 -0
  86. package/src/canvas/CanvasStack.tsx +31 -4
  87. package/src/canvas/Stage.less +18 -0
  88. package/src/canvas/Stage.test.tsx +186 -0
  89. package/src/canvas/Stage.tsx +134 -0
  90. package/src/canvas/camera.test.ts +17 -2
  91. package/src/canvas/camera.ts +9 -9
  92. package/src/canvas/index.ts +4 -0
  93. package/src/canvas/usePanZoom.ts +10 -5
  94. package/src/chrome/ChromeRegions.stories.tsx +0 -3
  95. package/src/chrome/LabChrome.tsx +114 -0
  96. package/src/chrome/builtins.tsx +20 -31
  97. package/src/chrome/index.ts +15 -0
  98. package/src/chrome/labRegions.test.tsx +208 -0
  99. package/src/chrome/labTypes.ts +50 -0
  100. package/src/chrome/merge.ts +7 -9
  101. package/src/chrome/regions/PaletteRegion.test.tsx +6 -4
  102. package/src/chrome/regions/PaletteRegion.tsx +21 -9
  103. package/src/chrome/regions/SidebarRegion.tsx +25 -13
  104. package/src/chrome/regions/StatusRegion.tsx +15 -8
  105. package/src/chrome/regions/ToolbarRegion.tsx +30 -16
  106. package/src/chrome/types.ts +47 -20
  107. package/src/config/builder.test.ts +4 -0
  108. package/src/config/builder.ts +16 -10
  109. package/src/config/declarationEmit.fixture.ts +11 -0
  110. package/src/config/declarationEmit.test.ts +51 -0
  111. package/src/config/entry.test.ts +46 -0
  112. package/src/config/index.ts +13 -1
  113. package/src/config/nodeClasses.test.ts +18 -0
  114. package/src/config/types.ts +3 -1
  115. package/src/controls/ControlPanel.test.tsx +16 -0
  116. package/src/controls/ControlPanel.tsx +5 -2
  117. package/src/fake-indexeddb-auto.d.ts +5 -0
  118. package/src/index.test.ts +7 -0
  119. package/src/index.ts +38 -5
  120. package/src/instrument/serializers.test.ts +22 -0
  121. package/src/instrument/serializers.ts +11 -0
  122. package/src/instrument/types.ts +25 -1
  123. package/src/lab/Lab.less +4 -13
  124. package/src/lab/Lab.persist.test.tsx +147 -0
  125. package/src/lab/Lab.stories.tsx +0 -2
  126. package/src/lab/Lab.surface.test.tsx +66 -3
  127. package/src/lab/Lab.test.tsx +91 -17
  128. package/src/lab/Lab.tsx +337 -124
  129. package/src/lab/LabFit.stories.less +18 -0
  130. package/src/lab/LabFit.stories.tsx +219 -0
  131. package/src/lab/LabFullChrome.stories.tsx +0 -3
  132. package/src/lab/LabHeader.test.tsx +40 -9
  133. package/src/lab/LabHeader.tsx +7 -13
  134. package/src/lab/LabPalette.tsx +13 -26
  135. package/src/lab/LabShell.less +49 -12
  136. package/src/lab/LabSwitcher.less +8 -1
  137. package/src/lab/Workspace.surface.test.tsx +2 -1
  138. package/src/lab/fitCheck.test.ts +57 -0
  139. package/src/lab/fitCheck.ts +125 -0
  140. package/src/loupe/AGENTS.md +1 -1
  141. package/src/loupe/Loupe.less +1 -1
  142. package/src/loupe/canvasLens.ts +1 -1
  143. package/src/loupe/types.ts +1 -1
  144. package/src/loupe/useLoupe.test.tsx +1 -1
  145. package/src/loupe/useLoupe.ts +1 -1
  146. package/src/passthrough/weasel-ui.ts +9 -0
  147. package/src/primitives/FloatingPanel.stories.tsx +13 -1
  148. package/src/primitives/FloatingPanel.test.tsx +47 -16
  149. package/src/primitives/FloatingPanel.tsx +11 -25
  150. package/src/primitives/Split.less +14 -0
  151. package/src/primitives/Split.test.tsx +108 -0
  152. package/src/primitives/Split.tsx +159 -0
  153. package/src/primitives/ZoomControl.tsx +5 -2
  154. package/src/primitives/index.ts +2 -0
  155. package/src/state/Persistence.stories.tsx +75 -0
  156. package/src/state/Persistence.tsx +33 -0
  157. package/src/state/SingletonExperiment.test.tsx +47 -53
  158. package/src/state/SingletonExperiment.tsx +24 -13
  159. package/src/state/adapterContract.ts +116 -0
  160. package/src/state/adapters.test.ts +113 -61
  161. package/src/state/adapters.ts +328 -69
  162. package/src/state/document.test.ts +71 -96
  163. package/src/state/document.ts +66 -52
  164. package/src/state/helpers.test.ts +1 -1
  165. package/src/state/index.ts +8 -0
  166. package/src/state/labRecords.test.ts +105 -0
  167. package/src/state/labRecords.ts +160 -0
  168. package/src/state/openLabStore.test.ts +658 -0
  169. package/src/state/openLabStore.ts +403 -0
  170. package/src/state/records.test.ts +234 -0
  171. package/src/state/records.ts +219 -0
  172. package/src/state/store.test.ts +104 -635
  173. package/src/state/store.ts +116 -157
  174. package/src/state/toolSlot.test.ts +1 -1
  175. package/src/state/types.ts +39 -15
  176. package/src/state/useOpenOnce.ts +68 -0
  177. package/src/state/usePersistedState.test.tsx +125 -0
  178. package/src/state/usePersistedState.ts +66 -0
  179. package/src/state/useTrialState.test.tsx +2 -3
  180. package/src/state/view.test.ts +49 -15
  181. package/src/state/view.ts +18 -1
  182. package/src/styles.less +2 -0
  183. package/src/surface/AGENTS.md +39 -7
  184. package/src/surface/SurfaceContext.ts +26 -3
  185. package/src/surface/index.ts +2 -0
  186. package/src/surface/useSurfaceTile.test.tsx +2 -1
  187. package/src/surface/useSurfaceTile.ts +6 -5
  188. package/src/surface/useTiledSurface.test.tsx +175 -0
  189. package/src/surface/useTiledSurface.ts +54 -5
  190. package/src/theme/Interstellar.stories.tsx +7 -7
  191. package/src/theme/interstellar.test.ts +1 -1
  192. package/src/theme/interstellar.tokens.json +2 -2
  193. package/src/tools/labTool.ts +11 -0
  194. package/src/trial/Trial.annotations.persist.test.tsx +100 -58
  195. package/src/trial/Trial.annotations.test.tsx +47 -4
  196. package/src/trial/Trial.job.test.tsx +3 -3
  197. package/src/trial/Trial.less +1 -26
  198. package/src/trial/Trial.stories.tsx +1 -2
  199. package/src/trial/Trial.test.tsx +28 -4
  200. package/src/trial/Trial.trialId.test.tsx +23 -1
  201. package/src/trial/Trial.tsx +118 -38
  202. package/src/trial/TrialBody.tsx +17 -141
  203. package/src/trial/TrialChrome.tsx +7 -4
  204. package/src/trial/trialOps.ts +5 -3
  205. package/dist/_dts/types-BP2OCcpg.d.ts +0 -155
  206. package/dist/_dts/types-x92Kfeme.d.ts +0 -62
  207. package/dist/_dts/useTrialState-D6Vb3T-g.d.ts +0 -99
  208. package/dist/chunk-2P6PP5N4.js +0 -553
  209. package/dist/chunk-2P6PP5N4.js.map +0 -1
  210. package/dist/chunk-64ZCN3DA.js.map +0 -1
  211. package/dist/chunk-CTRKTLYZ.js.map +0 -1
  212. package/dist/chunk-FQMJUVHJ.js.map +0 -1
  213. package/dist/chunk-I6JCVE24.js.map +0 -1
  214. package/dist/chunk-MOM3GOVY.js.map +0 -1
  215. package/dist/chunk-SMHP6XZ4.js.map +0 -1
  216. package/dist/chunk-TJ7QY3OC.js.map +0 -1
  217. package/dist/chunk-U3IYHIAE.js.map +0 -1
  218. package/dist/chunk-W3ECWC2K.js +0 -7335
  219. package/dist/chunk-W3ECWC2K.js.map +0 -1
@@ -0,0 +1,704 @@
1
+ import { newId, emptyUndoStack, deserializeTrials, openRecords, labPrefix, documentOfRecords, recordsOfDocument, trialRecord, trialValuesPrefix, saveRecord, LAYOUT_RECORD, UNDOCK_RECORD, META_RECORD, parseRecordName, compareTrialOrder, serializeTrials, labStorageKey } from './chunk-RWNDDKOH.js';
2
+ import { withValueAtPath, fillConfigDefaults } from './chunk-E44SU6XS.js';
3
+ import { LabStoreContext, TrialIdContext } from './chunk-2YILQ7Y5.js';
4
+ import { createStore } from 'zustand/vanilla';
5
+ import { useContext } from 'react';
6
+ import { useStore } from 'zustand/react';
7
+
8
+ // src/state/document.ts
9
+ var CURRENT_DOCUMENT_VERSION = 4;
10
+ function labDocumentKey(storageKey) {
11
+ return `lk:${storageKey}:doc`;
12
+ }
13
+ function quarantineKey(storageKey) {
14
+ return `lk:${storageKey}:quarantine`;
15
+ }
16
+ function emptyDocument(mode) {
17
+ return {
18
+ version: CURRENT_DOCUMENT_VERSION,
19
+ undockedPanels: {},
20
+ trials: [],
21
+ saves: [],
22
+ layout: {},
23
+ mode
24
+ };
25
+ }
26
+ function runMigrations(raw, migrations, target) {
27
+ const from = typeof raw.version === "number" ? raw.version : 0;
28
+ if (from > target) return { ok: false, reason: "future" };
29
+ if (from === target) return { ok: true, doc: raw, migrated: false };
30
+ let doc = raw;
31
+ try {
32
+ for (let v = from; v < target; v++) {
33
+ const migration = migrations[v];
34
+ if (!migration) throw new Error(`[labkit] no migration from version ${v}`);
35
+ doc = migration(doc);
36
+ }
37
+ } catch (error) {
38
+ return { ok: false, reason: "failed", error };
39
+ }
40
+ return { ok: true, doc, migrated: true };
41
+ }
42
+ var LEGACY_BUCKETS = ["workspaces", "saves", "layout", "theme"];
43
+ function legacyKeys(storageKey) {
44
+ return LEGACY_BUCKETS.map((bucket) => labStorageKey(storageKey, bucket));
45
+ }
46
+ function parseStored(value) {
47
+ if (typeof value !== "string") return value;
48
+ try {
49
+ return JSON.parse(value);
50
+ } catch {
51
+ return void 0;
52
+ }
53
+ }
54
+ function bucketOr(value, fallback, bucket, shape) {
55
+ if (value === void 0) return fallback;
56
+ const parsed = parseStored(value);
57
+ const fits = shape === "array" ? Array.isArray(parsed) : !!parsed && typeof parsed === "object";
58
+ if (fits) return parsed;
59
+ console.warn(`[labkit] unparseable legacy bucket "${bucket}", dropping it`);
60
+ return fallback;
61
+ }
62
+ async function readLegacyDocument(read, storageKey, fallbackMode) {
63
+ const [workspaces, saves, layout, theme] = await Promise.all(
64
+ LEGACY_BUCKETS.map((bucket) => read(labStorageKey(storageKey, bucket)))
65
+ );
66
+ if ([workspaces, saves, layout, theme].every((v) => v === void 0)) return null;
67
+ const mode = theme === "light" || theme === "dark" || theme === "auto" || theme === "interstellar" ? theme : fallbackMode;
68
+ return {
69
+ version: 0,
70
+ workspaces: bucketOr(workspaces, [], "workspaces", "array"),
71
+ saves: bucketOr(saves, [], "saves", "array"),
72
+ layout: bucketOr(layout, {}, "layout", "object"),
73
+ mode
74
+ };
75
+ }
76
+ function sameStored(a, b) {
77
+ return JSON.stringify(a) === JSON.stringify(b);
78
+ }
79
+ async function deleteConfirmed(storage, keys) {
80
+ try {
81
+ await Promise.all(keys.map((key) => storage.delete(key)));
82
+ const left = await Promise.all(keys.map((key) => storage.get(key)));
83
+ return left.every((v) => v === void 0);
84
+ } catch {
85
+ return false;
86
+ }
87
+ }
88
+ async function quarantineDocument(storage, storageKey, value) {
89
+ try {
90
+ await storage.set(quarantineKey(storageKey), value);
91
+ return sameStored(await storage.get(quarantineKey(storageKey)), value);
92
+ } catch {
93
+ return false;
94
+ }
95
+ }
96
+ function normalizeDocument(doc, fallbackMode) {
97
+ const mode = doc.mode === "light" || doc.mode === "dark" || doc.mode === "auto" ? doc.mode : fallbackMode;
98
+ return {
99
+ version: CURRENT_DOCUMENT_VERSION,
100
+ trials: Array.isArray(doc.trials) ? doc.trials : [],
101
+ saves: Array.isArray(doc.saves) ? doc.saves : [],
102
+ layout: doc.layout && typeof doc.layout === "object" ? doc.layout : {},
103
+ undockedPanels: doc.undockedPanels && typeof doc.undockedPanels === "object" ? doc.undockedPanels : {},
104
+ mode
105
+ };
106
+ }
107
+ function migrateV0toV1(doc) {
108
+ const raw = doc.mode === "interstellar" ? "dark" : doc.mode;
109
+ const mode = raw === "light" || raw === "dark" || raw === "auto" ? raw : "auto";
110
+ return {
111
+ version: 1,
112
+ workspaces: Array.isArray(doc.workspaces) ? doc.workspaces : [],
113
+ saves: Array.isArray(doc.saves) ? doc.saves : [],
114
+ layout: doc.layout && typeof doc.layout === "object" ? doc.layout : {},
115
+ mode
116
+ };
117
+ }
118
+ function migrateV1toV2(doc) {
119
+ const { workspaces, ...rest } = doc;
120
+ return { ...rest, trials: Array.isArray(workspaces) ? workspaces : [], version: 2 };
121
+ }
122
+ function migrateV2toV3(doc) {
123
+ return { ...doc, undockedPanels: {}, version: 3 };
124
+ }
125
+ function migrateV3toV4(doc) {
126
+ return { ...doc, version: 4 };
127
+ }
128
+ var MIGRATIONS = [migrateV0toV1, migrateV1toV2, migrateV2toV3, migrateV3toV4];
129
+
130
+ // src/state/undock.ts
131
+ function panelKey(trialId, sectionId) {
132
+ return `${trialId}::${sectionId}`;
133
+ }
134
+ function undockPanel(panels, trialId, sectionId, as = "tile") {
135
+ return { ...panels, [panelKey(trialId, sectionId)]: { trialId, sectionId, as } };
136
+ }
137
+ function dockPanel(panels, trialId, sectionId) {
138
+ const out = {};
139
+ for (const [key, panel] of Object.entries(panels)) {
140
+ if (panel.trialId === trialId && (sectionId === void 0 || panel.sectionId === sectionId)) {
141
+ continue;
142
+ }
143
+ out[key] = panel;
144
+ }
145
+ return out;
146
+ }
147
+
148
+ // src/instrument/serializers.ts
149
+ function configDefaultsOf(instruments) {
150
+ const out = {};
151
+ for (const instrument of instruments) out[instrument.name] = () => instrument.defaultConfig();
152
+ return out;
153
+ }
154
+ function configMigrationsOf(instruments) {
155
+ const out = {};
156
+ for (const { name, migrateConfig } of instruments) {
157
+ if (migrateConfig) out[name] = (stored) => migrateConfig(stored);
158
+ }
159
+ return out;
160
+ }
161
+ function serializersOf(instruments) {
162
+ const out = {};
163
+ for (const instrument of instruments) {
164
+ const { name, serialize, deserialize } = instrument;
165
+ if (!serialize && !deserialize) continue;
166
+ out[name] = {
167
+ ...serialize ? { serialize: (state) => serialize(state) } : {},
168
+ ...deserialize ? { deserialize: (data, config) => deserialize(data, config) } : {}
169
+ };
170
+ }
171
+ return out;
172
+ }
173
+
174
+ // src/state/store.ts
175
+ function createLabStore(options = {}) {
176
+ let hooks = options.instruments ? hooksOf(options.instruments) : {
177
+ serializers: options.serializers ?? {},
178
+ configDefaults: options.configDefaults ?? {},
179
+ configMigrations: options.configMigrations ?? {}
180
+ };
181
+ const initial = options.initial ?? emptyDocument(options.initialMode ?? "auto");
182
+ const store = createStore()((set, get) => ({
183
+ trials: hydrateTrials(initial.trials, hooks.serializers, hooks),
184
+ savedSnapshots: hydrateSnapshots(initial.saves, hooks),
185
+ mode: initial.mode,
186
+ layout: initial.layout,
187
+ undockedPanels: initial.undockedPanels,
188
+ activeToolId: null,
189
+ instruments: options.instruments ?? null,
190
+ addTrial: (record) => {
191
+ set((s) => ({
192
+ trials: [...s.trials, { ...record, undoStack: emptyUndoStack() }]
193
+ }));
194
+ },
195
+ removeTrial: (id) => {
196
+ set((s) => ({
197
+ trials: s.trials.filter((w) => w.id !== id),
198
+ undockedPanels: dockPanel(s.undockedPanels, id)
199
+ }));
200
+ },
201
+ updateTrialState: (id, next) => {
202
+ set((s) => ({
203
+ trials: s.trials.map((w) => {
204
+ if (w.id !== id) return w;
205
+ const nextState = typeof next === "function" ? next(w.state) : next;
206
+ if (Object.is(nextState, w.state)) return w;
207
+ return { ...w, state: nextState };
208
+ })
209
+ }));
210
+ },
211
+ updateTrialConfig: (id, path, value) => {
212
+ set((s) => ({
213
+ trials: s.trials.map(
214
+ (w) => w.id === id ? { ...w, config: withValueAtPath(w.config, path, value) } : w
215
+ )
216
+ }));
217
+ },
218
+ updateTrialView: (id, view) => {
219
+ set((s) => ({
220
+ trials: s.trials.map((w) => w.id === id && !Object.is(view, w.view) ? { ...w, view } : w)
221
+ }));
222
+ },
223
+ updateTrialSidebarWidth: (id, width) => {
224
+ set((s) => ({
225
+ trials: s.trials.map(
226
+ (w) => w.id === id && w.sidebarWidth !== width ? { ...w, sidebarWidth: width } : w
227
+ )
228
+ }));
229
+ },
230
+ setTrialTitle: (id, title) => {
231
+ set((s) => ({
232
+ trials: s.trials.map((w) => {
233
+ if (w.id !== id) return w;
234
+ const next = title === null ? void 0 : title;
235
+ if ((w.title ?? void 0) === next) return w;
236
+ return { ...w, title: next };
237
+ })
238
+ }));
239
+ },
240
+ setTrialSectionCollapsed: (id, key, collapsed) => {
241
+ set((s) => ({
242
+ trials: s.trials.map((w) => {
243
+ if (w.id !== id) return w;
244
+ if (w.collapsedSections?.[key] === collapsed) return w;
245
+ return { ...w, collapsedSections: { ...w.collapsedSections, [key]: collapsed } };
246
+ })
247
+ }));
248
+ },
249
+ updateTrialAnnotations: (id, doc) => {
250
+ set((s) => ({
251
+ trials: s.trials.map(
252
+ (w) => w.id === id && !Object.is(doc, w.annotations) ? { ...w, annotations: doc } : w
253
+ )
254
+ }));
255
+ },
256
+ updateTrialUndoStack: (id, next) => {
257
+ set((s) => ({
258
+ trials: s.trials.map((w) => {
259
+ if (w.id !== id) return w;
260
+ const undoStack = typeof next === "function" ? next(w.undoStack) : next;
261
+ return { ...w, undoStack };
262
+ })
263
+ }));
264
+ },
265
+ setTrialInstrument: (id, instrumentName) => {
266
+ set((s) => ({
267
+ trials: s.trials.map((w) => w.id === id ? { ...w, instrumentName } : w)
268
+ }));
269
+ },
270
+ saveSnapshot: (trialId, name) => {
271
+ const trial = get().trials.find((w) => w.id === trialId);
272
+ if (!trial) return;
273
+ const reg = hooks.serializers[trial.instrumentName];
274
+ const serializedState = reg?.serialize ? reg.serialize(trial.state) : structuredClone(trial.state);
275
+ const clonedConfig = structuredClone(trial.config);
276
+ const lastAt = get().savedSnapshots.reduce((m, sn) => sn.savedAt > m ? sn.savedAt : m, 0);
277
+ const savedAt = Math.max(Date.now(), lastAt + 1);
278
+ const snapshot = {
279
+ id: newId(),
280
+ name,
281
+ trialId,
282
+ instrumentName: trial.instrumentName,
283
+ config: clonedConfig,
284
+ state: serializedState,
285
+ savedAt
286
+ };
287
+ set((s) => ({ savedSnapshots: [...s.savedSnapshots, snapshot] }));
288
+ },
289
+ loadSnapshot: (snapshotId, trialId) => {
290
+ const snapshot = get().savedSnapshots.find((sn) => sn.id === snapshotId);
291
+ if (!snapshot) return;
292
+ const trial = get().trials.find((w) => w.id === trialId);
293
+ if (!trial) return;
294
+ if (snapshot.instrumentName !== trial.instrumentName) {
295
+ console.warn(
296
+ `[labkit] loadSnapshot: instrument mismatch (snapshot=${snapshot.instrumentName}, trial=${trial.instrumentName}); refusing to load`
297
+ );
298
+ return;
299
+ }
300
+ const reg = hooks.serializers[snapshot.instrumentName];
301
+ const restoredState = reg?.deserialize ? reg.deserialize(snapshot.state, snapshot.config) : snapshot.state;
302
+ set((s) => ({
303
+ trials: s.trials.map(
304
+ (w) => w.id === trialId ? { ...w, state: restoredState, config: snapshot.config } : w
305
+ )
306
+ }));
307
+ },
308
+ deleteSnapshot: (snapshotId) => {
309
+ set((s) => ({
310
+ savedSnapshots: s.savedSnapshots.filter((sn) => sn.id !== snapshotId)
311
+ }));
312
+ },
313
+ listSnapshots: (trialId) => {
314
+ const all = get().savedSnapshots;
315
+ const filtered = trialId ? all.filter((sn) => sn.trialId === trialId) : all;
316
+ return [...filtered].sort((a, b) => b.savedAt - a.savedAt);
317
+ },
318
+ setMode: (mode) => {
319
+ set({ mode });
320
+ },
321
+ setLabTool: (id) => set({ activeToolId: id }),
322
+ setTrialTool: (trialId, id) => set((s) => ({
323
+ trials: s.trials.map((t) => t.id === trialId ? { ...t, activeToolId: id } : t)
324
+ })),
325
+ undockPanel: (trialId, sectionId, as) => {
326
+ set((s) => ({ undockedPanels: undockPanel(s.undockedPanels, trialId, sectionId, as) }));
327
+ },
328
+ dockPanel: (trialId, sectionId) => {
329
+ set((s) => ({ undockedPanels: dockPanel(s.undockedPanels, trialId, sectionId) }));
330
+ },
331
+ setLayout: (layout) => {
332
+ set({ layout });
333
+ },
334
+ setInstruments: (instruments) => {
335
+ const before = get().instruments;
336
+ if (before === instruments) return;
337
+ const next = mergedHooks(hooks, instruments);
338
+ const previous = new Map((before ?? []).map((i) => [i.name, i]));
339
+ const changed = new Set(
340
+ instruments.filter((i) => previous.get(i.name) !== i).map((i) => i.name)
341
+ );
342
+ if (changed.size === 0) {
343
+ hooks = next;
344
+ set({ instruments });
345
+ return;
346
+ }
347
+ const s = get();
348
+ const trials = s.trials.map(
349
+ (w) => changed.has(w.instrumentName) ? { ...w, config: hydratedConfig(next, w.instrumentName, w.config) } : w
350
+ );
351
+ const savedSnapshots = s.savedSnapshots.map(
352
+ (sn) => changed.has(sn.instrumentName) ? { ...sn, config: hydratedConfig(next, sn.instrumentName, sn.config) } : sn
353
+ );
354
+ hooks = next;
355
+ set({ instruments, trials, savedSnapshots });
356
+ },
357
+ instrumentHooks: () => hooks
358
+ }));
359
+ return store;
360
+ }
361
+ function hooksOf(instruments) {
362
+ return {
363
+ serializers: serializersOf(instruments),
364
+ configDefaults: configDefaultsOf(instruments),
365
+ configMigrations: configMigrationsOf(instruments)
366
+ };
367
+ }
368
+ function mergedHooks(old, instruments) {
369
+ const listed = new Set(instruments.map((i) => i.name));
370
+ const fresh = hooksOf(instruments);
371
+ const unlisted = (entries) => Object.fromEntries(Object.entries(entries).filter(([name]) => !listed.has(name)));
372
+ return {
373
+ serializers: { ...unlisted(old.serializers), ...fresh.serializers },
374
+ configDefaults: { ...unlisted(old.configDefaults), ...fresh.configDefaults },
375
+ configMigrations: { ...unlisted(old.configMigrations), ...fresh.configMigrations }
376
+ };
377
+ }
378
+ function hydratedConfig(hydration, instrumentName, config) {
379
+ const migrate = hydration.configMigrations?.[instrumentName];
380
+ const moved = migrate ? migrate(config) : config;
381
+ const defaults = hydration.configDefaults?.[instrumentName];
382
+ return defaults ? fillConfigDefaults(moved, defaults()) : moved;
383
+ }
384
+ function hydrateTrials(trials, serializers, hydration = {}) {
385
+ return deserializeTrials(
386
+ trials.map((t) => ({ ...t, config: hydratedConfig(hydration, t.instrumentName, t.config) })),
387
+ serializers
388
+ );
389
+ }
390
+ function hydrateSnapshots(saves, hydration = {}) {
391
+ return saves.map((sn) => ({
392
+ ...sn,
393
+ config: hydratedConfig(hydration, sn.instrumentName, sn.config)
394
+ }));
395
+ }
396
+
397
+ // src/state/openLabStore.ts
398
+ async function openLabStore(options) {
399
+ const records = await openRecords({
400
+ storage: options.storage,
401
+ prefix: labPrefix(options.storageKey)
402
+ });
403
+ const { doc, orders } = await readLab(options, records);
404
+ const store = createLabStore({ ...options, initial: doc });
405
+ const unbind = bindLabStore(store, records, { orders });
406
+ return {
407
+ store,
408
+ records,
409
+ close: async () => {
410
+ unbind();
411
+ await records.close();
412
+ }
413
+ };
414
+ }
415
+ function emptyLab(mode) {
416
+ return { doc: emptyDocument(mode), orders: /* @__PURE__ */ new Map() };
417
+ }
418
+ function ordersOf(doc) {
419
+ return new Map(doc.trials.map((t, i) => [t.id, i]));
420
+ }
421
+ async function readLab(options, records) {
422
+ const mode = options.initialMode ?? "auto";
423
+ if (!records.writable) return emptyLab(mode);
424
+ const joined = documentOfRecords(records.entries());
425
+ if (!joined) return foldOldStorage(options, records);
426
+ const outcome = runMigrations(joined.doc, MIGRATIONS, CURRENT_DOCUMENT_VERSION);
427
+ if (outcome.ok) {
428
+ return { doc: normalizeDocument(outcome.doc, mode), orders: joined.orders };
429
+ }
430
+ const why = outcome.reason === "future" ? "are from a newer version of labkit" : "failed to migrate";
431
+ console.warn(`[labkit] lab records ${why}; starting empty and leaving them alone`, outcome.error);
432
+ records.stopWriting();
433
+ return emptyLab(mode);
434
+ }
435
+ async function foldOldStorage(options, records) {
436
+ const { storage, storageKey } = options;
437
+ const mode = options.initialMode ?? "auto";
438
+ let raw;
439
+ let oldKeys;
440
+ try {
441
+ const stored = await storage.get(labDocumentKey(storageKey));
442
+ if (stored !== void 0) {
443
+ raw = stored;
444
+ oldKeys = [labDocumentKey(storageKey)];
445
+ } else {
446
+ raw = await readLegacyDocument((key) => storage.get(key), storageKey, mode);
447
+ oldKeys = legacyKeys(storageKey);
448
+ if (raw === null) return emptyLab(mode);
449
+ }
450
+ } catch (error) {
451
+ console.warn(
452
+ `[labkit] could not read the old storage for "${storageKey}"; not persisting`,
453
+ error
454
+ );
455
+ records.stopWriting();
456
+ return emptyLab(mode);
457
+ }
458
+ const setAside = async (value, why, error) => {
459
+ if (await quarantineDocument(storage, storageKey, value)) {
460
+ console.warn(`[labkit] ${why}; quarantined it and starting empty`, error);
461
+ await deleteConfirmed(storage, oldKeys);
462
+ records.set(META_RECORD, { version: CURRENT_DOCUMENT_VERSION, mode });
463
+ } else {
464
+ console.warn(
465
+ `[labkit] ${why} and could not be quarantined; leaving it in place and not persisting`,
466
+ error
467
+ );
468
+ records.stopWriting();
469
+ }
470
+ return emptyLab(mode);
471
+ };
472
+ const parsed = parseStored(raw);
473
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
474
+ return setAside(raw, "lab document is unparseable");
475
+ }
476
+ const outcome = runMigrations(
477
+ parsed,
478
+ MIGRATIONS,
479
+ CURRENT_DOCUMENT_VERSION
480
+ );
481
+ if (!outcome.ok) {
482
+ if (outcome.reason === "future") {
483
+ console.warn(
484
+ "[labkit] lab document is from a newer version of labkit; starting empty and leaving it alone"
485
+ );
486
+ records.stopWriting();
487
+ return emptyLab(mode);
488
+ }
489
+ return setAside(parsed, "lab document failed to migrate", outcome.error);
490
+ }
491
+ const doc = normalizeDocument(outcome.doc, mode);
492
+ const entries = recordsOfDocument(doc);
493
+ records.seed(entries);
494
+ if (await writeConfirmed(storage, records.prefix, entries)) {
495
+ await deleteConfirmed(storage, oldKeys);
496
+ } else {
497
+ console.warn(
498
+ `[labkit] keeping the old storage for "${storageKey}": could not confirm its records were written`
499
+ );
500
+ }
501
+ return { doc, orders: ordersOf(doc) };
502
+ }
503
+ async function writeConfirmed(storage, prefix, entries) {
504
+ try {
505
+ for (const [name, value] of entries) await storage.set(prefix + name, value);
506
+ const back = new Map(await storage.list(prefix));
507
+ return entries.every(([name, value]) => sameStored(back.get(prefix + name), value));
508
+ } catch {
509
+ return false;
510
+ }
511
+ }
512
+ function persistedFieldsDiffer(a, b) {
513
+ if (a === b) return false;
514
+ const left = a;
515
+ const right = b;
516
+ for (const key of /* @__PURE__ */ new Set([...Object.keys(left), ...Object.keys(right)])) {
517
+ if (key !== "undoStack" && !Object.is(left[key], right[key])) return true;
518
+ }
519
+ return false;
520
+ }
521
+ function bindLabStore(store, records, options) {
522
+ const orderOf = new Map(options.orders);
523
+ let applyingRemote = false;
524
+ const nextOrder = () => {
525
+ let max = -1;
526
+ for (const order of orderOf.values()) max = Math.max(max, order);
527
+ return max + 1;
528
+ };
529
+ const writeTrial = (trial) => {
530
+ const [serialized] = serializeTrials([trial], store.getState().instrumentHooks().serializers);
531
+ records.set(trialRecord(trial.id), { ...serialized, order: orderOf.get(trial.id) });
532
+ };
533
+ const inStoredOrder = (trials) => {
534
+ for (let i = 1; i < trials.length; i++) {
535
+ const a = trials[i - 1];
536
+ const b = trials[i];
537
+ const at = { trial: a, order: orderOf.get(a.id) ?? 0 };
538
+ const bt = { trial: b, order: orderOf.get(b.id) ?? 0 };
539
+ if (compareTrialOrder(at, bt) >= 0) return false;
540
+ }
541
+ return true;
542
+ };
543
+ const offStore = store.subscribe((state, prev) => {
544
+ if (applyingRemote) return;
545
+ let wrote = false;
546
+ if (state.trials !== prev.trials) {
547
+ const before = new Map(prev.trials.map((t) => [t.id, t]));
548
+ const now = new Set(state.trials.map((t) => t.id));
549
+ for (const t of prev.trials) {
550
+ if (now.has(t.id)) continue;
551
+ records.delete(trialRecord(t.id));
552
+ for (const [name] of records.entries(trialValuesPrefix(t.id))) records.delete(name);
553
+ orderOf.delete(t.id);
554
+ wrote = true;
555
+ }
556
+ const added = state.trials.filter((t) => !before.has(t.id));
557
+ for (const t of added) orderOf.set(t.id, nextOrder());
558
+ if (inStoredOrder(state.trials)) {
559
+ for (const t of state.trials) {
560
+ const old = before.get(t.id);
561
+ if (old && !persistedFieldsDiffer(old, t)) continue;
562
+ writeTrial(t);
563
+ wrote = true;
564
+ }
565
+ } else {
566
+ for (const [i, t] of state.trials.entries()) orderOf.set(t.id, i);
567
+ for (const t of state.trials) writeTrial(t);
568
+ wrote = true;
569
+ }
570
+ }
571
+ if (state.savedSnapshots !== prev.savedSnapshots) {
572
+ const before = new Map(prev.savedSnapshots.map((s) => [s.id, s]));
573
+ const now = new Set(state.savedSnapshots.map((s) => s.id));
574
+ for (const s of state.savedSnapshots) {
575
+ if (before.get(s.id) === s) continue;
576
+ records.set(saveRecord(s.id), s);
577
+ wrote = true;
578
+ }
579
+ for (const s of prev.savedSnapshots) {
580
+ if (now.has(s.id)) continue;
581
+ records.delete(saveRecord(s.id));
582
+ wrote = true;
583
+ }
584
+ }
585
+ if (state.layout !== prev.layout) {
586
+ records.set(LAYOUT_RECORD, state.layout);
587
+ wrote = true;
588
+ }
589
+ if (state.undockedPanels !== prev.undockedPanels) {
590
+ records.set(UNDOCK_RECORD, state.undockedPanels);
591
+ wrote = true;
592
+ }
593
+ if (state.mode !== prev.mode || wrote && !records.has(META_RECORD)) {
594
+ records.set(META_RECORD, {
595
+ version: CURRENT_DOCUMENT_VERSION,
596
+ mode: state.mode
597
+ });
598
+ }
599
+ });
600
+ const offRecords = records.subscribe((changes) => {
601
+ const remote = changes.filter((c) => c.origin === "remote");
602
+ if (remote.length === 0) return;
603
+ const current = store.getState();
604
+ let trials = current.trials;
605
+ let saves = current.savedSnapshots;
606
+ const patch = {};
607
+ for (const { name, value } of remote) {
608
+ const parsed = parseRecordName(name);
609
+ if (!parsed) continue;
610
+ switch (parsed.kind) {
611
+ case "trial": {
612
+ const id = parsed.id;
613
+ if (value === void 0 || !value || typeof value !== "object") {
614
+ trials = trials.filter((t) => t.id !== id);
615
+ orderOf.delete(id);
616
+ break;
617
+ }
618
+ const { order, ...rest } = value;
619
+ orderOf.set(
620
+ id,
621
+ typeof order === "number" && Number.isFinite(order) ? order : nextOrder()
622
+ );
623
+ const hooks = store.getState().instrumentHooks();
624
+ const [replacement] = hydrateTrials([{ ...rest, id }], hooks.serializers, hooks);
625
+ if (!replacement) break;
626
+ trials = trials.some((t) => t.id === id) ? trials.map((t) => t.id === id ? replacement : t) : [...trials, replacement];
627
+ break;
628
+ }
629
+ case "save": {
630
+ const id = parsed.id;
631
+ if (value === void 0 || !value || typeof value !== "object") {
632
+ saves = saves.filter((s) => s.id !== id);
633
+ break;
634
+ }
635
+ const [snapshot] = hydrateSnapshots(
636
+ [{ ...value, id }],
637
+ store.getState().instrumentHooks()
638
+ );
639
+ if (!snapshot) break;
640
+ saves = saves.some((s) => s.id === id) ? saves.map((s) => s.id === id ? snapshot : s) : [...saves, snapshot];
641
+ break;
642
+ }
643
+ case "layout":
644
+ patch.layout = value ?? {};
645
+ break;
646
+ case "undock":
647
+ patch.undockedPanels = value ?? {};
648
+ break;
649
+ case "meta": {
650
+ const meta = value;
651
+ if (typeof meta?.version === "number" && meta.version > CURRENT_DOCUMENT_VERSION) {
652
+ console.warn(
653
+ "[labkit] another tab wrote this lab with a newer labkit; no longer persisting"
654
+ );
655
+ if ("stopWriting" in records) records.stopWriting();
656
+ break;
657
+ }
658
+ if (meta?.mode === "light" || meta?.mode === "dark" || meta?.mode === "auto") {
659
+ patch.mode = meta.mode;
660
+ }
661
+ break;
662
+ }
663
+ }
664
+ }
665
+ if (trials !== current.trials) {
666
+ trials = [...trials].sort(
667
+ (a, b) => compareTrialOrder(
668
+ { trial: a, order: orderOf.get(a.id) ?? 0 },
669
+ { trial: b, order: orderOf.get(b.id) ?? 0 }
670
+ )
671
+ );
672
+ }
673
+ applyingRemote = true;
674
+ try {
675
+ store.setState({ ...patch, trials, savedSnapshots: saves });
676
+ } finally {
677
+ applyingRemote = false;
678
+ }
679
+ });
680
+ return () => {
681
+ offStore();
682
+ offRecords();
683
+ };
684
+ }
685
+ function useTrialState() {
686
+ const ctx = useContext(LabStoreContext);
687
+ if (!ctx) throw new Error("[labkit] useTrialState must be used inside <LabStoreProvider>");
688
+ const trialId = useContext(TrialIdContext);
689
+ if (!trialId) throw new Error("[labkit] useTrialState must be used inside <TrialIdProvider>");
690
+ const record = useStore(ctx.store, (s) => s.trials.find((w) => w.id === trialId));
691
+ if (!record) throw new Error(`[labkit] No trial found with id "${trialId}"`);
692
+ const updateTrialState = useStore(ctx.store, (s) => s.updateTrialState);
693
+ const updateTrialConfig = useStore(ctx.store, (s) => s.updateTrialConfig);
694
+ return {
695
+ state: record.state,
696
+ config: record.config,
697
+ setState: (next) => updateTrialState(trialId, next),
698
+ setConfig: (path, value) => updateTrialConfig(trialId, path, value)
699
+ };
700
+ }
701
+
702
+ export { CURRENT_DOCUMENT_VERSION, createLabStore, dockPanel, labDocumentKey, openLabStore, panelKey, quarantineKey, undockPanel, useTrialState };
703
+ //# sourceMappingURL=chunk-67SJLMC7.js.map
704
+ //# sourceMappingURL=chunk-67SJLMC7.js.map