@vpxa/aikit 0.1.160 → 0.1.162
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/package.json +2 -1
- package/packages/blocks-core/dist/index.d.ts +2 -1
- package/packages/blocks-core/dist/index.js +22 -23
- package/packages/present/dist/index.html +19 -20
- package/packages/server/dist/index.js +1 -1
- package/packages/server/dist/{server-DKj9yfXg.js → server-DLkC-unR.js} +41 -41
- package/packages/server/viewers/architecture-static.html +867 -0
- package/packages/server/viewers/c4-viewer.html +79 -0
- package/packages/server/viewers/canvas.html +1361 -0
- package/packages/server/viewers/process-flow-static.html +764 -0
- package/packages/server/viewers/report-template.html +724 -0
- package/packages/server/viewers/task-plan-static.html +1070 -0
- package/packages/server/viewers/tour-viewer.html +764 -0
- package/packages/store/dist/index.js +1 -1
- package/scaffold/dist/definitions/bodies.mjs +7 -0
- package/scaffold/dist/definitions/flows.mjs +13 -0
- package/scaffold/dist/definitions/skills/present.mjs +1 -1
|
@@ -0,0 +1,867 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" data-theme="auto">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>AI Kit - Architecture Static Viewer</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--dt-bg-primary: #020617;
|
|
10
|
+
--dt-bg-surface: #0f172a;
|
|
11
|
+
--dt-text-primary: #f8fafc;
|
|
12
|
+
--dt-text-secondary: #94a3b8;
|
|
13
|
+
--dt-border: #334155;
|
|
14
|
+
--dt-cyan: #06b6d4;
|
|
15
|
+
--dt-emerald: #10b981;
|
|
16
|
+
--dt-violet: #8b5cf6;
|
|
17
|
+
--dt-amber: #f59e0b;
|
|
18
|
+
--dt-rose: #f43f5e;
|
|
19
|
+
--dt-slate: #64748b;
|
|
20
|
+
--dt-grid: #1e293b;
|
|
21
|
+
--dt-shadow: rgba(2, 6, 23, 0.38);
|
|
22
|
+
--dt-toolbar-bg: rgba(15, 23, 42, 0.82);
|
|
23
|
+
--dt-button-bg: rgba(15, 23, 42, 0.88);
|
|
24
|
+
--dt-button-hover: rgba(30, 41, 59, 0.95);
|
|
25
|
+
--dt-panel-bg: linear-gradient(180deg, rgba(15, 23, 42, 0.94), rgba(2, 6, 23, 0.98));
|
|
26
|
+
--dt-badge-bg: rgba(15, 23, 42, 0.88);
|
|
27
|
+
--dt-node-fill: rgba(15, 23, 42, 0.92);
|
|
28
|
+
--dt-group-fill: rgba(15, 23, 42, 0.32);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
[data-theme="light"] {
|
|
32
|
+
--dt-bg-primary: #ffffff;
|
|
33
|
+
--dt-bg-surface: #f8fafc;
|
|
34
|
+
--dt-text-primary: #0f172a;
|
|
35
|
+
--dt-text-secondary: #64748b;
|
|
36
|
+
--dt-border: #e2e8f0;
|
|
37
|
+
--dt-grid: rgba(148, 163, 184, 0.12);
|
|
38
|
+
--dt-shadow: rgba(148, 163, 184, 0.24);
|
|
39
|
+
--dt-toolbar-bg: rgba(255, 255, 255, 0.88);
|
|
40
|
+
--dt-button-bg: rgba(248, 250, 252, 0.96);
|
|
41
|
+
--dt-button-hover: rgba(226, 232, 240, 0.96);
|
|
42
|
+
--dt-panel-bg: linear-gradient(180deg, rgba(248, 250, 252, 0.96), rgba(241, 245, 249, 1));
|
|
43
|
+
--dt-badge-bg: rgba(255, 255, 255, 0.9);
|
|
44
|
+
--dt-node-fill: rgba(255, 255, 255, 0.96);
|
|
45
|
+
--dt-group-fill: rgba(255, 255, 255, 0.2);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@media (prefers-color-scheme: light) {
|
|
49
|
+
:root:not([data-theme="dark"]) {
|
|
50
|
+
--dt-bg-primary: #ffffff;
|
|
51
|
+
--dt-bg-surface: #f8fafc;
|
|
52
|
+
--dt-text-primary: #0f172a;
|
|
53
|
+
--dt-text-secondary: #64748b;
|
|
54
|
+
--dt-border: #e2e8f0;
|
|
55
|
+
--dt-grid: rgba(148, 163, 184, 0.12);
|
|
56
|
+
--dt-shadow: rgba(148, 163, 184, 0.24);
|
|
57
|
+
--dt-toolbar-bg: rgba(255, 255, 255, 0.88);
|
|
58
|
+
--dt-button-bg: rgba(248, 250, 252, 0.96);
|
|
59
|
+
--dt-button-hover: rgba(226, 232, 240, 0.96);
|
|
60
|
+
--dt-panel-bg: linear-gradient(180deg, rgba(248, 250, 252, 0.96), rgba(241, 245, 249, 1));
|
|
61
|
+
--dt-badge-bg: rgba(255, 255, 255, 0.9);
|
|
62
|
+
--dt-node-fill: rgba(255, 255, 255, 0.96);
|
|
63
|
+
--dt-group-fill: rgba(255, 255, 255, 0.2);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
* {
|
|
68
|
+
box-sizing: border-box;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
html,
|
|
72
|
+
body {
|
|
73
|
+
height: 100%;
|
|
74
|
+
margin: 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
body {
|
|
78
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
79
|
+
color: var(--dt-text-primary);
|
|
80
|
+
background:
|
|
81
|
+
radial-gradient(circle at top, rgba(139, 92, 246, 0.14), transparent 30%),
|
|
82
|
+
radial-gradient(circle at bottom right, rgba(6, 182, 212, 0.12), transparent 32%),
|
|
83
|
+
var(--dt-bg-primary);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.shell {
|
|
87
|
+
min-height: 100%;
|
|
88
|
+
display: grid;
|
|
89
|
+
grid-template-rows: auto 1fr;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.toolbar {
|
|
93
|
+
position: sticky;
|
|
94
|
+
top: 0;
|
|
95
|
+
z-index: 2;
|
|
96
|
+
display: flex;
|
|
97
|
+
align-items: center;
|
|
98
|
+
justify-content: space-between;
|
|
99
|
+
gap: 1rem;
|
|
100
|
+
padding: 1rem 1.25rem;
|
|
101
|
+
border-bottom: 1px solid var(--dt-border);
|
|
102
|
+
background: var(--dt-toolbar-bg);
|
|
103
|
+
backdrop-filter: blur(16px);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.title h1 {
|
|
107
|
+
margin: 0;
|
|
108
|
+
font-size: 1rem;
|
|
109
|
+
font-weight: 700;
|
|
110
|
+
letter-spacing: 0.02em;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.title p {
|
|
114
|
+
margin: 0.2rem 0 0;
|
|
115
|
+
font-size: 0.82rem;
|
|
116
|
+
color: var(--dt-text-secondary);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.actions {
|
|
120
|
+
display: flex;
|
|
121
|
+
flex-wrap: wrap;
|
|
122
|
+
justify-content: flex-end;
|
|
123
|
+
gap: 0.6rem;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
button {
|
|
127
|
+
appearance: none;
|
|
128
|
+
border: 1px solid var(--dt-border);
|
|
129
|
+
background: var(--dt-button-bg);
|
|
130
|
+
color: var(--dt-text-primary);
|
|
131
|
+
border-radius: 999px;
|
|
132
|
+
padding: 0.7rem 1rem;
|
|
133
|
+
font: inherit;
|
|
134
|
+
cursor: pointer;
|
|
135
|
+
transition: background 160ms ease, border-color 160ms ease, transform 160ms ease;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
button:hover {
|
|
139
|
+
background: var(--dt-button-hover);
|
|
140
|
+
transform: translateY(-1px);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.stage {
|
|
144
|
+
padding: 1rem;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.canvas-shell {
|
|
148
|
+
position: relative;
|
|
149
|
+
min-height: calc(100vh - 5.5rem);
|
|
150
|
+
border: 1px solid var(--dt-border);
|
|
151
|
+
border-radius: 24px;
|
|
152
|
+
background:
|
|
153
|
+
linear-gradient(var(--dt-grid) 1px, transparent 1px),
|
|
154
|
+
linear-gradient(90deg, var(--dt-grid) 1px, transparent 1px),
|
|
155
|
+
var(--dt-panel-bg);
|
|
156
|
+
background-size: 40px 40px, 40px 40px, 100% 100%;
|
|
157
|
+
overflow: hidden;
|
|
158
|
+
box-shadow: 0 24px 60px var(--dt-shadow);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
svg {
|
|
162
|
+
display: block;
|
|
163
|
+
width: 100%;
|
|
164
|
+
height: min(78vh, 980px);
|
|
165
|
+
min-height: 480px;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.empty-state {
|
|
169
|
+
position: absolute;
|
|
170
|
+
inset: 0;
|
|
171
|
+
display: none;
|
|
172
|
+
place-items: center;
|
|
173
|
+
padding: 2rem;
|
|
174
|
+
text-align: center;
|
|
175
|
+
color: var(--dt-text-secondary);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.empty-state.is-visible {
|
|
179
|
+
display: grid;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.empty-state strong {
|
|
183
|
+
display: block;
|
|
184
|
+
margin-bottom: 0.4rem;
|
|
185
|
+
color: var(--dt-text-primary);
|
|
186
|
+
font-size: 1rem;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.badge {
|
|
190
|
+
position: fixed;
|
|
191
|
+
right: 1rem;
|
|
192
|
+
bottom: 1rem;
|
|
193
|
+
z-index: 3;
|
|
194
|
+
display: inline-flex;
|
|
195
|
+
align-items: center;
|
|
196
|
+
gap: 0.55rem;
|
|
197
|
+
padding: 0.55rem 0.8rem;
|
|
198
|
+
border: 1px solid var(--dt-border);
|
|
199
|
+
border-radius: 999px;
|
|
200
|
+
background: var(--dt-badge-bg);
|
|
201
|
+
color: var(--dt-text-secondary);
|
|
202
|
+
backdrop-filter: blur(14px);
|
|
203
|
+
font-size: 0.74rem;
|
|
204
|
+
letter-spacing: 0.04em;
|
|
205
|
+
text-transform: uppercase;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.badge-mark {
|
|
209
|
+
width: 0.55rem;
|
|
210
|
+
height: 0.55rem;
|
|
211
|
+
border-radius: 999px;
|
|
212
|
+
background: var(--dt-cyan);
|
|
213
|
+
box-shadow: 0 0 0 0.18rem rgba(6, 182, 212, 0.2);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.sr-only {
|
|
217
|
+
position: absolute;
|
|
218
|
+
width: 1px;
|
|
219
|
+
height: 1px;
|
|
220
|
+
padding: 0;
|
|
221
|
+
margin: -1px;
|
|
222
|
+
overflow: hidden;
|
|
223
|
+
clip: rect(0, 0, 0, 0);
|
|
224
|
+
white-space: nowrap;
|
|
225
|
+
border: 0;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
@media (max-width: 720px) {
|
|
229
|
+
.toolbar {
|
|
230
|
+
align-items: flex-start;
|
|
231
|
+
flex-direction: column;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.actions {
|
|
235
|
+
width: 100%;
|
|
236
|
+
justify-content: flex-start;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.stage {
|
|
240
|
+
padding: 0.75rem;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
svg {
|
|
244
|
+
min-height: 420px;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
</style>
|
|
248
|
+
</head>
|
|
249
|
+
<body>
|
|
250
|
+
<div class="shell">
|
|
251
|
+
<header class="toolbar">
|
|
252
|
+
<div class="title">
|
|
253
|
+
<h1>Architecture Diagram</h1>
|
|
254
|
+
<p>Pure SVG C4-style renderer for inline lightweight mode.</p>
|
|
255
|
+
</div>
|
|
256
|
+
<div class="actions">
|
|
257
|
+
<button id="theme-button" type="button">Theme: Auto</button>
|
|
258
|
+
<button id="export-button" type="button">Export SVG</button>
|
|
259
|
+
</div>
|
|
260
|
+
</header>
|
|
261
|
+
<main class="stage">
|
|
262
|
+
<div class="canvas-shell">
|
|
263
|
+
<div class="empty-state" id="empty-state" aria-live="polite"></div>
|
|
264
|
+
<svg id="diagram" role="img" aria-labelledby="diagram-title diagram-description"></svg>
|
|
265
|
+
</div>
|
|
266
|
+
</main>
|
|
267
|
+
</div>
|
|
268
|
+
<div class="badge"><span class="badge-mark"></span>AI Kit static viewer</div>
|
|
269
|
+
<script type="application/json" id="diagram-data">{}</script>
|
|
270
|
+
<script>
|
|
271
|
+
(() => {
|
|
272
|
+
const svgNs = 'http://www.w3.org/2000/svg';
|
|
273
|
+
const root = document.documentElement;
|
|
274
|
+
const svg = document.getElementById('diagram');
|
|
275
|
+
const emptyState = document.getElementById('empty-state');
|
|
276
|
+
const exportButton = document.getElementById('export-button');
|
|
277
|
+
const themeButton = document.getElementById('theme-button');
|
|
278
|
+
const themes = ['auto', 'dark', 'light'];
|
|
279
|
+
const typeVars = {
|
|
280
|
+
person: '--dt-cyan',
|
|
281
|
+
system: '--dt-emerald',
|
|
282
|
+
container: '--dt-violet',
|
|
283
|
+
component: '--dt-amber',
|
|
284
|
+
external: '--dt-slate',
|
|
285
|
+
database: '--dt-cyan',
|
|
286
|
+
queue: '--dt-amber',
|
|
287
|
+
boundary: '--dt-slate',
|
|
288
|
+
};
|
|
289
|
+
const leafSize = { width: 248, height: 132 };
|
|
290
|
+
const groupInset = { top: 62, right: 26, bottom: 26, left: 26, gap: 24 };
|
|
291
|
+
const rootGap = 52;
|
|
292
|
+
const canvasPadding = 48;
|
|
293
|
+
const monoFont = '"JetBrains Mono", "SFMono-Regular", Consolas, ui-monospace, monospace';
|
|
294
|
+
|
|
295
|
+
function node(tag, attributes, text) {
|
|
296
|
+
const element = document.createElementNS(svgNs, tag);
|
|
297
|
+
if (attributes) {
|
|
298
|
+
Object.entries(attributes).forEach(([name, value]) => {
|
|
299
|
+
if (value !== undefined && value !== null) {
|
|
300
|
+
element.setAttribute(name, String(value));
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
if (text !== undefined) {
|
|
305
|
+
element.textContent = text;
|
|
306
|
+
}
|
|
307
|
+
return element;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function wrapText(value, maxChars, maxLines) {
|
|
311
|
+
const words = String(value || '').trim().split(/\s+/).filter(Boolean);
|
|
312
|
+
if (!words.length) {
|
|
313
|
+
return [];
|
|
314
|
+
}
|
|
315
|
+
const lines = [];
|
|
316
|
+
let current = '';
|
|
317
|
+
for (const word of words) {
|
|
318
|
+
const next = current ? current + ' ' + word : word;
|
|
319
|
+
if (next.length > maxChars && current) {
|
|
320
|
+
lines.push(current);
|
|
321
|
+
current = word;
|
|
322
|
+
} else {
|
|
323
|
+
current = next;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (current) {
|
|
327
|
+
lines.push(current);
|
|
328
|
+
}
|
|
329
|
+
if (lines.length <= maxLines) {
|
|
330
|
+
return lines;
|
|
331
|
+
}
|
|
332
|
+
const clipped = lines.slice(0, maxLines);
|
|
333
|
+
clipped[maxLines - 1] = clipped[maxLines - 1].replace(/[\s.]+$/, '') + '...';
|
|
334
|
+
return clipped;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function cssValue(name) {
|
|
338
|
+
return getComputedStyle(root).getPropertyValue(name).trim();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function rgba(hex, alpha) {
|
|
342
|
+
const cleaned = String(hex || '').replace('#', '').trim();
|
|
343
|
+
const value = cleaned.length === 3
|
|
344
|
+
? cleaned.split('').map((char) => char + char).join('')
|
|
345
|
+
: cleaned;
|
|
346
|
+
const number = Number.parseInt(value, 16);
|
|
347
|
+
const red = (number >> 16) & 255;
|
|
348
|
+
const green = (number >> 8) & 255;
|
|
349
|
+
const blue = number & 255;
|
|
350
|
+
return 'rgba(' + red + ', ' + green + ', ' + blue + ', ' + alpha + ')';
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function accentFor(type) {
|
|
354
|
+
return cssValue(typeVars[type] || '--dt-slate');
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function parseData() {
|
|
358
|
+
const source = document.getElementById('diagram-data');
|
|
359
|
+
if (!source) {
|
|
360
|
+
return {};
|
|
361
|
+
}
|
|
362
|
+
try {
|
|
363
|
+
return JSON.parse(source.textContent || '{}');
|
|
364
|
+
} catch (error) {
|
|
365
|
+
showEmpty('Invalid diagram JSON', String(error && error.message ? error.message : error));
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function showEmpty(title, message) {
|
|
371
|
+
emptyState.classList.add('is-visible');
|
|
372
|
+
emptyState.innerHTML = '<div><strong>' + escapeHtml(title) + '</strong><span>' + escapeHtml(message) + '</span></div>';
|
|
373
|
+
svg.innerHTML = '';
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function hideEmpty() {
|
|
377
|
+
emptyState.classList.remove('is-visible');
|
|
378
|
+
emptyState.textContent = '';
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function escapeHtml(value) {
|
|
382
|
+
return String(value)
|
|
383
|
+
.replace(/&/g, '&')
|
|
384
|
+
.replace(/</g, '<')
|
|
385
|
+
.replace(/>/g, '>')
|
|
386
|
+
.replace(/"/g, '"')
|
|
387
|
+
.replace(/'/g, ''');
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function buildModel(raw) {
|
|
391
|
+
const nodes = Array.isArray(raw && raw.nodes) ? raw.nodes : [];
|
|
392
|
+
const edges = Array.isArray(raw && raw.edges) ? raw.edges : [];
|
|
393
|
+
const map = new Map();
|
|
394
|
+
const children = new Map();
|
|
395
|
+
|
|
396
|
+
nodes.forEach((item) => {
|
|
397
|
+
map.set(item.id, Object.assign({ type: 'container' }, item));
|
|
398
|
+
children.set(item.id, []);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
nodes.forEach((item) => {
|
|
402
|
+
if (item.parent && children.has(item.parent)) {
|
|
403
|
+
children.get(item.parent).push(item.id);
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
const roots = nodes
|
|
408
|
+
.filter((item) => !item.parent || !map.has(item.parent))
|
|
409
|
+
.map((item) => item.id);
|
|
410
|
+
|
|
411
|
+
return { nodes, edges, map, children, roots };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function measureEntity(id, model, cache) {
|
|
415
|
+
if (cache.has(id)) {
|
|
416
|
+
return cache.get(id);
|
|
417
|
+
}
|
|
418
|
+
const childIds = model.children.get(id) || [];
|
|
419
|
+
if (!childIds.length) {
|
|
420
|
+
const measuredLeaf = Object.assign({ id, cols: 1, rows: 1, childIds: [] }, leafSize, { isGroup: false });
|
|
421
|
+
cache.set(id, measuredLeaf);
|
|
422
|
+
return measuredLeaf;
|
|
423
|
+
}
|
|
424
|
+
const measuredChildren = childIds.map((childId) => measureEntity(childId, model, cache));
|
|
425
|
+
const cols = childIds.length <= 2 ? Math.max(1, childIds.length) : Math.ceil(Math.sqrt(childIds.length));
|
|
426
|
+
let rowWidth = 0;
|
|
427
|
+
let rowHeight = 0;
|
|
428
|
+
let totalHeight = 0;
|
|
429
|
+
let maxWidth = 0;
|
|
430
|
+
let column = 0;
|
|
431
|
+
|
|
432
|
+
measuredChildren.forEach((child, index) => {
|
|
433
|
+
rowWidth += child.width;
|
|
434
|
+
if (column > 0) {
|
|
435
|
+
rowWidth += groupInset.gap;
|
|
436
|
+
}
|
|
437
|
+
rowHeight = Math.max(rowHeight, child.height);
|
|
438
|
+
column += 1;
|
|
439
|
+
const endRow = column === cols || index === measuredChildren.length - 1;
|
|
440
|
+
if (endRow) {
|
|
441
|
+
maxWidth = Math.max(maxWidth, rowWidth);
|
|
442
|
+
totalHeight += rowHeight;
|
|
443
|
+
if (index !== measuredChildren.length - 1) {
|
|
444
|
+
totalHeight += groupInset.gap;
|
|
445
|
+
}
|
|
446
|
+
rowWidth = 0;
|
|
447
|
+
rowHeight = 0;
|
|
448
|
+
column = 0;
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
const measuredGroup = {
|
|
453
|
+
id,
|
|
454
|
+
isGroup: true,
|
|
455
|
+
childIds,
|
|
456
|
+
cols,
|
|
457
|
+
width: Math.max(312, groupInset.left + maxWidth + groupInset.right),
|
|
458
|
+
height: Math.max(220, groupInset.top + totalHeight + groupInset.bottom),
|
|
459
|
+
};
|
|
460
|
+
cache.set(id, measuredGroup);
|
|
461
|
+
return measuredGroup;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function placeEntity(id, x, y, model, cache, positions) {
|
|
465
|
+
const measured = cache.get(id);
|
|
466
|
+
positions.set(id, { x, y, width: measured.width, height: measured.height });
|
|
467
|
+
if (!measured.isGroup) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
let cursorX = x + groupInset.left;
|
|
471
|
+
let cursorY = y + groupInset.top;
|
|
472
|
+
let rowHeight = 0;
|
|
473
|
+
let column = 0;
|
|
474
|
+
|
|
475
|
+
measured.childIds.forEach((childId) => {
|
|
476
|
+
const child = cache.get(childId);
|
|
477
|
+
if (column === measured.cols) {
|
|
478
|
+
cursorX = x + groupInset.left;
|
|
479
|
+
cursorY += rowHeight + groupInset.gap;
|
|
480
|
+
rowHeight = 0;
|
|
481
|
+
column = 0;
|
|
482
|
+
}
|
|
483
|
+
placeEntity(childId, cursorX, cursorY, model, cache, positions);
|
|
484
|
+
cursorX += child.width + groupInset.gap;
|
|
485
|
+
rowHeight = Math.max(rowHeight, child.height);
|
|
486
|
+
column += 1;
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function layout(model) {
|
|
491
|
+
const cache = new Map();
|
|
492
|
+
const positions = new Map();
|
|
493
|
+
const roots = model.roots.map((id) => measureEntity(id, model, cache));
|
|
494
|
+
const cols = roots.length <= 2 ? Math.max(1, roots.length) : Math.ceil(Math.sqrt(roots.length));
|
|
495
|
+
let cursorX = canvasPadding;
|
|
496
|
+
let cursorY = canvasPadding;
|
|
497
|
+
let rowHeight = 0;
|
|
498
|
+
let column = 0;
|
|
499
|
+
let maxRight = 0;
|
|
500
|
+
let maxBottom = 0;
|
|
501
|
+
|
|
502
|
+
model.roots.forEach((id) => {
|
|
503
|
+
const measured = cache.get(id);
|
|
504
|
+
if (column === cols) {
|
|
505
|
+
cursorX = canvasPadding;
|
|
506
|
+
cursorY += rowHeight + rootGap;
|
|
507
|
+
rowHeight = 0;
|
|
508
|
+
column = 0;
|
|
509
|
+
}
|
|
510
|
+
placeEntity(id, cursorX, cursorY, model, cache, positions);
|
|
511
|
+
maxRight = Math.max(maxRight, cursorX + measured.width);
|
|
512
|
+
maxBottom = Math.max(maxBottom, cursorY + measured.height);
|
|
513
|
+
rowHeight = Math.max(rowHeight, measured.height);
|
|
514
|
+
cursorX += measured.width + rootGap;
|
|
515
|
+
column += 1;
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
return {
|
|
519
|
+
positions,
|
|
520
|
+
width: Math.max(960, maxRight + canvasPadding),
|
|
521
|
+
height: Math.max(640, maxBottom + canvasPadding),
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function anchor(box, target) {
|
|
526
|
+
const centerX = box.x + box.width / 2;
|
|
527
|
+
const centerY = box.y + box.height / 2;
|
|
528
|
+
const targetX = target.x + target.width / 2;
|
|
529
|
+
const targetY = target.y + target.height / 2;
|
|
530
|
+
const dx = targetX - centerX;
|
|
531
|
+
const dy = targetY - centerY;
|
|
532
|
+
if (Math.abs(dx) > Math.abs(dy)) {
|
|
533
|
+
return {
|
|
534
|
+
x: dx > 0 ? box.x + box.width : box.x,
|
|
535
|
+
y: centerY,
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
return {
|
|
539
|
+
x: centerX,
|
|
540
|
+
y: dy > 0 ? box.y + box.height : box.y,
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function edgePath(sourceBox, targetBox) {
|
|
545
|
+
const start = anchor(sourceBox, targetBox);
|
|
546
|
+
const end = anchor(targetBox, sourceBox);
|
|
547
|
+
const dx = Math.abs(end.x - start.x);
|
|
548
|
+
const dy = Math.abs(end.y - start.y);
|
|
549
|
+
const curve = Math.max(48, Math.min(160, Math.max(dx * 0.35, dy * 0.35)));
|
|
550
|
+
const c1x = start.x + (end.x >= start.x ? curve : -curve);
|
|
551
|
+
const c2x = end.x + (end.x >= start.x ? -curve : curve);
|
|
552
|
+
return {
|
|
553
|
+
d: 'M ' + start.x + ' ' + start.y + ' C ' + c1x + ' ' + start.y + ', ' + c2x + ' ' + end.y + ', ' + end.x + ' ' + end.y,
|
|
554
|
+
labelX: (start.x + end.x) / 2,
|
|
555
|
+
labelY: (start.y + end.y) / 2 - 10,
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function renderDefs() {
|
|
560
|
+
const defs = node('defs');
|
|
561
|
+
|
|
562
|
+
const marker = node('marker', {
|
|
563
|
+
id: 'arrow',
|
|
564
|
+
viewBox: '0 0 10 10',
|
|
565
|
+
refX: 8,
|
|
566
|
+
refY: 5,
|
|
567
|
+
markerWidth: 7,
|
|
568
|
+
markerHeight: 7,
|
|
569
|
+
orient: 'auto-start-reverse',
|
|
570
|
+
});
|
|
571
|
+
marker.appendChild(node('path', {
|
|
572
|
+
d: 'M 0 0 L 10 5 L 0 10 z',
|
|
573
|
+
fill: cssValue('--dt-text-secondary'),
|
|
574
|
+
}));
|
|
575
|
+
defs.appendChild(marker);
|
|
576
|
+
|
|
577
|
+
const filter = node('filter', {
|
|
578
|
+
id: 'node-shadow',
|
|
579
|
+
x: '-20%',
|
|
580
|
+
y: '-20%',
|
|
581
|
+
width: '140%',
|
|
582
|
+
height: '140%',
|
|
583
|
+
});
|
|
584
|
+
filter.appendChild(node('feDropShadow', {
|
|
585
|
+
dx: 0,
|
|
586
|
+
dy: 12,
|
|
587
|
+
stdDeviation: 12,
|
|
588
|
+
'flood-color': cssValue('--dt-shadow'),
|
|
589
|
+
}));
|
|
590
|
+
defs.appendChild(filter);
|
|
591
|
+
|
|
592
|
+
Object.keys(typeVars).forEach((type) => {
|
|
593
|
+
const color = accentFor(type);
|
|
594
|
+
const gradient = node('linearGradient', {
|
|
595
|
+
id: 'grad-' + type,
|
|
596
|
+
x1: '0%',
|
|
597
|
+
y1: '0%',
|
|
598
|
+
x2: '100%',
|
|
599
|
+
y2: '100%',
|
|
600
|
+
});
|
|
601
|
+
gradient.appendChild(node('stop', {
|
|
602
|
+
offset: '0%',
|
|
603
|
+
'stop-color': rgba(color, 0.24),
|
|
604
|
+
}));
|
|
605
|
+
gradient.appendChild(node('stop', {
|
|
606
|
+
offset: '100%',
|
|
607
|
+
'stop-color': rgba(color, 0.06),
|
|
608
|
+
}));
|
|
609
|
+
defs.appendChild(gradient);
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
return defs;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function renderTextBlock(group, lines, x, y, fontSize, color, fontWeight, lineHeight, anchorMode) {
|
|
616
|
+
lines.forEach((line, index) => {
|
|
617
|
+
group.appendChild(node('text', {
|
|
618
|
+
x,
|
|
619
|
+
y: y + index * lineHeight,
|
|
620
|
+
fill: color,
|
|
621
|
+
'font-size': fontSize,
|
|
622
|
+
'font-weight': fontWeight,
|
|
623
|
+
'text-anchor': anchorMode || 'start',
|
|
624
|
+
}, line));
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function renderLabelBadge(group, text, x, y, color) {
|
|
629
|
+
const width = Math.max(52, text.length * 7 + 18);
|
|
630
|
+
group.appendChild(node('rect', {
|
|
631
|
+
x,
|
|
632
|
+
y,
|
|
633
|
+
width,
|
|
634
|
+
height: 22,
|
|
635
|
+
rx: 11,
|
|
636
|
+
fill: rgba(color, 0.18),
|
|
637
|
+
stroke: rgba(color, 0.35),
|
|
638
|
+
}));
|
|
639
|
+
group.appendChild(node('text', {
|
|
640
|
+
x: x + width / 2,
|
|
641
|
+
y: y + 15,
|
|
642
|
+
fill: color,
|
|
643
|
+
'font-size': 10,
|
|
644
|
+
'font-family': monoFont,
|
|
645
|
+
'font-weight': 700,
|
|
646
|
+
'letter-spacing': '0.08em',
|
|
647
|
+
'text-anchor': 'middle',
|
|
648
|
+
}, text.toUpperCase()));
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function renderGroup(id, model, positions, groupLayer) {
|
|
652
|
+
const data = model.map.get(id);
|
|
653
|
+
const box = positions.get(id);
|
|
654
|
+
const color = accentFor(data.type);
|
|
655
|
+
const labelWidth = Math.max(120, String(data.label || data.id).length * 8 + 32);
|
|
656
|
+
|
|
657
|
+
groupLayer.appendChild(node('rect', {
|
|
658
|
+
x: box.x,
|
|
659
|
+
y: box.y,
|
|
660
|
+
width: box.width,
|
|
661
|
+
height: box.height,
|
|
662
|
+
rx: 24,
|
|
663
|
+
fill: cssValue('--dt-group-fill'),
|
|
664
|
+
stroke: color,
|
|
665
|
+
'stroke-width': 1.6,
|
|
666
|
+
'stroke-dasharray': '10 8',
|
|
667
|
+
}));
|
|
668
|
+
groupLayer.appendChild(node('rect', {
|
|
669
|
+
x: box.x + 16,
|
|
670
|
+
y: box.y - 14,
|
|
671
|
+
width: labelWidth,
|
|
672
|
+
height: 28,
|
|
673
|
+
rx: 14,
|
|
674
|
+
fill: cssValue('--dt-bg-primary'),
|
|
675
|
+
stroke: color,
|
|
676
|
+
}));
|
|
677
|
+
groupLayer.appendChild(node('text', {
|
|
678
|
+
x: box.x + 28,
|
|
679
|
+
y: box.y + 4,
|
|
680
|
+
fill: color,
|
|
681
|
+
'font-size': 11,
|
|
682
|
+
'font-family': monoFont,
|
|
683
|
+
'font-weight': 700,
|
|
684
|
+
'letter-spacing': '0.08em',
|
|
685
|
+
}, String(data.label || data.id).toUpperCase()));
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function renderLeaf(id, model, positions, nodeLayer) {
|
|
689
|
+
const data = model.map.get(id);
|
|
690
|
+
const box = positions.get(id);
|
|
691
|
+
const color = accentFor(data.type);
|
|
692
|
+
const labelLines = wrapText(data.label || data.id, 20, 2);
|
|
693
|
+
const descLines = wrapText(data.description || '', 28, 2);
|
|
694
|
+
const group = node('g');
|
|
695
|
+
|
|
696
|
+
group.appendChild(node('rect', {
|
|
697
|
+
x: box.x,
|
|
698
|
+
y: box.y,
|
|
699
|
+
width: box.width,
|
|
700
|
+
height: box.height,
|
|
701
|
+
rx: 22,
|
|
702
|
+
fill: 'url(#grad-' + (typeVars[data.type] ? data.type : 'external') + ')',
|
|
703
|
+
stroke: color,
|
|
704
|
+
'stroke-width': 1.8,
|
|
705
|
+
filter: 'url(#node-shadow)',
|
|
706
|
+
}));
|
|
707
|
+
group.appendChild(node('rect', {
|
|
708
|
+
x: box.x + 1,
|
|
709
|
+
y: box.y + 1,
|
|
710
|
+
width: box.width - 2,
|
|
711
|
+
height: box.height - 2,
|
|
712
|
+
rx: 21,
|
|
713
|
+
fill: cssValue('--dt-node-fill'),
|
|
714
|
+
}));
|
|
715
|
+
group.appendChild(node('rect', {
|
|
716
|
+
x: box.x + 18,
|
|
717
|
+
y: box.y + 18,
|
|
718
|
+
width: 6,
|
|
719
|
+
height: box.height - 36,
|
|
720
|
+
rx: 3,
|
|
721
|
+
fill: color,
|
|
722
|
+
}));
|
|
723
|
+
renderLabelBadge(group, data.type || 'node', box.x + 30, box.y + 16, color);
|
|
724
|
+
renderTextBlock(group, labelLines, box.x + 30, box.y + 56, 19, cssValue('--dt-text-primary'), 700, 22, 'start');
|
|
725
|
+
if (descLines.length) {
|
|
726
|
+
renderTextBlock(group, descLines, box.x + 30, box.y + 88, 12, cssValue('--dt-text-secondary'), 500, 16, 'start');
|
|
727
|
+
}
|
|
728
|
+
if (data.technology) {
|
|
729
|
+
const techY = box.y + box.height - 34;
|
|
730
|
+
const techWidth = Math.min(box.width - 48, Math.max(78, String(data.technology).length * 7 + 22));
|
|
731
|
+
group.appendChild(node('rect', {
|
|
732
|
+
x: box.x + 30,
|
|
733
|
+
y: techY,
|
|
734
|
+
width: techWidth,
|
|
735
|
+
height: 22,
|
|
736
|
+
rx: 11,
|
|
737
|
+
fill: rgba(color, 0.14),
|
|
738
|
+
stroke: rgba(color, 0.32),
|
|
739
|
+
}));
|
|
740
|
+
group.appendChild(node('text', {
|
|
741
|
+
x: box.x + 41,
|
|
742
|
+
y: techY + 15,
|
|
743
|
+
fill: cssValue('--dt-text-primary'),
|
|
744
|
+
'font-size': 10.5,
|
|
745
|
+
'font-family': monoFont,
|
|
746
|
+
}, data.technology));
|
|
747
|
+
}
|
|
748
|
+
nodeLayer.appendChild(group);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function renderEdge(edge, positions, edgeLayer) {
|
|
752
|
+
const source = positions.get(edge.source);
|
|
753
|
+
const target = positions.get(edge.target);
|
|
754
|
+
if (!source || !target) {
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
const route = edgePath(source, target);
|
|
758
|
+
edgeLayer.appendChild(node('path', {
|
|
759
|
+
d: route.d,
|
|
760
|
+
fill: 'none',
|
|
761
|
+
stroke: cssValue('--dt-text-secondary'),
|
|
762
|
+
'stroke-width': 2,
|
|
763
|
+
'stroke-linecap': 'round',
|
|
764
|
+
'stroke-linejoin': 'round',
|
|
765
|
+
'marker-end': 'url(#arrow)',
|
|
766
|
+
opacity: 0.86,
|
|
767
|
+
}));
|
|
768
|
+
if (edge.label) {
|
|
769
|
+
const width = Math.max(62, String(edge.label).length * 7 + 16);
|
|
770
|
+
edgeLayer.appendChild(node('rect', {
|
|
771
|
+
x: route.labelX - width / 2,
|
|
772
|
+
y: route.labelY - 14,
|
|
773
|
+
width,
|
|
774
|
+
height: 24,
|
|
775
|
+
rx: 12,
|
|
776
|
+
fill: cssValue('--dt-bg-primary'),
|
|
777
|
+
stroke: cssValue('--dt-border'),
|
|
778
|
+
}));
|
|
779
|
+
edgeLayer.appendChild(node('text', {
|
|
780
|
+
x: route.labelX,
|
|
781
|
+
y: route.labelY + 2,
|
|
782
|
+
fill: cssValue('--dt-text-secondary'),
|
|
783
|
+
'font-size': 10,
|
|
784
|
+
'font-family': monoFont,
|
|
785
|
+
'text-anchor': 'middle',
|
|
786
|
+
}, edge.label));
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function render() {
|
|
791
|
+
const raw = parseData();
|
|
792
|
+
if (raw === null) {
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
const model = buildModel(raw);
|
|
796
|
+
if (!model.nodes.length) {
|
|
797
|
+
showEmpty('No data provided', 'Populate #diagram-data with nodes and optional edges to render the diagram.');
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
hideEmpty();
|
|
801
|
+
const { positions, width, height } = layout(model);
|
|
802
|
+
svg.innerHTML = '';
|
|
803
|
+
svg.setAttribute('viewBox', '0 0 ' + width + ' ' + height);
|
|
804
|
+
svg.setAttribute('aria-label', (raw.title || 'Architecture diagram') + ' with ' + model.nodes.length + ' nodes');
|
|
805
|
+
|
|
806
|
+
svg.appendChild(renderDefs());
|
|
807
|
+
|
|
808
|
+
const title = node('title', { id: 'diagram-title' }, raw.title || 'Architecture diagram');
|
|
809
|
+
const description = node('desc', { id: 'diagram-description' }, raw.description || 'Static architecture diagram rendered with SVG');
|
|
810
|
+
svg.appendChild(title);
|
|
811
|
+
svg.appendChild(description);
|
|
812
|
+
|
|
813
|
+
const edgeLayer = node('g', { 'aria-hidden': 'true' });
|
|
814
|
+
const groupLayer = node('g', { 'aria-hidden': 'true' });
|
|
815
|
+
const nodeLayer = node('g');
|
|
816
|
+
|
|
817
|
+
(model.edges || []).forEach((edge) => renderEdge(edge, positions, edgeLayer));
|
|
818
|
+
model.nodes.forEach((item) => {
|
|
819
|
+
if ((model.children.get(item.id) || []).length) {
|
|
820
|
+
renderGroup(item.id, model, positions, groupLayer);
|
|
821
|
+
}
|
|
822
|
+
});
|
|
823
|
+
model.nodes.forEach((item) => {
|
|
824
|
+
if (!(model.children.get(item.id) || []).length) {
|
|
825
|
+
renderLeaf(item.id, model, positions, nodeLayer);
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
svg.appendChild(edgeLayer);
|
|
830
|
+
svg.appendChild(groupLayer);
|
|
831
|
+
svg.appendChild(nodeLayer);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function updateThemeButton() {
|
|
835
|
+
const value = root.getAttribute('data-theme') || 'auto';
|
|
836
|
+
themeButton.textContent = 'Theme: ' + value.charAt(0).toUpperCase() + value.slice(1);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function cycleTheme() {
|
|
840
|
+
const current = root.getAttribute('data-theme') || 'auto';
|
|
841
|
+
const next = themes[(themes.indexOf(current) + 1) % themes.length];
|
|
842
|
+
root.setAttribute('data-theme', next);
|
|
843
|
+
updateThemeButton();
|
|
844
|
+
render();
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function exportSvg() {
|
|
848
|
+
const clone = svg.cloneNode(true);
|
|
849
|
+
clone.setAttribute('xmlns', svgNs);
|
|
850
|
+
const payload = '<?xml version="1.0" encoding="UTF-8"?>\n' + new XMLSerializer().serializeToString(clone);
|
|
851
|
+
const blob = new Blob([payload], { type: 'image/svg+xml;charset=utf-8' });
|
|
852
|
+
const url = URL.createObjectURL(blob);
|
|
853
|
+
const anchorEl = document.createElement('a');
|
|
854
|
+
anchorEl.href = url;
|
|
855
|
+
anchorEl.download = 'architecture-diagram.svg';
|
|
856
|
+
anchorEl.click();
|
|
857
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
themeButton.addEventListener('click', cycleTheme);
|
|
861
|
+
exportButton.addEventListener('click', exportSvg);
|
|
862
|
+
updateThemeButton();
|
|
863
|
+
render();
|
|
864
|
+
})();
|
|
865
|
+
</script>
|
|
866
|
+
</body>
|
|
867
|
+
</html>
|