@weasel-js/labkit 1.5.1 → 1.6.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.
- package/README.md +3 -7
- package/dist/_dts/{CanvasStackContext-CJNMS6mz.d.ts → CanvasStackContext-DQgcH1ny.d.ts} +1 -1
- package/dist/_dts/LayerList.d-2c9yup5Y.d.ts +83 -0
- package/dist/_dts/{PrefsForm.d-DPy9I8It.d.ts → PrefsRow.d-DFpPIeIC.d.ts} +16 -1
- package/dist/_dts/{Select.d-Gwr8fLpK.d.ts → Select.d-BchGMC2Y.d.ts} +129 -12
- package/dist/_dts/{frac-B4okC2lX.d.ts → frac-DTRq7Xpb.d.ts} +31 -13
- package/dist/_dts/inDialog-D5ZKGjiD.d.ts +23 -0
- package/dist/_dts/{index-CSJoj4LC.d.ts → index-B0--8PEM.d.ts} +5 -4
- package/dist/_dts/index-DINmCuLA.d.ts +453 -0
- package/dist/_dts/number.d-OeQYQoUO.d.ts +28 -0
- package/dist/_dts/{types-DzoNY1pO.d.ts → types-B7y8hX1D.d.ts} +6 -6
- package/dist/_dts/{usePanZoom-Pzf9f2jh.d.ts → usePanZoom-Bq73pwFe.d.ts} +4 -47
- package/dist/_dts/{useTrialState-uLlmaccn.d.ts → useTrialState-CP4jfuKl.d.ts} +1 -1
- package/dist/canvas/index.d.ts +6 -5
- package/dist/canvas/index.js +1 -1
- package/dist/chrome/index.d.ts +5 -4
- package/dist/chrome/index.js +7 -7
- package/dist/chunk-2RVCB2PT.js +3 -0
- package/dist/chunk-2RVCB2PT.js.map +1 -0
- package/dist/{chunk-UN4U4P63.js → chunk-3FI7Y4R4.js} +9 -9
- package/dist/chunk-3FI7Y4R4.js.map +1 -0
- package/dist/{chunk-7PFH3CNM.js → chunk-AWH3SRPW.js} +31 -5
- package/dist/chunk-AWH3SRPW.js.map +1 -0
- package/dist/{chunk-ZDHY47EV.js → chunk-BVL7Y574.js} +25 -9
- package/dist/chunk-BVL7Y574.js.map +1 -0
- package/dist/{chunk-IHUVQ3TX.js → chunk-H5VIV7MQ.js} +31 -27
- package/dist/chunk-H5VIV7MQ.js.map +1 -0
- package/dist/chunk-HFRE4QQV.js +400 -0
- package/dist/chunk-HFRE4QQV.js.map +1 -0
- package/dist/{chunk-IXTQI7JX.js → chunk-MOLIUNVH.js} +4 -4
- package/dist/{chunk-IXTQI7JX.js.map → chunk-MOLIUNVH.js.map} +1 -1
- package/dist/{chunk-CEIUBPPW.js → chunk-TT5C5S6R.js} +3 -86
- package/dist/chunk-TT5C5S6R.js.map +1 -0
- package/dist/chunk-USO6TERP.js +90 -0
- package/dist/chunk-USO6TERP.js.map +1 -0
- package/dist/{chunk-PHK4W562.js → chunk-VQ47KWD5.js} +3 -3
- package/dist/{chunk-PHK4W562.js.map → chunk-VQ47KWD5.js.map} +1 -1
- package/dist/chunk-WUJXJC5O.js +14568 -0
- package/dist/chunk-WUJXJC5O.js.map +1 -0
- package/dist/{chunk-EHGVROVV.js → chunk-ZORVNOKX.js} +6 -25
- package/dist/chunk-ZORVNOKX.js.map +1 -0
- package/dist/config/index.d.ts +53 -11
- package/dist/config/index.js +5 -5
- package/dist/controls/index.d.ts +6 -3
- package/dist/controls/index.js +4 -4
- package/dist/dragdrop/index.d.ts +4 -3
- package/dist/index.d.ts +139 -114
- package/dist/index.js +470 -479
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +6 -35
- package/dist/layers/index.js +2 -3
- package/dist/loupe/index.d.ts +6 -5
- package/dist/passthrough/weasel-ui.d.ts +421 -215
- package/dist/passthrough/weasel-ui.js +1 -1
- package/dist/primitives/index.d.ts +2 -13
- package/dist/primitives/index.js +4 -4
- package/dist/state/index.d.ts +6 -5
- package/dist/state/index.js +5 -5
- package/dist/styles.css +410 -190
- package/dist/undo/index.d.ts +4 -3
- package/package.json +9 -13
- package/src/annotations/frac.test.ts +7 -0
- package/src/annotations/paint.test.ts +21 -0
- package/src/annotations/paint.ts +12 -0
- package/src/annotations/store.test.ts +31 -0
- package/src/annotations/types.ts +3 -2
- package/src/canvas/Stage.test.tsx +18 -0
- package/src/canvas/index.ts +0 -9
- package/src/chrome/labTypes.ts +1 -1
- package/src/chrome/regions/PaletteRegion.test.tsx +18 -0
- package/src/chrome/regions/PaletteRegion.tsx +5 -4
- package/src/chrome/regions/SidebarRegion.less +197 -0
- package/src/chrome/regions/SidebarRegion.test.tsx +20 -0
- package/src/chrome/regions/SidebarRegion.tsx +7 -2
- package/src/chrome/regions/TitleBarRegion.tsx +1 -1
- package/src/chrome/types.ts +13 -4
- package/src/config/autoConfig.ts +2 -2
- package/src/config/builder.ts +23 -5
- package/src/config/entry.test.ts +8 -0
- package/src/config/index.ts +2 -0
- package/src/config/rules.ts +2 -0
- package/src/config/sectionTree.test.ts +146 -0
- package/src/config/sectionTree.ts +124 -0
- package/src/config/types.ts +4 -4
- package/src/controls/AutoControls.stories.tsx +6 -6
- package/src/controls/ControlPanel.less +27 -0
- package/src/controls/ControlPanel.stories.tsx +95 -1
- package/src/controls/ControlPanel.test.tsx +82 -112
- package/src/controls/ControlPanel.tsx +159 -396
- package/src/controls/inDialog.test.tsx +82 -0
- package/src/controls/inDialog.tsx +46 -0
- package/src/controls/index.ts +1 -0
- package/src/index.test.ts +0 -2
- package/src/index.ts +14 -35
- package/src/instrument/types.ts +5 -2
- package/src/lab/Lab.nebula.test.tsx +31 -0
- package/src/lab/Lab.tsx +57 -18
- package/src/lab/LabBoundary.test.tsx +66 -0
- package/src/lab/LabBoundary.tsx +56 -0
- package/src/lab/LabHeader.tsx +4 -23
- package/src/lab/LabRoot.tsx +2 -3
- package/src/lab/LabShell.less +0 -1
- package/src/lab/LabShell.stories.tsx +15 -0
- package/src/lab/LabShell.test.tsx +18 -0
- package/src/lab/LabShell.tsx +14 -1
- package/src/lab/LabSwitcher.less +1 -1
- package/src/lab/LabSwitcher.test.tsx +41 -0
- package/src/lab/LabSwitcher.tsx +30 -8
- package/src/lab/index.ts +1 -0
- package/src/layers/AGENTS.md +18 -74
- package/src/layers/index.ts +4 -2
- package/src/passthrough/weasel-ui.ts +25 -10
- package/src/primitives/FpsMeter.less +6 -1
- package/src/primitives/FpsMeter.test.tsx +12 -4
- package/src/primitives/FpsMeter.tsx +6 -1
- package/src/primitives/JobProgress.stories.tsx +1 -1
- package/src/primitives/StatusBar.less +6 -0
- package/src/primitives/index.ts +0 -2
- package/src/specimen/Specimen.tsx +42 -56
- package/src/state/types.ts +8 -2
- package/src/state/view.test.ts +4 -4
- package/src/styles.less +4 -2
- package/src/theme/Interstellar.stories.tsx +8 -9
- package/src/theme/base.less +3 -20
- package/src/theme/interstellar.test.ts +6 -3
- package/src/theme/interstellar.theme.json +0 -56
- package/src/theme/interstellar.ts +2 -2
- package/src/trial/Trial.canvas.test.tsx +9 -5
- package/src/trial/Trial.config.test.tsx +1 -1
- package/src/trial/Trial.tsx +30 -8
- package/dist/_dts/index-BNdz-tNd.d.ts +0 -423
- package/dist/chunk-7PFH3CNM.js.map +0 -1
- package/dist/chunk-CEIUBPPW.js.map +0 -1
- package/dist/chunk-EH7UEQV7.js +0 -590
- package/dist/chunk-EH7UEQV7.js.map +0 -1
- package/dist/chunk-EHGVROVV.js.map +0 -1
- package/dist/chunk-IHUVQ3TX.js.map +0 -1
- package/dist/chunk-MDVTK5U3.js +0 -170
- package/dist/chunk-MDVTK5U3.js.map +0 -1
- package/dist/chunk-PJ7WEQBJ.js +0 -18
- package/dist/chunk-PJ7WEQBJ.js.map +0 -1
- package/dist/chunk-T3EC2DXD.js +0 -12628
- package/dist/chunk-T3EC2DXD.js.map +0 -1
- package/dist/chunk-UN4U4P63.js.map +0 -1
- package/dist/chunk-ZDHY47EV.js.map +0 -1
- package/dist/ui/layers/index.d.ts +0 -66
- package/dist/ui/layers/index.js +0 -3
- package/dist/ui/layers/index.js.map +0 -1
- package/src/canvas/useOrbit.test.ts +0 -167
- package/src/canvas/useOrbit.ts +0 -156
- package/src/lab/useSystemMode.ts +0 -30
- package/src/layers/LayerList.less +0 -93
- package/src/layers/LayerList.stories.tsx +0 -66
- package/src/layers/LayerList.test.tsx +0 -232
- package/src/layers/LayerList.tsx +0 -268
- package/src/primitives/Sidebar.less +0 -112
- package/src/primitives/Sidebar.stories.tsx +0 -38
- package/src/primitives/Sidebar.test.tsx +0 -74
- package/src/primitives/Sidebar.tsx +0 -35
- package/src/ui/layers/index.ts +0 -5
|
@@ -1,29 +1,23 @@
|
|
|
1
|
-
import { isBuiltinToolPref } from '@weasel-js/core';
|
|
1
|
+
import { getAlpha01, isBuiltinToolPref, toHex8, withAlpha01 } from '@weasel-js/core';
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
ColorRow,
|
|
3
|
+
DialogRow,
|
|
5
4
|
isPrefLeaf,
|
|
6
|
-
|
|
5
|
+
ListEditor,
|
|
7
6
|
type PrefGroup,
|
|
8
7
|
type PrefLeaf,
|
|
9
|
-
type PrefNumber,
|
|
10
|
-
type PrefNumberFormat,
|
|
11
|
-
type PrefNumberUnit,
|
|
12
8
|
type PropertyAlign,
|
|
9
|
+
type PropertyControlProps,
|
|
10
|
+
PropertyControl,
|
|
13
11
|
type PropertyDensity,
|
|
12
|
+
PropertyField,
|
|
14
13
|
PropertyGroup,
|
|
15
14
|
PropertyList,
|
|
16
15
|
type PropertyListPack,
|
|
16
|
+
PropertyPanel,
|
|
17
17
|
PropertyRow,
|
|
18
18
|
type PropertyRowLayout,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
SelectRow,
|
|
22
|
-
SliderRow,
|
|
23
|
-
Switch,
|
|
24
|
-
SwitchRow,
|
|
25
|
-
TextRow,
|
|
26
|
-
ToggleRow,
|
|
19
|
+
prefFieldProps,
|
|
20
|
+
type StanceProps,
|
|
27
21
|
} from '@weasel-js/ui';
|
|
28
22
|
import { Fragment, type ReactNode, useEffect, useMemo, useRef, useState } from 'react';
|
|
29
23
|
import { auto as autoValue } from '../config/auto';
|
|
@@ -32,6 +26,7 @@ import { fromConfigFields } from '../config/fromConfigField';
|
|
|
32
26
|
import { schemaNodeAtPath, valueAtPath } from '../config/path';
|
|
33
27
|
import type { ControlRenderer, ResolvedConfig, SectionSpec } from '../config/types';
|
|
34
28
|
import { isLeafVisible } from '../config/visible';
|
|
29
|
+
import { summarizeValue } from './inDialog';
|
|
35
30
|
import type { ConfigField } from './types';
|
|
36
31
|
|
|
37
32
|
/** How a panel packs its rows into the two-column property grid.
|
|
@@ -62,7 +57,7 @@ function sectionRows(section: SectionSpec, rows: Rows): Rows {
|
|
|
62
57
|
};
|
|
63
58
|
}
|
|
64
59
|
|
|
65
|
-
export interface ControlPanelProps<TC extends Record<string, unknown>> {
|
|
60
|
+
export interface ControlPanelProps<TC extends Record<string, unknown>> extends StanceProps {
|
|
66
61
|
/** The instrument's resolved config schema. */
|
|
67
62
|
schema?: ResolvedConfig;
|
|
68
63
|
/** @deprecated Pass `schema`. A field list is adapted into one internally. */
|
|
@@ -106,14 +101,17 @@ export interface ControlPanelProps<TC extends Record<string, unknown>> {
|
|
|
106
101
|
/** A fold moved. The key is a section's label — prefixed by its group's
|
|
107
102
|
* dotted path when the section sits inside one — or a group's own path. */
|
|
108
103
|
onCollapse?: (key: string, collapsed: boolean) => void;
|
|
109
|
-
/** Dotted paths currently unpinned. A row in this set
|
|
110
|
-
*
|
|
111
|
-
* `setConfig`. Omitted, the panel keeps the set itself: the
|
|
112
|
-
* and the sentinel never reaches `setConfig`, which would otherwise
|
|
113
|
-
* as the row's value. */
|
|
104
|
+
/** Dotted paths currently unpinned. A row in this set hides its control and
|
|
105
|
+
* reads out `auto`, and clicking its label writes the sentinel back through
|
|
106
|
+
* `setConfig`. Omitted, the panel keeps the set itself: the labels still
|
|
107
|
+
* toggle and the sentinel never reaches `setConfig`, which would otherwise
|
|
108
|
+
* store it as the row's value. */
|
|
114
109
|
auto?: ReadonlySet<string>;
|
|
115
110
|
/** Draw leaves marked `hidden`. */
|
|
116
111
|
showHidden?: boolean;
|
|
112
|
+
/** Heads the panel. With `title`, `stance` or `tone` given, the rows sit in a
|
|
113
|
+
* `<PropertyPanel>` carrying them; with none, they are the bare list. */
|
|
114
|
+
title?: ReactNode;
|
|
117
115
|
className?: string;
|
|
118
116
|
}
|
|
119
117
|
|
|
@@ -137,6 +135,9 @@ export function ControlPanel<TC extends Record<string, unknown>>({
|
|
|
137
135
|
onCollapse,
|
|
138
136
|
auto: given,
|
|
139
137
|
showHidden = false,
|
|
138
|
+
title,
|
|
139
|
+
stance,
|
|
140
|
+
tone,
|
|
140
141
|
className,
|
|
141
142
|
}: ControlPanelProps<TC>) {
|
|
142
143
|
const resolved = useMemo(() => schema ?? fromConfigFields(fields ?? []), [schema, fields]);
|
|
@@ -170,28 +171,6 @@ export function ControlPanel<TC extends Record<string, unknown>>({
|
|
|
170
171
|
[resolved, config, auto],
|
|
171
172
|
);
|
|
172
173
|
|
|
173
|
-
const listRef = useRef<HTMLDivElement>(null);
|
|
174
|
-
const toggles = useRef(new Map<string, () => void>());
|
|
175
|
-
// Capture phase, because `PropertyRow` is a <label>: a bubbled handler runs
|
|
176
|
-
// after the browser has already begun a range drag or a native control's
|
|
177
|
-
// activation, so the shift-click moves the very value it was meant to unpin.
|
|
178
|
-
useEffect(() => {
|
|
179
|
-
const host = listRef.current;
|
|
180
|
-
if (!host) return;
|
|
181
|
-
const onDown = (e: PointerEvent) => {
|
|
182
|
-
if (!e.shiftKey) return;
|
|
183
|
-
const row = (e.target as HTMLElement | null)?.closest('[data-auto-path]');
|
|
184
|
-
const path = row?.getAttribute('data-auto-path');
|
|
185
|
-
const toggle = path ? toggles.current.get(path) : undefined;
|
|
186
|
-
if (!toggle) return;
|
|
187
|
-
e.preventDefault();
|
|
188
|
-
e.stopPropagation();
|
|
189
|
-
toggle();
|
|
190
|
-
};
|
|
191
|
-
host.addEventListener('pointerdown', onDown, true);
|
|
192
|
-
return () => host.removeEventListener('pointerdown', onDown, true);
|
|
193
|
-
}, []);
|
|
194
|
-
|
|
195
174
|
const gridPack = gridOf(pack);
|
|
196
175
|
// A section that declares how it opens is foldable whether or not the lab
|
|
197
176
|
// asked for folds — there is nothing else for the declaration to mean.
|
|
@@ -230,7 +209,6 @@ export function ControlPanel<TC extends Record<string, unknown>>({
|
|
|
230
209
|
layout={rows.layout}
|
|
231
210
|
auto={auto}
|
|
232
211
|
setRowAuto={setRowAuto}
|
|
233
|
-
toggles={toggles.current}
|
|
234
212
|
/>
|
|
235
213
|
);
|
|
236
214
|
}
|
|
@@ -238,7 +216,16 @@ export function ControlPanel<TC extends Record<string, unknown>>({
|
|
|
238
216
|
// empty `PrefGroup.name` — so it contributes its rows and no chrome.
|
|
239
217
|
if (found.name === '') return <Fragment key={path}>{body(found, path, rows)}</Fragment>;
|
|
240
218
|
return (
|
|
241
|
-
<PropertyGroup
|
|
219
|
+
<PropertyGroup
|
|
220
|
+
key={path}
|
|
221
|
+
title={found.name}
|
|
222
|
+
description={found.description}
|
|
223
|
+
// A nested group is a block of rows, not a control: half of a
|
|
224
|
+
// two-column grid leaves it laying its own rows out inside a cell,
|
|
225
|
+
// which overlaps them.
|
|
226
|
+
span
|
|
227
|
+
{...fold(path, undefined, rows.grid)}
|
|
228
|
+
>
|
|
242
229
|
{body(found, path, rows)}
|
|
243
230
|
</PropertyGroup>
|
|
244
231
|
);
|
|
@@ -289,6 +276,9 @@ export function ControlPanel<TC extends Record<string, unknown>>({
|
|
|
289
276
|
shown={shown}
|
|
290
277
|
setConfig={setConfig}
|
|
291
278
|
layout={rows.layout}
|
|
279
|
+
// A segmented cell carries every choice at once, so a pair holding
|
|
280
|
+
// one needs the whole row the way a segmented row of its own does.
|
|
281
|
+
span={cells.some(({ leaf }) => extra<string>(leaf, 'control') === 'radio')}
|
|
292
282
|
/>,
|
|
293
283
|
);
|
|
294
284
|
i = j;
|
|
@@ -323,17 +313,21 @@ export function ControlPanel<TC extends Record<string, unknown>>({
|
|
|
323
313
|
);
|
|
324
314
|
};
|
|
325
315
|
|
|
316
|
+
const list = (
|
|
317
|
+
<PropertyList
|
|
318
|
+
pack={gridPack}
|
|
319
|
+
density={density}
|
|
320
|
+
align={align}
|
|
321
|
+
className={className ? `lk-control-panel ${className}` : 'lk-control-panel'}
|
|
322
|
+
>
|
|
323
|
+
{body(resolved.group, '', { pack, layout, grid: gridPack })}
|
|
324
|
+
</PropertyList>
|
|
325
|
+
);
|
|
326
|
+
if (title === undefined && stance === undefined && tone === undefined) return list;
|
|
326
327
|
return (
|
|
327
|
-
<
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
density={density}
|
|
331
|
-
align={align}
|
|
332
|
-
className={className ? `lk-control-panel ${className}` : 'lk-control-panel'}
|
|
333
|
-
>
|
|
334
|
-
{body(resolved.group, '', { pack, layout, grid: gridPack })}
|
|
335
|
-
</PropertyList>
|
|
336
|
-
</div>
|
|
328
|
+
<PropertyPanel title={title} stance={stance} tone={tone} density={density}>
|
|
329
|
+
{list}
|
|
330
|
+
</PropertyPanel>
|
|
337
331
|
);
|
|
338
332
|
}
|
|
339
333
|
|
|
@@ -357,7 +351,6 @@ interface ControlRowProps<TC extends Record<string, unknown>> {
|
|
|
357
351
|
layout?: PropertyRowLayout;
|
|
358
352
|
auto?: ReadonlySet<string>;
|
|
359
353
|
setRowAuto: (path: string, next: boolean, value: unknown) => void;
|
|
360
|
-
toggles: Map<string, () => void>;
|
|
361
354
|
}
|
|
362
355
|
|
|
363
356
|
/** Whether this leaf draws as a slider, mirroring the condition the `number`
|
|
@@ -390,7 +383,6 @@ function ControlRow<TC extends Record<string, unknown>>({
|
|
|
390
383
|
layout,
|
|
391
384
|
auto,
|
|
392
385
|
setRowAuto,
|
|
393
|
-
toggles,
|
|
394
386
|
}: ControlRowProps<TC>) {
|
|
395
387
|
const write = (value: unknown): void => setConfig(path, value);
|
|
396
388
|
const fallback = extra<unknown>(leaf, 'default');
|
|
@@ -408,30 +400,9 @@ function ControlRow<TC extends Record<string, unknown>>({
|
|
|
408
400
|
const value = resolvedValue ?? pinned;
|
|
409
401
|
const setAuto = (next: boolean): void => setRowAuto(path, next, value);
|
|
410
402
|
const onAutoChange = canAuto ? setAuto : undefined;
|
|
411
|
-
//
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
const readsItsOwnValue = !isSliderLeaf(leaf);
|
|
415
|
-
// A readout says what the control would say, so it reads in the display
|
|
416
|
-
// unit: a radian in a row that edits degrees is a different number.
|
|
417
|
-
const readoutValue =
|
|
418
|
-
leaf.kind === 'number' && typeof resolvedValue === 'number'
|
|
419
|
-
? numberField(leaf, resolvedValue).shown
|
|
420
|
-
: resolvedValue;
|
|
421
|
-
const autoReadout = !isAutoRow
|
|
422
|
-
? undefined
|
|
423
|
-
: resolvedValue === undefined || readsItsOwnValue
|
|
424
|
-
? 'auto'
|
|
425
|
-
: `auto · ${String(readoutValue)}`;
|
|
426
|
-
const autoProps = { auto: isAutoRow, onAutoChange, 'data-auto-path': canAuto ? path : undefined };
|
|
427
|
-
|
|
428
|
-
useEffect(() => {
|
|
429
|
-
if (!canAuto) return;
|
|
430
|
-
toggles.set(path, () => setAuto(!isAutoRow));
|
|
431
|
-
return () => {
|
|
432
|
-
toggles.delete(path);
|
|
433
|
-
};
|
|
434
|
-
});
|
|
403
|
+
// An auto row hides its control, so the word is all the row has to say.
|
|
404
|
+
const autoReadout = isAutoRow ? 'auto' : undefined;
|
|
405
|
+
const autoProps = { auto: isAutoRow, onAutoChange };
|
|
435
406
|
|
|
436
407
|
// Most specific wins, and within a tier the lab's entry beats the
|
|
437
408
|
// instrument's: controls[path] -> node .render -> controls[kind] -> built-in.
|
|
@@ -442,142 +413,83 @@ function ControlRow<TC extends Record<string, unknown>>({
|
|
|
442
413
|
|
|
443
414
|
const label = leaf.name;
|
|
444
415
|
const description = leaf.description;
|
|
445
|
-
const read = <T,>(): T => value as T;
|
|
446
416
|
// `auto` gives the whole width to the controls that read badly at half of a
|
|
447
417
|
// sidebar's: free text, a slider track, a segmented toggle. `pairs` doesn't.
|
|
448
418
|
const wide = pack === 'auto';
|
|
419
|
+
const row = { label, description, layout, readout: autoReadout, ...autoProps };
|
|
449
420
|
|
|
450
|
-
if (
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
unit={suffix}
|
|
468
|
-
onChange={(n) => write(field.store(n))}
|
|
469
|
-
readout={autoReadout}
|
|
470
|
-
layout={layout}
|
|
471
|
-
span={wide}
|
|
472
|
-
description={description}
|
|
473
|
-
{...autoProps}
|
|
421
|
+
if (leaf.kind === 'list') {
|
|
422
|
+
const entries = Array.isArray(value) ? (value as string[]) : [];
|
|
423
|
+
return (
|
|
424
|
+
<DialogRow
|
|
425
|
+
label={label}
|
|
426
|
+
summary={summarizeValue(entries)}
|
|
427
|
+
readout={autoReadout}
|
|
428
|
+
layout={layout}
|
|
429
|
+
description={description}
|
|
430
|
+
{...autoProps}
|
|
431
|
+
>
|
|
432
|
+
{() => (
|
|
433
|
+
<ListEditor
|
|
434
|
+
aria-label={label}
|
|
435
|
+
value={entries}
|
|
436
|
+
onChange={write}
|
|
437
|
+
placeholder={extra<string>(leaf, 'placeholder')}
|
|
474
438
|
/>
|
|
475
|
-
)
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
value={field.shown}
|
|
484
|
-
min={field.min}
|
|
485
|
-
max={field.max}
|
|
486
|
-
step={field.step}
|
|
487
|
-
unit={suffix}
|
|
488
|
-
onChange={(n) => write(field.store(n))}
|
|
489
|
-
readout={autoReadout}
|
|
490
|
-
layout={layout}
|
|
491
|
-
description={description}
|
|
492
|
-
{...autoProps}
|
|
493
|
-
/>
|
|
494
|
-
);
|
|
495
|
-
}
|
|
496
|
-
case 'boolean': {
|
|
497
|
-
const Row = extra<string>(leaf, 'control') === 'switch' ? SwitchRow : CheckboxRow;
|
|
498
|
-
return (
|
|
499
|
-
<Row
|
|
500
|
-
label={label}
|
|
501
|
-
value={read<boolean>()}
|
|
502
|
-
onChange={write}
|
|
503
|
-
readout={autoReadout}
|
|
504
|
-
layout={layout}
|
|
505
|
-
description={description}
|
|
506
|
-
{...autoProps}
|
|
507
|
-
/>
|
|
508
|
-
);
|
|
509
|
-
}
|
|
510
|
-
case 'enum': {
|
|
511
|
-
const options = extra<readonly { value: string; label: string }[]>(leaf, 'options') ?? [];
|
|
512
|
-
const segmented = extra<string>(leaf, 'control') === 'radio';
|
|
513
|
-
const Row = segmented ? ToggleRow : SelectRow;
|
|
514
|
-
return (
|
|
515
|
-
<Row
|
|
516
|
-
label={label}
|
|
517
|
-
value={read<string>()}
|
|
518
|
-
options={options}
|
|
519
|
-
onChange={write}
|
|
520
|
-
readout={autoReadout}
|
|
521
|
-
layout={layout}
|
|
522
|
-
span={segmented && wide}
|
|
523
|
-
description={description}
|
|
524
|
-
{...autoProps}
|
|
525
|
-
/>
|
|
526
|
-
);
|
|
527
|
-
}
|
|
439
|
+
)}
|
|
440
|
+
</DialogRow>
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const field = labField(leaf, value, write);
|
|
445
|
+
if (field === null) return <UnwiredRow label={label} kind={leaf.kind} description={description} />;
|
|
446
|
+
switch (field.kind) {
|
|
528
447
|
case 'string':
|
|
529
|
-
return
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
448
|
+
return <DebouncedTextRow leaf={leaf} field={field} span={wide} {...row} />;
|
|
449
|
+
case 'number':
|
|
450
|
+
return <PropertyField {...field} {...row} span={field.control === 'slider' && wide} />;
|
|
451
|
+
case 'enum':
|
|
452
|
+
return <PropertyField {...field} {...row} span={field.control === 'radio' && wide} />;
|
|
453
|
+
default:
|
|
454
|
+
return <PropertyField {...field} {...row} />;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* A leaf as the field it draws, with the labkit-only extras `prefFieldProps`
|
|
460
|
+
* knows nothing of. `null` for a kind the panel declines: a paint, which a
|
|
461
|
+
* hex swatch would flatten to a solid, and an object, which a flat row would
|
|
462
|
+
* write one field of. Override with `render` to edit either.
|
|
463
|
+
*/
|
|
464
|
+
function labField(
|
|
465
|
+
leaf: PrefLeaf,
|
|
466
|
+
value: unknown,
|
|
467
|
+
write: (value: unknown) => void,
|
|
468
|
+
): PropertyControlProps | null {
|
|
469
|
+
if (!isBuiltinToolPref(leaf) || leaf.kind === 'paint' || leaf.kind === 'object') return null;
|
|
470
|
+
const field = prefFieldProps(leaf, { value, setValue: write });
|
|
471
|
+
if (field === null) return null;
|
|
472
|
+
switch (field.kind) {
|
|
473
|
+
case 'number':
|
|
474
|
+
// `prefFieldProps` clamps what it stores, so an instrument is never
|
|
475
|
+
// handed a value outside the range it asked for.
|
|
476
|
+
return {
|
|
477
|
+
...field,
|
|
478
|
+
control: isSliderLeaf(leaf) ? 'slider' : 'input',
|
|
479
|
+
unit: extra<string>(leaf, 'suffix') ?? field.unit,
|
|
553
480
|
};
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
return (
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
}
|
|
567
|
-
case 'paint':
|
|
568
|
-
case 'object':
|
|
569
|
-
// Declined: a hex swatch would write a solid over a gradient, and a flat
|
|
570
|
-
// row would write one field into a half-built object. Override with
|
|
571
|
-
// `render` to edit either.
|
|
572
|
-
return <UnwiredRow label={label} kind={leaf.kind} description={description} />;
|
|
573
|
-
default: {
|
|
574
|
-
// Not reachable while every built-in kind has an arm — and a new kind
|
|
575
|
-
// that lacks one is a compile error here, never a blank row.
|
|
576
|
-
const _exhaustive: never = leaf;
|
|
577
|
-
throw new Error(
|
|
578
|
-
`ControlPanel: no control for built-in pref kind "${(_exhaustive as { kind: string }).kind}"`,
|
|
579
|
-
);
|
|
580
|
-
}
|
|
481
|
+
case 'boolean':
|
|
482
|
+
return { ...field, control: extra<string>(leaf, 'control') === 'switch' ? 'switch' : 'checkbox' };
|
|
483
|
+
case 'enum':
|
|
484
|
+
return { ...field, control: extra<string>(leaf, 'control') === 'radio' ? 'radio' : 'select' };
|
|
485
|
+
case 'string':
|
|
486
|
+
return {
|
|
487
|
+
...field,
|
|
488
|
+
placeholder: extra<string>(leaf, 'placeholder'),
|
|
489
|
+
maxLength: extra<number>(leaf, 'maxLength'),
|
|
490
|
+
};
|
|
491
|
+
default:
|
|
492
|
+
return field;
|
|
581
493
|
}
|
|
582
494
|
}
|
|
583
495
|
|
|
@@ -602,125 +514,25 @@ function UnwiredRow({
|
|
|
602
514
|
/** A text row whose writes are debounced — see {@link useDebouncedText}. */
|
|
603
515
|
function DebouncedTextRow({
|
|
604
516
|
leaf,
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
write,
|
|
608
|
-
layout,
|
|
609
|
-
span,
|
|
610
|
-
description,
|
|
611
|
-
readout,
|
|
612
|
-
auto,
|
|
613
|
-
onAutoChange,
|
|
614
|
-
'data-auto-path': autoPath,
|
|
517
|
+
field,
|
|
518
|
+
...row
|
|
615
519
|
}: {
|
|
616
520
|
leaf: PrefLeaf;
|
|
521
|
+
field: PropertyControlProps & { kind: 'string' };
|
|
617
522
|
label: string;
|
|
618
|
-
value: string;
|
|
619
|
-
write: (value: unknown) => void;
|
|
620
523
|
layout?: PropertyRowLayout;
|
|
621
524
|
span?: boolean;
|
|
622
525
|
description?: string;
|
|
623
526
|
readout?: ReactNode;
|
|
624
527
|
auto?: boolean;
|
|
625
528
|
onAutoChange?: (next: boolean) => void;
|
|
626
|
-
'data-auto-path'?: string;
|
|
627
529
|
}) {
|
|
628
530
|
const text = useDebouncedText(
|
|
629
|
-
value,
|
|
630
|
-
|
|
531
|
+
field.value ?? '',
|
|
532
|
+
field.onChange,
|
|
631
533
|
extra<number>(leaf, 'debounceMs') ?? 150,
|
|
632
534
|
);
|
|
633
|
-
|
|
634
|
-
return (
|
|
635
|
-
<TextRow
|
|
636
|
-
label={label}
|
|
637
|
-
readout={readout}
|
|
638
|
-
layout={layout}
|
|
639
|
-
span={span}
|
|
640
|
-
description={description}
|
|
641
|
-
auto={auto}
|
|
642
|
-
onAutoChange={onAutoChange}
|
|
643
|
-
data-auto-path={autoPath}
|
|
644
|
-
value={text.local}
|
|
645
|
-
placeholder={extra<string>(leaf, 'placeholder')}
|
|
646
|
-
maxLength={extra<number>(leaf, 'maxLength')}
|
|
647
|
-
onChange={text.type}
|
|
648
|
-
/>
|
|
649
|
-
);
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
/**
|
|
653
|
-
* A number leaf's field in the unit it is edited in: what the control shows,
|
|
654
|
-
* the bounds it shows them against, and what a typed number stores once it is
|
|
655
|
-
* clamped and converted back. Without a `unit` this is what the leaf declares,
|
|
656
|
-
* unchanged.
|
|
657
|
-
*/
|
|
658
|
-
function numberField(
|
|
659
|
-
leaf: PrefLeaf,
|
|
660
|
-
value: number,
|
|
661
|
-
): {
|
|
662
|
-
unit: PrefNumberUnit | undefined;
|
|
663
|
-
min: number | undefined;
|
|
664
|
-
max: number | undefined;
|
|
665
|
-
step: number | undefined;
|
|
666
|
-
shown: number;
|
|
667
|
-
store: (shown: number) => number;
|
|
668
|
-
} {
|
|
669
|
-
const unit = extra<PrefNumberUnit>(leaf, 'unit');
|
|
670
|
-
// `min`, `max` and `step` are declared in the stored unit alongside the
|
|
671
|
-
// value, so they convert with it — a leaf storing radians and showing
|
|
672
|
-
// degrees would otherwise clamp typed degrees against 0..6.28.
|
|
673
|
-
const bounds = unit
|
|
674
|
-
? prefDisplayBounds(leaf as PrefNumber)
|
|
675
|
-
: {
|
|
676
|
-
min: extra<number>(leaf, 'min'),
|
|
677
|
-
max: extra<number>(leaf, 'max'),
|
|
678
|
-
step: extra<number>(leaf, 'step'),
|
|
679
|
-
};
|
|
680
|
-
const lo = bounds.min ?? Number.NEGATIVE_INFINITY;
|
|
681
|
-
const hi = bounds.max ?? Number.POSITIVE_INFINITY;
|
|
682
|
-
return {
|
|
683
|
-
unit,
|
|
684
|
-
min: bounds.min,
|
|
685
|
-
max: bounds.max,
|
|
686
|
-
step: bounds.step,
|
|
687
|
-
shown: unit ? unit.toDisplay(value) : value,
|
|
688
|
-
store: (shown) => {
|
|
689
|
-
const clamped = Math.min(hi, Math.max(lo, shown));
|
|
690
|
-
return unit ? unit.fromDisplay(clamped) : clamped;
|
|
691
|
-
},
|
|
692
|
-
};
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
/** `#rgb`, `#rgba`, `#rrggbb` or `#rrggbbaa` as the `#rrggbb` an
|
|
696
|
-
* `<input type="color">` can hold and the 0..1 alpha beside it. Anything else
|
|
697
|
-
* reads as opaque black, which is what the input would show for it anyway. */
|
|
698
|
-
function splitHexAlpha(value: string): { rgb: string; alpha: number } {
|
|
699
|
-
const digits = /^#([0-9a-f]{3,8})$/i.exec(value?.trim() ?? '')?.[1];
|
|
700
|
-
const twice = (s: string): string =>
|
|
701
|
-
s
|
|
702
|
-
.split('')
|
|
703
|
-
.map((c) => c + c)
|
|
704
|
-
.join('');
|
|
705
|
-
const byte = (s: string): number => Number.parseInt(s, 16) / 255;
|
|
706
|
-
switch (digits?.length) {
|
|
707
|
-
case 3:
|
|
708
|
-
return { rgb: `#${twice(digits)}`, alpha: 1 };
|
|
709
|
-
case 4:
|
|
710
|
-
return { rgb: `#${twice(digits.slice(0, 3))}`, alpha: byte(twice(digits.slice(3))) };
|
|
711
|
-
case 6:
|
|
712
|
-
return { rgb: `#${digits}`, alpha: 1 };
|
|
713
|
-
case 8:
|
|
714
|
-
return { rgb: `#${digits.slice(0, 6)}`, alpha: byte(digits.slice(6)) };
|
|
715
|
-
default:
|
|
716
|
-
return { rgb: '#000000', alpha: 1 };
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
/** A `#rrggbb` and a 0..1 alpha back into the `#rrggbbaa` the config holds. */
|
|
721
|
-
function joinHexAlpha(rgb: string, alpha: number): string {
|
|
722
|
-
const byte = Math.max(0, Math.min(255, Math.round(alpha * 255)));
|
|
723
|
-
return `${rgb}${byte.toString(16).padStart(2, '0')}`;
|
|
535
|
+
return <PropertyField {...field} {...row} value={text.local} onChange={text.type} />;
|
|
724
536
|
}
|
|
725
537
|
|
|
726
538
|
/** One cell of a paired row, with its path and the leaf it draws. */
|
|
@@ -735,8 +547,8 @@ interface PairCellSpec {
|
|
|
735
547
|
*
|
|
736
548
|
* The cells are bare controls: a row holds one label column, so each leaf's
|
|
737
549
|
* own name stays with its control as the accessible name and its description
|
|
738
|
-
* as a title. For the same reason the row
|
|
739
|
-
* path, and this row has several.
|
|
550
|
+
* as a title. For the same reason the row's label does not toggle auto — it
|
|
551
|
+
* names one path, and this row has several.
|
|
740
552
|
*/
|
|
741
553
|
function PairedRow<TC extends Record<string, unknown>>({
|
|
742
554
|
label,
|
|
@@ -744,22 +556,29 @@ function PairedRow<TC extends Record<string, unknown>>({
|
|
|
744
556
|
shown,
|
|
745
557
|
setConfig,
|
|
746
558
|
layout,
|
|
559
|
+
span,
|
|
747
560
|
}: {
|
|
748
561
|
label: string;
|
|
749
562
|
cells: readonly PairCellSpec[];
|
|
750
563
|
shown: TC;
|
|
751
564
|
setConfig: (path: string, value: unknown) => void;
|
|
752
565
|
layout?: PropertyRowLayout;
|
|
566
|
+
span?: boolean;
|
|
753
567
|
}) {
|
|
754
568
|
return (
|
|
755
|
-
<PropertyRow group label={label} layout={layout}>
|
|
569
|
+
<PropertyRow group span={span} label={label} layout={layout}>
|
|
756
570
|
{cells.map(({ path, leaf }) => (
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
571
|
+
// The row is named by the pair, so a cell that shares it says which
|
|
572
|
+
// knob it is — without a caption a paired switch reads only to a
|
|
573
|
+
// screen reader. A cell whose leaf is deliberately unnamed keeps none.
|
|
574
|
+
<span key={path} className="lk-pair-cell" title={leaf.description || undefined}>
|
|
575
|
+
{leaf.name ? <span className="lk-pair-label">{leaf.name}</span> : null}
|
|
576
|
+
<PairCell
|
|
577
|
+
leaf={leaf}
|
|
578
|
+
value={valueAtPath(shown, path) ?? extra<unknown>(leaf, 'default')}
|
|
579
|
+
write={(value) => setConfig(path, value)}
|
|
580
|
+
/>
|
|
581
|
+
</span>
|
|
763
582
|
))}
|
|
764
583
|
</PropertyRow>
|
|
765
584
|
);
|
|
@@ -776,91 +595,35 @@ function PairCell({
|
|
|
776
595
|
value: unknown;
|
|
777
596
|
write: (value: unknown) => void;
|
|
778
597
|
}) {
|
|
779
|
-
const name = leaf.name;
|
|
780
|
-
const title = leaf.description;
|
|
781
598
|
const text = useDebouncedText(
|
|
782
599
|
typeof value === 'string' ? value : '',
|
|
783
600
|
write,
|
|
784
601
|
extra<number>(leaf, 'debounceMs') ?? 150,
|
|
785
602
|
);
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
<input
|
|
791
|
-
type="number"
|
|
792
|
-
aria-label={name}
|
|
793
|
-
title={title}
|
|
794
|
-
value={field.shown}
|
|
795
|
-
min={field.min}
|
|
796
|
-
max={field.max}
|
|
797
|
-
step={field.step}
|
|
798
|
-
onChange={(e) => {
|
|
799
|
-
const n = Number(e.target.value);
|
|
800
|
-
if (e.target.value !== '' && Number.isFinite(n)) write(field.store(n));
|
|
801
|
-
}}
|
|
802
|
-
/>
|
|
803
|
-
);
|
|
804
|
-
}
|
|
805
|
-
case 'boolean':
|
|
806
|
-
return extra<string>(leaf, 'control') === 'switch' ? (
|
|
807
|
-
<Switch aria-label={name} isSelected={value === true} onChange={write} />
|
|
808
|
-
) : (
|
|
809
|
-
<input
|
|
810
|
-
type="checkbox"
|
|
811
|
-
aria-label={name}
|
|
812
|
-
title={title}
|
|
813
|
-
checked={value === true}
|
|
814
|
-
onChange={(e) => write(e.target.checked)}
|
|
815
|
-
/>
|
|
816
|
-
);
|
|
603
|
+
const field = labField(leaf, value, write);
|
|
604
|
+
// Unreachable for null: `pairable` admits only kinds with a field.
|
|
605
|
+
if (field === null) return null;
|
|
606
|
+
switch (field.kind) {
|
|
817
607
|
case 'string':
|
|
818
|
-
return
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
title={title}
|
|
823
|
-
value={text.local}
|
|
824
|
-
placeholder={extra<string>(leaf, 'placeholder')}
|
|
825
|
-
maxLength={extra<number>(leaf, 'maxLength')}
|
|
826
|
-
onChange={(e) => text.type(e.target.value)}
|
|
827
|
-
/>
|
|
828
|
-
);
|
|
608
|
+
return <PropertyControl {...field} name={leaf.name} value={text.local} onChange={text.type} />;
|
|
609
|
+
case 'number':
|
|
610
|
+
// The pair names the row, so a cell has no room for a unit.
|
|
611
|
+
return <PropertyControl {...field} name={leaf.name} control="input" unit={undefined} />;
|
|
829
612
|
case 'color': {
|
|
830
613
|
// The alpha track needs a row of its own to sit under, so a paired
|
|
831
614
|
// swatch edits the color and carries the stored alpha through untouched.
|
|
832
|
-
const
|
|
833
|
-
const carriesAlpha = extra<boolean>(leaf, 'alpha') === true;
|
|
615
|
+
const stored = typeof value === 'string' ? toHex8(value) : '#000000';
|
|
834
616
|
return (
|
|
835
|
-
<
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
onChange={(e) =>
|
|
841
|
-
write(carriesAlpha ? joinHexAlpha(e.target.value, alpha) : e.target.value)
|
|
842
|
-
}
|
|
617
|
+
<PropertyControl
|
|
618
|
+
{...field}
|
|
619
|
+
name={leaf.name}
|
|
620
|
+
alpha={undefined}
|
|
621
|
+
onChange={(rgb: string) => write(field.alpha ? withAlpha01(rgb, getAlpha01(stored)) : rgb)}
|
|
843
622
|
/>
|
|
844
623
|
);
|
|
845
624
|
}
|
|
846
|
-
case 'enum': {
|
|
847
|
-
const options = extra<readonly { value: string; label: string }[]>(leaf, 'options') ?? [];
|
|
848
|
-
// `Select` takes no `title`, so the cell's help hangs off a wrapper.
|
|
849
|
-
return (
|
|
850
|
-
<span title={title}>
|
|
851
|
-
<Select<string>
|
|
852
|
-
variant="bare"
|
|
853
|
-
aria-label={name}
|
|
854
|
-
options={options}
|
|
855
|
-
selectedKey={typeof value === 'string' ? value : null}
|
|
856
|
-
onSelectionChange={write}
|
|
857
|
-
/>
|
|
858
|
-
</span>
|
|
859
|
-
);
|
|
860
|
-
}
|
|
861
625
|
default:
|
|
862
|
-
|
|
863
|
-
return null;
|
|
626
|
+
return <PropertyControl {...field} name={leaf.name} />;
|
|
864
627
|
}
|
|
865
628
|
}
|
|
866
629
|
|