Skip to content

Commit 4e48ed7

Browse files
committed
feat: add QA theme checks
1 parent d7f9384 commit 4e48ed7

6 files changed

Lines changed: 243 additions & 5 deletions

File tree

defaults/public/_stats/index.html

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
<meta charset="utf-8" />
55
<meta name="viewport" content="width=device-width, initial-scale=1" />
66
<title>Dashboard</title>
7+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
8+
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
79
<style>
810
@font-face {
911
font-family: "InterVariable";
@@ -20,7 +22,7 @@
2022
src: url("/fonts/jetbrainsmono.woff2") format("woff2");
2123
}
2224
:root {
23-
color-scheme: light;
25+
color-scheme: light dark;
2426
--bg: #f9fafb;
2527
--panel: #ffffff;
2628
--text: #111827;
@@ -29,6 +31,30 @@
2931
--soft: #f3f4f6;
3032
--brand: #0f766e;
3133
}
34+
:root[data-theme="light"] {
35+
color-scheme: light;
36+
}
37+
:root[data-theme="dark"] {
38+
color-scheme: dark;
39+
--bg: #101418;
40+
--panel: #171d22;
41+
--text: #eef4f3;
42+
--muted: #a8b3b1;
43+
--border: #2d3836;
44+
--soft: #202a2f;
45+
--brand: #4fd1c5;
46+
}
47+
@media (prefers-color-scheme: dark) {
48+
:root:not([data-theme]) {
49+
--bg: #101418;
50+
--panel: #171d22;
51+
--text: #eef4f3;
52+
--muted: #a8b3b1;
53+
--border: #2d3836;
54+
--soft: #202a2f;
55+
--brand: #4fd1c5;
56+
}
57+
}
3258
* {
3359
box-sizing: border-box;
3460
}

defaults/public/_tests/index.html

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,25 @@ <h3>Machine-readable files</h3>
4242
<li><a href="/site.webmanifest" target="_blank" rel="noreferrer">site.webmanifest</a></li>
4343
</ul>
4444
</section>
45+
<section class="qa-section">
46+
<h3>Theme checks</h3>
47+
<ul class="qa-links">
48+
<li><a href="/?theme=light" target="_blank" rel="noreferrer">Index light</a></li>
49+
<li><a href="/?theme=dark" target="_blank" rel="noreferrer">Index dark</a></li>
50+
<li><a href="/expand?theme=light" target="_blank" rel="noreferrer">Expand light</a></li>
51+
<li><a href="/expand?theme=dark" target="_blank" rel="noreferrer">Expand dark</a></li>
52+
<li><a href="/404?theme=light" target="_blank" rel="noreferrer">404 light</a></li>
53+
<li><a href="/404?theme=dark" target="_blank" rel="noreferrer">404 dark</a></li>
54+
<li><a href="/expired?theme=light" target="_blank" rel="noreferrer">Expired light</a></li>
55+
<li><a href="/expired?theme=dark" target="_blank" rel="noreferrer">Expired dark</a></li>
56+
<li><a href="/disabled?theme=light" target="_blank" rel="noreferrer">Disabled light</a></li>
57+
<li><a href="/disabled?theme=dark" target="_blank" rel="noreferrer">Disabled dark</a></li>
58+
<li><a href="/maintenance?theme=light" target="_blank" rel="noreferrer">Maintenance light</a></li>
59+
<li><a href="/maintenance?theme=dark" target="_blank" rel="noreferrer">Maintenance dark</a></li>
60+
<li><a href="/_stats/?theme=light" target="_blank" rel="noreferrer">Stats light</a></li>
61+
<li><a href="/_stats/?theme=dark" target="_blank" rel="noreferrer">Stats dark</a></li>
62+
</ul>
63+
</section>
4564
<section class="qa-section">
4665
<h3>Status Pages</h3>
4766
<ul class="qa-links">

defaults/public/status.css

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,27 @@
2929
--shadow: 0 16px 42px rgb(15 23 42 / 0.08);
3030
}
3131

