feat: Expandable metadata header with labeled "Details" toggle #175
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Replace the static document metadata display with an expandable drawer triggered by a labeled "Details ▼" toggle button in the topbar. The drawer pushes content down (not overlay) and shows a 3-column grid (desktop) or single-column stack (mobile) with document details, person cards, and tag chips.
This replaces the removed bottom panel — metadata is now exclusively accessed via this topbar drawer in all modes (read, transcribe, annotate).
Motivation
Spec
📄
docs/specs/expandable-metadata-header-spec.html— open locally in browser for full mockupsKey screens
Design decisions
Component changes
DocumentTopBar.svelteMetadataDrawer.svelteDocumentBottomPanel.sveltei18n keys needed
details_toggle_label— "Details"details_section_details— "Details"details_section_persons— "Personen"details_section_tags— "Schlagwörter"details_field_date— "Datum"details_field_sender— "Absender"details_field_receivers— "Empfänger"details_field_status— "Status"Acceptance criteria
/persons/{id}DocumentBottomPanel.svelteis removed from the codebase👨💻 Felix Brandt — Senior Fullstack Developer
Questions & Observations
Component decomposition looks right —
MetadataDrawer.svelteas a new component, Details toggle inDocumentTopBar.svelte. But the topbar is already ~150 lines after the recent person chip work. Adding the toggle button + drawer markup could push it past the splitting threshold. Should the toggle button + drawer be extracted as aDetailsToggle.sveltethat owns the open/close state and renders the drawer via a slot or child component?Props explosion on DocumentTopBar — the topbar already takes
doc,canWrite,canAnnotate,fileUrl,annotateMode. The drawer needs the full document (date, sender, receivers, tags, status). That's already indoc, but are tags loaded in the currentDoctype? The existing type definition has notagsfield. The+page.server.tsload function will need to supply tags — is that a new API call or does the existing document endpoint already include them?DocumentBottomPanel.svelteremoval — the issue says "remove entirely." Before deleting, we need to verify: does any route besidesdocuments/[id]use it? A quickgrepfor the import will confirm. If it's only used in one place, clean deletion. If it's referenced elsewhere, those references need updating too.Svelte
slidetransition — the spec calls for a Svelte slide transition. This istransition:slidefromsvelte/transition. Works well, but it requires the drawer to be conditionally rendered with{#if}, not justdisplay: none. That's the correct approach anyway — no DOM when collapsed means no accessibility tree pollution.Suggestions
TDD approach: Start with a Vitest test for
MetadataDrawer.svelte— render it with a mock document that has sender, receivers, and tags. Assert that person names render as links to/persons/{id}. Then test the toggle: renderDetailsToggle, click it, assert the drawer appears. These are the two core behaviors.Keep the drawer data-free —
MetadataDrawershould receive pre-formatted props (date: string,sender: Person,receivers: Person[],tags: Tag[]), not the rawdocobject. This keeps the component pure and testable without needing to construct a full document object in tests.i18n key naming — the proposed keys like
details_field_datefollow the pattern, but considerdoc_details_field_dateto namespace them under the document detail page and avoid collision with any future "details" sections in other contexts.</n">🏗️ Markus Keller — Application Architect
Questions & Observations
No backend changes needed — this is purely frontend, which is the right call. The metadata (date, sender, receivers, tags) is already loaded by the document detail
+page.server.ts. No new API endpoints, no schema changes. Clean scope.Removing
DocumentBottomPanel.svelte— this is a breaking change for the component interface. The document detail page currently orchestrates the bottom panel. Removing it means the[id]/+page.sveltelayout changes significantly. This should be done in a single commit, not spread across PRs, to avoid a half-migrated state where the drawer exists but the bottom panel is still referenced.Drawer state: local vs URL — the issue says "drawer state is local (not persisted across page loads)." That's correct for now. But consider: if the user navigates to
/documents/123via a shared link, should the drawer be open by default to show context? A URL search param (?details=open) would allow this without adding complexity now. Just flagging as a future consideration, not a blocker.Data flow — the 3-column grid (details, persons, tags) all comes from the same document object. No cross-domain fetching needed. The
docprop already has sender and receivers. Tags may need to be added to the document detail load if they're not already there — worth verifying before implementation starts.Suggestions
Module boundary: the drawer is part of the document detail page, not a shared component. Keep it in
src/lib/components/but name itDocumentMetadataDrawer.svelteto signal it's document-scoped. Don't put it in a genericdrawers/folder — YAGNI.No shared state store — the drawer's open/close state should be a simple
let open = $state(false)in the parent or toggle component. No need for a Svelte store or context. Keep it as local as possible.</n">🧪 Sara Holt — QA Engineer & Test Strategist
Questions & Observations
Acceptance criteria are testable — good. Each one maps to a concrete assertion. But a few gaps:
aria-expandedon the toggle button is implied by the spec but not listed as an AC.Missing edge cases in acceptance criteria:
documentDateis null? The "Details" column would show "Datum: —" or omit the field entirely?"Works in all modes: read, transcribe, annotate" — this is one acceptance criterion but it's really three separate test scenarios. The drawer should open/close identically regardless of mode. Worth splitting into three explicit test cases.
Suggestions
Test strategy for this feature:
MetadataDrawerrenders correct content given props. Person names are links. Tags display as chips. Empty states handled.aria-expanded.@testing-library/svelte— assert the drawer DOM appears after click).checkA11yon both states.Add explicit AC: "Drawer is keyboard-accessible: Enter/Space toggles, Tab navigates content, Escape closes."</n">
🔒 Nora "NullX" Steiner — Application Security Engineer
Questions & Observations
Person links in the drawer — person names link to
/persons/{id}. These are internal navigation links, not user-generated URLs, so no open redirect risk. Good.Tag chips navigate to filtered search — if the tag name is used as a query parameter (e.g.
/search?tag=Familienbrief), ensure the tag name is URL-encoded and the search page treats it as a filter parameter, not as raw input injected into a query. This is likely already handled by the existing search infrastructure, but worth confirming the tag value goes through the typed API client, not string interpolation into a URL.No new API endpoints, no new data exposure — this feature displays data that's already loaded on the document detail page. No new attack surface. The drawer doesn't fetch additional data, it just reorganizes what's already in the DOM. This is the ideal security profile for a UI feature.
Drawer content is read-only — no forms, no inputs, no user-editable content in the drawer. No XSS surface. Person names and tag names come from the backend and are rendered as text nodes, not
innerHTML. Clean.Suggestions
🎨 Leonie Voss — UI/UX Design Lead
Questions & Observations
44×28px minimum tap target for the toggle — the spec calls this out, which is good for our 60+ users. But 28px height is below the WCAG 2.2 target size recommendation of 44×44px. The width is fine at 44px+, but the height should be at least 44px including padding. Suggest
min-h-[44px]on the toggle button, which gives comfortable vertical tap area without making it visually oversized (the inner text can be centered in the larger hit area with padding)."Details ▼" / "Details ▲" label — using Unicode arrows (▼/▲) is good for visual affordance, but screen readers may read these as "black down-pointing triangle" which is noisy. Consider using
aria-hidden="true"on the arrow character and relying onaria-expandedfor the state announcement. Or use a small SVG chevron icon witharia-hidden="true".3-column grid on desktop — at 768px breakpoint, 3 columns could be tight if the "Details" column has long values (e.g. a long document title or date string). Consider whether 768px should still be single-column, with 3-column starting at 1024px. Test with real data lengths.
Push layout (not overlay) — excellent choice. Overlays create z-index battles with the PDF viewer and annotation layers. Push layout is also better for spatial awareness — users see the content shift, they understand where the information came from.
Dark mode — no mention of dark mode behavior for the drawer. The existing topbar uses
bg-surfaceandborder-linesemantic tokens, which should work. But the drawer's 3-column grid, person cards, and tag chips need to use the same semantic tokens, not hardcoded whites or grays. Worth noting as an implementation constraint.Suggestions
Add a subtle open/close animation on the chevron arrow itself (rotate 180°) to reinforce the toggle affordance. CSS:
transition: transform 200ms ease; transform: rotate(180deg)when open.The person avatar circles in the drawer should use the same color generation logic as
PersonChip.sveltein the topbar — consistency matters. Don't introduce a separate color mapping.Consider adding a subtle
backdrop(e.g.border-bottom: 2px solid var(--brand-sand)) on the drawer's bottom edge when open, to visually separate it from the content below. The Svelte slide transition handles the motion, but the resting state needs a clear boundary.</n">🔧 Tobias Wendt — DevOps & Platform Engineer
Questions & Observations
No infrastructure impact — this is a purely frontend UI change. No new services, no new environment variables, no new Docker configuration, no new ports, no backend changes. Clean from an ops perspective.
No new API calls — the drawer uses data already loaded by the page. No additional network requests, no impact on backend load or response times. No caching considerations.
Build size impact — adding
MetadataDrawer.svelteand possiblyDetailsToggle.svelteadds minimal bundle size. The Svelteslidetransition is already part of the Svelte runtime, so no new dependency. No concern here.Suggestions
🎨 Leonie Voss — UI/UX Discussion Summary
Worked through 8 open UI/UX items with the team. All resolved.
Resolved items
Tap target height — Update AC from "min 44×28px" to min 44×44px tap target, achieved via padding (not visual size). Keeps the button compact in the topbar while meeting WCAG 2.2 enhanced target size.
SVG chevron replaces Unicode arrows — Drop ▼/▲ Unicode characters (screen readers read them as "black down-pointing triangle"). Use an SVG chevron icon from the existing De Gruyter icon set with
aria-hidden="true". Animated rotation 180° on open/close (transition: transform 200ms ease). Screen readers rely onaria-expandedfor state.Breakpoint simplified to two steps — Single column below 1024px, 3-column grid at ≥1024px. The original 768px breakpoint was too tight for real data (long person names, dates). No intermediate 2-column state.
Empty/sparse data handling — Always show all 3 section headings (Details, Personen, Schlagwörter) even when empty. Muted empty-state text under empty sections (e.g. "Keine Personen zugeordnet"). Teaches users what metadata exists. Receivers capped at 5 visible cards with "+N weitere" expand button. Tags wrap freely, no cap. Drawer has no internal scrollbar — it pushes content down and grows as tall as needed.
Dark mode AC added — New acceptance criterion: "Drawer uses semantic color tokens (
bg-surface,text-ink,text-ink-2,border-line,bg-muted) — no hardcoded color values."Chevron animation — Covered by item 2. SVG chevron rotates 180° with 200ms ease transition.
Drawer bottom edge —
border-bottomusingborder-linesemantic token when drawer is open. No shadow — push layout shouldn't have overlay-style shadows.Person card hover —
hover:bg-mutedbackground highlight +cursor-pointeron the whole card. Large, obvious affordance for 60+ users. Name text doesn't need separate underline — the card-level highlight communicates clickability.Overall read
This is a well-scoped, low-risk UI feature. The main implementation traps are hardcoded colors (dark mode breakage) and the breakpoint being too aggressive. Both are now addressed. Ship it.