@weasel-js/labkit 1.5.1 → 1.6.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/README.md +3 -7
- package/dist/_dts/{CanvasStackContext-CJNMS6mz.d.ts → CanvasStackContext-DQgcH1ny.d.ts} +1 -1
- package/dist/_dts/LayerList.d-2c9yup5Y.d.ts +83 -0
- package/dist/_dts/{PrefsForm.d-DPy9I8It.d.ts → PrefsRow.d-DFpPIeIC.d.ts} +16 -1
- package/dist/_dts/{Select.d-Gwr8fLpK.d.ts → Select.d-BchGMC2Y.d.ts} +129 -12
- package/dist/_dts/{frac-B4okC2lX.d.ts → frac-DTRq7Xpb.d.ts} +31 -13
- package/dist/_dts/inDialog-D5ZKGjiD.d.ts +23 -0
- package/dist/_dts/{index-CSJoj4LC.d.ts → index-B0--8PEM.d.ts} +5 -4
- package/dist/_dts/index-DINmCuLA.d.ts +453 -0
- package/dist/_dts/number.d-OeQYQoUO.d.ts +28 -0
- package/dist/_dts/{types-DzoNY1pO.d.ts → types-B7y8hX1D.d.ts} +6 -6
- package/dist/_dts/{usePanZoom-Pzf9f2jh.d.ts → usePanZoom-Bq73pwFe.d.ts} +4 -47
- package/dist/_dts/{useTrialState-uLlmaccn.d.ts → useTrialState-CP4jfuKl.d.ts} +1 -1
- package/dist/canvas/index.d.ts +6 -5
- package/dist/canvas/index.js +1 -1
- package/dist/chrome/index.d.ts +5 -4
- package/dist/chrome/index.js +7 -7
- package/dist/chunk-2RVCB2PT.js +3 -0
- package/dist/chunk-2RVCB2PT.js.map +1 -0
- package/dist/{chunk-UN4U4P63.js → chunk-3FI7Y4R4.js} +9 -9
- package/dist/chunk-3FI7Y4R4.js.map +1 -0
- package/dist/{chunk-7PFH3CNM.js → chunk-AWH3SRPW.js} +31 -5
- package/dist/chunk-AWH3SRPW.js.map +1 -0
- package/dist/{chunk-ZDHY47EV.js → chunk-BVL7Y574.js} +25 -9
- package/dist/chunk-BVL7Y574.js.map +1 -0
- package/dist/{chunk-IHUVQ3TX.js → chunk-H5VIV7MQ.js} +31 -27
- package/dist/chunk-H5VIV7MQ.js.map +1 -0
- package/dist/chunk-HFRE4QQV.js +400 -0
- package/dist/chunk-HFRE4QQV.js.map +1 -0
- package/dist/{chunk-IXTQI7JX.js → chunk-MOLIUNVH.js} +4 -4
- package/dist/{chunk-IXTQI7JX.js.map → chunk-MOLIUNVH.js.map} +1 -1
- package/dist/{chunk-CEIUBPPW.js → chunk-TT5C5S6R.js} +3 -86
- package/dist/chunk-TT5C5S6R.js.map +1 -0
- package/dist/chunk-USO6TERP.js +90 -0
- package/dist/chunk-USO6TERP.js.map +1 -0
- package/dist/{chunk-PHK4W562.js → chunk-VQ47KWD5.js} +3 -3
- package/dist/{chunk-PHK4W562.js.map → chunk-VQ47KWD5.js.map} +1 -1
- package/dist/chunk-WUJXJC5O.js +14568 -0
- package/dist/chunk-WUJXJC5O.js.map +1 -0
- package/dist/{chunk-EHGVROVV.js → chunk-ZORVNOKX.js} +6 -25
- package/dist/chunk-ZORVNOKX.js.map +1 -0
- package/dist/config/index.d.ts +53 -11
- package/dist/config/index.js +5 -5
- package/dist/controls/index.d.ts +6 -3
- package/dist/controls/index.js +4 -4
- package/dist/dragdrop/index.d.ts +4 -3
- package/dist/index.d.ts +139 -114
- package/dist/index.js +470 -479
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +6 -35
- package/dist/layers/index.js +2 -3
- package/dist/loupe/index.d.ts +6 -5
- package/dist/passthrough/weasel-ui.d.ts +421 -215
- package/dist/passthrough/weasel-ui.js +1 -1
- package/dist/primitives/index.d.ts +2 -13
- package/dist/primitives/index.js +4 -4
- package/dist/state/index.d.ts +6 -5
- package/dist/state/index.js +5 -5
- package/dist/styles.css +410 -190
- package/dist/undo/index.d.ts +4 -3
- package/package.json +9 -13
- package/src/annotations/frac.test.ts +7 -0
- package/src/annotations/paint.test.ts +21 -0
- package/src/annotations/paint.ts +12 -0
- package/src/annotations/store.test.ts +31 -0
- package/src/annotations/types.ts +3 -2
- package/src/canvas/Stage.test.tsx +18 -0
- package/src/canvas/index.ts +0 -9
- package/src/chrome/labTypes.ts +1 -1
- package/src/chrome/regions/PaletteRegion.test.tsx +18 -0
- package/src/chrome/regions/PaletteRegion.tsx +5 -4
- package/src/chrome/regions/SidebarRegion.less +197 -0
- package/src/chrome/regions/SidebarRegion.test.tsx +20 -0
- package/src/chrome/regions/SidebarRegion.tsx +7 -2
- package/src/chrome/regions/TitleBarRegion.tsx +1 -1
- package/src/chrome/types.ts +13 -4
- package/src/config/autoConfig.ts +2 -2
- package/src/config/builder.ts +23 -5
- package/src/config/entry.test.ts +8 -0
- package/src/config/index.ts +2 -0
- package/src/config/rules.ts +2 -0
- package/src/config/sectionTree.test.ts +146 -0
- package/src/config/sectionTree.ts +124 -0
- package/src/config/types.ts +4 -4
- package/src/controls/AutoControls.stories.tsx +6 -6
- package/src/controls/ControlPanel.less +27 -0
- package/src/controls/ControlPanel.stories.tsx +95 -1
- package/src/controls/ControlPanel.test.tsx +82 -112
- package/src/controls/ControlPanel.tsx +159 -396
- package/src/controls/inDialog.test.tsx +82 -0
- package/src/controls/inDialog.tsx +46 -0
- package/src/controls/index.ts +1 -0
- package/src/index.test.ts +0 -2
- package/src/index.ts +14 -35
- package/src/instrument/types.ts +5 -2
- package/src/lab/Lab.nebula.test.tsx +31 -0
- package/src/lab/Lab.tsx +57 -18
- 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/passthrough/weasel-ui.ts +25 -10
- package/src/primitives/FpsMeter.less +6 -1
- package/src/primitives/FpsMeter.test.tsx +12 -4
- package/src/primitives/FpsMeter.tsx +6 -1
- package/src/primitives/JobProgress.stories.tsx +1 -1
- package/src/primitives/StatusBar.less +6 -0
- package/src/primitives/index.ts +0 -2
- package/src/specimen/Specimen.tsx +42 -56
- package/src/state/types.ts +8 -2
- package/src/state/view.test.ts +4 -4
- package/src/styles.less +4 -2
- package/src/theme/Interstellar.stories.tsx +8 -9
- 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/trial/Trial.canvas.test.tsx +9 -5
- package/src/trial/Trial.config.test.tsx +1 -1
- package/src/trial/Trial.tsx +30 -8
- package/dist/_dts/index-BNdz-tNd.d.ts +0 -423
- package/dist/chunk-7PFH3CNM.js.map +0 -1
- package/dist/chunk-CEIUBPPW.js.map +0 -1
- package/dist/chunk-EH7UEQV7.js +0 -590
- package/dist/chunk-EH7UEQV7.js.map +0 -1
- package/dist/chunk-EHGVROVV.js.map +0 -1
- package/dist/chunk-IHUVQ3TX.js.map +0 -1
- package/dist/chunk-MDVTK5U3.js +0 -170
- package/dist/chunk-MDVTK5U3.js.map +0 -1
- package/dist/chunk-PJ7WEQBJ.js +0 -18
- package/dist/chunk-PJ7WEQBJ.js.map +0 -1
- package/dist/chunk-T3EC2DXD.js +0 -12628
- package/dist/chunk-T3EC2DXD.js.map +0 -1
- package/dist/chunk-UN4U4P63.js.map +0 -1
- package/dist/chunk-ZDHY47EV.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/useOrbit.test.ts +0 -167
- package/src/canvas/useOrbit.ts +0 -156
- 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,82 @@
|
|
|
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
|
+
|
|
74
|
+
describe('summarizeValue', () => {
|
|
75
|
+
it('joins lists and names emptiness', () => {
|
|
76
|
+
expect(summarizeValue(['a', 'b'])).toBe('a, b');
|
|
77
|
+
expect(summarizeValue([])).toBe('None');
|
|
78
|
+
expect(summarizeValue('')).toBe('None');
|
|
79
|
+
expect(summarizeValue(3)).toBe('3');
|
|
80
|
+
expect(summarizeValue({ a: 1 })).toBe('{"a":1}');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { DialogRow } from '@weasel-js/ui';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
import type { ControlRenderer } from '../config/types';
|
|
4
|
+
|
|
5
|
+
/** Options for {@link inDialog}. */
|
|
6
|
+
export interface InDialogOptions {
|
|
7
|
+
/** What the row's button shows of the value. Defaults to a list joined with
|
|
8
|
+
* commas, or the value itself. */
|
|
9
|
+
summary?: (value: unknown) => ReactNode;
|
|
10
|
+
/** The dialog's heading. Defaults to the row's label. */
|
|
11
|
+
title?: ReactNode;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** The one-line summary a dialog row shows when it is given none. */
|
|
15
|
+
export function summarizeValue(value: unknown): string {
|
|
16
|
+
if (Array.isArray(value)) return value.length === 0 ? 'None' : value.join(', ');
|
|
17
|
+
if (value === undefined || value === null || value === '') return 'None';
|
|
18
|
+
return typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Moves a control into a modal: the row becomes a button showing a summary of
|
|
23
|
+
* the value, and the button opens a dialog whose body is `body`, handed the
|
|
24
|
+
* same context a row renderer gets. For a leaf whose editor will never fit a
|
|
25
|
+
* sidebar row.
|
|
26
|
+
*/
|
|
27
|
+
export function inDialog(body: ControlRenderer, opts: InDialogOptions = {}): ControlRenderer {
|
|
28
|
+
const summary = opts.summary ?? summarizeValue;
|
|
29
|
+
return (ctx) => {
|
|
30
|
+
const { pref, value, auto, setAuto } = ctx;
|
|
31
|
+
const manual = (pref as { manual?: boolean }).manual === true;
|
|
32
|
+
return (
|
|
33
|
+
<DialogRow
|
|
34
|
+
label={pref.name}
|
|
35
|
+
description={pref.description}
|
|
36
|
+
summary={summary(value)}
|
|
37
|
+
title={opts.title}
|
|
38
|
+
readout={auto ? 'auto' : undefined}
|
|
39
|
+
auto={auto}
|
|
40
|
+
onAutoChange={manual ? undefined : setAuto}
|
|
41
|
+
>
|
|
42
|
+
{() => body(ctx)}
|
|
43
|
+
</DialogRow>
|
|
44
|
+
);
|
|
45
|
+
};
|
|
46
|
+
}
|
package/src/controls/index.ts
CHANGED
package/src/index.test.ts
CHANGED
|
@@ -72,8 +72,6 @@ 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.useOrbit).toBe('function');
|
|
76
|
-
expect(typeof kit.orbitAfterDrag).toBe('function');
|
|
77
75
|
expect(typeof kit.usePanZoom).toBe('function');
|
|
78
76
|
expect(typeof kit.useJob).toBe('function');
|
|
79
77
|
expect(typeof kit.as2DView).toBe('function');
|
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,53 +25,44 @@ 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,
|
|
36
|
+
TokenScale,
|
|
37
|
+
TokenScaleRule,
|
|
46
38
|
} from '@weasel-js/ui';
|
|
47
39
|
export {
|
|
48
40
|
Button,
|
|
49
|
-
CheckboxRow,
|
|
50
41
|
CloseIcon,
|
|
51
42
|
CollapseIcon,
|
|
52
|
-
ColorRow,
|
|
53
43
|
CurveField,
|
|
54
|
-
EffectCard,
|
|
55
|
-
EffectCardList,
|
|
56
44
|
ExpandIcon,
|
|
57
45
|
Icon,
|
|
58
46
|
Input,
|
|
59
47
|
inferTokenType,
|
|
60
48
|
Jog,
|
|
61
|
-
LayerStack,
|
|
62
|
-
NumberRow,
|
|
63
49
|
PauseIcon,
|
|
64
50
|
PlayIcon,
|
|
51
|
+
PropertyControl,
|
|
52
|
+
PropertyField,
|
|
65
53
|
PropertyGroup,
|
|
66
54
|
PropertyList,
|
|
67
55
|
PropertyPanel,
|
|
68
56
|
PropertyRow,
|
|
69
57
|
PropertySpan,
|
|
70
58
|
ResetIcon,
|
|
59
|
+
refitScale,
|
|
71
60
|
Select,
|
|
72
|
-
SelectRow,
|
|
73
|
-
SliderRow,
|
|
74
61
|
StepBackIcon,
|
|
75
62
|
StepForwardIcon,
|
|
76
63
|
StopIcon,
|
|
77
64
|
Subpanel,
|
|
78
|
-
TextRow,
|
|
79
65
|
ToggleBar,
|
|
80
|
-
ToggleRow,
|
|
81
66
|
TokenPanel,
|
|
82
67
|
} from '@weasel-js/ui';
|
|
83
68
|
export type {
|
|
@@ -148,13 +133,9 @@ export type {
|
|
|
148
133
|
CanvasStackContextValue,
|
|
149
134
|
CanvasStackProps,
|
|
150
135
|
CanvasStackSurface,
|
|
151
|
-
OrbitHandlers,
|
|
152
|
-
OrbitView,
|
|
153
136
|
PanZoomHandlers,
|
|
154
137
|
StageProps,
|
|
155
|
-
UseOrbitOptions,
|
|
156
138
|
UsePanZoomOptions,
|
|
157
|
-
Vec3,
|
|
158
139
|
ViewportSize,
|
|
159
140
|
WorldFrame,
|
|
160
141
|
WorldSpec,
|
|
@@ -165,19 +146,13 @@ export {
|
|
|
165
146
|
CanvasStack,
|
|
166
147
|
CanvasStackContext,
|
|
167
148
|
centerOn,
|
|
168
|
-
clampPitch,
|
|
169
149
|
DEFAULT_FRAME,
|
|
170
150
|
fitStage,
|
|
171
|
-
orbitAfterDrag,
|
|
172
|
-
orbitAfterWheel,
|
|
173
|
-
PITCH_LIMIT,
|
|
174
151
|
resolveFrame,
|
|
175
152
|
Stage,
|
|
176
153
|
screenToWorld,
|
|
177
|
-
useOrbit,
|
|
178
154
|
usePanZoom,
|
|
179
155
|
worldToScreen,
|
|
180
|
-
wrapYaw,
|
|
181
156
|
zoomAt,
|
|
182
157
|
} from './canvas';
|
|
183
158
|
export * from './chrome';
|
|
@@ -192,11 +167,13 @@ export {
|
|
|
192
167
|
f,
|
|
193
168
|
GroupNode,
|
|
194
169
|
isConfigBranch,
|
|
170
|
+
ListNode,
|
|
195
171
|
NumberNode,
|
|
196
172
|
StringNode,
|
|
197
173
|
ValueNode,
|
|
198
174
|
} from './config/builder';
|
|
199
175
|
export { fromConfigFields } from './config/fromConfigField';
|
|
176
|
+
export { type SectionTree, sectionTree } from './config/sectionTree';
|
|
200
177
|
export {
|
|
201
178
|
fillConfigDefaults,
|
|
202
179
|
hasConfigPath,
|
|
@@ -234,6 +211,7 @@ export { useConfigSchema } from './config/useConfigSchema';
|
|
|
234
211
|
export { useResolvedConfig } from './config/useResolvedConfig';
|
|
235
212
|
export { isLeafVisible } from './config/visible';
|
|
236
213
|
export { ControlPanel } from './controls/ControlPanel';
|
|
214
|
+
export { type InDialogOptions, inDialog, summarizeValue } from './controls/inDialog';
|
|
237
215
|
export type {
|
|
238
216
|
CheckboxField,
|
|
239
217
|
ColorField,
|
|
@@ -269,8 +247,8 @@ export type { ValidationResult } from './instrument/validateConfigSchema';
|
|
|
269
247
|
export { validateConfigSchema } from './instrument/validateConfigSchema';
|
|
270
248
|
export * from './job';
|
|
271
249
|
export * from './lab';
|
|
272
|
-
export type { LayerListProps,
|
|
273
|
-
export { LayerList } from './layers';
|
|
250
|
+
export type { LayerListItem, LayerListProps, LayerMove } from './layers';
|
|
251
|
+
export { LayerList, moveLayers } from './layers';
|
|
274
252
|
export * from './loupe';
|
|
275
253
|
export * from './primitives';
|
|
276
254
|
export { SPECIMEN_SECTIONS, Specimen } from './specimen/Specimen';
|
|
@@ -298,6 +276,7 @@ export { Persistence, type PersistenceProps } from './state/Persistence';
|
|
|
298
276
|
export type { RecordCache, RecordChange } from './state/records';
|
|
299
277
|
export type {
|
|
300
278
|
CreateLabStoreOptions,
|
|
279
|
+
LabDensity,
|
|
301
280
|
LabDocument,
|
|
302
281
|
LabMode,
|
|
303
282
|
LabStoreState,
|
package/src/instrument/types.ts
CHANGED
|
@@ -70,7 +70,7 @@ export interface CanvasCapability<TS = unknown, TC = unknown> {
|
|
|
70
70
|
/** Declares that an instrument's DOM is content of a fixed size, which the
|
|
71
71
|
* trial pans and zooms the way it does a canvas's layers. Not combined with
|
|
72
72
|
* `canvas`: there the DOM is an unzoomed overlay, and `canvas` wins. */
|
|
73
|
-
export interface StageCapability {
|
|
73
|
+
export interface StageCapability<TS = unknown, TC = unknown> {
|
|
74
74
|
/** The content's own size in CSS pixels, at zoom 1. */
|
|
75
75
|
size: ViewportSize;
|
|
76
76
|
/** Where the view starts. Omitted, the content opens centered and shrunk to
|
|
@@ -78,6 +78,9 @@ export interface StageCapability {
|
|
|
78
78
|
initialView?: ViewTransform | ((viewport: ViewportSize) => ViewTransform);
|
|
79
79
|
minZoom?: number;
|
|
80
80
|
maxZoom?: number;
|
|
81
|
+
/** Drawn over the content in viewport pixels, outside the camera — a legend,
|
|
82
|
+
* a floating panel, anything that must not zoom with the picture. */
|
|
83
|
+
overlay?: (ctx: RenderContext<TS, TC>) => ReactNode;
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
/** Declares which of an instrument's layers the trial should offer
|
|
@@ -159,7 +162,7 @@ export interface Instrument<TS = unknown, TC = unknown, TItem = unknown> {
|
|
|
159
162
|
serialize?: (state: TS) => unknown;
|
|
160
163
|
deserialize?: (data: unknown, config: TC) => TS;
|
|
161
164
|
canvas?: CanvasCapability<TS, TC>;
|
|
162
|
-
stage?: StageCapability
|
|
165
|
+
stage?: StageCapability<TS, TC>;
|
|
163
166
|
layers?: LayerCapability;
|
|
164
167
|
dragDrop?: DragDropCapability<TS, TC>;
|
|
165
168
|
undo?: UndoCapability;
|
|
@@ -0,0 +1,31 @@
|
|
|
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(<Lab instruments={[bare]} defaultInstrument="Bare" mode="dark" nebula={['#ff0000', '#00ff00']} />);
|
|
23
|
+
expect(backdrop(container).match(/radial-gradient/g)).toHaveLength(3);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('resolves a theme ramp against the lab’s theme', () => {
|
|
27
|
+
const { container } = render(<Lab instruments={[bare]} defaultInstrument="Bare" mode="dark" nebula={{ ramp: 'swatch' }} />);
|
|
28
|
+
const sky = resolveTheme(interstellarTheme, { mode: 'dark' })['--wzl-swatch-sky'];
|
|
29
|
+
expect(backdrop(container)).toContain(sky);
|
|
30
|
+
});
|
|
31
|
+
});
|
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,
|
|
@@ -30,7 +31,7 @@ import { type OpenedLabStore, openLabStore } from '../state/openLabStore';
|
|
|
30
31
|
import { PersistenceContext } from '../state/Persistence';
|
|
31
32
|
import { createRecordCache } from '../state/records';
|
|
32
33
|
import { createLabStore, type LabStore } from '../state/store';
|
|
33
|
-
import type { LabMode, StorageAdapter, TrialRecord } from '../state/types';
|
|
34
|
+
import type { LabDensity, LabMode, StorageAdapter, TrialRecord } from '../state/types';
|
|
34
35
|
import { useOpenOnce, useWarnIgnoredChange } from '../state/useOpenOnce';
|
|
35
36
|
import { usePersistedState } from '../state/usePersistedState';
|
|
36
37
|
import { SurfaceCanvasContext, SurfaceContext } from '../surface/SurfaceContext';
|
|
@@ -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 {
|
|
@@ -64,15 +64,24 @@ interface LabBaseProps {
|
|
|
64
64
|
instruments: InstrumentList;
|
|
65
65
|
defaultInstrument: string;
|
|
66
66
|
mode?: LabMode;
|
|
67
|
+
/** How much room the lab's chrome takes. Default `'comfortable'`; a lab whose
|
|
68
|
+
* window is the whole app, rather than a panel beside one, reads better at
|
|
69
|
+
* `'roomy'`. The trials' own contents are unaffected. */
|
|
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;
|
|
67
75
|
/** Rendered while a stored lab loads. Default: the lab's empty shell. */
|
|
68
76
|
fallback?: ReactNode;
|
|
69
77
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
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.
|
|
74
83
|
*/
|
|
75
|
-
nebula?:
|
|
84
|
+
nebula?: ColorList;
|
|
76
85
|
title?: string;
|
|
77
86
|
/** The project's other labs. Given two or more, the title becomes the way to
|
|
78
87
|
* reach them — the same switcher `<LabShell>` renders, so a lab reached from
|
|
@@ -175,12 +184,18 @@ async function openStoredLab(
|
|
|
175
184
|
function LabFallback({
|
|
176
185
|
title,
|
|
177
186
|
mode,
|
|
187
|
+
density,
|
|
188
|
+
theme = interstellarTheme,
|
|
178
189
|
pages,
|
|
179
190
|
path,
|
|
180
|
-
}: Pick<LabBaseProps, 'title' | 'mode' | 'pages' | 'path'>) {
|
|
181
|
-
const resolvedMode =
|
|
191
|
+
}: Pick<LabBaseProps, 'title' | 'mode' | 'density' | 'theme' | 'pages' | 'path'>) {
|
|
192
|
+
const resolvedMode = useResolvedColorMode(mode ?? 'auto');
|
|
182
193
|
return (
|
|
183
|
-
<ThemeProvider
|
|
194
|
+
<ThemeProvider
|
|
195
|
+
theme={theme}
|
|
196
|
+
selection={{ mode: resolvedMode, density: density ?? 'comfortable' }}
|
|
197
|
+
className="lk-lab"
|
|
198
|
+
>
|
|
184
199
|
<LabShell
|
|
185
200
|
title={title ?? 'Labkit'}
|
|
186
201
|
mode={mode}
|
|
@@ -205,7 +220,15 @@ const NEBULA_SLOTS = [
|
|
|
205
220
|
{ cx: '85%', cy: '18%', sx: '55%', sy: '80%', stop: '70%' },
|
|
206
221
|
] as const;
|
|
207
222
|
|
|
208
|
-
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));
|
|
209
232
|
const blobs = colors.map((c, i) => {
|
|
210
233
|
const p = NEBULA_SLOTS[i % NEBULA_SLOTS.length];
|
|
211
234
|
return `radial-gradient(ellipse ${p.sx} ${p.sy} at ${p.cx} ${p.cy}, color-mix(in srgb, ${c} 22%, transparent), transparent ${p.stop})`;
|
|
@@ -289,7 +312,14 @@ export function Lab(props: LabProps) {
|
|
|
289
312
|
if (!opened) {
|
|
290
313
|
if (props.fallback !== undefined) return props.fallback;
|
|
291
314
|
return (
|
|
292
|
-
<LabFallback
|
|
315
|
+
<LabFallback
|
|
316
|
+
title={props.title}
|
|
317
|
+
mode={props.mode}
|
|
318
|
+
density={props.density}
|
|
319
|
+
{...(props.theme ? { theme: props.theme } : {})}
|
|
320
|
+
pages={props.pages}
|
|
321
|
+
path={props.path}
|
|
322
|
+
/>
|
|
293
323
|
);
|
|
294
324
|
}
|
|
295
325
|
return <LabRuntime {...props} opened={opened} />;
|
|
@@ -298,6 +328,8 @@ export function Lab(props: LabProps) {
|
|
|
298
328
|
function LabRuntime({
|
|
299
329
|
instruments,
|
|
300
330
|
mode,
|
|
331
|
+
density,
|
|
332
|
+
theme = interstellarTheme,
|
|
301
333
|
nebula,
|
|
302
334
|
title,
|
|
303
335
|
pages,
|
|
@@ -439,7 +471,7 @@ function LabRuntime({
|
|
|
439
471
|
})),
|
|
440
472
|
[undockedPanels],
|
|
441
473
|
);
|
|
442
|
-
const resolvedMode =
|
|
474
|
+
const resolvedMode = useResolvedColorMode(modeValue);
|
|
443
475
|
|
|
444
476
|
// Tools and lab contributions share one id namespace, so they merge once
|
|
445
477
|
// here — before any region renders — and a collision throws rather than one
|
|
@@ -548,8 +580,15 @@ function LabRuntime({
|
|
|
548
580
|
// Only override the backdrop in dark, where there is one to override.
|
|
549
581
|
// Setting a CSS custom property is the sanctioned use of inline style.
|
|
550
582
|
const backdropStyle =
|
|
551
|
-
resolvedMode === 'dark' && nebula && nebula
|
|
552
|
-
? ({
|
|
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)
|
|
553
592
|
: undefined;
|
|
554
593
|
|
|
555
594
|
const workspace = (
|
|
@@ -574,8 +613,8 @@ function LabRuntime({
|
|
|
574
613
|
<AnnotationPreloadContext.Provider value={opened.marks}>
|
|
575
614
|
<LabContext.Provider value={contextValue}>
|
|
576
615
|
<ThemeProvider
|
|
577
|
-
theme={
|
|
578
|
-
selection={{ mode: resolvedMode }}
|
|
616
|
+
theme={theme}
|
|
617
|
+
selection={{ mode: resolvedMode, density: density ?? 'comfortable' }}
|
|
579
618
|
className="lk-lab"
|
|
580
619
|
style={backdropStyle}
|
|
581
620
|
>
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { ThemeContext } from '@weasel-js/theme/react';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
import { AnnotationsContext } from '../annotations/AnnotationsContext';
|
|
4
|
+
import { AnnotationPreloadContext } from '../annotations/preload';
|
|
5
|
+
import { CameraWheelContext } from '../canvas/CameraWheelContext';
|
|
6
|
+
import { CanvasStackContext } from '../canvas/CanvasStackContext';
|
|
7
|
+
import { LabStoreContext, TrialIdContext } from '../state/context';
|
|
8
|
+
import { PersistenceContext } from '../state/Persistence';
|
|
9
|
+
import { SurfaceCanvasContext, SurfaceContext } from '../surface/SurfaceContext';
|
|
10
|
+
import { TrialDragContext } from '../trial/TrialDragContext';
|
|
11
|
+
import { LabContext } from './LabContext';
|
|
12
|
+
import { PanelHostContext } from './panelHost';
|
|
13
|
+
|
|
14
|
+
/** Props for `<LabBoundary>`. */
|
|
15
|
+
export interface LabBoundaryProps {
|
|
16
|
+
children: ReactNode;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Renders its children as though no lab, trial or theme were above them: every context a `<Lab>` or a
|
|
21
|
+
* `<Trial>` publishes reads as absent below it, so a labkit piece mounted inside a trial behaves the way it
|
|
22
|
+
* does on a bare page. A `<LabRoot>` under it applies its own theme instead of deferring to the lab's, and a
|
|
23
|
+
* `usePersistedState` under it keeps local state instead of writing into the enclosing lab's records.
|
|
24
|
+
*
|
|
25
|
+
* For a host that shows arbitrary content inside a trial, such as a component workshop; a lab's own chrome
|
|
26
|
+
* never needs it.
|
|
27
|
+
*/
|
|
28
|
+
export function LabBoundary({ children }: LabBoundaryProps) {
|
|
29
|
+
return (
|
|
30
|
+
<LabStoreContext.Provider value={null}>
|
|
31
|
+
<PersistenceContext.Provider value={null}>
|
|
32
|
+
<AnnotationPreloadContext.Provider value={null}>
|
|
33
|
+
<LabContext.Provider value={null}>
|
|
34
|
+
<PanelHostContext.Provider value={null}>
|
|
35
|
+
<SurfaceContext.Provider value={null}>
|
|
36
|
+
<SurfaceCanvasContext.Provider value={{ over: null, under: null }}>
|
|
37
|
+
<TrialIdContext.Provider value={null}>
|
|
38
|
+
<AnnotationsContext.Provider value={null}>
|
|
39
|
+
<CameraWheelContext.Provider value={null}>
|
|
40
|
+
<CanvasStackContext.Provider value={null}>
|
|
41
|
+
<TrialDragContext.Provider value={null}>
|
|
42
|
+
<ThemeContext.Provider value={null}>{children}</ThemeContext.Provider>
|
|
43
|
+
</TrialDragContext.Provider>
|
|
44
|
+
</CanvasStackContext.Provider>
|
|
45
|
+
</CameraWheelContext.Provider>
|
|
46
|
+
</AnnotationsContext.Provider>
|
|
47
|
+
</TrialIdContext.Provider>
|
|
48
|
+
</SurfaceCanvasContext.Provider>
|
|
49
|
+
</SurfaceContext.Provider>
|
|
50
|
+
</PanelHostContext.Provider>
|
|
51
|
+
</LabContext.Provider>
|
|
52
|
+
</AnnotationPreloadContext.Provider>
|
|
53
|
+
</PersistenceContext.Provider>
|
|
54
|
+
</LabStoreContext.Provider>
|
|
55
|
+
);
|
|
56
|
+
}
|