Compare commits
5 Commits
109c69c1b9
...
23f3a5b803
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23f3a5b803 | ||
|
|
d439e97729 | ||
|
|
1bb699ea2d | ||
|
|
bf36f8e642 | ||
|
|
0742eb8c3d |
@@ -1,4 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
/**
|
||||||
|
* App Component
|
||||||
|
*
|
||||||
|
* Application entry point component. Wraps the main page route within the shared
|
||||||
|
* layout shell. This is the root component mounted by the application.
|
||||||
|
*
|
||||||
|
* Structure:
|
||||||
|
* - Layout provides sidebar, header/footer, and page container
|
||||||
|
* - Page renders the current route content
|
||||||
|
*/
|
||||||
import Page from '$routes/Page.svelte';
|
import Page from '$routes/Page.svelte';
|
||||||
import Layout from './ui/Layout.svelte';
|
import Layout from './ui/Layout.svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,8 +1,23 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
/**
|
||||||
|
* Layout Component
|
||||||
|
*
|
||||||
|
* Root layout wrapper that provides the application shell structure. Handles favicon,
|
||||||
|
* sidebar provider initialization, and renders child routes with consistent structure.
|
||||||
|
*
|
||||||
|
* Layout structure:
|
||||||
|
* - Header area (currently empty, reserved for future use)
|
||||||
|
* - Collapsible sidebar with main content area
|
||||||
|
* - Footer area (currently empty, reserved for future use)
|
||||||
|
*
|
||||||
|
* Uses Sidebar.Provider to enable mobile-responsive collapsible sidebar behavior
|
||||||
|
* throughout the application.
|
||||||
|
*/
|
||||||
import favicon from '$shared/assets/favicon.svg';
|
import favicon from '$shared/assets/favicon.svg';
|
||||||
import * as Sidebar from '$shared/shadcn/ui/sidebar/index';
|
import * as Sidebar from '$shared/shadcn/ui/sidebar/index';
|
||||||
import { AppSidebar } from '$widgets/AppSidebar';
|
import { AppSidebar } from '$widgets/AppSidebar';
|
||||||
|
|
||||||
|
/** Slot content for route pages to render */
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
*/
|
*/
|
||||||
export const initialState: CategoryFilterModel = {
|
export const initialState: CategoryFilterModel = {
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
categories: FONT_CATEGORIES,
|
properties: FONT_CATEGORIES,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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 ProvidersFilterModel = FilterModel;
|
export type ProvidersFilterModel = FilterModel;
|
||||||
|
|
||||||
export const FONT_PROVIDERS: Category[] = [
|
export const FONT_PROVIDERS: Property[] = [
|
||||||
{
|
{
|
||||||
id: 'google',
|
id: 'google',
|
||||||
name: 'Google Fonts',
|
name: 'Google Fonts',
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
*/
|
*/
|
||||||
export const initialState: ProvidersFilterModel = {
|
export const initialState: ProvidersFilterModel = {
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
categories: FONT_PROVIDERS,
|
properties: FONT_PROVIDERS,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
*/
|
*/
|
||||||
export const initialState: SubsetsFilterModel = {
|
export const initialState: SubsetsFilterModel = {
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
categories: FONT_SUBSETS,
|
properties: FONT_SUBSETS,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
<script>
|
<script>
|
||||||
|
/**
|
||||||
|
* Page Component
|
||||||
|
*
|
||||||
|
* Main page route component. This is the default route that users see when
|
||||||
|
* accessing the application. Currently displays a welcome message.
|
||||||
|
*
|
||||||
|
* Note: This is a placeholder component. Replace with actual application content
|
||||||
|
* as the font comparison and filtering features are implemented.
|
||||||
|
*/
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h1>Welcome to Svelte + Vite</h1>
|
<h1>Welcome to Svelte + Vite</h1>
|
||||||
|
|||||||
@@ -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 })),
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
@@ -79,10 +79,10 @@ const hasSelection = $derived(selectedCount > 0);
|
|||||||
variant: 'ghost',
|
variant: 'ghost',
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
class:
|
class:
|
||||||
'flex-1 justify-start 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}
|
||||||
|
|||||||
@@ -1,31 +1,24 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { categoryFilterStore } from '$features/CategoryFilter';
|
/**
|
||||||
import { providersFilterStore } from '$features/ProvidersFilter';
|
* AppSidebar Component
|
||||||
import { subsetsFilterStore } from '$features/SubsetsFilter';
|
*
|
||||||
|
* Main application sidebar widget. Contains filter controls and action buttons
|
||||||
|
* for font filtering operations. Organized into two sections:
|
||||||
|
*
|
||||||
|
* - Filters: Category-based filter groups (providers, subsets, categories)
|
||||||
|
* - Controls: Apply/Reset buttons for filter actions
|
||||||
|
*
|
||||||
|
* Uses Sidebar.Root from shadcn for responsive sidebar behavior including
|
||||||
|
* mobile drawer and desktop persistent sidebar modes.
|
||||||
|
*/
|
||||||
import * as Sidebar from '$shared/shadcn/ui/sidebar/index';
|
import * as Sidebar from '$shared/shadcn/ui/sidebar/index';
|
||||||
import CheckboxFilter from '$shared/ui/CheckboxFilter/CheckboxFilter.svelte';
|
import Controls from './Controls.svelte';
|
||||||
|
import Filters from './Filters.svelte';
|
||||||
const { categories: providers } = $derived($providersFilterStore);
|
|
||||||
const { categories: subsets } = $derived($subsetsFilterStore);
|
|
||||||
const { categories } = $derived($categoryFilterStore);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Sidebar.Root>
|
<Sidebar.Root>
|
||||||
<Sidebar.Content>
|
<Sidebar.Content class="p-2">
|
||||||
<CheckboxFilter
|
<Filters />
|
||||||
filterName="Font provider"
|
<Controls />
|
||||||
categories={providers}
|
|
||||||
onCategoryToggle={providersFilterStore.toggleCategory}
|
|
||||||
/>
|
|
||||||
<CheckboxFilter
|
|
||||||
filterName="Font subset"
|
|
||||||
categories={subsets}
|
|
||||||
onCategoryToggle={subsetsFilterStore.toggleCategory}
|
|
||||||
/>
|
|
||||||
<CheckboxFilter
|
|
||||||
filterName="Font category"
|
|
||||||
categories={categories}
|
|
||||||
onCategoryToggle={categoryFilterStore.toggleCategory}
|
|
||||||
/>
|
|
||||||
</Sidebar.Content>
|
</Sidebar.Content>
|
||||||
</Sidebar.Root>
|
</Sidebar.Root>
|
||||||
|
|||||||
23
src/widgets/AppSidebar/ui/Controls.svelte
Normal file
23
src/widgets/AppSidebar/ui/Controls.svelte
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
/**
|
||||||
|
* Controls Component
|
||||||
|
*
|
||||||
|
* Action button group for filter operations. Provides two buttons:
|
||||||
|
*
|
||||||
|
* - Reset: Clears all active filters (outline variant for secondary action)
|
||||||
|
* - Apply: Applies selected filters (primary variant for main action)
|
||||||
|
*
|
||||||
|
* Buttons are equally sized (flex-1) for balanced layout. Note:
|
||||||
|
* Functionality not yet implemented - wire up to filter stores.
|
||||||
|
*/
|
||||||
|
import { Button } from '$shared/shadcn/ui/button';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<Button variant="outline" class="flex-1 cursor-pointer">
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
<Button class="flex-1 cursor-pointer">
|
||||||
|
Apply
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
42
src/widgets/AppSidebar/ui/Filters.svelte
Normal file
42
src/widgets/AppSidebar/ui/Filters.svelte
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
/**
|
||||||
|
* Filters Component
|
||||||
|
*
|
||||||
|
* Orchestrates all filter properties for the sidebar. Connects filter stores
|
||||||
|
* to CheckboxFilter components, organizing them by filter type:
|
||||||
|
*
|
||||||
|
* - Font provider: Google Fonts vs Fontshare
|
||||||
|
* - Font subset: Character subsets available (Latin, Latin Extended, etc.)
|
||||||
|
* - Font category: Serif, Sans-serif, Display, etc.
|
||||||
|
*
|
||||||
|
* Uses $derived for reactive access to filter states, ensuring UI updates
|
||||||
|
* when selections change through any means (sidebar, programmatically, etc.).
|
||||||
|
*/
|
||||||
|
import { categoryFilterStore } from '$features/CategoryFilter';
|
||||||
|
import { providersFilterStore } from '$features/ProvidersFilter';
|
||||||
|
import { subsetsFilterStore } from '$features/SubsetsFilter';
|
||||||
|
import CheckboxFilter from '$shared/ui/CheckboxFilter/CheckboxFilter.svelte';
|
||||||
|
|
||||||
|
/** Reactive properties from providers filter store */
|
||||||
|
const { properties: providers } = $derived($providersFilterStore);
|
||||||
|
/** Reactive properties from subsets filter store */
|
||||||
|
const { properties: subsets } = $derived($subsetsFilterStore);
|
||||||
|
/** Reactive properties from categories filter store */
|
||||||
|
const { properties: categories } = $derived($categoryFilterStore);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CheckboxFilter
|
||||||
|
displayedLabel="Font provider"
|
||||||
|
properties={providers}
|
||||||
|
onPropertyToggle={providersFilterStore.toggleProperty}
|
||||||
|
/>
|
||||||
|
<CheckboxFilter
|
||||||
|
displayedLabel="Font subset"
|
||||||
|
properties={subsets}
|
||||||
|
onPropertyToggle={subsetsFilterStore.toggleProperty}
|
||||||
|
/>
|
||||||
|
<CheckboxFilter
|
||||||
|
displayedLabel="Font category"
|
||||||
|
properties={categories}
|
||||||
|
onPropertyToggle={categoryFilterStore.toggleProperty}
|
||||||
|
/>
|
||||||
Reference in New Issue
Block a user