@vizejs/vite-plugin-musea 0.0.1-alpha.11 → 0.0.1-alpha.112

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.
Files changed (127) hide show
  1. package/dist/a11y-Bvx5TJb8.d.ts +61 -0
  2. package/dist/a11y-Bvx5TJb8.d.ts.map +1 -0
  3. package/dist/a11y-C6xqILwZ.js +305 -0
  4. package/dist/a11y-C6xqILwZ.js.map +1 -0
  5. package/dist/a11y.d.ts +3 -0
  6. package/dist/a11y.js +3 -0
  7. package/dist/autogen-D3Zjc3zI.d.ts +64 -0
  8. package/dist/autogen-D3Zjc3zI.d.ts.map +1 -0
  9. package/dist/autogen-ymQnARZK.js +193 -0
  10. package/dist/autogen-ymQnARZK.js.map +1 -0
  11. package/dist/autogen.d.ts +2 -0
  12. package/dist/autogen.js +3 -0
  13. package/dist/cli.js +207 -30
  14. package/dist/cli.js.map +1 -1
  15. package/dist/gallery/assets/abap-Cry0R76c.js +1 -0
  16. package/dist/gallery/assets/apex-GS4zZi0I.js +1 -0
  17. package/dist/gallery/assets/azcli-DMImymmY.js +1 -0
  18. package/dist/gallery/assets/bat-D6epFECU.js +1 -0
  19. package/dist/gallery/assets/bicep-7klDZ283.js +2 -0
  20. package/dist/gallery/assets/cameligo-PvLD8t4t.js +1 -0
  21. package/dist/gallery/assets/clojure-BTbSGpb3.js +1 -0
  22. package/dist/gallery/assets/codicon-DCmgc-ay.ttf +0 -0
  23. package/dist/gallery/assets/coffee-Bhl_9YuJ.js +1 -0
  24. package/dist/gallery/assets/cpp-CM5j04eT.js +1 -0
  25. package/dist/gallery/assets/csharp-Dh0Ee7SY.js +1 -0
  26. package/dist/gallery/assets/csp-CLRC61y6.js +1 -0
  27. package/dist/gallery/assets/css-B0t_muXd.js +3 -0
  28. package/dist/gallery/assets/css.worker-Cbw1kvi8.js +88 -0
  29. package/dist/gallery/assets/cssMode-DBTfE7c_.js +4 -0
  30. package/dist/gallery/assets/cypher-C5e5inIh.js +1 -0
  31. package/dist/gallery/assets/dart-DIK3l8YT.js +1 -0
  32. package/dist/gallery/assets/dockerfile-D7OAO0hl.js +1 -0
  33. package/dist/gallery/assets/ecl-CP7nM2KN.js +1 -0
  34. package/dist/gallery/assets/editor-B55U_qvj.css +1 -0
  35. package/dist/gallery/assets/editor-F8AxQWwE.css +1 -0
  36. package/dist/gallery/assets/editor.api-DOELtZec.js +644 -0
  37. package/dist/gallery/assets/editor.main-BWk0dmtj.js +63 -0
  38. package/dist/gallery/assets/editor.worker-Cs7HTPcl.js +12 -0
  39. package/dist/gallery/assets/elixir-DNRIIj6-.js +1 -0
  40. package/dist/gallery/assets/flow9-BC5Cr9X0.js +1 -0
  41. package/dist/gallery/assets/freemarker2-DaSd0g6o.js +3 -0
  42. package/dist/gallery/assets/fsharp-52P4yqMh.js +1 -0
  43. package/dist/gallery/assets/go-yKE3zUfB.js +1 -0
  44. package/dist/gallery/assets/graphql-D3sNVCLc.js +1 -0
  45. package/dist/gallery/assets/handlebars-B-cbMVnP.js +1 -0
  46. package/dist/gallery/assets/hcl-BB7aW7AX.js +1 -0
  47. package/dist/gallery/assets/html-BbDsaPQg.js +1 -0
  48. package/dist/gallery/assets/html.worker-CYmk49z4.js +495 -0
  49. package/dist/gallery/assets/htmlMode-C2OjYqY0.js +4 -0
  50. package/dist/gallery/assets/index-BIf-7q-k.css +1 -0
  51. package/dist/gallery/assets/index-DotH6Szk.js +63 -0
  52. package/dist/gallery/assets/ini-BdRufzJj.js +1 -0
  53. package/dist/gallery/assets/java-CeUu-z7Y.js +1 -0
  54. package/dist/gallery/assets/javascript-CogJNBNZ.js +1 -0
  55. package/dist/gallery/assets/json.worker-CWR6J9Qf.js +51 -0
  56. package/dist/gallery/assets/jsonMode-ssar7rkB.js +10 -0
  57. package/dist/gallery/assets/julia-CXu-Fn93.js +1 -0
  58. package/dist/gallery/assets/kotlin-TwsjxLJ3.js +1 -0
  59. package/dist/gallery/assets/less-CviwWNG4.js +2 -0
  60. package/dist/gallery/assets/lexon-BTOivnjP.js +1 -0
  61. package/dist/gallery/assets/liquid-D9uJH3tm.js +1 -0
  62. package/dist/gallery/assets/lua-6W3WJOvj.js +1 -0
  63. package/dist/gallery/assets/m3-tlthQ8Fo.js +1 -0
  64. package/dist/gallery/assets/markdown-CPR4Kr9O.js +1 -0
  65. package/dist/gallery/assets/mdx-D1ruboYf.js +1 -0
  66. package/dist/gallery/assets/mips-BfxZbsD8.js +1 -0
  67. package/dist/gallery/assets/monaco.contribution-w8cYIjzF.js +2 -0
  68. package/dist/gallery/assets/msdax-eKsr2VtO.js +1 -0
  69. package/dist/gallery/assets/mysql-D6-LO0bt.js +1 -0
  70. package/dist/gallery/assets/objective-c-DYtfYpNc.js +1 -0
  71. package/dist/gallery/assets/pascal-CPGyHbal.js +1 -0
  72. package/dist/gallery/assets/pascaligo-Dsp_VKxo.js +1 -0
  73. package/dist/gallery/assets/perl-CUVa2_Cu.js +1 -0
  74. package/dist/gallery/assets/pgsql-C2nbbU56.js +1 -0
  75. package/dist/gallery/assets/php-DxX2tlkL.js +1 -0
  76. package/dist/gallery/assets/pla-D55LHImG.js +1 -0
  77. package/dist/gallery/assets/postiats-Dw_nWtoT.js +1 -0
  78. package/dist/gallery/assets/powerquery-BldVOeNZ.js +1 -0
  79. package/dist/gallery/assets/powershell-fdqyoMut.js +1 -0
  80. package/dist/gallery/assets/protobuf-C-2cnAYL.js +2 -0
  81. package/dist/gallery/assets/pug-bDrVOc6m.js +1 -0
  82. package/dist/gallery/assets/python-Bk5JPOE3.js +1 -0
  83. package/dist/gallery/assets/qsharp-B83Ol6AR.js +1 -0
  84. package/dist/gallery/assets/r-DAxg6zn-.js +1 -0
  85. package/dist/gallery/assets/razor-BbS20zwD.js +1 -0
  86. package/dist/gallery/assets/redis-BSRYxJDu.js +1 -0
  87. package/dist/gallery/assets/redshift-BrtVU4Ki.js +1 -0
  88. package/dist/gallery/assets/restructuredtext-DdU6AjLQ.js +1 -0
  89. package/dist/gallery/assets/ruby-C-s7ovR-.js +1 -0
  90. package/dist/gallery/assets/rust-CmSb_pkG.js +1 -0
  91. package/dist/gallery/assets/sb-Bfo5Ukmr.js +1 -0
  92. package/dist/gallery/assets/scala-Cx2bkddK.js +1 -0
  93. package/dist/gallery/assets/scheme-DkT6GPaV.js +1 -0
  94. package/dist/gallery/assets/scss-DOPngiM2.js +3 -0
  95. package/dist/gallery/assets/shell-NYt6Xulf.js +1 -0
  96. package/dist/gallery/assets/solidity-CUiq_T3F.js +1 -0
  97. package/dist/gallery/assets/sophia-B4sI8Ij-.js +1 -0
  98. package/dist/gallery/assets/sparql-BNfhekQe.js +1 -0
  99. package/dist/gallery/assets/sql-Bzn3OZV3.js +1 -0
  100. package/dist/gallery/assets/st-CV0zI_0P.js +1 -0
  101. package/dist/gallery/assets/swift-CR3-zK7D.js +1 -0
  102. package/dist/gallery/assets/systemverilog-BxgPwTIi.js +1 -0
  103. package/dist/gallery/assets/tcl-BSnnsp36.js +1 -0
  104. package/dist/gallery/assets/ts.worker-B_5n269U.js +51339 -0
  105. package/dist/gallery/assets/tsMode-DWuHbc8i.js +11 -0
  106. package/dist/gallery/assets/twig-Bx06vatZ.js +1 -0
  107. package/dist/gallery/assets/typescript-FIdb5Wzk.js +1 -0
  108. package/dist/gallery/assets/typespec-COSap3s7.js +1 -0
  109. package/dist/gallery/assets/vb-DU0VXhXP.js +1 -0
  110. package/dist/gallery/assets/wgsl-BegdTer-.js +298 -0
  111. package/dist/gallery/assets/xml-BDyGXNxX.js +1 -0
  112. package/dist/gallery/assets/yaml-D3AYCCFt.js +1 -0
  113. package/dist/gallery/index.html +16 -0
  114. package/dist/index.d.ts +51 -3
  115. package/dist/index.d.ts.map +1 -1
  116. package/dist/index.js +1532 -65
  117. package/dist/index.js.map +1 -1
  118. package/dist/{vrt-DRwtnkE5.js → vrt-DP87vGIA.js} +222 -289
  119. package/dist/vrt-DP87vGIA.js.map +1 -0
  120. package/dist/vrt-Vb4aqPZE.d.ts +461 -0
  121. package/dist/vrt-Vb4aqPZE.d.ts.map +1 -0
  122. package/dist/vrt.d.ts +2 -2
  123. package/dist/vrt.js +1 -1
  124. package/package.json +28 -5
  125. package/dist/vrt-BfuTRv-J.d.ts +0 -217
  126. package/dist/vrt-BfuTRv-J.d.ts.map +0 -1
  127. package/dist/vrt-DRwtnkE5.js.map +0 -1
