@principal-ai/subsystems-react 0.52.0 → 0.53.1

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 (36) hide show
  1. package/dist/subsystem/C4Graph.d.ts +8 -1
  2. package/dist/subsystem/C4Graph.d.ts.map +1 -1
  3. package/dist/subsystem/C4Graph.js +470 -155
  4. package/dist/subsystem/C4Graph.js.map +1 -1
  5. package/dist/subsystem/C4NodeCard.d.ts +42 -1
  6. package/dist/subsystem/C4NodeCard.d.ts.map +1 -1
  7. package/dist/subsystem/C4NodeCard.js +75 -20
  8. package/dist/subsystem/C4NodeCard.js.map +1 -1
  9. package/dist/subsystem/IssueList.d.ts.map +1 -1
  10. package/dist/subsystem/IssueList.js +13 -4
  11. package/dist/subsystem/IssueList.js.map +1 -1
  12. package/dist/subsystem/c4.d.ts +27 -3
  13. package/dist/subsystem/c4.d.ts.map +1 -1
  14. package/dist/subsystem/c4.js +48 -0
  15. package/dist/subsystem/c4.js.map +1 -1
  16. package/dist/subsystem/model.d.ts +35 -245
  17. package/dist/subsystem/model.d.ts.map +1 -1
  18. package/dist/subsystem/model.js +11 -29
  19. package/dist/subsystem/model.js.map +1 -1
  20. package/dist/utils/elkLayout.d.ts +26 -2
  21. package/dist/utils/elkLayout.d.ts.map +1 -1
  22. package/dist/utils/elkLayout.js +40 -8
  23. package/dist/utils/elkLayout.js.map +1 -1
  24. package/package.json +4 -3
  25. package/src/stories/Subsystem/C4Graph/C4CardGrow.stories.tsx +125 -0
  26. package/src/stories/Subsystem/C4Graph/C4Component.stories.tsx +21 -11
  27. package/src/stories/Subsystem/C4Graph/C4Container.stories.tsx +1 -9
  28. package/src/stories/Subsystem/C4Graph/C4Transition.stories.tsx +131 -0
  29. package/src/subsystem/C4Graph.tsx +578 -198
  30. package/src/subsystem/C4NodeCard.test.ts +6 -3
  31. package/src/subsystem/C4NodeCard.tsx +158 -49
  32. package/src/subsystem/IssueList.category.test.ts +39 -0
  33. package/src/subsystem/IssueList.tsx +14 -3
  34. package/src/subsystem/c4.ts +74 -3
  35. package/src/subsystem/model.ts +62 -305
  36. package/src/utils/elkLayout.ts +60 -10
