@principal-ai/principal-view-react 0.16.33 → 0.16.35
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.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/subsystem/ComponentDetail.d.ts +12 -0
- package/dist/subsystem/ComponentDetail.d.ts.map +1 -0
- package/dist/subsystem/ComponentDetail.js +95 -0
- package/dist/subsystem/ComponentDetail.js.map +1 -0
- package/dist/subsystem/EdgeLegendModal.d.ts +18 -0
- package/dist/subsystem/EdgeLegendModal.d.ts.map +1 -0
- package/dist/subsystem/EdgeLegendModal.js +98 -0
- package/dist/subsystem/EdgeLegendModal.js.map +1 -0
- package/dist/subsystem/FileDrawer.d.ts +16 -0
- package/dist/subsystem/FileDrawer.d.ts.map +1 -0
- package/dist/subsystem/FileDrawer.js +72 -0
- package/dist/subsystem/FileDrawer.js.map +1 -0
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +34 -182
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/SubsystemFileTree.d.ts +10 -15
- package/dist/subsystem/SubsystemFileTree.d.ts.map +1 -1
- package/dist/subsystem/SubsystemFileTree.js +96 -93
- package/dist/subsystem/SubsystemFileTree.js.map +1 -1
- package/dist/subsystem/nodes.d.ts +4 -2
- package/dist/subsystem/nodes.d.ts.map +1 -1
- package/dist/subsystem/nodes.js +13 -26
- package/dist/subsystem/nodes.js.map +1 -1
- package/package.json +2 -1
- package/src/index.ts +1 -1
- package/src/stories/SubsystemComponentGraph.stories.tsx +8 -0
- package/src/subsystem/ComponentDetail.tsx +187 -0
- package/src/subsystem/EdgeLegendModal.tsx +146 -0
- package/src/subsystem/FileDrawer.tsx +105 -0
- package/src/subsystem/SubsystemComponentGraph.tsx +49 -316
- package/src/subsystem/SubsystemFileTree.tsx +102 -141
- package/src/subsystem/nodes.tsx +17 -35
|
@@ -32,40 +32,21 @@ import {
|
|
|
32
32
|
applyNodeChanges,
|
|
33
33
|
} from '@xyflow/react';
|
|
34
34
|
import { useTheme } from '@principal-ade/industry-theme';
|
|
35
|
-
import {
|
|
35
|
+
import { Map as MapIcon } from 'lucide-react';
|
|
36
36
|
import { IndustryMarkdownSlide } from 'themed-markdown';
|
|
37
37
|
import {
|
|
38
38
|
buildSubsystemGraph,
|
|
39
|
-
KIND_COLOR,
|
|
40
39
|
MECHANISM_COLOR,
|
|
41
|
-
deriveNameFromSymbol,
|
|
42
40
|
type SubsystemComponentEdge,
|
|
43
41
|
type SubsystemComponent,
|
|
44
42
|
type SubsystemEdgeMechanism,
|
|
45
43
|
} from './model';
|
|
46
|
-
import type { GraphifyComponentDetail } from '../graphify';
|
|
47
44
|
import { SubsystemComponentNode, SubsystemEdge, SUBSYSTEM_CALLBACKS } from './nodes';
|
|
48
45
|
import { SubsystemFileTree } from './SubsystemFileTree';
|
|
49
46
|
import { GraphLayoutCover } from './GraphLayoutCover';
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
['imports_from', 'imported by (reverse dependency)', true],
|
|
54
|
-
['re_exports', 're-exports symbols from', true],
|
|
55
|
-
['defines', 'defines / declares symbol', true],
|
|
56
|
-
['calls', 'function/method call (call graph edge)', true],
|
|
57
|
-
['extends', 'class inheritance', true],
|
|
58
|
-
['inherits', 'class inheritance', true],
|
|
59
|
-
['implements', 'implements interface / protocol', true],
|
|
60
|
-
['mixes_in', 'applies mixin', true],
|
|
61
|
-
['uses', 'general dependency (import, call, or reference)', false],
|
|
62
|
-
['method', 'structural: has method / member', true],
|
|
63
|
-
['references', 'type / symbol reference (not a call)', true],
|
|
64
|
-
['contains', 'structural: contains / encapsulates', true],
|
|
65
|
-
['feeds', 'data flow: output feeds into input', false],
|
|
66
|
-
['produces', 'data flow: produces / outputs', false],
|
|
67
|
-
['registers-into', 'registration pattern', false],
|
|
68
|
-
];
|
|
47
|
+
import { ComponentDetail } from './ComponentDetail';
|
|
48
|
+
import { FileDrawer } from './FileDrawer';
|
|
49
|
+
import { EdgeLegendModal, MECHANISM_DESCRIPTIONS } from './EdgeLegendModal';
|
|
69
50
|
|
|
70
51
|
export interface SubsystemComponentGraphProps {
|
|
71
52
|
components: SubsystemComponent[];
|
|
@@ -132,6 +113,10 @@ function Inner({ components, edges, onSelect, onEdgeSelect, measured: _measured,
|
|
|
132
113
|
const [selected, setSelected] = useState<SubsystemComponent | null>(null);
|
|
133
114
|
// File currently shown in the bottom drawer (repo-root-relative path).
|
|
134
115
|
const [openFile, setOpenFile] = useState<string | null>(null);
|
|
116
|
+
// Edge-legend modal visibility (opened from the canvas's top-left button).
|
|
117
|
+
const [legendOpen, setLegendOpen] = useState(false);
|
|
118
|
+
// Component the pointer is over (null on leave) → transient tree highlight.
|
|
119
|
+
const [hoveredComponentId, setHoveredComponentId] = useState<string | null>(null);
|
|
135
120
|
// Ref mirror of `selected` so the SUBSYSTEM_CALLBACKS click handler (a
|
|
136
121
|
// closure over the effect deps) can toggle without a stale value.
|
|
137
122
|
const selectedRef = useRef<SubsystemComponent | null>(null);
|
|
@@ -226,10 +211,12 @@ function Inner({ components, edges, onSelect, onEdgeSelect, measured: _measured,
|
|
|
226
211
|
};
|
|
227
212
|
SUBSYSTEM_CALLBACKS.onEdgeSelect = selectEdge;
|
|
228
213
|
SUBSYSTEM_CALLBACKS.maxNodeWidth = maxNodeWidth;
|
|
214
|
+
SUBSYSTEM_CALLBACKS.onHover = setHoveredComponentId;
|
|
229
215
|
return () => {
|
|
230
216
|
SUBSYSTEM_CALLBACKS.onSelect = undefined;
|
|
231
217
|
SUBSYSTEM_CALLBACKS.onEdgeSelect = undefined;
|
|
232
218
|
SUBSYSTEM_CALLBACKS.maxNodeWidth = undefined;
|
|
219
|
+
SUBSYSTEM_CALLBACKS.onHover = undefined;
|
|
233
220
|
};
|
|
234
221
|
}, [components, onSelect, maxNodeWidth, selectEdge]);
|
|
235
222
|
|
|
@@ -393,14 +380,18 @@ function Inner({ components, edges, onSelect, onEdgeSelect, measured: _measured,
|
|
|
393
380
|
return new Set(edgeLabels.map((l) => l.mechanism));
|
|
394
381
|
}, [edgeLabels]);
|
|
395
382
|
|
|
396
|
-
const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
|
|
397
|
-
|
|
398
383
|
// Unique source files across components → sidebar file tree.
|
|
399
384
|
const files = useMemo(
|
|
400
385
|
() => Array.from(new Set(components.map((c) => c.file).filter(Boolean))).sort(),
|
|
401
386
|
[components],
|
|
402
387
|
);
|
|
403
388
|
|
|
389
|
+
// The hovered node's file (null when not hovering / file-less component).
|
|
390
|
+
const hoveredFile = useMemo(() => {
|
|
391
|
+
if (!hoveredComponentId) return null;
|
|
392
|
+
return components.find((c) => c.id === hoveredComponentId)?.file ?? null;
|
|
393
|
+
}, [components, hoveredComponentId]);
|
|
394
|
+
|
|
404
395
|
// Drawer content renderer: prefer the path-keyed viewer; fall back to the
|
|
405
396
|
// legacy component-keyed one via a file → first-component lookup.
|
|
406
397
|
const fileViewer = useMemo(() => {
|
|
@@ -419,8 +410,8 @@ function Inner({ components, edges, onSelect, onEdgeSelect, measured: _measured,
|
|
|
419
410
|
|
|
420
411
|
return (
|
|
421
412
|
<div style={{ width: '100%', height: '100%', display: 'flex', flexDirection: 'row' }}>
|
|
422
|
-
{/* Sidebar: scrollable title/description
|
|
423
|
-
{(title || description ||
|
|
413
|
+
{/* Sidebar: scrollable title/description on top, file tree pinned to the bottom half */}
|
|
414
|
+
{(title || description || sidebarExtra || sidebarAfterDescription || files.length > 0) && (
|
|
424
415
|
<div
|
|
425
416
|
style={{
|
|
426
417
|
width: 340,
|
|
@@ -473,34 +464,12 @@ function Inner({ components, edges, onSelect, onEdgeSelect, measured: _measured,
|
|
|
473
464
|
</div>
|
|
474
465
|
)}
|
|
475
466
|
{sidebarAfterDescription}
|
|
476
|
-
{usedMechanisms.size > 0 && (
|
|
477
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
|
478
|
-
<span style={{ fontSize: theme.fontSizes[0] * 0.8, fontFamily: theme.fonts.monospace, textTransform: 'uppercase', color: muted, fontWeight: 600 }}>
|
|
479
|
-
Edge Legend
|
|
480
|
-
</span>
|
|
481
|
-
{MECHANISM_DESCRIPTIONS.filter(([m]) => usedMechanisms.has(m)).map(([mechanism, desc, verifiable]) => (
|
|
482
|
-
<div key={mechanism} style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: theme.fontSizes[0] }}>
|
|
483
|
-
<span
|
|
484
|
-
style={{
|
|
485
|
-
width: 8,
|
|
486
|
-
height: 8,
|
|
487
|
-
borderRadius: verifiable ? '50%' : '30% 70% 70% 30% / 30% 30% 70% 70%',
|
|
488
|
-
background: MECHANISM_COLOR[mechanism],
|
|
489
|
-
border: verifiable ? 'none' : `1px dashed ${MECHANISM_COLOR[mechanism]}`,
|
|
490
|
-
flexShrink: 0,
|
|
491
|
-
}}
|
|
492
|
-
/>
|
|
493
|
-
<span style={{ fontFamily: theme.fonts.monospace, color: MECHANISM_COLOR[mechanism], fontWeight: 500 }}>{mechanism}</span>
|
|
494
|
-
<span style={{ color: muted }}>{desc}</span>
|
|
495
|
-
</div>
|
|
496
|
-
))}
|
|
497
|
-
</div>
|
|
498
|
-
)}
|
|
499
467
|
</div>
|
|
500
468
|
{files.length > 0 && (
|
|
501
469
|
<SubsystemFileTree
|
|
502
470
|
files={files}
|
|
503
471
|
selectedFile={selected?.file ?? openFile}
|
|
472
|
+
hoveredFile={hoveredFile}
|
|
504
473
|
onSelectFile={onTreeSelectFile}
|
|
505
474
|
/>
|
|
506
475
|
)}
|
|
@@ -594,287 +563,51 @@ function Inner({ components, edges, onSelect, onEdgeSelect, measured: _measured,
|
|
|
594
563
|
<Background variant={BackgroundVariant.Dots} gap={16} size={1} />
|
|
595
564
|
<Controls showZoom showFitView showInteractive />
|
|
596
565
|
</ReactFlow>
|
|
597
|
-
{
|
|
598
|
-
|
|
599
|
-
{fileViewer && openFile ? fileViewer(openFile) : null}
|
|
600
|
-
</FileDrawer>
|
|
601
|
-
{/* Startup cover — hides measurement, layout swap, and camera settle. */}
|
|
602
|
-
<GraphLayoutCover revealed={layoutReady} />
|
|
603
|
-
{canvasOverlay}
|
|
604
|
-
</div>
|
|
605
|
-
</div>
|
|
606
|
-
);
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
/** Detail panel for the selected component — the verifiable identity + sources
|
|
610
|
-
* moved off the canvas so nodes stay minimal. File content lives in the
|
|
611
|
-
* bottom FileDrawer, not here. */
|
|
612
|
-
function ComponentDetail({ component }: { component: SubsystemComponent }) {
|
|
613
|
-
const { theme } = useTheme();
|
|
614
|
-
const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
|
|
615
|
-
const color = KIND_COLOR[component.kind] ?? '#888';
|
|
616
|
-
const displayName = deriveNameFromSymbol(component.symbol, component.kind, component.name, component.file);
|
|
617
|
-
const rows: Array<[string, string]> = [];
|
|
618
|
-
if (component.symbol) rows.push(['Symbol', component.symbol]);
|
|
619
|
-
if (component.file) rows.push(['File', component.file]);
|
|
620
|
-
if (component.purpose) rows.push(['Purpose', component.purpose]);
|
|
621
|
-
rows.push(['PURL', component.purl]);
|
|
622
|
-
|
|
623
|
-
return (
|
|
624
|
-
<div
|
|
625
|
-
style={{
|
|
626
|
-
borderTop: `1px solid ${theme.colors.border}`,
|
|
627
|
-
background: theme.colors.background,
|
|
628
|
-
padding: '10px 12px',
|
|
629
|
-
fontFamily: theme.fonts.body,
|
|
630
|
-
}}
|
|
631
|
-
>
|
|
632
|
-
<div
|
|
633
|
-
style={{
|
|
634
|
-
display: 'flex',
|
|
635
|
-
alignItems: 'center',
|
|
636
|
-
gap: 8,
|
|
637
|
-
fontSize: theme.fontSizes[1],
|
|
638
|
-
fontWeight: 600,
|
|
639
|
-
marginBottom: 6,
|
|
640
|
-
}}
|
|
641
|
-
>
|
|
642
|
-
<span style={{ color }}>{displayName}</span>
|
|
643
|
-
<span
|
|
644
|
-
style={{
|
|
645
|
-
fontSize: theme.fontSizes[0] * 0.8,
|
|
646
|
-
fontFamily: theme.fonts.monospace,
|
|
647
|
-
textTransform: 'uppercase',
|
|
648
|
-
color,
|
|
649
|
-
}}
|
|
650
|
-
>
|
|
651
|
-
{component.kind}
|
|
652
|
-
</span>
|
|
653
|
-
</div>
|
|
654
|
-
{rows.map(([k, v]) => (
|
|
655
|
-
<div
|
|
656
|
-
key={k}
|
|
657
|
-
style={{ display: 'flex', gap: 8, fontSize: theme.fontSizes[0], lineHeight: 1.5 }}
|
|
658
|
-
>
|
|
659
|
-
<span
|
|
660
|
-
style={{
|
|
661
|
-
width: 64,
|
|
662
|
-
flexShrink: 0,
|
|
663
|
-
color: muted,
|
|
664
|
-
fontFamily: theme.fonts.monospace,
|
|
665
|
-
textTransform: 'uppercase',
|
|
666
|
-
fontSize: theme.fontSizes[0] * 0.8,
|
|
667
|
-
}}
|
|
668
|
-
>
|
|
669
|
-
{k}
|
|
670
|
-
</span>
|
|
671
|
-
<span style={{ color: theme.colors.text, fontFamily: theme.fonts.monospace }}>
|
|
672
|
-
{v}
|
|
673
|
-
</span>
|
|
674
|
-
</div>
|
|
675
|
-
))}
|
|
676
|
-
{component.detail && <GraphifyDetailSections detail={component.detail} />}
|
|
677
|
-
</div>
|
|
678
|
-
);
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
/** Bottom panel that slides up from the bottom of the graph area to show a
|
|
682
|
-
* file's contents — opened by node clicks and sidebar file-tree clicks
|
|
683
|
-
* alike. Sits in normal flow (canvas shrinks while open, nothing covered)
|
|
684
|
-
* and animates via height; stays mounted so open/close animates. */
|
|
685
|
-
function FileDrawer({
|
|
686
|
-
file,
|
|
687
|
-
onClose,
|
|
688
|
-
children,
|
|
689
|
-
}: {
|
|
690
|
-
file: string | null;
|
|
691
|
-
onClose: () => void;
|
|
692
|
-
children?: ReactNode;
|
|
693
|
-
}) {
|
|
694
|
-
const { theme } = useTheme();
|
|
695
|
-
const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
|
|
696
|
-
const open = file !== null;
|
|
697
|
-
|
|
698
|
-
useEffect(() => {
|
|
699
|
-
if (!open) return;
|
|
700
|
-
const onKey = (e: KeyboardEvent) => {
|
|
701
|
-
if (e.key === 'Escape') onClose();
|
|
702
|
-
};
|
|
703
|
-
window.addEventListener('keydown', onKey);
|
|
704
|
-
return () => window.removeEventListener('keydown', onKey);
|
|
705
|
-
}, [open, onClose]);
|
|
706
|
-
|
|
707
|
-
return (
|
|
708
|
-
<div
|
|
709
|
-
style={{
|
|
710
|
-
position: 'relative',
|
|
711
|
-
// Above the absolute edge-label overlay (zIndex 5), which spans the
|
|
712
|
-
// whole graph-area container including this panel's slice.
|
|
713
|
-
zIndex: 6,
|
|
714
|
-
flexShrink: 0,
|
|
715
|
-
height: open ? '45%' : 0,
|
|
716
|
-
minHeight: 0,
|
|
717
|
-
overflow: 'hidden',
|
|
718
|
-
display: 'flex',
|
|
719
|
-
flexDirection: 'column',
|
|
720
|
-
background: theme.colors.background,
|
|
721
|
-
borderTop: open ? `1px solid ${theme.colors.border}` : 'none',
|
|
722
|
-
transition: 'height 200ms ease',
|
|
723
|
-
}}
|
|
724
|
-
>
|
|
725
|
-
<div
|
|
726
|
-
style={{
|
|
727
|
-
display: 'flex',
|
|
728
|
-
alignItems: 'center',
|
|
729
|
-
gap: 8,
|
|
730
|
-
padding: '6px 10px',
|
|
731
|
-
borderBottom: `1px solid ${theme.colors.border}`,
|
|
732
|
-
flexShrink: 0,
|
|
733
|
-
}}
|
|
734
|
-
>
|
|
735
|
-
<span
|
|
736
|
-
title={file ?? undefined}
|
|
737
|
-
style={{
|
|
738
|
-
flex: 1,
|
|
739
|
-
minWidth: 0,
|
|
740
|
-
overflow: 'hidden',
|
|
741
|
-
textOverflow: 'ellipsis',
|
|
742
|
-
whiteSpace: 'nowrap',
|
|
743
|
-
fontFamily: theme.fonts.monospace,
|
|
744
|
-
fontSize: theme.fontSizes[0],
|
|
745
|
-
color: muted,
|
|
746
|
-
}}
|
|
747
|
-
>
|
|
748
|
-
{file}
|
|
749
|
-
</span>
|
|
566
|
+
{/* Legend button — top-left overlay on the canvas; opens the modal. */}
|
|
567
|
+
{usedMechanisms.size > 0 && (
|
|
750
568
|
<button
|
|
751
569
|
type="button"
|
|
752
|
-
onClick={
|
|
753
|
-
aria-label="Close file"
|
|
570
|
+
onClick={() => setLegendOpen(true)}
|
|
754
571
|
style={{
|
|
572
|
+
position: 'absolute',
|
|
573
|
+
top: 10,
|
|
574
|
+
left: 10,
|
|
575
|
+
zIndex: 7,
|
|
755
576
|
display: 'inline-flex',
|
|
756
577
|
alignItems: 'center',
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
border: 'none',
|
|
762
|
-
borderRadius: 4,
|
|
763
|
-
background: 'transparent',
|
|
578
|
+
gap: 6,
|
|
579
|
+
padding: '4px 10px',
|
|
580
|
+
fontSize: theme.fontSizes[0],
|
|
581
|
+
fontFamily: theme.fonts.body,
|
|
764
582
|
color: theme.colors.text,
|
|
583
|
+
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
584
|
+
border: `1px solid ${theme.colors.border}`,
|
|
585
|
+
borderRadius: 6,
|
|
765
586
|
cursor: 'pointer',
|
|
587
|
+
boxShadow: '0 1px 4px rgba(0,0,0,0.25)',
|
|
766
588
|
}}
|
|
767
589
|
>
|
|
768
|
-
<
|
|
590
|
+
<MapIcon size={13} />
|
|
591
|
+
Legend
|
|
769
592
|
</button>
|
|
593
|
+
)}
|
|
594
|
+
<EdgeLegendModal
|
|
595
|
+
open={legendOpen}
|
|
596
|
+
mechanisms={usedMechanisms}
|
|
597
|
+
onClose={() => setLegendOpen(false)}
|
|
598
|
+
/>
|
|
599
|
+
{selected && <ComponentDetail component={selected} />}
|
|
600
|
+
<FileDrawer file={openFile} onClose={() => setOpenFile(null)}>
|
|
601
|
+
{fileViewer && openFile ? fileViewer(openFile) : null}
|
|
602
|
+
</FileDrawer>
|
|
603
|
+
{/* Startup cover — hides measurement, layout swap, and camera settle. */}
|
|
604
|
+
<GraphLayoutCover revealed={layoutReady} />
|
|
605
|
+
{canvasOverlay}
|
|
770
606
|
</div>
|
|
771
|
-
<div style={{ flex: 1, minHeight: 0, overflow: 'auto' }}>{children}</div>
|
|
772
607
|
</div>
|
|
773
608
|
);
|
|
774
609
|
}
|
|
775
610
|
|
|
776
|
-
/** Renders the graphify drill-down payload for a component. */
|
|
777
|
-
function GraphifyDetailSections({ detail }: { detail: GraphifyComponentDetail }) {
|
|
778
|
-
const { theme } = useTheme();
|
|
779
|
-
const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
|
|
780
|
-
|
|
781
|
-
const label = (k: string) => (
|
|
782
|
-
<span
|
|
783
|
-
style={{
|
|
784
|
-
width: 64,
|
|
785
|
-
flexShrink: 0,
|
|
786
|
-
color: muted,
|
|
787
|
-
fontFamily: theme.fonts.monospace,
|
|
788
|
-
textTransform: 'uppercase',
|
|
789
|
-
fontSize: theme.fontSizes[0] * 0.8,
|
|
790
|
-
}}
|
|
791
|
-
>
|
|
792
|
-
{k}
|
|
793
|
-
</span>
|
|
794
|
-
);
|
|
795
|
-
const chip = (name: string, meta?: string, key?: string | number) => (
|
|
796
|
-
<span
|
|
797
|
-
key={key}
|
|
798
|
-
style={{
|
|
799
|
-
fontFamily: theme.fonts.monospace,
|
|
800
|
-
fontSize: theme.fontSizes[0],
|
|
801
|
-
color: theme.colors.text,
|
|
802
|
-
border: `1px solid ${theme.colors.border}`,
|
|
803
|
-
borderRadius: 4,
|
|
804
|
-
padding: '1px 6px',
|
|
805
|
-
marginRight: 6,
|
|
806
|
-
marginBottom: 4,
|
|
807
|
-
display: 'inline-block',
|
|
808
|
-
}}
|
|
809
|
-
>
|
|
810
|
-
{name}
|
|
811
|
-
{meta ? <span style={{ color: muted }}> {meta}</span> : ''}
|
|
812
|
-
</span>
|
|
813
|
-
);
|
|
814
|
-
const row = (k: string, children: ReactNode) => (
|
|
815
|
-
<div
|
|
816
|
-
style={{ display: 'flex', gap: 8, fontSize: theme.fontSizes[0], lineHeight: 1.5, marginBottom: 4 }}
|
|
817
|
-
>
|
|
818
|
-
{label(k)}
|
|
819
|
-
<div style={{ minWidth: 0 }}>{children}</div>
|
|
820
|
-
</div>
|
|
821
|
-
);
|
|
822
|
-
|
|
823
|
-
switch (detail.kind) {
|
|
824
|
-
case 'class':
|
|
825
|
-
return (
|
|
826
|
-
<>
|
|
827
|
-
{detail.methods.length > 0 &&
|
|
828
|
-
row('methods', detail.methods.map((m, i) => chip(m.name, m.returnType, i)))}
|
|
829
|
-
{detail.properties.length > 0 &&
|
|
830
|
-
row('props', detail.properties.map((p, i) => chip(p.name, p.type, i)))}
|
|
831
|
-
{detail.extends.length > 0 && row('extends', detail.extends.map((e, i) => chip(e, undefined, i)))}
|
|
832
|
-
{detail.implements.length > 0 && row('impl', detail.implements.map((e, i) => chip(e, undefined, i)))}
|
|
833
|
-
{detail.instantiations.length > 0 &&
|
|
834
|
-
row('insts', detail.instantiations.map((i, idx) => chip(i.name, undefined, idx)))}
|
|
835
|
-
{detail.references.length > 0 &&
|
|
836
|
-
row('refs', detail.references.map((r, idx) => chip(r.name, undefined, idx)))}
|
|
837
|
-
</>
|
|
838
|
-
);
|
|
839
|
-
case 'function':
|
|
840
|
-
return (
|
|
841
|
-
<>
|
|
842
|
-
{detail.parameters.length > 0 &&
|
|
843
|
-
row('params', detail.parameters.map((p, i) => chip(p.type, undefined, i)))}
|
|
844
|
-
{detail.returnType && row('return', <span>{detail.returnType}</span>)}
|
|
845
|
-
{detail.callers.length > 0 &&
|
|
846
|
-
row('callers', detail.callers.map((c, i) => chip(c.name, undefined, i)))}
|
|
847
|
-
{detail.callees.length > 0 &&
|
|
848
|
-
row('callees', detail.callees.map((c, i) => chip(c.name, undefined, i)))}
|
|
849
|
-
</>
|
|
850
|
-
);
|
|
851
|
-
case 'type':
|
|
852
|
-
return (
|
|
853
|
-
<>
|
|
854
|
-
{detail.properties.length > 0 &&
|
|
855
|
-
row('props', detail.properties.map((p, i) => chip(p.name, p.type, i)))}
|
|
856
|
-
{detail.usedBy.length > 0 &&
|
|
857
|
-
row('used by', detail.usedBy.map((u, i) => chip(u.name, undefined, i)))}
|
|
858
|
-
{detail.implementors.length > 0 &&
|
|
859
|
-
row('impl', detail.implementors.map((e, i) => chip(e, undefined, i)))}
|
|
860
|
-
</>
|
|
861
|
-
);
|
|
862
|
-
case 'module':
|
|
863
|
-
return (
|
|
864
|
-
<>
|
|
865
|
-
{detail.exports.length > 0 &&
|
|
866
|
-
row('exports', detail.exports.map((e, i) => chip(e, undefined, i)))}
|
|
867
|
-
{detail.imports.length > 0 &&
|
|
868
|
-
row('imports', detail.imports.map((i, idx) => chip(i.name, undefined, idx)))}
|
|
869
|
-
{detail.symbols.length > 0 &&
|
|
870
|
-
row('symbols', detail.symbols.map((s, i) => chip(s, undefined, i)))}
|
|
871
|
-
</>
|
|
872
|
-
);
|
|
873
|
-
case 'external':
|
|
874
|
-
return row('source', <span>{detail.label}</span>);
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
|
|
878
611
|
export function SubsystemComponentGraph(props: SubsystemComponentGraphProps) {
|
|
879
612
|
const wrapRef = useRef<HTMLDivElement>(null);
|
|
880
613
|
const [size, setSize] = useState<{ w: number; h: number } | null>(null);
|