@likec4/diagram 1.6.1 → 1.7.1

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 (50) hide show
  1. package/dist/LikeC4Diagram.d.ts +1 -1
  2. package/dist/LikeC4Diagram.props.d.ts +10 -11
  3. package/dist/globals.css.d.ts +1 -0
  4. package/dist/hooks/useUpdateEffect.d.ts +1 -0
  5. package/dist/index.d.ts +1 -1
  6. package/dist/index.mjs +572 -464
  7. package/dist/state/DiagramContext.d.ts +746 -181
  8. package/dist/state/diagram-to-xyflow.d.ts +10 -0
  9. package/dist/state/diagramStore.d.ts +782 -202
  10. package/dist/style.css +273 -874
  11. package/dist/types.d.ts +20 -0
  12. package/dist/xyflow/EdgeMarkers.d.ts +1 -0
  13. package/dist/xyflow/FitviewOnDiagramChange.d.ts +1 -1
  14. package/dist/xyflow/XYFlow.d.ts +1 -4
  15. package/dist/xyflow/XYFlowEvents.d.ts +41 -12
  16. package/dist/xyflow/XYFlowInner.d.ts +0 -1
  17. package/dist/xyflow/edges/RelationshipEdge.d.ts +3 -3
  18. package/dist/xyflow/hooks/index.d.ts +0 -1
  19. package/dist/xyflow/hooks/useXYFlow.d.ts +27 -27
  20. package/dist/xyflow/types.d.ts +11 -19
  21. package/dist/xyflow/utils.d.ts +1 -0
  22. package/package.json +6 -6
  23. package/src/LikeC4Diagram.props.ts +17 -13
  24. package/src/LikeC4Diagram.tsx +62 -96
  25. package/src/globals.css.ts +12 -6
  26. package/src/hooks/useUpdateEffect.ts +16 -1
  27. package/src/index.ts +1 -2
  28. package/src/state/DiagramContext.tsx +19 -27
  29. package/src/{xyflow → state}/diagram-to-xyflow.ts +19 -18
  30. package/src/state/diagramStore.ts +404 -192
  31. package/src/types.ts +22 -0
  32. package/src/ui/BackwardForwardPanel.tsx +24 -7
  33. package/src/xyflow/EdgeMarkers.tsx +2 -0
  34. package/src/xyflow/FitviewOnDiagramChange.tsx +42 -75
  35. package/src/xyflow/SelectEdgesOnNodeFocus.tsx +1 -1
  36. package/src/xyflow/XYFlow.tsx +57 -32
  37. package/src/xyflow/XYFlowEvents.tsx +58 -10
  38. package/src/xyflow/XYFlowInner.tsx +8 -8
  39. package/src/xyflow/edges/RelationshipEdge.tsx +43 -29
  40. package/src/xyflow/edges/edges.css.ts +3 -3
  41. package/src/xyflow/edges/utils.ts +0 -1
  42. package/src/xyflow/hooks/index.ts +0 -1
  43. package/src/xyflow/hooks/useLayoutConstraints.ts +42 -65
  44. package/src/xyflow/nodes/element/ElementNode.tsx +0 -4
  45. package/src/xyflow/nodes/element/element.css.ts +8 -6
  46. package/src/xyflow/types.ts +14 -32
  47. package/src/xyflow/utils.ts +4 -0
  48. package/dist/xyflow/SyncWithDiagram.d.ts +0 -4
  49. package/dist/xyflow/diagram-to-xyflow.d.ts +0 -7
  50. package/src/xyflow/SyncWithDiagram.tsx +0 -85
package/dist/index.mjs CHANGED
@@ -2,26 +2,26 @@ var __defProp = Object.defineProperty;
2
2
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: !0, configurable: !0, writable: !0, value }) : obj[key] = value;
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key != "symbol" ? key + "" : key, value);
4
4
  import { jsx, Fragment, jsxs } from "react/jsx-runtime";
5
- import { useReactFlow, useStoreApi, useNodesData, useStore, getViewportForBounds, EdgeLabelRenderer, Handle, Position, useOnViewportChange, ReactFlow, useOnSelectionChange, Background, BackgroundVariant, Controls, ReactFlowProvider } from "@xyflow/react";
5
+ import { useReactFlow, useStoreApi, useNodesData, useStore, applyNodeChanges, applyEdgeChanges, getViewportForBounds, EdgeLabelRenderer, Handle, Position, useOnViewportChange, ReactFlow, useOnSelectionChange, Background, BackgroundVariant, Controls, ReactFlowProvider } from "@xyflow/react";
6
6
  import clsx from "clsx";
7
7
  import { shallowEqual, deepEqual } from "fast-equals";
8
8
  import { m, AnimatePresence, LazyMotion, domAnimation } from "framer-motion";
9
- import { useContext, useState, useCallback, createContext, useRef, useEffect, useMemo, memo, useId } from "react";
9
+ import { useContext, useState, useCallback, createContext, useRef, useEffect, memo, useMemo, useId } from "react";
10
10
  import { MantineContext, Box, ActionIcon, Text as Text$1, HoverCard, HoverCardTarget, UnstyledButton, HoverCardDropdown, Stack, Group, Anchor, CopyButton, Button, Card, CardSection, Spoiler, ButtonGroup, rem, Divider, Space, TooltipGroup, Flex, Tooltip, ColorSwatch, CheckIcon, Select, Slider, SegmentedControl } from "@mantine/core";
11
- import { isArray, entries, reduce, isEmpty, pickBy, isNonNullish, hasSubObject, hasAtLeast as hasAtLeast$1, omit, isTruthy, first, last, isNullish, clamp, isNumber, keys, takeWhile, isNonNull } from "remeda";
12
- import { invariant, hasAtLeast, StepEdgeId, nonexhaustive, extractStep, nonNullable, isAncestor, defaultTheme, ElementShapes } from "@likec4/core";
13
- import { getNodeDimensions, nodeToRect, getBoundsOfRects, getViewportForBounds as getViewportForBounds$1 } from "@xyflow/system";
11
+ import { invariant, hasAtLeast, extractStep, nonNullable, StepEdgeId, getBBoxCenter, nonexhaustive, isAncestor, defaultTheme, ElementShapes } from "@likec4/core";
12
+ import { getNodeDimensions, getBoundsOfRects, boxToRect, nodeToRect, getViewportForBounds as getViewportForBounds$1 } from "@xyflow/system";
14
13
  import { DEV } from "esm-env";
14
+ import { isArray, hasAtLeast as hasAtLeast$1, isNullish, entries, reduce, isTruthy, first, last, clamp, isNumber, isNonNullish, keys, takeWhile } from "remeda";
15
15
  import { subscribeWithSelector, devtools } from "zustand/middleware";
16
16
  import { shallow } from "zustand/shallow";
17
17
  import { createWithEqualityFn, useStoreWithEqualityFn } from "zustand/traditional";
18
18
  import { Bezier } from "bezier-js";
19
- import { useFirstMountState, useCustomCompareEffect, useDebouncedEffect, useIsomorphicLayoutEffect } from "@react-hookz/web";
19
+ import { useFirstMountState, useCustomCompareEffect, useDebouncedEffect } from "@react-hookz/web";
20
20
  import { assignInlineVars } from "@vanilla-extract/dynamic";
21
21
  import { line, curveCatmullRomOpen } from "d3-shape";
22
22
  import { Solver, Operator, Expression, Strength, Variable } from "@lume/kiwi";
23
23
  import { IconZoomScan, IconLink, IconChevronLeft, IconChevronRight, IconMenu, IconPlayerPlayFilled, IconPlayerSkipBackFilled, IconPlayerStopFilled, IconPlayerSkipForwardFilled } from "@tabler/icons-react";
24
- import { useStateHistory, useLocalStorage, useHotkeys } from "@mantine/hooks";
24
+ import { useLocalStorage, useHotkeys } from "@mantine/hooks";
25
25
  var rootClassName = "likec4-diagram-root", cssReactFlow = "_2h8g8h0", cssDisablePan = "_2h8g8h1", cssTransparentBg = "_2h8g8h2", cssNoControls = "_2h8g8h3";
