@weasel-js/labkit 1.1.0 → 1.3.0-pre.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 +27 -0
- package/dist/_dts/{DrawCommand-BkZztJsW.d.ts → DrawCommand-DBB45NfN.d.ts} +244 -58
- package/dist/_dts/PrefsForm-BYa6cWnO.d.ts +201 -0
- package/dist/_dts/index-DgcNdEdh.d.ts +314 -0
- package/dist/_dts/types-1Sdxy_Pv.d.ts +90 -0
- package/dist/_dts/types-AHlQxBNN.d.ts +162 -0
- package/dist/_dts/types-DJ79Tg5J.d.ts +56 -0
- package/dist/_dts/{useTrialState-DYe2vUwN.d.ts → useTrialState-CsGMjhu9.d.ts} +6 -92
- package/dist/canvas/index.d.ts +8 -3
- package/dist/canvas/index.js +2 -1
- package/dist/chrome/index.d.ts +86 -0
- package/dist/chrome/index.js +8 -0
- package/dist/chrome/index.js.map +1 -0
- package/dist/{chunk-3TYUJR7Z.js → chunk-4TMMVIDM.js} +9 -3
- package/dist/chunk-4TMMVIDM.js.map +1 -0
- package/dist/chunk-HFDVGF4X.js +170 -0
- package/dist/chunk-HFDVGF4X.js.map +1 -0
- package/dist/chunk-LN6JDUGB.js +106 -0
- package/dist/chunk-LN6JDUGB.js.map +1 -0
- package/dist/{chunk-574LJAV4.js → chunk-NS54R52R.js} +31 -21
- package/dist/chunk-NS54R52R.js.map +1 -0
- package/dist/{chunk-NRKWVTVT.js → chunk-O2BULFHX.js} +5 -14
- package/dist/chunk-O2BULFHX.js.map +1 -0
- package/dist/chunk-O7NVSCLN.js +266 -0
- package/dist/chunk-O7NVSCLN.js.map +1 -0
- package/dist/chunk-PO6L3NSH.js +366 -0
- package/dist/chunk-PO6L3NSH.js.map +1 -0
- package/dist/chunk-PQJ5B2U2.js +27 -0
- package/dist/chunk-PQJ5B2U2.js.map +1 -0
- package/dist/chunk-RVNN4CHQ.js +212 -0
- package/dist/chunk-RVNN4CHQ.js.map +1 -0
- package/dist/chunk-TLGRYALP.js +6151 -0
- package/dist/chunk-TLGRYALP.js.map +1 -0
- package/dist/{chunk-SFL7NFKN.js → chunk-V7PRSIRQ.js} +15 -7
- package/dist/chunk-V7PRSIRQ.js.map +1 -0
- package/dist/{chunk-DJLDIRFN.js → chunk-Z6HJ5FEM.js} +9626 -6050
- package/dist/chunk-Z6HJ5FEM.js.map +1 -0
- package/dist/chunk-ZZAYVX4X.js +505 -0
- package/dist/chunk-ZZAYVX4X.js.map +1 -0
- package/dist/controls/index.d.ts +19 -5
- package/dist/controls/index.js +3 -1
- package/dist/dragdrop/index.d.ts +5 -2
- package/dist/index.d.ts +307 -107
- package/dist/index.js +797 -540
- package/dist/index.js.map +1 -1
- package/dist/job/index.d.ts +13 -0
- package/dist/job/index.js +3 -0
- package/dist/job/index.js.map +1 -0
- package/dist/layers/index.d.ts +6 -3
- package/dist/layers/index.js +2 -1
- package/dist/passthrough/weasel-canvas.d.ts +69 -13
- package/dist/passthrough/weasel-canvas.js +1 -1
- package/dist/passthrough/weasel-ui.d.ts +3804 -3661
- package/dist/passthrough/weasel-ui.js +2 -2
- package/dist/primitives/index.d.ts +102 -8
- package/dist/primitives/index.js +4 -1
- package/dist/state/index.d.ts +3 -2
- package/dist/state/index.js +2 -2
- package/dist/styles.css +543 -144
- package/dist/surface/index.d.ts +77 -0
- package/dist/surface/index.js +4 -0
- package/dist/surface/index.js.map +1 -0
- package/dist/ui/layers/index.js +4 -3
- package/dist/undo/index.d.ts +5 -2
- package/package.json +24 -7
- package/src/canvas/AGENTS.md +12 -0
- package/src/canvas/CanvasStack.tsx +6 -2
- package/src/canvas/useLayerScheduler.ts +22 -15
- package/src/canvas/useOrbit.test.ts +71 -0
- package/src/canvas/useOrbit.ts +149 -0
- package/src/canvas/usePanZoom.test.ts +80 -0
- package/src/canvas/usePanZoom.ts +16 -2
- package/src/chrome/ChromeRegions.stories.tsx +106 -0
- package/src/chrome/builtins.test.ts +153 -0
- package/src/chrome/builtins.tsx +205 -0
- package/src/chrome/index.ts +25 -0
- package/src/chrome/merge.test.ts +51 -0
- package/src/chrome/merge.ts +52 -0
- package/src/chrome/regions/PaletteRegion.less +14 -0
- package/src/chrome/regions/PaletteRegion.test.tsx +36 -0
- package/src/chrome/regions/PaletteRegion.tsx +41 -0
- package/src/chrome/regions/SidebarRegion.tsx +56 -0
- package/src/chrome/regions/StatusRegion.tsx +31 -0
- package/src/chrome/regions/TitleBarRegion.tsx +41 -0
- package/src/chrome/regions/ToolbarRegion.test.tsx +82 -0
- package/src/chrome/regions/ToolbarRegion.tsx +71 -0
- package/src/chrome/regions/ViewportRegion.less +42 -0
- package/src/chrome/regions/ViewportRegion.tsx +37 -0
- package/src/chrome/regions/regions.test.tsx +81 -0
- package/src/chrome/types.ts +127 -0
- package/src/config/builder.test.ts +87 -0
- package/src/config/builder.ts +224 -0
- package/src/config/fromConfigField.test.ts +93 -0
- package/src/config/fromConfigField.ts +74 -0
- package/src/config/index.ts +22 -0
- package/src/config/resolve.test.ts +120 -0
- package/src/config/resolve.ts +67 -0
- package/src/config/rules.ts +68 -0
- package/src/config/types.ts +108 -0
- package/src/config/useConfigSchema.ts +30 -0
- package/src/config/visible.ts +20 -0
- package/src/controls/ControlPanel.less +4 -58
- package/src/controls/ControlPanel.stories.tsx +86 -0
- package/src/controls/ControlPanel.test.tsx +223 -10
- package/src/controls/ControlPanel.tsx +202 -203
- package/src/dragdrop/DragDropRuntime.tsx +36 -18
- package/src/dragdrop/DragGhost.less +1 -1
- package/src/dragdrop/Palette.less +1 -1
- package/src/index.test.ts +18 -0
- package/src/index.ts +38 -3
- package/src/instrument/SineWave.smoke.test.tsx +9 -1
- package/src/instrument/defineInstrument.test.ts +38 -1
- package/src/instrument/defineInstrument.ts +31 -4
- package/src/instrument/index.ts +1 -0
- package/src/instrument/types.ts +38 -3
- package/src/instrument/validateConfigSchema.test.ts +13 -3
- package/src/instrument/validateConfigSchema.ts +7 -10
- package/src/job/index.ts +3 -0
- package/src/job/types.ts +47 -0
- package/src/job/useJob.test.tsx +210 -0
- package/src/job/useJob.ts +134 -0
- package/src/lab/Lab.chrome.test.tsx +65 -0
- package/src/lab/Lab.stories.tsx +1 -2
- package/src/lab/Lab.tsx +58 -15
- package/src/lab/LabContext.ts +6 -0
- package/src/lab/LabFullChrome.stories.tsx +308 -0
- package/src/lab/LabHeader.test.tsx +42 -0
- package/src/lab/LabHeader.tsx +57 -0
- package/src/lab/LabPalette.test.tsx +35 -0
- package/src/lab/LabPalette.tsx +34 -0
- package/src/lab/LabShell.less +50 -3
- package/src/lab/Workspace.less +1 -30
- package/src/lab/Workspace.surface.test.tsx +49 -0
- package/src/lab/Workspace.test.tsx +20 -10
- package/src/lab/Workspace.tsx +19 -9
- package/src/lab/index.ts +2 -0
- package/src/layers/AGENTS.md +1 -1
- package/src/layers/LayerList.less +31 -7
- package/src/layers/LayerList.tsx +17 -4
- package/src/passthrough/weasel-ui.ts +23 -0
- package/src/primitives/DragHandleGlyph.tsx +29 -0
- package/src/primitives/FloatingPanel.less +28 -0
- package/src/primitives/FloatingPanel.stories.tsx +46 -0
- package/src/primitives/FloatingPanel.test.tsx +240 -0
- package/src/primitives/FloatingPanel.tsx +190 -0
- package/src/primitives/FpsMeter.test.tsx +44 -0
- package/src/primitives/FpsMeter.tsx +20 -9
- package/src/primitives/JobProgress.less +69 -0
- package/src/primitives/JobProgress.stories.tsx +48 -0
- package/src/primitives/JobProgress.tsx +65 -0
- package/src/primitives/Legend.less +47 -0
- package/src/primitives/Legend.stories.tsx +32 -0
- package/src/primitives/Legend.test.tsx +57 -0
- package/src/primitives/Legend.tsx +40 -0
- package/src/primitives/Sidebar.less +32 -2
- package/src/primitives/StatusBar.less +7 -0
- package/src/primitives/StatusBar.tsx +10 -3
- package/src/primitives/Toolbar.less +50 -3
- package/src/primitives/Toolbar.test.tsx +67 -1
- package/src/primitives/Toolbar.tsx +72 -6
- package/src/primitives/ZoomControl.less +31 -0
- package/src/primitives/ZoomControl.tsx +76 -0
- package/src/primitives/floating.entry.test.tsx +14 -0
- package/src/primitives/index.ts +10 -2
- package/src/primitives/useRovingTabIndex.test.ts +27 -0
- package/src/primitives/useRovingTabIndex.ts +79 -0
- package/src/state/store.ts +16 -2
- package/src/state/toolSlot.test.ts +36 -0
- package/src/state/types.ts +8 -2
- package/src/state/view.test.ts +127 -0
- package/src/state/view.ts +18 -0
- package/src/styles.less +6 -0
- package/src/surface/AGENTS.md +64 -0
- package/src/surface/SurfaceContext.ts +5 -0
- package/src/surface/composeRects.test.ts +50 -0
- package/src/surface/composeRects.ts +19 -0
- package/src/surface/deviceRect.test.ts +40 -0
- package/src/surface/deviceRect.ts +19 -0
- package/src/surface/index.ts +7 -0
- package/src/surface/rect.ts +16 -0
- package/src/surface/useSurfaceTile.test.tsx +67 -0
- package/src/surface/useSurfaceTile.ts +32 -0
- package/src/surface/useTiledSurface.test.tsx +231 -0
- package/src/surface/useTiledSurface.ts +161 -0
- package/src/test-setup.ts +26 -0
- package/src/theme/base.less +19 -2
- package/src/theme/interstellar.tokens.json +6 -2
- package/src/tools/types.ts +24 -0
- package/src/trial/Trial.config.test.tsx +113 -0
- package/src/trial/Trial.job.test.tsx +73 -0
- package/src/trial/Trial.less +121 -4
- package/src/trial/Trial.stories.tsx +9 -1
- package/src/trial/Trial.test.tsx +67 -96
- package/src/trial/Trial.tsx +112 -45
- package/src/trial/TrialChrome.tsx +110 -64
- package/src/trial/TrialDragContext.ts +10 -0
- package/src/trial/TrialTitleBar.tsx +41 -0
- package/src/trial/index.ts +1 -15
- package/src/trial/trialChrome.borders.test.ts +38 -0
- package/src/ui/layers/LayerStack.less +9 -9
- package/src/ui/layers/LayerStack.tsx +1 -13
- package/src/ui/properties/CurveField.less +2 -2
- package/src/ui/properties/CurveField.test.tsx +11 -11
- package/src/ui/properties/PropertyGroup.less +2 -2
- package/src/ui/properties/PropertyPanel.less +55 -41
- package/src/ui/properties/SpeechBalloonPanels.stories.tsx +4 -2
- package/src/ui/properties/storyLayouts.tsx +2 -2
- package/dist/_dts/index-iAP6XbH3.d.ts +0 -132
- package/dist/_dts/types-x92Kfeme.d.ts +0 -62
- package/dist/chunk-3TYUJR7Z.js.map +0 -1
- package/dist/chunk-574LJAV4.js.map +0 -1
- package/dist/chunk-5R2ATYPJ.js +0 -201
- package/dist/chunk-5R2ATYPJ.js.map +0 -1
- package/dist/chunk-C6GJKPUI.js +0 -5986
- package/dist/chunk-C6GJKPUI.js.map +0 -1
- package/dist/chunk-DJLDIRFN.js.map +0 -1
- package/dist/chunk-NRKWVTVT.js.map +0 -1
- package/dist/chunk-PMAU3SEE.js +0 -126
- package/dist/chunk-PMAU3SEE.js.map +0 -1
- package/dist/chunk-SFL7NFKN.js.map +0 -1
- package/src/instrument/capabilityDetector.test.ts +0 -64
- package/src/instrument/capabilityDetector.ts +0 -20
- package/src/trial/DefaultSidebar.tsx +0 -29
- package/src/trial/DefaultStatusBar.tsx +0 -19
- package/src/trial/DefaultToolbar.tsx +0 -92
- package/src/trial/slotTypes.ts +0 -53
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import { useStore } from 'zustand/react';
|
|
3
|
+
import { PaletteRegion } from '../chrome/regions/PaletteRegion';
|
|
4
|
+
import type { TrialChromeContext, TrialContribution } from '../chrome/types';
|
|
5
|
+
import { LabStoreContext } from '../state/context';
|
|
6
|
+
import type { TrialTool } from '../tools/types';
|
|
7
|
+
|
|
8
|
+
/** Props for `<LabPalette>`. */
|
|
9
|
+
export interface LabPaletteProps {
|
|
10
|
+
tools: readonly TrialTool[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** The lab's tool strip. Writes the lab's tool slot, which every trial whose
|
|
14
|
+
* instrument declares no tools of its own resolves to. */
|
|
15
|
+
export function LabPalette({ tools }: LabPaletteProps) {
|
|
16
|
+
const storeCtx = useContext(LabStoreContext);
|
|
17
|
+
if (!storeCtx) throw new Error('[labkit] LabPalette requires <LabStoreProvider>');
|
|
18
|
+
const activeToolId = useStore(storeCtx.store, (s) => s.activeToolId);
|
|
19
|
+
const setLabTool = useStore(storeCtx.store, (s) => s.setLabTool);
|
|
20
|
+
|
|
21
|
+
const contributions: TrialContribution[] = tools.map((t) => ({
|
|
22
|
+
id: t.id,
|
|
23
|
+
region: 'palette',
|
|
24
|
+
group: t.group,
|
|
25
|
+
item: { icon: t.icon, label: t.label, shortcut: t.shortcut },
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
const ctx = {
|
|
29
|
+
activeToolId,
|
|
30
|
+
setActiveTool: setLabTool,
|
|
31
|
+
} as unknown as TrialChromeContext;
|
|
32
|
+
|
|
33
|
+
return <PaletteRegion contributions={contributions} ctx={ctx} />;
|
|
34
|
+
}
|
package/src/lab/LabShell.less
CHANGED
|
@@ -6,6 +6,12 @@
|
|
|
6
6
|
background: var(--wzl-surface);
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
// Standalone, the shell owns its surface. Inside `<Lab>` that surface is the
|
|
10
|
+
// one thing between the viewer and `.lk-lab`'s nebula, so it steps aside.
|
|
11
|
+
.lk-lab > .lk-shell {
|
|
12
|
+
background: transparent;
|
|
13
|
+
}
|
|
14
|
+
|
|
9
15
|
.lk-shell-header {
|
|
10
16
|
display: flex;
|
|
11
17
|
align-items: center;
|
|
@@ -18,8 +24,8 @@
|
|
|
18
24
|
|
|
19
25
|
.lk-shell-title {
|
|
20
26
|
margin: 0;
|
|
21
|
-
font-size:
|
|
22
|
-
font-weight:
|
|
27
|
+
font-size: var(--wzl-font-size-xl);
|
|
28
|
+
font-weight: var(--wzl-font-weight-bold);
|
|
23
29
|
color: var(--wzl-fg);
|
|
24
30
|
}
|
|
25
31
|
|
|
@@ -33,7 +39,19 @@
|
|
|
33
39
|
flex: 1;
|
|
34
40
|
min-height: 0;
|
|
35
41
|
overflow: auto;
|
|
36
|
-
padding: var(--
|
|
42
|
+
padding: var(--lk-workspace-pad);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// `.lk-shell-body` is a block container, so `flex: 1` here resolves to nothing
|
|
46
|
+
// and the workspace's `height: 100%` collapses to zero.
|
|
47
|
+
.lk-lab__body {
|
|
48
|
+
display: flex;
|
|
49
|
+
height: 100%;
|
|
50
|
+
min-height: 0;
|
|
51
|
+
|
|
52
|
+
> .lk-workspace {
|
|
53
|
+
min-width: 0;
|
|
54
|
+
}
|
|
37
55
|
}
|
|
38
56
|
|
|
39
57
|
.lk-shell-footer {
|
|
@@ -43,3 +61,32 @@
|
|
|
43
61
|
color: var(--wzl-fg-muted);
|
|
44
62
|
font-size: var(--wzl-font-size-sm);
|
|
45
63
|
}
|
|
64
|
+
|
|
65
|
+
.lk-lab-header__add {
|
|
66
|
+
display: inline-flex;
|
|
67
|
+
align-items: center;
|
|
68
|
+
gap: var(--wzl-space-xs);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// `Select` is a form field and defaults to `width: 100%`. Left to resolve that
|
|
72
|
+
// against a shrink-to-fit header row it swallows the slack, which pushed the
|
|
73
|
+
// consumer's own header content down to min-content and wrapped the row to
|
|
74
|
+
// three lines. See the `.lk-root` prefix convention in theme/base.less.
|
|
75
|
+
.lk-root .lk-lab-header__add-select {
|
|
76
|
+
width: 160px;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// The mode toggle is a preference, not a primary action: it sits quiet until
|
|
80
|
+
// the pointer is on it.
|
|
81
|
+
.lk-lab-header__mode {
|
|
82
|
+
opacity: 0.75;
|
|
83
|
+
// A consumer's own header content shares this row and does not shrink, so
|
|
84
|
+
// without this the segments compress past their labels and the pill track
|
|
85
|
+
// disappears entirely.
|
|
86
|
+
flex-shrink: 0;
|
|
87
|
+
|
|
88
|
+
&:hover,
|
|
89
|
+
&:focus-within {
|
|
90
|
+
opacity: 1;
|
|
91
|
+
}
|
|
92
|
+
}
|
package/src/lab/Workspace.less
CHANGED
|
@@ -5,36 +5,7 @@
|
|
|
5
5
|
min-height: 0;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
// Only rendered when `reorderable` — a tile is full of controls, so the drag
|
|
9
|
-
// target has to be its own strip rather than the whole pane.
|
|
10
8
|
.lk-trial-tile {
|
|
11
|
-
display: flex;
|
|
12
|
-
flex-direction: column;
|
|
13
9
|
height: 100%;
|
|
14
|
-
|
|
15
|
-
&__grip {
|
|
16
|
-
flex: 0 0 auto;
|
|
17
|
-
display: flex;
|
|
18
|
-
align-items: center;
|
|
19
|
-
justify-content: center;
|
|
20
|
-
height: 14px;
|
|
21
|
-
cursor: grab;
|
|
22
|
-
border-radius: var(--wzl-radius-sm) var(--wzl-radius-sm) 0 0;
|
|
23
|
-
background: var(--wzl-surface-sunken);
|
|
24
|
-
|
|
25
|
-
&:active { cursor: grabbing; }
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
&__grip-dots {
|
|
29
|
-
width: 28px;
|
|
30
|
-
height: 3px;
|
|
31
|
-
border-radius: 999px;
|
|
32
|
-
background: var(--wzl-fg-muted);
|
|
33
|
-
opacity: 0.5;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
&__body {
|
|
37
|
-
flex: 1;
|
|
38
|
-
min-height: 0;
|
|
39
|
-
}
|
|
10
|
+
min-height: 0;
|
|
40
11
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { render } from '@testing-library/react';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { SurfaceContext } from '../surface/SurfaceContext';
|
|
4
|
+
import type { SurfaceHandle } from '../surface/useTiledSurface';
|
|
5
|
+
import { Workspace } from './Workspace';
|
|
6
|
+
|
|
7
|
+
function fakeHandle(): SurfaceHandle {
|
|
8
|
+
return {
|
|
9
|
+
invalidate: vi.fn(),
|
|
10
|
+
invalidateAll: vi.fn(),
|
|
11
|
+
invalidateRects: vi.fn(),
|
|
12
|
+
registerTile: vi.fn(),
|
|
13
|
+
containerRef: vi.fn(),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('Workspace with a surface above it', () => {
|
|
18
|
+
it('invalidates rects when the tile set changes, because a re-tile moves tiles', () => {
|
|
19
|
+
const handle = fakeHandle();
|
|
20
|
+
const { rerender } = render(
|
|
21
|
+
<SurfaceContext.Provider value={handle}>
|
|
22
|
+
<Workspace ids={['a']} viewport={{ w: 800, h: 600 }}>
|
|
23
|
+
<div>a</div>
|
|
24
|
+
</Workspace>
|
|
25
|
+
</SurfaceContext.Provider>,
|
|
26
|
+
);
|
|
27
|
+
(handle.invalidateRects as ReturnType<typeof vi.fn>).mockClear();
|
|
28
|
+
|
|
29
|
+
rerender(
|
|
30
|
+
<SurfaceContext.Provider value={handle}>
|
|
31
|
+
<Workspace ids={['a', 'b']} viewport={{ w: 800, h: 600 }}>
|
|
32
|
+
<div>a</div>
|
|
33
|
+
<div>b</div>
|
|
34
|
+
</Workspace>
|
|
35
|
+
</SurfaceContext.Provider>,
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
expect(handle.invalidateRects).toHaveBeenCalled();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('renders unchanged with no surface above it', () => {
|
|
42
|
+
const { getByText } = render(
|
|
43
|
+
<Workspace ids={['a']} viewport={{ w: 800, h: 600 }}>
|
|
44
|
+
<div>a</div>
|
|
45
|
+
</Workspace>,
|
|
46
|
+
);
|
|
47
|
+
expect(getByText('a')).toBeInTheDocument();
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import { render, screen } from '@testing-library/react';
|
|
2
2
|
import { describe, expect, test, vi } from 'vitest';
|
|
3
|
+
import { useTrialDrag } from '../trial/TrialDragContext';
|
|
3
4
|
import { Workspace } from './Workspace';
|
|
4
5
|
|
|
6
|
+
/** Reports whether its tile supplied a drag source — the title bar is the
|
|
7
|
+
* drag surface now, so there is no grip element to assert on. */
|
|
8
|
+
function DragProbe({ label }: { label: string }) {
|
|
9
|
+
const drag = useTrialDrag();
|
|
10
|
+
return <div data-testid={`drag-${label}`}>{drag ? 'draggable' : 'static'}</div>;
|
|
11
|
+
}
|
|
12
|
+
|
|
5
13
|
const VIEWPORT = { w: 800, h: 600 };
|
|
6
14
|
|
|
7
15
|
describe('Workspace', () => {
|
|
@@ -88,13 +96,14 @@ describe('Workspace', () => {
|
|
|
88
96
|
const onReorder = vi.fn();
|
|
89
97
|
const { container } = render(
|
|
90
98
|
<Workspace ids={['a', 'b']} reorderable onReorder={onReorder} viewport={VIEWPORT}>
|
|
91
|
-
<
|
|
92
|
-
<
|
|
99
|
+
<DragProbe label="a" />
|
|
100
|
+
<DragProbe label="b" />
|
|
93
101
|
</Workspace>,
|
|
94
102
|
);
|
|
95
|
-
// The grid is controlled:
|
|
96
|
-
// itself. Order still comes from `ids`.
|
|
97
|
-
expect(
|
|
103
|
+
// The grid is controlled: each tile offers itself as a drag source and
|
|
104
|
+
// commits nothing itself. Order still comes from `ids`.
|
|
105
|
+
expect(screen.getByTestId('drag-a')).toHaveTextContent('draggable');
|
|
106
|
+
expect(screen.getByTestId('drag-b')).toHaveTextContent('draggable');
|
|
98
107
|
expect(onReorder).not.toHaveBeenCalled();
|
|
99
108
|
const nodes = [...container.querySelectorAll('[data-node]')].map((el) =>
|
|
100
109
|
el.getAttribute('data-node'),
|
|
@@ -102,13 +111,14 @@ describe('Workspace', () => {
|
|
|
102
111
|
expect(nodes).toEqual(['a', 'b']);
|
|
103
112
|
});
|
|
104
113
|
|
|
105
|
-
test('
|
|
106
|
-
|
|
114
|
+
test('offers no drag source unless reorderable', () => {
|
|
115
|
+
render(
|
|
107
116
|
<Workspace ids={['a', 'b']} viewport={VIEWPORT}>
|
|
108
|
-
<
|
|
109
|
-
<
|
|
117
|
+
<DragProbe label="a" />
|
|
118
|
+
<DragProbe label="b" />
|
|
110
119
|
</Workspace>,
|
|
111
120
|
);
|
|
112
|
-
expect(
|
|
121
|
+
expect(screen.getByTestId('drag-a')).toHaveTextContent('static');
|
|
122
|
+
expect(screen.getByTestId('drag-b')).toHaveTextContent('static');
|
|
113
123
|
});
|
|
114
124
|
});
|
package/src/lab/Workspace.tsx
CHANGED
|
@@ -11,12 +11,14 @@ import { asNodeId, createNode, gridStrategy, type NodeId, Store } from 'windease
|
|
|
11
11
|
import {
|
|
12
12
|
type ChromeMap,
|
|
13
13
|
Container,
|
|
14
|
-
DragHandle,
|
|
15
14
|
DragProvider,
|
|
16
15
|
Provider,
|
|
17
16
|
StrategyRegistryProvider,
|
|
18
17
|
} from 'windease/react';
|
|
19
18
|
|
|
19
|
+
import { useSurfaceOptional } from '../surface/useSurfaceTile';
|
|
20
|
+
import { TrialDragContext } from '../trial/TrialDragContext';
|
|
21
|
+
|
|
20
22
|
const ZONE_ID = asNodeId('lk-workspace');
|
|
21
23
|
const STRATEGIES = { grid: gridStrategy as never };
|
|
22
24
|
const KIND = 'trial';
|
|
@@ -93,7 +95,7 @@ export function Workspace({
|
|
|
93
95
|
const items = Children.toArray(children);
|
|
94
96
|
const idKey = ids ? ids.join(',') : `#${items.length}`;
|
|
95
97
|
// biome-ignore lint/correctness/useExhaustiveDependencies: idKey is the stable projection of items/ids; depending on those directly rebuilds every render and re-runs the sync effect forever
|
|
96
|
-
const nodeIds = useMemo(() => items.map((_, i) => asNodeId(ids?.[i] ?? `lk-ws-${i}`)), [idKey]);
|
|
98
|
+
const nodeIds = useMemo(() => items.map((_, i) => asNodeId(ids?.[i] ?? `lk-ws-${i}`)), [idKey]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
97
99
|
|
|
98
100
|
// Held in refs rather than depended on: a fresh object each render would
|
|
99
101
|
// re-run the sync effect, and only a newly registered tile reads `layout`.
|
|
@@ -119,6 +121,10 @@ export function Workspace({
|
|
|
119
121
|
}
|
|
120
122
|
const store = storeRef.current;
|
|
121
123
|
|
|
124
|
+
// A tile that only moves reports nothing to a ResizeObserver, and only this
|
|
125
|
+
// component knows the grid moved one. Optional: a lab may own no surface.
|
|
126
|
+
const surface = useSurfaceOptional();
|
|
127
|
+
|
|
122
128
|
useLayoutEffect(() => {
|
|
123
129
|
store.updateContainerConfig(ZONE_ID, { resizable, gap, padding });
|
|
124
130
|
}, [store, resizable, gap, padding]);
|
|
@@ -139,6 +145,13 @@ export function Workspace({
|
|
|
139
145
|
store.setChildOrder(ZONE_ID, [...nodeIds]);
|
|
140
146
|
}, [store, nodeIds]);
|
|
141
147
|
|
|
148
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: nodeIds is the signal that the tile set changed — a re-tile moves tiles without resizing any — not a value this reads
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
if (!surface) return;
|
|
151
|
+
surface.invalidateRects();
|
|
152
|
+
return store.events.on('node.placementChanged', () => surface.invalidateRects());
|
|
153
|
+
}, [store, surface, nodeIds]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
154
|
+
|
|
142
155
|
useEffect(() => {
|
|
143
156
|
if (!onLayoutChange) return;
|
|
144
157
|
return store.events.on('node.placementChanged', () => {
|
|
@@ -162,14 +175,11 @@ export function Workspace({
|
|
|
162
175
|
[KIND]: ({ node }) => {
|
|
163
176
|
const content = byId.get(node.id) ?? null;
|
|
164
177
|
if (!reorderable) return content;
|
|
165
|
-
// A tile is full of controls, so the whole thing can't be the handle.
|
|
166
178
|
return (
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
<div className="lk-trial-tile__body">{content}</div>
|
|
172
|
-
</div>
|
|
179
|
+
// No grip strip: the trial's own title bar is the drag surface.
|
|
180
|
+
<TrialDragContext.Provider value={{ nodeId: node.id }}>
|
|
181
|
+
<div className="lk-trial-tile">{content}</div>
|
|
182
|
+
</TrialDragContext.Provider>
|
|
173
183
|
);
|
|
174
184
|
},
|
|
175
185
|
};
|
package/src/lab/index.ts
CHANGED
|
@@ -2,6 +2,8 @@ export type { LabProps } from './Lab';
|
|
|
2
2
|
export { Lab } from './Lab';
|
|
3
3
|
export type { LabContextValue } from './LabContext';
|
|
4
4
|
export { LabContext, useLabContext } from './LabContext';
|
|
5
|
+
export type { LabPaletteProps } from './LabPalette';
|
|
6
|
+
export { LabPalette } from './LabPalette';
|
|
5
7
|
export type { LabShellProps } from './LabShell';
|
|
6
8
|
export { LabShell } from './LabShell';
|
|
7
9
|
export type { WorkspaceProps } from './Workspace';
|
package/src/layers/AGENTS.md
CHANGED
|
@@ -46,7 +46,7 @@ This is intended for legend/HUD layers that should never be toggled off. Visibil
|
|
|
46
46
|
|
|
47
47
|
## Drag handle customization
|
|
48
48
|
|
|
49
|
-
The drag handle is a button with
|
|
49
|
+
The drag handle is a button with class `lk-layer-list__handle` holding `<DragHandleGlyph>`, 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.
|
|
50
50
|
|
|
51
51
|
Pointer capture is acquired on `pointerdown` and released on `pointerup`, so dragging works across the document without requiring window-level listeners.
|
|
52
52
|
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
// No padding of its own: whatever mounts the list insets it. The sidebar
|
|
2
|
+
// section body already does, and a second inset here put the grip 22px off a
|
|
3
|
+
// 161px-wide sidebar's edge.
|
|
1
4
|
.lk-layer-list {
|
|
2
5
|
display: flex;
|
|
3
6
|
flex-direction: column;
|
|
4
|
-
gap:
|
|
5
|
-
padding: var(--wzl-space-sm);
|
|
7
|
+
gap: 1px;
|
|
6
8
|
}
|
|
7
9
|
|
|
8
10
|
.lk-layer-list__empty {
|
|
@@ -14,9 +16,9 @@
|
|
|
14
16
|
.lk-layer-list__row {
|
|
15
17
|
display: flex;
|
|
16
18
|
align-items: center;
|
|
17
|
-
gap: var(--wzl-space-
|
|
18
|
-
padding:
|
|
19
|
-
border-radius:
|
|
19
|
+
gap: var(--wzl-space-xs);
|
|
20
|
+
padding: 1px 2px;
|
|
21
|
+
border-radius: var(--wzl-radius-sm);
|
|
20
22
|
user-select: none;
|
|
21
23
|
}
|
|
22
24
|
|
|
@@ -33,10 +35,28 @@
|
|
|
33
35
|
opacity: 0.7;
|
|
34
36
|
}
|
|
35
37
|
|
|
36
|
-
|
|
38
|
+
// Prefixed with `.lk-root` to outrank base.less's `:where(button)` default,
|
|
39
|
+
// which would otherwise wrap this grip in a 24px chrome box with a border, an
|
|
40
|
+
// elevated fill and 12px of side padding — around a glyph a third that size.
|
|
41
|
+
.lk-root .lk-layer-list__handle {
|
|
42
|
+
display: inline-flex;
|
|
43
|
+
align-items: center;
|
|
44
|
+
height: auto;
|
|
45
|
+
// Transparent, so the grab target is comfortable while nothing but the dots
|
|
46
|
+
// is drawn. The negative margin keeps it from widening the row.
|
|
47
|
+
padding: 4px;
|
|
48
|
+
margin: -4px;
|
|
49
|
+
border: 0;
|
|
50
|
+
border-radius: 0;
|
|
51
|
+
background: transparent;
|
|
52
|
+
backdrop-filter: none;
|
|
53
|
+
-webkit-backdrop-filter: none;
|
|
37
54
|
cursor: grab;
|
|
38
55
|
color: var(--wzl-fg-muted);
|
|
39
|
-
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.lk-root .lk-layer-list__handle:hover {
|
|
59
|
+
color: var(--wzl-fg);
|
|
40
60
|
}
|
|
41
61
|
|
|
42
62
|
.lk-layer-list__handle:active {
|
|
@@ -51,3 +71,7 @@
|
|
|
51
71
|
flex: 1;
|
|
52
72
|
font-size: var(--wzl-font-size-sm);
|
|
53
73
|
}
|
|
74
|
+
|
|
75
|
+
.lk-layer-list input[type='checkbox'] {
|
|
76
|
+
accent-color: var(--wzl-accent);
|
|
77
|
+
}
|
package/src/layers/LayerList.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type PointerEvent, useRef, useState } from 'react';
|
|
2
2
|
import type { LayerDescriptor } from '../instrument/types';
|
|
3
|
+
import { DragHandleGlyph } from '../primitives/DragHandleGlyph';
|
|
3
4
|
|
|
4
5
|
/** Props for `<LayerList>`. */
|
|
5
6
|
export interface LayerListProps {
|
|
@@ -14,6 +15,8 @@ interface DragState {
|
|
|
14
15
|
pointerId: number;
|
|
15
16
|
fromIndex: number;
|
|
16
17
|
startY: number;
|
|
18
|
+
/** Row height plus row gap, measured when the drag starts. */
|
|
19
|
+
pitch: number;
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
/** A reorderable list of layers with per-layer visibility toggles. Layers
|
|
@@ -34,15 +37,25 @@ export function LayerList({ layers, visibility, onReorder, onToggle, className }
|
|
|
34
37
|
|
|
35
38
|
const startDrag = (e: PointerEvent<HTMLElement>, index: number) => {
|
|
36
39
|
e.currentTarget.setPointerCapture(e.pointerId);
|
|
37
|
-
|
|
40
|
+
// Measured at grab time rather than hardcoded: a restyle that changes row
|
|
41
|
+
// height or gap would otherwise silently skew every drag distance.
|
|
42
|
+
const row = e.currentTarget.closest('.lk-layer-list__row');
|
|
43
|
+
const list = row?.parentElement;
|
|
44
|
+
const gap = list ? Number.parseFloat(getComputedStyle(list).rowGap) || 0 : 0;
|
|
45
|
+
const pitch = row ? row.getBoundingClientRect().height + gap : 0;
|
|
46
|
+
dragRef.current = {
|
|
47
|
+
pointerId: e.pointerId,
|
|
48
|
+
fromIndex: index,
|
|
49
|
+
startY: e.clientY,
|
|
50
|
+
pitch: pitch > 0 ? pitch : 1,
|
|
51
|
+
};
|
|
38
52
|
setDragIndex(index);
|
|
39
53
|
};
|
|
40
54
|
|
|
41
55
|
const moveDrag = (e: PointerEvent<HTMLElement>) => {
|
|
42
56
|
const drag = dragRef.current;
|
|
43
57
|
if (!drag || drag.pointerId !== e.pointerId) return;
|
|
44
|
-
const
|
|
45
|
-
const delta = Math.round((e.clientY - drag.startY) / rowHeight);
|
|
58
|
+
const delta = Math.round((e.clientY - drag.startY) / drag.pitch);
|
|
46
59
|
const target = Math.min(reorderable.length - 1, Math.max(0, drag.fromIndex + delta));
|
|
47
60
|
setDragIndex(target);
|
|
48
61
|
};
|
|
@@ -82,7 +95,7 @@ export function LayerList({ layers, visibility, onReorder, onToggle, className }
|
|
|
82
95
|
onPointerMove={moveDrag}
|
|
83
96
|
onPointerUp={endDrag}
|
|
84
97
|
>
|
|
85
|
-
|
|
98
|
+
<DragHandleGlyph size={13} />
|
|
86
99
|
</button>
|
|
87
100
|
<input
|
|
88
101
|
className="lk-layer-list__check"
|
|
@@ -114,6 +114,29 @@ export {
|
|
|
114
114
|
Select,
|
|
115
115
|
SelectItem,
|
|
116
116
|
type SelectItemProps,
|
|
117
|
+
type BuiltinPref,
|
|
118
|
+
isPrefLeaf,
|
|
119
|
+
type PrefBoolean,
|
|
120
|
+
type PrefBooleanControl,
|
|
121
|
+
type PrefColor,
|
|
122
|
+
type PrefCustom,
|
|
123
|
+
type PrefEnum,
|
|
124
|
+
type PrefEnumControl,
|
|
125
|
+
type PrefEnumEncoding,
|
|
126
|
+
type PrefGroup,
|
|
127
|
+
type PrefKind,
|
|
128
|
+
type PrefLeaf,
|
|
129
|
+
type PrefNumber,
|
|
130
|
+
type PrefNumberControl,
|
|
131
|
+
type PrefNumberUnit,
|
|
132
|
+
type PrefObject,
|
|
133
|
+
type PrefPaint,
|
|
134
|
+
type PrefRenderContext,
|
|
135
|
+
type PrefRenderer,
|
|
136
|
+
type PrefString,
|
|
137
|
+
type PrefStringControl,
|
|
138
|
+
prefValueAtPath,
|
|
139
|
+
visiblePrefSubtree,
|
|
117
140
|
type SelectOption,
|
|
118
141
|
type SelectProps,
|
|
119
142
|
Sidebar,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Props for `<DragHandleGlyph>`. */
|
|
2
|
+
export interface DragHandleGlyphProps {
|
|
3
|
+
/** Height in px; width scales with it. Default 16. */
|
|
4
|
+
size?: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** The two-column dot grip used on anything draggable by a handle.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately not part of `@weasel-js/ui`'s icon register: that register is
|
|
10
|
+
* outline strokes at a fixed weight, and a grip is filled dots. Forcing it in
|
|
11
|
+
* would either break the register's rule or produce a worse glyph. */
|
|
12
|
+
export function DragHandleGlyph({ size = 16 }: DragHandleGlyphProps) {
|
|
13
|
+
return (
|
|
14
|
+
<svg
|
|
15
|
+
width={(size * 8) / 16}
|
|
16
|
+
height={size}
|
|
17
|
+
viewBox="0 0 8 16"
|
|
18
|
+
fill="currentColor"
|
|
19
|
+
aria-hidden="true"
|
|
20
|
+
>
|
|
21
|
+
<circle cx="2" cy="3" r="1.1" />
|
|
22
|
+
<circle cx="6" cy="3" r="1.1" />
|
|
23
|
+
<circle cx="2" cy="8" r="1.1" />
|
|
24
|
+
<circle cx="6" cy="8" r="1.1" />
|
|
25
|
+
<circle cx="2" cy="13" r="1.1" />
|
|
26
|
+
<circle cx="6" cy="13" r="1.1" />
|
|
27
|
+
</svg>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
.lk-floating-panel {
|
|
2
|
+
cursor: grab;
|
|
3
|
+
touch-action: none;
|
|
4
|
+
user-select: none;
|
|
5
|
+
background: var(--wzl-surface);
|
|
6
|
+
border: var(--wzl-border-w) solid var(--wzl-border);
|
|
7
|
+
border-radius: var(--wzl-radius-md);
|
|
8
|
+
padding: var(--wzl-space-sm);
|
|
9
|
+
box-shadow: 0 6px 18px rgb(0 0 0 / 40%);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// An item with no measured size is withheld from layout, so the first paint
|
|
13
|
+
// would otherwise land at the top-left before jumping to its anchor.
|
|
14
|
+
.lk-floating-panel:not([data-placed='true']) {
|
|
15
|
+
visibility: hidden;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.lk-floating-panel[data-dragging='true'] {
|
|
19
|
+
cursor: grabbing;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.lk-floating-panel-story-host {
|
|
23
|
+
position: relative;
|
|
24
|
+
width: 520px;
|
|
25
|
+
height: 340px;
|
|
26
|
+
background: var(--wzl-surface-sunken);
|
|
27
|
+
border: var(--wzl-border-w) solid var(--wzl-border);
|
|
28
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { FloatingPanel } from './FloatingPanel';
|
|
3
|
+
import { Legend } from './Legend';
|
|
4
|
+
|
|
5
|
+
// The panel positions against its offset parent, so a story needs a sized,
|
|
6
|
+
// positioned host or there is nothing for it to float in.
|
|
7
|
+
const meta: Meta<typeof FloatingPanel> = {
|
|
8
|
+
title: 'labkit/Primitives/FloatingPanel',
|
|
9
|
+
component: FloatingPanel,
|
|
10
|
+
decorators: [
|
|
11
|
+
(Story) => (
|
|
12
|
+
<div className="lk-floating-panel-story-host">
|
|
13
|
+
<Story />
|
|
14
|
+
</div>
|
|
15
|
+
),
|
|
16
|
+
],
|
|
17
|
+
args: { children: 'drag me' },
|
|
18
|
+
};
|
|
19
|
+
export default meta;
|
|
20
|
+
|
|
21
|
+
type Story = StoryObj<typeof FloatingPanel>;
|
|
22
|
+
|
|
23
|
+
export const BottomLeft: Story = {};
|
|
24
|
+
|
|
25
|
+
export const TopRight: Story = { args: { anchor: 'top-right' } };
|
|
26
|
+
|
|
27
|
+
export const TwoCornersOnly: Story = {
|
|
28
|
+
args: { anchor: 'top-left', snapCorners: ['top-left', 'bottom-right'] },
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const HoldingALegend: Story = {
|
|
32
|
+
args: {
|
|
33
|
+
anchor: 'bottom-right',
|
|
34
|
+
children: (
|
|
35
|
+
<Legend
|
|
36
|
+
entries={[
|
|
37
|
+
{ key: 'contour', label: 'contour', color: '#7d7f86' },
|
|
38
|
+
{ key: 'floor', label: 'bend floor', color: '#9a9ca3', mark: 'dash' },
|
|
39
|
+
{ key: 'authored', label: 'authored', color: '#2aa87a', mark: 'dot' },
|
|
40
|
+
]}
|
|
41
|
+
/>
|
|
42
|
+
),
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const Remembered: Story = { args: { storageKey: 'labkit.story.panel' } };
|