@analitikisler/ai-tag-debugger 0.18.0 → 0.20.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/CHANGELOG.md +16 -0
- package/README.md +5 -5
- package/README.tr.md +5 -5
- package/package.json +3 -3
- package/src/brand.js +17 -11
- package/src/browser.js +44 -8
- package/src/html/css.js +186 -165
- package/src/html/lib.js +15 -4
- package/src/html/shell.js +7 -7
- package/src/i18n.js +5 -5
- package/src/plan-html.js +9 -8
- package/src/report.js +1 -1
- package/src/whitelabel.js +48 -15
package/src/html/lib.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// renderer imports its icon, logo and id helpers from here, so the sections cannot drift apart while
|
|
4
4
|
// both exist; this module imports neither of them. CONTRACT-IMPL.md in the build notes lists what each helper is for.
|
|
5
5
|
import { hitMatchesEvent } from "../checks.js";
|
|
6
|
-
import { LOGOS } from "../brand.js";
|
|
6
|
+
import { LOGOS, LOGO_ART } from "../brand.js";
|
|
7
7
|
import { OWNERS, ownerOf } from "../verify.js";
|
|
8
8
|
import { escapeHtml, platformLabel, safeKey, sortPlatforms } from "../report-shared.js";
|
|
9
9
|
|
|
@@ -52,9 +52,10 @@ const use = (id, size = 16, cls = "") => `<svg class="ic${cls ? ` ${cls}` : ""}"
|
|
|
52
52
|
|
|
53
53
|
/** The sprite with only the symbols the page references, so a report never carries artwork it does not show. */
|
|
54
54
|
function sprite(html) {
|
|
55
|
-
const used = new Set([...html.matchAll(/href="#((?:sev|ui)-[a-z]+|p-[a-z0-9_]+)"/g)].map((x) => x[1]));
|
|
55
|
+
const used = new Set([...html.matchAll(/href="#((?:sev|ui)-[a-z]+|p-[a-z0-9_]+|logo-[a-z-]+)"/g)].map((x) => x[1]));
|
|
56
56
|
const symbols = [...used].sort().map((id) => {
|
|
57
57
|
if (id.startsWith("p-")) return `<symbol id="${id}" viewBox="0 0 24 24">${PLATFORM_ART[id.slice(2)]}</symbol>`;
|
|
58
|
+
if (id.startsWith("logo-")) return `<symbol id="${id}" viewBox="${LOGO_ART[id.slice(5)].viewBox}">${LOGO_ART[id.slice(5)].art}</symbol>`;
|
|
58
59
|
return `<symbol id="${id}" ${STROKE}>${UI_SYMBOLS[id]}</symbol>`;
|
|
59
60
|
});
|
|
60
61
|
return `<svg hidden style="display:none" aria-hidden="true" xmlns="http://www.w3.org/2000/svg">${symbols.join("")}</svg>`;
|
|
@@ -84,8 +85,18 @@ const hostPath = (url) => {
|
|
|
84
85
|
|
|
85
86
|
/** A severity badge: glyph and word, never colour alone. A badge without a severity is a plain grey chip. */
|
|
86
87
|
const badge = (sev, text) => (UI_SYMBOLS[`sev-${sev}`] ? `<span class="badge sev-${sev}">${use(`sev-${sev}`, 14)}${escapeHtml(text)}</span>` : `<span class="badge">${escapeHtml(text)}</span>`);
|
|
87
|
-
/**
|
|
88
|
-
|
|
88
|
+
/**
|
|
89
|
+
* A logo at a given height. The brand's own logos (LOGO_ART) are drawn from the sprite, so their ink is the text colour and
|
|
90
|
+
* only the point is red, as brand/ai/BRAND.md §4 asks of a product's UI. Anything else in LOGOS (the favicon tiles, which
|
|
91
|
+
* carry their own ground) comes as its light and dark images, and the CSS shows the one that fits the colour scheme.
|
|
92
|
+
*/
|
|
93
|
+
export const logo = (key, alt, height, cls) => {
|
|
94
|
+
const art = LOGO_ART[key];
|
|
95
|
+
if (!art) return `<span class="logo ${cls}"><img class="l-light" src="${LOGOS[`${key}-light`]}" alt="${alt}" height="${height}"><img class="l-dark" src="${LOGOS[`${key}-dark`]}" alt="${alt}" height="${height}"></span>`;
|
|
96
|
+
const [, , w, h] = art.viewBox.split(" ").map(Number);
|
|
97
|
+
const named = alt ? `role="img" aria-label="${escapeHtml(alt)}"` : 'aria-hidden="true"';
|
|
98
|
+
return `<svg class="logo ${cls}" width="${+((height * w) / h).toFixed(1)}" height="${height}" ${named}><use href="#logo-${key}"/></svg>`;
|
|
99
|
+
};
|
|
89
100
|
|
|
90
101
|
/** The journey and step a finding's place points at. Steps are named by label; when labels repeat, prefer the step where the finding's event fired. */
|
|
91
102
|
function whereTarget(runs, w, f) {
|
package/src/html/shell.js
CHANGED
|
@@ -125,9 +125,9 @@ function topBar(ctx) {
|
|
|
125
125
|
const t = m.html.shell;
|
|
126
126
|
const brandName = brand?.name ? escapeHtml(brand.name) : "";
|
|
127
127
|
const brandLogo = brand?.logo ? `<img src="${brand.logo}" alt="${brandName}" height="26">` : "";
|
|
128
|
-
const product = whiteLabel ? (brandLogo || `<span class="brand-name">${brandName}</span>`) : `${logo("atd-lockup", "
|
|
129
|
-
const atdByline = whiteLabel ? `<a href="${ATD_REPO}" class="byline" rel="noopener noreferrer"><span>${m.ui.by}</span>${logo("atd-lockup", "
|
|
130
|
-
const aiByline = branding ? `<a href="https://analitikisler.com" class="byline" rel="noopener noreferrer"><span>${m.ui.by}</span>${logo("
|
|
128
|
+
const product = whiteLabel ? (brandLogo || `<span class="brand-name">${brandName}</span>`) : `${logo("atd-lockup", "ai tag debugger", 22, "l-lockup")}${logo("mark", "ai tag debugger", 24, "l-mark")}`;
|
|
129
|
+
const atdByline = whiteLabel ? `<a href="${ATD_REPO}" class="byline" rel="noopener noreferrer"><span>${m.ui.by}</span>${logo("atd-lockup", "ai tag debugger", 16, "logo-atd")}</a>` : "";
|
|
130
|
+
const aiByline = branding ? `<a href="https://analitikisler.com" class="byline" rel="noopener noreferrer"><span>${m.ui.by}</span>${logo("master", "Analitik İşler", 16, "logo-h")}</a>` : "";
|
|
131
131
|
const word = m.verdict.status[metrics.verdict.status];
|
|
132
132
|
// 0–49 red, 50–89 amber, 90–100 green, as Lighthouse bands its scores; no score reads as a fail.
|
|
133
133
|
const band = tally.score === null || tally.score < 50 ? "fail" : tally.score < 90 ? "mid" : "pass";
|
|
@@ -139,11 +139,11 @@ function topBar(ctx) {
|
|
|
139
139
|
|
|
140
140
|
function footer(ctx) {
|
|
141
141
|
const { m, site, title, generatedAt, brand, branding, whiteLabel } = ctx;
|
|
142
|
-
const atd = `<a class="f-atd" href="${ATD_REPO}" rel="noopener noreferrer"
|
|
142
|
+
const atd = `<a class="f-atd" href="${ATD_REPO}" rel="noopener noreferrer">${logo("favicon", "", 16, "f-mark")} ai tag debugger</a>`;
|
|
143
143
|
const meta = `<span class="muted">${escapeHtml(site ?? title)} · ${escapeHtml(generatedAt)} · v${VERSION}</span>`;
|
|
144
|
-
if (branding) return `<footer class="footer"><a href="https://analitikisler.com" class="logo-link" aria-label="analitikisler.com" rel="noopener noreferrer">${logo("
|
|
144
|
+
if (branding) return `<footer class="footer"><a href="https://analitikisler.com" class="logo-link" aria-label="analitikisler.com" rel="noopener noreferrer">${logo("master", "Analitik İşler", 24, "logo-s")}</a><p class="xs">${m.ui.credit(atd)} ${meta}</p></footer>`;
|
|
145
145
|
const brandName = brand?.name ? escapeHtml(brand.name) : "";
|
|
146
|
-
return `<footer class="footer"><p class="xs">${whiteLabel ? `${brandName}${brandName ? " · " : ""}${m.ui.credit(atd)}` : "
|
|
146
|
+
return `<footer class="footer"><p class="xs">${whiteLabel ? `${brandName}${brandName ? " · " : ""}${m.ui.credit(atd)}` : "ai tag debugger"} ${meta}</p></footer>`;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
/** The expand / collapse pair under a section's h2, for every section but Summary that holds a details.card. */
|
|
@@ -172,7 +172,7 @@ export function renderHtml(report) {
|
|
|
172
172
|
<html lang="${lang}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
173
173
|
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src data:; script-src ${scriptCspSource()}">
|
|
174
174
|
<meta name="color-scheme" content="light dark">
|
|
175
|
-
<title>${escapeHtml(title)}</title><link rel="icon"${brand?.logo ? "" : ' type="image/
|
|
175
|
+
<title>${escapeHtml(title)}</title><link rel="icon"${brand?.logo ? "" : ' type="image/svg+xml"'} href="${brand?.logo ?? LOGOS["favicon-light"]}">
|
|
176
176
|
<style>${CSS}${brandCss(brand?.color)}</style></head><body>
|
|
177
177
|
<a class="skip" href="#main">${escapeHtml(t.skip)}</a>
|
|
178
178
|
${sprite(`${top}${nav}${sections}${foot}`)}
|
package/src/i18n.js
CHANGED
|
@@ -26,7 +26,7 @@ const en = {
|
|
|
26
26
|
name: "English",
|
|
27
27
|
severity: { broken: "Broken", risk: "Risk", warning: "Warning", info: "Info", ok: "OK" },
|
|
28
28
|
ui: {
|
|
29
|
-
title: (host) => `
|
|
29
|
+
title: (host) => `ai tag debugger report: ${host}`,
|
|
30
30
|
generated: (at) => `Generated ${at} by ai-tag-debugger · Analitik İşler`,
|
|
31
31
|
summary: "Summary",
|
|
32
32
|
eyebrow: "AI tracking audit",
|
|
@@ -64,7 +64,7 @@ const en = {
|
|
|
64
64
|
status: "Status",
|
|
65
65
|
vendor: "Vendor",
|
|
66
66
|
clickedThrough: "clicked through an overlapping element",
|
|
67
|
-
generatedBy: "Generated by
|
|
67
|
+
generatedBy: "Generated by ai tag debugger",
|
|
68
68
|
nothing: "None",
|
|
69
69
|
counts: (c) => `${c.broken} broken · ${c.risk} risk · ${c.warning} warning · ${c.info} info · ${c.ok} ok`,
|
|
70
70
|
accepted: "Accepted",
|
|
@@ -722,8 +722,8 @@ const tr = {
|
|
|
722
722
|
name: "Turkish",
|
|
723
723
|
severity: { broken: "Bozuk", risk: "Risk", warning: "Uyarı", info: "Bilgi", ok: "Sorunsuz" },
|
|
724
724
|
ui: {
|
|
725
|
-
title: (host) => `
|
|
726
|
-
generated: (at) => `${at} tarihinde ai
|
|
725
|
+
title: (host) => `ai tag debugger raporu: ${host}`,
|
|
726
|
+
generated: (at) => `${at} tarihinde ai tag debugger ile oluşturuldu · Analitik İşler`,
|
|
727
727
|
summary: "Özet",
|
|
728
728
|
eyebrow: "Yapay zekâ ile izleme denetimi",
|
|
729
729
|
paramsShort: "parametre",
|
|
@@ -760,7 +760,7 @@ const tr = {
|
|
|
760
760
|
status: "Durum",
|
|
761
761
|
vendor: "Sağlayıcı",
|
|
762
762
|
clickedThrough: "üstündeki bir öğeye rağmen tıklandı",
|
|
763
|
-
generatedBy: "
|
|
763
|
+
generatedBy: "ai tag debugger ile oluşturuldu",
|
|
764
764
|
nothing: "Yok",
|
|
765
765
|
counts: (c) => `${c.broken} bozuk · ${c.risk} risk · ${c.warning} uyarı · ${c.info} bilgi · ${c.ok} sorunsuz`,
|
|
766
766
|
accepted: "Kabul edildi",
|
package/src/plan-html.js
CHANGED
|
@@ -18,11 +18,12 @@ const PLAN_CSS = `
|
|
|
18
18
|
.top-end{margin-left:auto;display:flex;align-items:center;gap:var(--s2)}
|
|
19
19
|
.byline{display:inline-flex;align-items:center;gap:var(--s2);font-size:12px;color:var(--muted)}
|
|
20
20
|
.logo{display:inline-flex}
|
|
21
|
+
svg.logo{display:inline-block;flex:none;color:var(--fg-strong)}
|
|
21
22
|
.logo .l-dark{display:none}
|
|
22
23
|
html[data-theme="dark"] .logo .l-light{display:none}
|
|
23
24
|
html[data-theme="dark"] .logo .l-dark{display:inline}
|
|
24
25
|
@media (prefers-color-scheme:dark){html:not([data-theme="light"]) .logo .l-light{display:none}html:not([data-theme="light"]) .logo .l-dark{display:inline}}
|
|
25
|
-
.eyebrow{font:
|
|
26
|
+
.eyebrow{font:500 12px/1.4 var(--font-sans);color:var(--muted)}
|
|
26
27
|
.plan{max-width:1100px;margin:0 auto;padding:var(--s5) var(--s6) var(--s7)}
|
|
27
28
|
.plan-title{display:grid;gap:var(--s2);margin-bottom:var(--s5)}
|
|
28
29
|
.plan-title .chips{margin-top:var(--s1)}
|
|
@@ -38,8 +39,8 @@ html[data-theme="dark"] .logo .l-dark{display:inline}
|
|
|
38
39
|
.pl-trigger{font-size:12px;color:var(--muted);margin-bottom:10px}
|
|
39
40
|
.pl-trigger b{font-weight:500;color:var(--fg)}
|
|
40
41
|
.chip .plat{display:inline-flex;align-items:center;gap:5px}
|
|
41
|
-
.chip.on{--k:var(--accent);--k-fg:var(--accent-fg)
|
|
42
|
-
.chip.req{--k:var(--sev-warning);--k-fg:var(--sev-warning-fg)
|
|
42
|
+
.chip.on{--k:var(--accent);--k-fg:var(--accent-fg)}
|
|
43
|
+
.chip.req{--k:var(--sev-warning);--k-fg:var(--sev-warning-fg)}
|
|
43
44
|
.pl-card-body{padding:var(--s4)}
|
|
44
45
|
.tbl td,.tbl th{vertical-align:top}
|
|
45
46
|
/* a narrow table scrolls inside its .tscroll instead of breaking names mid-word */
|
|
@@ -99,12 +100,12 @@ export function renderPlanHtml(plan, { id = "", generatedAt = "", branding = tru
|
|
|
99
100
|
const events = plan.events ?? [];
|
|
100
101
|
const custom = events.filter((ev) => ev.custom).length;
|
|
101
102
|
const title = `Measurement plan${plan.site ? `: ${plan.site}` : ""}`;
|
|
102
|
-
const meta = [id && `Plan ${id}`, generatedAt, `${events.length} events (${custom} custom)`, `ai
|
|
103
|
+
const meta = [id && `Plan ${id}`, generatedAt, `${events.length} events (${custom} custom)`, `ai tag debugger ${VERSION}`].filter(Boolean).join(" · ");
|
|
103
104
|
const consent = plan.consent_mode ? flag("Consent mode on", "on") : flag("Consent mode off");
|
|
104
105
|
const footer = branding
|
|
105
|
-
? `<a href="https://analitikisler.com" class="logo-link" aria-label="analitikisler.com" rel="noopener noreferrer">${logo("
|
|
106
|
-
: `<p class="xs">
|
|
107
|
-
const body = `<header class="top"><span class="brand">${logo("atd-lockup", "
|
|
106
|
+
? `<a href="https://analitikisler.com" class="logo-link" aria-label="analitikisler.com" rel="noopener noreferrer">${logo("master", "Analitik İşler", 24, "logo-s")}</a><div class="f-links xs"><a href="https://analitikisler.com" rel="noopener noreferrer">analitikisler.com</a><a href="https://github.com/analitikisler/ai-tag-debugger" rel="noopener noreferrer">GitHub</a></div><p class="f-meta xs muted">v${VERSION}</p>`
|
|
107
|
+
: `<p class="xs">ai tag debugger</p><p class="f-meta xs muted">v${VERSION}</p>`;
|
|
108
|
+
const body = `<header class="top"><span class="brand">${logo("atd-lockup", "ai tag debugger", 22, "l-lockup")}${logo("mark", "ai tag debugger", 24, "l-mark")}</span><span class="top-site"><span class="eyebrow">Measurement plan</span>${plan.site ? `<b>${e(plan.site)}</b>` : ""}</span><div class="top-end">${branding ? `<a href="https://analitikisler.com" class="byline" rel="noopener noreferrer"><span>by</span>${logo("master", "Analitik İşler", 16, "logo-h")}</a>` : ""}</div></header>
|
|
108
109
|
<main class="plan">
|
|
109
110
|
<div class="plan-title"><div class="eyebrow">Measurement plan</div><h1>${e(title)}</h1><div class="muted xs">${e(meta)}</div><div class="chips">${consent}</div></div>
|
|
110
111
|
<section class="card"><div class="pl-card-h"><h2>Events</h2><p class="pl-card-d">What should be tracked, where it fires and which parameters it carries.</p></div>${events.length ? events.map(eventCard).join("") : `<p class="pl-empty muted xs">No events.</p>`}</section>
|
|
@@ -114,7 +115,7 @@ ${userPropertiesCard(plan.user_properties ?? [])}
|
|
|
114
115
|
return `<!doctype html>
|
|
115
116
|
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
116
117
|
<meta name="color-scheme" content="light dark">
|
|
117
|
-
<title>${e(title)}</title><link rel="icon" type="image/
|
|
118
|
+
<title>${e(title)}</title><link rel="icon" type="image/svg+xml" href="${LOGOS["favicon-light"]}">
|
|
118
119
|
<style>${TOKENS}\n${COMPONENTS}\n${PRODUCT}\n${PLAN_CSS}</style></head><body>
|
|
119
120
|
${sprite(body)}
|
|
120
121
|
${body}
|
package/src/report.js
CHANGED
|
@@ -441,7 +441,7 @@ export function renderMarkdown({ title, site = null, generatedAt, findings, expl
|
|
|
441
441
|
lines.push(...mdJourneys(m, runs, rows));
|
|
442
442
|
lines.push(...mdNotChecked(m, runs, rows, plan, consentScenarios));
|
|
443
443
|
lines.push(...mdInventory(m, runs));
|
|
444
|
-
// A report with its own name or logo carries no Analitik İşler credit; the
|
|
444
|
+
// A report with its own name or logo carries no Analitik İşler credit; the ai tag debugger one stays.
|
|
445
445
|
if (brand?.name || brand?.logo) lines.push("---", "", `_${brand.name ? `${brand.name} · ` : ""}${m.ui.generatedBy}_`, "");
|
|
446
446
|
else if (branding) lines.push("---", "", `_${m.ui.generatedBy} · [analitikisler.com](https://analitikisler.com)_`, "");
|
|
447
447
|
return lines.join("\n");
|
package/src/whitelabel.js
CHANGED
|
@@ -53,33 +53,66 @@ export const contrast = (a, b) => {
|
|
|
53
53
|
return (h + 0.05) / (l + 0.05);
|
|
54
54
|
};
|
|
55
55
|
const MIN_TEXT_CONTRAST = 4.5;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
const MIN_MARK_CONTRAST = 3;
|
|
57
|
+
// The grounds of visual-system.md §1 the accent is drawn on, in each theme: card (Tally's surface), page (bg), card-2
|
|
58
|
+
// (surface-2: hovered and selected rows, hovered controls) and well. Links sit on all four; the tint is added per colour.
|
|
59
|
+
const GROUNDS = {
|
|
60
|
+
light: ["#ffffff", "#f4f6fa", "#ecf1f8", "#e9eef5"],
|
|
61
|
+
dark: ["#0e1e34", "#071529", "#15263f", "#040e1f"],
|
|
62
|
+
};
|
|
63
|
+
const BLACK = [0, 0, 0];
|
|
64
|
+
const WHITE = [255, 255, 255];
|
|
65
|
+
// The dark tint is the brand colour laid on the navy card, as the default --accent-bg is.
|
|
66
|
+
const DARK_CARD_RGB = rgb(GROUNDS.dark[0]);
|
|
59
67
|
|
|
60
|
-
/**
|
|
61
|
-
|
|
68
|
+
/**
|
|
69
|
+
* The colour mixed step by step towards `toward` until it reads at `min`:1 on every ground; the input when it already
|
|
70
|
+
* does. Mixing towards black or white keeps the hue, so a brand colour only gets darker or lighter, never another colour.
|
|
71
|
+
*/
|
|
72
|
+
function readable(hex, toward, grounds, min) {
|
|
62
73
|
let c = hex;
|
|
63
|
-
|
|
74
|
+
while (grounds.some((g) => contrast(c, g) < min)) {
|
|
75
|
+
const next = mix(c, toward, 0.1);
|
|
76
|
+
if (next === c) break; // as far as rounding goes: black and white pass every ground long before this
|
|
77
|
+
c = next;
|
|
78
|
+
}
|
|
64
79
|
return c;
|
|
65
80
|
}
|
|
66
81
|
|
|
67
82
|
/**
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* reads on the
|
|
71
|
-
*
|
|
83
|
+
* The three accent tokens for a brand colour, per theme. The tint (--accent-bg) is the brand colour on the card: 10% on
|
|
84
|
+
* white in light, 25% on the navy in dark. The link colour (--accent-fg) is darkened (light) or lightened (dark) until it
|
|
85
|
+
* reads at 4.5:1 on the card, the page, card-2, the well and that tint; the colour for marks (--accent: the active tab
|
|
86
|
+
* and rail rule, focus rings, pressed filter edges) until it reaches 3:1 on the same grounds. A colour that already
|
|
87
|
+
* passes is left as it is.
|
|
88
|
+
* @returns {{ light: { accent: string, fg: string, bg: string }, dark: { accent: string, fg: string, bg: string } }}
|
|
89
|
+
*/
|
|
90
|
+
export function brandTokens(color) {
|
|
91
|
+
const lightBg = mix(color, WHITE, 0.9);
|
|
92
|
+
const darkBg = mix(color, DARK_CARD_RGB, 0.75);
|
|
93
|
+
const light = [...GROUNDS.light, lightBg];
|
|
94
|
+
const dark = [...GROUNDS.dark, darkBg];
|
|
95
|
+
return {
|
|
96
|
+
light: { accent: readable(color, BLACK, light, MIN_MARK_CONTRAST), fg: readable(mix(color, BLACK, 0.2), BLACK, light, MIN_TEXT_CONTRAST), bg: lightBg },
|
|
97
|
+
dark: { accent: readable(mix(color, WHITE, 0.2), WHITE, dark, MIN_MARK_CONTRAST), fg: readable(mix(color, WHITE, 0.5), WHITE, dark, MIN_TEXT_CONTRAST), bg: darkBg },
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* CSS that recolors the report's accent: the active tab rule, focus rings, links and the step plates (visual-system.md §9),
|
|
103
|
+
* from brandTokens(), so a pale or a very dark brand colour never gives unreadable links or invisible focus rings in either
|
|
104
|
+
* theme. The dark set is written under both dark selectors the report uses, so a forced light theme on a dark OS keeps
|
|
105
|
+
* the light accent.
|
|
72
106
|
*/
|
|
73
107
|
export function brandCss(color) {
|
|
74
108
|
if (!color) return "";
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
return `:root{${light}}@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){${dark}}}:root[data-theme="dark"]{${dark}}`;
|
|
109
|
+
const { light, dark } = brandTokens(color);
|
|
110
|
+
const set = (t) => `--accent:${t.accent};--accent-fg:${t.fg};--accent-bg:${t.bg}`;
|
|
111
|
+
return `:root{${set(light)}}@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){${set(dark)}}}:root[data-theme="dark"]{${set(dark)}}`;
|
|
79
112
|
}
|
|
80
113
|
|
|
81
114
|
// A forwarded PDF loses its file name, so every page carries the document title and the page number (Chromium fills the spans).
|
|
82
|
-
const PDF_FOOTER = '<div style="width:100%;font:9px system-ui,sans-serif;color:#
|
|
115
|
+
const PDF_FOOTER = '<div style="width:100%;font:9px \'Google Sans\',system-ui,sans-serif;color:#566479;padding:0 10mm;display:flex;justify-content:space-between"><span class="title"></span><span><span class="pageNumber"></span> / <span class="totalPages"></span></span></div>';
|
|
83
116
|
|
|
84
117
|
/** Prints an HTML report to a PDF with Chromium. The print styles stack every tab and open every details, so the PDF holds the whole report. */
|
|
85
118
|
export async function writePdf(htmlPath, pdfPath) {
|