@principal-ai/subsystems-react 0.25.1 → 0.26.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 +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/stories/Subsystem/ComponentGraph/aggregateViewFixture.d.ts +5 -0
- package/dist/stories/Subsystem/ComponentGraph/aggregateViewFixture.d.ts.map +1 -0
- package/dist/stories/Subsystem/ComponentGraph/aggregateViewFixture.js +4 -0
- package/dist/stories/Subsystem/ComponentGraph/aggregateViewFixture.js.map +1 -0
- package/dist/subsystem/SubsystemAggregateGraph.d.ts +70 -0
- package/dist/subsystem/SubsystemAggregateGraph.d.ts.map +1 -0
- package/dist/subsystem/SubsystemAggregateGraph.js +386 -0
- package/dist/subsystem/SubsystemAggregateGraph.js.map +1 -0
- package/dist/subsystem/SubsystemComponentGraph.d.ts +8 -0
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +25 -4
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/graphChrome.d.ts +27 -0
- package/dist/subsystem/graphChrome.d.ts.map +1 -0
- package/dist/subsystem/graphChrome.js +30 -0
- package/dist/subsystem/graphChrome.js.map +1 -0
- package/dist/subsystem/model.d.ts +7 -0
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js +35 -0
- package/dist/subsystem/model.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +8 -0
- package/src/stories/Subsystem/ComponentGraph/AggregateFrameGraph.stories.tsx +61 -0
- package/src/stories/Subsystem/ComponentGraph/aggregateViewFixture.ts +8 -0
- package/src/subsystem/SubsystemAggregateGraph.tsx +610 -0
- package/src/subsystem/SubsystemComponentGraph.tsx +29 -18
- package/src/subsystem/graphChrome.tsx +41 -0
- package/src/subsystem/model.test.ts +17 -0
- package/src/subsystem/model.ts +34 -0
|
@@ -20,9 +20,6 @@ import type { MouseEvent as ReactMouseEvent, ReactNode } from 'react';
|
|
|
20
20
|
import {
|
|
21
21
|
ReactFlow,
|
|
22
22
|
ReactFlowProvider,
|
|
23
|
-
Background,
|
|
24
|
-
BackgroundVariant,
|
|
25
|
-
Controls,
|
|
26
23
|
useReactFlow,
|
|
27
24
|
useViewport,
|
|
28
25
|
type NodeTypes,
|
|
@@ -61,6 +58,7 @@ import { SubsystemDiagnosticToggle, type SubsystemDiagnostic } from './Diagnosti
|
|
|
61
58
|
import { SubsystemIssueList, type SubsystemIssue } from './IssueList';
|
|
62
59
|
import { SubsystemFileTree } from './SubsystemFileTree';
|
|
63
60
|
import { GraphLayoutCover } from './GraphLayoutCover';
|
|
61
|
+
import { GRAPH_NAV_PROPS, GraphChrome } from './graphChrome';
|
|
64
62
|
import { ComponentDeclaration } from './ComponentDeclaration';
|
|
65
63
|
import type { ComponentVerificationState } from './ComponentDeclaration';
|
|
66
64
|
import { FileDrawer, FILE_DRAWER_HEIGHT_MS } from './FileDrawer';
|
|
@@ -101,6 +99,14 @@ export interface SubsystemComponentGraphProps {
|
|
|
101
99
|
* (unselected ones dimmed); everything else is hidden.
|
|
102
100
|
*/
|
|
103
101
|
walkthroughs?: SubsystemWalkthrough[];
|
|
102
|
+
/**
|
|
103
|
+
* Deep-link target: when set, the matching walkthrough is selected on mount
|
|
104
|
+
* — its steps expanded and its flow focused on the canvas. Hosts use this
|
|
105
|
+
* when opening the graph from a walkthrough row in a list. Re-applies on a
|
|
106
|
+
* new id (or a remount); in-tab selection afterwards stays owned by the
|
|
107
|
+
* graph.
|
|
108
|
+
*/
|
|
109
|
+
initialWalkthroughId?: string | null;
|
|
104
110
|
onSelect?: (componentAlias: string) => void;
|
|
105
111
|
/** Called when an edge is clicked (relationship / mechanism + refs seam). */
|
|
106
112
|
onEdgeSelect?: (edge: SubsystemComponentEdge) => void;
|
|
@@ -307,7 +313,7 @@ interface InnerProps extends SubsystemComponentGraphProps {
|
|
|
307
313
|
measured: { w: number; h: number } | null;
|
|
308
314
|
}
|
|
309
315
|
|
|
310
|
-
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) {
|
|
316
|
+
function Inner({ components, relations, walkthroughs, initialWalkthroughId, 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) {
|
|
311
317
|
const { theme } = useTheme();
|
|
312
318
|
const { fitView } = useReactFlow();
|
|
313
319
|
const viewport = useViewport();
|
|
@@ -1138,6 +1144,23 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
1138
1144
|
setDrawerTarget((prev) => (prev?.kind === 'walkthrough' ? null : prev));
|
|
1139
1145
|
}, []);
|
|
1140
1146
|
|
|
1147
|
+
// Host deep-link: opening the graph from a walkthrough row selects that
|
|
1148
|
+
// flow — expand its steps, focus its edges, and switch the sidebar to
|
|
1149
|
+
// Walkthroughs. Guarded by a ref so a later in-tab selection isn't yanked
|
|
1150
|
+
// back; a remount (or a new id) re-applies it.
|
|
1151
|
+
const appliedInitialWalkthroughRef = useRef<string | null>(null);
|
|
1152
|
+
useEffect(() => {
|
|
1153
|
+
if (!initialWalkthroughId) return;
|
|
1154
|
+
if (appliedInitialWalkthroughRef.current === initialWalkthroughId) return;
|
|
1155
|
+
if (!layoutReady || !walkthroughs?.length) return;
|
|
1156
|
+
const tl = walkthroughs.find((t) => t.id === initialWalkthroughId);
|
|
1157
|
+
if (!tl) return;
|
|
1158
|
+
appliedInitialWalkthroughRef.current = initialWalkthroughId;
|
|
1159
|
+
setSidebarView('walkthroughs');
|
|
1160
|
+
setExpandedWalkthroughs((prev) => new Set(prev).add(tl.id));
|
|
1161
|
+
focusWalkthroughEdges(tl);
|
|
1162
|
+
}, [initialWalkthroughId, layoutReady, walkthroughs, focusWalkthroughEdges]);
|
|
1163
|
+
|
|
1141
1164
|
// Switching sidebar panels also switches the edge vocabulary. Leaving the
|
|
1142
1165
|
// Walkthroughs panel drops its canvas state (focus, expanded flows, selected
|
|
1143
1166
|
// edge) so the Files view's topology edges aren't gated by a flow the user
|
|
@@ -1847,24 +1870,13 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
1847
1870
|
edges={dispEdges}
|
|
1848
1871
|
nodeTypes={nodeTypes}
|
|
1849
1872
|
edgeTypes={edgeTypes}
|
|
1850
|
-
|
|
1851
|
-
maxZoom={4}
|
|
1873
|
+
{...GRAPH_NAV_PROPS}
|
|
1852
1874
|
onNodeClick={onNodeClick}
|
|
1853
1875
|
onEdgeClick={onEdgeClick}
|
|
1854
1876
|
onNodesChange={onNodesChange}
|
|
1855
1877
|
onEdgesChange={onEdgesChange}
|
|
1856
1878
|
proOptions={{ hideAttribution: true }}
|
|
1857
|
-
nodesDraggable={false}
|
|
1858
|
-
elementsSelectable
|
|
1859
|
-
selectNodesOnDrag={false}
|
|
1860
|
-
nodesConnectable={false}
|
|
1861
|
-
edgesReconnectable={false}
|
|
1862
1879
|
onPaneClick={onPaneClick}
|
|
1863
|
-
panOnDrag
|
|
1864
|
-
panOnScroll
|
|
1865
|
-
zoomOnScroll={false}
|
|
1866
|
-
zoomOnPinch
|
|
1867
|
-
zoomOnDoubleClick={false}
|
|
1868
1880
|
style={{
|
|
1869
1881
|
width: '100%',
|
|
1870
1882
|
height: '100%',
|
|
@@ -1873,8 +1885,7 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
1873
1885
|
background: theme.colors.background,
|
|
1874
1886
|
}}
|
|
1875
1887
|
>
|
|
1876
|
-
<
|
|
1877
|
-
<Controls showZoom showFitView showInteractive />
|
|
1888
|
+
<GraphChrome />
|
|
1878
1889
|
</ReactFlow>
|
|
1879
1890
|
{/* Graph / walkthrough / step titles — non-interactive chips at the top
|
|
1880
1891
|
of the canvas. Graph-only embeds use these without opening the sidebar. */}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared navigation surface for read-only graph canvases.
|
|
3
|
+
*
|
|
4
|
+
* Every graph in this package (component graph, aggregate graph) is a React
|
|
5
|
+
* Flow wrapper and must present the same interaction language: drag/scroll to
|
|
6
|
+
* pan, pinch to zoom (never scroll-zoom or double-click-zoom), and the
|
|
7
|
+
* Background + Controls chrome. Spreading `GRAPH_NAV_PROPS` and rendering
|
|
8
|
+
* `<GraphChrome />` keeps them from drifting apart.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
Background,
|
|
13
|
+
BackgroundVariant,
|
|
14
|
+
Controls,
|
|
15
|
+
type ReactFlowProps,
|
|
16
|
+
} from '@xyflow/react';
|
|
17
|
+
|
|
18
|
+
export const GRAPH_NAV_PROPS = {
|
|
19
|
+
minZoom: 0.05,
|
|
20
|
+
maxZoom: 4,
|
|
21
|
+
panOnDrag: true,
|
|
22
|
+
panOnScroll: true,
|
|
23
|
+
zoomOnScroll: false,
|
|
24
|
+
zoomOnPinch: true,
|
|
25
|
+
zoomOnDoubleClick: false,
|
|
26
|
+
nodesDraggable: false,
|
|
27
|
+
elementsSelectable: true,
|
|
28
|
+
selectNodesOnDrag: false,
|
|
29
|
+
nodesConnectable: false,
|
|
30
|
+
edgesReconnectable: false,
|
|
31
|
+
} satisfies Partial<ReactFlowProps>;
|
|
32
|
+
|
|
33
|
+
/** Background dots + zoom/fit/interactive widget; render inside `<ReactFlow>`. */
|
|
34
|
+
export function GraphChrome() {
|
|
35
|
+
return (
|
|
36
|
+
<>
|
|
37
|
+
<Background variant={BackgroundVariant.Dots} gap={16} size={1} />
|
|
38
|
+
<Controls showZoom showFitView showInteractive />
|
|
39
|
+
</>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
formatPurl,
|
|
29
29
|
packageColor,
|
|
30
30
|
subsystemGraphLayoutKey,
|
|
31
|
+
describeConstructBreakdown,
|
|
31
32
|
} from './model';
|
|
32
33
|
import type { SubsystemComponent, SubsystemComponentEdge } from './model';
|
|
33
34
|
|
|
@@ -727,6 +728,22 @@ describe('isConstructsOnlyModel', () => {
|
|
|
727
728
|
});
|
|
728
729
|
});
|
|
729
730
|
|
|
731
|
+
describe('describeConstructBreakdown', () => {
|
|
732
|
+
test('counts by construct, most common first', () => {
|
|
733
|
+
expect(describeConstructBreakdown(['function', 'class', 'function'])).toBe(
|
|
734
|
+
'2 functions · 1 class',
|
|
735
|
+
);
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
test('ignores blank constructs and handles empties', () => {
|
|
739
|
+
expect(describeConstructBreakdown(['', ' ', 'store'])).toBe('1 store');
|
|
740
|
+
expect(describeConstructBreakdown([])).toBe('empty');
|
|
741
|
+
expect(describeConstructBreakdown(['interface', 'type_alias', 'enum', 'custom_entity'])).toBe(
|
|
742
|
+
'1 custom_entity · 1 enum · 1 interface · +1 more',
|
|
743
|
+
);
|
|
744
|
+
});
|
|
745
|
+
});
|
|
746
|
+
|
|
730
747
|
describe('module badge labels', () => {
|
|
731
748
|
test('moduleBadgeLabel keeps the full path when it fits the module width', () => {
|
|
732
749
|
expect(moduleBadgeLabel('src/main.ts', 1000)).toBe('src/main.ts');
|
package/src/subsystem/model.ts
CHANGED
|
@@ -435,6 +435,40 @@ export function deriveNameFromSymbol(
|
|
|
435
435
|
return name;
|
|
436
436
|
}
|
|
437
437
|
|
|
438
|
+
const CONSTRUCT_PLURALS: Record<string, string> = {
|
|
439
|
+
class: 'classes',
|
|
440
|
+
function: 'functions',
|
|
441
|
+
method: 'methods',
|
|
442
|
+
interface: 'interfaces',
|
|
443
|
+
type_alias: 'type aliases',
|
|
444
|
+
enum: 'enums',
|
|
445
|
+
store: 'stores',
|
|
446
|
+
external: 'externals',
|
|
447
|
+
custom_entity: 'custom entities',
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* One-line summary of what a group of declarations contains, e.g.
|
|
452
|
+
* `6 functions · 2 classes`. Used for aggregate frame boxes, which describe
|
|
453
|
+
* their contents (constructs) rather than their container. Empty/blank
|
|
454
|
+
* constructs are ignored. At most three groups; the rest folds into `+N more`.
|
|
455
|
+
*/
|
|
456
|
+
export function describeConstructBreakdown(constructs: readonly string[]): string {
|
|
457
|
+
const counts = new Map<string, number>();
|
|
458
|
+
for (const raw of constructs) {
|
|
459
|
+
const c = (raw ?? '').trim();
|
|
460
|
+
if (!c) continue;
|
|
461
|
+
counts.set(c, (counts.get(c) ?? 0) + 1);
|
|
462
|
+
}
|
|
463
|
+
const ranked = [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
|
|
464
|
+
const parts = ranked.slice(0, 3).map(([c, n]) => {
|
|
465
|
+
if (n === 1) return `1 ${c}`;
|
|
466
|
+
return `${n} ${CONSTRUCT_PLURALS[c] ?? `${c}s`}`;
|
|
467
|
+
});
|
|
468
|
+
if (ranked.length > 3) parts.push(`+${ranked.length - 3} more`);
|
|
469
|
+
return parts.join(' · ') || 'empty';
|
|
470
|
+
}
|
|
471
|
+
|
|
438
472
|
/**
|
|
439
473
|
* Human-readable purl identity — drops the `pkg:<type>/` scheme wrapper and
|
|
440
474
|
* trailing version, keeping the package / owner-repo identity:
|