@weasel-js/labkit 0.8.0 → 1.0.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 +11 -3
- package/dist/_dts/{DrawCommand-DACv9lhe.d.ts → DrawCommand-BwRCZYuo.d.ts} +72 -6
- package/dist/_dts/{useExperimentState-eSvpwV_P.d.ts → useExperimentState-CJn2hHzd.d.ts} +6 -4
- package/dist/{chunk-SSMHPMDV.js → chunk-53XSBIUK.js} +12 -11
- package/dist/chunk-53XSBIUK.js.map +1 -0
- package/dist/{chunk-FBZCQXI2.js → chunk-FP5LYDVX.js} +1229 -1227
- package/dist/chunk-FP5LYDVX.js.map +1 -0
- package/dist/{chunk-OHGEOTCV.js → chunk-HNXVLKBG.js} +3 -3
- package/dist/{chunk-OHGEOTCV.js.map → chunk-HNXVLKBG.js.map} +1 -1
- package/dist/{chunk-6V7ROR7G.js → chunk-XVBXYKFJ.js} +1168 -557
- package/dist/chunk-XVBXYKFJ.js.map +1 -0
- package/dist/index.d.ts +37 -13
- package/dist/index.js +776 -27
- package/dist/index.js.map +1 -1
- package/dist/passthrough/weasel-canvas.d.ts +1 -1
- package/dist/passthrough/weasel-canvas.js +1 -1
- package/dist/passthrough/weasel-ui.d.ts +97 -69
- package/dist/passthrough/weasel-ui.js +2 -2
- package/dist/state/index.d.ts +2 -2
- package/dist/state/index.js +2 -2
- package/dist/styles.css +228 -287
- package/dist/ui/layers/index.js +3 -3
- package/package.json +4 -6
- package/src/canvas/CanvasStack.less +1 -1
- package/src/controls/ControlPanel.less +12 -12
- package/src/dragdrop/DragGhost.less +2 -2
- package/src/dragdrop/Palette.less +6 -6
- package/src/index.ts +2 -0
- package/src/lab/Lab.test.tsx +20 -15
- package/src/lab/Lab.tsx +33 -32
- package/src/lab/LabContext.ts +3 -3
- package/src/lab/LabShell.less +14 -14
- package/src/lab/LabShell.test.tsx +7 -20
- package/src/lab/LabShell.tsx +22 -13
- package/src/lab/WorkspaceGrid.less +1 -1
- package/src/lab/WorkspaceGrid.stories.tsx +4 -4
- package/src/lab/index.ts +1 -1
- package/src/lab/useSystemMode.ts +30 -0
- package/src/layers/LayerList.less +9 -9
- package/src/primitives/FpsMeter.less +5 -5
- package/src/primitives/ScaleIndicator.less +7 -7
- package/src/primitives/Sidebar.less +12 -12
- package/src/primitives/StatusBar.less +10 -10
- package/src/primitives/Toolbar.less +16 -16
- package/src/state/store.test.ts +17 -9
- package/src/state/store.ts +13 -10
- package/src/state/types.ts +5 -2
- package/src/theme/Interstellar.stories.less +20 -20
- package/src/theme/Interstellar.stories.tsx +44 -59
- package/src/theme/base.less +26 -26
- package/src/theme/interstellar.test.ts +30 -0
- package/src/theme/interstellar.tokens.json +57 -0
- package/src/theme/interstellar.ts +9 -0
- package/src/ui/layers/LayerStack.less +21 -21
- package/src/ui/properties/CurveField.less +5 -5
- package/src/ui/properties/EffectCard.tsx +2 -2
- package/src/ui/properties/PropertyGroup.less +6 -6
- package/src/ui/properties/PropertyPanel.less +70 -70
- package/src/ui/properties/SpeechBalloonPanels.stories.tsx +1 -1
- package/src/ui/properties/storyLayouts.tsx +1 -1
- package/src/workspace/Workspace.less +14 -14
- package/src/workspace/Workspace.stories.tsx +2 -2
- package/src/workspace/Workspace.test.tsx +2 -2
- package/dist/chunk-6V7ROR7G.js.map +0 -1
- package/dist/chunk-FBZCQXI2.js.map +0 -1
- package/dist/chunk-SSMHPMDV.js.map +0 -1
- package/dist/theme-interstellar.css +0 -25
- package/dist/theme-light.css +0 -13
- package/src/theme/interstellar.less +0 -34
- package/src/theme/light.less +0 -14
- package/src/theme/tokens.less +0 -85
package/dist/ui/layers/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { LayerStack } from '../../chunk-
|
|
2
|
-
import '../../chunk-
|
|
3
|
-
import '../../chunk-
|
|
1
|
+
export { LayerStack } from '../../chunk-HNXVLKBG.js';
|
|
2
|
+
import '../../chunk-FP5LYDVX.js';
|
|
3
|
+
import '../../chunk-XVBXYKFJ.js';
|
|
4
4
|
//# sourceMappingURL=index.js.map
|
|
5
5
|
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weasel-js/labkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "React widgets for building self-contained interactive lab pages",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "orochi235",
|
|
@@ -67,9 +67,7 @@
|
|
|
67
67
|
"types": "./dist/ui/layers/index.d.ts",
|
|
68
68
|
"import": "./dist/ui/layers/index.js"
|
|
69
69
|
},
|
|
70
|
-
"./styles.css": "./dist/styles.css"
|
|
71
|
-
"./theme-light.css": "./dist/theme-light.css",
|
|
72
|
-
"./theme-interstellar.css": "./dist/theme-interstellar.css"
|
|
70
|
+
"./styles.css": "./dist/styles.css"
|
|
73
71
|
},
|
|
74
72
|
"scripts": {
|
|
75
73
|
"dev": "vite",
|
|
@@ -78,7 +76,7 @@
|
|
|
78
76
|
"build": "tsup && npm run build:dts && npm run build:css && npm run build:fonts",
|
|
79
77
|
"build:dts": "tsx scripts/build-dts.mts",
|
|
80
78
|
"prepublishOnly": "npm run build",
|
|
81
|
-
"build:css": "lessc src/styles.less dist/styles.css
|
|
79
|
+
"build:css": "lessc src/styles.less dist/styles.css",
|
|
82
80
|
"build:fonts": "mkdir -p dist/fonts && cp ../theme/fonts/*.woff2 dist/fonts/",
|
|
83
81
|
"test": "vitest run",
|
|
84
82
|
"test:watch": "vitest",
|
|
@@ -95,6 +93,7 @@
|
|
|
95
93
|
"react-dom": "^19.0.0"
|
|
96
94
|
},
|
|
97
95
|
"dependencies": {
|
|
96
|
+
"@weasel-js/theme": "*",
|
|
98
97
|
"earcut": "2.2.4",
|
|
99
98
|
"polygon-clipping": "^0.15.7",
|
|
100
99
|
"react-aria-components": "^1.5.0",
|
|
@@ -105,7 +104,6 @@
|
|
|
105
104
|
"@rollup/plugin-alias": "^6.0.0",
|
|
106
105
|
"@testing-library/jest-dom": "^6.6.0",
|
|
107
106
|
"@weasel-js/modes": "*",
|
|
108
|
-
"@weasel-js/theme": "*",
|
|
109
107
|
"@weasel-js/ui": "*",
|
|
110
108
|
"less": "^4.2.0",
|
|
111
109
|
"rollup-plugin-dts": "^6.4.1",
|
|
@@ -1,32 +1,32 @@
|
|
|
1
1
|
.lk-control-panel {
|
|
2
2
|
display: flex;
|
|
3
3
|
flex-direction: column;
|
|
4
|
-
gap: var(--
|
|
5
|
-
padding: var(--
|
|
4
|
+
gap: var(--wzl-space-sm);
|
|
5
|
+
padding: var(--wzl-space-sm);
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
.lk-control-row {
|
|
9
9
|
display: flex;
|
|
10
10
|
flex-direction: column;
|
|
11
|
-
gap: var(--
|
|
11
|
+
gap: var(--wzl-space-xs);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
.lk-control-row--inline {
|
|
15
15
|
flex-direction: row;
|
|
16
16
|
align-items: center;
|
|
17
|
-
gap: var(--
|
|
17
|
+
gap: var(--wzl-space-sm);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
.lk-control-label {
|
|
21
|
-
font-size: var(--
|
|
22
|
-
color: var(--
|
|
21
|
+
font-size: var(--wzl-font-size-sm);
|
|
22
|
+
color: var(--wzl-fg);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
.lk-control-input {
|
|
26
26
|
width: 100%;
|
|
27
|
-
background: var(--
|
|
28
|
-
color: var(--
|
|
29
|
-
border: 1px solid var(--
|
|
27
|
+
background: var(--wzl-surface-raised);
|
|
28
|
+
color: var(--wzl-fg);
|
|
29
|
+
border: 1px solid var(--wzl-border);
|
|
30
30
|
border-radius: 4px;
|
|
31
31
|
padding: 2px 6px;
|
|
32
32
|
}
|
|
@@ -47,9 +47,9 @@
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
.lk-control-slider-value {
|
|
50
|
-
font-size: var(--
|
|
50
|
+
font-size: var(--wzl-font-size-sm);
|
|
51
51
|
font-variant-numeric: tabular-nums;
|
|
52
|
-
color: var(--
|
|
52
|
+
color: var(--wzl-fg-muted);
|
|
53
53
|
min-width: 3ch;
|
|
54
54
|
text-align: right;
|
|
55
55
|
}
|
|
@@ -57,5 +57,5 @@
|
|
|
57
57
|
.lk-control-slider-row {
|
|
58
58
|
display: flex;
|
|
59
59
|
align-items: center;
|
|
60
|
-
gap: var(--
|
|
60
|
+
gap: var(--wzl-space-sm);
|
|
61
61
|
}
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
pointer-events: none;
|
|
4
4
|
z-index: 9999;
|
|
5
5
|
padding: 4px 8px;
|
|
6
|
-
background: var(--
|
|
7
|
-
border: 1px solid var(--
|
|
6
|
+
background: var(--wzl-surface-raised);
|
|
7
|
+
border: 1px solid var(--wzl-border);
|
|
8
8
|
border-radius: 4px;
|
|
9
9
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.18);
|
|
10
10
|
font: inherit;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
.lk-palette {
|
|
2
2
|
display: grid;
|
|
3
3
|
grid-template-columns: repeat(2, 1fr);
|
|
4
|
-
gap: var(--
|
|
5
|
-
padding: var(--
|
|
4
|
+
gap: var(--wzl-space-sm);
|
|
5
|
+
padding: var(--wzl-space-sm);
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
.lk-palette__item {
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
align-items: center;
|
|
11
11
|
justify-content: center;
|
|
12
12
|
min-height: 48px;
|
|
13
|
-
padding: var(--
|
|
14
|
-
background: var(--
|
|
15
|
-
border: 1px solid var(--
|
|
13
|
+
padding: var(--wzl-space-sm);
|
|
14
|
+
background: var(--wzl-surface-raised);
|
|
15
|
+
border: 1px solid var(--wzl-border);
|
|
16
16
|
border-radius: 4px;
|
|
17
17
|
cursor: grab;
|
|
18
18
|
font: inherit;
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
touch-action: none;
|
|
22
22
|
|
|
23
23
|
&:hover {
|
|
24
|
-
background: var(--
|
|
24
|
+
background: var(--wzl-surface-hover);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
&:active {
|
package/src/index.ts
CHANGED
|
@@ -56,6 +56,7 @@ export {
|
|
|
56
56
|
export type {
|
|
57
57
|
CreateLabStoreOptions,
|
|
58
58
|
ExperimentStateHandle,
|
|
59
|
+
LabMode,
|
|
59
60
|
LabStoreState,
|
|
60
61
|
SavedSnapshot,
|
|
61
62
|
StorageAdapter,
|
|
@@ -63,6 +64,7 @@ export type {
|
|
|
63
64
|
WorkspaceRecord,
|
|
64
65
|
} from './state/types';
|
|
65
66
|
export { useExperimentState } from './state/useExperimentState';
|
|
67
|
+
export { interstellarTheme } from './theme/interstellar';
|
|
66
68
|
export * from './ui/layers';
|
|
67
69
|
export * from './ui/properties';
|
|
68
70
|
export type { EventBus, EventListener } from './undo';
|
package/src/lab/Lab.test.tsx
CHANGED
|
@@ -89,27 +89,32 @@ describe('<Lab>', () => {
|
|
|
89
89
|
expect(reset?.state).toEqual({ value: 0 });
|
|
90
90
|
});
|
|
91
91
|
|
|
92
|
-
it('
|
|
93
|
-
const { container } = mountLab({
|
|
94
|
-
expect(container.querySelector('.lk-lab')?.
|
|
92
|
+
it('mode="light" stamps light on the lab root', () => {
|
|
93
|
+
const { container } = mountLab({ mode: 'light' });
|
|
94
|
+
expect(container.querySelector('.lk-lab')?.getAttribute('data-wzl-mode')).toBe('light');
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
-
it('
|
|
98
|
-
const { container } = mountLab({
|
|
99
|
-
expect(container.querySelector('.lk-lab')?.
|
|
97
|
+
it('mode="dark" stamps dark on the lab root', () => {
|
|
98
|
+
const { container } = mountLab({ mode: 'dark' });
|
|
99
|
+
expect(container.querySelector('.lk-lab')?.getAttribute('data-wzl-mode')).toBe('dark');
|
|
100
100
|
});
|
|
101
101
|
|
|
102
|
-
it('
|
|
103
|
-
const { container } = mountLab({
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
102
|
+
it('mode="auto" resolves to a concrete mode', () => {
|
|
103
|
+
const { container } = mountLab({ mode: 'auto' });
|
|
104
|
+
expect(container.querySelector('.lk-lab')?.getAttribute('data-wzl-mode')).toMatch(
|
|
105
|
+
/^(light|dark)$/,
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('applies the interstellar theme in every mode', () => {
|
|
110
|
+
const { container } = mountLab({ mode: 'light' });
|
|
111
|
+
expect(container.querySelector('.lk-lab')?.getAttribute('data-wzl-theme')).toBe('interstellar');
|
|
107
112
|
});
|
|
108
113
|
|
|
109
|
-
it('
|
|
110
|
-
const { container } = mountLab({
|
|
111
|
-
act(() => labRef?.
|
|
112
|
-
expect(container.querySelector('.lk-lab')?.
|
|
114
|
+
it('setMode updates the stamped mode at runtime', () => {
|
|
115
|
+
const { container } = mountLab({ mode: 'auto' });
|
|
116
|
+
act(() => labRef?.setMode('light'));
|
|
117
|
+
expect(container.querySelector('.lk-lab')?.getAttribute('data-wzl-mode')).toBe('light');
|
|
113
118
|
});
|
|
114
119
|
|
|
115
120
|
it('throws when instruments is empty', () => {
|
package/src/lab/Lab.tsx
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { type CSSProperties, type ReactNode, useEffect, useMemo, useRef } from 'react';
|
|
2
2
|
import { useStore } from 'zustand/react';
|
|
3
|
+
import { ThemeProvider } from '@weasel-js/theme/react';
|
|
3
4
|
import type { Instrument } from '../instrument/types';
|
|
4
5
|
import { noneAdapter } from '../state/adapters';
|
|
5
6
|
import { LabStoreContext } from '../state/context';
|
|
6
7
|
import { createLabStore, type LabStore } from '../state/store';
|
|
7
|
-
import type { StorageAdapter, WorkspaceRecord } from '../state/types';
|
|
8
|
+
import type { LabMode, StorageAdapter, WorkspaceRecord } from '../state/types';
|
|
9
|
+
import { interstellarTheme } from '../theme/interstellar';
|
|
10
|
+
import { useResolvedMode } from './useSystemMode';
|
|
8
11
|
import { Workspace } from '../workspace/Workspace';
|
|
9
12
|
import {
|
|
10
13
|
addWorkspace as addWorkspaceOp,
|
|
@@ -21,12 +24,12 @@ export interface LabProps {
|
|
|
21
24
|
defaultInstrument: string;
|
|
22
25
|
storage?: StorageAdapter | null;
|
|
23
26
|
storageKey?: string;
|
|
24
|
-
|
|
27
|
+
mode?: LabMode;
|
|
25
28
|
/**
|
|
26
29
|
* Optional list of CSS colors used to compose the interstellar theme's
|
|
27
30
|
* cosmic backdrop. Each color becomes one radial-gradient blob on the
|
|
28
31
|
* dark void base. Order maps to a fixed spread of positions; extras wrap
|
|
29
|
-
* around. Ignored unless
|
|
32
|
+
* around. Ignored unless the resolved mode is dark.
|
|
30
33
|
*/
|
|
31
34
|
nebula?: readonly string[];
|
|
32
35
|
title?: string;
|
|
@@ -38,9 +41,9 @@ function buildStore(
|
|
|
38
41
|
defaultInstrument: string,
|
|
39
42
|
storage: StorageAdapter,
|
|
40
43
|
storageKey: string,
|
|
41
|
-
|
|
44
|
+
initialMode: LabMode,
|
|
42
45
|
): LabStore {
|
|
43
|
-
const store = createLabStore({ storageKey, storage,
|
|
46
|
+
const store = createLabStore({ storageKey, storage, initialMode });
|
|
44
47
|
if (store.getState().workspaces.length === 0) {
|
|
45
48
|
const seeded = addWorkspaceOp([], instruments, defaultInstrument);
|
|
46
49
|
const record = seeded[0];
|
|
@@ -78,7 +81,7 @@ export function Lab({
|
|
|
78
81
|
defaultInstrument,
|
|
79
82
|
storage,
|
|
80
83
|
storageKey,
|
|
81
|
-
|
|
84
|
+
mode,
|
|
82
85
|
nebula,
|
|
83
86
|
title,
|
|
84
87
|
children,
|
|
@@ -94,20 +97,21 @@ export function Lab({
|
|
|
94
97
|
defaultInstrument,
|
|
95
98
|
storage ?? noneAdapter,
|
|
96
99
|
storageKey ?? 'labkit',
|
|
97
|
-
|
|
100
|
+
mode ?? 'auto',
|
|
98
101
|
);
|
|
99
102
|
}
|
|
100
103
|
const store = storeRef.current;
|
|
101
104
|
|
|
102
105
|
const workspaces = useStore(store, (s) => s.workspaces);
|
|
103
106
|
const savedSnapshots = useStore(store, (s) => s.savedSnapshots);
|
|
104
|
-
const
|
|
107
|
+
const modeValue = useStore(store, (s) => s.mode);
|
|
108
|
+
const resolvedMode = useResolvedMode(modeValue);
|
|
105
109
|
|
|
106
110
|
useEffect(() => {
|
|
107
|
-
if (
|
|
108
|
-
store.getState().
|
|
111
|
+
if (mode && mode !== store.getState().mode) {
|
|
112
|
+
store.getState().setMode(mode);
|
|
109
113
|
}
|
|
110
|
-
}, [
|
|
114
|
+
}, [mode, store]);
|
|
111
115
|
|
|
112
116
|
const contextValue = useMemo<LabContextValue>(() => {
|
|
113
117
|
const replaceWorkspaces = (next: WorkspaceRecord[]): void => {
|
|
@@ -115,7 +119,7 @@ export function Lab({
|
|
|
115
119
|
const merged = next.map((w) => currentById.get(w.id) ?? w);
|
|
116
120
|
store.setState({ workspaces: merged });
|
|
117
121
|
// Trigger persistence flush via a tracked action.
|
|
118
|
-
store.getState().
|
|
122
|
+
store.getState().setMode(store.getState().mode);
|
|
119
123
|
};
|
|
120
124
|
|
|
121
125
|
return {
|
|
@@ -156,40 +160,37 @@ export function Lab({
|
|
|
156
160
|
deleteSnapshot: (snapshotId) => {
|
|
157
161
|
store.getState().deleteSnapshot(snapshotId);
|
|
158
162
|
},
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
store.getState().
|
|
163
|
+
mode: modeValue,
|
|
164
|
+
setMode: (m) => {
|
|
165
|
+
store.getState().setMode(m);
|
|
162
166
|
},
|
|
163
167
|
};
|
|
164
|
-
}, [instruments, workspaces, savedSnapshots,
|
|
168
|
+
}, [instruments, workspaces, savedSnapshots, modeValue, store]);
|
|
165
169
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
: '';
|
|
172
|
-
|
|
173
|
-
// Only apply the nebula override when interstellar is active and at
|
|
174
|
-
// least one color is provided. Setting a CSS custom property is the
|
|
175
|
-
// sanctioned use of inline style.
|
|
176
|
-
const nebulaStyle =
|
|
177
|
-
themeValue === 'interstellar' && nebula && nebula.length > 0
|
|
178
|
-
? ({ ['--lk-space-nebula' as string]: buildNebula(nebula) } as CSSProperties)
|
|
170
|
+
// Only override the backdrop in dark, where there is one to override.
|
|
171
|
+
// Setting a CSS custom property is the sanctioned use of inline style.
|
|
172
|
+
const backdropStyle =
|
|
173
|
+
resolvedMode === 'dark' && nebula && nebula.length > 0
|
|
174
|
+
? ({ ['--wzl-backdrop' as string]: buildNebula(nebula) } as CSSProperties)
|
|
179
175
|
: undefined;
|
|
180
176
|
|
|
181
177
|
return (
|
|
182
178
|
<LabStoreContext.Provider value={{ store }}>
|
|
183
179
|
<LabContext.Provider value={contextValue}>
|
|
184
|
-
<
|
|
185
|
-
|
|
180
|
+
<ThemeProvider
|
|
181
|
+
theme={interstellarTheme}
|
|
182
|
+
mode={resolvedMode}
|
|
183
|
+
className="lk-lab"
|
|
184
|
+
style={backdropStyle}
|
|
185
|
+
>
|
|
186
|
+
<LabShell title={title ?? 'Labkit'} mode={modeValue} header={children}>
|
|
186
187
|
<WorkspaceGrid>
|
|
187
188
|
{workspaces.map((w) => (
|
|
188
189
|
<Workspace key={w.id} id={w.id} />
|
|
189
190
|
))}
|
|
190
191
|
</WorkspaceGrid>
|
|
191
192
|
</LabShell>
|
|
192
|
-
</
|
|
193
|
+
</ThemeProvider>
|
|
193
194
|
</LabContext.Provider>
|
|
194
195
|
</LabStoreContext.Provider>
|
|
195
196
|
);
|
package/src/lab/LabContext.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createContext, useContext } from 'react';
|
|
2
2
|
import type { Instrument } from '../instrument/types';
|
|
3
|
-
import type { SavedSnapshot, WorkspaceRecord } from '../state/types';
|
|
3
|
+
import type { LabMode, SavedSnapshot, WorkspaceRecord } from '../state/types';
|
|
4
4
|
|
|
5
5
|
export interface LabContextValue {
|
|
6
6
|
instruments: Instrument[];
|
|
@@ -13,8 +13,8 @@ export interface LabContextValue {
|
|
|
13
13
|
saveSnapshot: (workspaceId: string, name?: string) => void;
|
|
14
14
|
loadSnapshot: (workspaceId: string, snapshotId: string) => void;
|
|
15
15
|
deleteSnapshot: (snapshotId: string) => void;
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
mode: LabMode;
|
|
17
|
+
setMode: (m: LabMode) => void;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export const LabContext = createContext<LabContextValue | null>(null);
|
package/src/lab/LabShell.less
CHANGED
|
@@ -3,43 +3,43 @@
|
|
|
3
3
|
flex-direction: column;
|
|
4
4
|
width: 100%;
|
|
5
5
|
height: 100vh;
|
|
6
|
-
background: var(--
|
|
6
|
+
background: var(--wzl-surface);
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
.lk-shell-header {
|
|
10
10
|
display: flex;
|
|
11
11
|
align-items: center;
|
|
12
|
-
gap: var(--
|
|
13
|
-
padding: var(--
|
|
14
|
-
background: var(--
|
|
15
|
-
border-bottom: 1px solid var(--
|
|
16
|
-
z-index: var(--
|
|
12
|
+
gap: var(--wzl-space-md);
|
|
13
|
+
padding: var(--wzl-space-sm) var(--wzl-space-md);
|
|
14
|
+
background: var(--wzl-surface-raised);
|
|
15
|
+
border-bottom: 1px solid var(--wzl-border);
|
|
16
|
+
z-index: var(--wzl-z-toolbar);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
.lk-shell-title {
|
|
20
20
|
margin: 0;
|
|
21
21
|
font-size: 16px;
|
|
22
22
|
font-weight: 600;
|
|
23
|
-
color: var(--
|
|
23
|
+
color: var(--wzl-fg);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
.lk-shell-header-actions {
|
|
27
27
|
margin-left: auto;
|
|
28
28
|
display: flex;
|
|
29
|
-
gap: var(--
|
|
29
|
+
gap: var(--wzl-space-sm);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
.lk-shell-body {
|
|
33
33
|
flex: 1;
|
|
34
34
|
min-height: 0;
|
|
35
35
|
overflow: auto;
|
|
36
|
-
padding: var(--
|
|
36
|
+
padding: var(--wzl-space-md);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
.lk-shell-footer {
|
|
40
|
-
border-top: 1px solid var(--
|
|
41
|
-
padding: var(--
|
|
42
|
-
background: var(--
|
|
43
|
-
color: var(--
|
|
44
|
-
font-size: var(--
|
|
40
|
+
border-top: 1px solid var(--wzl-border);
|
|
41
|
+
padding: var(--wzl-space-xs) var(--wzl-space-md);
|
|
42
|
+
background: var(--wzl-surface-raised);
|
|
43
|
+
color: var(--wzl-fg-muted);
|
|
44
|
+
font-size: var(--wzl-font-size-sm);
|
|
45
45
|
}
|
|
@@ -22,38 +22,25 @@ describe('LabShell', () => {
|
|
|
22
22
|
expect(screen.getByRole('button', { name: 'action' })).toBeInTheDocument();
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
-
test('
|
|
25
|
+
test('stamps the requested mode under the interstellar theme', () => {
|
|
26
26
|
const { container } = render(
|
|
27
|
-
<LabShell title="t"
|
|
27
|
+
<LabShell title="t" mode="light">
|
|
28
28
|
x
|
|
29
29
|
</LabShell>,
|
|
30
30
|
);
|
|
31
31
|
const root = container.firstChild as HTMLElement;
|
|
32
|
-
expect(root.
|
|
33
|
-
expect(root.
|
|
32
|
+
expect(root.getAttribute('data-wzl-theme')).toBe('interstellar');
|
|
33
|
+
expect(root.getAttribute('data-wzl-mode')).toBe('light');
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
-
test('
|
|
37
|
-
const { container } = render(
|
|
38
|
-
<LabShell title="t" theme="interstellar">
|
|
39
|
-
x
|
|
40
|
-
</LabShell>,
|
|
41
|
-
);
|
|
42
|
-
const root = container.firstChild as HTMLElement;
|
|
43
|
-
expect(root.classList.contains('lk-theme-interstellar')).toBe(true);
|
|
44
|
-
expect(root.classList.contains('lk-theme-light')).toBe(false);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test('applies neither theme class when theme="auto" (default)', () => {
|
|
36
|
+
test('resolves mode="auto" to a concrete mode', () => {
|
|
48
37
|
const { container } = render(<LabShell title="t">x</LabShell>);
|
|
49
38
|
const root = container.firstChild as HTMLElement;
|
|
50
|
-
expect(root.
|
|
51
|
-
expect(root.classList.contains('lk-theme-interstellar')).toBe(false);
|
|
39
|
+
expect(root.getAttribute('data-wzl-mode')).toMatch(/^(light|dark)$/);
|
|
52
40
|
});
|
|
53
41
|
|
|
54
42
|
test('always applies lk-root class', () => {
|
|
55
43
|
const { container } = render(<LabShell title="t">x</LabShell>);
|
|
56
|
-
|
|
57
|
-
expect(root.classList.contains('lk-root')).toBe(true);
|
|
44
|
+
expect(container.querySelector('.lk-root')).not.toBeNull();
|
|
58
45
|
});
|
|
59
46
|
});
|
package/src/lab/LabShell.tsx
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import { ThemeProvider, useThemeOptional } from '@weasel-js/theme/react';
|
|
3
|
+
import type { LabMode } from '../state/types';
|
|
4
|
+
import { interstellarTheme } from '../theme/interstellar';
|
|
5
|
+
import { useResolvedMode } from './useSystemMode';
|
|
4
6
|
|
|
5
7
|
export interface LabShellProps {
|
|
6
8
|
title: string;
|
|
@@ -9,19 +11,16 @@ export interface LabShellProps {
|
|
|
9
11
|
header?: ReactNode;
|
|
10
12
|
/** Optional content rendered into the footer. */
|
|
11
13
|
footer?: ReactNode;
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
+
/** Color mode. "auto" (default) follows prefers-color-scheme. */
|
|
15
|
+
mode?: LabMode;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
|
-
export function LabShell({ title, children, header, footer,
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
: '';
|
|
23
|
-
return (
|
|
24
|
-
<div className={`lk-root lk-shell${themeClass}`}>
|
|
18
|
+
export function LabShell({ title, children, header, footer, mode = 'auto' }: LabShellProps) {
|
|
19
|
+
const resolved = useResolvedMode(mode);
|
|
20
|
+
const outer = useThemeOptional();
|
|
21
|
+
|
|
22
|
+
const shell = (
|
|
23
|
+
<div className="lk-root lk-shell">
|
|
25
24
|
<header className="lk-shell-header">
|
|
26
25
|
<h1 className="lk-shell-title">{title}</h1>
|
|
27
26
|
{header && <div className="lk-shell-header-actions">{header}</div>}
|
|
@@ -30,4 +29,14 @@ export function LabShell({ title, children, header, footer, theme = 'auto' }: La
|
|
|
30
29
|
{footer && <footer className="lk-shell-footer">{footer}</footer>}
|
|
31
30
|
</div>
|
|
32
31
|
);
|
|
32
|
+
|
|
33
|
+
// Inside <Lab> the theme is already applied on `.lk-lab`; wrapping again
|
|
34
|
+
// would only add a div.
|
|
35
|
+
return outer ? (
|
|
36
|
+
shell
|
|
37
|
+
) : (
|
|
38
|
+
<ThemeProvider theme={interstellarTheme} mode={resolved}>
|
|
39
|
+
{shell}
|
|
40
|
+
</ThemeProvider>
|
|
41
|
+
);
|
|
33
42
|
}
|
|
@@ -12,10 +12,10 @@ type Story = StoryObj<typeof WorkspaceGrid>;
|
|
|
12
12
|
const Tile = ({ children }: { children: React.ReactNode }) => (
|
|
13
13
|
<div
|
|
14
14
|
style={{
|
|
15
|
-
background: 'var(--
|
|
16
|
-
border: '1px solid var(--
|
|
17
|
-
borderRadius: 'var(--
|
|
18
|
-
padding: 'var(--
|
|
15
|
+
background: 'var(--wzl-surface-raised)',
|
|
16
|
+
border: '1px solid var(--wzl-border)',
|
|
17
|
+
borderRadius: 'var(--wzl-radius-md)',
|
|
18
|
+
padding: 'var(--wzl-space-md)',
|
|
19
19
|
minHeight: 120,
|
|
20
20
|
display: 'grid',
|
|
21
21
|
placeItems: 'center',
|
package/src/lab/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ export type { LabProps } from './Lab';
|
|
|
4
4
|
export { Lab } from './Lab';
|
|
5
5
|
export type { LabContextValue } from './LabContext';
|
|
6
6
|
export { LabContext, useLabContext } from './LabContext';
|
|
7
|
-
export type { LabShellProps
|
|
7
|
+
export type { LabShellProps } from './LabShell';
|
|
8
8
|
export { LabShell } from './LabShell';
|
|
9
9
|
export type { WorkspaceGridProps } from './WorkspaceGrid';
|
|
10
10
|
export { WorkspaceGrid } from './WorkspaceGrid';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import type { LabMode } from '../state/types';
|
|
3
|
+
|
|
4
|
+
function query(): MediaQueryList | null {
|
|
5
|
+
if (typeof window === 'undefined' || !window.matchMedia) return null;
|
|
6
|
+
return window.matchMedia('(prefers-color-scheme: light)');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolve a `LabMode` to the mode the theme resolver understands.
|
|
11
|
+
*
|
|
12
|
+
* `auto` is not a mode — it means "read the OS", live. An explicit choice
|
|
13
|
+
* opts out of the subscription entirely.
|
|
14
|
+
*/
|
|
15
|
+
export function useResolvedMode(mode: LabMode): 'light' | 'dark' {
|
|
16
|
+
const [system, setSystem] = useState<'light' | 'dark'>(() =>
|
|
17
|
+
query()?.matches ? 'light' : 'dark',
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (mode !== 'auto') return;
|
|
22
|
+
const mq = query();
|
|
23
|
+
if (!mq) return;
|
|
24
|
+
const onChange = (e: MediaQueryListEvent) => setSystem(e.matches ? 'light' : 'dark');
|
|
25
|
+
mq.addEventListener('change', onChange);
|
|
26
|
+
return () => mq.removeEventListener('change', onChange);
|
|
27
|
+
}, [mode]);
|
|
28
|
+
|
|
29
|
+
return mode === 'auto' ? system : mode;
|
|
30
|
+
}
|