@principal-ai/subsystems-react 0.53.1 → 0.53.3

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.
Files changed (42) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +1 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/subsystem/C4Graph.d.ts +83 -0
  6. package/dist/subsystem/C4Graph.d.ts.map +1 -1
  7. package/dist/subsystem/C4Graph.js +314 -105
  8. package/dist/subsystem/C4Graph.js.map +1 -1
  9. package/dist/subsystem/C4NodeCard.d.ts +45 -10
  10. package/dist/subsystem/C4NodeCard.d.ts.map +1 -1
  11. package/dist/subsystem/C4NodeCard.js +75 -21
  12. package/dist/subsystem/C4NodeCard.js.map +1 -1
  13. package/dist/subsystem/IssueList.js +2 -2
  14. package/dist/subsystem/IssueList.js.map +1 -1
  15. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  16. package/dist/subsystem/SubsystemComponentGraph.js +24 -7
  17. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  18. package/dist/subsystem/model.d.ts +13 -2
  19. package/dist/subsystem/model.d.ts.map +1 -1
  20. package/dist/subsystem/model.js.map +1 -1
  21. package/dist/subsystem/nodes.d.ts.map +1 -1
  22. package/dist/subsystem/nodes.js +14 -6
  23. package/dist/subsystem/nodes.js.map +1 -1
  24. package/dist/utils/elkLayout.d.ts +49 -13
  25. package/dist/utils/elkLayout.d.ts.map +1 -1
  26. package/dist/utils/elkLayout.js +57 -8
  27. package/dist/utils/elkLayout.js.map +1 -1
  28. package/package.json +1 -1
  29. package/src/index.ts +1 -1
  30. package/src/stories/Subsystem/C4Graph/C4NodeGallery.stories.tsx +1 -1
  31. package/src/stories/Subsystem/C4Graph/C4Transition.stories.tsx +2 -1
  32. package/src/subsystem/C4Graph.groups.test.ts +217 -0
  33. package/src/subsystem/C4Graph.morph.test.tsx +208 -0
  34. package/src/subsystem/C4Graph.tsx +344 -111
  35. package/src/subsystem/C4NodeCard.test.ts +47 -8
  36. package/src/subsystem/C4NodeCard.tsx +84 -20
  37. package/src/subsystem/IssueList.tsx +2 -2
  38. package/src/subsystem/SubsystemComponentGraph.tsx +25 -7
  39. package/src/subsystem/model.ts +13 -2
  40. package/src/subsystem/nodes.tsx +14 -6
  41. package/src/utils/elkLayout.test.ts +131 -0
  42. package/src/utils/elkLayout.ts +81 -14
@@ -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,