@weasel-js/labkit 1.5.2 → 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-CMMo180l.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-ZTiwzYyT.d.ts → Select.d-BchGMC2Y.d.ts} +102 -11
- package/dist/_dts/{frac-B4hFJMiN.d.ts → frac-DTRq7Xpb.d.ts} +18 -10
- package/dist/_dts/inDialog-D5ZKGjiD.d.ts +23 -0
- package/dist/_dts/{index-DlKolEIZ.d.ts → index-B0--8PEM.d.ts} +2 -2
- package/dist/_dts/index-DINmCuLA.d.ts +453 -0
- package/dist/_dts/number.d-OeQYQoUO.d.ts +28 -0
- package/dist/_dts/{types-Db-3NNk1.d.ts → types-B7y8hX1D.d.ts} +2 -2
- package/dist/_dts/{usePanZoom-SSTZe410.d.ts → usePanZoom-Bq73pwFe.d.ts} +2 -2
- package/dist/_dts/{useTrialState-pnAE-UBD.d.ts → useTrialState-CP4jfuKl.d.ts} +1 -1
- package/dist/canvas/index.d.ts +6 -5
- 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-753Y6HE6.js → chunk-3FI7Y4R4.js} +4 -4
- package/dist/chunk-3FI7Y4R4.js.map +1 -0
- package/dist/{chunk-REOBGYMC.js → chunk-AWH3SRPW.js} +31 -5
- package/dist/chunk-AWH3SRPW.js.map +1 -0
- package/dist/{chunk-A2ACAGW6.js → chunk-BVL7Y574.js} +22 -5
- package/dist/chunk-BVL7Y574.js.map +1 -0
- package/dist/{chunk-CKUKF72R.js → chunk-H5VIV7MQ.js} +26 -22
- 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-LRI3MBGR.js → chunk-MOLIUNVH.js} +4 -4
- package/dist/{chunk-LRI3MBGR.js.map → chunk-MOLIUNVH.js.map} +1 -1
- package/dist/chunk-USO6TERP.js +90 -0
- package/dist/chunk-USO6TERP.js.map +1 -0
- package/dist/{chunk-EMXAIRUJ.js → chunk-VQ47KWD5.js} +3 -3
- package/dist/{chunk-EMXAIRUJ.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-KJCUERQF.js → chunk-ZORVNOKX.js} +6 -25
- package/dist/chunk-ZORVNOKX.js.map +1 -0
- package/dist/config/index.d.ts +50 -7
- 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 +83 -102
- package/dist/index.js +430 -458
- 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 +407 -204
- 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 +298 -174
- 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/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/types.ts +5 -2
- package/src/config/autoConfig.ts +2 -2
- package/src/config/builder.ts +20 -1
- 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/controls/ControlPanel.less +4 -0
- package/src/controls/ControlPanel.stories.tsx +55 -2
- package/src/controls/ControlPanel.test.tsx +30 -3
- package/src/controls/ControlPanel.tsx +116 -337
- 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.ts +11 -26
- package/src/instrument/types.ts +5 -2
- package/src/lab/Lab.nebula.test.tsx +31 -0
- package/src/lab/Lab.tsx +38 -15
- 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/index.ts +0 -2
- package/src/specimen/Specimen.tsx +42 -56
- package/src/state/types.ts +2 -1
- 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 +1 -1
- package/src/trial/Trial.tsx +30 -8
- package/dist/_dts/index-8LiKe3A-.d.ts +0 -410
- 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-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/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/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,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.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 {
|
|
@@ -185,11 +167,13 @@ export {
|
|
|
185
167
|
f,
|
|
186
168
|
GroupNode,
|
|
187
169
|
isConfigBranch,
|
|
170
|
+
ListNode,
|
|
188
171
|
NumberNode,
|
|
189
172
|
StringNode,
|
|
190
173
|
ValueNode,
|
|
191
174
|
} from './config/builder';
|
|
192
175
|
export { fromConfigFields } from './config/fromConfigField';
|
|
176
|
+
export { type SectionTree, sectionTree } from './config/sectionTree';
|
|
193
177
|
export {
|
|
194
178
|
fillConfigDefaults,
|
|
195
179
|
hasConfigPath,
|
|
@@ -227,6 +211,7 @@ export { useConfigSchema } from './config/useConfigSchema';
|
|
|
227
211
|
export { useResolvedConfig } from './config/useResolvedConfig';
|
|
228
212
|
export { isLeafVisible } from './config/visible';
|
|
229
213
|
export { ControlPanel } from './controls/ControlPanel';
|
|
214
|
+
export { type InDialogOptions, inDialog, summarizeValue } from './controls/inDialog';
|
|
230
215
|
export type {
|
|
231
216
|
CheckboxField,
|
|
232
217
|
ColorField,
|
|
@@ -262,8 +247,8 @@ export type { ValidationResult } from './instrument/validateConfigSchema';
|
|
|
262
247
|
export { validateConfigSchema } from './instrument/validateConfigSchema';
|
|
263
248
|
export * from './job';
|
|
264
249
|
export * from './lab';
|
|
265
|
-
export type { LayerListProps,
|
|
266
|
-
export { LayerList } from './layers';
|
|
250
|
+
export type { LayerListItem, LayerListProps, LayerMove } from './layers';
|
|
251
|
+
export { LayerList, moveLayers } from './layers';
|
|
267
252
|
export * from './loupe';
|
|
268
253
|
export * from './primitives';
|
|
269
254
|
export { SPECIMEN_SECTIONS, Specimen } from './specimen/Specimen';
|
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,
|
|
@@ -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,7 +471,7 @@ 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
|
|
@@ -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
|
+
});
|
|
@@ -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
|
+
}
|
package/src/lab/LabHeader.tsx
CHANGED
|
@@ -1,22 +1,7 @@
|
|
|
1
|
-
import { AddIcon
|
|
2
|
-
import {
|
|
3
|
-
import type { LabMode } from '../state/types';
|
|
1
|
+
import { AddIcon } from '@weasel-js/ui';
|
|
2
|
+
import { ColorModeControl, MenuButton } from '../passthrough/weasel-ui';
|
|
4
3
|
import { useLabContext } from './LabContext';
|
|
5
4
|
|
|
6
|
-
// The glyph is the segment's content and the word is its accessible name, so
|
|
7
|
-
// the bar stays a three-way radiogroup announcing Auto / Light / Dark.
|
|
8
|
-
const MODES = [
|
|
9
|
-
{ value: 'auto' as LabMode, label: 'Auto', glyph: <ModeAutoIcon size={14} /> },
|
|
10
|
-
{ value: 'light' as LabMode, label: 'Light', glyph: <ModeLightIcon size={14} /> },
|
|
11
|
-
{ value: 'dark' as LabMode, label: 'Dark', glyph: <ModeDarkIcon size={14} /> },
|
|
12
|
-
];
|
|
13
|
-
|
|
14
|
-
const MODE_ITEMS = MODES.map(({ value, label, glyph }) => ({
|
|
15
|
-
value,
|
|
16
|
-
label: glyph,
|
|
17
|
-
ariaLabel: label,
|
|
18
|
-
}));
|
|
19
|
-
|
|
20
5
|
/** The controls `<Lab>` puts in its header: add a trial, and choose the color
|
|
21
6
|
* mode. Both drive `LabContext`, which carried them with no UI at all — so
|
|
22
7
|
* every consumer rebuilt these two. Rendered before a consumer's own header
|
|
@@ -45,15 +30,11 @@ export function LabHeader({ addTrial = true }: { addTrial?: boolean }) {
|
|
|
45
30
|
/>
|
|
46
31
|
)}
|
|
47
32
|
|
|
48
|
-
<
|
|
33
|
+
<ColorModeControl
|
|
49
34
|
className="lk-lab-header__mode"
|
|
50
|
-
ariaLabel="Color mode"
|
|
51
35
|
variant="flat"
|
|
52
|
-
items={MODE_ITEMS}
|
|
53
36
|
value={lab.mode}
|
|
54
|
-
onChange={
|
|
55
|
-
if (next) lab.setMode(next);
|
|
56
|
-
}}
|
|
37
|
+
onChange={lab.setMode}
|
|
57
38
|
/>
|
|
58
39
|
</>
|
|
59
40
|
);
|
package/src/lab/LabRoot.tsx
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { ThemeProvider, useThemeOptional } from '@weasel-js/theme/react';
|
|
1
|
+
import { ThemeProvider, useResolvedColorMode, useThemeOptional } from '@weasel-js/theme/react';
|
|
2
2
|
import type { ReactNode } from 'react';
|
|
3
3
|
import type { LabMode } from '../state/types';
|
|
4
4
|
import { interstellarTheme } from '../theme/interstellar';
|
|
5
|
-
import { useResolvedMode } from './useSystemMode';
|
|
6
5
|
|
|
7
6
|
/** Props for `<LabRoot>`. */
|
|
8
7
|
export interface LabRootProps {
|
|
@@ -24,7 +23,7 @@ export interface LabRootProps {
|
|
|
24
23
|
* Pair it with `import '@weasel-js/labkit/styles.css'`, which is what defines
|
|
25
24
|
* the rules this element scopes. */
|
|
26
25
|
export function LabRoot({ children, mode = 'auto', className }: LabRootProps) {
|
|
27
|
-
const resolved =
|
|
26
|
+
const resolved = useResolvedColorMode(mode);
|
|
28
27
|
const outer = useThemeOptional();
|
|
29
28
|
|
|
30
29
|
// Overlays portal here rather than to the themed wrapper above: `.lk-root`
|
package/src/lab/LabShell.less
CHANGED
|
@@ -86,3 +86,18 @@ export const HeaderControlsLineUp: Story = {
|
|
|
86
86
|
await expect(middles).toEqual(middles.map(() => middles[0]));
|
|
87
87
|
},
|
|
88
88
|
};
|
|
89
|
+
|
|
90
|
+
// One document serving its pages from the hash: the open route is the one the
|
|
91
|
+
// switcher marks, and each entry is a link to its fragment.
|
|
92
|
+
export const HashRoutedPages: Story = {
|
|
93
|
+
args: {
|
|
94
|
+
title: 'Registry',
|
|
95
|
+
pages: [
|
|
96
|
+
{ href: '/app/', label: 'App' },
|
|
97
|
+
{ href: '#/dev/tools', label: 'Tools' },
|
|
98
|
+
{ href: '#/dev/registry', label: 'Registry' },
|
|
99
|
+
],
|
|
100
|
+
path: '/app/#/dev/registry?entry=rect',
|
|
101
|
+
children: <p>Open the title to see the current route marked.</p>,
|
|
102
|
+
},
|
|
103
|
+
};
|
|
@@ -47,6 +47,24 @@ describe('LabShell', () => {
|
|
|
47
47
|
expect(container.querySelector('.lk-root')?.hasAttribute('data-wzl-portal-host')).toBe(true);
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
+
test('holds the document title while mounted when asked to', () => {
|
|
51
|
+
document.title = 'before';
|
|
52
|
+
const { unmount } = render(
|
|
53
|
+
<LabShell title="t" documentTitle="Page title">
|
|
54
|
+
x
|
|
55
|
+
</LabShell>,
|
|
56
|
+
);
|
|
57
|
+
expect(document.title).toBe('Page title');
|
|
58
|
+
unmount();
|
|
59
|
+
expect(document.title).toBe('before');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('leaves the document title alone by default', () => {
|
|
63
|
+
document.title = 'before';
|
|
64
|
+
render(<LabShell title="t">x</LabShell>);
|
|
65
|
+
expect(document.title).toBe('before');
|
|
66
|
+
});
|
|
67
|
+
|
|
50
68
|
test('always applies lk-root class', () => {
|
|
51
69
|
const { container } = render(<LabShell title="t">x</LabShell>);
|
|
52
70
|
expect(container.querySelector('.lk-root')).not.toBeNull();
|
package/src/lab/LabShell.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ReactNode, useEffect } from 'react';
|
|
2
2
|
import type { LabMode } from '../state/types';
|
|
3
3
|
import { LabRoot } from './LabRoot';
|
|
4
4
|
import type { LabPage } from './LabSwitcher';
|
|
@@ -20,6 +20,9 @@ export interface LabShellProps {
|
|
|
20
20
|
/** The path the switcher reads to mark the open page. Defaults to the
|
|
21
21
|
* current location. */
|
|
22
22
|
path?: string;
|
|
23
|
+
/** Set as `document.title` while the shell is mounted, and the previous
|
|
24
|
+
* title restored after. Omitted, the document's title is left alone. */
|
|
25
|
+
documentTitle?: string;
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
/** Page frame for a lab: a titled header, a body, and an optional footer,
|
|
@@ -33,7 +36,17 @@ export function LabShell({
|
|
|
33
36
|
mode = 'auto',
|
|
34
37
|
pages,
|
|
35
38
|
path,
|
|
39
|
+
documentTitle,
|
|
36
40
|
}: LabShellProps) {
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (documentTitle === undefined) return;
|
|
43
|
+
const previous = document.title;
|
|
44
|
+
document.title = documentTitle;
|
|
45
|
+
return () => {
|
|
46
|
+
document.title = previous;
|
|
47
|
+
};
|
|
48
|
+
}, [documentTitle]);
|
|
49
|
+
|
|
37
50
|
return (
|
|
38
51
|
<LabRoot mode={mode} className="lk-shell">
|
|
39
52
|
<header className="lk-shell-header">
|
package/src/lab/LabSwitcher.less
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
// hyphen and the control becomes three ragged lines with a caret adrift.
|
|
27
27
|
white-space: nowrap;
|
|
28
28
|
|
|
29
|
-
&:hover { color: var(--wzl-accent); }
|
|
29
|
+
&:hover { color: var(--wzl-accent-fg); }
|
|
30
30
|
|
|
31
31
|
&:focus-visible {
|
|
32
32
|
outline: 2px solid var(--wzl-focus-ring, currentColor);
|