@principal-ai/subsystems-react 0.23.0 → 0.23.2
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/pierre/PierreWalkthroughCodeView.d.ts +2 -1
- package/dist/pierre/PierreWalkthroughCodeView.d.ts.map +1 -1
- package/dist/pierre/PierreWalkthroughCodeView.js +4 -4
- package/dist/pierre/PierreWalkthroughCodeView.js.map +1 -1
- package/dist/pierre/constructColors.d.ts +2 -3
- package/dist/pierre/constructColors.d.ts.map +1 -1
- package/dist/pierre/constructColors.js +31 -21
- package/dist/pierre/constructColors.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +8 -6
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/model.d.ts +69 -23
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js +184 -40
- package/dist/subsystem/model.js.map +1 -1
- package/dist/subsystem/nodes.d.ts.map +1 -1
- package/dist/subsystem/nodes.js +53 -25
- package/dist/subsystem/nodes.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +2 -0
- package/src/pierre/PierreWalkthroughCodeView.tsx +12 -5
- package/src/pierre/constructColors.test.ts +2 -2
- package/src/pierre/constructColors.ts +41 -23
- package/src/stories/Subsystem/ComponentGraph/DetailPanel.stories.tsx +2 -1
- package/src/stories/Subsystem/ComponentGraph/Flows.stories.tsx +1 -0
- package/src/stories/Subsystem/ComponentGraph/Packages.stories.tsx +242 -0
- package/src/subsystem/SubsystemComponentGraph.tsx +23 -7
- package/src/subsystem/model.test.ts +167 -0
- package/src/subsystem/model.ts +236 -46
- package/src/subsystem/nodes.tsx +55 -26
package/src/subsystem/model.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
import { computeElkLayout, calculatePathLength } from '../utils/elkLayout';
|
|
21
21
|
import type { GraphifyComponentDetail } from '../graphify';
|
|
22
22
|
import type { SubsystemDeclarationRef } from './declarationRef';
|
|
23
|
+
import { purlOwnerName, purlRepoKey } from './paths';
|
|
23
24
|
|
|
24
25
|
/** Structured declaration shape — same union as graphify drill-down payloads. */
|
|
25
26
|
export type SubsystemConstructDeclaration = GraphifyComponentDetail;
|
|
@@ -430,13 +431,15 @@ export function formatPurl(purl: string): string {
|
|
|
430
431
|
export type SubsystemGraphNodeType = 'subsystem-component' | 'subsystem-group';
|
|
431
432
|
|
|
432
433
|
/**
|
|
433
|
-
* One boundary region —
|
|
434
|
-
*
|
|
434
|
+
* One boundary region — process (runtime), module (source file), or package
|
|
435
|
+
* (repo identity from `purl`). Nodes without that membership sit outside
|
|
436
|
+
* those frames. Package frames are derived from `purl` (no separate field);
|
|
437
|
+
* they only appear when the graph spans multiple repos (by default).
|
|
435
438
|
*/
|
|
436
439
|
export interface SubsystemProcessRegion {
|
|
437
|
-
/** Discriminator — which field produced this region. */
|
|
438
|
-
kind: 'process' | 'module';
|
|
439
|
-
/** The `process`
|
|
440
|
+
/** Discriminator — which field / identity produced this region. */
|
|
441
|
+
kind: 'process' | 'module' | 'package';
|
|
442
|
+
/** The `process` / `module` value, or purl repo key for packages. */
|
|
440
443
|
key: string;
|
|
441
444
|
/** Display label for the boundary frame. */
|
|
442
445
|
label: string;
|
|
@@ -444,6 +447,22 @@ export interface SubsystemProcessRegion {
|
|
|
444
447
|
memberIds: string[];
|
|
445
448
|
}
|
|
446
449
|
|
|
450
|
+
/** Options for which boundary frames are kept. */
|
|
451
|
+
export interface BoundaryFrameOptions {
|
|
452
|
+
/**
|
|
453
|
+
* Keep 1-member process / module / package frames. Default false
|
|
454
|
+
* (singleton rule — frames need 2+ members).
|
|
455
|
+
*/
|
|
456
|
+
showSingletonFrames?: boolean;
|
|
457
|
+
/**
|
|
458
|
+
* When package frames are drawn from component `purl`s.
|
|
459
|
+
* - `multi-repo` (default): only when 2+ distinct repo purls
|
|
460
|
+
* - `always`: frame every multi-member package even in a single-repo graph
|
|
461
|
+
* - `never`: no package frames
|
|
462
|
+
*/
|
|
463
|
+
packageFrames?: 'multi-repo' | 'always' | 'never';
|
|
464
|
+
}
|
|
465
|
+
|
|
447
466
|
/** React Flow id for a process boundary group node. */
|
|
448
467
|
export function processGroupNodeId(processKey: string): string {
|
|
449
468
|
return `process:${processKey}`;
|
|
@@ -454,11 +473,33 @@ export function moduleGroupNodeId(moduleKey: string): string {
|
|
|
454
473
|
return `module:${moduleKey}`;
|
|
455
474
|
}
|
|
456
475
|
|
|
457
|
-
/** React Flow id for a boundary group
|
|
476
|
+
/** React Flow id for a package (repo) boundary group node. */
|
|
477
|
+
export function packageGroupNodeId(packageKey: string): string {
|
|
478
|
+
return `package:${packageKey}`;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** React Flow id for a boundary group of any kind. */
|
|
458
482
|
export function boundaryGroupNodeId(region: Pick<SubsystemProcessRegion, 'kind' | 'key'>): string {
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
483
|
+
if (region.kind === 'module') return moduleGroupNodeId(region.key);
|
|
484
|
+
if (region.kind === 'package') return packageGroupNodeId(region.key);
|
|
485
|
+
return processGroupNodeId(region.key);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Repo/package key used for package frames — `purl` with fragment stripped.
|
|
490
|
+
* Externals and custom entities are not package-frame members.
|
|
491
|
+
*/
|
|
492
|
+
export function componentPackageKey(
|
|
493
|
+
c: Pick<SubsystemComponent, 'purl' | 'construct'>,
|
|
494
|
+
): string | undefined {
|
|
495
|
+
if (c.construct === 'external' || c.construct === 'custom_entity') return undefined;
|
|
496
|
+
const key = purlRepoKey(c.purl);
|
|
497
|
+
if (!key || key === 'external') return undefined;
|
|
498
|
+
return key;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function packageRegionLabel(packageKey: string): string {
|
|
502
|
+
return purlOwnerName(packageKey) ?? formatPurl(packageKey);
|
|
462
503
|
}
|
|
463
504
|
|
|
464
505
|
/**
|
|
@@ -508,42 +549,103 @@ export function getSubsystemModuleRegions(
|
|
|
508
549
|
}
|
|
509
550
|
|
|
510
551
|
/**
|
|
511
|
-
*
|
|
512
|
-
*
|
|
552
|
+
* Derive package (repo) boundary regions from component `purl`s — one per
|
|
553
|
+
* distinct repo key. Does not apply multi-repo / singleton filters; callers
|
|
554
|
+
* decide via {@link buildBoundaryLayoutGroups}.
|
|
555
|
+
*/
|
|
556
|
+
export function getSubsystemPackageRegions(
|
|
557
|
+
doc: Pick<SubsystemModelDocument, 'components'>,
|
|
558
|
+
): SubsystemProcessRegion[] {
|
|
559
|
+
const byPackage = new Map<string, string[]>();
|
|
560
|
+
for (const c of doc.components) {
|
|
561
|
+
const key = componentPackageKey(c);
|
|
562
|
+
if (!key) continue;
|
|
563
|
+
const list = byPackage.get(key) ?? [];
|
|
564
|
+
list.push(c.id);
|
|
565
|
+
byPackage.set(key, list);
|
|
566
|
+
}
|
|
567
|
+
return [...byPackage.entries()].map(([key, memberIds]) => ({
|
|
568
|
+
kind: 'package' as const,
|
|
569
|
+
key,
|
|
570
|
+
label: packageRegionLabel(key),
|
|
571
|
+
memberIds,
|
|
572
|
+
}));
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* One compound frame for ELK / React Flow — modules nest under processes,
|
|
577
|
+
* processes under packages, when membership is shared.
|
|
513
578
|
*/
|
|
514
579
|
export interface BoundaryLayoutGroup {
|
|
515
580
|
id: string;
|
|
516
581
|
memberIds: string[];
|
|
517
|
-
/** When set, this group is a child of another boundary group
|
|
582
|
+
/** When set, this group is a child of another boundary group. */
|
|
518
583
|
parentId?: string;
|
|
519
584
|
region: SubsystemProcessRegion;
|
|
520
585
|
}
|
|
521
586
|
|
|
587
|
+
function keepRegion(
|
|
588
|
+
r: SubsystemProcessRegion,
|
|
589
|
+
showSingletons: boolean,
|
|
590
|
+
): boolean {
|
|
591
|
+
return showSingletons || r.memberIds.length >= 2;
|
|
592
|
+
}
|
|
593
|
+
|
|
522
594
|
/**
|
|
523
|
-
* Build the process → module → leaf group tree for layout.
|
|
524
|
-
*
|
|
525
|
-
*
|
|
526
|
-
*
|
|
595
|
+
* Build the package → process → module → leaf group tree for layout.
|
|
596
|
+
*
|
|
597
|
+
* Package frames come from `purl` (no separate component field). By default
|
|
598
|
+
* they only appear when the graph spans 2+ distinct repos. Multi-member
|
|
599
|
+
* modules nest under a process when every member shares that process;
|
|
600
|
+
* processes nest under a package the same way.
|
|
527
601
|
*/
|
|
528
602
|
export function buildBoundaryLayoutGroups(
|
|
529
603
|
doc: Pick<SubsystemModelDocument, 'components'>,
|
|
604
|
+
opts: BoundaryFrameOptions = {},
|
|
530
605
|
): BoundaryLayoutGroup[] {
|
|
606
|
+
const showSingletons = opts.showSingletonFrames === true;
|
|
607
|
+
const packageMode = opts.packageFrames ?? 'multi-repo';
|
|
531
608
|
const byId = new Map(doc.components.map((c) => [c.id, c]));
|
|
532
|
-
|
|
533
|
-
const
|
|
609
|
+
|
|
610
|
+
const allPackages = getSubsystemPackageRegions(doc);
|
|
611
|
+
const packageEligible =
|
|
612
|
+
packageMode === 'always'
|
|
613
|
+
? true
|
|
614
|
+
: packageMode === 'never'
|
|
615
|
+
? false
|
|
616
|
+
: allPackages.length >= 2;
|
|
617
|
+
const packages = packageEligible
|
|
618
|
+
? allPackages.filter((r) => keepRegion(r, showSingletons))
|
|
619
|
+
: [];
|
|
620
|
+
const keptPackageKeys = new Set(packages.map((r) => r.key));
|
|
621
|
+
|
|
622
|
+
const modules = getSubsystemModuleRegions(doc).filter((r) =>
|
|
623
|
+
keepRegion(r, showSingletons),
|
|
624
|
+
);
|
|
625
|
+
const processes = getSubsystemRegions(doc).filter((r) =>
|
|
626
|
+
keepRegion(r, showSingletons),
|
|
627
|
+
);
|
|
534
628
|
const keptProcessKeys = new Set(processes.map((r) => r.key));
|
|
535
629
|
|
|
536
630
|
const moduleGroups: BoundaryLayoutGroup[] = modules.map((r) => {
|
|
537
631
|
const processesOfMembers = new Set<string>();
|
|
632
|
+
const packagesOfMembers = new Set<string>();
|
|
538
633
|
for (const id of r.memberIds) {
|
|
539
|
-
const
|
|
634
|
+
const c = byId.get(id);
|
|
635
|
+
const p = c?.process?.trim();
|
|
540
636
|
if (p) processesOfMembers.add(p);
|
|
637
|
+
const pkg = c ? componentPackageKey(c) : undefined;
|
|
638
|
+
if (pkg) packagesOfMembers.add(pkg);
|
|
541
639
|
}
|
|
542
640
|
let parentId: string | undefined;
|
|
543
641
|
if (processesOfMembers.size === 1) {
|
|
544
642
|
const p = [...processesOfMembers][0]!;
|
|
545
643
|
if (keptProcessKeys.has(p)) parentId = processGroupNodeId(p);
|
|
546
644
|
}
|
|
645
|
+
if (!parentId && packagesOfMembers.size === 1) {
|
|
646
|
+
const pkg = [...packagesOfMembers][0]!;
|
|
647
|
+
if (keptPackageKeys.has(pkg)) parentId = packageGroupNodeId(pkg);
|
|
648
|
+
}
|
|
547
649
|
return {
|
|
548
650
|
id: moduleGroupNodeId(r.key),
|
|
549
651
|
memberIds: [...r.memberIds],
|
|
@@ -561,14 +663,51 @@ export function buildBoundaryLayoutGroups(
|
|
|
561
663
|
if (!mod) return true;
|
|
562
664
|
return !nestedModuleKeys.has(mod);
|
|
563
665
|
});
|
|
666
|
+
|
|
667
|
+
const packagesOfMembers = new Set<string>();
|
|
668
|
+
for (const id of r.memberIds) {
|
|
669
|
+
const c = byId.get(id);
|
|
670
|
+
const pkg = c ? componentPackageKey(c) : undefined;
|
|
671
|
+
if (pkg) packagesOfMembers.add(pkg);
|
|
672
|
+
}
|
|
673
|
+
let parentId: string | undefined;
|
|
674
|
+
if (packagesOfMembers.size === 1) {
|
|
675
|
+
const pkg = [...packagesOfMembers][0]!;
|
|
676
|
+
if (keptPackageKeys.has(pkg)) parentId = packageGroupNodeId(pkg);
|
|
677
|
+
}
|
|
678
|
+
|
|
564
679
|
return {
|
|
565
680
|
id: processId,
|
|
566
681
|
memberIds: [...nestedModules.map((m) => m.id), ...directLeaves],
|
|
682
|
+
parentId,
|
|
567
683
|
region: r,
|
|
568
684
|
};
|
|
569
685
|
});
|
|
570
686
|
|
|
571
|
-
|
|
687
|
+
const packageGroups: BoundaryLayoutGroup[] = packages.map((r) => {
|
|
688
|
+
const packageId = packageGroupNodeId(r.key);
|
|
689
|
+
const nestedProcesses = processGroups.filter((p) => p.parentId === packageId);
|
|
690
|
+
const nestedModules = moduleGroups.filter((m) => m.parentId === packageId);
|
|
691
|
+
const claimed = new Set<string>();
|
|
692
|
+
for (const p of nestedProcesses) {
|
|
693
|
+
for (const id of p.region.memberIds) claimed.add(id);
|
|
694
|
+
}
|
|
695
|
+
for (const m of nestedModules) {
|
|
696
|
+
for (const id of m.region.memberIds) claimed.add(id);
|
|
697
|
+
}
|
|
698
|
+
const directLeaves = r.memberIds.filter((id) => !claimed.has(id));
|
|
699
|
+
return {
|
|
700
|
+
id: packageId,
|
|
701
|
+
memberIds: [
|
|
702
|
+
...nestedProcesses.map((p) => p.id),
|
|
703
|
+
...nestedModules.map((m) => m.id),
|
|
704
|
+
...directLeaves,
|
|
705
|
+
],
|
|
706
|
+
region: r,
|
|
707
|
+
};
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
return [...moduleGroups, ...processGroups, ...packageGroups];
|
|
572
711
|
}
|
|
573
712
|
|
|
574
713
|
export interface SubsystemGraphNodeData extends Record<string, unknown> {
|
|
@@ -689,6 +828,14 @@ export const ROLE_COLOR: Record<SubsystemComponentRole, string> = {
|
|
|
689
828
|
service: '#0893d2', // blue — external system
|
|
690
829
|
};
|
|
691
830
|
|
|
831
|
+
/**
|
|
832
|
+
* Brand color for the left construct/stereotype badge when a framework owns
|
|
833
|
+
* the label (e.g. `react · component`). Node border stays construct-colored.
|
|
834
|
+
*/
|
|
835
|
+
export const FRAMEWORK_BADGE_COLOR: Record<string, string> = {
|
|
836
|
+
react: '#61dafb', // React cyan
|
|
837
|
+
};
|
|
838
|
+
|
|
692
839
|
export const ROLE_LABEL: Record<SubsystemComponentRole, string> = {
|
|
693
840
|
entry: 'entry',
|
|
694
841
|
service: 'service',
|
|
@@ -790,14 +937,27 @@ export function constructBadgeLabel(component: {
|
|
|
790
937
|
return constructLabel ?? '';
|
|
791
938
|
}
|
|
792
939
|
|
|
940
|
+
/**
|
|
941
|
+
* Left-badge accent: framework brand when the badge shows a framework
|
|
942
|
+
* stereotype (e.g. React cyan for `react · component`); otherwise null so
|
|
943
|
+
* the caller falls back to construct color. Border stays construct-colored.
|
|
944
|
+
*/
|
|
945
|
+
export function constructBadgeColor(component: {
|
|
946
|
+
framework?: string;
|
|
947
|
+
stereotype?: string;
|
|
948
|
+
}): string | null {
|
|
949
|
+
if (component.framework == null || component.stereotype == null) return null;
|
|
950
|
+
return FRAMEWORK_BADGE_COLOR[component.framework] ?? null;
|
|
951
|
+
}
|
|
952
|
+
|
|
793
953
|
/** Default CSS floor for component nodes (padding aside). */
|
|
794
954
|
export const NODE_CSS_MIN_WIDTH = 150;
|
|
795
955
|
/** Inset of each top badge from the node edge (`left` / `right` style). */
|
|
796
956
|
export const BADGE_EDGE_INSET = 5;
|
|
797
957
|
/** Minimum gap between left construct badge and right role badge. */
|
|
798
958
|
const BADGE_PAIR_GAP = 8;
|
|
799
|
-
/** Badge box chrome: padding
|
|
800
|
-
const BADGE_BOX_CHROME =
|
|
959
|
+
/** Badge box chrome: padding 8+8 + border 2+2. */
|
|
960
|
+
const BADGE_BOX_CHROME = 20;
|
|
801
961
|
/** Approx monospace uppercase width incl. letter-spacing (~0.5px). */
|
|
802
962
|
const BADGE_CHAR_WIDTH = 8;
|
|
803
963
|
|
|
@@ -840,11 +1000,11 @@ export function nodeMinWidthForBadges(component: {
|
|
|
840
1000
|
/**
|
|
841
1001
|
* Convert a subsystem graph document into React Flow nodes. Components that
|
|
842
1002
|
* carry a `module` get a `parentId` pointing at their module frame
|
|
843
|
-
* (`module:<path>`); otherwise a `process` stamps `process:<process>`.
|
|
844
|
-
*
|
|
845
|
-
*
|
|
846
|
-
* grid clusters by `module ?? process ?? purl`; ELK
|
|
847
|
-
* layout.
|
|
1003
|
+
* (`module:<path>`); otherwise a `process` stamps `process:<process>`.
|
|
1004
|
+
* Package (`purl`) parents are stamped later in `buildSubsystemGraph` only
|
|
1005
|
+
* when package frames are kept. Module frames may nest under process →
|
|
1006
|
+
* package. The initial grid clusters by `module ?? process ?? purl`; ELK
|
|
1007
|
+
* then refines with compound layout.
|
|
848
1008
|
*/
|
|
849
1009
|
export function convertSubsystemToNodes(
|
|
850
1010
|
doc: SubsystemModelDocument,
|
|
@@ -913,14 +1073,15 @@ export function convertSubsystemToNodes(
|
|
|
913
1073
|
}
|
|
914
1074
|
|
|
915
1075
|
/**
|
|
916
|
-
* Convert boundary regions into React Flow parent (group) nodes.
|
|
917
|
-
*
|
|
1076
|
+
* Convert boundary regions into React Flow parent (group) nodes. Nesting
|
|
1077
|
+
* (package → process → module) is stamped via `parentId` on groups.
|
|
918
1078
|
* Positions/sizes are placeholders — ELK compound layout overwrites them.
|
|
919
1079
|
*/
|
|
920
1080
|
export function convertSubsystemToGroups(
|
|
921
1081
|
doc: Pick<SubsystemModelDocument, 'components'>,
|
|
1082
|
+
opts: BoundaryFrameOptions = {},
|
|
922
1083
|
): SubsystemGraphNode[] {
|
|
923
|
-
return buildBoundaryLayoutGroups(doc).map((g) => ({
|
|
1084
|
+
return buildBoundaryLayoutGroups(doc, opts).map((g) => ({
|
|
924
1085
|
id: g.id,
|
|
925
1086
|
type: 'subsystem-group' as const,
|
|
926
1087
|
position: { x: 0, y: 0 },
|
|
@@ -988,36 +1149,55 @@ export function subsystemGraphLayoutKey(
|
|
|
988
1149
|
*/
|
|
989
1150
|
export async function buildSubsystemGraph(
|
|
990
1151
|
doc: SubsystemModelDocument,
|
|
991
|
-
opts: {
|
|
1152
|
+
opts: {
|
|
1153
|
+
maxNodeWidth?: number;
|
|
1154
|
+
showEdgeLabels?: boolean;
|
|
1155
|
+
measuredWidths?: Map<string, number>;
|
|
1156
|
+
measuredHeights?: Map<string, number>;
|
|
1157
|
+
} & BoundaryFrameOptions = {},
|
|
992
1158
|
): Promise<{
|
|
993
1159
|
nodes: SubsystemGraphNode[];
|
|
994
1160
|
edges: SubsystemGraphEdge[];
|
|
995
1161
|
regions: SubsystemProcessRegion[];
|
|
996
1162
|
}> {
|
|
997
|
-
const {
|
|
1163
|
+
const {
|
|
1164
|
+
maxNodeWidth,
|
|
1165
|
+
showEdgeLabels,
|
|
1166
|
+
measuredWidths,
|
|
1167
|
+
measuredHeights,
|
|
1168
|
+
showSingletonFrames,
|
|
1169
|
+
packageFrames,
|
|
1170
|
+
} = opts;
|
|
1171
|
+
const frameOpts: BoundaryFrameOptions = { showSingletonFrames, packageFrames };
|
|
998
1172
|
const nodes = convertSubsystemToNodes(doc, { maxNodeWidth });
|
|
999
1173
|
const edges = convertSubsystemToEdges(doc);
|
|
1000
|
-
// Nested boundary tree: process → module → leaves
|
|
1001
|
-
const layoutGroups = buildBoundaryLayoutGroups(doc);
|
|
1174
|
+
// Nested boundary tree: package → process → module → leaves.
|
|
1175
|
+
const layoutGroups = buildBoundaryLayoutGroups(doc, frameOpts);
|
|
1002
1176
|
const regions = layoutGroups.map((g) => g.region);
|
|
1003
1177
|
const regionIds = new Set(layoutGroups.map((g) => g.id));
|
|
1004
1178
|
const byId = new Map(doc.components.map((c) => [c.id, c]));
|
|
1005
1179
|
|
|
1006
|
-
//
|
|
1007
|
-
//
|
|
1180
|
+
// Resolve leaf parentIds against kept frames: module → process → package.
|
|
1181
|
+
// Stamp package parents here (not in convertSubsystemToNodes) so single-repo
|
|
1182
|
+
// graphs never provisionally parent under a package that will be dropped.
|
|
1008
1183
|
for (const n of nodes) {
|
|
1009
1184
|
if (n.type !== 'subsystem-component') continue;
|
|
1010
1185
|
const parentId = (n as { parentId?: string }).parentId;
|
|
1011
|
-
if (
|
|
1012
|
-
|
|
1013
|
-
const
|
|
1014
|
-
const processKey = comp?.process?.trim() || byId.get(n.id)?.process?.trim();
|
|
1186
|
+
if (parentId && regionIds.has(parentId)) continue;
|
|
1187
|
+
const comp = (n.data as SubsystemGraphNodeData).component ?? byId.get(n.id);
|
|
1188
|
+
const processKey = comp?.process?.trim();
|
|
1015
1189
|
const processId = processKey ? processGroupNodeId(processKey) : undefined;
|
|
1016
1190
|
if (processId && regionIds.has(processId)) {
|
|
1017
1191
|
(n as { parentId?: string }).parentId = processId;
|
|
1018
|
-
|
|
1019
|
-
delete (n as { parentId?: string }).parentId;
|
|
1192
|
+
continue;
|
|
1020
1193
|
}
|
|
1194
|
+
const packageKey = comp ? componentPackageKey(comp) : undefined;
|
|
1195
|
+
const packageId = packageKey ? packageGroupNodeId(packageKey) : undefined;
|
|
1196
|
+
if (packageId && regionIds.has(packageId)) {
|
|
1197
|
+
(n as { parentId?: string }).parentId = packageId;
|
|
1198
|
+
continue;
|
|
1199
|
+
}
|
|
1200
|
+
if (parentId) delete (n as { parentId?: string }).parentId;
|
|
1021
1201
|
}
|
|
1022
1202
|
|
|
1023
1203
|
// External edge targets that aren't real components → create stub nodes so
|
|
@@ -1090,7 +1270,8 @@ export async function buildSubsystemGraph(
|
|
|
1090
1270
|
})),
|
|
1091
1271
|
});
|
|
1092
1272
|
const builtGroupIds = new Set(result.groupBounds.keys());
|
|
1093
|
-
// Parents before children — process
|
|
1273
|
+
// Parents before children — package, then process, then module frames.
|
|
1274
|
+
const packageGroupNodes: SubsystemGraphNode[] = [];
|
|
1094
1275
|
const processGroupNodes: SubsystemGraphNode[] = [];
|
|
1095
1276
|
const moduleGroupNodes: SubsystemGraphNode[] = [];
|
|
1096
1277
|
for (const g of layoutGroups) {
|
|
@@ -1107,7 +1288,8 @@ export async function buildSubsystemGraph(
|
|
|
1107
1288
|
...(parentId ? { parentId } : {}),
|
|
1108
1289
|
data: { region: g.region },
|
|
1109
1290
|
};
|
|
1110
|
-
if (g.region.kind === '
|
|
1291
|
+
if (g.region.kind === 'package') packageGroupNodes.push(group);
|
|
1292
|
+
else if (g.region.kind === 'process') processGroupNodes.push(group);
|
|
1111
1293
|
else moduleGroupNodes.push(group);
|
|
1112
1294
|
}
|
|
1113
1295
|
// Clear leaf parentIds that point at groups ELK dropped.
|
|
@@ -1119,6 +1301,7 @@ export async function buildSubsystemGraph(
|
|
|
1119
1301
|
}
|
|
1120
1302
|
}
|
|
1121
1303
|
placedNodes = [
|
|
1304
|
+
...packageGroupNodes,
|
|
1122
1305
|
...processGroupNodes,
|
|
1123
1306
|
...moduleGroupNodes,
|
|
1124
1307
|
...(result.nodes as SubsystemGraphNode[]),
|
|
@@ -1130,6 +1313,7 @@ export async function buildSubsystemGraph(
|
|
|
1130
1313
|
// Fall back to the (unpositioned) grid if ELK is unavailable — still
|
|
1131
1314
|
// emit multi-member frames so parentId targets exist.
|
|
1132
1315
|
console.warn('[subsystem-graph] ELK layout failed, using manual positions:', err);
|
|
1316
|
+
const packageGroupNodes: SubsystemGraphNode[] = [];
|
|
1133
1317
|
const processGroupNodes: SubsystemGraphNode[] = [];
|
|
1134
1318
|
const moduleGroupNodes: SubsystemGraphNode[] = [];
|
|
1135
1319
|
for (const g of layoutGroups) {
|
|
@@ -1142,10 +1326,16 @@ export async function buildSubsystemGraph(
|
|
|
1142
1326
|
...(g.parentId ? { parentId: g.parentId } : {}),
|
|
1143
1327
|
data: { region: g.region },
|
|
1144
1328
|
};
|
|
1145
|
-
if (g.region.kind === '
|
|
1329
|
+
if (g.region.kind === 'package') packageGroupNodes.push(group);
|
|
1330
|
+
else if (g.region.kind === 'process') processGroupNodes.push(group);
|
|
1146
1331
|
else moduleGroupNodes.push(group);
|
|
1147
1332
|
}
|
|
1148
|
-
placedNodes = [
|
|
1333
|
+
placedNodes = [
|
|
1334
|
+
...packageGroupNodes,
|
|
1335
|
+
...processGroupNodes,
|
|
1336
|
+
...moduleGroupNodes,
|
|
1337
|
+
...nodes,
|
|
1338
|
+
];
|
|
1149
1339
|
}
|
|
1150
1340
|
}
|
|
1151
1341
|
|
package/src/subsystem/nodes.tsx
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
MECHANISM_COLOR,
|
|
21
21
|
MECHANISM_STYLE,
|
|
22
22
|
PROPOSED_COLOR,
|
|
23
|
+
constructBadgeColor,
|
|
23
24
|
constructBadgeLabel,
|
|
24
25
|
rightBadgeLabel,
|
|
25
26
|
rightBadgeColor,
|
|
@@ -88,6 +89,9 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
|
|
|
88
89
|
// shows as the hover badge, not as color — for now; a role glyph/accent may
|
|
89
90
|
// come later. A component-authored `color` override wins over the construct.
|
|
90
91
|
const color = componentColor(c, resolvePierreSyntaxThemeName(mode));
|
|
92
|
+
// Left badge: framework brand (e.g. React cyan) when the label is a
|
|
93
|
+
// framework stereotype; otherwise the same construct color as the border.
|
|
94
|
+
const badgeColor = constructBadgeColor(c) ?? color;
|
|
91
95
|
const [hover, setHover] = useState(false);
|
|
92
96
|
const configuredMax = SUBSYSTEM_CALLBACKS.maxNodeWidth;
|
|
93
97
|
const maxWidth = configuredMax ?? 300;
|
|
@@ -114,6 +118,10 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
|
|
|
114
118
|
// Selection is stamped into data by the graph component — React Flow's own
|
|
115
119
|
// `selected` never updates because the node stops click propagation.
|
|
116
120
|
const isSelected = selected || (data.isSelected as boolean | undefined) === true;
|
|
121
|
+
// Externals are "outside" the system — square corners on the node + badges.
|
|
122
|
+
const isExternal = c.construct === 'external';
|
|
123
|
+
const nodeRadius = isExternal ? 0 : 8;
|
|
124
|
+
const badgeRadius = isExternal ? 0 : 4;
|
|
117
125
|
|
|
118
126
|
return (
|
|
119
127
|
<div
|
|
@@ -141,7 +149,7 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
|
|
|
141
149
|
minWidth: badgeMinWidth,
|
|
142
150
|
maxWidth,
|
|
143
151
|
padding: '6px 10px',
|
|
144
|
-
borderRadius:
|
|
152
|
+
borderRadius: nodeRadius,
|
|
145
153
|
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
146
154
|
// Selected / file-matched nodes get a thicker border. Proposed nodes
|
|
147
155
|
// use a dashed goldenrod border; left construct badge keeps construct color.
|
|
@@ -157,11 +165,12 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
|
|
|
157
165
|
>
|
|
158
166
|
{/* Construct / stereotype badge — prefers framework stereotype so a
|
|
159
167
|
React UI unit reads as "react · component" instead of "function".
|
|
160
|
-
Persistent; pointer-events none so clicks pass through to the node.
|
|
168
|
+
Persistent; pointer-events none so clicks pass through to the node.
|
|
169
|
+
Framework badges use brand color; border stays construct-colored. */}
|
|
161
170
|
<div
|
|
162
171
|
style={{
|
|
163
172
|
position: 'absolute',
|
|
164
|
-
top: -
|
|
173
|
+
top: -11,
|
|
165
174
|
left: BADGE_EDGE_INSET,
|
|
166
175
|
zIndex: 1,
|
|
167
176
|
fontFamily: theme.fonts.monospace,
|
|
@@ -170,11 +179,11 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
|
|
|
170
179
|
textTransform: 'uppercase',
|
|
171
180
|
lineHeight: '17px',
|
|
172
181
|
whiteSpace: 'nowrap',
|
|
173
|
-
color,
|
|
182
|
+
color: badgeColor,
|
|
174
183
|
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
175
|
-
border: `
|
|
176
|
-
borderRadius:
|
|
177
|
-
padding: '
|
|
184
|
+
border: `2px solid ${badgeColor}`,
|
|
185
|
+
borderRadius: badgeRadius,
|
|
186
|
+
padding: '2px 8px',
|
|
178
187
|
}}
|
|
179
188
|
>
|
|
180
189
|
{constructBadgeLabel(c)}
|
|
@@ -187,7 +196,7 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
|
|
|
187
196
|
<div
|
|
188
197
|
style={{
|
|
189
198
|
position: 'absolute',
|
|
190
|
-
top: -
|
|
199
|
+
top: -11,
|
|
191
200
|
right: BADGE_EDGE_INSET,
|
|
192
201
|
zIndex: 1,
|
|
193
202
|
display: 'flex',
|
|
@@ -205,9 +214,9 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
|
|
|
205
214
|
lineHeight: '17px',
|
|
206
215
|
color: storageColor,
|
|
207
216
|
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
208
|
-
border: `
|
|
209
|
-
borderRadius:
|
|
210
|
-
padding: '
|
|
217
|
+
border: `2px solid ${storageColor}`,
|
|
218
|
+
borderRadius: badgeRadius,
|
|
219
|
+
padding: '2px 8px',
|
|
211
220
|
}}
|
|
212
221
|
>
|
|
213
222
|
{storageLabel}
|
|
@@ -223,9 +232,9 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
|
|
|
223
232
|
lineHeight: '17px',
|
|
224
233
|
color: topRightColor,
|
|
225
234
|
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
226
|
-
border: `
|
|
227
|
-
borderRadius:
|
|
228
|
-
padding: '
|
|
235
|
+
border: `2px solid ${topRightColor}`,
|
|
236
|
+
borderRadius: badgeRadius,
|
|
237
|
+
padding: '2px 8px',
|
|
229
238
|
}}
|
|
230
239
|
>
|
|
231
240
|
{topRightLabel}
|
|
@@ -241,7 +250,7 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
|
|
|
241
250
|
<div
|
|
242
251
|
style={{
|
|
243
252
|
position: 'absolute',
|
|
244
|
-
bottom: -
|
|
253
|
+
bottom: -14,
|
|
245
254
|
left: '50%',
|
|
246
255
|
transform: 'translateX(-50%)',
|
|
247
256
|
zIndex: 1,
|
|
@@ -254,9 +263,9 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
|
|
|
254
263
|
// borders and badges, not the file name
|
|
255
264
|
color: theme.colors.text ?? theme.colors.textSecondary,
|
|
256
265
|
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
257
|
-
border: `
|
|
258
|
-
borderRadius:
|
|
259
|
-
padding: '
|
|
266
|
+
border: `2px solid ${color}`,
|
|
267
|
+
borderRadius: badgeRadius,
|
|
268
|
+
padding: '2px 8px',
|
|
260
269
|
cursor: 'pointer',
|
|
261
270
|
}}
|
|
262
271
|
onClick={(e) => {
|
|
@@ -341,8 +350,21 @@ export function SubsystemGroupNode(props: NodeProps<Node<SubsystemGroupNodeData,
|
|
|
341
350
|
const color = packageColor(region?.key ?? 'process');
|
|
342
351
|
const dimmed = data.dimmed === true;
|
|
343
352
|
const hidden = (data as { hidden?: boolean }).hidden === true;
|
|
344
|
-
|
|
345
|
-
|
|
353
|
+
// Label is the region identity alone (path / process key / owner/name).
|
|
354
|
+
// Kind is carried by frame chrome (square = process; rounded = module/package)
|
|
355
|
+
// — don't prefix `module ·` / `package ·` or the path reads twice.
|
|
356
|
+
const label = region?.label ?? '';
|
|
357
|
+
// Process frames match external nodes — square corners (outside the soft
|
|
358
|
+
// component language). Module / package frames keep the rounded look.
|
|
359
|
+
const isProcess = region?.kind === 'process';
|
|
360
|
+
const frameRadius = isProcess ? 0 : 12;
|
|
361
|
+
const badgeRadius = isProcess ? 0 : 4;
|
|
362
|
+
// Processes are deployment units — solid frame. Modules/packages stay dashed
|
|
363
|
+
// until selected.
|
|
364
|
+
const frameStyle = isProcess || selected ? 'solid' : 'dashed';
|
|
365
|
+
// Process keeps a tinted fill; module / package bodies stay transparent so
|
|
366
|
+
// nested frames don't stack washes.
|
|
367
|
+
const frameFill = isProcess ? `${color}14` : 'transparent';
|
|
346
368
|
|
|
347
369
|
if (!region) return null;
|
|
348
370
|
|
|
@@ -353,9 +375,9 @@ export function SubsystemGroupNode(props: NodeProps<Node<SubsystemGroupNodeData,
|
|
|
353
375
|
width: width ?? 400,
|
|
354
376
|
height: height ?? 300,
|
|
355
377
|
boxSizing: 'border-box',
|
|
356
|
-
borderRadius:
|
|
357
|
-
border: `2px ${
|
|
358
|
-
background:
|
|
378
|
+
borderRadius: frameRadius,
|
|
379
|
+
border: `2px ${frameStyle} ${color}`,
|
|
380
|
+
background: frameFill,
|
|
359
381
|
opacity: hidden ? 0 : dimmed ? 0.35 : 1,
|
|
360
382
|
transition: 'opacity 150ms ease',
|
|
361
383
|
pointerEvents: 'none',
|
|
@@ -364,8 +386,14 @@ export function SubsystemGroupNode(props: NodeProps<Node<SubsystemGroupNodeData,
|
|
|
364
386
|
<div
|
|
365
387
|
style={{
|
|
366
388
|
position: 'absolute',
|
|
367
|
-
|
|
368
|
-
|
|
389
|
+
// Process: badge fill starts with the process fill (inside the
|
|
390
|
+
// frame border); no top badge border so widths don't fight.
|
|
391
|
+
// Module/package sit astride the top edge like component badges.
|
|
392
|
+
// Always set left/transform/borderTop explicitly — React Flow reuses
|
|
393
|
+
// group DOM nodes and `undefined` does not clear a prior value.
|
|
394
|
+
top: isProcess ? 0 : -19,
|
|
395
|
+
left: isProcess ? '50%' : 12,
|
|
396
|
+
transform: isProcess ? 'translateX(-50%)' : 'none',
|
|
369
397
|
fontFamily: theme.fonts.monospace,
|
|
370
398
|
fontSize: theme.fontSizes[3],
|
|
371
399
|
fontWeight: 700,
|
|
@@ -373,7 +401,8 @@ export function SubsystemGroupNode(props: NodeProps<Node<SubsystemGroupNodeData,
|
|
|
373
401
|
color,
|
|
374
402
|
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
375
403
|
border: `1px solid ${color}`,
|
|
376
|
-
|
|
404
|
+
borderTopWidth: isProcess ? 0 : 1,
|
|
405
|
+
borderRadius: badgeRadius,
|
|
377
406
|
padding: '1px 7px',
|
|
378
407
|
whiteSpace: 'nowrap',
|
|
379
408
|
}}
|