@weasel-js/labkit 1.7.1 → 1.7.2
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/chrome/index.js +4 -4
- package/dist/{chunk-GAHELLJT.js → chunk-RODIY7HI.js} +7 -7
- package/dist/{chunk-GAHELLJT.js.map → chunk-RODIY7HI.js.map} +1 -1
- package/dist/{chunk-4KSC7NXL.js → chunk-U6UQVGFP.js} +1984 -1892
- package/dist/chunk-U6UQVGFP.js.map +1 -0
- package/dist/chunk-WFALF2X2.js +302 -0
- package/dist/chunk-WFALF2X2.js.map +1 -0
- package/dist/{chunk-JFOJESJF.js → chunk-WWJDPNZZ.js} +22 -22
- package/dist/chunk-WWJDPNZZ.js.map +1 -0
- package/dist/{chunk-CEGFXTBD.js → chunk-XRX5S5NT.js} +4 -4
- package/dist/{chunk-CEGFXTBD.js.map → chunk-XRX5S5NT.js.map} +1 -1
- package/dist/{chunk-FB4MIZOO.js → chunk-XZDCEMEL.js} +6 -6
- package/dist/chunk-XZDCEMEL.js.map +1 -0
- package/dist/controls/index.d.ts +55 -3
- package/dist/controls/index.js +3 -2
- package/dist/fonts/oswald-tabular-digits.woff2 +0 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +135 -134
- package/dist/index.js.map +1 -1
- package/dist/layers/index.js +1 -1
- package/dist/passthrough/weasel-ui.js +1 -1
- package/dist/primitives/index.js +3 -3
- package/dist/styles.css +228 -11
- package/package.json +11 -11
- package/src/annotations/capture.browser.test.ts +9 -3
- package/src/canvas/Stage.test.tsx +34 -25
- package/src/chrome/labRegions.test.tsx +37 -26
- package/src/chrome/regions/PaletteRegion.test.tsx +2 -2
- package/src/controls/ControlMatrix.less +245 -0
- package/src/controls/ControlMatrix.stories.less +13 -0
- package/src/controls/ControlMatrix.stories.tsx +135 -0
- package/src/controls/ControlMatrix.test.tsx +206 -0
- package/src/controls/ControlMatrix.tsx +446 -0
- package/src/controls/ControlPanel.tsx +4 -2
- package/src/controls/index.ts +6 -0
- package/src/dragdrop/dragDrop.test.tsx +34 -33
- package/src/index.ts +6 -0
- package/src/lab/Lab.chrome.test.tsx +15 -10
- package/src/lab/Lab.nebula.test.tsx +5 -5
- package/src/lab/Lab.surface.test.tsx +30 -21
- package/src/lab/Lab.test.tsx +75 -74
- package/src/lab/Lab.undock.test.tsx +15 -10
- package/src/lab/LabHeader.test.tsx +15 -14
- package/src/lab/LabPalette.test.tsx +12 -7
- package/src/lab/LabShell.less +1 -2
- package/src/lab/LabZoom.test.tsx +55 -44
- package/src/lab/Workspace.surface.test.tsx +16 -13
- package/src/lab/Workspace.test.tsx +32 -27
- package/src/overview/OverviewMarks.test.tsx +9 -8
- package/src/overview/TrialOverview.test.tsx +18 -17
- package/src/primitives/FpsMeter.less +1 -2
- package/src/primitives/JobProgress.less +1 -2
- package/src/primitives/ScaleIndicator.less +1 -1
- package/src/primitives/StatusBar.less +1 -1
- package/src/specimen/Specimen.less +1 -0
- package/src/styles.less +3 -0
- package/src/trial/Trial.annotations.persist.test.tsx +20 -9
- package/src/trial/Trial.annotations.test.tsx +46 -41
- package/src/trial/Trial.canvas.test.tsx +21 -17
- package/src/trial/Trial.config.test.tsx +34 -27
- package/src/trial/Trial.loupe.test.tsx +27 -18
- package/src/trial/Trial.targets.test.tsx +9 -8
- package/src/trial/Trial.test.tsx +51 -46
- package/src/trial/Trial.trialId.test.tsx +7 -6
- package/dist/chunk-4KSC7NXL.js.map +0 -1
- package/dist/chunk-FB4MIZOO.js.map +0 -1
- package/dist/chunk-JFOJESJF.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weasel-js/labkit",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.2",
|
|
4
4
|
"description": "React widgets for building self-contained interactive lab pages",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "orochi235",
|
|
@@ -115,18 +115,18 @@
|
|
|
115
115
|
"dev:3d": "LABKIT_EXAMPLE=3d-lab vite"
|
|
116
116
|
},
|
|
117
117
|
"peerDependencies": {
|
|
118
|
-
"@weasel-js/core": "1.7.
|
|
119
|
-
"@weasel-js/theme": "1.7.
|
|
118
|
+
"@weasel-js/core": "1.7.2",
|
|
119
|
+
"@weasel-js/theme": "1.7.2",
|
|
120
120
|
"react": "^19.0.0",
|
|
121
121
|
"react-dom": "^19.0.0"
|
|
122
122
|
},
|
|
123
123
|
"dependencies": {
|
|
124
|
-
"@weasel-js/geom": "1.7.
|
|
125
|
-
"@weasel-js/kernel3d": "1.7.
|
|
126
|
-
"@weasel-js/loupe": "1.7.
|
|
127
|
-
"@weasel-js/quantity": "1.7.
|
|
128
|
-
"@weasel-js/svg": "1.7.
|
|
129
|
-
"@weasel-js/ui": "1.7.
|
|
124
|
+
"@weasel-js/geom": "1.7.2",
|
|
125
|
+
"@weasel-js/kernel3d": "1.7.2",
|
|
126
|
+
"@weasel-js/loupe": "1.7.2",
|
|
127
|
+
"@weasel-js/quantity": "1.7.2",
|
|
128
|
+
"@weasel-js/svg": "1.7.2",
|
|
129
|
+
"@weasel-js/ui": "1.7.2",
|
|
130
130
|
"earcut": "3.2.3",
|
|
131
131
|
"polygon-clipping": "^0.15.7",
|
|
132
132
|
"react-aria-components": "^1.5.0",
|
|
@@ -136,8 +136,8 @@
|
|
|
136
136
|
"devDependencies": {
|
|
137
137
|
"@biomejs/biome": "^2.4.12",
|
|
138
138
|
"@testing-library/jest-dom": "^7.0.1",
|
|
139
|
-
"@weasel-js/guides": "1.7.
|
|
140
|
-
"@weasel-js/modes": "1.7.
|
|
139
|
+
"@weasel-js/guides": "1.7.2",
|
|
140
|
+
"@weasel-js/modes": "1.7.2",
|
|
141
141
|
"less": "^4.2.0",
|
|
142
142
|
"rollup-plugin-dts": "^6.4.1",
|
|
143
143
|
"tsx": "^4.19.0",
|
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
* default family registered lazily the way an app registers it: a text mark
|
|
4
4
|
* captured before anything has drawn text still carries its glyphs.
|
|
5
5
|
*/
|
|
6
|
-
|
|
6
|
+
|
|
7
7
|
import { registerFont } from '@weasel-js/font';
|
|
8
8
|
import { _resetFontRegistryForTests } from '@weasel-js/font/test-seams';
|
|
9
|
-
import {
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
10
10
|
import metricsUrl from '../../../../assets/fonts/inter/inter.json?url';
|
|
11
11
|
import atlasUrl from '../../../../assets/fonts/inter/inter.png?url';
|
|
12
|
+
import { createAnnotationStore } from './store';
|
|
12
13
|
|
|
13
14
|
/** Pixels where the mark's red ink landed on the transparent export. */
|
|
14
15
|
async function inkPixels(blob: Blob): Promise<number> {
|
|
@@ -32,7 +33,12 @@ describe('a raster capture of a text mark', () => {
|
|
|
32
33
|
const store = createAnnotationStore({
|
|
33
34
|
targets: () => [{ id: 'bare', content: { w: 120, h: 40 } }],
|
|
34
35
|
});
|
|
35
|
-
store.add({
|
|
36
|
+
store.add({
|
|
37
|
+
target: 'bare',
|
|
38
|
+
kind: 'text',
|
|
39
|
+
title: 'Hello',
|
|
40
|
+
frac: { x: 0.1, y: 0.2, w: 0.5, h: 0.5 },
|
|
41
|
+
});
|
|
36
42
|
|
|
37
43
|
const result = await store.capture('bare', { format: 'png', scale: 2 });
|
|
38
44
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { act, fireEvent,
|
|
1
|
+
import { act, fireEvent, screen, within } from '@testing-library/react';
|
|
2
|
+
import { renderSettled } from '@weasel-js/react/testing/renderSettled';
|
|
2
3
|
import { useState } from 'react';
|
|
3
4
|
import { beforeAll, describe, expect, it, vi } from 'vitest';
|
|
4
5
|
import { defineInstrument } from '../instrument/defineInstrument';
|
|
@@ -47,8 +48,8 @@ describe('fitStage', () => {
|
|
|
47
48
|
});
|
|
48
49
|
|
|
49
50
|
describe('<Stage>', () => {
|
|
50
|
-
it('draws its content through the camera', () => {
|
|
51
|
-
const { container } =
|
|
51
|
+
it('draws its content through the camera', async () => {
|
|
52
|
+
const { container } = await renderSettled(
|
|
52
53
|
<Stage
|
|
53
54
|
size={{ width: 300, height: 100 }}
|
|
54
55
|
view={{ zoom: 2, pan: { x: 10, y: 20 } }}
|
|
@@ -66,7 +67,7 @@ describe('<Stage>', () => {
|
|
|
66
67
|
expect(el.style.getPropertyValue('--lk-stage-h')).toBe('100px');
|
|
67
68
|
});
|
|
68
69
|
|
|
69
|
-
it('zooms on the wheel', () => {
|
|
70
|
+
it('zooms on the wheel', async () => {
|
|
70
71
|
function Host() {
|
|
71
72
|
const [view, setView] = useState<ViewTransform>({ zoom: 1, pan: { x: 0, y: 0 } });
|
|
72
73
|
return (
|
|
@@ -75,14 +76,14 @@ describe('<Stage>', () => {
|
|
|
75
76
|
</Stage>
|
|
76
77
|
);
|
|
77
78
|
}
|
|
78
|
-
const { container } =
|
|
79
|
+
const { container } = await renderSettled(<Host />);
|
|
79
80
|
const host = container.querySelector('.lk-stage');
|
|
80
81
|
if (!host) throw new Error('no stage');
|
|
81
82
|
fireEvent.wheel(host, { deltaY: -200, clientX: 0, clientY: 0 });
|
|
82
83
|
expect(zoomOf(content(container))).toBeGreaterThan(1);
|
|
83
84
|
});
|
|
84
85
|
|
|
85
|
-
it('zooms on a wheel handed to its camera from outside the stage', () => {
|
|
86
|
+
it('zooms on a wheel handed to its camera from outside the stage', async () => {
|
|
86
87
|
// What an annotation target's input box does: it is portalled out of the
|
|
87
88
|
// stage, so its wheel never bubbles here.
|
|
88
89
|
const slot: CameraWheelSlot = { current: null };
|
|
@@ -96,19 +97,19 @@ describe('<Stage>', () => {
|
|
|
96
97
|
</CameraWheelContext.Provider>
|
|
97
98
|
);
|
|
98
99
|
}
|
|
99
|
-
const { container } =
|
|
100
|
-
act(() => {
|
|
100
|
+
const { container } = await renderSettled(<Host />);
|
|
101
|
+
await act(async () => {
|
|
101
102
|
slot.current?.(new WheelEvent('wheel', { deltaY: -200, cancelable: true }));
|
|
102
103
|
});
|
|
103
104
|
expect(zoomOf(content(container))).toBeGreaterThan(1);
|
|
104
105
|
});
|
|
105
106
|
|
|
106
|
-
it('listens for the wheel actively, so it can keep the page from scrolling', () => {
|
|
107
|
+
it('listens for the wheel actively, so it can keep the page from scrolling', async () => {
|
|
107
108
|
// A proxy: jsdom does not enforce passive listeners, so a preventDefault in
|
|
108
109
|
// a passive one succeeds here and fails in a browser. React registers its
|
|
109
110
|
// wheel listeners passive; asserting the registration is what can fail.
|
|
110
111
|
const add = vi.spyOn(HTMLElement.prototype, 'addEventListener');
|
|
111
|
-
const { container } =
|
|
112
|
+
const { container } = await renderSettled(
|
|
112
113
|
<Stage
|
|
113
114
|
size={{ width: 100, height: 100 }}
|
|
114
115
|
view={{ zoom: 1, pan: { x: 0, y: 0 } }}
|
|
@@ -127,7 +128,7 @@ describe('<Stage>', () => {
|
|
|
127
128
|
add.mockRestore();
|
|
128
129
|
});
|
|
129
130
|
|
|
130
|
-
it('tells the surface its tiles moved when the camera does', () => {
|
|
131
|
+
it('tells the surface its tiles moved when the camera does', async () => {
|
|
131
132
|
// A transform moves a tile without resizing anything, which is the one
|
|
132
133
|
// kind of move a ResizeObserver never reports.
|
|
133
134
|
const invalidateRects = vi.fn();
|
|
@@ -139,7 +140,7 @@ describe('<Stage>', () => {
|
|
|
139
140
|
</Stage>
|
|
140
141
|
</SurfaceContext.Provider>
|
|
141
142
|
);
|
|
142
|
-
const { rerender } =
|
|
143
|
+
const { rerender } = await renderSettled(at({ zoom: 1, pan: { x: 0, y: 0 } }));
|
|
143
144
|
invalidateRects.mockClear();
|
|
144
145
|
rerender(at({ zoom: 1.5, pan: { x: 0, y: 0 } }));
|
|
145
146
|
expect(invalidateRects).toHaveBeenCalled();
|
|
@@ -162,25 +163,29 @@ const plain = defineInstrument<Record<string, never>, Record<string, never>>({
|
|
|
162
163
|
});
|
|
163
164
|
|
|
164
165
|
describe('an instrument that declares a stage', () => {
|
|
165
|
-
it('renders its DOM on the stage', () => {
|
|
166
|
-
const { container } =
|
|
166
|
+
it('renders its DOM on the stage', async () => {
|
|
167
|
+
const { container } = await renderSettled(
|
|
168
|
+
<Lab instruments={[staged]} defaultInstrument="Staged" />,
|
|
169
|
+
);
|
|
167
170
|
expect(content(container)).toContainElement(screen.getByTestId('art'));
|
|
168
171
|
});
|
|
169
172
|
|
|
170
|
-
it('gets the zoom controls, and they move the stage', () => {
|
|
171
|
-
const { container } =
|
|
173
|
+
it('gets the zoom controls, and they move the stage', async () => {
|
|
174
|
+
const { container } = await renderSettled(
|
|
175
|
+
<Lab instruments={[staged]} defaultInstrument="Staged" />,
|
|
176
|
+
);
|
|
172
177
|
const trialView = within(screen.getByRole('toolbar', { name: 'View' }));
|
|
173
|
-
act(() => {
|
|
178
|
+
await act(async () => {
|
|
174
179
|
fireEvent.click(trialView.getByRole('button', { name: 'Zoom in' }));
|
|
175
180
|
});
|
|
176
181
|
expect(zoomOf(content(container))).toBeCloseTo(1.25);
|
|
177
|
-
act(() => {
|
|
182
|
+
await act(async () => {
|
|
178
183
|
fireEvent.click(trialView.getByRole('button', { name: 'Actual size' }));
|
|
179
184
|
});
|
|
180
185
|
expect(zoomOf(content(container))).toBe(1);
|
|
181
186
|
});
|
|
182
187
|
|
|
183
|
-
it('zooms about the middle of the view, not the stage origin', () => {
|
|
188
|
+
it('zooms about the middle of the view, not the stage origin', async () => {
|
|
184
189
|
const rect = vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockReturnValue({
|
|
185
190
|
x: 0,
|
|
186
191
|
y: 0,
|
|
@@ -193,9 +198,11 @@ describe('an instrument that declares a stage', () => {
|
|
|
193
198
|
toJSON: () => ({}),
|
|
194
199
|
});
|
|
195
200
|
try {
|
|
196
|
-
const { container } =
|
|
201
|
+
const { container } = await renderSettled(
|
|
202
|
+
<Lab instruments={[staged]} defaultInstrument="Staged" />,
|
|
203
|
+
);
|
|
197
204
|
const trialView = within(screen.getByRole('toolbar', { name: 'View' }));
|
|
198
|
-
act(() => {
|
|
205
|
+
await act(async () => {
|
|
199
206
|
fireEvent.click(trialView.getByRole('button', { name: 'Zoom in' }));
|
|
200
207
|
});
|
|
201
208
|
const el = content(container);
|
|
@@ -208,12 +215,12 @@ describe('an instrument that declares a stage', () => {
|
|
|
208
215
|
}
|
|
209
216
|
});
|
|
210
217
|
|
|
211
|
-
it('is what earns them: DOM content without one gets none', () => {
|
|
212
|
-
|
|
218
|
+
it('is what earns them: DOM content without one gets none', async () => {
|
|
219
|
+
await renderSettled(<Lab instruments={[plain]} defaultInstrument="Plain" />);
|
|
213
220
|
expect(screen.queryByRole('toolbar', { name: 'View' })).toBeNull();
|
|
214
221
|
});
|
|
215
222
|
|
|
216
|
-
it('draws its overlay on the stage, outside the camera', () => {
|
|
223
|
+
it('draws its overlay on the stage, outside the camera', async () => {
|
|
217
224
|
const overlaid = defineInstrument<{ n: number }, Record<string, never>>({
|
|
218
225
|
name: 'Overlaid',
|
|
219
226
|
defaultConfig: () => ({}),
|
|
@@ -224,7 +231,9 @@ describe('an instrument that declares a stage', () => {
|
|
|
224
231
|
overlay: ({ state }) => <div data-testid="legend">{state.n}</div>,
|
|
225
232
|
},
|
|
226
233
|
});
|
|
227
|
-
const { container } =
|
|
234
|
+
const { container } = await renderSettled(
|
|
235
|
+
<Lab instruments={[overlaid]} defaultInstrument="Overlaid" />,
|
|
236
|
+
);
|
|
228
237
|
const legend = screen.getByTestId('legend');
|
|
229
238
|
expect(legend).toHaveTextContent('7');
|
|
230
239
|
expect(container.querySelector('.lk-stage')).toContainElement(legend);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { fireEvent, render, screen } from '@testing-library/react';
|
|
1
|
+
import { act, fireEvent, render, screen } from '@testing-library/react';
|
|
2
|
+
import { renderSettled } from '@weasel-js/react/testing/renderSettled';
|
|
2
3
|
import { describe, expect, it, vi } from 'vitest';
|
|
3
4
|
import type { Instrument } from '../instrument/types';
|
|
4
5
|
import { Lab } from '../lab/Lab';
|
|
@@ -19,19 +20,19 @@ function silenceRenderError(): void {
|
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
describe('lab-level chrome', () => {
|
|
22
|
-
it('renders a header contribution beside the lab header', () => {
|
|
23
|
+
it('renders a header contribution beside the lab header', async () => {
|
|
23
24
|
const contribution: LabContribution = {
|
|
24
25
|
id: 'publish',
|
|
25
26
|
region: 'header',
|
|
26
27
|
item: { icon: Glyph, label: 'Publish', onActivate: () => {} },
|
|
27
28
|
};
|
|
28
|
-
|
|
29
|
+
await renderSettled(
|
|
29
30
|
<Lab title="T" instruments={[bare]} defaultInstrument="Bare" labChrome={[contribution]} />,
|
|
30
31
|
);
|
|
31
32
|
expect(screen.getByRole('button', { name: 'Publish' })).toBeInTheDocument();
|
|
32
33
|
});
|
|
33
34
|
|
|
34
|
-
it('hands a header contribution the lab, not a trial', () => {
|
|
35
|
+
it('hands a header contribution the lab, not a trial', async () => {
|
|
35
36
|
const contribution: LabContribution = {
|
|
36
37
|
id: 'another',
|
|
37
38
|
region: 'header',
|
|
@@ -41,21 +42,23 @@ describe('lab-level chrome', () => {
|
|
|
41
42
|
onActivate: (ctx) => ctx.addTrial('Bare'),
|
|
42
43
|
},
|
|
43
44
|
};
|
|
44
|
-
|
|
45
|
+
await renderSettled(
|
|
45
46
|
<Lab title="T" instruments={[bare]} defaultInstrument="Bare" labChrome={[contribution]} />,
|
|
46
47
|
);
|
|
47
48
|
expect(screen.getAllByLabelText(/^Trial /)).toHaveLength(1);
|
|
48
|
-
|
|
49
|
+
await act(async () => {
|
|
50
|
+
fireEvent.click(screen.getByRole('button', { name: 'Another' }));
|
|
51
|
+
});
|
|
49
52
|
expect(screen.getAllByLabelText(/^Trial /)).toHaveLength(2);
|
|
50
53
|
});
|
|
51
54
|
|
|
52
|
-
it('renders a palette contribution in the lab rail and selects it', () => {
|
|
55
|
+
it('renders a palette contribution in the lab rail and selects it', async () => {
|
|
53
56
|
const contribution: LabContribution = {
|
|
54
57
|
id: 'measure',
|
|
55
58
|
region: 'palette',
|
|
56
59
|
item: { icon: Glyph, label: 'Measure' },
|
|
57
60
|
};
|
|
58
|
-
|
|
61
|
+
await renderSettled(
|
|
59
62
|
<Lab title="T" instruments={[bare]} defaultInstrument="Bare" labChrome={[contribution]} />,
|
|
60
63
|
);
|
|
61
64
|
const button = screen.getByRole('button', { name: 'Measure' });
|
|
@@ -63,7 +66,7 @@ describe('lab-level chrome', () => {
|
|
|
63
66
|
expect(screen.getByRole('button', { name: 'Measure' })).toHaveAttribute('aria-current', 'true');
|
|
64
67
|
});
|
|
65
68
|
|
|
66
|
-
it('gives a palette contribution the whole lab context, not a tool slot alone', () => {
|
|
69
|
+
it('gives a palette contribution the whole lab context, not a tool slot alone', async () => {
|
|
67
70
|
// The shape the `as unknown as TrialChromeContext` cast used to fake: a
|
|
68
71
|
// contribution reading anything past the tool slot got `undefined`.
|
|
69
72
|
const seen: LabChromeContext[] = [];
|
|
@@ -75,7 +78,7 @@ describe('lab-level chrome', () => {
|
|
|
75
78
|
return <span>{`${ctx.trials.length} trial(s), mode ${ctx.mode}`}</span>;
|
|
76
79
|
},
|
|
77
80
|
};
|
|
78
|
-
|
|
81
|
+
await renderSettled(
|
|
79
82
|
<Lab title="T" instruments={[bare]} defaultInstrument="Bare" labChrome={[contribution]} />,
|
|
80
83
|
);
|
|
81
84
|
expect(screen.getByText('1 trial(s), mode auto')).toBeInTheDocument();
|
|
@@ -83,19 +86,19 @@ describe('lab-level chrome', () => {
|
|
|
83
86
|
expect(typeof seen[0]?.saveSnapshot).toBe('function');
|
|
84
87
|
});
|
|
85
88
|
|
|
86
|
-
it('renders a footer contribution', () => {
|
|
89
|
+
it('renders a footer contribution', async () => {
|
|
87
90
|
const contribution: LabContribution = {
|
|
88
91
|
id: 'count',
|
|
89
92
|
region: 'footer',
|
|
90
93
|
item: { text: 'idle' },
|
|
91
94
|
};
|
|
92
|
-
|
|
95
|
+
await renderSettled(
|
|
93
96
|
<Lab title="T" instruments={[bare]} defaultInstrument="Bare" labChrome={[contribution]} />,
|
|
94
97
|
);
|
|
95
98
|
expect(screen.getByText('idle')).toBeInTheDocument();
|
|
96
99
|
});
|
|
97
100
|
|
|
98
|
-
it('throws when two lab contributions share an id', () => {
|
|
101
|
+
it('throws when two lab contributions share an id', async () => {
|
|
99
102
|
silenceRenderError();
|
|
100
103
|
const clash: LabContribution[] = [
|
|
101
104
|
{ id: 'same', region: 'header', item: { icon: Glyph, label: 'One', onActivate: () => {} } },
|
|
@@ -106,7 +109,7 @@ describe('lab-level chrome', () => {
|
|
|
106
109
|
).toThrow(/duplicate contribution id "same"/);
|
|
107
110
|
});
|
|
108
111
|
|
|
109
|
-
it('keeps the lab tool slot and lab palette contributions in one namespace', () => {
|
|
112
|
+
it('keeps the lab tool slot and lab palette contributions in one namespace', async () => {
|
|
110
113
|
silenceRenderError();
|
|
111
114
|
const clash: LabContribution = {
|
|
112
115
|
id: 'pick',
|
|
@@ -134,8 +137,8 @@ describe('lab sidebar region', () => {
|
|
|
134
137
|
item: { title: 'Stories', body: <p>story list</p> },
|
|
135
138
|
};
|
|
136
139
|
|
|
137
|
-
it('renders a section beside the workspace', () => {
|
|
138
|
-
const { container } =
|
|
140
|
+
it('renders a section beside the workspace', async () => {
|
|
141
|
+
const { container } = await renderSettled(
|
|
139
142
|
<Lab title="T" instruments={[bare]} defaultInstrument="Bare" labChrome={[tree]} />,
|
|
140
143
|
);
|
|
141
144
|
expect(screen.getByText('story list')).toBeInTheDocument();
|
|
@@ -143,18 +146,22 @@ describe('lab sidebar region', () => {
|
|
|
143
146
|
expect(screen.getByRole('separator', { name: /lab sidebar/i })).toBeInTheDocument();
|
|
144
147
|
});
|
|
145
148
|
|
|
146
|
-
it('folds a section', () => {
|
|
147
|
-
|
|
149
|
+
it('folds a section', async () => {
|
|
150
|
+
await renderSettled(
|
|
151
|
+
<Lab title="T" instruments={[bare]} defaultInstrument="Bare" labChrome={[tree]} />,
|
|
152
|
+
);
|
|
148
153
|
fireEvent.click(screen.getByRole('button', { name: 'Stories' }));
|
|
149
154
|
expect(screen.queryByText('story list')).toBeNull();
|
|
150
155
|
});
|
|
151
156
|
|
|
152
|
-
it('offers no tear-out at lab level', () => {
|
|
153
|
-
|
|
157
|
+
it('offers no tear-out at lab level', async () => {
|
|
158
|
+
await renderSettled(
|
|
159
|
+
<Lab title="T" instruments={[bare]} defaultInstrument="Bare" labChrome={[tree]} />,
|
|
160
|
+
);
|
|
154
161
|
expect(screen.queryByRole('button', { name: 'Undock Stories' })).toBeNull();
|
|
155
162
|
});
|
|
156
163
|
|
|
157
|
-
it('hands a render contribution the lab context', () => {
|
|
164
|
+
it('hands a render contribution the lab context', async () => {
|
|
158
165
|
const seen: LabChromeContext[] = [];
|
|
159
166
|
const probe: LabContribution = {
|
|
160
167
|
id: 'probe',
|
|
@@ -164,13 +171,17 @@ describe('lab sidebar region', () => {
|
|
|
164
171
|
return <p>probe</p>;
|
|
165
172
|
},
|
|
166
173
|
};
|
|
167
|
-
|
|
174
|
+
await renderSettled(
|
|
175
|
+
<Lab title="T" instruments={[bare]} defaultInstrument="Bare" labChrome={[probe]} />,
|
|
176
|
+
);
|
|
168
177
|
expect(screen.getByText('probe')).toBeInTheDocument();
|
|
169
178
|
expect(seen[0]?.trials).toHaveLength(1);
|
|
170
179
|
});
|
|
171
180
|
|
|
172
|
-
it('adds no strip to a lab without sidebar chrome', () => {
|
|
173
|
-
const { container } =
|
|
181
|
+
it('adds no strip to a lab without sidebar chrome', async () => {
|
|
182
|
+
const { container } = await renderSettled(
|
|
183
|
+
<Lab title="T" instruments={[bare]} defaultInstrument="Bare" />,
|
|
184
|
+
);
|
|
174
185
|
expect(container.querySelector('.lk-lab__panes')).toBeNull();
|
|
175
186
|
});
|
|
176
187
|
});
|
|
@@ -182,7 +193,7 @@ describe('lab chrome without <Lab>', () => {
|
|
|
182
193
|
undo: () => void;
|
|
183
194
|
}
|
|
184
195
|
|
|
185
|
-
it('renders a contribution typed against a consumer context and hands it back', () => {
|
|
196
|
+
it('renders a contribution typed against a consumer context and hands it back', async () => {
|
|
186
197
|
const undo = vi.fn();
|
|
187
198
|
const ctx: RailCtx = { undo };
|
|
188
199
|
const rail: readonly LabContribution<RailCtx>[] = [
|
|
@@ -192,7 +203,7 @@ describe('lab chrome without <Lab>', () => {
|
|
|
192
203
|
item: { icon: Glyph, label: 'Undo', showLabel: true, onActivate: (c) => c.undo() },
|
|
193
204
|
},
|
|
194
205
|
];
|
|
195
|
-
|
|
206
|
+
await renderSettled(
|
|
196
207
|
<LabShell
|
|
197
208
|
title="Palette lab"
|
|
198
209
|
header={
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { fireEvent, render, screen } from '@testing-library/react';
|
|
1
|
+
import { act, fireEvent, render, screen } from '@testing-library/react';
|
|
2
2
|
import { describe, expect, it, vi } from 'vitest';
|
|
3
3
|
import type { RegionContribution, ToolSlotContext } from '../types';
|
|
4
4
|
import { PaletteRegion } from './PaletteRegion';
|
|
@@ -65,7 +65,7 @@ describe('PaletteRegion', () => {
|
|
|
65
65
|
const brush = screen.getByRole('button', { name: 'brush' });
|
|
66
66
|
const eraser = screen.getByRole('button', { name: 'eraser' });
|
|
67
67
|
|
|
68
|
-
brush.focus();
|
|
68
|
+
act(() => brush.focus());
|
|
69
69
|
fireEvent.keyDown(brush, { key: 'ArrowDown' });
|
|
70
70
|
expect(document.activeElement).toBe(eraser);
|
|
71
71
|
expect(eraser).toHaveAttribute('tabindex', '0');
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
// A settings-by-scopes grid. Built to sit beside a ControlPanel in a ~360px
|
|
2
|
+
// sidebar: the label column takes what seven value columns leave, and every cell
|
|
3
|
+
// is a fixed-height button so the rows rule up evenly.
|
|
4
|
+
.lk-control-matrix {
|
|
5
|
+
min-width: 0;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.lk-control-matrix__table {
|
|
9
|
+
width: 100%;
|
|
10
|
+
table-layout: fixed;
|
|
11
|
+
border-collapse: collapse;
|
|
12
|
+
font-family: var(--wzl-font-ui);
|
|
13
|
+
font-size: var(--wzl-font-size-xs);
|
|
14
|
+
color: var(--wzl-fg);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.lk-control-matrix__corner {
|
|
18
|
+
width: var(--lk-control-matrix-label-width, 30%);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.lk-control-matrix__head,
|
|
22
|
+
.lk-control-matrix__label {
|
|
23
|
+
font-family: var(--wzl-font-display);
|
|
24
|
+
font-weight: var(--wzl-font-weight-light);
|
|
25
|
+
font-size: var(--wzl-font-size-sm);
|
|
26
|
+
line-height: var(--wzl-leading-snug);
|
|
27
|
+
color: var(--wzl-fg-muted);
|
|
28
|
+
text-transform: var(--wzl-params-label-case, uppercase);
|
|
29
|
+
letter-spacing: var(--wzl-params-label-tracking, var(--wzl-tracking-wide));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.lk-control-matrix__head {
|
|
33
|
+
padding: 0 0 var(--wzl-space-2);
|
|
34
|
+
text-align: center;
|
|
35
|
+
font-size: var(--wzl-font-size-2xs);
|
|
36
|
+
white-space: nowrap;
|
|
37
|
+
overflow: hidden;
|
|
38
|
+
text-overflow: ellipsis;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// The fallback scope every other column inherits from.
|
|
42
|
+
.lk-control-matrix__head.is-first {
|
|
43
|
+
color: var(--wzl-fg);
|
|
44
|
+
font-weight: var(--wzl-font-weight-medium);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.lk-control-matrix__head-text {
|
|
48
|
+
text-decoration: none;
|
|
49
|
+
cursor: default;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.lk-control-matrix__head-button {
|
|
53
|
+
width: 100%;
|
|
54
|
+
padding: 0;
|
|
55
|
+
border: 0;
|
|
56
|
+
border-radius: var(--wzl-radius-sm);
|
|
57
|
+
background: none;
|
|
58
|
+
color: inherit;
|
|
59
|
+
font: inherit;
|
|
60
|
+
letter-spacing: var(--wzl-params-label-tracking, var(--wzl-tracking-wide));
|
|
61
|
+
text-transform: var(--wzl-params-label-case, uppercase);
|
|
62
|
+
cursor: pointer;
|
|
63
|
+
overflow: hidden;
|
|
64
|
+
text-overflow: ellipsis;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.lk-control-matrix__head-button:hover {
|
|
68
|
+
color: var(--wzl-fg);
|
|
69
|
+
background: var(--wzl-surface-hover);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.lk-control-matrix__label {
|
|
73
|
+
padding: 0 var(--wzl-space-2) 0 0;
|
|
74
|
+
text-align: start;
|
|
75
|
+
white-space: nowrap;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// The text truncates; the help glyph after it never does.
|
|
79
|
+
.lk-control-matrix__label-text {
|
|
80
|
+
display: inline-block;
|
|
81
|
+
max-width: calc(100% - 1.4em);
|
|
82
|
+
overflow: hidden;
|
|
83
|
+
text-overflow: ellipsis;
|
|
84
|
+
vertical-align: middle;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.lk-control-matrix__cell {
|
|
88
|
+
padding: 1px;
|
|
89
|
+
height: var(--wzl-control-h-sm);
|
|
90
|
+
text-align: center;
|
|
91
|
+
vertical-align: middle;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.lk-control-matrix__cell.is-missing {
|
|
95
|
+
color: var(--wzl-fg-subtle);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Inherited: the value in effect, but not chosen here.
|
|
99
|
+
.lk-control-matrix__cell.is-inherited > * {
|
|
100
|
+
opacity: 0.4;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Every cell control fills its cell, so the cell is the click target.
|
|
104
|
+
.lk-control-matrix__value,
|
|
105
|
+
.lk-control-matrix__switch {
|
|
106
|
+
display: flex;
|
|
107
|
+
align-items: center;
|
|
108
|
+
justify-content: center;
|
|
109
|
+
width: 100%;
|
|
110
|
+
height: var(--wzl-control-h-sm);
|
|
111
|
+
padding: 0 var(--wzl-space-1);
|
|
112
|
+
border: 1px solid transparent;
|
|
113
|
+
border-radius: var(--wzl-radius-sm);
|
|
114
|
+
background: none;
|
|
115
|
+
color: inherit;
|
|
116
|
+
font: inherit;
|
|
117
|
+
cursor: pointer;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.lk-control-matrix__value {
|
|
121
|
+
display: block;
|
|
122
|
+
overflow: hidden;
|
|
123
|
+
text-overflow: ellipsis;
|
|
124
|
+
white-space: nowrap;
|
|
125
|
+
line-height: calc(var(--wzl-control-h-sm) - 2px);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.lk-control-matrix__value.is-number {
|
|
129
|
+
.numeric();
|
|
130
|
+
text-align: end;
|
|
131
|
+
// A column of numbers is read down, so it keeps every digit: a glow of
|
|
132
|
+
// 0.35 cut to `0…` says nothing.
|
|
133
|
+
text-overflow: clip;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.lk-control-matrix__value:hover,
|
|
137
|
+
.lk-control-matrix__switch:hover {
|
|
138
|
+
border-color: var(--wzl-border);
|
|
139
|
+
background: var(--wzl-surface-hover);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.lk-control-matrix__value:focus-visible,
|
|
143
|
+
.lk-control-matrix__switch:focus-visible,
|
|
144
|
+
.lk-control-matrix__swatch:focus-visible,
|
|
145
|
+
.lk-control-matrix__head-button:focus-visible {
|
|
146
|
+
outline: 2px solid var(--wzl-focus-ring);
|
|
147
|
+
outline-offset: -1px;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// A small switch, drawn with the same parts as weasel-ui's.
|
|
151
|
+
.lk-control-matrix__track {
|
|
152
|
+
position: relative;
|
|
153
|
+
flex: 0 0 auto;
|
|
154
|
+
width: 20px;
|
|
155
|
+
height: 12px;
|
|
156
|
+
background: var(--wzl-surface-sunken);
|
|
157
|
+
border: 1px solid var(--wzl-border-strong);
|
|
158
|
+
border-radius: var(--wzl-radius-pill);
|
|
159
|
+
transition: background-color var(--wzl-motion-fast), border-color var(--wzl-motion-fast);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.lk-control-matrix__thumb {
|
|
163
|
+
position: absolute;
|
|
164
|
+
top: 1px;
|
|
165
|
+
left: 1px;
|
|
166
|
+
width: 8px;
|
|
167
|
+
height: 8px;
|
|
168
|
+
background: var(--wzl-fg);
|
|
169
|
+
border-radius: 50%;
|
|
170
|
+
transition: transform var(--wzl-motion-fast);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.lk-control-matrix__switch[aria-checked='true'] .lk-control-matrix__track {
|
|
174
|
+
background: var(--wzl-accent-strong);
|
|
175
|
+
border-color: var(--wzl-accent-strong);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.lk-control-matrix__switch[aria-checked='true'] .lk-control-matrix__thumb {
|
|
179
|
+
transform: translateX(8px);
|
|
180
|
+
background: var(--wzl-fg-on-accent);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.lk-control-matrix__swatch {
|
|
184
|
+
display: block;
|
|
185
|
+
width: 100%;
|
|
186
|
+
height: var(--wzl-control-h-xs);
|
|
187
|
+
padding: 0;
|
|
188
|
+
border: 1px solid var(--wzl-border);
|
|
189
|
+
border-radius: var(--wzl-radius-sm);
|
|
190
|
+
background: none;
|
|
191
|
+
cursor: pointer;
|
|
192
|
+
-webkit-appearance: none;
|
|
193
|
+
appearance: none;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.lk-control-matrix__swatch::-webkit-color-swatch-wrapper {
|
|
197
|
+
padding: 0;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.lk-control-matrix__swatch::-webkit-color-swatch {
|
|
201
|
+
border: 0;
|
|
202
|
+
border-radius: var(--wzl-radius-sm);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.lk-control-matrix__swatch::-moz-color-swatch {
|
|
206
|
+
border: 0;
|
|
207
|
+
border-radius: var(--wzl-radius-sm);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.lk-control-matrix__chip {
|
|
211
|
+
display: inline-block;
|
|
212
|
+
width: 22px;
|
|
213
|
+
height: var(--wzl-control-h-xs);
|
|
214
|
+
border: 1px solid var(--wzl-border);
|
|
215
|
+
border-radius: var(--wzl-radius-sm);
|
|
216
|
+
background: var(--lk-chip);
|
|
217
|
+
vertical-align: middle;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.lk-control-matrix__chip.is-unset {
|
|
221
|
+
border-style: dashed;
|
|
222
|
+
background: none;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.lk-control-matrix__popover {
|
|
226
|
+
outline: none;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.lk-control-matrix__editor {
|
|
230
|
+
display: flex;
|
|
231
|
+
flex-direction: column;
|
|
232
|
+
gap: var(--wzl-space-3);
|
|
233
|
+
inline-size: 240px;
|
|
234
|
+
padding: var(--wzl-space-4);
|
|
235
|
+
background: var(--wzl-surface-raised);
|
|
236
|
+
border: 1px solid var(--wzl-border);
|
|
237
|
+
border-radius: var(--wzl-radius-md);
|
|
238
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
|
239
|
+
outline: none;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.lk-control-matrix__editor-actions {
|
|
243
|
+
display: flex;
|
|
244
|
+
justify-content: flex-end;
|
|
245
|
+
}
|