@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/README.md CHANGED
@@ -13,10 +13,16 @@ algorithm and would rather not build the page around it.
13
13
  ## Install
14
14
 
15
15
  ```bash
16
- npm i @weasel-js/labkit
16
+ npm i @weasel-js/labkit @weasel-js/core
17
17
  ```
18
18
 
19
- React 19 is a peer dependency (`react` and `react-dom`, `^19.0.0`). One
19
+ `@weasel-js/core` is a peer dependency, pinned to the matching version. labkit
20
+ does not ship its own copy: core keeps its content handlers, paint kinds, shape
21
+ painters and markers in module-global registries, and two copies means anything
22
+ registered through one is invisible to the other — a blank canvas with no error.
23
+ Installing it alongside is what guarantees there is exactly one.
24
+
25
+ React 19 is a peer dependency too (`react` and `react-dom`, `^19.0.0`). One
20
26
  stylesheet import covers everything labkit draws, including the theme tokens and
21
27
  the `@weasel-js/ui` components it passes through:
22
28
 
@@ -70,6 +76,18 @@ renders the settings panel from it. `render` returns the instrument's own DOM;
70
76
  returning `null` alongside `canvas` means the canvas layers are the whole
71
77
  picture. `storage` keeps open trials and their state across reloads.
72
78
 
79
+ A value is addressed by its path: `bins` above sits at `config.bins` and is
80
+ written as `setConfig('bins', 40)`. `f.group` nests one —
81
+ `paper: f.group({ width: f.number(210) })` puts the value at
82
+ `config.paper.width` and addresses it as `'paper.width'`, in `setConfig`, in a
83
+ control override, and anywhere else a path is taken. `.section('Advanced')` is
84
+ the other thing and only looks like it: it puts a heading over sibling rows and
85
+ leaves their paths alone.
86
+
87
+ A stored config missing something its schema has since gained — a whole nested
88
+ group included — is filled from the instrument's defaults as it loads, so
89
+ growing a schema never strands a saved trial or a snapshot.
90
+
73
91
  ## Capabilities
74
92
 
75
93
  A capability is a field on the instrument. Declaring it is what makes the trial
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { RefObject } from 'react';
3
- import { V as ViewTransform, W as WorldFrame } from './frac-A4R6v4ld.js';
3
+ import { V as ViewTransform, W as WorldFrame } from './frac-z7ker2Vx.js';
4
4
 
5
5
  /** One layer of a canvas stack: its id, whether it is currently shown, and how
6
6
  * it paints itself. */
@@ -1,5 +1,5 @@
1
1
  import { ReactNode } from 'react';
2
- import { T as ToolPrefLeaf } from './weasel-canvas-FJTFi3ZZ.js';
2
+ import { ToolPrefLeaf } from '@weasel-js/core';
3
3
 
4
4
  /** What a {@link PrefRenderer} is given for the leaf it is rendering. */
