@weasel-js/labkit 1.4.2 → 1.4.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 (117) hide show
  1. package/README.md +20 -2
  2. package/dist/_dts/{CanvasStackContext-kjILVnPj.d.ts → CanvasStackContext-kTEZvEgE.d.ts} +1 -1
  3. package/dist/_dts/{PrefsForm.d-CgxUequc.d.ts → PrefsForm.d-DDHFANkc.d.ts} +1 -1
  4. package/dist/_dts/{frac-A4R6v4ld.d.ts → frac-z7ker2Vx.d.ts} +11 -7
  5. package/dist/_dts/{index-CsU9WhjM.d.ts → index-Blv9uQzu.d.ts} +42 -9
  6. package/dist/_dts/types-B9_zrHmb.d.ts +166 -0
  7. package/dist/_dts/{types-BbXpvQa8.d.ts → types-BP2OCcpg.d.ts} +21 -3
  8. package/dist/_dts/types-x92Kfeme.d.ts +62 -0
  9. package/dist/_dts/{useTrialState-DKjqpv20.d.ts → useTrialState-D6Vb3T-g.d.ts} +7 -7
  10. package/dist/canvas/index.d.ts +9 -8
  11. package/dist/canvas/index.js +1 -2
  12. package/dist/chrome/index.d.ts +7 -6
  13. package/dist/chrome/index.js +5 -5
  14. package/dist/{chunk-ULDW42CR.js → chunk-2P6PP5N4.js} +33 -23
  15. package/dist/chunk-2P6PP5N4.js.map +1 -0
  16. package/dist/{chunk-W2FJR5FF.js → chunk-64ZCN3DA.js} +74 -76
  17. package/dist/chunk-64ZCN3DA.js.map +1 -0
  18. package/dist/{chunk-NSOVI3AZ.js → chunk-BKFVHKJH.js} +6 -6
  19. package/dist/{chunk-NSOVI3AZ.js.map → chunk-BKFVHKJH.js.map} +1 -1
  20. package/dist/chunk-E44SU6XS.js +50 -0
  21. package/dist/chunk-E44SU6XS.js.map +1 -0
  22. package/dist/{chunk-WS6ZRV75.js → chunk-FQMJUVHJ.js} +3 -3
  23. package/dist/{chunk-WS6ZRV75.js.map → chunk-FQMJUVHJ.js.map} +1 -1
  24. package/dist/{chunk-FJG4PHTL.js → chunk-I6JCVE24.js} +3 -3
  25. package/dist/chunk-I6JCVE24.js.map +1 -0
  26. package/dist/{chunk-UDXOYZEC.js → chunk-MOM3GOVY.js} +4 -4
  27. package/dist/{chunk-UDXOYZEC.js.map → chunk-MOM3GOVY.js.map} +1 -1
  28. package/dist/{chunk-TN7YSJVU.js → chunk-RS3HRQWU.js} +3 -3
  29. package/dist/{chunk-TN7YSJVU.js.map → chunk-RS3HRQWU.js.map} +1 -1
  30. package/dist/{chunk-UTOEDPNU.js → chunk-SMHP6XZ4.js} +7 -6
  31. package/dist/chunk-SMHP6XZ4.js.map +1 -0
  32. package/dist/{chunk-D5KQ5OY6.js → chunk-TJ7QY3OC.js} +3 -3
  33. package/dist/{chunk-D5KQ5OY6.js.map → chunk-TJ7QY3OC.js.map} +1 -1
  34. package/dist/{chunk-XKENZTNE.js → chunk-U3IYHIAE.js} +71 -55
  35. package/dist/chunk-U3IYHIAE.js.map +1 -0
  36. package/dist/chunk-W3ECWC2K.js +7335 -0
  37. package/dist/chunk-W3ECWC2K.js.map +1 -0
  38. package/dist/controls/index.d.ts +5 -4
  39. package/dist/controls/index.js +3 -3
  40. package/dist/dragdrop/index.d.ts +6 -5
  41. package/dist/dragdrop/index.js +1 -2
  42. package/dist/index.d.ts +139 -26
  43. package/dist/index.js +309 -144
  44. package/dist/index.js.map +1 -1
  45. package/dist/layers/index.d.ts +7 -6
  46. package/dist/layers/index.js +2 -3
  47. package/dist/loupe/index.d.ts +8 -7
  48. package/dist/loupe/index.js +1 -2
  49. package/dist/passthrough/weasel-canvas.d.ts +1 -3
  50. package/dist/passthrough/weasel-canvas.js +1 -1
  51. package/dist/passthrough/weasel-canvas.js.map +1 -1
  52. package/dist/passthrough/weasel-ui.d.ts +98 -20
  53. package/dist/passthrough/weasel-ui.js +1 -2
  54. package/dist/primitives/index.js +3 -4
  55. package/dist/state/index.d.ts +6 -3
  56. package/dist/state/index.js +3 -2
  57. package/dist/state/index.js.map +1 -1
  58. package/dist/styles.css +82 -15
  59. package/dist/surface/index.js +1 -2
  60. package/dist/ui/layers/index.js +1 -2
  61. package/dist/undo/index.d.ts +6 -6
  62. package/package.json +7 -7
  63. package/src/annotations/ExportMenu.test.tsx +16 -0
  64. package/src/annotations/ExportMenu.tsx +0 -5
  65. package/src/annotations/frac.test.ts +4 -4
  66. package/src/annotations/frac.ts +3 -2
  67. package/src/annotations/index.ts +1 -1
  68. package/src/annotations/store.ts +2 -2
  69. package/src/annotations/svgNodes.ts +1 -2
  70. package/src/config/builder.test.ts +56 -2
  71. package/src/config/builder.ts +73 -13
  72. package/src/config/index.ts +16 -1
  73. package/src/config/path.test.ts +109 -0
  74. package/src/config/path.ts +76 -0
  75. package/src/config/resolve.test.ts +93 -2
  76. package/src/config/resolve.ts +72 -29
  77. package/src/config/types.ts +99 -9
  78. package/src/config/visible.ts +6 -4
  79. package/src/controls/ControlPanel.stories.tsx +38 -4
  80. package/src/controls/ControlPanel.test.tsx +114 -0
  81. package/src/controls/ControlPanel.tsx +99 -50
  82. package/src/index.ts +17 -2
  83. package/src/instrument/serializers.ts +36 -0
  84. package/src/instrument/types.ts +4 -2
  85. package/src/lab/Lab.test.tsx +29 -1
  86. package/src/lab/Lab.tsx +10 -1
  87. package/src/lab/LabShell.test.tsx +8 -0
  88. package/src/lab/LabShell.tsx +17 -3
  89. package/src/lab/LabSwitcher.less +85 -0
  90. package/src/lab/LabSwitcher.test.tsx +96 -0
  91. package/src/lab/LabSwitcher.tsx +112 -0
  92. package/src/lab/index.ts +2 -0
  93. package/src/primitives/ZoomControl.less +6 -2
  94. package/src/primitives/ZoomControl.tsx +1 -0
  95. package/src/state/helpers.test.ts +20 -0
  96. package/src/state/helpers.ts +17 -1
  97. package/src/state/store.test.ts +155 -1
  98. package/src/state/store.ts +32 -26
  99. package/src/state/types.ts +24 -3
  100. package/src/state/useTrialState.ts +1 -1
  101. package/src/styles.less +1 -0
  102. package/src/theme/base.less +4 -4
  103. package/src/trial/Trial.config.test.tsx +44 -0
  104. package/src/trial/Trial.tsx +3 -3
  105. package/src/trial/TrialChrome.tsx +10 -5
  106. package/src/trial/trialOps.ts +7 -10
  107. package/dist/_dts/types-lg4TSCb2.d.ts +0 -164
  108. package/dist/_dts/weasel-canvas-FJTFi3ZZ.d.ts +0 -5041
  109. package/dist/chunk-54ZWZ5FQ.js +0 -29997
  110. package/dist/chunk-54ZWZ5FQ.js.map +0 -1
  111. package/dist/chunk-FJG4PHTL.js.map +0 -1
  112. package/dist/chunk-ISSVF5PT.js +0 -7263
  113. package/dist/chunk-ISSVF5PT.js.map +0 -1
  114. package/dist/chunk-ULDW42CR.js.map +0 -1
  115. package/dist/chunk-UTOEDPNU.js.map +0 -1
  116. package/dist/chunk-W2FJR5FF.js.map +0 -1
  117. package/dist/chunk-XKENZTNE.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,43 +1,45 @@
