diff --git a/docs/.astro/collections/blog.schema.json b/docs/.astro/collections/blog.schema.json
index 04f161b81..1c681e397 100644
--- a/docs/.astro/collections/blog.schema.json
+++ b/docs/.astro/collections/blog.schema.json
@@ -11,6 +11,15 @@
"date": {
"type": "string"
},
+ "hero": {
+ "type": "string"
+ },
+ "wave": {
+ "type": [
+ "number",
+ "null"
+ ]
+ },
"tags": {
"type": "array",
"items": {
diff --git a/docs/.gitignore b/docs/.gitignore
index ddce69b68..2594e0c92 100644
--- a/docs/.gitignore
+++ b/docs/.gitignore
@@ -1,3 +1,5 @@
node_modules/
dist/
.astro/
+visual-review/
+test-results/
diff --git a/docs/README.md b/docs/README.md
index b48dc5eb3..6bdcfa388 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1,6 +1,6 @@
# Squad Docs
-The documentation site for Squad, built with [Astro](https://astro.build/), [Tailwind CSS](https://tailwindcss.com/), and [Pagefind](https://pagefind.app/) for search.
+The documentation site for Squad, built with [Astro](https://astro.build/), [Primer](https://primer.style/) (`@primer/css` + `@primer/primitives`), and [Pagefind](https://pagefind.app/) for search.
## Prerequisites
@@ -78,7 +78,7 @@ docs/
│ ├── navigation.ts # Sidebar navigation structure
│ ├── pages/ # Astro page routes
│ ├── plugins/ # Remark plugins (link rewriting)
-│ └── styles/ # Global CSS (Tailwind)
+│ └── styles/ # Primer imports + token-based site CSS
├── public/ # Static assets (images)
└── dist/ # Build output (gitignored)
```
@@ -103,3 +103,13 @@ Post content here.
```
Blog posts are automatically discovered and listed on the blog index page.
+
+## Styling and accessibility
+
+- UI is built from [Primer](https://primer.style/) classes and design tokens; site-specific rules live in `src/styles/site.css` and use tokens only (no raw colors).
+- Color mode uses Primer's `data-color-mode` (`auto` by default; the toggle stores `light`/`dark` in `localStorage`).
+- `npm run test:e2e` includes axe-core WCAG A/AA checks in light and dark mode (`tests/a11y.spec.mjs`). Start `npm run preview -- --port 4321` first. The API reference specs need generated TypeDoc pages.
+
+## Visual review screenshots
+
+`npm run build && npm run screenshots` captures every layout page (landing, blog index and states, blog post, docs pages, 404, search dialog, mobile drawer) on mobile (390px), tablet (820px) and desktop (1440px) in light and dark mode into `docs/visual-review/` (gitignored) and writes `visual-review/index.html` as a gallery. Options: `--base`, `--out`, `--full-height`, `--only home,blog-post`. It starts `astro preview` itself if nothing is listening, and exits non-zero if any capture logged a console error.
diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs
index b57a56339..b92728e1b 100644
--- a/docs/astro.config.mjs
+++ b/docs/astro.config.mjs
@@ -1,8 +1,8 @@
import { defineConfig } from 'astro/config';
-import tailwindcss from '@tailwindcss/vite';
import sitemap from '@astrojs/sitemap';
import { remarkRewriteLinks } from './src/plugins/remark-rewrite-links.mjs';
import { rehypePagefindAttrs } from './src/plugins/rehype-pagefind-attrs.mjs';
+import { rehypeA11y } from './src/plugins/rehype-a11y.mjs';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
@@ -12,7 +12,6 @@ export default defineConfig({
base: '/squad/',
integrations: [sitemap()],
vite: {
- plugins: [tailwindcss()],
define: {
__VERSION__: JSON.stringify(process.env.SQUAD_VERSION || require('../package.json').version),
__COMMIT_SHA__: JSON.stringify(process.env.GITHUB_SHA || 'local'),
@@ -21,11 +20,11 @@ export default defineConfig({
},
markdown: {
remarkPlugins: [remarkRewriteLinks],
- rehypePlugins: [rehypePagefindAttrs],
+ rehypePlugins: [rehypePagefindAttrs, rehypeA11y],
shikiConfig: {
themes: {
- light: 'github-light',
- dark: 'github-dark',
+ light: 'github-light-high-contrast',
+ dark: 'github-dark-high-contrast',
},
},
},
diff --git a/docs/package-lock.json b/docs/package-lock.json
index 49d230fd4..4a1a36412 100644
--- a/docs/package-lock.json
+++ b/docs/package-lock.json
@@ -11,13 +11,15 @@
"@astrojs/markdown-remark": "^7.3.2",
"@astrojs/rss": "^4.0.19",
"@astrojs/sitemap": "^3.7.4",
- "@tailwindcss/vite": "^4.3.3",
+ "@fontsource-variable/mona-sans": "^5.3.0",
+ "@primer/css": "^22.3.3",
+ "@primer/primitives": "^11.10.0",
"astro": "^7.3.8",
"sharp": "^0.35.5",
- "tailwindcss": "^4.1.0",
"unist-util-visit": "^5.1.0"
},
"devDependencies": {
+ "@axe-core/playwright": "^4.14.0",
"@playwright/test": "^1.64.0",
"pagefind": "^1.5.2"
}
@@ -98,9 +100,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -117,9 +116,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -136,9 +132,6 @@
"cpu": [
"x64"
],
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -155,9 +148,6 @@
"cpu": [
"x64"
],
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -351,6 +341,22 @@
"node": "18.20.8 || ^20.3.0 || >=22.0.0"
}
},
+ "node_modules/@axe-core/playwright": {
+ "version": "4.14.0",
+ "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.14.0.tgz",
+ "integrity": "sha512-SGUhG5PIHd2eMdpgBF2PyQLOhX+qyDR2PT4cmoS4Qk2ak+jX/rrSvst9l0PGv5WTHolKpUMvVmoHpr4cZgf1RQ==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "axe-core": "~4.14.0"
+ },
+ "engines": {
+ "node": ">=22.12.0"
+ },
+ "peerDependencies": {
+ "playwright-core": ">= 1.0.0"
+ }
+ },
"node_modules/@babel/helper-string-parser": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
@@ -430,9 +436,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -446,9 +449,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -462,9 +462,6 @@
"cpu": [
"x64"
],
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -478,9 +475,6 @@
"cpu": [
"x64"
],
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -633,6 +627,15 @@
"tslib": "^2.4.0"
}
},
+ "node_modules/@fontsource-variable/mona-sans": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/@fontsource-variable/mona-sans/-/mona-sans-5.3.0.tgz",
+ "integrity": "sha512-PkxRB3KI4z9Gjpk7vVqgO3GMSyQuOIIj4TaRDbvbcDj8SzWwI38jvLTQxJM/cTRorhcT7h8qjcllR+qjRtM/Fg==",
+ "license": "OFL-1.1",
+ "funding": {
+ "url": "https://github.com/sponsors/ayuhito"
+ }
+ },
"node_modules/@img/colour": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
@@ -744,9 +747,6 @@
"cpu": [
"arm"
],
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -763,9 +763,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -782,9 +779,6 @@
"cpu": [
"ppc64"
],
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -801,9 +795,6 @@
"cpu": [
"riscv64"
],
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -820,9 +811,6 @@
"cpu": [
"s390x"
],
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -839,9 +827,6 @@
"cpu": [
"x64"
],
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -858,9 +843,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "musl"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -877,9 +859,6 @@
"cpu": [
"x64"
],
- "libc": [
- "musl"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -896,9 +875,6 @@
"cpu": [
"arm"
],
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -921,9 +897,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -946,9 +919,6 @@
"cpu": [
"ppc64"
],
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -971,9 +941,6 @@
"cpu": [
"riscv64"
],
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -996,9 +963,6 @@
"cpu": [
"s390x"
],
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -1021,9 +985,6 @@
"cpu": [
"x64"
],
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -1046,9 +1007,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "musl"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -1071,9 +1029,6 @@
"cpu": [
"x64"
],
- "libc": [
- "musl"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -1181,51 +1136,12 @@
"url": "https://opencollective.com/libvips"
}
},
- "node_modules/@jridgewell/gen-mapping": {
- "version": "0.3.13",
- "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
- "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
- "license": "MIT",
- "dependencies": {
- "@jridgewell/sourcemap-codec": "^1.5.0",
- "@jridgewell/trace-mapping": "^0.3.24"
- }
- },
- "node_modules/@jridgewell/remapping": {
- "version": "2.3.5",
- "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
- "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
- "license": "MIT",
- "dependencies": {
- "@jridgewell/gen-mapping": "^0.3.5",
- "@jridgewell/trace-mapping": "^0.3.24"
- }
- },
- "node_modules/@jridgewell/resolve-uri": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
- "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
- "license": "MIT",
- "engines": {
- "node": ">=6.0.0"
- }
- },
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
"integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==",
"license": "MIT"
},
- "node_modules/@jridgewell/trace-mapping": {
- "version": "0.3.31",
- "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
- "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
- "license": "MIT",
- "dependencies": {
- "@jridgewell/resolve-uri": "^3.1.0",
- "@jridgewell/sourcemap-codec": "^1.4.14"
- }
- },
"node_modules/@mdx-js/mdx": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz",
@@ -1425,6 +1341,28 @@
"node": ">=20"
}
},
+ "node_modules/@primer/css": {
+ "version": "22.3.3",
+ "resolved": "https://registry.npmjs.org/@primer/css/-/css-22.3.3.tgz",
+ "integrity": "sha512-PpWux+RXnqt2ub4F9NJ7JCc5e9VfWLJ9FEbN6wjJj+6XvVElcWw5aw/zW0cAyHmhzhkMqh4lT3WYw7gSRWPn7g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=16.0.0"
+ },
+ "peerDependencies": {
+ "@primer/primitives": "10.x || 11.x"
+ }
+ },
+ "node_modules/@primer/primitives": {
+ "version": "11.10.0",
+ "resolved": "https://registry.npmjs.org/@primer/primitives/-/primitives-11.10.0.tgz",
+ "integrity": "sha512-ihZxIst4Izry284StKFjpbYvX6R/IBFTLu+qiuAmR93PsKF2hdjoabhSYwKI8iCxOUx94jVP55bI3NoY7STjYQ==",
+ "license": "MIT",
+ "workspaces": [
+ ".",
+ "docs/storybook"
+ ]
+ },
"node_modules/@rolldown/binding-android-arm-eabi": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.13.tgz",
@@ -1528,9 +1466,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -1547,9 +1482,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -1566,9 +1498,6 @@
"cpu": [
"ppc64"
],
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -1585,9 +1514,6 @@
"cpu": [
"s390x"
],
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -1604,9 +1530,6 @@
"cpu": [
"x64"
],
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -1623,9 +1546,6 @@
"cpu": [
"x64"
],
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -1789,335 +1709,6 @@
"integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==",
"license": "MIT"
},
- "node_modules/@tailwindcss/node": {
- "version": "4.3.3",
- "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz",
- "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==",
- "license": "MIT",
- "dependencies": {
- "@jridgewell/remapping": "^2.3.5",
- "enhanced-resolve": "^5.24.1",
- "jiti": "^2.7.0",
- "lightningcss": "1.32.0",
- "magic-string": "^0.30.21",
- "source-map-js": "^1.2.1",
- "tailwindcss": "4.3.3"
- }
- },
- "node_modules/@tailwindcss/oxide": {
- "version": "4.3.3",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz",
- "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==",
- "license": "MIT",
- "engines": {
- "node": ">= 20"
- },
- "optionalDependencies": {
- "@tailwindcss/oxide-android-arm64": "4.3.3",
- "@tailwindcss/oxide-darwin-arm64": "4.3.3",
- "@tailwindcss/oxide-darwin-x64": "4.3.3",
- "@tailwindcss/oxide-freebsd-x64": "4.3.3",
- "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3",
- "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3",
- "@tailwindcss/oxide-linux-arm64-musl": "4.3.3",
- "@tailwindcss/oxide-linux-x64-gnu": "4.3.3",
- "@tailwindcss/oxide-linux-x64-musl": "4.3.3",
- "@tailwindcss/oxide-wasm32-wasi": "4.3.3",
- "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3",
- "@tailwindcss/oxide-win32-x64-msvc": "4.3.3"
- }
- },
- "node_modules/@tailwindcss/oxide-android-arm64": {
- "version": "4.3.3",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz",
- "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">= 20"
- }
- },
- "node_modules/@tailwindcss/oxide-darwin-arm64": {
- "version": "4.3.3",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz",
- "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 20"
- }
- },
- "node_modules/@tailwindcss/oxide-darwin-x64": {
- "version": "4.3.3",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz",
- "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 20"
- }
- },
- "node_modules/@tailwindcss/oxide-freebsd-x64": {
- "version": "4.3.3",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz",
- "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">= 20"
- }
- },
- "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
- "version": "4.3.3",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz",
- "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==",
- "cpu": [
- "arm"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 20"
- }
- },
- "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
- "version": "4.3.3",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz",
- "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==",
- "cpu": [
- "arm64"
- ],
- "libc": [
- "glibc"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 20"
- }
- },
- "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
- "version": "4.3.3",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz",
- "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==",
- "cpu": [
- "arm64"
- ],
- "libc": [
- "musl"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 20"
- }
- },
- "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
- "version": "4.3.3",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz",
- "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==",
- "cpu": [
- "x64"
- ],
- "libc": [
- "glibc"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 20"
- }
- },
- "node_modules/@tailwindcss/oxide-linux-x64-musl": {
- "version": "4.3.3",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz",
- "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==",
- "cpu": [
- "x64"
- ],
- "libc": [
- "musl"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 20"
- }
- },
- "node_modules/@tailwindcss/oxide-wasm32-wasi": {
- "version": "4.3.3",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz",
- "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==",
- "bundleDependencies": [
- "@napi-rs/wasm-runtime",
- "@emnapi/core",
- "@emnapi/runtime",
- "@tybys/wasm-util",
- "@emnapi/wasi-threads",
- "tslib"
- ],
- "cpu": [
- "wasm32"
- ],
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@emnapi/core": "^1.11.1",
- "@emnapi/runtime": "^1.11.1",
- "@emnapi/wasi-threads": "^1.2.2",
- "@napi-rs/wasm-runtime": "^1.1.4",
- "@tybys/wasm-util": "^0.10.2",
- "tslib": "^2.8.1"
- },
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
- "version": "1.11.1",
- "inBundle": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@emnapi/wasi-threads": "1.2.2",
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
- "version": "1.11.1",
- "inBundle": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
- "version": "1.2.2",
- "inBundle": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
- "version": "1.1.4",
- "inBundle": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@tybys/wasm-util": "^0.10.1"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/Brooooooklyn"
- },
- "peerDependencies": {
- "@emnapi/core": "^1.7.1",
- "@emnapi/runtime": "^1.7.1"
- }
- },
- "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
- "version": "0.10.2",
- "inBundle": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
- "version": "2.8.1",
- "inBundle": true,
- "license": "0BSD",
- "optional": true
- },
- "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
- "version": "4.3.3",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz",
- "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 20"
- }
- },
- "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
- "version": "4.3.3",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz",
- "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 20"
- }
- },
- "node_modules/@tailwindcss/vite": {
- "version": "4.3.3",
- "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz",
- "integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==",
- "license": "MIT",
- "dependencies": {
- "@tailwindcss/node": "4.3.3",
- "@tailwindcss/oxide": "4.3.3",
- "tailwindcss": "4.3.3"
- },
- "peerDependencies": {
- "vite": "^5.2.0 || ^6 || ^7 || ^8"
- }
- },
"node_modules/@tybys/wasm-util": {
"version": "0.10.4",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.4.tgz",
@@ -2433,6 +2024,16 @@
"@jridgewell/sourcemap-codec": "^1.6.0"
}
},
+ "node_modules/axe-core": {
+ "version": "4.14.0",
+ "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.14.0.tgz",
+ "integrity": "sha512-9WTZxEjsZ7b13TH8JPmbV2z8CHbl80/2hm3XPEG4JgNdQLK81IBRXmSxHfMAOkSqQeRxT/0dwNDz2GOm3zzpcQ==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/axobject-query": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
@@ -2851,19 +2452,6 @@
"node": ">=4"
}
},
- "node_modules/enhanced-resolve": {
- "version": "5.24.3",
- "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.3.tgz",
- "integrity": "sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ==",
- "license": "MIT",
- "dependencies": {
- "graceful-fs": "^4.2.4",
- "tapable": "^2.3.3"
- },
- "engines": {
- "node": ">=10.13.0"
- }
- },
"node_modules/entities": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
@@ -3183,12 +2771,6 @@
"integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==",
"license": "ISC"
},
- "node_modules/graceful-fs": {
- "version": "4.2.11",
- "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
- "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
- "license": "ISC"
- },
"node_modules/h3": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/h3/-/h3-1.15.11.tgz",
@@ -3558,270 +3140,12 @@
],
"license": "MIT"
},
- "node_modules/jiti": {
- "version": "2.7.0",
- "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
- "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
- "license": "MIT",
- "bin": {
- "jiti": "lib/jiti-cli.mjs"
- }
- },
"node_modules/jsonc-parser": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz",
"integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==",
"license": "MIT"
},
- "node_modules/lightningcss": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
- "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
- "license": "MPL-2.0",
- "dependencies": {
- "detect-libc": "^2.0.3"
- },
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- },
- "optionalDependencies": {
- "lightningcss-android-arm64": "1.32.0",
- "lightningcss-darwin-arm64": "1.32.0",
- "lightningcss-darwin-x64": "1.32.0",
- "lightningcss-freebsd-x64": "1.32.0",
- "lightningcss-linux-arm-gnueabihf": "1.32.0",
- "lightningcss-linux-arm64-gnu": "1.32.0",
- "lightningcss-linux-arm64-musl": "1.32.0",
- "lightningcss-linux-x64-gnu": "1.32.0",
- "lightningcss-linux-x64-musl": "1.32.0",
- "lightningcss-win32-arm64-msvc": "1.32.0",
- "lightningcss-win32-x64-msvc": "1.32.0"
- }
- },
- "node_modules/lightningcss-android-arm64": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
- "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
- "cpu": [
- "arm64"
- ],
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-darwin-arm64": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
- "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
- "cpu": [
- "arm64"
- ],
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-darwin-x64": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
- "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
- "cpu": [
- "x64"
- ],
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-freebsd-x64": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
- "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
- "cpu": [
- "x64"
- ],
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-arm-gnueabihf": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
- "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
- "cpu": [
- "arm"
- ],
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-arm64-gnu": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
- "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
- "cpu": [
- "arm64"
- ],
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-arm64-musl": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
- "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
- "cpu": [
- "arm64"
- ],
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-x64-gnu": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
- "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
- "cpu": [
- "x64"
- ],
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-x64-musl": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
- "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
- "cpu": [
- "x64"
- ],
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-win32-arm64-msvc": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
- "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
- "cpu": [
- "arm64"
- ],
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-win32-x64-msvc": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
- "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
- "cpu": [
- "x64"
- ],
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
"node_modules/longest-streak": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
@@ -3841,15 +3165,6 @@
"node": "20 || >=22"
}
},
- "node_modules/magic-string": {
- "version": "0.30.21",
- "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
- "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
- "license": "MIT",
- "dependencies": {
- "@jridgewell/sourcemap-codec": "^1.5.5"
- }
- },
"node_modules/magicast": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.5.tgz",
@@ -5892,25 +5207,6 @@
"url": "https://opencollective.com/svgo"
}
},
- "node_modules/tailwindcss": {
- "version": "4.3.3",
- "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz",
- "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==",
- "license": "MIT"
- },
- "node_modules/tapable": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
- "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
- "license": "MIT",
- "engines": {
- "node": ">=6"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/webpack"
- }
- },
"node_modules/tiny-inflate": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
@@ -6540,9 +5836,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "glibc"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -6563,9 +5856,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "musl"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -6586,9 +5876,6 @@
"cpu": [
"x64"
],
- "libc": [
- "glibc"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -6609,9 +5896,6 @@
"cpu": [
"x64"
],
- "libc": [
- "musl"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
diff --git a/docs/package.json b/docs/package.json
index 2c4975010..7d9609718 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -10,19 +10,22 @@
"astro": "astro",
"test": "node --test tests/build-output.test.mjs && npx playwright test",
"test:build": "node --test tests/build-output.test.mjs",
- "test:e2e": "npx playwright test"
+ "test:e2e": "npx playwright test",
+ "screenshots": "node scripts/capture-screenshots.mjs"
},
"dependencies": {
"@astrojs/markdown-remark": "^7.3.2",
"@astrojs/rss": "^4.0.19",
"@astrojs/sitemap": "^3.7.4",
- "@tailwindcss/vite": "^4.3.3",
+ "@fontsource-variable/mona-sans": "^5.3.0",
+ "@primer/css": "^22.3.3",
+ "@primer/primitives": "^11.10.0",
"astro": "^7.3.8",
"sharp": "^0.35.5",
- "tailwindcss": "^4.1.0",
"unist-util-visit": "^5.1.0"
},
"devDependencies": {
+ "@axe-core/playwright": "^4.14.0",
"@playwright/test": "^1.64.0",
"pagefind": "^1.5.2"
}
diff --git a/docs/scripts/capture-screenshots.mjs b/docs/scripts/capture-screenshots.mjs
new file mode 100644
index 000000000..baf3bbc31
--- /dev/null
+++ b/docs/scripts/capture-screenshots.mjs
@@ -0,0 +1,226 @@
+#!/usr/bin/env node
+/**
+ * Captures screenshots of every layout page on mobile, tablet, and desktop in light and dark mode,
+ * and writes a browsable gallery (index.html) so maintainers can review rendering at a glance.
+ *
+ * Usage (from docs/):
+ * npm run build && npm run screenshots
+ * npm run screenshots -- --out visual-review --base http://localhost:4321 --full-height 3200
+ *
+ * Options:
+ * --base Site origin to capture (default http://localhost:4321). If nothing is listening
+ * there, `astro preview` is started automatically and stopped afterwards.
+ * --out Output directory, relative to docs/ (default visual-review). Gitignored; only files this script generates are replaced.
+ * --full-height Max captured page height in px for full-page shots (default 3200).
+ * --only Comma-separated layout ids to capture (e.g. home,blog-post).
+ */
+import { chromium } from '@playwright/test';
+import { spawn } from 'node:child_process';
+import fs from 'node:fs';
+import path from 'node:path';
+
+const args = Object.fromEntries(
+ process.argv.slice(2).reduce((acc, a, i, all) => {
+ if (a.startsWith('--')) acc.push([a.slice(2), all[i + 1] && !all[i + 1].startsWith('--') ? all[i + 1] : 'true']);
+ return acc;
+ }, []),
+);
+const BASE = (args.base ?? 'http://localhost:4321').replace(/\/$/, '');
+const OUT = path.resolve(args.out ?? 'visual-review');
+const MAX_H = Number(args['full-height'] ?? 3200);
+const ONLY = args.only ? new Set(args.only.split(',')) : null;
+
+const DEVICES = [
+ { id: 'mobile', label: 'Mobile', viewport: { width: 390, height: 844 }, deviceScaleFactor: 2, isMobile: true, hasTouch: true },
+ { id: 'tablet', label: 'Tablet', viewport: { width: 820, height: 1180 }, deviceScaleFactor: 2, isMobile: true, hasTouch: true },
+ { id: 'desktop', label: 'Desktop', viewport: { width: 1440, height: 900 }, deviceScaleFactor: 1 },
+];
+const THEMES = ['light', 'dark'];
+
+// Each layout gets one representative URL. `before` prepares interactive states.
+const LAYOUTS = [
+ { id: 'home', title: 'Landing page', url: '/squad/' },
+ { id: 'blog-index', title: 'Blog index (hub)', url: '/squad/blog/' },
+ { id: 'blog-filtered', title: 'Blog index, filtered by category', url: '/squad/blog/?category=Release' },
+ { id: 'blog-empty', title: 'Blog index, empty search', url: '/squad/blog/?q=%40%40%40%40' },
+ { id: 'blog-post', title: 'Blog post', url: '/squad/blog/004-v020-release/' },
+ { id: 'docs-page', title: 'Docs page (sidebar section, breadcrumb, pager)', url: '/squad/docs/features/skills/' },
+ { id: 'docs-first', title: 'Docs page, first in section (single pager link)', url: '/squad/docs/get-started/five-minute-start/' },
+ { id: 'docs-root', title: 'Docs root page (no breadcrumb)', url: '/squad/docs/whatsnew/' },
+ { id: 'docs-reference', title: 'Docs reference (tables and code)', url: '/squad/docs/reference/cli/' },
+ { id: 'not-found', title: '404 page', url: '/squad/this-page-does-not-exist/' },
+ {
+ id: 'search-empty',
+ title: 'Site search dialog, suggestions',
+ url: '/squad/',
+ viewportOnly: true,
+ before: async (page) => {
+ await page.evaluate(() => document.getElementById('search-btn')?.click());
+ await page.locator('#search-modal').waitFor({ state: 'visible' });
+ },
+ },
+ {
+ id: 'search-results',
+ title: 'Site search dialog, results',
+ url: '/squad/',
+ viewportOnly: true,
+ before: async (page) => {
+ await page.evaluate(() => document.getElementById('search-btn')?.click());
+ await page.locator('#search-input').fill('skills');
+ await page.locator('#search-results a.search-result').first().waitFor({ state: 'visible', timeout: 15000 });
+ },
+ },
+ {
+ id: 'nav-drawer',
+ title: 'Docs sidebar drawer (mobile and tablet)',
+ url: '/squad/docs/features/skills/',
+ viewportOnly: true,
+ devices: ['mobile', 'tablet'],
+ before: async (page) => {
+ await page.locator('#mobile-nav-toggle').click();
+ await page.locator('#sidebar').waitFor({ state: 'visible' });
+ },
+ },
+].filter((l) => !ONLY || ONLY.has(l.id));
+
+async function reachable(url) {
+ try {
+ const r = await fetch(url + '/squad/');
+ return r.ok;
+ } catch {
+ return false;
+ }
+}
+
+let server;
+async function ensureServer() {
+ if (await reachable(BASE)) return;
+ const port = new URL(BASE).port || '4321';
+ console.log(`No server at ${BASE}; starting astro preview on :${port}`);
+ server = spawn(process.platform === 'win32' ? 'npx.cmd' : 'npx', ['astro', 'preview', '--port', port], { stdio: 'ignore', shell: process.platform === 'win32' });
+ for (let i = 0; i < 40; i++) {
+ if (await reachable(BASE)) return;
+ await new Promise((r) => setTimeout(r, 500));
+ }
+ throw new Error('Preview server did not start. Run `npm run build` first.');
+}
+
+async function settle(page) {
+ await page.evaluate(() => document.fonts?.ready);
+ // Reveal scroll-triggered sections so full-page shots are complete
+ await page.evaluate(() => document.querySelectorAll('.reveal').forEach((e) => e.classList.add('visible')));
+ await page.waitForTimeout(250);
+}
+
+async function main() {
+ await ensureServer();
+ // Only clear files this script generates so a custom --out never deletes unrelated files
+ fs.mkdirSync(OUT, { recursive: true });
+ for (const f of fs.readdirSync(OUT)) {
+ if (/__(mobile|tablet|desktop)__(light|dark)\.png$/.test(f) || f === 'index.html') fs.rmSync(path.join(OUT, f));
+ }
+
+ const browser = await chromium.launch();
+ const manifest = [];
+ let count = 0;
+
+ for (const theme of THEMES) {
+ for (const device of DEVICES) {
+ const { id: _id, label: _label, ...ctxOptions } = device;
+ const context = await browser.newContext({ ...ctxOptions, colorScheme: theme, reducedMotion: 'reduce' });
+ await context.addInitScript((t) => localStorage.setItem('theme', t), theme);
+ const page = await context.newPage();
+ const errors = [];
+ page.on('pageerror', (e) => errors.push(e.message));
+ page.on('console', (m) => {
+ // The 404 route intentionally returns HTTP 404, which the browser logs as a resource error
+ if (m.type() === 'error' && !/status of 404/.test(m.text())) errors.push(m.text());
+ });
+
+ for (const layout of LAYOUTS) {
+ if (layout.devices && !layout.devices.includes(device.id)) continue;
+ await page.goto(BASE + layout.url, { waitUntil: 'load' });
+ if (layout.before) await layout.before(page);
+ await settle(page);
+ const file = `${layout.id}__${device.id}__${theme}.png`;
+ const target = path.join(OUT, file);
+ if (layout.viewportOnly) {
+ await page.screenshot({ path: target });
+ } else {
+ const height = await page.evaluate(() => document.documentElement.scrollHeight);
+ if (height > MAX_H) {
+ await page.screenshot({ path: target, clip: { x: 0, y: 0, width: device.viewport.width, height: MAX_H }, fullPage: true });
+ } else {
+ await page.screenshot({ path: target, fullPage: true });
+ }
+ }
+ manifest.push({ layout: layout.id, title: layout.title, url: layout.url, device: device.id, theme, file, errors: [...errors] });
+ errors.length = 0;
+ count++;
+ process.stdout.write(`\r${count} screenshots`);
+ }
+ await context.close();
+ }
+ }
+ await browser.close();
+ console.log();
+
+ writeGallery(manifest);
+ const withErrors = manifest.filter((m) => m.errors.length);
+ console.log(`Saved ${count} screenshots to ${OUT}`);
+ console.log(`Gallery: ${path.join(OUT, 'index.html')}`);
+ if (withErrors.length) {
+ console.log(`Console/page errors on ${withErrors.length} captures:`);
+ withErrors.forEach((m) => console.log(` ${m.file}: ${m.errors.join(' | ').slice(0, 160)}`));
+ process.exitCode = 1;
+ }
+}
+
+function writeGallery(manifest) {
+ const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
+ const layouts = [...new Set(manifest.map((m) => m.layout))];
+ const sections = layouts
+ .map((id) => {
+ const items = manifest.filter((m) => m.layout === id);
+ const title = items[0].title;
+ const rows = THEMES.map((theme) => {
+ const cells = DEVICES.map((d) => {
+ const m = items.find((i) => i.theme === theme && i.device === d.id);
+ return m
+ ? `
${esc(items[0].url)}