chore: search filter animation improvement and reactive search cue example (#30866)

* chore: better animation for search form and search cue

* better search cue example

* chore: revert mise.lock changes
This commit is contained in:
Alex
2026-08-21 15:11:23 -05:00
committed by GitHub
parent a36fb67736
commit 7412079f88
4 changed files with 52 additions and 15 deletions
+1
View File
@@ -1845,6 +1845,7 @@
"search": "Search", "search": "Search",
"search_albums": "Search albums", "search_albums": "Search albums",
"search_by_context": "Search by context", "search_by_context": "Search by context",
"search_by_context_example": "Describe the photo you want to find, e.g. \"Children jumping on a trampoline\"",
"search_by_description": "Search by description", "search_by_description": "Search by description",
"search_by_description_example": "Hiking day in Sapa", "search_by_description_example": "Hiking day in Sapa",
"search_by_filename": "Search by file name or extension", "search_by_filename": "Search by file name or extension",
@@ -11,6 +11,7 @@
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import SearchFilters from './SearchFilters.svelte'; import SearchFilters from './SearchFilters.svelte';
import { searchManager } from '$lib/managers/search-manager.svelte'; import { searchManager } from '$lib/managers/search-manager.svelte';
import { getSearchTypePlaceholder } from './search-bar-utils';
type Props = { type Props = {
grayTheme: boolean; grayTheme: boolean;
@@ -19,6 +20,9 @@
let { grayTheme }: Props = $props(); let { grayTheme }: Props = $props();
let showClearIcon = $derived(searchManager.filter.query.length > 0); let showClearIcon = $derived(searchManager.filter.query.length > 0);
let placeholder = $derived(
searchStore.isSearchEnabled ? getSearchTypePlaceholder(searchManager.filter.queryType) : $t('search_your_photos'),
);
let input = $state<HTMLInputElement>(); let input = $state<HTMLInputElement>();
let searchFilters = $state<ReturnType<typeof SearchFilters>>(); let searchFilters = $state<ReturnType<typeof SearchFilters>>();
@@ -145,7 +149,7 @@
{grayTheme || showSuggestions ? 'dark:bg-immich-dark-gray' : 'dark:bg-immich-dark-bg'} {grayTheme || showSuggestions ? 'dark:bg-immich-dark-gray' : 'dark:bg-immich-dark-bg'}
{showSuggestions ? 'rounded-t-3xl shadow-[0_8px_20px_rgba(0,0,0,0.12)]' : 'rounded-3xl bg-gray-200'} {showSuggestions ? 'rounded-t-3xl shadow-[0_8px_20px_rgba(0,0,0,0.12)]' : 'rounded-3xl bg-gray-200'}
{searchStore.isSearchEnabled ? 'border-light-200 bg-white dark:border-dark-600' : 'border-transparent'}" {searchStore.isSearchEnabled ? 'border-light-200 bg-white dark:border-dark-600' : 'border-transparent'}"
placeholder={$t('search_your_photos')} {placeholder}
required required
pattern="^(?!m:$).*$" pattern="^(?!m:$).*$"
bind:value={searchManager.filter.query} bind:value={searchManager.filter.query}
@@ -62,6 +62,7 @@
let searchHistory = $state<SearchHistorySection>(); let searchHistory = $state<SearchHistorySection>();
let activeFilter = $state('type'); let activeFilter = $state('type');
let showAdvanced = $state(false);
let peoplePromise = $state<Promise<PersonResponseDto[]>>(); let peoplePromise = $state<Promise<PersonResponseDto[]>>();
let people = $state<PersonResponseDto[]>(); let people = $state<PersonResponseDto[]>();
let tagsPromise = $state<Promise<TagResponseDto[]>>(); let tagsPromise = $state<Promise<TagResponseDto[]>>();
@@ -187,7 +188,7 @@
<div role="listbox" {id}> <div role="listbox" {id}>
{#if isOpen} {#if isOpen}
<div <div
transition:fly={{ y: 25, duration: 150 }} transition:fly={{ y: 25, duration: 250 }}
class="absolute z-1 w-full rounded-b-3xl bg-white shadow-[0_8px_20px_rgba(0,0,0,0.12)] transition-all dark:bg-immich-dark-gray dark:text-gray-300" class="absolute z-1 w-full rounded-b-3xl bg-white shadow-[0_8px_20px_rgba(0,0,0,0.12)] transition-all dark:bg-immich-dark-gray dark:text-gray-300"
> >
<SearchHistorySection <SearchHistorySection
@@ -212,16 +213,7 @@
{/each} {/each}
</div> </div>
</div> </div>
{#if activeFilter === 'advanced'} {#if activeFilter}
<div class="my-5 h-px w-full bg-light-200 dark:bg-dark-600"></div>
<div class="px-5">
<SearchCameraSection />
{#if authManager.authenticated && authManager.preferences.ratings.enabled}
<SearchRatingsSection />
{/if}
<SearchDisplaySection />
</div>
{:else if activeFilter}
<div class="px-5 pt-5"> <div class="px-5 pt-5">
{#if activeFilter === 'type'} {#if activeFilter === 'type'}
<SearchTextSection /> <SearchTextSection />
@@ -238,15 +230,31 @@
{/if} {/if}
</div> </div>
{/if} {/if}
<div
class="grid transition-[grid-template-rows] duration-200 ease-in-out {showAdvanced
? 'grid-rows-[1fr]'
: 'grid-rows-[0fr]'}"
inert={!showAdvanced}
>
<div class="overflow-hidden">
<div class="my-5 h-px w-full bg-light-200 dark:bg-dark-600"></div>
<div class="px-5">
<SearchCameraSection />
{#if authManager.authenticated && authManager.preferences.ratings.enabled}
<SearchRatingsSection />
{/if}
<SearchDisplaySection />
</div>
</div>
</div>
<div class="my-5 h-px w-full bg-light-200 dark:bg-dark-600"></div> <div class="my-5 h-px w-full bg-light-200 dark:bg-dark-600"></div>
<div class="flex gap-2 px-5 pb-5"> <div class="flex gap-2 px-5 pb-5">
<Button <Button
size="small" size="small"
variant={advancedFiltersSet ? 'outline' : 'ghost'} variant={advancedFiltersSet ? 'outline' : 'ghost'}
leadingIcon={mdiTune} leadingIcon={mdiTune}
trailingIcon={activeFilter === 'advanced' ? mdiChevronUp : mdiChevronDown} trailingIcon={showAdvanced ? mdiChevronUp : mdiChevronDown}
onclick={() => (activeFilter = activeFilter === 'advanced' ? '' : 'advanced')} onclick={() => (showAdvanced = !showAdvanced)}>{$t('advanced_filters')}</Button
>{$t('advanced_filters')}</Button
> >
<div class="flex-1"></div> <div class="flex-1"></div>
<Button <Button
@@ -95,6 +95,30 @@ export const getSearchTypeTitle = (type: string) => {
} }
}; };
export const getSearchTypePlaceholder = (type: string) => {
const $t = get(t);
switch (type) {
case 'metadata': {
return $t('search_by_filename_example');
}
case 'description': {
return $t('search_by_description_example');
}
case 'fullPath': {
return $t('search_by_full_path_example');
}
case 'ocr': {
return $t('search_by_ocr_example');
}
case 'smart': {
return $t('search_by_context_example');
}
default: {
return $t('search_your_photos');
}
}
};
export const getSearchPlacesTitle = (city?: string, state?: string, country?: string) => export const getSearchPlacesTitle = (city?: string, state?: string, country?: string) =>
[city, state, country].filter(Boolean).join(', ') || undefined; [city, state, country].filter(Boolean).join(', ') || undefined;