@heyramzi/ui 1.54.1 → 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 (664) hide show
  1. package/COMPONENTS.md +3 -2
  2. package/README.md +34 -9
  3. package/package.json +200 -7
  4. package/scripts/sync-shadcn.mjs +2 -2
  5. package/src/ai/ai-button/AiButton.svelte +1 -1
  6. package/src/ai/confirmation/Confirmation.svelte +1 -1
  7. package/src/ai/confirmation/ConfirmationAction.svelte +1 -1
  8. package/src/ai/confirmation/ConfirmationActions.svelte +1 -1
  9. package/src/ai/confirmation/ConfirmationTitle.svelte +1 -1
  10. package/src/ai/generation-progress/GenerationProgress.svelte +13 -13
  11. package/src/ai/loading-state/PixelLoader.svelte +1 -1
  12. package/src/ai/prompt-input/PromptInput.svelte +1 -1
  13. package/src/ai/prompt-input/PromptInputBody.svelte +1 -1
  14. package/src/ai/prompt-input/PromptInputSubmit.svelte +1 -1
  15. package/src/ai/prompt-input/PromptInputTextarea.svelte +1 -1
  16. package/src/ai/prompt-input/PromptInputToolbar.svelte +1 -1
  17. package/src/ai/reasoning/Reasoning.svelte +1 -1
  18. package/src/ai/reasoning/ReasoningContent.svelte +1 -1
  19. package/src/ai/reasoning/ReasoningTrigger.svelte +1 -1
  20. package/src/ai/response/Response.svelte +1 -1
  21. package/src/ai/suggestion/Suggestion.svelte +1 -1
  22. package/src/ai/suggestion/Suggestions.svelte +1 -1
  23. package/src/ai/thinking-dots/ThinkingDots.svelte +1 -1
  24. package/src/ai/typing-dots/TypingDots.svelte +1 -1
  25. package/src/ai/voice-waveform/VoiceWaveform.svelte +1 -1
  26. package/src/ambient.d.ts +8 -0
  27. package/src/brand/brand.test.ts +67 -0
  28. package/src/brand/index.ts +137 -0
  29. package/src/canvas/README.md +236 -0
  30. package/src/canvas/card/card-frame.svelte +61 -0
  31. package/src/canvas/card/card.css +39 -0
  32. package/src/canvas/card/resize-handles.svelte +119 -0
  33. package/src/canvas/card/resize.ts +33 -0
  34. package/src/canvas/chrome/chrome-zoom.svelte +120 -0
  35. package/src/canvas/chrome/chrome.css +231 -0
  36. package/src/canvas/chrome/lock-badge.svelte +18 -0
  37. package/src/canvas/chrome/side-handles.svelte +30 -0
  38. package/src/canvas/chrome/size-chip.svelte +45 -0
  39. package/src/canvas/chrome/snap-guides.svelte +69 -0
  40. package/src/canvas/components/icons/ToolIcon.svelte +30 -0
  41. package/src/canvas/components/shared/svg/PreviewSvg.svelte +67 -0
  42. package/src/canvas/components/shared/svg/ShapeOutline.svelte +76 -0
  43. package/src/canvas/components/shared/svg/StrokeNode.svelte +69 -0
  44. package/src/canvas/components/shared/svg/TextRule.svelte +37 -0
  45. package/src/canvas/edges/bend-handle.svelte +76 -0
  46. package/src/canvas/edges/edge-gradient.svelte +30 -0
  47. package/src/canvas/edges/edge-path.test.ts +58 -0
  48. package/src/canvas/edges/edge-path.ts +293 -0
  49. package/src/canvas/edges/gradient-edge.svelte +43 -0
  50. package/src/canvas/editor/context-menu.test.ts +65 -0
  51. package/src/canvas/editor/context-menu.ts +48 -0
  52. package/src/canvas/editor/dispatch.svelte.test.ts +243 -0
  53. package/src/canvas/editor/dispatch.svelte.ts +205 -0
  54. package/src/canvas/editor/editor-context.ts +62 -0
  55. package/src/canvas/editor/editor-mode.ts +67 -0
  56. package/src/canvas/editor/frame.ts +98 -0
  57. package/src/canvas/editor/hand-context.ts +15 -0
  58. package/src/canvas/editor/history-core.test.ts +239 -0
  59. package/src/canvas/editor/history-core.ts +247 -0
  60. package/src/canvas/editor/keymap.test.ts +161 -0
  61. package/src/canvas/editor/keymap.ts +151 -0
  62. package/src/canvas/editor/layout-actions.test.ts +156 -0
  63. package/src/canvas/editor/layout-actions.ts +219 -0
  64. package/src/canvas/editor/lock.test.ts +68 -0
  65. package/src/canvas/editor/lock.ts +87 -0
  66. package/src/canvas/editor/rebase.test.ts +208 -0
  67. package/src/canvas/editor/rebase.ts +272 -0
  68. package/src/canvas/editor/style.ts +184 -0
  69. package/src/canvas/editor/text-field-context.ts +41 -0
  70. package/src/canvas/editor/z-order.test.ts +75 -0
  71. package/src/canvas/editor/z-order.ts +87 -0
  72. package/src/canvas/headless/alignment.test.ts +59 -0
  73. package/src/canvas/headless/alignment.ts +23 -0
  74. package/src/canvas/headless/break-apart-find.ts +418 -0
  75. package/src/canvas/headless/break-apart-pieces.ts +104 -0
  76. package/src/canvas/headless/break-apart-plan.ts +439 -0
  77. package/src/canvas/headless/break-apart-style.ts +106 -0
  78. package/src/canvas/headless/break-apart.ts +244 -0
  79. package/src/canvas/headless/canvas-cleanup.test.ts +181 -0
  80. package/src/canvas/headless/canvas-cleanup.ts +102 -0
  81. package/src/canvas/headless/edge-anchors.test.ts +70 -0
  82. package/src/canvas/headless/edge-anchors.ts +143 -0
  83. package/src/canvas/headless/edge-memo.test.ts +58 -0
  84. package/src/canvas/headless/edge-memo.ts +32 -0
  85. package/src/canvas/headless/edge-serialization.test.ts +55 -0
  86. package/src/canvas/headless/edge-serialization.ts +32 -0
  87. package/src/canvas/headless/figure-geometry.ts +293 -0
  88. package/src/canvas/headless/fit-viewport.test.ts +85 -0
  89. package/src/canvas/headless/fit-viewport.ts +137 -0
  90. package/src/canvas/headless/grid.test.ts +34 -0
  91. package/src/canvas/headless/grid.ts +94 -0
  92. package/src/canvas/headless/icon-node.test.ts +23 -0
  93. package/src/canvas/headless/icon-node.ts +66 -0
  94. package/src/canvas/headless/index.ts +9 -0
  95. package/src/canvas/headless/linkify.test.ts +23 -0
  96. package/src/canvas/headless/linkify.ts +25 -0
  97. package/src/canvas/headless/pen-marks.ts +133 -0
  98. package/src/canvas/headless/sketch.test.ts +99 -0
  99. package/src/canvas/headless/sketch.ts +302 -0
  100. package/src/canvas/headless/snap-guides.test.ts +61 -0
  101. package/src/canvas/headless/snap-guides.ts +146 -0
  102. package/src/canvas/headless/svg-figure.test.ts +49 -0
  103. package/src/canvas/headless/svg-figure.ts +176 -0
  104. package/src/canvas/headless/table-layout.test.ts +158 -0
  105. package/src/canvas/headless/table-layout.ts +309 -0
  106. package/src/canvas/index.ts +35 -0
  107. package/src/canvas/lod/lod-mode.svelte.ts +30 -0
  108. package/src/canvas/lod/lod-switch.svelte +140 -0
  109. package/src/canvas/lod/render-all.svelte.ts +52 -0
  110. package/src/canvas/ops/arrange.ts +108 -0
  111. package/src/canvas/ops/ops.test.ts +98 -0
  112. package/src/canvas/ops/ops.ts +347 -0
  113. package/src/canvas/pen/PenField.svelte +128 -0
  114. package/src/canvas/pen/PenField.test.ts +87 -0
  115. package/src/canvas/pen/PenOverlay.svelte +172 -0
  116. package/src/canvas/pen/ShapeNode.svelte +64 -0
  117. package/src/canvas/pen/TextNode.svelte +97 -0
  118. package/src/canvas/pen/colors.ts +28 -0
  119. package/src/canvas/pen/eraser.ts +46 -0
  120. package/src/canvas/pen/geometry.ts +148 -0
  121. package/src/canvas/pen/host-field.test.svelte +13 -0
  122. package/src/canvas/pen/index.ts +40 -0
  123. package/src/canvas/pen/ops.ts +16 -0
  124. package/src/canvas/pen/pen-session.ts +177 -0
  125. package/src/canvas/pen/pen.css +149 -0
  126. package/src/canvas/pen/plugin.ts +139 -0
  127. package/src/canvas/pen/shapes.ts +104 -0
  128. package/src/canvas/pen/stroke-path.ts +54 -0
  129. package/src/canvas/pen/types.ts +96 -0
  130. package/src/canvas/pen/ui.ts +26 -0
  131. package/src/canvas/plugin.test.ts +45 -0
  132. package/src/canvas/plugin.ts +159 -0
  133. package/src/canvas/screen/GlyphIcon.svelte +25 -0
  134. package/src/canvas/screen/PartView.svelte +176 -0
  135. package/src/canvas/screen/ScreenBody.svelte +142 -0
  136. package/src/canvas/screen/ScreenCard.svelte +32 -0
  137. package/src/canvas/screen/glyphs/button.svg +1 -0
  138. package/src/canvas/screen/glyphs/close.svg +1 -0
  139. package/src/canvas/screen/glyphs/down.svg +1 -0
  140. package/src/canvas/screen/glyphs/header.svg +1 -0
  141. package/src/canvas/screen/glyphs/hills.svg +1 -0
  142. package/src/canvas/screen/glyphs/image.svg +1 -0
  143. package/src/canvas/screen/glyphs/input.svg +1 -0
  144. package/src/canvas/screen/glyphs/list.svg +1 -0
  145. package/src/canvas/screen/glyphs/modal.svg +1 -0
  146. package/src/canvas/screen/glyphs/plus.svg +1 -0
  147. package/src/canvas/screen/glyphs/up.svg +1 -0
  148. package/src/canvas/screen/glyphs.ts +40 -0
  149. package/src/canvas/screen/labels.ts +41 -0
  150. package/src/canvas/screen/parts.ts +164 -0
  151. package/src/canvas/screen/plugin.ts +67 -0
  152. package/src/canvas/screen/screen.css +593 -0
  153. package/src/canvas/screen/types.ts +47 -0
  154. package/src/canvas/screen/ui.ts +13 -0
  155. package/src/canvas/test-utils.ts +18 -0
  156. package/src/canvas/types.ts +53 -0
  157. package/src/canvas/ui/CanvasToolbar.svelte +169 -0
  158. package/src/canvas/ui/FrameLayer.svelte +116 -0
  159. package/src/canvas/ui/PenLayer.svelte +39 -0
  160. package/src/canvas/ui/StylePanel.svelte +268 -0
  161. package/src/canvas/ui/canvas-grid.svelte +156 -0
  162. package/src/canvas/ui/canvas-ui.css +350 -0
  163. package/src/canvas/ui/forward-wheel.ts +29 -0
  164. package/src/canvas/ui/icons.ts +124 -0
  165. package/src/canvas/ui/place-panel.ts +80 -0
  166. package/src/canvas/ui/shortcut-tip.test.ts +16 -0
  167. package/src/canvas/ui/shortcut-tip.ts +17 -0
  168. package/src/canvas/ui/tools.ts +146 -0
  169. package/src/charts/README.md +104 -0
  170. package/src/charts/ambient.d.ts +4 -0
  171. package/src/charts/animated-value.css +7 -0
  172. package/src/charts/animated-value.svelte +110 -0
  173. package/src/charts/area-chart.css +29 -0
  174. package/src/charts/area-chart.svelte +204 -0
  175. package/src/charts/bar-chart.css +119 -0
  176. package/src/charts/bar-chart.svelte +275 -0
  177. package/src/charts/charts.css +201 -0
  178. package/src/charts/crosshair.svelte +35 -0
  179. package/src/charts/dashboard/chart.ts +5 -0
  180. package/src/charts/dashboard/dashboard-empty.svelte +55 -0
  181. package/src/charts/dashboard/dashboard-section.svelte +58 -0
  182. package/src/charts/dashboard/data-panel.svelte +81 -0
  183. package/src/charts/dashboard/data-sort-head.svelte +102 -0
  184. package/src/charts/dashboard/data-table.svelte +278 -0
  185. package/src/charts/dashboard/data-table.ts +26 -0
  186. package/src/charts/dashboard/index.ts +25 -0
  187. package/src/charts/dashboard/panel-grid.svelte +43 -0
  188. package/src/charts/dashboard/period-filter.svelte +53 -0
  189. package/src/charts/dashboard/rating-chip.svelte +35 -0
  190. package/src/charts/dashboard/stat-band.svelte +47 -0
  191. package/src/charts/dashboard/stat-tile.svelte +176 -0
  192. package/src/charts/dashboard/windows.ts +8 -0
  193. package/src/charts/donut-chart.css +56 -0
  194. package/src/charts/donut-chart.svelte +158 -0
  195. package/src/charts/edge-fade.svelte +43 -0
  196. package/src/charts/edge-fade.ts +16 -0
  197. package/src/charts/figure.css +100 -0
  198. package/src/charts/figure.svelte +122 -0
  199. package/src/charts/gauge-chart.css +94 -0
  200. package/src/charts/gauge-chart.svelte +302 -0
  201. package/src/charts/hover-band.test.ts +30 -0
  202. package/src/charts/hover-band.ts +21 -0
  203. package/src/charts/hover-focus.svelte +85 -0
  204. package/src/charts/index.ts +31 -0
  205. package/src/charts/legend.css +136 -0
  206. package/src/charts/legend.svelte +112 -0
  207. package/src/charts/line-chart.css +26 -0
  208. package/src/charts/line-chart.svelte +256 -0
  209. package/src/charts/motion.ts +19 -0
  210. package/src/charts/number-chart.css +179 -0
  211. package/src/charts/number-chart.svelte +187 -0
  212. package/src/charts/primitives/chart-container.svelte +57 -0
  213. package/src/charts/primitives/chart-style.svelte +36 -0
  214. package/src/charts/primitives/chart-tooltip.svelte +161 -0
  215. package/src/charts/primitives/chart-utils.ts +54 -0
  216. package/src/charts/primitives/index.ts +11 -0
  217. package/src/charts/reference-line.css +18 -0
  218. package/src/charts/reference-line.svelte +31 -0
  219. package/src/charts/reveal.svelte +61 -0
  220. package/src/charts/ring-chart.css +104 -0
  221. package/src/charts/ring-chart.svelte +166 -0
  222. package/src/charts/sparkline.css +45 -0
  223. package/src/charts/sparkline.svelte +141 -0
  224. package/src/charts/stat-card.css +105 -0
  225. package/src/charts/stat-card.svelte +267 -0
  226. package/src/charts/time-series-chart.css +28 -0
  227. package/src/charts/time-series-chart.svelte +179 -0
  228. package/src/charts/trend-pill.css +38 -0
  229. package/src/charts/trend-pill.svelte +48 -0
  230. package/src/components/copy/clipboard.ts +13 -9
  231. package/src/components/copy/copy-button.svelte +2 -2
  232. package/src/components/data-table/row-arrow.svelte +1 -1
  233. package/src/components/data-table/sortable-head.svelte +1 -1
  234. package/src/components/docs/doc-page.svelte +1 -1
  235. package/src/components/docs/on-this-page.svelte +2 -2
  236. package/src/components/field-row/field-row.svelte +1 -1
  237. package/src/components/hold-to-confirm/hold-to-confirm.svelte +1 -1
  238. package/src/components/language-flag/language-flag.svelte +1 -1
  239. package/src/components/list-page/detail-empty.svelte +1 -1
  240. package/src/components/list-page/detail-header.svelte +1 -1
  241. package/src/components/list-page/detail-panel.svelte +1 -1
  242. package/src/components/list-page/detail-row.svelte +1 -1
  243. package/src/components/list-page/detail-stack.svelte +1 -1
  244. package/src/components/list-page/empty-state.svelte +1 -1
  245. package/src/components/list-page/filter-item.svelte +1 -1
  246. package/src/components/list-page/filter-popover.svelte +2 -2
  247. package/src/components/list-page/filter-section.ts +1 -1
  248. package/src/components/list-page/grouped-list.svelte +1 -1
  249. package/src/components/list-page/list-page-shell.svelte +1 -1
  250. package/src/components/list-page/list-table.ts +1 -1
  251. package/src/components/list-page/master-detail.svelte +1 -1
  252. package/src/components/list-page/notice-bar.svelte +1 -1
  253. package/src/components/list-page/prev-next.svelte +1 -1
  254. package/src/components/list-page/rail-body.svelte +1 -1
  255. package/src/components/list-page/rail-empty.svelte +1 -1
  256. package/src/components/list-page/rail-head.svelte +1 -1
  257. package/src/components/list-page/rail-row.svelte +1 -1
  258. package/src/components/list-page/section-label.svelte +2 -2
  259. package/src/components/list-page/toolbar-search.svelte +1 -1
  260. package/src/components/list-page/toolbar-trigger.ts +1 -1
  261. package/src/components/mark/mark.svelte +1 -1
  262. package/src/components/open-in/open-in.svelte +1 -1
  263. package/src/components/picker/mark-picker.svelte +1 -1
  264. package/src/components/picker/picker.svelte +1 -1
  265. package/src/components/section-card/section-card.svelte +1 -1
  266. package/src/components/segmented/animated-tabs.svelte +1 -1
  267. package/src/components/segmented/section-runs.svelte +1 -1
  268. package/src/components/segmented/segment.svelte +5 -5
  269. package/src/components/segmented/segmented-filter.svelte +1 -1
  270. package/src/components/segmented/segmented-nav.svelte +1 -1
  271. package/src/components/segmented/segmented-overflow.svelte +1 -1
  272. package/src/components/segmented/segmented-track.svelte +1 -1
  273. package/src/components/star-rating-input/star-rating-input.svelte +1 -1
  274. package/src/components/table-view/table-view.svelte +1 -1
  275. package/src/components/theme/theme-provider.svelte +6 -4
  276. package/src/components/toolbar/index.ts +5 -0
  277. package/src/components/toolbar/toolbar.svelte +168 -0
  278. package/src/components/toolbar/toolbar.test.ts +66 -0
  279. package/src/editor/README.md +58 -0
  280. package/src/editor/ambient.d.ts +1 -0
  281. package/src/editor/block-id.ts +77 -0
  282. package/src/editor/block-ops.test.ts +237 -0
  283. package/src/editor/block-ops.ts +157 -0
  284. package/src/editor/content.css +418 -0
  285. package/src/editor/editor-toolbar.svelte +303 -0
  286. package/src/editor/editor.svelte +462 -0
  287. package/src/editor/extensions.ts +145 -0
  288. package/src/editor/image-toolbar.svelte +123 -0
  289. package/src/editor/index.ts +17 -0
  290. package/src/editor/inline-editor.svelte +261 -0
  291. package/src/editor/markdown-escaping.test.ts +158 -0
  292. package/src/editor/markdown-marks.test.ts +89 -0
  293. package/src/editor/markdown-test-helpers.ts +13 -0
  294. package/src/editor/markdown-youtube.test.ts +93 -0
  295. package/src/editor/markdown.test.ts +250 -0
  296. package/src/editor/markdown.ts +511 -0
  297. package/src/editor/renderer.svelte +92 -0
  298. package/src/editor/serializer.test.ts +133 -0
  299. package/src/editor/serializer.ts +370 -0
  300. package/src/editor/slash-items.ts +115 -0
  301. package/src/editor/slash-menu.svelte +159 -0
  302. package/src/editor/youtube-url.ts +35 -0
  303. package/src/effects/animated-beam/AnimatedBeam.svelte +1 -1
  304. package/src/effects/border-beam/register-properties.ts +4 -2
  305. package/src/effects/image-comparison/ImageComparison.svelte +1 -1
  306. package/src/effects/image-reveal/ImageReveal.svelte +1 -1
  307. package/src/effects/interactive-hover-button/InteractiveHoverButton.svelte +13 -13
  308. package/src/effects/magic-card/MagicCard.svelte +1 -1
  309. package/src/effects/marquee/Marquee.svelte +1 -1
  310. package/src/effects/progressive-blur/ProgressiveBlur.svelte +1 -1
  311. package/src/effects/reveal/AnimatedGroup.svelte +1 -1
  312. package/src/effects/reveal/InView.svelte +1 -1
  313. package/src/effects/reveal/TextEffect.svelte +1 -1
  314. package/src/effects/shader-gradient/ShaderGradient.svelte +1 -1
  315. package/src/effects/spotlight/Spotlight.svelte +1 -1
  316. package/src/effects/theme-toggle/ThemeToggle.svelte +4 -4
  317. package/src/lib/result.ts +19 -0
  318. package/src/lib/utils.ts +18 -0
  319. package/src/onboarding/README.md +120 -0
  320. package/src/onboarding/define-tours.test.ts +50 -0
  321. package/src/onboarding/define-tours.ts +31 -0
  322. package/src/onboarding/demo.svelte +86 -0
  323. package/src/onboarding/index.ts +12 -0
  324. package/src/onboarding/onboarding-provider.svelte +120 -0
  325. package/src/onboarding/onboarding.svelte.ts +24 -0
  326. package/src/onboarding/pick.test.ts +79 -0
  327. package/src/onboarding/pick.ts +36 -0
  328. package/src/onboarding/stores.test.ts +103 -0
  329. package/src/onboarding/stores.ts +58 -0
  330. package/src/onboarding/tour-card.svelte +70 -0
  331. package/src/onboarding/tour-runner.svelte +197 -0
  332. package/src/onboarding/types.ts +34 -0
  333. package/src/onboarding/wait.ts +29 -0
  334. package/src/shadcn/accordion/accordion-content.svelte +1 -1
  335. package/src/shadcn/accordion/accordion-item.svelte +1 -1
  336. package/src/shadcn/accordion/accordion-trigger.svelte +1 -1
  337. package/src/shadcn/accordion/accordion.svelte +1 -1
  338. package/src/shadcn/alert/alert-description.svelte +1 -1
  339. package/src/shadcn/alert/alert-title.svelte +1 -1
  340. package/src/shadcn/alert/alert.svelte +1 -1
  341. package/src/shadcn/alert-dialog/alert-dialog-action.svelte +1 -1
  342. package/src/shadcn/alert-dialog/alert-dialog-cancel.svelte +1 -1
  343. package/src/shadcn/alert-dialog/alert-dialog-content.svelte +1 -1
  344. package/src/shadcn/alert-dialog/alert-dialog-description.svelte +1 -1
  345. package/src/shadcn/alert-dialog/alert-dialog-footer.svelte +1 -1
  346. package/src/shadcn/alert-dialog/alert-dialog-header.svelte +1 -1
  347. package/src/shadcn/alert-dialog/alert-dialog-overlay.svelte +1 -1
  348. package/src/shadcn/alert-dialog/alert-dialog-title.svelte +1 -1
  349. package/src/shadcn/attachment/attachment-action.svelte +1 -1
  350. package/src/shadcn/attachment/attachment-actions.svelte +1 -1
  351. package/src/shadcn/attachment/attachment-content.svelte +1 -1
  352. package/src/shadcn/attachment/attachment-description.svelte +1 -1
  353. package/src/shadcn/attachment/attachment-group.svelte +1 -1
  354. package/src/shadcn/attachment/attachment-media.svelte +1 -1
  355. package/src/shadcn/attachment/attachment-title.svelte +1 -1
  356. package/src/shadcn/attachment/attachment-trigger.svelte +1 -1
  357. package/src/shadcn/attachment/attachment.svelte +1 -1
  358. package/src/shadcn/avatar/avatar-badge.svelte +1 -1
  359. package/src/shadcn/avatar/avatar-fallback.svelte +1 -1
  360. package/src/shadcn/avatar/avatar-group-count.svelte +1 -1
  361. package/src/shadcn/avatar/avatar-group.svelte +1 -1
  362. package/src/shadcn/avatar/avatar-image.svelte +1 -1
  363. package/src/shadcn/avatar/avatar.svelte +1 -1
  364. package/src/shadcn/badge/badge.svelte +1 -1
  365. package/src/shadcn/breadcrumb/breadcrumb-ellipsis.svelte +1 -1
  366. package/src/shadcn/breadcrumb/breadcrumb-item.svelte +1 -1
  367. package/src/shadcn/breadcrumb/breadcrumb-link.svelte +1 -1
  368. package/src/shadcn/breadcrumb/breadcrumb-list.svelte +1 -1
  369. package/src/shadcn/breadcrumb/breadcrumb-page.svelte +1 -1
  370. package/src/shadcn/breadcrumb/breadcrumb-separator.svelte +1 -1
  371. package/src/shadcn/breadcrumb/breadcrumb.svelte +1 -1
  372. package/src/shadcn/bubble/bubble-content.svelte +1 -1
  373. package/src/shadcn/bubble/bubble-group.svelte +1 -1
  374. package/src/shadcn/bubble/bubble-reactions.svelte +1 -1
  375. package/src/shadcn/bubble/bubble.svelte +1 -1
  376. package/src/shadcn/button/button.svelte +1 -1
  377. package/src/shadcn/calendar/calendar-cell.svelte +1 -1
  378. package/src/shadcn/calendar/calendar-day.svelte +1 -1
  379. package/src/shadcn/calendar/calendar-grid-body.svelte +1 -1
  380. package/src/shadcn/calendar/calendar-grid-head.svelte +1 -1
  381. package/src/shadcn/calendar/calendar-grid-row.svelte +1 -1
  382. package/src/shadcn/calendar/calendar-grid.svelte +1 -1
  383. package/src/shadcn/calendar/calendar-head-cell.svelte +1 -1
  384. package/src/shadcn/calendar/calendar-header.svelte +1 -1
  385. package/src/shadcn/calendar/calendar-heading.svelte +1 -1
  386. package/src/shadcn/calendar/calendar-month-select.svelte +1 -1
  387. package/src/shadcn/calendar/calendar-month.svelte +1 -1
  388. package/src/shadcn/calendar/calendar-months.svelte +1 -1
  389. package/src/shadcn/calendar/calendar-nav.svelte +1 -1
  390. package/src/shadcn/calendar/calendar-next-button.svelte +1 -1
  391. package/src/shadcn/calendar/calendar-prev-button.svelte +1 -1
  392. package/src/shadcn/calendar/calendar-year-select.svelte +1 -1
  393. package/src/shadcn/calendar/calendar.svelte +1 -1
  394. package/src/shadcn/card/card-action.svelte +1 -1
  395. package/src/shadcn/card/card-content.svelte +1 -1
  396. package/src/shadcn/card/card-description.svelte +1 -1
  397. package/src/shadcn/card/card-footer.svelte +1 -1
  398. package/src/shadcn/card/card-header.svelte +1 -1
  399. package/src/shadcn/card/card-title.svelte +1 -1
  400. package/src/shadcn/card/card.svelte +1 -1
  401. package/src/shadcn/checkbox/checkbox.svelte +1 -1
  402. package/src/shadcn/code-block/code-block-actions.svelte +1 -1
  403. package/src/shadcn/code-block/code-block-content.svelte +1 -1
  404. package/src/shadcn/code-block/code-block-footer.svelte +1 -1
  405. package/src/shadcn/code-block/code-block-header.svelte +1 -1
  406. package/src/shadcn/code-block/code-block-line.svelte +1 -1
  407. package/src/shadcn/code-block/code-block-title.svelte +1 -1
  408. package/src/shadcn/code-block/code-block.svelte +1 -1
  409. package/src/shadcn/command/command-dialog.svelte +1 -1
  410. package/src/shadcn/command/command-empty.svelte +1 -1
  411. package/src/shadcn/command/command-group.svelte +1 -1
  412. package/src/shadcn/command/command-input.svelte +2 -2
  413. package/src/shadcn/command/command-item.svelte +1 -1
  414. package/src/shadcn/command/command-link-item.svelte +1 -1
  415. package/src/shadcn/command/command-list.svelte +1 -1
  416. package/src/shadcn/command/command-separator.svelte +1 -1
  417. package/src/shadcn/command/command-shortcut.svelte +1 -1
  418. package/src/shadcn/command/command.svelte +1 -1
  419. package/src/shadcn/context-menu/context-menu-checkbox-item.svelte +1 -1
  420. package/src/shadcn/context-menu/context-menu-content.svelte +2 -2
  421. package/src/shadcn/context-menu/context-menu-group-heading.svelte +1 -1
  422. package/src/shadcn/context-menu/context-menu-item.svelte +1 -1
  423. package/src/shadcn/context-menu/context-menu-label.svelte +1 -1
  424. package/src/shadcn/context-menu/context-menu-radio-item.svelte +1 -1
  425. package/src/shadcn/context-menu/context-menu-separator.svelte +1 -1
  426. package/src/shadcn/context-menu/context-menu-shortcut.svelte +1 -1
  427. package/src/shadcn/context-menu/context-menu-sub-content.svelte +1 -1
  428. package/src/shadcn/context-menu/context-menu-sub-trigger.svelte +1 -1
  429. package/src/shadcn/context-menu/context-menu-trigger.svelte +1 -1
  430. package/src/shadcn/dialog/dialog-content.svelte +1 -1
  431. package/src/shadcn/dialog/dialog-description.svelte +1 -1
  432. package/src/shadcn/dialog/dialog-footer.svelte +1 -1
  433. package/src/shadcn/dialog/dialog-header.svelte +1 -1
  434. package/src/shadcn/dialog/dialog-overlay.svelte +1 -1
  435. package/src/shadcn/dialog/dialog-title.svelte +1 -1
  436. package/src/shadcn/dropdown-menu/dropdown-menu-checkbox-item.svelte +1 -1
  437. package/src/shadcn/dropdown-menu/dropdown-menu-content.svelte +1 -1
  438. package/src/shadcn/dropdown-menu/dropdown-menu-group-heading.svelte +1 -1
  439. package/src/shadcn/dropdown-menu/dropdown-menu-item.svelte +1 -1
  440. package/src/shadcn/dropdown-menu/dropdown-menu-label.svelte +1 -1
  441. package/src/shadcn/dropdown-menu/dropdown-menu-radio-item.svelte +1 -1
  442. package/src/shadcn/dropdown-menu/dropdown-menu-separator.svelte +1 -1
  443. package/src/shadcn/dropdown-menu/dropdown-menu-shortcut.svelte +1 -1
  444. package/src/shadcn/dropdown-menu/dropdown-menu-sub-content.svelte +1 -1
  445. package/src/shadcn/dropdown-menu/dropdown-menu-sub-trigger.svelte +1 -1
  446. package/src/shadcn/empty/empty-content.svelte +1 -1
  447. package/src/shadcn/empty/empty-description.svelte +1 -1
  448. package/src/shadcn/empty/empty-header.svelte +1 -1
  449. package/src/shadcn/empty/empty-media.svelte +1 -1
  450. package/src/shadcn/empty/empty-title.svelte +1 -1
  451. package/src/shadcn/empty/empty.svelte +1 -1
  452. package/src/shadcn/field/field-content.svelte +1 -1
  453. package/src/shadcn/field/field-description.svelte +1 -1
  454. package/src/shadcn/field/field-error.svelte +1 -1
  455. package/src/shadcn/field/field-group.svelte +1 -1
  456. package/src/shadcn/field/field-label.svelte +1 -1
  457. package/src/shadcn/field/field-legend.svelte +1 -1
  458. package/src/shadcn/field/field-separator.svelte +1 -1
  459. package/src/shadcn/field/field-set.svelte +1 -1
  460. package/src/shadcn/field/field-title.svelte +1 -1
  461. package/src/shadcn/field/field.svelte +1 -1
  462. package/src/shadcn/field/simple-field.svelte +1 -1
  463. package/src/shadcn/hover-card/hover-card-content.svelte +1 -1
  464. package/src/shadcn/input/input.svelte +1 -1
  465. package/src/shadcn/input/secret-input.svelte +1 -1
  466. package/src/shadcn/input-otp/input-otp-group.svelte +1 -1
  467. package/src/shadcn/input-otp/input-otp-separator.svelte +1 -1
  468. package/src/shadcn/input-otp/input-otp-slot.svelte +1 -1
  469. package/src/shadcn/input-otp/input-otp.svelte +1 -1
  470. package/src/shadcn/kbd/kbd-group.svelte +1 -1
  471. package/src/shadcn/kbd/kbd.svelte +1 -1
  472. package/src/shadcn/label/label.svelte +1 -1
  473. package/src/shadcn/marker/marker-content.svelte +1 -1
  474. package/src/shadcn/marker/marker-icon.svelte +1 -1
  475. package/src/shadcn/marker/marker.svelte +1 -1
  476. package/src/shadcn/message/message-avatar.svelte +1 -1
  477. package/src/shadcn/message/message-content.svelte +1 -1
  478. package/src/shadcn/message/message-footer.svelte +1 -1
  479. package/src/shadcn/message/message-group.svelte +1 -1
  480. package/src/shadcn/message/message-header.svelte +1 -1
  481. package/src/shadcn/message/message.svelte +1 -1
  482. package/src/shadcn/message-scroller/message-scroller-button.svelte +1 -1
  483. package/src/shadcn/message-scroller/message-scroller-content.svelte +1 -1
  484. package/src/shadcn/message-scroller/message-scroller-item.svelte +1 -1
  485. package/src/shadcn/message-scroller/message-scroller-viewport.svelte +1 -1
  486. package/src/shadcn/message-scroller/message-scroller.svelte +1 -1
  487. package/src/shadcn/popover/popover-content.svelte +1 -1
  488. package/src/shadcn/popover/popover-description.svelte +1 -1
  489. package/src/shadcn/popover/popover-header.svelte +1 -1
  490. package/src/shadcn/popover/popover-title.svelte +1 -1
  491. package/src/shadcn/popover/popover-trigger.svelte +1 -1
  492. package/src/shadcn/progress/progress.svelte +1 -1
  493. package/src/shadcn/radio-group/radio-group-item.svelte +1 -1
  494. package/src/shadcn/radio-group/radio-group.svelte +1 -1
  495. package/src/shadcn/scroll-area/scroll-area-scrollbar.svelte +1 -1
  496. package/src/shadcn/scroll-area/scroll-area.svelte +1 -1
  497. package/src/shadcn/select/select-content.svelte +2 -2
  498. package/src/shadcn/select/select-group-heading.svelte +1 -1
  499. package/src/shadcn/select/select-group.svelte +1 -1
  500. package/src/shadcn/select/select-item.svelte +1 -1
  501. package/src/shadcn/select/select-label.svelte +1 -1
  502. package/src/shadcn/select/select-scroll-down-button.svelte +1 -1
  503. package/src/shadcn/select/select-scroll-up-button.svelte +1 -1
  504. package/src/shadcn/select/select-separator.svelte +1 -1
  505. package/src/shadcn/select/select-trigger.svelte +1 -1
  506. package/src/shadcn/separator/separator.svelte +1 -1
  507. package/src/shadcn/sheet/sheet-content.svelte +1 -1
  508. package/src/shadcn/sheet/sheet-description.svelte +1 -1
  509. package/src/shadcn/sheet/sheet-footer.svelte +1 -1
  510. package/src/shadcn/sheet/sheet-header.svelte +1 -1
  511. package/src/shadcn/sheet/sheet-overlay.svelte +1 -1
  512. package/src/shadcn/sheet/sheet-title.svelte +1 -1
  513. package/src/shadcn/sidebar/sidebar-content.svelte +1 -1
  514. package/src/shadcn/sidebar/sidebar-footer.svelte +1 -1
  515. package/src/shadcn/sidebar/sidebar-group-action.svelte +1 -1
  516. package/src/shadcn/sidebar/sidebar-group-content.svelte +1 -1
  517. package/src/shadcn/sidebar/sidebar-group-label.svelte +1 -1
  518. package/src/shadcn/sidebar/sidebar-group.svelte +1 -1
  519. package/src/shadcn/sidebar/sidebar-header.svelte +1 -1
  520. package/src/shadcn/sidebar/sidebar-input.svelte +1 -1
  521. package/src/shadcn/sidebar/sidebar-inset.svelte +1 -1
  522. package/src/shadcn/sidebar/sidebar-menu-action.svelte +1 -1
  523. package/src/shadcn/sidebar/sidebar-menu-badge.svelte +1 -1
  524. package/src/shadcn/sidebar/sidebar-menu-button.svelte +1 -1
  525. package/src/shadcn/sidebar/sidebar-menu-item.svelte +1 -1
  526. package/src/shadcn/sidebar/sidebar-menu-skeleton.svelte +1 -1
  527. package/src/shadcn/sidebar/sidebar-menu-sub-button.svelte +1 -1
  528. package/src/shadcn/sidebar/sidebar-menu-sub-item.svelte +1 -1
  529. package/src/shadcn/sidebar/sidebar-menu-sub.svelte +1 -1
  530. package/src/shadcn/sidebar/sidebar-menu.svelte +1 -1
  531. package/src/shadcn/sidebar/sidebar-provider.svelte +1 -1
  532. package/src/shadcn/sidebar/sidebar-rail.svelte +1 -1
  533. package/src/shadcn/sidebar/sidebar-separator.svelte +1 -1
  534. package/src/shadcn/sidebar/sidebar-trigger.svelte +1 -1
  535. package/src/shadcn/sidebar/sidebar.svelte +1 -1
  536. package/src/shadcn/skeleton/skeleton.svelte +1 -1
  537. package/src/shadcn/slider/slider.svelte +1 -1
  538. package/src/shadcn/spinner/spinner.svelte +1 -1
  539. package/src/shadcn/switch/switch.svelte +1 -1
  540. package/src/shadcn/table/table-body.svelte +1 -1
  541. package/src/shadcn/table/table-caption.svelte +1 -1
  542. package/src/shadcn/table/table-cell.svelte +1 -1
  543. package/src/shadcn/table/table-footer.svelte +1 -1
  544. package/src/shadcn/table/table-head.svelte +1 -1
  545. package/src/shadcn/table/table-header.svelte +1 -1
  546. package/src/shadcn/table/table-row.svelte +1 -1
  547. package/src/shadcn/table/table.svelte +1 -1
  548. package/src/shadcn/tabs/tabs-content.svelte +1 -1
  549. package/src/shadcn/tabs/tabs-list.svelte +1 -1
  550. package/src/shadcn/tabs/tabs-trigger.svelte +1 -1
  551. package/src/shadcn/tabs/tabs.svelte +1 -1
  552. package/src/shadcn/textarea/textarea.svelte +1 -1
  553. package/src/shadcn/toggle/toggle.svelte +1 -1
  554. package/src/shadcn/toggle-group/toggle-group-item.svelte +1 -1
  555. package/src/shadcn/toggle-group/toggle-group.svelte +1 -1
  556. package/src/shadcn/tooltip/tooltip-content.svelte +2 -2
  557. package/src/shadcn/trace/trace-collapsible-content.svelte +1 -1
  558. package/src/shadcn/trace/trace-collapsible-trigger.svelte +1 -1
  559. package/src/shadcn/trace/trace-collapsible.svelte +1 -1
  560. package/src/shadcn/trace/trace-description.svelte +1 -1
  561. package/src/shadcn/trace/trace-icon.svelte +1 -1
  562. package/src/shadcn/trace/trace-line.svelte +1 -1
  563. package/src/shadcn/trace/trace-title.svelte +1 -1
  564. package/src/shadcn/trace/trace.svelte +1 -1
  565. package/src/slides/Controls.svelte +138 -0
  566. package/src/slides/Deck.svelte +271 -0
  567. package/src/slides/Fragment.svelte +71 -0
  568. package/src/slides/Overview.svelte +113 -0
  569. package/src/slides/Presenter.svelte +292 -0
  570. package/src/slides/README.md +89 -0
  571. package/src/slides/Slide.svelte +179 -0
  572. package/src/slides/SlideList.svelte +19 -0
  573. package/src/slides/ambient.d.ts +1 -0
  574. package/src/slides/bridges.ts +69 -0
  575. package/src/slides/deck-controller.svelte.ts +292 -0
  576. package/src/slides/demo-slides.svelte +358 -0
  577. package/src/slides/demo.svelte +176 -0
  578. package/src/slides/export/capture.ts +113 -0
  579. package/src/slides/export/ffmpeg.ts +120 -0
  580. package/src/slides/export/index.ts +9 -0
  581. package/src/slides/export/launch.ts +10 -0
  582. package/src/slides/export/pdf.ts +152 -0
  583. package/src/slides/export/types.ts +48 -0
  584. package/src/slides/index.ts +19 -0
  585. package/src/slides/labels.ts +48 -0
  586. package/src/slides/layout.ts +51 -0
  587. package/src/slides/model.ts +47 -0
  588. package/src/slides/navigation.ts +100 -0
  589. package/src/slides/theme.css +205 -0
  590. package/src/slides/types.ts +86 -0
  591. package/src/tasks/README.md +312 -0
  592. package/src/tasks/ambient.d.ts +3 -0
  593. package/src/tasks/board-order.ts +137 -0
  594. package/src/tasks/dates.ts +31 -0
  595. package/src/tasks/define-statuses.ts +32 -0
  596. package/src/tasks/demo.svelte +389 -0
  597. package/src/tasks/detail/detail-demo.svelte +186 -0
  598. package/src/tasks/detail/detail-types.ts +54 -0
  599. package/src/tasks/detail/task-chip.svelte +28 -0
  600. package/src/tasks/detail/task-detail.svelte +315 -0
  601. package/src/tasks/detail/task-property.svelte +17 -0
  602. package/src/tasks/detail/task-thread.svelte +258 -0
  603. package/src/tasks/gantt/gantt-bar.svelte +243 -0
  604. package/src/tasks/gantt/gantt-grid.svelte +94 -0
  605. package/src/tasks/gantt/gantt-group-row.svelte +55 -0
  606. package/src/tasks/gantt/gantt-header.svelte +71 -0
  607. package/src/tasks/gantt/gantt-task-row.svelte +186 -0
  608. package/src/tasks/gantt/gantt.css +22 -0
  609. package/src/tasks/gantt/gantt.test.ts +77 -0
  610. package/src/tasks/gantt/logic.test.ts +163 -0
  611. package/src/tasks/gantt/logic.ts +248 -0
  612. package/src/tasks/inbox/inbox-card.svelte +283 -0
  613. package/src/tasks/inbox/inbox-draft.svelte +84 -0
  614. package/src/tasks/inbox/inbox-logic.ts +55 -0
  615. package/src/tasks/inbox/inbox-statuses.ts +48 -0
  616. package/src/tasks/inbox/inbox-trace.svelte +45 -0
  617. package/src/tasks/inbox/inbox-types.ts +108 -0
  618. package/src/tasks/inbox/inbox.svelte +203 -0
  619. package/src/tasks/inbox/index.ts +23 -0
  620. package/src/tasks/index.ts +85 -0
  621. package/src/tasks/kanban-board.svelte +561 -0
  622. package/src/tasks/kanban-column.svelte +117 -0
  623. package/src/tasks/kanban-drag.ts +38 -0
  624. package/src/tasks/kanban-types.ts +65 -0
  625. package/src/tasks/presets/sales.ts +16 -0
  626. package/src/tasks/record/demo-data.ts +286 -0
  627. package/src/tasks/record/field-section.svelte +53 -0
  628. package/src/tasks/record/fields.ts +142 -0
  629. package/src/tasks/record/format.ts +87 -0
  630. package/src/tasks/record/gutter.ts +2 -0
  631. package/src/tasks/record/index.ts +54 -0
  632. package/src/tasks/record/info-list.svelte +316 -0
  633. package/src/tasks/record/kinds.ts +42 -0
  634. package/src/tasks/record/logic.ts +324 -0
  635. package/src/tasks/record/next-up.svelte +70 -0
  636. package/src/tasks/record/project-list.svelte +133 -0
  637. package/src/tasks/record/record-demo.svelte +324 -0
  638. package/src/tasks/record/record-header.svelte +70 -0
  639. package/src/tasks/record/record-shell.svelte +138 -0
  640. package/src/tasks/record/record-stats.svelte +48 -0
  641. package/src/tasks/record/record-timeline.svelte +231 -0
  642. package/src/tasks/record/record.test.ts +350 -0
  643. package/src/tasks/record/steps-rail.svelte +474 -0
  644. package/src/tasks/record/types.ts +280 -0
  645. package/src/tasks/record/where-we-are.svelte +121 -0
  646. package/src/tasks/reorder.ts +19 -0
  647. package/src/tasks/side-panel-types.ts +16 -0
  648. package/src/tasks/side-panel.svelte +193 -0
  649. package/src/tasks/status-badge.svelte +63 -0
  650. package/src/tasks/status-dot.svelte +228 -0
  651. package/src/tasks/status-icon.svelte +30 -0
  652. package/src/tasks/status-types.ts +40 -0
  653. package/src/tasks/task-gantt.svelte +230 -0
  654. package/src/tasks/task-list.svelte +470 -0
  655. package/src/tasks/task-types.ts +33 -0
  656. package/src/tasks/time/duration.ts +69 -0
  657. package/src/tasks/time/time-chip.svelte +62 -0
  658. package/src/tasks/time/time-dot.svelte +58 -0
  659. package/src/tasks/time/time-ring.svelte +74 -0
  660. package/src/tasks/time/time-tracker.svelte +565 -0
  661. package/src/tasks/time/time-types.ts +74 -0
  662. package/src/tasks/use-kanban-dnd.svelte.ts +173 -0
  663. package/src/tokens/base.css +5 -5
  664. package/src/tokens/palette.css +115 -120
