@principal-ai/subsystems-react 0.37.12 → 0.39.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.
Files changed (74) hide show
  1. package/dist/graphify/consolidated.d.ts +8 -0
  2. package/dist/graphify/consolidated.d.ts.map +1 -1
  3. package/dist/graphify/construct.d.ts +18 -0
  4. package/dist/graphify/construct.d.ts.map +1 -1
  5. package/dist/graphify/construct.js +23 -0
  6. package/dist/graphify/construct.js.map +1 -1
  7. package/dist/graphify/index.d.ts +2 -2
  8. package/dist/graphify/index.d.ts.map +1 -1
  9. package/dist/graphify/index.js +1 -1
  10. package/dist/graphify/index.js.map +1 -1
  11. package/dist/index.d.ts +3 -3
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +2 -2
  14. package/dist/index.js.map +1 -1
  15. package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts.map +1 -1
  16. package/dist/stories/Subsystem/ComponentGraph/fixtures.js +0 -1
  17. package/dist/stories/Subsystem/ComponentGraph/fixtures.js.map +1 -1
  18. package/dist/subsystem/IssueList.d.ts +49 -3
  19. package/dist/subsystem/IssueList.d.ts.map +1 -1
  20. package/dist/subsystem/IssueList.js +180 -79
  21. package/dist/subsystem/IssueList.js.map +1 -1
  22. package/dist/subsystem/SubsystemComponentGraph.d.ts +23 -2
  23. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  24. package/dist/subsystem/SubsystemComponentGraph.js +492 -23
  25. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  26. package/dist/subsystem/formatDeclaration.js +19 -5
  27. package/dist/subsystem/formatDeclaration.js.map +1 -1
  28. package/dist/subsystem/model.d.ts +161 -7
  29. package/dist/subsystem/model.d.ts.map +1 -1
  30. package/dist/subsystem/model.js +91 -12
  31. package/dist/subsystem/model.js.map +1 -1
  32. package/dist/subsystem/nodes.d.ts.map +1 -1
  33. package/dist/subsystem/nodes.js +133 -72
  34. package/dist/subsystem/nodes.js.map +1 -1
  35. package/dist/subsystem/symbolRefs.d.ts.map +1 -1
  36. package/dist/subsystem/symbolRefs.js +2 -0
  37. package/dist/subsystem/symbolRefs.js.map +1 -1
  38. package/dist/utils/elkLayout.d.ts +9 -0
  39. package/dist/utils/elkLayout.d.ts.map +1 -1
  40. package/dist/utils/elkLayout.js +21 -2
  41. package/dist/utils/elkLayout.js.map +1 -1
  42. package/package.json +3 -3
  43. package/src/graphify/__fixtures__/ego-samples.json +545 -0
  44. package/src/graphify/consolidated.ts +8 -0
  45. package/src/graphify/construct.test.ts +61 -1
  46. package/src/graphify/construct.ts +33 -0
  47. package/src/graphify/index.ts +6 -1
  48. package/src/index.ts +13 -1
  49. package/src/stories/Subsystem/ComponentGraph/Basics.stories.tsx +2 -2
  50. package/src/stories/Subsystem/ComponentGraph/Captures.stories.tsx +9 -9
  51. package/src/stories/Subsystem/ComponentGraph/EdgeViews.stories.tsx +4 -4
  52. package/src/stories/Subsystem/ComponentGraph/IssueOverlay.stories.tsx +625 -0
  53. package/src/stories/Subsystem/ComponentGraph/Issues.stories.tsx +7 -1
  54. package/src/stories/Subsystem/ComponentGraph/ModuleBadges.stories.tsx +5 -5
  55. package/src/stories/Subsystem/ComponentGraph/Modules.stories.tsx +5 -5
  56. package/src/stories/Subsystem/ComponentGraph/NodeAnatomy.stories.tsx +40 -73
  57. package/src/stories/Subsystem/ComponentGraph/Scenarios.stories.tsx +6 -6
  58. package/src/stories/Subsystem/ComponentGraph/StoreValueType.stories.tsx +261 -0
  59. package/src/stories/Subsystem/ComponentGraph/fixtures.ts +0 -1
  60. package/src/stories/Subsystem/EgoGraph/EgoGraph.stories.tsx +354 -0
  61. package/src/stories/Subsystem/StoreValueTypeDeclaration.stories.tsx +182 -0
  62. package/src/subsystem/IssueList.focus.test.tsx +113 -0
  63. package/src/subsystem/IssueList.icon.test.tsx +95 -0
  64. package/src/subsystem/IssueList.tsx +267 -111
  65. package/src/subsystem/SubsystemComponentGraph.tsx +582 -27
  66. package/src/subsystem/formatDeclaration.test.ts +64 -0
  67. package/src/subsystem/formatDeclaration.ts +20 -5
  68. package/src/subsystem/model.test.ts +75 -2
  69. package/src/subsystem/model.ts +247 -17
  70. package/src/subsystem/nodes.group.test.tsx +52 -0
  71. package/src/subsystem/nodes.tsx +112 -8
  72. package/src/subsystem/symbolRefs.ts +2 -0
  73. package/src/subsystem/toC4.test.ts +3 -3
  74. package/src/utils/elkLayout.ts +32 -1
