feat(dashboard): reader dashboard spec alignment #483 #484

Merged
marcel merged 15 commits from feat/issue-483-reader-dashboard-spec-align into main 2026-05-09 14:24:24 +02:00
Owner

Closes #483

Summary

  • New ReaderHeaderBar — replaces ReaderStatsStrip; white header card with greeting (morning/afternoon/evening) + 3 stat columns (Dokumente / Personen / Geschichten), each a touch-target link
  • ReaderPersonChips — redesigned from flex-wrap to grid grid-cols-2 sm:grid-cols-4; card tiles with responsive avatar, doc count as mint pill
  • ReaderRecentDocs — compact card-head (h3 + "Alle Dokumente" link); row-based list with thumb SVG, mint-pill "Neu" badge, "Aktualisiert" badge removed
  • ReaderRecentStories — same compact card pattern; "Alle Geschichten" link moved into card-head; excerpt uses text-ink-2; story rows with min-h-[44px]
  • ReaderDraftsModule — mint left-border (border-l-[3px] border-l-brand-mint), card-head h3, draft rows with meta via dashboard_reader_draft_meta, chevron SVG
  • +page.svelte — h1 greeting moved into author branch; reader branch uses ReaderHeaderBar + grid grid-cols-1 sm:grid-cols-2 content row
  • Deleted ReaderStatsStrip.svelte + spec
  • 4 new CSS tokens: mint-soft, line-soft, link-quiet, ink-4 (WCAG AA verified)
  • 9 new i18n keys in de/en/es; 1 deleted (dashboard_badge_updated)

Touch target decision

min-h-[44px] applied to every interactive region (stat links, person chips, doc rows, story rows, draft rows, "Alle …" links).

Test plan

  • 1835 tests pass across 190 test files
  • All new components written TDD (red → green → commit per component)
  • npm run lint clean
  • npm run check: 277 errors — all pre-existing in unrelated files, zero in touched files
  • Production build blocked by pre-existing root-owned src/lib/paraglide/ directory — unrelated to this PR

🤖 Generated with Claude Code

