From e6f12e6d9042937466fd6ad89c84601f179e3f3a Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 6 Apr 2026 12:09:00 +0200 Subject: [PATCH] docs(design): add sort integration specs for issue #180 Exploration spec (sort-integration-spec.html) covers 4 placement variants with comparison matrix. Final spec (sort-inline-final-spec.html) locks in Variant A (inline sort in search bar row) with full desktop/mobile states, dropdown interaction anatomy, loading/empty states, and backend wiring checklist. Co-Authored-By: Claude Sonnet 4.6 --- docs/specs/sort-inline-final-spec.html | 1052 +++++++++++++++++++ docs/specs/sort-integration-spec.html | 1291 ++++++++++++++++++++++++ 2 files changed, 2343 insertions(+) create mode 100644 docs/specs/sort-inline-final-spec.html create mode 100644 docs/specs/sort-integration-spec.html diff --git a/docs/specs/sort-inline-final-spec.html b/docs/specs/sort-inline-final-spec.html new file mode 100644 index 00000000..fdf2f00c --- /dev/null +++ b/docs/specs/sort-inline-final-spec.html @@ -0,0 +1,1052 @@ + + + + + +Sort — Inline Variant A · Final Design Spec · Familienarchiv #180 + + + +
+ + +
+
+
+

Sort — Inline Variant · Final Design Spec

+

Sort dropdown sits inline in the search bar row, between the text input and the Filter button. Always visible, always labelled. Selected from 4 exploration variants (see sort-integration-spec.html). Addresses Issue #180 Problem 1.