@@ -20,11 +20,11 @@ import { ReactFlow, ReactFlowProvider, useReactFlow, useViewport, applyNodeChang
20
20
  import { useTheme } from '@principal-ade/industry-theme';
21
21
  import { FileText, X } from 'lucide-react';
22
22
  import { IndustryMarkdownSlide } from 'themed-markdown';
23
- import { buildSubsystemGraph, deriveGraphEdges, isConstructsOnlyModel, isRelationMechanism, isWalkthroughMechanism, MECHANISM_COLOR, MECHANISM_DESCRIPTIONS, subsystemGraphLayoutKey, walkthroughStepGraphEdgeId, } from './model';
23
+ import { buildSubsystemGraph, deriveGraphEdges, isConstructsOnlyModel, moduleGroupNodeId, isRelationMechanism, isWalkthroughMechanism, edgeColor, MECHANISM_DESCRIPTIONS, subsystemGraphLayoutKey, walkthroughStepGraphEdgeId, } from './model';
24
24
  import { ConstructsCatalog } from './ConstructsCatalog';
25
25
  import { SubsystemComponentNode, SubsystemGroupNode, SubsystemEdge, SUBSYSTEM_CALLBACKS, hexWithAlpha, EDGE_DIM_ALPHA, fileMatchForNode, flowElementVisibility } from './nodes';
26
26
  import { SubsystemDiagnosticToggle } from './DiagnosticToggle';
27
- import { SubsystemIssueList, } from './IssueList';
27
+ import { SubsystemIssueList, issueCategory, issueKindOrder, issueRung, ISSUE_KIND_ICON, ISSUE_RUNG_ORDER, } from './IssueList';
28
28
  import { SubsystemFileTree } from './SubsystemFileTree';
29
29
  import { GraphLayoutCover } from './GraphLayoutCover';
30
30
  import { GRAPH_CANVAS_CLASS, GRAPH_NAV_PROPS, GraphChrome, GraphLayerStyle } from './graphChrome';
@@ -120,7 +120,7 @@ const WalkthroughDrawerContent = memo(function WalkthroughDrawerContent({ render
120
120
  onSymbolClick,
121
121
  }) }));
122
122
  });
123
- function Inner({ components, relations, walkthroughs, initialWalkthroughId, onReorderWalkthroughs, onSelect, onEdgeSelect, measured: _measured, maxNodeWidth, showEdgeLabels, showSingletonFrames = true, edgeView, title, hideSidebar, walkthroughStepMode = 'focus', autoPlayWalkthroughs = false, walkthroughAutoPlayIntervalMs = WALKTHROUGH_PLAY_PAUSE_MS, zoomOnWalkthroughFocus = true, walkthroughFocusDurationMs = 300, graphTitle, showWalkthroughTitle = false, description, canvasOverlay, sidebarExtra, sidebarAfterDescription, diagnostic, issues, showIssues, focusIssueCategory, onSelectIssue, onApplyIssueFix, onHoverIssue, renderFileView, renderFileViewer, renderWalkthroughViewer, onFileSelect, componentVerification, onInspectSymbol, boundaryColors, hideDrawer = false, persistKey }) {
123
+ function Inner({ components, relations, walkthroughs, graphifyRelations, orderByLine, initialWalkthroughId, onReorderWalkthroughs, onSelect, onEdgeSelect, measured: _measured, maxNodeWidth, showEdgeLabels, showSingletonFrames = true, edgeView, title, hideSidebar, walkthroughStepMode = 'focus', autoPlayWalkthroughs = false, walkthroughAutoPlayIntervalMs = WALKTHROUGH_PLAY_PAUSE_MS, zoomOnWalkthroughFocus = true, walkthroughFocusDurationMs = 300, graphTitle, showWalkthroughTitle = false, description, canvasOverlay, sidebarExtra, sidebarAfterDescription, diagnostic, issues, showIssues, focusIssueCategory, onSelectIssue, onApplyIssueFix, onHoverIssue, renderFileView, renderFileViewer, renderWalkthroughViewer, onFileSelect, componentVerification, onInspectSymbol, boundaryColors, hideDrawer = false, persistKey }) {
124
124
  const { theme } = useTheme();
125
125
  const { fitView, fitBounds } = useReactFlow();
126
126
  const viewport = useViewport();
@@ -161,13 +161,19 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
161
161
  const [hoveredWalkthroughStep, setHoveredWalkthroughStep] = useState(null);
162
162
  // Sidebar bottom half: which panel is shown when walkthroughs exist.
163
163
  const [sidebarView, setSidebarView] = useState(() => walkthroughs?.length ? 'walkthroughs' : 'files');
164
+ // Temporary edge-vocabulary override. Focusing a relation finding whose edge
165
+ // belongs to the *other* vocabulary flips the canvas so the edge is actually
166
+ // visible; it reverts when the focus is undone (see `unfocusIssueTarget`) or
167
+ // when the diagnostics list closes. Null = use the host-pinned / tab view.
168
+ const [edgeViewOverride, setEdgeViewOverride] = useState(null);
164
169
  // One edge vocabulary at a time. When the caller doesn't pick, the sidebar's
165
170
  // Files / Walkthroughs tab picks: Files draws topology relation edges,
166
171
  // Walkthroughs draws runtime hop edges. Without visible tabs (no walkthroughs,
167
172
  // or a sidebar-less embed) fall back to the model: a flow-only graph defaults
168
173
  // to walkthrough edges rather than empty, everything else to relations.
169
174
  const sidebarTabsVisible = !hideSidebar && (walkthroughs?.length ?? 0) > 0;
170
- const resolvedEdgeView = edgeView ??
175
+ const resolvedEdgeView = edgeViewOverride ??
176
+ edgeView ??
171
177
  (sidebarTabsVisible
172
178
  ? (sidebarView === 'walkthroughs' ? 'walkthroughs' : 'relations')
173
179
  : relations.length === 0 && (walkthroughs?.length ?? 0) > 0
@@ -249,6 +255,11 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
249
255
  // closure over the effect deps) can toggle without a stale value.
250
256
  const selectedRef = useRef(null);
251
257
  selectedRef.current = selected;
258
+ // The module frame an issue card framed. A module target selects no node and
259
+ // no edge, so nothing else records that focus — without this the canvas has
260
+ // no way to tell a module focus it still owns from one a later click took
261
+ // over, and the camera would stay parked on the region forever.
262
+ const issueModuleFocusRef = useRef(null);
252
263
  // Ref mirror of the open file drawer target for tree-click toggle.
253
264
  const openFileRef = useRef(null);
254
265
  const openFile = fileOverlay?.file ??
@@ -356,13 +367,15 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
356
367
  // Pass 1: build with estimated widths so React Flow can measure the DOM.
357
368
  // The pane stays hidden until Pass 2 completes. Key off layout-affecting
358
369
  // fields only — declarationRef updates after verify must not re-run ELK.
359
- const layoutKey = useMemo(() => subsystemGraphLayoutKey({ components, relations, walkthroughs }), [components, relations, walkthroughs]);
370
+ const layoutKey = useMemo(() => subsystemGraphLayoutKey({ components, relations, walkthroughs, graphifyRelations }), [components, relations, walkthroughs, graphifyRelations]);
360
371
  const componentsRef = useRef(components);
361
372
  const relationsRef = useRef(relations);
362
373
  const walkthroughsRef = useRef(walkthroughs);
374
+ const graphifyRelationsRef = useRef(graphifyRelations);
363
375
  componentsRef.current = components;
364
376
  relationsRef.current = relations;
365
377
  walkthroughsRef.current = walkthroughs;
378
+ graphifyRelationsRef.current = graphifyRelations;
366
379
  // Track measured dimensions from React Flow's dimension changes.
367
380
  // These arrive as { type: 'dimensions', id, dimensions } in onNodesChange.
368
381
  // Retained across same-id layoutKey updates so Pass 2 can run without a remount.
@@ -386,6 +399,8 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
386
399
  maxNodeWidth,
387
400
  showEdgeLabels,
388
401
  showSingletonFrames,
402
+ graphifyRelations: graphifyRelationsRef.current,
403
+ orderByLine,
389
404
  })
390
405
  .then(({ nodes, edges: e, absoluteRects }) => {
391
406
  if (!alive)
@@ -436,7 +451,7 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
436
451
  const measuredWidths = new Map(leafNodes.map((n) => [n.id, dims.get(n.id).width]));
437
452
  const measuredHeights = new Map(leafNodes.map((n) => [n.id, dims.get(n.id).height]));
438
453
  const gen = ++pass2GenRef.current;
439
- void buildSubsystemGraph({ components, relations, walkthroughs }, { maxNodeWidth, showEdgeLabels, measuredWidths, measuredHeights, showSingletonFrames })
454
+ void buildSubsystemGraph({ components, relations, walkthroughs }, { maxNodeWidth, showEdgeLabels, measuredWidths, measuredHeights, showSingletonFrames, graphifyRelations, orderByLine })
440
455
  .then(({ nodes, edges: e, absoluteRects }) => {
441
456
  if (gen !== pass2GenRef.current)
442
457
  return;
@@ -450,7 +465,7 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
450
465
  // Reveal Pass 1 layout rather than leaving the cover up forever.
451
466
  setLayoutReady(true);
452
467
  });
453
- }, [built.nodes, components, relations, walkthroughs, maxNodeWidth, showEdgeLabels]);
468
+ }, [built.nodes, components, relations, walkthroughs, graphifyRelations, orderByLine, maxNodeWidth, showEdgeLabels]);
454
469
  // After Pass 1 commits, try Pass 2 immediately with retained measurements.
