@weasel-js/labkit 1.5.0 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -4
- package/dist/_dts/{CanvasStackContext-CqkxkFJW.d.ts → CanvasStackContext-CJNMS6mz.d.ts} +1 -1
- package/dist/_dts/{PrefsForm.d-DDHFANkc.d.ts → PrefsForm.d-DPy9I8It.d.ts} +4 -0
- package/dist/_dts/Select.d-Gwr8fLpK.d.ts +453 -0
- package/dist/_dts/{frac-8R6UmlvN.d.ts → frac-B4okC2lX.d.ts} +42 -4
- package/dist/_dts/{index-retnAut7.d.ts → index-BNdz-tNd.d.ts} +90 -10
- package/dist/_dts/{index-B8uu2ba7.d.ts → index-CSJoj4LC.d.ts} +17 -6
- package/dist/_dts/{types-Ca2LCnPq.d.ts → types-DzoNY1pO.d.ts} +28 -4
- package/dist/_dts/{usePanZoom-nS798rOF.d.ts → usePanZoom-Pzf9f2jh.d.ts} +2 -2
- package/dist/_dts/{useTrialState-jcx_TdU6.d.ts → useTrialState-uLlmaccn.d.ts} +1 -1
- package/dist/canvas/index.d.ts +5 -5
- package/dist/canvas/index.js +1 -1
- package/dist/chrome/index.d.ts +4 -4
- package/dist/chrome/index.js +7 -7
- package/dist/chunk-7PFH3CNM.js +150 -0
- package/dist/chunk-7PFH3CNM.js.map +1 -0
- package/dist/{chunk-PGETSEDK.js → chunk-CEIUBPPW.js} +10 -6
- package/dist/chunk-CEIUBPPW.js.map +1 -0
- package/dist/chunk-EH7UEQV7.js +590 -0
- package/dist/chunk-EH7UEQV7.js.map +1 -0
- package/dist/{chunk-EKIICY6X.js → chunk-EHGVROVV.js} +37 -25
- package/dist/chunk-EHGVROVV.js.map +1 -0
- package/dist/{chunk-ZPH5WTWF.js → chunk-ERFMC6WU.js} +142 -66
- package/dist/chunk-ERFMC6WU.js.map +1 -0
- package/dist/{chunk-NS64DXMN.js → chunk-IHUVQ3TX.js} +39 -16
- package/dist/chunk-IHUVQ3TX.js.map +1 -0
- package/dist/{chunk-QLSV2N3G.js → chunk-IJJUK4FU.js} +25 -13
- package/dist/chunk-IJJUK4FU.js.map +1 -0
- package/dist/{chunk-67SJLMC7.js → chunk-IXTQI7JX.js} +45 -12
- package/dist/chunk-IXTQI7JX.js.map +1 -0
- package/dist/{chunk-MUKOW3TC.js → chunk-MDVTK5U3.js} +4 -4
- package/dist/{chunk-MUKOW3TC.js.map → chunk-MDVTK5U3.js.map} +1 -1
- package/dist/{chunk-UU3NJO6N.js → chunk-PHK4W562.js} +3 -3
- package/dist/{chunk-UU3NJO6N.js.map → chunk-PHK4W562.js.map} +1 -1
- package/dist/chunk-PJ7WEQBJ.js +18 -0
- package/dist/chunk-PJ7WEQBJ.js.map +1 -0
- package/dist/chunk-T3EC2DXD.js +12628 -0
- package/dist/chunk-T3EC2DXD.js.map +1 -0
- package/dist/{chunk-KJALCDWE.js → chunk-UN4U4P63.js} +4 -4
- package/dist/{chunk-KJALCDWE.js.map → chunk-UN4U4P63.js.map} +1 -1
- package/dist/{chunk-NDRYLVOW.js → chunk-Z76KEZAX.js} +2 -2
- package/dist/chunk-Z76KEZAX.js.map +1 -0
- package/dist/{chunk-6ZDGOZQV.js → chunk-ZDHY47EV.js} +70 -17
- package/dist/chunk-ZDHY47EV.js.map +1 -0
- package/dist/config/index.d.ts +60 -16
- package/dist/config/index.js +6 -4
- package/dist/controls/index.d.ts +3 -3
- package/dist/controls/index.js +4 -4
- package/dist/dragdrop/index.d.ts +3 -3
- package/dist/index.d.ts +177 -53
- package/dist/index.js +1333 -1683
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +4 -4
- package/dist/layers/index.js +2 -2
- package/dist/loupe/index.d.ts +67 -16
- package/dist/loupe/index.js +1 -1
- package/dist/passthrough/weasel-ui.d.ts +26 -202
- package/dist/passthrough/weasel-ui.js +1 -1
- package/dist/primitives/index.d.ts +3 -1
- package/dist/primitives/index.js +3 -3
- package/dist/state/index.d.ts +5 -5
- package/dist/state/index.js +5 -3
- package/dist/state/index.js.map +1 -1
- package/dist/styles.css +153 -40
- package/dist/surface/index.js +1 -1
- package/dist/ui/layers/index.js +1 -1
- package/dist/undo/index.d.ts +3 -3
- package/package.json +9 -9
- package/src/annotations/AnnotationOverlay.tsx +3 -0
- package/src/annotations/Annotations.less +4 -0
- package/src/annotations/Annotations.overlay.test.tsx +20 -0
- package/src/annotations/svgNodes.ts +1 -0
- package/src/annotations/toolMap.test.ts +9 -5
- package/src/annotations/toolMap.ts +3 -0
- package/src/canvas/CanvasStack.stories.tsx +1 -1
- package/src/canvas/usePanZoom.test.ts +28 -0
- package/src/canvas/usePanZoom.ts +11 -4
- package/src/chrome/ChromeRegions.stories.tsx +1 -1
- package/src/chrome/LabChrome.tsx +21 -5
- package/src/chrome/builtins.test.ts +1 -0
- package/src/chrome/builtins.tsx +1 -0
- package/src/chrome/index.ts +1 -0
- package/src/chrome/labTypes.ts +2 -2
- package/src/chrome/regions/PaletteRegion.tsx +1 -6
- package/src/chrome/regions/SidebarRegion.stories.tsx +44 -0
- package/src/chrome/regions/ToolbarRegion.tsx +11 -7
- package/src/chrome/types.ts +3 -0
- package/src/config/auto.test.ts +12 -0
- package/src/config/auto.ts +15 -0
- package/src/config/autoConfig.test.ts +107 -0
- package/src/config/autoConfig.ts +153 -0
- package/src/config/builder.test.ts +38 -0
- package/src/config/builder.ts +53 -8
- package/src/config/index.ts +3 -0
- package/src/config/path.ts +7 -2
- package/src/config/resolve.test.ts +25 -0
- package/src/config/resolve.ts +17 -1
- package/src/config/types.ts +32 -2
- package/src/config/useResolvedConfig.ts +26 -0
- package/src/controls/AutoControls.stories.tsx +88 -0
- package/src/controls/ControlPanel.stories.tsx +42 -1
- package/src/controls/ControlPanel.test.tsx +514 -4
- package/src/controls/ControlPanel.tsx +557 -80
- package/src/index.ts +29 -1
- package/src/instrument/serializers.ts +29 -1
- package/src/lab/Lab.chrome.test.tsx +16 -0
- package/src/lab/Lab.stories.tsx +1 -1
- package/src/lab/Lab.surface.test.tsx +7 -2
- package/src/lab/Lab.test.tsx +79 -1
- package/src/lab/Lab.tsx +105 -13
- package/src/lab/LabContext.ts +8 -0
- package/src/lab/LabFit.stories.tsx +4 -4
- package/src/lab/LabFullChrome.stories.tsx +1 -1
- package/src/lab/LabHeader.tsx +0 -1
- package/src/lab/LabRoot.test.tsx +56 -0
- package/src/lab/LabRoot.tsx +47 -0
- package/src/lab/LabShell.less +24 -3
- package/src/lab/LabShell.stories.tsx +50 -1
- package/src/lab/LabShell.tsx +18 -27
- package/src/lab/LabSwitcher.less +10 -10
- package/src/lab/LabSwitcher.test.tsx +14 -9
- package/src/lab/LabSwitcher.tsx +8 -4
- package/src/lab/Workspace.less +1 -1
- package/src/lab/Workspace.stories.tsx +1 -1
- package/src/lab/Workspace.surface.test.tsx +1 -1
- package/src/lab/index.ts +2 -0
- package/src/layers/LayerList.stories.tsx +1 -1
- package/src/loupe/AGENTS.md +16 -4
- package/src/loupe/LoupeGestures.tsx +67 -0
- package/src/loupe/TrialLoupe.tsx +43 -28
- package/src/loupe/index.ts +4 -0
- package/src/loupe/loupeActions.ts +98 -0
- package/src/loupe/useLoupe.test.tsx +59 -2
- package/src/loupe/useLoupe.ts +28 -48
- package/src/passthrough/weasel-ui.ts +3 -3
- package/src/primitives/FloatingPanel.stories.tsx +1 -1
- package/src/primitives/FpsMeter.stories.tsx +1 -1
- package/src/primitives/JobProgress.stories.tsx +1 -1
- package/src/primitives/Legend.stories.tsx +1 -1
- package/src/primitives/ScaleIndicator.stories.tsx +1 -1
- package/src/primitives/Sidebar.less +7 -0
- package/src/primitives/Sidebar.stories.tsx +1 -1
- package/src/primitives/Split.test.tsx +25 -0
- package/src/primitives/Split.tsx +36 -20
- package/src/primitives/StatusBar.stories.tsx +1 -1
- package/src/primitives/Toolbar.stories.tsx +1 -1
- package/src/specimen/Specimen.less +95 -0
- package/src/specimen/Specimen.stories.tsx +13 -0
- package/src/specimen/Specimen.test.tsx +13 -0
- package/src/specimen/Specimen.tsx +888 -0
- package/src/state/Persistence.stories.tsx +4 -4
- package/src/state/store.test.ts +65 -0
- package/src/state/store.ts +32 -8
- package/src/state/types.ts +26 -2
- package/src/state/useTrialState.test.tsx +85 -0
- package/src/state/useTrialState.ts +9 -1
- package/src/styles.less +1 -0
- package/src/surface/index.ts +1 -1
- package/src/surface/useSurfaceTile.test.tsx +1 -1
- package/src/surface/useTiledSurface.test.tsx +65 -10
- package/src/surface/useTiledSurface.ts +23 -11
- package/src/theme/Interstellar.stories.tsx +2 -2
- package/src/theme/interstellar.test.ts +16 -5
- package/src/theme/interstellar.theme.json +180 -0
- package/src/theme/interstellar.ts +8 -5
- package/src/tools/labTool.ts +3 -3
- package/src/trial/Trial.annotations.test.tsx +13 -4
- package/src/trial/Trial.canvas.test.tsx +32 -0
- package/src/trial/Trial.config.test.tsx +75 -2
- package/src/trial/Trial.less +4 -0
- package/src/trial/Trial.stories.tsx +4 -1
- package/src/trial/Trial.test.tsx +3 -0
- package/src/trial/Trial.tsx +21 -14
- package/src/trial/TrialChrome.tsx +17 -3
- package/src/trial/trialOps.test.ts +101 -1
- package/src/trial/trialOps.ts +62 -4
- package/dist/_dts/ToggleBar.d-DbHoWKIA.d.ts +0 -116
- package/dist/chunk-67SJLMC7.js.map +0 -1
- package/dist/chunk-6ZDGOZQV.js.map +0 -1
- package/dist/chunk-E44SU6XS.js +0 -50
- package/dist/chunk-E44SU6XS.js.map +0 -1
- package/dist/chunk-EKIICY6X.js.map +0 -1
- package/dist/chunk-NDRYLVOW.js.map +0 -1
- package/dist/chunk-NS64DXMN.js.map +0 -1
- package/dist/chunk-PGETSEDK.js.map +0 -1
- package/dist/chunk-QLSV2N3G.js.map +0 -1
- package/dist/chunk-S2SHZRD7.js +0 -7681
- package/dist/chunk-S2SHZRD7.js.map +0 -1
- package/dist/chunk-TQBBYTAF.js +0 -280
- package/dist/chunk-TQBBYTAF.js.map +0 -1
- package/dist/chunk-ZPH5WTWF.js.map +0 -1
- package/src/theme/interstellar.tokens.json +0 -61
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from '@
|
|
1
|
+
import type { Meta, StoryObj } from '@weasel-js/forge';
|
|
2
|
+
import { expect, userEvent, waitFor, within } from '@weasel-js/forge/play';
|
|
2
3
|
import { useState } from 'react';
|
|
3
|
-
import { expect, userEvent, waitFor, within } from 'storybook/test';
|
|
4
4
|
import { createIndexedDbAdapter } from './adapters';
|
|
5
5
|
import { Persistence } from './Persistence';
|
|
6
6
|
import { usePersistedState } from './usePersistedState';
|
|
@@ -64,11 +64,11 @@ export const SurvivesARemountInIndexedDb: StoryObj<typeof Remountable> = {
|
|
|
64
64
|
args: { database: `labkit-story-${Date.now()}` },
|
|
65
65
|
play: async ({ canvasElement }) => {
|
|
66
66
|
const canvas = within(canvasElement);
|
|
67
|
-
await waitFor(() => expect(canvas.getByLabelText('tab')).
|
|
67
|
+
await waitFor(() => expect(canvas.getByLabelText('tab').textContent).toBe('shape'));
|
|
68
68
|
await userEvent.click(canvas.getByText('choose color'));
|
|
69
69
|
await userEvent.click(canvas.getByText('remount'));
|
|
70
70
|
await waitFor(() => expect(canvas.queryByLabelText('tab')).toBeNull());
|
|
71
|
-
await waitFor(() => expect(canvas.getByLabelText('tab')).
|
|
71
|
+
await waitFor(() => expect(canvas.getByLabelText('tab').textContent).toBe('color'), {
|
|
72
72
|
timeout: 3000,
|
|
73
73
|
});
|
|
74
74
|
},
|
package/src/state/store.test.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { auto } from '../config/auto';
|
|
2
3
|
import type { Instrument } from '../instrument/types';
|
|
3
4
|
import { createLabStore } from './store';
|
|
4
5
|
import type { CreateLabStoreOptions } from './types';
|
|
@@ -163,6 +164,70 @@ describe('save/load/delete snapshots', () => {
|
|
|
163
164
|
expect((s.getState().trials[0]?.state as { n: number }).n).toBe(5);
|
|
164
165
|
});
|
|
165
166
|
|
|
167
|
+
it('loadSnapshot restores the auto paths the trial had when saved', () => {
|
|
168
|
+
const s = makeStore();
|
|
169
|
+
s.getState().addTrial({
|
|
170
|
+
id: 'w1',
|
|
171
|
+
instrumentName: 'T',
|
|
172
|
+
config: { x: 1, y: 2 },
|
|
173
|
+
state: {},
|
|
174
|
+
view: { zoom: 1, pan: { x: 0, y: 0 } },
|
|
175
|
+
auto: ['x'],
|
|
176
|
+
});
|
|
177
|
+
s.getState().saveSnapshot('w1', 'snap1');
|
|
178
|
+
expect(s.getState().savedSnapshots[0]?.auto).toEqual(['x']);
|
|
179
|
+
const snapId = s.getState().savedSnapshots[0]?.id ?? '';
|
|
180
|
+
|
|
181
|
+
const w = s.getState().trials[0];
|
|
182
|
+
s.getState().addTrial({ ...(w as NonNullable<typeof w>), id: 'w2', auto: ['y'] });
|
|
183
|
+
s.getState().loadSnapshot(snapId, 'w2');
|
|
184
|
+
expect(s.getState().trials[1]?.auto).toEqual(['x']);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('a snapshot saved with nothing auto pins every field on load', () => {
|
|
188
|
+
const s = makeStore();
|
|
189
|
+
s.getState().addTrial({
|
|
190
|
+
id: 'w1',
|
|
191
|
+
instrumentName: 'T',
|
|
192
|
+
config: { x: 1 },
|
|
193
|
+
state: {},
|
|
194
|
+
view: { zoom: 1, pan: { x: 0, y: 0 } },
|
|
195
|
+
});
|
|
196
|
+
s.getState().saveSnapshot('w1', 'snap1');
|
|
197
|
+
expect(s.getState().savedSnapshots[0]?.auto).toEqual([]);
|
|
198
|
+
const snapId = s.getState().savedSnapshots[0]?.id ?? '';
|
|
199
|
+
s.getState().updateTrialConfig('w1', 'x', auto);
|
|
200
|
+
expect(s.getState().trials[0]?.auto).toEqual(['x']);
|
|
201
|
+
s.getState().loadSnapshot(snapId, 'w1');
|
|
202
|
+
expect(s.getState().trials[0]?.auto).toBeUndefined();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("a snapshot saved before snapshots carried auto paths leaves the trial's own", () => {
|
|
206
|
+
const snap = {
|
|
207
|
+
id: 'old',
|
|
208
|
+
name: 'old',
|
|
209
|
+
trialId: 'w1',
|
|
210
|
+
instrumentName: 'T',
|
|
211
|
+
config: { x: 7 },
|
|
212
|
+
state: {},
|
|
213
|
+
savedAt: 1,
|
|
214
|
+
};
|
|
215
|
+
const s = makeStore({
|
|
216
|
+
initial: { version: 1, trials: [], saves: [snap], mode: 'auto' } as never,
|
|
217
|
+
});
|
|
218
|
+
s.getState().addTrial({
|
|
219
|
+
id: 'w1',
|
|
220
|
+
instrumentName: 'T',
|
|
221
|
+
config: { x: 1 },
|
|
222
|
+
state: {},
|
|
223
|
+
view: { zoom: 1, pan: { x: 0, y: 0 } },
|
|
224
|
+
auto: ['x'],
|
|
225
|
+
});
|
|
226
|
+
s.getState().loadSnapshot('old', 'w1');
|
|
227
|
+
expect(s.getState().trials[0]?.config).toEqual({ x: 7 });
|
|
228
|
+
expect(s.getState().trials[0]?.auto).toEqual(['x']);
|
|
229
|
+
});
|
|
230
|
+
|
|
166
231
|
it('loadSnapshot blocks cross-instrument load', () => {
|
|
167
232
|
const s = makeStore();
|
|
168
233
|
s.getState().addTrial({
|
package/src/state/store.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { createStore, type StoreApi } from 'zustand/vanilla';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { applyConfigWrite } from '../config/autoConfig';
|
|
3
|
+
import { fillConfigDefaults } from '../config/path';
|
|
4
|
+
import {
|
|
5
|
+
configDefaultsOf,
|
|
6
|
+
configMigrationsOf,
|
|
7
|
+
configSchemasOf,
|
|
8
|
+
serializersOf,
|
|
9
|
+
} from '../instrument/serializers';
|
|
4
10
|
import type { InstrumentList } from '../instrument/types';
|
|
5
11
|
import { emptyDocument } from './document';
|
|
6
12
|
import { deserializeTrials, emptyUndoStack, newId } from './helpers';
|
|
@@ -75,6 +81,7 @@ export function createLabStore(options: CreateLabStoreOptions = {}): LabStore {
|
|
|
75
81
|
serializers: options.serializers ?? {},
|
|
76
82
|
configDefaults: options.configDefaults ?? {},
|
|
77
83
|
configMigrations: options.configMigrations ?? {},
|
|
84
|
+
configSchemas: options.configSchemas ?? {},
|
|
78
85
|
};
|
|
79
86
|
const initial = options.initial ?? emptyDocument(options.initialMode ?? 'auto');
|
|
80
87
|
|
|
@@ -117,9 +124,18 @@ export function createLabStore(options: CreateLabStoreOptions = {}): LabStore {
|
|
|
117
124
|
|
|
118
125
|
updateTrialConfig: (id, path, value) => {
|
|
119
126
|
set((s) => ({
|
|
120
|
-
trials: s.trials.map((w) =>
|
|
121
|
-
w.id
|
|
122
|
-
|
|
127
|
+
trials: s.trials.map((w) => {
|
|
128
|
+
if (w.id !== id) return w;
|
|
129
|
+
const was = w.auto ?? [];
|
|
130
|
+
// The sentinel is never stored: it becomes membership in `auto`, and
|
|
131
|
+
// the pinned value at the path is left exactly where it is.
|
|
132
|
+
const next = applyConfigWrite(w.config, was, path, value);
|
|
133
|
+
if (next.config === w.config && next.autoPaths === was) return w;
|
|
134
|
+
const { auto: _was, ...kept } = w;
|
|
135
|
+
const record: TrialRecord = { ...kept, config: next.config };
|
|
136
|
+
if (next.autoPaths.length > 0) record.auto = next.autoPaths;
|
|
137
|
+
return record;
|
|
138
|
+
}),
|
|
123
139
|
}));
|
|
124
140
|
},
|
|
125
141
|
|
|
@@ -202,6 +218,7 @@ export function createLabStore(options: CreateLabStoreOptions = {}): LabStore {
|
|
|
202
218
|
instrumentName: trial.instrumentName,
|
|
203
219
|
config: clonedConfig,
|
|
204
220
|
state: serializedState,
|
|
221
|
+
auto: [...(trial.auto ?? [])],
|
|
205
222
|
savedAt,
|
|
206
223
|
};
|
|
207
224
|
set((s) => ({ savedSnapshots: [...s.savedSnapshots, snapshot] }));
|
|
@@ -223,9 +240,14 @@ export function createLabStore(options: CreateLabStoreOptions = {}): LabStore {
|
|
|
223
240
|
? reg.deserialize(snapshot.state, snapshot.config)
|
|
224
241
|
: snapshot.state;
|
|
225
242
|
set((s) => ({
|
|
226
|
-
trials: s.trials.map((w) =>
|
|
227
|
-
w.id
|
|
228
|
-
|
|
243
|
+
trials: s.trials.map((w) => {
|
|
244
|
+
if (w.id !== trialId) return w;
|
|
245
|
+
const loaded: TrialRecord = { ...w, state: restoredState, config: snapshot.config };
|
|
246
|
+
if (snapshot.auto === undefined) return loaded;
|
|
247
|
+
delete loaded.auto;
|
|
248
|
+
if (snapshot.auto.length > 0) loaded.auto = [...snapshot.auto];
|
|
249
|
+
return loaded;
|
|
250
|
+
}),
|
|
229
251
|
}));
|
|
230
252
|
},
|
|
231
253
|
|
|
@@ -302,6 +324,7 @@ function hooksOf(instruments: InstrumentList): InstrumentHooks {
|
|
|
302
324
|
return {
|
|
303
325
|
serializers: serializersOf(instruments),
|
|
304
326
|
configDefaults: configDefaultsOf(instruments),
|
|
327
|
+
configSchemas: configSchemasOf(instruments),
|
|
305
328
|
configMigrations: configMigrationsOf(instruments),
|
|
306
329
|
};
|
|
307
330
|
}
|
|
@@ -317,6 +340,7 @@ function mergedHooks(old: InstrumentHooks, instruments: InstrumentList): Instrum
|
|
|
317
340
|
return {
|
|
318
341
|
serializers: { ...unlisted(old.serializers), ...fresh.serializers },
|
|
319
342
|
configDefaults: { ...unlisted(old.configDefaults), ...fresh.configDefaults },
|
|
343
|
+
configSchemas: { ...unlisted(old.configSchemas), ...fresh.configSchemas },
|
|
320
344
|
configMigrations: { ...unlisted(old.configMigrations), ...fresh.configMigrations },
|
|
321
345
|
};
|
|
322
346
|
}
|
package/src/state/types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Auto } from '../config/auto';
|
|
2
|
+
import type { ConfigPath, ResolvedConfig, ValueAtPath } from '../config/types';
|
|
2
3
|
import type { InstrumentList } from '../instrument/types';
|
|
3
4
|
import type { UndockedPanels } from './undock';
|
|
4
5
|
/** A trial's undo history, as snapshots of its state either side of the
|
|
@@ -31,6 +32,15 @@ export interface TrialRecord<TS = unknown, TC = unknown, TV = unknown> {
|
|
|
31
32
|
* defaults. Kept so Reset restores the trial's own subject rather than the
|
|
32
33
|
* bare defaults. */
|
|
33
34
|
configSeed?: Partial<TC>;
|
|
35
|
+
/** Dotted paths the trial's seed config opened as auto. Kept beside
|
|
36
|
+
* `configSeed`, which cannot carry them: the sentinel is stripped on the way
|
|
37
|
+
* in, so Reset would otherwise have no way back to the state the trial
|
|
38
|
+
* opened in. Absent means none. */
|
|
39
|
+
autoSeed?: readonly string[];
|
|
40
|
+
/** Dotted paths this trial has unpinned. The value at such a path stays in
|
|
41
|
+
* `config` — it is what the field pins back to — but the instrument reads
|
|
42
|
+
* the resolver's value, or `undefined`, instead. Absent means none. */
|
|
43
|
+
auto?: readonly string[];
|
|
34
44
|
/** This trial's own tool slot. Undefined means it reads the lab's. */
|
|
35
45
|
activeToolId?: string | null;
|
|
36
46
|
/** The extent the trial's sidebar was last dragged to, in pixels. Undefined
|
|
@@ -60,6 +70,10 @@ export interface SavedSnapshot {
|
|
|
60
70
|
instrumentName: string;
|
|
61
71
|
config: unknown;
|
|
62
72
|
state: unknown;
|
|
73
|
+
/** The trial's `auto` paths when saved; empty means every field was pinned.
|
|
74
|
+
* Absent on a snapshot saved before snapshots carried them, which loads
|
|
75
|
+
* over the trial's current set rather than guessing one. */
|
|
76
|
+
auto?: readonly string[];
|
|
63
77
|
savedAt: number;
|
|
64
78
|
}
|
|
65
79
|
|
|
@@ -111,9 +125,14 @@ export interface TrialStateHandle<TS, TC> {
|
|
|
111
125
|
state: TS;
|
|
112
126
|
setState: (next: TS | ((prev: TS) => TS)) => void;
|
|
113
127
|
config: TC;
|
|
128
|
+
/** The config as stored, before any auto path is resolved. What the control
|
|
129
|
+
* panel renders, so a ghosted control sits at the value it pins back to. */
|
|
130
|
+
raw: TC;
|
|
131
|
+
/** Dotted paths this trial has unpinned. */
|
|
132
|
+
auto: ReadonlySet<string>;
|
|
114
133
|
/** Write one config value, by dotted path — `'grid.size'` for a leaf under
|
|
115
134
|
* an `f.group`, `'cellSize'` for one at the root. */
|
|
116
|
-
setConfig: <P extends ConfigPath<TC> & string>(path: P, value: ValueAtPath<TC, P>) => void;
|
|
135
|
+
setConfig: <P extends ConfigPath<TC> & string>(path: P, value: ValueAtPath<TC, P> | Auto) => void;
|
|
117
136
|
}
|
|
118
137
|
|
|
119
138
|
/** Options for `createLabStore`, which knows nothing about storage. */
|
|
@@ -129,6 +148,10 @@ export interface CreateLabStoreOptions {
|
|
|
129
148
|
/** Each instrument's `migrateConfig`, keyed by instrument name, run on a
|
|
130
149
|
* stored config before its defaults fill it. */
|
|
131
150
|
configMigrations?: Record<string, (stored: unknown) => unknown>;
|
|
151
|
+
/** Each instrument's resolved config schema, keyed by instrument name. What
|
|
152
|
+
* a trial's auto paths are read against, away from the lab that would
|
|
153
|
+
* otherwise supply the rules. */
|
|
154
|
+
configSchemas?: Record<string, ResolvedConfig>;
|
|
132
155
|
/** How each instrument's state survives a reload. Hydration is the first
|
|
133
156
|
* thing `createLabStore` does, so these have to arrive with the store. */
|
|
134
157
|
serializers?: InstrumentSerializers;
|
|
@@ -142,6 +165,7 @@ export interface InstrumentHooks {
|
|
|
142
165
|
serializers: InstrumentSerializers;
|
|
143
166
|
configDefaults: Record<string, () => unknown>;
|
|
144
167
|
configMigrations: Record<string, (stored: unknown) => unknown>;
|
|
168
|
+
configSchemas: Record<string, ResolvedConfig>;
|
|
145
169
|
}
|
|
146
170
|
|
|
147
171
|
/** Per-instrument serialize/deserialize hooks, keyed by instrument name. An
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { act, renderHook } from '@testing-library/react';
|
|
2
2
|
import type { ReactNode } from 'react';
|
|
3
3
|
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { auto } from '../config/auto';
|
|
5
|
+
import { f } from '../config/builder';
|
|
6
|
+
import { defineInstrument } from '../instrument/defineInstrument';
|
|
7
|
+
import type { Instrument } from '../instrument/types';
|
|
4
8
|
import { LabStoreProvider, TrialIdProvider } from './context';
|
|
5
9
|
import { createLabStore } from './store';
|
|
6
10
|
import { useTrialState } from './useTrialState';
|
|
@@ -99,3 +103,84 @@ describe('useTrialState', () => {
|
|
|
99
103
|
expect(() => renderHook(() => useTrialState())).toThrow('[labkit]');
|
|
100
104
|
});
|
|
101
105
|
});
|
|
106
|
+
|
|
107
|
+
const autoInstrument = defineInstrument({
|
|
108
|
+
name: 'Auto',
|
|
109
|
+
config: f.schema({
|
|
110
|
+
width: f.number(432),
|
|
111
|
+
gap: f.number(12).auto((c) => (c.width as number) / 24),
|
|
112
|
+
}),
|
|
113
|
+
initialState: () => ({ count: 0 }),
|
|
114
|
+
render: () => null,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
function makeAutoWrapper(trialId: string) {
|
|
118
|
+
const store = createLabStore({ instruments: [autoInstrument as Instrument] });
|
|
119
|
+
store.getState().addTrial({
|
|
120
|
+
id: trialId,
|
|
121
|
+
instrumentName: 'Auto',
|
|
122
|
+
config: autoInstrument.defaultConfig(),
|
|
123
|
+
state: { count: 0 },
|
|
124
|
+
view: { zoom: 1, pan: { x: 0, y: 0 } },
|
|
125
|
+
});
|
|
126
|
+
return {
|
|
127
|
+
store,
|
|
128
|
+
wrapper: ({ children }: { children: ReactNode }) => (
|
|
129
|
+
<LabStoreProvider store={store}>
|
|
130
|
+
<TrialIdProvider trialId={trialId}>{children}</TrialIdProvider>
|
|
131
|
+
</LabStoreProvider>
|
|
132
|
+
),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
describe('auto paths', () => {
|
|
137
|
+
it('hands the instrument the resolved config and the raw one separately', () => {
|
|
138
|
+
const { store, wrapper } = makeAutoWrapper('w1');
|
|
139
|
+
const { result } = renderHook(
|
|
140
|
+
() => useTrialState<{ count: number }, { width: number; gap: number }>(),
|
|
141
|
+
{ wrapper },
|
|
142
|
+
);
|
|
143
|
+
act(() => store.getState().updateTrialConfig('w1', 'gap', auto));
|
|
144
|
+
expect(result.current.config.gap).toBe(18);
|
|
145
|
+
expect(result.current.raw.gap).toBe(12);
|
|
146
|
+
expect([...result.current.auto]).toEqual(['gap']);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('leaves a pinned config untouched', () => {
|
|
150
|
+
const { wrapper } = makeAutoWrapper('w1');
|
|
151
|
+
const { result } = renderHook(
|
|
152
|
+
() => useTrialState<{ count: number }, { width: number; gap: number }>(),
|
|
153
|
+
{ wrapper },
|
|
154
|
+
);
|
|
155
|
+
expect(result.current.config.gap).toBe(12);
|
|
156
|
+
expect(result.current.config).toBe(result.current.raw);
|
|
157
|
+
expect([...result.current.auto]).toEqual([]);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('records a path written as auto instead of storing the sentinel', () => {
|
|
161
|
+
const { store } = makeAutoWrapper('w1');
|
|
162
|
+
store.getState().updateTrialConfig('w1', 'gap', 24);
|
|
163
|
+
store.getState().updateTrialConfig('w1', 'gap', auto);
|
|
164
|
+
const rec = store.getState().trials[0];
|
|
165
|
+
expect(rec?.auto).toEqual(['gap']);
|
|
166
|
+
// The last pinned value survives, so un-pinning is lossless.
|
|
167
|
+
expect(rec?.config).toEqual({ width: 432, gap: 24 });
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('pins again when a real value is written to an auto path', () => {
|
|
171
|
+
const { store } = makeAutoWrapper('w1');
|
|
172
|
+
store.getState().updateTrialConfig('w1', 'gap', auto);
|
|
173
|
+
store.getState().updateTrialConfig('w1', 'gap', 30);
|
|
174
|
+
const rec = store.getState().trials[0];
|
|
175
|
+
expect(rec?.auto).toBeUndefined();
|
|
176
|
+
expect((rec?.config as { gap: number }).gap).toBe(30);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('does not allocate a new record when the path is already auto', () => {
|
|
180
|
+
const { store } = makeAutoWrapper('w1');
|
|
181
|
+
store.getState().updateTrialConfig('w1', 'gap', auto);
|
|
182
|
+
const first = store.getState().trials[0];
|
|
183
|
+
store.getState().updateTrialConfig('w1', 'gap', auto);
|
|
184
|
+
expect(store.getState().trials[0]).toBe(first);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useContext } from 'react';
|
|
2
2
|
import { useStore } from 'zustand/react';
|
|
3
|
+
import { useResolvedConfig } from '../config/useResolvedConfig';
|
|
3
4
|
import { LabStoreContext, TrialIdContext } from './context';
|
|
4
5
|
import type { TrialStateHandle } from './types';
|
|
5
6
|
|
|
@@ -19,10 +20,17 @@ export function useTrialState<TS = unknown, TC = unknown>(): TrialStateHandle<TS
|
|
|
19
20
|
|
|
20
21
|
const updateTrialState = useStore(ctx.store, (s) => s.updateTrialState);
|
|
21
22
|
const updateTrialConfig = useStore(ctx.store, (s) => s.updateTrialConfig);
|
|
23
|
+
// The store's own copy, not `useConfigSchema`: that reads `LabContext`, and
|
|
24
|
+
// this hook only has the store's.
|
|
25
|
+
const instrumentHooks = useStore(ctx.store, (s) => s.instrumentHooks);
|
|
26
|
+
const schema = instrumentHooks().configSchemas[record.instrumentName];
|
|
27
|
+
const { config, auto } = useResolvedConfig<TC>(schema, record.config as TC, record.auto);
|
|
22
28
|
|
|
23
29
|
return {
|
|
24
30
|
state: record.state as TS,
|
|
25
|
-
config
|
|
31
|
+
config,
|
|
32
|
+
raw: record.config as TC,
|
|
33
|
+
auto,
|
|
26
34
|
setState: (next) => updateTrialState(trialId, next as Parameters<typeof updateTrialState>[1]),
|
|
27
35
|
setConfig: (path, value) => updateTrialConfig(trialId, path, value),
|
|
28
36
|
};
|
package/src/styles.less
CHANGED
package/src/surface/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export { composeRects, rectsEqual } from './composeRects';
|
|
2
2
|
export { toDeviceRect } from './deviceRect';
|
|
3
3
|
export type { Box, Rect } from './rect';
|
|
4
|
-
export { SurfaceCanvasContext, SurfaceContext } from './SurfaceContext';
|
|
5
4
|
export type { SurfaceCanvases, SurfaceLayer } from './SurfaceContext';
|
|
5
|
+
export { SurfaceCanvasContext, SurfaceContext } from './SurfaceContext';
|
|
6
6
|
export {
|
|
7
7
|
useSurface,
|
|
8
8
|
useSurfaceCanvas,
|
|
@@ -11,7 +11,7 @@ function fakeHandle(): SurfaceHandle {
|
|
|
11
11
|
invalidateRects: vi.fn(),
|
|
12
12
|
registerTile: vi.fn(),
|
|
13
13
|
registerClear: vi.fn(() => () => {}),
|
|
14
|
-
|
|
14
|
+
registerPainter: vi.fn(() => () => {}),
|
|
15
15
|
containerRef: vi.fn(),
|
|
16
16
|
getContainer: vi.fn(() => null),
|
|
17
17
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { act, render } from '@testing-library/react';
|
|
1
|
+
import { act, render, renderHook } from '@testing-library/react';
|
|
2
2
|
import { useEffect } from 'react';
|
|
3
3
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
4
|
import type { SurfaceFrame, SurfaceHandle } from './useTiledSurface';
|
|
@@ -315,8 +315,20 @@ describe('useTiledSurface', () => {
|
|
|
315
315
|
surface.containerRef(el);
|
|
316
316
|
}}
|
|
317
317
|
>
|
|
318
|
-
<div
|
|
319
|
-
|
|
318
|
+
<div
|
|
319
|
+
ref={(el) => {
|
|
320
|
+
if (!el) return;
|
|
321
|
+
stubBox(el, 0, 0, 400, 600);
|
|
322
|
+
surface.registerTile('a', el);
|
|
323
|
+
}}
|
|
324
|
+
/>
|
|
325
|
+
<div
|
|
326
|
+
ref={(el) => {
|
|
327
|
+
if (!el) return;
|
|
328
|
+
stubBox(el, 400, 0, 400, 600);
|
|
329
|
+
surface.registerTile('b', el);
|
|
330
|
+
}}
|
|
331
|
+
/>
|
|
320
332
|
</div>
|
|
321
333
|
);
|
|
322
334
|
}
|
|
@@ -345,7 +357,12 @@ describe('useTiledSurface', () => {
|
|
|
345
357
|
const frames: SurfaceFrame[] = [];
|
|
346
358
|
let handle: SurfaceHandle | null = null;
|
|
347
359
|
const { getByTestId } = render(
|
|
348
|
-
<Harness
|
|
360
|
+
<Harness
|
|
361
|
+
frames={frames}
|
|
362
|
+
onHandle={(h) => {
|
|
363
|
+
handle = h;
|
|
364
|
+
}}
|
|
365
|
+
/>,
|
|
349
366
|
);
|
|
350
367
|
flushFrames();
|
|
351
368
|
frames.length = 0;
|
|
@@ -368,7 +385,12 @@ describe('useTiledSurface', () => {
|
|
|
368
385
|
const frames: SurfaceFrame[] = [];
|
|
369
386
|
let handle: SurfaceHandle | null = null;
|
|
370
387
|
const { getByTestId } = render(
|
|
371
|
-
<Harness
|
|
388
|
+
<Harness
|
|
389
|
+
frames={frames}
|
|
390
|
+
onHandle={(h) => {
|
|
391
|
+
handle = h;
|
|
392
|
+
}}
|
|
393
|
+
/>,
|
|
372
394
|
);
|
|
373
395
|
flushFrames();
|
|
374
396
|
|
|
@@ -407,7 +429,12 @@ describe('useTiledSurface', () => {
|
|
|
407
429
|
const frames: SurfaceFrame[] = [];
|
|
408
430
|
let handle: SurfaceHandle | null = null;
|
|
409
431
|
const { getByTestId } = render(
|
|
410
|
-
<Harness
|
|
432
|
+
<Harness
|
|
433
|
+
frames={frames}
|
|
434
|
+
onHandle={(h) => {
|
|
435
|
+
handle = h;
|
|
436
|
+
}}
|
|
437
|
+
/>,
|
|
411
438
|
);
|
|
412
439
|
flushFrames();
|
|
413
440
|
|
|
@@ -423,8 +450,18 @@ describe('useTiledSurface', () => {
|
|
|
423
450
|
const stops = [340, 370, 400, 400, 400];
|
|
424
451
|
let call = 0;
|
|
425
452
|
vi.spyOn(b, 'getBoundingClientRect').mockImplementation(() => {
|
|
426
|
-
const left = stops[Math.min(call++, stops.length - 1)]
|
|
427
|
-
return {
|
|
453
|
+
const left = stops[Math.min(call++, stops.length - 1)] ?? 0;
|
|
454
|
+
return {
|
|
455
|
+
left,
|
|
456
|
+
top: 0,
|
|
457
|
+
width: 400,
|
|
458
|
+
height: 600,
|
|
459
|
+
right: left + 400,
|
|
460
|
+
bottom: 600,
|
|
461
|
+
x: left,
|
|
462
|
+
y: 0,
|
|
463
|
+
toJSON: () => ({}),
|
|
464
|
+
} as DOMRect;
|
|
428
465
|
});
|
|
429
466
|
|
|
430
467
|
seen.length = 0;
|
|
@@ -442,13 +479,20 @@ describe('useTiledSurface', () => {
|
|
|
442
479
|
const frames: SurfaceFrame[] = [];
|
|
443
480
|
let handle: SurfaceHandle | null = null;
|
|
444
481
|
const { getByTestId } = render(
|
|
445
|
-
<Harness
|
|
482
|
+
<Harness
|
|
483
|
+
frames={frames}
|
|
484
|
+
onHandle={(h) => {
|
|
485
|
+
handle = h;
|
|
486
|
+
}}
|
|
487
|
+
/>,
|
|
446
488
|
);
|
|
447
489
|
flushFrames();
|
|
448
490
|
|
|
449
491
|
const clear = vi.fn();
|
|
450
492
|
let off: (() => void) | undefined;
|
|
451
|
-
act(() => {
|
|
493
|
+
act(() => {
|
|
494
|
+
off = handle?.registerClear('a', clear);
|
|
495
|
+
});
|
|
452
496
|
stubBox(getByTestId('b'), 310, 0, 400, 600);
|
|
453
497
|
act(() => handle?.invalidateRects());
|
|
454
498
|
flushFrames();
|
|
@@ -462,3 +506,14 @@ describe('useTiledSurface', () => {
|
|
|
462
506
|
});
|
|
463
507
|
});
|
|
464
508
|
});
|
|
509
|
+
|
|
510
|
+
describe('useTiledSurface handle identity', () => {
|
|
511
|
+
it('returns the same handle across renders', () => {
|
|
512
|
+
// A new handle per render reaches useSurfaceTile through context as a new
|
|
513
|
+
// ref callback, which unregisters and re-registers every tile and retiles.
|
|
514
|
+
const { result, rerender } = renderHook(() => useTiledSurface({ onFrame: () => {} }));
|
|
515
|
+
const first = result.current;
|
|
516
|
+
rerender();
|
|
517
|
+
expect(result.current).toBe(first);
|
|
518
|
+
});
|
|
519
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useVisibleRaf } from '@weasel-js/core';
|
|
2
|
-
import { useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
3
3
|
import { composeRects, rectsEqual } from './composeRects';
|
|
4
4
|
import type { Box, Rect } from './rect';
|
|
5
5
|
|
|
@@ -235,14 +235,26 @@ export function useTiledSurface({ onFrame }: UseTiledSurfaceOptions): SurfaceHan
|
|
|
235
235
|
};
|
|
236
236
|
}, [schedule, frameLoop]);
|
|
237
237
|
|
|
238
|
-
return
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
238
|
+
return useMemo(
|
|
239
|
+
() => ({
|
|
240
|
+
invalidate,
|
|
241
|
+
invalidateAll,
|
|
242
|
+
invalidateRects,
|
|
243
|
+
registerTile,
|
|
244
|
+
registerClear,
|
|
245
|
+
registerPainter,
|
|
246
|
+
containerRef,
|
|
247
|
+
getContainer,
|
|
248
|
+
}),
|
|
249
|
+
[
|
|
250
|
+
invalidate,
|
|
251
|
+
invalidateAll,
|
|
252
|
+
invalidateRects,
|
|
253
|
+
registerTile,
|
|
254
|
+
registerClear,
|
|
255
|
+
registerPainter,
|
|
256
|
+
containerRef,
|
|
257
|
+
getContainer,
|
|
258
|
+
],
|
|
259
|
+
);
|
|
248
260
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from '@
|
|
1
|
+
import type { Meta, StoryObj } from '@weasel-js/forge';
|
|
2
2
|
import type { TokenName } from '@weasel-js/theme';
|
|
3
3
|
import { useTheme } from '@weasel-js/theme/react';
|
|
4
4
|
import {
|
|
@@ -18,7 +18,7 @@ const meta: Meta = {
|
|
|
18
18
|
description: {
|
|
19
19
|
component:
|
|
20
20
|
'The interstellar theme: deep-space cosmic gradient + starscape backdrop, glass surfaces, Oswald display type, purple accent. ' +
|
|
21
|
-
'
|
|
21
|
+
'A pins-only theme definition in `src/theme/interstellar.theme.json`, loaded with `defineTheme`; it extends the built-in weasel theme, overriding values rather than adding tokens.',
|
|
22
22
|
},
|
|
23
23
|
},
|
|
24
24
|
},
|
|
@@ -1,11 +1,20 @@
|
|
|
1
|
-
import { resolveTheme } from '@weasel-js/theme';
|
|
1
|
+
import { resolveTheme, THEME_SOURCES, type ThemeDefinition } from '@weasel-js/theme';
|
|
2
|
+
import { derive } from '@weasel-js/theme/engine';
|
|
2
3
|
import { describe, expect, it } from 'vitest';
|
|
3
4
|
import { interstellarTheme } from './interstellar';
|
|
5
|
+
import definition from './interstellar.theme.json' with { type: 'json' };
|
|
4
6
|
|
|
5
7
|
describe('interstellarTheme', () => {
|
|
8
|
+
it('derives as a definition extending weasel with no issues', () => {
|
|
9
|
+
const lookup = (name: string) => (name === 'weasel' ? THEME_SOURCES.weasel : undefined);
|
|
10
|
+
for (const mode of ['dark', 'light']) {
|
|
11
|
+
expect(derive(definition as ThemeDefinition, { mode }, lookup).issues, mode).toEqual([]);
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
|
|
6
15
|
it('extends weasel and resolves both modes', () => {
|
|
7
|
-
const dark = resolveTheme(interstellarTheme, 'dark');
|
|
8
|
-
const light = resolveTheme(interstellarTheme, 'light');
|
|
16
|
+
const dark = resolveTheme(interstellarTheme, { mode: 'dark' });
|
|
17
|
+
const light = resolveTheme(interstellarTheme, { mode: 'light' });
|
|
9
18
|
|
|
10
19
|
// Values carried over verbatim from the retired Less.
|
|
11
20
|
expect(dark['--wzl-surface']).toBe('#0a0a14');
|
|
@@ -24,7 +33,9 @@ describe('interstellarTheme', () => {
|
|
|
24
33
|
});
|
|
25
34
|
|
|
26
35
|
it('carries the cosmic backdrop in dark and drops it in light', () => {
|
|
27
|
-
expect(resolveTheme(interstellarTheme, 'dark')['--wzl-backdrop']).toContain(
|
|
28
|
-
|
|
36
|
+
expect(resolveTheme(interstellarTheme, { mode: 'dark' })['--wzl-backdrop']).toContain(
|
|
37
|
+
'radial-gradient',
|
|
38
|
+
);
|
|
39
|
+
expect(resolveTheme(interstellarTheme, { mode: 'light' })['--wzl-backdrop']).toBe('none');
|
|
29
40
|
});
|
|
30
41
|
});
|