feature: change filterStore model

This commit is contained in:
Ilia Mashkov
2026-01-02 21:16:07 +03:00
parent 1bb699ea2d
commit d439e97729
7 changed files with 88 additions and 88 deletions

View File

@@ -1,6 +1,6 @@
import type { import type {
Category,
FilterModel, FilterModel,
Property,
} from '$shared/store/createFilterStore'; } from '$shared/store/createFilterStore';
/** /**
@@ -8,7 +8,7 @@ import type {
*/ */
export type CategoryFilterModel = FilterModel; export type CategoryFilterModel = FilterModel;
export const FONT_CATEGORIES: Category[] = [ export const FONT_CATEGORIES: Property[] = [
{ {
id: 'serif', id: 'serif',
name: 'Serif', name: 'Serif',

View File

@@ -9,7 +9,7 @@ import {
*/ */
export const initialState: CategoryFilterModel = { export const initialState: CategoryFilterModel = {
searchQuery: '', searchQuery: '',
categories: FONT_CATEGORIES, properties: FONT_CATEGORIES,
}; };
/** /**

View File

@@ -9,7 +9,7 @@ import {
*/ */
export const initialState: ProvidersFilterModel = { export const initialState: ProvidersFilterModel = {
searchQuery: '', searchQuery: '',
categories: FONT_PROVIDERS, properties: FONT_PROVIDERS,
}; };
/** /**

View File

@@ -1,6 +1,6 @@
import type { import type {
Category,
FilterModel, FilterModel,
Property,
} from '$shared/store/createFilterStore'; } from '$shared/store/createFilterStore';
/** /**
@@ -8,7 +8,7 @@ import type {
*/ */
export type SubsetsFilterModel = FilterModel; export type SubsetsFilterModel = FilterModel;
export const FONT_SUBSETS: Category[] = [ export const FONT_SUBSETS: Property[] = [
{ {
id: 'latin', id: 'latin',
name: 'Latin', name: 'Latin',

View File

@@ -9,7 +9,7 @@ import {
*/ */
export const initialState: SubsetsFilterModel = { export const initialState: SubsetsFilterModel = {
searchQuery: '', searchQuery: '',
categories: FONT_SUBSETS, properties: FONT_SUBSETS,
}; };
/** /**

View File

@@ -5,17 +5,17 @@ import {
writable, writable,
} from 'svelte/store'; } from 'svelte/store';
export interface Category { export interface Property {
/** /**
* Category identifier * Property identifier
*/ */
id: string; id: string;
/** /**
* Category name * Property name
*/ */
name: string; name: string;
/** /**
* Category selected state * Property selected state
*/ */
selected?: boolean; selected?: boolean;
} }
@@ -26,13 +26,13 @@ export interface FilterModel {
*/ */
searchQuery?: string; searchQuery?: string;
/** /**
* Categories * Properties
*/ */
categories: Category[]; properties: Property[];
} }
/** /**
* Model for reusable filter store with search support and category selection * Model for reusable filter store with search support and property selection
*/ */
export interface FilterStore<T extends FilterModel> extends Writable<T> { export interface FilterStore<T extends FilterModel> extends Writable<T> {
/** /**
@@ -41,20 +41,20 @@ export interface FilterStore<T extends FilterModel> extends Writable<T> {
*/ */
getStore: () => Readable<T>; getStore: () => Readable<T>;
/** /**
* Get all categories. * Get all properties.
* @returns Readable store with categories * @returns Readable store with properties
*/ */
getAllCategories: () => Readable<Category[]>; getAllProperties: () => Readable<Property[]>;
/** /**
* Get the selected categories. * Get the selected properties.
* @returns Readable store with selected categories * @returns Readable store with selected properties
*/ */
getSelectedCategories: () => Readable<Category[]>; getSelectedProperties: () => Readable<Property[]>;
/** /**
* Get the filtered categories. * Get the filtered properties.
* @returns Readable store with filtered categories * @returns Readable store with filtered properties
*/ */
getFilteredCategories: () => Readable<Category[]>; getFilteredProperties: () => Readable<Property[]>;
/** /**
* Update the search query filter. * Update the search query filter.
* *
@@ -66,31 +66,31 @@ export interface FilterStore<T extends FilterModel> extends Writable<T> {
*/ */
clearSearchQuery: () => void; clearSearchQuery: () => void;
/** /**
* Select a category. * Select a property.
* *
* @param category - Category to select * @param property - Property to select
*/ */
selectCategory: (categoryId: string) => void; selectProperty: (propertyId: string) => void;
/** /**
* Deselect a category. * Deselect a property.
* *
* @param category - Category to deselect * @param property - Property to deselect
*/ */
deselectCategory: (categoryId: string) => void; deselectProperty: (propertyId: string) => void;
/** /**
* Toggle a category. * Toggle a property.
* *
* @param categoryId - Category ID * @param propertyId - Property ID
*/ */
toggleCategory: (categoryId: string) => void; toggleProperty: (propertyId: string) => void;
/** /**
* Select all categories. * Select all properties.
*/ */
selectAllCategories: () => void; selectAllProperties: () => void;
/** /**
* Deselect all categories. * Deselect all properties.
*/ */
deselectAllCategories: () => void; deselectAllProperties: () => void;
} }
/** /**
@@ -120,28 +120,28 @@ export function createFilterStore<T extends FilterModel>(
}; };
}, },
/** /**
* Get the filtered categories. * Get the filtered properties.
*/ */
getAllCategories: () => { getAllProperties: () => {
return derived({ subscribe }, $store => { return derived({ subscribe }, $store => {
return $store.categories; return $store.properties;
}); });
}, },
/** /**
* Get the selected categories. * Get the selected properties.
*/ */
getSelectedCategories: () => { getSelectedProperties: () => {
return derived({ subscribe }, $store => { return derived({ subscribe }, $store => {
return $store.categories.filter(category => category.selected); return $store.properties.filter(property => property.selected);
}); });
}, },
/** /**
* Get the filtered categories. * Get the filtered properties.
*/ */
getFilteredCategories: () => { getFilteredProperties: () => {
return derived({ subscribe }, $store => { return derived({ subscribe }, $store => {
return $store.categories.filter(category => return $store.properties.filter(property =>
category.name.includes($store.searchQuery || '') property.name.includes($store.searchQuery || '')
); );
}); });
}, },
@@ -166,60 +166,60 @@ export function createFilterStore<T extends FilterModel>(
})); }));
}, },
/** /**
* Select a category. * Select a property.
* *
* @param categoryId - Category ID * @param propertyId - Property ID
*/ */
selectCategory: (categoryId: string) => { selectProperty: (propertyId: string) => {
update(state => ({ update(state => ({
...state, ...state,
categories: state.categories.map(c => properties: state.properties.map(c =>
c.id === categoryId ? { ...c, selected: true } : c c.id === propertyId ? { ...c, selected: true } : c
), ),
})); }));
}, },
/** /**
* Deselect a category. * Deselect a property.
* *
* @param categoryId - Category ID * @param propertyId - Property ID
*/ */
deselectCategory: (categoryId: string) => { deselectProperty: (propertyId: string) => {
update(state => ({ update(state => ({
...state, ...state,
categories: state.categories.map(c => properties: state.properties.map(c =>
c.id === categoryId ? { ...c, selected: false } : c c.id === propertyId ? { ...c, selected: false } : c
), ),
})); }));
}, },
/** /**
* Toggle a category. * Toggle a property.
* *
* @param categoryId - Category ID * @param propertyId - Property ID
*/ */
toggleCategory: (categoryId: string) => { toggleProperty: (propertyId: string) => {
update(state => ({ update(state => ({
...state, ...state,
categories: state.categories.map(c => properties: state.properties.map(c =>
c.id === categoryId ? { ...c, selected: !c.selected } : c c.id === propertyId ? { ...c, selected: !c.selected } : c
), ),
})); }));
}, },
/** /**
* Select all categories * Select all properties
*/ */
selectAllCategories: () => { selectAllProperties: () => {
update(state => ({ update(state => ({
...state, ...state,
categories: state.categories.map(c => ({ ...c, selected: true })), properties: state.properties.map(c => ({ ...c, selected: true })),
})); }));
}, },
/** /**
* Deselect all categories * Deselect all properties
*/ */
deselectAllCategories: () => { deselectAllProperties: () => {
update(state => ({ update(state => ({
...state, ...state,
categories: state.categories.map(c => ({ ...c, selected: false })), properties: state.properties.map(c => ({ ...c, selected: false })),
})); }));
}, },
}; };