455
470
  // Same-id live updates often get no new React Flow `dimensions` events, so
456
471
  // waiting only on onNodesChange leaves the cover stuck.
@@ -528,12 +543,239 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
528
543
  const { nodes, edges: convertedEdges } = built;
529
544
  const xyflowNodesBase = nodes;
530
545
  const baseEdges = convertedEdges;
546
+ // Resolve an issue's component target to the matching node. `id` is the
547
+ // stable alias; `label` may be an alias, name, or symbol. Non-component
548
+ // targets (relation / module / flow / repo) have no single node → null.
549
+ const issueComponent = useCallback((issue) => {
550
+ const target = issue.target;
551
+ if (target?.kind !== 'component')
552
+ return null;
553
+ return (components.find((c) => (target.id != null && c.alias === target.id) ||
554
+ c.alias === target.label ||
555
+ c.name === target.label ||
556
+ c.symbol === target.label) ?? null);
557
+ }, [components]);
558
+ // Resolve a `relation` issue target to the display edge it flags. The target
559
+ // names the two endpoints (`from → to`); match an edge joining both, in
560
+ // either direction, preferring the target's `detail` mechanism when it names
561
+ // one. Null when no display edge matches.
562
+ const issueEdge = useCallback((issue) => {
563
+ const target = issue.target;
564
+ if (target?.kind !== 'relation')
565
+ return null;
566
+ const parts = `${target.id ?? ''} → ${target.label}`
567
+ .split('→')
568
+ .map((p) => p.replace(/\(\)$/, '').trim())
569
+ .filter(Boolean);
570
+ const joins = (e) => parts.includes(e.source) && parts.includes(e.target);
571
+ const matches = baseEdges.filter(joins);
572
+ const byMechanism = target.detail
573
+ ? matches.filter((e) => e.data?.mechanism ===
574
+ target.detail)
575
+ : matches;
576
+ return byMechanism[0] ?? matches[0] ?? null;
577
+ }, [baseEdges]);
578
+ // A `module` issue target names a boundary frame, so focusing the card frames
579
+ // that region on the canvas. Null when the target names no frame (a singleton
580
+ // dropped from the layout, or a region key nothing declares).
581
+ const issueModuleNodeId = useCallback((issue) => {
582
+ const target = issue.target;
583
+ if (target?.kind !== 'module')
584
+ return null;
585
+ const key = target.id ?? target.label;
586
+ if (!components.some((c) => c.module?.trim() === key))
587
+ return null;
588
+ return moduleGroupNodeId(key);
589
+ }, [components]);
590
+ // A `step` issue target names ONE step of a walkthrough — the walkthrough id in
591
+ // `id`, the 0-based step position in `stepIndex`. Validated against the loaded
592
+ // walkthroughs so a finding left over from an edited flow resolves to null
593
+ // rather than framing whatever now happens to sit at that index.
594
+ const issueStep = useCallback((issue) => {
595
+ const target = issue.target;
596
+ if (target?.kind !== 'step')
597
+ return null;
598
+ if (target.id == null || target.stepIndex == null)
599
+ return null;
600
+ const walkthrough = walkthroughs?.find((t) => t.id === target.id);
601
+ if (!walkthrough?.steps[target.stepIndex])
602
+ return null;
603
+ return { walkthrough, stepIndex: target.stepIndex };
604
+ }, [walkthroughs]);
605
+ // Per-node diagnostics badge: fold each component-targeted finding that maps
606
+ // to a verification rung into one badge per node — worst severity wins, the
607
+ // chip shows the earliest failing rung, and the count tallies the findings.
608
+ const issueBadgeByAlias = useMemo(() => {
609
+ const map = new Map();
610
+ if (!issues?.length)
611
+ return map;
612
+ for (const issue of issues) {
613
+ const rung = issueRung(issue.kind);
614
+ if (!rung)
615
+ continue;
616
+ const comp = issueComponent(issue);
617
+ if (!comp)
618
+ continue;
619
+ const prev = map.get(comp.alias);
620
+ map.set(comp.alias, {
621
+ severity: prev?.severity === 'error' || issue.severity === 'error'
622
+ ? 'error'
623
+ : 'info',
624
+ rung: prev != null && ISSUE_RUNG_ORDER[prev.rung] <= ISSUE_RUNG_ORDER[rung]
625
+ ? prev.rung
626
+ : rung,
627
+ count: (prev?.count ?? 0) + 1,
628
+ });
629
+ }
630
+ return map;
631
+ }, [issues, issueComponent]);
632
+ // Per-frame diagnostics badge: the same fold, but for findings about a
633
+ // BOUNDARY rather than a construct. Keyed by the region's React Flow node id.
634
+ // Only kinds with a dedicated frame icon qualify (`ISSUE_KIND_ICON`) — a
635
+ // finding with no icon of its own has nothing to badge the frame with, and
636
+ // borrowing its layer's icon would imply the frame is at fault for it.
637
+ const regionIssueByNodeId = useMemo(() => {
638
+ const map = new Map();
639
+ if (!issues?.length)
640
+ return map;
641
+ for (const issue of issues) {
642
+ const target = issue.target;
643
+ if (target?.kind !== 'module' || !ISSUE_KIND_ICON[issue.kind])
644
+ continue;
645
+ const id = moduleGroupNodeId(target.id ?? target.label);
646
+ const prev = map.get(id);
647
+ map.set(id, {
648
+ severity: prev?.severity === 'error' || issue.severity === 'error' ? 'error' : 'info',
649
+ kind: prev != null && issueKindOrder(prev.kind) <= issueKindOrder(issue.kind)
650
+ ? prev.kind
651
+ : issue.kind,
652
+ count: (prev?.count ?? 0) + 1,
653
+ });
654
+ }
655
+ return map;
656
+ }, [issues]);
657
+ // Expanded verification layers in the diagnostics list. When any layer with
658
+ // findings is expanded, the canvas dims everything that layer does not
659
+ // implicate (see `issueFocus`). The list publishes this through
660
+ // `onExpandedCategoriesChange`; empty while every layer is collapsed.
661
+ const [expandedIssueCategories, setExpandedIssueCategories] = useState([]);
662
+ // Stable setter — the list re-emits on mount and on every toggle, so compare
663
+ // before storing to avoid churn (and to keep the list effect's dep stable).
664
+ const handleExpandedCategoriesChange = useCallback((cats) => {
665
+ setExpandedIssueCategories((prev) => prev.length === cats.length && prev.every((c, i) => c === cats[i])
666
+ ? prev
667
+ : cats);
668
+ }, []);
669
+ // Resolve a component name / symbol / alias reference to a component alias.
670
+ const resolveAlias = useCallback((ref) => {
671
+ const clean = ref.replace(/\(\)$/, '').trim();
672
+ const c = components.find((x) => x.alias === clean ||
673
+ x.name === clean ||
674
+ (x.symbol != null && x.symbol.replace(/\(\)$/, '') === clean));
675
+ return c?.alias ?? null;
676
+ }, [components]);
677
+ // Graph elements implicated by the expanded layers' findings — the bright set
678
+ // the canvas keeps while everything else dims. Derived from each finding's
679
+ // target:
680
+ // component → that node
681
+ // relation → the edge it flags (+ its two endpoint nodes)
682
+ // module → every component in that module
683
+ // walkthrough → the flow's nodes + its hop edges
684
+ // repo → every component of that repo
685
+ // graph → whole-graph finding; implicates nothing specific
686
+ // Returns null when nothing is expanded, no expanded layer has findings, or
687
+ // the findings implicate nothing — so no dimming is applied.
688
+ const issueFocus = useMemo(() => {
689
+ if (!issues?.length || expandedIssueCategories.length === 0)
690
+ return null;
691
+ const active = new Set(expandedIssueCategories);
692
+ const activeIssues = issues.filter((i) => active.has(issueCategory(i)));
693
+ if (activeIssues.length === 0)
694
+ return null;
695
+ const nodeIds = new Set();
696
+ const edgeIds = new Set();
697
+ for (const issue of activeIssues) {
698
+ const t = issue.target;
699
+ if (!t)
700
+ continue; // whole-graph finding — no specific element
701
+ if (t.kind === 'component') {
702
+ const a = resolveAlias(t.id ?? t.label);
703
+ if (a)
704
+ nodeIds.add(a);
705
+ }
706
+ else if (t.kind === 'relation') {
707
+ // The target names the two endpoints (`from → to`); light the edge and
708
+ // its endpoints. Falls back to endpoints alone if no edge matches.
709
+ const edge = issueEdge(issue);
710
+ if (edge) {
711
+ edgeIds.add(edge.id);
712
+ nodeIds.add(edge.source);
713
+ nodeIds.add(edge.target);
714
+ }
715
+ else {
716
+ for (const part of `${t.id ?? ''} → ${t.label}`.split('→')) {
717
+ const a = resolveAlias(part);
718
+ if (a)
719
+ nodeIds.add(a);
720
+ }
721
+ }
722
+ }
723
+ else if (t.kind === 'module') {
724
+ const key = t.id ?? t.label;
725
+ const prefix = `${key.replace(/\/$/, '')}/`;
726
+ for (const c of components) {
727
+ if (c.module === key ||
728
+ c.file === key ||
729
+ (c.file !== '' && c.file.endsWith(key)) ||
730
+ c.file.startsWith(prefix)) {
731
+ nodeIds.add(c.alias);
732
+ }
733
+ }
734
+ }
735
+ else if (t.kind === 'walkthrough') {
736
+ const wt = (walkthroughs ?? []).find((w) => w.id === t.id || w.title === t.label);
737
+ if (wt) {
738
+ for (const s of wt.steps) {
739
+ nodeIds.add(s.from);
740
+ nodeIds.add(s.to);
741
+ edgeIds.add(walkthroughStepGraphEdgeId(s));
742
+ }
743
+ }
744
+ }
745
+ else if (t.kind === 'repo') {
746
+ const key = t.id ?? t.label;
747
+ for (const c of components) {
748
+ if (c.purl === key)
749
+ nodeIds.add(c.alias);
750
+ }
751
+ }
752
+ }
753
+ if (nodeIds.size === 0 && edgeIds.size === 0)
754
+ return null;
755
+ // An edge whose endpoints both sit in the bright set stays lit too, so a
756
+ // focused cluster doesn't read as isolated nodes.
757
+ for (const e of baseEdges) {
758
+ if (nodeIds.has(e.source) && nodeIds.has(e.target))
759
+ edgeIds.add(e.id);
760
+ }
761
+ return { nodeIds, edgeIds };
762
+ }, [issues, expandedIssueCategories, components, walkthroughs, baseEdges, resolveAlias, issueEdge]);
763
+ const issueFocusNodeIds = issueFocus?.nodeIds ?? null;
764
+ const issueFocusEdgeIds = issueFocus?.edgeIds ?? null;
531
765
  // When an edge is selected, dim every other edge + its label to focus it.
