From 4bb891de2b10fe20362185c30006699d230cf3e4 Mon Sep 17 00:00:00 2001 From: Sanskar Date: Fri, 6 Nov 2026 13:07:30 +0530 Subject: [PATCH 01/32] test(a11y): add pinned local axe-core WCAG A/AA audit with per-state JSON evidence --- scripts/lib/offline-a11y.ts | 70 +++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 scripts/lib/offline-a11y.ts diff --git a/scripts/lib/offline-a11y.ts b/scripts/lib/offline-a11y.ts new file mode 100644 index 00000000..462709a7 --- /dev/null +++ b/scripts/lib/offline-a11y.ts @@ -0,0 +1,70 @@ +import { mkdirSync, writeFileSync } from 'node:fs' +import { join, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import type { AxeResults } from 'axe-core' +import type { Page } from 'playwright' + +/** + * Automated WCAG A/AA signals only. axe-core cannot replace manual testing + * with screen readers, keyboard-only users, platform zoom or assistive tech. + * + * Bundles axe locally from devDependencies and injects it into the *extracted* + * offline documentation browser session. No CDN or remote API is used. + */ +const root = resolve(fileURLToPath(new URL('../..', import.meta.url))) +const axeBundle = join(root, 'node_modules', 'axe-core', 'axe.min.js') +const tags = ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22aa'] + +type AccessibilityViolation = Pick + +export interface AuditReport { + label: string + url: string + violations: AccessibilityViolation[] + incomplete: number + passes: number +} + +export async function auditOfflineAccessibility( + page: Page, label: string, output: string +): Promise<{ passes: number; violations: number }> { + // Script injection via Playwright avoids network access and runs the + // exact pinned axe-core package shipped in our repository's CI installs. + await page.addScriptTag({ path: axeBundle }) + const results: AxeResults = await page.evaluate(async (requiredTags) => { + const axe = (window as unknown as { + axe: { + run: ( + root: Document, + options: { runOnly: { type: 'tag'; values: string[] } } + ) => Promise + } + }).axe + return axe.run(document, { runOnly: { type: 'tag', values: requiredTags } }) + }, tags) + + const summary: AuditReport = { + label, + url: page.url(), + violations: results.violations.map(({ id, impact, description, help, helpUrl, nodes }) => ({ + id, impact, description, help, helpUrl, + nodes: nodes.map(({ target, html, failureSummary, impact: nodeImpact }) => ({ + target, html, failureSummary, impact: nodeImpact + })) as AxeResults['violations'][number]['nodes'] + })), + incomplete: results.incomplete.length, + passes: results.passes.length + } + mkdirSync(output, { recursive: true }) + writeFileSync(join(output, label + '.json'), JSON.stringify(summary, null, 2) + '\n') + if (results.violations.length) { + const descriptions = results.violations.map(v => + v.id + ' (' + (v.impact ?? 'unspecified') + '): ' + + v.nodes.slice(0, 3).map(n => n.target.join(' ')).join(', ') + ) + throw new Error('Accessibility audit failed for ' + label + + ': ' + descriptions.join(' | ') + '. See artifacts/browser-a11y/' + label + '.json') + } + return { passes: results.passes.length, violations: results.violations.length } +} From 6651b6e4a52c4582dc8524094af6dfffc2d3a72b Mon Sep 17 00:00:00 2001 From: Sanskar Date: Fri, 6 Nov 2026 13:08:12 +0530 Subject: [PATCH 02/32] test(browser): audit 10 bilingual offline UI states for WCAG A/AA and 320px reflow --- scripts/test-browser-offline.ts | 62 ++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/scripts/test-browser-offline.ts b/scripts/test-browser-offline.ts index 7e08a82e..4bf9a1d6 100644 --- a/scripts/test-browser-offline.ts +++ b/scripts/test-browser-offline.ts @@ -5,6 +5,7 @@ import { tmpdir } from 'node:os' import { join, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { chromium, type Browser, type BrowserContext, type Page } from 'playwright' +import { auditOfflineAccessibility } from './lib/offline-a11y.ts' /** * Browser acceptance tests of the EXTRACTED distribution, not a dev server. @@ -311,6 +312,64 @@ async function reducedMotionNavigation(base: string, locale: typeof pages[number } finally { await context.close(); activePage = undefined } } + +/** + * Test actual generated English/Hindi HTML with locally injected axe-core, + * including content/CLI UI, search dialog, dark theme and narrow reflow. + * The same network firewall used by behavioral checks remains in force. + */ +async function auditOfflineSite(base: string, locale: typeof pages[number]): Promise { + const output = join(root, 'artifacts', 'browser-a11y') + const variants = [ + { id: 'page-light', route: locale.path, width: 1280, dark: false, search: false }, + { id: 'page-dark', route: locale.path, width: 1280, dark: true, search: false }, + { id: 'narrow-reflow', route: locale.path, width: 320, dark: false, search: false }, + { id: 'search-dialog', route: locale.path, width: 1280, dark: false, search: true }, + { id: 'homepage', route: locale.locale === 'hi' ? '/scarp-docs/hi/' : '/scarp-docs/', + width: 1280, dark: false, search: false } + ] as const + + for (const variant of variants) { + const { context, external } = await isolatedContext(base, variant.width, 850) + const label = locale.locale + '-' + variant.id + try { + activePage = await context.newPage() + const page = activePage + await page.goto(base + variant.route, {waitUntil:'networkidle'}) + + if (variant.dark) { + const toggle = page.locator('.VPNavBarAppearance button').first() + await toggle.waitFor({state:'visible'}) + const previouslyDark = await page.locator('html').evaluate(el => el.classList.contains('dark')) + if (!previouslyDark) await toggle.click() + await page.waitForFunction(() => document.documentElement.classList.contains('dark')) + } + if (variant.search) { + await page.keyboard.press('Control+k') + await page.locator('.VPLocalSearchBox input[type="search"]') + .waitFor({state:'visible'}) + } + if (variant.width === 320) { + // 320 CSS pixels approximates the content viewport at 200% zoom + // on a 640 CSS-pixel display; this is NOT a native browser zoom test. + const overflow = await page.evaluate(() => + document.documentElement.scrollWidth - window.innerWidth) + assert.ok(overflow <= 2, label + ': unscrollable page overflows by ' + overflow + 'px') + await page.locator('.scarp-command-finder #scarp-task').selectOption('inspect-symbols') + assert.equal(await page.locator('.scarp-command-finder pre').count(), 1) + } + + const result = await auditOfflineAccessibility(page, label, output) + assert.ok(result.passes > 0, label + ': no accessibility rule was evaluated') + assert.deepEqual(external, [], label + ': external request attempted during audit') + checked++ + } finally { + await context.close() + activePage = undefined + } + } +} + try { execFileSync('tar',['-xzf',join(root,'artifacts/scarp-docs-offline.tar.gz'),'-C',extracted]) const base=await serve() @@ -329,9 +388,10 @@ try { await languageSwitch(base,locale) await themeAndSkip(base,locale) await reducedMotionNavigation(base,locale) + await auditOfflineSite(base,locale) } console.log('PASS: '+checked+ - ' real Chromium offline checks across English/Hindi desktop/mobile: keyboard, release gates, copy/failure, localized search/no-results, language switch, skip link, theme persistence, reduced-motion and zero external requests') + ' real Chromium offline checks across English/Hindi desktop/mobile: keyboard, release gates, copy/failure, localized search/no-results, language switch, skip link, theme persistence, reduced-motion, axe-core WCAG A/AA audit and zero external requests') }catch(err){ if(activePage) { try {await activePage.screenshot({path:join(shots,'last-failure.png'),fullPage:true,timeout:3000})} From 8066d37ff4073caf3036866961a4fe2fc5a88868 Mon Sep 17 00:00:00 2001 From: Sanskar Date: Fri, 6 Nov 2026 13:08:44 +0530 Subject: [PATCH 03/32] build(a11y): pin offline axe-core WCAG auditing dependency --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 5429645b..dfe11f9b 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,8 @@ "typescript": "6.0.3", "tsx": "4.23.15", "vue-tsc": "3.3.13", - "playwright": "1.64.1" + "playwright": "1.64.1", + "axe-core": "4.14.0" }, "engines": { "node": ">=20.19.0" From 344ee28d666acb4dd31afb216aa182615c60e471 Mon Sep 17 00:00:00 2001 From: Sanskar Date: Fri, 6 Nov 2026 13:08:49 +0530 Subject: [PATCH 04/32] ci(a11y): attach English/Hindi WCAG audit JSON evidence on browser test failures --- .github/workflows/ci.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc5c988c..f7104f14 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -156,6 +156,14 @@ jobs: run: npm run docs:build && npm run package:offline - name: Check English/Hindi accessibility, CLI selector, offline search and mobile navigation in Chromium run: npm run test:browser-offline + - name: Upload accessibility audit evidence on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: scarp-docs-accessibility-audit + path: artifacts/browser-a11y/ + if-no-files-found: ignore + retention-days: 14 - name: Attach browser failure screenshot if: failure() uses: actions/upload-artifact@v4 From b791a6768c8931c0486b132522beeda8bb8f0bae Mon Sep 17 00:00:00 2001 From: Sanskar Date: Fri, 6 Nov 2026 13:08:52 +0530 Subject: [PATCH 05/32] chore(a11y): ignore local axe browser report files --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 9c6efe2f..916c90de 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ docs/.vitepress/dist/ artifacts/ test-results/ playwright-report/ +browser-a11y/ From 78b4caa8fc88e2d395178f28771cfbf6f04dc523 Mon Sep 17 00:00:00 2001 From: Sanskar Date: Fri, 6 Nov 2026 13:08:56 +0530 Subject: [PATCH 06/32] docs(maintenance): explain audited states, scope and manual screen reader limitations --- docs/project/maintenance.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/project/maintenance.md b/docs/project/maintenance.md index 155f4de3..4b2d0915 100644 --- a/docs/project/maintenance.md +++ b/docs/project/maintenance.md @@ -20,6 +20,12 @@ The dedicated `browser-offline` Documentation CI job installs pinned Playwright/ For a local repeat, install dependencies with `npm install`, install Chromium with `npx playwright install chromium` (CI uses the same browser install on GitHub-hosted Ubuntu, which has its required system libraries), run `npm run docs:build && npm run package:offline`, then `npm run test:browser-offline`. Failure screenshots, when possible, are saved under ignored `artifacts/browser-failures/` and uploaded on CI failure. The expanded suite also checks a keyboard-opened empty search state and focus restoration on close (implemented by the small theme-level `local-search-focus.ts` manager for VitePress 1.6), translated-page switching, skip-to-content link and target, persisted appearance mode across reloads, and usable CLI controls under a reduced-motion viewport. These tests exercise selected interactive controls, not every screen-reader combination or a full WCAG conformance audit; track broader access testing in [Issue #65](https://github.com/scarp-lang/scarp-docs/issues/65). The offline distribution remains pinned to the tested docs commit, and the browser suite has no external-search backend. +## Offline accessibility rule audits + +The offline browser job now runs **axe-core 4.14.0** from the pinned local npm devDependency against ten extracted-site states: English/Hindi Command Finder in light and dark appearances, a narrow 320 CSS-pixel viewport, an open local-search dialog, and the English/Hindi homepages. It asserts zero automated WCAG A/AA violations, verifies that the browser did not make external network requests, and writes per-state JSON results under ignored `artifacts/browser-a11y/` (uploaded to CI on a failure). The 320px viewport checks reflow; it approximates the content width at 200% browser zoom on a 640 CSS-pixel display, **not** a test of native browser zoom or text-only zoom. + +The checks run automatically within `npm run test:browser-offline` after `npm run docs:build && npm run package:offline`. If an audit reports a violation, inspect its concrete rule ID, offending selector and remediation in the uploaded JSON rather than excluding the rule. Review `incomplete` manual-review signals as well. These tests catch common machine-detectable accessibility issues but **cannot establish WCAG compliance**, screen-reader compatibility, color differentiation perception, or real-user navigation quality. Manual assistive-technology review remains tracked under [Issue #65](https://github.com/scarp-lang/scarp-docs/issues/65). + ## Supported TypeScript and Vue toolchain The docs run strict Vue SFC checks through `vue-tsc`. The tested compatibility line is **TypeScript 6.0.x** with **vue-tsc 3.3.x**. TypeScript 7.0 removes the programmatic TypeScript API expected by Vue's checker, causing `ERR_PACKAGE_PATH_NOT_EXPORTED` before actual document diagnostics run. This is an **upstream integration limitation**, not evidence that the TypeScript 7 compiler itself is defective. From 8a75c02670416187b10812bd3ef04e59e6f3f95c Mon Sep 17 00:00:00 2001 From: Sanskar Date: Fri, 6 Nov 2026 13:08:59 +0530 Subject: [PATCH 07/32] docs(readme): surface offline accessibility audit gate and scope --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 70191b38..e89f6b30 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ Official VitePress documentation for the experimental [Scarp Programming Languag **Install the latest published compiler:** [English: Scarp v0.0.6-alpha.1](docs/guide/installation-alpha6.md) · [हिन्दी: alpha.6 इंस्टॉलेशन](docs/hi/guide/installation-alpha6.md). Windows x64, Linux x64 GNU, and Apple Silicon archives are paired with official SHA-256 checksums; the guides also provide commands for building the immutable source tag. Earlier installation walkthroughs remain historically scoped. +**Automated accessibility auditing:** The extracted offline site is checked with pinned local axe-core in 10 bilingual light/dark/search/mobile states; CI attaches per-state WCAG A/AA findings on failures. Manual screen-reader verification remains separately tracked in [Issue #65](https://github.com/scarp-lang/scarp-docs/issues/65). + **Real browser checks:** The `browser-offline` CI job runs a pinned Chromium browser against the extracted offline archive, checking keyboard navigation and search focus return, translated page switching, dark/light preference, reduced motion, mobile layout and both English/Hindi Command Finder flows. See `npm run test:browser-offline` and [maintainer guidance](docs/project/maintenance.md). **Read the bilingual site offline:** [English offline download instructions](docs/guide/offline-docs.md) · [हिन्दी offline documentation](docs/hi/guide/offline-docs.md). A tested GitHub Actions artifact contains reproducible bundled docs, a Node.js loopback server and SHA-256 checksums. Offline docs are not compiler releases. From 6d6aa0a1a37081278da4c0eab2873b26d03bb364 Mon Sep 17 00:00:00 2001 From: Sanskar Date: Fri, 6 Nov 2026 13:09:20 +0530 Subject: [PATCH 08/32] docs(offline): explain new localized WCAG audit and narrow viewport safety limits --- docs/guide/offline-docs.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/guide/offline-docs.md b/docs/guide/offline-docs.md index c774e89c..e2395c60 100644 --- a/docs/guide/offline-docs.md +++ b/docs/guide/offline-docs.md @@ -58,6 +58,8 @@ The included server binds only to **127.0.0.1** (this computer). Stop with **Ctr ## What works offline? +**Automated accessibility checks:** The offline test suite scans a selection of actual English/Hindi pages and interactive states with locally bundled axe-core WCAG A/AA rules and checks 320px-wide reflow. These checks find common machine-detectable problems; they **do not replace** manual keyboard, screen-reader or contrast perception reviews. + **Automated browser verification:** the documentation CI now starts the extracted local server in headless Chromium and checks English/Hindi Command Finder interaction, real clipboard success and denied permissions, keyboard-activated local search and its empty-results state, language switching, skip-to-content, persisted dark/light appearance, mobile navigation, reduced-motion operation and the absence of external requests. The browser runs locally against the packaged archive, not the hosted GitHub Pages site. This does not constitute a complete WCAG audit. The snapshot includes **all generated English and Hindi pages**, their local CSS/JavaScript, and the assets used by VitePress local search and the version-aware [Command Finder](/tooling/command-finder). Regular internal navigation runs against your local server. Links to GitHub, official releases, social profiles, or other external sites **still require internet**. The browser's actual keyboard/search behavior is being tracked separately in [accessibility test issue #65](https://github.com/scarp-lang/scarp-docs/issues/65); static asset checks are not equivalent to a full browser accessibility audit. From 888d23c78f4a6cb5cd530f579e0f0f742b5e3d0b Mon Sep 17 00:00:00 2001 From: Sanskar Date: Fri, 6 Nov 2026 13:09:23 +0530 Subject: [PATCH 09/32] docs(hi-offline): document WCAG rules and human-review limits in Hindi --- docs/hi/guide/offline-docs.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/hi/guide/offline-docs.md b/docs/hi/guide/offline-docs.md index ce0832ab..a59c3710 100644 --- a/docs/hi/guide/offline-docs.md +++ b/docs/hi/guide/offline-docs.md @@ -58,6 +58,8 @@ Server केवल **127.0.0.1** अर्थात आपके अपने ## Offline में क्या उपलब्ध है? +**Automated accessibility जाँच:** Offline test suite, local axe-core के WCAG A/AA नियमों के अनुसार English/Hindi पृष्ठों तथा interactive स्थितियों की जाँच करती है। 320px viewport पर reflow भी जाँचा जाता है। ये परीक्षण कुछ सामान्य समस्याएँ पकड़ते हैं; **manual keyboard, screen-reader और contrast जाँच का विकल्प नहीं** हैं। + **असल browser परीक्षण:** Documentation CI extracted local server को headless Chromium में चलाकर English/Hindi Command Finder, clipboard success या permission failure, keyboard से local search तथा zero-results, भाषा बदलना, skip-to-content, dark/light mode की preference, mobile navigation, reduced-motion और बाहरी network requests न होने की पुष्टि करती है। यह जाँच packaged offline archive पर होती है, hosted GitHub Pages पर नहीं। इसे सम्पूर्ण WCAG accessibility audit **नहीं** मानें। इस package में सभी English/Hindi HTML पृष्ठ, उनके स्थानीय CSS/JavaScript, VitePress search assets और [Command Finder](/hi/tooling/command-finder) शामिल हैं। पृष्ठों के अंदर के links स्थानीय server पर खुलते हैं। **बाहरी GitHub, social profiles तथा download links** के लिए फिर भी इंटरनेट चाहिए। असली browser में keyboard, search और accessibility परीक्षण का अलग काम [Issue #65](https://github.com/scarp-lang/scarp-docs/issues/65) पर रखा गया है; static assets की पुष्टि को पूरी browser accessibility जाँच न समझें। From bb5a11a67430fd927dd59bfcc65634d64974f26c Mon Sep 17 00:00:00 2001 From: Sanskar Date: Fri, 6 Nov 2026 13:12:28 +0530 Subject: [PATCH 10/32] fix(a11y): include visible search keyboard shortcut in English/Hindi accessible button names --- docs/.vitepress/local-search.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/.vitepress/local-search.ts b/docs/.vitepress/local-search.ts index 0304fb99..78aea451 100644 --- a/docs/.vitepress/local-search.ts +++ b/docs/.vitepress/local-search.ts @@ -6,12 +6,21 @@ * VitePress already generates separate English/Hindi locale indexes. */ export const localSearchOptions = { + // Visible VitePress 1.6 search-button text includes the keyboard key "K". + // Label-in-name WCAG checks require that visible character in the accessible + // label in both English and Hindi; otherwise axe reports a serious mismatch. + translations: { + button: { + buttonText: 'Search', + buttonAriaLabel: 'Search K' + } + }, locales: { hi: { translations: { button: { buttonText: 'दस्तावेज़ खोजें', - buttonAriaLabel: 'दस्तावेज़ खोजें' + buttonAriaLabel: 'दस्तावेज़ खोजें K' }, modal: { displayDetails: 'विस्तृत परिणाम दिखाएँ', From 7fd355bb3778baf6ee27528bc8c44f7f82d6f95b Mon Sep 17 00:00:00 2001 From: Sanskar Date: Fri, 6 Nov 2026 13:12:55 +0530 Subject: [PATCH 11/32] test(a11y): enforce label-in-name compliance for both search translations --- scripts/lib/hindi-search-policy.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/scripts/lib/hindi-search-policy.ts b/scripts/lib/hindi-search-policy.ts index 5d3dcc08..de5cdd0b 100644 --- a/scripts/lib/hindi-search-policy.ts +++ b/scripts/lib/hindi-search-policy.ts @@ -3,6 +3,7 @@ interface SearchTranslationBlock { [key: string]: string | Record | undefined } export interface SearchLocalizationOptions { + translations?: { button?: Record } locales?: Record @@ -35,6 +36,19 @@ export function validateHindiLocalSearch( const failures: string[] = [] const translations = options.locales?.hi?.translations const button = translations?.button ?? {} + const englishButton = options.translations?.button ?? {} + // The VitePress search trigger displays a keycap "K" next to its label. + // Both localized ARIA names must contain all user-visible text in order. + for (const [name, values] of [ + ['English',englishButton], + ['Hindi',button] + ] as const) { + const visible = values.buttonText + const accessible = values.buttonAriaLabel + if (!visible || accessible !== visible + ' K') { + failures.push(name + ' search accessible name must contain visible label and shortcut K') + } + } const modal = translations?.modal ?? {} const footerCandidate = modal.footer const footer = footerCandidate && typeof footerCandidate === 'object' From 42a841b70d720861feb8c24e7470587eff7bdb0a Mon Sep 17 00:00:00 2001 From: Sanskar Date: Fri, 6 Nov 2026 13:12:58 +0530 Subject: [PATCH 12/32] test(a11y): add negative English/Hindi regression cases for visible search keycap --- scripts/test-hindi-search.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/scripts/test-hindi-search.ts b/scripts/test-hindi-search.ts index 8b950821..142092f0 100644 --- a/scripts/test-hindi-search.ts +++ b/scripts/test-hindi-search.ts @@ -34,6 +34,14 @@ mutateOptions('English-only search button',x=>{ mutateOptions('missing accessible label',x=>{ if(x.locales?.hi?.translations?.button) x.locales.hi.translations.button.buttonAriaLabel='' }) +mutateOptions('English search keycap missing from accessible name',x=>{ + if(x.translations?.button) x.translations.button.buttonAriaLabel='Search' +}) +mutateOptions('Hindi search keycap missing from accessible name',x=>{ + if(x.locales?.hi?.translations?.button) { + x.locales.hi.translations.button.buttonAriaLabel='दस्तावेज़ खोजें' + } +}) mutateOptions('English-only empty-result text',x=>{ if(x.locales?.hi?.translations?.modal) x.locales.hi.translations.modal.noResultsText='No results' }) @@ -59,4 +67,4 @@ mutatePage('missing Hindi intro link','hindiIntro','](/hi/guide/search-and-navig mutatePage('missing README Hindi path','readme','docs/hi/guide/search-and-navigation.md','docs/hi/guide/not-found.md') assert.ok(validateHindiLocalSearch(localSearchOptions,{...pages,hindi:pages.hindi.replace(/[\u0900-\u097f]/gu,'')}).length, 'Hindi locale cannot silently become untranslated') -console.log('PASS: 16 negative regressions for Hindi search UI, locale navigation, language parity and keyboard access') +console.log('PASS: 18 negative regressions for Hindi search UI, locale navigation, language parity and keyboard access') From 323f281eb51df5480b2180fbff816e9d055dc5e9 Mon Sep 17 00:00:00 2001 From: Sanskar Date: Fri, 6 Nov 2026 13:13:01 +0530 Subject: [PATCH 13/32] docs(search): explain accessible search shortcut naming --- docs/guide/search-and-navigation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/search-and-navigation.md b/docs/guide/search-and-navigation.md index dc8946d7..b3fad5c1 100644 --- a/docs/guide/search-and-navigation.md +++ b/docs/guide/search-and-navigation.md @@ -9,7 +9,7 @@ Find an implemented feature, an exact CLI command or a release-specific example 3. Use the arrow keys to move through results and **Enter** to open one. Press **Esc** to close search. 4. Choose the **English / हिन्दी** language switcher to search the other translated set of pages. -The search dialog follows the current documentation locale. The **Hindi** interface includes translated search buttons, results messages and keyboard hints. It indexes Hindi documents directly; it is not automatic translation of English answers. Code identifiers and release tags remain written as in the published compiler. +The keyboard shortcut shown in the search trigger is part of its accessible name, so its text remains consistent for keyboard and screen-reader users. The search dialog follows the current documentation locale. The **Hindi** interface includes translated search buttons, results messages and keyboard hints. It indexes Hindi documents directly; it is not automatic translation of English answers. Code identifiers and release tags remain written as in the published compiler. ## Find the right version From 009964198fc486418d76d3c8747a43dff5c28742 Mon Sep 17 00:00:00 2001 From: Sanskar Date: Fri, 6 Nov 2026 13:13:05 +0530 Subject: [PATCH 14/32] docs(hi-search): describe matching visible and accessible keyboard shortcut --- docs/hi/guide/search-and-navigation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/hi/guide/search-and-navigation.md b/docs/hi/guide/search-and-navigation.md index d359a6e6..df6b4883 100644 --- a/docs/hi/guide/search-and-navigation.md +++ b/docs/hi/guide/search-and-navigation.md @@ -9,7 +9,7 @@ Scarp के सही keyword, compiler command या किसी प्र 3. Arrow keys से परिणाम बदलें और **Enter** से संबंधित पृष्ठ खोलें। Search बंद करने के लिए **Esc** दबाएँ। 4. **English / हिन्दी** language switcher से दूसरी भाषा के दस्तावेज़ चुनें और फिर खोजें। -हिन्दी पृष्ठों में search button, परिणाम न मिलने का संदेश और keyboard सहायता हिन्दी में दिखाई जाती है। Search हिन्दी दस्तावेज़ों को सीधे index करती है; यह English उत्तरों का अपने-आप **अनुवाद नहीं** करती। Scarp code के keywords तथा release tags मूल लिखावट में ही रहते हैं। +Search button के साथ दिखाई देने वाला **K** shortcut, उसके accessible name में भी शामिल है, ताकि screen-reader और keyboard users को एक-समान जानकारी मिले। हिन्दी पृष्ठों में search button, परिणाम न मिलने का संदेश और keyboard सहायता हिन्दी में दिखाई जाती है। Search हिन्दी दस्तावेज़ों को सीधे index करती है; यह English उत्तरों का अपने-आप **अनुवाद नहीं** करती। Scarp code के keywords तथा release tags मूल लिखावट में ही रहते हैं। ## सही compiler version चुनें From 727fc68affa0e7998859bef3a3f889c29a5f29c7 Mon Sep 17 00:00:00 2001 From: Sanskar Date: Fri, 6 Nov 2026 13:14:02 +0530 Subject: [PATCH 15/32] test(a11y): expose rule failure summaries and offending HTML in CI log for actual repairs --- scripts/lib/offline-a11y.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/lib/offline-a11y.ts b/scripts/lib/offline-a11y.ts index 462709a7..84ea4717 100644 --- a/scripts/lib/offline-a11y.ts +++ b/scripts/lib/offline-a11y.ts @@ -61,7 +61,11 @@ export async function auditOfflineAccessibility( if (results.violations.length) { const descriptions = results.violations.map(v => v.id + ' (' + (v.impact ?? 'unspecified') + '): ' + - v.nodes.slice(0, 3).map(n => n.target.join(' ')).join(', ') + v.nodes.slice(0, 3).map(n => + n.target.join(' ') + ' [' + + (n.failureSummary ?? 'No failure summary').replace(/\\s+/g, ' ').slice(0, 450) + + '] ' + n.html.slice(0, 400) + ).join(', ') ) throw new Error('Accessibility audit failed for ' + label + ': ' + descriptions.join(' | ') + '. See artifacts/browser-a11y/' + label + '.json') From 69f59d2d10ccc3efb9c14bf1598b2f63e19cc2ee Mon Sep 17 00:00:00 2001 From: Sanskar Date: Fri, 6 Nov 2026 13:15:52 +0530 Subject: [PATCH 16/32] test(a11y): capture actual search trigger text, keycap and accessibility label on audit failure --- scripts/lib/offline-a11y.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/scripts/lib/offline-a11y.ts b/scripts/lib/offline-a11y.ts index 84ea4717..47036104 100644 --- a/scripts/lib/offline-a11y.ts +++ b/scripts/lib/offline-a11y.ts @@ -59,6 +59,20 @@ export async function auditOfflineAccessibility( mkdirSync(output, { recursive: true }) writeFileSync(join(output, label + '.json'), JSON.stringify(summary, null, 2) + '\n') if (results.violations.length) { + // Keep detailed browser evidence in CI even when the downloadable JSON + // artifact needs a GitHub sign-in. Helps diagnose visible-label mismatches. + const searchTrigger = page.locator('button.DocSearch-Button').first() + if (await searchTrigger.count()) { + const evidence = await searchTrigger.evaluate(button => ({ + outerHTML: button.outerHTML.slice(0, 1700), + textContent: button.textContent, + innerText: (button as HTMLElement).innerText, + accessibleLabel: button.getAttribute('aria-label'), + placeholder: (button.querySelector('.DocSearch-Button-Placeholder') as HTMLElement | null)?.innerText, + keycaps: (button.querySelector('.DocSearch-Button-Keys') as HTMLElement | null)?.innerText + })) + console.error('A11Y_SEARCH_TRIGGER '+label+' '+JSON.stringify(evidence)) + } const descriptions = results.violations.map(v => v.id + ' (' + (v.impact ?? 'unspecified') + '): ' + v.nodes.slice(0, 3).map(n => From 07d5a3c79a464180a907e19c370add2d580d644e Mon Sep 17 00:00:00 2001 From: Sanskar Date: Fri, 6 Nov 2026 13:18:48 +0530 Subject: [PATCH 17/32] fix(a11y): make desktop search use its visible name and keep translated mobile aria labels --- .../theme/search-trigger-accessibility.ts | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 docs/.vitepress/theme/search-trigger-accessibility.ts diff --git a/docs/.vitepress/theme/search-trigger-accessibility.ts b/docs/.vitepress/theme/search-trigger-accessibility.ts new file mode 100644 index 00000000..7519de73 --- /dev/null +++ b/docs/.vitepress/theme/search-trigger-accessibility.ts @@ -0,0 +1,55 @@ +/** + * VitePress 1.6 renders local-search button text and the visible "K" keycap + * without an actual separating space (textContent is "SearchK"). Its separate + * aria-label can violate WCAG "Label in Name" even when it says "Search K". + * + * When the button's visible placeholder is rendered, derive the accessible + * name from its DOM text instead of overriding it with aria-label. At narrow + * mobile sizes where the placeholder is visually hidden, keep a translated + * aria-label from that same placeholder, so an icon-only button is named. + * + * No extra network, CSS hacks, or fork of VitePress is needed. + */ +let installed = false + +export function installAccessibleSearchTrigger(): void { + if (installed || typeof document === 'undefined' || !document.body) return + installed = true + + const synchronizeLabels = (): void => { + for (const button of document.querySelectorAll( + 'button.DocSearch-Button' + )) { + const placeholder = button.querySelector( + '.DocSearch-Button-Placeholder' + ) + const text = placeholder?.textContent?.trim() + if (!text) continue + + const visible = placeholder.getClientRects().length > 0 && + getComputedStyle(placeholder).display !== 'none' && + getComputedStyle(placeholder).visibility !== 'hidden' + + if (visible) { + // The button's child text is now the true accessible name, + // preserving the visible shortcut keycap verbatim. + if (button.hasAttribute('aria-label')) button.removeAttribute('aria-label') + } else if (button.getAttribute('aria-label') !== text) { + // On mobile, Search/Hindi text is visually hidden and must be announced. + button.setAttribute('aria-label', text) + } + } + } + + // Vue renders/updates the search button after hydration and locale changes. + // Observe relevant changes; the no-op checks above prevent mutation loops. + const observer = new MutationObserver(synchronizeLabels) + observer.observe(document.body, { + subtree: true, + childList: true, + attributes: true, + attributeFilter: ['aria-label'] + }) + window.addEventListener('resize', synchronizeLabels, { passive: true }) + synchronizeLabels() +} From 788398700043d6221707bfb6d45f4280368f2062 Mon Sep 17 00:00:00 2001 From: Sanskar Date: Fri, 6 Nov 2026 13:19:03 +0530 Subject: [PATCH 18/32] fix(a11y): register adaptive search accessible-name manager in VitePress theme --- docs/.vitepress/theme/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts index 68373cd2..b1c2c279 100644 --- a/docs/.vitepress/theme/index.ts +++ b/docs/.vitepress/theme/index.ts @@ -3,6 +3,7 @@ import type { Theme } from 'vitepress' import './custom.css' import PagesDeployments from './components/PagesDeployments.vue' import { installLocalSearchFocusReturn } from './local-search-focus' +import { installAccessibleSearchTrigger } from './search-trigger-accessibility' import ScarpCommandFinder from './components/ScarpCommandFinder.vue' const theme: Theme = { @@ -12,6 +13,7 @@ const theme: Theme = { ctx.app.component('PagesDeployments', PagesDeployments) ctx.app.component('ScarpCommandFinder', ScarpCommandFinder) installLocalSearchFocusReturn() + installAccessibleSearchTrigger() } } From 4727f428d5696aabcf80f8f3f2f54fb8d0773aa6 Mon Sep 17 00:00:00 2001 From: Sanskar Date: Fri, 6 Nov 2026 13:19:07 +0530 Subject: [PATCH 19/32] fix(a11y): let adaptive theme use VitePress default English/Hindi source labels --- docs/.vitepress/local-search.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/docs/.vitepress/local-search.ts b/docs/.vitepress/local-search.ts index 78aea451..0304fb99 100644 --- a/docs/.vitepress/local-search.ts +++ b/docs/.vitepress/local-search.ts @@ -6,21 +6,12 @@ * VitePress already generates separate English/Hindi locale indexes. */ export const localSearchOptions = { - // Visible VitePress 1.6 search-button text includes the keyboard key "K". - // Label-in-name WCAG checks require that visible character in the accessible - // label in both English and Hindi; otherwise axe reports a serious mismatch. - translations: { - button: { - buttonText: 'Search', - buttonAriaLabel: 'Search K' - } - }, locales: { hi: { translations: { button: { buttonText: 'दस्तावेज़ खोजें', - buttonAriaLabel: 'दस्तावेज़ खोजें K' + buttonAriaLabel: 'दस्तावेज़ खोजें' }, modal: { displayDetails: 'विस्तृत परिणाम दिखाएँ', From 9d25d8c083247803edf3041fb31b4ed52b49ef82 Mon Sep 17 00:00:00 2001 From: Sanskar Date: Fri, 6 Nov 2026 13:19:41 +0530 Subject: [PATCH 20/32] test(a11y): match mobile translated label and leave desktop name to visible button content --- scripts/lib/hindi-search-policy.ts | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/scripts/lib/hindi-search-policy.ts b/scripts/lib/hindi-search-policy.ts index de5cdd0b..085a26ff 100644 --- a/scripts/lib/hindi-search-policy.ts +++ b/scripts/lib/hindi-search-policy.ts @@ -3,7 +3,6 @@ interface SearchTranslationBlock { [key: string]: string | Record | undefined } export interface SearchLocalizationOptions { - translations?: { button?: Record } locales?: Record @@ -36,18 +35,11 @@ export function validateHindiLocalSearch( const failures: string[] = [] const translations = options.locales?.hi?.translations const button = translations?.button ?? {} - const englishButton = options.translations?.button ?? {} - // The VitePress search trigger displays a keycap "K" next to its label. - // Both localized ARIA names must contain all user-visible text in order. - for (const [name, values] of [ - ['English',englishButton], - ['Hindi',button] - ] as const) { - const visible = values.buttonText - const accessible = values.buttonAriaLabel - if (!visible || accessible !== visible + ' K') { - failures.push(name + ' search accessible name must contain visible label and shortcut K') - } + // On mobile the search placeholder is visually hidden, so the translated + // aria-label must name the actual button. Desktop uses its visible DOM text + // directly; the locally installed theme performs this responsive switch. + if (button.buttonText && button.buttonAriaLabel !== button.buttonText) { + failures.push('Hindi mobile search accessible name differs from its translated button') } const modal = translations?.modal ?? {} const footerCandidate = modal.footer From 320bc5c20f708e2b1d6ddfe8fdee0530f6c728aa Mon Sep 17 00:00:00 2001 From: Sanskar Date: Fri, 6 Nov 2026 13:19:44 +0530 Subject: [PATCH 21/32] test(a11y): check adaptive desktop/mobile search semantics and divergent-label regression --- scripts/test-hindi-search.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/scripts/test-hindi-search.ts b/scripts/test-hindi-search.ts index 142092f0..ee6538ba 100644 --- a/scripts/test-hindi-search.ts +++ b/scripts/test-hindi-search.ts @@ -20,6 +20,14 @@ const pages: SearchLocalizationSources = { hindiIntro: read('docs/hi/guide/introduction.md') } assert.deepEqual(validateHindiLocalSearch(localSearchOptions, pages), []) +const searchTrigger = read('docs/.vitepress/theme/search-trigger-accessibility.ts') +const themeIndex = read('docs/.vitepress/theme/index.ts') +assert.ok(themeIndex.includes('installAccessibleSearchTrigger()'), + 'Theme must install responsive search trigger semantics') +assert.ok(searchTrigger.includes("button.removeAttribute('aria-label')") && + searchTrigger.includes("button.setAttribute('aria-label', text)"), + 'Search button must derive its desktop accessible name from visible content and keep mobile name') + function mutateOptions(name: string, change: (config: SearchLocalizationOptions) => void): void { const copy = JSON.parse(JSON.stringify(localSearchOptions)) as SearchLocalizationOptions @@ -34,12 +42,9 @@ mutateOptions('English-only search button',x=>{ mutateOptions('missing accessible label',x=>{ if(x.locales?.hi?.translations?.button) x.locales.hi.translations.button.buttonAriaLabel='' }) -mutateOptions('English search keycap missing from accessible name',x=>{ - if(x.translations?.button) x.translations.button.buttonAriaLabel='Search' -}) -mutateOptions('Hindi search keycap missing from accessible name',x=>{ +mutateOptions('Hindi mobile label diverges from visible translation',x=>{ if(x.locales?.hi?.translations?.button) { - x.locales.hi.translations.button.buttonAriaLabel='दस्तावेज़ खोजें' + x.locales.hi.translations.button.buttonAriaLabel='असंबंधित नाम' } }) mutateOptions('English-only empty-result text',x=>{ @@ -67,4 +72,4 @@ mutatePage('missing Hindi intro link','hindiIntro','](/hi/guide/search-and-navig mutatePage('missing README Hindi path','readme','docs/hi/guide/search-and-navigation.md','docs/hi/guide/not-found.md') assert.ok(validateHindiLocalSearch(localSearchOptions,{...pages,hindi:pages.hindi.replace(/[\u0900-\u097f]/gu,'')}).length, 'Hindi locale cannot silently become untranslated') -console.log('PASS: 18 negative regressions for Hindi search UI, locale navigation, language parity and keyboard access') +console.log('PASS: 17 negative regressions for Hindi search UI, locale navigation, language parity and keyboard access') From 337d500b3a2a9a3532e235d8612e050dbe86a0d4 Mon Sep 17 00:00:00 2001 From: Sanskar Date: Fri, 6 Nov 2026 13:20:34 +0530 Subject: [PATCH 22/32] fix(ts): narrow optional search placeholder before DOM metrics and computed styles --- docs/.vitepress/theme/search-trigger-accessibility.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/.vitepress/theme/search-trigger-accessibility.ts b/docs/.vitepress/theme/search-trigger-accessibility.ts index 7519de73..5c542a40 100644 --- a/docs/.vitepress/theme/search-trigger-accessibility.ts +++ b/docs/.vitepress/theme/search-trigger-accessibility.ts @@ -23,7 +23,8 @@ export function installAccessibleSearchTrigger(): void { const placeholder = button.querySelector( '.DocSearch-Button-Placeholder' ) - const text = placeholder?.textContent?.trim() + if (!placeholder) continue + const text = placeholder.textContent?.trim() if (!text) continue const visible = placeholder.getClientRects().length > 0 && From 9c31ff0d4fffb7d08452d3dbfbdbc5fa585dcd00 Mon Sep 17 00:00:00 2001 From: Sanskar Date: Fri, 6 Nov 2026 13:21:40 +0530 Subject: [PATCH 23/32] fix(a11y): enable keyboard scrolling and visible focus for long Scarp CLI code blocks --- docs/.vitepress/theme/components/ScarpCommandFinder.vue | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/.vitepress/theme/components/ScarpCommandFinder.vue b/docs/.vitepress/theme/components/ScarpCommandFinder.vue index 547c95f1..b859b141 100644 --- a/docs/.vitepress/theme/components/ScarpCommandFinder.vue +++ b/docs/.vitepress/theme/components/ScarpCommandFinder.vue @@ -69,7 +69,7 @@ async function copyCommands(): Promise {