@weasel-js/labkit 1.4.1 → 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 (145) hide show
  1. package/README.md +28 -2
  2. package/dist/_dts/{CanvasStackContext-BIRUVVGH.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-X7mWgd7y.d.ts → frac-B5mB8LsJ.d.ts} +33 -13
  5. package/dist/_dts/{index-FEa1MudK.d.ts → index-DelFfeV0.d.ts} +99 -31
  6. package/dist/_dts/types-B9_zrHmb.d.ts +166 -0
  7. package/dist/_dts/{types-ChVJJHvk.d.ts → types-BP2OCcpg.d.ts} +41 -4
  8. package/dist/_dts/types-x92Kfeme.d.ts +62 -0
  9. package/dist/_dts/{useTrialState-Cdm13fvl.d.ts → useTrialState-D6Vb3T-g.d.ts} +11 -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 +16 -9
  13. package/dist/chrome/index.js +5 -5
  14. package/dist/{chunk-MSR7EVS6.js → chunk-2RQPGOQF.js} +6 -5
  15. package/dist/chunk-2RQPGOQF.js.map +1 -0
  16. package/dist/{chunk-LJSIUFYD.js → chunk-5TROQVQ2.js} +46 -21
  17. package/dist/chunk-5TROQVQ2.js.map +1 -0
  18. package/dist/{chunk-53AZ2O2N.js → chunk-ASWKLRKJ.js} +92 -45
  19. package/dist/chunk-ASWKLRKJ.js.map +1 -0
  20. package/dist/{chunk-BMQWLKOL.js → chunk-C42T6DJT.js} +94 -83
  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-OFCBEZMU.js → chunk-FQMJUVHJ.js} +3 -3
  25. package/dist/{chunk-OFCBEZMU.js.map → chunk-FQMJUVHJ.js.map} +1 -1
  26. package/dist/{chunk-PP7ZWAMV.js → chunk-I6JCVE24.js} +3 -3
  27. package/dist/chunk-I6JCVE24.js.map +1 -0
  28. package/dist/chunk-ISCOFAUO.js +170 -0
  29. package/dist/chunk-ISCOFAUO.js.map +1 -0
  30. package/dist/chunk-MHAGC6VD.js +7320 -0
  31. package/dist/chunk-MHAGC6VD.js.map +1 -0
  32. package/dist/{chunk-7TRFMIWX.js → chunk-QAGAHJKZ.js} +4 -4
  33. package/dist/{chunk-7TRFMIWX.js.map → chunk-QAGAHJKZ.js.map} +1 -1
  34. package/dist/{chunk-D7QIPGEB.js → chunk-RS3HRQWU.js} +62 -60
  35. package/dist/chunk-RS3HRQWU.js.map +1 -0
  36. package/dist/{chunk-RLOXQZW3.js → chunk-TJ7QY3OC.js} +3 -3
  37. package/dist/{chunk-RLOXQZW3.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 +9 -8
  41. package/dist/dragdrop/index.js +1 -2
  42. package/dist/index.d.ts +152 -57
  43. package/dist/index.js +315 -200
  44. package/dist/index.js.map +1 -1
  45. package/dist/layers/index.d.ts +27 -11
  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 +171 -22
  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 +21 -4
  59. package/dist/surface/index.js +1 -2
  60. package/dist/ui/layers/index.d.ts +22 -9
  61. package/dist/ui/layers/index.js +1 -2
  62. package/dist/undo/index.d.ts +6 -6
  63. package/package.json +7 -7
  64. package/src/annotations/AnnotationTargets.tsx +6 -1
  65. package/src/annotations/Annotations.overlay.test.tsx +2 -0
  66. package/src/annotations/ExportMenu.test.tsx +16 -0
  67. package/src/annotations/ExportMenu.tsx +0 -5
  68. package/src/annotations/types.ts +4 -1
  69. package/src/canvas/useOrbit.test.ts +7 -3
  70. package/src/canvas/usePanZoom.test.ts +7 -3
  71. package/src/chrome/ChromeRegions.stories.tsx +42 -1
  72. package/src/chrome/builtins.test.ts +4 -0
  73. package/src/chrome/builtins.tsx +18 -0
  74. package/src/chrome/index.ts +1 -1
  75. package/src/chrome/regions/SidebarRegion.tsx +7 -6
  76. package/src/chrome/regions/TitleBarRegion.test.tsx +64 -0
  77. package/src/chrome/regions/TitleBarRegion.tsx +16 -7
  78. package/src/chrome/regions/ToolbarRegion.test.tsx +14 -0
  79. package/src/chrome/regions/ToolbarRegion.tsx +2 -2
  80. package/src/chrome/regions/ViewportRegion.tsx +2 -2
  81. package/src/chrome/regions/regions.test.tsx +45 -1
  82. package/src/chrome/types.ts +21 -3
  83. package/src/config/builder.test.ts +56 -2
  84. package/src/config/builder.ts +73 -13
  85. package/src/config/index.ts +16 -1
  86. package/src/config/path.test.ts +109 -0
  87. package/src/config/path.ts +76 -0
  88. package/src/config/resolve.test.ts +93 -2
  89. package/src/config/resolve.ts +72 -29
  90. package/src/config/types.ts +99 -9
  91. package/src/config/visible.ts +6 -4
  92. package/src/controls/ControlPanel.stories.tsx +38 -4
  93. package/src/controls/ControlPanel.test.tsx +152 -0
  94. package/src/controls/ControlPanel.tsx +134 -40
  95. package/src/dragdrop/DragDropRuntime.tsx +74 -61
  96. package/src/dragdrop/Palette.tsx +4 -3
  97. package/src/dragdrop/dragDrop.test.tsx +35 -12
  98. package/src/index.ts +18 -2
  99. package/src/instrument/serializers.ts +36 -0
  100. package/src/instrument/types.ts +8 -7
  101. package/src/lab/Lab.test.tsx +36 -1
  102. package/src/lab/Lab.tsx +12 -3
  103. package/src/lab/LabContext.ts +5 -1
  104. package/src/lab/LabShell.less +17 -2
  105. package/src/lab/LabShell.test.tsx +8 -0
  106. package/src/lab/LabShell.tsx +3 -1
  107. package/src/layers/AGENTS.md +29 -7
  108. package/src/layers/LayerList.less +16 -0
  109. package/src/layers/LayerList.stories.tsx +66 -0
  110. package/src/layers/LayerList.test.tsx +117 -5
  111. package/src/layers/LayerList.tsx +192 -53
  112. package/src/layers/index.ts +1 -1
  113. package/src/passthrough/weasel-ui.ts +5 -0
  114. package/src/primitives/FloatingPanel.test.tsx +8 -3
  115. package/src/primitives/ZoomControl.less +6 -2
  116. package/src/primitives/ZoomControl.tsx +1 -0
  117. package/src/state/helpers.ts +1 -1
  118. package/src/state/store.test.ts +233 -1
  119. package/src/state/store.ts +57 -24
  120. package/src/state/types.ts +44 -3
  121. package/src/state/useTrialState.ts +1 -1
  122. package/src/trial/Trial.config.test.tsx +44 -0
  123. package/src/trial/Trial.less +4 -0
  124. package/src/trial/Trial.targets.test.tsx +96 -0
  125. package/src/trial/Trial.test.tsx +171 -14
  126. package/src/trial/Trial.tsx +7 -4
  127. package/src/trial/TrialChrome.tsx +32 -7
  128. package/src/trial/TrialTitleBar.tsx +7 -3
  129. package/src/trial/index.ts +1 -0
  130. package/src/trial/trialOps.test.ts +49 -1
  131. package/src/trial/trialOps.ts +24 -6
  132. package/dist/_dts/types-lg4TSCb2.d.ts +0 -164
  133. package/dist/_dts/weasel-canvas-FJTFi3ZZ.d.ts +0 -5041
  134. package/dist/chunk-53AZ2O2N.js.map +0 -1
  135. package/dist/chunk-BMQWLKOL.js.map +0 -1
  136. package/dist/chunk-D7QIPGEB.js.map +0 -1
  137. package/dist/chunk-DG2IFMLA.js +0 -29970
  138. package/dist/chunk-DG2IFMLA.js.map +0 -1
  139. package/dist/chunk-J6SBFZYT.js +0 -7121
  140. package/dist/chunk-J6SBFZYT.js.map +0 -1
  141. package/dist/chunk-LJSIUFYD.js.map +0 -1
  142. package/dist/chunk-MIT3ISW4.js +0 -100
  143. package/dist/chunk-MIT3ISW4.js.map +0 -1
  144. package/dist/chunk-MSR7EVS6.js.map +0 -1
  145. package/dist/chunk-PP7ZWAMV.js.map +0 -1
@@ -1,7 +1,8 @@
1
- import { C2, x3, k, P2, N2, A2, D, M2, T2, j2 } from './chunk-J6SBFZYT.js';
2
- import { isBuiltinToolPref } from './chunk-DG2IFMLA.js';
3
- import { useMemo, useState, useRef, useEffect } from 'react';
4
- import { jsxs, jsx } from 'react/jsx-runtime';
1
+ import { schemaNodeAtPath, valueAtPath } from './chunk-E44SU6XS.js';
2
+ import { T2, V, t2, j2, I, F, M2, k, P2, D, N2 } from './chunk-MHAGC6VD.js';
3
+ import { isBuiltinToolPref } from '@weasel-js/core';
4
+ import { useMemo, Fragment as Fragment$1, useState, useRef, useEffect } from 'react';
5
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
6
 
6
7
  // src/config/fromConfigField.ts
7
8
  function toLeaf(field) {
@@ -64,8 +65,8 @@ function fromConfigFields(fields) {
64
65
 
65
66
  // src/config/visible.ts
66
67
  function isLeafVisible(resolved, path, config, showHidden = false) {
67
- const leaf = resolved.group.children[path];
68
- if (leaf && "hidden" in leaf && leaf.hidden === true && !showHidden) return false;
68
+ const node = schemaNodeAtPath(resolved.group, path);
69
+ if (node && "hidden" in node && node.hidden === true && !showHidden) return false;
69
70
  const predicate = resolved.showIf.get(path);
70
71
  return predicate ? predicate(config) : true;
71
72
  }
@@ -76,60 +77,96 @@ function ControlPanel({
76
77
  setConfig,
77
78
  renderers,
78
79
  pack = "pairs",
79
- layout = "block",
80
+ layout,
81
+ density,
82
+ align,
83
+ collapse,
84
+ collapsed,
85
+ onCollapse,
80
86
  showHidden = false,
81
87
  className
82
88
  }) {
83
89
  const resolved = useMemo(() => schema ?? fromConfigFields(fields ?? []), [schema, fields]);
84
- const paths = Object.keys(resolved.group.children);
85
- const sectioned = new Set(resolved.sections.flatMap((s) => s.paths));
86
- const loose = paths.filter((p) => !sectioned.has(p));
87
- const row = (path) => /* @__PURE__ */ jsx(
88
- ControlRow,
89
- {
90
- path,
91
- resolved,
92
- config,
93
- setConfig,
94
- renderers,
95
- pack,
96
- layout,
97
- showHidden
98
- },
99
- path
100
- );
101
90
  const gridPack = pack === "one-up" ? "auto-color" : "pairs";
102
- return /* @__PURE__ */ jsxs(
103
- C2,
91
+ const folds = collapse !== void 0 || collapsed !== void 0 || onCollapse !== void 0 || resolved.sections.some((s) => s.collapsed !== void 0);
92
+ const startsFolded = collapse === "closed";
93
+ const fold = (key, declared) => ({
94
+ pack: gridPack,
95
+ collapsible: folds,
96
+ defaultCollapsed: declared ?? startsFolded,
97
+ collapsed: collapsed ? collapsed[key] ?? declared ?? startsFolded : void 0,
98
+ onCollapsedChange: onCollapse ? (next) => onCollapse(key, next) : void 0
99
+ });
100
+ const node = (path) => {
101
+ const found = schemaNodeAtPath(resolved.group, path);
102
+ if (!found) return null;
103
+ if (!isLeafVisible(resolved, path, config, showHidden)) return null;
104
+ if (t2(found)) {
105
+ return /* @__PURE__ */ jsx(
106
+ ControlRow,
107
+ {
108
+ path,
109
+ leaf: found,
110
+ resolved,
111
+ config,
112
+ setConfig,
113
+ renderers,
114
+ pack,
115
+ layout
116
+ },
117
+ path
118
+ );
119
+ }
120
+ if (found.name === "") return /* @__PURE__ */ jsx(Fragment$1, { children: body(found, path) }, path);
121
+ return /* @__PURE__ */ jsx(V, { title: found.name, ...fold(path), children: body(found, path) }, path);
122
+ };
123
+ const body = (group, at) => {
124
+ const sections = resolved.sections.filter((s) => s.at === at);
125
+ const sectioned = new Set(sections.flatMap((s) => s.paths));
126
+ const paths = Object.keys(group.children).map((key) => at === "" ? key : `${at}.${key}`);
127
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
128
+ paths.filter((p) => !sectioned.has(p)).map(node),
129
+ sections.map((section) => /* @__PURE__ */ jsx(
130
+ V,
131
+ {
132
+ title: section.label,
133
+ ...fold(sectionKey(section), section.collapsed),
134
+ children: section.paths.map(node)
135
+ },
136
+ sectionKey(section)
137
+ ))
138
+ ] });
139
+ };
140
+ return /* @__PURE__ */ jsx(
141
+ T2,
104
142
  {
105
143
  pack: gridPack,
144
+ density,
145
+ align,
106
146
  className: className ? `lk-control-panel ${className}` : "lk-control-panel",
107
- children: [
108
- loose.map(row),
109
- resolved.sections.map((section) => /* @__PURE__ */ jsx(x3, { title: section.label, pack: gridPack, children: section.paths.map(row) }, section.label))
110
- ]
147
+ children: body(resolved.group, "")
111
148
  }
112
149
  );
113
150
  }
151
+ function sectionKey(section) {
152
+ return section.at === "" ? section.label : `${section.at}.${section.label}`;
153
+ }
114
154
  function extra(leaf, key) {
115
155
  return leaf[key];
116
156
  }
117
157
  function ControlRow({
118
158
  path,
159
+ leaf,
119
160
  resolved,
120
161
  config,
121
162
  setConfig,
122
163
  renderers,
123
164
  pack,
124
- layout,
125
- showHidden
165
+ layout
126
166
  }) {
127
- const leaf = resolved.group.children[path];
128
- if (!leaf || !("kind" in leaf)) return null;
129
- if (!isLeafVisible(resolved, path, config, showHidden)) return null;
130
167
  const write = (value2) => setConfig(path, value2);
131
168
  const fallback = extra(leaf, "default");
132
- const value = config[path] ?? fallback;
169
+ const value = valueAtPath(config, path) ?? fallback;
133
170
  const custom = renderers?.[path] ?? resolved.renderers[path] ?? renderers?.[leaf.kind];
134
171
  if (custom) return custom({ path, pref: leaf, value, setValue: write });
135
172
  const label = leaf.name;
@@ -146,7 +183,7 @@ function ControlRow({
146
183
  const suffix = extra(leaf, "suffix");
147
184
  if (extra(leaf, "control") === "slider" && min !== void 0 && max !== void 0) {
148
185
  return /* @__PURE__ */ jsx(
149
- D,
186
+ k,
150
187
  {
151
188
  label,
152
189
  value: read(),
@@ -164,7 +201,7 @@ function ControlRow({
164
201
  const lo = min ?? Number.NEGATIVE_INFINITY;
165
202
  const hi = max ?? Number.POSITIVE_INFINITY;
166
203
  return /* @__PURE__ */ jsx(
167
- M2,
204
+ P2,
168
205
  {
169
206
  label,
170
207
  value: read(),
@@ -180,18 +217,19 @@ function ControlRow({
180
217
  }
181
218
  case "boolean":
182
219
  return /* @__PURE__ */ jsx(
183
- A2,
220
+ M2,
184
221
  {
185
222
  label,
186
223
  value: read(),
187
224
  onChange: write,
225
+ layout,
188
226
  description
189
227
  }
190
228
  );
191
229
  case "enum": {
192
230
  const options = extra(leaf, "options") ?? [];
193
231
  const segmented = extra(leaf, "control") === "radio";
194
- const Row = segmented ? P2 : N2;
232
+ const Row = segmented ? I : F;
195
233
  return /* @__PURE__ */ jsx(
196
234
  Row,
197
235
  {
@@ -219,7 +257,16 @@ function ControlRow({
219
257
  }
220
258
  );
221
259
  case "color":
222
- return /* @__PURE__ */ jsx(k, { label, value: read(), onChange: write, description });
260
+ return /* @__PURE__ */ jsx(
261
+ j2,
262
+ {
263
+ label,
264
+ value: read(),
265
+ onChange: write,
266
+ layout,
267
+ description
268
+ }
269
+ );
223
270
  case "paint":
224
271
  case "object":
225
272
  return /* @__PURE__ */ jsx(UnwiredRow, { label, kind: leaf.kind, description });
@@ -236,7 +283,7 @@ function UnwiredRow({
236
283
  kind,
237
284
  description
238
285
  }) {
239
- return /* @__PURE__ */ jsx(T2, { label, description, children: /* @__PURE__ */ jsxs("span", { className: "lk-control-panel__unknown", children: [
286
+ return /* @__PURE__ */ jsx(D, { label, description, children: /* @__PURE__ */ jsxs("span", { className: "lk-control-panel__unknown", children: [
240
287
  "no control for \u201C",
241
288
  kind,
242
289
  "\u201D"
@@ -268,7 +315,7 @@ function DebouncedTextRow({
268
315
  []
269
316
  );
270
317
  return /* @__PURE__ */ jsx(
271
- j2,
318
+ N2,
272
319
  {
273
320
  label,
274
321
  layout,
@@ -292,5 +339,5 @@ function DebouncedTextRow({
292
339
  }
293
340
 
294
341
  export { ControlPanel, fromConfigFields, isLeafVisible };
295
- //# sourceMappingURL=chunk-53AZ2O2N.js.map
296
- //# sourceMappingURL=chunk-53AZ2O2N.js.map
342
+ //# sourceMappingURL=chunk-ASWKLRKJ.js.map
343
+ //# sourceMappingURL=chunk-ASWKLRKJ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/config/fromConfigField.ts","../src/config/visible.ts","../src/controls/ControlPanel.tsx"],"names":["t","Fragment","T","value","P","M","j","N"],"mappings":"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","file":"chunk-ASWKLRKJ.js","sourcesContent":["import type { PrefLeaf } from '@weasel-js/ui';\nimport type { ConfigField } from '../controls/types';\nimport type { LeafPatch, ResolvedConfig } from './types';\n\n/** One `ConfigField` as the `Pref*` vocabulary spells it. The two describe the\n * same thing; `ConfigField` just keys on the control where `PrefLeaf`\n * separates the value's kind from how it is presented. */\nfunction toLeaf(field: ConfigField): LeafPatch {\n const base = { name: field.label, description: '' };\n switch (field.type) {\n case 'slider':\n return {\n ...base,\n kind: 'number',\n control: 'slider',\n min: field.min,\n max: field.max,\n step: field.step,\n };\n case 'number':\n return {\n ...base,\n kind: 'number',\n control: 'input',\n min: field.min,\n max: field.max,\n step: field.step,\n };\n case 'checkbox':\n return { ...base, kind: 'boolean', control: 'checkbox' };\n case 'select':\n return { ...base, kind: 'enum', options: field.options };\n case 'text':\n return {\n ...base,\n kind: 'string',\n placeholder: field.placeholder,\n maxLength: field.maxLength,\n debounceMs: field.debounceMs,\n };\n case 'color':\n return { ...base, kind: 'color' };\n default:\n // A schema can arrive from outside TypeScript. Carry the kind through\n // so a lab-supplied renderer can still claim it.\n return { ...base, kind: (field as ConfigField).type };\n }\n}\n\n/**\n * Adapt a legacy `configSchema(): ConfigField[]` into the resolved shape, so\n * both instrument paths reach one renderer.\n *\n * Rules do not run here: a hand-written `ConfigField` already states its label\n * and control, which is everything the rule chain would have inferred.\n */\nexport function fromConfigFields(fields: readonly ConfigField[]): ResolvedConfig {\n const children: Record<string, PrefLeaf> = {};\n for (const field of fields) {\n const patch = toLeaf(field);\n for (const key of Object.keys(patch)) {\n if ((patch as Record<string, unknown>)[key] === undefined) {\n delete (patch as Record<string, unknown>)[key];\n }\n }\n children[field.key] = { ...patch, default: field.default } as PrefLeaf;\n }\n return {\n group: { name: '', children },\n sections: [],\n showIf: new Map(),\n renderers: {},\n };\n}\n","import { schemaNodeAtPath } from './path';\nimport type { ResolvedConfig } from './types';\n\n/**\n * Whether the row at a dotted path should be drawn right now. A group answers\n * for its whole subtree — hiding one hides everything under it.\n *\n * `hidden` is the node's own static flag; `showIf` is the schema's predicate\n * over the live config. Both are presentational — a hidden leaf keeps its\n * value and the instrument still reads it.\n */\nexport function isLeafVisible(\n resolved: ResolvedConfig,\n path: string,\n config: Record<string, unknown>,\n showHidden = false,\n): boolean {\n const node = schemaNodeAtPath(resolved.group, path);\n if (node && 'hidden' in node && node.hidden === true && !showHidden) return false;\n const predicate = resolved.showIf.get(path);\n return predicate ? predicate(config) : true;\n}\n","import { isBuiltinToolPref } from '@weasel-js/core';\nimport {\n CheckboxRow,\n ColorRow,\n isPrefLeaf,\n NumberRow,\n type PrefGroup,\n type PrefLeaf,\n type PropertyAlign,\n type PropertyDensity,\n PropertyGroup,\n PropertyList,\n type PropertyListPack,\n PropertyRow,\n type PropertyRowLayout,\n SelectRow,\n SliderRow,\n TextRow,\n ToggleRow,\n} from '@weasel-js/ui';\nimport { Fragment, type ReactNode, useEffect, useMemo, useRef, useState } from 'react';\nimport { fromConfigFields } from '../config/fromConfigField';\nimport { schemaNodeAtPath, valueAtPath } from '../config/path';\nimport type { ControlRenderer, ResolvedConfig, SectionSpec } from '../config/types';\nimport { isLeafVisible } from '../config/visible';\nimport type { ConfigField } from './types';\n\n/** How a panel packs its rows into the two-column property grid.\n * - `'auto'`: narrow controls (numbers, checkboxes, dropdowns, colours) sit\n * two per row; the ones that need the width — text, sliders, segmented\n * toggles — span it.\n * - `'pairs'`: every row pairs. A control that needs the full width says so\n * itself, with `<PropertyRow span>`.\n * - `'one-up'`: one control per row, colours excepted.\n */\nexport type ControlPack = 'auto' | 'pairs' | 'one-up';\n\nexport interface ControlPanelProps<TC extends Record<string, unknown>> {\n /** The instrument's resolved config schema. */\n schema?: ResolvedConfig;\n /** @deprecated Pass `schema`. A field list is adapted into one internally. */\n fields?: ConfigField[];\n config: TC;\n /** Writes one value. The path is dotted for a leaf inside an `f.group`, and\n * the bare key for one at the root. */\n setConfig: (path: string, value: unknown) => void;\n /**\n * Control overrides and app-defined kinds, PrefsForm-style. Keys are config\n * paths (checked first) or leaf kinds. A renderer returning `null` collapses\n * the row; entries override the built-in rows on collision.\n */\n renderers?: Record<string, ControlRenderer>;\n /** How rows pack into the two-column grid. Defaults to `'pairs'`. */\n pack?: ControlPack;\n /**\n * Where a row's label sits relative to its control. Unset takes each row's\n * own orientation — `block` (label above, which is what leaves a paired row\n * room for its value) for most, `inline` for colors and checkboxes.\n */\n layout?: PropertyRowLayout;\n /** Room the rows get. Passed straight to the property list. */\n density?: PropertyDensity;\n /** Cross-axis alignment of an inline row's label and control. */\n align?: PropertyAlign;\n /**\n * Fold each section away behind a twisty, starting `'open'` or `'closed'`.\n * Unset draws the heading alone, as the panel always has — unless a section\n * in the schema declares how it opens, which makes that section foldable on\n * its own and outranks this for that one section.\n */\n collapse?: 'open' | 'closed';\n /**\n * Which sections are folded, keyed as `onCollapse` reports them. Given, the\n * panel keeps no state of its own: every toggle arrives at `onCollapse`\n * instead, which is where a lab that remembers a trial's sections writes\n * them.\n */\n collapsed?: Readonly<Record<string, boolean>>;\n /** A fold moved. The key is a section's label — prefixed by its group's\n * dotted path when the section sits inside one — or a group's own path. */\n onCollapse?: (key: string, collapsed: boolean) => void;\n /** Draw leaves marked `hidden`. */\n showHidden?: boolean;\n className?: string;\n}\n\n/** Render an instrument's config schema as a stack of controls, each writing\n * back through `setConfig`. Built on the property rows, so a lab's controls\n * are the same aligned, themed rows the rest of the kit uses. */\nexport function ControlPanel<TC extends Record<string, unknown>>({\n schema,\n fields,\n config,\n setConfig,\n renderers,\n pack = 'pairs',\n layout,\n density,\n align,\n collapse,\n collapsed,\n onCollapse,\n showHidden = false,\n className,\n}: ControlPanelProps<TC>) {\n const resolved = useMemo(() => schema ?? fromConfigFields(fields ?? []), [schema, fields]);\n\n const gridPack: PropertyListPack = pack === 'one-up' ? 'auto-color' : 'pairs';\n // A section that declares how it opens is foldable whether or not the lab\n // asked for folds — there is nothing else for the declaration to mean.\n const folds =\n collapse !== undefined ||\n collapsed !== undefined ||\n onCollapse !== undefined ||\n resolved.sections.some((s) => s.collapsed !== undefined);\n const startsFolded = collapse === 'closed';\n\n const fold = (key: string, declared?: boolean) => ({\n pack: gridPack,\n collapsible: folds,\n defaultCollapsed: declared ?? startsFolded,\n collapsed: collapsed ? (collapsed[key] ?? declared ?? startsFolded) : undefined,\n onCollapsedChange: onCollapse ? (next: boolean) => onCollapse(key, next) : undefined,\n });\n\n /** One node, which is either a group to recurse into or a row to draw. */\n const node = (path: string): ReactNode => {\n const found = schemaNodeAtPath(resolved.group, path);\n if (!found) return null;\n if (!isLeafVisible(resolved, path, config as Record<string, unknown>, showHidden)) return null;\n if (isPrefLeaf(found)) {\n return (\n <ControlRow\n key={path}\n path={path}\n leaf={found}\n resolved={resolved}\n config={config}\n setConfig={setConfig}\n renderers={renderers}\n pack={pack}\n layout={layout}\n />\n );\n }\n // A group with no name organizes without heading it — core's rule for an\n // empty `PrefGroup.name` — so it contributes its rows and no chrome.\n if (found.name === '') return <Fragment key={path}>{body(found, path)}</Fragment>;\n return (\n <PropertyGroup key={path} title={found.name} {...fold(path)}>\n {body(found, path)}\n </PropertyGroup>\n );\n };\n\n /** One group's children: its loose nodes, then its sections. */\n const body = (group: PrefGroup, at: string): ReactNode => {\n const sections = resolved.sections.filter((s) => s.at === at);\n const sectioned = new Set(sections.flatMap((s) => s.paths));\n const paths = Object.keys(group.children).map((key) => (at === '' ? key : `${at}.${key}`));\n return (\n <>\n {paths.filter((p) => !sectioned.has(p)).map(node)}\n {sections.map((section) => (\n <PropertyGroup\n key={sectionKey(section)}\n title={section.label}\n {...fold(sectionKey(section), section.collapsed)}\n >\n {section.paths.map(node)}\n </PropertyGroup>\n ))}\n </>\n );\n };\n\n return (\n <PropertyList\n pack={gridPack}\n density={density}\n align={align}\n className={className ? `lk-control-panel ${className}` : 'lk-control-panel'}\n >\n {body(resolved.group, '')}\n </PropertyList>\n );\n}\n\n/** How a section is keyed for folding: its label at the root, and its group's\n * path in front of it anywhere else, so two groups may both have an\n * `Advanced`. */\nfunction sectionKey(section: SectionSpec): string {\n return section.at === '' ? section.label : `${section.at}.${section.label}`;\n}\n\ninterface ControlRowProps<TC extends Record<string, unknown>> {\n path: string;\n leaf: PrefLeaf;\n resolved: ResolvedConfig;\n config: TC;\n setConfig: (path: string, value: unknown) => void;\n renderers?: Record<string, ControlRenderer>;\n pack: ControlPack;\n layout?: PropertyRowLayout;\n}\n\n/** Reads a labkit-only extra off a leaf. `PrefLeaf` has no field for these,\n * and extra keys survive the resolve pass at runtime. */\nfunction extra<T>(leaf: PrefLeaf, key: string): T | undefined {\n return (leaf as unknown as Record<string, T | undefined>)[key];\n}\n\nfunction ControlRow<TC extends Record<string, unknown>>({\n path,\n leaf,\n resolved,\n config,\n setConfig,\n renderers,\n pack,\n layout,\n}: ControlRowProps<TC>) {\n const write = (value: unknown): void => setConfig(path, value);\n const fallback = extra<unknown>(leaf, 'default');\n const value = valueAtPath(config, path) ?? fallback;\n\n // Most specific wins, and within a tier the lab's entry beats the\n // instrument's: controls[path] -> node .render -> controls[kind] -> built-in.\n const custom = renderers?.[path] ?? resolved.renderers[path] ?? renderers?.[leaf.kind];\n // A custom row places itself like any other; one that needs the full width\n // says so with `<PropertyRow span>`, which is the same opt-out a built-in has.\n if (custom) return custom({ path, pref: leaf, value, setValue: write });\n\n const label = leaf.name;\n const description = leaf.description;\n const read = <T,>(): T => value as T;\n // `auto` gives the whole width to the controls that read badly at half of a\n // sidebar's: free text, a slider track, a segmented toggle. `pairs` doesn't.\n const wide = pack === 'auto';\n\n if (!isBuiltinToolPref(leaf))\n return <UnwiredRow label={label} kind={leaf.kind} description={description} />;\n\n switch (leaf.kind) {\n case 'number': {\n const min = extra<number>(leaf, 'min');\n const max = extra<number>(leaf, 'max');\n const step = extra<number>(leaf, 'step');\n const suffix = extra<string>(leaf, 'suffix');\n if (extra<string>(leaf, 'control') === 'slider' && min !== undefined && max !== undefined) {\n return (\n <SliderRow\n label={label}\n value={read<number>()}\n min={min}\n max={max}\n step={step}\n unit={suffix}\n onChange={write}\n layout={layout}\n span={wide}\n description={description}\n />\n );\n }\n // NumberRow commits every keystroke and does not clamp, so the bounds a\n // schema declares are enforced here — an instrument should never be\n // handed a config value outside the range it asked for.\n const lo = min ?? Number.NEGATIVE_INFINITY;\n const hi = max ?? Number.POSITIVE_INFINITY;\n return (\n <NumberRow\n label={label}\n value={read<number>()}\n min={min}\n max={max}\n step={step}\n unit={suffix}\n onChange={(n) => write(Math.min(hi, Math.max(lo, n)))}\n layout={layout}\n description={description}\n />\n );\n }\n case 'boolean':\n return (\n <CheckboxRow\n label={label}\n value={read<boolean>()}\n onChange={write}\n layout={layout}\n description={description}\n />\n );\n case 'enum': {\n const options = extra<readonly { value: string; label: string }[]>(leaf, 'options') ?? [];\n const segmented = extra<string>(leaf, 'control') === 'radio';\n const Row = segmented ? ToggleRow : SelectRow;\n return (\n <Row\n label={label}\n value={read<string>()}\n options={options}\n onChange={write}\n layout={layout}\n span={segmented && wide}\n description={description}\n />\n );\n }\n case 'string':\n return (\n <DebouncedTextRow\n leaf={leaf}\n label={label}\n value={read<string>()}\n write={write}\n layout={layout}\n span={wide}\n description={description}\n />\n );\n case 'color':\n return (\n <ColorRow\n label={label}\n value={read<string>()}\n onChange={write}\n layout={layout}\n description={description}\n />\n );\n case 'paint':\n case 'object':\n // Declined: a hex swatch would write a solid over a gradient, and a flat\n // row would write one field into a half-built object. Override with\n // `render` to edit either.\n return <UnwiredRow label={label} kind={leaf.kind} description={description} />;\n default: {\n // Not reachable while every built-in kind has an arm — and a new kind\n // that lacks one is a compile error here, never a blank row.\n const _exhaustive: never = leaf;\n throw new Error(\n `ControlPanel: no control for built-in pref kind \"${(_exhaustive as { kind: string }).kind}\"`,\n );\n }\n }\n}\n\n/** A leaf this panel has no control for is named rather than dropped: a silent\n * gap reads as \"this control does not exist\". */\nfunction UnwiredRow({\n label,\n kind,\n description,\n}: {\n label: string;\n kind: string;\n description?: string;\n}) {\n return (\n <PropertyRow label={label} description={description}>\n <span className=\"lk-control-panel__unknown\">no control for “{kind}”</span>\n </PropertyRow>\n );\n}\n\n/** Text writes are debounced so typing does not re-run the instrument on every\n * keystroke, which means the row is locally controlled between commits and has\n * to notice when the config changes underneath it. */\nfunction DebouncedTextRow({\n leaf,\n label,\n value,\n write,\n layout,\n span,\n description,\n}: {\n leaf: PrefLeaf;\n label: string;\n value: string;\n write: (value: unknown) => void;\n layout?: PropertyRowLayout;\n span?: boolean;\n description?: string;\n}) {\n const debounceMs = extra<number>(leaf, 'debounceMs') ?? 150;\n const [local, setLocal] = useState(value);\n const timer = useRef<ReturnType<typeof setTimeout> | null>(null);\n const lastExternal = useRef(value);\n\n useEffect(() => {\n if (value !== lastExternal.current) {\n lastExternal.current = value;\n setLocal(value);\n }\n }, [value]);\n\n useEffect(\n () => () => {\n if (timer.current) clearTimeout(timer.current);\n },\n [],\n );\n\n return (\n <TextRow\n label={label}\n layout={layout}\n span={span}\n description={description}\n value={local}\n placeholder={extra<string>(leaf, 'placeholder')}\n maxLength={extra<number>(leaf, 'maxLength')}\n onChange={(next) => {\n setLocal(next);\n if (timer.current) clearTimeout(timer.current);\n const commit = () => {\n lastExternal.current = next;\n write(next);\n };\n if (debounceMs === 0) commit();\n else timer.current = setTimeout(commit, debounceMs);\n }}\n />\n );\n}\n"]}