532
766
  const [selectedEdgeId, setSelectedEdgeId] = useState(null);
533
767
  // Ref mirror so `selectEdge` (a useCallback over early deps) can toggle
534
768
  // without a stale closure value.
535
769
  const selectedEdgeIdRef = useRef(null);
536
770
  selectedEdgeIdRef.current = selectedEdgeId;
771
+ // Ref mirrors of the focused step, so `unfocusIssueTarget` (a useCallback
772
+ // over early deps) can tell a step focus it still owns from one the user has
773
+ // since moved elsewhere in the walkthrough panel.
774
+ const focusedStepRef = useRef(null);
775
+ focusedStepRef.current =
776
+ focusedWalkthroughId != null && focusedStepIndex != null
777
+ ? { walkthroughId: focusedWalkthroughId, stepIndex: focusedStepIndex }
778
+ : null;
537
779
  // Edge ids in walkthrough focus (an active flow's edge set, or a single
538
780
  // step's edge). Used to frame the camera. `null` = no walkthrough focus.
539
781
  const focusEdgeIds = useMemo(() => {
@@ -678,12 +920,22 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
678
920
  anyOpened: openedNodeIds != null,
679
921
  anySelected: brightNodeIds != null || previewNodeIds != null,
680
922
  });
681
- const dimmed = previewNodeIds
682
- ? vis.hidden || !memberAliases.some((alias) => previewNodeIds.has(alias))
923
+ // `brightNodeIds` is the focused step's endpoints. It has to be a dim
924
+ // source, not just a bright set: without an expanded flow,
925
+ // `flowElementVisibility` reports non-participants as *hidden*, and the
926
+ // noOpenedFlow escape below discards that — leaving them neither hidden
927
+ // nor dimmed. Naming the participants as the dim source dims everything
928
+ // outside the focused step however the flow got focused.
929
+ const dimSource = previewNodeIds ?? brightNodeIds ?? issueFocusNodeIds;
930
+ const dimmed = dimSource
931
+ ? vis.hidden || !memberAliases.some((alias) => dimSource.has(alias))
683
932
  : vis.dimmed;
