@likec4/diagram 1.1.0 → 1.1.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.
package/dist/index.mjs CHANGED
@@ -2,18 +2,20 @@ 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), value);
4
4
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
5
- import { createContext, memo, useState, forwardRef, useCallback, useMemo, useEffect, useRef, useContext, useLayoutEffect, useId } from "react";
5
+ import { createContext, memo, useState, useEffect, useContext, useMemo, forwardRef, useCallback, useRef, useLayoutEffect, useId } from "react";
6
6
  import { createWithEqualityFn, useStoreWithEqualityFn } from "zustand/traditional";
7
7
  import { shallow } from "zustand/shallow";
8
8
  import { createPortal } from "react-dom";
9
- import { Box, MantineContext, Stack, Text, TooltipGroup, Divider, Flex, Tooltip, ColorSwatch, CheckIcon, rem, Select, Card, ActionIcon, HoverCard, HoverCardTarget, UnstyledButton, HoverCardDropdown, Group, Anchor, CopyButton, Button, useMantineColorScheme } from "@mantine/core";
9
+ import { Box, MantineContext, Paper, Stack, Group, Text, Spoiler, Button, Anchor, CopyButton, Divider, TooltipGroup, Flex, Tooltip, ColorSwatch, CheckIcon, rem, Select, Slider, SegmentedControl, Card, ActionIcon, HoverCard, HoverCardTarget, UnstyledButton, HoverCardDropdown, useMantineColorScheme } from "@mantine/core";
10
10
  import { invariant, isAncestor, nonNullable, nonexhaustive, defaultTheme, ElementShapes } from "@likec4/core";
11
11
  import { DEV } from "esm-env";
12
12
  import { shallowEqual, deepEqual } from "fast-equals";
13
+ import { isNullish, entries, keys, hasAtLeast, takeWhile, clamp as clamp$1, isNumber } from "remeda";
13
14
  import { subscribeWithSelector, devtools } from "zustand/middleware";
14
15
  import { useFirstMountState, useCustomCompareEffect, useDebouncedCallback, useRafEffect, useDebouncedEffect } from "@react-hookz/web";
15
16
  import { AnimatePresence, motion } from "framer-motion";
16
- import { isNullish, keys, hasAtLeast, takeWhile, clamp as clamp$1, isNumber } from "remeda";
17
+ import { useLocalStorage } from "@mantine/hooks";
18
+ import { line, curveCatmullRomOpen } from "d3-shape";
17
19
  var rootClassName = "likec4-diagram-root";
