@weasel-js/labkit 1.2.0 → 1.3.0-pre.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/index-DgcNdEdh.d.ts +314 -0
- package/dist/_dts/types-1Sdxy_Pv.d.ts +90 -0
- package/dist/_dts/types-AHlQxBNN.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 +2 -1
- package/dist/chrome/index.d.ts +86 -0
- package/dist/chrome/index.js +8 -0
- package/dist/chrome/index.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-THBG7FQZ.js → chunk-HFDVGF4X.js} +15 -12
- package/dist/chunk-HFDVGF4X.js.map +1 -0
- package/dist/{chunk-574LJAV4.js → chunk-NS54R52R.js} +31 -21
- package/dist/chunk-NS54R52R.js.map +1 -0
- package/dist/{chunk-G5TJVQQT.js → chunk-O2BULFHX.js} +5 -14
- package/dist/chunk-O2BULFHX.js.map +1 -0
- package/dist/chunk-O7NVSCLN.js +266 -0
- package/dist/chunk-O7NVSCLN.js.map +1 -0
- package/dist/chunk-PO6L3NSH.js +366 -0
- package/dist/chunk-PO6L3NSH.js.map +1 -0
- package/dist/chunk-PQJ5B2U2.js +27 -0
- package/dist/chunk-PQJ5B2U2.js.map +1 -0
- package/dist/chunk-RVNN4CHQ.js +212 -0
- package/dist/chunk-RVNN4CHQ.js.map +1 -0
- package/dist/chunk-TLGRYALP.js +6151 -0
- package/dist/chunk-TLGRYALP.js.map +1 -0
- package/dist/{chunk-SFL7NFKN.js → chunk-V7PRSIRQ.js} +15 -7
- package/dist/chunk-V7PRSIRQ.js.map +1 -0
- package/dist/{chunk-BOHF3PQO.js → chunk-Z6HJ5FEM.js} +8111 -5190
- package/dist/chunk-Z6HJ5FEM.js.map +1 -0
- package/dist/chunk-ZZAYVX4X.js +505 -0
- package/dist/chunk-ZZAYVX4X.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 +4 -2
- package/dist/index.d.ts +246 -106
- package/dist/index.js +673 -559
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +5 -3
- package/dist/layers/index.js +2 -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 +3799 -3672
- package/dist/passthrough/weasel-ui.js +2 -2
- package/dist/primitives/index.d.ts +102 -8
- package/dist/primitives/index.js +4 -1
- package/dist/state/index.d.ts +3 -2
- package/dist/state/index.js +2 -2
- package/dist/styles.css +542 -153
- package/dist/surface/index.js +2 -1
- package/dist/ui/layers/index.js +4 -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 +87 -0
- package/src/config/builder.ts +224 -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 +120 -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 +86 -0
- package/src/controls/ControlPanel.test.tsx +223 -10
- package/src/controls/ControlPanel.tsx +202 -203
- package/src/dragdrop/DragDropRuntime.tsx +36 -18
- package/src/dragdrop/DragGhost.less +1 -1
- package/src/dragdrop/Palette.less +1 -1
- package/src/index.ts +25 -3
- 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 +2 -0
- 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 +23 -0
- package/src/primitives/DragHandleGlyph.tsx +29 -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 -0
- package/src/surface/useTiledSurface.ts +14 -10
- package/src/test-setup.ts +26 -0
- 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 +113 -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/LayerStack.less +9 -9
- package/src/ui/layers/LayerStack.tsx +1 -13
- package/src/ui/properties/CurveField.less +2 -2
- package/src/ui/properties/CurveField.test.tsx +11 -11
- package/src/ui/properties/PropertyGroup.less +2 -2
- package/src/ui/properties/PropertyPanel.less +55 -41
- package/src/ui/properties/SpeechBalloonPanels.stories.tsx +4 -2
- package/src/ui/properties/storyLayouts.tsx +2 -2
- 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.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
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
2
|
import { useState } from 'react';
|
|
3
|
+
import { f } from '../config/builder';
|
|
4
|
+
import { resolveConfigSchema } from '../config/resolve';
|
|
5
|
+
import type { ConfigRule, ControlRenderer } from '../config/types';
|
|
6
|
+
import { PropertyRow } from '../ui/properties/PropertyPanel';
|
|
3
7
|
import { ControlPanel } from './ControlPanel';
|
|
4
8
|
import type { ConfigField } from './types';
|
|
5
9
|
|
|
@@ -75,3 +79,85 @@ export const Minimal: Story = {
|
|
|
75
79
|
/>
|
|
76
80
|
),
|
|
77
81
|
};
|
|
82
|
+
|
|
83
|
+
const sectioned = f.schema({
|
|
84
|
+
showGrid: f.boolean(true),
|
|
85
|
+
cellSize: f.number(20).range(5, 80).step(5).label('Grid spacing'),
|
|
86
|
+
seed: f.number(1).section('Advanced'),
|
|
87
|
+
jitter: f.number(0).range(0, 1).step(0.05).section('Advanced'),
|
|
88
|
+
label: f.string('untitled').section('Advanced').placeholder('name this run'),
|
|
89
|
+
offset: f.custom('vector2', { x: 0, y: 0 }).section('Advanced').label('Offset'),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
function SchemaHarness({
|
|
93
|
+
rules = [],
|
|
94
|
+
renderers,
|
|
95
|
+
}: {
|
|
96
|
+
rules?: readonly ConfigRule[];
|
|
97
|
+
renderers?: Record<string, ControlRenderer>;
|
|
98
|
+
}) {
|
|
99
|
+
const schema = resolveConfigSchema(sectioned, rules);
|
|
100
|
+
const [config, setConfig] = useState<Record<string, unknown>>(sectioned.defaults());
|
|
101
|
+
return (
|
|
102
|
+
<ControlPanel
|
|
103
|
+
schema={schema}
|
|
104
|
+
config={config}
|
|
105
|
+
setConfig={(key, value) => setConfig((prev) => ({ ...prev, [key as string]: value }))}
|
|
106
|
+
renderers={renderers}
|
|
107
|
+
/>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Sections, and a kind no control is registered for — the placeholder names
|
|
112
|
+
* the gap rather than dropping the row. */
|
|
113
|
+
export const Sections: Story = {
|
|
114
|
+
render: () => <SchemaHarness />,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/** The same schema with a control supplied for `vector2`. */
|
|
118
|
+
export const CustomControl: Story = {
|
|
119
|
+
render: () => (
|
|
120
|
+
<SchemaHarness
|
|
121
|
+
renderers={{
|
|
122
|
+
vector2: ({ pref, value, setValue }) => {
|
|
123
|
+
const v = value as { x: number; y: number };
|
|
124
|
+
return (
|
|
125
|
+
<PropertyRow label={(pref as { name: string }).name}>
|
|
126
|
+
<input
|
|
127
|
+
type="number"
|
|
128
|
+
value={v.x}
|
|
129
|
+
onChange={(e) => setValue({ ...v, x: Number(e.target.value) })}
|
|
130
|
+
/>
|
|
131
|
+
<input
|
|
132
|
+
type="number"
|
|
133
|
+
value={v.y}
|
|
134
|
+
onChange={(e) => setValue({ ...v, y: Number(e.target.value) })}
|
|
135
|
+
/>
|
|
136
|
+
</PropertyRow>
|
|
137
|
+
);
|
|
138
|
+
},
|
|
139
|
+
}}
|
|
140
|
+
/>
|
|
141
|
+
),
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const conditional = f.schema({
|
|
145
|
+
showGrid: f.boolean(true),
|
|
146
|
+
cellSize: f.number(20).range(5, 80).step(5).label('Grid spacing').showIf((c) => c.showGrid === true),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
/** `showIf` hides the row while the value stays in config. Toggle the
|
|
150
|
+
* checkbox to watch the slider come and go. */
|
|
151
|
+
export const Conditional: Story = {
|
|
152
|
+
render: () => {
|
|
153
|
+
const schema = resolveConfigSchema(conditional, []);
|
|
154
|
+
const [config, setConfig] = useState<Record<string, unknown>>(conditional.defaults());
|
|
155
|
+
return (
|
|
156
|
+
<ControlPanel
|
|
157
|
+
schema={schema}
|
|
158
|
+
config={config}
|
|
159
|
+
setConfig={(key, value) => setConfig((prev) => ({ ...prev, [key as string]: value }))}
|
|
160
|
+
/>
|
|
161
|
+
);
|
|
162
|
+
},
|
|
163
|
+
};
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { fireEvent, render, screen } from '@testing-library/react';
|
|
2
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { TOOL_PREF_KINDS, type ToolPrefKind } from '@weasel-js/core';
|
|
4
|
+
import type { PrefLeaf } from '@weasel-js/ui';
|
|
5
|
+
import type { ResolvedConfig } from '../config/types';
|
|
6
|
+
import { f } from '../config/builder';
|
|
7
|
+
import { resolveConfigSchema } from '../config/resolve';
|
|
3
8
|
import { ControlPanel } from './ControlPanel';
|
|
4
9
|
import type { ConfigField } from './types';
|
|
5
10
|
|
|
@@ -9,12 +14,13 @@ describe('<ControlPanel> slider', () => {
|
|
|
9
14
|
{ key: 'freq', label: 'Frequency', type: 'slider', min: 0, max: 10, step: 0.5, default: 2 },
|
|
10
15
|
];
|
|
11
16
|
render(<ControlPanel fields={fields} config={{ freq: 2 }} setConfig={vi.fn()} />);
|
|
12
|
-
|
|
13
|
-
|
|
17
|
+
// The row holds two controls -- the range and an editable readout -- inside
|
|
18
|
+
// one wrapping label, so query the slider by role rather than by label text.
|
|
19
|
+
const input = screen.getByRole('slider') as HTMLInputElement;
|
|
14
20
|
expect(input.min).toBe('0');
|
|
15
21
|
expect(input.max).toBe('10');
|
|
16
22
|
expect(input.step).toBe('0.5');
|
|
17
|
-
expect(screen.
|
|
23
|
+
expect(screen.getByDisplayValue('2')).toBeInTheDocument();
|
|
18
24
|
});
|
|
19
25
|
|
|
20
26
|
it('calls setConfig with numeric value on change', () => {
|
|
@@ -23,7 +29,7 @@ describe('<ControlPanel> slider', () => {
|
|
|
23
29
|
{ key: 'freq', label: 'Frequency', type: 'slider', min: 0, max: 10, default: 2 },
|
|
24
30
|
];
|
|
25
31
|
render(<ControlPanel fields={fields} config={{ freq: 2 }} setConfig={setConfig} />);
|
|
26
|
-
fireEvent.change(screen.
|
|
32
|
+
fireEvent.change(screen.getByRole('slider'), { target: { value: '5' } });
|
|
27
33
|
expect(setConfig).toHaveBeenCalledWith('freq', 5);
|
|
28
34
|
});
|
|
29
35
|
});
|
|
@@ -86,8 +92,9 @@ describe('<ControlPanel> number', () => {
|
|
|
86
92
|
{ key: 'n', label: 'N', type: 'number', default: 0, min: 0, max: 100 },
|
|
87
93
|
];
|
|
88
94
|
render(<ControlPanel fields={fields} config={{ n: 50 }} setConfig={setConfig} />);
|
|
89
|
-
|
|
90
|
-
|
|
95
|
+
// The number row commits as you type, so the clamp applies on change
|
|
96
|
+
// rather than waiting for blur.
|
|
97
|
+
fireEvent.change(screen.getByRole('spinbutton'), { target: { value: '999' } });
|
|
91
98
|
expect(setConfig).toHaveBeenCalledWith('n', 100);
|
|
92
99
|
});
|
|
93
100
|
});
|
|
@@ -136,14 +143,220 @@ describe('<ControlPanel> color', () => {
|
|
|
136
143
|
});
|
|
137
144
|
|
|
138
145
|
describe('<ControlPanel> defensive', () => {
|
|
139
|
-
it('
|
|
146
|
+
it('names an unknown field type rather than dropping its row', () => {
|
|
140
147
|
const fields = [
|
|
141
148
|
{ key: 'mystery', label: 'Mystery', type: 'mystery', default: 1 },
|
|
149
|
+
{ key: 'ok', label: 'Ok', type: 'checkbox', default: true },
|
|
142
150
|
] as unknown as ConfigField[];
|
|
143
151
|
const { container } = render(
|
|
144
|
-
<ControlPanel fields={fields} config={{ mystery: 1 }} setConfig={vi.fn()} />,
|
|
152
|
+
<ControlPanel fields={fields} config={{ mystery: 1, ok: true }} setConfig={vi.fn()} />,
|
|
145
153
|
);
|
|
146
154
|
expect(container.querySelector('.lk-control-panel')).not.toBeNull();
|
|
147
|
-
|
|
155
|
+
// A kind a lab has not wired up must not blank the panel, and a silently
|
|
156
|
+
// dropped row reads as "this control does not exist".
|
|
157
|
+
expect(screen.getByText(/mystery/)).toBeInTheDocument();
|
|
158
|
+
expect(screen.getByLabelText('Ok')).toBeInTheDocument();
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe('<ControlPanel> schema', () => {
|
|
163
|
+
it('renders a schema through the built-in rows', () => {
|
|
164
|
+
const schema = resolveConfigSchema(f.schema({ showGrid: f.boolean(true) }), []);
|
|
165
|
+
render(<ControlPanel schema={schema} config={{ showGrid: true }} setConfig={vi.fn()} />);
|
|
166
|
+
expect(screen.getByLabelText('Show grid')).toBeInTheDocument();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('writes back through setConfig keyed by path', () => {
|
|
170
|
+
const setConfig = vi.fn();
|
|
171
|
+
const schema = resolveConfigSchema(f.schema({ showGrid: f.boolean(true) }), []);
|
|
172
|
+
render(<ControlPanel schema={schema} config={{ showGrid: true }} setConfig={setConfig} />);
|
|
173
|
+
fireEvent.click(screen.getByLabelText('Show grid'));
|
|
174
|
+
expect(setConfig).toHaveBeenCalledWith('showGrid', false);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('picks a slider for a bounded number and an input for an open one', () => {
|
|
178
|
+
const schema = resolveConfigSchema(
|
|
179
|
+
f.schema({ a: f.number(5).range(0, 10), b: f.number(5) }),
|
|
180
|
+
[],
|
|
181
|
+
);
|
|
182
|
+
render(<ControlPanel schema={schema} config={{ a: 5, b: 5 }} setConfig={vi.fn()} />);
|
|
183
|
+
expect(screen.getByLabelText('B')).toHaveAttribute('type', 'number');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('falls back to the leaf default when config holds no value', () => {
|
|
187
|
+
const schema = resolveConfigSchema(f.schema({ tint: f.color('#123456') }), []);
|
|
188
|
+
render(<ControlPanel schema={schema} config={{}} setConfig={vi.fn()} />);
|
|
189
|
+
expect(screen.getByLabelText('Tint')).toHaveValue('#123456');
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe('<ControlPanel> renderers', () => {
|
|
194
|
+
it('renderers[path] beats renderers[kind]', () => {
|
|
195
|
+
const schema = resolveConfigSchema(f.schema({ tint: f.color('#ffffff') }), []);
|
|
196
|
+
render(
|
|
197
|
+
<ControlPanel
|
|
198
|
+
schema={schema}
|
|
199
|
+
config={{ tint: '#ffffff' }}
|
|
200
|
+
setConfig={vi.fn()}
|
|
201
|
+
renderers={{ color: () => <span>by-kind</span>, tint: () => <span>by-path</span> }}
|
|
202
|
+
/>,
|
|
203
|
+
);
|
|
204
|
+
expect(screen.getByText('by-path')).toBeInTheDocument();
|
|
205
|
+
expect(screen.queryByText('by-kind')).not.toBeInTheDocument();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('a lab renderer for the path beats the node\'s own .render', () => {
|
|
209
|
+
const schema = resolveConfigSchema(
|
|
210
|
+
f.schema({ tint: f.color('#ffffff').render(() => <span>by-node</span>) }),
|
|
211
|
+
[],
|
|
212
|
+
);
|
|
213
|
+
render(
|
|
214
|
+
<ControlPanel
|
|
215
|
+
schema={schema}
|
|
216
|
+
config={{ tint: '#ffffff' }}
|
|
217
|
+
setConfig={vi.fn()}
|
|
218
|
+
renderers={{ tint: () => <span>by-path</span> }}
|
|
219
|
+
/>,
|
|
220
|
+
);
|
|
221
|
+
expect(screen.getByText('by-path')).toBeInTheDocument();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("a node's .render beats a lab renderer for the kind", () => {
|
|
225
|
+
const schema = resolveConfigSchema(
|
|
226
|
+
f.schema({ tint: f.color('#ffffff').render(() => <span>by-node</span>) }),
|
|
227
|
+
[],
|
|
228
|
+
);
|
|
229
|
+
render(
|
|
230
|
+
<ControlPanel
|
|
231
|
+
schema={schema}
|
|
232
|
+
config={{ tint: '#ffffff' }}
|
|
233
|
+
setConfig={vi.fn()}
|
|
234
|
+
renderers={{ color: () => <span>by-kind</span> }}
|
|
235
|
+
/>,
|
|
236
|
+
);
|
|
237
|
+
expect(screen.getByText('by-node')).toBeInTheDocument();
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('supplies a control for a kind labkit does not ship', () => {
|
|
241
|
+
const schema = resolveConfigSchema(f.schema({ offset: f.custom('vector2', { x: 1 }) }), []);
|
|
242
|
+
render(
|
|
243
|
+
<ControlPanel
|
|
244
|
+
schema={schema}
|
|
245
|
+
config={{ offset: { x: 1 } }}
|
|
246
|
+
setConfig={vi.fn()}
|
|
247
|
+
renderers={{ vector2: (ctx) => <span>vec:{String((ctx.value as { x: number }).x)}</span> }}
|
|
248
|
+
/>,
|
|
249
|
+
);
|
|
250
|
+
expect(screen.getByText('vec:1')).toBeInTheDocument();
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('a renderer returning null collapses the row', () => {
|
|
254
|
+
const schema = resolveConfigSchema(f.schema({ tint: f.color('#ffffff') }), []);
|
|
255
|
+
const { container } = render(
|
|
256
|
+
<ControlPanel
|
|
257
|
+
schema={schema}
|
|
258
|
+
config={{ tint: '#ffffff' }}
|
|
259
|
+
setConfig={vi.fn()}
|
|
260
|
+
renderers={{ color: () => null }}
|
|
261
|
+
/>,
|
|
262
|
+
);
|
|
263
|
+
expect(container.querySelector('.lk-property-row')).toBeNull();
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
describe('<ControlPanel> visibility and sections', () => {
|
|
268
|
+
it('hides a row whose showIf is false', () => {
|
|
269
|
+
const schema = resolveConfigSchema(
|
|
270
|
+
f.schema({
|
|
271
|
+
showGrid: f.boolean(true),
|
|
272
|
+
cellSize: f.number(20).showIf((c) => c.showGrid === true),
|
|
273
|
+
}),
|
|
274
|
+
[],
|
|
275
|
+
);
|
|
276
|
+
const { rerender } = render(
|
|
277
|
+
<ControlPanel schema={schema} config={{ showGrid: true, cellSize: 20 }} setConfig={vi.fn()} />,
|
|
278
|
+
);
|
|
279
|
+
expect(screen.getByLabelText('Cell size')).toBeInTheDocument();
|
|
280
|
+
rerender(
|
|
281
|
+
<ControlPanel
|
|
282
|
+
schema={schema}
|
|
283
|
+
config={{ showGrid: false, cellSize: 20 }}
|
|
284
|
+
setConfig={vi.fn()}
|
|
285
|
+
/>,
|
|
286
|
+
);
|
|
287
|
+
expect(screen.queryByLabelText('Cell size')).not.toBeInTheDocument();
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('keeps a hidden leaf out unless asked for it', () => {
|
|
291
|
+
const schema = resolveConfigSchema(f.schema({ seed: f.number(0).hidden() }), []);
|
|
292
|
+
const { rerender } = render(
|
|
293
|
+
<ControlPanel schema={schema} config={{ seed: 0 }} setConfig={vi.fn()} />,
|
|
294
|
+
);
|
|
295
|
+
expect(screen.queryByLabelText('Seed')).not.toBeInTheDocument();
|
|
296
|
+
rerender(<ControlPanel schema={schema} config={{ seed: 0 }} setConfig={vi.fn()} showHidden />);
|
|
297
|
+
expect(screen.getByLabelText('Seed')).toBeInTheDocument();
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('groups sectioned leaves under a heading, ungrouped ones first', () => {
|
|
301
|
+
const schema = resolveConfigSchema(
|
|
302
|
+
f.schema({ showGrid: f.boolean(true), seed: f.number(0).section('Advanced') }),
|
|
303
|
+
[],
|
|
304
|
+
);
|
|
305
|
+
const { container } = render(
|
|
306
|
+
<ControlPanel schema={schema} config={{ showGrid: true, seed: 0 }} setConfig={vi.fn()} />,
|
|
307
|
+
);
|
|
308
|
+
expect(screen.getByText('Advanced')).toBeInTheDocument();
|
|
309
|
+
const group = container.querySelector('.lk-property-group');
|
|
310
|
+
expect(group).not.toBeNull();
|
|
311
|
+
expect(group?.textContent).toContain('Seed');
|
|
312
|
+
expect(group?.textContent).not.toContain('Show grid');
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
describe('<ControlPanel> built-in kind coverage', () => {
|
|
317
|
+
const KIND = 'spline' as ToolPrefKind;
|
|
318
|
+
afterEach(() => {
|
|
319
|
+
delete (TOOL_PREF_KINDS as Record<string, true>)[KIND];
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const schemaWith = (...leaves: PrefLeaf[]): ResolvedConfig => ({
|
|
323
|
+
group: {
|
|
324
|
+
name: 'root',
|
|
325
|
+
children: Object.fromEntries(leaves.map((leaf, i) => [`k${i}`, leaf])),
|
|
326
|
+
},
|
|
327
|
+
sections: [],
|
|
328
|
+
showIf: new Map(),
|
|
329
|
+
renderers: {},
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('declines paint and object with a named row rather than a blank one', () => {
|
|
333
|
+
render(
|
|
334
|
+
<ControlPanel
|
|
335
|
+
schema={schemaWith(
|
|
336
|
+
{ kind: 'paint', name: 'Fill', description: '', default: null },
|
|
337
|
+
{
|
|
338
|
+
kind: 'object', name: 'Stroke', description: '', default: {},
|
|
339
|
+
children: { width: { kind: 'number', name: 'Width', description: '', default: 1 } },
|
|
340
|
+
},
|
|
341
|
+
)}
|
|
342
|
+
config={{}}
|
|
343
|
+
setConfig={vi.fn()}
|
|
344
|
+
/>,
|
|
345
|
+
);
|
|
346
|
+
expect(screen.getByText(/no control for .paint./)).toBeInTheDocument();
|
|
347
|
+
expect(screen.getByText(/no control for .object./)).toBeInTheDocument();
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('throws for a built-in kind with no case arm', () => {
|
|
351
|
+
(TOOL_PREF_KINDS as Record<string, true>)[KIND] = true;
|
|
352
|
+
expect(() =>
|
|
353
|
+
render(
|
|
354
|
+
<ControlPanel
|
|
355
|
+
schema={schemaWith({ kind: KIND, name: 'Curve', description: '', default: null })}
|
|
356
|
+
config={{}}
|
|
357
|
+
setConfig={vi.fn()}
|
|
358
|
+
/>,
|
|
359
|
+
),
|
|
360
|
+
).toThrow(/spline/);
|
|
148
361
|
});
|
|
149
362
|
});
|