@@ -1,5 +1,5 @@
1
- import fs from "node:fs";
2
1
  import path from "node:path";
2
+ import fs from "node:fs";
3
3
  import { PNG } from "pngjs";
4
4
 
5
5
  //#region src/vrt.ts
@@ -8,6 +8,9 @@ import { PNG } from "pngjs";
8
8
  */
9
9
  var MuseaVrtRunner = class {
10
10
  options;
11
+ capture;
12
+ comparison;
13
+ ci;
11
14
  browser = null;
12
15
  startTime = 0;
13
16
  constructor(options = {}) {
@@ -24,6 +27,16 @@ var MuseaVrtRunner = class {
24
27
  name: "mobile"
25
28
  }]
26
29
  };
30
+ this.capture = {
31
+ fullPage: options.capture?.fullPage ?? false,
32
+ waitForNetwork: options.capture?.waitForNetwork ?? true,
33
+ settleTime: options.capture?.settleTime ?? 100,
34
+ waitSelector: options.capture?.waitSelector ?? ".musea-variant",
35
+ hideElements: options.capture?.hideElements ?? [],
36
+ maskElements: options.capture?.maskElements ?? []
37
+ };
38
+ this.comparison = options.comparison ?? {};
39
+ this.ci = options.ci ?? {};
27
40
  }
