@weasel-js/labkit 1.6.0 → 1.7.0

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 (222) hide show
  1. package/dist/_dts/{CanvasStackContext-DQgcH1ny.d.ts → CanvasStackContext-D2jnIwfk.d.ts} +1 -1
  2. package/dist/_dts/FloatingPanel-7j0IcXwz.d.ts +30 -0
  3. package/dist/_dts/{LayerList.d-2c9yup5Y.d.ts → LayerList.d-DiuEArKq.d.ts} +4 -4
  4. package/dist/_dts/PrefsRow.d-TOZ7BgD9.d.ts +79 -0
  5. package/dist/_dts/PropertyPanel.d-D-1paCbE.d.ts +141 -0
  6. package/dist/_dts/{Select.d-BchGMC2Y.d.ts → Select.d-Bensr_MT.d.ts} +15 -6
  7. package/dist/_dts/Stage-CCWfQQWM.d.ts +202 -0
  8. package/dist/_dts/index-B2Yj5aA0.d.ts +274 -0
  9. package/dist/_dts/{index-B0--8PEM.d.ts → index-CsYqUHD3.d.ts} +12 -13
  10. package/dist/_dts/{types-B7y8hX1D.d.ts → types-CRPSGFAp.d.ts} +33 -5
  11. package/dist/_dts/{frac-DTRq7Xpb.d.ts → types-CSm9jgol.d.ts} +700 -650
  12. package/dist/_dts/{useTrialState-CP4jfuKl.d.ts → useTrialState-CTZPgeZ7.d.ts} +2 -3
  13. package/dist/canvas/index.d.ts +40 -8
  14. package/dist/canvas/index.js +5 -3
  15. package/dist/chrome/index.d.ts +5 -6
  16. package/dist/chrome/index.js +9 -7
  17. package/dist/chunk-24W5VEJW.js +18 -0
  18. package/dist/chunk-24W5VEJW.js.map +1 -0
  19. package/dist/chunk-275JRRVD.js +728 -0
  20. package/dist/chunk-275JRRVD.js.map +1 -0
  21. package/dist/chunk-3RY6WL5T.js +38 -0
  22. package/dist/chunk-3RY6WL5T.js.map +1 -0
  23. package/dist/chunk-4LJSC7MB.js +143 -0
  24. package/dist/chunk-4LJSC7MB.js.map +1 -0
  25. package/dist/{chunk-3FI7Y4R4.js → chunk-7BO7HNGF.js} +5 -5
  26. package/dist/{chunk-3FI7Y4R4.js.map → chunk-7BO7HNGF.js.map} +1 -1
  27. package/dist/chunk-AWQG7FCC.js +19 -0
  28. package/dist/chunk-AWQG7FCC.js.map +1 -0
  29. package/dist/chunk-CZZPXUOU.js +3 -0
  30. package/dist/{chunk-SBXNQW4G.js.map → chunk-CZZPXUOU.js.map} +1 -1
  31. package/dist/{chunk-ZORVNOKX.js → chunk-FMK7UQ6V.js} +24 -145
  32. package/dist/chunk-FMK7UQ6V.js.map +1 -0
  33. package/dist/{chunk-BVL7Y574.js → chunk-FZ7YFQN7.js} +26 -10
  34. package/dist/chunk-FZ7YFQN7.js.map +1 -0
  35. package/dist/{chunk-USO6TERP.js → chunk-GOZLNM4L.js} +11 -9
  36. package/dist/chunk-GOZLNM4L.js.map +1 -0
  37. package/dist/{chunk-H5VIV7MQ.js → chunk-I5DUQ5HM.js} +88 -82
  38. package/dist/chunk-I5DUQ5HM.js.map +1 -0
  39. package/dist/chunk-J7OWWTCW.js +143 -0
  40. package/dist/chunk-J7OWWTCW.js.map +1 -0
  41. package/dist/{chunk-H6ZAOWNE.js → chunk-JVBYV6TL.js} +3 -3
  42. package/dist/{chunk-H6ZAOWNE.js.map → chunk-JVBYV6TL.js.map} +1 -1
  43. package/dist/{chunk-VQ47KWD5.js → chunk-KMJ7RWD3.js} +18 -7
  44. package/dist/chunk-KMJ7RWD3.js.map +1 -0
  45. package/dist/{chunk-ERFMC6WU.js → chunk-M7HP3FTK.js} +20 -11
  46. package/dist/chunk-M7HP3FTK.js.map +1 -0
  47. package/dist/chunk-MIM5R4ZT.js +14972 -0
  48. package/dist/chunk-MIM5R4ZT.js.map +1 -0
  49. package/dist/{chunk-HFRE4QQV.js → chunk-T5IBSPAM.js} +96 -32
  50. package/dist/chunk-T5IBSPAM.js.map +1 -0
  51. package/dist/chunk-UNAZSLWE.js +294 -0
  52. package/dist/chunk-UNAZSLWE.js.map +1 -0
  53. package/dist/{chunk-RS3HRQWU.js → chunk-WPLAANH4.js} +4 -3
  54. package/dist/chunk-WPLAANH4.js.map +1 -0
  55. package/dist/{chunk-AWH3SRPW.js → chunk-Y23G2VZL.js} +5 -37
  56. package/dist/chunk-Y23G2VZL.js.map +1 -0
  57. package/dist/{chunk-MOLIUNVH.js → chunk-YT75VYRD.js} +4 -4
  58. package/dist/{chunk-MOLIUNVH.js.map → chunk-YT75VYRD.js.map} +1 -1
  59. package/dist/chunk-ZIFPHIWZ.js +302 -0
  60. package/dist/chunk-ZIFPHIWZ.js.map +1 -0
  61. package/dist/config/index.d.ts +48 -40
  62. package/dist/config/index.js +4 -5
  63. package/dist/controls/index.d.ts +91 -7
  64. package/dist/controls/index.js +5 -4
  65. package/dist/dragdrop/index.d.ts +9 -7
  66. package/dist/dragdrop/index.js +3 -2
  67. package/dist/index.d.ts +294 -82
  68. package/dist/index.js +425 -402
  69. package/dist/index.js.map +1 -1
  70. package/dist/layers/index.d.ts +4 -5
  71. package/dist/layers/index.js +2 -1
  72. package/dist/loupe/index.d.ts +20 -17
  73. package/dist/loupe/index.js +5 -3
  74. package/dist/overview/index.d.ts +56 -0
  75. package/dist/overview/index.js +289 -0
  76. package/dist/overview/index.js.map +1 -0
  77. package/dist/passthrough/weasel-ui.d.ts +239 -69
  78. package/dist/passthrough/weasel-ui.js +3 -2
  79. package/dist/primitives/index.d.ts +5 -204
  80. package/dist/primitives/index.js +6 -4
  81. package/dist/state/index.d.ts +8 -8
  82. package/dist/state/index.js +4 -5
  83. package/dist/state/index.js.map +1 -1
  84. package/dist/styles.css +152 -82
  85. package/dist/undo/index.d.ts +4 -4
  86. package/package.json +17 -12
  87. package/src/annotations/AnnotationOverlay.tsx +2 -2
  88. package/src/annotations/Annotations.less +0 -10
  89. package/src/annotations/MarkList.tsx +4 -8
  90. package/src/annotations/index.ts +1 -0
  91. package/src/annotations/paint.test.ts +3 -14
  92. package/src/annotations/paint.ts +3 -19
  93. package/src/annotations/store.test.ts +26 -0
  94. package/src/annotations/store.ts +25 -6
  95. package/src/annotations/svgNodes.test.ts +6 -9
  96. package/src/annotations/svgNodes.ts +3 -3
  97. package/src/annotations/toolMap.test.ts +37 -2
  98. package/src/annotations/toolMap.ts +13 -0
  99. package/src/annotations/types.ts +27 -0
  100. package/src/canvas/AGENTS.md +9 -10
  101. package/src/canvas/CameraInput.test.tsx +136 -0
  102. package/src/canvas/CameraInput.tsx +322 -0
  103. package/src/canvas/CanvasStack.tsx +47 -58
  104. package/src/canvas/LinkedCursor.less +16 -0
  105. package/src/canvas/LinkedCursor.tsx +54 -0
  106. package/src/canvas/Stage.test.tsx +33 -4
  107. package/src/canvas/Stage.tsx +47 -37
  108. package/src/canvas/cameraRegistry.test.ts +55 -0
  109. package/src/canvas/cameraRegistry.ts +86 -0
  110. package/src/canvas/cameraView.test.ts +60 -0
  111. package/src/canvas/cameraView.ts +61 -0
  112. package/src/canvas/cameraZoom.test.ts +46 -0
  113. package/src/canvas/cameraZoom.ts +24 -0
  114. package/src/canvas/index.ts +23 -2
  115. package/src/canvas/sharedScope.test.tsx +94 -0
  116. package/src/chrome/ChromeRegions.stories.tsx +1 -0
  117. package/src/chrome/regions/PaletteRegion.less +0 -7
  118. package/src/chrome/regions/SidebarRegion.less +12 -2
  119. package/src/chrome/regions/SidebarRegion.test.tsx +9 -2
  120. package/src/config/builder.test.ts +2 -1
  121. package/src/config/builder.ts +15 -8
  122. package/src/config/entry.test.ts +3 -1
  123. package/src/config/fromConfigField.test.ts +14 -0
  124. package/src/config/fromConfigField.ts +19 -4
  125. package/src/config/index.ts +4 -1
  126. package/src/config/path.ts +2 -5
  127. package/src/config/resolve.ts +15 -1
  128. package/src/config/sectionTree.test.ts +12 -6
  129. package/src/config/sectionTree.ts +15 -5
  130. package/src/config/types.ts +34 -9
  131. package/src/controls/ControlPanel.less +1 -1
  132. package/src/controls/ControlPanel.stories.tsx +3 -2
  133. package/src/controls/ControlPanel.test.tsx +76 -13
  134. package/src/controls/ControlPanel.tsx +80 -11
  135. package/src/controls/inDialog.test.tsx +18 -0
  136. package/src/controls/inDialog.tsx +2 -10
  137. package/src/index.test.ts +1 -1
  138. package/src/index.ts +23 -5
  139. package/src/instrument/SineWave.smoke.test.tsx +2 -0
  140. package/src/instrument/types.ts +10 -1
  141. package/src/lab/Lab.nebula.test.tsx +11 -2
  142. package/src/lab/Lab.test.tsx +3 -1
  143. package/src/lab/Lab.tsx +87 -76
  144. package/src/lab/{LabFit.stories.less → LabFit.browser.test.less} +1 -1
  145. package/src/lab/{LabFit.stories.tsx → LabFit.browser.test.tsx} +41 -63
  146. package/src/lab/LabFullChrome.stories.tsx +1 -0
  147. package/src/lab/LabHeader.test.tsx +24 -14
  148. package/src/lab/LabHeader.tsx +11 -12
  149. package/src/lab/LabShell.less +8 -17
  150. package/src/lab/LabSwitcher.less +6 -6
  151. package/src/lab/LabZoom.test.tsx +223 -0
  152. package/src/lab/LabZoom.tsx +170 -0
  153. package/src/lab/index.ts +1 -0
  154. package/src/loupe/AGENTS.md +7 -6
  155. package/src/loupe/LoupeGestures.tsx +15 -10
  156. package/src/loupe/TrialLoupe.tsx +5 -5
  157. package/src/loupe/index.ts +1 -0
  158. package/src/loupe/loupeActions.ts +6 -2
  159. package/src/overview/AGENTS.md +38 -0
  160. package/src/overview/OverviewMarks.test.tsx +100 -0
  161. package/src/overview/OverviewMarks.tsx +93 -0
  162. package/src/overview/TrialOverview.less +53 -0
  163. package/src/overview/TrialOverview.stories.tsx +50 -0
  164. package/src/overview/TrialOverview.test.tsx +116 -0
  165. package/src/overview/TrialOverview.tsx +376 -0
  166. package/src/overview/index.ts +2 -0
  167. package/src/passthrough/weasel-ui.test.ts +2 -0
  168. package/src/passthrough/weasel-ui.ts +23 -4
  169. package/src/primitives/FloatingPanel.test.tsx +11 -0
  170. package/src/primitives/FloatingPanel.tsx +2 -5
  171. package/src/primitives/JobProgress.less +1 -1
  172. package/src/primitives/Legend.stories.tsx +1 -0
  173. package/src/primitives/Readout.browser.test.tsx +137 -0
  174. package/src/primitives/Readout.less +12 -0
  175. package/src/primitives/Readout.stories.tsx +58 -0
  176. package/src/primitives/Readout.test.tsx +93 -0
  177. package/src/primitives/Readout.tsx +56 -0
  178. package/src/primitives/index.ts +2 -0
  179. package/src/specimen/Specimen.stories.tsx +1 -0
  180. package/src/specimen/Specimen.tsx +21 -4
  181. package/src/state/Persistence.browser.test.tsx +47 -0
  182. package/src/state/labRecords.test.ts +4 -1
  183. package/src/state/store.test.ts +9 -5
  184. package/src/state/usePersistedState.test.tsx +1 -1
  185. package/src/state/useTrialState.test.tsx +1 -1
  186. package/src/styles.less +3 -0
  187. package/src/theme/Interstellar.stories.tsx +4 -6
  188. package/src/theme/base.less +21 -47
  189. package/src/theme/interstellar.test.ts +1 -1
  190. package/src/theme/interstellar.theme.json +10 -3
  191. package/src/tools/labTool.ts +6 -2
  192. package/src/trial/Trial.annotations.test.tsx +16 -0
  193. package/src/trial/Trial.less +0 -13
  194. package/src/trial/Trial.loupe.test.tsx +73 -1
  195. package/src/trial/Trial.stories.tsx +2 -0
  196. package/src/trial/Trial.tsx +23 -1
  197. package/src/trial/TrialChrome.tsx +21 -3
  198. package/src/trial/trialOps.test.ts +4 -4
  199. package/dist/_dts/PrefsRow.d-DFpPIeIC.d.ts +0 -39
  200. package/dist/_dts/inDialog-D5ZKGjiD.d.ts +0 -23
  201. package/dist/_dts/index-DINmCuLA.d.ts +0 -453
  202. package/dist/_dts/number.d-OeQYQoUO.d.ts +0 -28
  203. package/dist/_dts/usePanZoom-Bq73pwFe.d.ts +0 -117
  204. package/dist/chunk-2AYGEN57.js +0 -32
  205. package/dist/chunk-2AYGEN57.js.map +0 -1
  206. package/dist/chunk-AWH3SRPW.js.map +0 -1
  207. package/dist/chunk-BVL7Y574.js.map +0 -1
  208. package/dist/chunk-ERFMC6WU.js.map +0 -1
  209. package/dist/chunk-H5VIV7MQ.js.map +0 -1
  210. package/dist/chunk-HFRE4QQV.js.map +0 -1
  211. package/dist/chunk-RS3HRQWU.js.map +0 -1
  212. package/dist/chunk-SBXNQW4G.js +0 -3
  213. package/dist/chunk-TT5C5S6R.js +0 -368
  214. package/dist/chunk-TT5C5S6R.js.map +0 -1
  215. package/dist/chunk-USO6TERP.js.map +0 -1
  216. package/dist/chunk-VQ47KWD5.js.map +0 -1
  217. package/dist/chunk-WUJXJC5O.js +0 -14568
  218. package/dist/chunk-WUJXJC5O.js.map +0 -1
  219. package/dist/chunk-ZORVNOKX.js.map +0 -1
  220. package/src/canvas/usePanZoom.test.ts +0 -244
  221. package/src/canvas/usePanZoom.ts +0 -147
  222. package/src/state/Persistence.stories.tsx +0 -75
