feat(mobile): reader surfaces (Home · /documents · /briefwechsel · /persons) pass mobile-first bar at 375 px #318

Open
opened 2026-04-24 13:22:03 +02:00 by marcel · 8 comments
Owner

Context

The stated target audience for the archive — the younger generation of the family — will mostly consume letters on phones. The transcriber persona (family members 60+ with Kurrent literacy) works on laptops/tablets, so the authoring surface is allowed to stay desktop-first. This issue scopes the mobile-first bar for the reader/consumer surface only.

Current state, captured in a 375×812 Playwright sweep on 2026-04-24: every reader-facing surface shows a visible horizontal scrollbar at 375 px and clips content off the right edge.

Surface Symptom at 375 px
Home dashboard Resume hero card content spills horizontally; title truncated mid-word
/documents Filter bar (search · date · filter · +new) overflows; cards push right
/briefwechsel Hero typeahead fits, but header-nav overflow scrolls
/persons Person cards clip right edge; "13 docs" chip cropped

Evidence screenshots at /tmp/fa-audit/D1-…D4-*.png (captured during Phase B2 UX audit). The common root cause is a fixed desktop grid / wide paddings without md: / lg: breakpoint narrowing.

Sister issue #95 addressed the save-bar button wrap on the authoring edit page at 320 px — narrow, one-page fix. This issue is the systemic counterpart for the reader path: every route a reader visits must pass a mobile-first bar defined below.

