From 851e4d8c8b878e44826b524ef27c47b798a8985c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20HOUZ=C3=89?= Date: Sun, 8 Mar 2026 13:27:25 +0100 Subject: [PATCH 01/24] =?UTF-8?q?feat:=20promotion=20polish=20=E2=80=94=20?= =?UTF-8?q?docs=20UX,=20CLI=20hyperlinks,=20completions=20auto-install?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Docs / VitePress - ComparisonTable: add per-row descriptions, target=_blank links, bump font sizes, fix hover contrast (text stays readable on violet bg) - VersionBadge: bump badge font-size 13→15 px - InstallSection: subtitle and step-label font-size 15→16 px - ProductionCta: body text 15→16 px, replace ↗ emoji with inline SVG arrow (iOS Safari fix), switch button to inline-flex - TestimonialsSection: fix hover border/shadow, replace arrow emoji with SVG, fix cta-btn hover contrast (force color:#fff) - RichFooter: replace ↗ emoji with SVG arrow on fulll.fr link ## CLI / core - help colorizer: colorize all bare https:// URLs in any description line (not only Docs: prefix) - upgrade output: render all post-upgrade URLs in cyan underline via upgradeLink() helper - refreshCompletions: always create completion file (install + refresh), not only overwrite existing ones - output.ts: replay command summary now links to https://fulll.github.io/github-code-search/ ## Tests - output.test.ts: update summary assertion to match new link text - upgrade.test.ts: update refreshCompletions tests to reflect 'always install' behaviour --- .vscode/settings.json | 8 + docs/.vitepress/theme/ComparisonTable.vue | 151 ++++++++++++++++-- docs/.vitepress/theme/InstallSection.vue | 4 +- docs/.vitepress/theme/ProductionCta.vue | 27 +++- docs/.vitepress/theme/RichFooter.vue | 24 ++- docs/.vitepress/theme/TestimonialsSection.vue | 46 +++++- docs/.vitepress/theme/VersionBadge.vue | 2 +- github-code-search.ts | 5 +- src/output.test.ts | 16 +- src/output.ts | 2 +- src/upgrade.test.ts | 24 ++- src/upgrade.ts | 26 +-- 12 files changed, 284 insertions(+), 51 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..07b5b75 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "chat.tools.terminal.autoApprove": { + "/^git checkout -b feat/promotion-polish && git add -A && git diff --cached --stat$/": { + "approve": true, + "matchCommandLine": true + } + } +} diff --git a/docs/.vitepress/theme/ComparisonTable.vue b/docs/.vitepress/theme/ComparisonTable.vue index f4dfb64..fa58ee7 100644 --- a/docs/.vitepress/theme/ComparisonTable.vue +++ b/docs/.vitepress/theme/ComparisonTable.vue @@ -1,19 +1,69 @@ @@ -48,7 +98,39 @@ const ROWS: Row[] = [ - {{ row.feature }} + + + + {{ row.feature }} + + + {{ row.desc }} + + + {{ row.feature }} + {{ row.desc }} + + @@ -109,7 +191,7 @@ const ROWS: Row[] = [ .ct-intro { margin: 0; padding: 20px 24px 18px; - font-size: 14px; + font-size: 15.5px; line-height: 1.7; color: var(--vp-c-text-2); border-bottom: 1px solid var(--vp-c-divider); @@ -160,7 +242,7 @@ thead tr { } .ct-tool-name { - font-size: 13px; + font-size: 14px; font-weight: 600; white-space: nowrap; } @@ -210,12 +292,55 @@ thead tr { } .ct-feature { - padding: 14px 24px; - font-size: 14px; + padding: 12px 24px; + font-size: 15px; color: var(--vp-c-text-1); line-height: 1.5; } +.ct-feature-link, +.ct-feature-plain { + display: flex; + flex-direction: column; + gap: 3px; + color: var(--vp-c-text-1); + text-decoration: none; +} + +.ct-feature-link:hover { + color: var(--vp-c-text-1); +} + +.ct-feature-title { + display: inline-flex; + align-items: center; + gap: 5px; + font-size: 15px; + font-weight: 600; + color: inherit; +} + +.ct-feature-desc { + font-size: 13px; + font-weight: 400; + color: var(--vp-c-text-3); + line-height: 1.45; +} + +.ct-feature-link:hover .ct-feature-desc { + color: var(--vp-c-text-2); +} + +.ct-ext-icon { + opacity: 0.35; + flex-shrink: 0; + transition: opacity 0.15s; +} + +.ct-feature-link:hover .ct-ext-icon { + opacity: 0.75; +} + .ct-cell { padding: 14px 12px; text-align: center; diff --git a/docs/.vitepress/theme/InstallSection.vue b/docs/.vitepress/theme/InstallSection.vue index 2a5a364..639f68a 100644 --- a/docs/.vitepress/theme/InstallSection.vue +++ b/docs/.vitepress/theme/InstallSection.vue @@ -232,7 +232,7 @@ function copySearch() { .is-subtitle { margin: 0; - font-size: 15px; + font-size: 16px; color: var(--vp-c-text-2); line-height: 1.65; } @@ -284,7 +284,7 @@ function copySearch() { .is-step-label { margin: 8px 0 10px; - font-size: 15px; + font-size: 16px; font-weight: 600; color: var(--vp-c-text-1); } diff --git a/docs/.vitepress/theme/ProductionCta.vue b/docs/.vitepress/theme/ProductionCta.vue index fdd0808..4d33af8 100644 --- a/docs/.vitepress/theme/ProductionCta.vue +++ b/docs/.vitepress/theme/ProductionCta.vue @@ -45,7 +45,22 @@ target="_blank" rel="noopener noreferrer" > - Share your story ↗ + Share your story + @@ -133,7 +148,7 @@ .cta-text { margin: 0; - font-size: 15px; + font-size: 16px; line-height: 1.65; color: var(--vp-c-text-2); } @@ -149,7 +164,9 @@ /* ── Button ────────────────────────────────────────────────────────────── */ .cta-btn { flex-shrink: 0; - display: inline-block; + display: inline-flex; + align-items: center; + gap: 7px; padding: 10px 22px; border-radius: 9999px; background: linear-gradient(135deg, #9933ff 0%, #7a1fd4 100%); @@ -165,6 +182,10 @@ background 0.2s; } +.cta-btn-arrow { + flex-shrink: 0; +} + .cta-btn:hover { background: linear-gradient(135deg, #aa44ff 0%, #9933ff 100%); box-shadow: 0 6px 28px rgba(153, 51, 255, 0.5); diff --git a/docs/.vitepress/theme/RichFooter.vue b/docs/.vitepress/theme/RichFooter.vue index 87ad0a3..2e3105a 100644 --- a/docs/.vitepress/theme/RichFooter.vue +++ b/docs/.vitepress/theme/RichFooter.vue @@ -108,7 +108,24 @@ >
  • - fulll.fr ↗ + + fulll.fr + +
  • @@ -228,6 +245,11 @@ const year = new Date().getFullYear(); opacity: 0.7; } +.rf-ext-arrow { + flex-shrink: 0; + opacity: 0.6; +} + /* ── Responsive ────────────────────────────────────────────────────────── */ @media (max-width: 840px) { .rf-inner { diff --git a/docs/.vitepress/theme/TestimonialsSection.vue b/docs/.vitepress/theme/TestimonialsSection.vue index dabe50f..126afb4 100644 --- a/docs/.vitepress/theme/TestimonialsSection.vue +++ b/docs/.vitepress/theme/TestimonialsSection.vue @@ -44,7 +44,22 @@ target="_blank" rel="noopener noreferrer" > - Share your story → + Share your story + @@ -132,12 +147,19 @@ const testimonials = [ gap: 20px; transition: transform 0.18s, - box-shadow 0.18s; + box-shadow 0.18s, + border-color 0.18s; } .ts-card:hover { transform: translateY(-2px); - box-shadow: 0 6px 24px rgba(153, 51, 255, 0.1); + box-shadow: 0 6px 24px rgba(153, 51, 255, 0.14); + border-color: rgba(153, 51, 255, 0.32); +} + +.dark .ts-card:hover { + border-color: rgba(204, 136, 255, 0.3); + box-shadow: 0 6px 28px rgba(153, 51, 255, 0.2); } /* ── Quote ─────────────────────────────────────────────────────────────── */ @@ -226,19 +248,29 @@ const testimonials = [ } .ts-cta-btn { - display: inline-block; + display: inline-flex; + align-items: center; + gap: 6px; padding: 8px 18px; border-radius: 8px; background: var(--vp-c-brand-1); - color: #fff; + color: #fff !important; font-size: 13px; font-weight: 600; - text-decoration: none; - transition: background 0.15s; + text-decoration: none !important; + transition: + background 0.15s, + box-shadow 0.15s; } .ts-cta-btn:hover { background: #7a1fd4; + color: #fff !important; + box-shadow: 0 4px 16px rgba(153, 51, 255, 0.35); +} + +.ts-cta-arrow { + flex-shrink: 0; } /* ── Responsive ────────────────────────────────────────────────────────── */ diff --git a/docs/.vitepress/theme/VersionBadge.vue b/docs/.vitepress/theme/VersionBadge.vue index 93a7094..a80b208 100644 --- a/docs/.vitepress/theme/VersionBadge.vue +++ b/docs/.vitepress/theme/VersionBadge.vue @@ -39,7 +39,7 @@ const releaseLink = withBase(`/blog/${__LATEST_BLOG_SLUG__}`); border: 1px solid rgba(153, 51, 255, 0.35); background: rgba(153, 51, 255, 0.08); color: var(--vp-c-brand-1); - font-size: 13px; + font-size: 15px; font-weight: 500; text-decoration: none; transition: diff --git a/github-code-search.ts b/github-code-search.ts index 7dea71d..889fc72 100644 --- a/github-code-search.ts +++ b/github-code-search.ts @@ -64,7 +64,8 @@ function colorDesc(s: string): string { const exampleMatch = line.match(/^(\s*Example:\s*)(.+)$/); if (exampleMatch) return pc.dim(exampleMatch[1]) + pc.italic(exampleMatch[2]); if (/^\s+(e\.g\.|repoA|myorg\/|squad-|chapter-)/.test(line)) return pc.dim(line); - return line; + // Colorize any remaining bare URL (http/https) anywhere in the line + return line.replace(/(https?:\/\/\S+)/g, (url) => pc.cyan(pc.underline(url))); }) .join("\n"); } @@ -354,7 +355,7 @@ program const { refreshCompletions } = await import("./src/upgrade.ts"); const refreshedPath = await refreshCompletions(detectShell(), undefined, opts.debug); if (refreshedPath) { - process.stdout.write(`✓ Shell completions refreshed at ${refreshedPath}\n`); + process.stdout.write(`✓ Shell completions installed/refreshed at ${refreshedPath}\n`); } } catch (e: unknown) { const message = e instanceof Error ? e.message : String(e); diff --git a/src/output.test.ts b/src/output.test.ts index 476825f..0db1a7a 100644 --- a/src/output.test.ts +++ b/src/output.test.ts @@ -189,7 +189,9 @@ describe("buildReplayDetails", () => { const groups = [makeGroup("myorg/repoA", ["a.ts"])]; const out = buildReplayDetails(groups, QUERY, ORG, new Set(), new Set()); expect(out).toContain("
    "); - expect(out).toContain("replay command"); + expect(out).toContain( + "[github-code-search](https://fulll.github.io/github-code-search/) replay command", + ); expect(out).toContain("```bash"); expect(out).toContain("
    "); }); @@ -293,7 +295,9 @@ describe("buildMarkdownOutput", () => { const groups = [makeGroup("myorg/repoA", ["a.ts"])]; const out = buildMarkdownOutput(groups, QUERY, ORG, new Set(), new Set()); expect(out).toContain("
    "); - expect(out).toContain("replay command"); + expect(out).toContain( + "[github-code-search](https://fulll.github.io/github-code-search/) replay command", + ); expect(out).toContain("```bash"); expect(out).toContain("
    "); }); @@ -309,7 +313,9 @@ describe("buildMarkdownOutput", () => { const groups = [makeGroup("myorg/repoA", ["src/foo.ts"])]; const out = buildMarkdownOutput(groups, QUERY, ORG, new Set(), new Set(), "repo-only"); expect(out).toContain("
    "); - expect(out).toContain("replay command"); + expect(out).toContain( + "[github-code-search](https://fulll.github.io/github-code-search/) replay command", + ); }); it("repo-only mode returns newline-terminated list of repo names followed by replay", () => { @@ -317,7 +323,9 @@ describe("buildMarkdownOutput", () => { const out = buildMarkdownOutput(groups, QUERY, ORG, new Set(), new Set(), "repo-only"); expect(out).toContain("myorg/repoA\nmyorg/repoB\n"); expect(out).toContain("
    "); - expect(out).toContain("replay command"); + expect(out).toContain( + "[github-code-search](https://fulll.github.io/github-code-search/) replay command", + ); }); it("repo-only mode returns empty string when no groups are selected", () => { diff --git a/src/output.ts b/src/output.ts index 75c8ab2..73152c1 100644 --- a/src/output.ts +++ b/src/output.ts @@ -108,7 +108,7 @@ export function buildReplayDetails( const shellCmd = raw.replace(/^# Replay:\n/, ""); return [ "
    ", - "replay command", + "[github-code-search](https://fulll.github.io/github-code-search/) replay command", "", "```bash", shellCmd, diff --git a/src/upgrade.test.ts b/src/upgrade.test.ts index a3ff77f..1912cea 100644 --- a/src/upgrade.test.ts +++ b/src/upgrade.test.ts @@ -369,7 +369,10 @@ describe("performUpgrade — download path", () => { /** Returns a release mock + matching asset name for the current platform. */ function mockReleaseAndDownload(downloadResponse: Response): void { - const platformMap: Record = { darwin: "macos", win32: "windows" }; + const platformMap: Record = { + darwin: "macos", + win32: "windows", + }; const p = platformMap[process.platform] ?? process.platform; const suffix = p === "windows" ? ".exe" : ""; const assetName = `github-code-search-${p}-${process.arch}${suffix}`; @@ -410,7 +413,10 @@ describe("performUpgrade — download path", () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (Bun as any).write = async () => 3; // eslint-disable-next-line @typescript-eslint/no-explicit-any - (Bun as any).spawnSync = () => ({ exitCode: 0, stderr: { toString: () => "" } }); + (Bun as any).spawnSync = () => ({ + exitCode: 0, + stderr: { toString: () => "" }, + }); const stdoutWrites: string[] = []; const origWrite = process.stdout.write.bind(process.stdout); @@ -456,11 +462,13 @@ describe("refreshCompletions", () => { expect(await refreshCompletions(null)).toBeNull(); }); - it("returns null when the completion file does not exist (never installed)", async () => { + it("creates the file even if it did not already exist", async () => { const tmp = await mkdtemp(join(tmpdir(), "gcs-test-")); try { const result = await refreshCompletions("fish", tmp); - expect(result).toBeNull(); + const dir = join(tmp, ".config", "fish", "completions"); + expect(result).toBe(join(dir, "github-code-search.fish")); + expect(existsSync(join(dir, "github-code-search.fish"))).toBe(true); } finally { rmSync(tmp, { recursive: true, force: true }); } @@ -534,7 +542,7 @@ describe("refreshCompletions", () => { try { await refreshCompletions("bash", tmp, true); expect( - stdoutWrites.some((s) => s.includes("[debug]") && s.includes("skipping refresh")), + stdoutWrites.some((s) => s.includes("[debug]") && s.includes("installing completions")), ).toBe(true); } finally { process.stdout.write = origWrite; @@ -557,7 +565,7 @@ describe("refreshCompletions", () => { await refreshCompletions("fish", tmp, true); expect( - stdoutWrites.some((s) => s.includes("[debug]") && s.includes("refreshed completions")), + stdoutWrites.some((s) => s.includes("[debug]") && s.includes("refreshing completions")), ).toBe(true); } finally { process.stdout.write = origWrite; @@ -565,12 +573,12 @@ describe("refreshCompletions", () => { } }); - it("does not create the file if it did not already exist", async () => { + it("creates the file even if it did not already exist (always installs)", async () => { const tmp = await mkdtemp(join(tmpdir(), "gcs-test-")); try { await refreshCompletions("fish", tmp); const dir = join(tmp, ".config", "fish", "completions"); - expect(existsSync(join(dir, "github-code-search.fish"))).toBe(false); + expect(existsSync(join(dir, "github-code-search.fish"))).toBe(true); } finally { rmSync(tmp, { recursive: true, force: true }); } diff --git a/src/upgrade.ts b/src/upgrade.ts index df7dac7..35a2bfd 100644 --- a/src/upgrade.ts +++ b/src/upgrade.ts @@ -1,8 +1,13 @@ import { existsSync, mkdirSync } from "node:fs"; import { dirname } from "node:path"; +import pc from "picocolors"; import { generateCompletion, getCompletionFilePath } from "./completions.ts"; import type { Shell } from "./completions.ts"; +/** Renders a hyperlink in cyan+underline when stdout is a TTY, plain otherwise. */ +const upgradeLink = (url: string): string => + process.stdout.isTTY ? pc.cyan(pc.underline(url)) : url; + // ─── Types ──────────────────────────────────────────────────────────────────── export interface ReleaseAsset { @@ -221,16 +226,16 @@ export async function performUpgrade( `Welcome to github-code-search ${latestVersion}!`, ``, `What's new in ${latestVersion}:`, - ` ${blogPostUrl(latestVersion)}`, + ` ${upgradeLink(blogPostUrl(latestVersion))}`, ``, `Release notes:`, - ` ${release.html_url}`, + ` ${upgradeLink(release.html_url)}`, ``, `Commit log:`, - ` https://github.com/fulll/github-code-search/compare/${currentVersion.startsWith("v") ? currentVersion : `v${currentVersion}`}...${latestVersion}`, + ` ${upgradeLink(`https://github.com/fulll/github-code-search/compare/${currentVersion.startsWith("v") ? currentVersion : `v${currentVersion}`}...${latestVersion}`)}`, ``, `Report a bug:`, - ` https://github.com/fulll/github-code-search/issues/new`, + ` ${upgradeLink("https://github.com/fulll/github-code-search/issues/new")}`, ``, `Run \`github-code-search --help\` to explore all options.`, ``, @@ -274,15 +279,18 @@ export async function refreshCompletions( const opts = homeDir ? { homeDir } : {}; const filePath = getCompletionFilePath(shell, opts); - if (!existsSync(filePath)) { - if (debug) - process.stdout.write(`[debug] no existing completions at ${filePath}, skipping refresh\n`); - return null; + const alreadyExists = existsSync(filePath); + if (debug) { + process.stdout.write( + alreadyExists + ? `[debug] refreshing completions at ${filePath}\n` + : `[debug] installing completions at ${filePath}\n`, + ); } const script = generateCompletion(shell); mkdirSync(dirname(filePath), { recursive: true }); await Bun.write(filePath, script); - if (debug) process.stdout.write(`[debug] refreshed completions at ${filePath}\n`); + if (debug) process.stdout.write(`[debug] completions written at ${filePath}\n`); return filePath; } From 82a37b853821be1e0f59b33484811a68f3fdcfa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20HOUZ=C3=89?= Date: Sun, 8 Mar 2026 17:07:42 +0100 Subject: [PATCH 02/24] Make VitePress docs fully WCAG 2.1 AA accessible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add ARIA tabs pattern (roving tabindex, keyboard nav) to UseCaseTabs - Add table semantics (caption, scope, aria-label) to ComparisonTable - Fix heading hierarchy and landmarks in InstallSection, ProductionCta, TestimonialsSection, HowItWorks - Fix contrast failures: .ct-feature-desc, .ct-tool-alt, .ts-role → text-1/2; .ts-avatar #CC88FF → #8833cc; .td-ps #9933ff → #aa55ff - Switch Shiki light theme to github-light-high-contrast (fixes #D73A49, #6A737D, #22863A tokens below 4.5:1); add CSS fallback overrides - aria-hidden="true" on decorative TerminalDemo - Add .sr-only utility and global :focus-visible ring to custom.css - Fix hero alt="" (decorative image) in docs/index.md - Add .github/workflows/a11y.yml (pa11y-ci, WCAG2AA, sitemap-driven) - Add .pa11yci.json with WCAG2AA config and F77 ignore (Mermaid SVG ids) - Add docs:a11y and docs:build:a11y scripts (VITEPRESS_HOSTNAME override so sitemap.xml uses localhost URLs during CI audit) - Split mermaid+d3 into dedicated Rollup chunk; raise chunkSizeWarningLimit to 2500 kB to silence legitimate Mermaid size warning - Increase HowItWorks step description font-size 14px → 15px --- .github/workflows/a11y.yml | 100 ++++++++++++++++++ .github/workflows/cd.yaml | 2 +- .github/workflows/ci.yaml | 2 +- .github/workflows/docs.yml | 8 +- .gitignore | 1 + .pa11yci.json | 17 +++ docs/.vitepress/config.mts | 49 ++++++++- docs/.vitepress/theme/ComparisonTable.vue | 23 ++-- docs/.vitepress/theme/HowItWorks.vue | 20 ++-- docs/.vitepress/theme/InstallSection.vue | 45 ++++---- docs/.vitepress/theme/ProductionCta.vue | 18 +++- docs/.vitepress/theme/TerminalDemo.vue | 11 +- docs/.vitepress/theme/TestimonialsSection.vue | 12 ++- docs/.vitepress/theme/UseCaseTabs.vue | 58 +++++++++- docs/.vitepress/theme/custom.css | 53 ++++++++++ docs/index.md | 2 +- package.json | 4 +- 17 files changed, 360 insertions(+), 65 deletions(-) create mode 100644 .github/workflows/a11y.yml create mode 100644 .pa11yci.json diff --git a/.github/workflows/a11y.yml b/.github/workflows/a11y.yml new file mode 100644 index 0000000..57153cd --- /dev/null +++ b/.github/workflows/a11y.yml @@ -0,0 +1,100 @@ +# Accessibility audit — WCAG 2.1 AA +# +# Runs pa11y-ci against the built VitePress docs to ensure the homepage and +# key pages remain WCAG 2.1 AA compliant. +# +# Triggers: +# - push to main touching docs/** or this workflow / config +# - every pull_request touching the same paths +# - workflow_dispatch (manual run) +# +# Strategy: +# 1. Build the docs with `bun run docs:build:a11y` (sets VITEPRESS_HOSTNAME= +# http://localhost:4173 so the generated sitemap.xml contains localhost +# URLs that pa11y-ci can reach directly) +# 2. Start `vitepress preview` in the background (serves on port 4173) +# 3. Wait until the server is accepting connections +# 4. Run pa11y-ci configured in .pa11yci.json (WCAG 2.1 AA, errors only) +# +# Chrome: Ubuntu-latest ships google-chrome-stable. We skip Puppeteer's own +# Chromium download (PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1) and point directly to +# the system Chrome via PUPPETEER_EXECUTABLE_PATH at runtime. This cuts ~200 MB +# from every CI run. +name: Accessibility audit (WCAG 2.1 AA) + +on: + push: + branches: [main] + paths: + - "docs/**" + - ".github/workflows/a11y.yml" + - ".pa11yci.json" + pull_request: + paths: + - "docs/**" + - ".github/workflows/a11y.yml" + - ".pa11yci.json" + workflow_dispatch: + +# One audit at a time per branch; cancel stale runs on new push. +concurrency: + group: a11y-${{ github.ref }} + cancel-in-progress: true + +jobs: + audit: + name: WCAG 2.1 AA pages audit + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Bun + uses: oven-sh/setup-bun@ecf28ddc73e819eb6fa29df6b34ef8921c743461 # v2.1.3cf28ddc73e819eb6fa29df6b34ef8921c743461 # v2.1.3 + with: + bun-version: latest + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Build docs (a11y — sitemap will use localhost URLs) + run: bun run docs:build:a11y + + # Start VitePress preview in the background; base URL: /github-code-search/ + # --port 4173 matches the URLs in .pa11yci.json + - name: Start VitePress preview server + run: bun run docs:preview -- --port 4173 & + + # Poll until the preview server responds (max 60 s = 30 × 2 s). + - name: Wait for preview server to be ready + run: | + echo "Waiting for VitePress preview on http://localhost:4173/github-code-search/ …" + for i in $(seq 1 30); do + if curl -sf http://localhost:4173/github-code-search/ > /dev/null 2>&1; then + echo "Server ready after $((i * 2)) seconds." + exit 0 + fi + echo "Attempt $i/30 — retrying in 2 s …" + sleep 2 + done + echo "ERROR: preview server did not start within 60 seconds." >&2 + exit 1 + + # Run the audit. The env vars tell Puppeteer (used by pa11y) to use the + # pre-installed system Chrome instead of downloading a Chromium binary. + - name: Run accessibility audit (pa11y-ci) + env: + PUPPETEER_EXECUTABLE_PATH: /usr/bin/google-chrome-stable + PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: "1" + run: bun run docs:a11y + + # Upload the pa11y-ci JSON report as an artifact so failures are + # easy to inspect without re-running the workflow. + - name: Upload audit report + if: always() + uses: actions/upload-artifact@v4 + with: + name: pa11y-ci-report + path: a11y-report.json + if-no-files-found: ignore diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml index edfb792..65832f1 100644 --- a/.github/workflows/cd.yaml +++ b/.github/workflows/cd.yaml @@ -43,7 +43,7 @@ jobs: uses: actions/checkout@v6 - name: Setup Bun - uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2.1.2 + uses: oven-sh/setup-bun@ecf28ddc73e819eb6fa29df6b34ef8921c743461 # v2.1.3d267786b128fe76c2f16a390aa2448b815359f3 # v2.1.2 with: bun-version: latest diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c921956..6efeac1 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -16,7 +16,7 @@ jobs: uses: actions/checkout@v6 - name: Setup Bun - uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2.1.2 + uses: oven-sh/setup-bun@ecf28ddc73e819eb6fa29df6b34ef8921c743461 # v2.1.3d267786b128fe76c2f16a390aa2448b815359f3 # v2.1.2 with: bun-version: latest diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 70d69fa..d81f238 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -57,13 +57,13 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v6 with: # Needed to fetch the gh-pages storage branch for versioned snapshots. fetch-depth: 0 - name: Setup Bun - uses: oven-sh/setup-bun@v2.1.2 + uses: oven-sh/setup-bun@ecf28ddc73e819eb6fa29df6b34ef8921c743461 # v2.1.32.1.3 with: bun-version: latest @@ -127,7 +127,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v6 with: # Full history needed to push to gh-pages and commit versions.json to main. fetch-depth: 0 @@ -146,7 +146,7 @@ jobs: echo "major=$MAJOR" >> "$GITHUB_OUTPUT" - name: Setup Bun - uses: oven-sh/setup-bun@v2.1.2 + uses: oven-sh/setup-bun@ecf28ddc73e819eb6fa29df6b34ef8921c743461 # v2.1.32.1.3 with: bun-version: latest diff --git a/.gitignore b/.gitignore index bd3cb7e..beef780 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ dist/ coverage/ .env *.local +a11y-report.json # VitePress docs/.vitepress/cache diff --git a/.pa11yci.json b/.pa11yci.json new file mode 100644 index 0000000..2c96b54 --- /dev/null +++ b/.pa11yci.json @@ -0,0 +1,17 @@ +{ + "defaults": { + "standard": "WCAG2AA", + "reporters": ["cli", ["json", { "fileName": "./a11y-report.json" }]], + "level": "error", + "wait": 1500, + "timeout": 60000, + "chromeLaunchConfig": { + "args": ["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage"] + }, + "ignore": [ + "WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.BgImage", + "WCAG2AA.Principle1.Guideline1_4.1_4_3.G145.BgImage", + "WCAG2AA.Principle4.Guideline4_1.4_1_1.F77" + ] + } +} diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index c29c0ce..bedf776 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -119,7 +119,13 @@ export default defineConfig({ content: "https://fulll.github.io/github-code-search/social-preview.png", }, ], - ["meta", { property: "og:url", content: "https://fulll.github.io/github-code-search/" }], + [ + "meta", + { + property: "og:url", + content: "https://fulll.github.io/github-code-search/", + }, + ], // ── Twitter Card ──────────────────────────────────────────────────────── ["meta", { name: "twitter:card", content: "summary_large_image" }], ["meta", { name: "twitter:title", content: "github-code-search" }], @@ -158,11 +164,40 @@ export default defineConfig({ const svgPath = fileURLToPath(new URL("../public/social-preview.svg", import.meta.url)); const pngPath = fileURLToPath(new URL("../public/social-preview.png", import.meta.url)); const svg = readFileSync(svgPath, "utf-8"); - const resvg = new Resvg(svg, { fitTo: { mode: "width", value: 1200 } }); + const resvg = new Resvg(svg, { + fitTo: { mode: "width", value: 1200 }, + }); writeFileSync(pngPath, resvg.render().asPng()); }, }, ], + // ── Chunk splitting ────────────────────────────────────────────────────── + // Mermaid alone is >900 kB minified; split it + the d3 sub-tree into + // dedicated async chunks to eliminate the Rollup 500 kB warning and + // improve long-term caching. No generic vendor catch-all — VitePress + // internals (mark.js etc.) need Rollup's default resolution. + build: { + // Mermaid (bundled with d3) is legitimately large (~2.4 MB minified). + // 2500 kB threshold avoids the Rollup warning without masking real bloat + // on other chunks (next largest is katex at ~260 kB). + chunkSizeWarningLimit: 2500, + rollupOptions: { + output: { + manualChunks(id: string) { + // Mermaid + d3 must be co-located (circular dependency between them). + if ( + id.includes("node_modules/mermaid") || + id.includes("node_modules/vitepress-plugin-mermaid") || + id.includes("node_modules/d3") || + id.includes("node_modules/dagre-d3-es") || + id.includes("node_modules/internmap") || + id.includes("node_modules/robust-predicates") + ) + return "mermaid"; + }, + }, + }, + }, }, themeConfig: { @@ -305,13 +340,19 @@ export default defineConfig({ // ── Markdown ────────────────────────────────────────────────────────────── markdown: { theme: { - light: "github-light", + // github-light-high-contrast fixes WCAG AA contrast for Shiki tokens + // (github-light has #D73A49 4.24:1, #6A737D 4.46:1, #22863A 4.28:1 — all below 4.5:1) + light: "github-light-high-contrast", dark: "github-dark", }, }, // ── Sitemap ─────────────────────────────────────────────────────────────── + // VITEPRESS_HOSTNAME overrides the default for local/CI a11y audits: + // VITEPRESS_HOSTNAME=http://localhost:4173 vitepress build docs + // → sitemap.xml contains localhost URLs that pa11y-ci can reach directly. sitemap: { - hostname: "https://fulll.github.io/github-code-search/", + hostname: + (process.env.VITEPRESS_HOSTNAME ?? "https://fulll.github.io") + "/github-code-search/", }, }); diff --git a/docs/.vitepress/theme/ComparisonTable.vue b/docs/.vitepress/theme/ComparisonTable.vue index fa58ee7..5157f1d 100644 --- a/docs/.vitepress/theme/ComparisonTable.vue +++ b/docs/.vitepress/theme/ComparisonTable.vue @@ -80,15 +80,18 @@ const ROWS: Row[] = [ org-wide code audits and interactive triage.

    + - - + - @@ -248,7 +251,8 @@ thead tr { } .ct-tool-alt { - color: var(--vp-c-text-3); + /* Fix: var(--vp-c-text-3) = 2.87:1, below WCAG AA 4.5:1. text-2 ≥ 5.4:1. */ + color: var(--vp-c-text-2); } .ct-tool-brand { @@ -323,7 +327,8 @@ thead tr { .ct-feature-desc { font-size: 13px; font-weight: 400; - color: var(--vp-c-text-3); + /* Fix: var(--vp-c-text-3) ≈ 2.87:1, below WCAG AA. text-1 ensures ≥4.5:1. */ + color: var(--vp-c-text-1); line-height: 1.45; } diff --git a/docs/.vitepress/theme/HowItWorks.vue b/docs/.vitepress/theme/HowItWorks.vue index b14884f..a42498b 100644 --- a/docs/.vitepress/theme/HowItWorks.vue +++ b/docs/.vitepress/theme/HowItWorks.vue @@ -1,7 +1,7 @@
    + Feature comparison between gh search code and github-code-search +
    +
    gh search code
    +
    github-code-search Purpose-built @@ -132,12 +135,12 @@ const ROWS: Row[] = [
    - - + + - - + +