fix(korrespondenz): address 10 visual and functional regressions

- Strip full-bleed: remove max-w container, put strips at page level
- Remove page heading/subtitle above strip (not in spec)
- Swap button always visible (drop opacity-0, keep pointer-events-none)
- Korrespondent placeholder "Alle Korrespondenten" + label "— optional"
- Add placeholder prop to PersonTypeahead; add onfocused callback prop
- "Person suchen" button now focuses #senderId-search instead of no-op navigate
- Wire CorrespondentSuggestionsDropdown on correspondent field focus
- Hint bar: bold name via <strong>, year-only dates (no ISO strings)
- Asymmetry bar: use first name only to prevent label overflow

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-03-30 13:57:00 +02:00
parent 49f6b0a8c7
commit 0387e9f428
5 changed files with 83 additions and 64 deletions

View File

@@ -10,8 +10,10 @@ interface Props {
value?: string;
initialName?: string;
suggestedName?: string;
placeholder?: string;
restrictToCorrespondentsOf?: string;
onchange?: (value: string) => void;
onfocused?: () => void;
}
let {
@@ -20,8 +22,10 @@ let {
value = $bindable(''),
initialName = '',
suggestedName = '',
placeholder,
restrictToCorrespondentsOf,
onchange
onchange,
onfocused
}: Props = $props();
let searchTerm = $state(initialName);
@@ -79,6 +83,7 @@ function handleInput() {
}
function handleFocus() {
onfocused?.();
showDropdown = true;
if (restrictToCorrespondentsOf) {
const personId = untrack(() => restrictToCorrespondentsOf)!;
@@ -131,7 +136,7 @@ function clickOutside(node: HTMLElement) {
bind:value={searchTerm}
oninput={handleInput}
onfocus={handleFocus}
placeholder={m.comp_typeahead_placeholder()}
placeholder={placeholder ?? m.comp_typeahead_placeholder()}
class="mt-1 block w-full rounded-md border border-line p-2 shadow-sm focus:border-accent focus:ring-accent"
/>

View File

@@ -76,53 +76,49 @@ function swapPersons() {
}
function selectPerson(id: string) {
if (!id) {
document.querySelector<HTMLInputElement>('#senderId-search')?.focus();
return;
}
senderId = id;
receiverId = '';
applyFilters();
}
</script>
<div class="mx-auto max-w-3xl px-4 py-8">
<!-- Page Header -->
<div class="mb-6 border-b border-[#E0DDD6] pb-4">
<h1 class="font-serif text-2xl font-semibold text-[#002850]">{m.conv_heading()}</h1>
<p class="mt-1 font-sans text-sm text-[#666]">
{m.conv_subtitle()}
</p>
</div>
<!-- Strip: Row 1 — full width, no container -->
<CorrespondenzPersonBar
bind:senderId={senderId}
bind:receiverId={receiverId}
initialSenderName={data.initialValues.senderName}
initialReceiverName={data.initialValues.receiverName}
onapplyFilters={applyFilters}
onswapPersons={swapPersons}
/>
<!-- Filter strip: Row 1persons -->
<CorrespondenzPersonBar
bind:senderId={senderId}
bind:receiverId={receiverId}
initialSenderName={data.initialValues.senderName}
initialReceiverName={data.initialValues.receiverName}
onapplyFilters={applyFilters}
onswapPersons={swapPersons}
<!-- Strip: Row 2full width -->
<CorrespondenzFilterControls
senderId={senderId}
bind:fromDate={fromDate}
bind:toDate={toDate}
bind:sortDir={sortDir}
documentCount={data.documents.length}
onapplyFilters={applyFilters}
ontoggleSort={toggleSort}
/>
<!-- Single-person hint bar -->
{#if isSinglePerson}
<SinglePersonHintBar
senderName={senderName}
fromDate={fromDate || undefined}
toDate={toDate || undefined}
sortDir={sortDir}
/>
{/if}
<!-- Filter strip: Row 2 — date/sort/count -->
<CorrespondenzFilterControls
senderId={senderId}
bind:fromDate={fromDate}
bind:toDate={toDate}
bind:sortDir={sortDir}
documentCount={data.documents.length}
onapplyFilters={applyFilters}
ontoggleSort={toggleSort}
/>
<!-- Single-person hint bar -->
{#if isSinglePerson}
<SinglePersonHintBar
senderName={senderName}
fromDate={fromDate || undefined}
toDate={toDate || undefined}
sortDir={sortDir}
/>
{/if}
<!-- Results -->
<!-- Content area with padding -->
<div class="px-[18px] py-[14px]">
{#if !senderId}
<CorrespondenzEmptyState onSelectPerson={selectPerson} />
{:else if data.documents.length === 0}

View File

@@ -51,6 +51,9 @@ const outPct = $derived(documents.length > 0 ? (outCount / documents.length) * 1
const isBilateral = $derived(!!senderId && !!receiverId);
const shortSenderName = $derived(senderName?.split(' ')[0] ?? senderName ?? '');
const shortReceiverName = $derived(receiverName?.split(' ')[0] ?? receiverName ?? '');
function statusDotClass(status: string): string {
const map: Record<string, string> = {
PLACEHOLDER: 'bg-yellow-400',
@@ -82,8 +85,8 @@ const newDocUrl = $derived(
aria-label="Briefverteilung in diesem Zeitraum: {outCount} von {senderName ?? ''}, {inCount} von {receiverName ?? ''}"
>
<div class="flex justify-between text-[10px] font-bold">
<span class="text-[#002850]">{outCount} von {senderName}</span>
<span class="text-[#0F5755]">{inCount} von {receiverName}</span>
<span class="text-[#002850]">{outCount} von {shortSenderName}</span>
<span class="text-[#0F5755]">{inCount} von {shortReceiverName}</span>
</div>
<div class="flex h-[5px] overflow-hidden rounded-full bg-[#E0DDD6]">
<div class="h-full bg-[#002850] transition-all" style="width: {outPct}%"></div>

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import PersonTypeahead from '$lib/components/PersonTypeahead.svelte';
import CorrespondentSuggestionsDropdown from './CorrespondentSuggestionsDropdown.svelte';
interface Props {
senderId?: string;
@@ -20,6 +21,18 @@ let {
}: Props = $props();
let swapVisible = $derived(!!(senderId && receiverId));
let showSuggestions = $state(false);
function handleCorrespondentFocused() {
if (senderId) showSuggestions = true;
}
function handleSuggestionSelect(id: string) {
receiverId = id;
showSuggestions = false;
onapplyFilters();
}
</script>
<div class="flex items-end gap-[9px] border-b border-[#EAE7E0] bg-white px-4 py-[9px] sm:px-[18px]">
@@ -42,7 +55,6 @@ let swapVisible = $derived(!!(senderId && receiverId));
aria-label="Personen tauschen"
onclick={onswapPersons}
class="mb-1 flex h-7 w-7 shrink-0 items-center justify-center rounded border border-[#D1D5DB] bg-white text-[#888] transition-colors hover:border-[#002850] hover:text-[#002850]"
class:opacity-0={!swapVisible}
class:pointer-events-none={!swapVisible}
tabindex={swapVisible ? 0 : -1}
>
@@ -65,23 +77,31 @@ let swapVisible = $derived(!!(senderId && receiverId));
<!-- Korrespondent field -->
<div
class="min-w-0 flex-1"
class="relative min-w-0 flex-1"
class:[&_input]:border-dashed={!receiverId}
class:[&_input]:border-solid={!!receiverId}
class:[&_input]:bg-[#F9F8F6]={!receiverId}
>
<PersonTypeahead
name="receiverId"
label={receiverId ? 'Korrespondent' : 'Korrespondent'}
label={receiverId ? 'Korrespondent' : 'Korrespondent — optional'}
bind:value={receiverId}
initialName={initialReceiverName}
placeholder="Alle Korrespondenten"
restrictToCorrespondentsOf={senderId || undefined}
onchange={() => onapplyFilters()}
onchange={() => {
showSuggestions = false;
onapplyFilters();
}}
onfocused={handleCorrespondentFocused}
/>
{#if !receiverId}
<span class="pointer-events-none absolute -mt-[1px] text-[11px] text-[#AAA] italic">
— optional
</span>
{#if showSuggestions && senderId && !receiverId}
<CorrespondentSuggestionsDropdown
senderId={senderId}
senderName=""
onselect={handleSuggestionSelect}
onclose={() => (showSuggestions = false)}
/>
{/if}
</div>
</div>

View File

@@ -1,10 +1,5 @@
<script lang="ts">
import {
conv_hint_single_person,
conv_hint_single_person_filtered,
conv_strip_sort_newest,
conv_strip_sort_oldest
} from '$lib/messages-extra';
import { conv_strip_sort_newest, conv_strip_sort_oldest } from '$lib/messages-extra';
interface Props {
senderName: string;
@@ -16,8 +11,9 @@ interface Props {
let { senderName, fromDate = '', toDate = '', sortDir = 'DESC' }: Props = $props();
let hasDateFilter = $derived(!!(fromDate || toDate));
let sortLabel = $derived(sortDir === 'ASC' ? conv_strip_sort_oldest() : conv_strip_sort_newest());
let fromYear = $derived(fromDate ? fromDate.substring(0, 4) : '');
let toYear = $derived(toDate ? toDate.substring(0, 4) : '');
</script>
<div
@@ -26,13 +22,12 @@ let sortLabel = $derived(sortDir === 'ASC' ? conv_strip_sort_oldest() : conv_str
<span class="text-sm" aria-hidden="true">📋</span>
{#if hasDateFilter}
{conv_hint_single_person_filtered({
name: senderName,
from: fromDate ?? '',
to: toDate ?? '',
sortLabel
})}
<strong>{senderName}</strong>
<span>·</span>
<span>{fromYear}{toYear}</span>
<span>·</span>
<span>{sortLabel}</span>
{:else}
{conv_hint_single_person({ name: senderName })}
Alle Briefe von <strong>{senderName}</strong> — wähle einen Korrespondenten oben um einzugrenzen
{/if}
</div>