Non-goals

  • Authoring surfaces stay desktop/tablet-first: /documents/new, /documents/[id]/edit, the Transcribe panel on /documents/[id], and every /admin/* surface. Their narrow-viewport issues are separate (or already addressed by #95).
  • No new reader-only components — this issue is responsive behaviour of existing components.
  • No design-system rewrite — work within the current brand-navy/mint/sand palette and Tailwind 4 utilities already in use.

Mobile-first bar (the shared contract this issue enforces)

A reader surface passes iff, at a viewport of 375 × 812:

  1. document.documentElement.scrollWidth <= 375 — no horizontal overflow.
  2. No content is clipped by overflow: hidden inside a cropped container that has no alternative way to see the clipped information.
  3. Every interactive element (button, link, chip, icon-button) has computed min-height ≥ 44 px AND min-width ≥ 44 px (WCAG 2.5.5).
  4. Body text is at least 16 px (prevents iOS Safari auto-zoom when a nearby input is focused).
  5. Primary actions (e.g. "Read", "Open", "Continue transcribing") are reachable without a second tap through a kebab menu.
  6. Dark-mode renders without contrast regressions (axe-core passes AA in both modes — see #312 for the thumbnail precedent).

In-scope routes and the known-bad spots

/ (Home) — reader parts only

Reader-facing on Home: the Resume Strip ("Continue where you left off"), the Comments & Activity feed, and What Needs Attention counts. The transcriber queues (Mark Text · Transcribe Text · Ready to Read) are allowed to collapse behind a "Transcriber Tools" disclosure on sm: screens.

Known bad at 375 px: Resume hero card is a two-column grid; the right column (metadata + progress ring + CTA) forces its own fixed width, pushing the left column (thumbnail + first-line preview) off-screen.

/documents — list

  • Filter bar (search + Date sort + Filter + "+ New document") horizontally overflows. On narrow screens these stack: search on its own line, then sort + filter, and "+ New document" as a floating action or moved to the empty-state.
  • Document cards render at max-w-none — should be w-full inside a container that honours the viewport.
  • Group headers ("UNDATED", year) are fine; keep them.

Depends on #315/#316 (pagination) — do not re-regress: whatever this issue ships must keep the paginated-list-first contract.

/documents/[id] — detail (read mode)

  • Header row with sender→receiver chips + Details / Transcribe / Edit actions overflows. Transcribe and Edit are authoring actions; on sm: they collapse behind a single kebab menu so only Details remains visible as a reader action.
  • PDF/image viewer — object-contain + max-w-full + respect safe area; zoom controls (already present) stay visible at the bottom.
  • Annotations overlay keeps working on mobile; "Hide annotations" link moves into the kebab.

/briefwechsel — Letters

  • Entry page (typeahead) already fits at 375 px — minor CSS tune to centre vertically inside the viewport minus header.
  • Conversation detail — being iterated on in #224 / #306. Verify mobile while shipping those; this issue scopes the entry state only.

/persons — list

  • Person cards (avatar + name + alias + doc count chip) clip at 375 px. Card padding shrinks (p-4p-3 on sm:), alias collapses to a second line instead of a third column.
  • Search bar is already full-width — good. Group letter headers stay.

/persons/[id] — detail

  • Two-column layout collapses to one column on sm: with correspondents as a horizontally scrollable chip rail (opt-in horizontal scroll is fine; involuntary overflow is not).
  • Sent / Received list items wrap cleanly; chevron stays right-aligned.

Out of scope (explicit)

  • Dark-mode bugs unrelated to mobile (file separately).
  • Admin master-detail pages — authoring surface.
  • Document edit form and save bar — #95 tracks its symptom.
  • PDF viewer pinch-zoom improvements — separate perf issue.
  • New mobile-only features (bottom-bar nav, PWA shell, offline) — separate follow-ups.

Implementation plan

1. Tailwind breakpoint discipline

  • Convention: reader surfaces are mobile-first — no class without a breakpoint prefix assumes desktop. Desktop styles opt-in via md: / lg:.
  • Add a lint rule (or svelte-check) that flags flex-row / grid-cols-{n≥2} / w-[≥500px] without a breakpoint prefix on files under routes/+page.svelte, routes/documents/*, routes/persons/*, routes/briefwechsel/*. Warn-on-PR is enough.

2. Route-by-route fixes (one commit per route)

Each commit touches listed files AND adds/updates a matching spec in frontend/e2e/responsive/*.spec.ts.

  • Homefrontend/src/routes/+page.svelte; split hero into sm:grid-cols-1 md:grid-cols-[auto_1fr]; thumbnail shrinks at sm:.
  • Documents listfrontend/src/routes/documents/+page.svelte; filter bar flex-col sm:flex-row.
  • Document detailfrontend/src/routes/documents/[id]/+page.svelte; action cluster collapses into a new KebabMenu.svelte at sm:.
  • Briefwechsel entryfrontend/src/routes/briefwechsel/+page.svelte; confirm with a snapshot.
  • Persons listfrontend/src/routes/persons/+page.svelte + PersonCard.svelte (verify name).
  • Person detailfrontend/src/routes/persons/[id]/+page.svelte; correspondents row becomes overflow-x-auto snap-x snap-mandatory on sm:.

3. Global layout

  • frontend/src/routes/+layout.svelte — verify Menü öffnen toggle carries aria-expanded; drawer closes on route change.
  • Header at sm: shows: logo + burger + upload icon; locale, dark-mode, notifications, and avatar move inside the drawer.

4. i18n

No new user-visible strings expected. If any are added (e.g. a kebab "More actions" label), add keys to frontend/messages/{de,en,es}.json.

Tests

Frontend e2e (Playwright)

New folder frontend/e2e/responsive/ with one spec per route:

  • home.responsive.spec.ts
  • documents-list.responsive.spec.ts
  • document-detail.responsive.spec.ts
  • briefwechsel-entry.responsive.spec.ts
  • persons-list.responsive.spec.ts
  • person-detail.responsive.spec.ts

Each spec:

  1. page.setViewportSize({ width: 375, height: 812 }).
  2. Navigate with a signed-in admin session (reuse existing fixture).
  3. Assert await page.evaluate(() => document.documentElement.scrollWidth) <= 375.
  4. For every a, button matched by page.locator('a, button'): assert boundingBox().height >= 44 and width >= 44 (with opt-out list for decorative icons).
  5. Snapshot into e2e/snapshots/responsive/ — fails if pixel delta > threshold. Re-enable #124 for this.

Add a shared helper frontend/e2e/responsive/mobile.helper.ts exporting viewport setup + the three standard assertions.

axe-core

Extend existing axe sweeps to run each route at 375 px in both light and dark mode; fail on any new violations (baseline snapshot, then diff).

Verification

  1. cd frontend && npm run test — unit tests pass.
  2. cd frontend && npx playwright test e2e/responsive — all six specs pass.
  3. Manual smoke on a real phone (iPhone Safari, Android Chrome): sign in → home → open a document → persons → person detail → briefwechsel. No horizontal scroll, no action hidden off-screen, no tap target < 44 px.
  4. Re-run the B2 evidence pass from 2026-04-24 — every D*.png shows no scrollbar.

Acceptance criteria

  • All six reader routes pass the mobile-first bar at 375 × 812
  • Six Playwright responsive specs exist and pass in CI
  • axe-core passes at 375 px in both light and dark mode on all six routes
  • Documents list stays paginated (no regression of #315)
  • Authoring surfaces unchanged — no responsive work leaks into them
  • Linter (or svelte-check rule) warns on flex-row / multi-column grid without breakpoint prefix inside reader routes
  • No new user-visible strings are untranslated in de/en/es

Out-of-band dependencies

  • Re-enable #124 (Playwright visual regression — currently descoped).
  • Coordinate with #315/#316 — merge first; this issue must not regress pagination.

Critical files

frontend/src/routes/+layout.svelte
frontend/src/routes/+page.svelte
frontend/src/routes/documents/+page.svelte
frontend/src/routes/documents/[id]/+page.svelte
frontend/src/routes/briefwechsel/+page.svelte
frontend/src/routes/persons/+page.svelte
frontend/src/routes/persons/[id]/+page.svelte
frontend/src/lib/components/DocumentRow.svelte          (verify name)
frontend/src/lib/components/PersonCard.svelte           (verify name)
frontend/src/lib/components/dashboard/ResumeHero.svelte (verify name)
frontend/src/lib/components/KebabMenu.svelte            (new)
frontend/e2e/responsive/*.spec.ts                       (new folder)
frontend/e2e/responsive/mobile.helper.ts                (new)
## Context The stated target audience for the archive — the younger generation of the family — will mostly consume letters on phones. The transcriber persona (family members 60+ with Kurrent literacy) works on laptops/tablets, so the authoring surface is allowed to stay desktop-first. This issue scopes the mobile-first bar for the **reader/consumer surface only.** Current state, captured in a 375×812 Playwright sweep on 2026-04-24: every reader-facing surface shows a visible horizontal scrollbar at 375 px and clips content off the right edge. | Surface | Symptom at 375 px | |------------------|--------------------------------------------------------------------------| | Home dashboard | Resume hero card content spills horizontally; title truncated mid-word | | `/documents` | Filter bar (search · date · filter · +new) overflows; cards push right | | `/briefwechsel` | Hero typeahead fits, but header-nav overflow scrolls | | `/persons` | Person cards clip right edge; "13 docs" chip cropped | Evidence screenshots at `/tmp/fa-audit/D1-…D4-*.png` (captured during Phase B2 UX audit). The common root cause is a fixed desktop grid / wide paddings without `md:` / `lg:` breakpoint narrowing. Sister issue #95 addressed the save-bar button wrap on the authoring edit page at 320 px — narrow, one-page fix. This issue is the systemic counterpart for the reader path: every route a reader visits must pass a mobile-first bar defined below. ## Non-goals - Authoring surfaces stay desktop/tablet-first: `/documents/new`, `/documents/[id]/edit`, the Transcribe panel on `/documents/[id]`, and every `/admin/*` surface. Their narrow-viewport issues are separate (or already addressed by #95). - No new reader-only components — this issue is responsive behaviour of existing components. - No design-system rewrite — work within the current brand-navy/mint/sand palette and Tailwind 4 utilities already in use. ## Mobile-first bar (the shared contract this issue enforces) A reader surface **passes** iff, at a viewport of `375 × 812`: 1. `document.documentElement.scrollWidth <= 375` — no horizontal overflow. 2. No content is clipped by `overflow: hidden` inside a cropped container that has no alternative way to see the clipped information. 3. Every interactive element (button, link, chip, icon-button) has computed min-height **≥ 44 px** AND min-width ≥ 44 px (WCAG 2.5.5). 4. Body text is at least 16 px (prevents iOS Safari auto-zoom when a nearby input is focused). 5. Primary actions (e.g. "Read", "Open", "Continue transcribing") are reachable without a second tap through a kebab menu. 6. Dark-mode renders without contrast regressions (axe-core passes AA in both modes — see #312 for the thumbnail precedent). ## In-scope routes and the known-bad spots ### `/` (Home) — reader parts only Reader-facing on Home: the `Resume Strip` ("Continue where you left off"), the `Comments & Activity` feed, and `What Needs Attention` counts. The transcriber queues (Mark Text · Transcribe Text · Ready to Read) are allowed to collapse behind a "Transcriber Tools" disclosure on `sm:` screens. Known bad at 375 px: Resume hero card is a two-column grid; the right column (metadata + progress ring + CTA) forces its own fixed width, pushing the left column (thumbnail + first-line preview) off-screen. ### `/documents` — list - Filter bar (search + Date sort + Filter + "+ New document") horizontally overflows. On narrow screens these stack: search on its own line, then sort + filter, and "+ New document" as a floating action or moved to the empty-state. - Document cards render at `max-w-none` — should be `w-full` inside a container that honours the viewport. - Group headers ("UNDATED", year) are fine; keep them. **Depends on #315/#316** (pagination) — do not re-regress: whatever this issue ships must keep the paginated-list-first contract. ### `/documents/[id]` — detail (read mode) - Header row with sender→receiver chips + `Details / Transcribe / Edit` actions overflows. Transcribe and Edit are authoring actions; on `sm:` they collapse behind a single kebab menu so only `Details` remains visible as a reader action. - PDF/image viewer — `object-contain` + `max-w-full` + respect safe area; zoom controls (already present) stay visible at the bottom. - Annotations overlay keeps working on mobile; "Hide annotations" link moves into the kebab. ### `/briefwechsel` — Letters - Entry page (typeahead) already fits at 375 px — minor CSS tune to centre vertically inside the viewport minus header. - Conversation detail — being iterated on in #224 / #306. Verify mobile while shipping those; this issue scopes the entry state only. ### `/persons` — list - Person cards (avatar + name + alias + doc count chip) clip at 375 px. Card padding shrinks (`p-4` → `p-3` on `sm:`), alias collapses to a second line instead of a third column. - Search bar is already full-width — good. Group letter headers stay. ### `/persons/[id]` — detail - Two-column layout collapses to one column on `sm:` with correspondents as a horizontally scrollable chip rail (opt-in horizontal scroll is fine; involuntary overflow is not). - Sent / Received list items wrap cleanly; chevron stays right-aligned. ## Out of scope (explicit) - Dark-mode bugs unrelated to mobile (file separately). - Admin master-detail pages — authoring surface. - Document edit form and save bar — #95 tracks its symptom. - PDF viewer pinch-zoom improvements — separate perf issue. - New mobile-only features (bottom-bar nav, PWA shell, offline) — separate follow-ups. ## Implementation plan ### 1. Tailwind breakpoint discipline - Convention: reader surfaces are **mobile-first** — no class without a breakpoint prefix assumes desktop. Desktop styles opt-in via `md:` / `lg:`. - Add a lint rule (or svelte-check) that flags `flex-row` / `grid-cols-{n≥2}` / `w-[≥500px]` without a breakpoint prefix on files under `routes/+page.svelte`, `routes/documents/*`, `routes/persons/*`, `routes/briefwechsel/*`. Warn-on-PR is enough. ### 2. Route-by-route fixes (one commit per route) Each commit touches listed files AND adds/updates a matching spec in `frontend/e2e/responsive/*.spec.ts`. - **Home** — `frontend/src/routes/+page.svelte`; split hero into `sm:grid-cols-1 md:grid-cols-[auto_1fr]`; thumbnail shrinks at `sm:`. - **Documents list** — `frontend/src/routes/documents/+page.svelte`; filter bar `flex-col sm:flex-row`. - **Document detail** — `frontend/src/routes/documents/[id]/+page.svelte`; action cluster collapses into a new `KebabMenu.svelte` at `sm:`. - **Briefwechsel entry** — `frontend/src/routes/briefwechsel/+page.svelte`; confirm with a snapshot. - **Persons list** — `frontend/src/routes/persons/+page.svelte` + `PersonCard.svelte` (verify name). - **Person detail** — `frontend/src/routes/persons/[id]/+page.svelte`; correspondents row becomes `overflow-x-auto snap-x snap-mandatory` on `sm:`. ### 3. Global layout - `frontend/src/routes/+layout.svelte` — verify `Menü öffnen` toggle carries `aria-expanded`; drawer closes on route change. - Header at `sm:` shows: logo + burger + upload icon; locale, dark-mode, notifications, and avatar move inside the drawer. ### 4. i18n No new user-visible strings expected. If any are added (e.g. a kebab "More actions" label), add keys to `frontend/messages/{de,en,es}.json`. ## Tests ### Frontend e2e (Playwright) New folder `frontend/e2e/responsive/` with one spec per route: - `home.responsive.spec.ts` - `documents-list.responsive.spec.ts` - `document-detail.responsive.spec.ts` - `briefwechsel-entry.responsive.spec.ts` - `persons-list.responsive.spec.ts` - `person-detail.responsive.spec.ts` Each spec: 1. `page.setViewportSize({ width: 375, height: 812 })`. 2. Navigate with a signed-in admin session (reuse existing fixture). 3. Assert `await page.evaluate(() => document.documentElement.scrollWidth) <= 375`. 4. For every `a, button` matched by `page.locator('a, button')`: assert `boundingBox().height >= 44` and `width >= 44` (with opt-out list for decorative icons). 5. Snapshot into `e2e/snapshots/responsive/` — fails if pixel delta > threshold. Re-enable #124 for this. Add a shared helper `frontend/e2e/responsive/mobile.helper.ts` exporting viewport setup + the three standard assertions. ### axe-core Extend existing axe sweeps to run each route at 375 px in both light and dark mode; fail on any new violations (baseline snapshot, then diff). ## Verification 1. `cd frontend && npm run test` — unit tests pass. 2. `cd frontend && npx playwright test e2e/responsive` — all six specs pass. 3. Manual smoke on a real phone (iPhone Safari, Android Chrome): sign in → home → open a document → persons → person detail → briefwechsel. No horizontal scroll, no action hidden off-screen, no tap target < 44 px. 4. Re-run the B2 evidence pass from 2026-04-24 — every `D*.png` shows no scrollbar. ## Acceptance criteria - [ ] All six reader routes pass the mobile-first bar at 375 × 812 - [ ] Six Playwright responsive specs exist and pass in CI - [ ] axe-core passes at 375 px in both light and dark mode on all six routes - [ ] Documents list stays paginated (no regression of #315) - [ ] Authoring surfaces unchanged — no responsive work leaks into them - [ ] Linter (or svelte-check rule) warns on `flex-row` / multi-column grid without breakpoint prefix inside reader routes - [ ] No new user-visible strings are untranslated in de/en/es ## Out-of-band dependencies - Re-enable #124 (Playwright visual regression — currently `descoped`). - Coordinate with #315/#316 — merge first; this issue must not regress pagination. ## Critical files ``` frontend/src/routes/+layout.svelte frontend/src/routes/+page.svelte frontend/src/routes/documents/+page.svelte frontend/src/routes/documents/[id]/+page.svelte frontend/src/routes/briefwechsel/+page.svelte frontend/src/routes/persons/+page.svelte frontend/src/routes/persons/[id]/+page.svelte frontend/src/lib/components/DocumentRow.svelte (verify name) frontend/src/lib/components/PersonCard.svelte (verify name) frontend/src/lib/components/dashboard/ResumeHero.svelte (verify name) frontend/src/lib/components/KebabMenu.svelte (new) frontend/e2e/responsive/*.spec.ts (new folder) frontend/e2e/responsive/mobile.helper.ts (new) ```
marcel added this to the Reader Experience v1 milestone 2026-04-24 13:22:03 +02:00
marcel added the P0-criticalfeatureui labels 2026-04-24 13:28:04 +02:00
Author
Owner

🏛️ Markus Keller — Application Architect

Observations

  • The issue correctly scopes the work to reader routes only and calls out the root cause (fixed desktop grids without breakpoint prefixes) — this is the right level of analysis.
  • The new KebabMenu.svelte component is a sound extraction: the DocumentTopBar.svelte already has a mobileMenuOpen state variable (line 65) with partial md:flex/md:block/md:hidden classes in place. The issue is formalising what half-exists already.
  • The proposed lint rule (flag flex-row/grid-cols-{n≥2} without breakpoint prefix in reader route files) is the right forcing function. However, as specified it will fire false positives on flex-row items that only appear inside md:flex wrappers — the rule needs to be scope-aware or it will be ignored from day one.
  • The issue depends on #315/#316 (pagination) merging first. This is architecturally sound: the responsive specs must assert against a paginated list, not an unguarded full-list endpoint that may no longer exist.
  • Six specs in frontend/e2e/responsive/ is the right organisational choice. The existing briefwechsel-rows.visual.spec.ts already establishes the pattern of gating visual snapshots on VISUAL=1; the new specs should follow the same convention.
  • Re-enabling #124 (visual regression) is listed as an out-of-band dependency. This is an architectural concern: if #124 is currently descoped (test.skip(!VISUAL, ...) is already in place in briefwechsel-rows.visual.spec.ts), then the same pattern already works — re-enabling #124 means adding snapshots and baseline images, not changing the test infrastructure.
  • The C4 frontend diagram and CLAUDE.md route table do not need updating (no new routes). The new KebabMenu.svelte component is not a new route — no doc update required per the architect documentation table.

Recommendations

  • Narrow the lint rule before it ships. The rule as written will false-positive on any utility-class composition. Implement it as a comment-pattern // @mobile-first-only at the top of each in-scope file, or as a Svelte plugin that only fires when the parent element has no breakpoint wrapper. A warn-on-PR eslint rule covering only routes/+page.svelte, routes/documents/*, routes/persons/*, routes/briefwechsel/* is sufficient — add it as its own atomic commit so it can be reverted without touching the layout fixes.
  • Make the KebabMenu boundary explicit. DocumentTopBar.svelte already manages mobileMenuOpen state. Extract the kebab menu dropdown (lines 247–270 of the current TopBar) into KebabMenu.svelte with a prop interface of items: { label: string; icon?: ...; action: () => void }[]. The TopBar becomes an orchestrator. This is one visual region → one component, which is the correct boundary.
  • Do not couple the responsive fix to the axe-core change. The issue bundles "fix overflow" + "add responsive specs" + "re-enable #124" + "axe at 375px in both modes". These are three separable concerns. Each deserves its own commit and should be independently revertable. Ship the layout fixes first so CI is green; add the axe extension to existing accessibility specs as a follow-on.
  • One commit per route is stated in the plan — hold to this. Bundling routes in one commit has been the source of hard-to-bisect regressions before.
## 🏛️ Markus Keller — Application Architect ### Observations - The issue correctly scopes the work to reader routes only and calls out the root cause (fixed desktop grids without breakpoint prefixes) — this is the right level of analysis. - The new `KebabMenu.svelte` component is a sound extraction: the `DocumentTopBar.svelte` already has a `mobileMenuOpen` state variable (line 65) with partial `md:flex`/`md:block`/`md:hidden` classes in place. The issue is formalising what half-exists already. - The proposed lint rule (flag `flex-row`/`grid-cols-{n≥2}` without breakpoint prefix in reader route files) is the right forcing function. However, as specified it will fire false positives on `flex-row` items that only appear inside `md:flex` wrappers — the rule needs to be scope-aware or it will be ignored from day one. - The issue depends on #315/#316 (pagination) merging first. This is architecturally sound: the responsive specs must assert against a paginated list, not an unguarded full-list endpoint that may no longer exist. - Six specs in `frontend/e2e/responsive/` is the right organisational choice. The existing `briefwechsel-rows.visual.spec.ts` already establishes the pattern of gating visual snapshots on `VISUAL=1`; the new specs should follow the same convention. - Re-enabling #124 (visual regression) is listed as an out-of-band dependency. This is an architectural concern: if #124 is currently descoped (`test.skip(!VISUAL, ...)` is already in place in `briefwechsel-rows.visual.spec.ts`), then the same pattern already works — re-enabling #124 means adding snapshots and baseline images, not changing the test infrastructure. - The C4 frontend diagram and `CLAUDE.md` route table do not need updating (no new routes). The new `KebabMenu.svelte` component is not a new route — no doc update required per the architect documentation table. ### Recommendations - **Narrow the lint rule before it ships.** The rule as written will false-positive on any utility-class composition. Implement it as a comment-pattern `// @mobile-first-only` at the top of each in-scope file, or as a Svelte plugin that only fires when the parent element has no breakpoint wrapper. A warn-on-PR eslint rule covering only `routes/+page.svelte`, `routes/documents/*`, `routes/persons/*`, `routes/briefwechsel/*` is sufficient — add it as its own atomic commit so it can be reverted without touching the layout fixes. - **Make the `KebabMenu` boundary explicit.** `DocumentTopBar.svelte` already manages `mobileMenuOpen` state. Extract the kebab menu dropdown (lines 247–270 of the current TopBar) into `KebabMenu.svelte` with a prop interface of `items: { label: string; icon?: ...; action: () => void }[]`. The TopBar becomes an orchestrator. This is one visual region → one component, which is the correct boundary. - **Do not couple the responsive fix to the axe-core change.** The issue bundles "fix overflow" + "add responsive specs" + "re-enable #124" + "axe at 375px in both modes". These are three separable concerns. Each deserves its own commit and should be independently revertable. Ship the layout fixes first so CI is green; add the axe extension to existing accessibility specs as a follow-on. - **One commit per route** is stated in the plan — hold to this. Bundling routes in one commit has been the source of hard-to-bisect regressions before.
Author
Owner

👨‍💻 Felix Brandt — Fullstack Developer

Observations

  • DashboardResumeStrip has a confirmed fixed-width offender. Line 48: class="relative h-[252px] w-[180px] flex-shrink-0 ..." — the thumbnail is w-[180px] flex-shrink-0 inside a flex gap-4 container. At 375 px viewport the remaining flex child (metadata + progress ring + CTA) gets pushed off screen. This is the exact symptom described in the issue. The fix: w-[120px] sm:w-[180px] on the thumbnail, or wrap the whole strip in a flex-col sm:flex-row so thumbnail and metadata stack on narrow screens.
  • Persons list grid is desktop-first. /persons/+page.svelte renders grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 — the sm:grid-cols-2 means at 375 px (which is below the sm breakpoint at 640 px) it collapses to single column. This one is already correct. However, the header row flex flex-wrap items-end justify-between with a w-56 search input inside will still overflow when the "New person" CTA is also present — flex-wrap helps but the right-side group has flex items-center gap-3 with no wrapping of its own.
  • DocumentTopBar already has a mobile menu (mobileMenuOpen). The KebabMenu.svelte referenced in the issue is mostly already there (lines 247–270 in DocumentTopBar). The issue is that the edit button still has hidden sm:inline on the label (line 235) and the Transcribe button uses md:flex (line 78). On exactly 375 px the Transcribe and Edit buttons are hidden and the three-dot mobile menu shows — this part is functional. The gap is that the mobile-menu dropdown has no aria-label and its trigger button has no aria-label either (line 253: onclick={() => (mobileMenuOpen = !mobileMenuOpen)} — check for missing accessible label).
  • SearchFilterBar row 1 is flex items-center gap-4. At 375 px this puts the search input, sort dropdown, filter toggle, and a "New document" link in one horizontal flex row. There is no flex-wrap or sm:flex-row variant. The flex children will overflow.
  • Person detail two-column layout uses lg:grid lg:grid-cols-[35%_65%]lg:grid means it is block (single column) below the lg breakpoint (1024 px). At 375 px this is already single-column. No fix needed for layout; the correspondents rail check is valid.
  • No e2e/responsive/ folder exists yet. The issue spec correctly identifies this as new. The existing briefwechsel-rows.visual.spec.ts and briefwechsel-a11y.spec.ts already demonstrate the page.setViewportSize pattern. The new mobile.helper.ts should export exactly three assertions: scrollWidth <= 375, boundingBox().height >= 44, boundingBox().width >= 44.

Recommendations

  • Fix DashboardResumeStrip first — it is the highest-visibility overflow and has a single-line root cause. Change w-[180px] flex-shrink-0 to w-[120px] flex-shrink-0 sm:w-[180px] and add flex-col sm:flex-row to the strip container. Update the spec DashboardResumeStrip.svelte.spec.ts to assert the strip does not overflow at 375 px.
  • Add flex-wrap to SearchFilterBar row 1 and stack: flex flex-wrap gap-3 with flex-col sm:flex-row so search fills the first line on narrow and the sort+filter controls wrap below. Move "New document" to float at the end of row 1 or use absolute-positioned FAB at sm:hidden.
  • Before adding KebabMenu.svelte as a new file, check whether extracting the existing dropdown from DocumentTopBar is sufficient. The menu logic is already implemented — the issue needs a clean boundary, not a rewrite.
  • Key all {#each} blocks in the new spec fixtures with (doc.id) — the spec's page.locator('a, button') loop must not rely on position.
  • Write the failing Playwright spec first (red) before touching any Svelte layout — per TDD discipline. The spec asserting scrollWidth <= 375 will be red immediately for every route listed in the issue, giving you a mechanical green gate.
  • The opt-out list for decorative icons in the 44×44 assertion is important — implement it as a data-touch-exempt attribute on known decorative SVGs so the assertion can skip them with page.locator('a, button').filter({ hasNot: page.locator('[data-touch-exempt]') }).
## 👨‍💻 Felix Brandt — Fullstack Developer ### Observations - **DashboardResumeStrip has a confirmed fixed-width offender.** Line 48: `class="relative h-[252px] w-[180px] flex-shrink-0 ..."` — the thumbnail is `w-[180px] flex-shrink-0` inside a `flex gap-4` container. At 375 px viewport the remaining flex child (metadata + progress ring + CTA) gets pushed off screen. This is the exact symptom described in the issue. The fix: `w-[120px] sm:w-[180px]` on the thumbnail, or wrap the whole strip in a `flex-col sm:flex-row` so thumbnail and metadata stack on narrow screens. - **Persons list grid is desktop-first.** `/persons/+page.svelte` renders `grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4` — the `sm:grid-cols-2` means at 375 px (which is below the `sm` breakpoint at 640 px) it collapses to single column. This one is already correct. However, the header row `flex flex-wrap items-end justify-between` with a `w-56` search input inside will still overflow when the "New person" CTA is also present — `flex-wrap` helps but the right-side group has `flex items-center gap-3` with no wrapping of its own. - **DocumentTopBar already has a mobile menu (`mobileMenuOpen`).** The `KebabMenu.svelte` referenced in the issue is mostly already there (lines 247–270 in DocumentTopBar). The issue is that the edit button still has `hidden sm:inline` on the label (line 235) and the Transcribe button uses `md:flex` (line 78). On exactly 375 px the Transcribe and Edit buttons are hidden and the three-dot mobile menu shows — this part is functional. The gap is that the mobile-menu dropdown has no `aria-label` and its trigger button has no `aria-label` either (line 253: `onclick={() => (mobileMenuOpen = !mobileMenuOpen)}` — check for missing accessible label). - **SearchFilterBar row 1 is `flex items-center gap-4`.** At 375 px this puts the search input, sort dropdown, filter toggle, and a "New document" link in one horizontal flex row. There is no `flex-wrap` or `sm:flex-row` variant. The flex children will overflow. - **Person detail two-column layout** uses `lg:grid lg:grid-cols-[35%_65%]` — `lg:grid` means it is `block` (single column) below the `lg` breakpoint (1024 px). At 375 px this is already single-column. No fix needed for layout; the correspondents rail check is valid. - **No `e2e/responsive/` folder exists yet.** The issue spec correctly identifies this as new. The existing `briefwechsel-rows.visual.spec.ts` and `briefwechsel-a11y.spec.ts` already demonstrate the `page.setViewportSize` pattern. The new `mobile.helper.ts` should export exactly three assertions: `scrollWidth <= 375`, `boundingBox().height >= 44`, `boundingBox().width >= 44`. ### Recommendations - **Fix DashboardResumeStrip first** — it is the highest-visibility overflow and has a single-line root cause. Change `w-[180px] flex-shrink-0` to `w-[120px] flex-shrink-0 sm:w-[180px]` and add `flex-col sm:flex-row` to the strip container. Update the spec `DashboardResumeStrip.svelte.spec.ts` to assert the strip does not overflow at 375 px. - **Add `flex-wrap` to SearchFilterBar row 1** and stack: `flex flex-wrap gap-3` with `flex-col sm:flex-row` so search fills the first line on narrow and the sort+filter controls wrap below. Move "New document" to float at the end of row 1 or use absolute-positioned FAB at `sm:hidden`. - **Before adding `KebabMenu.svelte` as a new file**, check whether extracting the existing dropdown from `DocumentTopBar` is sufficient. The menu logic is already implemented — the issue needs a clean boundary, not a rewrite. - **Key all `{#each}` blocks in the new spec fixtures** with `(doc.id)` — the spec's `page.locator('a, button')` loop must not rely on position. - **Write the failing Playwright spec first (red)** before touching any Svelte layout — per TDD discipline. The spec asserting `scrollWidth <= 375` will be red immediately for every route listed in the issue, giving you a mechanical green gate. - **The opt-out list for decorative icons** in the 44×44 assertion is important — implement it as a `data-touch-exempt` attribute on known decorative SVGs so the assertion can skip them with `page.locator('a, button').filter({ hasNot: page.locator('[data-touch-exempt]') })`.
Author
Owner

🚀 Tobias Wendt — DevOps & Platform Engineer

Observations

  • The issue creates a new frontend/e2e/responsive/ folder with six specs. The existing playwright.config.ts runs all tests under ./e2e with testDir: './e2e', so the new subfolder is picked up automatically — no CI config change needed.
  • The Playwright config runs workers: 1 (sequential) and fullyParallel: false because tests share auth state. Six additional responsive specs will run sequentially after the existing 35+ specs. The issue does not address CI time impact.
  • The existing briefwechsel-rows.visual.spec.ts gates snapshot tests behind VISUAL=1. The issue says "Re-enable #124" and "Snapshot into e2e/snapshots/responsive/ — fails if pixel delta > threshold." This implies the new responsive specs will always run snapshot assertions (not gated), which differs from the existing convention. That inconsistency needs resolving before the first CI run.
  • The playwright.config.ts has no mobile project — there is no devices['Pixel 5'] or equivalent. All tests run with devices['Desktop Chrome'] and setViewportSize is called per-test. This is fine but means Safari and Firefox at mobile widths are untested. The issue's acceptance criteria do include manual smoke on "iPhone Safari, Android Chrome" — this is appropriately scoped as manual given the single-worker CI constraint.
  • Docker Compose and infrastructure files are not touched by this issue. No new services, no new environment variables. DevOps surface is minimal.
  • The issue does not mention any CI step to install Playwright browsers. The existing CI presumably handles this — confirm npx playwright install chromium is in the workflow before adding specs that would fail on a fresh runner without the browser binary.

Recommendations

  • Gate snapshot assertions in the new responsive specs behind VISUAL=1 — match the convention in briefwechsel-rows.visual.spec.ts. This keeps CI green on the first run without requiring pre-captured baselines, and the --update-snapshots workflow is already documented in that file.
  • Add a CI time estimate to the issue or PR description. Six sequential Playwright specs at ~15s each is ~90 seconds added to the pipeline. At workers: 1 this is the true cost. Acceptable, but worth logging so the next person doesn't wonder why CI got slower.
  • Confirm npx playwright install --with-deps chromium is in the CI workflow before this PR merges. If the CI workflow already has it (implied by the 35+ existing E2E specs), no action needed — just verify.
  • Do not add a mobile Playwright project for this issue. The setViewportSize per-test approach is sufficient and avoids doubling the CI matrix. A proper mobile browser project (Safari via WebKit) belongs in a future dedicated accessibility milestone.
  • The e2e/responsive/ subfolder is self-contained. If the responsive specs become flaky after a UI change, they can be quarantined as a group by excluding the subfolder in CI via --ignore-snapshots or a test.skip flag — this isolation is a benefit of the new folder structure.
## 🚀 Tobias Wendt — DevOps & Platform Engineer ### Observations - The issue creates a new `frontend/e2e/responsive/` folder with six specs. The existing `playwright.config.ts` runs all tests under `./e2e` with `testDir: './e2e'`, so the new subfolder is picked up automatically — no CI config change needed. - The Playwright config runs `workers: 1` (sequential) and `fullyParallel: false` because tests share auth state. Six additional responsive specs will run sequentially after the existing 35+ specs. The issue does not address CI time impact. - The existing `briefwechsel-rows.visual.spec.ts` gates snapshot tests behind `VISUAL=1`. The issue says "Re-enable #124" and "Snapshot into `e2e/snapshots/responsive/` — fails if pixel delta > threshold." This implies the new responsive specs will always run snapshot assertions (not gated), which differs from the existing convention. That inconsistency needs resolving before the first CI run. - The `playwright.config.ts` has no `mobile` project — there is no `devices['Pixel 5']` or equivalent. All tests run with `devices['Desktop Chrome']` and `setViewportSize` is called per-test. This is fine but means Safari and Firefox at mobile widths are untested. The issue's acceptance criteria do include manual smoke on "iPhone Safari, Android Chrome" — this is appropriately scoped as manual given the single-worker CI constraint. - Docker Compose and infrastructure files are not touched by this issue. No new services, no new environment variables. DevOps surface is minimal. - The issue does not mention any CI step to install Playwright browsers. The existing CI presumably handles this — confirm `npx playwright install chromium` is in the workflow before adding specs that would fail on a fresh runner without the browser binary. ### Recommendations - **Gate snapshot assertions in the new responsive specs behind `VISUAL=1`** — match the convention in `briefwechsel-rows.visual.spec.ts`. This keeps CI green on the first run without requiring pre-captured baselines, and the `--update-snapshots` workflow is already documented in that file. - **Add a CI time estimate to the issue or PR description.** Six sequential Playwright specs at ~15s each is ~90 seconds added to the pipeline. At `workers: 1` this is the true cost. Acceptable, but worth logging so the next person doesn't wonder why CI got slower. - **Confirm `npx playwright install --with-deps chromium` is in the CI workflow** before this PR merges. If the CI workflow already has it (implied by the 35+ existing E2E specs), no action needed — just verify. - **Do not add a `mobile` Playwright project** for this issue. The `setViewportSize` per-test approach is sufficient and avoids doubling the CI matrix. A proper mobile browser project (Safari via WebKit) belongs in a future dedicated accessibility milestone. - **The `e2e/responsive/` subfolder is self-contained.** If the responsive specs become flaky after a UI change, they can be quarantined as a group by excluding the subfolder in CI via `--ignore-snapshots` or a `test.skip` flag — this isolation is a benefit of the new folder structure.
Author
Owner

📋 Elicit — Requirements Engineer

Observations

  • The issue is well-structured and unusually complete: it defines a measurable pass/fail bar (the five-point mobile-first contract), lists known-bad spots with symptoms, scopes non-goals explicitly, and provides acceptance criteria with checkboxes. This is well above average quality for a front-end layout issue.
  • The acceptance criteria are verifiablescrollWidth <= 375 is a machine-testable assertion, and the six Playwright specs map 1:1 to the six in-scope routes. No ambiguity on what "passes" means.
  • One requirement is potentially ambiguous: criterion 2 ("No content is clipped by overflow: hidden inside a cropped container that has no alternative way to see the clipped information") has no automated test defined for it. The spec only checks scrollWidth. A hidden overflow on an inner container (e.g. a truncated tag name with no tooltip) would pass the scrollWidth check but fail criterion 2. The issue does not explain how this is tested.
  • The "Primary actions reachable without a second tap" criterion (5) is defined in words but no corresponding test step is specified. The 44×44 test covers tap target size but not discoverability. A user-facing action hidden inside the kebab menu on mobile is technically reachable with two taps, which may or may not satisfy criterion 5 depending on interpretation.
  • Dependency ordering is specified (#315/#316 must merge first) — this is correctly called out. However, the issue does not specify what happens if #315 is delayed: does this issue block, or can it be implemented against the current unpaginated list with a note to verify after #315 merges?
  • The out-of-scope list is correct and complete. Authoring surfaces, PDF pinch-zoom, dark-mode-only bugs, and new mobile-only features are explicitly parked. This prevents scope creep.
  • i18n requirement is specified (add keys to de/en/es if new strings appear), but the issue doesn't identify which new string is most likely. The "More actions" kebab label is the only probable candidate — it should be pre-registered as a JTBD risk.

Recommendations

  • Add a test step for criterion 2 (clipped overflow). The simplest automated check: for each route, enumerate all elements with overflow: hidden and assert that the element's scrollWidth <= clientWidth (i.e. it is not actually clipping visible text that has no other access path). Add this as a fourth assertion in mobile.helper.ts alongside the three already specified.
  • Define "primary action reachable without a second tap" as a named element in each spec. For the document detail page, the primary action is the "Read" / "Open" (viewer) affordance; for the persons list it is the person card link itself. Name these concretely in the spec as page.getByRole('link', { name: /lesen|read/i }) and assert toBeVisible() at 375 px — not hidden inside a menu.
  • Clarify the #315 dependency. Add a sentence: "If #315 has not merged, implement against the current list and add a follow-up task to verify the responsive spec still passes after #315 merges." This prevents the issue from blocking indefinitely.
  • Pre-register the kebab "More actions" i18n key. Add more_actions to messages/{de,en,es}.json as part of the first commit touching DocumentTopBar. This avoids a last-minute i18n gap discovered in review.
  • The verification step 3 ("Manual smoke on a real phone") is the only non-automated acceptance criterion. Mark it explicitly as a manual gate in the PR checklist so it is not skipped under time pressure.
## 📋 Elicit — Requirements Engineer ### Observations - The issue is well-structured and unusually complete: it defines a measurable pass/fail bar (the five-point mobile-first contract), lists known-bad spots with symptoms, scopes non-goals explicitly, and provides acceptance criteria with checkboxes. This is well above average quality for a front-end layout issue. - **The acceptance criteria are verifiable** — `scrollWidth <= 375` is a machine-testable assertion, and the six Playwright specs map 1:1 to the six in-scope routes. No ambiguity on what "passes" means. - **One requirement is potentially ambiguous:** criterion 2 ("No content is clipped by `overflow: hidden` inside a cropped container that has no alternative way to see the clipped information") has no automated test defined for it. The spec only checks `scrollWidth`. A hidden overflow on an inner container (e.g. a truncated tag name with no tooltip) would pass the scrollWidth check but fail criterion 2. The issue does not explain how this is tested. - **The "Primary actions reachable without a second tap" criterion (5)** is defined in words but no corresponding test step is specified. The 44×44 test covers tap target size but not discoverability. A user-facing action hidden inside the kebab menu on mobile is technically reachable with two taps, which may or may not satisfy criterion 5 depending on interpretation. - **Dependency ordering is specified** (#315/#316 must merge first) — this is correctly called out. However, the issue does not specify what happens if #315 is delayed: does this issue block, or can it be implemented against the current unpaginated list with a note to verify after #315 merges? - **The out-of-scope list is correct and complete.** Authoring surfaces, PDF pinch-zoom, dark-mode-only bugs, and new mobile-only features are explicitly parked. This prevents scope creep. - **i18n requirement is specified** (add keys to de/en/es if new strings appear), but the issue doesn't identify which new string is most likely. The "More actions" kebab label is the only probable candidate — it should be pre-registered as a JTBD risk. ### Recommendations - **Add a test step for criterion 2** (clipped overflow). The simplest automated check: for each route, enumerate all elements with `overflow: hidden` and assert that the element's `scrollWidth <= clientWidth` (i.e. it is not actually clipping visible text that has no other access path). Add this as a fourth assertion in `mobile.helper.ts` alongside the three already specified. - **Define "primary action reachable without a second tap" as a named element in each spec.** For the document detail page, the primary action is the "Read" / "Open" (viewer) affordance; for the persons list it is the person card link itself. Name these concretely in the spec as `page.getByRole('link', { name: /lesen|read/i })` and assert `toBeVisible()` at 375 px — not hidden inside a menu. - **Clarify the #315 dependency.** Add a sentence: "If #315 has not merged, implement against the current list and add a follow-up task to verify the responsive spec still passes after #315 merges." This prevents the issue from blocking indefinitely. - **Pre-register the kebab "More actions" i18n key.** Add `more_actions` to `messages/{de,en,es}.json` as part of the first commit touching `DocumentTopBar`. This avoids a last-minute i18n gap discovered in review. - **The verification step 3** ("Manual smoke on a real phone") is the only non-automated acceptance criterion. Mark it explicitly as a manual gate in the PR checklist so it is not skipped under time pressure.
Author
Owner

🔒 Nora "NullX" Steiner — Security Engineer

Observations

  • This issue is a layout/responsive fix — the primary security surface is minimal. No new API endpoints, no new authentication flows, no new data access paths.
  • The new KebabMenu.svelte component collapses authoring actions (Transcribe, Edit) behind a dropdown on mobile. This has a security-adjacent implication: if the "Edit" action is hidden inside a kebab menu on mobile, but the underlying route (/documents/[id]/edit) is still reachable by direct URL navigation, the access control is backend-enforced (@RequirePermission(Permission.WRITE_ALL) on the edit endpoint) — the visual hiding is not a security boundary. This is correct architecture. Confirm the DocumentTopBar does not accidentally remove the permission check from the visible desktop button while adding the mobile menu.
  • The overflow-x-auto snap-x snap-mandatory chip rail proposed for /persons/[id] correspondents is a UI pattern with no security implications. The opt-in horizontal scroll is contained within a named element and does not expose data that wasn't already visible.
  • axe-core AA compliance is required by the acceptance criteria. This is relevant to security from a Leonie/accessibility angle, but also relevant here: focus trapping in the new kebab menu dropdown must be implemented correctly. A dropdown that does not trap focus allows keyboard users to interact with background content while the menu is visually in the foreground — not a traditional vulnerability, but a WCAG failure that also degrades the security of "did the user intentionally click this?" interactions.
  • The mobile nav drawer (AppNav.svelte) already implements aria-expanded={mobileNavOpen} and aria-controls="mobile-nav" correctly (lines 98–99). No regression risk there.
  • No new external URLs or fetch calls are introduced by this issue. The localStorage usage in briefwechsel/+page.svelte (for korrespondenz_recent_persons) is pre-existing and unaffected.

Recommendations

  • Verify focus trap in the new KebabMenu dropdown. The existing DocumentTopBar mobile menu (lines 267–270) closes on clickOutside but there is no Escape key handler and no focus trap. Add onkeydown handling for Escape to close the menu, and ensure focus returns to the trigger button on close. This is both a WCAG 2.1 criterion (4.1.2) and good UX for keyboard-first senior users.
  • Add aria-label to the three-dot mobile trigger button in DocumentTopBar (line 253: the button currently has no label — screen readers will announce "button"). Use the same m.more_actions() i18n key recommended by Elicit.
  • Do not use pointer-events: none as a security boundary. The issue does not propose this, but verify during implementation that any "authoring-only" button hidden on mobile is hidden via CSS (hidden md:flex) not pointer-events: none — the latter is invisible to assistive technology but does not actually prevent interaction via keyboard or accessibility APIs.
  • The linter rule (flagging flex-row/multi-column grid without breakpoint) is a development guardrail, not a security control. No security review needed for it.
## 🔒 Nora "NullX" Steiner — Security Engineer ### Observations - This issue is a layout/responsive fix — the primary security surface is minimal. No new API endpoints, no new authentication flows, no new data access paths. - **The new `KebabMenu.svelte` component** collapses authoring actions (Transcribe, Edit) behind a dropdown on mobile. This has a security-adjacent implication: if the "Edit" action is hidden inside a kebab menu on mobile, but the underlying route (`/documents/[id]/edit`) is still reachable by direct URL navigation, the access control is backend-enforced (`@RequirePermission(Permission.WRITE_ALL)` on the edit endpoint) — the visual hiding is not a security boundary. This is correct architecture. Confirm the `DocumentTopBar` does not accidentally remove the permission check from the visible desktop button while adding the mobile menu. - **The `overflow-x-auto snap-x snap-mandatory` chip rail** proposed for `/persons/[id]` correspondents is a UI pattern with no security implications. The opt-in horizontal scroll is contained within a named element and does not expose data that wasn't already visible. - **axe-core AA compliance** is required by the acceptance criteria. This is relevant to security from a Leonie/accessibility angle, but also relevant here: focus trapping in the new kebab menu dropdown must be implemented correctly. A dropdown that does not trap focus allows keyboard users to interact with background content while the menu is visually in the foreground — not a traditional vulnerability, but a WCAG failure that also degrades the security of "did the user intentionally click this?" interactions. - **The mobile nav drawer** (`AppNav.svelte`) already implements `aria-expanded={mobileNavOpen}` and `aria-controls="mobile-nav"` correctly (lines 98–99). No regression risk there. - **No new external URLs or fetch calls** are introduced by this issue. The `localStorage` usage in `briefwechsel/+page.svelte` (for `korrespondenz_recent_persons`) is pre-existing and unaffected. ### Recommendations - **Verify focus trap in the new KebabMenu dropdown.** The existing `DocumentTopBar` mobile menu (lines 267–270) closes on `clickOutside` but there is no `Escape` key handler and no focus trap. Add `onkeydown` handling for `Escape` to close the menu, and ensure focus returns to the trigger button on close. This is both a WCAG 2.1 criterion (4.1.2) and good UX for keyboard-first senior users. - **Add `aria-label` to the three-dot mobile trigger button** in `DocumentTopBar` (line 253: the button currently has no label — screen readers will announce "button"). Use the same `m.more_actions()` i18n key recommended by Elicit. - **Do not use `pointer-events: none` as a security boundary.** The issue does not propose this, but verify during implementation that any "authoring-only" button hidden on mobile is hidden via CSS (`hidden md:flex`) not `pointer-events: none` — the latter is invisible to assistive technology but does not actually prevent interaction via keyboard or accessibility APIs. - **The linter rule** (flagging `flex-row`/multi-column grid without breakpoint) is a development guardrail, not a security control. No security review needed for it.
Author
Owner

🧪 Sara Holt — QA Engineer

Observations

  • The test strategy is well-designed. Six specs, one per route, each with the same three assertions (scrollWidth <= 375, height >= 44, width >= 44) extracted into a shared mobile.helper.ts. This is the correct approach — shared helper, route-specific specs, not one mega-spec.
  • The existing briefwechsel-a11y.spec.ts already runs at { name: 'mobile', width: 375, height: 812 } (line 15) and the briefwechsel-rows.visual.spec.ts does the same. The new folder reuses this pattern, which is good for consistency. The issue should call out that these two specs are not being moved — they stay in place.
  • The 44×44 assertion across all a, button elements will be flaky without a well-defined opt-out list. The issue mentions "with opt-out list for decorative icons" but does not define what that list is or how it is maintained. In the existing codebase there are several icon-only buttons and SVG elements (e.g. the three-dot trigger in DocumentTopBar, the upload icon in the header) that may not meet 44×44 until the fix is applied. Without the opt-out list defined upfront, the spec will fail on elements not targeted by this issue, creating noise.
  • No unit test coverage is mentioned for the responsive helper itself. mobile.helper.ts will contain three assertion functions. These are plain TypeScript functions — they should have at least a minimal test that they throw when scrollWidth > 375.
  • The axe-core extension (run at 375 px in both light and dark mode) is specified but no spec file or test name is given. Is this added to the existing accessibility.spec.ts, or to each new responsive spec, or to the existing briefwechsel-a11y.spec.ts? The lack of specificity means this is likely to be deferred or done inconsistently.
  • The Playwright config has no mobile project — all tests run on Desktop Chrome with manual setViewportSize. This means the 375 px tests do not simulate actual mobile browser rendering (no touch events, no device pixel ratio). For this issue's scope (overflow detection, tap target size) this is acceptable. Flag it as a known gap in the PR description.
  • CI time impact: 6 specs × ~15s each = ~90s added sequentially. With retries: 2 in CI, worst case is 6 × 45s = 270s. This is acceptable given the existing suite size.

Recommendations

  • Define the opt-out list as a concrete data- attribute before writing any spec. Recommend data-touch-exempt on known decorative/small elements. The helper assertion becomes: page.locator('a:visible, button:visible').filter({ hasNot: page.locator('[data-touch-exempt]') }). Document in the PR which elements are marked exempt and why.
  • Specify the axe-core extension concretely: add checkA11y calls at 375 px to the existing accessibility.spec.ts for the six in-scope routes, in both light and dark mode. Do not create a seventh spec file for axe — fold it into the existing accessibility spec to keep the test surface coherent.
  • Write mobile.helper.ts first, test it, then use it. The helper is the keystone of the whole test strategy. If assertNoHorizontalOverflow has a bug (e.g. it checks scrollWidth before the page finishes layout), every spec silently passes. Give it a unit test in Vitest that confirms it throws on a synthetic overflow fixture.
  • Add test.describe.configure({ mode: 'serial' }) to each responsive spec — since the specs share an auth session and run at an unusual viewport, serial execution within each file prevents viewport state from leaking between tests.
  • The acceptance criterion "Six Playwright responsive specs exist and pass in CI" is verifiable. Add it to the PR description as a CI gate, not just a checklist item.
## 🧪 Sara Holt — QA Engineer ### Observations - **The test strategy is well-designed.** Six specs, one per route, each with the same three assertions (`scrollWidth <= 375`, `height >= 44`, `width >= 44`) extracted into a shared `mobile.helper.ts`. This is the correct approach — shared helper, route-specific specs, not one mega-spec. - **The existing `briefwechsel-a11y.spec.ts`** already runs at `{ name: 'mobile', width: 375, height: 812 }` (line 15) and the `briefwechsel-rows.visual.spec.ts` does the same. The new folder reuses this pattern, which is good for consistency. The issue should call out that these two specs are not being moved — they stay in place. - **The 44×44 assertion across all `a, button` elements will be flaky without a well-defined opt-out list.** The issue mentions "with opt-out list for decorative icons" but does not define what that list is or how it is maintained. In the existing codebase there are several icon-only buttons and SVG elements (e.g. the three-dot trigger in `DocumentTopBar`, the upload icon in the header) that may not meet 44×44 until the fix is applied. Without the opt-out list defined upfront, the spec will fail on elements not targeted by this issue, creating noise. - **No unit test coverage is mentioned for the responsive helper itself.** `mobile.helper.ts` will contain three assertion functions. These are plain TypeScript functions — they should have at least a minimal test that they throw when `scrollWidth > 375`. - **The axe-core extension** (run at 375 px in both light and dark mode) is specified but no spec file or test name is given. Is this added to the existing `accessibility.spec.ts`, or to each new responsive spec, or to the existing `briefwechsel-a11y.spec.ts`? The lack of specificity means this is likely to be deferred or done inconsistently. - **The Playwright config has no `mobile` project** — all tests run on Desktop Chrome with manual `setViewportSize`. This means the 375 px tests do not simulate actual mobile browser rendering (no touch events, no device pixel ratio). For this issue's scope (overflow detection, tap target size) this is acceptable. Flag it as a known gap in the PR description. - **CI time impact**: 6 specs × ~15s each = ~90s added sequentially. With `retries: 2` in CI, worst case is 6 × 45s = 270s. This is acceptable given the existing suite size. ### Recommendations - **Define the opt-out list as a concrete `data-` attribute before writing any spec.** Recommend `data-touch-exempt` on known decorative/small elements. The helper assertion becomes: `page.locator('a:visible, button:visible').filter({ hasNot: page.locator('[data-touch-exempt]') })`. Document in the PR which elements are marked exempt and why. - **Specify the axe-core extension concretely:** add `checkA11y` calls at 375 px to the existing `accessibility.spec.ts` for the six in-scope routes, in both light and dark mode. Do not create a seventh spec file for axe — fold it into the existing accessibility spec to keep the test surface coherent. - **Write `mobile.helper.ts` first, test it, then use it.** The helper is the keystone of the whole test strategy. If `assertNoHorizontalOverflow` has a bug (e.g. it checks `scrollWidth` before the page finishes layout), every spec silently passes. Give it a unit test in Vitest that confirms it throws on a synthetic overflow fixture. - **Add `test.describe.configure({ mode: 'serial' })` to each responsive spec** — since the specs share an auth session and run at an unusual viewport, serial execution within each file prevents viewport state from leaking between tests. - **The acceptance criterion "Six Playwright responsive specs exist and pass in CI"** is verifiable. Add it to the PR description as a CI gate, not just a checklist item.
Author
Owner

🎨 Leonie Voss — UX Designer & Accessibility Strategist

Observations

  • The mobile-first bar is correctly defined. 375×812, scrollWidth <= 375, 44×44 tap targets, 16px minimum body text, primary action one tap away — this is WCAG 2.5.5 compliant and appropriate for the dual audience (60+ seniors on phones).
  • DashboardResumeStrip's w-[180px] flex-shrink-0 thumbnail creates the exact layout failure described. At 375 px, the 180 px thumbnail plus gap plus metadata column exceeds the viewport. The fix must respect the thumbnail's aspect ratio (the image is object-cover object-top) — shrinking the width to w-[100px] sm:w-[180px] will also change the height proportionally given the fixed h-[252px]. Use a ratio-preserving container: aspect-[180/252] or simply h-auto on the image with max-h-[252px].
  • The DashboardResumeStrip empty state (rounded-sm border border-line bg-surface p-8 text-center) is already mobile-safe — it is a single-column centered block with no fixed widths.
  • SearchFilterBar's row 1 (flex items-center gap-4) with search + sort + filter + "New document" horizontally overflows. The issue's proposed fix (stack: search on its own line, sort + filter, "+ New document" floating or moved) is correct UX. The senior audience benefits from the search input being full-width and clearly labeled — ensure the aria-label on the search input (m.docs_search_placeholder()) matches the visible placeholder text.
  • The kebab menu approach for document detail is sound. The issue correctly identifies that "Transcribe" and "Edit" are authoring actions that belong behind a kebab on sm:, and "Details" (viewer action) stays visible. This matches the dual-audience split: readers see "Details"; transcribers see the full bar on their larger screens. Make sure the kebab trigger button has aria-label={m.more_actions()} and aria-haspopup="menu" to meet WCAG 4.1.2.
  • The correspondents chip rail (overflow-x-auto snap-x snap-mandatory) on Person detail is the correct pattern for opt-in horizontal scroll. This is not involuntary overflow — the user consciously scrolls. Apply scroll-snap-type: x mandatory with snap-start on each chip so the scroll feels intentional, not accidental.
  • Font sizes: The issue requires 16px minimum body text. The existing font-size: text-sm (14px) appears on person card secondary text and document metadata lines. These must be checked. Tailwind's text-sm is 0.875rem = 14px — below the 16px iOS Safari auto-zoom threshold. Any <input> adjacent to text-sm labels will trigger iOS auto-zoom on focus. Fix: raise all input labels to text-base (16px) or set the input font-size to 16px explicitly.
  • Dark mode axe sweep: The existing header.spec.ts tests dark mode for the header. The issue requires extending this to all six routes at 375 px. The senior audience (who may use high-contrast or dark mode for readability) depends on this passing.

Recommendations

  • Use aspect-[9/13] on the resume thumbnail container instead of fixed h-[252px] w-[180px]. This gives w-full sm:w-[180px] a proportional height on mobile without needing both dimensions to be fixed. The max-w-[180px] prevents it from growing too large on desktop.
  • Set font-size: 16px on all <input> elements in reader routes via a global CSS rule in layout.css. This prevents iOS Safari auto-zoom universally — aligns with criterion 4 of the mobile-first bar without per-input changes. Use: input, select, textarea { font-size: max(16px, 1rem); }.
  • The kebab trigger must be 44×44px — the existing three-dot button in DocumentTopBar is styled as relative md:hidden with no explicit min-height. Add min-h-[44px] min-w-[44px] to the trigger button.
  • Verify brand-mint is never used as body text color on white in the affected routes. The persona file documents that brand-mint on white = ~2.8:1 which fails AA for normal text. A quick grep for text-accent in the six affected route files will surface any violations before they reach the PR.
  • The floating action button ("+ New document") pattern mentioned for narrow screens must use the brand-navy primary button style (bg-primary text-primary-fg) to maintain brand consistency — do not use a generic FAB pattern that diverges from the existing design system.

Open Decisions

  • Resume thumbnail shrink strategy: The issue says "thumbnail shrinks at sm:" but does not specify whether the two-column strip becomes single-column (thumbnail above, metadata below) or stays side-by-side with a smaller thumbnail. Side-by-side with a smaller thumbnail preserves the visual context of the document preview; single-column gives more metadata width. For a 375 px screen with a 60+ year-old user, side-by-side with a smaller thumbnail (~100px) is the better choice — the thumbnail gives orientation cues. But this is a product judgment about information hierarchy on the home screen.
## 🎨 Leonie Voss — UX Designer & Accessibility Strategist ### Observations - **The mobile-first bar is correctly defined.** 375×812, `scrollWidth <= 375`, 44×44 tap targets, 16px minimum body text, primary action one tap away — this is WCAG 2.5.5 compliant and appropriate for the dual audience (60+ seniors on phones). - **DashboardResumeStrip's `w-[180px] flex-shrink-0` thumbnail** creates the exact layout failure described. At 375 px, the 180 px thumbnail plus gap plus metadata column exceeds the viewport. The fix must respect the thumbnail's aspect ratio (the image is `object-cover object-top`) — shrinking the width to `w-[100px] sm:w-[180px]` will also change the height proportionally given the fixed `h-[252px]`. Use a ratio-preserving container: `aspect-[180/252]` or simply `h-auto` on the image with `max-h-[252px]`. - **The `DashboardResumeStrip` empty state** (`rounded-sm border border-line bg-surface p-8 text-center`) is already mobile-safe — it is a single-column centered block with no fixed widths. - **SearchFilterBar's row 1** (`flex items-center gap-4`) with search + sort + filter + "New document" horizontally overflows. The issue's proposed fix (stack: search on its own line, sort + filter, "+ New document" floating or moved) is correct UX. The senior audience benefits from the search input being full-width and clearly labeled — ensure the `aria-label` on the search input (`m.docs_search_placeholder()`) matches the visible placeholder text. - **The kebab menu approach for document detail** is sound. The issue correctly identifies that "Transcribe" and "Edit" are authoring actions that belong behind a kebab on `sm:`, and "Details" (viewer action) stays visible. This matches the dual-audience split: readers see "Details"; transcribers see the full bar on their larger screens. Make sure the kebab trigger button has `aria-label={m.more_actions()}` and `aria-haspopup="menu"` to meet WCAG 4.1.2. - **The correspondents chip rail** (`overflow-x-auto snap-x snap-mandatory`) on Person detail is the correct pattern for opt-in horizontal scroll. This is not involuntary overflow — the user consciously scrolls. Apply `scroll-snap-type: x mandatory` with `snap-start` on each chip so the scroll feels intentional, not accidental. - **Font sizes**: The issue requires 16px minimum body text. The existing `font-size: text-sm` (14px) appears on person card secondary text and document metadata lines. These must be checked. Tailwind's `text-sm` is 0.875rem = 14px — below the 16px iOS Safari auto-zoom threshold. Any `<input>` adjacent to `text-sm` labels will trigger iOS auto-zoom on focus. Fix: raise all input labels to `text-base` (16px) or set the input font-size to 16px explicitly. - **Dark mode axe sweep**: The existing `header.spec.ts` tests dark mode for the header. The issue requires extending this to all six routes at 375 px. The senior audience (who may use high-contrast or dark mode for readability) depends on this passing. ### Recommendations - **Use `aspect-[9/13]` on the resume thumbnail container** instead of fixed `h-[252px] w-[180px]`. This gives `w-full sm:w-[180px]` a proportional height on mobile without needing both dimensions to be fixed. The `max-w-[180px]` prevents it from growing too large on desktop. - **Set `font-size: 16px` on all `<input>` elements** in reader routes via a global CSS rule in `layout.css`. This prevents iOS Safari auto-zoom universally — aligns with criterion 4 of the mobile-first bar without per-input changes. Use: `input, select, textarea { font-size: max(16px, 1rem); }`. - **The kebab trigger must be 44×44px** — the existing three-dot button in `DocumentTopBar` is styled as `relative md:hidden` with no explicit min-height. Add `min-h-[44px] min-w-[44px]` to the trigger button. - **Verify brand-mint is never used as body text color on white** in the affected routes. The persona file documents that `brand-mint on white = ~2.8:1` which fails AA for normal text. A quick grep for `text-accent` in the six affected route files will surface any violations before they reach the PR. - **The floating action button ("+ New document") pattern** mentioned for narrow screens must use the brand-navy primary button style (`bg-primary text-primary-fg`) to maintain brand consistency — do not use a generic FAB pattern that diverges from the existing design system. ### Open Decisions - **Resume thumbnail shrink strategy**: The issue says "thumbnail shrinks at `sm:`" but does not specify whether the two-column strip becomes single-column (thumbnail above, metadata below) or stays side-by-side with a smaller thumbnail. Side-by-side with a smaller thumbnail preserves the visual context of the document preview; single-column gives more metadata width. For a 375 px screen with a 60+ year-old user, side-by-side with a smaller thumbnail (~100px) is the better choice — the thumbnail gives orientation cues. But this is a product judgment about information hierarchy on the home screen.
Author
Owner

Decision Queue

One genuine tradeoff was raised that needs a product call before implementation starts.


D1 — Resume strip layout at 375 px: side-by-side (smaller thumbnail) vs. single-column stack

Raised by: Leonie Voss

Context: The issue says "thumbnail shrinks at sm:" but does not specify the exact layout change. Two approaches are viable:

Option A — Side-by-side, thumbnail shrinks to ~100 px

  • Strip stays flex-row but thumbnail changes from w-[180px] to w-[100px] sm:w-[180px]
  • Metadata column gets the remaining ~255 px (375 − 100 − gap)
  • Thumbnail gives orientation cue — the reader immediately sees which document they are resuming
  • Downside: 100 px thumbnail may be too small to read Kurrent script or distinguish letter layout

Option B — Single-column stack (thumbnail above, metadata below)

  • Strip becomes flex-col sm:flex-row
  • Full 375 px width for thumbnail on mobile (better legibility of the document image)
  • Metadata (title, date, progress ring, CTA) renders below
  • Downside: strip is taller; on a 375×812 viewport it may push below the fold

Recommendation from Leonie: Option A (side-by-side, ~100 px thumbnail) — the orientation cue matters more than image fidelity at this stage; the reader is not reading the document in the strip, they are recognising it.

Who decides: Marcel (product owner) — this is an information hierarchy judgment, not a technical constraint.

## Decision Queue One genuine tradeoff was raised that needs a product call before implementation starts. --- ### D1 — Resume strip layout at 375 px: side-by-side (smaller thumbnail) vs. single-column stack **Raised by:** Leonie Voss **Context:** The issue says "thumbnail shrinks at `sm:`" but does not specify the exact layout change. Two approaches are viable: **Option A — Side-by-side, thumbnail shrinks to ~100 px** - Strip stays `flex-row` but thumbnail changes from `w-[180px]` to `w-[100px] sm:w-[180px]` - Metadata column gets the remaining ~255 px (375 − 100 − gap) - Thumbnail gives orientation cue — the reader immediately sees which document they are resuming - Downside: 100 px thumbnail may be too small to read Kurrent script or distinguish letter layout **Option B — Single-column stack (thumbnail above, metadata below)** - Strip becomes `flex-col sm:flex-row` - Full 375 px width for thumbnail on mobile (better legibility of the document image) - Metadata (title, date, progress ring, CTA) renders below - Downside: strip is taller; on a 375×812 viewport it may push below the fold **Recommendation from Leonie:** Option A (side-by-side, ~100 px thumbnail) — the orientation cue matters more than image fidelity at this stage; the reader is not reading the document in the strip, they are recognising it. **Who decides:** Marcel (product owner) — this is an information hierarchy judgment, not a technical constraint.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marcel/familienarchiv#318