@vpxa/aikit 0.1.159 → 0.1.161
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-7yl2VzTw.js → server--D-DJ9_g.js} +41 -41
- package/packages/server/viewers/architecture-static.html +877 -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 +1081 -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,764 @@
|
|
|
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 - Process Flow 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
|
+
}
|
|
29
|
+
|
|
30
|
+
[data-theme="light"] {
|
|
31
|
+
--dt-bg-primary: #ffffff;
|
|
32
|
+
--dt-bg-surface: #f8fafc;
|
|
33
|
+
--dt-text-primary: #0f172a;
|
|
34
|
+
--dt-text-secondary: #64748b;
|
|
35
|
+
--dt-border: #e2e8f0;
|
|
36
|
+
--dt-grid: #f1f5f9;
|
|
37
|
+
--dt-shadow: rgba(148, 163, 184, 0.24);
|
|
38
|
+
--dt-toolbar-bg: rgba(255, 255, 255, 0.88);
|
|
39
|
+
--dt-button-bg: rgba(248, 250, 252, 0.96);
|
|
40
|
+
--dt-button-hover: rgba(226, 232, 240, 0.96);
|
|
41
|
+
--dt-panel-bg: linear-gradient(180deg, rgba(248, 250, 252, 0.96), rgba(241, 245, 249, 1));
|
|
42
|
+
--dt-badge-bg: rgba(255, 255, 255, 0.9);
|
|
43
|
+
--dt-node-fill: rgba(255, 255, 255, 0.96);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@media (prefers-color-scheme: light) {
|
|
47
|
+
:root:not([data-theme="dark"]) {
|
|
48
|
+
--dt-bg-primary: #ffffff;
|
|
49
|
+
--dt-bg-surface: #f8fafc;
|
|
50
|
+
--dt-text-primary: #0f172a;
|
|
51
|
+
--dt-text-secondary: #64748b;
|
|
52
|
+
--dt-border: #e2e8f0;
|
|
53
|
+
--dt-grid: #f1f5f9;
|
|
54
|
+
--dt-shadow: rgba(148, 163, 184, 0.24);
|
|
55
|
+
--dt-toolbar-bg: rgba(255, 255, 255, 0.88);
|
|
56
|
+
--dt-button-bg: rgba(248, 250, 252, 0.96);
|
|
57
|
+
--dt-button-hover: rgba(226, 232, 240, 0.96);
|
|
58
|
+
--dt-panel-bg: linear-gradient(180deg, rgba(248, 250, 252, 0.96), rgba(241, 245, 249, 1));
|
|
59
|
+
--dt-badge-bg: rgba(255, 255, 255, 0.9);
|
|
60
|
+
--dt-node-fill: rgba(255, 255, 255, 0.96);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
* {
|
|
65
|
+
box-sizing: border-box;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
html,
|
|
69
|
+
body {
|
|
70
|
+
height: 100%;
|
|
71
|
+
margin: 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
body {
|
|
75
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
76
|
+
color: var(--dt-text-primary);
|
|
77
|
+
background:
|
|
78
|
+
radial-gradient(circle at top left, rgba(245, 158, 11, 0.13), transparent 28%),
|
|
79
|
+
radial-gradient(circle at bottom right, rgba(244, 63, 94, 0.12), transparent 28%),
|
|
80
|
+
var(--dt-bg-primary);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.shell {
|
|
84
|
+
min-height: 100%;
|
|
85
|
+
display: grid;
|
|
86
|
+
grid-template-rows: auto 1fr;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.toolbar {
|
|
90
|
+
position: sticky;
|
|
91
|
+
top: 0;
|
|
92
|
+
z-index: 2;
|
|
93
|
+
display: flex;
|
|
94
|
+
align-items: center;
|
|
95
|
+
justify-content: space-between;
|
|
96
|
+
gap: 1rem;
|
|
97
|
+
padding: 1rem 1.25rem;
|
|
98
|
+
border-bottom: 1px solid var(--dt-border);
|
|
99
|
+
background: var(--dt-toolbar-bg);
|
|
100
|
+
backdrop-filter: blur(16px);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.title h1 {
|
|
104
|
+
margin: 0;
|
|
105
|
+
font-size: 1rem;
|
|
106
|
+
font-weight: 700;
|
|
107
|
+
letter-spacing: 0.02em;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.title p {
|
|
111
|
+
margin: 0.2rem 0 0;
|
|
112
|
+
font-size: 0.82rem;
|
|
113
|
+
color: var(--dt-text-secondary);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.actions {
|
|
117
|
+
display: flex;
|
|
118
|
+
flex-wrap: wrap;
|
|
119
|
+
justify-content: flex-end;
|
|
120
|
+
gap: 0.6rem;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
button {
|
|
124
|
+
appearance: none;
|
|
125
|
+
border: 1px solid var(--dt-border);
|
|
126
|
+
background: var(--dt-button-bg);
|
|
127
|
+
color: var(--dt-text-primary);
|
|
128
|
+
border-radius: 999px;
|
|
129
|
+
padding: 0.7rem 1rem;
|
|
130
|
+
font: inherit;
|
|
131
|
+
cursor: pointer;
|
|
132
|
+
transition: background 160ms ease, border-color 160ms ease, transform 160ms ease;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
button:hover {
|
|
136
|
+
background: var(--dt-button-hover);
|
|
137
|
+
transform: translateY(-1px);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.stage {
|
|
141
|
+
padding: 1rem;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.canvas-shell {
|
|
145
|
+
position: relative;
|
|
146
|
+
min-height: calc(100vh - 5.5rem);
|
|
147
|
+
border: 1px solid var(--dt-border);
|
|
148
|
+
border-radius: 24px;
|
|
149
|
+
background: var(--dt-panel-bg);
|
|
150
|
+
overflow: hidden;
|
|
151
|
+
box-shadow: 0 24px 60px var(--dt-shadow);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
svg {
|
|
155
|
+
display: block;
|
|
156
|
+
width: 100%;
|
|
157
|
+
height: min(78vh, 980px);
|
|
158
|
+
min-height: 480px;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.empty-state {
|
|
162
|
+
position: absolute;
|
|
163
|
+
inset: 0;
|
|
164
|
+
display: none;
|
|
165
|
+
place-items: center;
|
|
166
|
+
padding: 2rem;
|
|
167
|
+
text-align: center;
|
|
168
|
+
color: var(--dt-text-secondary);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.empty-state.is-visible {
|
|
172
|
+
display: grid;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.empty-state strong {
|
|
176
|
+
display: block;
|
|
177
|
+
margin-bottom: 0.4rem;
|
|
178
|
+
color: var(--dt-text-primary);
|
|
179
|
+
font-size: 1rem;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.badge {
|
|
183
|
+
position: fixed;
|
|
184
|
+
right: 1rem;
|
|
185
|
+
bottom: 1rem;
|
|
186
|
+
z-index: 3;
|
|
187
|
+
display: inline-flex;
|
|
188
|
+
align-items: center;
|
|
189
|
+
gap: 0.55rem;
|
|
190
|
+
padding: 0.55rem 0.8rem;
|
|
191
|
+
border: 1px solid var(--dt-border);
|
|
192
|
+
border-radius: 999px;
|
|
193
|
+
background: var(--dt-badge-bg);
|
|
194
|
+
color: var(--dt-text-secondary);
|
|
195
|
+
backdrop-filter: blur(14px);
|
|
196
|
+
font-size: 0.74rem;
|
|
197
|
+
letter-spacing: 0.04em;
|
|
198
|
+
text-transform: uppercase;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.badge-mark {
|
|
202
|
+
width: 0.55rem;
|
|
203
|
+
height: 0.55rem;
|
|
204
|
+
border-radius: 999px;
|
|
205
|
+
background: var(--dt-violet);
|
|
206
|
+
box-shadow: 0 0 0 0.18rem rgba(139, 92, 246, 0.18);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
@media (max-width: 720px) {
|
|
210
|
+
.toolbar {
|
|
211
|
+
align-items: flex-start;
|
|
212
|
+
flex-direction: column;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.actions {
|
|
216
|
+
width: 100%;
|
|
217
|
+
justify-content: flex-start;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.stage {
|
|
221
|
+
padding: 0.75rem;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
svg {
|
|
225
|
+
min-height: 420px;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
</style>
|
|
229
|
+
</head>
|
|
230
|
+
<body>
|
|
231
|
+
<div class="shell">
|
|
232
|
+
<header class="toolbar">
|
|
233
|
+
<div class="title">
|
|
234
|
+
<h1>Process Flow Diagram</h1>
|
|
235
|
+
<p>Static SVG flow renderer for lightweight inline transport.</p>
|
|
236
|
+
</div>
|
|
237
|
+
<div class="actions">
|
|
238
|
+
<button id="theme-button" type="button">Theme: Auto</button>
|
|
239
|
+
<button id="export-button" type="button">Export SVG</button>
|
|
240
|
+
</div>
|
|
241
|
+
</header>
|
|
242
|
+
<main class="stage">
|
|
243
|
+
<div class="canvas-shell">
|
|
244
|
+
<div class="empty-state" id="empty-state" aria-live="polite"></div>
|
|
245
|
+
<svg id="diagram" role="img" aria-labelledby="diagram-title diagram-description"></svg>
|
|
246
|
+
</div>
|
|
247
|
+
</main>
|
|
248
|
+
</div>
|
|
249
|
+
<div class="badge"><span class="badge-mark"></span>AI Kit static viewer</div>
|
|
250
|
+
<script type="application/json" id="diagram-data">{}</script>
|
|
251
|
+
<script>
|
|
252
|
+
(() => {
|
|
253
|
+
const svgNs = 'http://www.w3.org/2000/svg';
|
|
254
|
+
const root = document.documentElement;
|
|
255
|
+
const svg = document.getElementById('diagram');
|
|
256
|
+
const emptyState = document.getElementById('empty-state');
|
|
257
|
+
const exportButton = document.getElementById('export-button');
|
|
258
|
+
const themeButton = document.getElementById('theme-button');
|
|
259
|
+
const themes = ['auto', 'dark', 'light'];
|
|
260
|
+
const monoFont = '"JetBrains Mono", "SFMono-Regular", Consolas, ui-monospace, monospace';
|
|
261
|
+
const typeVars = {
|
|
262
|
+
'start-end': '--dt-cyan',
|
|
263
|
+
manual: '--dt-emerald',
|
|
264
|
+
automated: '--dt-violet',
|
|
265
|
+
integration: '--dt-amber',
|
|
266
|
+
decision: '--dt-rose',
|
|
267
|
+
prerequisite: '--dt-slate',
|
|
268
|
+
};
|
|
269
|
+
const laneOffsets = [0, 280, -280, 520, -520];
|
|
270
|
+
const topPadding = 88;
|
|
271
|
+
const centerX = 560;
|
|
272
|
+
const gapY = 148;
|
|
273
|
+
const canvasPadding = 64;
|
|
274
|
+
|
|
275
|
+
function node(tag, attributes, text) {
|
|
276
|
+
const element = document.createElementNS(svgNs, tag);
|
|
277
|
+
if (attributes) {
|
|
278
|
+
Object.entries(attributes).forEach(([name, value]) => {
|
|
279
|
+
if (value !== undefined && value !== null) {
|
|
280
|
+
element.setAttribute(name, String(value));
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
if (text !== undefined) {
|
|
285
|
+
element.textContent = text;
|
|
286
|
+
}
|
|
287
|
+
return element;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function cssValue(name) {
|
|
291
|
+
return getComputedStyle(root).getPropertyValue(name).trim();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function rgba(hex, alpha) {
|
|
295
|
+
const cleaned = String(hex || '').replace('#', '').trim();
|
|
296
|
+
const value = cleaned.length === 3
|
|
297
|
+
? cleaned.split('').map((char) => char + char).join('')
|
|
298
|
+
: cleaned;
|
|
299
|
+
const number = Number.parseInt(value, 16);
|
|
300
|
+
const red = (number >> 16) & 255;
|
|
301
|
+
const green = (number >> 8) & 255;
|
|
302
|
+
const blue = number & 255;
|
|
303
|
+
return 'rgba(' + red + ', ' + green + ', ' + blue + ', ' + alpha + ')';
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function colorFor(type) {
|
|
307
|
+
return cssValue(typeVars[type] || '--dt-slate');
|
|
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 escapeHtml(value) {
|
|
338
|
+
return String(value)
|
|
339
|
+
.replace(/&/g, '&')
|
|
340
|
+
.replace(/</g, '<')
|
|
341
|
+
.replace(/>/g, '>')
|
|
342
|
+
.replace(/"/g, '"')
|
|
343
|
+
.replace(/'/g, ''');
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function showEmpty(title, message) {
|
|
347
|
+
emptyState.classList.add('is-visible');
|
|
348
|
+
emptyState.innerHTML = '<div><strong>' + escapeHtml(title) + '</strong><span>' + escapeHtml(message) + '</span></div>';
|
|
349
|
+
svg.innerHTML = '';
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function hideEmpty() {
|
|
353
|
+
emptyState.classList.remove('is-visible');
|
|
354
|
+
emptyState.textContent = '';
|
|
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 measureNode(item) {
|
|
371
|
+
const hasDescription = Boolean(item.description);
|
|
372
|
+
if (item.type === 'start-end') {
|
|
373
|
+
return { width: 220, height: 72 + (hasDescription ? 16 : 0) };
|
|
374
|
+
}
|
|
375
|
+
if (item.type === 'decision') {
|
|
376
|
+
return { width: 196, height: 120 };
|
|
377
|
+
}
|
|
378
|
+
return { width: 224, height: hasDescription ? 98 : 82 };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function buildModel(raw) {
|
|
382
|
+
const nodes = Array.isArray(raw && raw.nodes) ? raw.nodes : [];
|
|
383
|
+
const edges = Array.isArray(raw && raw.edges) ? raw.edges : [];
|
|
384
|
+
const map = new Map();
|
|
385
|
+
const outgoing = new Map();
|
|
386
|
+
const incoming = new Map();
|
|
387
|
+
nodes.forEach((item) => {
|
|
388
|
+
map.set(item.id, Object.assign({ type: 'manual' }, item));
|
|
389
|
+
outgoing.set(item.id, []);
|
|
390
|
+
incoming.set(item.id, []);
|
|
391
|
+
});
|
|
392
|
+
edges.forEach((edge) => {
|
|
393
|
+
if (outgoing.has(edge.source)) {
|
|
394
|
+
outgoing.get(edge.source).push(edge);
|
|
395
|
+
}
|
|
396
|
+
if (incoming.has(edge.target)) {
|
|
397
|
+
incoming.get(edge.target).push(edge);
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
return { nodes, edges, map, outgoing, incoming };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function layout(model) {
|
|
404
|
+
const positions = new Map();
|
|
405
|
+
const nodeOrder = model.nodes.map((item) => item.id);
|
|
406
|
+
|
|
407
|
+
model.nodes.forEach((item, index) => {
|
|
408
|
+
const size = measureNode(item);
|
|
409
|
+
positions.set(item.id, {
|
|
410
|
+
width: size.width,
|
|
411
|
+
height: size.height,
|
|
412
|
+
x: centerX - size.width / 2,
|
|
413
|
+
y: topPadding + index * gapY,
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
model.nodes.forEach((item) => {
|
|
418
|
+
const pos = positions.get(item.id);
|
|
419
|
+
const incoming = model.incoming.get(item.id) || [];
|
|
420
|
+
if (!incoming.length) {
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
const nonLoop = incoming.filter((edge) => edge.type !== 'loop-back');
|
|
424
|
+
if (nonLoop.length === 1) {
|
|
425
|
+
const source = positions.get(nonLoop[0].source);
|
|
426
|
+
if (source && model.map.get(nonLoop[0].source).type !== 'decision') {
|
|
427
|
+
pos.x = source.x + (source.width - pos.width) / 2;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
model.nodes.forEach((item) => {
|
|
433
|
+
if (item.type !== 'decision') {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
const sourcePos = positions.get(item.id);
|
|
437
|
+
const standardTargets = (model.outgoing.get(item.id) || []).filter((edge) => edge.type !== 'loop-back');
|
|
438
|
+
standardTargets.forEach((edge, index) => {
|
|
439
|
+
const targetPos = positions.get(edge.target);
|
|
440
|
+
if (!targetPos) {
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
const lane = laneOffsets[index + 1] || laneOffsets[laneOffsets.length - 1];
|
|
444
|
+
targetPos.x = centerX - targetPos.width / 2 + lane;
|
|
445
|
+
targetPos.y = Math.max(targetPos.y, sourcePos.y + gapY);
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
model.nodes.forEach((item) => {
|
|
450
|
+
const pos = positions.get(item.id);
|
|
451
|
+
const deps = (model.incoming.get(item.id) || []).filter((edge) => edge.type !== 'loop-back');
|
|
452
|
+
if (deps.length > 1) {
|
|
453
|
+
const average = deps.reduce((sum, edge) => {
|
|
454
|
+
const source = positions.get(edge.source);
|
|
455
|
+
return sum + (source ? source.x + source.width / 2 : centerX);
|
|
456
|
+
}, 0) / deps.length;
|
|
457
|
+
pos.x = average - pos.width / 2;
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
let minX = Infinity;
|
|
462
|
+
let maxX = -Infinity;
|
|
463
|
+
let maxY = 0;
|
|
464
|
+
positions.forEach((box) => {
|
|
465
|
+
minX = Math.min(minX, box.x);
|
|
466
|
+
maxX = Math.max(maxX, box.x + box.width);
|
|
467
|
+
maxY = Math.max(maxY, box.y + box.height);
|
|
468
|
+
});
|
|
469
|
+
const shift = minX < canvasPadding ? canvasPadding - minX : 0;
|
|
470
|
+
positions.forEach((box) => {
|
|
471
|
+
box.x += shift;
|
|
472
|
+
});
|
|
473
|
+
return {
|
|
474
|
+
positions,
|
|
475
|
+
width: Math.max(1020, maxX + shift + canvasPadding),
|
|
476
|
+
height: Math.max(700, maxY + canvasPadding),
|
|
477
|
+
order: nodeOrder,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function renderDefs() {
|
|
482
|
+
const defs = node('defs');
|
|
483
|
+
const pattern = node('pattern', {
|
|
484
|
+
id: 'grid',
|
|
485
|
+
width: 40,
|
|
486
|
+
height: 40,
|
|
487
|
+
patternUnits: 'userSpaceOnUse',
|
|
488
|
+
});
|
|
489
|
+
pattern.appendChild(node('path', {
|
|
490
|
+
d: 'M 40 0 L 0 0 0 40',
|
|
491
|
+
fill: 'none',
|
|
492
|
+
stroke: cssValue('--dt-grid'),
|
|
493
|
+
'stroke-width': 1,
|
|
494
|
+
}));
|
|
495
|
+
defs.appendChild(pattern);
|
|
496
|
+
|
|
497
|
+
const marker = node('marker', {
|
|
498
|
+
id: 'arrow',
|
|
499
|
+
viewBox: '0 0 10 10',
|
|
500
|
+
refX: 8,
|
|
501
|
+
refY: 5,
|
|
502
|
+
markerWidth: 7,
|
|
503
|
+
markerHeight: 7,
|
|
504
|
+
orient: 'auto-start-reverse',
|
|
505
|
+
});
|
|
506
|
+
marker.appendChild(node('path', {
|
|
507
|
+
d: 'M 0 0 L 10 5 L 0 10 z',
|
|
508
|
+
fill: cssValue('--dt-text-secondary'),
|
|
509
|
+
}));
|
|
510
|
+
defs.appendChild(marker);
|
|
511
|
+
|
|
512
|
+
const filter = node('filter', {
|
|
513
|
+
id: 'node-shadow',
|
|
514
|
+
x: '-20%',
|
|
515
|
+
y: '-20%',
|
|
516
|
+
width: '140%',
|
|
517
|
+
height: '140%',
|
|
518
|
+
});
|
|
519
|
+
filter.appendChild(node('feDropShadow', {
|
|
520
|
+
dx: 0,
|
|
521
|
+
dy: 12,
|
|
522
|
+
stdDeviation: 12,
|
|
523
|
+
'flood-color': cssValue('--dt-shadow'),
|
|
524
|
+
}));
|
|
525
|
+
defs.appendChild(filter);
|
|
526
|
+
|
|
527
|
+
Object.keys(typeVars).forEach((type) => {
|
|
528
|
+
const color = colorFor(type);
|
|
529
|
+
const gradient = node('linearGradient', {
|
|
530
|
+
id: 'grad-' + type.replace(/[^a-z]/g, ''),
|
|
531
|
+
x1: '0%',
|
|
532
|
+
y1: '0%',
|
|
533
|
+
x2: '100%',
|
|
534
|
+
y2: '100%',
|
|
535
|
+
});
|
|
536
|
+
gradient.appendChild(node('stop', { offset: '0%', 'stop-color': rgba(color, 0.24) }));
|
|
537
|
+
gradient.appendChild(node('stop', { offset: '100%', 'stop-color': rgba(color, 0.06) }));
|
|
538
|
+
defs.appendChild(gradient);
|
|
539
|
+
});
|
|
540
|
+
return defs;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function edgeColor(edge) {
|
|
544
|
+
if (edge.type === 'exception') {
|
|
545
|
+
return cssValue('--dt-rose');
|
|
546
|
+
}
|
|
547
|
+
return cssValue('--dt-text-secondary');
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function routeEdge(edge, source, target) {
|
|
551
|
+
if (edge.type === 'loop-back') {
|
|
552
|
+
const startX = source.x;
|
|
553
|
+
const startY = source.y + source.height / 2;
|
|
554
|
+
const endX = target.x;
|
|
555
|
+
const endY = target.y + target.height / 2;
|
|
556
|
+
const bend = Math.max(120, Math.abs(startY - endY) * 0.35);
|
|
557
|
+
return {
|
|
558
|
+
d: 'M ' + startX + ' ' + startY + ' C ' + (startX - 170) + ' ' + startY + ', ' + (endX - bend) + ' ' + endY + ', ' + endX + ' ' + endY,
|
|
559
|
+
labelX: Math.min(startX, endX) - 84,
|
|
560
|
+
labelY: (startY + endY) / 2 - 8,
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
const startX = source.x + source.width / 2;
|
|
564
|
+
const startY = source.y + source.height;
|
|
565
|
+
const endX = target.x + target.width / 2;
|
|
566
|
+
const endY = target.y;
|
|
567
|
+
const curve = Math.max(52, Math.abs(endY - startY) * 0.45);
|
|
568
|
+
return {
|
|
569
|
+
d: 'M ' + startX + ' ' + startY + ' C ' + startX + ' ' + (startY + curve) + ', ' + endX + ' ' + (endY - curve) + ', ' + endX + ' ' + endY,
|
|
570
|
+
labelX: (startX + endX) / 2,
|
|
571
|
+
labelY: (startY + endY) / 2 - 8,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function renderEdge(edge, positions, edgeLayer) {
|
|
576
|
+
const source = positions.get(edge.source);
|
|
577
|
+
const target = positions.get(edge.target);
|
|
578
|
+
if (!source || !target) {
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
const route = routeEdge(edge, source, target);
|
|
582
|
+
const style = {
|
|
583
|
+
d: route.d,
|
|
584
|
+
fill: 'none',
|
|
585
|
+
stroke: edgeColor(edge),
|
|
586
|
+
'stroke-width': edge.type === 'exception' ? 2.4 : 2,
|
|
587
|
+
'stroke-linecap': 'round',
|
|
588
|
+
'stroke-linejoin': 'round',
|
|
589
|
+
'marker-end': 'url(#arrow)',
|
|
590
|
+
};
|
|
591
|
+
if (edge.type === 'loop-back' || edge.type === 'exception') {
|
|
592
|
+
style['stroke-dasharray'] = edge.type === 'loop-back' ? '8 8' : '6 8';
|
|
593
|
+
}
|
|
594
|
+
edgeLayer.appendChild(node('path', style));
|
|
595
|
+
if (edge.label) {
|
|
596
|
+
const width = Math.max(54, String(edge.label).length * 7 + 16);
|
|
597
|
+
edgeLayer.appendChild(node('rect', {
|
|
598
|
+
x: route.labelX - width / 2,
|
|
599
|
+
y: route.labelY - 14,
|
|
600
|
+
width,
|
|
601
|
+
height: 24,
|
|
602
|
+
rx: 12,
|
|
603
|
+
fill: cssValue('--dt-bg-primary'),
|
|
604
|
+
stroke: cssValue('--dt-border'),
|
|
605
|
+
}));
|
|
606
|
+
edgeLayer.appendChild(node('text', {
|
|
607
|
+
x: route.labelX,
|
|
608
|
+
y: route.labelY + 2,
|
|
609
|
+
fill: edgeColor(edge),
|
|
610
|
+
'font-size': 10,
|
|
611
|
+
'font-family': monoFont,
|
|
612
|
+
'text-anchor': 'middle',
|
|
613
|
+
}, edge.label));
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function renderTextLines(lines, group, x, y, fontSize, color, fontWeight, lineHeight) {
|
|
618
|
+
lines.forEach((line, index) => {
|
|
619
|
+
group.appendChild(node('text', {
|
|
620
|
+
x,
|
|
621
|
+
y: y + index * lineHeight,
|
|
622
|
+
fill: color,
|
|
623
|
+
'font-size': fontSize,
|
|
624
|
+
'font-weight': fontWeight,
|
|
625
|
+
'text-anchor': 'middle',
|
|
626
|
+
}, line));
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function renderNode(item, box, nodeLayer) {
|
|
631
|
+
const color = colorFor(item.type);
|
|
632
|
+
const fillId = 'grad-' + item.type.replace(/[^a-z]/g, '');
|
|
633
|
+
const group = node('g');
|
|
634
|
+
const centerX = box.x + box.width / 2;
|
|
635
|
+
const centerY = box.y + box.height / 2;
|
|
636
|
+
const labelLines = wrapText(item.label || item.id, item.type === 'decision' ? 14 : 20, 2);
|
|
637
|
+
const descLines = wrapText(item.description || '', item.type === 'decision' ? 16 : 22, 2);
|
|
638
|
+
|
|
639
|
+
if (item.type === 'decision') {
|
|
640
|
+
const points = [
|
|
641
|
+
[centerX, box.y],
|
|
642
|
+
[box.x + box.width, centerY],
|
|
643
|
+
[centerX, box.y + box.height],
|
|
644
|
+
[box.x, centerY],
|
|
645
|
+
].map((point) => point.join(',')).join(' ');
|
|
646
|
+
group.appendChild(node('polygon', {
|
|
647
|
+
points,
|
|
648
|
+
fill: 'url(#' + fillId + ')',
|
|
649
|
+
stroke: color,
|
|
650
|
+
'stroke-width': 1.8,
|
|
651
|
+
filter: 'url(#node-shadow)',
|
|
652
|
+
}));
|
|
653
|
+
group.appendChild(node('polygon', {
|
|
654
|
+
points,
|
|
655
|
+
fill: cssValue('--dt-node-fill'),
|
|
656
|
+
transform: 'translate(0 0) scale(0.988 0.988)',
|
|
657
|
+
'transform-origin': centerX + 'px ' + centerY + 'px',
|
|
658
|
+
}));
|
|
659
|
+
} else {
|
|
660
|
+
const rx = item.type === 'start-end' ? box.height / 2 : 22;
|
|
661
|
+
group.appendChild(node('rect', {
|
|
662
|
+
x: box.x,
|
|
663
|
+
y: box.y,
|
|
664
|
+
width: box.width,
|
|
665
|
+
height: box.height,
|
|
666
|
+
rx,
|
|
667
|
+
fill: 'url(#' + fillId + ')',
|
|
668
|
+
stroke: color,
|
|
669
|
+
'stroke-width': 1.8,
|
|
670
|
+
filter: 'url(#node-shadow)',
|
|
671
|
+
'stroke-dasharray': item.type === 'prerequisite' ? '8 7' : undefined,
|
|
672
|
+
}));
|
|
673
|
+
group.appendChild(node('rect', {
|
|
674
|
+
x: box.x + 1,
|
|
675
|
+
y: box.y + 1,
|
|
676
|
+
width: box.width - 2,
|
|
677
|
+
height: box.height - 2,
|
|
678
|
+
rx: Math.max(0, rx - 1),
|
|
679
|
+
fill: cssValue('--dt-node-fill'),
|
|
680
|
+
'stroke-dasharray': item.type === 'prerequisite' ? '8 7' : undefined,
|
|
681
|
+
}));
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
group.appendChild(node('text', {
|
|
685
|
+
x: centerX,
|
|
686
|
+
y: box.y + 22,
|
|
687
|
+
fill: color,
|
|
688
|
+
'font-size': 10,
|
|
689
|
+
'font-family': monoFont,
|
|
690
|
+
'font-weight': 700,
|
|
691
|
+
'letter-spacing': '0.08em',
|
|
692
|
+
'text-anchor': 'middle',
|
|
693
|
+
}, (item.type || 'manual').toUpperCase()));
|
|
694
|
+
renderTextLines(labelLines, group, centerX, centerY - (descLines.length ? 8 : 2), 16, cssValue('--dt-text-primary'), 700, 19);
|
|
695
|
+
if (descLines.length) {
|
|
696
|
+
renderTextLines(descLines, group, centerX, centerY + 18, 11.5, cssValue('--dt-text-secondary'), 500, 15);
|
|
697
|
+
}
|
|
698
|
+
nodeLayer.appendChild(group);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function render() {
|
|
702
|
+
const raw = parseData();
|
|
703
|
+
if (raw === null) {
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
const model = buildModel(raw);
|
|
707
|
+
if (!model.nodes.length) {
|
|
708
|
+
showEmpty('No data provided', 'Populate #diagram-data with process nodes and edges to render the flow.');
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
hideEmpty();
|
|
712
|
+
const { positions, width, height, order } = layout(model);
|
|
713
|
+
svg.innerHTML = '';
|
|
714
|
+
svg.setAttribute('viewBox', '0 0 ' + width + ' ' + height);
|
|
715
|
+
svg.setAttribute('aria-label', (raw.title || 'Process flow diagram') + ' with ' + model.nodes.length + ' steps');
|
|
716
|
+
svg.appendChild(renderDefs());
|
|
717
|
+
svg.appendChild(node('rect', { x: 0, y: 0, width, height, fill: 'url(#grid)' }));
|
|
718
|
+
svg.appendChild(node('title', { id: 'diagram-title' }, raw.title || 'Process flow diagram'));
|
|
719
|
+
svg.appendChild(node('desc', { id: 'diagram-description' }, raw.description || 'Static process flow diagram rendered with SVG'));
|
|
720
|
+
|
|
721
|
+
const edgeLayer = node('g', { 'aria-hidden': 'true' });
|
|
722
|
+
const nodeLayer = node('g');
|
|
723
|
+
|
|
724
|
+
model.edges.forEach((edge) => renderEdge(edge, positions, edgeLayer));
|
|
725
|
+
order.forEach((id) => renderNode(model.map.get(id), positions.get(id), nodeLayer));
|
|
726
|
+
|
|
727
|
+
svg.appendChild(edgeLayer);
|
|
728
|
+
svg.appendChild(nodeLayer);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function updateThemeButton() {
|
|
732
|
+
const value = root.getAttribute('data-theme') || 'auto';
|
|
733
|
+
themeButton.textContent = 'Theme: ' + value.charAt(0).toUpperCase() + value.slice(1);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function cycleTheme() {
|
|
737
|
+
const current = root.getAttribute('data-theme') || 'auto';
|
|
738
|
+
const next = themes[(themes.indexOf(current) + 1) % themes.length];
|
|
739
|
+
root.setAttribute('data-theme', next);
|
|
740
|
+
updateThemeButton();
|
|
741
|
+
render();
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function exportSvg() {
|
|
745
|
+
const clone = svg.cloneNode(true);
|
|
746
|
+
clone.setAttribute('xmlns', svgNs);
|
|
747
|
+
const payload = '<?xml version="1.0" encoding="UTF-8"?>\n' + new XMLSerializer().serializeToString(clone);
|
|
748
|
+
const blob = new Blob([payload], { type: 'image/svg+xml;charset=utf-8' });
|
|
749
|
+
const url = URL.createObjectURL(blob);
|
|
750
|
+
const anchorEl = document.createElement('a');
|
|
751
|
+
anchorEl.href = url;
|
|
752
|
+
anchorEl.download = 'process-flow-diagram.svg';
|
|
753
|
+
anchorEl.click();
|
|
754
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
themeButton.addEventListener('click', cycleTheme);
|
|
758
|
+
exportButton.addEventListener('click', exportSvg);
|
|
759
|
+
updateThemeButton();
|
|
760
|
+
render();
|
|
761
|
+
})();
|
|
762
|
+
</script>
|
|
763
|
+
</body>
|
|
764
|
+
</html>
|