feat(ui): implement responsive DocumentTopBar — final spec #173

Closed
opened 2026-03-31 20:23:05 +02:00 by marcel · 7 comments
Owner

Context

Implement the DocumentTopBar component per the final implementation spec at docs/specs/document-topbar-final-spec.html. This spec supersedes document-topbar-b1-responsive.html and incorporates all resolutions from the issue #161 team review.

Open the spec in a browser before starting — it contains visual mockups, impl-ref tables, and acceptance criteria.


Component decomposition

Implement as six separate Svelte 5 components (never merge into a monolith):

Component Responsibility
DocumentTopBar.svelte Orchestrator. Owns overflowOpen: $state(false). Passes props down.
PersonChipRow.svelte Chip row with arrow. hidden xs:flex. Receives sender, receivers, abbreviated.
PersonChip.svelte Single chip — avatar initials + name. abbreviated: boolean prop.
OverflowPill.svelte <button> at ≥768px with tooltip; <span aria-hidden> at <768px.
DocumentStatusChip.svelte Dot-only status indicator. Hidden below 768px.
AnnotateHintStrip.svelte 18px strip. Only when annotateMode===true AND ≥768px.

Utility module

Create src/lib/utils/personFormat.ts with these pure functions — write Vitest unit tests before implementing each:

  • abbreviateName(person) — "Karl Raddatz" → "K. Raddatz"; single name → as-is; hyphenated last name preserved
  • formatXsMeta(doc) — "K.Raddatz → E.Raddatz +2 · 24.12.1943"
  • personAvatarColor(personId) — deterministic hash → one of ['#012851','#5A3080','#007596','#2A6040','#803020']. Use PALETTE[hash(id) % 5] — never interpolate ID into CSS.
  • formatDate(isoDate, 'short'|'long') — parse with T12:00:00 suffix to avoid UTC off-by-one
  • statusDotClass(status) — Tailwind bg class per DocumentStatus
  • statusLabel(status) — German label string (for title + aria-label only)

Responsive behaviour (authoritative — overrides B1 spec)

Element XS <375px Mobile 375–767px Tablet/Desktop ≥768px
Topbar height h-12 (48px) h-14 (56px) h-14 (56px)
Title font text-[11px] text-[11px] text-[12px] / lg:text-[13px]
Chip row Hidden — xsMetaLine Abbreviated names Full names (+location at ≥1024px)
Chip name text text-[9px] text-[9px]
Status indicator Hidden Hidden Dot only (no text label)
Edit button Icon only Icon only Icon + "Bearbeiten"
Annotate button Hidden Hidden Shown — "Annotieren" / "Beenden"
Download button Hidden Hidden Icon only
Overflow pill — (chips hidden) <span aria-hidden> "+N" <button> "+N weitere" → tooltip
Hint strip Hidden Hidden 18px strip when annotateMode

Back button touch target: visual icon 24–28px, wrapper w-11 h-11 -ml-2 (44×44px). Always <a>, never <button>.

Date format: <span class="lg:hidden">24.12.1943</span><span class="hidden lg:inline">{longDate}</span> — no JS viewport check.


Design tokens

All colours via CSS custom properties — no hardcoded hex in components.

Spec token Tailwind class Notes
bg-surface bg-surface Auto light/dark
border-line border-line Bottom border, chip borders, dividers
bg-primary / accent bar border-l-[3px] border-primary Always present, all breakpoints
bg-muted bg-muted Chip bg, back btn bg at XS
text-ink text-ink Title, chip names
text-ink-2 text-ink-2 Date, meta text

--color-primary must be RGB channels (1 40 81), not hex, for Tailwind opacity modifiers (/5, /20) to work in the hint strip. If conversion is risky, use explicit bg-[rgba(1,40,81,0.05)] instead.


Annotate mode

