@logicsync/pixelprobe 0.1.0

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 (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +377 -0
  3. package/dist/bin/pixelprobe.d.ts +3 -0
  4. package/dist/bin/pixelprobe.d.ts.map +1 -0
  5. package/dist/bin/pixelprobe.js +218 -0
  6. package/dist/bin/pixelprobe.js.map +1 -0
  7. package/dist/browser/BrowserManager.d.ts +20 -0
  8. package/dist/browser/BrowserManager.d.ts.map +1 -0
  9. package/dist/browser/BrowserManager.js +64 -0
  10. package/dist/browser/BrowserManager.js.map +1 -0
  11. package/dist/browser/ViewportManager.d.ts +15 -0
  12. package/dist/browser/ViewportManager.d.ts.map +1 -0
  13. package/dist/browser/ViewportManager.js +65 -0
  14. package/dist/browser/ViewportManager.js.map +1 -0
  15. package/dist/cli/formatter.d.ts +4 -0
  16. package/dist/cli/formatter.d.ts.map +1 -0
  17. package/dist/cli/formatter.js +107 -0
  18. package/dist/cli/formatter.js.map +1 -0
  19. package/dist/comparator/DiffAggregator.d.ts +24 -0
  20. package/dist/comparator/DiffAggregator.d.ts.map +1 -0
  21. package/dist/comparator/DiffAggregator.js +94 -0
  22. package/dist/comparator/DiffAggregator.js.map +1 -0
  23. package/dist/comparator/LayoutComparator.d.ts +8 -0
  24. package/dist/comparator/LayoutComparator.d.ts.map +1 -0
  25. package/dist/comparator/LayoutComparator.js +68 -0
  26. package/dist/comparator/LayoutComparator.js.map +1 -0
  27. package/dist/comparator/PixelComparator.d.ts +9 -0
  28. package/dist/comparator/PixelComparator.d.ts.map +1 -0
  29. package/dist/comparator/PixelComparator.js +72 -0
  30. package/dist/comparator/PixelComparator.js.map +1 -0
  31. package/dist/comparator/StyleComparator.d.ts +8 -0
  32. package/dist/comparator/StyleComparator.d.ts.map +1 -0
  33. package/dist/comparator/StyleComparator.js +119 -0
  34. package/dist/comparator/StyleComparator.js.map +1 -0
  35. package/dist/core.d.ts +23 -0
  36. package/dist/core.d.ts.map +1 -0
  37. package/dist/core.js +211 -0
  38. package/dist/core.js.map +1 -0
  39. package/dist/extractor/ScreenshotCapturer.d.ts +20 -0
  40. package/dist/extractor/ScreenshotCapturer.d.ts.map +1 -0
  41. package/dist/extractor/ScreenshotCapturer.js +56 -0
  42. package/dist/extractor/ScreenshotCapturer.js.map +1 -0
  43. package/dist/extractor/SectionExtractor.d.ts +26 -0
  44. package/dist/extractor/SectionExtractor.d.ts.map +1 -0
  45. package/dist/extractor/SectionExtractor.js +92 -0
  46. package/dist/extractor/SectionExtractor.js.map +1 -0
  47. package/dist/extractor/StyleExtractor.d.ts +23 -0
  48. package/dist/extractor/StyleExtractor.d.ts.map +1 -0
  49. package/dist/extractor/StyleExtractor.js +88 -0
  50. package/dist/extractor/StyleExtractor.js.map +1 -0
  51. package/dist/index.d.ts +8 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +9 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/mcp/server.d.ts +3 -0
  56. package/dist/mcp/server.d.ts.map +1 -0
  57. package/dist/mcp/server.js +203 -0
  58. package/dist/mcp/server.js.map +1 -0
  59. package/dist/reporter/HTMLReporter.d.ts +8 -0
  60. package/dist/reporter/HTMLReporter.d.ts.map +1 -0
  61. package/dist/reporter/HTMLReporter.js +219 -0
  62. package/dist/reporter/HTMLReporter.js.map +1 -0
  63. package/dist/reporter/JSONReporter.d.ts +17 -0
  64. package/dist/reporter/JSONReporter.d.ts.map +1 -0
  65. package/dist/reporter/JSONReporter.js +77 -0
  66. package/dist/reporter/JSONReporter.js.map +1 -0
  67. package/dist/types/comparison.d.ts +133 -0
  68. package/dist/types/comparison.d.ts.map +1 -0
  69. package/dist/types/comparison.js +3 -0
  70. package/dist/types/comparison.js.map +1 -0
  71. package/dist/types/config.d.ts +138 -0
  72. package/dist/types/config.d.ts.map +1 -0
  73. package/dist/types/config.js +151 -0
  74. package/dist/types/config.js.map +1 -0
  75. package/package.json +72 -0
@@ -0,0 +1,88 @@
1
+ import { STYLE_CATEGORIES } from "../types/config.js";
2
+ export class StyleExtractor {
3
+ /**
4
+ * Extract computed styles for specific categories from an element.
5
+ */
6
+ static async extractStyles(locator, categories) {
7
+ const propertiesToExtract = StyleExtractor.getProperties(categories);
8
+ return await locator.evaluate((el, props) => {
9
+ const computed = window.getComputedStyle(el);
10
+ const styles = {};
11
+ for (const prop of props) {
12
+ const value = computed.getPropertyValue(prop);
13
+ if (value) {
14
+ styles[prop] = value;
15
+ }
16
+ }
17
+ return styles;
18
+ }, propertiesToExtract);
19
+ }
20
+ /**
21
+ * Extract layout metrics (box model) from an element.
22
+ */
23
+ static async extractLayoutMetrics(locator) {
24
+ return await locator.evaluate((el) => {
25
+ const rect = el.getBoundingClientRect();
26
+ const computed = window.getComputedStyle(el);
27
+ const parseNum = (val) => parseFloat(val) || 0;
28
+ return {
29
+ boundingBox: {
30
+ x: Math.round(rect.x * 100) / 100,
31
+ y: Math.round(rect.y * 100) / 100,
32
+ width: Math.round(rect.width * 100) / 100,
33
+ height: Math.round(rect.height * 100) / 100,
34
+ },
35
+ margin: {
36
+ top: parseNum(computed.marginTop),
37
+ right: parseNum(computed.marginRight),
38
+ bottom: parseNum(computed.marginBottom),
39
+ left: parseNum(computed.marginLeft),
40
+ },
41
+ padding: {
42
+ top: parseNum(computed.paddingTop),
43
+ right: parseNum(computed.paddingRight),
44
+ bottom: parseNum(computed.paddingBottom),
45
+ left: parseNum(computed.paddingLeft),
46
+ },
47
+ border: {
48
+ top: parseNum(computed.borderTopWidth),
49
+ right: parseNum(computed.borderRightWidth),
50
+ bottom: parseNum(computed.borderBottomWidth),
51
+ left: parseNum(computed.borderLeftWidth),
52
+ },
53
+ scrollWidth: el.scrollWidth,
54
+ scrollHeight: el.scrollHeight,
55
+ };
56
+ });
57
+ }
58
+ /**
59
+ * Extract computed styles for ALL properties (not just categorized ones).
60
+ * Useful for deep analysis.
61
+ */
62
+ static async extractAllStyles(locator) {
63
+ return await locator.evaluate((el) => {
64
+ const computed = window.getComputedStyle(el);
65
+ const styles = {};
66
+ for (let i = 0; i < computed.length; i++) {
67
+ const prop = computed[i];
68
+ styles[prop] = computed.getPropertyValue(prop);
69
+ }
70
+ return styles;
71
+ });
72
+ }
73
+ /**
74
+ * Get the flat list of CSS properties for given categories.
75
+ */
76
+ static getProperties(categories) {
77
+ const cats = categories ?? Object.keys(STYLE_CATEGORIES);
78
+ const props = [];
79
+ for (const cat of cats) {
80
+ const catProps = STYLE_CATEGORIES[cat];
81
+ if (catProps) {
82
+ props.push(...catProps);
83
+ }
84
+ }
85
+ return [...new Set(props)]; // deduplicate
86
+ }
87
+ }
88
+ //# sourceMappingURL=StyleExtractor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StyleExtractor.js","sourceRoot":"","sources":["../../src/extractor/StyleExtractor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ export { WebSectionComparator } from "./core.js";
2
+ export type { ComparisonConfig, ResolvedConfig, Breakpoint, PresetBreakpointName, StyleCategory, } from "./types/config.js";
3
+ export { ComparisonConfigSchema, PRESET_BREAKPOINTS, STYLE_CATEGORIES, } from "./types/config.js";
4
+ export type { ComparisonResult, ComparisonSummary, BreakpointResult, StyleDifference, StyleComparisonResult, PixelComparisonResult, LayoutDifference, LayoutComparisonResult, ElementInfo, ExtractedSection, LayoutMetrics, ProgressCallback, ProgressEvent, Severity, } from "./types/comparison.js";
5
+ export { JSONReporter } from "./reporter/JSONReporter.js";
6
+ export { HTMLReporter } from "./reporter/HTMLReporter.js";
7
+ export { ViewportManager } from "./browser/ViewportManager.js";
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC;AAGjD,YAAY,EACV,gBAAgB,EAChB,cAAc,EACd,UAAU,EACV,oBAAoB,EACpB,aAAa,GACd,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,sBAAsB,EACtB,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,mBAAmB,CAAC;AAE3B,YAAY,EACV,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,eAAe,EACf,qBAAqB,EACrB,qBAAqB,EACrB,gBAAgB,EAChB,sBAAsB,EACtB,WAAW,EACX,gBAAgB,EAChB,aAAa,EACb,gBAAgB,EAChB,aAAa,EACb,QAAQ,GACT,MAAM,uBAAuB,CAAC;AAG/B,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAG1D,OAAO,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ // ── Main API ────────────────────────────────────────────────────────
2
+ export { WebSectionComparator } from "./core.js";
3
+ export { ComparisonConfigSchema, PRESET_BREAKPOINTS, STYLE_CATEGORIES, } from "./types/config.js";
4
+ // ── Reporters ───────────────────────────────────────────────────────
5
+ export { JSONReporter } from "./reporter/JSONReporter.js";
6
+ export { HTMLReporter } from "./reporter/HTMLReporter.js";
7
+ // ── Browser utilities ───────────────────────────────────────────────
8
+ export { ViewportManager } from "./browser/ViewportManager.js";
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,OAAO,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC;AAWjD,OAAO,EACL,sBAAsB,EACtB,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,mBAAmB,CAAC;AAmB3B,uEAAuE;AACvE,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAE1D,uEAAuE;AACvE,OAAO,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC"}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":""}
@@ -0,0 +1,203 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import { WebSectionComparator } from "../core.js";
6
+ import { JSONReporter } from "../reporter/JSONReporter.js";
7
+ import { HTMLReporter } from "../reporter/HTMLReporter.js";
8
+ import { PRESET_BREAKPOINTS } from "../types/config.js";
9
+ const server = new McpServer({
10
+ name: "pixelprobe",
11
+ version: "0.1.0",
12
+ });
13
+ // ── Tool: compare_sections ──────────────────────────────────────────
14
+ server.tool("compare_sections", "Compare a web page section between source and target URLs across responsive breakpoints. " +
15
+ "Returns detailed style, layout, and visual differences at each breakpoint. " +
16
+ "Use this to verify that a rebuilt section matches the original design.", {
17
+ sourceUrl: z.string().url().describe("Source URL (the reference/original page)"),
18
+ targetUrl: z.string().url().describe("Target URL (the local build or new version)"),
19
+ selector: z
20
+ .string()
21
+ .min(1)
22
+ .describe('CSS selector for the section on the source page (e.g. ".hero-section", "#pricing")'),
23
+ targetSelector: z
24
+ .string()
25
+ .min(1)
26
+ .optional()
27
+ .describe('CSS selector for the section on the target page, if different from source. Falls back to selector.'),
28
+ elementIndex: z
29
+ .number()
30
+ .int()
31
+ .min(0)
32
+ .optional()
33
+ .describe("Element index if selector matches multiple elements (default: 0)"),
34
+ breakpoints: z
35
+ .array(z.string())
36
+ .optional()
37
+ .describe('Breakpoint names to test. Presets: mobile, tablet, desktop, desktop-lg. Or use WIDTHxHEIGHT format. Default: ["mobile", "tablet", "desktop", "desktop-lg"]'),
38
+ pixelThreshold: z
39
+ .number()
40
+ .min(0)
41
+ .max(1)
42
+ .optional()
43
+ .describe("Pixel comparison sensitivity (0-1, lower = stricter). Default: 0.1"),
44
+ includeVisual: z
45
+ .boolean()
46
+ .optional()
47
+ .describe("Include pixel-level visual comparison. Default: true"),
48
+ includeStyles: z
49
+ .boolean()
50
+ .optional()
51
+ .describe("Include computed style comparison. Default: true"),
52
+ includeLayout: z
53
+ .boolean()
54
+ .optional()
55
+ .describe("Include layout metrics comparison. Default: true"),
56
+ waitForTimeout: z
57
+ .number()
58
+ .int()
59
+ .min(0)
60
+ .optional()
61
+ .describe("Wait N ms after page load before capturing. Default: 0"),
62
+ outputDir: z
63
+ .string()
64
+ .optional()
65
+ .describe("Directory to save report artifacts. Default: ./pixelprobe-output"),
66
+ }, async (args) => {
67
+ const comparator = new WebSectionComparator({
68
+ headless: true,
69
+ timeout: 30000,
70
+ });
71
+ try {
72
+ const result = await comparator.compare({
73
+ sourceUrl: args.sourceUrl,
74
+ targetUrl: args.targetUrl,
75
+ selector: args.selector,
76
+ targetSelector: args.targetSelector,
77
+ elementIndex: args.elementIndex ?? 0,
78
+ breakpoints: args.breakpoints,
79
+ pixelThreshold: args.pixelThreshold ?? 0.1,
80
+ includeVisual: args.includeVisual ?? true,
81
+ includeStyles: args.includeStyles ?? true,
82
+ includeLayout: args.includeLayout ?? true,
83
+ waitForTimeout: args.waitForTimeout ?? 0,
84
+ });
85
+ // Save reports into a timestamped run directory
86
+ const baseDir = args.outputDir ?? "./pixelprobe-output";
87
+ const timestamp = new Date()
88
+ .toISOString()
89
+ .replace(/[:.]/g, "-")
90
+ .replace("T", "_")
91
+ .slice(0, 19);
92
+ const runDir = `${baseDir}/run_${timestamp}_${result.id}`;
93
+ const jsonPath = await JSONReporter.generate(result, runDir);
94
+ const htmlPath = await HTMLReporter.generate(result, runDir);
95
+ result.artifacts.jsonPath = jsonPath;
96
+ result.artifacts.htmlReportPath = htmlPath;
97
+ // Return compact summary for the agent
98
+ const summary = JSONReporter.toCompactSummary(result);
99
+ return {
100
+ content: [
101
+ {
102
+ type: "text",
103
+ text: JSON.stringify(summary, null, 2),
104
+ },
105
+ {
106
+ type: "text",
107
+ text: `\n---\nReports saved:\n JSON: ${jsonPath}\n HTML: ${htmlPath}`,
108
+ },
109
+ ],
110
+ };
111
+ }
112
+ catch (err) {
113
+ return {
114
+ content: [
115
+ {
116
+ type: "text",
117
+ text: `Error during comparison: ${err.message}`,
118
+ },
119
+ ],
120
+ isError: true,
121
+ };
122
+ }
123
+ });
124
+ // ── Tool: capture_section ───────────────────────────────────────────
125
+ server.tool("capture_section", "Capture information about a section on a web page — its computed styles, layout metrics, " +
126
+ "and a screenshot. Useful for inspecting a single page before running a comparison.", {
127
+ url: z.string().url().describe("URL of the page to inspect"),
128
+ selector: z.string().min(1).describe("CSS selector for the section"),
129
+ elementIndex: z
130
+ .number()
131
+ .int()
132
+ .min(0)
133
+ .optional()
134
+ .describe("Element index if selector matches multiple (default: 0)"),
135
+ breakpoint: z
136
+ .string()
137
+ .optional()
138
+ .describe('Breakpoint to use. Default: "desktop"'),
139
+ }, async (args) => {
140
+ const comparator = new WebSectionComparator({ headless: true });
141
+ try {
142
+ // First enumerate to show what's available
143
+ const elements = await comparator.enumerate(args.url, args.selector, args.breakpoint);
144
+ if (elements.length === 0) {
145
+ return {
146
+ content: [
147
+ {
148
+ type: "text",
149
+ text: `No elements found matching "${args.selector}" on ${args.url}`,
150
+ },
151
+ ],
152
+ isError: true,
153
+ };
154
+ }
155
+ const elementList = elements
156
+ .map((el) => ` [${el.index}] <${el.tagName}> ${el.boundingBox.width}×${el.boundingBox.height} at (${el.boundingBox.x},${el.boundingBox.y}) — "${el.textPreview}"`)
157
+ .join("\n");
158
+ return {
159
+ content: [
160
+ {
161
+ type: "text",
162
+ text: `Found ${elements.length} element(s) matching "${args.selector}":\n${elementList}\n\nUse elementIndex to select a specific one for comparison.`,
163
+ },
164
+ ],
165
+ };
166
+ }
167
+ catch (err) {
168
+ return {
169
+ content: [
170
+ {
171
+ type: "text",
172
+ text: `Error: ${err.message}`,
173
+ },
174
+ ],
175
+ isError: true,
176
+ };
177
+ }
178
+ });
179
+ // ── Tool: list_breakpoints ──────────────────────────────────────────
180
+ server.tool("list_breakpoints", "List all available responsive breakpoint presets with their dimensions.", {}, async () => {
181
+ const presets = Object.entries(PRESET_BREAKPOINTS)
182
+ .map(([name, bp]) => ` ${name.padEnd(14)} ${bp.width}×${bp.height} ${bp.label}`)
183
+ .join("\n");
184
+ return {
185
+ content: [
186
+ {
187
+ type: "text",
188
+ text: `Available breakpoint presets:\n${presets}\n\nYou can also use custom WIDTHxHEIGHT format (e.g. "1440x900").`,
189
+ },
190
+ ],
191
+ };
192
+ });
193
+ // ── Start Server ────────────────────────────────────────────────────
194
+ async function main() {
195
+ const transport = new StdioServerTransport();
196
+ await server.connect(transport);
197
+ console.error("pixelprobe MCP server running on stdio");
198
+ }
199
+ main().catch((err) => {
200
+ console.error("Fatal error:", err);
201
+ process.exit(1);
202
+ });
203
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import type { ComparisonResult } from "../types/comparison.js";
2
+ export declare class HTMLReporter {
3
+ /**
4
+ * Generate a self-contained interactive HTML report.
5
+ */
6
+ static generate(result: ComparisonResult, outputDir: string): Promise<string>;
7
+ }
8
+ //# sourceMappingURL=HTMLReporter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HTMLReporter.d.ts","sourceRoot":"","sources":["../../src/reporter/HTMLReporter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAoB,MAAM,wBAAwB,CAAC;AAEjF,qBAAa,YAAY;IACvB;;OAEG;WACU,QAAQ,CACnB,MAAM,EAAE,gBAAgB,EACxB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,CAAC;CA2BnB"}
@@ -0,0 +1,219 @@
1
+ import { writeFile, mkdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ export class HTMLReporter {
4
+ /**
5
+ * Generate a self-contained interactive HTML report.
6
+ */
7
+ static async generate(result, outputDir) {
8
+ await mkdir(outputDir, { recursive: true });
9
+ // Save screenshots and diff images, collect base64 for embedding
10
+ const imageData = {};
11
+ for (const [bpName, bpResult] of Object.entries(result.breakpoints)) {
12
+ imageData[bpName] = {};
13
+ if (bpResult.visual?.diffImage) {
14
+ imageData[bpName].diff = bufferToDataUrl(bpResult.visual.diffImage);
15
+ // Also save to disk
16
+ const diffPath = join(outputDir, `diff-${bpName}.png`);
17
+ await writeFile(diffPath, bpResult.visual.diffImage);
18
+ result.artifacts.diffImages[bpName] = diffPath;
19
+ }
20
+ }
21
+ const html = buildHTML(result, imageData);
22
+ const htmlPath = join(outputDir, `report-${result.id}.html`);
23
+ await writeFile(htmlPath, html);
24
+ return htmlPath;
25
+ }
26
+ }
27
+ function bufferToDataUrl(buf) {
28
+ return `data:image/png;base64,${buf.toString("base64")}`;
29
+ }
30
+ function severityBadge(severity) {
31
+ const colors = {
32
+ critical: "#ef4444",
33
+ warning: "#f59e0b",
34
+ info: "#3b82f6",
35
+ pass: "#22c55e",
36
+ };
37
+ const color = colors[severity] || "#6b7280";
38
+ return `<span style="background:${color};color:white;padding:2px 8px;border-radius:4px;font-size:12px;font-weight:600;text-transform:uppercase">${severity}</span>`;
39
+ }
40
+ function buildHTML(result, imageData) {
41
+ const breakpointTabs = Object.entries(result.breakpoints)
42
+ .map(([name, bp], i) => `
43
+ <button class="tab ${i === 0 ? "active" : ""}" onclick="showTab('${name}', this)">
44
+ ${name} (${bp.breakpoint.width}px)
45
+ ${bp.error ? "⚠️" : bp.styles?.summary.critical ? "🔴" : bp.styles?.summary.warning ? "🟡" : "🟢"}
46
+ </button>`)
47
+ .join("");
48
+ const breakpointPanels = Object.entries(result.breakpoints)
49
+ .map(([name, bp], i) => `
50
+ <div class="panel ${i === 0 ? "active" : ""}" id="panel-${name}">
51
+ ${bp.error ? `<div class="error">Error: ${escapeHtml(bp.error)}</div>` : buildBreakpointPanel(name, bp, imageData[name])}
52
+ </div>`)
53
+ .join("");
54
+ return `<!DOCTYPE html>
55
+ <html lang="en">
56
+ <head>
57
+ <meta charset="UTF-8">
58
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
59
+ <title>Selection Comparison Report — ${result.id}</title>
60
+ <style>
61
+ * { box-sizing: border-box; margin: 0; padding: 0; }
62
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #0f172a; color: #e2e8f0; padding: 24px; }
63
+ .container { max-width: 1400px; margin: 0 auto; }
64
+ h1 { font-size: 24px; margin-bottom: 8px; }
65
+ .meta { color: #94a3b8; font-size: 14px; margin-bottom: 24px; }
66
+ .meta a { color: #60a5fa; }
67
+ .summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 32px; }
68
+ .summary-card { background: #1e293b; border-radius: 8px; padding: 16px; }
69
+ .summary-card .label { font-size: 12px; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.5px; }
70
+ .summary-card .value { font-size: 28px; font-weight: 700; margin-top: 4px; }
71
+ .tabs { display: flex; gap: 4px; border-bottom: 2px solid #1e293b; margin-bottom: 24px; overflow-x: auto; overflow-y: hidden; }
72
+ .tab { background: none; border: none; color: #94a3b8; padding: 10px 16px; cursor: pointer; font-size: 14px; border-bottom: 2px solid transparent; margin-bottom: -2px; white-space: nowrap; }
73
+ .tab:hover { color: #e2e8f0; }
74
+ .tab.active { color: #60a5fa; border-bottom-color: #60a5fa; }
75
+ .panel { display: none; }
76
+ .panel.active { display: block; }
77
+ .section { background: #1e293b; border-radius: 8px; padding: 20px; margin-bottom: 16px; }
78
+ .section h3 { font-size: 16px; margin-bottom: 12px; display: flex; align-items: center; gap: 8px; }
79
+ .diff-table { width: 100%; border-collapse: collapse; font-size: 13px; }
80
+ .diff-table th { text-align: left; padding: 8px 12px; border-bottom: 1px solid #334155; color: #94a3b8; font-weight: 500; }
81
+ .diff-table td { padding: 8px 12px; border-bottom: 1px solid #1e293b; font-family: "SF Mono", Monaco, monospace; font-size: 12px; }
82
+ .diff-table tr:hover { background: #334155; }
83
+ .severity-critical { color: #ef4444; }
84
+ .severity-warning { color: #f59e0b; }
85
+ .severity-info { color: #3b82f6; }
86
+ .diff-img { max-width: 100%; border-radius: 4px; border: 1px solid #334155; }
87
+ .error { background: #451a1a; border: 1px solid #ef4444; border-radius: 8px; padding: 16px; color: #fca5a5; }
88
+ .pixel-stats { display: flex; gap: 24px; margin-bottom: 12px; font-size: 14px; }
89
+ .pixel-stats .stat { color: #94a3b8; }
90
+ .pixel-stats .stat strong { color: #e2e8f0; }
91
+ .category-tag { background: #334155; color: #94a3b8; padding: 2px 6px; border-radius: 3px; font-size: 11px; }
92
+ </style>
93
+ </head>
94
+ <body>
95
+ <div class="container">
96
+ <h1>Selection Comparison Report</h1>
97
+ <div class="meta">
98
+ <strong>Source:</strong> <a href="${escapeHtml(result.config.sourceUrl)}">${escapeHtml(result.config.sourceUrl)}</a> · <code>${escapeHtml(result.config.selector)}</code><br>
99
+ <strong>Target:</strong> <a href="${escapeHtml(result.config.targetUrl)}">${escapeHtml(result.config.targetUrl)}</a> · <code>${escapeHtml(result.config.targetSelector ?? result.config.selector)}</code><br>
100
+ <strong>Duration:</strong> ${(result.duration / 1000).toFixed(1)}s ·
101
+ <strong>Generated:</strong> ${result.timestamp}
102
+ </div>
103
+
104
+ <div class="summary-grid">
105
+ <div class="summary-card">
106
+ <div class="label">Overall</div>
107
+ <div class="value">${severityBadge(result.summary.overallSeverity)}</div>
108
+ </div>
109
+ <div class="summary-card">
110
+ <div class="label">Breakpoints with Differences</div>
111
+ <div class="value">${result.summary.breakpointsWithDifferences}/${result.summary.totalBreakpoints}</div>
112
+ </div>
113
+ <div class="summary-card">
114
+ <div class="label">Style Differences</div>
115
+ <div class="value">${result.summary.totalStyleDifferences}</div>
116
+ </div>
117
+ <div class="summary-card">
118
+ <div class="label">Avg Pixel Diff</div>
119
+ <div class="value">${result.summary.averagePixelDiff}%</div>
120
+ </div>
121
+ <div class="summary-card">
122
+ <div class="label">Worst Breakpoint</div>
123
+ <div class="value" style="font-size:18px">${result.summary.worstBreakpoint ?? "None"}</div>
124
+ </div>
125
+ </div>
126
+
127
+ <div class="tabs">${breakpointTabs}</div>
128
+ ${breakpointPanels}
129
+ </div>
130
+
131
+ <script>
132
+ function showTab(name, btn) {
133
+ document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
134
+ document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
135
+ btn.classList.add('active');
136
+ document.getElementById('panel-' + name).classList.add('active');
137
+ }
138
+ </script>
139
+ </body>
140
+ </html>`;
141
+ }
142
+ function buildBreakpointPanel(name, bp, images) {
143
+ let html = "";
144
+ // Visual diff
145
+ if (bp.visual) {
146
+ html += `
147
+ <div class="section">
148
+ <h3>Visual Comparison ${severityBadge(bp.visual.diffPercentage > 5 ? "critical" : bp.visual.diffPercentage > 1 ? "warning" : bp.visual.diffPercentage > 0 ? "info" : "pass")}</h3>
149
+ <div class="pixel-stats">
150
+ <div class="stat">Pixels changed: <strong>${bp.visual.diffPixelCount.toLocaleString()}</strong> / ${bp.visual.totalPixels.toLocaleString()}</div>
151
+ <div class="stat">Diff: <strong>${bp.visual.diffPercentage}%</strong></div>
152
+ <div class="stat">Source size: <strong>${bp.visual.sourceWidth}×${bp.visual.sourceHeight}</strong></div>
153
+ <div class="stat">Target size: <strong>${bp.visual.targetWidth}×${bp.visual.targetHeight}</strong></div>
154
+ ${bp.visual.sizeMismatch ? '<div class="stat severity-warning">⚠ Size mismatch</div>' : ""}
155
+ </div>
156
+ ${images?.diff ? `<img class="diff-img" src="${images.diff}" alt="Visual diff for ${name}">` : ""}
157
+ </div>`;
158
+ }
159
+ // Style diffs
160
+ if (bp.styles && bp.styles.differences.length > 0) {
161
+ html += `
162
+ <div class="section">
163
+ <h3>Style Differences (${bp.styles.summary.total}) ${severityBadge(bp.styles.summary.critical > 0 ? "critical" : bp.styles.summary.warning > 0 ? "warning" : "info")}</h3>
164
+ <table class="diff-table">
165
+ <thead><tr><th>Property</th><th>Category</th><th>Source</th><th>Target</th><th>Severity</th></tr></thead>
166
+ <tbody>
167
+ ${bp.styles.differences
168
+ .map((d) => `
169
+ <tr>
170
+ <td><strong>${escapeHtml(d.property)}</strong></td>
171
+ <td><span class="category-tag">${escapeHtml(d.category)}</span></td>
172
+ <td>${escapeHtml(d.sourceValue)}</td>
173
+ <td>${escapeHtml(d.targetValue)}</td>
174
+ <td class="severity-${d.severity}">${d.severity}</td>
175
+ </tr>`)
176
+ .join("")}
177
+ </tbody>
178
+ </table>
179
+ </div>`;
180
+ }
181
+ else if (bp.styles) {
182
+ html += `<div class="section"><h3>Style Differences ${severityBadge("pass")}</h3><p style="color:#94a3b8">No style differences found.</p></div>`;
183
+ }
184
+ // Layout diffs
185
+ if (bp.layout && bp.layout.differences.length > 0) {
186
+ html += `
187
+ <div class="section">
188
+ <h3>Layout Differences (${bp.layout.summary.total}) ${severityBadge(bp.layout.summary.critical > 0 ? "critical" : bp.layout.summary.warning > 0 ? "warning" : "info")}</h3>
189
+ <table class="diff-table">
190
+ <thead><tr><th>Property</th><th>Source</th><th>Target</th><th>Diff (px)</th><th>Diff (%)</th><th>Severity</th></tr></thead>
191
+ <tbody>
192
+ ${bp.layout.differences
193
+ .map((d) => `
194
+ <tr>
195
+ <td><strong>${escapeHtml(d.property)}</strong></td>
196
+ <td>${d.sourceValue}px</td>
197
+ <td>${d.targetValue}px</td>
198
+ <td>${d.absoluteDiff > 0 ? "+" : ""}${d.absoluteDiff}px</td>
199
+ <td>${d.percentageDiff > 0 ? "+" : ""}${d.percentageDiff}%</td>
200
+ <td class="severity-${d.severity}">${d.severity}</td>
201
+ </tr>`)
202
+ .join("")}
203
+ </tbody>
204
+ </table>
205
+ </div>`;
206
+ }
207
+ else if (bp.layout) {
208
+ html += `<div class="section"><h3>Layout Differences ${severityBadge("pass")}</h3><p style="color:#94a3b8">No layout differences found.</p></div>`;
209
+ }
210
+ return html;
211
+ }
212
+ function escapeHtml(str) {
213
+ return str
214
+ .replace(/&/g, "&amp;")
215
+ .replace(/</g, "&lt;")
216
+ .replace(/>/g, "&gt;")
217
+ .replace(/"/g, "&quot;");
218
+ }
219
+ //# sourceMappingURL=HTMLReporter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HTMLReporter.js","sourceRoot":"","sources":["../../src/reporter/HTMLReporter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import type { ComparisonResult } from "../types/comparison.js";
2
+ export declare class JSONReporter {
3
+ /**
4
+ * Generate a JSON report file.
5
+ * Strips binary data (screenshots, diff images) and replaces with file paths.
6
+ */
7
+ static generate(result: ComparisonResult, outputDir: string): Promise<string>;
8
+ /**
9
+ * Convert result to a JSON-safe object (no Buffers).
10
+ */
11
+ static toSerializable(result: ComparisonResult): any;
12
+ /**
13
+ * Generate a compact JSON summary (no per-property details).
14
+ */
15
+ static toCompactSummary(result: ComparisonResult): object;
16
+ }
17
+ //# sourceMappingURL=JSONReporter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"JSONReporter.d.ts","sourceRoot":"","sources":["../../src/reporter/JSONReporter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE/D,qBAAa,YAAY;IACvB;;;OAGG;WACU,QAAQ,CACnB,MAAM,EAAE,gBAAgB,EACxB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,CAAC;IAwBlB;;OAEG;IACH,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,gBAAgB,GAAG,GAAG;IAuBpD;;OAEG;IACH,MAAM,CAAC,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM;CA2B1D"}