@weasel-js/labkit 1.0.3 → 1.0.4

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 (45) hide show
  1. package/dist/_dts/{DrawCommand-uKHt4Vul.d.ts → DrawCommand-BkZztJsW.d.ts} +3 -1
  2. package/dist/_dts/{useExperimentState-vrttakTt.d.ts → useExperimentState-D7EQnnwJ.d.ts} +4 -0
  3. package/dist/{chunk-N5KTQKQA.js → chunk-BAPZPDDA.js} +628 -588
  4. package/dist/chunk-BAPZPDDA.js.map +1 -0
  5. package/dist/{chunk-73PXCRCR.js → chunk-DEWXYFEU.js} +6 -6
  6. package/dist/chunk-DEWXYFEU.js.map +1 -0
  7. package/dist/{chunk-3WPOGKUP.js → chunk-DWV7SFKR.js} +518 -217
  8. package/dist/chunk-DWV7SFKR.js.map +1 -0
  9. package/dist/{chunk-T7OKNJTY.js → chunk-KSTEW2AF.js} +18 -2
  10. package/dist/chunk-KSTEW2AF.js.map +1 -0
  11. package/dist/index.d.ts +64 -15
  12. package/dist/index.js +133 -24
  13. package/dist/index.js.map +1 -1
  14. package/dist/passthrough/weasel-canvas.d.ts +1 -1
  15. package/dist/passthrough/weasel-canvas.js +1 -1
  16. package/dist/passthrough/weasel-ui.d.ts +1 -1
  17. package/dist/passthrough/weasel-ui.js +2 -2
  18. package/dist/state/index.d.ts +4 -4
  19. package/dist/state/index.js +2 -2
  20. package/dist/styles.css +163 -6
  21. package/dist/ui/layers/index.js +3 -3
  22. package/package.json +2 -1
  23. package/src/lab/Lab.tsx +14 -2
  24. package/src/lab/LabContext.ts +1 -0
  25. package/src/lab/WorkspaceGrid.less +37 -5
  26. package/src/lab/WorkspaceGrid.stories.tsx +16 -0
  27. package/src/lab/WorkspaceGrid.test.tsx +84 -10
  28. package/src/lab/WorkspaceGrid.tsx +188 -14
  29. package/src/lab/index.ts +0 -2
  30. package/src/state/helpers.ts +2 -2
  31. package/src/state/store.test.ts +2 -1
  32. package/src/state/store.ts +19 -0
  33. package/src/state/types.ts +3 -0
  34. package/src/test-setup.ts +19 -0
  35. package/src/workspace/Workspace.stories.tsx +1 -0
  36. package/src/workspace/Workspace.test.tsx +1 -0
  37. package/src/workspace/index.ts +1 -0
  38. package/src/workspace/workspaceOps.test.ts +34 -1
  39. package/src/workspace/workspaceOps.ts +16 -0
  40. package/dist/chunk-3WPOGKUP.js.map +0 -1
  41. package/dist/chunk-73PXCRCR.js.map +0 -1
  42. package/dist/chunk-N5KTQKQA.js.map +0 -1
  43. package/dist/chunk-T7OKNJTY.js.map +0 -1
  44. package/src/lab/gridDims.test.ts +0 -35
  45. package/src/lab/gridDims.ts +0 -13
@@ -183,6 +183,16 @@ function createLabStore(options) {
183
183
  hydratedSnapshots = [];
184
184
  }
185
185
  }
186
+ const layoutRaw = options.storage.read(labStorageKey(options.storageKey, "layout"));
187
+ let hydratedLayout = {};
188
+ if (layoutRaw) {
189
+ try {
190
+ hydratedLayout = JSON.parse(layoutRaw);
191
+ } catch {
192
+ console.warn("[labkit] failed to parse saved layout, starting empty");
193
+ hydratedLayout = {};
194
+ }
195
+ }
186
196
  const modeRaw = options.storage.read(labStorageKey(options.storageKey, "theme"));
187
197
  const stored = modeRaw === "interstellar" ? "dark" : modeRaw;
188
198
  let hydratedMode;