18
20
  function cc(names) {
19
21
  if (typeof names == "string" || typeof names == "number")
@@ -4669,9 +4671,9 @@ function useVisibleNodeIds(onlyRenderVisible) {
4669
4671
  }
4670
4672
  const selector$e = (s) => s.updateNodeInternals;
4671
4673
  function useResizeObserver() {
4672
- const updateNodeInternals2 = useStore(selector$e), [resizeObserver] = useState(() => typeof ResizeObserver > "u" ? null : new ResizeObserver((entries) => {
4674
+ const updateNodeInternals2 = useStore(selector$e), [resizeObserver] = useState(() => typeof ResizeObserver > "u" ? null : new ResizeObserver((entries2) => {
4673
4675
  const updates = /* @__PURE__ */ new Map();
4674
- entries.forEach((entry) => {
4676
+ entries2.forEach((entry) => {
4675
4677
  const id2 = entry.target.getAttribute("data-id");
4676
4678
  updates.set(id2, {
4677
4679
  id: id2,
@@ -5645,7 +5647,7 @@ function NodeComponentWrapperInner({ id: id2, nodeOrigin, nodeColorFunc, nodeStr
5645
5647
  }
5646
5648
  const NodeComponentWrapper = memo(NodeComponentWrapperInner);
5647
5649
  var MiniMapNodes$1 = memo(MiniMapNodes);
5648
- const defaultWidth = 200, defaultHeight = 150, selector$1 = (s) => {
5650
+ const defaultWidth = 200, defaultHeight = 150, selector$1$1 = (s) => {
5649
5651
  const viewBB = {
5650
5652
  x: -s.transform[0] / s.transform[2],
5651
5653
  y: -s.transform[1] / s.transform[2],
@@ -5688,7 +5690,7 @@ function MiniMapComponent({
5688
5690
  zoomStep = 10,
5689
5691
  offsetScale = 5
5690
5692
  }) {
5691
- const store = useStoreApi(), svg = useRef(null), { boundingRect, viewBB, rfId, panZoom, translateExtent, flowWidth, flowHeight } = useStore(selector$1, shallow), elementWidth = (style2 == null ? void 0 : style2.width) ?? defaultWidth, elementHeight = (style2 == null ? void 0 : style2.height) ?? defaultHeight, scaledWidth = boundingRect.width / elementWidth, scaledHeight = boundingRect.height / elementHeight, viewScale = Math.max(scaledWidth, scaledHeight), viewWidth = viewScale * elementWidth, viewHeight = viewScale * elementHeight, offset = offsetScale * viewScale, x = boundingRect.x - (viewWidth - boundingRect.width) / 2 - offset, y = boundingRect.y - (viewHeight - boundingRect.height) / 2 - offset, width = viewWidth + offset * 2, height = viewHeight + offset * 2, labelledBy = `${ARIA_LABEL_KEY}-${rfId}`, viewScaleRef = useRef(0), minimapInstance = useRef();
5693
+ const store = useStoreApi(), svg = useRef(null), { boundingRect, viewBB, rfId, panZoom, translateExtent, flowWidth, flowHeight } = useStore(selector$1$1, shallow), elementWidth = (style2 == null ? void 0 : style2.width) ?? defaultWidth, elementHeight = (style2 == null ? void 0 : style2.height) ?? defaultHeight, scaledWidth = boundingRect.width / elementWidth, scaledHeight = boundingRect.height / elementHeight, viewScale = Math.max(scaledWidth, scaledHeight), viewWidth = viewScale * elementWidth, viewHeight = viewScale * elementHeight, offset = offsetScale * viewScale, x = boundingRect.x - (viewWidth - boundingRect.width) / 2 - offset, y = boundingRect.y - (viewHeight - boundingRect.height) / 2 - offset, width = viewWidth + offset * 2, height = viewHeight + offset * 2, labelledBy = `${ARIA_LABEL_KEY}-${rfId}`, viewScaleRef = useRef(0), minimapInstance = useRef();
5692
5694
  viewScaleRef.current = viewScale, useEffect(() => {
5693
5695
  if (svg.current && panZoom)
5694
5696
  return minimapInstance.current = XYMinimap({
@@ -7294,51 +7296,87 @@ function createDiagramStore(props) {
7294
7296
  setLastClickedEdge: (edgeId) => {
7295
7297
  edgeId !== get2().lastClickedEdgeId && set2({ lastClickedEdgeId: edgeId }, noReplace, edgeId ? "setLastClickedEdge" : "unsetLastClickedEdge");
7296
7298
  },
7299
+ resetLastClicked: () => {
7300
+ let {
7301
+ focusedNodeId,
7302
+ lastClickedNodeId,
7303
+ lastClickedEdgeId
7304
+ } = get2();
7305
+ (focusedNodeId || lastClickedNodeId || lastClickedEdgeId) && set2(
7306
+ {
7307
+ focusedNodeId: null,
7308
+ lastClickedNodeId: null,
7309
+ lastClickedEdgeId: null
7310
+ },
7311
+ noReplace,
7312
+ "resetLastClicked"
7313
+ );
7314
+ },
7297
7315
  getElement: (fqn) => {
7298
7316
  const { view } = get2();
7299
7317
  return view.nodes.find(({ id: id2 }) => id2 === fqn) ?? null;
7300
7318
  },
7301
7319
  triggerOnChange: (changes) => {
7302
7320
  var _a, _b;
7303
- const newShapes = /* @__PURE__ */ new Map(), newColor = /* @__PURE__ */ new Map();
7321
+ DEV && console.debug("triggerOnChange", changes);
7322
+ const newShapes = /* @__PURE__ */ new Map(), newColor = /* @__PURE__ */ new Map(), newOpacity = /* @__PURE__ */ new Map(), newBorder = /* @__PURE__ */ new Map();
7304
7323
  for (const change of changes) {
7305
- if (change.op === "change-shape") {
7306
- for (const target of change.targets)
7307
- newShapes.set(target, change.shape);
7308
- continue;
7309
- }
7310
- if (change.op === "change-color") {
7311
- for (const target of change.targets)
7312
- newColor.set(target, change.color);
7313
- continue;
7314
- }
7315
- nonexhaustive(change);
7324
+ change.op !== "change-element-style" && nonexhaustive(change.op);
7325
+ for (const target of change.targets)
7326
+ for (const [key, value] of entries.strict(change.style))
7327
+ switch (key) {
7328
+ case "shape":
7329
+ newShapes.set(target, value);
7330
+ break;
7331
+ case "color":
7332
+ newColor.set(target, value);
7333
+ break;
7334
+ case "opacity":
7335
+ newOpacity.set(target, value);
7336
+ break;
7337
+ case "border":
7338
+ newBorder.set(target, value);
7339
+ break;
7340
+ default:
7341
+ nonexhaustive(key);
7342
+ }
7316
7343
  }
7317
7344
  get2().xyflow.setNodes(
7318
7345
  (nodes) => nodes.map((node) => {
7319
- let { data } = node;
7320
- const shape = newShapes.get(data.fqn);
7321
- shape && shape !== data.element.shape && (node = {
7322
- ...node,
7323
- data: {
7324
- ...data,
7325
- element: {
7326
- ...data.element,
7327
- shape
7328
- }
7346
+ var _a2, _b2;
7347
+ let element = node.data.element;
7348
+ const shape = newShapes.get(element.id);
7349
+ shape && shape !== element.shape && (element = {
7350
+ ...element,
7351
+ shape
7352
+ });
7353
+ const color2 = newColor.get(element.id);
7354
+ color2 && color2 !== element.color && (element = {
7355
+ ...element,
7356
+ color: color2
7357
+ });
7358
+ const opacity = newOpacity.get(element.id);
7359
+ opacity && opacity !== ((_a2 = element.style) == null ? void 0 : _a2.opacity) && (element = {
7360
+ ...element,
7361
+ style: {
7362
+ ...element.style,
7363
+ opacity
7329
7364
  }
7330
- }, data = node.data);
7331
- const color2 = newColor.get(data.fqn);
7332
- return color2 && color2 !== data.element.color && (node = {
7365
+ });
7366
+ const border = newBorder.get(element.id);
7367
+ return border && border !== ((_b2 = element.style) == null ? void 0 : _b2.border) && (element = {
7368
+ ...element,
7369
+ style: {
7370
+ ...element.style,
7371
+ border
7372
+ }
7373
+ }), element === node.data.element ? node : {
7333
7374
  ...node,
7334
7375
  data: {
7335
- ...data,
7336
- element: {
7337
- ...data.element,
7338
- color: color2
7339
- }
7376
+ ...node.data,
7377
+ element
7340
7378
  }
7341
- }), node;
7379
+ };
7342
7380
  })
7343
7381
  ), (_b = (_a = get2()).onChange) == null || _b.call(_a, { changes });
7344
7382
  },
@@ -7376,7 +7414,7 @@ function createDiagramStore(props) {
7376
7414
  xyflow.fitView({
7377
7415
  includeHiddenNodes: !0,
7378
7416
  duration: centerNode && focusedNodeId ? 300 : 400,
7379
- padding: centerNode ? 0 : fitViewPadding,
7417
+ padding: fitViewPadding,
7380
7418
  minZoom: MinZoom,
7381
7419
  maxZoom: 1,
7382
7420
  ...centerNode && { nodes: [centerNode] }
@@ -7420,6 +7458,146 @@ function DiagramContextProvider({ children: children2, view, ...props }) {
7420
7458
  [props]
7421
7459
  ), /* @__PURE__ */ jsx(DiagramContext.Provider, { value: store.current, children: children2 });
7422
7460
  }
7461
+ var container$3 = "hbkbtj0", paper = "hbkbtj1", title$2 = "hbkbtj2", description$1 = "hbkbtj3";
7462
+ const selector$1 = (s) => ({
7463
+ id: s.view.id,
7464
+ title: s.view.title ?? "untitled",
7465
+ description: s.view.description,
7466
+ links: s.view.links
7467
+ });
7468
+ function DiagramTitlePanel() {
7469
+ const { id: id2, title: title2, description: description2, links } = useDiagramState(selector$1), [isCollapsed, setCollapsed] = useLocalStorage({
7470
+ key: "diagram-title-panel-collapsed",
7471
+ defaultValue: !1
7472
+ }), toggle = () => setCollapsed((v) => !v);
7473
+ return /* @__PURE__ */ jsx(AnimatePresence, { mode: "wait", children: /* @__PURE__ */ jsx(
7474
+ motion.div,
7475
+ {
7476
+ initial: { opacity: 0.05, scale: 0.7 },
7477
+ animate: { opacity: 1, scale: 1 },
7478
+ exit: {
7479
+ opacity: 0.05,
7480
+ scale: 0.6
7481
+ },
7482
+ className: clsx("react-flow__panel", container$3),
7483
+ style: {
7484
+ transformOrigin: "left center"
7485
+ },
7486
+ children: /* @__PURE__ */ jsx(
7487
+ Paper,
7488
+ {
7489
+ radius: "sm",
7490
+ withBorder: !isCollapsed,
7491
+ className: paper,
7492
+ p: isCollapsed ? "sm" : "md",
7493
+ onDoubleClick: (e) => e.stopPropagation(),
7494
+ children: /* @__PURE__ */ jsxs(Stack, { gap: "sm", justify: "stretch", align: "stretch", children: [
7495
+ /* @__PURE__ */ jsxs(Group, { justify: "stretch", wrap: "nowrap", onClick: toggle, style: { cursor: "pointer" }, children: [
7496
+ /* @__PURE__ */ jsx(
7497
+ Text,
7498
+ {
7499
+ component: "div",
7500
+ flex: "1",
7501
+ size: isCollapsed ? "sm" : "lg",
7502
+ fw: 500,
7503
+ lh: 1.1,
7504
+ className: title$2,
7505
+ children: title2
7506
+ }
7507
+ ),
7508
+ /* @__PURE__ */ jsxs(
7509
+ Text,
7510
+ {
7511
+ hidden: isCollapsed,
7512
+ component: "div",
7513
+ flex: "0 0 auto",
7514
+ inline: !0,
7515
+ size: "xs",
7516
+ fz: 9,
7517
+ fw: 500,
7518
+ c: "dimmed",
7519
+ style: {
7520
+ userSelect: "all"
7521
+ },
7522
+ children: [
7523
+ /* @__PURE__ */ jsxs("span", { style: { userSelect: "none" }, children: [
7524
+ "id:",
7525
+ " "
7526
+ ] }),
7527
+ id2
7528
+ ]
7529
+ }
7530
+ )
7531
+ ] }),
7532
+ !isCollapsed && /* @__PURE__ */ jsxs(Fragment, { children: [
7533
+ description2 && /* @__PURE__ */ jsx(
7534
+ Spoiler,
7535
+ {
7536
+ maxHeight: 42,
7537
+ showLabel: /* @__PURE__ */ jsx(Button, { color: "gray", variant: "light", fz: "10", size: "compact-xs", children: "show more" }),
7538
+ hideLabel: /* @__PURE__ */ jsx(Button, { color: "gray", variant: "light", fz: "10", size: "compact-xs", children: "hide" }),
7539
+ children: /* @__PURE__ */ jsx(
7540
+ Text,
7541
+ {
7542
+ component: "div",
7543
+ size: "sm",
7544
+ className: description$1,
7545
+ children: description2 || "no description"
7546
+ }
7547
+ )
7548
+ }
7549
+ ),
7550
+ !description2 && /* @__PURE__ */ jsx(
7551
+ Text,
7552
+ {
7553
+ component: "div",
7554
+ size: "xs",
7555
+ c: "dimmed",
7556
+ children: "no description"
7557
+ }
7558
+ ),
7559
+ links && /* @__PURE__ */ jsx(
7560
+ Stack,
7561
+ {
7562
+ gap: 3,
7563
+ justify: "stretch",
7564
+ align: "stretch",
7565
+ children: links.map((link) => /* @__PURE__ */ jsxs(Group, { wrap: "nowrap", align: "center", gap: "sm", children: [
7566
+ /* @__PURE__ */ jsx(Box, { flex: "1", style: { overflow: "hidden" }, children: /* @__PURE__ */ jsx(
7567
+ Anchor,
7568
+ {
7569
+ href: link,
7570
+ target: "_blank",
7571
+ fz: "xs",
7572
+ truncate: "end",
7573
+ display: "inline-block",
7574
+ w: "100%",
7575
+ children: link
7576
+ }
7577
+ ) }),
7578
+ /* @__PURE__ */ jsx(CopyButton, { value: link, children: ({ copied, copy }) => /* @__PURE__ */ jsx(
7579
+ Button,
7580
+ {
7581
+ size: "compact-xs",
7582
+ fz: "10",
7583
+ variant: "light",
7584
+ onClick: copy,
7585
+ color: copied ? "teal" : "gray",
7586
+ children: copied ? "copied" : "copy"
7587
+ }
7588
+ ) })
7589
+ ] }, link))
7590
+ }
7591
+ )
7592
+ ] })
7593
+ ] })
7594
+ }
7595
+ )
7596
+ },
7597
+ id2
7598
+ ) });
7599
+ }
7600
+ var panel = "_1lmzt1h0";
7423
7601
  const {
7424
7602
  primary,
7425
7603
  secondary,
@@ -7437,7 +7615,16 @@ function NodeOptions({ selectedNodeIds }) {
7437
7615
  const diagramApi = useDiagramStoreApi(), nodes = useXYNodesData(selectedNodeIds);
7438
7616
  if (!hasAtLeast(nodes, 1))
7439
7617
  return null;
7440
- const showShapeOption = nodes.some((node) => node.type === "element"), [firstNode, ...rest] = nodes;
7618
+ if (nodes.length !== selectedNodeIds.length)
7619
+ throw new Error("NodeOptions: nodes and props.nodes should have the same length");
7620
+ const showShapeOption = nodes.some((node) => node.type === "element"), [firstNode, ...rest] = nodes, showOpacityOption = rest.length === 0 && firstNode.type === "compound", triggerChange = (style2) => {
7621
+ const targets = nodes.map((node) => node.data.element.id);
7622
+ invariant(hasAtLeast(targets, 1), "At least one target is required"), diagramApi.getState().triggerOnChange([{
7623
+ op: "change-element-style",
7624
+ style: style2,
7625
+ targets
7626
+ }]);
7627
+ };
7441
7628
  return /* @__PURE__ */ jsxs(Stack, { children: [
7442
7629
  /* @__PURE__ */ jsx(Box, { maw: 200, children: /* @__PURE__ */ jsx(Text, { size: "xs", c: rest.length > 0 ? "dimmed" : "", truncate: !0, children: rest.length === 0 ? firstNode.data.element.title : "[ multiple ]" }) }),
7443
7630
  showShapeOption && /* @__PURE__ */ jsx(
@@ -7445,14 +7632,7 @@ function NodeOptions({ selectedNodeIds }) {
7445
7632
  {
7446
7633
  nodes,
7447
7634
  onShapeChange: (shape) => {
7448
- const targets = [];
7449
- for (const nd of nodes)
7450
- nd.data.element.shape !== shape && targets.push(nd.data.element.id);
7451
- hasAtLeast(targets, 1) && diagramApi.getState().triggerOnChange([{
7452
- op: "change-shape",
7453
- shape,
7454
- targets
7455
- }]);
7635
+ triggerChange({ shape });
7456
7636
  }
7457
7637
  }
7458
7638
  ),
@@ -7461,18 +7641,32 @@ function NodeOptions({ selectedNodeIds }) {
7461
7641
  {
7462
7642
  nodes,
7463
7643
  onColorChange: (color2) => {
7464
- const targets = [];
7465
- for (const nd of nodes)
7466
- nd.data.element.color !== color2 && targets.push(nd.data.element.id);
7467
- hasAtLeast(targets, 1) && diagramApi.getState().triggerOnChange([{
7468
- op: "change-color",
7469
- color: color2,
7470
- targets
7471
- }]);
7644
+ triggerChange({ color: color2 });
7472
7645
  }
7473
7646
  }
7474
7647
  ),
7475
- /* @__PURE__ */ jsx(NavigateToOption, { nodes })
7648
+ /* @__PURE__ */ jsx(NavigateToOption, { nodes }),
7649
+ /* @__PURE__ */ jsx(Box, { children: showOpacityOption && /* @__PURE__ */ jsxs(Fragment, { children: [
7650
+ /* @__PURE__ */ jsx(Divider, { label: "opacity and border", labelPosition: "left" }),
7651
+ /* @__PURE__ */ jsx(
7652
+ OpacityOption,
7653
+ {
7654
+ node: firstNode,
7655
+ onOpacityChange: (opacity) => {
7656
+ triggerChange({ opacity });
7657
+ }
7658
+ }
7659
+ ),
7660
+ /* @__PURE__ */ jsx(
7661
+ BorderStyleOption,
7662
+ {
7663
+ node: firstNode,
7664
+ onChange: (border) => {
7665
+ triggerChange({ border });
7666
+ }
7667
+ }
7668
+ )
7669
+ ] }) }, firstNode.id)
7476
7670
  ] });
7477
7671
  }
7478
7672
  function Colors({
@@ -7578,8 +7772,54 @@ function NavigateToOption({
7578
7772
  )
7579
7773
  ] });
7580
7774
  }
7581
- var panel = "_1lmzt1h0";
7582
- const OptionsPanelMemo = /* @__PURE__ */ memo(function() {
7775
+ function OpacityOption({
7776
+ node,
7777
+ onOpacityChange
7778
+ }) {
7779
+ let selectedOpacity = node.data.element.style.opacity ?? 100;
7780
+ const [value, setValue] = useState(selectedOpacity);
7781
+ return useEffect(() => {
7782
+ setValue(selectedOpacity);
7783
+ }, [selectedOpacity]), /* @__PURE__ */ jsx(
7784
+ Slider,
7785
+ {
7786
+ mt: "xs",
7787
+ size: "sm",
7788
+ color: "dark",
7789
+ value,
7790
+ onChange: setValue,
7791
+ onChangeEnd: onOpacityChange
7792
+ }
7793
+ );
7794
+ }
7795
+ function BorderStyleOption({
7796
+ node,
7797
+ onChange
7798
+ }) {
7799
+ let selecteBorderStyle = node.data.element.style.border ?? "dashed";
7800
+ const [value, setValue] = useState(selecteBorderStyle);
7801
+ return useEffect(() => {
7802
+ setValue(selecteBorderStyle);
7803
+ }, [selecteBorderStyle]), /* @__PURE__ */ jsx(Box, { mt: "md", children: /* @__PURE__ */ jsx(
7804
+ SegmentedControl,
7805
+ {
7806
+ size: "xs",
7807
+ fullWidth: !0,
7808
+ withItemsBorders: !1,
7809
+ value,
7810
+ onChange: (v) => {
7811
+ setValue(v), onChange(v);
7812
+ },
7813
+ data: [
7814
+ { label: "Solid", value: "solid" },
7815
+ { label: "Dashed", value: "dashed" },
7816
+ { label: "Dotted", value: "dotted" },
7817
+ { label: "None", value: "none" }
7818
+ ]
7819
+ }
7820
+ ) });
7821
+ }
7822
+ function OptionsPanel() {
7583
7823
  const [selectedNodes, setSelectedNodes] = useState([]);
7584
7824
  return useOnSelectionChange({
7585
7825
  onChange: ({ nodes }) => {
@@ -7603,7 +7843,7 @@ const OptionsPanelMemo = /* @__PURE__ */ memo(function() {
7603
7843
  children: /* @__PURE__ */ jsx(Card, { shadow: "sm", children: /* @__PURE__ */ jsx(NodeOptions, { selectedNodeIds: selectedNodes }) })
7604
7844
  }
7605
7845
  ) });
7606
- }), OptionsPanel2 = OptionsPanelMemo;
7846
+ }
7607
7847
  function deriveEdgePoints(bezierSpline) {
7608
7848
  let [start2, ...bezierPoints] = bezierSpline;
7609
7849
  invariant(start2, "start should be defined");
@@ -7758,12 +7998,12 @@ function FitViewOnDiagramChanges() {
7758
7998
  (s) => s.viewportChanged,
7759
7999
  setViewportMoved
7760
8000
  ),
7761
- [diagramApi]
8001
+ [diagramApi, setViewportMoved]
7762
8002
  ), useRafEffect(
7763
8003
  () => {
7764
8004
  !isReady || processedChangeId === pendingChangeId || (diagramApi.getState().fitDiagram(), setProcessed(pendingChangeId));
7765
8005
  },
7766
- [isReady, processedChangeId, pendingChangeId]
8006
+ [isReady, processedChangeId, pendingChangeId, diagramApi]
7767
8007
  ), !viewportMoved && isReady ? /* @__PURE__ */ jsx(FitViewOnViewportResize, { diagramApi }) : null;
7768
8008
  }
7769
8009
  const selectFitViewFlag = (s) => s.fitViewEnabled, FitViewOnDiagramChange = memo(() => useDiagramState(selectFitViewFlag) ? /* @__PURE__ */ jsx(FitViewOnDiagramChanges, {}) : null);
@@ -7779,13 +8019,19 @@ const isSameNode = (a) => (b) => a.id === b.id && a.type === b.type && Object.is
7779
8019
  (state) => ({
7780
8020
  viewId: state.view.id,
7781
8021
  nodes: state.view.nodes,
7782
- edges: state.view.edges,
7783
- draggable: state.nodesDraggable,
7784
- selectable: state.nodesSelectable
8022
+ edges: state.view.edges
7785
8023
  }),
7786
8024
  // listener
7787
- ({ viewId, nodes, edges, ...opts }) => {
7788
- const { lastOnNavigate } = diagramStoreApi.getState(), updates = diagramViewToXYFlowData({ nodes, edges }, opts);
8025
+ ({ viewId, nodes, edges }) => {
8026
+ const {
8027
+ lastOnNavigate,
8028
+ nodesDraggable,
8029
+ nodesSelectable,
8030
+ focusedNodeId
8031
+ } = diagramStoreApi.getState(), updates = diagramViewToXYFlowData({ nodes, edges }, {
8032
+ draggable: nodesDraggable,
8033
+ selectable: nodesSelectable || focusedNodeId !== null
8034
+ });
7789
8035
  if (xyflowRef.current.setNodes(
7790
8036
  (prev) => updates.nodes.map((update) => {
7791
8037
  const existing = prev.find(isSameNode(update));
@@ -7888,7 +8134,7 @@ function assignInlineVars(varsOrContract, tokens) {
7888
8134
  writable: !1
7889
8135
  }), styles;
7890
8136
  }
7891
- var container$2 = "bexn7q0", edgePathBg = "bexn7q1", fillStrokeCtx = "bexn7q2", cssEdgePath = "bexn7q3", varLabelX = "var(--bexn7q4)", varLabelY = "var(--bexn7q5)", edgeLabel = "bexn7q7", edgeLabelBody = "bexn7q8";
8137
+ var container$2 = "bexn7q1", edgePathBg = "bexn7q2", fillStrokeCtx = "bexn7q3", cssEdgePath = "bexn7q6", varLabelX = "var(--bexn7q7)", varLabelY = "var(--bexn7q8)", edgeLabel = "bexn7qa", edgeLabelBody = "bexn7qb";
7892
8138
  function getNodeIntersection(intersectionNode, targetNode) {
7893
8139
  const {
7894
8140
  width: intersectionNodeWidth,
@@ -7950,28 +8196,23 @@ function bezierPath(bezierSpline) {
7950
8196
  }
7951
8197
  return invariant(points.length === 0, "all points should be consumed"), path;
7952
8198
  }
7953
- const isSamePoint = (a, b) => Math.abs(a - b) < 2.1, isSamePosition = (a, [bx, by]) => isSamePoint(a.x, bx) && isSamePoint(a.y, by), isEqualProps = (prev, next) => prev.id === next.id && prev.source === next.source && prev.target === next.target && isSamePoint(prev.sourceX, next.sourceX) && isSamePoint(prev.sourceY, next.sourceY) && isSamePoint(prev.targetX, next.targetX) && isSamePoint(prev.targetY, next.targetY) && deepEqual(prev.data, next.data), RelationshipEdge = /* @__PURE__ */ memo(function({
8199
+ const isSamePoint = (a, b) => Math.abs(a - b) < 2.1, isSamePosition = (a, [bx, by]) => isSamePoint(a.x, bx) && isSamePoint(a.y, by), isEqualProps = (prev, next) => prev.id === next.id && prev.source === next.source && prev.target === next.target && deepEqual(prev.selected ?? !1, next.selected ?? !1) && isSamePoint(prev.sourceX, next.sourceX) && isSamePoint(prev.sourceY, next.sourceY) && isSamePoint(prev.targetX, next.targetX) && isSamePoint(prev.targetY, next.targetY) && deepEqual(prev.data, next.data), curve = line().curve(curveCatmullRomOpen.alpha(1)), RelationshipEdge = /* @__PURE__ */ memo(function({
7954
8200
  id: id2,
7955
8201
  data,
7956
8202
  selected: _selected,
7957
- markerEnd: _markerEnd,
7958
8203
  style: style2,
7959
8204
  source,
7960
8205
  target,
7961
8206
  interactionWidth
7962
8207
  }) {
7963
8208
  var _a, _b, _c;
7964
- const { nodeLookup, edgeLookup } = useXYStoreApi().getState(), sourceNode = nonNullable(nodeLookup.get(source), `source node ${source} not found`), targetNode = nonNullable(nodeLookup.get(target), `target node ${target} not found`), isNotModified = isSamePosition(sourceNode.internals.positionAbsolute, sourceNode.data.element.position) && isSamePosition(targetNode.internals.positionAbsolute, targetNode.data.element.position);
7965
- invariant(data, "data is required");
7966
- const {
7967
- edge
7968
- } = data, color2 = edge.color ?? "gray", isHovered = useDiagramState((s) => s.hoveredEdgeId === id2), line = edge.line ?? "dashed", isDotted = line === "dotted", isDashed = isDotted || line === "dashed";
8209
+ const { nodeLookup, edgeLookup } = useXYStoreApi().getState(), sourceNode = nonNullable(nodeLookup.get(source), `source node ${source} not found`), targetNode = nonNullable(nodeLookup.get(target), `target node ${target} not found`), isNotModified = isSamePosition(sourceNode.internals.positionAbsolute, sourceNode.data.element.position) && isSamePosition(targetNode.internals.positionAbsolute, targetNode.data.element.position), { edge: diagramEdge, controlPoints } = data, color2 = diagramEdge.color ?? "gray", isHovered = useDiagramState((s) => s.hoveredEdgeId === id2), line2 = diagramEdge.line ?? "dashed", isDotted = line2 === "dotted", isDashed = isDotted || line2 === "dashed";
7969
8210
  let strokeDasharray;
7970
8211
  isDotted ? strokeDasharray = "1,8" : isDashed && (strokeDasharray = "8,10");
7971
8212
  const marker = `url(#arrow-${id2})`;
7972
8213
  let edgePath, labelX, labelY;
7973
8214
  if (isNotModified)
7974
- edgePath = bezierPath(edge.points), labelX = ((_a = data.label) == null ? void 0 : _a.bbox.x) ?? 0, labelY = ((_b = data.label) == null ? void 0 : _b.bbox.y) ?? 0;
8215
+ edgePath = bezierPath(diagramEdge.points), labelX = ((_a = data.label) == null ? void 0 : _a.bbox.x) ?? 0, labelY = ((_b = data.label) == null ? void 0 : _b.bbox.y) ?? 0;
7975
8216
  else {
7976
8217
  const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(sourceNode, targetNode), [_edgePath, _labelX, _labelY] = getBezierPath({
7977
8218
  sourceX: sx,
@@ -7982,6 +8223,14 @@ const isSamePoint = (a, b) => Math.abs(a - b) < 2.1, isSamePosition = (a, [bx, b
7982
8223
  targetY: ty
7983
8224
  });
7984
8225
  edgePath = _edgePath, labelX = _labelX, labelY = _labelY;
8226
+ const sourceCenterPos = getNodePositionWithOrigin(sourceNode, [-0.5, -0.5]), targetCenterPos = getNodePositionWithOrigin(targetNode, [-0.5, -0.5]);
8227
+ edgePath = curve([
8228
+ [sourceCenterPos.positionAbsolute.x, sourceCenterPos.positionAbsolute.y],
8229
+ [sx, sy],
8230
+ ...controlPoints,
8231
+ [tx, ty],
8232
+ [targetCenterPos.positionAbsolute.x, targetCenterPos.positionAbsolute.y]
8233
+ ]);
7985
8234
  }
7986
8235
  return /* @__PURE__ */ jsxs("g", { className: clsx(container$2), "data-likec4-color": color2, "data-edge-hovered": isHovered, children: [
7987
8236
  /* @__PURE__ */ jsx("g", { className: clsx(fillStrokeCtx), children: /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsx(
@@ -8005,8 +8254,8 @@ const isSamePoint = (a, b) => Math.abs(a - b) < 2.1, isSamePosition = (a, [bx, b
8005
8254
  edgePath,
8006
8255
  interactionWidth: interactionWidth ?? 10,
8007
8256
  strokeDasharray,
8008
- markerStart: edge.tailArrow ? marker : void 0,
8009
- markerEnd: edge.headArrow ? marker : void 0,
8257
+ markerStart: diagramEdge.tailArrow ? marker : void 0,
8258
+ markerEnd: diagramEdge.headArrow ? marker : void 0,
8010
8259
  style: style2
8011
8260
  }
8012
8261
  ),
@@ -8030,7 +8279,7 @@ const isSamePoint = (a, b) => Math.abs(a - b) < 2.1, isSamePosition = (a, [bx, b
8030
8279
  }
8031
8280
  ) })
8032
8281
  ] });
8033
- }, isEqualProps), RelationshipPath = memo(({
8282
+ }, isEqualProps), RelationshipPath = ({
8034
8283
  edgePath,
8035
8284
  interactionWidth,
8036
8285
  strokeDasharray,
@@ -8069,7 +8318,7 @@ const isSamePoint = (a, b) => Math.abs(a - b) < 2.1, isSamePosition = (a, [bx, b
8069
8318
  strokeWidth: interactionWidth
8070
8319
  }
8071
8320
  )
8072
- ] })), Link = forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
8321
+ ] }), Link = forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
8073
8322
  "svg",
8074
8323
  {
8075
8324
  ref,
@@ -8596,6 +8845,36 @@ function ElementNode({
8596
8845
  }
8597
8846
  );
8598
8847
  }
8848
+ function SelectEdgesOnNodeFocus() {
8849
+ const focusedNodeId = useDiagramState((s) => s.focusedNodeId), xyflowApi = useXYStoreApi();
8850
+ return useUpdateEffect(() => {
8851
+ const edgeChanges = [], nodeChanges = [], { edges, nodes, triggerNodeChanges, triggerEdgeChanges } = xyflowApi.getState();
8852
+ if (focusedNodeId) {
8853
+ for (const edge of edges)
8854
+ edge.source === focusedNodeId || edge.target === focusedNodeId ? edgeChanges.push({
8855
+ id: edge.id,
8856
+ type: "select",
8857
+ selected: !0
8858
+ }) : edge.selected && edgeChanges.push({
8859
+ id: edge.id,
8860
+ type: "select",
8861
+ selected: !1
8862
+ });
8863
+ for (const node of nodes)
8864
+ node.selected && node.id !== focusedNodeId && nodeChanges.push({
8865
+ id: node.id,
8866
+ type: "select",
8867
+ selected: !1
8868
+ });
8869
+ nodeChanges.push({
8870
+ id: focusedNodeId,
8871
+ type: "select",
8872
+ selected: !0
8873
+ }), edgeChanges.length > 0 && triggerEdgeChanges(edgeChanges), nodeChanges.length > 0 && triggerNodeChanges(nodeChanges);
8874
+ } else
8875
+ xyflowApi.getState().resetSelectedElements();
8876
+ }, [focusedNodeId]), null;
8877
+ }
8599
8878
  function literalToEnum(value) {
8600
8879
  switch (value) {
8601
8880
  case "dots":
@@ -8613,14 +8892,19 @@ const XYFlowBackground = memo(({ background }) => {
8613
8892
  return /* @__PURE__ */ jsx(Background, { variant: literalToEnum(background) });
8614
8893
  const { variant, ...rest } = background;
8615
8894
  return /* @__PURE__ */ jsx(Background, { variant: literalToEnum(variant), ...rest });
8616
- }, deepEqual);
8895
+ }, deepEqual), viewportDomNode = (state) => {
8896
+ var _a;
8897
+ return ((_a = state.domNode) == null ? void 0 : _a.querySelector(".react-flow__viewport")) ?? null;
8898
+ };
8617
8899
  function useXYFlowEvents() {
8618
- const diagramApi = useDiagramStoreApi(), xyflowApi = useXYStoreApi(), dblclickTimeout = useRef(), dbclickGuarg = () => dblclickTimeout.current !== void 0 ? !0 : (dblclickTimeout.current = window.setTimeout(() => {
8900
+ const diagramApi = useDiagramStoreApi(), xyflowApi = useXYStoreApi(), dblclickTimeout = useRef(), viewportRef = useRef(viewportDomNode(xyflowApi.getState())), dbclickGuarg = () => dblclickTimeout.current !== void 0 ? !0 : (dblclickTimeout.current = window.setTimeout(() => {
8619
8901
  dblclickTimeout.current = void 0;
8620
- }, 250), !1);
8621
- return useMemo(() => ({
8902
+ }, 250), !1), transformRef = useRef(""), cbRef = useRef();
8903
+ return useEffect(() => xyflowApi.subscribe((state, prev) => {
8904
+ (state.domNode !== prev.domNode || state.domNode && !viewportRef.current) && (viewportRef.current = viewportDomNode(state));
8905
+ }), [xyflowApi]), useMemo(() => ({
8622
8906
  onDoubleClick: (event) => {
8623
- diagramApi.getState().setLastClickedNode(null), diagramApi.getState().setLastClickedEdge(null);
8907
+ diagramApi.getState().resetLastClicked();
8624
8908
  const diagramState = diagramApi.getState();
8625
8909
  if (diagramState.onCanvasDblClick) {
8626
8910
  diagramState.onCanvasDblClick(event);
@@ -8634,9 +8918,8 @@ function useXYFlowEvents() {
8634
8918
  onPaneClick: (event) => {
8635
8919
  if (dbclickGuarg())
8636
8920
  return;
8637
- diagramApi.getState().setLastClickedNode(null), diagramApi.getState().setLastClickedEdge(null);
8638
- const { focusedNodeId, fitDiagram, onCanvasClick } = diagramApi.getState();
8639
- if (onCanvasClick) {
8921
+ const { focusedNodeId, fitDiagram, onCanvasClick, resetLastClicked } = diagramApi.getState();
8922
+ if (resetLastClicked(), onCanvasClick) {
8640
8923
  onCanvasClick(event);
8641
8924
  return;
8642
8925
  }
@@ -8652,7 +8935,7 @@ function useXYFlowEvents() {
8652
8935
  },
8653
8936
  onPaneContextMenu: (event) => {
8654
8937
  var _a, _b;
8655
- diagramApi.getState().setLastClickedNode(null), diagramApi.getState().setLastClickedEdge(null), (_b = (_a = diagramApi.getState()).onCanvasContextMenu) == null || _b.call(_a, event);
8938
+ diagramApi.getState().resetLastClicked(), (_b = (_a = diagramApi.getState()).onCanvasContextMenu) == null || _b.call(_a, event);
8656
8939
  },
8657
8940
  onEdgeContextMenu: (event, xyedge) => {
8658
8941
  var _a, _b;
@@ -8676,16 +8959,25 @@ function useXYFlowEvents() {
8676
8959
  focusedNodeId && focusedNodeId !== xynode.id && (fitDiagram(xynode), event.stopPropagation());
8677
8960
  },
8678
8961
  onNodeDoubleClick: (event, xynode) => {
8679
- const { zoomable, fitDiagram, onNodeClick } = diagramApi.getState();
8680
- onNodeClick || zoomable && (fitDiagram(xynode), event.stopPropagation());
8962
+ diagramApi.getState().setLastClickedNode(xynode.id);
8963
+ const { zoomable, fitViewEnabled, fitDiagram, onNodeClick } = diagramApi.getState();
8964
+ onNodeClick || zoomable && fitViewEnabled && (fitDiagram(xynode), event.stopPropagation());
8681
8965
  },
8682
8966
  onEdgeClick: (event, xyedge) => {
8683
- var _a, _b;
8684
- diagramApi.getState().setLastClickedEdge(xyedge.id), (_b = (_a = diagramApi.getState()).onEdgeClick) == null || _b.call(_a, {
8685
- relation: xyedge.data.edge,
8686
- xyedge,
8687
- event
8688
- });
8967
+ diagramApi.getState().setLastClickedEdge(xyedge.id);
8968
+ const { focusedNodeId, fitDiagram, onEdgeClick, xyflow } = diagramApi.getState();
8969
+ if (onEdgeClick) {
8970
+ onEdgeClick({
8971
+ relation: xyedge.data.edge,
8972
+ xyedge,
8973
+ event
8974
+ });
8975
+ return;
8976
+ }
8977
+ if (focusedNodeId && (focusedNodeId === xyedge.source || focusedNodeId === xyedge.target)) {
8978
+ const nextFocus = focusedNodeId === xyedge.source ? xyedge.target : xyedge.source;
8979
+ fitDiagram(xyflow.getNode(nextFocus)), event.stopPropagation();
8980
+ }
8689
8981
  },
8690
8982
  onMoveEnd: (event, _viewport) => {
8691
8983
  const viewportChanged = !!event;
@@ -8702,6 +8994,19 @@ function useXYFlowEvents() {
8702
8994
  },
8703
8995
  onEdgeMouseLeave: (event, xyedge) => {
8704
8996
  diagramApi.getState().setHoveredEdge(null);
8997
+ },
8998
+ /**
8999
+ * WOKAROUND:
9000
+ * Viewport transform is not rounded to integers which results in blurry nodes on some resolution
9001
+ * https://github.com/xyflow/xyflow/issues/3282
9002
+ */
9003
+ onViewportChange: ({ x, y, zoom: zoom2 }) => {
9004
+ const scale = zoom2 != 1 ? zoom2.toFixed(6) : "1";
9005
+ transformRef.current = `translate(${Math.round(x)}px, ${Math.round(y)}px) scale(${scale})`, cbRef.current ?? (cbRef.current = requestIdleCallback(() => {
9006
+ cbRef.current = void 0, viewportRef.current && viewportRef.current.style.transform !== transformRef.current && (viewportRef.current.style.transform = transformRef.current);
9007
+ }, {
9008
+ timeout: 1e3
9009
+ }));
8705
9010
  }
8706
9011
  }), [diagramApi, xyflowApi]);
8707
9012
  }
@@ -8711,7 +9016,7 @@ const nodeTypes = {
8711
9016
  }, edgeTypes = {
8712
9017
  relationship: RelationshipEdge
8713
9018
  }, selector = (s) => ({
8714
- nodesSelectable: s.nodesSelectable,
9019
+ nodesSelectable: s.nodesSelectable || s.focusedNodeId !== null,
8715
9020
  nodesDraggable: s.nodesDraggable,
8716
9021
  fitView: s.fitViewEnabled,
8717
9022
  fitViewPadding: s.fitViewPadding,
@@ -8797,6 +9102,7 @@ function XYFlowWrapper({
8797
9102
  onNodeClick: handlers.onNodeClick,
8798
9103
  onNodeDoubleClick: handlers.onNodeDoubleClick,
8799
9104
  onEdgeClick: handlers.onEdgeClick,
9105
+ onViewportChange: handlers.onViewportChange,
8800
9106
  onInit: () => {
8801
9107
  diagramApi.setState({ initialized: !0 }, !1, "initialized");
8802
9108
  },
@@ -8811,7 +9117,8 @@ function XYFlowWrapper({
8811
9117
  },
8812
9118
  children: [
8813
9119
  isBgWithPattern && /* @__PURE__ */ jsx(XYFlowBackground, { background }),
8814
- controls && /* @__PURE__ */ jsx(Controls, {}),
9120
+ controls && /* @__PURE__ */ jsx(Controls, { position: "bottom-right" }),
9121
+ zoomable && !editor.hasOnNodeClick && /* @__PURE__ */ jsx(SelectEdgesOnNodeFocus, {}),
8815
9122
  children2
8816
9123
  ]
8817
9124
  }
@@ -8831,6 +9138,7 @@ function LikeC4Diagram({
8831
9138
  background = "dots",
8832
9139
  controls = !1,
8833
9140
  showElementLinks = !0,
9141
+ showDiagramTitle = !0,
8834
9142
  initialWidth,
8835
9143
  initialHeight,
8836
9144
  keepAspectRatio = !1,
@@ -8891,7 +9199,7 @@ function LikeC4Diagram({
8891
9199
  enabled: keepAspectRatio,
8892
9200
  width: view.width,
8893
9201
  height: view.height,
8894
- children: /* @__PURE__ */ jsx(
9202
+ children: /* @__PURE__ */ jsxs(
8895
9203
  XYFlow,
8896
9204
  {
8897
9205
  className: clsx(
@@ -8905,7 +9213,10 @@ function LikeC4Diagram({
8905
9213
  controls,
8906
9214
  defaultNodes: initialRef.current.defaultNodes,
8907
9215
  defaultEdges: initialRef.current.defaultEdges,
8908
- children: readonly !== !0 && /* @__PURE__ */ jsx(OptionsPanel2, {})
9216
+ children: [
9217
+ readonly !== !0 && /* @__PURE__ */ jsx(OptionsPanel, {}),
9218
+ showDiagramTitle === !0 && /* @__PURE__ */ jsx(DiagramTitlePanel, {})
9219
+ ]
8909
9220
  }
8910
9221
  )
8911
9222
  }
@@ -8939,6 +9250,7 @@ function StaticLikeC4Diagram({
8939
9250
  controls: !1,
8940
9251
  background,
8941
9252
  showElementLinks: !0,
9253
+ showDiagramTitle: !1,
8942
9254
  nodesSelectable: !1,
8943
9255
  nodesDraggable: !1,
8944
9256
  ...rest