@principal-ai/subsystems-react 0.53.1 → 0.53.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.
@@ -14,6 +14,7 @@
14
14
  import { useEffect, useRef, useState } from 'react';
15
15
  import type { LucideIcon } from 'lucide-react';
16
16
  import {
17
+ AppWindow,
17
18
  ChevronDown,
18
19
  ChevronRight,
19
20
  CircleCheck,
@@ -32,7 +33,6 @@ import {
32
33
  Network,
33
34
  Route,
34
35
  Search,
35
- Server,
36
36
  Shapes,
37
37
  Sigma,
38
38
  Split,
@@ -82,7 +82,7 @@ export const SUBSYSTEM_ISSUE_CATEGORY_ICON: Record<
82
82
  repo: FolderGit2,
83
83
  construct: Component,
84
84
  'static-topology': Network,
85
- 'dynamic-topology': Server,
85
+ 'dynamic-topology': AppWindow,
86
86
  trail: Route,
87
87
  };
88
88
 
@@ -16,10 +16,126 @@ import {
16
16
  fractionAlongPath,
17
17
  planCompoundGroups,
18
18
  getElkOptions,
19
+ absoluteBoundsOf,
20
+ flowPositionOf,
19
21
  type Point,
20
22
  } from './elkLayout';
21
23
  import { EDGE_LABEL_WIDTH, EDGE_LABEL_SIDE_PADDING } from './edgeLabel';
22
24
 
25
+ /**
26
+ * A synthetic laid-out result standing in for the drill-down's open layout: a
27
+ * compound parent at (12,12) with two children, a sibling container, and a
28
+ * flattened frame that never enters the ELK tree.
29
+ */
30
+ function syntheticResult() {
31
+ const rect = (x: number, y: number, width: number, height: number) => ({
32
+ x, y, width, height,
33
+ });
34
+ return {
35
+ absoluteRects: new Map([
36
+ ['container:host', rect(12, 12, 508, 416)],
37
+ ['cmp:a', rect(24, 96, 140, 140)],
38
+ ['cmp:b', rect(24, 276, 140, 140)],
39
+ ['container:api', rect(576, 218, 250, 150)],
40
+ ]),
41
+ // A nested group's entry is parent-relative — the trap `flowPositionOf` avoids.
42
+ groupBounds: new Map([
43
+ ['container:host', rect(12, 12, 508, 416)],
44
+ ['container:api', rect(576, 218, 250, 150)],
45
+ ['system:acme', rect(-12, -52, 1152, 504)],
46
+ ]),
47
+ };
48
+ }
49
+
50
+ describe('absolute coordinate space', () => {
51
+ test('a root node keeps its absolute position', () => {
52
+ const r = syntheticResult();
53
+ expect(flowPositionOf(r, 'container:host')).toEqual({ x: 12, y: 12 });
54
+ expect(flowPositionOf(r, 'container:api')).toEqual({ x: 576, y: 218 });
55
+ });
56
+
57
+ // The bug this guards: a child's own position is already parent-relative, so
58
+ // subtracting the parent's absolute origin again pushed it out of its card.
59
+ test('a child is relative to its parent, offset exactly once', () => {
60
+ const r = syntheticResult();
61
+ expect(flowPositionOf(r, 'cmp:a', 'container:host')).toEqual({ x: 12, y: 84 });
62
+ expect(flowPositionOf(r, 'cmp:b', 'container:host')).toEqual({ x: 12, y: 264 });
63
+ });
64
+
65
+ test('children land inside the parent box they are nested in', () => {
66
+ const r = syntheticResult();
67
+ const box = absoluteBoundsOf(r, 'container:host')!;
68
+ for (const id of ['cmp:a', 'cmp:b']) {
69
+ const p = flowPositionOf(r, id, 'container:host')!;
70
+ const own = r.absoluteRects.get(id)!;
71
+ expect(p.x).toBeGreaterThanOrEqual(0);
72
+ expect(p.y).toBeGreaterThanOrEqual(0);
73
+ expect(p.x + own.width).toBeLessThanOrEqual(box.width);
74
+ expect(p.y + own.height).toBeLessThanOrEqual(box.height);
75
+ }
76
+ });
77
+
78
+ // A compound parent has no `result.nodes` entry at all, so anything reading
79
+ // positions from there leaves it on its authored slot.
80
+ test('a compound parent gets a real position, not a fallback', () => {
81
+ const r = syntheticResult();
82
+ expect(flowPositionOf(r, 'container:host')).not.toBeUndefined();
83
+ expect(flowPositionOf(r, 'container:host')).toEqual(
84
+ r.absoluteRects.get('container:host') && { x: 12, y: 12 },
85
+ );
86
+ });
87
+
88
+ test('a flattened frame resolves from the synthesized bounds', () => {
89
+ // Not in absoluteRects — it never entered the ELK tree.
90
+ expect(syntheticResult().absoluteRects.has('system:acme')).toBe(false);
91
+ expect(absoluteBoundsOf(syntheticResult(), 'system:acme')).toEqual({
92
+ x: -12, y: -52, width: 1152, height: 504,
93
+ });
94
+ });
95
+
96
+ test('an unknown id has no position rather than a silent zero', () => {
97
+ expect(flowPositionOf(syntheticResult(), 'nope')).toBeUndefined();
98
+ expect(absoluteBoundsOf(syntheticResult(), 'nope')).toBeUndefined();
99
+ });
100
+
101
+ test('an unknown parent falls back to absolute, not to a wrong origin', () => {
102
+ expect(flowPositionOf(syntheticResult(), 'cmp:a', 'nope')).toEqual({ x: 24, y: 96 });
103
+ });
104
+ });
105
+
106
+ /**
107
+ * A card's FLIP offset is the difference between where it was and where the new
108
+ * layout puts it. If this is computed in the wrong space the card slides to a
109
+ * plausible-looking wrong place, which is what "the anchors shift" looks like.
110
+ */
111
+ describe('morph offsets', () => {
112
+ interface Rect { x: number; y: number; width: number; height: number }
113
+ const offsetTo = (from: Rect, to: Rect) => ({
114
+ x: from.x - to.x,
115
+ y: from.y - to.y,
116
+ });
117
+
118
+ test('a card that moves is offset by exactly the move', () => {
119
+ // Closed: (330,182) 250x150. Open: the compound parent ELK fitted at (12,12).
120
+ const closed = { x: 330, y: 182, width: 250, height: 150 };
121
+ const open = { x: 12, y: 12, width: 508, height: 416 };
122
+ expect(offsetTo(closed, open)).toEqual({ x: 318, y: 170 });
123
+ });
124
+
125
+ test('a card that does not move gets no offset', () => {
126
+ const same = { x: 576, y: 218, width: 250, height: 150 };
127
+ expect(offsetTo(same, same)).toEqual({ x: 0, y: 0 });
128
+ });
129
+
130
+ test('an offset ignores size, because the card transitions its own size', () => {
131
+ // Growing in place must not shift the anchor: the card's width/height
132
+ // transition handles the size, a translate must stay 0.
133
+ const before = { x: 12, y: 12, width: 250, height: 150 };
134
+ const after = { x: 12, y: 12, width: 508, height: 416 };
135
+ expect(offsetTo(before, after)).toEqual({ x: 0, y: 0 });
136
+ });
137
+ });
138
+
23
139
  describe('planCompoundGroups', () => {
24
140
  test('drops a single-leaf group by default and promotes its member', () => {
25
141
  const plan = planCompoundGroups(
@@ -130,6 +246,21 @@ describe('planCompoundGroups', () => {
130
246
  );
131
247
  expect(plan.built[0]!.minWidth).toBe(220);
132
248
  });
249
+
250
+ // `padTop` only reaches ELK if the plan carries it; when it was dropped here
251
+ // the parent's own header was silently overlaid by its first child.
252
+ test('carries padTop through to the plan', () => {
253
+ const plan = planCompoundGroups(
254
+ [{ id: 'mod', memberIds: ['a', 'b'], padTop: 84 }],
255
+ ['a', 'b'],
256
+ );
257
+ expect(plan.built[0]!.padTop).toBe(84);
258
+ });
259
+
260
+ test('leaves padTop unset when the group does not reserve chrome', () => {
261
+ const plan = planCompoundGroups([{ id: 'mod', memberIds: ['a', 'b'] }], ['a', 'b']);
262
+ expect(plan.built[0]!.padTop).toBeUndefined();
263
+ });
133
264
  });
134
265
 
135
266
  describe('elkLayout helper functions', () => {
@@ -126,7 +126,7 @@ export interface ElkLayoutOptions {
126
126
  * nests this group under another group; omit for a root-level frame.
127
127
  * Members reference their immediate parent via React Flow `parentId`.
128
128
  */
129
- groups?: Array<{ id: string; memberIds: string[]; parentId?: string; minWidth?: number }>;
129
+ groups?: CompoundGroupDef[];
130
130
 
131
131
  /**
132
132
  * Keep groups that hold a single leaf instead of dropping them and
@@ -195,6 +195,46 @@ export interface ElkLayoutResult {
195
195
  absoluteRects: Map<string, { x: number; y: number; width: number; height: number }>;
196
196
  }
197
197
 
198
+ /**
199
+ * A node's rect in absolute (root) coordinates — the space `edgePathPoints` and
200
+ * the synthesized frame bounds are in.
201
+ *
202
+ * `absoluteRects` is the only map that is absolute for *every* node ELK placed.
203
+ * The alternatives each have a hole: `result.nodes` is parent-relative for a real
204
+ * group's children and has **no entry at all** for the group itself, and
205
+ * `groupBounds` is parent-relative for a nested group (and is the only source for
206
+ * a flattened group, which never enters the ELK tree).
207
+ */
208
+ export function absoluteBoundsOf(
209
+ result: Pick<ElkLayoutResult, 'absoluteRects' | 'groupBounds'>,
210
+ id: string,
211
+ ): { x: number; y: number; width: number; height: number } | undefined {
212
+ return result.absoluteRects.get(id) ?? result.groupBounds.get(id);
213
+ }
214
+
215
+ /**
216
+ * Where a node belongs in React Flow coordinates: a root node keeps its absolute
217
+ * position, a child is expressed relative to its parent.
218
+ *
219
+ * Convert in exactly one place, from the absolute space, subtracting the parent's
220
+ * absolute origin **once**. The two failure modes here are silent — a child
221
+ * double-offset by its parent's origin lands outside its own card, and a compound
222
+ * parent with no `result.nodes` entry silently keeps the authored position it was
223
+ * built with, so the container drifts away from its siblings, its frame and its
224
+ * edges while everything around it uses ELK's answer.
225
+ */
226
+ export function flowPositionOf(
227
+ result: Pick<ElkLayoutResult, 'absoluteRects' | 'groupBounds'>,
228
+ id: string,
229
+ parentId?: string,
230
+ ): { x: number; y: number } | undefined {
231
+ const abs = result.absoluteRects.get(id);
232
+ if (!abs) return undefined;
233
+ if (!parentId) return { x: abs.x, y: abs.y };
234
+ const origin = absoluteBoundsOf(result, parentId);
235
+ return origin ? { x: abs.x - origin.x, y: abs.y - origin.y } : { x: abs.x, y: abs.y };
236
+ }
237
+
198
238
  /** Point in 2D space */
199
239
  export interface Point {
200
240
  x: number;
@@ -526,20 +566,31 @@ export function getElkOptions(options: ElkLayoutOptions): LayoutOptions {
526
566
  export interface CompoundGroupDef {
527
567
  id: string;
528
568
  memberIds: string[];
569
+ /** Nested inside another group. Omitted for a root-level group. */
570
+ parentId?: string;
529
571
  minWidth?: number;
530
572
  /**
531
- * ELK partition for this group. Partitioning is activated on the root and
532
- * inherited, so a compound parent must be told its band explicitly —
533
- * otherwise it falls to partition 0 and lays out away from the siblings it is
534
- * supposed to sit among (which, for a flat frame around those siblings, drags
535
- * the frame across an unrelated band).
573
+ * This id is *also* a drawn node — a card that hosts a group. Set it when the
574
+ * element is drawn as a card but must be laid out as a parent: the drill-down's
575
+ * opened container.
576
+ *
577
+ * The node and the group are not alternatives. The node is how the group is
578
+ * *drawn*, and the group is how it is *laid out* — so this says which one wins
579
+ * when a member id could be either. Without it the frame takes the bare leaf box
580
+ * and the group is built but never referenced, leaving its components laid out
581
+ * nowhere: the container reads as closed, at its 250x150 card size, whatever
582
+ * ELK was told about its children.
536
583
  */
537
- partition?: number;
584
+ drawnAsCard?: boolean;
538
585
  /**
539
586
  * Space reserved above the first child, in px. Set it when the parent draws
540
587
  * its own chrome at the top (C4's grown container draws a label slot and a
541
588
  * technology row) so ELK still owns the fit — it pads the parent by this and
542
589
  * the children land below the chrome rather than under it.
590
+ *
591
+ * Note: there is deliberately no per-group partition. `partitionByNode` bands
592
+ * leaves, and ELK derives a compound node's band from its children, so a
593
+ * partition set on the parent itself is ignored.
543
594
  */
544
595
  padTop?: number;
545
596
  }
@@ -550,7 +601,7 @@ export interface CompoundGroupPlan {
550
601
  * Built groups in build order — a group's children always appear before it,
551
602
  * so callers can map ids to shells in one forward pass.
552
603
  */
553
- built: Array<{ id: string; childIds: string[]; minWidth?: number; padTop?: number; partition?: number }>;
604
+ built: Array<{ id: string; childIds: string[]; minWidth?: number; padTop?: number }>;
554
605
  /**
555
606
  * Dropped groups. Their members are promoted into the nearest built
556
607
  * ancestor, so callers must clear those members' `parentId`.
@@ -645,7 +696,14 @@ export function planCompoundGroups(
645
696
  continue;
646
697
  }
647
698
 
648
- built.push({ id: g.id, childIds, minWidth: g.minWidth });
699
+ built.push({
700
+ id: g.id,
701
+ childIds,
702
+ minWidth: g.minWidth,
703
+ // Carried through, not just validated here: the caller turns each of
704
+ // these into ELK layout options on the parent node.
705
+ padTop: g.padTop,
706
+ });
649
707
  }
650
708
  if (!progress) {
651
709
  // Cycle or unresolved refs — leave the rest unbuilt.
@@ -882,6 +940,9 @@ export async function computeElkLayout(
882
940
  const plan = planCompoundGroups(groupDefs, elkById.keys(), keepSingletonGroups);
883
941
  const skippedGroups = new Set(plan.skipped);
884
942
  const builtGroups = new Map<string, ElkNode>();
943
+ // Ids that are drawn as a card *and* laid out as a group. Such an id is in
944
+ // `elkById` too, so this is what says which of the two an enclosing frame takes.
945
+ const drawnAsCardIds = new Set(groupDefs.filter((g) => g.drawnAsCard).map((g) => g.id));
885
946
  // Flat groups are not ELK parents at all: their members lay out at the root,
886
947
  // and the frame is synthesized from the members' bounds after layout. This
887
948
  // keeps every edge on one spacing instead of inheriting a nested frame's.
@@ -909,11 +970,18 @@ export async function computeElkLayout(
909
970
  if (flatGroupIds.has(g.id)) continue;
910
971
  const children: ElkNode[] = [];
911
972
  for (const cid of g.childIds) {
912
- const leaf = elkById.get(cid);
913
- if (leaf) children.push(leaf);
973
+ // A member id can be a drawn node *and* a built group. Preferring the leaf —
974
+ // which is what this did unconditionally — silently drops the group: the
975
+ // frame ends up holding a bare 250x150 box, nothing references the group, and
976
+ // its components are laid out nowhere. So the card ids take the group, and
977
+ // everything else keeps the leaf. (For those, `builtGroups` has no entry
978
+ // anyway: flat groups are skipped above, and a nested group has no node.)
979
+ const nested = builtGroups.get(cid);
980
+ if (nested && drawnAsCardIds.has(cid)) children.push(nested);
914
981
  else {
915
- const nested = builtGroups.get(cid);
916
- if (nested) children.push(nested);
982
+ const leaf = elkById.get(cid);
983
+ if (leaf) children.push(leaf);
984
+ else if (nested) children.push(nested);
917
985
  }
918
986
  }
919
987
  const sizeOptions: Record<string, string> = {};
@@ -928,7 +996,6 @@ export async function computeElkLayout(
928
996
  // the parent's height still comes from ELK's fit.
929
997
  sizeOptions['elk.padding'] = `[top=${g.padTop},left=12,bottom=12,right=12]`;
930
998
  }
931
- if (g.partition != null) sizeOptions['partition'] = String(g.partition);
932
999
  builtGroups.set(g.id, {
933
1000
  id: g.id,
934
1001
  children,