diff --git a/src/lib/components/ui/CharacterTypeahead.svelte b/src/lib/components/ui/CharacterTypeahead.svelte index 8aab0375..4e30f77a 100644 --- a/src/lib/components/ui/CharacterTypeahead.svelte +++ b/src/lib/components/ui/CharacterTypeahead.svelte @@ -5,6 +5,7 @@ import Svelecte from 'svelecte' import Icon from '../Icon.svelte' import { searchAdapter, type SearchResult } from '$lib/api/adapters/search.adapter' + import { getCharacterImage } from '$lib/utils/images' interface CharacterOption { id: string @@ -46,39 +47,42 @@ contained = false }: Props = $props() - // Initialize selectedOption from initialCharacter if provided - function initializeFromCharacter() { - if (initialCharacter && value) { - return { + let searchResults = $state([]) + // Only used when user selects something NEW (different from initialCharacter) + let userSelectedOption = $state(null) + let isLoading = $state(false) + let searchTimeout: ReturnType | null = null + + // Clear userSelectedOption when value is cleared + $effect(() => { + if (!value) { + userSelectedOption = null + } + }) + + // Derive options: include initialCharacter or userSelectedOption so Svelecte can find the value + const options = $derived.by(() => { + const results = [...searchResults] + + // If user selected something new, prioritize that + const userSelected = userSelectedOption + if (userSelected && !results.find((o) => o.granblueId === userSelected.granblueId)) { + return [userSelected, ...results] + } + + // Otherwise, include initialCharacter if we have a value matching it + if (value && initialCharacter && initialCharacter.granblueId === value) { + const initOption: CharacterOption = { id: initialCharacter.id, label: initialCharacter.name, granblueId: initialCharacter.granblueId } + if (!results.find((o) => o.granblueId === initOption.granblueId)) { + return [initOption, ...results] + } } - return null - } - let searchResults = $state([]) - let selectedOption = $state(initializeFromCharacter()) - let isLoading = $state(false) - let searchTimeout: ReturnType | null = null - - // Update selectedOption when initialCharacter changes or value is cleared - $effect(() => { - if (!value) { - selectedOption = null - } else if (initialCharacter && !selectedOption) { - selectedOption = initializeFromCharacter() - } - }) - - // Combine search results with the selected option so Svelecte can always find it - const options = $derived.by(() => { - const selected = selectedOption - if (selected && !searchResults.find((o) => o.granblueId === selected.granblueId)) { - return [selected, ...searchResults] - } - return searchResults + return results }) const typeaheadClasses = $derived( @@ -132,7 +136,12 @@ function handleChange(selected: CharacterOption | null) { const newValue = selected?.granblueId || null value = newValue - selectedOption = selected + // Only track as userSelectedOption if it's different from initialCharacter + if (selected && initialCharacter && selected.granblueId === initialCharacter.granblueId) { + userSelectedOption = null // Use initialCharacter instead + } else { + userSelectedOption = selected + } onValueChange?.(newValue) } @@ -153,6 +162,30 @@ {#snippet toggleIcon(dropdownShow)} {/snippet} + {#snippet option(opt)} + {@const char = opt as CharacterOption} +
+ + {char.label} +
+ {/snippet} + {#snippet selection(sel)} + {@const char = (sel as CharacterOption[])[0]} + {#if char} +
+ + {char.label} +
+ {/if} + {/snippet} {#if isLoading} ... @@ -258,6 +291,47 @@ :global(.sv-btn-separator) { display: none; } + + // Custom option item styling + .option-item { + display: flex; + align-items: center; + gap: $unit; + } + + .option-image { + width: 24px; + height: 24px; + border-radius: $item-corner-small; + flex-shrink: 0; + } + + .option-label { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + // Custom selection item styling (shown in input when value selected) + .selection-item { + display: flex; + align-items: center; + gap: $unit-half; + } + + .selection-image { + width: 20px; + height: 20px; + border-radius: $item-corner-small; + flex-shrink: 0; + } + + .selection-label { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } } // Size variants