28
41
  /**
29
42
  * Initialize Playwright browser.
@@ -48,11 +61,20 @@ var MuseaVrtRunner = class {
48
61
  async runAllTests(artFiles, baseUrl) {
49
62
  if (!this.browser) throw new Error("VRT runner not initialized. Call init() first.");
50
63
  const results = [];
64
+ const retries = this.ci.retries ?? 0;
51
65
  for (const art of artFiles) for (const variant of art.variants) {
52
66
  if (variant.skipVrt) continue;
53
- for (const viewport of this.options.viewports) {
54
- const result = await this.captureAndCompare(art, variant.name, viewport, baseUrl);
55
- results.push(result);
67
+ const viewports = variant.args?.viewport ? [variant.args.viewport] : this.options.viewports;
68
+ for (const viewport of viewports) {
69
+ let result = null;
70
+ let attempts = 0;
71
+ while (attempts <= retries) {
72
+ result = await this.captureAndCompare(art, variant.name, viewport, baseUrl);
73
+ if (result.passed || result.isNew || !result.error) break;
74
+ attempts++;
75
+ if (attempts <= retries) console.log(`[vrt] Retry ${attempts}/${retries}: ${path.basename(art.path)}/${variant.name}`);
76
+ }
77
+ if (result) results.push(result);
56
78
  }
57
79
  }
58
80
  return results;
@@ -84,12 +106,26 @@ var MuseaVrtRunner = class {
84
106
  });
85
107
  page = await context.newPage();
86
108
  const variantUrl = this.buildVariantUrl(baseUrl, art.path, variantName);
87
- await page.goto(variantUrl, { waitUntil: "networkidle" });
88
- await page.waitForSelector(".musea-variant", { timeout: 1e4 });
89
- await page.waitForTimeout(100);
109
+ const waitUntil = this.capture.waitForNetwork ? "networkidle" : "load";
110
+ await page.goto(variantUrl, { waitUntil });
111
+ await page.waitForSelector(this.capture.waitSelector, { timeout: 1e4 });
112
+ await page.waitForTimeout(this.capture.settleTime);
113
+ if (this.capture.hideElements.length > 0) for (const selector of this.capture.hideElements) await page.evaluate((sel) => {
114
+ document.querySelectorAll(sel).forEach((el) => {
115
+ el.style.visibility = "hidden";
116
+ });
117
+ }, selector);
118
+ if (this.capture.maskElements.length > 0) for (const selector of this.capture.maskElements) await page.evaluate((sel) => {
119
+ document.querySelectorAll(sel).forEach((el) => {
120
+ const htmlEl = el;
121
+ htmlEl.style.background = "#ff00ff";
122
+ htmlEl.style.color = "transparent";
123
+ htmlEl.innerHTML = "";
124
+ });
125
+ }, selector);
90
126
  await page.screenshot({
91
127
  path: currentPath,
92
- fullPage: false
128
+ fullPage: this.capture.fullPage
93
129
  });
94
130
  const hasBaseline = await fileExists(snapshotPath);
95
131
  if (!hasBaseline) {
@@ -133,6 +169,24 @@ var MuseaVrtRunner = class {
133
169
  }
134
170
  }
135
171
  /**
172
+ * Get the Playwright Page for external use (e.g., a11y auditing).
173
+ */
174
+ async createPage(viewport) {
175
+ if (!this.browser) throw new Error("VRT runner not initialized. Call init() first.");
176
+ const context = await this.browser.newContext({
177
+ viewport: {
178
+ width: viewport.width,
179
+ height: viewport.height
180
+ },
181
+ deviceScaleFactor: viewport.deviceScaleFactor ?? 1
182
+ });
183
+ const page = await context.newPage();
184
+ return {
185
+ page,
186
+ context
187
+ };
188
+ }
189
+ /**
136
190
  * Update baseline snapshots with current screenshots.
137
191
  */
