Skip to content

Commit 055d099

Browse files
author
Foscat
committed
fix: restore demo code contrast
1 parent fab8e26 commit 055d099

4 files changed

Lines changed: 100 additions & 5 deletions

File tree

demo/demo.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,11 @@ body[data-ecosystem="all-three"] .demo-status {
460460
white-space: pre;
461461
}
462462

463+
/* Copy-ready snippets inherit the dedicated code surface instead of inline-code paint. */
464+
.demo-code-card pre code {
465+
color: inherit;
466+
}
467+
463468
.demo-copy-status[data-copy-state="error"] {
464469
color: var(--demo-danger);
465470
}

scripts/build-pages.mjs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { createHash } from "node:crypto";
12
import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
23
import { join, relative } from "node:path";
34
import { fileURLToPath } from "node:url";
@@ -17,6 +18,23 @@ function assertInsideRoot(path) {
1718
}
1819
}
1920

21+
function fingerprintVersionedAsset(index, assetPath, contents) {
22+
const assetUrl = `./${assetPath}`;
23+
const escapedAssetUrl = assetUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
24+
const versionedAssetPattern = new RegExp(`${escapedAssetUrl}\\?v=([^"'\\s&]+)`, "g");
25+
const fingerprint = createHash("sha256").update(contents).digest("hex").slice(0, 12);
26+
const fingerprintedIndex = index.replace(
27+
versionedAssetPattern,
28+
`${assetUrl}?v=$1-${fingerprint}`
29+
);
30+
31+
if (fingerprintedIndex === index) {
32+
throw new Error(`Expected a versioned Pages asset URL for ${assetPath}.`);
33+
}
34+
35+
return fingerprintedIndex;
36+
}
37+
2038
assertInsideRoot(demoDir);
2139
assertInsideRoot(distDir);
2240
assertInsideRoot(pagesDir);
@@ -28,11 +46,20 @@ await cp(distDir, join(pagesDir, "dist"), { recursive: true });
2846

2947
const indexPath = join(pagesDir, "index.html");
3048
const index = await readFile(indexPath, "utf8");
31-
const pagesIndex = index.replaceAll(
49+
let pagesIndex = index.replaceAll(
3250
"../dist/layout-style-css.css",
3351
"./dist/layout-style-css.css"
3452
);
3553

54+
/*
55+
* Pages receives immutable asset URLs derived from deployed content, so a demo
56+
* hotfix cannot be hidden by a browser cache that still holds the same version.
57+
*/
58+
for (const assetPath of ["demo.css", "demo.js", "dist/layout-style-css.css"]) {
59+
const assetContents = await readFile(join(pagesDir, ...assetPath.split("/")));
60+
pagesIndex = fingerprintVersionedAsset(pagesIndex, assetPath, assetContents);
61+
}
62+
3663
await writeFile(indexPath, pagesIndex);
3764
await writeFile(join(pagesDir, ".nojekyll"), "");
3865

test/demo-smoke.test.mjs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,57 @@ const assertTopologyReadout = async (page, expected) => {
273273
);
274274
};
275275

276+
const parseRgbColor = (value) => {
277+
const channels = value.match(/[\d.]+/g)?.slice(0, 3).map(Number);
278+
assert.equal(channels?.length, 3, `Expected an RGB color, got "${value}".`);
279+
return channels;
280+
};
281+
282+
const relativeLuminance = (channels) =>
283+
channels
284+
.map((channel) => channel / 255)
285+
.map((channel) => (channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4))
286+
.reduce(
287+
(luminance, channel, index) => luminance + channel * [0.2126, 0.7152, 0.0722][index],
288+
0
289+
);
290+
291+
const contrastRatio = (foreground, background) => {
292+
const lighter = Math.max(relativeLuminance(foreground), relativeLuminance(background));
293+
const darker = Math.min(relativeLuminance(foreground), relativeLuminance(background));
294+
return (lighter + 0.05) / (darker + 0.05);
295+
};
296+
297+
const verifyCodeBlockContrast = async (page, baseUrl) => {
298+
await page.setViewportSize({ width: 1440, height: 900 });
299+
await page.goto(`${baseUrl}?ecosystem=all-three&mode=light`, {
300+
waitUntil: "domcontentloaded"
301+
});
302+
await page.waitForFunction(() => document.body.dataset.demoReady === "true");
303+
304+
for (const mode of ["light", "dark", "contrast"]) {
305+
await setControl(page, "modeSelect", mode);
306+
const blocks = await page.locator(".demo-code-card pre").evaluateAll((preElements) =>
307+
preElements.map((preElement) => {
308+
const codeElement = preElement.querySelector("code");
309+
return {
310+
background: getComputedStyle(preElement).backgroundColor,
311+
color: getComputedStyle(codeElement).color
312+
};
313+
})
314+
);
315+
316+
assert.equal(blocks.length, 2, `${mode}: expected both copy-ready code blocks.`);
317+
for (const [index, block] of blocks.entries()) {
318+
const ratio = contrastRatio(parseRgbColor(block.color), parseRgbColor(block.background));
319+
assert(
320+
ratio >= 4.5,
321+
`${mode} code block ${index + 1} has ${ratio.toFixed(2)}:1 contrast; expected at least 4.5:1.`
322+
);
323+
}
324+
}
325+
};
326+
276327
const layoutSnapshot = async (page) =>
277328
page.evaluate(() => {
278329
const documentElement = document.documentElement;
@@ -953,6 +1004,7 @@ try {
9531004
await verifyBreakoutGeometry(page, server.baseUrl);
9541005
await verifyProfileAndUtilityIsolation(page, server.baseUrl);
9551006
await verifyPrimitiveOverflow(page, server.baseUrl);
1007+
await verifyCodeBlockContrast(page, server.baseUrl);
9561008
await verifyInteractions(page, server.baseUrl);
9571009

9581010
assert.deepEqual(pageErrors, [], `Page errors:\n${pageErrors.join("\n")}`);

test/pages-artifact.test.mjs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { join } from "node:path";
33
import { spawnSync } from "node:child_process";
44
import { fileURLToPath } from "node:url";
55
import assert from "node:assert/strict";
6+
import { createHash } from "node:crypto";
67

78
const root = fileURLToPath(new URL("..", import.meta.url));
89
const outputDir = join(root, "output", "github-pages");
@@ -63,14 +64,24 @@ assert.equal(
6364
"The canonical URL must use the repository's case-sensitive GitHub Pages slug."
6465
);
6566

66-
assert(
67-
index.includes('href="./dist/layout-style-css.css?v=3.0.0"'),
68-
"Pages root demo should load the default v3 bundle from ./dist"
69-
);
7067
assert(
7168
!index.includes("../dist/layout-style-css.css"),
7269
"Pages root demo should not reference parent dist paths"
7370
);
71+
72+
for (const { attribute, path } of [
73+
{ attribute: "href", path: "demo.css" },
74+
{ attribute: "src", path: "demo.js" },
75+
{ attribute: "href", path: "dist/layout-style-css.css" }
76+
]) {
77+
const contents = readFileSync(join(outputDir, ...path.split("/")));
78+
const fingerprint = createHash("sha256").update(contents).digest("hex").slice(0, 12);
79+
assert(
80+
index.includes(`${attribute}="./${path}?v=3.0.0-${fingerprint}"`),
81+
`Pages should fingerprint ${path} from its deployed contents.`
82+
);
83+
}
84+
7485
assert(!index.includes("integrations/ui-style-kit.css"), "Pages must not reference the removed bridge");
7586
assert(
7687
!pagesBuild.includes("integrations/ui-style-kit.css"),

0 commit comments

Comments
 (0)