@principal-ai/subsystems-react 0.24.0 → 0.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/subsystem/ComponentDeclaration.d.ts +3 -1
- package/dist/subsystem/ComponentDeclaration.d.ts.map +1 -1
- package/dist/subsystem/ComponentDeclaration.js +2 -2
- package/dist/subsystem/ComponentDeclaration.js.map +1 -1
- package/dist/subsystem/ConstructsCatalog.d.ts +30 -0
- package/dist/subsystem/ConstructsCatalog.d.ts.map +1 -0
- package/dist/subsystem/ConstructsCatalog.js +349 -0
- package/dist/subsystem/ConstructsCatalog.js.map +1 -0
- package/dist/subsystem/DiagnosticToggle.d.ts +46 -0
- package/dist/subsystem/DiagnosticToggle.d.ts.map +1 -0
- package/dist/subsystem/DiagnosticToggle.js +95 -0
- package/dist/subsystem/DiagnosticToggle.js.map +1 -0
- package/dist/subsystem/IssueList.d.ts +102 -0
- package/dist/subsystem/IssueList.d.ts.map +1 -0
- package/dist/subsystem/IssueList.js +282 -0
- package/dist/subsystem/IssueList.js.map +1 -0
- package/dist/subsystem/SubsystemComponentGraph.d.ts +28 -0
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +80 -57
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/model.d.ts +13 -2
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js +12 -0
- package/dist/subsystem/model.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +37 -0
- package/src/stories/Subsystem/ComponentGraph/Constructs.stories.tsx +287 -0
- package/src/stories/Subsystem/ComponentGraph/Diagnostics.stories.tsx +138 -0
- package/src/stories/Subsystem/ComponentGraph/Issues.stories.tsx +130 -0
- package/src/subsystem/ComponentDeclaration.tsx +4 -1
- package/src/subsystem/ConstructsCatalog.tsx +644 -0
- package/src/subsystem/DiagnosticToggle.tsx +160 -0
- package/src/subsystem/IssueList.tsx +488 -0
- package/src/subsystem/SubsystemComponentGraph.tsx +118 -36
- package/src/subsystem/model.test.ts +34 -0
- package/src/subsystem/model.ts +18 -2
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
* inside one labeled boundary frame; nodes without one sit outside every
|
|
8
8
|
* boundary. Clicking a component invokes `onSelect`.
|
|
9
9
|
*
|
|
10
|
+
* When the model has components but no topology or walkthrough edges, the
|
|
11
|
+
* canvas is a constructs catalog (list + signature) instead of a graph.
|
|
12
|
+
*
|
|
10
13
|
* This is a focused fork of the package's `GraphRenderer` pipeline (same ELK
|
|
11
14
|
* edge routing, delayed fitView, Background/Controls/MiniMap, node/edge type
|
|
12
15
|
* injection, onNodeClick) adapted to the subsystem model — read-only.
|
|
@@ -37,6 +40,7 @@ import { IndustryMarkdownSlide } from 'themed-markdown';
|
|
|
37
40
|
import {
|
|
38
41
|
buildSubsystemGraph,
|
|
39
42
|
deriveGraphEdges,
|
|
43
|
+
isConstructsOnlyModel,
|
|
40
44
|
isRelationMechanism,
|
|
41
45
|
isWalkthroughMechanism,
|
|
42
46
|
MECHANISM_COLOR,
|
|
@@ -50,8 +54,11 @@ import {
|
|
|
50
54
|
type SubsystemRelation,
|
|
51
55
|
type SubsystemWalkthrough,
|
|
52
56
|
} from './model';
|
|
57
|
+
import { ConstructsCatalog } from './ConstructsCatalog';
|
|
53
58
|
import type { SubsystemOpenFileOptions } from './declarationRef';
|
|
54
59
|
import { SubsystemComponentNode, SubsystemGroupNode, SubsystemEdge, SUBSYSTEM_CALLBACKS, hexWithAlpha, EDGE_DIM_ALPHA, fileMatchForNode, flowElementVisibility } from './nodes';
|
|
60
|
+
import { SubsystemDiagnosticToggle, type SubsystemDiagnostic } from './DiagnosticToggle';
|
|
61
|
+
import { SubsystemIssueList, type SubsystemIssue } from './IssueList';
|
|
55
62
|
import { SubsystemFileTree } from './SubsystemFileTree';
|
|
56
63
|
import { GraphLayoutCover } from './GraphLayoutCover';
|
|
57
64
|
import { ComponentDeclaration } from './ComponentDeclaration';
|
|
@@ -160,6 +167,29 @@ export interface SubsystemComponentGraphProps {
|
|
|
160
167
|
sidebarExtra?: ReactNode;
|
|
161
168
|
/** Rendered in the sidebar under the description (e.g. selection inspector). */
|
|
162
169
|
sidebarAfterDescription?: ReactNode;
|
|
170
|
+
/**
|
|
171
|
+
* Diagnostics status chip in the sidebar title row (beside the description
|
|
172
|
+
* toggle). Shows the last verification pass's state as an icon color + count;
|
|
173
|
+
* `onOpen` opens the full report. Omit to hide the chip.
|
|
174
|
+
*/
|
|
175
|
+
diagnostic?: SubsystemDiagnostic;
|
|
176
|
+
/**
|
|
177
|
+
* Verification issues for this graph (audit findings). When diagnostics are
|
|
178
|
+
* active (see `showIssues`) the sidebar's bottom panel becomes the issue
|
|
179
|
+
* list instead of the file tree / walkthroughs.
|
|
180
|
+
*/
|
|
181
|
+
issues?: SubsystemIssue[];
|
|
182
|
+
/**
|
|
183
|
+
* Force the sidebar into diagnostics mode. Defaults to `true` whenever
|
|
184
|
+
* `issues` is non-empty (and nothing else has been forced).
|
|
185
|
+
*/
|
|
186
|
+
showIssues?: boolean;
|
|
187
|
+
/** Click an issue — focus its target on the graph / open detail. */
|
|
188
|
+
onSelectIssue?: (issue: SubsystemIssue) => void;
|
|
189
|
+
/** Apply an issue's deterministic fix. */
|
|
190
|
+
onApplyIssueFix?: (issue: SubsystemIssue) => void;
|
|
191
|
+
/** Hover an issue — transiently highlight its target (null on leave). */
|
|
192
|
+
onHoverIssue?: (issue: SubsystemIssue | null) => void;
|
|
163
193
|
/**
|
|
164
194
|
* Host-injected reader/renderer for the bottom file drawer, keyed by
|
|
165
195
|
* repo-root-relative path. Opening happens on declaration/file-tree clicks.
|
|
@@ -274,7 +304,7 @@ interface InnerProps extends SubsystemComponentGraphProps {
|
|
|
274
304
|
measured: { w: number; h: number } | null;
|
|
275
305
|
}
|
|
276
306
|
|
|
277
|
-
function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, measured: _measured, maxNodeWidth, showEdgeLabels, edgeView, title, hideSidebar, walkthroughStepMode = 'focus', autoPlayWalkthroughs = false, walkthroughAutoPlayIntervalMs = WALKTHROUGH_PLAY_PAUSE_MS, zoomOnWalkthroughFocus = true, graphTitle, showWalkthroughTitle = false, description, canvasOverlay, sidebarExtra, sidebarAfterDescription, renderFileView, renderFileViewer, renderWalkthroughViewer, onFileSelect, onVerifyComponent, componentVerification }: InnerProps) {
|
|
307
|
+
function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, measured: _measured, maxNodeWidth, showEdgeLabels, edgeView, title, hideSidebar, walkthroughStepMode = 'focus', autoPlayWalkthroughs = false, walkthroughAutoPlayIntervalMs = WALKTHROUGH_PLAY_PAUSE_MS, zoomOnWalkthroughFocus = true, graphTitle, showWalkthroughTitle = false, description, canvasOverlay, sidebarExtra, sidebarAfterDescription, diagnostic, issues, showIssues, onSelectIssue, onApplyIssueFix, onHoverIssue, renderFileView, renderFileViewer, renderWalkthroughViewer, onFileSelect, onVerifyComponent, componentVerification }: InnerProps) {
|
|
278
308
|
const { theme } = useTheme();
|
|
279
309
|
const { fitView } = useReactFlow();
|
|
280
310
|
const viewport = useViewport();
|
|
@@ -935,6 +965,10 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
935
965
|
// own owner-avatar header. Clicking a header collapses that repo's tree.
|
|
936
966
|
const repoGroups = useMemo(() => buildRepoGroups(components), [components]);
|
|
937
967
|
const hasWalkthroughs = useMemo(() => (walkthroughs?.length ?? 0) > 0, [walkthroughs]);
|
|
968
|
+
// Diagnostics mode: the bottom panel shows the issue list instead of the
|
|
969
|
+
// file tree / walkthroughs. Explicit `showIssues` wins; otherwise any issues
|
|
970
|
+
// present turn it on.
|
|
971
|
+
const issuesActive = showIssues ?? (issues?.length ?? 0) > 0;
|
|
938
972
|
const [collapsedRepos, setCollapsedRepos] = useState<Set<string>>(new Set());
|
|
939
973
|
const toggleRepoCollapsed = useCallback((key: string) => {
|
|
940
974
|
setCollapsedRepos((prev) => {
|
|
@@ -1426,7 +1460,7 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
1426
1460
|
{/* Sidebar: scrollable title/description on top, files or flows pinned below.
|
|
1427
1461
|
The description hides by default so files/flows get the room; the
|
|
1428
1462
|
title-row toggle reveals it, and the lower panel yields back to 50%. */}
|
|
1429
|
-
{!hideSidebar && (title || description || sidebarExtra || sidebarAfterDescription || treeFilePaths.length > 0 || hasWalkthroughs) && (
|
|
1463
|
+
{!hideSidebar && (title || description || diagnostic || issuesActive || sidebarExtra || sidebarAfterDescription || treeFilePaths.length > 0 || hasWalkthroughs) && (
|
|
1430
1464
|
<div
|
|
1431
1465
|
style={{
|
|
1432
1466
|
width: sidebarWidth,
|
|
@@ -1450,7 +1484,7 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
1450
1484
|
}}
|
|
1451
1485
|
>
|
|
1452
1486
|
{sidebarExtra}
|
|
1453
|
-
{(title || description) && (
|
|
1487
|
+
{(title || description || diagnostic) && (
|
|
1454
1488
|
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 8 }}>
|
|
1455
1489
|
{title && (
|
|
1456
1490
|
<h2
|
|
@@ -1467,34 +1501,47 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
1467
1501
|
{title}
|
|
1468
1502
|
</h2>
|
|
1469
1503
|
)}
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1504
|
+
<div
|
|
1505
|
+
style={{
|
|
1506
|
+
display: 'flex',
|
|
1507
|
+
alignItems: 'center',
|
|
1508
|
+
gap: 4,
|
|
1509
|
+
flexShrink: 0,
|
|
1510
|
+
// Title's flex:1 already pushes this cluster right; without a
|
|
1511
|
+
// title, keep the actions flush right instead of flush-left.
|
|
1512
|
+
marginLeft: title ? undefined : 'auto',
|
|
1513
|
+
}}
|
|
1514
|
+
>
|
|
1515
|
+
{diagnostic && <SubsystemDiagnosticToggle {...diagnostic} />}
|
|
1516
|
+
{description && (
|
|
1517
|
+
<button
|
|
1518
|
+
type="button"
|
|
1519
|
+
aria-expanded={descriptionVisible}
|
|
1520
|
+
aria-label={descriptionVisible ? 'Hide description' : 'Show description'}
|
|
1521
|
+
title={descriptionVisible ? 'Hide description' : 'Show description'}
|
|
1522
|
+
onMouseEnter={() => setDescToggleHover(true)}
|
|
1523
|
+
onMouseLeave={() => setDescToggleHover(false)}
|
|
1524
|
+
onClick={() => setDescriptionVisible((v) => !v)}
|
|
1525
|
+
style={{
|
|
1526
|
+
display: 'inline-flex',
|
|
1527
|
+
alignItems: 'center',
|
|
1528
|
+
justifyContent: 'center',
|
|
1529
|
+
flexShrink: 0,
|
|
1530
|
+
width: 22,
|
|
1531
|
+
height: 22,
|
|
1532
|
+
padding: 0,
|
|
1533
|
+
border: 'none',
|
|
1534
|
+
borderRadius: 4,
|
|
1535
|
+
background: descToggleHover ? theme.colors.border : 'transparent',
|
|
1536
|
+
color: descToggleHover ? theme.colors.text : (theme.colors.textMuted ?? theme.colors.textSecondary),
|
|
1537
|
+
cursor: 'pointer',
|
|
1538
|
+
transition: 'background 120ms ease, color 120ms ease',
|
|
1539
|
+
}}
|
|
1540
|
+
>
|
|
1541
|
+
{descriptionVisible ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
|
|
1542
|
+
</button>
|
|
1543
|
+
)}
|
|
1544
|
+
</div>
|
|
1498
1545
|
</div>
|
|
1499
1546
|
)}
|
|
1500
1547
|
{description && descriptionVisible && (
|
|
@@ -1515,7 +1562,7 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
1515
1562
|
)}
|
|
1516
1563
|
{sidebarAfterDescription}
|
|
1517
1564
|
</div>
|
|
1518
|
-
{(treeFilePaths.length > 0 || hasWalkthroughs) && (
|
|
1565
|
+
{(treeFilePaths.length > 0 || hasWalkthroughs || issuesActive) && (
|
|
1519
1566
|
<div
|
|
1520
1567
|
style={{
|
|
1521
1568
|
...(showDesc ? { height: '50%' as const } : { flex: 1, minHeight: 0 }),
|
|
@@ -1527,6 +1574,15 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
1527
1574
|
overflow: 'hidden',
|
|
1528
1575
|
}}
|
|
1529
1576
|
>
|
|
1577
|
+
{issuesActive ? (
|
|
1578
|
+
<SubsystemIssueList
|
|
1579
|
+
issues={issues ?? []}
|
|
1580
|
+
onSelectIssue={onSelectIssue}
|
|
1581
|
+
onApplyFix={onApplyIssueFix}
|
|
1582
|
+
onHoverIssue={onHoverIssue}
|
|
1583
|
+
/>
|
|
1584
|
+
) : (
|
|
1585
|
+
<>
|
|
1530
1586
|
{hasWalkthroughs && (
|
|
1531
1587
|
<div
|
|
1532
1588
|
role="tablist"
|
|
@@ -1640,13 +1696,15 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
1640
1696
|
})}
|
|
1641
1697
|
</>
|
|
1642
1698
|
) : null}
|
|
1699
|
+
</>
|
|
1700
|
+
)}
|
|
1643
1701
|
</div>
|
|
1644
1702
|
)}
|
|
1645
1703
|
</div>
|
|
1646
1704
|
)}
|
|
1647
1705
|
|
|
1648
1706
|
{/* Drag handle between sidebar and canvas — resize the left panel. */}
|
|
1649
|
-
{!hideSidebar && (title || description || sidebarExtra || sidebarAfterDescription || treeFilePaths.length > 0 || hasWalkthroughs) && (
|
|
1707
|
+
{!hideSidebar && (title || description || diagnostic || issuesActive || sidebarExtra || sidebarAfterDescription || treeFilePaths.length > 0 || hasWalkthroughs) && (
|
|
1650
1708
|
<div
|
|
1651
1709
|
onMouseDown={onSidebarResizeStart}
|
|
1652
1710
|
aria-label="Resize sidebar"
|
|
@@ -2435,6 +2493,12 @@ export function SubsystemComponentGraph(props: SubsystemComponentGraphProps) {
|
|
|
2435
2493
|
return () => ro.disconnect();
|
|
2436
2494
|
}, []);
|
|
2437
2495
|
|
|
2496
|
+
const constructsOnly = isConstructsOnlyModel({
|
|
2497
|
+
components: props.components,
|
|
2498
|
+
relations: props.relations,
|
|
2499
|
+
walkthroughs: props.walkthroughs,
|
|
2500
|
+
});
|
|
2501
|
+
|
|
2438
2502
|
return (
|
|
2439
2503
|
<div
|
|
2440
2504
|
ref={wrapRef}
|
|
@@ -2448,9 +2512,27 @@ export function SubsystemComponentGraph(props: SubsystemComponentGraphProps) {
|
|
|
2448
2512
|
}}
|
|
2449
2513
|
>
|
|
2450
2514
|
<div style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
|
|
2451
|
-
|
|
2452
|
-
<
|
|
2453
|
-
|
|
2515
|
+
{constructsOnly ? (
|
|
2516
|
+
<ConstructsCatalog
|
|
2517
|
+
components={props.components}
|
|
2518
|
+
onSelect={props.onSelect}
|
|
2519
|
+
title={props.title}
|
|
2520
|
+
hideSidebar={props.hideSidebar}
|
|
2521
|
+
description={props.description}
|
|
2522
|
+
diagnostic={props.diagnostic}
|
|
2523
|
+
sidebarExtra={props.sidebarExtra}
|
|
2524
|
+
sidebarAfterDescription={props.sidebarAfterDescription}
|
|
2525
|
+
renderFileViewer={props.renderFileViewer}
|
|
2526
|
+
renderFileView={props.renderFileView}
|
|
2527
|
+
onFileSelect={props.onFileSelect}
|
|
2528
|
+
onVerifyComponent={props.onVerifyComponent}
|
|
2529
|
+
componentVerification={props.componentVerification}
|
|
2530
|
+
/>
|
|
2531
|
+
) : (
|
|
2532
|
+
<ReactFlowProvider>
|
|
2533
|
+
<Inner {...props} measured={size} />
|
|
2534
|
+
</ReactFlowProvider>
|
|
2535
|
+
)}
|
|
2454
2536
|
</div>
|
|
2455
2537
|
</div>
|
|
2456
2538
|
);
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
buildSubsystemGraph,
|
|
14
14
|
componentPackageKey,
|
|
15
15
|
deriveNameFromSymbol,
|
|
16
|
+
isConstructsOnlyModel,
|
|
16
17
|
constructBadgeLabel,
|
|
17
18
|
constructBadgeColor,
|
|
18
19
|
FRAMEWORK_BADGE_COLOR,
|
|
@@ -642,3 +643,36 @@ describe('subsystem graph model', () => {
|
|
|
642
643
|
expect(subsystemGraphLayoutKey(base)).toBe(subsystemGraphLayoutKey(withRef));
|
|
643
644
|
});
|
|
644
645
|
});
|
|
646
|
+
|
|
647
|
+
describe('isConstructsOnlyModel', () => {
|
|
648
|
+
test('true when components exist and there are no edges', () => {
|
|
649
|
+
expect(isConstructsOnlyModel({ components: comps, relations: [], walkthroughs: [] })).toBe(true);
|
|
650
|
+
expect(isConstructsOnlyModel({ components: comps })).toBe(true);
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
test('false when empty, or when relations or walkthrough hops exist', () => {
|
|
654
|
+
expect(isConstructsOnlyModel({ components: [], relations: [] })).toBe(false);
|
|
655
|
+
expect(isConstructsOnlyModel({ components: comps, relations })).toBe(false);
|
|
656
|
+
expect(
|
|
657
|
+
isConstructsOnlyModel({
|
|
658
|
+
components: comps,
|
|
659
|
+
relations: [],
|
|
660
|
+
walkthroughs: [
|
|
661
|
+
{
|
|
662
|
+
id: 'wt',
|
|
663
|
+
title: 'flow',
|
|
664
|
+
steps: [
|
|
665
|
+
{
|
|
666
|
+
from: 'transcript',
|
|
667
|
+
to: 'reader',
|
|
668
|
+
mechanism: 'calls',
|
|
669
|
+
file: 'transcript.ts',
|
|
670
|
+
line: 1,
|
|
671
|
+
},
|
|
672
|
+
],
|
|
673
|
+
},
|
|
674
|
+
],
|
|
675
|
+
}),
|
|
676
|
+
).toBe(false);
|
|
677
|
+
});
|
|
678
|
+
});
|
package/src/subsystem/model.ts
CHANGED
|
@@ -331,8 +331,8 @@ export function walkthroughStepGraphEdgeId(
|
|
|
331
331
|
}
|
|
332
332
|
|
|
333
333
|
export function deriveGraphEdges(doc: {
|
|
334
|
-
relations?: SubsystemRelation[];
|
|
335
|
-
walkthroughs?: SubsystemWalkthrough[];
|
|
334
|
+
relations?: readonly SubsystemRelation[];
|
|
335
|
+
walkthroughs?: readonly SubsystemWalkthrough[];
|
|
336
336
|
}): SubsystemComponentEdge[] {
|
|
337
337
|
const byId = new Map<string, SubsystemComponentEdge>();
|
|
338
338
|
for (const r of doc.relations ?? []) {
|
|
@@ -363,6 +363,22 @@ export function deriveGraphEdges(doc: {
|
|
|
363
363
|
return [...byId.values()];
|
|
364
364
|
}
|
|
365
365
|
|
|
366
|
+
/**
|
|
367
|
+
* True when the model has components but no topology or walkthrough edges.
|
|
368
|
+
* Those snapshots are a catalog of declarations, not a graph.
|
|
369
|
+
*/
|
|
370
|
+
export function isConstructsOnlyModel(doc: {
|
|
371
|
+
components: readonly { id: string }[];
|
|
372
|
+
relations?: readonly SubsystemRelation[];
|
|
373
|
+
walkthroughs?: readonly SubsystemWalkthrough[];
|
|
374
|
+
}): boolean {
|
|
375
|
+
if (doc.components.length === 0) return false;
|
|
376
|
+
return deriveGraphEdges({
|
|
377
|
+
relations: doc.relations,
|
|
378
|
+
walkthroughs: doc.walkthroughs,
|
|
379
|
+
}).length === 0;
|
|
380
|
+
}
|
|
381
|
+
|
|
366
382
|
/**
|
|
367
383
|
* Derive a consistent display `name` from a code `symbol` + construct.
|
|
368
384
|
*
|