684
933
  const hidden = noOpenedFlow ? false : vis.hidden;
685
934
  // Host override wins over the library's derived frame color.
686
935
  const color = region?.key != null ? boundaryColors?.[region.key] : undefined;
936
+ // Boundary findings badge the FRAME, not a member — a region's shape is
937
+ // the fault, so no leaf construct carries it.
938
+ const regionIssue = regionIssueByNodeId.get(n.id);
687
939
  return {
688
940
  ...n,
689
941
  hidden,
@@ -692,6 +944,7 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
692
944
  ...n.data,
693
945
  ...(color != null && { color }),
694
946
  ...(dimmed && { dimmed: true }),
947
+ ...(regionIssue && { issue: regionIssue }),
695
948
  },
696
949
  };
697
950
  }
@@ -708,9 +961,16 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
708
961
  anyOpened: openedNodeIds != null,
709
962
  anySelected: brightNodeIds != null || previewNodeIds != null,
710
963
  });
711
- const dimmed = previewNodeIds ? (vis.hidden || !previewNodeIds.has(n.id)) : vis.dimmed;
964
+ // `brightNodeIds` (the focused step's endpoints) is a dim source as well
965
+ // as a bright set — see the group branch above.
966
+ const dimSource = previewNodeIds ?? brightNodeIds ?? issueFocusNodeIds;
967
+ const dimmed = dimSource ? (vis.hidden || !dimSource.has(n.id)) : vis.dimmed;
712
968
  const hidden = noOpenedFlow ? false : vis.hidden;
713
- if (fileMatch === undefined && !isSelected && !dimmed) {
969
+ // Component findings key by alias, boundary findings by region node id.
970
+ // The two id spaces are disjoint (`module:` / `process:` are prefixed), so
971
+ // one lookup covers both node kinds.
972
+ const issueBadge = issueBadgeByAlias.get(n.id) ?? regionIssueByNodeId.get(n.id);
973
+ if (fileMatch === undefined && !isSelected && !dimmed && !issueBadge) {
714
974
  const { fileMatch: _f, isSelected: _s, dimmed: _d, ...rest } = n.data;
715
975
  return { ...n, hidden, data: rest };
716
976
  }
@@ -722,10 +982,11 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
722
982
  ...(fileMatch !== undefined && { fileMatch }),
723
983
  ...(isSelected && { isSelected }),
724
984
  ...(dimmed && { dimmed: true }),
985
+ ...(issueBadge && { issue: issueBadge }),
725
986
  },
726
987
  };
727
988
  });