@@ -0,0 +1,125 @@
1
+ import React, { useState } from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
4
+ import { C4NodeCard, NODE_W, NODE_H } from '../../../subsystem/C4NodeCard';
5
+ import type { C4Container, C4Component } from '../../../subsystem/c4';
6
+
7
+ const meta = {
8
+ title: 'Subsystem/C4Graph/C4CardGrow',
9
+ parameters: { layout: 'fullscreen' },
10
+ tags: ['autodocs'],
11
+ decorators: [
12
+ (Story) => (
13
+ <ThemeProvider theme={defaultEditorTheme}>
14
+ <Story />
15
+ </ThemeProvider>
16
+ ),
17
+ ],
18
+ } satisfies Meta;
19
+
20
+ export default meta;
21
+ type Story = StoryObj<typeof meta>;
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Isolated grow/shrink of one container card — no graph, no frames, no
25
+ // components. Toggling the expand affordance animates the card's own size
26
+ // between the box dimensions and an expanded rectangle, so the FLIP motion can
27
+ // be judged on its own.
28
+ // ---------------------------------------------------------------------------
29
+
30
+ const GROWN_W = 420;
31
+ const GROWN_H = 300;
32
+
33
+ const card: C4Container = {
34
+ kind: 'container',
35
+ id: 'container:studio-host',
36
+ label: 'Subsystem Studio — host',
37
+ containerKind: 'application',
38
+ technology: 'Bun + Electrobun',
39
+ description: 'Owns the model store on disk and serves the renderer over RPC.',
40
+ state: 'accepted',
41
+ parentId: 'system:acme',
42
+ };
43
+
44
+ const COMPONENTS: C4Component[] = [
45
+ {
46
+ kind: 'component',
47
+ id: 'component:audit-verification',
48
+ label: 'Audit & verification',
49
+ container: card.id,
50
+ technology: 'Bun',
51
+ description: 'Checks a subsystem model and reports what is wrong.',
52
+ state: 'accepted',
53
+ },
54
+ {
55
+ kind: 'component',
56
+ id: 'component:model-store',
57
+ label: 'Model store I/O',
58
+ container: card.id,
59
+ technology: 'Bun fs',
60
+ description: 'Reads and writes model JSON under ~/.principal.',
61
+ state: 'accepted',
62
+ },
63
+ ];
64
+
65
+ function Demo() {
66
+ const [open, setOpen] = useState(false);
67
+ const [grown, setGrown] = useState(false);
68
+ // True when the card has finished shrinking back to its box — only then is the
69
+ // description shown again (it stays hidden through open *and* the shrink).
70
+ const [settled, setSettled] = useState(true);
71
+ return (
72
+ <div
73
+ style={{
74
+ width: '100%',
75
+ height: '100vh',
76
+ display: 'flex',
77
+ alignItems: 'center',
78
+ justifyContent: 'center',
79
+ background: '#12141a',
80
+ }}
81
+ >
82
+ <div
83
+ onTransitionEnd={(e) => {
84
+ // The card's own size transition bubbles here.
85
+ if (e.propertyName !== 'width') return;
86
+ if (open) setGrown(true);
87
+ else setSettled(true);
88
+ }}
89
+ >
90
+ <C4NodeCard
91
+ node={card}
92
+ width={open ? GROWN_W : NODE_W}
93
+ height={open ? GROWN_H : NODE_H}
94
+ showDescription={settled}
95
+ onExpand={() => {
96
+ // Leaving the resting state: hide the description immediately.
97
+ setSettled(false);
98
+ if (open) setGrown(false);
99
+ setOpen((v) => !v);
100
+ }}
101
+ >
102
+ {grown && (
103
+ <div
104
+ style={{
105
+ display: 'grid',
106
+ gridTemplateColumns: '1fr 1fr',
107
+ gap: 10,
108
+ marginTop: 8,
109
+ minHeight: 0,
110
+ }}
111
+ >
112
+ {COMPONENTS.map((c) => (
113
+ <C4NodeCard key={c.id} node={c} />
114
+ ))}
115
+ </div>
116
+ )}
117
+ </C4NodeCard>
118
+ </div>
119
+ </div>
120
+ );
121
+ }
122
+
123
+ export const Grow: Story = {
124
+ render: () => <Demo />,
125
+ };
@@ -3,7 +3,7 @@ import '@xyflow/react/dist/style.css';
3
3
  import type { Meta, StoryObj } from '@storybook/react';
4
4
  import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
5
5
  import { C4Graph } from '../../../subsystem/C4Graph';
6
- import type { C4Edge, C4Group, C4Model, C4Element, C4Component } from '../../../subsystem/c4';
6
+ import type { C4Edge, C4Model, C4Element, C4Component, C4Container } from '../../../subsystem/c4';
7
7
 
8
8
  const meta = {
9
9
  title: 'Subsystem/C4Graph/C4Component',
@@ -53,7 +53,25 @@ function component(
53
53
  };
54
54
  }
55
55
 
