@weasel-js/labkit 1.5.0 → 1.5.2
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-CMMo180l.d.ts} +1 -1
- package/dist/_dts/{PrefsForm.d-DDHFANkc.d.ts → PrefsForm.d-DPy9I8It.d.ts} +4 -0
- package/dist/_dts/Select.d-ZTiwzYyT.d.ts +479 -0
- package/dist/_dts/{frac-8R6UmlvN.d.ts → frac-B4hFJMiN.d.ts} +54 -6
- package/dist/_dts/{index-retnAut7.d.ts → index-8LiKe3A-.d.ts} +77 -10
- package/dist/_dts/{index-B8uu2ba7.d.ts → index-DlKolEIZ.d.ts} +20 -8
- package/dist/_dts/{types-Ca2LCnPq.d.ts → types-Db-3NNk1.d.ts} +28 -4
- package/dist/_dts/{usePanZoom-nS798rOF.d.ts → usePanZoom-SSTZe410.d.ts} +4 -47
- package/dist/_dts/{useTrialState-jcx_TdU6.d.ts → useTrialState-pnAE-UBD.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-6BUQ77ZV.js +18 -0
- package/dist/chunk-6BUQ77ZV.js.map +1 -0
- package/dist/{chunk-KJALCDWE.js → chunk-753Y6HE6.js} +9 -9
- package/dist/chunk-753Y6HE6.js.map +1 -0
- package/dist/{chunk-6ZDGOZQV.js → chunk-A2ACAGW6.js} +72 -20
- package/dist/chunk-A2ACAGW6.js.map +1 -0
- package/dist/{chunk-NS64DXMN.js → chunk-CKUKF72R.js} +44 -21
- package/dist/chunk-CKUKF72R.js.map +1 -0
- package/dist/{chunk-MUKOW3TC.js → chunk-D35PFTS7.js} +4 -4
- package/dist/{chunk-MUKOW3TC.js.map → chunk-D35PFTS7.js.map} +1 -1
- package/dist/{chunk-UU3NJO6N.js → chunk-EMXAIRUJ.js} +3 -3
- package/dist/{chunk-UU3NJO6N.js.map → chunk-EMXAIRUJ.js.map} +1 -1
- package/dist/{chunk-ZPH5WTWF.js → chunk-ERFMC6WU.js} +142 -66
- package/dist/chunk-ERFMC6WU.js.map +1 -0
- package/dist/chunk-FKLT3ITC.js +590 -0
- package/dist/chunk-FKLT3ITC.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-EKIICY6X.js → chunk-KJCUERQF.js} +37 -25
- package/dist/chunk-KJCUERQF.js.map +1 -0
- package/dist/chunk-LASCBWW7.js +13456 -0
- package/dist/chunk-LASCBWW7.js.map +1 -0
- package/dist/{chunk-67SJLMC7.js → chunk-LRI3MBGR.js} +45 -12
- package/dist/chunk-LRI3MBGR.js.map +1 -0
- package/dist/chunk-REOBGYMC.js +150 -0
- package/dist/chunk-REOBGYMC.js.map +1 -0
- package/dist/{chunk-PGETSEDK.js → chunk-TT5C5S6R.js} +11 -90
- package/dist/chunk-TT5C5S6R.js.map +1 -0
- package/dist/{chunk-NDRYLVOW.js → chunk-Z76KEZAX.js} +2 -2
- package/dist/chunk-Z76KEZAX.js.map +1 -0
- package/dist/config/index.d.ts +62 -19
- 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 +230 -62
- package/dist/index.js +1367 -1698
- 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 +57 -230
- 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 +266 -57
- 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/index.ts +0 -9
- 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 +3 -3
- package/src/chrome/regions/PaletteRegion.test.tsx +18 -0
- package/src/chrome/regions/PaletteRegion.tsx +6 -10
- package/src/chrome/regions/SidebarRegion.stories.tsx +44 -0
- package/src/chrome/regions/TitleBarRegion.tsx +1 -1
- package/src/chrome/regions/ToolbarRegion.tsx +11 -7
- package/src/chrome/types.ts +11 -2
- 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 +55 -11
- 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.less +23 -0
- package/src/controls/ControlPanel.stories.tsx +83 -1
- package/src/controls/ControlPanel.test.tsx +457 -4
- package/src/controls/ControlPanel.tsx +534 -73
- package/src/index.test.ts +0 -2
- package/src/index.ts +33 -11
- 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 +124 -16
- 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.less +1 -1
- package/src/layers/LayerList.stories.tsx +2 -2
- 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.less +6 -1
- package/src/primitives/FpsMeter.stories.tsx +1 -1
- package/src/primitives/FpsMeter.test.tsx +12 -4
- package/src/primitives/FpsMeter.tsx +6 -1
- package/src/primitives/JobProgress.stories.tsx +2 -2
- 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.less +6 -0
- 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 +31 -2
- package/src/state/useTrialState.test.tsx +85 -0
- package/src/state/useTrialState.ts +9 -1
- package/src/state/view.test.ts +4 -4
- 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 +36 -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-KJALCDWE.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/canvas/useOrbit.test.ts +0 -167
- package/src/canvas/useOrbit.ts +0 -156
- package/src/theme/interstellar.tokens.json +0 -61
|
@@ -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
|
+
}
|
package/src/lab/LabShell.less
CHANGED
|
@@ -39,12 +39,24 @@
|
|
|
39
39
|
.lk-shell-header-actions {
|
|
40
40
|
margin-left: auto;
|
|
41
41
|
display: flex;
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
|
|
42
|
+
// Every control here runs at the one control height and shares a middle.
|
|
43
|
+
// The row still hands the header its first item's baseline, which is what
|
|
44
|
+
// lines the controls' labels up with the title.
|
|
45
|
+
align-items: center;
|
|
45
46
|
gap: var(--wzl-space-sm);
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
// A toolbar dropped into the header is a run of header controls, not a strip of
|
|
50
|
+
// its own: it takes the header's control height rather than the compact one
|
|
51
|
+
// trial toolbars use, and none of the strip's box.
|
|
52
|
+
.lk-shell-header-actions .lk-toolbar {
|
|
53
|
+
--wzl-control-h: inherit;
|
|
54
|
+
min-height: 0;
|
|
55
|
+
padding: 0;
|
|
56
|
+
background: none;
|
|
57
|
+
border: 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
48
60
|
.lk-shell-body {
|
|
49
61
|
display: flex;
|
|
50
62
|
flex-direction: column;
|
|
@@ -81,6 +93,15 @@
|
|
|
81
93
|
border-right: 1px solid var(--wzl-border);
|
|
82
94
|
}
|
|
83
95
|
|
|
96
|
+
.lk-lab__aside {
|
|
97
|
+
display: flex;
|
|
98
|
+
flex-direction: column;
|
|
99
|
+
height: 100%;
|
|
100
|
+
overflow-y: auto;
|
|
101
|
+
background: var(--wzl-surface);
|
|
102
|
+
border-left: 1px solid var(--wzl-border);
|
|
103
|
+
}
|
|
104
|
+
|
|
84
105
|
.lk-lab__main {
|
|
85
106
|
display: flex;
|
|
86
107
|
min-width: 0;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from '@
|
|
1
|
+
import type { Meta, StoryObj } from '@weasel-js/forge';
|
|
2
|
+
import { expect, within } from '@weasel-js/forge/play';
|
|
3
|
+
import { Select, ToggleBar } from '../passthrough/weasel-ui';
|
|
4
|
+
import { Toolbar } from '../primitives/Toolbar';
|
|
2
5
|
import { LabShell } from './LabShell';
|
|
3
6
|
|
|
4
7
|
const meta: Meta<typeof LabShell> = {
|
|
@@ -37,3 +40,49 @@ export const WithFooter: Story = {
|
|
|
37
40
|
footer: <span>Status: ready</span>,
|
|
38
41
|
},
|
|
39
42
|
};
|
|
43
|
+
|
|
44
|
+
// jsdom has no layout, so this runs in the browser story projects.
|
|
45
|
+
export const HeaderControlsLineUp: Story = {
|
|
46
|
+
args: {
|
|
47
|
+
title: 'My Lab',
|
|
48
|
+
header: (
|
|
49
|
+
<>
|
|
50
|
+
<button type="button">Add</button>
|
|
51
|
+
<ToggleBar
|
|
52
|
+
ariaLabel="Mode"
|
|
53
|
+
items={[
|
|
54
|
+
{ value: 'a', label: 'A' },
|
|
55
|
+
{ value: 'b', label: 'B' },
|
|
56
|
+
]}
|
|
57
|
+
value="a"
|
|
58
|
+
onChange={() => {}}
|
|
59
|
+
/>
|
|
60
|
+
<Select
|
|
61
|
+
aria-label="Font"
|
|
62
|
+
width="fit"
|
|
63
|
+
selectedKey="oswald"
|
|
64
|
+
options={[{ value: 'oswald', label: 'Oswald' }]}
|
|
65
|
+
/>
|
|
66
|
+
<Toolbar aria-label="Actions">
|
|
67
|
+
<Toolbar.Group>
|
|
68
|
+
<Toolbar.Button onClick={() => {}}>Undo</Toolbar.Button>
|
|
69
|
+
</Toolbar.Group>
|
|
70
|
+
</Toolbar>
|
|
71
|
+
</>
|
|
72
|
+
),
|
|
73
|
+
children: <p>Body content.</p>,
|
|
74
|
+
},
|
|
75
|
+
play: async ({ canvasElement }) => {
|
|
76
|
+
const canvas = within(canvasElement);
|
|
77
|
+
const boxes = [
|
|
78
|
+
canvas.getByRole('button', { name: 'Add' }),
|
|
79
|
+
canvas.getByRole('radiogroup', { name: 'Mode' }),
|
|
80
|
+
canvas.getByRole('button', { name: /Font/ }),
|
|
81
|
+
canvas.getByRole('button', { name: 'Undo' }),
|
|
82
|
+
].map((el) => el.getBoundingClientRect());
|
|
83
|
+
const heights = boxes.map((box) => Math.round(box.height * 2) / 2);
|
|
84
|
+
const middles = boxes.map((box) => Math.round((box.top + box.height / 2) * 2) / 2);
|
|
85
|
+
await expect(heights).toEqual(heights.map(() => heights[0]));
|
|
86
|
+
await expect(middles).toEqual(middles.map(() => middles[0]));
|
|
87
|
+
},
|
|
88
|
+
};
|
package/src/lab/LabShell.tsx
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import { ThemeProvider, useThemeOptional } from '@weasel-js/theme/react';
|
|
2
1
|
import type { ReactNode } from 'react';
|
|
3
2
|
import type { LabMode } from '../state/types';
|
|
4
|
-
import {
|
|
3
|
+
import { LabRoot } from './LabRoot';
|
|
5
4
|
import type { LabPage } from './LabSwitcher';
|
|
6
5
|
import { LabSwitcher } from './LabSwitcher';
|
|
7
|
-
import { useResolvedMode } from './useSystemMode';
|
|
8
6
|
|
|
9
7
|
/** Props for `<LabShell>`. */
|
|
10
8
|
export interface LabShellProps {
|
|
@@ -27,34 +25,27 @@ export interface LabShellProps {
|
|
|
27
25
|
/** Page frame for a lab: a titled header, a body, and an optional footer,
|
|
28
26
|
* themed for the resolved color mode. Presentational only — use `<Lab>` when
|
|
29
27
|
* the trial runtime is wanted too. */
|
|
30
|
-
export function LabShell({
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
28
|
+
export function LabShell({
|
|
29
|
+
title,
|
|
30
|
+
children,
|
|
31
|
+
header,
|
|
32
|
+
footer,
|
|
33
|
+
mode = 'auto',
|
|
34
|
+
pages,
|
|
35
|
+
path,
|
|
36
|
+
}: LabShellProps) {
|
|
37
|
+
return (
|
|
38
|
+
<LabRoot mode={mode} className="lk-shell">
|
|
39
39
|
<header className="lk-shell-header">
|
|
40
|
-
{pages && pages.length > 0
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
{pages && pages.length > 0 ? (
|
|
41
|
+
<LabSwitcher title={title} pages={pages} path={path} className="lk-shell-title" />
|
|
42
|
+
) : (
|
|
43
|
+
<h1 className="lk-shell-title">{title}</h1>
|
|
44
|
+
)}
|
|
44
45
|
{header && <div className="lk-shell-header-actions">{header}</div>}
|
|
45
46
|
</header>
|
|
46
47
|
<main className="lk-shell-body">{children}</main>
|
|
47
48
|
{footer && <footer className="lk-shell-footer">{footer}</footer>}
|
|
48
|
-
</
|
|
49
|
-
);
|
|
50
|
-
|
|
51
|
-
// Inside <Lab> the theme is already applied on `.lk-lab`; wrapping again
|
|
52
|
-
// would only add a div.
|
|
53
|
-
return outer ? (
|
|
54
|
-
shell
|
|
55
|
-
) : (
|
|
56
|
-
<ThemeProvider theme={interstellarTheme} mode={resolved}>
|
|
57
|
-
{shell}
|
|
58
|
-
</ThemeProvider>
|
|
49
|
+
</LabRoot>
|
|
59
50
|
);
|
|
60
51
|
}
|
package/src/lab/LabSwitcher.less
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
.lk-switcher-title {
|
|
7
7
|
margin: 0;
|
|
8
8
|
font-size: var(--wzl-font-size-xl);
|
|
9
|
-
line-height: var(--wzl-
|
|
9
|
+
line-height: var(--wzl-leading-snug);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
.lk-switcher-trigger {
|
|
@@ -26,12 +26,12 @@
|
|
|
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-
|
|
29
|
+
&:hover { color: var(--wzl-accent); }
|
|
30
30
|
|
|
31
31
|
&:focus-visible {
|
|
32
32
|
outline: 2px solid var(--wzl-focus-ring, currentColor);
|
|
33
33
|
outline-offset: 2px;
|
|
34
|
-
border-radius: var(--wzl-radius-sm,
|
|
34
|
+
border-radius: var(--wzl-radius-sm, 3px);
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
position: absolute;
|
|
55
55
|
top: calc(100% + var(--wzl-space-xs));
|
|
56
56
|
left: 0;
|
|
57
|
-
z-index: var(--wzl-z-
|
|
57
|
+
z-index: var(--wzl-z-overlay);
|
|
58
58
|
min-width: max(100%, 11rem);
|
|
59
59
|
display: flex;
|
|
60
60
|
flex-direction: column;
|
|
@@ -64,15 +64,15 @@
|
|
|
64
64
|
// surface, the way `FloatingPanel` does.
|
|
65
65
|
background: var(--wzl-surface);
|
|
66
66
|
border: 1px solid var(--wzl-border);
|
|
67
|
-
border-radius: var(--wzl-radius-md,
|
|
68
|
-
box-shadow:
|
|
67
|
+
border-radius: var(--wzl-radius-md, 5px);
|
|
68
|
+
box-shadow: 0 6px 20px var(--wzl-shadow);
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
.lk-switcher-item {
|
|
72
72
|
padding: var(--wzl-space-xs) var(--wzl-space-sm);
|
|
73
73
|
border-radius: var(--wzl-radius-sm, 3px);
|
|
74
|
-
color: var(--wzl-
|
|
75
|
-
font-size: var(--wzl-font-size
|
|
74
|
+
color: var(--wzl-fg);
|
|
75
|
+
font-size: var(--wzl-font-size);
|
|
76
76
|
text-decoration: none;
|
|
77
77
|
white-space: nowrap;
|
|
78
78
|
|
|
@@ -87,6 +87,6 @@
|
|
|
87
87
|
// The open page stays in the list rather than being filtered out: a menu that
|
|
88
88
|
// drops its current entry shifts every other one as you move between pages.
|
|
89
89
|
.lk-switcher-item[aria-current='page'] {
|
|
90
|
-
color: var(--wzl-
|
|
91
|
-
font-weight: var(--wzl-font-weight-
|
|
90
|
+
color: var(--wzl-fg);
|
|
91
|
+
font-weight: var(--wzl-font-weight-bold);
|
|
92
92
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { fireEvent, render, screen } from '@testing-library/react';
|
|
2
2
|
import { describe, expect, it } from 'vitest';
|
|
3
|
-
import { currentPage, LabSwitcher } from './LabSwitcher';
|
|
4
3
|
import { LabShell } from './LabShell';
|
|
4
|
+
import { currentPage, LabSwitcher } from './LabSwitcher';
|
|
5
5
|
|
|
6
6
|
const PAGES = [
|
|
7
7
|
{ href: '/corpus', label: 'Wall' },
|
|
@@ -19,8 +19,7 @@ describe('currentPage', () => {
|
|
|
19
19
|
it('survives a query string, a trailing slash and a leftover extension', () => {
|
|
20
20
|
// A dev server maps `/stats` to `stats.html` and serves both spellings, so
|
|
21
21
|
// an old bookmark must still resolve to the same page.
|
|
22
|
-
for (const path of ['/stats?kind=base', '/stats/', '/stats.html',
|
|
23
|
-
'/lab/stats.html#top']) {
|
|
22
|
+
for (const path of ['/stats?kind=base', '/stats/', '/stats.html', '/lab/stats.html#top']) {
|
|
24
23
|
expect(currentPage(path, PAGES)).toBe(1);
|
|
25
24
|
}
|
|
26
25
|
});
|
|
@@ -34,7 +33,9 @@ describe('<LabSwitcher>', () => {
|
|
|
34
33
|
it('shows the title and nothing else until it is opened', () => {
|
|
35
34
|
render(<LabSwitcher title="corpus stats" pages={PAGES} path="/stats" />);
|
|
36
35
|
expect(screen.getByRole('button', { name: /corpus stats/ })).toHaveAttribute(
|
|
37
|
-
'aria-expanded',
|
|
36
|
+
'aria-expanded',
|
|
37
|
+
'false',
|
|
38
|
+
);
|
|
38
39
|
expect(screen.queryByRole('menu')).toBeNull();
|
|
39
40
|
});
|
|
40
41
|
|
|
@@ -54,8 +55,7 @@ describe('<LabSwitcher>', () => {
|
|
|
54
55
|
render(<LabSwitcher title="corpus stats" pages={PAGES} path="/stats" />);
|
|
55
56
|
open();
|
|
56
57
|
expect(screen.getByRole('menuitem', { name: 'Wall' }).tagName).toBe('A');
|
|
57
|
-
expect(screen.getByRole('menuitem', { name: 'Wall' })).toHaveAttribute(
|
|
58
|
-
'href', '/corpus');
|
|
58
|
+
expect(screen.getByRole('menuitem', { name: 'Wall' })).toHaveAttribute('href', '/corpus');
|
|
59
59
|
});
|
|
60
60
|
|
|
61
61
|
it('closes on Escape and on a press outside it', () => {
|
|
@@ -63,7 +63,8 @@ describe('<LabSwitcher>', () => {
|
|
|
63
63
|
<div>
|
|
64
64
|
<LabSwitcher title="corpus stats" pages={PAGES} path="/stats" />
|
|
65
65
|
<button type="button">elsewhere</button>
|
|
66
|
-
</div
|
|
66
|
+
</div>,
|
|
67
|
+
);
|
|
67
68
|
open();
|
|
68
69
|
fireEvent.keyDown(document, { key: 'Escape' });
|
|
69
70
|
expect(screen.queryByRole('menu')).toBeNull();
|
|
@@ -76,7 +77,7 @@ describe('<LabSwitcher>', () => {
|
|
|
76
77
|
it('stays a plain heading when there is nowhere to switch to', () => {
|
|
77
78
|
// A disclosure arrow promising a menu of the page you are already on is
|
|
78
79
|
// worse than no control.
|
|
79
|
-
render(<LabSwitcher title="corpus stats" pages={
|
|
80
|
+
render(<LabSwitcher title="corpus stats" pages={PAGES.slice(1, 2)} path="/stats" />);
|
|
80
81
|
expect(screen.getByRole('heading', { name: 'corpus stats' })).toBeInTheDocument();
|
|
81
82
|
expect(screen.queryByRole('button')).toBeNull();
|
|
82
83
|
});
|
|
@@ -84,7 +85,11 @@ describe('<LabSwitcher>', () => {
|
|
|
84
85
|
|
|
85
86
|
describe('<LabShell> with pages', () => {
|
|
86
87
|
it('turns its title into the switcher', () => {
|
|
87
|
-
render(
|
|
88
|
+
render(
|
|
89
|
+
<LabShell title="corpus stats" pages={PAGES} path="/stats">
|
|
90
|
+
body
|
|
91
|
+
</LabShell>,
|
|
92
|
+
);
|
|
88
93
|
expect(screen.getByRole('button', { name: /corpus stats/ })).toBeInTheDocument();
|
|
89
94
|
});
|
|
90
95
|
|
package/src/lab/LabSwitcher.tsx
CHANGED
|
@@ -25,8 +25,10 @@ export interface LabSwitcherProps {
|
|
|
25
25
|
* to `stats.html` serves both spellings, and a bookmark from before the URLs
|
|
26
26
|
* lost their extension still resolves to the same page. */
|
|
27
27
|
export function currentPage(path: string, pages: readonly LabPage[]): number {
|
|
28
|
-
const here = path
|
|
29
|
-
.replace(
|
|
28
|
+
const here = path
|
|
29
|
+
.replace(/[?#].*$/, '')
|
|
30
|
+
.replace(/\/$/, '')
|
|
31
|
+
.replace(/\.html$/, '');
|
|
30
32
|
return pages.findIndex((p) => here.endsWith(p.href.replace(/\.html$/, '')));
|
|
31
33
|
}
|
|
32
34
|
|
|
@@ -44,8 +46,10 @@ export function LabSwitcher({ title, pages, path, className }: LabSwitcherProps)
|
|
|
44
46
|
const [open, setOpen] = useState(false);
|
|
45
47
|
const root = useRef<HTMLDivElement>(null);
|
|
46
48
|
const menuId = useId();
|
|
47
|
-
const here = currentPage(
|
|
48
|
-
? '' : window.location.pathname),
|
|
49
|
+
const here = currentPage(
|
|
50
|
+
path ?? (typeof window === 'undefined' ? '' : window.location.pathname),
|
|
51
|
+
pages,
|
|
52
|
+
);
|
|
49
53
|
|
|
50
54
|
const close = useCallback(() => setOpen(false), []);
|
|
51
55
|
|
package/src/lab/Workspace.less
CHANGED
|
@@ -11,7 +11,7 @@ function fakeHandle(): SurfaceHandle {
|
|
|
11
11
|
invalidateRects: vi.fn(),
|
|
12
12
|
registerTile: vi.fn(),
|
|
13
13
|
registerClear: vi.fn(() => () => {}),
|
|
14
|
-
|
|
14
|
+
registerPainter: vi.fn(() => () => {}),
|
|
15
15
|
containerRef: vi.fn(),
|
|
16
16
|
getContainer: vi.fn(() => null),
|
|
17
17
|
};
|
package/src/lab/index.ts
CHANGED
|
@@ -4,6 +4,8 @@ export type { LabContextValue } from './LabContext';
|
|
|
4
4
|
export { LabContext, useLabContext } from './LabContext';
|
|
5
5
|
export type { LabPaletteProps } from './LabPalette';
|
|
6
6
|
export { LabPalette } from './LabPalette';
|
|
7
|
+
export type { LabRootProps } from './LabRoot';
|
|
8
|
+
export { LabRoot } from './LabRoot';
|
|
7
9
|
export type { LabShellProps } from './LabShell';
|
|
8
10
|
export { LabShell } from './LabShell';
|
|
9
11
|
export type { LabPage, LabSwitcherProps } from './LabSwitcher';
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from '@
|
|
1
|
+
import type { Meta, StoryObj } from '@weasel-js/forge';
|
|
2
2
|
import { useState } from 'react';
|
|
3
3
|
import { LayerList, type LayerTreeNode } from './LayerList';
|
|
4
4
|
|
|
5
5
|
const meta: Meta<typeof LayerList> = {
|
|
6
|
-
title: 'labkit/
|
|
6
|
+
title: 'labkit/Layers/LayerList',
|
|
7
7
|
component: LayerList,
|
|
8
8
|
};
|
|
9
9
|
export default meta;
|
package/src/loupe/AGENTS.md
CHANGED
|
@@ -12,7 +12,9 @@ This directory binds that model to a labkit trial and draws it.
|
|
|
12
12
|
| File | Role |
|
|
13
13
|
|---|---|
|
|
14
14
|
| `types.ts` | `LoupeCapability`, and `resolveLoupe` filling in every default |
|
|
15
|
-
| `useLoupe.ts` | The model over a host element,
|
|
15
|
+
| `useLoupe.ts` | The model over a host element, and pointer aiming |
|
|
16
|
+
| `loupeActions.ts` | `loupe.peek` and `loupe.magnify`, as `Action` descriptors |
|
|
17
|
+
| `LoupeGestures.tsx` | Registers those and mounts the dispatcher on the host |
|
|
16
18
|
| `TrialLoupe.tsx` | Picks the painter and mounts the lens |
|
|
17
19
|
| `LoupeBubble.tsx` | The circular clip, positioned on the aim |
|
|
18
20
|
| `CanvasLoupe.tsx` | Painter for a `<CanvasStack>` |
|
|
@@ -44,6 +46,16 @@ StrictMode mounts / unmounts / mounts every effect — so disposing in the
|
|
|
44
46
|
cleanup leaves a magnifier that draws but silently ignores every aim. It owns
|
|
45
47
|
no resources; unmounting only reports the lens gone.
|
|
46
48
|
|
|
47
|
-
**The wheel
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
**The wheel is taken from pan/zoom by declining it, not by capturing it.**
|
|
50
|
+
`loupe.magnify`'s `enabled` returns a disabled reason while the lens is down, so
|
|
51
|
+
the dispatcher leaves the event unhandled and `usePanZoom` — a React handler on
|
|
52
|
+
the same element — sees it as usual. While the lens is up the action fires, the
|
|
53
|
+
dispatcher stops propagation, and React's root listener never runs.
|
|
54
|
+
|
|
55
|
+
**Aiming is a plain listener because a hover is not a gesture.**
|
|
56
|
+
`GESTURE_DESCRIPTORS` names no continuous-motion gesture, so `pointermove` /
|
|
57
|
+
`pointerleave` stay hand-attached in `useLoupe`. Everything else routes.
|
|
58
|
+
|
|
59
|
+
**`<LoupeGestures>` mounts outside the visibility gate.** Hold-to-peek is what
|
|
60
|
+
raises a lens that is down; gate its registration on `loupe.visible` and the
|
|
61
|
+
peek key stops working entirely.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mounts the gesture dispatcher on a loupe's host and registers the loupe's
|
|
3
|
+
* actions on it, so the peek key and the wheel route the way every other
|
|
4
|
+
* weasel binding does.
|
|
5
|
+
*
|
|
6
|
+
* Three of the dispatcher's four element channels are off. Only `wheel` is
|
|
7
|
+
* wanted here; `contextMenu` suppresses the native menu unconditionally and
|
|
8
|
+
* `ingest` makes the host a file-drop target, neither of which a lab asked
|
|
9
|
+
* for by turning a magnifier on.
|
|
10
|
+
*/
|
|
11
|
+
import {
|
|
12
|
+
type ActionsRegistry,
|
|
13
|
+
type Tool,
|
|
14
|
+
useActionsRegistry,
|
|
15
|
+
useGestureDispatcher,
|
|
16
|
+
} from '@weasel-js/core';
|
|
17
|
+
import { type RefObject, useEffect, useMemo } from 'react';
|
|
18
|
+
import { createLoupeActions, type LoupeInputApi } from './loupeActions';
|
|
19
|
+
|
|
20
|
+
/** Props for `<LoupeGestures>`. */
|
|
21
|
+
export interface LoupeGesturesProps {
|
|
22
|
+
/** The element the dispatcher listens on — the same one the lens tracks. */
|
|
23
|
+
hostRef: RefObject<HTMLElement | null>;
|
|
24
|
+
input: LoupeInputApi;
|
|
25
|
+
/** `KeyboardEvent.key` for hold-to-peek, or `null` for no peek binding. */
|
|
26
|
+
peekKey: string | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const NO_TOOLS: ReadonlyMap<string, Tool> = new Map();
|
|
30
|
+
|
|
31
|
+
const CHANNELS = { pointer: false, contextMenu: false, ingest: false } as const;
|
|
32
|
+
|
|
33
|
+
function LoupeDispatch({
|
|
34
|
+
hostRef,
|
|
35
|
+
input,
|
|
36
|
+
peekKey,
|
|
37
|
+
registry,
|
|
38
|
+
}: LoupeGesturesProps & { registry: ActionsRegistry }) {
|
|
39
|
+
const actions = useMemo(() => createLoupeActions(input, peekKey), [input, peekKey]);
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
const offs = actions.map((a) => registry.register(a));
|
|
43
|
+
return () => {
|
|
44
|
+
for (const off of offs) off();
|
|
45
|
+
};
|
|
46
|
+
}, [registry, actions]);
|
|
47
|
+
|
|
48
|
+
useGestureDispatcher({
|
|
49
|
+
canvasRef: hostRef,
|
|
50
|
+
actions: registry,
|
|
51
|
+
toolsById: NO_TOOLS,
|
|
52
|
+
channels: CHANNELS,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The loupe's input, routed. Render it inside a `<WeaselProvider isolate>` —
|
|
60
|
+
* with no registry in scope it renders nothing rather than registering
|
|
61
|
+
* actions that could never fire.
|
|
62
|
+
*/
|
|
63
|
+
export function LoupeGestures(props: LoupeGesturesProps) {
|
|
64
|
+
const registry = useActionsRegistry();
|
|
65
|
+
if (!registry) return null;
|
|
66
|
+
return <LoupeDispatch {...props} registry={registry} />;
|
|
67
|
+
}
|