When annotateMode === true:

  1. Edit and Download buttons are hidden
  2. Annotate button fills primary bg, label changes to "Beenden", aria-pressed=true
  3. Hint strip appears below main row (≥768px only — use {#if annotateMode}, no CSS animation)

Hint strip: hidden md:flex h-[18px] items-center gap-2 border-t border-dashed px-3.5 bg-[rgba(1,40,81,0.05)] border-[rgba(1,40,81,0.20)]


Overflow tooltip (≥768px only)

  • Panel: absolute top-full left-0 mt-1 bg-surface border border-line rounded-md shadow-lg p-3 min-w-[160px] z-50
  • role="listbox" on panel; role="option" on each person row (link to /persons/{id})
  • Header: "Weitere Empfänger" in text-[9px] font-bold uppercase tracking-wide text-ink-2
  • Focus flow: opening moves focus to first role="option"; Tab/Shift+Tab navigates; Escape closes + returns focus to pill
  • Click-outside: use use:clickOutside Svelte action — not document.addEventListener in $effect
  • aria-haspopup="listbox" (not "true"), aria-expanded={overflowOpen}

Tailwind config

Add custom breakpoint to tailwind.config.ts:

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

⚠ Tailwind 4 may use different syntax — verify before using xs: prefix classes.


Accessibility

  • Back link: aria-label="Zurück zur Dokumentenliste" always
  • Edit button: aria-label="Bearbeiten" always (even icon-only)
  • Annotate button: aria-pressed={annotateMode}
  • Overflow pill: aria-haspopup="listbox", aria-expanded, aria-label="{count} weitere Empfänger anzeigen"
  • Status dot: title={statusLabel} + aria-label={statusLabel}
  • Arrow : aria-hidden="true" always
  • All interactive elements: focus-visible:ring-2 focus-visible:ring-primary — never outline:none without replacement

Out of scope

  • Actual annotation functionality (handled by AnnotationSidePanel)
  • Changes to AnnotationSidePanel or DocumentBottomPanel
  • doc.location field if not yet on backend DTO — leave // TODO: show location when available

Acceptance criteria

  • Renders correctly at 320px, 375px, 768px, 1024px, 1440px — matches spec
  • Light and dark themes match token table — no hardcoded hex values
  • 0, 1, 2, 3, 5 receiver cases all correct; no-sender case: no arrow
  • Overflow tooltip opens/closes on click, Enter/Space, Escape; focus moves into tooltip on open
  • Overflow tooltip links navigate to correct /persons/{id}
  • Overflow pill at <768px is a non-interactive <span> — no tooltip
  • Annotate mode: Edit + Download hidden; hint strip visible at ≥768px; hidden below
  • Status dot visible at ≥768px, correct colour per status, hidden below
  • All touch targets ≥44×44px on mobile
  • aria-pressed, aria-haspopup="listbox", aria-expanded, aria-label on all interactive elements
  • svelte-check passes with no new type errors
  • Unit tests pass for all 6 utility functions (all edge cases per spec Section 6)
  • Visual proof: /proofshot at all 5 viewport widths × 2 themes = 10 screenshots

Reference

Final spec (authoritative): docs/specs/document-topbar-final-spec.html — open in browser
Visual reference: docs/specs/document-topbar-b1-responsive.html
Existing component: src/lib/components/DocumentTopBar.svelte
Consuming page: src/routes/documents/[id]/+page.svelte
Review thread: #161

## Context Implement the `DocumentTopBar` component per the **final implementation spec** at `docs/specs/document-topbar-final-spec.html`. This spec supersedes `document-topbar-b1-responsive.html` and incorporates all resolutions from the issue #161 team review. Open the spec in a browser before starting — it contains visual mockups, impl-ref tables, and acceptance criteria. --- ## Component decomposition Implement as six separate Svelte 5 components (never merge into a monolith): | Component | Responsibility | |---|---| | `DocumentTopBar.svelte` | Orchestrator. Owns `overflowOpen: $state(false)`. Passes props down. | | `PersonChipRow.svelte` | Chip row with arrow. `hidden xs:flex`. Receives `sender`, `receivers`, `abbreviated`. | | `PersonChip.svelte` | Single chip — avatar initials + name. `abbreviated: boolean` prop. | | `OverflowPill.svelte` | `<button>` at ≥768px with tooltip; `<span aria-hidden>` at <768px. | | `DocumentStatusChip.svelte` | Dot-only status indicator. Hidden below 768px. | | `AnnotateHintStrip.svelte` | 18px strip. Only when `annotateMode===true` AND ≥768px. | --- ## Utility module Create `src/lib/utils/personFormat.ts` with these pure functions — **write Vitest unit tests before implementing each**: - `abbreviateName(person)` — "Karl Raddatz" → "K. Raddatz"; single name → as-is; hyphenated last name preserved - `formatXsMeta(doc)` — "K.Raddatz → E.Raddatz +2 · 24.12.1943" - `personAvatarColor(personId)` — deterministic hash → one of `['#012851','#5A3080','#007596','#2A6040','#803020']`. Use `PALETTE[hash(id) % 5]` — never interpolate ID into CSS. - `formatDate(isoDate, 'short'|'long')` — parse with `T12:00:00` suffix to avoid UTC off-by-one - `statusDotClass(status)` — Tailwind bg class per `DocumentStatus` - `statusLabel(status)` — German label string (for `title` + `aria-label` only) --- ## Responsive behaviour (authoritative — overrides B1 spec) | Element | XS <375px | Mobile 375–767px | Tablet/Desktop ≥768px | |---|---|---|---| | Topbar height | `h-12` (48px) | `h-14` (56px) | `h-14` (56px) | | Title font | `text-[11px]` | `text-[11px]` | `text-[12px]` / `lg:text-[13px]` | | Chip row | Hidden — xsMetaLine | Abbreviated names | Full names (+location at ≥1024px) | | Chip name text | — | `text-[9px]` | `text-[9px]` | | Status indicator | Hidden | Hidden | Dot only (no text label) | | Edit button | Icon only | Icon only | Icon + "Bearbeiten" | | Annotate button | Hidden | Hidden | Shown — "Annotieren" / "Beenden" | | Download button | Hidden | Hidden | Icon only | | Overflow pill | — (chips hidden) | `<span aria-hidden>` "+N" | `<button>` "+N weitere" → tooltip | | Hint strip | Hidden | Hidden | 18px strip when `annotateMode` | **Back button touch target:** visual icon 24–28px, wrapper `w-11 h-11 -ml-2` (44×44px). Always `<a>`, never `<button>`. **Date format:** `<span class="lg:hidden">24.12.1943</span><span class="hidden lg:inline">{longDate}</span>` — no JS viewport check. --- ## Design tokens All colours via CSS custom properties — no hardcoded hex in components. | Spec token | Tailwind class | Notes | |---|---|---| | `bg-surface` | `bg-surface` | Auto light/dark | | `border-line` | `border-line` | Bottom border, chip borders, dividers | | `bg-primary` / accent bar | `border-l-[3px] border-primary` | Always present, all breakpoints | | `bg-muted` | `bg-muted` | Chip bg, back btn bg at XS | | `text-ink` | `text-ink` | Title, chip names | | `text-ink-2` | `text-ink-2` | Date, meta text | ⚠ **`--color-primary` must be RGB channels** (`1 40 81`), not hex, for Tailwind opacity modifiers (`/5`, `/20`) to work in the hint strip. If conversion is risky, use explicit `bg-[rgba(1,40,81,0.05)]` instead. --- ## Annotate mode When `annotateMode === true`: 1. Edit and Download buttons are hidden 2. Annotate button fills primary bg, label changes to "Beenden", `aria-pressed=true` 3. Hint strip appears below main row (≥768px only — use `{#if annotateMode}`, no CSS animation) Hint strip: `hidden md:flex h-[18px] items-center gap-2 border-t border-dashed px-3.5 bg-[rgba(1,40,81,0.05)] border-[rgba(1,40,81,0.20)]` --- ## Overflow tooltip (≥768px only) - Panel: `absolute top-full left-0 mt-1 bg-surface border border-line rounded-md shadow-lg p-3 min-w-[160px] z-50` - `role="listbox"` on panel; `role="option"` on each person row (link to `/persons/{id}`) - Header: "Weitere Empfänger" in `text-[9px] font-bold uppercase tracking-wide text-ink-2` - **Focus flow:** opening moves focus to first `role="option"`; Tab/Shift+Tab navigates; Escape closes + returns focus to pill - **Click-outside:** use `use:clickOutside` Svelte action — not `document.addEventListener` in `$effect` - `aria-haspopup="listbox"` (not `"true"`), `aria-expanded={overflowOpen}` --- ## Tailwind config Add custom breakpoint to `tailwind.config.ts`: ```typescript theme: { extend: { screens: { xs: '375px', } } } ``` ⚠ Tailwind 4 may use different syntax — verify before using `xs:` prefix classes. --- ## Accessibility - Back link: `aria-label="Zurück zur Dokumentenliste"` always - Edit button: `aria-label="Bearbeiten"` always (even icon-only) - Annotate button: `aria-pressed={annotateMode}` - Overflow pill: `aria-haspopup="listbox"`, `aria-expanded`, `aria-label="{count} weitere Empfänger anzeigen"` - Status dot: `title={statusLabel}` + `aria-label={statusLabel}` - Arrow `→`: `aria-hidden="true"` always - All interactive elements: `focus-visible:ring-2 focus-visible:ring-primary` — never `outline:none` without replacement --- ## Out of scope - Actual annotation functionality (handled by `AnnotationSidePanel`) - Changes to `AnnotationSidePanel` or `DocumentBottomPanel` - `doc.location` field if not yet on backend DTO — leave `// TODO: show location when available` --- ## Acceptance criteria - [ ] Renders correctly at 320px, 375px, 768px, 1024px, 1440px — matches spec - [ ] Light and dark themes match token table — no hardcoded hex values - [ ] 0, 1, 2, 3, 5 receiver cases all correct; no-sender case: no arrow - [ ] Overflow tooltip opens/closes on click, Enter/Space, Escape; focus moves into tooltip on open - [ ] Overflow tooltip links navigate to correct `/persons/{id}` - [ ] Overflow pill at <768px is a non-interactive `<span>` — no tooltip - [ ] Annotate mode: Edit + Download hidden; hint strip visible at ≥768px; hidden below - [ ] Status dot visible at ≥768px, correct colour per status, hidden below - [ ] All touch targets ≥44×44px on mobile - [ ] `aria-pressed`, `aria-haspopup="listbox"`, `aria-expanded`, `aria-label` on all interactive elements - [ ] `svelte-check` passes with no new type errors - [ ] Unit tests pass for all 6 utility functions (all edge cases per spec Section 6) - [ ] Visual proof: `/proofshot` at all 5 viewport widths × 2 themes = 10 screenshots --- ## Reference **Final spec (authoritative):** `docs/specs/document-topbar-final-spec.html` — open in browser **Visual reference:** `docs/specs/document-topbar-b1-responsive.html` **Existing component:** `src/lib/components/DocumentTopBar.svelte` **Consuming page:** `src/routes/documents/[id]/+page.svelte` **Review thread:** #161
marcel added the featureui labels 2026-03-31 20:23:16 +02:00
Author
Owner

👨‍💻 Felix Brandt — Senior Fullstack Developer

Questions & Observations

  • TDD scope is partially specified. The issue explicitly calls for Vitest unit tests before implementing each of the 6 utility functions — good. But what about component-level tests? PersonChip, PersonChipRow, OverflowPill all have conditional rendering logic (abbreviated names, span-vs-button, aria attributes) that deserves unit test coverage beyond the utility layer. Is the expectation that the Vitest utility tests + the 10 proofshots cover this, or should there be component tests as well?

  • OverflowPill is doing two visually distinct things. At ≥768px it's a <button> with a tooltip; below that it's a <span aria-hidden>. The spec says these live in one component. That means OverflowPill contains layout-conditional rendering logic and interaction logic — two responsibilities. Consider whether OverflowPillButton and OverflowPillDisplay should be separate, with PersonChipRow conditionally rendering the right one. I'd ask Leonie whether the spec intent was "one file for co-location" or "one component doing both jobs."

  • The clickOutside Svelte action — is this action already in the project, or does it need to be created? If it needs to be written, its implementation needs its own unit test first (red/green applies to actions too). Using document.addEventListener inside a Svelte action is fine — that's exactly the pattern actions are designed for — but the teardown in the destroy callback must be tested.

  • personAvatarColor hash function — "deterministic hash → one of 5 palette entries" is under-specified. What hash algorithm? charCodeAt sum mod 5 will produce a skewed distribution for UUIDs (which start with the same hex groups). A simple djb2 or FNV-1a over the full ID string would distribute more evenly. The test suite should cover: same ID always returns same color, all 5 colors are reachable.

  • abbreviated: boolean on PersonChip — this is a prop, not a method argument, so the boolean-flag rule from the style guide doesn't apply. No concern there.

  • Keyed {#each} blocks — the receivers list in PersonChipRow must key by receiver.id. If any implementation iterates receivers without a key expression, that's a rule violation.

Suggestions

  • Write the clickOutside action test first, then implement it, before starting OverflowPill. It unblocks the tooltip work and keeps TDD clean.
  • For formatXsMeta, write tests for: no sender + 0 receivers, sender + 0 receivers, sender + 1 receiver, sender + 5 receivers — the edge cases that affect the arrow character and the +N count.
  • The $derived rune should be used for any computed value in DocumentTopBar (e.g. overflow count, abbreviated names). Flag any $state + $effect pattern that computes a value — that's a rule violation regardless of how small the component is.
## 👨‍💻 Felix Brandt — Senior Fullstack Developer ### Questions & Observations - **TDD scope is partially specified.** The issue explicitly calls for Vitest unit tests before implementing each of the 6 utility functions — good. But what about component-level tests? `PersonChip`, `PersonChipRow`, `OverflowPill` all have conditional rendering logic (abbreviated names, span-vs-button, aria attributes) that deserves unit test coverage beyond the utility layer. Is the expectation that the Vitest utility tests + the 10 proofshots cover this, or should there be component tests as well? - **`OverflowPill` is doing two visually distinct things.** At ≥768px it's a `<button>` with a tooltip; below that it's a `<span aria-hidden>`. The spec says these live in one component. That means `OverflowPill` contains layout-conditional rendering logic *and* interaction logic — two responsibilities. Consider whether `OverflowPillButton` and `OverflowPillDisplay` should be separate, with `PersonChipRow` conditionally rendering the right one. I'd ask Leonie whether the spec intent was "one file for co-location" or "one component doing both jobs." - **The `clickOutside` Svelte action** — is this action already in the project, or does it need to be created? If it needs to be written, its implementation needs its own unit test first (red/green applies to actions too). Using `document.addEventListener` inside a Svelte action is fine — that's exactly the pattern actions are designed for — but the teardown in the `destroy` callback must be tested. - **`personAvatarColor` hash function** — "deterministic hash → one of 5 palette entries" is under-specified. What hash algorithm? `charCodeAt` sum mod 5 will produce a skewed distribution for UUIDs (which start with the same hex groups). A simple djb2 or FNV-1a over the full ID string would distribute more evenly. The test suite should cover: same ID always returns same color, all 5 colors are reachable. - **`abbreviated: boolean` on `PersonChip`** — this is a prop, not a method argument, so the boolean-flag rule from the style guide doesn't apply. No concern there. - **Keyed `{#each}` blocks** — the receivers list in `PersonChipRow` must key by `receiver.id`. If any implementation iterates receivers without a key expression, that's a rule violation. ### Suggestions - Write the `clickOutside` action test first, then implement it, before starting `OverflowPill`. It unblocks the tooltip work and keeps TDD clean. - For `formatXsMeta`, write tests for: no sender + 0 receivers, sender + 0 receivers, sender + 1 receiver, sender + 5 receivers — the edge cases that affect the arrow character and the `+N` count. - The `$derived` rune should be used for any computed value in `DocumentTopBar` (e.g. overflow count, abbreviated names). Flag any `$state` + `$effect` pattern that computes a value — that's a rule violation regardless of how small the component is.
Author
Owner

🏛️ Markus Keller — Application Architect

Questions & Observations

  • Where do the design tokens live? The spec references bg-surface, border-line, text-ink, text-ink-2, bg-muted as Tailwind classes backed by CSS custom properties. Are these already defined in layout.css (the existing brand token file), or does this issue introduce them? If they're new tokens, that's a non-trivial CSS change that touches the entire app's theme layer — the scope should be explicit, and the tokens should be added in a separate, reviewable commit.

  • --color-primary as RGB channels is a design token contract with the Tailwind config. The spec correctly flags this (1 40 81 not #002850), but it needs to be established before any component tries to use /5 or /20 opacity modifiers. Is the existing brand-navy token already in RGB channel format, or is this a migration?

  • The xs: '375px' breakpoint in tailwind.config.ts — Tailwind 4 moved away from tailwind.config.ts in favour of @theme directives in CSS. The spec flags this uncertainty ("⚠ Tailwind 4 may use different syntax — verify before using xs: prefix classes"). Before implementing anything that depends on xs:flex or hidden xs:flex, this config question must be resolved. A component that conditionally hides on xs: will silently fail if the breakpoint is never registered.

  • role="listbox" / role="option" on the overflow tooltip — this ARIA pattern implies a selection widget, where one option is "selected" and the widget communicates a chosen value. The overflow tooltip is really a navigation list (links to person pages), not a listbox. role="menu" / role="menuitem" (or simply role="list" / role="listitem" with links) would be semantically more accurate. This is worth checking against the ARIA spec before implementation, as it affects the expected keyboard interaction model.

  • use:clickOutside action — if this doesn't already exist in src/lib/actions/, it needs to be created as part of this issue. Its placement should be src/lib/actions/clickOutside.ts to stay consistent with how other shared utilities are organized. It should not live inside the component file itself.

  • Module boundary is clean. The utility module src/lib/utils/personFormat.ts is a pure-function module with no component dependencies — correct layering. The six components pass data via props only. No concerns about domain coupling here.

Suggestions

  • Resolve the Tailwind 4 xs breakpoint question before writing a single line of component code. A quick npm run build with an xs:flex class present will confirm whether it's registered and purged correctly.
  • If bg-surface, text-ink etc. are new tokens: introduce them in a dedicated "add dark-mode design tokens" commit that is separate from the component work. That makes the token surface area reviewable independently.
  • Confirm role="listbox" intent with accessibility sources — if it should be role="menu", the keyboard interaction model (arrow keys vs Tab) changes, and that affects focus management implementation.
## 🏛️ Markus Keller — Application Architect ### Questions & Observations - **Where do the design tokens live?** The spec references `bg-surface`, `border-line`, `text-ink`, `text-ink-2`, `bg-muted` as Tailwind classes backed by CSS custom properties. Are these already defined in `layout.css` (the existing brand token file), or does this issue introduce them? If they're new tokens, that's a non-trivial CSS change that touches the entire app's theme layer — the scope should be explicit, and the tokens should be added in a separate, reviewable commit. - **`--color-primary` as RGB channels** is a design token contract with the Tailwind config. The spec correctly flags this (`1 40 81` not `#002850`), but it needs to be established before any component tries to use `/5` or `/20` opacity modifiers. Is the existing `brand-navy` token already in RGB channel format, or is this a migration? - **The `xs: '375px'` breakpoint in `tailwind.config.ts`** — Tailwind 4 moved away from `tailwind.config.ts` in favour of `@theme` directives in CSS. The spec flags this uncertainty ("⚠ Tailwind 4 may use different syntax — verify before using `xs:` prefix classes"). Before implementing anything that depends on `xs:flex` or `hidden xs:flex`, this config question must be resolved. A component that conditionally hides on `xs:` will silently fail if the breakpoint is never registered. - **`role="listbox"` / `role="option"` on the overflow tooltip** — this ARIA pattern implies a selection widget, where one option is "selected" and the widget communicates a chosen value. The overflow tooltip is really a navigation list (links to person pages), not a listbox. `role="menu"` / `role="menuitem"` (or simply `role="list"` / `role="listitem"` with links) would be semantically more accurate. This is worth checking against the ARIA spec before implementation, as it affects the expected keyboard interaction model. - **`use:clickOutside` action** — if this doesn't already exist in `src/lib/actions/`, it needs to be created as part of this issue. Its placement should be `src/lib/actions/clickOutside.ts` to stay consistent with how other shared utilities are organized. It should not live inside the component file itself. - **Module boundary is clean.** The utility module `src/lib/utils/personFormat.ts` is a pure-function module with no component dependencies — correct layering. The six components pass data via props only. No concerns about domain coupling here. ### Suggestions - Resolve the Tailwind 4 `xs` breakpoint question before writing a single line of component code. A quick `npm run build` with an `xs:flex` class present will confirm whether it's registered and purged correctly. - If `bg-surface`, `text-ink` etc. are new tokens: introduce them in a dedicated "add dark-mode design tokens" commit that is separate from the component work. That makes the token surface area reviewable independently. - Confirm `role="listbox"` intent with accessibility sources — if it should be `role="menu"`, the keyboard interaction model (arrow keys vs Tab) changes, and that affects focus management implementation.
Author
Owner

🧪 Sara Holt — QA Engineer

Questions & Observations

Unit tests (specified):
The issue specifies unit tests for all 6 utility functions. The acceptance criteria mention "all edge cases per spec Section 6." I want to make sure the following cases are explicitly covered before the PR lands:

Function Edge cases to verify
abbreviateName Single name (no space), hyphenated last name ("Karl-Heinz Raddatz"), empty string, name with trailing/leading spaces
formatXsMeta No sender (no arrow rendered), 0 receivers, exactly 1 receiver (no +N), 2+ receivers (+N count correct), no date
personAvatarColor Same ID → same color (determinism), all 5 palette indexes are reachable, UUID with repeated hex groups
formatDate T12:00:00 suffix prevents off-by-one at UTC boundary — test at Dec 31, Jan 1 boundary dates
statusDotClass All DocumentStatus enum values covered — no unhandled case
statusLabel All DocumentStatus enum values covered, returns German string

The acceptance criteria list receiver cases as "0, 1, 2, 3, 5" — 4 receivers is not listed. Was that intentional? If 4 is not a special case, it should still be tested to verify the +N arithmetic is correct.

Component-level tests (not specified):
The issue only mentions utility unit tests. These component behaviors are not covered by utility tests alone:

  • OverflowPill renders as <span aria-hidden> below 768px and <button> above — this conditional rendering should have a Vitest component test.
  • DocumentStatusChip hidden below 768px — testing this with CSS visibility requires either an E2E test or a component test that checks rendered class names.
  • AnnotateHintStrip only rendered when annotateMode === true — a {#if} block: straightforward component test.

Acceptance criteria gap:

  • Criterion "All touch targets ≥44×44px on mobile" — how is this verified? Visual screenshots can't reliably confirm this. A Playwright assertion on getBoundingClientRect() for the back button and edit button would make this deterministic.
  • The 10 proofshots (5 viewports × 2 themes) are visual artifacts. Who reviews them? Is there a visual regression baseline stored in the repo, or is this a manual sign-off?

svelte-check passes is listed as an acceptance criterion — good, this is a CI gate. But it should already be passing before the PR opens, not just "passes at some point."

Suggestions

  • Add a test spec file src/lib/utils/personFormat.test.ts as the first commit, with all test skeletons in place (failing) before any implementation. This makes the TDD intent visible in the commit graph.
  • Consider adding @testing-library/svelte component tests for OverflowPill and AnnotateHintStrip — they have branching render logic that's hard to cover with screenshots alone.
  • For the focus management spec ("opening moves focus to first role=\"option\"") — this is a Playwright E2E test, not a unit test. Should it be added to the existing document detail E2E suite?
## 🧪 Sara Holt — QA Engineer ### Questions & Observations **Unit tests (specified):** The issue specifies unit tests for all 6 utility functions. The acceptance criteria mention "all edge cases per spec Section 6." I want to make sure the following cases are explicitly covered before the PR lands: | Function | Edge cases to verify | |---|---| | `abbreviateName` | Single name (no space), hyphenated last name (`"Karl-Heinz Raddatz"`), empty string, name with trailing/leading spaces | | `formatXsMeta` | No sender (no arrow rendered), 0 receivers, exactly 1 receiver (no `+N`), 2+ receivers (+N count correct), no date | | `personAvatarColor` | Same ID → same color (determinism), all 5 palette indexes are reachable, UUID with repeated hex groups | | `formatDate` | `T12:00:00` suffix prevents off-by-one at UTC boundary — test at Dec 31, Jan 1 boundary dates | | `statusDotClass` | All `DocumentStatus` enum values covered — no unhandled case | | `statusLabel` | All `DocumentStatus` enum values covered, returns German string | The acceptance criteria list receiver cases as "0, 1, 2, 3, 5" — 4 receivers is not listed. Was that intentional? If 4 is not a special case, it should still be tested to verify the `+N` arithmetic is correct. **Component-level tests (not specified):** The issue only mentions utility unit tests. These component behaviors are not covered by utility tests alone: - `OverflowPill` renders as `<span aria-hidden>` below 768px and `<button>` above — this conditional rendering should have a Vitest component test. - `DocumentStatusChip` hidden below 768px — testing this with CSS visibility requires either an E2E test or a component test that checks rendered class names. - `AnnotateHintStrip` only rendered when `annotateMode === true` — a `{#if}` block: straightforward component test. **Acceptance criteria gap:** - Criterion "All touch targets ≥44×44px on mobile" — how is this verified? Visual screenshots can't reliably confirm this. A Playwright assertion on `getBoundingClientRect()` for the back button and edit button would make this deterministic. - The 10 proofshots (5 viewports × 2 themes) are visual artifacts. Who reviews them? Is there a visual regression baseline stored in the repo, or is this a manual sign-off? **`svelte-check` passes** is listed as an acceptance criterion — good, this is a CI gate. But it should already be passing before the PR opens, not just "passes at some point." ### Suggestions - Add a test spec file `src/lib/utils/personFormat.test.ts` as the first commit, with all test skeletons in place (failing) before any implementation. This makes the TDD intent visible in the commit graph. - Consider adding `@testing-library/svelte` component tests for `OverflowPill` and `AnnotateHintStrip` — they have branching render logic that's hard to cover with screenshots alone. - For the focus management spec ("opening moves focus to first `role=\"option\"`") — this is a Playwright E2E test, not a unit test. Should it be added to the existing document detail E2E suite?
Author
Owner

🔒 Nora "NullX" Steiner — Security Engineer

Questions & Observations

Overall, this is a pure frontend rendering component with no new backend surface — the risk profile is low. That said, a few things are worth flagging:

personAvatarColor — CSS injection guard (correctly specified, verify in implementation)
The spec explicitly states: "never interpolate ID into CSS" and mandates PALETTE[hash(id) % 5] — returning a hex string from a static array, not inserting the ID into a CSS string. This is the right pattern. In Svelte, style="background: {color}" binds a value from a trusted palette array, not user input — no XSS vector here as long as the implementation follows the spec. Worth a quick implementation review to confirm no one sneaks in style="background: {person.id}" by accident.

Overflow tooltip: person name rendering
The tooltip renders person.name as link text inside the panel. Svelte's template compiler auto-escapes text bindings ({person.name}), so stored XSS is not a concern here — as long as no {@html} is used anywhere in the overflow panel. Flag during PR review: search for {@html} in all 6 new component files.

clickOutside action implementation
The spec mandates use:clickOutside rather than document.addEventListener directly in $effect. This is the safer pattern — the action teardown is guaranteed by Svelte's lifecycle, preventing event listener leaks. The main risk in a custom clickOutside implementation is checking contains() incorrectly, which can cause the tooltip to close when clicking inside it (UX bug, not a security bug). Not a vulnerability, but worth a careful implementation.

/persons/{id} links in the tooltip
These links use person UUIDs from the backend DTO. UUIDs are safe in URL paths — no injection vector. Svelte's href binding handles this safely.

Focus management and keyboard trap risk
The spec describes focus moving into the tooltip on open, with Tab/Shift+Tab navigating within it, and Escape returning focus to the pill. This is correct — it's not a focus trap (Tab can exit). However: if the implementation accidentally creates a true focus trap (Tab loops indefinitely inside the tooltip), it would be an accessibility failure, not a security issue. Verify the Tab-out behavior in testing.

No new backend endpoints, no auth changes, no data exposure — this issue does not expand the app's attack surface.

Suggestions

  • Add a PR checklist item: grep for {@html} in all 6 new .svelte files — confirm zero occurrences.
  • Add a PR checklist item: verify personAvatarColor returns from a static palette array, not a constructed CSS string.
  • The clickOutside action deserves a code review eye specifically on the event.target check — confirm it uses node.contains(event.target) and handles shadow DOM or portal cases if any exist in the app.
## 🔒 Nora "NullX" Steiner — Security Engineer ### Questions & Observations Overall, this is a pure frontend rendering component with no new backend surface — the risk profile is low. That said, a few things are worth flagging: **`personAvatarColor` — CSS injection guard (correctly specified, verify in implementation)** The spec explicitly states: _"never interpolate ID into CSS"_ and mandates `PALETTE[hash(id) % 5]` — returning a hex string from a static array, not inserting the ID into a CSS string. This is the right pattern. In Svelte, `style="background: {color}"` binds a value from a trusted palette array, not user input — no XSS vector here as long as the implementation follows the spec. Worth a quick implementation review to confirm no one sneaks in `style="background: {person.id}"` by accident. **Overflow tooltip: person name rendering** The tooltip renders `person.name` as link text inside the panel. Svelte's template compiler auto-escapes text bindings (`{person.name}`), so stored XSS is not a concern here — as long as no `{@html}` is used anywhere in the overflow panel. Flag during PR review: search for `{@html}` in all 6 new component files. **`clickOutside` action implementation** The spec mandates `use:clickOutside` rather than `document.addEventListener` directly in `$effect`. This is the safer pattern — the action teardown is guaranteed by Svelte's lifecycle, preventing event listener leaks. The main risk in a custom `clickOutside` implementation is checking `contains()` incorrectly, which can cause the tooltip to close when clicking inside it (UX bug, not a security bug). Not a vulnerability, but worth a careful implementation. **`/persons/{id}` links in the tooltip** These links use person UUIDs from the backend DTO. UUIDs are safe in URL paths — no injection vector. Svelte's `href` binding handles this safely. **Focus management and keyboard trap risk** The spec describes focus moving _into_ the tooltip on open, with Tab/Shift+Tab navigating within it, and Escape returning focus to the pill. This is correct — it's not a focus trap (Tab can exit). However: if the implementation accidentally creates a true focus trap (Tab loops indefinitely inside the tooltip), it would be an accessibility failure, not a security issue. Verify the Tab-out behavior in testing. **No new backend endpoints, no auth changes, no data exposure** — this issue does not expand the app's attack surface. ### Suggestions - Add a PR checklist item: grep for `{@html}` in all 6 new `.svelte` files — confirm zero occurrences. - Add a PR checklist item: verify `personAvatarColor` returns from a static palette array, not a constructed CSS string. - The `clickOutside` action deserves a code review eye specifically on the `event.target` check — confirm it uses `node.contains(event.target)` and handles shadow DOM or portal cases if any exist in the app.
Author
Owner

🎨 Leonie Voss — UI/UX Designer

Questions & Observations

Font sizes below 12px minimum — needs resolution before implementation

The spec specifies:

  • text-[9px] for chip name text at all breakpoints ≥375px
  • text-[11px] for title font at XS and Mobile

Both values are below my 12px floor. At text-[9px] chip names will be illegible for older users on any display, and at non-retina resolution this will render as blurry subpixel text. I understand the space constraint is real — the topbar is only 48–56px tall — but I need to know: was this a deliberate trade-off in the final spec, or is it a scaled-down mockup value that accidentally ended up in the implementation reference table?

If it was intentional: document the rationale (e.g. "chip names are decorative/supplemental, the primary content is the title"). If it was accidental: the minimum acceptable chip name size is text-[10px]/text-xs (10–12px), and the title should be at least text-xs (12px) at XS.

hidden xs:flex and the XS chip row

At <375px (XS), the chip row is hidden and replaced with xsMetaLine (the formatXsMeta output). What is the visual treatment of xsMetaLine? The spec defines the utility function but I don't see where the resulting string is rendered or styled in the component breakdown. Is it a secondary line below the title? What's its font size and color? This rendering path needs to be in the spec or implementation notes.

Status dot — "dot only, no text label"

At ≥768px the status indicator shows a colored dot with no visible label — only title and aria-label. For users who are unfamiliar with the color coding, this is an undiscoverable UI element. Color alone is not sufficient per WCAG 1.4.1. The dot-only approach is acceptable if and only if it's explicitly supplemental (users can discover the status elsewhere, e.g. on the document detail page). Can you confirm this constraint is met by the surrounding page layout?

Annotate hint strip at 18px height

h-[18px] is extremely compact. At 56px topbar + 18px strip = 74px total header height on tablet — this is tight but workable. My concern is the strip content: gap-2 with icon and text at what font size? The spec shows the class string but not the text size inside the strip. Anything below text-[10px] in an 18px strip will be unreadable.

Touch targets

Back button at w-11 h-11 (44×44px) — correct. Edit button at icon-only on mobile — what is the actual touch target size? The spec mentions icon-only rendering but doesn't specify wrapper dimensions. The visual icon may be 24px but the tap target must be 44×44px minimum.

focus-visible:ring-2 focus-visible:ring-primary on all interactive elements

This is the right pattern. Confirm during implementation that ring-primary resolves correctly — if --color-primary is in RGB channel format (1 40 81), then ring-primary needs to be declared as a Tailwind color utility in the config, not just a CSS variable. Verify this before review.

Suggestions

  • Clarify in the spec or implementation notes: is text-[9px] for chips a final product decision, or a leftover scaled value?
  • Add a note to the spec for xsMetaLine rendering location, font size, and color class.
  • For the edit button on mobile: add min-w-[44px] min-h-[44px] (or equivalent) to the wrapper, not just the visible icon size.
## 🎨 Leonie Voss — UI/UX Designer ### Questions & Observations **Font sizes below 12px minimum — needs resolution before implementation** The spec specifies: - `text-[9px]` for chip name text at all breakpoints ≥375px - `text-[11px]` for title font at XS and Mobile Both values are below my 12px floor. At `text-[9px]` chip names will be illegible for older users on any display, and at non-retina resolution this will render as blurry subpixel text. I understand the space constraint is real — the topbar is only 48–56px tall — but I need to know: was this a deliberate trade-off in the final spec, or is it a scaled-down mockup value that accidentally ended up in the implementation reference table? If it was intentional: document the rationale (e.g. "chip names are decorative/supplemental, the primary content is the title"). If it was accidental: the minimum acceptable chip name size is `text-[10px]`/`text-xs` (10–12px), and the title should be at least `text-xs` (12px) at XS. **`hidden xs:flex` and the XS chip row** At <375px (XS), the chip row is hidden and replaced with `xsMetaLine` (the `formatXsMeta` output). What is the visual treatment of `xsMetaLine`? The spec defines the utility function but I don't see where the resulting string is rendered or styled in the component breakdown. Is it a secondary line below the title? What's its font size and color? This rendering path needs to be in the spec or implementation notes. **Status dot — "dot only, no text label"** At ≥768px the status indicator shows a colored dot with no visible label — only `title` and `aria-label`. For users who are unfamiliar with the color coding, this is an undiscoverable UI element. Color alone is not sufficient per WCAG 1.4.1. The dot-only approach is acceptable *if and only if* it's explicitly supplemental (users can discover the status elsewhere, e.g. on the document detail page). Can you confirm this constraint is met by the surrounding page layout? **Annotate hint strip at 18px height** `h-[18px]` is extremely compact. At 56px topbar + 18px strip = 74px total header height on tablet — this is tight but workable. My concern is the strip content: `gap-2` with icon and text at what font size? The spec shows the class string but not the text size inside the strip. Anything below `text-[10px]` in an 18px strip will be unreadable. **Touch targets** Back button at `w-11 h-11` (44×44px) — correct. Edit button at icon-only on mobile — what is the actual touch target size? The spec mentions icon-only rendering but doesn't specify wrapper dimensions. The visual icon may be 24px but the tap target must be 44×44px minimum. **`focus-visible:ring-2 focus-visible:ring-primary` on all interactive elements** This is the right pattern. Confirm during implementation that `ring-primary` resolves correctly — if `--color-primary` is in RGB channel format (`1 40 81`), then `ring-primary` needs to be declared as a Tailwind color utility in the config, not just a CSS variable. Verify this before review. ### Suggestions - Clarify in the spec or implementation notes: is `text-[9px]` for chips a final product decision, or a leftover scaled value? - Add a note to the spec for `xsMetaLine` rendering location, font size, and color class. - For the edit button on mobile: add `min-w-[44px] min-h-[44px]` (or equivalent) to the wrapper, not just the visible icon size.
Author
Owner

🛠️ Tobias Wendt — DevOps & Platform Engineer

Questions & Observations

This is a pure frontend component issue — no backend changes, no infrastructure changes. My concerns are limited but concrete:

Tailwind 4 custom breakpoint syntax

The spec correctly flags this as a risk: "⚠ Tailwind 4 may use different syntax — verify before using xs: prefix classes." In Tailwind 4, the tailwind.config.ts theme.extend.screens approach is replaced by @theme CSS directives:

/* Tailwind 4 — correct approach */
@theme {
  --breakpoint-xs: 375px;
}

If tailwind.config.ts doesn't exist in this project (Tailwind 4 dropped it), adding an xs breakpoint there will have no effect. The xs:flex and hidden xs:flex classes will be treated as unknown and silently stripped by the optimizer. The result: the chip row visibility logic breaks at runtime with no build error. This must be verified against the actual Tailwind version in package.json before any xs: class is used.

npm run build + svelte-check in CI

Both are already in the pipeline. The svelte-check AC criterion is covered. No new CI jobs are needed for this issue.

10 proofshots as acceptance artifacts

The /proofshot process produces screenshots at 5 viewports × 2 themes. These live in proofshot-artifacts/. They're not automated visual regression — they're manual sign-off artifacts. That's fine for a small team, but worth noting: if the topbar is modified again in a future issue, there's no baseline diff to catch regressions. Something to consider when the visual test story matures.

src/lib/utils/personFormat.ts as a new module

No infrastructure or build config changes required — Vite and SvelteKit pick this up automatically. The new src/lib/actions/clickOutside.ts (if created) is the same story.

No new environment variables, no new dependencies (likely)

Unless clickOutside is pulled from a package like svelte-legos or @svelte-put/clickoutside, no package.json changes are expected. If a dependency is added, it should be pinned to a specific version, not a range, given this is a production app.

Suggestions

  • Before any component code: run npx tailwindcss --version and check package.json to confirm Tailwind 4. If Tailwind 4: use @theme { --breakpoint-xs: 375px; } in the CSS entry point, not tailwind.config.ts.
  • If clickOutside is brought in as a package, pin the exact version and run npm audit after install.
  • No other DevOps concerns from my side on this issue.
## 🛠️ Tobias Wendt — DevOps & Platform Engineer ### Questions & Observations This is a pure frontend component issue — no backend changes, no infrastructure changes. My concerns are limited but concrete: **Tailwind 4 custom breakpoint syntax** The spec correctly flags this as a risk: _"⚠ Tailwind 4 may use different syntax — verify before using `xs:` prefix classes."_ In Tailwind 4, the `tailwind.config.ts` `theme.extend.screens` approach is replaced by `@theme` CSS directives: ```css /* Tailwind 4 — correct approach */ @theme { --breakpoint-xs: 375px; } ``` If `tailwind.config.ts` doesn't exist in this project (Tailwind 4 dropped it), adding an `xs` breakpoint there will have no effect. The `xs:flex` and `hidden xs:flex` classes will be treated as unknown and silently stripped by the optimizer. The result: the chip row visibility logic breaks at runtime with no build error. This must be verified against the actual Tailwind version in `package.json` before any `xs:` class is used. **`npm run build` + `svelte-check` in CI** Both are already in the pipeline. The `svelte-check` AC criterion is covered. No new CI jobs are needed for this issue. **10 proofshots as acceptance artifacts** The `/proofshot` process produces screenshots at 5 viewports × 2 themes. These live in `proofshot-artifacts/`. They're not automated visual regression — they're manual sign-off artifacts. That's fine for a small team, but worth noting: if the topbar is modified again in a future issue, there's no baseline diff to catch regressions. Something to consider when the visual test story matures. **`src/lib/utils/personFormat.ts` as a new module** No infrastructure or build config changes required — Vite and SvelteKit pick this up automatically. The new `src/lib/actions/clickOutside.ts` (if created) is the same story. **No new environment variables, no new dependencies (likely)** Unless `clickOutside` is pulled from a package like `svelte-legos` or `@svelte-put/clickoutside`, no `package.json` changes are expected. If a dependency is added, it should be pinned to a specific version, not a range, given this is a production app. ### Suggestions - Before any component code: run `npx tailwindcss --version` and check `package.json` to confirm Tailwind 4. If Tailwind 4: use `@theme { --breakpoint-xs: 375px; }` in the CSS entry point, not `tailwind.config.ts`. - If `clickOutside` is brought in as a package, pin the exact version and run `npm audit` after install. - No other DevOps concerns from my side on this issue.
Author
Owner

👨‍💻 Felix Brandt — Senior Fullstack Developer

Follow-up discussion resolving open implementation questions raised by the team review.


Resolved

  1. clickOutside action — extract to shared module

    • src/lib/actions/ does not exist yet; the action is currently copy-pasted inline into 5 components (PersonTypeahead, PersonMultiSelect, TagInput, UserMenu, CorrespondentSuggestionsDropdown)
    • Decision: Create src/lib/actions/clickOutside.ts, write Vitest test first (red/green), then replace all 5 inline copies with the shared import
    • This is in scope for this PR — 3+ duplications, clear name, independently testable
  2. personAvatarColor hash algorithm — djb2

    • Decision: djb2 over the full UUID string, Math.abs(hash) % 5
    • Test suite will assert: same ID always returns same color, all 5 palette indexes are reachable (500 random UUIDs)
  3. Tailwind 4 xs breakpoint — confirmed approach

    • Confirmed Tailwind 4.1.17 in package.json; tailwind.config.ts approach has no effect
    • --breakpoint-xs is not yet defined anywhere in the codebase; xs: classes are not used anywhere
    • Decision: Add --breakpoint-xs: 375px; to the existing @theme block in src/routes/layout.css — prerequisite before any xs:flex class is written
  4. OverflowPill split — two components

    • Decision: Split into OverflowPillButton (≥768px, interactive <button> with tooltip) and OverflowPillDisplay (<768px, non-interactive <span aria-hidden>); PersonChipRow conditionally renders the right one
    • Total component count is 7, not 6 — the spec decomposition table is superseded by this decision
  5. role="listbox"role="list" / role="listitem"

    • The overflow tooltip contains navigation links (/persons/{id}), not a selection widget — role="listbox" is semantically incorrect
    • Decision: role="list" on the panel, role="listitem" on each row, standard <a> links; aria-haspopup="true" on the pill button
    • Focus management spec unchanged: focus moves to first link on open, Tab/Shift+Tab navigates natively, Escape returns to pill
  6. xsMetaLine rendering — not rendered in topbar

    • The rendering location for formatXsMeta output is undocumented in the spec
    • The bottom drawer already exposes full metadata at XS — hiding the chip row without a replacement is not a loss
    • Decision: At <375px, chip row is hidden, no meta line rendered in the topbar. formatXsMeta is implemented and tested as a pure utility function but has no call site in this PR
  7. Design tokens — already defined, no new work needed

    • bg-surface, bg-muted, border-line, text-ink, text-ink-2, bg-primary, border-primary are all already defined in src/routes/layout.css via @theme inline
    • --color-primary stays as hex; hint strip uses explicit bg-[rgba(1,40,81,0.05)] and border-[rgba(1,40,81,0.20)] per spec fallback — no token format migration needed
  8. Component tests — in scope

    • Decision: @testing-library/svelte component tests for OverflowPillButton (button/tooltip conditional rendering) and AnnotateHintStrip ({#if annotateMode} branch); TDD applies — failing component test before implementation

Overall: the spec is implementable as written with these 8 clarifications. The main structural deviations from the spec are the OverflowPill split (item 4) and the ARIA role correction (item 5). Everything else is additive or confirmatory.

## 👨‍💻 Felix Brandt — Senior Fullstack Developer Follow-up discussion resolving open implementation questions raised by the team review. --- ### Resolved 1. **`clickOutside` action — extract to shared module** - `src/lib/actions/` does not exist yet; the action is currently copy-pasted inline into 5 components (`PersonTypeahead`, `PersonMultiSelect`, `TagInput`, `UserMenu`, `CorrespondentSuggestionsDropdown`) - **Decision:** Create `src/lib/actions/clickOutside.ts`, write Vitest test first (red/green), then replace all 5 inline copies with the shared import - This is in scope for this PR — 3+ duplications, clear name, independently testable 2. **`personAvatarColor` hash algorithm — djb2** - **Decision:** djb2 over the full UUID string, `Math.abs(hash) % 5` - Test suite will assert: same ID always returns same color, all 5 palette indexes are reachable (500 random UUIDs) 3. **Tailwind 4 `xs` breakpoint — confirmed approach** - Confirmed Tailwind 4.1.17 in `package.json`; `tailwind.config.ts` approach has no effect - `--breakpoint-xs` is not yet defined anywhere in the codebase; `xs:` classes are not used anywhere - **Decision:** Add `--breakpoint-xs: 375px;` to the existing `@theme` block in `src/routes/layout.css` — prerequisite before any `xs:flex` class is written 4. **`OverflowPill` split — two components** - **Decision:** Split into `OverflowPillButton` (≥768px, interactive `<button>` with tooltip) and `OverflowPillDisplay` (<768px, non-interactive `<span aria-hidden>`); `PersonChipRow` conditionally renders the right one - Total component count is 7, not 6 — the spec decomposition table is superseded by this decision 5. **`role="listbox"` → `role="list"` / `role="listitem"`** - The overflow tooltip contains navigation links (`/persons/{id}`), not a selection widget — `role="listbox"` is semantically incorrect - **Decision:** `role="list"` on the panel, `role="listitem"` on each row, standard `<a>` links; `aria-haspopup="true"` on the pill button - Focus management spec unchanged: focus moves to first link on open, Tab/Shift+Tab navigates natively, Escape returns to pill 6. **`xsMetaLine` rendering — not rendered in topbar** - The rendering location for `formatXsMeta` output is undocumented in the spec - The bottom drawer already exposes full metadata at XS — hiding the chip row without a replacement is not a loss - **Decision:** At <375px, chip row is hidden, no meta line rendered in the topbar. `formatXsMeta` is implemented and tested as a pure utility function but has no call site in this PR 7. **Design tokens — already defined, no new work needed** - `bg-surface`, `bg-muted`, `border-line`, `text-ink`, `text-ink-2`, `bg-primary`, `border-primary` are all already defined in `src/routes/layout.css` via `@theme inline` - `--color-primary` stays as hex; hint strip uses explicit `bg-[rgba(1,40,81,0.05)]` and `border-[rgba(1,40,81,0.20)]` per spec fallback — no token format migration needed 8. **Component tests — in scope** - **Decision:** `@testing-library/svelte` component tests for `OverflowPillButton` (button/tooltip conditional rendering) and `AnnotateHintStrip` (`{#if annotateMode}` branch); TDD applies — failing component test before implementation --- Overall: the spec is implementable as written with these 8 clarifications. The main structural deviations from the spec are the `OverflowPill` split (item 4) and the ARIA role correction (item 5). Everything else is additive or confirmatory.
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#173