@weasel-js/labkit 0.1.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 +72 -0
- package/dist/_dts/fitViewToBounds-rWrBI4a6.d.ts +870 -0
- package/dist/_dts/index-C6Yze7sQ.d.ts +93 -0
- package/dist/_dts/types-Si4Fw-1F.d.ts +48 -0
- package/dist/_dts/useExperimentState-eSvpwV_P.d.ts +102 -0
- package/dist/canvas/index.d.ts +34 -0
- package/dist/canvas/index.js +4 -0
- package/dist/canvas/index.js.map +1 -0
- package/dist/chunk-2QNYYL3V.js +86 -0
- package/dist/chunk-2QNYYL3V.js.map +1 -0
- package/dist/chunk-54IQ2DX7.js +222 -0
- package/dist/chunk-54IQ2DX7.js.map +1 -0
- package/dist/chunk-6FO7G5BB.js +6095 -0
- package/dist/chunk-6FO7G5BB.js.map +1 -0
- package/dist/chunk-7BKDG73Z.js +54 -0
- package/dist/chunk-7BKDG73Z.js.map +1 -0
- package/dist/chunk-B5ZUYRKN.js +23330 -0
- package/dist/chunk-B5ZUYRKN.js.map +1 -0
- package/dist/chunk-HXZHVU4G.js +126 -0
- package/dist/chunk-HXZHVU4G.js.map +1 -0
- package/dist/chunk-PWC7AQZM.js +8 -0
- package/dist/chunk-PWC7AQZM.js.map +1 -0
- package/dist/chunk-SD3HDXR7.js +161 -0
- package/dist/chunk-SD3HDXR7.js.map +1 -0
- package/dist/chunk-SSMHPMDV.js +350 -0
- package/dist/chunk-SSMHPMDV.js.map +1 -0
- package/dist/chunk-VLAHRJOC.js +201 -0
- package/dist/chunk-VLAHRJOC.js.map +1 -0
- package/dist/controls/index.d.ts +13 -0
- package/dist/controls/index.js +3 -0
- package/dist/controls/index.js.map +1 -0
- package/dist/dragdrop/index.d.ts +3 -0
- package/dist/dragdrop/index.js +3 -0
- package/dist/dragdrop/index.js.map +1 -0
- package/dist/fonts/oswald-latin-variable.woff2 +0 -0
- package/dist/index.d.ts +410 -0
- package/dist/index.js +1425 -0
- package/dist/index.js.map +1 -0
- package/dist/layers/index.d.ts +17 -0
- package/dist/layers/index.js +3 -0
- package/dist/layers/index.js.map +1 -0
- package/dist/passthrough/weasel-canvas.d.ts +258 -0
- package/dist/passthrough/weasel-canvas.js +3 -0
- package/dist/passthrough/weasel-canvas.js.map +1 -0
- package/dist/passthrough/weasel-ui.d.ts +2826 -0
- package/dist/passthrough/weasel-ui.js +4 -0
- package/dist/passthrough/weasel-ui.js.map +1 -0
- package/dist/primitives/index.d.ts +57 -0
- package/dist/primitives/index.js +4 -0
- package/dist/primitives/index.js.map +1 -0
- package/dist/state/index.d.ts +33 -0
- package/dist/state/index.js +34 -0
- package/dist/state/index.js.map +1 -0
- package/dist/styles.css +1224 -0
- package/dist/theme-interstellar.css +25 -0
- package/dist/theme-light.css +13 -0
- package/dist/ui/layers/index.d.ts +42 -0
- package/dist/ui/layers/index.js +5 -0
- package/dist/ui/layers/index.js.map +1 -0
- package/dist/undo/index.d.ts +30 -0
- package/dist/undo/index.js +3 -0
- package/dist/undo/index.js.map +1 -0
- package/package.json +99 -0
- package/src/canvas/AGENTS.md +72 -0
- package/src/canvas/CanvasStack.less +22 -0
- package/src/canvas/CanvasStack.stories.tsx +63 -0
- package/src/canvas/CanvasStack.test.tsx +82 -0
- package/src/canvas/CanvasStack.tsx +108 -0
- package/src/canvas/CanvasStackContext.ts +8 -0
- package/src/canvas/canvasCoords.test.ts +30 -0
- package/src/canvas/canvasCoords.ts +15 -0
- package/src/canvas/index.ts +6 -0
- package/src/canvas/useLayerScheduler.ts +72 -0
- package/src/canvas/usePanZoom.ts +98 -0
- package/src/controls/ControlPanel.less +61 -0
- package/src/controls/ControlPanel.stories.tsx +77 -0
- package/src/controls/ControlPanel.test.tsx +149 -0
- package/src/controls/ControlPanel.tsx +249 -0
- package/src/controls/index.ts +12 -0
- package/src/controls/types.ts +61 -0
- package/src/dragdrop/DragDropRuntime.tsx +135 -0
- package/src/dragdrop/DragGhost.less +17 -0
- package/src/dragdrop/DragGhost.tsx +24 -0
- package/src/dragdrop/Palette.less +34 -0
- package/src/dragdrop/Palette.tsx +27 -0
- package/src/dragdrop/dragDrop.test.tsx +145 -0
- package/src/dragdrop/index.ts +1 -0
- package/src/fonts/oswald-latin-variable.woff2 +0 -0
- package/src/index.ts +70 -0
- package/src/instrument/SineWave.smoke.test.tsx +94 -0
- package/src/instrument/capabilityDetector.test.ts +64 -0
- package/src/instrument/capabilityDetector.ts +17 -0
- package/src/instrument/defineInstrument.test.ts +27 -0
- package/src/instrument/defineInstrument.ts +5 -0
- package/src/instrument/index.ts +19 -0
- package/src/instrument/types.ts +65 -0
- package/src/instrument/validateConfigSchema.test.ts +125 -0
- package/src/instrument/validateConfigSchema.ts +65 -0
- package/src/lab/Lab.less +3 -0
- package/src/lab/Lab.stories.tsx +51 -0
- package/src/lab/Lab.test.tsx +126 -0
- package/src/lab/Lab.tsx +196 -0
- package/src/lab/LabContext.test.ts +11 -0
- package/src/lab/LabContext.ts +28 -0
- package/src/lab/LabShell.less +45 -0
- package/src/lab/LabShell.stories.tsx +39 -0
- package/src/lab/LabShell.test.tsx +59 -0
- package/src/lab/LabShell.tsx +33 -0
- package/src/lab/WorkspaceGrid.less +8 -0
- package/src/lab/WorkspaceGrid.stories.tsx +61 -0
- package/src/lab/WorkspaceGrid.test.tsx +40 -0
- package/src/lab/WorkspaceGrid.tsx +20 -0
- package/src/lab/gridDims.test.ts +35 -0
- package/src/lab/gridDims.ts +11 -0
- package/src/lab/index.ts +10 -0
- package/src/layers/AGENTS.md +65 -0
- package/src/layers/LayerList.less +53 -0
- package/src/layers/LayerList.test.tsx +49 -0
- package/src/layers/LayerList.tsx +105 -0
- package/src/layers/index.ts +3 -0
- package/src/passthrough/weasel-canvas.test.ts +30 -0
- package/src/passthrough/weasel-canvas.ts +23 -0
- package/src/passthrough/weasel-ui.test.ts +79 -0
- package/src/passthrough/weasel-ui.ts +150 -0
- package/src/primitives/FpsMeter.less +14 -0
- package/src/primitives/FpsMeter.stories.tsx +14 -0
- package/src/primitives/FpsMeter.test.tsx +38 -0
- package/src/primitives/FpsMeter.tsx +36 -0
- package/src/primitives/ScaleIndicator.less +25 -0
- package/src/primitives/ScaleIndicator.stories.tsx +16 -0
- package/src/primitives/ScaleIndicator.test.tsx +46 -0
- package/src/primitives/ScaleIndicator.tsx +48 -0
- package/src/primitives/Sidebar.less +57 -0
- package/src/primitives/Sidebar.stories.tsx +38 -0
- package/src/primitives/Sidebar.test.tsx +74 -0
- package/src/primitives/Sidebar.tsx +32 -0
- package/src/primitives/StatusBar.less +21 -0
- package/src/primitives/StatusBar.stories.tsx +24 -0
- package/src/primitives/StatusBar.test.tsx +25 -0
- package/src/primitives/StatusBar.tsx +18 -0
- package/src/primitives/Toolbar.less +43 -0
- package/src/primitives/Toolbar.stories.tsx +36 -0
- package/src/primitives/Toolbar.test.tsx +65 -0
- package/src/primitives/Toolbar.tsx +44 -0
- package/src/primitives/fpsAverage.test.ts +21 -0
- package/src/primitives/fpsAverage.ts +5 -0
- package/src/primitives/index.ts +9 -0
- package/src/state/SingletonExperiment.test.tsx +101 -0
- package/src/state/SingletonExperiment.tsx +52 -0
- package/src/state/adapters.test.ts +94 -0
- package/src/state/adapters.ts +102 -0
- package/src/state/context.tsx +42 -0
- package/src/state/helpers.test.ts +89 -0
- package/src/state/helpers.ts +61 -0
- package/src/state/index.ts +40 -0
- package/src/state/store.test.ts +243 -0
- package/src/state/store.ts +225 -0
- package/src/state/types.ts +53 -0
- package/src/state/useExperimentState.test.tsx +102 -0
- package/src/state/useExperimentState.ts +28 -0
- package/src/styles.less +20 -0
- package/src/test-setup.ts +17 -0
- package/src/theme/Interstellar.stories.less +92 -0
- package/src/theme/Interstellar.stories.tsx +243 -0
- package/src/theme/base.less +122 -0
- package/src/theme/interstellar.less +34 -0
- package/src/theme/light.less +14 -0
- package/src/theme/tokens.less +85 -0
- package/src/ui/format.ts +19 -0
- package/src/ui/layers/LayerStack.less +143 -0
- package/src/ui/layers/LayerStack.stories.tsx +45 -0
- package/src/ui/layers/LayerStack.test.tsx +170 -0
- package/src/ui/layers/LayerStack.tsx +215 -0
- package/src/ui/layers/index.ts +1 -0
- package/src/ui/properties/CheckboxRow.stories.tsx +27 -0
- package/src/ui/properties/ColorRow.stories.tsx +106 -0
- package/src/ui/properties/CurveField.less +48 -0
- package/src/ui/properties/CurveField.stories.tsx +20 -0
- package/src/ui/properties/CurveField.test.tsx +127 -0
- package/src/ui/properties/CurveField.tsx +181 -0
- package/src/ui/properties/EffectCard.tsx +312 -0
- package/src/ui/properties/Gallery.stories.tsx +178 -0
- package/src/ui/properties/NumberRow.stories.tsx +65 -0
- package/src/ui/properties/PropertyGroup.less +50 -0
- package/src/ui/properties/PropertyGroup.stories.tsx +34 -0
- package/src/ui/properties/PropertyGroup.test.tsx +33 -0
- package/src/ui/properties/PropertyGroup.tsx +44 -0
- package/src/ui/properties/PropertyList.stories.tsx +83 -0
- package/src/ui/properties/PropertyPanel.less +592 -0
- package/src/ui/properties/PropertyPanel.stories.tsx +191 -0
- package/src/ui/properties/PropertyPanel.test.tsx +193 -0
- package/src/ui/properties/PropertyPanel.tsx +441 -0
- package/src/ui/properties/PropertyRow.stories.tsx +53 -0
- package/src/ui/properties/SelectRow.stories.tsx +58 -0
- package/src/ui/properties/SliderRow.stories.tsx +74 -0
- package/src/ui/properties/SpeechBalloonPanels.stories.tsx +396 -0
- package/src/ui/properties/TextRow.stories.tsx +62 -0
- package/src/ui/properties/ToggleRow.stories.tsx +57 -0
- package/src/ui/properties/index.ts +39 -0
- package/src/ui/properties/storyLayouts.tsx +36 -0
- package/src/undo/eventBus.test.ts +49 -0
- package/src/undo/eventBus.ts +32 -0
- package/src/undo/index.ts +5 -0
- package/src/undo/undoStack.test.ts +70 -0
- package/src/undo/undoStack.ts +38 -0
- package/src/workspace/DefaultSidebar.tsx +26 -0
- package/src/workspace/DefaultStatusBar.tsx +15 -0
- package/src/workspace/DefaultToolbar.tsx +88 -0
- package/src/workspace/Workspace.less +58 -0
- package/src/workspace/Workspace.stories.tsx +78 -0
- package/src/workspace/Workspace.test.tsx +186 -0
- package/src/workspace/Workspace.tsx +255 -0
- package/src/workspace/WorkspaceChrome.tsx +150 -0
- package/src/workspace/index.ts +24 -0
- package/src/workspace/slotTypes.ts +44 -0
- package/src/workspace/workspaceOps.test.ts +142 -0
- package/src/workspace/workspaceOps.ts +78 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Built to dist/theme-interstellar.css. Forces interstellar regardless of OS preference.
|
|
2
|
+
.lk-theme-interstellar {
|
|
3
|
+
--lk-bg: #0a0a14;
|
|
4
|
+
--lk-bg-elevated: rgba(20, 20, 36, 0.55);
|
|
5
|
+
--lk-bg-canvas: #02020a;
|
|
6
|
+
--lk-border: rgba(255, 255, 255, 0.14);
|
|
7
|
+
--lk-divider: rgba(255, 255, 255, 0.08);
|
|
8
|
+
--lk-glass-blur: 12px;
|
|
9
|
+
|
|
10
|
+
--lk-text: #e8e8f0;
|
|
11
|
+
--lk-text-muted: #a8a8c0;
|
|
12
|
+
--lk-text-disabled: #5a5a78;
|
|
13
|
+
|
|
14
|
+
--lk-accent: #b08adb;
|
|
15
|
+
--lk-accent-hover: #c6a3ee;
|
|
16
|
+
--lk-focus-ring: rgba(176, 138, 219, 0.45);
|
|
17
|
+
|
|
18
|
+
--lk-swatch-green: #2fdd18;
|
|
19
|
+
--lk-swatch-pink: #ff5885;
|
|
20
|
+
--lk-swatch-cyan: #00dfff;
|
|
21
|
+
--lk-swatch-gold: #dcb700;
|
|
22
|
+
--lk-swatch-amber: #ff8c00;
|
|
23
|
+
--lk-swatch-violet: #a497ff;
|
|
24
|
+
--lk-swatch-mint: #00e7af;
|
|
25
|
+
--lk-swatch-sky: #00b8ff;
|
|
26
|
+
--lk-swatch-orange: #ff6b00;
|
|
27
|
+
--lk-swatch-magenta: #ff6eff;
|
|
28
|
+
|
|
29
|
+
--lk-space-nebula:
|
|
30
|
+
radial-gradient(ellipse 60% 80% at 18% 30%, rgba(150, 80, 200, 0.22), transparent 70%),
|
|
31
|
+
radial-gradient(ellipse 55% 90% at 78% 70%, rgba(220, 100, 140, 0.18), transparent 65%),
|
|
32
|
+
radial-gradient(ellipse 50% 70% at 50% 50%, rgba(60, 110, 200, 0.18), transparent 75%),
|
|
33
|
+
radial-gradient(ellipse at center, #0a0a18 0%, #02020a 100%);
|
|
34
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Built to dist/theme-light.css. Forces light regardless of OS preference.
|
|
2
|
+
.lk-theme-light {
|
|
3
|
+
--lk-bg: #fafaf7;
|
|
4
|
+
--lk-bg-elevated: #ffffff;
|
|
5
|
+
--lk-bg-canvas: #f0ebe2;
|
|
6
|
+
--lk-border: #c9bba5;
|
|
7
|
+
--lk-divider: rgba(0, 0, 0, 0.08);
|
|
8
|
+
--lk-text: #2a2018;
|
|
9
|
+
--lk-text-muted: #6b5c4a;
|
|
10
|
+
--lk-text-disabled: #a89b85;
|
|
11
|
+
--lk-accent: #a86f3c;
|
|
12
|
+
--lk-accent-hover: #c2854f;
|
|
13
|
+
--lk-focus-ring: rgba(168, 111, 60, 0.35);
|
|
14
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// tokens.less — the contract.
|
|
2
|
+
// Interstellar is the base; @media (prefers-color-scheme: light) overrides for OS-following.
|
|
3
|
+
// Explicit .lk-theme-light / .lk-theme-interstellar classes (added by <Lab>) win via specificity.
|
|
4
|
+
|
|
5
|
+
:root {
|
|
6
|
+
// Surface
|
|
7
|
+
--lk-bg: #0a0a14;
|
|
8
|
+
--lk-bg-elevated: rgba(20, 20, 36, 0.55);
|
|
9
|
+
--lk-bg-canvas: #02020a;
|
|
10
|
+
--lk-border: rgba(255, 255, 255, 0.14);
|
|
11
|
+
--lk-divider: rgba(255, 255, 255, 0.08);
|
|
12
|
+
--lk-glass-blur: 12px;
|
|
13
|
+
|
|
14
|
+
// Text
|
|
15
|
+
--lk-text: #e8e8f0;
|
|
16
|
+
--lk-text-muted: #a8a8c0;
|
|
17
|
+
--lk-text-disabled: #5a5a78;
|
|
18
|
+
|
|
19
|
+
// Accent / interactive
|
|
20
|
+
--lk-accent: #b08adb;
|
|
21
|
+
--lk-accent-hover: #c6a3ee;
|
|
22
|
+
--lk-focus-ring: rgba(176, 138, 219, 0.45);
|
|
23
|
+
|
|
24
|
+
// Accent swatches — bright spot colors for plotting, status, data viz.
|
|
25
|
+
--lk-swatch-green: #2fdd18;
|
|
26
|
+
--lk-swatch-pink: #ff5885;
|
|
27
|
+
--lk-swatch-cyan: #00dfff;
|
|
28
|
+
--lk-swatch-gold: #dcb700;
|
|
29
|
+
--lk-swatch-amber: #ff8c00;
|
|
30
|
+
--lk-swatch-violet: #a497ff;
|
|
31
|
+
--lk-swatch-mint: #00e7af;
|
|
32
|
+
--lk-swatch-sky: #00b8ff;
|
|
33
|
+
--lk-swatch-orange: #ff6b00;
|
|
34
|
+
--lk-swatch-magenta: #ff6eff;
|
|
35
|
+
|
|
36
|
+
// Cosmic background — layered fill on .lk-lab.
|
|
37
|
+
--lk-space-nebula:
|
|
38
|
+
radial-gradient(ellipse 60% 80% at 18% 30%, rgba(150, 80, 200, 0.22), transparent 70%),
|
|
39
|
+
radial-gradient(ellipse 55% 90% at 78% 70%, rgba(220, 100, 140, 0.18), transparent 65%),
|
|
40
|
+
radial-gradient(ellipse 50% 70% at 50% 50%, rgba(60, 110, 200, 0.18), transparent 75%),
|
|
41
|
+
radial-gradient(ellipse at center, #0a0a18 0%, #02020a 100%);
|
|
42
|
+
|
|
43
|
+
// Sizing
|
|
44
|
+
--lk-radius: 6px;
|
|
45
|
+
--lk-radius-sm: 3px;
|
|
46
|
+
--lk-radius-lg: 14px;
|
|
47
|
+
--lk-control-height: 28px;
|
|
48
|
+
--lk-spacing-xs: 4px;
|
|
49
|
+
--lk-spacing-sm: 8px;
|
|
50
|
+
--lk-spacing-md: 12px;
|
|
51
|
+
--lk-spacing-lg: 16px;
|
|
52
|
+
|
|
53
|
+
// Typography
|
|
54
|
+
--lk-font: 'Oswald', system-ui, -apple-system, sans-serif;
|
|
55
|
+
--lk-font-mono: ui-monospace, SFMono-Regular, monospace;
|
|
56
|
+
--lk-font-display: 'Oswald', system-ui, sans-serif;
|
|
57
|
+
--lk-font-size: 13px;
|
|
58
|
+
--lk-font-size-sm: 11px;
|
|
59
|
+
--lk-font-weight-light: 300;
|
|
60
|
+
--lk-font-weight-medium: 500;
|
|
61
|
+
--lk-font-weight-bold: 700;
|
|
62
|
+
|
|
63
|
+
// Z-layers
|
|
64
|
+
--lk-z-toolbar: 10;
|
|
65
|
+
--lk-z-overlay: 20;
|
|
66
|
+
--lk-z-modal: 30;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@media (prefers-color-scheme: light) {
|
|
70
|
+
:root {
|
|
71
|
+
--lk-bg: #fafaf7;
|
|
72
|
+
--lk-bg-elevated: #ffffff;
|
|
73
|
+
--lk-bg-canvas: #f0ebe2;
|
|
74
|
+
--lk-border: #c9bba5;
|
|
75
|
+
--lk-divider: rgba(0, 0, 0, 0.08);
|
|
76
|
+
|
|
77
|
+
--lk-text: #2a2018;
|
|
78
|
+
--lk-text-muted: #6b5c4a;
|
|
79
|
+
--lk-text-disabled: #a89b85;
|
|
80
|
+
|
|
81
|
+
--lk-accent: #a86f3c;
|
|
82
|
+
--lk-accent-hover: #c2854f;
|
|
83
|
+
--lk-focus-ring: rgba(168, 111, 60, 0.35);
|
|
84
|
+
}
|
|
85
|
+
}
|
package/src/ui/format.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
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(--lk-font-display);
|
|
16
|
+
font-weight: var(--lk-font-weight-light, 300);
|
|
17
|
+
font-size: 1rem;
|
|
18
|
+
color: var(--lk-text);
|
|
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(--lk-border);
|
|
29
|
+
border-radius: var(--lk-radius-sm, 6px);
|
|
30
|
+
padding: 2px 8px;
|
|
31
|
+
font-size: 0.78rem;
|
|
32
|
+
color: var(--lk-text-dim);
|
|
33
|
+
cursor: pointer;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.lk-layer-stack__add:hover {
|
|
37
|
+
background: var(--lk-bg-elevated);
|
|
38
|
+
color: var(--lk-text);
|
|
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(--lk-accent, #6cf);
|
|
49
|
+
border-radius: 1px;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.lk-layer-stack__empty {
|
|
53
|
+
color: var(--lk-text-dim);
|
|
54
|
+
font-size: 0.85rem;
|
|
55
|
+
padding: 8px;
|
|
56
|
+
text-align: center;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.lk-layer-card {
|
|
60
|
+
background: var(--lk-bg-elevated);
|
|
61
|
+
border: 1px solid var(--lk-border);
|
|
62
|
+
border-radius: var(--lk-radius-md, 8px);
|
|
63
|
+
overflow: hidden;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.lk-layer-card.has-accent {
|
|
67
|
+
// Re-bind --lk-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
|
+
--lk-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(--lk-text-dim);
|
|
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(--lk-radius-sm, 6px);
|
|
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(--lk-text);
|
|
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(--lk-text-dim);
|
|
131
|
+
font-size: 1rem;
|
|
132
|
+
cursor: pointer;
|
|
133
|
+
padding: 2px 6px;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.lk-layer-card__remove:hover {
|
|
137
|
+
color: var(--lk-text);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.lk-layer-card__body {
|
|
141
|
+
padding: 8px;
|
|
142
|
+
border-top: 1px solid var(--lk-border);
|
|
143
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,170 @@
|
|
|
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
|
+
});
|