@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.
Files changed (227) hide show
  1. package/dist/_dts/{DrawCommand-C_XboUpz.d.ts → DrawCommand-DBB45NfN.d.ts} +196 -52
  2. package/dist/_dts/PrefsForm-BYa6cWnO.d.ts +201 -0
  3. package/dist/_dts/types-1Sdxy_Pv.d.ts +90 -0
  4. package/dist/_dts/types-BttTIent.d.ts +314 -0
  5. package/dist/_dts/types-DCGEcMO_.d.ts +162 -0
  6. package/dist/_dts/{useTrialState-BMNIx3Cy.d.ts → useTrialState-CsGMjhu9.d.ts} +5 -87
  7. package/dist/canvas/index.d.ts +7 -3
  8. package/dist/canvas/index.js +3 -1
  9. package/dist/chrome/index.d.ts +86 -0
  10. package/dist/chrome/index.js +9 -0
  11. package/dist/chrome/index.js.map +1 -0
  12. package/dist/chunk-3XXPU73K.js +138 -0
  13. package/dist/chunk-3XXPU73K.js.map +1 -0
  14. package/dist/{chunk-VUU5UXHE.js → chunk-4TMMVIDM.js} +8 -5
  15. package/dist/chunk-4TMMVIDM.js.map +1 -0
  16. package/dist/chunk-6X5RBC6G.js +366 -0
  17. package/dist/chunk-6X5RBC6G.js.map +1 -0
  18. package/dist/{chunk-574LJAV4.js → chunk-BDJXIBRZ.js} +33 -35
  19. package/dist/chunk-BDJXIBRZ.js.map +1 -0
  20. package/dist/{chunk-SFL7NFKN.js → chunk-BMW4TDP5.js} +15 -7
  21. package/dist/chunk-BMW4TDP5.js.map +1 -0
  22. package/dist/chunk-JDROYQM3.js +266 -0
  23. package/dist/chunk-JDROYQM3.js.map +1 -0
  24. package/dist/{chunk-THBG7FQZ.js → chunk-LGKRIUSW.js} +15 -12
  25. package/dist/chunk-LGKRIUSW.js.map +1 -0
  26. package/dist/{chunk-BOHF3PQO.js → chunk-QCICIHEO.js} +8129 -5196
  27. package/dist/chunk-QCICIHEO.js.map +1 -0
  28. package/dist/chunk-QRYPXSGP.js +212 -0
  29. package/dist/chunk-QRYPXSGP.js.map +1 -0
  30. package/dist/chunk-RGT4EJIB.js +6961 -0
  31. package/dist/chunk-RGT4EJIB.js.map +1 -0
  32. package/dist/chunk-SBXNQW4G.js +3 -0
  33. package/dist/chunk-SBXNQW4G.js.map +1 -0
  34. package/dist/chunk-T66HU3HX.js +17 -0
  35. package/dist/chunk-T66HU3HX.js.map +1 -0
  36. package/dist/chunk-WTI26YTM.js +266 -0
  37. package/dist/chunk-WTI26YTM.js.map +1 -0
  38. package/dist/controls/index.d.ts +19 -5
  39. package/dist/controls/index.js +3 -1
  40. package/dist/dragdrop/index.d.ts +46 -3
  41. package/dist/dragdrop/index.js +3 -1
  42. package/dist/index.d.ts +735 -565
  43. package/dist/index.js +661 -1019
  44. package/dist/index.js.map +1 -1
  45. package/dist/layers/index.d.ts +5 -3
  46. package/dist/layers/index.js +4 -1
  47. package/dist/passthrough/weasel-canvas.d.ts +69 -13
  48. package/dist/passthrough/weasel-canvas.js +1 -1
  49. package/dist/passthrough/weasel-ui.d.ts +3811 -3671
  50. package/dist/passthrough/weasel-ui.js +3 -2
  51. package/dist/primitives/index.d.ts +102 -8
  52. package/dist/primitives/index.js +5 -1
  53. package/dist/state/index.d.ts +3 -2
  54. package/dist/state/index.js +2 -2
  55. package/dist/styles.css +490 -803
  56. package/dist/surface/index.js +2 -1
  57. package/dist/ui/layers/index.d.ts +8 -2
  58. package/dist/ui/layers/index.js +2 -3
  59. package/dist/undo/index.d.ts +4 -2
  60. package/package.json +16 -7
  61. package/src/canvas/AGENTS.md +4 -0
  62. package/src/canvas/CanvasStack.tsx +6 -2
  63. package/src/canvas/useLayerScheduler.ts +22 -15
  64. package/src/canvas/usePanZoom.test.ts +80 -0
  65. package/src/canvas/usePanZoom.ts +16 -2
  66. package/src/chrome/ChromeRegions.stories.tsx +106 -0
  67. package/src/chrome/builtins.test.ts +153 -0
  68. package/src/chrome/builtins.tsx +205 -0
  69. package/src/chrome/index.ts +25 -0
  70. package/src/chrome/merge.test.ts +51 -0
  71. package/src/chrome/merge.ts +52 -0
  72. package/src/chrome/regions/PaletteRegion.less +14 -0
  73. package/src/chrome/regions/PaletteRegion.test.tsx +36 -0
  74. package/src/chrome/regions/PaletteRegion.tsx +41 -0
  75. package/src/chrome/regions/SidebarRegion.tsx +56 -0
  76. package/src/chrome/regions/StatusRegion.tsx +31 -0
  77. package/src/chrome/regions/TitleBarRegion.tsx +41 -0
  78. package/src/chrome/regions/ToolbarRegion.test.tsx +82 -0
  79. package/src/chrome/regions/ToolbarRegion.tsx +71 -0
  80. package/src/chrome/regions/ViewportRegion.less +42 -0
  81. package/src/chrome/regions/ViewportRegion.tsx +37 -0
  82. package/src/chrome/regions/regions.test.tsx +81 -0
  83. package/src/chrome/types.ts +127 -0
  84. package/src/config/builder.test.ts +93 -0
  85. package/src/config/builder.ts +220 -0
  86. package/src/config/fromConfigField.test.ts +93 -0
  87. package/src/config/fromConfigField.ts +74 -0
  88. package/src/config/index.ts +22 -0
  89. package/src/config/resolve.test.ts +119 -0
  90. package/src/config/resolve.ts +67 -0
  91. package/src/config/rules.ts +68 -0
  92. package/src/config/types.ts +108 -0
  93. package/src/config/useConfigSchema.ts +30 -0
  94. package/src/config/visible.ts +20 -0
  95. package/src/controls/ControlPanel.less +4 -58
  96. package/src/controls/ControlPanel.stories.tsx +125 -0
  97. package/src/controls/ControlPanel.test.tsx +245 -10
  98. package/src/controls/ControlPanel.tsx +236 -203
  99. package/src/dragdrop/DragDropRuntime.tsx +36 -18
  100. package/src/dragdrop/DragGhost.less +1 -1
  101. package/src/dragdrop/Palette.less +1 -1
  102. package/src/dragdrop/index.ts +11 -0
  103. package/src/index.ts +74 -5
  104. package/src/instrument/SineWave.smoke.test.tsx +8 -1
  105. package/src/instrument/defineInstrument.test.ts +38 -1
  106. package/src/instrument/defineInstrument.ts +31 -4
  107. package/src/instrument/index.ts +1 -0
  108. package/src/instrument/types.ts +27 -4
  109. package/src/instrument/validateConfigSchema.test.ts +13 -3
  110. package/src/instrument/validateConfigSchema.ts +7 -10
  111. package/src/lab/Lab.chrome.test.tsx +65 -0
  112. package/src/lab/Lab.tsx +58 -15
  113. package/src/lab/LabContext.ts +6 -0
  114. package/src/lab/LabFullChrome.stories.tsx +308 -0
  115. package/src/lab/LabHeader.test.tsx +42 -0
  116. package/src/lab/LabHeader.tsx +57 -0
  117. package/src/lab/LabPalette.test.tsx +35 -0
  118. package/src/lab/LabPalette.tsx +34 -0
  119. package/src/lab/LabShell.less +50 -3
  120. package/src/lab/Workspace.less +1 -30
  121. package/src/lab/Workspace.test.tsx +20 -10
  122. package/src/lab/Workspace.tsx +5 -8
  123. package/src/lab/index.ts +3 -1
  124. package/src/layers/AGENTS.md +1 -1
  125. package/src/layers/LayerList.less +31 -7
  126. package/src/layers/LayerList.tsx +17 -4
  127. package/src/passthrough/weasel-ui.ts +28 -0
  128. package/src/primitives/FloatingPanel.less +28 -0
  129. package/src/primitives/FloatingPanel.stories.tsx +46 -0
  130. package/src/primitives/FloatingPanel.test.tsx +240 -0
  131. package/src/primitives/FloatingPanel.tsx +190 -0
  132. package/src/primitives/FpsMeter.test.tsx +44 -0
  133. package/src/primitives/FpsMeter.tsx +20 -9
  134. package/src/primitives/JobProgress.less +69 -0
  135. package/src/primitives/JobProgress.stories.tsx +48 -0
  136. package/src/primitives/JobProgress.tsx +65 -0
  137. package/src/primitives/Legend.less +47 -0
  138. package/src/primitives/Legend.stories.tsx +32 -0
  139. package/src/primitives/Legend.test.tsx +57 -0
  140. package/src/primitives/Legend.tsx +40 -0
  141. package/src/primitives/Sidebar.less +32 -2
  142. package/src/primitives/StatusBar.less +7 -0
  143. package/src/primitives/StatusBar.tsx +10 -3
  144. package/src/primitives/Toolbar.less +50 -3
  145. package/src/primitives/Toolbar.test.tsx +67 -1
  146. package/src/primitives/Toolbar.tsx +72 -6
  147. package/src/primitives/ZoomControl.less +31 -0
  148. package/src/primitives/ZoomControl.tsx +76 -0
  149. package/src/primitives/floating.entry.test.tsx +14 -0
  150. package/src/primitives/index.ts +10 -2
  151. package/src/primitives/useRovingTabIndex.test.ts +27 -0
  152. package/src/primitives/useRovingTabIndex.ts +79 -0
  153. package/src/state/store.ts +11 -3
  154. package/src/state/toolSlot.test.ts +36 -0
  155. package/src/state/types.ts +4 -0
  156. package/src/styles.less +6 -4
  157. package/src/surface/useTiledSurface.ts +14 -10
  158. package/src/test-setup.ts +26 -0
  159. package/src/theme/Interstellar.stories.tsx +1 -1
  160. package/src/theme/base.less +19 -2
  161. package/src/theme/interstellar.tokens.json +6 -2
  162. package/src/tools/types.ts +24 -0
  163. package/src/trial/Trial.config.test.tsx +108 -0
  164. package/src/trial/Trial.job.test.tsx +13 -15
  165. package/src/trial/Trial.less +115 -10
  166. package/src/trial/Trial.stories.tsx +1 -0
  167. package/src/trial/Trial.test.tsx +67 -96
  168. package/src/trial/Trial.tsx +80 -39
  169. package/src/trial/TrialChrome.tsx +99 -80
  170. package/src/trial/TrialDragContext.ts +10 -0
  171. package/src/trial/TrialTitleBar.tsx +41 -0
  172. package/src/trial/index.ts +1 -15
  173. package/src/trial/trialChrome.borders.test.ts +38 -0
  174. package/src/ui/layers/index.ts +5 -1
  175. package/dist/_dts/index-JFAYj5Tv.d.ts +0 -144
  176. package/dist/_dts/types-x92Kfeme.d.ts +0 -62
  177. package/dist/chunk-574LJAV4.js.map +0 -1
  178. package/dist/chunk-5R2ATYPJ.js +0 -201
  179. package/dist/chunk-5R2ATYPJ.js.map +0 -1
  180. package/dist/chunk-73KA7WBO.js +0 -5992
  181. package/dist/chunk-73KA7WBO.js.map +0 -1
  182. package/dist/chunk-BOHF3PQO.js.map +0 -1
  183. package/dist/chunk-G5TJVQQT.js +0 -161
  184. package/dist/chunk-G5TJVQQT.js.map +0 -1
  185. package/dist/chunk-PMAU3SEE.js +0 -126
  186. package/dist/chunk-PMAU3SEE.js.map +0 -1
  187. package/dist/chunk-SFL7NFKN.js.map +0 -1
  188. package/dist/chunk-THBG7FQZ.js.map +0 -1
  189. package/dist/chunk-VUU5UXHE.js.map +0 -1
  190. package/src/instrument/capabilityDetector.test.ts +0 -64
  191. package/src/instrument/capabilityDetector.ts +0 -20
  192. package/src/trial/DefaultSidebar.tsx +0 -29
  193. package/src/trial/DefaultStatusBar.tsx +0 -19
  194. package/src/trial/DefaultToolbar.tsx +0 -92
  195. package/src/trial/slotTypes.ts +0 -54
  196. package/src/ui/format.test.ts +0 -33
  197. package/src/ui/format.ts +0 -30
  198. package/src/ui/layers/LayerStack.less +0 -143
  199. package/src/ui/layers/LayerStack.stories.tsx +0 -45
  200. package/src/ui/layers/LayerStack.test.tsx +0 -170
  201. package/src/ui/layers/LayerStack.tsx +0 -220
  202. package/src/ui/properties/CheckboxRow.stories.tsx +0 -27
  203. package/src/ui/properties/ColorRow.stories.tsx +0 -106
  204. package/src/ui/properties/CurveField.less +0 -48
  205. package/src/ui/properties/CurveField.stories.tsx +0 -20
  206. package/src/ui/properties/CurveField.test.tsx +0 -127
  207. package/src/ui/properties/CurveField.tsx +0 -184
  208. package/src/ui/properties/EffectCard.tsx +0 -324
  209. package/src/ui/properties/Gallery.stories.tsx +0 -178
  210. package/src/ui/properties/NumberRow.stories.tsx +0 -65
  211. package/src/ui/properties/PropertyGroup.less +0 -50
  212. package/src/ui/properties/PropertyGroup.stories.tsx +0 -34
  213. package/src/ui/properties/PropertyGroup.test.tsx +0 -33
  214. package/src/ui/properties/PropertyGroup.tsx +0 -45
  215. package/src/ui/properties/PropertyList.stories.tsx +0 -83
  216. package/src/ui/properties/PropertyPanel.less +0 -592
  217. package/src/ui/properties/PropertyPanel.stories.tsx +0 -190
  218. package/src/ui/properties/PropertyPanel.test.tsx +0 -193
  219. package/src/ui/properties/PropertyPanel.tsx +0 -469
  220. package/src/ui/properties/PropertyRow.stories.tsx +0 -53
  221. package/src/ui/properties/SelectRow.stories.tsx +0 -58
  222. package/src/ui/properties/SliderRow.stories.tsx +0 -74
  223. package/src/ui/properties/SpeechBalloonPanels.stories.tsx +0 -396
  224. package/src/ui/properties/TextRow.stories.tsx +0 -62
  225. package/src/ui/properties/ToggleRow.stories.tsx +0 -57
  226. package/src/ui/properties/index.ts +0 -39
  227. 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
- }
@@ -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;
@@ -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
- });