@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
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: var(--wzl-font-size-lg);
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: var(--wzl-font-size-sm);
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: var(--wzl-radius-sm);
50
- }
51
-
52
- .lk-layer-stack__empty {
53
- color: var(--wzl-fg-subtle);
54
- font-size: var(--wzl-font-size);
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: var(--wzl-font-weight-bold);
100
- font-size: var(--wzl-font-size);
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: var(--wzl-font-size);
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: var(--wzl-tracking-wide);
125
- }
126
-
127
- .lk-root .lk-layer-card__remove {
128
- background: transparent;
129
- border: 0;
130
- color: var(--wzl-fg-subtle);
131
- font-size: var(--wzl-font-size-lg);
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
- });
@@ -1,208 +0,0 @@
1
- import { type CSSProperties, type ReactNode, type RefCallback, useEffect, useState } from 'react';
2
- import { dlog, useReorderDragList } from '../../passthrough/weasel-ui';
3
- import { DragHandleGlyph } from '../../primitives/DragHandleGlyph';
4
-
5
- /** One card in a layer stack: its identity, the label shown when collapsed,
6
- * and the optional select hoisted into its header. */
7
- export interface LayerStackItem {
8
- /** Stable id used for keys, onRemove, onReorder. Numeric to match
9
- * common id-from-nextId conventions; string ids also work. */
10
- id: number | string;
11
- /** Short kind label rendered in the header when no primary select
12
- * is hoisted (e.g. "shadow", "stroke"). */
13
- kind: string;
14
- /** When present, hoist this select into the card header so the user
15
- * can switch mode/shape without expanding. */
16
- primaryValue?: string;
17
- primaryOptions?: string[];
18
- /** Accent CSS color used as the left border / index-badge fill. */
19
- accent?: string;
20
- /** Optional badge text rendered before the primary control
21
- * (e.g. tail index "1", "2", "3"). When omitted a drag handle
22
- * glyph renders in its place. */
23
- badge?: string;
24
- /** Initial expanded state. Defaults to true for newly-added items. */
25
- defaultExpanded?: boolean;
26
- }
27
-
28
- /** Props for `<LayerStack>`. */
29
- export interface LayerStackProps {
30
- title: string;
31
- items: LayerStackItem[];
32
- /** Kinds the user can add via the header palette. */
33
- paletteKinds: string[];
34
- onAdd: (kind: string) => void;
35
- onRemove: (id: number | string) => void;
36
- onReorder: (orderedIds: Array<number | string>) => void;
37
- onPrimaryChange: (id: number | string, nextValue: string) => void;
38
- /** Render the body controls for each item. */
39
- renderBody: (item: LayerStackItem) => ReactNode;
40
- /** Hide the title + palette row (used when an outer wrap renders its
41
- * own head — see speech-balloons Tails panel). */
42
- hideHead?: boolean;
43
- }
44
-
45
- /** A drag-reorderable stack of expandable cards, with a palette in the header
46
- * for adding more. The body of each card is the caller's to render. */
47
- export function LayerStack({
48
- title,
49
- items,
50
- paletteKinds,
51
- onAdd,
52
- onRemove,
53
- onReorder,
54
- onPrimaryChange,
55
- renderBody,
56
- hideHead,
57
- }: LayerStackProps) {
58
- const [expandedIds, setExpandedIds] = useState<Set<number | string>>(
59
- () => new Set(items.filter((i) => i.defaultExpanded !== false).map((i) => i.id)),
60
- );
61
-
62
- useEffect(() => {
63
- setExpandedIds((prev) => {
64
- let next: Set<number | string> | null = null;
65
- for (const item of items) {
66
- if (item.defaultExpanded === false) continue;
67
- if (prev.has(item.id)) continue;
68
- if (next === null) next = new Set(prev);
69
- next.add(item.id);
70
- }
71
- return next ?? prev;
72
- });
73
- }, [items]);
74
-
75
- const dragItems = items.map((it) => ({ id: String(it.id), label: it.kind }));
76
- const drag = useReorderDragList({
77
- items: dragItems,
78
- selectedIds: [],
79
- onReorder: (ids, targetIndex) => {
80
- dlog('layer-stack', 'onReorder', { ids, targetIndex });
81
- const orig = items.map((i) => i.id);
82
- const moving = new Set(ids);
83
- const remaining = orig.filter((id) => !moving.has(String(id)));
84
- const movedIds = items.map((i) => i.id).filter((id) => moving.has(String(id)));
85
- // weasel reports targetIndex against the original list (which includes
86
- // the dragged row). Each moved item that originally sat before
87
- // targetIndex needs to shift the insertion point left by one.
88
- const shift = orig.filter((id, idx) => moving.has(String(id)) && idx < targetIndex).length;
89
- const adjusted = targetIndex - shift;
90
- const out = [...remaining];
91
- out.splice(adjusted, 0, ...movedIds);
92
- onReorder(out);
93
- },
94
- });
95
-
96
- const toggleExpanded = (id: number | string) => {
97
- setExpandedIds((s) => {
98
- const n = new Set(s);
99
- if (n.has(id)) n.delete(id);
100
- else n.add(id);
101
- return n;
102
- });
103
- };
104
-
105
- return (
106
- <div className="lk-layer-stack">
107
- {!hideHead && (
108
- <div className="lk-layer-stack__head">
109
- <h2 className="lk-layer-stack__title">{title}</h2>
110
- <div className="lk-layer-stack__palette">
111
- {paletteKinds.map((k) => (
112
- <button
113
- key={k}
114
- type="button"
115
- className="lk-layer-stack__add"
116
- onClick={() => {
117
- dlog('layer-stack', 'onAdd', { kind: k });
118
- onAdd(k);
119
- }}
120
- aria-label={`Add ${k}`}
121
- >
122
- {k}
123
- </button>
124
- ))}
125
- </div>
126
- </div>
127
- )}
128
- <div
129
- className="lk-layer-stack__list"
130
- ref={drag.containerProps.ref as RefCallback<HTMLDivElement>}
131
- onPointerMove={drag.containerProps.onPointerMove}
132
- onPointerUp={drag.containerProps.onPointerUp}
133
- onPointerCancel={drag.containerProps.onPointerCancel}
134
- >
135
- {items.map((item, i) => {
136
- const expanded = expandedIds.has(item.id);
137
- const draggedId = drag.state.draggedIds?.[0];
138
- const isDragging = draggedId === String(item.id);
139
- const showHintBefore = drag.state.targetIndex === i && draggedId !== String(item.id);
140
- const showHintAfter = drag.state.targetIndex === items.length && i === items.length - 1;
141
- const cardCls = [
142
- 'lk-layer-card',
143
- expanded ? 'is-expanded' : 'is-collapsed',
144
- isDragging ? 'is-dragging' : '',
145
- item.accent ? 'has-accent' : '',
146
- ]
147
- .filter(Boolean)
148
- .join(' ');
149
- const cardStyle = item.accent
150
- ? ({ '--lk-layer-card-accent': item.accent } as CSSProperties)
151
- : undefined;
152
- const { onPointerDown } = drag.rowProps(String(item.id), i);
153
- return (
154
- <div key={item.id} className="lk-layer-card-wrap">
155
- {showHintBefore && <div className="lk-layer-stack__drop-hint" />}
156
- <div className={cardCls} data-testid={`lk-layer-card-${item.id}`} style={cardStyle}>
157
- <div className="lk-layer-card__head">
158
- <button
159
- type="button"
160
- className="lk-layer-card__handle"
161
- aria-label={`Drag to reorder layer ${item.id}`}
162
- onPointerDown={onPointerDown}
163
- onClick={() => toggleExpanded(item.id)}
164
- >
165
- {item.badge ?? <DragHandleGlyph />}
166
- </button>
167
- {item.primaryValue !== undefined && item.primaryOptions ? (
168
- <select
169
- className="lk-layer-card__primary"
170
- value={item.primaryValue}
171
- aria-label={`Primary select for layer ${item.id}`}
172
- onChange={(e) => onPrimaryChange(item.id, e.target.value)}
173
- onClick={(e) => e.stopPropagation()}
174
- >
175
- {item.primaryOptions.map((o) => (
176
- <option key={o} value={o}>
177
- {o}
178
- </option>
179
- ))}
180
- </select>
181
- ) : (
182
- <span className="lk-layer-card__kind">{item.kind}</span>
183
- )}
184
- <button
185
- type="button"
186
- className="lk-layer-card__remove"
187
- aria-label="Remove layer"
188
- onClick={(e) => {
189
- e.stopPropagation();
190
- onRemove(item.id);
191
- }}
192
- >
193
- ✕
194
- </button>
195
- </div>
196
- {expanded && <div className="lk-layer-card__body">{renderBody(item)}</div>}
197
- </div>
198
- {showHintAfter && <div className="lk-layer-stack__drop-hint" />}
199
- </div>
200
- );
201
- })}
202
- {items.length === 0 && (
203
- <div className="lk-layer-stack__empty">No layers — add one above.</div>
204
- )}
205
- </div>
206
- </div>
207
- );
208
- }
@@ -1,27 +0,0 @@
1
- import type { Meta, StoryObj } from '@storybook/react-vite';
2
- import { useState } from 'react';
3
- import { CheckboxRow } from './PropertyPanel';
4
-
5
- const meta: Meta<typeof CheckboxRow> = {
6
- title: 'labkit/UI/Properties/Rows/CheckboxRow',
7
- component: CheckboxRow,
8
- };
9
- export default meta;
10
- type Story = StoryObj<typeof CheckboxRow>;
11
-
12
- function Controlled({ initial, label }: { initial: boolean; label: string }) {
13
- const [value, setValue] = useState(initial);
14
- return (
15
- <div style={{ width: 280 }}>
16
- <CheckboxRow label={label} value={value} onChange={setValue} />
17
- </div>
18
- );
19
- }
20
-
21
- export const Default: Story = {
22
- render: () => <Controlled initial={true} label="Visible" />,
23
- };
24
-
25
- export const Unchecked: Story = {
26
- render: () => <Controlled initial={false} label="Snap to grid" />,
27
- };