@weasel-js/labkit 1.3.0-pre.0 → 1.4.0-pre.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 (259) hide show
  1. package/dist/_dts/CanvasStackContext-DtAn_3O-.d.ts +42 -0
  2. package/dist/_dts/DrawCommand-B3bskUsC.d.ts +564 -0
  3. package/dist/_dts/{PrefsForm-BYa6cWnO.d.ts → PrefsForm-BkUJZx0A.d.ts} +4 -1
  4. package/dist/_dts/fitViewToBounds-dZ2UDB6e.d.ts +21 -0
  5. package/dist/_dts/frac-Cp3NivlC.d.ts +728 -0
  6. package/dist/_dts/index-BDVzvRzQ.d.ts +236 -0
  7. package/dist/_dts/shapeKinds-Cx_rxwsa.d.ts +87 -0
  8. package/dist/_dts/{DrawCommand-DBB45NfN.d.ts → types-C-gh9Ap-.d.ts} +1 -582
  9. package/dist/_dts/{types-AHlQxBNN.d.ts → types-D6s4b7if.d.ts} +2 -2
  10. package/dist/_dts/{types-1Sdxy_Pv.d.ts → types-DYMaEvM5.d.ts} +26 -1
  11. package/dist/_dts/{useTrialState-CsGMjhu9.d.ts → useTrialState-gmMvZqPc.d.ts} +7 -2
  12. package/dist/canvas/index.d.ts +43 -25
  13. package/dist/canvas/index.js +5 -3
  14. package/dist/chrome/index.d.ts +9 -6
  15. package/dist/chrome/index.js +7 -6
  16. package/dist/chunk-2AYGEN57.js +32 -0
  17. package/dist/chunk-2AYGEN57.js.map +1 -0
  18. package/dist/{chunk-RVNN4CHQ.js → chunk-3P5TTTA7.js} +16 -8
  19. package/dist/chunk-3P5TTTA7.js.map +1 -0
  20. package/dist/{chunk-NS54R52R.js → chunk-3ZELRRBV.js} +45 -40
  21. package/dist/chunk-3ZELRRBV.js.map +1 -0
  22. package/dist/{chunk-HFDVGF4X.js → chunk-4EY67BKJ.js} +32 -7
  23. package/dist/chunk-4EY67BKJ.js.map +1 -0
  24. package/dist/chunk-5TCEVMPI.js +140 -0
  25. package/dist/chunk-5TCEVMPI.js.map +1 -0
  26. package/dist/{chunk-O7NVSCLN.js → chunk-6JMU56CU.js} +133 -134
  27. package/dist/chunk-6JMU56CU.js.map +1 -0
  28. package/dist/{chunk-CPUJ3QXL.js → chunk-CQLQPQ4P.js} +2 -2
  29. package/dist/chunk-CQLQPQ4P.js.map +1 -0
  30. package/dist/chunk-CTRKTLYZ.js +33 -0
  31. package/dist/chunk-CTRKTLYZ.js.map +1 -0
  32. package/dist/chunk-EQIB2PGC.js +458 -0
  33. package/dist/chunk-EQIB2PGC.js.map +1 -0
  34. package/dist/chunk-HEORBLVT.js +7128 -0
  35. package/dist/chunk-HEORBLVT.js.map +1 -0
  36. package/dist/{chunk-V7PRSIRQ.js → chunk-JDDIZSLL.js} +4 -4
  37. package/dist/chunk-JDDIZSLL.js.map +1 -0
  38. package/dist/chunk-LITE5YFB.js +669 -0
  39. package/dist/chunk-LITE5YFB.js.map +1 -0
  40. package/dist/{chunk-Z6HJ5FEM.js → chunk-ROQDRXKW.js} +732 -315
  41. package/dist/chunk-ROQDRXKW.js.map +1 -0
  42. package/dist/chunk-SBXNQW4G.js +3 -0
  43. package/dist/chunk-SBXNQW4G.js.map +1 -0
  44. package/dist/{chunk-4TMMVIDM.js → chunk-XJ6N32QP.js} +50 -6
  45. package/dist/chunk-XJ6N32QP.js.map +1 -0
  46. package/dist/chunk-YQJNT4XQ.js +293 -0
  47. package/dist/chunk-YQJNT4XQ.js.map +1 -0
  48. package/dist/controls/index.d.ts +4 -28
  49. package/dist/controls/index.js +3 -3
  50. package/dist/dragdrop/index.d.ts +50 -5
  51. package/dist/dragdrop/index.js +3 -1
  52. package/dist/index.d.ts +660 -302
  53. package/dist/index.js +1793 -587
  54. package/dist/index.js.map +1 -1
  55. package/dist/layers/index.d.ts +6 -5
  56. package/dist/layers/index.js +4 -2
  57. package/dist/loupe/index.d.ts +187 -0
  58. package/dist/loupe/index.js +7 -0
  59. package/dist/loupe/index.js.map +1 -0
  60. package/dist/passthrough/weasel-canvas.d.ts +3 -1
  61. package/dist/passthrough/weasel-canvas.js +1 -1
  62. package/dist/passthrough/weasel-ui.d.ts +151 -91
  63. package/dist/passthrough/weasel-ui.js +3 -2
  64. package/dist/primitives/index.d.ts +3 -1
  65. package/dist/primitives/index.js +6 -5
  66. package/dist/state/index.d.ts +3 -3
  67. package/dist/state/index.js +2 -2
  68. package/dist/styles.css +229 -703
  69. package/dist/surface/index.d.ts +18 -2
  70. package/dist/surface/index.js +2 -2
  71. package/dist/ui/layers/index.d.ts +8 -2
  72. package/dist/ui/layers/index.js +2 -4
  73. package/dist/undo/index.d.ts +5 -4
  74. package/package.json +14 -7
  75. package/src/annotations/AnnotationOverlay.tsx +205 -0
  76. package/src/annotations/AnnotationTargets.tsx +37 -0
  77. package/src/annotations/Annotations.less +129 -0
  78. package/src/annotations/Annotations.meaning.test.tsx +111 -0
  79. package/src/annotations/Annotations.overlay.test.tsx +142 -0
  80. package/src/annotations/AnnotationsContext.ts +49 -0
  81. package/src/annotations/ExportMenu.test.tsx +86 -0
  82. package/src/annotations/ExportMenu.tsx +182 -0
  83. package/src/annotations/MarkList.tsx +75 -0
  84. package/src/annotations/capture.test.ts +140 -0
  85. package/src/annotations/capture.ts +237 -0
  86. package/src/annotations/drawOne.test.ts +56 -0
  87. package/src/annotations/drawOne.ts +47 -0
  88. package/src/annotations/frac.test.ts +59 -0
  89. package/src/annotations/frac.ts +66 -0
  90. package/src/annotations/history.test.ts +82 -0
  91. package/src/annotations/history.ts +118 -0
  92. package/src/annotations/index.ts +37 -0
  93. package/src/annotations/paint.test.ts +125 -0
  94. package/src/annotations/paint.ts +130 -0
  95. package/src/annotations/staleness.test.ts +56 -0
  96. package/src/annotations/staleness.ts +41 -0
  97. package/src/annotations/store.test.ts +175 -0
  98. package/src/annotations/store.ts +317 -0
  99. package/src/annotations/svgNodes.test.ts +77 -0
  100. package/src/annotations/svgNodes.ts +74 -0
  101. package/src/annotations/toolMap.test.ts +36 -0
  102. package/src/annotations/toolMap.ts +54 -0
  103. package/src/annotations/types.ts +222 -0
  104. package/src/annotations/view.test.ts +37 -0
  105. package/src/annotations/view.ts +40 -0
  106. package/src/canvas/AGENTS.md +55 -5
  107. package/src/canvas/CanvasStack.test.tsx +27 -1
  108. package/src/canvas/CanvasStack.tsx +29 -4
  109. package/src/canvas/CanvasStackContext.ts +23 -4
  110. package/src/canvas/camera.test.ts +78 -0
  111. package/src/canvas/camera.ts +62 -0
  112. package/src/canvas/canvasCoords.test.ts +26 -0
  113. package/src/canvas/canvasCoords.ts +20 -7
  114. package/src/canvas/index.ts +5 -1
  115. package/src/canvas/useLayerScheduler.ts +7 -3
  116. package/src/canvas/usePanZoom.test.ts +36 -5
  117. package/src/canvas/usePanZoom.ts +20 -15
  118. package/src/canvas/worldSpec.test.ts +63 -0
  119. package/src/canvas/worldSpec.ts +48 -0
  120. package/src/chrome/builtins.test.ts +31 -1
  121. package/src/chrome/builtins.tsx +63 -19
  122. package/src/chrome/regions/PaletteRegion.test.tsx +22 -1
  123. package/src/chrome/regions/PaletteRegion.tsx +4 -0
  124. package/src/chrome/regions/SidebarRegion.tsx +37 -13
  125. package/src/chrome/regions/ToolbarRegion.tsx +2 -1
  126. package/src/chrome/regions/ViewportRegion.tsx +9 -1
  127. package/src/chrome/regions/regions.test.tsx +6 -1
  128. package/src/chrome/types.ts +20 -0
  129. package/src/config/builder.test.ts +7 -1
  130. package/src/config/builder.ts +1 -5
  131. package/src/config/resolve.test.ts +1 -2
  132. package/src/controls/ControlPanel.stories.tsx +41 -2
  133. package/src/controls/ControlPanel.test.tsx +69 -9
  134. package/src/controls/ControlPanel.tsx +106 -25
  135. package/src/dragdrop/DragDropRuntime.tsx +8 -2
  136. package/src/dragdrop/dragDrop.test.tsx +29 -2
  137. package/src/dragdrop/index.ts +11 -0
  138. package/src/index.test.ts +22 -0
  139. package/src/index.ts +136 -4
  140. package/src/instrument/SineWave.smoke.test.tsx +1 -0
  141. package/src/instrument/types.ts +24 -1
  142. package/src/lab/Lab.surface.test.tsx +162 -0
  143. package/src/lab/Lab.tsx +102 -17
  144. package/src/lab/Lab.undock.test.tsx +85 -0
  145. package/src/lab/LabFullChrome.stories.tsx +20 -0
  146. package/src/lab/LabShell.less +11 -0
  147. package/src/lab/Workspace.less +73 -0
  148. package/src/lab/Workspace.surface.test.tsx +2 -0
  149. package/src/lab/Workspace.tsx +97 -8
  150. package/src/lab/index.ts +1 -1
  151. package/src/lab/panelHost.ts +37 -0
  152. package/src/layers/AGENTS.md +1 -1
  153. package/src/layers/LayerList.tsx +1 -1
  154. package/src/loupe/AGENTS.md +49 -0
  155. package/src/loupe/CanvasLoupe.tsx +83 -0
  156. package/src/loupe/DomLoupe.tsx +62 -0
  157. package/src/loupe/Loupe.less +38 -0
  158. package/src/loupe/LoupeBubble.tsx +32 -0
  159. package/src/loupe/TrialLoupe.tsx +93 -0
  160. package/src/loupe/canvasLens.test.ts +205 -0
  161. package/src/loupe/canvasLens.ts +141 -0
  162. package/src/loupe/index.ts +19 -0
  163. package/src/loupe/types.test.ts +29 -0
  164. package/src/loupe/types.ts +90 -0
  165. package/src/loupe/useHostSize.ts +30 -0
  166. package/src/loupe/useLoupe.test.tsx +197 -0
  167. package/src/loupe/useLoupe.ts +189 -0
  168. package/src/passthrough/weasel-ui.ts +25 -17
  169. package/src/primitives/FloatingPanel.test.tsx +76 -1
  170. package/src/primitives/FloatingPanel.tsx +19 -3
  171. package/src/primitives/ScaleIndicator.test.tsx +7 -2
  172. package/src/primitives/Sidebar.less +18 -0
  173. package/src/primitives/Toolbar.less +8 -0
  174. package/src/primitives/Toolbar.tsx +5 -0
  175. package/src/primitives/useRovingTabIndex.test.ts +21 -0
  176. package/src/primitives/useRovingTabIndex.ts +26 -17
  177. package/src/state/document.test.ts +23 -0
  178. package/src/state/document.ts +14 -2
  179. package/src/state/index.ts +2 -0
  180. package/src/state/store.test.ts +24 -0
  181. package/src/state/store.ts +35 -1
  182. package/src/state/types.ts +11 -0
  183. package/src/state/undock.test.ts +40 -0
  184. package/src/state/undock.ts +39 -0
  185. package/src/styles.less +2 -4
  186. package/src/surface/SurfaceContext.ts +4 -0
  187. package/src/surface/index.ts +8 -3
  188. package/src/surface/useSurfaceTile.test.tsx +2 -0
  189. package/src/surface/useSurfaceTile.ts +7 -1
  190. package/src/surface/useTiledSurface.test.tsx +58 -0
  191. package/src/surface/useTiledSurface.ts +38 -3
  192. package/src/theme/Interstellar.stories.tsx +1 -1
  193. package/src/trial/Trial.annotations.persist.test.tsx +166 -0
  194. package/src/trial/Trial.annotations.test.tsx +121 -0
  195. package/src/trial/Trial.canvas.test.tsx +76 -1
  196. package/src/trial/Trial.config.test.tsx +3 -8
  197. package/src/trial/Trial.less +19 -0
  198. package/src/trial/Trial.loupe.test.tsx +128 -0
  199. package/src/trial/Trial.stories.tsx +1 -0
  200. package/src/trial/Trial.test.tsx +12 -0
  201. package/src/trial/Trial.trialId.test.tsx +36 -0
  202. package/src/trial/Trial.tsx +215 -40
  203. package/src/trial/TrialChrome.tsx +35 -0
  204. package/src/trial/UndockedSections.tsx +65 -0
  205. package/src/trial/trialOps.test.ts +21 -0
  206. package/src/trial/trialOps.ts +5 -3
  207. package/src/ui/layers/index.ts +5 -1
  208. package/dist/_dts/index-DgcNdEdh.d.ts +0 -314
  209. package/dist/chunk-4TMMVIDM.js.map +0 -1
  210. package/dist/chunk-CPUJ3QXL.js.map +0 -1
  211. package/dist/chunk-HFDVGF4X.js.map +0 -1
  212. package/dist/chunk-NS54R52R.js.map +0 -1
  213. package/dist/chunk-O2BULFHX.js +0 -152
  214. package/dist/chunk-O2BULFHX.js.map +0 -1
  215. package/dist/chunk-O7NVSCLN.js.map +0 -1
  216. package/dist/chunk-PO6L3NSH.js +0 -366
  217. package/dist/chunk-PO6L3NSH.js.map +0 -1
  218. package/dist/chunk-PQJ5B2U2.js +0 -27
  219. package/dist/chunk-PQJ5B2U2.js.map +0 -1
  220. package/dist/chunk-RVNN4CHQ.js.map +0 -1
  221. package/dist/chunk-TLGRYALP.js +0 -6151
  222. package/dist/chunk-TLGRYALP.js.map +0 -1
  223. package/dist/chunk-V7PRSIRQ.js.map +0 -1
  224. package/dist/chunk-Z6HJ5FEM.js.map +0 -1
  225. package/dist/chunk-ZZAYVX4X.js +0 -505
  226. package/dist/chunk-ZZAYVX4X.js.map +0 -1
  227. package/src/primitives/DragHandleGlyph.tsx +0 -29
  228. package/src/ui/format.test.ts +0 -33
  229. package/src/ui/format.ts +0 -30
  230. package/src/ui/layers/LayerStack.less +0 -143
  231. package/src/ui/layers/LayerStack.stories.tsx +0 -45
  232. package/src/ui/layers/LayerStack.test.tsx +0 -170
  233. package/src/ui/layers/LayerStack.tsx +0 -208
  234. package/src/ui/properties/CheckboxRow.stories.tsx +0 -27
  235. package/src/ui/properties/ColorRow.stories.tsx +0 -106
  236. package/src/ui/properties/CurveField.less +0 -48
  237. package/src/ui/properties/CurveField.stories.tsx +0 -20
  238. package/src/ui/properties/CurveField.test.tsx +0 -127
  239. package/src/ui/properties/CurveField.tsx +0 -184
  240. package/src/ui/properties/EffectCard.tsx +0 -324
  241. package/src/ui/properties/Gallery.stories.tsx +0 -178
  242. package/src/ui/properties/NumberRow.stories.tsx +0 -65
  243. package/src/ui/properties/PropertyGroup.less +0 -50
  244. package/src/ui/properties/PropertyGroup.stories.tsx +0 -34
  245. package/src/ui/properties/PropertyGroup.test.tsx +0 -33
  246. package/src/ui/properties/PropertyGroup.tsx +0 -45
  247. package/src/ui/properties/PropertyList.stories.tsx +0 -83
  248. package/src/ui/properties/PropertyPanel.less +0 -606
  249. package/src/ui/properties/PropertyPanel.stories.tsx +0 -190
  250. package/src/ui/properties/PropertyPanel.test.tsx +0 -193
  251. package/src/ui/properties/PropertyPanel.tsx +0 -469
  252. package/src/ui/properties/PropertyRow.stories.tsx +0 -53
  253. package/src/ui/properties/SelectRow.stories.tsx +0 -58
  254. package/src/ui/properties/SliderRow.stories.tsx +0 -74
  255. package/src/ui/properties/SpeechBalloonPanels.stories.tsx +0 -398
  256. package/src/ui/properties/TextRow.stories.tsx +0 -62
  257. package/src/ui/properties/ToggleRow.stories.tsx +0 -57
  258. package/src/ui/properties/index.ts +0 -39
  259. package/src/ui/properties/storyLayouts.tsx +0 -36
