@weasel-js/labkit 1.4.2 → 1.4.3

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 (105) hide show
  1. package/README.md +20 -2
  2. package/dist/_dts/{CanvasStackContext-kjILVnPj.d.ts → CanvasStackContext-Dde8_1my.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-B5mB8LsJ.d.ts} +7 -4
  5. package/dist/_dts/{index-CsU9WhjM.d.ts → index-DelFfeV0.d.ts} +15 -7
  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-UTOEDPNU.js → chunk-2RQPGOQF.js} +7 -6
  15. package/dist/chunk-2RQPGOQF.js.map +1 -0
  16. package/dist/{chunk-ULDW42CR.js → chunk-5TROQVQ2.js} +25 -21
  17. package/dist/chunk-5TROQVQ2.js.map +1 -0
  18. package/dist/{chunk-XKENZTNE.js → chunk-ASWKLRKJ.js} +65 -49
  19. package/dist/chunk-ASWKLRKJ.js.map +1 -0
  20. package/dist/{chunk-W2FJR5FF.js → chunk-C42T6DJT.js} +74 -76
  21. package/dist/chunk-C42T6DJT.js.map +1 -0
  22. package/dist/chunk-E44SU6XS.js +50 -0
  23. package/dist/chunk-E44SU6XS.js.map +1 -0
  24. package/dist/{chunk-WS6ZRV75.js → chunk-FQMJUVHJ.js} +3 -3
  25. package/dist/{chunk-WS6ZRV75.js.map → chunk-FQMJUVHJ.js.map} +1 -1
  26. package/dist/{chunk-FJG4PHTL.js → chunk-I6JCVE24.js} +3 -3
  27. package/dist/chunk-I6JCVE24.js.map +1 -0
  28. package/dist/{chunk-NSOVI3AZ.js → chunk-ISCOFAUO.js} +6 -6
  29. package/dist/{chunk-NSOVI3AZ.js.map → chunk-ISCOFAUO.js.map} +1 -1
  30. package/dist/chunk-MHAGC6VD.js +7320 -0
  31. package/dist/chunk-MHAGC6VD.js.map +1 -0
  32. package/dist/{chunk-UDXOYZEC.js → chunk-QAGAHJKZ.js} +4 -4
  33. package/dist/{chunk-UDXOYZEC.js.map → chunk-QAGAHJKZ.js.map} +1 -1
  34. package/dist/{chunk-TN7YSJVU.js → chunk-RS3HRQWU.js} +3 -3
  35. package/dist/{chunk-TN7YSJVU.js.map → chunk-RS3HRQWU.js.map} +1 -1
  36. package/dist/{chunk-D5KQ5OY6.js → chunk-TJ7QY3OC.js} +3 -3
  37. package/dist/{chunk-D5KQ5OY6.js.map → chunk-TJ7QY3OC.js.map} +1 -1
  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 +90 -23
  43. package/dist/index.js +199 -117
  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 +88 -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 +2 -2
  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/config/builder.test.ts +56 -2
  66. package/src/config/builder.ts +73 -13
  67. package/src/config/index.ts +16 -1
  68. package/src/config/path.test.ts +109 -0
  69. package/src/config/path.ts +76 -0
  70. package/src/config/resolve.test.ts +93 -2
  71. package/src/config/resolve.ts +72 -29
  72. package/src/config/types.ts +99 -9
  73. package/src/config/visible.ts +6 -4
  74. package/src/controls/ControlPanel.stories.tsx +38 -4
  75. package/src/controls/ControlPanel.test.tsx +114 -0
  76. package/src/controls/ControlPanel.tsx +99 -50
  77. package/src/index.ts +16 -1
  78. package/src/instrument/serializers.ts +36 -0
  79. package/src/instrument/types.ts +4 -2
  80. package/src/lab/Lab.test.tsx +29 -1
  81. package/src/lab/Lab.tsx +10 -1
  82. package/src/lab/LabShell.test.tsx +8 -0
  83. package/src/lab/LabShell.tsx +3 -1
  84. package/src/primitives/ZoomControl.less +6 -2
  85. package/src/primitives/ZoomControl.tsx +1 -0
  86. package/src/state/helpers.ts +1 -1
  87. package/src/state/store.test.ts +155 -1
  88. package/src/state/store.ts +30 -24
  89. package/src/state/types.ts +24 -3
  90. package/src/state/useTrialState.ts +1 -1
  91. package/src/trial/Trial.config.test.tsx +44 -0
  92. package/src/trial/Trial.tsx +3 -3
  93. package/src/trial/TrialChrome.tsx +10 -5
  94. package/src/trial/trialOps.ts +4 -8
  95. package/dist/_dts/types-lg4TSCb2.d.ts +0 -164
  96. package/dist/_dts/weasel-canvas-FJTFi3ZZ.d.ts +0 -5041
  97. package/dist/chunk-54ZWZ5FQ.js +0 -29997
  98. package/dist/chunk-54ZWZ5FQ.js.map +0 -1
  99. package/dist/chunk-FJG4PHTL.js.map +0 -1
  100. package/dist/chunk-ISSVF5PT.js +0 -7263
  101. package/dist/chunk-ISSVF5PT.js.map +0 -1
  102. package/dist/chunk-ULDW42CR.js.map +0 -1
  103. package/dist/chunk-UTOEDPNU.js.map +0 -1
  104. package/dist/chunk-W2FJR5FF.js.map +0 -1
  105. 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-B5mB8LsJ.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 & {
@@ -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,
@@ -258,7 +259,9 @@ interface ControlPanelProps<TC extends Record<string, unknown>> {
258
259
  /** @deprecated Pass `schema`. A field list is adapted into one internally. */
259
260
  fields?: ConfigField[];
260
261
  config: TC;
261
- setConfig: (key: keyof TC, value: unknown) => void;
262
+ /** Writes one value. The path is dotted for a leaf inside an `f.group`, and
263
+ * the bare key for one at the root. */
264
+ setConfig: (path: string, value: unknown) => void;
262
265
  /**
263
266
  * Control overrides and app-defined kinds, PrefsForm-style. Keys are config
264
267
  * paths (checked first) or leaf kinds. A renderer returning `null` collapses
@@ -279,16 +282,21 @@ interface ControlPanelProps<TC extends Record<string, unknown>> {
279
282
  align?: PropertyAlign;
280
283
  /**
281
284
  * Fold each section away behind a twisty, starting `'open'` or `'closed'`.
282
- * Unset draws the heading alone, as the panel always has.
285
+ * Unset draws the heading alone, as the panel always has — unless a section
286
+ * in the schema declares how it opens, which makes that section foldable on
287
+ * its own and outranks this for that one section.
283
288
  */
284
289
  collapse?: 'open' | 'closed';
285
290
  /**
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.
291
+ * Which sections are folded, keyed as `onCollapse` reports them. Given, the
292
+ * panel keeps no state of its own: every toggle arrives at `onCollapse`
293
+ * instead, which is where a lab that remembers a trial's sections writes
294
+ * them.
289
295
  */
290
296
  collapsed?: Readonly<Record<string, boolean>>;
291
- onCollapse?: (label: string, collapsed: boolean) => void;
297
+ /** A fold moved. The key is a section's label — prefixed by its group's
298
+ * dotted path when the section sits inside one — or a group's own path. */
299
+ onCollapse?: (key: string, collapsed: boolean) => void;
292
300
  /** Draw leaves marked `hidden`. */
293
301
  showHidden?: boolean;
294
302
  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-B5mB8LsJ.js';
4
+ export { x as DEFAULT_FRAME, a4 as applyCamera, aa as resolveFrame } from '../_dts/frac-B5mB8LsJ.js';
5
+ import { C as CanvasLayerDescriptor } from '../_dts/CanvasStackContext-Dde8_1my.js';
6
+ export { a as CanvasStackContext, b as CanvasStackContextValue, c as CanvasStackSurface } from '../_dts/CanvasStackContext-Dde8_1my.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-B5mB8LsJ.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-B5mB8LsJ.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-C42T6DJT.js';
2
+ import '../chunk-2RQPGOQF.js';
3
+ import '../chunk-ASWKLRKJ.js';
4
+ import '../chunk-E44SU6XS.js';
4
5
  import '../chunk-SBXNQW4G.js';
5
- import '../chunk-ISSVF5PT.js';
6
+ import '../chunk-MHAGC6VD.js';
6
7
  import '../chunk-CQLQPQ4P.js';
7
- import '../chunk-54ZWZ5FQ.js';
8
8
  //# sourceMappingURL=index.js.map
9
9
  //# sourceMappingURL=index.js.map
@@ -1,8 +1,8 @@
1
- import { y3, f } from './chunk-ISSVF5PT.js';
1
+ import { y2, f2 } from './chunk-MHAGC6VD.js';
2
2
  import { CanvasStackContext } from './chunk-CQLQPQ4P.js';
3
- import { useVisibleRaf } from './chunk-54ZWZ5FQ.js';
4
3
  import { useRef, useCallback, useEffect, useState, useContext } from 'react';
5
4
  import { jsx, jsxs } from 'react/jsx-runtime';
5
+ import { useVisibleRaf } from '@weasel-js/core';
6
6
 
7
7
  function nextIndex(current, key, count, orientation = "horizontal") {
8
8
  if (count === 0) return null;
@@ -220,7 +220,7 @@ function ZoomControl({
220
220
  };
221
221
  return /* @__PURE__ */ jsxs("div", { className: `lk-zoom${className ? ` ${className}` : ""}`, children: [
222
222
  /* @__PURE__ */ jsx(
223
- y3,
223
+ y2,
224
224
  {
225
225
  className: "lk-zoom__slider",
226
226
  ariaLabel: "Zoom",
@@ -235,9 +235,10 @@ function ZoomControl({
235
235
  }
236
236
  ),
237
237
  /* @__PURE__ */ jsx(
238
- f,
238
+ f2,
239
239
  {
240
240
  className: "lk-zoom__field",
241
+ width: "fit",
241
242
  "aria-label": "Zoom",
242
243
  value: zoom,
243
244
  minValue: min,
@@ -263,5 +264,5 @@ function Section({ children, end }) {
263
264
  StatusBar.Section = Section;
264
265
 
265
266
  export { FpsMeter, ScaleIndicator, StatusBar, Toolbar, ZoomControl, useRovingTabIndex };
266
- //# sourceMappingURL=chunk-UTOEDPNU.js.map
267
- //# sourceMappingURL=chunk-UTOEDPNU.js.map
267
+ //# sourceMappingURL=chunk-2RQPGOQF.js.map
268
+ //# sourceMappingURL=chunk-2RQPGOQF.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/primitives/useRovingTabIndex.ts","../src/primitives/Toolbar.tsx","../src/primitives/fpsAverage.ts","../src/primitives/FpsMeter.tsx","../src/primitives/ScaleIndicator.tsx","../src/primitives/ZoomControl.tsx","../src/primitives/StatusBar.tsx"],"names":["useRef","useEffect","jsx","jsxs","y","f"],"mappings":"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","file":"chunk-2RQPGOQF.js","sourcesContent":["import { type KeyboardEvent, useCallback, useEffect, useRef } from 'react';\n\n/** Which axis a toolbar's arrow keys walk. Matches `aria-orientation`. */\nexport type RovingOrientation = 'horizontal' | 'vertical';\n\n/** Where a key takes focus within `count` items, or null if it does not move it.\n *\n * Only the arrows on the toolbar's own axis move focus — APG leaves the\n * cross-axis arrows to the page, so a vertical strip does not swallow\n * ArrowLeft. `Home` / `End` work on both. */\nexport function nextIndex(\n current: number,\n key: string,\n count: number,\n orientation: RovingOrientation = 'horizontal',\n): number | null {\n if (count === 0) return null;\n const forward = orientation === 'vertical' ? 'ArrowDown' : 'ArrowRight';\n const back = orientation === 'vertical' ? 'ArrowUp' : 'ArrowLeft';\n if (key === forward) return (current + 1) % count;\n if (key === back) return (current - 1 + count) % count;\n if (key === 'Home') return 0;\n if (key === 'End') return count - 1;\n return null;\n}\n\n/**\n * The APG roving-tabindex contract: exactly one item in the tab order, arrows\n * moving focus within. `tabIndex` is written onto the DOM nodes rather than\n * threaded through props, because the items are arbitrary children — the\n * container never sees them as a list it could index.\n */\nexport function useRovingTabIndex<T extends HTMLElement>(\n orientation: RovingOrientation = 'horizontal',\n) {\n const ref = useRef<T | null>(null);\n const stop = useRef(0);\n\n const items = useCallback(\n (): HTMLElement[] =>\n ref.current\n ? Array.from(\n ref.current.querySelectorAll<HTMLElement>('button:not([disabled]), [role=\"button\"]'),\n )\n : [],\n [],\n );\n\n const setTabStop = useCallback(\n (index: number) => {\n const found = items();\n const at = Math.min(Math.max(index, 0), Math.max(found.length - 1, 0));\n stop.current = at;\n for (const [i, el] of found.entries()) el.tabIndex = i === at ? 0 : -1;\n },\n [items],\n );\n\n // Re-establish the tab stop whenever the item set changes — a trial's toolbar\n // contributions are dynamic, and `disabled` moves a button in and out of the\n // set without touching the child list. The focused item keeps the stop.\n useEffect(() => {\n const resync = () => {\n const focused = items().indexOf(document.activeElement as HTMLElement);\n setTabStop(focused === -1 ? stop.current : focused);\n };\n resync();\n if (!ref.current) return;\n const mo = new MutationObserver(resync);\n mo.observe(ref.current, { childList: true, subtree: true, attributeFilter: ['disabled'] });\n return () => mo.disconnect();\n }, [items, setTabStop]);\n\n const onKeyDown = useCallback(\n (e: KeyboardEvent<T>) => {\n const found = items();\n const current = found.indexOf(document.activeElement as HTMLElement);\n const next = nextIndex(current === -1 ? 0 : current, e.key, found.length, orientation);\n if (next === null) return;\n e.preventDefault();\n found[next]?.focus();\n setTabStop(next);\n },\n [items, setTabStop, orientation],\n );\n\n return { ref, onKeyDown };\n}\n","import type { MouseEventHandler, ReactNode } from 'react';\nimport { useRovingTabIndex } from './useRovingTabIndex';\n\n/** Props for `<Toolbar>`. */\nexport interface ToolbarProps {\n children: ReactNode;\n /** Names the toolbar for assistive tech. Required by the APG pattern when a\n * view holds more than one. */\n 'aria-label'?: string;\n}\n\n/** A horizontal bar of controls. Fill it with `<Toolbar.Title>`,\n * `<Toolbar.Group>`, `<Toolbar.Button>` and `<Toolbar.Spacer>`. */\nexport function Toolbar({ children, 'aria-label': ariaLabel }: ToolbarProps) {\n const { ref, onKeyDown } = useRovingTabIndex<HTMLDivElement>();\n return (\n <div\n ref={ref}\n className=\"lk-toolbar\"\n role=\"toolbar\"\n aria-label={ariaLabel}\n onKeyDown={onKeyDown}\n >\n {children}\n </div>\n );\n}\n\ninterface TitleProps {\n children: ReactNode;\n}\nfunction Title({ children }: TitleProps) {\n return <span className=\"lk-toolbar-title\">{children}</span>;\n}\n\n/** Props for `<Toolbar.Group>`. */\nexport interface ToolbarGroupProps {\n children: ReactNode;\n /** Pushes this group, and everything after it, to the far end. */\n end?: boolean;\n 'aria-label'?: string;\n}\n\n/** Related controls, kept tight and ruled off from their neighbors. Group\n * rather than separate with repeated `<Toolbar.Spacer>`: consecutive spacers\n * share the free space, so every gap collapses to the same slack and the bar\n * reads as one undifferentiated run. */\nfunction Group({ children, end, 'aria-label': ariaLabel }: ToolbarGroupProps) {\n return (\n // biome-ignore lint/a11y/useSemanticElements: a group inside role=\"toolbar\" is the APG pattern; the rule is local and cannot see the parent. <fieldset> means form controls, needs a <legend> to be named, and its UA min-width breaks flex children.\n <div\n className={`lk-toolbar-group${end ? ' lk-toolbar-group--end' : ''}`}\n role=\"group\"\n aria-label={ariaLabel}\n >\n {children}\n </div>\n );\n}\n\n/** Props for `<Toolbar.Button>`. */\nexport interface ToolbarButtonProps {\n children: ReactNode;\n onClick: MouseEventHandler<HTMLButtonElement>;\n disabled?: boolean;\n title?: string;\n /** Required when `children` is an icon with no text of its own. */\n 'aria-label'?: string;\n /** `danger` reddens on hover — for actions that discard work. */\n variant?: 'default' | 'danger';\n /** Square, for a button whose whole content is one glyph. */\n iconOnly?: boolean;\n /** Makes this a toggle: `aria-pressed`, and held down while true. */\n pressed?: boolean;\n}\n\nfunction Button({\n children,\n onClick,\n disabled,\n title,\n 'aria-label': ariaLabel,\n variant = 'default',\n iconOnly,\n pressed,\n}: ToolbarButtonProps) {\n const cls = [\n 'lk-toolbar-button',\n variant === 'danger' && 'lk-toolbar-button--danger',\n iconOnly && 'lk-toolbar-button--icon',\n pressed && 'lk-toolbar-button--pressed',\n ]\n .filter(Boolean)\n .join(' ');\n return (\n <button\n type=\"button\"\n className={cls}\n onClick={onClick}\n disabled={disabled}\n title={title}\n aria-label={ariaLabel ?? title}\n aria-pressed={pressed}\n >\n {children}\n </button>\n );\n}\n\nfunction Spacer() {\n return <span className=\"lk-toolbar-spacer\" aria-hidden=\"true\" />;\n}\n\n/** A label within a toolbar. */\nToolbar.Title = Title;\n/** A ruled-off run of related controls. */\nToolbar.Group = Group;\n/** A button within a toolbar. */\nToolbar.Button = Button;\n/** Flexible space that pushes what follows to the far end of the toolbar. */\nToolbar.Spacer = Spacer;\n","export function rollingAverage(samples: readonly number[]): number {\n if (samples.length === 0) return 0;\n const sum = samples.reduce((acc, n) => acc + n, 0);\n return Math.round(sum / samples.length);\n}\n","import { useVisibleRaf } from '@weasel-js/core';\nimport { useEffect, useRef, useState } from 'react';\nimport { rollingAverage } from './fpsAverage';\n\nconst SAMPLE_WINDOW = 30;\n\n/** A live frame-rate readout, averaged over the last 30 frames. */\nexport function FpsMeter() {\n const [fps, setFps] = useState(0);\n const samplesRef = useRef<number[]>([]);\n const lastTimeRef = useRef<number | null>(null);\n const rootRef = useRef<HTMLDivElement | null>(null);\n\n const loop = useVisibleRaf(\n (time) => {\n const last = lastTimeRef.current;\n if (last !== null) {\n const delta = time - last;\n if (delta > 0) {\n const samples = samplesRef.current;\n samples.push(1000 / delta);\n if (samples.length > SAMPLE_WINDOW) samples.shift();\n setFps(rollingAverage(samples));\n }\n }\n lastTimeRef.current = time;\n loop.request();\n },\n {\n target: rootRef,\n // The gap spent suspended is not a frame that took an hour to draw.\n onResume: () => {\n lastTimeRef.current = null;\n },\n },\n );\n\n useEffect(() => {\n loop.request();\n return () => loop.cancel();\n }, [loop]);\n\n return (\n <div className=\"lk-fps-meter\" ref={rootRef}>\n <span className=\"lk-fps-meter-value\">FPS {fps}</span>\n </div>\n );\n}\n","import { useContext } from 'react';\nimport { CanvasStackContext } from '../canvas/CanvasStackContext';\n\n/** Props for `<ScaleIndicator>`. */\nexport interface ScaleIndicatorProps {\n /** Current view zoom factor. If omitted, reads from CanvasStackContext. Defaults to 1. */\n zoom?: number;\n /** World pixels per unit at zoom=1. Default: 1. */\n pixelsPerUnit?: number;\n /** Unit label. Default: 'px'. */\n unit?: string;\n /** Target bar width in display pixels. Default: 100. */\n targetWidth?: number;\n}\n\n// Rounds n to a \"nice\" number — 1, 2, 5, 10, 20, 50, ...\nfunction niceNumber(n: number): number {\n if (n <= 0) return 1;\n const exp = Math.floor(Math.log10(n));\n const fraction = n / 10 ** exp;\n let nice: number;\n if (fraction < 1.5) nice = 1;\n else if (fraction < 3.5) nice = 2;\n else if (fraction < 7.5) nice = 5;\n else nice = 10;\n return nice * 10 ** exp;\n}\n\n/** A scale bar: a labeled rule showing how far a round number of world units\n * is at the current zoom. Reads the zoom from the surrounding canvas stack\n * unless one is passed. */\nexport function ScaleIndicator({\n zoom,\n pixelsPerUnit = 1,\n unit = 'px',\n targetWidth = 100,\n}: ScaleIndicatorProps) {\n const ctxValue = useContext(CanvasStackContext);\n const effectiveZoom = zoom ?? ctxValue?.view.zoom ?? 1;\n const effectivePxPerUnit = pixelsPerUnit * effectiveZoom;\n const targetUnits = targetWidth / effectivePxPerUnit;\n const niceUnits = niceNumber(targetUnits);\n const barWidth = niceUnits * effectivePxPerUnit;\n return (\n <div className=\"lk-scale-indicator\" role=\"img\" aria-label={`Scale: ${niceUnits} ${unit}`}>\n <div className=\"lk-scale-indicator-bar\" style={{ width: `${barWidth}px` }} />\n <span className=\"lk-scale-indicator-label\">\n {niceUnits} {unit}\n </span>\n </div>\n );\n}\n","import { NumberField, Slider } from '../passthrough/weasel-ui';\n\n/** Props for `<ZoomControl>`. */\nexport interface ZoomControlProps {\n /** Current zoom as a scale factor: 1 is actual size. */\n zoom: number;\n /** Fired continuously while the slider drags. */\n onZoomChange: (zoom: number) => void;\n /** Fired when a drag settles or the field is committed. Defaults to\n * `onZoomChange`. */\n onZoomCommit?: (zoom: number) => void;\n /** Smallest selectable zoom. Defaults to 0.1 (10%). */\n min?: number;\n /** Largest selectable zoom. Defaults to 8 (800%). */\n max?: number;\n className?: string;\n}\n\n// Zoom is perceptually geometric — halving is as big a step as doubling — so\n// the slider runs on log2 and 100% lands mid-track for a symmetric range.\nconst toTrack = (zoom: number): number => Math.log2(zoom);\nconst fromTrack = (v: number): number => 2 ** v;\n\nconst clamp = (v: number, lo: number, hi: number): number => Math.max(lo, Math.min(hi, v));\n\n/** Zoom as a log-scaled slider with an editable percentage beside it. Pair it\n * with zoom-in / zoom-out buttons in a `<Toolbar.Group>`; this component owns\n * only the continuous part. */\nexport function ZoomControl({\n zoom,\n onZoomChange,\n onZoomCommit,\n min = 0.1,\n max = 8,\n className,\n}: ZoomControlProps) {\n const commit = onZoomCommit ?? onZoomChange;\n const emit = (track: number, done: boolean): void => {\n const next = clamp(fromTrack(track), min, max);\n (done ? commit : onZoomChange)(next);\n };\n\n return (\n <div className={`lk-zoom${className ? ` ${className}` : ''}`}>\n <Slider\n className=\"lk-zoom__slider\"\n ariaLabel=\"Zoom\"\n density=\"slim\"\n min={toTrack(min)}\n max={toTrack(max)}\n // Detents at the octaves plus actual size, so 25/50/100/200/400% are\n // reachable by drag rather than only by typing.\n stops={[-2, -1, 0, 1, 2]}\n thumbs={[{ value: clamp(toTrack(zoom), toTrack(min), toTrack(max)) }]}\n onInput={(next) => emit(next[0].value, false)}\n onChange={(next) => emit(next[0].value, true)}\n readoutPlacement=\"none\"\n />\n {/* `style: 'percent'` formats a fraction, so the field carries the scale\n factor itself and renders 0.5 as \"50%\". */}\n <NumberField\n className=\"lk-zoom__field\"\n width=\"fit\"\n aria-label=\"Zoom\"\n value={zoom}\n minValue={min}\n maxValue={max}\n step={0.01}\n hideSteppers\n ghost\n formatOptions={{ style: 'percent', maximumFractionDigits: 0 }}\n onChange={(next) => {\n if (!Number.isFinite(next)) return;\n commit(clamp(next, min, max));\n }}\n />\n </div>\n );\n}\n","import type { ReactNode } from 'react';\n\n/** Props for `<StatusBar>`. */\nexport interface StatusBarProps {\n children: ReactNode;\n}\n\n/** A footer strip for readouts. Fill it with `<StatusBar.Section>`. */\nexport function StatusBar({ children }: StatusBarProps) {\n return <div className=\"lk-status-bar\">{children}</div>;\n}\n\n/** Props for `<StatusBar.Section>`. */\nexport interface StatusBarSectionProps {\n children: ReactNode;\n /** Pushes this section, and everything after it, to the far end. */\n end?: boolean;\n}\nfunction Section({ children, end }: StatusBarSectionProps) {\n return (\n <span className={`lk-status-bar-section${end ? ' lk-status-bar-section--end' : ''}`}>\n {children}\n </span>\n );\n}\n\n/** One readout within a status bar. */\nStatusBar.Section = Section;\n"]}