@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.
- package/dist/subsystem/C4Graph.d.ts +8 -1
- package/dist/subsystem/C4Graph.d.ts.map +1 -1
- package/dist/subsystem/C4Graph.js +470 -155
- package/dist/subsystem/C4Graph.js.map +1 -1
- package/dist/subsystem/C4NodeCard.d.ts +42 -1
- package/dist/subsystem/C4NodeCard.d.ts.map +1 -1
- package/dist/subsystem/C4NodeCard.js +75 -20
- package/dist/subsystem/C4NodeCard.js.map +1 -1
- package/dist/subsystem/IssueList.d.ts.map +1 -1
- package/dist/subsystem/IssueList.js +13 -4
- package/dist/subsystem/IssueList.js.map +1 -1
- package/dist/subsystem/c4.d.ts +27 -3
- package/dist/subsystem/c4.d.ts.map +1 -1
- package/dist/subsystem/c4.js +48 -0
- package/dist/subsystem/c4.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/utils/elkLayout.d.ts +26 -2
- package/dist/utils/elkLayout.d.ts.map +1 -1
- package/dist/utils/elkLayout.js +40 -8
- package/dist/utils/elkLayout.js.map +1 -1
- package/package.json +4 -3
- package/src/stories/Subsystem/C4Graph/C4CardGrow.stories.tsx +125 -0
- package/src/stories/Subsystem/C4Graph/C4Component.stories.tsx +21 -11
- package/src/stories/Subsystem/C4Graph/C4Container.stories.tsx +1 -9
- package/src/stories/Subsystem/C4Graph/C4Transition.stories.tsx +131 -0
- package/src/subsystem/C4Graph.tsx +578 -198
- package/src/subsystem/C4NodeCard.test.ts +6 -3
- package/src/subsystem/C4NodeCard.tsx +158 -49
- package/src/subsystem/IssueList.category.test.ts +39 -0
- package/src/subsystem/IssueList.tsx +14 -3
- package/src/subsystem/c4.ts +74 -3
- package/src/subsystem/model.ts +62 -305
- 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,
|
|
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}
|
|
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.
|
|
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}
|
|
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
|
+
};
|