@heyramzi/ui 2.11.1 → 2.11.2

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 (266) hide show
  1. package/dist/agents/helpers.js +38 -38
  2. package/dist/agents/page-state.js +49 -50
  3. package/dist/agents/vendor/color.js +146 -127
  4. package/dist/agents/vendor/draw.js +617 -508
  5. package/dist/agents/vendor/engine.js +698 -599
  6. package/dist/agents/vendor/mesh.js +653 -683
  7. package/dist/agents/vendor/plastic.js +3913 -3112
  8. package/dist/agents/vendor/presets.js +163 -164
  9. package/dist/agents/vendor/shapes.js +21 -20
  10. package/dist/agents/vendor/ticker.js +41 -42
  11. package/dist/agents/vendor/types.js +1 -0
  12. package/dist/agents/vendor/wear.js +1421 -1448
  13. package/dist/ai/confirmation/confirmation-context.svelte.js +6 -6
  14. package/dist/ai/confirmation/index.js +3 -1
  15. package/dist/ai/index.js +33 -0
  16. package/dist/ai/prompt-input/index.js +3 -1
  17. package/dist/ai/reasoning/index.js +3 -1
  18. package/dist/ai/reasoning/reasoning-context.svelte.js +6 -6
  19. package/dist/ai/suggestion/index.js +3 -1
  20. package/dist/ai/typing-dots/index.js +3 -1
  21. package/dist/animations/text-swap.js +24 -24
  22. package/dist/brand/index.js +54 -48
  23. package/dist/canvas/card/resize.js +5 -3
  24. package/dist/canvas/edges/edge-path.js +255 -242
  25. package/dist/canvas/editor/context-menu.js +39 -36
  26. package/dist/canvas/editor/dispatch.svelte.js +154 -151
  27. package/dist/canvas/editor/editor-context.js +7 -2
  28. package/dist/canvas/editor/editor-mode.js +1 -0
  29. package/dist/canvas/editor/frame.js +73 -76
  30. package/dist/canvas/editor/hand-context.js +5 -2
  31. package/dist/canvas/editor/history-core.js +163 -191
  32. package/dist/canvas/editor/keymap.js +95 -169
  33. package/dist/canvas/editor/layout-actions.js +143 -144
  34. package/dist/canvas/editor/lock.js +52 -51
  35. package/dist/canvas/editor/rebase.js +217 -240
  36. package/dist/canvas/editor/style.js +132 -137
  37. package/dist/canvas/editor/text-field-context.js +7 -2
  38. package/dist/canvas/editor/z-order.js +65 -70
  39. package/dist/canvas/headless/alignment.js +22 -16
  40. package/dist/canvas/headless/break-apart-find.js +286 -279
  41. package/dist/canvas/headless/break-apart-pieces.js +62 -58
  42. package/dist/canvas/headless/break-apart-plan.js +299 -325
  43. package/dist/canvas/headless/break-apart-style.js +75 -70
  44. package/dist/canvas/headless/break-apart.js +188 -181
  45. package/dist/canvas/headless/canvas-cleanup.js +78 -58
  46. package/dist/canvas/headless/edge-anchors.js +115 -116
  47. package/dist/canvas/headless/edge-memo.js +24 -22
  48. package/dist/canvas/headless/edge-serialization.js +17 -27
  49. package/dist/canvas/headless/figure-geometry.js +232 -264
  50. package/dist/canvas/headless/fit-viewport.js +84 -67
  51. package/dist/canvas/headless/grid.js +88 -86
  52. package/dist/canvas/headless/icon-node.js +44 -36
  53. package/dist/canvas/headless/index.js +1 -0
  54. package/dist/canvas/headless/linkify.js +17 -19
  55. package/dist/canvas/headless/pen-marks.js +101 -95
  56. package/dist/canvas/headless/sketch.js +231 -209
  57. package/dist/canvas/headless/snap-guides.js +99 -83
  58. package/dist/canvas/headless/svg-figure.js +133 -117
  59. package/dist/canvas/headless/table-layout.js +146 -107
  60. package/dist/canvas/index.js +2 -0
  61. package/dist/canvas/lod/lod-mode.svelte.js +20 -18
  62. package/dist/canvas/lod/render-all.svelte.js +36 -39
  63. package/dist/canvas/ops/arrange.js +78 -71
  64. package/dist/canvas/ops/ops.js +260 -255
  65. package/dist/canvas/pen/colors.js +37 -35
  66. package/dist/canvas/pen/eraser.js +28 -27
  67. package/dist/canvas/pen/geometry.js +120 -130
  68. package/dist/canvas/pen/index.js +1 -0
  69. package/dist/canvas/pen/ops.js +11 -11
  70. package/dist/canvas/pen/pen-session.js +150 -159
  71. package/dist/canvas/pen/plugin.js +107 -104
  72. package/dist/canvas/pen/shapes.js +73 -68
  73. package/dist/canvas/pen/stroke-path.js +32 -30
  74. package/dist/canvas/pen/types.js +21 -21
  75. package/dist/canvas/pen/ui.js +10 -8
  76. package/dist/canvas/plugin.js +46 -46
  77. package/dist/canvas/screen/glyphs.js +12 -11
  78. package/dist/canvas/screen/labels.js +22 -22
  79. package/dist/canvas/screen/parts.js +98 -111
  80. package/dist/canvas/screen/plugin.js +47 -56
  81. package/dist/canvas/screen/types.js +30 -35
  82. package/dist/canvas/screen/ui.js +7 -7
  83. package/dist/canvas/test-utils.js +14 -14
  84. package/dist/canvas/types.js +1 -0
  85. package/dist/canvas/ui/forward-wheel.js +24 -25
  86. package/dist/canvas/ui/icons.js +64 -131
  87. package/dist/canvas/ui/place-panel.js +58 -49
  88. package/dist/canvas/ui/shortcut-tip.js +14 -12
  89. package/dist/canvas/ui/tools.js +300 -385
  90. package/dist/charts/dashboard/chart.js +4 -0
  91. package/dist/charts/dashboard/data-table.js +11 -0
  92. package/dist/charts/dashboard/windows.js +8 -3
  93. package/dist/charts/edge-fade.js +10 -5
  94. package/dist/charts/hover-band.js +16 -11
  95. package/dist/charts/index.js +0 -1
  96. package/dist/charts/motion.js +11 -7
  97. package/dist/charts/primitives/chart-utils.js +12 -12
  98. package/dist/components/copy/clipboard.js +32 -32
  99. package/dist/components/copy/copied.svelte.js +31 -23
  100. package/dist/components/data-table/data-table.js +39 -39
  101. package/dist/components/docs/index.js +4 -0
  102. package/dist/components/docs/scroll-spy.svelte.js +73 -68
  103. package/dist/components/list-page/filter-section.js +4 -1
  104. package/dist/components/list-page/index.js +2 -0
  105. package/dist/components/list-page/list-table.js +7 -2
  106. package/dist/components/list-page/queue-rail.js +14 -0
  107. package/dist/components/list-page/section-host.js +4 -2
  108. package/dist/components/list-page/step.js +4 -3
  109. package/dist/components/list-page/types.js +1 -0
  110. package/dist/components/picker/trigger.js +48 -30
  111. package/dist/components/picker/types.js +1 -0
  112. package/dist/components/segmented/collapse.js +38 -27
  113. package/dist/components/segmented/index.js +8 -0
  114. package/dist/components/segmented/segmented.js +67 -42
  115. package/dist/components/star-rating-input/index.js +3 -1
  116. package/dist/components/table-view/types.js +1 -0
  117. package/dist/components/tags-input/tags.js +81 -69
  118. package/dist/components/theme/index.js +4 -0
  119. package/dist/components/toolbar/index.js +4 -0
  120. package/dist/components/toolbar/toolbar-trigger.js +14 -1
  121. package/dist/editor/anchors.js +17 -13
  122. package/dist/editor/block-id.js +52 -51
  123. package/dist/editor/block-ops.js +108 -80
  124. package/dist/editor/doc-blocks.js +155 -166
  125. package/dist/editor/extensions.js +139 -180
  126. package/dist/editor/index.js +4 -0
  127. package/dist/editor/markdown-test-helpers.js +4 -2
  128. package/dist/editor/markdown.js +420 -408
  129. package/dist/editor/serializer.js +204 -200
  130. package/dist/editor/slash-items.js +89 -101
  131. package/dist/editor/youtube-url.js +25 -20
  132. package/dist/effects/animated-beam/path-calculator.svelte.js +43 -38
  133. package/dist/effects/animated-beam/types.js +1 -0
  134. package/dist/effects/border-beam/css.js +92 -78
  135. package/dist/effects/border-beam/presets.js +1705 -1720
  136. package/dist/effects/border-beam/register-properties.js +43 -33
  137. package/dist/effects/border-beam/types.js +1 -0
  138. package/dist/effects/image-comparison/compare.js +3 -2
  139. package/dist/effects/image-reveal/types.js +1 -0
  140. package/dist/effects/magnetic/magnetic.js +58 -52
  141. package/dist/effects/reveal/observe.js +50 -40
  142. package/dist/effects/reveal/types.js +1 -0
  143. package/dist/effects/shader-gradient/color.js +36 -29
  144. package/dist/effects/shader-gradient/renderer.js +384 -295
  145. package/dist/effects/shader-gradient/shaders.js +31 -0
  146. package/dist/effects/shader-gradient/types.js +1 -0
  147. package/dist/icons/apple-symbol-exports.js +99 -124
  148. package/dist/icons/sf-symbols.js +357 -461
  149. package/dist/icons/trades.js +43 -28
  150. package/dist/inbox/index.js +1 -0
  151. package/dist/inbox/logic.js +126 -122
  152. package/dist/inbox/types.js +43 -43
  153. package/dist/lib/arrive.js +19 -12
  154. package/dist/lib/icon-types.js +1 -0
  155. package/dist/lib/initials.js +16 -10
  156. package/dist/lib/layout.js +5 -0
  157. package/dist/lib/panel.js +13 -0
  158. package/dist/lib/result.js +22 -22
  159. package/dist/lib/sliding-pill.js +59 -44
  160. package/dist/lib/theme-transition.js +43 -43
  161. package/dist/lib/tone.js +31 -24
  162. package/dist/lib/utils.js +3 -1
  163. package/dist/onboarding/define-tours.js +24 -23
  164. package/dist/onboarding/onboarding.svelte.js +16 -12
  165. package/dist/onboarding/pick.js +10 -6
  166. package/dist/onboarding/stores.js +40 -41
  167. package/dist/onboarding/wait.js +24 -23
  168. package/dist/panel/side-panel-types.js +1 -0
  169. package/dist/record/demo-data.js +459 -486
  170. package/dist/record/fields.js +129 -131
  171. package/dist/record/format.js +70 -82
  172. package/dist/record/gutter.js +1 -0
  173. package/dist/record/kinds.js +95 -105
  174. package/dist/record/logic.js +238 -219
  175. package/dist/record/types.js +49 -48
  176. package/dist/shadcn/accordion/index.js +3 -1
  177. package/dist/shadcn/alert/index.js +3 -1
  178. package/dist/shadcn/alert-dialog/index.js +3 -1
  179. package/dist/shadcn/attachment/index.js +3 -1
  180. package/dist/shadcn/avatar/index.js +3 -1
  181. package/dist/shadcn/breadcrumb/index.js +3 -1
  182. package/dist/shadcn/bubble/index.js +3 -1
  183. package/dist/shadcn/button/index.js +3 -1
  184. package/dist/shadcn/calendar/index.js +3 -1
  185. package/dist/shadcn/card/index.js +3 -1
  186. package/dist/shadcn/checkbox/index.js +3 -1
  187. package/dist/shadcn/code-block/index.js +3 -1
  188. package/dist/shadcn/collapsible/index.js +3 -1
  189. package/dist/shadcn/command/index.js +4 -1
  190. package/dist/shadcn/context-menu/index.js +3 -1
  191. package/dist/shadcn/dialog/index.js +3 -1
  192. package/dist/shadcn/dropdown-menu/menu-item.js +40 -20
  193. package/dist/shadcn/empty/index.js +3 -1
  194. package/dist/shadcn/field/index.js +3 -1
  195. package/dist/shadcn/hooks/highlighter.js +77 -75
  196. package/dist/shadcn/hooks/is-mobile.svelte.js +3 -3
  197. package/dist/shadcn/hooks/use-highlighted-code.svelte.js +62 -62
  198. package/dist/shadcn/input/index.js +3 -1
  199. package/dist/shadcn/kbd/index.js +3 -1
  200. package/dist/shadcn/label/index.js +3 -1
  201. package/dist/shadcn/marker/index.js +3 -1
  202. package/dist/shadcn/message/index.js +3 -1
  203. package/dist/shadcn/message-scroller/index.js +3 -1
  204. package/dist/shadcn/popover/index.js +3 -1
  205. package/dist/shadcn/progress/index.js +3 -1
  206. package/dist/shadcn/progress/tone.js +8 -7
  207. package/dist/shadcn/radio-group/index.js +3 -1
  208. package/dist/shadcn/scroll-area/index.js +3 -1
  209. package/dist/shadcn/select/index.js +3 -1
  210. package/dist/shadcn/separator/index.js +3 -1
  211. package/dist/shadcn/sheet/index.js +3 -1
  212. package/dist/shadcn/sidebar/context.svelte.js +42 -28
  213. package/dist/shadcn/sidebar/index.js +3 -1
  214. package/dist/shadcn/skeleton/index.js +3 -1
  215. package/dist/shadcn/slider/index.js +3 -1
  216. package/dist/shadcn/switch/index.js +3 -1
  217. package/dist/shadcn/table/index.js +3 -1
  218. package/dist/shadcn/tabs/index.js +3 -1
  219. package/dist/shadcn/textarea/index.js +3 -1
  220. package/dist/shadcn/toggle/index.js +3 -1
  221. package/dist/shadcn/toggle-group/index.js +3 -1
  222. package/dist/shadcn/tooltip/context.js +7 -2
  223. package/dist/shadcn/tooltip/index.js +3 -1
  224. package/dist/shadcn/trace/index.js +3 -1
  225. package/dist/slides/bridges.js +65 -59
  226. package/dist/slides/deck-controller.svelte.js +210 -207
  227. package/dist/slides/export/capture.js +66 -71
  228. package/dist/slides/export/ffmpeg.js +76 -72
  229. package/dist/slides/export/index.js +3 -0
  230. package/dist/slides/export/launch.js +9 -6
  231. package/dist/slides/export/pdf.js +60 -54
  232. package/dist/slides/export/types.js +1 -0
  233. package/dist/slides/labels.js +24 -24
  234. package/dist/slides/layout.js +39 -41
  235. package/dist/slides/model.js +9 -7
  236. package/dist/slides/navigation.js +78 -73
  237. package/dist/slides/types.js +2 -2
  238. package/dist/status/define-statuses.js +18 -14
  239. package/dist/status/status-types.js +1 -0
  240. package/dist/test-setup.js +24 -20
  241. package/dist/thread/logic.js +73 -77
  242. package/dist/thread/types.js +21 -21
  243. package/dist/work/board-order.js +94 -90
  244. package/dist/work/dates.js +16 -9
  245. package/dist/work/detail/detail-types.js +18 -18
  246. package/dist/work/docs/edited.js +32 -29
  247. package/dist/work/docs/index.js +4 -0
  248. package/dist/work/docs/labels.js +34 -34
  249. package/dist/work/docs/menu.js +113 -106
  250. package/dist/work/docs/outline.js +39 -29
  251. package/dist/work/gantt/logic.js +211 -235
  252. package/dist/work/inbox/inbox-logic.js +71 -75
  253. package/dist/work/inbox/inbox-statuses.js +75 -72
  254. package/dist/work/inbox/inbox-types.js +1 -0
  255. package/dist/work/kanban-drag.js +27 -22
  256. package/dist/work/kanban-types.js +1 -0
  257. package/dist/work/presets/sales.js +36 -32
  258. package/dist/work/reorder.js +9 -8
  259. package/dist/work/task-types.js +1 -0
  260. package/dist/work/time/duration.js +51 -46
  261. package/dist/work/time/time-types.js +23 -23
  262. package/dist/work/use-kanban-dnd.svelte.js +126 -125
  263. package/package.json +3 -1
  264. package/src/components/list-page/master-detail.svelte +3 -1
  265. package/src/components/list-page/rail-row.svelte +4 -4
  266. package/src/editor/markdown.ts +30 -35
