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:
@@ -10,8 +10,10 @@ interface Props {
|
|||||||
value?: string;
|
value?: string;
|
||||||
initialName?: string;
|
initialName?: string;
|
||||||
suggestedName?: string;
|
suggestedName?: string;
|
||||||
|
placeholder?: string;
|
||||||
restrictToCorrespondentsOf?: string;
|
restrictToCorrespondentsOf?: string;
|
||||||
onchange?: (value: string) => void;
|
onchange?: (value: string) => void;
|
||||||
|
onfocused?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -20,8 +22,10 @@ let {
|
|||||||
value = $bindable(''),
|
value = $bindable(''),
|
||||||
initialName = '',
|
initialName = '',
|
||||||
suggestedName = '',
|
suggestedName = '',
|
||||||
|
placeholder,
|
||||||
restrictToCorrespondentsOf,
|
restrictToCorrespondentsOf,
|
||||||
onchange
|
onchange,
|
||||||
|
onfocused
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let searchTerm = $state(initialName);
|
let searchTerm = $state(initialName);
|
||||||
@@ -79,6 +83,7 @@ function handleInput() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleFocus() {
|
function handleFocus() {
|
||||||
|
onfocused?.();
|
||||||
showDropdown = true;
|
showDropdown = true;
|
||||||
if (restrictToCorrespondentsOf) {
|
if (restrictToCorrespondentsOf) {
|
||||||
const personId = untrack(() => restrictToCorrespondentsOf)!;
|
const personId = untrack(() => restrictToCorrespondentsOf)!;
|
||||||
@@ -131,7 +136,7 @@ function clickOutside(node: HTMLElement) {
|
|||||||
bind:value={searchTerm}
|
bind:value={searchTerm}
|
||||||
oninput={handleInput}
|
oninput={handleInput}
|
||||||
onfocus={handleFocus}
|
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"
|
class="mt-1 block w-full rounded-md border border-line p-2 shadow-sm focus:border-accent focus:ring-accent"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -76,53 +76,49 @@ function swapPersons() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function selectPerson(id: string) {
|
function selectPerson(id: string) {
|
||||||
|
if (!id) {
|
||||||
|
document.querySelector<HTMLInputElement>('#senderId-search')?.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
senderId = id;
|
senderId = id;
|
||||||
receiverId = '';
|
receiverId = '';
|
||||||
applyFilters();
|
applyFilters();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mx-auto max-w-3xl px-4 py-8">
|
<!-- Strip: Row 1 — full width, no container -->
|
||||||
<!-- Page Header -->
|
<CorrespondenzPersonBar
|
||||||
<div class="mb-6 border-b border-[#E0DDD6] pb-4">
|
bind:senderId={senderId}
|
||||||
<h1 class="font-serif text-2xl font-semibold text-[#002850]">{m.conv_heading()}</h1>
|
bind:receiverId={receiverId}
|
||||||
<p class="mt-1 font-sans text-sm text-[#666]">
|
initialSenderName={data.initialValues.senderName}
|
||||||
{m.conv_subtitle()}
|
initialReceiverName={data.initialValues.receiverName}
|
||||||
</p>
|
onapplyFilters={applyFilters}
|
||||||
</div>
|
onswapPersons={swapPersons}
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Filter strip: Row 1 — persons -->
|
<!-- Strip: Row 2 — full width -->
|
||||||
<CorrespondenzPersonBar
|
<CorrespondenzFilterControls
|
||||||
bind:senderId={senderId}
|
senderId={senderId}
|
||||||
bind:receiverId={receiverId}
|
bind:fromDate={fromDate}
|
||||||
initialSenderName={data.initialValues.senderName}
|
bind:toDate={toDate}
|
||||||
initialReceiverName={data.initialValues.receiverName}
|
bind:sortDir={sortDir}
|
||||||
onapplyFilters={applyFilters}
|
documentCount={data.documents.length}
|
||||||
onswapPersons={swapPersons}
|
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 -->
|
<!-- Content area with padding -->
|
||||||
<CorrespondenzFilterControls
|
<div class="px-[18px] py-[14px]">
|
||||||
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 -->
|
|
||||||
{#if !senderId}
|
{#if !senderId}
|
||||||
<CorrespondenzEmptyState onSelectPerson={selectPerson} />
|
<CorrespondenzEmptyState onSelectPerson={selectPerson} />
|
||||||
{:else if data.documents.length === 0}
|
{:else if data.documents.length === 0}
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ const outPct = $derived(documents.length > 0 ? (outCount / documents.length) * 1
|
|||||||
|
|
||||||
const isBilateral = $derived(!!senderId && !!receiverId);
|
const isBilateral = $derived(!!senderId && !!receiverId);
|
||||||
|
|
||||||
|
const shortSenderName = $derived(senderName?.split(' ')[0] ?? senderName ?? '');
|
||||||
|
const shortReceiverName = $derived(receiverName?.split(' ')[0] ?? receiverName ?? '');
|
||||||
|
|
||||||
function statusDotClass(status: string): string {
|
function statusDotClass(status: string): string {
|
||||||
const map: Record<string, string> = {
|
const map: Record<string, string> = {
|
||||||
PLACEHOLDER: 'bg-yellow-400',
|
PLACEHOLDER: 'bg-yellow-400',
|
||||||
@@ -82,8 +85,8 @@ const newDocUrl = $derived(
|
|||||||
aria-label="Briefverteilung in diesem Zeitraum: {outCount} von {senderName ?? ''}, {inCount} von {receiverName ?? ''}"
|
aria-label="Briefverteilung in diesem Zeitraum: {outCount} von {senderName ?? ''}, {inCount} von {receiverName ?? ''}"
|
||||||
>
|
>
|
||||||
<div class="flex justify-between text-[10px] font-bold">
|
<div class="flex justify-between text-[10px] font-bold">
|
||||||
<span class="text-[#002850]">{outCount} von {senderName} →</span>
|
<span class="text-[#002850]">{outCount} von {shortSenderName} →</span>
|
||||||
<span class="text-[#0F5755]">{inCount} von {receiverName} ←</span>
|
<span class="text-[#0F5755]">{inCount} von {shortReceiverName} ←</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex h-[5px] overflow-hidden rounded-full bg-[#E0DDD6]">
|
<div class="flex h-[5px] overflow-hidden rounded-full bg-[#E0DDD6]">
|
||||||
<div class="h-full bg-[#002850] transition-all" style="width: {outPct}%"></div>
|
<div class="h-full bg-[#002850] transition-all" style="width: {outPct}%"></div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import PersonTypeahead from '$lib/components/PersonTypeahead.svelte';
|
import PersonTypeahead from '$lib/components/PersonTypeahead.svelte';
|
||||||
|
import CorrespondentSuggestionsDropdown from './CorrespondentSuggestionsDropdown.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
senderId?: string;
|
senderId?: string;
|
||||||
@@ -20,6 +21,18 @@ let {
|
|||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let swapVisible = $derived(!!(senderId && receiverId));
|
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>
|
</script>
|
||||||
|
|
||||||
<div class="flex items-end gap-[9px] border-b border-[#EAE7E0] bg-white px-4 py-[9px] sm:px-[18px]">
|
<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"
|
aria-label="Personen tauschen"
|
||||||
onclick={onswapPersons}
|
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="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}
|
class:pointer-events-none={!swapVisible}
|
||||||
tabindex={swapVisible ? 0 : -1}
|
tabindex={swapVisible ? 0 : -1}
|
||||||
>
|
>
|
||||||
@@ -65,23 +77,31 @@ let swapVisible = $derived(!!(senderId && receiverId));
|
|||||||
|
|
||||||
<!-- Korrespondent field -->
|
<!-- Korrespondent field -->
|
||||||
<div
|
<div
|
||||||
class="min-w-0 flex-1"
|
class="relative min-w-0 flex-1"
|
||||||
class:[&_input]:border-dashed={!receiverId}
|
class:[&_input]:border-dashed={!receiverId}
|
||||||
class:[&_input]:border-solid={!!receiverId}
|
class:[&_input]:border-solid={!!receiverId}
|
||||||
class:[&_input]:bg-[#F9F8F6]={!receiverId}
|
class:[&_input]:bg-[#F9F8F6]={!receiverId}
|
||||||
>
|
>
|
||||||
<PersonTypeahead
|
<PersonTypeahead
|
||||||
name="receiverId"
|
name="receiverId"
|
||||||
label={receiverId ? 'Korrespondent' : 'Korrespondent'}
|
label={receiverId ? 'Korrespondent' : 'Korrespondent — optional'}
|
||||||
bind:value={receiverId}
|
bind:value={receiverId}
|
||||||
initialName={initialReceiverName}
|
initialName={initialReceiverName}
|
||||||
|
placeholder="Alle Korrespondenten"
|
||||||
restrictToCorrespondentsOf={senderId || undefined}
|
restrictToCorrespondentsOf={senderId || undefined}
|
||||||
onchange={() => onapplyFilters()}
|
onchange={() => {
|
||||||
|
showSuggestions = false;
|
||||||
|
onapplyFilters();
|
||||||
|
}}
|
||||||
|
onfocused={handleCorrespondentFocused}
|
||||||
/>
|
/>
|
||||||
{#if !receiverId}
|
{#if showSuggestions && senderId && !receiverId}
|
||||||
<span class="pointer-events-none absolute -mt-[1px] text-[11px] text-[#AAA] italic">
|
<CorrespondentSuggestionsDropdown
|
||||||
— optional
|
senderId={senderId}
|
||||||
</span>
|
senderName=""
|
||||||
|
onselect={handleSuggestionSelect}
|
||||||
|
onclose={() => (showSuggestions = false)}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import { conv_strip_sort_newest, conv_strip_sort_oldest } from '$lib/messages-extra';
|
||||||
conv_hint_single_person,
|
|
||||||
conv_hint_single_person_filtered,
|
|
||||||
conv_strip_sort_newest,
|
|
||||||
conv_strip_sort_oldest
|
|
||||||
} from '$lib/messages-extra';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
senderName: string;
|
senderName: string;
|
||||||
@@ -16,8 +11,9 @@ interface Props {
|
|||||||
let { senderName, fromDate = '', toDate = '', sortDir = 'DESC' }: Props = $props();
|
let { senderName, fromDate = '', toDate = '', sortDir = 'DESC' }: Props = $props();
|
||||||
|
|
||||||
let hasDateFilter = $derived(!!(fromDate || toDate));
|
let hasDateFilter = $derived(!!(fromDate || toDate));
|
||||||
|
|
||||||
let sortLabel = $derived(sortDir === 'ASC' ? conv_strip_sort_oldest() : conv_strip_sort_newest());
|
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>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -26,13 +22,12 @@ let sortLabel = $derived(sortDir === 'ASC' ? conv_strip_sort_oldest() : conv_str
|
|||||||
<span class="text-sm" aria-hidden="true">📋</span>
|
<span class="text-sm" aria-hidden="true">📋</span>
|
||||||
|
|
||||||
{#if hasDateFilter}
|
{#if hasDateFilter}
|
||||||
{conv_hint_single_person_filtered({
|
<strong>{senderName}</strong>
|
||||||
name: senderName,
|
<span>·</span>
|
||||||
from: fromDate ?? '',
|
<span>{fromYear}–{toYear}</span>
|
||||||
to: toDate ?? '',
|
<span>·</span>
|
||||||
sortLabel
|
<span>{sortLabel}</span>
|
||||||
})}
|
|
||||||
{:else}
|
{:else}
|
||||||
{conv_hint_single_person({ name: senderName })}
|
Alle Briefe von <strong>{senderName}</strong> — wähle einen Korrespondenten oben um einzugrenzen
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user