@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.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { accordionSchema, annotateSchema, architectureSchema, calloutSchema, cardSchema, chartSchema, codediffSchema, comparisonSchema, equationSchema, filetreeSchema, flowchartSchema, formSchema, graphSchema, infographicSchema, kanbanSchema, kpiSchema, matrixSchema, mindmapSchema, pollSchema, quizSchema, rankerSchema, ratingSchema, relationSchema, sequenceSchema, sliderSchema, stepsSchema, tableSchema, tabsSchema, timelineSchema } from '@glyphjs/schemas';
2
2
  import { RichText } from '@glyphjs/runtime';
3
- import { jsxs, jsx } from 'react/jsx-runtime';
3
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
4
4
  import { useRef, useState, useEffect, useCallback, useMemo } from 'react';
5
5
  import * as d32 from 'd3';
6
6
  import { scaleTime, scaleOrdinal } from 'd3';
@@ -50,7 +50,7 @@ function Callout({ data }) {
50
50
  return /* @__PURE__ */ jsxs("div", { role: "note", "aria-label": CALLOUT_LABELS[type], style: containerStyle11, children: [
51
51
  /* @__PURE__ */ jsx("span", { style: iconStyle, "aria-hidden": "true", children: CALLOUT_ICONS[type] }),
52
52
  /* @__PURE__ */ jsxs("div", { style: bodyStyle3, children: [
53
- title && /* @__PURE__ */ jsx("div", { style: titleStyle2, children: title }),
53
+ title && /* @__PURE__ */ jsx("div", { style: titleStyle2, children: /* @__PURE__ */ jsx(RichText, { content: title }) }),
54
54
  /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(RichText, { content }) })
55
55
  ] })
56
56
  ] });
@@ -1430,6 +1430,326 @@ function computeForceLayout(nodes, edges) {
1430
1430
  height: maxY + LAYOUT_PADDING
1431
1431
  };
1432
1432
  }