@@ -1,65 +1,66 @@
1
1
  export function isLocked(node) {
2
- const data = node?.data;
3
- return data?.locked === true;
2
+ const data = node?.data;
3
+ return data?.locked === true;
4
4
  }
5
+ /** What xyflow needs to hold a locked card still. */
5
6
  export const LOCK_FLAGS = {
6
- draggable: false,
7
- deletable: false
7
+ draggable: false,
8
+ deletable: false
8
9
  };
10
+ /** The card with its flow flags matching `data.locked`. A synced copy keeps its own flags. */
9
11
  export function withLockFlags(node) {
10
- if (node.data?.syncedCopy) return node;
11
- if (isLocked(node)) return {
12
- ...node,
13
- ...LOCK_FLAGS
14
- };
15
- if (node.draggable !== false && node.deletable !== false) return node;
16
- const { draggable: _drag, deletable: _delete, ...rest } = node;
17
- return rest;
12
+ if (node.data?.syncedCopy) return node;
13
+ if (isLocked(node)) return {
14
+ ...node,
15
+ ...LOCK_FLAGS
16
+ };
17
+ if (node.draggable !== false && node.deletable !== false) return node;
18
+ const { draggable: _drag, deletable: _delete, ...rest } = node;
19
+ return rest;
18
20
  }