+
+ Final · Ready for implementation +
+
+
+
Issue
+
#180 — Sort & Search UX
+
+
+
Sort options (6)
+
Datum · Titel · Absender · Empfänger · Tag · Hochgeladen
+
+
+
Default
+
Datum ↓ (newest first) — no URL param needed for default
+
+
+
New URL params
+
?sort=date&dir=desc — omitted when default
+
+
+
+ + +
+ 📐 Mockup scale notice — all font-size, height, and padding values + in the mockup CSS are scaled to ~55% of actual implementation values. + Do not copy sizes from mockup CSS. Use the ⚙ Implementation + Reference tables after each section. +
+ + + +
+
+ 1 + Anatomy at rest — desktop +
+ +
+ + +
+ Default — no active search, default sort +
+
+
+
+
localhost:3000/
+
+
+ + Dokumente + Personen +
+
+
+
+
+
+ + Titel, Personen, Tags durchsuchen … +
+ +
+ Sortieren: + Datum ↓ + +
+
⊞ Filter
+
+
+
+ +
Dashboard widgets appear here when no search is active
+
+
+
+
On the dashboard (no active search) the sort button is still visible and operable. Sorting here has no effect until a search is triggered — the button communicates the "ready" state.
+
+ + +
+ Active — search running, custom sort set +
+
+
+
+
localhost:3000/?q=raddatz&sort=sender&dir=asc
+
+
+ + Dokumente + Personen +
+
+
+
+
+
+ + raddatz +
+ +
+ Sortieren: + Absender ↑ + +
+
⊞ Filter
+
+
+
+ +
+ 8 Dokumente gefunden +
+ +
+
PDF
+
+
Brief an Tante Klara, Weihnachten 1954
+
Ernst Raddatz·15. Dez 1954·Briefe
+
+
+
+
PDF
+
+
Urlaubspostkarte Schwarzwald 1962
+
Hildegard Raddatz·8. Aug 1962·Karten
+
+
+
+
+
+
Active sort changes the button border and text color to navy. The label updates to the selected field + direction arrow. Reset button (✕) clears both search and sort, returning to the dashboard.
+
+
+ +
+
Implementation Reference — §1 Search bar anatomy + Real values · mockup above is ~55% scale · do not copy mockup CSS +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ElementTailwind classesReal sizeNotes
Search card containerbg-surface border border-line rounded-sm p-4 shadow-sm mb-2p 16pxExisting SearchFilterBar.svelte outer wrapper — no change needed
Row 1 flex containerflex items-center gap-3Existing — insert sort button between SINPUT and filter button
Search inputflex-1 h-10 flex items-center gap-2 px-3 border border-line rounded-sm text-base placeholder-ink-3 focus-visible:ring-2 focus-visible:ring-focus-ring focus:outline-noneh 40px, text 16pxPlaceholder: "Titel, Personen, Tags durchsuchen …". Addresses #180 Problem 2 discoverability.
Sort trigger button — defaulth-10 shrink-0 flex items-center gap-1.5 px-3 border border-line rounded-sm bg-muted text-sm font-bold text-ink-2 hover:bg-surface hover:text-ink transition whitespace-nowrap cursor-pointerh 40px, px 12px, text 14px / 700Shows "Sortieren: Datum ↓". "Sortieren:" prefix: text-xs font-normal text-ink-3 mr-0.5
Sort trigger button — activeSame + override: border-brand-navy text-brand-navy bg-surfaceActive when sort ≠ default (date/desc). "Sortieren:" prefix color: text-brand-navy/50
Sort trigger button — focusAdd: focus-visible:ring-2 focus-visible:ring-focus-ring focus:outline-noneNever remove focus ring. Tab order: input → sort → filter → reset
Filter button (unchanged)h-10 shrink-0 flex items-center gap-2 px-3 border border-line rounded-sm bg-muted text-sm font-bold text-ink-2 uppercase tracking-wide hover:text-ink transitionh 40pxExisting — no change
Reset button (unchanged)h-10 shrink-0 flex items-center justify-center px-2 border border-transparent text-ink-3 hover:text-red-500 transitionh 40px, min-w 40pxExisting — no change
Result count linetext-sm font-medium text-ink-2 mb-3<strong class="text-brand-navy font-bold">14px / 500aria-live="polite". Only rendered when !isDashboard.
+
+
+ + + +
+
+ 2 + Sort dropdown — open states & interaction anatomy +
+ +
+ + +
+ Open — default sort (Datum ↓) +
+ +
+ Sortieren: + Datum ↓ + +
+ +
+
Sortieren nach
+
+ Datum +
+ + +
+
+
+ Titel +
+
+
+ Absender +
+
+
+ Empfänger +
+
+
+ Tag +
+
+
+ Hochgeladen +
+
+
+
+
Active option row has light blue tint. The currently selected direction arrow is navy-filled. Inactive rows show both ↑ ↓ with no fill.
+
+ + +
+ Hover — "Absender" row, ↑ direction hovered +
+
+ Sortieren: + Datum ↓ + +
+
+
Sortieren nach
+
+ Datum +
+
+
+ Titel +
+
+ +
+ Absender +
+ + +
+
+
+ Empfänger +
+
+
+ Tag +
+
+
+ Hochgeladen +
+
+
+
+
Hovering a row highlights the whole row in muted gray. Hovering a specific direction arrow highlights that arrow with a border. Clicking either closes the dropdown and triggers a search.
+
+ + +
+ After selection — "Absender ↑" now active +
+
+ Sortieren: + Absender ↑ + +
+
+
Sortieren nach
+
+ Datum +
+
+
+ Titel +
+
+
+ Absender +
+ + +
+
+
+ Empfänger +
+
+
+ Tag +
+
+
+ Hochgeladen +
+
+
+
+
After selection: trigger button label updates instantly. Dropdown typically closes after a direction is chosen. The user can re-open to change direction without re-selecting the field.
+
+
+ +
+ Interaction model: Clicking a field label selects that field and retains the last-used direction for that field (defaults to ↓ if no prior selection). Clicking a direction arrow selects that field + direction and closes the dropdown. This means two distinct affordances — quick tap on a label re-sorts with same direction, precise tap on an arrow picks direction explicitly. +
+ +
+
Implementation Reference — §2 Sort dropdown + Real values · mockup above is ~55% scale +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ElementTailwind classesReal sizeNotes
Dropdown containerabsolute top-[calc(100%+4px)] left-0 z-50 min-w-[200px] bg-surface border border-brand-navy rounded-sm shadow-[0_6px_20px_rgba(0,0,0,.12)]min-w 200pxUse Svelte's use:clickOutside action to close. Also close on Escape. Position: position: relative on .SDROP-WRAP.
Dropdown headertext-xs font-bold uppercase tracking-widest text-ink-3 px-3 py-2 border-b border-line12px / 700"SORTIEREN NACH" — orientation label for screen readers + seniors
Option row — defaultflex items-center px-3 py-2.5 gap-4 border-b border-line last:border-b-0 cursor-pointer hover:bg-mutedh ~44px, py 10pxTouch target exactly 44px. Most commonly undersized element — do not reduce py.
Option row — activeSame + bg-blue-50aria-selected="true" on the active row. Role: role="option" on each row, role="listbox" on dropdown.
Option labelflex-1 text-sm font-semibold text-ink14px / 600Active row: text-brand-navy font-bold
Direction toggle groupflex gap-1 shrink-0role="group" with aria-label="Richtung"
Direction button — inactivetext-xs font-bold px-2 py-1 rounded-sm border border-line text-ink-3 hover:border-ink-2 hover:text-ink-2 transition min-w-[28px] text-center28px min-width, 24px hLabels: "↑" + aria-label="Aufsteigend" / "↓" + aria-label="Absteigend"
Direction button — activeSame + bg-brand-navy text-white border-brand-navyaria-pressed="true"
Keyboard navigation/ to move between rows. Enter / Space to select. Escape to close without change. Tab moves through direction arrows within a row. Focus returns to trigger on close.
Svelte statelet sort = $state('date'), let dir = $state('desc'). Add both to triggerSearch() URL params. Bind to new sort and dir props on SearchFilterBar.
+
+
+ + + +
+
+ 3 + Mobile layout — 320px breakpoint +
+ +
+ + +
+ Default — no search +
+
+ 9:41 +
+
+
+
+
+ +
+
+ + Durchsuchen… +
+
+
+ +
+
Datum ↓
+
⊞ Filter
+
+
+
Dashboard
+
+
+
Row 1: input + reset only. Row 2: sort and filter as equal-width buttons, full-width of the card.
+
+ + +
+ Active — custom sort set +
+
+ 9:41 +
+
+
+
+
+
+
+ + raddatz +
+
+
+
+ +
Absender ↑
+
⊞ Filter
+
+
+
8 Dokumente gefunden
+
+
PDF
+
+
+
+
PDF
+
+
+
+
+
Sort button adopts navy border + text when non-default, identical to desktop active state. Button label shortens to field name + direction (no "Sortieren:" prefix).
+
+ + +
+ Dropdown open — bottom sheet on mobile +
+
+ 9:41 +
+
+
+
+
+
+
raddatz
+
+
+
+
Absender ↑
+
⊞ Filter
+
+
+
+ +
+ +
+
+
+
Sortieren nach
+
+ Datum +
+
+
+ Titel +
+
+
+ Absender +
+
+
+ Empfänger +
+
+
+ Tag +
+
+
+ Hochgeladen +
+
+
+
+
On mobile (<768px) the dropdown renders as a bottom sheet with a drag handle and rounded top corners. Overlay taps the backdrop to close. Option rows use generous py for thumb targets.
+
+
+ +
+
Implementation Reference — §3 Mobile layout + Real values · mockup above is ~55% scale +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ElementTailwind classesReal sizeNotes
Search card row 1 (mobile)flex items-center gap-2 mb-2Input + reset only. Sort and filter move to row 2. Breakpoint: sm:hidden on row 2 wrapper, show on mobile only.
Search card row 2 (mobile)flex items-center gap-2 sm:hiddenSort + filter as two equal-width buttons. Desktop keeps sort in row 1 between input and filter.
Sort button label — mobileOmit "Sortieren:" prefix (hidden with hidden sm:inline). Show field name + direction only: "Absender ↑"
Sort button width — mobileflex-1 justify-center min-h-[44px]h 44px min, flex-1Equal width with filter button. Most commonly undersized on mobile.
Bottom sheet wrapperfixed inset-x-0 bottom-0 z-50 bg-surface border-t-2 border-brand-navy rounded-t-2xl shadow-[0_-4px_24px_rgba(0,0,0,.15)] sm:hiddenOnly on <768px. Desktop: absolute dropdown. Backdrop: fixed inset-0 z-40 bg-black/20 behind sheet.
Bottom sheet drag handlemx-auto mt-2 mb-1 w-10 h-1 rounded-full bg-line40px × 4pxDecorative — does not need to be draggable. Tap backdrop or select option to close.
Bottom sheet option rowsflex items-center px-4 py-3.5 gap-4 border-b border-line last:border-b-0py 14px → row h ~52pxTaller than desktop rows for thumb comfort. 52px exceeds 44px minimum — intentional for seniors.
Direction buttons — mobiletext-sm font-bold min-w-[40px] min-h-[40px] flex items-center justify-center rounded-sm border border-line40px × 40px minLarger touch target than desktop. Active: bg-brand-navy text-white border-brand-navy
+
+
+ + + +
+
+ 4 + Loading & empty states +
+ +
+ + +
+ Loading — search request in flight +
+
+
+
+
localhost:3000/?q=raddatz&sort=sender&dir=asc
+
+
+ + Dokumente + Personen +
+
+
+
+
+
+ +
+ raddatz +
+
+ Sortieren: + Absender ↑ + +
+
⊞ Filter
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Spinner (animated ring) replaces the search icon inside the input while the request is in-flight. Skeleton rows replace the document list. Sort button remains interactive during loading so users can change sort without waiting.
+
+ + +
+ Empty state — no results for query +
+
+
+
+
localhost:3000/?q=beethoven&sort=date&dir=desc
+
+
+ + Dokumente + Personen +
+
+
+
+
+
+ + beethoven +
+
+ Sortieren: + Datum ↓ + +
+
⊞ Filter
+
+
+
+ + +
+
🔍
+
Keine Dokumente gefunden
+
Keine Treffer für „beethoven". Versuche andere Begriffe oder entferne Filter.
+ Suche zurücksetzen +
+
+
+
+
Zero-results state shows an icon, a friendly headline, and the search term quoted back. Sort button dims to indicate it has no effect. A single CTA resets the search. Addresses #180 Problem 4 (empty state).
+
+
+ +
+
Implementation Reference — §4 Loading & empty states + Real values · mockup above is ~55% scale +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ElementTailwind classesReal sizeNotes
Spinner (inside input)w-4 h-4 rounded-full border-2 border-line border-t-brand-navy shrink-0 animate-spin16px × 16pxReplaces search icon during isLoading state. aria-label="Suche läuft" on a visually hidden span. Add let isLoading = $state(false) to +page.svelte; set true on goto(), false when data reactive updates.
Skeleton rowbg-surface border border-line rounded-sm px-4 py-3 mb-2 flex flex-col gap-2py 12pxTwo <div class="h-3 rounded bg-muted animate-pulse"> lines (80% and 50% width). 3 skeleton rows sufficient.
Sort button — no resultsAdd opacity-40 pointer-events-none when result count is 0Dimmed to signal no effect. Do not disable entirely — allow user to change sort to try different result order.
Empty state containerbg-surface border border-line rounded-sm py-16 px-6 flex flex-col items-center text-centerpy 64pxOnly rendered when !isLoading && documents.length === 0 && !isDashboard
Empty state iconw-14 h-14 rounded-full bg-muted flex items-center justify-center text-2xl mb-456px × 56pxUse a search/magnifier SVG, not an emoji, in production
Empty state headlinefont-serif text-xl font-bold text-ink mb-220px / 700"Keine Dokumente gefunden" — most commonly undersized on empty states
Empty state subtexttext-sm text-ink-3 max-w-xs leading-relaxed mb-514px, leading 1.625Quote the search term: „{q}". Paraglide key: docs_empty_state_text
Empty state CTAtext-sm font-bold text-brand-navy border border-brand-navy rounded-sm px-4 py-2 hover:bg-brand-navy hover:text-white transitionh ~40px<a href="/"> — no JS needed. Paraglide key: docs_btn_reset_title
+
+
+ + + +
+
+ 5 + Backend & data wiring — implementation checklist +
+ +
+
Implementation Reference — §5 Full-stack wiring + Not a visual section — agent checklist only +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
LayerWhat to changeDetailNotes
Frontend stateAdd sort + dir to +page.sveltelet sort = $state(untrack(() => data.filters?.sort || 'date'))
let dir = $state(untrack(() => data.filters?.dir || 'desc'))
Default: date/desc. Omit from URL when default to keep URLs clean.
Frontend search triggerAdd sort + dir to triggerSearch()if (sort !== 'date' || dir !== 'desc') { params.set('sort', sort); params.set('dir', dir); }Only append if non-default — avoids polluting the URL for most users.
Frontend server loadRead sort + dir in +page.server.tsconst sort = url.searchParams.get('sort') || 'date';
const dir = url.searchParams.get('dir') || 'desc';
Pass to /api/documents/search query params. Add to the returned filters object for sync.
Frontend sync effectSync sort + dir in the $effect that syncs filter state from datasort = data.filters?.sort || 'date';
dir = data.filters?.dir || 'desc';
Keeps sort state consistent on browser back/forward navigation.
SearchFilterBar propsAdd sort + dir as bindable propssort = $bindable('date'), dir = $bindable('desc')Pass bind:sort + bind:dir from +page.svelte.
Backend endpointExtend GET /api/documents/searchAdd query params: sort (string, default "date") and dir (string, default "desc")Map values: date→documentDate, title→title, sender→sender.lastName, receiver→receivers[0].lastName, tag→tags[0].name, uploaded→createdAt
Backend sort mappingIn DocumentService.search(), add Sort parameterBuild Sort sort = Sort.by(direction, field). Pass to repository.findAll(spec, pageable) or equivalent query method.Sender/receiver sort requires a JOIN — use @Query with explicit ORDER BY or a Specification with Join. Tag sort: sort by first tag name alphabetically.
API type regenerationRun npm run generate:api after backend changesRequires backend running with --spring.profiles.active=devThe new sort + dir params must appear in the OpenAPI spec for the typed client to pick them up.
i18n keys neededAdd to messages/de.json, en.json, es.jsondocs_sort_label, docs_sort_date, docs_sort_title, docs_sort_sender, docs_sort_receiver, docs_sort_tag, docs_sort_uploaded, docs_sort_dir_asc, docs_sort_dir_desc, docs_empty_state_textAlso update docs_search_placeholder to "Titel, Personen, Tags durchsuchen …" to address #180 Problem 2.
+
+
+ +
+ + diff --git a/docs/specs/sort-integration-spec.html b/docs/specs/sort-integration-spec.html new file mode 100644 index 00000000..700774d9 --- /dev/null +++ b/docs/specs/sort-integration-spec.html @@ -0,0 +1,1291 @@ + + + + + +Sort Integration — 4 Exploration Variants · Familienarchiv #180 + + + +
+ + +
+
+
+