View File

@@ -4,7 +4,7 @@ import { buttonVariants } from '$shared/shadcn/ui/button';
import { Checkbox } from '$shared/shadcn/ui/checkbox'; import { Checkbox } from '$shared/shadcn/ui/checkbox';
import * as Collapsible from '$shared/shadcn/ui/collapsible'; import * as Collapsible from '$shared/shadcn/ui/collapsible';
import { Label } from '$shared/shadcn/ui/label'; import { Label } from '$shared/shadcn/ui/label';
import type { Category } from '$shared/store/createFilterStore'; import type { Property } from '$shared/store/createFilterStore';
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down'; import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { cubicOut } from 'svelte/easing'; import { cubicOut } from 'svelte/easing';
@@ -13,7 +13,7 @@ import { slide } from 'svelte/transition';
/** /**
* CheckboxFilter Component * CheckboxFilter Component
* *
* A collapsible category filter with checkboxes. Displays selected count as a badge * A collapsible property filter with checkboxes. Displays selected count as a badge
* and supports reduced motion for accessibility. Used in sidebar filtering UIs. * and supports reduced motion for accessibility. Used in sidebar filtering UIs.
* *
* Design choices: * Design choices:
@@ -23,16 +23,16 @@ import { slide } from 'svelte/transition';
* - Local transition prevents animation when component first renders * - Local transition prevents animation when component first renders
*/ */
interface CategoryFilterProps { interface PropertyFilterProps {
/** Display name for this filter group (e.g., "Categories", "Tags") */ /** Label for this filter group (e.g., "Properties", "Tags") */
filterName: string; displayedLabel: string;
/** Array of categories with their selection states */ /** Array of properties with their selection states */
categories: Category[]; properties: Property[];
/** Callback when a category checkbox is toggled */ /** Callback when a property checkbox is toggled */
onCategoryToggle: (id: string) => void; onPropertyToggle: (id: string) => void;
} }
const { filterName, categories, onCategoryToggle }: CategoryFilterProps = $props(); const { displayedLabel, properties, onPropertyToggle }: PropertyFilterProps = $props();
// Toggle state - defaults to open for better discoverability // Toggle state - defaults to open for better discoverability
let isOpen = $state(true); let isOpen = $state(true);
@@ -62,8 +62,8 @@ const slideConfig = $derived({
easing: cubicOut, easing: cubicOut,
}); });
// Derived for reactive updates when categories change - avoids recomputing on every render // Derived for reactive updates when properties change - avoids recomputing on every render
const selectedCount = $derived(categories.filter(c => c.selected).length); const selectedCount = $derived(properties.filter(c => c.selected).length);
const hasSelection = $derived(selectedCount > 0); const hasSelection = $derived(selectedCount > 0);
</script> </script>
@@ -82,7 +82,7 @@ const hasSelection = $derived(selectedCount > 0);
'flex-1 justify-between gap-2 hover:bg-transparent focus-visible:ring-1 focus-visible:ring-ring', 'flex-1 justify-between gap-2 hover:bg-transparent focus-visible:ring-1 focus-visible:ring-ring',
})} })}
> >
<h4 class="text-sm font-semibold">{filterName}</h4> <h4 class="text-sm font-semibold">{displayedLabel}</h4>
<!-- Chevron rotates based on open state for visual feedback --> <!-- Chevron rotates based on open state for visual feedback -->
<div <div
@@ -112,10 +112,10 @@ const hasSelection = $derived(selectedCount > 0);
<div class="px-4 py-3"> <div class="px-4 py-3">
<div class="flex flex-col gap-1.5"> <div class="flex flex-col gap-1.5">
<!-- Each item: checkbox + label with interactive hover/focus states --> <!-- Each item: checkbox + label with interactive hover/focus states -->
<!-- Keyed by category.id for efficient DOM updates --> <!-- Keyed by property.id for efficient DOM updates -->
{#each categories as category (category.id)} {#each properties as property (property.id)}
<Label <Label
for={category.id} for={property.id}
class=" class="
group flex items-center gap-3 cursor-pointer rounded-md px-2 py-1.5 -mx-2 group flex items-center gap-3 cursor-pointer rounded-md px-2 py-1.5 -mx-2
transition-colors duration-150 ease-out transition-colors duration-150 ease-out
@@ -127,9 +127,9 @@ const hasSelection = $derived(selectedCount > 0);
Checkbox handles toggle, styled for accessibility with focus rings Checkbox handles toggle, styled for accessibility with focus rings
--> -->
<Checkbox <Checkbox
id={category.id} id={property.id}
checked={category.selected} checked={property.selected}
onclick={() => onCategoryToggle(category.id)} onclick={() => onPropertyToggle(property.id)}
class=" class="
shrink-0 cursor-pointer transition-all duration-150 ease-out shrink-0 cursor-pointer transition-all duration-150 ease-out
data-[state=checked]:scale-100 data-[state=checked]:scale-100
@@ -143,10 +143,10 @@ const hasSelection = $derived(selectedCount > 0);
text-sm select-none transition-all duration-150 ease-out text-sm select-none transition-all duration-150 ease-out
group-hover:text-foreground group-hover:text-foreground
text-muted-foreground text-muted-foreground
{category.selected ? 'font-medium text-foreground' : ''} {property.selected ? 'font-medium text-foreground' : ''}
" "
> >
{category.name} {property.name}
</span> </span>
</Label> </Label>
{/each} {/each}