@principal-ai/subsystems-react 0.53.6 → 0.54.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.
Files changed (104) hide show
  1. package/dist/index.d.ts +8 -2
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +3 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/pierre/PierreTrailCodeView.d.ts.map +1 -1
  6. package/dist/pierre/PierreTrailCodeView.js +7 -5
  7. package/dist/pierre/PierreTrailCodeView.js.map +1 -1
  8. package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts.map +1 -1
  9. package/dist/stories/Subsystem/ComponentGraph/fixtures.js +1 -2
  10. package/dist/stories/Subsystem/ComponentGraph/fixtures.js.map +1 -1
  11. package/dist/subsystem/C4Graph.d.ts +144 -9
  12. package/dist/subsystem/C4Graph.d.ts.map +1 -1
  13. package/dist/subsystem/C4Graph.js +924 -148
  14. package/dist/subsystem/C4Graph.js.map +1 -1
  15. package/dist/subsystem/C4NodeCard.d.ts +22 -1
  16. package/dist/subsystem/C4NodeCard.d.ts.map +1 -1
  17. package/dist/subsystem/C4NodeCard.js +10 -2
  18. package/dist/subsystem/C4NodeCard.js.map +1 -1
  19. package/dist/subsystem/ComponentDeclaration.d.ts +5 -4
  20. package/dist/subsystem/ComponentDeclaration.d.ts.map +1 -1
  21. package/dist/subsystem/ComponentDeclaration.js +12 -4
  22. package/dist/subsystem/ComponentDeclaration.js.map +1 -1
  23. package/dist/subsystem/ConstructsCatalog.d.ts.map +1 -1
  24. package/dist/subsystem/ConstructsCatalog.js +10 -7
  25. package/dist/subsystem/ConstructsCatalog.js.map +1 -1
  26. package/dist/subsystem/GraphSidebar.d.ts +71 -0
  27. package/dist/subsystem/GraphSidebar.d.ts.map +1 -0
  28. package/dist/subsystem/GraphSidebar.js +145 -0
  29. package/dist/subsystem/GraphSidebar.js.map +1 -0
  30. package/dist/subsystem/GraphWithSidebar.d.ts +24 -0
  31. package/dist/subsystem/GraphWithSidebar.d.ts.map +1 -0
  32. package/dist/subsystem/GraphWithSidebar.js +25 -0
  33. package/dist/subsystem/GraphWithSidebar.js.map +1 -0
  34. package/dist/subsystem/SubsystemComponentGraph.d.ts +33 -0
  35. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  36. package/dist/subsystem/SubsystemComponentGraph.js +364 -411
  37. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  38. package/dist/subsystem/TrailStepsNav.d.ts +48 -0
  39. package/dist/subsystem/TrailStepsNav.d.ts.map +1 -0
  40. package/dist/subsystem/TrailStepsNav.js +189 -0
  41. package/dist/subsystem/TrailStepsNav.js.map +1 -0
  42. package/dist/subsystem/TrailsPanel.d.ts.map +1 -1
  43. package/dist/subsystem/TrailsPanel.js +3 -4
  44. package/dist/subsystem/TrailsPanel.js.map +1 -1
  45. package/dist/subsystem/__fixtures__/c4Drilldown.d.ts +151 -0
  46. package/dist/subsystem/__fixtures__/c4Drilldown.d.ts.map +1 -0
  47. package/dist/subsystem/__fixtures__/c4Drilldown.js +783 -0
  48. package/dist/subsystem/__fixtures__/c4Drilldown.js.map +1 -0
  49. package/dist/subsystem/c4.d.ts +39 -8
  50. package/dist/subsystem/c4.d.ts.map +1 -1
  51. package/dist/subsystem/c4.js.map +1 -1
  52. package/dist/subsystem/declarationRef.d.ts +26 -18
  53. package/dist/subsystem/declarationRef.d.ts.map +1 -1
  54. package/dist/subsystem/declarationRef.js +9 -4
  55. package/dist/subsystem/declarationRef.js.map +1 -1
  56. package/dist/subsystem/model.d.ts +19 -1
  57. package/dist/subsystem/model.d.ts.map +1 -1
  58. package/dist/subsystem/model.js +12 -2
  59. package/dist/subsystem/model.js.map +1 -1
  60. package/dist/subsystem/nodes.d.ts.map +1 -1
  61. package/dist/subsystem/nodes.js +24 -4
  62. package/dist/subsystem/nodes.js.map +1 -1
  63. package/dist/subsystem/trailBrief.d.ts.map +1 -1
  64. package/dist/subsystem/trailBrief.js +6 -1
  65. package/dist/subsystem/trailBrief.js.map +1 -1
  66. package/package.json +3 -3
  67. package/src/index.ts +9 -2
  68. package/src/pierre/PierreTrailCodeView.tsx +9 -13
  69. package/src/stories/Subsystem/C4Graph/C4ToComponentDrill.stories.tsx +496 -0
  70. package/src/stories/Subsystem/ComponentGraph/AnimatedProgression.stories.tsx +7 -7
  71. package/src/stories/Subsystem/ComponentGraph/Constructs.stories.tsx +19 -25
  72. package/src/stories/Subsystem/ComponentGraph/DetailPanel.stories.tsx +8 -10
  73. package/src/stories/Subsystem/ComponentGraph/Issues.stories.tsx +19 -19
  74. package/src/stories/Subsystem/ComponentGraph/ProposedTrails.stories.tsx +6 -6
  75. package/src/stories/Subsystem/ComponentGraph/TrailAutoplay.stories.tsx +7 -7
  76. package/src/stories/Subsystem/ComponentGraph/Trails.stories.tsx +27 -30
  77. package/src/stories/Subsystem/ComponentGraph/fixtures.ts +1 -2
  78. package/src/stories/Subsystem/ConstructsCatalog.stories.tsx +10 -13
  79. package/src/stories/Subsystem/EgoGraph/EgoGraph.stories.tsx +3 -4
  80. package/src/stories/Subsystem/Sidebar/GraphSidebar.stories.tsx +206 -0
  81. package/src/subsystem/C4Graph.groups.test.ts +96 -2
  82. package/src/subsystem/C4Graph.tsx +1377 -165
  83. package/src/subsystem/C4NodeCard.test.ts +1 -2
  84. package/src/subsystem/C4NodeCard.tsx +24 -2
  85. package/src/subsystem/ComponentDeclaration.tsx +18 -8
  86. package/src/subsystem/ConstructsCatalog.tsx +23 -8
  87. package/src/subsystem/GraphSidebar.test.tsx +152 -0
  88. package/src/subsystem/GraphSidebar.tsx +300 -0
  89. package/src/subsystem/GraphWithSidebar.test.tsx +78 -0
  90. package/src/subsystem/GraphWithSidebar.tsx +60 -0
  91. package/src/subsystem/SubsystemComponentGraph.tsx +284 -370
  92. package/src/subsystem/TrailStepsNav.test.tsx +179 -0
  93. package/src/subsystem/TrailStepsNav.tsx +372 -0
  94. package/src/subsystem/TrailsPanel.tsx +3 -4
  95. package/src/subsystem/__fixtures__/c4Drilldown.test.ts +338 -0
  96. package/src/subsystem/__fixtures__/c4Drilldown.ts +847 -0
  97. package/src/subsystem/c4.ts +41 -8
  98. package/src/subsystem/declarationRef.ts +28 -19
  99. package/src/subsystem/model.test.ts +32 -12
  100. package/src/subsystem/model.ts +30 -1
  101. package/src/subsystem/nodes.group.test.tsx +82 -0
  102. package/src/subsystem/nodes.tsx +30 -2
  103. package/src/subsystem/trailBrief.test.ts +2 -2
  104. package/src/subsystem/trailBrief.ts +6 -1