@@ -195,6 +205,7 @@ function createLabStore(options) {
195
205
  workspaces: hydratedWorkspaces,
196
206
  savedSnapshots: hydratedSnapshots,
197
207
  mode: hydratedMode,
208
+ layout: hydratedLayout,
198
209
  addWorkspace: (record) => {
199
210
  set((s) => ({
200
211
  workspaces: [...s.workspaces, { ...record, undoStack: emptyUndoStack() }]
@@ -304,6 +315,10 @@ function createLabStore(options) {
304
315
  setMode: (mode) => {
305
316
  set({ mode });
306
317
  scheduleFlush();
318
+ },
319
+ setLayout: (layout) => {
320
+ set({ layout });
321
+ scheduleFlush();
307
322
  }
308
323
  }));
309
324
  function scheduleFlush() {
@@ -319,6 +334,7 @@ function createLabStore(options) {
319
334
  JSON.stringify(s.savedSnapshots)
320
335
  );
321
336
  options.storage.write(labStorageKey(options.storageKey, "theme"), s.mode);
337
+ options.storage.write(labStorageKey(options.storageKey, "layout"), JSON.stringify(s.layout));
322
338
  flushTimer = null;
323
339
  }, 300);
324
340
  }
@@ -347,5 +363,5 @@ function useExperimentState() {
347
363
  }
348
364
 
349
365
  export { LabStoreContext, LabStoreProvider, WorkspaceIdContext, WorkspaceIdProvider, createLabStore, createMemoryAdapter, decodeUrlHash, deserializeWorkspaces, emptyUndoStack, encodeUrlHash, labStorageKey, localStorageAdapter, noneAdapter, serializeWorkspaces, sessionStorageAdapter, urlHashAdapter, useExperimentState, useLabStore, useWorkspaceId };
350
- //# sourceMappingURL=chunk-T7OKNJTY.js.map
351
- //# sourceMappingURL=chunk-T7OKNJTY.js.map
366
+ //# sourceMappingURL=chunk-KSTEW2AF.js.map
367
+ //# sourceMappingURL=chunk-KSTEW2AF.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":";;;;;;AAIO,SAAS,aAAA,CACd,YACA,MAAA,EACQ;AACR,EAAA,OAAO,CAAA,GAAA,EAAM,UAAU,CAAA,CAAA,EAAI,MAAM,CAAA,CAAA;AACnC;AAGO,SAAS,cAAc,KAAA,EAAuB;AACnD,EAAA,OAAO,IAAA,CAAK,kBAAA,CAAmB,KAAK,CAAC,CAAA;AACvC;AAIO,SAAS,cAAc,IAAA,EAA6B;AACzD,EAAA,IAAI,CAAC,MAAM,OAAO,IAAA;AAClB,EAAA,IAAI;AACF,IAAA,OAAO,kBAAA,CAAmB,IAAA,CAAK,IAAI,CAAC,CAAA;AAAA,EACtC,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AACF;AAGO,SAAS,cAAA,GAA4B;AAC1C,EAAA,OAAO,EAAE,IAAA,EAAM,EAAC,EAAG,MAAA,EAAQ,EAAC,EAAE;AAChC;AAOO,SAAS,mBAAA,CACd,YACA,WAAA,EACQ;AACR,EAAA,MAAM,OAAA,GAA8B,WAAW,GAAA,CAAI,CAAC,EAAE,SAAA,EAAW,KAAA,EAAO,GAAG,CAAA,EAAE,KAAM;AACjF,IAAA,MAAM,CAAA,GAAI,WAAA,CAAY,CAAA,CAAE,cAAc,CAAA;AACtC,IAAA,OAAO;AAAA,MACL,GAAG,CAAA;AAAA,MACH,KAAA,EAAO,GAAG,SAAA,GAAY,CAAA,CAAE,UAAU,CAAA,CAAE,KAAK,IAAI,CAAA,CAAE;AAAA,KACjD;AAAA,EACF,CAAC,CAAA;AACD,EAAA,OAAO,IAAA,CAAK,UAAU,OAAO,CAAA;AAC/B;AAKO,SAAS,qBAAA,CACd,KACA,aAAA,EACmB;AACnB,EAAA,IAAI;AACF,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,KAAA,CAAM,GAAG,CAAA;AAC9B,IAAA,OAAO,OAAA,CAAQ,GAAA,CAAI,CAAC,CAAA,KAAM;AACxB,MAAA,MAAM,CAAA,GAAI,aAAA,CAAc,CAAA,CAAE,cAAc,CAAA;AACxC,MAAA,OAAO;AAAA,QACL,GAAG,CAAA;AAAA,QACH,KAAA,EAAO,GAAG,WAAA,GAAc,CAAA,CAAE,YAAY,CAAA,CAAE,KAAK,IAAI,CAAA,CAAE,KAAA;AAAA,QACnD,WAAW,cAAA;AAAe,OAC5B;AAAA,IACF,CAAC,CAAA;AAAA,EACH,CAAA,CAAA,MAAQ;AACN,IAAA,OAAA,CAAQ,KAAK,uEAAuE,CAAA;AACpF,IAAA,OAAO,EAAC;AAAA,EACV;AACF;;;ACnEO,IAAM,mBAAA,GAAsC;AAAA,EACjD,IAAA,EAAM,CAAC,GAAA,KAAQ;AACb,IAAA,IAAI;AACF,MAAA,OAAO,YAAA,CAAa,QAAQ,GAAG,CAAA;AAAA,IACjC,CAAA,CAAA,MAAQ;AACN,MAAA,OAAO,IAAA;AAAA,IACT;AAAA,EACF,CAAA;AAAA,EACA,KAAA,EAAO,CAAC,GAAA,EAAK,KAAA,KAAU;AACrB,IAAA,IAAI;AACF,MAAA,YAAA,CAAa,OAAA,CAAQ,KAAK,KAAK,CAAA;AAAA,IACjC,SAAS,CAAA,EAAG;AACV,MAAA,OAAA,CAAQ,IAAA,CAAK,uCAAuC,CAAC,CAAA;AAAA,IACvD;AAAA,EACF,CAAA;AAAA,EACA,MAAA,EAAQ,CAAC,GAAA,KAAQ;AACf,IAAA,IAAI;AACF,MAAA,YAAA,CAAa,WAAW,GAAG,CAAA;AAAA,IAC7B,CAAA,CAAA,MAAQ;AAAA,IAER;AAAA,EACF;AACF;AAGO,IAAM,qBAAA,GAAwC;AAAA,EACnD,IAAA,EAAM,CAAC,GAAA,KAAQ;AACb,IAAA,IAAI;AACF,MAAA,OAAO,cAAA,CAAe,QAAQ,GAAG,CAAA;AAAA,IACnC,CAAA,CAAA,MAAQ;AACN,MAAA,OAAO,IAAA;AAAA,IACT;AAAA,EACF,CAAA;AAAA,EACA,KAAA,EAAO,CAAC,GAAA,EAAK,KAAA,KAAU;AACrB,IAAA,IAAI;AACF,MAAA,cAAA,CAAe,OAAA,CAAQ,KAAK,KAAK,CAAA;AAAA,IACnC,SAAS,CAAA,EAAG;AACV,MAAA,OAAA,CAAQ,IAAA,CAAK,yCAAyC,CAAC,CAAA;AAAA,IACzD;AAAA,EACF,CAAA;AAAA,EACA,MAAA,EAAQ,CAAC,GAAA,KAAQ;AACf,IAAA,IAAI;AACF,MAAA,cAAA,CAAe,WAAW,GAAG,CAAA;AAAA,IAC/B,CAAA,CAAA,MAAQ;AAAA,IAER;AAAA,EACF;AACF;AAEA,IAAM,cAAA,GAAiB,OAAO,MAAA,KAAW,WAAA;AAEzC,SAAS,WAAA,GAAsC;AAC7C,EAAA,IAAI,CAAC,cAAA,EAAgB,OAAO,EAAC;AAC7B,EAAA,MAAM,GAAA,GAAM,cAAc,MAAA,CAAO,QAAA,CAAS,KAAK,OAAA,CAAQ,IAAA,EAAM,EAAE,CAAC,CAAA;AAChE,EAAA,IAAI,CAAC,GAAA,EAAK,OAAO,EAAC;AAClB,EAAA,IAAI;AACF,IAAA,OAAO,IAAA,CAAK,MAAM,GAAG,CAAA;AAAA,EACvB,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,EAAC;AAAA,EACV;AACF;AAEA,SAAS,aAAa,GAAA,EAAmC;AACvD,EAAA,IAAI,CAAC,cAAA,EAAgB;AACrB,EAAA,MAAM,OAAA,GAAU,aAAA,CAAc,IAAA,CAAK,SAAA,CAAU,GAAG,CAAC,CAAA;AACjD,EAAA,MAAA,CAAO,QAAQ,YAAA,CAAa,IAAA,EAAM,EAAA,EAAI,CAAA,CAAA,EAAI,OAAO,CAAA,CAAE,CAAA;AACrD;AAIO,IAAM,cAAA,GAAiC;AAAA,EAC5C,MAAM,CAAC,GAAA,KAAQ,WAAA,EAAY,CAAE,GAAG,CAAA,IAAK,IAAA;AAAA,EACrC,KAAA,EAAO,CAAC,GAAA,EAAK,KAAA,KAAU;AACrB,IAAA,MAAM,MAAM,WAAA,EAAY;AACxB,IAAA,GAAA,CAAI,GAAG,CAAA,GAAI,KAAA;AACX,IAAA,YAAA,CAAa,GAAG,CAAA;AAAA,EAClB,CAAA;AAAA,EACA,MAAA,EAAQ,CAAC,GAAA,KAAQ;AACf,IAAA,MAAM,MAAM,WAAA,EAAY;AACxB,IAAA,OAAO,IAAI,GAAG,CAAA;AACd,IAAA,YAAA,CAAa,GAAG,CAAA;AAAA,EAClB;AACF;AAIO,SAAS,mBAAA,GAAsC;AACpD,EAAA,MAAM,KAAA,uBAAY,GAAA,EAAoB;AACtC,EAAA,OAAO;AAAA,IACL,MAAM,CAAC,GAAA,KAAQ,KAAA,CAAM,GAAA,CAAI,GAAG,CAAA,IAAK,IAAA;AAAA,IACjC,KAAA,EAAO,CAAC,GAAA,EAAK,KAAA,KAAU;AACrB,MAAA,KAAA,CAAM,GAAA,CAAI,KAAK,KAAK,CAAA;AAAA,IACtB,CAAA;AAAA,IACA,MAAA,EAAQ,CAAC,GAAA,KAAQ;AACf,MAAA,KAAA,CAAM,OAAO,GAAG,CAAA;AAAA,IAClB;AAAA,GACF;AACF;AAGO,IAAM,WAAA,GAA8B;AAAA,EACzC,MAAM,MAAM,IAAA;AAAA,EACZ,OAAO,MAAM;AAAA,EAAC,CAAA;AAAA,EACd,QAAQ,MAAM;AAAA,EAAC;AACjB;ACpGO,IAAM,eAAA,GAAkB,cAA2B,IAAI;AAGvD,SAAS,gBAAA,CAAiB;AAAA,EAC/B,KAAA;AAAA,EACA;AACF,CAAA,EAGiB;AACf,EAAA,uBAAO,GAAA,CAAC,gBAAgB,QAAA,EAAhB,EAAyB,OAAO,EAAE,KAAA,IAAU,QAAA,EAAS,CAAA;AAC/D;AAGO,SAAS,WAAA,GAAgE;AAC9E,EAAA,MAAM,GAAA,GAAM,WAAW,eAAe,CAAA;AACtC,EAAA,IAAI,CAAC,GAAA,EAAK,MAAM,IAAI,MAAM,6DAA6D,CAAA;AACvF,EAAA,OAAO,QAAA,CAAS,IAAI,KAAK,CAAA;AAC3B;AAGO,IAAM,kBAAA,GAAqB,cAA6B,IAAI;AAI5D,SAAS,mBAAA,CAAoB;AAAA,EAClC,WAAA;AAAA,EACA;AACF,CAAA,EAGiB;AACf,EAAA,2BAAQ,kBAAA,CAAmB,QAAA,EAAnB,EAA4B,KAAA,EAAO,aAAc,QAAA,EAAS,CAAA;AACpE;AAIO,SAAS,cAAA,GAAyB;AACvC,EAAA,MAAM,EAAA,GAAK,WAAW,kBAAkB,CAAA;AACxC,EAAA,IAAI,CAAC,EAAA,EAAI,MAAM,IAAI,MAAM,mEAAmE,CAAA;AAC5F,EAAA,OAAO,EAAA;AACT;ACHO,SAAS,eAAe,OAAA,EAA0C;AACvE,EAAA,IAAI,cAAqC,EAAC;AAC1C,EAAA,IAAI,UAAA,GAAmD,IAAA;AAEvD,EAAA,MAAM,aAAA,GAAgB,QAAQ,OAAA,CAAQ,IAAA,CAAK,cAAc,OAAA,CAAQ,UAAA,EAAY,YAAY,CAAC,CAAA;AAC1F,EAAA,IAAI,qBAAwC,EAAC;AAC7C,EAAA,IAAI,aAAA,EAAe;AACjB,IAAA,kBAAA,GAAqB,qBAAA,CAAsB,eAAe,WAAW,CAAA;AAAA,EACvE;AAEA,EAAA,MAAM,QAAA,GAAW,QAAQ,OAAA,CAAQ,IAAA,CAAK,cAAc,OAAA,CAAQ,UAAA,EAAY,OAAO,CAAC,CAAA;AAChF,EAAA,IAAI,oBAAqC,EAAC;AAC1C,EAAA,IAAI,QAAA,EAAU;AACZ,IAAA,IAAI;AACF,MAAA,iBAAA,GAAoB,IAAA,CAAK,MAAM,QAAQ,CAAA;AAAA,IACzC,CAAA,CAAA,MAAQ;AACN,MAAA,OAAA,CAAQ,KAAK,0DAA0D,CAAA;AACvE,MAAA,iBAAA,GAAoB,EAAC;AAAA,IACvB;AAAA,EACF;AAEA,EAAA,MAAM,SAAA,GAAY,QAAQ,OAAA,CAAQ,IAAA,CAAK,cAAc,OAAA,CAAQ,UAAA,EAAY,QAAQ,CAAC,CAAA;AAClF,EAAA,IAAI,iBAA0C,EAAC;AAC/C,EAAA,IAAI,SAAA,EAAW;AACb,IAAA,IAAI;AACF,MAAA,cAAA,GAAiB,IAAA,CAAK,MAAM,SAAS,CAAA;AAAA,IACvC,CAAA,CAAA,MAAQ;AACN,MAAA,OAAA,CAAQ,KAAK,uDAAuD,CAAA;AACpE,MAAA,cAAA,GAAiB,EAAC;AAAA,IACpB;AAAA,EACF;AAEA,EAAA,MAAM,OAAA,GAAU,QAAQ,OAAA,CAAQ,IAAA,CAAK,cAAc,OAAA,CAAQ,UAAA,EAAY,OAAO,CAAC,CAAA;AAE/E,EAAA,MAAM,MAAA,GAAS,OAAA,KAAY,cAAA,GAAiB,MAAA,GAAS,OAAA;AACrD,EAAA,IAAI,YAAA;AACJ,EAAA,IAAI,MAAA,KAAW,OAAA,IAAW,MAAA,KAAW,MAAA,IAAU,WAAW,MAAA,EAAQ;AAChE,IAAA,YAAA,GAAe,MAAA;AAAA,EACjB,CAAA,MAAO;AACL,IAAA,YAAA,GAAe,QAAQ,WAAA,IAAe,MAAA;AAAA,EACxC;AAEA,EAAA,MAAM,KAAA,GAAQ,WAAA,EAA6C,CAAE,CAAC,KAAK,GAAA,MAAS;AAAA,IAC1E,UAAA,EAAY,kBAAA;AAAA,IACZ,cAAA,EAAgB,iBAAA;AAAA,IAChB,IAAA,EAAM,YAAA;AAAA,IACN,MAAA,EAAQ,cAAA;AAAA,IAER,YAAA,EAAc,CAAC,MAAA,KAAW;AACxB,MAAA,GAAA,CAAI,CAAC,CAAA,MAAO;AAAA,QACV,UAAA,EAAY,CAAC,GAAG,CAAA,CAAE,UAAA,EAAY,EAAE,GAAG,MAAA,EAAQ,SAAA,EAAW,cAAA,EAAe,EAAG;AAAA,OAC1E,CAAE,CAAA;AACF,MAAA,aAAA,EAAc;AAAA,IAChB,CAAA;AAAA,IAEA,eAAA,EAAiB,CAAC,EAAA,KAAO;AACvB,MAAA,GAAA,CAAI,CAAC,CAAA,MAAO,EAAE,UAAA,EAAY,CAAA,CAAE,UAAA,CAAW,MAAA,CAAO,CAAC,CAAA,KAAM,CAAA,CAAE,EAAA,KAAO,EAAE,GAAE,CAAE,CAAA;AACpE,MAAA,aAAA,EAAc;AAAA,IAChB,CAAA;AAAA,IAEA,oBAAA,EAAsB,CAAC,EAAA,EAAI,IAAA,KAAS;AAClC,MAAA,GAAA,CAAI,CAAC,CAAA,MAAO;AAAA,QACV,UAAA,EAAY,CAAA,CAAE,UAAA,CAAW,GAAA,CAAI,CAAC,CAAA,KAAM;AAClC,UAAA,IAAI,CAAA,CAAE,EAAA,KAAO,EAAA,EAAI,OAAO,CAAA;AACxB,UAAA,MAAM,YACJ,OAAO,IAAA,KAAS,aAAc,IAAA,CAAoC,CAAA,CAAE,KAAK,CAAA,GAAI,IAAA;AAC/E,UAAA,OAAO,EAAE,GAAG,CAAA,EAAG,KAAA,EAAO,SAAA,EAAU;AAAA,QAClC,CAAC;AAAA,OACH,CAAE,CAAA;AACF,MAAA,aAAA,EAAc;AAAA,IAChB,CAAA;AAAA,IAEA,qBAAA,EAAuB,CAAC,EAAA,EAAI,GAAA,EAAK,KAAA,KAAU;AACzC,MAAA,GAAA,CAAI,CAAC,CAAA,MAAO;AAAA,QACV,UAAA,EAAY,CAAA,CAAE,UAAA,CAAW,GAAA,CAAI,CAAC,CAAA,KAAM;AAClC,UAAA,IAAI,CAAA,CAAE,EAAA,KAAO,EAAA,EAAI,OAAO,CAAA;AACxB,UAAA,OAAO;AAAA,YACL,GAAG,CAAA;AAAA,YACH,MAAA,EAAQ,EAAE,GAAI,CAAA,CAAE,QAAoC,CAAC,GAAa,GAAG,KAAA;AAAM,WAC7E;AAAA,QACF,CAAC;AAAA,OACH,CAAE,CAAA;AACF,MAAA,aAAA,EAAc;AAAA,IAChB,CAAA;AAAA,IAEA,mBAAA,EAAqB,CAAC,EAAA,EAAI,IAAA,KAAS;AACjC,MAAA,GAAA,CAAI,CAAC,CAAA,MAAO;AAAA,QACV,UAAA,EAAY,CAAA,CAAE,UAAA,CAAW,GAAA,CAAI,CAAC,CAAA,KAAO,CAAA,CAAE,EAAA,KAAO,EAAA,GAAK,EAAE,GAAG,CAAA,EAAG,IAAA,KAAS,CAAE;AAAA,OACxE,CAAE,CAAA;AACF,MAAA,aAAA,EAAc;AAAA,IAChB,CAAA;AAAA,IAEA,wBAAA,EAA0B,CAAC,EAAA,EAAI,IAAA,KAAS;AACtC,MAAA,GAAA,CAAI,CAAC,CAAA,MAAO;AAAA,QACV,UAAA,EAAY,CAAA,CAAE,UAAA,CAAW,GAAA,CAAI,CAAC,CAAA,KAAM;AAClC,UAAA,IAAI,CAAA,CAAE,EAAA,KAAO,EAAA,EAAI,OAAO,CAAA;AACxB,UAAA,MAAM,SAAA,GACJ,OAAO,IAAA,KAAS,UAAA,GACX,IAAA;AAAA,YACC,CAAA,CAAE;AAAA,WACJ,GACA,IAAA;AACN,UAAA,OAAO,EAAE,GAAG,CAAA,EAAG,SAAA,EAAU;AAAA,QAC3B,CAAC;AAAA,OACH,CAAE,CAAA;AAAA,IACJ,CAAA;AAAA,IAEA,sBAAA,EAAwB,CAAC,EAAA,EAAI,cAAA,KAAmB;AAC9C,MAAA,GAAA,CAAI,CAAC,CAAA,MAAO;AAAA,QACV,UAAA,EAAY,CAAA,CAAE,UAAA,CAAW,GAAA,CAAI,CAAC,CAAA,KAAO,CAAA,CAAE,EAAA,KAAO,EAAA,GAAK,EAAE,GAAG,CAAA,EAAG,cAAA,KAAmB,CAAE;AAAA,OAClF,CAAE,CAAA;AACF,MAAA,aAAA,EAAc;AAAA,IAChB,CAAA;AAAA,IAEA,YAAA,EAAc,CAAC,WAAA,EAAa,IAAA,KAAS;AACnC,MAAA,MAAM,SAAA,GAAY,KAAI,CAAE,UAAA,CAAW,KAAK,CAAC,CAAA,KAAM,CAAA,CAAE,EAAA,KAAO,WAAW,CAAA;AACnE,MAAA,IAAI,CAAC,SAAA,EAAW;AAChB,MAAA,MAAM,GAAA,GAAM,WAAA,CAAY,SAAA,CAAU,cAAc,CAAA;AAChD,MAAA,MAAM,eAAA,GAAkB,GAAA,EAAK,SAAA,GACzB,GAAA,CAAI,SAAA,CAAU,UAAU,KAAK,CAAA,GAC7B,eAAA,CAAgB,SAAA,CAAU,KAAK,CAAA;AACnC,MAAA,MAAM,YAAA,GAAe,eAAA,CAAgB,SAAA,CAAU,MAAM,CAAA;AACrD,MAAA,MAAM,MAAA,GAAS,GAAA,EAAI,CAAE,cAAA,CAAe,OAAO,CAAC,CAAA,EAAG,EAAA,KAAQ,EAAA,CAAG,OAAA,GAAU,CAAA,GAAI,EAAA,CAAG,OAAA,GAAU,GAAI,CAAC,CAAA;AAC1F,MAAA,MAAM,UAAU,IAAA,CAAK,GAAA,CAAI,KAAK,GAAA,EAAI,EAAG,SAAS,CAAC,CAAA;AAC/C,MAAA,MAAM,QAAA,GAA0B;AAAA,QAC9B,EAAA,EAAI,OAAO,UAAA,EAAW;AAAA,QACtB,IAAA;AAAA,QACA,WAAA;AAAA,QACA,gBAAgB,SAAA,CAAU,cAAA;AAAA,QAC1B,MAAA,EAAQ,YAAA;AAAA,QACR,KAAA,EAAO,eAAA;AAAA,QACP;AAAA,OACF;AACA,MAAA,GAAA,CAAI,CAAC,CAAA,MAAO,EAAE,cAAA,EAAgB,CAAC,GAAG,CAAA,CAAE,cAAA,EAAgB,QAAQ,CAAA,EAAE,CAAE,CAAA;AAChE,MAAA,aAAA,EAAc;AAAA,IAChB,CAAA;AAAA,IAEA,YAAA,EAAc,CAAC,UAAA,EAAY,WAAA,KAAgB;AACzC,MAAA,MAAM,QAAA,GAAW,KAAI,CAAE,cAAA,CAAe,KAAK,CAAC,EAAA,KAAO,EAAA,CAAG,EAAA,KAAO,UAAU,CAAA;AACvE,MAAA,IAAI,CAAC,QAAA,EAAU;AACf,MAAA,MAAM,SAAA,GAAY,KAAI,CAAE,UAAA,CAAW,KAAK,CAAC,CAAA,KAAM,CAAA,CAAE,EAAA,KAAO,WAAW,CAAA;AACnE,MAAA,IAAI,CAAC,SAAA,EAAW;AAChB,MAAA,IAAI,QAAA,CAAS,cAAA,KAAmB,SAAA,CAAU,cAAA,EAAgB;AACxD,QAAA,OAAA,CAAQ,IAAA;AAAA,UACN,CAAA,qDAAA,EAAwD,QAAA,CAAS,cAAc,CAAA,YAAA,EAAe,UAAU,cAAc,CAAA,mBAAA;AAAA,SACxH;AACA,QAAA;AAAA,MACF;AACA,MAAA,MAAM,GAAA,GAAM,WAAA,CAAY,QAAA,CAAS,cAAc,CAAA;AAC/C,MAAA,MAAM,aAAA,GAAgB,KAAK,WAAA,GAAc,GAAA,CAAI,YAAY,QAAA,CAAS,KAAK,IAAI,QAAA,CAAS,KAAA;AACpF,MAAA,GAAA,CAAI,CAAC,CAAA,MAAO;AAAA,QACV,UAAA,EAAY,EAAE,UAAA,CAAW,GAAA;AAAA,UAAI,CAAC,CAAA,KAC5B,CAAA,CAAE,EAAA,KAAO,WAAA,GAAc,EAAE,GAAG,CAAA,EAAG,KAAA,EAAO,aAAA,EAAe,MAAA,EAAQ,QAAA,CAAS,QAAO,GAAI;AAAA;AACnF,OACF,CAAE,CAAA;AACF,MAAA,aAAA,EAAc;AAAA,IAChB,CAAA;AAAA,IAEA,cAAA,EAAgB,CAAC,UAAA,KAAe;AAC9B,MAAA,GAAA,CAAI,CAAC,CAAA,MAAO;AAAA,QACV,cAAA,EAAgB,EAAE,cAAA,CAAe,MAAA,CAAO,CAAC,EAAA,KAAO,EAAA,CAAG,OAAO,UAAU;AAAA,OACtE,CAAE,CAAA;AACF,MAAA,aAAA,EAAc;AAAA,IAChB,CAAA;AAAA,IAEA,aAAA,EAAe,CAAC,WAAA,KAAgB;AAC9B,MAAA,MAAM,GAAA,GAAM,KAAI,CAAE,cAAA;AAClB,MAAA,MAAM,QAAA,GAAW,cAAc,GAAA,CAAI,MAAA,CAAO,CAAC,EAAA,KAAO,EAAA,CAAG,WAAA,KAAgB,WAAW,CAAA,GAAI,GAAA;AACpF,MAAA,OAAO,CAAC,GAAG,QAAQ,CAAA,CAAE,IAAA,CAAK,CAAC,CAAA,EAAG,CAAA,KAAM,CAAA,CAAE,OAAA,GAAU,CAAA,CAAE,OAAO,CAAA;AAAA,IAC3D,CAAA;AAAA,IAEA,OAAA,EAAS,CAAC,IAAA,KAAS;AACjB,MAAA,GAAA,CAAI,EAAE,MAAM,CAAA;AACZ,MAAA,aAAA,EAAc;AAAA,IAChB,CAAA;AAAA,IAEA,SAAA,EAAW,CAAC,MAAA,KAAW;AACrB,MAAA,GAAA,CAAI,EAAE,QAAQ,CAAA;AACd,MAAA,aAAA,EAAc;AAAA,IAChB;AAAA,GACF,CAAE,CAAA;AAEF,EAAA,SAAS,aAAA,GAAsB;AAC7B,IAAA,IAAI,UAAA,eAAyB,UAAU,CAAA;AACvC,IAAA,UAAA,GAAa,WAAW,MAAM;AAC5B,MAAA,MAAM,CAAA,GAAI,MAAM,QAAA,EAAS;AACzB,MAAA,OAAA,CAAQ,OAAA,CAAQ,KAAA;AAAA,QACd,aAAA,CAAc,OAAA,CAAQ,UAAA,EAAY,YAAY,CAAA;AAAA,QAC9C,mBAAA,CAAoB,CAAA,CAAE,UAAA,EAAY,WAAW;AAAA,OAC/C;AACA,MAAA,OAAA,CAAQ,OAAA,CAAQ,KAAA;AAAA,QACd,aAAA,CAAc,OAAA,CAAQ,UAAA,EAAY,OAAO,CAAA;AAAA,QACzC,IAAA,CAAK,SAAA,CAAU,CAAA,CAAE,cAAc;AAAA,OACjC;AACA,MAAA,OAAA,CAAQ,OAAA,CAAQ,MAAM,aAAA,CAAc,OAAA,CAAQ,YAAY,OAAO,CAAA,EAAG,EAAE,IAAI,CAAA;AACxE,MAAA,OAAA,CAAQ,OAAA,CAAQ,KAAA,CAAM,aAAA,CAAc,OAAA,CAAQ,UAAA,EAAY,QAAQ,CAAA,EAAG,IAAA,CAAK,SAAA,CAAU,CAAA,CAAE,MAAM,CAAC,CAAA;AAC3F,MAAA,UAAA,GAAa,IAAA;AAAA,IACf,GAAG,GAAG,CAAA;AAAA,EACR;AAEA,EAAA,OAAO,MAAA,CAAO,OAAO,KAAA,EAAO;AAAA,IAC1B,oBAAoB,CAAA,EAA0B;AAC5C,MAAA,WAAA,GAAc,CAAA;AAAA,IAChB;AAAA,GACD,CAAA;AACH;ACpPO,SAAS,kBAAA,GAAgF;AAC9F,EAAA,MAAM,GAAA,GAAMA,WAAW,eAAe,CAAA;AACtC,EAAA,IAAI,CAAC,GAAA,EAAK,MAAM,IAAI,MAAM,oEAAoE,CAAA;AAE9F,EAAA,MAAM,WAAA,GAAcA,WAAW,kBAAkB,CAAA;AACjD,EAAA,IAAI,CAAC,WAAA;AACH,IAAA,MAAM,IAAI,MAAM,uEAAuE,CAAA;AAEzF,EAAA,MAAM,MAAA,GAASC,QAAAA,CAAS,GAAA,CAAI,KAAA,EAAO,CAAC,CAAA,KAAM,CAAA,CAAE,UAAA,CAAW,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,EAAA,KAAO,WAAW,CAAC,CAAA;AAExF,EAAA,IAAI,CAAC,MAAA,EAAQ,MAAM,IAAI,KAAA,CAAM,CAAA,qCAAA,EAAwC,WAAW,CAAA,CAAA,CAAG,CAAA;AAEnF,EAAA,MAAM,uBAAuBA,QAAAA,CAAS,GAAA,CAAI,OAAO,CAAC,CAAA,KAAM,EAAE,oBAAoB,CAAA;AAC9E,EAAA,MAAM,wBAAwBA,QAAAA,CAAS,GAAA,CAAI,OAAO,CAAC,CAAA,KAAM,EAAE,qBAAqB,CAAA;AAEhF,EAAA,OAAO;AAAA,IACL,OAAO,MAAA,CAAO,KAAA;AAAA,IACd,QAAQ,MAAA,CAAO,MAAA;AAAA,IACf,QAAA,EAAU,CAAC,IAAA,KACT,oBAAA,CAAqB,aAAa,IAAkD,CAAA;AAAA,IACtF,WAAW,CAAC,GAAA,EAAK,UAAU,qBAAA,CAAsB,WAAA,EAAa,KAAc,KAAc;AAAA,GAC5F;AACF","file":"chunk-KSTEW2AF.js","sourcesContent":["import type { InstrumentSerializers, UndoStack, WorkspaceRecord } from './types';\n\n/** The storage key a lab writes one of its buckets under. Namespaced by\n * `storageKey` so two labs sharing an origin do not collide. */\nexport function labStorageKey(\n storageKey: string,\n bucket: 'workspaces' | 'saves' | 'theme' | 'layout',\n): string {\n return `lk:${storageKey}:${bucket}`;\n}\n\n/** Encode a string for the URL fragment. */\nexport function encodeUrlHash(value: string): string {\n return btoa(encodeURIComponent(value));\n}\n\n/** Decode a URL fragment written by `encodeUrlHash`, or `null` if it is\n * malformed. */\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\n/** A fresh, empty undo history. */\nexport function emptyUndoStack(): UndoStack {\n return { past: [], future: [] };\n}\n\ntype SerializedRecord = Omit<WorkspaceRecord, 'undoStack'>;\n\n/** Serialize workspaces for storage, running each instrument's own serializer\n * over its state. Undo history is deliberately dropped — it does not survive\n * a reload. */\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\n/** Rebuild workspaces from storage, running each instrument's deserializer\n * over its state and starting each with an empty undo history. Returns an\n * empty list rather than throwing on malformed input. */\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\n/** Persist to `localStorage` — state survives a reload and a new tab. Falls\n * back to no-ops when storage is unavailable. */\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\n/** Persist to `sessionStorage` — state survives a reload but not a new tab. */\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\n/** Persist into the URL fragment, so the page's link carries its state and\n * can be shared or bookmarked. */\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\n/** An in-memory store, discarded on reload. For tests, and for labs that\n * should start fresh every time. */\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\n/** Persists nothing and reads back nothing. */\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\n/** Context carrying the lab store. Prefer `useLabStore`; this is exported for\n * code that needs to read the context without subscribing. */\nexport const LabStoreContext = createContext<LabStoreCtx>(null);\n\n/** Provides a lab store to its subtree. */\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\n/** Subscribe to the whole lab store. Throws outside a `<LabStoreProvider>`. */\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\n/** Context carrying which workspace the subtree belongs to. */\nexport const WorkspaceIdContext = createContext<string | null>(null);\n\n/** Names the workspace its subtree belongs to, so an instrument's hooks can\n * find their own record in the store without being passed an id. */\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\n/** The id of the workspace this component is inside. Throws outside a\n * `<WorkspaceIdProvider>`. */\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\n/** Every mutation a lab store supports: managing workspaces, saving and\n * restoring snapshots, and setting the color mode. */\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 setLayout: (layout: Record<string, unknown>) => void;\n}\n\n/** A lab's store: its state and actions, plus the hook instruments use to\n * register how their state is serialized. */\nexport type LabStore = StoreApi<LabStoreState & LabStoreActions> & {\n registerSerializers: (s: InstrumentSerializers) => void;\n};\n\n/** Build a lab store, hydrating from storage if anything was saved under the\n * same key. Writes back are debounced. */\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 layoutRaw = options.storage.read(labStorageKey(options.storageKey, 'layout'));\n let hydratedLayout: Record<string, unknown> = {};\n if (layoutRaw) {\n try {\n hydratedLayout = JSON.parse(layoutRaw) as Record<string, unknown>;\n } catch {\n console.warn('[labkit] failed to parse saved layout, starting empty');\n hydratedLayout = {};\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 layout: hydratedLayout,\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 setLayout: (layout) => {\n set({ layout });\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 options.storage.write(labStorageKey(options.storageKey, 'layout'), JSON.stringify(s.layout));\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\n/** An instrument's own state and config, plus setters. Reads the surrounding\n * workspace id, so an instrument never has to know which workspace it is\n * running in. Throws outside a lab store and workspace. */\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"]}
package/dist/index.d.ts CHANGED
@@ -7,8 +7,8 @@ export { C as CanvasCapability, a as CanvasLayer, D as DragDropCapability, b as
7
7
  import * as react_jsx_runtime from 'react/jsx-runtime';
8
8
  import * as react from 'react';
9
9
  import { ReactNode } from 'react';
10
- import { S as StorageAdapter, o as LabMode, W as WorkspaceRecord, e as SavedSnapshot } from './_dts/useExperimentState-vrttakTt.js';
11
- export { C as CreateLabStoreOptions, E as ExperimentStateHandle, b as LabStoreContext, c as LabStoreProvider, d as LabStoreState, U as UndoStack, f as WorkspaceIdContext, g as WorkspaceIdProvider, i as createMemoryAdapter, l as localStorageAdapter, n as noneAdapter, s as sessionStorageAdapter, u as urlHashAdapter, j as useExperimentState, k as useLabStore, m as useWorkspaceId } from './_dts/useExperimentState-vrttakTt.js';
10
+ import { S as StorageAdapter, o as LabMode, W as WorkspaceRecord, e as SavedSnapshot } from './_dts/useExperimentState-D7EQnnwJ.js';
11
+ export { C as CreateLabStoreOptions, E as ExperimentStateHandle, b as LabStoreContext, c as LabStoreProvider, d as LabStoreState, U as UndoStack, f as WorkspaceIdContext, g as WorkspaceIdProvider, i as createMemoryAdapter, l as localStorageAdapter, n as noneAdapter, s as sessionStorageAdapter, u as urlHashAdapter, j as useExperimentState, k as useLabStore, m as useWorkspaceId } from './_dts/useExperimentState-D7EQnnwJ.js';
12
12
  export { LayerList, LayerListProps } from './layers/index.js';
13
13
  export { FpsMeter, ScaleIndicator, ScaleIndicatorProps, Sidebar, SidebarProps, StatusBar, StatusBarProps, Toolbar, ToolbarProps } from './primitives/index.js';
14
14
  export { LayerStack, LayerStackItem, LayerStackProps } from './ui/layers/index.js';
@@ -41,14 +41,6 @@ interface ValidationResult {
41
41
  * fields whose own constraints do not hold. */
42
42
  declare function validateConfigSchema(fields: ConfigField[]): ValidationResult;
43
43
 
44
- /** A grid's column and row counts. */
45
- interface GridDims {
46
- cols: number;
47
- rows: number;
48
- }
49
- /** The most nearly square grid that fits `count` items. */
50
- declare function gridDims(count: number): GridDims;
51
-
52
44
  /** Props for `<Lab>`. */
53
45
  interface LabProps {
54
46
  instruments: Instrument[];
@@ -80,6 +72,7 @@ interface LabContextValue {
80
72
  cloneWorkspace: (id: string) => void;
81
73
  closeWorkspace: (id: string) => void;
82
74
  resetWorkspace: (id: string) => void;
75
+ reorderWorkspaces: (ids: readonly string[]) => void;
83
76
  savedSnapshots: SavedSnapshot[];
84
77
  saveSnapshot: (workspaceId: string, name?: string) => void;
85
78
  loadSnapshot: (workspaceId: string, snapshotId: string) => void;
@@ -108,12 +101,62 @@ interface LabShellProps {
108
101
  * the workspace runtime is wanted too. */
109
102
  declare function LabShell({ title, children, header, footer, mode }: LabShellProps): react_jsx_runtime.JSX.Element;
110
103
 
111
- /** Props for `<WorkspaceGrid>`. */
104
+ /** A tile's persisted extent, keyed by the id its caller gave it. Grid resizes
105
+ * write `span`; `size` is here because a strategy swap would write that. */
106
+ type WorkspaceLayout = Record<string, {
107
+ size?: {
108
+ w?: number;
109
+ h?: number;
110
+ };
111
+ span?: {
112
+ cols?: number;
113
+ rows?: number;
114
+ };
115
+ }>;
112
116
  interface WorkspaceGridProps {
113
117
  children: ReactNode;
118
+ /**
119
+ * Stable id per child, positionally matched. Supply these whenever a tile
120
+ * can be closed from the middle: without them a tile is identified by its
121
+ * position, so closing one shifts every id after it and the panes inherit
122
+ * each other's dragged extents. `layout` and `reorderable` both key off
123
+ * these, so neither means much without them.
124
+ */
125
+ ids?: readonly string[];
126
+ /** Draggable seams between tiles. Off by default — an even tiling is the
127
+ * behavior every existing caller has. */
128
+ resizable?: boolean;
129
+ /** Let a tile be dragged to a new position. Off by default. The grid never
130
+ * reorders `children` itself: it reports the order a drop would produce and
131
+ * the caller commits it. */
132
+ reorderable?: boolean;
133
+ /** The full id list a drop would produce, in its new order. */
134
+ onReorder?: (ids: string[]) => void;
135
+ /** Extents from a previous session, applied to tiles as they register. */
136
+ layout?: WorkspaceLayout;
137
+ /** Fires when a tile's extent changes. Persist it and hand it back as
138
+ * `layout` to make a resize survive a reload. */
139
+ onLayoutChange?: (layout: WorkspaceLayout) => void;
140
+ gap?: number;
141
+ padding?: number;
142
+ /**
143
+ * Fixed tiling extent. Omit in an app — the grid measures its own box. Supply
144
+ * it where nothing measures, notably jsdom: at a zero measurement the grid
145
+ * renders no tiles at all.
146
+ */
147
+ viewport?: {
148
+ w: number;
149
+ h: number;
150
+ };
114
151
  }
115
- /** Lays its children out in the most nearly square grid that fits them. */
116
- declare function WorkspaceGrid({ children }: WorkspaceGridProps): react_jsx_runtime.JSX.Element;
152
+ /**
153
+ * Auto-balanced tiling of workspaces, `ceil(sqrt(n))` columns wide.
154
+ *
155
+ * Tiles are absolutely positioned at the rects `gridStrategy` computes, not
156
+ * laid out by CSS — `windease/styles.css` (folded into
157
+ * `@weasel-js/labkit/styles.css`) carries the rules that positioning depends on.
158
+ */
159
+ declare function WorkspaceGrid({ children, ids, resizable, reorderable, onReorder, layout, onLayoutChange, gap, padding, viewport, }: WorkspaceGridProps): react_jsx_runtime.JSX.Element;
117
160
 
118
161
  type TokenValue = string | number | readonly (string | number)[];
119
162
  /**
@@ -545,6 +588,12 @@ declare function closeWorkspace(workspaces: WorkspaceRecord[], id: string): Work
545
588
  /** Return a workspace to its instrument's defaults, keeping its id and its
546
589
  * place in the list. */
547
590
  declare function resetWorkspace(workspaces: WorkspaceRecord[], id: string, instruments: Instrument[]): WorkspaceRecord[];
591
+ /**
592
+ * Reorder to match `ids`. Ids the list doesn't mention keep their relative
593
+ * order at the end, and ids it names that no longer exist are dropped — a
594
+ * reorder that raced a close should not resurrect the closed workspace.
595
+ */
596
+ declare function reorderWorkspaces(workspaces: WorkspaceRecord[], ids: readonly string[]): WorkspaceRecord[];
548
597
 
549
- export { CheckboxRow, ColorRow, ConfigField, CurveField, DefaultSidebar, DefaultStatusBar, DefaultToolbar, EffectCard, EffectCardList, Instrument, Lab, LabContext, LabMode, LabShell, NumberRow, PropertyGroup, PropertyList, PropertyPanel, PropertyRow, SavedSnapshot, SelectRow, SliderRow, StorageAdapter, Subpanel, TextRow, ToggleRow, Workspace, WorkspaceChrome, WorkspaceGrid, WorkspaceRecord, addWorkspace, cloneWorkspace, closeWorkspace, defineInstrument, detectCapabilities, gridDims, interstellarTheme, resetWorkspace, useLabContext, validateConfigSchema };
550
- export type { CapabilityFlags, CheckboxRowProps, ColorRowProps, CurveFieldProps, CurveMark, DefaultSidebarProps, DefaultStatusBarProps, DefaultToolbarProps, EffectCardListItem, EffectCardListProps, EffectCardProps, GridDims, LabContextValue, LabProps, LabShellProps, NumberRowProps, PropertyGroupProps, PropertyListPack, PropertyListProps, PropertyPanelProps, PropertyRowLayout, PropertyRowProps, PropertyRowVariant, SelectRowProps, SidebarSlot, SliderRowProps, StatusBarSlot, SubpanelProps, TextRowProps, ToggleRowProps, ToolbarSlot, ValidationResult, WorkspaceChromeProps, WorkspaceGridProps, WorkspaceProps, WorkspaceSidebarContext, WorkspaceStatusBarContext, WorkspaceToolbarContext };
598
+ export { CheckboxRow, ColorRow, ConfigField, CurveField, DefaultSidebar, DefaultStatusBar, DefaultToolbar, EffectCard, EffectCardList, Instrument, Lab, LabContext, LabMode, LabShell, NumberRow, PropertyGroup, PropertyList, PropertyPanel, PropertyRow, SavedSnapshot, SelectRow, SliderRow, StorageAdapter, Subpanel, TextRow, ToggleRow, Workspace, WorkspaceChrome, WorkspaceGrid, WorkspaceRecord, addWorkspace, cloneWorkspace, closeWorkspace, defineInstrument, detectCapabilities, interstellarTheme, reorderWorkspaces, resetWorkspace, useLabContext, validateConfigSchema };
599
+ export type { CapabilityFlags, CheckboxRowProps, ColorRowProps, CurveFieldProps, CurveMark, DefaultSidebarProps, DefaultStatusBarProps, DefaultToolbarProps, EffectCardListItem, EffectCardListProps, EffectCardProps, LabContextValue, LabProps, LabShellProps, NumberRowProps, PropertyGroupProps, PropertyListPack, PropertyListProps, PropertyPanelProps, PropertyRowLayout, PropertyRowProps, PropertyRowVariant, SelectRowProps, SidebarSlot, SliderRowProps, StatusBarSlot, SubpanelProps, TextRowProps, ToggleRowProps, ToolbarSlot, ValidationResult, WorkspaceChromeProps, WorkspaceGridProps, WorkspaceProps, WorkspaceSidebarContext, WorkspaceStatusBarContext, WorkspaceToolbarContext };
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
- export { LayerStack } from './chunk-73PXCRCR.js';
2
- import { p, F } from './chunk-N5KTQKQA.js';
3
- import './chunk-3WPOGKUP.js';
1
+ export { LayerStack } from './chunk-DEWXYFEU.js';
2
+ import { p, F } from './chunk-BAPZPDDA.js';
3
+ import './chunk-DWV7SFKR.js';
4
4
  import { Sidebar, StatusBar, Toolbar } from './chunk-PMAU3SEE.js';
5
5
  export { FpsMeter, ScaleIndicator, Sidebar, StatusBar, Toolbar } from './chunk-PMAU3SEE.js';
6
- import { LabStoreContext, createLabStore, noneAdapter } from './chunk-T7OKNJTY.js';
7
- export { LabStoreContext, LabStoreProvider, WorkspaceIdContext, WorkspaceIdProvider, createMemoryAdapter, localStorageAdapter, noneAdapter, sessionStorageAdapter, urlHashAdapter, useExperimentState, useLabStore, useWorkspaceId } from './chunk-T7OKNJTY.js';
6
+ import { LabStoreContext, createLabStore, noneAdapter } from './chunk-KSTEW2AF.js';
7
+ export { LabStoreContext, LabStoreProvider, WorkspaceIdContext, WorkspaceIdProvider, createMemoryAdapter, localStorageAdapter, noneAdapter, sessionStorageAdapter, urlHashAdapter, useExperimentState, useLabStore, useWorkspaceId } from './chunk-KSTEW2AF.js';
8
8
  import { ControlPanel } from './chunk-5R2ATYPJ.js';
9
9
  export { ControlPanel } from './chunk-5R2ATYPJ.js';
10
10
  import { CanvasStack, screenToWorld } from './chunk-574LJAV4.js';
@@ -14,10 +14,12 @@ import { LayerList } from './chunk-SFL7NFKN.js';
14
14
  export { LayerList } from './chunk-SFL7NFKN.js';
15
15
  import { createEventBus, redo, undo, pushSnapshot } from './chunk-RL2LOLNI.js';
16
16
  export { clearUndo, createEventBus, emptyStack, pushSnapshot, redo, undo } from './chunk-RL2LOLNI.js';
17
- import { createContext, useContext, useMemo, useRef, useState, Children, useEffect, useCallback, useLayoutEffect } from 'react';
17
+ import { createContext, useContext, useMemo, useRef, useState, Children, useLayoutEffect, useEffect, useCallback } from 'react';
18
18
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
19
19
  import { useStore } from 'zustand/react';
20
20
  import { createPortal } from 'react-dom';
21
+ import { asNodeId, Store, createNode, gridStrategy } from 'windease';
22
+ import { DragHandle, Provider, StrategyRegistryProvider, DragProvider, Container } from 'windease/react';
21
23
 
22
24
  // src/instrument/capabilityDetector.ts
23
25
  function detectCapabilities(instrument) {
@@ -88,14 +90,6 @@ function validateConfigSchema(fields) {
88
90
  return { valid: errors.length === 0, errors };
89
91
  }
90
92
 
91
- // src/lab/gridDims.ts
92
- function gridDims(count) {
93
- if (count <= 1) return { cols: 1, rows: 1 };
94
- const cols = Math.ceil(Math.sqrt(count));
95
- const rows = Math.ceil(count / cols);
96
- return { cols, rows };
97
- }
98
-
99
93
  // ../theme/dist/chunk-STFENPNX.js
100
94
  var THEME_SOURCES = {
101
95
  "weasel": {
@@ -1390,6 +1384,13 @@ function resetWorkspace(workspaces, id, instruments) {
1390
1384
  };
1391
1385
  return [...workspaces.slice(0, idx), reset, ...workspaces.slice(idx + 1)];
1392
1386
  }
1387
+ function reorderWorkspaces(workspaces, ids) {
1388
+ const byId = new Map(workspaces.map((w) => [w.id, w]));
1389
+ const named = ids.map((id) => byId.get(id)).filter((w) => w !== void 0);
1390
+ const seen = new Set(named.map((w) => w.id));
1391
+ const rest = workspaces.filter((w) => !seen.has(w.id));
1392
+ return [...named, ...rest];
1393
+ }
1393
1394
  function query() {
1394
1395
  if (typeof window === "undefined" || !window.matchMedia) return null;
1395
1396
  return window.matchMedia("(prefers-color-scheme: light)");
@@ -1421,14 +1422,107 @@ function LabShell({ title, children, header, footer, mode = "auto" }) {
1421
1422
  ] });
1422
1423
  return outer ? shell : /* @__PURE__ */ jsx(ThemeProvider, { theme: interstellarTheme, mode: resolved, children: shell });
1423
1424
  }
1424
- function WorkspaceGrid({ children }) {
1425
- const count = Children.count(children);
1426
- const { cols, rows } = gridDims(count);
1427
- const style = {
1428
- "--lk-grid-cols": String(cols),
1429
- "--lk-grid-rows": String(rows)
1430
- };
1431
- return /* @__PURE__ */ jsx("div", { className: "lk-workspace-grid", style, children });
1425
+ var ZONE_ID = asNodeId("lk-workspaces");
1426
+ var STRATEGIES = { grid: gridStrategy };
1427
+ var KIND = "workspace";
1428
+ function extentOf(store, id) {
1429
+ const p2 = store.getNode(id)?.membership?.placement;
1430
+ if (!p2) return null;
1431
+ const out = {};
1432
+ if (p2.size) out.size = p2.size;
1433
+ if (p2.span) out.span = p2.span;
1434
+ return Object.keys(out).length > 0 ? out : null;
1435
+ }
1436
+ function WorkspaceGrid({
1437
+ children,
1438
+ ids,
1439
+ resizable = false,
1440
+ reorderable = false,
1441
+ onReorder,
1442
+ layout,
1443
+ onLayoutChange,
1444
+ gap = 12,
1445
+ padding = 0,
1446
+ viewport
1447
+ }) {
1448
+ const items = Children.toArray(children);
1449
+ const idKey = ids ? ids.join(",") : `#${items.length}`;
1450
+ const nodeIds = useMemo(() => items.map((_, i) => asNodeId(ids?.[i] ?? `lk-ws-${i}`)), [idKey]);
1451
+ const layoutRef = useRef(layout);
1452
+ layoutRef.current = layout;
1453
+ const onLayoutChangeRef = useRef(onLayoutChange);
1454
+ onLayoutChangeRef.current = onLayoutChange;
1455
+ const storeRef = useRef(null);
1456
+ if (storeRef.current === null) {
1457
+ const store2 = new Store();
1458
+ store2.registerNode(
1459
+ createNode({
1460
+ kind: "zone",
1461
+ id: ZONE_ID,
1462
+ container: { strategyId: "grid", config: { resizable, gap, padding } }
1463
+ })
1464
+ );
1465
+ storeRef.current = store2;
1466
+ }
1467
+ const store = storeRef.current;
1468
+ useLayoutEffect(() => {
1469
+ store.updateContainerConfig(ZONE_ID, { resizable, gap, padding });
1470
+ }, [store, resizable, gap, padding]);
1471
+ useLayoutEffect(() => {
1472
+ const present = new Set(store.getContainerView(ZONE_ID)?.childOrder ?? []);
1473
+ const wanted = new Set(nodeIds);
1474
+ for (const id of present) {
1475
+ if (!wanted.has(id)) store.unregisterNode(id);
1476
+ }
1477
+ for (const id of nodeIds) {
1478
+ if (present.has(id)) continue;
1479
+ store.registerNode(createNode({ kind: KIND, id, parentId: ZONE_ID, focus: true }));
1480
+ store.showNode(id);
1481
+ const saved = layoutRef.current?.[id];
1482
+ if (saved) store.patchPlacement(id, saved);
1483
+ }
1484
+ store.setChildOrder(ZONE_ID, [...nodeIds]);
1485
+ }, [store, nodeIds]);
1486
+ useEffect(() => {
1487
+ if (!onLayoutChange) return;
1488
+ return store.events.on("node.placementChanged", () => {
1489
+ const next = {};
1490
+ for (const child of store.getChildren(ZONE_ID)) {
1491
+ const extent = extentOf(store, child.id);
1492
+ if (extent) next[child.id] = extent;
1493
+ }
1494
+ onLayoutChangeRef.current?.(next);
1495
+ });
1496
+ }, [store, onLayoutChange]);
1497
+ const commitOrder = useCallback(
1498
+ (nextIds) => onReorder?.(nextIds.map(String)),
1499
+ [onReorder]
1500
+ );
1501
+ const chrome = useMemo(() => {
1502
+ const byId = new Map(nodeIds.map((id, i) => [id, items[i]]));
1503
+ return {
1504
+ [KIND]: ({ node }) => {
1505
+ const content = byId.get(node.id) ?? null;
1506
+ if (!reorderable) return content;
1507
+ return /* @__PURE__ */ jsxs("div", { className: "lk-workspace-tile", children: [
1508
+ /* @__PURE__ */ jsx(DragHandle, { nodeId: node.id, className: "lk-workspace-tile__grip", children: /* @__PURE__ */ jsx("span", { className: "lk-workspace-tile__grip-dots", "aria-hidden": "true" }) }),
1509
+ /* @__PURE__ */ jsx("div", { className: "lk-workspace-tile__body", children: content })
1510
+ ] });
1511
+ }
1512
+ };
1513
+ }, [nodeIds, items, reorderable]);
1514
+ const grid = /* @__PURE__ */ jsx(
1515
+ Container,
1516
+ {
1517
+ parentId: ZONE_ID,
1518
+ chrome,
1519
+ className: "lk-workspace-grid windease-zone",
1520
+ affordances: resizable,
1521
+ viewport,
1522
+ onChildOrderChange: reorderable ? commitOrder : void 0
1523
+ }
1524
+ );
1525
+ return /* @__PURE__ */ jsx(Provider, { store, children: /* @__PURE__ */ jsx(StrategyRegistryProvider, { strategies: STRATEGIES, children: reorderable ? /* @__PURE__ */ jsx(DragProvider, { children: grid }) : grid }) });
1432
1526
  }
1433
1527
  function buildStore(instruments, defaultInstrument, storage, storageKey, initialMode) {
1434
1528
  const store = createLabStore({ storageKey, storage, initialMode });
@@ -1484,6 +1578,7 @@ function Lab({
1484
1578
  const workspaces = useStore(store, (s) => s.workspaces);
1485
1579
  const savedSnapshots = useStore(store, (s) => s.savedSnapshots);
1486
1580
  const modeValue = useStore(store, (s) => s.mode);
1581
+ const layout = useStore(store, (s) => s.layout);
1487
1582
  const resolvedMode = useResolvedMode(modeValue);
1488
1583
  useEffect(() => {
1489
1584
  if (mode && mode !== store.getState().mode) {
@@ -1512,6 +1607,9 @@ function Lab({
1512
1607
  const next = closeWorkspace(store.getState().workspaces, id);
1513
1608
  replaceWorkspaces(next);
1514
1609
  },
1610
+ reorderWorkspaces: (ids) => {
1611
+ replaceWorkspaces(reorderWorkspaces(store.getState().workspaces, ids));
1612
+ },
1515
1613
  resetWorkspace: (id) => {
1516
1614
  const next = resetWorkspace(store.getState().workspaces, id, instruments);
1517
1615
  const record = next.find((w) => w.id === id);
@@ -1547,7 +1645,18 @@ function Lab({
1547
1645
  mode: resolvedMode,
1548
1646
  className: "lk-lab",
1549
1647
  style: backdropStyle,
1550
- children: /* @__PURE__ */ jsx(LabShell, { title: title ?? "Labkit", mode: modeValue, header: children, children: /* @__PURE__ */ jsx(WorkspaceGrid, { children: workspaces.map((w) => /* @__PURE__ */ jsx(Workspace, { id: w.id }, w.id)) }) })
1648
+ children: /* @__PURE__ */ jsx(LabShell, { title: title ?? "Labkit", mode: modeValue, header: children, children: /* @__PURE__ */ jsx(
1649
+ WorkspaceGrid,
1650
+ {
1651
+ ids: workspaces.map((w) => w.id),
1652
+ resizable: true,
1653
+ reorderable: true,
1654
+ onReorder: (ids) => contextValue.reorderWorkspaces(ids),
1655
+ layout,
1656
+ onLayoutChange: (next) => store.getState().setLayout(next),
1657
+ children: workspaces.map((w) => /* @__PURE__ */ jsx(Workspace, { id: w.id }, w.id))
1658
+ }
1659
+ ) })
1551
1660
  }
1552
1661
  ) }) });
1553
1662
  }
@@ -2169,6 +2278,6 @@ function ToggleRow({
2169
2278
  }) }) });
2170
2279
  }
2171
2280
 
2172
- export { CheckboxRow, ColorRow, CurveField, DefaultSidebar, DefaultStatusBar, DefaultToolbar, EffectCard, EffectCardList, Lab, LabContext, LabShell, NumberRow, PropertyGroup, PropertyList, PropertyPanel, PropertyRow, SelectRow, SliderRow, Subpanel, TextRow, ToggleRow, Workspace, WorkspaceChrome, WorkspaceGrid, addWorkspace, cloneWorkspace, closeWorkspace, defineInstrument, detectCapabilities, gridDims, interstellarTheme, resetWorkspace, useLabContext, validateConfigSchema };
2281
+ export { CheckboxRow, ColorRow, CurveField, DefaultSidebar, DefaultStatusBar, DefaultToolbar, EffectCard, EffectCardList, Lab, LabContext, LabShell, NumberRow, PropertyGroup, PropertyList, PropertyPanel, PropertyRow, SelectRow, SliderRow, Subpanel, TextRow, ToggleRow, Workspace, WorkspaceChrome, WorkspaceGrid, addWorkspace, cloneWorkspace, closeWorkspace, defineInstrument, detectCapabilities, interstellarTheme, reorderWorkspaces, resetWorkspace, useLabContext, validateConfigSchema };
2173
2282
  //# sourceMappingURL=index.js.map
2174
2283
  //# sourceMappingURL=index.js.map