mirror of
https://github.com/github/awesome-copilot.git
synced 2026-07-14 01:51:02 +00:00
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:
@@ -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": {
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Generated
+61
-102
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user