@@ -0,0 +1,206 @@
1
+ import React, { useMemo, useState } from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
4
+ import { GraphSidebar } from '../../../subsystem/GraphSidebar';
5
+ import { TrailsPanel } from '../../../subsystem/TrailsPanel';
6
+ import {
7
+ DRILLDOWN_COMPONENTS,
8
+ DRILLDOWN_DOC,
9
+ DRILLDOWN_TRAILS,
10
+ } from '../../../subsystem/__fixtures__/c4Drilldown';
11
+
12
+ /**
13
+ * `GraphSidebar` — the side panel extracted out of `SubsystemComponentGraph`,
14
+ * on its own. It owns the chrome and nothing else: the host supplies the
15
+ * header / tabs / body / footer, and the panel owns the resize handle, the
16
+ * width, and its persistence.
17
+ *
18
+ * Everything below is the real reviewed model `sg-1788913589907-ieokymqg9`
19
+ * ("Gist share from Studio") the drill-down story is built on — the same
20
+ * header the component graph shows, the same `TrailsPanel` body, the same
21
+ * trails — so the panel is exercised against the content it actually carries,
22
+ * not a mock. Drag the right edge to resize (the width persists); switch to
23
+ * the Files tab for the component file inventory.
24
+ */
25
+
26
+ const META_DESCRIPTION =
27
+ 'The subsystem the C4 view summarizes. Focused trails are the implementation of the selected C4 line.';
28
+
29
+ /** Distinct, real component file paths — the inventory the Files tab lists. */
30
+ const COMPONENT_FILES = [
31
+ ...new Set(DRILLDOWN_COMPONENTS.map((c) => c.file).filter(Boolean)),
32
+ ].sort();
33
+
34
+ const STEP_COUNT = DRILLDOWN_TRAILS.reduce((n, t) => n + t.steps.length, 0);
35
+
36
+ function SidebarHeader() {
37
+ return (
38
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
39
+ <span style={{ fontFamily: 'monospace', fontWeight: 600, fontSize: 13, color: '#ddd' }}>
40
+ {DRILLDOWN_DOC.title}
41
+ </span>
42
+ <span style={{ fontSize: 12, color: '#777', lineHeight: 1.5 }}>{META_DESCRIPTION}</span>
43
+ </div>
44
+ );
45
+ }
46
+
47
+ function FilesBody() {
48
+ return (
49
+ <div style={{ flex: 1, minHeight: 0, overflowY: 'auto', padding: '8px 4px' }}>
50
+ {COMPONENT_FILES.map((file) => (
51
+ <div
52
+ key={file}
53
+ title={file}
54
+ style={{
55
+ fontFamily: 'monospace',
56
+ fontSize: 11,
57
+ color: '#999',
58
+ padding: '4px 8px',
59
+ whiteSpace: 'nowrap',
60
+ overflow: 'hidden',
61
+ textOverflow: 'ellipsis',
62
+ }}
63
+ >
64
+ {file}
65
+ </div>
66
+ ))}
67
+ </div>
68
+ );
69
+ }
70
+
71
+ function SidebarFooter() {
72
+ return (
73
+ <div
74
+ style={{
75
+ flexShrink: 0,
76
+ borderTop: `1px solid ${defaultEditorTheme.colors.border}`,
77
+ padding: '6px 12px',
78
+ fontFamily: 'monospace',
79
+ fontSize: 11,
80
+ color: '#777',
81
+ }}
82
+ >
83
+ {DRILLDOWN_TRAILS.length} trails · {STEP_COUNT} steps
84
+ </div>
85
+ );
86
+ }
87
+
88
+ function PanelDemo({ controlled = false }: { controlled?: boolean }) {
89
+ const [tab, setTab] = useState('trails');
90
+ const [expanded, setExpanded] = useState<Set<string>>(new Set());
91
+ const [focusedTrailId, setFocusedTrailId] = useState<string | null>(null);
92
+ const [focusedStepIndex, setFocusedStepIndex] = useState<number | null>(null);
93
+ const [hoveredTrailStep, setHoveredTrailStep] = useState<{
94
+ trailId: string;
95
+ stepIndex: number | null;
96
+ } | null>(null);
97
+ const [width, setWidth] = useState(360);
98
+
99
+ const body = useMemo(() => {
100
+ if (tab === 'files') return <FilesBody />;
101
+ return (
102
+ <TrailsPanel
103
+ trails={DRILLDOWN_TRAILS}
104
+ expandedTrails={expanded}
105
+ focusedTrailId={focusedTrailId}
106
+ focusedStepIndex={focusedStepIndex}
107
+ hoveredTrailStep={hoveredTrailStep}
108
+ onToggleCollapsed={(id) =>
109
+ setExpanded((prev) => {
110
+ const next = new Set(prev);
111
+ if (next.has(id)) next.delete(id);
112
+ else next.add(id);
113
+ return next;
114
+ })
115
+ }
116
+ onFocusFlow={(tl) => {
117
+ setFocusedTrailId(tl.id);
118
+ setFocusedStepIndex(null);
119
+ }}
120
+ onClearFocus={() => {
121
+ setFocusedTrailId(null);
122
+ setFocusedStepIndex(null);
123
+ }}
124
+ onFocusStep={(tl, stepIndex) => {
125
+ setFocusedTrailId(tl.id);
126
+ setFocusedStepIndex(stepIndex);
127
+ }}
128
+ onHoverStep={(tl, stepIndex) => setHoveredTrailStep({ trailId: tl.id, stepIndex })}
129
+ onHoverFlow={(tl) => setHoveredTrailStep({ trailId: tl.id, stepIndex: null })}
130
+ onLeaveStep={() => setHoveredTrailStep(null)}
131
+ />
132
+ );
133
+ }, [tab, expanded, focusedTrailId, focusedStepIndex, hoveredTrailStep]);
134
+
135
+ return (
136
+ <div
137
+ style={{
138
+ width: '100%',
139
+ height: '100vh',
140
+ display: 'flex',
141
+ flexDirection: 'row',
142
+ background: defaultEditorTheme.colors.background,
143
+ fontFamily: 'monospace',
144
+ }}
145
+ >
146
+ <GraphSidebar
147
+ header={<SidebarHeader />}
148
+ tabs={[
149
+ { id: 'trails', label: 'Trails' },
150
+ { id: 'files', label: 'Files' },
151
+ ]}
152
+ activeTab={tab}
153
+ onTabChange={setTab}
154
+ sidebarBody={body}
155
+ footer={<SidebarFooter />}
156
+ persistKey={controlled ? undefined : 'sidebar-story'}
157
+ {...(controlled ? { width, onWidthChange: setWidth } : {})}
158
+ />
159
+ <div
160
+ style={{
161
+ flex: 1,
162
+ minWidth: 0,
163
+ display: 'flex',
164
+ alignItems: 'center',
165
+ justifyContent: 'center',
166
+ color: '#555',
167
+ fontSize: 12,
168
+ }}
169
+ >
170
+ {controlled
171
+ ? `controlled width: ${width}px — drag the handle; the host owns the value`
172
+ : 'the panel on its own — the graph would render to the right'}
173
+ </div>
174
+ </div>
175
+ );
176
+ }
177
+
178
+ const meta = {
179
+ title: 'Subsystem/Sidebar/GraphSidebar',
180
+ component: GraphSidebar,
181
+ parameters: { layout: 'fullscreen' },
182
+ tags: ['autodocs'],
183
+ decorators: [
184
+ (Story) => (
185
+ <ThemeProvider theme={defaultEditorTheme}>
186
+ <Story />
187
+ </ThemeProvider>
188
+ ),
189
+ ],
190
+ } satisfies Meta<typeof GraphSidebar>;
191
+
192
+ export default meta;
193
+ type Story = StoryObj<typeof meta>;
194
+
195
+ /** The panel with no graph beside it: real header, Trails/Files tabs, the real
196
+ * `TrailsPanel` body, and a footer. Drag the right edge — the width is
197
+ * remembered under `sidebar-story`. */
198
+ export const Panel: Story = {
199
+ render: () => <PanelDemo />,
200
+ };
201
+
202
+ /** Controlled width: the host owns the value (here, local state), so drags
203
+ * report through `onWidthChange` instead of persisting. */
204
+ export const ControlledWidth: Story = {
205
+ render: () => <PanelDemo controlled />,
206
+ };
@@ -12,10 +12,18 @@
12
12
  * pure and needs no ELK — the layout half it feeds is covered by the node e2e.
