@petrarca/sonnet-graph 0.4.1 → 0.5.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
@@ -78,6 +78,7 @@ function extractDisplayLabel(item, displayConfig) {
78
78
  }
79
79
 
80
80
  // src/graph/enrichment.ts
81
+ var SELECTION_COLOR = "#2d64d2";
81
82
  var BASE_COLORS = [
82
83
  "#f94144",
83
84
  "#118ab2",
@@ -399,6 +400,8 @@ function createGraphFilterStore() {
399
400
  const now = !get().allLabelsDisabled;
400
401
  set({ allLabelsDisabled: now, nodeLabelFilters: [] });
401
402
  },
403
+ setAllLabelsDisabled: (disabled) => set({ allLabelsDisabled: disabled }),
404
+ setNodeFilterState: (disabled, filters) => set({ allLabelsDisabled: disabled, nodeLabelFilters: filters }),
402
405
  toggleEdgeLabelFilter: (label, allAvailableLabels, edgeToNodeLabels) => {
403
406
  const getNodeLabelUpdate = (edgeLabel) => {
404
407
  if (!edgeToNodeLabels) return null;
@@ -449,12 +452,12 @@ function collectReachable(startId, direction, allEdges, includeStart) {
449
452
  for (const edge of allEdges) {
450
453
  const srcId = String(edge.start_id_);
451
454
  const tgtId = String(edge.end_id_);
452
- const edgeId = String(edge.id_);
455
+ const edgeId2 = String(edge.id_);
453
456
  if (direction === "in" && tgtId === currentId) {
454
- edges.add(edgeId);
457
+ edges.add(edgeId2);
455
458
  traverse(srcId, false);
456
459
  } else if (direction === "out" && srcId === currentId) {
457
- edges.add(edgeId);
460
+ edges.add(edgeId2);
458
461
  traverse(tgtId, false);
459
462
  }
460
463
  }
@@ -494,24 +497,24 @@ function buildFocusSet(referenceNodeId, direction, edgeLabel, allEdges) {
494
497
  }
495
498
  return focusNodeIds;
496
499
  }
497
- function handleExistingNode(nodeId, nodeIdStr, existingNodes, autoFocus, graphFocusStore) {
498
- const existingNode = existingNodes.find((n) => n.id_ === nodeId);
500
+ function handleExistingNode(nodeId2, nodeIdStr, existingNodes, autoFocus, graphFocusStore) {
501
+ const existingNode = existingNodes.find((n) => n.id_ === nodeId2);
499
502
  if (!existingNode) return false;
500
503
  if (autoFocus) {
501
- devLog3("[GraphExploration] Node already in graph, focusing", { nodeId });
504
+ devLog3("[GraphExploration] Node already in graph, focusing", { nodeId: nodeId2 });
502
505
  graphFocusStore.getState().requestFocus(nodeIdStr, "node", false);
503
506
  } else {
504
507
  devLog3("[GraphExploration] Node already in graph, skipping focus", {
505
- nodeId
508
+ nodeId: nodeId2
506
509
  });
507
510
  }
508
511
  return true;
509
512
  }
510
- async function fetchAndEnrichNode(nodeId, existingNodes, stores) {
511
- devLog3("[GraphExploration] Fetching node from backend", { nodeId });
512
- const result = await stores.getNodes([nodeId]);
513
+ async function fetchAndEnrichNode(nodeId2, existingNodes, stores) {
514
+ devLog3("[GraphExploration] Fetching node from backend", { nodeId: nodeId2 });
515
+ const result = await stores.getNodes([nodeId2]);
513
516
  if (result.length === 0) {
514
- throw new Error(`Node ${nodeId} not found`);
517
+ throw new Error(`Node ${nodeId2} not found`);
515
518
  }
516
519
  const colorPalette = buildColorPaletteFromNodes([
517
520
  ...existingNodes,
@@ -523,27 +526,27 @@ async function fetchAndEnrichNode(nodeId, existingNodes, stores) {
523
526
  stores.getLabelDisplayMap?.()
524
527
  );
525
528
  devLog3("[GraphExploration] Enriched nodes", {
526
- nodeId,
529
+ nodeId: nodeId2,
527
530
  enrichedCount: enrichedNodes.length
528
531
  });
529
532
  stores.graphData.getState().mergeExpandedData(enrichedNodes, []);
530
533
  return enrichedNodes;
531
534
  }
532
- function ensureNodeLabelInFilters(enrichedNodes, nodeId, graphFilterStore) {
535
+ function ensureNodeLabelInFilters(enrichedNodes, nodeId2, graphFilterStore) {
533
536
  const addedNode = enrichedNodes[0];
534
537
  if (!addedNode) return;
535
538
  const { nodeLabelFilters: currentFilters } = graphFilterStore.getState();
536
539
  const nodeLabel = addedNode.label_;
537
540
  if (currentFilters.length > 0 && !currentFilters.includes(nodeLabel)) {
538
541
  devLog3("[GraphExploration] Adding node label to active filters", {
539
- nodeId,
542
+ nodeId: nodeId2,
540
543
  nodeLabel,
541
544
  currentFilters
542
545
  });
543
546
  graphFilterStore.getState().setNodeLabelFilters([...currentFilters, nodeLabel]);
544
547
  }
545
548
  }
546
- function mergeExpandedLabelsIntoFilters(stores, enrichedNodes, nodeId) {
549
+ function mergeExpandedLabelsIntoFilters(stores, enrichedNodes, nodeId2) {
547
550
  const graphFilterState = stores.graphFilter.getState();
548
551
  const currentFilters = graphFilterState.nodeLabelFilters;
549
552
  if (currentFilters.length === 0 || enrichedNodes.length === 0) return;
@@ -553,7 +556,7 @@ function mergeExpandedLabelsIntoFilters(stores, enrichedNodes, nodeId) {
553
556
  );
554
557
  if (labelsToAdd.length > 0) {
555
558
  devLog3("[GraphExploration] Adding expanded node labels to active filters", {
556
- nodeId,
559
+ nodeId: nodeId2,
557
560
  labelsToAdd,
558
561
  currentFilters
559
562
  });
@@ -577,10 +580,10 @@ function createGraphExplorationStore(deps) {
577
580
  isFocusMode: true
578
581
  });
579
582
  },
580
- setFocusModeWithNeighborhood: (nodeId) => {
583
+ setFocusModeWithNeighborhood: (nodeId2) => {
581
584
  const edges = stores.graphData.getState().edges;
582
585
  const connectedNodeIds = /* @__PURE__ */ new Set();
583
- const nodeIdStr = String(nodeId);
586
+ const nodeIdStr = String(nodeId2);
584
587
  for (const edge of edges) {
585
588
  const sourceId = String(edge.start_id_);
586
589
  const targetId = String(edge.end_id_);
@@ -628,19 +631,19 @@ function createGraphExplorationStore(deps) {
628
631
  });
629
632
  set({ hiddenElementIds: updated });
630
633
  },
631
- hideNodeIncoming: (nodeId) => {
634
+ hideNodeIncoming: (nodeId2) => {
632
635
  devLog3("[GraphExploration] hideNodeIncoming - hiding predecessors", {
633
- nodeId
636
+ nodeId: nodeId2
634
637
  });
635
638
  const { edges } = stores.graphData.getState();
636
639
  const { nodes: nodesToHide, edges: edgesToHide } = collectReachable(
637
- nodeId,
640
+ nodeId2,
638
641
  "in",
639
642
  edges,
640
643
  false
641
644
  );
642
645
  devLog3("[GraphExploration] hideNodeIncoming - traversal complete", {
643
- nodeId,
646
+ nodeId: nodeId2,
644
647
  nodesToHide: nodesToHide.size,
645
648
  edgesToHide: edgesToHide.size
646
649
  });
@@ -651,19 +654,19 @@ function createGraphExplorationStore(deps) {
651
654
  devLog3("[GraphExploration] hideNodeIncoming - no predecessors found");
652
655
  }
653
656
  },
654
- hideNodeOutgoing: (nodeId) => {
657
+ hideNodeOutgoing: (nodeId2) => {
655
658
  devLog3("[GraphExploration] hideNodeOutgoing - hiding successors", {
656
- nodeId
659
+ nodeId: nodeId2
657
660
  });
658
661
  const { edges } = stores.graphData.getState();
659
662
  const { nodes: nodesToHide, edges: edgesToHide } = collectReachable(
660
- nodeId,
663
+ nodeId2,
661
664
  "out",
662
665
  edges,
663
666
  false
664
667
  );
665
668
  devLog3("[GraphExploration] hideNodeOutgoing - traversal complete", {
666
- nodeId,
669
+ nodeId: nodeId2,
667
670
  nodesToHide: nodesToHide.size,
668
671
  edgesToHide: edgesToHide.size
669
672
  });
@@ -674,21 +677,21 @@ function createGraphExplorationStore(deps) {
674
677
  devLog3("[GraphExploration] hideNodeOutgoing - no successors found");
675
678
  }
676
679
  },
677
- hideEdgeForwardPath: (edgeId) => {
680
+ hideEdgeForwardPath: (edgeId2) => {
678
681
  devLog3(
679
682
  "[GraphExploration] hideEdgeForwardPath - hiding edge + target + downstream",
680
- { edgeId }
683
+ { edgeId: edgeId2 }
681
684
  );
682
685
  const { edges } = stores.graphData.getState();
683
- const edge = edges.find((e) => String(e.id_) === edgeId);
686
+ const edge = edges.find((e) => String(e.id_) === edgeId2);
684
687
  if (!edge) {
685
688
  devLog3("[GraphExploration] hideEdgeForwardPath - edge not found", {
686
- edgeId
689
+ edgeId: edgeId2
687
690
  });
688
691
  return;
689
692
  }
690
693
  const targetNodeId = String(edge.end_id_);
691
- get().hideElements([edgeId]);
694
+ get().hideElements([edgeId2]);
692
695
  const { nodes: nodesToHide, edges: edgesToHide } = collectReachable(
693
696
  targetNodeId,
694
697
  "out",
@@ -696,28 +699,28 @@ function createGraphExplorationStore(deps) {
696
699
  true
697
700
  );
698
701
  devLog3("[GraphExploration] hideEdgeForwardPath - traversal complete", {
699
- edgeId,
702
+ edgeId: edgeId2,
700
703
  nodesToHide: nodesToHide.size,
701
704
  edgesToHide: edgesToHide.size
702
705
  });
703
706
  const allElementsToHide = [...nodesToHide, ...edgesToHide];
704
707
  if (allElementsToHide.length > 0) get().hideElements(allElementsToHide);
705
708
  },
706
- hideEdgeReversePath: (edgeId) => {
709
+ hideEdgeReversePath: (edgeId2) => {
707
710
  devLog3(
708
711
  "[GraphExploration] hideEdgeReversePath - hiding edge + source + upstream",
709
- { edgeId }
712
+ { edgeId: edgeId2 }
710
713
  );
711
714
  const { edges } = stores.graphData.getState();
712
- const edge = edges.find((e) => String(e.id_) === edgeId);
715
+ const edge = edges.find((e) => String(e.id_) === edgeId2);
713
716
  if (!edge) {
714
717
  devLog3("[GraphExploration] hideEdgeReversePath - edge not found", {
715
- edgeId
718
+ edgeId: edgeId2
716
719
  });
717
720
  return;
718
721
  }
719
722
  const sourceNodeId = String(edge.start_id_);
720
- get().hideElements([edgeId]);
723
+ get().hideElements([edgeId2]);
721
724
  const { nodes: nodesToHide, edges: edgesToHide } = collectReachable(
722
725
  sourceNodeId,
723
726
  "in",
@@ -725,29 +728,29 @@ function createGraphExplorationStore(deps) {
725
728
  true
726
729
  );
727
730
  devLog3("[GraphExploration] hideEdgeReversePath - traversal complete", {
728
- edgeId,
731
+ edgeId: edgeId2,
729
732
  nodesToHide: nodesToHide.size,
730
733
  edgesToHide: edgesToHide.size
731
734
  });
732
735
  const allElementsToHide = [...nodesToHide, ...edgesToHide];
733
736
  if (allElementsToHide.length > 0) get().hideElements(allElementsToHide);
734
737
  },
735
- focusOnEdgeType: (edgeId) => {
738
+ focusOnEdgeType: (edgeId2) => {
736
739
  devLog3(
737
740
  "[GraphExploration] focusOnEdgeType - show only edges of this type and connected nodes",
738
- { edgeId }
741
+ { edgeId: edgeId2 }
739
742
  );
740
743
  const graphData = stores.graphData.getState();
741
- const edge = graphData.edges.find((e) => String(e.id_) === edgeId);
744
+ const edge = graphData.edges.find((e) => String(e.id_) === edgeId2);
742
745
  if (!edge) {
743
746
  devLog3("[GraphExploration] focusOnEdgeType - edge not found", {
744
- edgeId
747
+ edgeId: edgeId2
745
748
  });
746
749
  return;
747
750
  }
748
751
  const edgeLabel = edge.label_;
749
752
  devLog3("[GraphExploration] focusOnEdgeType - filtering by label", {
750
- edgeId,
753
+ edgeId: edgeId2,
751
754
  edgeLabel
752
755
  });
753
756
  const edgesOfType = graphData.edges.filter((e) => e.label_ === edgeLabel);
@@ -761,9 +764,9 @@ function createGraphExplorationStore(deps) {
761
764
  const nodesToHide = [];
762
765
  const edgesToHide = [];
763
766
  for (const node of graphData.nodes) {
764
- const nodeId = String(node.id_);
765
- if (!connectedNodeIds.has(nodeId)) {
766
- nodesToHide.push(nodeId);
767
+ const nodeId2 = String(node.id_);
768
+ if (!connectedNodeIds.has(nodeId2)) {
769
+ nodesToHide.push(nodeId2);
767
770
  }
768
771
  }
769
772
  for (const e of graphData.edges) {
@@ -784,16 +787,16 @@ function createGraphExplorationStore(deps) {
784
787
  get().hideElements(allElementsToHide);
785
788
  }
786
789
  },
787
- focusEdge: (edgeId) => {
790
+ focusEdge: (edgeId2) => {
788
791
  devLog3(
789
792
  "[GraphExploration] focusEdge - unified focus with auto-direction detection",
790
- { edgeId }
793
+ { edgeId: edgeId2 }
791
794
  );
792
795
  const graphData = stores.graphData.getState();
793
796
  const selection = stores.selection.getState();
794
- const edge = graphData.edges.find((e) => String(e.id_) === edgeId);
797
+ const edge = graphData.edges.find((e) => String(e.id_) === edgeId2);
795
798
  if (!edge) {
796
- devLog3("[GraphExploration] focusEdge - edge not found", { edgeId });
799
+ devLog3("[GraphExploration] focusEdge - edge not found", { edgeId: edgeId2 });
797
800
  return;
798
801
  }
799
802
  const endpoints = {
@@ -806,7 +809,7 @@ function createGraphExplorationStore(deps) {
806
809
  selection.selectedNodeIds
807
810
  );
808
811
  devLog3("[GraphExploration] focusEdge - determined direction", {
809
- edgeId,
812
+ edgeId: edgeId2,
810
813
  edgeLabel: endpoints.edgeLabel,
811
814
  direction,
812
815
  referenceNodeId,
@@ -849,27 +852,27 @@ function createGraphExplorationStore(deps) {
849
852
  expandedNodeIds: /* @__PURE__ */ new Set()
850
853
  });
851
854
  },
852
- markNodeExpanded: (nodeId) => {
855
+ markNodeExpanded: (nodeId2) => {
853
856
  const current = get().expandedNodeIds;
854
857
  const updated = new Set(current);
855
- updated.add(nodeId);
856
- devLog3("[GraphExploration] markNodeExpanded", { nodeId });
858
+ updated.add(nodeId2);
859
+ devLog3("[GraphExploration] markNodeExpanded", { nodeId: nodeId2 });
857
860
  set({ expandedNodeIds: updated });
858
861
  },
859
- isNodeExpanded: (nodeId) => {
860
- return get().expandedNodeIds.has(nodeId);
862
+ isNodeExpanded: (nodeId2) => {
863
+ return get().expandedNodeIds.has(nodeId2);
861
864
  },
862
- expandNode: async (nodeId, options) => {
863
- devLog3("[GraphExploration] expandNode", { nodeId, options });
865
+ expandNode: async (nodeId2, options) => {
866
+ devLog3("[GraphExploration] expandNode", { nodeId: nodeId2, options });
864
867
  try {
865
- const result = await stores.expandNode(Number(nodeId), options);
868
+ const result = await stores.expandNode(Number(nodeId2), options);
866
869
  devLog3("[GraphExploration] Expansion API result", {
867
- nodeId,
870
+ nodeId: nodeId2,
868
871
  newNodes: result.nodes.length,
869
872
  newEdges: result.edges.length
870
873
  });
871
874
  if (result.nodes.length === 0 && result.edges.length === 0) {
872
- devLog3("[GraphExploration] No connected nodes found", { nodeId });
875
+ devLog3("[GraphExploration] No connected nodes found", { nodeId: nodeId2 });
873
876
  return;
874
877
  }
875
878
  const existingNodes = stores.graphData.getState().nodes;
@@ -884,8 +887,8 @@ function createGraphExplorationStore(deps) {
884
887
  );
885
888
  const enrichedEdges = enrichEdges(result.edges);
886
889
  stores.graphData.getState().mergeExpandedData(enrichedNodes, enrichedEdges);
887
- mergeExpandedLabelsIntoFilters(stores, enrichedNodes, nodeId);
888
- get().markNodeExpanded(nodeId);
890
+ mergeExpandedLabelsIntoFilters(stores, enrichedNodes, nodeId2);
891
+ get().markNodeExpanded(nodeId2);
889
892
  const currentState = get();
890
893
  if (currentState.isFocusMode) {
891
894
  const newFocusedIds = new Set(currentState.focusedNodeIds);
@@ -893,7 +896,7 @@ function createGraphExplorationStore(deps) {
893
896
  newFocusedIds.add(String(node.id_));
894
897
  }
895
898
  devLog3("[GraphExploration] Adding expanded nodes to focus set", {
896
- nodeId,
899
+ nodeId: nodeId2,
897
900
  previousFocusCount: currentState.focusedNodeIds.size,
898
901
  addedToFocus: enrichedNodes.length,
899
902
  newFocusCount: newFocusedIds.size
@@ -906,23 +909,23 @@ function createGraphExplorationStore(deps) {
906
909
  );
907
910
  }
908
911
  devLog3("[GraphExploration] Expansion complete", {
909
- nodeId,
912
+ nodeId: nodeId2,
910
913
  addedNodes: enrichedNodes.length,
911
914
  addedEdges: enrichedEdges.length,
912
915
  hasMore: result.has_more
913
916
  });
914
917
  } catch (error) {
915
- errorLog("[GraphExploration] Expansion failed", { nodeId, error });
918
+ errorLog("[GraphExploration] Expansion failed", { nodeId: nodeId2, error });
916
919
  throw error;
917
920
  }
918
921
  },
919
- addNodeById: async (nodeId, autoFocus = false) => {
920
- const nodeIdStr = String(nodeId);
921
- devLog3("[GraphExploration] addNodeById", { nodeId, autoFocus });
922
+ addNodeById: async (nodeId2, autoFocus = false) => {
923
+ const nodeIdStr = String(nodeId2);
924
+ devLog3("[GraphExploration] addNodeById", { nodeId: nodeId2, autoFocus });
922
925
  try {
923
926
  const existingNodes = stores.graphData.getState().nodes;
924
927
  if (handleExistingNode(
925
- nodeId,
928
+ nodeId2,
926
929
  nodeIdStr,
927
930
  existingNodes,
928
931
  autoFocus,
@@ -931,19 +934,19 @@ function createGraphExplorationStore(deps) {
931
934
  return;
932
935
  }
933
936
  const enrichedNodes = await fetchAndEnrichNode(
934
- nodeId,
937
+ nodeId2,
935
938
  existingNodes,
936
939
  stores
937
940
  );
938
- ensureNodeLabelInFilters(enrichedNodes, nodeId, stores.graphFilter);
941
+ ensureNodeLabelInFilters(enrichedNodes, nodeId2, stores.graphFilter);
939
942
  if (autoFocus) {
940
943
  devLog3("[GraphExploration] Requesting focus for newly added node", {
941
- nodeId
944
+ nodeId: nodeId2
942
945
  });
943
946
  stores.graphFocus.getState().requestFocus(nodeIdStr, "node", false);
944
947
  } else {
945
948
  devLog3("[GraphExploration] Skipping focus for newly added node", {
946
- nodeId
949
+ nodeId: nodeId2
947
950
  });
948
951
  }
949
952
  const currentState = get();
@@ -951,19 +954,19 @@ function createGraphExplorationStore(deps) {
951
954
  const newFocusedIds = new Set(currentState.focusedNodeIds);
952
955
  newFocusedIds.add(nodeIdStr);
953
956
  devLog3("[GraphExploration] Adding node to focus set", {
954
- nodeId,
957
+ nodeId: nodeId2,
955
958
  previousFocusCount: currentState.focusedNodeIds.size,
956
959
  newFocusCount: newFocusedIds.size
957
960
  });
958
961
  set({ focusedNodeIds: newFocusedIds });
959
962
  }
960
963
  devLog3("[GraphExploration] addNodeById complete", {
961
- nodeId,
964
+ nodeId: nodeId2,
962
965
  addedNodes: enrichedNodes.length,
963
966
  autoFocus
964
967
  });
965
968
  } catch (error) {
966
- devLog3("[GraphExploration] addNodeById failed", { nodeId, error });
969
+ devLog3("[GraphExploration] addNodeById failed", { nodeId: nodeId2, error });
967
970
  throw error;
968
971
  }
969
972
  }
@@ -1061,6 +1064,17 @@ function useWorkspaceSelection() {
1061
1064
  return useGraphStores().selection;
1062
1065
  }
1063
1066
 
1067
+ // src/hooks/useStableRef.ts
1068
+ import { useCallback, useRef } from "react";
1069
+ function useStableRef(fn) {
1070
+ const ref = useRef(fn);
1071
+ ref.current = fn;
1072
+ return useCallback(
1073
+ ((...args) => ref.current?.(...args)),
1074
+ []
1075
+ );
1076
+ }
1077
+
1064
1078
  // src/hooks/useLiveGraphData.ts
1065
1079
  import { useMemo, useEffect } from "react";
1066
1080
  import { devLog as devLog6 } from "@petrarca/sonnet-core";
@@ -1425,7 +1439,7 @@ function useGraphRenderer() {
1425
1439
  }
1426
1440
 
1427
1441
  // src/hooks/graphRendererProvider.tsx
1428
- import { useState, useCallback } from "react";
1442
+ import { useState, useCallback as useCallback2 } from "react";
1429
1443
  import { jsx } from "react/jsx-runtime";
1430
1444
  function GraphRendererProvider({
1431
1445
  children,
@@ -1433,7 +1447,7 @@ function GraphRendererProvider({
1433
1447
  onRendererChange
1434
1448
  }) {
1435
1449
  const [renderer, setRendererState] = useState(defaultRenderer);
1436
- const setRenderer = useCallback(
1450
+ const setRenderer = useCallback2(
1437
1451
  (newRenderer) => {
1438
1452
  setRendererState(newRenderer);
1439
1453
  onRendererChange?.(newRenderer);
@@ -1446,9 +1460,9 @@ function GraphRendererProvider({
1446
1460
  // src/components/rea/ReaGraph.tsx
1447
1461
  import {
1448
1462
  useEffect as useEffect3,
1449
- useRef as useRef2,
1463
+ useRef as useRef3,
1450
1464
  useState as useState5,
1451
- useCallback as useCallback3
1465
+ useCallback as useCallback4
1452
1466
  } from "react";
1453
1467
  import { devLog as devLog8 } from "@petrarca/sonnet-core";
1454
1468
  import {
@@ -1458,8 +1472,8 @@ import {
1458
1472
 
1459
1473
  // src/components/rea/theme.ts
1460
1474
  import { lightTheme } from "reagraph";
1461
- var highlightColor = "#ffb512";
1462
- var highlightColorText = "#c98a02";
1475
+ var highlightColor = SELECTION_COLOR;
1476
+ var highlightColorText = "#1d4ed8";
1463
1477
  var theme = {
1464
1478
  ...lightTheme,
1465
1479
  node: {
@@ -1672,7 +1686,7 @@ import React3, { useState as useState4 } from "react";
1672
1686
  import { devLog as devLog7 } from "@petrarca/sonnet-core";
1673
1687
 
1674
1688
  // src/components/rea/ExpandOptionsPanel.tsx
1675
- import { useCallback as useCallback2, useEffect as useEffect2, useRef, useState as useState3 } from "react";
1689
+ import { useCallback as useCallback3, useEffect as useEffect2, useRef as useRef2, useState as useState3 } from "react";
1676
1690
  import { Checkbox, Input, Label } from "@petrarca/sonnet-ui";
1677
1691
  import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1678
1692
  function ExpandOptionsPanel({
@@ -1687,8 +1701,9 @@ function ExpandOptionsPanel({
1687
1701
  const [selectedTypes, setSelectedTypes] = useState3([]);
1688
1702
  const [loading, setLoading] = useState3(!!getEdgeTypes);
1689
1703
  const [offset, setOffset] = useState3({ x: 0, y: 0 });
1690
- const dragRef = useRef(null);
1691
- const onPointerDown = useCallback2(
1704
+ const expandBtnRef = useRef2(null);
1705
+ const dragRef = useRef2(null);
1706
+ const onPointerDown = useCallback3(
1692
1707
  (e) => {
1693
1708
  if (e.target.closest("button")) return;
1694
1709
  e.preventDefault();
@@ -1702,13 +1717,13 @@ function ExpandOptionsPanel({
1702
1717
  },
1703
1718
  [offset]
1704
1719
  );
1705
- const onPointerMove = useCallback2((e) => {
1720
+ const onPointerMove = useCallback3((e) => {
1706
1721
  if (!dragRef.current) return;
1707
1722
  const dx = e.clientX - dragRef.current.startX;
1708
1723
  const dy = e.clientY - dragRef.current.startY;
1709
1724
  setOffset({ x: dragRef.current.origX + dx, y: dragRef.current.origY + dy });
1710
1725
  }, []);
1711
- const onPointerUp = useCallback2(() => {
1726
+ const onPointerUp = useCallback3(() => {
1712
1727
  dragRef.current = null;
1713
1728
  }, []);
1714
1729
  useEffect2(() => {
@@ -1736,6 +1751,11 @@ function ExpandOptionsPanel({
1736
1751
  };
1737
1752
  const allSelected = selectedTypes.length === edgeTypes.length;
1738
1753
  const noneSelected = edgeTypes.length > 0 && selectedTypes.length === 0;
1754
+ useEffect2(() => {
1755
+ if (!loading && !noneSelected) {
1756
+ expandBtnRef.current?.focus();
1757
+ }
1758
+ }, [loading, noneSelected]);
1739
1759
  return /* @__PURE__ */ jsxs4(
1740
1760
  "div",
1741
1761
  {
@@ -1825,6 +1845,7 @@ function ExpandOptionsPanel({
1825
1845
  /* @__PURE__ */ jsx5(
1826
1846
  "button",
1827
1847
  {
1848
+ ref: expandBtnRef,
1828
1849
  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
1850
  onClick: handleExpand,
1830
1851
  disabled: loading || noneSelected,
@@ -1858,9 +1879,9 @@ function ReaContextMenu({
1858
1879
  }, [onClose]);
1859
1880
  const handleFocus = () => {
1860
1881
  if (elementType === "node") {
1861
- const nodeId = data.id;
1862
- devLog7("[ReaContextMenu] Focus on node and neighborhood:", nodeId);
1863
- useGraphExploration.getState().setFocusModeWithNeighborhood(nodeId);
1882
+ const nodeId2 = data.id;
1883
+ devLog7("[ReaContextMenu] Focus on node and neighborhood:", nodeId2);
1884
+ useGraphExploration.getState().setFocusModeWithNeighborhood(nodeId2);
1864
1885
  } else {
1865
1886
  const startNodeId = data.data.start_id_;
1866
1887
  devLog7("[ReaContextMenu] Focus on edge start node:", startNodeId);
@@ -1886,58 +1907,58 @@ function ReaContextMenu({
1886
1907
  };
1887
1908
  const handleHideNode = () => {
1888
1909
  if (elementType === "node") {
1889
- const nodeId = data.id;
1890
- devLog7("[ReaContextMenu] Hide node:", nodeId);
1891
- useGraphExploration.getState().hideElements([nodeId]);
1910
+ const nodeId2 = data.id;
1911
+ devLog7("[ReaContextMenu] Hide node:", nodeId2);
1912
+ useGraphExploration.getState().hideElements([nodeId2]);
1892
1913
  }
1893
1914
  onClose();
1894
1915
  };
1895
1916
  const handleHideIncoming = () => {
1896
1917
  if (elementType === "node") {
1897
- const nodeId = data.id;
1898
- devLog7("[ReaContextMenu] Hide incoming edges:", nodeId);
1899
- useGraphExploration.getState().hideNodeIncoming(nodeId);
1918
+ const nodeId2 = data.id;
1919
+ devLog7("[ReaContextMenu] Hide incoming edges:", nodeId2);
1920
+ useGraphExploration.getState().hideNodeIncoming(nodeId2);
1900
1921
  }
1901
1922
  onClose();
1902
1923
  };
1903
1924
  const handleHideOutgoing = () => {
1904
1925
  if (elementType === "node") {
1905
- const nodeId = data.id;
1906
- devLog7("[ReaContextMenu] Hide outgoing edges:", nodeId);
1907
- useGraphExploration.getState().hideNodeOutgoing(nodeId);
1926
+ const nodeId2 = data.id;
1927
+ devLog7("[ReaContextMenu] Hide outgoing edges:", nodeId2);
1928
+ useGraphExploration.getState().hideNodeOutgoing(nodeId2);
1908
1929
  }
1909
1930
  onClose();
1910
1931
  };
1911
1932
  const handleExpandWithOptions = (options) => {
1912
1933
  if (elementType === "node") {
1913
- const nodeId = data.id;
1914
- devLog7("[ReaContextMenu] Expand node with options:", nodeId, options);
1915
- useGraphExploration.getState().expandNode(nodeId, options);
1934
+ const nodeId2 = data.id;
1935
+ devLog7("[ReaContextMenu] Expand node with options:", nodeId2, options);
1936
+ useGraphExploration.getState().expandNode(nodeId2, options);
1916
1937
  }
1917
1938
  onClose();
1918
1939
  };
1919
1940
  const handleHideForwardPath = () => {
1920
1941
  if (elementType === "edge") {
1921
- const edgeId = data.id;
1922
- devLog7("[ReaContextMenu] Hide forward path from edge:", edgeId);
1923
- useGraphExploration.getState().hideEdgeForwardPath(edgeId);
1942
+ const edgeId2 = data.id;
1943
+ devLog7("[ReaContextMenu] Hide forward path from edge:", edgeId2);
1944
+ useGraphExploration.getState().hideEdgeForwardPath(edgeId2);
1924
1945
  }
1925
1946
  onClose();
1926
1947
  };
1927
1948
  const handleHideReversePath = () => {
1928
1949
  if (elementType === "edge") {
1929
- const edgeId = data.id;
1930
- devLog7("[ReaContextMenu] Hide reverse path from edge:", edgeId);
1931
- useGraphExploration.getState().hideEdgeReversePath(edgeId);
1950
+ const edgeId2 = data.id;
1951
+ devLog7("[ReaContextMenu] Hide reverse path from edge:", edgeId2);
1952
+ useGraphExploration.getState().hideEdgeReversePath(edgeId2);
1932
1953
  }
1933
1954
  onClose();
1934
1955
  };
1935
1956
  const handleFocusOnEdgeType = () => {
1936
1957
  if (elementType === "edge") {
1937
- const edgeId = data.id;
1958
+ const edgeId2 = data.id;
1938
1959
  const edgeLabel = data.data?.label_ || "edge";
1939
- devLog7("[ReaContextMenu] Focus on edge type:", edgeId, edgeLabel);
1940
- useGraphExploration.getState().focusOnEdgeType(edgeId);
1960
+ devLog7("[ReaContextMenu] Focus on edge type:", edgeId2, edgeLabel);
1961
+ useGraphExploration.getState().focusOnEdgeType(edgeId2);
1941
1962
  }
1942
1963
  onClose();
1943
1964
  };
@@ -2063,7 +2084,7 @@ function ReaGraph({
2063
2084
  extraToolbarContent,
2064
2085
  getNodeEdgeTypes
2065
2086
  }) {
2066
- const isMountedRef = useRef2(false);
2087
+ const isMountedRef = useRef3(false);
2067
2088
  const { nodes, edges } = useReagraphData();
2068
2089
  const { normalized } = useLiveGraphData();
2069
2090
  useEffect3(() => {
@@ -2071,7 +2092,7 @@ function ReaGraph({
2071
2092
  if (nodes.length > 0) devLog8("[ReGraph] sample node", nodes[0]);
2072
2093
  if (edges.length > 0) devLog8("[ReGraph] sample edge", edges[0]);
2073
2094
  }, [nodes, edges]);
2074
- const canvas = useRef2(null);
2095
+ const canvas = useRef3(null);
2075
2096
  const [hoveredNode, setHoveredNode] = useState5(
2076
2097
  void 0
2077
2098
  );
@@ -2132,7 +2153,7 @@ function ReaGraph({
2132
2153
  clearSelections();
2133
2154
  clearSelectionStore();
2134
2155
  }, [lastDataChangeTime, clearSelections, clearSelectionStore]);
2135
- const fitToScreen = useCallback3(
2156
+ const fitToScreen = useCallback4(
2136
2157
  (reason) => {
2137
2158
  if (!isMountedRef.current) return;
2138
2159
  if (nodes.length === 0) return;
@@ -2178,6 +2199,20 @@ function ReaGraph({
2178
2199
  isMountedRef.current = false;
2179
2200
  };
2180
2201
  }, [fitToScreen, setSelections, setSelectedEdge, useGraphSelection]);
2202
+ const selectionsRef = useRef3(selections);
2203
+ selectionsRef.current = selections;
2204
+ useEffect3(() => {
2205
+ const inSync = (nodeIds) => {
2206
+ const cur = selectionsRef.current;
2207
+ return cur.length === nodeIds.length && nodeIds.every((id) => cur.includes(id));
2208
+ };
2209
+ const unsubscribe = useGraphSelection.subscribe((state) => {
2210
+ if (!inSync(state.selectedNodeIds)) {
2211
+ setSelections(state.selectedNodeIds);
2212
+ }
2213
+ });
2214
+ return unsubscribe;
2215
+ }, [useGraphSelection, setSelections]);
2181
2216
  useEffect3(() => {
2182
2217
  if (isMountedRef.current) fitToScreen("counts-changed");
2183
2218
  }, [nodes.length, edges.length, fitToScreen]);
@@ -2293,7 +2328,7 @@ function ReaGraph({
2293
2328
  window.removeEventListener("resize", handleResize);
2294
2329
  };
2295
2330
  }, [fitToScreen]);
2296
- const firstDataRef = useRef2(true);
2331
+ const firstDataRef = useRef3(true);
2297
2332
  useEffect3(() => {
2298
2333
  if (!isMountedRef.current) return;
2299
2334
  if (nodes.length > 0) {
@@ -2394,9 +2429,9 @@ var ReaGraph_default = ReaGraph;
2394
2429
  // src/components/cyto/CytoscapeGraph.tsx
2395
2430
  import {
2396
2431
  useEffect as useEffect4,
2397
- useRef as useRef3,
2432
+ useRef as useRef4,
2398
2433
  useState as useState6,
2399
- useCallback as useCallback4
2434
+ useCallback as useCallback5
2400
2435
  } from "react";
2401
2436
  import { devLog as devLog10, errorLog as errorLog3 } from "@petrarca/sonnet-core";
2402
2437
  import cytoscape from "cytoscape";
@@ -2817,9 +2852,9 @@ var createNodeContextMenu = ({
2817
2852
  {
2818
2853
  content: "Focus",
2819
2854
  select: (ele) => {
2820
- const nodeId = ele.id();
2821
- devLog9("[CxtMenu] Focus on node and neighborhood:", nodeId);
2822
- useGraphExploration.getState().setFocusModeWithNeighborhood(nodeId);
2855
+ const nodeId2 = ele.id();
2856
+ devLog9("[CxtMenu] Focus on node and neighborhood:", nodeId2);
2857
+ useGraphExploration.getState().setFocusModeWithNeighborhood(nodeId2);
2823
2858
  }
2824
2859
  },
2825
2860
  {
@@ -2837,38 +2872,38 @@ var createNodeContextMenu = ({
2837
2872
  {
2838
2873
  content: "Hide Node",
2839
2874
  select: (ele) => {
2840
- const nodeId = ele.id();
2841
- devLog9("[CxtMenu] Hide node:", nodeId);
2842
- useGraphExploration.getState().hideElements([nodeId]);
2875
+ const nodeId2 = ele.id();
2876
+ devLog9("[CxtMenu] Hide node:", nodeId2);
2877
+ useGraphExploration.getState().hideElements([nodeId2]);
2843
2878
  }
2844
2879
  },
2845
2880
  {
2846
2881
  content: "Hide Incoming",
2847
2882
  select: (ele) => {
2848
- const nodeId = ele.id();
2849
- devLog9("[CxtMenu] Hide incoming (predecessors) of node:", nodeId);
2850
- useGraphExploration.getState().hideNodeIncoming(nodeId);
2883
+ const nodeId2 = ele.id();
2884
+ devLog9("[CxtMenu] Hide incoming (predecessors) of node:", nodeId2);
2885
+ useGraphExploration.getState().hideNodeIncoming(nodeId2);
2851
2886
  }
2852
2887
  },
2853
2888
  {
2854
2889
  content: "Hide Outgoing",
2855
2890
  select: (ele) => {
2856
- const nodeId = ele.id();
2857
- devLog9("[CxtMenu] Hide outgoing (successors) of node:", nodeId);
2858
- useGraphExploration.getState().hideNodeOutgoing(nodeId);
2891
+ const nodeId2 = ele.id();
2892
+ devLog9("[CxtMenu] Hide outgoing (successors) of node:", nodeId2);
2893
+ useGraphExploration.getState().hideNodeOutgoing(nodeId2);
2859
2894
  }
2860
2895
  },
2861
2896
  {
2862
2897
  content: "Expand",
2863
2898
  select: (ele) => {
2864
- const nodeId = ele.id();
2865
- devLog9("[CxtMenu] Expand request for node:", nodeId);
2899
+ const nodeId2 = ele.id();
2900
+ devLog9("[CxtMenu] Expand request for node:", nodeId2);
2866
2901
  if (onExpandRequest) {
2867
2902
  const pos = ele.renderedPosition();
2868
- const label = ele.data("label") || nodeId;
2869
- onExpandRequest({ nodeId, nodeLabel: label, x: pos.x, y: pos.y });
2903
+ const label = ele.data("label") || nodeId2;
2904
+ onExpandRequest({ nodeId: nodeId2, nodeLabel: label, x: pos.x, y: pos.y });
2870
2905
  } else {
2871
- useGraphExploration.getState().expandNode(nodeId).catch((error) => {
2906
+ useGraphExploration.getState().expandNode(nodeId2).catch((error) => {
2872
2907
  errorLog2("[CxtMenu] Expansion failed:", error);
2873
2908
  toast3.error("Failed to expand node");
2874
2909
  });
@@ -2904,42 +2939,42 @@ var createEdgeContextMenu = ({
2904
2939
  {
2905
2940
  content: "Focus",
2906
2941
  select: (ele) => {
2907
- const edgeId = ele.id();
2942
+ const edgeId2 = ele.id();
2908
2943
  devLog9(
2909
2944
  "[CxtMenu] Focus on edge (auto-direction based on selection):",
2910
- edgeId
2945
+ edgeId2
2911
2946
  );
2912
- useGraphExploration.getState().focusEdge(edgeId);
2947
+ useGraphExploration.getState().focusEdge(edgeId2);
2913
2948
  }
2914
2949
  },
2915
2950
  {
2916
2951
  content: "Focus Edge Type",
2917
2952
  select: (ele) => {
2918
- const edgeId = ele.id();
2919
- devLog9("[CxtMenu] Focus on edge type:", edgeId);
2920
- useGraphExploration.getState().focusOnEdgeType(edgeId);
2953
+ const edgeId2 = ele.id();
2954
+ devLog9("[CxtMenu] Focus on edge type:", edgeId2);
2955
+ useGraphExploration.getState().focusOnEdgeType(edgeId2);
2921
2956
  }
2922
2957
  },
2923
2958
  {
2924
2959
  content: "Hide Forward Path",
2925
2960
  select: (ele) => {
2926
- const edgeId = ele.id();
2961
+ const edgeId2 = ele.id();
2927
2962
  devLog9(
2928
2963
  "[CxtMenu] Hide forward path (edge + target + downstream):",
2929
- edgeId
2964
+ edgeId2
2930
2965
  );
2931
- useGraphExploration.getState().hideEdgeForwardPath(edgeId);
2966
+ useGraphExploration.getState().hideEdgeForwardPath(edgeId2);
2932
2967
  }
2933
2968
  },
2934
2969
  {
2935
2970
  content: "Hide Reverse Path",
2936
2971
  select: (ele) => {
2937
- const edgeId = ele.id();
2972
+ const edgeId2 = ele.id();
2938
2973
  devLog9(
2939
2974
  "[CxtMenu] Hide reverse path (edge + source + upstream):",
2940
- edgeId
2975
+ edgeId2
2941
2976
  );
2942
- useGraphExploration.getState().hideEdgeReversePath(edgeId);
2977
+ useGraphExploration.getState().hideEdgeReversePath(edgeId2);
2943
2978
  }
2944
2979
  }
2945
2980
  ],
@@ -2990,9 +3025,9 @@ function CytoscapeExpandOverlay({
2990
3025
  {
2991
3026
  nodeLabel: expandRequest.nodeLabel,
2992
3027
  onExpand: (options) => {
2993
- const { nodeId } = expandRequest;
3028
+ const { nodeId: nodeId2 } = expandRequest;
2994
3029
  onClose();
2995
- useGraphExploration.getState().expandNode(nodeId, options).catch((err) => {
3030
+ useGraphExploration.getState().expandNode(nodeId2, options).catch((err) => {
2996
3031
  errorLog3("[CxtMenu] Expansion failed:", err);
2997
3032
  toast4.error("Failed to expand node");
2998
3033
  });
@@ -3322,7 +3357,7 @@ function useFullscreen(fitToScreen, cyRef) {
3322
3357
  return {
3323
3358
  fullscreen,
3324
3359
  setFullscreen,
3325
- toggleFullscreen: useCallback4(() => setFullscreen((f) => !f), [])
3360
+ toggleFullscreen: useCallback5(() => setFullscreen((f) => !f), [])
3326
3361
  };
3327
3362
  }
3328
3363
  function setupCytoscapeEventHandlers({
@@ -3385,8 +3420,8 @@ function setupCytoscapeEventHandlers({
3385
3420
  });
3386
3421
  cy.on("tap", "edge", (evt) => {
3387
3422
  const edgeEle = evt.target;
3388
- const edgeId = edgeEle.id();
3389
- const edgeObj = edgesRef.current.find((e) => String(e.id_) === edgeId);
3423
+ const edgeId2 = edgeEle.id();
3424
+ const edgeObj = edgesRef.current.find((e) => String(e.id_) === edgeId2);
3390
3425
  if (!edgeObj) return;
3391
3426
  const sourceId = edgeEle.data("source");
3392
3427
  const targetId = edgeEle.data("target");
@@ -3396,7 +3431,7 @@ function setupCytoscapeEventHandlers({
3396
3431
  const selectedNodeId = currentlySelectedNodes[0].id();
3397
3432
  if (selectedNodeId === sourceId || selectedNodeId === targetId) {
3398
3433
  devLog10("[CytoscapeGraph] tap edge - node+edge selection", {
3399
- edgeId,
3434
+ edgeId: edgeId2,
3400
3435
  selectedNode: selectedNodeId,
3401
3436
  direction: selectedNodeId === sourceId ? "outgoing" : "incoming"
3402
3437
  });
@@ -3410,7 +3445,7 @@ function setupCytoscapeEventHandlers({
3410
3445
  return;
3411
3446
  }
3412
3447
  }
3413
- devLog10("[CytoscapeGraph] tap edge - default (both endpoints)", { edgeId });
3448
+ devLog10("[CytoscapeGraph] tap edge - default (both endpoints)", { edgeId: edgeId2 });
3414
3449
  evtCy.batch(() => {
3415
3450
  evtCy.elements().unselect();
3416
3451
  evtCy.$id(sourceId).select();
@@ -3438,16 +3473,16 @@ function CytoscapeGraph({
3438
3473
  extraToolbarContent,
3439
3474
  getNodeEdgeTypes
3440
3475
  }) {
3441
- const containerRef = useRef3(null);
3442
- const cyRef = useRef3(null);
3443
- const isInitializedRef = useRef3(false);
3476
+ const containerRef = useRef4(null);
3477
+ const cyRef = useRef4(null);
3478
+ const isInitializedRef = useRef4(false);
3444
3479
  const [currentLayout, setCurrentLayout] = useState6(initialLayout);
3445
3480
  const [expandRequest, setExpandRequest] = useState6(
3446
3481
  null
3447
3482
  );
3448
- const nodesRef = useRef3([]);
3449
- const edgesRef = useRef3([]);
3450
- const pendingFocusActionRef = useRef3(null);
3483
+ const nodesRef = useRef4([]);
3484
+ const edgesRef = useRef4([]);
3485
+ const pendingFocusActionRef = useRef4(null);
3451
3486
  const cytoData = useCytoscapeData();
3452
3487
  const live = useLiveGraphData();
3453
3488
  const nodes = live.nodes;
@@ -3467,31 +3502,51 @@ function CytoscapeGraph({
3467
3502
  setSelectedEdge: setSelectedEdgeStore,
3468
3503
  clear: clearSelectionStore
3469
3504
  } = useSelection3.getState();
3470
- const selectedNodeIdsRef = useRef3(
3505
+ const selectedNodeIdsRef = useRef4(
3471
3506
  useSelection3.getState().selectedNodeIds
3472
3507
  );
3473
- const selectedEdgeRef = useRef3(
3508
+ const selectedEdgeRef = useRef4(
3474
3509
  useSelection3.getState().selectedEdge
3475
3510
  );
3476
3511
  useEffect4(() => {
3477
- const state = useSelection3.getState();
3478
- selectedNodeIdsRef.current = state.selectedNodeIds;
3479
- selectedEdgeRef.current = state.selectedEdge;
3480
- const cy = cyRef.current;
3481
- if (cy && (state.selectedNodeIds.length || state.selectedEdge)) {
3512
+ const applyStoreSelection = (nodeIds, edge) => {
3513
+ selectedNodeIdsRef.current = nodeIds;
3514
+ selectedEdgeRef.current = edge;
3515
+ const cy = cyRef.current;
3516
+ if (!cy) return;
3517
+ const want = new Set(nodeIds);
3518
+ const haveNodes = cy.$("node:selected");
3519
+ const have = new Set(haveNodes.map((e) => e.id()));
3520
+ const wantEdgeId = edge ? String(edge.id_) : null;
3521
+ const haveEdge = cy.$("edge:selected");
3522
+ const haveEdgeId = haveEdge.length ? haveEdge[0].id() : null;
3523
+ const nodesInSync = want.size === have.size && [...want].every((id) => have.has(id));
3524
+ const edgeInSync = wantEdgeId === haveEdgeId;
3525
+ if (nodesInSync && edgeInSync) return;
3482
3526
  cy.batch(() => {
3483
- cy.elements().unselect();
3484
- state.selectedNodeIds.forEach((id) => cy.$id(id).select());
3485
- if (state.selectedEdge) {
3486
- const edge = cy.$id(String(state.selectedEdge.id_));
3487
- if (edge) {
3488
- edge.select();
3489
- cy.$id(edge.data("source")).select();
3490
- cy.$id(edge.data("target")).select();
3527
+ haveNodes.forEach((ele) => {
3528
+ if (!want.has(ele.id())) ele.unselect();
3529
+ });
3530
+ if (haveEdgeId && haveEdgeId !== wantEdgeId) haveEdge.unselect();
3531
+ nodeIds.forEach((id) => {
3532
+ if (!have.has(id)) cy.$id(id).select();
3533
+ });
3534
+ if (wantEdgeId && wantEdgeId !== haveEdgeId) {
3535
+ const edgeEle = cy.$id(wantEdgeId);
3536
+ if (edgeEle) {
3537
+ edgeEle.select();
3538
+ cy.$id(edgeEle.data("source")).select();
3539
+ cy.$id(edgeEle.data("target")).select();
3491
3540
  }
3492
3541
  }
3493
3542
  });
3494
- }
3543
+ };
3544
+ const initial = useSelection3.getState();
3545
+ applyStoreSelection(initial.selectedNodeIds, initial.selectedEdge);
3546
+ const unsubscribe = useSelection3.subscribe((state) => {
3547
+ applyStoreSelection(state.selectedNodeIds, state.selectedEdge);
3548
+ });
3549
+ return unsubscribe;
3495
3550
  }, [useSelection3]);
3496
3551
  useEffect4(() => {
3497
3552
  nodesRef.current = nodes;
@@ -3505,8 +3560,10 @@ function CytoscapeGraph({
3505
3560
  cyRef.current = cytoscape({
3506
3561
  container: containerRef.current,
3507
3562
  elements: [],
3508
- selectionType: "additive",
3509
- // allow clicking multiple nodes without modifier
3563
+ // Plain click selects a single node (replaces the selection); hold
3564
+ // Shift/Cmd to add a second node (e.g. for the two-node edge action).
3565
+ // This matches the conventional canvas-selection expectation.
3566
+ selectionType: "single",
3510
3567
  style: [
3511
3568
  // Base node style with data mappers for dynamic properties
3512
3569
  {
@@ -3600,19 +3657,20 @@ function CytoscapeGraph({
3600
3657
  {
3601
3658
  selector: "edge:selected",
3602
3659
  style: {
3603
- "line-color": "#ff1a1a",
3604
- "target-arrow-color": "#ff1a1a",
3605
- width: 8,
3606
- // even thicker
3660
+ "line-color": SELECTION_COLOR,
3661
+ "target-arrow-color": SELECTION_COLOR,
3662
+ // Modest bump over the base width (~3); selection is conveyed mainly
3663
+ // by colour + overlay, not a slab. (Was 8, which looked oversized.)
3664
+ width: 4,
3607
3665
  "z-index": 999,
3608
3666
  "font-size": "11px",
3609
3667
  "font-weight": "bold",
3610
3668
  "text-background-opacity": 1,
3611
3669
  "text-background-color": "#ffffff",
3612
3670
  "text-background-padding": "3px",
3613
- "arrow-scale": 1.35,
3671
+ "arrow-scale": 0.9,
3614
3672
  opacity: 1,
3615
- "overlay-color": "#ff1a1a",
3673
+ "overlay-color": SELECTION_COLOR,
3616
3674
  "overlay-opacity": 0.08
3617
3675
  }
3618
3676
  },
@@ -3621,13 +3679,13 @@ function CytoscapeGraph({
3621
3679
  style: {
3622
3680
  "border-width": 10,
3623
3681
  // thicker circle outline
3624
- "border-color": "#ff1a1a",
3682
+ "border-color": SELECTION_COLOR,
3625
3683
  "border-opacity": 1,
3626
3684
  "background-opacity": 1,
3627
3685
  "text-background-opacity": 1,
3628
3686
  "text-background-color": "#ffffff",
3629
3687
  "text-background-padding": "4px",
3630
- "overlay-color": "#ff1a1a",
3688
+ "overlay-color": SELECTION_COLOR,
3631
3689
  "overlay-opacity": 0.18,
3632
3690
  width: 48,
3633
3691
  // slight size bump from 40
@@ -3659,9 +3717,9 @@ function CytoscapeGraph({
3659
3717
  cyRef.current = null;
3660
3718
  };
3661
3719
  }, []);
3662
- const elementIdsRef = useRef3("");
3663
- const lastLayoutRef = useRef3("");
3664
- const lastFitPaddingRef = useRef3(fitPadding);
3720
+ const elementIdsRef = useRef4("");
3721
+ const lastLayoutRef = useRef4("");
3722
+ const lastFitPaddingRef = useRef4(fitPadding);
3665
3723
  useEffect4(() => {
3666
3724
  const cy = cyRef.current;
3667
3725
  if (!cy || !isInitializedRef.current) return;
@@ -3759,7 +3817,7 @@ function CytoscapeGraph({
3759
3817
  const updateLayout = (newLayout) => {
3760
3818
  setCurrentLayout(newLayout);
3761
3819
  };
3762
- const fitToScreen = useCallback4(() => {
3820
+ const fitToScreen = useCallback5(() => {
3763
3821
  if (cyRef.current) {
3764
3822
  cyRef.current.fit(void 0, fitPadding);
3765
3823
  }
@@ -3816,9 +3874,9 @@ function CytoscapeGraph({
3816
3874
  extraToolbarContent,
3817
3875
  zoomIn: () => {
3818
3876
  if (cyRef.current) {
3819
- const z = cyRef.current.zoom();
3877
+ const z3 = cyRef.current.zoom();
3820
3878
  cyRef.current.zoom({
3821
- level: Math.min(z * 1.2, 5),
3879
+ level: Math.min(z3 * 1.2, 5),
3822
3880
  renderedPosition: {
3823
3881
  x: cyRef.current.width() / 2,
3824
3882
  y: cyRef.current.height() / 2
@@ -3828,9 +3886,9 @@ function CytoscapeGraph({
3828
3886
  },
3829
3887
  zoomOut: () => {
3830
3888
  if (cyRef.current) {
3831
- const z = cyRef.current.zoom();
3889
+ const z3 = cyRef.current.zoom();
3832
3890
  cyRef.current.zoom({
3833
- level: Math.max(z / 1.2, 0.1),
3891
+ level: Math.max(z3 / 1.2, 0.1),
3834
3892
  renderedPosition: {
3835
3893
  x: cyRef.current.width() / 2,
3836
3894
  y: cyRef.current.height() / 2
@@ -4028,18 +4086,11 @@ function NodeLabelBadges({
4028
4086
  labels,
4029
4087
  labelCounts,
4030
4088
  colorPalette,
4031
- allLabelsDisabled,
4032
- nodeLabelFilters,
4033
- toggleNodeLabelFilter
4089
+ shown,
4090
+ onToggleLabel
4034
4091
  }) {
4035
4092
  return labels.map((label) => {
4036
- const bgColor = resolveBadgeColor(
4037
- allLabelsDisabled,
4038
- nodeLabelFilters,
4039
- label,
4040
- colorPalette,
4041
- label
4042
- );
4093
+ const bgColor = shown.has(label) ? colorPalette[label] : colorPalette["faded"];
4043
4094
  return /* @__PURE__ */ jsxs12(
4044
4095
  Badge,
4045
4096
  {
@@ -4049,7 +4100,7 @@ function NodeLabelBadges({
4049
4100
  backgroundColor: bgColor,
4050
4101
  borderRadius: "10px"
4051
4102
  },
4052
- onClick: () => toggleNodeLabelFilter(label, labels),
4103
+ onClick: () => onToggleLabel(label),
4053
4104
  children: [
4054
4105
  label,
4055
4106
  " (",
@@ -4099,12 +4150,53 @@ function EdgeLabelBadges({
4099
4150
  );
4100
4151
  });
4101
4152
  }
4153
+ function bucketCount(labels, labelCounts) {
4154
+ return labels.reduce((sum, l) => sum + (labelCounts[l] ?? 0), 0);
4155
+ }
4156
+ function effectiveShownNodeLabels(allLabels, allLabelsDisabled, nodeLabelFilters) {
4157
+ if (allLabelsDisabled) return /* @__PURE__ */ new Set();
4158
+ if (nodeLabelFilters.length === 0) return new Set(allLabels);
4159
+ return new Set(nodeLabelFilters);
4160
+ }
4161
+ function isBucketAllOff(bucketLabels, shown) {
4162
+ return bucketLabels.every((l) => !shown.has(l));
4163
+ }
4164
+ function commitShownNodeLabels(shown, allLabels, setNodeFilterState) {
4165
+ if (shown.size === 0) {
4166
+ setNodeFilterState(true, []);
4167
+ } else {
4168
+ setNodeFilterState(
4169
+ false,
4170
+ shown.size === allLabels.length ? [] : [...shown]
4171
+ );
4172
+ }
4173
+ }
4174
+ function toggleBucketAll(bucketLabels, allLabels, shown, setNodeFilterState) {
4175
+ const next = new Set(shown);
4176
+ if (isBucketAllOff(bucketLabels, next)) {
4177
+ for (const l of bucketLabels) next.add(l);
4178
+ } else {
4179
+ for (const l of bucketLabels) next.delete(l);
4180
+ }
4181
+ commitShownNodeLabels(next, allLabels, setNodeFilterState);
4182
+ }
4183
+ function toggleNodeLabelScoped(label, allLabels, shown, setNodeFilterState) {
4184
+ const next = new Set(shown);
4185
+ if (next.has(label)) {
4186
+ next.delete(label);
4187
+ } else {
4188
+ next.add(label);
4189
+ }
4190
+ commitShownNodeLabels(next, allLabels, setNodeFilterState);
4191
+ }
4102
4192
  function GraphOverview({
4103
4193
  stats,
4104
4194
  colorPalette,
4105
4195
  labelCounts,
4106
4196
  edgeLabelCounts,
4107
- edgeToNodeLabels
4197
+ edgeToNodeLabels,
4198
+ nodeBuckets,
4199
+ edgeSectionTitle = "Relations"
4108
4200
  }) {
4109
4201
  const useGraphFilter2 = useWorkspaceGraphFilter();
4110
4202
  const nodeLabelFilters = useGraphFilter2((state) => state.nodeLabelFilters);
@@ -4113,24 +4205,15 @@ function GraphOverview({
4113
4205
  const allEdgeLabelsDisabled = useGraphFilter2(
4114
4206
  (state) => state.allEdgeLabelsDisabled
4115
4207
  );
4116
- const toggleNodeLabelFilter = useGraphFilter2(
4117
- (state) => state.toggleNodeLabelFilter
4118
- );
4119
- const clearNodeLabelFilters = useGraphFilter2(
4120
- (state) => state.clearNodeLabelFilters
4121
- );
4122
- const toggleAllLabelsDisabled = useGraphFilter2(
4123
- (state) => state.toggleAllLabelsDisabled
4124
- );
4125
4208
  const toggleEdgeLabelFilter = useGraphFilter2(
4126
4209
  (state) => state.toggleEdgeLabelFilter
4127
4210
  );
4128
- const clearEdgeLabelFilters = useGraphFilter2(
4129
- (state) => state.clearEdgeLabelFilters
4130
- );
4131
4211
  const toggleAllEdgeLabelsDisabled = useGraphFilter2(
4132
4212
  (state) => state.toggleAllEdgeLabelsDisabled
4133
4213
  );
4214
+ const setNodeFilterState = useGraphFilter2(
4215
+ (state) => state.setNodeFilterState
4216
+ );
4134
4217
  const allLabels = Object.keys(labelCounts).sort();
4135
4218
  devLog11("[Overview] Rendering labels", {
4136
4219
  labels: allLabels,
@@ -4139,52 +4222,63 @@ function GraphOverview({
4139
4222
  stats
4140
4223
  });
4141
4224
  const allEdgeLabels = Object.keys(edgeLabelCounts || {}).sort();
4142
- const handleNodeToggle = () => {
4143
- if (allLabelsDisabled) {
4144
- toggleAllLabelsDisabled();
4145
- clearNodeLabelFilters();
4146
- } else {
4147
- toggleAllLabelsDisabled();
4148
- }
4149
- };
4150
- const handleEdgeToggle = () => {
4151
- if (allEdgeLabelsDisabled) {
4152
- toggleAllEdgeLabelsDisabled();
4153
- clearEdgeLabelFilters();
4154
- } else {
4155
- toggleAllEdgeLabelsDisabled();
4156
- }
4157
- };
4225
+ const buckets = nodeBuckets ? nodeBuckets.map((b) => ({
4226
+ title: b.title,
4227
+ labels: b.labels.filter((l) => l in labelCounts).sort()
4228
+ })) : [{ title: "Node Labels", labels: allLabels }];
4229
+ const shownNodeLabels = effectiveShownNodeLabels(
4230
+ allLabels,
4231
+ allLabelsDisabled,
4232
+ nodeLabelFilters
4233
+ );
4234
+ const handleEdgeToggle = toggleAllEdgeLabelsDisabled;
4158
4235
  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(
4164
- AllToggleBadge,
4165
- {
4166
- allDisabled: allLabelsDisabled,
4167
- colorPalette,
4168
- count: stats.nodes,
4169
- onToggle: handleNodeToggle,
4170
- badgeKey: "all"
4171
- }
4172
- ),
4173
- /* @__PURE__ */ jsx13(
4174
- NodeLabelBadges,
4175
- {
4176
- labels: allLabels,
4177
- labelCounts,
4178
- colorPalette,
4179
- allLabelsDisabled,
4180
- nodeLabelFilters,
4181
- toggleNodeLabelFilter
4182
- }
4183
- )
4184
- ] })
4185
- ] }),
4236
+ /* @__PURE__ */ jsx13("p", { className: "mt-2 heading-component", children: "Overview" }),
4237
+ allLabels.length > 0 && /* @__PURE__ */ jsx13("div", { children: buckets.map((bucket) => {
4238
+ if (bucket.labels.length === 0) return null;
4239
+ const bucketAllDisabled = isBucketAllOff(
4240
+ bucket.labels,
4241
+ shownNodeLabels
4242
+ );
4243
+ const onBucketAll = () => toggleBucketAll(
4244
+ bucket.labels,
4245
+ allLabels,
4246
+ shownNodeLabels,
4247
+ setNodeFilterState
4248
+ );
4249
+ return /* @__PURE__ */ jsxs12("div", { className: "mb-2", children: [
4250
+ /* @__PURE__ */ jsx13("p", { className: "heading-meta mb-2", children: bucket.title }),
4251
+ /* @__PURE__ */ jsxs12("div", { className: "flex flex-col items-start gap-2", children: [
4252
+ /* @__PURE__ */ jsx13(
4253
+ AllToggleBadge,
4254
+ {
4255
+ allDisabled: bucketAllDisabled,
4256
+ colorPalette,
4257
+ count: bucketCount(bucket.labels, labelCounts),
4258
+ onToggle: onBucketAll,
4259
+ badgeKey: `all-${bucket.title}`
4260
+ }
4261
+ ),
4262
+ /* @__PURE__ */ jsx13(
4263
+ NodeLabelBadges,
4264
+ {
4265
+ labels: bucket.labels,
4266
+ labelCounts,
4267
+ colorPalette,
4268
+ shown: shownNodeLabels,
4269
+ onToggleLabel: (label) => toggleNodeLabelScoped(
4270
+ label,
4271
+ allLabels,
4272
+ shownNodeLabels,
4273
+ setNodeFilterState
4274
+ )
4275
+ }
4276
+ )
4277
+ ] })
4278
+ ] }, bucket.title);
4279
+ }) }),
4186
4280
  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" }),
4281
+ /* @__PURE__ */ jsx13("p", { className: "heading-meta mb-2", children: edgeSectionTitle }),
4188
4282
  /* @__PURE__ */ jsxs12("div", { className: "flex flex-col items-start gap-2", children: [
4189
4283
  /* @__PURE__ */ jsx13(
4190
4284
  AllToggleBadge,
@@ -4212,7 +4306,7 @@ function GraphOverview({
4212
4306
  ] })
4213
4307
  ] });
4214
4308
  }
4215
- function Overview() {
4309
+ function Overview({ nodeBuckets, edgeSectionTitle } = {}) {
4216
4310
  const { stats, colorPalette, labelCounts, edgeLabelCounts, baseNormalized } = useLiveGraphData();
4217
4311
  const edgeToNodeLabels = useMemo4(() => {
4218
4312
  if (!baseNormalized?.nodesById || !baseNormalized?.edgesById) return {};
@@ -4223,7 +4317,7 @@ function Overview() {
4223
4317
  }, [baseNormalized]);
4224
4318
  if (stats && stats.values != null && stats.values > 0) {
4225
4319
  return /* @__PURE__ */ jsxs12(Fragment3, { children: [
4226
- /* @__PURE__ */ jsx13("p", { className: "mt-2 text-sm font-semibold", children: "Overview" }),
4320
+ /* @__PURE__ */ jsx13("p", { className: "mt-2 heading-component", children: "Overview" }),
4227
4321
  /* @__PURE__ */ jsxs12("p", { className: "text-sm text-muted-foreground", children: [
4228
4322
  formatNumber(stats.values),
4229
4323
  " Values"
@@ -4238,7 +4332,9 @@ function Overview() {
4238
4332
  colorPalette,
4239
4333
  labelCounts,
4240
4334
  edgeLabelCounts,
4241
- edgeToNodeLabels
4335
+ edgeToNodeLabels,
4336
+ nodeBuckets,
4337
+ edgeSectionTitle
4242
4338
  }
4243
4339
  );
4244
4340
  }
@@ -4284,11 +4380,12 @@ function ActionPanel({ actions }) {
4284
4380
  var ActionPanel_default = ActionPanel;
4285
4381
 
4286
4382
  // src/components/GraphA11yList/GraphA11yList.tsx
4287
- import { useMemo as useMemo6, useRef as useRef4 } from "react";
4383
+ import { useCallback as useCallback6, useMemo as useMemo6, useRef as useRef5 } from "react";
4288
4384
  import { useVirtualizer } from "@tanstack/react-virtual";
4289
4385
  import { cn as cn2 } from "@petrarca/sonnet-core";
4290
4386
  import { jsx as jsx15 } from "react/jsx-runtime";
4291
4387
  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";
4388
+ var MAX_NODE_SELECTION = 2;
4292
4389
  function GraphA11yList({
4293
4390
  visible = false,
4294
4391
  rowHeight = 28,
@@ -4322,28 +4419,46 @@ function GraphA11yList({
4322
4419
  }));
4323
4420
  return [...nodeRows, ...edgeRows];
4324
4421
  }, [live.nodes, live.edges]);
4325
- const parentRef = useRef4(null);
4422
+ const parentRef = useRef5(null);
4326
4423
  const virtualizer = useVirtualizer({
4327
4424
  count: rows.length,
4328
4425
  getScrollElement: () => parentRef.current,
4329
4426
  estimateSize: () => rowHeight,
4330
4427
  overscan: 8
4331
4428
  });
4332
- const selectRow = (row) => {
4333
- if (row.kind === "node") {
4429
+ const activateNode = useCallback6(
4430
+ (id, additive) => {
4334
4431
  setSelectedEdge(null);
4335
- setSelectedNodeIds([row.id]);
4336
- } else {
4337
- setSelectedNodeIds([]);
4338
- setSelectedEdge(row.edge);
4339
- }
4340
- };
4432
+ if (!additive) {
4433
+ setSelectedNodeIds([id]);
4434
+ return;
4435
+ }
4436
+ if (selectedNodeIds.includes(id)) {
4437
+ setSelectedNodeIds(selectedNodeIds.filter((x) => x !== id));
4438
+ return;
4439
+ }
4440
+ setSelectedNodeIds([...selectedNodeIds, id].slice(-MAX_NODE_SELECTION));
4441
+ },
4442
+ [selectedNodeIds, setSelectedNodeIds, setSelectedEdge]
4443
+ );
4444
+ const selectRow = useCallback6(
4445
+ (row, e) => {
4446
+ if (row.kind === "node") {
4447
+ activateNode(row.id, e.metaKey || e.ctrlKey || e.shiftKey);
4448
+ } else {
4449
+ setSelectedNodeIds([]);
4450
+ setSelectedEdge(row.edge);
4451
+ }
4452
+ },
4453
+ [activateNode, setSelectedNodeIds, setSelectedEdge]
4454
+ );
4341
4455
  const isSelected = (row) => row.kind === "node" ? selectedNodeIds.includes(row.id) : selectedEdge?.id_ === row.edge.id_;
4342
4456
  return /* @__PURE__ */ jsx15(
4343
4457
  "div",
4344
4458
  {
4345
4459
  role: "listbox",
4346
4460
  "aria-label": "Graph elements",
4461
+ "aria-multiselectable": true,
4347
4462
  className: cn2(visible ? "rounded-md border" : SR_ONLY, className),
4348
4463
  children: /* @__PURE__ */ jsx15(
4349
4464
  "div",
@@ -4371,7 +4486,7 @@ function GraphA11yList({
4371
4486
  "aria-label": `${row.kind === "node" ? "Node" : "Edge"} ${row.label}`,
4372
4487
  "data-testid": `graph-${row.kind}-${row.id}`,
4373
4488
  ...row.kind === "node" ? { "data-graph-node-id": row.id } : { "data-graph-edge-id": row.id },
4374
- onClick: () => selectRow(row),
4489
+ onClick: (e) => selectRow(row, e),
4375
4490
  className: cn2(
4376
4491
  "absolute left-0 top-0 flex w-full items-center px-2 text-left text-sm",
4377
4492
  visible && "hover:bg-accent aria-selected:bg-state-selected aria-selected:text-foreground"
@@ -4392,46 +4507,2977 @@ function GraphA11yList({
4392
4507
  }
4393
4508
  );
4394
4509
  }
4510
+
4511
+ // src/graph-model/types.ts
4512
+ function isNodeType(schema) {
4513
+ return schema["x-graph"].type !== "EDGE";
4514
+ }
4515
+ function isEdgeType(schema) {
4516
+ return schema["x-graph"].type === "EDGE";
4517
+ }
4518
+ function typeLabel(schema) {
4519
+ return schema["x-graph"].label || schema.title || "Unknown";
4520
+ }
4521
+
4522
+ // src/graph-model/converter.ts
4523
+ function stableId(seed) {
4524
+ let h = 2166136261;
4525
+ for (let i = 0; i < seed.length; i++) {
4526
+ h ^= seed.charCodeAt(i);
4527
+ h = Math.imul(h, 16777619);
4528
+ }
4529
+ return (h & 2147483647) >>> 0;
4530
+ }
4531
+ function nodeId(typeName) {
4532
+ return stableId(`schema.node.${typeName}`);
4533
+ }
4534
+ function edgeId(source, target, rel) {
4535
+ return stableId(`schema.edge.${source}.${target}.${rel}`);
4536
+ }
4537
+ function toCamelCase(key) {
4538
+ const parts = key.split("-");
4539
+ return parts[0] + parts.slice(1).map((w) => w ? w[0].toUpperCase() + w.slice(1) : "").join("");
4540
+ }
4541
+ function buildNode(schema) {
4542
+ const name = typeLabel(schema);
4543
+ const properties = {
4544
+ gid_: `schema:${name}`,
4545
+ name,
4546
+ type: schema.type ?? "object",
4547
+ propertyDefinitions: schema.properties ?? {},
4548
+ required: schema.required ?? []
4549
+ };
4550
+ for (const key of Object.keys(schema)) {
4551
+ if (key.startsWith("x-")) {
4552
+ properties[toCamelCase(key)] = schema[key];
4553
+ }
4554
+ }
4555
+ return {
4556
+ id_: nodeId(name),
4557
+ // Type label drives legend colour/filtering; node vs edge type is in xGraph.
4558
+ label_: name,
4559
+ properties_: properties
4560
+ };
4561
+ }
4562
+ function buildRelationEdges(schema, knownTypes) {
4563
+ const source = typeLabel(schema);
4564
+ const sourceId = nodeId(source);
4565
+ const edges = [];
4566
+ for (const relation of schema["x-graph"].relations ?? []) {
4567
+ const target = relation.to_type_name;
4568
+ if (!target || !knownTypes.has(target)) continue;
4569
+ const rel = relation.rel_type_name || "RELATED_TO";
4570
+ const properties = {
4571
+ gid_: `schema:rel:${source}\0${target}`,
4572
+ name: rel,
4573
+ cardinality: relation.cardinality ?? "ONE_TO_MANY"
4574
+ };
4575
+ if (relation.form_field !== void 0)
4576
+ properties.form_field = relation.form_field;
4577
+ if (relation.description !== void 0)
4578
+ properties.description = relation.description;
4579
+ edges.push({
4580
+ id_: edgeId(source, target, rel),
4581
+ label_: rel,
4582
+ start_id_: sourceId,
4583
+ end_id_: nodeId(target),
4584
+ properties_: properties
4585
+ });
4586
+ }
4587
+ return edges;
4588
+ }
4589
+ function buildInheritanceEdges(schema, knownTypes) {
4590
+ const source = typeLabel(schema);
4591
+ const sourceId = nodeId(source);
4592
+ const edges = [];
4593
+ for (const parent of schema["x-graph"].inherits_from ?? []) {
4594
+ if (!knownTypes.has(parent)) continue;
4595
+ edges.push({
4596
+ id_: edgeId(source, parent, "INHERITS_FROM_"),
4597
+ label_: "INHERITS_FROM_",
4598
+ start_id_: sourceId,
4599
+ end_id_: nodeId(parent),
4600
+ properties_: {
4601
+ gid_: `schema:rel:${source}\0${parent}`,
4602
+ name: "INHERITS_FROM_",
4603
+ parent
4604
+ }
4605
+ });
4606
+ }
4607
+ return edges;
4608
+ }
4609
+ function modelToGraph(model) {
4610
+ const nodeTypes = model.filter(isNodeType);
4611
+ const knownTypes = new Set(nodeTypes.map(typeLabel));
4612
+ const nodes = model.map(buildNode);
4613
+ const edges = [];
4614
+ for (const schema of nodeTypes) {
4615
+ edges.push(...buildRelationEdges(schema, knownTypes));
4616
+ edges.push(...buildInheritanceEdges(schema, knownTypes));
4617
+ }
4618
+ return { nodes, edges };
4619
+ }
4620
+
4621
+ // src/graph-model/filter.ts
4622
+ function reconcileFilter(current, model, baselineReset, showEdgeTypes) {
4623
+ const nodeTypeLabels2 = model.filter(isNodeType).map(typeLabel);
4624
+ if (baselineReset) {
4625
+ return showEdgeTypes ? [] : nodeTypeLabels2;
4626
+ }
4627
+ if (current.length === 0) return [];
4628
+ const allLabels = new Set(model.map(typeLabel));
4629
+ const kept = current.filter((l) => allLabels.has(l));
4630
+ const keptSet = new Set(kept);
4631
+ const added = nodeTypeLabels2.filter((l) => !keptSet.has(l));
4632
+ return added.length > 0 ? [...kept, ...added] : kept;
4633
+ }
4634
+
4635
+ // src/graph-model/GraphModelEditor.tsx
4636
+ import { useCallback as useCallback7, useEffect as useEffect5, useRef as useRef6, useState as useState13 } from "react";
4637
+ import { ResizeHandle } from "@petrarca/sonnet-ui";
4638
+
4639
+ // src/graph-model/ModelActionPanel.tsx
4640
+ import { useMemo as useMemo8, useState as useState12 } from "react";
4641
+ import { Button as Button6, IconButton as IconButton2 } from "@petrarca/sonnet-ui";
4642
+ import { GitBranchPlus, CirclePlus, Waypoints } from "lucide-react";
4643
+
4644
+ // src/graph-model/TypeDetailPanel.tsx
4645
+ import { useState as useState11 } from "react";
4646
+ import { Badge as Badge2, IconButton } from "@petrarca/sonnet-ui";
4647
+ import { Plus, Pencil, Trash2 } from "lucide-react";
4648
+
4649
+ // src/graph-model/view.ts
4650
+ function cardinalityLabel(c) {
4651
+ return c === "ONE_TO_ONE" ? "one-to-one" : "one-to-many";
4652
+ }
4653
+ function propTypeOf(prop) {
4654
+ if (Array.isArray(prop.type)) return prop.type.join(" | ");
4655
+ return prop.type ?? "object";
4656
+ }
4657
+ function toPropertyViews(schema) {
4658
+ if (!schema?.properties) return [];
4659
+ const required = new Set(schema.required ?? []);
4660
+ return Object.entries(schema.properties).map(([name, prop]) => ({
4661
+ name,
4662
+ type: propTypeOf(prop),
4663
+ required: required.has(name),
4664
+ title: typeof prop.title === "string" ? prop.title : void 0,
4665
+ description: typeof prop.description === "string" ? prop.description : void 0,
4666
+ widget: typeof prop["x-ui-widget"] === "string" ? prop["x-ui-widget"] : void 0,
4667
+ enum: prop.enum
4668
+ }));
4669
+ }
4670
+ function toRelationViews(schema) {
4671
+ const relations = schema["x-graph"].relations ?? [];
4672
+ return relations.map((r) => ({
4673
+ relType: r.rel_type_name,
4674
+ target: r.to_type_name,
4675
+ cardinality: r.cardinality ?? "ONE_TO_MANY",
4676
+ description: r.description
4677
+ }));
4678
+ }
4679
+ function findType(model, label) {
4680
+ return model.find((s) => typeLabel(s) === label);
4681
+ }
4682
+ function findEdgeType(model, relType) {
4683
+ return model.find(
4684
+ (s) => s["x-graph"].type === "EDGE" && typeLabel(s) === relType
4685
+ );
4686
+ }
4687
+ function resolveRelationEdge(edge) {
4688
+ if (edge.label_ === "INHERITS_FROM_") return null;
4689
+ const gid = edge.properties_?.gid_;
4690
+ if (typeof gid !== "string" || !gid.startsWith("schema:rel:")) return null;
4691
+ const rest = gid.slice("schema:rel:".length);
4692
+ const sep = rest.indexOf("\0");
4693
+ if (sep < 0) return null;
4694
+ return {
4695
+ source: rest.slice(0, sep),
4696
+ relType: edge.label_,
4697
+ target: rest.slice(sep + 1)
4698
+ };
4699
+ }
4700
+ function nodeTypeView(model, label) {
4701
+ const schema = findType(model, label);
4702
+ if (!schema || !isNodeType(schema)) return null;
4703
+ return {
4704
+ kind: "node",
4705
+ label: typeLabel(schema),
4706
+ description: typeof schema.description === "string" ? schema.description : void 0,
4707
+ properties: toPropertyViews(schema),
4708
+ relations: toRelationViews(schema)
4709
+ };
4710
+ }
4711
+ function edgeTypeView(model, label) {
4712
+ const schema = findType(model, label);
4713
+ if (!schema || isNodeType(schema)) return null;
4714
+ return {
4715
+ kind: "edge",
4716
+ label: typeLabel(schema),
4717
+ description: typeof schema.description === "string" ? schema.description : void 0,
4718
+ properties: toPropertyViews(schema)
4719
+ };
4720
+ }
4721
+ function relationView(model, source, relType, target) {
4722
+ const sourceSchema = findType(model, source);
4723
+ if (!sourceSchema || !isNodeType(sourceSchema)) return null;
4724
+ const relation = (sourceSchema["x-graph"].relations ?? []).find(
4725
+ (r) => r.rel_type_name === relType && r.to_type_name === target
4726
+ );
4727
+ if (!relation) return null;
4728
+ const edgeType = findEdgeType(model, relType);
4729
+ return {
4730
+ kind: "relation",
4731
+ relType,
4732
+ source,
4733
+ target,
4734
+ cardinality: relation.cardinality ?? "ONE_TO_MANY",
4735
+ description: relation.description,
4736
+ hasEdgeType: !!edgeType,
4737
+ properties: toPropertyViews(edgeType)
4738
+ };
4739
+ }
4740
+
4741
+ // src/graph-model/mutate.ts
4742
+ function toSchemaProperty(input) {
4743
+ const prop = { type: input.type };
4744
+ if (input.title) prop.title = input.title;
4745
+ if (input.description) prop.description = input.description;
4746
+ if (input.widget && input.widget !== "default")
4747
+ prop["x-ui-widget"] = input.widget;
4748
+ if (input.enum && input.enum.length) prop.enum = input.enum;
4749
+ return prop;
4750
+ }
4751
+ function replaceSchema(model, label, next) {
4752
+ return model.map((s) => typeLabel(s) === label ? next : s);
4753
+ }
4754
+ function withProperty(schema, input, prevName) {
4755
+ const properties = {
4756
+ ...schema.properties ?? {}
4757
+ };
4758
+ const required = new Set(schema.required ?? []);
4759
+ if (prevName) {
4760
+ delete properties[prevName];
4761
+ required.delete(prevName);
4762
+ }
4763
+ if (input) {
4764
+ properties[input.name] = toSchemaProperty(input);
4765
+ if (input.required) required.add(input.name);
4766
+ else required.delete(input.name);
4767
+ }
4768
+ const next = { ...schema, properties };
4769
+ if (required.size) next.required = [...required];
4770
+ else delete next.required;
4771
+ return next;
4772
+ }
4773
+ function setNodeProperty(model, typeLabel_, input, prevName) {
4774
+ const schema = findType(model, typeLabel_);
4775
+ if (!schema) return model;
4776
+ return replaceSchema(
4777
+ model,
4778
+ typeLabel_,
4779
+ withProperty(schema, input, prevName)
4780
+ );
4781
+ }
4782
+ function deleteNodeProperty(model, typeLabel_, name) {
4783
+ const schema = findType(model, typeLabel_);
4784
+ if (!schema) return model;
4785
+ return replaceSchema(model, typeLabel_, withProperty(schema, null, name));
4786
+ }
4787
+ function newEdgeType(relType) {
4788
+ return {
4789
+ title: relType,
4790
+ type: "object",
4791
+ properties: {},
4792
+ "x-graph": { type: "EDGE", label: relType }
4793
+ };
4794
+ }
4795
+ function setRelationProperty(model, relType, input, prevName) {
4796
+ const existing = findEdgeType(model, relType);
4797
+ const base = existing ?? newEdgeType(relType);
4798
+ const next = withProperty(base, input, prevName);
4799
+ return existing ? replaceSchema(model, relType, next) : [...model, next];
4800
+ }
4801
+ function deleteRelationProperty(model, relType, name) {
4802
+ const existing = findEdgeType(model, relType);
4803
+ if (!existing) return model;
4804
+ const next = withProperty(existing, null, name);
4805
+ const empty = Object.keys(next.properties ?? {}).length === 0;
4806
+ if (empty) return model.filter((s) => s !== existing);
4807
+ return replaceSchema(model, relType, next);
4808
+ }
4809
+ function setEdgeTypeProperty(model, edgeLabel, input, prevName) {
4810
+ const existing = findEdgeType(model, edgeLabel);
4811
+ if (!existing) return model;
4812
+ return replaceSchema(
4813
+ model,
4814
+ edgeLabel,
4815
+ withProperty(existing, input, prevName)
4816
+ );
4817
+ }
4818
+ function deleteEdgeTypeProperty(model, edgeLabel, name) {
4819
+ const existing = findEdgeType(model, edgeLabel);
4820
+ if (!existing) return model;
4821
+ return replaceSchema(model, edgeLabel, withProperty(existing, null, name));
4822
+ }
4823
+ function addEdgeType(model, label) {
4824
+ const name = label.trim();
4825
+ if (!name || model.some((s) => typeLabel(s) === name)) return model;
4826
+ return [...model, newEdgeType(name)];
4827
+ }
4828
+ function addRelation(model, source, input) {
4829
+ const schema = findType(model, source);
4830
+ if (!schema) return model;
4831
+ const relations = schema["x-graph"].relations ?? [];
4832
+ if (relations.some(
4833
+ (r) => r.rel_type_name === input.relType && r.to_type_name === input.target
4834
+ )) {
4835
+ return model;
4836
+ }
4837
+ const rel = {
4838
+ rel_type_name: input.relType,
4839
+ to_type_name: input.target,
4840
+ cardinality: input.cardinality
4841
+ };
4842
+ if (input.description) rel.description = input.description;
4843
+ const next = {
4844
+ ...schema,
4845
+ "x-graph": { ...schema["x-graph"], relations: [...relations, rel] }
4846
+ };
4847
+ return replaceSchema(model, source, next);
4848
+ }
4849
+ function updateRelation(model, source, prevRelType, prevTarget, input) {
4850
+ const schema = findType(model, source);
4851
+ if (!schema) return model;
4852
+ const relations = (schema["x-graph"].relations ?? []).map(
4853
+ (r) => r.rel_type_name === prevRelType && r.to_type_name === prevTarget ? {
4854
+ rel_type_name: input.relType,
4855
+ to_type_name: input.target,
4856
+ cardinality: input.cardinality,
4857
+ ...input.description ? { description: input.description } : {}
4858
+ } : r
4859
+ );
4860
+ const next = {
4861
+ ...schema,
4862
+ "x-graph": { ...schema["x-graph"], relations }
4863
+ };
4864
+ return replaceSchema(model, source, next);
4865
+ }
4866
+ function addNodeType(model, label) {
4867
+ const name = label.trim();
4868
+ if (!name || model.some((s) => typeLabel(s) === name)) return model;
4869
+ const schema = {
4870
+ title: name,
4871
+ type: "object",
4872
+ properties: {},
4873
+ "x-graph": { type: "NODE", label: name }
4874
+ };
4875
+ return [...model, schema];
4876
+ }
4877
+ function renameNodeType(model, oldLabel, newLabel) {
4878
+ const name = newLabel.trim();
4879
+ if (!name || name === oldLabel) return model;
4880
+ if (model.some((s) => typeLabel(s) === name)) return model;
4881
+ return model.map((s) => {
4882
+ const xg = s["x-graph"];
4883
+ const isTarget = typeLabel(s) === oldLabel;
4884
+ const relations = xg.relations?.map(
4885
+ (r) => r.to_type_name === oldLabel ? { ...r, to_type_name: name } : r
4886
+ );
4887
+ const inherits = xg.inherits_from?.map((p) => p === oldLabel ? name : p);
4888
+ return {
4889
+ ...s,
4890
+ ...isTarget && s.title ? { title: name } : {},
4891
+ "x-graph": {
4892
+ ...xg,
4893
+ ...isTarget ? { label: name } : {},
4894
+ ...relations ? { relations } : {},
4895
+ ...inherits ? { inherits_from: inherits } : {}
4896
+ }
4897
+ };
4898
+ });
4899
+ }
4900
+ function deleteNodeType(model, label) {
4901
+ return model.filter((s) => typeLabel(s) !== label).map((s) => {
4902
+ const xg = s["x-graph"];
4903
+ const relations = xg.relations?.filter((r) => r.to_type_name !== label);
4904
+ const inherits = xg.inherits_from?.filter((p) => p !== label);
4905
+ return {
4906
+ ...s,
4907
+ "x-graph": {
4908
+ ...xg,
4909
+ ...relations ? { relations } : {},
4910
+ ...inherits ? { inherits_from: inherits } : {}
4911
+ }
4912
+ };
4913
+ });
4914
+ }
4915
+ function deleteRelation(model, source, relType, target) {
4916
+ const schema = findType(model, source);
4917
+ if (!schema) return model;
4918
+ const relations = (schema["x-graph"].relations ?? []).filter(
4919
+ (r) => !(r.rel_type_name === relType && r.to_type_name === target)
4920
+ );
4921
+ const next = {
4922
+ ...schema,
4923
+ "x-graph": { ...schema["x-graph"], relations }
4924
+ };
4925
+ return replaceSchema(model, source, next);
4926
+ }
4927
+ function renameEdgeType(model, oldLabel, newLabel) {
4928
+ const name = newLabel.trim();
4929
+ if (!name || name === oldLabel) return model;
4930
+ if (model.some((s) => typeLabel(s) === name)) return model;
4931
+ const edgeType = findEdgeType(model, oldLabel);
4932
+ if (!edgeType) return model;
4933
+ return model.map((s) => {
4934
+ const xg = s["x-graph"];
4935
+ const isTarget = s === edgeType;
4936
+ const relations = xg.relations?.map(
4937
+ (r) => r.rel_type_name === oldLabel ? { ...r, rel_type_name: name } : r
4938
+ );
4939
+ return {
4940
+ ...s,
4941
+ ...isTarget && s.title ? { title: name } : {},
4942
+ "x-graph": {
4943
+ ...xg,
4944
+ ...isTarget ? { label: name } : {},
4945
+ ...relations ? { relations } : {}
4946
+ }
4947
+ };
4948
+ });
4949
+ }
4950
+ function deleteEdgeType(model, label) {
4951
+ return model.filter((s) => !(isEdgeType(s) && typeLabel(s) === label));
4952
+ }
4953
+
4954
+ // src/graph-model/PropertyFormSheet.tsx
4955
+ import { useMemo as useMemo7 } from "react";
4956
+ import {
4957
+ Sheet,
4958
+ SheetContent,
4959
+ SheetHeader,
4960
+ SheetBody,
4961
+ SheetTitle,
4962
+ SheetDescription
4963
+ } from "@petrarca/sonnet-ui";
4964
+ import { JsonSchemaFormRenderer } from "@petrarca/sonnet-forms";
4965
+ import { jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
4966
+ var TYPE_OPTIONS = [
4967
+ "string",
4968
+ "integer",
4969
+ "number",
4970
+ "boolean",
4971
+ "array",
4972
+ "object"
4973
+ ];
4974
+ var DEFAULT_WIDGETS = ["default"];
4975
+ function buildSchema(widgetOptions) {
4976
+ return {
4977
+ type: "object",
4978
+ properties: {
4979
+ name: {
4980
+ type: "string",
4981
+ title: "Property name",
4982
+ description: "The JSON Schema property key."
4983
+ },
4984
+ type: { type: "string", title: "Type", enum: TYPE_OPTIONS },
4985
+ required: { type: "boolean", title: "Required" },
4986
+ title: { type: "string", title: "Title" },
4987
+ description: {
4988
+ type: "string",
4989
+ title: "Description",
4990
+ "x-ui-widget": "textarea"
4991
+ },
4992
+ widget: {
4993
+ type: "string",
4994
+ title: "Widget",
4995
+ description: "x-ui-widget -- render this property with a specific widget.",
4996
+ enum: widgetOptions
4997
+ },
4998
+ enumValues: {
4999
+ type: "array",
5000
+ title: "Allowed values",
5001
+ description: "Restrict the property to a fixed set of values (leave empty for free input).",
5002
+ items: { type: "string" },
5003
+ "x-ui-widget": "tags"
5004
+ }
5005
+ },
5006
+ required: ["name", "type"]
5007
+ };
5008
+ }
5009
+ function toFormData(prop) {
5010
+ if (!prop)
5011
+ return {
5012
+ type: "string",
5013
+ required: false,
5014
+ widget: "default",
5015
+ enumValues: []
5016
+ };
5017
+ return {
5018
+ name: prop.name,
5019
+ type: prop.type,
5020
+ required: prop.required,
5021
+ title: prop.title ?? "",
5022
+ description: prop.description ?? "",
5023
+ widget: prop.widget ?? "default",
5024
+ enumValues: prop.enum ? prop.enum.map(String) : []
5025
+ };
5026
+ }
5027
+ function toInput(data) {
5028
+ const raw = data.enumValues;
5029
+ const enumValues = Array.isArray(raw) ? raw.map(String).filter((v) => v.length > 0) : void 0;
5030
+ return {
5031
+ name: String(data.name ?? "").trim(),
5032
+ type: String(data.type ?? "string"),
5033
+ required: Boolean(data.required),
5034
+ title: data.title ? String(data.title) : void 0,
5035
+ description: data.description ? String(data.description) : void 0,
5036
+ widget: data.widget ? String(data.widget) : void 0,
5037
+ enum: enumValues && enumValues.length ? enumValues : void 0
5038
+ };
5039
+ }
5040
+ function PropertyFormSheet({
5041
+ open,
5042
+ onOpenChange,
5043
+ property,
5044
+ ownerLabel,
5045
+ widgetOptions,
5046
+ onSave
5047
+ }) {
5048
+ const widgets = useMemo7(
5049
+ () => [...DEFAULT_WIDGETS, ...widgetOptions ?? []],
5050
+ [widgetOptions]
5051
+ );
5052
+ const schema = useMemo7(() => buildSchema(widgets), [widgets]);
5053
+ const data = useMemo7(() => toFormData(property), [property]);
5054
+ const isEdit = !!property;
5055
+ return /* @__PURE__ */ jsx16(Sheet, { open, onOpenChange, children: /* @__PURE__ */ jsxs14(SheetContent, { children: [
5056
+ /* @__PURE__ */ jsxs14(SheetHeader, { children: [
5057
+ /* @__PURE__ */ jsxs14(SheetTitle, { children: [
5058
+ isEdit ? "Edit property" : "Add property",
5059
+ " ",
5060
+ "\u2014",
5061
+ " ",
5062
+ ownerLabel
5063
+ ] }),
5064
+ /* @__PURE__ */ jsx16(SheetDescription, { children: "Define the property and how it is rendered." })
5065
+ ] }),
5066
+ /* @__PURE__ */ jsx16(SheetBody, { children: /* @__PURE__ */ jsx16(
5067
+ JsonSchemaFormRenderer,
5068
+ {
5069
+ schema,
5070
+ data,
5071
+ showActions: true,
5072
+ saveLabel: isEdit ? "Save" : "Add",
5073
+ showExtraProperties: false,
5074
+ onUpdate: (d) => {
5075
+ onSave(toInput(d), property?.name);
5076
+ onOpenChange(false);
5077
+ },
5078
+ onCancel: () => onOpenChange(false)
5079
+ }
5080
+ ) })
5081
+ ] }) });
5082
+ }
5083
+
5084
+ // src/graph-model/RelationFormSheet.tsx
5085
+ import { useState as useState9 } from "react";
5086
+ import {
5087
+ Sheet as Sheet2,
5088
+ SheetContent as SheetContent2,
5089
+ SheetHeader as SheetHeader2,
5090
+ SheetBody as SheetBody2,
5091
+ SheetTitle as SheetTitle2,
5092
+ SheetDescription as SheetDescription2,
5093
+ Button as Button4
5094
+ } from "@petrarca/sonnet-ui";
5095
+ import { FormSelect } from "@petrarca/sonnet-forms";
5096
+ import { ArrowRight } from "lucide-react";
5097
+ import { jsx as jsx17, jsxs as jsxs15 } from "react/jsx-runtime";
5098
+ var CARDINALITIES = ["ONE_TO_MANY", "ONE_TO_ONE"];
5099
+ function RelationTypeField({
5100
+ relType,
5101
+ setRelType,
5102
+ edgeTypeOptions,
5103
+ edgeTypeMode
5104
+ }) {
5105
+ const labels = Array.from(
5106
+ /* @__PURE__ */ new Set([...edgeTypeOptions, ...relType ? [relType] : []])
5107
+ );
5108
+ const options = labels.map((l) => ({ value: l, label: l }));
5109
+ const canCreate = edgeTypeMode === "edit";
5110
+ return /* @__PURE__ */ jsx17(
5111
+ FormSelect,
5112
+ {
5113
+ label: "Relation type",
5114
+ options,
5115
+ value: relType || null,
5116
+ onChange: (v) => setRelType(v ?? ""),
5117
+ allowCreate: canCreate,
5118
+ onCreate: setRelType,
5119
+ clearable: false,
5120
+ placeholder: "Select an edge type",
5121
+ searchPlaceholder: canCreate ? "Search or type to create..." : "Search edge types...",
5122
+ emptyMessage: canCreate ? "Type a name to create a new edge type." : "No matching edge type."
5123
+ }
5124
+ );
5125
+ }
5126
+ function DirectionField({
5127
+ source,
5128
+ target,
5129
+ canSwap,
5130
+ onSwap
5131
+ }) {
5132
+ return /* @__PURE__ */ jsxs15("div", { children: [
5133
+ /* @__PURE__ */ jsx17("label", { className: "mb-1 block text-xs font-medium", children: "Direction" }),
5134
+ /* @__PURE__ */ jsxs15("div", { className: "flex items-center gap-2 text-sm", children: [
5135
+ /* @__PURE__ */ jsx17("span", { className: "font-medium", children: source }),
5136
+ /* @__PURE__ */ jsx17(ArrowRight, { className: "h-4 w-4 text-muted-foreground" }),
5137
+ /* @__PURE__ */ jsx17("span", { className: "font-medium", children: target }),
5138
+ canSwap && /* @__PURE__ */ jsx17(
5139
+ Button4,
5140
+ {
5141
+ variant: "ghost",
5142
+ size: "sm",
5143
+ className: "ml-auto",
5144
+ onClick: onSwap,
5145
+ children: "Swap"
5146
+ }
5147
+ )
5148
+ ] })
5149
+ ] });
5150
+ }
5151
+ function CardinalityField({
5152
+ cardinality,
5153
+ onChange
5154
+ }) {
5155
+ return /* @__PURE__ */ jsxs15("div", { children: [
5156
+ /* @__PURE__ */ jsx17("label", { className: "mb-1 block text-xs font-medium", children: "Cardinality" }),
5157
+ /* @__PURE__ */ jsx17("div", { className: "flex gap-2", children: CARDINALITIES.map((c) => /* @__PURE__ */ jsx17(
5158
+ Button4,
5159
+ {
5160
+ variant: c === cardinality ? "default" : "outline",
5161
+ size: "sm",
5162
+ onClick: () => onChange(c),
5163
+ children: cardinalityLabel(c)
5164
+ },
5165
+ c
5166
+ )) })
5167
+ ] });
5168
+ }
5169
+ function RelationFormSheet({
5170
+ open,
5171
+ onOpenChange,
5172
+ nodeA,
5173
+ nodeB,
5174
+ initial,
5175
+ edgeTypeOptions,
5176
+ edgeTypeMode = "edit",
5177
+ onSave
5178
+ }) {
5179
+ const [relType, setRelType] = useState9(initial?.relType ?? "");
5180
+ const [swapped, setSwapped] = useState9(false);
5181
+ const [cardinality, setCardinality] = useState9(
5182
+ initial?.cardinality ?? "ONE_TO_MANY"
5183
+ );
5184
+ const source = swapped ? nodeB : nodeA;
5185
+ const target = swapped ? nodeA : nodeB;
5186
+ const isEdit = !!initial;
5187
+ const canSave = relType.trim().length > 0;
5188
+ const handleSave = () => {
5189
+ if (!canSave) return;
5190
+ onSave(source, target, {
5191
+ relType: relType.trim(),
5192
+ target,
5193
+ cardinality
5194
+ });
5195
+ onOpenChange(false);
5196
+ };
5197
+ return /* @__PURE__ */ jsx17(Sheet2, { open, onOpenChange, children: /* @__PURE__ */ jsxs15(SheetContent2, { children: [
5198
+ /* @__PURE__ */ jsxs15(SheetHeader2, { children: [
5199
+ /* @__PURE__ */ jsx17(SheetTitle2, { children: isEdit ? "Edit relation" : "Create relation" }),
5200
+ /* @__PURE__ */ jsx17(SheetDescription2, { children: "A relation is stored on the source node type." })
5201
+ ] }),
5202
+ /* @__PURE__ */ jsxs15(SheetBody2, { className: "flex flex-col gap-4", children: [
5203
+ /* @__PURE__ */ jsx17(
5204
+ RelationTypeField,
5205
+ {
5206
+ relType,
5207
+ setRelType,
5208
+ edgeTypeOptions,
5209
+ edgeTypeMode
5210
+ }
5211
+ ),
5212
+ /* @__PURE__ */ jsx17(
5213
+ DirectionField,
5214
+ {
5215
+ source,
5216
+ target,
5217
+ canSwap: !isEdit,
5218
+ onSwap: () => setSwapped((s) => !s)
5219
+ }
5220
+ ),
5221
+ /* @__PURE__ */ jsx17(
5222
+ CardinalityField,
5223
+ {
5224
+ cardinality,
5225
+ onChange: setCardinality
5226
+ }
5227
+ ),
5228
+ /* @__PURE__ */ jsxs15("div", { className: "flex justify-end gap-2 pt-2", children: [
5229
+ /* @__PURE__ */ jsx17(Button4, { variant: "outline", onClick: () => onOpenChange(false), children: "Cancel" }),
5230
+ /* @__PURE__ */ jsx17(Button4, { disabled: !canSave, onClick: handleSave, children: isEdit ? "Save" : "Create relation" })
5231
+ ] })
5232
+ ] })
5233
+ ] }) });
5234
+ }
5235
+
5236
+ // src/graph-model/NodeTypeNameSheet.tsx
5237
+ import { useState as useState10 } from "react";
5238
+ import {
5239
+ Sheet as Sheet3,
5240
+ SheetContent as SheetContent3,
5241
+ SheetHeader as SheetHeader3,
5242
+ SheetBody as SheetBody3,
5243
+ SheetTitle as SheetTitle3,
5244
+ SheetDescription as SheetDescription3,
5245
+ Button as Button5,
5246
+ Input as Input2
5247
+ } from "@petrarca/sonnet-ui";
5248
+ import { jsx as jsx18, jsxs as jsxs16 } from "react/jsx-runtime";
5249
+ function NodeTypeNameSheet({
5250
+ open,
5251
+ onOpenChange,
5252
+ initial,
5253
+ existing,
5254
+ noun = "node",
5255
+ onSave
5256
+ }) {
5257
+ const [name, setName] = useState10(initial ?? "");
5258
+ const isEdit = initial !== void 0;
5259
+ const trimmed = name.trim();
5260
+ const duplicate = trimmed !== initial && existing.includes(trimmed);
5261
+ const canSave = trimmed.length > 0 && !duplicate;
5262
+ const typeNoun = noun === "edge" ? "edge type" : "node type";
5263
+ return /* @__PURE__ */ jsx18(Sheet3, { open, onOpenChange, children: /* @__PURE__ */ jsxs16(SheetContent3, { children: [
5264
+ /* @__PURE__ */ jsxs16(SheetHeader3, { children: [
5265
+ /* @__PURE__ */ jsx18(SheetTitle3, { children: isEdit ? `Rename ${typeNoun}` : `Add ${typeNoun}` }),
5266
+ /* @__PURE__ */ jsx18(SheetDescription3, { children: "The label identifies this type and its instances." })
5267
+ ] }),
5268
+ /* @__PURE__ */ jsxs16(SheetBody3, { className: "flex flex-col gap-4", children: [
5269
+ /* @__PURE__ */ jsxs16("div", { children: [
5270
+ /* @__PURE__ */ jsx18("label", { className: "mb-1 block text-xs font-medium", children: "Label" }),
5271
+ /* @__PURE__ */ jsx18(
5272
+ Input2,
5273
+ {
5274
+ value: name,
5275
+ onChange: (e) => setName(e.target.value),
5276
+ placeholder: "e.g. Hospital",
5277
+ autoFocus: true
5278
+ }
5279
+ ),
5280
+ duplicate && /* @__PURE__ */ jsx18("p", { className: "mt-1 text-xs text-destructive", children: "A type with this label already exists." })
5281
+ ] }),
5282
+ /* @__PURE__ */ jsxs16("div", { className: "flex justify-end gap-2", children: [
5283
+ /* @__PURE__ */ jsx18(Button5, { variant: "outline", onClick: () => onOpenChange(false), children: "Cancel" }),
5284
+ /* @__PURE__ */ jsx18(
5285
+ Button5,
5286
+ {
5287
+ disabled: !canSave,
5288
+ onClick: () => {
5289
+ onSave(trimmed);
5290
+ onOpenChange(false);
5291
+ },
5292
+ children: isEdit ? "Rename" : "Add"
5293
+ }
5294
+ )
5295
+ ] })
5296
+ ] })
5297
+ ] }) });
5298
+ }
5299
+
5300
+ // src/graph-model/TypeDetailPanel.tsx
5301
+ import { Fragment as Fragment4, jsx as jsx19, jsxs as jsxs17 } from "react/jsx-runtime";
5302
+ function relationViewOf(view) {
5303
+ return {
5304
+ relType: view.relType,
5305
+ target: view.target,
5306
+ cardinality: view.cardinality,
5307
+ description: view.description
5308
+ };
5309
+ }
5310
+ function SectionTitle({
5311
+ children,
5312
+ action
5313
+ }) {
5314
+ return /* @__PURE__ */ jsxs17("div", { className: "mt-4 mb-2 flex items-center gap-2", children: [
5315
+ /* @__PURE__ */ jsx19("span", { className: "heading-meta", children }),
5316
+ action && /* @__PURE__ */ jsx19("span", { className: "ml-auto", children: action })
5317
+ ] });
5318
+ }
5319
+ function PropertyRows({
5320
+ properties,
5321
+ emptyText,
5322
+ editable,
5323
+ onEdit,
5324
+ onDelete
5325
+ }) {
5326
+ if (properties.length === 0) {
5327
+ return /* @__PURE__ */ jsx19("p", { className: "text-xs text-muted-foreground", children: emptyText });
5328
+ }
5329
+ return /* @__PURE__ */ jsx19("div", { className: "rounded-lg border divide-y", children: properties.map((p) => /* @__PURE__ */ jsxs17(
5330
+ "div",
5331
+ {
5332
+ className: "group/row flex items-center gap-2 px-3 py-2",
5333
+ children: [
5334
+ /* @__PURE__ */ jsx19("span", { className: "text-sm font-medium min-w-24", children: p.name }),
5335
+ /* @__PURE__ */ jsxs17("span", { className: "flex-1 font-mono text-xs text-muted-foreground", children: [
5336
+ p.type,
5337
+ p.widget ? ` \xB7 ${p.widget}` : ""
5338
+ ] }),
5339
+ p.required && /* @__PURE__ */ jsx19(Badge2, { badgeColor: "orange", children: "required" }),
5340
+ editable && /* @__PURE__ */ jsxs17("span", { className: "flex gap-1", children: [
5341
+ /* @__PURE__ */ jsx19(
5342
+ IconButton,
5343
+ {
5344
+ label: "Edit property",
5345
+ revealOnHover: true,
5346
+ onClick: () => onEdit(p),
5347
+ children: /* @__PURE__ */ jsx19(Pencil, {})
5348
+ }
5349
+ ),
5350
+ /* @__PURE__ */ jsx19(
5351
+ IconButton,
5352
+ {
5353
+ label: "Delete property",
5354
+ tone: "destructive",
5355
+ revealOnHover: true,
5356
+ onClick: () => onDelete(p),
5357
+ children: /* @__PURE__ */ jsx19(Trash2, {})
5358
+ }
5359
+ )
5360
+ ] })
5361
+ ]
5362
+ },
5363
+ p.name
5364
+ )) });
5365
+ }
5366
+ function areaEditable(editable, mode) {
5367
+ return editable && (mode ?? "edit") === "edit";
5368
+ }
5369
+ function resolveGating(kind, editable, c) {
5370
+ const propsMode = (kind === "node" ? c?.nodeProperties : c?.edgeProperties) ?? "edit";
5371
+ const edgeTypeEditable = editable && (c?.edgeTypes ?? "edit") === "edit";
5372
+ return {
5373
+ propsHidden: propsMode === "hidden",
5374
+ propsEditable: areaEditable(editable, propsMode),
5375
+ nodeTypeEditable: areaEditable(editable, c?.nodeTypes),
5376
+ edgeTypeEditable,
5377
+ relationsEditable: areaEditable(editable, c?.relations)
5378
+ };
5379
+ }
5380
+ function resolveView(model, selection) {
5381
+ if (selection.kind === "node") return nodeTypeView(model, selection.label);
5382
+ if (selection.kind === "edge") return edgeTypeView(model, selection.label);
5383
+ return relationView(
5384
+ model,
5385
+ selection.source,
5386
+ selection.relType,
5387
+ selection.target
5388
+ );
5389
+ }
5390
+ function applySaveProperty(model, view, input, prevName) {
5391
+ if (view.kind === "node")
5392
+ return setNodeProperty(model, view.label, input, prevName);
5393
+ if (view.kind === "edge")
5394
+ return setEdgeTypeProperty(model, view.label, input, prevName);
5395
+ return setRelationProperty(model, view.relType, input, prevName);
5396
+ }
5397
+ function applyRemoveProperty(model, view, name) {
5398
+ if (view.kind === "node") return deleteNodeProperty(model, view.label, name);
5399
+ if (view.kind === "edge")
5400
+ return deleteEdgeTypeProperty(model, view.label, name);
5401
+ return deleteRelationProperty(model, view.relType, name);
5402
+ }
5403
+ function TypeDetailPanel({
5404
+ model,
5405
+ selection,
5406
+ onChange,
5407
+ readOnly,
5408
+ capabilities,
5409
+ widgetOptions
5410
+ }) {
5411
+ const [sheet, setSheet] = useState11({ open: false });
5412
+ const [relSheet, setRelSheet] = useState11(null);
5413
+ const [renameOpen, setRenameOpen] = useState11(false);
5414
+ const view = resolveView(model, selection);
5415
+ if (!view) return null;
5416
+ const editable = !readOnly && !!onChange;
5417
+ const ownerLabel = view.kind === "relation" ? view.relType : view.label;
5418
+ const {
5419
+ propsHidden,
5420
+ propsEditable,
5421
+ nodeTypeEditable,
5422
+ edgeTypeEditable,
5423
+ relationsEditable
5424
+ } = resolveGating(view.kind, editable, capabilities);
5425
+ const saveProperty = (input, prevName) => {
5426
+ if (onChange) onChange(applySaveProperty(model, view, input, prevName));
5427
+ };
5428
+ const removeProperty = (p) => {
5429
+ if (onChange) onChange(applyRemoveProperty(model, view, p.name));
5430
+ };
5431
+ const removeRelation = (r) => {
5432
+ if (!onChange || view.kind === "edge") return;
5433
+ const source = view.kind === "node" ? view.label : view.source;
5434
+ onChange(deleteRelation(model, source, r.relType, r.target));
5435
+ };
5436
+ const saveRelation = (source, _target, input, prev) => {
5437
+ if (!onChange) return;
5438
+ onChange(updateRelation(model, source, prev.relType, prev.target, input));
5439
+ };
5440
+ const propertiesSection = propsHidden ? null : /* @__PURE__ */ jsx19(
5441
+ PropertiesSection,
5442
+ {
5443
+ view,
5444
+ editable: propsEditable,
5445
+ onAdd: () => setSheet({ open: true, property: void 0 }),
5446
+ onEdit: (p) => setSheet({ open: true, property: p }),
5447
+ onDelete: removeProperty
5448
+ }
5449
+ );
5450
+ return /* @__PURE__ */ jsxs17("div", { children: [
5451
+ view.kind === "node" ? /* @__PURE__ */ jsx19(
5452
+ NodeDetail,
5453
+ {
5454
+ view,
5455
+ nodeTypeEditable,
5456
+ relationsEditable,
5457
+ onRename: () => setRenameOpen(true),
5458
+ onDelete: () => onChange?.(deleteNodeType(model, view.label)),
5459
+ onEditRelation: setRelSheet,
5460
+ onDeleteRelation: removeRelation,
5461
+ children: propertiesSection
5462
+ }
5463
+ ) : view.kind === "edge" ? /* @__PURE__ */ jsx19(
5464
+ EdgeDetail,
5465
+ {
5466
+ view,
5467
+ editable: edgeTypeEditable,
5468
+ onRename: () => setRenameOpen(true),
5469
+ onDelete: () => onChange?.(deleteEdgeType(model, view.label)),
5470
+ children: propertiesSection
5471
+ }
5472
+ ) : /* @__PURE__ */ jsx19(
5473
+ RelationDetail,
5474
+ {
5475
+ view,
5476
+ editable: relationsEditable,
5477
+ onEdit: () => setRelSheet(relationViewOf(view)),
5478
+ onDelete: () => removeRelation(relationViewOf(view)),
5479
+ children: propertiesSection
5480
+ }
5481
+ ),
5482
+ editable && /* @__PURE__ */ jsx19(
5483
+ DetailSheets,
5484
+ {
5485
+ model,
5486
+ view,
5487
+ ownerLabel,
5488
+ widgetOptions,
5489
+ edgeTypeMode: capabilities?.edgeTypes ?? "edit",
5490
+ sheet,
5491
+ setSheet,
5492
+ relSheet,
5493
+ setRelSheet,
5494
+ renameOpen,
5495
+ setRenameOpen,
5496
+ onSaveProperty: saveProperty,
5497
+ onSaveRelation: saveRelation,
5498
+ onRename: (label) => {
5499
+ if (!onChange) return;
5500
+ if (view.kind === "node")
5501
+ onChange(renameNodeType(model, view.label, label));
5502
+ else if (view.kind === "edge")
5503
+ onChange(renameEdgeType(model, view.label, label));
5504
+ }
5505
+ }
5506
+ )
5507
+ ] });
5508
+ }
5509
+ function DetailSheets({
5510
+ model,
5511
+ view,
5512
+ ownerLabel,
5513
+ widgetOptions,
5514
+ edgeTypeMode,
5515
+ sheet,
5516
+ setSheet,
5517
+ relSheet,
5518
+ setRelSheet,
5519
+ renameOpen,
5520
+ setRenameOpen,
5521
+ onSaveProperty,
5522
+ onSaveRelation,
5523
+ onRename
5524
+ }) {
5525
+ const edgeTypeOptions = model.filter(isEdgeType).map(typeLabel);
5526
+ return /* @__PURE__ */ jsxs17(Fragment4, { children: [
5527
+ /* @__PURE__ */ jsx19(
5528
+ PropertyFormSheet,
5529
+ {
5530
+ open: sheet.open,
5531
+ onOpenChange: (o) => setSheet((s) => ({ ...s, open: o })),
5532
+ property: sheet.property,
5533
+ ownerLabel,
5534
+ widgetOptions,
5535
+ onSave: onSaveProperty
5536
+ }
5537
+ ),
5538
+ relSheet && (view.kind === "node" || view.kind === "relation") && /* @__PURE__ */ jsx19(
5539
+ RelationFormSheet,
5540
+ {
5541
+ open: !!relSheet,
5542
+ onOpenChange: (o) => !o && setRelSheet(null),
5543
+ nodeA: view.kind === "node" ? view.label : view.source,
5544
+ nodeB: relSheet.target,
5545
+ initial: {
5546
+ relType: relSheet.relType,
5547
+ cardinality: relSheet.cardinality
5548
+ },
5549
+ edgeTypeOptions,
5550
+ edgeTypeMode,
5551
+ onSave: (source, target, input) => onSaveRelation(source, target, input, relSheet)
5552
+ },
5553
+ `${relSheet.relType}:${relSheet.target}`
5554
+ ),
5555
+ (view.kind === "node" || view.kind === "edge") && renameOpen && /* @__PURE__ */ jsx19(
5556
+ NodeTypeNameSheet,
5557
+ {
5558
+ open: renameOpen,
5559
+ onOpenChange: setRenameOpen,
5560
+ initial: view.label,
5561
+ existing: model.map(typeLabel),
5562
+ noun: view.kind === "edge" ? "edge" : "node",
5563
+ onSave: onRename
5564
+ },
5565
+ view.label
5566
+ )
5567
+ ] });
5568
+ }
5569
+ function PropertiesSection({
5570
+ view,
5571
+ editable,
5572
+ onAdd,
5573
+ onEdit,
5574
+ onDelete
5575
+ }) {
5576
+ return /* @__PURE__ */ jsxs17(Fragment4, { children: [
5577
+ /* @__PURE__ */ jsx19(
5578
+ SectionTitle,
5579
+ {
5580
+ action: editable ? /* @__PURE__ */ jsx19(IconButton, { label: "Add property", onClick: onAdd, children: /* @__PURE__ */ jsx19(Plus, {}) }) : void 0,
5581
+ children: "Properties"
5582
+ }
5583
+ ),
5584
+ /* @__PURE__ */ jsx19(
5585
+ PropertyRows,
5586
+ {
5587
+ properties: view.properties,
5588
+ emptyText: "No properties yet.",
5589
+ editable,
5590
+ onEdit,
5591
+ onDelete
5592
+ }
5593
+ )
5594
+ ] });
5595
+ }
5596
+ function NodeDetail({
5597
+ view,
5598
+ nodeTypeEditable,
5599
+ relationsEditable,
5600
+ onRename,
5601
+ onDelete,
5602
+ onEditRelation,
5603
+ onDeleteRelation,
5604
+ children
5605
+ }) {
5606
+ return /* @__PURE__ */ jsxs17(Fragment4, { children: [
5607
+ /* @__PURE__ */ jsxs17("div", { className: "flex items-center gap-2", children: [
5608
+ /* @__PURE__ */ jsx19("span", { className: "text-base font-semibold", children: view.label }),
5609
+ /* @__PURE__ */ jsx19(Badge2, { children: "NODE TYPE" }),
5610
+ nodeTypeEditable && /* @__PURE__ */ jsx19(
5611
+ RenameDeleteActions,
5612
+ {
5613
+ noun: "node type",
5614
+ onRename,
5615
+ onDelete
5616
+ }
5617
+ )
5618
+ ] }),
5619
+ view.description && /* @__PURE__ */ jsx19("p", { className: "mt-1 text-sm text-muted-foreground", children: view.description }),
5620
+ children,
5621
+ /* @__PURE__ */ jsx19(SectionTitle, { children: "Relations" }),
5622
+ view.relations.length === 0 ? /* @__PURE__ */ jsx19("p", { className: "text-xs text-muted-foreground", children: "No relations." }) : /* @__PURE__ */ jsx19("div", { className: "rounded-lg border divide-y", children: view.relations.map((r) => /* @__PURE__ */ jsxs17(
5623
+ "div",
5624
+ {
5625
+ className: "group/row flex items-center gap-2 px-3 py-2",
5626
+ children: [
5627
+ /* @__PURE__ */ jsx19("span", { className: "text-sm font-medium min-w-24", children: r.relType }),
5628
+ /* @__PURE__ */ jsxs17("span", { className: "flex-1 font-mono text-xs text-muted-foreground", children: [
5629
+ "\u2192",
5630
+ " ",
5631
+ r.target,
5632
+ " ",
5633
+ "\xB7",
5634
+ " ",
5635
+ cardinalityLabel(r.cardinality)
5636
+ ] }),
5637
+ relationsEditable && /* @__PURE__ */ jsxs17("span", { className: "flex gap-1", children: [
5638
+ /* @__PURE__ */ jsx19(
5639
+ IconButton,
5640
+ {
5641
+ label: "Edit relation",
5642
+ revealOnHover: true,
5643
+ onClick: () => onEditRelation(r),
5644
+ children: /* @__PURE__ */ jsx19(Pencil, {})
5645
+ }
5646
+ ),
5647
+ /* @__PURE__ */ jsx19(
5648
+ IconButton,
5649
+ {
5650
+ label: "Delete relation",
5651
+ tone: "destructive",
5652
+ revealOnHover: true,
5653
+ onClick: () => onDeleteRelation(r),
5654
+ children: /* @__PURE__ */ jsx19(Trash2, {})
5655
+ }
5656
+ )
5657
+ ] })
5658
+ ]
5659
+ },
5660
+ `${r.relType}:${r.target}`
5661
+ )) })
5662
+ ] });
5663
+ }
5664
+ function EdgeTypeNote({
5665
+ children
5666
+ }) {
5667
+ return /* @__PURE__ */ jsx19("p", { className: "mt-3 rounded-md bg-muted/50 px-3 py-2 text-xs text-muted-foreground", children });
5668
+ }
5669
+ function RenameDeleteActions({
5670
+ noun,
5671
+ editVerb = "Rename",
5672
+ onRename,
5673
+ onDelete
5674
+ }) {
5675
+ return /* @__PURE__ */ jsxs17("span", { className: "ml-auto flex gap-1", children: [
5676
+ /* @__PURE__ */ jsx19(IconButton, { label: `${editVerb} ${noun}`, onClick: onRename, children: /* @__PURE__ */ jsx19(Pencil, {}) }),
5677
+ /* @__PURE__ */ jsx19(
5678
+ IconButton,
5679
+ {
5680
+ label: `Delete ${noun}`,
5681
+ tone: "destructive",
5682
+ onClick: onDelete,
5683
+ children: /* @__PURE__ */ jsx19(Trash2, {})
5684
+ }
5685
+ )
5686
+ ] });
5687
+ }
5688
+ function EdgeDetail({
5689
+ view,
5690
+ editable,
5691
+ onRename,
5692
+ onDelete,
5693
+ children
5694
+ }) {
5695
+ return /* @__PURE__ */ jsxs17(Fragment4, { children: [
5696
+ /* @__PURE__ */ jsxs17("div", { className: "flex items-center gap-2", children: [
5697
+ /* @__PURE__ */ jsx19("span", { className: "text-base font-semibold", children: view.label }),
5698
+ /* @__PURE__ */ jsx19(Badge2, { children: "EDGE TYPE" }),
5699
+ editable && /* @__PURE__ */ jsx19(
5700
+ RenameDeleteActions,
5701
+ {
5702
+ noun: "edge type",
5703
+ onRename,
5704
+ onDelete
5705
+ }
5706
+ )
5707
+ ] }),
5708
+ view.description && /* @__PURE__ */ jsx19("p", { className: "mt-1 text-sm text-muted-foreground", children: view.description }),
5709
+ /* @__PURE__ */ jsxs17(EdgeTypeNote, { children: [
5710
+ "These properties belong to the",
5711
+ " ",
5712
+ /* @__PURE__ */ jsx19("span", { className: "font-medium", children: view.label }),
5713
+ " edge type and are shared by every relation of this type."
5714
+ ] }),
5715
+ children
5716
+ ] });
5717
+ }
5718
+ function RelationDetail({
5719
+ view,
5720
+ editable,
5721
+ onEdit,
5722
+ onDelete,
5723
+ children
5724
+ }) {
5725
+ return /* @__PURE__ */ jsxs17(Fragment4, { children: [
5726
+ /* @__PURE__ */ jsxs17("div", { className: "flex items-center gap-2", children: [
5727
+ /* @__PURE__ */ jsx19("span", { className: "text-base font-semibold", children: view.relType }),
5728
+ /* @__PURE__ */ jsx19(Badge2, { children: "RELATION" }),
5729
+ editable && /* @__PURE__ */ jsx19(
5730
+ RenameDeleteActions,
5731
+ {
5732
+ noun: "relation",
5733
+ editVerb: "Edit",
5734
+ onRename: onEdit,
5735
+ onDelete
5736
+ }
5737
+ )
5738
+ ] }),
5739
+ /* @__PURE__ */ jsxs17("p", { className: "mt-1 text-sm text-muted-foreground", children: [
5740
+ /* @__PURE__ */ jsx19("span", { className: "font-medium text-foreground", children: view.source }),
5741
+ " \u2192 ",
5742
+ /* @__PURE__ */ jsx19("span", { className: "font-medium text-foreground", children: view.target }),
5743
+ " \xB7 ",
5744
+ cardinalityLabel(view.cardinality)
5745
+ ] }),
5746
+ /* @__PURE__ */ jsxs17(EdgeTypeNote, { children: [
5747
+ "Properties are defined on the",
5748
+ " ",
5749
+ /* @__PURE__ */ jsx19("span", { className: "font-medium", children: view.relType }),
5750
+ " edge type, shared by every relation of this type -- not on this single relation."
5751
+ ] }),
5752
+ children
5753
+ ] });
5754
+ }
5755
+
5756
+ // src/graph-model/capabilities.ts
5757
+ import { z } from "zod";
5758
+ import { warnLog } from "@petrarca/sonnet-core";
5759
+ var capabilitiesSchema = z.object({
5760
+ nodeTypes: z.enum(["edit", "readonly"]).default("edit").meta({
5761
+ description: "Node types (add / rename / delete). 'edit' (default): add new types, rename and delete from the panel. 'readonly': types are shown but cannot be added, renamed, or deleted. No 'hidden': node types are the canvas nodes and are always present."
5762
+ }),
5763
+ edgeTypes: z.enum(["edit", "catalog", "readonly"]).default("edit").meta({
5764
+ description: "Edge-type catalog governance for relation creation. 'edit' (default): select an existing edge type or create one inline, plus 'Add edge type' and edge-type rename/delete. 'catalog': select only from existing edge types; no create/rename/delete. 'readonly': no edge-type changes."
5765
+ }),
5766
+ nodeProperties: z.enum(["edit", "readonly", "hidden"]).default("edit").meta({
5767
+ description: "Node-type properties. 'edit' (default): add / edit / delete. 'readonly': shown, but no add / edit / delete affordances. 'hidden': the Properties section is not rendered."
5768
+ }),
5769
+ edgeProperties: z.enum(["edit", "readonly", "hidden"]).default("edit").meta({
5770
+ description: "Edge-type / relation properties (defined on the edge type, shared by every relation of that type). 'edit' (default): add / edit / delete. 'readonly': shown, but not editable. 'hidden': the Properties section is not rendered."
5771
+ }),
5772
+ relations: z.enum(["edit", "readonly"]).default("edit").meta({
5773
+ description: "Relations between node types (create / edit / delete). 'edit' (default): create (select two nodes), edit, and delete relations. 'readonly': relations are shown but cannot be changed. How the relation type is chosen is governed by edgeTypes."
5774
+ }),
5775
+ showEdgeTypes: z.boolean().default(false).meta({
5776
+ description: "Whether edge-type nodes (the catalog of relationship types) are shown on the canvas initially. false (default): edge types start filtered out (re-enable from the Edge types legend). true: shown from the start."
5777
+ })
5778
+ }).meta({
5779
+ id: "GraphModelCapabilities",
5780
+ title: "GraphModelCapabilities",
5781
+ description: "Capability profile for GraphModelEditor. Gates what a consumer can edit. Every field is optional and defaults to the most permissive value (forward-compatible). Passing readOnly to the editor overrides this object to a fully-locked profile."
5782
+ });
5783
+ var LOCKED = {
5784
+ nodeTypes: "readonly",
5785
+ edgeTypes: "readonly",
5786
+ nodeProperties: "readonly",
5787
+ edgeProperties: "readonly",
5788
+ relations: "readonly",
5789
+ showEdgeTypes: false
5790
+ };
5791
+ function resolveCapabilities(capabilities, readOnly) {
5792
+ if (readOnly) return LOCKED;
5793
+ const parsed = capabilitiesSchema.safeParse(capabilities ?? {});
5794
+ if (!parsed.success) {
5795
+ warnLog(
5796
+ "[GraphModelEditor] Invalid capabilities \u2014 falling back to defaults.",
5797
+ parsed.error.issues
5798
+ );
5799
+ return capabilitiesSchema.parse({});
5800
+ }
5801
+ return parsed.data;
5802
+ }
5803
+ function validateCapabilities(input) {
5804
+ const parsed = capabilitiesSchema.safeParse(input);
5805
+ if (parsed.success) return { ok: true, value: parsed.data };
5806
+ const errors = parsed.error.issues.map((i) => {
5807
+ const path = i.path.length ? i.path.join(".") : "(root)";
5808
+ return `${path}: ${i.message}`;
5809
+ });
5810
+ return { ok: false, errors };
5811
+ }
5812
+
5813
+ // src/graph-model/ModelActionPanel.tsx
5814
+ import { Fragment as Fragment5, jsx as jsx20, jsxs as jsxs18 } from "react/jsx-runtime";
5815
+ function nodeTypeLabels(model) {
5816
+ return model.filter(isNodeType).map(typeLabel);
5817
+ }
5818
+ function edgeTypeLabels(model) {
5819
+ return model.filter(isEdgeType).map(typeLabel);
5820
+ }
5821
+ function CreateRelationPrompt({
5822
+ nodeA,
5823
+ nodeB,
5824
+ model,
5825
+ edgeTypeMode,
5826
+ onChange
5827
+ }) {
5828
+ const [open, setOpen] = useState12(false);
5829
+ return /* @__PURE__ */ jsxs18("div", { children: [
5830
+ /* @__PURE__ */ jsxs18("div", { className: "text-sm", children: [
5831
+ /* @__PURE__ */ jsx20("span", { className: "font-medium", children: nodeA }),
5832
+ " and ",
5833
+ /* @__PURE__ */ jsx20("span", { className: "font-medium", children: nodeB }),
5834
+ " selected."
5835
+ ] }),
5836
+ /* @__PURE__ */ jsxs18(Button6, { size: "sm", className: "mt-3 gap-1.5", onClick: () => setOpen(true), children: [
5837
+ /* @__PURE__ */ jsx20(GitBranchPlus, { className: "h-4 w-4" }),
5838
+ "Create relation"
5839
+ ] }),
5840
+ /* @__PURE__ */ jsx20(
5841
+ RelationFormSheet,
5842
+ {
5843
+ open,
5844
+ onOpenChange: setOpen,
5845
+ nodeA,
5846
+ nodeB,
5847
+ edgeTypeOptions: edgeTypeLabels(model),
5848
+ edgeTypeMode,
5849
+ onSave: (source, _target, input) => onChange(addRelation(model, source, input))
5850
+ }
5851
+ )
5852
+ ] });
5853
+ }
5854
+ function ModelActionPanel({
5855
+ model,
5856
+ onChange,
5857
+ readOnly,
5858
+ capabilities,
5859
+ widgetOptions
5860
+ }) {
5861
+ const caps = resolveCapabilities(capabilities, readOnly);
5862
+ const useSelection3 = useWorkspaceSelection();
5863
+ const selectedNodeIds = useSelection3(
5864
+ (s) => s.selectedNodeIds
5865
+ );
5866
+ const selectedEdge = useSelection3((s) => s.selectedEdge);
5867
+ const live = useLiveGraphData();
5868
+ const labelsById = (ids) => ids.map((id) => live.normalized.nodesById[id]?.label_).filter((l) => !!l);
5869
+ const selection = useMemo8(() => {
5870
+ if (selectedEdge) {
5871
+ const r = resolveRelationEdge(selectedEdge);
5872
+ if (r) {
5873
+ return {
5874
+ kind: "relation",
5875
+ source: r.source,
5876
+ relType: r.relType,
5877
+ target: r.target
5878
+ };
5879
+ }
5880
+ }
5881
+ const firstId = selectedNodeIds[0];
5882
+ if (selectedNodeIds.length === 1 && firstId != null) {
5883
+ const node = live.normalized.nodesById[firstId];
5884
+ if (node) {
5885
+ const schema = findType(model, node.label_);
5886
+ const kind = schema && isEdgeType(schema) ? "edge" : "node";
5887
+ return { kind, label: node.label_ };
5888
+ }
5889
+ }
5890
+ return null;
5891
+ }, [selectedEdge, selectedNodeIds, live.normalized, model]);
5892
+ const twoLabels = selectedNodeIds.length === 2 && !selectedEdge ? labelsById(selectedNodeIds).filter((l) => {
5893
+ const schema = findType(model, l);
5894
+ return schema ? isNodeType(schema) : false;
5895
+ }) : [];
5896
+ return /* @__PURE__ */ jsx20("div", { className: "h-full overflow-auto p-4", children: /* @__PURE__ */ jsx20(
5897
+ PanelBody,
5898
+ {
5899
+ model,
5900
+ onChange,
5901
+ readOnly,
5902
+ caps,
5903
+ widgetOptions,
5904
+ selection,
5905
+ twoLabels
5906
+ }
5907
+ ) });
5908
+ }
5909
+ function PanelBody({
5910
+ model,
5911
+ onChange,
5912
+ readOnly,
5913
+ caps,
5914
+ widgetOptions,
5915
+ selection,
5916
+ twoLabels
5917
+ }) {
5918
+ if (selection) {
5919
+ return /* @__PURE__ */ jsx20(
5920
+ TypeDetailPanel,
5921
+ {
5922
+ model,
5923
+ selection,
5924
+ onChange,
5925
+ readOnly,
5926
+ capabilities: caps,
5927
+ widgetOptions
5928
+ }
5929
+ );
5930
+ }
5931
+ const canEdit = !readOnly && !!onChange;
5932
+ if (twoLabels.length === 2 && canEdit && caps.relations === "edit") {
5933
+ return /* @__PURE__ */ jsx20(
5934
+ CreateRelationPrompt,
5935
+ {
5936
+ nodeA: twoLabels[0],
5937
+ nodeB: twoLabels[1],
5938
+ model,
5939
+ edgeTypeMode: caps.edgeTypes,
5940
+ onChange
5941
+ }
5942
+ );
5943
+ }
5944
+ return /* @__PURE__ */ jsxs18(Fragment5, { children: [
5945
+ canEdit && /* @__PURE__ */ jsx20(
5946
+ AddTypeToolbar,
5947
+ {
5948
+ model,
5949
+ onChange,
5950
+ canAddNode: caps.nodeTypes === "edit",
5951
+ canAddEdge: caps.edgeTypes === "edit"
5952
+ }
5953
+ ),
5954
+ /* @__PURE__ */ jsx20(
5955
+ Overview_default,
5956
+ {
5957
+ nodeBuckets: [
5958
+ { title: "Node types", labels: nodeTypeLabels(model) },
5959
+ { title: "Edge types", labels: edgeTypeLabels(model) }
5960
+ ],
5961
+ edgeSectionTitle: "Relations"
5962
+ }
5963
+ )
5964
+ ] });
5965
+ }
5966
+ function AddTypeIconButton({
5967
+ tooltip,
5968
+ icon,
5969
+ noun,
5970
+ existing,
5971
+ onSave
5972
+ }) {
5973
+ const [open, setOpen] = useState12(false);
5974
+ return /* @__PURE__ */ jsxs18(Fragment5, { children: [
5975
+ /* @__PURE__ */ jsx20(
5976
+ IconButton2,
5977
+ {
5978
+ label: tooltip,
5979
+ variant: "outline",
5980
+ onClick: () => setOpen(true),
5981
+ children: icon
5982
+ }
5983
+ ),
5984
+ /* @__PURE__ */ jsx20(
5985
+ NodeTypeNameSheet,
5986
+ {
5987
+ open,
5988
+ onOpenChange: setOpen,
5989
+ existing,
5990
+ noun,
5991
+ onSave
5992
+ }
5993
+ )
5994
+ ] });
5995
+ }
5996
+ function AddTypeToolbar({
5997
+ model,
5998
+ onChange,
5999
+ canAddNode,
6000
+ canAddEdge
6001
+ }) {
6002
+ if (!canAddNode && !canAddEdge) return null;
6003
+ const existing = model.map(typeLabel);
6004
+ return /* @__PURE__ */ jsxs18("div", { className: "mb-3 flex gap-1.5", children: [
6005
+ canAddNode && /* @__PURE__ */ jsx20(
6006
+ AddTypeIconButton,
6007
+ {
6008
+ tooltip: "Add node type",
6009
+ icon: /* @__PURE__ */ jsx20(CirclePlus, {}),
6010
+ noun: "node",
6011
+ existing,
6012
+ onSave: (name) => onChange(addNodeType(model, name))
6013
+ }
6014
+ ),
6015
+ canAddEdge && /* @__PURE__ */ jsx20(
6016
+ AddTypeIconButton,
6017
+ {
6018
+ tooltip: "Add edge type",
6019
+ icon: /* @__PURE__ */ jsx20(Waypoints, {}),
6020
+ noun: "edge",
6021
+ existing,
6022
+ onSave: (name) => onChange(addEdgeType(model, name))
6023
+ }
6024
+ )
6025
+ ] });
6026
+ }
6027
+
6028
+ // src/graph-model/GraphModelEditor.tsx
6029
+ import { Fragment as Fragment6, jsx as jsx21, jsxs as jsxs19 } from "react/jsx-runtime";
6030
+ function createEditorStores() {
6031
+ const graphFilter = createGraphFilterStore();
6032
+ const graphData = createGraphDataStore();
6033
+ const selection = createSelectionStore();
6034
+ const graphFocus = createGraphFocusStore();
6035
+ const graphExploration = createGraphExplorationStore({
6036
+ graphData,
6037
+ selection,
6038
+ graphFocus,
6039
+ graphFilter,
6040
+ expandNode: async () => ({ nodes: [], edges: [], has_more: false }),
6041
+ getNodes: async () => []
6042
+ });
6043
+ return { graphFilter, graphData, graphExploration, selection };
6044
+ }
6045
+ function loadModel(stores, model) {
6046
+ const { nodes: rawNodes, edges: rawEdges } = modelToGraph(model);
6047
+ const labelCounts = {};
6048
+ for (const n of rawNodes) {
6049
+ labelCounts[n.label_] = (labelCounts[n.label_] ?? 0) + 1;
6050
+ }
6051
+ const palette = buildColorPalette(labelCounts);
6052
+ const nodes = enrichNodes(rawNodes, palette);
6053
+ const edges = enrichEdges(rawEdges, palette);
6054
+ stores.graphData.getState().setQueryData(nodes, edges, { nodes: nodes.length, edges: edges.length });
6055
+ stores.graphFilter.getState().notifyDataChange();
6056
+ }
6057
+ function GraphModelEditor({
6058
+ value,
6059
+ readOnly,
6060
+ capabilities,
6061
+ onChange,
6062
+ widgetOptions,
6063
+ defaultRenderer = "cytoscape",
6064
+ hideActionPanel = false
6065
+ }) {
6066
+ const storesRef = useRef6(null);
6067
+ if (!storesRef.current) storesRef.current = createEditorStores();
6068
+ const stores = storesRef.current;
6069
+ const caps = resolveCapabilities(capabilities, readOnly);
6070
+ const showEdgeTypes = caps.showEdgeTypes;
6071
+ const firstLoadRef = useRef6(true);
6072
+ const prevShowEdgeTypesRef = useRef6(showEdgeTypes);
6073
+ useEffect5(() => {
6074
+ loadModel(stores, value);
6075
+ const filter = stores.graphFilter.getState();
6076
+ const baselineReset = firstLoadRef.current || prevShowEdgeTypesRef.current !== showEdgeTypes;
6077
+ const next = reconcileFilter(
6078
+ filter.nodeLabelFilters,
6079
+ value,
6080
+ baselineReset,
6081
+ showEdgeTypes
6082
+ );
6083
+ filter.setNodeLabelFilters(next);
6084
+ firstLoadRef.current = false;
6085
+ prevShowEdgeTypesRef.current = showEdgeTypes;
6086
+ }, [stores, value, showEdgeTypes]);
6087
+ const rowRef = useRef6(null);
6088
+ const [panelWidth, setPanelWidth] = useState13(380);
6089
+ const startResize = useCallback7((e) => {
6090
+ e.preventDefault();
6091
+ const target = e.currentTarget;
6092
+ target.setPointerCapture(e.pointerId);
6093
+ const move = (ev) => {
6094
+ const right = rowRef.current?.getBoundingClientRect().right ?? 0;
6095
+ setPanelWidth(Math.min(640, Math.max(300, right - ev.clientX)));
6096
+ };
6097
+ const up = () => {
6098
+ window.removeEventListener("pointermove", move);
6099
+ window.removeEventListener("pointerup", up);
6100
+ };
6101
+ window.addEventListener("pointermove", move);
6102
+ window.addEventListener("pointerup", up);
6103
+ }, []);
6104
+ return /* @__PURE__ */ jsx21(GraphStoresContext.Provider, { value: stores, children: /* @__PURE__ */ jsx21(GraphRendererProvider, { defaultRenderer, children: /* @__PURE__ */ jsxs19("div", { ref: rowRef, className: "flex w-full h-full overflow-hidden", children: [
6105
+ /* @__PURE__ */ jsx21("div", { className: "flex-1 min-w-0", children: /* @__PURE__ */ jsx21(GraphVisualizer_default, {}) }),
6106
+ !hideActionPanel && /* @__PURE__ */ jsxs19(Fragment6, { children: [
6107
+ /* @__PURE__ */ jsx21(
6108
+ ResizeHandle,
6109
+ {
6110
+ orientation: "vertical",
6111
+ "aria-valuenow": panelWidth,
6112
+ "aria-valuemin": 300,
6113
+ "aria-valuemax": 640,
6114
+ onPointerDown: startResize,
6115
+ onDoubleClick: () => setPanelWidth(380)
6116
+ }
6117
+ ),
6118
+ /* @__PURE__ */ jsx21(
6119
+ "div",
6120
+ {
6121
+ style: { width: panelWidth },
6122
+ className: "shrink-0 border-l overflow-hidden bg-card",
6123
+ children: /* @__PURE__ */ jsx21(
6124
+ ModelActionPanel,
6125
+ {
6126
+ model: value,
6127
+ onChange,
6128
+ readOnly,
6129
+ capabilities,
6130
+ widgetOptions
6131
+ }
6132
+ )
6133
+ }
6134
+ )
6135
+ ] })
6136
+ ] }) }) });
6137
+ }
6138
+
6139
+ // src/graph-edit/capabilities.ts
6140
+ import { z as z2 } from "zod";
6141
+ import { warnLog as warnLog2 } from "@petrarca/sonnet-core";
6142
+ var graphCapabilitiesSchema = z2.object({
6143
+ createNode: z2.enum(["edit", "readonly"]).default("readonly").meta({
6144
+ description: "Create new nodes. 'edit': the add-node affordance is shown. 'readonly' (default): hidden."
6145
+ }),
6146
+ updateNode: z2.enum(["edit", "readonly"]).default("readonly").meta({
6147
+ description: "Edit existing nodes (properties and relations). 'edit': the edit-node affordance is shown. 'readonly' (default): hidden."
6148
+ }),
6149
+ deleteNode: z2.enum(["edit", "readonly"]).default("readonly").meta({
6150
+ description: "Delete nodes. 'edit': the delete-node affordance is shown. 'readonly' (default): hidden."
6151
+ }),
6152
+ createEdge: z2.enum(["edit", "readonly"]).default("readonly").meta({
6153
+ description: "Create edges between two selected nodes. 'edit': the add-edge affordance is shown. 'readonly' (default): hidden."
6154
+ }),
6155
+ updateEdge: z2.enum(["edit", "readonly"]).default("readonly").meta({
6156
+ description: "Edit edge properties. 'edit': the edit-edge affordance is shown. 'readonly' (default): hidden."
6157
+ }),
6158
+ deleteEdge: z2.enum(["edit", "readonly"]).default("readonly").meta({
6159
+ description: "Delete edges. 'edit': the delete-edge affordance is shown. 'readonly' (default): hidden."
6160
+ })
6161
+ }).meta({
6162
+ id: "GraphCapabilities",
6163
+ title: "GraphCapabilities",
6164
+ description: "Capability profile for the graph viewer + instance editor. Gates what a consumer can do with a live data graph. Every field is optional and defaults to read-only (no editing affordances) so existing viewers are unaffected until a consumer opts in. Passing readOnly to the editor overrides this object to a fully read-only profile."
6165
+ });
6166
+ var READ_ONLY = {
6167
+ createNode: "readonly",
6168
+ updateNode: "readonly",
6169
+ deleteNode: "readonly",
6170
+ createEdge: "readonly",
6171
+ updateEdge: "readonly",
6172
+ deleteEdge: "readonly"
6173
+ };
6174
+ function resolveGraphCapabilities(capabilities, readOnly) {
6175
+ if (readOnly) return READ_ONLY;
6176
+ const parsed = graphCapabilitiesSchema.safeParse(capabilities ?? {});
6177
+ if (!parsed.success) {
6178
+ warnLog2(
6179
+ "[GraphEditor] Invalid capabilities \u2014 falling back to defaults.",
6180
+ parsed.error.issues
6181
+ );
6182
+ return graphCapabilitiesSchema.parse({});
6183
+ }
6184
+ return parsed.data;
6185
+ }
6186
+ function validateGraphCapabilities(input) {
6187
+ const parsed = graphCapabilitiesSchema.safeParse(input);
6188
+ if (parsed.success) return { ok: true, value: parsed.data };
6189
+ const errors = parsed.error.issues.map((i) => {
6190
+ const path = i.path.length ? i.path.join(".") : "(root)";
6191
+ return `${path}: ${i.message}`;
6192
+ });
6193
+ return { ok: false, errors };
6194
+ }
6195
+
6196
+ // src/graph-edit/relationFields.ts
6197
+ var REL_PREFIX = "__rel_";
6198
+ var REL_SUFFIX = "__";
6199
+ function isRelationField(key) {
6200
+ return key.startsWith(REL_PREFIX) && key.endsWith(REL_SUFFIX);
6201
+ }
6202
+ function relationFieldName(relationType) {
6203
+ return `${REL_PREFIX}${relationType.toLowerCase()}${REL_SUFFIX}`;
6204
+ }
6205
+ function relationTypeFromField(fieldName) {
6206
+ return fieldName.slice(REL_PREFIX.length, -REL_SUFFIX.length).toUpperCase();
6207
+ }
6208
+ function groupEdgesByRelation(nodeId2, edges) {
6209
+ const relations = {};
6210
+ for (const edge of edges) {
6211
+ const targetId = edge.start_id_ === nodeId2 ? edge.end_id_ : edge.start_id_;
6212
+ (relations[edge.label_] ??= []).push(targetId);
6213
+ }
6214
+ return relations;
6215
+ }
6216
+ function initializeFormData(properties, relations, editableRelationTypes) {
6217
+ const formData = { ...properties };
6218
+ for (const relationType of editableRelationTypes) {
6219
+ const ids = relations[relationType] ?? [];
6220
+ formData[relationFieldName(relationType)] = ids.map(String);
6221
+ }
6222
+ return formData;
6223
+ }
6224
+ function extractRelationFields(formData) {
6225
+ const properties = {};
6226
+ const relations = {};
6227
+ for (const [key, value] of Object.entries(formData)) {
6228
+ if (isRelationField(key)) {
6229
+ const relationType = relationTypeFromField(key);
6230
+ if (Array.isArray(value)) {
6231
+ relations[relationType] = value.map(Number);
6232
+ } else if (value != null) {
6233
+ relations[relationType] = [Number(value)];
6234
+ } else {
6235
+ relations[relationType] = [];
6236
+ }
6237
+ } else {
6238
+ properties[key] = value;
6239
+ }
6240
+ }
6241
+ return { properties, relations };
6242
+ }
6243
+ function hasRelationFields(formData) {
6244
+ return Object.keys(formData).some(isRelationField);
6245
+ }
6246
+ function relationTitle(relationType) {
6247
+ return relationType.toLowerCase().split("_").map((w) => w ? w.charAt(0).toUpperCase() + w.slice(1) : w).join(" ");
6248
+ }
6249
+ function editableRelations(relations) {
6250
+ return relations.filter((r) => r.form_field === true);
6251
+ }
6252
+ function hasProperties(schema) {
6253
+ return !!schema && Object.keys(schema.properties ?? {}).length > 0;
6254
+ }
6255
+ var DEFAULT_RELATION_WIDGET = "entity-select";
6256
+ function enrichSchemaWithRelations(baseSchema, relations) {
6257
+ const editable = editableRelations(relations);
6258
+ if (editable.length === 0) {
6259
+ return { schema: baseSchema, uiSchema: {} };
6260
+ }
6261
+ const properties = {
6262
+ ...baseSchema.properties ?? {}
6263
+ };
6264
+ const uiSchema = {};
6265
+ for (const rel of editable) {
6266
+ const fieldName = relationFieldName(rel.rel_type_name);
6267
+ const multiple = rel.cardinality !== "ONE_TO_ONE";
6268
+ const title = rel.title ?? relationTitle(rel.rel_type_name);
6269
+ properties[fieldName] = multiple ? { type: "array", items: { type: "integer" }, title } : { type: "integer", title };
6270
+ uiSchema[fieldName] = {
6271
+ "x-ui-widget": rel.widget ?? DEFAULT_RELATION_WIDGET,
6272
+ "x-ui-options": {
6273
+ targetType: rel.to_type_name,
6274
+ multiple,
6275
+ relationType: rel.rel_type_name
6276
+ }
6277
+ };
6278
+ }
6279
+ const schema = {
6280
+ ...baseSchema,
6281
+ properties
6282
+ };
6283
+ return { schema, uiSchema };
6284
+ }
6285
+
6286
+ // src/graph-edit/GraphEditToolbar.tsx
6287
+ import { useCallback as useCallback11, useMemo as useMemo11, useState as useState17 } from "react";
6288
+ import { IconButton as IconButton4 } from "@petrarca/sonnet-ui";
6289
+ import { Plus as Plus3, Edit, Link, Trash2 as Trash23 } from "lucide-react";
6290
+
6291
+ // src/graph-edit/NodeFormSheet.tsx
6292
+ import {
6293
+ useCallback as useCallback8,
6294
+ useEffect as useEffect6,
6295
+ useMemo as useMemo9,
6296
+ useRef as useRef7,
6297
+ useState as useState14
6298
+ } from "react";
6299
+ import {
6300
+ Sheet as Sheet4,
6301
+ SheetContent as SheetContent4,
6302
+ SheetHeader as SheetHeader4,
6303
+ SheetBody as SheetBody4,
6304
+ SheetTitle as SheetTitle4,
6305
+ SheetDescription as SheetDescription4,
6306
+ Alert
6307
+ } from "@petrarca/sonnet-ui";
6308
+ import {
6309
+ FormSelect as FormSelect2,
6310
+ FormCheckbox,
6311
+ JsonSchemaFormRenderer as JsonSchemaFormRenderer2
6312
+ } from "@petrarca/sonnet-forms";
6313
+ import { errorLog as errorLog4 } from "@petrarca/sonnet-core";
6314
+ import { Fragment as Fragment7, jsx as jsx22, jsxs as jsxs20 } from "react/jsx-runtime";
6315
+ var EMPTY_RESOLVED = {
6316
+ schema: null,
6317
+ uiSchema: {},
6318
+ relationTypes: []
6319
+ };
6320
+ function temporarySchema(properties) {
6321
+ const props = {};
6322
+ for (const [key, value] of Object.entries(properties)) {
6323
+ if (key.endsWith("_")) continue;
6324
+ const type = Array.isArray(value) ? "array" : typeof value;
6325
+ props[key] = { type, title: key };
6326
+ }
6327
+ return { type: "object", properties: props };
6328
+ }
6329
+ function resolveBaseSchema(provided, node) {
6330
+ if (provided) return provided;
6331
+ const properties = node?.properties_;
6332
+ if (properties && Object.keys(properties).length > 0) {
6333
+ return temporarySchema(properties);
6334
+ }
6335
+ return { type: "object", properties: {} };
6336
+ }
6337
+ function useNodeSchema(schemaProvider, label, node) {
6338
+ const [resolved, setResolved] = useState14(EMPTY_RESOLVED);
6339
+ const nodeRef = useRef7(node);
6340
+ nodeRef.current = node;
6341
+ useEffect6(() => {
6342
+ let cancelled = false;
6343
+ if (!label) {
6344
+ setResolved(EMPTY_RESOLVED);
6345
+ return;
6346
+ }
6347
+ void (async () => {
6348
+ try {
6349
+ const [provided, rels] = await Promise.all([
6350
+ schemaProvider.getNodeSchema(label),
6351
+ schemaProvider.relationsFor(label)
6352
+ ]);
6353
+ if (cancelled) return;
6354
+ const base = resolveBaseSchema(provided, nodeRef.current);
6355
+ const editable = editableRelations(rels);
6356
+ const { schema, uiSchema } = enrichSchemaWithRelations(base, editable);
6357
+ setResolved({
6358
+ schema,
6359
+ uiSchema,
6360
+ relationTypes: editable.map((r) => r.rel_type_name)
6361
+ });
6362
+ } catch (e) {
6363
+ if (cancelled) return;
6364
+ errorLog4("[NodeFormSheet] failed to load node schema", e);
6365
+ setResolved({
6366
+ schema: resolveBaseSchema(null, nodeRef.current),
6367
+ uiSchema: {},
6368
+ relationTypes: []
6369
+ });
6370
+ }
6371
+ })();
6372
+ return () => {
6373
+ cancelled = true;
6374
+ };
6375
+ }, [schemaProvider, label]);
6376
+ return resolved;
6377
+ }
6378
+ function useExistingFormData(port, node, relationTypes) {
6379
+ const [data, setData] = useState14(null);
6380
+ const nodeId2 = node?.id_;
6381
+ const propertiesRef = useRef7(node?.properties_);
6382
+ propertiesRef.current = node?.properties_;
6383
+ useEffect6(() => {
6384
+ let cancelled = false;
6385
+ if (nodeId2 == null) {
6386
+ setData(null);
6387
+ return;
6388
+ }
6389
+ const properties = propertiesRef.current ?? {};
6390
+ if (relationTypes.length === 0) {
6391
+ setData({ ...properties });
6392
+ return;
6393
+ }
6394
+ void (async () => {
6395
+ try {
6396
+ const [inbound, outbound] = await Promise.all([
6397
+ port.getInboundEdges(nodeId2),
6398
+ port.getOutboundEdges(nodeId2)
6399
+ ]);
6400
+ if (cancelled) return;
6401
+ const relations = groupEdgesByRelation(nodeId2, [
6402
+ ...inbound,
6403
+ ...outbound
6404
+ ]);
6405
+ setData(initializeFormData(properties, relations, relationTypes));
6406
+ } catch (e) {
6407
+ if (cancelled) return;
6408
+ errorLog4("[NodeFormSheet] failed to load node relations", e);
6409
+ setData({ ...properties });
6410
+ }
6411
+ })();
6412
+ return () => {
6413
+ cancelled = true;
6414
+ };
6415
+ }, [port, nodeId2, relationTypes]);
6416
+ return data;
6417
+ }
6418
+ function LabelField({
6419
+ isEdit,
6420
+ selectedLabel,
6421
+ labels,
6422
+ onSelect
6423
+ }) {
6424
+ if (isEdit) {
6425
+ return /* @__PURE__ */ jsxs20("div", { children: [
6426
+ /* @__PURE__ */ jsx22("span", { className: "heading-meta block", children: "Label" }),
6427
+ /* @__PURE__ */ jsx22("span", { className: "text-sm text-muted-foreground", children: selectedLabel })
6428
+ ] });
6429
+ }
6430
+ return /* @__PURE__ */ jsx22(
6431
+ FormSelect2,
6432
+ {
6433
+ label: "Label",
6434
+ options: labels.map((l) => ({ value: l, label: l })),
6435
+ clearable: false,
6436
+ value: selectedLabel,
6437
+ onChange: onSelect
6438
+ }
6439
+ );
6440
+ }
6441
+ function useCreateModeLabels(schemaProvider, open, isEdit) {
6442
+ const [labels, setLabels] = useState14([]);
6443
+ useEffect6(() => {
6444
+ let cancelled = false;
6445
+ if (!open || isEdit) return;
6446
+ void schemaProvider.listNodeLabels().then((ls) => {
6447
+ if (!cancelled) setLabels(ls);
6448
+ });
6449
+ return () => {
6450
+ cancelled = true;
6451
+ };
6452
+ }, [open, isEdit, schemaProvider]);
6453
+ return labels;
6454
+ }
6455
+ function NodeForm({
6456
+ formKey,
6457
+ schema,
6458
+ uiSchema,
6459
+ data,
6460
+ widgets,
6461
+ isEdit,
6462
+ onSave,
6463
+ onCancel
6464
+ }) {
6465
+ return /* @__PURE__ */ jsx22(
6466
+ JsonSchemaFormRenderer2,
6467
+ {
6468
+ schema,
6469
+ uiSchema,
6470
+ data,
6471
+ widgets,
6472
+ showActions: true,
6473
+ allowPristineSave: !isEdit,
6474
+ saveLabel: isEdit ? "Save" : "Create",
6475
+ cancelLabel: "Cancel",
6476
+ showExtraProperties: true,
6477
+ onUpdate: onSave,
6478
+ onCancel
6479
+ },
6480
+ formKey
6481
+ );
6482
+ }
6483
+ function NodeFormSheetBody({
6484
+ isEdit,
6485
+ selectedLabel,
6486
+ labels,
6487
+ onSelectLabel,
6488
+ schema,
6489
+ uiSchema,
6490
+ formKey,
6491
+ formData,
6492
+ widgets,
6493
+ error,
6494
+ keepOpen,
6495
+ onKeepOpenChange,
6496
+ onSave,
6497
+ onCancel
6498
+ }) {
6499
+ return /* @__PURE__ */ jsxs20(Fragment7, { children: [
6500
+ /* @__PURE__ */ jsxs20(SheetHeader4, { children: [
6501
+ /* @__PURE__ */ jsx22(SheetTitle4, { children: isEdit ? "Edit node" : "Add node" }),
6502
+ /* @__PURE__ */ jsx22(SheetDescription4, { children: isEdit ? "Edit this node's properties and relations." : "Create a node and (optionally) its relations." })
6503
+ ] }),
6504
+ /* @__PURE__ */ jsxs20(SheetBody4, { className: "flex flex-col gap-4", children: [
6505
+ /* @__PURE__ */ jsx22(
6506
+ LabelField,
6507
+ {
6508
+ isEdit,
6509
+ selectedLabel,
6510
+ labels,
6511
+ onSelect: onSelectLabel
6512
+ }
6513
+ ),
6514
+ schema && /* @__PURE__ */ jsx22(
6515
+ NodeForm,
6516
+ {
6517
+ formKey,
6518
+ schema,
6519
+ uiSchema,
6520
+ data: formData,
6521
+ widgets,
6522
+ isEdit,
6523
+ onSave,
6524
+ onCancel
6525
+ }
6526
+ ),
6527
+ !isEdit && /* @__PURE__ */ jsx22(
6528
+ FormCheckbox,
6529
+ {
6530
+ label: "Create another node",
6531
+ checked: keepOpen,
6532
+ onChange: (c) => onKeepOpenChange(c === true)
6533
+ }
6534
+ ),
6535
+ error && /* @__PURE__ */ jsx22(Alert, { variant: "destructive", children: error })
6536
+ ] })
6537
+ ] });
6538
+ }
6539
+ function NodeFormSheet({
6540
+ open,
6541
+ onOpenChange,
6542
+ node,
6543
+ port,
6544
+ schemaProvider,
6545
+ widgets,
6546
+ onSaved
6547
+ }) {
6548
+ const isEdit = !!node?.id_;
6549
+ const [selectedLabel, setSelectedLabel] = useState14(
6550
+ node?.label_ ?? null
6551
+ );
6552
+ const [error, setError] = useState14("");
6553
+ const [keepOpen, setKeepOpen] = useState14(false);
6554
+ const [formNonce, setFormNonce] = useState14(0);
6555
+ const labels = useCreateModeLabels(schemaProvider, open, isEdit);
6556
+ useEffect6(() => {
6557
+ if (open) {
6558
+ setSelectedLabel(node?.label_ ?? null);
6559
+ setError("");
6560
+ setKeepOpen(false);
6561
+ setFormNonce(0);
6562
+ }
6563
+ }, [open, node]);
6564
+ const { schema, uiSchema, relationTypes } = useNodeSchema(
6565
+ schemaProvider,
6566
+ selectedLabel,
6567
+ node
6568
+ );
6569
+ const existingData = useExistingFormData(port, node, relationTypes);
6570
+ const formData = useMemo9(() => existingData ?? {}, [existingData]);
6571
+ const stableOnSaved = useStableRef(onSaved);
6572
+ const stableOnOpenChange = useStableRef(onOpenChange);
6573
+ const handleSave = useCallback8(
6574
+ (data) => {
6575
+ if (!selectedLabel) return;
6576
+ void (async () => {
6577
+ try {
6578
+ const saved = await saveNode({
6579
+ port,
6580
+ label: selectedLabel,
6581
+ id: node?.id_,
6582
+ data
6583
+ });
6584
+ setError("");
6585
+ stableOnSaved?.(saved);
6586
+ if (!isEdit && keepOpen) {
6587
+ setFormNonce((n) => n + 1);
6588
+ } else {
6589
+ stableOnOpenChange(false);
6590
+ }
6591
+ } catch (e) {
6592
+ setError(saveErrorMessage(e, isEdit));
6593
+ }
6594
+ })();
6595
+ },
6596
+ [
6597
+ port,
6598
+ selectedLabel,
6599
+ node?.id_,
6600
+ isEdit,
6601
+ keepOpen,
6602
+ stableOnSaved,
6603
+ stableOnOpenChange
6604
+ ]
6605
+ );
6606
+ return /* @__PURE__ */ jsx22(Sheet4, { open, onOpenChange, children: /* @__PURE__ */ jsx22(SheetContent4, { children: /* @__PURE__ */ jsx22(
6607
+ NodeFormSheetBody,
6608
+ {
6609
+ isEdit,
6610
+ selectedLabel,
6611
+ labels,
6612
+ onSelectLabel: setSelectedLabel,
6613
+ schema,
6614
+ uiSchema,
6615
+ formKey: `${selectedLabel}:${node?.id_ ?? "new"}:${formNonce}`,
6616
+ formData,
6617
+ widgets,
6618
+ error,
6619
+ keepOpen,
6620
+ onKeepOpenChange: setKeepOpen,
6621
+ onSave: handleSave,
6622
+ onCancel: () => stableOnOpenChange(false)
6623
+ }
6624
+ ) }) });
6625
+ }
6626
+ function saveErrorMessage(e, isEdit) {
6627
+ if (e instanceof Error) return e.message;
6628
+ return isEdit ? "Failed to update node" : "Failed to create node";
6629
+ }
6630
+ async function saveNode({
6631
+ port,
6632
+ label,
6633
+ id,
6634
+ data
6635
+ }) {
6636
+ const isEdit = id != null;
6637
+ const { properties, relations } = extractRelationFields(data);
6638
+ const hasRelations = Object.keys(relations).length > 0;
6639
+ if (hasRelations) {
6640
+ if (isEdit) {
6641
+ const res2 = await port.updateNodeWithRelations({
6642
+ id_: id,
6643
+ label_: label,
6644
+ properties_: properties,
6645
+ relations
6646
+ });
6647
+ return res2.node;
6648
+ }
6649
+ const res = await port.createNodeWithRelations({
6650
+ label_: label,
6651
+ properties_: properties,
6652
+ relations
6653
+ });
6654
+ return res.node;
6655
+ }
6656
+ if (isEdit) {
6657
+ return port.updateNode({ id_: id, label_: label, properties_: properties });
6658
+ }
6659
+ return port.createNode({ label_: label, properties_: properties });
6660
+ }
6661
+
6662
+ // src/graph-edit/EdgeFormSheet.tsx
6663
+ import { useCallback as useCallback9, useEffect as useEffect7, useMemo as useMemo10, useState as useState15 } from "react";
6664
+ import {
6665
+ Sheet as Sheet5,
6666
+ SheetContent as SheetContent5,
6667
+ SheetHeader as SheetHeader5,
6668
+ SheetBody as SheetBody5,
6669
+ SheetTitle as SheetTitle5,
6670
+ SheetDescription as SheetDescription5,
6671
+ Dialog,
6672
+ DialogContent,
6673
+ DialogHeader,
6674
+ DialogTitle,
6675
+ DialogDescription,
6676
+ DialogFooter,
6677
+ Button as Button7,
6678
+ IconButton as IconButton3,
6679
+ Alert as Alert2
6680
+ } from "@petrarca/sonnet-ui";
6681
+ import { Pencil as Pencil2, Plus as Plus2, Trash2 as Trash22, ArrowRight as ArrowRight2 } from "lucide-react";
6682
+ import {
6683
+ JsonSchemaFormRenderer as JsonSchemaFormRenderer3
6684
+ } from "@petrarca/sonnet-forms";
6685
+ import { errorLog as errorLog5 } from "@petrarca/sonnet-core";
6686
+ import { jsx as jsx23, jsxs as jsxs21 } from "react/jsx-runtime";
6687
+ function usePairState(port, schemaProvider, open, nodeAId, nodeALabel, nodeBId, nodeBLabel, reloadKey) {
6688
+ const [active, setActive] = useState15([]);
6689
+ const [available, setAvailable] = useState15([]);
6690
+ useEffect7(() => {
6691
+ let cancelled = false;
6692
+ if (!open || nodeAId == null || nodeBId == null) {
6693
+ setActive([]);
6694
+ setAvailable([]);
6695
+ return;
6696
+ }
6697
+ void (async () => {
6698
+ try {
6699
+ const [edges, relationsAB, relationsBA] = await Promise.all([
6700
+ port.getPairEdges(nodeAId, nodeBId),
6701
+ schemaProvider.relationsFor(nodeALabel),
6702
+ schemaProvider.relationsFor(nodeBLabel)
6703
+ ]);
6704
+ if (cancelled) return;
6705
+ setActive(edges);
6706
+ const present = new Set(edges.map((e) => e.label_));
6707
+ const forward = relationsAB.filter((r) => r.to_type_name === nodeBLabel).map((r) => ({
6708
+ label: r.rel_type_name,
6709
+ sourceId: nodeAId,
6710
+ targetId: nodeBId
6711
+ }));
6712
+ const backward = relationsBA.filter((r) => r.to_type_name === nodeALabel).map((r) => ({
6713
+ label: r.rel_type_name,
6714
+ sourceId: nodeBId,
6715
+ targetId: nodeAId
6716
+ }));
6717
+ const seen = /* @__PURE__ */ new Set();
6718
+ const creatable = [];
6719
+ for (const entry of [...forward, ...backward]) {
6720
+ const key = `${entry.label}:${entry.sourceId}:${entry.targetId}`;
6721
+ if (present.has(entry.label) || seen.has(key)) continue;
6722
+ seen.add(key);
6723
+ creatable.push(entry);
6724
+ }
6725
+ setAvailable(creatable);
6726
+ } catch (e) {
6727
+ if (cancelled) return;
6728
+ errorLog5("[EdgeFormSheet] failed to load pair edges", e);
6729
+ }
6730
+ })();
6731
+ return () => {
6732
+ cancelled = true;
6733
+ };
6734
+ }, [
6735
+ port,
6736
+ schemaProvider,
6737
+ open,
6738
+ nodeAId,
6739
+ nodeALabel,
6740
+ nodeBId,
6741
+ nodeBLabel,
6742
+ reloadKey
6743
+ ]);
6744
+ return { active, available };
6745
+ }
6746
+ function draftLabel(draft) {
6747
+ if (!draft) return null;
6748
+ return draft.mode === "create" ? draft.entry.label : draft.edge.label_;
6749
+ }
6750
+ var EMPTY_EDGE_SCHEMA = {
6751
+ type: "object",
6752
+ properties: {}
6753
+ };
6754
+ function useEdgeSchema(schemaProvider, draft) {
6755
+ const [schema, setSchema] = useState15(null);
6756
+ const label = useMemo10(() => draftLabel(draft), [draft]);
6757
+ useEffect7(() => {
6758
+ let cancelled = false;
6759
+ if (!label) {
6760
+ setSchema(null);
6761
+ return;
6762
+ }
6763
+ void schemaProvider.getEdgeSchema(label).then((s) => {
6764
+ if (!cancelled) setSchema(s ?? EMPTY_EDGE_SCHEMA);
6765
+ }).catch((e) => {
6766
+ if (cancelled) return;
6767
+ errorLog5("[EdgeFormSheet] failed to load edge schema", e);
6768
+ setSchema(EMPTY_EDGE_SCHEMA);
6769
+ });
6770
+ return () => {
6771
+ cancelled = true;
6772
+ };
6773
+ }, [schemaProvider, label]);
6774
+ return schema;
6775
+ }
6776
+ function useEditableEdgeTypes(schemaProvider, active) {
6777
+ const [editable, setEditable] = useState15({});
6778
+ const labelsKey = useMemo10(
6779
+ () => Array.from(new Set(active.map((e) => e.label_))).sort().join("\0"),
6780
+ [active]
6781
+ );
6782
+ useEffect7(() => {
6783
+ let cancelled = false;
6784
+ const labels = labelsKey ? labelsKey.split("\0") : [];
6785
+ if (labels.length === 0) {
6786
+ setEditable({});
6787
+ return;
6788
+ }
6789
+ void (async () => {
6790
+ const entries = await Promise.all(
6791
+ labels.map(async (label) => {
6792
+ try {
6793
+ const schema = await schemaProvider.getEdgeSchema(label);
6794
+ return [label, hasProperties(schema)];
6795
+ } catch {
6796
+ return [label, false];
6797
+ }
6798
+ })
6799
+ );
6800
+ if (!cancelled) setEditable(Object.fromEntries(entries));
6801
+ })();
6802
+ return () => {
6803
+ cancelled = true;
6804
+ };
6805
+ }, [schemaProvider, labelsKey]);
6806
+ return editable;
6807
+ }
6808
+ function SectionTitle2({
6809
+ children
6810
+ }) {
6811
+ return /* @__PURE__ */ jsx23("div", { className: "mb-2", children: /* @__PURE__ */ jsx23("span", { className: "heading-meta", children }) });
6812
+ }
6813
+ function EdgeRow({
6814
+ label,
6815
+ onEdit,
6816
+ onDelete,
6817
+ onAdd
6818
+ }) {
6819
+ return /* @__PURE__ */ jsxs21("div", { className: "group/row flex items-center gap-2 px-3 py-2", children: [
6820
+ /* @__PURE__ */ jsx23("span", { className: "flex-1 text-sm font-medium", children: label }),
6821
+ /* @__PURE__ */ jsxs21("span", { className: "flex gap-1", children: [
6822
+ onAdd && /* @__PURE__ */ jsx23(IconButton3, { label: "Create edge", revealOnHover: true, onClick: onAdd, children: /* @__PURE__ */ jsx23(Plus2, {}) }),
6823
+ onEdit && /* @__PURE__ */ jsx23(IconButton3, { label: "Edit edge", revealOnHover: true, onClick: onEdit, children: /* @__PURE__ */ jsx23(Pencil2, {}) }),
6824
+ onDelete && /* @__PURE__ */ jsx23(
6825
+ IconButton3,
6826
+ {
6827
+ label: "Delete edge",
6828
+ tone: "destructive",
6829
+ revealOnHover: true,
6830
+ onClick: onDelete,
6831
+ children: /* @__PURE__ */ jsx23(Trash22, {})
6832
+ }
6833
+ )
6834
+ ] })
6835
+ ] });
6836
+ }
6837
+ function PairLists({
6838
+ nodeALabel,
6839
+ nodeBLabel,
6840
+ nodeAId,
6841
+ active,
6842
+ available,
6843
+ editableTypes,
6844
+ onEdit,
6845
+ onDelete,
6846
+ onCreate
6847
+ }) {
6848
+ const empty = active.length === 0 && available.length === 0;
6849
+ const labelCounts = available.reduce((acc, e) => {
6850
+ acc[e.label] = (acc[e.label] ?? 0) + 1;
6851
+ return acc;
6852
+ }, {});
6853
+ const rowLabel = (entry) => {
6854
+ if (labelCounts[entry.label] <= 1) return entry.label;
6855
+ const from = entry.sourceId === nodeAId ? nodeALabel : nodeBLabel;
6856
+ const to = entry.sourceId === nodeAId ? nodeBLabel : nodeALabel;
6857
+ return `${entry.label} (${from} -> ${to})`;
6858
+ };
6859
+ return /* @__PURE__ */ jsxs21("div", { className: "flex flex-col gap-4", children: [
6860
+ empty && /* @__PURE__ */ jsxs21("p", { className: "text-xs text-muted-foreground", children: [
6861
+ "No edges are defined between ",
6862
+ nodeALabel,
6863
+ " and ",
6864
+ nodeBLabel,
6865
+ "."
6866
+ ] }),
6867
+ active.length > 0 && /* @__PURE__ */ jsxs21("div", { children: [
6868
+ /* @__PURE__ */ jsx23(SectionTitle2, { children: "Active edges" }),
6869
+ /* @__PURE__ */ jsx23("div", { className: "rounded-lg border divide-y", children: active.map((edge) => /* @__PURE__ */ jsx23(
6870
+ EdgeRow,
6871
+ {
6872
+ label: edge.label_,
6873
+ onEdit: editableTypes[edge.label_] ? () => onEdit(edge) : void 0,
6874
+ onDelete: () => onDelete(edge)
6875
+ },
6876
+ edge.id_
6877
+ )) })
6878
+ ] }),
6879
+ available.length > 0 && /* @__PURE__ */ jsxs21("div", { children: [
6880
+ /* @__PURE__ */ jsx23(SectionTitle2, { children: "Available edges" }),
6881
+ /* @__PURE__ */ jsx23("div", { className: "rounded-lg border divide-y", children: available.map((entry) => /* @__PURE__ */ jsx23(
6882
+ EdgeRow,
6883
+ {
6884
+ label: rowLabel(entry),
6885
+ onAdd: () => onCreate(entry)
6886
+ },
6887
+ `${entry.label}:${entry.sourceId}:${entry.targetId}`
6888
+ )) })
6889
+ ] })
6890
+ ] });
6891
+ }
6892
+ function DraftForm({
6893
+ draft,
6894
+ schema,
6895
+ widgets,
6896
+ onSave,
6897
+ onCancel
6898
+ }) {
6899
+ const label = draft.mode === "create" ? draft.entry.label : draft.edge.label_;
6900
+ const data = draft.mode === "edit" ? draft.edge.properties_ ?? {} : {};
6901
+ return /* @__PURE__ */ jsxs21("div", { children: [
6902
+ /* @__PURE__ */ jsxs21(SectionTitle2, { children: [
6903
+ draft.mode === "create" ? "Add" : "Edit",
6904
+ " ",
6905
+ label
6906
+ ] }),
6907
+ /* @__PURE__ */ jsx23(
6908
+ JsonSchemaFormRenderer3,
6909
+ {
6910
+ schema,
6911
+ data,
6912
+ widgets,
6913
+ showActions: true,
6914
+ allowPristineSave: draft.mode === "create",
6915
+ saveLabel: draft.mode === "create" ? "Add" : "Save",
6916
+ cancelLabel: "Cancel",
6917
+ showExtraProperties: true,
6918
+ onUpdate: onSave,
6919
+ onCancel
6920
+ },
6921
+ label
6922
+ )
6923
+ ] });
6924
+ }
6925
+ function EdgeFormSheet({
6926
+ open,
6927
+ onOpenChange,
6928
+ nodeA,
6929
+ nodeB,
6930
+ port,
6931
+ schemaProvider,
6932
+ widgets,
6933
+ onChanged
6934
+ }) {
6935
+ const [reloadKey, setReloadKey] = useState15(0);
6936
+ const [draft, setDraft] = useState15(null);
6937
+ const [error, setError] = useState15("");
6938
+ const [pendingDelete, setPendingDelete] = useState15(null);
6939
+ const { active, available } = usePairState(
6940
+ port,
6941
+ schemaProvider,
6942
+ open,
6943
+ nodeA.id_,
6944
+ nodeA.label_,
6945
+ nodeB.id_,
6946
+ nodeB.label_,
6947
+ reloadKey
6948
+ );
6949
+ const schema = useEdgeSchema(schemaProvider, draft);
6950
+ const editableTypes = useEditableEdgeTypes(schemaProvider, active);
6951
+ const nodeAId = nodeA.id_;
6952
+ const nodeBId = nodeB.id_;
6953
+ useEffect7(() => {
6954
+ if (open) {
6955
+ setDraft(null);
6956
+ setError("");
6957
+ setPendingDelete(null);
6958
+ }
6959
+ }, [open, nodeAId, nodeBId]);
6960
+ const reload = useCallback9(() => {
6961
+ setReloadKey((k) => k + 1);
6962
+ onChanged?.();
6963
+ }, [onChanged]);
6964
+ const handleSave = useCallback9(
6965
+ (formData) => {
6966
+ if (!draft) return;
6967
+ const { properties } = extractRelationFields(formData);
6968
+ void (async () => {
6969
+ try {
6970
+ if (draft.mode === "create") {
6971
+ await port.createEdge({
6972
+ label_: draft.entry.label,
6973
+ start_id_: draft.entry.sourceId,
6974
+ end_id_: draft.entry.targetId,
6975
+ properties_: properties
6976
+ });
6977
+ } else {
6978
+ await port.updateEdge({
6979
+ id_: draft.edge.id_,
6980
+ label_: draft.edge.label_,
6981
+ start_id_: draft.edge.start_id_,
6982
+ end_id_: draft.edge.end_id_,
6983
+ properties_: properties
6984
+ });
6985
+ }
6986
+ setDraft(null);
6987
+ setError("");
6988
+ reload();
6989
+ } catch (e) {
6990
+ setError(e instanceof Error ? e.message : "Failed to save edge");
6991
+ }
6992
+ })();
6993
+ },
6994
+ [draft, port, reload]
6995
+ );
6996
+ const confirmDelete = useCallback9(async () => {
6997
+ const edge = pendingDelete;
6998
+ if (!edge || edge.id_ == null) return;
6999
+ try {
7000
+ await port.deleteEdge(edge.id_);
7001
+ setError("");
7002
+ setPendingDelete(null);
7003
+ reload();
7004
+ } catch (e) {
7005
+ setPendingDelete(null);
7006
+ setError(e instanceof Error ? e.message : "Failed to delete edge");
7007
+ }
7008
+ }, [pendingDelete, port, reload]);
7009
+ return /* @__PURE__ */ jsxs21(Sheet5, { open, onOpenChange, children: [
7010
+ /* @__PURE__ */ jsxs21(SheetContent5, { children: [
7011
+ /* @__PURE__ */ jsxs21(SheetHeader5, { children: [
7012
+ /* @__PURE__ */ jsx23(SheetTitle5, { children: "Edges" }),
7013
+ /* @__PURE__ */ jsx23(SheetDescription5, { children: "Manage the edges between the two selected nodes." })
7014
+ ] }),
7015
+ /* @__PURE__ */ jsxs21(SheetBody5, { className: "flex flex-col gap-4", children: [
7016
+ /* @__PURE__ */ jsxs21("div", { className: "flex items-center gap-2 text-sm", children: [
7017
+ /* @__PURE__ */ jsx23("span", { className: "font-medium", children: extractDisplayLabel(nodeA) }),
7018
+ /* @__PURE__ */ jsx23(ArrowRight2, { className: "h-4 w-4 text-muted-foreground" }),
7019
+ /* @__PURE__ */ jsx23("span", { className: "font-medium", children: extractDisplayLabel(nodeB) })
7020
+ ] }),
7021
+ !draft && /* @__PURE__ */ jsx23(
7022
+ PairLists,
7023
+ {
7024
+ nodeALabel: nodeA.label_,
7025
+ nodeBLabel: nodeB.label_,
7026
+ nodeAId: nodeA.id_,
7027
+ active,
7028
+ available,
7029
+ editableTypes,
7030
+ onEdit: (edge) => setDraft({ mode: "edit", edge }),
7031
+ onDelete: (edge) => setPendingDelete(edge),
7032
+ onCreate: (entry) => setDraft({ mode: "create", entry })
7033
+ }
7034
+ ),
7035
+ draft && schema && /* @__PURE__ */ jsx23(
7036
+ DraftForm,
7037
+ {
7038
+ draft,
7039
+ schema,
7040
+ widgets,
7041
+ onSave: handleSave,
7042
+ onCancel: () => setDraft(null)
7043
+ }
7044
+ ),
7045
+ error && /* @__PURE__ */ jsx23(Alert2, { variant: "destructive", children: error })
7046
+ ] })
7047
+ ] }),
7048
+ /* @__PURE__ */ jsx23(
7049
+ DeleteEdgeConfirm,
7050
+ {
7051
+ edge: pendingDelete,
7052
+ onCancel: () => setPendingDelete(null),
7053
+ onConfirm: () => void confirmDelete()
7054
+ }
7055
+ )
7056
+ ] });
7057
+ }
7058
+ function DeleteEdgeConfirm({
7059
+ edge,
7060
+ onCancel,
7061
+ onConfirm
7062
+ }) {
7063
+ return /* @__PURE__ */ jsx23(Dialog, { open: !!edge, onOpenChange: (o) => !o && onCancel(), children: /* @__PURE__ */ jsxs21(DialogContent, { children: [
7064
+ /* @__PURE__ */ jsxs21(DialogHeader, { children: [
7065
+ /* @__PURE__ */ jsx23(DialogTitle, { children: "Delete edge" }),
7066
+ /* @__PURE__ */ jsxs21(DialogDescription, { children: [
7067
+ "Delete the edge ",
7068
+ /* @__PURE__ */ jsx23("strong", { children: edge?.label_ }),
7069
+ "? This removes the relationship between the two nodes."
7070
+ ] })
7071
+ ] }),
7072
+ /* @__PURE__ */ jsxs21(DialogFooter, { children: [
7073
+ /* @__PURE__ */ jsx23(Button7, { variant: "outline", onClick: onCancel, children: "Cancel" }),
7074
+ /* @__PURE__ */ jsx23(Button7, { variant: "destructive", onClick: onConfirm, children: "Delete" })
7075
+ ] })
7076
+ ] }) });
7077
+ }
7078
+
7079
+ // src/graph-edit/DeleteNodeDialog.tsx
7080
+ import { useCallback as useCallback10, useEffect as useEffect8, useState as useState16 } from "react";
7081
+ import {
7082
+ Dialog as Dialog2,
7083
+ DialogContent as DialogContent2,
7084
+ DialogHeader as DialogHeader2,
7085
+ DialogTitle as DialogTitle2,
7086
+ DialogDescription as DialogDescription2,
7087
+ DialogFooter as DialogFooter2,
7088
+ Button as Button8,
7089
+ Alert as Alert3
7090
+ } from "@petrarca/sonnet-ui";
7091
+ import { errorLog as errorLog6 } from "@petrarca/sonnet-core";
7092
+ import { jsx as jsx24, jsxs as jsxs22 } from "react/jsx-runtime";
7093
+ var EMPTY_DEPENDENT = {
7094
+ inbound: [],
7095
+ outbound: [],
7096
+ names: {}
7097
+ };
7098
+ function useDependentEdges(port, open, node) {
7099
+ const [dependent, setDependent] = useState16(EMPTY_DEPENDENT);
7100
+ useEffect8(() => {
7101
+ if (!open) return;
7102
+ if (node.id_ == null) {
7103
+ setDependent(EMPTY_DEPENDENT);
7104
+ return;
7105
+ }
7106
+ let cancelled = false;
7107
+ void (async () => {
7108
+ try {
7109
+ const [inbound, outbound] = await Promise.all([
7110
+ port.getInboundEdges(node.id_),
7111
+ port.getOutboundEdges(node.id_)
7112
+ ]);
7113
+ if (cancelled) return;
7114
+ const neighbourIds = Array.from(
7115
+ /* @__PURE__ */ new Set([
7116
+ ...inbound.map((e) => e.start_id_),
7117
+ ...outbound.map((e) => e.end_id_)
7118
+ ])
7119
+ );
7120
+ const names = await resolveNames(port, neighbourIds);
7121
+ if (cancelled) return;
7122
+ setDependent({ inbound, outbound, names });
7123
+ } catch (e) {
7124
+ if (cancelled) return;
7125
+ errorLog6("[DeleteNodeDialog] failed to load dependent edges", e);
7126
+ }
7127
+ })();
7128
+ return () => {
7129
+ cancelled = true;
7130
+ };
7131
+ }, [port, open, node.id_]);
7132
+ return dependent;
7133
+ }
7134
+ async function resolveNames(port, ids) {
7135
+ if (ids.length === 0) return {};
7136
+ const nodes = await port.getNodes(ids);
7137
+ const names = {};
7138
+ for (const n of nodes) {
7139
+ if (n.id_ != null) names[n.id_] = extractDisplayLabel(n);
7140
+ }
7141
+ return names;
7142
+ }
7143
+ function EdgeList({
7144
+ title,
7145
+ edges,
7146
+ names,
7147
+ direction
7148
+ }) {
7149
+ if (edges.length === 0) return null;
7150
+ return /* @__PURE__ */ jsxs22("div", { className: "mb-2", children: [
7151
+ /* @__PURE__ */ jsx24("span", { className: "heading-meta block", children: title }),
7152
+ /* @__PURE__ */ jsx24("ul", { className: "list-disc pl-6 text-sm", children: edges.map((edge) => {
7153
+ const neighbour = direction === "inbound" ? names[edge.start_id_] : names[edge.end_id_];
7154
+ const text = direction === "inbound" ? `${neighbour ?? "?"} \u2192 ${edge.label_}` : `${edge.label_} \u2192 ${neighbour ?? "?"}`;
7155
+ return /* @__PURE__ */ jsx24("li", { children: neighbour ? text : edge.label_ }, edge.id_);
7156
+ }) })
7157
+ ] });
7158
+ }
7159
+ function DeleteNodeDialog({
7160
+ open,
7161
+ onOpenChange,
7162
+ node,
7163
+ port,
7164
+ onDeleted
7165
+ }) {
7166
+ const { inbound, outbound, names } = useDependentEdges(port, open, node);
7167
+ const [error, setError] = useState16("");
7168
+ const hasEdges = inbound.length > 0 || outbound.length > 0;
7169
+ const handleDelete = useCallback10(() => {
7170
+ if (node.id_ == null) return;
7171
+ void (async () => {
7172
+ try {
7173
+ await port.deleteNode(node.id_);
7174
+ setError("");
7175
+ onDeleted?.(node.id_);
7176
+ onOpenChange(false);
7177
+ } catch (e) {
7178
+ setError(e instanceof Error ? e.message : "Unable to delete node");
7179
+ }
7180
+ })();
7181
+ }, [port, node.id_, onDeleted, onOpenChange]);
7182
+ return /* @__PURE__ */ jsx24(Dialog2, { open, onOpenChange, children: /* @__PURE__ */ jsxs22(DialogContent2, { children: [
7183
+ /* @__PURE__ */ jsxs22(DialogHeader2, { children: [
7184
+ /* @__PURE__ */ jsx24(DialogTitle2, { children: "Delete node" }),
7185
+ /* @__PURE__ */ jsxs22(DialogDescription2, { children: [
7186
+ "Delete the node ",
7187
+ /* @__PURE__ */ jsx24("strong", { children: extractDisplayLabel(node) }),
7188
+ "?"
7189
+ ] })
7190
+ ] }),
7191
+ !hasEdges && /* @__PURE__ */ jsx24("p", { className: "text-sm text-muted-foreground", children: "No connected edges." }),
7192
+ /* @__PURE__ */ jsx24(
7193
+ EdgeList,
7194
+ {
7195
+ title: "Inbound edges",
7196
+ edges: inbound,
7197
+ names,
7198
+ direction: "inbound"
7199
+ }
7200
+ ),
7201
+ /* @__PURE__ */ jsx24(
7202
+ EdgeList,
7203
+ {
7204
+ title: "Outbound edges",
7205
+ edges: outbound,
7206
+ names,
7207
+ direction: "outbound"
7208
+ }
7209
+ ),
7210
+ error && /* @__PURE__ */ jsx24(Alert3, { variant: "destructive", children: error }),
7211
+ /* @__PURE__ */ jsxs22(DialogFooter2, { children: [
7212
+ /* @__PURE__ */ jsx24(Button8, { variant: "outline", onClick: () => onOpenChange(false), children: "Cancel" }),
7213
+ /* @__PURE__ */ jsx24(Button8, { variant: "destructive", onClick: handleDelete, children: "Delete" })
7214
+ ] })
7215
+ ] }) });
7216
+ }
7217
+
7218
+ // src/graph-edit/GraphEditToolbar.tsx
7219
+ import { Fragment as Fragment8, jsx as jsx25, jsxs as jsxs23 } from "react/jsx-runtime";
7220
+ function toPerms(caps) {
7221
+ const canCreateNode = caps.createNode === "edit";
7222
+ const canUpdateNode = caps.updateNode === "edit";
7223
+ const canDeleteNode = caps.deleteNode === "edit";
7224
+ const canEditEdges = caps.createEdge === "edit" || caps.updateEdge === "edit" || caps.deleteEdge === "edit";
7225
+ return {
7226
+ canCreateNode,
7227
+ canUpdateNode,
7228
+ canDeleteNode,
7229
+ canEditEdges,
7230
+ anyEditable: canCreateNode || canUpdateNode || canDeleteNode || canEditEdges
7231
+ };
7232
+ }
7233
+ function ToolbarButtons({
7234
+ perms,
7235
+ count,
7236
+ onOpen
7237
+ }) {
7238
+ return /* @__PURE__ */ jsxs23("div", { className: "flex gap-0.5", children: [
7239
+ perms.canCreateNode && /* @__PURE__ */ jsx25(
7240
+ IconButton4,
7241
+ {
7242
+ label: "Add node",
7243
+ variant: "outline",
7244
+ disabled: count > 0,
7245
+ onClick: () => onOpen("node-create"),
7246
+ children: /* @__PURE__ */ jsx25(Plus3, {})
7247
+ }
7248
+ ),
7249
+ perms.canUpdateNode && /* @__PURE__ */ jsx25(
7250
+ IconButton4,
7251
+ {
7252
+ label: "Edit node",
7253
+ variant: "outline",
7254
+ disabled: count !== 1,
7255
+ onClick: () => onOpen("node-edit"),
7256
+ children: /* @__PURE__ */ jsx25(Edit, {})
7257
+ }
7258
+ ),
7259
+ perms.canEditEdges && /* @__PURE__ */ jsx25(
7260
+ IconButton4,
7261
+ {
7262
+ label: "Edges",
7263
+ variant: "outline",
7264
+ disabled: count !== 2,
7265
+ onClick: () => onOpen("edge"),
7266
+ children: /* @__PURE__ */ jsx25(Link, {})
7267
+ }
7268
+ ),
7269
+ perms.canDeleteNode && /* @__PURE__ */ jsx25(
7270
+ IconButton4,
7271
+ {
7272
+ label: "Delete node",
7273
+ variant: "outline",
7274
+ tone: "destructive",
7275
+ disabled: count !== 1,
7276
+ onClick: () => onOpen("delete"),
7277
+ children: /* @__PURE__ */ jsx25(Trash23, {})
7278
+ }
7279
+ )
7280
+ ] });
7281
+ }
7282
+ function ToolbarDialogs({
7283
+ perms,
7284
+ dialog,
7285
+ firstNode,
7286
+ secondNode,
7287
+ port,
7288
+ schemaProvider,
7289
+ widgets,
7290
+ onClose,
7291
+ onMutated
7292
+ }) {
7293
+ const onChange = useCallback11(
7294
+ (o) => {
7295
+ if (!o) onClose();
7296
+ },
7297
+ [onClose]
7298
+ );
7299
+ return /* @__PURE__ */ jsxs23(Fragment8, { children: [
7300
+ (perms.canCreateNode || perms.canUpdateNode) && /* @__PURE__ */ jsx25(
7301
+ NodeFormSheet,
7302
+ {
7303
+ open: dialog === "node-create" || dialog === "node-edit",
7304
+ onOpenChange: onChange,
7305
+ node: dialog === "node-edit" ? firstNode : null,
7306
+ port,
7307
+ schemaProvider,
7308
+ widgets,
7309
+ onSaved: onMutated
7310
+ }
7311
+ ),
7312
+ perms.canEditEdges && firstNode && secondNode && /* @__PURE__ */ jsx25(
7313
+ EdgeFormSheet,
7314
+ {
7315
+ open: dialog === "edge",
7316
+ onOpenChange: onChange,
7317
+ nodeA: firstNode,
7318
+ nodeB: secondNode,
7319
+ port,
7320
+ schemaProvider,
7321
+ widgets,
7322
+ onChanged: onMutated
7323
+ }
7324
+ ),
7325
+ perms.canDeleteNode && firstNode && /* @__PURE__ */ jsx25(
7326
+ DeleteNodeDialog,
7327
+ {
7328
+ open: dialog === "delete",
7329
+ onOpenChange: onChange,
7330
+ node: firstNode,
7331
+ port,
7332
+ onDeleted: onMutated
7333
+ }
7334
+ )
7335
+ ] });
7336
+ }
7337
+ function GraphEditToolbar({
7338
+ port,
7339
+ schemaProvider,
7340
+ capabilities,
7341
+ readOnly,
7342
+ widgets,
7343
+ onMutated
7344
+ }) {
7345
+ const useSelection3 = useWorkspaceSelection();
7346
+ const selectedNodeIds = useSelection3(
7347
+ (s) => s.selectedNodeIds
7348
+ );
7349
+ const live = useLiveGraphData();
7350
+ const [dialog, setDialog] = useState17(null);
7351
+ const perms = useMemo11(
7352
+ () => toPerms(resolveGraphCapabilities(capabilities, readOnly)),
7353
+ [capabilities, readOnly]
7354
+ );
7355
+ const selectedNodes = useMemo11(
7356
+ () => selectedNodeIds.map((id) => live.normalized.nodesById[id]).filter(Boolean),
7357
+ [selectedNodeIds, live.normalized]
7358
+ );
7359
+ const stableOnMutated = useStableRef(onMutated);
7360
+ if (!perms.anyEditable) return null;
7361
+ return /* @__PURE__ */ jsxs23(Fragment8, { children: [
7362
+ /* @__PURE__ */ jsx25(
7363
+ ToolbarButtons,
7364
+ {
7365
+ perms,
7366
+ count: selectedNodes.length,
7367
+ onOpen: setDialog
7368
+ }
7369
+ ),
7370
+ /* @__PURE__ */ jsx25(
7371
+ ToolbarDialogs,
7372
+ {
7373
+ perms,
7374
+ dialog,
7375
+ firstNode: selectedNodes[0],
7376
+ secondNode: selectedNodes[1],
7377
+ port,
7378
+ schemaProvider,
7379
+ widgets,
7380
+ onClose: () => setDialog(null),
7381
+ onMutated: stableOnMutated
7382
+ }
7383
+ )
7384
+ ] });
7385
+ }
4395
7386
  export {
4396
7387
  ActionPanel_default as ActionPanel,
7388
+ DeleteNodeDialog,
4397
7389
  EMPTY_NORMALIZED,
7390
+ EdgeFormSheet,
4398
7391
  EdgeInfoCard_default as EdgeInfoCard,
4399
7392
  GraphA11yList,
7393
+ GraphEditToolbar,
7394
+ GraphModelEditor,
4400
7395
  GraphRendererProvider,
4401
7396
  GraphStoresContext,
4402
7397
  GraphVisualizer_default as GraphVisualizer,
7398
+ ModelActionPanel,
7399
+ NodeFormSheet,
4403
7400
  NodeInfoCard_default as NodeInfoCard,
7401
+ NodeTypeNameSheet,
4404
7402
  Overview_default as Overview,
7403
+ PropertyFormSheet,
7404
+ RelationFormSheet,
7405
+ TypeDetailPanel,
7406
+ addEdgeType,
7407
+ addNodeType,
7408
+ addRelation,
4405
7409
  buildColorPalette,
4406
7410
  buildColorPaletteFromNodes,
7411
+ capabilitiesSchema,
7412
+ cardinalityLabel,
4407
7413
  createActionPanelStore,
4408
7414
  createGraphDataStore,
4409
7415
  createGraphExplorationStore,
4410
7416
  createGraphFilterStore,
4411
7417
  createGraphFocusStore,
4412
7418
  createSelectionStore,
7419
+ deleteEdgeType,
7420
+ deleteEdgeTypeProperty,
7421
+ deleteNodeProperty,
7422
+ deleteNodeType,
7423
+ deleteRelation,
7424
+ deleteRelationProperty,
7425
+ edgeTypeView,
7426
+ editableRelations,
4413
7427
  enrichEdge,
4414
7428
  enrichEdges,
4415
7429
  enrichNode,
4416
7430
  enrichNodes,
7431
+ enrichSchemaWithRelations,
4417
7432
  extractDisplayLabel,
7433
+ extractRelationFields,
7434
+ findEdgeType,
7435
+ findType,
7436
+ graphCapabilitiesSchema,
7437
+ groupEdgesByRelation,
7438
+ hasProperties,
7439
+ hasRelationFields,
7440
+ initializeFormData,
7441
+ isEdgeType,
7442
+ isNodeType,
4418
7443
  isRawGraphNode,
7444
+ isRelationField,
4419
7445
  isVisualEdge,
4420
7446
  isVisualNode,
7447
+ modelToGraph,
7448
+ nodeTypeView,
4421
7449
  normalizeGraph,
7450
+ reconcileFilter,
7451
+ relationFieldName,
7452
+ relationTitle,
7453
+ relationTypeFromField,
7454
+ relationView,
7455
+ renameEdgeType,
7456
+ renameNodeType,
7457
+ resolveCapabilities,
7458
+ resolveGraphCapabilities,
7459
+ resolveRelationEdge,
7460
+ setEdgeTypeProperty,
7461
+ setNodeProperty,
7462
+ setRelationProperty,
4422
7463
  toCytoscapeEdge,
4423
7464
  toCytoscapeNode,
4424
7465
  toReagraphEdge,
4425
7466
  toReagraphNode,
7467
+ typeLabel,
7468
+ updateRelation,
4426
7469
  useActionPanelStore,
4427
7470
  useCytoscapeData,
4428
7471
  GraphFocus_default as useGraphFocus,
4429
7472
  useGraphRenderer,
4430
7473
  useLiveGraphData,
4431
7474
  useReagraphData,
7475
+ useStableRef,
4432
7476
  useWorkspaceGraphData,
4433
7477
  useWorkspaceGraphExploration,
4434
7478
  useWorkspaceGraphFilter,
4435
- useWorkspaceSelection
7479
+ useWorkspaceSelection,
7480
+ validateCapabilities,
7481
+ validateGraphCapabilities
4436
7482
  };
4437
7483
  //# sourceMappingURL=index.js.map