From d9be001f1f55d0ad664285f3d83360c76f52cf46 Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 19 May 2026 21:03:13 +0200 Subject: [PATCH] feat(transcription): wire dropdown search input to editor @-text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For issue #380. The search input mirrors the @-text the user types until the user takes ownership by typing into the input itself. After that, the input owns its own state and editor typing no longer overrides it. Two empty states now exist: - "Namen eingeben…" when the search input is empty (AC-4) - "Keine Personen gefunden" when the search input has a query but the list is empty (existing behavior) The dropdown reads editorQuery through the shared $state proxy via a getter prop, matching the established pattern for model.items. Co-Authored-By: Claude Opus 4.7 --- .../shared/discussion/MentionDropdown.svelte | 35 ++++++++++++++----- .../discussion/MentionDropdown.svelte.spec.ts | 31 +++++++++++++--- .../discussion/PersonMentionEditor.svelte | 21 +++++++++-- .../PersonMentionEditor.svelte.spec.ts | 15 ++++++++ 4 files changed, 86 insertions(+), 16 deletions(-) diff --git a/frontend/src/lib/shared/discussion/MentionDropdown.svelte b/frontend/src/lib/shared/discussion/MentionDropdown.svelte index 07c0f628..5e6c2157 100644 --- a/frontend/src/lib/shared/discussion/MentionDropdown.svelte +++ b/frontend/src/lib/shared/discussion/MentionDropdown.svelte @@ -20,17 +20,25 @@ type DropdownState = { let { model, - initialQuery = '', + editorQuery = '', onSearch = () => {} }: { model: DropdownState; - initialQuery?: string; + /** Text typed after `@` in the host editor. Mirrors into the search input + * until the user takes manual ownership by typing into the input itself. */ + editorQuery?: string; onSearch?: (query: string) => void; } = $props(); -// initialQuery is a one-shot prop — PersonMentionEditor mounts a fresh dropdown -// with the typed text on each Tiptap onStart, so we deliberately snapshot here. -let searchQuery = $state(untrack(() => initialQuery)); +let searchQuery = $state(untrack(() => editorQuery)); +let userHasEdited = $state(false); + +// Mirror the editor's typed text until the user takes ownership. +$effect(() => { + if (!userHasEdited) { + searchQuery = editorQuery; + } +}); // highlightedIndex must be both writable (keyboard handler mutates it) and // reset when `items` changes (so it never points past the end of a new list). @@ -153,15 +161,24 @@ function selectItem(item: Person) { class="min-h-[44px] w-full bg-transparent font-sans text-sm text-ink placeholder:text-ink-3 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-navy focus-visible:ring-inset" placeholder={m.person_mention_search_prompt()} bind:value={searchQuery} - oninput={(e) => onSearch(e.currentTarget.value)} + oninput={(e) => { + userHasEdited = true; + onSearch(e.currentTarget.value); + }} onmousedown={(e) => e.stopPropagation()} /> {#if model.items.length === 0} -

- {m.person_mention_popup_empty()} -

+ {#if searchQuery.trim() === ''} +

+ {m.person_mention_search_prompt()} +

+ {:else} +

+ {m.person_mention_popup_empty()} +

+ {/if}