@principal-ai/subsystems-react 0.51.4 → 0.53.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/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/subsystem/C4Graph.d.ts +3 -7
- package/dist/subsystem/C4Graph.d.ts.map +1 -1
- package/dist/subsystem/C4Graph.js +193 -47
- package/dist/subsystem/C4Graph.js.map +1 -1
- package/dist/subsystem/C4NodeCard.d.ts +33 -27
- package/dist/subsystem/C4NodeCard.d.ts.map +1 -1
- package/dist/subsystem/C4NodeCard.js +89 -43
- package/dist/subsystem/C4NodeCard.js.map +1 -1
- package/dist/subsystem/IssueList.d.ts +1 -1
- package/dist/subsystem/IssueList.d.ts.map +1 -1
- package/dist/subsystem/IssueList.js +20 -1
- package/dist/subsystem/IssueList.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +42 -19
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/c4.d.ts +48 -49
- package/dist/subsystem/c4.d.ts.map +1 -1
- package/dist/subsystem/c4.js +50 -0
- package/dist/subsystem/c4.js.map +1 -1
- package/dist/subsystem/c4Evidence.d.ts +88 -84
- package/dist/subsystem/c4Evidence.d.ts.map +1 -1
- package/dist/subsystem/c4Evidence.js +113 -166
- package/dist/subsystem/c4Evidence.js.map +1 -1
- package/dist/subsystem/model.d.ts +35 -245
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js +11 -29
- package/dist/subsystem/model.js.map +1 -1
- package/dist/subsystem/nodes.js +5 -5
- package/dist/subsystem/nodes.js.map +1 -1
- package/dist/subsystem/techIcons/index.d.ts +5 -0
- package/dist/subsystem/techIcons/index.d.ts.map +1 -1
- package/dist/subsystem/techIcons/index.js +3 -0
- package/dist/subsystem/techIcons/index.js.map +1 -1
- package/dist/utils/edgeLabel.d.ts +15 -1
- package/dist/utils/edgeLabel.d.ts.map +1 -1
- package/dist/utils/edgeLabel.js +25 -1
- package/dist/utils/edgeLabel.js.map +1 -1
- package/dist/utils/elkLayout.d.ts +47 -0
- package/dist/utils/elkLayout.d.ts.map +1 -1
- package/dist/utils/elkLayout.js +71 -7
- package/dist/utils/elkLayout.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +8 -6
- package/src/stories/Subsystem/C4Graph/C4Component.stories.tsx +198 -0
- package/src/stories/Subsystem/C4Graph/C4Container.stories.tsx +42 -6
- package/src/stories/Subsystem/C4Graph/C4NodeGallery.stories.tsx +9 -7
- package/src/stories/Subsystem/ComponentGraph/Issues.stories.tsx +686 -2
- package/src/subsystem/C4Graph.tsx +278 -82
- package/src/subsystem/C4NodeCard.test.ts +32 -22
- package/src/subsystem/C4NodeCard.tsx +134 -69
- package/src/subsystem/IssueList.tsx +23 -0
- package/src/subsystem/SubsystemComponentGraph.tsx +38 -16
- package/src/subsystem/c4.protocolColor.test.ts +26 -0
- package/src/subsystem/c4.ts +76 -52
- package/src/subsystem/c4Evidence.test.ts +177 -237
- package/src/subsystem/c4Evidence.ts +160 -235
- package/src/subsystem/model.ts +62 -305
- package/src/subsystem/nodes.group.test.tsx +15 -0
- package/src/subsystem/nodes.tsx +7 -7
- package/src/subsystem/techIcons/index.tsx +8 -0
- package/src/utils/edgeLabel.test.ts +21 -2
- package/src/utils/edgeLabel.ts +29 -1
- package/src/utils/elkLayout.test.ts +16 -0
- package/src/utils/elkLayout.ts +119 -7
- package/src/stories/Subsystem/ComponentGraph/IssueOverlay.stories.tsx +0 -602
package/src/utils/elkLayout.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type { Node, Edge } from '@xyflow/react';
|
|
|
10
10
|
import {
|
|
11
11
|
EDGE_LABEL_WIDTH,
|
|
12
12
|
EDGE_LABEL_HEIGHT,
|
|
13
|
-
|
|
13
|
+
EDGE_LABEL_SIDE_PADDING,
|
|
14
14
|
} from './edgeLabel';
|
|
15
15
|
|
|
16
16
|
/** ELK layout options for different routing styles */
|
|
@@ -76,6 +76,22 @@ export interface ElkLayoutOptions {
|
|
|
76
76
|
enabled?: boolean;
|
|
77
77
|
/** Placement of inline labels. @default 'CENTER' */
|
|
78
78
|
placement?: 'CENTER' | 'TAIL' | 'HEAD';
|
|
79
|
+
/**
|
|
80
|
+
* Width of the reserved label box, in flow px. This is what actually sets
|
|
81
|
+
* the between-layer run for a labelled edge — ELK's CENTER_LAYER creates a
|
|
82
|
+
* label layer of this width, which dominates the layering spacing. Defaults
|
|
83
|
+
* to the shared `EDGE_LABEL_WIDTH`; a caller with shorter labels passes a
|
|
84
|
+
* smaller value to tighten its edges.
|
|
85
|
+
*/
|
|
86
|
+
width?: number;
|
|
87
|
+
/**
|
|
88
|
+
* Per-label width, computed from the label text. When supplied, wins over
|
|
89
|
+
* `width` for each edge, so the reserved box tracks content instead of one
|
|
90
|
+
* fixed width. The overlay must use the same function.
|
|
91
|
+
*/
|
|
92
|
+
measure?: (text: string) => number;
|
|
93
|
+
/** Height of the reserved label box. Defaults to `EDGE_LABEL_HEIGHT`. */
|
|
94
|
+
height?: number;
|
|
79
95
|
};
|
|
80
96
|
|
|
81
97
|
/**
|
|
@@ -121,6 +137,34 @@ export interface ElkLayoutOptions {
|
|
|
121
137
|
* on the duplicate. @default false
|
|
122
138
|
*/
|
|
123
139
|
keepSingletonGroups?: boolean;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Lay a group's members out at the root instead of nesting them inside an
|
|
143
|
+
* ELK parent, then synthesize the frame's bounds as the bounding box of its
|
|
144
|
+
* members.
|
|
145
|
+
*
|
|
146
|
+
* A nested frame is a real ELK parent, so edges wholly inside it inherit the
|
|
147
|
+
* frame's padding and its own between-layer spacing on top — which makes an
|
|
148
|
+
* in-frame edge run longer than a root-level one touching the same nodes.
|
|
149
|
+
* Flattening removes that: every edge uses one spacing, and the frame is pure
|
|
150
|
+
* decoration drawn around wherever the members landed.
|
|
151
|
+
*
|
|
152
|
+
* Only for groups whose members need no parent-relative positioning (C4's
|
|
153
|
+
* system frame). @default false
|
|
154
|
+
*/
|
|
155
|
+
flatGroups?: boolean;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* ELK partition per node id. Nodes are banded by `partition` — ELK places all
|
|
159
|
+
* of partition 0, then partition 1, etc., in the layout direction — so a
|
|
160
|
+
* group of nodes can be kept apart from the rest without nesting them in a
|
|
161
|
+
* parent. C4 uses this to push out-of-boundary nodes (external systems,
|
|
162
|
+
* people) into their own band, away from the framed containers.
|
|
163
|
+
*
|
|
164
|
+
* Requires every laid-out node to have a partition; unknown ids are ignored,
|
|
165
|
+
* unlisted nodes get partition 0. @default undefined (partitioning off)
|
|
166
|
+
*/
|
|
167
|
+
partitionByNode?: ReadonlyMap<string, number>;
|
|
124
168
|
}
|
|
125
169
|
|
|
126
170
|
/** Result of ELK layout computation */
|
|
@@ -397,7 +441,7 @@ export function fractionAlongPath(points: Point[], at: Point): number {
|
|
|
397
441
|
/**
|
|
398
442
|
* Get ELK layout options based on configuration
|
|
399
443
|
*/
|
|
400
|
-
function getElkOptions(options: ElkLayoutOptions): LayoutOptions {
|
|
444
|
+
export function getElkOptions(options: ElkLayoutOptions): LayoutOptions {
|
|
401
445
|
const {
|
|
402
446
|
routingStyle = 'orthogonal',
|
|
403
447
|
nodeSpacing = 50,
|
|
@@ -421,6 +465,12 @@ function getElkOptions(options: ElkLayoutOptions): LayoutOptions {
|
|
|
421
465
|
'elk.spacing.edgeNode': String(edgeNodeSpacing),
|
|
422
466
|
'elk.layered.spacing.edgeEdgeBetweenLayers': String(edgeSpacing),
|
|
423
467
|
'elk.layered.spacing.edgeNodeBetweenLayers': String(edgeNodeSpacing),
|
|
468
|
+
// Edges host inline labels and `CENTER_LAYER` already reserves a label
|
|
469
|
+
// layer, so this is EXTRA space stacked on top of that layer — not a
|
|
470
|
+
// replacement for it. Reserving the full label box here double-counts and
|
|
471
|
+
// the run balloons (measured: w=140 + 204 here = 548px between two nodes).
|
|
472
|
+
// `interLayerSpacing` is the only caller-set value; otherwise this is the
|
|
473
|
+
// small clearance the label layer can't express.
|
|
424
474
|
'elk.layered.spacing.nodeNodeBetweenLayers': String(interLayerSpacing),
|
|
425
475
|
// Port constraints - edges connect at specific sides
|
|
426
476
|
'elk.portConstraints': 'FIXED_SIDE',
|
|
@@ -598,7 +648,12 @@ export async function computeElkLayout(
|
|
|
598
648
|
edges: Edge[],
|
|
599
649
|
options: ElkLayoutOptions = {}
|
|
600
650
|
): Promise<ElkLayoutResult> {
|
|
601
|
-
const {
|
|
651
|
+
const {
|
|
652
|
+
preserveNodePositions = true,
|
|
653
|
+
keepSingletonGroups = false,
|
|
654
|
+
flatGroups = false,
|
|
655
|
+
partitionByNode,
|
|
656
|
+
} = options;
|
|
602
657
|
const edgeLabels = options.edgeLabels;
|
|
603
658
|
const endpointInset = options.endpointInset ?? 0;
|
|
604
659
|
const direction = options.direction ?? 'RIGHT';
|
|
@@ -648,6 +703,7 @@ export async function computeElkLayout(
|
|
|
648
703
|
properties: {
|
|
649
704
|
'portConstraints': 'FIXED_SIDE',
|
|
650
705
|
...(layer !== undefined ? { 'layering.layer': String(layer) } : {}),
|
|
706
|
+
...(partitionByNode ? { partition: String(partitionByNode.get(node.id) ?? 0) } : {}),
|
|
651
707
|
},
|
|
652
708
|
};
|
|
653
709
|
});
|
|
@@ -745,11 +801,20 @@ export async function computeElkLayout(
|
|
|
745
801
|
sources: [sourcePort],
|
|
746
802
|
targets: [targetPort],
|
|
747
803
|
};
|
|
748
|
-
// Reserve the
|
|
749
|
-
//
|
|
804
|
+
// Reserve the label box. ELK's `CENTER_LAYER` sizes a dedicated label layer
|
|
805
|
+
// from this and adds no clearance of its own, so the box carries the chip
|
|
806
|
+
// width PLUS the clearance we want on each side — that is the only place
|
|
807
|
+
// the side padding can take effect. The overlay draws the chip at the
|
|
808
|
+
// narrower `edgeLabels.width`, centred in this reserved run.
|
|
750
809
|
if (edgeLabels?.enabled !== false && typeof edge.label === 'string') {
|
|
810
|
+
const chipWidth =
|
|
811
|
+
edgeLabels?.measure?.(edge.label) ?? edgeLabels?.width ?? EDGE_LABEL_WIDTH;
|
|
751
812
|
elkEdge.labels = [
|
|
752
|
-
{
|
|
813
|
+
{
|
|
814
|
+
text: edge.label,
|
|
815
|
+
width: chipWidth + EDGE_LABEL_SIDE_PADDING * 2,
|
|
816
|
+
height: edgeLabels?.height ?? EDGE_LABEL_HEIGHT,
|
|
817
|
+
},
|
|
753
818
|
];
|
|
754
819
|
}
|
|
755
820
|
return elkEdge;
|
|
@@ -778,7 +843,9 @@ export async function computeElkLayout(
|
|
|
778
843
|
// the full reserved width (label box + clearance at each end); labels off
|
|
779
844
|
// falls back to a plain between-layer gap.
|
|
780
845
|
'elk.layered.spacing.nodeNodeBetweenLayers': String(
|
|
781
|
-
edgeLabels?.enabled === false
|
|
846
|
+
edgeLabels?.enabled === false
|
|
847
|
+
? 40
|
|
848
|
+
: (edgeLabels?.width ?? EDGE_LABEL_WIDTH) + EDGE_LABEL_SIDE_PADDING * 2,
|
|
782
849
|
),
|
|
783
850
|
};
|
|
784
851
|
// Match the root's inline-label reservation, so ELK inside the frame leaves a
|
|
@@ -793,7 +860,15 @@ export async function computeElkLayout(
|
|
|
793
860
|
const plan = planCompoundGroups(groupDefs, elkById.keys(), keepSingletonGroups);
|
|
794
861
|
const skippedGroups = new Set(plan.skipped);
|
|
795
862
|
const builtGroups = new Map<string, ElkNode>();
|
|
863
|
+
// Flat groups are not ELK parents at all: their members lay out at the root,
|
|
864
|
+
// and the frame is synthesized from the members' bounds after layout. This
|
|
865
|
+
// keeps every edge on one spacing instead of inheriting a nested frame's.
|
|
866
|
+
const flatGroupIds = new Set<string>();
|
|
867
|
+
if (flatGroups) {
|
|
868
|
+
for (const g of plan.built) flatGroupIds.add(g.id);
|
|
869
|
+
}
|
|
796
870
|
for (const g of plan.built) {
|
|
871
|
+
if (flatGroups) break;
|
|
797
872
|
const children: ElkNode[] = [];
|
|
798
873
|
for (const cid of g.childIds) {
|
|
799
874
|
const leaf = elkById.get(cid);
|
|
@@ -857,6 +932,9 @@ export async function computeElkLayout(
|
|
|
857
932
|
|
|
858
933
|
// Create ELK graph
|
|
859
934
|
const rootOptions = getElkOptions(options);
|
|
935
|
+
if (partitionByNode) {
|
|
936
|
+
rootOptions['elk.partitioning.activate'] = 'true';
|
|
937
|
+
}
|
|
860
938
|
if (elkParents.length > 0 || builtGroups.size > 0) {
|
|
861
939
|
rootOptions['elk.hierarchyHandling'] = 'INCLUDE_CHILDREN';
|
|
862
940
|
}
|
|
@@ -929,6 +1007,40 @@ export async function computeElkLayout(
|
|
|
929
1007
|
}
|
|
930
1008
|
}
|
|
931
1009
|
|
|
1010
|
+
// Flat groups: synthesize each frame from the bounding box of its members.
|
|
1011
|
+
// `plan.built` is bottom-up, so a nested frame's bounds already exist when its
|
|
1012
|
+
// parent is computed — a frame containing frames unions the child frames, not
|
|
1013
|
+
// just leaves. Without that, an outer frame (the system) would omit the
|
|
1014
|
+
// container frames inside it and mis-size.
|
|
1015
|
+
if (flatGroupIds.size > 0) {
|
|
1016
|
+
const FLAT_PAD = 24;
|
|
1017
|
+
const FLAT_TOP = 64;
|
|
1018
|
+
/** Absolute rect of a group member: a leaf rect, or an already-built frame. */
|
|
1019
|
+
const rectOf = (id: string) =>
|
|
1020
|
+
absoluteRects.get(id) ??
|
|
1021
|
+
(() => {
|
|
1022
|
+
const b = groupBounds.get(id);
|
|
1023
|
+
return b ? { x: b.x, y: b.y, width: b.width, height: b.height } : undefined;
|
|
1024
|
+
})();
|
|
1025
|
+
for (const g of plan.built) {
|
|
1026
|
+
if (!flatGroupIds.has(g.id)) continue;
|
|
1027
|
+
const rects = g.childIds
|
|
1028
|
+
.map(rectOf)
|
|
1029
|
+
.filter((r): r is { x: number; y: number; width: number; height: number } => !!r);
|
|
1030
|
+
if (rects.length === 0) continue;
|
|
1031
|
+
const minX = Math.min(...rects.map((r) => r.x));
|
|
1032
|
+
const minY = Math.min(...rects.map((r) => r.y));
|
|
1033
|
+
const maxX = Math.max(...rects.map((r) => r.x + r.width));
|
|
1034
|
+
const maxY = Math.max(...rects.map((r) => r.y + r.height));
|
|
1035
|
+
groupBounds.set(g.id, {
|
|
1036
|
+
x: minX - FLAT_PAD,
|
|
1037
|
+
y: minY - FLAT_TOP,
|
|
1038
|
+
width: maxX - minX + FLAT_PAD * 2,
|
|
1039
|
+
height: maxY - minY + FLAT_TOP + FLAT_PAD,
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
932
1044
|
// Extract results
|
|
933
1045
|
const edgePaths = new Map<string, string>();
|
|
934
1046
|
const edgeLabelPositions = new Map<string, { x: number; y: number }>();
|