@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,76 @@
1
+ <script lang="ts">
2
+ // =========================
3
+ // IMPORTS
4
+ // =========================
5
+ import { useStore } from "@xyflow/svelte";
6
+
7
+ // The grab dot on a selected elbow, one size on screen. Only the edge showing it reads the zoom; a root value restyles every card per frame.
8
+
9
+ // =========================
10
+ // CONSTANTS
11
+ // =========================
12
+ const MIN_ZOOM = 0.05;
13
+
14
+ // =========================
15
+ // TYPES
16
+ // =========================
17
+ interface Props {
18
+ /** The way the run moves: x slides it sideways, y up and down. */
19
+ axis: "x" | "y";
20
+ title: string;
21
+ onpointerdown: (event: PointerEvent) => void;
22
+ ondblclick: () => void;
23
+ }
24
+
25
+ // =========================
26
+ // PROPS
27
+ // =========================
28
+ let { axis, title, onpointerdown, ondblclick }: Props = $props();
29
+
30
+ // =========================
31
+ // STATE
32
+ // =========================
33
+ const store = useStore();
34
+
35
+ // =========================
36
+ // DERIVED
37
+ // =========================
38
+ let unzoom = $derived(1 / Math.max(store.viewport.zoom, MIN_ZOOM));
39
+ </script>
40
+
41
+ <!-- =========================== -->
42
+ <!-- MARKUP -->
43
+ <!-- =========================== -->
44
+ <button
45
+ type="button"
46
+ class="cv-bend-handle"
47
+ class:cv-bend-x={axis === "x"}
48
+ data-edge-bend
49
+ {title}
50
+ aria-label={title}
51
+ style:scale={unzoom}
52
+ {onpointerdown}
53
+ {ondblclick}
54
+ ></button>
55
+
56
+ <!-- =========================== -->
57
+ <!-- STYLES -->
58
+ <!-- =========================== -->
59
+ <style>
60
+ .cv-bend-handle {
61
+ display: block;
62
+ box-sizing: border-box;
63
+ width: 12px;
64
+ height: 12px;
65
+ padding: 0;
66
+ border: 2px solid var(--cv-chrome-surface, var(--background, #fff));
67
+ border-radius: 9999px;
68
+ background: var(--cv-chrome-ring, var(--ring, #4a4ad8));
69
+ cursor: row-resize;
70
+ touch-action: none;
71
+ }
72
+
73
+ .cv-bend-x {
74
+ cursor: col-resize;
75
+ }
76
+ </style>
@@ -0,0 +1,30 @@
1
+ <svelte:options namespace="svg" />
2
+
3
+ <script lang="ts">
4
+ interface Props {
5
+ id: string;
6
+ from: { x: number; y: number };
7
+ to: { x: number; y: number };
8
+ fromColor: string;
9
+ toColor: string;
10
+ }
11
+
12
+ let { id, from, to, fromColor, toColor }: Props = $props();
13
+
14
+ // A zero-length gradient paints nothing, so a link drawn onto itself keeps one pixel of run.
15
+ let flat = $derived(Math.abs(from.x - to.x) < 0.5 && Math.abs(from.y - to.y) < 0.5);
16
+ </script>
17
+
18
+ <defs>
19
+ <linearGradient
20
+ {id}
21
+ gradientUnits="userSpaceOnUse"
22
+ x1={from.x}
23
+ y1={from.y}
24
+ x2={flat ? from.x : to.x}
25
+ y2={flat ? from.y + 1 : to.y}
26
+ >
27
+ <stop stop-color={fromColor} />
28
+ <stop offset="1" stop-color={toColor} />
29
+ </linearGradient>
30
+ </defs>
@@ -0,0 +1,58 @@
1
+ import { describe, expect, it } from "vite-plus/test";
2
+ import { Position } from "@xyflow/svelte";
3
+ import { arrowTips, routeEdgePath } from "./edge-path.js";
4
+
5
+ const geometry = {
6
+ sourceX: 0,
7
+ sourceY: 0,
8
+ targetX: 200,
9
+ targetY: 100,
10
+ sourcePosition: Position.Right,
11
+ targetPosition: Position.Left,
12
+ };
13
+
14
+ describe("routeEdgePath", () => {
15
+ it("draws a straight link as one segment", () => {
16
+ expect(routeEdgePath("straight", geometry, null).path).toBe("M 0,0L 200,100");
17
+ });
18
+
19
+ it("draws a curve with a cubic", () => {
20
+ expect(routeEdgePath("curved", geometry, null).path).toContain("C");
21
+ });
22
+
23
+ it("draws an elbow with right angles", () => {
24
+ const { path } = routeEdgePath("elbow", geometry, null);
25
+ expect(path).toContain("L");
26
+ expect(path).not.toContain("C");
27
+ });
28
+
29
+ it("detours a same-side pair past both cards", () => {
30
+ const anchors = {
31
+ source: { x: 100, y: 0, side: "right" as const },
32
+ target: { x: 100, y: 120, side: "right" as const },
33
+ };
34
+ const { path, labelX } = routeEdgePath(
35
+ "elbow",
36
+ { ...geometry, sourceX: 100, targetX: 100, targetY: 120 },
37
+ anchors,
38
+ );
39
+ expect(labelX).toBe(140);
40
+ expect(path.startsWith("M 100 0")).toBe(true);
41
+ });
42
+ });
43
+
44
+ describe("arrowTips", () => {
45
+ it("points a straight link's heads along the line", () => {
46
+ const tips = arrowTips("straight", { ...geometry, targetY: 0 }, null);
47
+ expect(tips.end.angle).toBeCloseTo(0);
48
+ expect(tips.start.angle).toBeCloseTo(Math.PI);
49
+ });
50
+
51
+ it("meets a box square on", () => {
52
+ const anchors = {
53
+ source: { x: 0, y: 0, side: "right" as const },
54
+ target: { x: 200, y: 100, side: "top" as const },
55
+ };
56
+ expect(arrowTips("elbow", geometry, anchors).end.angle).toBeCloseTo(Math.PI / 2);
57
+ });
58
+ });
@@ -0,0 +1,293 @@
1
+ // The path and arrowhead placement for one link, chosen by its route. Pure, so the choice is testable.
2
+
3
+ import { getBezierPath, getSmoothStepPath, getStraightPath, Position } from "@xyflow/svelte";
4
+ import {
5
+ bestAnchors,
6
+ sideDetour,
7
+ sideFromHandle,
8
+ type Anchor,
9
+ type Side,
10
+ } from "../headless/edge-anchors.js";
11
+ import { parseBox } from "../headless/edge-memo.js";
12
+
13
+ export type EdgeRoute = "elbow" | "curved" | "straight";
14
+
15
+ export type EdgeGeometry = {
16
+ sourceX: number;
17
+ sourceY: number;
18
+ targetX: number;
19
+ targetY: number;
20
+ sourcePosition: Position;
21
+ targetPosition: Position;
22
+ };
23
+
24
+ export type Anchors = { source: Anchor; target: Anchor };
25
+ export type Point = { x: number; y: number };
26
+ export type ArrowTip = Point & { angle: number };
27
+
28
+ /** The movable run of an elbow: it slides along `axis` from `base` within `min` and `max`; `x`, `y` is its middle. */
29
+ export type BendHandle = Point & { axis: "x" | "y"; base: number; min: number; max: number };
30
+ export type RoutedPath = { path: string; labelX: number; labelY: number; bend: BendHandle | null };
31
+
32
+ /** The corner radius a stepped link shares with the cards it joins. */
33
+ const CORNER = 8;
34
+ /** How far a same-side link runs past the cards, and the least it keeps once bent. */
35
+ const DETOUR = 40;
36
+ const DETOUR_FLOOR = 8;
37
+
38
+ const OUT: Record<Side, Point> = {
39
+ top: { x: 0, y: -1 },
40
+ right: { x: 1, y: 0 },
41
+ bottom: { x: 0, y: 1 },
42
+ left: { x: -1, y: 0 },
43
+ };
44
+
45
+ /** The bend an edge stores in its data: pixels the movable run sits off its unbent place. */
46
+ export function readBend(data: unknown): number {
47
+ const raw = data && typeof data === "object" ? (data as Record<string, unknown>).bend : null;
48
+ return typeof raw === "number" && Number.isFinite(raw) ? raw : 0;
49
+ }
50
+
51
+ /** The bend that puts a handle's run under the pointer, kept inside the handle's limits. */
52
+ export function bendFromPointer(handle: BendHandle, pointer: Point): number {
53
+ const at = handle.axis === "x" ? pointer.x : pointer.y;
54
+ return Math.round(Math.min(handle.max, Math.max(handle.min, at)) - handle.base);
55
+ }
56
+
57
+ function mid(a: number, b: number): number {
58
+ return (a + b) / 2;
59
+ }
60
+
61
+ function clamp(value: number, a: number, b: number): number {
62
+ return Math.min(Math.max(a, b), Math.max(Math.min(a, b), value));
63
+ }
64
+
65
+ type Elbow = { points: Point[]; handle: BendHandle | null };
66
+
67
+ /** Out from `s` to the run at `at`, across it, then in to `t`. */
68
+ function runPoints(s: Anchor, t: Anchor, horizontal: boolean, at: number): Point[] {
69
+ return horizontal
70
+ ? [s, { x: at, y: s.y }, { x: at, y: t.y }, t]
71
+ : [s, { x: s.x, y: at }, { x: t.x, y: at }, t];
72
+ }
73
+
74
+ /** The handle on the run at `at`, or none when the two ends line up and there is nothing to slide. */
75
+ function runHandle(
76
+ { source: s, target: t }: Anchors,
77
+ horizontal: boolean,
78
+ at: number,
79
+ limits: { base: number; min: number; max: number },
80
+ ): BendHandle | null {
81
+ const span = horizontal ? t.y - s.y : t.x - s.x;
82
+ if (!(Math.abs(span) > 1)) return null;
83
+ return {
84
+ axis: horizontal ? "x" : "y",
85
+ x: horizontal ? at : mid(s.x, t.x),
86
+ y: horizontal ? mid(s.y, t.y) : at,
87
+ ...limits,
88
+ };
89
+ }
90
+
91
+ /** Same side: out past both cards, along, back in. The run keeps a margin outside them. */
92
+ function sameSideElbow(anchors: Anchors, bend: number): Elbow {
93
+ const { source: s, target: t } = anchors;
94
+ const out = OUT[s.side];
95
+ const horizontal = out.x !== 0;
96
+ const base = sideDetour(anchors, DETOUR);
97
+ const edge = sideDetour(anchors, 0);
98
+ const outward = horizontal ? out.x : out.y;
99
+ const at = clamp(base + bend, edge + outward * DETOUR_FLOOR, outward > 0 ? Infinity : -Infinity);
100
+ const handle = runHandle(anchors, horizontal, at, {
101
+ base,
102
+ min: outward > 0 ? edge + DETOUR_FLOOR : -Infinity,
103
+ max: outward > 0 ? Infinity : edge - DETOUR_FLOOR,
104
+ });
105
+ return { points: runPoints(s, t, horizontal, at), handle };
106
+ }
107
+
108
+ /** Opposite sides: out, across at the middle, in. The across run is what a bend moves. */
109
+ function acrossElbow(anchors: Anchors, bend: number): Elbow {
110
+ const { source: s, target: t } = anchors;
111
+ const horizontal = OUT[s.side].x !== 0;
112
+ const base = horizontal ? mid(s.x, t.x) : mid(s.y, t.y);
113
+ const lo = horizontal ? Math.min(s.x, t.x) : Math.min(s.y, t.y);
114
+ const hi = horizontal ? Math.max(s.x, t.x) : Math.max(s.y, t.y);
115
+ const at = clamp(base + bend, lo, hi);
116
+ const handle = runHandle(anchors, horizontal, at, { base, min: lo, max: hi });
117
+ return { points: runPoints(s, t, horizontal, at), handle };
118
+ }
119
+
120
+ /** Square sides: one corner, when both ends are on the way there. */
121
+ function cornerElbow(anchors: Anchors): Elbow | null {
122
+ const { source: s, target: t } = anchors;
123
+ const inward = OUT[t.side];
124
+ const horizontalOut = OUT[s.side].x !== 0;
125
+ if (horizontalOut === (inward.x !== 0)) return null;
126
+ const corner = horizontalOut ? { x: t.x, y: s.y } : { x: s.x, y: t.y };
127
+ const arriving = horizontalOut ? Math.sign(t.y - s.y) : Math.sign(t.x - s.x);
128
+ const wanted = horizontalOut ? -inward.y : -inward.x;
129
+ return arriving === wanted ? { points: [s, corner, t], handle: null } : null;
130
+ }
131
+
132
+ /** The corners of an elbow between two anchors and the run a bend moves. Null when it must turn back, which getSmoothStepPath routes. */
133
+ function elbowPoints(anchors: Anchors, bend: number): Elbow | null {
134
+ const { source: s, target: t } = anchors;
135
+ if (s.side === t.side) return sameSideElbow(anchors, bend);
136
+
137
+ const out = OUT[s.side];
138
+ const inward = OUT[t.side];
139
+ const ahead = out.x * (t.x - s.x) + out.y * (t.y - s.y);
140
+ if (ahead <= 0) return null;
141
+
142
+ if (out.x === -inward.x && out.y === -inward.y) return acrossElbow(anchors, bend);
143
+ return cornerElbow(anchors);
144
+ }
145
+
146
+ /** Corners drawn as the same quadratic arcs getSmoothStepPath draws, so the two routes match. */
147
+ function roundedPath(points: Point[]): string {
148
+ const pts = points.filter(
149
+ (p, i) => i === 0 || p.x !== points[i - 1].x || p.y !== points[i - 1].y,
150
+ );
151
+ let d = `M ${pts[0].x} ${pts[0].y}`;
152
+ for (let i = 1; i < pts.length - 1; i++) {
153
+ const a = pts[i - 1];
154
+ const b = pts[i];
155
+ const c = pts[i + 1];
156
+ const r = Math.min(
157
+ CORNER,
158
+ Math.hypot(b.x - a.x, b.y - a.y) / 2,
159
+ Math.hypot(c.x - b.x, c.y - b.y) / 2,
160
+ );
161
+ const collinear = (a.x === b.x && b.x === c.x) || (a.y === b.y && b.y === c.y);
162
+ if (collinear || !(r > 0.5)) {
163
+ d += ` L ${b.x} ${b.y}`;
164
+ continue;
165
+ }
166
+ const before = { x: b.x - Math.sign(b.x - a.x) * r, y: b.y - Math.sign(b.y - a.y) * r };
167
+ const after = { x: b.x + Math.sign(c.x - b.x) * r, y: b.y + Math.sign(c.y - b.y) * r };
168
+ d += ` L ${before.x} ${before.y} Q ${b.x} ${b.y} ${after.x} ${after.y}`;
169
+ }
170
+ const last = pts[pts.length - 1];
171
+ return `${d} L ${last.x} ${last.y}`;
172
+ }
173
+
174
+ /** The middle of the longest run, where a label sits when the link has no bend handle. */
175
+ function middleOf(points: Point[]): Point {
176
+ let best = { length: -1, at: points[0] };
177
+ for (let i = 1; i < points.length; i++) {
178
+ const length = Math.hypot(points[i].x - points[i - 1].x, points[i].y - points[i - 1].y);
179
+ if (length > best.length)
180
+ best = {
181
+ length,
182
+ at: { x: mid(points[i].x, points[i - 1].x), y: mid(points[i].y, points[i - 1].y) },
183
+ };
184
+ }
185
+ return best.at;
186
+ }
187
+
188
+ /** The corners an elbow turns at, for a hand-ruled drawing to follow. Null for any other route. */
189
+ export function elbowCorners(route: EdgeRoute, anchors: Anchors | null, bend = 0): Point[] | null {
190
+ if (route !== "elbow" || !anchors) return null;
191
+ return elbowPoints(anchors, bend)?.points ?? null;
192
+ }
193
+
194
+ export function routeEdgePath(
195
+ route: EdgeRoute,
196
+ geometry: EdgeGeometry,
197
+ anchors: Anchors | null,
198
+ bend = 0,
199
+ ): RoutedPath {
200
+ if (route === "straight") {
201
+ const [path, labelX, labelY] = getStraightPath(geometry);
202
+ return { path, labelX, labelY, bend: null };
203
+ }
204
+ if (route === "curved") {
205
+ const [path, labelX, labelY] = getBezierPath(geometry);
206
+ return { path, labelX, labelY, bend: null };
207
+ }
208
+ const plan = anchors ? elbowPoints(anchors, bend) : null;
209
+ if (plan) {
210
+ const label = plan.handle ?? middleOf(plan.points);
211
+ return { path: roundedPath(plan.points), labelX: label.x, labelY: label.y, bend: plan.handle };
212
+ }
213
+ const [path, labelX, labelY] = getSmoothStepPath({ ...geometry, borderRadius: CORNER });
214
+ return { path, labelX, labelY, bend: null };
215
+ }
216
+
217
+ export type LinkInput = {
218
+ route: EdgeRoute;
219
+ bend: number;
220
+ /** Each end's `boxKey`, "" until the card is measured. */
221
+ fromBox: string;
222
+ toBox: string;
223
+ /** The edge's stored handle ids. */
224
+ sourceHandle?: string | null;
225
+ targetHandle?: string | null;
226
+ /** The geometry xyflow measured, used until both cards have a size. */
227
+ fallback: EdgeGeometry;
228
+ };
229
+
230
+ export type Link = RoutedPath & {
231
+ anchors: Anchors | null;
232
+ geometry: EdgeGeometry;
233
+ tips: { start: ArrowTip; end: ArrowTip };
234
+ };
235
+
236
+ /** Everything one link draws: its sides, path, bend handle and arrowheads, from where its cards sit. */
237
+ export function planLink(input: LinkInput): Link {
238
+ const from = parseBox(input.fromBox);
239
+ const to = parseBox(input.toBox);
240
+ let anchors: Anchors | null = null;
241
+ let geometry = input.fallback;
242
+ if (from && to) {
243
+ anchors = bestAnchors(from, to, {
244
+ source: sideFromHandle(input.sourceHandle),
245
+ target: sideFromHandle(input.targetHandle),
246
+ });
247
+ geometry = {
248
+ sourceX: anchors.source.x,
249
+ sourceY: anchors.source.y,
250
+ sourcePosition: anchors.source.side as Position,
251
+ targetX: anchors.target.x,
252
+ targetY: anchors.target.y,
253
+ targetPosition: anchors.target.side as Position,
254
+ };
255
+ }
256
+ return {
257
+ ...routeEdgePath(input.route, geometry, anchors, input.bend),
258
+ anchors,
259
+ geometry,
260
+ tips: arrowTips(input.route, geometry, anchors),
261
+ };
262
+ }
263
+
264
+ /** The direction a link travels when it meets a box on `side`: straight into it. */
265
+ function into(side: Side): number {
266
+ switch (side) {
267
+ case "left":
268
+ return 0;
269
+ case "right":
270
+ return Math.PI;
271
+ case "top":
272
+ return Math.PI / 2;
273
+ case "bottom":
274
+ return -Math.PI / 2;
275
+ }
276
+ }
277
+
278
+ /** Where an arrowhead sits and points at each end: along a straight link's line, square on to the box otherwise. */
279
+ export function arrowTips(
280
+ route: EdgeRoute,
281
+ geometry: EdgeGeometry,
282
+ anchors: Anchors | null,
283
+ ): { start: ArrowTip; end: ArrowTip } {
284
+ const { sourceX, sourceY, targetX, targetY } = geometry;
285
+ const straight = Math.atan2(targetY - sourceY, targetX - sourceX);
286
+ const startAngle =
287
+ route === "straight" || !anchors ? straight + Math.PI : into(anchors.source.side);
288
+ const endAngle = route === "straight" || !anchors ? straight : into(anchors.target.side);
289
+ return {
290
+ start: { x: sourceX, y: sourceY, angle: startAngle },
291
+ end: { x: targetX, y: targetY, angle: endAngle },
292
+ };
293
+ }
@@ -0,0 +1,43 @@
1
+ <svelte:options namespace="svg" />
2
+
3
+ <script lang="ts">
4
+ // =========================
5
+ // IMPORTS
6
+ // =========================
7
+ import { BaseEdge } from "@xyflow/svelte";
8
+ import EdgeGradient from "./edge-gradient.svelte";
9
+
10
+ // =========================
11
+ // TYPES
12
+ // =========================
13
+ interface Props {
14
+ id: string;
15
+ path: string;
16
+ from: { x: number; y: number };
17
+ to: { x: number; y: number };
18
+ fromColor: string;
19
+ toColor?: string;
20
+ markerEnd?: string;
21
+ /** The edge's own style. A stroke set here beats the gradient. */
22
+ style?: string;
23
+ class?: string;
24
+ }
25
+
26
+ // =========================
27
+ // PROPS
28
+ // =========================
29
+ let { id, path, from, to, fromColor, toColor = fromColor, markerEnd, style = "", class: className = "" }: Props = $props();
30
+
31
+ // =========================
32
+ // DERIVED
33
+ // =========================
34
+ let gradientId = $derived(`canvas-gradient-${id.replace(/[^\w-]/g, "_")}`);
35
+ </script>
36
+
37
+ <!-- =========================== -->
38
+ <!-- MARKUP -->
39
+ <!-- =========================== -->
40
+ <EdgeGradient id={gradientId} {from} {to} {fromColor} {toColor} />
41
+ <BaseEdge {id} {path} {markerEnd} class="canvas-edge {className}" style="stroke: url(#{gradientId}); {style}" />
42
+ <!-- Hidden until a test run crosses this link; the canvas stylesheet draws the moving dashes. -->
43
+ <path d={path} class="canvas-edge-flow" fill="none" aria-hidden="true" />
@@ -0,0 +1,65 @@
1
+ import { describe, expect, it } from "vite-plus/test";
2
+ import { resolveContextMenuHit } from "./context-menu.js";
3
+ import type { Node } from "@xyflow/svelte";
4
+
5
+ function elementStub(attrs: Record<string, string> = {}, data: Record<string, string> = {}) {
6
+ const rect = { x: 1, y: 2, width: 3, height: 4 };
7
+ const el = {
8
+ getAttribute: (name: string) => attrs[name] ?? null,
9
+ getBoundingClientRect: () => rect,
10
+ dataset: data,
11
+ closest: (sel: string) => {
12
+ if (sel === ".svelte-flow__edge" && attrs.edge) return el;
13
+ if (sel === "[data-id]" && attrs["data-id"]) return el;
14
+ if (sel === "[data-table-row]" && data.tableRow) return el;
15
+ return null;
16
+ },
17
+ };
18
+ return el as unknown as HTMLElement;
19
+ }
20
+
21
+ function event(target: HTMLElement): MouseEvent {
22
+ return { target, clientX: 10, clientY: 20 } as unknown as MouseEvent;
23
+ }
24
+
25
+ describe("resolveContextMenuHit", () => {
26
+ it("reads an edge hit and skips the node lookup", () => {
27
+ const target = elementStub({ edge: "1", "data-id": "e1" });
28
+ const hit = resolveContextMenuHit(event(target), []);
29
+ expect(hit).toEqual({ kind: "edge", edgeId: "e1", position: { x: 10, y: 20 } });
30
+ });
31
+
32
+ it("reads a node hit with its rect and type", () => {
33
+ const target = elementStub({ "data-id": "n1" });
34
+ const nodes = [{ id: "n1", type: "note" }] as unknown as Node[];
35
+ const hit = resolveContextMenuHit(event(target), nodes);
36
+ expect(hit).toEqual({
37
+ kind: "canvas",
38
+ position: { x: 10, y: 20 },
39
+ anchorRect: { x: 1, y: 2, width: 3, height: 4 },
40
+ nodeId: "n1",
41
+ nodeType: "note",
42
+ tableCell: null,
43
+ });
44
+ });
45
+
46
+ it("reads a table cell under the pointer", () => {
47
+ const target = elementStub({ "data-id": "n1" }, { tableRow: "1", tableCol: "2" });
48
+ const nodes = [{ id: "n1", type: "table" }] as unknown as Node[];
49
+ const hit = resolveContextMenuHit(event(target), nodes);
50
+ expect(hit.kind === "canvas" && hit.tableCell).toEqual({ row: 1, col: 2 });
51
+ });
52
+
53
+ it("reads a bare canvas click as a null node", () => {
54
+ const target = elementStub({});
55
+ const hit = resolveContextMenuHit(event(target), []);
56
+ expect(hit).toEqual({
57
+ kind: "canvas",
58
+ position: { x: 10, y: 20 },
59
+ anchorRect: null,
60
+ nodeId: null,
61
+ nodeType: null,
62
+ tableCell: null,
63
+ });
64
+ });
65
+ });
@@ -0,0 +1,48 @@
1
+ import type { Node } from "@xyflow/svelte";
2
+
3
+ export type Rect = { x: number; y: number; width: number; height: number };
4
+ export type TableCellHit = { row: number; col: number };
5
+
6
+ export type ContextMenuHit =
7
+ | { kind: "edge"; edgeId: string; position: { x: number; y: number } }
8
+ | {
9
+ kind: "canvas";
10
+ position: { x: number; y: number };
11
+ anchorRect: Rect | null;
12
+ nodeId: string | null;
13
+ nodeType: string | null;
14
+ tableCell: TableCellHit | null;
15
+ };
16
+
17
+ function rectOf(el: Element | null): Rect | null {
18
+ if (!el) return null;
19
+ const r = el.getBoundingClientRect();
20
+ return { x: r.x, y: r.y, width: r.width, height: r.height };
21
+ }
22
+
23
+ function tableCellOf(target: HTMLElement): TableCellHit | null {
24
+ const cell = target.closest<HTMLElement>("[data-table-row]");
25
+ if (!cell) return null;
26
+ return { row: Number(cell.dataset.tableRow), col: Number(cell.dataset.tableCol) };
27
+ }
28
+
29
+ /** Reads a right-click into an edge hit, or a canvas/node hit with its anchor
30
+ * rect and cell. The edge check runs first, because an arrow also carries
31
+ * `data-id`. */
32
+ export function resolveContextMenuHit(event: MouseEvent, nodes: Node[]): ContextMenuHit {
33
+ const target = event.target as HTMLElement;
34
+ const position = { x: event.clientX, y: event.clientY };
35
+ const edgeId = target.closest(".svelte-flow__edge")?.getAttribute("data-id");
36
+ if (edgeId) return { kind: "edge", edgeId, position };
37
+ const nodeElement = target.closest("[data-id]");
38
+ const nodeId = nodeElement?.getAttribute("data-id") ?? null;
39
+ const node = nodeId ? nodes.find((n) => n.id === nodeId) : undefined;
40
+ return {
41
+ kind: "canvas",
42
+ position,
43
+ anchorRect: rectOf(nodeElement),
44
+ nodeId,
45
+ nodeType: node?.type ?? null,
46
+ tableCell: tableCellOf(target),
47
+ };
48
+ }