@principal-ai/subsystems-react 0.24.26 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts +1 -1
- package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts.map +1 -1
- package/dist/stories/Subsystem/ComponentGraph/fixtures.js +7 -7
- package/dist/stories/Subsystem/ComponentGraph/fixtures.js.map +1 -1
- package/dist/subsystem/ComponentDeclaration.d.ts +1 -1
- package/dist/subsystem/ComponentDeclaration.d.ts.map +1 -1
- package/dist/subsystem/ComponentDeclaration.js +1 -1
- package/dist/subsystem/ComponentDeclaration.js.map +1 -1
- package/dist/subsystem/ConstructsCatalog.d.ts +2 -2
- package/dist/subsystem/ConstructsCatalog.d.ts.map +1 -1
- package/dist/subsystem/ConstructsCatalog.js +62 -62
- package/dist/subsystem/ConstructsCatalog.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts +2 -2
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +27 -27
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/model.d.ts +21 -5
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js +72 -38
- package/dist/subsystem/model.js.map +1 -1
- package/dist/subsystem/nodes.d.ts +3 -3
- package/dist/subsystem/nodes.d.ts.map +1 -1
- package/dist/subsystem/nodes.js +39 -11
- package/dist/subsystem/nodes.js.map +1 -1
- package/dist/subsystem/paths.d.ts +1 -1
- package/dist/subsystem/paths.js +1 -1
- package/package.json +1 -1
- package/src/stories/Subsystem/ComponentGraph/ModuleBadges.stories.tsx +183 -0
- package/src/stories/Subsystem/ComponentGraph/fixtures.ts +8 -8
- package/src/subsystem/ComponentDeclaration.tsx +2 -2
- package/src/subsystem/ConstructsCatalog.tsx +71 -75
- package/src/subsystem/SubsystemComponentGraph.tsx +28 -28
- package/src/subsystem/model.test.ts +103 -51
- package/src/subsystem/model.ts +83 -43
- package/src/subsystem/nodes.tsx +57 -18
- package/src/subsystem/paths.ts +1 -1
package/src/subsystem/model.ts
CHANGED
|
@@ -127,7 +127,13 @@ export type SubsystemEdgeView = 'relations' | 'walkthroughs';
|
|
|
127
127
|
|
|
128
128
|
/** A component node — the named unit, construct-tagged; `file` is its location. */
|
|
129
129
|
export interface SubsystemComponent {
|
|
130
|
-
|
|
130
|
+
/**
|
|
131
|
+
* Model-local stable alias, unique per model. Referenced by relation /
|
|
132
|
+
* walkthrough `from` / `to`; edges point at the alias, not the location.
|
|
133
|
+
* Code identity (for composed multi-model views) lives on
|
|
134
|
+
* `purl` + `file` + `symbol`, not here.
|
|
135
|
+
*/
|
|
136
|
+
alias: string;
|
|
131
137
|
name: string;
|
|
132
138
|
/**
|
|
133
139
|
* The node's construct — what it IS as a declaration (class, function,
|
|
@@ -268,8 +274,8 @@ export interface SubsystemRelation {
|
|
|
268
274
|
*/
|
|
269
275
|
export interface SubsystemComponentEdge {
|
|
270
276
|
id: string;
|
|
271
|
-
from: string; // component
|
|
272
|
-
to: string; // component
|
|
277
|
+
from: string; // component alias
|
|
278
|
+
to: string; // component alias or external target label
|
|
273
279
|
mechanism: SubsystemEdgeMechanism;
|
|
274
280
|
/** Concrete file/symbol refs backing the edge (the seam). */
|
|
275
281
|
refs?: string[];
|
|
@@ -374,7 +380,7 @@ export function deriveGraphEdges(doc: {
|
|
|
374
380
|
* Those snapshots are a catalog of declarations, not a graph.
|
|
375
381
|
*/
|
|
376
382
|
export function isConstructsOnlyModel(doc: {
|
|
377
|
-
components: readonly {
|
|
383
|
+
components: readonly { alias: string }[];
|
|
378
384
|
relations?: readonly SubsystemRelation[];
|
|
379
385
|
walkthroughs?: readonly SubsystemWalkthrough[];
|
|
380
386
|
}): boolean {
|
|
@@ -473,7 +479,7 @@ export interface SubsystemProcessRegion {
|
|
|
473
479
|
/** Display label for the boundary frame. */
|
|
474
480
|
label: string;
|
|
475
481
|
/** Component ids that are members of this region. */
|
|
476
|
-
|
|
482
|
+
memberAliases: string[];
|
|
477
483
|
}
|
|
478
484
|
|
|
479
485
|
/** Options for which boundary frames are kept. */
|
|
@@ -516,10 +522,44 @@ export function moduleBadgeLabel(path: string, availableWidth = 0): string {
|
|
|
516
522
|
return `${parts[0]}/…/${parts[parts.length - 1]}`;
|
|
517
523
|
}
|
|
518
524
|
|
|
525
|
+
/**
|
|
526
|
+
* Hover form of the badge: the collapsed `first/…/last` plus as many trailing
|
|
527
|
+
* path segments as fit in `availableWidth`. Greedily pulls segments off the
|
|
528
|
+
* tail so hovering reveals the path without ever distorting the glyphs or
|
|
529
|
+
* spilling past the frame's inner width. Falls back to the full path (no
|
|
530
|
+
* collapse) if everything fits.
|
|
531
|
+
*/
|
|
532
|
+
export function moduleBadgeHoverLabel(path: string, availableWidth = 0): string {
|
|
533
|
+
if (path.length * MODULE_BADGE_CHAR_WIDTH + MODULE_BADGE_CHROME <= availableWidth) {
|
|
534
|
+
return path;
|
|
535
|
+
}
|
|
536
|
+
const parts = path.split(/[\\/]/).filter((part) => part !== '' && part !== '.');
|
|
537
|
+
if (parts.length <= 2) return path;
|
|
538
|
+
let tailStart = parts.length - 1;
|
|
539
|
+
while (tailStart > 1) {
|
|
540
|
+
const candidate = `${parts[0]}/…/${parts.slice(tailStart - 1).join('/')}`;
|
|
541
|
+
if (candidate.length * MODULE_BADGE_CHAR_WIDTH + MODULE_BADGE_CHROME <= availableWidth) {
|
|
542
|
+
tailStart -= 1;
|
|
543
|
+
} else {
|
|
544
|
+
break;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return `${parts[0]}/…/${parts.slice(tailStart).join('/')}`;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/** Estimated render width (px) of the badge for `path` in the badge font. */
|
|
551
|
+
export function moduleBadgeWidth(path: string): number {
|
|
552
|
+
return Math.ceil(path.length * MODULE_BADGE_CHAR_WIDTH + MODULE_BADGE_CHROME);
|
|
553
|
+
}
|
|
554
|
+
|
|
519
555
|
export function moduleMinWidthForBadge(path: string): number {
|
|
556
|
+
const collapsed = moduleBadgeLabel(path);
|
|
557
|
+
// `…` renders ~1.4× a regular char in the bold badge font — account for it
|
|
558
|
+
// so a path that just crosses the fit threshold doesn't collapse in vain.
|
|
559
|
+
const expanded = path[0] === '…' ? 0 : 1;
|
|
560
|
+
const glyphWidth = collapsed.length * MODULE_BADGE_CHAR_WIDTH + 4 * expanded;
|
|
520
561
|
return Math.ceil(
|
|
521
|
-
|
|
522
|
-
MODULE_BADGE_CHROME + MODULE_BADGE_INSET * 2 + MODULE_FRAME_BORDER,
|
|
562
|
+
glyphWidth + MODULE_BADGE_CHROME + MODULE_BADGE_INSET * 2 + MODULE_FRAME_BORDER,
|
|
523
563
|
);
|
|
524
564
|
}
|
|
525
565
|
|
|
@@ -564,14 +604,14 @@ export function getSubsystemRegions(
|
|
|
564
604
|
const p = c.process?.trim();
|
|
565
605
|
if (!p) continue;
|
|
566
606
|
const list = byProcess.get(p) ?? [];
|
|
567
|
-
list.push(c.
|
|
607
|
+
list.push(c.alias);
|
|
568
608
|
byProcess.set(p, list);
|
|
569
609
|
}
|
|
570
|
-
return [...byProcess.entries()].map(([key,
|
|
610
|
+
return [...byProcess.entries()].map(([key, memberAliases]) => ({
|
|
571
611
|
kind: 'process' as const,
|
|
572
612
|
key,
|
|
573
613
|
label: key,
|
|
574
|
-
|
|
614
|
+
memberAliases,
|
|
575
615
|
}));
|
|
576
616
|
}
|
|
577
617
|
|
|
@@ -587,14 +627,14 @@ export function getSubsystemModuleRegions(
|
|
|
587
627
|
const m = c.module?.trim();
|
|
588
628
|
if (!m) continue;
|
|
589
629
|
const list = byModule.get(m) ?? [];
|
|
590
|
-
list.push(c.
|
|
630
|
+
list.push(c.alias);
|
|
591
631
|
byModule.set(m, list);
|
|
592
632
|
}
|
|
593
|
-
return [...byModule.entries()].map(([key,
|
|
633
|
+
return [...byModule.entries()].map(([key, memberAliases]) => ({
|
|
594
634
|
kind: 'module' as const,
|
|
595
635
|
key,
|
|
596
636
|
label: key,
|
|
597
|
-
|
|
637
|
+
memberAliases,
|
|
598
638
|
}));
|
|
599
639
|
}
|
|
600
640
|
|
|
@@ -611,14 +651,14 @@ export function getSubsystemPackageRegions(
|
|
|
611
651
|
const key = componentPackageKey(c);
|
|
612
652
|
if (!key) continue;
|
|
613
653
|
const list = byPackage.get(key) ?? [];
|
|
614
|
-
list.push(c.
|
|
654
|
+
list.push(c.alias);
|
|
615
655
|
byPackage.set(key, list);
|
|
616
656
|
}
|
|
617
|
-
return [...byPackage.entries()].map(([key,
|
|
657
|
+
return [...byPackage.entries()].map(([key, memberAliases]) => ({
|
|
618
658
|
kind: 'package' as const,
|
|
619
659
|
key,
|
|
620
660
|
label: packageRegionLabel(key),
|
|
621
|
-
|
|
661
|
+
memberAliases,
|
|
622
662
|
}));
|
|
623
663
|
}
|
|
624
664
|
|
|
@@ -628,7 +668,7 @@ export function getSubsystemPackageRegions(
|
|
|
628
668
|
*/
|
|
629
669
|
export interface BoundaryLayoutGroup {
|
|
630
670
|
id: string;
|
|
631
|
-
|
|
671
|
+
memberAliases: string[];
|
|
632
672
|
/** When set, this group is a child of another boundary group. */
|
|
633
673
|
parentId?: string;
|
|
634
674
|
region: SubsystemProcessRegion;
|
|
@@ -638,7 +678,7 @@ function keepRegion(
|
|
|
638
678
|
r: SubsystemProcessRegion,
|
|
639
679
|
showSingletons: boolean,
|
|
640
680
|
): boolean {
|
|
641
|
-
return showSingletons || r.
|
|
681
|
+
return showSingletons || r.memberAliases.length >= 2;
|
|
642
682
|
}
|
|
643
683
|
|
|
644
684
|
/**
|
|
@@ -655,7 +695,7 @@ export function buildBoundaryLayoutGroups(
|
|
|
655
695
|
): BoundaryLayoutGroup[] {
|
|
656
696
|
const showSingletons = opts.showSingletonFrames === true;
|
|
657
697
|
const packageMode = opts.packageFrames ?? 'multi-repo';
|
|
658
|
-
const
|
|
698
|
+
const byAlias = new Map(doc.components.map((c) => [c.alias, c]));
|
|
659
699
|
|
|
660
700
|
const allPackages = getSubsystemPackageRegions(doc);
|
|
661
701
|
const packageEligible =
|
|
@@ -680,8 +720,8 @@ export function buildBoundaryLayoutGroups(
|
|
|
680
720
|
const moduleGroups: BoundaryLayoutGroup[] = modules.map((r) => {
|
|
681
721
|
const processesOfMembers = new Set<string>();
|
|
682
722
|
const packagesOfMembers = new Set<string>();
|
|
683
|
-
for (const
|
|
684
|
-
const c =
|
|
723
|
+
for (const alias of r.memberAliases) {
|
|
724
|
+
const c = byAlias.get(alias);
|
|
685
725
|
const p = c?.process?.trim();
|
|
686
726
|
if (p) processesOfMembers.add(p);
|
|
687
727
|
const pkg = c ? componentPackageKey(c) : undefined;
|
|
@@ -698,7 +738,7 @@ export function buildBoundaryLayoutGroups(
|
|
|
698
738
|
}
|
|
699
739
|
return {
|
|
700
740
|
id: moduleGroupNodeId(r.key),
|
|
701
|
-
|
|
741
|
+
memberAliases: [...r.memberAliases],
|
|
702
742
|
parentId,
|
|
703
743
|
region: r,
|
|
704
744
|
};
|
|
@@ -708,15 +748,15 @@ export function buildBoundaryLayoutGroups(
|
|
|
708
748
|
const processId = processGroupNodeId(r.key);
|
|
709
749
|
const nestedModules = moduleGroups.filter((m) => m.parentId === processId);
|
|
710
750
|
const nestedModuleKeys = new Set(nestedModules.map((m) => m.region.key));
|
|
711
|
-
const directLeaves = r.
|
|
712
|
-
const mod =
|
|
751
|
+
const directLeaves = r.memberAliases.filter((alias) => {
|
|
752
|
+
const mod = byAlias.get(alias)?.module?.trim();
|
|
713
753
|
if (!mod) return true;
|
|
714
754
|
return !nestedModuleKeys.has(mod);
|
|
715
755
|
});
|
|
716
756
|
|
|
717
757
|
const packagesOfMembers = new Set<string>();
|
|
718
|
-
for (const
|
|
719
|
-
const c =
|
|
758
|
+
for (const alias of r.memberAliases) {
|
|
759
|
+
const c = byAlias.get(alias);
|
|
720
760
|
const pkg = c ? componentPackageKey(c) : undefined;
|
|
721
761
|
if (pkg) packagesOfMembers.add(pkg);
|
|
722
762
|
}
|
|
@@ -728,7 +768,7 @@ export function buildBoundaryLayoutGroups(
|
|
|
728
768
|
|
|
729
769
|
return {
|
|
730
770
|
id: processId,
|
|
731
|
-
|
|
771
|
+
memberAliases: [...nestedModules.map((m) => m.id), ...directLeaves],
|
|
732
772
|
parentId,
|
|
733
773
|
region: r,
|
|
734
774
|
};
|
|
@@ -740,15 +780,15 @@ export function buildBoundaryLayoutGroups(
|
|
|
740
780
|
const nestedModules = moduleGroups.filter((m) => m.parentId === packageId);
|
|
741
781
|
const claimed = new Set<string>();
|
|
742
782
|
for (const p of nestedProcesses) {
|
|
743
|
-
for (const
|
|
783
|
+
for (const alias of p.region.memberAliases) claimed.add(alias);
|
|
744
784
|
}
|
|
745
785
|
for (const m of nestedModules) {
|
|
746
|
-
for (const
|
|
786
|
+
for (const alias of m.region.memberAliases) claimed.add(alias);
|
|
747
787
|
}
|
|
748
|
-
const directLeaves = r.
|
|
788
|
+
const directLeaves = r.memberAliases.filter((alias) => !claimed.has(alias));
|
|
749
789
|
return {
|
|
750
790
|
id: packageId,
|
|
751
|
-
|
|
791
|
+
memberAliases: [
|
|
752
792
|
...nestedProcesses.map((p) => p.id),
|
|
753
793
|
...nestedModules.map((m) => m.id),
|
|
754
794
|
...directLeaves,
|
|
@@ -1175,7 +1215,7 @@ export function convertSubsystemToNodes(
|
|
|
1175
1215
|
? processGroupNodeId(processKey)
|
|
1176
1216
|
: undefined;
|
|
1177
1217
|
nodes.push({
|
|
1178
|
-
id: c.
|
|
1218
|
+
id: c.alias,
|
|
1179
1219
|
type: 'subsystem-component',
|
|
1180
1220
|
...(parentId ? { parentId } : {}),
|
|
1181
1221
|
position: { x: PAD + col * COL_W, y: cursorY + row * ROW_H },
|
|
@@ -1211,18 +1251,18 @@ export function convertSubsystemToGroups(
|
|
|
1211
1251
|
|
|
1212
1252
|
/**
|
|
1213
1253
|
* Convert a subsystem graph document into React Flow edges. Edges whose target
|
|
1214
|
-
* is an external label (not a component
|
|
1254
|
+
* is an external label (not a component alias) point at a synthetic stub so the
|
|
1215
1255
|
* relationship is visible without a member node.
|
|
1216
1256
|
*/
|
|
1217
1257
|
export function convertSubsystemToEdges(doc: SubsystemModelDocument): SubsystemGraphEdge[] {
|
|
1218
|
-
const
|
|
1258
|
+
const compAliases = new Set(doc.components.map((c) => c.alias));
|
|
1219
1259
|
const edges: SubsystemGraphEdge[] = [];
|
|
1220
1260
|
|
|
1221
1261
|
for (const e of deriveGraphEdges(doc)) {
|
|
1222
1262
|
const color = MECHANISM_COLOR[e.mechanism];
|
|
1223
1263
|
const style = MECHANISM_STYLE[e.mechanism];
|
|
1224
1264
|
// If `to` is a real component, connect directly; otherwise point at a stub node.
|
|
1225
|
-
const isExternal = !
|
|
1265
|
+
const isExternal = !compAliases.has(e.to);
|
|
1226
1266
|
const targetId = isExternal ? `external:${e.to}` : e.to;
|
|
1227
1267
|
|
|
1228
1268
|
edges.push({
|
|
@@ -1248,8 +1288,8 @@ export function subsystemGraphLayoutKey(
|
|
|
1248
1288
|
doc: Pick<SubsystemModelDocument, 'components' | 'relations' | 'walkthroughs'>,
|
|
1249
1289
|
): string {
|
|
1250
1290
|
const components = doc.components
|
|
1251
|
-
.map(({
|
|
1252
|
-
[
|
|
1291
|
+
.map(({ alias, purl, name, symbol, construct, file, purpose, process, module }) =>
|
|
1292
|
+
[alias, purl, name, symbol ?? '', construct, file, purpose ?? '', process ?? '', module ?? ''].join('\0'))
|
|
1253
1293
|
.sort()
|
|
1254
1294
|
.join('\n');
|
|
1255
1295
|
const edgeKey = deriveGraphEdges(doc)
|
|
@@ -1292,7 +1332,7 @@ export async function buildSubsystemGraph(
|
|
|
1292
1332
|
const layoutGroups = buildBoundaryLayoutGroups(doc, frameOpts);
|
|
1293
1333
|
const regions = layoutGroups.map((g) => g.region);
|
|
1294
1334
|
const regionIds = new Set(layoutGroups.map((g) => g.id));
|
|
1295
|
-
const
|
|
1335
|
+
const byAlias = new Map(doc.components.map((c) => [c.alias, c]));
|
|
1296
1336
|
|
|
1297
1337
|
// Resolve leaf parentIds against kept frames: module → process → package.
|
|
1298
1338
|
// Stamp package parents here (not in convertSubsystemToNodes) so single-repo
|
|
@@ -1301,7 +1341,7 @@ export async function buildSubsystemGraph(
|
|
|
1301
1341
|
if (n.type !== 'subsystem-component') continue;
|
|
1302
1342
|
const parentId = (n as { parentId?: string }).parentId;
|
|
1303
1343
|
if (parentId && regionIds.has(parentId)) continue;
|
|
1304
|
-
const comp = (n.data as SubsystemGraphNodeData).component ??
|
|
1344
|
+
const comp = (n.data as SubsystemGraphNodeData).component ?? byAlias.get(n.id);
|
|
1305
1345
|
const processKey = comp?.process?.trim();
|
|
1306
1346
|
const processId = processKey ? processGroupNodeId(processKey) : undefined;
|
|
1307
1347
|
if (processId && regionIds.has(processId)) {
|
|
@@ -1319,10 +1359,10 @@ export async function buildSubsystemGraph(
|
|
|
1319
1359
|
|
|
1320
1360
|
// External edge targets that aren't real components → create stub nodes so
|
|
1321
1361
|
// cross-package edges have something to land on.
|
|
1322
|
-
const
|
|
1362
|
+
const realAliases = new Set(doc.components.map((c) => c.alias));
|
|
1323
1363
|
const externalIds: string[] = [];
|
|
1324
1364
|
for (const e of deriveGraphEdges(doc)) {
|
|
1325
|
-
if (!
|
|
1365
|
+
if (!realAliases.has(e.to)) {
|
|
1326
1366
|
const extId = `external:${e.to}`;
|
|
1327
1367
|
if (!externalIds.includes(extId)) externalIds.push(extId);
|
|
1328
1368
|
}
|
|
@@ -1338,7 +1378,7 @@ export async function buildSubsystemGraph(
|
|
|
1338
1378
|
height: 60,
|
|
1339
1379
|
data: {
|
|
1340
1380
|
component: {
|
|
1341
|
-
|
|
1381
|
+
alias: extId,
|
|
1342
1382
|
name: label,
|
|
1343
1383
|
construct: 'external',
|
|
1344
1384
|
purl: 'external',
|
|
@@ -1382,7 +1422,7 @@ export async function buildSubsystemGraph(
|
|
|
1382
1422
|
edgeLabels: showEdgeLabels === false ? { enabled: false } : { enabled: true, placement: 'CENTER' },
|
|
1383
1423
|
groups: layoutGroups.map((g) => ({
|
|
1384
1424
|
id: g.id,
|
|
1385
|
-
memberIds: g.
|
|
1425
|
+
memberIds: g.memberAliases,
|
|
1386
1426
|
parentId: g.parentId,
|
|
1387
1427
|
minWidth: g.region.kind === 'module' ? moduleMinWidthForBadge(g.region.label) : undefined,
|
|
1388
1428
|
})),
|
package/src/subsystem/nodes.tsx
CHANGED
|
@@ -31,6 +31,8 @@ import {
|
|
|
31
31
|
nodeMinWidthForBadges,
|
|
32
32
|
MODULE_BADGE_INSET,
|
|
33
33
|
moduleBadgeLabel,
|
|
34
|
+
moduleBadgeHoverLabel,
|
|
35
|
+
moduleBadgeWidth,
|
|
34
36
|
packageColor,
|
|
35
37
|
type SubsystemGraphNodeData,
|
|
36
38
|
type SubsystemGroupNodeData,
|
|
@@ -67,14 +69,14 @@ function breakWords(s: string): string {
|
|
|
67
69
|
|
|
68
70
|
export interface SubsystemGraphCallbacks {
|
|
69
71
|
/** Click a component — open its file/entry point. */
|
|
70
|
-
onSelect?: (
|
|
72
|
+
onSelect?: (componentAlias: string) => void;
|
|
71
73
|
/** Click the filename badge — open that component's file in the drawer
|
|
72
74
|
* (same path as clicking the file link in the declaration panel). */
|
|
73
|
-
onOpenFile?: (
|
|
75
|
+
onOpenFile?: (componentAlias: string) => void;
|
|
74
76
|
/** Click an edge (or its label) — select the relationship. */
|
|
75
77
|
onEdgeSelect?: (edgeId: string) => void;
|
|
76
78
|
/** Hover a component (null on leave) — associates it with the file tree. */
|
|
77
|
-
onHover?: (
|
|
79
|
+
onHover?: (componentAlias: string | null) => void;
|
|
78
80
|
/** Upper bound for node width; nodes grow with content up to this, then wrap. */
|
|
79
81
|
maxNodeWidth?: number;
|
|
80
82
|
}
|
|
@@ -129,7 +131,7 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
|
|
|
129
131
|
<div
|
|
130
132
|
onMouseEnter={() => {
|
|
131
133
|
setHover(true);
|
|
132
|
-
SUBSYSTEM_CALLBACKS.onHover?.(c.
|
|
134
|
+
SUBSYSTEM_CALLBACKS.onHover?.(c.alias);
|
|
133
135
|
}}
|
|
134
136
|
onMouseLeave={() => {
|
|
135
137
|
setHover(false);
|
|
@@ -137,7 +139,7 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
|
|
|
137
139
|
}}
|
|
138
140
|
onClick={(e) => {
|
|
139
141
|
e.stopPropagation();
|
|
140
|
-
SUBSYSTEM_CALLBACKS.onSelect?.(c.
|
|
142
|
+
SUBSYSTEM_CALLBACKS.onSelect?.(c.alias);
|
|
141
143
|
}}
|
|
142
144
|
style={{
|
|
143
145
|
position: 'relative',
|
|
@@ -273,7 +275,7 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
|
|
|
273
275
|
onClick={(e) => {
|
|
274
276
|
// Open the file directly; don't also toggle node selection.
|
|
275
277
|
e.stopPropagation();
|
|
276
|
-
SUBSYSTEM_CALLBACKS.onOpenFile?.(c.
|
|
278
|
+
SUBSYSTEM_CALLBACKS.onOpenFile?.(c.alias);
|
|
277
279
|
}}
|
|
278
280
|
>
|
|
279
281
|
{c.file.split('/').pop()}
|
|
@@ -356,13 +358,18 @@ export function SubsystemGroupNode(props: NodeProps<Node<SubsystemGroupNodeData,
|
|
|
356
358
|
const label = region?.label ?? '';
|
|
357
359
|
const [expandedLabel, setExpandedLabel] = useState<string | null>(null);
|
|
358
360
|
const isModule = region?.kind === 'module';
|
|
361
|
+
const isProcess = region?.kind === 'process';
|
|
359
362
|
const expanded = isModule && expandedLabel === label;
|
|
360
363
|
const availableBadgeWidth = Math.max(0, (width ?? 400) - MODULE_BADGE_INSET * 2 - 4);
|
|
361
364
|
const collapsedLabel = isModule ? moduleBadgeLabel(label, availableBadgeWidth) : label;
|
|
362
|
-
|
|
363
|
-
//
|
|
364
|
-
|
|
365
|
-
|
|
365
|
+
// A module badge is collapsible while something collapsed, and stays
|
|
366
|
+
// togglable while expanded (so the full path can collapse back).
|
|
367
|
+
const canToggle = isModule && (expanded || collapsedLabel !== label);
|
|
368
|
+
// Hover affords clickability by revealing more of the path — the badge
|
|
369
|
+
// widens to fit real text (never stretched), hinting before the click.
|
|
370
|
+
const [hover, setHover] = useState(false);
|
|
371
|
+
const hoveredLabel =
|
|
372
|
+
isModule && canToggle ? moduleBadgeHoverLabel(label, availableBadgeWidth) : label;
|
|
366
373
|
const frameRadius = isProcess ? 0 : 12;
|
|
367
374
|
const badgeRadius = isProcess ? 0 : 4;
|
|
368
375
|
// Processes are deployment units — solid frame. Modules/packages stay dashed
|
|
@@ -391,16 +398,17 @@ export function SubsystemGroupNode(props: NodeProps<Node<SubsystemGroupNodeData,
|
|
|
391
398
|
>
|
|
392
399
|
<div
|
|
393
400
|
className={isModule ? 'nodrag nopan' : undefined}
|
|
394
|
-
role={
|
|
395
|
-
tabIndex={
|
|
396
|
-
aria-expanded={
|
|
397
|
-
aria-label={
|
|
398
|
-
|
|
399
|
-
|
|
401
|
+
role={canToggle ? 'button' : undefined}
|
|
402
|
+
tabIndex={canToggle && !hidden ? 0 : undefined}
|
|
403
|
+
aria-expanded={canToggle ? expanded : undefined}
|
|
404
|
+
aria-label={canToggle ? `${expanded ? 'Collapse' : 'Expand'} module path: ${label}` : undefined}
|
|
405
|
+
onMouseEnter={canToggle ? () => setHover(true) : undefined}
|
|
406
|
+
onMouseLeave={canToggle ? () => setHover(false) : undefined}
|
|
407
|
+
onClick={canToggle ? (event) => {
|
|
400
408
|
event.stopPropagation();
|
|
401
409
|
setExpandedLabel(expanded ? null : label);
|
|
402
410
|
} : undefined}
|
|
403
|
-
onKeyDown={
|
|
411
|
+
onKeyDown={canToggle ? (event) => {
|
|
404
412
|
if (event.key === 'Enter' || event.key === ' ') {
|
|
405
413
|
event.preventDefault();
|
|
406
414
|
event.stopPropagation();
|
|
@@ -420,6 +428,18 @@ export function SubsystemGroupNode(props: NodeProps<Node<SubsystemGroupNodeData,
|
|
|
420
428
|
top: isProcess ? 0 : -19,
|
|
421
429
|
left: isProcess ? '50%' : 12,
|
|
422
430
|
transform: isProcess ? 'translateX(-50%)' : 'none',
|
|
431
|
+
zIndex: canToggle ? 10 : undefined,
|
|
432
|
+
// Width is explicit while collapsible so hover-reveal of more path
|
|
433
|
+
// text animates; the cap keeps both states inside the frame.
|
|
434
|
+
width:
|
|
435
|
+
isModule && canToggle && !expanded
|
|
436
|
+
? hover
|
|
437
|
+
? Math.min(availableBadgeWidth, moduleBadgeWidth(hoveredLabel))
|
|
438
|
+
: Math.min(availableBadgeWidth, moduleBadgeWidth(collapsedLabel))
|
|
439
|
+
: undefined,
|
|
440
|
+
transition: 'width 140ms ease, box-shadow 120ms ease',
|
|
441
|
+
cursor: canToggle ? 'pointer' : undefined,
|
|
442
|
+
boxShadow: canToggle && hover ? `0 2px 10px ${color}55` : undefined,
|
|
423
443
|
fontFamily: theme.fonts.monospace,
|
|
424
444
|
fontSize: theme.fontSizes[3],
|
|
425
445
|
fontWeight: 700,
|
|
@@ -431,9 +451,28 @@ export function SubsystemGroupNode(props: NodeProps<Node<SubsystemGroupNodeData,
|
|
|
431
451
|
borderRadius: badgeRadius,
|
|
432
452
|
padding: '1px 7px',
|
|
433
453
|
whiteSpace: 'nowrap',
|
|
454
|
+
overflow: 'hidden',
|
|
455
|
+
textOverflow: 'ellipsis',
|
|
456
|
+
boxSizing: 'border-box',
|
|
457
|
+
pointerEvents: hidden ? 'none' : 'auto',
|
|
458
|
+
...(isModule
|
|
459
|
+
? {
|
|
460
|
+
// Cap the collapsed badge at the frame's inner width so it
|
|
461
|
+
// never spills past the module — it gets the `…`/click-to-
|
|
462
|
+
// expand treatment instead. Expanding lifts the cap so the
|
|
463
|
+
// full path grows the badge past the frame's edge.
|
|
464
|
+
maxWidth: expanded ? undefined : availableBadgeWidth,
|
|
465
|
+
}
|
|
466
|
+
: undefined),
|
|
434
467
|
}}
|
|
435
468
|
>
|
|
436
|
-
{
|
|
469
|
+
{canToggle ? (
|
|
470
|
+
<span style={{ whiteSpace: 'nowrap' }}>
|
|
471
|
+
{expanded ? label : hover ? hoveredLabel : collapsedLabel}
|
|
472
|
+
</span>
|
|
473
|
+
) : (
|
|
474
|
+
label
|
|
475
|
+
)}
|
|
437
476
|
</div>
|
|
438
477
|
</div>
|
|
439
478
|
);
|
package/src/subsystem/paths.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* by its `purl` (`pkg:github/<owner>/<name>[#<path>]`) — never to the machine.
|
|
6
6
|
* When a graph spans several repos the sidebar tree needs synthetic
|
|
7
7
|
* `<owner>/<name>/` prefixes to keep same-named paths apart; hosts resolve
|
|
8
|
-
* each file against
|
|
8
|
+
* each file against its own purl's checkout (via the Alexandria registry).
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
/** Normalize a purl to its repo key: fragment and surrounding whitespace stripped. */
|