@weasel-js/labkit 1.5.0 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -4
- package/dist/_dts/{CanvasStackContext-CqkxkFJW.d.ts → CanvasStackContext-CJNMS6mz.d.ts} +1 -1
- package/dist/_dts/{PrefsForm.d-DDHFANkc.d.ts → PrefsForm.d-DPy9I8It.d.ts} +4 -0
- package/dist/_dts/Select.d-Gwr8fLpK.d.ts +453 -0
- package/dist/_dts/{frac-8R6UmlvN.d.ts → frac-B4okC2lX.d.ts} +42 -4
- package/dist/_dts/{index-retnAut7.d.ts → index-BNdz-tNd.d.ts} +90 -10
- package/dist/_dts/{index-B8uu2ba7.d.ts → index-CSJoj4LC.d.ts} +17 -6
- package/dist/_dts/{types-Ca2LCnPq.d.ts → types-DzoNY1pO.d.ts} +28 -4
- package/dist/_dts/{usePanZoom-nS798rOF.d.ts → usePanZoom-Pzf9f2jh.d.ts} +2 -2
- package/dist/_dts/{useTrialState-jcx_TdU6.d.ts → useTrialState-uLlmaccn.d.ts} +1 -1
- package/dist/canvas/index.d.ts +5 -5
- package/dist/canvas/index.js +1 -1
- package/dist/chrome/index.d.ts +4 -4
- package/dist/chrome/index.js +7 -7
- package/dist/chunk-7PFH3CNM.js +150 -0
- package/dist/chunk-7PFH3CNM.js.map +1 -0
- package/dist/{chunk-PGETSEDK.js → chunk-CEIUBPPW.js} +10 -6
- package/dist/chunk-CEIUBPPW.js.map +1 -0
- package/dist/chunk-EH7UEQV7.js +590 -0
- package/dist/chunk-EH7UEQV7.js.map +1 -0
- package/dist/{chunk-EKIICY6X.js → chunk-EHGVROVV.js} +37 -25
- package/dist/chunk-EHGVROVV.js.map +1 -0
- package/dist/{chunk-ZPH5WTWF.js → chunk-ERFMC6WU.js} +142 -66
- package/dist/chunk-ERFMC6WU.js.map +1 -0
- package/dist/{chunk-NS64DXMN.js → chunk-IHUVQ3TX.js} +39 -16
- package/dist/chunk-IHUVQ3TX.js.map +1 -0
- package/dist/{chunk-QLSV2N3G.js → chunk-IJJUK4FU.js} +25 -13
- package/dist/chunk-IJJUK4FU.js.map +1 -0
- package/dist/{chunk-67SJLMC7.js → chunk-IXTQI7JX.js} +45 -12
- package/dist/chunk-IXTQI7JX.js.map +1 -0
- package/dist/{chunk-MUKOW3TC.js → chunk-MDVTK5U3.js} +4 -4
- package/dist/{chunk-MUKOW3TC.js.map → chunk-MDVTK5U3.js.map} +1 -1
- package/dist/{chunk-UU3NJO6N.js → chunk-PHK4W562.js} +3 -3
- package/dist/{chunk-UU3NJO6N.js.map → chunk-PHK4W562.js.map} +1 -1
- package/dist/chunk-PJ7WEQBJ.js +18 -0
- package/dist/chunk-PJ7WEQBJ.js.map +1 -0
- package/dist/chunk-T3EC2DXD.js +12628 -0
- package/dist/chunk-T3EC2DXD.js.map +1 -0
- package/dist/{chunk-KJALCDWE.js → chunk-UN4U4P63.js} +4 -4
- package/dist/{chunk-KJALCDWE.js.map → chunk-UN4U4P63.js.map} +1 -1
- package/dist/{chunk-NDRYLVOW.js → chunk-Z76KEZAX.js} +2 -2
- package/dist/chunk-Z76KEZAX.js.map +1 -0
- package/dist/{chunk-6ZDGOZQV.js → chunk-ZDHY47EV.js} +70 -17
- package/dist/chunk-ZDHY47EV.js.map +1 -0
- package/dist/config/index.d.ts +60 -16
- package/dist/config/index.js +6 -4
- package/dist/controls/index.d.ts +3 -3
- package/dist/controls/index.js +4 -4
- package/dist/dragdrop/index.d.ts +3 -3
- package/dist/index.d.ts +177 -53
- package/dist/index.js +1333 -1683
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +4 -4
- package/dist/layers/index.js +2 -2
- package/dist/loupe/index.d.ts +67 -16
- package/dist/loupe/index.js +1 -1
- package/dist/passthrough/weasel-ui.d.ts +26 -202
- package/dist/passthrough/weasel-ui.js +1 -1
- package/dist/primitives/index.d.ts +3 -1
- package/dist/primitives/index.js +3 -3
- package/dist/state/index.d.ts +5 -5
- package/dist/state/index.js +5 -3
- package/dist/state/index.js.map +1 -1
- package/dist/styles.css +153 -40
- package/dist/surface/index.js +1 -1
- package/dist/ui/layers/index.js +1 -1
- package/dist/undo/index.d.ts +3 -3
- package/package.json +9 -9
- package/src/annotations/AnnotationOverlay.tsx +3 -0
- package/src/annotations/Annotations.less +4 -0
- package/src/annotations/Annotations.overlay.test.tsx +20 -0
- package/src/annotations/svgNodes.ts +1 -0
- package/src/annotations/toolMap.test.ts +9 -5
- package/src/annotations/toolMap.ts +3 -0
- package/src/canvas/CanvasStack.stories.tsx +1 -1
- package/src/canvas/usePanZoom.test.ts +28 -0
- package/src/canvas/usePanZoom.ts +11 -4
- package/src/chrome/ChromeRegions.stories.tsx +1 -1
- package/src/chrome/LabChrome.tsx +21 -5
- package/src/chrome/builtins.test.ts +1 -0
- package/src/chrome/builtins.tsx +1 -0
- package/src/chrome/index.ts +1 -0
- package/src/chrome/labTypes.ts +2 -2
- package/src/chrome/regions/PaletteRegion.tsx +1 -6
- package/src/chrome/regions/SidebarRegion.stories.tsx +44 -0
- package/src/chrome/regions/ToolbarRegion.tsx +11 -7
- package/src/chrome/types.ts +3 -0
- package/src/config/auto.test.ts +12 -0
- package/src/config/auto.ts +15 -0
- package/src/config/autoConfig.test.ts +107 -0
- package/src/config/autoConfig.ts +153 -0
- package/src/config/builder.test.ts +38 -0
- package/src/config/builder.ts +53 -8
- package/src/config/index.ts +3 -0
- package/src/config/path.ts +7 -2
- package/src/config/resolve.test.ts +25 -0
- package/src/config/resolve.ts +17 -1
- package/src/config/types.ts +32 -2
- package/src/config/useResolvedConfig.ts +26 -0
- package/src/controls/AutoControls.stories.tsx +88 -0
- package/src/controls/ControlPanel.stories.tsx +42 -1
- package/src/controls/ControlPanel.test.tsx +514 -4
- package/src/controls/ControlPanel.tsx +557 -80
- package/src/index.ts +29 -1
- package/src/instrument/serializers.ts +29 -1
- package/src/lab/Lab.chrome.test.tsx +16 -0
- package/src/lab/Lab.stories.tsx +1 -1
- package/src/lab/Lab.surface.test.tsx +7 -2
- package/src/lab/Lab.test.tsx +79 -1
- package/src/lab/Lab.tsx +105 -13
- package/src/lab/LabContext.ts +8 -0
- package/src/lab/LabFit.stories.tsx +4 -4
- package/src/lab/LabFullChrome.stories.tsx +1 -1
- package/src/lab/LabHeader.tsx +0 -1
- package/src/lab/LabRoot.test.tsx +56 -0
- package/src/lab/LabRoot.tsx +47 -0
- package/src/lab/LabShell.less +24 -3
- package/src/lab/LabShell.stories.tsx +50 -1
- package/src/lab/LabShell.tsx +18 -27
- package/src/lab/LabSwitcher.less +10 -10
- package/src/lab/LabSwitcher.test.tsx +14 -9
- package/src/lab/LabSwitcher.tsx +8 -4
- package/src/lab/Workspace.less +1 -1
- package/src/lab/Workspace.stories.tsx +1 -1
- package/src/lab/Workspace.surface.test.tsx +1 -1
- package/src/lab/index.ts +2 -0
- package/src/layers/LayerList.stories.tsx +1 -1
- package/src/loupe/AGENTS.md +16 -4
- package/src/loupe/LoupeGestures.tsx +67 -0
- package/src/loupe/TrialLoupe.tsx +43 -28
- package/src/loupe/index.ts +4 -0
- package/src/loupe/loupeActions.ts +98 -0
- package/src/loupe/useLoupe.test.tsx +59 -2
- package/src/loupe/useLoupe.ts +28 -48
- package/src/passthrough/weasel-ui.ts +3 -3
- package/src/primitives/FloatingPanel.stories.tsx +1 -1
- package/src/primitives/FpsMeter.stories.tsx +1 -1
- package/src/primitives/JobProgress.stories.tsx +1 -1
- package/src/primitives/Legend.stories.tsx +1 -1
- package/src/primitives/ScaleIndicator.stories.tsx +1 -1
- package/src/primitives/Sidebar.less +7 -0
- package/src/primitives/Sidebar.stories.tsx +1 -1
- package/src/primitives/Split.test.tsx +25 -0
- package/src/primitives/Split.tsx +36 -20
- package/src/primitives/StatusBar.stories.tsx +1 -1
- package/src/primitives/Toolbar.stories.tsx +1 -1
- package/src/specimen/Specimen.less +95 -0
- package/src/specimen/Specimen.stories.tsx +13 -0
- package/src/specimen/Specimen.test.tsx +13 -0
- package/src/specimen/Specimen.tsx +888 -0
- package/src/state/Persistence.stories.tsx +4 -4
- package/src/state/store.test.ts +65 -0
- package/src/state/store.ts +32 -8
- package/src/state/types.ts +26 -2
- package/src/state/useTrialState.test.tsx +85 -0
- package/src/state/useTrialState.ts +9 -1
- package/src/styles.less +1 -0
- package/src/surface/index.ts +1 -1
- package/src/surface/useSurfaceTile.test.tsx +1 -1
- package/src/surface/useTiledSurface.test.tsx +65 -10
- package/src/surface/useTiledSurface.ts +23 -11
- package/src/theme/Interstellar.stories.tsx +2 -2
- package/src/theme/interstellar.test.ts +16 -5
- package/src/theme/interstellar.theme.json +180 -0
- package/src/theme/interstellar.ts +8 -5
- package/src/tools/labTool.ts +3 -3
- package/src/trial/Trial.annotations.test.tsx +13 -4
- package/src/trial/Trial.canvas.test.tsx +32 -0
- package/src/trial/Trial.config.test.tsx +75 -2
- package/src/trial/Trial.less +4 -0
- package/src/trial/Trial.stories.tsx +4 -1
- package/src/trial/Trial.test.tsx +3 -0
- package/src/trial/Trial.tsx +21 -14
- package/src/trial/TrialChrome.tsx +17 -3
- package/src/trial/trialOps.test.ts +101 -1
- package/src/trial/trialOps.ts +62 -4
- package/dist/_dts/ToggleBar.d-DbHoWKIA.d.ts +0 -116
- package/dist/chunk-67SJLMC7.js.map +0 -1
- package/dist/chunk-6ZDGOZQV.js.map +0 -1
- package/dist/chunk-E44SU6XS.js +0 -50
- package/dist/chunk-E44SU6XS.js.map +0 -1
- package/dist/chunk-EKIICY6X.js.map +0 -1
- package/dist/chunk-NDRYLVOW.js.map +0 -1
- package/dist/chunk-NS64DXMN.js.map +0 -1
- package/dist/chunk-PGETSEDK.js.map +0 -1
- package/dist/chunk-QLSV2N3G.js.map +0 -1
- package/dist/chunk-S2SHZRD7.js +0 -7681
- package/dist/chunk-S2SHZRD7.js.map +0 -1
- package/dist/chunk-TQBBYTAF.js +0 -280
- package/dist/chunk-TQBBYTAF.js.map +0 -1
- package/dist/chunk-ZPH5WTWF.js.map +0 -1
- package/src/theme/interstellar.tokens.json +0 -61
package/src/index.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
// The property-panel family, LayerStack, ToggleBar and Button live in
|
|
1
|
+
// The property-panel family, LayerStack, 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.
|
|
5
|
+
// The playback glyphs ride along for the same reason: chrome that labels a control with
|
|
6
|
+
// one should not need `@weasel-js/ui` in its manifest to draw a play button.
|
|
5
7
|
export type {
|
|
6
8
|
ButtonProps,
|
|
7
9
|
ButtonSize,
|
|
@@ -13,6 +15,9 @@ export type {
|
|
|
13
15
|
EffectCardListItem,
|
|
14
16
|
EffectCardListProps,
|
|
15
17
|
EffectCardProps,
|
|
18
|
+
IconProps,
|
|
19
|
+
InputProps,
|
|
20
|
+
JogProps,
|
|
16
21
|
LayerStackItem,
|
|
17
22
|
LayerStackProps,
|
|
18
23
|
NumberRowProps,
|
|
@@ -25,6 +30,7 @@ export type {
|
|
|
25
30
|
PropertyRowProps,
|
|
26
31
|
PropertyRowVariant,
|
|
27
32
|
PropertySpanProps,
|
|
33
|
+
SelectProps,
|
|
28
34
|
SelectRowProps,
|
|
29
35
|
SliderRowProps,
|
|
30
36
|
SubpanelProps,
|
|
@@ -34,27 +40,45 @@ export type {
|
|
|
34
40
|
ToggleBarSize,
|
|
35
41
|
ToggleBarVariant,
|
|
36
42
|
ToggleRowProps,
|
|
43
|
+
TokenCategory,
|
|
44
|
+
TokenEntry,
|
|
45
|
+
TokenPanelProps,
|
|
37
46
|
} from '@weasel-js/ui';
|
|
38
47
|
export {
|
|
39
48
|
Button,
|
|
40
49
|
CheckboxRow,
|
|
50
|
+
CloseIcon,
|
|
51
|
+
CollapseIcon,
|
|
41
52
|
ColorRow,
|
|
42
53
|
CurveField,
|
|
43
54
|
EffectCard,
|
|
44
55
|
EffectCardList,
|
|
56
|
+
ExpandIcon,
|
|
57
|
+
Icon,
|
|
58
|
+
Input,
|
|
59
|
+
inferTokenType,
|
|
60
|
+
Jog,
|
|
45
61
|
LayerStack,
|
|
46
62
|
NumberRow,
|
|
63
|
+
PauseIcon,
|
|
64
|
+
PlayIcon,
|
|
47
65
|
PropertyGroup,
|
|
48
66
|
PropertyList,
|
|
49
67
|
PropertyPanel,
|
|
50
68
|
PropertyRow,
|
|
51
69
|
PropertySpan,
|
|
70
|
+
ResetIcon,
|
|
71
|
+
Select,
|
|
52
72
|
SelectRow,
|
|
53
73
|
SliderRow,
|
|
74
|
+
StepBackIcon,
|
|
75
|
+
StepForwardIcon,
|
|
76
|
+
StopIcon,
|
|
54
77
|
Subpanel,
|
|
55
78
|
TextRow,
|
|
56
79
|
ToggleBar,
|
|
57
80
|
ToggleRow,
|
|
81
|
+
TokenPanel,
|
|
58
82
|
} from '@weasel-js/ui';
|
|
59
83
|
export type {
|
|
60
84
|
Annotation,
|
|
@@ -157,6 +181,8 @@ export {
|
|
|
157
181
|
zoomAt,
|
|
158
182
|
} from './canvas';
|
|
159
183
|
export * from './chrome';
|
|
184
|
+
export { type Auto, auto, isAuto } from './config/auto';
|
|
185
|
+
export { autoPathsOf, resolveAutoConfig } from './config/autoConfig';
|
|
160
186
|
export {
|
|
161
187
|
BaseNode,
|
|
162
188
|
BooleanNode,
|
|
@@ -205,6 +231,7 @@ export type {
|
|
|
205
231
|
ValueAtPath,
|
|
206
232
|
} from './config/types';
|
|
207
233
|
export { useConfigSchema } from './config/useConfigSchema';
|
|
234
|
+
export { useResolvedConfig } from './config/useResolvedConfig';
|
|
208
235
|
export { isLeafVisible } from './config/visible';
|
|
209
236
|
export { ControlPanel } from './controls/ControlPanel';
|
|
210
237
|
export type {
|
|
@@ -246,6 +273,7 @@ export type { LayerListProps, LayerTreeNode } from './layers';
|
|
|
246
273
|
export { LayerList } from './layers';
|
|
247
274
|
export * from './loupe';
|
|
248
275
|
export * from './primitives';
|
|
276
|
+
export { SPECIMEN_SECTIONS, Specimen } from './specimen/Specimen';
|
|
249
277
|
export {
|
|
250
278
|
createIndexedDbAdapter,
|
|
251
279
|
createMemoryAdapter,
|
|
@@ -1,5 +1,33 @@
|
|
|
1
|
+
import { resolveConfigSchema } from '../config/resolve';
|
|
2
|
+
import type { ResolvedConfig } from '../config/types';
|
|
1
3
|
import type { InstrumentSerializers } from '../state/types';
|
|
2
|
-
import type { InstrumentList } from './types';
|
|
4
|
+
import type { Instrument, InstrumentList } from './types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* One instrument's config schema, resolved without the lab's rules — which
|
|
8
|
+
* neither the store nor a trial composed outside a lab has access to. No rule
|
|
9
|
+
* can reach `autoResolve`, `unpinned` or `manual`, so for reading those the
|
|
10
|
+
* two resolutions agree; anything reading the rendered leaves wants
|
|
11
|
+
* `useConfigSchema` instead. `undefined` for a legacy `configSchema()`
|
|
12
|
+
* instrument, which declares no auto annotations at all.
|
|
13
|
+
*/
|
|
14
|
+
export function unruledConfigSchema(
|
|
15
|
+
// biome-ignore lint/suspicious/noExplicitAny: instruments are stored contravariantly; see InstrumentList
|
|
16
|
+
instrument: Instrument<any, any, any>, // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
17
|
+
): ResolvedConfig | undefined {
|
|
18
|
+
return instrument.config ? resolveConfigSchema(instrument.config, []) : undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Each instrument's unruled config schema, keyed by name — what the store
|
|
22
|
+
* reads a leaf's auto annotations off. */
|
|
23
|
+
export function configSchemasOf(instruments: InstrumentList): Record<string, ResolvedConfig> {
|
|
24
|
+
const out: Record<string, ResolvedConfig> = {};
|
|
25
|
+
for (const instrument of instruments) {
|
|
26
|
+
const schema = unruledConfigSchema(instrument);
|
|
27
|
+
if (schema) out[instrument.name] = schema;
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
3
31
|
|
|
4
32
|
/**
|
|
5
33
|
* Each instrument's default config, keyed by name — what `createLabStore`
|
|
@@ -62,4 +62,20 @@ describe('<Lab> chrome', () => {
|
|
|
62
62
|
render(<Lab title="T" instruments={[withChrome]} defaultInstrument="Bare" />);
|
|
63
63
|
expect(screen.getByText('ready')).toBeInTheDocument();
|
|
64
64
|
});
|
|
65
|
+
|
|
66
|
+
it('renders an aside contribution in its own pane, after the workspace', () => {
|
|
67
|
+
render(
|
|
68
|
+
<Lab
|
|
69
|
+
title="T"
|
|
70
|
+
instruments={[bare]}
|
|
71
|
+
defaultInstrument="Bare"
|
|
72
|
+
labChrome={[{ id: 'notes', region: 'aside', item: { title: 'Notes', body: <p>jot</p> } }]}
|
|
73
|
+
/>,
|
|
74
|
+
);
|
|
75
|
+
const body = screen.getByText('jot');
|
|
76
|
+
const trial = screen.getByRole('region', { name: /trial/i });
|
|
77
|
+
expect(body.closest('.lk-lab__aside')).not.toBeNull();
|
|
78
|
+
expect(trial.compareDocumentPosition(body) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
|
79
|
+
expect(screen.getByRole('separator', { name: /Lab aside/ })).toBeInTheDocument();
|
|
80
|
+
});
|
|
65
81
|
});
|
package/src/lab/Lab.stories.tsx
CHANGED
|
@@ -12,7 +12,12 @@ import type { ReactNode } from 'react';
|
|
|
12
12
|
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
13
13
|
import { defineInstrument } from '../instrument/defineInstrument';
|
|
14
14
|
import { SurfaceContext } from '../surface/SurfaceContext';
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
useSurfaceCanvas,
|
|
17
|
+
useSurfaceOptional,
|
|
18
|
+
useSurfaceTile,
|
|
19
|
+
useTileId,
|
|
20
|
+
} from '../surface/useSurfaceTile';
|
|
16
21
|
import type { SurfaceFrame, SurfaceHandle } from '../surface/useTiledSurface';
|
|
17
22
|
import { useTiledSurface } from '../surface/useTiledSurface';
|
|
18
23
|
import { Lab } from './Lab';
|
|
@@ -203,7 +208,7 @@ describe('<Lab> tile round trip', () => {
|
|
|
203
208
|
});
|
|
204
209
|
|
|
205
210
|
describe('<Lab> pages', () => {
|
|
206
|
-
it(
|
|
211
|
+
it("gives the title a way back to the project's other labs", () => {
|
|
207
212
|
const { getByRole } = render(
|
|
208
213
|
<Lab
|
|
209
214
|
instruments={[probeInstrument]}
|
package/src/lab/Lab.test.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { act, render, screen, waitFor } from '@testing-library/react';
|
|
1
|
+
import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
|
2
2
|
import { type ReactNode, useEffect } from 'react';
|
|
3
3
|
import { describe, expect, it } from 'vitest';
|
|
4
4
|
import type { Instrument } from '../instrument/types';
|
|
@@ -238,3 +238,81 @@ describe('instruments that change while mounted', () => {
|
|
|
238
238
|
expect(mounts).toBe(1);
|
|
239
239
|
});
|
|
240
240
|
});
|
|
241
|
+
|
|
242
|
+
describe('focused trial', () => {
|
|
243
|
+
const regions = () => screen.getAllByRole('region', { name: /trial/i });
|
|
244
|
+
const framed: Instrument = {
|
|
245
|
+
...stub,
|
|
246
|
+
name: 'Framed',
|
|
247
|
+
render: () => <iframe title="story frame" />,
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
it('is the only trial until another opens, then the one that opened', () => {
|
|
251
|
+
mountLab();
|
|
252
|
+
expect(labRef?.focusedTrialId).toBe(labRef?.trials[0]?.id);
|
|
253
|
+
act(() => labRef?.addTrial('StubB'));
|
|
254
|
+
expect(labRef?.focusedTrialId).toBe(labRef?.trials[1]?.id);
|
|
255
|
+
act(() => labRef?.cloneTrial(labRef?.trials[0]?.id ?? ''));
|
|
256
|
+
expect(labRef?.focusedTrialId).toBe(labRef?.trials[1]?.id);
|
|
257
|
+
expect(labRef?.trials.map((t) => t.instrumentName)).toEqual(['Stub', 'Stub', 'StubB']);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('moves to a trial a pointer goes down in', () => {
|
|
261
|
+
mountLab();
|
|
262
|
+
act(() => labRef?.addTrial('StubB'));
|
|
263
|
+
fireEvent.pointerDown(within(regions()[0] as HTMLElement).getByTestId('stub-content'));
|
|
264
|
+
expect(labRef?.focusedTrialId).toBe(labRef?.trials[0]?.id);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('moves to a trial focus moves into', () => {
|
|
268
|
+
mountLab();
|
|
269
|
+
act(() => labRef?.addTrial('StubB'));
|
|
270
|
+
act(() => (regions()[0] as HTMLElement).focus());
|
|
271
|
+
expect(labRef?.focusedTrialId).toBe(labRef?.trials[0]?.id);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('moves to a trial whose frame takes focus, which fires nothing in the lab’s document', async () => {
|
|
275
|
+
mountLab({ instruments: [framed, stubB], defaultInstrument: 'Framed' });
|
|
276
|
+
act(() => labRef?.addTrial('StubB'));
|
|
277
|
+
act(() => screen.getByTitle('story frame').focus());
|
|
278
|
+
fireEvent.blur(window);
|
|
279
|
+
await waitFor(() => expect(labRef?.focusedTrialId).toBe(labRef?.trials[0]?.id));
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('falls back to the first trial when the focused one closes', () => {
|
|
283
|
+
mountLab();
|
|
284
|
+
act(() => labRef?.addTrial('StubB'));
|
|
285
|
+
act(() => labRef?.closeTrial(labRef?.trials[1]?.id ?? ''));
|
|
286
|
+
expect(labRef?.focusedTrialId).toBe(labRef?.trials[0]?.id);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('is marked on its trial only when there is more than one', () => {
|
|
290
|
+
mountLab();
|
|
291
|
+
expect(regions()[0]).not.toHaveAttribute('data-focused');
|
|
292
|
+
act(() => labRef?.addTrial('StubB'));
|
|
293
|
+
expect(regions()[1]).toHaveAttribute('data-focused', 'true');
|
|
294
|
+
expect(regions()[0]).not.toHaveAttribute('data-focused');
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
describe('swapTrial', () => {
|
|
299
|
+
it('runs another instrument in the trial’s place, and keeps focus on that place', () => {
|
|
300
|
+
mountLab();
|
|
301
|
+
act(() => labRef?.addTrial('Stub'));
|
|
302
|
+
const second = labRef?.trials[1]?.id;
|
|
303
|
+
act(() => labRef?.focusTrial(labRef?.trials[0]?.id ?? ''));
|
|
304
|
+
act(() => labRef?.swapTrial(labRef?.trials[0]?.id ?? '', 'StubB'));
|
|
305
|
+
expect(labRef?.trials.map((t) => t.instrumentName)).toEqual(['StubB', 'Stub']);
|
|
306
|
+
expect(labRef?.trials[1]?.id).toBe(second);
|
|
307
|
+
expect(labRef?.focusedTrialId).toBe(labRef?.trials[0]?.id);
|
|
308
|
+
expect(screen.getByTestId('stub-b-content')).toBeInTheDocument();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('leaves focus where it was when the swapped trial did not have it', () => {
|
|
312
|
+
mountLab();
|
|
313
|
+
act(() => labRef?.addTrial('Stub'));
|
|
314
|
+
const focused = labRef?.focusedTrialId;
|
|
315
|
+
act(() => labRef?.swapTrial(labRef?.trials[0]?.id ?? '', 'StubB'));
|
|
316
|
+
expect(labRef?.focusedTrialId).toBe(focused);
|
|
317
|
+
});
|
|
318
|
+
});
|
package/src/lab/Lab.tsx
CHANGED
|
@@ -13,6 +13,7 @@ import { useStore } from 'zustand/react';
|
|
|
13
13
|
import { AnnotationPreloadContext } from '../annotations/preload';
|
|
14
14
|
import { ANNOTATION_TOOLS } from '../annotations/toolMap';
|
|
15
15
|
import {
|
|
16
|
+
LabAsideRegion,
|
|
16
17
|
LabFooterRegion,
|
|
17
18
|
LabHeaderRegion,
|
|
18
19
|
LabSidebarRegion,
|
|
@@ -44,6 +45,7 @@ import {
|
|
|
44
45
|
closeTrial as closeTrialOp,
|
|
45
46
|
reorderTrials as reorderTrialsOp,
|
|
46
47
|
resetTrial as resetTrialOp,
|
|
48
|
+
swapTrial as swapTrialOp,
|
|
47
49
|
} from '../trial/trialOps';
|
|
48
50
|
import { useLabFitWarning } from './fitCheck';
|
|
49
51
|
import { LabContext, type LabContextValue } from './LabContext';
|
|
@@ -178,7 +180,7 @@ function LabFallback({
|
|
|
178
180
|
}: Pick<LabBaseProps, 'title' | 'mode' | 'pages' | 'path'>) {
|
|
179
181
|
const resolvedMode = useResolvedMode(mode ?? 'auto');
|
|
180
182
|
return (
|
|
181
|
-
<ThemeProvider theme={interstellarTheme}
|
|
183
|
+
<ThemeProvider theme={interstellarTheme} selection={{ mode: resolvedMode }} className="lk-lab">
|
|
182
184
|
<LabShell
|
|
183
185
|
title={title ?? 'Labkit'}
|
|
184
186
|
mode={mode}
|
|
@@ -228,6 +230,32 @@ function LabPanes({
|
|
|
228
230
|
undefined,
|
|
229
231
|
{ scope: 'lab' },
|
|
230
232
|
);
|
|
233
|
+
const [asideWidth, setAsideWidth] = usePersistedState<number | undefined>(
|
|
234
|
+
'lk-lab-aside-width',
|
|
235
|
+
undefined,
|
|
236
|
+
{ scope: 'lab' },
|
|
237
|
+
);
|
|
238
|
+
const main = contributions.some((c) => c.region === 'aside') ? (
|
|
239
|
+
<Split
|
|
240
|
+
className="lk-lab__panes"
|
|
241
|
+
side="end"
|
|
242
|
+
sidebarClassName="lk-lab__aside"
|
|
243
|
+
contentClassName="lk-lab__main"
|
|
244
|
+
label="Lab aside"
|
|
245
|
+
defaultWidth={320}
|
|
246
|
+
width={asideWidth}
|
|
247
|
+
onWidthChange={(w) => {
|
|
248
|
+
setAsideWidth(w);
|
|
249
|
+
surface?.invalidateRects();
|
|
250
|
+
}}
|
|
251
|
+
sidebar={<LabAsideRegion contributions={contributions} />}
|
|
252
|
+
>
|
|
253
|
+
{children}
|
|
254
|
+
</Split>
|
|
255
|
+
) : (
|
|
256
|
+
children
|
|
257
|
+
);
|
|
258
|
+
if (!contributions.some((c) => c.region === 'sidebar')) return main;
|
|
231
259
|
return (
|
|
232
260
|
<Split
|
|
233
261
|
className="lk-lab__panes"
|
|
@@ -242,7 +270,7 @@ function LabPanes({
|
|
|
242
270
|
}}
|
|
243
271
|
sidebar={<LabSidebarRegion contributions={contributions} />}
|
|
244
272
|
>
|
|
245
|
-
{
|
|
273
|
+
{main}
|
|
246
274
|
</Split>
|
|
247
275
|
);
|
|
248
276
|
}
|
|
@@ -259,9 +287,8 @@ export function Lab(props: LabProps) {
|
|
|
259
287
|
storageKey === undefined ? openUnstoredLab(props) : openStoredLab(props, storageKey, storage),
|
|
260
288
|
);
|
|
261
289
|
if (!opened) {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
) : (
|
|
290
|
+
if (props.fallback !== undefined) return props.fallback;
|
|
291
|
+
return (
|
|
265
292
|
<LabFallback title={props.title} mode={props.mode} pages={props.pages} path={props.path} />
|
|
266
293
|
);
|
|
267
294
|
}
|
|
@@ -295,6 +322,10 @@ function LabRuntime({
|
|
|
295
322
|
}, [instruments, store]);
|
|
296
323
|
|
|
297
324
|
const trials = useStore(store, (s) => s.trials);
|
|
325
|
+
const [focusPick, setFocusPick] = useState<string | null>(null);
|
|
326
|
+
const focusedTrialId = trials.some((t) => t.id === focusPick)
|
|
327
|
+
? focusPick
|
|
328
|
+
: (trials[0]?.id ?? null);
|
|
298
329
|
const savedSnapshots = useStore(store, (s) => s.savedSnapshots);
|
|
299
330
|
const modeValue = useStore(store, (s) => s.mode);
|
|
300
331
|
const layout = useStore(store, (s) => s.layout);
|
|
@@ -359,6 +390,39 @@ function LabRuntime({
|
|
|
359
390
|
[outerSurface, attachOwnSurface],
|
|
360
391
|
);
|
|
361
392
|
useLabFitWarning(labBody);
|
|
393
|
+
|
|
394
|
+
useEffect(() => {
|
|
395
|
+
if (!labBody) return;
|
|
396
|
+
const pick = (node: unknown): void => {
|
|
397
|
+
const owned = new Set(store.getState().trials.map((t) => t.id));
|
|
398
|
+
const nearest = (from: Element | null | undefined) =>
|
|
399
|
+
from?.closest<HTMLElement>('.lk-trial[data-trial-id]') ?? null;
|
|
400
|
+
let trial =
|
|
401
|
+
typeof (node as Element | null)?.closest === 'function' ? nearest(node as Element) : null;
|
|
402
|
+
// A trial can hold a lab of its own, whose trials this lab does not own.
|
|
403
|
+
while (trial && !owned.has(trial.dataset.trialId ?? '')) trial = nearest(trial.parentElement);
|
|
404
|
+
if (trial?.dataset.trialId) setFocusPick(trial.dataset.trialId);
|
|
405
|
+
};
|
|
406
|
+
const onInput = (event: Event): void => pick(event.target);
|
|
407
|
+
// Focus moving into a frame fires nothing in this document: the window
|
|
408
|
+
// blurs, and the frame is the active element once it has.
|
|
409
|
+
let pending: ReturnType<typeof setTimeout> | undefined;
|
|
410
|
+
const doc = labBody.ownerDocument;
|
|
411
|
+
const win = doc.defaultView;
|
|
412
|
+
const onBlur = (): void => {
|
|
413
|
+
clearTimeout(pending);
|
|
414
|
+
pending = setTimeout(() => pick(doc.activeElement), 0);
|
|
415
|
+
};
|
|
416
|
+
labBody.addEventListener('pointerdown', onInput, true);
|
|
417
|
+
labBody.addEventListener('focusin', onInput, true);
|
|
418
|
+
win?.addEventListener('blur', onBlur);
|
|
419
|
+
return () => {
|
|
420
|
+
clearTimeout(pending);
|
|
421
|
+
labBody.removeEventListener('pointerdown', onInput, true);
|
|
422
|
+
labBody.removeEventListener('focusin', onInput, true);
|
|
423
|
+
win?.removeEventListener('blur', onBlur);
|
|
424
|
+
};
|
|
425
|
+
}, [labBody, store]);
|
|
362
426
|
const surface = outerSurface ?? ownSurface;
|
|
363
427
|
const surfaceCanvases = useMemo(
|
|
364
428
|
() =>
|
|
@@ -388,7 +452,7 @@ function LabRuntime({
|
|
|
388
452
|
[annotates, tools, labChrome],
|
|
389
453
|
);
|
|
390
454
|
const hasFooterChrome = labChromeAll.some((c) => c.region === 'footer');
|
|
391
|
-
const
|
|
455
|
+
const hasPaneChrome = labChromeAll.some((c) => c.region === 'sidebar' || c.region === 'aside');
|
|
392
456
|
|
|
393
457
|
useEffect(() => {
|
|
394
458
|
if (mode && mode !== store.getState().mode) {
|
|
@@ -402,17 +466,23 @@ function LabRuntime({
|
|
|
402
466
|
const merged = next.map((w) => currentById.get(w.id) ?? w);
|
|
403
467
|
store.setState({ trials: merged });
|
|
404
468
|
};
|
|
469
|
+
const replaceAndFocusAdded = (next: TrialRecord[]): void => {
|
|
470
|
+
const before = new Set(store.getState().trials.map((w) => w.id));
|
|
471
|
+
replaceTrials(next);
|
|
472
|
+
const added = next.find((w) => !before.has(w.id));
|
|
473
|
+
if (added) setFocusPick(added.id);
|
|
474
|
+
};
|
|
405
475
|
|
|
406
476
|
return {
|
|
407
477
|
instruments,
|
|
408
478
|
trials,
|
|
409
479
|
addTrial: (instrumentName, options) => {
|
|
410
|
-
|
|
411
|
-
|
|
480
|
+
replaceAndFocusAdded(
|
|
481
|
+
addTrialOp(store.getState().trials, instruments, instrumentName, options),
|
|
482
|
+
);
|
|
412
483
|
},
|
|
413
484
|
cloneTrial: (id) => {
|
|
414
|
-
|
|
415
|
-
replaceTrials(next);
|
|
485
|
+
replaceAndFocusAdded(cloneTrialOp(store.getState().trials, id));
|
|
416
486
|
},
|
|
417
487
|
closeTrial: (id) => {
|
|
418
488
|
const next = closeTrialOp(store.getState().trials, id);
|
|
@@ -421,6 +491,19 @@ function LabRuntime({
|
|
|
421
491
|
reorderTrials: (ids) => {
|
|
422
492
|
replaceTrials(reorderTrialsOp(store.getState().trials, ids));
|
|
423
493
|
},
|
|
494
|
+
swapTrial: (id, instrumentName, options) => {
|
|
495
|
+
const current = store.getState();
|
|
496
|
+
const next = swapTrialOp(current.trials, instruments, id, instrumentName, options);
|
|
497
|
+
if (next === current.trials) return;
|
|
498
|
+
const record = next[current.trials.findIndex((w) => w.id === id)] as TrialRecord;
|
|
499
|
+
// Written before the tile registers, which is when the grid reads it.
|
|
500
|
+
const { [id]: extent, ...layoutRest } = current.layout as TrialLayout;
|
|
501
|
+
if (extent) current.setLayout({ ...layoutRest, [record.id]: extent });
|
|
502
|
+
replaceTrials(next);
|
|
503
|
+
if (focusedTrialId === id) setFocusPick(record.id);
|
|
504
|
+
},
|
|
505
|
+
focusedTrialId,
|
|
506
|
+
focusTrial: (id) => setFocusPick(id),
|
|
424
507
|
resetTrial: (id) => {
|
|
425
508
|
const next = resetTrialOp(store.getState().trials, id, instruments);
|
|
426
509
|
const record = next.find((w) => w.id === id);
|
|
@@ -451,7 +534,16 @@ function LabRuntime({
|
|
|
451
534
|
configRules,
|
|
452
535
|
controls,
|
|
453
536
|
};
|
|
454
|
-
}, [
|
|
537
|
+
}, [
|
|
538
|
+
instruments,
|
|
539
|
+
trials,
|
|
540
|
+
focusedTrialId,
|
|
541
|
+
savedSnapshots,
|
|
542
|
+
modeValue,
|
|
543
|
+
store,
|
|
544
|
+
configRules,
|
|
545
|
+
controls,
|
|
546
|
+
]);
|
|
455
547
|
|
|
456
548
|
// Only override the backdrop in dark, where there is one to override.
|
|
457
549
|
// Setting a CSS custom property is the sanctioned use of inline style.
|
|
@@ -483,7 +575,7 @@ function LabRuntime({
|
|
|
483
575
|
<LabContext.Provider value={contextValue}>
|
|
484
576
|
<ThemeProvider
|
|
485
577
|
theme={interstellarTheme}
|
|
486
|
-
|
|
578
|
+
selection={{ mode: resolvedMode }}
|
|
487
579
|
className="lk-lab"
|
|
488
580
|
style={backdropStyle}
|
|
489
581
|
>
|
|
@@ -537,7 +629,7 @@ function LabRuntime({
|
|
|
537
629
|
/>
|
|
538
630
|
</>
|
|
539
631
|
)}
|
|
540
|
-
{
|
|
632
|
+
{hasPaneChrome ? (
|
|
541
633
|
<LabPanes contributions={labChromeAll}>
|
|
542
634
|
<LabPalette contributions={labChromeAll} />
|
|
543
635
|
{workspace}
|
package/src/lab/LabContext.ts
CHANGED
|
@@ -18,6 +18,14 @@ export interface LabContextValue {
|
|
|
18
18
|
closeTrial: (id: string) => void;
|
|
19
19
|
resetTrial: (id: string) => void;
|
|
20
20
|
reorderTrials: (ids: readonly string[]) => void;
|
|
21
|
+
/** Put a fresh trial running `instrumentName` in trial `id`'s place, at its
|
|
22
|
+
* size. The replacement has a new id, and takes the focus if `id` had it. */
|
|
23
|
+
swapTrial: (id: string, instrumentName: string, options?: AddTrialOptions) => void;
|
|
24
|
+
/** The trial last pointed at or focused, counting focus that moved into a
|
|
25
|
+
* frame inside it. A trial `addTrial` or `cloneTrial` opens takes it. Falls
|
|
26
|
+
* back to the first trial; null only when there are none. */
|
|
27
|
+
focusedTrialId: string | null;
|
|
28
|
+
focusTrial: (id: string) => void;
|
|
21
29
|
savedSnapshots: SavedSnapshot[];
|
|
22
30
|
saveSnapshot: (trialId: string, name?: string) => void;
|
|
23
31
|
loadSnapshot: (trialId: string, snapshotId: string) => void;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from '@
|
|
1
|
+
import type { Meta, StoryObj } from '@weasel-js/forge';
|
|
2
|
+
import { expect, userEvent, waitFor, within } from '@weasel-js/forge/play';
|
|
2
3
|
import { type ReactNode, useLayoutEffect, useState } from 'react';
|
|
3
4
|
import { createPortal } from 'react-dom';
|
|
4
|
-
import { expect, userEvent, waitFor, within } from 'storybook/test';
|
|
5
5
|
import type { LabContribution } from '../chrome/labTypes';
|
|
6
6
|
import type { TrialContribution } from '../chrome/types';
|
|
7
7
|
import type { Instrument } from '../instrument/types';
|
|
@@ -9,7 +9,7 @@ import { Lab } from './Lab';
|
|
|
9
9
|
import './LabFit.stories.less';
|
|
10
10
|
|
|
11
11
|
// A lab must fit whatever it is mounted in without scrolling. jsdom cannot see
|
|
12
|
-
// layout, so these run in the
|
|
12
|
+
// layout, so these run in the forge-stories vitest project's real browser.
|
|
13
13
|
|
|
14
14
|
const Stub: Instrument = {
|
|
15
15
|
name: 'Stub',
|
|
@@ -43,7 +43,7 @@ const VIEWPORTS = {
|
|
|
43
43
|
|
|
44
44
|
type Host = 'reset' | 'wrapped' | 'framed';
|
|
45
45
|
|
|
46
|
-
//
|
|
46
|
+
// The frame wraps every story in divs of its own, so a fixture that needs a
|
|
47
47
|
// known ancestor chain builds that chain directly on <body> and portals into it.
|
|
48
48
|
function BodyHost({ host, children }: { host: Host; children: ReactNode }) {
|
|
49
49
|
const [mount, setMount] = useState<HTMLElement | null>(null);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from '@
|
|
1
|
+
import type { Meta, StoryObj } from '@weasel-js/forge';
|
|
2
2
|
import { useEffect, useRef } from 'react';
|
|
3
3
|
import type { ConfigField } from '../controls/types';
|
|
4
4
|
import { defineInstrument } from '../instrument/defineInstrument';
|
package/src/lab/LabHeader.tsx
CHANGED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { ThemeProvider } from '@weasel-js/theme/react';
|
|
3
|
+
import { describe, expect, test } from 'vitest';
|
|
4
|
+
import { interstellarTheme } from '../theme/interstellar';
|
|
5
|
+
import { LabRoot } from './LabRoot';
|
|
6
|
+
|
|
7
|
+
describe('LabRoot', () => {
|
|
8
|
+
test('applies lk-root to a labkit piece mounted on its own', () => {
|
|
9
|
+
const { container } = render(
|
|
10
|
+
<LabRoot>
|
|
11
|
+
<div>bare</div>
|
|
12
|
+
</LabRoot>,
|
|
13
|
+
);
|
|
14
|
+
expect(container.querySelector('.lk-root')).not.toBeNull();
|
|
15
|
+
expect(screen.getByText('bare')).toBeInTheDocument();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('marks lk-root as the portal host for overlays', () => {
|
|
19
|
+
const { container } = render(<LabRoot>x</LabRoot>);
|
|
20
|
+
expect(container.querySelector('.lk-root')?.hasAttribute('data-wzl-portal-host')).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('appends className after lk-root on the same element', () => {
|
|
24
|
+
const { container } = render(<LabRoot className="lk-shell">x</LabRoot>);
|
|
25
|
+
const root = container.querySelector('.lk-root') as HTMLElement;
|
|
26
|
+
expect(root.className).toBe('lk-root lk-shell');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('stamps the requested mode under the interstellar theme', () => {
|
|
30
|
+
const { container } = render(<LabRoot mode="light">x</LabRoot>);
|
|
31
|
+
const root = container.firstChild as HTMLElement;
|
|
32
|
+
expect(root.getAttribute('data-wzl-theme')).toBe('interstellar');
|
|
33
|
+
expect(root.getAttribute('data-wzl-mode')).toBe('light');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('resolves mode="auto" to a concrete mode', () => {
|
|
37
|
+
const { container } = render(<LabRoot>x</LabRoot>);
|
|
38
|
+
const root = container.firstChild as HTMLElement;
|
|
39
|
+
expect(root.getAttribute('data-wzl-mode')).toMatch(/^(light|dark)$/);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// The app's theme wins, and no second provider is mounted over it. This
|
|
43
|
+
// only holds while labkit resolves `@weasel-js/theme` to the same copy the
|
|
44
|
+
// app does — it is the in-repo half of the peer-externalization the smoke
|
|
45
|
+
// test's dist audit enforces for a published install.
|
|
46
|
+
test('defers to a theme the app already applied', () => {
|
|
47
|
+
const { container } = render(
|
|
48
|
+
<ThemeProvider theme={interstellarTheme} selection={{ mode: 'dark' }}>
|
|
49
|
+
<LabRoot mode="light">x</LabRoot>
|
|
50
|
+
</ThemeProvider>,
|
|
51
|
+
);
|
|
52
|
+
const root = container.querySelector('.lk-root') as HTMLElement;
|
|
53
|
+
expect(root.getAttribute('data-wzl-mode')).toBeNull();
|
|
54
|
+
expect(container.querySelectorAll('[data-wzl-theme]')).toHaveLength(1);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { ThemeProvider, useThemeOptional } from '@weasel-js/theme/react';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
import type { LabMode } from '../state/types';
|
|
4
|
+
import { interstellarTheme } from '../theme/interstellar';
|
|
5
|
+
import { useResolvedMode } from './useSystemMode';
|
|
6
|
+
|
|
7
|
+
/** Props for `<LabRoot>`. */
|
|
8
|
+
export interface LabRootProps {
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
/** Color mode. "auto" (default) follows prefers-color-scheme. */
|
|
11
|
+
mode?: LabMode;
|
|
12
|
+
/** Added after `lk-root` on the same element. */
|
|
13
|
+
className?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** The element every labkit component expects above it: `.lk-root`, which
|
|
17
|
+
* carries the design tokens, the font stack, the box-sizing reset and the
|
|
18
|
+
* element defaults a lab's bare markup is styled by, plus a theme when the
|
|
19
|
+
* app has not already applied one.
|
|
20
|
+
*
|
|
21
|
+
* `<LabShell>` and `<Lab>` render this themselves. Mount it by hand around a
|
|
22
|
+
* labkit piece used on its own — a bare `<Workspace>` or `<ControlPanel>` —
|
|
23
|
+
* or those components inherit whatever the host page happens to define.
|
|
24
|
+
* Pair it with `import '@weasel-js/labkit/styles.css'`, which is what defines
|
|
25
|
+
* the rules this element scopes. */
|
|
26
|
+
export function LabRoot({ children, mode = 'auto', className }: LabRootProps) {
|
|
27
|
+
const resolved = useResolvedMode(mode);
|
|
28
|
+
const outer = useThemeOptional();
|
|
29
|
+
|
|
30
|
+
// Overlays portal here rather than to the themed wrapper above: `.lk-root`
|
|
31
|
+
// carries the element defaults a lab's bare markup is styled by.
|
|
32
|
+
const root = (
|
|
33
|
+
<div className={className ? `lk-root ${className}` : 'lk-root'} data-wzl-portal-host="">
|
|
34
|
+
{children}
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
// Inside <Lab> the theme is already applied on `.lk-lab`; wrapping again
|
|
39
|
+
// would only add a div.
|
|
40
|
+
return outer ? (
|
|
41
|
+
root
|
|
42
|
+
) : (
|
|
43
|
+
<ThemeProvider theme={interstellarTheme} selection={{ mode: resolved }}>
|
|
44
|
+
{root}
|
|
45
|
+
</ThemeProvider>
|
|
46
|
+
);
|
|
47
|
+
}
|