1
1
  import { createEventBus, pushSnapshot, redo, undo } from './chunk-RL2LOLNI.js';
2
2
  export { clearUndo, createEventBus, emptyStack, pushSnapshot, redo, undo } from './chunk-RL2LOLNI.js';
3
- import { useDragDrop, DragOverlay, Palette } from './chunk-TN7YSJVU.js';
4
- import { useSurfaceOptional, useSurfaceCanvas, useTileId, useTiledSurface, SurfaceContext, SurfaceCanvasContext } from './chunk-WS6ZRV75.js';
5
- export { SurfaceCanvasContext, SurfaceContext, composeRects, rectsEqual, toDeviceRect, useSurface, useSurfaceCanvas, useSurfaceOptional, useSurfaceTile, useTileId, useTiledSurface } from './chunk-WS6ZRV75.js';
3
+ import { useDragDrop, DragOverlay, Palette } from './chunk-RS3HRQWU.js';
4
+ import { useSurfaceOptional, useSurfaceCanvas, useTileId, useTiledSurface, SurfaceContext, SurfaceCanvasContext } from './chunk-FQMJUVHJ.js';
5
+ export { SurfaceCanvasContext, SurfaceContext, composeRects, rectsEqual, toDeviceRect, useSurface, useSurfaceCanvas, useSurfaceOptional, useSurfaceTile, useTileId, useTiledSurface } from './chunk-FQMJUVHJ.js';
6
6
  import { useJob } from './chunk-HXXTULDN.js';
7
7
  export { useJob } from './chunk-HXXTULDN.js';
8
- import { ANNOTATION_WEASEL_TOOLS, annotationToolInfo, suppressContributions, mergeContributions, builtinContributions, TitleBarRegion, ToolbarRegion, PaletteRegion, ViewportRegion, SidebarRegion, StatusRegion, AnnotationsContext } from './chunk-W2FJR5FF.js';
9
- export { ANNOTATION_TOOLS, ANNOTATION_WEASEL_TOOLS, AnnotationsContext, ExportMenu, MarkList, PaletteRegion, SidebarRegion, StatusRegion, TitleBarRegion, ToolbarRegion, ViewportRegion, annotationToolInfo, builtinContributions, mergeContributions, suppressContributions, useAnnotations, useAnnotationsOptional } from './chunk-W2FJR5FF.js';
10
- import { JobProgress } from './chunk-UDXOYZEC.js';
11
- export { FloatingPanel, JobProgress, Legend, Sidebar } from './chunk-UDXOYZEC.js';
12
- export { FpsMeter, ScaleIndicator, StatusBar, Toolbar, ZoomControl } from './chunk-UTOEDPNU.js';
13
- import { createLabStore, panelKey, noneAdapter } from './chunk-ULDW42CR.js';
14
- export { CURRENT_DOCUMENT_VERSION, createMemoryAdapter, dockPanel, labDocumentKey, localStorageAdapter, noneAdapter, panelKey, quarantineKey, sessionStorageAdapter, undockPanel, urlHashAdapter, useTrialState } from './chunk-ULDW42CR.js';
8
+ import { ANNOTATION_WEASEL_TOOLS, annotationToolInfo, suppressContributions, mergeContributions, builtinContributions, TitleBarRegion, ToolbarRegion, PaletteRegion, ViewportRegion, SidebarRegion, StatusRegion, AnnotationsContext } from './chunk-64ZCN3DA.js';
9
+ export { ANNOTATION_TOOLS, ANNOTATION_WEASEL_TOOLS, AnnotationsContext, ExportMenu, MarkList, PaletteRegion, SidebarRegion, StatusRegion, TitleBarRegion, ToolbarRegion, ViewportRegion, annotationToolInfo, builtinContributions, mergeContributions, suppressContributions, useAnnotations, useAnnotationsOptional } from './chunk-64ZCN3DA.js';
10
+ import { JobProgress } from './chunk-MOM3GOVY.js';
11
+ export { FloatingPanel, JobProgress, Legend, Sidebar } from './chunk-MOM3GOVY.js';
12
+ export { FpsMeter, ScaleIndicator, StatusBar, Toolbar, ZoomControl } from './chunk-SMHP6XZ4.js';
13
+ import { newId, createLabStore, panelKey, noneAdapter } from './chunk-2P6PP5N4.js';
14
+ export { CURRENT_DOCUMENT_VERSION, createMemoryAdapter, dockPanel, labDocumentKey, localStorageAdapter, noneAdapter, panelKey, quarantineKey, sessionStorageAdapter, undockPanel, urlHashAdapter, useTrialState } from './chunk-2P6PP5N4.js';
15
15
  import { LabStoreContext, TrialIdProvider } from './chunk-2YILQ7Y5.js';
16
16
  export { LabStoreContext, LabStoreProvider, TrialIdContext, TrialIdProvider, useLabStore, useTrialId } from './chunk-2YILQ7Y5.js';
17
- import { fromConfigFields } from './chunk-XKENZTNE.js';
18
- export { ControlPanel, fromConfigFields, isLeafVisible } from './chunk-XKENZTNE.js';
19
- import { CanvasStack } from './chunk-FJG4PHTL.js';
20
- export { CanvasStack, PITCH_LIMIT, clampPitch, orbitAfterDrag, orbitAfterWheel, useOrbit, usePanZoom, wrapYaw } from './chunk-FJG4PHTL.js';
21
- import { LayerList } from './chunk-NSOVI3AZ.js';
22
- export { LayerList } from './chunk-NSOVI3AZ.js';
17
+ import { fromConfigFields } from './chunk-U3IYHIAE.js';
18
+ export { ControlPanel, fromConfigFields, isLeafVisible } from './chunk-U3IYHIAE.js';
19
+ import { hasConfigPath, schemaNodeAtPath, withValueAtPath, fillConfigDefaults } from './chunk-E44SU6XS.js';
20
+ export { fillConfigDefaults, hasConfigPath, schemaNodeAtPath, valueAtPath, withValueAtPath } from './chunk-E44SU6XS.js';
21
+ import { CanvasStack } from './chunk-I6JCVE24.js';
22
+ export { CanvasStack, PITCH_LIMIT, clampPitch, orbitAfterDrag, orbitAfterWheel, useOrbit, usePanZoom, wrapYaw } from './chunk-I6JCVE24.js';
23
+ import { LayerList } from './chunk-BKFVHKJH.js';
24
+ export { LayerList } from './chunk-BKFVHKJH.js';
23
25
  import './chunk-SBXNQW4G.js';