728
- }, [xyflowNodesBase, openFile, selected, focusNodeIds, openedNodeIds, brightNodeIds, previewNodeIds, boundaryColors]);
989
+ }, [xyflowNodesBase, openFile, selected, focusNodeIds, openedNodeIds, brightNodeIds, previewNodeIds, issueFocusNodeIds, issueBadgeByAlias, regionIssueByNodeId, boundaryColors]);
729
990
  const baseNodesKey = useMemo(() => nodes.map((n) => n.id).sort().join(','), [nodes]);
730
991
  const baseEdgesKey = useMemo(() => convertedEdges.map((e) => e.id).sort().join(','), [convertedEdges]);
731
992
  const dispEdges = useMemo(() => {
@@ -741,13 +1002,17 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
741
1002
  };
742
1003
  };
743
1004
  // Edges outside the selected view are hidden entirely (labels included).
1005
+ // graphify-native edges belong to the relations view (they are static
1006
+ // topology, not runtime hops) regardless of their verb.
744
1007
  const edgeInView = (e) => {
745
- const mechanism = e.data?.mechanism ?? 'uses';
746
- return resolvedEdgeView === 'relations'
747
- ? isRelationMechanism(mechanism)
748
- : isWalkthroughMechanism(mechanism);
1008
+ const d = e.data;
1009
+ const mechanism = d?.mechanism ?? 'uses';
1010
+ if (resolvedEdgeView === 'relations') {
1011
+ return d?.provenance === 'graphify' || isRelationMechanism(mechanism);
1012
+ }
1013
+ return isWalkthroughMechanism(mechanism);
749
1014
  };
750
- if (openedEdgeIds || focusEdgeIds || previewEdgeIds) {
1015
+ if (openedEdgeIds || focusEdgeIds || previewEdgeIds || issueFocusEdgeIds) {
751
1016
  return baseEdges.map((e) => {
752
1017
  const vis = flowElementVisibility({
753
1018
  inOpened: openedEdgeIds?.has(e.id) === true,
@@ -755,7 +1020,8 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
755
1020
  anyOpened: openedEdgeIds != null,
756
1021
  anySelected: focusEdgeIds != null,
757
1022
  });
758
- const dimmed = previewEdgeIds ? vis.hidden || !previewEdgeIds.has(e.id) : vis.dimmed;
1023
+ const dimSource = previewEdgeIds ?? issueFocusEdgeIds;
1024
+ const dimmed = dimSource ? vis.hidden || !dimSource.has(e.id) : vis.dimmed;
759
1025
  const hidden = vis.hidden || !edgeInView(e);
760
1026
  return { ...paint(e, dimmed), hidden };
761
1027
  });
@@ -767,7 +1033,7 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
767
1033
  }));
768
1034
  }
769
1035
  return baseEdges.map((e) => ({ ...e, hidden: !edgeInView(e) }));
770
- }, [baseEdges, selectedEdgeId, openedEdgeIds, focusEdgeIds, previewEdgeIds, resolvedEdgeView]);
1036
+ }, [baseEdges, selectedEdgeId, openedEdgeIds, focusEdgeIds, previewEdgeIds, issueFocusEdgeIds, resolvedEdgeView]);
771
1037
  const onNodesChange = useCallback((changes) => {
772
1038
  // Capture dimension changes (React Flow's measurement callback).
773
1039
  // Group parents are ELK-sized — ignore their measurements.
@@ -809,6 +1075,7 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
809
1075
  return;
810
1076
  }
811
1077
  setSelected(comp);
1078
+ issueModuleFocusRef.current = null;
812
1079
  if (comp.alias)
813
1080
  onSelect?.(comp.alias);
814
1081
  }
@@ -822,6 +1089,8 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
822
1089
  setSelectedEdgeId(null);
823
1090
  setFocusedWalkthroughId(null);
824
1091
  setFocusedStepIndex(null);
1092
+ setEdgeViewOverride(null);
1093
+ issueModuleFocusRef.current = null;
825
1094
  }, []);
826
1095
  // Sidebar file trees — one per repo on multi-repo graphs, each under its
827
1096
  // own owner-avatar header. Clicking a header collapses that repo's tree.
@@ -835,6 +1104,14 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
835
1104
  const [diagnosticsOpen, setDiagnosticsOpen] = useState(showIssues ?? (issues?.length ?? 0) > 0);
836
1105
  const controlledIssues = showIssues !== undefined;
837
1106
  const issuesActive = controlledIssues ? showIssues : diagnosticsOpen;
1107
+ // Layer focus only applies while the diagnostics list is on screen — closing
1108
+ // it clears any dimming the expanded layers were driving.
1109
+ useEffect(() => {
1110
+ if (!issuesActive) {
1111
+ setExpandedIssueCategories([]);
1112
+ setEdgeViewOverride(null);
1113
+ }
1114
+ }, [issuesActive]);
838
1115
  const toggleIssues = () => {
839
1116
  if (!controlledIssues)
840
1117
  setDiagnosticsOpen((v) => !v);
@@ -1015,6 +1292,52 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
1015
1292
  zoomOnWalkthroughFocus,
1016
1293
  walkthroughFocusDurationMs,
1017
1294
  ]);
