From 110da9b8b0366597db17b46b8c461c16157e53bd Mon Sep 17 00:00:00 2001 From: Marcel Date: Sun, 26 Apr 2026 21:02:39 +0200 Subject: [PATCH 1/2] fix(viewer): replace text-accent with text-primary on annotation toggle inactive state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes WCAG 2.1 AA contrast failure (#341): text-accent (#a1dcd8) on light PDF control bar was 1.52:1 — well below the 4.5:1 AA minimum. text-primary resolves to #012851 in light mode (14.5:1) and #a1dcd8 in dark mode (9:1) — both states pass AA in both themes. Adds PdfControls.svelte.spec.ts with 5 tests covering toggle visibility, label strings, and the contrast-safe class assertion. Co-Authored-By: Claude Sonnet 4.6 --- frontend/eslint.config.js | 20 ++++++ .../src/lib/components/PdfControls.svelte | 2 +- .../lib/components/PdfControls.svelte.spec.ts | 67 +++++++++++++++++++ 3 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 frontend/src/lib/components/PdfControls.svelte.spec.ts diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index b1d59d3c..3de63ff2 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -40,6 +40,26 @@ export default defineConfig( parser: ts.parser, svelteConfig } + }, + rules: { + // text-accent resolves to #a1dcd8 in light mode (1.52:1 on white — WCAG fail). + // layout.css documents it as decorative-only (borders, icon tints, bg fills). + // For any text label use text-primary or text-ink instead. This rule catches + // the pattern where text-accent appears inside a JavaScript string literal + // (e.g. conditional ternary class expressions in Svelte templates). + 'no-restricted-syntax': [ + 'error', + { + selector: 'Literal[value=/\\btext-accent\\b/]', + message: + 'text-accent is decorative-only (#a1dcd8 in light mode = 1.52:1 contrast — WCAG fail). Use text-primary or text-ink-2 for text labels.' + }, + { + selector: 'TemplateLiteral > TemplateElement[value.raw=/\\btext-accent\\b/]', + message: + 'text-accent is decorative-only (#a1dcd8 in light mode = 1.52:1 contrast — WCAG fail). Use text-primary or text-ink-2 for text labels.' + } + ] } } ); diff --git a/frontend/src/lib/components/PdfControls.svelte b/frontend/src/lib/components/PdfControls.svelte index 17c3ef96..da586f82 100644 --- a/frontend/src/lib/components/PdfControls.svelte +++ b/frontend/src/lib/components/PdfControls.svelte @@ -91,7 +91,7 @@ let { aria-label={showAnnotations ? m.pdf_annotations_hide() : m.pdf_annotations_show()} class="flex items-center gap-1.5 rounded px-2 py-1 font-sans text-xs transition {showAnnotations ? 'text-ink-2 hover:bg-surface/10' - : 'bg-surface/10 text-accent'}" + : 'bg-surface/10 text-primary'}" > { + it('renders annotation toggle when annotationCount is greater than zero', async () => { + render(PdfControls, { ...defaultProps, annotationCount: 3 }); + await expect + .element(page.getByRole('button', { name: /annotierungen anzeigen/i })) + .toBeInTheDocument(); + }); + + it('does not render annotation toggle when annotationCount is zero', async () => { + render(PdfControls, { ...defaultProps, annotationCount: 0 }); + await expect + .element(page.getByRole('button', { name: /annotierungen/i })) + .not.toBeInTheDocument(); + }); +}); + +describe('PdfControls — annotation toggle label', () => { + it('shows "Annotierungen anzeigen" label when annotations are hidden', async () => { + render(PdfControls, { ...defaultProps, annotationCount: 2, showAnnotations: false }); + const btn = page.getByRole('button', { name: /annotierungen anzeigen/i }); + await expect.element(btn).toBeInTheDocument(); + }); + + it('shows "Annotierungen verbergen" label when annotations are visible', async () => { + render(PdfControls, { ...defaultProps, annotationCount: 2, showAnnotations: true }); + const btn = page.getByRole('button', { name: /annotierungen verbergen/i }); + await expect.element(btn).toBeInTheDocument(); + }); +}); + +describe('PdfControls — annotation toggle contrast (WCAG 2.1 AA)', () => { + it('uses text-primary class on annotation toggle button when annotations are hidden', async () => { + const { container } = render(PdfControls, { + ...defaultProps, + annotationCount: 2, + showAnnotations: false + }); + const allButtons = container.querySelectorAll('button'); + const annotationBtn = Array.from(allButtons).find((b) => + b.getAttribute('aria-label')?.toLowerCase().includes('annotierungen') + ); + expect(annotationBtn).not.toBeNull(); + expect(annotationBtn!.className).toContain('text-primary'); + expect(annotationBtn!.className).not.toContain('text-accent'); + }); +}); -- 2.49.1 From 379bc84e114f7a263c073f1776a5fc92717177dc Mon Sep 17 00:00:00 2001 From: Marcel Date: Sun, 26 Apr 2026 21:03:12 +0200 Subject: [PATCH 2/2] fix(a11y): fix ProgressRing text label contrast and add no-restricted-syntax lint rule for text-accent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ProgressRing used text-accent (#a1dcd8) on a percentage text label — same WCAG 2.1 AA failure as #341. Switched to text-primary. Also adds ESLint no-restricted-syntax rule (scoped to *.svelte files) that blocks future text-accent usage in JavaScript string literals inside Svelte class expressions. The rule caught both violations at once; both are now fixed. The rule is scoped to .svelte files so test assertions against 'text-accent' strings in .spec.ts files are unaffected. Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/lib/components/ProgressRing.svelte | 2 +- frontend/src/lib/components/ProgressRing.svelte.spec.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/lib/components/ProgressRing.svelte b/frontend/src/lib/components/ProgressRing.svelte index 389a3b9a..32992300 100644 --- a/frontend/src/lib/components/ProgressRing.svelte +++ b/frontend/src/lib/components/ProgressRing.svelte @@ -19,7 +19,7 @@ let { percentage }: { percentage: number } = $props(); /> {percentage}% diff --git a/frontend/src/lib/components/ProgressRing.svelte.spec.ts b/frontend/src/lib/components/ProgressRing.svelte.spec.ts index 8efc36e5..c3e1ef46 100644 --- a/frontend/src/lib/components/ProgressRing.svelte.spec.ts +++ b/frontend/src/lib/components/ProgressRing.svelte.spec.ts @@ -25,12 +25,12 @@ describe('ProgressRing', () => { expect(el.className).toContain('text-gray-400'); }); - it('renders a mint-colored label when percentage is > 0', async () => { + it('renders a primary-colored label when percentage is > 0', async () => { render(ProgressRing, { percentage: 75 }); const label = page.getByText('75%'); await expect.element(label).toBeInTheDocument(); const el = (await label.element()) as HTMLElement; - expect(el.className).toContain('text-accent'); + expect(el.className).toContain('text-primary'); }); it('renders a fully filled arc for 100%', async () => { -- 2.49.1