feat(topbar): responsive DocumentTopBar — issue #173 #174

Merged
marcel merged 32 commits from feat/issue-173-document-topbar into main 2026-04-02 16:13:49 +02:00
Owner

Summary

  • Replaces DocumentTopBar with a fully responsive orchestrator built to spec (docs/specs/document-topbar-final-spec.html)
  • Extracts clickOutside action from 5 inline copies into a shared module
  • Adds personFormat.ts utility module (6 functions, 27 tests)
  • Introduces 7 new sub-components: PersonChip, DocumentStatusChip, AnnotateHintStrip, OverflowPillDisplay, PersonChipRow, OverflowPillButton
  • Adds xs breakpoint (375px) to Tailwind @theme

Key design decisions

  • OverflowPill split into OverflowPillButton (interactive, desktop) + OverflowPillDisplay (static aria-hidden, chip row)
  • 2nd receiver visibility: CSS-only via hidden md:contents wrapper (not JS viewport check)
  • Tooltip uses role="list" / role="listitem" (not listbox)
  • statusLabel is a thin alias to existing formatDocumentStatus
  • OverflowPillButton Escape key returns focus via await tick() before .focus()

Test plan

  • 529 tests pass across 55 test files
  • npm run check — 0 new type errors (190 pre-existing errors in unrelated files)
  • All new components have browser-project component tests
  • Run /proofshot at 5 viewport widths × 2 themes (AC-16)

Closes #173

🤖 Generated with Claude Code