21
+ /** `locked` alone is the one data change a locked card takes: it is how it comes unlocked. */
19
22
  function onlyUnlocks(op) {
20
- if (op.kind !== "patchNode") return false;
21
- const patch = op;
22
- return patch.data !== undefined && patch.style === undefined && patch.width === undefined && patch.height === undefined && Object.keys(patch.data).every((key)=>key === "locked");
23
+ if (op.kind !== "patchNode") return false;
24
+ const patch = op;
25
+ return patch.data !== undefined && patch.style === undefined && patch.width === undefined && patch.height === undefined && Object.keys(patch.data).every((key) => key === "locked");
23
26
  }
27
+ /**
28
+ * The one gate every dispatched edit passes: ops that don't reach a locked card. An op on an
29
+ * arrow, or a card not on the board, passes.
30
+ */
24
31
  export function refuseLockedOps(nodes, ops) {
25
- const locked = new Set(nodes.filter(isLocked).map((node)=>node.id));
26
- if (locked.size === 0) return [
27
- ...ops
28
- ];
29
- return ops.filter((op)=>{
30
- const nodeId = op.nodeId;
31
- return typeof nodeId !== "string" || !locked.has(nodeId) || onlyUnlocks(op);
32
- });
32
+ const locked = new Set(nodes.filter(isLocked).map((node) => node.id));
33
+ if (locked.size === 0) return [...ops];
34
+ return ops.filter((op) => {
35
+ const nodeId = op.nodeId;
36
+ return typeof nodeId !== "string" || !locked.has(nodeId) || onlyUnlocks(op);
37
+ });
33
38
  }
