@weasel-js/labkit 1.4.3 → 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 (229) hide show
  1. package/README.md +13 -2
  2. package/dist/_dts/{CanvasStackContext-Dde8_1my.d.ts → CanvasStackContext-CqkxkFJW.d.ts} +1 -1
  3. package/dist/_dts/ToggleBar.d-DbHoWKIA.d.ts +116 -0
  4. package/dist/_dts/{frac-B5mB8LsJ.d.ts → frac-8R6UmlvN.d.ts} +256 -33
  5. package/dist/_dts/index-B8uu2ba7.d.ts +237 -0
  6. package/dist/_dts/{index-DelFfeV0.d.ts → index-retnAut7.d.ts} +45 -12
  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-QAGAHJKZ.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-2RQPGOQF.js → chunk-KJALCDWE.js} +9 -7
  23. package/dist/chunk-KJALCDWE.js.map +1 -0
  24. package/dist/{chunk-ISCOFAUO.js → chunk-MUKOW3TC.js} +3 -3
  25. package/dist/{chunk-ISCOFAUO.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-C42T6DJT.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-ASWKLRKJ.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 +123 -348
  53. package/dist/index.js +794 -837
  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 +169 -117
  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 +163 -62
  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/frac.test.ts +4 -4
  77. package/src/annotations/frac.ts +3 -2
  78. package/src/annotations/index.ts +1 -1
  79. package/src/annotations/paint.test.ts +2 -2
  80. package/src/annotations/paint.ts +1 -1
  81. package/src/annotations/preload.ts +6 -0
  82. package/src/annotations/store.test.ts +18 -0
  83. package/src/annotations/store.ts +7 -6
  84. package/src/annotations/svgNodes.test.ts +2 -2
  85. package/src/annotations/svgNodes.ts +1 -2
  86. package/src/annotations/toolMap.ts +3 -3
  87. package/src/annotations/types.ts +6 -3
  88. package/src/canvas/CameraWheelContext.ts +12 -0
  89. package/src/canvas/CanvasStack.test.tsx +25 -0
  90. package/src/canvas/CanvasStack.tsx +31 -4
  91. package/src/canvas/Stage.less +18 -0
  92. package/src/canvas/Stage.test.tsx +186 -0
  93. package/src/canvas/Stage.tsx +134 -0
  94. package/src/canvas/camera.test.ts +17 -2
  95. package/src/canvas/camera.ts +9 -9
  96. package/src/canvas/index.ts +4 -0
  97. package/src/canvas/usePanZoom.ts +10 -5
  98. package/src/chrome/ChromeRegions.stories.tsx +0 -3
  99. package/src/chrome/LabChrome.tsx +114 -0
  100. package/src/chrome/builtins.tsx +20 -31
  101. package/src/chrome/index.ts +15 -0
  102. package/src/chrome/labRegions.test.tsx +208 -0
  103. package/src/chrome/labTypes.ts +50 -0
  104. package/src/chrome/merge.ts +7 -9
  105. package/src/chrome/regions/PaletteRegion.test.tsx +6 -4
  106. package/src/chrome/regions/PaletteRegion.tsx +21 -9
  107. package/src/chrome/regions/SidebarRegion.tsx +25 -13
  108. package/src/chrome/regions/StatusRegion.tsx +15 -8
  109. package/src/chrome/regions/ToolbarRegion.tsx +30 -16
  110. package/src/chrome/types.ts +47 -20
  111. package/src/config/builder.test.ts +4 -0
  112. package/src/config/builder.ts +16 -10
  113. package/src/config/declarationEmit.fixture.ts +11 -0
  114. package/src/config/declarationEmit.test.ts +51 -0
  115. package/src/config/entry.test.ts +46 -0
  116. package/src/config/index.ts +13 -1
  117. package/src/config/nodeClasses.test.ts +18 -0
  118. package/src/config/types.ts +3 -1
  119. package/src/controls/ControlPanel.test.tsx +16 -0
  120. package/src/controls/ControlPanel.tsx +5 -2
  121. package/src/fake-indexeddb-auto.d.ts +5 -0
  122. package/src/index.test.ts +7 -0
  123. package/src/index.ts +39 -6
  124. package/src/instrument/serializers.test.ts +22 -0
  125. package/src/instrument/serializers.ts +11 -0
  126. package/src/instrument/types.ts +25 -1
  127. package/src/lab/Lab.less +4 -13
  128. package/src/lab/Lab.persist.test.tsx +147 -0
  129. package/src/lab/Lab.stories.tsx +0 -2
  130. package/src/lab/Lab.surface.test.tsx +66 -3
  131. package/src/lab/Lab.test.tsx +91 -17
  132. package/src/lab/Lab.tsx +337 -124
  133. package/src/lab/LabFit.stories.less +18 -0
  134. package/src/lab/LabFit.stories.tsx +219 -0
  135. package/src/lab/LabFullChrome.stories.tsx +0 -3
  136. package/src/lab/LabHeader.test.tsx +40 -9
  137. package/src/lab/LabHeader.tsx +7 -13
  138. package/src/lab/LabPalette.tsx +13 -26
  139. package/src/lab/LabShell.less +49 -12
  140. package/src/lab/LabShell.tsx +14 -2
  141. package/src/lab/LabSwitcher.less +92 -0
  142. package/src/lab/LabSwitcher.test.tsx +96 -0
  143. package/src/lab/LabSwitcher.tsx +112 -0
  144. package/src/lab/Workspace.surface.test.tsx +2 -1
  145. package/src/lab/fitCheck.test.ts +57 -0
  146. package/src/lab/fitCheck.ts +125 -0
  147. package/src/lab/index.ts +2 -0
  148. package/src/loupe/AGENTS.md +1 -1
  149. package/src/loupe/Loupe.less +1 -1
  150. package/src/loupe/canvasLens.ts +1 -1
  151. package/src/loupe/types.ts +1 -1
  152. package/src/loupe/useLoupe.test.tsx +1 -1
  153. package/src/loupe/useLoupe.ts +1 -1
  154. package/src/passthrough/weasel-ui.ts +9 -0
  155. package/src/primitives/FloatingPanel.stories.tsx +13 -1
  156. package/src/primitives/FloatingPanel.test.tsx +47 -16
  157. package/src/primitives/FloatingPanel.tsx +11 -25
  158. package/src/primitives/Split.less +14 -0
  159. package/src/primitives/Split.test.tsx +108 -0
  160. package/src/primitives/Split.tsx +159 -0
  161. package/src/primitives/ZoomControl.tsx +5 -2
  162. package/src/primitives/index.ts +2 -0
  163. package/src/state/Persistence.stories.tsx +75 -0
  164. package/src/state/Persistence.tsx +33 -0
  165. package/src/state/SingletonExperiment.test.tsx +47 -53
  166. package/src/state/SingletonExperiment.tsx +24 -13
  167. package/src/state/adapterContract.ts +116 -0
  168. package/src/state/adapters.test.ts +113 -61
  169. package/src/state/adapters.ts +328 -69
  170. package/src/state/document.test.ts +71 -96
  171. package/src/state/document.ts +66 -52
  172. package/src/state/helpers.test.ts +20 -0
  173. package/src/state/helpers.ts +16 -0
  174. package/src/state/index.ts +8 -0
  175. package/src/state/labRecords.test.ts +105 -0
  176. package/src/state/labRecords.ts +160 -0
  177. package/src/state/openLabStore.test.ts +658 -0
  178. package/src/state/openLabStore.ts +403 -0
  179. package/src/state/records.test.ts +234 -0
  180. package/src/state/records.ts +219 -0
  181. package/src/state/store.test.ts +104 -635
  182. package/src/state/store.ts +117 -158
  183. package/src/state/toolSlot.test.ts +1 -1
  184. package/src/state/types.ts +39 -15
  185. package/src/state/useOpenOnce.ts +68 -0
  186. package/src/state/usePersistedState.test.tsx +125 -0
  187. package/src/state/usePersistedState.ts +66 -0
  188. package/src/state/useTrialState.test.tsx +2 -3
  189. package/src/state/view.test.ts +49 -15
  190. package/src/state/view.ts +18 -1
  191. package/src/styles.less +3 -0
  192. package/src/surface/AGENTS.md +39 -7
  193. package/src/surface/SurfaceContext.ts +26 -3
  194. package/src/surface/index.ts +2 -0
  195. package/src/surface/useSurfaceTile.test.tsx +2 -1
  196. package/src/surface/useSurfaceTile.ts +6 -5
  197. package/src/surface/useTiledSurface.test.tsx +175 -0
  198. package/src/surface/useTiledSurface.ts +54 -5
  199. package/src/theme/Interstellar.stories.tsx +7 -7
  200. package/src/theme/base.less +4 -4
  201. package/src/theme/interstellar.test.ts +1 -1
  202. package/src/theme/interstellar.tokens.json +2 -2
  203. package/src/tools/labTool.ts +11 -0
  204. package/src/trial/Trial.annotations.persist.test.tsx +100 -58
  205. package/src/trial/Trial.annotations.test.tsx +47 -4
  206. package/src/trial/Trial.job.test.tsx +3 -3
  207. package/src/trial/Trial.less +1 -26
  208. package/src/trial/Trial.stories.tsx +1 -2
  209. package/src/trial/Trial.test.tsx +28 -4
  210. package/src/trial/Trial.trialId.test.tsx +23 -1
  211. package/src/trial/Trial.tsx +118 -38
  212. package/src/trial/TrialBody.tsx +17 -141
  213. package/src/trial/TrialChrome.tsx +7 -4
  214. package/src/trial/trialOps.ts +7 -4
  215. package/dist/_dts/types-BP2OCcpg.d.ts +0 -155
  216. package/dist/_dts/types-x92Kfeme.d.ts +0 -62
  217. package/dist/_dts/useTrialState-D6Vb3T-g.d.ts +0 -99
  218. package/dist/chunk-2RQPGOQF.js.map +0 -1
  219. package/dist/chunk-5TROQVQ2.js +0 -547
  220. package/dist/chunk-5TROQVQ2.js.map +0 -1
  221. package/dist/chunk-ASWKLRKJ.js.map +0 -1
  222. package/dist/chunk-C42T6DJT.js.map +0 -1
  223. package/dist/chunk-CTRKTLYZ.js.map +0 -1
  224. package/dist/chunk-FQMJUVHJ.js.map +0 -1
  225. package/dist/chunk-I6JCVE24.js.map +0 -1
  226. package/dist/chunk-MHAGC6VD.js +0 -7320
  227. package/dist/chunk-MHAGC6VD.js.map +0 -1
  228. package/dist/chunk-QAGAHJKZ.js.map +0 -1
  229. package/dist/chunk-TJ7QY3OC.js.map +0 -1
@@ -0,0 +1,682 @@
1
+ import { TrialIdContext } from './chunk-2YILQ7Y5.js';
2
+ import { createContext, useRef, useState, useEffect, useContext, useCallback, useSyncExternalStore } from 'react';
3
+ import { jsx, Fragment } from 'react/jsx-runtime';
4
+
5
+ // src/state/helpers.ts
6
+ function labStorageKey(storageKey, bucket) {
7
+ return `lk:${storageKey}:${bucket}`;
8
+ }
9
+ function encodeUrlHash(value) {
10
+ return btoa(encodeURIComponent(value));
11
+ }
12
+ function decodeUrlHash(hash) {
13
+ if (!hash) return null;
14
+ try {
15
+ return decodeURIComponent(atob(hash));
16
+ } catch {
17
+ return null;
18
+ }
19
+ }
20
+ var idCounter = 0;
21
+ function newId() {
22
+ const c = globalThis.crypto;
23
+ if (c && typeof c.randomUUID === "function") return c.randomUUID();
24
+ return `lk-${Date.now().toString(36)}-${idCounter++}`;
25
+ }
26
+ function emptyUndoStack() {
27
+ return { past: [], future: [] };
28
+ }
29
+ function serializeTrials(trials, serializers) {
30
+ return trials.map(({ undoStack: _undo, ...w }) => {
31
+ const s = serializers[w.instrumentName];
32
+ return { ...w, state: s?.serialize ? s.serialize(w.state) : w.state };
33
+ });
34
+ }
35
+ function deserializeTrials(records, deserializers) {
36
+ if (!Array.isArray(records)) {
37
+ console.warn("[labkit] deserializeTrials: not an array, returning empty list");
38
+ return [];
39
+ }
40
+ return records.map((r) => {
41
+ const d = deserializers[r.instrumentName];
42
+ return {
43
+ ...r,
44
+ state: d?.deserialize ? d.deserialize(r.state, r.config) : r.state,
45
+ undoStack: emptyUndoStack()
46
+ };
47
+ });
48
+ }
49
+
50
+ // src/state/adapters.ts
51
+ function parse(raw) {
52
+ try {
53
+ return JSON.parse(raw);
54
+ } catch {
55
+ return raw;
56
+ }
57
+ }
58
+ function isJsonSafe(value, seen = /* @__PURE__ */ new Set()) {
59
+ if (value === null) return true;
60
+ switch (typeof value) {
61
+ case "string":
62
+ case "boolean":
63
+ return true;
64
+ case "number":
65
+ return Number.isFinite(value);
66
+ case "object": {
67
+ if (seen.has(value)) return false;
68
+ seen.add(value);
69
+ const proto = Object.getPrototypeOf(value);
70
+ const ok = Array.isArray(value) ? value.every((v) => isJsonSafe(v, seen)) : (proto === Object.prototype || proto === null) && Object.values(value).every((v) => v === void 0 || isJsonSafe(v, seen));
71
+ seen.delete(value);
72
+ return ok;
73
+ }
74
+ default:
75
+ return false;
76
+ }
77
+ }
78
+ function webStorageAdapter(area, name, crossTab) {
79
+ const warned = /* @__PURE__ */ new Set();
80
+ const adapter = {
81
+ get: async (key) => {
82
+ const raw = area().getItem(key);
83
+ return raw === null ? void 0 : parse(raw);
84
+ },
85
+ list: async (prefix) => {
86
+ const store = area();
87
+ const out = [];
88
+ for (let i = 0; i < store.length; i++) {
89
+ const key = store.key(i);
90
+ if (key === null || !key.startsWith(prefix)) continue;
91
+ const raw = store.getItem(key);
92
+ if (raw !== null) out.push([key, parse(raw)]);
93
+ }
94
+ return out;
95
+ },
96
+ set: async (key, value) => {
97
+ if (!warned.has(key) && !isJsonSafe(value)) {
98
+ warned.add(key);
99
+ console.warn(
100
+ `[labkit] "${key}" holds a value JSON cannot keep exactly; ${name} stores what JSON keeps. Use IndexedDB for binary or non-JSON state.`
101
+ );
102
+ }
103
+ area().setItem(key, JSON.stringify(value));
104
+ },
105
+ delete: async (key) => {
106
+ area().removeItem(key);
107
+ }
108
+ };
109
+ if (crossTab) {
110
+ adapter.subscribe = (prefix, on) => {
111
+ if (typeof window === "undefined") return () => {
112
+ };
113
+ const handler = (e) => {
114
+ if (e.key === null || !e.key.startsWith(prefix)) return;
115
+ if (e.storageArea !== area()) return;
116
+ on([[e.key, e.newValue === null ? void 0 : parse(e.newValue)]]);
117
+ };
118
+ window.addEventListener("storage", handler);
119
+ return () => window.removeEventListener("storage", handler);
120
+ };
121
+ }
122
+ return adapter;
123
+ }
124
+ var localStorageAdapter = webStorageAdapter(
125
+ () => localStorage,
126
+ "localStorage",
127
+ true
128
+ );
129
+ var sessionStorageAdapter = webStorageAdapter(
130
+ () => sessionStorage,
131
+ "sessionStorage",
132
+ false
133
+ );
134
+ var URL_HASH_GUARD = typeof window !== "undefined";
135
+ function readHashMap() {
136
+ if (!URL_HASH_GUARD) return {};
137
+ const raw = decodeUrlHash(window.location.hash.replace(/^#/, ""));
138
+ if (!raw) return {};
139
+ try {
140
+ return JSON.parse(raw);
141
+ } catch {
142
+ return {};
143
+ }
144
+ }
145
+ var hashSubscribers = /* @__PURE__ */ new Set();
146
+ function writeHashMap(map) {
147
+ if (!URL_HASH_GUARD) return;
148
+ const encoded = encodeUrlHash(JSON.stringify(map));
149
+ window.history.replaceState(null, "", `#${encoded}`);
150
+ for (const sub of hashSubscribers) sub.last = { ...map };
151
+ }
152
+ var urlHashAdapter = {
153
+ get: async (key) => {
154
+ const raw = readHashMap()[key];
155
+ return raw === void 0 ? void 0 : parse(raw);
156
+ },
157
+ list: async (prefix) => Object.entries(readHashMap()).filter(([key]) => key.startsWith(prefix)).map(([key, raw]) => [key, parse(raw)]),
158
+ set: async (key, value) => {
159
+ const map = readHashMap();
160
+ map[key] = JSON.stringify(value);
161
+ writeHashMap(map);
162
+ },
163
+ delete: async (key) => {
164
+ const map = readHashMap();
165
+ delete map[key];
166
+ writeHashMap(map);
167
+ },
168
+ subscribe: (prefix, on) => {
169
+ if (!URL_HASH_GUARD) return () => {
170
+ };
171
+ const sub = { last: readHashMap() };
172
+ hashSubscribers.add(sub);
173
+ const handler = () => {
174
+ const next = readHashMap();
175
+ const changes = [];
176
+ for (const key of /* @__PURE__ */ new Set([...Object.keys(sub.last), ...Object.keys(next)])) {
177
+ if (!key.startsWith(prefix) || sub.last[key] === next[key]) continue;
178
+ const raw = next[key];
179
+ changes.push([key, raw === void 0 ? void 0 : parse(raw)]);
180
+ }
181
+ sub.last = next;
182
+ if (changes.length > 0) on(changes);
183
+ };
184
+ window.addEventListener("hashchange", handler);
185
+ return () => {
186
+ hashSubscribers.delete(sub);
187
+ window.removeEventListener("hashchange", handler);
188
+ };
189
+ }
190
+ };
191
+ var memorySubscribers = /* @__PURE__ */ new WeakMap();
192
+ function createMemoryAdapter(backing = /* @__PURE__ */ new Map()) {
193
+ let subscribers = memorySubscribers.get(backing);
194
+ if (!subscribers) {
195
+ subscribers = /* @__PURE__ */ new Set();
196
+ memorySubscribers.set(backing, subscribers);
197
+ }
198
+ const peers = subscribers;
199
+ const notify = (key, value) => {
200
+ for (const sub of peers) {
201
+ if (sub.owner === adapter || !key.startsWith(sub.prefix)) continue;
202
+ const copy = value === void 0 ? void 0 : structuredClone(value);
203
+ queueMicrotask(() => sub.on([[key, copy]]));
204
+ }
205
+ };
206
+ const adapter = {
207
+ get: async (key) => backing.has(key) ? structuredClone(backing.get(key)) : void 0,
208
+ list: async (prefix) => [...backing].filter(([key]) => key.startsWith(prefix)).map(([key, value]) => [key, structuredClone(value)]),
209
+ set: async (key, value) => {
210
+ const copy = structuredClone(value);
211
+ backing.set(key, copy);
212
+ notify(key, copy);
213
+ },
214
+ delete: async (key) => {
215
+ backing.delete(key);
216
+ notify(key, void 0);
217
+ },
218
+ subscribe: (prefix, on) => {
219
+ const sub = { owner: adapter, prefix, on };
220
+ peers.add(sub);
221
+ return () => {
222
+ peers.delete(sub);
223
+ };
224
+ }
225
+ };
226
+ return adapter;
227
+ }
228
+ var noneAdapter = {
229
+ get: async () => void 0,
230
+ list: async () => [],
231
+ set: async () => {
232
+ },
233
+ delete: async () => {
234
+ }
235
+ };
236
+ function createIndexedDbAdapter({
237
+ database = "labkit",
238
+ store = "records"
239
+ } = {}) {
240
+ let opened = null;
241
+ const open = () => {
242
+ opened ??= new Promise((resolve, reject) => {
243
+ if (typeof indexedDB === "undefined") {
244
+ reject(new Error("[labkit] IndexedDB is not available here"));
245
+ return;
246
+ }
247
+ const request = indexedDB.open(database, 1);
248
+ request.onupgradeneeded = () => {
249
+ if (!request.result.objectStoreNames.contains(store)) {
250
+ request.result.createObjectStore(store);
251
+ }
252
+ };
253
+ request.onsuccess = () => {
254
+ const db = request.result;
255
+ if (!db.objectStoreNames.contains(store)) {
256
+ reject(new Error(`[labkit] IndexedDB database "${database}" has no store "${store}"`));
257
+ return;
258
+ }
259
+ resolve(db);
260
+ };
261
+ request.onerror = () => reject(request.error);
262
+ request.onblocked = () => reject(new Error(`[labkit] IndexedDB "${database}" is blocked`));
263
+ });
264
+ return opened;
265
+ };
266
+ const run = (mode, body) => open().then(
267
+ (db) => new Promise((resolve, reject) => {
268
+ const tx = db.transaction(store, mode);
269
+ const result = body(tx.objectStore(store));
270
+ tx.oncomplete = () => resolve(result());
271
+ tx.onerror = () => reject(tx.error);
272
+ tx.onabort = () => reject(tx.error);
273
+ })
274
+ );
275
+ let channel = null;
276
+ const getChannel = () => {
277
+ if (typeof BroadcastChannel === "undefined") return null;
278
+ if (!channel) {
279
+ channel = new BroadcastChannel(`labkit:${database}:${store}`);
280
+ channel.unref?.();
281
+ }
282
+ return channel;
283
+ };
284
+ const announce = (changes) => {
285
+ getChannel()?.postMessage(changes);
286
+ };
287
+ const adapter = {
288
+ get: (key) => run("readonly", (s) => {
289
+ const request = s.get(key);
290
+ return () => request.result;
291
+ }),
292
+ list: (prefix) => run("readonly", (s) => {
293
+ const range = IDBKeyRange.bound(prefix, `${prefix}\uFFFF`);
294
+ const keys = s.getAllKeys(range);
295
+ const values = s.getAll(range);
296
+ return () => keys.result.map((key, i) => [String(key), values.result[i]]);
297
+ }),
298
+ set: async (key, value) => {
299
+ await run("readwrite", (s) => {
300
+ s.put(value, key);
301
+ return () => void 0;
302
+ });
303
+ announce([[key, value]]);
304
+ },
305
+ delete: async (key) => {
306
+ await run("readwrite", (s) => {
307
+ s.delete(key);
308
+ return () => void 0;
309
+ });
310
+ announce([[key, void 0]]);
311
+ }
312
+ };
313
+ if (typeof BroadcastChannel !== "undefined") {
314
+ adapter.subscribe = (prefix, on) => {
315
+ const ch = getChannel();
316
+ if (!ch) return () => {
317
+ };
318
+ const handler = (e) => {
319
+ const mine = e.data.filter(([key]) => key.startsWith(prefix));
320
+ if (mine.length > 0) on(mine);
321
+ };
322
+ ch.addEventListener("message", handler);
323
+ return () => ch.removeEventListener("message", handler);
324
+ };
325
+ }
326
+ return adapter;
327
+ }
328
+ var indexedDbAdapter = createIndexedDbAdapter();
329
+ var resolvedDefault = null;
330
+ function defaultStorage() {
331
+ resolvedDefault ??= indexedDbAdapter.list("\0").then(
332
+ () => indexedDbAdapter,
333
+ (error) => {
334
+ console.warn("[labkit] IndexedDB would not open; persisting to localStorage instead", error);
335
+ return localStorageAdapter;
336
+ }
337
+ );
338
+ return resolvedDefault;
339
+ }
340
+
341
+ // src/state/labRecords.ts
342
+ function labPrefix(storageKey) {
343
+ return `lk:${encodeURIComponent(storageKey)}:`;
344
+ }
345
+ var META_RECORD = "meta";
346
+ var LAYOUT_RECORD = "layout";
347
+ var UNDOCK_RECORD = "undock";
348
+ function trialRecord(id) {
349
+ return `trial:${encodeURIComponent(id)}`;
350
+ }
351
+ function saveRecord(id) {
352
+ return `save:${encodeURIComponent(id)}`;
353
+ }
354
+ function trialValuesPrefix(trialId) {
355
+ return `value:trial:${encodeURIComponent(trialId)}:`;
356
+ }
357
+ function valueRecord(trialId, name) {
358
+ const encoded = encodeURIComponent(name);
359
+ return trialId === null ? `value:lab:${encoded}` : `${trialValuesPrefix(trialId)}${encoded}`;
360
+ }
361
+ function parseRecordName(name) {
362
+ const parts = name.split(":");
363
+ try {
364
+ switch (parts[0]) {
365
+ case "meta":
366
+ case "layout":
367
+ case "undock":
368
+ return parts.length === 1 ? { kind: parts[0] } : null;
369
+ case "trial":
370
+ case "save":
371
+ return parts.length === 2 && parts[1] ? { kind: parts[0], id: decodeURIComponent(parts[1]) } : null;
372
+ case "value":
373
+ return parts[1] === "lab" && parts.length === 3 || parts[1] === "trial" && parts.length === 4 ? { kind: "value" } : null;
374
+ default:
375
+ return null;
376
+ }
377
+ } catch {
378
+ return null;
379
+ }
380
+ }
381
+ function isObject(value) {
382
+ return !!value && typeof value === "object" && !Array.isArray(value);
383
+ }
384
+ function recordsOfDocument(doc) {
385
+ return [
386
+ ...doc.trials.map((t, order) => [trialRecord(t.id), { ...t, order }]),
387
+ ...doc.saves.map((s) => [saveRecord(s.id), s]),
388
+ [LAYOUT_RECORD, doc.layout],
389
+ [UNDOCK_RECORD, doc.undockedPanels],
390
+ [META_RECORD, { version: doc.version, mode: doc.mode }]
391
+ ];
392
+ }
393
+ function documentOfRecords(entries) {
394
+ let meta;
395
+ let layout;
396
+ let undock;
397
+ const trials = [];
398
+ const saves = [];
399
+ for (const [name, value] of entries) {
400
+ const parsed = parseRecordName(name);
401
+ if (!parsed) continue;
402
+ switch (parsed.kind) {
403
+ case "meta":
404
+ meta = value;
405
+ break;
406
+ case "layout":
407
+ layout = value;
408
+ break;
409
+ case "undock":
410
+ undock = value;
411
+ break;
412
+ case "trial":
413
+ if (isObject(value)) {
414
+ const { order, ...rest } = value;
415
+ trials.push({
416
+ trial: { ...rest, id: parsed.id },
417
+ order: typeof order === "number" && Number.isFinite(order) ? order : Infinity
418
+ });
419
+ }
420
+ break;
421
+ case "save":
422
+ if (isObject(value)) saves.push({ ...value, id: parsed.id });
423
+ break;
424
+ }
425
+ }
426
+ if (!isObject(meta)) return null;
427
+ trials.sort(compareTrialOrder);
428
+ const orders = /* @__PURE__ */ new Map();
429
+ let next = 0;
430
+ for (const { trial, order } of trials) {
431
+ const at = Number.isFinite(order) ? order : next;
432
+ orders.set(trial.id, at);
433
+ next = Math.max(next, at + 1);
434
+ }
435
+ saves.sort((a, b) => a.savedAt - b.savedAt);
436
+ return {
437
+ doc: {
438
+ version: meta.version,
439
+ mode: meta.mode,
440
+ trials: trials.map((t) => t.trial),
441
+ saves,
442
+ layout,
443
+ undockedPanels: undock
444
+ },
445
+ orders
446
+ };
447
+ }
448
+ function compareTrialOrder(a, b) {
449
+ if (a.order !== b.order) return a.order < b.order ? -1 : 1;
450
+ return a.trial.id < b.trial.id ? -1 : a.trial.id > b.trial.id ? 1 : 0;
451
+ }
452
+
453
+ // src/state/records.ts
454
+ var DELETED = /* @__PURE__ */ Symbol("deleted");
455
+ function createRecordCache(options) {
456
+ const { storage, prefix } = options;
457
+ const debounceMs = options.debounceMs ?? 300;
458
+ const maxWaitMs = options.maxWaitMs ?? 1e3;
459
+ const values = new Map(options.initial ?? []);
460
+ const queued = /* @__PURE__ */ new Map();
461
+ const listeners = /* @__PURE__ */ new Set();
462
+ let writable = options.writable ?? true;
463
+ let closed = false;
464
+ let timer = null;
465
+ let firstQueuedAt = null;
466
+ const emit = (changes) => {
467
+ for (const listener of [...listeners]) listener(changes);
468
+ };
469
+ const schedule = () => {
470
+ if (!writable || closed) return;
471
+ const now = Date.now();
472
+ firstQueuedAt ??= now;
473
+ if (timer) clearTimeout(timer);
474
+ const wait = Math.max(0, Math.min(debounceMs, firstQueuedAt + maxWaitMs - now));
475
+ timer = setTimeout(() => {
476
+ timer = null;
477
+ void flush();
478
+ }, wait);
479
+ };
480
+ async function flush() {
481
+ if (timer) clearTimeout(timer);
482
+ timer = null;
483
+ firstQueuedAt = null;
484
+ if (!writable || queued.size === 0) return;
485
+ const batch = [...queued];
486
+ queued.clear();
487
+ await Promise.all(
488
+ batch.map(async ([name, value]) => {
489
+ const key = prefix + name;
490
+ try {
491
+ if (value === DELETED) await storage.delete(key);
492
+ else await storage.set(key, value);
493
+ } catch (error) {
494
+ console.warn(`[labkit] could not write "${key}"; keeping it in memory`, error);
495
+ }
496
+ })
497
+ );
498
+ }
499
+ const applyRemote = (changes) => {
500
+ const applied = [];
501
+ for (const [key, value] of changes) {
502
+ if (!key.startsWith(prefix)) continue;
503
+ const name = key.slice(prefix.length);
504
+ if (queued.has(name)) continue;
505
+ if (value === void 0) {
506
+ if (!values.has(name)) continue;
507
+ values.delete(name);
508
+ } else {
509
+ values.set(name, value);
510
+ }
511
+ applied.push({ name, value, origin: "remote" });
512
+ }
513
+ if (applied.length > 0) emit(applied);
514
+ };
515
+ const unsubscribe = writable ? storage.subscribe?.(prefix, applyRemote) : void 0;
516
+ const onPageHide = () => {
517
+ void flush();
518
+ };
519
+ const onVisibility = () => {
520
+ if (document.visibilityState === "hidden") void flush();
521
+ };
522
+ if (typeof window !== "undefined") {
523
+ window.addEventListener("pagehide", onPageHide);
524
+ document.addEventListener("visibilitychange", onVisibility);
525
+ }
526
+ const cache = {
527
+ prefix,
528
+ get writable() {
529
+ return writable;
530
+ },
531
+ has: (name) => values.has(name),
532
+ get: (name) => values.get(name),
533
+ entries: (namePrefix = "") => [...values].filter(([name]) => name.startsWith(namePrefix)),
534
+ set: (name, value) => {
535
+ values.set(name, value);
536
+ queued.set(name, value);
537
+ emit([{ name, value, origin: "local" }]);
538
+ schedule();
539
+ },
540
+ delete: (name) => {
541
+ if (!values.has(name) && !queued.has(name)) return;
542
+ values.delete(name);
543
+ queued.set(name, DELETED);
544
+ emit([{ name, value: void 0, origin: "local" }]);
545
+ schedule();
546
+ },
547
+ subscribe: (listener) => {
548
+ listeners.add(listener);
549
+ return () => {
550
+ listeners.delete(listener);
551
+ };
552
+ },
553
+ flush,
554
+ close: async () => {
555
+ if (closed) return;
556
+ await flush();
557
+ closed = true;
558
+ unsubscribe?.();
559
+ if (typeof window !== "undefined") {
560
+ window.removeEventListener("pagehide", onPageHide);
561
+ document.removeEventListener("visibilitychange", onVisibility);
562
+ }
563
+ },
564
+ seed: (entries) => {
565
+ for (const [name, value] of entries) values.set(name, value);
566
+ },
567
+ stopWriting: () => {
568
+ writable = false;
569
+ queued.clear();
570
+ if (timer) clearTimeout(timer);
571
+ timer = null;
572
+ unsubscribe?.();
573
+ },
574
+ applyRemote
575
+ };
576
+ return cache;
577
+ }
578
+ async function openRecords(options) {
579
+ const early = [];
580
+ const stopEarly = options.storage.subscribe?.(options.prefix, (c) => early.push(...c));
581
+ let listed;
582
+ try {
583
+ listed = await options.storage.list(options.prefix);
584
+ } catch (error) {
585
+ stopEarly?.();
586
+ console.warn(
587
+ `[labkit] could not read "${options.prefix}"; opening empty and not persisting`,
588
+ error
589
+ );
590
+ return createRecordCache({ ...options, writable: false });
591
+ }
592
+ const cache = createRecordCache({
593
+ ...options,
594
+ initial: listed.map(([key, value]) => [
595
+ key.slice(options.prefix.length),
596
+ value
597
+ ])
598
+ });
599
+ stopEarly?.();
600
+ cache.applyRemote(early);
601
+ return cache;
602
+ }
603
+ function useOpenOnce(open) {
604
+ const entry = useRef(null);
605
+ if (entry.current === null) {
606
+ const result = open();
607
+ entry.current = result instanceof Promise ? { value: null, pending: result, mounts: 0 } : { value: result, pending: null, mounts: 0 };
608
+ }
609
+ const [value, setValue] = useState(entry.current.value);
610
+ useEffect(() => {
611
+ const e = entry.current;
612
+ e.mounts += 1;
613
+ let live = true;
614
+ void e.pending?.then((opened) => {
615
+ e.value = opened;
616
+ if (live) setValue(opened);
617
+ });
618
+ return () => {
619
+ live = false;
620
+ e.mounts -= 1;
621
+ queueMicrotask(() => {
622
+ if (e.mounts > 0) return;
623
+ if (e.value) void e.value.close();
624
+ else
625
+ void e.pending?.then((opened) => {
626
+ if (e.mounts === 0) void opened.close();
627
+ });
628
+ });
629
+ };
630
+ }, []);
631
+ return value;
632
+ }
633
+ function useWarnIgnoredChange(component, props) {
634
+ const first = useRef(props);
635
+ const warned = useRef(false);
636
+ if (process.env.NODE_ENV === "production" || warned.current) return;
637
+ for (const key of Object.keys(props)) {
638
+ if (Object.is(first.current[key], props[key])) continue;
639
+ warned.current = true;
640
+ console.warn(
641
+ `[labkit] ${component} reads \`${key}\` once, at mount; a later change is ignored`
642
+ );
643
+ return;
644
+ }
645
+ }
646
+ var PersistenceContext = createContext(null);
647
+ function Persistence({ storageKey, storage, fallback = null, children }) {
648
+ useWarnIgnoredChange("<Persistence>", { storageKey, storage });
649
+ const records = useOpenOnce(
650
+ async () => openRecords({ storage: storage ?? await defaultStorage(), prefix: labPrefix(storageKey) })
651
+ );
652
+ if (!records) return /* @__PURE__ */ jsx(Fragment, { children: fallback });
653
+ return /* @__PURE__ */ jsx(PersistenceContext.Provider, { value: records, children });
654
+ }
655
+ function usePersistedState(name, initial, options = {}) {
656
+ const records = useContext(PersistenceContext);
657
+ const trialId = useContext(TrialIdContext);
658
+ const [local, setLocal] = useState(initial);
659
+ const record = records && name != null ? valueRecord(options.scope === "lab" ? null : trialId, name) : null;
660
+ const subscribe = useCallback(
661
+ (notify) => records && record ? records.subscribe((changes) => {
662
+ if (changes.some((c) => c.name === record)) notify();
663
+ }) : () => {
664
+ },
665
+ [records, record]
666
+ );
667
+ const read = () => records && record && records.has(record) ? records.get(record) : local;
668
+ const persisted = useSyncExternalStore(subscribe, read, read);
669
+ const setPersisted = useCallback(
670
+ (next) => {
671
+ if (!records || !record) return;
672
+ const prev = records.has(record) ? records.get(record) : local;
673
+ records.set(record, typeof next === "function" ? next(prev) : next);
674
+ },
675
+ [records, record, local]
676
+ );
677
+ return records && record ? [persisted, setPersisted] : [local, setLocal];
678
+ }
679
+
680
+ export { LAYOUT_RECORD, META_RECORD, Persistence, PersistenceContext, UNDOCK_RECORD, compareTrialOrder, createIndexedDbAdapter, createMemoryAdapter, createRecordCache, decodeUrlHash, defaultStorage, deserializeTrials, documentOfRecords, emptyUndoStack, encodeUrlHash, indexedDbAdapter, labPrefix, labStorageKey, localStorageAdapter, newId, noneAdapter, openRecords, parseRecordName, recordsOfDocument, saveRecord, serializeTrials, sessionStorageAdapter, trialRecord, trialValuesPrefix, urlHashAdapter, useOpenOnce, usePersistedState, useWarnIgnoredChange };
681
+ //# sourceMappingURL=chunk-RWNDDKOH.js.map
682
+ //# sourceMappingURL=chunk-RWNDDKOH.js.map