13
13
  */
14
14
  import { describe, expect, test } from 'bun:test';
15
- import { c4GroupDefs, hidesComponentDuringMorph, rebaseGroupBoundsAbsolute, showDescriptionFor } from './C4Graph';
16
- import { OPEN_FOOTER_PAD, OPEN_SIDE_PAD } from './C4NodeCard';
15
+ import {
16
+ absoluteRectOf,
17
+ c4GroupDefs,
18
+ hidesComponentDuringMorph,
19
+ rebaseGroupBoundsAbsolute,
20
+ showDescriptionFor,
21
+ } from './C4Graph';
22
+ import type { C4ConstructGraph } from './C4Graph';
23
+ import { COMPONENT_HEADER_PAD, OPEN_FOOTER_PAD, OPEN_SIDE_PAD } from './C4NodeCard';
17
24
  import { openContainerSet, openStateKey } from './c4';
18
25
  import type { C4Element, C4Model } from './c4';
26
+ import type { SubsystemComponent } from './model';
19
27
 
20
28
  const SYSTEM = 'system:studio';
21
29
 
@@ -148,6 +156,46 @@ describe('c4GroupDefs — the opened container', () => {
148
156
  });
149
157
  });
150
158
 
159
+ describe('c4GroupDefs — constructs nest container → component → construct', () => {
160
+ const comp = (alias: string): SubsystemComponent => ({
161
+ alias,
162
+ name: alias,
163
+ construct: 'function',
164
+ file: `${alias}.ts`,
165
+ purl: `pkg:x#${alias}.ts`,
166
+ });
167
+
168
+ test('a component that claims constructs grows into a nested ELK parent', () => {
169
+ const cg: C4ConstructGraph = {
170
+ components: [comp('a'), comp('b')],
171
+ trails: [],
172
+ componentOf: new Map([['a', 'cmp:a']]),
173
+ };
174
+ const groups = groupById(c4GroupDefs(base, [HOST], new Map([[HOST, cg]])).groups);
175
+ const compGroup = groups.get('cmp:a')!;
176
+ // The construct nests inside its component, which nests inside the container.
177
+ expect(compGroup.parentId).toBe(HOST);
178
+ expect(compGroup.memberIds).toEqual(['a']);
179
+ expect(compGroup.drawnAsCard).toBe(true);
180
+ expect(compGroup.padTop).toBe(COMPONENT_HEADER_PAD);
181
+ // The container holds the component (as a group) and the unclaimed construct.
182
+ expect(groups.get(HOST)!.memberIds).toEqual(['cmp:a', 'cmp:b', 'b']);
183
+ });
184
+
185
+ test('a construct no component claims sits directly in the container', () => {
186
+ const cg: C4ConstructGraph = { components: [comp('orphan')], trails: [] };
187
+ const groups = groupById(c4GroupDefs(base, [HOST], new Map([[HOST, cg]])).groups);
188
+ expect(groups.get(HOST)!.memberIds).toEqual(['cmp:a', 'cmp:b', 'orphan']);
189
+ // No component claims it, so it is not a group of its own.
190
+ expect(groups.has('orphan')).toBe(false);
191
+ });
192
+
193
+ test('no construct graph leaves the container holding only its components', () => {
194
+ const groups = groupById(c4GroupDefs(base, [HOST]).groups);
195
+ expect(groups.get(HOST)!.memberIds).toEqual(['cmp:a', 'cmp:b']);
196
+ });
197
+ });
198
+
151
199
  describe('c4GroupDefs — frames', () => {
152
200
  test('the opened container is not also drawn as a frame', () => {
153
201
  // It is drawn as a grown card. Leaving it in `frames` would draw a frame
@@ -238,6 +286,52 @@ describe('c4GroupDefs — several containers open at once', () => {
238
286
  });
239
287
  });
240
288
 
289
+ describe('absoluteRectOf — a node rect in absolute flow space', () => {
290
+ const node = (id: string, x: number, y: number, parentId?: string) => ({
291
+ id,
292
+ position: { x, y },
293
+ width: 100,
294
+ height: 50,
295
+ ...(parentId ? { parentId } : {}),
296
+ });
297
+ const layout = () => ({
298
+ nodes: [
299
+ node('system', 12, 12),
300
+ node('container', 10, 190, 'system'),
301
+ node('component', 24, 64, 'container'),
302
+ ],
303
+ groupBounds: new Map([['container', { x: 22, y: 202, width: 250, height: 500 }]]),
304
+ });
305
+
306
+ test('a group takes its absolute bounds', () => {
307
+ // The group bounds are already absolute — the node box must not be consulted.
308
+ expect(absoluteRectOf(layout(), 'container')).toEqual({
309
+ x: 22,
310
+ y: 202,
311
+ width: 250,
312
+ height: 500,
313
+ });
314
+ });
315
+
316
+ test('a nested leaf adds every ancestor origin', () => {
317
+ // system(12,12) + container(10,190) + component(24,64)
318
+ expect(absoluteRectOf(layout(), 'component')).toEqual({
319
+ x: 46,
320
+ y: 266,
321
+ width: 100,
322
+ height: 50,
323
+ });
324
+ });
325
+
326
+ test('a root leaf is its own position', () => {
327
+ expect(absoluteRectOf(layout(), 'system')).toEqual({ x: 12, y: 12, width: 100, height: 50 });
328
+ });
329
+
330
+ test('an absent id is null', () => {
331
+ expect(absoluteRectOf(layout(), 'nope')).toBeNull();
332
+ });
333
+ });
334
+
241
335
  describe('openContainerSet', () => {
242
336
  test('is empty when nothing is open, in either spelling', () => {
243
337
  expect(openContainerSet({})).toEqual([]);