@weasel-js/labkit 1.2.0 → 1.3.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/dist/_dts/{DrawCommand-C_XboUpz.d.ts → DrawCommand-DBB45NfN.d.ts} +196 -52
- package/dist/_dts/PrefsForm-BYa6cWnO.d.ts +201 -0
- package/dist/_dts/types-1Sdxy_Pv.d.ts +90 -0
- package/dist/_dts/types-BttTIent.d.ts +314 -0
- package/dist/_dts/types-DCGEcMO_.d.ts +162 -0
- package/dist/_dts/{useTrialState-BMNIx3Cy.d.ts → useTrialState-CsGMjhu9.d.ts} +5 -87
- package/dist/canvas/index.d.ts +7 -3
- package/dist/canvas/index.js +3 -1
- package/dist/chrome/index.d.ts +86 -0
- package/dist/chrome/index.js +9 -0
- package/dist/chrome/index.js.map +1 -0
- package/dist/chunk-3XXPU73K.js +138 -0
- package/dist/chunk-3XXPU73K.js.map +1 -0
- package/dist/{chunk-VUU5UXHE.js → chunk-4TMMVIDM.js} +8 -5
- package/dist/chunk-4TMMVIDM.js.map +1 -0
- package/dist/chunk-6X5RBC6G.js +366 -0
- package/dist/chunk-6X5RBC6G.js.map +1 -0
- package/dist/{chunk-574LJAV4.js → chunk-BDJXIBRZ.js} +33 -35
- package/dist/chunk-BDJXIBRZ.js.map +1 -0
- package/dist/{chunk-SFL7NFKN.js → chunk-BMW4TDP5.js} +15 -7
- package/dist/chunk-BMW4TDP5.js.map +1 -0
- package/dist/chunk-JDROYQM3.js +266 -0
- package/dist/chunk-JDROYQM3.js.map +1 -0
- package/dist/{chunk-THBG7FQZ.js → chunk-LGKRIUSW.js} +15 -12
- package/dist/chunk-LGKRIUSW.js.map +1 -0
- package/dist/{chunk-BOHF3PQO.js → chunk-QCICIHEO.js} +8129 -5196
- package/dist/chunk-QCICIHEO.js.map +1 -0
- package/dist/chunk-QRYPXSGP.js +212 -0
- package/dist/chunk-QRYPXSGP.js.map +1 -0
- package/dist/chunk-RGT4EJIB.js +6961 -0
- package/dist/chunk-RGT4EJIB.js.map +1 -0
- package/dist/chunk-SBXNQW4G.js +3 -0
- package/dist/chunk-SBXNQW4G.js.map +1 -0
- package/dist/chunk-T66HU3HX.js +17 -0
- package/dist/chunk-T66HU3HX.js.map +1 -0
- package/dist/chunk-WTI26YTM.js +266 -0
- package/dist/chunk-WTI26YTM.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 +46 -3
- package/dist/dragdrop/index.js +3 -1
- package/dist/index.d.ts +735 -565
- package/dist/index.js +661 -1019
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +5 -3
- package/dist/layers/index.js +4 -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 +3811 -3671
- package/dist/passthrough/weasel-ui.js +3 -2
- package/dist/primitives/index.d.ts +102 -8
- package/dist/primitives/index.js +5 -1
- package/dist/state/index.d.ts +3 -2
- package/dist/state/index.js +2 -2
- package/dist/styles.css +490 -803
- package/dist/surface/index.js +2 -1
- package/dist/ui/layers/index.d.ts +8 -2
- package/dist/ui/layers/index.js +2 -3
- package/dist/undo/index.d.ts +4 -2
- package/package.json +16 -7
- package/src/canvas/AGENTS.md +4 -0
- package/src/canvas/CanvasStack.tsx +6 -2
- package/src/canvas/useLayerScheduler.ts +22 -15
- 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 +93 -0
- package/src/config/builder.ts +220 -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 +119 -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 +125 -0
- package/src/controls/ControlPanel.test.tsx +245 -10
- package/src/controls/ControlPanel.tsx +236 -203
- package/src/dragdrop/DragDropRuntime.tsx +36 -18
- package/src/dragdrop/DragGhost.less +1 -1
- package/src/dragdrop/Palette.less +1 -1
- package/src/dragdrop/index.ts +11 -0
- package/src/index.ts +74 -5
- package/src/instrument/SineWave.smoke.test.tsx +8 -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 +27 -4
- package/src/instrument/validateConfigSchema.test.ts +13 -3
- package/src/instrument/validateConfigSchema.ts +7 -10
- package/src/lab/Lab.chrome.test.tsx +65 -0
- 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.test.tsx +20 -10
- package/src/lab/Workspace.tsx +5 -8
- package/src/lab/index.ts +3 -1
- 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 +28 -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 +11 -3
- package/src/state/toolSlot.test.ts +36 -0
- package/src/state/types.ts +4 -0
- package/src/styles.less +6 -4
- package/src/surface/useTiledSurface.ts +14 -10
- package/src/test-setup.ts +26 -0
- package/src/theme/Interstellar.stories.tsx +1 -1
- 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 +108 -0
- package/src/trial/Trial.job.test.tsx +13 -15
- package/src/trial/Trial.less +115 -10
- package/src/trial/Trial.stories.tsx +1 -0
- package/src/trial/Trial.test.tsx +67 -96
- package/src/trial/Trial.tsx +80 -39
- package/src/trial/TrialChrome.tsx +99 -80
- 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/index.ts +5 -1
- package/dist/_dts/index-JFAYj5Tv.d.ts +0 -144
- package/dist/_dts/types-x92Kfeme.d.ts +0 -62
- 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-73KA7WBO.js +0 -5992
- package/dist/chunk-73KA7WBO.js.map +0 -1
- package/dist/chunk-BOHF3PQO.js.map +0 -1
- package/dist/chunk-G5TJVQQT.js +0 -161
- package/dist/chunk-G5TJVQQT.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/dist/chunk-THBG7FQZ.js.map +0 -1
- package/dist/chunk-VUU5UXHE.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 -54
- package/src/ui/format.test.ts +0 -33
- package/src/ui/format.ts +0 -30
- package/src/ui/layers/LayerStack.less +0 -143
- package/src/ui/layers/LayerStack.stories.tsx +0 -45
- package/src/ui/layers/LayerStack.test.tsx +0 -170
- package/src/ui/layers/LayerStack.tsx +0 -220
- package/src/ui/properties/CheckboxRow.stories.tsx +0 -27
- package/src/ui/properties/ColorRow.stories.tsx +0 -106
- package/src/ui/properties/CurveField.less +0 -48
- package/src/ui/properties/CurveField.stories.tsx +0 -20
- package/src/ui/properties/CurveField.test.tsx +0 -127
- package/src/ui/properties/CurveField.tsx +0 -184
- package/src/ui/properties/EffectCard.tsx +0 -324
- package/src/ui/properties/Gallery.stories.tsx +0 -178
- package/src/ui/properties/NumberRow.stories.tsx +0 -65
- package/src/ui/properties/PropertyGroup.less +0 -50
- package/src/ui/properties/PropertyGroup.stories.tsx +0 -34
- package/src/ui/properties/PropertyGroup.test.tsx +0 -33
- package/src/ui/properties/PropertyGroup.tsx +0 -45
- package/src/ui/properties/PropertyList.stories.tsx +0 -83
- package/src/ui/properties/PropertyPanel.less +0 -592
- package/src/ui/properties/PropertyPanel.stories.tsx +0 -190
- package/src/ui/properties/PropertyPanel.test.tsx +0 -193
- package/src/ui/properties/PropertyPanel.tsx +0 -469
- package/src/ui/properties/PropertyRow.stories.tsx +0 -53
- package/src/ui/properties/SelectRow.stories.tsx +0 -58
- package/src/ui/properties/SliderRow.stories.tsx +0 -74
- package/src/ui/properties/SpeechBalloonPanels.stories.tsx +0 -396
- package/src/ui/properties/TextRow.stories.tsx +0 -62
- package/src/ui/properties/ToggleRow.stories.tsx +0 -57
- package/src/ui/properties/index.ts +0 -39
- package/src/ui/properties/storyLayouts.tsx +0 -36
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { StatusBar } from '../primitives/StatusBar';
|
|
2
|
-
import { formatZoom } from '../ui/format';
|
|
3
|
-
import type { TrialStatusBarContext } from './slotTypes';
|
|
4
|
-
|
|
5
|
-
/** Props for `<DefaultStatusBar>`. */
|
|
6
|
-
export interface DefaultStatusBarProps {
|
|
7
|
-
ctx: TrialStatusBarContext;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/** The status bar a trial renders when no `statusBar` slot is supplied:
|
|
11
|
-
* the instrument's name, and the zoom when the trial's view has one. */
|
|
12
|
-
export function DefaultStatusBar({ ctx }: DefaultStatusBarProps) {
|
|
13
|
-
return (
|
|
14
|
-
<StatusBar>
|
|
15
|
-
<StatusBar.Section>{ctx.instrumentName}</StatusBar.Section>
|
|
16
|
-
{ctx.zoom === null ? null : <StatusBar.Section>{formatZoom(ctx.zoom)}</StatusBar.Section>}
|
|
17
|
-
</StatusBar>
|
|
18
|
-
);
|
|
19
|
-
}
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { Toolbar } from '../primitives/Toolbar';
|
|
2
|
-
import { formatZoom } from '../ui/format';
|
|
3
|
-
import type { TrialToolbarContext } from './slotTypes';
|
|
4
|
-
|
|
5
|
-
/** Props for `<DefaultToolbar>`. */
|
|
6
|
-
export interface DefaultToolbarProps {
|
|
7
|
-
ctx: TrialToolbarContext;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/** The toolbar a trial renders when no `toolbar` slot is supplied. Only
|
|
11
|
-
* shows the controls the instrument's declared capabilities support. */
|
|
12
|
-
export function DefaultToolbar({ ctx }: DefaultToolbarProps) {
|
|
13
|
-
return (
|
|
14
|
-
<Toolbar>
|
|
15
|
-
{ctx.hasUndo && (
|
|
16
|
-
<>
|
|
17
|
-
<Toolbar.Button onClick={ctx.undo} disabled={!ctx.canUndo} title="Undo (Cmd/Ctrl+Z)">
|
|
18
|
-
Undo
|
|
19
|
-
</Toolbar.Button>
|
|
20
|
-
<Toolbar.Button
|
|
21
|
-
onClick={ctx.redo}
|
|
22
|
-
disabled={!ctx.canRedo}
|
|
23
|
-
title="Redo (Cmd/Ctrl+Shift+Z)"
|
|
24
|
-
>
|
|
25
|
-
Redo
|
|
26
|
-
</Toolbar.Button>
|
|
27
|
-
<Toolbar.Spacer />
|
|
28
|
-
</>
|
|
29
|
-
)}
|
|
30
|
-
|
|
31
|
-
{ctx.hasCanvas && (
|
|
32
|
-
<>
|
|
33
|
-
<Toolbar.Button onClick={ctx.zoomOut} title="Zoom out">
|
|
34
|
-
−
|
|
35
|
-
</Toolbar.Button>
|
|
36
|
-
<span className="lk-toolbar__zoom-label">{formatZoom(ctx.zoom)}</span>
|
|
37
|
-
<Toolbar.Button onClick={ctx.zoomIn} title="Zoom in">
|
|
38
|
-
+
|
|
39
|
-
</Toolbar.Button>
|
|
40
|
-
<Toolbar.Button onClick={ctx.resetZoom} title="Reset zoom">
|
|
41
|
-
1:1
|
|
42
|
-
</Toolbar.Button>
|
|
43
|
-
<Toolbar.Spacer />
|
|
44
|
-
</>
|
|
45
|
-
)}
|
|
46
|
-
|
|
47
|
-
<Toolbar.Button onClick={() => ctx.saveSnapshot()} title="Save snapshot (Cmd/Ctrl+S)">
|
|
48
|
-
Save
|
|
49
|
-
</Toolbar.Button>
|
|
50
|
-
|
|
51
|
-
{ctx.savedSnapshots.length > 0 && (
|
|
52
|
-
<select
|
|
53
|
-
className="lk-toolbar__load-select"
|
|
54
|
-
aria-label="Load snapshot"
|
|
55
|
-
defaultValue=""
|
|
56
|
-
onChange={(e) => {
|
|
57
|
-
const id = e.target.value;
|
|
58
|
-
if (id) {
|
|
59
|
-
ctx.loadSnapshot(id);
|
|
60
|
-
e.target.value = '';
|
|
61
|
-
}
|
|
62
|
-
}}
|
|
63
|
-
>
|
|
64
|
-
<option value="" disabled>
|
|
65
|
-
Load…
|
|
66
|
-
</option>
|
|
67
|
-
{ctx.savedSnapshots.map((sn) => (
|
|
68
|
-
<option key={sn.id} value={sn.id}>
|
|
69
|
-
{sn.name}
|
|
70
|
-
</option>
|
|
71
|
-
))}
|
|
72
|
-
</select>
|
|
73
|
-
)}
|
|
74
|
-
|
|
75
|
-
<Toolbar.Spacer />
|
|
76
|
-
|
|
77
|
-
<Toolbar.Button onClick={ctx.clone} title="Clone trial">
|
|
78
|
-
Clone
|
|
79
|
-
</Toolbar.Button>
|
|
80
|
-
<Toolbar.Button onClick={ctx.reset} title="Reset trial">
|
|
81
|
-
Reset
|
|
82
|
-
</Toolbar.Button>
|
|
83
|
-
<Toolbar.Button
|
|
84
|
-
onClick={ctx.close}
|
|
85
|
-
disabled={ctx.isLastTrial}
|
|
86
|
-
title={ctx.isLastTrial ? 'Cannot close the last trial' : 'Close trial'}
|
|
87
|
-
>
|
|
88
|
-
Close
|
|
89
|
-
</Toolbar.Button>
|
|
90
|
-
</Toolbar>
|
|
91
|
-
);
|
|
92
|
-
}
|
package/src/trial/slotTypes.ts
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import type { ReactNode } from 'react';
|
|
2
|
-
import type { ConfigField } from '../controls/types';
|
|
3
|
-
import type { SavedSnapshot } from '../state/types';
|
|
4
|
-
|
|
5
|
-
/** What a trial hands its toolbar: which instrument is running, the undo
|
|
6
|
-
* state and commands, the zoom controls, and the snapshot commands. */
|
|
7
|
-
export interface TrialToolbarContext {
|
|
8
|
-
trialId: string;
|
|
9
|
-
instrumentName: string;
|
|
10
|
-
hasUndo: boolean;
|
|
11
|
-
canUndo: boolean;
|
|
12
|
-
canRedo: boolean;
|
|
13
|
-
undo: () => void;
|
|
14
|
-
redo: () => void;
|
|
15
|
-
zoom: number;
|
|
16
|
-
setZoom: (z: number) => void;
|
|
17
|
-
zoomIn: () => void;
|
|
18
|
-
zoomOut: () => void;
|
|
19
|
-
resetZoom: () => void;
|
|
20
|
-
hasCanvas: boolean;
|
|
21
|
-
savedSnapshots: SavedSnapshot[];
|
|
22
|
-
saveSnapshot: (name?: string) => void;
|
|
23
|
-
loadSnapshot: (snapshotId: string) => void;
|
|
24
|
-
clone: () => void;
|
|
25
|
-
reset: () => void;
|
|
26
|
-
close: () => void;
|
|
27
|
-
isLastTrial: boolean;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** What a trial hands its sidebar: the instrument's config schema, its
|
|
31
|
-
* current values, and the setter. */
|
|
32
|
-
export interface TrialSidebarContext {
|
|
33
|
-
trialId: string;
|
|
34
|
-
instrumentName: string;
|
|
35
|
-
configFields: ConfigField[];
|
|
36
|
-
config: unknown;
|
|
37
|
-
setConfig: (key: string, value: unknown) => void;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** What a trial hands its status bar. */
|
|
41
|
-
export interface TrialStatusBarContext {
|
|
42
|
-
trialId: string;
|
|
43
|
-
instrumentName: string;
|
|
44
|
-
/** Null when the trial holds a view that is not the 2D one. */
|
|
45
|
-
zoom: number | null;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** Replaces a trial's toolbar. Receives everything the default one uses,
|
|
49
|
-
* so a custom toolbar need not reach into the store. */
|
|
50
|
-
export type ToolbarSlot = (ctx: TrialToolbarContext) => ReactNode;
|
|
51
|
-
/** Replaces a trial's sidebar. */
|
|
52
|
-
export type SidebarSlot = (ctx: TrialSidebarContext) => ReactNode;
|
|
53
|
-
/** Replaces a trial's status bar. */
|
|
54
|
-
export type StatusBarSlot = (ctx: TrialStatusBarContext) => ReactNode;
|
package/src/ui/format.test.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { formatZoom } from './format';
|
|
3
|
-
|
|
4
|
-
describe('formatZoom', () => {
|
|
5
|
-
it('shows a percentage up to and including 2x', () => {
|
|
6
|
-
expect(formatZoom(0.5)).toBe('50%');
|
|
7
|
-
expect(formatZoom(1)).toBe('100%');
|
|
8
|
-
expect(formatZoom(1.5)).toBe('150%');
|
|
9
|
-
expect(formatZoom(2)).toBe('200%');
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it('switches to a multiplier above 2x', () => {
|
|
13
|
-
expect(formatZoom(2.5)).toBe('2.5x');
|
|
14
|
-
expect(formatZoom(4)).toBe('4x');
|
|
15
|
-
expect(formatZoom(16)).toBe('16x');
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it('rounds the multiplier to one decimal and drops a trailing zero', () => {
|
|
19
|
-
expect(formatZoom(3.04)).toBe('3x');
|
|
20
|
-
expect(formatZoom(3.06)).toBe('3.1x');
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('drops the decimal and groups thousands past 100x', () => {
|
|
24
|
-
expect(formatZoom(100)).toBe('100x');
|
|
25
|
-
expect(formatZoom(1009.74)).toBe('1,010x');
|
|
26
|
-
expect(formatZoom(99.9)).toBe('99.9x');
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('passes non-finite zoom through rather than printing NaN%', () => {
|
|
30
|
-
expect(formatZoom(Number.NaN)).toBe('NaN');
|
|
31
|
-
expect(formatZoom(Number.POSITIVE_INFINITY)).toBe('Infinity');
|
|
32
|
-
});
|
|
33
|
-
});
|
package/src/ui/format.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
// Display formatting helpers shared across the UI.
|
|
2
|
-
//
|
|
3
|
-
// House style: render negative numbers with the typographic MINUS SIGN
|
|
4
|
-
// (U+2212, "−") instead of ASCII hyphen-minus ("-"). Editing surfaces should
|
|
5
|
-
// accept either glyph on input via `parseSignedNumber`.
|
|
6
|
-
|
|
7
|
-
const MINUS = '−';
|
|
8
|
-
|
|
9
|
-
/** Format a finite number for display, swapping the leading "-" to U+2212. */
|
|
10
|
-
export function formatNumber(n: number, fractionDigits?: number): string {
|
|
11
|
-
if (!Number.isFinite(n)) return String(n);
|
|
12
|
-
const s = fractionDigits == null ? String(n) : n.toFixed(fractionDigits);
|
|
13
|
-
return s.startsWith('-') ? MINUS + s.slice(1) : s;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/** Parse a string that may use U+2212 or ASCII hyphen-minus for the negative sign. */
|
|
17
|
-
export function parseSignedNumber(s: string): number {
|
|
18
|
-
return Number(s.replace(MINUS, '-'));
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** Zoom for display. Below 2x a percentage reads naturally; past it the
|
|
22
|
-
* numbers get long and a multiplier is what people say out loud, so 250%
|
|
23
|
-
* shows as `2.5x`. Past 100x a tenth is noise, so the decimal is dropped
|
|
24
|
-
* and thousands are grouped: `1009.74` reads as `1,010x`. */
|
|
25
|
-
export function formatZoom(zoom: number): string {
|
|
26
|
-
if (!Number.isFinite(zoom)) return String(zoom);
|
|
27
|
-
if (zoom <= 2) return `${formatNumber(Math.round(zoom * 100))}%`;
|
|
28
|
-
if (zoom >= 100) return `${Math.round(zoom).toLocaleString('en-US')}x`;
|
|
29
|
-
return `${formatNumber(Math.round(zoom * 10) / 10)}x`;
|
|
30
|
-
}
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
.lk-layer-stack {
|
|
2
|
-
display: grid;
|
|
3
|
-
gap: 8px;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
.lk-layer-stack__head {
|
|
7
|
-
display: flex;
|
|
8
|
-
align-items: center;
|
|
9
|
-
justify-content: space-between;
|
|
10
|
-
gap: 8px;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
.lk-layer-stack__title {
|
|
14
|
-
margin: 0;
|
|
15
|
-
font-family: var(--wzl-font-display);
|
|
16
|
-
font-weight: var(--wzl-font-weight-light);
|
|
17
|
-
font-size: 1rem;
|
|
18
|
-
color: var(--wzl-fg);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
.lk-layer-stack__palette {
|
|
22
|
-
display: flex;
|
|
23
|
-
gap: 4px;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
.lk-layer-stack__add {
|
|
27
|
-
background: transparent;
|
|
28
|
-
border: 1px dashed var(--wzl-border);
|
|
29
|
-
border-radius: var(--wzl-radius-sm);
|
|
30
|
-
padding: 2px 8px;
|
|
31
|
-
font-size: 0.78rem;
|
|
32
|
-
color: var(--wzl-fg-subtle);
|
|
33
|
-
cursor: pointer;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
.lk-layer-stack__add:hover {
|
|
37
|
-
background: var(--wzl-surface-raised);
|
|
38
|
-
color: var(--wzl-fg);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
.lk-layer-stack__list {
|
|
42
|
-
display: grid;
|
|
43
|
-
gap: 6px;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
.lk-layer-stack__drop-hint {
|
|
47
|
-
height: 2px;
|
|
48
|
-
background: var(--wzl-accent);
|
|
49
|
-
border-radius: 1px;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
.lk-layer-stack__empty {
|
|
53
|
-
color: var(--wzl-fg-subtle);
|
|
54
|
-
font-size: 0.85rem;
|
|
55
|
-
padding: 8px;
|
|
56
|
-
text-align: center;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
.lk-layer-card {
|
|
60
|
-
background: var(--wzl-surface-raised);
|
|
61
|
-
border: 1px solid var(--wzl-border);
|
|
62
|
-
border-radius: var(--wzl-radius-md);
|
|
63
|
-
overflow: hidden;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
.lk-layer-card.has-accent {
|
|
67
|
-
// Re-bind --wzl-accent inside the card so descendant controls (slider tracks
|
|
68
|
-
// and thumbs, readouts, etc.) tint to the per-card accent instead of the
|
|
69
|
-
// global theme accent. Mirrors EffectCard's behavior.
|
|
70
|
-
--wzl-accent: var(--lk-layer-card-accent);
|
|
71
|
-
border-left: 3px solid var(--lk-layer-card-accent);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
.lk-layer-card.is-dragging {
|
|
75
|
-
opacity: 0.5;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
.lk-layer-card__head {
|
|
79
|
-
display: flex;
|
|
80
|
-
align-items: center;
|
|
81
|
-
gap: 6px;
|
|
82
|
-
padding: 6px 8px;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Prefixed with `.lk-root` to outrank the .lk-root button rule in base.less,
|
|
86
|
-
// which would otherwise paint a chrome border + elevated bg on these buttons.
|
|
87
|
-
.lk-root .lk-layer-card__handle {
|
|
88
|
-
background: transparent;
|
|
89
|
-
border: 0;
|
|
90
|
-
color: var(--wzl-fg-subtle);
|
|
91
|
-
padding: 2px 4px;
|
|
92
|
-
cursor: grab;
|
|
93
|
-
display: inline-flex;
|
|
94
|
-
align-items: center;
|
|
95
|
-
justify-content: center;
|
|
96
|
-
min-width: 22px;
|
|
97
|
-
height: 22px;
|
|
98
|
-
border-radius: var(--wzl-radius-sm);
|
|
99
|
-
font-weight: 600;
|
|
100
|
-
font-size: 0.85rem;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
.lk-layer-card.has-accent .lk-layer-card__handle {
|
|
104
|
-
background: var(--lk-layer-card-accent);
|
|
105
|
-
color: white;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
.lk-layer-card__handle:active {
|
|
109
|
-
cursor: grabbing;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
.lk-layer-card__kind,
|
|
113
|
-
.lk-layer-card__primary {
|
|
114
|
-
flex: 1;
|
|
115
|
-
font-size: 0.9rem;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
.lk-layer-card__primary {
|
|
119
|
-
background: transparent;
|
|
120
|
-
border: 0;
|
|
121
|
-
color: var(--wzl-fg);
|
|
122
|
-
font: inherit;
|
|
123
|
-
text-transform: uppercase;
|
|
124
|
-
letter-spacing: 0.06em;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
.lk-root .lk-layer-card__remove {
|
|
128
|
-
background: transparent;
|
|
129
|
-
border: 0;
|
|
130
|
-
color: var(--wzl-fg-subtle);
|
|
131
|
-
font-size: 1rem;
|
|
132
|
-
cursor: pointer;
|
|
133
|
-
padding: 2px 6px;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
.lk-layer-card__remove:hover {
|
|
137
|
-
color: var(--wzl-fg);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
.lk-layer-card__body {
|
|
141
|
-
padding: 8px;
|
|
142
|
-
border-top: 1px solid var(--wzl-border);
|
|
143
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
-
import { useState } from 'react';
|
|
3
|
-
import { LayerStack, type LayerStackItem } from './LayerStack';
|
|
4
|
-
|
|
5
|
-
const meta: Meta<typeof LayerStack> = {
|
|
6
|
-
title: 'labkit/UI/Layers/LayerStack',
|
|
7
|
-
component: LayerStack,
|
|
8
|
-
};
|
|
9
|
-
export default meta;
|
|
10
|
-
|
|
11
|
-
const seed: LayerStackItem[] = [
|
|
12
|
-
{ id: 1, kind: 'fill', primaryValue: 'dome', primaryOptions: ['aqua', 'bevel', 'dome'] },
|
|
13
|
-
{ id: 2, kind: 'tail', accent: '#f55', badge: '1' },
|
|
14
|
-
{ id: 3, kind: 'shadow' },
|
|
15
|
-
];
|
|
16
|
-
|
|
17
|
-
export const Basic: StoryObj<typeof LayerStack> = {
|
|
18
|
-
render: () => {
|
|
19
|
-
const [items, setItems] = useState(seed);
|
|
20
|
-
let next = 4;
|
|
21
|
-
return (
|
|
22
|
-
<div style={{ width: 320 }}>
|
|
23
|
-
<LayerStack
|
|
24
|
-
title="Layers"
|
|
25
|
-
items={items}
|
|
26
|
-
paletteKinds={['fill', 'tail', 'shadow']}
|
|
27
|
-
onAdd={(kind) => setItems([...items, { id: next++, kind }])}
|
|
28
|
-
onRemove={(id) => setItems(items.filter((i) => i.id !== id))}
|
|
29
|
-
onReorder={(ids) => {
|
|
30
|
-
const byId = new Map(items.map((i) => [i.id, i]));
|
|
31
|
-
const reordered = ids.flatMap((id) => {
|
|
32
|
-
const found = byId.get(id);
|
|
33
|
-
return found ? [found] : [];
|
|
34
|
-
});
|
|
35
|
-
setItems(reordered);
|
|
36
|
-
}}
|
|
37
|
-
onPrimaryChange={(id, value) =>
|
|
38
|
-
setItems(items.map((i) => (i.id === id ? { ...i, primaryValue: value } : i)))
|
|
39
|
-
}
|
|
40
|
-
renderBody={(item) => <div style={{ color: '#888' }}>body for {item.kind}</div>}
|
|
41
|
-
/>
|
|
42
|
-
</div>
|
|
43
|
-
);
|
|
44
|
-
},
|
|
45
|
-
};
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
import { fireEvent, render, screen } from '@testing-library/react';
|
|
2
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
-
|
|
4
|
-
// useReorderDragList pulls its own React copy from weasel/node_modules, which
|
|
5
|
-
// breaks hooks in jsdom. Stub it out with a no-op implementation.
|
|
6
|
-
vi.mock('../../passthrough/weasel-ui', () => ({
|
|
7
|
-
dlog: () => {},
|
|
8
|
-
useReorderDragList: () => ({
|
|
9
|
-
rowProps: (_id: string, _i: number) => ({ onPointerDown: () => {} }),
|
|
10
|
-
containerProps: {
|
|
11
|
-
ref: () => {},
|
|
12
|
-
onPointerMove: () => {},
|
|
13
|
-
onPointerUp: () => {},
|
|
14
|
-
onPointerCancel: () => {},
|
|
15
|
-
},
|
|
16
|
-
state: { draggedIds: null, targetIndex: null },
|
|
17
|
-
}),
|
|
18
|
-
}));
|
|
19
|
-
|
|
20
|
-
import { LayerStack, type LayerStackItem } from './LayerStack';
|
|
21
|
-
|
|
22
|
-
const items: LayerStackItem[] = [
|
|
23
|
-
{ id: 1, kind: 'fill', primaryValue: 'aqua', primaryOptions: ['aqua', 'bevel', 'dome'] },
|
|
24
|
-
{ id: 2, kind: 'tail', accent: '#f44', badge: '1' },
|
|
25
|
-
{ id: 3, kind: 'shadow' },
|
|
26
|
-
];
|
|
27
|
-
|
|
28
|
-
describe('LayerStack', () => {
|
|
29
|
-
it('renders header + add buttons + each item', () => {
|
|
30
|
-
render(
|
|
31
|
-
<LayerStack
|
|
32
|
-
title="Fill"
|
|
33
|
-
items={items}
|
|
34
|
-
paletteKinds={['fill', 'tail', 'shadow']}
|
|
35
|
-
onAdd={() => {}}
|
|
36
|
-
onRemove={() => {}}
|
|
37
|
-
onReorder={() => {}}
|
|
38
|
-
onPrimaryChange={() => {}}
|
|
39
|
-
renderBody={(item) => <div>body-{item.id}</div>}
|
|
40
|
-
/>,
|
|
41
|
-
);
|
|
42
|
-
expect(screen.getByText('Fill')).toBeInTheDocument();
|
|
43
|
-
for (const k of ['fill', 'tail', 'shadow']) {
|
|
44
|
-
expect(screen.getByRole('button', { name: new RegExp(`add ${k}`, 'i') })).toBeInTheDocument();
|
|
45
|
-
}
|
|
46
|
-
expect(screen.getAllByTestId(/lk-layer-card-/)).toHaveLength(3);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('clicking a palette button calls onAdd with that kind', () => {
|
|
50
|
-
const onAdd = vi.fn();
|
|
51
|
-
render(
|
|
52
|
-
<LayerStack
|
|
53
|
-
title="Fill"
|
|
54
|
-
items={items}
|
|
55
|
-
paletteKinds={['fill', 'tail']}
|
|
56
|
-
onAdd={onAdd}
|
|
57
|
-
onRemove={() => {}}
|
|
58
|
-
onReorder={() => {}}
|
|
59
|
-
onPrimaryChange={() => {}}
|
|
60
|
-
renderBody={() => null}
|
|
61
|
-
/>,
|
|
62
|
-
);
|
|
63
|
-
fireEvent.click(screen.getByRole('button', { name: /add tail/i }));
|
|
64
|
-
expect(onAdd).toHaveBeenCalledWith('tail');
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('clicking the remove button calls onRemove with that id', () => {
|
|
68
|
-
const onRemove = vi.fn();
|
|
69
|
-
render(
|
|
70
|
-
<LayerStack
|
|
71
|
-
title="Fill"
|
|
72
|
-
items={items}
|
|
73
|
-
paletteKinds={[]}
|
|
74
|
-
onAdd={() => {}}
|
|
75
|
-
onRemove={onRemove}
|
|
76
|
-
onReorder={() => {}}
|
|
77
|
-
onPrimaryChange={() => {}}
|
|
78
|
-
renderBody={() => null}
|
|
79
|
-
/>,
|
|
80
|
-
);
|
|
81
|
-
const removes = screen.getAllByRole('button', { name: /remove layer/i });
|
|
82
|
-
fireEvent.click(removes[1]);
|
|
83
|
-
expect(onRemove).toHaveBeenCalledWith(2);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('shows empty state when items is empty', () => {
|
|
87
|
-
render(
|
|
88
|
-
<LayerStack
|
|
89
|
-
title="Empty"
|
|
90
|
-
items={[]}
|
|
91
|
-
paletteKinds={['fill']}
|
|
92
|
-
onAdd={() => {}}
|
|
93
|
-
onRemove={() => {}}
|
|
94
|
-
onReorder={() => {}}
|
|
95
|
-
onPrimaryChange={() => {}}
|
|
96
|
-
renderBody={() => null}
|
|
97
|
-
/>,
|
|
98
|
-
);
|
|
99
|
-
expect(screen.getByText(/no layers/i)).toBeInTheDocument();
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('changing the primary select calls onPrimaryChange', () => {
|
|
103
|
-
const onPrimaryChange = vi.fn();
|
|
104
|
-
render(
|
|
105
|
-
<LayerStack
|
|
106
|
-
title="Fill"
|
|
107
|
-
items={items}
|
|
108
|
-
paletteKinds={[]}
|
|
109
|
-
onAdd={() => {}}
|
|
110
|
-
onRemove={() => {}}
|
|
111
|
-
onReorder={() => {}}
|
|
112
|
-
onPrimaryChange={onPrimaryChange}
|
|
113
|
-
renderBody={() => null}
|
|
114
|
-
/>,
|
|
115
|
-
);
|
|
116
|
-
const sel = screen.getByLabelText(/primary select for layer 1/i) as HTMLSelectElement;
|
|
117
|
-
fireEvent.change(sel, { target: { value: 'bevel' } });
|
|
118
|
-
expect(onPrimaryChange).toHaveBeenCalledWith(1, 'bevel');
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it('newly added items render expanded by default', () => {
|
|
122
|
-
const initial: LayerStackItem[] = [{ id: 1, kind: 'fill' }];
|
|
123
|
-
const { rerender } = render(
|
|
124
|
-
<LayerStack
|
|
125
|
-
title="Fill"
|
|
126
|
-
items={initial}
|
|
127
|
-
paletteKinds={[]}
|
|
128
|
-
onAdd={() => {}}
|
|
129
|
-
onRemove={() => {}}
|
|
130
|
-
onReorder={() => {}}
|
|
131
|
-
onPrimaryChange={() => {}}
|
|
132
|
-
renderBody={(item) => <div data-testid={`body-${item.id}`}>b{item.id}</div>}
|
|
133
|
-
/>,
|
|
134
|
-
);
|
|
135
|
-
expect(screen.getByTestId('body-1')).toBeInTheDocument();
|
|
136
|
-
rerender(
|
|
137
|
-
<LayerStack
|
|
138
|
-
title="Fill"
|
|
139
|
-
items={[...initial, { id: 2, kind: 'tail' }]}
|
|
140
|
-
paletteKinds={[]}
|
|
141
|
-
onAdd={() => {}}
|
|
142
|
-
onRemove={() => {}}
|
|
143
|
-
onReorder={() => {}}
|
|
144
|
-
onPrimaryChange={() => {}}
|
|
145
|
-
renderBody={(item) => <div data-testid={`body-${item.id}`}>b{item.id}</div>}
|
|
146
|
-
/>,
|
|
147
|
-
);
|
|
148
|
-
expect(screen.getByTestId('body-2')).toBeInTheDocument();
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it('items with defaultExpanded: false render collapsed', () => {
|
|
152
|
-
render(
|
|
153
|
-
<LayerStack
|
|
154
|
-
title="Fill"
|
|
155
|
-
items={[{ id: 1, kind: 'fill', defaultExpanded: false }]}
|
|
156
|
-
paletteKinds={[]}
|
|
157
|
-
onAdd={() => {}}
|
|
158
|
-
onRemove={() => {}}
|
|
159
|
-
onReorder={() => {}}
|
|
160
|
-
onPrimaryChange={() => {}}
|
|
161
|
-
renderBody={(item) => <div data-testid={`body-${item.id}`}>b{item.id}</div>}
|
|
162
|
-
/>,
|
|
163
|
-
);
|
|
164
|
-
expect(screen.queryByTestId('body-1')).not.toBeInTheDocument();
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
// TODO: reorder off-by-one fix (Bug 1) is covered by manual integration
|
|
168
|
-
// testing in speech-balloons once LayerStack is consumed there (B3).
|
|
169
|
-
// useReorderDragList is mocked in unit tests, making onReorder untestable here.
|
|
170
|
-
});
|