feature/project-redesign #28
@@ -1,46 +1,86 @@
|
||||
<!--
|
||||
Component: FiltersControl
|
||||
Renders a group of action buttons for filter operations.
|
||||
- Reset: Clears all active filters (outline variant for secondary action)
|
||||
Component: FilterControls
|
||||
Sort options + Reset_Filters button.
|
||||
Sits below the filter list, separated by a top border.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Button } from '$shared/shadcn/ui/button';
|
||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
||||
import Rotate from '@lucide/svelte/icons/rotate-ccw';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import { Tween } from 'svelte/motion';
|
||||
import { Button } from '$shared/ui';
|
||||
import { Label } from '$shared/ui';
|
||||
import RefreshCwIcon from '@lucide/svelte/icons/refresh-cw';
|
||||
import { filterManager } from '../../model';
|
||||
|
||||
type SortOption = 'Name' | 'Popularity' | 'Newest';
|
||||
|
||||
const SORT_OPTIONS: SortOption[] = ['Name', 'Popularity', 'Newest'];
|
||||
|
||||
interface Props {
|
||||
sort?: SortOption;
|
||||
onSortChange?: (v: SortOption) => void;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const { class: className }: Props = $props();
|
||||
const {
|
||||
sort = 'Popularity',
|
||||
onSortChange,
|
||||
class: className,
|
||||
}: Props = $props();
|
||||
|
||||
const transform = new Tween(
|
||||
{ scale: 1, rotate: 0 },
|
||||
{ duration: 150, easing: cubicOut },
|
||||
);
|
||||
|
||||
function handleClick() {
|
||||
function handleReset() {
|
||||
filterManager.deselectAllGlobal();
|
||||
|
||||
transform.set({ scale: 0.98, rotate: 1 }).then(() => {
|
||||
transform.set({ scale: 1, rotate: 0 });
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={cn('flex flex-row gap-2', className)}
|
||||
style:transform="scale({transform.current.scale}) rotate({transform.current.rotate}deg)"
|
||||
class={cn(
|
||||
'flex flex-col md:flex-row justify-between items-start md:items-center',
|
||||
'gap-4 md:gap-6',
|
||||
'pt-6 mt-6 md:pt-8 md:mt-8',
|
||||
'border-t border-foreground/5 dark:border-white/10',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<!-- Left: Sort By label + options -->
|
||||
<div class="flex flex-col md:flex-row items-start md:items-center gap-3 md:gap-8 w-full md:w-auto">
|
||||
<Label variant="muted" size="sm">Sort By:</Label>
|
||||
|
||||
<div class="flex gap-3 md:gap-4">
|
||||
{#each SORT_OPTIONS as option}
|
||||
<!--
|
||||
Ghost button with red-only hover (no bg lift).
|
||||
active prop turns text [#ff3b30] for the selected sort.
|
||||
class overrides: Space_Grotesk bold tracking-wide, no padding bg.
|
||||
-->
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="group flex flex-1 cursor-pointer gap-1"
|
||||
onclick={handleClick}
|
||||
active={sort === option}
|
||||
onclick={() => onSortChange?.(option)}
|
||||
class="text-xs font-bold uppercase tracking-wide font-primary"
|
||||
>
|
||||
<Rotate class="size-4 group-hover:-rotate-180 transition-transform duration-300" />
|
||||
Reset
|
||||
{option}
|
||||
</Button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right: Reset_Filters -->
|
||||
<!--
|
||||
Bare ghost, red hover. Space_Mono to match Swiss technical text pattern.
|
||||
Icon uses CSS group-hover for the spin — no Tween needed.
|
||||
-->
|
||||
<Button
|
||||
variant="ghost"
|
||||
onclick={handleReset}
|
||||
class="
|
||||
group
|
||||
text-[0.5625rem] md:text-[0.625rem] font-mono font-bold uppercase tracking-widest
|
||||
text-neutral-400
|
||||
"
|
||||
iconPosition="left"
|
||||
>
|
||||
{#snippet icon()}
|
||||
<RefreshCwIcon class="size-4 transition-transform duration-300 group-hover:rotate-180" />
|
||||
{/snippet}
|
||||
Reset_Filters
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user