@glyphjs/components 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -75,7 +75,7 @@ function Callout({ data }) {
75
75
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { role: "note", "aria-label": CALLOUT_LABELS[type], style: containerStyle11, children: [
76
76
  /* @__PURE__ */ jsxRuntime.jsx("span", { style: iconStyle, "aria-hidden": "true", children: CALLOUT_ICONS[type] }),
77
77
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: bodyStyle3, children: [
78
- title && /* @__PURE__ */ jsxRuntime.jsx("div", { style: titleStyle2, children: title }),
78
+ title && /* @__PURE__ */ jsxRuntime.jsx("div", { style: titleStyle2, children: /* @__PURE__ */ jsxRuntime.jsx(runtime.RichText, { content: title }) }),
79
79
  /* @__PURE__ */ jsxRuntime.jsx("div", { children: /* @__PURE__ */ jsxRuntime.jsx(runtime.RichText, { content }) })
80
80
  ] })
81
81
  ] });
@@ -1455,6 +1455,326 @@ function computeForceLayout(nodes, edges) {
1455
1455
  height: maxY + LAYOUT_PADDING
1456
1456
  };
1457
1457
  }
1458
+ function useZoomInteraction({
1459
+ svgRef,
1460
+ rootRef,
1461
+ interactionMode
1462
+ }) {
1463
+ const [isActive, setIsActive] = react.useState(interactionMode === "always");
1464
+ const [hasAttemptedScroll, setHasAttemptedScroll] = react.useState(false);
1465
+ const containerRef = react.useRef(null);
1466
+ react.useEffect(() => {
1467
+ setIsActive(interactionMode === "always");
1468
+ setHasAttemptedScroll(false);
1469
+ }, [interactionMode]);
1470
+ const filterFunction = react.useCallback(
1471
+ (event) => {
1472
+ if (interactionMode === "always") {
1473
+ return true;
1474
+ }
1475
+ if (interactionMode === "modifier-key") {
1476
+ if (event.type === "mousedown") return true;
1477
+ if (event.type === "wheel") {
1478
+ const wheelEvent = event;
1479
+ const hasModifier = wheelEvent.altKey;
1480
+ if (!hasModifier && !hasAttemptedScroll) {
1481
+ setHasAttemptedScroll(true);
1482
+ setTimeout(() => setHasAttemptedScroll(false), 3e3);
1483
+ }
1484
+ return hasModifier;
1485
+ }
1486
+ return true;
1487
+ }
1488
+ if (interactionMode === "click-to-activate") {
1489
+ return isActive;
1490
+ }
1491
+ return true;
1492
+ },
1493
+ [interactionMode, isActive, hasAttemptedScroll]
1494
+ );
1495
+ react.useEffect(() => {
1496
+ if (interactionMode !== "modifier-key" || !svgRef.current) return;
1497
+ const svg = svgRef.current;
1498
+ const container = svg.parentElement;
1499
+ if (!container) return;
1500
+ const handleWheel = (event) => {
1501
+ const target = event.target;
1502
+ if (event.altKey && svg.contains(target)) {
1503
+ event.preventDefault();
1504
+ event.stopPropagation();
1505
+ }
1506
+ };
1507
+ container.addEventListener("wheel", handleWheel, { passive: false, capture: true });
1508
+ return () => {
1509
+ container.removeEventListener("wheel", handleWheel, { capture: true });
1510
+ };
1511
+ }, [interactionMode, svgRef]);
1512
+ const zoomBehavior = react.useMemo(() => {
1513
+ const zoom3 = d32__namespace.zoom().scaleExtent([0.1, 4]);
1514
+ if (typeof zoom3.filter === "function") {
1515
+ zoom3.filter(filterFunction);
1516
+ }
1517
+ zoom3.on("zoom", (event) => {
1518
+ if (rootRef.current) {
1519
+ d32__namespace.select(rootRef.current).attr("transform", event.transform.toString());
1520
+ }
1521
+ });
1522
+ return zoom3;
1523
+ }, [filterFunction, rootRef]);
1524
+ const handleActivate = react.useCallback(() => {
1525
+ if (interactionMode === "click-to-activate") {
1526
+ setIsActive(true);
1527
+ }
1528
+ }, [interactionMode]);
1529
+ react.useEffect(() => {
1530
+ if (interactionMode !== "click-to-activate" || !isActive) return;
1531
+ const handleKeyDown = (e) => {
1532
+ if (e.key === "Escape") {
1533
+ setIsActive(false);
1534
+ }
1535
+ };
1536
+ document.addEventListener("keydown", handleKeyDown);
1537
+ return () => document.removeEventListener("keydown", handleKeyDown);
1538
+ }, [interactionMode, isActive]);
1539
+ react.useEffect(() => {
1540
+ if (interactionMode !== "click-to-activate" || !isActive) return;
1541
+ const handleClickOutside = (e) => {
1542
+ const container = svgRef.current?.parentElement;
1543
+ if (container && !container.contains(e.target)) {
1544
+ setIsActive(false);
1545
+ }
1546
+ };
1547
+ document.addEventListener("click", handleClickOutside, true);
1548
+ return () => document.removeEventListener("click", handleClickOutside, true);
1549
+ }, [interactionMode, isActive, svgRef]);
1550
+ react.useEffect(() => {
1551
+ if (svgRef.current) {
1552
+ containerRef.current = svgRef.current.parentElement;
1553
+ }
1554
+ }, [svgRef]);
1555
+ const overlayProps = react.useMemo(() => {
1556
+ if (interactionMode === "always") return null;
1557
+ if (interactionMode === "modifier-key" && !hasAttemptedScroll) return null;
1558
+ if (interactionMode === "click-to-activate" && isActive) return null;
1559
+ return {
1560
+ mode: interactionMode,
1561
+ isActive,
1562
+ onActivate: handleActivate,
1563
+ width: "100%",
1564
+ height: "100%"
1565
+ };
1566
+ }, [interactionMode, isActive, hasAttemptedScroll, handleActivate]);
1567
+ const zoomIn = react.useCallback(() => {
1568
+ if (!svgRef.current) return;
1569
+ d32__namespace.select(svgRef.current).transition().duration(300).call(zoomBehavior.scaleBy, 1.3);
1570
+ }, [svgRef, zoomBehavior]);
1571
+ const zoomOut = react.useCallback(() => {
1572
+ if (!svgRef.current) return;
1573
+ d32__namespace.select(svgRef.current).transition().duration(300).call(zoomBehavior.scaleBy, 1 / 1.3);
1574
+ }, [svgRef, zoomBehavior]);
1575
+ const resetZoom = react.useCallback(() => {
1576
+ if (!svgRef.current) return;
1577
+ d32__namespace.select(svgRef.current).transition().duration(300).call(zoomBehavior.transform, d32__namespace.zoomIdentity);
1578
+ }, [svgRef, zoomBehavior]);
1579
+ return {
1580
+ isActive,
1581
+ overlayProps,
1582
+ zoomBehavior,
1583
+ zoomIn,
1584
+ zoomOut,
1585
+ resetZoom
1586
+ };
1587
+ }
1588
+ function InteractionOverlay({
1589
+ mode,
1590
+ isActive,
1591
+ onActivate,
1592
+ width,
1593
+ height
1594
+ }) {
1595
+ if (mode === "modifier-key") {
1596
+ return /* @__PURE__ */ jsxRuntime.jsx(
1597
+ "div",
1598
+ {
1599
+ className: "glyph-interaction-overlay",
1600
+ style: {
1601
+ ...OVERLAY_BASE_STYLE,
1602
+ width,
1603
+ height,
1604
+ pointerEvents: "none"
1605
+ },
1606
+ "aria-hidden": "true",
1607
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: TOOLTIP_STYLE, children: /* @__PURE__ */ jsxRuntime.jsx("span", { style: TOOLTIP_TEXT_STYLE, children: "Alt + scroll to zoom" }) })
1608
+ }
1609
+ );
1610
+ }
1611
+ if (mode === "click-to-activate" && !isActive) {
1612
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1613
+ /* @__PURE__ */ jsxRuntime.jsx(
1614
+ "div",
1615
+ {
1616
+ className: "glyph-interaction-overlay",
1617
+ style: {
1618
+ ...OVERLAY_BASE_STYLE,
1619
+ ...ACTIVATION_OVERLAY_STYLE,
1620
+ width,
1621
+ height
1622
+ },
1623
+ onClick: onActivate,
1624
+ role: "button",
1625
+ tabIndex: 0,
1626
+ "aria-label": "Click to activate graph interaction",
1627
+ onKeyDown: (e) => {
1628
+ if (e.key === "Enter" || e.key === " ") {
1629
+ e.preventDefault();
1630
+ onActivate();
1631
+ }
1632
+ },
1633
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: ACTIVATION_TEXT_STYLE, children: "Click to interact" })
1634
+ }
1635
+ ),
1636
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: SR_ONLY_STYLE, role: "status", "aria-live": "polite", "aria-atomic": "true", children: "Graph interaction inactive. Click to activate." })
1637
+ ] });
1638
+ }
1639
+ if (mode === "click-to-activate" && isActive) {
1640
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1641
+ /* @__PURE__ */ jsxRuntime.jsx(
1642
+ "div",
1643
+ {
1644
+ style: {
1645
+ ...OVERLAY_BASE_STYLE,
1646
+ width,
1647
+ height,
1648
+ pointerEvents: "none",
1649
+ border: "2px solid var(--glyph-interaction-active-border, #0a9d7c)",
1650
+ borderRadius: "4px"
1651
+ },
1652
+ "aria-hidden": "true"
1653
+ }
1654
+ ),
1655
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: SR_ONLY_STYLE, role: "status", "aria-live": "polite", "aria-atomic": "true", children: "Graph interaction active. Press Escape to deactivate." })
1656
+ ] });
1657
+ }
1658
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, {});
1659
+ }
1660
+ var OVERLAY_BASE_STYLE = {
1661
+ position: "absolute",
1662
+ top: 0,
1663
+ left: 0,
1664
+ display: "flex",
1665
+ alignItems: "center",
1666
+ justifyContent: "center",
1667
+ zIndex: 10
1668
+ };
1669
+ var TOOLTIP_STYLE = {
1670
+ position: "absolute",
1671
+ bottom: "12px",
1672
+ right: "12px",
1673
+ padding: "6px 10px",
1674
+ backgroundColor: "var(--glyph-interaction-tooltip-bg, rgba(26, 32, 53, 0.9))",
1675
+ color: "var(--glyph-interaction-tooltip-text, #f4f6fa)",
1676
+ borderRadius: "4px",
1677
+ fontSize: "12px",
1678
+ fontFamily: "Inter, system-ui, sans-serif",
1679
+ fontWeight: 500,
1680
+ boxShadow: "0 2px 8px rgba(0, 0, 0, 0.15)",
1681
+ pointerEvents: "none"
1682
+ };
1683
+ var TOOLTIP_TEXT_STYLE = {
1684
+ display: "flex",
1685
+ alignItems: "center",
1686
+ gap: "4px"
1687
+ };
1688
+ var ACTIVATION_OVERLAY_STYLE = {
1689
+ backgroundColor: "var(--glyph-interaction-overlay-bg, rgba(244, 246, 250, 0.8))",
1690
+ cursor: "pointer",
1691
+ transition: "background-color 0.2s ease"
1692
+ };
1693
+ var ACTIVATION_TEXT_STYLE = {
1694
+ padding: "12px 20px",
1695
+ backgroundColor: "var(--glyph-interaction-tooltip-bg, rgba(26, 32, 53, 0.9))",
1696
+ color: "var(--glyph-interaction-tooltip-text, #f4f6fa)",
1697
+ borderRadius: "6px",
1698
+ fontSize: "14px",
1699
+ fontFamily: "Inter, system-ui, sans-serif",
1700
+ fontWeight: 500,
1701
+ boxShadow: "0 4px 12px rgba(0, 0, 0, 0.2)"
1702
+ };
1703
+ var SR_ONLY_STYLE = {
1704
+ position: "absolute",
1705
+ width: "1px",
1706
+ height: "1px",
1707
+ padding: 0,
1708
+ margin: "-1px",
1709
+ overflow: "hidden",
1710
+ clip: "rect(0, 0, 0, 0)",
1711
+ whiteSpace: "nowrap",
1712
+ border: 0
1713
+ };
1714
+ function ZoomControls({ onZoomIn, onZoomOut, onReset }) {
1715
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: CONTROLS_CONTAINER_STYLE, children: [
1716
+ /* @__PURE__ */ jsxRuntime.jsx(
1717
+ "button",
1718
+ {
1719
+ onClick: onZoomIn,
1720
+ style: BUTTON_STYLE,
1721
+ "aria-label": "Zoom in",
1722
+ title: "Zoom in",
1723
+ type: "button",
1724
+ children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", children: [
1725
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "8", y1: "4", x2: "8", y2: "12", strokeWidth: "2" }),
1726
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "8", x2: "12", y2: "8", strokeWidth: "2" })
1727
+ ] })
1728
+ }
1729
+ ),
1730
+ /* @__PURE__ */ jsxRuntime.jsx(
1731
+ "button",
1732
+ {
1733
+ onClick: onZoomOut,
1734
+ style: BUTTON_STYLE,
1735
+ "aria-label": "Zoom out",
1736
+ title: "Zoom out",
1737
+ type: "button",
1738
+ children: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", children: /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "8", x2: "12", y2: "8", strokeWidth: "2" }) })
1739
+ }
1740
+ ),
1741
+ /* @__PURE__ */ jsxRuntime.jsx(
1742
+ "button",
1743
+ {
1744
+ onClick: onReset,
1745
+ style: BUTTON_STYLE,
1746
+ "aria-label": "Reset zoom",
1747
+ title: "Reset zoom",
1748
+ type: "button",
1749
+ children: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", children: /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "3", y: "3", width: "10", height: "10", strokeWidth: "2", rx: "1" }) })
1750
+ }
1751
+ )
1752
+ ] });
1753
+ }
1754
+ var CONTROLS_CONTAINER_STYLE = {
1755
+ position: "absolute",
1756
+ top: "12px",
1757
+ right: "12px",
1758
+ display: "flex",
1759
+ flexDirection: "column",
1760
+ gap: "4px",
1761
+ zIndex: 10
1762
+ };
1763
+ var BUTTON_STYLE = {
1764
+ width: "32px",
1765
+ height: "32px",
1766
+ padding: "0",
1767
+ display: "flex",
1768
+ alignItems: "center",
1769
+ justifyContent: "center",
1770
+ backgroundColor: "var(--glyph-surface-raised, #f4f6fa)",
1771
+ border: "1px solid var(--glyph-border, #d0d8e4)",
1772
+ borderRadius: "4px",
1773
+ color: "var(--glyph-text, #1a2035)",
1774
+ cursor: "pointer",
1775
+ transition: "all 0.2s ease",
1776
+ boxShadow: "0 2px 4px rgba(0, 0, 0, 0.1)"
1777
+ };
1458
1778
  var GROUP_PALETTE = [
1459
1779
  "#00d4aa",
1460
1780
  // cyan-green
@@ -1501,7 +1821,7 @@ function getThemeVar(container, varName, fallback) {
1501
1821
  return getComputedStyle(container).getPropertyValue(varName).trim() || fallback;
1502
1822
  }
1503
1823
  var ARROW_MARKER_ID = "glyph-graph-arrowhead";
1504
- function renderGraph(svgElement, layout, groupIndex, outgoingRefs, onNavigate, onNodeClick) {
1824
+ function renderGraph(svgElement, layout, groupIndex, outgoingRefs, onNavigate, zoomBehavior, onNodeClick) {
1505
1825
  const svg = d32__namespace.select(svgElement);
1506
1826
  svg.selectAll("*").remove();
1507
1827
  const width = Math.max(layout.width, 200);
@@ -1514,9 +1834,6 @@ function renderGraph(svgElement, layout, groupIndex, outgoingRefs, onNavigate, o
1514
1834
  const nodeStrokeWidth = getThemeVar(container, "--glyph-node-stroke-width", "1.5");
1515
1835
  const nodeFillOpacity = getThemeVar(container, "--glyph-node-fill-opacity", "0.85");
1516
1836
  const root = svg.append("g").attr("class", "glyph-graph-root");
1517
- const zoomBehavior = d32__namespace.zoom().scaleExtent([0.1, 4]).on("zoom", (event) => {
1518
- root.attr("transform", event.transform.toString());
1519
- });
1520
1837
  svg.call(zoomBehavior);
1521
1838
  const navigableNodes = /* @__PURE__ */ new Set();
1522
1839
  const refByAnchor = /* @__PURE__ */ new Map();
@@ -1573,6 +1890,7 @@ function Graph({
1573
1890
  container
1574
1891
  }) {
1575
1892
  const svgRef = react.useRef(null);
1893
+ const rootRef = react.useRef(null);
1576
1894
  const groupIndex = react.useRef(/* @__PURE__ */ new Map());
1577
1895
  const layoutResult = react.useMemo(() => {
1578
1896
  const direction = resolveLayout(data);
@@ -1581,6 +1899,12 @@ function Graph({
1581
1899
  }
1582
1900
  return computeDagreLayout(data.nodes, data.edges, direction);
1583
1901
  }, [data]);
1902
+ const { overlayProps, zoomBehavior, zoomIn, zoomOut, resetZoom } = useZoomInteraction({
1903
+ svgRef,
1904
+ rootRef,
1905
+ interactionMode: data.interactionMode ?? "modifier-key",
1906
+ blockId: block.id
1907
+ });
1584
1908
  const handleNodeClick = react.useMemo(() => {
1585
1909
  if (!onInteraction) return void 0;
1586
1910
  return (nodeId, nodeLabel) => {
@@ -1601,27 +1925,36 @@ function Graph({
1601
1925
  groupIndex.current,
1602
1926
  outgoingRefs,
1603
1927
  onNavigate,
1928
+ zoomBehavior,
1604
1929
  handleNodeClick
1605
1930
  );
1606
- }, [layoutResult, outgoingRefs, onNavigate, handleNodeClick]);
1931
+ const rootElement = svgRef.current.querySelector(".glyph-graph-root");
1932
+ if (rootElement) {
1933
+ rootRef.current = rootElement;
1934
+ }
1935
+ }, [layoutResult, outgoingRefs, onNavigate, zoomBehavior, handleNodeClick]);
1607
1936
  const ariaLabel = `${data.type} graph with ${data.nodes.length} nodes and ${data.edges.length} edges`;
1608
1937
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "glyph-graph-container", children: [
1609
- /* @__PURE__ */ jsxRuntime.jsx(
1610
- "svg",
1611
- {
1612
- ref: svgRef,
1613
- role: "img",
1614
- "aria-label": ariaLabel,
1615
- width: "100%",
1616
- height: "100%",
1617
- style: {
1618
- minHeight: container.tier === "compact" ? 200 : 300,
1619
- maxHeight: container.tier === "compact" ? 500 : 700,
1620
- display: "block"
1938
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { position: "relative" }, children: [
1939
+ /* @__PURE__ */ jsxRuntime.jsx(
1940
+ "svg",
1941
+ {
1942
+ ref: svgRef,
1943
+ role: "img",
1944
+ "aria-label": ariaLabel,
1945
+ width: "100%",
1946
+ height: "100%",
1947
+ style: {
1948
+ minHeight: container.tier === "compact" ? 200 : 300,
1949
+ maxHeight: container.tier === "compact" ? 500 : 700,
1950
+ display: "block"
1951
+ }
1621
1952
  }
1622
- }
1623
- ),
1624
- /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "sr-only", "aria-label": "Graph data", style: SR_ONLY_STYLE, children: [
1953
+ ),
1954
+ overlayProps && /* @__PURE__ */ jsxRuntime.jsx(InteractionOverlay, { ...overlayProps }),
1955
+ /* @__PURE__ */ jsxRuntime.jsx(ZoomControls, { onZoomIn: zoomIn, onZoomOut: zoomOut, onReset: resetZoom })
1956
+ ] }),
1957
+ /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "sr-only", "aria-label": "Graph data", style: SR_ONLY_STYLE2, children: [
1625
1958
  /* @__PURE__ */ jsxRuntime.jsx("caption", { children: "Graph nodes and connections" }),
1626
1959
  /* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
1627
1960
  /* @__PURE__ */ jsxRuntime.jsx("th", { scope: "col", children: "Node" }),
@@ -1643,7 +1976,7 @@ function Graph({
1643
1976
  ] })
1644
1977
  ] });
1645
1978
  }
1646
- var SR_ONLY_STYLE = {
1979
+ var SR_ONLY_STYLE2 = {
1647
1980
  position: "absolute",
1648
1981
  width: "1px",
1649
1982
  height: "1px",
@@ -1767,16 +2100,13 @@ function drawCrowsFoot(g, x, y, angle, symbol) {
1767
2100
  g.append("line").attr("x1", tx - Math.cos(perpAngle) * halfLen).attr("y1", ty - Math.sin(perpAngle) * halfLen).attr("x2", tx + Math.cos(perpAngle) * halfLen).attr("y2", ty + Math.sin(perpAngle) * halfLen).attr("stroke", "var(--glyph-relation-line, #6b7a94)").attr("stroke-width", "var(--glyph-node-stroke-width, 1.5)");
1768
2101
  }
1769
2102
  }
1770
- function renderRelation(svgElement, layout) {
2103
+ function renderRelation(svgElement, layout, zoomBehavior) {
1771
2104
  const svg = d32__namespace.select(svgElement);
1772
2105
  svg.selectAll("*").remove();
1773
2106
  const width = Math.max(layout.width, 200);
1774
2107
  const height = Math.max(layout.height, 200);
1775
2108
  svg.attr("viewBox", `0 0 ${width} ${height}`);
1776
2109
  const root = svg.append("g").attr("class", "glyph-relation-root");
1777
- const zoomBehavior = d32__namespace.zoom().scaleExtent([0.1, 4]).on("zoom", (event) => {
1778
- root.attr("transform", event.transform.toString());
1779
- });
1780
2110
  svg.call(zoomBehavior);
1781
2111
  const entityMap = /* @__PURE__ */ new Map();
1782
2112
  for (const entity of layout.entities) {
@@ -1848,29 +2178,44 @@ function renderRelation(svgElement, layout) {
1848
2178
  }
1849
2179
  }
1850
2180
  }
1851
- function Relation({ data }) {
2181
+ function Relation({ data, block }) {
1852
2182
  const svgRef = react.useRef(null);
2183
+ const rootRef = react.useRef(null);
1853
2184
  const layoutResult = react.useMemo(() => {
1854
2185
  return computeRelationLayout(data);
1855
2186
  }, [data]);
2187
+ const { overlayProps, zoomBehavior, zoomIn, zoomOut, resetZoom } = useZoomInteraction({
2188
+ svgRef,
2189
+ rootRef,
2190
+ interactionMode: data.interactionMode ?? "modifier-key",
2191
+ blockId: block.id
2192
+ });
1856
2193
  react.useEffect(() => {
1857
2194
  if (!svgRef.current) return;
1858
- renderRelation(svgRef.current, layoutResult);
1859
- }, [layoutResult]);
2195
+ renderRelation(svgRef.current, layoutResult, zoomBehavior);
2196
+ const rootElement = svgRef.current.querySelector(".glyph-relation-root");
2197
+ if (rootElement) {
2198
+ rootRef.current = rootElement;
2199
+ }
2200
+ }, [layoutResult, zoomBehavior]);
1860
2201
  const ariaLabel = `Entity-relationship diagram with ${data.entities.length} entities and ${data.relationships.length} relationships`;
1861
2202
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "glyph-relation-container", children: [
1862
- /* @__PURE__ */ jsxRuntime.jsx(
1863
- "svg",
1864
- {
1865
- ref: svgRef,
1866
- role: "img",
1867
- "aria-label": ariaLabel,
1868
- width: "100%",
1869
- height: "100%",
1870
- style: { minHeight: 300, maxHeight: 700, display: "block" }
1871
- }
1872
- ),
1873
- /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "sr-only", "aria-label": "Entity-relationship data", style: SR_ONLY_STYLE2, children: [
2203
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { position: "relative" }, children: [
2204
+ /* @__PURE__ */ jsxRuntime.jsx(
2205
+ "svg",
2206
+ {
2207
+ ref: svgRef,
2208
+ role: "img",
2209
+ "aria-label": ariaLabel,
2210
+ width: "100%",
2211
+ height: "100%",
2212
+ style: { minHeight: 300, maxHeight: 700, display: "block" }
2213
+ }
2214
+ ),
2215
+ overlayProps && /* @__PURE__ */ jsxRuntime.jsx(InteractionOverlay, { ...overlayProps }),
2216
+ /* @__PURE__ */ jsxRuntime.jsx(ZoomControls, { onZoomIn: zoomIn, onZoomOut: zoomOut, onReset: resetZoom })
2217
+ ] }),
2218
+ /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "sr-only", "aria-label": "Entity-relationship data", style: SR_ONLY_STYLE3, children: [
1874
2219
  /* @__PURE__ */ jsxRuntime.jsx("caption", { children: "Entities and relationships" }),
1875
2220
  /* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
1876
2221
  /* @__PURE__ */ jsxRuntime.jsx("th", { scope: "col", children: "Entity" }),
@@ -1893,7 +2238,7 @@ function Relation({ data }) {
1893
2238
  ] })
1894
2239
  ] });
1895
2240
  }
