@petrarca/sonnet-graph 0.4.2 → 0.6.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,9 +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 expandBtnRef = useRef(null);
1691
- const dragRef = useRef(null);
1692
- const onPointerDown = useCallback2(
1704
+ const expandBtnRef = useRef2(null);
1705
+ const dragRef = useRef2(null);
1706
+ const onPointerDown = useCallback3(
1693
1707
  (e) => {
1694
1708
  if (e.target.closest("button")) return;
1695
1709
  e.preventDefault();
@@ -1703,13 +1717,13 @@ function ExpandOptionsPanel({
1703
1717
  },
1704
1718
  [offset]
1705
1719
  );
1706
- const onPointerMove = useCallback2((e) => {
1720
+ const onPointerMove = useCallback3((e) => {
1707
1721
  if (!dragRef.current) return;
1708
1722
  const dx = e.clientX - dragRef.current.startX;
1709
1723
  const dy = e.clientY - dragRef.current.startY;
1710
1724
  setOffset({ x: dragRef.current.origX + dx, y: dragRef.current.origY + dy });
1711
1725
  }, []);
1712
- const onPointerUp = useCallback2(() => {
1726
+ const onPointerUp = useCallback3(() => {
1713
1727
  dragRef.current = null;
1714
1728
  }, []);
1715
1729
  useEffect2(() => {
@@ -1865,9 +1879,9 @@ function ReaContextMenu({
1865
1879
  }, [onClose]);
1866
1880
  const handleFocus = () => {
1867
1881
  if (elementType === "node") {
1868
- const nodeId = data.id;
1869
- devLog7("[ReaContextMenu] Focus on node and neighborhood:", nodeId);
1870
- useGraphExploration.getState().setFocusModeWithNeighborhood(nodeId);
1882
+ const nodeId2 = data.id;
1883
+ devLog7("[ReaContextMenu] Focus on node and neighborhood:", nodeId2);
1884
+ useGraphExploration.getState().setFocusModeWithNeighborhood(nodeId2);
1871
1885
  } else {
1872
1886
  const startNodeId = data.data.start_id_;
1873
1887
  devLog7("[ReaContextMenu] Focus on edge start node:", startNodeId);
@@ -1893,58 +1907,58 @@ function ReaContextMenu({
1893
1907
  };
1894
1908
  const handleHideNode = () => {
1895
1909
  if (elementType === "node") {
1896
- const nodeId = data.id;
1897
- devLog7("[ReaContextMenu] Hide node:", nodeId);
1898
- useGraphExploration.getState().hideElements([nodeId]);
1910
+ const nodeId2 = data.id;
1911
+ devLog7("[ReaContextMenu] Hide node:", nodeId2);
1912
+ useGraphExploration.getState().hideElements([nodeId2]);
1899
1913
  }
1900
1914
  onClose();
1901
1915
  };
1902
1916
  const handleHideIncoming = () => {
1903
1917
  if (elementType === "node") {
1904
- const nodeId = data.id;
1905
- devLog7("[ReaContextMenu] Hide incoming edges:", nodeId);
1906
- useGraphExploration.getState().hideNodeIncoming(nodeId);
1918
+ const nodeId2 = data.id;
1919
+ devLog7("[ReaContextMenu] Hide incoming edges:", nodeId2);
1920
+ useGraphExploration.getState().hideNodeIncoming(nodeId2);
1907
1921
  }
1908
1922
  onClose();
1909
1923
  };
1910
1924
  const handleHideOutgoing = () => {
1911
1925
  if (elementType === "node") {
1912
- const nodeId = data.id;
1913
- devLog7("[ReaContextMenu] Hide outgoing edges:", nodeId);
1914
- useGraphExploration.getState().hideNodeOutgoing(nodeId);
1926
+ const nodeId2 = data.id;
1927
+ devLog7("[ReaContextMenu] Hide outgoing edges:", nodeId2);
1928
+ useGraphExploration.getState().hideNodeOutgoing(nodeId2);
1915
1929
  }
1916
1930
  onClose();
1917
1931
  };
1918
1932
  const handleExpandWithOptions = (options) => {
1919
1933
  if (elementType === "node") {
1920
- const nodeId = data.id;
1921
- devLog7("[ReaContextMenu] Expand node with options:", nodeId, options);
1922
- 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);
1923
1937
  }
1924
1938
  onClose();
1925
1939
  };
1926
1940
  const handleHideForwardPath = () => {
1927
1941
  if (elementType === "edge") {
1928
- const edgeId = data.id;
1929
- devLog7("[ReaContextMenu] Hide forward path from edge:", edgeId);
1930
- useGraphExploration.getState().hideEdgeForwardPath(edgeId);
1942
+ const edgeId2 = data.id;
1943
+ devLog7("[ReaContextMenu] Hide forward path from edge:", edgeId2);
1944
+ useGraphExploration.getState().hideEdgeForwardPath(edgeId2);
1931
1945
  }
1932
1946
  onClose();
1933
1947
  };
1934
1948
  const handleHideReversePath = () => {
1935
1949
  if (elementType === "edge") {
1936
- const edgeId = data.id;
1937
- devLog7("[ReaContextMenu] Hide reverse path from edge:", edgeId);
1938
- useGraphExploration.getState().hideEdgeReversePath(edgeId);
1950
+ const edgeId2 = data.id;
1951
+ devLog7("[ReaContextMenu] Hide reverse path from edge:", edgeId2);
1952
+ useGraphExploration.getState().hideEdgeReversePath(edgeId2);
1939
1953
  }
1940
1954
  onClose();
1941
1955
  };
1942
1956
  const handleFocusOnEdgeType = () => {
1943
1957
  if (elementType === "edge") {
1944
- const edgeId = data.id;
1958
+ const edgeId2 = data.id;
1945
1959
  const edgeLabel = data.data?.label_ || "edge";
1946
- devLog7("[ReaContextMenu] Focus on edge type:", edgeId, edgeLabel);
1947
- useGraphExploration.getState().focusOnEdgeType(edgeId);
1960
+ devLog7("[ReaContextMenu] Focus on edge type:", edgeId2, edgeLabel);
1961
+ useGraphExploration.getState().focusOnEdgeType(edgeId2);
1948
1962
  }
1949
1963
  onClose();
1950
1964
  };
@@ -2070,7 +2084,7 @@ function ReaGraph({
2070
2084
  extraToolbarContent,
2071
2085
  getNodeEdgeTypes
2072
2086
  }) {
2073
- const isMountedRef = useRef2(false);
2087
+ const isMountedRef = useRef3(false);
2074
2088
  const { nodes, edges } = useReagraphData();
2075
2089
  const { normalized } = useLiveGraphData();
2076
2090
  useEffect3(() => {
@@ -2078,7 +2092,7 @@ function ReaGraph({
2078
2092
  if (nodes.length > 0) devLog8("[ReGraph] sample node", nodes[0]);
2079
2093
  if (edges.length > 0) devLog8("[ReGraph] sample edge", edges[0]);
2080
2094
  }, [nodes, edges]);
2081
- const canvas = useRef2(null);
2095
+ const canvas = useRef3(null);
2082
2096
  const [hoveredNode, setHoveredNode] = useState5(
2083
2097
  void 0
2084
2098
  );
@@ -2139,7 +2153,7 @@ function ReaGraph({
2139
2153
  clearSelections();
2140
2154
  clearSelectionStore();
2141
2155
  }, [lastDataChangeTime, clearSelections, clearSelectionStore]);
2142
- const fitToScreen = useCallback3(
2156
+ const fitToScreen = useCallback4(
2143
2157
  (reason) => {
2144
2158
  if (!isMountedRef.current) return;
2145
2159
  if (nodes.length === 0) return;
@@ -2185,6 +2199,20 @@ function ReaGraph({
2185
2199
  isMountedRef.current = false;
2186
2200
  };
2187
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]);
2188
2216
  useEffect3(() => {
2189
2217
  if (isMountedRef.current) fitToScreen("counts-changed");
2190
2218
  }, [nodes.length, edges.length, fitToScreen]);
@@ -2300,7 +2328,7 @@ function ReaGraph({
2300
2328
  window.removeEventListener("resize", handleResize);
2301
2329
  };
2302
2330
  }, [fitToScreen]);
2303
- const firstDataRef = useRef2(true);
2331
+ const firstDataRef = useRef3(true);
2304
2332
  useEffect3(() => {
2305
2333
  if (!isMountedRef.current) return;
2306
2334
  if (nodes.length > 0) {
@@ -2401,9 +2429,9 @@ var ReaGraph_default = ReaGraph;
2401
2429
  // src/components/cyto/CytoscapeGraph.tsx
2402
2430
  import {
2403
2431
  useEffect as useEffect4,
2404
- useRef as useRef3,
2432
+ useRef as useRef4,
2405
2433
  useState as useState6,
2406
- useCallback as useCallback4
2434
+ useCallback as useCallback5
2407
2435
  } from "react";
2408
2436
  import { devLog as devLog10, errorLog as errorLog3 } from "@petrarca/sonnet-core";
2409
2437
  import cytoscape from "cytoscape";
@@ -2824,9 +2852,9 @@ var createNodeContextMenu = ({
2824
2852
  {
2825
2853
  content: "Focus",
2826
2854
  select: (ele) => {
2827
- const nodeId = ele.id();
2828
- devLog9("[CxtMenu] Focus on node and neighborhood:", nodeId);
2829
- useGraphExploration.getState().setFocusModeWithNeighborhood(nodeId);
2855
+ const nodeId2 = ele.id();
2856
+ devLog9("[CxtMenu] Focus on node and neighborhood:", nodeId2);
2857
+ useGraphExploration.getState().setFocusModeWithNeighborhood(nodeId2);
2830
2858
  }
2831
2859
  },
2832
2860
  {
@@ -2844,38 +2872,38 @@ var createNodeContextMenu = ({
2844
2872
  {
2845
2873
  content: "Hide Node",
2846
2874
  select: (ele) => {
2847
- const nodeId = ele.id();
2848
- devLog9("[CxtMenu] Hide node:", nodeId);
2849
- useGraphExploration.getState().hideElements([nodeId]);
2875
+ const nodeId2 = ele.id();
2876
+ devLog9("[CxtMenu] Hide node:", nodeId2);
2877
+ useGraphExploration.getState().hideElements([nodeId2]);
2850
2878
  }
2851
2879
  },
2852
2880
  {
2853
2881
  content: "Hide Incoming",
2854
2882
  select: (ele) => {
2855
- const nodeId = ele.id();
2856
- devLog9("[CxtMenu] Hide incoming (predecessors) of node:", nodeId);
2857
- useGraphExploration.getState().hideNodeIncoming(nodeId);
2883
+ const nodeId2 = ele.id();
2884
+ devLog9("[CxtMenu] Hide incoming (predecessors) of node:", nodeId2);
2885
+ useGraphExploration.getState().hideNodeIncoming(nodeId2);
2858
2886
  }
2859
2887
  },
2860
2888
  {
2861
2889
  content: "Hide Outgoing",
2862
2890
  select: (ele) => {
2863
- const nodeId = ele.id();
2864
- devLog9("[CxtMenu] Hide outgoing (successors) of node:", nodeId);
2865
- useGraphExploration.getState().hideNodeOutgoing(nodeId);
2891
+ const nodeId2 = ele.id();
2892
+ devLog9("[CxtMenu] Hide outgoing (successors) of node:", nodeId2);
2893
+ useGraphExploration.getState().hideNodeOutgoing(nodeId2);
2866
2894
  }
2867
2895
  },
2868
2896
  {
2869
2897
  content: "Expand",
2870
2898
  select: (ele) => {
2871
- const nodeId = ele.id();
2872
- devLog9("[CxtMenu] Expand request for node:", nodeId);
2899
+ const nodeId2 = ele.id();
2900
+ devLog9("[CxtMenu] Expand request for node:", nodeId2);
2873
2901
  if (onExpandRequest) {
2874
2902
  const pos = ele.renderedPosition();
2875
- const label = ele.data("label") || nodeId;
2876
- 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 });
2877
2905
  } else {
2878
- useGraphExploration.getState().expandNode(nodeId).catch((error) => {
2906
+ useGraphExploration.getState().expandNode(nodeId2).catch((error) => {
2879
2907
  errorLog2("[CxtMenu] Expansion failed:", error);
2880
2908
  toast3.error("Failed to expand node");
2881
2909
  });
@@ -2911,42 +2939,42 @@ var createEdgeContextMenu = ({
2911
2939
  {
2912
2940
  content: "Focus",
2913
2941
  select: (ele) => {
2914
- const edgeId = ele.id();
2942
+ const edgeId2 = ele.id();
2915
2943
  devLog9(
2916
2944
  "[CxtMenu] Focus on edge (auto-direction based on selection):",
2917
- edgeId
2945
+ edgeId2
2918
2946
  );
2919
- useGraphExploration.getState().focusEdge(edgeId);
2947
+ useGraphExploration.getState().focusEdge(edgeId2);
2920
2948
  }
2921
2949
  },
2922
2950
  {
2923
2951
  content: "Focus Edge Type",
2924
2952
  select: (ele) => {
2925
- const edgeId = ele.id();
2926
- devLog9("[CxtMenu] Focus on edge type:", edgeId);
2927
- useGraphExploration.getState().focusOnEdgeType(edgeId);
2953
+ const edgeId2 = ele.id();
2954
+ devLog9("[CxtMenu] Focus on edge type:", edgeId2);
2955
+ useGraphExploration.getState().focusOnEdgeType(edgeId2);
2928
2956
  }
2929
2957
  },
2930
2958
  {
2931
2959
  content: "Hide Forward Path",
2932
2960
  select: (ele) => {
2933
- const edgeId = ele.id();
2961
+ const edgeId2 = ele.id();
2934
2962
  devLog9(
2935
2963
  "[CxtMenu] Hide forward path (edge + target + downstream):",
2936
- edgeId
2964
+ edgeId2
2937
2965
  );
2938
- useGraphExploration.getState().hideEdgeForwardPath(edgeId);
2966
+ useGraphExploration.getState().hideEdgeForwardPath(edgeId2);
2939
2967
  }
2940
2968
  },
2941
2969
  {
2942
2970
  content: "Hide Reverse Path",
2943
2971
  select: (ele) => {
2944
- const edgeId = ele.id();
2972
+ const edgeId2 = ele.id();
2945
2973
  devLog9(
2946
2974
  "[CxtMenu] Hide reverse path (edge + source + upstream):",
2947
- edgeId
2975
+ edgeId2
2948
2976
  );
2949
- useGraphExploration.getState().hideEdgeReversePath(edgeId);
2977
+ useGraphExploration.getState().hideEdgeReversePath(edgeId2);
2950
2978
  }
2951
2979
  }
2952
2980
  ],
@@ -2997,9 +3025,9 @@ function CytoscapeExpandOverlay({
2997
3025
  {
2998
3026
  nodeLabel: expandRequest.nodeLabel,
2999
3027
  onExpand: (options) => {
3000
- const { nodeId } = expandRequest;
3028
+ const { nodeId: nodeId2 } = expandRequest;
3001
3029
  onClose();
3002
- useGraphExploration.getState().expandNode(nodeId, options).catch((err) => {
3030
+ useGraphExploration.getState().expandNode(nodeId2, options).catch((err) => {
3003
3031
  errorLog3("[CxtMenu] Expansion failed:", err);
3004
3032
  toast4.error("Failed to expand node");
3005
3033
  });
@@ -3329,7 +3357,7 @@ function useFullscreen(fitToScreen, cyRef) {
3329
3357
  return {
3330
3358
  fullscreen,
3331
3359
  setFullscreen,
3332
- toggleFullscreen: useCallback4(() => setFullscreen((f) => !f), [])
3360
+ toggleFullscreen: useCallback5(() => setFullscreen((f) => !f), [])
3333
3361
  };
3334
3362
  }
3335
3363
  function setupCytoscapeEventHandlers({
@@ -3392,8 +3420,8 @@ function setupCytoscapeEventHandlers({
3392
3420
  });
3393
3421
  cy.on("tap", "edge", (evt) => {
3394
3422
  const edgeEle = evt.target;
3395
- const edgeId = edgeEle.id();
3396
- 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);
3397
3425
  if (!edgeObj) return;
3398
3426
  const sourceId = edgeEle.data("source");
3399
3427
  const targetId = edgeEle.data("target");
@@ -3403,7 +3431,7 @@ function setupCytoscapeEventHandlers({
3403
3431
  const selectedNodeId = currentlySelectedNodes[0].id();
3404
3432
  if (selectedNodeId === sourceId || selectedNodeId === targetId) {
3405
3433
  devLog10("[CytoscapeGraph] tap edge - node+edge selection", {
3406
- edgeId,
3434
+ edgeId: edgeId2,
3407
3435
  selectedNode: selectedNodeId,
3408
3436
  direction: selectedNodeId === sourceId ? "outgoing" : "incoming"
3409
3437
  });
@@ -3417,7 +3445,7 @@ function setupCytoscapeEventHandlers({
3417
3445
  return;
3418
3446
  }
3419
3447
  }
3420
- devLog10("[CytoscapeGraph] tap edge - default (both endpoints)", { edgeId });
3448
+ devLog10("[CytoscapeGraph] tap edge - default (both endpoints)", { edgeId: edgeId2 });
3421
3449
  evtCy.batch(() => {
3422
3450
  evtCy.elements().unselect();
3423
3451
  evtCy.$id(sourceId).select();
@@ -3445,16 +3473,16 @@ function CytoscapeGraph({
3445
3473
  extraToolbarContent,
3446
3474
  getNodeEdgeTypes
3447
3475
  }) {
3448
- const containerRef = useRef3(null);
3449
- const cyRef = useRef3(null);
3450
- const isInitializedRef = useRef3(false);
3476
+ const containerRef = useRef4(null);
3477
+ const cyRef = useRef4(null);
3478
+ const isInitializedRef = useRef4(false);
3451
3479
  const [currentLayout, setCurrentLayout] = useState6(initialLayout);
3452
3480
  const [expandRequest, setExpandRequest] = useState6(
3453
3481
  null
3454
3482
  );
3455
- const nodesRef = useRef3([]);
3456
- const edgesRef = useRef3([]);
3457
- const pendingFocusActionRef = useRef3(null);
3483
+ const nodesRef = useRef4([]);
3484
+ const edgesRef = useRef4([]);
3485
+ const pendingFocusActionRef = useRef4(null);
3458
3486
  const cytoData = useCytoscapeData();
3459
3487
  const live = useLiveGraphData();
3460
3488
  const nodes = live.nodes;
@@ -3474,31 +3502,51 @@ function CytoscapeGraph({
3474
3502
  setSelectedEdge: setSelectedEdgeStore,
3475
3503
  clear: clearSelectionStore
3476
3504
  } = useSelection3.getState();
3477
- const selectedNodeIdsRef = useRef3(
3505
+ const selectedNodeIdsRef = useRef4(
3478
3506
  useSelection3.getState().selectedNodeIds
3479
3507
  );
3480
- const selectedEdgeRef = useRef3(
3508
+ const selectedEdgeRef = useRef4(
3481
3509
  useSelection3.getState().selectedEdge
3482
3510
  );
3483
3511
  useEffect4(() => {
3484
- const state = useSelection3.getState();
3485
- selectedNodeIdsRef.current = state.selectedNodeIds;
3486
- selectedEdgeRef.current = state.selectedEdge;
3487
- const cy = cyRef.current;
3488
- 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;
3489
3526
  cy.batch(() => {
3490
- cy.elements().unselect();
3491
- state.selectedNodeIds.forEach((id) => cy.$id(id).select());
3492
- if (state.selectedEdge) {
3493
- const edge = cy.$id(String(state.selectedEdge.id_));
3494
- if (edge) {
3495
- edge.select();
3496
- cy.$id(edge.data("source")).select();
3497
- 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();
3498
3540
  }
3499
3541
  }
3500
3542
  });
3501
- }
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;
3502
3550
  }, [useSelection3]);
3503
3551
  useEffect4(() => {
3504
3552
  nodesRef.current = nodes;
@@ -3512,8 +3560,10 @@ function CytoscapeGraph({
3512
3560
  cyRef.current = cytoscape({
3513
3561
  container: containerRef.current,
3514
3562
  elements: [],
3515
- selectionType: "additive",
3516
- // 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",
3517
3567
  style: [
3518
3568
  // Base node style with data mappers for dynamic properties
3519
3569
  {
@@ -3607,19 +3657,20 @@ function CytoscapeGraph({
3607
3657
  {
3608
3658
  selector: "edge:selected",
3609
3659
  style: {
3610
- "line-color": "#ff1a1a",
3611
- "target-arrow-color": "#ff1a1a",
3612
- width: 8,
3613
- // 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,
3614
3665
  "z-index": 999,
3615
3666
  "font-size": "11px",
3616
3667
  "font-weight": "bold",
3617
3668
  "text-background-opacity": 1,
3618
3669
  "text-background-color": "#ffffff",
3619
3670
  "text-background-padding": "3px",
3620
- "arrow-scale": 1.35,
3671
+ "arrow-scale": 0.9,
3621
3672
  opacity: 1,
3622
- "overlay-color": "#ff1a1a",
3673
+ "overlay-color": SELECTION_COLOR,
3623
3674
  "overlay-opacity": 0.08
3624
3675
  }
3625
3676
  },
@@ -3628,13 +3679,13 @@ function CytoscapeGraph({
3628
3679
  style: {
3629
3680
  "border-width": 10,
3630
3681
  // thicker circle outline
3631
- "border-color": "#ff1a1a",
3682
+ "border-color": SELECTION_COLOR,
3632
3683
  "border-opacity": 1,
3633
3684
  "background-opacity": 1,
3634
3685
  "text-background-opacity": 1,
3635
3686
  "text-background-color": "#ffffff",
3636
3687
  "text-background-padding": "4px",
3637
- "overlay-color": "#ff1a1a",
3688
+ "overlay-color": SELECTION_COLOR,
3638
3689
  "overlay-opacity": 0.18,
3639
3690
  width: 48,
3640
3691
  // slight size bump from 40
@@ -3666,9 +3717,9 @@ function CytoscapeGraph({
3666
3717
  cyRef.current = null;
3667
3718
  };
3668
3719
  }, []);
3669
- const elementIdsRef = useRef3("");
3670
- const lastLayoutRef = useRef3("");
3671
- const lastFitPaddingRef = useRef3(fitPadding);
3720
+ const elementIdsRef = useRef4("");
3721
+ const lastLayoutRef = useRef4("");
3722
+ const lastFitPaddingRef = useRef4(fitPadding);
3672
3723
  useEffect4(() => {
3673
3724
  const cy = cyRef.current;
3674
3725
  if (!cy || !isInitializedRef.current) return;
@@ -3766,7 +3817,7 @@ function CytoscapeGraph({
3766
3817
  const updateLayout = (newLayout) => {
3767
3818
  setCurrentLayout(newLayout);
3768
3819
  };
3769
- const fitToScreen = useCallback4(() => {
3820
+ const fitToScreen = useCallback5(() => {
3770
3821
  if (cyRef.current) {
3771
3822
  cyRef.current.fit(void 0, fitPadding);
3772
3823
  }
@@ -3823,9 +3874,9 @@ function CytoscapeGraph({
3823
3874
  extraToolbarContent,
3824
3875
  zoomIn: () => {
3825
3876
  if (cyRef.current) {
3826
- const z = cyRef.current.zoom();
3877
+ const z3 = cyRef.current.zoom();
3827
3878
  cyRef.current.zoom({
3828
- level: Math.min(z * 1.2, 5),
3879
+ level: Math.min(z3 * 1.2, 5),
3829
3880
  renderedPosition: {
3830
3881
  x: cyRef.current.width() / 2,
3831
3882
  y: cyRef.current.height() / 2
@@ -3835,9 +3886,9 @@ function CytoscapeGraph({
3835
3886
  },
3836
3887
  zoomOut: () => {
3837
3888
  if (cyRef.current) {
3838
- const z = cyRef.current.zoom();
3889
+ const z3 = cyRef.current.zoom();
3839
3890
  cyRef.current.zoom({
3840
- level: Math.max(z / 1.2, 0.1),
3891
+ level: Math.max(z3 / 1.2, 0.1),
3841
3892
  renderedPosition: {
3842
3893
  x: cyRef.current.width() / 2,
3843
3894
  y: cyRef.current.height() / 2
@@ -4035,18 +4086,11 @@ function NodeLabelBadges({
4035
4086
  labels,
4036
4087
  labelCounts,
4037
4088
  colorPalette,
4038
- allLabelsDisabled,
4039
- nodeLabelFilters,
4040
- toggleNodeLabelFilter
4089
+ shown,
4090
+ onToggleLabel
4041
4091
  }) {
4042
4092
  return labels.map((label) => {
4043
- const bgColor = resolveBadgeColor(
4044
- allLabelsDisabled,
4045
- nodeLabelFilters,
4046
- label,
4047
- colorPalette,
4048
- label
4049
- );
4093
+ const bgColor = shown.has(label) ? colorPalette[label] : colorPalette["faded"];
4050
4094
  return /* @__PURE__ */ jsxs12(
4051
4095
  Badge,
4052
4096
  {
@@ -4056,7 +4100,7 @@ function NodeLabelBadges({
4056
4100
  backgroundColor: bgColor,
4057
4101
  borderRadius: "10px"
4058
4102
  },
4059
- onClick: () => toggleNodeLabelFilter(label, labels),
4103
+ onClick: () => onToggleLabel(label),
4060
4104
  children: [
4061
4105
  label,
4062
4106
  " (",
@@ -4106,12 +4150,53 @@ function EdgeLabelBadges({
4106
4150
  );
4107
4151
  });
4108
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
+ }
4109
4192
  function GraphOverview({
4110
4193
  stats,
4111
4194
  colorPalette,
4112
4195
  labelCounts,
4113
4196
  edgeLabelCounts,
4114
- edgeToNodeLabels
4197
+ edgeToNodeLabels,
4198
+ nodeBuckets,
4199
+ edgeSectionTitle = "Relations"
4115
4200
  }) {
4116
4201
  const useGraphFilter2 = useWorkspaceGraphFilter();
4117
4202
  const nodeLabelFilters = useGraphFilter2((state) => state.nodeLabelFilters);
@@ -4120,24 +4205,15 @@ function GraphOverview({
4120
4205
  const allEdgeLabelsDisabled = useGraphFilter2(
4121
4206
  (state) => state.allEdgeLabelsDisabled
4122
4207
  );
4123
- const toggleNodeLabelFilter = useGraphFilter2(
4124
- (state) => state.toggleNodeLabelFilter
4125
- );
4126
- const clearNodeLabelFilters = useGraphFilter2(
4127
- (state) => state.clearNodeLabelFilters
4128
- );
4129
- const toggleAllLabelsDisabled = useGraphFilter2(
4130
- (state) => state.toggleAllLabelsDisabled
4131
- );
4132
4208
  const toggleEdgeLabelFilter = useGraphFilter2(
4133
4209
  (state) => state.toggleEdgeLabelFilter
4134
4210
  );
4135
- const clearEdgeLabelFilters = useGraphFilter2(
4136
- (state) => state.clearEdgeLabelFilters
4137
- );
4138
4211
  const toggleAllEdgeLabelsDisabled = useGraphFilter2(
4139
4212
  (state) => state.toggleAllEdgeLabelsDisabled
4140
4213
  );
4214
+ const setNodeFilterState = useGraphFilter2(
4215
+ (state) => state.setNodeFilterState
4216
+ );
4141
4217
  const allLabels = Object.keys(labelCounts).sort();
4142
4218
  devLog11("[Overview] Rendering labels", {
4143
4219
  labels: allLabels,
@@ -4146,52 +4222,63 @@ function GraphOverview({
4146
4222
  stats
4147
4223
  });
4148
4224
  const allEdgeLabels = Object.keys(edgeLabelCounts || {}).sort();
4149
- const handleNodeToggle = () => {
4150
- if (allLabelsDisabled) {
4151
- toggleAllLabelsDisabled();
4152
- clearNodeLabelFilters();
4153
- } else {
4154
- toggleAllLabelsDisabled();
4155
- }
4156
- };
4157
- const handleEdgeToggle = () => {
4158
- if (allEdgeLabelsDisabled) {
4159
- toggleAllEdgeLabelsDisabled();
4160
- clearEdgeLabelFilters();
4161
- } else {
4162
- toggleAllEdgeLabelsDisabled();
4163
- }
4164
- };
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;
4165
4235
  return /* @__PURE__ */ jsxs12(Fragment3, { children: [
4166
- /* @__PURE__ */ jsx13("p", { className: "mt-2 text-sm font-semibold", children: "Overview" }),
4167
- allLabels.length > 0 && /* @__PURE__ */ jsxs12("div", { children: [
4168
- /* @__PURE__ */ jsx13("p", { className: "text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2", children: "Node Labels" }),
4169
- /* @__PURE__ */ jsxs12("div", { className: "flex flex-col items-start gap-2", children: [
4170
- /* @__PURE__ */ jsx13(
4171
- AllToggleBadge,
4172
- {
4173
- allDisabled: allLabelsDisabled,
4174
- colorPalette,
4175
- count: stats.nodes,
4176
- onToggle: handleNodeToggle,
4177
- badgeKey: "all"
4178
- }
4179
- ),
4180
- /* @__PURE__ */ jsx13(
4181
- NodeLabelBadges,
4182
- {
4183
- labels: allLabels,
4184
- labelCounts,
4185
- colorPalette,
4186
- allLabelsDisabled,
4187
- nodeLabelFilters,
4188
- toggleNodeLabelFilter
4189
- }
4190
- )
4191
- ] })
4192
- ] }),
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
+ }) }),
4193
4280
  allEdgeLabels.length > 0 && /* @__PURE__ */ jsxs12("div", { children: [
4194
- /* @__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 }),
4195
4282
  /* @__PURE__ */ jsxs12("div", { className: "flex flex-col items-start gap-2", children: [
4196
4283
  /* @__PURE__ */ jsx13(
4197
4284
  AllToggleBadge,
@@ -4219,7 +4306,7 @@ function GraphOverview({
4219
4306
  ] })
4220
4307
  ] });
4221
4308
  }
4222
- function Overview() {
4309
+ function Overview({ nodeBuckets, edgeSectionTitle } = {}) {
4223
4310
  const { stats, colorPalette, labelCounts, edgeLabelCounts, baseNormalized } = useLiveGraphData();
4224
4311
  const edgeToNodeLabels = useMemo4(() => {
4225
4312
  if (!baseNormalized?.nodesById || !baseNormalized?.edgesById) return {};
@@ -4230,7 +4317,7 @@ function Overview() {
4230
4317
  }, [baseNormalized]);
4231
4318
  if (stats && stats.values != null && stats.values > 0) {
4232
4319
  return /* @__PURE__ */ jsxs12(Fragment3, { children: [
4233
- /* @__PURE__ */ jsx13("p", { className: "mt-2 text-sm font-semibold", children: "Overview" }),
4320
+ /* @__PURE__ */ jsx13("p", { className: "mt-2 heading-component", children: "Overview" }),
4234
4321
  /* @__PURE__ */ jsxs12("p", { className: "text-sm text-muted-foreground", children: [
4235
4322
  formatNumber(stats.values),
4236
4323
  " Values"
@@ -4245,7 +4332,9 @@ function Overview() {
4245
4332
  colorPalette,
4246
4333
  labelCounts,
4247
4334
  edgeLabelCounts,
4248
- edgeToNodeLabels
4335
+ edgeToNodeLabels,
4336
+ nodeBuckets,
4337
+ edgeSectionTitle
4249
4338
  }
4250
4339
  );
4251
4340
  }
@@ -4291,11 +4380,12 @@ function ActionPanel({ actions }) {
4291
4380
  var ActionPanel_default = ActionPanel;
4292
4381
 
4293
4382
  // src/components/GraphA11yList/GraphA11yList.tsx
4294
- import { useMemo as useMemo6, useRef as useRef4 } from "react";
4383
+ import { useCallback as useCallback6, useMemo as useMemo6, useRef as useRef5 } from "react";
4295
4384
  import { useVirtualizer } from "@tanstack/react-virtual";
4296
4385
  import { cn as cn2 } from "@petrarca/sonnet-core";
4297
4386
  import { jsx as jsx15 } from "react/jsx-runtime";
4298
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;
4299
4389
  function GraphA11yList({
4300
4390
  visible = false,
4301
4391
  rowHeight = 28,
@@ -4329,28 +4419,46 @@ function GraphA11yList({
4329
4419
  }));
4330
4420
  return [...nodeRows, ...edgeRows];
4331
4421
  }, [live.nodes, live.edges]);
4332
- const parentRef = useRef4(null);
4422
+ const parentRef = useRef5(null);
4333
4423
  const virtualizer = useVirtualizer({
4334
4424
  count: rows.length,
4335
4425
  getScrollElement: () => parentRef.current,
4336
4426
  estimateSize: () => rowHeight,
4337
4427
  overscan: 8
4338
4428
  });
4339
- const selectRow = (row) => {
4340
- if (row.kind === "node") {
4429
+ const activateNode = useCallback6(
4430
+ (id, additive) => {
4341
4431
  setSelectedEdge(null);
4342
- setSelectedNodeIds([row.id]);
4343
- } else {
4344
- setSelectedNodeIds([]);
4345
- setSelectedEdge(row.edge);
4346
- }
4347
- };
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
+ );
4348
4455
  const isSelected = (row) => row.kind === "node" ? selectedNodeIds.includes(row.id) : selectedEdge?.id_ === row.edge.id_;
4349
4456
  return /* @__PURE__ */ jsx15(
4350
4457
  "div",
4351
4458
  {
4352
4459
  role: "listbox",
4353
4460
  "aria-label": "Graph elements",
4461
+ "aria-multiselectable": true,
4354
4462
  className: cn2(visible ? "rounded-md border" : SR_ONLY, className),
4355
4463
  children: /* @__PURE__ */ jsx15(
4356
4464
  "div",
@@ -4378,7 +4486,7 @@ function GraphA11yList({
4378
4486
  "aria-label": `${row.kind === "node" ? "Node" : "Edge"} ${row.label}`,
4379
4487
  "data-testid": `graph-${row.kind}-${row.id}`,
4380
4488
  ...row.kind === "node" ? { "data-graph-node-id": row.id } : { "data-graph-edge-id": row.id },
4381
- onClick: () => selectRow(row),
4489
+ onClick: (e) => selectRow(row, e),
4382
4490
  className: cn2(
4383
4491
  "absolute left-0 top-0 flex w-full items-center px-2 text-left text-sm",
4384
4492
  visible && "hover:bg-accent aria-selected:bg-state-selected aria-selected:text-foreground"
@@ -4399,46 +4507,2977 @@ function GraphA11yList({
4399
4507
  }
4400
4508
  );
4401
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
+ }
4402
7386
  export {
4403
7387
  ActionPanel_default as ActionPanel,
7388
+ DeleteNodeDialog,
4404
7389
  EMPTY_NORMALIZED,
7390
+ EdgeFormSheet,
4405
7391
  EdgeInfoCard_default as EdgeInfoCard,
4406
7392
  GraphA11yList,
7393
+ GraphEditToolbar,
7394
+ GraphModelEditor,
4407
7395
  GraphRendererProvider,
4408
7396
  GraphStoresContext,
4409
7397
  GraphVisualizer_default as GraphVisualizer,
7398
+ ModelActionPanel,
7399
+ NodeFormSheet,
4410
7400
  NodeInfoCard_default as NodeInfoCard,
7401
+ NodeTypeNameSheet,
4411
7402
  Overview_default as Overview,
7403
+ PropertyFormSheet,
7404
+ RelationFormSheet,
7405
+ TypeDetailPanel,
7406
+ addEdgeType,
7407
+ addNodeType,
7408
+ addRelation,
4412
7409
  buildColorPalette,
4413
7410
  buildColorPaletteFromNodes,
7411
+ capabilitiesSchema,
7412
+ cardinalityLabel,
4414
7413
  createActionPanelStore,
4415
7414
  createGraphDataStore,
4416
7415
  createGraphExplorationStore,
4417
7416
  createGraphFilterStore,
4418
7417
  createGraphFocusStore,
4419
7418
  createSelectionStore,
7419
+ deleteEdgeType,
7420
+ deleteEdgeTypeProperty,
7421
+ deleteNodeProperty,
7422
+ deleteNodeType,
7423
+ deleteRelation,
7424
+ deleteRelationProperty,
7425
+ edgeTypeView,
7426
+ editableRelations,
4420
7427
  enrichEdge,
4421
7428
  enrichEdges,
4422
7429
  enrichNode,
4423
7430
  enrichNodes,
7431
+ enrichSchemaWithRelations,
4424
7432
  extractDisplayLabel,
7433
+ extractRelationFields,
7434
+ findEdgeType,
7435
+ findType,
7436
+ graphCapabilitiesSchema,
7437
+ groupEdgesByRelation,
7438
+ hasProperties,
7439
+ hasRelationFields,
7440
+ initializeFormData,
7441
+ isEdgeType,
7442
+ isNodeType,
4425
7443
  isRawGraphNode,
7444
+ isRelationField,
4426
7445
  isVisualEdge,
4427
7446
  isVisualNode,
7447
+ modelToGraph,
7448
+ nodeTypeView,
4428
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,
4429
7463
  toCytoscapeEdge,
4430
7464
  toCytoscapeNode,
4431
7465
  toReagraphEdge,
4432
7466
  toReagraphNode,
7467
+ typeLabel,
7468
+ updateRelation,
4433
7469
  useActionPanelStore,
4434
7470
  useCytoscapeData,
4435
7471
  GraphFocus_default as useGraphFocus,
4436
7472
  useGraphRenderer,
4437
7473
  useLiveGraphData,
4438
7474
  useReagraphData,
7475
+ useStableRef,
4439
7476
  useWorkspaceGraphData,
4440
7477
  useWorkspaceGraphExploration,
4441
7478
  useWorkspaceGraphFilter,
4442
- useWorkspaceSelection
7479
+ useWorkspaceSelection,
7480
+ validateCapabilities,
7481
+ validateGraphCapabilities
4443
7482
  };
4444
7483
  //# sourceMappingURL=index.js.map