fix(viewer): fix "Annotieren anzeigen" contrast and add lint rule (#341) #349
Reference in New Issue
Block a user
Delete Branch "feat/issue-341-annotieren-contrast"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #341
What was fixed
Root cause
PdfControls.svelteusedtext-accent(--c-accent=#a1dcd8in light mode) for the inactive state of the annotation toggle. This color is explicitly documented inlayout.cssas decorative-only and a WCAG fail on light surfaces (1.52:1 contrast ratio). The bug was invisible in dark mode where--c-accentresolves to a legible turquoise.Changes
fix(viewer): replace text-accent with text-primary on annotation toggle inactive state(110da9b8)PdfControls.svelteline 94:text-accent→text-primaryin the inactive toggle class expression#012851→ 14.5:1 on white (AAA pass)#a1dcd8→ 9:1 on dark surface (AA pass)PdfControls.svelte.spec.tswith 5 tests:annotationCount > 0annotationCount === 0showAnnotations: falseshowAnnotations: truetext-primaryclass (nottext-accent) in inactive statefix(a11y): fix ProgressRing text label contrast and add no-restricted-syntax lint rule for text-accent(379bc84e)ProgressRing.svelteusedtext-accenton a percentage label. Fixed totext-primary.ProgressRing.svelte.spec.tsto asserttext-primary.no-restricted-syntaxESLint rule ineslint.config.jsscoped to*.sveltefiles — catchestext-accentin JS string literals (ternary/conditional class expressions) so this class of bug cannot slip through CI again.Decision Queue outcomes (from #341)
text-primary(stronger visual distinction from active state)PdfControls.svelte.spec.tsno-restricted-syntaxineslint.config.jsscoped to.sveltefilesTest results
entity-nav.svelte.spec.ts) unrelated to these changes🎨 Leonie Voss — UI/UX Design Lead & Accessibility Advocate
Verdict: ✅ Approved
What I checked
Findings
Contrast fix is correct and complete.
text-primaryresolves to--c-primary=#012851in light mode. On white: 14.5:1 (AAA). On--c-pdf-ctrl=#d8d8d8: still well above 7:1 (AAA). In dark mode it resolves to#a1dcd8on#011526≈ 9:1 (AA+). Both toggle states (activetext-ink-2, inactivetext-primary) now pass WCAG 2.1 AA in both themes.ProgressRing fix is also correct.
text-primaryon the percentage label is semantically appropriate — it is interactive context data, not decoration.#012851on the white ring background is well above 4.5:1.ESLint rule is a good guardrail.
The
no-restricted-syntaxrule with two selectors (string literal and template literal) covers the two real-world patterns wheretext-accentslips into class bindings. The error message names the exact contrast ratio and suggests the correct alternatives — exactly what a developer needs at the point of failure.Suggestions (non-blocking)
Touch target is still undersized (pre-existing, not introduced here, but surfaced by the fix):
The annotation toggle
px-2 py-1computes to approximately 28×24px — below the 44×44px minimum for our 60+ audience. This was noted in the issue discussion. Consider following up in a separate issue:Active state visual distinction: with
text-primary(navy) in inactive andtext-ink-2(#4b5563) in active, the inactive state is now darker than the active state. This is slightly counterintuitive — typically "active" feels heavier visually. The background fill (bg-surface/10) still distinguishes them, and the design decision was explicitly made in the Decision Queue (Theme A), so this is an observation not a blocker. Worth a quick visual check in-browser to confirm the hierarchy reads as intended.Verdict
The contrast failure is fixed, both themes pass AA, the ProgressRing opportunistic fix is clean, and the ESLint rule closes the recurrence vector. All Decision Queue choices are implemented as agreed.
👨💻 Felix Brandt — Senior Fullstack Developer
Verdict: ✅ Approved
What I checked
Findings
Production fix is minimal and correct.
PdfControls.svelteline 94: single token swap, no other changes. No unrelated edits. The fix does exactly what it says.PdfControls.svelte.spec.tsis well-structured.defaultPropswith per-test overrides: correct.describegrouping by concern (visibility, label, contrast): readable.afterEach(cleanup): correct — prevents DOM bleed between tests.page.getByRole('button', { name: /…/i }): tests user-visible behavior, not internals.One style note on the contrast test (non-blocking):
The fifth test queries
container.querySelectorAll('button')and iterates withArray.from().find()to locate the annotation button byaria-label. This is more fragile than usingpage.getByRolewith the i18n name, and it asserts onclassNamedirectly — which is an implementation detail rather than user-visible behavior. For a contrast/class assertion this is pragmatically acceptable (there's no better DOM API for "what CSS class is applied"), but it's worth noting as the one test that reaches into internals.A slightly cleaner form:
ProgressRing.svelte.spec.tsupdate is correct.Test name updated from "mint-colored" to "primary-colored" — good. Assertion updated from
text-accenttotext-primary— correct. The test was green before (on wrong behavior) and is now green on the right behavior after the fix.ESLint rule selectors are precise.
Literal[value=/\btext-accent\b/]andTemplateLiteral > TemplateElement[value.raw=/\btext-accent\b/]— these two selectors together cover the realistic occurrence patterns in Svelte conditional class expressions. The word-boundary\bprevents false positives on hypothetical tokens liketext-accent-strong. The error message is actionable.Verdict
Clean, minimal fix. Tests are correct and follow the project's patterns. The ESLint rule is well-targeted. No blockers.
🏗️ Markus Keller — Senior Application Architect
Verdict: ✅ Approved
What I checked
Findings
No architectural concerns. This is a purely presentational fix contained entirely within the frontend component layer. No cross-domain calls, no layering violations, no new dependencies.
Token system is respected. The fix corrects a usage violation of an existing well-designed token —
--c-accentis correctly defined as decorative-only inlayout.css. The token definition itself was not changed. The architecture of the token system (palette constants → semantic tokens → Tailwind utilities) remains intact.ESLint rule placement is correct.
The rule is added inside the existing svelte-files config block in
eslint.config.js, scoped only to*.sveltefiles. This is the right placement — the constraint is Svelte-template-specific (class binding strings), not a general JS concern. It does not bleed into.tsfiles, so test assertions likeexpect(el.className).toContain('text-primary')are unaffected.Two selectors cover the realistic patterns. The
Literalselector catches'bg-surface/10 text-accent'-style string literals in ternary expressions. TheTemplateLiteral > TemplateElementselector catches template-literal variants. Both are the patterns seen in this codebase.Suggestion (non-blocking)
The rule currently lives at the bottom of the svelte config block with no visual separation from the surrounding config. Given that this is a documented accessibility constraint (not a typical code-style rule), a short section comment would help a future developer understand why it exists without reading
layout.css:The existing inline comment already says this — it's adequate. This is cosmetic only.
Verdict
The fix is architecturally clean. The lint rule is correctly scoped and well-targeted. The opportunistic ProgressRing fix is appropriate — it was caught by the same rule and fixed in the same PR, avoiding a lingering violation.
🔒 Nora "NullX" Steiner — Application Security Engineer
Verdict: ✅ Approved
What I checked
Findings
No security concerns. All changed lines are either static CSS class strings or static i18n keys. No user-controlled data is interpolated into class attributes or ARIA labels.
ARIA label is safe.
Both branches call Paraglide i18n functions that return static translated strings — not user data. No XSS vector here.
Annotation DOM-presence behavior confirmed benign.
As noted in the issue discussion, annotations are rendered only when
showAnnotationsistrue— the toggle controls DOM presence, not CSS visibility. This means hidden annotations are not in the DOM and cannot be extracted by an attacker via DOM inspection when toggled off. The fix does not change this behavior.ESLint rule introduces no security regression.
no-restricted-syntaxis a code quality rule operating at lint time. It does not affect runtime behavior. The selectors are AST-pattern based, not executable.ProgressRingfix carries no security implication.The percentage value rendered in the span is a numeric prop — not user-supplied string data that could be exploited via class injection.
Verdict
Clean bill of health. No security surface is affected by this PR.
🧪 Sara Holt — QA Engineer & Test Strategist
Verdict: ⚠️ Approved with concerns
What I checked
Blockers
None — the unit-level coverage gap for the toggle is now filled and the existing test suite is not broken.
Concerns
The axe-playwright E2E gate is still missing (non-blocking for this PR, but flagged as follow-up).
The issue acceptance criteria included "Fix verified with browser DevTools accessibility contrast checker." My recommendation in the issue discussion was to replace this manual step with an automated axe-playwright assertion. That assertion is not in this PR. The unit test in
PdfControls.svelte.spec.tsasserts the class name (text-primarynottext-accent), which proves the fix at the DOM layer — but it does not prove the computed contrast ratio in a real browser. A future regression that changes--c-primary's value without touching the class name would pass all current tests.Recommended follow-up (new issue or added to this PR as a second commit):
Suggestions (non-blocking)
PdfControls.svelte.spec.ts— contrast test usescontainer.querySelectorAll.The test works and the assertion is meaningful, but querying via
container.querySelectorAllandArray.from().find()is more fragile than usingpage.getByRole. Felix noted this too. For a class-level assertion it's the only viable approach, but it's worth a comment explaining why direct DOM querying is used here:Test coverage for ProgressRing contrast is correct but minimal.
The updated test verifies
text-primaryis present and implicitly thattext-accentis absent (since the token swap is the change). This is adequate for a one-line fix. The four existing ProgressRing tests are preserved without modification.Decision Queue item B (standalone spec file) was implemented correctly.
PdfControls.svelte.spec.tsis a separate file — the PDF rendering mocking complexity ofPdfViewer.svelte.spec.tsdoes not bleed into these tests. Good.Verdict
Unit coverage for the fixed component is now in place. The PR ships the bug fix with meaningful regression protection. The missing axe-playwright gate is the one gap — it should be tracked as a follow-up issue.
🛠️ Tobias Wendt — DevOps & Platform Engineer
Verdict: ✅ Approved
What I checked
Findings
Zero infrastructure impact. This PR changes three
.sveltefiles, one.spec.tsfile, andeslint.config.js. No Docker Compose, no CI workflow YAML, no environment variables, no backend changes.ESLint rule slots into existing
npm run lintcleanly.The
no-restricted-syntaxrule is added inside the existing svelte config block. ESLint picks it up automatically — no script changes, no new dev dependency, no CI step modification required. The rule runs on everynpm run lintinvocation.New test file
PdfControls.svelte.spec.tsuses the existing Vitest browser setup.It imports from
vitest-browser-svelteandvitest/browser— both are already inpackage.json. It usesafterEach(cleanup)correctly. The file follows the same pattern as other*.svelte.spec.tsfiles in the project — it will be picked up by the existingnpm run testscript without any configuration change.No new npm dependencies introduced. All imports in the new spec file are from packages already present in the project.
One observation
The CI pipeline currently has no axe-playwright job. Sara flagged this as a follow-up gap. When that job is eventually added, it will need a document fixture with annotations to test the PDF viewer page. That's a CI concern to address in that follow-up PR, not here.
Verdict
Clean drop-in. Existing CI runs unchanged. New test integrates with existing tooling.
📋 Elicit — Requirements Engineer
Verdict: ✅ Approved
What I checked
Findings
All stated acceptance criteria are met:
text-primary(#012851) on pdf-ctrl bg: >7:1 in both statestext-primary→#a1dcd8on#011526: ≈9:1text-primarynottext-accent; class name verifiableDecision Queue outcomes from issue #341 are all implemented:
text-primaryfor inactive state — ✅ implemented inPdfControls.sveltePdfControls.svelte.spec.ts— ✅ new file presentno-restricted-syntaxlint rule — ✅ added toeslint.config.jsScope is clean. The PR fixes the stated bug plus one opportunistic pre-existing violation in
ProgressRing.sveltethat was surfaced by the new lint rule. The ProgressRing change is in-scope: it is a direct consequence of applying the guardrail the team agreed to add, and it is covered by an updated test. No unrelated changes are present.One open requirement gap (non-blocking)
The acceptance criterion "Fix verified with browser DevTools accessibility contrast checker" is manual and non-repeatable, as noted in the issue discussion. The NFR I proposed (
NFR-ACC-341-01: zero axe WCAG2AA violations) is not yet automated. Sara has flagged this as a follow-up. The requirement is met at the class-assertion level but not at the computed-contrast level. This is a known gap, deliberately deferred — it should be tracked as a follow-up issue.Verdict
The PR fully delivers what was specified in issue #341 and implements all three Decision Queue resolutions. The scope is appropriate. One follow-up issue should be created for the axe-playwright E2E gate to close the NFR gap permanently.
🗳️ Decision Queue — PR #349
Consolidated open decisions from the persona reviews. All original Decision Queue items from issue #341 have been resolved and implemented. The following new items emerged from reviewing the implementation:
Theme A: Add axe-playwright E2E gate for PDF viewer WCAG compliance
From Sara & Elicit:
The unit tests assert the correct CSS class (
text-primarynottext-accent), but do not verify the computed contrast ratio in a real browser. A regression that changes the value of--c-primarywithout touching the class name would pass all current tests undetected.Should an axe-playwright E2E test be added to the existing
frontend/e2e/suite to verify WCAG 2.1 AA compliance on the PDF viewer page?Theme B: Fix annotation toggle touch target size
From Leonie:
The annotation toggle button is
px-2 py-1≈ 28×24px — below the 44×44px WCAG 2.2 and project standard minimum for the 60+ audience. This is a pre-existing issue not introduced by this PR.Should a follow-up issue be created to increase the touch target to
min-h-[44px] min-w-[44px] px-3 py-2?Decision Queue — Resolved
Both items from the Decision Queue have been actioned as follow-up issues:
Theme A: axe-playwright E2E gate
→ Issue #353: test(a11y): add axe-playwright E2E gate for PDF viewer WCAG 2.1 AA compliance
Labels:
test,ui,P2-mediumTheme B: annotation toggle touch target
→ Issue #354: fix(a11y): increase annotation toggle touch target to 44×44px minimum
Labels:
bug,ui,P2-mediumDecision Queue is now empty. This PR is ready to merge.