@weasel-js/labkit 1.5.0 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -4
- package/dist/_dts/{CanvasStackContext-CqkxkFJW.d.ts → CanvasStackContext-CJNMS6mz.d.ts} +1 -1
- package/dist/_dts/{PrefsForm.d-DDHFANkc.d.ts → PrefsForm.d-DPy9I8It.d.ts} +4 -0
- package/dist/_dts/Select.d-Gwr8fLpK.d.ts +453 -0
- package/dist/_dts/{frac-8R6UmlvN.d.ts → frac-B4okC2lX.d.ts} +42 -4
- package/dist/_dts/{index-retnAut7.d.ts → index-BNdz-tNd.d.ts} +90 -10
- package/dist/_dts/{index-B8uu2ba7.d.ts → index-CSJoj4LC.d.ts} +17 -6
- package/dist/_dts/{types-Ca2LCnPq.d.ts → types-DzoNY1pO.d.ts} +28 -4
- package/dist/_dts/{usePanZoom-nS798rOF.d.ts → usePanZoom-Pzf9f2jh.d.ts} +2 -2
- package/dist/_dts/{useTrialState-jcx_TdU6.d.ts → useTrialState-uLlmaccn.d.ts} +1 -1
- package/dist/canvas/index.d.ts +5 -5
- package/dist/canvas/index.js +1 -1
- package/dist/chrome/index.d.ts +4 -4
- package/dist/chrome/index.js +7 -7
- package/dist/chunk-7PFH3CNM.js +150 -0
- package/dist/chunk-7PFH3CNM.js.map +1 -0
- package/dist/{chunk-PGETSEDK.js → chunk-CEIUBPPW.js} +10 -6
- package/dist/chunk-CEIUBPPW.js.map +1 -0
- package/dist/chunk-EH7UEQV7.js +590 -0
- package/dist/chunk-EH7UEQV7.js.map +1 -0
- package/dist/{chunk-EKIICY6X.js → chunk-EHGVROVV.js} +37 -25
- package/dist/chunk-EHGVROVV.js.map +1 -0
- package/dist/{chunk-ZPH5WTWF.js → chunk-ERFMC6WU.js} +142 -66
- package/dist/chunk-ERFMC6WU.js.map +1 -0
- package/dist/{chunk-NS64DXMN.js → chunk-IHUVQ3TX.js} +39 -16
- package/dist/chunk-IHUVQ3TX.js.map +1 -0
- package/dist/{chunk-QLSV2N3G.js → chunk-IJJUK4FU.js} +25 -13
- package/dist/chunk-IJJUK4FU.js.map +1 -0
- package/dist/{chunk-67SJLMC7.js → chunk-IXTQI7JX.js} +45 -12
- package/dist/chunk-IXTQI7JX.js.map +1 -0
- package/dist/{chunk-MUKOW3TC.js → chunk-MDVTK5U3.js} +4 -4
- package/dist/{chunk-MUKOW3TC.js.map → chunk-MDVTK5U3.js.map} +1 -1
- package/dist/{chunk-UU3NJO6N.js → chunk-PHK4W562.js} +3 -3
- package/dist/{chunk-UU3NJO6N.js.map → chunk-PHK4W562.js.map} +1 -1
- package/dist/chunk-PJ7WEQBJ.js +18 -0
- package/dist/chunk-PJ7WEQBJ.js.map +1 -0
- package/dist/chunk-T3EC2DXD.js +12628 -0
- package/dist/chunk-T3EC2DXD.js.map +1 -0
- package/dist/{chunk-KJALCDWE.js → chunk-UN4U4P63.js} +4 -4
- package/dist/{chunk-KJALCDWE.js.map → chunk-UN4U4P63.js.map} +1 -1
- package/dist/{chunk-NDRYLVOW.js → chunk-Z76KEZAX.js} +2 -2
- package/dist/chunk-Z76KEZAX.js.map +1 -0
- package/dist/{chunk-6ZDGOZQV.js → chunk-ZDHY47EV.js} +70 -17
- package/dist/chunk-ZDHY47EV.js.map +1 -0
- package/dist/config/index.d.ts +60 -16
- package/dist/config/index.js +6 -4
- package/dist/controls/index.d.ts +3 -3
- package/dist/controls/index.js +4 -4
- package/dist/dragdrop/index.d.ts +3 -3
- package/dist/index.d.ts +177 -53
- package/dist/index.js +1333 -1683
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +4 -4
- package/dist/layers/index.js +2 -2
- package/dist/loupe/index.d.ts +67 -16
- package/dist/loupe/index.js +1 -1
- package/dist/passthrough/weasel-ui.d.ts +26 -202
- package/dist/passthrough/weasel-ui.js +1 -1
- package/dist/primitives/index.d.ts +3 -1
- package/dist/primitives/index.js +3 -3
- package/dist/state/index.d.ts +5 -5
- package/dist/state/index.js +5 -3
- package/dist/state/index.js.map +1 -1
- package/dist/styles.css +153 -40
- package/dist/surface/index.js +1 -1
- package/dist/ui/layers/index.js +1 -1
- package/dist/undo/index.d.ts +3 -3
- package/package.json +9 -9
- package/src/annotations/AnnotationOverlay.tsx +3 -0
- package/src/annotations/Annotations.less +4 -0
- package/src/annotations/Annotations.overlay.test.tsx +20 -0
- package/src/annotations/svgNodes.ts +1 -0
- package/src/annotations/toolMap.test.ts +9 -5
- package/src/annotations/toolMap.ts +3 -0
- package/src/canvas/CanvasStack.stories.tsx +1 -1
- package/src/canvas/usePanZoom.test.ts +28 -0
- package/src/canvas/usePanZoom.ts +11 -4
- package/src/chrome/ChromeRegions.stories.tsx +1 -1
- package/src/chrome/LabChrome.tsx +21 -5
- package/src/chrome/builtins.test.ts +1 -0
- package/src/chrome/builtins.tsx +1 -0
- package/src/chrome/index.ts +1 -0
- package/src/chrome/labTypes.ts +2 -2
- package/src/chrome/regions/PaletteRegion.tsx +1 -6
- package/src/chrome/regions/SidebarRegion.stories.tsx +44 -0
- package/src/chrome/regions/ToolbarRegion.tsx +11 -7
- package/src/chrome/types.ts +3 -0
- package/src/config/auto.test.ts +12 -0
- package/src/config/auto.ts +15 -0
- package/src/config/autoConfig.test.ts +107 -0
- package/src/config/autoConfig.ts +153 -0
- package/src/config/builder.test.ts +38 -0
- package/src/config/builder.ts +53 -8
- package/src/config/index.ts +3 -0
- package/src/config/path.ts +7 -2
- package/src/config/resolve.test.ts +25 -0
- package/src/config/resolve.ts +17 -1
- package/src/config/types.ts +32 -2
- package/src/config/useResolvedConfig.ts +26 -0
- package/src/controls/AutoControls.stories.tsx +88 -0
- package/src/controls/ControlPanel.stories.tsx +42 -1
- package/src/controls/ControlPanel.test.tsx +514 -4
- package/src/controls/ControlPanel.tsx +557 -80
- package/src/index.ts +29 -1
- package/src/instrument/serializers.ts +29 -1
- package/src/lab/Lab.chrome.test.tsx +16 -0
- package/src/lab/Lab.stories.tsx +1 -1
- package/src/lab/Lab.surface.test.tsx +7 -2
- package/src/lab/Lab.test.tsx +79 -1
- package/src/lab/Lab.tsx +105 -13
- package/src/lab/LabContext.ts +8 -0
- package/src/lab/LabFit.stories.tsx +4 -4
- package/src/lab/LabFullChrome.stories.tsx +1 -1
- package/src/lab/LabHeader.tsx +0 -1
- package/src/lab/LabRoot.test.tsx +56 -0
- package/src/lab/LabRoot.tsx +47 -0
- package/src/lab/LabShell.less +24 -3
- package/src/lab/LabShell.stories.tsx +50 -1
- package/src/lab/LabShell.tsx +18 -27
- package/src/lab/LabSwitcher.less +10 -10
- package/src/lab/LabSwitcher.test.tsx +14 -9
- package/src/lab/LabSwitcher.tsx +8 -4
- package/src/lab/Workspace.less +1 -1
- package/src/lab/Workspace.stories.tsx +1 -1
- package/src/lab/Workspace.surface.test.tsx +1 -1
- package/src/lab/index.ts +2 -0
- package/src/layers/LayerList.stories.tsx +1 -1
- package/src/loupe/AGENTS.md +16 -4
- package/src/loupe/LoupeGestures.tsx +67 -0
- package/src/loupe/TrialLoupe.tsx +43 -28
- package/src/loupe/index.ts +4 -0
- package/src/loupe/loupeActions.ts +98 -0
- package/src/loupe/useLoupe.test.tsx +59 -2
- package/src/loupe/useLoupe.ts +28 -48
- package/src/passthrough/weasel-ui.ts +3 -3
- package/src/primitives/FloatingPanel.stories.tsx +1 -1
- package/src/primitives/FpsMeter.stories.tsx +1 -1
- package/src/primitives/JobProgress.stories.tsx +1 -1
- package/src/primitives/Legend.stories.tsx +1 -1
- package/src/primitives/ScaleIndicator.stories.tsx +1 -1
- package/src/primitives/Sidebar.less +7 -0
- package/src/primitives/Sidebar.stories.tsx +1 -1
- package/src/primitives/Split.test.tsx +25 -0
- package/src/primitives/Split.tsx +36 -20
- package/src/primitives/StatusBar.stories.tsx +1 -1
- package/src/primitives/Toolbar.stories.tsx +1 -1
- package/src/specimen/Specimen.less +95 -0
- package/src/specimen/Specimen.stories.tsx +13 -0
- package/src/specimen/Specimen.test.tsx +13 -0
- package/src/specimen/Specimen.tsx +888 -0
- package/src/state/Persistence.stories.tsx +4 -4
- package/src/state/store.test.ts +65 -0
- package/src/state/store.ts +32 -8
- package/src/state/types.ts +26 -2
- package/src/state/useTrialState.test.tsx +85 -0
- package/src/state/useTrialState.ts +9 -1
- package/src/styles.less +1 -0
- package/src/surface/index.ts +1 -1
- package/src/surface/useSurfaceTile.test.tsx +1 -1
- package/src/surface/useTiledSurface.test.tsx +65 -10
- package/src/surface/useTiledSurface.ts +23 -11
- package/src/theme/Interstellar.stories.tsx +2 -2
- package/src/theme/interstellar.test.ts +16 -5
- package/src/theme/interstellar.theme.json +180 -0
- package/src/theme/interstellar.ts +8 -5
- package/src/tools/labTool.ts +3 -3
- package/src/trial/Trial.annotations.test.tsx +13 -4
- package/src/trial/Trial.canvas.test.tsx +32 -0
- package/src/trial/Trial.config.test.tsx +75 -2
- package/src/trial/Trial.less +4 -0
- package/src/trial/Trial.stories.tsx +4 -1
- package/src/trial/Trial.test.tsx +3 -0
- package/src/trial/Trial.tsx +21 -14
- package/src/trial/TrialChrome.tsx +17 -3
- package/src/trial/trialOps.test.ts +101 -1
- package/src/trial/trialOps.ts +62 -4
- package/dist/_dts/ToggleBar.d-DbHoWKIA.d.ts +0 -116
- package/dist/chunk-67SJLMC7.js.map +0 -1
- package/dist/chunk-6ZDGOZQV.js.map +0 -1
- package/dist/chunk-E44SU6XS.js +0 -50
- package/dist/chunk-E44SU6XS.js.map +0 -1
- package/dist/chunk-EKIICY6X.js.map +0 -1
- package/dist/chunk-NDRYLVOW.js.map +0 -1
- package/dist/chunk-NS64DXMN.js.map +0 -1
- package/dist/chunk-PGETSEDK.js.map +0 -1
- package/dist/chunk-QLSV2N3G.js.map +0 -1
- package/dist/chunk-S2SHZRD7.js +0 -7681
- package/dist/chunk-S2SHZRD7.js.map +0 -1
- package/dist/chunk-TQBBYTAF.js +0 -280
- package/dist/chunk-TQBBYTAF.js.map +0 -1
- package/dist/chunk-ZPH5WTWF.js.map +0 -1
- package/src/theme/interstellar.tokens.json +0 -61
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { act, fireEvent, render, screen } from '@testing-library/react';
|
|
2
|
+
import { WeaselProvider } from '@weasel-js/core';
|
|
2
3
|
import { type RefObject, StrictMode, useRef } from 'react';
|
|
3
4
|
import { describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { LoupeGestures } from './LoupeGestures';
|
|
4
6
|
import { resolveLoupe } from './types';
|
|
5
7
|
import { type LoupeState, useLoupe } from './useLoupe';
|
|
6
8
|
|
|
@@ -15,18 +17,29 @@ interface HarnessProps {
|
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
/** Reports the loupe's state as text, which is every assertion jsdom can make
|
|
18
|
-
* about a magnifier — that the lens shows the right region is a screenshot.
|
|
20
|
+
* about a magnifier — that the lens shows the right region is a screenshot.
|
|
21
|
+
*
|
|
22
|
+
* Mounts `<LoupeGestures>` the way `<TrialLoupe>` does, so the key and wheel
|
|
23
|
+
* cases below exercise the real dispatcher route rather than a stand-in. */
|
|
19
24
|
function Harness({ enabled = true, seen, sample, ...rest }: HarnessProps) {
|
|
20
25
|
const hostRef = useRef<HTMLDivElement | null>(null);
|
|
26
|
+
const capability = resolveLoupe(rest);
|
|
21
27
|
const loupe = useLoupe({
|
|
22
28
|
hostRef: hostRef as RefObject<HTMLElement | null>,
|
|
23
29
|
enabled,
|
|
24
30
|
sample,
|
|
25
|
-
capability
|
|
31
|
+
capability,
|
|
26
32
|
});
|
|
27
33
|
seen?.(loupe);
|
|
28
34
|
return (
|
|
29
35
|
<div ref={hostRef} data-testid="host">
|
|
36
|
+
<WeaselProvider isolate>
|
|
37
|
+
<LoupeGestures
|
|
38
|
+
hostRef={hostRef as RefObject<HTMLElement | null>}
|
|
39
|
+
input={loupe.input}
|
|
40
|
+
peekKey={capability.peekKey ?? null}
|
|
41
|
+
/>
|
|
42
|
+
</WeaselProvider>
|
|
30
43
|
<span data-testid="visible">{String(loupe.visible)}</span>
|
|
31
44
|
<span data-testid="aim">{`${loupe.aim.x},${loupe.aim.y}`}</span>
|
|
32
45
|
<span data-testid="factor">{loupe.factor}</span>
|
|
@@ -94,6 +107,7 @@ describe('useLoupe', () => {
|
|
|
94
107
|
act(() => {
|
|
95
108
|
fireEvent.keyDown(window, { key: 'Alt' });
|
|
96
109
|
});
|
|
110
|
+
expect(read('visible')).toBe('true');
|
|
97
111
|
act(() => {
|
|
98
112
|
fireEvent.blur(window);
|
|
99
113
|
});
|
|
@@ -148,6 +162,49 @@ describe('useLoupe', () => {
|
|
|
148
162
|
expect(wheel.defaultPrevented).toBe(true);
|
|
149
163
|
});
|
|
150
164
|
|
|
165
|
+
it('stops a claimed wheel reaching the pan-zoom handler above it', () => {
|
|
166
|
+
// The real handler is React's, delegated to the root container, so what
|
|
167
|
+
// decides whether pan-zoom runs is whether the event bubbles past the
|
|
168
|
+
// host — `defaultPrevented` says nothing about that. A listener on an
|
|
169
|
+
// ancestor is the proxy for React's root one.
|
|
170
|
+
const above = vi.fn();
|
|
171
|
+
const { container } = render(<Harness />);
|
|
172
|
+
container.addEventListener('wheel', above);
|
|
173
|
+
|
|
174
|
+
const host = screen.getByTestId('host');
|
|
175
|
+
move(40, 25);
|
|
176
|
+
act(() => {
|
|
177
|
+
host.dispatchEvent(
|
|
178
|
+
new WheelEvent('wheel', { deltaY: -100, bubbles: true, cancelable: true }),
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
expect(above).not.toHaveBeenCalled();
|
|
182
|
+
|
|
183
|
+
fireEvent.pointerLeave(host);
|
|
184
|
+
act(() => {
|
|
185
|
+
host.dispatchEvent(
|
|
186
|
+
new WheelEvent('wheel', { deltaY: -100, bubbles: true, cancelable: true }),
|
|
187
|
+
);
|
|
188
|
+
});
|
|
189
|
+
expect(above).toHaveBeenCalledTimes(1);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('leaves the native context menu and file drops to the lab', () => {
|
|
193
|
+
// The dispatcher suppresses `contextmenu` unconditionally and makes its
|
|
194
|
+
// element a drop target; the loupe mounts with both channels off, so a
|
|
195
|
+
// lab that turns a magnifier on keeps its right-click and its drops.
|
|
196
|
+
render(<Harness />);
|
|
197
|
+
const host = screen.getByTestId('host');
|
|
198
|
+
move(40, 25);
|
|
199
|
+
|
|
200
|
+
const menu = new MouseEvent('contextmenu', { bubbles: true, cancelable: true });
|
|
201
|
+
act(() => {
|
|
202
|
+
host.dispatchEvent(menu);
|
|
203
|
+
});
|
|
204
|
+
expect(menu.defaultPrevented).toBe(false);
|
|
205
|
+
expect(host.classList.contains('weasel-dropover')).toBe(false);
|
|
206
|
+
});
|
|
207
|
+
|
|
151
208
|
it('reports the color under the aim', () => {
|
|
152
209
|
const onColorChange = vi.fn();
|
|
153
210
|
render(
|
package/src/loupe/useLoupe.ts
CHANGED
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
type LoupePoint,
|
|
6
6
|
} from '@weasel-js/loupe';
|
|
7
7
|
import { type RefObject, useCallback, useEffect, useReducer, useRef } from 'react';
|
|
8
|
+
import type { LoupeInputApi } from './loupeActions';
|
|
9
|
+
import { WHEEL_RATE } from './loupeActions';
|
|
8
10
|
import type { ResolvedLoupe } from './types';
|
|
9
11
|
|
|
10
12
|
/** Options for {@link useLoupe}. */
|
|
@@ -32,21 +34,21 @@ export interface LoupeState {
|
|
|
32
34
|
setFactor: (factor: number) => void;
|
|
33
35
|
/** Sample what the lens shows at a point inside it. */
|
|
34
36
|
pick: (p?: LoupePoint) => string | null;
|
|
37
|
+
/** What `<LoupeGestures>` drives the lens through. Stable for the life of
|
|
38
|
+
* the hook, so registering the actions on it does not churn. */
|
|
39
|
+
input: LoupeInputApi;
|
|
35
40
|
}
|
|
36
41
|
|
|
37
|
-
/** How fast the wheel walks the magnification. Gentler than the pan-zoom
|
|
38
|
-
* wheel: the lens' whole range is one order of magnitude. */
|
|
39
|
-
const WHEEL_RATE = 0.002;
|
|
40
|
-
|
|
41
42
|
/**
|
|
42
|
-
* Binds `@weasel-js/loupe`'s model to a host element and
|
|
43
|
-
*
|
|
43
|
+
* Binds `@weasel-js/loupe`'s model to a host element and tracks the pointer
|
|
44
|
+
* across it.
|
|
44
45
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
46
|
+
* The peek key and the wheel are not here: they are `loupe.peek` and
|
|
47
|
+
* `loupe.magnify`, routed through the gesture dispatcher that
|
|
48
|
+
* `<LoupeGestures>` mounts on the same host. Aiming stays a plain listener
|
|
49
|
+
* because a hover is not a gesture the grammar names — `GESTURE_DESCRIPTORS`
|
|
50
|
+
* has no continuous-motion entry, and inventing one to carry the lens' aim
|
|
51
|
+
* would be an input taxonomy change, not a loupe change.
|
|
50
52
|
*/
|
|
51
53
|
export function useLoupe({ capability, hostRef, enabled, sample }: UseLoupeOptions): LoupeState {
|
|
52
54
|
const [, bump] = useReducer((n: number) => n + 1, 0);
|
|
@@ -128,53 +130,30 @@ export function useLoupe({ capability, hostRef, enabled, sample }: UseLoupeOptio
|
|
|
128
130
|
overRef.current = false;
|
|
129
131
|
bump();
|
|
130
132
|
};
|
|
131
|
-
const onWheel = (e: WheelEvent): void => {
|
|
132
|
-
if (!shown()) return;
|
|
133
|
-
// Ahead of the stack's own pan-zoom, which is a React handler on this same
|
|
134
|
-
// element: capture runs first, and stopping propagation keeps the event
|
|
135
|
-
// from reaching React's root listener at all.
|
|
136
|
-
e.preventDefault();
|
|
137
|
-
e.stopPropagation();
|
|
138
|
-
model.setFactor(model.factor * Math.exp(-e.deltaY * WHEEL_RATE));
|
|
139
|
-
};
|
|
140
133
|
|
|
141
134
|
host.addEventListener('pointermove', onPointerMove);
|
|
142
135
|
host.addEventListener('pointerleave', onPointerLeave);
|
|
143
|
-
host.addEventListener('wheel', onWheel, { capture: true, passive: false });
|
|
144
136
|
return () => {
|
|
145
137
|
host.removeEventListener('pointermove', onPointerMove);
|
|
146
138
|
host.removeEventListener('pointerleave', onPointerLeave);
|
|
147
|
-
host.removeEventListener('wheel', onWheel, { capture: true });
|
|
148
139
|
};
|
|
149
140
|
}, [hostRef, model, shown]);
|
|
150
141
|
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
};
|
|
165
|
-
// A key held while the window loses focus never sends its keyup, so the
|
|
166
|
-
// peek would stay on until the key was pressed and released again.
|
|
167
|
-
const onBlur = (): void => setPeeking(false);
|
|
168
|
-
|
|
169
|
-
window.addEventListener('keydown', onKeyDown);
|
|
170
|
-
window.addEventListener('keyup', onKeyUp);
|
|
171
|
-
window.addEventListener('blur', onBlur);
|
|
172
|
-
return () => {
|
|
173
|
-
window.removeEventListener('keydown', onKeyDown);
|
|
174
|
-
window.removeEventListener('keyup', onKeyUp);
|
|
175
|
-
window.removeEventListener('blur', onBlur);
|
|
142
|
+
const inputRef = useRef<LoupeInputApi | null>(null);
|
|
143
|
+
if (inputRef.current === null) {
|
|
144
|
+
inputRef.current = {
|
|
145
|
+
shown,
|
|
146
|
+
setPeeking: (on) => {
|
|
147
|
+
if (peekingRef.current === on) return;
|
|
148
|
+
peekingRef.current = on;
|
|
149
|
+
bump();
|
|
150
|
+
},
|
|
151
|
+
magnifyBy: (deltaY) => {
|
|
152
|
+
const m = modelRef.current;
|
|
153
|
+
if (m) m.setFactor(m.factor * Math.exp(-deltaY * WHEEL_RATE));
|
|
154
|
+
},
|
|
176
155
|
};
|
|
177
|
-
}
|
|
156
|
+
}
|
|
178
157
|
|
|
179
158
|
return {
|
|
180
159
|
visible: shown(),
|
|
@@ -185,5 +164,6 @@ export function useLoupe({ capability, hostRef, enabled, sample }: UseLoupeOptio
|
|
|
185
164
|
setMode: model.setMode,
|
|
186
165
|
setFactor: model.setFactor,
|
|
187
166
|
pick: model.pick,
|
|
167
|
+
input: inputRef.current,
|
|
188
168
|
};
|
|
189
169
|
}
|
|
@@ -15,6 +15,8 @@ export {
|
|
|
15
15
|
type ActionsBarVariant,
|
|
16
16
|
type AddPointMode,
|
|
17
17
|
type AnchorRenderProps,
|
|
18
|
+
type AsyncOption,
|
|
19
|
+
type AsyncOptionsResult,
|
|
18
20
|
type AxesSettings,
|
|
19
21
|
Badge,
|
|
20
22
|
type BadgeProps,
|
|
@@ -191,12 +193,10 @@ export {
|
|
|
191
193
|
ToolPalette,
|
|
192
194
|
type ToolPaletteProps,
|
|
193
195
|
type TrackCtx,
|
|
196
|
+
type UseAsyncOptionsProps,
|
|
194
197
|
type UseReorderDragListOptions,
|
|
195
198
|
type UseRovingTabIndexOptions,
|
|
196
199
|
useAsyncOptions,
|
|
197
|
-
type AsyncOption,
|
|
198
|
-
type AsyncOptionsResult,
|
|
199
|
-
type UseAsyncOptionsProps,
|
|
200
200
|
useReorderDragList,
|
|
201
201
|
useRovingTabIndex,
|
|
202
202
|
visiblePrefSubtree,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from '@
|
|
1
|
+
import type { Meta, StoryObj } from '@weasel-js/forge';
|
|
2
2
|
import { localStorageAdapter } from '../state/adapters';
|
|
3
3
|
import { Persistence } from '../state/Persistence';
|
|
4
4
|
import { FloatingPanel } from './FloatingPanel';
|
|
@@ -60,6 +60,13 @@
|
|
|
60
60
|
|
|
61
61
|
.lk-sidebar-section {
|
|
62
62
|
border-bottom: var(--wzl-border-w) solid var(--wzl-line-subtle);
|
|
63
|
+
|
|
64
|
+
// The last section runs to the bottom of its pane. Stopped at its content,
|
|
65
|
+
// its rule and the bare pane under it read as a second, empty zone.
|
|
66
|
+
&:last-child {
|
|
67
|
+
flex-grow: 1;
|
|
68
|
+
border-bottom: 0;
|
|
69
|
+
}
|
|
63
70
|
}
|
|
64
71
|
|
|
65
72
|
.lk-sidebar-section__bar {
|
|
@@ -83,6 +83,31 @@ describe('Split', () => {
|
|
|
83
83
|
expect(container.querySelector('.lk-x__main')?.textContent).toBe('main');
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
+
it('puts the sidebar after the content on the end side, where dragging the seam left widens it', () => {
|
|
87
|
+
const onWidthChange = vi.fn();
|
|
88
|
+
const { container } = render(
|
|
89
|
+
<Split
|
|
90
|
+
viewport={VIEWPORT}
|
|
91
|
+
side="end"
|
|
92
|
+
width={320}
|
|
93
|
+
sidebarClassName="lk-x__side"
|
|
94
|
+
contentClassName="lk-x__main"
|
|
95
|
+
sidebar={<p>side</p>}
|
|
96
|
+
onWidthChange={onWidthChange}
|
|
97
|
+
>
|
|
98
|
+
<p>main</p>
|
|
99
|
+
</Split>,
|
|
100
|
+
);
|
|
101
|
+
const side = container.querySelector('.lk-x__side') as HTMLElement;
|
|
102
|
+
const main = container.querySelector('.lk-x__main') as HTMLElement;
|
|
103
|
+
expect(main.compareDocumentPosition(side) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
|
104
|
+
const seam = screen.getByRole('separator');
|
|
105
|
+
fireEvent.pointerDown(seam, { clientX: 480, clientY: 100 });
|
|
106
|
+
fireEvent.pointerMove(seam, { clientX: 420, clientY: 100 });
|
|
107
|
+
fireEvent.pointerUp(seam, { clientX: 420, clientY: 100 });
|
|
108
|
+
expect(onWidthChange).toHaveBeenLastCalledWith(380);
|
|
109
|
+
});
|
|
110
|
+
|
|
86
111
|
it('names its seam after the sidebar label', () => {
|
|
87
112
|
render(
|
|
88
113
|
<Split viewport={VIEWPORT} label="Stories" sidebar={<p>side</p>}>
|
package/src/primitives/Split.tsx
CHANGED
|
@@ -11,6 +11,8 @@ const STRATEGIES = { strip: stripStrategy as never };
|
|
|
11
11
|
export interface SplitProps {
|
|
12
12
|
/** The sidebar pane's contents. */
|
|
13
13
|
sidebar: ReactNode;
|
|
14
|
+
/** The edge the sidebar sits on. Read once, when the strip mounts. Default `'start'`. */
|
|
15
|
+
side?: 'start' | 'end';
|
|
14
16
|
/** The content pane's contents. */
|
|
15
17
|
children: ReactNode;
|
|
16
18
|
/** Names the sidebar pane, and through it the seam. Default `'Sidebar'`. */
|
|
@@ -51,6 +53,7 @@ export interface SplitProps {
|
|
|
51
53
|
export function Split({
|
|
52
54
|
sidebar,
|
|
53
55
|
children,
|
|
56
|
+
side = 'start',
|
|
54
57
|
label = 'Sidebar',
|
|
55
58
|
className,
|
|
56
59
|
sidebarClassName,
|
|
@@ -72,28 +75,37 @@ export function Split({
|
|
|
72
75
|
createNode({
|
|
73
76
|
kind: 'zone',
|
|
74
77
|
id: ZONE_ID,
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
hints: { minSize: { w: minWidth, h: 0 }, maxSize: { w: maxWidth, h: 0 } },
|
|
86
|
-
}),
|
|
87
|
-
);
|
|
88
|
-
store.registerNode(
|
|
89
|
-
createNode({
|
|
90
|
-
kind: 'content',
|
|
91
|
-
id: CONTENT_ID,
|
|
92
|
-
parentId: ZONE_ID,
|
|
93
|
-
meta: { title: 'Content' },
|
|
94
|
-
hints: { minSize: { w: contentMinWidth, h: 0 } },
|
|
78
|
+
// A strip's seam resizes the pane before it, which on the end side is the
|
|
79
|
+
// content; 'neighbor' has the seam size the sidebar after it too.
|
|
80
|
+
container: {
|
|
81
|
+
strategyId: 'strip',
|
|
82
|
+
config: {
|
|
83
|
+
axis: 'x',
|
|
84
|
+
resizable: true,
|
|
85
|
+
...(side === 'end' ? { resizeMode: 'neighbor' } : {}),
|
|
86
|
+
},
|
|
87
|
+
},
|
|
95
88
|
}),
|
|
96
89
|
);
|
|
90
|
+
const sidebarNode = createNode({
|
|
91
|
+
kind: 'sidebar',
|
|
92
|
+
id: SIDEBAR_ID,
|
|
93
|
+
parentId: ZONE_ID,
|
|
94
|
+
meta: { title: label },
|
|
95
|
+
placement: { size: { w: width ?? defaultWidth } },
|
|
96
|
+
hints: { minSize: { w: minWidth, h: 0 }, maxSize: { w: maxWidth, h: 0 } },
|
|
97
|
+
});
|
|
98
|
+
const contentNode = createNode({
|
|
99
|
+
kind: 'content',
|
|
100
|
+
id: CONTENT_ID,
|
|
101
|
+
parentId: ZONE_ID,
|
|
102
|
+
meta: { title: 'Content' },
|
|
103
|
+
hints: { minSize: { w: contentMinWidth, h: 0 } },
|
|
104
|
+
});
|
|
105
|
+
// A strip lays its children out in registration order.
|
|
106
|
+
for (const node of side === 'end' ? [contentNode, sidebarNode] : [sidebarNode, contentNode]) {
|
|
107
|
+
store.registerNode(node);
|
|
108
|
+
}
|
|
97
109
|
store.showNode(SIDEBAR_ID);
|
|
98
110
|
store.showNode(CONTENT_ID);
|
|
99
111
|
storeRef.current = store;
|
|
@@ -127,6 +139,10 @@ export function Split({
|
|
|
127
139
|
useEffect(
|
|
128
140
|
() =>
|
|
129
141
|
store.events.on('node.placementChanged', () => {
|
|
142
|
+
// A neighbor drag pins the content's width too; unpinned, it goes on filling the strip.
|
|
143
|
+
if ((store.getPlacement(CONTENT_ID)?.size as { w?: number } | undefined)?.w !== undefined) {
|
|
144
|
+
store.patchPlacement(CONTENT_ID, { size: {} });
|
|
145
|
+
}
|
|
130
146
|
const next = (store.getPlacement(SIDEBAR_ID)?.size as { w?: number } | undefined)?.w;
|
|
131
147
|
if (typeof next !== 'number' || next === widthRef.current) return;
|
|
132
148
|
widthRef.current = next;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
.lk-specimen {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
gap: var(--wzl-space-lg);
|
|
5
|
+
padding: var(--wzl-space-lg);
|
|
6
|
+
background: var(--wzl-surface);
|
|
7
|
+
color: var(--wzl-fg);
|
|
8
|
+
font-family: var(--wzl-font-ui);
|
|
9
|
+
font-size: var(--wzl-font-size);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.lk-specimen__section {
|
|
13
|
+
display: flex;
|
|
14
|
+
flex-direction: column;
|
|
15
|
+
gap: var(--wzl-space-sm);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.lk-specimen__heading {
|
|
19
|
+
margin: 0;
|
|
20
|
+
padding-block-end: var(--wzl-space-xs);
|
|
21
|
+
border-bottom: var(--wzl-border-w) solid var(--wzl-line-subtle);
|
|
22
|
+
color: var(--wzl-fg-muted);
|
|
23
|
+
font-family: var(--wzl-font-display);
|
|
24
|
+
font-size: var(--wzl-font-size-sm);
|
|
25
|
+
font-weight: var(--wzl-font-weight-normal);
|
|
26
|
+
letter-spacing: 0.08em;
|
|
27
|
+
text-transform: uppercase;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.lk-specimen__grid {
|
|
31
|
+
display: grid;
|
|
32
|
+
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
|
33
|
+
gap: var(--wzl-space-md);
|
|
34
|
+
align-items: start;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.lk-specimen__cell {
|
|
38
|
+
display: flex;
|
|
39
|
+
flex-direction: column;
|
|
40
|
+
gap: var(--wzl-space-xs);
|
|
41
|
+
min-width: 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.lk-specimen__label {
|
|
45
|
+
color: var(--wzl-fg-subtle);
|
|
46
|
+
font-family: var(--wzl-font-mono);
|
|
47
|
+
font-size: var(--wzl-font-size-xs);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.lk-specimen__row {
|
|
51
|
+
display: flex;
|
|
52
|
+
flex-wrap: wrap;
|
|
53
|
+
align-items: center;
|
|
54
|
+
gap: var(--wzl-space-sm);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.lk-specimen__cell--wide {
|
|
58
|
+
grid-column: 1 / -1;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.lk-specimen__box {
|
|
62
|
+
position: relative;
|
|
63
|
+
display: flex;
|
|
64
|
+
height: 160px;
|
|
65
|
+
overflow: hidden;
|
|
66
|
+
border: var(--wzl-border-w) solid var(--wzl-line-subtle);
|
|
67
|
+
border-radius: var(--wzl-radius-sm);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.lk-specimen__box-main {
|
|
71
|
+
flex: 1;
|
|
72
|
+
min-width: 0;
|
|
73
|
+
padding: var(--wzl-space-sm);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.lk-specimen__pad {
|
|
77
|
+
padding: var(--wzl-space-xs) var(--wzl-space-sm);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.lk-specimen__pane {
|
|
81
|
+
flex: none;
|
|
82
|
+
width: var(--lk-specimen-pane-w);
|
|
83
|
+
padding: var(--wzl-space-sm);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.lk-specimen__timeline {
|
|
87
|
+
height: 180px;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.lk-specimen__floating-host {
|
|
91
|
+
position: relative;
|
|
92
|
+
height: 160px;
|
|
93
|
+
border: var(--wzl-border-w) solid var(--wzl-line-subtle);
|
|
94
|
+
border-radius: var(--wzl-radius-sm);
|
|
95
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@weasel-js/forge';
|
|
2
|
+
import { Specimen } from './Specimen';
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof Specimen> = {
|
|
5
|
+
title: 'labkit/Specimen',
|
|
6
|
+
component: Specimen,
|
|
7
|
+
parameters: { layout: 'fullscreen' },
|
|
8
|
+
};
|
|
9
|
+
export default meta;
|
|
10
|
+
|
|
11
|
+
type Story = StoryObj<typeof Specimen>;
|
|
12
|
+
|
|
13
|
+
export const Everything: Story = {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { SPECIMEN_SECTIONS, Specimen } from './Specimen';
|
|
4
|
+
|
|
5
|
+
describe('<Specimen>', () => {
|
|
6
|
+
it('renders every section as a named region', () => {
|
|
7
|
+
render(<Specimen />);
|
|
8
|
+
expect(SPECIMEN_SECTIONS.length).toBeGreaterThan(0);
|
|
9
|
+
for (const title of SPECIMEN_SECTIONS) {
|
|
10
|
+
expect(screen.getByRole('region', { name: title })).toBeInTheDocument();
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
});
|