@@ -1,10 +1,35 @@
1
- import { fromConfigFields, isLeafVisible } from './chunk-VQ47KWD5.js';
2
- import { resolveAutoConfig, schemaNodeAtPath, valueAtPath, summarizeValue, auto } from './chunk-AWH3SRPW.js';
3
- import { oe2, ae, b2, s4, L3, v3, s2, B, V, m3 } from './chunk-WUJXJC5O.js';
1
+ import { fromConfigFields, isLeafVisible } from './chunk-KMJ7RWD3.js';
2
+ import { resolveAutoConfig, schemaNodeAtPath, valueAtPath, auto } from './chunk-Y23G2VZL.js';
3
+ import { v2, ie2, re2, b2, s3, I, c2, R, c, B, z2, m2 } from './chunk-MIM5R4ZT.js';
4
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
5
  import { isBuiltinToolPref, toHex8, withAlpha01, getAlpha01 } from '@weasel-js/core';
5
6
  import { useMemo, useState, Fragment as Fragment$1, useRef, useEffect } from 'react';
6
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
7
7
 
8
+ function summarizeValue(value) {
9
+ if (Array.isArray(value)) return value.length === 0 ? "None" : value.join(", ");
10
+ if (value === void 0 || value === null || value === "") return "None";
11
+ return typeof value === "object" ? JSON.stringify(value) : String(value);
12
+ }
13
+ function inDialog(body, opts = {}) {
14
+ const summary = opts.summary ?? summarizeValue;
15
+ return (ctx) => {
16
+ const { pref, value, auto: auto2, setAuto } = ctx;
17
+ const manual = pref.manual === true;
18
+ return /* @__PURE__ */ jsx(
19
+ v2,
20
+ {
21
+ label: pref.name,
22
+ description: pref.description,
23
+ summary: summary(value),
24
+ title: opts.title,
25
+ readout: auto2 ? "auto" : void 0,
26
+ auto: auto2,
27
+ onAutoChange: manual ? void 0 : setAuto,
28
+ children: () => body(ctx)
29
+ }
30
+ );
31
+ };
32
+ }
8
33
  var gridOf = (pack) => pack === "one-up" ? "auto-color" : "pairs";