39
+ /** Of these ids, the ones a delete may take: not locked, and not a group holding something locked. */
34
40
  export function deletableIds(nodes, ids) {
35
- const parentOf = new Map(nodes.map((node)=>[
36
- node.id,
37
- node.parentId
38
- ]));
39
- const holdsLocked = new Set();
40
- for (const node of nodes){
41
- if (!isLocked(node)) continue;
42
- for(let up = node.parentId; up; up = parentOf.get(up))holdsLocked.add(up);
43
- }
44
- const byId = new Map(nodes.map((node)=>[
45
- node.id,
46
- node
47
- ]));
48
- return ids.filter((id)=>!isLocked(byId.get(id)) && !holdsLocked.has(id));
41
+ const parentOf = new Map(nodes.map((node) => [node.id, node.parentId]));
42
+ const holdsLocked = new Set();
43
+ for (const node of nodes) {
44
+ if (!isLocked(node)) continue;
45
+ for (let up = node.parentId; up; up = parentOf.get(up)) holdsLocked.add(up);
46
+ }
47
+ const byId = new Map(nodes.map((node) => [node.id, node]));
48
+ return ids.filter((id) => !isLocked(byId.get(id)) && !holdsLocked.has(id));
49
49
  }
50
+ /** The board with the picked cards locked, or all unlocked when every one of them already was locked. */
50
51
  export function toggleLock(nodes, picked) {
51
- const targets = nodes.filter((node)=>picked.has(node.id) && !node.data.syncedCopy);
52
- if (targets.length === 0) return null;
53
- const locked = !targets.every(isLocked);
54
- const ids = new Set(targets.map((node)=>node.id));
55
- return {
56
- locked,
57
- nodes: nodes.map((node)=>ids.has(node.id) ? withLockFlags({
58
- ...node,
59
- data: {
60
- ...node.data,
61
- locked
62
- }
63
- }) : node)
64
- };
52
+ const targets = nodes.filter((node) => picked.has(node.id) && !node.data.syncedCopy);
53
+ if (targets.length === 0) return null;
54
+ const locked = !targets.every(isLocked);
55
+ const ids = new Set(targets.map((node) => node.id));
56
+ return {
57
+ locked,
58
+ nodes: nodes.map((node) => ids.has(node.id) ? withLockFlags({
59
+ ...node,
60
+ data: {
61
+ ...node.data,
62
+ locked
63
+ }
64
+ }) : node)
65
+ };
65
66
  }
