Forráskód Böngészése

feat: add filters component

Thomas Zhang 3 hónapja
szülő
commit
95577c98fc

+ 77 - 0
src/lib/components/common/filters/filters-root.svelte

@@ -0,0 +1,77 @@
+<script lang="ts">
+	import { sineInOut } from 'svelte/easing';
+	import type { ClassValue } from 'svelte/elements';
+	import { slide } from 'svelte/transition';
+
+	import type { FilterRow } from './filters.types';
+
+	let {
+		class: className = '',
+		shown,
+		rows = $bindable(),
+	}: { class?: ClassValue; shown: boolean; rows: FilterRow[] } = $props();
+
+	const onSelect = (idx: number, value: string) => {
+		rows = rows.map((row, i) => {
+			if (i === idx) {
+				if (row.type === 'single') {
+					return {
+						...row,
+						selected: value,
+					};
+				} else if (row.type === 'multi') {
+					if (row.selected.has(value)) row.selected.delete(value);
+					else row.selected.add(value);
+				}
+				if (row.onchange) row.onchange(value);
+			}
+			return row;
+		});
+	};
+</script>
+
+{#if shown}
+	<div
+		class={['flex w-full flex-col gap-1 overflow-hidden', className]}
+		transition:slide={{ easing: sineInOut, duration: 200 }}
+	>
+		{#each rows as row, idx (row.label)}
+			<div class="flex w-full items-center gap-1">
+				<span class="w-20 shrink-0 text-xs text-white/25">{row.label}</span>
+				<div class="flex flex-wrap items-center gap-1">
+					{#if row.type === 'single'}
+						{#each row.options as opt (opt.value)}
+							{@const active = row.selected === opt.value}
+							<button
+								class={[
+									'inline-flex h-7 items-center gap-1 rounded-md px-2 text-xs font-medium transition-colors',
+									active
+										? 'bg-primary/50 text-white/75'
+										: 'text-white/40 hover:bg-white/6 hover:text-white/70',
+								]}
+								onclick={() => onSelect(idx, opt.value)}
+							>
+								{opt.label}
+							</button>
+						{/each}
+					{:else}
+						{#each row.options as opt (opt.value)}
+							{@const active = row.selected.has(opt.value)}
+							<button
+								class={[
+									'inline-flex h-7 items-center rounded-md px-2 text-xs font-medium transition-colors',
+									active
+										? 'bg-primary/50 text-white/75'
+										: 'text-white/40 hover:bg-white/6 hover:text-white/70',
+								]}
+								onclick={() => onSelect(idx, opt.value)}
+							>
+								{opt.label}
+							</button>
+						{/each}
+					{/if}
+				</div>
+			</div>
+		{/each}
+	</div>
+{/if}

+ 28 - 0
src/lib/components/common/filters/filters-toggle.svelte

@@ -0,0 +1,28 @@
+<script lang="ts">
+	import { SlidersHorizontal } from '@lucide/svelte';
+
+	let {
+		shown,
+		count = 0,
+		onclick,
+	}: {
+		shown: boolean;
+		count?: number;
+		onclick: () => void;
+	} = $props();
+</script>
+
+<button
+	class="inline-flex h-8 items-center gap-1 rounded-md px-2.5 text-xs font-medium transition-colors {shown
+		? 'bg-primary/50 text-white/75'
+		: 'text-white/45 hover:bg-white/6 hover:text-white/75'}"
+	{onclick}
+>
+	<SlidersHorizontal size={13} strokeWidth={1.5} />
+	Filters
+	{#if count > 0}
+		<span class="rounded bg-primary/50 p-1 text-[10px] leading-none text-white/75">
+			{count}
+		</span>
+	{/if}
+</button>

+ 22 - 0
src/lib/components/common/filters/filters.types.ts

@@ -0,0 +1,22 @@
+import type { SvelteSet } from 'svelte/reactivity';
+
+type FilterRow = {
+	id: string;
+	label: string;
+	options: { value: string; label: string }[];
+	default?: string;
+	countSelected?: () => number;
+	onchange?: (value: string) => void;
+	onreset?: () => void;
+} & (
+	| {
+			type: 'single';
+			selected: string | undefined;
+	  }
+	| {
+			type: 'multi';
+			selected: SvelteSet<string>;
+	  }
+);
+
+export type { FilterRow };

+ 5 - 0
src/lib/components/common/filters/index.ts

@@ -0,0 +1,5 @@
+import Root from './filters-root.svelte';
+import Toggle from './filters-toggle.svelte';
+
+export type { FilterRow } from './filters.types';
+export { Root, Toggle };