@zenuml/core 3.46.0 → 3.46.2
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/.claude/skills/dia-scoring/SKILL.md +139 -0
- package/.claude/skills/dia-scoring/agents/openai.yaml +7 -0
- package/.claude/skills/dia-scoring/references/selectors-and-keys.md +253 -0
- package/.claude/skills/land-pr/SKILL.md +98 -0
- package/.claude/skills/ship-branch/SKILL.md +81 -0
- package/.claude/skills/submit-branch/SKILL.md +76 -0
- package/.claude/skills/validate-branch/SKILL.md +54 -0
- package/CLAUDE.md +1 -1
- package/bun.lock +25 -11
- package/cy/canonical-history.html +908 -0
- package/cy/compare-case.html +357 -0
- package/cy/compare-cases.js +824 -0
- package/cy/compare.html +35 -0
- package/cy/diff-algorithm.js +199 -0
- package/cy/element-report.html +705 -0
- package/cy/icons-test.html +29 -0
- package/cy/legacy-vs-html.html +291 -0
- package/cy/native-diff-ext/background.js +60 -0
- package/cy/native-diff-ext/bridge.js +26 -0
- package/cy/native-diff-ext/content.js +194 -0
- package/cy/parity-test.html +122 -0
- package/cy/return-in-nested-if.html +29 -0
- package/cy/svg-preview.html +56 -0
- package/cy/svg-test.html +21 -0
- package/cy/theme-default-test.html +28 -0
- package/dist/stats.html +1 -1
- package/dist/zenuml.esm.mjs +16352 -15223
- package/dist/zenuml.js +701 -575
- package/docs/ship-branch-skill-plan.md +134 -0
- package/docs/superpowers/plans/2026-03-23-svg-parity-features.md +283 -0
- package/index.html +568 -73
- package/package.json +15 -4
- package/scripts/analyze-compare-case/collect-data.mjs +991 -0
- package/scripts/analyze-compare-case/config.mjs +102 -0
- package/scripts/analyze-compare-case/geometry.mjs +101 -0
- package/scripts/analyze-compare-case/native-diff.mjs +224 -0
- package/scripts/analyze-compare-case/output.mjs +74 -0
- package/scripts/analyze-compare-case/panel-diff.mjs +114 -0
- package/scripts/analyze-compare-case/report.mjs +157 -0
- package/scripts/analyze-compare-case/residual-scopes.mjs +325 -0
- package/scripts/analyze-compare-case/scoring.mjs +816 -0
- package/scripts/analyze-compare-case.mjs +149 -0
- package/scripts/snapshot-dual.js +34 -34
- package/skills/dia-scoring/SKILL.md +129 -0
- package/skills/dia-scoring/agents/openai.yaml +7 -0
- package/skills/dia-scoring/references/selectors-and-keys.md +253 -0
- package/test-setup.ts +8 -0
- package/types/index.d.ts +56 -0
- package/vite.config.ts +4 -0
- package/dist/10029-icon-service-Function-Apps-ObflOLuF.js +0 -5
- package/dist/Res_AWS-Identity-Access-Management_IAM-Access-Analyzer_48-BPq60XMY.js +0 -11
- package/dist/Res_AWS-Lambda_Lambda-Function_48-Co38UB_2.js +0 -12
- package/dist/Res_Amazon-EC2_Instance_48-CRaqbNUl.js +0 -12
- package/dist/Res_Amazon-Simple-Notification-Service_Topic_48-q13mxUeM.js +0 -11
- package/dist/Res_Amazon-Simple-Queue-Service_Queue_48-D2-8gbFw.js +0 -11
- package/dist/Robustness_Diagram_Boundary-nYnmTPs8.js +0 -10
- package/dist/Robustness_Diagram_Control-DLNLoMxd.js +0 -11
- package/dist/Robustness_Diagram_Entity-Be3kcbIE.js +0 -11
- package/dist/actor-BMj_HFpo.js +0 -11
- package/dist/database-BKHQQWQK.js +0 -8
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<title>Compare</title>
|
|
6
|
+
<style>
|
|
7
|
+
body { margin: 0; font-family: sans-serif; }
|
|
8
|
+
.compare-bar {
|
|
9
|
+
display: flex; align-items: center; gap: 12px;
|
|
10
|
+
padding: 8px 16px; background: #1e293b; color: white;
|
|
11
|
+
}
|
|
12
|
+
.compare-bar a { color: #93c5fd; text-decoration: none; font-size: 13px; }
|
|
13
|
+
.compare-bar a:hover { text-decoration: underline; }
|
|
14
|
+
.compare-bar h2 { margin: 0; font-size: 15px; font-weight: 500; }
|
|
15
|
+
.nav { display: flex; gap: 8px; margin-left: auto; align-items: center; }
|
|
16
|
+
.nav a { padding: 4px 10px; background: #334155; border-radius: 4px; font-size: 12px; }
|
|
17
|
+
.nav button { padding: 4px 12px; background: #7c3aed; border: none; border-radius: 4px; color: white; font-size: 12px; cursor: pointer; }
|
|
18
|
+
.nav button:hover { background: #6d28d9; }
|
|
19
|
+
.nav button:disabled { background: #64748b; cursor: wait; }
|
|
20
|
+
.nav .match-badge { font-size: 11px; color: #94a3b8; }
|
|
21
|
+
.container { display: flex; gap: 0; }
|
|
22
|
+
.container.stacked { flex-direction: column; }
|
|
23
|
+
.panel { flex: 1; overflow: auto; }
|
|
24
|
+
.panel > h3 { margin: 0; padding: 8px 16px; background: #334155; color: white; font-size: 13px; }
|
|
25
|
+
.panel-content { padding: 10px; background: white; }
|
|
26
|
+
.diff-panel { display: none; }
|
|
27
|
+
.diff-panel.visible { display: block; }
|
|
28
|
+
.diff-panel .panel-content canvas { max-width: 100%; }
|
|
29
|
+
#html-output .footer { display: none !important; }
|
|
30
|
+
#html-output .privacy { visibility: hidden !important; }
|
|
31
|
+
</style>
|
|
32
|
+
</head>
|
|
33
|
+
<body>
|
|
34
|
+
<div class="compare-bar">
|
|
35
|
+
<a href="/cy/compare.html">← All cases</a>
|
|
36
|
+
<h2 id="case-name"></h2>
|
|
37
|
+
<div class="nav">
|
|
38
|
+
<a id="prev-link" href="#">← Prev</a>
|
|
39
|
+
<a id="next-link" href="#">Next →</a>
|
|
40
|
+
<span class="match-badge" id="match-badge"></span>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="container">
|
|
44
|
+
<div class="panel">
|
|
45
|
+
<h3>HTML/React Renderer (theme-default)</h3>
|
|
46
|
+
<div class="panel-content" id="html-output">
|
|
47
|
+
<div id="diagram"><pre class="zenuml" style="margin: 0"></pre></div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
<div class="panel">
|
|
51
|
+
<h3>Native SVG Renderer</h3>
|
|
52
|
+
<div class="panel-content" id="svg-output"></div>
|
|
53
|
+
</div>
|
|
54
|
+
<div class="panel diff-panel" id="diff-panel">
|
|
55
|
+
<h3>Pixel Diff</h3>
|
|
56
|
+
<div class="panel-content" id="diff-output"></div>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
<script type="module">
|
|
60
|
+
import ZenUml from "/src/core.tsx";
|
|
61
|
+
import { renderToSvg } from "/src/svg/renderToSvg.ts";
|
|
62
|
+
|
|
63
|
+
import { CASES } from "./compare-cases.js";
|
|
64
|
+
import { diffImages, DEFAULTS } from "./diff-algorithm.js";
|
|
65
|
+
|
|
66
|
+
const caseNames = Object.keys(CASES);
|
|
67
|
+
const params = new URLSearchParams(window.location.search);
|
|
68
|
+
const caseName = params.get("case") || caseNames[0];
|
|
69
|
+
const code = CASES[caseName];
|
|
70
|
+
|
|
71
|
+
// Update title and header
|
|
72
|
+
document.title = `Compare: ${caseName}`;
|
|
73
|
+
document.getElementById("case-name").textContent = caseName;
|
|
74
|
+
|
|
75
|
+
// Prev/Next navigation
|
|
76
|
+
const idx = caseNames.indexOf(caseName);
|
|
77
|
+
const prevLink = document.getElementById("prev-link");
|
|
78
|
+
const nextLink = document.getElementById("next-link");
|
|
79
|
+
if (idx > 0) {
|
|
80
|
+
prevLink.href = `?case=${encodeURIComponent(caseNames[idx - 1])}`;
|
|
81
|
+
} else {
|
|
82
|
+
prevLink.style.visibility = "hidden";
|
|
83
|
+
}
|
|
84
|
+
if (idx < caseNames.length - 1) {
|
|
85
|
+
nextLink.href = `?case=${encodeURIComponent(caseNames[idx + 1])}`;
|
|
86
|
+
} else {
|
|
87
|
+
nextLink.style.visibility = "hidden";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Expose DSL for the extension's batch mode to read
|
|
91
|
+
window.__currentDSL = code || "";
|
|
92
|
+
|
|
93
|
+
if (code === undefined) {
|
|
94
|
+
const errP = document.createElement("p");
|
|
95
|
+
errP.style.color = "red";
|
|
96
|
+
errP.textContent = "Unknown case: " + caseName;
|
|
97
|
+
document.getElementById("svg-output").appendChild(errP);
|
|
98
|
+
} else {
|
|
99
|
+
// SVG side
|
|
100
|
+
const result = renderToSvg(code);
|
|
101
|
+
document.getElementById("svg-output").innerHTML = result.svg;
|
|
102
|
+
|
|
103
|
+
// HTML side — render with theme-default
|
|
104
|
+
const diagram = document.getElementById("diagram");
|
|
105
|
+
const pre = diagram.querySelector("pre.zenuml");
|
|
106
|
+
pre.textContent = code;
|
|
107
|
+
const zenUml = new ZenUml(pre);
|
|
108
|
+
await zenUml.render(code, { theme: "theme-default" });
|
|
109
|
+
|
|
110
|
+
// Hide footer in HTML output (SVG has no footer equivalent)
|
|
111
|
+
requestAnimationFrame(() => {
|
|
112
|
+
const footer = document.querySelector("#html-output .footer");
|
|
113
|
+
if (footer) footer.style.display = "none";
|
|
114
|
+
|
|
115
|
+
// Stack panels vertically when diagram is wider than half the viewport.
|
|
116
|
+
// This prevents overflow clipping in CDP screenshots for wide diagrams.
|
|
117
|
+
const frame = document.querySelector("#html-output .frame, #html-output .sequence-diagram");
|
|
118
|
+
const svgEl = document.querySelector("#svg-output > svg");
|
|
119
|
+
const maxW = Math.max(frame?.offsetWidth || 0, svgEl?.offsetWidth || 0);
|
|
120
|
+
if (maxW > window.innerWidth / 2 - 40) {
|
|
121
|
+
document.querySelector(".container").classList.add("stacked");
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ——— Element extraction for structural comparison ———
|
|
127
|
+
|
|
128
|
+
function extractHtmlElements(container) {
|
|
129
|
+
const elements = { participants: [], messages: [], selfCalls: [], creations: [], returns: [], fragments: [] };
|
|
130
|
+
|
|
131
|
+
// Participants
|
|
132
|
+
container.querySelectorAll("[data-participant-id]").forEach(el => {
|
|
133
|
+
const name = el.getAttribute("data-participant-id");
|
|
134
|
+
if (name) elements.participants.push(name);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Messages (non-self, non-return, non-creation interactions)
|
|
138
|
+
container.querySelectorAll('.interaction:not(.return):not(.creation)').forEach(el => {
|
|
139
|
+
const isSelf = el.classList.contains("self-invocation") ||
|
|
140
|
+
el.querySelector(".self-invocation") !== null ||
|
|
141
|
+
el.classList.contains("self");
|
|
142
|
+
if (isSelf) return;
|
|
143
|
+
const sig = el.getAttribute("data-signature") || el.getAttribute("data-to") || "";
|
|
144
|
+
if (sig) elements.messages.push(sig);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Self-calls
|
|
148
|
+
container.querySelectorAll('.self-invocation').forEach(el => {
|
|
149
|
+
const interaction = el.closest(".interaction");
|
|
150
|
+
const sig = interaction?.getAttribute("data-signature") || "";
|
|
151
|
+
if (sig) elements.selfCalls.push(sig);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Creations
|
|
155
|
+
container.querySelectorAll('.interaction.creation').forEach(el => {
|
|
156
|
+
const sig = el.getAttribute("data-signature") || "";
|
|
157
|
+
if (sig) elements.creations.push(sig);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Returns — match .interaction.return (some have data-type="return", some don't)
|
|
161
|
+
container.querySelectorAll('.interaction.return').forEach(el => {
|
|
162
|
+
const sig = el.getAttribute("data-signature") || "";
|
|
163
|
+
const msgDiv = el.querySelector(".message");
|
|
164
|
+
const label = sig || (msgDiv ? msgDiv.textContent.trim().replace(/[\d.]+$/, "").trim() : "");
|
|
165
|
+
if (label) elements.returns.push(label);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Fragments
|
|
169
|
+
container.querySelectorAll(".fragment").forEach(el => {
|
|
170
|
+
const kind = [...el.classList].find(c => c.startsWith("fragment-"))?.replace("fragment-", "") ||
|
|
171
|
+
[...el.classList].find(c => ["alt", "loop", "opt", "par", "tcf", "critical", "section", "ref"].includes(c)) || "unknown";
|
|
172
|
+
elements.fragments.push(kind);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
return elements;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function extractSvgElements(svgElement) {
|
|
179
|
+
const elements = { participants: [], messages: [], selfCalls: [], creations: [], returns: [], fragments: [] };
|
|
180
|
+
if (!svgElement) return elements;
|
|
181
|
+
|
|
182
|
+
// Participants
|
|
183
|
+
svgElement.querySelectorAll("g.participant, g.participant-starter").forEach(g => {
|
|
184
|
+
const name = g.getAttribute("data-participant");
|
|
185
|
+
if (name) elements.participants.push(name);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Messages (non-self)
|
|
189
|
+
svgElement.querySelectorAll("g.message").forEach(g => {
|
|
190
|
+
if (g.classList.contains("self-call")) return;
|
|
191
|
+
const text = g.querySelector("text.message-label");
|
|
192
|
+
elements.messages.push(text ? text.textContent.trim() : "");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Self-calls
|
|
196
|
+
svgElement.querySelectorAll("g.message.self-call").forEach(g => {
|
|
197
|
+
const text = g.querySelector("text.message-label");
|
|
198
|
+
elements.selfCalls.push(text ? text.textContent.trim() : "");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Creations
|
|
202
|
+
svgElement.querySelectorAll("g.creation").forEach(g => {
|
|
203
|
+
const text = g.querySelector("text.message-label");
|
|
204
|
+
elements.creations.push(text ? text.textContent.trim() : "");
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Returns
|
|
208
|
+
svgElement.querySelectorAll("g.return").forEach(g => {
|
|
209
|
+
const text = g.querySelector("text.return-label");
|
|
210
|
+
elements.returns.push(text ? text.textContent.trim() : "");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// Fragments
|
|
214
|
+
svgElement.querySelectorAll("g.fragment, g[class*='fragment-']").forEach(g => {
|
|
215
|
+
const kind = ([...g.classList].find(c => c.startsWith("fragment-")) || "").replace("fragment-", "") || "unknown";
|
|
216
|
+
elements.fragments.push(kind);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
return elements;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function computeStructuralMatch(htmlElements, svgElements) {
|
|
223
|
+
const types = ["participants", "messages", "selfCalls", "creations", "returns", "fragments"];
|
|
224
|
+
let totalScore = 0;
|
|
225
|
+
let scoredTypes = 0;
|
|
226
|
+
let totalElements = 0;
|
|
227
|
+
let matchedElements = 0;
|
|
228
|
+
const details = {};
|
|
229
|
+
|
|
230
|
+
for (const type of types) {
|
|
231
|
+
const htmlList = htmlElements[type] || [];
|
|
232
|
+
const svgList = svgElements[type] || [];
|
|
233
|
+
const maxLen = Math.max(htmlList.length, svgList.length);
|
|
234
|
+
|
|
235
|
+
if (maxLen === 0) continue; // skip types with no elements in either
|
|
236
|
+
|
|
237
|
+
// Count matches by order
|
|
238
|
+
let matched = 0;
|
|
239
|
+
const minLen = Math.min(htmlList.length, svgList.length);
|
|
240
|
+
for (let i = 0; i < minLen; i++) {
|
|
241
|
+
matched++; // count as matched if present in both at same index
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const score = matched / maxLen;
|
|
245
|
+
totalScore += score;
|
|
246
|
+
scoredTypes++;
|
|
247
|
+
totalElements += maxLen;
|
|
248
|
+
matchedElements += matched;
|
|
249
|
+
|
|
250
|
+
details[type] = { html: htmlList.length, svg: svgList.length, matched, score };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
score: scoredTypes > 0 ? totalScore / scoredTypes : 1,
|
|
255
|
+
totalElements,
|
|
256
|
+
matchedElements,
|
|
257
|
+
details,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// --- Reusable capture/diff API (also exposed on window for external tools) ---
|
|
262
|
+
|
|
263
|
+
// State for hide/restore cycle
|
|
264
|
+
let _hiddenEls = [];
|
|
265
|
+
let _savedStyles = [];
|
|
266
|
+
|
|
267
|
+
function _hideEl(el) {
|
|
268
|
+
if (el) { _hiddenEls.push({ el, prev: el.style.display }); el.style.display = "none"; }
|
|
269
|
+
}
|
|
270
|
+
function _stripMargin(el) {
|
|
271
|
+
if (el) { _savedStyles.push({ el, prev: el.style.margin }); el.style.margin = "0"; }
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Prepare HTML panel for screenshot: hide chrome, numbering, icons
|
|
275
|
+
window.prepareHtmlForCapture = function() {
|
|
276
|
+
_hiddenEls = [];
|
|
277
|
+
_savedStyles = [];
|
|
278
|
+
|
|
279
|
+
const htmlEl = document.getElementById("html-output");
|
|
280
|
+
const htmlFrame = htmlEl.querySelector(".frame");
|
|
281
|
+
const seqDiagram = htmlEl.querySelector(".sequence-diagram");
|
|
282
|
+
const svgElement = document.getElementById("svg-output").querySelector("svg");
|
|
283
|
+
|
|
284
|
+
_hideEl(htmlEl.querySelector(".footer"));
|
|
285
|
+
_stripMargin(htmlFrame);
|
|
286
|
+
|
|
287
|
+
// Hide bottom participant labels (SVG no longer renders them)
|
|
288
|
+
const captureEl = htmlFrame || seqDiagram || htmlEl;
|
|
289
|
+
const captureRect = captureEl.getBoundingClientRect();
|
|
290
|
+
const captureMidY = captureRect.top + captureRect.height * 0.7;
|
|
291
|
+
captureEl.querySelectorAll(".participant").forEach(el => {
|
|
292
|
+
const r = el.getBoundingClientRect();
|
|
293
|
+
if (r.top > captureMidY) _hideEl(el);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
if (htmlFrame) {
|
|
297
|
+
const frameRect2 = htmlFrame.getBoundingClientRect();
|
|
298
|
+
// Hide SVG icons in the header area (top 35px) — checkbox, etc.
|
|
299
|
+
htmlFrame.querySelectorAll("svg").forEach(svg => {
|
|
300
|
+
const r = svg.getBoundingClientRect();
|
|
301
|
+
if (r.width > 0 && (r.top - frameRect2.top) < 35) _hideEl(svg);
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return { htmlSelector: "#html-output .frame, #html-output .sequence-diagram", svgSelector: "#svg-output svg" };
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
// Restore HTML panel after screenshot
|
|
309
|
+
window.restoreHtmlAfterCapture = function() {
|
|
310
|
+
for (const { el, prev } of _hiddenEls) el.style.display = prev;
|
|
311
|
+
for (const { el, prev } of _savedStyles) el.style.margin = prev;
|
|
312
|
+
_hiddenEls = [];
|
|
313
|
+
_savedStyles = [];
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
// Run pixel diff from two PNG data URLs and display results on the page.
|
|
317
|
+
// Returns { pixelPct, posPct, structPct, matched, total, htmlOnly, svgOnly, colorDiff }
|
|
318
|
+
window.diffFromImages = async function(htmlDataUrl, svgDataUrl) {
|
|
319
|
+
const htmlEl = document.getElementById("html-output");
|
|
320
|
+
const svgEl = document.getElementById("svg-output");
|
|
321
|
+
const seqDiagram = htmlEl.querySelector(".sequence-diagram");
|
|
322
|
+
const svgElement = svgEl.querySelector("svg");
|
|
323
|
+
|
|
324
|
+
// Structural comparison
|
|
325
|
+
const htmlElements = extractHtmlElements(seqDiagram || htmlEl);
|
|
326
|
+
const svgElements = extractSvgElements(svgElement);
|
|
327
|
+
const structural = computeStructuralMatch(htmlElements, svgElements);
|
|
328
|
+
|
|
329
|
+
// Allow URL param overrides for debugging
|
|
330
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
331
|
+
const opts = {
|
|
332
|
+
LUMA_THRESHOLD: parseInt(urlParams.get('luma') || DEFAULTS.LUMA_THRESHOLD, 10),
|
|
333
|
+
CHANNEL_TOLERANCE: parseInt(urlParams.get('ctol') || DEFAULTS.CHANNEL_TOLERANCE, 10),
|
|
334
|
+
POSITION_TOLERANCE: parseInt(urlParams.get('ptol') || DEFAULTS.POSITION_TOLERANCE, 10),
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const { canvas: diffCanvas, stats, badgeHtml } = await diffImages(htmlDataUrl, svgDataUrl, opts);
|
|
338
|
+
|
|
339
|
+
const diffOutput = document.getElementById("diff-output");
|
|
340
|
+
diffOutput.innerHTML = "";
|
|
341
|
+
diffOutput.appendChild(diffCanvas);
|
|
342
|
+
|
|
343
|
+
const panel = document.getElementById("diff-panel");
|
|
344
|
+
panel.classList.add("visible");
|
|
345
|
+
|
|
346
|
+
const structPct = (structural.score * 100).toFixed(0);
|
|
347
|
+
document.getElementById("match-badge").innerHTML =
|
|
348
|
+
`${structPct}% structural · ${badgeHtml}`;
|
|
349
|
+
|
|
350
|
+
return { pixelPct: stats.pixelPct, posPct: stats.posPct, structPct: parseFloat(structPct), ...stats };
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
</script>
|
|
356
|
+
</body>
|
|
357
|
+
</html>
|