1896
- var SR_ONLY_STYLE2 = {
2241
+ var SR_ONLY_STYLE3 = {
1897
2242
  position: "absolute",
1898
2243
  width: "1px",
1899
2244
  height: "1px",
@@ -2529,7 +2874,7 @@ function renderNodeShape(nodeG, node, fillOpacity, strokeWidth) {
2529
2874
  }
2530
2875
  }
2531
2876
  }
2532
- function renderFlowchart(svgElement, layout) {
2877
+ function renderFlowchart(svgElement, layout, zoomBehavior) {
2533
2878
  const svg = d32__namespace.select(svgElement);
2534
2879
  svg.selectAll("*").remove();
2535
2880
  const width = Math.max(layout.width, 200);
@@ -2541,9 +2886,6 @@ function renderFlowchart(svgElement, layout) {
2541
2886
  const nodeStrokeWidth = getThemeVar2(container, "--glyph-node-stroke-width", "1.5");
2542
2887
  const nodeFillOpacity = getThemeVar2(container, "--glyph-node-fill-opacity", "0.85");
2543
2888
  const root = svg.append("g").attr("class", "glyph-flowchart-root");
2544
- const zoomBehavior = d32__namespace.zoom().scaleExtent([0.1, 4]).on("zoom", (event) => {
2545
- root.attr("transform", event.transform.toString());
2546
- });
2547
2889
  svg.call(zoomBehavior);
2548
2890
  const lineGen = d32__namespace.line().x((d) => d.x).y((d) => d.y).curve(d32__namespace.curveBasis);
2549
2891
  const edgeGroup = root.append("g").attr("class", "glyph-flowchart-edges");
@@ -2564,13 +2906,28 @@ function renderFlowchart(svgElement, layout) {
2564
2906
  nodeG.append("text").attr("x", node.x).attr("y", node.y).attr("dy", "0.35em").attr("text-anchor", "middle").attr("font-size", "13px").attr("font-family", "Inter, system-ui, sans-serif").attr("fill", "var(--glyph-node-label-color, #fff)").attr("pointer-events", "none").text(node.label);
2565
2907
  }
2566
2908
  }
2567
- function Flowchart({ data, container }) {
2909
+ function Flowchart({
2910
+ data,
2911
+ block,
2912
+ container
2913
+ }) {
2568
2914
  const svgRef = react.useRef(null);
2915
+ const rootRef = react.useRef(null);
2569
2916
  const layoutResult = react.useMemo(() => computeLayout(data.nodes, data.edges, data.direction), [data]);
2917
+ const { overlayProps, zoomBehavior, zoomIn, zoomOut, resetZoom } = useZoomInteraction({
2918
+ svgRef,
2919
+ rootRef,
2920
+ interactionMode: data.interactionMode ?? "modifier-key",
2921
+ blockId: block.id
2922
+ });
2570
2923
  react.useEffect(() => {
2571
2924
  if (!svgRef.current) return;
2572
- renderFlowchart(svgRef.current, layoutResult);
2573
- }, [layoutResult]);
2925
+ renderFlowchart(svgRef.current, layoutResult, zoomBehavior);
2926
+ const rootElement = svgRef.current.querySelector(".glyph-flowchart-root");
2927
+ if (rootElement) {
2928
+ rootRef.current = rootElement;
2929
+ }
2930
+ }, [layoutResult, zoomBehavior]);
2574
2931
  const nodeCount = data.nodes.length;
2575
2932
  const edgeCount = data.edges.length;
2576
2933
  const ariaLabel = data.title ? `${data.title}: flowchart with ${nodeCount} nodes and ${edgeCount} edges` : `Flowchart with ${nodeCount} nodes and ${edgeCount} edges`;
@@ -2588,22 +2945,26 @@ function Flowchart({ data, container }) {
2588
2945
  children: data.title
2589
2946
  }
2590
2947
  ),
2591
- /* @__PURE__ */ jsxRuntime.jsx(
2592
- "svg",
2593
- {
2594
- ref: svgRef,
2595
- role: "img",
2596
- "aria-label": ariaLabel,
2597
- width: "100%",
2598
- height: "100%",
2599
- style: {
2600
- minHeight: container.tier === "compact" ? 200 : 300,
2601
- maxHeight: container.tier === "compact" ? 500 : 700,
2602
- display: "block"
2948
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { position: "relative" }, children: [
2949
+ /* @__PURE__ */ jsxRuntime.jsx(
2950
+ "svg",
2951
+ {
2952
+ ref: svgRef,
2953
+ role: "img",
2954
+ "aria-label": ariaLabel,
2955
+ width: "100%",
2956
+ height: "100%",
2957
+ style: {
2958
+ minHeight: container.tier === "compact" ? 200 : 300,
2959
+ maxHeight: container.tier === "compact" ? 500 : 700,
2960
+ display: "block"
2961
+ }
2603
2962
  }
2604
- }
2605
- ),
2606
- /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "sr-only", "aria-label": "Flowchart data", style: SR_ONLY_STYLE3, children: [
2963
+ ),
2964
+ overlayProps && /* @__PURE__ */ jsxRuntime.jsx(InteractionOverlay, { ...overlayProps }),
2965
+ /* @__PURE__ */ jsxRuntime.jsx(ZoomControls, { onZoomIn: zoomIn, onZoomOut: zoomOut, onReset: resetZoom })
2966
+ ] }),
2967
+ /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "sr-only", "aria-label": "Flowchart data", style: SR_ONLY_STYLE4, children: [
2607
2968
  /* @__PURE__ */ jsxRuntime.jsx("caption", { children: "Flowchart nodes and connections" }),
2608
2969
  /* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
2609
2970
  /* @__PURE__ */ jsxRuntime.jsx("th", { scope: "col", children: "Node" }),
@@ -2625,7 +2986,7 @@ function Flowchart({ data, container }) {
2625
2986
  ] })