24
- import { E, w, T, C, h, r4 } from './chunk-ISSVF5PT.js';
25
- export { M2 as CheckboxRow, j2 as ColorRow, y4 as CurveField, R as EffectCard, B as EffectCardList, c3 as LayerStack, P2 as NumberRow, V as PropertyGroup, T2 as PropertyList, w2 as PropertyPanel, D as PropertyRow, E2 as PropertySpan, F as SelectRow, k as SliderRow, L2 as Subpanel, N2 as TextRow, I as ToggleRow } from './chunk-ISSVF5PT.js';
26
- import { resolveLoupe, TrialLoupe } from './chunk-D5KQ5OY6.js';
27
- export { CanvasLoupe, DomLoupe, LOUPE_DEFAULTS, LoupeBubble, TrialLoupe, drawCanvasLens, lensCamera, lensSourceRect, resolveLoupe, sampleStack, useLoupe } from './chunk-D5KQ5OY6.js';
26
+ import { E, w, T, C, g, r6 } from './chunk-W3ECWC2K.js';
27
+ export { N2 as CheckboxRow, M2 as ColorRow, y3 as CurveField, z as EffectCard, V as EffectCardList, c3 as LayerStack, F as NumberRow, H as PropertyGroup, T2 as PropertyList, w2 as PropertyPanel, D as PropertyRow, E2 as PropertySpan, I as SelectRow, A2 as SliderRow, R as Subpanel, P2 as TextRow, L2 as ToggleRow } from './chunk-W3ECWC2K.js';
28
+ import { resolveLoupe, TrialLoupe } from './chunk-TJ7QY3OC.js';
29
+ export { CanvasLoupe, DomLoupe, LOUPE_DEFAULTS, LoupeBubble, TrialLoupe, drawCanvasLens, lensCamera, lensSourceRect, resolveLoupe, sampleStack, useLoupe } from './chunk-TJ7QY3OC.js';
28
30
  export { centerOn, zoomAt } from './chunk-CTRKTLYZ.js';
29
31
  import { applyCamera } from './chunk-2AYGEN57.js';
30
32
  export { DEFAULT_FRAME, applyCamera, resolveFrame, screenToWorld, worldToScreen } from './chunk-2AYGEN57.js';
31
33
  export { CanvasStackContext } from './chunk-CQLQPQ4P.js';
32
- import { textCommand, linePath, markerDrawCommands, ellipsePath, rectPath, createScene, asNodeId as asNodeId$1, WeaselProvider, SceneCanvas, sceneFromJSON, PathBuilder, renderSceneToPixels, useActiveToolContext, resolveStrokeWidth, getMarker, getPaintKind, boundsOfPath, tileGeometry, PATH_M, PATH_L, PATH_C, PATH_Q, PATH_Z, SCRIPT_METRICS } from './chunk-54ZWZ5FQ.js';
33
- import { createContext, useState, useRef, useEffect, useContext, useMemo, Children, useLayoutEffect, useCallback, useSyncExternalStore } from 'react';
34
+ import { textCommand, linePath, markerDrawCommands, ellipsePath, rectPath, createScene, asNodeId as asNodeId$1, WeaselProvider, SceneCanvas, sceneFromJSON, PathBuilder, renderSceneToPixels, useActiveToolContext, getMarker, getPaintKind, boundsOfPath, PATH_M, PATH_L, PATH_C, PATH_Q, PATH_Z, SCRIPT_METRICS } from '@weasel-js/core';
35
+ import { tileGeometry } from '@weasel-js/core/patterns-builtin';
36
+ import { createContext, useState, useRef, useEffect, useContext, useMemo, useId, useCallback, Children, useLayoutEffect, useSyncExternalStore } from 'react';
34
37
  import { createPortal } from 'react-dom';
35
38
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
36
39
  import { useStore } from 'zustand/react';
37
40
  import { asNodeId, Store, createNode, stripStrategy, floatingStrategy, gridStrategy } from 'windease';
38
41
  import { Provider, StrategyRegistryProvider, Container, DragProvider, useDragHandle } from 'windease/react';
39
42
 
40
- // ../svg/dist/index.js
41
43
  var IDENTITY_MATRIX = [1, 0, 0, 1, 0, 0];