@@ -1,279 +1,256 @@
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.
1
3
  import { tryCatch } from "../../lib/result.js";
2
4
  import { applyOps } from "../ops/ops.js";
3
5
  import { sortParentsFirst } from "../headless/canvas-cleanup.js";
4
6
  const EDGE_PATCH_FIELDS = [
5
- "source",
6
- "target",
7
- "label",
8
- "animated",
9
- "type",
10
- "style",
11
- "sourceHandle",
12
- "targetHandle"
7
+ "source",
8
+ "target",
9
+ "label",
10
+ "animated",
11
+ "type",
12
+ "style",
13
+ "sourceHandle",
14
+ "targetHandle"
13
15
  ];
14
16
  function json(value) {
15
- return JSON.stringify(value ?? null);
17
+ return JSON.stringify(value ?? null);
16
18
  }
17
19
  function objectStyle(node) {
18
- return node.style && typeof node.style === "object" ? node.style : undefined;
20
+ return node.style && typeof node.style === "object" ? node.style : undefined;
19
21
  }
20
22
  function numberOrUndefined(value) {
21
- return typeof value === "number" ? value : undefined;
23
+ return typeof value === "number" ? value : undefined;
22
24
  }
23
25
  function addNodeOp(node) {
24
- const style = objectStyle(node);
25
- const width = numberOrUndefined(node.width);
26
- const height = numberOrUndefined(node.height);
27
- return {
28
- kind: "addNode",
29
- node: {
30
- id: node.id,
31
- type: String(node.type ?? "node"),
32
- position: node.position ?? {
33
- x: 0,
34
- y: 0
35
- },
36
- data: node.data ?? {},
37
- ...style ? {
38
- style
39
- } : {},
40
- ...width !== undefined ? {
41
- width
42
- } : {},
43
- ...height !== undefined ? {
44
- height
45
- } : {},
46
- ...node.parentId ? {
47
- parentId: node.parentId
48
- } : {}
49
- }
50
- };
26
+ const style = objectStyle(node);
27
+ const width = numberOrUndefined(node.width);
28
+ const height = numberOrUndefined(node.height);
29
+ return {
30
+ kind: "addNode",
31
+ node: {
32
+ id: node.id,
33
+ type: String(node.type ?? "node"),
34
+ position: node.position ?? {
35
+ x: 0,
36
+ y: 0
37
+ },
38
+ data: node.data ?? {},
39
+ ...style ? { style } : {},
40
+ ...width !== undefined ? { width } : {},
41
+ ...height !== undefined ? { height } : {},
42
+ ...node.parentId ? { parentId: node.parentId } : {}
43
+ }
44
+ };
51
45
  }
52
46
  function sizePatchOf(before, node) {
53
- const patch = {};
54
- const style = objectStyle(node);
55
- if (style && json(before.style) !== json(node.style)) patch.style = style;
56
- const width = numberOrUndefined(node.width);
57
- if (width !== undefined && before.width !== node.width) patch.width = width;
58
- const height = numberOrUndefined(node.height);
59
- if (height !== undefined && before.height !== node.height) patch.height = height;
60
- return patch;
47
+ const patch = {};
48
+ const style = objectStyle(node);
49
+ if (style && json(before.style) !== json(node.style)) patch.style = style;
50
+ const width = numberOrUndefined(node.width);
51
+ if (width !== undefined && before.width !== node.width) patch.width = width;
52
+ const height = numberOrUndefined(node.height);
53
+ if (height !== undefined && before.height !== node.height) patch.height = height;
54
+ return patch;
61
55
  }
