@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,184 +0,0 @@
1
- import { useCallback, useMemo } from 'react';
2
- import { type ControlPoint, CurveEditor, dlog } from '../../passthrough/weasel-ui';
3
-
4
- /** An annotation drawn over a curve plot: a shaded band across a range of x,
5
- * or a vertical line at one x. */
6
- export type CurveMark =
7
- | { kind: 'band'; x: [number, number]; color?: string }
8
- | { kind: 'line'; x: number; color?: string };
9
-
10
- /** Props for `<CurveField>`. */
11
- export interface CurveFieldProps {
12
- /** Flat [x0, y0, x1, y1, …] — matches how curve-as-array configs
13
- * serialize in JSON snapshots. */
14
- values: number[];
15
- min: number;
16
- max: number;
17
- step: number;
18
- /** Plot width in CSS px. Caller sizes (typically uses a ResizeObserver). */
19
- width: number;
20
- /** Plot height in CSS px. Default 110. */
21
- height?: number;
22
- /** Flat default curve. When provided, a Reset button restores it. */
23
- defaults?: readonly number[];
24
- onChange: (next: number[]) => void;
25
- /** Optional vertical marks overlaid on the plot (e.g. landmarks). */
26
- marks?: readonly CurveMark[];
27
- }
28
-
29
- /**
30
- * A function-domain (y = f(x), x ∈ [0,1]) curve editor with per-stop
31
- * numeric readouts and flip buttons. Wraps weasel-ui's
32
- * `CurveEditor`; consumers wanting the raw editor (2D paths,
33
- * custom anchors) should import it from `@weasel-js/labkit/weasel-ui`.
34
- */
35
- export function CurveField({
36
- values,
37
- min,
38
- max,
39
- step,
40
- width,
41
- height = 110,
42
- defaults,
43
- onChange,
44
- marks,
45
- }: CurveFieldProps) {
46
- const points: ControlPoint[] = useMemo(() => {
47
- const out: ControlPoint[] = [];
48
- for (let i = 0; i + 1 < values.length; i += 2) out.push({ x: values[i], y: values[i + 1] });
49
- return out;
50
- }, [values]);
51
-
52
- const handleChange = useCallback(
53
- (next: ControlPoint[]) => {
54
- const flat: number[] = new Array(next.length * 2);
55
- for (let i = 0; i < next.length; i++) {
56
- const ySnap = Math.round(next[i].y / step) * step;
57
- const y = Math.max(min, Math.min(max, ySnap));
58
- flat[i * 2] = next[i].x;
59
- flat[i * 2 + 1] = y;
60
- }
61
- dlog('curve-field', 'handleChange', {
62
- nLen: next.length,
63
- flatLen: flat.length,
64
- sample: flat.slice(0, 4),
65
- });
66
- onChange(flat);
67
- },
68
- [onChange, min, max, step],
69
- );
70
-
71
- const handleFlip = useCallback(() => {
72
- const flipped: Array<{ x: number; y: number }> = [];
73
- for (let i = 0; i + 1 < values.length; i += 2) {
74
- flipped.push({ x: 1 - values[i], y: values[i + 1] });
75
- }
76
- flipped.sort((a, b) => a.x - b.x);
77
- const out: number[] = [];
78
- for (const p of flipped) out.push(p.x, p.y);
79
- onChange(out);
80
- }, [values, onChange]);
81
-
82
- const handleFlipVertical = useCallback(() => {
83
- const mid = (min + max) / 2;
84
- const flipped: number[] = new Array(values.length);
85
- for (let i = 0; i + 1 < values.length; i += 2) {
86
- flipped[i] = values[i];
87
- flipped[i + 1] = 2 * mid - values[i + 1];
88
- }
89
- onChange(flipped);
90
- }, [values, min, max, onChange]);
91
-
92
- return (
93
- <div className="lk-curve-field">
94
- <div className="lk-curve-field__plot">
95
- <CurveEditor
96
- value={points}
97
- onInput={handleChange}
98
- domain="1d"
99
- constrain="function"
100
- xRange={[0, 1]}
101
- yRange={[min, max]}
102
- width={width}
103
- height={height}
104
- endpoints="pinned-x"
105
- addPointMode="click-curve"
106
- minPoints={2}
107
- grid={{}}
108
- history={false}
109
- />
110
- {marks && marks.length > 0 && (
111
- <svg
112
- className="lk-curve-field__marks"
113
- width={width}
114
- height={height}
115
- viewBox={`0 0 ${width} ${height}`}
116
- aria-hidden="true"
117
- >
118
- {marks.map((m) => {
119
- const color = m.color ?? '#ffcc00';
120
- if (m.kind === 'band') {
121
- const x0 = Math.max(0, Math.min(1, m.x[0])) * width;
122
- const x1 = Math.max(0, Math.min(1, m.x[1])) * width;
123
- const left = Math.min(x0, x1);
124
- const w = Math.abs(x1 - x0);
125
- return (
126
- <g key={`band-${m.x[0]}-${m.x[1]}-${color}`}>
127
- <rect x={left} y={0} width={w} height={height} fill={color} fillOpacity={0.4} />
128
- <line
129
- x1={left}
130
- y1={0}
131
- x2={left}
132
- y2={height}
133
- stroke={color}
134
- strokeOpacity={0.85}
135
- strokeWidth={1}
136
- />
137
- <line
138
- x1={left + w}
139
- y1={0}
140
- x2={left + w}
141
- y2={height}
142
- stroke={color}
143
- strokeOpacity={0.85}
144
- strokeWidth={1}
145
- />
146
- </g>
147
- );
148
- }
149
- const x = Math.max(0, Math.min(1, m.x)) * width;
150
- return (
151
- <line
152
- key={`mark-${m.x}-${color}`}
153
- x1={x}
154
- y1={0}
155
- x2={x}
156
- y2={height}
157
- stroke={color}
158
- strokeWidth={1}
159
- />
160
- );
161
- })}
162
- </svg>
163
- )}
164
- </div>
165
- <div className="lk-curve-field__actions">
166
- <button type="button" className="lk-curve-field__action" onClick={handleFlip}>
167
- Flip horizontally
168
- </button>
169
- <button type="button" className="lk-curve-field__action" onClick={handleFlipVertical}>
170
- Flip vertically
171
- </button>
172
- {defaults && (
173
- <button
174
- type="button"
175
- className="lk-curve-field__action"
176
- onClick={() => onChange([...defaults])}
177
- >
178
- Reset
179
- </button>
180
- )}
181
- </div>
182
- </div>
183
- );
184
- }
@@ -1,324 +0,0 @@
1
- import { type CSSProperties, type ReactNode, useState } from 'react';
2
-
3
- // ── Subpanel ─────────────────────────────────────────────────────────
4
- // Headered group of controls inside a wider panel/list. The header is a
5
- // label flanked by a horizontal rule — no background, padding, or border;
6
- // purely a typographic divider. Ported from speech-balloons styles.css:277-313.
7
-
8
- /** Props for `<Subpanel>`. */
9
- export interface SubpanelProps {
10
- title: ReactNode;
11
- children: ReactNode;
12
- className?: string;
13
- }
14
-
15
- /** A typographic divider inside a property list: a small title flanked by a
16
- * rule. Purely a heading — use `<PropertyGroup>` for a bordered section. */
17
- export function Subpanel({ title, children, className }: SubpanelProps) {
18
- const cls = className ? `lk-subpanel ${className}` : 'lk-subpanel';
19
- return (
20
- <div className={cls}>
21
- <h4 className="lk-subpanel__title">
22
- <span>{title}</span>
23
- <hr />
24
- </h4>
25
- {children}
26
- </div>
27
- );
28
- }
29
-
30
- // ── EffectCard ───────────────────────────────────────────────────────
31
- // Draggable, collapsible card for stacked effect editors. Optional --lk-panel-accent
32
- // per-instance recolors the title bar, border-left, and (via re-binding --wzl-accent
33
- // inside the card) every descendant control that reads from the accent token.
34
-
35
- /** Props for `<EffectCard>`. */
36
- export interface EffectCardProps {
37
- /** Card title rendered in the title bar (e.g. effect kind label or a select). */
38
- title: ReactNode;
39
- /** Optional badge/chip displayed before the remove button (e.g. primary value summary). */
40
- primary?: ReactNode;
41
- /** Optional accent color in any CSS color form. Sets --lk-panel-accent on the card. */
42
- accent?: string;
43
- /** Optional ordinal badge that doubles as the drag handle (used by tail cards in SB). */
44
- index?: number;
45
- expanded?: boolean;
46
- onToggleExpanded?: () => void;
47
- onRemove?: () => void;
48
- /**
49
- * Drag-handle wiring. When provided, an inline handle (⋮⋮ icon or `index`)
50
- * activates HTML5 drag on press. Consumer owns the drag/drop coordination —
51
- * see EffectCardList for the batteries-included wrapper.
52
- */
53
- draggable?: boolean;
54
- onPressHandle?: () => void;
55
- onReleaseHandle?: () => void;
56
- onDragStart?: (e: React.DragEvent<HTMLDivElement>) => void;
57
- onDragEnd?: (e: React.DragEvent<HTMLDivElement>) => void;
58
- onDragOver?: (e: React.DragEvent<HTMLDivElement>) => void;
59
- onDragLeave?: (e: React.DragEvent<HTMLDivElement>) => void;
60
- onDrop?: (e: React.DragEvent<HTMLDivElement>) => void;
61
- dragging?: boolean;
62
- children?: ReactNode;
63
- className?: string;
64
- }
65
-
66
- /** An accented, collapsible card for one item in a list of like things —
67
- * effects, layers, tails. The accent color rebinds the theme accent token
68
- * within the card, so its controls pick it up too. */
69
- export function EffectCard({
70
- title,
71
- primary,
72
- accent,
73
- index,
74
- expanded = true,
75
- onToggleExpanded,
76
- onRemove,
77
- draggable,
78
- onPressHandle,
79
- onReleaseHandle,
80
- onDragStart,
81
- onDragEnd,
82
- onDragOver,
83
- onDragLeave,
84
- onDrop,
85
- dragging,
86
- children,
87
- className,
88
- }: EffectCardProps) {
89
- const stateClass = `${expanded ? 'is-expanded' : 'is-collapsed'}${dragging ? ' is-dragging' : ''}${accent ? ' has-accent' : ''}`;
90
- const cls = `lk-effect-card ${stateClass}${className ? ` ${className}` : ''}`;
91
- // Per-instance dynamic color → CSS custom property requires inline style.
92
- // The card's CSS re-binds --wzl-accent to --lk-panel-accent so descendants tint.
93
- const style = accent ? ({ '--lk-panel-accent': accent } as CSSProperties) : undefined;
94
-
95
- // When a toggle handler is supplied the header acts as a disclosure button:
96
- // role + tab stop + Enter/Space. Bundled together so the interactive props are
97
- // applied as a set (no half-interactive element when onToggleExpanded is absent).
98
- const headProps = onToggleExpanded
99
- ? {
100
- role: 'button' as const,
101
- tabIndex: 0,
102
- onClick: onToggleExpanded,
103
- onKeyDown: (e: React.KeyboardEvent) => {
104
- if (e.key === 'Enter' || e.key === ' ') {
105
- e.preventDefault();
106
- onToggleExpanded();
107
- }
108
- },
109
- }
110
- : {};
111
-
112
- return (
113
- // biome-ignore lint/a11y/noStaticElementInteractions: native HTML5 drag-and-drop container — drag events are not click/keyboard interactions and have no semantic element; the in-card handle and header carry the a11y affordances.
114
- <div
115
- className={cls}
116
- style={style}
117
- draggable={draggable}
118
- onDragStart={onDragStart}
119
- onDragEnd={onDragEnd}
120
- onDragOver={onDragOver}
121
- onDragLeave={onDragLeave}
122
- onDrop={onDrop}
123
- >
124
- <div className="lk-effect-card__head" {...headProps}>
125
- {index != null ? (
126
- <button
127
- type="button"
128
- className="lk-effect-card__index-badge lk-effect-card__handle"
129
- title="Drag to reorder · click to toggle"
130
- aria-label={`Item ${index + 1} — drag to reorder, click to toggle`}
131
- onMouseDown={onPressHandle}
132
- onMouseUp={onReleaseHandle}
133
- onClick={(e) => {
134
- e.stopPropagation();
135
- onToggleExpanded?.();
136
- }}
137
- >
138
- {index + 1}
139
- </button>
140
- ) : (
141
- <span
142
- className="lk-effect-card__handle"
143
- aria-hidden="true"
144
- title="Drag to reorder"
145
- onMouseDown={onPressHandle}
146
- onMouseUp={onReleaseHandle}
147
- >
148
- <DragHandleIcon />
149
- </span>
150
- )}
151
- <span className="lk-effect-card__title">{title}</span>
152
- {primary != null && <span className="lk-effect-card__primary">{primary}</span>}
153
- {onRemove && (
154
- <button
155
- type="button"
156
- className="lk-effect-card__remove"
157
- onClick={(e) => {
158
- e.stopPropagation();
159
- onRemove();
160
- }}
161
- title="Remove"
162
- aria-label="Remove"
163
- >
164
- ✕
165
- </button>
166
- )}
167
- </div>
168
- {expanded && <div className="lk-effect-card__body">{children}</div>}
169
- </div>
170
- );
171
- }
172
-
173
- function DragHandleIcon() {
174
- return (
175
- <svg width="10" height="14" viewBox="0 0 10 14" aria-hidden="true">
176
- <circle cx="3" cy="3" r="1.3" fill="currentColor" />
177
- <circle cx="7" cy="3" r="1.3" fill="currentColor" />
178
- <circle cx="3" cy="7" r="1.3" fill="currentColor" />
179
- <circle cx="7" cy="7" r="1.3" fill="currentColor" />
180
- <circle cx="3" cy="11" r="1.3" fill="currentColor" />
181
- <circle cx="7" cy="11" r="1.3" fill="currentColor" />
182
- </svg>
183
- );
184
- }
185
-
186
- // ── EffectCardList ───────────────────────────────────────────────────
187
- // Batteries-included list wrapper that coordinates HTML5 drag-and-drop across
188
- // a list of EffectCards. Consumer supplies items + a renderItem callback;
189
- // onReorder fires with (sourceId, targetId, position) on drop. Mirrors the
190
- // LayerStack pattern from speech-balloons Lab.tsx:630-707.
191
-
192
- /** The minimum an item must carry to appear in an `<EffectCardList>`. */
193
- export interface EffectCardListItem {
194
- id: string | number;
195
- }
196
-
197
- /** Props for `<EffectCardList>`. */
198
- export interface EffectCardListProps<T extends EffectCardListItem> {
199
- items: ReadonlyArray<T>;
200
- renderItem: (
201
- item: T,
202
- helpers: {
203
- isExpanded: boolean;
204
- isDragging: boolean;
205
- toggleExpanded: () => void;
206
- cardProps: Pick<
207
- EffectCardProps,
208
- | 'expanded'
209
- | 'dragging'
210
- | 'draggable'
211
- | 'onToggleExpanded'
212
- | 'onPressHandle'
213
- | 'onReleaseHandle'
214
- | 'onDragStart'
215
- | 'onDragEnd'
216
- | 'onDragOver'
217
- | 'onDragLeave'
218
- | 'onDrop'
219
- >;
220
- },
221
- ) => ReactNode;
222
- onReorder: (sourceId: T['id'], targetId: T['id'], position: 'before' | 'after') => void;
223
- /** Optional rendered when the list is empty. */
224
- empty?: ReactNode;
225
- /** IDs to render expanded on first mount. Defaults to all collapsed. */
226
- defaultExpandedIds?: ReadonlyArray<T['id']>;
227
- }
228
-
229
- /** A drag-reorderable list of `<EffectCard>`s. Owns the expanded and dragging
230
- * state and hands it back to `renderItem`, so the caller only supplies each
231
- * card's contents. */
232
- export function EffectCardList<T extends EffectCardListItem>({
233
- items,
234
- renderItem,
235
- onReorder,
236
- empty,
237
- defaultExpandedIds,
238
- }: EffectCardListProps<T>) {
239
- const [expanded, setExpanded] = useState<Set<T['id']>>(() => new Set(defaultExpandedIds ?? []));
240
- const [draggingId, setDraggingId] = useState<T['id'] | null>(null);
241
- const [pressedId, setPressedId] = useState<T['id'] | null>(null);
242
- const [dropHint, setDropHint] = useState<{ id: T['id']; position: 'before' | 'after' } | null>(
243
- null,
244
- );
245
-
246
- if (items.length === 0) {
247
- return <>{empty ?? <div className="lk-effect-card-list__empty">No items.</div>}</>;
248
- }
249
-
250
- return (
251
- <div className="lk-effect-card-list">
252
- {items.map((item) => {
253
- const isExpanded = expanded.has(item.id);
254
- const isDragging = draggingId === item.id;
255
- const isPressed = pressedId === item.id;
256
- const showHintBefore =
257
- dropHint && dropHint.id === item.id && dropHint.position === 'before';
258
- const showHintAfter = dropHint && dropHint.id === item.id && dropHint.position === 'after';
259
-
260
- const toggleExpanded = () =>
261
- setExpanded((prev) => {
262
- const next = new Set(prev);
263
- if (next.has(item.id)) next.delete(item.id);
264
- else next.add(item.id);
265
- return next;
266
- });
267
-
268
- return (
269
- <div key={String(item.id)} className="lk-effect-card-list__wrap">
270
- {showHintBefore && <div className="lk-effect-card-list__drop-hint" />}
271
- {renderItem(item, {
272
- isExpanded,
273
- isDragging,
274
- toggleExpanded,
275
- cardProps: {
276
- expanded: isExpanded,
277
- dragging: isDragging,
278
- draggable: isPressed,
279
- onToggleExpanded: toggleExpanded,
280
- onPressHandle: () => setPressedId(item.id),
281
- onReleaseHandle: () => setPressedId(null),
282
- onDragStart: (e) => {
283
- e.dataTransfer.effectAllowed = 'move';
284
- e.dataTransfer.setData('text/plain', String(item.id));
285
- setDraggingId(item.id);
286
- },
287
- onDragEnd: () => {
288
- setDraggingId(null);
289
- setPressedId(null);
290
- setDropHint(null);
291
- },
292
- onDragOver: (e) => {
293
- if (draggingId === null || draggingId === item.id) return;
294
- e.preventDefault();
295
- e.dataTransfer.dropEffect = 'move';
296
- const rect = (e.currentTarget as HTMLDivElement).getBoundingClientRect();
297
- const position: 'before' | 'after' =
298
- e.clientY < rect.top + rect.height / 2 ? 'before' : 'after';
299
- setDropHint({ id: item.id, position });
300
- },
301
- onDragLeave: () => {
302
- setDropHint((h) => (h && h.id === item.id ? null : h));
303
- },
304
- onDrop: (e) => {
305
- e.preventDefault();
306
- if (draggingId === null || draggingId === item.id) {
307
- setDropHint(null);
308
- return;
309
- }
310
- const rect = (e.currentTarget as HTMLDivElement).getBoundingClientRect();
311
- const position: 'before' | 'after' =
312
- e.clientY < rect.top + rect.height / 2 ? 'before' : 'after';
313
- onReorder(draggingId, item.id, position);
314
- setDropHint(null);
315
- },
316
- },
317
- })}
318
- {showHintAfter && <div className="lk-effect-card-list__drop-hint" />}
319
- </div>
320
- );
321
- })}
322
- </div>
323
- );
324
- }
@@ -1,178 +0,0 @@
1
- import type { Meta, StoryObj } from '@storybook/react-vite';
2
- import { useState } from 'react';
3
- import {
4
- CheckboxRow,
5
- ColorRow,
6
- NumberRow,
7
- PropertyList,
8
- PropertyPanel,
9
- PropertyRow,
10
- SelectRow,
11
- SliderRow,
12
- TextRow,
13
- ToggleRow,
14
- } from './PropertyPanel';
15
-
16
- // Titled "Gallery" so it sorts above PropertyPanel/PropertyList/Rows in the
17
- // sidebar alphabetically (G < P < R). Acts as the visual root of the section.
18
- const meta: Meta = {
19
- title: 'labkit/UI/Properties/Gallery',
20
- };
21
- export default meta;
22
- type Story = StoryObj;
23
-
24
- function BlockRows() {
25
- const [opacity, setOpacity] = useState(0.65);
26
- const [count, setCount] = useState(8);
27
- const [name, setName] = useState('Untitled');
28
- const [mode, setMode] = useState<'fill' | 'stroke' | 'both'>('both');
29
- const [align, setAlign] = useState<'left' | 'center' | 'right'>('center');
30
- return (
31
- <PropertyPanel title="Block layout (default)">
32
- <PropertyList>
33
- <SliderRow
34
- label="Opacity"
35
- value={opacity}
36
- min={0}
37
- max={1}
38
- step={0.01}
39
- onChange={setOpacity}
40
- format={(v) => v.toFixed(2)}
41
- />
42
- <NumberRow label="Count" value={count} onChange={setCount} min={0} max={100} />
43
- <TextRow label="Name" value={name} onChange={setName} />
44
- <SelectRow
45
- label="Mode"
46
- value={mode}
47
- onChange={setMode}
48
- options={[
49
- { value: 'fill', label: 'Fill only' },
50
- { value: 'stroke', label: 'Stroke only' },
51
- { value: 'both', label: 'Fill + stroke' },
52
- ]}
53
- />
54
- <ToggleRow
55
- label="Align"
56
- value={align}
57
- onChange={setAlign}
58
- options={[
59
- { value: 'left', label: 'L' },
60
- { value: 'center', label: 'C' },
61
- { value: 'right', label: 'R' },
62
- ]}
63
- />
64
- </PropertyList>
65
- </PropertyPanel>
66
- );
67
- }
68
-
69
- function InlineRows() {
70
- const [opacity, setOpacity] = useState(0.5);
71
- const [count, setCount] = useState(8);
72
- const [name, setName] = useState('Untitled');
73
- const [mode, setMode] = useState<'fill' | 'stroke' | 'both'>('both');
74
- const [align, setAlign] = useState<'left' | 'center' | 'right'>('center');
75
- return (
76
- <PropertyPanel title="Inline layout">
77
- <PropertyList>
78
- <SliderRow
79
- label="Opacity"
80
- layout="inline"
81
- value={opacity}
82
- min={0}
83
- max={1}
84
- step={0.01}
85
- onChange={setOpacity}
86
- format={(v) => v.toFixed(2)}
87
- />
88
- <NumberRow
89
- label="Count"
90
- layout="inline"
91
- value={count}
92
- onChange={setCount}
93
- min={0}
94
- max={100}
95
- />
96
- <TextRow label="Name" layout="inline" value={name} onChange={setName} />
97
- <SelectRow
98
- label="Mode"
99
- layout="inline"
100
- value={mode}
101
- onChange={setMode}
102
- options={[
103
- { value: 'fill', label: 'Fill only' },
104
- { value: 'stroke', label: 'Stroke only' },
105
- { value: 'both', label: 'Fill + stroke' },
106
- ]}
107
- />
108
- <ToggleRow
109
- label="Align"
110
- layout="inline"
111
- value={align}
112
- onChange={setAlign}
113
- options={[
114
- { value: 'left', label: 'L' },
115
- { value: 'center', label: 'C' },
116
- { value: 'right', label: 'R' },
117
- ]}
118
- />
119
- </PropertyList>
120
- </PropertyPanel>
121
- );
122
- }
123
-
124
- function IntrinsicRows() {
125
- const [fill, setFill] = useState('#b08adb');
126
- const [stroke, setStroke] = useState('#1a1428');
127
- const [highlight, setHighlight] = useState('#7fb069');
128
- const [shadow, setShadow] = useState('#000000');
129
- const [visible, setVisible] = useState(true);
130
- const [snap, setSnap] = useState(false);
131
- return (
132
- <PropertyPanel title="Intrinsic-layout variants">
133
- <PropertyList>
134
- <ColorRow label="Fill" value={fill} onChange={setFill} />
135
- <ColorRow label="Stroke" value={stroke} onChange={setStroke} />
136
- <ColorRow label="Highlight" value={highlight} onChange={setHighlight} />
137
- <ColorRow label="Shadow" value={shadow} onChange={setShadow} />
138
- <CheckboxRow label="Visible" value={visible} onChange={setVisible} />
139
- <CheckboxRow label="Snap to grid" value={snap} onChange={setSnap} />
140
- </PropertyList>
141
- </PropertyPanel>
142
- );
143
- }
144
-
145
- function CustomRows() {
146
- return (
147
- <PropertyPanel title="Custom (PropertyRow)">
148
- <PropertyList>
149
- <PropertyRow label="Tempo" readout="120 bpm">
150
- <button type="button">Tap to set</button>
151
- </PropertyRow>
152
- <PropertyRow label="Free-form" layout="inline">
153
- <code style={{ background: 'rgba(0,0,0,0.35)', padding: '2px 6px', borderRadius: 3 }}>
154
- arbitrary children
155
- </code>
156
- </PropertyRow>
157
- </PropertyList>
158
- </PropertyPanel>
159
- );
160
- }
161
-
162
- export const All: Story = {
163
- render: () => (
164
- <div
165
- style={{
166
- display: 'grid',
167
- gridTemplateColumns: 'repeat(auto-fit, minmax(320px, 1fr))',
168
- gap: 16,
169
- maxWidth: 1100,
170
- }}
171
- >
172
- <BlockRows />
173
- <InlineRows />
174
- <IntrinsicRows />
175
- <CustomRows />
176
- </div>
177
- ),
178
- };