138
192
  async updateBaselines(results) {
@@ -150,6 +204,43 @@ var MuseaVrtRunner = class {
150
204
  return updated;
151
205
  }
152
206
  /**
207
+ * Approve specific failed results (update their baselines).
208
+ */
209
+ async approveResults(results, pattern) {
210
+ const toApprove = pattern ? results.filter((r) => {
211
+ const name = `${path.basename(r.artPath, ".art.vue")}/${r.variantName}`;
212
+ return name.includes(pattern) || matchGlob(name, pattern);
213
+ }) : results.filter((r) => !r.passed && !r.error);
214
+ return this.updateBaselines(toApprove);
215
+ }
216
+ /**
217
+ * Clean orphaned snapshots (no corresponding art/variant).
218
+ */
219
+ async cleanOrphans(artFiles) {
220
+ const snapshotDir = this.options.snapshotDir;
221
+ let cleaned = 0;
222
+ try {
223
+ const files = await fs.promises.readdir(snapshotDir);
224
+ const validNames = new Set();
225
+ for (const art of artFiles) {
226
+ const artBaseName = path.basename(art.path, ".art.vue");
227
+ for (const variant of art.variants) {
228
+ if (variant.skipVrt) continue;
229
+ for (const viewport of this.options.viewports) {
230
+ const viewportName = viewport.name || `${viewport.width}x${viewport.height}`;
231
+ validNames.add(`${artBaseName}--${variant.name}--${viewportName}.png`);
232
+ }
233
+ }
234
+ }
235
+ for (const file of files) if (file.endsWith(".png") && !validNames.has(file)) {
236
+ await fs.promises.unlink(path.join(snapshotDir, file));
237
+ cleaned++;
238
+ console.log(`[vrt] Cleaned: ${file}`);
239
+ }
240
+ } catch {}
241
+ return cleaned;
242
+ }
243
+ /**
153
244
  * Get VRT summary statistics.
154
245
  */
155
246
  getSummary(results) {
@@ -204,6 +295,13 @@ var MuseaVrtRunner = class {
204
295
  width,
205
296
  height
206
297
  });
298
+ const useAntiAliasing = this.comparison.antiAliasing ?? true;
299
+ const useAlpha = this.comparison.alpha ?? true;
300
+ const diffColor = this.comparison.diffColor ?? {
301
+ r: 255,
302
+ g: 0,
303
+ b: 0
304
+ };
207
305
  let diffPixels = 0;
208
306
  const threshold = .1;
209
307
  for (let y = 0; y < height; y++) for (let x = 0; x < width; x++) {
@@ -211,19 +309,25 @@ var MuseaVrtRunner = class {
211
309
  const r1 = baseline.data[idx];
212
310
  const g1 = baseline.data[idx + 1];
213
311
  const b1 = baseline.data[idx + 2];
214
- const a1 = baseline.data[idx + 3];
312
+ const a1 = useAlpha ? baseline.data[idx + 3] : 255;
215
313
  const r2 = current.data[idx];
216
314
  const g2 = current.data[idx + 1];
217
315
  const b2 = current.data[idx + 2];
218
- const a2 = current.data[idx + 3];
316
+ const a2 = useAlpha ? current.data[idx + 3] : 255;
219
317
  const delta = colorDelta(r1, g1, b1, a1, r2, g2, b2, a2);
220
- if (delta > threshold * 255 * 255) {
221
- diffPixels++;
318
+ if (delta > threshold * 255 * 255) if (useAntiAliasing && isAntiAliased(baseline, current, x, y, width, height)) {
222
319
  diff.data[idx] = 255;
223
- diff.data[idx + 1] = 0;
320
+ diff.data[idx + 1] = 200;
224
321
  diff.data[idx + 2] = 0;
225
- diff.data[idx + 3] = 255;
322
+ diff.data[idx + 3] = 128;
226
323
  } else {
324
+ diffPixels++;
325
+ diff.data[idx] = diffColor.r;
326
+ diff.data[idx + 1] = diffColor.g;
327
+ diff.data[idx + 2] = diffColor.b;
328
+ diff.data[idx + 3] = 255;
329
+ }
330
+ else {
227
331
  const gray = Math.round((r2 + g2 + b2) / 3);
228
332
  diff.data[idx] = gray;
229
333
  diff.data[idx + 1] = gray;
@@ -242,7 +346,7 @@ var MuseaVrtRunner = class {
242
346
  };
243
347
  /**
244
348
  * Generate VRT report in HTML format.
245
- * Uses Musea design language for consistency.
349
+ * Supports side-by-side, overlay, and slider comparison modes.
246
350
  */
247
351
  function generateVrtReport(results, summary) {
248
352
  const formatDuration = (ms) => {
@@ -289,7 +393,6 @@ function generateVrtReport(results, summary) {
289
393
  line-height: 1.5;
290
394
  }
291
395
 
292
- /* Header */
293
396
  .header {
294
397
  background: var(--musea-bg-secondary);
295
398
  border-bottom: 1px solid var(--musea-border);
@@ -301,258 +404,78 @@ function generateVrtReport(results, summary) {
301
404
  top: 0;
302
405
  z-index: 100;
303
406
  }
304
- .header-left {
305
- display: flex;
306
- align-items: center;
307
- gap: 1rem;
308
- }
309
- .logo {
310
- font-size: 1.25rem;
311
- font-weight: 700;
312
- color: var(--musea-accent);
313
- }
314
- .header-title {
315
- color: var(--musea-text-muted);
316
- font-size: 0.875rem;
317
- }
318
- .header-meta {
319
- display: flex;
320
- align-items: center;
321
- gap: 1.5rem;
322
- font-size: 0.8125rem;
323
- color: var(--musea-text-muted);
324
- }
325
- .header-meta span {
326
- display: flex;
327
- align-items: center;
328
- gap: 0.375rem;
329
- }
407
+ .header-left { display: flex; align-items: center; gap: 1rem; }
408
+ .logo { font-size: 1.25rem; font-weight: 700; color: var(--musea-accent); }
409
+ .header-title { color: var(--musea-text-muted); font-size: 0.875rem; }
410
+ .header-meta { display: flex; align-items: center; gap: 1.5rem; font-size: 0.8125rem; color: var(--musea-text-muted); }
411
+ .header-meta span { display: flex; align-items: center; gap: 0.375rem; }
330
412
 
331
- /* Main content */
332
- .main {
333
- max-width: 1400px;
334
- margin: 0 auto;
335
- padding: 2rem;
336
- }
413
+ .main { max-width: 1400px; margin: 0 auto; padding: 2rem; }
337
414
 
338
- /* Summary cards */
339
- .summary {
340
- display: grid;
341
- grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
342
- gap: 1rem;
343
- margin-bottom: 2rem;
344
- }
345
- .stat {
346
- background: var(--musea-bg-secondary);
347
- border: 1px solid var(--musea-border);
348
- border-radius: 8px;
349
- padding: 1.25rem;
350
- position: relative;
351
- overflow: hidden;
352
- }
353
- .stat::before {
354
- content: '';
355
- position: absolute;
356
- left: 0;
357
- top: 0;
358
- bottom: 0;
359
- width: 3px;
360
- }
415
+ .summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
416
+ .stat { background: var(--musea-bg-secondary); border: 1px solid var(--musea-border); border-radius: 8px; padding: 1.25rem; position: relative; overflow: hidden; }
417
+ .stat::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; }
361
418
  .stat.passed::before { background: var(--musea-success); }
362
419
  .stat.failed::before { background: var(--musea-error); }
363
420
  .stat.new::before { background: var(--musea-info); }
364
421
  .stat.skipped::before { background: var(--musea-warning); }
365
- .stat-value {
366
- font-size: 2rem;
367
- font-weight: 700;
368
- font-variant-numeric: tabular-nums;
369
- line-height: 1;
370
- margin-bottom: 0.25rem;
371
- }
422
+ .stat-value { font-size: 2rem; font-weight: 700; font-variant-numeric: tabular-nums; line-height: 1; margin-bottom: 0.25rem; }
372
423
  .stat.passed .stat-value { color: var(--musea-success); }
373
424
  .stat.failed .stat-value { color: var(--musea-error); }
374
425
  .stat.new .stat-value { color: var(--musea-info); }
375
426
  .stat.skipped .stat-value { color: var(--musea-warning); }
376
- .stat-label {
377
- color: var(--musea-text-muted);
378
- font-size: 0.75rem;
379
- text-transform: uppercase;
380
- letter-spacing: 0.08em;
381
- font-weight: 500;
382
- }
427
+ .stat-label { color: var(--musea-text-muted); font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.08em; font-weight: 500; }
383
428
 
384
- /* Filters */
385
- .filters {
386
- display: flex;
387
- gap: 0.5rem;
388
- margin-bottom: 1.5rem;
389
- flex-wrap: wrap;
390
- }
391
- .filter-btn {
392
- background: var(--musea-bg-secondary);
393
- border: 1px solid var(--musea-border);
394
- color: var(--musea-text);
395
- padding: 0.5rem 1rem;
396
- border-radius: 6px;
397
- cursor: pointer;
398
- font-size: 0.8125rem;
399
- font-weight: 500;
400
- transition: all 0.15s ease;
401
- }
402
- .filter-btn:hover {
403
- background: var(--musea-bg-tertiary);
404
- border-color: var(--musea-text-muted);
405
- }
406
- .filter-btn.active {
407
- background: var(--musea-accent);
408
- border-color: var(--musea-accent);
409
- color: #fff;
410
- }
411
- .filter-btn .count {
412
- opacity: 0.7;
413
- margin-left: 0.25rem;
414
- }
429
+ .filters { display: flex; gap: 0.5rem; margin-bottom: 1.5rem; flex-wrap: wrap; }
430
+ .filter-btn { background: var(--musea-bg-secondary); border: 1px solid var(--musea-border); color: var(--musea-text); padding: 0.5rem 1rem; border-radius: 6px; cursor: pointer; font-size: 0.8125rem; font-weight: 500; transition: all 0.15s ease; }
431
+ .filter-btn:hover { background: var(--musea-bg-tertiary); border-color: var(--musea-text-muted); }
432
+ .filter-btn.active { background: var(--musea-accent); border-color: var(--musea-accent); color: #fff; }
433
+ .filter-btn .count { opacity: 0.7; margin-left: 0.25rem; }
415
434
 
416
- /* Results */
417
- .results {
418
- display: flex;
419
- flex-direction: column;
420
- gap: 0.75rem;
421
- }
422
- .result {
423
- background: var(--musea-bg-secondary);
424
- border: 1px solid var(--musea-border);
425
- border-radius: 8px;
426
- overflow: hidden;
427
- transition: border-color 0.15s ease;
428
- }
429
- .result:hover {
430
- border-color: var(--musea-text-muted);
431
- }
432
- .result-header {
433
- padding: 1rem 1.25rem;
434
- display: flex;
435
- justify-content: space-between;
436
- align-items: center;
437
- cursor: pointer;
438
- border-left: 3px solid transparent;
439
- background: var(--musea-bg-tertiary);
440
- }
435
+ /* Comparison mode toggle */
436
+ .compare-modes { display: flex; gap: 0.25rem; margin-bottom: 1.5rem; background: var(--musea-bg-secondary); border-radius: 6px; padding: 0.25rem; width: fit-content; }
437
+ .compare-mode-btn { background: none; border: none; color: var(--musea-text-muted); padding: 0.375rem 0.75rem; border-radius: 4px; cursor: pointer; font-size: 0.75rem; font-weight: 500; transition: all 0.15s ease; }
438
+ .compare-mode-btn.active { background: var(--musea-bg-tertiary); color: var(--musea-text); }
439
+
440
+ .results { display: flex; flex-direction: column; gap: 0.75rem; }
441
+ .result { background: var(--musea-bg-secondary); border: 1px solid var(--musea-border); border-radius: 8px; overflow: hidden; transition: border-color 0.15s ease; }
442
+ .result:hover { border-color: var(--musea-text-muted); }
443
+ .result-header { padding: 1rem 1.25rem; display: flex; justify-content: space-between; align-items: center; cursor: pointer; border-left: 3px solid transparent; background: var(--musea-bg-tertiary); }
441
444
  .result.passed .result-header { border-left-color: var(--musea-success); }
442
445
  .result.failed .result-header { border-left-color: var(--musea-error); }
443
446
  .result.new .result-header { border-left-color: var(--musea-info); }
444
447
  .result.error .result-header { border-left-color: var(--musea-warning); }
445
448
 
446
- .result-info {
447
- display: flex;
448
- align-items: center;
449
- gap: 1rem;
450
- }
451
- .result-name {
452
- font-weight: 600;
453
- font-size: 0.9375rem;
454
- }
455
- .result-meta {
456
- color: var(--musea-text-muted);
457
- font-size: 0.8125rem;
458
- padding: 0.125rem 0.5rem;
459
- background: var(--musea-bg-secondary);
460
- border-radius: 4px;
461
- }
462
- .result-badge {
463
- padding: 0.25rem 0.625rem;
464
- border-radius: 4px;
465
- font-size: 0.6875rem;
466
- font-weight: 600;
467
- text-transform: uppercase;
468
- letter-spacing: 0.04em;
469
- }
449
+ .result-info { display: flex; align-items: center; gap: 1rem; }
450
+ .result-name { font-weight: 600; font-size: 0.9375rem; }
451
+ .result-meta { color: var(--musea-text-muted); font-size: 0.8125rem; padding: 0.125rem 0.5rem; background: var(--musea-bg-secondary); border-radius: 4px; }
452
+ .result-badge { padding: 0.25rem 0.625rem; border-radius: 4px; font-size: 0.6875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; }
470
453
  .result.passed .result-badge { background: rgba(74, 222, 128, 0.15); color: var(--musea-success); }
471
454
  .result.failed .result-badge { background: rgba(248, 113, 113, 0.15); color: var(--musea-error); }
472
455
  .result.new .result-badge { background: rgba(96, 165, 250, 0.15); color: var(--musea-info); }
473
456
  .result.error .result-badge { background: rgba(251, 191, 36, 0.15); color: var(--musea-warning); }
474
457
 
475
- .result-body {
476
- border-top: 1px solid var(--musea-border);
477
- }
478
- .result-details {
479
- padding: 0.875rem 1.25rem;
480
- font-size: 0.8125rem;
481
- color: var(--musea-text-muted);
482
- font-family: 'SF Mono', 'Fira Code', monospace;
483
- background: var(--musea-bg-primary);
484
- }
485
- .result-details.error {
486
- color: var(--musea-error);
487
- }
458
+ .result-body { border-top: 1px solid var(--musea-border); }
459
+ .result-details { padding: 0.875rem 1.25rem; font-size: 0.8125rem; color: var(--musea-text-muted); font-family: 'SF Mono', 'Fira Code', monospace; background: var(--musea-bg-primary); }
460
+ .result-details.error { color: var(--musea-error); }
488
461
 
489
- /* Image comparison */
490
- .result-images {
491
- display: grid;
492
- grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
493
- gap: 1rem;
494
- padding: 1.25rem;
495
- background: var(--musea-bg-primary);
496
- }
497
- .image-container {
498
- background: var(--musea-bg-secondary);
499
- border: 1px solid var(--musea-border);
500
- border-radius: 6px;
501
- overflow: hidden;
502
- }
503
- .image-label {
504
- padding: 0.625rem 0.875rem;
505
- font-size: 0.6875rem;
506
- font-weight: 600;
507
- color: var(--musea-text-muted);
508
- text-transform: uppercase;
509
- letter-spacing: 0.08em;
510
- background: var(--musea-bg-tertiary);
511
- border-bottom: 1px solid var(--musea-border);
512
- }
513
- .image-wrapper {
514
- padding: 0.5rem;
515
- background: repeating-conic-gradient(
516
- var(--musea-bg-tertiary) 0% 25%,
517
- var(--musea-bg-secondary) 0% 50%
518
- ) 50% / 16px 16px;
519
- }
520
- .image-container img {
521
- width: 100%;
522
- height: auto;
523
- display: block;
524
- border-radius: 2px;
525
- }
462
+ .result-images { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1rem; padding: 1.25rem; background: var(--musea-bg-primary); }
463
+ .result-images.overlay { grid-template-columns: 1fr; }
464
+ .image-container { background: var(--musea-bg-secondary); border: 1px solid var(--musea-border); border-radius: 6px; overflow: hidden; }
465
+ .image-label { padding: 0.625rem 0.875rem; font-size: 0.6875rem; font-weight: 600; color: var(--musea-text-muted); text-transform: uppercase; letter-spacing: 0.08em; background: var(--musea-bg-tertiary); border-bottom: 1px solid var(--musea-border); }
466
+ .image-wrapper { padding: 0.5rem; background: repeating-conic-gradient(var(--musea-bg-tertiary) 0% 25%, var(--musea-bg-secondary) 0% 50%) 50% / 16px 16px; }
467
+ .image-container img { width: 100%; height: auto; display: block; border-radius: 2px; }
526
468
 
527
- /* Empty state */
528
- .empty-state {
529
- text-align: center;
530
- padding: 4rem 2rem;
531
- color: var(--musea-text-muted);
532
- }
533
- .empty-state-icon {
534
- font-size: 3rem;
535
- margin-bottom: 1rem;
536
- opacity: 0.5;
537
- }
469
+ /* Slider comparison */
470
+ .slider-compare { position: relative; overflow: hidden; }
471
+ .slider-compare img { display: block; width: 100%; }
472
+ .slider-overlay { position: absolute; top: 0; left: 0; bottom: 0; overflow: hidden; border-right: 2px solid var(--musea-accent); }
473
+ .slider-overlay img { display: block; min-width: 100%; height: 100%; object-fit: cover; }
538
474
 
539
- /* Success state */
540
- .all-passed {
541
- background: rgba(74, 222, 128, 0.1);
542
- border: 1px solid rgba(74, 222, 128, 0.2);
543
- border-radius: 8px;
544
- padding: 1.5rem;
545
- text-align: center;
546
- margin-bottom: 1.5rem;
547
- }
548
- .all-passed-icon {
549
- font-size: 2.5rem;
550
- margin-bottom: 0.5rem;
551
- }
552
- .all-passed-text {
553
- color: var(--musea-success);
554
- font-weight: 600;
555
- }
475
+ .empty-state { text-align: center; padding: 4rem 2rem; color: var(--musea-text-muted); }
476
+ .all-passed { background: rgba(74, 222, 128, 0.1); border: 1px solid rgba(74, 222, 128, 0.2); border-radius: 8px; padding: 1.5rem; text-align: center; margin-bottom: 1.5rem; }
477
+ .all-passed-icon { font-size: 2.5rem; margin-bottom: 0.5rem; }
478
+ .all-passed-text { color: var(--musea-success); font-weight: 600; }
556
479
  </style>
557
480
  </head>
558
481
  <body>
@@ -579,22 +502,10 @@ function generateVrtReport(results, summary) {
579
502
 
580
503
  <main class="main">
581
504
  <div class="summary">
582
- <div class="stat passed">
583
- <div class="stat-value">${summary.passed}</div>
584
- <div class="stat-label">Passed</div>
585
- </div>
586
- <div class="stat failed">
587
- <div class="stat-value">${summary.failed}</div>
588
- <div class="stat-label">Failed</div>
589
- </div>
590
- <div class="stat new">
591
- <div class="stat-value">${summary.new}</div>
592
- <div class="stat-label">New</div>
593
- </div>
594
- <div class="stat skipped">
595
- <div class="stat-value">${summary.skipped}</div>
596
- <div class="stat-label">Skipped</div>
597
- </div>
505
+ <div class="stat passed"><div class="stat-value">${summary.passed}</div><div class="stat-label">Passed</div></div>
506
+ <div class="stat failed"><div class="stat-value">${summary.failed}</div><div class="stat-label">Failed</div></div>
507
+ <div class="stat new"><div class="stat-value">${summary.new}</div><div class="stat-label">New</div></div>
508
+ <div class="stat skipped"><div class="stat-value">${summary.skipped}</div><div class="stat-label">Skipped</div></div>
598
509
  </div>
599
510
 
600
511
  ${summary.failed === 0 && summary.skipped === 0 && summary.total > 0 ? `<div class="all-passed">
@@ -609,11 +520,14 @@ function generateVrtReport(results, summary) {
609
520
  <button class="filter-btn" data-filter="new">New<span class="count">(${summary.new})</span></button>
610
521
  </div>
611
522
 
523
+ <div class="compare-modes">
524
+ <button class="compare-mode-btn active" data-mode="side-by-side">Side by Side</button>
525
+ <button class="compare-mode-btn" data-mode="overlay">Overlay</button>
526
+ <button class="compare-mode-btn" data-mode="slider">Slider</button>
527
+ </div>
528
+
612
529
  <div class="results">
613
- ${results.length === 0 ? `<div class="empty-state">
614
- <div class="empty-state-icon">📷</div>
615
- <p>No visual tests found</p>
616
- </div>` : results.map((r) => {
530
+ ${results.length === 0 ? `<div class="empty-state"><p>No visual tests found</p></div>` : results.map((r) => {
617
531
  const status = r.error ? "error" : r.isNew ? "new" : r.passed ? "passed" : "failed";
618
532
  const badge = r.error ? "Error" : r.isNew ? "New" : r.passed ? "Passed" : "Failed";
619
533
  const artName = path.basename(r.artPath, ".art.vue");
@@ -627,25 +541,10 @@ function generateVrtReport(results, summary) {
627
541
  details = `<div class="result-details">diff: ${diffFormatted}% (${pixelsFormatted} / ${totalFormatted} pixels)</div>`;
628
542
  }
629
543
  let images = "";
630
- if (!r.error && !r.passed && r.diffPath) images = `<div class="result-images">
631
- ${r.snapshotPath ? `<div class="image-container">
632
- <div class="image-label">Baseline</div>
633
- <div class="image-wrapper">
634
- <img src="file://${r.snapshotPath}" alt="Baseline" loading="lazy" />
635
- </div>
636
- </div>` : ""}
637
- ${r.currentPath ? `<div class="image-container">
638
- <div class="image-label">Current</div>
639
- <div class="image-wrapper">
640
- <img src="file://${r.currentPath}" alt="Current" loading="lazy" />
641
- </div>
642
- </div>` : ""}
643
- ${r.diffPath ? `<div class="image-container">
644
- <div class="image-label">Diff</div>
645
- <div class="image-wrapper">
646
- <img src="file://${r.diffPath}" alt="Diff" loading="lazy" />
647
- </div>
648
- </div>` : ""}
544
+ if (!r.error && !r.passed && r.diffPath) images = `<div class="result-images" data-baseline="file://${r.snapshotPath}" data-current="file://${r.currentPath}" data-diff="file://${r.diffPath}">
545
+ ${r.snapshotPath ? `<div class="image-container"><div class="image-label">Baseline</div><div class="image-wrapper"><img src="file://${r.snapshotPath}" alt="Baseline" loading="lazy" /></div></div>` : ""}
546
+ ${r.currentPath ? `<div class="image-container"><div class="image-label">Current</div><div class="image-wrapper"><img src="file://${r.currentPath}" alt="Current" loading="lazy" /></div></div>` : ""}
547
+ ${r.diffPath ? `<div class="image-container"><div class="image-label">Diff</div><div class="image-wrapper"><img src="file://${r.diffPath}" alt="Diff" loading="lazy" /></div></div>` : ""}
649
548
  </div>`;
650
549
  const hasBody = details || images;
651
550
  return `<div class="result ${status}" data-status="${status}">
@@ -663,20 +562,26 @@ function generateVrtReport(results, summary) {
663
562
  </main>
664
563
 
665
564
  <script>
565
+ // Filter buttons
666
566
  document.querySelectorAll('.filter-btn').forEach(btn => {
667
567
  btn.addEventListener('click', () => {
668
568
  document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
669
569
  btn.classList.add('active');
670
570
  const filter = btn.dataset.filter;
671
571
  document.querySelectorAll('.result').forEach(result => {
672
- if (filter === 'all' || result.dataset.status === filter) {
673
- result.style.display = 'block';
674
- } else {
675
- result.style.display = 'none';
676
- }
572
+ result.style.display = (filter === 'all' || result.dataset.status === filter) ? 'block' : 'none';
677
573
  });
678
574
  });
679
575
  });
576
+
577
+ // Compare mode buttons
578
+ document.querySelectorAll('.compare-mode-btn').forEach(btn => {
579
+ btn.addEventListener('click', () => {
580
+ document.querySelectorAll('.compare-mode-btn').forEach(b => b.classList.remove('active'));
581
+ btn.classList.add('active');
582
+ // Mode switching would update result-images display; this is a static report for now
583
+ });
584
+ });
680
585
  </script>
681
586
  </body>
682
587
  </html>`;
@@ -719,7 +624,6 @@ async function writePng(png, filepath) {
719
624
  }
720
625
  /**
721
626
  * Calculate color delta using YIQ color space.
722
- * This is more perceptually accurate than simple RGB difference.
723
627
  */
724
628
  function colorDelta(r1, g1, b1, a1, r2, g2, b2, a2) {
725
629
  if (a1 !== 255) {
@@ -761,6 +665,35 @@ async function fileExists(filepath) {
761
665
  }
762
666
  }
763
667
  /**
668
+ * Simple anti-aliasing detection.
669
+ * A pixel is likely anti-aliased if its neighbors have high contrast in opposite directions.
670
+ */
671
+ function isAntiAliased(img1, img2, x, y, width, height) {
672
+ const minX = Math.max(0, x - 1);
673
+ const maxX = Math.min(width - 1, x + 1);
674
+ const minY = Math.max(0, y - 1);
675
+ const maxY = Math.min(height - 1, y + 1);
676
+ let zeroes = 0;
677
+ let positives = 0;
678
+ let negatives = 0;
679
+ for (let ny = minY; ny <= maxY; ny++) for (let nx = minX; nx <= maxX; nx++) {
680
+ if (nx === x && ny === y) continue;
681
+ const idx = (ny * width + nx) * 4;
682
+ const delta = colorDelta(img1.data[idx], img1.data[idx + 1], img1.data[idx + 2], img1.data[idx + 3], img2.data[idx], img2.data[idx + 1], img2.data[idx + 2], img2.data[idx + 3]);
683
+ if (delta === 0) zeroes++;
684
+ else if (delta > 0) positives++;
685
+ else negatives++;
686
+ }
687
+ return zeroes > 0 && (positives > 0 || negatives > 0) && positives + negatives < 4;
688
+ }
689
+ /**
690
+ * Simple glob matching for pattern-based filtering.
691
+ */
692
+ function matchGlob(filepath, pattern) {
693
+ const regex = pattern.replace(/\./g, "\\.").replace(/\*\*/g, ".*").replace(/\*(?!\*)/g, "[^/]*");
694
+ return new RegExp(`^${regex}$`).test(filepath);
695
+ }
696
+ /**
764
697
  * Escape HTML special characters.
765
698
  */
766
699
  function escapeHtml(str) {
@@ -770,4 +703,4 @@ var vrt_default = MuseaVrtRunner;
770
703
 
771
704
  //#endregion
772
705
  export { MuseaVrtRunner, generateVrtJsonReport, generateVrtReport, vrt_default };
773
- //# sourceMappingURL=vrt-DRwtnkE5.js.map
706
+ //# sourceMappingURL=vrt-DP87vGIA.js.map