@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,5 +1,9 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { PropertyRow } from '@weasel-js/ui';
|
|
2
3
|
import { useState } from 'react';
|
|
4
|
+
import { f } from '../config/builder';
|
|
5
|
+
import { resolveConfigSchema } from '../config/resolve';
|
|
6
|
+
import type { ConfigRule, ControlRenderer } from '../config/types';
|
|
3
7
|
import { ControlPanel } from './ControlPanel';
|
|
4
8
|
import type { ConfigField } from './types';
|
|
5
9
|
|
|
@@ -75,3 +79,124 @@ 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 described = f.schema({
|
|
145
|
+
frequency: f
|
|
146
|
+
.number(2)
|
|
147
|
+
.range(0.1, 10)
|
|
148
|
+
.step(0.1)
|
|
149
|
+
.describe('Cycles per second the oscillator runs at.'),
|
|
150
|
+
amplitude: f.number(0.5).range(0, 1).step(0.05),
|
|
151
|
+
showGrid: f.boolean(true).describe('Draw the alignment grid behind the plot.'),
|
|
152
|
+
wave: f
|
|
153
|
+
.enum('sine', [
|
|
154
|
+
{ value: 'sine', label: 'Sine' },
|
|
155
|
+
{ value: 'square', label: 'Square' },
|
|
156
|
+
])
|
|
157
|
+
.describe('Waveform the generator emits.'),
|
|
158
|
+
title: f.string('My experiment'),
|
|
159
|
+
tint: f.color('#3a86ff').describe('Plot line color.'),
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
/** A leaf with a `describe(...)` gets an \u24d8 beside its label whose tooltip
|
|
163
|
+
* carries the text; leaves without one stay bare. */
|
|
164
|
+
export const Described: Story = {
|
|
165
|
+
render: () => {
|
|
166
|
+
const schema = resolveConfigSchema(described, []);
|
|
167
|
+
const [config, setConfig] = useState<Record<string, unknown>>(described.defaults());
|
|
168
|
+
return (
|
|
169
|
+
<ControlPanel
|
|
170
|
+
schema={schema}
|
|
171
|
+
config={config}
|
|
172
|
+
setConfig={(key, value) => setConfig((prev) => ({ ...prev, [key as string]: value }))}
|
|
173
|
+
/>
|
|
174
|
+
);
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const conditional = f.schema({
|
|
179
|
+
showGrid: f.boolean(true),
|
|
180
|
+
cellSize: f
|
|
181
|
+
.number(20)
|
|
182
|
+
.range(5, 80)
|
|
183
|
+
.step(5)
|
|
184
|
+
.label('Grid spacing')
|
|
185
|
+
.showIf((c) => c.showGrid === true),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
/** `showIf` hides the row while the value stays in config. Toggle the
|
|
189
|
+
* checkbox to watch the slider come and go. */
|
|
190
|
+
export const Conditional: Story = {
|
|
191
|
+
render: () => {
|
|
192
|
+
const schema = resolveConfigSchema(conditional, []);
|
|
193
|
+
const [config, setConfig] = useState<Record<string, unknown>>(conditional.defaults());
|
|
194
|
+
return (
|
|
195
|
+
<ControlPanel
|
|
196
|
+
schema={schema}
|
|
197
|
+
config={config}
|
|
198
|
+
setConfig={(key, value) => setConfig((prev) => ({ ...prev, [key as string]: value }))}
|
|
199
|
+
/>
|
|
200
|
+
);
|
|
201
|
+
},
|
|
202
|
+
};
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { fireEvent, render, screen } from '@testing-library/react';
|
|
2
|
-
import {
|
|
2
|
+
import { TOOL_PREF_KINDS, type ToolPrefKind } from '@weasel-js/core';
|
|
3
|
+
import type { PrefLeaf } from '@weasel-js/ui';
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { f } from '../config/builder';
|
|
6
|
+
import { resolveConfigSchema } from '../config/resolve';
|
|
7
|
+
import type { ResolvedConfig } from '../config/types';
|
|
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,242 @@ 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
|
+
it('hangs a described leaf\u2019s description off a help affordance, and leaves an undescribed leaf bare', () => {
|
|
193
|
+
const schema = resolveConfigSchema(
|
|
194
|
+
f.schema({
|
|
195
|
+
showGrid: f.boolean(true).describe('Draw the alignment grid.'),
|
|
196
|
+
snap: f.boolean(true),
|
|
197
|
+
}),
|
|
198
|
+
[],
|
|
199
|
+
);
|
|
200
|
+
render(
|
|
201
|
+
<ControlPanel schema={schema} config={{ showGrid: true, snap: true }} setConfig={vi.fn()} />,
|
|
202
|
+
);
|
|
203
|
+
expect(screen.getByRole('button', { name: 'About Show grid' })).toBeInTheDocument();
|
|
204
|
+
expect(screen.getAllByRole('button', { name: /^About / })).toHaveLength(1);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe('<ControlPanel> renderers', () => {
|
|
209
|
+
it('renderers[path] beats renderers[kind]', () => {
|
|
210
|
+
const schema = resolveConfigSchema(f.schema({ tint: f.color('#ffffff') }), []);
|
|
211
|
+
render(
|
|
212
|
+
<ControlPanel
|
|
213
|
+
schema={schema}
|
|
214
|
+
config={{ tint: '#ffffff' }}
|
|
215
|
+
setConfig={vi.fn()}
|
|
216
|
+
renderers={{ color: () => <span>by-kind</span>, tint: () => <span>by-path</span> }}
|
|
217
|
+
/>,
|
|
218
|
+
);
|
|
219
|
+
expect(screen.getByText('by-path')).toBeInTheDocument();
|
|
220
|
+
expect(screen.queryByText('by-kind')).not.toBeInTheDocument();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("a lab renderer for the path beats the node's own .render", () => {
|
|
224
|
+
const schema = resolveConfigSchema(
|
|
225
|
+
f.schema({ tint: f.color('#ffffff').render(() => <span>by-node</span>) }),
|
|
226
|
+
[],
|
|
227
|
+
);
|
|
228
|
+
render(
|
|
229
|
+
<ControlPanel
|
|
230
|
+
schema={schema}
|
|
231
|
+
config={{ tint: '#ffffff' }}
|
|
232
|
+
setConfig={vi.fn()}
|
|
233
|
+
renderers={{ tint: () => <span>by-path</span> }}
|
|
234
|
+
/>,
|
|
235
|
+
);
|
|
236
|
+
expect(screen.getByText('by-path')).toBeInTheDocument();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("a node's .render beats a lab renderer for the kind", () => {
|
|
240
|
+
const schema = resolveConfigSchema(
|
|
241
|
+
f.schema({ tint: f.color('#ffffff').render(() => <span>by-node</span>) }),
|
|
242
|
+
[],
|
|
243
|
+
);
|
|
244
|
+
render(
|
|
245
|
+
<ControlPanel
|
|
246
|
+
schema={schema}
|
|
247
|
+
config={{ tint: '#ffffff' }}
|
|
248
|
+
setConfig={vi.fn()}
|
|
249
|
+
renderers={{ color: () => <span>by-kind</span> }}
|
|
250
|
+
/>,
|
|
251
|
+
);
|
|
252
|
+
expect(screen.getByText('by-node')).toBeInTheDocument();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('supplies a control for a kind labkit does not ship', () => {
|
|
256
|
+
const schema = resolveConfigSchema(f.schema({ offset: f.custom('vector2', { x: 1 }) }), []);
|
|
257
|
+
render(
|
|
258
|
+
<ControlPanel
|
|
259
|
+
schema={schema}
|
|
260
|
+
config={{ offset: { x: 1 } }}
|
|
261
|
+
setConfig={vi.fn()}
|
|
262
|
+
renderers={{ vector2: (ctx) => <span>vec:{String((ctx.value as { x: number }).x)}</span> }}
|
|
263
|
+
/>,
|
|
264
|
+
);
|
|
265
|
+
expect(screen.getByText('vec:1')).toBeInTheDocument();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('a renderer returning null collapses the row', () => {
|
|
269
|
+
const schema = resolveConfigSchema(f.schema({ tint: f.color('#ffffff') }), []);
|
|
270
|
+
render(
|
|
271
|
+
<ControlPanel
|
|
272
|
+
schema={schema}
|
|
273
|
+
config={{ tint: '#ffffff' }}
|
|
274
|
+
setConfig={vi.fn()}
|
|
275
|
+
renderers={{ color: () => null }}
|
|
276
|
+
/>,
|
|
277
|
+
);
|
|
278
|
+
expect(screen.queryByText('Tint')).toBeNull();
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
describe('<ControlPanel> visibility and sections', () => {
|
|
283
|
+
it('hides a row whose showIf is false', () => {
|
|
284
|
+
const schema = resolveConfigSchema(
|
|
285
|
+
f.schema({
|
|
286
|
+
showGrid: f.boolean(true),
|
|
287
|
+
cellSize: f.number(20).showIf((c) => c.showGrid === true),
|
|
288
|
+
}),
|
|
289
|
+
[],
|
|
290
|
+
);
|
|
291
|
+
const { rerender } = render(
|
|
292
|
+
<ControlPanel
|
|
293
|
+
schema={schema}
|
|
294
|
+
config={{ showGrid: true, cellSize: 20 }}
|
|
295
|
+
setConfig={vi.fn()}
|
|
296
|
+
/>,
|
|
297
|
+
);
|
|
298
|
+
expect(screen.getByLabelText('Cell size')).toBeInTheDocument();
|
|
299
|
+
rerender(
|
|
300
|
+
<ControlPanel
|
|
301
|
+
schema={schema}
|
|
302
|
+
config={{ showGrid: false, cellSize: 20 }}
|
|
303
|
+
setConfig={vi.fn()}
|
|
304
|
+
/>,
|
|
305
|
+
);
|
|
306
|
+
expect(screen.queryByLabelText('Cell size')).not.toBeInTheDocument();
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('keeps a hidden leaf out unless asked for it', () => {
|
|
310
|
+
const schema = resolveConfigSchema(f.schema({ seed: f.number(0).hidden() }), []);
|
|
311
|
+
const { rerender } = render(
|
|
312
|
+
<ControlPanel schema={schema} config={{ seed: 0 }} setConfig={vi.fn()} />,
|
|
313
|
+
);
|
|
314
|
+
expect(screen.queryByLabelText('Seed')).not.toBeInTheDocument();
|
|
315
|
+
rerender(<ControlPanel schema={schema} config={{ seed: 0 }} setConfig={vi.fn()} showHidden />);
|
|
316
|
+
expect(screen.getByLabelText('Seed')).toBeInTheDocument();
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('groups sectioned leaves under a heading, ungrouped ones first', () => {
|
|
320
|
+
const schema = resolveConfigSchema(
|
|
321
|
+
f.schema({ showGrid: f.boolean(true), seed: f.number(0).section('Advanced') }),
|
|
322
|
+
[],
|
|
323
|
+
);
|
|
324
|
+
render(
|
|
325
|
+
<ControlPanel schema={schema} config={{ showGrid: true, seed: 0 }} setConfig={vi.fn()} />,
|
|
326
|
+
);
|
|
327
|
+
expect(screen.getByText('Advanced')).toBeInTheDocument();
|
|
328
|
+
const group = screen.getByRole('heading', { name: 'Advanced' }).parentElement;
|
|
329
|
+
expect(group).not.toBeNull();
|
|
330
|
+
expect(group?.textContent).toContain('Seed');
|
|
331
|
+
expect(group?.textContent).not.toContain('Show grid');
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
describe('<ControlPanel> built-in kind coverage', () => {
|
|
336
|
+
const KIND = 'spline' as ToolPrefKind;
|
|
337
|
+
afterEach(() => {
|
|
338
|
+
delete (TOOL_PREF_KINDS as Record<string, true>)[KIND];
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
const schemaWith = (...leaves: PrefLeaf[]): ResolvedConfig => ({
|
|
342
|
+
group: {
|
|
343
|
+
name: 'root',
|
|
344
|
+
children: Object.fromEntries(leaves.map((leaf, i) => [`k${i}`, leaf])),
|
|
345
|
+
},
|
|
346
|
+
sections: [],
|
|
347
|
+
showIf: new Map(),
|
|
348
|
+
renderers: {},
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('declines paint and object with a named row rather than a blank one', () => {
|
|
352
|
+
render(
|
|
353
|
+
<ControlPanel
|
|
354
|
+
schema={schemaWith(
|
|
355
|
+
{ kind: 'paint', name: 'Fill', description: '', default: null },
|
|
356
|
+
{
|
|
357
|
+
kind: 'object',
|
|
358
|
+
name: 'Stroke',
|
|
359
|
+
description: '',
|
|
360
|
+
default: {},
|
|
361
|
+
children: { width: { kind: 'number', name: 'Width', description: '', default: 1 } },
|
|
362
|
+
},
|
|
363
|
+
)}
|
|
364
|
+
config={{}}
|
|
365
|
+
setConfig={vi.fn()}
|
|
366
|
+
/>,
|
|
367
|
+
);
|
|
368
|
+
expect(screen.getByText(/no control for .paint./)).toBeInTheDocument();
|
|
369
|
+
expect(screen.getByText(/no control for .object./)).toBeInTheDocument();
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('throws for a built-in kind with no case arm', () => {
|
|
373
|
+
(TOOL_PREF_KINDS as Record<string, true>)[KIND] = true;
|
|
374
|
+
expect(() =>
|
|
375
|
+
render(
|
|
376
|
+
<ControlPanel
|
|
377
|
+
schema={schemaWith({ kind: KIND, name: 'Curve', description: '', default: null })}
|
|
378
|
+
config={{}}
|
|
379
|
+
setConfig={vi.fn()}
|
|
380
|
+
/>,
|
|
381
|
+
),
|
|
382
|
+
).toThrow(/spline/);
|
|
148
383
|
});
|
|
149
384
|
});
|