32+
:root[data-theme="light"] {
33+
color-scheme: light;
34+
}
35+
36+
:root[data-theme="dark"] {
37+
color-scheme: dark;
38+
--page-bg: #101418;
39+
--panel: #171d22;
40+
--text: #eef4f3;
41+
--muted: #a8b3b1;
42+
--subtle: #788481;
43+
--border: #2d3836;
44+
--brand: #4fd1c5;
45+
--focus: #5eead4;
46+
--code-bg: #202a2f;
47+
--status-code: rgb(238 244 243 / 0.14);
48+
--shadow: 0 18px 52px rgb(0 0 0 / 0.28);
49+
}
50+
3251
@media (prefers-color-scheme: dark) {
33-
:root {
52+
:root:not([data-theme]) {
3453
--page-bg: #101418;
3554
--panel: #171d22;
3655
--text: #eef4f3;

defaults/public/style.css

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,28 @@
3232
--visual-viewport-height: 100dvh;
3333
}
3434

35+
:root[data-theme="light"] {
36+
color-scheme: light;
37+
}
38+
39+
:root[data-theme="dark"] {
40+
color-scheme: dark;
41+
--page-bg: #101418;
42+
--panel: #171d22;
43+
--text: #eef4f3;
44+
--muted: #a8b3b1;
45+
--subtle: #788481;
46+
--border: #2d3836;
47+
--brand: #4fd1c5;
48+
--brand-strong: #7ddbd4;
49+
--focus: #5eead4;
50+
--hover-panel: #202a2f;
51+
--code-bg: #202a2f;
52+
--shadow: 0 18px 52px rgb(0 0 0 / 0.28);
53+
}
54+
3555
@media (prefers-color-scheme: dark) {
36-
:root {
56+
:root:not([data-theme]) {
3757
--page-bg: #101418;
3858
--panel: #171d22;
3959
--text: #eef4f3;

scripts/build.mjs

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,11 +219,70 @@ function rewriteHtmlFiles(directory, transform) {
219219
if (entry.isDirectory()) {
220220
rewriteHtmlFiles(entryPath, transform);
221221
} else if (entry.isFile() && entry.name.endsWith(".html")) {
222-
fs.writeFileSync(entryPath, transform(fs.readFileSync(entryPath, "utf8")));
222+
fs.writeFileSync(entryPath, transform(fs.readFileSync(entryPath, "utf8"), entryPath));
223223
}
224224
}
225225
}
226226

227+
function normalizeHtmlHeadAssets() {
228+
rewriteHtmlFiles(BUILD_DIR, (html) => normalizeHtmlHead(html));
229+
}
230+
231+
function normalizeHtmlHead(html) {
232+
let normalized = html;
233+
234+
if (!normalized.includes('rel="icon"')) {
235+
normalized = insertBeforeHeadClose(normalized, ' <link rel="icon" type="image/svg+xml" href="/favicon.svg">\n');
236+
}
237+
238+
if (!normalized.includes('rel="apple-touch-icon"')) {
239+
normalized = insertBeforeHeadClose(normalized, ' <link rel="apple-touch-icon" href="/apple-touch-icon.png">\n');
240+
}
241+
242+
if (!normalized.includes("data-v8s-theme-override")) {
243+
normalized = insertBeforeFirstStylesheet(normalized, `${THEME_OVERRIDE_SCRIPT}\n`);
244+
}
245+
246+
return normalized;
247+
}
248+
249+
function insertBeforeHeadClose(html, insertion) {
250+
return html.replace(/<\/head>/i, `${insertion}</head>`);
251+
}
252+
253+
function insertBeforeFirstStylesheet(html, insertion) {
254+
if (/<link\s+[^>]*rel=["']stylesheet["'][^>]*>/i.test(html)) {
255+
return html.replace(/(<link\s+[^>]*rel=["']stylesheet["'][^>]*>)/i, `${insertion}$1`);
256+
}
257+
258+
return insertBeforeHeadClose(html, insertion);
259+
}
260+
261+
const THEME_OVERRIDE_SCRIPT = ` <script data-v8s-theme-override>
262+
(() => {
263+
const theme = new URLSearchParams(window.location.search).get("theme");
264+
if (theme !== "light" && theme !== "dark") return;
265+
266+
document.documentElement.dataset.theme = theme;
267+
268+
const applyThemeImages = () => {
269+
if (theme !== "dark") return;
270+
271+
document.querySelectorAll('picture source[media*="prefers-color-scheme"][srcset]').forEach((source) => {
272+
const image = source.parentElement?.querySelector("img");
273+
const candidate = source.getAttribute("srcset")?.split(",")[0]?.trim()?.split(/\\s+/)[0];
274+
if (image && candidate) image.src = candidate;
275+
});
276+
};
277+
278+
if (document.readyState === "loading") {
279+
document.addEventListener("DOMContentLoaded", applyThemeImages, { once: true });
280+
} else {
281+
applyThemeImages();
282+
}
283+
})();
284+
</script>`;
285+
227286
function isDefaultLegalTemplate(filePath) {
228287
if (!fs.existsSync(filePath)) return false;
229288
const html = fs.readFileSync(filePath, "utf8");
@@ -714,6 +773,7 @@ ${pageLinks}
714773
</ul>
715774
</section>
716775
${language === "en" ? renderMachineReadableTestsSection() : ""}
776+
${language === "en" ? renderThemeTestsSection() : ""}
717777
<section class="qa-section">
718778
<h3>${escapeHtml(metadata.statusTitle)}</h3>
719779
<ul class="qa-links">
@@ -750,6 +810,36 @@ ${links}
750810
</section>`;
751811
}
752812

813+
function renderThemeTestsSection() {
814+
const links = [
815+
["/", "Index"],
816+
["/expand", "Expand"],
817+
["/404", "404"],
818+
["/expired", "Expired"],
819+
["/disabled", "Disabled"],
820+
["/maintenance", "Maintenance"],
821+
["/_stats/", "Stats"]
822+
]
823+
.flatMap(([href, label]) => [
824+
[withTheme(href, "light"), `${label} light`],
825+
[withTheme(href, "dark"), `${label} dark`]
826+
])
827+
.map(([href, label]) => renderTestsLink(href, label))
828+
.join("\n");
829+
830+
return ` <section class="qa-section">
831+
<h3>Theme checks</h3>
832+
<ul class="qa-links">
833+
${links}
834+
</ul>
835+
</section>`;
836+
}
837+
838+
function withTheme(href, theme) {
839+
const separator = href.includes("?") ? "&" : "?";
840+
return `${href}${separator}theme=${theme}`;
841+
}
842+
753843
function encodePathSegment(value) {
754844
return encodeURIComponent(String(value || "").trim()).replace(/%2F/gi, "/");
755845
}
@@ -907,6 +997,7 @@ function main() {
907997
writeRuntimeSiteConfig(runtimeSiteConfig(siteConfig), RUNTIME_SITE_CONFIG_PATH);
908998
removeDeferredLegalPages(siteConfig);
909999
buildTestsPage(siteConfig);
1000+
normalizeHtmlHeadAssets();
9101001
copyRuntimeBlocklist();
9111002
buildRedirectTargets();
9121003
validateRuntimeRegistry();

scripts/install.mjs

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -774,7 +774,9 @@ function customizePublicPages(args) {
774774
fs.rmSync(CUSTOM_PUBLIC_DIR, { recursive: true, force: true });
775775
copyDirectory(DEFAULT_PUBLIC_DIR, CUSTOM_PUBLIC_DIR);
776776
pruneUnsupportedLanguageDirs(CUSTOM_PUBLIC_DIR, args.languages);
777-
rewriteHtmlFiles(CUSTOM_PUBLIC_DIR, (html, filePath) => applyBranding(html, args, languageForPublicFile(filePath)));
777+
rewriteHtmlFiles(CUSTOM_PUBLIC_DIR, (html, filePath) =>
778+
normalizeHtmlHead(applyBranding(html, args, languageForPublicFile(filePath)))
779+
);
778780
formatFiles(CUSTOM_PUBLIC_DIR, [".html"]);
779781
}
780782

@@ -822,6 +824,67 @@ function rewriteHtmlFiles(directory, transform) {
822824
}
823825
}
824826

827+
function normalizeHtmlHead(html) {
828+
let normalized = html;
829+
830+
if (!normalized.includes('rel="icon"')) {
831+
normalized = insertBeforeHeadClose(
832+
normalized,
833+
' <link rel="icon" type="image/svg+xml" href="/favicon.svg" />\n'
834+
);
835+
}
836+
837+
if (!normalized.includes('rel="apple-touch-icon"')) {
838+
normalized = insertBeforeHeadClose(
839+
normalized,
840+
' <link rel="apple-touch-icon" href="/apple-touch-icon.png" />\n'
841+
);
842+
}
843+
844+
if (!normalized.includes("data-v8s-theme-override")) {
845+
normalized = insertBeforeFirstStylesheet(normalized, `${THEME_OVERRIDE_SCRIPT}\n`);
846+
}
847+
848+
return normalized;
849+
}
850+
851+
function insertBeforeHeadClose(html, insertion) {
852+
return html.replace(/<\/head>/i, `${insertion}</head>`);
853+
}
854+
855+
function insertBeforeFirstStylesheet(html, insertion) {
856+
if (/<link\s+[^>]*rel=["']stylesheet["'][^>]*>/i.test(html)) {
857+
return html.replace(/(<link\s+[^>]*rel=["']stylesheet["'][^>]*>)/i, `${insertion}$1`);
858+
}
859+
860+
return insertBeforeHeadClose(html, insertion);
861+
}
862+
863+
const THEME_OVERRIDE_SCRIPT = ` <script data-v8s-theme-override>
864+
(() => {
865+
const theme = new URLSearchParams(window.location.search).get("theme");
866+
if (theme !== "light" && theme !== "dark") return;
867+
868+
document.documentElement.dataset.theme = theme;
869+
870+
const applyThemeImages = () => {
871+
if (theme !== "dark") return;
872+
873+
document.querySelectorAll('picture source[media*="prefers-color-scheme"][srcset]').forEach((source) => {
874+
const image = source.parentElement?.querySelector("img");
875+
const candidate = source.getAttribute("srcset")?.split(",")[0]?.trim()?.split(/\\s+/)[0];
876+
if (image && candidate) image.src = candidate;
877+
});
878+
};
879+
880+
if (document.readyState === "loading") {
881+
document.addEventListener("DOMContentLoaded", applyThemeImages, { once: true });
882+
} else {
883+
applyThemeImages();
884+
}
885+
})();
886+
</script>`;
887+
825888
function languageForPublicFile(filePath) {
826889
const [language] = path.relative(CUSTOM_PUBLIC_DIR, filePath).split(path.sep);
827890
return DEFAULT_LANGUAGES.includes(language) ? language : "en";

0 commit comments

Comments
 (0)