@weasel-js/labkit 1.2.0 → 1.3.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/dist/_dts/{DrawCommand-C_XboUpz.d.ts → DrawCommand-DBB45NfN.d.ts} +196 -52
- package/dist/_dts/PrefsForm-BYa6cWnO.d.ts +201 -0
- package/dist/_dts/types-1Sdxy_Pv.d.ts +90 -0
- package/dist/_dts/types-BttTIent.d.ts +314 -0
- package/dist/_dts/types-DCGEcMO_.d.ts +162 -0
- package/dist/_dts/{useTrialState-BMNIx3Cy.d.ts → useTrialState-CsGMjhu9.d.ts} +5 -87
- package/dist/canvas/index.d.ts +7 -3
- package/dist/canvas/index.js +3 -1
- package/dist/chrome/index.d.ts +86 -0
- package/dist/chrome/index.js +9 -0
- package/dist/chrome/index.js.map +1 -0
- package/dist/chunk-3XXPU73K.js +138 -0
- package/dist/chunk-3XXPU73K.js.map +1 -0
- package/dist/{chunk-VUU5UXHE.js → chunk-4TMMVIDM.js} +8 -5
- package/dist/chunk-4TMMVIDM.js.map +1 -0
- package/dist/chunk-6X5RBC6G.js +366 -0
- package/dist/chunk-6X5RBC6G.js.map +1 -0
- package/dist/{chunk-574LJAV4.js → chunk-BDJXIBRZ.js} +33 -35
- package/dist/chunk-BDJXIBRZ.js.map +1 -0
- package/dist/{chunk-SFL7NFKN.js → chunk-BMW4TDP5.js} +15 -7
- package/dist/chunk-BMW4TDP5.js.map +1 -0
- package/dist/chunk-JDROYQM3.js +266 -0
- package/dist/chunk-JDROYQM3.js.map +1 -0
- package/dist/{chunk-THBG7FQZ.js → chunk-LGKRIUSW.js} +15 -12
- package/dist/chunk-LGKRIUSW.js.map +1 -0
- package/dist/{chunk-BOHF3PQO.js → chunk-QCICIHEO.js} +8129 -5196
- package/dist/chunk-QCICIHEO.js.map +1 -0
- package/dist/chunk-QRYPXSGP.js +212 -0
- package/dist/chunk-QRYPXSGP.js.map +1 -0
- package/dist/chunk-RGT4EJIB.js +6961 -0
- package/dist/chunk-RGT4EJIB.js.map +1 -0
- package/dist/chunk-SBXNQW4G.js +3 -0
- package/dist/chunk-SBXNQW4G.js.map +1 -0
- package/dist/chunk-T66HU3HX.js +17 -0
- package/dist/chunk-T66HU3HX.js.map +1 -0
- package/dist/chunk-WTI26YTM.js +266 -0
- package/dist/chunk-WTI26YTM.js.map +1 -0
- package/dist/controls/index.d.ts +19 -5
- package/dist/controls/index.js +3 -1
- package/dist/dragdrop/index.d.ts +46 -3
- package/dist/dragdrop/index.js +3 -1
- package/dist/index.d.ts +735 -565
- package/dist/index.js +661 -1019
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +5 -3
- package/dist/layers/index.js +4 -1
- package/dist/passthrough/weasel-canvas.d.ts +69 -13
- package/dist/passthrough/weasel-canvas.js +1 -1
- package/dist/passthrough/weasel-ui.d.ts +3811 -3671
- package/dist/passthrough/weasel-ui.js +3 -2
- package/dist/primitives/index.d.ts +102 -8
- package/dist/primitives/index.js +5 -1
- package/dist/state/index.d.ts +3 -2
- package/dist/state/index.js +2 -2
- package/dist/styles.css +490 -803
- package/dist/surface/index.js +2 -1
- package/dist/ui/layers/index.d.ts +8 -2
- package/dist/ui/layers/index.js +2 -3
- package/dist/undo/index.d.ts +4 -2
- package/package.json +16 -7
- package/src/canvas/AGENTS.md +4 -0
- package/src/canvas/CanvasStack.tsx +6 -2
- package/src/canvas/useLayerScheduler.ts +22 -15
- package/src/canvas/usePanZoom.test.ts +80 -0
- package/src/canvas/usePanZoom.ts +16 -2
- package/src/chrome/ChromeRegions.stories.tsx +106 -0
- package/src/chrome/builtins.test.ts +153 -0
- package/src/chrome/builtins.tsx +205 -0
- package/src/chrome/index.ts +25 -0
- package/src/chrome/merge.test.ts +51 -0
- package/src/chrome/merge.ts +52 -0
- package/src/chrome/regions/PaletteRegion.less +14 -0
- package/src/chrome/regions/PaletteRegion.test.tsx +36 -0
- package/src/chrome/regions/PaletteRegion.tsx +41 -0
- package/src/chrome/regions/SidebarRegion.tsx +56 -0
- package/src/chrome/regions/StatusRegion.tsx +31 -0
- package/src/chrome/regions/TitleBarRegion.tsx +41 -0
- package/src/chrome/regions/ToolbarRegion.test.tsx +82 -0
- package/src/chrome/regions/ToolbarRegion.tsx +71 -0
- package/src/chrome/regions/ViewportRegion.less +42 -0
- package/src/chrome/regions/ViewportRegion.tsx +37 -0
- package/src/chrome/regions/regions.test.tsx +81 -0
- package/src/chrome/types.ts +127 -0
- package/src/config/builder.test.ts +93 -0
- package/src/config/builder.ts +220 -0
- package/src/config/fromConfigField.test.ts +93 -0
- package/src/config/fromConfigField.ts +74 -0
- package/src/config/index.ts +22 -0
- package/src/config/resolve.test.ts +119 -0
- package/src/config/resolve.ts +67 -0
- package/src/config/rules.ts +68 -0
- package/src/config/types.ts +108 -0
- package/src/config/useConfigSchema.ts +30 -0
- package/src/config/visible.ts +20 -0
- package/src/controls/ControlPanel.less +4 -58
- package/src/controls/ControlPanel.stories.tsx +125 -0
- package/src/controls/ControlPanel.test.tsx +245 -10
- package/src/controls/ControlPanel.tsx +236 -203
- package/src/dragdrop/DragDropRuntime.tsx +36 -18
- package/src/dragdrop/DragGhost.less +1 -1
- package/src/dragdrop/Palette.less +1 -1
- package/src/dragdrop/index.ts +11 -0
- package/src/index.ts +74 -5
- package/src/instrument/SineWave.smoke.test.tsx +8 -1
- package/src/instrument/defineInstrument.test.ts +38 -1
- package/src/instrument/defineInstrument.ts +31 -4
- package/src/instrument/index.ts +1 -0
- package/src/instrument/types.ts +27 -4
- package/src/instrument/validateConfigSchema.test.ts +13 -3
- package/src/instrument/validateConfigSchema.ts +7 -10
- package/src/lab/Lab.chrome.test.tsx +65 -0
- package/src/lab/Lab.tsx +58 -15
- package/src/lab/LabContext.ts +6 -0
- package/src/lab/LabFullChrome.stories.tsx +308 -0
- package/src/lab/LabHeader.test.tsx +42 -0
- package/src/lab/LabHeader.tsx +57 -0
- package/src/lab/LabPalette.test.tsx +35 -0
- package/src/lab/LabPalette.tsx +34 -0
- package/src/lab/LabShell.less +50 -3
- package/src/lab/Workspace.less +1 -30
- package/src/lab/Workspace.test.tsx +20 -10
- package/src/lab/Workspace.tsx +5 -8
- package/src/lab/index.ts +3 -1
- package/src/layers/AGENTS.md +1 -1
- package/src/layers/LayerList.less +31 -7
- package/src/layers/LayerList.tsx +17 -4
- package/src/passthrough/weasel-ui.ts +28 -0
- package/src/primitives/FloatingPanel.less +28 -0
- package/src/primitives/FloatingPanel.stories.tsx +46 -0
- package/src/primitives/FloatingPanel.test.tsx +240 -0
- package/src/primitives/FloatingPanel.tsx +190 -0
- package/src/primitives/FpsMeter.test.tsx +44 -0
- package/src/primitives/FpsMeter.tsx +20 -9
- package/src/primitives/JobProgress.less +69 -0
- package/src/primitives/JobProgress.stories.tsx +48 -0
- package/src/primitives/JobProgress.tsx +65 -0
- package/src/primitives/Legend.less +47 -0
- package/src/primitives/Legend.stories.tsx +32 -0
- package/src/primitives/Legend.test.tsx +57 -0
- package/src/primitives/Legend.tsx +40 -0
- package/src/primitives/Sidebar.less +32 -2
- package/src/primitives/StatusBar.less +7 -0
- package/src/primitives/StatusBar.tsx +10 -3
- package/src/primitives/Toolbar.less +50 -3
- package/src/primitives/Toolbar.test.tsx +67 -1
- package/src/primitives/Toolbar.tsx +72 -6
- package/src/primitives/ZoomControl.less +31 -0
- package/src/primitives/ZoomControl.tsx +76 -0
- package/src/primitives/floating.entry.test.tsx +14 -0
- package/src/primitives/index.ts +10 -2
- package/src/primitives/useRovingTabIndex.test.ts +27 -0
- package/src/primitives/useRovingTabIndex.ts +79 -0
- package/src/state/store.ts +11 -3
- package/src/state/toolSlot.test.ts +36 -0
- package/src/state/types.ts +4 -0
- package/src/styles.less +6 -4
- package/src/surface/useTiledSurface.ts +14 -10
- package/src/test-setup.ts +26 -0
- package/src/theme/Interstellar.stories.tsx +1 -1
- package/src/theme/base.less +19 -2
- package/src/theme/interstellar.tokens.json +6 -2
- package/src/tools/types.ts +24 -0
- package/src/trial/Trial.config.test.tsx +108 -0
- package/src/trial/Trial.job.test.tsx +13 -15
- package/src/trial/Trial.less +115 -10
- package/src/trial/Trial.stories.tsx +1 -0
- package/src/trial/Trial.test.tsx +67 -96
- package/src/trial/Trial.tsx +80 -39
- package/src/trial/TrialChrome.tsx +99 -80
- package/src/trial/TrialDragContext.ts +10 -0
- package/src/trial/TrialTitleBar.tsx +41 -0
- package/src/trial/index.ts +1 -15
- package/src/trial/trialChrome.borders.test.ts +38 -0
- package/src/ui/layers/index.ts +5 -1
- package/dist/_dts/index-JFAYj5Tv.d.ts +0 -144
- package/dist/_dts/types-x92Kfeme.d.ts +0 -62
- package/dist/chunk-574LJAV4.js.map +0 -1
- package/dist/chunk-5R2ATYPJ.js +0 -201
- package/dist/chunk-5R2ATYPJ.js.map +0 -1
- package/dist/chunk-73KA7WBO.js +0 -5992
- package/dist/chunk-73KA7WBO.js.map +0 -1
- package/dist/chunk-BOHF3PQO.js.map +0 -1
- package/dist/chunk-G5TJVQQT.js +0 -161
- package/dist/chunk-G5TJVQQT.js.map +0 -1
- package/dist/chunk-PMAU3SEE.js +0 -126
- package/dist/chunk-PMAU3SEE.js.map +0 -1
- package/dist/chunk-SFL7NFKN.js.map +0 -1
- package/dist/chunk-THBG7FQZ.js.map +0 -1
- package/dist/chunk-VUU5UXHE.js.map +0 -1
- package/src/instrument/capabilityDetector.test.ts +0 -64
- package/src/instrument/capabilityDetector.ts +0 -20
- package/src/trial/DefaultSidebar.tsx +0 -29
- package/src/trial/DefaultStatusBar.tsx +0 -19
- package/src/trial/DefaultToolbar.tsx +0 -92
- package/src/trial/slotTypes.ts +0 -54
- package/src/ui/format.test.ts +0 -33
- package/src/ui/format.ts +0 -30
- package/src/ui/layers/LayerStack.less +0 -143
- package/src/ui/layers/LayerStack.stories.tsx +0 -45
- package/src/ui/layers/LayerStack.test.tsx +0 -170
- package/src/ui/layers/LayerStack.tsx +0 -220
- package/src/ui/properties/CheckboxRow.stories.tsx +0 -27
- package/src/ui/properties/ColorRow.stories.tsx +0 -106
- package/src/ui/properties/CurveField.less +0 -48
- package/src/ui/properties/CurveField.stories.tsx +0 -20
- package/src/ui/properties/CurveField.test.tsx +0 -127
- package/src/ui/properties/CurveField.tsx +0 -184
- package/src/ui/properties/EffectCard.tsx +0 -324
- package/src/ui/properties/Gallery.stories.tsx +0 -178
- package/src/ui/properties/NumberRow.stories.tsx +0 -65
- package/src/ui/properties/PropertyGroup.less +0 -50
- package/src/ui/properties/PropertyGroup.stories.tsx +0 -34
- package/src/ui/properties/PropertyGroup.test.tsx +0 -33
- package/src/ui/properties/PropertyGroup.tsx +0 -45
- package/src/ui/properties/PropertyList.stories.tsx +0 -83
- package/src/ui/properties/PropertyPanel.less +0 -592
- package/src/ui/properties/PropertyPanel.stories.tsx +0 -190
- package/src/ui/properties/PropertyPanel.test.tsx +0 -193
- package/src/ui/properties/PropertyPanel.tsx +0 -469
- package/src/ui/properties/PropertyRow.stories.tsx +0 -53
- package/src/ui/properties/SelectRow.stories.tsx +0 -58
- package/src/ui/properties/SliderRow.stories.tsx +0 -74
- package/src/ui/properties/SpeechBalloonPanels.stories.tsx +0 -396
- package/src/ui/properties/TextRow.stories.tsx +0 -62
- package/src/ui/properties/ToggleRow.stories.tsx +0 -57
- package/src/ui/properties/index.ts +0 -39
- package/src/ui/properties/storyLayouts.tsx +0 -36
|
@@ -1,251 +1,284 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
import { isBuiltinToolPref } from '@weasel-js/core';
|
|
2
|
+
import {
|
|
3
|
+
CheckboxRow,
|
|
4
|
+
ColorRow,
|
|
5
|
+
NumberRow,
|
|
6
|
+
type PrefLeaf,
|
|
7
|
+
PropertyGroup,
|
|
8
|
+
PropertyList,
|
|
9
|
+
PropertyRow,
|
|
10
|
+
SelectRow,
|
|
11
|
+
SliderRow,
|
|
12
|
+
TextRow,
|
|
13
|
+
ToggleRow,
|
|
14
|
+
} from '@weasel-js/ui';
|
|
15
|
+
import { type ReactNode, useEffect, useMemo, useRef, useState } from 'react';
|
|
16
|
+
import { fromConfigFields } from '../config/fromConfigField';
|
|
17
|
+
import type { ControlRenderer, ResolvedConfig } from '../config/types';
|
|
18
|
+
import { isLeafVisible } from '../config/visible';
|
|
19
|
+
import type { ConfigField } from './types';
|
|
11
20
|
|
|
12
21
|
export interface ControlPanelProps<TC extends Record<string, unknown>> {
|
|
13
|
-
|
|
22
|
+
/** The instrument's resolved config schema. */
|
|
23
|
+
schema?: ResolvedConfig;
|
|
24
|
+
/** @deprecated Pass `schema`. A field list is adapted into one internally. */
|
|
25
|
+
fields?: ConfigField[];
|
|
14
26
|
config: TC;
|
|
15
27
|
setConfig: (key: keyof TC, value: unknown) => void;
|
|
28
|
+
/**
|
|
29
|
+
* Control overrides and app-defined kinds, PrefsForm-style. Keys are config
|
|
30
|
+
* paths (checked first) or leaf kinds. A renderer returning `null` collapses
|
|
31
|
+
* the row; entries override the built-in rows on collision.
|
|
32
|
+
*/
|
|
33
|
+
renderers?: Record<string, ControlRenderer>;
|
|
34
|
+
/** Draw leaves marked `hidden`. */
|
|
35
|
+
showHidden?: boolean;
|
|
16
36
|
className?: string;
|
|
17
37
|
}
|
|
18
38
|
|
|
19
39
|
/** Render an instrument's config schema as a stack of controls, each writing
|
|
20
|
-
* back through `setConfig`.
|
|
40
|
+
* back through `setConfig`. Built on the property rows, so a lab's controls
|
|
41
|
+
* are the same aligned, themed rows the rest of the kit uses. */
|
|
21
42
|
export function ControlPanel<TC extends Record<string, unknown>>({
|
|
43
|
+
schema,
|
|
22
44
|
fields,
|
|
23
45
|
config,
|
|
24
46
|
setConfig,
|
|
47
|
+
renderers,
|
|
48
|
+
showHidden = false,
|
|
25
49
|
className,
|
|
26
50
|
}: ControlPanelProps<TC>) {
|
|
51
|
+
const resolved = useMemo(() => schema ?? fromConfigFields(fields ?? []), [schema, fields]);
|
|
52
|
+
|
|
53
|
+
const paths = Object.keys(resolved.group.children);
|
|
54
|
+
const sectioned = new Set(resolved.sections.flatMap((s) => s.paths));
|
|
55
|
+
const loose = paths.filter((p) => !sectioned.has(p));
|
|
56
|
+
|
|
57
|
+
const row = (path: string): ReactNode => (
|
|
58
|
+
<ControlRow
|
|
59
|
+
key={path}
|
|
60
|
+
path={path}
|
|
61
|
+
resolved={resolved}
|
|
62
|
+
config={config}
|
|
63
|
+
setConfig={setConfig}
|
|
64
|
+
renderers={renderers}
|
|
65
|
+
showHidden={showHidden}
|
|
66
|
+
/>
|
|
67
|
+
);
|
|
68
|
+
|
|
27
69
|
return (
|
|
28
|
-
<
|
|
29
|
-
{
|
|
30
|
-
|
|
70
|
+
<PropertyList className={className ? `lk-control-panel ${className}` : 'lk-control-panel'}>
|
|
71
|
+
{loose.map(row)}
|
|
72
|
+
{resolved.sections.map((section) => (
|
|
73
|
+
<PropertyGroup key={section.label} title={section.label}>
|
|
74
|
+
{section.paths.map(row)}
|
|
75
|
+
</PropertyGroup>
|
|
31
76
|
))}
|
|
32
|
-
</
|
|
77
|
+
</PropertyList>
|
|
33
78
|
);
|
|
34
79
|
}
|
|
35
80
|
|
|
36
81
|
interface ControlRowProps<TC extends Record<string, unknown>> {
|
|
37
|
-
|
|
82
|
+
path: string;
|
|
83
|
+
resolved: ResolvedConfig;
|
|
38
84
|
config: TC;
|
|
39
85
|
setConfig: (key: keyof TC, value: unknown) => void;
|
|
86
|
+
renderers?: Record<string, ControlRenderer>;
|
|
87
|
+
showHidden: boolean;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Reads a labkit-only extra off a leaf. `PrefLeaf` has no field for these,
|
|
91
|
+
* and extra keys survive the resolve pass at runtime. */
|
|
92
|
+
function extra<T>(leaf: PrefLeaf, key: string): T | undefined {
|
|
93
|
+
return (leaf as unknown as Record<string, T | undefined>)[key];
|
|
40
94
|
}
|
|
41
95
|
|
|
42
96
|
function ControlRow<TC extends Record<string, unknown>>({
|
|
43
|
-
|
|
97
|
+
path,
|
|
98
|
+
resolved,
|
|
44
99
|
config,
|
|
45
100
|
setConfig,
|
|
101
|
+
renderers,
|
|
102
|
+
showHidden,
|
|
46
103
|
}: ControlRowProps<TC>) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
case 'checkbox':
|
|
51
|
-
return <CheckboxRow field={field} config={config} setConfig={setConfig} />;
|
|
52
|
-
case 'select':
|
|
53
|
-
return <SelectRow field={field} config={config} setConfig={setConfig} />;
|
|
54
|
-
case 'number':
|
|
55
|
-
return <NumberRow field={field} config={config} setConfig={setConfig} />;
|
|
56
|
-
case 'text':
|
|
57
|
-
return <TextRow field={field} config={config} setConfig={setConfig} />;
|
|
58
|
-
case 'color':
|
|
59
|
-
return <ColorRow field={field} config={config} setConfig={setConfig} />;
|
|
60
|
-
default:
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
104
|
+
const leaf = resolved.group.children[path] as PrefLeaf | undefined;
|
|
105
|
+
if (!leaf || !('kind' in leaf)) return null;
|
|
106
|
+
if (!isLeafVisible(resolved, path, config as Record<string, unknown>, showHidden)) return null;
|
|
64
107
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
config
|
|
68
|
-
setConfig: (key: keyof TC, value: unknown) => void;
|
|
69
|
-
};
|
|
108
|
+
const write = (value: unknown): void => setConfig(path as keyof TC, value);
|
|
109
|
+
const fallback = extra<unknown>(leaf, 'default');
|
|
110
|
+
const value = config[path] ?? fallback;
|
|
70
111
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}: RowProps<SliderField, TC>) {
|
|
76
|
-
const value = (config[field.key] as number | undefined) ?? field.default;
|
|
77
|
-
return (
|
|
78
|
-
<div className="lk-control-row">
|
|
79
|
-
<label className="lk-control-label" htmlFor={`lk-ctrl-${field.key}`}>
|
|
80
|
-
{field.label}
|
|
81
|
-
</label>
|
|
82
|
-
<div className="lk-control-slider-row">
|
|
83
|
-
<input
|
|
84
|
-
id={`lk-ctrl-${field.key}`}
|
|
85
|
-
className="lk-control-input lk-control-input--range"
|
|
86
|
-
type="range"
|
|
87
|
-
min={field.min}
|
|
88
|
-
max={field.max}
|
|
89
|
-
step={field.step ?? 1}
|
|
90
|
-
value={value}
|
|
91
|
-
onChange={(e) => setConfig(field.key as keyof TC, Number(e.target.value))}
|
|
92
|
-
/>
|
|
93
|
-
<span className="lk-control-slider-value">{value}</span>
|
|
94
|
-
</div>
|
|
95
|
-
</div>
|
|
96
|
-
);
|
|
97
|
-
}
|
|
112
|
+
// Most specific wins, and within a tier the lab's entry beats the
|
|
113
|
+
// instrument's: controls[path] -> node .render -> controls[kind] -> built-in.
|
|
114
|
+
const custom = renderers?.[path] ?? resolved.renderers[path] ?? renderers?.[leaf.kind];
|
|
115
|
+
if (custom) return custom({ path, pref: leaf, value, setValue: write });
|
|
98
116
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
setConfig,
|
|
103
|
-
}: RowProps<CheckboxField, TC>) {
|
|
104
|
-
const value = (config[field.key] as boolean | undefined) ?? field.default;
|
|
105
|
-
return (
|
|
106
|
-
<div className="lk-control-row lk-control-row--inline">
|
|
107
|
-
<input
|
|
108
|
-
id={`lk-ctrl-${field.key}`}
|
|
109
|
-
className="lk-control-input lk-control-input--checkbox"
|
|
110
|
-
type="checkbox"
|
|
111
|
-
checked={value}
|
|
112
|
-
onChange={(e) => setConfig(field.key as keyof TC, e.target.checked)}
|
|
113
|
-
/>
|
|
114
|
-
<label className="lk-control-label" htmlFor={`lk-ctrl-${field.key}`}>
|
|
115
|
-
{field.label}
|
|
116
|
-
</label>
|
|
117
|
-
</div>
|
|
118
|
-
);
|
|
119
|
-
}
|
|
117
|
+
const label = leaf.name;
|
|
118
|
+
const description = leaf.description;
|
|
119
|
+
const read = <T,>(): T => value as T;
|
|
120
120
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
<
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
121
|
+
if (!isBuiltinToolPref(leaf))
|
|
122
|
+
return <UnwiredRow label={label} kind={leaf.kind} description={description} />;
|
|
123
|
+
|
|
124
|
+
switch (leaf.kind) {
|
|
125
|
+
case 'number': {
|
|
126
|
+
const min = extra<number>(leaf, 'min');
|
|
127
|
+
const max = extra<number>(leaf, 'max');
|
|
128
|
+
const step = extra<number>(leaf, 'step');
|
|
129
|
+
if (extra<string>(leaf, 'control') === 'slider' && min !== undefined && max !== undefined) {
|
|
130
|
+
return (
|
|
131
|
+
<SliderRow
|
|
132
|
+
label={label}
|
|
133
|
+
value={read<number>()}
|
|
134
|
+
min={min}
|
|
135
|
+
max={max}
|
|
136
|
+
step={step}
|
|
137
|
+
onChange={write}
|
|
138
|
+
description={description}
|
|
139
|
+
/>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
// NumberRow commits every keystroke and does not clamp, so the bounds a
|
|
143
|
+
// schema declares are enforced here — an instrument should never be
|
|
144
|
+
// handed a config value outside the range it asked for.
|
|
145
|
+
const lo = min ?? Number.NEGATIVE_INFINITY;
|
|
146
|
+
const hi = max ?? Number.POSITIVE_INFINITY;
|
|
147
|
+
return (
|
|
148
|
+
<NumberRow
|
|
149
|
+
label={label}
|
|
150
|
+
value={read<number>()}
|
|
151
|
+
min={min}
|
|
152
|
+
max={max}
|
|
153
|
+
step={step}
|
|
154
|
+
onChange={(n) => write(Math.min(hi, Math.max(lo, n)))}
|
|
155
|
+
description={description}
|
|
156
|
+
/>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
case 'boolean':
|
|
160
|
+
return (
|
|
161
|
+
<CheckboxRow
|
|
162
|
+
label={label}
|
|
163
|
+
value={read<boolean>()}
|
|
164
|
+
onChange={write}
|
|
165
|
+
description={description}
|
|
166
|
+
/>
|
|
167
|
+
);
|
|
168
|
+
case 'enum': {
|
|
169
|
+
const options = extra<readonly { value: string; label: string }[]>(leaf, 'options') ?? [];
|
|
170
|
+
const Row = extra<string>(leaf, 'control') === 'radio' ? ToggleRow : SelectRow;
|
|
171
|
+
return (
|
|
172
|
+
<Row
|
|
173
|
+
label={label}
|
|
174
|
+
value={read<string>()}
|
|
175
|
+
options={options}
|
|
176
|
+
onChange={write}
|
|
177
|
+
description={description}
|
|
178
|
+
/>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
case 'string':
|
|
182
|
+
return (
|
|
183
|
+
<DebouncedTextRow
|
|
184
|
+
leaf={leaf}
|
|
185
|
+
label={label}
|
|
186
|
+
value={read<string>()}
|
|
187
|
+
write={write}
|
|
188
|
+
description={description}
|
|
189
|
+
/>
|
|
190
|
+
);
|
|
191
|
+
case 'color':
|
|
192
|
+
return (
|
|
193
|
+
<ColorRow label={label} value={read<string>()} onChange={write} description={description} />
|
|
194
|
+
);
|
|
195
|
+
case 'paint':
|
|
196
|
+
case 'object':
|
|
197
|
+
// Declined: a hex swatch would write a solid over a gradient, and a flat
|
|
198
|
+
// row would write one field into a half-built object. Override with
|
|
199
|
+
// `render` to edit either.
|
|
200
|
+
return <UnwiredRow label={label} kind={leaf.kind} description={description} />;
|
|
201
|
+
default: {
|
|
202
|
+
// Not reachable while every built-in kind has an arm — and a new kind
|
|
203
|
+
// that lacks one is a compile error here, never a blank row.
|
|
204
|
+
const _exhaustive: never = leaf;
|
|
205
|
+
throw new Error(
|
|
206
|
+
`ControlPanel: no control for built-in pref kind "${(_exhaustive as { kind: string }).kind}"`,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
146
210
|
}
|
|
147
211
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
212
|
+
/** A leaf this panel has no control for is named rather than dropped: a silent
|
|
213
|
+
* gap reads as "this control does not exist". */
|
|
214
|
+
function UnwiredRow({
|
|
215
|
+
label,
|
|
216
|
+
kind,
|
|
217
|
+
description,
|
|
218
|
+
}: {
|
|
219
|
+
label: string;
|
|
220
|
+
kind: string;
|
|
221
|
+
description?: string;
|
|
222
|
+
}) {
|
|
154
223
|
return (
|
|
155
|
-
<
|
|
156
|
-
<
|
|
157
|
-
|
|
158
|
-
</label>
|
|
159
|
-
<input
|
|
160
|
-
id={`lk-ctrl-${field.key}`}
|
|
161
|
-
className="lk-control-input"
|
|
162
|
-
type="number"
|
|
163
|
-
value={value}
|
|
164
|
-
min={field.min}
|
|
165
|
-
max={field.max}
|
|
166
|
-
step={field.step}
|
|
167
|
-
onChange={(e) => setConfig(field.key as keyof TC, Number(e.target.value))}
|
|
168
|
-
onBlur={(e) => {
|
|
169
|
-
const n = Number(e.target.value);
|
|
170
|
-
if (!Number.isFinite(n)) return;
|
|
171
|
-
let clamped = n;
|
|
172
|
-
if (field.min !== undefined && clamped < field.min) clamped = field.min;
|
|
173
|
-
if (field.max !== undefined && clamped > field.max) clamped = field.max;
|
|
174
|
-
if (clamped !== n) setConfig(field.key as keyof TC, clamped);
|
|
175
|
-
}}
|
|
176
|
-
/>
|
|
177
|
-
</div>
|
|
224
|
+
<PropertyRow label={label} description={description}>
|
|
225
|
+
<span className="lk-control-panel__unknown">no control for “{kind}”</span>
|
|
226
|
+
</PropertyRow>
|
|
178
227
|
);
|
|
179
228
|
}
|
|
180
229
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
config
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
230
|
+
/** Text writes are debounced so typing does not re-run the instrument on every
|
|
231
|
+
* keystroke, which means the row is locally controlled between commits and has
|
|
232
|
+
* to notice when the config changes underneath it. */
|
|
233
|
+
function DebouncedTextRow({
|
|
234
|
+
leaf,
|
|
235
|
+
label,
|
|
236
|
+
value,
|
|
237
|
+
write,
|
|
238
|
+
description,
|
|
239
|
+
}: {
|
|
240
|
+
leaf: PrefLeaf;
|
|
241
|
+
label: string;
|
|
242
|
+
value: string;
|
|
243
|
+
write: (value: unknown) => void;
|
|
244
|
+
description?: string;
|
|
245
|
+
}) {
|
|
246
|
+
const debounceMs = extra<number>(leaf, 'debounceMs') ?? 150;
|
|
247
|
+
const [local, setLocal] = useState(value);
|
|
189
248
|
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
190
|
-
const lastExternal = useRef(
|
|
249
|
+
const lastExternal = useRef(value);
|
|
191
250
|
|
|
192
251
|
useEffect(() => {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
setLocal(ext);
|
|
252
|
+
if (value !== lastExternal.current) {
|
|
253
|
+
lastExternal.current = value;
|
|
254
|
+
setLocal(value);
|
|
197
255
|
}
|
|
198
|
-
}, [
|
|
256
|
+
}, [value]);
|
|
199
257
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
<input
|
|
206
|
-
id={`lk-ctrl-${field.key}`}
|
|
207
|
-
className="lk-control-input"
|
|
208
|
-
type="text"
|
|
209
|
-
value={local}
|
|
210
|
-
placeholder={field.placeholder}
|
|
211
|
-
maxLength={field.maxLength}
|
|
212
|
-
onChange={(e) => {
|
|
213
|
-
const next = e.target.value;
|
|
214
|
-
setLocal(next);
|
|
215
|
-
if (timer.current) clearTimeout(timer.current);
|
|
216
|
-
if (debounceMs === 0) {
|
|
217
|
-
lastExternal.current = next;
|
|
218
|
-
setConfig(field.key as keyof TC, next);
|
|
219
|
-
} else {
|
|
220
|
-
timer.current = setTimeout(() => {
|
|
221
|
-
lastExternal.current = next;
|
|
222
|
-
setConfig(field.key as keyof TC, next);
|
|
223
|
-
}, debounceMs);
|
|
224
|
-
}
|
|
225
|
-
}}
|
|
226
|
-
/>
|
|
227
|
-
</div>
|
|
258
|
+
useEffect(
|
|
259
|
+
() => () => {
|
|
260
|
+
if (timer.current) clearTimeout(timer.current);
|
|
261
|
+
},
|
|
262
|
+
[],
|
|
228
263
|
);
|
|
229
|
-
}
|
|
230
264
|
|
|
231
|
-
function ColorRow<TC extends Record<string, unknown>>({
|
|
232
|
-
field,
|
|
233
|
-
config,
|
|
234
|
-
setConfig,
|
|
235
|
-
}: RowProps<ColorField, TC>) {
|
|
236
|
-
const value = (config[field.key] as string | undefined) ?? field.default;
|
|
237
265
|
return (
|
|
238
|
-
<
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
<
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
266
|
+
<TextRow
|
|
267
|
+
label={label}
|
|
268
|
+
description={description}
|
|
269
|
+
value={local}
|
|
270
|
+
placeholder={extra<string>(leaf, 'placeholder')}
|
|
271
|
+
maxLength={extra<number>(leaf, 'maxLength')}
|
|
272
|
+
onChange={(next) => {
|
|
273
|
+
setLocal(next);
|
|
274
|
+
if (timer.current) clearTimeout(timer.current);
|
|
275
|
+
const commit = () => {
|
|
276
|
+
lastExternal.current = next;
|
|
277
|
+
write(next);
|
|
278
|
+
};
|
|
279
|
+
if (debounceMs === 0) commit();
|
|
280
|
+
else timer.current = setTimeout(commit, debounceMs);
|
|
281
|
+
}}
|
|
282
|
+
/>
|
|
250
283
|
);
|
|
251
284
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useVisibleRaf } from '@weasel-js/core';
|
|
1
2
|
import { type RefObject, useCallback, useEffect, useRef, useState } from 'react';
|
|
2
3
|
import { screenToWorld } from '../canvas/canvasCoords';
|
|
3
4
|
import type {
|
|
@@ -42,7 +43,9 @@ export function useDragDrop<TS, TC>({
|
|
|
42
43
|
const [drag, setDrag] = useState<DragState | null>(null);
|
|
43
44
|
const dragRef = useRef<DragState | null>(null);
|
|
44
45
|
dragRef.current = drag;
|
|
45
|
-
|
|
46
|
+
/** The move the next frame will resolve. Doubles as the throttle's flag:
|
|
47
|
+
* non-null means a frame is already queued. */
|
|
48
|
+
const pendingPos = useRef<Point | null>(null);
|
|
46
49
|
|
|
47
50
|
const isOverCanvas = useCallback(
|
|
48
51
|
(screenPos: Point): boolean => {
|
|
@@ -69,37 +72,42 @@ export function useDragDrop<TS, TC>({
|
|
|
69
72
|
[canvasContainerRef, view],
|
|
70
73
|
);
|
|
71
74
|
|
|
75
|
+
const frameLoop = useVisibleRaf(() => {
|
|
76
|
+
const screenPos = pendingPos.current;
|
|
77
|
+
pendingPos.current = null;
|
|
78
|
+
const active = dragRef.current;
|
|
79
|
+
if (!active || !screenPos) return;
|
|
80
|
+
let feedback: DragFeedback | null = null;
|
|
81
|
+
if (capability.onDragOver && isOverCanvas(screenPos)) {
|
|
82
|
+
const world = screenToWorldFromContainer(screenPos);
|
|
83
|
+
if (world) feedback = capability.onDragOver(world, active.item, state, config);
|
|
84
|
+
}
|
|
85
|
+
setDrag({ ...active, screenPos, feedback });
|
|
86
|
+
});
|
|
87
|
+
|
|
72
88
|
useEffect(() => {
|
|
73
89
|
if (!drag) return;
|
|
74
90
|
const handleMove = (e: PointerEvent) => {
|
|
75
91
|
const screenPos = { x: e.clientX, y: e.clientY };
|
|
76
92
|
const current = dragRef.current;
|
|
77
93
|
if (!current) return;
|
|
78
|
-
if (
|
|
94
|
+
if (pendingPos.current !== null) {
|
|
95
|
+
// A frame is already queued: keep the ghost under the cursor and let
|
|
96
|
+
// that frame probe the drop target from the newest position.
|
|
97
|
+
pendingPos.current = screenPos;
|
|
79
98
|
dragRef.current = { ...current, screenPos };
|
|
80
99
|
setDrag(dragRef.current);
|
|
81
100
|
return;
|
|
82
101
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const active = dragRef.current;
|
|
86
|
-
if (!active) return;
|
|
87
|
-
let feedback: DragFeedback | null = null;
|
|
88
|
-
if (capability.onDragOver && isOverCanvas(screenPos)) {
|
|
89
|
-
const world = screenToWorldFromContainer(screenPos);
|
|
90
|
-
if (world) feedback = capability.onDragOver(world, active.item, state, config);
|
|
91
|
-
}
|
|
92
|
-
setDrag({ ...active, screenPos, feedback });
|
|
93
|
-
});
|
|
102
|
+
pendingPos.current = screenPos;
|
|
103
|
+
frameLoop.request();
|
|
94
104
|
};
|
|
95
105
|
const handleUp = (e: PointerEvent) => {
|
|
96
106
|
const screenPos = { x: e.clientX, y: e.clientY };
|
|
97
107
|
const active = dragRef.current;
|
|
98
108
|
if (!active) return;
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
rafThrottle.current = null;
|
|
102
|
-
}
|
|
109
|
+
pendingPos.current = null;
|
|
110
|
+
frameLoop.cancel();
|
|
103
111
|
if (isOverCanvas(screenPos)) {
|
|
104
112
|
const world = screenToWorldFromContainer(screenPos);
|
|
105
113
|
if (world) {
|
|
@@ -116,7 +124,17 @@ export function useDragDrop<TS, TC>({
|
|
|
116
124
|
window.removeEventListener('pointermove', handleMove);
|
|
117
125
|
window.removeEventListener('pointerup', handleUp);
|
|
118
126
|
};
|
|
119
|
-
}, [
|
|
127
|
+
}, [
|
|
128
|
+
drag,
|
|
129
|
+
capability,
|
|
130
|
+
state,
|
|
131
|
+
config,
|
|
132
|
+
setState,
|
|
133
|
+
emit,
|
|
134
|
+
frameLoop,
|
|
135
|
+
isOverCanvas,
|
|
136
|
+
screenToWorldFromContainer,
|
|
137
|
+
]);
|
|
120
138
|
|
|
121
139
|
const startDrag = useCallback((item: PaletteItem, originScreenPos: Point) => {
|
|
122
140
|
setDrag({ item, screenPos: originScreenPos, feedback: null });
|
package/src/dragdrop/index.ts
CHANGED
|
@@ -1 +1,12 @@
|
|
|
1
1
|
export type { DragDropCapability, DragFeedback, PaletteItem } from '../instrument/types';
|
|
2
|
+
export type {
|
|
3
|
+
DragOverlayProps,
|
|
4
|
+
DragState,
|
|
5
|
+
UseDragDropArgs,
|
|
6
|
+
UseDragDropResult,
|
|
7
|
+
} from './DragDropRuntime';
|
|
8
|
+
export { DragOverlay, useDragDrop } from './DragDropRuntime';
|
|
9
|
+
export type { DragGhostProps } from './DragGhost';
|
|
10
|
+
export { DragGhost } from './DragGhost';
|
|
11
|
+
export type { PaletteProps } from './Palette';
|
|
12
|
+
export { Palette } from './Palette';
|