feat(ui): responsive DocumentTopBar — TopBar B1 spec implementation #161

Closed
opened 2026-03-30 10:37:49 +02:00 by marcel · 7 comments
Owner

Context

The document detail page (/documents/[id]) has a DocumentTopBar component (src/lib/components/DocumentTopBar.svelte) that currently renders a single flat layout at all screen sizes. This issue implements the TopBar B1 Responsive design spec (docs/specs/document-topbar-b1-responsive.html), which defines a mobile-first, five-breakpoint layout with full dark mode support, person-chip overflow, and an annotate-mode state.

Open the spec file in a browser to see all visual references referenced in this issue.


What currently exists

DocumentTopBar.svelte already:

  • Has a back link, title <h1>, a compact compactMeta string (date + sender/receiver as plain text), annotate toggle, edit link, and download link
  • Uses CSS custom properties (bg-surface, border-line, text-ink, etc.) from the project's theme
  • Receives props: doc, canWrite, canAnnotate, fileUrl, annotateMode (bindable)

What is missing: responsive breakpoint logic, chip-based person display, receiver overflow pill, dark mode token variants, the annotate hint strip, and the viewport-specific show/hide rules described below.


Design tokens

All colours are already present in the project theme. Map spec names to Tailwind classes as follows:

Spec token Light value Dark value Tailwind class
bg-surface #FFFFFF #0F1923 bg-surface (already var(--color-surface))
border-line #E4E2D8 #1E2D3D border-line
bg-primary / accent bar #012851 #A1DCD8 border-primary / bg-primary
text-primary-fg #A1DCD8 #012851 text-primary-fg
text-ink #1A1A1A #EAE8E2 text-ink
text-ink-3 (meta) #AAAAAA #3E5065 text-ink-2 (closest existing)
bg-muted (chip bg) #F0EFE9 #0A1218 bg-muted or bg-canvas
chip border #DDD9D0 #1E2D3D border-line

Accent bar: border-l-[3px] border-primary — always present, all breakpoints, both themes.


Responsive behaviour rules

Implement these as Tailwind responsive classes. The topbar height and element visibility change at four breakpoints:

Element < 375px (XS) 375–767px (mobile) 768–1023px (tablet) ≥ 1024px (laptop/desktop)
Topbar height h-11 (44 px) h-[50px] h-[52px] h-[52px]
Back button shape Square 24×24, rounded Circle 26×26, rounded-full Circle 28×28 Circle 28×28
Title font size text-[10px] text-[10.5px] text-[11.5px] text-[12px] (text-[12.5px] at 1440)
Person chip row Hidden — show plain-text meta line instead Shown — abbreviated names (K. Raddatz) Shown — full names Shown — full names + location
Date format dd.mm.yyyy inline in plain-text meta dd.mm.yyyy dd.mm.yyyy Full: 24. Dezember 1943 (use month: 'long')
Location in meta Hidden Hidden Shown if doc.location present Shown
Status chip Hidden Hidden Shown inline next to title Shown
Annotate button Hidden Hidden Shown with text label Shown with text label
Edit button label "Edit" "Edit" "Bearbeiten" "Bearbeiten"
Download button Hidden Hidden Shown (icon only) Shown (icon only)
Divider between actions Hidden Hidden Shown Shown
Hint strip (annotate active) Hidden Hidden 18px strip below main row 18px strip

Person chips

Replace the current plain-text compactMeta receiver string with a chip row at ≥ 375px.

Chip structure (one chip)

<div class="inline-flex items-center gap-0.5 rounded-full border border-line bg-muted px-1.5 py-0.5 whitespace-nowrap shrink-0">
  <!-- Avatar: initials, coloured bg -->
  <div class="flex h-[14px] w-[14px] shrink-0 items-center justify-center rounded-full bg-primary text-[5px] font-bold text-primary-fg">
    KR
  </div>
  <!-- Name -->
  <span class="text-[6.5px] font-semibold text-ink">Karl Raddatz</span>
</div>
  • Avatar background cycles through a small set of colours keyed by person ID (use a deterministic hash). Suggested palette: #012851 (navy), #5A3080 (purple), #007596 (teal), #2A6040 (moss), #803020 (rust).
  • At 375–767px: use abbreviated name — first initial + last name (K. Raddatz). At ≥ 768px: full name.

Arrow between sender and receiver

<span class="text-ink-2 shrink-0 text-[9px]" aria-hidden="true"></span>

Omit the arrow when there is no sender (direction unknown — e.g. photos, certificates).

Overflow rule — always show sender + 1st receiver, collapse the rest

receivers.length === 0  →  sender chip only, no arrow
receivers.length === 1  →  sender → receiver (no overflow)
receivers.length === 2  →  show both at ≥ 768px; collapse to sender → 1st + "+1" at < 768px
receivers.length ≥ 3   →  sender → 1st receiver → "+N weitere" (≥ 768px) / "+N" (375–767px)

Overflow pill:

<button
  class="inline-flex items-center rounded-full border border-line bg-muted px-1.5 py-0.5 text-[6.5px] font-bold text-ink-2 whitespace-nowrap shrink-0"
  onclick={() => overflowOpen = !overflowOpen}
  aria-haspopup="true"
  aria-expanded={overflowOpen}
>
  +{extraCount} {viewport >= 768 ? 'weitere' : ''}
</button>

At < 375px (XS), chips are hidden entirely. Overflow is shown as plain text appended to the meta line: "K.Raddatz → E.Raddatz +4 · 24.12.1943".


Overflow tooltip

When the overflow pill is clicked/tapped (at ≥ 768px), show a floating panel:

  • Positioned absolute top-full left-0 mt-1 relative to the pill
  • Background: bg-surface, border border-line, rounded-md, shadow-lg, p-3, min-w-[160px], z-50
  • Header: "Weitere Empfänger" in text-[7px] font-bold uppercase tracking-wide text-ink-2, with a bottom border
  • Each person: avatar (same colour logic as chips, 16×16) + full name in text-[9.5px] font-medium text-ink
  • Each name is a link to /persons/{id}
  • Dismiss on: click outside, press Escape, click same pill again
  • Dark mode: panel uses bg-surface dark token automatically