9
34
  function sectionRows(section, rows) {
10
35
  return {
@@ -14,6 +39,35 @@ function sectionRows(section, rows) {
14
39
  };
15
40
  }
16
41
  var NO_AUTO = /* @__PURE__ */ new Set();
42
+ function leavesUnder(group, at) {
43
+ return Object.entries(group.children).flatMap(([key, child]) => {
44
+ const path = at === "" ? key : `${at}.${key}`;
45
+ return s3(child) ? [[path, child.default]] : leavesUnder(child, path);
46
+ });
47
+ }
48
+ function ResetGroup({
49
+ group,
50
+ path,
51
+ config,
52
+ setConfig
53
+ }) {
54
+ const changed = leavesUnder(group, path).filter(([p, d]) => !Object.is(valueAtPath(config, p), d));
55
+ return /* @__PURE__ */ jsx(
56
+ c,
57
+ {
58
+ iconOnly: true,
59
+ variant: "ghost",
60
+ size: "sm",
61
+ ariaLabel: `Reset ${group.name}`,
62
+ tooltip: `Reset ${group.name} to defaults`,
63
+ disabled: changed.length === 0,
64
+ onClick: () => {
65
+ for (const [p, d] of changed) setConfig(p, d);
66
+ },
67
+ children: /* @__PURE__ */ jsx(B, {})
68
+ }
69
+ );
70
+ }
17
71
  function ControlPanel({
18
72
  schema,
19
73
  fields,
@@ -55,7 +109,7 @@ function ControlPanel({
55
109
  [resolved, config, auto2]
56
110
  );
57
111
  const gridPack = gridOf(pack);
58
- const folds = collapse !== void 0 || collapsed !== void 0 || onCollapse !== void 0 || resolved.sections.some((s3) => s3.collapsed !== void 0);
112
+ const folds = collapse !== void 0 || collapsed !== void 0 || onCollapse !== void 0 || resolved.sections.some((s2) => s2.collapsed !== void 0);
59
113
  const startsFolded = collapse === "closed";
60
114
  const fold = (key, declared, grid = gridPack) => ({
61
115
  pack: grid,
@@ -68,7 +122,7 @@ function ControlPanel({
68
122
  const found = schemaNodeAtPath(resolved.group, path);
69
123
  if (!found) return null;
70
124
  if (!isLeafVisible(resolved, path, config, showHidden)) return null;
71
- if (s4(found)) {
125
+ if (s3(found)) {
72
126
  return /* @__PURE__ */ jsx(
73
127
  ControlRow,
74
128
  {
@@ -94,6 +148,7 @@ function ControlPanel({
94
148
  title: found.name,
95
149
  description: found.description,
96
150
  span: true,
151
+ actions: resolved.resettable?.has(path) ? /* @__PURE__ */ jsx(ResetGroup, { group: found, path, config, setConfig }) : void 0,
97
152
  ...fold(path, void 0, rows.grid),
98
153
  children: body(found, path, rows)
99
154
  },
@@ -102,12 +157,13 @@ function ControlPanel({
102
157
  };
103
158
  const pairable = (path) => {
104
159
  const found = schemaNodeAtPath(resolved.group, path);
105
- if (!found || !s4(found)) return void 0;
160
+ if (!found || !s3(found)) return void 0;
106
161
  const pair = extra(found, "pair");
107
162
  if (pair === void 0) return void 0;
108
163
  if (!isLeafVisible(resolved, path, config, showHidden))
109
164
  return void 0;
110
- if (renderers?.[path] ?? resolved.renderers[path] ?? renderers?.[found.kind]) return void 0;
165
+ if (renderers?.[path] ?? resolved.renderers[path] ?? resolved.dialogs[path] ?? renderers?.[found.kind])
166
+ return void 0;
111
167
  if (!isBuiltinToolPref(found)) return void 0;
112
168
  if (found.kind === "paint" || found.kind === "object" || isSliderLeaf(found)) return void 0;
113
169
  return { leaf: found, pair };
@@ -148,8 +204,8 @@ function ControlPanel({
148
204
  return out;
149
205
  };
150
206
  const body = (group, at, rows) => {
151
- const sections = resolved.sections.filter((s3) => s3.at === at);
152
- const sectioned = new Set(sections.flatMap((s3) => s3.paths));
207
+ const sections = resolved.sections.filter((s2) => s2.at === at);
208
+ const sectioned = new Set(sections.flatMap((s2) => s2.paths));
153
209
  const paths = Object.keys(group.children).map((key) => at === "" ? key : `${at}.${key}`);
154
210
  return /* @__PURE__ */ jsxs(Fragment, { children: [
155
211
  rowsFor(
@@ -171,7 +227,7 @@ function ControlPanel({
171
227
  ] });
172
228
  };
173
229
  const list = /* @__PURE__ */ jsx(
174
- oe2,
230
+ ie2,
175
231
  {
176
232
  pack: gridPack,
177
233
  density,
@@ -181,7 +237,7 @@ function ControlPanel({
181
237
  }
182
238
  );
183
239
  if (title === void 0 && stance === void 0 && tone === void 0) return list;
184
- return /* @__PURE__ */ jsx(ae, { title, stance, tone, density, children: list });
240
+ return /* @__PURE__ */ jsx(re2, { title, stance, tone, density, children: list });
185
241
  }
186
242
  function sectionKey(section) {
187
243
  return section.at === "" ? section.label : `${section.at}.${section.label}`;
@@ -216,7 +272,8 @@ function ControlRow({
216
272
  const onAutoChange = canAuto ? setAuto : void 0;
217
273
  const autoReadout = isAutoRow ? "auto" : void 0;
218
274
  const autoProps = { auto: isAutoRow, onAutoChange };
219
- const custom = renderers?.[path] ?? resolved.renderers[path] ?? renderers?.[leaf.kind];
275
+ const dialog = resolved.dialogs[path];
276
+ const custom = renderers?.[path] ?? resolved.renderers[path] ?? (dialog ? inDialog(dialog.body, dialog) : void 0) ?? renderers?.[leaf.kind];
220
277
  if (custom) return custom({ path, pref: leaf, value, setValue: write, auto: isAutoRow, setAuto });
221
278
  const label = leaf.name;
222
279
  const description = leaf.description;
@@ -225,7 +282,7 @@ function ControlRow({
225
282
  if (leaf.kind === "list") {
226
283
  const entries = Array.isArray(value) ? value : [];
227
284
  return /* @__PURE__ */ jsx(
228
- v3,
285
+ v2,
229
286
  {
230
287
  label,
231
288
  summary: summarizeValue(entries),
@@ -234,7 +291,7 @@ function ControlRow({
234
291
  description,
235
292
  ...autoProps,
236
293
  children: () => /* @__PURE__ */ jsx(
237
- s2,
294
+ c2,
238
295
  {
239
296
  "aria-label": label,
240
297
  value: entries,
@@ -246,21 +303,22 @@ function ControlRow({
246
303
  );
247
304
  }
248
305
  const field = labField(leaf, value, write);
249
- if (field === null) return /* @__PURE__ */ jsx(UnwiredRow, { label, kind: leaf.kind, description });
306
+ if (field === null)
307
+ return /* @__PURE__ */ jsx(UnwiredRow, { label, kind: leaf.kind, description });
250
308
  switch (field.kind) {
251
309
  case "string":
252
310
  return /* @__PURE__ */ jsx(DebouncedTextRow, { leaf, field, span: wide, ...row });
253
311
  case "number":
254
- return /* @__PURE__ */ jsx(B, { ...field, ...row, span: field.control === "slider" && wide });
312
+ return /* @__PURE__ */ jsx(R, { ...field, ...row, span: field.control === "slider" && wide });
255
313
  case "enum":
256
- return /* @__PURE__ */ jsx(B, { ...field, ...row, span: field.control === "radio" && wide });
314
+ return /* @__PURE__ */ jsx(R, { ...field, ...row, span: field.control === "toggle" && wide });
257
315
  default:
258
- return /* @__PURE__ */ jsx(B, { ...field, ...row });
316
+ return /* @__PURE__ */ jsx(R, { ...field, ...row });
259
317
  }
260
318
  }
261
319
  function labField(leaf, value, write) {
262
320
  if (!isBuiltinToolPref(leaf) || leaf.kind === "paint" || leaf.kind === "object") return null;
263
- const field = m3(leaf, { value, setValue: write });
321
+ const field = m2(leaf, { value, setValue: write });
264
322
  if (field === null) return null;
265
323
  switch (field.kind) {
266
324
  case "number":
@@ -270,9 +328,15 @@ function labField(leaf, value, write) {
270
328
  unit: extra(leaf, "suffix") ?? field.unit
271
329
  };
272
330
  case "boolean":
273
- return { ...field, control: extra(leaf, "control") === "switch" ? "switch" : "checkbox" };
331
+ return {
332
+ ...field,
333
+ control: extra(leaf, "control") === "switch" ? "switch" : "checkbox"
334
+ };
274
335
  case "enum":
275
- return { ...field, control: extra(leaf, "control") === "radio" ? "radio" : "select" };
336
+ return {
337
+ ...field,
338
+ control: extra(leaf, "control") === "radio" ? "toggle" : "select"
339
+ };
276
340
  case "string":
277
341
  return {
278
342
  ...field,
@@ -288,7 +352,7 @@ function UnwiredRow({
288
352
  kind,
289
353
  description
290
354
  }) {
291
- return /* @__PURE__ */ jsx(L3, { label, description, children: /* @__PURE__ */ jsxs("span", { className: "lk-control-panel__unknown", children: [
355
+ return /* @__PURE__ */ jsx(I, { label, description, children: /* @__PURE__ */ jsxs("span", { className: "lk-control-panel__unknown", children: [
292
356
  "no control for \u201C",
293
357
  kind,
294
358
  "\u201D"
@@ -304,7 +368,7 @@ function DebouncedTextRow({
304
368
  field.onChange,
305
369
  extra(leaf, "debounceMs") ?? 150
306
370
  );
307
- return /* @__PURE__ */ jsx(B, { ...field, ...row, value: text.local, onChange: text.type });
371
+ return /* @__PURE__ */ jsx(R, { ...field, ...row, value: text.local, onChange: text.type });
308
372
  }
309
373
  function PairedRow({
310
374
  label,
@@ -314,7 +378,7 @@ function PairedRow({
314
378
  layout,
315
379
  span
316
380
  }) {
317
- return /* @__PURE__ */ jsx(L3, { group: true, span, label, layout, children: cells.map(({ path, leaf }) => (
381
+ return /* @__PURE__ */ jsx(I, { group: true, span, label, layout, children: cells.map(({ path, leaf }) => (
318
382
  // The row is named by the pair, so a cell that shares it says which
319
383
  // knob it is — without a caption a paired switch reads only to a
320
384
  // screen reader. A cell whose leaf is deliberately unnamed keeps none.
@@ -345,13 +409,13 @@ function PairCell({
345
409
  if (field === null) return null;
346
410
  switch (field.kind) {
347
411
  case "string":
348
- return /* @__PURE__ */ jsx(V, { ...field, name: leaf.name, value: text.local, onChange: text.type });
412
+ return /* @__PURE__ */ jsx(z2, { ...field, name: leaf.name, value: text.local, onChange: text.type });
349
413
  case "number":
350
- return /* @__PURE__ */ jsx(V, { ...field, name: leaf.name, control: "input", unit: void 0 });
414
+ return /* @__PURE__ */ jsx(z2, { ...field, name: leaf.name, control: "input", unit: void 0 });
351
415
  case "color": {
352
416
  const stored = typeof value === "string" ? toHex8(value) : "#000000";
353
417
  return /* @__PURE__ */ jsx(
354
- V,
418
+ z2,
355
419
  {
356
420
  ...field,
357
421
  name: leaf.name,
@@ -361,7 +425,7 @@ function PairCell({
361
425
  );
362
426
  }
363
427
  default:
364
- return /* @__PURE__ */ jsx(V, { ...field, name: leaf.name });
428
+ return /* @__PURE__ */ jsx(z2, { ...field, name: leaf.name });
365
429
  }
366
430
  }
367
431
  function useDebouncedText(value, write, debounceMs) {
@@ -395,6 +459,6 @@ function useDebouncedText(value, write, debounceMs) {
395
459
  };
396
460
  }
397
461
 
398
- export { ControlPanel };
399
- //# sourceMappingURL=chunk-HFRE4QQV.js.map
400
- //# sourceMappingURL=chunk-HFRE4QQV.js.map
462
+ export { ControlPanel, inDialog, summarizeValue };
463
+ //# sourceMappingURL=chunk-T5IBSPAM.js.map
464
+ //# sourceMappingURL=chunk-T5IBSPAM.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/controls/inDialog.tsx","../src/controls/ControlPanel.tsx"],"names":["auto","v","s","jsx","Fragment","b","ie","re","value","c","m","z"],"mappings":"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","file":"chunk-T5IBSPAM.js","sourcesContent":["import { DialogRow } from '@weasel-js/ui';\nimport type { ControlRenderer, InDialogOptions } from '../config/types';\n\nexport type { InDialogOptions };\n\n/** The one-line summary a dialog row shows when it is given none. */\nexport function summarizeValue(value: unknown): string {\n if (Array.isArray(value)) return value.length === 0 ? 'None' : value.join(', ');\n if (value === undefined || value === null || value === '') return 'None';\n return typeof value === 'object' ? JSON.stringify(value) : String(value);\n}\n\n/**\n * Moves a control into a modal: the row becomes a button showing a summary of\n * the value, and the button opens a dialog whose body is `body`, handed the\n * same context a row renderer gets. For a leaf whose editor will never fit a\n * sidebar row.\n */\nexport function inDialog(body: ControlRenderer, opts: InDialogOptions = {}): ControlRenderer {\n const summary = opts.summary ?? summarizeValue;\n return (ctx) => {\n const { pref, value, auto, setAuto } = ctx;\n const manual = (pref as { manual?: boolean }).manual === true;\n return (\n <DialogRow\n label={pref.name}\n description={pref.description}\n summary={summary(value)}\n title={opts.title}\n readout={auto ? 'auto' : undefined}\n auto={auto}\n onAutoChange={manual ? undefined : setAuto}\n >\n {() => body(ctx)}\n </DialogRow>\n );\n };\n}\n","import { getAlpha01, isBuiltinToolPref, toHex8, withAlpha01 } from '@weasel-js/core';\nimport {\n Button,\n DialogRow,\n isPrefLeaf,\n ListEditor,\n type PrefGroup,\n type PrefLeaf,\n type PropertyAlign,\n PropertyControl,\n type PropertyControlProps,\n type PropertyDensity,\n PropertyField,\n PropertyGroup,\n PropertyList,\n type PropertyListPack,\n PropertyPanel,\n PropertyRow,\n type PropertyRowLayout,\n prefFieldProps,\n ResetIcon,\n type StanceProps,\n} from '@weasel-js/ui';\nimport { Fragment, type ReactNode, useEffect, useMemo, useRef, useState } from 'react';\nimport { auto as autoValue } from '../config/auto';\nimport { resolveAutoConfig } from '../config/autoConfig';\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 { inDialog, summarizeValue } from './inDialog';\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, colors) 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, colors excepted.\n */\nexport type ControlPack = 'auto' | 'pairs' | 'one-up';\n\n/** How the rows under one heading are laid out: the panel's, or a section's own. */\ninterface Rows {\n pack: ControlPack;\n layout: PropertyRowLayout | undefined;\n grid: PropertyListPack;\n}\n\nconst gridOf = (pack: ControlPack): PropertyListPack =>\n pack === 'one-up' ? 'auto-color' : 'pairs';\n\nfunction sectionRows(section: SectionSpec, rows: Rows): Rows {\n return {\n pack: section.pack ?? rows.pack,\n layout: section.layout ?? rows.layout,\n grid: section.pack ? gridOf(section.pack) : rows.grid,\n };\n}\n\nexport interface ControlPanelProps<TC extends Record<string, unknown>> extends StanceProps {\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 /** Dotted paths currently unpinned. A row in this set hides its control and\n * reads out `auto`, and clicking its label writes the sentinel back through\n * `setConfig`. Omitted, the panel keeps the set itself: the labels still\n * toggle and the sentinel never reaches `setConfig`, which would otherwise\n * store it as the row's value. */\n auto?: ReadonlySet<string>;\n /** Draw leaves marked `hidden`. */\n showHidden?: boolean;\n /** Heads the panel. With `title`, `stance` or `tone` given, the rows sit in a\n * `<PropertyPanel>` carrying them; with none, they are the bare list. */\n title?: ReactNode;\n className?: string;\n}\n\nconst NO_AUTO: ReadonlySet<string> = new Set();\n\n/** Every leaf beneath a group, as its dotted path and its default. */\nfunction leavesUnder(group: PrefGroup, at: string): [string, unknown][] {\n return Object.entries(group.children).flatMap(([key, child]) => {\n const path = at === '' ? key : `${at}.${key}`;\n return isPrefLeaf(child) ? [[path, child.default] as [string, unknown]] : leavesUnder(child, path);\n });\n}\n\n/** The heading button of a `.resettable()` group: writes each changed leaf's\n * default, and is disabled while nothing has changed. */\nfunction ResetGroup({\n group,\n path,\n config,\n setConfig,\n}: {\n group: PrefGroup;\n path: string;\n config: Record<string, unknown>;\n setConfig: (path: string, value: unknown) => void;\n}) {\n const changed = leavesUnder(group, path).filter(([p, d]) => !Object.is(valueAtPath(config, p), d));\n return (\n <Button\n iconOnly\n variant=\"ghost\"\n size=\"sm\"\n ariaLabel={`Reset ${group.name}`}\n tooltip={`Reset ${group.name} to defaults`}\n disabled={changed.length === 0}\n onClick={() => {\n for (const [p, d] of changed) setConfig(p, d);\n }}\n >\n <ResetIcon />\n </Button>\n );\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 auto: given,\n showHidden = false,\n title,\n stance,\n tone,\n className,\n}: ControlPanelProps<TC>) {\n const resolved = useMemo(() => schema ?? fromConfigFields(fields ?? []), [schema, fields]);\n\n // An owner that tracks the set decides what a dot means; one that does not\n // gets a panel that decides for itself, because the alternative is handing it\n // a sentinel it would write into the config as though it were a value.\n const [ownAuto, setOwnAuto] = useState<ReadonlySet<string>>(NO_AUTO);\n const auto = given ?? ownAuto;\n const setRowAuto = (path: string, next: boolean, value: unknown): void => {\n if (given) {\n setConfig(path, next ? autoValue : value);\n return;\n }\n setOwnAuto((prev) => {\n const now = new Set(prev);\n if (next) now.add(path);\n else now.delete(path);\n return now;\n });\n // Pinning keeps whatever the row was showing, the same value the controlled\n // path writes back.\n if (!next) setConfig(path, value);\n };\n\n // What the instrument is reading. A row draws and reports from this rather\n // than calling its own resolver against the raw config, which would disagree\n // with the instrument whenever a resolver depends on another auto path.\n const shown = useMemo(\n () => (auto && auto.size > 0 ? resolveAutoConfig(resolved, config, auto) : config),\n [resolved, config, auto],\n );\n\n const gridPack = gridOf(pack);\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, grid: PropertyListPack = gridPack) => ({\n pack: grid,\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, rows: Rows): 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 shown={shown}\n setConfig={setConfig}\n renderers={renderers}\n pack={rows.pack}\n layout={rows.layout}\n auto={auto}\n setRowAuto={setRowAuto}\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, rows)}</Fragment>;\n return (\n <PropertyGroup\n key={path}\n title={found.name}\n description={found.description}\n // A nested group is a block of rows, not a control: half of a\n // two-column grid leaves it laying its own rows out inside a cell,\n // which overlaps them.\n span\n actions={\n resolved.resettable?.has(path) ? (\n <ResetGroup group={found} path={path} config={config as Record<string, unknown>} setConfig={setConfig} />\n ) : undefined\n }\n {...fold(path, undefined, rows.grid)}\n >\n {body(found, path, rows)}\n </PropertyGroup>\n );\n };\n\n /**\n * The leaf at `path` if it may share a row with its neighbours: `pair` is\n * presentational, so a leaf whose control the lab draws itself, or one whose\n * control needs the whole row, keeps its own row instead.\n */\n const pairable = (path: string): { leaf: PrefLeaf; pair: string } | undefined => {\n const found = schemaNodeAtPath(resolved.group, path);\n if (!found || !isPrefLeaf(found)) return undefined;\n const pair = extra<string>(found, 'pair');\n if (pair === undefined) return undefined;\n if (!isLeafVisible(resolved, path, config as Record<string, unknown>, showHidden))\n return undefined;\n if (\n renderers?.[path] ??\n resolved.renderers[path] ??\n resolved.dialogs[path] ??\n renderers?.[found.kind]\n )\n return undefined;\n if (!isBuiltinToolPref(found)) return undefined;\n if (found.kind === 'paint' || found.kind === 'object' || isSliderLeaf(found)) return undefined;\n return { leaf: found, pair };\n };\n\n /** A run of sibling paths as rows, merging each run of adjacent leaves\n * sharing a `pair` id into one row named by the pair. */\n const rowsFor = (paths: readonly string[], rows: Rows): ReactNode[] => {\n const out: ReactNode[] = [];\n for (let i = 0; i < paths.length; ) {\n const head = pairable(paths[i]);\n if (!head) {\n out.push(node(paths[i], rows));\n i += 1;\n continue;\n }\n const { pair } = head;\n const cells = [{ path: paths[i], leaf: head.leaf }];\n let j = i + 1;\n for (; j < paths.length; j += 1) {\n const next = pairable(paths[j]);\n if (!next || next.pair !== pair) break;\n cells.push({ path: paths[j], leaf: next.leaf });\n }\n out.push(\n <PairedRow\n key={paths[i]}\n label={pair}\n cells={cells}\n shown={shown}\n setConfig={setConfig}\n layout={rows.layout}\n // A segmented cell carries every choice at once, so a pair holding\n // one needs the whole row the way a segmented row of its own does.\n span={cells.some(({ leaf }) => extra<string>(leaf, 'control') === 'radio')}\n />,\n );\n i = j;\n }\n return out;\n };\n\n /** One group's children: its loose nodes, then its sections. */\n const body = (group: PrefGroup, at: string, rows: Rows): 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 {rowsFor(\n paths.filter((p) => !sectioned.has(p)),\n rows,\n )}\n {sections.map((section) => {\n const inner = sectionRows(section, rows);\n return (\n <PropertyGroup\n key={sectionKey(section)}\n title={section.label}\n {...fold(sectionKey(section), section.collapsed, inner.grid)}\n >\n {rowsFor(section.paths, inner)}\n </PropertyGroup>\n );\n })}\n </>\n );\n };\n\n const list = (\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, '', { pack, layout, grid: gridPack })}\n </PropertyList>\n );\n if (title === undefined && stance === undefined && tone === undefined) return list;\n return (\n <PropertyPanel title={title} stance={stance} tone={tone} density={density}>\n {list}\n </PropertyPanel>\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 /** `config` with every auto path resolved -- what the instrument reads. */\n shown: TC;\n setConfig: (path: string, value: unknown) => void;\n renderers?: Record<string, ControlRenderer>;\n pack: ControlPack;\n layout?: PropertyRowLayout;\n auto?: ReadonlySet<string>;\n setRowAuto: (path: string, next: boolean, value: unknown) => void;\n}\n\n/** Whether this leaf draws as a slider, mirroring the condition the `number`\n * arm below branches on. A slider is the one control whose value cannot be\n * read off the control itself. */\nfunction isSliderLeaf(leaf: PrefLeaf): boolean {\n return (\n leaf.kind === 'number' &&\n extra<string>(leaf, 'control') === 'slider' &&\n extra<number>(leaf, 'min') !== undefined &&\n extra<number>(leaf, 'max') !== undefined\n );\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 shown,\n setConfig,\n renderers,\n pack,\n layout,\n auto,\n setRowAuto,\n}: ControlRowProps<TC>) {\n const write = (value: unknown): void => setConfig(path, value);\n const fallback = extra<unknown>(leaf, 'default');\n const pinned = valueAtPath(config, path) ?? fallback;\n\n const isAutoRow = auto?.has(path) ?? false;\n const canAuto = !extra<boolean>(leaf, 'manual');\n // Read out of the resolved config rather than re-running this leaf's own\n // resolver, so the row reports the value the instrument actually got — the\n // two diverge as soon as a resolver reads another auto path.\n const resolvedValue = isAutoRow ? valueAtPath(shown, path) : undefined;\n // An auto row draws what the resolver decided, not the value underneath it —\n // a handle sitting at the pinned number while the readout says something else\n // reads as a rendering bug. Pinning then keeps what you were looking at.\n const value = resolvedValue ?? pinned;\n const setAuto = (next: boolean): void => setRowAuto(path, next, value);\n const onAutoChange = canAuto ? setAuto : undefined;\n // An auto row hides its control, so the word is all the row has to say.\n const autoReadout = isAutoRow ? 'auto' : undefined;\n const autoProps = { auto: isAutoRow, onAutoChange };\n\n // Most specific wins, and within a tier the lab's entry beats the\n // instrument's: controls[path] -> node .render/.dialog -> controls[kind] -> built-in.\n const dialog = resolved.dialogs[path];\n const custom =\n renderers?.[path] ??\n resolved.renderers[path] ??\n (dialog ? inDialog(dialog.body, dialog) : undefined) ??\n 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, auto: isAutoRow, setAuto });\n\n const label = leaf.name;\n const description = leaf.description;\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 const row = { label, description, layout, readout: autoReadout, ...autoProps };\n\n if (leaf.kind === 'list') {\n const entries = Array.isArray(value) ? (value as string[]) : [];\n return (\n <DialogRow\n label={label}\n summary={summarizeValue(entries)}\n readout={autoReadout}\n layout={layout}\n description={description}\n {...autoProps}\n >\n {() => (\n <ListEditor\n aria-label={label}\n value={entries}\n onChange={write}\n placeholder={extra<string>(leaf, 'placeholder')}\n />\n )}\n </DialogRow>\n );\n }\n\n const field = labField(leaf, value, write);\n if (field === null)\n return <UnwiredRow label={label} kind={leaf.kind} description={description} />;\n switch (field.kind) {\n case 'string':\n return <DebouncedTextRow leaf={leaf} field={field} span={wide} {...row} />;\n case 'number':\n return <PropertyField {...field} {...row} span={field.control === 'slider' && wide} />;\n case 'enum':\n return <PropertyField {...field} {...row} span={field.control === 'toggle' && wide} />;\n default:\n return <PropertyField {...field} {...row} />;\n }\n}\n\n/**\n * A leaf as the field it draws, with the labkit-only extras `prefFieldProps`\n * knows nothing of. `null` for a kind the panel declines: a paint, which a\n * hex swatch would flatten to a solid, and an object, which a flat row would\n * write one field of. Override with `render` to edit either.\n */\nfunction labField(\n leaf: PrefLeaf,\n value: unknown,\n write: (value: unknown) => void,\n): PropertyControlProps | null {\n if (!isBuiltinToolPref(leaf) || leaf.kind === 'paint' || leaf.kind === 'object') return null;\n const field = prefFieldProps(leaf, { value, setValue: write });\n if (field === null) return null;\n switch (field.kind) {\n case 'number':\n // `prefFieldProps` clamps what it stores, so an instrument is never\n // handed a value outside the range it asked for.\n return {\n ...field,\n control: isSliderLeaf(leaf) ? 'slider' : 'input',\n unit: extra<string>(leaf, 'suffix') ?? field.unit,\n };\n case 'boolean':\n return {\n ...field,\n control: extra<string>(leaf, 'control') === 'switch' ? 'switch' : 'checkbox',\n };\n case 'enum':\n // `.radio()` asks for every option at once, as segments.\n return {\n ...field,\n control: extra<string>(leaf, 'control') === 'radio' ? 'toggle' : 'select',\n };\n case 'string':\n return {\n ...field,\n placeholder: extra<string>(leaf, 'placeholder'),\n maxLength: extra<number>(leaf, 'maxLength'),\n };\n default:\n return field;\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/** A text row whose writes are debounced — see {@link useDebouncedText}. */\nfunction DebouncedTextRow({\n leaf,\n field,\n ...row\n}: {\n leaf: PrefLeaf;\n field: PropertyControlProps & { kind: 'string' };\n label: string;\n layout?: PropertyRowLayout;\n span?: boolean;\n description?: string;\n readout?: ReactNode;\n auto?: boolean;\n onAutoChange?: (next: boolean) => void;\n}) {\n const text = useDebouncedText(\n field.value ?? '',\n field.onChange,\n extra<number>(leaf, 'debounceMs') ?? 150,\n );\n return <PropertyField {...field} {...row} value={text.local} onChange={text.type} />;\n}\n\n/** One cell of a paired row, with its path and the leaf it draws. */\ninterface PairCellSpec {\n path: string;\n leaf: PrefLeaf;\n}\n\n/**\n * Leaves sharing a `pair` id, side by side on one row the pair names — the\n * compact `X` / `Y` idiom `SelectionPanel` draws for the same annotation.\n *\n * The cells are bare controls: a row holds one label column, so each leaf's\n * own name stays with its control as the accessible name and its description\n * as a title. For the same reason the row's label does not toggle auto — it\n * names one path, and this row has several.\n */\nfunction PairedRow<TC extends Record<string, unknown>>({\n label,\n cells,\n shown,\n setConfig,\n layout,\n span,\n}: {\n label: string;\n cells: readonly PairCellSpec[];\n shown: TC;\n setConfig: (path: string, value: unknown) => void;\n layout?: PropertyRowLayout;\n span?: boolean;\n}) {\n return (\n <PropertyRow group span={span} label={label} layout={layout}>\n {cells.map(({ path, leaf }) => (\n // The row is named by the pair, so a cell that shares it says which\n // knob it is — without a caption a paired switch reads only to a\n // screen reader. A cell whose leaf is deliberately unnamed keeps none.\n <span key={path} className=\"lk-pair-cell\" title={leaf.description || undefined}>\n {leaf.name ? <span className=\"lk-pair-label\">{leaf.name}</span> : null}\n <PairCell\n leaf={leaf}\n value={valueAtPath(shown, path) ?? extra<unknown>(leaf, 'default')}\n write={(value) => setConfig(path, value)}\n />\n </span>\n ))}\n </PropertyRow>\n );\n}\n\n/** The control a paired cell holds, without the row chrome a whole row of its\n * own would bring. */\nfunction PairCell({\n leaf,\n value,\n write,\n}: {\n leaf: PrefLeaf;\n value: unknown;\n write: (value: unknown) => void;\n}) {\n const text = useDebouncedText(\n typeof value === 'string' ? value : '',\n write,\n extra<number>(leaf, 'debounceMs') ?? 150,\n );\n const field = labField(leaf, value, write);\n // Unreachable for null: `pairable` admits only kinds with a field.\n if (field === null) return null;\n switch (field.kind) {\n case 'string':\n return (\n <PropertyControl {...field} name={leaf.name} value={text.local} onChange={text.type} />\n );\n case 'number':\n // The pair names the row, so a cell has no room for a unit.\n return <PropertyControl {...field} name={leaf.name} control=\"input\" unit={undefined} />;\n case 'color': {\n // The alpha track needs a row of its own to sit under, so a paired\n // swatch edits the color and carries the stored alpha through untouched.\n const stored = typeof value === 'string' ? toHex8(value) : '#000000';\n return (\n <PropertyControl\n {...field}\n name={leaf.name}\n alpha={undefined}\n onChange={(rgb: string) =>\n write(field.alpha ? withAlpha01(rgb, getAlpha01(stored)) : rgb)\n }\n />\n );\n }\n default:\n return <PropertyControl {...field} name={leaf.name} />;\n }\n}\n\n/**\n * Live text held locally between debounced commits, so typing does not re-run\n * the instrument on every keystroke. Locally controlled between commits, which\n * means it has to notice the value changing underneath it.\n */\nfunction useDebouncedText(\n value: string,\n write: (value: string) => void,\n debounceMs: number,\n): { local: string; type: (next: string) => void } {\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 local,\n type: (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"]}