@weasel-js/labkit 1.5.2 → 1.6.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 +3 -7
- package/dist/_dts/{CanvasStackContext-CMMo180l.d.ts → CanvasStackContext-Wo6OwIVM.d.ts} +1 -1
- package/dist/_dts/FloatingPanel-7j0IcXwz.d.ts +30 -0
- package/dist/_dts/LayerList.d-BoELAMsM.d.ts +83 -0
- package/dist/_dts/{PrefsForm.d-DPy9I8It.d.ts → PrefsRow.d-DFpPIeIC.d.ts} +16 -1
- package/dist/_dts/PropertyField.d-D0ndfIIt.d.ts +404 -0
- package/dist/_dts/{Select.d-ZTiwzYyT.d.ts → Select.d-Cr8-zoup.d.ts} +105 -15
- package/dist/_dts/{usePanZoom-SSTZe410.d.ts → Stage-DWBBi8IU.d.ts} +80 -35
- package/dist/_dts/{index-DlKolEIZ.d.ts → index-CKyX0ME_.d.ts} +12 -13
- package/dist/_dts/{frac-B4hFJMiN.d.ts → types-B7yBTTF9.d.ts} +709 -651
- package/dist/_dts/{types-Db-3NNk1.d.ts → types-DTuViBvc.d.ts} +21 -2
- package/dist/_dts/{useTrialState-pnAE-UBD.d.ts → useTrialState-CMaV6UD4.d.ts} +2 -3
- package/dist/canvas/index.d.ts +41 -8
- package/dist/canvas/index.js +5 -3
- package/dist/chrome/index.d.ts +6 -6
- package/dist/chrome/index.js +9 -7
- package/dist/chunk-24W5VEJW.js +18 -0
- package/dist/chunk-24W5VEJW.js.map +1 -0
- package/dist/chunk-2RVCB2PT.js +3 -0
- package/dist/chunk-2RVCB2PT.js.map +1 -0
- package/dist/chunk-3RY6WL5T.js +38 -0
- package/dist/chunk-3RY6WL5T.js.map +1 -0
- package/dist/{chunk-CKUKF72R.js → chunk-4TT3PNPR.js} +68 -55
- package/dist/chunk-4TT3PNPR.js.map +1 -0
- package/dist/{chunk-KJCUERQF.js → chunk-6FLXL3I7.js} +9 -164
- package/dist/chunk-6FLXL3I7.js.map +1 -0
- package/dist/chunk-ASOK76ZH.js +431 -0
- package/dist/chunk-ASOK76ZH.js.map +1 -0
- package/dist/chunk-AWQG7FCC.js +19 -0
- package/dist/chunk-AWQG7FCC.js.map +1 -0
- package/dist/chunk-CGHF6RZV.js +300 -0
- package/dist/chunk-CGHF6RZV.js.map +1 -0
- package/dist/chunk-DWMSB4CA.js +228 -0
- package/dist/chunk-DWMSB4CA.js.map +1 -0
- package/dist/chunk-GHGO52SO.js +92 -0
- package/dist/chunk-GHGO52SO.js.map +1 -0
- package/dist/{chunk-LRI3MBGR.js → chunk-HKV7LRHY.js} +4 -4
- package/dist/{chunk-LRI3MBGR.js.map → chunk-HKV7LRHY.js.map} +1 -1
- package/dist/{chunk-EMXAIRUJ.js → chunk-IRTGUQ5O.js} +5 -4
- package/dist/chunk-IRTGUQ5O.js.map +1 -0
- package/dist/{chunk-H6ZAOWNE.js → chunk-JVBYV6TL.js} +3 -3
- package/dist/{chunk-H6ZAOWNE.js.map → chunk-JVBYV6TL.js.map} +1 -1
- package/dist/chunk-KHK5FGER.js +145 -0
- package/dist/chunk-KHK5FGER.js.map +1 -0
- package/dist/chunk-NEXR37YJ.js +144 -0
- package/dist/chunk-NEXR37YJ.js.map +1 -0
- package/dist/{chunk-A2ACAGW6.js → chunk-O6FFFOHS.js} +32 -7
- package/dist/chunk-O6FFFOHS.js.map +1 -0
- package/dist/chunk-QGMOA2YE.js +731 -0
- package/dist/chunk-QGMOA2YE.js.map +1 -0
- package/dist/{chunk-ERFMC6WU.js → chunk-S4NCI32V.js} +19 -10
- package/dist/chunk-S4NCI32V.js.map +1 -0
- package/dist/{chunk-753Y6HE6.js → chunk-SLAQPUI4.js} +4 -4
- package/dist/chunk-SLAQPUI4.js.map +1 -0
- package/dist/{chunk-REOBGYMC.js → chunk-TJ77A4A3.js} +3 -5
- package/dist/chunk-TJ77A4A3.js.map +1 -0
- package/dist/chunk-TYN6MLXX.js +13864 -0
- package/dist/chunk-TYN6MLXX.js.map +1 -0
- package/dist/{chunk-RS3HRQWU.js → chunk-WPLAANH4.js} +4 -3
- package/dist/chunk-WPLAANH4.js.map +1 -0
- package/dist/config/index.d.ts +51 -7
- package/dist/config/index.js +4 -5
- package/dist/controls/index.d.ts +92 -5
- package/dist/controls/index.js +5 -4
- package/dist/dragdrop/index.d.ts +9 -6
- package/dist/dragdrop/index.js +3 -2
- package/dist/index.d.ts +108 -154
- package/dist/index.js +456 -600
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +5 -35
- package/dist/layers/index.js +3 -3
- package/dist/loupe/index.d.ts +21 -17
- package/dist/loupe/index.js +5 -3
- package/dist/overview/index.d.ts +56 -0
- package/dist/overview/index.js +288 -0
- package/dist/overview/index.js.map +1 -0
- package/dist/passthrough/weasel-ui.d.ts +434 -231
- package/dist/passthrough/weasel-ui.js +2 -1
- package/dist/primitives/index.d.ts +19 -54
- package/dist/primitives/index.js +6 -4
- package/dist/state/index.d.ts +8 -7
- package/dist/state/index.js +4 -5
- package/dist/state/index.js.map +1 -1
- package/dist/styles.css +360 -174
- package/dist/undo/index.d.ts +5 -4
- package/package.json +13 -13
- package/src/annotations/frac.test.ts +7 -0
- package/src/annotations/index.ts +1 -0
- package/src/annotations/paint.test.ts +21 -0
- package/src/annotations/paint.ts +12 -0
- package/src/annotations/store.test.ts +57 -0
- package/src/annotations/store.ts +25 -6
- package/src/annotations/toolMap.test.ts +37 -2
- package/src/annotations/toolMap.ts +13 -0
- package/src/annotations/types.ts +30 -2
- package/src/canvas/AGENTS.md +8 -10
- package/src/canvas/CameraInput.test.tsx +136 -0
- package/src/canvas/CameraInput.tsx +311 -0
- package/src/canvas/CanvasStack.tsx +42 -58
- package/src/canvas/LinkedCursor.less +16 -0
- package/src/canvas/LinkedCursor.tsx +54 -0
- package/src/canvas/Stage.test.tsx +18 -0
- package/src/canvas/Stage.tsx +42 -37
- package/src/canvas/cameraView.test.ts +60 -0
- package/src/canvas/cameraView.ts +61 -0
- package/src/canvas/index.ts +11 -2
- package/src/chrome/regions/SidebarRegion.less +197 -0
- package/src/chrome/regions/SidebarRegion.test.tsx +27 -0
- package/src/chrome/regions/SidebarRegion.tsx +7 -2
- package/src/chrome/types.ts +5 -2
- package/src/config/autoConfig.ts +2 -2
- package/src/config/builder.ts +21 -2
- package/src/config/entry.test.ts +10 -0
- package/src/config/fromConfigField.ts +1 -0
- package/src/config/index.ts +5 -0
- package/src/config/resolve.ts +11 -1
- package/src/config/rules.ts +2 -0
- package/src/config/sectionTree.test.ts +152 -0
- package/src/config/sectionTree.ts +134 -0
- package/src/config/types.ts +21 -0
- package/src/controls/ControlPanel.less +4 -0
- package/src/controls/ControlPanel.stories.tsx +55 -2
- package/src/controls/ControlPanel.test.tsx +40 -3
- package/src/controls/ControlPanel.tsx +134 -336
- package/src/controls/inDialog.test.tsx +100 -0
- package/src/controls/inDialog.tsx +38 -0
- package/src/controls/index.ts +1 -0
- package/src/index.test.ts +1 -1
- package/src/index.ts +18 -29
- package/src/instrument/SineWave.smoke.test.tsx +2 -0
- package/src/instrument/types.ts +15 -3
- package/src/lab/Lab.nebula.test.tsx +40 -0
- package/src/lab/Lab.test.tsx +3 -1
- package/src/lab/Lab.tsx +42 -19
- package/src/lab/LabBoundary.test.tsx +66 -0
- package/src/lab/LabBoundary.tsx +56 -0
- package/src/lab/LabHeader.tsx +4 -23
- package/src/lab/LabRoot.tsx +2 -3
- package/src/lab/LabShell.less +0 -1
- package/src/lab/LabShell.stories.tsx +15 -0
- package/src/lab/LabShell.test.tsx +18 -0
- package/src/lab/LabShell.tsx +14 -1
- package/src/lab/LabSwitcher.less +1 -1
- package/src/lab/LabSwitcher.test.tsx +41 -0
- package/src/lab/LabSwitcher.tsx +30 -8
- package/src/lab/index.ts +1 -0
- package/src/layers/AGENTS.md +18 -74
- package/src/layers/index.ts +4 -2
- package/src/loupe/AGENTS.md +7 -6
- package/src/loupe/LoupeGestures.tsx +14 -9
- package/src/loupe/TrialLoupe.tsx +5 -5
- package/src/loupe/index.ts +1 -0
- package/src/loupe/loupeActions.ts +6 -2
- package/src/overview/AGENTS.md +38 -0
- package/src/overview/OverviewMarks.test.tsx +100 -0
- package/src/overview/OverviewMarks.tsx +93 -0
- package/src/overview/TrialOverview.less +53 -0
- package/src/overview/TrialOverview.stories.tsx +50 -0
- package/src/overview/TrialOverview.test.tsx +116 -0
- package/src/overview/TrialOverview.tsx +374 -0
- package/src/overview/index.ts +2 -0
- package/src/passthrough/weasel-ui.ts +32 -10
- package/src/primitives/index.ts +0 -2
- package/src/specimen/Specimen.tsx +59 -56
- package/src/state/labRecords.test.ts +4 -1
- package/src/state/store.test.ts +4 -4
- package/src/state/types.ts +2 -1
- package/src/state/useTrialState.test.tsx +1 -1
- package/src/styles.less +6 -2
- package/src/theme/Interstellar.stories.tsx +8 -13
- package/src/theme/base.less +3 -20
- package/src/theme/interstellar.test.ts +6 -3
- package/src/theme/interstellar.theme.json +0 -56
- package/src/theme/interstellar.ts +2 -2
- package/src/tools/labTool.ts +6 -2
- package/src/trial/Trial.annotations.test.tsx +16 -0
- package/src/trial/Trial.canvas.test.tsx +1 -1
- package/src/trial/Trial.loupe.test.tsx +73 -1
- package/src/trial/Trial.stories.tsx +2 -0
- package/src/trial/Trial.tsx +47 -9
- package/src/trial/trialOps.test.ts +4 -4
- package/dist/_dts/index-8LiKe3A-.d.ts +0 -410
- package/dist/chunk-2AYGEN57.js +0 -32
- package/dist/chunk-2AYGEN57.js.map +0 -1
- package/dist/chunk-6BUQ77ZV.js +0 -18
- package/dist/chunk-6BUQ77ZV.js.map +0 -1
- package/dist/chunk-753Y6HE6.js.map +0 -1
- package/dist/chunk-A2ACAGW6.js.map +0 -1
- package/dist/chunk-CKUKF72R.js.map +0 -1
- package/dist/chunk-D35PFTS7.js +0 -170
- package/dist/chunk-D35PFTS7.js.map +0 -1
- package/dist/chunk-EMXAIRUJ.js.map +0 -1
- package/dist/chunk-ERFMC6WU.js.map +0 -1
- package/dist/chunk-FKLT3ITC.js +0 -590
- package/dist/chunk-FKLT3ITC.js.map +0 -1
- package/dist/chunk-KJCUERQF.js.map +0 -1
- package/dist/chunk-LASCBWW7.js +0 -13456
- package/dist/chunk-LASCBWW7.js.map +0 -1
- package/dist/chunk-REOBGYMC.js.map +0 -1
- package/dist/chunk-RS3HRQWU.js.map +0 -1
- package/dist/chunk-TT5C5S6R.js +0 -368
- package/dist/chunk-TT5C5S6R.js.map +0 -1
- package/dist/ui/layers/index.d.ts +0 -66
- package/dist/ui/layers/index.js +0 -3
- package/dist/ui/layers/index.js.map +0 -1
- package/src/canvas/usePanZoom.test.ts +0 -244
- package/src/canvas/usePanZoom.ts +0 -147
- package/src/lab/useSystemMode.ts +0 -30
- package/src/layers/LayerList.less +0 -93
- package/src/layers/LayerList.stories.tsx +0 -66
- package/src/layers/LayerList.test.tsx +0 -232
- package/src/layers/LayerList.tsx +0 -268
- package/src/primitives/Sidebar.less +0 -112
- package/src/primitives/Sidebar.stories.tsx +0 -38
- package/src/primitives/Sidebar.test.tsx +0 -74
- package/src/primitives/Sidebar.tsx +0 -35
- package/src/ui/layers/index.ts +0 -5
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { f } from '../config/builder';
|
|
5
|
+
import { withValueAtPath } from '../config/path';
|
|
6
|
+
import { resolveConfigSchema } from '../config/resolve';
|
|
7
|
+
import { ControlPanel } from './ControlPanel';
|
|
8
|
+
import { summarizeValue } from './inDialog';
|
|
9
|
+
|
|
10
|
+
function Panel({ schema }: { schema: Parameters<typeof resolveConfigSchema>[0] }) {
|
|
11
|
+
const resolved = resolveConfigSchema(schema);
|
|
12
|
+
const [config, setConfig] = useState<Record<string, unknown>>(
|
|
13
|
+
schema.defaults() as Record<string, unknown>,
|
|
14
|
+
);
|
|
15
|
+
return (
|
|
16
|
+
<>
|
|
17
|
+
<ControlPanel
|
|
18
|
+
schema={resolved}
|
|
19
|
+
config={config}
|
|
20
|
+
setConfig={(path, value) => setConfig((prev) => withValueAtPath(prev, path, value))}
|
|
21
|
+
/>
|
|
22
|
+
<output data-testid="config">{JSON.stringify(config)}</output>
|
|
23
|
+
</>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('f.list', () => {
|
|
28
|
+
it('summarizes the list on the row and edits it in a dialog', () => {
|
|
29
|
+
const schema = f.schema({
|
|
30
|
+
icons: f.list(['**/node_modules/**', 'dist']).label('Draw as icons'),
|
|
31
|
+
});
|
|
32
|
+
render(<Panel schema={schema} />);
|
|
33
|
+
const trigger = screen.getByRole('button', { name: 'Edit Draw as icons' });
|
|
34
|
+
expect(trigger.textContent).toContain('**/node_modules/**, dist');
|
|
35
|
+
|
|
36
|
+
fireEvent.click(trigger);
|
|
37
|
+
fireEvent.change(screen.getByRole('textbox', { name: 'Draw as icons 2' }), {
|
|
38
|
+
target: { value: 'build' },
|
|
39
|
+
});
|
|
40
|
+
expect(screen.getByTestId('config').textContent).toBe(
|
|
41
|
+
JSON.stringify({ icons: ['**/node_modules/**', 'build'] }),
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('is the kind an f.value with a string-array default takes', () => {
|
|
46
|
+
const resolved = resolveConfigSchema(f.schema({ names: f.value(['a']) }));
|
|
47
|
+
expect(resolved.group.children.names).toMatchObject({ kind: 'list' });
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('.dialog', () => {
|
|
52
|
+
it('hosts any renderer in the dialog, handed the row context', () => {
|
|
53
|
+
const schema = f.schema({
|
|
54
|
+
note: f.string('hi').dialog(({ value, setValue }) => (
|
|
55
|
+
<button type="button" onClick={() => setValue(`${value}!`)}>
|
|
56
|
+
shout
|
|
57
|
+
</button>
|
|
58
|
+
)),
|
|
59
|
+
});
|
|
60
|
+
render(<Panel schema={schema} />);
|
|
61
|
+
fireEvent.click(screen.getByRole('button', { name: 'Edit Note' }));
|
|
62
|
+
fireEvent.click(screen.getByRole('button', { name: 'shout' }));
|
|
63
|
+
expect(screen.getByTestId('config').textContent).toBe(JSON.stringify({ note: 'hi!' }));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('takes a summary of its own', () => {
|
|
67
|
+
const summary = vi.fn((v: unknown) => `${(v as number[]).length} points`);
|
|
68
|
+
const schema = f.schema({ pts: f.custom('points', [1, 2, 3]).dialog(() => null, { summary }) });
|
|
69
|
+
render(<Panel schema={schema} />);
|
|
70
|
+
expect(screen.getByRole('button', { name: 'Edit Pts' }).textContent).toContain('3 points');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('gives way to a later .render, and takes over from an earlier one', () => {
|
|
74
|
+
const schema = f.schema({
|
|
75
|
+
a: f
|
|
76
|
+
.string('x')
|
|
77
|
+
.dialog(() => null)
|
|
78
|
+
.render(() => <span>row a</span>),
|
|
79
|
+
b: f
|
|
80
|
+
.string('y')
|
|
81
|
+
.render(() => <span>row b</span>)
|
|
82
|
+
.dialog(() => null),
|
|
83
|
+
});
|
|
84
|
+
render(<Panel schema={schema} />);
|
|
85
|
+
expect(screen.getByText('row a')).toBeTruthy();
|
|
86
|
+
expect(screen.queryByRole('button', { name: 'Edit A' })).toBeNull();
|
|
87
|
+
expect(screen.queryByText('row b')).toBeNull();
|
|
88
|
+
expect(screen.getByRole('button', { name: 'Edit B' })).toBeTruthy();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('summarizeValue', () => {
|
|
93
|
+
it('joins lists and names emptiness', () => {
|
|
94
|
+
expect(summarizeValue(['a', 'b'])).toBe('a, b');
|
|
95
|
+
expect(summarizeValue([])).toBe('None');
|
|
96
|
+
expect(summarizeValue('')).toBe('None');
|
|
97
|
+
expect(summarizeValue(3)).toBe('3');
|
|
98
|
+
expect(summarizeValue({ a: 1 })).toBe('{"a":1}');
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { DialogRow } from '@weasel-js/ui';
|
|
2
|
+
import type { ControlRenderer, InDialogOptions } from '../config/types';
|
|
3
|
+
|
|
4
|
+
export type { InDialogOptions };
|
|
5
|
+
|
|
6
|
+
/** The one-line summary a dialog row shows when it is given none. */
|
|
7
|
+
export function summarizeValue(value: unknown): string {
|
|
8
|
+
if (Array.isArray(value)) return value.length === 0 ? 'None' : value.join(', ');
|
|
9
|
+
if (value === undefined || value === null || value === '') return 'None';
|
|
10
|
+
return typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Moves a control into a modal: the row becomes a button showing a summary of
|
|
15
|
+
* the value, and the button opens a dialog whose body is `body`, handed the
|
|
16
|
+
* same context a row renderer gets. For a leaf whose editor will never fit a
|
|
17
|
+
* sidebar row.
|
|
18
|
+
*/
|
|
19
|
+
export function inDialog(body: ControlRenderer, opts: InDialogOptions = {}): ControlRenderer {
|
|
20
|
+
const summary = opts.summary ?? summarizeValue;
|
|
21
|
+
return (ctx) => {
|
|
22
|
+
const { pref, value, auto, setAuto } = ctx;
|
|
23
|
+
const manual = (pref as { manual?: boolean }).manual === true;
|
|
24
|
+
return (
|
|
25
|
+
<DialogRow
|
|
26
|
+
label={pref.name}
|
|
27
|
+
description={pref.description}
|
|
28
|
+
summary={summary(value)}
|
|
29
|
+
title={opts.title}
|
|
30
|
+
readout={auto ? 'auto' : undefined}
|
|
31
|
+
auto={auto}
|
|
32
|
+
onAutoChange={manual ? undefined : setAuto}
|
|
33
|
+
>
|
|
34
|
+
{() => body(ctx)}
|
|
35
|
+
</DialogRow>
|
|
36
|
+
);
|
|
37
|
+
};
|
|
38
|
+
}
|
package/src/controls/index.ts
CHANGED
package/src/index.test.ts
CHANGED
|
@@ -72,7 +72,7 @@ describe('surface, job and orbit entry points', () => {
|
|
|
72
72
|
expect(typeof kit.annotationsFromJSON).toBe('function');
|
|
73
73
|
expect(typeof kit.seenFrom).toBe('function');
|
|
74
74
|
expect(typeof kit.fracToWorld).toBe('function');
|
|
75
|
-
expect(typeof kit.
|
|
75
|
+
expect(typeof kit.CameraInput).toBe('function');
|
|
76
76
|
expect(typeof kit.useJob).toBe('function');
|
|
77
77
|
expect(typeof kit.as2DView).toBe('function');
|
|
78
78
|
expect(kit.SurfaceContext).toBeDefined();
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The property-panel family,
|
|
1
|
+
// The property-panel family, LayerList, Input, Select, TokenPanel, ToggleBar and Button live in
|
|
2
2
|
// `@weasel-js/ui`; labkit re-exports them so chrome built on labkit needs no
|
|
3
3
|
// direct ui dependency. Named, not `export *` — a star re-export of an external
|
|
4
4
|
// package emits no binding in the bundle.
|
|
@@ -8,19 +8,13 @@ export type {
|
|
|
8
8
|
ButtonProps,
|
|
9
9
|
ButtonSize,
|
|
10
10
|
ButtonVariant,
|
|
11
|
-
CheckboxRowProps,
|
|
12
|
-
ColorRowProps,
|
|
13
11
|
CurveFieldProps,
|
|
14
12
|
CurveMark,
|
|
15
|
-
EffectCardListItem,
|
|
16
|
-
EffectCardListProps,
|
|
17
|
-
EffectCardProps,
|
|
18
13
|
IconProps,
|
|
19
14
|
InputProps,
|
|
20
15
|
JogProps,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
NumberRowProps,
|
|
16
|
+
PropertyControlProps,
|
|
17
|
+
PropertyFieldProps,
|
|
24
18
|
PropertyGroupProps,
|
|
25
19
|
PropertyListPack,
|
|
26
20
|
PropertyListProps,
|
|
@@ -31,15 +25,11 @@ export type {
|
|
|
31
25
|
PropertyRowVariant,
|
|
32
26
|
PropertySpanProps,
|
|
33
27
|
SelectProps,
|
|
34
|
-
SelectRowProps,
|
|
35
|
-
SliderRowProps,
|
|
36
28
|
SubpanelProps,
|
|
37
|
-
TextRowProps,
|
|
38
29
|
ToggleBarItem,
|
|
39
30
|
ToggleBarProps,
|
|
40
31
|
ToggleBarSize,
|
|
41
32
|
ToggleBarVariant,
|
|
42
|
-
ToggleRowProps,
|
|
43
33
|
TokenCategory,
|
|
44
34
|
TokenEntry,
|
|
45
35
|
TokenPanelProps,
|
|
@@ -48,39 +38,31 @@ export type {
|
|
|
48
38
|
} from '@weasel-js/ui';
|
|
49
39
|
export {
|
|
50
40
|
Button,
|
|
51
|
-
CheckboxRow,
|
|
52
41
|
CloseIcon,
|
|
53
42
|
CollapseIcon,
|
|
54
|
-
ColorRow,
|
|
55
43
|
CurveField,
|
|
56
|
-
EffectCard,
|
|
57
|
-
EffectCardList,
|
|
58
44
|
ExpandIcon,
|
|
59
45
|
Icon,
|
|
60
46
|
Input,
|
|
61
47
|
inferTokenType,
|
|
62
48
|
Jog,
|
|
63
|
-
LayerStack,
|
|
64
|
-
NumberRow,
|
|
65
49
|
PauseIcon,
|
|
66
50
|
PlayIcon,
|
|
51
|
+
PropertyControl,
|
|
52
|
+
PropertyField,
|
|
67
53
|
PropertyGroup,
|
|
68
54
|
PropertyList,
|
|
69
55
|
PropertyPanel,
|
|
70
56
|
PropertyRow,
|
|
71
57
|
PropertySpan,
|
|
72
|
-
refitScale,
|
|
73
58
|
ResetIcon,
|
|
59
|
+
refitScale,
|
|
74
60
|
Select,
|
|
75
|
-
SelectRow,
|
|
76
|
-
SliderRow,
|
|
77
61
|
StepBackIcon,
|
|
78
62
|
StepForwardIcon,
|
|
79
63
|
StopIcon,
|
|
80
64
|
Subpanel,
|
|
81
|
-
TextRow,
|
|
82
65
|
ToggleBar,
|
|
83
|
-
ToggleRow,
|
|
84
66
|
TokenPanel,
|
|
85
67
|
} from '@weasel-js/ui';
|
|
86
68
|
export type {
|
|
@@ -97,6 +79,7 @@ export type {
|
|
|
97
79
|
AnnotationsCapability,
|
|
98
80
|
AnnotationTarget,
|
|
99
81
|
AnnotationTargetInfo,
|
|
82
|
+
AnnotationToolId,
|
|
100
83
|
CaptureDeps,
|
|
101
84
|
CaptureOptions,
|
|
102
85
|
CaptureResult,
|
|
@@ -147,13 +130,12 @@ export {
|
|
|
147
130
|
export type { ContentSize, PaneSize } from './annotations/view';
|
|
148
131
|
export { fitView, fromWeaselView, toWeaselView } from './annotations/view';
|
|
149
132
|
export type {
|
|
133
|
+
CameraContextValue,
|
|
150
134
|
CanvasLayerDescriptor,
|
|
151
135
|
CanvasStackContextValue,
|
|
152
136
|
CanvasStackProps,
|
|
153
137
|
CanvasStackSurface,
|
|
154
|
-
PanZoomHandlers,
|
|
155
138
|
StageProps,
|
|
156
|
-
UsePanZoomOptions,
|
|
157
139
|
ViewportSize,
|
|
158
140
|
WorldFrame,
|
|
159
141
|
WorldSpec,
|
|
@@ -161,6 +143,9 @@ export type {
|
|
|
161
143
|
} from './canvas';
|
|
162
144
|
export {
|
|
163
145
|
applyCamera,
|
|
146
|
+
CameraContext,
|
|
147
|
+
CameraInput,
|
|
148
|
+
CameraScope,
|
|
164
149
|
CanvasStack,
|
|
165
150
|
CanvasStackContext,
|
|
166
151
|
centerOn,
|
|
@@ -169,7 +154,6 @@ export {
|
|
|
169
154
|
resolveFrame,
|
|
170
155
|
Stage,
|
|
171
156
|
screenToWorld,
|
|
172
|
-
usePanZoom,
|
|
173
157
|
worldToScreen,
|
|
174
158
|
zoomAt,
|
|
175
159
|
} from './canvas';
|
|
@@ -185,6 +169,7 @@ export {
|
|
|
185
169
|
f,
|
|
186
170
|
GroupNode,
|
|
187
171
|
isConfigBranch,
|
|
172
|
+
ListNode,
|
|
188
173
|
NumberNode,
|
|
189
174
|
StringNode,
|
|
190
175
|
ValueNode,
|
|
@@ -199,6 +184,7 @@ export {
|
|
|
199
184
|
} from './config/path';
|
|
200
185
|
export { resolveConfigSchema } from './config/resolve';
|
|
201
186
|
export { applyRules, builtinRules, titleCase } from './config/rules';
|
|
187
|
+
export { type SectionTree, sectionTree } from './config/sectionTree';
|
|
202
188
|
export type {
|
|
203
189
|
Annotations,
|
|
204
190
|
BranchAnnotations,
|
|
@@ -214,12 +200,14 @@ export type {
|
|
|
214
200
|
ConfigSchema,
|
|
215
201
|
ConfigShape,
|
|
216
202
|
ControlRenderer,
|
|
203
|
+
DialogSpec,
|
|
217
204
|
EntryValue,
|
|
218
205
|
InferConfig,
|
|
219
206
|
LeafPatch,
|
|
220
207
|
NodeOptions,
|
|
221
208
|
NodeValue,
|
|
222
209
|
ResolvedConfig,
|
|
210
|
+
SectionOption,
|
|
223
211
|
SectionSpec,
|
|
224
212
|
ValueAtPath,
|
|
225
213
|
} from './config/types';
|
|
@@ -227,6 +215,7 @@ export { useConfigSchema } from './config/useConfigSchema';
|
|
|
227
215
|
export { useResolvedConfig } from './config/useResolvedConfig';
|
|
228
216
|
export { isLeafVisible } from './config/visible';
|
|
229
217
|
export { ControlPanel } from './controls/ControlPanel';
|
|
218
|
+
export { type InDialogOptions, inDialog, summarizeValue } from './controls/inDialog';
|
|
230
219
|
export type {
|
|
231
220
|
CheckboxField,
|
|
232
221
|
ColorField,
|
|
@@ -262,8 +251,8 @@ export type { ValidationResult } from './instrument/validateConfigSchema';
|
|
|
262
251
|
export { validateConfigSchema } from './instrument/validateConfigSchema';
|
|
263
252
|
export * from './job';
|
|
264
253
|
export * from './lab';
|
|
265
|
-
export type { LayerListProps,
|
|
266
|
-
export { LayerList } from './layers';
|
|
254
|
+
export type { LayerListItem, LayerListProps, LayerMove } from './layers';
|
|
255
|
+
export { LayerList, moveLayers } from './layers';
|
|
267
256
|
export * from './loupe';
|
|
268
257
|
export * from './primitives';
|
|
269
258
|
export { SPECIMEN_SECTIONS, Specimen } from './specimen/Specimen';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { render } from '@testing-library/react';
|
|
2
|
+
import { createPointerStore } from '@weasel-js/core';
|
|
2
3
|
import type { ReactElement } from 'react';
|
|
3
4
|
import { describe, expect, it, vi } from 'vitest';
|
|
4
5
|
import { DEFAULT_VIEW } from '../state/view';
|
|
@@ -94,6 +95,7 @@ describe('SineWave instrument smoke test', () => {
|
|
|
94
95
|
setZoom: vi.fn(),
|
|
95
96
|
activeToolId: null,
|
|
96
97
|
visibleLayers: [],
|
|
98
|
+
pointer: createPointerStore(),
|
|
97
99
|
},
|
|
98
100
|
emit: vi.fn(),
|
|
99
101
|
};
|
package/src/instrument/types.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { PointerContextValue } from '@weasel-js/core';
|
|
1
2
|
import type { ReactNode } from 'react';
|
|
2
3
|
import type { AnnotationsCapability } from '../annotations/types';
|
|
3
4
|
import type { ViewportSize, WorldSpec } from '../canvas/worldSpec';
|
|
@@ -33,6 +34,14 @@ export interface RenderContext<TS = unknown, TC = unknown> {
|
|
|
33
34
|
* hidden layer's `draw`, so this is the only way to know what was
|
|
34
35
|
* painted — a legend listing rows the view did not draw needs it. */
|
|
35
36
|
visibleLayers: readonly string[];
|
|
37
|
+
/**
|
|
38
|
+
* Where the pointer is over this trial, in the instrument's world:
|
|
39
|
+
* `get()` answers `{ worldX, worldY, viewId }`, where `viewId` is
|
|
40
|
+
* `'stage'` over the trial's own camera and `'overview'` over its
|
|
41
|
+
* `<TrialOverview>`, or `null` when it is over neither. A key handler
|
|
42
|
+
* reading it works over either view.
|
|
43
|
+
*/
|
|
44
|
+
pointer: PointerContextValue;
|
|
36
45
|
};
|
|
37
46
|
emit: (event: string) => void;
|
|
38
47
|
/** Present only when the instrument declares a `job`. */
|
|
@@ -61,7 +70,7 @@ export interface CanvasCapability<TS = unknown, TC = unknown> {
|
|
|
61
70
|
* size, so an instrument can frame content it can only place in terms of
|
|
62
71
|
* the viewport; until then the trial's view is `null`. */
|
|
63
72
|
initialView?: ViewTransform | ((size: ViewportSize) => ViewTransform);
|
|
64
|
-
/** Widens
|
|
73
|
+
/** Widens the camera's default zoom clamp; the opening zoom stays reachable
|
|
65
74
|
* regardless of these. */
|
|
66
75
|
minZoom?: number;
|
|
67
76
|
maxZoom?: number;
|
|
@@ -70,7 +79,7 @@ export interface CanvasCapability<TS = unknown, TC = unknown> {
|
|
|
70
79
|
/** Declares that an instrument's DOM is content of a fixed size, which the
|
|
71
80
|
* trial pans and zooms the way it does a canvas's layers. Not combined with
|
|
72
81
|
* `canvas`: there the DOM is an unzoomed overlay, and `canvas` wins. */
|
|
73
|
-
export interface StageCapability {
|
|
82
|
+
export interface StageCapability<TS = unknown, TC = unknown> {
|
|
74
83
|
/** The content's own size in CSS pixels, at zoom 1. */
|
|
75
84
|
size: ViewportSize;
|
|
76
85
|
/** Where the view starts. Omitted, the content opens centered and shrunk to
|
|
@@ -78,6 +87,9 @@ export interface StageCapability {
|
|
|
78
87
|
initialView?: ViewTransform | ((viewport: ViewportSize) => ViewTransform);
|
|
79
88
|
minZoom?: number;
|
|
80
89
|
maxZoom?: number;
|
|
90
|
+
/** Drawn over the content in viewport pixels, outside the camera — a legend,
|
|
91
|
+
* a floating panel, anything that must not zoom with the picture. */
|
|
92
|
+
overlay?: (ctx: RenderContext<TS, TC>) => ReactNode;
|
|
81
93
|
}
|
|
82
94
|
|
|
83
95
|
/** Declares which of an instrument's layers the trial should offer
|
|
@@ -159,7 +171,7 @@ export interface Instrument<TS = unknown, TC = unknown, TItem = unknown> {
|
|
|
159
171
|
serialize?: (state: TS) => unknown;
|
|
160
172
|
deserialize?: (data: unknown, config: TC) => TS;
|
|
161
173
|
canvas?: CanvasCapability<TS, TC>;
|
|
162
|
-
stage?: StageCapability
|
|
174
|
+
stage?: StageCapability<TS, TC>;
|
|
163
175
|
layers?: LayerCapability;
|
|
164
176
|
dragDrop?: DragDropCapability<TS, TC>;
|
|
165
177
|
undo?: UndoCapability;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { render } from '@testing-library/react';
|
|
2
|
+
import { resolveTheme } from '@weasel-js/theme';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import type { Instrument } from '../instrument/types';
|
|
5
|
+
import { interstellarTheme } from '../theme/interstellar';
|
|
6
|
+
import { Lab } from './Lab';
|
|
7
|
+
|
|
8
|
+
const bare: Instrument = {
|
|
9
|
+
name: 'Bare',
|
|
10
|
+
defaultConfig: () => ({}),
|
|
11
|
+
initialState: () => ({}),
|
|
12
|
+
render: () => null,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const backdrop = (container: HTMLElement) =>
|
|
16
|
+
[...container.querySelectorAll<HTMLElement>('[style]')]
|
|
17
|
+
.map((el) => el.style.getPropertyValue('--wzl-backdrop'))
|
|
18
|
+
.find(Boolean) ?? '';
|
|
19
|
+
|
|
20
|
+
describe('<Lab nebula>', () => {
|
|
21
|
+
it('draws one blob per literal color', () => {
|
|
22
|
+
const { container } = render(
|
|
23
|
+
<Lab
|
|
24
|
+
instruments={[bare]}
|
|
25
|
+
defaultInstrument="Bare"
|
|
26
|
+
mode="dark"
|
|
27
|
+
nebula={['#ff0000', '#00ff00']}
|
|
28
|
+
/>,
|
|
29
|
+
);
|
|
30
|
+
expect(backdrop(container).match(/radial-gradient/g)).toHaveLength(3);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('resolves a theme ramp against the lab’s theme', () => {
|
|
34
|
+
const { container } = render(
|
|
35
|
+
<Lab instruments={[bare]} defaultInstrument="Bare" mode="dark" nebula={{ ramp: 'swatch' }} />,
|
|
36
|
+
);
|
|
37
|
+
const sky = resolveTheme(interstellarTheme, { mode: 'dark' })['--wzl-swatch-sky'];
|
|
38
|
+
expect(backdrop(container)).toContain(sky);
|
|
39
|
+
});
|
|
40
|
+
});
|
package/src/lab/Lab.test.tsx
CHANGED
|
@@ -165,7 +165,9 @@ describe('<Lab>', () => {
|
|
|
165
165
|
storageKey: 's',
|
|
166
166
|
});
|
|
167
167
|
await waitFor(() => expect(labRef?.trials[0]).toBeDefined());
|
|
168
|
-
expect((labRef?.trials[0]?.state as { seen: Set<string> })
|
|
168
|
+
expect((labRef?.trials[0]?.state as { seen: Set<string> } | undefined)?.seen).toEqual(
|
|
169
|
+
new Set(['a']),
|
|
170
|
+
);
|
|
169
171
|
});
|
|
170
172
|
});
|
|
171
173
|
|
package/src/lab/Lab.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type ColorList, colorAt, colorCount, resolveTheme, type Theme } from '@weasel-js/theme';
|
|
2
|
+
import { ThemeProvider, useResolvedColorMode } from '@weasel-js/theme/react';
|
|
2
3
|
import {
|
|
3
4
|
type CSSProperties,
|
|
4
5
|
type ReactNode,
|
|
@@ -11,7 +12,7 @@ import {
|
|
|
11
12
|
} from 'react';
|
|
12
13
|
import { useStore } from 'zustand/react';
|
|
13
14
|
import { AnnotationPreloadContext } from '../annotations/preload';
|
|
14
|
-
import {
|
|
15
|
+
import { labAnnotationTools } from '../annotations/toolMap';
|
|
15
16
|
import {
|
|
16
17
|
LabAsideRegion,
|
|
17
18
|
LabFooterRegion,
|
|
@@ -54,7 +55,6 @@ import { LabPalette } from './LabPalette';
|
|
|
54
55
|
import { LabShell } from './LabShell';
|
|
55
56
|
import type { LabPage } from './LabSwitcher';
|
|
56
57
|
import { createPanelHostRegistry, PanelHostContext } from './panelHost';
|
|
57
|
-
import { useResolvedMode } from './useSystemMode';
|
|
58
58
|
import { type PanelDescriptor, type TrialLayout, Workspace } from './Workspace';
|
|
59
59
|
|
|
60
60
|
interface LabBaseProps {
|
|
@@ -68,15 +68,20 @@ interface LabBaseProps {
|
|
|
68
68
|
* window is the whole app, rather than a panel beside one, reads better at
|
|
69
69
|
* `'roomy'`. The trials' own contents are unaffected. */
|
|
70
70
|
density?: LabDensity;
|
|
71
|
+
/** The theme the lab's chrome resolves against, applied at `mode` and
|
|
72
|
+
* `density`. Default `interstellarTheme`; extend it rather than replace it to
|
|
73
|
+
* keep the lab's look while changing a few tokens. */
|
|
74
|
+
theme?: Theme;
|
|
71
75
|
/** Rendered while a stored lab loads. Default: the lab's empty shell. */
|
|
72
76
|
fallback?: ReactNode;
|
|
73
77
|
/**
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
+
* Colors composing the interstellar theme's cosmic backdrop. Each becomes
|
|
79
|
+
* one radial-gradient blob on the dark void base, in a fixed spread of
|
|
80
|
+
* positions; extras wrap around. A theme ramp resolves against the lab's
|
|
81
|
+
* theme; a function list gives as many blobs as there are positions.
|
|
82
|
+
* Ignored unless the resolved mode is dark.
|
|
78
83
|
*/
|
|
79
|
-
nebula?:
|
|
84
|
+
nebula?: ColorList;
|
|
80
85
|
title?: string;
|
|
81
86
|
/** The project's other labs. Given two or more, the title becomes the way to
|
|
82
87
|
* reach them — the same switcher `<LabShell>` renders, so a lab reached from
|
|
@@ -180,13 +185,14 @@ function LabFallback({
|
|
|
180
185
|
title,
|
|
181
186
|
mode,
|
|
182
187
|
density,
|
|
188
|
+
theme = interstellarTheme,
|
|
183
189
|
pages,
|
|
184
190
|
path,
|
|
185
|
-
}: Pick<LabBaseProps, 'title' | 'mode' | 'density' | 'pages' | 'path'>) {
|
|
186
|
-
const resolvedMode =
|
|
191
|
+
}: Pick<LabBaseProps, 'title' | 'mode' | 'density' | 'theme' | 'pages' | 'path'>) {
|
|
192
|
+
const resolvedMode = useResolvedColorMode(mode ?? 'auto');
|
|
187
193
|
return (
|
|
188
194
|
<ThemeProvider
|
|
189
|
-
theme={
|
|
195
|
+
theme={theme}
|
|
190
196
|
selection={{ mode: resolvedMode, density: density ?? 'comfortable' }}
|
|
191
197
|
className="lk-lab"
|
|
192
198
|
>
|
|
@@ -214,7 +220,15 @@ const NEBULA_SLOTS = [
|
|
|
214
220
|
{ cx: '85%', cy: '18%', sx: '55%', sy: '80%', stop: '70%' },
|
|
215
221
|
] as const;
|
|
216
222
|
|
|
217
|
-
function buildNebula(
|
|
223
|
+
function buildNebula(
|
|
224
|
+
list: ColorList,
|
|
225
|
+
theme: Theme,
|
|
226
|
+
mode: 'light' | 'dark',
|
|
227
|
+
density: string,
|
|
228
|
+
): string {
|
|
229
|
+
const ctx = { theme, resolved: resolveTheme(theme, { mode, density }) };
|
|
230
|
+
const n = colorCount(list, ctx) ?? NEBULA_SLOTS.length;
|
|
231
|
+
const colors = Array.from({ length: n }, (_, i) => colorAt(list, i, ctx));
|
|
218
232
|
const blobs = colors.map((c, i) => {
|
|
219
233
|
const p = NEBULA_SLOTS[i % NEBULA_SLOTS.length];
|
|
220
234
|
return `radial-gradient(ellipse ${p.sx} ${p.sy} at ${p.cx} ${p.cy}, color-mix(in srgb, ${c} 22%, transparent), transparent ${p.stop})`;
|
|
@@ -302,6 +316,7 @@ export function Lab(props: LabProps) {
|
|
|
302
316
|
title={props.title}
|
|
303
317
|
mode={props.mode}
|
|
304
318
|
density={props.density}
|
|
319
|
+
{...(props.theme ? { theme: props.theme } : {})}
|
|
305
320
|
pages={props.pages}
|
|
306
321
|
path={props.path}
|
|
307
322
|
/>
|
|
@@ -314,6 +329,7 @@ function LabRuntime({
|
|
|
314
329
|
instruments,
|
|
315
330
|
mode,
|
|
316
331
|
density,
|
|
332
|
+
theme = interstellarTheme,
|
|
317
333
|
nebula,
|
|
318
334
|
title,
|
|
319
335
|
pages,
|
|
@@ -455,17 +471,17 @@ function LabRuntime({
|
|
|
455
471
|
})),
|
|
456
472
|
[undockedPanels],
|
|
457
473
|
);
|
|
458
|
-
const resolvedMode =
|
|
474
|
+
const resolvedMode = useResolvedColorMode(modeValue);
|
|
459
475
|
|
|
460
476
|
// Tools and lab contributions share one id namespace, so they merge once
|
|
461
477
|
// here — before any region renders — and a collision throws rather than one
|
|
462
478
|
// of them silently losing.
|
|
463
479
|
// Declaring `annotations` on any instrument puts the drawing tools here, in
|
|
464
480
|
// the lab's rail: one tool is armed across every trial.
|
|
465
|
-
const
|
|
481
|
+
const annotationTools = useMemo(() => labAnnotationTools(instruments), [instruments]);
|
|
466
482
|
const labChromeAll = useMemo(
|
|
467
|
-
() => labContributions([...
|
|
468
|
-
[
|
|
483
|
+
() => labContributions([...annotationTools, ...(tools ?? [])], labChrome),
|
|
484
|
+
[annotationTools, tools, labChrome],
|
|
469
485
|
);
|
|
470
486
|
const hasFooterChrome = labChromeAll.some((c) => c.region === 'footer');
|
|
471
487
|
const hasPaneChrome = labChromeAll.some((c) => c.region === 'sidebar' || c.region === 'aside');
|
|
@@ -564,8 +580,15 @@ function LabRuntime({
|
|
|
564
580
|
// Only override the backdrop in dark, where there is one to override.
|
|
565
581
|
// Setting a CSS custom property is the sanctioned use of inline style.
|
|
566
582
|
const backdropStyle =
|
|
567
|
-
resolvedMode === 'dark' && nebula && nebula
|
|
568
|
-
? ({
|
|
583
|
+
resolvedMode === 'dark' && nebula && colorCount(nebula) !== 0
|
|
584
|
+
? ({
|
|
585
|
+
['--wzl-backdrop' as string]: buildNebula(
|
|
586
|
+
nebula,
|
|
587
|
+
theme,
|
|
588
|
+
resolvedMode,
|
|
589
|
+
density ?? 'comfortable',
|
|
590
|
+
),
|
|
591
|
+
} as CSSProperties)
|
|
569
592
|
: undefined;
|
|
570
593
|
|
|
571
594
|
const workspace = (
|
|
@@ -590,7 +613,7 @@ function LabRuntime({
|
|
|
590
613
|
<AnnotationPreloadContext.Provider value={opened.marks}>
|
|
591
614
|
<LabContext.Provider value={contextValue}>
|
|
592
615
|
<ThemeProvider
|
|
593
|
-
theme={
|
|
616
|
+
theme={theme}
|
|
594
617
|
selection={{ mode: resolvedMode, density: density ?? 'comfortable' }}
|
|
595
618
|
className="lk-lab"
|
|
596
619
|
style={backdropStyle}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { render } from '@testing-library/react';
|
|
2
|
+
import { ThemeProvider, useThemeOptional } from '@weasel-js/theme/react';
|
|
3
|
+
import { useContext } from 'react';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
import { LabStoreContext, TrialIdContext } from '../state/context';
|
|
6
|
+
import { PersistenceContext } from '../state/Persistence';
|
|
7
|
+
import { interstellarTheme } from '../theme/interstellar';
|
|
8
|
+
import { LabBoundary } from './LabBoundary';
|
|
9
|
+
import { LabContext, type LabContextValue } from './LabContext';
|
|
10
|
+
import { LabRoot } from './LabRoot';
|
|
11
|
+
|
|
12
|
+
function Probe() {
|
|
13
|
+
const lab = useContext(LabContext);
|
|
14
|
+
const store = useContext(LabStoreContext);
|
|
15
|
+
const trial = useContext(TrialIdContext);
|
|
16
|
+
const records = useContext(PersistenceContext);
|
|
17
|
+
const theme = useThemeOptional();
|
|
18
|
+
return (
|
|
19
|
+
<output>
|
|
20
|
+
{JSON.stringify({
|
|
21
|
+
lab: lab !== null,
|
|
22
|
+
store: store !== null,
|
|
23
|
+
trial,
|
|
24
|
+
records: records !== null,
|
|
25
|
+
theme: theme !== null,
|
|
26
|
+
})}
|
|
27
|
+
</output>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const fakeLab = { trials: [], instruments: [] } as unknown as LabContextValue;
|
|
32
|
+
|
|
33
|
+
describe('LabBoundary', () => {
|
|
34
|
+
it('hides the enclosing lab, trial, records and theme from its children', () => {
|
|
35
|
+
const { container } = render(
|
|
36
|
+
<ThemeProvider theme={interstellarTheme} selection={{ mode: 'dark' }}>
|
|
37
|
+
<LabContext.Provider value={fakeLab}>
|
|
38
|
+
<TrialIdContext.Provider value="t1">
|
|
39
|
+
<Probe />
|
|
40
|
+
<LabBoundary>
|
|
41
|
+
<Probe />
|
|
42
|
+
</LabBoundary>
|
|
43
|
+
</TrialIdContext.Provider>
|
|
44
|
+
</LabContext.Provider>
|
|
45
|
+
</ThemeProvider>,
|
|
46
|
+
);
|
|
47
|
+
const [outside, inside] = Array.from(container.querySelectorAll('output')).map((el) =>
|
|
48
|
+
JSON.parse(el.textContent ?? ''),
|
|
49
|
+
);
|
|
50
|
+
expect(outside).toEqual({ lab: true, store: false, trial: 't1', records: false, theme: true });
|
|
51
|
+
expect(inside).toEqual({ lab: false, store: false, trial: null, records: false, theme: false });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('lets a LabRoot under it apply its own theme instead of deferring to the one above', () => {
|
|
55
|
+
const { container } = render(
|
|
56
|
+
<ThemeProvider theme={interstellarTheme} selection={{ mode: 'dark' }}>
|
|
57
|
+
<LabBoundary>
|
|
58
|
+
<LabRoot mode="light">x</LabRoot>
|
|
59
|
+
</LabBoundary>
|
|
60
|
+
</ThemeProvider>,
|
|
61
|
+
);
|
|
62
|
+
const themed = container.querySelectorAll('[data-wzl-theme]');
|
|
63
|
+
expect(themed).toHaveLength(2);
|
|
64
|
+
expect(themed[1]?.getAttribute('data-wzl-mode')).toBe('light');
|
|
65
|
+
});
|
|
66
|
+
});
|