Sort Integration — 4 Exploration Variants

+

Four distinct approaches for adding sort controls to the document search. Issue #180 (Problem 1). Each variant has a different placement philosophy — choose based on the user model you want to reinforce. Not a final spec; pick one and lock it down.

+
+ Exploration · Pick One +
+
+
+
Issue
+
#180 — Sort & Search UX
+
+
+
Sort options (6)
+
Datum · Titel · Absender · Empfänger · Tag · Hochgeladen
+
+
+
Direction
+
Aufsteigend ↑ / Absteigend ↓ per option
+
+
+
New URL params
+
?sort=date&dir=desc
+
+
+
+ + +
+ 📐 Mockup scale notice — all font-size, height, and padding values + in the mockup CSS are scaled to ~55% of actual implementation values. + Do not copy sizes from mockup CSS. Use the ⚙ Implementation + Reference tables after each section. +
+ + + +
+
+ A + Inline sort in search bar row + Sort dropdown sits directly in row 1 — always visible, no extra space +
+ +
+
+

Pros

+
    +
  • Always visible — zero discovery friction
  • +
  • Sort and search feel like one cohesive control surface
  • +
  • Familiar pattern (GitHub Issues, Notion tables)
  • +
  • Active state is immediately legible without opening anything
  • +
+
+
+