56
+ /** Only the top-level keys local changed, so a key the server edited alone keeps its value. */
62
57
  function changedData(before, data) {
63
- if (!data) return undefined;
64
- const changed = Object.entries(data).filter(([key, value])=>json(before?.[key]) !== json(value));
65
- return changed.length > 0 ? Object.fromEntries(changed) : undefined;
58
+ if (!data) return undefined;
59
+ const changed = Object.entries(data).filter(([key, value]) => json(before?.[key]) !== json(value));
60
+ return changed.length > 0 ? Object.fromEntries(changed) : undefined;
66
61
  }
67
62
  function changedNodeOps(before, node) {
68
- const ops = [];
69
- if (node.position && json(before.position) !== json(node.position)) {
70
- ops.push({
71
- kind: "moveNode",
72
- nodeId: node.id,
73
- position: node.position
74
- });
75
- }
76
- const parentId = node.parentId ?? null;
77
- if ((before.parentId ?? null) !== parentId) {
78
- ops.push({
79
- kind: "setParent",
80
- nodeId: node.id,
81
- parentId
82
- });
83
- }
84
- const data = changedData(before.data, node.data);
85
- if (data) ops.push({
86
- kind: "patchNode",
87
- nodeId: node.id,
88
- data
89
- });
90
- const sizePatch = sizePatchOf(before, node);
91
- if (Object.keys(sizePatch).length > 0) {
92
- ops.push({
93
- kind: "patchNode",
94
- nodeId: node.id,
95
- ...sizePatch
96
- });
97
- }
98
- return ops;
63
+ const ops = [];
64
+ if (node.position && json(before.position) !== json(node.position)) {
65
+ ops.push({
66
+ kind: "moveNode",
67
+ nodeId: node.id,
68
+ position: node.position
69
+ });
70
+ }
71
+ const parentId = node.parentId ?? null;
72
+ if ((before.parentId ?? null) !== parentId) {
73
+ ops.push({
74
+ kind: "setParent",
75
+ nodeId: node.id,
76
+ parentId
77
+ });
78
+ }
79
+ const data = changedData(before.data, node.data);
80
+ if (data) ops.push({
81
+ kind: "patchNode",
82
+ nodeId: node.id,
83
+ data
84
+ });
85
+ const sizePatch = sizePatchOf(before, node);
86
+ if (Object.keys(sizePatch).length > 0) {
87
+ ops.push({
88
+ kind: "patchNode",
89
+ nodeId: node.id,
90
+ ...sizePatch
91
+ });
92
+ }
93
+ return ops;
99
94
  }
100
95
  function nodeOps(base, current) {
101
- const baseNodes = new Map(base.nodes.map((n)=>[
102
- n.id,
103
- n
104
- ]));
105
- const currentIds = new Set(current.nodes.map((n)=>n.id));
106
- const removed = base.nodes.filter((n)=>!currentIds.has(n.id)).map((n)=>({
107
- kind: "removeNode",
108
- nodeId: n.id
109
- }));
110
- const added = sortParentsFirst(current.nodes.filter((n)=>!baseNodes.has(n.id))).map(addNodeOp);
111
- const changed = current.nodes.flatMap((node)=>{
112
- const before = baseNodes.get(node.id);
113
- return before ? changedNodeOps(before, node) : [];
114
- });
115
- return [
116
- ...removed,
117
- ...added,
118
- ...changed
119
- ];
96
+ const baseNodes = new Map(base.nodes.map((n) => [n.id, n]));
97
+ const currentIds = new Set(current.nodes.map((n) => n.id));
98
+ const removed = base.nodes.filter((n) => !currentIds.has(n.id)).map((n) => ({
99
+ kind: "removeNode",
100
+ nodeId: n.id
101
+ }));
102
+ // WHY parents first: an added child references its added parent in the same
103
+ // batch, and applyOps validates parent existence at each step.
104
+ const added = sortParentsFirst(current.nodes.filter((n) => !baseNodes.has(n.id))).map(addNodeOp);
105
+ const changed = current.nodes.flatMap((node) => {
106
+ const before = baseNodes.get(node.id);
107
+ return before ? changedNodeOps(before, node) : [];
108
+ });
109
+ return [
110
+ ...removed,
111
+ ...added,
112
+ ...changed
113
+ ];
120
114
  }
121
115
  function addEdgeOp(edge) {
122
- const optional = (key)=>typeof edge[key] === "string" ? {
123
- [key]: edge[key]
124
- } : {};
125
- const record = (key)=>{
126
- const value = edge[key];
127
- return value && typeof value === "object" && !Array.isArray(value) ? {
128
- [key]: value
129
- } : {};
130
- };
131
- return {
132
- kind: "addEdge",
133
- edge: {
134
- id: edge.id,
135
- source: String(edge.source),
136
- target: String(edge.target),
137
- ...optional("sourceHandle"),
138
- ...optional("targetHandle"),
139
- ...optional("type"),
140
- ...optional("label"),
141
- ...typeof edge.animated === "boolean" ? {
142
- animated: edge.animated
143
- } : {},
144
- ...record("style"),
145
- ...record("data")
146
- }
147
- };
116
+ const optional = (key) => typeof edge[key] === "string" ? { [key]: edge[key] } : {};
117
+ const record = (key) => {
118
+ const value = edge[key];
119
+ return value && typeof value === "object" && !Array.isArray(value) ? { [key]: value } : {};
120
+ };
121
+ return {
122
+ kind: "addEdge",
123
+ edge: {
124
+ id: edge.id,
125
+ source: String(edge.source),
126
+ target: String(edge.target),
127
+ ...optional("sourceHandle"),
128
+ ...optional("targetHandle"),
129
+ ...optional("type"),
130
+ ...optional("label"),
131
+ ...typeof edge.animated === "boolean" ? { animated: edge.animated } : {},
132
+ ...record("style"),
133
+ ...record("data")
134
+ }
135
+ };
148
136
  }