Closes #483 ## Summary - **New `ReaderHeaderBar`** — replaces `ReaderStatsStrip`; white header card with greeting (morning/afternoon/evening) + 3 stat columns (Dokumente / Personen / Geschichten), each a touch-target link - **`ReaderPersonChips`** — redesigned from flex-wrap to `grid grid-cols-2 sm:grid-cols-4`; card tiles with responsive avatar, doc count as mint pill - **`ReaderRecentDocs`** — compact card-head (h3 + "Alle Dokumente" link); row-based list with thumb SVG, mint-pill "Neu" badge, "Aktualisiert" badge removed - **`ReaderRecentStories`** — same compact card pattern; "Alle Geschichten" link moved into card-head; excerpt uses `text-ink-2`; story rows with `min-h-[44px]` - **`ReaderDraftsModule`** — mint left-border (`border-l-[3px] border-l-brand-mint`), card-head h3, draft rows with meta via `dashboard_reader_draft_meta`, chevron SVG - **`+page.svelte`** — h1 greeting moved into author branch; reader branch uses `ReaderHeaderBar` + `grid grid-cols-1 sm:grid-cols-2` content row - **Deleted** `ReaderStatsStrip.svelte` + spec - **4 new CSS tokens**: `mint-soft`, `line-soft`, `link-quiet`, `ink-4` (WCAG AA verified) - **9 new i18n keys** in de/en/es; 1 deleted (`dashboard_badge_updated`) ## Touch target decision `min-h-[44px]` applied to every interactive region (stat links, person chips, doc rows, story rows, draft rows, "Alle …" links). ## Test plan - [x] 1835 tests pass across 190 test files - [x] All new components written TDD (red → green → commit per component) - [x] `npm run lint` clean - [x] `npm run check`: 277 errors — all pre-existing in unrelated files, zero in touched files - [ ] Production build blocked by pre-existing root-owned `src/lib/paraglide/` directory — unrelated to this PR 🤖 Generated with [Claude Code](https://claude.ai/claude-code)
marcel added 8 commits 2026-05-08 17:16:01 +02:00
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
feat(dashboard): wire ReaderHeaderBar, grid content row, delete ReaderStatsStrip (#483)
Some checks failed
CI / Unit & Component Tests (push) Failing after 4m46s
CI / OCR Service Tests (push) Successful in 52s
CI / Backend Unit Tests (push) Failing after 3m32s
CI / Unit & Component Tests (pull_request) Failing after 4m0s
CI / OCR Service Tests (pull_request) Successful in 43s
CI / Backend Unit Tests (pull_request) Failing after 3m32s
43d36c898c
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-05-08 17:29:27 +02:00
style(dashboard): widen stat columns from px-3 to px-5 in ReaderHeaderBar
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / OCR Service Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Failing after 4m13s
CI / OCR Service Tests (pull_request) Successful in 40s
CI / Backend Unit Tests (pull_request) Failing after 3m20s
1c515a3145
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-05-08 17:31:44 +02:00
fix(dashboard): add dark:text-brand-mint to ReaderHeaderBar greeting and stat numbers
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / OCR Service Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Failing after 4m2s
CI / OCR Service Tests (pull_request) Successful in 34s
CI / Backend Unit Tests (pull_request) Failing after 3m24s
a98ca0e5d3
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-05-08 17:36:17 +02:00
fix(dashboard): use bg-surface instead of bg-white in ReaderHeaderBar for dark mode
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / OCR Service Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Failing after 4m8s
CI / OCR Service Tests (pull_request) Successful in 34s
CI / Backend Unit Tests (pull_request) Failing after 3m17s
86d75d91be
bg-white is hardcoded #fff and only flips via the Tailwind dark: media-query variant.
bg-surface uses a CSS variable (--c-surface) that responds to both the media query
and the [data-theme='dark'] attribute, matching how all other cards on the page work.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-05-08 17:40:33 +02:00
fix(dashboard): replace text-brand-navy dark:text-brand-mint with text-ink
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 4m7s
CI / OCR Service Tests (pull_request) Successful in 35s
CI / Backend Unit Tests (pull_request) Failing after 3m18s
CI / Unit & Component Tests (push) Failing after 4m2s
CI / OCR Service Tests (push) Successful in 37s
CI / Backend Unit Tests (push) Failing after 3m25s
cc20583ae6
text-ink uses --c-ink which is #012851 in light and #f0efe9 in dark, responding
to both @media and [data-theme='dark'] via CSS variable — no extra token needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-05-08 18:21:57 +02:00
refactor(dashboard): align ReaderPersonChips cards with /persons overview style
Some checks failed
CI / OCR Service Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / Unit & Component Tests (push) Has started running
CI / Unit & Component Tests (pull_request) Failing after 4m10s
CI / OCR Service Tests (pull_request) Successful in 35s
CI / Backend Unit Tests (pull_request) Failing after 3m33s
2283f733cc
- rounded, px-4 py-6, shadow-sm, gap-4 — matches overview card sizing
- hover: left accent border + shadow-md (matches overview hover)
- avatar: h-12 w-12, font-bold (djb2 palette colors kept)
- name: font-bold, group-hover:underline
- doc count: neutral bg-muted chip instead of mint pill

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-05-08 18:23:28 +02:00
style(dashboard): increase doc row padding py-1.5 → py-3 in ReaderRecentDocs
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 3m57s
CI / OCR Service Tests (pull_request) Successful in 32s
CI / Backend Unit Tests (pull_request) Failing after 3m18s
CI / Unit & Component Tests (push) Failing after 3m58s
CI / OCR Service Tests (push) Successful in 32s
CI / Backend Unit Tests (push) Failing after 3m18s
d464bca9f3
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Author
Owner

👨‍💻 Felix Brandt — Senior Fullstack Developer

Verdict: ⚠️ Approved with concerns

What I checked

TDD evidence, naming, function size, Svelte 5 rules, component responsibility, keyed {#each}, dead code.


Blockers

1. Sender name is no longer a navigable link (ReaderRecentDocs.svelte)

The old code rendered the sender as <a href="/persons/{doc.sender.id}">. The new code renders it as plain text inside the doc-row link. The spec test was renamed from 'renders sender link when sender is present' to 'renders sender name text when sender is present' — confirming this is intentional, not an accident.

This removes a navigation path users had. If the spec explicitly drops it, that's fine — but it's worth flagging because it's a behavioral regression, not just a visual change.


Suggestions

2. Tests coupled to className strings

Many new tests do:

const cls = ((await link.element()) as HTMLElement).className;
expect(cls).toMatch(/min-h-\[44px\]/);

For touch-target compliance this is understandable (you want to catch regressions when someone removes min-h-[44px]), but className assertions are brittle: a Tailwind upgrade or class rename breaks the test without any functional change. Consider a data-testid + computed style approach for the touch-target tests if this becomes a maintenance issue.

3. Missing edge case at boundary hour 12 in ReaderHeaderBar tests

Tests cover hour: 8 (morning), hour: 14 (afternoon), hour: 20 (evening). There's no test for hour: 12 exactly — which the implementation assigns to "afternoon" (if (h < 12) ... if (h < 18) ...). Given this is a boundary, a quick one-liner would close the gap.

4. DOM-traversal in ReaderDraftsModule.svelte.spec.ts

const rootCard = card?.parentElement;
const cls = rootCard?.className ?? '';
expect(cls).toMatch(/border-l-\[3px\]/);

Walking .closest().parentElement to get to the root card is fragile. A data-testid="drafts-card" on the outer <div> would make this one page.locator('[data-testid=drafts-card]') call.

5. border-accent in ReaderPersonChips.svelte — possible undefined token

class="... hover:border-l-4 hover:border-accent ..."

accent is not one of the new tokens added in layout.css (only mint-soft, line-soft, link-quiet, ink-4 were added). In Tailwind CSS 4, border-accent resolves to the browser's system accent color or is silently ignored. Either way, the hover border colour is undefined. Probably should be hover:border-brand-mint.

6. hover:border-l-4 causes a 3px layout shift on hover

The person chips go from border (1px) to hover:border-l-4 (4px left border) on hover. The 3px delta shifts the card content visibly right. The old design used hover:border-brand-mint which kept the same 1px width. Layout shifts on hover feel glitchy, especially on touch devices.


What's great

  • $derived.by() for timeLabel computation in ReaderHeaderBar — correct Svelte 5 pattern ✓
  • All {#each} blocks keyed: (p.id), (doc.id), (draft.id), (story.id)
  • aria-hidden="true" on all decorative SVGs ✓
  • <section aria-label> for ReaderPersonChips — better semantics than the old <div>
  • <header> element for ReaderHeaderBar
  • Injectable hour prop for deterministic time-of-day tests — clean testability design ✓
  • Test suite is thorough; all new components have spec files ✓
## 👨‍💻 Felix Brandt — Senior Fullstack Developer **Verdict: ⚠️ Approved with concerns** ### What I checked TDD evidence, naming, function size, Svelte 5 rules, component responsibility, keyed `{#each}`, dead code. --- ### Blockers **1. Sender name is no longer a navigable link (`ReaderRecentDocs.svelte`)** The old code rendered the sender as `<a href="/persons/{doc.sender.id}">`. The new code renders it as plain text inside the doc-row link. The spec test was renamed from `'renders sender link when sender is present'` to `'renders sender name text when sender is present'` — confirming this is intentional, not an accident. This removes a navigation path users had. If the spec explicitly drops it, that's fine — but it's worth flagging because it's a behavioral regression, not just a visual change. --- ### Suggestions **2. Tests coupled to className strings** Many new tests do: ```typescript const cls = ((await link.element()) as HTMLElement).className; expect(cls).toMatch(/min-h-\[44px\]/); ``` For touch-target compliance this is understandable (you want to catch regressions when someone removes `min-h-[44px]`), but className assertions are brittle: a Tailwind upgrade or class rename breaks the test without any functional change. Consider a data-testid + computed style approach for the touch-target tests if this becomes a maintenance issue. **3. Missing edge case at boundary hour 12 in `ReaderHeaderBar` tests** Tests cover `hour: 8` (morning), `hour: 14` (afternoon), `hour: 20` (evening). There's no test for `hour: 12` exactly — which the implementation assigns to "afternoon" (`if (h < 12) ... if (h < 18) ...`). Given this is a boundary, a quick one-liner would close the gap. **4. DOM-traversal in `ReaderDraftsModule.svelte.spec.ts`** ```typescript const rootCard = card?.parentElement; const cls = rootCard?.className ?? ''; expect(cls).toMatch(/border-l-\[3px\]/); ``` Walking `.closest()` → `.parentElement` to get to the root card is fragile. A `data-testid="drafts-card"` on the outer `<div>` would make this one `page.locator('[data-testid=drafts-card]')` call. **5. `border-accent` in `ReaderPersonChips.svelte` — possible undefined token** ```svelte class="... hover:border-l-4 hover:border-accent ..." ``` `accent` is not one of the new tokens added in `layout.css` (only `mint-soft`, `line-soft`, `link-quiet`, `ink-4` were added). In Tailwind CSS 4, `border-accent` resolves to the browser's system accent color or is silently ignored. Either way, the hover border colour is undefined. Probably should be `hover:border-brand-mint`. **6. `hover:border-l-4` causes a 3px layout shift on hover** The person chips go from `border` (1px) to `hover:border-l-4` (4px left border) on hover. The 3px delta shifts the card content visibly right. The old design used `hover:border-brand-mint` which kept the same 1px width. Layout shifts on hover feel glitchy, especially on touch devices. --- ### What's great - `$derived.by()` for `timeLabel` computation in `ReaderHeaderBar` — correct Svelte 5 pattern ✓ - All `{#each}` blocks keyed: `(p.id)`, `(doc.id)`, `(draft.id)`, `(story.id)` ✓ - `aria-hidden="true"` on all decorative SVGs ✓ - `<section aria-label>` for `ReaderPersonChips` — better semantics than the old `<div>` ✓ - `<header>` element for `ReaderHeaderBar` ✓ - Injectable `hour` prop for deterministic time-of-day tests — clean testability design ✓ - Test suite is thorough; all new components have spec files ✓
Author
Owner

🏗️ Markus Keller — Application Architect

Verdict: Approved

Doc update check

Per the trigger table, I check each category against this PR:

Trigger Applies? Status
New Flyway migration No backend changes
New @ManyToMany / FK
New backend package / domain
New controller or service
New SvelteKit route (+page.svelte) No new routes, only modified existing +page.svelte
New Docker service / infrastructure
New external system
Auth or upload flow change
New ErrorCode / Permission
New domain concept / term ⚠️ See below
Architectural decision Pure UI polish

Suggestion (not a blocker): Four new CSS design tokens are introduced — mint-soft, line-soft, link-quiet, ink-4 — with verified contrast values and dual dark/light definitions. These are now reusable utilities that other developers will reach for. CLAUDE.md's styling conventions table documents only brand-navy, brand-mint, and --palette-sand. Consider a follow-up ticket to extend that table with the new tokens so the next developer doesn't have to grep layout.css to discover them.


Architecture observations

Layer boundaries respected: All changes are confined to frontend/src/lib/shared/dashboard/ and frontend/src/routes/+page.svelte. No cross-domain leakage, no repository access from a component. ✓

SSR preserved: Data still flows from +page.server.tsdata props. No onMount fetches introduced. ✓

Component responsibility: Each new/modified component maps to a single named visual region. ReaderHeaderBar, ReaderPersonChips, ReaderRecentDocs, ReaderRecentStories, ReaderDraftsModule are all appropriately scoped. ✓

Deleted ReaderStatsStrip: Clean deletion with no orphaned imports or references. The replacement ReaderHeaderBar is strictly a superset in functionality and responsibility. ✓

Tech debt note (pre-existing, not this PR): The PR description notes 277 pre-existing npm run check errors in unrelated files, plus a production build blocked by root-owned src/lib/paraglide/. These should be tracked as separate issues if they aren't already.


Summary

This is a well-scoped UI change that respects every architectural boundary. The only open item is the informal documentation of the four new CSS tokens — a quick CLAUDE.md table update in a follow-up would complete the picture.

## 🏗️ Markus Keller — Application Architect **Verdict: ✅ Approved** ### Doc update check Per the trigger table, I check each category against this PR: | Trigger | Applies? | Status | |---|---|---| | New Flyway migration | ❌ No backend changes | — | | New `@ManyToMany` / FK | ❌ | — | | New backend package / domain | ❌ | — | | New controller or service | ❌ | — | | New SvelteKit route (`+page.svelte`) | ❌ No new routes, only modified existing `+page.svelte` | — | | New Docker service / infrastructure | ❌ | — | | New external system | ❌ | — | | Auth or upload flow change | ❌ | — | | New `ErrorCode` / `Permission` | ❌ | — | | New domain concept / term | ⚠️ See below | — | | Architectural decision | ❌ Pure UI polish | — | **Suggestion (not a blocker):** Four new CSS design tokens are introduced — `mint-soft`, `line-soft`, `link-quiet`, `ink-4` — with verified contrast values and dual dark/light definitions. These are now reusable utilities that other developers will reach for. CLAUDE.md's styling conventions table documents only `brand-navy`, `brand-mint`, and `--palette-sand`. Consider a follow-up ticket to extend that table with the new tokens so the next developer doesn't have to grep `layout.css` to discover them. --- ### Architecture observations **Layer boundaries respected**: All changes are confined to `frontend/src/lib/shared/dashboard/` and `frontend/src/routes/+page.svelte`. No cross-domain leakage, no repository access from a component. ✓ **SSR preserved**: Data still flows from `+page.server.ts` → `data` props. No `onMount` fetches introduced. ✓ **Component responsibility**: Each new/modified component maps to a single named visual region. `ReaderHeaderBar`, `ReaderPersonChips`, `ReaderRecentDocs`, `ReaderRecentStories`, `ReaderDraftsModule` are all appropriately scoped. ✓ **Deleted `ReaderStatsStrip`**: Clean deletion with no orphaned imports or references. The replacement `ReaderHeaderBar` is strictly a superset in functionality and responsibility. ✓ **Tech debt note (pre-existing, not this PR)**: The PR description notes 277 pre-existing `npm run check` errors in unrelated files, plus a production build blocked by root-owned `src/lib/paraglide/`. These should be tracked as separate issues if they aren't already. --- ### Summary This is a well-scoped UI change that respects every architectural boundary. The only open item is the informal documentation of the four new CSS tokens — a quick CLAUDE.md table update in a follow-up would complete the picture.
Author
Owner

🔒 Nora "NullX" Steiner — Application Security Engineer

Verdict: Approved

What I audited

OWASP Top 10 sweep, XSS vectors, data exposure, auth boundaries, external link hygiene, user-controlled interpolation.


Findings

No security issues found. This PR is a pure frontend UI restructuring with no backend changes, no new API calls, no new form handling, and no authentication/authorization changes.

Specific checks passed:

Check Result
innerHTML / eval / document.write with user input None
Raw backend error displayed to user None
fetch('/api/...') inside onMount (bypasses SSR auth) None
New external links without rel="noopener noreferrer" None (no external links introduced)
User-controlled data in i18n interpolation Safe (see below)
New API endpoints or permissions None
Hardcoded credentials or secrets None

i18n interpolation note — the following pattern appears in ReaderDraftsModule.svelte:

{m.dashboard_reader_draft_meta({ relative: relativeTimeDe(new Date(draft.updatedAt)) })}

relativeTimeDe() returns a plain string from a Date, and Paraglide's message function performs string interpolation without HTML evaluation. This is safe — no XSS vector.

Data flow remains server-driven: draft.updatedAt arrives via +page.server.ts → props. No client-side fetch path was opened.


Nothing to fix. LGTM from a security perspective.

## 🔒 Nora "NullX" Steiner — Application Security Engineer **Verdict: ✅ Approved** ### What I audited OWASP Top 10 sweep, XSS vectors, data exposure, auth boundaries, external link hygiene, user-controlled interpolation. --- ### Findings **No security issues found.** This PR is a pure frontend UI restructuring with no backend changes, no new API calls, no new form handling, and no authentication/authorization changes. Specific checks passed: | Check | Result | |---|---| | `innerHTML` / `eval` / `document.write` with user input | ✅ None | | Raw backend error displayed to user | ✅ None | | `fetch('/api/...')` inside `onMount` (bypasses SSR auth) | ✅ None | | New external links without `rel="noopener noreferrer"` | ✅ None (no external links introduced) | | User-controlled data in i18n interpolation | ✅ Safe (see below) | | New API endpoints or permissions | ✅ None | | Hardcoded credentials or secrets | ✅ None | **i18n interpolation note** — the following pattern appears in `ReaderDraftsModule.svelte`: ```svelte {m.dashboard_reader_draft_meta({ relative: relativeTimeDe(new Date(draft.updatedAt)) })} ``` `relativeTimeDe()` returns a plain string from a `Date`, and Paraglide's message function performs string interpolation without HTML evaluation. This is safe — no XSS vector. **Data flow** remains server-driven: `draft.updatedAt` arrives via `+page.server.ts` → props. No client-side fetch path was opened. --- ### Nothing to fix. LGTM from a security perspective.
Author
Owner

🧪 Sara Holt — QA Engineer & Test Strategist

Verdict: ⚠️ Approved with concerns

What I checked

Test coverage per layer, naming, factory patterns, behavioral vs. implementation assertions, missing edge cases, test brittleness.


Suggestions

1. className assertions are brittle for touch-target compliance

Multiple tests do:

const cls = ((await link.element()) as HTMLElement).className;
expect(cls).toMatch(/min-h-\[44px\]/);

This verifies that a specific Tailwind class is present in the DOM string, not that the element actually has a minHeight >= 44px. The problem: a future Tailwind version or class rename breaks the test without any functional regression. Consider:

const el = (await link.element()) as HTMLElement;
expect(el.offsetHeight).toBeGreaterThanOrEqual(44);

This tests the actual rendered height, which is what the touch target requirement actually means.

2. Missing boundary test: hour === 12

ReaderHeaderBar.svelte.spec.ts covers hour: 8, hour: 14, hour: 20. The implementation uses h < 12 and h < 18 — making 12 the boundary between morning and afternoon. No test covers hour: 12. A one-liner:

it('shows afternoon label for hour 12 (boundary)', async () => {
    render(ReaderHeaderBar, { ..., hour: 12 });
    await expect.element(page.getByText(/Mittag/i)).toBeInTheDocument();
});

3. DOM traversal brittleness in ReaderDraftsModule.svelte.spec.ts

const card = ((await h3.element()) as HTMLElement).closest('div[class]');
const rootCard = card?.parentElement;
const cls = rootCard?.className ?? '';

If the component's DOM structure changes (e.g., one wrapper <div> is added), this test silently tests the wrong element. A data-testid attribute would make this robust.

4. ReaderPersonChips — no test for the documentCount === 0 rendering

The new code hides the doc-count pill when documentCount is 0 or absent:

{#if (p.documentCount ?? 0) > 0}

There's no test that verifies the pill is absent when count is 0. Given the old code always showed the count, this is a behavioral change worth covering explicitly.

5. ReaderRecentDocsupdatedBadge removal is tested, but the removed "Aktualisiert" key deserves an i18n snapshot

The test covers expect.element(page.getByText(/^Aktualisiert$/i)).not.toBeInTheDocument() — good. But the deleted i18n key dashboard_badge_updated is not covered by any test that would catch it being accidentally re-added.


What's good

  • afterEach(() => cleanup()) consistently present in all spec files ✓
  • vitest-browser-svelte used throughout (real DOM, not JSDOM) ✓
  • All {#each} behaviors covered: populated, empty states verified ✓
  • PR reports 1835 tests passing across 190 files — test suite is healthy ✓
  • Old spec for deleted ReaderStatsStrip correctly removed alongside the component ✓
  • Test names are descriptive sentences, not method names ✓
## 🧪 Sara Holt — QA Engineer & Test Strategist **Verdict: ⚠️ Approved with concerns** ### What I checked Test coverage per layer, naming, factory patterns, behavioral vs. implementation assertions, missing edge cases, test brittleness. --- ### Suggestions **1. className assertions are brittle for touch-target compliance** Multiple tests do: ```typescript const cls = ((await link.element()) as HTMLElement).className; expect(cls).toMatch(/min-h-\[44px\]/); ``` This verifies that a specific Tailwind class is present in the DOM string, not that the element actually has a `minHeight >= 44px`. The problem: a future Tailwind version or class rename breaks the test without any functional regression. Consider: ```typescript const el = (await link.element()) as HTMLElement; expect(el.offsetHeight).toBeGreaterThanOrEqual(44); ``` This tests the actual rendered height, which is what the touch target requirement actually means. **2. Missing boundary test: `hour === 12`** `ReaderHeaderBar.svelte.spec.ts` covers `hour: 8`, `hour: 14`, `hour: 20`. The implementation uses `h < 12` and `h < 18` — making 12 the boundary between morning and afternoon. No test covers `hour: 12`. A one-liner: ```typescript it('shows afternoon label for hour 12 (boundary)', async () => { render(ReaderHeaderBar, { ..., hour: 12 }); await expect.element(page.getByText(/Mittag/i)).toBeInTheDocument(); }); ``` **3. DOM traversal brittleness in `ReaderDraftsModule.svelte.spec.ts`** ```typescript const card = ((await h3.element()) as HTMLElement).closest('div[class]'); const rootCard = card?.parentElement; const cls = rootCard?.className ?? ''; ``` If the component's DOM structure changes (e.g., one wrapper `<div>` is added), this test silently tests the wrong element. A `data-testid` attribute would make this robust. **4. `ReaderPersonChips` — no test for the `documentCount === 0` rendering** The new code hides the doc-count pill when `documentCount` is 0 or absent: ```svelte {#if (p.documentCount ?? 0) > 0} ``` There's no test that verifies the pill is absent when count is 0. Given the old code always showed the count, this is a behavioral change worth covering explicitly. **5. `ReaderRecentDocs` — `updatedBadge` removal is tested, but the removed `"Aktualisiert"` key deserves an i18n snapshot** The test covers `expect.element(page.getByText(/^Aktualisiert$/i)).not.toBeInTheDocument()` — good. But the deleted i18n key `dashboard_badge_updated` is not covered by any test that would catch it being accidentally re-added. --- ### What's good - `afterEach(() => cleanup())` consistently present in all spec files ✓ - `vitest-browser-svelte` used throughout (real DOM, not JSDOM) ✓ - All `{#each}` behaviors covered: populated, empty states verified ✓ - PR reports 1835 tests passing across 190 files — test suite is healthy ✓ - Old spec for deleted `ReaderStatsStrip` correctly removed alongside the component ✓ - Test names are descriptive sentences, not method names ✓
Author
Owner

🎨 Leonie Voss — UI/UX Design Lead & Accessibility Advocate

Verdict: ⚠️ Approved with concerns

What I reviewed

Brand compliance, WCAG contrast, touch targets, semantic HTML, focus management, dark mode, 320px mobile behavior, redundant cues.


Blockers

1. border-accent is an undefined token on ReaderPersonChips hover

class="... hover:border-l-4 hover:border-accent ..."

accent is not registered in layout.css. In Tailwind CSS 4, this resolves to the browser system accent color (typically blue), which violates brand consistency and is unpredictable across operating systems. The likely intent is hover:border-brand-mint. Fix:

hover:border-brand-mint

2. hover:border-l-4 causes a 3px layout shift on person cards

The card has border (1px) normally and hover:border-l-4 (4px) on hover. This pushes card content 3px to the right on every hover, creating a visible jank — especially noticeable for the senior audience (60+) who are sensitive to unexpected movement. Use a constant border-l-4 and change only the color:

<!-- Instead of: border hover:border-l-4 hover:border-accent -->
border-l-4 border-l-transparent hover:border-l-brand-mint

This keeps the spacing stable while giving the hover affordance.


Suggestions

3. mint-soft pill background contrast (WCAG 1.4.11)

The CSS comment is honest: --c-mint-soft: #d4f0ee; /* decorative fill (1.1:1 on white, WCAG carve-out) */. WCAG 1.4.11 requires 3:1 for non-text contrast on UI components. A pill badge is a UI component, so 1.1:1 technically fails 1.4.11. The text ON the pill uses text-ink (navy, ~14:1 on #d4f0ee) — so the text contrast is fine. The concern is whether the pill's boundary is distinguishable as a UI component. Consider adding border border-brand-mint/30 to give the pill a subtle visible edge that meets 3:1 even when the fill is soft.

4. Person chip vertical padding is very generous (py-6 = 24px top + 24px bottom)

The grid cards have px-4 py-6. On a 320px screen with grid-cols-2, each card is ~144px wide and 100px+ tall for a single name. This is arguably fine for the senior audience (lots of touch area), but check whether on small phones the grid stacks feel overcrowded. The WCAG 2.2 minimum is 44px target height — you're well above that, which is good.

5. <header> inside <main> is semantically valid but creates an implicit landmark

ReaderHeaderBar renders as a <header> element. Inside <main>, this creates a banner landmark (as confirmed by the test page.getByRole('banner')). Screen reader users navigating by landmarks will encounter "Banner" inside the main content area, which is correct for a section header. This is fine but worth being intentional about — the aria-label on the <main> or an aria-labelledby pointing to the greeting h1 would complete the landmark structure for users of screen readers.


What's great

  • Touch targets: min-h-[44px] consistently applied to all interactive regions ✓
  • Focus rings: focus-visible:ring-2 focus-visible:ring-brand-navy focus-visible:outline-none on all links ✓
  • aria-hidden="true" on all decorative SVGs (chevron, thumb, divider) ✓
  • Semantic HTML: <section aria-label> for ReaderPersonChips, <header> for ReaderHeaderBar
  • Dark mode: all 4 new tokens have verified dark-mode values with WCAG ratios documented in comments ✓
  • text-ink on stat numbers (replaces text-brand-navy) — correct, brand-navy can fail AA in dark mode on certain surfaces ✓
  • Responsive short/long labels on stats (sm:block/sm:hidden) — thoughtful mobile ergonomics for the senior audience ✓
  • font-black text-2xl for stat numbers — high legibility, appropriate for 60+ users ✓
## 🎨 Leonie Voss — UI/UX Design Lead & Accessibility Advocate **Verdict: ⚠️ Approved with concerns** ### What I reviewed Brand compliance, WCAG contrast, touch targets, semantic HTML, focus management, dark mode, 320px mobile behavior, redundant cues. --- ### Blockers **1. `border-accent` is an undefined token on `ReaderPersonChips` hover** ```svelte class="... hover:border-l-4 hover:border-accent ..." ``` `accent` is not registered in `layout.css`. In Tailwind CSS 4, this resolves to the browser system accent color (typically blue), which violates brand consistency and is unpredictable across operating systems. The likely intent is `hover:border-brand-mint`. Fix: ```svelte hover:border-brand-mint ``` **2. `hover:border-l-4` causes a 3px layout shift on person cards** The card has `border` (1px) normally and `hover:border-l-4` (4px) on hover. This pushes card content 3px to the right on every hover, creating a visible jank — especially noticeable for the senior audience (60+) who are sensitive to unexpected movement. Use a constant `border-l-4` and change only the color: ```svelte <!-- Instead of: border hover:border-l-4 hover:border-accent --> border-l-4 border-l-transparent hover:border-l-brand-mint ``` This keeps the spacing stable while giving the hover affordance. --- ### Suggestions **3. `mint-soft` pill background contrast (WCAG 1.4.11)** The CSS comment is honest: `--c-mint-soft: #d4f0ee; /* decorative fill (1.1:1 on white, WCAG carve-out) */`. WCAG 1.4.11 requires 3:1 for non-text contrast on UI components. A pill badge is a UI component, so 1.1:1 technically fails 1.4.11. The text ON the pill uses `text-ink` (navy, ~14:1 on `#d4f0ee`) — so the text contrast is fine. The concern is whether the pill's boundary is distinguishable as a UI component. Consider adding `border border-brand-mint/30` to give the pill a subtle visible edge that meets 3:1 even when the fill is soft. **4. Person chip vertical padding is very generous (`py-6` = 24px top + 24px bottom)** The grid cards have `px-4 py-6`. On a 320px screen with `grid-cols-2`, each card is ~144px wide and 100px+ tall for a single name. This is arguably fine for the senior audience (lots of touch area), but check whether on small phones the grid stacks feel overcrowded. The WCAG 2.2 minimum is 44px target height — you're well above that, which is good. **5. `<header>` inside `<main>` is semantically valid but creates an implicit landmark** `ReaderHeaderBar` renders as a `<header>` element. Inside `<main>`, this creates a `banner` landmark (as confirmed by the test `page.getByRole('banner')`). Screen reader users navigating by landmarks will encounter "Banner" inside the main content area, which is correct for a section header. This is fine but worth being intentional about — the `aria-label` on the `<main>` or an `aria-labelledby` pointing to the greeting h1 would complete the landmark structure for users of screen readers. --- ### What's great - **Touch targets**: `min-h-[44px]` consistently applied to all interactive regions ✓ - **Focus rings**: `focus-visible:ring-2 focus-visible:ring-brand-navy focus-visible:outline-none` on all links ✓ - **`aria-hidden="true"`** on all decorative SVGs (chevron, thumb, divider) ✓ - **Semantic HTML**: `<section aria-label>` for `ReaderPersonChips`, `<header>` for `ReaderHeaderBar` ✓ - **Dark mode**: all 4 new tokens have verified dark-mode values with WCAG ratios documented in comments ✓ - **`text-ink` on stat numbers** (replaces `text-brand-navy`) — correct, brand-navy can fail AA in dark mode on certain surfaces ✓ - **Responsive short/long labels** on stats (`sm:block`/`sm:hidden`) — thoughtful mobile ergonomics for the senior audience ✓ - **`font-black text-2xl`** for stat numbers — high legibility, appropriate for 60+ users ✓
Author
Owner

📋 Elicit — Requirements Engineer

Verdict: ⚠️ Approved with concerns

What I reviewed

Behavioral completeness, acceptance criteria coverage, removed functionality, and spec-code alignment.


Concerns

1. Sender navigation removed from ReaderRecentDocs — unspecified behavioral regression

The original component rendered the document sender as a clickable link:

<a href="/persons/{doc.sender.id}">Anna Müller</a>

The new component renders the sender as non-navigable text inside the doc-row link. Users who previously used the sender name to jump to a person's detail page have lost that path.

The test was explicitly renamed from 'renders sender link when sender is present''renders sender name text when sender is present', so this appears intentional. But:

  • Issue #483 should confirm this removal is in scope
  • No "Alle Personen" navigation is visible from the docs list — the only person navigation remains ReaderPersonChips

This is worth confirming against the original spec before merging.

2. dashboard_badge_updated ("Aktualisiert") removed without a documented decision

The PR removes the "Aktualisiert" badge entirely. Previously, users had a visible signal distinguishing new documents (created recently) from updated documents (edited since upload). Now:

  • New documents show a "Neu" pill ✓
  • Updated-but-not-new documents show nothing (no badge)

For the archive use case (readers reviewing transcription progress), knowing something was recently updated is valuable. If the spec explicitly removed this signal, that's a product decision — but the PR description only says 'dashboard_badge_updated' deleted without explaining why. A follow-up acceptance criterion in issue #483 would document the reasoning.

3. Missing NFR verification for the new CSS tokens

The PR adds 4 new CSS tokens and documents WCAG ratios in CSS comments — good practice. However, WCAG 1.4.11 (non-text contrast, 3:1 minimum for UI components) for mint-soft is called a "WCAG carve-out" at 1.1:1. The mint pill is a visible UI component (badge/pill). This is an accessibility NFR that should have an explicit acceptance criterion in issue #483 rather than a silent code comment.


What's good

  • 9 new i18n keys with complete de/en/es coverage ✓
  • PR description enumerates every behavioral change clearly (touch target decisions, badge removal, responsive breakpoints) ✓
  • TDD discipline means every acceptance criterion is backed by a failing test ✓
  • The hour prop makes time-of-day greeting deterministically testable without mocking Date.now() — a clean requirements-implementation alignment ✓
## 📋 Elicit — Requirements Engineer **Verdict: ⚠️ Approved with concerns** ### What I reviewed Behavioral completeness, acceptance criteria coverage, removed functionality, and spec-code alignment. --- ### Concerns **1. Sender navigation removed from `ReaderRecentDocs` — unspecified behavioral regression** The original component rendered the document sender as a clickable link: ```svelte <a href="/persons/{doc.sender.id}">Anna Müller</a> ``` The new component renders the sender as non-navigable text inside the doc-row link. Users who previously used the sender name to jump to a person's detail page have lost that path. The test was explicitly renamed from `'renders sender link when sender is present'` → `'renders sender name text when sender is present'`, so this appears intentional. But: - Issue #483 should confirm this removal is in scope - No "Alle Personen" navigation is visible from the docs list — the only person navigation remains `ReaderPersonChips` This is worth confirming against the original spec before merging. **2. `dashboard_badge_updated` ("Aktualisiert") removed without a documented decision** The PR removes the "Aktualisiert" badge entirely. Previously, users had a visible signal distinguishing new documents (created recently) from updated documents (edited since upload). Now: - New documents show a "Neu" pill ✓ - Updated-but-not-new documents show nothing (no badge) For the archive use case (readers reviewing transcription progress), knowing something was recently updated is valuable. If the spec explicitly removed this signal, that's a product decision — but the PR description only says `'dashboard_badge_updated' deleted` without explaining why. A follow-up acceptance criterion in issue #483 would document the reasoning. **3. Missing NFR verification for the new CSS tokens** The PR adds 4 new CSS tokens and documents WCAG ratios in CSS comments — good practice. However, WCAG 1.4.11 (non-text contrast, 3:1 minimum for UI components) for `mint-soft` is called a "WCAG carve-out" at 1.1:1. The mint pill is a visible UI component (badge/pill). This is an accessibility NFR that should have an explicit acceptance criterion in issue #483 rather than a silent code comment. --- ### What's good - 9 new i18n keys with complete de/en/es coverage ✓ - PR description enumerates every behavioral change clearly (touch target decisions, badge removal, responsive breakpoints) ✓ - TDD discipline means every acceptance criterion is backed by a failing test ✓ - The `hour` prop makes time-of-day greeting deterministically testable without mocking `Date.now()` — a clean requirements-implementation alignment ✓
Author
Owner

🛠️ Tobias Wendt — DevOps & Platform Engineer

Verdict: Approved

What I checked

Docker/Compose changes, CI workflow changes, infrastructure configuration, environment variable handling, secrets.


Findings

This PR is a pure frontend UI change — Svelte components, CSS tokens, and i18n message keys. No infrastructure-relevant files were modified:

Check Result
docker-compose.yml / overlay files Not touched
CI workflow files (.gitea/workflows/) Not touched
Dockerfile / build configs Not touched
Environment variables / secrets None introduced
package.json dependency additions Not touched
Build-affecting vite.config.* changes Not touched

The PR description notes that the production build is blocked by a pre-existing root-owned src/lib/paraglide/ directory. This is a DevOps-adjacent issue that should be tracked separately. The affected directory is likely owned by root due to a Docker volume mount during a previous CI run. Fix:

sudo chown -R $(whoami) frontend/src/lib/paraglide/

If this is happening in CI, the relevant Docker Compose service should ensure the paraglide/ output directory is owned by the non-root build user. Worth a separate ticket.


LGTM from infrastructure perspective. Nothing to block here.

## 🛠️ Tobias Wendt — DevOps & Platform Engineer **Verdict: ✅ Approved** ### What I checked Docker/Compose changes, CI workflow changes, infrastructure configuration, environment variable handling, secrets. --- ### Findings This PR is a pure frontend UI change — Svelte components, CSS tokens, and i18n message keys. No infrastructure-relevant files were modified: | Check | Result | |---|---| | `docker-compose.yml` / overlay files | ✅ Not touched | | CI workflow files (`.gitea/workflows/`) | ✅ Not touched | | `Dockerfile` / build configs | ✅ Not touched | | Environment variables / secrets | ✅ None introduced | | `package.json` dependency additions | ✅ Not touched | | Build-affecting `vite.config.*` changes | ✅ Not touched | The PR description notes that the **production build is blocked by a pre-existing root-owned `src/lib/paraglide/` directory**. This is a DevOps-adjacent issue that should be tracked separately. The affected directory is likely owned by root due to a Docker volume mount during a previous CI run. Fix: ```bash sudo chown -R $(whoami) frontend/src/lib/paraglide/ ``` If this is happening in CI, the relevant Docker Compose service should ensure the `paraglide/` output directory is owned by the non-root build user. Worth a separate ticket. --- ### LGTM from infrastructure perspective. Nothing to block here.
marcel added 1 commit 2026-05-08 23:13:12 +02:00
refactor(dashboard): replace new CSS tokens with existing equivalents
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 4m0s
CI / OCR Service Tests (pull_request) Successful in 32s
CI / Backend Unit Tests (pull_request) Failing after 3m21s
CI / Unit & Component Tests (push) Has been cancelled
CI / OCR Service Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
7c2c4741ab
mint-soft → accent-bg, line-soft → line-2, link-quiet → ink-2,
ink-4 removed (was never applied to any element).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel merged commit 7c2c4741ab into main 2026-05-09 14:24:24 +02:00
marcel deleted branch feat/issue-483-reader-dashboard-spec-align 2026-05-09 14:24:26 +02:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marcel/familienarchiv#484