@design-parity/report-html 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.
package/README.md ADDED
@@ -0,0 +1,58 @@
1
+ # @design-parity/report-html
2
+
3
+ The per-run **self-contained HTML comparison page**: the rich artifact a
4
+ reviewer opens from a PR to actually *review* a mismatch.
5
+
6
+ Given the same `(DesignReference, CandidateRender, Verdict)` the markdown
7
+ summary uses — plus optional diff panels — `renderHtmlReport` emits **one
8
+ offline `.html` string**:
9
+
10
+ - **reference | candidate | diff** side by side per variant (paired by
11
+ `state/theme/size`, tolerant when a side omits or relabels the size), with a
12
+ vanilla opacity/overlay slider to wipe between reference and candidate.
13
+ - the verdict findings in **value order** (a11y + i18n → token → semantic →
14
+ visual), each with its severity, and the token expected-vs-actual shown
15
+ inline from `finding.detail`.
16
+ - everything inlined: CSS in a `<style>`, the slider JS in a `<script>`, and
17
+ every image as a `data:image/png;base64,…` URI. **No external requests** — it
18
+ opens from a file, as a CI artifact, or on GitHub Pages.
19
+
20
+ It is a **leaf consumer**: it depends only on `@design-parity/core`. The diff
21
+ panels arrive as a generic `DiffImage { key; png }`, so it never depends on
22
+ `@design-parity/diff`.
23
+
24
+ Output is **deterministic** — same input, byte-identical HTML. No timestamps,
25
+ random ids, or `Date.now()` (docs/PRINCIPLES.md, Principle 1).
26
+
27
+ ## Usage
28
+
29
+ ```ts
30
+ import { renderHtmlReport } from "@design-parity/report-html";
31
+
32
+ const html = renderHtmlReport({
33
+ reference, // DesignReference
34
+ candidate, // CandidateRender
35
+ verdict, // Verdict
36
+ diffImages: triptychs.map((t) => ({ key: t.key, png: t.png })), // optional
37
+ repoRoot, // resolves reference/candidate Image.uri (defaults to cwd)
38
+ });
39
+ // write `html` to one .html file
40
+ ```
41
+
42
+ Reference and candidate PNGs are read from disk via `repoRoot + Image.uri`
43
+ (the same resolution the diff engine uses) and inlined. A side with no images
44
+ (a semantic/token-only verdict) degrades cleanly — the findings still render.
45
+
46
+ ## CLI
47
+
48
+ A small demo CLI ships as `design-parity-report`:
49
+
50
+ ```sh
51
+ design-parity-report \
52
+ --reference fixtures/figma/button-primary.reference.json \
53
+ --candidate fixtures/candidate/button-primary.candidate.json \
54
+ --verdict out/verdict.json \
55
+ --diff default/dark/compact=out/diff-dark.png \
56
+ --repo . \
57
+ --out report.html
58
+ ```
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=report.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../../src/cli/report.ts"],"names":[],"mappings":""}
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `design-parity-report` — write a self-contained HTML comparison page.
4
+ *
5
+ * design-parity-report \
6
+ * --reference figma/button.reference.json \
7
+ * --candidate candidate/button.candidate.json \
8
+ * --verdict out/verdict.json \
9
+ * --diff default/dark/compact=out/diff-dark.png \
10
+ * --repo . \
11
+ * --out report.html
12
+ *
13
+ * Reads a `DesignReference`, a `CandidateRender`, and a `Verdict` (the diff
14
+ * engine's JSON), optionally one or more `key=path.png` diff panels, and writes
15
+ * the rendered HTML to `--out` (or stdout). Handy for demos; the Action wires
16
+ * the same `renderHtmlReport` call in steady state.
17
+ */
18
+ import { readFileSync, writeFileSync } from "node:fs";
19
+ import { resolve } from "node:path";
20
+ import { argv, cwd, exit, stderr, stdout } from "node:process";
21
+ import { renderHtmlReport } from "../render.js";
22
+ function parseArgs(args) {
23
+ const out = { repoRoot: cwd(), diffs: [] };
24
+ for (let i = 0; i < args.length; i++) {
25
+ const a = args[i];
26
+ const next = () => {
27
+ const v = args[(i += 1)];
28
+ if (v === undefined)
29
+ throw new Error(`missing value for ${a}`);
30
+ return v;
31
+ };
32
+ switch (a) {
33
+ case "--reference":
34
+ out.reference = next();
35
+ break;
36
+ case "--candidate":
37
+ out.candidate = next();
38
+ break;
39
+ case "--verdict":
40
+ out.verdict = next();
41
+ break;
42
+ case "--repo":
43
+ out.repoRoot = resolve(next());
44
+ break;
45
+ case "--out":
46
+ out.out = next();
47
+ break;
48
+ case "--diff": {
49
+ const spec = next();
50
+ const eq = spec.indexOf("=");
51
+ if (eq === -1)
52
+ throw new Error(`--diff expects key=path, got ${spec}`);
53
+ out.diffs.push({ key: spec.slice(0, eq), path: spec.slice(eq + 1) });
54
+ break;
55
+ }
56
+ default:
57
+ throw new Error(`unknown argument: ${a}`);
58
+ }
59
+ }
60
+ return out;
61
+ }
62
+ function readJson(path) {
63
+ return JSON.parse(readFileSync(path, "utf8"));
64
+ }
65
+ function main() {
66
+ const args = parseArgs(argv.slice(2));
67
+ if (!args.reference || !args.candidate || !args.verdict) {
68
+ stderr.write("usage: design-parity-report --reference <json> --candidate <json> --verdict <json> [--diff key=png]... [--repo dir] [--out file]\n");
69
+ exit(2);
70
+ }
71
+ const diffImages = args.diffs.map((d) => ({
72
+ key: d.key,
73
+ png: readFileSync(d.path),
74
+ }));
75
+ const html = renderHtmlReport({
76
+ reference: readJson(args.reference),
77
+ candidate: readJson(args.candidate),
78
+ verdict: readJson(args.verdict),
79
+ diffImages,
80
+ repoRoot: args.repoRoot,
81
+ });
82
+ if (args.out) {
83
+ writeFileSync(args.out, html);
84
+ stderr.write(`wrote ${args.out}\n`);
85
+ }
86
+ else {
87
+ stdout.write(html);
88
+ }
89
+ }
90
+ main();
91
+ //# sourceMappingURL=report.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report.js","sourceRoot":"","sources":["../../src/cli/report.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Group verdict findings into value-ordered sections (docs/PRINCIPLES.md,
3
+ * Principle 2): accessibility + i18n first, then token compliance, then
4
+ * semantics, then the raw visual diff. Mirrors the markdown summary so the two
5
+ * surfaces agree.
6
+ */
7
+ import type { Finding, FindingKind } from "@design-parity/core";
8
+ export interface FindingSection {
9
+ title: string;
10
+ kinds: FindingKind[];
11
+ }
12
+ export declare const SECTIONS: FindingSection[];
13
+ export interface GroupedSection {
14
+ title: string;
15
+ findings: Finding[];
16
+ }
17
+ /** Findings bucketed by section, in value order; empty sections dropped. */
18
+ export declare function groupFindings(findings: readonly Finding[]): GroupedSection[];
19
+ /**
20
+ * Pull an expected/actual token delta out of a finding's `detail` when present
21
+ * (the diff engine stamps `{ token, expected, actual, delta }`).
22
+ */
23
+ export interface TokenDelta {
24
+ token?: string;
25
+ expected?: string;
26
+ actual?: string;
27
+ }
28
+ export declare function tokenDelta(finding: Finding): TokenDelta | undefined;
29
+ //# sourceMappingURL=findings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"findings.d.ts","sourceRoot":"","sources":["../src/findings.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAEhE,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,WAAW,EAAE,CAAC;CACtB;AAED,eAAO,MAAM,QAAQ,EAAE,cAAc,EAKpC,CAAC;AAEF,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,OAAO,EAAE,CAAC;CACrB;AAED,4EAA4E;AAC5E,wBAAgB,aAAa,CAAC,QAAQ,EAAE,SAAS,OAAO,EAAE,GAAG,cAAc,EAAE,CAO5E;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,UAAU,GAAG,SAAS,CAanE"}
@@ -0,0 +1,31 @@
1
+ export const SECTIONS = [
2
+ { title: "Accessibility & i18n", kinds: ["contrast", "a11y", "i18n"] },
3
+ { title: "Token compliance", kinds: ["token"] },
4
+ { title: "Semantics", kinds: ["semantic"] },
5
+ { title: "Visual", kinds: ["visual"] },
6
+ ];
7
+ /** Findings bucketed by section, in value order; empty sections dropped. */
8
+ export function groupFindings(findings) {
9
+ const grouped = [];
10
+ for (const section of SECTIONS) {
11
+ const matched = findings.filter((f) => section.kinds.includes(f.kind));
12
+ if (matched.length > 0)
13
+ grouped.push({ title: section.title, findings: matched });
14
+ }
15
+ return grouped;
16
+ }
17
+ export function tokenDelta(finding) {
18
+ const d = finding.detail;
19
+ if (!d)
20
+ return undefined;
21
+ const has = "token" in d || "expected" in d || "actual" in d;
22
+ if (!has)
23
+ return undefined;
24
+ const fmt = (v) => v === undefined || v === null ? undefined : String(v);
25
+ return {
26
+ token: fmt(d["token"]),
27
+ expected: fmt(d["expected"]),
28
+ actual: fmt(d["actual"]),
29
+ };
30
+ }
31
+ //# sourceMappingURL=findings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"findings.js","sourceRoot":"","sources":["../src/findings.ts"],"names":[],"mappings":"AAaA,MAAM,CAAC,MAAM,QAAQ,GAAqB;IACxC,EAAE,KAAK,EAAE,sBAAsB,EAAE,KAAK,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE;IACtE,EAAE,KAAK,EAAE,kBAAkB,EAAE,KAAK,EAAE,CAAC,OAAO,CAAC,EAAE;IAC/C,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC,UAAU,CAAC,EAAE;IAC3C,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,QAAQ,CAAC,EAAE;CACvC,CAAC;AAOF,4EAA4E;AAC5E,MAAM,UAAU,aAAa,CAAC,QAA4B;IACxD,MAAM,OAAO,GAAqB,EAAE,CAAC;IACrC,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;QACvE,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC;IACpF,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAYD,MAAM,UAAU,UAAU,CAAC,OAAgB;IACzC,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;IACzB,IAAI,CAAC,CAAC;QAAE,OAAO,SAAS,CAAC;IACzB,MAAM,GAAG,GACP,OAAO,IAAI,CAAC,IAAI,UAAU,IAAI,CAAC,IAAI,QAAQ,IAAI,CAAC,CAAC;IACnD,IAAI,CAAC,GAAG;QAAE,OAAO,SAAS,CAAC;IAC3B,MAAM,GAAG,GAAG,CAAC,CAAU,EAAsB,EAAE,CAC7C,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IACxD,OAAO;QACL,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;QACtB,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;QAC5B,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;KACzB,CAAC;AACJ,CAAC"}
package/dist/html.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Tiny HTML helpers. No template engine — everything is inlined by hand so the
3
+ * output is deterministic and the page is fully self-contained (no escaping
4
+ * surprises, no external dependency).
5
+ */
6
+ /** Escape text for use in element content / double-quoted attributes. */
7
+ export declare function escapeHtml(value: string): string;
8
+ /** Wrap raw PNG bytes in a `data:` URI suitable for an `<img src>`. */
9
+ export declare function pngDataUri(png: Uint8Array): string;
10
+ //# sourceMappingURL=html.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../src/html.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,yEAAyE;AACzE,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOhD;AAED,uEAAuE;AACvE,wBAAgB,UAAU,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,CAGlD"}
package/dist/html.js ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Tiny HTML helpers. No template engine — everything is inlined by hand so the
3
+ * output is deterministic and the page is fully self-contained (no escaping
4
+ * surprises, no external dependency).
5
+ */
6
+ /** Escape text for use in element content / double-quoted attributes. */
7
+ export function escapeHtml(value) {
8
+ return value
9
+ .replace(/&/g, "&amp;")
10
+ .replace(/</g, "&lt;")
11
+ .replace(/>/g, "&gt;")
12
+ .replace(/"/g, "&quot;")
13
+ .replace(/'/g, "&#39;");
14
+ }
15
+ /** Wrap raw PNG bytes in a `data:` URI suitable for an `<img src>`. */
16
+ export function pngDataUri(png) {
17
+ // Buffer.from(Uint8Array) shares no quirks here; base64 is deterministic.
18
+ return `data:image/png;base64,${Buffer.from(png).toString("base64")}`;
19
+ }
20
+ //# sourceMappingURL=html.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html.js","sourceRoot":"","sources":["../src/html.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,yEAAyE;AACzE,MAAM,UAAU,UAAU,CAAC,KAAa;IACtC,OAAO,KAAK;SACT,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC;SACvB,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;AAC5B,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,UAAU,CAAC,GAAe;IACxC,0EAA0E;IAC1E,OAAO,yBAAyB,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;AACxE,CAAC"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * `@design-parity/report-html` — the per-run, self-contained HTML comparison
3
+ * page.
4
+ *
5
+ * Consume the same `(DesignReference, CandidateRender, Verdict)` the markdown
6
+ * summary uses, plus optional diff panels, and emit ONE offline `.html` string:
7
+ * reference | candidate | diff side by side per variant, the verdict findings
8
+ * in value order, every image inlined as a `data:` URI. Deterministic, no
9
+ * external assets. A leaf consumer — depends only on `@design-parity/core`.
10
+ */
11
+ export { renderHtmlReport } from "./render.js";
12
+ export type { DiffImage, ReportInput } from "./types.js";
13
+ export { pairVariants } from "./variants.js";
14
+ export type { Variant } from "./variants.js";
15
+ export { groupFindings, tokenDelta, SECTIONS } from "./findings.js";
16
+ export type { FindingSection, GroupedSection, TokenDelta } from "./findings.js";
17
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,YAAY,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,YAAY,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACpE,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * `@design-parity/report-html` — the per-run, self-contained HTML comparison
3
+ * page.
4
+ *
5
+ * Consume the same `(DesignReference, CandidateRender, Verdict)` the markdown
6
+ * summary uses, plus optional diff panels, and emit ONE offline `.html` string:
7
+ * reference | candidate | diff side by side per variant, the verdict findings
8
+ * in value order, every image inlined as a `data:` URI. Deterministic, no
9
+ * external assets. A leaf consumer — depends only on `@design-parity/core`.
10
+ */
11
+ export { renderHtmlReport } from "./render.js";
12
+ export { pairVariants } from "./variants.js";
13
+ export { groupFindings, tokenDelta, SECTIONS } from "./findings.js";
14
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE/C,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAE7C,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1,8 @@
1
+ import type { ReportInput } from "./types.js";
2
+ /**
3
+ * Render a parity run as one self-contained HTML document string.
4
+ *
5
+ * @see ReportInput
6
+ */
7
+ export declare function renderHtmlReport(input: ReportInput): string;
8
+ //# sourceMappingURL=render.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAa,WAAW,EAAE,MAAM,YAAY,CAAC;AA2MzD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CAmD3D"}
package/dist/render.js ADDED
@@ -0,0 +1,241 @@
1
+ /**
2
+ * `renderHtmlReport` — turn a parity run into one self-contained HTML page.
3
+ *
4
+ * The page inlines everything: CSS in a `<style>`, a small vanilla `<script>`
5
+ * for the overlay/opacity slider and theme/size switches, and every image as a
6
+ * `data:image/png;base64,…` URI. No external requests — it opens offline, from
7
+ * a file, as a CI artifact, or on GitHub Pages.
8
+ *
9
+ * Output is deterministic: same input → byte-identical string. No timestamps,
10
+ * no random ids, no `Date.now()`.
11
+ */
12
+ import { readFileSync } from "node:fs";
13
+ import { resolve } from "node:path";
14
+ import { cwd } from "node:process";
15
+ import { groupFindings, tokenDelta } from "./findings.js";
16
+ import { escapeHtml, pngDataUri } from "./html.js";
17
+ import { pairVariants } from "./variants.js";
18
+ const STATUS_LABEL = {
19
+ pass: "Pass",
20
+ warn: "Warn",
21
+ fail: "Fail",
22
+ };
23
+ const SEVERITY_LABEL = {
24
+ info: "info",
25
+ warn: "warn",
26
+ error: "error",
27
+ };
28
+ /** Inline an image from disk as a data URI, or `undefined` if absent/unreadable. */
29
+ function inlineFromDisk(root, img) {
30
+ if (!img)
31
+ return undefined;
32
+ // A source may already hand us a `data:` URI; pass it through untouched.
33
+ if (img.uri.startsWith("data:"))
34
+ return img.uri;
35
+ const bytes = readFileSync(resolve(root, img.uri));
36
+ return pngDataUri(bytes);
37
+ }
38
+ function panelMarkup(panel, role) {
39
+ const inner = panel.src
40
+ ? `<img class="panel-img" data-role="${role}" src="${panel.src}" alt="${escapeHtml(panel.label)}" />`
41
+ : `<div class="panel-empty">no image</div>`;
42
+ return `<figure class="panel" data-role="${role}">
43
+ <figcaption>${escapeHtml(panel.label)}</figcaption>
44
+ <div class="panel-body">${inner}</div>
45
+ </figure>`;
46
+ }
47
+ function variantMarkup(variant, index, refSrc, candSrc, diffSrc) {
48
+ const meta = [variant.state, variant.theme, variant.size]
49
+ .filter(Boolean)
50
+ .map((v) => `<span class="chip">${escapeHtml(String(v))}</span>`)
51
+ .join(" ");
52
+ const ref = { label: "Reference", src: refSrc };
53
+ const cand = { label: "Candidate", src: candSrc };
54
+ const diff = { label: "Diff", src: diffSrc };
55
+ // The overlay stacks reference under candidate; the slider sets candidate
56
+ // opacity so you can wipe between them. Only meaningful when both exist.
57
+ const overlay = refSrc && candSrc
58
+ ? `<div class="overlay">
59
+ <div class="overlay-stack">
60
+ <img class="overlay-ref" src="${refSrc}" alt="reference" />
61
+ <img class="overlay-cand" src="${candSrc}" alt="candidate" data-overlay="${index}" />
62
+ </div>
63
+ <label class="overlay-control">
64
+ <span>reference</span>
65
+ <input type="range" min="0" max="100" value="100" data-slider="${index}" />
66
+ <span>candidate</span>
67
+ </label>
68
+ </div>`
69
+ : "";
70
+ return `<section class="variant" data-variant="${escapeHtml(variant.key)}">
71
+ <header class="variant-head">
72
+ <h3>${escapeHtml(variant.key)}</h3>
73
+ <div class="variant-meta">${meta}</div>
74
+ </header>
75
+ <div class="triptych">
76
+ ${panelMarkup(ref, "reference")}
77
+ ${panelMarkup(cand, "candidate")}
78
+ ${panelMarkup(diff, "diff")}
79
+ </div>
80
+ ${overlay}
81
+ </section>`;
82
+ }
83
+ function findingsMarkup(verdict) {
84
+ const sections = groupFindings(verdict.findings);
85
+ if (sections.length === 0) {
86
+ return `<p class="no-findings">No parity findings — candidate matches the reference spec.</p>`;
87
+ }
88
+ return sections
89
+ .map((section) => {
90
+ const items = section.findings
91
+ .map((f) => {
92
+ const delta = tokenDelta(f);
93
+ const deltaMarkup = delta && (delta.expected !== undefined || delta.actual !== undefined)
94
+ ? `<div class="delta">
95
+ ${delta.token ? `<code>${escapeHtml(delta.token)}</code>` : ""}
96
+ <span class="expected">expected ${escapeHtml(delta.expected ?? "—")}</span>
97
+ <span class="actual">actual ${escapeHtml(delta.actual ?? "—")}</span>
98
+ </div>`
99
+ : "";
100
+ return `<li class="finding sev-${f.severity}">
101
+ <span class="sev-tag">${SEVERITY_LABEL[f.severity]}</span>
102
+ <div class="finding-body">
103
+ <span class="finding-msg">${escapeHtml(f.message)}</span>
104
+ ${deltaMarkup}
105
+ </div>
106
+ </li>`;
107
+ })
108
+ .join("\n");
109
+ return `<div class="finding-section">
110
+ <h3>${escapeHtml(section.title)}</h3>
111
+ <ul class="finding-list">${items}</ul>
112
+ </div>`;
113
+ })
114
+ .join("\n");
115
+ }
116
+ function scoresMarkup(verdict) {
117
+ const scores = verdict.visualScores;
118
+ if (!scores || Object.keys(scores).length === 0)
119
+ return "";
120
+ const rows = Object.entries(scores)
121
+ .map(([key, score]) => `<li><code>${escapeHtml(key)}</code> ${score === 0
122
+ ? "<span class=\"match\">match</span>"
123
+ : `<span class="differ">${(score * 100).toFixed(1)}% differ</span>`}</li>`)
124
+ .join("\n");
125
+ return `<div class="finding-section">
126
+ <h3>Visual scores</h3>
127
+ <ul class="score-list">${rows}</ul>
128
+ </div>`;
129
+ }
130
+ const STYLE = `:root{color-scheme:light dark}
131
+ *{box-sizing:border-box}
132
+ body{margin:0;font:14px/1.5 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;background:#0f0f14;color:#e7e7ef}
133
+ a{color:inherit}
134
+ header.page{padding:24px 28px;border-bottom:1px solid #26262f}
135
+ header.page h1{margin:0 0 6px;font-size:18px}
136
+ .subtitle{color:#9a9ab0;font-size:13px}
137
+ .status{display:inline-block;margin-left:10px;padding:2px 10px;border-radius:999px;font-weight:600;font-size:12px;vertical-align:middle}
138
+ .status-pass{background:#16351f;color:#7ee29a}
139
+ .status-warn{background:#3a3115;color:#e8c66b}
140
+ .status-fail{background:#3a1820;color:#f08a9c}
141
+ main{padding:20px 28px;display:grid;gap:24px;max-width:1200px}
142
+ .variant{border:1px solid #26262f;border-radius:10px;padding:16px;background:#15151c}
143
+ .variant-head{display:flex;align-items:baseline;gap:12px;flex-wrap:wrap;margin-bottom:12px}
144
+ .variant-head h3{margin:0;font-size:14px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}
145
+ .chip{display:inline-block;padding:1px 8px;border-radius:6px;background:#222230;color:#b8b8cc;font-size:11px;margin-left:4px}
146
+ .triptych{display:grid;grid-template-columns:repeat(3,1fr);gap:12px}
147
+ .panel{margin:0;border:1px solid #26262f;border-radius:8px;overflow:hidden;background:#0c0c11}
148
+ .panel figcaption{padding:6px 10px;font-size:11px;letter-spacing:.04em;text-transform:uppercase;color:#9a9ab0;border-bottom:1px solid #26262f}
149
+ .panel-body{display:flex;align-items:center;justify-content:center;min-height:64px;padding:10px}
150
+ .panel-img{max-width:100%;height:auto;image-rendering:pixelated}
151
+ .panel-empty{color:#666;font-size:12px;padding:20px}
152
+ .overlay{margin-top:14px}
153
+ .overlay-stack{position:relative;display:inline-block;border:1px solid #26262f;border-radius:8px;overflow:hidden;background:#0c0c11}
154
+ .overlay-stack img{display:block;max-width:100%}
155
+ .overlay-cand{position:absolute;inset:0;width:100%;height:100%}
156
+ .overlay-control{display:flex;align-items:center;gap:10px;margin-top:8px;font-size:12px;color:#9a9ab0}
157
+ .overlay-control input{flex:1;max-width:280px}
158
+ .findings{border:1px solid #26262f;border-radius:10px;padding:16px;background:#15151c}
159
+ .finding-section+.finding-section{margin-top:16px}
160
+ .finding-section h3{margin:0 0 8px;font-size:13px;color:#c9c9dd}
161
+ .finding-list,.score-list{list-style:none;margin:0;padding:0;display:grid;gap:6px}
162
+ .finding{display:flex;gap:10px;align-items:flex-start;padding:8px 10px;border-radius:8px;background:#0f0f15;border:1px solid #222230}
163
+ .sev-tag{flex:none;font-size:10px;text-transform:uppercase;letter-spacing:.05em;padding:2px 7px;border-radius:999px;font-weight:700}
164
+ .sev-info .sev-tag{background:#16283a;color:#7db4e8}
165
+ .sev-warn .sev-tag{background:#3a3115;color:#e8c66b}
166
+ .sev-error .sev-tag{background:#3a1820;color:#f08a9c}
167
+ .finding-body{display:flex;flex-direction:column;gap:4px}
168
+ .delta{display:flex;gap:8px;flex-wrap:wrap;font-size:12px;color:#9a9ab0}
169
+ .delta code{background:#222230;padding:1px 6px;border-radius:5px}
170
+ .delta .expected{color:#7ee29a}
171
+ .delta .actual{color:#f08a9c}
172
+ .score-list code{background:#222230;padding:1px 6px;border-radius:5px}
173
+ .match{color:#7ee29a}
174
+ .differ{color:#e8c66b}
175
+ .no-findings{color:#7ee29a;margin:0}
176
+ @media (max-width:760px){.triptych{grid-template-columns:1fr}}`;
177
+ const SCRIPT = `(function(){
178
+ var sliders=document.querySelectorAll('input[data-slider]');
179
+ for(var i=0;i<sliders.length;i++){
180
+ (function(slider){
181
+ var id=slider.getAttribute('data-slider');
182
+ var cand=document.querySelector('img[data-overlay="'+id+'"]');
183
+ function apply(){ if(cand){ cand.style.opacity=(slider.value/100).toString(); } }
184
+ slider.addEventListener('input',apply);
185
+ apply();
186
+ })(sliders[i]);
187
+ }
188
+ })();`;
189
+ /**
190
+ * Render a parity run as one self-contained HTML document string.
191
+ *
192
+ * @see ReportInput
193
+ */
194
+ export function renderHtmlReport(input) {
195
+ const { reference, candidate, verdict } = input;
196
+ const root = input.repoRoot ?? cwd();
197
+ const diffByKey = new Map();
198
+ for (const d of input.diffImages ?? [])
199
+ diffByKey.set(d.key, d);
200
+ const variants = pairVariants(reference.referenceImages, candidate.images);
201
+ const variantsHtml = variants
202
+ .map((variant, index) => {
203
+ const refSrc = inlineFromDisk(root, variant.reference);
204
+ const candSrc = inlineFromDisk(root, variant.candidate);
205
+ const diff = diffByKey.get(variant.key);
206
+ const diffSrc = diff ? pngDataUri(diff.png) : undefined;
207
+ return variantMarkup(variant, index, refSrc, candSrc, diffSrc);
208
+ })
209
+ .join("\n");
210
+ const hasVariants = variants.length > 0;
211
+ const variantsSection = hasVariants
212
+ ? variantsHtml
213
+ : `<section class="variant"><p class="panel-empty">No images for this verdict — findings only.</p></section>`;
214
+ const status = verdict.status;
215
+ const title = `Parity report: ${escapeHtml(verdict.componentId)}`;
216
+ return `<!doctype html>
217
+ <html lang="en">
218
+ <head>
219
+ <meta charset="utf-8" />
220
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
221
+ <title>${title}</title>
222
+ <style>${STYLE}</style>
223
+ </head>
224
+ <body>
225
+ <header class="page">
226
+ <h1>${escapeHtml(verdict.componentId)}<span class="status status-${status}">${STATUS_LABEL[status]}</span></h1>
227
+ <div class="subtitle">${escapeHtml(reference.source)} reference vs candidate render</div>
228
+ </header>
229
+ <main>
230
+ ${variantsSection}
231
+ <section class="findings">
232
+ ${findingsMarkup(verdict)}
233
+ ${scoresMarkup(verdict)}
234
+ </section>
235
+ </main>
236
+ <script>${SCRIPT}</script>
237
+ </body>
238
+ </html>
239
+ `;
240
+ }
241
+ //# sourceMappingURL=render.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.js","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAInC,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAEnD,OAAO,EAAE,YAAY,EAAgB,MAAM,eAAe,CAAC;AAE3D,MAAM,YAAY,GAAG;IACnB,IAAI,EAAE,MAAM;IACZ,IAAI,EAAE,MAAM;IACZ,IAAI,EAAE,MAAM;CACJ,CAAC;AAEX,MAAM,cAAc,GAAG;IACrB,IAAI,EAAE,MAAM;IACZ,IAAI,EAAE,MAAM;IACZ,KAAK,EAAE,OAAO;CACN,CAAC;AAEX,oFAAoF;AACpF,SAAS,cAAc,CAAC,IAAY,EAAE,GAAsB;IAC1D,IAAI,CAAC,GAAG;QAAE,OAAO,SAAS,CAAC;IAC3B,yEAAyE;IACzE,IAAI,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,OAAO,CAAC;QAAE,OAAO,GAAG,CAAC,GAAG,CAAC;IAChD,MAAM,KAAK,GAAG,YAAY,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;IACnD,OAAO,UAAU,CAAC,KAAK,CAAC,CAAC;AAC3B,CAAC;AASD,SAAS,WAAW,CAAC,KAAY,EAAE,IAAY;IAC7C,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG;QACrB,CAAC,CAAC,qCAAqC,IAAI,UAAU,KAAK,CAAC,GAAG,UAAU,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM;QACrG,CAAC,CAAC,yCAAyC,CAAC;IAC9C,OAAO,oCAAoC,IAAI;0BACvB,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC;sCACX,KAAK;oBACvB,CAAC;AACrB,CAAC;AAED,SAAS,aAAa,CACpB,OAAgB,EAChB,KAAa,EACb,MAA0B,EAC1B,OAA2B,EAC3B,OAA2B;IAE3B,MAAM,IAAI,GAAG,CAAC,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC;SACtD,MAAM,CAAC,OAAO,CAAC;SACf,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,sBAAsB,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;SAChE,IAAI,CAAC,GAAG,CAAC,CAAC;IAEb,MAAM,GAAG,GAAU,EAAE,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC;IACvD,MAAM,IAAI,GAAU,EAAE,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC;IACzD,MAAM,IAAI,GAAU,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC;IAEpD,0EAA0E;IAC1E,yEAAyE;IACzE,MAAM,OAAO,GACX,MAAM,IAAI,OAAO;QACf,CAAC,CAAC;;6CAEqC,MAAM;8CACL,OAAO,mCAAmC,KAAK;;;;8EAIf,KAAK;;;gBAGnE;QACV,CAAC,CAAC,EAAE,CAAC;IAET,OAAO,0CAA0C,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC;;oBAEtD,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC;0CACD,IAAI;;;gBAG9B,WAAW,CAAC,GAAG,EAAE,WAAW,CAAC;gBAC7B,WAAW,CAAC,IAAI,EAAE,WAAW,CAAC;gBAC9B,WAAW,CAAC,IAAI,EAAE,MAAM,CAAC;;cAE3B,OAAO;qBACA,CAAC;AACtB,CAAC;AAED,SAAS,cAAc,CAAC,OAAgB;IACtC,MAAM,QAAQ,GAAG,aAAa,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACjD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,uFAAuF,CAAC;IACjG,CAAC;IACD,OAAO,QAAQ;SACZ,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE;QACf,MAAM,KAAK,GAAG,OAAO,CAAC,QAAQ;aAC3B,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;YACT,MAAM,KAAK,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC;YAC5B,MAAM,WAAW,GACf,KAAK,IAAI,CAAC,KAAK,CAAC,QAAQ,KAAK,SAAS,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS,CAAC;gBACnE,CAAC,CAAC;qBACK,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE;qDAC5B,UAAU,CAAC,KAAK,CAAC,QAAQ,IAAI,GAAG,CAAC;iDACrC,UAAU,CAAC,KAAK,CAAC,MAAM,IAAI,GAAG,CAAC;wBACxD;gBACV,CAAC,CAAC,EAAE,CAAC;YACT,OAAO,0BAA0B,CAAC,CAAC,QAAQ;4CACT,cAAc,CAAC,CAAC,CAAC,QAAQ,CAAC;;kDAEpB,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC;wBAC/C,WAAW;;wBAEX,CAAC;QACjB,CAAC,CAAC;aACD,IAAI,CAAC,IAAI,CAAC,CAAC;QACd,OAAO;sBACS,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC;2CACJ,KAAK;qBAC3B,CAAC;IAClB,CAAC,CAAC;SACD,IAAI,CAAC,IAAI,CAAC,CAAC;AAChB,CAAC;AAED,SAAS,YAAY,CAAC,OAAgB;IACpC,MAAM,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IACpC,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAC3D,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC;SAChC,GAAG,CACF,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CACf,aAAa,UAAU,CAAC,GAAG,CAAC,WAC1B,KAAK,KAAK,CAAC;QACT,CAAC,CAAC,oCAAoC;QACtC,CAAC,CAAC,wBAAwB,CAAC,KAAK,GAAG,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,iBACtD,OAAO,CACV;SACA,IAAI,CAAC,IAAI,CAAC,CAAC;IACd,OAAO;;qCAE4B,IAAI;iBACxB,CAAC;AAClB,CAAC;AAED,MAAM,KAAK,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;+DA8CiD,CAAC;AAEhE,MAAM,MAAM,GAAG;;;;;;;;;;;MAWT,CAAC;AAEP;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAkB;IACjD,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,KAAK,CAAC;IAChD,MAAM,IAAI,GAAG,KAAK,CAAC,QAAQ,IAAI,GAAG,EAAE,CAAC;IAErC,MAAM,SAAS,GAAG,IAAI,GAAG,EAAqB,CAAC;IAC/C,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,UAAU,IAAI,EAAE;QAAE,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;IAEhE,MAAM,QAAQ,GAAG,YAAY,CAAC,SAAS,CAAC,eAAe,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC;IAE3E,MAAM,YAAY,GAAG,QAAQ;SAC1B,GAAG,CAAC,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE;QACtB,MAAM,MAAM,GAAG,cAAc,CAAC,IAAI,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;QACvD,MAAM,OAAO,GAAG,cAAc,CAAC,IAAI,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;QACxD,MAAM,IAAI,GAAG,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACxC,MAAM,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACxD,OAAO,aAAa,CAAC,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;IACjE,CAAC,CAAC;SACD,IAAI,CAAC,IAAI,CAAC,CAAC;IAEd,MAAM,WAAW,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;IACxC,MAAM,eAAe,GAAG,WAAW;QACjC,CAAC,CAAC,YAAY;QACd,CAAC,CAAC,2GAA2G,CAAC;IAEhH,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAC9B,MAAM,KAAK,GAAG,kBAAkB,UAAU,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC;IAElE,OAAO;;;;;SAKA,KAAK;SACL,KAAK;;;;QAIN,UAAU,CAAC,OAAO,CAAC,WAAW,CAAC,8BAA8B,MAAM,KAAK,YAAY,CAAC,MAAM,CAAC;0BAC1E,UAAU,CAAC,SAAS,CAAC,MAAM,CAAC;;;EAGpD,eAAe;;EAEf,cAAc,CAAC,OAAO,CAAC;EACvB,YAAY,CAAC,OAAO,CAAC;;;UAGb,MAAM;;;CAGf,CAAC;AACF,CAAC"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Local contracts for the HTML report.
3
+ *
4
+ * This package is a leaf consumer: it depends only on `@design-parity/core`.
5
+ * It deliberately does *not* import `@design-parity/diff` — instead the
6
+ * triptych/diff panels arrive as a generic {@link DiffImage}, so the diff
7
+ * engine (or any producer) can hand us heatmaps without this package taking a
8
+ * dependency on it.
9
+ */
10
+ import type { CandidateRender, DesignReference, Verdict } from "@design-parity/core";
11
+ /**
12
+ * A diff panel to inline, keyed by the same `state/theme/size` key the diff
13
+ * engine uses for its triptychs/visual scores. `png` is the raw PNG bytes.
14
+ */
15
+ export interface DiffImage {
16
+ key: string;
17
+ png: Uint8Array;
18
+ }
19
+ /** Everything {@link renderHtmlReport} needs to emit one offline page. */
20
+ export interface ReportInput {
21
+ reference: DesignReference;
22
+ candidate: CandidateRender;
23
+ verdict: Verdict;
24
+ /** Per-variant diff panels (e.g. the pixelmatch heatmaps). Optional. */
25
+ diffImages?: DiffImage[];
26
+ /**
27
+ * Root the reference/candidate `Image.uri`s resolve against (mirrors the
28
+ * diff engine). Defaults to `process.cwd()`.
29
+ */
30
+ repoRoot?: string;
31
+ }
32
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EACV,eAAe,EACf,eAAe,EACf,OAAO,EACR,MAAM,qBAAqB,CAAC;AAE7B;;;GAGG;AACH,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,UAAU,CAAC;CACjB;AAED,0EAA0E;AAC1E,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,eAAe,CAAC;IAC3B,SAAS,EAAE,eAAe,CAAC;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,wEAAwE;IACxE,UAAU,CAAC,EAAE,SAAS,EAAE,CAAC;IACzB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Pair reference and candidate images into per-variant rows, tolerant of a side
3
+ * that omits or labels `size` differently (same rule the diff engine uses:
4
+ * canonicalize via `normalizeSize`, and let an unknown/missing size pair with
5
+ * anything).
6
+ */
7
+ import { type Image } from "@design-parity/core";
8
+ /** One reference | candidate | diff row. Any side may be missing. */
9
+ export interface Variant {
10
+ /** `state/theme/size` key (raw size), stable for the DOM + diff lookup. */
11
+ key: string;
12
+ state: string;
13
+ theme?: string;
14
+ size?: string;
15
+ reference?: Image;
16
+ candidate?: Image;
17
+ }
18
+ /**
19
+ * Pair references with candidates. Output order is stable: references in their
20
+ * declared order first (each with its best candidate match), then any
21
+ * candidate-only variants in their declared order.
22
+ */
23
+ export declare function pairVariants(references: readonly Image[], candidates: readonly Image[]): Variant[];
24
+ //# sourceMappingURL=variants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"variants.d.ts","sourceRoot":"","sources":["../src/variants.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAiB,KAAK,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAEhE,qEAAqE;AACrE,MAAM,WAAW,OAAO;IACtB,2EAA2E;IAC3E,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,KAAK,CAAC;IAClB,SAAS,CAAC,EAAE,KAAK,CAAC;CACnB;AA4BD;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,UAAU,EAAE,SAAS,KAAK,EAAE,EAC5B,UAAU,EAAE,SAAS,KAAK,EAAE,GAC3B,OAAO,EAAE,CA2CX"}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Pair reference and candidate images into per-variant rows, tolerant of a side
3
+ * that omits or labels `size` differently (same rule the diff engine uses:
4
+ * canonicalize via `normalizeSize`, and let an unknown/missing size pair with
5
+ * anything).
6
+ */
7
+ import { normalizeSize } from "@design-parity/core";
8
+ /** Human/diff key — raw size, matches `@design-parity/diff`'s `imageKey`. */
9
+ function imageKey(img) {
10
+ return [img.state, img.theme, img.size].filter(Boolean).join("/");
11
+ }
12
+ /** Canonical size token for pairing (falls back to the lowercased label). */
13
+ function sizeToken(img) {
14
+ return normalizeSize(img.size) ?? img.size?.toLowerCase();
15
+ }
16
+ /** Pairing key with the size normalized. */
17
+ function pairKey(img) {
18
+ return [img.state, img.theme, sizeToken(img)].filter(Boolean).join("/");
19
+ }
20
+ /** Looser key ignoring size — used when a side omits/uses an unknown size. */
21
+ function looseKey(img) {
22
+ return [img.state, img.theme].filter(Boolean).join("/");
23
+ }
24
+ function sizeCompatible(a, b) {
25
+ const ca = normalizeSize(a.size);
26
+ const cb = normalizeSize(b.size);
27
+ return ca === undefined || cb === undefined || ca === cb;
28
+ }
29
+ /**
30
+ * Pair references with candidates. Output order is stable: references in their
31
+ * declared order first (each with its best candidate match), then any
32
+ * candidate-only variants in their declared order.
33
+ */
34
+ export function pairVariants(references, candidates) {
35
+ const used = new Set();
36
+ const findCandidate = (ref) => {
37
+ // Prefer an exact normalized-key match, then a size-tolerant loose match.
38
+ const exact = candidates.findIndex((c, i) => !used.has(i) && pairKey(c) === pairKey(ref));
39
+ if (exact !== -1)
40
+ return exact;
41
+ return candidates.findIndex((c, i) => !used.has(i) && looseKey(c) === looseKey(ref) && sizeCompatible(c, ref));
42
+ };
43
+ const variants = [];
44
+ for (const ref of references) {
45
+ const ci = findCandidate(ref);
46
+ const candidate = ci === -1 ? undefined : candidates[ci];
47
+ if (ci !== -1)
48
+ used.add(ci);
49
+ variants.push({
50
+ key: imageKey(ref),
51
+ state: ref.state,
52
+ theme: ref.theme,
53
+ size: ref.size,
54
+ reference: ref,
55
+ candidate,
56
+ });
57
+ }
58
+ candidates.forEach((c, i) => {
59
+ if (used.has(i))
60
+ return;
61
+ variants.push({
62
+ key: imageKey(c),
63
+ state: c.state,
64
+ theme: c.theme,
65
+ size: c.size,
66
+ candidate: c,
67
+ });
68
+ });
69
+ return variants;
70
+ }
71
+ //# sourceMappingURL=variants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"variants.js","sourceRoot":"","sources":["../src/variants.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@design-parity/report-html",
3
+ "version": "0.1.0",
4
+ "description": "Self-contained HTML comparison page per parity run: reference | candidate | diff side by side with the verdict findings, inlined to one offline .html file (data-URI PNGs + inline CSS/JS, no external assets).",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": "./dist/index.js"
11
+ },
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "bin": {
16
+ "design-parity-report": "./dist/cli/report.js"
17
+ },
18
+ "scripts": {
19
+ "build": "tsc --build",
20
+ "test": "vitest run"
21
+ },
22
+ "dependencies": {
23
+ "@design-parity/core": "^0.1.0"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/yschimke/design-parity.git",
28
+ "directory": "packages/report-html"
29
+ },
30
+ "homepage": "https://github.com/yschimke/design-parity/tree/main/packages/report-html#readme",
31
+ "bugs": "https://github.com/yschimke/design-parity/issues"
32
+ }