129 lines
3.8 KiB
Svelte
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>
|