fix(website): remediate WCAG accessibility issues and add axe regression guardrail (#2180)

* fix(website): remediate WCAG accessibility issues and add axe regression guardrail

Fixes accessibility violations found by an axe-core sweep of every website
page in both light and dark themes:

- aria-allowed-role (WCAG 1.3.1, 4.1.2): the shared resource card rendered
  an <article> with role="listitem", which is not an allowed role for that
  element. Switched the wrapper to a <div role="listitem"> so the listitem
  role is valid (affected every card, e.g. #arcade-canvas on /extensions/).
- aria-required-children (WCAG 1.3.1): removed role="list" from the tools,
  contributors, and cookbook containers whose children are not list items.
- nested-interactive (WCAG 4.1.2): removed tabIndex=0 from extension cards
  and rendered the author as a non-interactive span so no interactive
  control is nested inside another (the author link remains in the modal).
- color-contrast (WCAG 1.4.3): gave .btn-primary explicit white text with an
  AA-compliant hover (#7326d6), and bumped the dark-theme secondary text gray
  (--sl-color-gray-3) to #84849c (5.32:1) so ToC / meta / footer text passes.

Adds a checked-in regression guardrail:

- website/scripts/a11y-audit.mjs runs axe-core over all routes in both themes
  against the production build and fails on critical/serious violations only
  (moderate/minor are reported but non-blocking). Transitions/animations are
  disabled before sampling so axe measures settled, steady-state colors
  instead of mid-theme-transition frames.
- Adds npm scripts (website:a11y at the root, a11y in website/) and README
  docs. The matching Build Website CI step is proposed in the PR description
  (omitted from this commit because the authoring token lacks workflow scope).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* fix(website): harden a11y audit per PR review

- Fail fast when a route navigation returns a non-2xx response, so the
  guardrail can't silently pass against a broken/missing route (page.goto
  resolves even for 4xx/5xx).
- Launch the Astro preview server via `node <astro-bin>` instead of
  `spawn(..., { shell: true })`. Removing the shell layer keeps signal
  delivery / detached-PGID shutdown predictable; resolving Astro's bin and
  running it with process.execPath also avoids the EINVAL that modern Node
  raises when spawning the npx.cmd shim without a shell on Windows.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: David Pine <7679720+IEvangelist@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Pine
2026-07-06 20:11:17 -05:00
committed by GitHub
parent 96f3275a66
commit d61dfdcddc
12 changed files with 471 additions and 122 deletions
+1
View File
@@ -20,6 +20,7 @@
"website:data": "node ./eng/generate-website-data.mjs", "website:data": "node ./eng/generate-website-data.mjs",
"website:dev": "npm run website:data && npm run --prefix website dev", "website:dev": "npm run website:data && npm run --prefix website dev",
"website:build": "npm run build && npm run website:data && npm run --prefix website build", "website:build": "npm run build && npm run website:data && npm run --prefix website build",
"website:a11y": "npm run website:build && npm run --prefix website a11y",
"website:preview": "npm run --prefix website preview" "website:preview": "npm run --prefix website preview"
}, },
"repository": { "repository": {
+8
View File
@@ -12,6 +12,14 @@ npm run website:dev # generate data + start the dev server
npm run website:build # full production build npm run website:build # full production build
``` ```
## Accessibility
The website has an automated axe-core + Playwright audit. Run it locally with `npm run website:a11y` from the repository root, or run `npm run a11y` from `website/` after building `dist` first.
CI blocks on critical and serious violations. Minor and moderate best-practice issues are reported as non-blocking.
Authoring conventions: resource cards use `div[role="listitem"]` wrappers, not `<article>`; only add `role="list"` to containers whose direct children are list items; do not nest interactive controls inside another focusable element; `.btn-primary` and ToC links must meet WCAG AA (4.5:1) contrast in both light and dark themes.
## Social preview cards (LinkedIn, etc.) ## Social preview cards (LinkedIn, etc.)
Shared links render as large preview cards driven by Open Graph / Twitter meta tags. Shared links render as large preview cards driven by Open Graph / Twitter meta tags.
+61 -102
View File
@@ -18,6 +18,10 @@
"jszip": "^3.10.1", "jszip": "^3.10.1",
"marked": "^18.0.2", "marked": "^18.0.2",
"shiki": "^4.0.2" "shiki": "^4.0.2"
},
"devDependencies": {
"axe-core": "^4.10.2",
"playwright": "^1.49.0"
} }
}, },
"node_modules/@astrojs/compiler-binding": { "node_modules/@astrojs/compiler-binding": {
@@ -79,9 +83,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -98,9 +99,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -117,9 +115,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -136,9 +131,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -454,9 +446,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"glibc"
],
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
@@ -469,9 +458,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"musl"
],
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
@@ -484,9 +470,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"glibc"
],
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
@@ -499,9 +482,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"musl"
],
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
@@ -1182,9 +1162,6 @@
"cpu": [ "cpu": [
"arm" "arm"
], ],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later", "license": "LGPL-3.0-or-later",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1201,9 +1178,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later", "license": "LGPL-3.0-or-later",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1220,9 +1194,6 @@
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later", "license": "LGPL-3.0-or-later",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1239,9 +1210,6 @@
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later", "license": "LGPL-3.0-or-later",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1258,9 +1226,6 @@
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later", "license": "LGPL-3.0-or-later",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1277,9 +1242,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later", "license": "LGPL-3.0-or-later",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1296,9 +1258,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"musl"
],
"license": "LGPL-3.0-or-later", "license": "LGPL-3.0-or-later",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1315,9 +1274,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"musl"
],
"license": "LGPL-3.0-or-later", "license": "LGPL-3.0-or-later",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1334,9 +1290,6 @@
"cpu": [ "cpu": [
"arm" "arm"
], ],
"libc": [
"glibc"
],
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1359,9 +1312,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"glibc"
],
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1384,9 +1334,6 @@
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
"libc": [
"glibc"
],
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1409,9 +1356,6 @@
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
"libc": [
"glibc"
],
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1434,9 +1378,6 @@
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
"libc": [
"glibc"
],
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1459,9 +1400,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"glibc"
],
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1484,9 +1422,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"musl"
],
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1509,9 +1444,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"musl"
],
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1863,9 +1795,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1882,9 +1811,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1901,9 +1827,6 @@
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1920,9 +1843,6 @@
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1939,9 +1859,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1958,9 +1875,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2477,6 +2391,16 @@
"astro": "^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta || ^7.0.0" "astro": "^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta || ^7.0.0"
} }
}, },
"node_modules/axe-core": {
"version": "4.12.1",
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.12.1.tgz",
"integrity": "sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA==",
"dev": true,
"license": "MPL-2.0",
"engines": {
"node": ">=4"
}
},
"node_modules/axobject-query": { "node_modules/axobject-query": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
@@ -4212,9 +4136,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"glibc"
],
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -4235,9 +4156,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"musl"
],
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -4258,9 +4176,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"glibc"
],
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -4281,9 +4196,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"musl"
],
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -5762,6 +5674,53 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/playwright": {
"version": "1.61.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz",
"integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.61.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.61.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz",
"integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/playwright/node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.16", "version": "8.5.16",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
+5
View File
@@ -6,6 +6,7 @@
"scripts": { "scripts": {
"dev": "astro dev", "dev": "astro dev",
"build": "astro build", "build": "astro build",
"a11y": "node ./scripts/a11y-audit.mjs",
"preview": "astro preview", "preview": "astro preview",
"astro": "astro" "astro": "astro"
}, },
@@ -27,5 +28,9 @@
"jszip": "^3.10.1", "jszip": "^3.10.1",
"marked": "^18.0.2", "marked": "^18.0.2",
"shiki": "^4.0.2" "shiki": "^4.0.2"
},
"devDependencies": {
"axe-core": "^4.10.2",
"playwright": "^1.49.0"
} }
} }
+384
View File
@@ -0,0 +1,384 @@
import { execFile, spawn } from 'node:child_process';
import { access } from 'node:fs/promises';
import { createRequire } from 'node:module';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { chromium } from 'playwright';
const require = createRequire(import.meta.url);
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const websiteDir = path.resolve(scriptDir, '..');
const distDir = path.join(websiteDir, 'dist');
const axeMainPath = require.resolve('axe-core');
const axeSourcePath = path.join(path.dirname(axeMainPath), 'axe.min.js');
// Resolve Astro's CLI entry point so the preview server can be launched with `node <bin>`
// directly — no shell and no `npx` shim. This keeps signal delivery / detached-PGID shutdown
// predictable (see startPreviewServer) and works identically on Windows and POSIX (spawning a
// .cmd shim without a shell throws EINVAL on modern Node).
const astroPackageJsonPath = require.resolve('astro/package.json');
const astroBinField = require(astroPackageJsonPath).bin;
const astroBinRelative = typeof astroBinField === 'string' ? astroBinField : astroBinField.astro;
const astroBinPath = path.join(path.dirname(astroPackageJsonPath), astroBinRelative);
const routes = [
'/',
'/agents/',
'/instructions/',
'/skills/',
'/hooks/',
'/workflows/',
'/extensions/',
'/plugins/',
'/tools/',
'/contributors/',
'/learning-hub/cookbook/',
'/learning-hub/github-copilot-app/',
];
const themes = ['dark', 'light'];
const blockingImpacts = new Set(['critical', 'serious']);
const serverTimeoutMs = 60_000;
const fetchTimeoutMs = 2_000;
async function main() {
const port = getPort();
const providedBaseUrl = process.env.A11Y_BASE_URL?.trim();
const baseUrl = providedBaseUrl || `http://localhost:${port}/`;
let previewServer;
let browser;
try {
await access(axeSourcePath);
if (!providedBaseUrl) {
await access(distDir);
previewServer = startPreviewServer(port);
}
await waitForServer(new URL('/', baseUrl).toString(), previewServer);
browser = await chromium.launch();
const violations = await auditSite(browser, baseUrl);
printReport(violations);
// Gate only critical and serious violations; moderate/minor findings are reported but non-blocking.
const blockingCount = violations.filter((violation) => blockingImpacts.has(violation.impact)).length;
const nonBlockingCount = violations.length - blockingCount;
console.log(
`\nSummary: ${blockingCount} blocking (critical/serious), ${nonBlockingCount} non-blocking (moderate/minor) violation(s).`,
);
if (blockingCount > 0) {
process.exitCode = 1;
console.error(`Accessibility audit FAILED — ${blockingCount} blocking violation(s)`);
} else {
process.exitCode = 0;
console.log('Accessibility audit PASSED (no critical/serious violations)');
}
} catch (error) {
process.exitCode = 1;
console.error(`Accessibility audit ERROR — ${error instanceof Error ? error.message : String(error)}`);
} finally {
await closeBrowser(browser);
await stopPreviewServer(previewServer);
}
}
function getPort() {
const port = Number.parseInt(process.env.A11Y_PORT || '4322', 10);
if (!Number.isInteger(port) || port <= 0) {
throw new Error(`Invalid A11Y_PORT value: ${process.env.A11Y_PORT}`);
}
return port;
}
function startPreviewServer(port) {
// Launch `node <astro-bin> preview` directly — no shell layer and no npx shim. A shell
// (shell:true) spawns an extra intermediate process that makes signal delivery / PGID-based
// shutdown (detached + process.kill(-pid) below) unreliable, and spawning the npx.cmd shim
// without a shell throws EINVAL on modern Node for Windows. Invoking the resolved bin with
// process.execPath sidesteps both problems on every platform.
const child = spawn(process.execPath, [astroBinPath, 'preview', '--port', String(port), '--host'], {
cwd: websiteDir,
detached: process.platform !== 'win32',
stdio: ['ignore', 'pipe', 'pipe'],
});
const server = {
child,
exited: false,
exitCode: null,
signal: null,
spawnError: null,
logs: [],
};
const rememberOutput = (chunk) => {
const lines = String(chunk)
.split(/\r?\n/)
.map((line) => line.trimEnd())
.filter(Boolean);
server.logs.push(...lines);
if (server.logs.length > 60) {
server.logs.splice(0, server.logs.length - 60);
}
};
child.stdout.on('data', rememberOutput);
child.stderr.on('data', rememberOutput);
child.on('error', (error) => {
server.spawnError = error;
rememberOutput(`Failed to start preview server: ${error.message}`);
});
child.on('exit', (code, signal) => {
server.exited = true;
server.exitCode = code;
server.signal = signal;
});
return server;
}
async function waitForServer(url, server) {
const deadline = Date.now() + serverTimeoutMs;
let lastError;
while (Date.now() < deadline) {
if (server?.spawnError) {
throw new Error(`${server.spawnError.message}${formatServerLogs(server)}`);
}
if (server?.exited) {
throw new Error(
`Astro preview exited before it was ready (code ${server.exitCode ?? 'null'}, signal ${
server.signal ?? 'null'
}).${formatServerLogs(server)}`,
);
}
try {
const response = await fetchWithTimeout(url);
if (response.ok) {
return;
}
lastError = new Error(`HTTP ${response.status}`);
} catch (error) {
lastError = error;
}
await delay(500);
}
throw new Error(
`Timed out after ${serverTimeoutMs / 1000}s waiting for ${url}.${
lastError ? ` Last error: ${lastError.message}` : ''
}${formatServerLogs(server)}`,
);
}
async function fetchWithTimeout(url) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), fetchTimeoutMs);
try {
return await fetch(url, { signal: controller.signal });
} finally {
clearTimeout(timeout);
}
}
async function auditSite(browser, baseUrl) {
const page = await browser.newPage();
const violations = [];
for (const route of routes) {
for (const theme of themes) {
const url = new URL(route, baseUrl).toString();
// 'load' (not 'networkidle') keeps the audit robust: cards are server-rendered
// into the initial HTML, and lazy images / search-index requests can otherwise
// keep the network busy indefinitely and stall navigation.
const response = await page.goto(url, { waitUntil: 'load', timeout: 45_000 });
// Fail fast on broken/missing routes. page.goto() resolves even for 4xx/5xx responses,
// so without this check axe would run against an error page and the guardrail could
// "pass" while silently masking a broken route.
if (!response) {
throw new Error(`No navigation response for ${url} (theme: ${theme}).`);
}
if (!response.ok()) {
throw new Error(`Route ${url} (theme: ${theme}) returned HTTP ${response.status()}.`);
}
await page.waitForTimeout(500);
// Disable CSS transitions/animations before switching themes. Theme changes animate
// background/text colors over ~0.2s; if axe runs mid-transition it samples intermediate
// colors (e.g. a dark card background bleeding through a light page) and reports
// false-positive contrast violations that no real user ever sees. Killing transitions
// makes axe measure the settled, steady-state colors — the actual conformance target.
await page.addStyleTag({
content: '*,*::before,*::after{transition:none!important;animation:none!important;transition-duration:0s!important;animation-duration:0s!important;}',
});
// Force both theme modes so persisted user preference logic cannot hide regressions.
await page.evaluate((selectedTheme) => {
document.documentElement.setAttribute('data-theme', selectedTheme);
localStorage.setItem('awesome-copilot-theme', selectedTheme);
}, theme);
// Let the forced theme settle (style recalc / reflow) before sampling colors.
await page.waitForTimeout(150);
await page.addScriptTag({ path: axeSourcePath });
const results = await page.evaluate(async () => await axe.run(document, { resultTypes: ['violations'] }));
for (const violation of results.violations) {
violations.push({
route,
theme,
id: violation.id,
impact: violation.impact ?? 'unknown',
help: violation.help,
helpUrl: violation.helpUrl,
nodeCount: violation.nodes.length,
});
}
}
}
await page.close();
return violations;
}
function printReport(violations) {
console.log(`Audited ${routes.length} route(s) across ${themes.length} theme(s).`);
if (violations.length === 0) {
console.log('\nNo axe violations found.');
return;
}
const groupedViolations = new Map();
for (const violation of violations) {
const pageTheme = `${violation.route} [${violation.theme}]`;
const pageViolations = groupedViolations.get(pageTheme) || [];
pageViolations.push(violation);
groupedViolations.set(pageTheme, pageViolations);
}
for (const [pageTheme, pageViolations] of groupedViolations) {
console.log(`\n${pageTheme}`);
for (const violation of pageViolations) {
const gate = blockingImpacts.has(violation.impact) ? 'BLOCKING' : 'NON-BLOCKING';
console.log(` - ${violation.id} (${violation.impact}, ${gate})`);
console.log(` ${violation.help}`);
console.log(` ${violation.helpUrl}`);
console.log(` Nodes: ${violation.nodeCount}`);
}
}
}
async function closeBrowser(browser) {
if (!browser) {
return;
}
try {
await browser.close();
} catch (error) {
process.exitCode = 1;
console.error(`Failed to close browser cleanly: ${error instanceof Error ? error.message : String(error)}`);
}
}
async function stopPreviewServer(server) {
if (!server || server.exited || !server.child.pid) {
return;
}
await new Promise((resolve) => {
let resolved = false;
const finish = () => {
if (!resolved) {
resolved = true;
clearTimeout(timeout);
resolve();
}
};
const timeout = setTimeout(() => {
forceKill(server.child);
finish();
}, 5_000);
server.child.once('exit', finish);
if (process.platform === 'win32') {
execFile('taskkill', ['/pid', String(server.child.pid), '/T', '/F'], (error) => {
if (error && !server.exited) {
server.child.kill();
}
});
return;
}
try {
process.kill(-server.child.pid, 'SIGTERM');
} catch {
server.child.kill('SIGTERM');
}
});
}
function forceKill(child) {
if (!child.pid) {
return;
}
try {
if (process.platform === 'win32') {
child.kill();
} else {
process.kill(-child.pid, 'SIGKILL');
}
} catch {
try {
child.kill('SIGKILL');
} catch {
// The process already exited.
}
}
}
function formatServerLogs(server) {
if (!server?.logs.length) {
return '';
}
return `\n\nAstro preview output:\n${server.logs.join('\n')}`;
}
function delay(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
await main();
+1 -1
View File
@@ -9,7 +9,7 @@ import PageHeader from '../components/PageHeader.astro';
<div class="page-content"> <div class="page-content">
<div class="container"> <div class="container">
<div class="contributor-grid" id="contributor-grid" role="list"> <div class="contributor-grid" id="contributor-grid">
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section --> <!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start --> <!-- prettier-ignore-start -->
<!-- markdownlint-disable --> <!-- markdownlint-disable -->
@@ -53,7 +53,7 @@ const languageOptions = Array.from(
<div class="results-count" id="results-count" aria-live="polite">{getRecipeResultsCountText(samplesData.totalRecipes, samplesData.totalRecipes)}</div> <div class="results-count" id="results-count" aria-live="polite">{getRecipeResultsCountText(samplesData.totalRecipes, samplesData.totalRecipes)}</div>
<div id="samples-list" role="list" set:html={renderCookbookSectionsHtml(initialRecipeMatches)}></div> <div id="samples-list" set:html={renderCookbookSectionsHtml(initialRecipeMatches)}></div>
</div> </div>
<Modal /> <Modal />
+1 -1
View File
@@ -53,7 +53,7 @@ const initialItems = sortTools(
</div> </div>
</div> </div>
<div id="tools-list" role="list" set:html={renderToolsHtml(initialItems)}></div> <div id="tools-list" set:html={renderToolsHtml(initialItems)}></div>
<div class="coming-soon"> <div class="coming-soon">
<h2>More Tools Coming Soon</h2> <h2>More Tools Coming Soon</h2>
+2 -2
View File
@@ -36,7 +36,7 @@ export function renderSharedCardHtml(item: SharedCardRenderItem): string {
: "resource-item"; : "resource-item";
return ` return `
<article class="${articleClass}" role="${escapeHtml(role)}"${item.tabIndex !== undefined ? ` tabindex="${String(item.tabIndex)}"` : ""}${renderAttributes(item.articleAttributes)}> <div class="${articleClass}" role="${escapeHtml(role)}"${item.tabIndex !== undefined ? ` tabindex="${String(item.tabIndex)}"` : ""}${renderAttributes(item.articleAttributes)}>
<button type="button" class="resource-preview"> <button type="button" class="resource-preview">
${item.previewMediaHtml || ""} ${item.previewMediaHtml || ""}
<div class="resource-info"> <div class="resource-info">
@@ -49,6 +49,6 @@ export function renderSharedCardHtml(item: SharedCardRenderItem): string {
</div> </div>
</button> </button>
${item.actionsHtml ? `<div class="resource-actions">${item.actionsHtml}</div>` : ""} ${item.actionsHtml ? `<div class="resource-actions">${item.actionsHtml}</div>` : ""}
</article> </div>
`; `;
} }
+2 -10
View File
@@ -3,7 +3,6 @@ import {
getGitHubHandle, getGitHubHandle,
getGitHubUrl, getGitHubUrl,
getLastUpdatedHtml, getLastUpdatedHtml,
sanitizeUrl,
} from "../utils"; } from "../utils";
import { renderEmptyStateHtml, renderSharedCardHtml } from "./card-render"; import { renderEmptyStateHtml, renderSharedCardHtml } from "./card-render";
@@ -111,15 +110,9 @@ export function renderExtensionsHtml(items: RenderableExtension[]): string {
? getGitHubHandle(authorUrl, authorName) ? getGitHubHandle(authorUrl, authorName)
: authorName || ""; : authorName || "";
const authorHtml = authorName const authorHtml = authorName
? `<span class="resource-tag resource-author">by ${ ? `<span class="resource-tag resource-author" title="${escapeHtml(
authorUrl
? `<a href="${escapeHtml(
sanitizeUrl(authorUrl)
)}" target="_blank" rel="noopener noreferrer" title="${escapeHtml(
authorName authorName
)}">${escapeHtml(authorHandle)}</a>` )}">by ${escapeHtml(authorHandle || authorName)}</span>`
: escapeHtml(authorName)
}</span>`
: ""; : "";
const metaHtml = ` const metaHtml = `
@@ -161,7 +154,6 @@ export function renderExtensionsHtml(items: RenderableExtension[]): string {
infoExtraHtml, infoExtraHtml,
metaHtml, metaHtml,
actionsHtml, actionsHtml,
tabIndex: 0,
articleAttributes: { articleAttributes: {
id: item.id, id: item.id,
"data-extension-id": item.id, "data-extension-id": item.id,
+3 -3
View File
@@ -662,16 +662,16 @@ body:has(#main-content) {
.btn-primary { .btn-primary {
background: var(--color-accent); background: var(--color-accent);
color: var(--sl-color-text-invert) !important; color: #fff !important;
border-color: transparent; border-color: transparent;
font-weight: 600; font-weight: 600;
} }
.btn-primary:hover { .btn-primary:hover {
background: var(--color-accent-hover); background: #7326d6;
transform: translateY(-1px); transform: translateY(-1px);
box-shadow: var(--shadow-glow); box-shadow: var(--shadow-glow);
color: white; color: #fff;
} }
.btn-secondary { .btn-secondary {
+1 -1
View File
@@ -11,7 +11,7 @@
--sl-color-white: #f0f0f5; --sl-color-white: #f0f0f5;
--sl-color-gray-1: #d0d0da; --sl-color-gray-1: #d0d0da;
--sl-color-gray-2: #9898a6; --sl-color-gray-2: #9898a6;
--sl-color-gray-3: #60607a; --sl-color-gray-3: #84849c;
--sl-color-gray-4: #30304a; --sl-color-gray-4: #30304a;
--sl-color-gray-5: #1a1a2e; --sl-color-gray-5: #1a1a2e;
--sl-color-gray-6: #0d0d12; --sl-color-gray-6: #0d0d12;