26
26
  function EnsureMantine({ children }) {
27
27
  if (!useContext(MantineContext))
@@ -42,13 +42,22 @@ function useSetState(initialState) {
42
42
  return [state, setState];
43
43
  }
44
44
  const noop = () => {
45
+ }, depsShallowEqual = (d1, d2) => {
46
+ if (d1 === d2)
47
+ return !0;
48
+ if (d1.length !== d2.length)
49
+ return !1;
50
+ for (const [i, element] of d1.entries())
51
+ if (!shallowEqual(element, d2[i]))
52
+ return !1;
53
+ return !0;
45
54
  };
46
55
  function useUpdateEffect(callback, deps, equalityFn, effectHook) {
47
56
  const isFirstMount = useFirstMountState();
48
57
  useCustomCompareEffect(
49
58
  isFirstMount ? noop : callback,
50
59
  deps,
51
- equalityFn ?? shallowEqual,
60
+ equalityFn ?? depsShallowEqual,
52
61
  effectHook
53
62
  );
54
63
  }
@@ -91,11 +100,79 @@ function isSamePoint$1(a, b) {
91
100
  const [ax, ay] = isArray(a) ? a : [a.x, a.y], [bx, by] = isArray(b) ? b : [b.x, b.y];
92
101
  return isClose(ax, bx) && isClose(ay, by);
93
102
  }
103
+ function diagramViewToXYFlowData(view, opts) {
104
+ const isDynamicView = view.__ === "dynamic", xynodes = [], xyedges = [], nodeLookup = /* @__PURE__ */ new Map(), traverse = view.nodes.reduce(
105
+ (acc, node) => (nodeLookup.set(node.id, node), node.parent || acc.push({ node, parent: null }), acc),
106
+ new Array()
107
+ ), ns = "", nodeById = (id) => nonNullable(nodeLookup.get(id), `Node not found: ${id}`);
108
+ let next;
109
+ for (; next = traverse.shift(); ) {
110
+ const { node, parent } = next;
111
+ node.children.length > 0 && traverse.push(...node.children.map((child) => ({ node: nodeById(child), parent: node })));
112
+ const isCompound = hasAtLeast$1(node.children, 1), position = {
113
+ x: node.position[0],
114
+ y: node.position[1]
115
+ };
116
+ parent && (position.x -= parent.position[0], position.y -= parent.position[1]);
117
+ const id = ns + node.id, draggable = opts.draggable && (!parent || parent.children.length > 1);
118
+ xynodes.push({
119
+ id,
120
+ type: isCompound ? "compound" : "element",
121
+ data: {
122
+ fqn: node.id,
123
+ element: node
124
+ },
125
+ draggable,
126
+ selectable: opts.selectable,
127
+ deletable: !1,
128
+ position,
129
+ zIndex: isCompound ? ZIndexes.Compound : ZIndexes.Element,
130
+ hidden: !1,
131
+ width: node.width,
132
+ height: node.height,
133
+ // parentId: parent ? ns + parent.id : null,
134
+ ...parent && {
135
+ parentId: ns + parent.id
136
+ },
137
+ ...isCompound && {
138
+ dragHandle: ".likec4-compound-title"
139
+ }
140
+ });
141
+ }
142
+ for (const edge of view.edges) {
143
+ const source = edge.source, target = edge.target, id = ns + edge.id;
144
+ invariant(hasAtLeast$1(edge.points, 2), "edge should have at least 2 points"), xyedges.push({
145
+ id,
146
+ type: "relationship",
147
+ source: ns + source,
148
+ target: ns + target,
149
+ zIndex: ZIndexes.Edge,
150
+ selectable: opts.selectable,
151
+ deletable: !1,
152
+ data: {
153
+ edge,
154
+ controlPoints: edge.controlPoints || null,
155
+ stepNum: isDynamicView ? extractStep(edge.id) : null,
156
+ label: edge.labelBBox ? {
157
+ bbox: edge.labelBBox,
158
+ text: edge.label ?? ""
159
+ } : null
160
+ },
161
+ interactionWidth: 20
162
+ });
163
+ }
164
+ return {
165
+ xynodes,
166
+ xyedges
167
+ };
168
+ }
94
169
  const StringSet = Set, DEFAULT_PROPS = {
170
+ xyedges: [],
171
+ xynodes: [],
172
+ isDynamicView: !1,
95
173
  viewSyncDebounceTimeout: null,
96
174
  initialized: !1,
97
- xyflowSynced: !1,
98
- previousViews: [],
175
+ navigationHistoryIndex: 0,
99
176
  viewportChanged: !1,
100
177
  activeDynamicViewStep: null,
101
178
  focusedNodeId: null,
@@ -104,47 +181,63 @@ const StringSet = Set, DEFAULT_PROPS = {
104
181
  lastClickedNodeId: null,
105
182
  lastClickedEdgeId: null,
106
183
  dimmed: new StringSet(),
107
- lastOnNavigate: null,
108
- onChange: null,
109
- onNavigateTo: null,
110
- onNodeClick: null,
111
- onNodeContextMenu: null,
112
- onCanvasContextMenu: null,
113
- onEdgeClick: null,
114
- onEdgeContextMenu: null,
115
- onCanvasClick: null,
116
- onCanvasDblClick: null
184
+ lastOnNavigate: null
117
185
  }, noReplace = !1;
118
186
  let StoreDevId = 1;
119
187
  const EmptyStringSet = new StringSet();
120
188
  function createDiagramStore(props) {
121
- const storeDevId = "DiagramStore" + String(StoreDevId++).padStart(2, "0");
189
+ const storeDevId = "DiagramStore" + String(StoreDevId++).padStart(2, "0"), {
190
+ xynodes,
191
+ xyedges
192
+ } = diagramViewToXYFlowData(props.view, {
193
+ draggable: props.nodesDraggable,
194
+ selectable: props.nodesSelectable
195
+ });
122
196
  return createWithEqualityFn(
123
197
  subscribeWithSelector(
124
198
  devtools(
125
199
  (set, get) => ({
126
- storeDevId,
127
200
  ...DEFAULT_PROPS,
128
201
  ...props,
129
- isDynamicView: () => {
130
- var _a;
131
- return ((_a = get().view) == null ? void 0 : _a.__) === "dynamic";
202
+ storeDevId,
203
+ isDynamicView: props.view.__ === "dynamic",
204
+ xynodes,
205
+ xyedges,
206
+ navigationHistory: [{
207
+ viewId: props.view.id,
208
+ nodeId: null
209
+ }],
210
+ navigationHistoryIndex: 0,
211
+ onNodesChange: (changes) => {
212
+ set({
213
+ xynodes: applyNodeChanges(changes, get().xynodes)
214
+ });
215
+ },
216
+ onEdgesChange: (changes) => {
217
+ set({
218
+ xyedges: applyEdgeChanges(changes, get().xyedges)
219
+ });
132
220
  },
133
221
  updateView: (nextView) => {
134
222
  let {
135
223
  viewSyncDebounceTimeout,
136
224
  xyflow,
225
+ xystore,
137
226
  dimmed: dimmed2,
138
- xyflowSynced,
139
227
  view: current,
140
228
  lastOnNavigate,
141
- previousViews,
229
+ navigationHistory,
230
+ navigationHistoryIndex,
142
231
  focusedNodeId,
143
232
  lastClickedNodeId,
144
233
  lastClickedEdgeId,
145
234
  activeDynamicViewStep,
235
+ nodesDraggable,
236
+ nodesSelectable,
146
237
  hoveredEdgeId,
147
- hoveredNodeId
238
+ hoveredNodeId,
239
+ xyedges: xyedges2,
240
+ xynodes: xynodes2
148
241
  } = get();
149
242
  if (shallowEqual(current, nextView)) {
150
243
  DEV && console.debug("store: skip updateView");
@@ -154,42 +247,90 @@ function createDiagramStore(props) {
154
247
  const isSameView = current.id === nextView.id;
155
248
  if (isSameView) {
156
249
  const nodeIds = new StringSet(nextView.nodes.map((n) => n.id)), edgeIds = new StringSet(nextView.edges.map((e) => e.id));
157
- if (lastClickedNodeId && !nodeIds.has(lastClickedNodeId) && (lastClickedNodeId = null), hoveredNodeId && !nodeIds.has(hoveredNodeId) && (hoveredNodeId = null), focusedNodeId && !nodeIds.has(focusedNodeId) && (focusedNodeId = null), lastClickedEdgeId && !edgeIds.has(lastClickedEdgeId) && (lastClickedEdgeId = null), hoveredEdgeId && !edgeIds.has(hoveredEdgeId) && (hoveredEdgeId = null), activeDynamicViewStep && !edgeIds.has(StepEdgeId(activeDynamicViewStep)) && (activeDynamicViewStep = null), xyflowSynced = deepEqual([current.nodes, current.edges], [nextView.nodes, nextView.edges]), dimmed2.size > 0) {
250
+ if (lastClickedNodeId && !nodeIds.has(lastClickedNodeId) && (lastClickedNodeId = null), hoveredNodeId && !nodeIds.has(hoveredNodeId) && (hoveredNodeId = null), focusedNodeId && !nodeIds.has(focusedNodeId) && (focusedNodeId = null), lastClickedEdgeId && !edgeIds.has(lastClickedEdgeId) && (lastClickedEdgeId = null), hoveredEdgeId && !edgeIds.has(hoveredEdgeId) && (hoveredEdgeId = null), activeDynamicViewStep && !edgeIds.has(StepEdgeId(activeDynamicViewStep)) && (activeDynamicViewStep = null), dimmed2.size > 0) {
158
251
  let nextDimmed = new StringSet([...dimmed2].filter((id) => nodeIds.has(id) || edgeIds.has(id)));
159
252
  nextDimmed.size !== dimmed2.size && (dimmed2 = nextDimmed);
160
253
  }
161
254
  } else {
162
- if (lastOnNavigate && lastOnNavigate.toView !== nextView.id && lastOnNavigate.fromView !== nextView.id && (lastOnNavigate = null), !lastOnNavigate) {
163
- const zoom = xyflow.getZoom();
164
- xyflow.setCenter(nextView.width / 2, nextView.height / 2, { zoom });
255
+ const stepCurrent = nonNullable(navigationHistory[navigationHistoryIndex]);
256
+ stepCurrent.viewId !== nextView.id ? (navigationHistory = [
257
+ ...navigationHistory.slice(0, navigationHistoryIndex + 1),
258
+ {
259
+ viewId: nextView.id,
260
+ nodeId: (lastOnNavigate == null ? void 0 : lastOnNavigate.fromNode) || null
261
+ }
262
+ ], navigationHistoryIndex = navigationHistory.length - 1) : stepCurrent.nodeId && (lastOnNavigate ?? (lastOnNavigate = {
263
+ fromView: current.id,
264
+ toView: nextView.id,
265
+ fromNode: stepCurrent.nodeId
266
+ })), lastOnNavigate && lastOnNavigate.toView !== nextView.id && (lastOnNavigate = null);
267
+ const elTo = lastOnNavigate && nextView.nodes.find((n) => n.id === (lastOnNavigate == null ? void 0 : lastOnNavigate.fromNode)), xynodeFrom = elTo && xyflow.getInternalNode(elTo.id);
268
+ if (!lastOnNavigate || isNullish(elTo) || isNullish(xynodeFrom)) {
269
+ const zoom = xyflow.getZoom(), { x, y } = getBBoxCenter(nextView.bounds);
270
+ xyflow.setCenter(x, y, { zoom }), lastOnNavigate = null;
271
+ }
272
+ if (lastOnNavigate && elTo && xynodeFrom) {
273
+ const fromPos = xyflow.flowToScreenPosition({
274
+ x: xynodeFrom.internals.positionAbsolute.x,
275
+ // + dimensions.width / 2,
276
+ y: xynodeFrom.internals.positionAbsolute.y
277
+ // + dimensions.height / 2
278
+ }), toPos = xyflow.flowToScreenPosition({
279
+ x: elTo.position[0],
280
+ // + elFrom.width / 2,
281
+ y: elTo.position[1]
282
+ // + elFrom.height / 2
283
+ }), diff = {
284
+ x: toDomPrecision(fromPos.x - toPos.x),
285
+ y: toDomPrecision(fromPos.y - toPos.y)
286
+ };
287
+ xystore.getState().panBy(diff), lastOnNavigate = null;
165
288
  }
166
- lastClickedEdgeId = null, lastClickedNodeId = null, hoveredEdgeId = null, hoveredNodeId = null, focusedNodeId = null, activeDynamicViewStep = null, dimmed2 = EmptyStringSet, previousViews = [
167
- current,
168
- ...previousViews.filter((v) => v.id !== nextView.id && v.id !== current.id)
169
- ], xyflowSynced = !1;
289
+ lastClickedEdgeId = null, lastClickedNodeId = null, hoveredEdgeId = null, hoveredNodeId = null, focusedNodeId = null, activeDynamicViewStep = null, dimmed2 = EmptyStringSet;
170
290
  }
291
+ const update = diagramViewToXYFlowData(nextView, {
292
+ draggable: nodesDraggable,
293
+ selectable: nodesSelectable
294
+ });
295
+ update.xynodes = update.xynodes.map((update2) => {
296
+ const existing = xynodes2.find((n) => n.id === update2.id);
297
+ return existing && existing.type === update2.type && deepEqual(existing.parentId ?? null, update2.parentId ?? null) ? deepEqual(existing.data.element, update2.data.element) ? existing : {
298
+ ...existing,
299
+ ...update2
300
+ } : update2;
301
+ }), isSameView && !nextView.hasLayoutDrift && (update.xyedges = update.xyedges.map((update2) => {
302
+ const existing = xyedges2.find((n) => n.id === update2.id);
303
+ return existing ? deepEqual(existing.data, update2.data) ? existing : {
304
+ ...existing,
305
+ ...update2
306
+ } : update2;
307
+ }));
308
+ const hasChanges = !isSameView || !shallowEqual(update.xynodes, xynodes2) || !shallowEqual(update.xyedges, xyedges2);
171
309
  set(
172
310
  {
311
+ isDynamicView: nextView.__ === "dynamic",
173
312
  viewSyncDebounceTimeout,
174
313
  view: nextView,
175
314
  activeDynamicViewStep,
176
- xyflowSynced,
177
315
  lastOnNavigate,
178
- previousViews,
179
316
  lastClickedNodeId,
180
317
  lastClickedEdgeId,
181
318
  focusedNodeId,
182
319
  hoveredEdgeId,
183
320
  hoveredNodeId,
184
- dimmed: dimmed2
321
+ navigationHistory,
322
+ navigationHistoryIndex,
323
+ dimmed: dimmed2,
324
+ xynodes: hasChanges ? update.xynodes : xynodes2,
325
+ xyedges: hasChanges ? update.xyedges : xyedges2
185
326
  },
186
327
  noReplace,
187
328
  isSameView ? "update-view [same]" : "update-view [another]"
188
329
  );
189
330
  },
190
331
  focusOnNode: (nodeId) => {
191
- const { focusedNodeId, view } = get();
192
- if (nodeId !== focusedNodeId) {
332
+ const { focusedNodeId, view, enableFocusMode } = get();
333
+ if (invariant(enableFocusMode, "focus mode is not enabled"), nodeId !== focusedNodeId) {
193
334
  const notDimmed = new StringSet([nodeId]), dimmed2 = new StringSet();
194
335
  for (const edge of view.edges)
195
336
  edge.source === nodeId || edge.target === nodeId ? (notDimmed.add(edge.source), notDimmed.add(edge.target)) : dimmed2.add(edge.id);
@@ -294,87 +435,139 @@ function createDiagramStore(props) {
294
435
  },
295
436
  cancelSaveManualLayout: () => {
296
437
  let { viewSyncDebounceTimeout } = get();
297
- viewSyncDebounceTimeout !== null && (clearTimeout(viewSyncDebounceTimeout), set({ viewSyncDebounceTimeout: null }));
438
+ return viewSyncDebounceTimeout !== null ? (clearTimeout(viewSyncDebounceTimeout), set({ viewSyncDebounceTimeout: null }), !0) : !1;
298
439
  },
299
440
  triggerSaveManualLayout: () => {
300
- let { viewSyncDebounceTimeout: debounced, onChange } = get();
301
- if (!onChange) {
441
+ const { xyflow, view, onChange, xystore, viewSyncDebounceTimeout } = get();
442
+ if (viewSyncDebounceTimeout !== null && (clearTimeout(viewSyncDebounceTimeout), set({ viewSyncDebounceTimeout: null })), !onChange) {
302
443
  DEV && console.debug("ignore triggerSaveManualLayout, as no onChange handler is set");
303
444
  return;
304
445
  }
305
- debounced && clearTimeout(debounced), debounced = setTimeout(() => {
306
- const { xyflow, onChange: onChange2, xystore } = get(), { nodeLookup } = xystore.getState();
307
- set({ viewSyncDebounceTimeout: null });
308
- const movedNodes = new StringSet(), nodes = reduce([...nodeLookup.values()], (acc, node) => {
309
- const dimensions = getNodeDimensions(node);
310
- return isSamePoint$1(node.internals.positionAbsolute, node.data.element.position) || movedNodes.add(node.id), acc[node.data.fqn] = {
311
- x: node.internals.positionAbsolute.x,
312
- y: node.internals.positionAbsolute.y,
313
- width: dimensions.width,
314
- height: dimensions.height
315
- }, acc;
316
- }, {}), edges = reduce(xyflow.getEdges(), (acc, { source, target, data }) => {
317
- let controlPoints = data.controlPoints;
318
- return !controlPoints && (movedNodes.has(source) || movedNodes.has(target)) && (controlPoints = bezierControlPoints(data.edge)), controlPoints && (acc[data.edge.id] = {
319
- controlPoints
320
- }), acc;
321
- }, {});
322
- if (movedNodes.size === 0 && isEmpty(edges)) {
323
- DEV && console.debug("ignore triggerSaveManualLayout, as no changes detected");
324
- return;
325
- }
326
- const change = {
327
- op: "save-manual-layout",
328
- nodes,
329
- edges
446
+ const { nodeLookup } = xystore.getState(), movedNodes = new StringSet();
447
+ let bounds = {
448
+ x: 0,
449
+ y: 0,
450
+ width: 1,
451
+ height: 1
452
+ };
453
+ const nodes = reduce([...nodeLookup.values()], (acc, node) => {
454
+ const dimensions = getNodeDimensions(node);
455
+ isSamePoint$1(node.internals.positionAbsolute, node.data.element.position) || movedNodes.add(node.id);
456
+ const rect = acc[node.data.fqn] = {
457
+ isCompound: node.data.element.children.length > 0,
458
+ x: Math.floor(node.internals.positionAbsolute.x),
459
+ y: Math.floor(node.internals.positionAbsolute.y),
460
+ width: Math.ceil(dimensions.width),
461
+ height: Math.ceil(dimensions.height)
462
+ };
463
+ return bounds = getBoundsOfRects(bounds, rect), acc;
464
+ }, {}), edges = reduce(xyflow.getEdges(), (acc, { source, target, data }) => {
465
+ var _a, _b;
466
+ let controlPoints = data.controlPoints;
467
+ const sourceOrTargetMoved = movedNodes.has(source) || movedNodes.has(target);
468
+ if ((!controlPoints || controlPoints.length === 0) && sourceOrTargetMoved && (controlPoints = bezierControlPoints(data.edge)), data.edge.points.length === 0 && (!controlPoints || controlPoints.length === 0))
469
+ return acc;
470
+ const _updated = acc[data.edge.id] = {
471
+ points: data.edge.points
330
472
  };
331
- DEV && console.debug("triggerSaveManualLayout", change), onChange2 == null || onChange2({ change });
332
- }, 2e3), set(
473
+ (_a = data.label) != null && _a.bbox && (_updated.labelBBox = (_b = data.label) == null ? void 0 : _b.bbox), data.edge.labelBBox && (_updated.labelBBox ?? (_updated.labelBBox = data.edge.labelBBox)), controlPoints && hasAtLeast$1(controlPoints, 1) && (_updated.controlPoints = controlPoints), !sourceOrTargetMoved && data.edge.dotpos && (_updated.dotpos = data.edge.dotpos);
474
+ const allX = [...data.edge.points.map((p) => p[0]), ...(controlPoints ?? []).map((p) => p.x)], allY = [...data.edge.points.map((p) => p[1]), ...(controlPoints ?? []).map((p) => p.y)], rect = boxToRect({
475
+ x: Math.floor(Math.min(...allX)),
476
+ y: Math.floor(Math.min(...allY)),
477
+ x2: Math.ceil(Math.max(...allX)),
478
+ y2: Math.ceil(Math.max(...allY))
479
+ });
480
+ return bounds = getBoundsOfRects(bounds, rect), acc;
481
+ }, {}), change = {
482
+ op: "save-manual-layout",
483
+ layout: {
484
+ hash: view.hash,
485
+ autoLayout: view.autoLayout,
486
+ nodes,
487
+ edges,
488
+ ...bounds
489
+ }
490
+ };
491
+ DEV && console.debug("triggerSaveManualLayout", change), onChange({ change });
492
+ },
493
+ scheduleSaveManualLayout: () => {
494
+ let { viewSyncDebounceTimeout, onChange } = get();
495
+ if (!onChange) {
496
+ DEV && console.debug("ignore triggerSaveManualLayout, as no onChange handler is set");
497
+ return;
498
+ }
499
+ viewSyncDebounceTimeout && clearTimeout(viewSyncDebounceTimeout), set(
333
500
  {
334
- viewSyncDebounceTimeout: debounced
501
+ viewSyncDebounceTimeout: setTimeout(() => {
502
+ get().triggerSaveManualLayout();
503
+ }, 1e3)
504
+ // explicit typecast to number to suppress TS error in astro build
335
505
  },
336
506
  noReplace,
337
507
  "debounce sync state"
338
508
  );
339
509
  },
340
510
  triggerOnNavigateTo: (xynodeId, event) => {
341
- var _a, _b;
342
- const { xyflow, view } = get(), xynode = xyflow.getInternalNode(xynodeId);
343
- invariant(!!xynode, `node not found: ${xynodeId}`);
344
- const element = xynode.data.element;
345
- invariant(element.navigateTo, `node is not navigable: ${xynodeId}`), set(
511
+ const { view, xynodes: xynodes2, onNavigateTo } = get();
512
+ if (!onNavigateTo)
513
+ return;
514
+ const xynode = xynodes2.find(({ id }) => id === xynodeId);
515
+ invariant(xynode, `xynode not found: ${xynodeId}`);
516
+ const element = view.nodes.find(({ id }) => id === xynodeId);
517
+ invariant(element == null ? void 0 : element.navigateTo, `node is not navigable: ${xynodeId}`), set(
346
518
  {
347
519
  lastClickedNodeId: xynodeId,
348
520
  lastOnNavigate: {
349
521
  fromView: view.id,
350
522
  toView: element.navigateTo,
351
- element,
352
- elementScreenPosition: xyflow.flowToScreenPosition({
353
- x: xynode.internals.positionAbsolute.x,
354
- // + dimensions.width / 2,
355
- y: xynode.internals.positionAbsolute.y
356
- // + dimensions.height / 2
357
- }),
358
- positionCorrected: !1
523
+ fromNode: element.id
359
524
  }
360
525
  },
361
526
  noReplace,
362
527
  "triggerOnNavigateTo"
363
- ), (_b = (_a = get()).onNavigateTo) == null || _b.call(
364
- _a,
528
+ ), onNavigateTo(
365
529
  element.navigateTo,
366
530
  event,
367
531
  element,
368
- xynode.internals.userNode
532
+ xynode
369
533
  );
370
534
  },
535
+ goBack: () => {
536
+ const { navigationHistory, navigationHistoryIndex, onNavigateTo } = get(), { viewId, nodeId } = nonNullable(navigationHistory[navigationHistoryIndex]), stepBack = navigationHistoryIndex > 0 && navigationHistory[navigationHistoryIndex - 1] || null;
537
+ stepBack && onNavigateTo && (set(
538
+ {
539
+ lastClickedEdgeId: null,
540
+ lastClickedNodeId: null,
541
+ navigationHistoryIndex: navigationHistoryIndex - 1,
542
+ lastOnNavigate: nodeId ? {
543
+ fromView: viewId,
544
+ toView: stepBack.viewId,
545
+ fromNode: nodeId
546
+ } : null
547
+ },
548
+ noReplace,
549
+ "goBack"
550
+ ), onNavigateTo(stepBack.viewId));
551
+ },
552
+ goForward: () => {
553
+ const { navigationHistory, navigationHistoryIndex, onNavigateTo } = get(), { viewId } = nonNullable(navigationHistory[navigationHistoryIndex]), stepForward = navigationHistoryIndex < navigationHistory.length - 1 ? navigationHistory[navigationHistoryIndex + 1] : null;
554
+ stepForward && onNavigateTo && (set(
555
+ {
556
+ lastClickedEdgeId: null,
557
+ lastClickedNodeId: null,
558
+ navigationHistoryIndex: navigationHistoryIndex + 1,
559
+ lastOnNavigate: stepForward.nodeId ? {
560
+ fromView: viewId,
561
+ toView: stepForward.viewId,
562
+ fromNode: stepForward.nodeId
563
+ } : null
564
+ },
565
+ noReplace,
566
+ "goForward"
567
+ ), onNavigateTo(stepForward.viewId));
568
+ },
371
569
  fitDiagram: (duration = 500) => {
372
- const { fitViewPadding, view, focusedNodeId, activeDynamicViewStep, xystore } = get(), { width, height, panZoom, transform } = xystore.getState(), bounds = {
373
- x: 0,
374
- y: 0,
375
- width: view.width,
376
- height: view.height
377
- }, maxZoom = Math.max(1, transform[2]), viewport = getViewportForBounds(bounds, width, height, MinZoom, maxZoom, fitViewPadding);
570
+ const { fitViewPadding, view, focusedNodeId, activeDynamicViewStep, xystore } = get(), { width, height, panZoom, transform } = xystore.getState(), bounds = view.bounds, maxZoom = Math.max(1, transform[2]), viewport = getViewportForBounds(bounds, width, height, MinZoom, maxZoom, fitViewPadding);
378
571
  panZoom == null || panZoom.setViewport(viewport, { duration }), (focusedNodeId ?? activeDynamicViewStep) !== null && set(
379
572
  {
380
573
  activeDynamicViewStep: null,
@@ -386,11 +579,12 @@ function createDiagramStore(props) {
386
579
  );
387
580
  },
388
581
  nextDynamicStep: (increment = 1) => {
389
- const { view, activeDynamicViewStep, xyflow, xystore, fitViewPadding } = get();
390
- invariant(view.__ === "dynamic", "view is not dynamic");
391
- const nextStep = (activeDynamicViewStep ?? 0) + increment;
392
- if (nextStep <= 0 || nextStep > view.edges.length)
393
- return;
582
+ const { activeDynamicViewStep, activateDynamicStep } = get(), nextStep = (activeDynamicViewStep ?? 0) + increment;
583
+ nextStep <= 0 || nextStep !== activeDynamicViewStep && activateDynamicStep(nextStep);
584
+ },
585
+ activateDynamicStep: (nextStep) => {
586
+ const { isDynamicView, xyflow, xystore, fitViewPadding } = get();
587
+ invariant(isDynamicView, "view is not dynamic");
394
588
  const edgeId = StepEdgeId(nextStep), dimmed2 = new StringSet();
395
589
  let edge = null;
396
590
  for (const e of xyflow.getEdges()) {
@@ -424,7 +618,7 @@ function createDiagramStore(props) {
424
618
  dimmed: dimmed2
425
619
  },
426
620
  noReplace,
427
- "nextDynamicStep"
621
+ "activateDynamicStep"
428
622
  );
429
623
  },
430
624
  stopDynamicView: () => {
@@ -437,6 +631,17 @@ function createDiagramStore(props) {
437
631
  noReplace,
438
632
  "stopDynamicView"
439
633
  ), get().fitDiagram());
634
+ },
635
+ onInit: (instance) => {
636
+ const { xyflow, initialized } = get();
637
+ (!initialized || xyflow !== instance) && set(
638
+ {
639
+ xyflow: instance,
640
+ initialized: !0
641
+ },
642
+ noReplace,
643
+ "onInit"
644
+ );
440
645
  }
441
646
  }),
442
647
  {
@@ -456,28 +661,31 @@ function DiagramContextProvider({
456
661
  keepAspectRatio,
457
662
  ...props
458
663
  }) {
459
- const containerRef = useRef(null), xystore = useXYStoreApi(), xyflow = useXYFlow(), store = useRef();
664
+ const containerRef = useRef(null), xystore = useXYStoreApi(), xyflow = useXYFlow(), store = useRef(), getContainer = useCallback(() => containerRef.current, [containerRef]);
460
665
  return store.current || (store.current = createDiagramStore({
461
666
  xystore,
462
667
  xyflow,
463
668
  view,
464
- getContainer: () => containerRef.current,
669
+ getContainer,
465
670
  ...props
466
- })), useEffect(
671
+ })), useUpdateEffect(
672
+ () => {
673
+ var _a;
674
+ return (_a = store.current) == null ? void 0 : _a.setState({ xyflow, xystore, getContainer }, !1, "update xyflow and xystore");
675
+ },
676
+ [xyflow, xystore, getContainer]
677
+ ), useUpdateEffect(
467
678
  () => {
468
- if (!store.current) return;
469
- const state = store.current.getState();
470
- (state.xyflow !== xyflow || state.xystore !== xystore) && store.current.setState({ xyflow, xystore }, !1, "update xyflow and xystore");
679
+ var _a;
680
+ return (_a = store.current) == null ? void 0 : _a.setState(props, !1, "update incoming props");
471
681
  },
472
- [xyflow, xystore]
682
+ [props]
473
683
  ), useUpdateEffect(
474
684
  () => {
475
- if (!store.current) return;
476
- const state = store.current.getState(), newProps = pickBy(props, isNonNullish);
477
- hasSubObject(state, newProps) || store.current.setState(newProps, !1, "update incoming props"), state.view !== view && state.updateView(view);
685
+ var _a;
686
+ (_a = store.current) == null || _a.getState().updateView(view);
478
687
  },
479
- [view, props],
480
- (prev, next) => shallowEqual(prev[0], next[0]) && shallowEqual(prev[1], next[1])
688
+ [view]
481
689
  ), /* @__PURE__ */ jsx(
482
690
  "div",
483
691
  {
@@ -485,8 +693,8 @@ function DiagramContextProvider({
485
693
  className,
486
694
  ...keepAspectRatio && {
487
695
  style: {
488
- aspectRatio: `${Math.ceil(view.width)}/${Math.ceil(view.height)}`,
489
- maxHeight: Math.ceil(view.height)
696
+ aspectRatio: `${Math.ceil(view.bounds.width)}/${Math.ceil(view.bounds.height)}`,
697
+ maxHeight: Math.ceil(view.bounds.height)
490
698
  }
491
699
  },
492
700
  children: /* @__PURE__ */ jsx(DiagramContext.Provider, { value: store.current, children })
@@ -505,158 +713,48 @@ function useDiagramStoreApi() {
505
713
  throw new Error("useDiagramStoreApi could be used only inside DiagramContext");
506
714
  return store;
507
715
  }
508
- function diagramViewToXYFlowData(view, opts) {
509
- var _a, _b;
510
- const isDynamicView = view.__ === "dynamic", editor = {
511
- nodes: [],
512
- edges: []
513
- }, nodeLookup = /* @__PURE__ */ new Map(), traverse = view.nodes.reduce(
514
- (acc, node) => (nodeLookup.set(node.id, node), node.parent || acc.push({ node, parent: null }), acc),
515
- new Array()
516
- ), ns = "", nodeById = (id) => nonNullable(nodeLookup.get(id), `Node not found: ${id}`);
517
- let next;
518
- for (; next = traverse.shift(); ) {
519
- const { node, parent } = next;
520
- node.children.length > 0 && traverse.push(...node.children.map((child) => ({ node: nodeById(child), parent: node })));
521
- const isCompound = hasAtLeast$1(node.children, 1), position = {
522
- x: node.position[0],
523
- y: node.position[1]
524
- };
525
- parent && (position.x -= parent.position[0], position.y -= parent.position[1]);
526
- const id = ns + node.id, draggable = opts.draggable && (!parent || parent.children.length > 1);
527
- editor.nodes.push({
528
- id,
529
- type: isCompound ? "compound" : "element",
530
- data: {
531
- fqn: node.id,
532
- element: node
533
- },
534
- draggable,
535
- selectable: opts.selectable,
536
- deletable: !1,
537
- position,
538
- zIndex: isCompound ? ZIndexes.Compound : ZIndexes.Element,
539
- hidden: !1,
540
- initialWidth: node.width,
541
- initialHeight: node.height,
542
- width: node.width,
543
- height: node.height,
544
- ...parent && {
545
- parentId: ns + parent.id
546
- },
547
- ...isCompound && {
548
- dragHandle: ".likec4-compound-title"
549
- }
550
- });
551
- }
552
- for (const edge of view.edges) {
553
- const source = edge.source, target = edge.target, id = ns + edge.id;
554
- invariant(hasAtLeast$1(edge.points, 2), "edge should have at least 2 points"), editor.edges.push({
555
- id,
556
- type: "relationship",
557
- source: ns + source,
558
- target: ns + target,
559
- zIndex: ZIndexes.Edge,
560
- selectable: opts.selectable,
561
- deletable: !1,
562
- data: {
563
- edge,
564
- type: "bezier",
565
- controlPoints: ((_b = (_a = view.manualLayout) == null ? void 0 : _a.edges[edge.id]) == null ? void 0 : _b.controlPoints) || null,
566
- stepNum: isDynamicView ? extractStep(edge.id) : null,
567
- label: edge.labelBBox ? {
568
- bbox: edge.labelBBox,
569
- text: edge.label ?? ""
570
- } : null
571
- },
572
- interactionWidth: 20
573
- });
574
- }
575
- return editor;
576
- }
577
- function useNavigationHistory() {
578
- const store = useDiagramStoreApi(), viewId = useDiagramState((s) => s.view.id), [historyViewId, historyOps, {
579
- history,
580
- current: historyIndex
581
- }] = useStateHistory(viewId), hasBack = historyIndex > 0, hasForward = historyIndex < history.length - 1;
582
- return useUpdateEffect(() => {
583
- viewId !== historyViewId && historyOps.set(viewId);
584
- }, [viewId]), useUpdateEffect(() => {
585
- var _a, _b;
586
- viewId !== historyViewId && ((_b = (_a = store.getState()).onNavigateTo) == null || _b.call(_a, historyViewId));
587
- }, [historyViewId]), useMemo(() => ({
588
- hasBack,
589
- hasForward,
590
- ops: historyOps
591
- }), [
592
- hasBack,
593
- hasForward,
594
- historyOps
595
- ]);
596
- }
597
- function selectDimensions(state) {
716
+ function selectXYFlowSize(state) {
598
717
  return `${Math.round(state.width)}:${Math.round(state.height)}`;
599
718
  }
600
- function FitViewOnViewportResize({ diagramApi, xyflowApi }) {
601
- const dimensions = useXYStore(selectDimensions), prevDimensionsRef = useRef(dimensions);
719
+ function selectDiagramsize({ view, fitViewPadding }) {
720
+ return [
721
+ view.bounds.x,
722
+ view.bounds.y,
723
+ view.bounds.width,
724
+ view.bounds.height,
725
+ fitViewPadding
726
+ ].map(Math.round).join(":");
727
+ }
728
+ function FitViewOnViewportResize({ diagramApi }) {
729
+ const xyflowsize = useXYStore(selectXYFlowSize), viewsize = useDiagramState(selectDiagramsize), dimensions = xyflowsize + viewsize, prevDimensionsRef = useRef(dimensions);
602
730
  return useDebouncedEffect(
603
731
  () => {
604
732
  const { focusedNodeId, fitDiagram } = diagramApi.getState();
605
- focusedNodeId || prevDimensionsRef.current === dimensions || (fitDiagram(300), prevDimensionsRef.current = dimensions);
733
+ focusedNodeId || prevDimensionsRef.current === dimensions || (prevDimensionsRef.current = dimensions, fitDiagram(250));
606
734
  },
607
- [dimensions, diagramApi, xyflowApi],
608
- 300,
735
+ [dimensions, diagramApi],
736
+ 250,
609
737
  1e3
610
738
  ), null;
611
739
  }
612
- function selector$3({ view, xyflowSynced, viewportChanged, lastOnNavigate, ...s }) {
613
- const viewId = view.id + "_" + view.autoLayout + "_" + s.fitViewPadding, waitCorrection = lastOnNavigate && lastOnNavigate.toView === view.id && lastOnNavigate.positionCorrected === !1, elTo = waitCorrection ? view.nodes.find((n) => n.id === lastOnNavigate.element.id) : void 0;
614
- return waitCorrection && elTo ? {
615
- viewId,
616
- xyflowSynced,
617
- viewportChanged,
618
- waitCorrection: !0,
619
- elFrom: lastOnNavigate.element,
620
- elTo
621
- } : {
622
- viewId,
623
- xyflowSynced,
624
- viewportChanged,
625
- waitCorrection: !1,
626
- fromElement: null,
627
- toElement: null
740
+ function selector$3({ view, viewportChanged, fitViewPadding }) {
741
+ return {
742
+ layoutId: view.id + "_" + view.autoLayout + "_" + fitViewPadding,
743
+ viewportNotMoved: !viewportChanged
628
744
  };
629
745
  }
630
746
  function FitViewOnDiagramChange() {
631
- const xyflowApi = useXYStoreApi(), {
632
- viewId: pendingViewId,
633
- xyflowSynced,
634
- viewportChanged,
635
- waitCorrection,
636
- elFrom,
637
- elTo
638
- } = useDiagramState(selector$3, shallowEqual), diagramApi = useDiagramStoreApi(), processedRef = useRef(pendingViewId), durationRef = useRef(300);
639
- return useEffect(() => {
640
- if (!(!xyflowSynced || pendingViewId === processedRef.current)) {
641
- if (waitCorrection) {
642
- invariant(elTo, "elTo should be defined"), invariant(elFrom, "elFrom should be defined");
643
- const { xyflow, lastOnNavigate } = diagramApi.getState();
644
- invariant(lastOnNavigate, "lastOnNavigate should be defined");
645
- const fromPos = lastOnNavigate.elementScreenPosition, toPos = xyflow.flowToScreenPosition({
646
- x: elTo.position[0],
647
- // + elFrom.width / 2,
648
- y: elTo.position[1]
649
- // + elFrom.height / 2
650
- }), diff = {
651
- x: toDomPrecision(fromPos.x - toPos.x),
652
- y: toDomPrecision(fromPos.y - toPos.y)
653
- };
654
- xyflowApi.getState().panBy(diff), durationRef.current = 500, diagramApi.setState({ lastOnNavigate: null });
655
- return;
656
- }
657
- processedRef.current = pendingViewId, diagramApi.getState().fitDiagram(durationRef.current), durationRef.current = 300;
658
- }
659
- }, [pendingViewId, xyflowSynced, waitCorrection]), !viewportChanged && xyflowSynced ? /* @__PURE__ */ jsx(FitViewOnViewportResize, { xyflowApi, diagramApi }) : null;
747
+ const {
748
+ layoutId,
749
+ viewportNotMoved
750
+ } = useDiagramState(selector$3, shallowEqual), diagramApi = useDiagramStoreApi(), [currentLayoutId, setCurrent] = useState(layoutId), requiresFit = layoutId !== currentLayoutId;
751
+ return useDebouncedEffect(
752
+ () => {
753
+ layoutId !== currentLayoutId && (setCurrent(layoutId), diagramApi.getState().fitDiagram(450));
754
+ },
755
+ [requiresFit, diagramApi],
756
+ 50
757
+ ), viewportNotMoved && !requiresFit ? /* @__PURE__ */ jsx(FitViewOnViewportResize, { diagramApi }) : null;
660
758
  }
661
759
  function SelectEdgesOnNodeFocus() {
662
760
  const diagramStore = useDiagramStoreApi();
@@ -705,52 +803,6 @@ function SelectEdgesOnNodeFocus() {
705
803
  [diagramStore]
706
804
  ), null;
707
805
  }
708
- function SyncWithDiagram() {
709
- const xyflowSynced = useDiagramState((s) => s.xyflowSynced), diagramStoreApi = useDiagramStoreApi();
710
- return useEffect(() => {
711
- if (xyflowSynced)
712
- return;
713
- const {
714
- view,
715
- nodesDraggable,
716
- nodesSelectable,
717
- focusedNodeId,
718
- xystore
719
- } = diagramStoreApi.getState(), updates = diagramViewToXYFlowData(view, {
720
- draggable: nodesDraggable,
721
- selectable: nodesSelectable || focusedNodeId !== null
722
- }), {
723
- nodeLookup,
724
- edgeLookup,
725
- setNodes,
726
- setEdges
727
- } = xystore.getState();
728
- setNodes(updates.nodes.map((update) => {
729
- var _a;
730
- const existing = (_a = nodeLookup.get(update.id)) == null ? void 0 : _a.internals.userNode;
731
- return existing && existing.type === update.type && deepEqual(existing.parentId, update.parentId) ? deepEqual(existing.data.element, update.data.element) ? existing : {
732
- ...existing,
733
- ...update
734
- } : update;
735
- })), setEdges(updates.edges.map((update) => {
736
- const existing = edgeLookup.get(update.id);
737
- return existing ? deepEqual(existing.data.controlPoints, update.data.controlPoints) && deepEqual(existing.markerStart, update.markerStart) && deepEqual(existing.markerEnd, update.markerEnd) && deepEqual(existing.data.edge, update.data.edge) ? existing : {
738
- ...omit(existing, ["data", "markerStart", "markerEnd"]),
739
- ...update,
740
- data: {
741
- ...existing.data,
742
- ...update.data
743
- }
744
- } : update;
745
- })), diagramStoreApi.setState(
746
- {
747
- xyflowSynced: !0
748
- },
749
- !1,
750
- "xyflowSynced"
751
- );
752
- }, [xyflowSynced, diagramStoreApi]), null;
753
- }
754
806
  const Open = (props) => /* @__PURE__ */ jsx(
755
807
  "marker",
756
808
  {
@@ -954,7 +1006,7 @@ function bezierPath(bezierSpline) {
954
1006
  const isSame = (a, b) => Math.abs(a - b) < 3.1, isSamePoint = (a, b) => {
955
1007
  const [ax, ay] = isArray(a) ? a : [a.x, a.y], [bx, by] = isArray(b) ? b : [b.x, b.y];
956
1008
  return isSame(ax, bx) && isSame(ay, by);
957
- }, sameControlPoints = (a, b) => a === b ? !0 : !a || !b || a.length !== b.length ? !1 : a.every((ap, i) => isSamePoint(ap, b[i])), isEqualProps$2 = (prev, next) => prev.id === next.id && deepEqual(prev.source, next.source) && deepEqual(prev.target, next.target) && deepEqual(prev.selected ?? !1, next.selected ?? !1) && isSame(prev.sourceX, next.sourceX) && isSame(prev.sourceY, next.sourceY) && isSame(prev.targetX, next.targetX) && isSame(prev.targetY, next.targetY) && deepEqual(prev.data.stepNum, next.data.stepNum) && sameControlPoints(prev.data.controlPoints, next.data.controlPoints) && deepEqual(prev.data.edge, next.data.edge), curve = line().curve(curveCatmullRomOpen).x((d) => d.x).y((d) => d.y), RelationshipEdge = /* @__PURE__ */ memo(function({
1009
+ }, sameControlPoints = (a, b) => a === b ? !0 : !a || !b || a.length !== b.length ? !1 : a.every((ap, i) => isSamePoint(ap, b[i])), isEqualProps$2 = (prev, next) => prev.id === next.id && deepEqual(prev.source, next.source) && deepEqual(prev.target, next.target) && deepEqual(prev.selected ?? !1, next.selected ?? !1) && isSame(prev.sourceX, next.sourceX) && isSame(prev.sourceY, next.sourceY) && isSame(prev.targetX, next.targetX) && isSame(prev.targetY, next.targetY) && deepEqual(prev.data.stepNum, next.data.stepNum) && deepEqual(prev.data.label, next.data.label) && sameControlPoints(prev.data.controlPoints, next.data.controlPoints) && deepEqual(prev.data.edge, next.data.edge), curve = line().curve(curveCatmullRomOpen).x((d) => d.x).y((d) => d.y), RelationshipEdge = /* @__PURE__ */ memo(function({
958
1010
  id,
959
1011
  data,
960
1012
  selected,
@@ -967,18 +1019,18 @@ const isSame = (a, b) => Math.abs(a - b) < 3.1, isSamePoint = (a, b) => {
967
1019
  target,
968
1020
  interactionWidth
969
1021
  }) {
970
- const [isControlPointDragging, setIsControlPointDragging] = useState(!1), diagramStore = useDiagramStoreApi(), xyflowStore = useXYStoreApi(), { isActive, isEditable, isEdgePathEditable, isHovered, isDimmed } = useDiagramState((s) => ({
971
- isEditable: s.readonly !== !0,
1022
+ const [isControlPointDragging, setIsControlPointDragging] = useState(!1), diagramStore = useDiagramStoreApi(), xyflowStore = useXYStoreApi(), { isActive, isEdgePathEditable, isHovered, isDimmed } = useDiagramState((s) => ({
972
1023
  isEdgePathEditable: s.readonly !== !0 && s.experimentalEdgeEditing === !0,
973
1024
  isActive: s.focusedNodeId === source || s.focusedNodeId === target || s.activeDynamicViewStep !== null && s.activeDynamicViewStep === data.stepNum,
974
1025
  isHovered: s.hoveredEdgeId === id,
975
1026
  isDimmed: s.dimmed.has(id)
976
- })), { nodeLookup, edgeLookup } = xyflowStore.getState(), sourceNode = nonNullable(nodeLookup.get(source), `source node ${source} not found`), targetNode = nonNullable(nodeLookup.get(target), `target node ${target} not found`), isModified = isEditable && (isTruthy(data.controlPoints) || !isSamePoint(sourceNode.internals.positionAbsolute, sourceNode.data.element.position) || !isSamePoint(targetNode.internals.positionAbsolute, targetNode.data.element.position)), {
1027
+ })), { nodeLookup, edgeLookup } = xyflowStore.getState(), sourceNode = nonNullable(nodeLookup.get(source), `source node ${source} not found`), targetNode = nonNullable(nodeLookup.get(target), `target node ${target} not found`), isModified = isTruthy(data.controlPoints) || !isSamePoint(sourceNode.internals.positionAbsolute, sourceNode.data.element.position) || !isSamePoint(targetNode.internals.positionAbsolute, targetNode.data.element.position), {
977
1028
  label,
978
1029
  edge: {
979
1030
  points: diagramEdgePoints,
980
1031
  line: line2 = "dashed",
981
1032
  color = "gray",
1033
+ labelBBox,
982
1034
  ...diagramEdge
983
1035
  }
984
1036
  } = data;
@@ -986,10 +1038,10 @@ const isSame = (a, b) => Math.abs(a - b) < 3.1, isSamePoint = (a, b) => {
986
1038
  const isStepEdge = data.stepNum !== null, isDotted = line2 === "dotted", isDashed = isDotted || line2 === "dashed";
987
1039
  let strokeDasharray;
988
1040
  isDotted ? strokeDasharray = "1,8" : isDashed && (strokeDasharray = "8,10");
989
- let labelX = (label == null ? void 0 : label.bbox.x) ?? 0, labelY = (label == null ? void 0 : label.bbox.y) ?? 0;
1041
+ let labelX = (labelBBox == null ? void 0 : labelBBox.x) ?? 0, labelY = (labelBBox == null ? void 0 : labelBBox.y) ?? 0;
990
1042
  const [labelPos, setLabelPosition] = useState({
991
- x: labelX,
992
- y: labelY
1043
+ x: (label == null ? void 0 : label.bbox.x) ?? labelX,
1044
+ y: (label == null ? void 0 : label.bbox.y) ?? labelY
993
1045
  });
994
1046
  let edgePath;
995
1047
  if (isModified) {
@@ -1011,7 +1063,7 @@ const isSame = (a, b) => Math.abs(a - b) < 3.1, isSamePoint = (a, b) => {
1011
1063
  } else
1012
1064
  edgePath = bezierPath(diagramEdgePoints);
1013
1065
  const svgPathRef = useRef(null);
1014
- useIsomorphicLayoutEffect(() => {
1066
+ useEffect(() => {
1015
1067
  const path = svgPathRef.current;
1016
1068
  if (!path) return;
1017
1069
  const dompoint = path.getPointAtLength(path.getTotalLength() * 0.5), point = {
@@ -1019,7 +1071,22 @@ const isSame = (a, b) => Math.abs(a - b) < 3.1, isSamePoint = (a, b) => {
1019
1071
  y: Math.round(dompoint.y)
1020
1072
  };
1021
1073
  setLabelPosition((current) => isSamePoint(current, point) ? current : point);
1022
- }, [edgePath]);
1074
+ }, [edgePath]), useDebouncedEffect(
1075
+ () => {
1076
+ !label || label.bbox.x === labelPos.x && label.bbox.y === labelPos.y || diagramStore.getState().xyflow.updateEdgeData(id, {
1077
+ label: {
1078
+ ...label,
1079
+ bbox: {
1080
+ ...label.bbox,
1081
+ ...labelPos
1082
+ }
1083
+ }
1084
+ });
1085
+ },
1086
+ [labelPos],
1087
+ 100,
1088
+ 300
1089
+ );
1023
1090
  const onControlPointerDown = (index, e) => {
1024
1091
  const { domNode } = xyflowStore.getState();
1025
1092
  if (!domNode || e.pointerType !== "mouse")
@@ -1034,11 +1101,12 @@ const isSame = (a, b) => Math.abs(a - b) < 3.1, isSamePoint = (a, b) => {
1034
1101
  }
1035
1102
  if (e.button !== 0)
1036
1103
  return;
1104
+ const wasCanceled = diagramStore.getState().cancelSaveManualLayout();
1037
1105
  e.stopPropagation();
1038
1106
  let hasMoved = !1, pointer = { x: e.clientX, y: e.clientY };
1039
1107
  const onPointerMove = (e2) => {
1040
1108
  if (!isSamePoint(pointer, [e2.clientX, e2.clientY])) {
1041
- setIsControlPointDragging(!0), hasMoved || diagramStore.getState().cancelSaveManualLayout(), hasMoved = !0, pointer = { x: e2.clientX, y: e2.clientY };
1109
+ setIsControlPointDragging(!0), hasMoved = !0, pointer = { x: e2.clientX, y: e2.clientY };
1042
1110
  const { x, y } = xyflow.screenToFlowPosition(pointer, { snapToGrid: !1 }), newControlPoints = controlPoints.slice();
1043
1111
  newControlPoints[index] = {
1044
1112
  x: Math.round(x),
@@ -1046,21 +1114,21 @@ const isSame = (a, b) => Math.abs(a - b) < 3.1, isSamePoint = (a, b) => {
1046
1114
  }, xyflow.updateEdgeData(id, { controlPoints: newControlPoints });
1047
1115
  }
1048
1116
  }, onPointerUp = () => {
1049
- setIsControlPointDragging(!1), domNode.removeEventListener("pointermove", onPointerMove), domNode.removeEventListener("pointerup", onPointerUp), hasMoved && diagramStore.getState().triggerSaveManualLayout();
1117
+ domNode.removeEventListener("pointermove", onPointerMove), domNode.removeEventListener("pointerup", onPointerUp), (hasMoved || wasCanceled) && diagramStore.getState().scheduleSaveManualLayout(), setIsControlPointDragging(!1);
1050
1118
  };
1051
1119
  domNode.addEventListener("pointermove", onPointerMove), domNode.addEventListener("pointerup", onPointerUp, { once: !0 });
1052
1120
  };
1053
1121
  let markerStartName = toMarker(diagramEdge.tail), markerEndName = toMarker(diagramEdge.head ?? "normal");
1054
1122
  diagramEdge.dir === "back" && ([markerStartName, markerEndName] = [markerEndName, markerStartName]);
1055
- const MarkerStart = markerStartName ? EdgeMarkers[markerStartName] : null, MarkerEnd = markerEndName ? EdgeMarkers[markerEndName] : null;
1123
+ const MarkerStart = markerStartName ? EdgeMarkers[markerStartName] : null, MarkerEnd = markerEndName ? EdgeMarkers[markerEndName] : null, labelZIndex = 1 + (isHovered ? ZIndexes.Element : edgeLookup.get(id).zIndex ?? ZIndexes.Edge);
1056
1124
  return /* @__PURE__ */ jsxs(
1057
1125
  "g",
1058
1126
  {
1059
- className: clsx([
1127
+ className: clsx(
1060
1128
  container$4,
1061
1129
  isDimmed && dimmed$2,
1062
1130
  isControlPointDragging && controlDragging
1063
- ]),
1131
+ ),
1064
1132
  "data-likec4-color": color,
1065
1133
  "data-edge-dir": diagramEdge.dir,
1066
1134
  "data-edge-active": isActive,
@@ -1135,7 +1203,7 @@ const isSame = (a, b) => Math.abs(a - b) < 3.1, isSamePoint = (a, b) => {
1135
1203
  selected: selected ?? !1,
1136
1204
  stepNum: data.stepNum,
1137
1205
  label: data.label,
1138
- zIndex: (edgeLookup.get(id).zIndex ?? ZIndexes.Edge) + 1,
1206
+ zIndex: labelZIndex,
1139
1207
  isHovered,
1140
1208
  isActive,
1141
1209
  isStepEdge
@@ -1240,22 +1308,22 @@ class Compound extends Rect {
1240
1308
  rect.minX,
1241
1309
  [-1, this.minX]
1242
1310
  ), { weak } = Strength;
1243
- this.solver.createConstraint(leftPadding, Operator.Ge, 40), this.solver.createConstraint(leftPadding, Operator.Eq, 40, weak);
1311
+ this.solver.createConstraint(leftPadding, Operator.Ge, 40), this.solver.createConstraint(leftPadding, Operator.Eq, 41, weak);
1244
1312
  const topPadding = new Expression(
1245
1313
  rect.minY,
1246
1314
  [-1, this.minY]
1247
1315
  );
1248
- this.solver.createConstraint(topPadding, Operator.Ge, 60), this.solver.createConstraint(topPadding, Operator.Eq, 60, weak);
1316
+ this.solver.createConstraint(topPadding, Operator.Ge, 54), this.solver.createConstraint(topPadding, Operator.Eq, 55, weak);
1249
1317
  const rightPadding = new Expression(
1250
1318
  this.maxX,
1251
1319
  [-1, rect.maxX]
1252
1320
  );
1253
- this.solver.createConstraint(rightPadding, Operator.Ge, 40), this.solver.createConstraint(rightPadding, Operator.Eq, 40, weak);
1321
+ this.solver.createConstraint(rightPadding, Operator.Ge, 40), this.solver.createConstraint(rightPadding, Operator.Eq, 41, weak);
1254
1322
  const bottomPadding = new Expression(
1255
1323
  this.maxY,
1256
1324
  [-1, rect.maxY]
1257
1325
  );
1258
- this.solver.createConstraint(bottomPadding, Operator.Ge, 40), this.solver.createConstraint(bottomPadding, Operator.Eq, 40, weak);
1326
+ this.solver.createConstraint(bottomPadding, Operator.Ge, 40), this.solver.createConstraint(bottomPadding, Operator.Eq, 41, weak);
1259
1327
  }
1260
1328
  }
1261
1329
  class Leaf extends Rect {
@@ -1272,7 +1340,7 @@ class Leaf extends Rect {
1272
1340
  parent && parent.addChild(this);
1273
1341
  }
1274
1342
  }
1275
- function createLayoutConstraints(xyflow, xyflowApi, draggingNodeId) {
1343
+ function createLayoutConstraints(xyflowApi, draggingNodeId) {
1276
1344
  var _a;
1277
1345
  const { parentLookup, nodeLookup } = xyflowApi.getState(), solver = new Solver(), rects = /* @__PURE__ */ new Map(), traverse = new Array();
1278
1346
  for (const [, xynode] of nodeLookup)
@@ -1290,36 +1358,30 @@ function createLayoutConstraints(xyflow, xyflowApi, draggingNodeId) {
1290
1358
  }));
1291
1359
  }
1292
1360
  solver.updateVariables(), solver.maxIterations = 1e3;
1361
+ const rectsToUpdate = [...rects.values()].filter((r) => r.id !== draggingNodeId);
1293
1362
  function updateXYFlowNodes() {
1294
- solver.updateVariables(), xyflow.setNodes(
1295
- (nodes) => nodes.map((n) => {
1296
- if (n.id === draggingNodeId || n.dragging === !0)
1297
- return n;
1298
- const rect = rects.get(n.id);
1299
- if (!rect)
1300
- return n;
1301
- const dimensions = rect.dimensions, newXY = rect.position;
1302
- return newXY.x !== n.position.x || newXY.y !== n.position.y ? {
1303
- ...n,
1304
- position: rect.position,
1305
- width: dimensions.width,
1306
- height: dimensions.height,
1307
- measured: dimensions
1308
- } : dimensions.width !== n.width || dimensions.height !== n.height ? {
1309
- ...n,
1310
- width: dimensions.width,
1311
- height: dimensions.height,
1312
- measured: dimensions
1313
- } : n;
1314
- })
1363
+ solver.updateVariables(), xyflowApi.getState().triggerNodeChanges(
1364
+ rectsToUpdate.reduce((acc, r) => (acc.push({
1365
+ id: r.id,
1366
+ type: "position",
1367
+ dragging: !1,
1368
+ position: r.position,
1369
+ positionAbsolute: r.positionAbsolute
1370
+ }), r instanceof Compound && acc.push({
1371
+ id: r.id,
1372
+ type: "dimensions",
1373
+ setAttributes: !0,
1374
+ dimensions: r.dimensions
1375
+ }), acc), [])
1315
1376
  );
1316
1377
  }
1317
1378
  let animationFrameId = null;
1318
1379
  function onNodeDrag(xynode) {
1319
- const pos = nodeLookup.get(xynode.id).internals.positionAbsolute, rect = nonNullable(rects.get(xynode.id));
1320
- solver.suggestValue(rect.minX, Math.ceil(pos.x)), solver.suggestValue(rect.minY, Math.ceil(pos.y)), animationFrameId ?? (animationFrameId = requestAnimationFrame(() => {
1321
- updateXYFlowNodes(), animationFrameId = null;
1322
- }));
1380
+ rectsToUpdate.length !== 0 && (animationFrameId ?? (animationFrameId = requestAnimationFrame(() => {
1381
+ animationFrameId = null;
1382
+ const pos = nodeLookup.get(xynode.id).internals.positionAbsolute, rect = nonNullable(rects.get(xynode.id));
1383
+ solver.suggestValue(rect.minX, Math.ceil(pos.x)), solver.suggestValue(rect.minY, Math.ceil(pos.y)), updateXYFlowNodes();
1384
+ })));
1323
1385
  }
1324
1386
  return {
1325
1387
  onNodeDrag,
@@ -1327,27 +1389,20 @@ function createLayoutConstraints(xyflow, xyflowApi, draggingNodeId) {
1327
1389
  };
1328
1390
  }
1329
1391
  function useLayoutConstraints() {
1330
- const diagramApi = useDiagramStoreApi(), xyflowApi = useXYStoreApi(), solverRef = useRef(), dragStartedAt = useRef({
1331
- x: 0,
1332
- y: 0
1333
- });
1392
+ const diagramApi = useDiagramStoreApi(), solverRef = useRef();
1334
1393
  return useMemo(() => ({
1335
- onNodeDragStart: (event, xynode) => {
1336
- dragStartedAt.current = {
1337
- x: event.clientX,
1338
- y: event.clientY
1339
- };
1340
- const { xyflow, cancelSaveManualLayout } = diagramApi.getState();
1341
- cancelSaveManualLayout(), solverRef.current = createLayoutConstraints(xyflow, xyflowApi, xynode.id);
1394
+ onNodeDragStart: (_event, xynode) => {
1395
+ const { cancelSaveManualLayout, xystore } = diagramApi.getState();
1396
+ cancelSaveManualLayout(), solverRef.current = createLayoutConstraints(xystore, xynode.id);
1342
1397
  },
1343
1398
  onNodeDrag: (_event, xynode) => {
1344
1399
  var _a;
1345
1400
  invariant(solverRef.current, "solverRef.current should be defined"), (_a = solverRef.current) == null || _a.onNodeDrag(xynode);
1346
1401
  },
1347
- onNodeDragStop: (event, _xynode) => {
1348
- isSamePoint$1(dragStartedAt.current, { x: event.clientX, y: event.clientY }) || diagramApi.getState().triggerSaveManualLayout(), solverRef.current = void 0;
1402
+ onNodeDragStop: (_event, _xynode) => {
1403
+ diagramApi.getState().scheduleSaveManualLayout(), solverRef.current = void 0;
1349
1404
  }
1350
- }), [xyflowApi, diagramApi]);
1405
+ }), [diagramApi]);
1351
1406
  }
1352
1407
  function NavigateToBtn({ xynodeId, className }) {
1353
1408
  const diagramApi = useDiagramStoreApi();
@@ -1931,18 +1986,17 @@ function useXYFlowEvents() {
1931
1986
  },
1932
1987
  onNodeClick: (event, xynode) => {
1933
1988
  const {
1934
- zoomable,
1935
- fitViewEnabled,
1936
1989
  focusedNodeId,
1937
1990
  fitDiagram,
1938
1991
  focusOnNode,
1939
1992
  onNodeClick,
1993
+ enableFocusMode,
1940
1994
  lastClickedNodeId,
1941
1995
  setLastClickedNode
1942
1996
  } = diagramApi.getState();
1943
1997
  setLastClickedNode(xynode.id);
1944
- const focusPossible = zoomable && fitViewEnabled, shallChangeFocus = !!focusedNodeId && focusedNodeId !== xynode.id, clickedRecently = lastClickedNodeId === xynode.id && lastClickWasRecent();
1945
- if (focusPossible) {
1998
+ const shallChangeFocus = !!focusedNodeId && focusedNodeId !== xynode.id, clickedRecently = lastClickedNodeId === xynode.id && lastClickWasRecent();
1999
+ if (enableFocusMode) {
1946
2000
  let stopPropagation = !1;
1947
2001
  switch (!0) {
1948
2002
  case (!focusedNodeId && clickedRecently):
@@ -1964,27 +2018,60 @@ function useXYFlowEvents() {
1964
2018
  onNodeDoubleClick: (event, xynode) => {
1965
2019
  const {
1966
2020
  focusedNodeId,
1967
- zoomable,
1968
- fitViewEnabled,
2021
+ enableFocusMode,
1969
2022
  fitDiagram,
1970
2023
  focusOnNode,
1971
2024
  setLastClickedNode
1972
2025
  } = diagramApi.getState();
1973
- setLastClickedNode(xynode.id), lastClickTimestamp.current = Date.now(), (focusedNodeId || zoomable && fitViewEnabled) && (focusedNodeId === xynode.id ? fitDiagram() : focusOnNode(xynode.id), event.stopPropagation());
2026
+ setLastClickedNode(xynode.id), lastClickTimestamp.current = Date.now(), (isNonNullish(focusedNodeId) || enableFocusMode) && (focusedNodeId === xynode.id ? fitDiagram() : focusOnNode(xynode.id), event.stopPropagation());
1974
2027
  },
1975
2028
  onEdgeClick: (event, xyedge) => {
1976
- diagramApi.getState().setLastClickedEdge(xyedge.id);
1977
2029
  const {
2030
+ lastClickedEdgeId,
2031
+ isDynamicView,
2032
+ enableDynamicViewWalkthrough,
2033
+ activateDynamicStep,
2034
+ activeDynamicViewStep,
1978
2035
  focusedNodeId,
1979
2036
  focusOnNode,
1980
- onEdgeClick
2037
+ onEdgeClick,
2038
+ setLastClickedEdge
1981
2039
  } = diagramApi.getState();
1982
- focusedNodeId && (focusedNodeId === xyedge.source || focusedNodeId === xyedge.target) && (focusOnNode(focusedNodeId === xyedge.source ? xyedge.target : xyedge.source), onEdgeClick || event.stopPropagation()), onEdgeClick == null || onEdgeClick({
2040
+ lastClickedEdgeId !== xyedge.id && setLastClickedEdge(xyedge.id);
2041
+ const isNotAFirstClick = lastClickedEdgeId === xyedge.id, isEdgeOfFocusedNode = isTruthy(focusedNodeId) && (focusedNodeId === xyedge.source || focusedNodeId === xyedge.target);
2042
+ if (isDynamicView && enableDynamicViewWalkthrough && (isEdgeOfFocusedNode || isNotAFirstClick || isNonNullish(activeDynamicViewStep))) {
2043
+ const nextStep = extractStep(xyedge.data.edge.id);
2044
+ if (activeDynamicViewStep !== nextStep) {
2045
+ activateDynamicStep(nextStep), event.stopPropagation();
2046
+ return;
2047
+ }
2048
+ }
2049
+ isEdgeOfFocusedNode && (focusOnNode(focusedNodeId === xyedge.source ? xyedge.target : xyedge.source), onEdgeClick || event.stopPropagation()), onEdgeClick == null || onEdgeClick({
1983
2050
  edge: xyedge.data.edge,
1984
2051
  xyedge,
1985
2052
  event
1986
2053
  });
1987
2054
  },
2055
+ onEdgeDoubleClick: (event, xyedge) => {
2056
+ diagramApi.getState().setLastClickedEdge(xyedge.id);
2057
+ const {
2058
+ enableFocusMode,
2059
+ isDynamicView,
2060
+ enableDynamicViewWalkthrough,
2061
+ focusOnNode,
2062
+ activateDynamicStep,
2063
+ activeDynamicViewStep
2064
+ } = diagramApi.getState();
2065
+ if (isDynamicView && enableDynamicViewWalkthrough) {
2066
+ const nextStep = extractStep(xyedge.data.edge.id);
2067
+ activeDynamicViewStep !== nextStep && (activateDynamicStep(extractStep(xyedge.data.edge.id)), event.stopPropagation());
2068
+ return;
2069
+ }
2070
+ if (enableFocusMode) {
2071
+ focusOnNode(xyedge.source), event.stopPropagation();
2072
+ return;
2073
+ }
2074
+ },
1988
2075
  onMoveEnd: (event, _viewport) => {
1989
2076
  const viewportChanged = !!event;
1990
2077
  viewportChanged !== diagramApi.getState().viewportChanged && diagramApi.setState({ viewportChanged }, !1, `viewport-changed: ${viewportChanged}`);
@@ -2016,15 +2103,15 @@ const nodeTypes = {
2016
2103
  }, edgeTypes = {
2017
2104
  relationship: RelationshipEdge
2018
2105
  }, selector$1 = (s) => ({
2106
+ nodes: s.xynodes,
2107
+ edges: s.xyedges,
2108
+ onInit: s.onInit,
2109
+ onNodesChange: s.onNodesChange,
2110
+ onEdgesChange: s.onEdgesChange,
2019
2111
  nodesSelectable: s.nodesSelectable || s.focusedNodeId !== null,
2020
2112
  nodesDraggable: s.nodesDraggable,
2021
2113
  fitView: s.fitViewEnabled,
2022
- fitViewOptions: {
2023
- minZoom: MinZoom,
2024
- maxZoom: 1,
2025
- padding: s.fitViewPadding,
2026
- includeHiddenNodes: !0
2027
- },
2114
+ fitViewPadding: s.fitViewPadding,
2028
2115
  hasOnNavigateTo: !!s.onNavigateTo,
2029
2116
  hasOnNodeClick: !!s.onNodeClick,
2030
2117
  hasOnNodeContextMenu: !!s.onNodeContextMenu,
@@ -2032,21 +2119,28 @@ const nodeTypes = {
2032
2119
  hasOnEdgeContextMenu: !!s.onEdgeContextMenu,
2033
2120
  hasOnEdgeClick: !!s.onEdgeClick,
2034
2121
  zoomable: s.zoomable,
2035
- pannable: s.pannable
2122
+ pannable: s.pannable,
2123
+ // If fitView is not enabled
2124
+ // And diagram starts with a negative x or y - we need to translate viewprot
2125
+ translateX: s.fitViewEnabled ? 0 : -Math.min(s.view.bounds.x, 0),
2126
+ translateY: s.fitViewEnabled ? 0 : -Math.min(s.view.bounds.y, 0)
2036
2127
  });
2037
2128
  function XYFlow({
2038
2129
  colorMode = "system",
2039
2130
  className,
2040
2131
  children,
2041
- defaultNodes,
2042
- defaultEdges,
2043
2132
  style
2044
2133
  }) {
2045
- const xyflowApi = useXYStoreApi(), diagramApi = useDiagramStoreApi(), {
2134
+ const xyflowApi = useXYStoreApi(), {
2135
+ nodes,
2136
+ edges,
2137
+ onInit,
2138
+ onNodesChange,
2139
+ onEdgesChange,
2046
2140
  nodesSelectable,
2047
2141
  nodesDraggable,
2048
2142
  fitView,
2049
- fitViewOptions,
2143
+ fitViewPadding,
2050
2144
  pannable,
2051
2145
  zoomable,
2052
2146
  hasOnNodeClick,
@@ -2054,15 +2148,13 @@ function XYFlow({
2054
2148
  hasOnNodeContextMenu,
2055
2149
  hasOnCanvasContextMenu,
2056
2150
  hasOnEdgeContextMenu,
2057
- hasOnEdgeClick
2058
- } = useDiagramState(selector$1, deepEqual), layoutConstraints = useLayoutConstraints(), handlers = useXYFlowEvents();
2151
+ translateX,
2152
+ translateY
2153
+ } = useDiagramState(selector$1, shallowEqual), layoutConstraints = useLayoutConstraints(), handlers = useXYFlowEvents();
2059
2154
  return useOnViewportChange({
2060
2155
  onEnd: ({ x, y, zoom }) => {
2061
- const rounded = {
2062
- x: Math.round(x),
2063
- y: Math.round(y)
2064
- };
2065
- (x !== rounded.x || y !== rounded.y) && xyflowApi.setState({ transform: [rounded.x, rounded.y, zoom] });
2156
+ const roundedX = Math.round(x), roundedY = Math.round(y);
2157
+ (x !== roundedX || y !== roundedY) && xyflowApi.setState({ transform: [roundedX, roundedY, zoom] });
2066
2158
  }
2067
2159
  }), /* @__PURE__ */ jsx(
2068
2160
  ReactFlow,
@@ -2070,8 +2162,8 @@ function XYFlow({
2070
2162
  className,
2071
2163
  style,
2072
2164
  ...colorMode && { colorMode },
2073
- defaultNodes,
2074
- defaultEdges,
2165
+ nodes,
2166
+ edges,
2075
2167
  nodeTypes,
2076
2168
  edgeTypes,
2077
2169
  zoomOnPinch: zoomable,
@@ -2082,26 +2174,43 @@ function XYFlow({
2082
2174
  maxZoom: zoomable ? 1.9 : 1,
2083
2175
  minZoom: zoomable ? MinZoom : 1,
2084
2176
  fitView,
2085
- fitViewOptions,
2177
+ fitViewOptions: useMemo(() => ({
2178
+ minZoom: MinZoom,
2179
+ maxZoom: 1,
2180
+ padding: fitViewPadding,
2181
+ includeHiddenNodes: !0
2182
+ }), [fitViewPadding]),
2183
+ ...!fitView && {
2184
+ viewport: {
2185
+ x: translateX,
2186
+ y: translateY,
2187
+ zoom: 1
2188
+ }
2189
+ },
2086
2190
  preventScrolling: zoomable || pannable,
2087
2191
  defaultMarkerColor: "var(--xy-edge-stroke)",
2088
2192
  noDragClassName: "nodrag",
2089
2193
  noPanClassName: "nopan",
2090
2194
  panOnScroll: pannable,
2091
2195
  panOnDrag: pannable,
2196
+ ...!pannable && {
2197
+ selectionKeyCode: null
2198
+ },
2092
2199
  elementsSelectable: nodesSelectable,
2093
2200
  nodesFocusable: nodesDraggable || nodesSelectable || hasOnNodeClick || hasOnNavigateTo,
2094
- edgesFocusable: hasOnEdgeClick,
2201
+ edgesFocusable: !1,
2095
2202
  nodesDraggable,
2096
2203
  ...nodesDraggable && {
2097
2204
  onNodeDragStart: layoutConstraints.onNodeDragStart,
2098
2205
  onNodeDrag: layoutConstraints.onNodeDrag,
2099
2206
  onNodeDragStop: layoutConstraints.onNodeDragStop
2100
2207
  },
2208
+ nodeDragThreshold: 3,
2101
2209
  zoomOnDoubleClick: !1,
2102
2210
  elevateNodesOnSelect: !1,
2103
2211
  selectNodesOnDrag: !1,
2104
- selectionKeyCode: null,
2212
+ onNodesChange,
2213
+ onEdgesChange,
2105
2214
  onDoubleClick: handlers.onDoubleClick,
2106
2215
  onPaneClick: handlers.onPaneClick,
2107
2216
  onMoveEnd: handlers.onMoveEnd,
@@ -2112,9 +2221,8 @@ function XYFlow({
2112
2221
  onNodeClick: handlers.onNodeClick,
2113
2222
  onNodeDoubleClick: handlers.onNodeDoubleClick,
2114
2223
  onEdgeClick: handlers.onEdgeClick,
2115
- onInit: useCallback(() => {
2116
- diagramApi.setState({ initialized: !0 }, !1, "initialized");
2117
- }, [diagramApi]),
2224
+ onEdgeDoubleClick: handlers.onEdgeDoubleClick,
2225
+ onInit,
2118
2226
  ...hasOnNodeContextMenu && {
2119
2227
  onNodeContextMenu: handlers.onNodeContextMenu
2120
2228
  },
@@ -2129,8 +2237,19 @@ function XYFlow({
2129
2237
  );
2130
2238
  }
2131
2239
  var panel$1 = "_19mimxc0";
2240
+ const historySelector = (s) => ({
2241
+ hasStepBack: s.navigationHistoryIndex > 0,
2242
+ hasStepForward: s.navigationHistoryIndex < s.navigationHistory.length - 1,
2243
+ goBack: s.goBack,
2244
+ goForward: s.goForward
2245
+ });
2132
2246
  function BackwardForwardPanel() {
2133
- const { hasBack, hasForward, ops } = useNavigationHistory();
2247
+ const {
2248
+ hasStepBack,
2249
+ hasStepForward,
2250
+ goBack,
2251
+ goForward
2252
+ } = useDiagramState(historySelector);
2134
2253
  return /* @__PURE__ */ jsx(
2135
2254
  Group,
2136
2255
  {
@@ -2141,7 +2260,7 @@ function BackwardForwardPanel() {
2141
2260
  ),
2142
2261
  gap: "xs",
2143
2262
  children: /* @__PURE__ */ jsxs(AnimatePresence, { children: [
2144
- hasBack && /* @__PURE__ */ jsx(
2263
+ hasStepBack && /* @__PURE__ */ jsx(
2145
2264
  m.div,
2146
2265
  {
2147
2266
  layout: !0,
@@ -2156,14 +2275,16 @@ function BackwardForwardPanel() {
2156
2275
  {
2157
2276
  variant: "light",
2158
2277
  color: "gray",
2159
- onClick: () => ops.back(),
2278
+ onClick: (e) => {
2279
+ e.stopPropagation(), goBack();
2280
+ },
2160
2281
  children: /* @__PURE__ */ jsx(IconChevronLeft, {})
2161
2282
  }
2162
2283
  )
2163
2284
  },
2164
2285
  "back"
2165
2286
  ),
2166
- hasForward && /* @__PURE__ */ jsx(
2287
+ hasStepForward && /* @__PURE__ */ jsx(
2167
2288
  m.div,
2168
2289
  {
2169
2290
  layout: !0,
@@ -2178,7 +2299,9 @@ function BackwardForwardPanel() {
2178
2299
  {
2179
2300
  variant: "light",
2180
2301
  color: "gray",
2181
- onClick: () => ops.forward(),
2302
+ onClick: (e) => {
2303
+ e.stopPropagation(), goForward();
2304
+ },
2182
2305
  children: /* @__PURE__ */ jsx(IconChevronRight, {})
2183
2306
  }
2184
2307
  )
@@ -2723,23 +2846,24 @@ const XYFlowInner = memo(function({
2723
2846
  background,
2724
2847
  controls,
2725
2848
  showDiagramTitle,
2726
- showNavigationButtons,
2727
- enableDynamicViewWalkthrough
2849
+ showNavigationButtons
2728
2850
  }) {
2729
2851
  const {
2730
2852
  isDynamicView,
2731
2853
  isDynamicViewActive,
2732
- readonly
2854
+ readonly,
2855
+ enableDynamicViewWalkthrough
2733
2856
  } = useDiagramState((s) => ({
2734
- isDynamicView: s.isDynamicView(),
2857
+ isDynamicView: s.isDynamicView,
2735
2858
  readonly: s.readonly,
2736
- isDynamicViewActive: isNonNull(s.activeDynamicViewStep)
2859
+ enableDynamicViewWalkthrough: s.enableDynamicViewWalkthrough,
2860
+ isDynamicViewActive: isNonNullish(s.activeDynamicViewStep)
2737
2861
  }));
2738
2862
  return /* @__PURE__ */ jsxs(Fragment, { children: [
2739
2863
  background !== "transparent" && background !== "solid" && /* @__PURE__ */ jsx(XYFlowBackground, { background }),
2740
2864
  controls && /* @__PURE__ */ jsx(Controls, { position: "bottom-right" }),
2741
2865
  readonly === !1 && /* @__PURE__ */ jsx(OptionsPanel, {}),
2742
- !isDynamicViewActive && showDiagramTitle === !0 && /* @__PURE__ */ jsx(DiagramTitlePanel, {}),
2866
+ showDiagramTitle === !0 && !isDynamicViewActive && /* @__PURE__ */ jsx(DiagramTitlePanel, {}),
2743
2867
  isDynamicView && enableDynamicViewWalkthrough && /* @__PURE__ */ jsx(DynamicViewWalkthrough, {}),
2744
2868
  showNavigationButtons && /* @__PURE__ */ jsx(BackwardForwardPanel, {}),
2745
2869
  children
@@ -2761,7 +2885,8 @@ function LikeC4Diagram({
2761
2885
  showElementLinks = !0,
2762
2886
  showDiagramTitle = !0,
2763
2887
  showNavigationButtons = !1,
2764
- enableDynamicViewWalkthrough = !0,
2888
+ enableDynamicViewWalkthrough = !1,
2889
+ enableFocusMode = readonly,
2765
2890
  initialWidth,
2766
2891
  initialHeight,
2767
2892
  keepAspectRatio = !1,
@@ -2775,22 +2900,16 @@ function LikeC4Diagram({
2775
2900
  onNavigateTo,
2776
2901
  onNodeClick,
2777
2902
  onNodeContextMenu,
2903
+ onEditorCommand,
2778
2904
  renderIcon
2779
2905
  }) {
2780
- const initialRef = useRef();
2781
- if (!initialRef.current) {
2782
- const initial = diagramViewToXYFlowData(view, {
2783
- selectable: nodesSelectable,
2784
- draggable: nodesDraggable
2785
- });
2786
- initialRef.current = {
2787
- defaultNodes: initial.nodes,
2788
- defaultEdges: initial.edges,
2789
- initialWidth: initialWidth ?? view.width,
2790
- initialHeight: initialHeight ?? view.height
2791
- };
2792
- }
2793
- return /* @__PURE__ */ jsx(EnsureMantine, { children: /* @__PURE__ */ jsx(
2906
+ const initialRef = useRef({
2907
+ defaultNodes: [],
2908
+ defaultEdges: [],
2909
+ initialWidth: initialWidth ?? view.bounds.width,
2910
+ initialHeight: initialHeight ?? view.bounds.height
2911
+ });
2912
+ return /* @__PURE__ */ jsx(EnsureMantine, { children: /* @__PURE__ */ jsx(LazyMotion, { features: domAnimation, strict: !0, children: /* @__PURE__ */ jsx(
2794
2913
  ReactFlowProvider,
2795
2914
  {
2796
2915
  fitView,
@@ -2810,64 +2929,55 @@ function LikeC4Diagram({
2810
2929
  nodesDraggable,
2811
2930
  nodesSelectable,
2812
2931
  experimentalEdgeEditing,
2932
+ enableDynamicViewWalkthrough,
2933
+ enableFocusMode,
2813
2934
  renderIcon: renderIcon ?? null,
2814
- onCanvasClick,
2815
- onCanvasContextMenu,
2816
- onEdgeClick,
2817
- onEdgeContextMenu,
2818
- onNodeClick,
2819
- onNodeContextMenu,
2820
- onChange,
2821
- onNavigateTo,
2822
- onCanvasDblClick,
2823
- children: /* @__PURE__ */ jsx(LazyMotion, { features: domAnimation, strict: !0, children: /* @__PURE__ */ jsx(
2935
+ onCanvasClick: onCanvasClick ?? null,
2936
+ onCanvasContextMenu: onCanvasContextMenu ?? null,
2937
+ onEdgeClick: onEdgeClick ?? null,
2938
+ onEdgeContextMenu: onEdgeContextMenu ?? null,
2939
+ onNodeClick: onNodeClick ?? null,
2940
+ onNodeContextMenu: onNodeContextMenu ?? null,
2941
+ onChange: onChange ?? null,
2942
+ onNavigateTo: onNavigateTo ?? null,
2943
+ onCanvasDblClick: onCanvasDblClick ?? null,
2944
+ onEditorCommand: onEditorCommand ?? null,
2945
+ children: /* @__PURE__ */ jsx(
2824
2946
  LikeC4DiagramInnerMemo,
2825
2947
  {
2826
- defaultNodes: initialRef.current.defaultNodes,
2827
- defaultEdges: initialRef.current.defaultEdges,
2828
- fitView,
2829
- zoomable,
2830
- background,
2831
2948
  controls,
2832
- pannable,
2949
+ background,
2833
2950
  showDiagramTitle,
2834
- showNavigationButtons,
2835
- enableDynamicViewWalkthrough
2951
+ showNavigationButtons
2836
2952
  }
2837
- ) })
2953
+ )
2838
2954
  }
2839
2955
  )
2840
2956
  }
2841
- ) });
2957
+ ) }) });
2842
2958
  }
2843
2959
  const LikeC4DiagramInnerMemo = memo(function({
2844
2960
  background,
2845
- fitView,
2846
- zoomable,
2847
2961
  controls,
2848
- pannable,
2849
- defaultNodes,
2850
- defaultEdges,
2851
2962
  showDiagramTitle,
2852
- showNavigationButtons,
2853
- enableDynamicViewWalkthrough
2963
+ showNavigationButtons
2854
2964
  }) {
2855
- const diagramApi = useDiagramStoreApi(), [isInitialized, setIsInitialized] = useState(diagramApi.getState().initialized);
2856
- return useEffect(() => {
2857
- if (!isInitialized)
2858
- return diagramApi.subscribe(
2859
- (s) => s.initialized,
2860
- setIsInitialized,
2861
- {
2862
- fireImmediately: !0
2863
- }
2864
- );
2865
- }, [isInitialized]), /* @__PURE__ */ jsxs(Fragment, { children: [
2965
+ const {
2966
+ isInitialized,
2967
+ pannable,
2968
+ fitView,
2969
+ enableFocusMode
2970
+ } = useDiagramState((s) => ({
2971
+ isInitialized: s.initialized,
2972
+ zoomable: s.zoomable,
2973
+ pannable: s.pannable,
2974
+ fitView: s.fitViewEnabled,
2975
+ enableFocusMode: s.enableFocusMode
2976
+ }));
2977
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2866
2978
  /* @__PURE__ */ jsx(
2867
2979
  XYFlow,
2868
2980
  {
2869
- defaultNodes,
2870
- defaultEdges,
2871
2981
  className: clsx(
2872
2982
  "likec4-diagram",
2873
2983
  cssReactFlow,
@@ -2880,7 +2990,6 @@ const LikeC4DiagramInnerMemo = memo(function({
2880
2990
  {
2881
2991
  showNavigationButtons,
2882
2992
  showDiagramTitle,
2883
- enableDynamicViewWalkthrough,
2884
2993
  background,
2885
2994
  controls
2886
2995
  }
@@ -2888,9 +2997,8 @@ const LikeC4DiagramInnerMemo = memo(function({
2888
2997
  }
2889
2998
  ),
2890
2999
  isInitialized && /* @__PURE__ */ jsxs(Fragment, { children: [
2891
- /* @__PURE__ */ jsx(SyncWithDiagram, {}),
2892
3000
  fitView && /* @__PURE__ */ jsx(FitViewOnDiagramChange, {}),
2893
- fitView && zoomable && /* @__PURE__ */ jsx(SelectEdgesOnNodeFocus, {})
3001
+ enableFocusMode && /* @__PURE__ */ jsx(SelectEdgesOnNodeFocus, {})
2894
3002
  ] })
2895
3003
  ] });
2896
3004
  }, shallowEqual);