42
44
  function multiply(a, b) {
43
45
  return [
@@ -420,20 +422,25 @@ function paintAttrs(paint, name, registry, includeOpacity = true) {
420
422
  const id = registry.register(paint.paint);
421
423
  return [`${name}="url(#${id})"`];
422
424
  }
425
+ function strokeWidthAttrs(width) {
426
+ if (typeof width === "number") return [`stroke-width="${trimNumber(width)}"`];
427
+ return [`stroke-width="${trimNumber(width.px)}"`, 'vector-effect="non-scaling-stroke"'];
428
+ }
423
429
  function coreStrokeAttrs(stroke, registry) {
424
- if (!stroke) return [];
425
- const width = resolveStrokeWidth(stroke.width ?? 1, 1);
426
- if (!(width > 0)) return [];
430
+ const paint = stroke?.paint;
431
+ if (!stroke || !paint) return [];
432
+ const width = stroke.width ?? 1;
433
+ if (!((typeof width === "object" ? width.px : width) > 0)) return [];
427
434
  const attrs = [];
428
- if ("color" in stroke.paint) {
429
- attrs.push(`stroke="${stroke.paint.color}"`);
430
- if (stroke.paint.opacity != null && stroke.paint.opacity !== 1) {
431
- attrs.push(`stroke-opacity="${trimNumber(stroke.paint.opacity)}"`);
435
+ if ("color" in paint) {
436
+ attrs.push(`stroke="${paint.color}"`);
437
+ if (paint.opacity != null && paint.opacity !== 1) {
438
+ attrs.push(`stroke-opacity="${trimNumber(paint.opacity)}"`);
432
439
  }
433
440
  } else {
434
- attrs.push(`stroke="url(#${registry.register(stroke.paint)})"`);
441
+ attrs.push(`stroke="url(#${registry.register(paint)})"`);
435
442
  }
436
- attrs.push(`stroke-width="${trimNumber(width)}"`);
443
+ attrs.push(...strokeWidthAttrs(width));
437
444
  if (stroke.cap) attrs.push(`stroke-linecap="${stroke.cap}"`);
438
445
  if (stroke.join) attrs.push(`stroke-linejoin="${stroke.join}"`);
439
446
  if (stroke.dash && stroke.dash.length > 0) {
@@ -456,7 +463,7 @@ function coreStrokeAttrs(stroke, registry) {
456
463
  }
457
464
  function strokeAttrsFor(stroke, registry) {
458
465
  const attrs = paintAttrs(stroke.paint, "stroke", registry, false);
459
- attrs.push(`stroke-width="${trimNumber(stroke.width)}"`);
466
+ attrs.push(...strokeWidthAttrs(stroke.width));
460
467
  const opacity = stroke.opacity ?? (stroke.paint.kind === "solid" ? stroke.paint.opacity : void 0);
461
468
  if (opacity != null && opacity !== 1) {
462
469
  attrs.push(`stroke-opacity="${trimNumber(opacity)}"`);
@@ -549,7 +556,9 @@ function textXml(node, registry, namespaces) {
549
556
  }
550
557
  const decoration = textDecorationValue(style?.underline, style?.strikethrough, style?.overline);
551
558
  if (decoration) attrs.push(`text-decoration="${decoration}"`);
552
- if (node.fill) {
559
+ if (node.fill === null) {
560
+ attrs.push('fill="none"');
561
+ } else if (node.fill) {
553
562
  if ("color" in node.fill) {
554
563
  attrs.push(`fill="${node.fill.color}"`);
555
564
  if (node.fill.opacity != null && node.fill.opacity !== 1) {
@@ -631,8 +640,6 @@ function pathBounds(path) {
631
640
  const b = boundsOfPath(path);
632
641
  return { minX: b.x, minY: b.y, maxX: b.x + b.width, maxY: b.y + b.height };
633
642
  }
634
-
635
- // src/annotations/paint.ts
636
643
  var MARK_COLOR = "#e5484d";
637
644
  var MARK_WIDTH = 2;
638
645
  var TEXT_SIZE = 14;
@@ -714,8 +721,8 @@ function seenFrom(config, keys) {
714
721
  const out = {};
715
722
  if (config === null || typeof config !== "object") return out;
716
723
  const src = config;
717
- for (const k2 of keys) {
718
- if (k2 in src) out[k2] = src[k2];
724
+ for (const k of keys) {
725
+ if (k in src) out[k] = src[k];
719
726
  }
720
727
  return out;
721
728
  }
@@ -723,9 +730,9 @@ function isStale(seen, config, keys) {
723
730
  if (!seen || keys.length === 0) return false;
724
731
  if (config === null || typeof config !== "object") return false;
725
732
  const src = config;
726
- for (const k2 of keys) {
727
- if (!(k2 in seen)) continue;
728
- if (!Object.is(seen[k2], src[k2])) return true;
733
+ for (const k of keys) {
734
+ if (!(k in seen)) continue;
735
+ if (!Object.is(seen[k], src[k])) return true;
729
736
  }
730
737
  return false;
731
738
  }
@@ -759,7 +766,7 @@ function toSvgPaint(paint) {
759
766
  function toSvgStroke(stroke) {
760
767
  return {
761
768
  paint: toSvgPaint(stroke.paint),
762
- width: resolveStrokeWidth(stroke.width ?? 1, 1),
769
+ width: stroke.width ?? 1,
763
770
  ...stroke.cap ? { cap: stroke.cap } : {},
764
771
  ...stroke.join ? { join: stroke.join } : {},
765
772
  ...stroke.dash ? { dash: [...stroke.dash] } : {},
@@ -799,7 +806,7 @@ function capturePlan(base, format) {
799
806
  if (format === "svg") return "svg-document";
800
807
  return base?.kind === "svg" ? "svg-document" : "raster-stack";
801
808
  }
802
- function nestBaseSvg(markup, w3, h2) {
809
+ function nestBaseSvg(markup, w3, h) {
803
810
  const doc = new DOMParser().parseFromString(markup, "image/svg+xml");
804
811
  const root = doc.documentElement;
805
812
  if (doc.getElementsByTagName("parsererror").length > 0 || root.localName !== "svg") {
@@ -808,16 +815,16 @@ function nestBaseSvg(markup, w3, h2) {
808
815
  root.setAttribute("x", "0");
809
816
  root.setAttribute("y", "0");
810
817
  root.setAttribute("width", String(w3));
811
- root.setAttribute("height", String(h2));
818
+ root.setAttribute("height", String(h));
812
819
  return new XMLSerializer().serializeToString(root);
813
820
  }
814
- function embedRasterBase(base, w3, h2, onWarn) {
821
+ function embedRasterBase(base, w3, h, onWarn) {
815
822
  const href = base.kind === "canvas" ? base.canvas.toDataURL() : base.kind === "image" ? base.src : "";
816
823
  if (base.kind === "image" && !href.startsWith("data:")) {
817
824
  onWarn?.(`base image '${href}' is a reference, so this SVG is not self-contained`);
818
825
  }
819
- return serializeSvg([{ kind: "image", href, x: 0, y: 0, width: w3, height: h2 }], {
820
- viewBox: { x: 0, y: 0, width: w3, height: h2 }
826
+ return serializeSvg([{ kind: "image", href, x: 0, y: 0, width: w3, height: h }], {
827
+ viewBox: { x: 0, y: 0, width: w3, height: h }
821
828
  });
822
829
  }
823
830
  function markNodesOf(scene, draw) {
@@ -833,15 +840,15 @@ function markNodesOf(scene, draw) {
833
840
  }
834
841
  function composeCaptureSvg(args) {
835
842
  const { base, scene, draw, scale, onWarn } = args;
836
- const { w: w3, h: h2 } = draw.content;
837
- const baseXml = base === void 0 ? "" : base.kind === "svg" ? nestBaseSvg(base.markup, w3, h2) : embedRasterBase(base, w3, h2, onWarn);
843
+ const { w: w3, h } = draw.content;
844
+ const baseXml = base === void 0 ? "" : base.kind === "svg" ? nestBaseSvg(base.markup, w3, h) : embedRasterBase(base, w3, h, onWarn);
838
845
  const marksXml = serializeSvg(markNodesOf(scene, draw), {
839
- viewBox: { x: 0, y: 0, width: w3, height: h2 },
846
+ viewBox: { x: 0, y: 0, width: w3, height: h },
840
847
  onWarn
841
848
  });
842
849
  const outW = Math.max(1, Math.round(w3 * scale));
843
- const outH = Math.max(1, Math.round(h2 * scale));
844
- return `<svg xmlns="${SVG_NS}" width="${outW}" height="${outH}" viewBox="0 0 ${w3} ${h2}">${baseXml}${marksXml}</svg>`;
850
+ const outH = Math.max(1, Math.round(h * scale));
851
+ return `<svg xmlns="${SVG_NS}" width="${outW}" height="${outH}" viewBox="0 0 ${w3} ${h}">${baseXml}${marksXml}</svg>`;
845
852
  }
846
853
  function context2d(width, height) {
847
854
  const canvas = document.createElement("canvas");
@@ -960,7 +967,7 @@ function roundFrac(f2) {
960
967
  function fracContains(box, pt, tol = 0) {
961
968
  return pt.x >= box.x - tol && pt.x <= box.x + box.w + tol && pt.y >= box.y - tol && pt.y <= box.y + box.h + tol;
962
969
  }
963
- function fracIntersects(outer, inner) {
970
+ function fracEncloses(outer, inner) {
964
971
  return inner.x >= outer.x && inner.y >= outer.y && inner.x + inner.w <= outer.x + outer.w && inner.y + inner.h <= outer.y + outer.h;
965
972
  }
966
973
 
@@ -1041,8 +1048,6 @@ function drop(stack, value) {
1041
1048
  }
1042
1049
  }
1043
1050
  }
1044
-
1045
- // src/annotations/store.ts
1046
1051
  function createAnnotationScene() {
1047
1052
  return createScene({ systemLayers: [{ id: "marks" }] });
1048
1053
  }
@@ -1137,7 +1142,7 @@ function createAnnotationStore(opts) {
1137
1142
  return marksOn(target).filter((a) => fracContains(a.frac, pt, tol)).reverse();
1138
1143
  },
1139
1144
  within(target, box) {
1140
- return marksOn(target).filter((a) => fracIntersects(box, a.frac));
1145
+ return marksOn(target).filter((a) => fracEncloses(box, a.frac));
1141
1146
  },
1142
1147
  isStale(a, config) {
1143
1148
  const keys = targetOf(a.target)?.positionDependsOn ?? [];
@@ -1430,6 +1435,9 @@ function AnnotationTargets({
1430
1435
  }
1431
1436
 
1432
1437
  // src/config/builder.ts
1438
+ function isConfigBranch(entry) {
1439
+ return "children" in entry;
1440
+ }
1433
1441
  var BaseNode = class {
1434
1442
  constructor(defaultValue, annotations = {}, options = {}) {
1435
1443
  this.annotations = annotations;
@@ -1469,9 +1477,10 @@ var BaseNode = class {
1469
1477
  pair(pair) {
1470
1478
  return this.ann({ pair });
1471
1479
  }
1472
- /** Render under a named section heading. */
1473
- section(section) {
1474
- return this.opt({ section });
1480
+ /** Render under a named section heading. `collapsed` opens the section
1481
+ * folded — say it on any one of the section's leaves. */
1482
+ section(label, opts = {}) {
1483
+ return this.opt({ section: { label, ...opts } });
1475
1484
  }
1476
1485
  /** Show this row only while the predicate holds. Presentational — the value
1477
1486
  * stays in config and the instrument still reads it. */
@@ -1555,6 +1564,45 @@ var CustomNode = class _CustomNode extends BaseNode {
1555
1564
  return new _CustomNode(this.kind, this.default, annotations, options);
1556
1565
  }
1557
1566
  };
1567
+ var GroupNode = class _GroupNode {
1568
+ constructor(children, annotations = {}, options = {}) {
1569
+ this.children = children;
1570
+ this.annotations = annotations;
1571
+ this.options = options;
1572
+ }
1573
+ children;
1574
+ annotations;
1575
+ options;
1576
+ with(annotations, options) {
1577
+ return new _GroupNode(this.children, annotations, options);
1578
+ }
1579
+ /** Heading for the group's rows. Defaults to the key, title-cased. */
1580
+ label(name) {
1581
+ return this.with({ ...this.annotations, name }, this.options);
1582
+ }
1583
+ /** Longer help text. Carried on the resolved `PrefGroup`, where weasel-ui's
1584
+ * `PrefsForm` draws it under the heading; `ControlPanel` has no place for
1585
+ * it yet and shows the heading alone. */
1586
+ describe(description) {
1587
+ return this.with({ ...this.annotations, description }, this.options);
1588
+ }
1589
+ /** Render this whole group under a named section heading. */
1590
+ section(label, opts = {}) {
1591
+ return this.with(this.annotations, { ...this.options, section: { label, ...opts } });
1592
+ }
1593
+ /** Show this group only while the predicate holds. Presentational — every
1594
+ * value beneath it stays in config and the instrument still reads it. */
1595
+ showIf(predicate) {
1596
+ return this.with(this.annotations, { ...this.options, showIf: predicate });
1597
+ }
1598
+ };
1599
+ function shapeDefaults(shape) {
1600
+ const out = {};
1601
+ for (const [key, entry] of Object.entries(shape)) {
1602
+ out[key] = isConfigBranch(entry) ? shapeDefaults(entry.children) : entry.default;
1603
+ }
1604
+ return out;
1605
+ }
1558
1606
  var expand = (options) => options.map((o) => typeof o === "string" ? { value: o, label: o } : o);
1559
1607
  var f = {
1560
1608
  number: (def) => new NumberNode(def),
@@ -1568,18 +1616,15 @@ var f = {
1568
1616
  /** A leaf with no declared kind: the rule chain decides, which is how a lab
1569
1617
  * states a convention like "every `*Color` key is a color picker". */
1570
1618
  value: (def) => new ValueNode(def),
1619
+ /** A branch: its children nest under this key, so `grid: f.group({ size })`
1620
+ * puts the value at `config.grid.size` and addresses it as `'grid.size'`.
1621
+ * Groups nest as deeply as the schema wants. */
1622
+ group: (children) => new GroupNode(children),
1571
1623
  /** A leaf of a kind a lab supplies the control for, through `controls`. */
1572
1624
  custom: (kind, def, validate) => new CustomNode(kind, def, {}, validate ? { validate } : {}),
1573
- /** Collect leaves into an instrument's config. */
1625
+ /** Collect leaves and groups into an instrument's config. */
1574
1626
  schema(nodes) {
1575
- return {
1576
- nodes,
1577
- defaults: () => {
1578
- const out = {};
1579
- for (const [key, node] of Object.entries(nodes)) out[key] = node.default;
1580
- return out;
1581
- }
1582
- };
1627
+ return { nodes, defaults: () => shapeDefaults(nodes) };
1583
1628
  }
1584
1629
  };
1585
1630
 
@@ -1611,9 +1656,9 @@ function applyRules(seed, ctx, rules) {
1611
1656
  for (const rule of rules) {
1612
1657
  const patch = rule({ ...ctx, leaf: acc });
1613
1658
  if (patch === null) continue;
1614
- for (const [k2, v] of Object.entries(patch)) {
1615
- if (v !== void 0 && acc[k2] === void 0) {
1616
- acc[k2] = v;
1659
+ for (const [k, v] of Object.entries(patch)) {
1660
+ if (v !== void 0 && acc[k] === void 0) {
1661
+ acc[k] = v;
1617
1662
  }
1618
1663
  }
1619
1664
  }
@@ -1623,40 +1668,59 @@ function applyRules(seed, ctx, rules) {
1623
1668
  // src/config/resolve.ts
1624
1669
  function defined(annotations) {
1625
1670
  const out = {};
1626
- for (const [k2, v] of Object.entries(annotations)) if (v !== void 0) out[k2] = v;
1671
+ for (const [k, v] of Object.entries(annotations)) if (v !== void 0) out[k] = v;
1627
1672
  return out;
1628
1673
  }
1629
1674
  function resolveConfigSchema(schema, rules = []) {
1675
+ const sink = {
1676
+ sections: [],
1677
+ showIf: /* @__PURE__ */ new Map(),
1678
+ renderers: {},
1679
+ chain: [...rules, ...builtinRules]
1680
+ };
1681
+ const group = resolveShape(schema.nodes, "", "", sink);
1682
+ return { group, sections: sink.sections, showIf: sink.showIf, renderers: sink.renderers };
1683
+ }
1684
+ function resolveShape(shape, at, name, sink) {
1630
1685
  const children = {};
1631
- const sectionOrder = [];
1632
- const sectionPaths = /* @__PURE__ */ new Map();
1633
- const showIf = /* @__PURE__ */ new Map();
1634
- const renderers = {};
1635
- const chain2 = [...rules, ...builtinRules];
1636
- for (const [key, node] of Object.entries(schema.nodes)) {
1637
- const seed = defined({
1638
- ...node.annotations,
1639
- ...node.kind === null ? {} : { kind: node.kind }
1640
- });
1641
- const patch = applyRules(seed, { key, path: key, default: node.default }, chain2);
1642
- children[key] = { ...patch, default: node.default };
1643
- const { section, showIf: predicate, render } = node.options;
1686
+ const sections = /* @__PURE__ */ new Map();
1687
+ for (const [key, entry] of Object.entries(shape)) {
1688
+ const path = at === "" ? key : `${at}.${key}`;
1689
+ children[key] = resolveEntry(entry, key, path, sink);
1690
+ const { section, showIf: predicate } = entry.options;
1644
1691
  if (section !== void 0) {
1645
- if (!sectionPaths.has(section)) {
1646
- sectionOrder.push(section);
1647
- sectionPaths.set(section, []);
1692
+ let spec = sections.get(section.label);
1693
+ if (!spec) {
1694
+ spec = { at, label: section.label, paths: [] };
1695
+ sections.set(section.label, spec);
1696
+ sink.sections.push(spec);
1697
+ }
1698
+ spec.paths.push(path);
1699
+ if (section.collapsed !== void 0) {
1700
+ spec.collapsed = (spec.collapsed ?? false) || section.collapsed;
1648
1701
  }
1649
- sectionPaths.get(section)?.push(key);
1650
1702
  }
1651
- if (predicate) showIf.set(key, predicate);
1652
- if (render) renderers[key] = render;
1703
+ if (predicate) sink.showIf.set(path, predicate);
1704
+ if (!isConfigBranch(entry) && entry.options.render) sink.renderers[path] = entry.options.render;
1705
+ }
1706
+ return { name, children };
1707
+ }
1708
+ function resolveEntry(entry, key, path, sink) {
1709
+ if (isConfigBranch(entry)) {
1710
+ const group = resolveShape(
1711
+ entry.children,
1712
+ path,
1713
+ entry.annotations.name ?? titleCase(key),
1714
+ sink
1715
+ );
1716
+ return entry.annotations.description === void 0 ? group : { ...group, description: entry.annotations.description };
1653
1717
  }
1654
- const group = { name: "", children };
1655
- const sections = sectionOrder.map((label) => ({
1656
- label,
1657
- paths: sectionPaths.get(label) ?? []
1658
- }));
1659
- return { group, sections, showIf, renderers };
1718
+ const seed = defined({
1719
+ ...entry.annotations,
1720
+ ...entry.kind === null ? {} : { kind: entry.kind }
1721
+ });
1722
+ const patch = applyRules(seed, { key, path, default: entry.default }, sink.chain);
1723
+ return { ...patch, default: entry.default };
1660
1724
  }
1661
1725
  var LabContext = createContext(null);
1662
1726
  function useLabContext() {
@@ -1736,7 +1800,7 @@ function validateConfigSchema(fields) {
1736
1800
  return { valid: errors.length === 0, errors };
1737
1801
  }
1738
1802
 
1739
- // ../theme/dist/chunk-CXVFAOTM.js
1803
+ // ../theme/dist/chunk-D6AFL2JW.js
1740
1804
  var THEME_SOURCES = {
1741
1805
  "weasel": {
1742
1806
  "primitives": {
@@ -1965,15 +2029,15 @@ var THEME_SOURCES = {
1965
2029
  "value": "8px",
1966
2030
  "description": "Diameter of a linear range thumb."
1967
2031
  },
1968
- "slider-track-tint": {
2032
+ "slider-track-mix": {
1969
2033
  "type": "dimension",
1970
2034
  "value": "18%",
1971
- "description": "Accent proportion in a range track. Mixed into a real property, never into another custom property."
2035
+ "description": "Accent proportion in a range track, as a color-mix percentage \u2014 not a color. Mixed into a real property, never into another custom property."
1972
2036
  },
1973
- "slider-thumb-tint": {
2037
+ "slider-thumb-mix": {
1974
2038
  "type": "dimension",
1975
2039
  "value": "70%",
1976
- "description": "Accent proportion in a range thumb."
2040
+ "description": "Accent proportion in a range thumb, as a color-mix percentage \u2014 not a color."
1977
2041
  },
1978
2042
  "field-pad-x": {
1979
2043
  "type": "dimension",
@@ -2304,9 +2368,9 @@ function hexToRgba(hex, alpha) {
2304
2368
  const body = m[1];
2305
2369
  const full = body.length === 3 ? body.replace(/./g, (c2) => c2 + c2) : body;
2306
2370
  const r2 = Number.parseInt(full.slice(0, 2), 16);
2307
- const g = Number.parseInt(full.slice(2, 4), 16);
2371
+ const g2 = Number.parseInt(full.slice(2, 4), 16);
2308
2372
  const b = Number.parseInt(full.slice(4, 6), 16);
2309
- return `rgba(${r2}, ${g}, ${b}, ${alpha})`;
2373
+ return `rgba(${r2}, ${g2}, ${b}, ${alpha})`;
2310
2374
  }
2311
2375
  var REF = /^\{([^}]+)\}$/;
2312
2376
  function refTarget(value) {
@@ -2444,6 +2508,25 @@ function useThemeOptional() {
2444
2508
  return useContext(ThemeContext);
2445
2509
  }
2446
2510
 
2511
+ // src/instrument/serializers.ts
2512
+ function configDefaultsOf(instruments) {
2513
+ const out = {};
2514
+ for (const instrument of instruments) out[instrument.name] = () => instrument.defaultConfig();
2515
+ return out;
2516
+ }
2517
+ function serializersOf(instruments) {
2518
+ const out = {};
2519
+ for (const instrument of instruments) {
2520
+ const { name, serialize: serialize2, deserialize } = instrument;
2521
+ if (!serialize2 && !deserialize) continue;
2522
+ out[name] = {
2523
+ ...serialize2 ? { serialize: (state) => serialize2(state) } : {},
2524
+ ...deserialize ? { deserialize: (data, config) => deserialize(data, config) } : {}
2525
+ };
2526
+ }
2527
+ return out;
2528
+ }
2529
+
2447
2530
  // ../theme/dist/index.js
2448
2531
  var ALPHA_EXT = "com.weasel.alpha";
2449
2532
  var isToken = (v) => typeof v === "object" && v !== null && "$value" in v;
@@ -2452,9 +2535,9 @@ function flattenTokens(doc) {
2452
2535
  for (const [groupName, group] of Object.entries(doc)) {
2453
2536
  if (groupName.startsWith("$")) continue;
2454
2537
  if (typeof group !== "object" || group === null) continue;
2455
- const g = group;
2456
- const groupType = g.$type;
2457
- for (const [leaf, node] of Object.entries(g)) {
2538
+ const g2 = group;
2539
+ const groupType = g2.$type;
2540
+ for (const [leaf, node] of Object.entries(g2)) {
2458
2541
  if (leaf.startsWith("$")) continue;
2459
2542
  if (!isToken(node)) continue;
2460
2543
  const type = node.$type ?? groupType;
@@ -2792,9 +2875,9 @@ function TrialChrome({
2792
2875
  const title = record.title ?? record.instrumentName;
2793
2876
  const collapsedSections = record.collapsedSections ?? NO_SECTIONS;
2794
2877
  const ctx = useMemo(() => {
2795
- const setZoom = (z) => {
2878
+ const setZoom = (z2) => {
2796
2879
  if (!view2d) return;
2797
- updateTrialView(trialId, { ...view2d, zoom: z });
2880
+ updateTrialView(trialId, { ...view2d, zoom: z2 });
2798
2881
  };
2799
2882
  return {
2800
2883
  trialId,
@@ -2813,16 +2896,16 @@ function TrialChrome({
2813
2896
  configSchema,
2814
2897
  configFields: instrument.config ? [] : instrument.configSchema?.() ?? [],
2815
2898
  config: record.config,
2816
- setConfig: (key, value) => {
2899
+ setConfig: (path, value) => {
2817
2900
  const prevConfig = record.config;
2818
- if (process.env.NODE_ENV !== "production" && !(key in prevConfig)) {
2901
+ if (process.env.NODE_ENV !== "production" && !hasConfigPath(prevConfig, path) && schemaNodeAtPath(configSchema.group, path) === void 0) {
2819
2902
  console.warn(
2820
- `[labkit] setConfig: unknown key "${key}" for instrument "${record.instrumentName}"`
2903
+ `[labkit] setConfig: unknown path "${path}" for instrument "${record.instrumentName}"`
2821
2904
  );
2822
2905
  }
2823
- updateTrialConfig(trialId, key, value);
2906
+ updateTrialConfig(trialId, path, value);
2824
2907
  if (instrument.onConfigChange) {
2825
- const nextConfig = { ...prevConfig, [key]: value };
2908
+ const nextConfig = withValueAtPath(prevConfig, path, value);
2826
2909
  const nextState = instrument.onConfigChange(nextConfig, prevConfig, record.state);
2827
2910
  updateTrialState(trialId, nextState);
2828
2911
  }
@@ -3086,11 +3169,11 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
3086
3169
  updateTrialState(record.id, next);
3087
3170
  bus.emit("state.change");
3088
3171
  },
3089
- setConfig: (key, value) => {
3090
- const evt = `config.change:${String(key)}`;
3172
+ setConfig: (path, value) => {
3173
+ const evt = `config.change:${path}`;
3091
3174
  snapshotIfNeeded("config.change");
3092
3175
  snapshotIfNeeded(evt);
3093
- updateTrialConfig(record.id, key, value);
3176
+ updateTrialConfig(record.id, path, value);
3094
3177
  bus.emit("config.change");
3095
3178
  bus.emit(evt);
3096
3179
  },
@@ -3099,9 +3182,9 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
3099
3182
  view: record.view,
3100
3183
  setView,
3101
3184
  zoom: view2d?.zoom ?? 1,
3102
- setZoom: (z) => {
3185
+ setZoom: (z2) => {
3103
3186
  if (!view2d) return;
3104
- updateTrialView(record.id, { ...view2d, zoom: z });
3187
+ updateTrialView(record.id, { ...view2d, zoom: z2 });
3105
3188
  },
3106
3189
  activeToolId: resolvedToolId,
3107
3190
  visibleLayers
@@ -3335,19 +3418,14 @@ function initialView(instrument) {
3335
3418
  return declared ? structuredClone(declared) : { ...DEFAULT_VIEW2, pan: { ...DEFAULT_VIEW2.pan } };
3336
3419
  }
3337
3420
  function seedConfig(defaults, seed) {
3338
- if (!seed) return defaults;
3339
- const out = { ...defaults };
3340
- for (const [key, value] of Object.entries(seed)) {
3341
- if (value !== void 0) out[key] = value;
3342
- }
3343
- return out;
3421
+ return seed ? fillConfigDefaults(seed, defaults) : defaults;
3344
3422
  }
3345
3423
  function addTrial(trials, instruments, instrumentName, options = {}) {
3346
3424
  const instrument = findInstrument(instruments, instrumentName);
3347
3425
  const config = seedConfig(instrument.defaultConfig(), options.config);
3348
3426
  const state = instrument.initialState(config);
3349
3427
  const record = {
3350
- id: crypto.randomUUID(),
3428
+ id: newId(),
3351
3429
  instrumentName,
3352
3430
  config,
3353
3431
  state,
@@ -3363,7 +3441,7 @@ function cloneTrial(trials, id) {
3363
3441
  if (!source) return trials;
3364
3442
  const clone = {
3365
3443
  ...source,
3366
- id: crypto.randomUUID(),
3444
+ id: newId(),
3367
3445
  config: structuredClone(source.config),
3368
3446
  state: structuredClone(source.state),
3369
3447
  view: structuredClone(source.view),
@@ -3427,7 +3505,7 @@ function LabHeader() {
3427
3505
  // Held at null so the control stays an "add one" action rather than
3428
3506
  // reading as the current instrument.
3429
3507
  /* @__PURE__ */ jsx(
3430
- h,
3508
+ g,
3431
3509
  {
3432
3510
  className: "lk-lab-header__add-select",
3433
3511
  "aria-label": "Add trial",
@@ -3441,7 +3519,7 @@ function LabHeader() {
3441
3519
  )
3442
3520
  ),
3443
3521
  /* @__PURE__ */ jsx(
3444
- r4,
3522
+ r6,
3445
3523
  {
3446
3524
  className: "lk-lab-header__mode",
3447
3525
  ariaLabel: "Color mode",
@@ -3473,6 +3551,67 @@ function LabPalette({ tools }) {
3473
3551
  };
3474
3552
  return /* @__PURE__ */ jsx(PaletteRegion, { contributions, ctx });
3475
3553
  }
3554
+ function currentPage(path, pages) {
3555
+ const here = path.split("?")[0].split("#")[0].replace(/\/$/, "").replace(/\.html$/, "");
3556
+ return pages.findIndex((p) => here.endsWith(p.href.replace(/\.html$/, "")));
3557
+ }
3558
+ function LabSwitcher({ title, pages, path, className }) {
3559
+ const [open, setOpen] = useState(false);
3560
+ const root = useRef(null);
3561
+ const menuId = useId();
3562
+ const here = currentPage(path ?? (typeof window === "undefined" ? "" : window.location.pathname), pages);
3563
+ const close = useCallback(() => setOpen(false), []);
3564
+ useEffect(() => {
3565
+ if (!open) return;
3566
+ const onKey = (e) => {
3567
+ if (e.key === "Escape") {
3568
+ close();
3569
+ root.current?.querySelector("button")?.focus();
3570
+ }
3571
+ };
3572
+ const onDown = (e) => {
3573
+ if (!root.current?.contains(e.target)) close();
3574
+ };
3575
+ document.addEventListener("keydown", onKey);
3576
+ document.addEventListener("pointerdown", onDown);
3577
+ return () => {
3578
+ document.removeEventListener("keydown", onKey);
3579
+ document.removeEventListener("pointerdown", onDown);
3580
+ };
3581
+ }, [open, close]);
3582
+ if (pages.length < 2) {
3583
+ return /* @__PURE__ */ jsx("h1", { className: `lk-switcher-title${className ? ` ${className}` : ""}`, children: title });
3584
+ }
3585
+ return /* @__PURE__ */ jsxs("div", { className: `lk-switcher${className ? ` ${className}` : ""}`, ref: root, children: [
3586
+ /* @__PURE__ */ jsx("h1", { className: "lk-switcher-title", children: /* @__PURE__ */ jsxs(
3587
+ "button",
3588
+ {
3589
+ type: "button",
3590
+ className: "lk-switcher-trigger",
3591
+ "aria-expanded": open,
3592
+ "aria-haspopup": "menu",
3593
+ "aria-controls": open ? menuId : void 0,
3594
+ onClick: () => setOpen((was) => !was),
3595
+ children: [
3596
+ title,
3597
+ /* @__PURE__ */ jsx("span", { className: "lk-switcher-caret", "aria-hidden": "true" })
3598
+ ]
3599
+ }
3600
+ ) }),
3601
+ open && /* @__PURE__ */ jsx("div", { className: "lk-switcher-menu", id: menuId, role: "menu", children: pages.map((page, i) => /* @__PURE__ */ jsx(
3602
+ "a",
3603
+ {
3604
+ href: page.href,
3605
+ role: "menuitem",
3606
+ className: "lk-switcher-item",
3607
+ "aria-current": i === here ? "page" : void 0,
3608
+ onClick: close,
3609
+ children: page.label
3610
+ },
3611
+ page.href
3612
+ )) })
3613
+ ] });
3614
+ }
3476
3615
  function query() {
3477
3616
  if (typeof window === "undefined" || !window.matchMedia) return null;
3478
3617
  return window.matchMedia("(prefers-color-scheme: light)");
@@ -3491,17 +3630,37 @@ function useResolvedMode(mode) {
3491
3630
  }, [mode]);
3492
3631
  return mode === "auto" ? system : mode;
3493
3632
  }
3494
- function LabShell({ title, children, header, footer, mode = "auto" }) {
3633
+ function LabShell({
3634
+ title,
3635
+ children,
3636
+ header,
3637
+ footer,
3638
+ mode = "auto",
3639
+ pages,
3640
+ path
3641
+ }) {
3495
3642
  const resolved = useResolvedMode(mode);
3496
3643
  const outer = useThemeOptional();
3497
- const shell = /* @__PURE__ */ jsxs("div", { className: "lk-root lk-shell", children: [
3498
- /* @__PURE__ */ jsxs("header", { className: "lk-shell-header", children: [
3499
- /* @__PURE__ */ jsx("h1", { className: "lk-shell-title", children: title }),
3500
- header && /* @__PURE__ */ jsx("div", { className: "lk-shell-header-actions", children: header })
3501
- ] }),
3502
- /* @__PURE__ */ jsx("main", { className: "lk-shell-body", children }),
3503
- footer && /* @__PURE__ */ jsx("footer", { className: "lk-shell-footer", children: footer })
3504
- ] });
3644
+ const shell = (
3645
+ // Overlays portal here rather than to the themed wrapper above: `.lk-root`
3646
+ // carries the element defaults a lab's bare markup is styled by.
3647
+ /* @__PURE__ */ jsxs("div", { className: "lk-root lk-shell", "data-wzl-portal-host": "", children: [
3648
+ /* @__PURE__ */ jsxs("header", { className: "lk-shell-header", children: [
3649
+ pages && pages.length > 0 ? /* @__PURE__ */ jsx(
3650
+ LabSwitcher,
3651
+ {
3652
+ title,
3653
+ pages,
3654
+ path,
3655
+ className: "lk-shell-title"
3656
+ }
3657
+ ) : /* @__PURE__ */ jsx("h1", { className: "lk-shell-title", children: title }),
3658
+ header && /* @__PURE__ */ jsx("div", { className: "lk-shell-header-actions", children: header })
3659
+ ] }),
3660
+ /* @__PURE__ */ jsx("main", { className: "lk-shell-body", children }),
3661
+ footer && /* @__PURE__ */ jsx("footer", { className: "lk-shell-footer", children: footer })
3662
+ ] })
3663
+ );
3505
3664
  return outer ? shell : /* @__PURE__ */ jsx(ThemeProvider, { theme: interstellarTheme, mode: resolved, children: shell });
3506
3665
  }
3507
3666
  var ZONE_ID2 = asNodeId("lk-workspace");
@@ -3657,7 +3816,13 @@ function Workspace({
3657
3816
  ) : reorderable ? /* @__PURE__ */ jsx(DragProvider, { children: grid }) : grid }) });
3658
3817
  }
3659
3818
  function buildStore(instruments, defaultInstrument, storage, storageKey, initialMode) {
3660
- const store = createLabStore({ storageKey, storage, initialMode });
3819
+ const store = createLabStore({
3820
+ storageKey,
3821
+ storage,
3822
+ initialMode,
3823
+ serializers: serializersOf(instruments),
3824
+ configDefaults: configDefaultsOf(instruments)
3825
+ });
3661
3826
  if (store.getState().trials.length === 0) {
3662
3827
  const seeded = addTrial([], instruments, defaultInstrument);
3663
3828
  const record = seeded[0];
@@ -3730,11 +3895,11 @@ function Lab({
3730
3895
  const c2 = ownCanvasRef.current;
3731
3896
  if (!c2) return;
3732
3897
  const w3 = Math.round(frame.size.width * frame.dpr);
3733
- const h2 = Math.round(frame.size.height * frame.dpr);
3734
- if (bufferRef.current.w === w3 && bufferRef.current.h === h2) return;
3735
- bufferRef.current = { w: w3, h: h2 };
3898
+ const h = Math.round(frame.size.height * frame.dpr);
3899
+ if (bufferRef.current.w === w3 && bufferRef.current.h === h) return;
3900
+ bufferRef.current = { w: w3, h };
3736
3901
  c2.width = w3;
3737
- c2.height = h2;
3902
+ c2.height = h;
3738
3903
  c2.style.width = `${frame.size.width}px`;
3739
3904
  c2.style.height = `${frame.size.height}px`;
3740
3905
  surfaceRef.current?.invalidateAll();
@@ -3874,6 +4039,6 @@ function Lab({
3874
4039
  ) }) });
3875
4040
  }
3876
4041
 
3877
- export { AnnotationOverlay, AnnotationTargets, DEFAULT_VIEW, Lab, LabContext, LabPalette, LabShell, Trial, TrialBody, TrialChrome, Workspace, addTrial, annotationsFromJSON, applyRules, as2DView, builtinRules, capturePlan, captureTarget, cloneTrial, closeTrial, composeCaptureSvg, createAnnotationScene, createAnnotationStore, createMarkDrawOne, defineInstrument, f, fitView, fracContains, fracIntersects, fracToWorld, fromWeaselView, interstellarTheme, isStale, markCommands, markSvgNodes, reorderTrials, resetTrial, resolveConfigSchema, resolveMarkStyle, roundFrac, seenFrom, titleCase, toWeaselView, useConfigSchema, useLabContext, validateConfigSchema, worldToFrac };
4042
+ export { AnnotationOverlay, AnnotationTargets, DEFAULT_VIEW, Lab, LabContext, LabPalette, LabShell, LabSwitcher, Trial, TrialBody, TrialChrome, Workspace, addTrial, annotationsFromJSON, applyRules, as2DView, builtinRules, capturePlan, captureTarget, cloneTrial, closeTrial, composeCaptureSvg, createAnnotationScene, createAnnotationStore, createMarkDrawOne, currentPage, defineInstrument, f, fitView, fracContains, fracEncloses, fracToWorld, fromWeaselView, interstellarTheme, isConfigBranch, isStale, markCommands, markSvgNodes, reorderTrials, resetTrial, resolveConfigSchema, resolveMarkStyle, roundFrac, seenFrom, titleCase, toWeaselView, useConfigSchema, useLabContext, validateConfigSchema, worldToFrac };
3878
4043
  //# sourceMappingURL=index.js.map
3879
4044
  //# sourceMappingURL=index.js.map