149
137
  function patchEdgeOps(before, edge) {
150
- const patch = {};
151
- for (const field of EDGE_PATCH_FIELDS){
152
- if (edge[field] !== undefined && json(before[field]) !== json(edge[field])) {
153
- patch[field] = edge[field];
154
- }
155
- }
156
- if (Object.keys(patch).length === 0) return [];
157
- return [
158
- {
159
- kind: "patchEdge",
160
- edgeId: edge.id,
161
- patch
162
- }
163
- ];
138
+ const patch = {};
139
+ for (const field of EDGE_PATCH_FIELDS) {
140
+ if (edge[field] !== undefined && json(before[field]) !== json(edge[field])) {
141
+ patch[field] = edge[field];
142
+ }
143
+ }
144
+ if (Object.keys(patch).length === 0) return [];
145
+ return [{
146
+ kind: "patchEdge",
147
+ edgeId: edge.id,
148
+ patch
149
+ }];
164
150
  }
165
151
  function edgeOps(base, current) {
166
- const baseEdges = new Map(base.edges.map((e)=>[
167
- e.id,
168
- e
169
- ]));
170
- const currentIds = new Set(current.edges.map((e)=>e.id));
171
- const removed = base.edges.filter((e)=>!currentIds.has(e.id)).map((e)=>({
172
- kind: "removeEdge",
173
- edgeId: e.id
174
- }));
175
- const upserted = current.edges.flatMap((edge)=>{
176
- const before = baseEdges.get(edge.id);
177
- return before ? patchEdgeOps(before, edge) : [
178
- addEdgeOp(edge)
179
- ];
180
- });
181
- return [
182
- ...removed,
183
- ...upserted
184
- ];
152
+ const baseEdges = new Map(base.edges.map((e) => [e.id, e]));
153
+ const currentIds = new Set(current.edges.map((e) => e.id));
154
+ const removed = base.edges.filter((e) => !currentIds.has(e.id)).map((e) => ({
155
+ kind: "removeEdge",
156
+ edgeId: e.id
157
+ }));
158
+ const upserted = current.edges.flatMap((edge) => {
159
+ const before = baseEdges.get(edge.id);
160
+ return before ? patchEdgeOps(before, edge) : [addEdgeOp(edge)];
161
+ });
162
+ return [...removed, ...upserted];
185
163
  }
164
+ /**
165
+ * Diff two persisted documents into ops. Data patches carry only changed keys. A deleted
166
+ * data key, and a node's `type` or `zIndex`, have no op: `nodeFieldPatches` carries those.
167
+ */
186
168
  export function diffToOps(base, current) {
187
- return [
188
- ...nodeOps(base, current),
189
- ...edgeOps(base, current)
190
- ];
169
+ return [...nodeOps(base, current), ...edgeOps(base, current)];
191
170
  }
192
- const NODE_FIELDS = [
193
- "type",
194
- "zIndex"
195
- ];
171
+ const NODE_FIELDS = ["type", "zIndex"];
172
+ /** What changed on a node that `patchNode` can't say: `type`, `zIndex` and deleted data keys. */
196
173
  function nodeFieldPatches(base, current) {
197
- const baseNodes = new Map(base.nodes.map((n)=>[
198
- n.id,
199
- n
200
- ]));
201
- const patches = [];
202
- for (const node of current.nodes){
203
- const before = baseNodes.get(node.id);
204
- if (!before) continue;
205
- const set = {};
206
- const unset = [];
207
- for (const field of NODE_FIELDS){
208
- if (json(before[field]) === json(node[field])) continue;
209
- if (node[field] === undefined) unset.push(field);
210
- else set[field] = node[field];
211
- }
212
- const unsetData = Object.keys(before.data ?? {}).filter((key)=>!(key in (node.data ?? {})));
213
- if (unset.length + unsetData.length + Object.keys(set).length > 0) {
214
- patches.push({
215
- nodeId: node.id,
216
- set,
217
- unset,
218
- unsetData
219
- });
220
- }
221
- }
222
- return patches;
174
+ const baseNodes = new Map(base.nodes.map((n) => [n.id, n]));
175
+ const patches = [];
176
+ for (const node of current.nodes) {
177
+ const before = baseNodes.get(node.id);
178
+ if (!before) continue;
179
+ const set = {};
180
+ const unset = [];
181
+ for (const field of NODE_FIELDS) {
182
+ if (json(before[field]) === json(node[field])) continue;
183
+ if (node[field] === undefined) unset.push(field);
184
+ else set[field] = node[field];
185
+ }
186
+ const unsetData = Object.keys(before.data ?? {}).filter((key) => !(key in (node.data ?? {})));
187
+ if (unset.length + unsetData.length + Object.keys(set).length > 0) {
188
+ patches.push({
189
+ nodeId: node.id,
190
+ set,
191
+ unset,
192
+ unsetData
193
+ });
194
+ }
195
+ }
196
+ return patches;
223
197
  }