1295
+ // A node's laid-out rect in absolute flow coords. `absoluteRects` is the
1296
+ // authority; the fallback covers an unparented group node, whose `position` is
1297
+ // already absolute. Nested groups are parent-relative, so they are skipped
1298
+ // rather than mis-boxed.
1299
+ const rectForNode = useCallback((id) => {
1300
+ const r = built.absoluteRects.get(id);
1301
+ if (r)
1302
+ return r;
1303
+ const n = built.nodes.find((x) => x.id === id);
1304
+ if (!n?.position || n.parentId)
1305
+ return undefined;
1306
+ return {
1307
+ x: n.position.x,
1308
+ y: n.position.y,
1309
+ width: n.width ?? 0,
1310
+ height: n.height ?? 0,
1311
+ };
1312
+ }, [built.absoluteRects, built.nodes]);
1313
+ // Frame a set of nodes by their laid-out rects. Unlike `fitFocusBounds` this
1314
+ // takes node ids, so it works for boundary frames too — React Flow's own
1315
+ // `fitView({ nodes })` can't be relied on for group nodes here, and the rects
1316
+ // are already in absolute flow coords (grouped child `position`s are
1317
+ // parent-relative and would skew the box).
1318
+ const fitNodeRects = useCallback((ids, padding) => {
1319
+ let minX = Infinity;
1320
+ let minY = Infinity;
1321
+ let maxX = -Infinity;
1322
+ let maxY = -Infinity;
1323
+ for (const id of ids) {
1324
+ const r = rectForNode(id);
1325
+ if (!r)
1326
+ continue;
1327
+ minX = Math.min(minX, r.x);
1328
+ minY = Math.min(minY, r.y);
1329
+ maxX = Math.max(maxX, r.x + r.width);
1330
+ maxY = Math.max(maxY, r.y + r.height);
1331
+ }
1332
+ if (!Number.isFinite(minX) || !Number.isFinite(minY))
1333
+ return;
1334
+ fitBounds({
1335
+ x: minX,
1336
+ y: minY,
1337
+ width: Math.max(1, maxX - minX),
1338
+ height: Math.max(1, maxY - minY),
1339
+ }, { padding, duration: walkthroughFocusDurationMs });
1340
+ }, [rectForNode, fitBounds, walkthroughFocusDurationMs]);
1018
1341
  // Zoom back out to the full diagram after the last expanded walkthrough
1019
1342
  // closes (visibility restores non-flow nodes that were hidden).
1020
1343
  const fitOverview = useCallback(() => {
@@ -1368,6 +1691,140 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
1368
1691
  setFocusedStepIndex(null);
1369
1692
  onSelect?.(comp.alias);
1370
1693
  }, [components, onSelect]);
1694
+ // Issue click → select + frame the target on the canvas. Component targets
1695
+ // frame their node; relation targets frame their edge (endpoints + the routed
1696
+ // line); other kinds (module / flow / repo) have no single element and just
1697
+ // forward. Selections / focus that would hide the target are cleared first,
1698
+ // then the camera flies to it on the next frame. The host's `onSelectIssue`
1699
+ // still fires afterwards for its own detail.
1700
+ const focusIssueTarget = useCallback((issue) => {
1701
+ const comp = issueComponent(issue);
1702
+ const edge = comp ? null : issueEdge(issue);
1703
+ const step = comp || edge ? null : issueStep(issue);
1704
+ if (comp) {
1705
+ setSelected(comp);
1706
+ setSelectedEdgeId(null);
1707
+ setFocusedWalkthroughId(null);
1708
+ setFocusedStepIndex(null);
1709
+ setHoveredWalkthroughStep(null);
1710
+ issueModuleFocusRef.current = null;
1711
+ onSelect?.(comp.alias);
1712
+ requestAnimationFrame(() => {
1713
+ fitView({
1714
+ nodes: [{ id: comp.alias }],
1715
+ padding: 0.4,
1716
+ duration: 300,
1717
+ minZoom: 0.05,
1718
+ maxZoom: 1.5,
1719
+ });
1720
+ });
1721
+ }
1722
+ else if (edge) {
1723
+ setSelected(null);
1724
+ setFocusedWalkthroughId(null);
1725
+ setFocusedStepIndex(null);
1726
+ setHoveredWalkthroughStep(null);
1727
+ setSelectedEdgeId(edge.id);
1728
+ issueModuleFocusRef.current = null;
1729
+ // Flip to the vocabulary this edge belongs to, so a hop flagged in the
1730
+ // relations view (or vice versa) is actually drawn while focused.
1731
+ const d = edge.data;
1732
+ const mechanism = d?.mechanism ?? 'uses';
1733
+ setEdgeViewOverride(d?.provenance === 'graphify' || isRelationMechanism(mechanism)
1734
+ ? 'relations'
1735
+ : 'walkthroughs');
1736
+ requestAnimationFrame(() => {
1737
+ fitFocusBounds(new Set([edge.id]));
1738
+ });
1739
+ }
1740
+ else if (step) {
1741
+ // A step finding focuses the step itself, through the same entry point
1742
+ // the walkthrough panel uses — so the drawer, the step numbering, the
1743
+ // dim-mode hover preview, and the camera all behave identically whether
1744
+ // the step was reached from the panel or from a diagnostics card.
1745
+ issueModuleFocusRef.current = null;
1746
+ focusWalkthroughStep(step.walkthrough, step.stepIndex);
1747
+ }
1748
+ else {
1749
+ // Nothing selectable — but a module target names a boundary frame, so
1750
+ // frame that region.
1751
+ const moduleNodeId = issueModuleNodeId(issue);
1752
+ if (moduleNodeId) {
1753
+ setSelected(null);
1754
+ setSelectedEdgeId(null);
1755
+ setFocusedWalkthroughId(null);
1756
+ setFocusedStepIndex(null);
1757
+ setHoveredWalkthroughStep(null);
1758
+ issueModuleFocusRef.current = moduleNodeId;
1759
+ requestAnimationFrame(() => {
1760
+ fitNodeRects(new Set([moduleNodeId]), 0.4);
1761
+ });
1762
+ }
1763
+ }
1764
+ onSelectIssue?.(issue);
1765
+ }, [
1766
+ issueComponent,
1767
+ issueEdge,
1768
+ issueStep,
1769
+ issueModuleNodeId,
1770
+ focusWalkthroughStep,
1771
+ onSelect,
1772
+ onSelectIssue,
1773
+ fitView,
1774
+ fitFocusBounds,
1775
+ fitNodeRects,
1776
+ ]);
1777
+ // Collapsing the issue card again undoes the focus: drop the node / edge
1778
+ // selection (only when it is still this issue's target — another card may
1779
+ // have taken it over) and zoom back out to the whole graph. A module target
1780
+ // frames a region rather than selecting anything, so its ownership lives in
1781
+ // `issueModuleFocusRef`; without this branch a module focus never unwinds and
1782
+ // the camera never zooms back out.
1783
+ const unfocusIssueTarget = useCallback((issue) => {
1784
+ const comp = issueComponent(issue);
1785
+ if (comp) {
1786
+ if (selectedRef.current?.alias !== comp.alias)
1787
+ return;
1788
+ setSelected(null);
1789
+ setSelectedEdgeId(null);
1790
+ }
1791
+ else {
1792
+ const edge = issueEdge(issue);
1793
+ const step = edge ? null : issueStep(issue);
1794
+ const moduleNodeId = edge || step ? null : issueModuleNodeId(issue);
1795
+ if (edge) {
1796
+ if (selectedEdgeIdRef.current !== edge.id)
1797
+ return;
1798
+ setSelectedEdgeId(null);
1799
+ setEdgeViewOverride(null);
1800
+ }
1801
+ else if (step) {
1802
+ // Only unwind a step focus this card still owns. A dim-mode graph
1803
+ // keeps its step focus in hover state instead (owned by the pointer,
1804
+ // not the card), and correctly leaves that alone here.
1805
+ const owned = focusedStepRef.current;
1806
+ if (owned?.walkthroughId !== step.walkthrough.id ||
1807
+ owned?.stepIndex !== step.stepIndex) {
1808
+ return;
1809
+ }
1810
+ setFocusedWalkthroughId(null);
1811
+ setFocusedStepIndex(null);
1812
+ setDrawerTarget((prev) => (prev?.kind === 'walkthrough' ? null : prev));
1813
+ }
1814
+ else if (moduleNodeId) {
1815
+ // Only unwind framing this card still owns.
1816
+ if (issueModuleFocusRef.current !== moduleNodeId)
1817
+ return;
1818
+ issueModuleFocusRef.current = null;
1819
+ }
1820
+ else {
1821
+ return;
1822
+ }
1823
+ }
1824
+ requestAnimationFrame(() => {
1825
+ fitView({ padding: 0.1, duration: 300, minZoom: 0.05, maxZoom: 2 });
1826
+ });
1827
+ }, [issueComponent, issueEdge, issueStep, issueModuleNodeId, fitView]);
1371
1828
  // Construct tokens inside a walkthrough snippet. A step's line is an edge
