@weasel-js/labkit 1.5.2 → 1.6.1
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-CMMo180l.d.ts → CanvasStackContext-Wo6OwIVM.d.ts} +1 -1
- package/dist/_dts/FloatingPanel-7j0IcXwz.d.ts +30 -0
- package/dist/_dts/LayerList.d-BoELAMsM.d.ts +83 -0
- package/dist/_dts/{PrefsForm.d-DPy9I8It.d.ts → PrefsRow.d-DFpPIeIC.d.ts} +16 -1
- package/dist/_dts/PropertyField.d-D0ndfIIt.d.ts +404 -0
- package/dist/_dts/{Select.d-ZTiwzYyT.d.ts → Select.d-Cr8-zoup.d.ts} +105 -15
- package/dist/_dts/{usePanZoom-SSTZe410.d.ts → Stage-DWBBi8IU.d.ts} +80 -35
- package/dist/_dts/{index-DlKolEIZ.d.ts → index-CKyX0ME_.d.ts} +12 -13
- package/dist/_dts/{frac-B4hFJMiN.d.ts → types-B7yBTTF9.d.ts} +709 -651
- package/dist/_dts/{types-Db-3NNk1.d.ts → types-DTuViBvc.d.ts} +21 -2
- package/dist/_dts/{useTrialState-pnAE-UBD.d.ts → useTrialState-CMaV6UD4.d.ts} +2 -3
- package/dist/canvas/index.d.ts +41 -8
- package/dist/canvas/index.js +5 -3
- package/dist/chrome/index.d.ts +6 -6
- package/dist/chrome/index.js +9 -7
- package/dist/chunk-24W5VEJW.js +18 -0
- package/dist/chunk-24W5VEJW.js.map +1 -0
- package/dist/chunk-2RVCB2PT.js +3 -0
- package/dist/chunk-2RVCB2PT.js.map +1 -0
- package/dist/chunk-3RY6WL5T.js +38 -0
- package/dist/chunk-3RY6WL5T.js.map +1 -0
- package/dist/{chunk-CKUKF72R.js → chunk-4TT3PNPR.js} +68 -55
- package/dist/chunk-4TT3PNPR.js.map +1 -0
- package/dist/{chunk-KJCUERQF.js → chunk-6FLXL3I7.js} +9 -164
- package/dist/chunk-6FLXL3I7.js.map +1 -0
- package/dist/chunk-ASOK76ZH.js +431 -0
- package/dist/chunk-ASOK76ZH.js.map +1 -0
- package/dist/chunk-AWQG7FCC.js +19 -0
- package/dist/chunk-AWQG7FCC.js.map +1 -0
- package/dist/chunk-CGHF6RZV.js +300 -0
- package/dist/chunk-CGHF6RZV.js.map +1 -0
- package/dist/chunk-DWMSB4CA.js +228 -0
- package/dist/chunk-DWMSB4CA.js.map +1 -0
- package/dist/chunk-GHGO52SO.js +92 -0
- package/dist/chunk-GHGO52SO.js.map +1 -0
- package/dist/{chunk-LRI3MBGR.js → chunk-HKV7LRHY.js} +4 -4
- package/dist/{chunk-LRI3MBGR.js.map → chunk-HKV7LRHY.js.map} +1 -1
- package/dist/{chunk-EMXAIRUJ.js → chunk-IRTGUQ5O.js} +5 -4
- package/dist/chunk-IRTGUQ5O.js.map +1 -0
- package/dist/{chunk-H6ZAOWNE.js → chunk-JVBYV6TL.js} +3 -3
- package/dist/{chunk-H6ZAOWNE.js.map → chunk-JVBYV6TL.js.map} +1 -1
- package/dist/chunk-KHK5FGER.js +145 -0
- package/dist/chunk-KHK5FGER.js.map +1 -0
- package/dist/chunk-NEXR37YJ.js +144 -0
- package/dist/chunk-NEXR37YJ.js.map +1 -0
- package/dist/{chunk-A2ACAGW6.js → chunk-O6FFFOHS.js} +32 -7
- package/dist/chunk-O6FFFOHS.js.map +1 -0
- package/dist/chunk-QGMOA2YE.js +731 -0
- package/dist/chunk-QGMOA2YE.js.map +1 -0
- package/dist/{chunk-ERFMC6WU.js → chunk-S4NCI32V.js} +19 -10
- package/dist/chunk-S4NCI32V.js.map +1 -0
- package/dist/{chunk-753Y6HE6.js → chunk-SLAQPUI4.js} +4 -4
- package/dist/chunk-SLAQPUI4.js.map +1 -0
- package/dist/{chunk-REOBGYMC.js → chunk-TJ77A4A3.js} +3 -5
- package/dist/chunk-TJ77A4A3.js.map +1 -0
- package/dist/chunk-TYN6MLXX.js +13864 -0
- package/dist/chunk-TYN6MLXX.js.map +1 -0
- package/dist/{chunk-RS3HRQWU.js → chunk-WPLAANH4.js} +4 -3
- package/dist/chunk-WPLAANH4.js.map +1 -0
- package/dist/config/index.d.ts +51 -7
- package/dist/config/index.js +4 -5
- package/dist/controls/index.d.ts +92 -5
- package/dist/controls/index.js +5 -4
- package/dist/dragdrop/index.d.ts +9 -6
- package/dist/dragdrop/index.js +3 -2
- package/dist/index.d.ts +108 -154
- package/dist/index.js +456 -600
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +5 -35
- package/dist/layers/index.js +3 -3
- package/dist/loupe/index.d.ts +21 -17
- package/dist/loupe/index.js +5 -3
- package/dist/overview/index.d.ts +56 -0
- package/dist/overview/index.js +288 -0
- package/dist/overview/index.js.map +1 -0
- package/dist/passthrough/weasel-ui.d.ts +434 -231
- package/dist/passthrough/weasel-ui.js +2 -1
- package/dist/primitives/index.d.ts +19 -54
- package/dist/primitives/index.js +6 -4
- package/dist/state/index.d.ts +8 -7
- package/dist/state/index.js +4 -5
- package/dist/state/index.js.map +1 -1
- package/dist/styles.css +360 -174
- package/dist/undo/index.d.ts +5 -4
- package/package.json +13 -13
- package/src/annotations/frac.test.ts +7 -0
- package/src/annotations/index.ts +1 -0
- package/src/annotations/paint.test.ts +21 -0
- package/src/annotations/paint.ts +12 -0
- package/src/annotations/store.test.ts +57 -0
- package/src/annotations/store.ts +25 -6
- package/src/annotations/toolMap.test.ts +37 -2
- package/src/annotations/toolMap.ts +13 -0
- package/src/annotations/types.ts +30 -2
- package/src/canvas/AGENTS.md +8 -10
- package/src/canvas/CameraInput.test.tsx +136 -0
- package/src/canvas/CameraInput.tsx +311 -0
- package/src/canvas/CanvasStack.tsx +42 -58
- package/src/canvas/LinkedCursor.less +16 -0
- package/src/canvas/LinkedCursor.tsx +54 -0
- package/src/canvas/Stage.test.tsx +18 -0
- package/src/canvas/Stage.tsx +42 -37
- package/src/canvas/cameraView.test.ts +60 -0
- package/src/canvas/cameraView.ts +61 -0
- package/src/canvas/index.ts +11 -2
- package/src/chrome/regions/SidebarRegion.less +197 -0
- package/src/chrome/regions/SidebarRegion.test.tsx +27 -0
- package/src/chrome/regions/SidebarRegion.tsx +7 -2
- package/src/chrome/types.ts +5 -2
- package/src/config/autoConfig.ts +2 -2
- package/src/config/builder.ts +21 -2
- package/src/config/entry.test.ts +10 -0
- package/src/config/fromConfigField.ts +1 -0
- package/src/config/index.ts +5 -0
- package/src/config/resolve.ts +11 -1
- package/src/config/rules.ts +2 -0
- package/src/config/sectionTree.test.ts +152 -0
- package/src/config/sectionTree.ts +134 -0
- package/src/config/types.ts +21 -0
- package/src/controls/ControlPanel.less +4 -0
- package/src/controls/ControlPanel.stories.tsx +55 -2
- package/src/controls/ControlPanel.test.tsx +40 -3
- package/src/controls/ControlPanel.tsx +134 -336
- package/src/controls/inDialog.test.tsx +100 -0
- package/src/controls/inDialog.tsx +38 -0
- package/src/controls/index.ts +1 -0
- package/src/index.test.ts +1 -1
- package/src/index.ts +18 -29
- package/src/instrument/SineWave.smoke.test.tsx +2 -0
- package/src/instrument/types.ts +15 -3
- package/src/lab/Lab.nebula.test.tsx +40 -0
- package/src/lab/Lab.test.tsx +3 -1
- package/src/lab/Lab.tsx +42 -19
- 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/loupe/AGENTS.md +7 -6
- package/src/loupe/LoupeGestures.tsx +14 -9
- package/src/loupe/TrialLoupe.tsx +5 -5
- package/src/loupe/index.ts +1 -0
- package/src/loupe/loupeActions.ts +6 -2
- package/src/overview/AGENTS.md +38 -0
- package/src/overview/OverviewMarks.test.tsx +100 -0
- package/src/overview/OverviewMarks.tsx +93 -0
- package/src/overview/TrialOverview.less +53 -0
- package/src/overview/TrialOverview.stories.tsx +50 -0
- package/src/overview/TrialOverview.test.tsx +116 -0
- package/src/overview/TrialOverview.tsx +374 -0
- package/src/overview/index.ts +2 -0
- package/src/passthrough/weasel-ui.ts +32 -10
- package/src/primitives/index.ts +0 -2
- package/src/specimen/Specimen.tsx +59 -56
- package/src/state/labRecords.test.ts +4 -1
- package/src/state/store.test.ts +4 -4
- package/src/state/types.ts +2 -1
- package/src/state/useTrialState.test.tsx +1 -1
- package/src/styles.less +6 -2
- package/src/theme/Interstellar.stories.tsx +8 -13
- 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/tools/labTool.ts +6 -2
- package/src/trial/Trial.annotations.test.tsx +16 -0
- package/src/trial/Trial.canvas.test.tsx +1 -1
- package/src/trial/Trial.loupe.test.tsx +73 -1
- package/src/trial/Trial.stories.tsx +2 -0
- package/src/trial/Trial.tsx +47 -9
- package/src/trial/trialOps.test.ts +4 -4
- package/dist/_dts/index-8LiKe3A-.d.ts +0 -410
- package/dist/chunk-2AYGEN57.js +0 -32
- package/dist/chunk-2AYGEN57.js.map +0 -1
- package/dist/chunk-6BUQ77ZV.js +0 -18
- package/dist/chunk-6BUQ77ZV.js.map +0 -1
- package/dist/chunk-753Y6HE6.js.map +0 -1
- package/dist/chunk-A2ACAGW6.js.map +0 -1
- package/dist/chunk-CKUKF72R.js.map +0 -1
- package/dist/chunk-D35PFTS7.js +0 -170
- package/dist/chunk-D35PFTS7.js.map +0 -1
- package/dist/chunk-EMXAIRUJ.js.map +0 -1
- package/dist/chunk-ERFMC6WU.js.map +0 -1
- package/dist/chunk-FKLT3ITC.js +0 -590
- package/dist/chunk-FKLT3ITC.js.map +0 -1
- package/dist/chunk-KJCUERQF.js.map +0 -1
- package/dist/chunk-LASCBWW7.js +0 -13456
- package/dist/chunk-LASCBWW7.js.map +0 -1
- package/dist/chunk-REOBGYMC.js.map +0 -1
- package/dist/chunk-RS3HRQWU.js.map +0 -1
- package/dist/chunk-TT5C5S6R.js +0 -368
- package/dist/chunk-TT5C5S6R.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/usePanZoom.test.ts +0 -244
- package/src/canvas/usePanZoom.ts +0 -147
- 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,30 +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
|
+
PropertyControl,
|
|
10
|
+
type PropertyControlProps,
|
|
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
|
-
ToggleBar,
|
|
27
|
-
ToggleRow,
|
|
19
|
+
prefFieldProps,
|
|
20
|
+
type StanceProps,
|
|
28
21
|
} from '@weasel-js/ui';
|
|
29
22
|
import { Fragment, type ReactNode, useEffect, useMemo, useRef, useState } from 'react';
|
|
30
23
|
import { auto as autoValue } from '../config/auto';
|
|
@@ -33,6 +26,7 @@ import { fromConfigFields } from '../config/fromConfigField';
|
|
|
33
26
|
import { schemaNodeAtPath, valueAtPath } from '../config/path';
|
|
34
27
|
import type { ControlRenderer, ResolvedConfig, SectionSpec } from '../config/types';
|
|
35
28
|
import { isLeafVisible } from '../config/visible';
|
|
29
|
+
import { inDialog, summarizeValue } from './inDialog';
|
|
36
30
|
import type { ConfigField } from './types';
|
|
37
31
|
|
|
38
32
|
/** How a panel packs its rows into the two-column property grid.
|
|
@@ -63,7 +57,7 @@ function sectionRows(section: SectionSpec, rows: Rows): Rows {
|
|
|
63
57
|
};
|
|
64
58
|
}
|
|
65
59
|
|
|
66
|
-
export interface ControlPanelProps<TC extends Record<string, unknown>> {
|
|
60
|
+
export interface ControlPanelProps<TC extends Record<string, unknown>> extends StanceProps {
|
|
67
61
|
/** The instrument's resolved config schema. */
|
|
68
62
|
schema?: ResolvedConfig;
|
|
69
63
|
/** @deprecated Pass `schema`. A field list is adapted into one internally. */
|
|
@@ -115,6 +109,9 @@ export interface ControlPanelProps<TC extends Record<string, unknown>> {
|
|
|
115
109
|
auto?: ReadonlySet<string>;
|
|
116
110
|
/** Draw leaves marked `hidden`. */
|
|
117
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;
|
|
118
115
|
className?: string;
|
|
119
116
|
}
|
|
120
117
|
|
|
@@ -138,6 +135,9 @@ export function ControlPanel<TC extends Record<string, unknown>>({
|
|
|
138
135
|
onCollapse,
|
|
139
136
|
auto: given,
|
|
140
137
|
showHidden = false,
|
|
138
|
+
title,
|
|
139
|
+
stance,
|
|
140
|
+
tone,
|
|
141
141
|
className,
|
|
142
142
|
}: ControlPanelProps<TC>) {
|
|
143
143
|
const resolved = useMemo(() => schema ?? fromConfigFields(fields ?? []), [schema, fields]);
|
|
@@ -243,7 +243,13 @@ export function ControlPanel<TC extends Record<string, unknown>>({
|
|
|
243
243
|
if (pair === undefined) return undefined;
|
|
244
244
|
if (!isLeafVisible(resolved, path, config as Record<string, unknown>, showHidden))
|
|
245
245
|
return undefined;
|
|
246
|
-
if (
|
|
246
|
+
if (
|
|
247
|
+
renderers?.[path] ??
|
|
248
|
+
resolved.renderers[path] ??
|
|
249
|
+
resolved.dialogs[path] ??
|
|
250
|
+
renderers?.[found.kind]
|
|
251
|
+
)
|
|
252
|
+
return undefined;
|
|
247
253
|
if (!isBuiltinToolPref(found)) return undefined;
|
|
248
254
|
if (found.kind === 'paint' || found.kind === 'object' || isSliderLeaf(found)) return undefined;
|
|
249
255
|
return { leaf: found, pair };
|
|
@@ -313,7 +319,7 @@ export function ControlPanel<TC extends Record<string, unknown>>({
|
|
|
313
319
|
);
|
|
314
320
|
};
|
|
315
321
|
|
|
316
|
-
|
|
322
|
+
const list = (
|
|
317
323
|
<PropertyList
|
|
318
324
|
pack={gridPack}
|
|
319
325
|
density={density}
|
|
@@ -323,6 +329,12 @@ export function ControlPanel<TC extends Record<string, unknown>>({
|
|
|
323
329
|
{body(resolved.group, '', { pack, layout, grid: gridPack })}
|
|
324
330
|
</PropertyList>
|
|
325
331
|
);
|
|
332
|
+
if (title === undefined && stance === undefined && tone === undefined) return list;
|
|
333
|
+
return (
|
|
334
|
+
<PropertyPanel title={title} stance={stance} tone={tone} density={density}>
|
|
335
|
+
{list}
|
|
336
|
+
</PropertyPanel>
|
|
337
|
+
);
|
|
326
338
|
}
|
|
327
339
|
|
|
328
340
|
/** How a section is keyed for folding: its label at the root, and its group's
|
|
@@ -399,150 +411,100 @@ function ControlRow<TC extends Record<string, unknown>>({
|
|
|
399
411
|
const autoProps = { auto: isAutoRow, onAutoChange };
|
|
400
412
|
|
|
401
413
|
// Most specific wins, and within a tier the lab's entry beats the
|
|
402
|
-
// instrument's: controls[path] -> node .render -> controls[kind] -> built-in.
|
|
403
|
-
const
|
|
414
|
+
// instrument's: controls[path] -> node .render/.dialog -> controls[kind] -> built-in.
|
|
415
|
+
const dialog = resolved.dialogs[path];
|
|
416
|
+
const custom =
|
|
417
|
+
renderers?.[path] ??
|
|
418
|
+
resolved.renderers[path] ??
|
|
419
|
+
(dialog ? inDialog(dialog.body, dialog) : undefined) ??
|
|
420
|
+
renderers?.[leaf.kind];
|
|
404
421
|
// A custom row places itself like any other; one that needs the full width
|
|
405
422
|
// says so with `<PropertyRow span>`, which is the same opt-out a built-in has.
|
|
406
423
|
if (custom) return custom({ path, pref: leaf, value, setValue: write, auto: isAutoRow, setAuto });
|
|
407
424
|
|
|
408
425
|
const label = leaf.name;
|
|
409
426
|
const description = leaf.description;
|
|
410
|
-
const read = <T,>(): T => value as T;
|
|
411
427
|
// `auto` gives the whole width to the controls that read badly at half of a
|
|
412
428
|
// sidebar's: free text, a slider track, a segmented toggle. `pairs` doesn't.
|
|
413
429
|
const wide = pack === 'auto';
|
|
430
|
+
const row = { label, description, layout, readout: autoReadout, ...autoProps };
|
|
431
|
+
|
|
432
|
+
if (leaf.kind === 'list') {
|
|
433
|
+
const entries = Array.isArray(value) ? (value as string[]) : [];
|
|
434
|
+
return (
|
|
435
|
+
<DialogRow
|
|
436
|
+
label={label}
|
|
437
|
+
summary={summarizeValue(entries)}
|
|
438
|
+
readout={autoReadout}
|
|
439
|
+
layout={layout}
|
|
440
|
+
description={description}
|
|
441
|
+
{...autoProps}
|
|
442
|
+
>
|
|
443
|
+
{() => (
|
|
444
|
+
<ListEditor
|
|
445
|
+
aria-label={label}
|
|
446
|
+
value={entries}
|
|
447
|
+
onChange={write}
|
|
448
|
+
placeholder={extra<string>(leaf, 'placeholder')}
|
|
449
|
+
/>
|
|
450
|
+
)}
|
|
451
|
+
</DialogRow>
|
|
452
|
+
);
|
|
453
|
+
}
|
|
414
454
|
|
|
415
|
-
|
|
455
|
+
const field = labField(leaf, value, write);
|
|
456
|
+
if (field === null)
|
|
416
457
|
return <UnwiredRow label={label} kind={leaf.kind} description={description} />;
|
|
458
|
+
switch (field.kind) {
|
|
459
|
+
case 'string':
|
|
460
|
+
return <DebouncedTextRow leaf={leaf} field={field} span={wide} {...row} />;
|
|
461
|
+
case 'number':
|
|
462
|
+
return <PropertyField {...field} {...row} span={field.control === 'slider' && wide} />;
|
|
463
|
+
case 'enum':
|
|
464
|
+
return <PropertyField {...field} {...row} span={field.control === 'radio' && wide} />;
|
|
465
|
+
default:
|
|
466
|
+
return <PropertyField {...field} {...row} />;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
417
469
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
value={field.shown}
|
|
449
|
-
min={field.min}
|
|
450
|
-
max={field.max}
|
|
451
|
-
step={field.step}
|
|
452
|
-
unit={suffix}
|
|
453
|
-
onChange={(n) => write(field.store(n))}
|
|
454
|
-
readout={autoReadout}
|
|
455
|
-
layout={layout}
|
|
456
|
-
description={description}
|
|
457
|
-
{...autoProps}
|
|
458
|
-
/>
|
|
459
|
-
);
|
|
460
|
-
}
|
|
461
|
-
case 'boolean': {
|
|
462
|
-
const Row = extra<string>(leaf, 'control') === 'switch' ? SwitchRow : CheckboxRow;
|
|
463
|
-
return (
|
|
464
|
-
<Row
|
|
465
|
-
label={label}
|
|
466
|
-
value={read<boolean>()}
|
|
467
|
-
onChange={write}
|
|
468
|
-
readout={autoReadout}
|
|
469
|
-
layout={layout}
|
|
470
|
-
description={description}
|
|
471
|
-
{...autoProps}
|
|
472
|
-
/>
|
|
473
|
-
);
|
|
474
|
-
}
|
|
475
|
-
case 'enum': {
|
|
476
|
-
const options = extra<readonly { value: string; label: string }[]>(leaf, 'options') ?? [];
|
|
477
|
-
const segmented = extra<string>(leaf, 'control') === 'radio';
|
|
478
|
-
const Row = segmented ? ToggleRow : SelectRow;
|
|
479
|
-
return (
|
|
480
|
-
<Row
|
|
481
|
-
label={label}
|
|
482
|
-
value={read<string>()}
|
|
483
|
-
options={options}
|
|
484
|
-
onChange={write}
|
|
485
|
-
readout={autoReadout}
|
|
486
|
-
layout={layout}
|
|
487
|
-
span={segmented && wide}
|
|
488
|
-
description={description}
|
|
489
|
-
{...autoProps}
|
|
490
|
-
/>
|
|
491
|
-
);
|
|
492
|
-
}
|
|
470
|
+
/**
|
|
471
|
+
* A leaf as the field it draws, with the labkit-only extras `prefFieldProps`
|
|
472
|
+
* knows nothing of. `null` for a kind the panel declines: a paint, which a
|
|
473
|
+
* hex swatch would flatten to a solid, and an object, which a flat row would
|
|
474
|
+
* write one field of. Override with `render` to edit either.
|
|
475
|
+
*/
|
|
476
|
+
function labField(
|
|
477
|
+
leaf: PrefLeaf,
|
|
478
|
+
value: unknown,
|
|
479
|
+
write: (value: unknown) => void,
|
|
480
|
+
): PropertyControlProps | null {
|
|
481
|
+
if (!isBuiltinToolPref(leaf) || leaf.kind === 'paint' || leaf.kind === 'object') return null;
|
|
482
|
+
const field = prefFieldProps(leaf, { value, setValue: write });
|
|
483
|
+
if (field === null) return null;
|
|
484
|
+
switch (field.kind) {
|
|
485
|
+
case 'number':
|
|
486
|
+
// `prefFieldProps` clamps what it stores, so an instrument is never
|
|
487
|
+
// handed a value outside the range it asked for.
|
|
488
|
+
return {
|
|
489
|
+
...field,
|
|
490
|
+
control: isSliderLeaf(leaf) ? 'slider' : 'input',
|
|
491
|
+
unit: extra<string>(leaf, 'suffix') ?? field.unit,
|
|
492
|
+
};
|
|
493
|
+
case 'boolean':
|
|
494
|
+
return {
|
|
495
|
+
...field,
|
|
496
|
+
control: extra<string>(leaf, 'control') === 'switch' ? 'switch' : 'checkbox',
|
|
497
|
+
};
|
|
498
|
+
case 'enum':
|
|
499
|
+
return { ...field, control: extra<string>(leaf, 'control') === 'radio' ? 'radio' : 'select' };
|
|
493
500
|
case 'string':
|
|
494
|
-
return
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
value={read<string>()}
|
|
499
|
-
write={write}
|
|
500
|
-
readout={autoReadout}
|
|
501
|
-
layout={layout}
|
|
502
|
-
span={wide}
|
|
503
|
-
description={description}
|
|
504
|
-
{...autoProps}
|
|
505
|
-
/>
|
|
506
|
-
);
|
|
507
|
-
case 'color': {
|
|
508
|
-
// `ColorRow` takes alpha as a number beside a `#rrggbb` swatch, so an
|
|
509
|
-
// `#rrggbbaa` value has to be split going in and rejoined coming out —
|
|
510
|
-
// handed whole to the swatch, the browser cannot parse it and the first
|
|
511
|
-
// edit writes back black.
|
|
512
|
-
const common = {
|
|
513
|
-
label,
|
|
514
|
-
readout: autoReadout,
|
|
515
|
-
layout,
|
|
516
|
-
description,
|
|
517
|
-
...autoProps,
|
|
501
|
+
return {
|
|
502
|
+
...field,
|
|
503
|
+
placeholder: extra<string>(leaf, 'placeholder'),
|
|
504
|
+
maxLength: extra<number>(leaf, 'maxLength'),
|
|
518
505
|
};
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
const { rgb, alpha } = splitHexAlpha(read<string>());
|
|
522
|
-
return (
|
|
523
|
-
<ColorRow
|
|
524
|
-
{...common}
|
|
525
|
-
value={rgb}
|
|
526
|
-
onChange={(next) => write(joinHexAlpha(next, alpha))}
|
|
527
|
-
alpha={alpha}
|
|
528
|
-
onAlphaChange={(next) => write(joinHexAlpha(rgb, next))}
|
|
529
|
-
/>
|
|
530
|
-
);
|
|
531
|
-
}
|
|
532
|
-
case 'paint':
|
|
533
|
-
case 'object':
|
|
534
|
-
// Declined: a hex swatch would write a solid over a gradient, and a flat
|
|
535
|
-
// row would write one field into a half-built object. Override with
|
|
536
|
-
// `render` to edit either.
|
|
537
|
-
return <UnwiredRow label={label} kind={leaf.kind} description={description} />;
|
|
538
|
-
default: {
|
|
539
|
-
// Not reachable while every built-in kind has an arm — and a new kind
|
|
540
|
-
// that lacks one is a compile error here, never a blank row.
|
|
541
|
-
const _exhaustive: never = leaf;
|
|
542
|
-
throw new Error(
|
|
543
|
-
`ControlPanel: no control for built-in pref kind "${(_exhaustive as { kind: string }).kind}"`,
|
|
544
|
-
);
|
|
545
|
-
}
|
|
506
|
+
default:
|
|
507
|
+
return field;
|
|
546
508
|
}
|
|
547
509
|
}
|
|
548
510
|
|
|
@@ -567,20 +529,12 @@ function UnwiredRow({
|
|
|
567
529
|
/** A text row whose writes are debounced — see {@link useDebouncedText}. */
|
|
568
530
|
function DebouncedTextRow({
|
|
569
531
|
leaf,
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
write,
|
|
573
|
-
layout,
|
|
574
|
-
span,
|
|
575
|
-
description,
|
|
576
|
-
readout,
|
|
577
|
-
auto,
|
|
578
|
-
onAutoChange,
|
|
532
|
+
field,
|
|
533
|
+
...row
|
|
579
534
|
}: {
|
|
580
535
|
leaf: PrefLeaf;
|
|
536
|
+
field: PropertyControlProps & { kind: 'string' };
|
|
581
537
|
label: string;
|
|
582
|
-
value: string;
|
|
583
|
-
write: (value: unknown) => void;
|
|
584
538
|
layout?: PropertyRowLayout;
|
|
585
539
|
span?: boolean;
|
|
586
540
|
description?: string;
|
|
@@ -589,100 +543,11 @@ function DebouncedTextRow({
|
|
|
589
543
|
onAutoChange?: (next: boolean) => void;
|
|
590
544
|
}) {
|
|
591
545
|
const text = useDebouncedText(
|
|
592
|
-
value,
|
|
593
|
-
|
|
546
|
+
field.value ?? '',
|
|
547
|
+
field.onChange,
|
|
594
548
|
extra<number>(leaf, 'debounceMs') ?? 150,
|
|
595
549
|
);
|
|
596
|
-
|
|
597
|
-
return (
|
|
598
|
-
<TextRow
|
|
599
|
-
label={label}
|
|
600
|
-
readout={readout}
|
|
601
|
-
layout={layout}
|
|
602
|
-
span={span}
|
|
603
|
-
description={description}
|
|
604
|
-
auto={auto}
|
|
605
|
-
onAutoChange={onAutoChange}
|
|
606
|
-
value={text.local}
|
|
607
|
-
placeholder={extra<string>(leaf, 'placeholder')}
|
|
608
|
-
maxLength={extra<number>(leaf, 'maxLength')}
|
|
609
|
-
onChange={text.type}
|
|
610
|
-
/>
|
|
611
|
-
);
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
/**
|
|
615
|
-
* A number leaf's field in the unit it is edited in: what the control shows,
|
|
616
|
-
* the bounds it shows them against, and what a typed number stores once it is
|
|
617
|
-
* clamped and converted back. Without a `unit` this is what the leaf declares,
|
|
618
|
-
* unchanged.
|
|
619
|
-
*/
|
|
620
|
-
function numberField(
|
|
621
|
-
leaf: PrefLeaf,
|
|
622
|
-
value: number,
|
|
623
|
-
): {
|
|
624
|
-
unit: PrefNumberUnit | undefined;
|
|
625
|
-
min: number | undefined;
|
|
626
|
-
max: number | undefined;
|
|
627
|
-
step: number | undefined;
|
|
628
|
-
shown: number;
|
|
629
|
-
store: (shown: number) => number;
|
|
630
|
-
} {
|
|
631
|
-
const unit = extra<PrefNumberUnit>(leaf, 'unit');
|
|
632
|
-
// `min`, `max` and `step` are declared in the stored unit alongside the
|
|
633
|
-
// value, so they convert with it — a leaf storing radians and showing
|
|
634
|
-
// degrees would otherwise clamp typed degrees against 0..6.28.
|
|
635
|
-
const bounds = unit
|
|
636
|
-
? prefDisplayBounds(leaf as PrefNumber)
|
|
637
|
-
: {
|
|
638
|
-
min: extra<number>(leaf, 'min'),
|
|
639
|
-
max: extra<number>(leaf, 'max'),
|
|
640
|
-
step: extra<number>(leaf, 'step'),
|
|
641
|
-
};
|
|
642
|
-
const lo = bounds.min ?? Number.NEGATIVE_INFINITY;
|
|
643
|
-
const hi = bounds.max ?? Number.POSITIVE_INFINITY;
|
|
644
|
-
return {
|
|
645
|
-
unit,
|
|
646
|
-
min: bounds.min,
|
|
647
|
-
max: bounds.max,
|
|
648
|
-
step: bounds.step,
|
|
649
|
-
shown: unit ? unit.toDisplay(value) : value,
|
|
650
|
-
store: (shown) => {
|
|
651
|
-
const clamped = Math.min(hi, Math.max(lo, shown));
|
|
652
|
-
return unit ? unit.fromDisplay(clamped) : clamped;
|
|
653
|
-
},
|
|
654
|
-
};
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
/** `#rgb`, `#rgba`, `#rrggbb` or `#rrggbbaa` as the `#rrggbb` an
|
|
658
|
-
* `<input type="color">` can hold and the 0..1 alpha beside it. Anything else
|
|
659
|
-
* reads as opaque black, which is what the input would show for it anyway. */
|
|
660
|
-
function splitHexAlpha(value: string): { rgb: string; alpha: number } {
|
|
661
|
-
const digits = /^#([0-9a-f]{3,8})$/i.exec(value?.trim() ?? '')?.[1];
|
|
662
|
-
const twice = (s: string): string =>
|
|
663
|
-
s
|
|
664
|
-
.split('')
|
|
665
|
-
.map((c) => c + c)
|
|
666
|
-
.join('');
|
|
667
|
-
const byte = (s: string): number => Number.parseInt(s, 16) / 255;
|
|
668
|
-
switch (digits?.length) {
|
|
669
|
-
case 3:
|
|
670
|
-
return { rgb: `#${twice(digits)}`, alpha: 1 };
|
|
671
|
-
case 4:
|
|
672
|
-
return { rgb: `#${twice(digits.slice(0, 3))}`, alpha: byte(twice(digits.slice(3))) };
|
|
673
|
-
case 6:
|
|
674
|
-
return { rgb: `#${digits}`, alpha: 1 };
|
|
675
|
-
case 8:
|
|
676
|
-
return { rgb: `#${digits.slice(0, 6)}`, alpha: byte(digits.slice(6)) };
|
|
677
|
-
default:
|
|
678
|
-
return { rgb: '#000000', alpha: 1 };
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
/** A `#rrggbb` and a 0..1 alpha back into the `#rrggbbaa` the config holds. */
|
|
683
|
-
function joinHexAlpha(rgb: string, alpha: number): string {
|
|
684
|
-
const byte = Math.max(0, Math.min(255, Math.round(alpha * 255)));
|
|
685
|
-
return `${rgb}${byte.toString(16).padStart(2, '0')}`;
|
|
550
|
+
return <PropertyField {...field} {...row} value={text.local} onChange={text.type} />;
|
|
686
551
|
}
|
|
687
552
|
|
|
688
553
|
/** One cell of a paired row, with its path and the leaf it draws. */
|
|
@@ -721,7 +586,7 @@ function PairedRow<TC extends Record<string, unknown>>({
|
|
|
721
586
|
// The row is named by the pair, so a cell that shares it says which
|
|
722
587
|
// knob it is — without a caption a paired switch reads only to a
|
|
723
588
|
// screen reader. A cell whose leaf is deliberately unnamed keeps none.
|
|
724
|
-
<span key={path} className="lk-pair-cell">
|
|
589
|
+
<span key={path} className="lk-pair-cell" title={leaf.description || undefined}>
|
|
725
590
|
{leaf.name ? <span className="lk-pair-label">{leaf.name}</span> : null}
|
|
726
591
|
<PairCell
|
|
727
592
|
leaf={leaf}
|
|
@@ -745,106 +610,39 @@ function PairCell({
|
|
|
745
610
|
value: unknown;
|
|
746
611
|
write: (value: unknown) => void;
|
|
747
612
|
}) {
|
|
748
|
-
const name = leaf.name;
|
|
749
|
-
const title = leaf.description;
|
|
750
613
|
const text = useDebouncedText(
|
|
751
614
|
typeof value === 'string' ? value : '',
|
|
752
615
|
write,
|
|
753
616
|
extra<number>(leaf, 'debounceMs') ?? 150,
|
|
754
617
|
);
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
<input
|
|
760
|
-
type="number"
|
|
761
|
-
aria-label={name}
|
|
762
|
-
title={title}
|
|
763
|
-
value={field.shown}
|
|
764
|
-
min={field.min}
|
|
765
|
-
max={field.max}
|
|
766
|
-
step={field.step}
|
|
767
|
-
onChange={(e) => {
|
|
768
|
-
const n = Number(e.target.value);
|
|
769
|
-
if (e.target.value !== '' && Number.isFinite(n)) write(field.store(n));
|
|
770
|
-
}}
|
|
771
|
-
/>
|
|
772
|
-
);
|
|
773
|
-
}
|
|
774
|
-
case 'boolean':
|
|
775
|
-
return extra<string>(leaf, 'control') === 'switch' ? (
|
|
776
|
-
<Switch aria-label={name} isSelected={value === true} onChange={write} />
|
|
777
|
-
) : (
|
|
778
|
-
<input
|
|
779
|
-
type="checkbox"
|
|
780
|
-
aria-label={name}
|
|
781
|
-
title={title}
|
|
782
|
-
checked={value === true}
|
|
783
|
-
onChange={(e) => write(e.target.checked)}
|
|
784
|
-
/>
|
|
785
|
-
);
|
|
618
|
+
const field = labField(leaf, value, write);
|
|
619
|
+
// Unreachable for null: `pairable` admits only kinds with a field.
|
|
620
|
+
if (field === null) return null;
|
|
621
|
+
switch (field.kind) {
|
|
786
622
|
case 'string':
|
|
787
623
|
return (
|
|
788
|
-
<
|
|
789
|
-
type="text"
|
|
790
|
-
aria-label={name}
|
|
791
|
-
title={title}
|
|
792
|
-
value={text.local}
|
|
793
|
-
placeholder={extra<string>(leaf, 'placeholder')}
|
|
794
|
-
maxLength={extra<number>(leaf, 'maxLength')}
|
|
795
|
-
onChange={(e) => text.type(e.target.value)}
|
|
796
|
-
/>
|
|
624
|
+
<PropertyControl {...field} name={leaf.name} value={text.local} onChange={text.type} />
|
|
797
625
|
);
|
|
626
|
+
case 'number':
|
|
627
|
+
// The pair names the row, so a cell has no room for a unit.
|
|
628
|
+
return <PropertyControl {...field} name={leaf.name} control="input" unit={undefined} />;
|
|
798
629
|
case 'color': {
|
|
799
630
|
// The alpha track needs a row of its own to sit under, so a paired
|
|
800
631
|
// swatch edits the color and carries the stored alpha through untouched.
|
|
801
|
-
const
|
|
802
|
-
const carriesAlpha = extra<boolean>(leaf, 'alpha') === true;
|
|
632
|
+
const stored = typeof value === 'string' ? toHex8(value) : '#000000';
|
|
803
633
|
return (
|
|
804
|
-
<
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
write(carriesAlpha ? joinHexAlpha(e.target.value, alpha) : e.target.value)
|
|
634
|
+
<PropertyControl
|
|
635
|
+
{...field}
|
|
636
|
+
name={leaf.name}
|
|
637
|
+
alpha={undefined}
|
|
638
|
+
onChange={(rgb: string) =>
|
|
639
|
+
write(field.alpha ? withAlpha01(rgb, getAlpha01(stored)) : rgb)
|
|
811
640
|
}
|
|
812
641
|
/>
|
|
813
642
|
);
|
|
814
643
|
}
|
|
815
|
-
case 'enum': {
|
|
816
|
-
const options = extra<readonly { value: string; label: string }[]>(leaf, 'options') ?? [];
|
|
817
|
-
// A leaf that asked to be segmented stays segmented when it shares a row:
|
|
818
|
-
// the choice is the control's shape, not a whole row's worth of chrome.
|
|
819
|
-
if (extra<string>(leaf, 'control') === 'radio') {
|
|
820
|
-
return (
|
|
821
|
-
<span title={title}>
|
|
822
|
-
<ToggleBar<string>
|
|
823
|
-
ariaLabel={name}
|
|
824
|
-
size="sm"
|
|
825
|
-
value={typeof value === 'string' ? value : null}
|
|
826
|
-
onChange={(next) => next !== null && write(next)}
|
|
827
|
-
items={options.map((o) => ({ value: o.value, label: o.label }))}
|
|
828
|
-
/>
|
|
829
|
-
</span>
|
|
830
|
-
);
|
|
831
|
-
}
|
|
832
|
-
// `Select` takes no `title`, so the cell's help hangs off a wrapper.
|
|
833
|
-
return (
|
|
834
|
-
<span title={title}>
|
|
835
|
-
<Select<string>
|
|
836
|
-
variant="bare"
|
|
837
|
-
aria-label={name}
|
|
838
|
-
options={options}
|
|
839
|
-
selectedKey={typeof value === 'string' ? value : null}
|
|
840
|
-
onSelectionChange={write}
|
|
841
|
-
/>
|
|
842
|
-
</span>
|
|
843
|
-
);
|
|
844
|
-
}
|
|
845
644
|
default:
|
|
846
|
-
|
|
847
|
-
return null;
|
|
645
|
+
return <PropertyControl {...field} name={leaf.name} />;
|
|
848
646
|
}
|
|
849
647
|
}
|
|
850
648
|
|