224
198
  function withoutKeys(record, keys) {
225
- return Object.fromEntries(Object.entries(record).filter(([key])=>!keys.includes(key)));
199
+ return Object.fromEntries(Object.entries(record).filter(([key]) => !keys.includes(key)));
226
200
  }
201
+ /** Null when a patched node is gone, the same answer `applyOps` gives for an op on a missing node. */
227
202
  function applyNodeFieldPatches(nodes, patches) {
228
- const byId = new Map(patches.map((patch)=>[
229
- patch.nodeId,
230
- patch
231
- ]));
232
- const seen = new Set();
233
- const next = nodes.map((node)=>{
234
- const patch = typeof node.id === "string" ? byId.get(node.id) : undefined;
235
- if (!patch) return node;
236
- seen.add(patch.nodeId);
237
- const patched = withoutKeys({
238
- ...node,
239
- ...patch.set
240
- }, patch.unset);
241
- if (patch.unsetData.length === 0 || typeof node.data !== "object" || node.data === null) {
242
- return patched;
243
- }
244
- return {
245
- ...patched,
246
- data: withoutKeys(node.data, patch.unsetData)
247
- };
248
- });
249
- return seen.size === byId.size ? next : null;
203
+ const byId = new Map(patches.map((patch) => [patch.nodeId, patch]));
204
+ const seen = new Set();
205
+ const next = nodes.map((node) => {
206
+ const patch = typeof node.id === "string" ? byId.get(node.id) : undefined;
207
+ if (!patch) return node;
208
+ seen.add(patch.nodeId);
209
+ const patched = withoutKeys({
210
+ ...node,
211
+ ...patch.set
212
+ }, patch.unset);
213
+ if (patch.unsetData.length === 0 || typeof node.data !== "object" || node.data === null) {
214
+ return patched;
215
+ }
216
+ return {
217
+ ...patched,
218
+ data: withoutKeys(node.data, patch.unsetData)
219
+ };
220
+ });
221
+ return seen.size === byId.size ? next : null;
250
222
  }
223
+ /** A local reparent can name a container that sits later in the server's order. */
251
224
  function parentsFirst(nodes) {
252
- const ids = new Set(nodes.map((node)=>node.id));
253
- const seen = new Set();
254
- const misordered = nodes.some((node)=>{
255
- const early = ids.has(node.parentId) && !seen.has(node.parentId);
256
- seen.add(node.id);
257
- return early;
258
- });
259
- if (!misordered) return nodes;
260
- const wrapped = nodes.map((node)=>({
261
- id: String(node.id),
262
- parentId: typeof node.parentId === "string" ? node.parentId : undefined,
263
- node
264
- }));
265
- return sortParentsFirst(wrapped).map(({ node })=>node);
225
+ const ids = new Set(nodes.map((node) => node.id));
226
+ const seen = new Set();
227
+ const misordered = nodes.some((node) => {
228
+ const early = ids.has(node.parentId) && !seen.has(node.parentId);
229
+ seen.add(node.id);
230
+ return early;
231
+ });
232
+ if (!misordered) return nodes;
233
+ const wrapped = nodes.map((node) => ({
234
+ id: String(node.id),
235
+ parentId: typeof node.parentId === "string" ? node.parentId : undefined,
236
+ node
237
+ }));
238
+ return sortParentsFirst(wrapped).map(({ node }) => node);
266
239
  }
240
+ /**
241
+ * Replay local changes (lastSaved -> local) on the server document. Returns
242
+ * null when impossible, like a patched node deleted, so the save error shows.
243
+ */
267
244
  export function rebaseDiagram(serverDoc, lastSaved, local) {
268
- const ops = diffToOps(lastSaved, local);
269
- const res = tryCatch(()=>applyOps({
270
- nodes: serverDoc.nodes,
271
- edges: serverDoc.edges
272
- }, ops));
273
- if (!res.ok) return null;
274
- const nodes = applyNodeFieldPatches(res.value.nodes, nodeFieldPatches(lastSaved, local));
275
- return nodes ? {
276
- ...res.value,
277
- nodes: parentsFirst(nodes)
278
- } : null;
245
+ const ops = diffToOps(lastSaved, local);
246
+ const res = tryCatch(() => applyOps({
247
+ nodes: serverDoc.nodes,
248
+ edges: serverDoc.edges
249
+ }, ops));
250
+ if (!res.ok) return null;
251
+ const nodes = applyNodeFieldPatches(res.value.nodes, nodeFieldPatches(lastSaved, local));
252
+ return nodes ? {
253
+ ...res.value,
254
+ nodes: parentsFirst(nodes)
255
+ } : null;
279
256
  }