@@ -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
- };
@@ -1,65 +0,0 @@
1
- import type { Meta, StoryObj } from '@storybook/react-vite';
2
- import { useState } from 'react';
3
- import { NumberRow } from './PropertyPanel';
4
- import { SideBySide } from './storyLayouts';
5
-
6
- const meta: Meta<typeof NumberRow> = {
7
- title: 'labkit/UI/Properties/Rows/NumberRow',
8
- component: NumberRow,
9
- };
10
- export default meta;
11
- type Story = StoryObj<typeof NumberRow>;
12
-
13
- function Controlled({
14
- initial,
15
- label,
16
- min,
17
- max,
18
- step,
19
- layout,
20
- }: {
21
- initial: number;
22
- label: string;
23
- min?: number;
24
- max?: number;
25
- step?: number;
26
- layout?: 'block' | 'inline';
27
- }) {
28
- const [value, setValue] = useState(initial);
29
- return (
30
- <NumberRow
31
- label={label}
32
- value={value}
33
- onChange={setValue}
34
- min={min}
35
- max={max}
36
- step={step}
37
- layout={layout}
38
- />
39
- );
40
- }
41
-
42
- export const BothLayouts: Story = {
43
- render: () => (
44
- <SideBySide
45
- block={<Controlled initial={8} label="Count" min={0} max={100} step={1} />}
46
- inline={<Controlled initial={8} label="Count" min={0} max={100} step={1} layout="inline" />}
47
- />
48
- ),
49
- };
50
-
51
- export const Float: Story = {
52
- render: () => (
53
- <div style={{ width: 280 }}>
54
- <Controlled initial={1.5} label="Factor" min={0} max={10} step={0.1} />
55
- </div>
56
- ),
57
- };
58
-
59
- export const Unbounded: Story = {
60
- render: () => (
61
- <div style={{ width: 280 }}>
62
- <Controlled initial={0} label="Offset" />
63
- </div>
64
- ),
65
- };
@@ -1,50 +0,0 @@
1
- .lk-property-group {
2
- grid-column: 1 / -1;
3
- background: var(--wzl-surface-sunken);
4
- border-radius: var(--wzl-radius-md);
5
- padding: 10px 12px 12px;
6
- }
7
-
8
- .lk-property-group__title {
9
- display: grid;
10
- grid-template-columns: 1fr auto 1fr;
11
- align-items: center;
12
- gap: 10px;
13
- margin: 0 0 8px;
14
- font-family: var(--wzl-font-display);
15
- font-weight: var(--wzl-font-weight-light);
16
- font-size: var(--wzl-font-size-sm);
17
- letter-spacing: var(--wzl-tracking-wider);
18
- text-transform: uppercase;
19
- color: var(--wzl-fg-subtle);
20
- }
21
-
22
- .lk-property-group__title hr {
23
- border: 0;
24
- border-top: 1px solid var(--wzl-border);
25
- margin: 0;
26
- }
27
-
28
- .lk-property-group__body {
29
- display: grid;
30
- grid-template-columns: 1fr 1fr;
31
- column-gap: 16px;
32
- row-gap: 10px;
33
- // Stretch so paired rows match in height — slider rows then bottom-align
34
- // their tracks (via `margin-top: auto`) and stay lined up even when one
35
- // sibling's label wraps to two lines.
36
- align-items: stretch;
37
- }
38
-
39
- .lk-property-group:not(.lk-property-group--pairs) .lk-property-group__body > :not(.lk-property-row--color) {
40
- grid-column: 1 / -1;
41
- }
42
-
43
- .lk-property-group--pairs .lk-property-group__body > * {
44
- grid-column: auto;
45
- }
46
-
47
- .lk-property-group--pairs .lk-property-group__body > h3,
48
- .lk-property-group--pairs .lk-property-group__body > .lk-property-group__span {
49
- grid-column: 1 / -1;
50
- }
@@ -1,34 +0,0 @@
1
- import type { Meta, StoryObj } from '@storybook/react-vite';
2
- import { PropertyGroup } from './PropertyGroup';
3
- import { PropertyList, PropertyPanel, SliderRow } from './PropertyPanel';
4
-
5
- const meta: Meta<typeof PropertyGroup> = {
6
- title: 'labkit/UI/Properties/PropertyGroup',
7
- component: PropertyGroup,
8
- };
9
- export default meta;
10
-
11
- export const Basic: StoryObj<typeof PropertyGroup> = {
12
- render: () => (
13
- <PropertyPanel title="Fill">
14
- <PropertyList>
15
- <SliderRow label="Amount" value={0.6} min={0} max={1} step={0.02} onChange={() => {}} />
16
- <PropertyGroup title="Aqua">
17
- <SliderRow
18
- label="Light angle"
19
- value={270}
20
- min={0}
21
- max={359}
22
- step={1}
23
- unit="°"
24
- onChange={() => {}}
25
- />
26
- <SliderRow label="Gloss" value={0.55} min={0} max={1} step={0.02} onChange={() => {}} />
27
- </PropertyGroup>
28
- <PropertyGroup title="Bevel" hidden>
29
- <SliderRow label="Rings" value={32} min={4} max={96} step={1} onChange={() => {}} />
30
- </PropertyGroup>
31
- </PropertyList>
32
- </PropertyPanel>
33
- ),
34
- };
@@ -1,33 +0,0 @@
1
- import { render, screen } from '@testing-library/react';
2
- import { describe, expect, it } from 'vitest';
3
- import { PropertyGroup } from './PropertyGroup';
4
-
5
- describe('PropertyGroup', () => {
6
- it('renders title and children', () => {
7
- render(
8
- <PropertyGroup title="Aqua">
9
- <div>child</div>
10
- </PropertyGroup>,
11
- );
12
- expect(screen.getByText('Aqua')).toBeInTheDocument();
13
- expect(screen.getByText('child')).toBeInTheDocument();
14
- });
15
-
16
- it('renders nothing when hidden is true', () => {
17
- const { container } = render(
18
- <PropertyGroup title="Bevel" hidden>
19
- <div>child</div>
20
- </PropertyGroup>,
21
- );
22
- expect(container).toBeEmptyDOMElement();
23
- });
24
-
25
- it('applies subpanel class', () => {
26
- const { container } = render(
27
- <PropertyGroup title="Dome">
28
- <div>x</div>
29
- </PropertyGroup>,
30
- );
31
- expect(container.firstChild).toHaveClass('lk-property-group');
32
- });
33
- });