1372
1829
  // between its `from`/`to` components, so a token naming either of those
1373
1830
  // constructs should navigate to that construct's declaration. Index the
@@ -1428,6 +1885,7 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
1428
1885
  return {
1429
1886
  id: e.id,
1430
1887
  mechanism: d?.mechanism ?? 'uses',
1888
+ provenance: d?.provenance,
1431
1889
  dimmed: d?.dimmed === true,
1432
1890
  midX: d?.labelX ?? 0,
1433
1891
  midY: d?.labelY ?? 0,
@@ -1518,7 +1976,7 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
1518
1976
  display: 'flex',
1519
1977
  flexDirection: 'column',
1520
1978
  overflow: 'hidden',
1521
- }, children: issuesActive ? (_jsx(SubsystemIssueList, { issues: issues ?? [], focusCategory: focusIssueCategory, onSelectIssue: onSelectIssue, onApplyFix: onApplyIssueFix, onHoverIssue: onHoverIssue })) : (_jsxs(_Fragment, { children: [hasWalkthroughs && (_jsx("div", { role: "tablist", "aria-label": "Sidebar view", style: {
1979
+ }, children: issuesActive ? (_jsx(SubsystemIssueList, { issues: issues ?? [], focusCategory: focusIssueCategory, onSelectIssue: focusIssueTarget, onDeselectIssue: unfocusIssueTarget, onApplyFix: onApplyIssueFix, onHoverIssue: onHoverIssue, onExpandedCategoriesChange: handleExpandedCategoriesChange })) : (_jsxs(_Fragment, { children: [hasWalkthroughs && (_jsx("div", { role: "tablist", "aria-label": "Sidebar view", style: {
1522
1980
  display: 'flex',
1523
1981
  width: '100%',
1524
1982
  flexShrink: 0,
@@ -1571,14 +2029,24 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
1571
2029
  zIndex: 5,
1572
2030
  }, children: edgeLabels.map((lbl) => {
1573
2031
  const mechanism = lbl.mechanism;
1574
- const color = MECHANISM_COLOR[mechanism] ?? '#888';
1575
- const verifiable = MECHANISM_DESCRIPTIONS.find(([m]) => m === mechanism)?.[2] ?? true;
2032
+ const isGraphify = lbl.provenance === 'graphify';
2033
+ const color = edgeColor({ mechanism, provenance: lbl.provenance });
2034
+ // Label chrome stays the crisp rounded box for every edge — graphify
2035
+ // provenance is carried by hue (GRAPHIFY_RELATION_COLOR), the dashed
2036
+ // stroke, and the tooltip, NOT by the "ambiguous" cloud silhouette
2037
+ // (that idiom is reserved for soft authored mechanisms).
2038
+ const verifiable = true;
2039
+ const labelTitle = isGraphify
2040
+ ? 'Derived from graphify (static symbol graph)'
2041
+ : MECHANISM_DESCRIPTIONS.find(([m]) => m === mechanism)?.[2] === false
2042
+ ? 'Not directly verifiable with graphify'
2043
+ : undefined;
1576
2044
  const screenX = lbl.midX * viewport.zoom + viewport.x;
1577
2045
  const screenY = lbl.midY * viewport.zoom + viewport.y;
1578
2046
  const text = lbl.stepNos?.length
1579
2047
  ? `${lbl.stepNos.map((n) => `${n}:`).join(' ')} ${lbl.mechanism}`
1580
2048
  : lbl.mechanism;
1581
- return (_jsxs("div", { "data-edge-label": lbl.id, title: verifiable ? undefined : 'Not directly verifiable with graphify', onClick: (e) => {
2049
+ return (_jsxs("div", { "data-edge-label": lbl.id, title: labelTitle, onClick: (e) => {
1582
2050
  e.stopPropagation();
1583
2051
  selectEdge(lbl.id);
1584
2052
  }, style: {
@@ -1828,6 +2296,7 @@ export function SubsystemComponentGraph(props) {
1828
2296
  components: props.components,
1829
2297
  relations: props.relations,
1830
2298
  walkthroughs: props.walkthroughs,
2299
+ graphifyRelations: props.graphifyRelations,
1831
2300
  });
1832
2301
  return (_jsx("div", { ref: wrapRef, style: {
1833
2302
  position: 'relative',