@weasel-js/labkit 1.4.2 → 1.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -2
- package/dist/_dts/{CanvasStackContext-kjILVnPj.d.ts → CanvasStackContext-kTEZvEgE.d.ts} +1 -1
- package/dist/_dts/{PrefsForm.d-CgxUequc.d.ts → PrefsForm.d-DDHFANkc.d.ts} +1 -1
- package/dist/_dts/{frac-A4R6v4ld.d.ts → frac-z7ker2Vx.d.ts} +11 -7
- package/dist/_dts/{index-CsU9WhjM.d.ts → index-Blv9uQzu.d.ts} +42 -9
- package/dist/_dts/types-B9_zrHmb.d.ts +166 -0
- package/dist/_dts/{types-BbXpvQa8.d.ts → types-BP2OCcpg.d.ts} +21 -3
- package/dist/_dts/types-x92Kfeme.d.ts +62 -0
- package/dist/_dts/{useTrialState-DKjqpv20.d.ts → useTrialState-D6Vb3T-g.d.ts} +7 -7
- package/dist/canvas/index.d.ts +9 -8
- package/dist/canvas/index.js +1 -2
- package/dist/chrome/index.d.ts +7 -6
- package/dist/chrome/index.js +5 -5
- package/dist/{chunk-ULDW42CR.js → chunk-2P6PP5N4.js} +33 -23
- package/dist/chunk-2P6PP5N4.js.map +1 -0
- package/dist/{chunk-W2FJR5FF.js → chunk-64ZCN3DA.js} +74 -76
- package/dist/chunk-64ZCN3DA.js.map +1 -0
- package/dist/{chunk-NSOVI3AZ.js → chunk-BKFVHKJH.js} +6 -6
- package/dist/{chunk-NSOVI3AZ.js.map → chunk-BKFVHKJH.js.map} +1 -1
- package/dist/chunk-E44SU6XS.js +50 -0
- package/dist/chunk-E44SU6XS.js.map +1 -0
- package/dist/{chunk-WS6ZRV75.js → chunk-FQMJUVHJ.js} +3 -3
- package/dist/{chunk-WS6ZRV75.js.map → chunk-FQMJUVHJ.js.map} +1 -1
- package/dist/{chunk-FJG4PHTL.js → chunk-I6JCVE24.js} +3 -3
- package/dist/chunk-I6JCVE24.js.map +1 -0
- package/dist/{chunk-UDXOYZEC.js → chunk-MOM3GOVY.js} +4 -4
- package/dist/{chunk-UDXOYZEC.js.map → chunk-MOM3GOVY.js.map} +1 -1
- package/dist/{chunk-TN7YSJVU.js → chunk-RS3HRQWU.js} +3 -3
- package/dist/{chunk-TN7YSJVU.js.map → chunk-RS3HRQWU.js.map} +1 -1
- package/dist/{chunk-UTOEDPNU.js → chunk-SMHP6XZ4.js} +7 -6
- package/dist/chunk-SMHP6XZ4.js.map +1 -0
- package/dist/{chunk-D5KQ5OY6.js → chunk-TJ7QY3OC.js} +3 -3
- package/dist/{chunk-D5KQ5OY6.js.map → chunk-TJ7QY3OC.js.map} +1 -1
- package/dist/{chunk-XKENZTNE.js → chunk-U3IYHIAE.js} +71 -55
- package/dist/chunk-U3IYHIAE.js.map +1 -0
- package/dist/chunk-W3ECWC2K.js +7335 -0
- package/dist/chunk-W3ECWC2K.js.map +1 -0
- package/dist/controls/index.d.ts +5 -4
- package/dist/controls/index.js +3 -3
- package/dist/dragdrop/index.d.ts +6 -5
- package/dist/dragdrop/index.js +1 -2
- package/dist/index.d.ts +139 -26
- package/dist/index.js +309 -144
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +7 -6
- package/dist/layers/index.js +2 -3
- package/dist/loupe/index.d.ts +8 -7
- package/dist/loupe/index.js +1 -2
- package/dist/passthrough/weasel-canvas.d.ts +1 -3
- package/dist/passthrough/weasel-canvas.js +1 -1
- package/dist/passthrough/weasel-canvas.js.map +1 -1
- package/dist/passthrough/weasel-ui.d.ts +98 -20
- package/dist/passthrough/weasel-ui.js +1 -2
- package/dist/primitives/index.js +3 -4
- package/dist/state/index.d.ts +6 -3
- package/dist/state/index.js +3 -2
- package/dist/state/index.js.map +1 -1
- package/dist/styles.css +82 -15
- package/dist/surface/index.js +1 -2
- package/dist/ui/layers/index.js +1 -2
- package/dist/undo/index.d.ts +6 -6
- package/package.json +7 -7
- package/src/annotations/ExportMenu.test.tsx +16 -0
- package/src/annotations/ExportMenu.tsx +0 -5
- package/src/annotations/frac.test.ts +4 -4
- package/src/annotations/frac.ts +3 -2
- package/src/annotations/index.ts +1 -1
- package/src/annotations/store.ts +2 -2
- package/src/annotations/svgNodes.ts +1 -2
- package/src/config/builder.test.ts +56 -2
- package/src/config/builder.ts +73 -13
- package/src/config/index.ts +16 -1
- package/src/config/path.test.ts +109 -0
- package/src/config/path.ts +76 -0
- package/src/config/resolve.test.ts +93 -2
- package/src/config/resolve.ts +72 -29
- package/src/config/types.ts +99 -9
- package/src/config/visible.ts +6 -4
- package/src/controls/ControlPanel.stories.tsx +38 -4
- package/src/controls/ControlPanel.test.tsx +114 -0
- package/src/controls/ControlPanel.tsx +99 -50
- package/src/index.ts +17 -2
- package/src/instrument/serializers.ts +36 -0
- package/src/instrument/types.ts +4 -2
- package/src/lab/Lab.test.tsx +29 -1
- package/src/lab/Lab.tsx +10 -1
- package/src/lab/LabShell.test.tsx +8 -0
- package/src/lab/LabShell.tsx +17 -3
- package/src/lab/LabSwitcher.less +85 -0
- package/src/lab/LabSwitcher.test.tsx +96 -0
- package/src/lab/LabSwitcher.tsx +112 -0
- package/src/lab/index.ts +2 -0
- package/src/primitives/ZoomControl.less +6 -2
- package/src/primitives/ZoomControl.tsx +1 -0
- package/src/state/helpers.test.ts +20 -0
- package/src/state/helpers.ts +17 -1
- package/src/state/store.test.ts +155 -1
- package/src/state/store.ts +32 -26
- package/src/state/types.ts +24 -3
- package/src/state/useTrialState.ts +1 -1
- package/src/styles.less +1 -0
- package/src/theme/base.less +4 -4
- package/src/trial/Trial.config.test.tsx +44 -0
- package/src/trial/Trial.tsx +3 -3
- package/src/trial/TrialChrome.tsx +10 -5
- package/src/trial/trialOps.ts +7 -10
- package/dist/_dts/types-lg4TSCb2.d.ts +0 -164
- package/dist/_dts/weasel-canvas-FJTFi3ZZ.d.ts +0 -5041
- package/dist/chunk-54ZWZ5FQ.js +0 -29997
- package/dist/chunk-54ZWZ5FQ.js.map +0 -1
- package/dist/chunk-FJG4PHTL.js.map +0 -1
- package/dist/chunk-ISSVF5PT.js +0 -7263
- package/dist/chunk-ISSVF5PT.js.map +0 -1
- package/dist/chunk-ULDW42CR.js.map +0 -1
- package/dist/chunk-UTOEDPNU.js.map +0 -1
- package/dist/chunk-W2FJR5FF.js.map +0 -1
- package/dist/chunk-XKENZTNE.js.map +0 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { schemaNodeAtPath, valueAtPath } from './chunk-E44SU6XS.js';
|
|
2
|
+
import { T2, H, t2, M2, L2, I, N2, A2, F, D, P2 } from './chunk-W3ECWC2K.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
|
|
68
|
-
if (
|
|
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
|
}
|
|
@@ -86,71 +87,86 @@ function ControlPanel({
|
|
|
86
87
|
className
|
|
87
88
|
}) {
|
|
88
89
|
const resolved = useMemo(() => schema ?? fromConfigFields(fields ?? []), [schema, fields]);
|
|
89
|
-
const paths = Object.keys(resolved.group.children);
|
|
90
|
-
const sectioned = new Set(resolved.sections.flatMap((s) => s.paths));
|
|
91
|
-
const loose = paths.filter((p) => !sectioned.has(p));
|
|
92
|
-
const row = (path) => /* @__PURE__ */ jsx(
|
|
93
|
-
ControlRow,
|
|
94
|
-
{
|
|
95
|
-
path,
|
|
96
|
-
resolved,
|
|
97
|
-
config,
|
|
98
|
-
setConfig,
|
|
99
|
-
renderers,
|
|
100
|
-
pack,
|
|
101
|
-
layout,
|
|
102
|
-
showHidden
|
|
103
|
-
},
|
|
104
|
-
path
|
|
105
|
-
);
|
|
106
90
|
const gridPack = pack === "one-up" ? "auto-color" : "pairs";
|
|
107
|
-
const folds = collapse !== void 0 || collapsed !== void 0 || onCollapse !== void 0;
|
|
91
|
+
const folds = collapse !== void 0 || collapsed !== void 0 || onCollapse !== void 0 || resolved.sections.some((s) => s.collapsed !== void 0);
|
|
108
92
|
const startsFolded = collapse === "closed";
|
|
109
|
-
|
|
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(H, { 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
|
+
H,
|
|
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(
|
|
110
141
|
T2,
|
|
111
142
|
{
|
|
112
143
|
pack: gridPack,
|
|
113
144
|
density,
|
|
114
145
|
align,
|
|
115
146
|
className: className ? `lk-control-panel ${className}` : "lk-control-panel",
|
|
116
|
-
children:
|
|
117
|
-
loose.map(row),
|
|
118
|
-
resolved.sections.map((section) => /* @__PURE__ */ jsx(
|
|
119
|
-
V,
|
|
120
|
-
{
|
|
121
|
-
title: section.label,
|
|
122
|
-
pack: gridPack,
|
|
123
|
-
collapsible: folds,
|
|
124
|
-
defaultCollapsed: startsFolded,
|
|
125
|
-
collapsed: collapsed ? collapsed[section.label] ?? startsFolded : void 0,
|
|
126
|
-
onCollapsedChange: onCollapse ? (next) => onCollapse(section.label, next) : void 0,
|
|
127
|
-
children: section.paths.map(row)
|
|
128
|
-
},
|
|
129
|
-
section.label
|
|
130
|
-
))
|
|
131
|
-
]
|
|
147
|
+
children: body(resolved.group, "")
|
|
132
148
|
}
|
|
133
149
|
);
|
|
134
150
|
}
|
|
151
|
+
function sectionKey(section) {
|
|
152
|
+
return section.at === "" ? section.label : `${section.at}.${section.label}`;
|
|
153
|
+
}
|
|
135
154
|
function extra(leaf, key) {
|
|
136
155
|
return leaf[key];
|
|
137
156
|
}
|
|
138
157
|
function ControlRow({
|
|
139
158
|
path,
|
|
159
|
+
leaf,
|
|
140
160
|
resolved,
|
|
141
161
|
config,
|
|
142
162
|
setConfig,
|
|
143
163
|
renderers,
|
|
144
164
|
pack,
|
|
145
|
-
layout
|
|
146
|
-
showHidden
|
|
165
|
+
layout
|
|
147
166
|
}) {
|
|
148
|
-
const leaf = resolved.group.children[path];
|
|
149
|
-
if (!leaf || !("kind" in leaf)) return null;
|
|
150
|
-
if (!isLeafVisible(resolved, path, config, showHidden)) return null;
|
|
151
167
|
const write = (value2) => setConfig(path, value2);
|
|
152
168
|
const fallback = extra(leaf, "default");
|
|
153
|
-
const value = config
|
|
169
|
+
const value = valueAtPath(config, path) ?? fallback;
|
|
154
170
|
const custom = renderers?.[path] ?? resolved.renderers[path] ?? renderers?.[leaf.kind];
|
|
155
171
|
if (custom) return custom({ path, pref: leaf, value, setValue: write });
|
|
156
172
|
const label = leaf.name;
|
|
@@ -167,7 +183,7 @@ function ControlRow({
|
|
|
167
183
|
const suffix = extra(leaf, "suffix");
|
|
168
184
|
if (extra(leaf, "control") === "slider" && min !== void 0 && max !== void 0) {
|
|
169
185
|
return /* @__PURE__ */ jsx(
|
|
170
|
-
|
|
186
|
+
A2,
|
|
171
187
|
{
|
|
172
188
|
label,
|
|
173
189
|
value: read(),
|
|
@@ -185,7 +201,7 @@ function ControlRow({
|
|
|
185
201
|
const lo = min ?? Number.NEGATIVE_INFINITY;
|
|
186
202
|
const hi = max ?? Number.POSITIVE_INFINITY;
|
|
187
203
|
return /* @__PURE__ */ jsx(
|
|
188
|
-
|
|
204
|
+
F,
|
|
189
205
|
{
|
|
190
206
|
label,
|
|
191
207
|
value: read(),
|
|
@@ -201,7 +217,7 @@ function ControlRow({
|
|
|
201
217
|
}
|
|
202
218
|
case "boolean":
|
|
203
219
|
return /* @__PURE__ */ jsx(
|
|
204
|
-
|
|
220
|
+
N2,
|
|
205
221
|
{
|
|
206
222
|
label,
|
|
207
223
|
value: read(),
|
|
@@ -213,7 +229,7 @@ function ControlRow({
|
|
|
213
229
|
case "enum": {
|
|
214
230
|
const options = extra(leaf, "options") ?? [];
|
|
215
231
|
const segmented = extra(leaf, "control") === "radio";
|
|
216
|
-
const Row = segmented ?
|
|
232
|
+
const Row = segmented ? L2 : I;
|
|
217
233
|
return /* @__PURE__ */ jsx(
|
|
218
234
|
Row,
|
|
219
235
|
{
|
|
@@ -242,7 +258,7 @@ function ControlRow({
|
|
|
242
258
|
);
|
|
243
259
|
case "color":
|
|
244
260
|
return /* @__PURE__ */ jsx(
|
|
245
|
-
|
|
261
|
+
M2,
|
|
246
262
|
{
|
|
247
263
|
label,
|
|
248
264
|
value: read(),
|
|
@@ -299,7 +315,7 @@ function DebouncedTextRow({
|
|
|
299
315
|
[]
|
|
300
316
|
);
|
|
301
317
|
return /* @__PURE__ */ jsx(
|
|
302
|
-
|
|
318
|
+
P2,
|
|
303
319
|
{
|
|
304
320
|
label,
|
|
305
321
|
layout,
|
|
@@ -323,5 +339,5 @@ function DebouncedTextRow({
|
|
|
323
339
|
}
|
|
324
340
|
|
|
325
341
|
export { ControlPanel, fromConfigFields, isLeafVisible };
|
|
326
|
-
//# sourceMappingURL=chunk-
|
|
327
|
-
//# sourceMappingURL=chunk-
|
|
342
|
+
//# sourceMappingURL=chunk-U3IYHIAE.js.map
|
|
343
|
+
//# sourceMappingURL=chunk-U3IYHIAE.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","A","N","L","M","P"],"mappings":";;;;;;;AAOA,SAAS,OAAO,KAAA,EAA+B;AAC7C,EAAA,MAAM,OAAO,EAAE,IAAA,EAAM,KAAA,CAAM,KAAA,EAAO,aAAa,EAAA,EAAG;AAClD,EAAA,QAAQ,MAAM,IAAA;AAAM,IAClB,KAAK,QAAA;AACH,MAAA,OAAO;AAAA,QACL,GAAG,IAAA;AAAA,QACH,IAAA,EAAM,QAAA;AAAA,QACN,OAAA,EAAS,QAAA;AAAA,QACT,KAAK,KAAA,CAAM,GAAA;AAAA,QACX,KAAK,KAAA,CAAM,GAAA;AAAA,QACX,MAAM,KAAA,CAAM;AAAA,OACd;AAAA,IACF,KAAK,QAAA;AACH,MAAA,OAAO;AAAA,QACL,GAAG,IAAA;AAAA,QACH,IAAA,EAAM,QAAA;AAAA,QACN,OAAA,EAAS,OAAA;AAAA,QACT,KAAK,KAAA,CAAM,GAAA;AAAA,QACX,KAAK,KAAA,CAAM,GAAA;AAAA,QACX,MAAM,KAAA,CAAM;AAAA,OACd;AAAA,IACF,KAAK,UAAA;AACH,MAAA,OAAO,EAAE,GAAG,IAAA,EAAM,IAAA,EAAM,SAAA,EAAW,SAAS,UAAA,EAAW;AAAA,IACzD,KAAK,QAAA;AACH,MAAA,OAAO,EAAE,GAAG,IAAA,EAAM,MAAM,MAAA,EAAQ,OAAA,EAAS,MAAM,OAAA,EAAQ;AAAA,IACzD,KAAK,MAAA;AACH,MAAA,OAAO;AAAA,QACL,GAAG,IAAA;AAAA,QACH,IAAA,EAAM,QAAA;AAAA,QACN,aAAa,KAAA,CAAM,WAAA;AAAA,QACnB,WAAW,KAAA,CAAM,SAAA;AAAA,QACjB,YAAY,KAAA,CAAM;AAAA,OACpB;AAAA,IACF,KAAK,OAAA;AACH,MAAA,OAAO,EAAE,GAAG,IAAA,EAAM,IAAA,EAAM,OAAA,EAAQ;AAAA,IAClC;AAGE,MAAA,OAAO,EAAE,GAAG,IAAA,EAAM,IAAA,EAAO,MAAsB,IAAA,EAAK;AAAA;AAE1D;AASO,SAAS,iBAAiB,MAAA,EAAgD;AAC/E,EAAA,MAAM,WAAqC,EAAC;AAC5C,EAAA,KAAA,MAAW,SAAS,MAAA,EAAQ;AAC1B,IAAA,MAAM,KAAA,GAAQ,OAAO,KAAK,CAAA;AAC1B,IAAA,KAAA,MAAW,GAAA,IAAO,MAAA,CAAO,IAAA,CAAK,KAAK,CAAA,EAAG;AACpC,MAAA,IAAK,KAAA,CAAkC,GAAG,CAAA,KAAM,MAAA,EAAW;AACzD,QAAA,OAAQ,MAAkC,GAAG,CAAA;AAAA,MAC/C;AAAA,IACF;AACA,IAAA,QAAA,CAAS,KAAA,CAAM,GAAG,CAAA,GAAI,EAAE,GAAG,KAAA,EAAO,OAAA,EAAS,MAAM,OAAA,EAAQ;AAAA,EAC3D;AACA,EAAA,OAAO;AAAA,IACL,KAAA,EAAO,EAAE,IAAA,EAAM,EAAA,EAAI,QAAA,EAAS;AAAA,IAC5B,UAAU,EAAC;AAAA,IACX,MAAA,sBAAY,GAAA,EAAI;AAAA,IAChB,WAAW;AAAC,GACd;AACF;;;AC9DO,SAAS,aAAA,CACd,QAAA,EACA,IAAA,EACA,MAAA,EACA,aAAa,KAAA,EACJ;AACT,EAAA,MAAM,IAAA,GAAO,gBAAA,CAAiB,QAAA,CAAS,KAAA,EAAO,IAAI,CAAA;AAClD,EAAA,IAAI,IAAA,IAAQ,YAAY,IAAA,IAAQ,IAAA,CAAK,WAAW,IAAA,IAAQ,CAAC,YAAY,OAAO,KAAA;AAC5E,EAAA,MAAM,SAAA,GAAY,QAAA,CAAS,MAAA,CAAO,GAAA,CAAI,IAAI,CAAA;AAC1C,EAAA,OAAO,SAAA,GAAY,SAAA,CAAU,MAAM,CAAA,GAAI,IAAA;AACzC;ACoEO,SAAS,YAAA,CAAiD;AAAA,EAC/D,MAAA;AAAA,EACA,MAAA;AAAA,EACA,MAAA;AAAA,EACA,SAAA;AAAA,EACA,SAAA;AAAA,EACA,IAAA,GAAO,OAAA;AAAA,EACP,MAAA;AAAA,EACA,OAAA;AAAA,EACA,KAAA;AAAA,EACA,QAAA;AAAA,EACA,SAAA;AAAA,EACA,UAAA;AAAA,EACA,UAAA,GAAa,KAAA;AAAA,EACb;AACF,CAAA,EAA0B;AACxB,EAAA,MAAM,QAAA,GAAW,OAAA,CAAQ,MAAM,MAAA,IAAU,gBAAA,CAAiB,MAAA,IAAU,EAAE,CAAA,EAAG,CAAC,MAAA,EAAQ,MAAM,CAAC,CAAA;AAEzF,EAAA,MAAM,QAAA,GAA6B,IAAA,KAAS,QAAA,GAAW,YAAA,GAAe,OAAA;AAGtE,EAAA,MAAM,KAAA,GACJ,QAAA,KAAa,MAAA,IACb,SAAA,KAAc,UACd,UAAA,KAAe,MAAA,IACf,QAAA,CAAS,QAAA,CAAS,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,cAAc,MAAS,CAAA;AACzD,EAAA,MAAM,eAAe,QAAA,KAAa,QAAA;AAElC,EAAA,MAAM,IAAA,GAAO,CAAC,GAAA,EAAa,QAAA,MAAwB;AAAA,IACjD,IAAA,EAAM,QAAA;AAAA,IACN,WAAA,EAAa,KAAA;AAAA,IACb,kBAAkB,QAAA,IAAY,YAAA;AAAA,IAC9B,WAAW,SAAA,GAAa,SAAA,CAAU,GAAG,CAAA,IAAK,YAAY,YAAA,GAAgB,MAAA;AAAA,IACtE,mBAAmB,UAAA,GAAa,CAAC,SAAkB,UAAA,CAAW,GAAA,EAAK,IAAI,CAAA,GAAI;AAAA,GAC7E,CAAA;AAGA,EAAA,MAAM,IAAA,GAAO,CAAC,IAAA,KAA4B;AACxC,IAAA,MAAM,KAAA,GAAQ,gBAAA,CAAiB,QAAA,CAAS,KAAA,EAAO,IAAI,CAAA;AACnD,IAAA,IAAI,CAAC,OAAO,OAAO,IAAA;AACnB,IAAA,IAAI,CAAC,aAAA,CAAc,QAAA,EAAU,MAAM,MAAA,EAAmC,UAAU,GAAG,OAAO,IAAA;AAC1F,IAAA,IAAIA,EAAA,CAAW,KAAK,CAAA,EAAG;AACrB,MAAA,uBACE,GAAA;AAAA,QAAC,UAAA;AAAA,QAAA;AAAA,UAEC,IAAA;AAAA,UACA,IAAA,EAAM,KAAA;AAAA,UACN,QAAA;AAAA,UACA,MAAA;AAAA,UACA,SAAA;AAAA,UACA,SAAA;AAAA,UACA,IAAA;AAAA,UACA;AAAA,SAAA;AAAA,QARK;AAAA,OASP;AAAA,IAEJ;AAGA,IAAA,IAAI,KAAA,CAAM,IAAA,KAAS,EAAA,EAAI,uBAAO,GAAA,CAACC,cAAqB,QAAA,EAAA,IAAA,CAAK,KAAA,EAAO,IAAI,CAAA,EAAA,EAAvB,IAAyB,CAAA;AACtE,IAAA,uBACE,GAAA,CAAC,CAAA,EAAA,EAAyB,KAAA,EAAO,KAAA,CAAM,IAAA,EAAO,GAAG,IAAA,CAAK,IAAI,CAAA,EACvD,QAAA,EAAA,IAAA,CAAK,KAAA,EAAO,IAAI,KADC,IAEpB,CAAA;AAAA,EAEJ,CAAA;AAGA,EAAA,MAAM,IAAA,GAAO,CAAC,KAAA,EAAkB,EAAA,KAA0B;AACxD,IAAA,MAAM,QAAA,GAAW,SAAS,QAAA,CAAS,MAAA,CAAO,CAAC,CAAA,KAAM,CAAA,CAAE,OAAO,EAAE,CAAA;AAC5D,IAAA,MAAM,SAAA,GAAY,IAAI,GAAA,CAAI,QAAA,CAAS,QAAQ,CAAC,CAAA,KAAM,CAAA,CAAE,KAAK,CAAC,CAAA;AAC1D,IAAA,MAAM,QAAQ,MAAA,CAAO,IAAA,CAAK,KAAA,CAAM,QAAQ,EAAE,GAAA,CAAI,CAAC,GAAA,KAAS,EAAA,KAAO,KAAK,GAAA,GAAM,CAAA,EAAG,EAAE,CAAA,CAAA,EAAI,GAAG,CAAA,CAAG,CAAA;AACzF,IAAA,uBACE,IAAA,CAAAA,UAAA,EACG,QAAA,EAAA;AAAA,MAAA,KAAA,CAAM,MAAA,CAAO,CAAC,CAAA,KAAM,CAAC,SAAA,CAAU,IAAI,CAAC,CAAC,CAAA,CAAE,GAAA,CAAI,IAAI,CAAA;AAAA,MAC/C,QAAA,CAAS,GAAA,CAAI,CAAC,OAAA,qBACb,GAAA;AAAA,QAAC,CAAA;AAAA,QAAA;AAAA,UAEC,OAAO,OAAA,CAAQ,KAAA;AAAA,UACd,GAAG,IAAA,CAAK,UAAA,CAAW,OAAO,CAAA,EAAG,QAAQ,SAAS,CAAA;AAAA,UAE9C,QAAA,EAAA,OAAA,CAAQ,KAAA,CAAM,GAAA,CAAI,IAAI;AAAA,SAAA;AAAA,QAJlB,WAAW,OAAO;AAAA,OAM1B;AAAA,KAAA,EACH,CAAA;AAAA,EAEJ,CAAA;AAEA,EAAA,uBACE,GAAA;AAAA,IAACC,EAAA;AAAA,IAAA;AAAA,MACC,IAAA,EAAM,QAAA;AAAA,MACN,OAAA;AAAA,MACA,KAAA;AAAA,MACA,SAAA,EAAW,SAAA,GAAY,CAAA,iBAAA,EAAoB,SAAS,CAAA,CAAA,GAAK,kBAAA;AAAA,MAExD,QAAA,EAAA,IAAA,CAAK,QAAA,CAAS,KAAA,EAAO,EAAE;AAAA;AAAA,GAC1B;AAEJ;AAKA,SAAS,WAAW,OAAA,EAA8B;AAChD,EAAA,OAAO,OAAA,CAAQ,EAAA,KAAO,EAAA,GAAK,OAAA,CAAQ,KAAA,GAAQ,GAAG,OAAA,CAAQ,EAAE,CAAA,CAAA,EAAI,OAAA,CAAQ,KAAK,CAAA,CAAA;AAC3E;AAeA,SAAS,KAAA,CAAS,MAAgB,GAAA,EAA4B;AAC5D,EAAA,OAAQ,KAAkD,GAAG,CAAA;AAC/D;AAEA,SAAS,UAAA,CAA+C;AAAA,EACtD,IAAA;AAAA,EACA,IAAA;AAAA,EACA,QAAA;AAAA,EACA,MAAA;AAAA,EACA,SAAA;AAAA,EACA,SAAA;AAAA,EACA,IAAA;AAAA,EACA;AACF,CAAA,EAAwB;AACtB,EAAA,MAAM,KAAA,GAAQ,CAACC,MAAAA,KAAyB,SAAA,CAAU,MAAMA,MAAK,CAAA;AAC7D,EAAA,MAAM,QAAA,GAAW,KAAA,CAAe,IAAA,EAAM,SAAS,CAAA;AAC/C,EAAA,MAAM,KAAA,GAAQ,WAAA,CAAY,MAAA,EAAQ,IAAI,CAAA,IAAK,QAAA;AAI3C,EAAA,MAAM,MAAA,GAAS,SAAA,GAAY,IAAI,CAAA,IAAK,QAAA,CAAS,UAAU,IAAI,CAAA,IAAK,SAAA,GAAY,IAAA,CAAK,IAAI,CAAA;AAGrF,EAAA,IAAI,MAAA,EAAQ,OAAO,MAAA,CAAO,EAAE,IAAA,EAAM,MAAM,IAAA,EAAM,KAAA,EAAO,QAAA,EAAU,KAAA,EAAO,CAAA;AAEtE,EAAA,MAAM,QAAQ,IAAA,CAAK,IAAA;AACnB,EAAA,MAAM,cAAc,IAAA,CAAK,WAAA;AACzB,EAAA,MAAM,OAAO,MAAa,KAAA;AAG1B,EAAA,MAAM,OAAO,IAAA,KAAS,MAAA;AAEtB,EAAA,IAAI,CAAC,kBAAkB,IAAI,CAAA;AACzB,IAAA,2BAAQ,UAAA,EAAA,EAAW,KAAA,EAAc,IAAA,EAAM,IAAA,CAAK,MAAM,WAAA,EAA0B,CAAA;AAE9E,EAAA,QAAQ,KAAK,IAAA;AAAM,IACjB,KAAK,QAAA,EAAU;AACb,MAAA,MAAM,GAAA,GAAM,KAAA,CAAc,IAAA,EAAM,KAAK,CAAA;AACrC,MAAA,MAAM,GAAA,GAAM,KAAA,CAAc,IAAA,EAAM,KAAK,CAAA;AACrC,MAAA,MAAM,IAAA,GAAO,KAAA,CAAc,IAAA,EAAM,MAAM,CAAA;AACvC,MAAA,MAAM,MAAA,GAAS,KAAA,CAAc,IAAA,EAAM,QAAQ,CAAA;AAC3C,MAAA,IAAI,KAAA,CAAc,MAAM,SAAS,CAAA,KAAM,YAAY,GAAA,KAAQ,MAAA,IAAa,QAAQ,MAAA,EAAW;AACzF,QAAA,uBACE,GAAA;AAAA,UAACC,EAAA;AAAA,UAAA;AAAA,YACC,KAAA;AAAA,YACA,OAAO,IAAA,EAAa;AAAA,YACpB,GAAA;AAAA,YACA,GAAA;AAAA,YACA,IAAA;AAAA,YACA,IAAA,EAAM,MAAA;AAAA,YACN,QAAA,EAAU,KAAA;AAAA,YACV,MAAA;AAAA,YACA,IAAA,EAAM,IAAA;AAAA,YACN;AAAA;AAAA,SACF;AAAA,MAEJ;AAIA,MAAA,MAAM,EAAA,GAAK,OAAO,MAAA,CAAO,iBAAA;AACzB,MAAA,MAAM,EAAA,GAAK,OAAO,MAAA,CAAO,iBAAA;AACzB,MAAA,uBACE,GAAA;AAAA,QAAC,CAAA;AAAA,QAAA;AAAA,UACC,KAAA;AAAA,UACA,OAAO,IAAA,EAAa;AAAA,UACpB,GAAA;AAAA,UACA,GAAA;AAAA,UACA,IAAA;AAAA,UACA,IAAA,EAAM,MAAA;AAAA,UACN,QAAA,EAAU,CAAC,CAAA,KAAM,KAAA,CAAM,IAAA,CAAK,GAAA,CAAI,EAAA,EAAI,IAAA,CAAK,GAAA,CAAI,EAAA,EAAI,CAAC,CAAC,CAAC,CAAA;AAAA,UACpD,MAAA;AAAA,UACA;AAAA;AAAA,OACF;AAAA,IAEJ;AAAA,IACA,KAAK,SAAA;AACH,MAAA,uBACE,GAAA;AAAA,QAACC,EAAA;AAAA,QAAA;AAAA,UACC,KAAA;AAAA,UACA,OAAO,IAAA,EAAc;AAAA,UACrB,QAAA,EAAU,KAAA;AAAA,UACV,MAAA;AAAA,UACA;AAAA;AAAA,OACF;AAAA,IAEJ,KAAK,MAAA,EAAQ;AACX,MAAA,MAAM,OAAA,GAAU,KAAA,CAAmD,IAAA,EAAM,SAAS,KAAK,EAAC;AACxF,MAAA,MAAM,SAAA,GAAY,KAAA,CAAc,IAAA,EAAM,SAAS,CAAA,KAAM,OAAA;AACrD,MAAA,MAAM,GAAA,GAAM,YAAYC,EAAA,GAAY,CAAA;AACpC,MAAA,uBACE,GAAA;AAAA,QAAC,GAAA;AAAA,QAAA;AAAA,UACC,KAAA;AAAA,UACA,OAAO,IAAA,EAAa;AAAA,UACpB,OAAA;AAAA,UACA,QAAA,EAAU,KAAA;AAAA,UACV,MAAA;AAAA,UACA,MAAM,SAAA,IAAa,IAAA;AAAA,UACnB;AAAA;AAAA,OACF;AAAA,IAEJ;AAAA,IACA,KAAK,QAAA;AACH,MAAA,uBACE,GAAA;AAAA,QAAC,gBAAA;AAAA,QAAA;AAAA,UACC,IAAA;AAAA,UACA,KAAA;AAAA,UACA,OAAO,IAAA,EAAa;AAAA,UACpB,KAAA;AAAA,UACA,MAAA;AAAA,UACA,IAAA,EAAM,IAAA;AAAA,UACN;AAAA;AAAA,OACF;AAAA,IAEJ,KAAK,OAAA;AACH,MAAA,uBACE,GAAA;AAAA,QAACC,EAAA;AAAA,QAAA;AAAA,UACC,KAAA;AAAA,UACA,OAAO,IAAA,EAAa;AAAA,UACpB,QAAA,EAAU,KAAA;AAAA,UACV,MAAA;AAAA,UACA;AAAA;AAAA,OACF;AAAA,IAEJ,KAAK,OAAA;AAAA,IACL,KAAK,QAAA;AAIH,MAAA,2BAAQ,UAAA,EAAA,EAAW,KAAA,EAAc,IAAA,EAAM,IAAA,CAAK,MAAM,WAAA,EAA0B,CAAA;AAAA,IAC9E,SAAS;AAGP,MAAA,MAAM,WAAA,GAAqB,IAAA;AAC3B,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,CAAA,iDAAA,EAAqD,YAAiC,IAAI,CAAA,CAAA;AAAA,OAC5F;AAAA,IACF;AAAA;AAEJ;AAIA,SAAS,UAAA,CAAW;AAAA,EAClB,KAAA;AAAA,EACA,IAAA;AAAA,EACA;AACF,CAAA,EAIG;AACD,EAAA,2BACG,CAAA,EAAA,EAAY,KAAA,EAAc,aACzB,QAAA,kBAAA,IAAA,CAAC,MAAA,EAAA,EAAK,WAAU,2BAAA,EAA4B,QAAA,EAAA;AAAA,IAAA,uBAAA;AAAA,IAAiB,IAAA;AAAA,IAAK;AAAA,GAAA,EAAC,CAAA,EACrE,CAAA;AAEJ;AAKA,SAAS,gBAAA,CAAiB;AAAA,EACxB,IAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,MAAA;AAAA,EACA,IAAA;AAAA,EACA;AACF,CAAA,EAQG;AACD,EAAA,MAAM,UAAA,GAAa,KAAA,CAAc,IAAA,EAAM,YAAY,CAAA,IAAK,GAAA;AACxD,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,CAAA,GAAI,SAAS,KAAK,CAAA;AACxC,EAAA,MAAM,KAAA,GAAQ,OAA6C,IAAI,CAAA;AAC/D,EAAA,MAAM,YAAA,GAAe,OAAO,KAAK,CAAA;AAEjC,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,KAAA,KAAU,aAAa,OAAA,EAAS;AAClC,MAAA,YAAA,CAAa,OAAA,GAAU,KAAA;AACvB,MAAA,QAAA,CAAS,KAAK,CAAA;AAAA,IAChB;AAAA,EACF,CAAA,EAAG,CAAC,KAAK,CAAC,CAAA;AAEV,EAAA,SAAA;AAAA,IACE,MAAM,MAAM;AACV,MAAA,IAAI,KAAA,CAAM,OAAA,EAAS,YAAA,CAAa,KAAA,CAAM,OAAO,CAAA;AAAA,IAC/C,CAAA;AAAA,IACA;AAAC,GACH;AAEA,EAAA,uBACE,GAAA;AAAA,IAACC,EAAA;AAAA,IAAA;AAAA,MACC,KAAA;AAAA,MACA,MAAA;AAAA,MACA,IAAA;AAAA,MACA,WAAA;AAAA,MACA,KAAA,EAAO,KAAA;AAAA,MACP,WAAA,EAAa,KAAA,CAAc,IAAA,EAAM,aAAa,CAAA;AAAA,MAC9C,SAAA,EAAW,KAAA,CAAc,IAAA,EAAM,WAAW,CAAA;AAAA,MAC1C,QAAA,EAAU,CAAC,IAAA,KAAS;AAClB,QAAA,QAAA,CAAS,IAAI,CAAA;AACb,QAAA,IAAI,KAAA,CAAM,OAAA,EAAS,YAAA,CAAa,KAAA,CAAM,OAAO,CAAA;AAC7C,QAAA,MAAM,SAAS,MAAM;AACnB,UAAA,YAAA,CAAa,OAAA,GAAU,IAAA;AACvB,UAAA,KAAA,CAAM,IAAI,CAAA;AAAA,QACZ,CAAA;AACA,QAAA,IAAI,UAAA,KAAe,GAAG,MAAA,EAAO;AAAA,aACxB,KAAA,CAAM,OAAA,GAAU,UAAA,CAAW,MAAA,EAAQ,UAAU,CAAA;AAAA,MACpD;AAAA;AAAA,GACF;AAEJ","file":"chunk-U3IYHIAE.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"]}
|