2626
2987
  ] });
2627
2988
  }
2628
- var SR_ONLY_STYLE3 = {
2989
+ var SR_ONLY_STYLE4 = {
2629
2990
  position: "absolute",
2630
2991
  width: "1px",
2631
2992
  height: "1px",
@@ -3247,7 +3608,7 @@ function Sequence({ data, container }) {
3247
3608
  ]
3248
3609
  }
3249
3610
  ),
3250
- /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "sr-only", "aria-label": "Sequence data", style: SR_ONLY_STYLE4, children: [
3611
+ /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "sr-only", "aria-label": "Sequence data", style: SR_ONLY_STYLE5, children: [
3251
3612
  /* @__PURE__ */ jsxRuntime.jsx("caption", { children: "Sequence messages in order" }),
3252
3613
  /* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
3253
3614
  /* @__PURE__ */ jsxRuntime.jsx("th", { scope: "col", children: "#" }),
@@ -3270,7 +3631,7 @@ function Sequence({ data, container }) {
3270
3631
  ] })
3271
3632
  ] });
3272
3633
  }
3273
- var SR_ONLY_STYLE4 = {
3634
+ var SR_ONLY_STYLE5 = {
3274
3635
  position: "absolute",
3275
3636
  width: "1px",
3276
3637
  height: "1px",
@@ -3676,7 +4037,7 @@ function Architecture({
3676
4037
  }
3677
4038
  }
3678
4039
  ),
3679
- /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "sr-only", "aria-label": "Architecture data", style: SR_ONLY_STYLE5, children: [
4040
+ /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "sr-only", "aria-label": "Architecture data", style: SR_ONLY_STYLE6, children: [
3680
4041
  /* @__PURE__ */ jsxRuntime.jsx("caption", { children: "Architecture nodes and connections" }),
3681
4042
  /* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
3682
4043
  /* @__PURE__ */ jsxRuntime.jsx("th", { scope: "col", children: "Node" }),
@@ -3720,7 +4081,7 @@ function countLeafNodes(children) {
3720
4081
  }
3721
4082
  return count;
3722
4083
  }
3723
- var SR_ONLY_STYLE5 = {
4084
+ var SR_ONLY_STYLE6 = {
3724
4085
  position: "absolute",
3725
4086
  width: "1px",
3726
4087
  height: "1px",
@@ -3908,7 +4269,7 @@ function layoutTree(data) {
3908
4269
  };
3909
4270
  }
3910
4271
  function renderAccessibleList(root, children) {
3911
- return /* @__PURE__ */ jsxRuntime.jsx("ul", { style: SR_ONLY_STYLE6, role: "list", "aria-label": "Mind map structure", children: /* @__PURE__ */ jsxRuntime.jsxs("li", { children: [
4272
+ return /* @__PURE__ */ jsxRuntime.jsx("ul", { style: SR_ONLY_STYLE7, role: "list", "aria-label": "Mind map structure", children: /* @__PURE__ */ jsxRuntime.jsxs("li", { children: [
3912
4273
  root,
3913
4274
  children.length > 0 && renderAccessibleChildren(children)
3914
4275
  ] }) });
@@ -4035,7 +4396,7 @@ function MindMap({ data, container }) {
4035
4396
  renderAccessibleList(data.root, data.children)
4036
4397
  ] });
4037
4398
  }
4038
- var SR_ONLY_STYLE6 = {
4399
+ var SR_ONLY_STYLE7 = {
4039
4400
  position: "absolute",
4040
4401
  width: "1px",
4041
4402
  height: "1px",