@weasel-js/labkit 1.5.1 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -7
- package/dist/_dts/{CanvasStackContext-CJNMS6mz.d.ts → CanvasStackContext-DQgcH1ny.d.ts} +1 -1
- package/dist/_dts/LayerList.d-2c9yup5Y.d.ts +83 -0
- package/dist/_dts/{PrefsForm.d-DPy9I8It.d.ts → PrefsRow.d-DFpPIeIC.d.ts} +16 -1
- package/dist/_dts/{Select.d-Gwr8fLpK.d.ts → Select.d-BchGMC2Y.d.ts} +129 -12
- package/dist/_dts/{frac-B4okC2lX.d.ts → frac-DTRq7Xpb.d.ts} +31 -13
- package/dist/_dts/inDialog-D5ZKGjiD.d.ts +23 -0
- package/dist/_dts/{index-CSJoj4LC.d.ts → index-B0--8PEM.d.ts} +5 -4
- package/dist/_dts/index-DINmCuLA.d.ts +453 -0
- package/dist/_dts/number.d-OeQYQoUO.d.ts +28 -0
- package/dist/_dts/{types-DzoNY1pO.d.ts → types-B7y8hX1D.d.ts} +6 -6
- package/dist/_dts/{usePanZoom-Pzf9f2jh.d.ts → usePanZoom-Bq73pwFe.d.ts} +4 -47
- package/dist/_dts/{useTrialState-uLlmaccn.d.ts → useTrialState-CP4jfuKl.d.ts} +1 -1
- package/dist/canvas/index.d.ts +6 -5
- package/dist/canvas/index.js +1 -1
- package/dist/chrome/index.d.ts +5 -4
- package/dist/chrome/index.js +7 -7
- package/dist/chunk-2RVCB2PT.js +3 -0
- package/dist/chunk-2RVCB2PT.js.map +1 -0
- package/dist/{chunk-UN4U4P63.js → chunk-3FI7Y4R4.js} +9 -9
- package/dist/chunk-3FI7Y4R4.js.map +1 -0
- package/dist/{chunk-7PFH3CNM.js → chunk-AWH3SRPW.js} +31 -5
- package/dist/chunk-AWH3SRPW.js.map +1 -0
- package/dist/{chunk-ZDHY47EV.js → chunk-BVL7Y574.js} +25 -9
- package/dist/chunk-BVL7Y574.js.map +1 -0
- package/dist/{chunk-IHUVQ3TX.js → chunk-H5VIV7MQ.js} +31 -27
- package/dist/chunk-H5VIV7MQ.js.map +1 -0
- package/dist/chunk-HFRE4QQV.js +400 -0
- package/dist/chunk-HFRE4QQV.js.map +1 -0
- package/dist/{chunk-IXTQI7JX.js → chunk-MOLIUNVH.js} +4 -4
- package/dist/{chunk-IXTQI7JX.js.map → chunk-MOLIUNVH.js.map} +1 -1
- package/dist/{chunk-CEIUBPPW.js → chunk-TT5C5S6R.js} +3 -86
- package/dist/chunk-TT5C5S6R.js.map +1 -0
- package/dist/chunk-USO6TERP.js +90 -0
- package/dist/chunk-USO6TERP.js.map +1 -0
- package/dist/{chunk-PHK4W562.js → chunk-VQ47KWD5.js} +3 -3
- package/dist/{chunk-PHK4W562.js.map → chunk-VQ47KWD5.js.map} +1 -1
- package/dist/chunk-WUJXJC5O.js +14568 -0
- package/dist/chunk-WUJXJC5O.js.map +1 -0
- package/dist/{chunk-EHGVROVV.js → chunk-ZORVNOKX.js} +6 -25
- package/dist/chunk-ZORVNOKX.js.map +1 -0
- package/dist/config/index.d.ts +53 -11
- package/dist/config/index.js +5 -5
- package/dist/controls/index.d.ts +6 -3
- package/dist/controls/index.js +4 -4
- package/dist/dragdrop/index.d.ts +4 -3
- package/dist/index.d.ts +139 -114
- package/dist/index.js +470 -479
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +6 -35
- package/dist/layers/index.js +2 -3
- package/dist/loupe/index.d.ts +6 -5
- package/dist/passthrough/weasel-ui.d.ts +421 -215
- package/dist/passthrough/weasel-ui.js +1 -1
- package/dist/primitives/index.d.ts +2 -13
- package/dist/primitives/index.js +4 -4
- package/dist/state/index.d.ts +6 -5
- package/dist/state/index.js +5 -5
- package/dist/styles.css +410 -190
- package/dist/undo/index.d.ts +4 -3
- package/package.json +9 -13
- package/src/annotations/frac.test.ts +7 -0
- package/src/annotations/paint.test.ts +21 -0
- package/src/annotations/paint.ts +12 -0
- package/src/annotations/store.test.ts +31 -0
- package/src/annotations/types.ts +3 -2
- package/src/canvas/Stage.test.tsx +18 -0
- package/src/canvas/index.ts +0 -9
- package/src/chrome/labTypes.ts +1 -1
- package/src/chrome/regions/PaletteRegion.test.tsx +18 -0
- package/src/chrome/regions/PaletteRegion.tsx +5 -4
- package/src/chrome/regions/SidebarRegion.less +197 -0
- package/src/chrome/regions/SidebarRegion.test.tsx +20 -0
- package/src/chrome/regions/SidebarRegion.tsx +7 -2
- package/src/chrome/regions/TitleBarRegion.tsx +1 -1
- package/src/chrome/types.ts +13 -4
- package/src/config/autoConfig.ts +2 -2
- package/src/config/builder.ts +23 -5
- package/src/config/entry.test.ts +8 -0
- package/src/config/index.ts +2 -0
- package/src/config/rules.ts +2 -0
- package/src/config/sectionTree.test.ts +146 -0
- package/src/config/sectionTree.ts +124 -0
- package/src/config/types.ts +4 -4
- package/src/controls/AutoControls.stories.tsx +6 -6
- package/src/controls/ControlPanel.less +27 -0
- package/src/controls/ControlPanel.stories.tsx +95 -1
- package/src/controls/ControlPanel.test.tsx +82 -112
- package/src/controls/ControlPanel.tsx +159 -396
- package/src/controls/inDialog.test.tsx +82 -0
- package/src/controls/inDialog.tsx +46 -0
- package/src/controls/index.ts +1 -0
- package/src/index.test.ts +0 -2
- package/src/index.ts +14 -35
- package/src/instrument/types.ts +5 -2
- package/src/lab/Lab.nebula.test.tsx +31 -0
- package/src/lab/Lab.tsx +57 -18
- package/src/lab/LabBoundary.test.tsx +66 -0
- package/src/lab/LabBoundary.tsx +56 -0
- package/src/lab/LabHeader.tsx +4 -23
- package/src/lab/LabRoot.tsx +2 -3
- package/src/lab/LabShell.less +0 -1
- package/src/lab/LabShell.stories.tsx +15 -0
- package/src/lab/LabShell.test.tsx +18 -0
- package/src/lab/LabShell.tsx +14 -1
- package/src/lab/LabSwitcher.less +1 -1
- package/src/lab/LabSwitcher.test.tsx +41 -0
- package/src/lab/LabSwitcher.tsx +30 -8
- package/src/lab/index.ts +1 -0
- package/src/layers/AGENTS.md +18 -74
- package/src/layers/index.ts +4 -2
- package/src/passthrough/weasel-ui.ts +25 -10
- package/src/primitives/FpsMeter.less +6 -1
- package/src/primitives/FpsMeter.test.tsx +12 -4
- package/src/primitives/FpsMeter.tsx +6 -1
- package/src/primitives/JobProgress.stories.tsx +1 -1
- package/src/primitives/StatusBar.less +6 -0
- package/src/primitives/index.ts +0 -2
- package/src/specimen/Specimen.tsx +42 -56
- package/src/state/types.ts +8 -2
- package/src/state/view.test.ts +4 -4
- package/src/styles.less +4 -2
- package/src/theme/Interstellar.stories.tsx +8 -9
- package/src/theme/base.less +3 -20
- package/src/theme/interstellar.test.ts +6 -3
- package/src/theme/interstellar.theme.json +0 -56
- package/src/theme/interstellar.ts +2 -2
- package/src/trial/Trial.canvas.test.tsx +9 -5
- package/src/trial/Trial.config.test.tsx +1 -1
- package/src/trial/Trial.tsx +30 -8
- package/dist/_dts/index-BNdz-tNd.d.ts +0 -423
- package/dist/chunk-7PFH3CNM.js.map +0 -1
- package/dist/chunk-CEIUBPPW.js.map +0 -1
- package/dist/chunk-EH7UEQV7.js +0 -590
- package/dist/chunk-EH7UEQV7.js.map +0 -1
- package/dist/chunk-EHGVROVV.js.map +0 -1
- package/dist/chunk-IHUVQ3TX.js.map +0 -1
- package/dist/chunk-MDVTK5U3.js +0 -170
- package/dist/chunk-MDVTK5U3.js.map +0 -1
- package/dist/chunk-PJ7WEQBJ.js +0 -18
- package/dist/chunk-PJ7WEQBJ.js.map +0 -1
- package/dist/chunk-T3EC2DXD.js +0 -12628
- package/dist/chunk-T3EC2DXD.js.map +0 -1
- package/dist/chunk-UN4U4P63.js.map +0 -1
- package/dist/chunk-ZDHY47EV.js.map +0 -1
- package/dist/ui/layers/index.d.ts +0 -66
- package/dist/ui/layers/index.js +0 -3
- package/dist/ui/layers/index.js.map +0 -1
- package/src/canvas/useOrbit.test.ts +0 -167
- package/src/canvas/useOrbit.ts +0 -156
- package/src/lab/useSystemMode.ts +0 -30
- package/src/layers/LayerList.less +0 -93
- package/src/layers/LayerList.stories.tsx +0 -66
- package/src/layers/LayerList.test.tsx +0 -232
- package/src/layers/LayerList.tsx +0 -268
- package/src/primitives/Sidebar.less +0 -112
- package/src/primitives/Sidebar.stories.tsx +0 -38
- package/src/primitives/Sidebar.test.tsx +0 -74
- package/src/primitives/Sidebar.tsx +0 -35
- package/src/ui/layers/index.ts +0 -5
package/src/lab/LabHeader.tsx
CHANGED
|
@@ -1,22 +1,7 @@
|
|
|
1
|
-
import { AddIcon
|
|
2
|
-
import {
|
|
3
|
-
import type { LabMode } from '../state/types';
|
|
1
|
+
import { AddIcon } from '@weasel-js/ui';
|
|
2
|
+
import { ColorModeControl, MenuButton } from '../passthrough/weasel-ui';
|
|
4
3
|
import { useLabContext } from './LabContext';
|
|
5
4
|
|
|
6
|
-
// The glyph is the segment's content and the word is its accessible name, so
|
|
7
|
-
// the bar stays a three-way radiogroup announcing Auto / Light / Dark.
|
|
8
|
-
const MODES = [
|
|
9
|
-
{ value: 'auto' as LabMode, label: 'Auto', glyph: <ModeAutoIcon size={14} /> },
|
|
10
|
-
{ value: 'light' as LabMode, label: 'Light', glyph: <ModeLightIcon size={14} /> },
|
|
11
|
-
{ value: 'dark' as LabMode, label: 'Dark', glyph: <ModeDarkIcon size={14} /> },
|
|
12
|
-
];
|
|
13
|
-
|
|
14
|
-
const MODE_ITEMS = MODES.map(({ value, label, glyph }) => ({
|
|
15
|
-
value,
|
|
16
|
-
label: glyph,
|
|
17
|
-
ariaLabel: label,
|
|
18
|
-
}));
|
|
19
|
-
|
|
20
5
|
/** The controls `<Lab>` puts in its header: add a trial, and choose the color
|
|
21
6
|
* mode. Both drive `LabContext`, which carried them with no UI at all — so
|
|
22
7
|
* every consumer rebuilt these two. Rendered before a consumer's own header
|
|
@@ -45,15 +30,11 @@ export function LabHeader({ addTrial = true }: { addTrial?: boolean }) {
|
|
|
45
30
|
/>
|
|
46
31
|
)}
|
|
47
32
|
|
|
48
|
-
<
|
|
33
|
+
<ColorModeControl
|
|
49
34
|
className="lk-lab-header__mode"
|
|
50
|
-
ariaLabel="Color mode"
|
|
51
35
|
variant="flat"
|
|
52
|
-
items={MODE_ITEMS}
|
|
53
36
|
value={lab.mode}
|
|
54
|
-
onChange={
|
|
55
|
-
if (next) lab.setMode(next);
|
|
56
|
-
}}
|
|
37
|
+
onChange={lab.setMode}
|
|
57
38
|
/>
|
|
58
39
|
</>
|
|
59
40
|
);
|
package/src/lab/LabRoot.tsx
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { ThemeProvider, useThemeOptional } from '@weasel-js/theme/react';
|
|
1
|
+
import { ThemeProvider, useResolvedColorMode, useThemeOptional } from '@weasel-js/theme/react';
|
|
2
2
|
import type { ReactNode } from 'react';
|
|
3
3
|
import type { LabMode } from '../state/types';
|
|
4
4
|
import { interstellarTheme } from '../theme/interstellar';
|
|
5
|
-
import { useResolvedMode } from './useSystemMode';
|
|
6
5
|
|
|
7
6
|
/** Props for `<LabRoot>`. */
|
|
8
7
|
export interface LabRootProps {
|
|
@@ -24,7 +23,7 @@ export interface LabRootProps {
|
|
|
24
23
|
* Pair it with `import '@weasel-js/labkit/styles.css'`, which is what defines
|
|
25
24
|
* the rules this element scopes. */
|
|
26
25
|
export function LabRoot({ children, mode = 'auto', className }: LabRootProps) {
|
|
27
|
-
const resolved =
|
|
26
|
+
const resolved = useResolvedColorMode(mode);
|
|
28
27
|
const outer = useThemeOptional();
|
|
29
28
|
|
|
30
29
|
// Overlays portal here rather than to the themed wrapper above: `.lk-root`
|
package/src/lab/LabShell.less
CHANGED
|
@@ -86,3 +86,18 @@ export const HeaderControlsLineUp: Story = {
|
|
|
86
86
|
await expect(middles).toEqual(middles.map(() => middles[0]));
|
|
87
87
|
},
|
|
88
88
|
};
|
|
89
|
+
|
|
90
|
+
// One document serving its pages from the hash: the open route is the one the
|
|
91
|
+
// switcher marks, and each entry is a link to its fragment.
|
|
92
|
+
export const HashRoutedPages: Story = {
|
|
93
|
+
args: {
|
|
94
|
+
title: 'Registry',
|
|
95
|
+
pages: [
|
|
96
|
+
{ href: '/app/', label: 'App' },
|
|
97
|
+
{ href: '#/dev/tools', label: 'Tools' },
|
|
98
|
+
{ href: '#/dev/registry', label: 'Registry' },
|
|
99
|
+
],
|
|
100
|
+
path: '/app/#/dev/registry?entry=rect',
|
|
101
|
+
children: <p>Open the title to see the current route marked.</p>,
|
|
102
|
+
},
|
|
103
|
+
};
|
|
@@ -47,6 +47,24 @@ describe('LabShell', () => {
|
|
|
47
47
|
expect(container.querySelector('.lk-root')?.hasAttribute('data-wzl-portal-host')).toBe(true);
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
+
test('holds the document title while mounted when asked to', () => {
|
|
51
|
+
document.title = 'before';
|
|
52
|
+
const { unmount } = render(
|
|
53
|
+
<LabShell title="t" documentTitle="Page title">
|
|
54
|
+
x
|
|
55
|
+
</LabShell>,
|
|
56
|
+
);
|
|
57
|
+
expect(document.title).toBe('Page title');
|
|
58
|
+
unmount();
|
|
59
|
+
expect(document.title).toBe('before');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('leaves the document title alone by default', () => {
|
|
63
|
+
document.title = 'before';
|
|
64
|
+
render(<LabShell title="t">x</LabShell>);
|
|
65
|
+
expect(document.title).toBe('before');
|
|
66
|
+
});
|
|
67
|
+
|
|
50
68
|
test('always applies lk-root class', () => {
|
|
51
69
|
const { container } = render(<LabShell title="t">x</LabShell>);
|
|
52
70
|
expect(container.querySelector('.lk-root')).not.toBeNull();
|
package/src/lab/LabShell.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ReactNode, useEffect } from 'react';
|
|
2
2
|
import type { LabMode } from '../state/types';
|
|
3
3
|
import { LabRoot } from './LabRoot';
|
|
4
4
|
import type { LabPage } from './LabSwitcher';
|
|
@@ -20,6 +20,9 @@ export interface LabShellProps {
|
|
|
20
20
|
/** The path the switcher reads to mark the open page. Defaults to the
|
|
21
21
|
* current location. */
|
|
22
22
|
path?: string;
|
|
23
|
+
/** Set as `document.title` while the shell is mounted, and the previous
|
|
24
|
+
* title restored after. Omitted, the document's title is left alone. */
|
|
25
|
+
documentTitle?: string;
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
/** Page frame for a lab: a titled header, a body, and an optional footer,
|
|
@@ -33,7 +36,17 @@ export function LabShell({
|
|
|
33
36
|
mode = 'auto',
|
|
34
37
|
pages,
|
|
35
38
|
path,
|
|
39
|
+
documentTitle,
|
|
36
40
|
}: LabShellProps) {
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (documentTitle === undefined) return;
|
|
43
|
+
const previous = document.title;
|
|
44
|
+
document.title = documentTitle;
|
|
45
|
+
return () => {
|
|
46
|
+
document.title = previous;
|
|
47
|
+
};
|
|
48
|
+
}, [documentTitle]);
|
|
49
|
+
|
|
37
50
|
return (
|
|
38
51
|
<LabRoot mode={mode} className="lk-shell">
|
|
39
52
|
<header className="lk-shell-header">
|
package/src/lab/LabSwitcher.less
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
// hyphen and the control becomes three ragged lines with a caret adrift.
|
|
27
27
|
white-space: nowrap;
|
|
28
28
|
|
|
29
|
-
&:hover { color: var(--wzl-accent); }
|
|
29
|
+
&:hover { color: var(--wzl-accent-fg); }
|
|
30
30
|
|
|
31
31
|
&:focus-visible {
|
|
32
32
|
outline: 2px solid var(--wzl-focus-ring, currentColor);
|
|
@@ -9,6 +9,8 @@ const PAGES = [
|
|
|
9
9
|
{ href: '/ingest', label: 'Ingestion' },
|
|
10
10
|
];
|
|
11
11
|
|
|
12
|
+
const open2 = (name: string) =>
|
|
13
|
+
fireEvent.click(screen.getByRole('button', { name: new RegExp(name) }));
|
|
12
14
|
const open = () => fireEvent.click(screen.getByRole('button', { name: /corpus stats/ }));
|
|
13
15
|
|
|
14
16
|
describe('currentPage', () => {
|
|
@@ -27,6 +29,45 @@ describe('currentPage', () => {
|
|
|
27
29
|
it('reports -1 for a page that is not one of them', () => {
|
|
28
30
|
expect(currentPage('/bench', PAGES)).toBe(-1);
|
|
29
31
|
});
|
|
32
|
+
|
|
33
|
+
// One document serving its pages from the hash: every href shares a path, so
|
|
34
|
+
// only the fragment can tell them apart.
|
|
35
|
+
const ROUTES = [
|
|
36
|
+
{ href: '/app/', label: 'App' },
|
|
37
|
+
{ href: '#/dev/tools', label: 'Tools' },
|
|
38
|
+
{ href: '#/dev/registry', label: 'Registry' },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
it('tells hash routes on one document apart', () => {
|
|
42
|
+
expect(currentPage('/app/#/dev/registry', ROUTES)).toBe(2);
|
|
43
|
+
expect(currentPage('/app/#/dev/tools', ROUTES)).toBe(1);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('matches a hash route through its own query and sub-routes', () => {
|
|
47
|
+
expect(currentPage('/app/#/dev/tools?bundle=minimal', ROUTES)).toBe(1);
|
|
48
|
+
expect(currentPage('/app/#/dev/tools/', ROUTES)).toBe(1);
|
|
49
|
+
expect(currentPage('/app/#/dev/registry/tool/rect', ROUTES)).toBe(2);
|
|
50
|
+
expect(currentPage('/app/#/dev/toolsets', ROUTES)).toBe(0);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('falls back to the page without a hash when no route matches', () => {
|
|
54
|
+
expect(currentPage('/app/', ROUTES)).toBe(0);
|
|
55
|
+
expect(currentPage('/app/#/elsewhere', ROUTES)).toBe(0);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('reads the hash off the current location by default', () => {
|
|
59
|
+
window.history.replaceState(null, '', '/app/#/dev/registry');
|
|
60
|
+
try {
|
|
61
|
+
render(<LabSwitcher title="Registry" pages={ROUTES} />);
|
|
62
|
+
open2('Registry');
|
|
63
|
+
expect(screen.getByRole('menuitem', { name: 'Registry' })).toHaveAttribute(
|
|
64
|
+
'aria-current',
|
|
65
|
+
'page',
|
|
66
|
+
);
|
|
67
|
+
} finally {
|
|
68
|
+
window.history.replaceState(null, '', '/');
|
|
69
|
+
}
|
|
70
|
+
});
|
|
30
71
|
});
|
|
31
72
|
|
|
32
73
|
describe('<LabSwitcher>', () => {
|
package/src/lab/LabSwitcher.tsx
CHANGED
|
@@ -2,7 +2,8 @@ import { useCallback, useEffect, useId, useRef, useState } from 'react';
|
|
|
2
2
|
|
|
3
3
|
/** One page in a project's set of labs. */
|
|
4
4
|
export interface LabPage {
|
|
5
|
-
/** The page's own URL, and how the switcher knows which page is open.
|
|
5
|
+
/** The page's own URL, and how the switcher knows which page is open. A
|
|
6
|
+
* hash route (`#/dev/tools`) names a page on the current document. */
|
|
6
7
|
href: string;
|
|
7
8
|
label: string;
|
|
8
9
|
}
|
|
@@ -18,18 +19,39 @@ export interface LabSwitcherProps {
|
|
|
18
19
|
className?: string;
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
/** A URL split into the document it names and the hash route on it, each
|
|
23
|
+
* without its query, trailing slash or `.html`. */
|
|
24
|
+
function locate(url: string): { doc: string; route: string } {
|
|
25
|
+
const hash = url.indexOf('#');
|
|
26
|
+
const trim = (part: string) => part.replace(/\?.*$/, '').replace(/\/$/, '');
|
|
27
|
+
return {
|
|
28
|
+
doc: trim(hash < 0 ? url : url.slice(0, hash)).replace(/\.html$/, ''),
|
|
29
|
+
route: hash < 0 ? '' : trim(url.slice(hash + 1)),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
21
33
|
/** Which of `pages` a path is on, or -1.
|
|
22
34
|
*
|
|
23
35
|
* Matched on the end of the path so a query string, a trailing slash or a
|
|
24
36
|
* leftover `.html` cannot lose it: a project whose dev server maps `/stats`
|
|
25
37
|
* to `stats.html` serves both spellings, and a bookmark from before the URLs
|
|
26
|
-
* lost their extension still resolves to the same page.
|
|
38
|
+
* lost their extension still resolves to the same page.
|
|
39
|
+
*
|
|
40
|
+
* A page whose `href` carries a hash is a route on a single document and wins
|
|
41
|
+
* when the path's hash is that route or one under it. A page without one
|
|
42
|
+
* ignores the hash, so an in-page anchor does not lose it. */
|
|
27
43
|
export function currentPage(path: string, pages: readonly LabPage[]): number {
|
|
28
|
-
const here = path
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
.
|
|
32
|
-
|
|
44
|
+
const here = locate(path);
|
|
45
|
+
const onDoc = (doc: string) => here.doc.endsWith(doc);
|
|
46
|
+
const routed = pages.findIndex((p) => {
|
|
47
|
+
if (!p.href.includes('#')) return false;
|
|
48
|
+
const page = locate(p.href);
|
|
49
|
+
return (
|
|
50
|
+
onDoc(page.doc) && (here.route === page.route || here.route.startsWith(`${page.route}/`))
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
if (routed >= 0) return routed;
|
|
54
|
+
return pages.findIndex((p) => !p.href.includes('#') && onDoc(locate(p.href).doc));
|
|
33
55
|
}
|
|
34
56
|
|
|
35
57
|
/** A project's lab title, doubling as the way to reach its other labs.
|
|
@@ -47,7 +69,7 @@ export function LabSwitcher({ title, pages, path, className }: LabSwitcherProps)
|
|
|
47
69
|
const root = useRef<HTMLDivElement>(null);
|
|
48
70
|
const menuId = useId();
|
|
49
71
|
const here = currentPage(
|
|
50
|
-
path ?? (typeof window === 'undefined' ? '' : window.location.pathname),
|
|
72
|
+
path ?? (typeof window === 'undefined' ? '' : window.location.pathname + window.location.hash),
|
|
51
73
|
pages,
|
|
52
74
|
);
|
|
53
75
|
|
package/src/lab/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export type { LabProps } from './Lab';
|
|
2
2
|
export { Lab } from './Lab';
|
|
3
|
+
export { LabBoundary, type LabBoundaryProps } from './LabBoundary';
|
|
3
4
|
export type { LabContextValue } from './LabContext';
|
|
4
5
|
export { LabContext, useLabContext } from './LabContext';
|
|
5
6
|
export type { LabPaletteProps } from './LabPalette';
|
package/src/layers/AGENTS.md
CHANGED
|
@@ -1,33 +1,11 @@
|
|
|
1
1
|
# Layers — Agent Guide
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`src/layers/` re-exports `<LayerList>` from `@weasel-js/ui`, along with the
|
|
4
|
+
`layers` capability types. The component itself — rows, cards, nesting,
|
|
5
|
+
selection, visibility, reorder — is documented at its source in
|
|
6
|
+
`packages/ui/src/components/LayerList/`.
|
|
4
7
|
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
| File | Role |
|
|
8
|
-
|---|---|
|
|
9
|
-
| `LayerList.tsx` | Layer toggle/reorder list, flat or nested (sidebar widget) |
|
|
10
|
-
| `LayerList.less` | Row styling, drag handle, pinned-row variant, subtree indent |
|
|
11
|
-
| `LayerList.stories.tsx` | Flat and nested renderings |
|
|
12
|
-
|
|
13
|
-
## Props
|
|
14
|
-
|
|
15
|
-
```ts
|
|
16
|
-
interface LayerListProps {
|
|
17
|
-
layers: LayerTreeNode[]; // LayerDescriptor + { children?, defaultCollapsed? }
|
|
18
|
-
visibility: Record<string, boolean>; // missing key → visible
|
|
19
|
-
onReorder: (newOrder: LayerTreeNode[]) => void;
|
|
20
|
-
onToggle: (id: string, visible: boolean) => void;
|
|
21
|
-
collapsedIds?: readonly string[]; // supplying it makes collapse controlled
|
|
22
|
-
onCollapsedChange?: (ids: string[]) => void;
|
|
23
|
-
className?: string;
|
|
24
|
-
}
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
`LayerDescriptor` lives in `src/instrument/types.ts`; `LayerTreeNode` extends it
|
|
28
|
-
in `LayerList.tsx`, so a plain `LayerDescriptor[]` is still a valid `layers`.
|
|
29
|
-
|
|
30
|
-
## How it integrates
|
|
8
|
+
## How the capability uses it
|
|
31
9
|
|
|
32
10
|
The instrument declares layer ids:
|
|
33
11
|
|
|
@@ -35,53 +13,19 @@ The instrument declares layer ids:
|
|
|
35
13
|
layers: { ids: ['grid', 'plants'] }
|
|
36
14
|
```
|
|
37
15
|
|
|
38
|
-
`Trial.tsx`
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
Setting `alwaysOn: true` on a `LayerDescriptor` does two things:
|
|
45
|
-
|
|
46
|
-
1. The row renders with a 🔒 badge instead of a drag handle and visibility checkbox.
|
|
47
|
-
2. The row is excluded from reorder operations — pinned rows always sort to the bottom of the rendered list.
|
|
48
|
-
|
|
49
|
-
This is intended for legend/HUD layers that should never be toggled off. Visibility for pinned rows is **not** read from the `visibility` prop; they always render.
|
|
50
|
-
|
|
51
|
-
## Nesting
|
|
52
|
-
|
|
53
|
-
A node with `children` renders an expandable subtree. The expand control is
|
|
54
|
-
`<Disclosure>` from `@weasel-js/ui` — the same twisty every other collapsible
|
|
55
|
-
surface uses.
|
|
56
|
-
|
|
57
|
-
Reordering is scoped to siblings: a drag moves a row within its own parent's
|
|
58
|
-
child list and never reparents it. `onReorder` still receives the whole tree,
|
|
59
|
-
with only that sibling group's order changed.
|
|
60
|
-
|
|
61
|
-
The twisty column appears only when some node in the tree has children — a flat
|
|
62
|
-
list renders exactly the rows it always did. Within a tree, childless rows get
|
|
63
|
-
`.lk-layer-list__twisty-gap` so labels stay aligned down a level.
|
|
64
|
-
|
|
65
|
-
Collapse is uncontrolled by default, seeded once from each node's
|
|
66
|
-
`defaultCollapsed`. Pass `collapsedIds` to own it; `onCollapsedChange` fires
|
|
67
|
-
either way.
|
|
68
|
-
|
|
69
|
-
## Drag handle customization
|
|
70
|
-
|
|
71
|
-
The drag handle is a button with class `lk-layer-list__handle` holding `<DragHandleGlyph>` (from `@weasel-js/ui`), the same grip `LayerStack` uses. Its padding is transparent and cancelled by an equal negative margin, so the grab target is larger than the drawn dots without widening the row. Row pitch — height plus row gap — is measured from the DOM when a drag starts, so restyling the row does not skew drag distance. It used to be a hardcoded `28`, which had already drifted from the rendered height by the time it was found.
|
|
72
|
-
|
|
73
|
-
Pointer capture is acquired on `pointerdown` and released on `pointerup`, so dragging works across the document without requiring window-level listeners.
|
|
74
|
-
|
|
75
|
-
## Empty state
|
|
76
|
-
|
|
77
|
-
If `layers.length === 0`, renders `.lk-layer-list__empty` with the text "No layers". Trial already guards this case (`layerDescriptors.length > 0`) so the empty state is rare in practice.
|
|
16
|
+
`Trial.tsx` turns each id into a `LayerDescriptor` (`label === id` unless the
|
|
17
|
+
entry is a descriptor), keeps `visibility` and `layerOrder` in local state, and
|
|
18
|
+
builds the list's items from them in the order the canvas draws. The order a
|
|
19
|
+
drag produces is applied to `instrument.canvas.layers` before they reach
|
|
20
|
+
`<CanvasStack>`, and each toggle and reorder emits `layers.toggle` /
|
|
21
|
+
`layers.reorder` on the bus.
|
|
78
22
|
|
|
79
|
-
|
|
23
|
+
`layers.ids` and `canvas.layers[].id` are assumed to match. A canvas layer not
|
|
24
|
+
in `layers.ids` keeps its default order and is always visible.
|
|
80
25
|
|
|
81
|
-
|
|
82
|
-
- Multi-select reorder (current implementation is single-row)
|
|
83
|
-
- Right-click context menu
|
|
84
|
-
- Reparenting by drag (nesting renders, but a drag stays within one parent)
|
|
85
|
-
- Solo/mute UI common in DAW-style apps
|
|
26
|
+
## `alwaysOn`
|
|
86
27
|
|
|
87
|
-
|
|
28
|
+
A descriptor with `alwaysOn: true` becomes a `locked` item, pinned below the
|
|
29
|
+
rest: it shows a lock in place of the grip and has no visibility checkbox,
|
|
30
|
+
cannot be dragged, and no drag can cross it. It always renders, whatever
|
|
31
|
+
`visibility` says. Intended for legend and HUD layers.
|
package/src/layers/index.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// LayerList lives in `@weasel-js/ui`. Named, not `export *` — a star re-export
|
|
2
|
+
// of an external package emits no binding in the bundle.
|
|
3
|
+
export type { LayerListItem, LayerListProps, LayerMove } from '@weasel-js/ui';
|
|
4
|
+
export { LayerList, moveLayers } from '@weasel-js/ui';
|
|
1
5
|
export type { LayerCapability, LayerDescriptor } from '../instrument/types';
|
|
2
|
-
export type { LayerListProps, LayerTreeNode } from './LayerList';
|
|
3
|
-
export { LayerList } from './LayerList';
|
|
@@ -8,11 +8,6 @@
|
|
|
8
8
|
export {
|
|
9
9
|
ActionBar,
|
|
10
10
|
type ActionBarProps,
|
|
11
|
-
ActionsBar,
|
|
12
|
-
type ActionsBarItem,
|
|
13
|
-
type ActionsBarProps,
|
|
14
|
-
type ActionsBarSize,
|
|
15
|
-
type ActionsBarVariant,
|
|
16
11
|
type AddPointMode,
|
|
17
12
|
type AnchorRenderProps,
|
|
18
13
|
type AsyncOption,
|
|
@@ -22,12 +17,17 @@ export {
|
|
|
22
17
|
type BadgeProps,
|
|
23
18
|
type BadgeShape,
|
|
24
19
|
type BadgeSize,
|
|
25
|
-
type
|
|
20
|
+
type BadgeStatus,
|
|
26
21
|
type BadgeVariant,
|
|
27
22
|
type BoundsCtx,
|
|
28
23
|
type BuiltInEdgeName,
|
|
29
24
|
type BuiltinPref,
|
|
30
25
|
Button,
|
|
26
|
+
ButtonBar,
|
|
27
|
+
type ButtonBarItem,
|
|
28
|
+
type ButtonBarProps,
|
|
29
|
+
type ButtonBarSize,
|
|
30
|
+
type ButtonBarVariant,
|
|
31
31
|
type ButtonProps,
|
|
32
32
|
type ButtonSize,
|
|
33
33
|
type ButtonVariant,
|
|
@@ -38,6 +38,8 @@ export {
|
|
|
38
38
|
type CheckboxProps,
|
|
39
39
|
type ChromaCurve,
|
|
40
40
|
type ChromaCurvePoint,
|
|
41
|
+
ColorModeControl,
|
|
42
|
+
type ColorModeControlProps,
|
|
41
43
|
ComboBox,
|
|
42
44
|
type ComboBoxCommit,
|
|
43
45
|
type ComboBoxFilter,
|
|
@@ -56,10 +58,13 @@ export {
|
|
|
56
58
|
Dialog,
|
|
57
59
|
type DialogProps,
|
|
58
60
|
Disclosure,
|
|
59
|
-
|
|
61
|
+
DisclosureMark,
|
|
62
|
+
type DisclosureMarkProps,
|
|
60
63
|
type DisclosureProps,
|
|
61
64
|
DisclosureRow,
|
|
62
65
|
type DisclosureRowProps,
|
|
66
|
+
DragGhost,
|
|
67
|
+
type DragGhostProps,
|
|
63
68
|
DragHandleGlyph,
|
|
64
69
|
type DragHandleGlyphProps,
|
|
65
70
|
detectPlatform,
|
|
@@ -98,16 +103,20 @@ export {
|
|
|
98
103
|
keyGlyph,
|
|
99
104
|
keySpecFromKey,
|
|
100
105
|
keySpecsFromMods,
|
|
106
|
+
keySpecsFromShortcut,
|
|
107
|
+
LayerList,
|
|
101
108
|
type LayerListItem,
|
|
102
|
-
|
|
103
|
-
type
|
|
104
|
-
|
|
109
|
+
type LayerListProps,
|
|
110
|
+
type LayerMove,
|
|
111
|
+
ListEditor,
|
|
112
|
+
type ListEditorProps,
|
|
105
113
|
type LogicalMod,
|
|
106
114
|
type LogicalModSpec,
|
|
107
115
|
MenuButton,
|
|
108
116
|
type MenuButtonItem,
|
|
109
117
|
type MenuButtonProps,
|
|
110
118
|
MINUS_SIGN,
|
|
119
|
+
moveLayers,
|
|
111
120
|
NumberField,
|
|
112
121
|
type NumberFieldProps,
|
|
113
122
|
OptionsBar,
|
|
@@ -145,6 +154,11 @@ export {
|
|
|
145
154
|
type PrefRenderer,
|
|
146
155
|
type PrefString,
|
|
147
156
|
type PrefStringControl,
|
|
157
|
+
PrefsDialog,
|
|
158
|
+
type PrefsDialogProps,
|
|
159
|
+
PrefsForm,
|
|
160
|
+
type PrefsFormProps,
|
|
161
|
+
type PrefsLayout,
|
|
148
162
|
paintGradientTrack,
|
|
149
163
|
prefValueAtPath,
|
|
150
164
|
Radio,
|
|
@@ -155,6 +169,7 @@ export {
|
|
|
155
169
|
type RangeSliderProps,
|
|
156
170
|
type ReorderDragHandlers,
|
|
157
171
|
type ReorderDragState,
|
|
172
|
+
type ReorderGhost,
|
|
158
173
|
type RovingItem,
|
|
159
174
|
type RovingTabIndex,
|
|
160
175
|
Select,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
.lk-fps-meter {
|
|
2
2
|
display: inline-flex;
|
|
3
3
|
align-items: center;
|
|
4
|
+
white-space: nowrap;
|
|
4
5
|
gap: var(--wzl-space-xs);
|
|
5
6
|
font-family: var(--wzl-font-mono);
|
|
6
7
|
font-size: var(--wzl-font-size-sm);
|
|
@@ -9,6 +10,10 @@
|
|
|
9
10
|
|
|
10
11
|
.lk-fps-meter-value {
|
|
11
12
|
color: var(--wzl-fg);
|
|
12
|
-
|
|
13
|
+
/* Sized for the widest rate it can hold rather than the one it holds now:
|
|
14
|
+
a 240Hz display reads three digits, and a readout that grows into them
|
|
15
|
+
pushes whatever shares its row. */
|
|
16
|
+
min-width: 3ch;
|
|
17
|
+
font-variant-numeric: tabular-nums;
|
|
13
18
|
text-align: right;
|
|
14
19
|
}
|
|
@@ -9,9 +9,17 @@ describe('FpsMeter', () => {
|
|
|
9
9
|
|
|
10
10
|
test('renders an FPS label', () => {
|
|
11
11
|
render(<FpsMeter />);
|
|
12
|
-
expect(screen.getByText(
|
|
12
|
+
expect(screen.getByText('FPS')).toBeInTheDocument();
|
|
13
13
|
});
|
|
14
14
|
|
|
15
|
+
/* The label and the rate are separate elements so the rate can hold a fixed
|
|
16
|
+
width; a text query spanning both would go back to matching one box. */
|
|
17
|
+
const rate = (): HTMLElement => {
|
|
18
|
+
const el = document.querySelector('.lk-fps-meter-value');
|
|
19
|
+
if (!el) throw new Error('no rate readout');
|
|
20
|
+
return el as HTMLElement;
|
|
21
|
+
};
|
|
22
|
+
|
|
15
23
|
test('uses lk-fps-meter class', () => {
|
|
16
24
|
const { container } = render(<FpsMeter />);
|
|
17
25
|
expect((container.firstChild as HTMLElement).className).toBe('lk-fps-meter');
|
|
@@ -31,7 +39,7 @@ describe('FpsMeter', () => {
|
|
|
31
39
|
raf?.(16.67);
|
|
32
40
|
raf?.(33.34);
|
|
33
41
|
});
|
|
34
|
-
expect(
|
|
42
|
+
expect(rate().textContent).toMatch(/^\d+$/);
|
|
35
43
|
|
|
36
44
|
vi.unstubAllGlobals();
|
|
37
45
|
});
|
|
@@ -59,7 +67,7 @@ describe('FpsMeter', () => {
|
|
|
59
67
|
run(16.67);
|
|
60
68
|
run(33.34);
|
|
61
69
|
});
|
|
62
|
-
const before =
|
|
70
|
+
const before = rate().textContent;
|
|
63
71
|
|
|
64
72
|
// An hour hidden. The frame that lands on resume is not a 60-minute frame,
|
|
65
73
|
// and must not enter the rolling average as one.
|
|
@@ -74,7 +82,7 @@ describe('FpsMeter', () => {
|
|
|
74
82
|
run(3_600_016.67);
|
|
75
83
|
});
|
|
76
84
|
|
|
77
|
-
expect(
|
|
85
|
+
expect(rate().textContent).toBe(before);
|
|
78
86
|
|
|
79
87
|
setHidden(false);
|
|
80
88
|
vi.unstubAllGlobals();
|
|
@@ -42,7 +42,12 @@ export function FpsMeter() {
|
|
|
42
42
|
|
|
43
43
|
return (
|
|
44
44
|
<div className="lk-fps-meter" ref={rootRef}>
|
|
45
|
-
<span className="lk-fps-meter-
|
|
45
|
+
<span className="lk-fps-meter-label">FPS</span>
|
|
46
|
+
{/* The number is its own cell because it is the only part that changes:
|
|
47
|
+
in one box with the label, every digit it gains or loses resizes the
|
|
48
|
+
readout, and the status bar it sits in wraps to a second line and
|
|
49
|
+
back — which in a small trial resizes the window. */}
|
|
50
|
+
<span className="lk-fps-meter-value">{fps}</span>
|
|
46
51
|
</div>
|
|
47
52
|
);
|
|
48
53
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
.lk-status-bar {
|
|
2
2
|
display: flex;
|
|
3
3
|
align-items: center;
|
|
4
|
+
/* One line, always. A bar that grows a second line takes it from the canvas
|
|
5
|
+
above it, and a readout that changes width would do that on and off as it
|
|
6
|
+
ticks. Too narrow for its contents clips them instead. */
|
|
7
|
+
overflow: hidden;
|
|
4
8
|
gap: var(--wzl-space-md);
|
|
5
9
|
padding: var(--wzl-space-xs) var(--wzl-space-sm);
|
|
6
10
|
background: var(--wzl-surface-raised);
|
|
@@ -13,6 +17,8 @@
|
|
|
13
17
|
.lk-status-bar-section {
|
|
14
18
|
display: inline-flex;
|
|
15
19
|
align-items: center;
|
|
20
|
+
white-space: nowrap;
|
|
21
|
+
min-width: 0;
|
|
16
22
|
gap: var(--wzl-space-xs);
|
|
17
23
|
padding: 0 var(--wzl-space-xs);
|
|
18
24
|
border-right: 1px solid var(--wzl-line-subtle);
|
package/src/primitives/index.ts
CHANGED
|
@@ -7,8 +7,6 @@ export type { LegendEntry, LegendMark, LegendProps } from './Legend';
|
|
|
7
7
|
export { Legend } from './Legend';
|
|
8
8
|
export type { ScaleIndicatorProps } from './ScaleIndicator';
|
|
9
9
|
export { ScaleIndicator } from './ScaleIndicator';
|
|
10
|
-
export type { SidebarProps } from './Sidebar';
|
|
11
|
-
export { Sidebar } from './Sidebar';
|
|
12
10
|
export type { SplitProps } from './Split';
|
|
13
11
|
export { Split } from './Split';
|
|
14
12
|
export type { StatusBarProps, StatusBarSectionProps } from './StatusBar';
|