## Summary - Replaces `DocumentTopBar` with a fully responsive orchestrator built to spec (`docs/specs/document-topbar-final-spec.html`) - Extracts `clickOutside` action from 5 inline copies into a shared module - Adds `personFormat.ts` utility module (6 functions, 27 tests) - Introduces 7 new sub-components: `PersonChip`, `DocumentStatusChip`, `AnnotateHintStrip`, `OverflowPillDisplay`, `PersonChipRow`, `OverflowPillButton` - Adds `xs` breakpoint (375px) to Tailwind `@theme` ## Key design decisions - `OverflowPill` split into `OverflowPillButton` (interactive, desktop) + `OverflowPillDisplay` (static `aria-hidden`, chip row) - 2nd receiver visibility: CSS-only via `hidden md:contents` wrapper (not JS viewport check) - Tooltip uses `role="list"` / `role="listitem"` (not `listbox`) - `statusLabel` is a thin alias to existing `formatDocumentStatus` - `OverflowPillButton` Escape key returns focus via `await tick()` before `.focus()` ## Test plan - [x] 529 tests pass across 55 test files - [x] `npm run check` — 0 new type errors (190 pre-existing errors in unrelated files) - [x] All new components have browser-project component tests - [ ] Run `/proofshot` at 5 viewport widths × 2 themes (AC-16) Closes #173 🤖 Generated with [Claude Code](https://claude.ai/claude-code)
marcel added 11 commits 2026-04-01 08:46:09 +02:00
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
abbreviateName, formatXsMeta, personAvatarColor (djb2), formatDate,
statusDotClass, statusLabel — 27 tests all green

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Accent bar, h-12/h-14 responsive height, 44×44px back link touch target
- PersonChipRow with sender→receivers chips, overflow pill button at ≥768px
- DocumentStatusChip dot-only at ≥768px
- Edit/annotate/download actions with annotateMode wiring
- AnnotateHintStrip below main row when annotateMode active
- status field added to Doc type

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fix(topbar): add role=group to OverflowPillButton outer div — a11y warning
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 3m32s
CI / Backend Unit Tests (pull_request) Failing after 3m5s
CI / E2E Tests (pull_request) Failing after 1h11m43s
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
0c40e10743
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-04-01 08:53:02 +02:00
feat(topbar): double all font sizes and increase bar height for legibility
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 1m54s
CI / Backend Unit Tests (pull_request) Failing after 2m55s
CI / E2E Tests (pull_request) Failing after 1h12m45s
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
1d2e6d7b86
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-04-01 09:04:31 +02:00
Revert "feat(topbar): double all font sizes and increase bar height for legibility"
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Failing after 1m22s
CI / Backend Unit Tests (pull_request) Failing after 2m35s
CI / E2E Tests (pull_request) Failing after 1h16m54s
965087b787
This reverts commit 1d2e6d7b86.
marcel added 1 commit 2026-04-01 09:10:36 +02:00
feat(topbar): increase all font sizes and bar height by 25% for legibility
Some checks failed
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Backend Unit Tests (pull_request) Failing after 2m38s
CI / Unit & Component Tests (push) Has been cancelled
CI / E2E Tests (pull_request) Failing after 1h15m22s
CI / Unit & Component Tests (pull_request) Failing after 1m40s
37d728b006
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-04-01 09:13:06 +02:00
feat(topbar): increase all font sizes and bar height by another 25%
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Failing after 1m23s
CI / Backend Unit Tests (pull_request) Failing after 2m38s
CI / E2E Tests (pull_request) Failing after 1h14m58s
7f2940f0f2
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-04-01 09:24:07 +02:00
feat(topbar): scale action button text and icons to match surrounding text size
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
CI / E2E Tests (pull_request) Has been cancelled
47960b5028
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-04-01 09:30:56 +02:00
feat(i18n): add doc_panel_annotate_hint message key in de/en/es, use in AnnotateHintStrip
Some checks failed
CI / Backend Unit Tests (pull_request) Has been cancelled
CI / Unit & Component Tests (pull_request) Has been cancelled
CI / Unit & Component Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / E2E Tests (pull_request) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
c64abccf63
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-04-01 09:36:11 +02:00
feat(topbar): increase sender→receiver arrow size for visibility
Some checks failed
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
CI / E2E Tests (pull_request) Has been cancelled
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
528e1e05ea
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-04-01 09:38:20 +02:00
feat(topbar): increase arrow to 30px and fix vertical alignment with leading-none
Some checks failed
CI / Backend Unit Tests (pull_request) Has been cancelled
CI / E2E Tests (pull_request) Has been cancelled
CI / Unit & Component Tests (pull_request) Has been cancelled
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
03e2382c8a
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-04-01 09:47:18 +02:00
fix(topbar): center arrow glyph vertically with inline-flex items-center
Some checks failed
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
CI / E2E Tests (pull_request) Has been cancelled
CI / Unit & Component Tests (push) Has been cancelled
c5b98af69b
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-04-01 10:00:19 +02:00
fix(topbar): replace → text char with degruyter arrow icon for reliable centering
Some checks failed
CI / E2E Tests (pull_request) Has been cancelled
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
654b1283c1
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-04-01 10:05:36 +02:00
fix(topbar): use Long-Arrow-Right icon for sender→receiver separator
Some checks failed
CI / Unit & Component Tests (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
CI / E2E Tests (pull_request) Has been cancelled
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
dba1e2a8eb
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-04-01 10:12:27 +02:00
feat(topbar): add mobile kebab menu for annotate/download actions hidden below md
Some checks failed
CI / Unit & Component Tests (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
CI / Unit & Component Tests (push) Has been cancelled
CI / E2E Tests (pull_request) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
51348ad26a
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-04-01 10:16:07 +02:00
refactor(topbar): extract annotate/download actions as Svelte snippets, render in desktop + kebab
Some checks failed
CI / Unit & Component Tests (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
CI / E2E Tests (pull_request) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (push) Has been cancelled
1e9ef63191
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-04-01 10:17:17 +02:00
feat(topbar): always show annotate-stop button — primary action, not hidden in kebab
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / E2E Tests (pull_request) Has been cancelled
CI / Unit & Component Tests (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
3f0b686963
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-04-01 10:22:43 +02:00
feat(topbar): hide sender→receiver chip row below md to make room for buttons
Some checks failed
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
CI / E2E Tests (pull_request) Has been cancelled
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
da7f94de84
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-04-01 10:24:14 +02:00
fix(topbar): add overflow-hidden to flex row so long titles truncate instead of pushing kebab off-screen
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
CI / E2E Tests (pull_request) Has been cancelled
e94f43264c
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Author
Owner

👨‍💻 Felix Brandt — Senior Fullstack Developer

Verdict: ⚠️ Approved with concerns

What I checked

TDD evidence, naming, function size, Svelte 5 rules (keyed {#each}, $derived vs $effect), component splitting, SOLID, dead code.

Highlights

  • Excellent clickOutside extraction — 5 inline copies → 1 shared action module with proper destroy() cleanup. Textbook DRY at the 3+ threshold.
  • personFormat.ts utility with 27 unit tests — well-structured pure functions, good edge-case coverage (UTC off-by-one guard, whitespace trimming).
  • Component decomposition is solid: PersonChip, PersonChipRow, OverflowPillButton, OverflowPillDisplay, DocumentStatusChip, AnnotateHintStrip — each named in 1-2 words, each does one visual job.
  • Svelte 5 runes used correctly: $state, $derived, $props, $bindable — no $effect misuse.
  • All {#each} blocks are keyed (receiver.id, person.id) — good.

Concerns

  1. DocumentTopBar.svelte template size — The orchestrator template is now ~180 lines of markup. The snippets (annotateBtn, annotateStopBtn, downloadLink) help, but the main {#if} cascade in the action buttons area (lines ~165–230) is dense. This is at the boundary of what I'd accept — not a blocker, but worth watching if more actions get added.

  2. Hardcoded German strings in componentsOverflowPillButton.svelte has "+{extraCount} weitere" and "Weitere Empfänger" hardcoded instead of using Paraglide m.* translation keys. Same for "Zurück zur Dokumentenliste" in DocumentTopBar.svelte and "Weitere Aktionen" in the kebab button. The project uses i18n (de/en/es) — these strings should go through Paraglide.

  3. statusLabel is a passthrough wrapperpersonFormat.ts:statusLabel() just calls formatDocumentStatus(). One level of indirection with no added value. Either use formatDocumentStatus directly in DocumentStatusChip.svelte, or if the name statusLabel is preferred, re-export it as an alias rather than wrapping.

  4. DocumentStatus type defined in 3 places — The union type 'PLACEHOLDER' | 'UPLOADED' | 'TRANSCRIBED' | 'REVIEWED' | 'ARCHIVED' is defined separately in DocumentTopBar.svelte, DocumentStatusChip.svelte, and personFormat.ts. Extract it once (e.g. in personFormat.ts or a shared types file) and import it.

  5. PersonChipRow has its own hidden xs:flex wrapper — But DocumentTopBar also wraps it in a hidden md:block div. The double-hiding at different breakpoints works but is confusing to read. Consider letting the parent control all visibility and having PersonChipRow always render.

Suggestions (non-blocking)

  • The clickOutside action dispatches a CustomEvent('clickoutside') but the Svelte components listen via onclickoutside — this works in Svelte 5 but is undocumented behavior (custom events on elements). A brief inline comment explaining this pattern would help future readers.
  • personFormat.spec.ts test for formatXsMeta uses inline Doc type instead of importing from the module — minor, but the type could be exported and reused.
## 👨‍💻 Felix Brandt — Senior Fullstack Developer **Verdict: ⚠️ Approved with concerns** ### What I checked TDD evidence, naming, function size, Svelte 5 rules (keyed `{#each}`, `$derived` vs `$effect`), component splitting, SOLID, dead code. ### Highlights - **Excellent `clickOutside` extraction** — 5 inline copies → 1 shared action module with proper `destroy()` cleanup. Textbook DRY at the 3+ threshold. - **`personFormat.ts` utility** with 27 unit tests — well-structured pure functions, good edge-case coverage (UTC off-by-one guard, whitespace trimming). - **Component decomposition** is solid: `PersonChip`, `PersonChipRow`, `OverflowPillButton`, `OverflowPillDisplay`, `DocumentStatusChip`, `AnnotateHintStrip` — each named in 1-2 words, each does one visual job. - **Svelte 5 runes used correctly**: `$state`, `$derived`, `$props`, `$bindable` — no `$effect` misuse. - **All `{#each}` blocks are keyed** (`receiver.id`, `person.id`) — good. ### Concerns 1. **`DocumentTopBar.svelte` template size** — The orchestrator template is now ~180 lines of markup. The snippets (`annotateBtn`, `annotateStopBtn`, `downloadLink`) help, but the main `{#if}` cascade in the action buttons area (lines ~165–230) is dense. This is at the boundary of what I'd accept — not a blocker, but worth watching if more actions get added. 2. **Hardcoded German strings in components** — `OverflowPillButton.svelte` has `"+{extraCount} weitere"` and `"Weitere Empfänger"` hardcoded instead of using Paraglide `m.*` translation keys. Same for `"Zurück zur Dokumentenliste"` in `DocumentTopBar.svelte` and `"Weitere Aktionen"` in the kebab button. The project uses i18n (de/en/es) — these strings should go through Paraglide. 3. **`statusLabel` is a passthrough wrapper** — `personFormat.ts:statusLabel()` just calls `formatDocumentStatus()`. One level of indirection with no added value. Either use `formatDocumentStatus` directly in `DocumentStatusChip.svelte`, or if the name `statusLabel` is preferred, re-export it as an alias rather than wrapping. 4. **`DocumentStatus` type defined in 3 places** — The union type `'PLACEHOLDER' | 'UPLOADED' | 'TRANSCRIBED' | 'REVIEWED' | 'ARCHIVED'` is defined separately in `DocumentTopBar.svelte`, `DocumentStatusChip.svelte`, and `personFormat.ts`. Extract it once (e.g. in `personFormat.ts` or a shared types file) and import it. 5. **`PersonChipRow` has its own `hidden xs:flex` wrapper** — But `DocumentTopBar` also wraps it in a `hidden md:block` div. The double-hiding at different breakpoints works but is confusing to read. Consider letting the parent control all visibility and having `PersonChipRow` always render. ### Suggestions (non-blocking) - The `clickOutside` action dispatches a `CustomEvent('clickoutside')` but the Svelte components listen via `onclickoutside` — this works in Svelte 5 but is undocumented behavior (custom events on elements). A brief inline comment explaining this pattern would help future readers. - `personFormat.spec.ts` test for `formatXsMeta` uses inline `Doc` type instead of importing from the module — minor, but the type could be exported and reused.
Author
Owner

🏗️ Markus Keller — Application Architect

Verdict: Approved

What I checked

Layer boundaries, module structure, accidental complexity, coupling between components, data flow direction.

Assessment

This PR is architecturally clean. The changes are frontend-only, well-scoped, and follow the established patterns.

Module boundaries are respected:

  • New utility module personFormat.ts owns formatting logic — components import from it, not from each other.
  • clickOutside action extracted to $lib/actions/ — correct location for shared Svelte actions.
  • New components live in $lib/components/ alongside existing ones.
  • No backend changes, no API contract changes, no new data fetching — pure presentation refactor.

Data flows down correctly:

  • DocumentTopBar receives doc via props from the page, derives computed values (receivers, extraCount, shortDate, longDate), and passes slices to children.
  • No component fetches its own data. No onMount + fetch patterns.
  • $bindable() used for annotateMode — appropriate for parent-child state sync on a toggle.

Coupling is minimal:

  • PersonChip, OverflowPillDisplay, AnnotateHintStrip are leaf components with no cross-dependencies.
  • OverflowPillButton and PersonChipRow compose leaf components but don't reach into siblings.

One observation (not a concern):
The Doc type in DocumentTopBar.svelte now includes status: DocumentStatus as a required field. This means the parent page's server load must supply status — which it should already be doing from the API response. Just confirm that status is always present in the Document schema returned by the backend (it is, per the entity model — DocumentStatus enum with a default).

## 🏗️ Markus Keller — Application Architect **Verdict: ✅ Approved** ### What I checked Layer boundaries, module structure, accidental complexity, coupling between components, data flow direction. ### Assessment This PR is architecturally clean. The changes are frontend-only, well-scoped, and follow the established patterns. **Module boundaries are respected:** - New utility module `personFormat.ts` owns formatting logic — components import from it, not from each other. - `clickOutside` action extracted to `$lib/actions/` — correct location for shared Svelte actions. - New components live in `$lib/components/` alongside existing ones. - No backend changes, no API contract changes, no new data fetching — pure presentation refactor. **Data flows down correctly:** - `DocumentTopBar` receives `doc` via props from the page, derives computed values (`receivers`, `extraCount`, `shortDate`, `longDate`), and passes slices to children. - No component fetches its own data. No `onMount` + `fetch` patterns. - `$bindable()` used for `annotateMode` — appropriate for parent-child state sync on a toggle. **Coupling is minimal:** - `PersonChip`, `OverflowPillDisplay`, `AnnotateHintStrip` are leaf components with no cross-dependencies. - `OverflowPillButton` and `PersonChipRow` compose leaf components but don't reach into siblings. **One observation (not a concern):** The `Doc` type in `DocumentTopBar.svelte` now includes `status: DocumentStatus` as a required field. This means the parent page's server load must supply `status` — which it should already be doing from the API response. Just confirm that `status` is always present in the `Document` schema returned by the backend (it is, per the entity model — `DocumentStatus` enum with a default).
Author
Owner

🧪 Sara Holt — QA Engineer & Test Strategist

Verdict: ⚠️ Approved with concerns

What I checked

Test coverage across the pyramid, test quality (naming, assertions, isolation), missing edge cases, test pyramid balance.

Highlights

  • 27 unit tests for personFormat.ts — excellent coverage of the utility module. Tests for abbreviateName, formatXsMeta, personAvatarColor, formatDate, statusDotClass, statusLabel all present with meaningful edge cases (UTC boundary, whitespace, empty last name).
  • Browser component tests for AnnotateHintStrip and OverflowPillButton using vitest-browser-svelte — proper rendering + interaction tests.
  • clickOutside action unit tests — covers mount, inside click, outside click, and destroy cleanup. Clean DOM management with afterEach.
  • 529 tests passing across 55 files — no regressions.

Concerns

  1. Missing component tests for several new components:

    • PersonChip.svelte — no test file. Should verify: correct initials rendering, abbreviated vs full name, avatar color application.
    • PersonChipRow.svelte — no test file. Should verify: sender-only rendering, sender+receivers, arrow visibility, overflow pill display, 2nd receiver hidden below md.
    • DocumentStatusChip.svelte — no test file. Should verify: correct dot class per status, correct label/title, hidden below md.
    • OverflowPillDisplay.svelte — no test file (though it's a thin static component).

    These are new components with logic (derived values, conditional rendering). They should have at least basic rendering tests.

  2. OverflowPillButton test doesn't verify clickOutside closing — The test checks Escape key closing, but doesn't test that clicking outside the tooltip closes it. This is a core behavior.

  3. formatXsMeta test coverage gap — No test for sender: undefined (only null is tested). The type allows undefined — verify both.

  4. No test for the DocumentTopBar orchestrator itself — The main component that wires everything together has no integration test. I understand it's complex to test, but at minimum a smoke test (renders without error with minimal props) would catch wiring bugs.

Suggestion

The proofshot visual verification at 5 viewport widths (AC-16 in the test plan) is still unchecked. This should be completed before merge to catch responsive layout issues that unit tests can't.

## 🧪 Sara Holt — QA Engineer & Test Strategist **Verdict: ⚠️ Approved with concerns** ### What I checked Test coverage across the pyramid, test quality (naming, assertions, isolation), missing edge cases, test pyramid balance. ### Highlights - **27 unit tests for `personFormat.ts`** — excellent coverage of the utility module. Tests for `abbreviateName`, `formatXsMeta`, `personAvatarColor`, `formatDate`, `statusDotClass`, `statusLabel` all present with meaningful edge cases (UTC boundary, whitespace, empty last name). - **Browser component tests** for `AnnotateHintStrip` and `OverflowPillButton` using `vitest-browser-svelte` — proper rendering + interaction tests. - **`clickOutside` action unit tests** — covers mount, inside click, outside click, and destroy cleanup. Clean DOM management with `afterEach`. - **529 tests passing across 55 files** — no regressions. ### Concerns 1. **Missing component tests for several new components:** - `PersonChip.svelte` — no test file. Should verify: correct initials rendering, abbreviated vs full name, avatar color application. - `PersonChipRow.svelte` — no test file. Should verify: sender-only rendering, sender+receivers, arrow visibility, overflow pill display, 2nd receiver hidden below md. - `DocumentStatusChip.svelte` — no test file. Should verify: correct dot class per status, correct label/title, hidden below md. - `OverflowPillDisplay.svelte` — no test file (though it's a thin static component). These are new components with logic (derived values, conditional rendering). They should have at least basic rendering tests. 2. **`OverflowPillButton` test doesn't verify `clickOutside` closing** — The test checks Escape key closing, but doesn't test that clicking outside the tooltip closes it. This is a core behavior. 3. **`formatXsMeta` test coverage gap** — No test for `sender: undefined` (only `null` is tested). The type allows `undefined` — verify both. 4. **No test for the `DocumentTopBar` orchestrator itself** — The main component that wires everything together has no integration test. I understand it's complex to test, but at minimum a smoke test (renders without error with minimal props) would catch wiring bugs. ### Suggestion The proofshot visual verification at 5 viewport widths (AC-16 in the test plan) is still unchecked. This should be completed before merge to catch responsive layout issues that unit tests can't.
Author
Owner

🔒 Nora "NullX" Steiner — Application Security Engineer

Verdict: Approved

What I checked

XSS vectors, injection sinks, ARIA/DOM manipulation safety, event handling, data exposure, authorization boundaries.

Assessment

This PR is frontend-only presentation code with no new data fetching, no API calls, no form submissions, and no user input processing. The attack surface change is minimal.

Checked and clear:

  1. No XSS sinks — No innerHTML, {@html}, document.write(), or insertAdjacentHTML(). All dynamic content is rendered through Svelte template interpolation ({variable}), which auto-escapes.

  2. clickOutside action is safe — It uses node.contains() and CustomEvent dispatch. No user-controlled input reaches the event handler. The capture-phase listener is properly cleaned up in destroy().

  3. Person links use safe href constructionhref="/persons/{person.id}" where person.id is a UUID from the backend. No user-controlled path segments.

  4. Download linkhref={fileUrl} and download={doc.originalFilename} are passed from the parent page's server load function, not from user input. The fileUrl is server-generated. No open redirect risk.

  5. No sensitive data exposure — Person names and document metadata displayed are already visible on the document detail page. No new data surface.

  6. Kebab menu aria-haspopup / aria-expanded — Correctly implemented. Not a security concern but shows attention to DOM state.

One minor note

The OverflowPillButton tooltip renders person links (/persons/{person.id}). These are internal navigation links, not external URLs. No SSRF or redirect concern. Just confirming I checked.

## 🔒 Nora "NullX" Steiner — Application Security Engineer **Verdict: ✅ Approved** ### What I checked XSS vectors, injection sinks, ARIA/DOM manipulation safety, event handling, data exposure, authorization boundaries. ### Assessment This PR is frontend-only presentation code with no new data fetching, no API calls, no form submissions, and no user input processing. The attack surface change is minimal. **Checked and clear:** 1. **No XSS sinks** — No `innerHTML`, `{@html}`, `document.write()`, or `insertAdjacentHTML()`. All dynamic content is rendered through Svelte template interpolation (`{variable}`), which auto-escapes. 2. **`clickOutside` action is safe** — It uses `node.contains()` and `CustomEvent` dispatch. No user-controlled input reaches the event handler. The capture-phase listener is properly cleaned up in `destroy()`. 3. **Person links use safe href construction** — `href="/persons/{person.id}"` where `person.id` is a UUID from the backend. No user-controlled path segments. 4. **Download link** — `href={fileUrl}` and `download={doc.originalFilename}` are passed from the parent page's server load function, not from user input. The `fileUrl` is server-generated. No open redirect risk. 5. **No sensitive data exposure** — Person names and document metadata displayed are already visible on the document detail page. No new data surface. 6. **Kebab menu `aria-haspopup` / `aria-expanded`** — Correctly implemented. Not a security concern but shows attention to DOM state. ### One minor note The `OverflowPillButton` tooltip renders person links (`/persons/{person.id}`). These are internal navigation links, not external URLs. No SSRF or redirect concern. Just confirming I checked.
Author
Owner

🎨 Leonie Voss — UI/UX Design Lead

Verdict: ⚠️ Approved with concerns

What I checked

Brand compliance, accessibility (WCAG 2.2), responsive behavior, touch targets, typography, color contrast, dark mode readiness.

Highlights

  • Back link touch target is 44×44px (h-11 w-11) — meets minimum requirement. Good.
  • Responsive breakpoint strategy is well-executed: xs (375px) for chip row, md (768px) for desktop actions, lg for long date format. Progressive enhancement approach.
  • CSS-only 2nd receiver visibility (hidden md:contents) — elegant, no JS viewport checks. Correct approach.
  • Font sizes use text-[16px], text-[18px], text-[14px] — above the 12px floor. Acceptable.
  • Focus-visible rings present on all interactive elements (focus-visible:ring-2 focus-visible:ring-primary). Good.

Concerns

  1. DocumentStatusChip uses color alone as the status indicator — A colored dot (h-4 w-4 rounded-full with status-dependent bg class) conveys status via color only. WCAG 1.4.1 (Use of Color): color must not be the sole visual means. The dot has a title and aria-label, which helps screen readers, but sighted users who cannot distinguish colors (8% of males) get no redundant cue. Fix: Add a text label visible on hover/focus, or use distinct shapes/icons per status, or show the status text alongside the dot at wider viewpoints.

  2. Hardcoded inline style attributesAnnotateHintStrip.svelte uses style="background: rgba(1,40,81,0.05); border-color: rgba(1,40,81,0.20)". These raw RGBA values bypass the design token system (bg-*, border-* classes). If dark mode is implemented, these won't adapt. Fix: Define these as semantic CSS custom properties or Tailwind utilities.

  3. PersonChip avatar circle is 25×25px — This is below the 44×44px touch target minimum. The chip itself is not a link/button (it's a display-only span), so this is acceptable for display. However, if chips ever become tappable, this needs to grow. Just flagging for awareness.

  4. Kebab menu button is 36×36px (h-9 w-9) — Below the 44×44px minimum for mobile touch targets. This button only appears on mobile (md:hidden), where touch targets matter most. Fix: Increase to h-11 w-11 (44px).

  5. OverflowPillButton tooltip has no max-height or scroll — With many receivers (e.g., 15+), the tooltip could overflow the viewport. Add max-h-[300px] overflow-y-auto to the tooltip container.

  6. Accent bar (w-[3px] bg-primary) — Nice brand touch. Verify it meets the 3px minimum thickness for decorative elements at all viewport sizes.

Suggestions (non-blocking)

  • The PersonChip initials circle uses style="background-color: {avatarColor}" — another inline style that won't adapt to dark mode. Consider mapping avatar colors to CSS custom properties.
  • Long document titles truncate well (truncate class), but there's no tooltip on mobile showing the full title. The title attribute exists but doesn't trigger on touch — consider a tap-to-expand pattern for mobile.
## 🎨 Leonie Voss — UI/UX Design Lead **Verdict: ⚠️ Approved with concerns** ### What I checked Brand compliance, accessibility (WCAG 2.2), responsive behavior, touch targets, typography, color contrast, dark mode readiness. ### Highlights - **Back link touch target is 44×44px** (`h-11 w-11`) — meets minimum requirement. Good. - **Responsive breakpoint strategy** is well-executed: `xs` (375px) for chip row, `md` (768px) for desktop actions, `lg` for long date format. Progressive enhancement approach. - **CSS-only 2nd receiver visibility** (`hidden md:contents`) — elegant, no JS viewport checks. Correct approach. - **Font sizes use `text-[16px]`, `text-[18px]`, `text-[14px]`** — above the 12px floor. Acceptable. - **Focus-visible rings** present on all interactive elements (`focus-visible:ring-2 focus-visible:ring-primary`). Good. ### Concerns 1. **`DocumentStatusChip` uses color alone as the status indicator** — A colored dot (`h-4 w-4 rounded-full` with status-dependent bg class) conveys status via color only. WCAG 1.4.1 (Use of Color): color must not be the sole visual means. The dot has a `title` and `aria-label`, which helps screen readers, but sighted users who cannot distinguish colors (8% of males) get no redundant cue. **Fix:** Add a text label visible on hover/focus, or use distinct shapes/icons per status, or show the status text alongside the dot at wider viewpoints. 2. **Hardcoded inline `style` attributes** — `AnnotateHintStrip.svelte` uses `style="background: rgba(1,40,81,0.05); border-color: rgba(1,40,81,0.20)"`. These raw RGBA values bypass the design token system (`bg-*`, `border-*` classes). If dark mode is implemented, these won't adapt. **Fix:** Define these as semantic CSS custom properties or Tailwind utilities. 3. **`PersonChip` avatar circle is 25×25px** — This is below the 44×44px touch target minimum. The chip itself is not a link/button (it's a display-only span), so this is acceptable for display. However, if chips ever become tappable, this needs to grow. Just flagging for awareness. 4. **Kebab menu button is 36×36px** (`h-9 w-9`) — Below the 44×44px minimum for mobile touch targets. This button only appears on mobile (`md:hidden`), where touch targets matter most. **Fix:** Increase to `h-11 w-11` (44px). 5. **`OverflowPillButton` tooltip has no max-height or scroll** — With many receivers (e.g., 15+), the tooltip could overflow the viewport. Add `max-h-[300px] overflow-y-auto` to the tooltip container. 6. **Accent bar** (`w-[3px] bg-primary`) — Nice brand touch. Verify it meets the 3px minimum thickness for decorative elements at all viewport sizes. ### Suggestions (non-blocking) - The `PersonChip` initials circle uses `style="background-color: {avatarColor}"` — another inline style that won't adapt to dark mode. Consider mapping avatar colors to CSS custom properties. - Long document titles truncate well (`truncate` class), but there's no tooltip on mobile showing the full title. The `title` attribute exists but doesn't trigger on touch — consider a tap-to-expand pattern for mobile.
Author
Owner

🔧 Tobias Wendt — DevOps & Platform Engineer

Verdict: Approved

What I checked

Build impact, bundle size, new dependencies, CI pipeline compatibility, infrastructure changes, Docker/deployment concerns.

Assessment

This PR is purely frontend component code. No infrastructure impact.

  • No new npm dependenciesclickOutside is a hand-written Svelte action, not a library. personFormat.ts uses only Intl.DateTimeFormat (browser-native) and an existing internal import (formatDocumentStatus). Zero bundle size increase from new packages.
  • No changes to docker-compose.yml, Dockerfiles, CI workflows, or deployment configuration.
  • No new environment variables or config files.
  • No new API endpoints — no backend changes at all.
  • The xs: 375px breakpoint added to layout.css is a CSS-only change with no build or runtime impact.
  • 22 changed files, 844 additions, 177 deletions — entirely .svelte, .ts, and .json (i18n) files. No build pipeline changes needed.
  • 529 tests passing — CI should be green.

Nothing for me to flag here. Clean frontend-only PR.

## 🔧 Tobias Wendt — DevOps & Platform Engineer **Verdict: ✅ Approved** ### What I checked Build impact, bundle size, new dependencies, CI pipeline compatibility, infrastructure changes, Docker/deployment concerns. ### Assessment This PR is purely frontend component code. No infrastructure impact. - **No new npm dependencies** — `clickOutside` is a hand-written Svelte action, not a library. `personFormat.ts` uses only `Intl.DateTimeFormat` (browser-native) and an existing internal import (`formatDocumentStatus`). Zero bundle size increase from new packages. - **No changes to `docker-compose.yml`**, Dockerfiles, CI workflows, or deployment configuration. - **No new environment variables** or config files. - **No new API endpoints** — no backend changes at all. - **The `xs: 375px` breakpoint** added to `layout.css` is a CSS-only change with no build or runtime impact. - **22 changed files, 844 additions, 177 deletions** — entirely `.svelte`, `.ts`, and `.json` (i18n) files. No build pipeline changes needed. - **529 tests passing** — CI should be green. Nothing for me to flag here. Clean frontend-only PR.
marcel added 5 commits 2026-04-02 12:13:28 +02:00
Remove overflow-hidden from the main flex row — the inner min-w-0 flex-1
overflow-hidden title container already handles truncation. Add relative z-10
to the topbar wrapper so it stacks above the pdf viewer. Pill is now hidden
below md (matching the chip row) and shows +N at md, +N weitere at lg+.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Consistent with the overflow pill popup which already linked to persons.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
feat(topbar): add divider between sender/receiver block and action buttons
Some checks failed
CI / E2E Tests (pull_request) Failing after 1h16m31s
CI / Unit & Component Tests (push) Failing after 1m30s
CI / Backend Unit Tests (push) Failing after 2m29s
CI / Unit & Component Tests (pull_request) Failing after 1m30s
CI / Backend Unit Tests (pull_request) Failing after 2m29s
CI / E2E Tests (push) Failing after 1h12m24s
1a57ec2036
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel merged commit 1a57ec2036 into main 2026-04-02 16:13:49 +02:00
marcel deleted branch feat/issue-173-document-topbar 2026-04-02 16:13:52 +02:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marcel/familienarchiv#174