@stacksjs/defaults 0.74.40 → 0.74.42
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/ai/skills/stacks-cms/SKILL.md +4 -4
- package/ai/skills/stacks-composables/SKILL.md +1 -1
- package/ai/skills/stacks-config/SKILL.md +1 -1
- package/ai/skills/stacks-dashboard/SKILL.md +1 -1
- package/ai/skills/stacks-technical-diagrams/LICENSE +1 -1
- package/ai/skills/stacks-technical-diagrams/SKILL.md +257 -229
- package/ai/skills/stacks-technical-diagrams/THIRD_PARTY_NOTICES.md +69 -0
- package/ai/skills/stacks-technical-diagrams/assets/JetBrainsMono-OFL.txt +93 -0
- package/ai/skills/stacks-technical-diagrams/assets/template.html +14588 -929
- package/ai/skills/stacks-technical-diagrams/bin/diagrams +10 -0
- package/ai/skills/stacks-technical-diagrams/bin/open-artifact.mjs +86 -0
- package/ai/skills/stacks-technical-diagrams/bin/preview.mjs +654 -0
- package/ai/skills/stacks-technical-diagrams/bin/technical-diagrams.mjs +1898 -89
- package/ai/skills/stacks-technical-diagrams/bin/visual-check.mjs +829 -0
- package/ai/skills/stacks-technical-diagrams/brand-marks/README.md +31 -0
- package/ai/skills/stacks-technical-diagrams/brand-marks/catalog.json +131 -0
- package/ai/skills/stacks-technical-diagrams/delta/architecture-delta.mjs +1221 -0
- package/ai/skills/stacks-technical-diagrams/examples/agent-run.lifecycle.json +18 -22
- package/ai/skills/stacks-technical-diagrams/examples/agent-tool-call.workflow.json +58 -52
- package/ai/skills/stacks-technical-diagrams/examples/async-job-roundtrip.sequence.json +61 -0
- package/ai/skills/stacks-technical-diagrams/examples/brand-aware-delivery.architecture.json +47 -0
- package/ai/skills/stacks-technical-diagrams/examples/cache-miss-request.sequence.json +30 -23
- package/ai/skills/stacks-technical-diagrams/examples/checkout-platform.base.architecture.json +31 -0
- package/ai/skills/stacks-technical-diagrams/examples/checkout-platform.head.architecture.json +31 -0
- package/ai/skills/stacks-technical-diagrams/examples/deployment-release.lifecycle.json +49 -0
- package/ai/skills/stacks-technical-diagrams/examples/event-stream.dataflow.json +57 -0
- package/ai/skills/stacks-technical-diagrams/examples/incident-response.workflow.json +64 -0
- package/ai/skills/stacks-technical-diagrams/examples/product-analytics.dataflow.json +22 -16
- package/ai/skills/stacks-technical-diagrams/examples/production-deployment.architecture.json +71 -0
- package/ai/skills/stacks-technical-diagrams/examples/release-delivery.workflow.json +62 -0
- package/ai/skills/stacks-technical-diagrams/examples/web-app.architecture.json +16 -11
- package/ai/skills/stacks-technical-diagrams/migrations/workflow-v2.mjs +279 -0
- package/ai/skills/stacks-technical-diagrams/recipes/scenarios.mjs +391 -0
- package/ai/skills/stacks-technical-diagrams/references/authoring-contract.md +243 -0
- package/ai/skills/stacks-technical-diagrams/references/brand-marks.md +65 -0
- package/ai/skills/stacks-technical-diagrams/references/delivery-contract.md +120 -0
- package/ai/skills/stacks-technical-diagrams/references/viewer-runtime.md +45 -0
- package/ai/skills/stacks-technical-diagrams/renderers/architecture/render-architecture.mjs +780 -73
- package/ai/skills/stacks-technical-diagrams/renderers/dataflow/README.md +25 -3
- package/ai/skills/stacks-technical-diagrams/renderers/dataflow/render-dataflow.mjs +240 -52
- package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/README.md +31 -7
- package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/render-lifecycle.mjs +227 -50
- package/ai/skills/stacks-technical-diagrams/renderers/sequence/README.md +36 -6
- package/ai/skills/stacks-technical-diagrams/renderers/sequence/render-sequence.mjs +270 -63
- package/ai/skills/stacks-technical-diagrams/renderers/shared/brand-marks.mjs +563 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/bun-runtime.mjs +20 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/cli.mjs +186 -10
- package/ai/skills/stacks-technical-diagrams/renderers/shared/desktop-readability.mjs +26 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/diagnostics.mjs +127 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/engineering-profiles.mjs +157 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/generated-brand-marks.mjs +2003 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/generated-validators.mjs +3 -3
- package/ai/skills/stacks-technical-diagrams/renderers/shared/geometry.mjs +1195 -2
- package/ai/skills/stacks-technical-diagrams/renderers/shared/i18n.mjs +595 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/legend.mjs +217 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/output-path.mjs +340 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/repository-evidence.mjs +238 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/repository-location.mjs +58 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/text-fit.mjs +49 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/utils.mjs +163 -19
- package/ai/skills/stacks-technical-diagrams/renderers/shared/validator.mjs +51 -5
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/README.md +137 -17
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/render-workflow.mjs +24 -470
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-compiler.mjs +4400 -0
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-migration-geometry.mjs +144 -0
- package/ai/skills/stacks-technical-diagrams/schemas/README.md +154 -11
- package/ai/skills/stacks-technical-diagrams/schemas/architecture.schema.json +61 -4
- package/ai/skills/stacks-technical-diagrams/schemas/common.schema.json +72 -0
- package/ai/skills/stacks-technical-diagrams/schemas/dataflow.schema.json +40 -18
- package/ai/skills/stacks-technical-diagrams/schemas/lifecycle.schema.json +43 -18
- package/ai/skills/stacks-technical-diagrams/schemas/sequence.schema.json +41 -4
- package/ai/skills/stacks-technical-diagrams/schemas/workflow.schema.json +97 -1
- package/ai/skills/stacks-technical-diagrams/scripts/check-render-output.mjs +551 -12
- package/ai/skills/stacks-technical-diagrams/scripts/render-examples.mjs +3 -4
- package/ide/vscode/package.json +1 -1
- package/package.json +2 -2
- package/resources/components/CookieConsent.stx +146 -0
- package/resources/components/CookieConsent.test.ts +79 -0
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
|
-
import { esc, renderDefinitions, textUnits } from '../shared/utils.mjs';
|
|
4
|
-
import { animateAttr,
|
|
5
|
-
import {
|
|
3
|
+
import { esc, renderDefinitions, renderSemanticSigil, textUnits } from '../shared/utils.mjs';
|
|
4
|
+
import { animateAttr, focusEdgeAttrs, focusNodeAttrs, focusNodeTitle, loadDiagramWithBrandMarks, writeDiagram, svgAccessibleText, svgRootAttrs } from '../shared/cli.mjs';
|
|
5
|
+
import { throwDiagnosticProblems } from '../shared/diagnostics.mjs';
|
|
6
|
+
import { resolveLegend, renderLegend as renderResolvedLegend } from '../shared/legend.mjs';
|
|
7
|
+
import { componentFill, arrowClassMap, rectsOverlap, cleanFlowProblems, cleanCrossingProblems, cleanAmbiguousCorridorProblems, cleanBorderRunProblems, cleanRouteRhythmProblems, cleanLabelRouteClearanceProblems, routePointsValue, asArray, isFinitePoint } from '../shared/geometry.mjs';
|
|
8
|
+
import { availableNodeTextWidth, fittedNodeFontSize, minimumNodeTextWidth } from '../shared/text-fit.mjs';
|
|
9
|
+
import { brandLabelFitWidth, brandMetadataFor, brandTopRailProblem, renderBrandMark } from '../shared/brand-marks.mjs';
|
|
10
|
+
import { translateMessage as i18nText } from '../shared/i18n.mjs';
|
|
11
|
+
|
|
12
|
+
const participantTextFit = {
|
|
13
|
+
sublabelPreferred: 7,
|
|
14
|
+
sublabelMinimum: 6,
|
|
15
|
+
};
|
|
6
16
|
|
|
7
17
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
const { diagram: sequence, template, outPath } =
|
|
18
|
+
const { diagram: sequence, template, outPath } = await loadDiagramWithBrandMarks({
|
|
9
19
|
rendererDir: __dirname,
|
|
10
20
|
diagramType: 'sequence',
|
|
11
21
|
defaultExample: 'cache-miss-request.sequence.json'
|
|
@@ -14,18 +24,36 @@ const { diagram: sequence, template, outPath } = loadDiagram({
|
|
|
14
24
|
const viewBox = sequence.meta?.viewBox || [920, 760];
|
|
15
25
|
// The timeline scales with viewBox height: a taller viewBox gains message room,
|
|
16
26
|
// a shorter one shrinks the readable band (validated below) instead of clipping.
|
|
27
|
+
// `column_fit: "spread"` widens the lanes with the viewBox instead of keeping
|
|
28
|
+
// the fixed 108px gap, so a wide canvas gains column distance and label room
|
|
29
|
+
// rather than dead space on the right. The default stays "fixed" so existing
|
|
30
|
+
// diagrams keep their coordinates.
|
|
31
|
+
const columnFit = sequence.meta?.column_fit === 'spread' ? 'spread' : 'fixed';
|
|
32
|
+
const participantCount = Math.max(1, asArray(sequence.participants).length);
|
|
33
|
+
const sideMargin = 62;
|
|
34
|
+
const participantW = columnFit === 'spread'
|
|
35
|
+
? Math.max(86, Math.min(190, Math.round((viewBox[0] - sideMargin * 2) / participantCount) - 24))
|
|
36
|
+
: 86;
|
|
37
|
+
const colGap = columnFit === 'spread' && participantCount > 1
|
|
38
|
+
? Math.max(108, (viewBox[0] - 40 - sideMargin - participantW) / (participantCount - 1))
|
|
39
|
+
: 108;
|
|
40
|
+
|
|
17
41
|
const layout = {
|
|
18
42
|
topY: 72,
|
|
19
|
-
participantW
|
|
43
|
+
participantW,
|
|
20
44
|
participantH: 54,
|
|
21
45
|
lifelineTop: 142,
|
|
22
46
|
lifelineBottom: viewBox[1] - 65,
|
|
23
47
|
legendY: viewBox[1] - 54,
|
|
24
|
-
leftX:
|
|
25
|
-
colGap
|
|
48
|
+
leftX: columnFit === 'spread' ? sideMargin + participantW / 2 : sideMargin,
|
|
49
|
+
colGap,
|
|
26
50
|
labelH: 16
|
|
27
51
|
};
|
|
28
52
|
|
|
53
|
+
const participantBoxWidthNote = columnFit === 'spread'
|
|
54
|
+
? `participant boxes are ${participantW}px for this viewBox width and ${participantCount} participants`
|
|
55
|
+
: `participant boxes are a fixed ${participantW}px unless meta.column_fit is "spread"`;
|
|
56
|
+
|
|
29
57
|
const arrowClass = {
|
|
30
58
|
...arrowClassMap,
|
|
31
59
|
return: ['a-default', 'arrowhead']
|
|
@@ -41,26 +69,85 @@ const participants = new Map(asArray(sequence.participants).map((participant, in
|
|
|
41
69
|
...participant,
|
|
42
70
|
index,
|
|
43
71
|
cx: participantX(index),
|
|
44
|
-
x: participantX(index) - layout.participantW / 2
|
|
72
|
+
x: participantX(index) - layout.participantW / 2,
|
|
73
|
+
y: layout.topY,
|
|
74
|
+
width: layout.participantW,
|
|
75
|
+
height: layout.participantH,
|
|
76
|
+
cy: layout.topY + layout.participantH / 2
|
|
45
77
|
}
|
|
46
78
|
]));
|
|
47
79
|
|
|
80
|
+
function messageGeometry(message) {
|
|
81
|
+
const from = participants.get(message.from);
|
|
82
|
+
const to = participants.get(message.to);
|
|
83
|
+
if (!from || !to || typeof message.y !== 'number') return null;
|
|
84
|
+
const direction = to.cx > from.cx ? 1 : -1;
|
|
85
|
+
const start = from.cx + direction * 7;
|
|
86
|
+
const end = to.cx - direction * 7;
|
|
87
|
+
return { start, end, center: (start + end) / 2 };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function messageLabelBox(message, relationIndex = null) {
|
|
91
|
+
const geometry = messageGeometry(message);
|
|
92
|
+
if (!geometry) return null;
|
|
93
|
+
const width = Math.max(34, textUnits(message.label) * 5.2 + 12);
|
|
94
|
+
return {
|
|
95
|
+
relation: message,
|
|
96
|
+
relationIndex,
|
|
97
|
+
label: message.label,
|
|
98
|
+
x: geometry.center - width / 2,
|
|
99
|
+
y: message.y - 20,
|
|
100
|
+
width,
|
|
101
|
+
height: layout.labelH,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function messageRouteBox(message) {
|
|
106
|
+
const geometry = messageGeometry(message);
|
|
107
|
+
if (!geometry) return null;
|
|
108
|
+
return {
|
|
109
|
+
x: Math.min(geometry.start, geometry.end),
|
|
110
|
+
y: message.y - 2,
|
|
111
|
+
width: Math.abs(geometry.end - geometry.start),
|
|
112
|
+
height: 4,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function segmentLabelBox(segment) {
|
|
117
|
+
const labelW = Math.max(42, textUnits(segment.label) * 5.2 + 14);
|
|
118
|
+
const occupied = asArray(sequence.messages)
|
|
119
|
+
.flatMap((message) => [messageLabelBox(message), messageRouteBox(message)])
|
|
120
|
+
.filter(Boolean);
|
|
121
|
+
const label = { x: 56, y: segment.from - 22, width: labelW, height: 18 };
|
|
122
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
123
|
+
if (!occupied.some((rect) => rectsOverlap(label, rect, 2))) break;
|
|
124
|
+
label.y -= 22;
|
|
125
|
+
}
|
|
126
|
+
return label;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const compositionFrames = asArray(sequence.segments).map((segment, index) => ({
|
|
130
|
+
id: index,
|
|
131
|
+
label: segment.label,
|
|
132
|
+
kind: 'segment',
|
|
133
|
+
x: 48,
|
|
134
|
+
y: segment.from,
|
|
135
|
+
width: viewBox[0] - 96,
|
|
136
|
+
height: segment.to - segment.from,
|
|
137
|
+
radius: 10,
|
|
138
|
+
}));
|
|
139
|
+
|
|
140
|
+
function messagePath(message) {
|
|
141
|
+
return {
|
|
142
|
+
points: participants.has(message.from) && participants.has(message.to)
|
|
143
|
+
? [[participants.get(message.from).cx, message.y], [participants.get(message.to).cx, message.y]]
|
|
144
|
+
: []
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
48
148
|
function validateSequence() {
|
|
49
149
|
const problems = [];
|
|
50
|
-
if (sequence.schema_version !== 1) problems.push('Sequence files must set "schema_version": 1.');
|
|
51
|
-
if (sequence.diagram_type !== 'sequence') problems.push('Sequence files must set "diagram_type": "sequence".');
|
|
52
|
-
if (!sequence.meta?.title) problems.push('Sequence files must include meta.title.');
|
|
53
|
-
if (!Array.isArray(sequence.participants) || sequence.participants.length < 2) {
|
|
54
|
-
problems.push('Sequence diagrams need at least two participants.');
|
|
55
|
-
}
|
|
56
150
|
if (participants.size !== asArray(sequence.participants).length) problems.push('Participant ids must be unique.');
|
|
57
|
-
if (!Array.isArray(sequence.messages) || sequence.messages.length < 1) {
|
|
58
|
-
problems.push('Sequence diagrams need at least one message.');
|
|
59
|
-
}
|
|
60
|
-
if (sequence.cards !== undefined && !Array.isArray(sequence.cards)) problems.push('Sequence "cards" must be an array.');
|
|
61
|
-
for (const arr of ['segments', 'activations']) {
|
|
62
|
-
if (sequence[arr] !== undefined && !Array.isArray(sequence[arr])) problems.push(`Sequence "${arr}" must be an array.`);
|
|
63
|
-
}
|
|
64
151
|
|
|
65
152
|
if (layout.lifelineBottom - layout.lifelineTop < 120) {
|
|
66
153
|
problems.push(`viewBox height ${viewBox[1]} leaves under 120px of timeline - set meta.viewBox[1] to at least ${layout.lifelineTop + 120 + 65}.`);
|
|
@@ -69,7 +156,18 @@ function validateSequence() {
|
|
|
69
156
|
for (const participant of participants.values()) {
|
|
70
157
|
const estLabelW = textUnits(participant.label) * 6.8;
|
|
71
158
|
if (estLabelW > layout.participantW + 6) {
|
|
72
|
-
problems.push(`Label "${participant.label}" (~${Math.round(estLabelW)}px) is wider than the ${layout.participantW}px participant box - shorten it
|
|
159
|
+
problems.push(`Label "${participant.label}" (~${Math.round(estLabelW)}px) is wider than the ${layout.participantW}px participant box - shorten it.`);
|
|
160
|
+
}
|
|
161
|
+
const brandRailProblem = brandTopRailProblem(participant, layout.participantW, 8, 'Participant');
|
|
162
|
+
if (brandRailProblem) problems.push(brandRailProblem);
|
|
163
|
+
// sublabel renders as a single unwrapped <text>; shrink-to-fit handles the
|
|
164
|
+
// ordinary case, this rejects what it cannot rescue.
|
|
165
|
+
if (participant.sublabel) {
|
|
166
|
+
const availableTextW = availableNodeTextWidth(layout.participantW);
|
|
167
|
+
const minimumW = minimumNodeTextWidth(participant.sublabel, participantTextFit.sublabelMinimum);
|
|
168
|
+
if (minimumW > availableTextW) {
|
|
169
|
+
problems.push(`Sublabel "${participant.sublabel}" needs ~${Math.ceil(minimumW)}px at the ${participantTextFit.sublabelMinimum}px legible minimum, but participant "${participant.id}" provides ${availableTextW}px - shorten the sublabel (${participantBoxWidthNote}).`);
|
|
170
|
+
}
|
|
73
171
|
}
|
|
74
172
|
}
|
|
75
173
|
|
|
@@ -86,6 +184,56 @@ function validateSequence() {
|
|
|
86
184
|
}
|
|
87
185
|
}
|
|
88
186
|
|
|
187
|
+
// Participant headers are opaque nodes. Lifelines, activation bars, and
|
|
188
|
+
// segment bands remain intentional pass-through geometry and are excluded.
|
|
189
|
+
problems.push(...cleanFlowProblems({
|
|
190
|
+
relations: sequence.messages,
|
|
191
|
+
obstacles: participants.values(),
|
|
192
|
+
pathFor: messagePath,
|
|
193
|
+
diagramType: 'sequence',
|
|
194
|
+
relationCollection: 'messages',
|
|
195
|
+
obstacleKind: 'participant header',
|
|
196
|
+
clearance: 0,
|
|
197
|
+
routeHint: 'move the message y below the participant headers or reorder participants'
|
|
198
|
+
}));
|
|
199
|
+
problems.push(...cleanCrossingProblems({
|
|
200
|
+
relations: sequence.messages,
|
|
201
|
+
endpointIds: new Set(participants.keys()),
|
|
202
|
+
pathFor: messagePath,
|
|
203
|
+
diagramType: 'sequence',
|
|
204
|
+
relationCollection: 'messages',
|
|
205
|
+
profile: sequence.meta?.quality_profile,
|
|
206
|
+
routeHint: 'separate the message y values; lifeline crossings remain allowed'
|
|
207
|
+
}));
|
|
208
|
+
problems.push(...cleanAmbiguousCorridorProblems({
|
|
209
|
+
relations: sequence.messages,
|
|
210
|
+
endpointIds: new Set(participants.keys()),
|
|
211
|
+
pathFor: messagePath,
|
|
212
|
+
diagramType: 'sequence',
|
|
213
|
+
relationCollection: 'messages',
|
|
214
|
+
profile: sequence.meta?.quality_profile,
|
|
215
|
+
routeHint: 'separate the message y values so unrelated messages do not visually merge'
|
|
216
|
+
}));
|
|
217
|
+
problems.push(...cleanBorderRunProblems({
|
|
218
|
+
relations: sequence.messages,
|
|
219
|
+
endpointIds: new Set(participants.keys()),
|
|
220
|
+
frames: compositionFrames,
|
|
221
|
+
pathFor: messagePath,
|
|
222
|
+
diagramType: 'sequence',
|
|
223
|
+
relationCollection: 'messages',
|
|
224
|
+
profile: sequence.meta?.quality_profile,
|
|
225
|
+
routeHint: 'move the message y so it crosses a segment boundary perpendicularly or stays clearly inside the segment'
|
|
226
|
+
}));
|
|
227
|
+
problems.push(...cleanRouteRhythmProblems({
|
|
228
|
+
relations: sequence.messages,
|
|
229
|
+
endpointIds: new Set(participants.keys()),
|
|
230
|
+
pathFor: messagePath,
|
|
231
|
+
diagramType: 'sequence',
|
|
232
|
+
relationCollection: 'messages',
|
|
233
|
+
profile: sequence.meta?.quality_profile,
|
|
234
|
+
routeHint: 'increase participant spacing or simplify message routing so every turn has room to read'
|
|
235
|
+
}));
|
|
236
|
+
|
|
89
237
|
// Vertical crowding only matters when the arrows share horizontal space;
|
|
90
238
|
// disjoint arrows may legitimately run in parallel rows.
|
|
91
239
|
const placed = asArray(sequence.messages)
|
|
@@ -108,13 +256,8 @@ function validateSequence() {
|
|
|
108
256
|
// Label masks can extend well past the arrow span, so check the actual
|
|
109
257
|
// label rectangles too - tangent arrows with long labels still collide.
|
|
110
258
|
const labelRects = asArray(sequence.messages)
|
|
111
|
-
.
|
|
112
|
-
.
|
|
113
|
-
const x1 = participants.get(m.from).cx;
|
|
114
|
-
const x2 = participants.get(m.to).cx;
|
|
115
|
-
const width = Math.max(34, textUnits(m.label) * 5.2 + 12);
|
|
116
|
-
return { label: m.label, x: (x1 + x2) / 2 - width / 2, y: m.y - 20, width, height: layout.labelH };
|
|
117
|
-
});
|
|
259
|
+
.map((m, messageIndex) => messageLabelBox(m, messageIndex))
|
|
260
|
+
.filter(Boolean);
|
|
118
261
|
for (let i = 0; i < labelRects.length; i += 1) {
|
|
119
262
|
for (let j = i + 1; j < labelRects.length; j += 1) {
|
|
120
263
|
if (rectsOverlap(labelRects[i], labelRects[j], -2)) {
|
|
@@ -122,6 +265,16 @@ function validateSequence() {
|
|
|
122
265
|
}
|
|
123
266
|
}
|
|
124
267
|
}
|
|
268
|
+
problems.push(...cleanLabelRouteClearanceProblems({
|
|
269
|
+
relations: sequence.messages,
|
|
270
|
+
labels: labelRects,
|
|
271
|
+
endpointIds: new Set(participants.keys()),
|
|
272
|
+
pathFor: messagePath,
|
|
273
|
+
diagramType: 'sequence',
|
|
274
|
+
relationCollection: 'messages',
|
|
275
|
+
profile: sequence.meta?.quality_profile,
|
|
276
|
+
routeHint: 'spread the message y values, shorten the label, or reorder participants so the adjacent route stays visible'
|
|
277
|
+
}));
|
|
125
278
|
|
|
126
279
|
for (const segment of asArray(sequence.segments)) {
|
|
127
280
|
if (segment.to <= segment.from) {
|
|
@@ -130,6 +283,12 @@ function validateSequence() {
|
|
|
130
283
|
if (segment.from < layout.topY || segment.to > layout.lifelineBottom + 20) {
|
|
131
284
|
problems.push(`Segment "${segment.label}" extends outside the canvas - keep its y range between ${layout.topY} and ${layout.lifelineBottom + 20}.`);
|
|
132
285
|
}
|
|
286
|
+
const labelBox = segmentLabelBox(segment);
|
|
287
|
+
const availableWidth = Math.max(0, viewBox[0] - 48 - labelBox.x);
|
|
288
|
+
if (labelBox.x + labelBox.width > viewBox[0] - 48) {
|
|
289
|
+
const requiredWidth = Math.ceil(labelBox.x + labelBox.width + 48);
|
|
290
|
+
problems.push(`Segment "${segment.label}" label (~${Math.round(labelBox.width)}px) exceeds the segment frame's available width (${availableWidth}px) - shorten the label or increase meta.viewBox[0] to at least ${requiredWidth}.`);
|
|
291
|
+
}
|
|
133
292
|
}
|
|
134
293
|
|
|
135
294
|
for (const activation of asArray(sequence.activations)) {
|
|
@@ -144,25 +303,49 @@ function validateSequence() {
|
|
|
144
303
|
}
|
|
145
304
|
|
|
146
305
|
if (problems.length) {
|
|
147
|
-
|
|
306
|
+
throwDiagnosticProblems('Sequence layout validation failed', problems, {
|
|
307
|
+
subject: { diagramType: 'sequence' },
|
|
308
|
+
});
|
|
148
309
|
}
|
|
149
310
|
}
|
|
150
311
|
|
|
151
312
|
function renderParticipant(participant) {
|
|
152
313
|
const fill = componentFill[participant.type] || 'c-external';
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
314
|
+
const hasSub = participant.sublabel != null && participant.sublabel !== '';
|
|
315
|
+
const sub = hasSub
|
|
316
|
+
? `\n <text data-detail="context" x="${participant.cx}" y="${layout.topY + 39}" class="t-muted" font-size="${fittedNodeFontSize(participant.sublabel, layout.participantW, participantTextFit.sublabelPreferred, participantTextFit.sublabelMinimum)}" text-anchor="middle">${esc(participant.sublabel)}</text>`
|
|
317
|
+
: '';
|
|
318
|
+
const brand = renderBrandMark(participant, { x: participant.x + layout.participantW - 22, y: layout.topY + 6 });
|
|
319
|
+
const labelFontSize = fittedNodeFontSize(participant.label, brandLabelFitWidth(participant, layout.participantW), 11, 8);
|
|
320
|
+
const passport = {
|
|
321
|
+
kind: participant.type,
|
|
322
|
+
sublabel: participant.sublabel,
|
|
323
|
+
context: i18nText(sequence.meta.locale, 'node.context.sequence'),
|
|
324
|
+
...brandMetadataFor(participant),
|
|
325
|
+
};
|
|
326
|
+
return ` <g ${focusNodeAttrs(participant.id, participant.label, passport, sequence.meta.locale)}>
|
|
327
|
+
${focusNodeTitle(participant.label, passport)}
|
|
328
|
+
<rect x="${participant.x}" y="${layout.topY}" width="${layout.participantW}" height="${layout.participantH}" rx="6" class="c-mask"/>
|
|
329
|
+
<rect x="${participant.x}" y="${layout.topY}" width="${layout.participantW}" height="${layout.participantH}" rx="6" class="${fill}"${animateAttr(sequence.meta, 'node', participant.index)} stroke-width="1.5"/>
|
|
330
|
+
${renderSemanticSigil(participant.type, { x: participant.x + 6, y: layout.topY + 6 })}${brand ? `\n ${brand}` : ''}
|
|
331
|
+
<text data-node-label=""${hasSub ? ' data-detail-anchor=""' : ''} x="${participant.cx}" y="${layout.topY + 22}" class="t-primary" font-size="${labelFontSize}" font-weight="600" text-anchor="middle">${esc(participant.label)}</text>${sub}
|
|
332
|
+
</g>`;
|
|
157
333
|
}
|
|
158
334
|
|
|
159
335
|
function renderLifeline(participant) {
|
|
160
336
|
return ` <path d="M ${participant.cx} ${layout.lifelineTop} L ${participant.cx} ${layout.lifelineBottom}" class="a-default" stroke-width="0.8" stroke-dasharray="3,7"/>`;
|
|
161
337
|
}
|
|
162
338
|
|
|
163
|
-
function renderSegment(segment) {
|
|
164
|
-
return ` <rect x="48" y="${segment.from}" width="${viewBox[0] - 96}" height="${segment.to - segment.from}" rx="10" class="c-lane" stroke-width="1"
|
|
165
|
-
|
|
339
|
+
function renderSegment(segment, index) {
|
|
340
|
+
return ` <rect data-graph-role="structural-frame" data-composition-frame-kind="segment" data-composition-frame-id="${index}" x="48" y="${segment.from}" width="${viewBox[0] - 96}" height="${segment.to - segment.from}" rx="10" class="c-lane" stroke-width="1"/>`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function renderSegmentLabel(segment, index) {
|
|
344
|
+
const label = segmentLabelBox(segment);
|
|
345
|
+
return ` <g data-graph-role="segment-label" data-segment-id="${index}">
|
|
346
|
+
<rect x="${label.x}" y="${label.y}" width="${label.width}" height="${label.height}" rx="3" class="c-mask"/>
|
|
347
|
+
<text x="${label.x + 6}" y="${label.y + 13}" class="t-dim" font-size="9" font-weight="600">${esc(segment.label)}</text>
|
|
348
|
+
</g>`;
|
|
166
349
|
}
|
|
167
350
|
|
|
168
351
|
function renderActivation(activation) {
|
|
@@ -175,9 +358,10 @@ function renderActivation(activation) {
|
|
|
175
358
|
}
|
|
176
359
|
|
|
177
360
|
function messageLabel(message, x1, x2) {
|
|
178
|
-
const
|
|
361
|
+
const box = messageLabelBox(message);
|
|
362
|
+
const center = box ? box.x + box.width / 2 : (x1 + x2) / 2;
|
|
179
363
|
const y = message.y - 10;
|
|
180
|
-
const labelW = Math.max(34, textUnits(message.label) * 5.2 + 12);
|
|
364
|
+
const labelW = box?.width || Math.max(34, textUnits(message.label) * 5.2 + 12);
|
|
181
365
|
const accent = message.variant === 'security'
|
|
182
366
|
? 't-security'
|
|
183
367
|
: message.variant === 'dashed'
|
|
@@ -185,42 +369,62 @@ function messageLabel(message, x1, x2) {
|
|
|
185
369
|
: message.variant === 'return'
|
|
186
370
|
? 't-muted'
|
|
187
371
|
: 't-backend';
|
|
188
|
-
return ` <
|
|
189
|
-
|
|
372
|
+
return ` <g data-detail="context">
|
|
373
|
+
<rect x="${center - labelW / 2}" y="${y - 10}" width="${labelW}" height="${layout.labelH}" rx="3" class="c-mask"/>
|
|
374
|
+
<text x="${center}" y="${y}" class="${accent}" font-size="9" text-anchor="middle">${esc(message.label)}</text>
|
|
375
|
+
</g>`;
|
|
190
376
|
}
|
|
191
377
|
|
|
192
378
|
function renderMessage(message, index) {
|
|
193
|
-
const
|
|
194
|
-
const to = participants.get(message.to);
|
|
195
|
-
const direction = to.cx > from.cx ? 1 : -1;
|
|
196
|
-
const start = from.cx + direction * 7;
|
|
197
|
-
const end = to.cx - direction * 7;
|
|
379
|
+
const { start, end } = messageGeometry(message);
|
|
198
380
|
const [cls, marker] = arrowClass[message.variant || 'default'] || arrowClass.default;
|
|
199
381
|
const strokeWidth = message.variant === 'emphasis' ? 1.8 : 1.4;
|
|
200
382
|
const dash = message.variant === 'return' ? ' stroke-dasharray="3,5"' : '';
|
|
201
383
|
const note = message.note
|
|
202
|
-
? `\n <text x="${Math.min(start, end) + 12}" y="${message.y + 18}" class="t-dim" font-size="7">${esc(message.note)}</text>`
|
|
384
|
+
? `\n <text data-detail="fine" x="${Math.min(start, end) + 12}" y="${message.y + 18}" class="t-dim" font-size="7">${esc(message.note)}</text>`
|
|
203
385
|
: '';
|
|
204
|
-
return ` <
|
|
205
|
-
${
|
|
386
|
+
return ` <g ${focusEdgeAttrs(message.from, message.to, message.label, index, message.id)}>
|
|
387
|
+
<path data-composition-edge-from="${esc(message.from)}" data-composition-edge-to="${esc(message.to)}"${message.id ? ` data-composition-edge-id="${esc(message.id)}"` : ''} data-composition-points="${routePointsValue([[start, message.y], [end, message.y]])}" d="M ${start} ${message.y} L ${end} ${message.y}" class="${cls}"${animateAttr(sequence.meta, 'edge', index)} stroke-width="${strokeWidth}"${dash} marker-end="url(#${marker})"/>
|
|
388
|
+
${messageLabel(message, start, end)}${note}
|
|
389
|
+
</g>`;
|
|
206
390
|
}
|
|
207
391
|
|
|
392
|
+
const LEGEND_CATALOG = [
|
|
393
|
+
{ kind: 'emphasis', className: 'a-emphasis', marker: 'arrowhead-emphasis', strokeWidth: 1.8 },
|
|
394
|
+
{ kind: 'return', className: 'a-default', marker: 'arrowhead', dash: '3,5' },
|
|
395
|
+
{ kind: 'security', className: 'a-security', marker: 'arrowhead-security' },
|
|
396
|
+
{ kind: 'dashed', className: 'a-dashed', marker: 'arrowhead-dashed' },
|
|
397
|
+
{ kind: 'default', className: 'a-default', marker: 'arrowhead' },
|
|
398
|
+
].map((entry) => ({
|
|
399
|
+
...entry,
|
|
400
|
+
interactive: false,
|
|
401
|
+
swatchWidth: 34,
|
|
402
|
+
swatchGap: 9,
|
|
403
|
+
label: i18nText(sequence.meta.locale, `legend.sequence.${entry.kind}`),
|
|
404
|
+
}));
|
|
405
|
+
|
|
208
406
|
function renderLegend() {
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
407
|
+
const presentKinds = new Set(asArray(sequence.messages).map((message) => message.variant || 'default'));
|
|
408
|
+
const entries = resolveLegend(sequence.meta?.legend, LEGEND_CATALOG, presentKinds);
|
|
409
|
+
return renderResolvedLegend({
|
|
410
|
+
entries,
|
|
411
|
+
locale: sequence.meta.locale,
|
|
412
|
+
layout: {
|
|
413
|
+
x: 40,
|
|
414
|
+
baselineY: layout.legendY,
|
|
415
|
+
width: viewBox[0] - 80,
|
|
416
|
+
minTitleY: layout.legendY - 30,
|
|
417
|
+
unfit: sequence.meta?.legend === undefined ? 'hide' : 'error',
|
|
418
|
+
diagramType: 'sequence',
|
|
419
|
+
},
|
|
420
|
+
renderSwatch: (entry) => `<path d="M ${entry.x} ${entry.baseline - 3} L ${entry.x + 34} ${entry.baseline - 3}" class="${entry.className}" stroke-width="${entry.strokeWidth || 1.4}"${entry.dash ? ` stroke-dasharray="${entry.dash}"` : ''} marker-end="url(#${entry.marker})"/>`,
|
|
421
|
+
});
|
|
219
422
|
}
|
|
220
423
|
|
|
221
424
|
function renderSvg() {
|
|
222
425
|
const participantList = [...participants.values()];
|
|
223
|
-
return ` <svg viewBox="0 0 ${viewBox[0]} ${viewBox[1]}" ${svgRootAttrs(sequence.meta
|
|
426
|
+
return ` <svg viewBox="0 0 ${viewBox[0]} ${viewBox[1]}" ${svgRootAttrs(sequence.meta)}>
|
|
427
|
+
${svgAccessibleText(sequence.meta, 'sequence')}
|
|
224
428
|
${renderDefinitions()}
|
|
225
429
|
|
|
226
430
|
<!-- Background Grid -->
|
|
@@ -232,11 +436,14 @@ ${asArray(sequence.segments).map(renderSegment).join('\n\n')}
|
|
|
232
436
|
<!-- Lifelines -->
|
|
233
437
|
${participantList.map(renderLifeline).join('\n')}
|
|
234
438
|
|
|
439
|
+
<!-- Activations -->
|
|
440
|
+
${asArray(sequence.activations).map(renderActivation).join('\n')}
|
|
441
|
+
|
|
235
442
|
<!-- Messages -->
|
|
236
443
|
${asArray(sequence.messages).map(renderMessage).join('\n\n')}
|
|
237
444
|
|
|
238
|
-
<!--
|
|
239
|
-
${asArray(sequence.
|
|
445
|
+
<!-- Segment Labels -->
|
|
446
|
+
${asArray(sequence.segments).map(renderSegmentLabel).join('\n')}
|
|
240
447
|
|
|
241
448
|
<!-- Participants -->
|
|
242
449
|
${participantList.map(renderParticipant).join('\n\n')}
|
|
@@ -250,8 +457,8 @@ validateSequence();
|
|
|
250
457
|
writeDiagram({
|
|
251
458
|
outPath,
|
|
252
459
|
template,
|
|
460
|
+
diagramType: 'sequence',
|
|
253
461
|
meta: sequence.meta,
|
|
254
|
-
footerLabel: 'Sequence diagram',
|
|
255
462
|
svg: renderSvg(),
|
|
256
463
|
cards: sequence.cards,
|
|
257
464
|
});
|