@heyramzi/ui 1.54.2 → 2.0.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 (361) hide show
  1. package/COMPONENTS.md +3 -2
  2. package/README.md +34 -9
  3. package/package.json +198 -6
  4. package/src/ai/generation-progress/GenerationProgress.svelte +12 -12
  5. package/src/ambient.d.ts +8 -0
  6. package/src/brand/brand.test.ts +67 -0
  7. package/src/brand/index.ts +137 -0
  8. package/src/canvas/README.md +236 -0
  9. package/src/canvas/card/card-frame.svelte +61 -0
  10. package/src/canvas/card/card.css +39 -0
  11. package/src/canvas/card/resize-handles.svelte +119 -0
  12. package/src/canvas/card/resize.ts +33 -0
  13. package/src/canvas/chrome/chrome-zoom.svelte +120 -0
  14. package/src/canvas/chrome/chrome.css +231 -0
  15. package/src/canvas/chrome/lock-badge.svelte +18 -0
  16. package/src/canvas/chrome/side-handles.svelte +30 -0
  17. package/src/canvas/chrome/size-chip.svelte +45 -0
  18. package/src/canvas/chrome/snap-guides.svelte +69 -0
  19. package/src/canvas/components/icons/ToolIcon.svelte +30 -0
  20. package/src/canvas/components/shared/svg/PreviewSvg.svelte +67 -0
  21. package/src/canvas/components/shared/svg/ShapeOutline.svelte +76 -0
  22. package/src/canvas/components/shared/svg/StrokeNode.svelte +69 -0
  23. package/src/canvas/components/shared/svg/TextRule.svelte +37 -0
  24. package/src/canvas/edges/bend-handle.svelte +76 -0
  25. package/src/canvas/edges/edge-gradient.svelte +30 -0
  26. package/src/canvas/edges/edge-path.test.ts +58 -0
  27. package/src/canvas/edges/edge-path.ts +293 -0
  28. package/src/canvas/edges/gradient-edge.svelte +43 -0
  29. package/src/canvas/editor/context-menu.test.ts +65 -0
  30. package/src/canvas/editor/context-menu.ts +48 -0
  31. package/src/canvas/editor/dispatch.svelte.test.ts +243 -0
  32. package/src/canvas/editor/dispatch.svelte.ts +205 -0
  33. package/src/canvas/editor/editor-context.ts +62 -0
  34. package/src/canvas/editor/editor-mode.ts +67 -0
  35. package/src/canvas/editor/frame.ts +98 -0
  36. package/src/canvas/editor/hand-context.ts +15 -0
  37. package/src/canvas/editor/history-core.test.ts +239 -0
  38. package/src/canvas/editor/history-core.ts +247 -0
  39. package/src/canvas/editor/keymap.test.ts +161 -0
  40. package/src/canvas/editor/keymap.ts +151 -0
  41. package/src/canvas/editor/layout-actions.test.ts +156 -0
  42. package/src/canvas/editor/layout-actions.ts +219 -0
  43. package/src/canvas/editor/lock.test.ts +68 -0
  44. package/src/canvas/editor/lock.ts +87 -0
  45. package/src/canvas/editor/rebase.test.ts +208 -0
  46. package/src/canvas/editor/rebase.ts +272 -0
  47. package/src/canvas/editor/style.ts +184 -0
  48. package/src/canvas/editor/text-field-context.ts +41 -0
  49. package/src/canvas/editor/z-order.test.ts +75 -0
  50. package/src/canvas/editor/z-order.ts +87 -0
  51. package/src/canvas/headless/alignment.test.ts +59 -0
  52. package/src/canvas/headless/alignment.ts +23 -0
  53. package/src/canvas/headless/break-apart-find.ts +418 -0
  54. package/src/canvas/headless/break-apart-pieces.ts +104 -0
  55. package/src/canvas/headless/break-apart-plan.ts +439 -0
  56. package/src/canvas/headless/break-apart-style.ts +106 -0
  57. package/src/canvas/headless/break-apart.ts +244 -0
  58. package/src/canvas/headless/canvas-cleanup.test.ts +181 -0
  59. package/src/canvas/headless/canvas-cleanup.ts +102 -0
  60. package/src/canvas/headless/edge-anchors.test.ts +70 -0
  61. package/src/canvas/headless/edge-anchors.ts +143 -0
  62. package/src/canvas/headless/edge-memo.test.ts +58 -0
  63. package/src/canvas/headless/edge-memo.ts +32 -0
  64. package/src/canvas/headless/edge-serialization.test.ts +55 -0
  65. package/src/canvas/headless/edge-serialization.ts +32 -0
  66. package/src/canvas/headless/figure-geometry.ts +293 -0
  67. package/src/canvas/headless/fit-viewport.test.ts +85 -0
  68. package/src/canvas/headless/fit-viewport.ts +137 -0
  69. package/src/canvas/headless/grid.test.ts +34 -0
  70. package/src/canvas/headless/grid.ts +94 -0
  71. package/src/canvas/headless/icon-node.test.ts +23 -0
  72. package/src/canvas/headless/icon-node.ts +66 -0
  73. package/src/canvas/headless/index.ts +9 -0
  74. package/src/canvas/headless/linkify.test.ts +23 -0
  75. package/src/canvas/headless/linkify.ts +25 -0
  76. package/src/canvas/headless/pen-marks.ts +133 -0
  77. package/src/canvas/headless/sketch.test.ts +99 -0
  78. package/src/canvas/headless/sketch.ts +302 -0
  79. package/src/canvas/headless/snap-guides.test.ts +61 -0
  80. package/src/canvas/headless/snap-guides.ts +146 -0
  81. package/src/canvas/headless/svg-figure.test.ts +49 -0
  82. package/src/canvas/headless/svg-figure.ts +176 -0
  83. package/src/canvas/headless/table-layout.test.ts +158 -0
  84. package/src/canvas/headless/table-layout.ts +309 -0
  85. package/src/canvas/index.ts +35 -0
  86. package/src/canvas/lod/lod-mode.svelte.ts +30 -0
  87. package/src/canvas/lod/lod-switch.svelte +140 -0
  88. package/src/canvas/lod/render-all.svelte.ts +52 -0
  89. package/src/canvas/ops/arrange.ts +108 -0
  90. package/src/canvas/ops/ops.test.ts +98 -0
  91. package/src/canvas/ops/ops.ts +347 -0
  92. package/src/canvas/pen/PenField.svelte +128 -0
  93. package/src/canvas/pen/PenField.test.ts +87 -0
  94. package/src/canvas/pen/PenOverlay.svelte +172 -0
  95. package/src/canvas/pen/ShapeNode.svelte +64 -0
  96. package/src/canvas/pen/TextNode.svelte +97 -0
  97. package/src/canvas/pen/colors.ts +28 -0
  98. package/src/canvas/pen/eraser.ts +46 -0
  99. package/src/canvas/pen/geometry.ts +148 -0
  100. package/src/canvas/pen/host-field.test.svelte +13 -0
  101. package/src/canvas/pen/index.ts +40 -0
  102. package/src/canvas/pen/ops.ts +16 -0
  103. package/src/canvas/pen/pen-session.ts +177 -0
  104. package/src/canvas/pen/pen.css +149 -0
  105. package/src/canvas/pen/plugin.ts +139 -0
  106. package/src/canvas/pen/shapes.ts +104 -0
  107. package/src/canvas/pen/stroke-path.ts +54 -0
  108. package/src/canvas/pen/types.ts +96 -0
  109. package/src/canvas/pen/ui.ts +26 -0
  110. package/src/canvas/plugin.test.ts +45 -0
  111. package/src/canvas/plugin.ts +159 -0
  112. package/src/canvas/screen/GlyphIcon.svelte +25 -0
  113. package/src/canvas/screen/PartView.svelte +176 -0
  114. package/src/canvas/screen/ScreenBody.svelte +142 -0
  115. package/src/canvas/screen/ScreenCard.svelte +32 -0
  116. package/src/canvas/screen/glyphs/button.svg +1 -0
  117. package/src/canvas/screen/glyphs/close.svg +1 -0
  118. package/src/canvas/screen/glyphs/down.svg +1 -0
  119. package/src/canvas/screen/glyphs/header.svg +1 -0
  120. package/src/canvas/screen/glyphs/hills.svg +1 -0
  121. package/src/canvas/screen/glyphs/image.svg +1 -0
  122. package/src/canvas/screen/glyphs/input.svg +1 -0
  123. package/src/canvas/screen/glyphs/list.svg +1 -0
  124. package/src/canvas/screen/glyphs/modal.svg +1 -0
  125. package/src/canvas/screen/glyphs/plus.svg +1 -0
  126. package/src/canvas/screen/glyphs/up.svg +1 -0
  127. package/src/canvas/screen/glyphs.ts +40 -0
  128. package/src/canvas/screen/labels.ts +41 -0
  129. package/src/canvas/screen/parts.ts +164 -0
  130. package/src/canvas/screen/plugin.ts +67 -0
  131. package/src/canvas/screen/screen.css +593 -0
  132. package/src/canvas/screen/types.ts +47 -0
  133. package/src/canvas/screen/ui.ts +13 -0
  134. package/src/canvas/test-utils.ts +18 -0
  135. package/src/canvas/types.ts +53 -0
  136. package/src/canvas/ui/CanvasToolbar.svelte +169 -0
  137. package/src/canvas/ui/FrameLayer.svelte +116 -0
  138. package/src/canvas/ui/PenLayer.svelte +39 -0
  139. package/src/canvas/ui/StylePanel.svelte +268 -0
  140. package/src/canvas/ui/canvas-grid.svelte +156 -0
  141. package/src/canvas/ui/canvas-ui.css +350 -0
  142. package/src/canvas/ui/forward-wheel.ts +29 -0
  143. package/src/canvas/ui/icons.ts +124 -0
  144. package/src/canvas/ui/place-panel.ts +80 -0
  145. package/src/canvas/ui/shortcut-tip.test.ts +16 -0
  146. package/src/canvas/ui/shortcut-tip.ts +17 -0
  147. package/src/canvas/ui/tools.ts +146 -0
  148. package/src/charts/README.md +104 -0
  149. package/src/charts/ambient.d.ts +4 -0
  150. package/src/charts/animated-value.css +7 -0
  151. package/src/charts/animated-value.svelte +110 -0
  152. package/src/charts/area-chart.css +29 -0
  153. package/src/charts/area-chart.svelte +204 -0
  154. package/src/charts/bar-chart.css +119 -0
  155. package/src/charts/bar-chart.svelte +275 -0
  156. package/src/charts/charts.css +201 -0
  157. package/src/charts/crosshair.svelte +35 -0
  158. package/src/charts/dashboard/chart.ts +5 -0
  159. package/src/charts/dashboard/dashboard-empty.svelte +55 -0
  160. package/src/charts/dashboard/dashboard-section.svelte +58 -0
  161. package/src/charts/dashboard/data-panel.svelte +81 -0
  162. package/src/charts/dashboard/data-sort-head.svelte +102 -0
  163. package/src/charts/dashboard/data-table.svelte +278 -0
  164. package/src/charts/dashboard/data-table.ts +26 -0
  165. package/src/charts/dashboard/index.ts +25 -0
  166. package/src/charts/dashboard/panel-grid.svelte +43 -0
  167. package/src/charts/dashboard/period-filter.svelte +53 -0
  168. package/src/charts/dashboard/rating-chip.svelte +35 -0
  169. package/src/charts/dashboard/stat-band.svelte +47 -0
  170. package/src/charts/dashboard/stat-tile.svelte +176 -0
  171. package/src/charts/dashboard/windows.ts +8 -0
  172. package/src/charts/donut-chart.css +56 -0
  173. package/src/charts/donut-chart.svelte +158 -0
  174. package/src/charts/edge-fade.svelte +43 -0
  175. package/src/charts/edge-fade.ts +16 -0
  176. package/src/charts/figure.css +100 -0
  177. package/src/charts/figure.svelte +122 -0
  178. package/src/charts/gauge-chart.css +94 -0
  179. package/src/charts/gauge-chart.svelte +302 -0
  180. package/src/charts/hover-band.test.ts +30 -0
  181. package/src/charts/hover-band.ts +21 -0
  182. package/src/charts/hover-focus.svelte +85 -0
  183. package/src/charts/index.ts +31 -0
  184. package/src/charts/legend.css +136 -0
  185. package/src/charts/legend.svelte +112 -0
  186. package/src/charts/line-chart.css +26 -0
  187. package/src/charts/line-chart.svelte +256 -0
  188. package/src/charts/motion.ts +19 -0
  189. package/src/charts/number-chart.css +179 -0
  190. package/src/charts/number-chart.svelte +187 -0
  191. package/src/charts/primitives/chart-container.svelte +57 -0
  192. package/src/charts/primitives/chart-style.svelte +36 -0
  193. package/src/charts/primitives/chart-tooltip.svelte +161 -0
  194. package/src/charts/primitives/chart-utils.ts +54 -0
  195. package/src/charts/primitives/index.ts +11 -0
  196. package/src/charts/reference-line.css +18 -0
  197. package/src/charts/reference-line.svelte +31 -0
  198. package/src/charts/reveal.svelte +61 -0
  199. package/src/charts/ring-chart.css +104 -0
  200. package/src/charts/ring-chart.svelte +166 -0
  201. package/src/charts/sparkline.css +45 -0
  202. package/src/charts/sparkline.svelte +141 -0
  203. package/src/charts/stat-card.css +105 -0
  204. package/src/charts/stat-card.svelte +267 -0
  205. package/src/charts/time-series-chart.css +28 -0
  206. package/src/charts/time-series-chart.svelte +179 -0
  207. package/src/charts/trend-pill.css +38 -0
  208. package/src/charts/trend-pill.svelte +48 -0
  209. package/src/components/copy/copy-button.svelte +1 -1
  210. package/src/components/docs/doc-page.svelte +1 -1
  211. package/src/components/docs/on-this-page.svelte +2 -2
  212. package/src/components/list-page/empty-state.svelte +1 -1
  213. package/src/components/list-page/filter-popover.svelte +1 -1
  214. package/src/components/list-page/section-label.svelte +1 -1
  215. package/src/components/segmented/segment.svelte +4 -4
  216. package/src/components/theme/theme-provider.svelte +6 -4
  217. package/src/components/toolbar/index.ts +5 -0
  218. package/src/components/toolbar/toolbar.svelte +168 -0
  219. package/src/components/toolbar/toolbar.test.ts +66 -0
  220. package/src/editor/README.md +58 -0
  221. package/src/editor/ambient.d.ts +1 -0
  222. package/src/editor/block-id.ts +77 -0
  223. package/src/editor/block-ops.test.ts +237 -0
  224. package/src/editor/block-ops.ts +157 -0
  225. package/src/editor/content.css +418 -0
  226. package/src/editor/editor-toolbar.svelte +303 -0
  227. package/src/editor/editor.svelte +462 -0
  228. package/src/editor/extensions.ts +145 -0
  229. package/src/editor/image-toolbar.svelte +123 -0
  230. package/src/editor/index.ts +17 -0
  231. package/src/editor/inline-editor.svelte +261 -0
  232. package/src/editor/markdown-escaping.test.ts +158 -0
  233. package/src/editor/markdown-marks.test.ts +89 -0
  234. package/src/editor/markdown-test-helpers.ts +13 -0
  235. package/src/editor/markdown-youtube.test.ts +93 -0
  236. package/src/editor/markdown.test.ts +250 -0
  237. package/src/editor/markdown.ts +511 -0
  238. package/src/editor/renderer.svelte +92 -0
  239. package/src/editor/serializer.test.ts +133 -0
  240. package/src/editor/serializer.ts +370 -0
  241. package/src/editor/slash-items.ts +115 -0
  242. package/src/editor/slash-menu.svelte +159 -0
  243. package/src/editor/youtube-url.ts +35 -0
  244. package/src/effects/interactive-hover-button/InteractiveHoverButton.svelte +13 -13
  245. package/src/lib/result.ts +19 -0
  246. package/src/onboarding/README.md +120 -0
  247. package/src/onboarding/define-tours.test.ts +50 -0
  248. package/src/onboarding/define-tours.ts +31 -0
  249. package/src/onboarding/demo.svelte +86 -0
  250. package/src/onboarding/index.ts +12 -0
  251. package/src/onboarding/onboarding-provider.svelte +120 -0
  252. package/src/onboarding/onboarding.svelte.ts +24 -0
  253. package/src/onboarding/pick.test.ts +79 -0
  254. package/src/onboarding/pick.ts +36 -0
  255. package/src/onboarding/stores.test.ts +103 -0
  256. package/src/onboarding/stores.ts +58 -0
  257. package/src/onboarding/tour-card.svelte +70 -0
  258. package/src/onboarding/tour-runner.svelte +197 -0
  259. package/src/onboarding/types.ts +34 -0
  260. package/src/onboarding/wait.ts +29 -0
  261. package/src/shadcn/command/command-input.svelte +1 -1
  262. package/src/slides/Controls.svelte +138 -0
  263. package/src/slides/Deck.svelte +271 -0
  264. package/src/slides/Fragment.svelte +71 -0
  265. package/src/slides/Overview.svelte +113 -0
  266. package/src/slides/Presenter.svelte +292 -0
  267. package/src/slides/README.md +89 -0
  268. package/src/slides/Slide.svelte +179 -0
  269. package/src/slides/SlideList.svelte +19 -0
  270. package/src/slides/ambient.d.ts +1 -0
  271. package/src/slides/bridges.ts +69 -0
  272. package/src/slides/deck-controller.svelte.ts +292 -0
  273. package/src/slides/demo-slides.svelte +358 -0
  274. package/src/slides/demo.svelte +176 -0
  275. package/src/slides/export/capture.ts +113 -0
  276. package/src/slides/export/ffmpeg.ts +120 -0
  277. package/src/slides/export/index.ts +9 -0
  278. package/src/slides/export/launch.ts +10 -0
  279. package/src/slides/export/pdf.ts +152 -0
  280. package/src/slides/export/types.ts +48 -0
  281. package/src/slides/index.ts +19 -0
  282. package/src/slides/labels.ts +48 -0
  283. package/src/slides/layout.ts +51 -0
  284. package/src/slides/model.ts +47 -0
  285. package/src/slides/navigation.ts +100 -0
  286. package/src/slides/theme.css +205 -0
  287. package/src/slides/types.ts +86 -0
  288. package/src/tasks/README.md +312 -0
  289. package/src/tasks/ambient.d.ts +3 -0
  290. package/src/tasks/board-order.ts +137 -0
  291. package/src/tasks/dates.ts +31 -0
  292. package/src/tasks/define-statuses.ts +32 -0
  293. package/src/tasks/demo.svelte +389 -0
  294. package/src/tasks/detail/detail-demo.svelte +186 -0
  295. package/src/tasks/detail/detail-types.ts +54 -0
  296. package/src/tasks/detail/task-chip.svelte +28 -0
  297. package/src/tasks/detail/task-detail.svelte +315 -0
  298. package/src/tasks/detail/task-property.svelte +17 -0
  299. package/src/tasks/detail/task-thread.svelte +258 -0
  300. package/src/tasks/gantt/gantt-bar.svelte +243 -0
  301. package/src/tasks/gantt/gantt-grid.svelte +94 -0
  302. package/src/tasks/gantt/gantt-group-row.svelte +55 -0
  303. package/src/tasks/gantt/gantt-header.svelte +71 -0
  304. package/src/tasks/gantt/gantt-task-row.svelte +186 -0
  305. package/src/tasks/gantt/gantt.css +22 -0
  306. package/src/tasks/gantt/gantt.test.ts +77 -0
  307. package/src/tasks/gantt/logic.test.ts +163 -0
  308. package/src/tasks/gantt/logic.ts +248 -0
  309. package/src/tasks/inbox/inbox-card.svelte +283 -0
  310. package/src/tasks/inbox/inbox-draft.svelte +84 -0
  311. package/src/tasks/inbox/inbox-logic.ts +55 -0
  312. package/src/tasks/inbox/inbox-statuses.ts +48 -0
  313. package/src/tasks/inbox/inbox-trace.svelte +45 -0
  314. package/src/tasks/inbox/inbox-types.ts +108 -0
  315. package/src/tasks/inbox/inbox.svelte +203 -0
  316. package/src/tasks/inbox/index.ts +23 -0
  317. package/src/tasks/index.ts +85 -0
  318. package/src/tasks/kanban-board.svelte +561 -0
  319. package/src/tasks/kanban-column.svelte +117 -0
  320. package/src/tasks/kanban-drag.ts +38 -0
  321. package/src/tasks/kanban-types.ts +65 -0
  322. package/src/tasks/presets/sales.ts +16 -0
  323. package/src/tasks/record/demo-data.ts +286 -0
  324. package/src/tasks/record/field-section.svelte +53 -0
  325. package/src/tasks/record/fields.ts +142 -0
  326. package/src/tasks/record/format.ts +87 -0
  327. package/src/tasks/record/gutter.ts +2 -0
  328. package/src/tasks/record/index.ts +54 -0
  329. package/src/tasks/record/info-list.svelte +316 -0
  330. package/src/tasks/record/kinds.ts +42 -0
  331. package/src/tasks/record/logic.ts +324 -0
  332. package/src/tasks/record/next-up.svelte +70 -0
  333. package/src/tasks/record/project-list.svelte +133 -0
  334. package/src/tasks/record/record-demo.svelte +324 -0
  335. package/src/tasks/record/record-header.svelte +70 -0
  336. package/src/tasks/record/record-shell.svelte +138 -0
  337. package/src/tasks/record/record-stats.svelte +48 -0
  338. package/src/tasks/record/record-timeline.svelte +231 -0
  339. package/src/tasks/record/record.test.ts +350 -0
  340. package/src/tasks/record/steps-rail.svelte +474 -0
  341. package/src/tasks/record/types.ts +280 -0
  342. package/src/tasks/record/where-we-are.svelte +121 -0
  343. package/src/tasks/reorder.ts +19 -0
  344. package/src/tasks/side-panel-types.ts +16 -0
  345. package/src/tasks/side-panel.svelte +193 -0
  346. package/src/tasks/status-badge.svelte +63 -0
  347. package/src/tasks/status-dot.svelte +228 -0
  348. package/src/tasks/status-icon.svelte +30 -0
  349. package/src/tasks/status-types.ts +40 -0
  350. package/src/tasks/task-gantt.svelte +230 -0
  351. package/src/tasks/task-list.svelte +470 -0
  352. package/src/tasks/task-types.ts +33 -0
  353. package/src/tasks/time/duration.ts +69 -0
  354. package/src/tasks/time/time-chip.svelte +62 -0
  355. package/src/tasks/time/time-dot.svelte +58 -0
  356. package/src/tasks/time/time-ring.svelte +74 -0
  357. package/src/tasks/time/time-tracker.svelte +565 -0
  358. package/src/tasks/time/time-types.ts +74 -0
  359. package/src/tasks/use-kanban-dnd.svelte.ts +173 -0
  360. package/src/tokens/base.css +5 -5
  361. package/src/tokens/palette.css +115 -120
