Files
frontend-svelte/src/widgets/FontSearch/ui/FontSearch/FontSearch.svelte
2026-02-18 16:57:03 +03:00

129 lines
3.8 KiB
Svelte

<!--
Component: FontSearch
Provides a search input and filtration for fonts
-->
<script lang="ts">
import { unifiedFontStore } from '$entities/Font';
import {
FilterControls,
Filters,
filterManager,
mapManagerToParams,
} from '$features/GetFonts';
import { springySlideFade } from '$shared/lib';
import { cn } from '$shared/shadcn/utils/shadcn-utils';
import {
Footnote,
IconButton,
SearchBar,
} from '$shared/ui';
import SlidersHorizontalIcon from '@lucide/svelte/icons/sliders-horizontal';
import { onMount } from 'svelte';
import { cubicOut } from 'svelte/easing';
import {
Tween,
prefersReducedMotion,
} from 'svelte/motion';
import { type SlideParams } from 'svelte/transition';
interface Props {
/**
* Controllable flag to show/hide filters (bindable)
*/
showFilters?: boolean;
}
let { showFilters = $bindable(true) }: Props = $props();
onMount(() => {
/**
* The Pairing:
* We "plug" this manager into the global store.
* addBinding returns a function that removes this binding when the component unmounts.
*/
const unbind = unifiedFontStore.addBinding(() => mapManagerToParams(filterManager));
return unbind;
});
const transform = new Tween(
{ scale: 1, rotate: 0 },
{ duration: 250, easing: cubicOut },
);
const slideConfig = $derived<SlideParams>({
duration: prefersReducedMotion.current ? 0 : 200,
easing: cubicOut,
axis: 'y',
});
function toggleFilters() {
showFilters = !showFilters;
transform.set({ scale: 0.9, rotate: 2 }).then(() => {
transform.set({ scale: 1, rotate: 0 });
});
}
</script>
<div class="flex flex-col gap-3 relative">
<div class="relative">
<SearchBar
id="font-search"
class="w-full"
placeholder="search_typefaces..."
bind:value={filterManager.queryValue}
/>
<div class="absolute right-4 top-1/2 translate-y-[-50%] z-10">
<div class="flex items-center gap-2">
<div class="w-px h-5 bg-border-subtle"></div>
<div style:transform="scale({transform.current.scale})">
<IconButton onclick={toggleFilters}>
{#snippet icon({ className })}
<SlidersHorizontalIcon
class={cn(
className,
showFilters ? 'stroke-foreground stroke-3' : 'stroke-text-muted',
)}
/>
{/snippet}
</IconButton>
</div>
</div>
</div>
</div>
{#if showFilters}
<div
transition:springySlideFade|local={slideConfig}
class="will-change-[height,opacity] contain-layout overflow-hidden"
>
<div
class="
p-3 sm:p-4 md:p-5 rounded-xl
backdrop-blur-md bg-background-80
border border-border-muted
shadow-[0_1px_3px_rgba(0,0,0,0.04)]
"
>
<div class="flex items-center gap-2 sm:gap-2.5 mb-3 sm:mb-4">
<div class="w-1 h-1 rounded-full bg-foreground opacity-70"></div>
<div class="w-px h-2.5 bg-border-subtle"></div>
<Footnote>
filter_params
</Footnote>
</div>
<div class="grid gap-3 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
<Filters />
</div>
<div class="mt-3 sm:mt-4 pt-3 sm:pt-4 border-t border-border-subtle">
<FilterControls class="m-auto w-fit" />
</div>
</div>
</div>
{/if}
</div>