@weasel-js/labkit 1.6.1 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_dts/{CanvasStackContext-Wo6OwIVM.d.ts → CanvasStackContext-D2jnIwfk.d.ts} +1 -1
- package/dist/_dts/{LayerList.d-BoELAMsM.d.ts → LayerList.d-DiuEArKq.d.ts} +2 -2
- package/dist/_dts/PrefsRow.d-TOZ7BgD9.d.ts +79 -0
- package/dist/_dts/PropertyPanel.d-D-1paCbE.d.ts +141 -0
- package/dist/_dts/{Select.d-Cr8-zoup.d.ts → Select.d-Bensr_MT.d.ts} +11 -1
- package/dist/_dts/{Stage-DWBBi8IU.d.ts → Stage-CCWfQQWM.d.ts} +58 -18
- package/dist/_dts/index-B2Yj5aA0.d.ts +274 -0
- package/dist/_dts/{index-CKyX0ME_.d.ts → index-CsYqUHD3.d.ts} +2 -2
- package/dist/_dts/{types-DTuViBvc.d.ts → types-CRPSGFAp.d.ts} +13 -4
- package/dist/_dts/{types-B7yBTTF9.d.ts → types-CSm9jgol.d.ts} +2 -2
- package/dist/_dts/{useTrialState-CMaV6UD4.d.ts → useTrialState-CTZPgeZ7.d.ts} +1 -1
- package/dist/canvas/index.d.ts +6 -6
- package/dist/canvas/index.js +2 -2
- package/dist/chrome/index.d.ts +5 -5
- package/dist/chrome/index.js +8 -8
- package/dist/{chunk-QGMOA2YE.js → chunk-275JRRVD.js} +67 -70
- package/dist/chunk-275JRRVD.js.map +1 -0
- package/dist/{chunk-KHK5FGER.js → chunk-4LJSC7MB.js} +8 -10
- package/dist/chunk-4LJSC7MB.js.map +1 -0
- package/dist/{chunk-SLAQPUI4.js → chunk-7BO7HNGF.js} +5 -5
- package/dist/{chunk-SLAQPUI4.js.map → chunk-7BO7HNGF.js.map} +1 -1
- package/dist/chunk-CZZPXUOU.js +3 -0
- package/dist/{chunk-SBXNQW4G.js.map → chunk-CZZPXUOU.js.map} +1 -1
- package/dist/{chunk-6FLXL3I7.js → chunk-FMK7UQ6V.js} +19 -4
- package/dist/chunk-FMK7UQ6V.js.map +1 -0
- package/dist/{chunk-O6FFFOHS.js → chunk-FZ7YFQN7.js} +16 -8
- package/dist/chunk-FZ7YFQN7.js.map +1 -0
- package/dist/{chunk-GHGO52SO.js → chunk-GOZLNM4L.js} +5 -5
- package/dist/{chunk-GHGO52SO.js.map → chunk-GOZLNM4L.js.map} +1 -1
- package/dist/{chunk-4TT3PNPR.js → chunk-I5DUQ5HM.js} +33 -36
- package/dist/chunk-I5DUQ5HM.js.map +1 -0
- package/dist/{chunk-NEXR37YJ.js → chunk-J7OWWTCW.js} +4 -5
- package/dist/chunk-J7OWWTCW.js.map +1 -0
- package/dist/{chunk-IRTGUQ5O.js → chunk-KMJ7RWD3.js} +16 -6
- package/dist/chunk-KMJ7RWD3.js.map +1 -0
- package/dist/{chunk-S4NCI32V.js → chunk-M7HP3FTK.js} +4 -4
- package/dist/{chunk-S4NCI32V.js.map → chunk-M7HP3FTK.js.map} +1 -1
- package/dist/chunk-MIM5R4ZT.js +14972 -0
- package/dist/chunk-MIM5R4ZT.js.map +1 -0
- package/dist/{chunk-ASOK76ZH.js → chunk-T5IBSPAM.js} +59 -26
- package/dist/chunk-T5IBSPAM.js.map +1 -0
- package/dist/{chunk-DWMSB4CA.js → chunk-UNAZSLWE.js} +82 -16
- package/dist/chunk-UNAZSLWE.js.map +1 -0
- package/dist/{chunk-TJ77A4A3.js → chunk-Y23G2VZL.js} +5 -9
- package/dist/chunk-Y23G2VZL.js.map +1 -0
- package/dist/{chunk-HKV7LRHY.js → chunk-YT75VYRD.js} +4 -4
- package/dist/{chunk-HKV7LRHY.js.map → chunk-YT75VYRD.js.map} +1 -1
- package/dist/{chunk-CGHF6RZV.js → chunk-ZIFPHIWZ.js} +9 -7
- package/dist/chunk-ZIFPHIWZ.js.map +1 -0
- package/dist/config/index.d.ts +16 -9
- package/dist/config/index.js +4 -4
- package/dist/controls/index.d.ts +2 -2
- package/dist/controls/index.js +5 -5
- package/dist/dragdrop/index.d.ts +3 -3
- package/dist/index.d.ts +263 -24
- package/dist/index.js +376 -237
- 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 +4 -4
- package/dist/loupe/index.js +2 -2
- package/dist/overview/index.d.ts +2 -2
- package/dist/overview/index.js +9 -8
- package/dist/overview/index.js.map +1 -1
- package/dist/passthrough/weasel-ui.d.ts +208 -38
- package/dist/passthrough/weasel-ui.js +3 -3
- package/dist/primitives/index.d.ts +3 -178
- package/dist/primitives/index.js +6 -6
- package/dist/state/index.d.ts +5 -5
- package/dist/state/index.js +4 -4
- package/dist/styles.css +91 -83
- package/dist/undo/index.d.ts +3 -3
- package/package.json +13 -12
- package/src/annotations/AnnotationOverlay.tsx +2 -2
- package/src/annotations/Annotations.less +0 -10
- package/src/annotations/MarkList.tsx +4 -8
- package/src/annotations/paint.test.ts +3 -14
- package/src/annotations/paint.ts +3 -19
- package/src/annotations/svgNodes.test.ts +6 -9
- package/src/annotations/svgNodes.ts +3 -3
- package/src/canvas/AGENTS.md +1 -0
- package/src/canvas/CameraInput.tsx +44 -33
- package/src/canvas/CanvasStack.tsx +6 -1
- package/src/canvas/LinkedCursor.less +1 -1
- package/src/canvas/Stage.test.tsx +33 -4
- package/src/canvas/Stage.tsx +6 -1
- package/src/canvas/cameraRegistry.test.ts +55 -0
- package/src/canvas/cameraRegistry.ts +86 -0
- package/src/canvas/cameraZoom.test.ts +46 -0
- package/src/canvas/cameraZoom.ts +24 -0
- package/src/canvas/index.ts +14 -2
- package/src/canvas/sharedScope.test.tsx +94 -0
- package/src/chrome/ChromeRegions.stories.tsx +1 -0
- package/src/chrome/regions/PaletteRegion.less +0 -7
- package/src/chrome/regions/SidebarRegion.less +12 -2
- package/src/config/builder.test.ts +2 -1
- package/src/config/builder.ts +12 -5
- package/src/config/fromConfigField.test.ts +14 -0
- package/src/config/fromConfigField.ts +18 -4
- package/src/config/path.ts +2 -5
- package/src/config/resolve.ts +4 -0
- package/src/config/types.ts +13 -9
- package/src/controls/ControlPanel.less +1 -1
- package/src/controls/ControlPanel.stories.tsx +3 -2
- package/src/controls/ControlPanel.test.tsx +64 -11
- package/src/controls/ControlPanel.tsx +52 -2
- package/src/index.ts +15 -1
- package/src/lab/Lab.tsx +83 -72
- package/src/lab/{LabFit.stories.less → LabFit.browser.test.less} +1 -1
- package/src/lab/{LabFit.stories.tsx → LabFit.browser.test.tsx} +41 -63
- package/src/lab/LabFullChrome.stories.tsx +1 -0
- package/src/lab/LabHeader.test.tsx +24 -14
- package/src/lab/LabHeader.tsx +11 -12
- package/src/lab/LabShell.less +8 -17
- package/src/lab/LabSwitcher.less +6 -6
- package/src/lab/LabZoom.test.tsx +223 -0
- package/src/lab/LabZoom.tsx +170 -0
- package/src/lab/index.ts +1 -0
- package/src/loupe/LoupeGestures.tsx +1 -1
- package/src/overview/TrialOverview.tsx +7 -5
- package/src/passthrough/weasel-ui.test.ts +2 -0
- package/src/passthrough/weasel-ui.ts +16 -4
- package/src/primitives/FloatingPanel.test.tsx +11 -0
- package/src/primitives/FloatingPanel.tsx +2 -5
- package/src/primitives/JobProgress.less +1 -1
- package/src/primitives/Legend.stories.tsx +1 -0
- package/src/primitives/Readout.browser.test.tsx +137 -0
- package/src/primitives/Readout.less +12 -0
- package/src/primitives/Readout.stories.tsx +58 -0
- package/src/primitives/Readout.test.tsx +93 -0
- package/src/primitives/Readout.tsx +56 -0
- package/src/primitives/index.ts +2 -0
- package/src/specimen/Specimen.stories.tsx +1 -0
- package/src/specimen/Specimen.tsx +2 -2
- package/src/state/Persistence.browser.test.tsx +47 -0
- package/src/state/store.test.ts +5 -1
- package/src/state/usePersistedState.test.tsx +1 -1
- package/src/styles.less +1 -0
- package/src/theme/Interstellar.stories.tsx +3 -1
- package/src/theme/base.less +21 -47
- package/src/theme/interstellar.test.ts +1 -1
- package/src/theme/interstellar.theme.json +10 -3
- package/src/trial/Trial.less +0 -13
- package/src/trial/Trial.tsx +12 -6
- package/src/trial/TrialChrome.tsx +21 -3
- package/dist/_dts/PrefsRow.d-DFpPIeIC.d.ts +0 -39
- package/dist/_dts/PropertyField.d-D0ndfIIt.d.ts +0 -404
- package/dist/chunk-4TT3PNPR.js.map +0 -1
- package/dist/chunk-6FLXL3I7.js.map +0 -1
- package/dist/chunk-ASOK76ZH.js.map +0 -1
- package/dist/chunk-CGHF6RZV.js.map +0 -1
- package/dist/chunk-DWMSB4CA.js.map +0 -1
- package/dist/chunk-IRTGUQ5O.js.map +0 -1
- package/dist/chunk-KHK5FGER.js.map +0 -1
- package/dist/chunk-NEXR37YJ.js.map +0 -1
- package/dist/chunk-O6FFFOHS.js.map +0 -1
- package/dist/chunk-QGMOA2YE.js.map +0 -1
- package/dist/chunk-SBXNQW4G.js +0 -3
- package/dist/chunk-TJ77A4A3.js.map +0 -1
- package/dist/chunk-TYN6MLXX.js +0 -13864
- package/dist/chunk-TYN6MLXX.js.map +0 -1
- package/src/state/Persistence.stories.tsx +0 -75
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { act, fireEvent, render, screen, within } from '@testing-library/react';
|
|
2
2
|
import userEvent from '@testing-library/user-event';
|
|
3
3
|
import { TOOL_PREF_KINDS, type ToolPrefKind } from '@weasel-js/core';
|
|
4
|
+
import { compact } from '@weasel-js/quantity';
|
|
4
5
|
import type { PrefLeaf } from '@weasel-js/ui';
|
|
5
6
|
import { useState } from 'react';
|
|
6
7
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
@@ -88,7 +89,7 @@ describe('<ControlPanel> number', () => {
|
|
|
88
89
|
];
|
|
89
90
|
render(<ControlPanel fields={fields} config={{ n: 42 }} setConfig={vi.fn()} />);
|
|
90
91
|
const input = screen.getByLabelText('N') as HTMLInputElement;
|
|
91
|
-
expect(input
|
|
92
|
+
expect(input).toHaveAttribute('role', 'spinbutton');
|
|
92
93
|
expect(input.value).toBe('42');
|
|
93
94
|
});
|
|
94
95
|
|
|
@@ -143,7 +144,7 @@ describe('<ControlPanel> color', () => {
|
|
|
143
144
|
const input = screen.getByLabelText('Color') as HTMLInputElement;
|
|
144
145
|
expect(input.type).toBe('color');
|
|
145
146
|
expect(input.value).toBe('#ff0000');
|
|
146
|
-
fireEvent.
|
|
147
|
+
fireEvent.input(input, { target: { value: '#00ff00' } });
|
|
147
148
|
expect(setConfig).toHaveBeenCalledWith('c', '#00ff00');
|
|
148
149
|
});
|
|
149
150
|
});
|
|
@@ -206,7 +207,7 @@ describe('<ControlPanel> schema', () => {
|
|
|
206
207
|
[],
|
|
207
208
|
);
|
|
208
209
|
render(<ControlPanel schema={schema} config={{ a: 5, b: 5 }} setConfig={vi.fn()} />);
|
|
209
|
-
expect(screen.getByLabelText('B')).toHaveAttribute('
|
|
210
|
+
expect(screen.getByLabelText('B')).toHaveAttribute('role', 'spinbutton');
|
|
210
211
|
});
|
|
211
212
|
|
|
212
213
|
it('falls back to the leaf default when config holds no value', () => {
|
|
@@ -466,7 +467,7 @@ describe('<ControlPanel> suffix', () => {
|
|
|
466
467
|
|
|
467
468
|
it('suffixes a typed number with the suffix the leaf declares', () => {
|
|
468
469
|
const { container } = panel(f.number(20).input().suffix('px'));
|
|
469
|
-
expect(container.querySelector('input[
|
|
470
|
+
expect(container.querySelector('input[role="spinbutton"]')).not.toBeNull();
|
|
470
471
|
expect(container.textContent).toContain('px');
|
|
471
472
|
});
|
|
472
473
|
});
|
|
@@ -476,7 +477,7 @@ describe('<ControlPanel> format', () => {
|
|
|
476
477
|
render(
|
|
477
478
|
<ControlPanel
|
|
478
479
|
schema={resolveConfigSchema(
|
|
479
|
-
f.schema({ glyphs: f.number(0).range(0, 2_000_000).
|
|
480
|
+
f.schema({ glyphs: f.number(0).range(0, 2_000_000).display(compact()) }),
|
|
480
481
|
[],
|
|
481
482
|
)}
|
|
482
483
|
config={{ glyphs: 2_000_000 }}
|
|
@@ -898,6 +899,28 @@ describe('<ControlPanel> auto', () => {
|
|
|
898
899
|
});
|
|
899
900
|
});
|
|
900
901
|
|
|
902
|
+
describe('<ControlPanel> option icons', () => {
|
|
903
|
+
it("draws an option's icon in its segment and keeps the label as its name", () => {
|
|
904
|
+
const status = f
|
|
905
|
+
.enum('ok', [
|
|
906
|
+
{ value: 'ok', label: 'ok', icon: 'statusSuccess' },
|
|
907
|
+
{ value: 'bad', label: 'bad' },
|
|
908
|
+
])
|
|
909
|
+
.radio();
|
|
910
|
+
render(
|
|
911
|
+
<ControlPanel
|
|
912
|
+
schema={resolveConfigSchema(f.schema({ status }), [])}
|
|
913
|
+
config={{ status: 'ok' }}
|
|
914
|
+
setConfig={() => {}}
|
|
915
|
+
/>,
|
|
916
|
+
);
|
|
917
|
+
const ok = screen.getByRole('radio', { name: 'ok' });
|
|
918
|
+
expect(ok.querySelector('svg')).not.toBeNull();
|
|
919
|
+
expect(ok.textContent).toBe('');
|
|
920
|
+
expect(screen.getByRole('radio', { name: 'bad' }).textContent).toBe('bad');
|
|
921
|
+
});
|
|
922
|
+
});
|
|
923
|
+
|
|
901
924
|
describe('<ControlPanel> pair', () => {
|
|
902
925
|
const paired = () =>
|
|
903
926
|
resolveConfigSchema(
|
|
@@ -913,8 +936,8 @@ describe('<ControlPanel> pair', () => {
|
|
|
913
936
|
it('draws two leaves sharing a pair id as one row named by the pair', () => {
|
|
914
937
|
render(<ControlPanel schema={paired()} config={config} setConfig={vi.fn()} />);
|
|
915
938
|
const row = screen.getByText('Offset').closest('div') as HTMLElement;
|
|
916
|
-
expect(within(row).getByLabelText('X')).toHaveValue(10);
|
|
917
|
-
expect(within(row).getByLabelText('Y')).toHaveValue(20);
|
|
939
|
+
expect(within(row).getByLabelText('X')).toHaveValue('10');
|
|
940
|
+
expect(within(row).getByLabelText('Y')).toHaveValue('20');
|
|
918
941
|
});
|
|
919
942
|
|
|
920
943
|
it('captions each cell of a pair with its own name, so a paired switch is not left unnamed', () => {
|
|
@@ -952,7 +975,7 @@ describe('<ControlPanel> pair', () => {
|
|
|
952
975
|
render(<ControlPanel schema={paired()} config={config} setConfig={vi.fn()} />);
|
|
953
976
|
const row = screen.getByText('Offset').closest('div') as HTMLElement;
|
|
954
977
|
expect(within(row).queryByLabelText('Scale')).toBeNull();
|
|
955
|
-
expect(screen.getByLabelText('Scale')).toHaveValue(1);
|
|
978
|
+
expect(screen.getByLabelText('Scale')).toHaveValue('1');
|
|
956
979
|
});
|
|
957
980
|
});
|
|
958
981
|
|
|
@@ -1045,18 +1068,18 @@ describe('<ControlPanel> color alpha', () => {
|
|
|
1045
1068
|
it('splits the stored hex into a swatch and an alpha track', () => {
|
|
1046
1069
|
panel();
|
|
1047
1070
|
expect((screen.getByLabelText('Tint') as HTMLInputElement).value).toBe('#336699');
|
|
1048
|
-
expect((screen.getByLabelText('Tint opacity') as HTMLInputElement).value).toBe('
|
|
1071
|
+
expect((screen.getByLabelText('Tint opacity') as HTMLInputElement).value).toBe('80');
|
|
1049
1072
|
});
|
|
1050
1073
|
|
|
1051
1074
|
it('keeps the stored alpha when the swatch moves', () => {
|
|
1052
1075
|
const setConfig = panel();
|
|
1053
|
-
fireEvent.
|
|
1076
|
+
fireEvent.input(screen.getByLabelText('Tint'), { target: { value: '#00ff00' } });
|
|
1054
1077
|
expect(setConfig).toHaveBeenCalledWith('tint', '#00ff00cc');
|
|
1055
1078
|
});
|
|
1056
1079
|
|
|
1057
1080
|
it('keeps the stored color when the alpha track moves', () => {
|
|
1058
1081
|
const setConfig = panel();
|
|
1059
|
-
fireEvent.
|
|
1082
|
+
fireEvent.input(screen.getByLabelText('Tint opacity'), { target: { value: '50' } });
|
|
1060
1083
|
expect(setConfig).toHaveBeenCalledWith('tint', '#33669980');
|
|
1061
1084
|
});
|
|
1062
1085
|
|
|
@@ -1101,3 +1124,33 @@ describe('<ControlPanel> title, stance and tone', () => {
|
|
|
1101
1124
|
expect(panel.querySelector('.lk-control-panel')).not.toBeNull();
|
|
1102
1125
|
});
|
|
1103
1126
|
});
|
|
1127
|
+
|
|
1128
|
+
describe('<ControlPanel> resettable group', () => {
|
|
1129
|
+
const schema = resolveConfigSchema(
|
|
1130
|
+
f.schema({
|
|
1131
|
+
name: f.string('a'),
|
|
1132
|
+
modes: f.group({ one: f.number(0), two: f.number(0), inner: f.group({ three: f.number(1) }) }).resettable(),
|
|
1133
|
+
plain: f.group({ four: f.number(0) }),
|
|
1134
|
+
}),
|
|
1135
|
+
);
|
|
1136
|
+
|
|
1137
|
+
it('puts a reset button on the group heading, disabled while nothing has changed', () => {
|
|
1138
|
+
render(
|
|
1139
|
+
<ControlPanel schema={schema} config={{ name: 'a', modes: { one: 0, two: 0, inner: { three: 1 } }, plain: { four: 0 } }} setConfig={vi.fn()} />,
|
|
1140
|
+
);
|
|
1141
|
+
expect(screen.getByRole('button', { name: 'Reset Modes' })).toBeDisabled();
|
|
1142
|
+
expect(screen.queryByRole('button', { name: 'Reset Plain' })).toBeNull();
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
it('writes the default of every changed value beneath the group and nothing else', () => {
|
|
1146
|
+
const setConfig = vi.fn();
|
|
1147
|
+
render(
|
|
1148
|
+
<ControlPanel schema={schema} config={{ name: 'b', modes: { one: 2, two: 0, inner: { three: 5 } }, plain: { four: 3 } }} setConfig={setConfig} />,
|
|
1149
|
+
);
|
|
1150
|
+
fireEvent.click(screen.getByRole('button', { name: 'Reset Modes' }));
|
|
1151
|
+
expect(setConfig.mock.calls).toEqual([
|
|
1152
|
+
['modes.one', 0],
|
|
1153
|
+
['modes.inner.three', 1],
|
|
1154
|
+
]);
|
|
1155
|
+
});
|
|
1156
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getAlpha01, isBuiltinToolPref, toHex8, withAlpha01 } from '@weasel-js/core';
|
|
2
2
|
import {
|
|
3
|
+
Button,
|
|
3
4
|
DialogRow,
|
|
4
5
|
isPrefLeaf,
|
|
5
6
|
ListEditor,
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
PropertyRow,
|
|
18
19
|
type PropertyRowLayout,
|
|
19
20
|
prefFieldProps,
|
|
21
|
+
ResetIcon,
|
|
20
22
|
type StanceProps,
|
|
21
23
|
} from '@weasel-js/ui';
|
|
22
24
|
import { Fragment, type ReactNode, useEffect, useMemo, useRef, useState } from 'react';
|
|
@@ -117,6 +119,45 @@ export interface ControlPanelProps<TC extends Record<string, unknown>> extends S
|
|
|
117
119
|
|
|
118
120
|
const NO_AUTO: ReadonlySet<string> = new Set();
|
|
119
121
|
|
|
122
|
+
/** Every leaf beneath a group, as its dotted path and its default. */
|
|
123
|
+
function leavesUnder(group: PrefGroup, at: string): [string, unknown][] {
|
|
124
|
+
return Object.entries(group.children).flatMap(([key, child]) => {
|
|
125
|
+
const path = at === '' ? key : `${at}.${key}`;
|
|
126
|
+
return isPrefLeaf(child) ? [[path, child.default] as [string, unknown]] : leavesUnder(child, path);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** The heading button of a `.resettable()` group: writes each changed leaf's
|
|
131
|
+
* default, and is disabled while nothing has changed. */
|
|
132
|
+
function ResetGroup({
|
|
133
|
+
group,
|
|
134
|
+
path,
|
|
135
|
+
config,
|
|
136
|
+
setConfig,
|
|
137
|
+
}: {
|
|
138
|
+
group: PrefGroup;
|
|
139
|
+
path: string;
|
|
140
|
+
config: Record<string, unknown>;
|
|
141
|
+
setConfig: (path: string, value: unknown) => void;
|
|
142
|
+
}) {
|
|
143
|
+
const changed = leavesUnder(group, path).filter(([p, d]) => !Object.is(valueAtPath(config, p), d));
|
|
144
|
+
return (
|
|
145
|
+
<Button
|
|
146
|
+
iconOnly
|
|
147
|
+
variant="ghost"
|
|
148
|
+
size="sm"
|
|
149
|
+
ariaLabel={`Reset ${group.name}`}
|
|
150
|
+
tooltip={`Reset ${group.name} to defaults`}
|
|
151
|
+
disabled={changed.length === 0}
|
|
152
|
+
onClick={() => {
|
|
153
|
+
for (const [p, d] of changed) setConfig(p, d);
|
|
154
|
+
}}
|
|
155
|
+
>
|
|
156
|
+
<ResetIcon />
|
|
157
|
+
</Button>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
120
161
|
/** Render an instrument's config schema as a stack of controls, each writing
|
|
121
162
|
* back through `setConfig`. Built on the property rows, so a lab's controls
|
|
122
163
|
* are the same aligned, themed rows the rest of the kit uses. */
|
|
@@ -224,6 +265,11 @@ export function ControlPanel<TC extends Record<string, unknown>>({
|
|
|
224
265
|
// two-column grid leaves it laying its own rows out inside a cell,
|
|
225
266
|
// which overlaps them.
|
|
226
267
|
span
|
|
268
|
+
actions={
|
|
269
|
+
resolved.resettable?.has(path) ? (
|
|
270
|
+
<ResetGroup group={found} path={path} config={config as Record<string, unknown>} setConfig={setConfig} />
|
|
271
|
+
) : undefined
|
|
272
|
+
}
|
|
227
273
|
{...fold(path, undefined, rows.grid)}
|
|
228
274
|
>
|
|
229
275
|
{body(found, path, rows)}
|
|
@@ -461,7 +507,7 @@ function ControlRow<TC extends Record<string, unknown>>({
|
|
|
461
507
|
case 'number':
|
|
462
508
|
return <PropertyField {...field} {...row} span={field.control === 'slider' && wide} />;
|
|
463
509
|
case 'enum':
|
|
464
|
-
return <PropertyField {...field} {...row} span={field.control === '
|
|
510
|
+
return <PropertyField {...field} {...row} span={field.control === 'toggle' && wide} />;
|
|
465
511
|
default:
|
|
466
512
|
return <PropertyField {...field} {...row} />;
|
|
467
513
|
}
|
|
@@ -496,7 +542,11 @@ function labField(
|
|
|
496
542
|
control: extra<string>(leaf, 'control') === 'switch' ? 'switch' : 'checkbox',
|
|
497
543
|
};
|
|
498
544
|
case 'enum':
|
|
499
|
-
|
|
545
|
+
// `.radio()` asks for every option at once, as segments.
|
|
546
|
+
return {
|
|
547
|
+
...field,
|
|
548
|
+
control: extra<string>(leaf, 'control') === 'radio' ? 'toggle' : 'select',
|
|
549
|
+
};
|
|
500
550
|
case 'string':
|
|
501
551
|
return {
|
|
502
552
|
...field,
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The property-panel family, LayerList, Input, Select, TokenPanel, ToggleBar and Button live in
|
|
1
|
+
// The property-panel family, DetailList, 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.
|
|
@@ -10,6 +10,11 @@ export type {
|
|
|
10
10
|
ButtonVariant,
|
|
11
11
|
CurveFieldProps,
|
|
12
12
|
CurveMark,
|
|
13
|
+
DetailListLayout,
|
|
14
|
+
DetailListProps,
|
|
15
|
+
DetailListValues,
|
|
16
|
+
DetailRowProps,
|
|
17
|
+
DetailRowStatus,
|
|
13
18
|
IconProps,
|
|
14
19
|
InputProps,
|
|
15
20
|
JogProps,
|
|
@@ -41,6 +46,8 @@ export {
|
|
|
41
46
|
CloseIcon,
|
|
42
47
|
CollapseIcon,
|
|
43
48
|
CurveField,
|
|
49
|
+
DetailList,
|
|
50
|
+
DetailRow,
|
|
44
51
|
ExpandIcon,
|
|
45
52
|
Icon,
|
|
46
53
|
Input,
|
|
@@ -131,6 +138,8 @@ export type { ContentSize, PaneSize } from './annotations/view';
|
|
|
131
138
|
export { fitView, fromWeaselView, toWeaselView } from './annotations/view';
|
|
132
139
|
export type {
|
|
133
140
|
CameraContextValue,
|
|
141
|
+
CameraRegistry,
|
|
142
|
+
CameraView,
|
|
134
143
|
CanvasLayerDescriptor,
|
|
135
144
|
CanvasStackContextValue,
|
|
136
145
|
CanvasStackProps,
|
|
@@ -140,20 +149,25 @@ export type {
|
|
|
140
149
|
WorldFrame,
|
|
141
150
|
WorldSpec,
|
|
142
151
|
ZoomAtOptions,
|
|
152
|
+
ZoomRange,
|
|
143
153
|
} from './canvas';
|
|
144
154
|
export {
|
|
145
155
|
applyCamera,
|
|
146
156
|
CameraContext,
|
|
147
157
|
CameraInput,
|
|
158
|
+
CameraPublishContext,
|
|
159
|
+
CameraRegistryContext,
|
|
148
160
|
CameraScope,
|
|
149
161
|
CanvasStack,
|
|
150
162
|
CanvasStackContext,
|
|
151
163
|
centerOn,
|
|
164
|
+
createCameraRegistry,
|
|
152
165
|
DEFAULT_FRAME,
|
|
153
166
|
fitStage,
|
|
154
167
|
resolveFrame,
|
|
155
168
|
Stage,
|
|
156
169
|
screenToWorld,
|
|
170
|
+
usePublishCamera,
|
|
157
171
|
worldToScreen,
|
|
158
172
|
zoomAt,
|
|
159
173
|
} from './canvas';
|
package/src/lab/Lab.tsx
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { useStore } from 'zustand/react';
|
|
14
14
|
import { AnnotationPreloadContext } from '../annotations/preload';
|
|
15
15
|
import { labAnnotationTools } from '../annotations/toolMap';
|
|
16
|
+
import { CameraRegistryContext, createCameraRegistry } from '../canvas/cameraRegistry';
|
|
16
17
|
import {
|
|
17
18
|
LabAsideRegion,
|
|
18
19
|
LabFooterRegion,
|
|
@@ -50,10 +51,11 @@ import {
|
|
|
50
51
|
} from '../trial/trialOps';
|
|
51
52
|
import { useLabFitWarning } from './fitCheck';
|
|
52
53
|
import { LabContext, type LabContextValue } from './LabContext';
|
|
53
|
-
import { LabHeader } from './LabHeader';
|
|
54
|
+
import { LabHeader, LabThemeSwitcher } from './LabHeader';
|
|
54
55
|
import { LabPalette } from './LabPalette';
|
|
55
56
|
import { LabShell } from './LabShell';
|
|
56
57
|
import type { LabPage } from './LabSwitcher';
|
|
58
|
+
import { LabZoom } from './LabZoom';
|
|
57
59
|
import { createPanelHostRegistry, PanelHostContext } from './panelHost';
|
|
58
60
|
import { type PanelDescriptor, type TrialLayout, Workspace } from './Workspace';
|
|
59
61
|
|
|
@@ -104,6 +106,9 @@ interface LabBaseProps {
|
|
|
104
106
|
/** Offer the header's add-trial control. Default `true`; a lab that opens
|
|
105
107
|
* trials some other way passes `false`. */
|
|
106
108
|
addTrial?: boolean;
|
|
109
|
+
/** Offer the header's zoom controls, which act on the focused trial's camera
|
|
110
|
+
* and answer Mod+=, Mod+- and Mod+0. Default `true`. */
|
|
111
|
+
zoom?: boolean;
|
|
107
112
|
/** Tools offered lab-wide. A trial whose instrument declares none of its own
|
|
108
113
|
* reflects and writes this slot. */
|
|
109
114
|
tools?: readonly TrialTool[];
|
|
@@ -339,6 +344,7 @@ function LabRuntime({
|
|
|
339
344
|
labChrome,
|
|
340
345
|
suppress,
|
|
341
346
|
addTrial,
|
|
347
|
+
zoom = true,
|
|
342
348
|
tools,
|
|
343
349
|
configRules,
|
|
344
350
|
controls,
|
|
@@ -354,6 +360,7 @@ function LabRuntime({
|
|
|
354
360
|
}, [instruments, store]);
|
|
355
361
|
|
|
356
362
|
const trials = useStore(store, (s) => s.trials);
|
|
363
|
+
const [cameras] = useState(createCameraRegistry);
|
|
357
364
|
const [focusPick, setFocusPick] = useState<string | null>(null);
|
|
358
365
|
const focusedTrialId = trials.some((t) => t.id === focusPick)
|
|
359
366
|
? focusPick
|
|
@@ -612,79 +619,83 @@ function LabRuntime({
|
|
|
612
619
|
<PersistenceContext.Provider value={opened.records}>
|
|
613
620
|
<AnnotationPreloadContext.Provider value={opened.marks}>
|
|
614
621
|
<LabContext.Provider value={contextValue}>
|
|
615
|
-
<
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
622
|
+
<CameraRegistryContext.Provider value={cameras}>
|
|
623
|
+
<ThemeProvider
|
|
624
|
+
theme={theme}
|
|
625
|
+
selection={{ mode: resolvedMode, density: density ?? 'comfortable' }}
|
|
626
|
+
className="lk-lab"
|
|
627
|
+
style={backdropStyle}
|
|
628
|
+
>
|
|
629
|
+
<LabShell
|
|
630
|
+
title={title ?? 'Labkit'}
|
|
631
|
+
mode={modeValue}
|
|
632
|
+
{...(pages ? { pages } : {})}
|
|
633
|
+
{...(path !== undefined ? { path } : {})}
|
|
634
|
+
footer={
|
|
635
|
+
hasFooterChrome ? (
|
|
636
|
+
<>
|
|
637
|
+
{footer}
|
|
638
|
+
<LabFooterRegion contributions={labChromeAll} />
|
|
639
|
+
</>
|
|
640
|
+
) : (
|
|
641
|
+
footer
|
|
642
|
+
)
|
|
643
|
+
}
|
|
644
|
+
header={
|
|
628
645
|
<>
|
|
629
|
-
{
|
|
630
|
-
<
|
|
646
|
+
<LabHeader {...(addTrial !== undefined ? { addTrial } : {})} />
|
|
647
|
+
{zoom ? <LabZoom /> : null}
|
|
648
|
+
{children}
|
|
649
|
+
<LabHeaderRegion contributions={labChromeAll} />
|
|
650
|
+
<LabThemeSwitcher />
|
|
631
651
|
</>
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
<
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
{workspace}
|
|
680
|
-
</>
|
|
681
|
-
)}
|
|
682
|
-
</div>
|
|
683
|
-
</SurfaceCanvasContext.Provider>
|
|
684
|
-
</SurfaceContext.Provider>
|
|
685
|
-
</PanelHostContext.Provider>
|
|
686
|
-
</LabShell>
|
|
687
|
-
</ThemeProvider>
|
|
652
|
+
}
|
|
653
|
+
>
|
|
654
|
+
<PanelHostContext.Provider value={panelHostsRef.current}>
|
|
655
|
+
<SurfaceContext.Provider value={surface}>
|
|
656
|
+
<SurfaceCanvasContext.Provider value={surfaceCanvases}>
|
|
657
|
+
<div className="lk-lab__body" ref={labBodyRef}>
|
|
658
|
+
{outerSurface ? null : (
|
|
659
|
+
// Two buffers stacked around the trials, both inert —
|
|
660
|
+
// each tile takes input from its own box. A tile's marks
|
|
661
|
+
// annotate the instrument's DOM from over it; an opaque
|
|
662
|
+
// renderer sits under it, so the pane can still hold a
|
|
663
|
+
// label. The under one is first so it paints first.
|
|
664
|
+
<>
|
|
665
|
+
<canvas
|
|
666
|
+
className="lk-lab__surface lk-lab__surface--under"
|
|
667
|
+
ref={(el) => {
|
|
668
|
+
ownUnderRef.current = el;
|
|
669
|
+
setOwnUnder(el);
|
|
670
|
+
}}
|
|
671
|
+
/>
|
|
672
|
+
<canvas
|
|
673
|
+
className="lk-lab__surface lk-lab__surface--over"
|
|
674
|
+
ref={(el) => {
|
|
675
|
+
ownOverRef.current = el;
|
|
676
|
+
setOwnOver(el);
|
|
677
|
+
}}
|
|
678
|
+
/>
|
|
679
|
+
</>
|
|
680
|
+
)}
|
|
681
|
+
{hasPaneChrome ? (
|
|
682
|
+
<LabPanes contributions={labChromeAll}>
|
|
683
|
+
<LabPalette contributions={labChromeAll} />
|
|
684
|
+
{workspace}
|
|
685
|
+
</LabPanes>
|
|
686
|
+
) : (
|
|
687
|
+
<>
|
|
688
|
+
<LabPalette contributions={labChromeAll} />
|
|
689
|
+
{workspace}
|
|
690
|
+
</>
|
|
691
|
+
)}
|
|
692
|
+
</div>
|
|
693
|
+
</SurfaceCanvasContext.Provider>
|
|
694
|
+
</SurfaceContext.Provider>
|
|
695
|
+
</PanelHostContext.Provider>
|
|
696
|
+
</LabShell>
|
|
697
|
+
</ThemeProvider>
|
|
698
|
+
</CameraRegistryContext.Provider>
|
|
688
699
|
</LabContext.Provider>
|
|
689
700
|
</AnnotationPreloadContext.Provider>
|
|
690
701
|
</PersistenceContext.Provider>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Host pages for LabFit.
|
|
1
|
+
// Host pages for LabFit.browser.test. Each is scoped by its own class, so a test
|
|
2
2
|
// that mounts no fixture leaves the page untouched.
|
|
3
3
|
|
|
4
4
|
// The mount every example documents: `html, body, #root { margin: 0; height: 100% }`.
|
|
@@ -1,15 +1,21 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import '@weasel-js/theme/tokens.css';
|
|
2
|
+
import 'windease/styles.css';
|
|
3
|
+
import '../styles.less';
|
|
4
|
+
import './LabFit.browser.test.less';
|
|
5
|
+
import { cleanup, render, screen, waitFor } from '@testing-library/react';
|
|
3
6
|
import { type ReactNode, useLayoutEffect, useState } from 'react';
|
|
4
7
|
import { createPortal } from 'react-dom';
|
|
8
|
+
import { afterEach, expect, test } from 'vitest';
|
|
9
|
+
import { page, userEvent } from 'vitest/browser';
|
|
5
10
|
import type { LabContribution } from '../chrome/labTypes';
|
|
6
11
|
import type { TrialContribution } from '../chrome/types';
|
|
7
12
|
import type { Instrument } from '../instrument/types';
|
|
8
13
|
import { Lab } from './Lab';
|
|
9
|
-
import './LabFit.stories.less';
|
|
10
14
|
|
|
11
|
-
// A lab must fit whatever it is mounted in without scrolling
|
|
12
|
-
// layout
|
|
15
|
+
// A lab must fit whatever it is mounted in without scrolling, which only real
|
|
16
|
+
// layout can show.
|
|
17
|
+
|
|
18
|
+
afterEach(cleanup);
|
|
13
19
|
|
|
14
20
|
const Stub: Instrument = {
|
|
15
21
|
name: 'Stub',
|
|
@@ -158,62 +164,34 @@ function fitProblems(fixture: Fixture, viewport: keyof typeof VIEWPORTS): string
|
|
|
158
164
|
return problems;
|
|
159
165
|
}
|
|
160
166
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
);
|
|
187
|
-
await expect(fitProblems(fixture, viewport)).toEqual([]);
|
|
167
|
+
const FIXTURES: Fixture[] = ['fullscreen', 'floating', 'wrapped', 'framed', 'sidebar'];
|
|
168
|
+
const CASES = FIXTURES.flatMap((fixture) =>
|
|
169
|
+
(Object.keys(VIEWPORTS) as (keyof typeof VIEWPORTS)[]).map((viewport) => ({ fixture, viewport })),
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
test.each(CASES)('$fixture fits a $viewport viewport', async ({ fixture, viewport }) => {
|
|
173
|
+
const { width, height } = VIEWPORTS[viewport].styles;
|
|
174
|
+
await page.viewport(Number.parseInt(width, 10), Number.parseInt(height, 10));
|
|
175
|
+
render(
|
|
176
|
+
<BodyHost host={HOST[fixture]}>
|
|
177
|
+
<Lab
|
|
178
|
+
title={`Fit: ${fixture}`}
|
|
179
|
+
instruments={[Stub]}
|
|
180
|
+
defaultInstrument="Stub"
|
|
181
|
+
chrome={fixture === 'floating' ? [floater] : undefined}
|
|
182
|
+
labChrome={fixture === 'sidebar' ? [sidebarTree] : undefined}
|
|
183
|
+
/>
|
|
184
|
+
</BodyHost>,
|
|
185
|
+
);
|
|
186
|
+
if (fixture === 'floating') {
|
|
187
|
+
await userEvent.click(await screen.findByRole('button', { name: 'Undock Floater' }));
|
|
188
|
+
}
|
|
189
|
+
await waitFor(
|
|
190
|
+
() => {
|
|
191
|
+
const problems = fitProblems(fixture, viewport);
|
|
192
|
+
if (problems.length > 0) throw new Error(problems.join('; '));
|
|
188
193
|
},
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
title: 'labkit/Lab/Fit',
|
|
194
|
-
component: Lab,
|
|
195
|
-
parameters: { layout: 'fullscreen', viewport: { options: VIEWPORTS } },
|
|
196
|
-
};
|
|
197
|
-
export default meta;
|
|
198
|
-
|
|
199
|
-
type Story = StoryObj<typeof Lab>;
|
|
200
|
-
|
|
201
|
-
export const FullscreenWide = fit('fullscreen', 'wide');
|
|
202
|
-
export const FullscreenMedium = fit('fullscreen', 'medium');
|
|
203
|
-
export const FullscreenNarrow = fit('fullscreen', 'narrow');
|
|
204
|
-
|
|
205
|
-
export const FloatingPanelWide = fit('floating', 'wide');
|
|
206
|
-
export const FloatingPanelMedium = fit('floating', 'medium');
|
|
207
|
-
export const FloatingPanelNarrow = fit('floating', 'narrow');
|
|
208
|
-
|
|
209
|
-
export const WrappedNoResetWide = fit('wrapped', 'wide');
|
|
210
|
-
export const WrappedNoResetMedium = fit('wrapped', 'medium');
|
|
211
|
-
export const WrappedNoResetNarrow = fit('wrapped', 'narrow');
|
|
212
|
-
|
|
213
|
-
export const FramedWide = fit('framed', 'wide');
|
|
214
|
-
export const FramedMedium = fit('framed', 'medium');
|
|
215
|
-
export const FramedNarrow = fit('framed', 'narrow');
|
|
216
|
-
|
|
217
|
-
export const SidebarWide = fit('sidebar', 'wide');
|
|
218
|
-
export const SidebarMedium = fit('sidebar', 'medium');
|
|
219
|
-
export const SidebarNarrow = fit('sidebar', 'narrow');
|
|
194
|
+
{ timeout: 2000 },
|
|
195
|
+
);
|
|
196
|
+
expect(fitProblems(fixture, viewport)).toEqual([]);
|
|
197
|
+
});
|
|
@@ -252,6 +252,7 @@ const runLog = defineInstrument<{ lines: string[] }, Record<string, never>>({
|
|
|
252
252
|
const meta: Meta<typeof Lab> = {
|
|
253
253
|
title: 'labkit/Lab/FullChrome',
|
|
254
254
|
component: Lab,
|
|
255
|
+
tags: ['gallery'],
|
|
255
256
|
parameters: { layout: 'fullscreen' },
|
|
256
257
|
};
|
|
257
258
|
export default meta;
|