Cons

+
    +
  • Row 1 gets crowded at 320px — needs a wrapping strategy
  • +
  • Sort button competes visually with Filter button
  • +
  • Dropdown overlaps results on short viewports
  • +
+
+
+

Best when

+
    +
  • Sort is a primary, frequently changed action
  • +
  • Users already understand the search bar as a control hub
  • +
  • You want to minimize total UI surface area
  • +
+
+
+ +
+ + +
+
Desktop — dropdown open
+
+
+
+
localhost:3000/?q=brief&sort=date&dir=desc
+
+
+ + Dokumente + Personen +
+
+
+
+
+ +
+
+ brief +
+ +
+
+ Sortieren: + Datum ↓ + +
+ +
+
+ Datum +
+ + +
+
+
+ Titel +
+ + +
+
+
+ Absender +
+ + +
+
+
+ Empfänger +
+ + +
+
+
+ Tag +
+ + +
+
+
+ Hochgeladen +
+ + +
+
+
+
+ +
+ Filter + +
+ +
+
+
+ + +
+
12 Dokumente gefunden
+
+ + +
+
+
PDF
+
+
Brief an Tante Klara, Weihnachten 1954
+
+ 15. Dezember 1954 + · + Ernst Raddatz → Klara Meier + · + Briefe +
+
+
+
+
PDF
+
+
Briefwechsel Sommer 1961
+
+ 3. Juli 1961 + · + Hildegard Raddatz → Stadtamt + · + Verwaltung +
+
+
+
+
+
+
Sort button sits between search input and Filter button. Dropdown shows all 6 options with per-option ↑/↓ direction toggle. Active option highlighted in navy.
+
+ + +
+
Mobile 320px — collapsed (no open dropdown)
+
+
+ 9:41 +
+
+
+ +
+
+
+
+ +
+
+
+ brief +
+
+
+ +
+
+ Datum ↓ +
+
+ ⊞ Filter +
+
+
+
12 Dokumente gefunden
+
+
PDF
+
+
+
+
+
+
+
PDF
+
+
+
+
+
+
+
+
On mobile, sort wraps to a second row inside the search card. Both sort and filter become equal-width buttons — same touch target, consistent layout.
+
+
+ +
+
Implementation Reference — Variant A: Inline Sort + Real values · mockup above is ~55% scale · do not copy mockup CSS +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ElementTailwind classesReal sizeNotes
Sort trigger buttonh-10 flex items-center gap-2 border border-line px-3 text-sm font-bold text-ink-2 bg-muted hover:bg-surface transition whitespace-nowrap rounded-smh 40px, px 12pxActive state: border-brand-navy text-brand-navy bg-surface
Sort prefix labeltext-xs font-normal text-ink-3 mr-112px / 400"Sortieren:" prefix — hide on mobile with hidden sm:inline
Sort dropdown containerabsolute top-full left-0 z-50 min-w-[180px] bg-surface border border-brand-navy border-t-0 shadow-lg rounded-b-smmin-w 180pxOpens below button; close on outside click via onpointerdown listener
Dropdown option rowflex items-center px-3 py-2.5 gap-3 border-b border-line cursor-pointer hover:bg-muted last:border-b-0h ~40px, py 10pxTouch target ≥ 44px — most commonly undersized element
Dropdown option labelflex-1 text-sm font-semibold text-ink14px / 600Active row: text-brand-navy + row background bg-blue-50
Direction toggle (↑/↓)flex gap-1 — each button: text-xs font-bold px-1.5 py-0.5 rounded-sm border border-transparent text-ink-324px touch area minActive direction: bg-brand-navy text-white border-brand-navy
Mobile: sort + filter rowflex gap-2 mt-2 — each button adds flex-1 justify-centerh 40px, flex-1Wraps to second line inside .SCARD below sm. Both buttons equal width.
New URL paramssort + dirValues: sort=date|title|sender|receiver|tag|uploaded, dir=asc|desc. Add to triggerSearch() in +page.svelte and to /api/documents/search query params.
+
+
+ + + +
+
+ B + Sort pill strip below search card + A horizontally scrollable row of pills between search and results +
+ +
+
+

