feat(search): case-appropriate disambiguation picker copy (#763)

A 1-item picker now reads "Meintest du …?" (a single direct match auto-selects
and never reaches the picker), while ≥2 keeps the "Person auswählen" framing.
The prompt lives in a visible, non-truncated panel heading (the trigger span
clips at 320px), and the "(auswählen…)" cue is dropped for the 1-item case.
DisambiguationPicker takes heading + showCue props; the page derives both from
ambiguousPersons.length. New search_disambiguation_did_you_mean key in de/en/es.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-06-07 01:14:25 +02:00
committed by marcel
parent f1bb9d3a69
commit 0ef4f4f07c
6 changed files with 80 additions and 25 deletions

View File

@@ -6,14 +6,24 @@ import type { components } from '$lib/generated/api';
type PersonHint = components['schemas']['PersonHint'];
let { persons, onSelect }: { persons: PersonHint[]; onSelect: (person: PersonHint) => void } =
$props();
let {
persons,
heading,
showCue,
onSelect
}: {
persons: PersonHint[];
heading: string;
showCue: boolean;
onSelect: (person: PersonHint) => void;
} = $props();
let open = $state(false);
let triggerEl = $state<HTMLButtonElement>();
let listEl = $state<HTMLUListElement>();
const panelId = 'disambiguation-panel';
const headingId = 'disambiguation-heading';
const names = $derived(persons.map((person) => person.displayName).join(', '));
async function openPicker() {
@@ -59,28 +69,31 @@ function onKeydown(event: KeyboardEvent) {
class="inline-flex min-h-[44px] items-center gap-1.5 rounded-full border border-line bg-muted px-3 text-sm text-ink-2 outline-none focus-visible:ring-2 focus-visible:ring-brand-navy"
>
<span class="max-w-[8rem] truncate sm:max-w-[12rem]">{names}</span>
<span class="text-ink-3">{m.search_disambiguation_cue()}</span>
{#if showCue}
<span class="text-ink-3">{m.search_disambiguation_cue()}</span>
{/if}
</button>
{#if open}
<ul
bind:this={listEl}
<div
id={panelId}
aria-label={m.search_disambiguation_heading()}
class="absolute left-0 z-10 mt-1 min-w-[12rem] rounded-sm border border-line bg-surface py-1 shadow-md"
>
{#each persons as person (person.id)}
<li>
<button
type="button"
aria-label={m.search_disambiguation_select_label({ name: person.displayName })}
onclick={() => select(person)}
class="flex min-h-[44px] w-full items-center px-4 text-left text-sm text-ink outline-none hover:bg-muted focus-visible:bg-muted focus-visible:ring-2 focus-visible:ring-brand-navy"
>
{person.displayName}
</button>
</li>
{/each}
</ul>
<p id={headingId} class="px-4 py-1.5 text-sm font-bold text-ink">{heading}</p>
<ul bind:this={listEl} aria-labelledby={headingId}>
{#each persons as person (person.id)}
<li>
<button
type="button"
aria-label={m.search_disambiguation_select_label({ name: person.displayName })}
onclick={() => select(person)}
class="flex min-h-[44px] w-full items-center px-4 text-left text-sm text-ink outline-none hover:bg-muted focus-visible:bg-muted focus-visible:ring-2 focus-visible:ring-brand-navy"
>
{person.displayName}
</button>
</li>
{/each}
</ul>
</div>
{/if}
</div>