1433
+ function useZoomInteraction({
1434
+ svgRef,
1435
+ rootRef,
1436
+ interactionMode
1437
+ }) {
1438
+ const [isActive, setIsActive] = useState(interactionMode === "always");
1439
+ const [hasAttemptedScroll, setHasAttemptedScroll] = useState(false);
1440
+ const containerRef = useRef(null);
1441
+ useEffect(() => {
1442
+ setIsActive(interactionMode === "always");
1443
+ setHasAttemptedScroll(false);
1444
+ }, [interactionMode]);
1445
+ const filterFunction = useCallback(
1446
+ (event) => {
1447
+ if (interactionMode === "always") {
1448
+ return true;
1449
+ }
1450
+ if (interactionMode === "modifier-key") {
1451
+ if (event.type === "mousedown") return true;
1452
+ if (event.type === "wheel") {
1453
+ const wheelEvent = event;
1454
+ const hasModifier = wheelEvent.altKey;
1455
+ if (!hasModifier && !hasAttemptedScroll) {
1456
+ setHasAttemptedScroll(true);
1457
+ setTimeout(() => setHasAttemptedScroll(false), 3e3);
1458
+ }
1459
+ return hasModifier;
1460
+ }
1461
+ return true;
1462
+ }
1463
+ if (interactionMode === "click-to-activate") {
1464
+ return isActive;
1465
+ }
1466
+ return true;
1467
+ },
1468
+ [interactionMode, isActive, hasAttemptedScroll]
1469
+ );
1470
+ useEffect(() => {
1471
+ if (interactionMode !== "modifier-key" || !svgRef.current) return;
1472
+ const svg = svgRef.current;
1473
+ const container = svg.parentElement;
1474
+ if (!container) return;
1475
+ const handleWheel = (event) => {
1476
+ const target = event.target;
1477
+ if (event.altKey && svg.contains(target)) {
1478
+ event.preventDefault();
1479
+ event.stopPropagation();
1480
+ }
1481
+ };
1482
+ container.addEventListener("wheel", handleWheel, { passive: false, capture: true });
1483
+ return () => {
1484
+ container.removeEventListener("wheel", handleWheel, { capture: true });
1485
+ };
1486
+ }, [interactionMode, svgRef]);
1487
+ const zoomBehavior = useMemo(() => {
1488
+ const zoom3 = d32.zoom().scaleExtent([0.1, 4]);
1489
+ if (typeof zoom3.filter === "function") {
1490
+ zoom3.filter(filterFunction);
1491
+ }
1492
+ zoom3.on("zoom", (event) => {
1493
+ if (rootRef.current) {
1494
+ d32.select(rootRef.current).attr("transform", event.transform.toString());
1495
+ }
1496
+ });
1497
+ return zoom3;
1498
+ }, [filterFunction, rootRef]);
1499
+ const handleActivate = useCallback(() => {
1500
+ if (interactionMode === "click-to-activate") {
1501
+ setIsActive(true);
1502
+ }
1503
+ }, [interactionMode]);
1504
+ useEffect(() => {
1505
+ if (interactionMode !== "click-to-activate" || !isActive) return;
1506
+ const handleKeyDown = (e) => {
1507
+ if (e.key === "Escape") {
1508
+ setIsActive(false);
1509
+ }
1510
+ };
1511
+ document.addEventListener("keydown", handleKeyDown);
1512
+ return () => document.removeEventListener("keydown", handleKeyDown);
1513
+ }, [interactionMode, isActive]);
1514
+ useEffect(() => {
1515
+ if (interactionMode !== "click-to-activate" || !isActive) return;
1516
+ const handleClickOutside = (e) => {
1517
+ const container = svgRef.current?.parentElement;
1518
+ if (container && !container.contains(e.target)) {
1519
+ setIsActive(false);
1520
+ }
1521
+ };
1522
+ document.addEventListener("click", handleClickOutside, true);
1523
+ return () => document.removeEventListener("click", handleClickOutside, true);
1524
+ }, [interactionMode, isActive, svgRef]);
1525
+ useEffect(() => {
1526
+ if (svgRef.current) {
1527
+ containerRef.current = svgRef.current.parentElement;
1528
+ }
1529
+ }, [svgRef]);
1530
+ const overlayProps = useMemo(() => {
1531
+ if (interactionMode === "always") return null;
1532
+ if (interactionMode === "modifier-key" && !hasAttemptedScroll) return null;
1533
+ if (interactionMode === "click-to-activate" && isActive) return null;
1534
+ return {
1535
+ mode: interactionMode,
1536
+ isActive,
1537
+ onActivate: handleActivate,
1538
+ width: "100%",
1539
+ height: "100%"
1540
+ };
1541
+ }, [interactionMode, isActive, hasAttemptedScroll, handleActivate]);
1542
+ const zoomIn = useCallback(() => {
1543
+ if (!svgRef.current) return;
1544
+ d32.select(svgRef.current).transition().duration(300).call(zoomBehavior.scaleBy, 1.3);
1545
+ }, [svgRef, zoomBehavior]);
1546
+ const zoomOut = useCallback(() => {
1547
+ if (!svgRef.current) return;
1548
+ d32.select(svgRef.current).transition().duration(300).call(zoomBehavior.scaleBy, 1 / 1.3);
1549
+ }, [svgRef, zoomBehavior]);
1550
+ const resetZoom = useCallback(() => {
1551
+ if (!svgRef.current) return;
1552
+ d32.select(svgRef.current).transition().duration(300).call(zoomBehavior.transform, d32.zoomIdentity);
1553
+ }, [svgRef, zoomBehavior]);
1554
+ return {
1555
+ isActive,
1556
+ overlayProps,
1557
+ zoomBehavior,
1558
+ zoomIn,
1559
+ zoomOut,
1560
+ resetZoom
1561
+ };
1562
+ }
1563
+ function InteractionOverlay({
1564
+ mode,
1565
+ isActive,
1566
+ onActivate,
1567
+ width,
1568
+ height
1569
+ }) {
1570
+ if (mode === "modifier-key") {
1571
+ return /* @__PURE__ */ jsx(
1572
+ "div",
1573
+ {
1574
+ className: "glyph-interaction-overlay",
1575
+ style: {
1576
+ ...OVERLAY_BASE_STYLE,
1577
+ width,
1578
+ height,
1579
+ pointerEvents: "none"
1580
+ },
1581
+ "aria-hidden": "true",
1582
+ children: /* @__PURE__ */ jsx("div", { style: TOOLTIP_STYLE, children: /* @__PURE__ */ jsx("span", { style: TOOLTIP_TEXT_STYLE, children: "Alt + scroll to zoom" }) })
1583
+ }
1584
+ );
1585
+ }
1586
+ if (mode === "click-to-activate" && !isActive) {
1587
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1588
+ /* @__PURE__ */ jsx(
1589
+ "div",
1590
+ {
1591
+ className: "glyph-interaction-overlay",
1592
+ style: {
1593
+ ...OVERLAY_BASE_STYLE,
1594
+ ...ACTIVATION_OVERLAY_STYLE,
1595
+ width,
1596
+ height
1597
+ },
1598
+ onClick: onActivate,
1599
+ role: "button",
1600
+ tabIndex: 0,
1601
+ "aria-label": "Click to activate graph interaction",
1602
+ onKeyDown: (e) => {
1603
+ if (e.key === "Enter" || e.key === " ") {
1604
+ e.preventDefault();
1605
+ onActivate();
1606
+ }
1607
+ },
1608
+ children: /* @__PURE__ */ jsx("div", { style: ACTIVATION_TEXT_STYLE, children: "Click to interact" })
1609
+ }
1610
+ ),
1611
+ /* @__PURE__ */ jsx("div", { style: SR_ONLY_STYLE, role: "status", "aria-live": "polite", "aria-atomic": "true", children: "Graph interaction inactive. Click to activate." })
1612
+ ] });
1613
+ }
1614
+ if (mode === "click-to-activate" && isActive) {
1615
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1616
+ /* @__PURE__ */ jsx(
1617
+ "div",
1618
+ {
1619
+ style: {
1620
+ ...OVERLAY_BASE_STYLE,
1621
+ width,
1622
+ height,
1623
+ pointerEvents: "none",
1624
+ border: "2px solid var(--glyph-interaction-active-border, #0a9d7c)",
1625
+ borderRadius: "4px"
1626
+ },
1627
+ "aria-hidden": "true"
1628
+ }
1629
+ ),
1630
+ /* @__PURE__ */ jsx("div", { style: SR_ONLY_STYLE, role: "status", "aria-live": "polite", "aria-atomic": "true", children: "Graph interaction active. Press Escape to deactivate." })
1631
+ ] });
1632
+ }
1633
+ return /* @__PURE__ */ jsx(Fragment, {});
1634
+ }
1635
+ var OVERLAY_BASE_STYLE = {
1636
+ position: "absolute",
1637
+ top: 0,
1638
+ left: 0,
1639
+ display: "flex",
1640
+ alignItems: "center",
1641
+ justifyContent: "center",
1642
+ zIndex: 10
1643
+ };
1644
+ var TOOLTIP_STYLE = {
1645
+ position: "absolute",
1646
+ bottom: "12px",
1647
+ right: "12px",
1648
+ padding: "6px 10px",
1649
+ backgroundColor: "var(--glyph-interaction-tooltip-bg, rgba(26, 32, 53, 0.9))",
1650
+ color: "var(--glyph-interaction-tooltip-text, #f4f6fa)",
1651
+ borderRadius: "4px",
1652
+ fontSize: "12px",
1653
+ fontFamily: "Inter, system-ui, sans-serif",
1654
+ fontWeight: 500,
1655
+ boxShadow: "0 2px 8px rgba(0, 0, 0, 0.15)",
1656
+ pointerEvents: "none"
1657
+ };
1658
+ var TOOLTIP_TEXT_STYLE = {
1659
+ display: "flex",
1660
+ alignItems: "center",
1661
+ gap: "4px"
1662
+ };
1663
+ var ACTIVATION_OVERLAY_STYLE = {
1664
+ backgroundColor: "var(--glyph-interaction-overlay-bg, rgba(244, 246, 250, 0.8))",
1665
+ cursor: "pointer",
1666
+ transition: "background-color 0.2s ease"
1667
+ };
1668
+ var ACTIVATION_TEXT_STYLE = {
1669
+ padding: "12px 20px",
1670
+ backgroundColor: "var(--glyph-interaction-tooltip-bg, rgba(26, 32, 53, 0.9))",
1671
+ color: "var(--glyph-interaction-tooltip-text, #f4f6fa)",
1672
+ borderRadius: "6px",
1673
+ fontSize: "14px",
1674
+ fontFamily: "Inter, system-ui, sans-serif",
1675
+ fontWeight: 500,
1676
+ boxShadow: "0 4px 12px rgba(0, 0, 0, 0.2)"
1677
+ };
1678
+ var SR_ONLY_STYLE = {
1679
+ position: "absolute",
1680
+ width: "1px",
1681
+ height: "1px",
1682
+ padding: 0,
1683
+ margin: "-1px",
1684
+ overflow: "hidden",
1685
+ clip: "rect(0, 0, 0, 0)",
1686
+ whiteSpace: "nowrap",
1687
+ border: 0
1688
+ };
1689
+ function ZoomControls({ onZoomIn, onZoomOut, onReset }) {
1690
+ return /* @__PURE__ */ jsxs("div", { style: CONTROLS_CONTAINER_STYLE, children: [
1691
+ /* @__PURE__ */ jsx(
1692
+ "button",
1693
+ {
1694
+ onClick: onZoomIn,
1695
+ style: BUTTON_STYLE,
1696
+ "aria-label": "Zoom in",
1697
+ title: "Zoom in",
1698
+ type: "button",
1699
+ children: /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", children: [
1700
+ /* @__PURE__ */ jsx("line", { x1: "8", y1: "4", x2: "8", y2: "12", strokeWidth: "2" }),
1701
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "8", x2: "12", y2: "8", strokeWidth: "2" })
1702
+ ] })
1703
+ }
1704
+ ),
1705
+ /* @__PURE__ */ jsx(
1706
+ "button",
1707
+ {
1708
+ onClick: onZoomOut,
1709
+ style: BUTTON_STYLE,
1710
+ "aria-label": "Zoom out",
1711
+ title: "Zoom out",
1712
+ type: "button",
1713
+ children: /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", children: /* @__PURE__ */ jsx("line", { x1: "4", y1: "8", x2: "12", y2: "8", strokeWidth: "2" }) })
1714
+ }
1715
+ ),
1716
+ /* @__PURE__ */ jsx(
1717
+ "button",
1718
+ {
1719
+ onClick: onReset,
1720
+ style: BUTTON_STYLE,
1721
+ "aria-label": "Reset zoom",
1722
+ title: "Reset zoom",
1723
+ type: "button",
1724
+ children: /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", children: /* @__PURE__ */ jsx("rect", { x: "3", y: "3", width: "10", height: "10", strokeWidth: "2", rx: "1" }) })
1725
+ }
1726
+ )
1727
+ ] });
1728
+ }
1729
+ var CONTROLS_CONTAINER_STYLE = {
1730
+ position: "absolute",
1731
+ top: "12px",
1732
+ right: "12px",
1733
+ display: "flex",
1734
+ flexDirection: "column",
1735
+ gap: "4px",
1736
+ zIndex: 10
1737
+ };
1738
+ var BUTTON_STYLE = {
1739
+ width: "32px",
1740
+ height: "32px",
1741
+ padding: "0",
1742
+ display: "flex",
1743
+ alignItems: "center",
1744
+ justifyContent: "center",
1745
+ backgroundColor: "var(--glyph-surface-raised, #f4f6fa)",
1746
+ border: "1px solid var(--glyph-border, #d0d8e4)",
1747
+ borderRadius: "4px",
1748
+ color: "var(--glyph-text, #1a2035)",
1749
+ cursor: "pointer",
1750
+ transition: "all 0.2s ease",
1751
+ boxShadow: "0 2px 4px rgba(0, 0, 0, 0.1)"
1752
+ };
1433
1753
  var GROUP_PALETTE = [
1434
1754
  "#00d4aa",
1435
1755
  // cyan-green
@@ -1476,7 +1796,7 @@ function getThemeVar(container, varName, fallback) {
1476
1796
  return getComputedStyle(container).getPropertyValue(varName).trim() || fallback;
1477
1797
  }
1478
1798
  var ARROW_MARKER_ID = "glyph-graph-arrowhead";
1479
- function renderGraph(svgElement, layout, groupIndex, outgoingRefs, onNavigate, onNodeClick) {
1799
+ function renderGraph(svgElement, layout, groupIndex, outgoingRefs, onNavigate, zoomBehavior, onNodeClick) {
1480
1800
  const svg = d32.select(svgElement);
1481
1801
  svg.selectAll("*").remove();
1482
1802
  const width = Math.max(layout.width, 200);
@@ -1489,9 +1809,6 @@ function renderGraph(svgElement, layout, groupIndex, outgoingRefs, onNavigate, o
1489
1809
  const nodeStrokeWidth = getThemeVar(container, "--glyph-node-stroke-width", "1.5");
1490
1810
  const nodeFillOpacity = getThemeVar(container, "--glyph-node-fill-opacity", "0.85");
1491
1811
  const root = svg.append("g").attr("class", "glyph-graph-root");
1492
- const zoomBehavior = d32.zoom().scaleExtent([0.1, 4]).on("zoom", (event) => {
1493
- root.attr("transform", event.transform.toString());
1494
- });
1495
1812
  svg.call(zoomBehavior);
1496
1813
  const navigableNodes = /* @__PURE__ */ new Set();
1497
1814
  const refByAnchor = /* @__PURE__ */ new Map();
@@ -1548,6 +1865,7 @@ function Graph({
1548
1865
  container
1549
1866
  }) {
1550
1867
  const svgRef = useRef(null);
1868
+ const rootRef = useRef(null);
1551
1869
  const groupIndex = useRef(/* @__PURE__ */ new Map());
1552
1870
  const layoutResult = useMemo(() => {
1553
1871
  const direction = resolveLayout(data);
@@ -1556,6 +1874,12 @@ function Graph({
1556
1874
  }
1557
1875
  return computeDagreLayout(data.nodes, data.edges, direction);
1558
1876
  }, [data]);
1877
+ const { overlayProps, zoomBehavior, zoomIn, zoomOut, resetZoom } = useZoomInteraction({
1878
+ svgRef,
1879
+ rootRef,
1880
+ interactionMode: data.interactionMode ?? "modifier-key",
1881
+ blockId: block.id
1882
+ });
1559
1883
  const handleNodeClick = useMemo(() => {
1560
1884
  if (!onInteraction) return void 0;
1561
1885
  return (nodeId, nodeLabel) => {
@@ -1576,27 +1900,36 @@ function Graph({
1576
1900
  groupIndex.current,
1577
1901
  outgoingRefs,
1578
1902
  onNavigate,
1903
+ zoomBehavior,
1579
1904
  handleNodeClick
1580
1905
  );
1581
- }, [layoutResult, outgoingRefs, onNavigate, handleNodeClick]);
1906
+ const rootElement = svgRef.current.querySelector(".glyph-graph-root");
1907
+ if (rootElement) {
1908
+ rootRef.current = rootElement;
1909
+ }
1910
+ }, [layoutResult, outgoingRefs, onNavigate, zoomBehavior, handleNodeClick]);
1582
1911
  const ariaLabel = `${data.type} graph with ${data.nodes.length} nodes and ${data.edges.length} edges`;
1583
1912
  return /* @__PURE__ */ jsxs("div", { className: "glyph-graph-container", children: [
1584
- /* @__PURE__ */ jsx(
1585
- "svg",
1586
- {
1587
- ref: svgRef,
1588
- role: "img",
1589
- "aria-label": ariaLabel,
1590
- width: "100%",
1591
- height: "100%",
1592
- style: {
1593
- minHeight: container.tier === "compact" ? 200 : 300,
1594
- maxHeight: container.tier === "compact" ? 500 : 700,
1595
- display: "block"
1913
+ /* @__PURE__ */ jsxs("div", { style: { position: "relative" }, children: [
1914
+ /* @__PURE__ */ jsx(
1915
+ "svg",
1916
+ {
1917
+ ref: svgRef,
1918
+ role: "img",
1919
+ "aria-label": ariaLabel,
1920
+ width: "100%",
1921
+ height: "100%",
1922
+ style: {
1923
+ minHeight: container.tier === "compact" ? 200 : 300,
1924
+ maxHeight: container.tier === "compact" ? 500 : 700,
1925
+ display: "block"
1926
+ }
1596
1927
  }
1597
- }
1598
- ),
1599
- /* @__PURE__ */ jsxs("table", { className: "sr-only", "aria-label": "Graph data", style: SR_ONLY_STYLE, children: [
1928
+ ),
1929
+ overlayProps && /* @__PURE__ */ jsx(InteractionOverlay, { ...overlayProps }),
1930
+ /* @__PURE__ */ jsx(ZoomControls, { onZoomIn: zoomIn, onZoomOut: zoomOut, onReset: resetZoom })
1931
+ ] }),
1932
+ /* @__PURE__ */ jsxs("table", { className: "sr-only", "aria-label": "Graph data", style: SR_ONLY_STYLE2, children: [
1600
1933
  /* @__PURE__ */ jsx("caption", { children: "Graph nodes and connections" }),
1601
1934
  /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
1602
1935
  /* @__PURE__ */ jsx("th", { scope: "col", children: "Node" }),
@@ -1618,7 +1951,7 @@ function Graph({
1618
1951
  ] })
1619
1952
  ] });
1620
1953
  }
1621
- var SR_ONLY_STYLE = {
1954
+ var SR_ONLY_STYLE2 = {
1622
1955
  position: "absolute",
1623
1956
  width: "1px",
1624
1957
  height: "1px",
@@ -1742,16 +2075,13 @@ function drawCrowsFoot(g, x, y, angle, symbol) {
1742
2075
  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)");
1743
2076
  }
1744
2077
  }
1745
- function renderRelation(svgElement, layout) {
2078
+ function renderRelation(svgElement, layout, zoomBehavior) {
1746
2079
  const svg = d32.select(svgElement);
1747
2080
  svg.selectAll("*").remove();
1748
2081
  const width = Math.max(layout.width, 200);
1749
2082
  const height = Math.max(layout.height, 200);
1750
2083
  svg.attr("viewBox", `0 0 ${width} ${height}`);
1751
2084
  const root = svg.append("g").attr("class", "glyph-relation-root");
1752
- const zoomBehavior = d32.zoom().scaleExtent([0.1, 4]).on("zoom", (event) => {
1753
- root.attr("transform", event.transform.toString());
1754
- });
1755
2085
  svg.call(zoomBehavior);
1756
2086
  const entityMap = /* @__PURE__ */ new Map();
1757
2087
  for (const entity of layout.entities) {
@@ -1823,29 +2153,44 @@ function renderRelation(svgElement, layout) {
1823
2153
  }
1824
2154
  }
1825
2155
  }
1826
- function Relation({ data }) {
2156
+ function Relation({ data, block }) {
1827
2157
  const svgRef = useRef(null);
2158
+ const rootRef = useRef(null);
1828
2159
  const layoutResult = useMemo(() => {
1829
2160
  return computeRelationLayout(data);
1830
2161
  }, [data]);
2162
+ const { overlayProps, zoomBehavior, zoomIn, zoomOut, resetZoom } = useZoomInteraction({
2163
+ svgRef,
2164
+ rootRef,
2165
+ interactionMode: data.interactionMode ?? "modifier-key",
2166
+ blockId: block.id
2167
+ });
1831
2168
  useEffect(() => {
1832
2169
  if (!svgRef.current) return;
1833
- renderRelation(svgRef.current, layoutResult);
1834
- }, [layoutResult]);
2170
+ renderRelation(svgRef.current, layoutResult, zoomBehavior);
2171
+ const rootElement = svgRef.current.querySelector(".glyph-relation-root");
2172
+ if (rootElement) {
2173
+ rootRef.current = rootElement;
2174
+ }
2175
+ }, [layoutResult, zoomBehavior]);
1835
2176
  const ariaLabel = `Entity-relationship diagram with ${data.entities.length} entities and ${data.relationships.length} relationships`;
1836
2177
  return /* @__PURE__ */ jsxs("div", { className: "glyph-relation-container", children: [
1837
- /* @__PURE__ */ jsx(
1838
- "svg",
1839
- {
1840
- ref: svgRef,
1841
- role: "img",
1842
- "aria-label": ariaLabel,
1843
- width: "100%",
1844
- height: "100%",
1845
- style: { minHeight: 300, maxHeight: 700, display: "block" }
1846
- }
1847
- ),
1848
- /* @__PURE__ */ jsxs("table", { className: "sr-only", "aria-label": "Entity-relationship data", style: SR_ONLY_STYLE2, children: [
2178
+ /* @__PURE__ */ jsxs("div", { style: { position: "relative" }, children: [
2179
+ /* @__PURE__ */ jsx(
2180
+ "svg",
2181
+ {
2182
+ ref: svgRef,
2183
+ role: "img",
2184
+ "aria-label": ariaLabel,
2185
+ width: "100%",
2186
+ height: "100%",
2187
+ style: { minHeight: 300, maxHeight: 700, display: "block" }
2188
+ }
2189
+ ),
2190
+ overlayProps && /* @__PURE__ */ jsx(InteractionOverlay, { ...overlayProps }),
2191
+ /* @__PURE__ */ jsx(ZoomControls, { onZoomIn: zoomIn, onZoomOut: zoomOut, onReset: resetZoom })
2192
+ ] }),
2193
+ /* @__PURE__ */ jsxs("table", { className: "sr-only", "aria-label": "Entity-relationship data", style: SR_ONLY_STYLE3, children: [
1849
2194
  /* @__PURE__ */ jsx("caption", { children: "Entities and relationships" }),
1850
2195
  /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
1851
2196
  /* @__PURE__ */ jsx("th", { scope: "col", children: "Entity" }),
@@ -1868,7 +2213,7 @@ function Relation({ data }) {
1868
2213
  ] })
1869
2214
  ] });
1870
2215
  }
1871
- var SR_ONLY_STYLE2 = {
2216
+ var SR_ONLY_STYLE3 = {
1872
2217
  position: "absolute",
1873
2218
  width: "1px",
1874
2219
  height: "1px",
@@ -2504,7 +2849,7 @@ function renderNodeShape(nodeG, node, fillOpacity, strokeWidth) {
2504
2849
  }
2505
2850
  }
2506
2851
  }
2507
- function renderFlowchart(svgElement, layout) {
2852
+ function renderFlowchart(svgElement, layout, zoomBehavior) {
2508
2853
  const svg = d32.select(svgElement);
2509
2854
  svg.selectAll("*").remove();
2510
2855
  const width = Math.max(layout.width, 200);
@@ -2516,9 +2861,6 @@ function renderFlowchart(svgElement, layout) {
2516
2861
  const nodeStrokeWidth = getThemeVar2(container, "--glyph-node-stroke-width", "1.5");
2517
2862
  const nodeFillOpacity = getThemeVar2(container, "--glyph-node-fill-opacity", "0.85");
2518
2863
  const root = svg.append("g").attr("class", "glyph-flowchart-root");
2519
- const zoomBehavior = d32.zoom().scaleExtent([0.1, 4]).on("zoom", (event) => {
2520
- root.attr("transform", event.transform.toString());
2521
- });
2522
2864
  svg.call(zoomBehavior);
2523
2865
  const lineGen = d32.line().x((d) => d.x).y((d) => d.y).curve(d32.curveBasis);
2524
2866
  const edgeGroup = root.append("g").attr("class", "glyph-flowchart-edges");
@@ -2539,13 +2881,28 @@ function renderFlowchart(svgElement, layout) {
2539
2881
  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);
2540
2882
  }
2541
2883
  }
2542
- function Flowchart({ data, container }) {
2884
+ function Flowchart({
2885
+ data,
2886
+ block,
2887
+ container
2888
+ }) {
2543
2889
  const svgRef = useRef(null);
2890
+ const rootRef = useRef(null);
2544
2891
  const layoutResult = useMemo(() => computeLayout(data.nodes, data.edges, data.direction), [data]);
2892
+ const { overlayProps, zoomBehavior, zoomIn, zoomOut, resetZoom } = useZoomInteraction({
2893
+ svgRef,
2894
+ rootRef,
2895
+ interactionMode: data.interactionMode ?? "modifier-key",
2896
+ blockId: block.id
2897
+ });
2545
2898
  useEffect(() => {
2546
2899
  if (!svgRef.current) return;
2547
- renderFlowchart(svgRef.current, layoutResult);
2548
- }, [layoutResult]);
2900
+ renderFlowchart(svgRef.current, layoutResult, zoomBehavior);
2901
+ const rootElement = svgRef.current.querySelector(".glyph-flowchart-root");
2902
+ if (rootElement) {
2903
+ rootRef.current = rootElement;
2904
+ }
2905
+ }, [layoutResult, zoomBehavior]);
2549
2906
  const nodeCount = data.nodes.length;
2550
2907
  const edgeCount = data.edges.length;
2551
2908
  const ariaLabel = data.title ? `${data.title}: flowchart with ${nodeCount} nodes and ${edgeCount} edges` : `Flowchart with ${nodeCount} nodes and ${edgeCount} edges`;
@@ -2563,22 +2920,26 @@ function Flowchart({ data, container }) {
2563
2920
  children: data.title
2564
2921
  }
2565
2922
  ),
2566
- /* @__PURE__ */ jsx(
2567
- "svg",
2568
- {
2569
- ref: svgRef,
2570
- role: "img",
2571
- "aria-label": ariaLabel,
2572
- width: "100%",
2573
- height: "100%",
2574
- style: {
2575
- minHeight: container.tier === "compact" ? 200 : 300,
2576
- maxHeight: container.tier === "compact" ? 500 : 700,
2577
- display: "block"
2923
+ /* @__PURE__ */ jsxs("div", { style: { position: "relative" }, children: [
2924
+ /* @__PURE__ */ jsx(
2925
+ "svg",
2926
+ {
2927
+ ref: svgRef,
2928
+ role: "img",
2929
+ "aria-label": ariaLabel,
2930
+ width: "100%",
2931
+ height: "100%",
2932
+ style: {
2933
+ minHeight: container.tier === "compact" ? 200 : 300,
2934
+ maxHeight: container.tier === "compact" ? 500 : 700,
2935
+ display: "block"
2936
+ }
2578
2937
  }
2579
- }
2580
- ),
2581
- /* @__PURE__ */ jsxs("table", { className: "sr-only", "aria-label": "Flowchart data", style: SR_ONLY_STYLE3, children: [
2938
+ ),
2939
+ overlayProps && /* @__PURE__ */ jsx(InteractionOverlay, { ...overlayProps }),
2940
+ /* @__PURE__ */ jsx(ZoomControls, { onZoomIn: zoomIn, onZoomOut: zoomOut, onReset: resetZoom })
2941
+ ] }),
2942
+ /* @__PURE__ */ jsxs("table", { className: "sr-only", "aria-label": "Flowchart data", style: SR_ONLY_STYLE4, children: [
2582
2943
  /* @__PURE__ */ jsx("caption", { children: "Flowchart nodes and connections" }),
2583
2944
  /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
2584
2945
  /* @__PURE__ */ jsx("th", { scope: "col", children: "Node" }),
@@ -2600,7 +2961,7 @@ function Flowchart({ data, container }) {
2600
2961
  ] })
2601
2962
  ] });
2602
2963
  }
2603
- var SR_ONLY_STYLE3 = {
2964
+ var SR_ONLY_STYLE4 = {
2604
2965
  position: "absolute",
2605
2966
  width: "1px",
2606
2967
  height: "1px",
@@ -3222,7 +3583,7 @@ function Sequence({ data, container }) {
3222
3583
  ]
3223
3584
  }
3224
3585
  ),
3225
- /* @__PURE__ */ jsxs("table", { className: "sr-only", "aria-label": "Sequence data", style: SR_ONLY_STYLE4, children: [
3586
+ /* @__PURE__ */ jsxs("table", { className: "sr-only", "aria-label": "Sequence data", style: SR_ONLY_STYLE5, children: [
3226
3587
  /* @__PURE__ */ jsx("caption", { children: "Sequence messages in order" }),
3227
3588
  /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
3228
3589
  /* @__PURE__ */ jsx("th", { scope: "col", children: "#" }),
@@ -3245,7 +3606,7 @@ function Sequence({ data, container }) {
3245
3606
  ] })
3246
3607
  ] });
3247
3608
  }
3248
- var SR_ONLY_STYLE4 = {
3609
+ var SR_ONLY_STYLE5 = {
3249
3610
  position: "absolute",
3250
3611
  width: "1px",
3251
3612
  height: "1px",
@@ -3651,7 +4012,7 @@ function Architecture({
3651
4012
  }
3652
4013
  }
3653
4014
  ),
3654
- /* @__PURE__ */ jsxs("table", { className: "sr-only", "aria-label": "Architecture data", style: SR_ONLY_STYLE5, children: [
4015
+ /* @__PURE__ */ jsxs("table", { className: "sr-only", "aria-label": "Architecture data", style: SR_ONLY_STYLE6, children: [
3655
4016
  /* @__PURE__ */ jsx("caption", { children: "Architecture nodes and connections" }),
3656
4017
  /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
3657
4018
  /* @__PURE__ */ jsx("th", { scope: "col", children: "Node" }),
@@ -3695,7 +4056,7 @@ function countLeafNodes(children) {
3695
4056
  }
3696
4057
  return count;
3697
4058
  }
3698
- var SR_ONLY_STYLE5 = {
4059
+ var SR_ONLY_STYLE6 = {
3699
4060
  position: "absolute",
3700
4061
  width: "1px",
3701
4062
  height: "1px",
@@ -3883,7 +4244,7 @@ function layoutTree(data) {
3883
4244
  };
3884
4245
  }
3885
4246
  function renderAccessibleList(root, children) {
3886
- return /* @__PURE__ */ jsx("ul", { style: SR_ONLY_STYLE6, role: "list", "aria-label": "Mind map structure", children: /* @__PURE__ */ jsxs("li", { children: [
4247
+ return /* @__PURE__ */ jsx("ul", { style: SR_ONLY_STYLE7, role: "list", "aria-label": "Mind map structure", children: /* @__PURE__ */ jsxs("li", { children: [
3887
4248
  root,
3888
4249
  children.length > 0 && renderAccessibleChildren(children)
3889
4250
  ] }) });
@@ -4010,7 +4371,7 @@ function MindMap({ data, container }) {
4010
4371
  renderAccessibleList(data.root, data.children)
4011
4372
  ] });
4012
4373
  }
4013
- var SR_ONLY_STYLE6 = {
4374
+ var SR_ONLY_STYLE7 = {
4014
4375
  position: "absolute",
4015
4376
  width: "1px",
4016
4377
  height: "1px",