56
+ /** A container element. Its components make it frame in the renderer. */
57
+ function container(over: Partial<C4Container> & Pick<C4Container, 'id' | 'label'>): C4Element {
58
+ return {
59
+ kind: 'container',
60
+ containerKind: 'application',
61
+ technology: '',
62
+ state: 'accepted',
63
+ members: [],
64
+ constructs: [],
65
+ parentId: SYSTEM.id,
66
+ ...over,
67
+ };
68
+ }
69
+
56
70
  const NODES: C4Element[] = [
71
+ // --- the containers these components sit in ------------------------------
72
+ container({ id: HOST, label: 'Subsystem Studio — host', technology: 'Bun + Electrobun' }),
73
+ container({ id: RENDERER, label: 'Subsystem Studio — renderer', technology: 'React 19' }),
74
+
57
75
  // --- inside the host ------------------------------------------------------
58
76
  component({
59
77
  id: 'component:audit-verification',
@@ -131,13 +149,6 @@ const NODES: C4Element[] = [
131
149
  }),
132
150
  ];
133
151
 
134
- /** The two containers at this level, drawn as frames. */
135
- const GROUPS: C4Group[] = [
136
- { id: HOST, kind: 'container', label: 'Subsystem Studio — host', parentId: SYSTEM.id, memberIds: NODES.filter((n) => n.parentId === HOST).map((n) => n.id) },
137
- { id: RENDERER, kind: 'container', label: 'Subsystem Studio — renderer', parentId: SYSTEM.id, memberIds: NODES.filter((n) => n.parentId === RENDERER).map((n) => n.id) },
138
- { id: SYSTEM.id, kind: 'system', label: SYSTEM.label, memberIds: [HOST, RENDERER] },
139
- ];
140
-
141
152
  /** A line between components — protocol-labelled, like the container view. */
142
153
  function edge(id: string, source: string, target: string, protocol: string): C4Edge {
143
154
  return { id, source, target, label: protocol, protocol, mechanisms: [], count: 1 };
@@ -158,7 +169,6 @@ const componentModel: C4Model = {
158
169
  view: 'component',
159
170
  system: SYSTEM,
160
171
  nodes: NODES,
161
- groups: GROUPS,
162
172
  edges: EDGES,
163
173
  };
164
174
 
@@ -170,7 +180,7 @@ function Demo() {
170
180
  return (
171
181
  <div style={{ width: '100%', height: '100vh', display: 'flex', flexDirection: 'column' }}>
172
182
  <div style={{ flex: 1, minHeight: 0, position: 'relative' }}>
173
- <C4Graph model={model} title="Subsystem Modeling" onSelectNode={setSelected} />
183
+ <C4Graph model={model} onSelectNode={setSelected} />
174
184
  </div>
175
185
  <div
176
186
  style={{
@@ -184,7 +194,7 @@ function Demo() {
184
194
  borderTop: '1px solid #333',
185
195
  }}
186
196
  >
187
- <span>{components} components in {model.groups.filter((g) => g.kind === 'container').length} containers</span>
197
+ <span>{components} components in {model.nodes.filter((n) => n.kind === 'container').length} containers</span>
188
198
  <span>{model.edges.length} lines</span>
189
199
  <span>selected: {selected ?? '(none)'}</span>
190
200
  <span style={{ color: '#777' }}>each component is a grouping stitched across many models</span>
@@ -195,14 +195,6 @@ const containerModel: C4Model = {
195
195
  view: 'container',
196
196
  system: SYSTEM,
197
197
  nodes: NODES,
198
- groups: [
199
- {
200
- id: SYSTEM.id,
201
- kind: 'system',
202
- label: SYSTEM.label,
203
- memberIds: NODES.filter((n) => n.parentId === SYSTEM.id).map((n) => n.id),
204
- },
205
- ],
206
198
  edges: EDGES,
207
199
  };
208
200
 
@@ -218,7 +210,7 @@ function Demo() {
218
210
  return (
219
211
  <div style={{ width: '100%', height: '100vh', display: 'flex', flexDirection: 'column' }}>
220
212
  <div style={{ flex: 1, minHeight: 0, position: 'relative' }}>
221
- <C4Graph model={model} title="Subsystem Modeling" onSelectNode={setSelected} />
213
+ <C4Graph model={model} onSelectNode={setSelected} />
222
214
  </div>
223
215
  <div
224
216
  style={{
@@ -0,0 +1,131 @@
1
+ import React, { useMemo, useState } from 'react';
2
+ import '@xyflow/react/dist/style.css';
3
+ import type { Meta, StoryObj } from '@storybook/react';
4
+ import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
5
+ import { C4Graph } from '../../../subsystem/C4Graph';
6
+ import type { C4Edge, C4Model, C4Element, C4Component, C4Container } from '../../../subsystem/c4';
7
+
8
+ const meta = {
9
+ title: 'Subsystem/C4Graph/C4Transition',
10
+ component: C4Graph,
11
+ parameters: { layout: 'fullscreen' },
12
+ tags: ['autodocs'],
13
+ decorators: [
14
+ (Story) => (
15
+ <ThemeProvider theme={defaultEditorTheme}>
16
+ <Story />
17
+ </ThemeProvider>
18
+ ),
19
+ ],
20
+ } satisfies Meta<typeof C4Graph>;
21
+
22
+ export default meta;
23
+ type Story = StoryObj<typeof meta>;
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Drill-down: two layouts, one transition.
27
+ //
28
+ // Closed, the container is a box. Its expand affordance (bottom-right) opens it,
29
+ // and the graph swaps to a *second, already-computed* layout in which the same
30
+ // element is an ELK parent — so ELK sizes it to hold its own components, and they
31
+ // arrive as nested cards. The same affordance closes it again.
32
+ //
33
+ // Nothing is a frame here: the container stays a card and grows (its own
34
+ // width/height transition), while the system frame around it morphs by transform.
35
+ // Components stay hidden until the grow settles, and the card's description
36
+ // returns only after the shrink — hence the two settled states below.
37
+ //
38
+ // The story also carries component→component edges, which the nested layout
39
+ // routes inside the parent.
40
+ // ---------------------------------------------------------------------------
41
+
42
+ const SYSTEM = { id: 'system:acme', label: 'Subsystem Modeling', repoKey: 'pkg:github/acme/subsystem-modeling' };
43
+ const HOST = 'container:studio-host';
44
+
45
+ function container(over: Partial<C4Container> & Pick<C4Container, 'id' | 'label'>): C4Element {
46
+ return {
47
+ kind: 'container',
48
+ containerKind: 'application',
49
+ technology: '',
50
+ state: 'accepted',
51
+ members: [],
52
+ constructs: [],
53
+ parentId: SYSTEM.id,
54
+ ...over,
55
+ };
56
+ }
57
+
58
+ function component(
59
+ over: Partial<C4Component> & Pick<C4Component, 'id' | 'label' | 'container'>,
60
+ ): C4Element {
61
+ return {
62
+ kind: 'component',
63
+ members: [],
64
+ constructs: [],
65
+ state: 'accepted',
66
+ parentId: over.container,
67
+ technology: 'Bun',
68
+ ...over,
69
+ };
70
+ }
71
+
72
+ const NODES: C4Element[] = [
73
+ container({ id: HOST, label: 'Subsystem Studio — host', technology: 'Bun + Electrobun', description: 'The Electron main process.' }),
74
+ component({ id: 'component:audit-verification', label: 'Audit & verification', container: HOST, description: 'Checks a model, reports problems.' }),
75
+ component({ id: 'component:maintain-proposals', label: 'Maintain & proposals', container: HOST, description: 'Runs the maintain agent, records proposals.' }),
76
+ component({ id: 'component:model-store', label: 'Model store I/O', container: HOST, technology: 'Bun fs', description: 'Reads and writes model JSON.' }),
77
+ ];
78
+
79
+ function edge(id: string, source: string, target: string, protocol: string): C4Edge {
80
+ return { id, source, target, label: protocol, protocol, mechanisms: [], count: 1 };
81
+ }
82
+
83
+ const EDGES: C4Edge[] = [
84
+ edge('e:audit-store', 'component:audit-verification', 'component:model-store', 'fs · JSON'),
85
+ edge('e:proposals-store', 'component:maintain-proposals', 'component:model-store', 'fs · JSON'),
86
+ ];
87
+
88
+ function Demo() {
89
+ const [openId, setOpenId] = useState<string | null>(null);
90
+ const [selected, setSelected] = useState<string | null>(null);
91
+
92
+ const model = useMemo<C4Model>(
93
+ () => ({ view: 'container', system: SYSTEM, nodes: NODES, edges: EDGES, openContainerId: openId }),
94
+ [openId],
95
+ );
96
+
97
+ return (
98
+ <div style={{ width: '100%', height: '100vh', display: 'flex', flexDirection: 'column' }}>
99
+ <div style={{ flex: 1, minHeight: 0, position: 'relative' }}>
100
+ <C4Graph
101
+ model={model}
102
+ onSelectNode={setSelected}
103
+ onOpenContainer={setOpenId}
104
+ onCloseContainer={() => setOpenId(null)}
105
+ />
106
+ </div>
107
+ <div
108
+ style={{
109
+ padding: '6px 12px',
110
+ display: 'flex',
111
+ alignItems: 'center',
112
+ gap: 18,
113
+ fontFamily: 'monospace',
114
+ fontSize: 12,
115
+ color: '#aaa',
116
+ borderTop: '1px solid #333',
117
+ }}
118
+ >
119
+ <span>{openId ? `opened: ${openId}` : 'closed'}</span>
120
+ <span>selected: {selected ?? '(none)'}</span>
121
+ <span style={{ color: '#777' }}>
122
+ the expand icon (bottom-right of the container) opens it, and closes it again
123
+ </span>
124
+ </div>
125
+ </div>
126
+ );
127
+ }
128
+
129
+ export const DrillDown: Story = {
130
+ render: () => <Demo />,
131
+ };