@@ -0,0 +1,68 @@
1
+ import { describe, expect, it } from "vite-plus/test";
2
+ import type { Node } from "@xyflow/svelte";
3
+ import { deletableIds, refuseLockedOps, toggleLock, withLockFlags } from "./lock.js";
4
+
5
+ function card(id: string, data: Record<string, unknown> = {}, extra: Partial<Node> = {}): Node {
6
+ return { id, type: "note", position: { x: 0, y: 0 }, data, ...extra };
7
+ }
8
+
9
+ describe("refuseLockedOps", () => {
10
+ const nodes = [card("free"), card("held", { locked: true })];
11
+
12
+ it("drops moves, edits and removals on a locked card", () => {
13
+ const ops = [
14
+ { kind: "moveNode", nodeId: "held", position: { x: 1, y: 1 } },
15
+ { kind: "patchNode", nodeId: "held", data: { content: "x" } },
16
+ { kind: "removeNode", nodeId: "held" },
17
+ { kind: "moveNode", nodeId: "free", position: { x: 1, y: 1 } },
18
+ ];
19
+ expect(refuseLockedOps(nodes, ops)).toEqual([ops[3]]);
20
+ });
21
+
22
+ it("lets a locked card be unlocked and linked", () => {
23
+ const ops = [
24
+ { kind: "patchNode", nodeId: "held", data: { locked: false } },
25
+ { kind: "addEdge", edge: { source: "held", target: "free" } },
26
+ ];
27
+ expect(refuseLockedOps(nodes, ops)).toEqual(ops);
28
+ });
29
+
30
+ it("refuses an unlock that also edits the card", () => {
31
+ const op = { kind: "patchNode", nodeId: "held", data: { locked: false, content: "x" } };
32
+ expect(refuseLockedOps(nodes, [op])).toEqual([]);
33
+ });
34
+ });
35
+
36
+ describe("toggleLock", () => {
37
+ it("locks, then unlocks when every pick is locked", () => {
38
+ const board = [card("a"), card("b", { locked: true })];
39
+ const locked = toggleLock(board, new Set(["a", "b"]));
40
+ expect(locked?.locked).toBe(true);
41
+ expect(locked?.nodes.every((n) => n.draggable === false && n.deletable === false)).toBe(true);
42
+ const unlocked = toggleLock(locked?.nodes ?? [], new Set(["a", "b"]));
43
+ expect(unlocked?.locked).toBe(false);
44
+ expect(unlocked?.nodes.some((n) => "draggable" in n)).toBe(false);
45
+ });
46
+
47
+ it("does nothing for an empty pick", () => {
48
+ expect(toggleLock([card("a")], new Set())).toBeNull();
49
+ });
50
+ });
51
+
52
+ describe("deletableIds", () => {
53
+ it("keeps a locked card and the group that holds one", () => {
54
+ const board = [
55
+ card("g", {}, { type: "wrapper" }),
56
+ card("held", { locked: true }, { parentId: "g" }),
57
+ card("free"),
58
+ ];
59
+ expect(deletableIds(board, ["g", "held", "free"])).toEqual(["free"]);
60
+ });
61
+ });
62
+
63
+ describe("withLockFlags", () => {
64
+ it("leaves a synced copy's own flags alone", () => {
65
+ const copy = card("c", { syncedCopy: true, locked: true });
66
+ expect(withLockFlags(copy)).toBe(copy);
67
+ });
68
+ });
@@ -0,0 +1,87 @@
1
+ // A locked card can be selected, copied and connected, and nothing else. `data.locked` saves,
2
+ // rebases and undoes with the card.
3
+
4
+ import type { Node } from "@xyflow/svelte";
5
+ import type { OpLike } from "../plugin.js";
6
+
7
+ export function isLocked(node: { data?: unknown } | null | undefined): boolean {
8
+ const data = node?.data as Record<string, unknown> | undefined;
9
+ return data?.locked === true;
10
+ }
11
+
12
+ /** What xyflow needs to hold a locked card still. */
13
+ export const LOCK_FLAGS = { draggable: false, deletable: false } as const;
14
+
15
+ /** The card with its flow flags matching `data.locked`. A synced copy keeps its own flags. */
16
+ export function withLockFlags<N extends Node>(node: N): N {
17
+ if ((node.data as Record<string, unknown> | undefined)?.syncedCopy) return node;
18
+ if (isLocked(node)) return { ...node, ...LOCK_FLAGS };
19
+ if (node.draggable !== false && node.deletable !== false) return node;
20
+ const { draggable: _drag, deletable: _delete, ...rest } = node;
21
+ return rest as N;
22
+ }
23
+
24
+ /** `locked` alone is the one data change a locked card takes: it is how it comes unlocked. */
25
+ function onlyUnlocks(op: OpLike): boolean {
26
+ if (op.kind !== "patchNode") return false;
27
+ const patch = op as {
28
+ data?: Record<string, unknown>;
29
+ style?: unknown;
30
+ width?: unknown;
31
+ height?: unknown;
32
+ };
33
+ return (
34
+ patch.data !== undefined &&
35
+ patch.style === undefined &&
36
+ patch.width === undefined &&
37
+ patch.height === undefined &&
38
+ Object.keys(patch.data).every((key) => key === "locked")
39
+ );
40
+ }
41
+
42
+ /**
43
+ * The one gate every dispatched edit passes: ops that don't reach a locked card. An op on an
44
+ * arrow, or a card not on the board, passes.
45
+ */
46
+ export function refuseLockedOps<Op extends OpLike>(
47
+ nodes: readonly Node[],
48
+ ops: readonly Op[],
49
+ ): Op[] {
50
+ const locked = new Set(nodes.filter(isLocked).map((node) => node.id));
51
+ if (locked.size === 0) return [...ops];
52
+ return ops.filter((op) => {
53
+ const nodeId = (op as { nodeId?: unknown }).nodeId;
54
+ return typeof nodeId !== "string" || !locked.has(nodeId) || onlyUnlocks(op);
55
+ });
56
+ }
57
+
58
+ /** Of these ids, the ones a delete may take: not locked, and not a group holding something locked. */
59
+ export function deletableIds(nodes: readonly Node[], ids: readonly string[]): string[] {
60
+ const parentOf = new Map(nodes.map((node) => [node.id, node.parentId]));
61
+ const holdsLocked = new Set<string>();
62
+ for (const node of nodes) {
63
+ if (!isLocked(node)) continue;
64
+ for (let up = node.parentId; up; up = parentOf.get(up)) holdsLocked.add(up);
65
+ }
66
+ const byId = new Map(nodes.map((node) => [node.id, node]));
67
+ return ids.filter((id) => !isLocked(byId.get(id)) && !holdsLocked.has(id));
68
+ }
69
+
70
+ /** The board with the picked cards locked, or all unlocked when every one of them already was locked. */
71
+ export function toggleLock(
72
+ nodes: readonly Node[],
73
+ picked: ReadonlySet<string>,
74
+ ): { nodes: Node[]; locked: boolean } | null {
75
+ const targets = nodes.filter(
76
+ (node) => picked.has(node.id) && !(node.data as { syncedCopy?: unknown }).syncedCopy,
77
+ );
78
+ if (targets.length === 0) return null;
79
+ const locked = !targets.every(isLocked);
80
+ const ids = new Set(targets.map((node) => node.id));
81
+ return {
82
+ locked,
83
+ nodes: nodes.map((node) =>
84
+ ids.has(node.id) ? withLockFlags({ ...node, data: { ...node.data, locked } }) : node,
85
+ ),
86
+ };
87
+ }
@@ -0,0 +1,208 @@
1
+ import { expectDefined } from "../test-utils.js";
2
+ import { describe, expect, it } from "vite-plus/test";
3
+ import { applyOps } from "../ops/ops.js";
4
+ import { diffToOps, rebaseDiagram, type DiagramDoc } from "./rebase.js";
5
+
6
+ function doc(nodes: DiagramDoc["nodes"], edges: DiagramDoc["edges"] = []): DiagramDoc {
7
+ return { nodes, edges };
8
+ }
9
+
10
+ function n(id: string, overrides: Record<string, unknown> = {}): DiagramDoc["nodes"][number] {
11
+ return { id, type: "node", position: { x: 0, y: 0 }, data: { label: id }, ...overrides };
12
+ }
13
+
14
+ describe("diffToOps", () => {
15
+ it("returns no ops for identical docs", () => {
16
+ expect(diffToOps(doc([n("a")]), doc([n("a")]))).toEqual([]);
17
+ });
18
+
19
+ it("replaying the diff on the base reproduces the current doc", () => {
20
+ const base = doc(
21
+ [n("stay"), n("move"), n("gone"), n("container", { type: "space" })],
22
+ [{ id: "e1", source: "stay", target: "move" }],
23
+ );
24
+ const current = doc(
25
+ [
26
+ n("stay"),
27
+ n("move", { position: { x: 120, y: 80 }, data: { label: "renamed" } }),
28
+ n("container", { type: "space", style: { width: 400, height: 300 } }),
29
+ n("child", { parentId: "container", position: { x: 20, y: 60 } }),
30
+ ],
31
+ [
32
+ { id: "e1", source: "stay", target: "move", label: "flows" },
33
+ { id: "e2", source: "stay", target: "child" },
34
+ ],
35
+ );
36
+
37
+ const ops = diffToOps(base, current);
38
+ const replayed = applyOps({ nodes: base.nodes, edges: base.edges }, ops);
39
+
40
+ const byId = new Map(replayed.nodes.map((node) => [node.id, node]));
41
+ expect(byId.has("gone")).toBe(false);
42
+ expect(byId.get("move")?.position).toEqual({ x: 120, y: 80 });
43
+ expect(
44
+ (expectDefined(byId.get("move"), "entry move").data as Record<string, unknown>).label,
45
+ ).toBe("renamed");
46
+ expect(byId.get("container")?.style).toEqual({ width: 400, height: 300 });
47
+ expect(byId.get("child")?.parentId).toBe("container");
48
+ // parentId implies extent at the op seam
49
+ expect(byId.get("child")?.extent).toBe("parent");
50
+ const edgeById = new Map(replayed.edges.map((edge) => [edge.id, edge]));
51
+ expect(edgeById.get("e1")?.label).toBe("flows");
52
+ expect(edgeById.has("e2")).toBe(true);
53
+ });
54
+
55
+ it("orders added parents before added children", () => {
56
+ const base = doc([]);
57
+ const current = doc([
58
+ n("child", { parentId: "parent", position: { x: 10, y: 10 } }),
59
+ n("parent", { type: "space" }),
60
+ ]);
61
+ // Would throw "Unknown parentId" if the child op ran first.
62
+ const replayed = applyOps({ nodes: [], edges: [] }, diffToOps(base, current));
63
+ expect(replayed.nodes).toHaveLength(2);
64
+ });
65
+ });
66
+
67
+ describe("rebaseDiagram", () => {
68
+ const lastSaved = doc([n("a"), n("b")]);
69
+
70
+ it("replays local changes on top of the server doc", () => {
71
+ // Server: an agent added "agent-node". Local: the user renamed "a".
72
+ const serverDoc = doc([n("a"), n("b"), n("agent-node")]);
73
+ const local = doc([n("a", { data: { label: "renamed" } }), n("b")]);
74
+
75
+ const rebased = expectDefined(rebaseDiagram(serverDoc, lastSaved, local), "rebased doc");
76
+ const byId = new Map(rebased.nodes.map((node) => [node.id, node]));
77
+ expect(byId.has("agent-node")).toBe(true);
78
+ expect((expectDefined(byId.get("a"), "entry a").data as Record<string, unknown>).label).toBe(
79
+ "renamed",
80
+ );
81
+ });
82
+
83
+ it("returns the server doc unchanged when there are no local changes", () => {
84
+ const serverDoc = doc([n("a"), n("b"), n("agent-node")]);
85
+ const rebased = expectDefined(rebaseDiagram(serverDoc, lastSaved, lastSaved), "rebased doc");
86
+ expect(rebased.nodes.map((node) => node.id)).toEqual(["a", "b", "agent-node"]);
87
+ });
88
+
89
+ it("keeps a locally re-pointed edge endpoint and handles", () => {
90
+ const base = doc([n("a"), n("b"), n("c")], [{ id: "e1", source: "a", target: "b" }]);
91
+ const server = doc(
92
+ [n("a"), n("b"), n("c"), n("agent")],
93
+ [{ id: "e1", source: "a", target: "b" }],
94
+ );
95
+ const local = doc(
96
+ [n("a"), n("b"), n("c")],
97
+ [{ id: "e1", source: "a", target: "c", targetHandle: "left" }],
98
+ );
99
+
100
+ const rebased = expectDefined(rebaseDiagram(server, base, local), "rebased doc");
101
+ expect(rebased.edges).toEqual([{ id: "e1", source: "a", target: "c", targetHandle: "left" }]);
102
+ });
103
+
104
+ it("returns null when a re-pointed edge's new endpoint was deleted on the server", () => {
105
+ const base = doc([n("a"), n("b"), n("c")], [{ id: "e1", source: "a", target: "b" }]);
106
+ const server = doc([n("a"), n("b")], [{ id: "e1", source: "a", target: "b" }]);
107
+ const local = doc([n("a"), n("b"), n("c")], [{ id: "e1", source: "a", target: "c" }]);
108
+
109
+ expect(rebaseDiagram(server, base, local)).toBeNull();
110
+ });
111
+
112
+ it("patches zIndex and type changes, and a removed zIndex", () => {
113
+ const base = doc([n("a", { zIndex: 1 }), n("b"), n("c", { zIndex: 4 })]);
114
+ const server = doc([n("a", { zIndex: 1 }), n("b"), n("c", { zIndex: 4 }), n("agent")]);
115
+ const local = doc([n("a", { zIndex: 9 }), n("b", { type: "sticky" }), n("c")]);
116
+
117
+ const rebased = expectDefined(rebaseDiagram(server, base, local), "rebased doc");
118
+ const byId = new Map(rebased.nodes.map((node) => [node.id, node]));
119
+ expect(byId.get("a")?.zIndex).toBe(9);
120
+ expect(byId.get("b")?.type).toBe("sticky");
121
+ expect(byId.get("c")).not.toHaveProperty("zIndex");
122
+ expect(byId.has("agent")).toBe(true);
123
+ });
124
+
125
+ it("keeps both sides' edits to different data keys of the same node", () => {
126
+ const base = doc([n("a", { data: { label: "a", note: "old" } })]);
127
+ const server = doc([n("a", { data: { label: "a", note: "server note" } })]);
128
+ const local = doc([n("a", { data: { label: "local label", note: "old" } })]);
129
+
130
+ const rebased = expectDefined(rebaseDiagram(server, base, local), "rebased doc");
131
+ expect(rebased.nodes[0].data).toEqual({ label: "local label", note: "server note" });
132
+ });
133
+
134
+ it("lets local win when both sides edit the same data key", () => {
135
+ const base = doc([n("a", { data: { label: "a" } })]);
136
+ const server = doc([n("a", { data: { label: "server" } })]);
137
+ const local = doc([n("a", { data: { label: "local" } })]);
138
+
139
+ const rebased = expectDefined(rebaseDiagram(server, base, local), "rebased doc");
140
+ expect(rebased.nodes[0].data).toEqual({ label: "local" });
141
+ });
142
+
143
+ it("replaces a changed top-level data key whole, without merging inside it", () => {
144
+ const base = doc([n("a", { data: { shape: { kind: "box", fill: "red" } } })]);
145
+ const server = doc([n("a", { data: { shape: { kind: "box", fill: "red", ring: 2 } } })]);
146
+ const local = doc([n("a", { data: { shape: { kind: "box", fill: "blue" } } })]);
147
+
148
+ const rebased = expectDefined(rebaseDiagram(server, base, local), "rebased doc");
149
+ expect(rebased.nodes[0].data).toEqual({ shape: { kind: "box", fill: "blue" } });
150
+ });
151
+
152
+ it("removes a data key the local side deleted, and keeps the server's other keys", () => {
153
+ const base = doc([n("a", { data: { label: "a", icon: "x", note: "old" } })]);
154
+ const server = doc([n("a", { data: { label: "a", icon: "x", note: "server note" } })]);
155
+ const local = doc([n("a", { data: { label: "a", note: "old" } })]);
156
+
157
+ const rebased = expectDefined(rebaseDiagram(server, base, local), "rebased doc");
158
+ expect(rebased.nodes[0].data).toEqual({ label: "a", note: "server note" });
159
+ expect(rebased.nodes[0].data).not.toHaveProperty("icon");
160
+ });
161
+
162
+ it("returns null when a field-only change hits a node the server deleted", () => {
163
+ const base = doc([n("a"), n("b")]);
164
+ const server = doc([n("a")]);
165
+ const local = doc([n("a"), n("b", { zIndex: 3 })]);
166
+
167
+ expect(rebaseDiagram(server, base, local)).toBeNull();
168
+ });
169
+
170
+ it("keeps parents before children when a local reparent targets a later server node", () => {
171
+ const base = doc([n("child"), n("box", { type: "space" })]);
172
+ const server = doc([n("child"), n("box", { type: "space" })]);
173
+ const local = doc([
174
+ n("box", { type: "space" }),
175
+ n("child", { parentId: "box", position: { x: 10, y: 10 } }),
176
+ ]);
177
+
178
+ const rebased = expectDefined(rebaseDiagram(server, base, local), "rebased doc");
179
+ const ids = rebased.nodes.map((node) => node.id);
180
+ expect(ids.indexOf("box")).toBeLessThan(ids.indexOf("child"));
181
+ });
182
+
183
+ it("returns null when the replay is impossible", () => {
184
+ // Server deleted "b"; local patched it. patchNode on a missing node throws.
185
+ const serverDoc = doc([n("a")]);
186
+ const local = doc([n("a"), n("b", { data: { label: "edited" } })]);
187
+ expect(rebaseDiagram(serverDoc, lastSaved, local)).toBeNull();
188
+ });
189
+ });
190
+
191
+ describe("a new edge keeps everything it was drawn with", () => {
192
+ it("carries label, animated, style and data through a rebase", () => {
193
+ const base = doc([n("a"), n("b")]);
194
+ const styled = {
195
+ id: "e9",
196
+ source: "a",
197
+ target: "b",
198
+ type: "smoothstep",
199
+ label: "pays",
200
+ animated: true,
201
+ style: { stroke: "#f00", strokeWidth: 3 },
202
+ data: { dash: "dotted", arrow: "both" },
203
+ };
204
+ const local = doc([n("a"), n("b")], [styled]);
205
+ const rebased = rebaseDiagram(doc([n("a"), n("b"), n("c")]), base, local);
206
+ expect(rebased?.edges).toEqual([styled]);
207
+ });
208
+ });
@@ -0,0 +1,272 @@
1
+ // Conflict rebase for the autosave PUT. On a 409, local changes since the last
2
+ // save replay as ops on the server document, instead of overwriting the other writer.
3
+
4
+ import { tryCatch } from "../../lib/result.js";
5
+ import { applyOps, type CanvasOp } from "../ops/ops.js";
6
+ import type { RawCanvasData } from "../types.js";
7
+ import { sortParentsFirst } from "../headless/canvas-cleanup.js";
8
+
9
+ type DocNode = Record<string, unknown> & {
10
+ id: string;
11
+ position?: { x: number; y: number };
12
+ parentId?: string;
13
+ data?: Record<string, unknown>;
14
+ style?: unknown;
15
+ width?: unknown;
16
+ height?: unknown;
17
+ type?: unknown;
18
+ };
19
+ type DocEdge = Record<string, unknown> & { id: string };
20
+ export type DiagramDoc = { nodes: DocNode[]; edges: DocEdge[] };
21
+
22
+ const EDGE_PATCH_FIELDS = [
23
+ "source",
24
+ "target",
25
+ "label",
26
+ "animated",
27
+ "type",
28
+ "style",
29
+ "sourceHandle",
30
+ "targetHandle",
31
+ ] as const;
32
+
33
+ function json(value: unknown): string {
34
+ return JSON.stringify(value ?? null);
35
+ }
36
+
37
+ type SizePatch = { style?: Record<string, unknown>; width?: number; height?: number };
38
+
39
+ function objectStyle(node: DocNode): Record<string, unknown> | undefined {
40
+ return node.style && typeof node.style === "object"
41
+ ? (node.style as Record<string, unknown>)
42
+ : undefined;
43
+ }
44
+
45
+ function numberOrUndefined(value: unknown): number | undefined {
46
+ return typeof value === "number" ? value : undefined;
47
+ }
48
+
49
+ function addNodeOp(node: DocNode): CanvasOp {
50
+ const style = objectStyle(node);
51
+ const width = numberOrUndefined(node.width);
52
+ const height = numberOrUndefined(node.height);
53
+ return {
54
+ kind: "addNode",
55
+ node: {
56
+ id: node.id,
57
+ type: String(node.type ?? "node"),
58
+ position: node.position ?? { x: 0, y: 0 },
59
+ data: node.data ?? {},
60
+ ...(style ? { style } : {}),
61
+ ...(width !== undefined ? { width } : {}),
62
+ ...(height !== undefined ? { height } : {}),
63
+ ...(node.parentId ? { parentId: node.parentId } : {}),
64
+ },
65
+ };
66
+ }
67
+
68
+ function sizePatchOf(before: DocNode, node: DocNode): SizePatch {
69
+ const patch: SizePatch = {};
70
+ const style = objectStyle(node);
71
+ if (style && json(before.style) !== json(node.style)) patch.style = style;
72
+ const width = numberOrUndefined(node.width);
73
+ if (width !== undefined && before.width !== node.width) patch.width = width;
74
+ const height = numberOrUndefined(node.height);
75
+ if (height !== undefined && before.height !== node.height) patch.height = height;
76
+ return patch;
77
+ }
78
+
79
+ /** Only the top-level keys local changed, so a key the server edited alone keeps its value. */
80
+ function changedData(
81
+ before: Record<string, unknown> | undefined,
82
+ data: Record<string, unknown> | undefined,
83
+ ): Record<string, unknown> | undefined {
84
+ if (!data) return undefined;
85
+ const changed = Object.entries(data).filter(
86
+ ([key, value]) => json(before?.[key]) !== json(value),
87
+ );
88
+ return changed.length > 0 ? Object.fromEntries(changed) : undefined;
89
+ }
90
+
91
+ function changedNodeOps(before: DocNode, node: DocNode): CanvasOp[] {
92
+ const ops: CanvasOp[] = [];
93
+ if (node.position && json(before.position) !== json(node.position)) {
94
+ ops.push({ kind: "moveNode", nodeId: node.id, position: node.position });
95
+ }
96
+ const parentId = node.parentId ?? null;
97
+ if ((before.parentId ?? null) !== parentId) {
98
+ ops.push({ kind: "setParent", nodeId: node.id, parentId });
99
+ }
100
+ const data = changedData(before.data, node.data);
101
+ if (data) ops.push({ kind: "patchNode", nodeId: node.id, data });
102
+ const sizePatch = sizePatchOf(before, node);
103
+ if (Object.keys(sizePatch).length > 0) {
104
+ ops.push({ kind: "patchNode", nodeId: node.id, ...sizePatch });
105
+ }
106
+ return ops;
107
+ }
108
+
109
+ function nodeOps(base: DiagramDoc, current: DiagramDoc): CanvasOp[] {
110
+ const baseNodes = new Map(base.nodes.map((n) => [n.id, n]));
111
+ const currentIds = new Set(current.nodes.map((n) => n.id));
112
+ const removed: CanvasOp[] = base.nodes
113
+ .filter((n) => !currentIds.has(n.id))
114
+ .map((n) => ({ kind: "removeNode", nodeId: n.id }));
115
+
116
+ // WHY parents first: an added child references its added parent in the same
117
+ // batch, and applyOps validates parent existence at each step.
118
+ const added = sortParentsFirst(current.nodes.filter((n) => !baseNodes.has(n.id))).map(addNodeOp);
119
+
120
+ const changed = current.nodes.flatMap((node) => {
121
+ const before = baseNodes.get(node.id);
122
+ return before ? changedNodeOps(before, node) : [];
123
+ });
124
+ return [...removed, ...added, ...changed];
125
+ }
126
+
127
+ function addEdgeOp(edge: DocEdge): CanvasOp {
128
+ const optional = (key: "sourceHandle" | "targetHandle" | "type" | "label") =>
129
+ typeof edge[key] === "string" ? { [key]: edge[key] } : {};
130
+ const record = (key: "style" | "data") => {
131
+ const value = edge[key];
132
+ return value && typeof value === "object" && !Array.isArray(value)
133
+ ? { [key]: value as Record<string, unknown> }
134
+ : {};
135
+ };
136
+ return {
137
+ kind: "addEdge",
138
+ edge: {
139
+ id: edge.id,
140
+ source: String(edge.source),
141
+ target: String(edge.target),
142
+ ...optional("sourceHandle"),
143
+ ...optional("targetHandle"),
144
+ ...optional("type"),
145
+ ...optional("label"),
146
+ ...(typeof edge.animated === "boolean" ? { animated: edge.animated } : {}),
147
+ ...record("style"),
148
+ ...record("data"),
149
+ },
150
+ };
151
+ }
152
+
153
+ function patchEdgeOps(before: DocEdge, edge: DocEdge): CanvasOp[] {
154
+ const patch: Record<string, unknown> = {};
155
+ for (const field of EDGE_PATCH_FIELDS) {
156
+ if (edge[field] !== undefined && json(before[field]) !== json(edge[field])) {
157
+ patch[field] = edge[field];
158
+ }
159
+ }
160
+ if (Object.keys(patch).length === 0) return [];
161
+ return [{ kind: "patchEdge", edgeId: edge.id, patch } as CanvasOp];
162
+ }
163
+
164
+ function edgeOps(base: DiagramDoc, current: DiagramDoc): CanvasOp[] {
165
+ const baseEdges = new Map(base.edges.map((e) => [e.id, e]));
166
+ const currentIds = new Set(current.edges.map((e) => e.id));
167
+ const removed: CanvasOp[] = base.edges
168
+ .filter((e) => !currentIds.has(e.id))
169
+ .map((e) => ({ kind: "removeEdge", edgeId: e.id }));
170
+ const upserted = current.edges.flatMap((edge) => {
171
+ const before = baseEdges.get(edge.id);
172
+ return before ? patchEdgeOps(before, edge) : [addEdgeOp(edge)];
173
+ });
174
+ return [...removed, ...upserted];
175
+ }
176
+
177
+ /**
178
+ * Diff two persisted documents into ops. Data patches carry only changed keys. A deleted
179
+ * data key, and a node's `type` or `zIndex`, have no op: `nodeFieldPatches` carries those.
180
+ */
181
+ export function diffToOps(base: DiagramDoc, current: DiagramDoc): CanvasOp[] {
182
+ return [...nodeOps(base, current), ...edgeOps(base, current)];
183
+ }
184
+
185
+ type NodeFieldPatch = {
186
+ nodeId: string;
187
+ set: Record<string, unknown>;
188
+ unset: string[];
189
+ unsetData: string[];
190
+ };
191
+
192
+ const NODE_FIELDS = ["type", "zIndex"] as const;
193
+
194
+ /** What changed on a node that `patchNode` can't say: `type`, `zIndex` and deleted data keys. */
195
+ function nodeFieldPatches(base: DiagramDoc, current: DiagramDoc): NodeFieldPatch[] {
196
+ const baseNodes = new Map(base.nodes.map((n) => [n.id, n]));
197
+ const patches: NodeFieldPatch[] = [];
198
+ for (const node of current.nodes) {
199
+ const before = baseNodes.get(node.id);
200
+ if (!before) continue;
201
+ const set: Record<string, unknown> = {};
202
+ const unset: string[] = [];
203
+ for (const field of NODE_FIELDS) {
204
+ if (json(before[field]) === json(node[field])) continue;
205
+ if (node[field] === undefined) unset.push(field);
206
+ else set[field] = node[field];
207
+ }
208
+ const unsetData = Object.keys(before.data ?? {}).filter((key) => !(key in (node.data ?? {})));
209
+ if (unset.length + unsetData.length + Object.keys(set).length > 0) {
210
+ patches.push({ nodeId: node.id, set, unset, unsetData });
211
+ }
212
+ }
213
+ return patches;
214
+ }
215
+
216
+ function withoutKeys(record: Record<string, unknown>, keys: string[]): Record<string, unknown> {
217
+ return Object.fromEntries(Object.entries(record).filter(([key]) => !keys.includes(key)));
218
+ }
219
+
220
+ /** Null when a patched node is gone, the same answer `applyOps` gives for an op on a missing node. */
221
+ function applyNodeFieldPatches(
222
+ nodes: Record<string, unknown>[],
223
+ patches: NodeFieldPatch[],
224
+ ): Record<string, unknown>[] | null {
225
+ const byId = new Map(patches.map((patch) => [patch.nodeId, patch]));
226
+ const seen = new Set<string>();
227
+ const next = nodes.map((node) => {
228
+ const patch = typeof node.id === "string" ? byId.get(node.id) : undefined;
229
+ if (!patch) return node;
230
+ seen.add(patch.nodeId);
231
+ const patched = withoutKeys({ ...node, ...patch.set }, patch.unset);
232
+ if (patch.unsetData.length === 0 || typeof node.data !== "object" || node.data === null) {
233
+ return patched;
234
+ }
235
+ return { ...patched, data: withoutKeys(node.data as Record<string, unknown>, patch.unsetData) };
236
+ });
237
+ return seen.size === byId.size ? next : null;
238
+ }
239
+
240
+ /** A local reparent can name a container that sits later in the server's order. */
241
+ function parentsFirst(nodes: Record<string, unknown>[]): Record<string, unknown>[] {
242
+ const ids = new Set(nodes.map((node) => node.id));
243
+ const seen = new Set<unknown>();
244
+ const misordered = nodes.some((node) => {
245
+ const early = ids.has(node.parentId) && !seen.has(node.parentId);
246
+ seen.add(node.id);
247
+ return early;
248
+ });
249
+ if (!misordered) return nodes;
250
+ const wrapped = nodes.map((node) => ({
251
+ id: String(node.id),
252
+ parentId: typeof node.parentId === "string" ? node.parentId : undefined,
253
+ node,
254
+ }));
255
+ return sortParentsFirst(wrapped).map(({ node }) => node);
256
+ }
257
+
258
+ /**
259
+ * Replay local changes (lastSaved -> local) on the server document. Returns
260
+ * null when impossible, like a patched node deleted, so the save error shows.
261
+ */
262
+ export function rebaseDiagram(
263
+ serverDoc: DiagramDoc,
264
+ lastSaved: DiagramDoc,
265
+ local: DiagramDoc,
266
+ ): RawCanvasData | null {
267
+ const ops = diffToOps(lastSaved, local);
268
+ const res = tryCatch(() => applyOps({ nodes: serverDoc.nodes, edges: serverDoc.edges }, ops));
269
+ if (!res.ok) return null;
270
+ const nodes = applyNodeFieldPatches(res.value.nodes, nodeFieldPatches(lastSaved, local));
271
+ return nodes ? { ...res.value, nodes: parentsFirst(nodes) } : null;
272
+ }