@petrarca/sonnet-graph 0.3.0 → 0.4.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.js CHANGED
@@ -436,7 +436,8 @@ var useGraphFilter = createGraphFilterStore();
436
436
 
437
437
  // src/stores/GraphExploration.ts
438
438
  import { create as create4 } from "zustand";
439
- import { devLog as devLog3 } from "@petrarca/sonnet-core";
439
+ import { devLog as devLog3, errorLog } from "@petrarca/sonnet-core";
440
+ import { toast } from "sonner";
440
441
  function collectReachable(startId, direction, allEdges, includeStart) {
441
442
  const nodes = /* @__PURE__ */ new Set();
442
443
  const edges = /* @__PURE__ */ new Set();
@@ -542,6 +543,23 @@ function ensureNodeLabelInFilters(enrichedNodes, nodeId, graphFilterStore) {
542
543
  graphFilterStore.getState().setNodeLabelFilters([...currentFilters, nodeLabel]);
543
544
  }
544
545
  }
546
+ function mergeExpandedLabelsIntoFilters(stores, enrichedNodes, nodeId) {
547
+ const graphFilterState = stores.graphFilter.getState();
548
+ const currentFilters = graphFilterState.nodeLabelFilters;
549
+ if (currentFilters.length === 0 || enrichedNodes.length === 0) return;
550
+ const newLabels = new Set(enrichedNodes.map((n) => n.label_));
551
+ const labelsToAdd = Array.from(newLabels).filter(
552
+ (label) => !currentFilters.includes(label)
553
+ );
554
+ if (labelsToAdd.length > 0) {
555
+ devLog3("[GraphExploration] Adding expanded node labels to active filters", {
556
+ nodeId,
557
+ labelsToAdd,
558
+ currentFilters
559
+ });
560
+ stores.graphFilter.getState().setNodeLabelFilters([...currentFilters, ...labelsToAdd]);
561
+ }
562
+ }
545
563
  function createGraphExplorationStore(deps) {
546
564
  const stores = deps;
547
565
  return create4()((set, get) => ({
@@ -841,10 +859,10 @@ function createGraphExplorationStore(deps) {
841
859
  isNodeExpanded: (nodeId) => {
842
860
  return get().expandedNodeIds.has(nodeId);
843
861
  },
844
- expandNode: async (nodeId) => {
845
- devLog3("[GraphExploration] expandNode", { nodeId });
862
+ expandNode: async (nodeId, options) => {
863
+ devLog3("[GraphExploration] expandNode", { nodeId, options });
846
864
  try {
847
- const result = await stores.expandNode(Number(nodeId));
865
+ const result = await stores.expandNode(Number(nodeId), options);
848
866
  devLog3("[GraphExploration] Expansion API result", {
849
867
  nodeId,
850
868
  newNodes: result.nodes.length,
@@ -866,25 +884,7 @@ function createGraphExplorationStore(deps) {
866
884
  );
867
885
  const enrichedEdges = enrichEdges(result.edges);
868
886
  stores.graphData.getState().mergeExpandedData(enrichedNodes, enrichedEdges);
869
- const graphFilterState = stores.graphFilter.getState();
870
- const currentFilters = graphFilterState.nodeLabelFilters;
871
- if (currentFilters.length > 0 && enrichedNodes.length > 0) {
872
- const newLabels = new Set(enrichedNodes.map((n) => n.label_));
873
- const labelsToAdd = Array.from(newLabels).filter(
874
- (label) => !currentFilters.includes(label)
875
- );
876
- if (labelsToAdd.length > 0) {
877
- devLog3(
878
- "[GraphExploration] Adding expanded node labels to active filters",
879
- {
880
- nodeId,
881
- labelsToAdd,
882
- currentFilters
883
- }
884
- );
885
- stores.graphFilter.getState().setNodeLabelFilters([...currentFilters, ...labelsToAdd]);
886
- }
887
- }
887
+ mergeExpandedLabelsIntoFilters(stores, enrichedNodes, nodeId);
888
888
  get().markNodeExpanded(nodeId);
889
889
  const currentState = get();
890
890
  if (currentState.isFocusMode) {
@@ -900,13 +900,19 @@ function createGraphExplorationStore(deps) {
900
900
  });
901
901
  set({ focusedNodeIds: newFocusedIds });
902
902
  }
903
+ if (result.has_more) {
904
+ toast.info(
905
+ `Showing first ${enrichedNodes.length} neighbors \u2014 more exist. Refine filters or increase the limit.`
906
+ );
907
+ }
903
908
  devLog3("[GraphExploration] Expansion complete", {
904
909
  nodeId,
905
910
  addedNodes: enrichedNodes.length,
906
- addedEdges: enrichedEdges.length
911
+ addedEdges: enrichedEdges.length,
912
+ hasMore: result.has_more
907
913
  });
908
914
  } catch (error) {
909
- devLog3("[GraphExploration] Expansion failed", { nodeId, error });
915
+ errorLog("[GraphExploration] Expansion failed", { nodeId, error });
910
916
  throw error;
911
917
  }
912
918
  },
@@ -1439,10 +1445,10 @@ function GraphRendererProvider({
1439
1445
 
1440
1446
  // src/components/rea/ReaGraph.tsx
1441
1447
  import {
1442
- useEffect as useEffect2,
1443
- useRef,
1444
- useState as useState3,
1445
- useCallback as useCallback2
1448
+ useEffect as useEffect3,
1449
+ useRef as useRef2,
1450
+ useState as useState5,
1451
+ useCallback as useCallback3
1446
1452
  } from "react";
1447
1453
  import { devLog as devLog8 } from "@petrarca/sonnet-core";
1448
1454
  import {
@@ -1662,16 +1668,194 @@ function NodeHoverInfo({ node }) {
1662
1668
  var NodeHoverInfo_default = NodeHoverInfo;
1663
1669
 
1664
1670
  // src/components/rea/ReaContextMenu.tsx
1665
- import React2 from "react";
1671
+ import React3, { useState as useState4 } from "react";
1666
1672
  import { devLog as devLog7 } from "@petrarca/sonnet-core";
1673
+
1674
+ // src/components/rea/ExpandOptionsPanel.tsx
1675
+ import { useCallback as useCallback2, useEffect as useEffect2, useRef, useState as useState3 } from "react";
1676
+ import { Checkbox, Input, Label } from "@petrarca/sonnet-ui";
1667
1677
  import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1678
+ function ExpandOptionsPanel({
1679
+ nodeLabel,
1680
+ onExpand,
1681
+ onBack,
1682
+ getEdgeTypes
1683
+ }) {
1684
+ const [direction, setDirection] = useState3("both");
1685
+ const [limit, setLimit] = useState3(100);
1686
+ const [edgeTypes, setEdgeTypes] = useState3([]);
1687
+ const [selectedTypes, setSelectedTypes] = useState3([]);
1688
+ const [loading, setLoading] = useState3(!!getEdgeTypes);
1689
+ const [offset, setOffset] = useState3({ x: 0, y: 0 });
1690
+ const dragRef = useRef(null);
1691
+ const onPointerDown = useCallback2(
1692
+ (e) => {
1693
+ if (e.target.closest("button")) return;
1694
+ e.preventDefault();
1695
+ e.target.setPointerCapture(e.pointerId);
1696
+ dragRef.current = {
1697
+ startX: e.clientX,
1698
+ startY: e.clientY,
1699
+ origX: offset.x,
1700
+ origY: offset.y
1701
+ };
1702
+ },
1703
+ [offset]
1704
+ );
1705
+ const onPointerMove = useCallback2((e) => {
1706
+ if (!dragRef.current) return;
1707
+ const dx = e.clientX - dragRef.current.startX;
1708
+ const dy = e.clientY - dragRef.current.startY;
1709
+ setOffset({ x: dragRef.current.origX + dx, y: dragRef.current.origY + dy });
1710
+ }, []);
1711
+ const onPointerUp = useCallback2(() => {
1712
+ dragRef.current = null;
1713
+ }, []);
1714
+ useEffect2(() => {
1715
+ if (!getEdgeTypes) return;
1716
+ setLoading(true);
1717
+ getEdgeTypes().then((types) => {
1718
+ setEdgeTypes(types);
1719
+ setSelectedTypes(types);
1720
+ }).finally(() => setLoading(false));
1721
+ }, [getEdgeTypes]);
1722
+ const handleToggleType = (type) => {
1723
+ setSelectedTypes(
1724
+ (prev) => prev.includes(type) ? prev.filter((t) => t !== type) : [...prev, type]
1725
+ );
1726
+ };
1727
+ const handleExpand = () => {
1728
+ const options = {};
1729
+ if (direction !== "both") options.direction = direction;
1730
+ const clampedLimit = Math.max(1, Math.min(1e4, limit));
1731
+ if (clampedLimit !== 100) options.limit = clampedLimit;
1732
+ if (selectedTypes.length > 0 && selectedTypes.length < edgeTypes.length) {
1733
+ options.relTypes = selectedTypes;
1734
+ }
1735
+ onExpand(options);
1736
+ };
1737
+ const allSelected = selectedTypes.length === edgeTypes.length;
1738
+ const noneSelected = edgeTypes.length > 0 && selectedTypes.length === 0;
1739
+ return /* @__PURE__ */ jsxs4(
1740
+ "div",
1741
+ {
1742
+ className: "bg-background border border-border rounded-md shadow-md p-2.5 min-w-48 space-y-2",
1743
+ style: { transform: `translate(${offset.x}px, ${offset.y}px)` },
1744
+ children: [
1745
+ /* @__PURE__ */ jsxs4(
1746
+ "div",
1747
+ {
1748
+ className: "flex items-center justify-between gap-2 cursor-grab active:cursor-grabbing select-none",
1749
+ onPointerDown,
1750
+ onPointerMove,
1751
+ onPointerUp,
1752
+ children: [
1753
+ /* @__PURE__ */ jsx5(
1754
+ "button",
1755
+ {
1756
+ className: "text-[11px] text-muted-foreground hover:text-foreground cursor-pointer",
1757
+ onClick: onBack,
1758
+ children: "Back"
1759
+ }
1760
+ ),
1761
+ /* @__PURE__ */ jsx5("span", { className: "text-[11px] font-medium text-foreground truncate max-w-32", children: nodeLabel })
1762
+ ]
1763
+ }
1764
+ ),
1765
+ /* @__PURE__ */ jsxs4("div", { className: "space-y-1", children: [
1766
+ /* @__PURE__ */ jsx5(Label, { className: "text-[11px] text-muted-foreground", children: "Direction" }),
1767
+ /* @__PURE__ */ jsx5("div", { className: "flex gap-0.5", children: ["both", "out", "in"].map((dir) => /* @__PURE__ */ jsx5(
1768
+ "button",
1769
+ {
1770
+ className: `flex-1 px-2 py-1 text-[11px] capitalize rounded border cursor-pointer ${direction === dir ? "bg-primary/10 text-primary border-primary/40 font-medium" : "bg-background text-muted-foreground border-border hover:bg-muted"}`,
1771
+ onClick: () => setDirection(dir),
1772
+ children: dir
1773
+ },
1774
+ dir
1775
+ )) })
1776
+ ] }),
1777
+ !loading && edgeTypes.length > 1 && /* @__PURE__ */ jsxs4("div", { className: "space-y-1", children: [
1778
+ /* @__PURE__ */ jsxs4("div", { className: "flex items-center justify-between", children: [
1779
+ /* @__PURE__ */ jsx5(Label, { className: "text-[11px] text-muted-foreground", children: "Rel Types" }),
1780
+ /* @__PURE__ */ jsx5(
1781
+ "button",
1782
+ {
1783
+ className: "text-[10px] text-muted-foreground hover:text-foreground cursor-pointer",
1784
+ onClick: () => setSelectedTypes(allSelected ? [] : [...edgeTypes]),
1785
+ children: allSelected ? "None" : "All"
1786
+ }
1787
+ )
1788
+ ] }),
1789
+ /* @__PURE__ */ jsx5("div", { className: "space-y-0.5 max-h-32 overflow-y-auto", children: edgeTypes.map((type) => /* @__PURE__ */ jsxs4(
1790
+ "label",
1791
+ {
1792
+ className: "flex items-center gap-1.5 text-[11px] cursor-pointer",
1793
+ children: [
1794
+ /* @__PURE__ */ jsx5(
1795
+ Checkbox,
1796
+ {
1797
+ checked: selectedTypes.includes(type),
1798
+ onCheckedChange: () => handleToggleType(type),
1799
+ className: "h-3 w-3"
1800
+ }
1801
+ ),
1802
+ /* @__PURE__ */ jsx5("span", { className: "truncate", children: type })
1803
+ ]
1804
+ },
1805
+ type
1806
+ )) })
1807
+ ] }),
1808
+ /* @__PURE__ */ jsxs4("div", { className: "space-y-1", children: [
1809
+ /* @__PURE__ */ jsx5(Label, { className: "text-[11px] text-muted-foreground", children: "Limit" }),
1810
+ /* @__PURE__ */ jsx5(
1811
+ Input,
1812
+ {
1813
+ type: "number",
1814
+ min: 1,
1815
+ max: 1e4,
1816
+ value: limit,
1817
+ onChange: (e) => {
1818
+ const v = Number(e.target.value);
1819
+ if (!Number.isNaN(v)) setLimit(Math.max(1, Math.min(1e4, v)));
1820
+ },
1821
+ className: "h-6 text-[11px]"
1822
+ }
1823
+ )
1824
+ ] }),
1825
+ /* @__PURE__ */ jsx5(
1826
+ "button",
1827
+ {
1828
+ className: "w-full py-1.5 text-[11px] font-medium rounded bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer",
1829
+ onClick: handleExpand,
1830
+ disabled: loading || noneSelected,
1831
+ title: noneSelected ? "Select at least one relationship type" : void 0,
1832
+ children: "Expand"
1833
+ }
1834
+ )
1835
+ ]
1836
+ }
1837
+ );
1838
+ }
1839
+ var ExpandOptionsPanel_default = ExpandOptionsPanel;
1840
+
1841
+ // src/components/rea/ReaContextMenu.tsx
1842
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1668
1843
  function ReaContextMenu({
1669
1844
  data,
1670
1845
  onClose,
1671
1846
  elementType,
1672
1847
  useGraphExploration,
1673
- onCopyNode
1848
+ onCopyNode,
1849
+ getNodeEdgeTypes
1674
1850
  }) {
1851
+ const [showExpandPanel, setShowExpandPanel] = useState4(false);
1852
+ React3.useEffect(() => {
1853
+ const handleKey = (e) => {
1854
+ if (e.key === "Escape") onClose();
1855
+ };
1856
+ document.addEventListener("keydown", handleKey);
1857
+ return () => document.removeEventListener("keydown", handleKey);
1858
+ }, [onClose]);
1675
1859
  const handleFocus = () => {
1676
1860
  if (elementType === "node") {
1677
1861
  const nodeId = data.id;
@@ -1724,11 +1908,11 @@ function ReaContextMenu({
1724
1908
  }
1725
1909
  onClose();
1726
1910
  };
1727
- const handleExpand = () => {
1911
+ const handleExpandWithOptions = (options) => {
1728
1912
  if (elementType === "node") {
1729
1913
  const nodeId = data.id;
1730
- devLog7("[ReaContextMenu] Expand node:", nodeId);
1731
- useGraphExploration.getState().expandNode(nodeId);
1914
+ devLog7("[ReaContextMenu] Expand node with options:", nodeId, options);
1915
+ useGraphExploration.getState().expandNode(nodeId, options);
1732
1916
  }
1733
1917
  onClose();
1734
1918
  };
@@ -1763,7 +1947,7 @@ function ReaContextMenu({
1763
1947
  { label: "Hide Node", action: handleHideNode },
1764
1948
  { label: "Hide Incoming", action: handleHideIncoming },
1765
1949
  { label: "Hide Outgoing", action: handleHideOutgoing },
1766
- { label: "Expand", action: handleExpand }
1950
+ { label: "Expand...", action: () => setShowExpandPanel(true) }
1767
1951
  ];
1768
1952
  const edgeMenuItems = [
1769
1953
  { label: "Focus", action: handleFocus },
@@ -1773,23 +1957,46 @@ function ReaContextMenu({
1773
1957
  { label: "Hide Reverse Path", action: handleHideReversePath }
1774
1958
  ];
1775
1959
  const menuItems = elementType === "node" ? nodeMenuItems : edgeMenuItems;
1776
- return /* @__PURE__ */ jsx5("div", { className: "bg-white border border-gray-200 rounded-md shadow-lg py-1 min-w-48", children: menuItems.map((item, index) => /* @__PURE__ */ jsxs4(React2.Fragment, { children: [
1777
- /* @__PURE__ */ jsx5(
1960
+ if (showExpandPanel && elementType === "node") {
1961
+ const nodeLabel = data.label || data.id;
1962
+ return /* @__PURE__ */ jsx6(
1963
+ ExpandOptionsPanel_default,
1964
+ {
1965
+ nodeLabel: String(nodeLabel),
1966
+ onExpand: handleExpandWithOptions,
1967
+ onBack: () => setShowExpandPanel(false),
1968
+ getEdgeTypes: getNodeEdgeTypes ? () => getNodeEdgeTypes(Number(data.id)) : void 0
1969
+ }
1970
+ );
1971
+ }
1972
+ return /* @__PURE__ */ jsxs5("div", { className: "bg-background border border-border rounded-md shadow-lg py-1 min-w-48", children: [
1973
+ menuItems.map((item, index) => /* @__PURE__ */ jsxs5(React3.Fragment, { children: [
1974
+ /* @__PURE__ */ jsx6(
1975
+ "button",
1976
+ {
1977
+ className: "w-full text-left px-3 py-2 text-sm hover:bg-muted/50 cursor-pointer",
1978
+ onClick: item.action,
1979
+ children: item.label
1980
+ }
1981
+ ),
1982
+ index === 1 && /* @__PURE__ */ jsx6("div", { className: "border-t border-border my-1" })
1983
+ ] }, item.label)),
1984
+ /* @__PURE__ */ jsx6("div", { className: "border-t border-border my-1" }),
1985
+ /* @__PURE__ */ jsx6(
1778
1986
  "button",
1779
1987
  {
1780
- className: "w-full text-left px-3 py-2 text-sm hover:bg-gray-100 cursor-pointer",
1781
- onClick: item.action,
1782
- children: item.label
1988
+ className: "w-full text-left px-3 py-1.5 text-xs text-muted-foreground hover:bg-muted/50 cursor-pointer",
1989
+ onClick: onClose,
1990
+ children: "Cancel"
1783
1991
  }
1784
- ),
1785
- index === 1 && /* @__PURE__ */ jsx5("div", { className: "border-t border-gray-200 my-1" })
1786
- ] }, item.label)) });
1992
+ )
1993
+ ] });
1787
1994
  }
1788
1995
  var ReaContextMenu_default = ReaContextMenu;
1789
1996
 
1790
1997
  // src/components/rea/ReaGraph.tsx
1791
- import { toast } from "sonner";
1792
- import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1998
+ import { toast as toast2 } from "sonner";
1999
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1793
2000
  function findElementInGraphData(elementId, elementType, nodes, edges) {
1794
2001
  if (elementType === "node") {
1795
2002
  const node = nodes.find((n) => n.id === elementId);
@@ -1851,22 +2058,26 @@ function processFocusAction(focusAction, canvasRef, nodes, edges, setSelectedNod
1851
2058
  }
1852
2059
  return true;
1853
2060
  }
1854
- function ReaGraph({ onCopyNode, extraToolbarContent }) {
1855
- const isMountedRef = useRef(false);
2061
+ function ReaGraph({
2062
+ onCopyNode,
2063
+ extraToolbarContent,
2064
+ getNodeEdgeTypes
2065
+ }) {
2066
+ const isMountedRef = useRef2(false);
1856
2067
  const { nodes, edges } = useReagraphData();
1857
2068
  const { normalized } = useLiveGraphData();
1858
- useEffect2(() => {
2069
+ useEffect3(() => {
1859
2070
  devLog8("[ReGraph] counts", { nodes: nodes.length, edges: edges.length });
1860
2071
  if (nodes.length > 0) devLog8("[ReGraph] sample node", nodes[0]);
1861
2072
  if (edges.length > 0) devLog8("[ReGraph] sample edge", edges[0]);
1862
2073
  }, [nodes, edges]);
1863
- const canvas = useRef(null);
1864
- const [hoveredNode, setHoveredNode] = useState3(
2074
+ const canvas = useRef2(null);
2075
+ const [hoveredNode, setHoveredNode] = useState5(
1865
2076
  void 0
1866
2077
  );
1867
- const [layout, setLayout] = useState3("forceDirected2d");
1868
- const [fullscreen, setFullscreen] = useState3(false);
1869
- const [pathSelectionType, setPathSelectionType] = useState3("direct");
2078
+ const [layout, setLayout] = useState5("forceDirected2d");
2079
+ const [fullscreen, setFullscreen] = useState5(false);
2080
+ const [pathSelectionType, setPathSelectionType] = useState5("direct");
1870
2081
  const useGraphSelection = useWorkspaceSelection();
1871
2082
  const useGraphFilter2 = useWorkspaceGraphFilter();
1872
2083
  const useGraphExploration = useWorkspaceGraphExploration();
@@ -1892,7 +2103,7 @@ function ReaGraph({ onCopyNode, extraToolbarContent }) {
1892
2103
  pathSelectionType,
1893
2104
  type: "multi"
1894
2105
  });
1895
- useEffect2(() => {
2106
+ useEffect3(() => {
1896
2107
  const unsubscribe = GraphFocus_default.subscribe((state) => {
1897
2108
  const focusAction = state.focusAction;
1898
2109
  if (!focusAction || !canvas.current) return;
@@ -1916,12 +2127,12 @@ function ReaGraph({ onCopyNode, extraToolbarContent }) {
1916
2127
  });
1917
2128
  return unsubscribe;
1918
2129
  }, [nodes, edges, setSelectedNodeIds, setSelectedEdge]);
1919
- useEffect2(() => {
2130
+ useEffect3(() => {
1920
2131
  if (!canvas.current) return;
1921
2132
  clearSelections();
1922
2133
  clearSelectionStore();
1923
2134
  }, [lastDataChangeTime, clearSelections, clearSelectionStore]);
1924
- const fitToScreen = useCallback2(
2135
+ const fitToScreen = useCallback3(
1925
2136
  (reason) => {
1926
2137
  if (!isMountedRef.current) return;
1927
2138
  if (nodes.length === 0) return;
@@ -1947,7 +2158,7 @@ function ReaGraph({ onCopyNode, extraToolbarContent }) {
1947
2158
  },
1948
2159
  [nodes.length]
1949
2160
  );
1950
- useEffect2(() => {
2161
+ useEffect3(() => {
1951
2162
  isMountedRef.current = true;
1952
2163
  fitToScreen("mount");
1953
2164
  const state = useGraphSelection.getState();
@@ -1967,7 +2178,7 @@ function ReaGraph({ onCopyNode, extraToolbarContent }) {
1967
2178
  isMountedRef.current = false;
1968
2179
  };
1969
2180
  }, [fitToScreen, setSelections, setSelectedEdge, useGraphSelection]);
1970
- useEffect2(() => {
2181
+ useEffect3(() => {
1971
2182
  if (isMountedRef.current) fitToScreen("counts-changed");
1972
2183
  }, [nodes.length, edges.length, fitToScreen]);
1973
2184
  function updateLayout(next) {
@@ -2009,14 +2220,14 @@ function ReaGraph({ onCopyNode, extraToolbarContent }) {
2009
2220
  setSelectedEdge(null);
2010
2221
  if (onCanvasClick) onCanvasClick(e);
2011
2222
  }
2012
- useEffect2(() => {
2223
+ useEffect3(() => {
2013
2224
  if (!isMountedRef.current) return;
2014
2225
  if (selections.length > 0 && selections.length <= 2) {
2015
2226
  const selectedNodes = selections.map((id) => nodes.find((n) => n.id === id)).filter(Boolean);
2016
2227
  setSelectedNodeIds(selectedNodes.map((n) => n.id));
2017
2228
  }
2018
2229
  }, [selections, nodes]);
2019
- useEffect2(() => {
2230
+ useEffect3(() => {
2020
2231
  if (!isMountedRef.current) return;
2021
2232
  const currentSelectedIds = useGraphSelection.getState().selectedNodeIds;
2022
2233
  if (!currentSelectedIds.length) return;
@@ -2035,7 +2246,7 @@ function ReaGraph({ onCopyNode, extraToolbarContent }) {
2035
2246
  setSelectedEdge,
2036
2247
  useGraphSelection
2037
2248
  ]);
2038
- useEffect2(() => {
2249
+ useEffect3(() => {
2039
2250
  if (!isMountedRef.current) return;
2040
2251
  const currentSelectedIds = useGraphSelection.getState().selectedNodeIds;
2041
2252
  if (!currentSelectedIds.length) return;
@@ -2047,7 +2258,7 @@ function ReaGraph({ onCopyNode, extraToolbarContent }) {
2047
2258
  if (stillPresentIds.length < 2) setSelectedEdge(null);
2048
2259
  }
2049
2260
  }, [nodes, setSelectedNodeIds, setSelectedEdge, useGraphSelection]);
2050
- useEffect2(() => {
2261
+ useEffect3(() => {
2051
2262
  if (!fullscreen) return;
2052
2263
  const prevOverflow = document.body.style.overflow;
2053
2264
  document.body.style.overflow = "hidden";
@@ -2061,7 +2272,7 @@ function ReaGraph({ onCopyNode, extraToolbarContent }) {
2061
2272
  window.removeEventListener("keydown", handleKey);
2062
2273
  };
2063
2274
  }, [fullscreen, fitToScreen]);
2064
- useEffect2(() => {
2275
+ useEffect3(() => {
2065
2276
  if (!fullscreen) return;
2066
2277
  const id = setTimeout(() => {
2067
2278
  if (canvas.current) {
@@ -2070,7 +2281,7 @@ function ReaGraph({ onCopyNode, extraToolbarContent }) {
2070
2281
  }, 50);
2071
2282
  return () => clearTimeout(id);
2072
2283
  }, [fullscreen, fitToScreen]);
2073
- useEffect2(() => {
2284
+ useEffect3(() => {
2074
2285
  if (!isMountedRef.current) return;
2075
2286
  const handleResize = () => {
2076
2287
  if (isMountedRef.current && canvas.current) {
@@ -2082,8 +2293,8 @@ function ReaGraph({ onCopyNode, extraToolbarContent }) {
2082
2293
  window.removeEventListener("resize", handleResize);
2083
2294
  };
2084
2295
  }, [fitToScreen]);
2085
- const firstDataRef = useRef(true);
2086
- useEffect2(() => {
2296
+ const firstDataRef = useRef2(true);
2297
+ useEffect3(() => {
2087
2298
  if (!isMountedRef.current) return;
2088
2299
  if (nodes.length > 0) {
2089
2300
  if (firstDataRef.current) {
@@ -2096,18 +2307,18 @@ function ReaGraph({ onCopyNode, extraToolbarContent }) {
2096
2307
  }, [nodes.length, fitToScreen]);
2097
2308
  if (nodes.length === 0) {
2098
2309
  devLog8("[ReGraph] empty state render");
2099
- return /* @__PURE__ */ jsxs5(Card2, { className: "relative w-full h-full items-center justify-center text-slate-500 gap-2 shadow-xs p-4", children: [
2100
- /* @__PURE__ */ jsx6("div", { children: "No data to display." }),
2101
- /* @__PURE__ */ jsxs5("div", { children: [
2310
+ return /* @__PURE__ */ jsxs6(Card2, { className: "relative w-full h-full items-center justify-center text-slate-500 gap-2 shadow-xs p-4", children: [
2311
+ /* @__PURE__ */ jsx7("div", { children: "No data to display." }),
2312
+ /* @__PURE__ */ jsxs6("div", { children: [
2102
2313
  "Please",
2103
2314
  " ",
2104
- /* @__PURE__ */ jsx6("span", { className: "font-semibold", children: "run a query, check filters, or add a node" }),
2315
+ /* @__PURE__ */ jsx7("span", { className: "font-semibold", children: "run a query, check filters, or add a node" }),
2105
2316
  " ",
2106
2317
  "to the workbench to populate the graph."
2107
2318
  ] })
2108
2319
  ] });
2109
2320
  }
2110
- return /* @__PURE__ */ jsxs5(
2321
+ return /* @__PURE__ */ jsxs6(
2111
2322
  "div",
2112
2323
  {
2113
2324
  className: cn(
@@ -2115,7 +2326,7 @@ function ReaGraph({ onCopyNode, extraToolbarContent }) {
2115
2326
  fullscreen ? "fixed inset-0 z-50 w-screen h-screen p-0 m-0" : "relative w-full h-full"
2116
2327
  ),
2117
2328
  children: [
2118
- /* @__PURE__ */ jsx6(Card2, { className: "h-full shadow-xs p-4", children: /* @__PURE__ */ jsx6(
2329
+ /* @__PURE__ */ jsx7(Card2, { className: "h-full shadow-xs p-4", children: /* @__PURE__ */ jsx7(
2119
2330
  GraphCanvas,
2120
2331
  {
2121
2332
  ref: canvas,
@@ -2140,21 +2351,22 @@ function ReaGraph({ onCopyNode, extraToolbarContent }) {
2140
2351
  elementId: data.id,
2141
2352
  elementType
2142
2353
  });
2143
- return /* @__PURE__ */ jsx6(
2354
+ return /* @__PURE__ */ jsx7(
2144
2355
  ReaContextMenu_default,
2145
2356
  {
2146
2357
  data,
2147
2358
  onClose,
2148
2359
  elementType,
2149
2360
  useGraphExploration,
2150
- onCopyNode
2361
+ onCopyNode,
2362
+ getNodeEdgeTypes
2151
2363
  }
2152
2364
  );
2153
2365
  }
2154
2366
  }
2155
2367
  ) }),
2156
- hoveredNode && /* @__PURE__ */ jsx6(NodeHoverInfo_default, { node: hoveredNode }),
2157
- /* @__PURE__ */ jsx6(
2368
+ hoveredNode && /* @__PURE__ */ jsx7(NodeHoverInfo_default, { node: hoveredNode }),
2369
+ /* @__PURE__ */ jsx7(
2158
2370
  LayoutAndCameraControls_default,
2159
2371
  {
2160
2372
  fitToScreen,
@@ -2166,7 +2378,7 @@ function ReaGraph({ onCopyNode, extraToolbarContent }) {
2166
2378
  isFocusMode,
2167
2379
  onResetView: () => {
2168
2380
  useGraphExploration.getState().resetView();
2169
- toast.success(
2381
+ toast2.success(
2170
2382
  isFocusMode ? "Focus mode cleared" : "Hidden elements restored"
2171
2383
  );
2172
2384
  },
@@ -2181,12 +2393,12 @@ var ReaGraph_default = ReaGraph;
2181
2393
 
2182
2394
  // src/components/cyto/CytoscapeGraph.tsx
2183
2395
  import {
2184
- useEffect as useEffect3,
2185
- useRef as useRef2,
2186
- useState as useState4,
2187
- useCallback as useCallback3
2396
+ useEffect as useEffect4,
2397
+ useRef as useRef3,
2398
+ useState as useState6,
2399
+ useCallback as useCallback4
2188
2400
  } from "react";
2189
- import { devLog as devLog10, errorLog as errorLog2 } from "@petrarca/sonnet-core";
2401
+ import { devLog as devLog10, errorLog as errorLog3 } from "@petrarca/sonnet-core";
2190
2402
  import cytoscape from "cytoscape";
2191
2403
  import fcose from "cytoscape-fcose";
2192
2404
  import cola from "cytoscape-cola";
@@ -2208,7 +2420,7 @@ import {
2208
2420
  SimpleTooltip as SimpleTooltip3
2209
2421
  } from "@petrarca/sonnet-ui";
2210
2422
  import { LayoutPanelTop as LayoutPanelTop2, Monitor, Frame, Eye as Eye2, Minus } from "lucide-react";
2211
- import { Fragment as Fragment2, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
2423
+ import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
2212
2424
  function ViewControls2({
2213
2425
  zoomIn,
2214
2426
  zoomOut,
@@ -2219,17 +2431,17 @@ function ViewControls2({
2219
2431
  isFocusMode,
2220
2432
  onResetView
2221
2433
  }) {
2222
- return /* @__PURE__ */ jsxs6(Fragment2, { children: [
2223
- /* @__PURE__ */ jsx7(SimpleTooltip3, { label: "Zoom In", side: "left", children: /* @__PURE__ */ jsx7(Button3, { variant: "ghost", size: "icon", onClick: zoomIn, children: /* @__PURE__ */ jsx7("span", { style: { fontWeight: 600 }, children: "+" }) }) }),
2224
- /* @__PURE__ */ jsx7(SimpleTooltip3, { label: "Zoom Out", side: "left", children: /* @__PURE__ */ jsx7(Button3, { variant: "ghost", size: "icon", onClick: zoomOut, children: /* @__PURE__ */ jsx7(Minus, {}) }) }),
2225
- /* @__PURE__ */ jsx7(SimpleTooltip3, { label: "Fullscreen", side: "left", children: /* @__PURE__ */ jsx7(Button3, { variant: "ghost", size: "icon", onClick: toggleFullscreen, children: !fullscreen ? /* @__PURE__ */ jsx7(Monitor, {}) : /* @__PURE__ */ jsx7(Frame, {}) }) }),
2226
- /* @__PURE__ */ jsx7(SimpleTooltip3, { label: "Fit to screen", side: "left", children: /* @__PURE__ */ jsx7(Button3, { variant: "ghost", size: "icon", onClick: fitToScreen, children: /* @__PURE__ */ jsx7(Frame, {}) }) }),
2227
- (hasHiddenElements || isFocusMode) && onResetView && /* @__PURE__ */ jsx7(
2434
+ return /* @__PURE__ */ jsxs7(Fragment2, { children: [
2435
+ /* @__PURE__ */ jsx8(SimpleTooltip3, { label: "Zoom In", side: "left", children: /* @__PURE__ */ jsx8(Button3, { variant: "ghost", size: "icon", onClick: zoomIn, children: /* @__PURE__ */ jsx8("span", { style: { fontWeight: 600 }, children: "+" }) }) }),
2436
+ /* @__PURE__ */ jsx8(SimpleTooltip3, { label: "Zoom Out", side: "left", children: /* @__PURE__ */ jsx8(Button3, { variant: "ghost", size: "icon", onClick: zoomOut, children: /* @__PURE__ */ jsx8(Minus, {}) }) }),
2437
+ /* @__PURE__ */ jsx8(SimpleTooltip3, { label: "Fullscreen", side: "left", children: /* @__PURE__ */ jsx8(Button3, { variant: "ghost", size: "icon", onClick: toggleFullscreen, children: !fullscreen ? /* @__PURE__ */ jsx8(Monitor, {}) : /* @__PURE__ */ jsx8(Frame, {}) }) }),
2438
+ /* @__PURE__ */ jsx8(SimpleTooltip3, { label: "Fit to screen", side: "left", children: /* @__PURE__ */ jsx8(Button3, { variant: "ghost", size: "icon", onClick: fitToScreen, children: /* @__PURE__ */ jsx8(Frame, {}) }) }),
2439
+ (hasHiddenElements || isFocusMode) && onResetView && /* @__PURE__ */ jsx8(
2228
2440
  SimpleTooltip3,
2229
2441
  {
2230
2442
  label: isFocusMode ? "Reset Focus" : "Show All Hidden",
2231
2443
  side: "left",
2232
- children: /* @__PURE__ */ jsx7(Button3, { variant: "ghost", size: "icon", onClick: onResetView, children: /* @__PURE__ */ jsx7(Eye2, {}) })
2444
+ children: /* @__PURE__ */ jsx8(Button3, { variant: "ghost", size: "icon", onClick: onResetView, children: /* @__PURE__ */ jsx8(Eye2, {}) })
2233
2445
  }
2234
2446
  )
2235
2447
  ] });
@@ -2264,12 +2476,12 @@ var LAYOUT_GROUPS = [
2264
2476
  }
2265
2477
  ];
2266
2478
  function LayoutDropdown2({ currentLayout, updateLayout }) {
2267
- return /* @__PURE__ */ jsxs6(DropdownMenu3, { children: [
2268
- /* @__PURE__ */ jsx7(SimpleTooltip3, { label: "Layout", side: "left", children: /* @__PURE__ */ jsx7(DropdownMenuTrigger3, { asChild: true, children: /* @__PURE__ */ jsx7(Button3, { variant: "ghost", size: "icon", children: /* @__PURE__ */ jsx7(LayoutPanelTop2, {}) }) }) }),
2269
- /* @__PURE__ */ jsx7(DropdownMenuContent3, { side: "left", align: "start", children: LAYOUT_GROUPS.map((group, groupIdx) => /* @__PURE__ */ jsxs6("div", { children: [
2270
- groupIdx > 0 && /* @__PURE__ */ jsx7(DropdownMenuSeparator2, {}),
2271
- /* @__PURE__ */ jsx7(DropdownMenuLabel2, { children: group.label }),
2272
- group.items.map((item) => /* @__PURE__ */ jsx7(
2479
+ return /* @__PURE__ */ jsxs7(DropdownMenu3, { children: [
2480
+ /* @__PURE__ */ jsx8(SimpleTooltip3, { label: "Layout", side: "left", children: /* @__PURE__ */ jsx8(DropdownMenuTrigger3, { asChild: true, children: /* @__PURE__ */ jsx8(Button3, { variant: "ghost", size: "icon", children: /* @__PURE__ */ jsx8(LayoutPanelTop2, {}) }) }) }),
2481
+ /* @__PURE__ */ jsx8(DropdownMenuContent3, { side: "left", align: "start", children: LAYOUT_GROUPS.map((group, groupIdx) => /* @__PURE__ */ jsxs7("div", { children: [
2482
+ groupIdx > 0 && /* @__PURE__ */ jsx8(DropdownMenuSeparator2, {}),
2483
+ /* @__PURE__ */ jsx8(DropdownMenuLabel2, { children: group.label }),
2484
+ group.items.map((item) => /* @__PURE__ */ jsx8(
2273
2485
  DropdownMenuItem3,
2274
2486
  {
2275
2487
  onClick: () => updateLayout(item.layout),
@@ -2294,9 +2506,9 @@ function CytoscapeControls({
2294
2506
  onResetView,
2295
2507
  extraToolbarContent
2296
2508
  }) {
2297
- return /* @__PURE__ */ jsxs6("div", { className: "absolute right-2 top-2 flex flex-col border bg-white rounded-md shadow-sm z-10", children: [
2298
- /* @__PURE__ */ jsx7(RendererDropdown, {}),
2299
- /* @__PURE__ */ jsx7(
2509
+ return /* @__PURE__ */ jsxs7("div", { className: "absolute right-2 top-2 flex flex-col border bg-white rounded-md shadow-sm z-10", children: [
2510
+ /* @__PURE__ */ jsx8(RendererDropdown, {}),
2511
+ /* @__PURE__ */ jsx8(
2300
2512
  ViewControls2,
2301
2513
  {
2302
2514
  zoomIn,
@@ -2310,7 +2522,7 @@ function CytoscapeControls({
2310
2522
  }
2311
2523
  ),
2312
2524
  extraToolbarContent,
2313
- /* @__PURE__ */ jsx7(
2525
+ /* @__PURE__ */ jsx8(
2314
2526
  LayoutDropdown2,
2315
2527
  {
2316
2528
  currentLayout,
@@ -2590,12 +2802,13 @@ function getLayoutPreset(options) {
2590
2802
  }
2591
2803
 
2592
2804
  // src/components/cyto/contextMenuConfig.ts
2593
- import { devLog as devLog9, errorLog } from "@petrarca/sonnet-core";
2594
- import { toast as toast2 } from "sonner";
2805
+ import { devLog as devLog9, errorLog as errorLog2 } from "@petrarca/sonnet-core";
2806
+ import { toast as toast3 } from "sonner";
2595
2807
  var createNodeContextMenu = ({
2596
2808
  cy,
2597
2809
  useGraphExploration,
2598
- onCopyNode
2810
+ onCopyNode,
2811
+ onExpandRequest
2599
2812
  }) => {
2600
2813
  devLog9("[CxtMenu] Initializing node context menu");
2601
2814
  return cy.cxtmenu({
@@ -2617,7 +2830,7 @@ var createNodeContextMenu = ({
2617
2830
  if (visualGraph) {
2618
2831
  onCopyNode?.(visualGraph);
2619
2832
  } else {
2620
- errorLog("[CxtMenu] visualGraph not found in element data");
2833
+ errorLog2("[CxtMenu] visualGraph not found in element data");
2621
2834
  }
2622
2835
  }
2623
2836
  },
@@ -2647,21 +2860,23 @@ var createNodeContextMenu = ({
2647
2860
  },
2648
2861
  {
2649
2862
  content: "Expand",
2650
- select: async (ele) => {
2863
+ select: (ele) => {
2651
2864
  const nodeId = ele.id();
2652
- devLog9("[CxtMenu] Expanding node:", nodeId);
2653
- try {
2654
- await useGraphExploration.getState().expandNode(nodeId);
2655
- devLog9("[CxtMenu] Expansion complete");
2656
- } catch (error) {
2657
- errorLog("[CxtMenu] Expansion failed:", error);
2658
- toast2.error("Failed to expand node");
2865
+ devLog9("[CxtMenu] Expand request for node:", nodeId);
2866
+ if (onExpandRequest) {
2867
+ const pos = ele.renderedPosition();
2868
+ const label = ele.data("label") || nodeId;
2869
+ onExpandRequest({ nodeId, nodeLabel: label, x: pos.x, y: pos.y });
2870
+ } else {
2871
+ useGraphExploration.getState().expandNode(nodeId).catch((error) => {
2872
+ errorLog2("[CxtMenu] Expansion failed:", error);
2873
+ toast3.error("Failed to expand node");
2874
+ });
2659
2875
  }
2660
2876
  }
2661
2877
  }
2662
2878
  ],
2663
2879
  menuRadius: 60,
2664
- // Increased for more items (default is 100)
2665
2880
  fillColor: "rgba(0, 0, 0, 0.75)",
2666
2881
  activeFillColor: "rgba(59, 130, 246, 0.75)",
2667
2882
  // blue-500
@@ -2746,8 +2961,49 @@ var createEdgeContextMenu = ({
2746
2961
  };
2747
2962
 
2748
2963
  // src/components/cyto/CytoscapeGraph.tsx
2749
- import { toast as toast3 } from "sonner";
2750
- import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
2964
+ import { toast as toast4 } from "sonner";
2965
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
2966
+ function CytoscapeExpandOverlay({
2967
+ expandRequest,
2968
+ containerRef,
2969
+ useGraphExploration,
2970
+ getNodeEdgeTypes,
2971
+ onClose
2972
+ }) {
2973
+ return /* @__PURE__ */ jsx9(
2974
+ "div",
2975
+ {
2976
+ className: "absolute z-50",
2977
+ style: {
2978
+ left: Math.min(
2979
+ Math.max(expandRequest.x, 120),
2980
+ (containerRef.current?.clientWidth ?? 800) - 120
2981
+ ),
2982
+ top: Math.min(
2983
+ Math.max(expandRequest.y, 10),
2984
+ (containerRef.current?.clientHeight ?? 600) - 260
2985
+ ),
2986
+ transform: "translate(-50%, 12px)"
2987
+ },
2988
+ children: /* @__PURE__ */ jsx9(
2989
+ ExpandOptionsPanel_default,
2990
+ {
2991
+ nodeLabel: expandRequest.nodeLabel,
2992
+ onExpand: (options) => {
2993
+ const { nodeId } = expandRequest;
2994
+ onClose();
2995
+ useGraphExploration.getState().expandNode(nodeId, options).catch((err) => {
2996
+ errorLog3("[CxtMenu] Expansion failed:", err);
2997
+ toast4.error("Failed to expand node");
2998
+ });
2999
+ },
3000
+ onBack: onClose,
3001
+ getEdgeTypes: getNodeEdgeTypes ? () => getNodeEdgeTypes(Number(expandRequest.nodeId)) : void 0
3002
+ }
3003
+ )
3004
+ }
3005
+ );
3006
+ }
2751
3007
  try {
2752
3008
  cytoscape.use(fcose);
2753
3009
  cytoscape.use(cola);
@@ -2825,12 +3081,12 @@ function applyLayout({
2825
3081
  try {
2826
3082
  cy.layout(layoutOpts).run();
2827
3083
  } catch (e) {
2828
- errorLog2("Layout error, falling back to dagre:", e);
3084
+ errorLog3("Layout error, falling back to dagre:", e);
2829
3085
  if (chosen === "klay") {
2830
3086
  try {
2831
3087
  cy.layout(getPreset("dagre")).run();
2832
3088
  } catch (inner) {
2833
- errorLog2("Fallback layout also failed, using grid:", inner);
3089
+ errorLog3("Fallback layout also failed, using grid:", inner);
2834
3090
  cy.layout(getPreset("grid")).run();
2835
3091
  }
2836
3092
  } else {
@@ -3037,8 +3293,8 @@ function computeGraphViewState({
3037
3293
  };
3038
3294
  }
3039
3295
  function useFullscreen(fitToScreen, cyRef) {
3040
- const [fullscreen, setFullscreen] = useState4(false);
3041
- useEffect3(() => {
3296
+ const [fullscreen, setFullscreen] = useState6(false);
3297
+ useEffect4(() => {
3042
3298
  if (fullscreen) {
3043
3299
  const prevOverflow = document.body.style.overflow;
3044
3300
  document.body.style.overflow = "hidden";
@@ -3053,7 +3309,7 @@ function useFullscreen(fitToScreen, cyRef) {
3053
3309
  };
3054
3310
  }
3055
3311
  }, [fullscreen, fitToScreen]);
3056
- useEffect3(() => {
3312
+ useEffect4(() => {
3057
3313
  if (!fullscreen) return;
3058
3314
  const id = setTimeout(() => {
3059
3315
  if (cyRef.current) {
@@ -3066,7 +3322,7 @@ function useFullscreen(fitToScreen, cyRef) {
3066
3322
  return {
3067
3323
  fullscreen,
3068
3324
  setFullscreen,
3069
- toggleFullscreen: useCallback3(() => setFullscreen((f) => !f), [])
3325
+ toggleFullscreen: useCallback4(() => setFullscreen((f) => !f), [])
3070
3326
  };
3071
3327
  }
3072
3328
  function setupCytoscapeEventHandlers({
@@ -3078,7 +3334,8 @@ function setupCytoscapeEventHandlers({
3078
3334
  setSelectedEdgeStore,
3079
3335
  clearSelectionStore,
3080
3336
  useGraphExploration,
3081
- onCopyNode
3337
+ onCopyNode,
3338
+ onExpandRequest
3082
3339
  }) {
3083
3340
  const updateSelectedNodeIds = (nodeIds) => {
3084
3341
  setSelectedNodeIdsStore(nodeIds);
@@ -3166,22 +3423,31 @@ function setupCytoscapeEventHandlers({
3166
3423
  cy.on("tap", (evt) => {
3167
3424
  if (evt.target === cy) clearAll();
3168
3425
  });
3169
- createNodeContextMenu({ cy, useGraphExploration, onCopyNode });
3426
+ createNodeContextMenu({
3427
+ cy,
3428
+ useGraphExploration,
3429
+ onCopyNode,
3430
+ onExpandRequest
3431
+ });
3170
3432
  createEdgeContextMenu({ cy, useGraphExploration });
3171
3433
  }
3172
3434
  function CytoscapeGraph({
3173
3435
  layout: initialLayout = "auto",
3174
3436
  fitPadding = 30,
3175
3437
  onCopyNode,
3176
- extraToolbarContent
3438
+ extraToolbarContent,
3439
+ getNodeEdgeTypes
3177
3440
  }) {
3178
- const containerRef = useRef2(null);
3179
- const cyRef = useRef2(null);
3180
- const isInitializedRef = useRef2(false);
3181
- const [currentLayout, setCurrentLayout] = useState4(initialLayout);
3182
- const nodesRef = useRef2([]);
3183
- const edgesRef = useRef2([]);
3184
- const pendingFocusActionRef = useRef2(null);
3441
+ const containerRef = useRef3(null);
3442
+ const cyRef = useRef3(null);
3443
+ const isInitializedRef = useRef3(false);
3444
+ const [currentLayout, setCurrentLayout] = useState6(initialLayout);
3445
+ const [expandRequest, setExpandRequest] = useState6(
3446
+ null
3447
+ );
3448
+ const nodesRef = useRef3([]);
3449
+ const edgesRef = useRef3([]);
3450
+ const pendingFocusActionRef = useRef3(null);
3185
3451
  const cytoData = useCytoscapeData();
3186
3452
  const live = useLiveGraphData();
3187
3453
  const nodes = live.nodes;
@@ -3201,13 +3467,13 @@ function CytoscapeGraph({
3201
3467
  setSelectedEdge: setSelectedEdgeStore,
3202
3468
  clear: clearSelectionStore
3203
3469
  } = useSelection3.getState();
3204
- const selectedNodeIdsRef = useRef2(
3470
+ const selectedNodeIdsRef = useRef3(
3205
3471
  useSelection3.getState().selectedNodeIds
3206
3472
  );
3207
- const selectedEdgeRef = useRef2(
3473
+ const selectedEdgeRef = useRef3(
3208
3474
  useSelection3.getState().selectedEdge
3209
3475
  );
3210
- useEffect3(() => {
3476
+ useEffect4(() => {
3211
3477
  const state = useSelection3.getState();
3212
3478
  selectedNodeIdsRef.current = state.selectedNodeIds;
3213
3479
  selectedEdgeRef.current = state.selectedEdge;
@@ -3227,13 +3493,13 @@ function CytoscapeGraph({
3227
3493
  });
3228
3494
  }
3229
3495
  }, [useSelection3]);
3230
- useEffect3(() => {
3496
+ useEffect4(() => {
3231
3497
  nodesRef.current = nodes;
3232
3498
  }, [nodes]);
3233
- useEffect3(() => {
3499
+ useEffect4(() => {
3234
3500
  edgesRef.current = edges;
3235
3501
  }, [edges]);
3236
- useEffect3(() => {
3502
+ useEffect4(() => {
3237
3503
  if (!containerRef.current || cyRef.current) return;
3238
3504
  isInitializedRef.current = true;
3239
3505
  cyRef.current = cytoscape({
@@ -3384,7 +3650,8 @@ function CytoscapeGraph({
3384
3650
  setSelectedEdgeStore,
3385
3651
  clearSelectionStore,
3386
3652
  useGraphExploration,
3387
- onCopyNode
3653
+ onCopyNode,
3654
+ onExpandRequest: setExpandRequest
3388
3655
  });
3389
3656
  }
3390
3657
  return () => {
@@ -3392,10 +3659,10 @@ function CytoscapeGraph({
3392
3659
  cyRef.current = null;
3393
3660
  };
3394
3661
  }, []);
3395
- const elementIdsRef = useRef2("");
3396
- const lastLayoutRef = useRef2("");
3397
- const lastFitPaddingRef = useRef2(fitPadding);
3398
- useEffect3(() => {
3662
+ const elementIdsRef = useRef3("");
3663
+ const lastLayoutRef = useRef3("");
3664
+ const lastFitPaddingRef = useRef3(fitPadding);
3665
+ useEffect4(() => {
3399
3666
  const cy = cyRef.current;
3400
3667
  if (!cy || !isInitializedRef.current) return;
3401
3668
  const currentIds = nodes.map((n) => n.id_).sort().join(",") + "|" + edges.map((e) => e.id_).sort().join(",");
@@ -3468,12 +3735,12 @@ function CytoscapeGraph({
3468
3735
  });
3469
3736
  }
3470
3737
  }, [nodes, edges, currentLayout, fitPadding]);
3471
- useEffect3(() => {
3738
+ useEffect4(() => {
3472
3739
  if (!cyRef.current) return;
3473
3740
  cyRef.current.elements().unselect();
3474
3741
  clearSelectionStore();
3475
3742
  }, [lastDataChangeTime, clearSelectionStore]);
3476
- useEffect3(() => {
3743
+ useEffect4(() => {
3477
3744
  const unsubscribe = GraphFocus_default.subscribe((state) => {
3478
3745
  handleFocusStateChange({
3479
3746
  focusAction: state.focusAction,
@@ -3492,15 +3759,15 @@ function CytoscapeGraph({
3492
3759
  const updateLayout = (newLayout) => {
3493
3760
  setCurrentLayout(newLayout);
3494
3761
  };
3495
- const fitToScreen = useCallback3(() => {
3762
+ const fitToScreen = useCallback4(() => {
3496
3763
  if (cyRef.current) {
3497
3764
  cyRef.current.fit(void 0, fitPadding);
3498
3765
  }
3499
3766
  }, [fitPadding]);
3500
- useEffect3(() => {
3767
+ useEffect4(() => {
3501
3768
  fitToScreen();
3502
3769
  }, [nodes.length, fitToScreen]);
3503
- useEffect3(() => {
3770
+ useEffect4(() => {
3504
3771
  if (!isInitializedRef.current) return;
3505
3772
  const handleResize = () => cyRef.current?.resize();
3506
3773
  window.addEventListener("resize", handleResize);
@@ -3519,18 +3786,28 @@ function CytoscapeGraph({
3519
3786
  isFocusMode,
3520
3787
  fullscreen
3521
3788
  });
3522
- return /* @__PURE__ */ jsx8("div", { className: outerClass, children: /* @__PURE__ */ jsxs7("div", { className: innerClass, children: [
3523
- /* @__PURE__ */ jsx8("div", { ref: containerRef, className: "absolute inset-0" }),
3524
- showEmptyMessage && /* @__PURE__ */ jsx8("div", { className: "absolute inset-0 flex items-center justify-center text-xs text-gray-500 pointer-events-none select-none", children: "No graph data to visualize (run a query or check the filters)" }),
3525
- showFilteredMessage && /* @__PURE__ */ jsx8("div", { className: "absolute inset-0 flex items-center justify-center text-xs text-gray-500 pointer-events-none select-none", children: 'All elements are hidden or filtered (click "Reset Focus" to restore)' }),
3526
- showControls && /* @__PURE__ */ jsx8(
3789
+ return /* @__PURE__ */ jsx9("div", { className: outerClass, children: /* @__PURE__ */ jsxs8("div", { className: innerClass, children: [
3790
+ /* @__PURE__ */ jsx9("div", { ref: containerRef, className: "absolute inset-0" }),
3791
+ expandRequest && /* @__PURE__ */ jsx9(
3792
+ CytoscapeExpandOverlay,
3793
+ {
3794
+ expandRequest,
3795
+ containerRef,
3796
+ useGraphExploration,
3797
+ getNodeEdgeTypes,
3798
+ onClose: () => setExpandRequest(null)
3799
+ }
3800
+ ),
3801
+ showEmptyMessage && /* @__PURE__ */ jsx9("div", { className: "absolute inset-0 flex items-center justify-center text-xs text-gray-500 pointer-events-none select-none", children: "No graph data to visualize (run a query or check the filters)" }),
3802
+ showFilteredMessage && /* @__PURE__ */ jsx9("div", { className: "absolute inset-0 flex items-center justify-center text-xs text-gray-500 pointer-events-none select-none", children: 'All elements are hidden or filtered (click "Reset Focus" to restore)' }),
3803
+ showControls && /* @__PURE__ */ jsx9(
3527
3804
  CytoscapeControls_default,
3528
3805
  {
3529
3806
  updateLayout,
3530
3807
  fitToScreen,
3531
3808
  onResetView: () => {
3532
3809
  resetView();
3533
- toast3.success(
3810
+ toast4.success(
3534
3811
  isFocusMode ? "Focus mode cleared" : "Hidden elements restored"
3535
3812
  );
3536
3813
  },
@@ -3571,26 +3848,29 @@ function CytoscapeGraph({
3571
3848
  var CytoscapeGraph_default = CytoscapeGraph;
3572
3849
 
3573
3850
  // src/components/GraphVisualizer.tsx
3574
- import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
3851
+ import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
3575
3852
  function GraphVisualizer({
3576
3853
  onCopyNode,
3577
- extraToolbarContent
3854
+ extraToolbarContent,
3855
+ getNodeEdgeTypes
3578
3856
  }) {
3579
3857
  const { renderer } = useGraphRenderer();
3580
- return /* @__PURE__ */ jsx9("div", { className: "relative w-full h-full flex flex-col overflow-hidden", children: /* @__PURE__ */ jsxs8("div", { className: "flex-1 h-full", children: [
3581
- renderer === "reagraph" && /* @__PURE__ */ jsx9(
3858
+ return /* @__PURE__ */ jsx10("div", { className: "relative w-full h-full flex flex-col overflow-hidden", children: /* @__PURE__ */ jsxs9("div", { className: "flex-1 h-full", children: [
3859
+ renderer === "reagraph" && /* @__PURE__ */ jsx10(
3582
3860
  ReaGraph_default,
3583
3861
  {
3584
3862
  onCopyNode,
3585
- extraToolbarContent
3863
+ extraToolbarContent,
3864
+ getNodeEdgeTypes
3586
3865
  },
3587
3866
  "reagraph"
3588
3867
  ),
3589
- renderer === "cytoscape" && /* @__PURE__ */ jsx9(
3868
+ renderer === "cytoscape" && /* @__PURE__ */ jsx10(
3590
3869
  CytoscapeGraph_default,
3591
3870
  {
3592
3871
  onCopyNode,
3593
- extraToolbarContent
3872
+ extraToolbarContent,
3873
+ getNodeEdgeTypes
3594
3874
  },
3595
3875
  "cytoscape"
3596
3876
  )
@@ -3602,29 +3882,29 @@ var GraphVisualizer_default = GraphVisualizer;
3602
3882
  import { Card as Card5 } from "@petrarca/sonnet-ui";
3603
3883
 
3604
3884
  // src/components/ActionPanel/NodeInfoCard.tsx
3605
- import { useState as useState5 } from "react";
3885
+ import { useState as useState7 } from "react";
3606
3886
  import {
3607
3887
  Card as Card3,
3608
3888
  Collapsible,
3609
3889
  CollapsibleContent,
3610
- CollapsibleTrigger
3890
+ CollapsibleTrigger,
3891
+ PropertyList
3611
3892
  } from "@petrarca/sonnet-ui";
3612
3893
  import { copyTextToClipboard } from "@petrarca/sonnet-core";
3613
- import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
3894
+ import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
3614
3895
  function NodeInfoCard({ node }) {
3615
- const [showProperties, setShowProperties] = useState5(false);
3616
- return /* @__PURE__ */ jsxs9(
3896
+ const [showProperties, setShowProperties] = useState7(false);
3897
+ return /* @__PURE__ */ jsxs10(
3617
3898
  Card3,
3618
3899
  {
3619
- className: "w-full rounded-lg overflow-hidden bg-gray-100 border p-4 shadow-xs",
3900
+ className: "w-full rounded-lg overflow-hidden bg-muted/30 border shadow-none p-3",
3620
3901
  style: {
3621
- background: "#f3f4f6",
3622
3902
  borderLeft: `4px solid ${node.visual.color}`
3623
3903
  },
3624
3904
  children: [
3625
- /* @__PURE__ */ jsx10("p", { className: "leading-tight font-semibold mb-0", children: node.label_ }),
3626
- /* @__PURE__ */ jsx10("p", { className: "leading-tight mb-1", children: node.visual.displayName }),
3627
- /* @__PURE__ */ jsx10(
3905
+ /* @__PURE__ */ jsx11("p", { className: "text-sm leading-tight font-medium mb-0", children: node.label_ }),
3906
+ /* @__PURE__ */ jsx11("p", { className: "text-sm leading-tight mb-1", children: node.visual.displayName }),
3907
+ /* @__PURE__ */ jsx11(
3628
3908
  "p",
3629
3909
  {
3630
3910
  className: "text-xs text-muted-foreground cursor-pointer select-all mb-2",
@@ -3633,23 +3913,9 @@ function NodeInfoCard({ node }) {
3633
3913
  children: node.id_
3634
3914
  }
3635
3915
  ),
3636
- /* @__PURE__ */ jsxs9(Collapsible, { open: showProperties, onOpenChange: setShowProperties, children: [
3637
- /* @__PURE__ */ jsx10(CollapsibleTrigger, { asChild: true, children: /* @__PURE__ */ jsx10("button", { className: "text-xs cursor-pointer", children: showProperties ? "Hide" : "Show more" }) }),
3638
- /* @__PURE__ */ jsx10(CollapsibleContent, { children: /* @__PURE__ */ jsxs9("ul", { className: "list-none mt-2", children: [
3639
- /* @__PURE__ */ jsx10("p", { className: "text-sm font-semibold mb-1 tracking-wide", children: "Properties" }),
3640
- Object.entries(node.properties_ || {}).map(([key, value]) => /* @__PURE__ */ jsxs9("li", { className: "mb-2 leading-tight", children: [
3641
- /* @__PURE__ */ jsx10("p", { className: "text-xs text-slate-600 uppercase tracking-wide", children: key }),
3642
- /* @__PURE__ */ jsx10(
3643
- "p",
3644
- {
3645
- className: "text-sm truncate max-w-[190px] cursor-pointer",
3646
- onClick: () => copyTextToClipboard(String(value)),
3647
- title: "Click to copy value",
3648
- children: String(value)
3649
- }
3650
- )
3651
- ] }, key))
3652
- ] }) })
3916
+ /* @__PURE__ */ jsxs10(Collapsible, { open: showProperties, onOpenChange: setShowProperties, children: [
3917
+ /* @__PURE__ */ jsx11(CollapsibleTrigger, { asChild: true, children: /* @__PURE__ */ jsx11("button", { className: "text-xs cursor-pointer", children: showProperties ? "Hide" : "Show more" }) }),
3918
+ /* @__PURE__ */ jsx11(CollapsibleContent, { children: /* @__PURE__ */ jsx11("div", { className: "mt-2", children: /* @__PURE__ */ jsx11(PropertyList, { properties: node.properties_ || {}, showHeader: true }) }) })
3653
3919
  ] })
3654
3920
  ]
3655
3921
  }
@@ -3658,32 +3924,32 @@ function NodeInfoCard({ node }) {
3658
3924
  var NodeInfoCard_default = NodeInfoCard;
3659
3925
 
3660
3926
  // src/components/ActionPanel/EdgeInfoCard.tsx
3661
- import { useState as useState6 } from "react";
3927
+ import { useState as useState8 } from "react";
3662
3928
  import {
3663
3929
  Card as Card4,
3664
3930
  Collapsible as Collapsible2,
3665
3931
  CollapsibleContent as CollapsibleContent2,
3666
- CollapsibleTrigger as CollapsibleTrigger2
3932
+ CollapsibleTrigger as CollapsibleTrigger2,
3933
+ PropertyList as PropertyList2
3667
3934
  } from "@petrarca/sonnet-ui";
3668
3935
  import { ArrowDown } from "lucide-react";
3669
3936
  import { copyTextToClipboard as copyTextToClipboard2 } from "@petrarca/sonnet-core";
3670
- import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
3937
+ import { jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
3671
3938
  function EdgeInfoCard({ edge }) {
3672
- const [showProperties, setShowProperties] = useState6(false);
3673
- return /* @__PURE__ */ jsxs10(
3939
+ const [showProperties, setShowProperties] = useState8(false);
3940
+ return /* @__PURE__ */ jsxs11(
3674
3941
  Card4,
3675
3942
  {
3676
- className: "w-full rounded-lg overflow-hidden bg-gray-100 border p-4 shadow-xs",
3943
+ className: "w-full rounded-lg overflow-hidden bg-muted/30 border shadow-none p-3",
3677
3944
  style: {
3678
- background: "#f3f4f6",
3679
3945
  borderLeft: `4px solid ${edge.visual.color}`
3680
3946
  },
3681
3947
  children: [
3682
- /* @__PURE__ */ jsxs10("p", { className: "leading-tight mb-1", children: [
3683
- /* @__PURE__ */ jsx11(ArrowDown, { className: "inline-block mr-1 opacity-70 align-middle" }),
3684
- /* @__PURE__ */ jsx11("strong", { className: "font-semibold align-middle", children: edge.label_ })
3948
+ /* @__PURE__ */ jsxs11("p", { className: "text-sm leading-tight mb-1", children: [
3949
+ /* @__PURE__ */ jsx12(ArrowDown, { className: "inline-block mr-1 opacity-70 align-middle h-3.5 w-3.5" }),
3950
+ /* @__PURE__ */ jsx12("strong", { className: "font-medium align-middle", children: edge.label_ })
3685
3951
  ] }),
3686
- edge.id_ && /* @__PURE__ */ jsx11(
3952
+ edge.id_ && /* @__PURE__ */ jsx12(
3687
3953
  "p",
3688
3954
  {
3689
3955
  className: "text-xs text-muted-foreground select-all cursor-pointer mt-1 mb-2",
@@ -3692,23 +3958,9 @@ function EdgeInfoCard({ edge }) {
3692
3958
  children: edge.id_
3693
3959
  }
3694
3960
  ),
3695
- edge.properties_ && Object.keys(edge.properties_).length > 0 && /* @__PURE__ */ jsxs10(Collapsible2, { open: showProperties, onOpenChange: setShowProperties, children: [
3696
- /* @__PURE__ */ jsx11(CollapsibleTrigger2, { asChild: true, children: /* @__PURE__ */ jsx11("button", { className: "text-xs cursor-pointer", children: showProperties ? "Hide" : "Show more" }) }),
3697
- /* @__PURE__ */ jsx11(CollapsibleContent2, { children: /* @__PURE__ */ jsxs10("ul", { className: "list-none mt-2", children: [
3698
- /* @__PURE__ */ jsx11("p", { className: "text-sm font-semibold mb-1 tracking-wide", children: "Properties" }),
3699
- Object.entries(edge.properties_ || {}).map(([key, value]) => /* @__PURE__ */ jsxs10("li", { className: "mb-2 leading-tight", children: [
3700
- /* @__PURE__ */ jsx11("p", { className: "text-xs text-slate-600 uppercase tracking-wide", children: key }),
3701
- /* @__PURE__ */ jsx11(
3702
- "p",
3703
- {
3704
- className: "text-sm truncate max-w-[190px] cursor-pointer",
3705
- onClick: () => copyTextToClipboard2(String(value)),
3706
- title: "Click to copy value",
3707
- children: String(value)
3708
- }
3709
- )
3710
- ] }, key))
3711
- ] }) })
3961
+ edge.properties_ && Object.keys(edge.properties_).length > 0 && /* @__PURE__ */ jsxs11(Collapsible2, { open: showProperties, onOpenChange: setShowProperties, children: [
3962
+ /* @__PURE__ */ jsx12(CollapsibleTrigger2, { asChild: true, children: /* @__PURE__ */ jsx12("button", { className: "text-xs cursor-pointer", children: showProperties ? "Hide" : "Show more" }) }),
3963
+ /* @__PURE__ */ jsx12(CollapsibleContent2, { children: /* @__PURE__ */ jsx12("div", { className: "mt-2", children: /* @__PURE__ */ jsx12(PropertyList2, { properties: edge.properties_ || {}, showHeader: true }) }) })
3712
3964
  ] })
3713
3965
  ]
3714
3966
  }
@@ -3720,7 +3972,7 @@ var EdgeInfoCard_default = EdgeInfoCard;
3720
3972
  import { Badge } from "@petrarca/sonnet-ui";
3721
3973
  import { formatNumber, devLog as devLog11 } from "@petrarca/sonnet-core";
3722
3974
  import { useMemo as useMemo4 } from "react";
3723
- import { Fragment as Fragment3, jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
3975
+ import { Fragment as Fragment3, jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
3724
3976
  function computeEdgeToNodeLabels(nodesById, edgesById) {
3725
3977
  const mapping = {};
3726
3978
  for (const edge of Object.values(edgesById)) {
@@ -3753,10 +4005,10 @@ function AllToggleBadge({
3753
4005
  badgeKey
3754
4006
  }) {
3755
4007
  const bgColor = allDisabled ? colorPalette["faded"] : colorPalette["neutral"];
3756
- return /* @__PURE__ */ jsxs11(
4008
+ return /* @__PURE__ */ jsxs12(
3757
4009
  Badge,
3758
4010
  {
3759
- className: "cursor-pointer text-sm px-3 py-1",
4011
+ className: "cursor-pointer text-xs px-2.5 py-0.5",
3760
4012
  style: {
3761
4013
  textTransform: "none",
3762
4014
  backgroundColor: bgColor,
@@ -3788,10 +4040,10 @@ function NodeLabelBadges({
3788
4040
  colorPalette,
3789
4041
  label
3790
4042
  );
3791
- return /* @__PURE__ */ jsxs11(
4043
+ return /* @__PURE__ */ jsxs12(
3792
4044
  Badge,
3793
4045
  {
3794
- className: "cursor-pointer text-sm px-3 py-1",
4046
+ className: "cursor-pointer text-xs px-2.5 py-0.5",
3795
4047
  style: {
3796
4048
  textTransform: "none",
3797
4049
  backgroundColor: bgColor,
@@ -3826,10 +4078,10 @@ function EdgeLabelBadges({
3826
4078
  colorPalette,
3827
4079
  "neutral"
3828
4080
  );
3829
- return /* @__PURE__ */ jsxs11(
4081
+ return /* @__PURE__ */ jsxs12(
3830
4082
  Badge,
3831
4083
  {
3832
- className: "cursor-pointer text-sm px-3 py-1",
4084
+ className: "cursor-pointer text-xs px-2.5 py-0.5",
3833
4085
  style: {
3834
4086
  textTransform: "none",
3835
4087
  backgroundColor: bgColor,
@@ -3903,12 +4155,12 @@ function GraphOverview({
3903
4155
  toggleAllEdgeLabelsDisabled();
3904
4156
  }
3905
4157
  };
3906
- return /* @__PURE__ */ jsxs11(Fragment3, { children: [
3907
- /* @__PURE__ */ jsx12("p", { className: "mt-2 text-sm font-semibold", children: "Overview" }),
3908
- allLabels.length > 0 && /* @__PURE__ */ jsxs11("div", { children: [
3909
- /* @__PURE__ */ jsx12("p", { className: "text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2", children: "Node Labels" }),
3910
- /* @__PURE__ */ jsxs11("div", { className: "flex flex-col items-start gap-2", children: [
3911
- /* @__PURE__ */ jsx12(
4158
+ return /* @__PURE__ */ jsxs12(Fragment3, { children: [
4159
+ /* @__PURE__ */ jsx13("p", { className: "mt-2 text-sm font-semibold", children: "Overview" }),
4160
+ allLabels.length > 0 && /* @__PURE__ */ jsxs12("div", { children: [
4161
+ /* @__PURE__ */ jsx13("p", { className: "text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2", children: "Node Labels" }),
4162
+ /* @__PURE__ */ jsxs12("div", { className: "flex flex-col items-start gap-2", children: [
4163
+ /* @__PURE__ */ jsx13(
3912
4164
  AllToggleBadge,
3913
4165
  {
3914
4166
  allDisabled: allLabelsDisabled,
@@ -3918,7 +4170,7 @@ function GraphOverview({
3918
4170
  badgeKey: "all"
3919
4171
  }
3920
4172
  ),
3921
- /* @__PURE__ */ jsx12(
4173
+ /* @__PURE__ */ jsx13(
3922
4174
  NodeLabelBadges,
3923
4175
  {
3924
4176
  labels: allLabels,
@@ -3931,10 +4183,10 @@ function GraphOverview({
3931
4183
  )
3932
4184
  ] })
3933
4185
  ] }),
3934
- allEdgeLabels.length > 0 && /* @__PURE__ */ jsxs11("div", { children: [
3935
- /* @__PURE__ */ jsx12("p", { className: "text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2", children: "Edge Types" }),
3936
- /* @__PURE__ */ jsxs11("div", { className: "flex flex-col items-start gap-2", children: [
3937
- /* @__PURE__ */ jsx12(
4186
+ allEdgeLabels.length > 0 && /* @__PURE__ */ jsxs12("div", { children: [
4187
+ /* @__PURE__ */ jsx13("p", { className: "text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2", children: "Edge Types" }),
4188
+ /* @__PURE__ */ jsxs12("div", { className: "flex flex-col items-start gap-2", children: [
4189
+ /* @__PURE__ */ jsx13(
3938
4190
  AllToggleBadge,
3939
4191
  {
3940
4192
  allDisabled: allEdgeLabelsDisabled,
@@ -3944,7 +4196,7 @@ function GraphOverview({
3944
4196
  badgeKey: "edge-all"
3945
4197
  }
3946
4198
  ),
3947
- /* @__PURE__ */ jsx12(
4199
+ /* @__PURE__ */ jsx13(
3948
4200
  EdgeLabelBadges,
3949
4201
  {
3950
4202
  labels: allEdgeLabels,
@@ -3970,16 +4222,16 @@ function Overview() {
3970
4222
  );
3971
4223
  }, [baseNormalized]);
3972
4224
  if (stats && stats.values != null && stats.values > 0) {
3973
- return /* @__PURE__ */ jsxs11(Fragment3, { children: [
3974
- /* @__PURE__ */ jsx12("p", { className: "mt-2 text-sm font-semibold", children: "Overview" }),
3975
- /* @__PURE__ */ jsxs11("p", { className: "text-sm text-slate-700", children: [
4225
+ return /* @__PURE__ */ jsxs12(Fragment3, { children: [
4226
+ /* @__PURE__ */ jsx13("p", { className: "mt-2 text-sm font-semibold", children: "Overview" }),
4227
+ /* @__PURE__ */ jsxs12("p", { className: "text-sm text-muted-foreground", children: [
3976
4228
  formatNumber(stats.values),
3977
4229
  " Values"
3978
4230
  ] })
3979
4231
  ] });
3980
4232
  }
3981
4233
  if (stats && stats.nodes > 0 && colorPalette) {
3982
- return /* @__PURE__ */ jsx12(
4234
+ return /* @__PURE__ */ jsx13(
3983
4235
  GraphOverview,
3984
4236
  {
3985
4237
  stats,
@@ -3996,7 +4248,7 @@ var Overview_default = Overview;
3996
4248
 
3997
4249
  // src/components/ActionPanel/ActionPanel.tsx
3998
4250
  import { useMemo as useMemo5 } from "react";
3999
- import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
4251
+ import { jsx as jsx14, jsxs as jsxs13 } from "react/jsx-runtime";
4000
4252
  function ActionPanel({ actions }) {
4001
4253
  const useSelection3 = useWorkspaceSelection();
4002
4254
  const selectedNodeIds = useSelection3(
@@ -4012,28 +4264,139 @@ function ActionPanel({ actions }) {
4012
4264
  const secondNode = selectedNodes?.[1];
4013
4265
  const hasEdge = !!selectedEdge;
4014
4266
  const showOverview = !firstNode && !hasEdge;
4015
- return /* @__PURE__ */ jsxs12(
4267
+ return /* @__PURE__ */ jsxs13(
4016
4268
  Card5,
4017
4269
  {
4018
- className: "h-full min-w-64 flex flex-col bg-gray-50 shadow-xs p-4",
4270
+ className: "h-full min-w-64 flex flex-col bg-card shadow-none p-4",
4019
4271
  style: { maxHeight: "100%", overflow: "hidden" },
4020
4272
  children: [
4021
- actions && /* @__PURE__ */ jsx13("div", { className: "flex-shrink-0 mb-2", children: actions }),
4022
- /* @__PURE__ */ jsx13("div", { className: "flex-1 overflow-auto pr-1", children: /* @__PURE__ */ jsxs12("div", { className: "flex flex-col gap-4", children: [
4023
- firstNode && /* @__PURE__ */ jsx13(NodeInfoCard_default, { node: firstNode }),
4024
- hasEdge && selectedEdge && /* @__PURE__ */ jsx13(EdgeInfoCard_default, { edge: selectedEdge }),
4025
- secondNode && /* @__PURE__ */ jsx13(NodeInfoCard_default, { node: secondNode }),
4026
- showOverview && /* @__PURE__ */ jsx13(Overview_default, {})
4273
+ actions && /* @__PURE__ */ jsx14("div", { className: "flex-shrink-0 mb-2", children: actions }),
4274
+ /* @__PURE__ */ jsx14("div", { className: "flex-1 overflow-auto pr-1", children: /* @__PURE__ */ jsxs13("div", { className: "flex flex-col gap-3", children: [
4275
+ firstNode && /* @__PURE__ */ jsx14(NodeInfoCard_default, { node: firstNode }),
4276
+ hasEdge && selectedEdge && /* @__PURE__ */ jsx14(EdgeInfoCard_default, { edge: selectedEdge }),
4277
+ secondNode && /* @__PURE__ */ jsx14(NodeInfoCard_default, { node: secondNode }),
4278
+ showOverview && /* @__PURE__ */ jsx14(Overview_default, {})
4027
4279
  ] }) })
4028
4280
  ]
4029
4281
  }
4030
4282
  );
4031
4283
  }
4032
4284
  var ActionPanel_default = ActionPanel;
4285
+
4286
+ // src/components/GraphA11yList/GraphA11yList.tsx
4287
+ import { useMemo as useMemo6, useRef as useRef4 } from "react";
4288
+ import { useVirtualizer } from "@tanstack/react-virtual";
4289
+ import { cn as cn2 } from "@petrarca/sonnet-core";
4290
+ import { jsx as jsx15 } from "react/jsx-runtime";
4291
+ var SR_ONLY = "absolute h-px w-px overflow-hidden whitespace-nowrap border-0 p-0 [clip:rect(0,0,0,0)] [clip-path:inset(50%)] -m-px";
4292
+ function GraphA11yList({
4293
+ visible = false,
4294
+ rowHeight = 28,
4295
+ maxHeight = 320,
4296
+ className
4297
+ }) {
4298
+ const live = useLiveGraphData();
4299
+ const useSelection3 = useWorkspaceSelection();
4300
+ const selectedNodeIds = useSelection3(
4301
+ (s) => s.selectedNodeIds
4302
+ );
4303
+ const selectedEdge = useSelection3((s) => s.selectedEdge);
4304
+ const setSelectedNodeIds = useSelection3(
4305
+ (s) => s.setSelectedNodeIds
4306
+ );
4307
+ const setSelectedEdge = useSelection3(
4308
+ (s) => s.setSelectedEdge
4309
+ );
4310
+ const rows = useMemo6(() => {
4311
+ const nodeRows = live.nodes.map((n) => ({
4312
+ kind: "node",
4313
+ id: String(n.id_),
4314
+ label: n.visual?.displayName || n.label_ || String(n.id_),
4315
+ node: n
4316
+ }));
4317
+ const edgeRows = live.edges.map((e) => ({
4318
+ kind: "edge",
4319
+ id: String(e.id_),
4320
+ label: e.label_ || String(e.id_),
4321
+ edge: e
4322
+ }));
4323
+ return [...nodeRows, ...edgeRows];
4324
+ }, [live.nodes, live.edges]);
4325
+ const parentRef = useRef4(null);
4326
+ const virtualizer = useVirtualizer({
4327
+ count: rows.length,
4328
+ getScrollElement: () => parentRef.current,
4329
+ estimateSize: () => rowHeight,
4330
+ overscan: 8
4331
+ });
4332
+ const selectRow = (row) => {
4333
+ if (row.kind === "node") {
4334
+ setSelectedEdge(null);
4335
+ setSelectedNodeIds([row.id]);
4336
+ } else {
4337
+ setSelectedNodeIds([]);
4338
+ setSelectedEdge(row.edge);
4339
+ }
4340
+ };
4341
+ const isSelected = (row) => row.kind === "node" ? selectedNodeIds.includes(row.id) : selectedEdge?.id_ === row.edge.id_;
4342
+ return /* @__PURE__ */ jsx15(
4343
+ "div",
4344
+ {
4345
+ role: "listbox",
4346
+ "aria-label": "Graph elements",
4347
+ className: cn2(visible ? "rounded-md border" : SR_ONLY, className),
4348
+ children: /* @__PURE__ */ jsx15(
4349
+ "div",
4350
+ {
4351
+ ref: parentRef,
4352
+ style: { maxHeight: visible ? maxHeight : void 0, overflow: "auto" },
4353
+ children: /* @__PURE__ */ jsx15(
4354
+ "div",
4355
+ {
4356
+ style: {
4357
+ height: `${virtualizer.getTotalSize()}px`,
4358
+ width: "100%",
4359
+ position: "relative"
4360
+ },
4361
+ children: virtualizer.getVirtualItems().map((vi) => {
4362
+ const row = rows[vi.index];
4363
+ if (!row) return null;
4364
+ const selected = isSelected(row);
4365
+ return /* @__PURE__ */ jsx15(
4366
+ "button",
4367
+ {
4368
+ type: "button",
4369
+ role: "option",
4370
+ "aria-selected": selected,
4371
+ "aria-label": `${row.kind === "node" ? "Node" : "Edge"} ${row.label}`,
4372
+ "data-testid": `graph-${row.kind}-${row.id}`,
4373
+ ...row.kind === "node" ? { "data-graph-node-id": row.id } : { "data-graph-edge-id": row.id },
4374
+ onClick: () => selectRow(row),
4375
+ className: cn2(
4376
+ "absolute left-0 top-0 flex w-full items-center px-2 text-left text-sm",
4377
+ visible && "hover:bg-accent aria-selected:bg-state-selected aria-selected:text-foreground"
4378
+ ),
4379
+ style: {
4380
+ height: `${vi.size}px`,
4381
+ transform: `translateY(${vi.start}px)`
4382
+ },
4383
+ children: /* @__PURE__ */ jsx15("span", { className: "truncate", children: row.label })
4384
+ },
4385
+ vi.key
4386
+ );
4387
+ })
4388
+ }
4389
+ )
4390
+ }
4391
+ )
4392
+ }
4393
+ );
4394
+ }
4033
4395
  export {
4034
4396
  ActionPanel_default as ActionPanel,
4035
4397
  EMPTY_NORMALIZED,
4036
4398
  EdgeInfoCard_default as EdgeInfoCard,
4399
+ GraphA11yList,
4037
4400
  GraphRendererProvider,
4038
4401
  GraphStoresContext,
4039
4402
  GraphVisualizer_default as GraphVisualizer,