@@ -0,0 +1,236 @@
1
+ # @heyramzi/ui/canvas
2
+
3
+ A canvas for Svelte 5 and SvelteKit, built on [@xyflow/svelte](https://svelteflow.dev). Cards and arrows from xyflow, a hand-drawing pen on top, and a plugin API for your own card types.
4
+
5
+ Strokes, text and shapes are ordinary xyflow nodes. So undo, copy and save work with no extra code. The same ops run on a server or in a CLI, with no browser.
6
+
7
+ MIT. It runs on any domain and costs nothing per install.
8
+
9
+ ## What's in it
10
+
11
+ - **Pen layer.** Freehand strokes (perfect-freehand), a highlighter, an eraser, plain text, and rectangle, ellipse and diamond shapes. Colours are token names, so a board drawn in light mode reads fine in dark.
12
+ - **Tool bar.** A floating bar with the tools, undo and redo. Pick from 6 colours and 3 brush sizes. Keys: V, H, P, M, E, T, R, O, D.
13
+ - **Plugin API.** Register your own card types, ops and layout passes. One registry builds xyflow's `nodeTypes` for you.
14
+ - **Ops.** 8 core ops (`addNode`, `removeNode`, `patchNode`, `moveNode`, `setParent`, `addEdge`, `removeEdge`, `patchEdge`) as plain functions over nodes and edges. Zod checks every one.
15
+ - **Undo history.** `createCanvasDispatcher` wraps each edit in a transaction. A whole pen stroke is one undo step.
16
+ - **Headless.** Everything that doesn't need a DOM, for servers and CLIs.
17
+
18
+ ## Install
19
+
20
+ ```sh
21
+ npm install @heyramzi/ui @xyflow/svelte svelte
22
+ ```
23
+
24
+ Peers: `svelte` ^5.57.2 and `@xyflow/svelte` ^1.7.0. The canvas is the one subpath whose peer you add yourself.
25
+
26
+ The package ships Svelte source, so your app needs `@sveltejs/vite-plugin-svelte`. A SvelteKit app has it already.
27
+
28
+ ## Use it in SvelteKit
29
+
30
+ Drop this in `src/routes/+page.svelte`.
31
+
32
+ ```svelte
33
+ <script lang="ts">
34
+ import { Background, SvelteFlow, type Edge, type Node } from "@xyflow/svelte";
35
+ import "@xyflow/svelte/dist/style.css";
36
+ import { createCanvasDispatcher, createRegistry } from "@heyramzi/ui/canvas";
37
+ import { penUiPlugin } from "@heyramzi/ui/canvas/pen/ui.js";
38
+ import type { PenColor } from "@heyramzi/ui/canvas/pen/index.js";
39
+ import {
40
+ brushSize,
41
+ DEFAULT_BRUSH_LEVEL,
42
+ isDrawingTool,
43
+ type CanvasTool,
44
+ } from "@heyramzi/ui/canvas/ui/tools.js";
45
+ import CanvasToolbar from "@heyramzi/ui/canvas/ui/CanvasToolbar.svelte";
46
+ import PenLayer from "@heyramzi/ui/canvas/ui/PenLayer.svelte";
47
+
48
+ const registry = createRegistry([penUiPlugin]);
49
+
50
+ let nodes = $state.raw<Node[]>([]);
51
+ let edges = $state.raw<Edge[]>([]);
52
+ let tool = $state<CanvasTool>("select");
53
+ let color = $state<PenColor>("ink");
54
+ let level = $state(DEFAULT_BRUSH_LEVEL);
55
+
56
+ const history = createCanvasDispatcher({
57
+ getState: () => ({ nodes, edges }),
58
+ setState: (next) => {
59
+ nodes = next.nodes;
60
+ edges = next.edges;
61
+ },
62
+ // Save here. It fires once per commit, undo and redo.
63
+ onMutated: () => {},
64
+ opDefinitions: [...registry.ops.values()],
65
+ });
66
+
67
+ const labels = {
68
+ toolbar: "Canvas tools",
69
+ style: "Style",
70
+ tools: {
71
+ select: "Select", hand: "Hand", pen: "Pen", highlighter: "Highlighter",
72
+ eraser: "Eraser", text: "Text", rectangle: "Rectangle", ellipse: "Ellipse",
73
+ diamond: "Diamond", shapes: "Shapes",
74
+ },
75
+ colors: { ink: "Ink", red: "Red", blue: "Blue", green: "Green", amber: "Amber", violet: "Violet" },
76
+ sizes: ["Small", "Medium", "Large"],
77
+ undo: "Undo",
78
+ redo: "Redo",
79
+ } as const;
80
+ </script>
81
+
82
+ <div style="height: 100vh">
83
+ <SvelteFlow bind:nodes bind:edges nodeTypes={registry.nodeTypes()} fitView>
84
+ <Background />
85
+ {#if isDrawingTool(tool)}
86
+ <PenLayer
87
+ {tool}
88
+ {color}
89
+ size={brushSize(tool, level)}
90
+ {nodes}
91
+ commit={(ops) => history.dispatch(ops)}
92
+ startGesture={() => {
93
+ history.begin();
94
+ return history.commit;
95
+ }}
96
+ />
97
+ {/if}
98
+ <CanvasToolbar
99
+ bind:tool
100
+ bind:color
101
+ bind:level
102
+ {labels}
103
+ canUndo={history.canUndo}
104
+ canRedo={history.canRedo}
105
+ onUndo={history.undo}
106
+ onRedo={history.redo}
107
+ />
108
+ </SvelteFlow>
109
+ </div>
110
+ ```
111
+
112
+ Pick the pen tool, drag, and you've drawn a stroke. Press undo and it's gone. The whole gesture is one step.
113
+
114
+ `PenLayer` and `CanvasToolbar` both read xyflow's store, so they go inside `<SvelteFlow>`. The tool bar takes a `labels` object because the package has no i18n. Pass your own strings.
115
+
116
+ Components import by path:
117
+
118
+ - `@heyramzi/ui/canvas/ui/CanvasToolbar.svelte`
119
+ - `@heyramzi/ui/canvas/ui/PenLayer.svelte`
120
+ - `@heyramzi/ui/canvas/ui/canvas-grid.svelte`, a dot grid that fades as you zoom out
121
+
122
+ The tool bar's styles read your theme tokens (`--card`, `--border`, `--foreground`, `--ring`) and fall back to plain colours when you have none.
123
+
124
+ ## The plugin API
125
+
126
+ A plugin is a name plus node kinds, ops and layout passes. Write it once for the server, then add the Svelte component for the browser. That's how the pen does it: `penPlugin` has no DOM, and `penUiPlugin` adds the renderers.
127
+
128
+ ```ts
129
+ // invoice-plugin.ts, safe on a server
130
+ import { z } from "zod";
131
+ import { definePlugin, patchData, type OpState } from "@heyramzi/ui/canvas";
132
+
133
+ const setTotal = z.object({
134
+ kind: z.literal("setTotal"),
135
+ nodeId: z.string().min(1),
136
+ total: z.number(),
137
+ });
138
+
139
+ function applySetTotal(state: OpState, op: z.infer<typeof setTotal>, opIndex: number) {
140
+ patchData(state, op.nodeId, opIndex, () => ({ total: op.total }));
141
+ }
142
+
143
+ export const invoicePlugin = definePlugin({
144
+ id: "invoice",
145
+ nodes: [
146
+ {
147
+ type: "invoice",
148
+ label: "Invoice",
149
+ size: { width: 240 },
150
+ palette: { group: "Billing", order: 1 },
151
+ createData: () => ({ total: 0 }),
152
+ },
153
+ ],
154
+ ops: [
155
+ { kind: "setTotal", schema: setTotal, apply: applySetTotal },
156
+ ],
157
+ });
158
+ ```
159
+
160
+ ```ts
161
+ // invoice-ui.ts, browser only
162
+ import { definePlugin } from "@heyramzi/ui/canvas";
163
+ import InvoiceCard from "./InvoiceCard.svelte";
164
+ import { invoicePlugin } from "./invoice-plugin";
165
+
166
+ export const invoiceUiPlugin = definePlugin({
167
+ ...invoicePlugin,
168
+ nodes: (invoicePlugin.nodes ?? []).map((kind) => ({ ...kind, component: InvoiceCard })),
169
+ });
170
+ ```
171
+
172
+ Then `createRegistry([penUiPlugin, invoiceUiPlugin])` gives you:
173
+
174
+ - `nodeTypes()`, the map xyflow wants.
175
+ - `ops`, to pass as `opDefinitions` to the dispatcher or `applyOps`.
176
+ - `parseOp(input)`, which checks one op against its kind's schema.
177
+ - `pickLayout(data)`, the first layout pass whose `matches` says yes.
178
+ - `contentSizedTypes()` and `layoutOwnedTypes()`, so a layout knows which heights the DOM owns.
179
+
180
+ A node type or op kind can have only one owner. If a second plugin claims one, `createRegistry` throws and names both.
181
+
182
+ A node kind can also set `validParents` (which containers it may sit in) and `sizing`. A layout pass is `{ id, matches, run }`, and `run` returns the new nodes and edges.
183
+
184
+ ## The pen
185
+
186
+ A stroke is a node with `type: "stroke"`. Its `data` holds the simplified points, a colour token, a size and a tool. Text is `"text"` and a shape is `"shape"`. They're real nodes, so the eraser, selection, copy and your save code treat them like cards.
187
+
188
+ The pen is `PenOverlay` (or `PenLayer` inside xyflow), and it needs 2 callbacks from you:
189
+
190
+ - `commit(ops)` receives `addNode` and `removeNode` ops. Send them to `history.dispatch`.
191
+ - `startGesture()` opens an undo transaction and returns the function that closes it. A stroke is a single history step.
192
+
193
+ The eraser removes whole strokes, never part of one. A swipe across a big shape doesn't wipe it.
194
+
195
+ The pen has 6 colours: `ink`, `red`, `blue`, `green`, `amber` and `violet`. `pen.css` maps them to CSS variables with a light and a dark value. Dark mode applies under a `.dark` class on an ancestor or on `.svelte-flow`.
196
+
197
+ ## Headless ops
198
+
199
+ Import from `@heyramzi/ui/canvas` on a server, in a worker or in a CLI. The root export touches no DOM.
200
+
201
+ ```ts
202
+ import { applyOps, createRegistry, type RawCanvasData } from "@heyramzi/ui/canvas";
203
+ import { penPlugin } from "@heyramzi/ui/canvas/pen/index.js";
204
+ import { compilePenMark, penMarkSchema } from "@heyramzi/ui/canvas/headless/index.js";
205
+
206
+ const registry = createRegistry([penPlugin]);
207
+
208
+ let board: RawCanvasData = { nodes: [], edges: [] };
209
+
210
+ // Ops from a request body or a CLI flag. An unknown kind or a bad shape throws.
211
+ const ops = [
212
+ { kind: "addShape", shape: "rectangle", box: { x: 0, y: 0, width: 200, height: 120 }, label: "Intake" },
213
+ { kind: "addText", at: { x: 20, y: 160 }, text: "Where leads land" },
214
+ ].map((op) => registry.parseOp(op));
215
+
216
+ board = applyOps(board, ops, [...registry.ops.values()]);
217
+ ```
218
+
219
+ `applyOps` is pure. It returns a new board and never edits the one you pass. A bad op throws a `CanvasOpError` with the index of the op that failed, so you know which one in a list of 57.
220
+
221
+ For a whole drawing in one call, write pen marks as JSON. There are 3 kinds: `stroke`, `text` and `shape`. They compile to the same nodes the pen tool draws.
222
+
223
+ ```ts
224
+ const marks = penMarkSchema.array().parse(JSON.parse(input));
225
+ const frames = new Map(); // frame id to { x, y }, if a mark uses `in`
226
+ const nodes = marks.map((mark, i) => compilePenMark(mark, i, frames));
227
+ board = { ...board, nodes: [...board.nodes, ...nodes] };
228
+ ```
229
+
230
+ A mark with no `id` gets `pen-<n>` from its place in the list. Run the same file twice and you get the same ids.
231
+
232
+ The root export also has the pure helpers the editor uses: edge anchors, alignment, snap guides, fit-to-viewport, table layout, SVG figure sanitising and the sketch (rough.js) geometry. Import what you need.
233
+
234
+ ## Develop
235
+
236
+ Tests run in jsdom, so no browser. From the repo root, `pnpm --filter @heyramzi/ui test` runs them and `pnpm --filter @heyramzi/ui check` typechecks.
@@ -0,0 +1,61 @@
1
+ <script lang="ts">
2
+ // =========================
3
+ // IMPORTS
4
+ // =========================
5
+ import type { Snippet } from "svelte";
6
+ import { getDiagramEditorContext } from "../editor/editor-context.js";
7
+ import { isLocked } from "../editor/lock.js";
8
+ import { userHeightOf, type CardResize } from "./resize.js";
9
+ import ResizeHandles from "./resize-handles.svelte";
10
+ import "./card.css";
11
+
12
+ // The frame every card sits in: it keeps the saved height as a floor and draws the grips
13
+ // on a selected card. A card whose policy is `false` has neither.
14
+
15
+ // =========================
16
+ // TYPES
17
+ // =========================
18
+ interface Props {
19
+ id: string;
20
+ data?: unknown;
21
+ selected?: boolean;
22
+ /** How this card type resizes, or false when its size isn't the person's to set. */
23
+ resize: CardResize | false;
24
+ children: Snippet;
25
+ }
26
+
27
+ // =========================
28
+ // PROPS
29
+ // =========================
30
+ let { id, data, selected = false, resize, children }: Props = $props();
31
+
32
+ // =========================
33
+ // STATE
34
+ // =========================
35
+ const editor = getDiagramEditorContext();
36
+ let frame = $state<HTMLElement>();
37
+
38
+ // =========================
39
+ // DERIVED
40
+ // =========================
41
+ let floor = $derived(userHeightOf(data));
42
+ // A box card is sized by the node itself, so it needs no frame.
43
+ let framed = $derived(!!resize && resize.store !== "box");
44
+ let grips = $derived(!!resize && !!editor && selected && !isLocked({ data }));
45
+ </script>
46
+
47
+ <!-- =========================== -->
48
+ <!-- MARKUP -->
49
+ <!-- =========================== -->
50
+ <!-- The grips come first, so a link dot on the same edge stays on top of them. -->
51
+ {#if resize && grips}
52
+ <ResizeHandles {id} {resize} {frame} />
53
+ {/if}
54
+
55
+ {#if framed}
56
+ <div class="cv-card-frame" bind:this={frame} style:--cv-card-h={floor > 0 ? `${floor}px` : undefined}>
57
+ {@render children()}
58
+ </div>
59
+ {:else}
60
+ {@render children()}
61
+ {/if}
@@ -0,0 +1,39 @@
1
+ /* The card frame and its resize hit areas. Handle looks come from chrome.css. */
2
+
3
+ /* A one-row grid: at least the saved height, growing with the content.
4
+ `min-height: 100%` shows a live drag at once. No position, so grips still anchor to the node. */
5
+ .cv-card-frame {
6
+ display: grid;
7
+ grid-template-rows: minmax(var(--cv-card-h, 0px), auto);
8
+ min-height: 100%;
9
+ }
10
+
11
+ /* A drag drops the saved floor, so the card can shrink as well as grow. */
12
+ .cv-card-frame[data-resizing] {
13
+ grid-template-rows: auto;
14
+ }
15
+
16
+ /* The edges are hit areas only, a finger wide at every zoom. */
17
+ .svelte-flow .svelte-flow__resize-control.line.cv-edge {
18
+ border-width: 0;
19
+ background: transparent;
20
+ }
21
+
22
+ .svelte-flow .svelte-flow__resize-control.line.cv-edge:is(.left, .right) {
23
+ width: calc(10px * var(--canvas-unzoom, 1));
24
+ }
25
+
26
+ .svelte-flow .svelte-flow__resize-control.line.cv-edge:is(.top, .bottom) {
27
+ height: calc(10px * var(--canvas-unzoom, 1));
28
+ }
29
+
30
+ /* A text card's side grips are sideways-moving pills near the top of the edge, since the middle belongs to the connection dot. */
31
+ .svelte-flow .svelte-flow__resize-control.handle.cv-grip-x {
32
+ top: calc(22px * var(--canvas-unzoom, 1)) !important;
33
+ /* Above the card's own positioned content, or half the pill hides behind it. */
34
+ z-index: 3;
35
+ width: 8px;
36
+ height: 20px;
37
+ border-radius: 9999px;
38
+ cursor: ew-resize;
39
+ }
@@ -0,0 +1,119 @@
1
+ <script lang="ts">
2
+ // =========================
3
+ // IMPORTS
4
+ // =========================
5
+ import { NodeResizeControl, ResizeControlVariant, useSvelteFlow } from "@xyflow/svelte";
6
+ import { getDiagramEditorContext } from "../editor/editor-context.js";
7
+ import {
8
+ DEFAULT_MAX_WIDTH,
9
+ DEFAULT_MIN_HEIGHT,
10
+ DEFAULT_MIN_WIDTH,
11
+ keptHeight,
12
+ type CardResize,
13
+ } from "./resize.js";
14
+ import "./card.css";
15
+
16
+ // The grips of a selected card. One drag is one undo step: width, kept height and extra data close together.
17
+
18
+ // =========================
19
+ // TYPES
20
+ // =========================
21
+ interface Props {
22
+ id: string;
23
+ resize: CardResize;
24
+ /** The framed element, read for the height its content needs. */
25
+ frame?: HTMLElement;
26
+ }
27
+
28
+ // =========================
29
+ // CONSTANTS
30
+ // =========================
31
+ const CORNERS = ["top-left", "top-right", "bottom-left", "bottom-right"] as const;
32
+ const EDGES = ["left", "right", "top", "bottom"] as const;
33
+ const SIDES = ["left", "right"] as const;
34
+
35
+ // =========================
36
+ // PROPS
37
+ // =========================
38
+ let { id, resize, frame }: Props = $props();
39
+
40
+ // =========================
41
+ // STATE
42
+ // =========================
43
+ const editor = getDiagramEditorContext();
44
+ const { updateNode } = useSvelteFlow();
45
+ let endGesture: (() => void) | null = null;
46
+ // The height the content needs, read when a drag starts.
47
+ let contentHeight = $state(DEFAULT_MIN_HEIGHT);
48
+
49
+ // =========================
50
+ // DERIVED
51
+ // =========================
52
+ let both = $derived(resize.axes === "both");
53
+ let ratio = $derived(resize.axes === "ratio");
54
+ let direction = $derived(both || ratio ? undefined : ("horizontal" as const));
55
+ let box = $derived(resize.store === "box");
56
+ let minWidth = $derived(resize.minWidth ?? DEFAULT_MIN_WIDTH);
57
+ let maxWidth = $derived(resize.maxWidth ?? DEFAULT_MAX_WIDTH);
58
+ let minHeight = $derived(both && !box ? contentHeight : (resize.minHeight ?? DEFAULT_MIN_HEIGHT));
59
+
60
+ // =========================
61
+ // FUNCTIONS
62
+ // =========================
63
+ function measureContent() {
64
+ if (!frame) return;
65
+ // With the saved floor dropped, the frame is as tall as the content needs.
66
+ frame.setAttribute("data-resizing", "");
67
+ contentHeight = Math.max(DEFAULT_MIN_HEIGHT, Math.ceil(frame.offsetHeight));
68
+ }
69
+
70
+ function start() {
71
+ endGesture = editor?.startGesture() ?? null;
72
+ if (both && !box) measureContent();
73
+ }
74
+
75
+ function finish(size: { width: number; height: number }) {
76
+ frame?.removeAttribute("data-resizing");
77
+ const width = Math.round(size.width);
78
+ if (!box) {
79
+ editor?.setNodeWidth(id, width);
80
+ const patch: Record<string, unknown> = { ...resize.extraData?.({ width, height: size.height }) };
81
+ // A text card keeps growing with its words, and an image's height follows its width.
82
+ if (both) patch.userHeight = keptHeight(size.height, contentHeight);
83
+ if (Object.keys(patch).length > 0) editor?.patchNodeData(id, patch);
84
+ // The drag gave the node a height; the content and the saved floor size it from here.
85
+ updateNode(id, { height: undefined });
86
+ }
87
+ endGesture?.();
88
+ endGesture = null;
89
+ }
90
+ </script>
91
+
92
+ <!-- =========================== -->
93
+ <!-- MARKUP -->
94
+ <!-- =========================== -->
95
+ {#snippet control(position: (typeof CORNERS)[number] | (typeof EDGES)[number], variant: ResizeControlVariant)}
96
+ <NodeResizeControl
97
+ nodeId={id}
98
+ {position}
99
+ {variant}
100
+ {minWidth}
101
+ {maxWidth}
102
+ {minHeight}
103
+ keepAspectRatio={ratio}
104
+ resizeDirection={direction}
105
+ class={variant === ResizeControlVariant.Line ? "cv-edge" : direction ? "cv-grip cv-grip-x" : "cv-grip"}
106
+ onResizeStart={start}
107
+ onResizeEnd={(_, size) => finish(size)}
108
+ />
109
+ {/snippet}
110
+
111
+ <!-- A text card moves only sideways, so it gets a grip on each side edge and no corners. -->
112
+ {#each both || ratio ? EDGES : SIDES as edge (edge)}
113
+ {#if !ratio}
114
+ {@render control(edge, ResizeControlVariant.Line)}
115
+ {/if}
116
+ {/each}
117
+ {#each direction ? SIDES : CORNERS as grip (grip)}
118
+ {@render control(grip, ResizeControlVariant.Handle)}
119
+ {/each}
@@ -0,0 +1,33 @@
1
+ // How a card resizes. The host names one policy per card type; the frame and the handles read it.
2
+
3
+ /** `both` saves `data.userHeight` as a floor. `width` is a text card. `ratio` keeps an image's shape. */
4
+ export type CardResizeAxes = "both" | "width" | "ratio";
5
+
6
+ /** Where the size lives. `content` cards size their own height; a `box` card has its size only on the node. */
7
+ export type CardResizeStore = "content" | "box";
8
+
9
+ export type CardResize = {
10
+ axes: CardResizeAxes;
11
+ /** Defaults to `content`. */
12
+ store?: CardResizeStore;
13
+ minWidth?: number;
14
+ maxWidth?: number;
15
+ minHeight?: number;
16
+ /** Extra data a finished resize writes with the size, in the same undo step. */
17
+ extraData?: (size: { width: number; height: number }) => Record<string, unknown>;
18
+ };
19
+
20
+ export const DEFAULT_MIN_WIDTH = 120;
21
+ export const DEFAULT_MAX_WIDTH = 2400;
22
+ export const DEFAULT_MIN_HEIGHT = 40;
23
+
24
+ /** The saved floor height of a card, or 0 when its content sets the height. */
25
+ export function userHeightOf(data: unknown): number {
26
+ const value = (data as { userHeight?: unknown } | null | undefined)?.userHeight;
27
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
28
+ }
29
+
30
+ /** The height a finished drag keeps: the dragged one, never under what the content needs. */
31
+ export function keptHeight(dragged: number, content: number): number {
32
+ return Math.max(Math.round(dragged), Math.ceil(content));
33
+ }
@@ -0,0 +1,120 @@
1
+ <script lang="ts">
2
+ // =========================
3
+ // IMPORTS
4
+ // =========================
5
+ import { useConnection, useStore } from "@xyflow/svelte";
6
+ import { untrack } from "svelte";
7
+
8
+ // A root `--canvas-unzoom` restyled every card per zoom frame. It lives only on nodes showing chrome: selected, hovered, or all while a link is dragged.
9
+
10
+ // =========================
11
+ // CONSTANTS
12
+ // =========================
13
+ const MIN_ZOOM = 0.05;
14
+ const NODE = ".svelte-flow__node";
15
+
16
+ // =========================
17
+ // STATE
18
+ // =========================
19
+ const store = useStore();
20
+ const connection = useConnection();
21
+
22
+ // Plain collections: they change on selection and hover, never per frame, and nothing reads them reactively.
23
+ const selected = new Set<HTMLElement>();
24
+ let hovered: HTMLElement | null = null;
25
+
26
+ // =========================
27
+ // DERIVED
28
+ // =========================
29
+ let unzoom = $derived(String(1 / Math.max(store.viewport.zoom, MIN_ZOOM)));
30
+ let linking = $derived(connection.current.inProgress);
31
+
32
+ // =========================
33
+ // FUNCTIONS
34
+ // =========================
35
+ const paint = (node: HTMLElement | null, value = unzoom) => node?.style.setProperty("--canvas-unzoom", value);
36
+
37
+ function paintShowing(value: string) {
38
+ for (const node of selected) {
39
+ if (node.isConnected) paint(node, value);
40
+ else selected.delete(node);
41
+ }
42
+ paint(hovered, value);
43
+ }
44
+
45
+ // Chrome that belongs to one card (grips, quick-add) hides while several are selected: see chrome.css.
46
+ function flagMulti() {
47
+ store.domNode?.toggleAttribute("data-cv-multi", selected.size > 1);
48
+ }
49
+
50
+ function track(node: HTMLElement) {
51
+ if (node.classList.contains("selected")) {
52
+ if (!selected.has(node)) {
53
+ selected.add(node);
54
+ paint(node);
55
+ }
56
+ } else selected.delete(node);
57
+ flagMulti();
58
+ }
59
+
60
+ // =========================
61
+ // EFFECTS
62
+ // =========================
63
+ $effect(() => {
64
+ paintShowing(unzoom);
65
+ });
66
+
67
+ $effect(() => {
68
+ const root = store.domNode;
69
+ if (!root) return;
70
+ if (linking) {
71
+ root.dataset.cvLinking = "";
72
+ const value = unzoom;
73
+ for (const node of root.querySelectorAll<HTMLElement>(NODE)) paint(node, value);
74
+ return () => delete root.dataset.cvLinking;
75
+ }
76
+ });
77
+
78
+ function watch(root: HTMLElement) {
79
+ const over = (event: PointerEvent) => {
80
+ const node = (event.target as Element | null)?.closest<HTMLElement>(NODE) ?? null;
81
+ if (node === hovered) return;
82
+ hovered = node;
83
+ paint(node);
84
+ };
85
+ root.addEventListener("pointerover", over, { passive: true });
86
+
87
+ // A node picks up or loses the `selected` class when the selection changes, and arrives selected when pasted or created.
88
+ for (const node of root.querySelectorAll<HTMLElement>(`${NODE}.selected`)) track(node);
89
+ const classes = new MutationObserver((records) => {
90
+ for (const record of records) {
91
+ const node = record.target as HTMLElement;
92
+ if (node.matches(NODE)) track(node);
93
+ }
94
+ });
95
+ classes.observe(root, { attributes: true, attributeFilter: ["class"], subtree: true });
96
+ const arrivals = new MutationObserver((records) => {
97
+ for (const record of records) {
98
+ for (const added of record.addedNodes) {
99
+ if (added instanceof HTMLElement && added.matches(NODE)) track(added);
100
+ }
101
+ }
102
+ });
103
+ const layer = root.querySelector(".svelte-flow__nodes");
104
+ if (layer) arrivals.observe(layer, { childList: true });
105
+
106
+ return () => {
107
+ root.removeEventListener("pointerover", over);
108
+ classes.disconnect();
109
+ arrivals.disconnect();
110
+ selected.clear();
111
+ root.removeAttribute("data-cv-multi");
112
+ };
113
+ }
114
+
115
+ $effect(() => {
116
+ const root = store.domNode;
117
+ if (!root) return;
118
+ return untrack(() => watch(root));
119
+ });
120
+ </script>