5
5
  interface PrefRenderContext {
@@ -1,8 +1,9 @@
1
1
  import { ComponentType, ReactNode, RefObject } from 'react';
2
- import { R as ResolvedConfig, C as ConfigField, a as ConfigSchema } from './types-lg4TSCb2.js';
3
- import { c as SavedSnapshot, i as TrialInfo } from './types-BbXpvQa8.js';
2
+ import { R as ResolvedConfig, a as ConfigSchema, C as ConfigPath } from './types-B9_zrHmb.js';
3
+ import { C as ConfigField } from './types-x92Kfeme.js';
4
+ import { c as SavedSnapshot, i as TrialInfo } from './types-BP2OCcpg.js';
4
5
  import { J as JobHandle, a as JobCapability } from './types-DJ79Tg5J.js';
5
- import { S as Scene } from './weasel-canvas-FJTFi3ZZ.js';
6
+ import { Scene } from '@weasel-js/core';
6
7
 
7
8
  /** How an instrument's world sits on the canvas, independent of the camera.
8
9
  * The defaults reproduce the convention labkit shipped before this existed:
@@ -299,7 +300,9 @@ interface RenderContext<TS = unknown, TC = unknown> {
299
300
  state: TS;
300
301
  config: TC;
301
302
  setState: (next: TS | ((prev: TS) => TS)) => void;
302
- setConfig: (key: keyof TC, value: unknown) => void;
303
+ /** Write one config value, by dotted path — `'grid.size'` for a leaf under
304
+ * an `f.group`, `'cellSize'` for one at the root. */
305
+ setConfig: (path: ConfigPath<TC>, value: unknown) => void;
303
306
  /** labkit persists `view` and restores it on Reset without ever reading
304
307
  * into it. */
305
308
  trial: TrialInfo & {
@@ -751,8 +754,9 @@ declare function roundFrac(f: FracRect): FracRect;
751
754
  * mark stays reachable. */
752
755
  declare function fracContains(box: FracRect, pt: FracPoint, tol?: number): boolean;
753
756
  /** Whether `outer` wholly encloses `inner`. A marquee takes what it encloses,
754
- * not what it grazes — brushing selection is a different gesture. */
755
- declare function fracIntersects(outer: FracRect, inner: FracRect): boolean;
757
+ * not what it grazes — brushing selection is a different gesture, and this
758
+ * answers false for two rects that merely overlap. */
759
+ declare function fracEncloses(outer: FracRect, inner: FracRect): boolean;
756
760
 
757
- export { LOUPE_DEFAULTS as L, annotationsFromJSON as a3, applyCamera as a4, createAnnotationScene as a5, createAnnotationStore as a6, fracContains as a7, fracIntersects as a8, fracToWorld as a9, resolveFrame as aa, resolveLoupe as ab, roundFrac as ac, worldToFrac as ad, DEFAULT_FRAME as x };
761
+ export { LOUPE_DEFAULTS as L, annotationsFromJSON as a3, applyCamera as a4, createAnnotationScene as a5, createAnnotationStore as a6, fracContains as a7, fracEncloses as a8, fracToWorld as a9, resolveFrame as aa, resolveLoupe as ab, roundFrac as ac, worldToFrac as ad, DEFAULT_FRAME as x };
758
762
  export type { TrialRegion as $, AnnotationData as A, LayerCapability as B, CaptureSource as C, DragFeedback as D, LayerDescriptor as E, FracPoint as F, LoupeCapability as G, HitResult as H, Instrument as I, LoupeDeclaration as J, LoupeRenderArgs as K, MarkScene as M, ResolvedLoupe as N, SidebarSection as O, PaletteItem as P, StatusReadout as Q, RenderContext as R, SerializedAnnotations as S, TrialTool as T, SystemEvent as U, ViewTransform as V, WorldFrame as W, ToolCapability as X, ToolItem as Y, ToolbarItem as Z, TrialChromeContext as _, Point as a, UndoCapability as a0, ViewportControl as a1, ViewportSize as a2, LoupePoint as ae, LoupeMode as af, DragDropCapability as b, WorldSpec as c, WorldRect as d, AnnotationMeaning as e, CaptureOptions as f, CaptureResult as g, AnnotationTarget as h, AnnotationsApi as i, AnnotationsCapability as j, AnnotationKind as k, InstrumentList as l, TrialContribution as m, Annotation as n, AnnotationInit as o, AnnotationPatch as p, AnnotationQuery as q, AnnotationStatus as r, AnnotationStoreOptions as s, AnnotationTargetInfo as t, CanvasCapability as u, CanvasLayer as v, CaptureDeps as w, FracRect as y, IconComponent as z };
@@ -1,6 +1,7 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { ReactNode } from 'react';
3
- import { R as ResolvedConfig, C as ConfigField, c as ControlRenderer } from './types-lg4TSCb2.js';
3
+ import { R as ResolvedConfig, c as ControlRenderer } from './types-B9_zrHmb.js';
4
+ import { C as ConfigField } from './types-x92Kfeme.js';
4
5
 
5
6
  /**
6
7
  * How much room a container gives its rows — gaps, padding, and field height,
@@ -143,10 +144,24 @@ declare function SliderRow({ label, value, min, max, step, onChange, onInput, fo
143
144
  interface ColorRowProps extends PropertyMetricProps {
144
145
  label: ReactNode;
145
146
  value: string;
147
+ /**
148
+ * The committed color. Given `onInput` as well, it fires once the picker
149
+ * closes; on its own it fires on every move inside it, which is what a row
150
+ * with one callback has always done.
151
+ */
146
152
  onChange: (next: string) => void;
153
+ /**
154
+ * The live color, fired continuously while the picker is open. Pass it
155
+ * alongside `onChange` when the write is expensive — cheap state here, the
156
+ * costly work there. Mirrors `<Slider>`'s `onInput` / `onChange` pair.
157
+ */
158
+ onInput?: (next: string) => void;
147
159
  /** 0..1 alpha. When provided, a translucent slider renders beneath the swatch. */
148
160
  alpha?: number;
161
+ /** The committed alpha, on the same terms as `onChange`. */
149
162
  onAlphaChange?: (next: number) => void;
163
+ /** The live alpha, on the same terms as `onInput`. */
164
+ onAlphaInput?: (next: number) => void;
150
165
  /**
151
166
  * Render the alpha track as inert (dimmed, no thumb, not-allowed cursor).
152
167
  * Use when the color's consumer drops alpha so the affordance reads dead.
@@ -159,7 +174,7 @@ interface ColorRowProps extends PropertyMetricProps {
159
174
  span?: boolean;
160
175
  }
161
176
  /** A color swatch, optionally with an alpha slider beneath it. */
162
- declare function ColorRow({ label, value, onChange, alpha, onAlphaChange, alphaDisabled, layout, description, span, density, align, }: ColorRowProps): react_jsx_runtime.JSX.Element;
177
+ declare function ColorRow({ label, value, onChange, onInput, alpha, onAlphaChange, onAlphaInput, alphaDisabled, layout, description, span, density, align, }: ColorRowProps): react_jsx_runtime.JSX.Element;
163
178
  /** Props for `<CheckboxRow>`. */
164
179
  interface CheckboxRowProps extends PropertyMetricProps {
165
180
  label: ReactNode;
@@ -191,7 +206,18 @@ declare function TextRow({ label, value, onChange, placeholder, maxLength, layou
191
206
  interface NumberRowProps extends PropertyMetricProps {
192
207
  label: ReactNode;
193
208
  value: number;
209
+ /**
210
+ * The committed value. Given `onInput` as well, it fires on blur or Enter;
211
+ * on its own it fires on every keystroke, which is what a row with one
212
+ * callback has always done.
213
+ */
194
214
  onChange: (next: number) => void;
215
+ /**
216
+ * The live value, fired per keystroke. Pass it alongside `onChange` when the
217
+ * write is expensive — cheap state here, the costly work there. Mirrors
218
+ * `<Slider>`'s `onInput` / `onChange` pair.
219
+ */
220
+ onInput?: (next: number) => void;
195
221
  min?: number;
196
222
  max?: number;
197
223
  step?: number;
@@ -209,7 +235,7 @@ interface NumberRowProps extends PropertyMetricProps {
209
235
  }
210
236
  /** A number typed directly. Reach for `<SliderRow>` when the range matters
211
237
  * more than the exact value. */
212
- declare function NumberRow({ label, value, onChange, min, max, step, placeholder, unit, layout, description, span, density, align, }: NumberRowProps): react_jsx_runtime.JSX.Element;
238
+ declare function NumberRow({ label, value, onChange, onInput, min, max, step, placeholder, unit, layout, description, span, density, align, }: NumberRowProps): react_jsx_runtime.JSX.Element;
213
239
  interface PropertyOption<T extends string> {
214
240
  value: T;
215
241
  label: ReactNode;
@@ -258,7 +284,9 @@ interface ControlPanelProps<TC extends Record<string, unknown>> {
258
284
  /** @deprecated Pass `schema`. A field list is adapted into one internally. */
259
285
  fields?: ConfigField[];
260
286
  config: TC;
261
- setConfig: (key: keyof TC, value: unknown) => void;
287
+ /** Writes one value. The path is dotted for a leaf inside an `f.group`, and
288
+ * the bare key for one at the root. */
289
+ setConfig: (path: string, value: unknown) => void;
262
290
  /**
263
291
  * Control overrides and app-defined kinds, PrefsForm-style. Keys are config
264
292
  * paths (checked first) or leaf kinds. A renderer returning `null` collapses
@@ -279,16 +307,21 @@ interface ControlPanelProps<TC extends Record<string, unknown>> {
279
307
  align?: PropertyAlign;
280
308
  /**
281
309
  * Fold each section away behind a twisty, starting `'open'` or `'closed'`.
282
- * Unset draws the heading alone, as the panel always has.
310
+ * Unset draws the heading alone, as the panel always has — unless a section
311
+ * in the schema declares how it opens, which makes that section foldable on
312
+ * its own and outranks this for that one section.
283
313
  */
284
314
  collapse?: 'open' | 'closed';
285
315
  /**
286
- * Which sections are folded, keyed by section label. Given, the panel keeps
287
- * no state of its own: every toggle arrives at `onCollapse` instead, which
288
- * is where a lab that remembers a trial's sections writes them.
316
+ * Which sections are folded, keyed as `onCollapse` reports them. Given, the
317
+ * panel keeps no state of its own: every toggle arrives at `onCollapse`
318
+ * instead, which is where a lab that remembers a trial's sections writes
319
+ * them.
289
320
  */
290
321
  collapsed?: Readonly<Record<string, boolean>>;
291
- onCollapse?: (label: string, collapsed: boolean) => void;
322
+ /** A fold moved. The key is a section's label — prefixed by its group's
323
+ * dotted path when the section sits inside one — or a group's own path. */
324
+ onCollapse?: (key: string, collapsed: boolean) => void;
292
325
  /** Draw leaves marked `hidden`. */
293
326
  showHidden?: boolean;
294
327
  className?: string;
@@ -0,0 +1,166 @@
1
+ import { ToolPrefLeaf, ToolPrefGroup } from '@weasel-js/core';
2
+ import { P as PrefRenderer } from './PrefsForm.d-DDHFANkc.js';
3
+
4
+ /**
5
+ * Renders the control cell for one config leaf. Identical to weasel-ui's
6
+ * `PrefRenderer` on purpose — aliased rather than redeclared so the two
7
+ * cannot drift.
8
+ */
9
+ type ControlRenderer = PrefRenderer;
10
+ /** One labeled choice in an `enum` leaf. */
11
+ interface ConfigOption {
12
+ value: string;
13
+ label: string;
14
+ }
15
+ /** Everything a leaf can carry beyond its kind and default. A flat union of
16
+ * every `Pref*` leaf's extras, plus labkit's own `debounceMs`. */
17
+ interface Annotations {
18
+ name?: string;
19
+ description?: string;
20
+ hidden?: boolean;
21
+ block?: boolean;
22
+ pair?: string;
23
+ min?: number;
24
+ max?: number;
25
+ step?: number;
26
+ suffix?: string;
27
+ control?: string;
28
+ options?: readonly ConfigOption[];
29
+ placeholder?: string;
30
+ maxLength?: number;
31
+ /** Milliseconds to debounce a string leaf's live writes. Default 150. */
32
+ debounceMs?: number;
33
+ }
34
+ /** What a rule contributes. `kind` is honored only while it is still unset,
35
+ * which is what makes `f.value` claimable and a kinded factory final. */
36
+ type LeafPatch = Annotations & {
37
+ kind?: string;
38
+ };
39
+ /** What a rule is given for the leaf it is deciding about. */
40
+ interface ConfigRuleContext {
41
+ /** The leaf's own key — its last path segment, and what a label is titled
42
+ * from. */
43
+ key: string;
44
+ /** Dotted path within the schema, which is the path the value is written
45
+ * at. Equal to `key` for a leaf sitting at the root. */
46
+ path: string;
47
+ /** The leaf's default value. A rule may read it but never change it. */
48
+ default: unknown;
49
+ /** What earlier rules and the author's own annotations have settled. */
50
+ leaf: Readonly<LeafPatch>;
51
+ }
52
+ /**
53
+ * Decides part of how a leaf is presented. Returns a patch, or null to
54
+ * abstain. Merging is gap-filling: a property already settled is never
55
+ * overwritten, so the author's annotations beat every rule and a consumer's
56
+ * rules beat labkit's built-ins.
57
+ */
58
+ type ConfigRule = (ctx: ConfigRuleContext) => LeafPatch | null;
59
+ /** A presentational bucket of nodes, rendered under one heading. Buckets
60
+ * siblings: a section never nests a value, which is what separates it from
61
+ * `f.group`. */
62
+ interface SectionSpec {
63
+ /** Dotted path of the group whose children this buckets. `''` is the root,
64
+ * which is where every section of a flat schema sits. */
65
+ at: string;
66
+ label: string;
67
+ /** Full dotted paths, so a section under a group names its children the way
68
+ * everything else does. */
69
+ paths: readonly string[];
70
+ /** Whether the section opens folded. Set, the panel folds this section
71
+ * whether or not it was given a panel-wide `collapse`; a fold the reader
72
+ * has since toggled outranks it. */
73
+ collapsed?: boolean;
74
+ }
75
+ /** A schema resolved against a set of rules: the vocabulary weasel-ui renders,
76
+ * plus the three things labkit keeps on the side because `PrefLeaf` has no
77
+ * field for them. */
78
+ interface ResolvedConfig {
79
+ /** The schema as a `PrefGroup` tree: an `f.group` is a nested group, so a
80
+ * leaf's dotted path within it is the path its value is written at. */
81
+ group: ToolPrefGroup;
82
+ sections: readonly SectionSpec[];
83
+ showIf: ReadonlyMap<string, (config: Record<string, unknown>) => boolean>;
84
+ /** Node-level `.render` overrides, keyed by path. */
85
+ renderers: Readonly<Record<string, ControlRenderer>>;
86
+ }
87
+ /** How a node names the section it belongs to: the heading, and how that
88
+ * section opens. Every node in one section repeats the heading; only one of
89
+ * them has to say `collapsed`. */
90
+ interface SectionOption {
91
+ label: string;
92
+ collapsed?: boolean;
93
+ }
94
+ /** Per-node extras that do not belong on a `PrefLeaf`. */
95
+ interface NodeOptions extends BranchOptions {
96
+ render?: ControlRenderer;
97
+ validate?: (leaf: ToolPrefLeaf) => string[];
98
+ }
99
+ /** What a branch can say about itself, beyond its children. */
100
+ interface BranchOptions {
101
+ section?: SectionOption;
102
+ /** Show this node only while the predicate holds. On a group it hides the
103
+ * whole subtree; the values stay in config either way. */
104
+ showIf?: (config: Record<string, unknown>) => boolean;
105
+ }
106
+ /** A group's own annotations: what it is called and, optionally, why. */
107
+ interface BranchAnnotations {
108
+ name?: string;
109
+ description?: string;
110
+ }
111
+ /** The builder's leaf: a kind (or null, to be decided by rules), a default,
112
+ * an annotation bag, and the extras above. */
113
+ interface ConfigNode<T = unknown> {
114
+ readonly kind: string | null;
115
+ readonly default: T;
116
+ readonly annotations: Readonly<Annotations>;
117
+ readonly options: Readonly<NodeOptions>;
118
+ }
119
+ /** The builder's branch: named children, nested as deeply as the schema
120
+ * wants. A branch nests the value too — `grid: f.group({ size })` puts the
121
+ * value at `grid.size`, where `.section('Grid')` would have left it at
122
+ * `size`. */
123
+ interface ConfigBranch<S extends ConfigShape = ConfigShape> {
124
+ readonly children: S;
125
+ readonly annotations: Readonly<BranchAnnotations>;
126
+ readonly options: Readonly<BranchOptions>;
127
+ }
128
+ /** Either half of a schema tree. */
129
+ type ConfigEntry = ConfigNode | ConfigBranch;
130
+ /** The children of a schema or a group. */
131
+ type ConfigShape = {
132
+ readonly [key: string]: ConfigEntry;
133
+ };
134
+ /** The value type a node produces. */
135
+ type NodeValue<N> = N extends ConfigNode<infer T> ? T : never;
136
+ /** The value type a schema entry produces — a leaf's own, or a branch's
137
+ * nested record. */
138
+ type EntryValue<E> = E extends ConfigBranch<infer S> ? InferConfig<S> : E extends ConfigNode<infer T> ? T : never;
139
+ /** The config type a builder shape produces. */
140
+ type InferConfig<S> = {
141
+ [K in keyof S]: EntryValue<S[K]>;
142
+ };
143
+ /** Whether a config value has children a path can descend into. Arrays and
144
+ * functions are values, not branches. */
145
+ type Branching<V> = V extends readonly unknown[] ? false : V extends (...args: never[]) => unknown ? false : V extends object ? true : false;
146
+ /**
147
+ * Every dotted path a config offers, a group's own path included. A config
148
+ * whose shape is not known — `unknown`, or a bare record — gives `string`,
149
+ * which is what keeps a generic instrument writable.
150
+ */
151
+ type ConfigPath<T> = unknown extends T ? string : T extends object ? {
152
+ [K in keyof T & string]: Branching<T[K]> extends true ? K | `${K}.${ConfigPath<T[K]>}` : K;
153
+ }[keyof T & string] : never;
154
+ /** The type at a dotted path, or `unknown` where the path is not one the
155
+ * config's type spells out. */
156
+ type ValueAtPath<T, P extends string> = unknown extends T ? unknown : P extends `${infer Head}.${infer Rest}` ? Head extends keyof T ? ValueAtPath<T[Head], Rest> : unknown : P extends keyof T ? T[P] : unknown;
157
+ /** An instrument's config, declared once. */
158
+ interface ConfigSchema<TC> {
159
+ readonly nodes: ConfigShape;
160
+ /** The starting config — what `defaultConfig()` would have returned. */
161
+ defaults(): TC;
162
+ }
163
+ /** The config type behind a schema: `ConfigOf<typeof sceneConfig>`. */
164
+ type ConfigOf<S> = S extends ConfigSchema<infer TC> ? TC : never;
165
+
166
+ export type { Annotations as A, BranchAnnotations as B, ConfigPath as C, EntryValue as E, InferConfig as I, LeafPatch as L, NodeOptions as N, ResolvedConfig as R, SectionSpec as S, ValueAtPath as V, ConfigSchema as a, ConfigNode as b, ControlRenderer as c, ConfigOption as d, ConfigShape as e, ConfigBranch as f, BranchOptions as g, ConfigEntry as h, ConfigRule as i, ConfigRuleContext as j, ConfigOf as k, NodeValue as l };
@@ -1,3 +1,5 @@
1
+ import { C as ConfigPath, V as ValueAtPath } from './types-B9_zrHmb.js';
2
+
1
3
  /** A sidebar section torn out of its trial, and where it went. */
2
4
  interface UndockedPanel {
3
5
  trialId: string;
@@ -101,7 +103,9 @@ interface TrialStateHandle<TS, TC> {
101
103
  state: TS;
102
104
  setState: (next: TS | ((prev: TS) => TS)) => void;
103
105
  config: TC;
104
- setConfig: (key: keyof TC, value: TC[keyof TC]) => void;
106
+ /** Write one config value, by dotted path — `'grid.size'` for a leaf under
107
+ * an `f.group`, `'cellSize'` for one at the root. */
108
+ setConfig: <P extends ConfigPath<TC> & string>(path: P, value: ValueAtPath<TC, P>) => void;
105
109
  }
106
110
  /** Options for `createLabStore`. `storageKey` namespaces the keys written, so
107
111
  * two labs on one origin do not collide. */
@@ -109,12 +113,26 @@ interface CreateLabStoreOptions {
109
113
  storageKey: string;
110
114
  storage: StorageAdapter;
111
115
  initialMode?: LabMode;
116
+ /** Each instrument's default config, keyed by instrument name, used to fill
117
+ * the gaps in a stored one. A config saved before its schema grew a branch
118
+ * arrives holding that branch's defaults rather than `undefined`, and keeps
119
+ * whatever keys the schema has since stopped naming. `<Lab>` collects these
120
+ * off its `instruments`. */
121
+ configDefaults?: Record<string, () => unknown>;
122
+ /** How each instrument's state survives a reload. Hydration is the first
123
+ * thing `createLabStore` does, so these have to arrive with the store —
124
+ * anything registered afterwards is already too late to read the document
125
+ * it was built from. `<Lab>` collects them off its `instruments`. */
126
+ serializers?: InstrumentSerializers;
112
127
  }
113
128
  /** Per-instrument serialize/deserialize hooks, keyed by instrument name. An
114
- * instrument whose state is already JSON-safe needs no entry. */
129
+ * instrument whose state is already JSON-safe needs no entry. `deserialize`
130
+ * is handed the config the state was saved against — a trial's own for a
131
+ * reload, the snapshot's for a load — since a state rebuilt without it can
132
+ * disagree with the settings sitting next to it. */
115
133
  type InstrumentSerializers = Record<string, {
116
134
  serialize?: (state: unknown) => unknown;
117
- deserialize?: (data: unknown) => unknown;
135
+ deserialize?: (data: unknown, config: unknown) => unknown;
118
136
  } | undefined>;
119
137
  /** A trial as it is persisted: everything but the undo history, which is
120
138
  * session-only. */
@@ -0,0 +1,62 @@
1
+ /** Which control a config field is edited with. */
2
+ type ConfigFieldType = 'slider' | 'checkbox' | 'select' | 'number' | 'text' | 'color';
3
+ /** What every config field carries: the config key it writes, the label shown
4
+ * beside it, and which control renders it. */
5
+ interface ConfigFieldBase {
6
+ key: string;
7
+ label: string;
8
+ type: ConfigFieldType;
9
+ }
10
+ /** A bounded number edited by dragging. */
11
+ interface SliderField extends ConfigFieldBase {
12
+ type: 'slider';
13
+ default: number;
14
+ min: number;
15
+ max: number;
16
+ step?: number;
17
+ }
18
+ /** A boolean. */
19
+ interface CheckboxField extends ConfigFieldBase {
20
+ type: 'checkbox';
21
+ default: boolean;
22
+ }
23
+ /** One choice in a select field. */
24
+ interface SelectOption {
25
+ value: string;
26
+ label: string;
27
+ }
28
+ /** A fixed set of labeled choices. */
29
+ interface SelectField extends ConfigFieldBase {
30
+ type: 'select';
31
+ default: string;
32
+ options: SelectOption[];
33
+ }
34
+ /** A number typed directly, optionally bounded. Use a slider field instead
35
+ * when the range matters more than the exact value. */
36
+ interface NumberField extends ConfigFieldBase {
37
+ type: 'number';
38
+ default: number;
39
+ min?: number;
40
+ max?: number;
41
+ step?: number;
42
+ }
43
+ /** A free-text string. Writes are debounced so typing does not re-run the
44
+ * instrument on every keystroke. */
45
+ interface TextField extends ConfigFieldBase {
46
+ type: 'text';
47
+ default: string;
48
+ placeholder?: string;
49
+ maxLength?: number;
50
+ /** Milliseconds to debounce live setConfig calls. Default 150 ms. Set to 0 to disable. */
51
+ debounceMs?: number;
52
+ }
53
+ /** A color, as a CSS color string. */
54
+ interface ColorField extends ConfigFieldBase {
55
+ type: 'color';
56
+ default: string;
57
+ }
58
+ /** One field of an instrument's config schema. The schema is what the control
59
+ * panel renders, and what `validateConfigSchema` checks. */
60
+ type ConfigField = SliderField | CheckboxField | SelectField | NumberField | TextField | ColorField;
61
+
62
+ export type { ConfigField as C, NumberField as N, SelectField as S, TextField as T, CheckboxField as a, ColorField as b, ConfigFieldBase as c, ConfigFieldType as d, SelectOption as e, SliderField as f };
@@ -1,4 +1,4 @@
1
- import { a as StorageAdapter, b as LabStoreState, T as TrialRecord, c as SavedSnapshot, h as LabMode, e as UndockedPanel, I as InstrumentSerializers, C as CreateLabStoreOptions, d as TrialStateHandle } from './types-BbXpvQa8.js';
1
+ import { a as StorageAdapter, b as LabStoreState, T as TrialRecord, c as SavedSnapshot, h as LabMode, e as UndockedPanel, C as CreateLabStoreOptions, d as TrialStateHandle } from './types-BP2OCcpg.js';
2
2
  import * as react from 'react';
3
3
  import { ReactNode, ReactElement } from 'react';
4
4
  import { StoreApi } from 'zustand/vanilla';
@@ -23,7 +23,9 @@ interface LabStoreActions {
23
23
  addTrial: (record: Omit<TrialRecord, 'undoStack'>) => void;
24
24
  removeTrial: (id: string) => void;
25
25
  updateTrialState: <TS>(id: string, next: TS | ((prev: TS) => TS)) => void;
26
- updateTrialConfig: <TC>(id: string, key: keyof TC, value: TC[keyof TC]) => void;
26
+ /** Write one config value. `path` is dotted for a value nested under an
27
+ * `f.group`, and the bare key for one at the root. */
28
+ updateTrialConfig: (id: string, path: string, value: unknown) => void;
27
29
  updateTrialView: (id: string, view: unknown) => void;
28
30
  updateTrialSidebarWidth: (id: string, width: number) => void;
29
31
  /** Retitle a trial. `null` returns it to its instrument's name. */
@@ -46,11 +48,9 @@ interface LabStoreActions {
46
48
  /** Put one section back, or — with no `sectionId` — every panel the trial owns. */
47
49
  dockPanel: (trialId: string, sectionId?: string) => void;
48
50
  }
49
- /** A lab's store: its state and actions, plus the hook instruments use to
50
- * register how their state is serialized. */
51
- type LabStore = StoreApi<LabStoreState & LabStoreActions> & {
52
- registerSerializers: (s: InstrumentSerializers) => void;
53
- };
51
+ /** A lab's store: its state and its actions. How each instrument's state is
52
+ * serialized comes in through `CreateLabStoreOptions.serializers`. */
53
+ type LabStore = StoreApi<LabStoreState & LabStoreActions>;
54
54
  /** Build a lab store, hydrating from storage if anything was saved under the
55
55
  * same key. Writes back are debounced. */
56
56
  declare function createLabStore(options: CreateLabStoreOptions): LabStore;
@@ -1,13 +1,14 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { ReactNode, WheelEvent, PointerEvent as PointerEvent$1 } from 'react';
3
- import { V as ViewTransform, c as WorldSpec, a2 as ViewportSize, a as Point, W as WorldFrame } from '../_dts/frac-A4R6v4ld.js';
4
- export { x as DEFAULT_FRAME, a4 as applyCamera, aa as resolveFrame } from '../_dts/frac-A4R6v4ld.js';
5
- import { C as CanvasLayerDescriptor } from '../_dts/CanvasStackContext-kjILVnPj.js';
6
- export { a as CanvasStackContext, b as CanvasStackContextValue, c as CanvasStackSurface } from '../_dts/CanvasStackContext-kjILVnPj.js';
7
- import '../_dts/types-lg4TSCb2.js';
8
- import '../_dts/weasel-canvas-FJTFi3ZZ.js';
9
- import '../_dts/PrefsForm.d-CgxUequc.js';
10
- import '../_dts/types-BbXpvQa8.js';
3
+ import { V as ViewTransform, c as WorldSpec, a2 as ViewportSize, a as Point, W as WorldFrame } from '../_dts/frac-z7ker2Vx.js';
4
+ export { x as DEFAULT_FRAME, a4 as applyCamera, aa as resolveFrame } from '../_dts/frac-z7ker2Vx.js';
5
+ import { C as CanvasLayerDescriptor } from '../_dts/CanvasStackContext-kTEZvEgE.js';
6
+ export { a as CanvasStackContext, b as CanvasStackContextValue, c as CanvasStackSurface } from '../_dts/CanvasStackContext-kTEZvEgE.js';
7
+ import '../_dts/types-B9_zrHmb.js';
8
+ import '@weasel-js/core';
9
+ import '../_dts/PrefsForm.d-DDHFANkc.js';
10
+ import '../_dts/types-x92Kfeme.js';
11
+ import '../_dts/types-BP2OCcpg.js';
11
12
  import '../_dts/types-DJ79Tg5J.js';
12
13
 
13
14
  /** Props for `<CanvasStack>`. */
@@ -1,7 +1,6 @@
1
- export { CanvasStack, PITCH_LIMIT, clampPitch, orbitAfterDrag, orbitAfterWheel, useOrbit, usePanZoom, wrapYaw } from '../chunk-FJG4PHTL.js';
1
+ export { CanvasStack, PITCH_LIMIT, clampPitch, orbitAfterDrag, orbitAfterWheel, useOrbit, usePanZoom, wrapYaw } from '../chunk-I6JCVE24.js';
2
2
  export { centerOn, zoomAt } from '../chunk-CTRKTLYZ.js';
3
3
  export { DEFAULT_FRAME, applyCamera, resolveFrame, screenToWorld, worldToScreen } from '../chunk-2AYGEN57.js';
4
4
  export { CanvasStackContext } from '../chunk-CQLQPQ4P.js';
5
- import '../chunk-54ZWZ5FQ.js';
6
5
  //# sourceMappingURL=index.js.map
7
6
  //# sourceMappingURL=index.js.map
@@ -1,11 +1,12 @@
1
- import { c as ControlRenderer } from '../_dts/types-lg4TSCb2.js';
2
- import { I as Instrument, _ as TrialChromeContext, m as TrialContribution } from '../_dts/frac-A4R6v4ld.js';
3
- export { z as IconComponent, O as SidebarSection, Q as StatusReadout, Y as ToolItem, Z as ToolbarItem, $ as TrialRegion, a1 as ViewportControl } from '../_dts/frac-A4R6v4ld.js';
1
+ import { c as ControlRenderer } from '../_dts/types-B9_zrHmb.js';
2
+ import { I as Instrument, _ as TrialChromeContext, m as TrialContribution } from '../_dts/frac-z7ker2Vx.js';
3
+ export { z as IconComponent, O as SidebarSection, Q as StatusReadout, Y as ToolItem, Z as ToolbarItem, $ as TrialRegion, a1 as ViewportControl } from '../_dts/frac-z7ker2Vx.js';
4
4
  import * as react_jsx_runtime from 'react/jsx-runtime';
5
- import '../_dts/weasel-canvas-FJTFi3ZZ.js';
5
+ import '@weasel-js/core';
6
+ import '../_dts/PrefsForm.d-DDHFANkc.js';
6
7
  import 'react';
7
- import '../_dts/PrefsForm.d-CgxUequc.js';
8
- import '../_dts/types-BbXpvQa8.js';
8
+ import '../_dts/types-x92Kfeme.js';
9
+ import '../_dts/types-BP2OCcpg.js';
9
10
  import '../_dts/types-DJ79Tg5J.js';
10
11
 
11
12
  /**
@@ -1,9 +1,9 @@
1
- export { PaletteRegion, SidebarRegion, StatusRegion, TitleBarRegion, ToolbarRegion, ViewportRegion, builtinContributions, mergeContributions, suppressContributions } from '../chunk-W2FJR5FF.js';
2
- import '../chunk-UTOEDPNU.js';
3
- import '../chunk-XKENZTNE.js';
1
+ export { PaletteRegion, SidebarRegion, StatusRegion, TitleBarRegion, ToolbarRegion, ViewportRegion, builtinContributions, mergeContributions, suppressContributions } from '../chunk-64ZCN3DA.js';
2
+ import '../chunk-SMHP6XZ4.js';
3
+ import '../chunk-U3IYHIAE.js';
4
+ import '../chunk-E44SU6XS.js';
4
5
  import '../chunk-SBXNQW4G.js';
5
- import '../chunk-ISSVF5PT.js';
6
+ import '../chunk-W3ECWC2K.js';
6
7
  import '../chunk-CQLQPQ4P.js';
7
- import '../chunk-54ZWZ5FQ.js';
8
8
  //# sourceMappingURL=index.js.map
9
9
  //# sourceMappingURL=index.js.map