Pros

+
    +
  • All 6 options visible at a glance — no dropdown needed
  • +
  • Active sort is unmistakable (navy pill, always in view)
  • +
  • Natural horizontal scroll on mobile — familiar to users
  • +
  • Search bar stays clean and single-purpose
  • +
+
+
+

Cons

+
    +
  • Adds a full row of vertical space even when sort isn't used
  • +
  • 6 options may overflow without scroll cue on narrow screens
  • +
  • Seniors may not discover horizontal scroll on mobile
  • +
+
+
+

Best when

+
    +
  • Sort is a secondary action but users switch it often
  • +
  • You want sort visible without modal/dropdown overhead
  • +
  • The team prefers a "tab strip" mental model over a select
  • +
+
+
+ +
+ + +
+
Desktop — "Absender" active, ascending
+
+
+
+
localhost:3000/?q=brief&sort=sender&dir=asc
+
+
+ + Dokumente + Personen +
+
+
+ +
+
+
+
+ brief +
+
⊞ Filter
+
+
+
+ + +
+ Sortieren: +
Datum
+
Titel
+
Absender ↑
+
Empfänger
+
Tag
+
Hochgeladen
+ +
+
↑ A–Z
+
↓ Z–A
+
+ + +
+
12 Dokumente, sortiert nach Absender ↑
+
+ + +
+
+
PDF
+
+
Brief an Tante Klara, Weihnachten 1954
+
+ Ernst Raddatz + · + 15. Dez 1954 + · + Briefe +
+
+
+
+
PDF
+
+
Urlaubspostkarte 1962
+
+ Hildegard Raddatz + · + 8. Aug 1962 + · + Karten +
+
+
+
+
+
+
Pill strip lives between search card and results as its own card. Active pill is navy-filled. Direction buttons (↑/↓) are separate toggle at the right end. Result count confirms active sort.
+
+ + +
+
Mobile 320px — horizontal scroll
+
+
+ 9:41 +
+
+
+ +
+
+
+
+
+
+ brief +
+
+
+
+
+ +
+
Datum ↓
+
Absender ↑
+
Empfänger
+
Tag…
+
+
12 Dokumente, Absender ↑
+
+
PDF
+
+
+
+
PDF
+
+
+
+
+
On mobile the pill row clips at screen edge — the half-visible last pill cues horizontal scroll. Direction toggle moves inside the active pill label (↑/↓ suffix). Min touch target: 44px height enforced in implementation.
+
+
+ +
+ Senior accessibility note: Horizontal scroll is invisible on desktop and may be missed by seniors on tablet. Consider adding a faint right-fade gradient mask (-webkit-mask-image: linear-gradient(to right, #000 80%, transparent)) to the pill container to signal overflow. +
+ +
+
Implementation Reference — Variant B: Sort Pill Strip + Real values · mockup above is ~55% scale +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ElementTailwind classesReal sizeNotes
Pill strip containerflex items-center gap-2 overflow-x-auto bg-surface border border-line rounded-sm px-4 py-3 shadow-sm scroll-smoothh ~48px, py 12pxAdd scrollbar-hide (Tailwind plugin) or ::-webkit-scrollbar { display: none }. Right-fade mask on mobile.
"Sortieren:" labeltext-xs font-bold uppercase tracking-widest text-ink-3 shrink-0 mr-112px / 700Hide on mobile: hidden sm:block
Inactive pillh-8 shrink-0 flex items-center gap-1 px-3 rounded-full border border-line text-sm font-semibold text-ink-2 cursor-pointer hover:border-brand-navy hover:text-brand-navy transition whitespace-nowraph 32px, px 12pxTouch target: wrap in min-h-[44px] flex items-center on mobile. Most commonly undersized element.
Active pillInactive classes + bg-brand-navy text-white border-brand-navyh 32pxShows direction suffix: "Absender ↑". Screen reader: aria-pressed="true"
Direction toggles (↑/↓)shrink-0 h-8 flex items-center gap-1 px-3 border border-line text-sm font-bold text-ink-2 rounded-sm cursor-pointer hover:border-brand-navy transitionh 32px, px 12pxActive: border-brand-navy text-brand-navy. aria-label="Aufsteigend" / "Absteigend"
Separatorw-px h-5 bg-line shrink-0 mx-11px × 20pxDivides pills from direction buttons. role="separator" aria-hidden="true"
Result count confirmationtext-sm font-medium text-ink-2 mb-214pxText: "12 Dokumente, sortiert nach Absender ↑". Live region: aria-live="polite"
+
+
+ + + +
+
+ C + Sort in the result-list header + Sort lives above the document list, co-located with result count — zero search bar pollution +
+ +
+
+

Pros

+
    +
  • Search bar stays completely clean — single responsibility
  • +
  • Sort sits next to what it affects — contextually obvious
  • +
  • Result count + sort in one bar = efficient use of space
  • +
  • Pattern familiar from e-commerce (Amazon, Zalando)
  • +
+
+
+

Cons

+
    +
  • Sort is below the fold on mobile — users may not scroll to find it
  • +
  • Only visible in search mode (disappears on dashboard)
  • +
  • Seniors may not understand why sort isn't near the search input
  • +
+
+
+

Best when

+
    +
  • Sort is considered a result-manipulation tool, not a search param
  • +
  • The result list is long enough to justify a persistent header
  • +
  • You want a clean aesthetic in the search bar at all costs
  • +
+
+
+ +
+ + +
+
Desktop — sort dropdown open
+
+
+
+
localhost:3000/?q=brief&sort=date&dir=desc
+
+
+ + Dokumente + Personen +
+
+
+ +
+
+
+
+ brief +
+
⊞ Filter
+
+
+
+ + +
+
12 Dokumente gefunden
+
+ +
+ Sortieren: + Datum ↓ + +
+ +
+
+ Datum +
+ + +
+
+
+ Titel +
+
+
+ Absender +
+
+
+ Empfänger +
+
+
+ Tag +
+
+
+ Hochgeladen +
+
+
+
+
+ + +
+
+
PDF
+
+
Brief an Tante Klara, Weihnachten 1954
+
15. Dez 1954·Ernst Raddatz·Briefe
+
+
+
+
PDF
+
+
Briefwechsel Sommer 1961
+
3. Jul 1961·Hildegard Raddatz·Verwaltung
+
+
+
+
+
+
Result header is its own card above the document list. Left: result count. Right: sort dropdown trigger. Sort dropdown opens upward on mobile if needed. This variant only renders during search — not on dashboard.
+
+ + +
+
Mobile 320px — result header sticky
+
+
+ 9:41 +
+
+
+ +
+
+
+
+
+
brief
+
+
+
+
+ +
+ 12 Dok. +
+ Sort: + Datum ↓ +
+
+
+
PDF
+
+
+
+
PDF
+
+
+
+
+
Result header is position: sticky; top: 0 on mobile so it stays in view while scrolling the document list. Sort dropdown opens upward (bottom: 100%) to avoid viewport overflow.
+
+
+ +
+
Implementation Reference — Variant C: Result-Header Sort + Real values · mockup above is ~55% scale +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ElementTailwind classesReal sizeNotes
Result header barflex items-center gap-3 px-4 py-3 bg-surface border border-line rounded-sm shadow-sm sticky top-0 z-20h ~48px, py 12pxSticky on mobile. Only rendered when !isDashboard — wrap in {#if !data.isDashboard} in +page.svelte
Result count textflex-1 text-sm font-medium text-ink-214px / 500<strong class="text-brand-navy">12</strong> Dokumente gefunden. aria-live="polite"
Sort trigger (result header)h-9 flex items-center gap-2 border border-line px-3 text-sm font-bold text-ink-2 bg-surface rounded-sm hover:border-brand-navy transition whitespace-nowraph 36px, px 12pxActive: border-brand-navy text-brand-navy. Touch target min 44px: add min-h-[44px] on mobile.
Sort dropdown (result header)absolute bottom-full right-0 mb-1 z-50 min-w-[180px] bg-surface border border-brand-navy shadow-lg rounded-smmin-w 180pxOpens upward (bottom-full) on mobile to stay in viewport. Same option rows as Variant A.
Dropdown option rowflex items-center px-3 py-2.5 gap-3 border-b border-line last:border-b-0 cursor-pointer hover:bg-mutedh ~40pxTouch target ≥ 44px — most commonly undersized
Direction toggleSame as Variant A direction toggleReuse same component
Svelte placementAdd ResultHeader.svelte component. Render between SearchFilterBar and DocumentList in +page.svelte, only when !data.isDashboard.
+
+
+ + + +
+
+ D + Sort inside the advanced filter panel + Sort lives at the top of the collapsible "Filter" section — hidden by default +
+ +
+
+

Pros

+
    +
  • Zero additional chrome when filters are closed
  • +
  • Logical grouping: "filtering and sorting" as one concept
  • +
  • Reveals naturally when filter badge shows active sort
  • +
  • Works well if sort is rarely changed once set
  • +
+
+
+

Cons

+
    +
  • Discoverability is low — users must click "Filter" to find sort
  • +
  • Seniors may not think to look inside "Filter" for sort
  • +
  • Active sort is invisible when filter panel is collapsed
  • +
+
+
+

Best when

+
    +
  • Sort is rarely changed (users set it once and leave it)
  • +
  • You want to keep the primary UI completely minimal
  • +
  • The Filter button shows an active-state badge when sort is non-default
  • +
+
+
+ +
+ + +
+
Desktop — advanced filter open, sort section visible
+
+
+
+
localhost:3000/?q=brief&sort=sender&dir=asc
+
+
+ + Dokumente + Personen +
+
+
+
+ +
+
+
+ brief +
+ +
+ ⊞ Filter + 1 + +
+
+
+ + +
+ + +
+
Sortieren nach
+
+
Datum
+
Titel
+
Absender
+
Empfänger
+
Tag
+
Hochgeladen
+
+
+
↑ Aufsteigend
+
↓ Absteigend
+
+
+ + +
+
+
Absender
+
Person wählen…
+
+
+
Empfänger
+
Person wählen…
+
+
+
Von
+
Datum…
+
+
+
Bis
+
Datum…
+
+
+
+
+ + +
12 Dokumente, sortiert nach Absender ↑
+
+
+
PDF
+
+
Brief an Tante Klara, Weihnachten 1954
+
Ernst Raddatz·15. Dez 1954·Briefe
+
+
+
+
+
+
Sort sits at the top of the advanced filter panel — above Absender, Empfänger, dates. When sort is non-default, the Filter button shows a navy badge with the count of active modifiers (sort + any active filters).
+
+ + +
+
Desktop — filter closed, active badge visible
+
+
+
+
localhost:3000/?q=brief&sort=sender&dir=asc
+
+
+ + Dokumente + Personen +
+
+
+
+
+
+
+ brief +
+
+ ⊞ Filter (1) + +
+
+
+
+ +
12 Dokumente, sortiert nach Absender ↑
+
+
+
PDF
+
+
Brief an Tante Klara, Weihnachten 1954
+
Ernst Raddatz·15. Dez 1954·Briefe
+
+
+
+
PDF
+
+
Briefwechsel Sommer 1961
+
Hildegard Raddatz·3. Jul 1961·Verwaltung
+
+
+
+
+
+
Filter button shows "(1)" suffix when sort is non-default. The result count line confirms active sort. This is the only surface that signals sort is active when the panel is collapsed — the discoverability weakness of this variant.
+
+
+ +
+ Discoverability mitigation: If choosing Variant D, the Filter button label should read "Filter (1)" (or show a dot badge) whenever sort is non-default. Additionally, the result-count line should always name the active sort: "12 Dokumente, Absender ↑". These two surfaces together are the only visual hints that a sort is active when the panel is closed. +
+ +
+
Implementation Reference — Variant D: Sort in Advanced Filter Panel + Real values · mockup above is ~55% scale +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ElementTailwind classesReal sizeNotes
Sort section in panelborder-b border-line pb-5 mb-5First child inside the transition:slide panel in SearchFilterBar.svelte
Sort section labeltext-xs font-bold uppercase tracking-widest text-ink-2 mb-3 block12px / 700"SORTIEREN NACH"
Sort option pills rowflex flex-wrap gap-2 mb-3Wraps on mobile. 6 pills total.
Inactive sort pillh-9 flex items-center px-4 rounded-full border border-line text-sm font-semibold text-ink-2 cursor-pointer hover:border-brand-navy transition whitespace-nowraph 36px, px 16pxTouch target: min-h-[44px] on mobile. Most commonly undersized.
Active sort pillSame + bg-brand-navy text-white border-brand-navyaria-pressed="true"
Direction rowflex gap-2Two buttons: "↑ Aufsteigend" / "↓ Absteigend"
Direction buttonh-9 flex items-center gap-1.5 px-4 border border-line text-sm font-bold text-ink-2 rounded-sm cursor-pointer hover:border-brand-navy transition whitespace-nowraph 36pxActive: bg-brand-navy text-white border-brand-navy
Filter button badge countInline "(N)" text suffix or: absolute -top-1.5 -right-1.5 w-4 h-4 rounded-full bg-brand-navy text-white text-[10px] font-bold flex items-center justify-center border-2 border-surface16px badgeCount = number of active modifiers: active sort (if non-default) + active filter fields. Computed in +page.svelte.
+
+
+ + + +
+
+ + Variant comparison — pick one +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CriterionA — InlineB — Pill stripC — Result headerD — In filter panel
DiscoverabilityExcellent — always in viewGood — visible stripMedium — below searchLow — hidden by default
Senior usabilityGood — labeled buttonMedium — scroll may confuseMedium — must scroll to findLow — requires two taps to find
Mobile footprintGood — wraps to row 2Medium — extra full rowExcellent — no extra row in search cardExcellent — zero chrome when closed
Search bar clarityMedium — adds sort button to row 1Good — search bar unchangedExcellent — search bar 100% cleanExcellent — search bar 100% clean
Active state visibilityExcellent — button label changesExcellent — active pill always visibleGood — result header shows itLow — only via badge + result count
Implementation complexityMedium — dropdown with dir toggleLow — pills + 2 buttons, no dropdownMedium — new ResultHeader componentLow — extends existing filter panel
Keyboard / screen readerMedium — dropdown needs focus trapExcellent — no dropdown, simple buttonsMedium — same dropdown focus trapExcellent — inside existing panel flow
Recommended for this project★ Primary recommendationStrong alternative if row 1 feels crowdedSuitable only if sort is secondaryNot recommended — discoverability gap
+ +
+ Recommendation — Variant A (Inline) with Variant B (Pill strip) as fallback: +
    +
  • Variant A puts sort at 0-click distance, which matters most for seniors and first-time users
  • +
  • If the team finds row 1 too dense (especially at 375px), switch to Variant B — it keeps all 6 options visible without a dropdown and has the cleanest keyboard flow
  • +
  • Variant C is acceptable if the product decision is that sort is a result-manipulation tool (not a search param). Make the result header sticky so it stays accessible while scrolling
  • +
  • Variant D is not recommended: the discoverability deficit is too severe for the senior audience. The badge workaround is a patch, not a solution
  • +
+
+
+ +
+ +