From e00e6ee8b74ba6158d9e9730d3e640e6144adffd Mon Sep 17 00:00:00 2001 From: Till JS Date: Mon, 23 Mar 2026 20:48:01 +0100 Subject: [PATCH] feat(shared-ui): add deferred search mode to QuickInputBar Instead of auto-searching on every keystroke (causing flickering loader), show two static options: "create task" and "search". Search only runs when explicitly selected, reducing API calls and improving UX. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../shared-ui/src/quick-input/InputBar.svelte | 92 ++++++++++++++++++- 1 file changed, 87 insertions(+), 5 deletions(-) diff --git a/packages/shared-ui/src/quick-input/InputBar.svelte b/packages/shared-ui/src/quick-input/InputBar.svelte index 236d2555c..b7a253ce1 100644 --- a/packages/shared-ui/src/quick-input/InputBar.svelte +++ b/packages/shared-ui/src/quick-input/InputBar.svelte @@ -65,6 +65,7 @@ placeholder?: string; emptyText?: string; searchingText?: string; + searchText?: string; createText?: string; appIcon?: string; /** Bottom offset from viewport bottom (default: '70px') */ @@ -75,6 +76,8 @@ hasFabLeft?: boolean; /** Enable context menu on right-click (default: true) */ enableContextMenu?: boolean; + /** Defer search until explicitly triggered (default: false). Shows create + search options instead of auto-searching. */ + deferSearch?: boolean; /** App-specific default options for context menu (e.g., calendars) */ defaultOptions?: DefaultOption[]; /** Currently selected default option ID */ @@ -100,12 +103,14 @@ placeholder = 'Suchen oder erstellen...', emptyText = 'Keine Ergebnisse gefunden', searchingText = 'Suche...', + searchText = 'Suchen', createText = 'Erstellen', appIcon = 'search', bottomOffset = '70px', hasFabRight = false, hasFabLeft = false, enableContextMenu = true, + deferSearch = false, defaultOptions = [], selectedDefaultId, defaultOptionLabel = 'Standard-Kalender', @@ -127,6 +132,8 @@ let isFocused = $state(false); let searchTimeout: ReturnType; let inputElement = $state(null); + // Whether search has been explicitly triggered in deferred mode + let searchTriggered = $state(false); // Context menu state let contextMenuVisible = $state(false); @@ -146,6 +153,12 @@ // Check if create option is selected (it's always first when available) let isCreateSelected = $derived(selectedIndex === 0 && createPreview !== null); + // In deferred mode: search option index is right after create (or 0 if no create) + let searchOptionIndex = $derived(createPreview !== null ? 1 : 0); + let isSearchSelected = $derived( + deferSearch && !searchTriggered && selectedIndex === searchOptionIndex + ); + // Show panel only when there's actual input $effect(() => { showPanel = isFocused && searchQuery.trim().length > 0; @@ -184,6 +197,18 @@ settingsStore.refresh(); } + function handleInput() { + if (deferSearch) { + // In deferred mode: reset search state on new input, don't auto-search + searchTriggered = false; + results = []; + loading = false; + selectedIndex = 0; + } else { + handleSearch(); + } + } + async function handleSearch() { clearTimeout(searchTimeout); @@ -211,6 +236,12 @@ }, 150); } + async function triggerDeferredSearch() { + if (!searchQuery.trim()) return; + searchTriggered = true; + await handleSearch(); + } + async function handleCreate() { if (!onCreate || !searchQuery.trim() || creating) return; @@ -220,6 +251,7 @@ searchQuery = ''; results = []; selectedIndex = 0; + searchTriggered = false; onSearchChange?.('', []); // Keep focus for rapid entry inputElement?.focus(); @@ -235,6 +267,7 @@ event.preventDefault(); searchQuery = ''; results = []; + searchTriggered = false; onSearchChange?.('', []); inputElement?.blur(); return; @@ -252,8 +285,14 @@ if (event.key === 'ArrowDown') { event.preventDefault(); const hasCreate = createPreview !== null; - const maxIndex = (hasCreate ? 1 : 0) + results.length - 1; - selectedIndex = Math.min(selectedIndex + 1, Math.max(0, maxIndex)); + // In deferred mode before search: options are create + search + if (deferSearch && !searchTriggered) { + const maxIndex = hasCreate ? 1 : 0; + selectedIndex = Math.min(selectedIndex + 1, maxIndex); + } else { + const maxIndex = (hasCreate ? 1 : 0) + results.length - 1; + selectedIndex = Math.min(selectedIndex + 1, Math.max(0, maxIndex)); + } return; } @@ -266,6 +305,11 @@ if (event.key === 'Enter') { event.preventDefault(); if (searchQuery.trim()) { + // If search option is selected in deferred mode + if (isSearchSelected) { + triggerDeferredSearch(); + return; + } // If create option is selected if (isCreateSelected && onCreate) { handleCreate(); @@ -376,7 +420,31 @@ {/if} - {#if loading} + + {#if deferSearch && !searchTriggered} + + {:else if loading}
{searchingText} @@ -390,7 +458,7 @@ Suchergebnisse
{#each results as item, index (item.id)} - {@const adjustedIndex = createPreview ? index + 1 : index} + {@const adjustedIndex = index + (createPreview ? 1 : 0)}