On mobile (< 768px) the overflow pill shows "+N" only (no tooltip) — tapping it does nothing (or could navigate to the edit page to see all receivers, but that's optional scope).

Active state of the pill (tooltip open or annotate mode active):

bg-primary text-primary-fg border-primary

Annotate mode

When annotateMode === true (passed as bindable prop):

  1. Edit button and Download button are hidden
  2. Annotate ghost button becomes active — filled navy bg (bg-primary text-primary-fg), label changes to "Beenden"
  3. Hint strip appears directly below the main topbar row (only at ≥ 768px):
<div class="flex h-[18px] items-center gap-2 border-t border-dashed border-primary/20 bg-primary/5 px-3.5 dark:border-primary/10 dark:bg-primary/4">
  <span class="text-[5.5px] font-bold uppercase tracking-wide text-primary">Annotierungsmodus aktiv</span>
  <span class="text-[5.5px] text-ink-2">Klicken Sie auf eine Textstelle im Dokument, um eine Anmerkung hinzuzufügen.</span>
</div>

Dark mode hint: text-primary-fg for the label, same muted bg.


XS (< 375px) plain-text meta line

At the smallest viewport, chips are not rendered. Instead, keep the existing compactMeta string but adjust its format:

K.Raddatz → E.Raddatz +4 · 24.12.1943
  • Sender: first initial + dot + last name (K.Raddatz)
  • Receivers: same abbreviated format; if overflow, append +N after the first receiver name
  • Date: dd.mm.yyyy (numeric, no spaces)

Status chip (≥ 768px)

The doc object exposes a status field of type DocumentStatus (PLACEHOLDER | UPLOADED | TRANSCRIBED | REVIEWED | ARCHIVED). Show a small status badge inline with the title:

<span class="inline-flex items-center gap-1 rounded-sm border px-1.5 py-0.5 text-[5.5px] font-bold uppercase tracking-wide
  {statusClass(doc.status)}">
  <span class="h-1 w-1 rounded-full {statusDotClass(doc.status)}"></span>
  {statusLabel(doc.status)}
</span>

Use green (bg-emerald-100 border-emerald-300 text-emerald-800) for UPLOADED/ARCHIVED, a neutral muted style for the rest. Dark mode: use dark: variants with 7–10% opacity backgrounds.


Accessibility requirements

  • The topbar must be wrapped in <header role="banner"> or already be inside <header> — check the parent layout.
  • Back link: aria-label="Zurück zur Dokumentenliste" (always, because the icon is the only visible element on XS).
  • Overflow pill: aria-haspopup="listbox", aria-expanded, aria-label="Alle Empfänger anzeigen".
  • Tooltip list items: role="listbox" on the panel, role="option" on each person row.
  • Annotate button: aria-pressed={annotateMode}.
  • Focus trap is not needed for the tooltip (it is not a modal), but Escape should dismiss it and return focus to the pill.
  • All interactive elements must have a visible focus ring — never outline: none without a replacement.

Out of scope for this issue

  • Actual annotation functionality (that is handled by AnnotationSidePanel)
  • Changes to AnnotationSidePanel or DocumentBottomPanel
  • The location field on Document — if it does not yet exist on the backend DTO, skip the location display for now and leave a // TODO: show location when available comment

Acceptance criteria

  • Topbar renders correctly at 320px, 375px, 768px, 1024px, and 1440px — matches spec screenshots
  • Light and dark themes both match the token table above — no hardcoded hex values in the component
  • 0-receiver, 1-receiver, 2-receiver, 3-receiver, and 5-receiver cases all render correctly
  • Overflow tooltip opens/closes on click, keyboard (Enter/Space on pill, Escape to close), and click-outside
  • Annotate mode: Edit + Download hidden, hint strip visible, button label "Beenden" — at ≥ 768px only
  • Status chip visible at ≥ 768px, hidden below
  • Annotate button hidden below 768px
  • All touch targets ≥ 44×44px on mobile (back button, edit, annotate pill)
  • aria-pressed, aria-haspopup, aria-expanded, aria-label on all interactive elements
  • svelte-check passes with no new type errors
  • Visual proof: run /proofshot against the document detail page at all five viewport widths in both themes

Reference

Full annotated spec: docs/specs/document-topbar-b1-responsive.html — open in a browser.
Existing component: src/lib/components/DocumentTopBar.svelte
Consuming page: src/routes/documents/[id]/+page.svelte

## Context The document detail page (`/documents/[id]`) has a `DocumentTopBar` component (`src/lib/components/DocumentTopBar.svelte`) that currently renders a single flat layout at all screen sizes. This issue implements the **TopBar B1 Responsive** design spec (`docs/specs/document-topbar-b1-responsive.html`), which defines a mobile-first, five-breakpoint layout with full dark mode support, person-chip overflow, and an annotate-mode state. Open the spec file in a browser to see all visual references referenced in this issue. --- ## What currently exists `DocumentTopBar.svelte` already: - Has a back link, title `<h1>`, a compact `compactMeta` string (date + sender/receiver as plain text), annotate toggle, edit link, and download link - Uses CSS custom properties (`bg-surface`, `border-line`, `text-ink`, etc.) from the project's theme - Receives props: `doc`, `canWrite`, `canAnnotate`, `fileUrl`, `annotateMode` (bindable) What is **missing**: responsive breakpoint logic, chip-based person display, receiver overflow pill, dark mode token variants, the annotate hint strip, and the viewport-specific show/hide rules described below. --- ## Design tokens All colours are already present in the project theme. Map spec names to Tailwind classes as follows: | Spec token | Light value | Dark value | Tailwind class | |---|---|---|---| | `bg-surface` | `#FFFFFF` | `#0F1923` | `bg-surface` (already `var(--color-surface)`) | | `border-line` | `#E4E2D8` | `#1E2D3D` | `border-line` | | `bg-primary` / accent bar | `#012851` | `#A1DCD8` | `border-primary` / `bg-primary` | | `text-primary-fg` | `#A1DCD8` | `#012851` | `text-primary-fg` | | `text-ink` | `#1A1A1A` | `#EAE8E2` | `text-ink` | | `text-ink-3` (meta) | `#AAAAAA` | `#3E5065` | `text-ink-2` (closest existing) | | `bg-muted` (chip bg) | `#F0EFE9` | `#0A1218` | `bg-muted` or `bg-canvas` | | chip border | `#DDD9D0` | `#1E2D3D` | `border-line` | **Accent bar**: `border-l-[3px] border-primary` — always present, all breakpoints, both themes. --- ## Responsive behaviour rules Implement these as Tailwind responsive classes. The topbar height and element visibility change at four breakpoints: | Element | `< 375px` (XS) | `375–767px` (mobile) | `768–1023px` (tablet) | `≥ 1024px` (laptop/desktop) | |---|---|---|---|---| | Topbar height | `h-11` (44 px) | `h-[50px]` | `h-[52px]` | `h-[52px]` | | Back button shape | Square `24×24`, `rounded` | Circle `26×26`, `rounded-full` | Circle `28×28` | Circle `28×28` | | Title font size | `text-[10px]` | `text-[10.5px]` | `text-[11.5px]` | `text-[12px]` (`text-[12.5px]` at 1440) | | Person chip row | **Hidden** — show plain-text meta line instead | Shown — abbreviated names (`K. Raddatz`) | Shown — full names | Shown — full names + location | | Date format | `dd.mm.yyyy` inline in plain-text meta | `dd.mm.yyyy` | `dd.mm.yyyy` | Full: `24. Dezember 1943` (use `month: 'long'`) | | Location in meta | Hidden | Hidden | Shown if `doc.location` present | Shown | | Status chip | Hidden | Hidden | Shown inline next to title | Shown | | Annotate button | Hidden | Hidden | Shown with text label | Shown with text label | | Edit button label | `"Edit"` | `"Edit"` | `"Bearbeiten"` | `"Bearbeiten"` | | Download button | Hidden | Hidden | Shown (icon only) | Shown (icon only) | | Divider between actions | Hidden | Hidden | Shown | Shown | | Hint strip (annotate active) | Hidden | Hidden | `18px` strip below main row | `18px` strip | --- ## Person chips Replace the current plain-text `compactMeta` receiver string with a chip row at `≥ 375px`. ### Chip structure (one chip) ```svelte <div class="inline-flex items-center gap-0.5 rounded-full border border-line bg-muted px-1.5 py-0.5 whitespace-nowrap shrink-0"> <!-- Avatar: initials, coloured bg --> <div class="flex h-[14px] w-[14px] shrink-0 items-center justify-center rounded-full bg-primary text-[5px] font-bold text-primary-fg"> KR </div> <!-- Name --> <span class="text-[6.5px] font-semibold text-ink">Karl Raddatz</span> </div> ``` - Avatar background cycles through a small set of colours keyed by person ID (use a deterministic hash). Suggested palette: `#012851` (navy), `#5A3080` (purple), `#007596` (teal), `#2A6040` (moss), `#803020` (rust). - At `375–767px`: use abbreviated name — first initial + last name (`K. Raddatz`). At `≥ 768px`: full name. ### Arrow between sender and receiver ```svelte <span class="text-ink-2 shrink-0 text-[9px]" aria-hidden="true">→</span> ``` Omit the arrow when there is no sender (direction unknown — e.g. photos, certificates). ### Overflow rule — always show sender + 1st receiver, collapse the rest ``` receivers.length === 0 → sender chip only, no arrow receivers.length === 1 → sender → receiver (no overflow) receivers.length === 2 → show both at ≥ 768px; collapse to sender → 1st + "+1" at < 768px receivers.length ≥ 3 → sender → 1st receiver → "+N weitere" (≥ 768px) / "+N" (375–767px) ``` **Overflow pill:** ```svelte <button class="inline-flex items-center rounded-full border border-line bg-muted px-1.5 py-0.5 text-[6.5px] font-bold text-ink-2 whitespace-nowrap shrink-0" onclick={() => overflowOpen = !overflowOpen} aria-haspopup="true" aria-expanded={overflowOpen} > +{extraCount} {viewport >= 768 ? 'weitere' : ''} </button> ``` At `< 375px` (XS), chips are hidden entirely. Overflow is shown as plain text appended to the meta line: `"K.Raddatz → E.Raddatz +4 · 24.12.1943"`. --- ## Overflow tooltip When the overflow pill is clicked/tapped (at `≥ 768px`), show a floating panel: - Positioned `absolute top-full left-0 mt-1` relative to the pill - Background: `bg-surface`, border `border-line`, `rounded-md`, `shadow-lg`, `p-3`, `min-w-[160px]`, `z-50` - Header: `"Weitere Empfänger"` in `text-[7px] font-bold uppercase tracking-wide text-ink-2`, with a bottom border - Each person: avatar (same colour logic as chips, `16×16`) + full name in `text-[9.5px] font-medium text-ink` - Each name is a link to `/persons/{id}` - Dismiss on: click outside, press Escape, click same pill again - Dark mode: panel uses `bg-surface` dark token automatically On mobile (`< 768px`) the overflow pill shows "+N" only (no tooltip) — tapping it does nothing (or could navigate to the edit page to see all receivers, but that's optional scope). **Active state of the pill** (tooltip open or annotate mode active): ``` bg-primary text-primary-fg border-primary ``` --- ## Annotate mode When `annotateMode === true` (passed as bindable prop): 1. **Edit button and Download button are hidden** 2. **Annotate ghost button becomes active** — filled navy bg (`bg-primary text-primary-fg`), label changes to `"Beenden"` 3. **Hint strip appears** directly below the main topbar row (only at `≥ 768px`): ```svelte <div class="flex h-[18px] items-center gap-2 border-t border-dashed border-primary/20 bg-primary/5 px-3.5 dark:border-primary/10 dark:bg-primary/4"> <span class="text-[5.5px] font-bold uppercase tracking-wide text-primary">Annotierungsmodus aktiv</span> <span class="text-[5.5px] text-ink-2">Klicken Sie auf eine Textstelle im Dokument, um eine Anmerkung hinzuzufügen.</span> </div> ``` Dark mode hint: `text-primary-fg` for the label, same muted bg. --- ## XS (< 375px) plain-text meta line At the smallest viewport, chips are not rendered. Instead, keep the existing `compactMeta` string but adjust its format: ``` K.Raddatz → E.Raddatz +4 · 24.12.1943 ``` - Sender: first initial + dot + last name (`K.Raddatz`) - Receivers: same abbreviated format; if overflow, append ` +N` after the first receiver name - Date: `dd.mm.yyyy` (numeric, no spaces) --- ## Status chip (≥ 768px) The `doc` object exposes a `status` field of type `DocumentStatus` (`PLACEHOLDER | UPLOADED | TRANSCRIBED | REVIEWED | ARCHIVED`). Show a small status badge inline with the title: ```svelte <span class="inline-flex items-center gap-1 rounded-sm border px-1.5 py-0.5 text-[5.5px] font-bold uppercase tracking-wide {statusClass(doc.status)}"> <span class="h-1 w-1 rounded-full {statusDotClass(doc.status)}"></span> {statusLabel(doc.status)} </span> ``` Use green (`bg-emerald-100 border-emerald-300 text-emerald-800`) for `UPLOADED`/`ARCHIVED`, a neutral muted style for the rest. Dark mode: use `dark:` variants with 7–10% opacity backgrounds. --- ## Accessibility requirements - The topbar must be wrapped in `<header role="banner">` or already be inside `<header>` — check the parent layout. - Back link: `aria-label="Zurück zur Dokumentenliste"` (always, because the icon is the only visible element on XS). - Overflow pill: `aria-haspopup="listbox"`, `aria-expanded`, `aria-label="Alle Empfänger anzeigen"`. - Tooltip list items: `role="listbox"` on the panel, `role="option"` on each person row. - Annotate button: `aria-pressed={annotateMode}`. - Focus trap is **not** needed for the tooltip (it is not a modal), but Escape should dismiss it and return focus to the pill. - All interactive elements must have a visible focus ring — never `outline: none` without a replacement. --- ## Out of scope for this issue - Actual annotation functionality (that is handled by `AnnotationSidePanel`) - Changes to `AnnotationSidePanel` or `DocumentBottomPanel` - The `location` field on `Document` — if it does not yet exist on the backend DTO, skip the location display for now and leave a `// TODO: show location when available` comment --- ## Acceptance criteria - [ ] Topbar renders correctly at 320px, 375px, 768px, 1024px, and 1440px — matches spec screenshots - [ ] Light and dark themes both match the token table above — no hardcoded hex values in the component - [ ] 0-receiver, 1-receiver, 2-receiver, 3-receiver, and 5-receiver cases all render correctly - [ ] Overflow tooltip opens/closes on click, keyboard (Enter/Space on pill, Escape to close), and click-outside - [ ] Annotate mode: Edit + Download hidden, hint strip visible, button label "Beenden" — at ≥ 768px only - [ ] Status chip visible at ≥ 768px, hidden below - [ ] Annotate button hidden below 768px - [ ] All touch targets ≥ 44×44px on mobile (back button, edit, annotate pill) - [ ] `aria-pressed`, `aria-haspopup`, `aria-expanded`, `aria-label` on all interactive elements - [ ] `svelte-check` passes with no new type errors - [ ] Visual proof: run `/proofshot` against the document detail page at all five viewport widths in both themes --- ## Reference Full annotated spec: `docs/specs/document-topbar-b1-responsive.html` — open in a browser. Existing component: `src/lib/components/DocumentTopBar.svelte` Consuming page: `src/routes/documents/[id]/+page.svelte`
marcel added the featureui labels 2026-03-30 10:37:55 +02:00
Author
Owner

👨‍💻 Felix Brandt — Senior Fullstack Developer

Component Decomposition First

The issue describes multiple distinct visual regions packed into one component. Before writing a single line of code, I'd draw these boundaries:

  • DocumentTopBar.svelte — orchestrator, holds overflowOpen and passes props down
  • PersonChipRow.svelte — chip row with arrow, visible at ≥ 375px
  • PersonChip.svelte — single chip (avatar + name), receives person and abbreviated: boolean
  • OverflowPill.svelte — the "+N weitere" button and its tooltip panel
  • DocumentStatusChip.svelte — status badge with dot indicator
  • AnnotateHintStrip.svelte — the 18px annotate-mode banner

A single component doing all of this will exceed 40 lines of template easily and become untestable. Each of these has a clear one-word name and a single visual responsibility.

TDD Questions

  • The abbreviation logic ("Karl Raddatz""K. Raddatz") is a pure function — it needs a Vitest unit test written before the implementation. What are the edge cases? Single-name persons? Hyphenated last names ("K. Müller-Schmidt")?
  • The deterministic avatar color hash (person ID → one of 5 colours) is independently testable. Will there be a test asserting that the same ID always maps to the same colour?
  • The XS compact meta string ("K.Raddatz → E.Raddatz +4 · 24.12.1943") is pure formatting logic — this belongs in a formatCompactMeta() utility module, not inline in the template.
  • The statusLabel() and statusClass() helper functions should each have unit tests covering all 5 DocumentStatus values.

Svelte 5 Concerns

  • Viewport detection: The spec references viewport >= 768 in the overflow pill template. If this means window.innerWidth at runtime, that is a side-effect, not a derived value. It must not be computed inline in markup. Use a $derived from a reactive windowWidth store, or — better — use CSS-only responsive classes everywhere and avoid JS viewport checks entirely.
  • $derived for computed values: extraCount, visibleReceivers, abbreviatedSender, formattedDate — all of these must be $derived, never $state + $effect.
  • Keyed {#each}: Every chip list must use (person.id) as the key, not index. Reordering receivers without a key will silently corrupt local chip state.
  • overflowOpen state: Fine as $state on the orchestrator — just make sure Escape dismissal uses $effect wired to a keydown handler, not inline template logic.

Naming

  • overflowOpen — good boolean name (reads as a yes/no question ✓)
  • extraCount — clear ✓
  • compactMeta — a bit vague; xsMetaLine or plainTextMeta would better communicate that this is the XS-only fallback

Suggestions

  • Extract abbreviateName(person: Person): string and formatXsMeta(doc: Document): string into src/lib/utils/personFormat.ts — these are reusable and independently testable.
  • Extract personAvatarColor(personId: string): string into the same util module.
  • The click-outside dismiss for the tooltip: use a Svelte action (use:clickOutside) rather than wiring document.addEventListener directly in $effect. Keeps the component clean and the action reusable.
## 👨‍💻 Felix Brandt — Senior Fullstack Developer ### Component Decomposition First The issue describes multiple distinct visual regions packed into one component. Before writing a single line of code, I'd draw these boundaries: - `DocumentTopBar.svelte` — orchestrator, holds `overflowOpen` and passes props down - `PersonChipRow.svelte` — chip row with arrow, visible at `≥ 375px` - `PersonChip.svelte` — single chip (avatar + name), receives `person` and `abbreviated: boolean` - `OverflowPill.svelte` — the "+N weitere" button and its tooltip panel - `DocumentStatusChip.svelte` — status badge with dot indicator - `AnnotateHintStrip.svelte` — the 18px annotate-mode banner A single component doing all of this will exceed 40 lines of template easily and become untestable. Each of these has a clear one-word name and a single visual responsibility. ### TDD Questions - The abbreviation logic (`"Karl Raddatz"` → `"K. Raddatz"`) is a pure function — it needs a Vitest unit test written **before** the implementation. What are the edge cases? Single-name persons? Hyphenated last names (`"K. Müller-Schmidt"`)? - The deterministic avatar color hash (person ID → one of 5 colours) is independently testable. Will there be a test asserting that the same ID always maps to the same colour? - The XS compact meta string (`"K.Raddatz → E.Raddatz +4 · 24.12.1943"`) is pure formatting logic — this belongs in a `formatCompactMeta()` utility module, not inline in the template. - The `statusLabel()` and `statusClass()` helper functions should each have unit tests covering all 5 `DocumentStatus` values. ### Svelte 5 Concerns - **Viewport detection**: The spec references `viewport >= 768` in the overflow pill template. If this means `window.innerWidth` at runtime, that is a side-effect, not a derived value. It must not be computed inline in markup. Use a `$derived` from a reactive `windowWidth` store, or — better — use CSS-only responsive classes everywhere and avoid JS viewport checks entirely. - **`$derived` for computed values**: `extraCount`, `visibleReceivers`, `abbreviatedSender`, `formattedDate` — all of these must be `$derived`, never `$state` + `$effect`. - **Keyed `{#each}`**: Every chip list must use `(person.id)` as the key, not index. Reordering receivers without a key will silently corrupt local chip state. - **`overflowOpen` state**: Fine as `$state` on the orchestrator — just make sure Escape dismissal uses `$effect` wired to a `keydown` handler, not inline template logic. ### Naming - `overflowOpen` — good boolean name (reads as a yes/no question ✓) - `extraCount` — clear ✓ - `compactMeta` — a bit vague; `xsMetaLine` or `plainTextMeta` would better communicate that this is the XS-only fallback ### Suggestions - Extract `abbreviateName(person: Person): string` and `formatXsMeta(doc: Document): string` into `src/lib/utils/personFormat.ts` — these are reusable and independently testable. - Extract `personAvatarColor(personId: string): string` into the same util module. - The click-outside dismiss for the tooltip: use a Svelte action (`use:clickOutside`) rather than wiring `document.addEventListener` directly in `$effect`. Keeps the component clean and the action reusable.
Author
Owner

🏛️ Markus Keller — Application Architect

Scope Assessment

This is a pure frontend component change — no backend, no database, no new services. The scope is well-contained and the layering is correct: data flows in from the server load function via doc props, the component renders. That's the right pattern.

SSR vs. Client-Side Viewport Detection

My main architectural concern: the overflow pill contains viewport >= 768 ? 'weitere' : '' — this implies a runtime JS check of window.innerWidth. On SvelteKit with SSR, window is not available during server render. If this lands in the component script without a browser guard, it will throw on SSR.

Two acceptable approaches:

  1. CSS-only: Never read window.innerWidth in JS at all. The word "weitere" can be shown/hidden purely with responsive Tailwind classes (hidden sm:inline). This is the right call — no hydration mismatch, no JS needed.
  2. $effect with browser guard: If JS viewport detection is genuinely necessary (e.g. for JS-driven tooltip positioning), gate it behind import { browser } from '$app/environment'. But I'd exhaust CSS-only options first.

The 375px Custom Breakpoint

Tailwind's default breakpoints are sm: 640px, md: 768px, lg: 1024px, xl: 1280px. The 375px breakpoint used throughout this spec is non-standard. This means a custom breakpoint must be added to tailwind.config.ts:

theme: {
  extend: {
    screens: {
      'xs': '375px',
    }
  }
}

Is this already in place? If not, classes like xs:flex will silently do nothing, and the responsive logic won't work. Worth confirming before implementation starts.

annotateMode State Ownership

The annotateMode prop is declared as bindable (bind:annotateMode from the parent page). This means the parent page (+page.svelte) owns the state, and the annotation side panel presumably also reads it. That's correct.

My question: if multiple components read and write annotateMode (TopBar toggles it, the annotation panel may reset it), is a simple bindable prop sufficient, or should this be a shared $state in a context or a small Svelte store? I'm not blocking implementation on this — a bindable prop is fine for now — but it's worth clarifying how the annotation panel's own close button will interact with this.

Design Token Mapping

The issue maps spec tokens to Tailwind classes and notes some are approximations (e.g. text-ink-3text-ink-2 as "closest existing"). I'd want this documented with a brief comment in the component or in layout.css so future maintainers understand the intentional approximation rather than treating it as a bug.

No Concerns On

  • The component consuming doc via props from server load — correct dependency direction
  • The tooltip not being a modal (no focus trap) — correct per ARIA popup pattern
  • The // TODO: show location when available placeholder — pragmatic and clearly scoped out
## 🏛️ Markus Keller — Application Architect ### Scope Assessment This is a pure frontend component change — no backend, no database, no new services. The scope is well-contained and the layering is correct: data flows in from the server load function via `doc` props, the component renders. That's the right pattern. ### SSR vs. Client-Side Viewport Detection My main architectural concern: the overflow pill contains `viewport >= 768 ? 'weitere' : ''` — this implies a runtime JS check of `window.innerWidth`. On SvelteKit with SSR, `window` is not available during server render. If this lands in the component script without a `browser` guard, it will throw on SSR. Two acceptable approaches: 1. **CSS-only**: Never read `window.innerWidth` in JS at all. The word `"weitere"` can be shown/hidden purely with responsive Tailwind classes (`hidden sm:inline`). This is the right call — no hydration mismatch, no JS needed. 2. **`$effect` with `browser` guard**: If JS viewport detection is genuinely necessary (e.g. for JS-driven tooltip positioning), gate it behind `import { browser } from '$app/environment'`. But I'd exhaust CSS-only options first. ### The `375px` Custom Breakpoint Tailwind's default breakpoints are `sm: 640px`, `md: 768px`, `lg: 1024px`, `xl: 1280px`. The `375px` breakpoint used throughout this spec is non-standard. This means a custom breakpoint must be added to `tailwind.config.ts`: ```typescript theme: { extend: { screens: { 'xs': '375px', } } } ``` Is this already in place? If not, classes like `xs:flex` will silently do nothing, and the responsive logic won't work. Worth confirming before implementation starts. ### `annotateMode` State Ownership The `annotateMode` prop is declared as bindable (`bind:annotateMode` from the parent page). This means the parent page (`+page.svelte`) owns the state, and the annotation side panel presumably also reads it. That's correct. My question: if multiple components read and write `annotateMode` (TopBar toggles it, the annotation panel may reset it), is a simple bindable prop sufficient, or should this be a shared `$state` in a context or a small Svelte store? I'm not blocking implementation on this — a bindable prop is fine for now — but it's worth clarifying how the annotation panel's own close button will interact with this. ### Design Token Mapping The issue maps spec tokens to Tailwind classes and notes some are approximations (e.g. `text-ink-3` → `text-ink-2` as "closest existing"). I'd want this documented with a brief comment in the component or in `layout.css` so future maintainers understand the intentional approximation rather than treating it as a bug. ### No Concerns On - The component consuming `doc` via props from server load — correct dependency direction - The tooltip not being a modal (no focus trap) — correct per ARIA popup pattern - The `// TODO: show location when available` placeholder — pragmatic and clearly scoped out
Author
Owner

🧪 Sara Holt — QA Engineer & Test Strategist

Test Strategy

This feature has significant logic complexity hiding behind its visual surface. Here's how I'd layer the test coverage:

Unit tests (Vitest) — pure utility functions, fast, no DOM:

  • abbreviateName(person) — full name, single word name, hyphenated surname
  • formatXsMeta(doc) — 0 receivers, 1 receiver, 3 receivers with overflow, no sender
  • personAvatarColor(personId) — deterministic: same ID always returns same colour; all IDs map to a valid palette entry
  • statusLabel(status) — all 5 DocumentStatus values
  • statusClass(status) — all 5 values, including dark-mode class variants

Component tests (Vitest + @testing-library/svelte) — render + ARIA assertions:

  • Chip row shows abbreviated names at < 768px prop context and full names at ≥ 768px
  • 0 receivers: sender chip only, no arrow rendered
  • 1 receiver: sender + arrow + receiver, no overflow pill
  • 2 receivers at mobile: sender + 1st receiver + "+1" pill; no "weitere" text
  • 3+ receivers: overflow pill with correct count
  • Annotate mode true: Edit and Download buttons not in DOM; hint strip present; button label "Beenden"
  • Annotate mode false: Edit and Download visible; hint strip absent; button label "Annotieren" (or equivalent)
  • Status chip: rendered at ≥ 768px breakpoint context; correct class per status

E2E tests (Playwright):

  • Overflow tooltip: opens on click, closes on second click, closes on Escape, closes on click outside
  • Keyboard: Enter/Space on overflow pill opens tooltip; Escape returns focus to pill
  • Back link is tabbable and has visible focus ring
  • Annotate button toggles aria-pressed correctly
  • checkA11y runs on the document detail page in both light and dark mode

Missing Acceptance Criteria

The current criteria don't specify:

  • What happens if doc.sender is null (the "no sender" case is described in the spec but not in the AC checkboxes)
  • Whether the tooltip links to /persons/{id} are tested for correct navigation
  • Whether the annotate hint strip is absent on mobile (< 768px) — the spec says so, but there's no AC item for it

I'd suggest adding:

  • No sender case: chip row shows receivers only, no arrow
  • Annotate hint strip is not rendered below 768px
  • Overflow tooltip links navigate to correct /persons/{id} URL

Viewport Coverage

The AC mentions 320px, 375px, 768px, 1024px, and 1440px. For Playwright visual regression, I'd capture both light and dark mode at each — that's 10 screenshots to baseline. The /proofshot skill should cover this, but make sure the Playwright config has viewport presets for all five widths and that prefers-color-scheme or the dark mode class toggle is automated.

Risk: The "click outside" dismiss

Click-outside logic is historically flaky in component tests. If using a Svelte action, test the action in isolation. If using document.addEventListener in an $effect, test via Playwright E2E only — JSDOM's event bubbling doesn't always match real browsers.

## 🧪 Sara Holt — QA Engineer & Test Strategist ### Test Strategy This feature has significant logic complexity hiding behind its visual surface. Here's how I'd layer the test coverage: **Unit tests (Vitest) — pure utility functions, fast, no DOM:** - `abbreviateName(person)` — full name, single word name, hyphenated surname - `formatXsMeta(doc)` — 0 receivers, 1 receiver, 3 receivers with overflow, no sender - `personAvatarColor(personId)` — deterministic: same ID always returns same colour; all IDs map to a valid palette entry - `statusLabel(status)` — all 5 `DocumentStatus` values - `statusClass(status)` — all 5 values, including dark-mode class variants **Component tests (Vitest + @testing-library/svelte) — render + ARIA assertions:** - Chip row shows abbreviated names at < 768px prop context and full names at ≥ 768px - 0 receivers: sender chip only, no arrow rendered - 1 receiver: sender + arrow + receiver, no overflow pill - 2 receivers at mobile: sender + 1st receiver + "+1" pill; no "weitere" text - 3+ receivers: overflow pill with correct count - Annotate mode `true`: Edit and Download buttons not in DOM; hint strip present; button label "Beenden" - Annotate mode `false`: Edit and Download visible; hint strip absent; button label "Annotieren" (or equivalent) - Status chip: rendered at ≥ 768px breakpoint context; correct class per status **E2E tests (Playwright):** - Overflow tooltip: opens on click, closes on second click, closes on Escape, closes on click outside - Keyboard: Enter/Space on overflow pill opens tooltip; Escape returns focus to pill - Back link is tabbable and has visible focus ring - Annotate button toggles `aria-pressed` correctly - `checkA11y` runs on the document detail page in both light and dark mode ### Missing Acceptance Criteria The current criteria don't specify: - What happens if `doc.sender` is `null` (the "no sender" case is described in the spec but not in the AC checkboxes) - Whether the tooltip links to `/persons/{id}` are tested for correct navigation - Whether the annotate hint strip is **absent** on mobile (< 768px) — the spec says so, but there's no AC item for it I'd suggest adding: - [ ] No sender case: chip row shows receivers only, no arrow - [ ] Annotate hint strip is not rendered below 768px - [ ] Overflow tooltip links navigate to correct `/persons/{id}` URL ### Viewport Coverage The AC mentions 320px, 375px, 768px, 1024px, and 1440px. For Playwright visual regression, I'd capture both light and dark mode at each — that's 10 screenshots to baseline. The `/proofshot` skill should cover this, but make sure the Playwright config has viewport presets for all five widths and that `prefers-color-scheme` or the dark mode class toggle is automated. ### Risk: The "click outside" dismiss Click-outside logic is historically flaky in component tests. If using a Svelte action, test the action in isolation. If using `document.addEventListener` in an `$effect`, test via Playwright E2E only — JSDOM's event bubbling doesn't always match real browsers.
Author
Owner

🔐 Nora "NullX" Steiner — Application Security Engineer

Attack Surface Assessment

This is a frontend-only UI component change with no new API endpoints, no new backend logic, and no new data flows. The attack surface is narrow. Findings below are low-severity observations and smells — nothing blocking — but worth addressing cleanly.

Observation 1: CSS Injection via Avatar Color (Informational)

The spec calls for a deterministic avatar color derived from a person ID via a hash. If this hash is ever used to set an inline style attribute (e.g. style="background-color: {personAvatarColor(person.id)}"), and the returned value is not constrained to the predefined palette, there's a theoretical CSS injection vector.

This is low risk in practice because the hash returns one of 5 hardcoded hex literals — user input never flows into the CSS value directly. But:

  • The implementation must return a value from the palette array, not construct a string from the person ID itself
  • Safe: PALETTE[hash(id) % PALETTE.length]
  • Unsafe: anything that interpolates id into the returned colour string

No action needed if implemented as above — just flagging the pattern to watch for.

Observation 2: aria-haspopup Attribute Type (Informational)

The spec shows:

aria-haspopup="true"

The ARIA spec defines aria-haspopup as an enumerated token (menu, listbox, tree, grid, dialog, or true). Svelte may render aria-haspopup="true" as the string "true" rather than the boolean. Given the tooltip is acting as a listbox, the correct value is:

aria-haspopup="listbox"

This is also what the issue body specifies in the Accessibility section — just making sure the code matches the spec, not the snippet in the "Overflow pill" section which says "true".

The tooltip renders <a href="/persons/{person.id}"> for each overflow receiver. Confirm that:

  • person.id values come from the authenticated server load response (they do, per the current architecture)
  • They are UUIDs, not user-controlled slugs or free-text strings rendered into the href

UUID hrefs are safe. If the person model ever gains a slug field and that slug is used here without sanitization, that would need a second look. Not a current concern, just a note for future development.

No Concerns On

  • No new authentication boundaries — the component renders behind existing auth
  • No new API calls from the component — all data comes from SSR props
  • The annotateMode bindable prop is internal UI state, not URL-derived — no risk of external manipulation
  • The Escape key handler is a standard event listener — no eval, no dynamic code execution
## 🔐 Nora "NullX" Steiner — Application Security Engineer ### Attack Surface Assessment This is a frontend-only UI component change with no new API endpoints, no new backend logic, and no new data flows. The attack surface is narrow. Findings below are low-severity observations and smells — nothing blocking — but worth addressing cleanly. ### Observation 1: CSS Injection via Avatar Color (Informational) The spec calls for a deterministic avatar color derived from a person ID via a hash. If this hash is ever used to set an inline `style` attribute (e.g. `style="background-color: {personAvatarColor(person.id)}"`), and the returned value is not constrained to the predefined palette, there's a theoretical CSS injection vector. **This is low risk in practice** because the hash returns one of 5 hardcoded hex literals — user input never flows into the CSS value directly. But: - The implementation must return a value **from the palette array**, not construct a string from the person ID itself - ✅ Safe: `PALETTE[hash(id) % PALETTE.length]` - ❌ Unsafe: anything that interpolates `id` into the returned colour string No action needed if implemented as above — just flagging the pattern to watch for. ### Observation 2: `aria-haspopup` Attribute Type (Informational) The spec shows: ```svelte aria-haspopup="true" ``` The ARIA spec defines `aria-haspopup` as an enumerated token (`menu`, `listbox`, `tree`, `grid`, `dialog`, or `true`). Svelte may render `aria-haspopup="true"` as the string `"true"` rather than the boolean. Given the tooltip is acting as a `listbox`, the correct value is: ```svelte aria-haspopup="listbox" ``` This is also what the issue body specifies in the Accessibility section — just making sure the code matches the spec, not the snippet in the "Overflow pill" section which says `"true"`. ### Observation 3: Tooltip Links to `/persons/{id}` — Confirm ID Source The tooltip renders `<a href="/persons/{person.id}">` for each overflow receiver. Confirm that: - `person.id` values come from the authenticated server load response (they do, per the current architecture) - They are UUIDs, not user-controlled slugs or free-text strings rendered into the href UUID hrefs are safe. If the person model ever gains a slug field and that slug is used here without sanitization, that would need a second look. Not a current concern, just a note for future development. ### No Concerns On - No new authentication boundaries — the component renders behind existing auth - No new API calls from the component — all data comes from SSR props - The `annotateMode` bindable prop is internal UI state, not URL-derived — no risk of external manipulation - The Escape key handler is a standard event listener — no eval, no dynamic code execution
Author
Owner

🎨 Leonie Voss — UI/UX Design Lead

Critical: Font Sizes Below Any Accessible Threshold

This is my primary concern with the spec as written. Several text sizes specified are dangerously small:

Element Spec size Rendered px (at 1×) WCAG concern
Hint strip label text-[5.5px] ~7px Fails — unreadable
Chip name text text-[6.5px] ~8.5px Fails — unreadable
Avatar initials text-[5px] ~6.5px Decorative, but marginal
Status chip label text-[5.5px] ~7px Fails — unreadable
Status dot text-[5.5px] Decorative ✓
Topbar title text-[10px]text-[12px] 10–12px Borderline — WCAG SC 1.4.4

WCAG SC 1.4.4 (Resize Text) requires that text can be scaled to 200% without loss of functionality. Text at 6–7px is effectively unreadable at 1× and becomes only marginally usable at 200%. More practically: no user, regardless of age or visual ability, can read 6.5px chip text on a physical device. This appears to be a spec artifact from designing at high zoom in Figma.

My recommendation: Scale up all chip and label text to a minimum of text-[9px] (chip names) and text-[8px] (status labels), with the topbar title at text-xs (text-[12px]) as the minimum. The topbar will need to accommodate this — consider increasing the h-[50px] heights slightly, or reducing horizontal padding to give text more room.

Status Chip: Color Is Not the Only Cue — But Check the Implementation

The spec uses green (bg-emerald-100) for UPLOADED/ARCHIVED and muted styles for others. Green vs. muted is a color-only distinction for status. This is acceptable only if the status label text is always visible (which it is per the spec — statusLabel(doc.status) renders text). The colored dot adds reinforcement. This passes WCAG 1.4.1 (Use of Color) as long as the text label is always present alongside the color.

However: what are the German label strings for each status? The issue doesn't specify. PLACEHOLDER, UPLOADED, etc. are English enum values — the rendered label should be in German and translated via Paraglide.

Touch Targets

The spec states all touch targets must be ≥ 44×44px on mobile. Checking the defined elements:

  • Back button (XS): Square 24×24px — fails the 44×44 requirement. Must be padded to 44×44 minimum with p-2.5 or similar, so the visual icon stays small but the hit area is large.
  • Overflow pill: Small text pill — needs min-h-[44px] or padding equivalent on mobile.
  • Annotate button: Hidden below 768px per the spec — not a touch-target concern at mobile.
  • Person chips: Not interactive (except overflow pill) — no touch target requirement.

Overflow Tooltip Accessibility

The ARIA pattern described (role="listbox", role="option") is correct for a non-modal popup. One addition: when the tooltip opens, focus should move to the first role="option" element (or the panel itself), not remain on the pill. The spec says "Escape should dismiss and return focus to the pill" — for that return to work, focus must have moved into the tooltip first.

Dark Mode Tokens

The token mapping table is clear and uses CSS custom properties throughout — this is the right approach. I'd just confirm that bg-primary/4 (used in the hint strip dark mode) is a valid Tailwind opacity modifier. bg-primary/5 is listed for light mode; dark uses /4. Both should work as long as --color-primary is defined as an RGB value (not a hex), which is required for Tailwind's opacity modifiers to function.

Minor: Arrow Character for Sender → Receiver

The arrow () is used inline as text. This should have aria-hidden="true" (already noted in the spec ✓) and should not be a hyphen-minus or ASCII > — the spec uses the correct Unicode directional arrow. Just flagging this so it isn't accidentally simplified during implementation.

## 🎨 Leonie Voss — UI/UX Design Lead ### Critical: Font Sizes Below Any Accessible Threshold This is my primary concern with the spec as written. Several text sizes specified are dangerously small: | Element | Spec size | Rendered px (at 1×) | WCAG concern | |---|---|---|---| | Hint strip label | `text-[5.5px]` | ~7px | Fails — unreadable | | Chip name text | `text-[6.5px]` | ~8.5px | Fails — unreadable | | Avatar initials | `text-[5px]` | ~6.5px | Decorative, but marginal | | Status chip label | `text-[5.5px]` | ~7px | Fails — unreadable | | Status dot | `text-[5.5px]` | — | Decorative ✓ | | Topbar title | `text-[10px]`–`text-[12px]` | 10–12px | Borderline — WCAG SC 1.4.4 | WCAG SC 1.4.4 (Resize Text) requires that text can be scaled to 200% without loss of functionality. Text at 6–7px is effectively unreadable at 1× and becomes only marginally usable at 200%. More practically: **no user, regardless of age or visual ability, can read 6.5px chip text on a physical device.** This appears to be a spec artifact from designing at high zoom in Figma. **My recommendation**: Scale up all chip and label text to a minimum of `text-[9px]` (chip names) and `text-[8px]` (status labels), with the topbar title at `text-xs` (`text-[12px]`) as the minimum. The topbar will need to accommodate this — consider increasing the `h-[50px]` heights slightly, or reducing horizontal padding to give text more room. ### Status Chip: Color Is Not the Only Cue — But Check the Implementation The spec uses green (`bg-emerald-100`) for `UPLOADED`/`ARCHIVED` and muted styles for others. Green vs. muted is a color-only distinction for status. This is acceptable *only if* the status label text is always visible (which it is per the spec — `statusLabel(doc.status)` renders text). The colored dot adds reinforcement. This passes WCAG 1.4.1 (Use of Color) as long as the text label is always present alongside the color. However: what are the German label strings for each status? The issue doesn't specify. `PLACEHOLDER`, `UPLOADED`, etc. are English enum values — the rendered label should be in German and translated via Paraglide. ### Touch Targets The spec states all touch targets must be ≥ 44×44px on mobile. Checking the defined elements: - **Back button (XS)**: Square `24×24px` — fails the 44×44 requirement. Must be padded to 44×44 minimum with `p-2.5` or similar, so the visual icon stays small but the hit area is large. - **Overflow pill**: Small text pill — needs `min-h-[44px]` or padding equivalent on mobile. - **Annotate button**: Hidden below 768px per the spec — not a touch-target concern at mobile. - **Person chips**: Not interactive (except overflow pill) — no touch target requirement. ### Overflow Tooltip Accessibility The ARIA pattern described (role="listbox", role="option") is correct for a non-modal popup. One addition: when the tooltip opens, focus should move to the first `role="option"` element (or the panel itself), not remain on the pill. The spec says "Escape should dismiss and return focus to the pill" — for that return to work, focus must have moved into the tooltip first. ### Dark Mode Tokens The token mapping table is clear and uses CSS custom properties throughout — this is the right approach. I'd just confirm that `bg-primary/4` (used in the hint strip dark mode) is a valid Tailwind opacity modifier. `bg-primary/5` is listed for light mode; dark uses `/4`. Both should work as long as `--color-primary` is defined as an RGB value (not a hex), which is required for Tailwind's opacity modifiers to function. ### Minor: Arrow Character for Sender → Receiver The `→` arrow (`→`) is used inline as text. This should have `aria-hidden="true"` (already noted in the spec ✓) and should not be a hyphen-minus or ASCII `>` — the spec uses the correct Unicode directional arrow. Just flagging this so it isn't accidentally simplified during implementation.
Author
Owner

⚙️ Tobias Wendt — DevOps & Platform Engineer

No Infrastructure Changes Required

This is a pure frontend component change. No new services, no new environment variables, no new Docker volumes or containers, no schema migrations. Clean from an ops perspective — this deploys with the normal frontend build and does not require a coordinated release.

Custom Tailwind Breakpoint — Check Before CI Runs

If a xs: 375px breakpoint is added to tailwind.config.ts (as Markus noted), make sure the npm run check and npm run build steps in the CI workflow still pass cleanly. Tailwind 4 handles custom screen tokens differently from v3 — confirm the syntax is correct for the version in use, otherwise the build will silently produce wrong output without failing.

Dark Mode in CI/Playwright

The acceptance criteria requires visual proof in both light and dark mode. Two questions:

  1. How is dark mode toggled in this project? If it's class-based (dark class on <html>) rather than prefers-color-scheme, Playwright tests need to explicitly add that class rather than using colorScheme: 'dark' in the browser context config. These are different mechanisms and need the right approach for tests to actually test dark mode.

  2. Does the /proofshot skill handle multi-theme screenshots automatically? If not, the acceptance criterion "visual proof in both themes" will require manual steps, which should be documented in the PR description so reviewers know what to verify.

svelte-check in CI

The AC includes "svelte-check passes with no new type errors." Confirm this runs as part of the existing CI workflow (it should — npm run check is typically included). If the new components introduce any missing prop types or untyped event handlers, it'll catch them before merge. No action needed if it's already wired up — just confirming.

Deployment Risk

None. The component is self-contained, server-rendered, and carries no state to the database. A rollback is as simple as reverting the frontend container. Low risk, standard deployment.

One Flag: proofshot-artifacts/ in Git Status

I notice proofshot-artifacts/ directories are showing as untracked in the repo. These should be in .gitignore if they aren't already — artifact directories from visual proof sessions shouldn't land in the working tree permanently. Not related to this issue, but worth a quick check.

## ⚙️ Tobias Wendt — DevOps & Platform Engineer ### No Infrastructure Changes Required This is a pure frontend component change. No new services, no new environment variables, no new Docker volumes or containers, no schema migrations. Clean from an ops perspective — this deploys with the normal frontend build and does not require a coordinated release. ### Custom Tailwind Breakpoint — Check Before CI Runs If a `xs: 375px` breakpoint is added to `tailwind.config.ts` (as Markus noted), make sure the `npm run check` and `npm run build` steps in the CI workflow still pass cleanly. Tailwind 4 handles custom screen tokens differently from v3 — confirm the syntax is correct for the version in use, otherwise the build will silently produce wrong output without failing. ### Dark Mode in CI/Playwright The acceptance criteria requires visual proof in both light and dark mode. Two questions: 1. **How is dark mode toggled in this project?** If it's class-based (`dark` class on `<html>`) rather than `prefers-color-scheme`, Playwright tests need to explicitly add that class rather than using `colorScheme: 'dark'` in the browser context config. These are different mechanisms and need the right approach for tests to actually test dark mode. 2. **Does the `/proofshot` skill handle multi-theme screenshots automatically?** If not, the acceptance criterion "visual proof in both themes" will require manual steps, which should be documented in the PR description so reviewers know what to verify. ### `svelte-check` in CI The AC includes "svelte-check passes with no new type errors." Confirm this runs as part of the existing CI workflow (it should — `npm run check` is typically included). If the new components introduce any missing prop types or untyped event handlers, it'll catch them before merge. No action needed if it's already wired up — just confirming. ### Deployment Risk None. The component is self-contained, server-rendered, and carries no state to the database. A rollback is as simple as reverting the frontend container. Low risk, standard deployment. ### One Flag: `proofshot-artifacts/` in Git Status I notice `proofshot-artifacts/` directories are showing as untracked in the repo. These should be in `.gitignore` if they aren't already — artifact directories from visual proof sessions shouldn't land in the working tree permanently. Not related to this issue, but worth a quick check.
Author
Owner

🎨 Leonie Voss — Design Review Discussion

Follow-up discussion resolving the open questions from my earlier review comment. All seven items were worked through with the team.


Resolved

1. Font sizes

  • Minimum floor of text-[9px] for all informational text (chip names, hint strip, tooltip header)
  • Status chip label: text-[8px]
  • Topbar title minimum: text-[11px] at XS (not text-[10px] per spec)
  • Topbar heights may increase slightly to accommodate: suggest h-12 (48px) at XS, h-14 (56px) at mobile/tablet. Implementer verifies against actual render.
  • Readable text takes priority over compact topbar height.

2. Status chip — labels removed

  • Text label dropped entirely; coloured dot only.
  • Chip gets title={doc.status} for hover tooltip and aria-label for screen readers.
  • Removes the i18n problem; no new Paraglide keys needed.

3. Back button touch target

  • Visual icon stays at spec size (24–28px).
  • Clickable element padded to w-11 h-11 (44×44px).
  • Use negative left margin (-ml-2) to preserve visual alignment to the left edge.

4. Overflow tooltip keyboard flow

  • Full keyboard support: opening the tooltip moves focus to the first role="option" (first person link).
  • Tab / Shift+Tab navigates between options inside the tooltip.
  • Escape closes the tooltip and returns focus to the overflow pill.
  • Applies at ≥ 768px only — tooltip does not exist on mobile.

5. Edit button on small screens

  • XS + mobile: icon only (pencil), no text label.
  • Tablet + desktop: icon + "Bearbeiten" label.
  • aria-label="Bearbeiten" always present on the element.
  • No language switching — consistent with how Download is handled at larger sizes.

6. Overflow pill on mobile (< 768px)

  • Rendered as a non-interactive <span> with aria-hidden="true".
  • No tap behaviour — the full receiver list is accessible via the metadata tab in the bottom drawer.

7. --color-primary CSS format

  • Currently defined as hex (#012851) — Tailwind opacity modifiers (/4, /20) will silently break.
  • Fix: convert --color-primary to raw RGB channels (1 40 81) in layout.css.
  • Before changing: grep all usages of var(--color-primary) to ensure nothing expects a hex string.
  • If conversion is risky, fallback: define explicit pre-computed tokens for the hint strip backgrounds instead.

Overall: the spec is solid and the implementation path is clear. The font size and CSS token issues are the two things that must be addressed before implementation starts — everything else can be handled inline during the build.

## 🎨 Leonie Voss — Design Review Discussion Follow-up discussion resolving the open questions from my earlier review comment. All seven items were worked through with the team. --- ### ✅ Resolved **1. Font sizes** - Minimum floor of `text-[9px]` for all informational text (chip names, hint strip, tooltip header) - Status chip label: `text-[8px]` - Topbar title minimum: `text-[11px]` at XS (not `text-[10px]` per spec) - Topbar heights may increase slightly to accommodate: suggest `h-12` (48px) at XS, `h-14` (56px) at mobile/tablet. Implementer verifies against actual render. - Readable text takes priority over compact topbar height. **2. Status chip — labels removed** - Text label dropped entirely; coloured dot only. - Chip gets `title={doc.status}` for hover tooltip and `aria-label` for screen readers. - Removes the i18n problem; no new Paraglide keys needed. **3. Back button touch target** - Visual icon stays at spec size (24–28px). - Clickable element padded to `w-11 h-11` (44×44px). - Use negative left margin (`-ml-2`) to preserve visual alignment to the left edge. **4. Overflow tooltip keyboard flow** - Full keyboard support: opening the tooltip moves focus to the first `role="option"` (first person link). - Tab / Shift+Tab navigates between options inside the tooltip. - Escape closes the tooltip and returns focus to the overflow pill. - Applies at `≥ 768px` only — tooltip does not exist on mobile. **5. Edit button on small screens** - XS + mobile: icon only (pencil), no text label. - Tablet + desktop: icon + `"Bearbeiten"` label. - `aria-label="Bearbeiten"` always present on the element. - No language switching — consistent with how Download is handled at larger sizes. **6. Overflow pill on mobile (`< 768px`)** - Rendered as a non-interactive `<span>` with `aria-hidden="true"`. - No tap behaviour — the full receiver list is accessible via the metadata tab in the bottom drawer. **7. `--color-primary` CSS format** - Currently defined as hex (`#012851`) — Tailwind opacity modifiers (`/4`, `/20`) will silently break. - Fix: convert `--color-primary` to raw RGB channels (`1 40 81`) in `layout.css`. - Before changing: grep all usages of `var(--color-primary)` to ensure nothing expects a hex string. - If conversion is risky, fallback: define explicit pre-computed tokens for the hint strip backgrounds instead. --- Overall: the spec is solid and the implementation path is clear. The font size and CSS token issues are the two things that must be addressed before implementation starts — everything else can be handled inline during the build.
Sign in to join this conversation.
No Label feature ui
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marcel/familienarchiv#161