refactor(GetFonts): rename filters/filterManager to available/appliedFilterStore

The 'filters' + 'filterManager' pair didn't reveal the schema-vs-selection
split. Rename to reflect the actual roles:

- FiltersStore / filtersStore       → AvailableFilterStore / availableFilterStore
- createFilterManager / FilterManager → createAppliedFilterStore / AppliedFilterStore
- filterManager singleton            → appliedFilterStore
- mapManagerToParams                 → mapAppliedFiltersToParams

Directories and file basenames follow the new singleton names. Public
barrel signature updated; all consumers (Search, FontSearch, Filters,
FilterControls) point at the new identifiers.
This commit is contained in:
Ilia Mashkov
2026-05-24 18:08:05 +03:00
parent b6494a8cb5
commit e0d39d861f
18 changed files with 153 additions and 153 deletions
+8 -8
View File
@@ -1,16 +1,16 @@
export { mapManagerToParams } from './lib'; export { mapAppliedFiltersToParams } from './lib';
export { export {
/** type AppliedFilterStore,
* Filter Manager appliedFilterStore,
*/
createFilterManager,
type FilterManager,
filterManager,
/** /**
* Filter Store * Filter Store
*/ */
filtersStore, availableFilterStore,
/**
* Filter Manager
*/
createAppliedFilterStore,
/** /**
* Sort Store * Sort Store
*/ */
+1 -1
View File
@@ -1 +1 @@
export { mapManagerToParams } from './mapper/mapManagerToParams'; export { mapAppliedFiltersToParams } from './mapper/mapAppliedFiltersToParams';
@@ -4,8 +4,8 @@ import {
expect, expect,
it, it,
} from 'vitest'; } from 'vitest';
import { createFilterManager } from '../../model/store/filterManager/filterManager.svelte'; import { createAppliedFilterStore } from '../../model/store/appliedFilterStore/appliedFilterStore.svelte';
import { mapManagerToParams } from './mapManagerToParams'; import { mapAppliedFiltersToParams } from './mapAppliedFiltersToParams';
/** /**
* Build a Property with explicit selection state. * Build a Property with explicit selection state.
@@ -25,47 +25,47 @@ function group(id: string, props: Array<[string, boolean]>) {
}; };
} }
describe('mapManagerToParams', () => { describe('mapAppliedFiltersToParams', () => {
describe('search query', () => { describe('search query', () => {
it('omits q when query is empty', () => { it('omits q when query is empty', () => {
const manager = createFilterManager({ queryValue: '', groups: [] }); const manager = createAppliedFilterStore({ queryValue: '', groups: [] });
expect(mapManagerToParams(manager).q).toBeUndefined(); expect(mapAppliedFiltersToParams(manager).q).toBeUndefined();
}); });
it('passes the debounced query through as q', () => { it('passes the debounced query through as q', () => {
// Constructor seeds both immediate and debounced synchronously. // Constructor seeds both immediate and debounced synchronously.
const manager = createFilterManager({ queryValue: 'roboto', groups: [] }); const manager = createAppliedFilterStore({ queryValue: 'roboto', groups: [] });
expect(mapManagerToParams(manager).q).toBe('roboto'); expect(mapAppliedFiltersToParams(manager).q).toBe('roboto');
}); });
}); });
describe('group selections', () => { describe('group selections', () => {
it('omits a group entirely when no group with that id exists', () => { it('omits a group entirely when no group with that id exists', () => {
const manager = createFilterManager({ queryValue: '', groups: [] }); const manager = createAppliedFilterStore({ queryValue: '', groups: [] });
const params = mapManagerToParams(manager); const params = mapAppliedFiltersToParams(manager);
expect(params.providers).toBeUndefined(); expect(params.providers).toBeUndefined();
expect(params.categories).toBeUndefined(); expect(params.categories).toBeUndefined();
expect(params.subsets).toBeUndefined(); expect(params.subsets).toBeUndefined();
}); });
it('omits a group when it exists but has no selections', () => { it('omits a group when it exists but has no selections', () => {
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: '', queryValue: '',
groups: [group('providers', [['google', false], ['fontshare', false]])], groups: [group('providers', [['google', false], ['fontshare', false]])],
}); });
expect(mapManagerToParams(manager).providers).toBeUndefined(); expect(mapAppliedFiltersToParams(manager).providers).toBeUndefined();
}); });
it('returns the selected values for a single group', () => { it('returns the selected values for a single group', () => {
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: '', queryValue: '',
groups: [group('providers', [['google', true], ['fontshare', false]])], groups: [group('providers', [['google', true], ['fontshare', false]])],
}); });
expect(mapManagerToParams(manager).providers).toEqual(['google']); expect(mapAppliedFiltersToParams(manager).providers).toEqual(['google']);
}); });
it('returns multiple selected values in selection order', () => { it('returns multiple selected values in selection order', () => {
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: '', queryValue: '',
groups: [ groups: [
group('categories', [ group('categories', [
@@ -76,11 +76,11 @@ describe('mapManagerToParams', () => {
]), ]),
], ],
}); });
expect(mapManagerToParams(manager).categories).toEqual(['serif', 'display', 'monospace']); expect(mapAppliedFiltersToParams(manager).categories).toEqual(['serif', 'display', 'monospace']);
}); });
it('maps each of the three recognized group ids independently', () => { it('maps each of the three recognized group ids independently', () => {
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: '', queryValue: '',
groups: [ groups: [
group('providers', [['google', true]]), group('providers', [['google', true]]),
@@ -88,18 +88,18 @@ describe('mapManagerToParams', () => {
group('subsets', [['latin', true]]), group('subsets', [['latin', true]]),
], ],
}); });
const params = mapManagerToParams(manager); const params = mapAppliedFiltersToParams(manager);
expect(params.providers).toEqual(['google']); expect(params.providers).toEqual(['google']);
expect(params.categories).toEqual(['serif', 'sans-serif']); expect(params.categories).toEqual(['serif', 'sans-serif']);
expect(params.subsets).toEqual(['latin']); expect(params.subsets).toEqual(['latin']);
}); });
it('ignores groups whose id does not match providers/categories/subsets', () => { it('ignores groups whose id does not match providers/categories/subsets', () => {
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: '', queryValue: '',
groups: [group('weights', [['400', true], ['700', true]])], groups: [group('weights', [['400', true], ['700', true]])],
}); });
const params = mapManagerToParams(manager); const params = mapAppliedFiltersToParams(manager);
expect(params.providers).toBeUndefined(); expect(params.providers).toBeUndefined();
expect(params.categories).toBeUndefined(); expect(params.categories).toBeUndefined();
expect(params.subsets).toBeUndefined(); expect(params.subsets).toBeUndefined();
@@ -108,7 +108,7 @@ describe('mapManagerToParams', () => {
describe('combined output', () => { describe('combined output', () => {
it('produces a complete param object when query and selections coexist', () => { it('produces a complete param object when query and selections coexist', () => {
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: 'inter', queryValue: 'inter',
groups: [ groups: [
group('providers', [['google', true]]), group('providers', [['google', true]]),
@@ -116,7 +116,7 @@ describe('mapManagerToParams', () => {
group('subsets', [['latin', false]]), group('subsets', [['latin', false]]),
], ],
}); });
expect(mapManagerToParams(manager)).toEqual({ expect(mapAppliedFiltersToParams(manager)).toEqual({
q: 'inter', q: 'inter',
providers: ['google'], providers: ['google'],
categories: ['sans-serif'], categories: ['sans-serif'],
@@ -1,5 +1,5 @@
import type { ProxyFontsParams } from '$entities/Font/api'; import type { ProxyFontsParams } from '$entities/Font/api';
import type { FilterManager } from '../../model'; import type { AppliedFilterStore } from '../../model';
/** /**
* Maps filter manager to proxy API parameters. * Maps filter manager to proxy API parameters.
@@ -19,7 +19,7 @@ import type { FilterManager } from '../../model';
* // subsets: ['latin'] * // subsets: ['latin']
* // } * // }
* *
* const params = mapManagerToParams(manager); * const params = mapAppliedFiltersToParams(manager);
* // Returns: { * // Returns: {
* // providers: ['google', 'fontshare'], * // providers: ['google', 'fontshare'],
* // categories: ['sans-serif', 'serif'], * // categories: ['sans-serif', 'serif'],
@@ -28,7 +28,7 @@ import type { FilterManager } from '../../model';
* // } * // }
* ``` * ```
*/ */
export function mapManagerToParams(manager: FilterManager): Partial<ProxyFontsParams> { export function mapAppliedFiltersToParams(manager: AppliedFilterStore): Partial<ProxyFontsParams> {
/** /**
* Return the list of selected values for a group, or undefined when * Return the list of selected values for a group, or undefined when
* the group is missing or has no selection matches the API's * the group is missing or has no selection matches the API's
+11 -11
View File
@@ -16,29 +16,29 @@ export {
/** /**
* Low-level property selection store * Low-level property selection store
*/ */
filtersStore, availableFilterStore,
} from './store/filters/filters.svelte'; } from './store/availableFilterStore/availableFilterStore.svelte';
/** /**
* Main filter controller * Main filter controller
*/ */
export { export {
/** /**
* Factory for constructing a filter manager instance * Reactive interface returned by `createAppliedFilterStore`
*/ */
createFilterManager, type AppliedFilterStore,
/**
* Reactive interface returned by `createFilterManager`
*/
type FilterManager,
/** /**
* High-level manager for syncing search and filters * High-level manager for syncing search and filters
*/ */
filterManager, appliedFilterStore,
} from './store/filterManager/filterManager.svelte'; /**
* Factory for constructing a filter manager instance
*/
createAppliedFilterStore,
} from './store/appliedFilterStore/appliedFilterStore.svelte';
/** /**
* Side-effect import: installs the global filterManager+sortStore → fontStore * Side-effect import: installs the global appliedFilterStore+sortStore → fontStore
* bridge on first import of this feature barrel. No exports. * bridge on first import of this feature barrel. No exports.
*/ */
import './store/bindings.svelte'; import './store/bindings.svelte';
@@ -5,12 +5,12 @@
* debounced search input. Provides reactive state for filter selections * debounced search input. Provides reactive state for filter selections
* and convenience methods for bulk operations. * and convenience methods for bulk operations.
* *
* The factory (`createFilterManager`) is exported for tests; the app * The factory (`createAppliedFilterStore`) is exported for tests; the app
* consumes the `filterManager` singleton at the bottom of this file. * consumes the `appliedFilterStore` singleton at the bottom of this file.
* *
* @example * @example
* ```ts * ```ts
* const manager = createFilterManager({ * const manager = createAppliedFilterStore({
* queryValue: '', * queryValue: '',
* groups: [ * groups: [
* { id: 'providers', label: 'Provider', properties: [...] }, * { id: 'providers', label: 'Provider', properties: [...] },
@@ -39,7 +39,7 @@ import type {
* @param config - Configuration with query value and filter groups * @param config - Configuration with query value and filter groups
* @returns Filter manager instance with reactive state and methods * @returns Filter manager instance with reactive state and methods
*/ */
export function createFilterManager<TValue extends string>(config: FilterConfig<TValue>) { export function createAppliedFilterStore<TValue extends string>(config: FilterConfig<TValue>) {
const search = createDebouncedState(config.queryValue ?? ''); const search = createDebouncedState(config.queryValue ?? '');
// Create filter instances upfront // Create filter instances upfront
@@ -125,16 +125,16 @@ export function createFilterManager<TValue extends string>(config: FilterConfig<
}; };
} }
export type FilterManager = ReturnType<typeof createFilterManager>; export type AppliedFilterStore = ReturnType<typeof createAppliedFilterStore>;
/** /**
* App-wide filter manager singleton. * App-wide filter manager singleton.
* *
* Constructed with empty groups; the filtersStore filterManager wiring * Constructed with empty groups; the availableFilterStore appliedFilterStore wiring
* lives in `./bindings.svelte` and populates groups once backend filter * lives in `./bindings.svelte` and populates groups once backend filter
* metadata arrives. * metadata arrives.
*/ */
export const filterManager = createFilterManager({ export const appliedFilterStore = createAppliedFilterStore({
queryValue: '', queryValue: '',
groups: [], groups: [],
}); });
@@ -7,10 +7,10 @@ import {
it, it,
vi, vi,
} from 'vitest'; } from 'vitest';
import { createFilterManager } from './filterManager.svelte'; import { createAppliedFilterStore } from './appliedFilterStore.svelte';
/** /**
* Test Suite for createFilterManager Helper Function * Test Suite for createAppliedFilterStore Helper Function
* *
* This suite tests the filter manager logic including: * This suite tests the filter manager logic including:
* - Debounced query state (immediate vs delayed) * - Debounced query state (immediate vs delayed)
@@ -54,9 +54,9 @@ function createTestGroups(count: number, propertiesPerGroup = 3) {
})); }));
} }
describe('createFilterManager - Initialization', () => { describe('createAppliedFilterStore - Initialization', () => {
it('creates manager with empty query value', () => { it('creates manager with empty query value', () => {
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: '', queryValue: '',
groups: createTestGroups(2), groups: createTestGroups(2),
}); });
@@ -66,7 +66,7 @@ describe('createFilterManager - Initialization', () => {
}); });
it('creates manager with initial query value', () => { it('creates manager with initial query value', () => {
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: 'search term', queryValue: 'search term',
groups: createTestGroups(1), groups: createTestGroups(1),
}); });
@@ -76,7 +76,7 @@ describe('createFilterManager - Initialization', () => {
}); });
it('creates manager with undefined query value (defaults to empty string)', () => { it('creates manager with undefined query value (defaults to empty string)', () => {
const manager = createFilterManager({ const manager = createAppliedFilterStore({
groups: createTestGroups(1), groups: createTestGroups(1),
}); });
@@ -86,7 +86,7 @@ describe('createFilterManager - Initialization', () => {
it('creates filter groups for each config group', () => { it('creates filter groups for each config group', () => {
const groups = createTestGroups(3); const groups = createTestGroups(3);
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: '', queryValue: '',
groups, groups,
}); });
@@ -99,7 +99,7 @@ describe('createFilterManager - Initialization', () => {
it('creates filter instances for each group', () => { it('creates filter instances for each group', () => {
const groups = createTestGroups(2, 5); const groups = createTestGroups(2, 5);
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: '', queryValue: '',
groups, groups,
}); });
@@ -118,7 +118,7 @@ describe('createFilterManager - Initialization', () => {
{ id: 'providers', label: 'Providers', properties: createTestProperties(2) }, { id: 'providers', label: 'Providers', properties: createTestProperties(2) },
{ id: 'categories', label: 'Categories', properties: createTestProperties(3) }, { id: 'categories', label: 'Categories', properties: createTestProperties(3) },
]; ];
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: '', queryValue: '',
groups, groups,
}); });
@@ -129,7 +129,7 @@ describe('createFilterManager - Initialization', () => {
it('handles single group', () => { it('handles single group', () => {
const groups = createTestGroups(1); const groups = createTestGroups(1);
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: '', queryValue: '',
groups, groups,
}); });
@@ -139,7 +139,7 @@ describe('createFilterManager - Initialization', () => {
}); });
}); });
describe('createFilterManager - Debounced Query', () => { describe('createAppliedFilterStore - Debounced Query', () => {
beforeEach(() => { beforeEach(() => {
vi.useFakeTimers(); vi.useFakeTimers();
}); });
@@ -149,7 +149,7 @@ describe('createFilterManager - Debounced Query', () => {
}); });
it('immediate query value updates instantly', () => { it('immediate query value updates instantly', () => {
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: '', queryValue: '',
groups: createTestGroups(1), groups: createTestGroups(1),
}); });
@@ -161,7 +161,7 @@ describe('createFilterManager - Debounced Query', () => {
}); });
it('debounced query value updates after default delay (300ms)', () => { it('debounced query value updates after default delay (300ms)', () => {
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: '', queryValue: '',
groups: createTestGroups(1), groups: createTestGroups(1),
}); });
@@ -178,7 +178,7 @@ describe('createFilterManager - Debounced Query', () => {
}); });
it('rapid query changes reset the debounce timer', () => { it('rapid query changes reset the debounce timer', () => {
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: '', queryValue: '',
groups: createTestGroups(1), groups: createTestGroups(1),
}); });
@@ -200,7 +200,7 @@ describe('createFilterManager - Debounced Query', () => {
}); });
it('handles empty string in query', () => { it('handles empty string in query', () => {
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: 'initial', queryValue: 'initial',
groups: createTestGroups(1), groups: createTestGroups(1),
}); });
@@ -213,7 +213,7 @@ describe('createFilterManager - Debounced Query', () => {
}); });
it('preserves initial query value until changed', () => { it('preserves initial query value until changed', () => {
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: 'initial search', queryValue: 'initial search',
groups: createTestGroups(1), groups: createTestGroups(1),
}); });
@@ -228,9 +228,9 @@ describe('createFilterManager - Debounced Query', () => {
}); });
}); });
describe('createFilterManager - hasAnySelection Derived State', () => { describe('createAppliedFilterStore - hasAnySelection Derived State', () => {
it('returns false when no filters are selected', () => { it('returns false when no filters are selected', () => {
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: '', queryValue: '',
groups: createTestGroups(3, 3), groups: createTestGroups(3, 3),
}); });
@@ -240,7 +240,7 @@ describe('createFilterManager - hasAnySelection Derived State', () => {
it('returns true when one filter in one group is selected', () => { it('returns true when one filter in one group is selected', () => {
const groups = createTestGroups(2, 3); const groups = createTestGroups(2, 3);
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: '', queryValue: '',
groups, groups,
}); });
@@ -255,7 +255,7 @@ describe('createFilterManager - hasAnySelection Derived State', () => {
it('returns true when multiple filters across groups are selected', () => { it('returns true when multiple filters across groups are selected', () => {
const groups = createTestGroups(3, 3); const groups = createTestGroups(3, 3);
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: '', queryValue: '',
groups, groups,
}); });
@@ -272,7 +272,7 @@ describe('createFilterManager - hasAnySelection Derived State', () => {
it('returns false after deselecting all filters', () => { it('returns false after deselecting all filters', () => {
const groups = createTestGroups(2, 3); const groups = createTestGroups(2, 3);
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: '', queryValue: '',
groups, groups,
}); });
@@ -286,7 +286,7 @@ describe('createFilterManager - hasAnySelection Derived State', () => {
it('reacts to selection changes in individual groups', () => { it('reacts to selection changes in individual groups', () => {
const groups = createTestGroups(2, 3); const groups = createTestGroups(2, 3);
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: '', queryValue: '',
groups, groups,
}); });
@@ -318,7 +318,7 @@ describe('createFilterManager - hasAnySelection Derived State', () => {
properties: createTestProperties(3, []), properties: createTestProperties(3, []),
}, },
]; ];
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: '', queryValue: '',
groups, groups,
}); });
@@ -331,7 +331,7 @@ describe('createFilterManager - hasAnySelection Derived State', () => {
{ id: 'group-0', label: 'Group 0', properties: [] }, { id: 'group-0', label: 'Group 0', properties: [] },
{ id: 'group-1', label: 'Group 1', properties: [] }, { id: 'group-1', label: 'Group 1', properties: [] },
]; ];
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: '', queryValue: '',
groups, groups,
}); });
@@ -340,10 +340,10 @@ describe('createFilterManager - hasAnySelection Derived State', () => {
}); });
}); });
describe('createFilterManager - getGroup() Method', () => { describe('createAppliedFilterStore - getGroup() Method', () => {
it('returns the correct group by ID', () => { it('returns the correct group by ID', () => {
const groups = createTestGroups(3); const groups = createTestGroups(3);
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: '', queryValue: '',
groups, groups,
}); });
@@ -357,7 +357,7 @@ describe('createFilterManager - getGroup() Method', () => {
it('returns undefined for non-existent group ID', () => { it('returns undefined for non-existent group ID', () => {
const groups = createTestGroups(2); const groups = createTestGroups(2);
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: '', queryValue: '',
groups, groups,
}); });
@@ -369,7 +369,7 @@ describe('createFilterManager - getGroup() Method', () => {
it('returns group with accessible filter instance', () => { it('returns group with accessible filter instance', () => {
const groups = createTestGroups(2, 3); const groups = createTestGroups(2, 3);
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: '', queryValue: '',
groups, groups,
}); });
@@ -385,7 +385,7 @@ describe('createFilterManager - getGroup() Method', () => {
it('returns first group when requested', () => { it('returns first group when requested', () => {
const groups = createTestGroups(3); const groups = createTestGroups(3);
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: '', queryValue: '',
groups, groups,
}); });
@@ -398,7 +398,7 @@ describe('createFilterManager - getGroup() Method', () => {
it('returns last group when requested', () => { it('returns last group when requested', () => {
const groups = createTestGroups(5); const groups = createTestGroups(5);
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: '', queryValue: '',
groups, groups,
}); });
@@ -410,10 +410,10 @@ describe('createFilterManager - getGroup() Method', () => {
}); });
}); });
describe('createFilterManager - deselectAllGlobal() Method', () => { describe('createAppliedFilterStore - deselectAllGlobal() Method', () => {
it('deselects all filters across all groups', () => { it('deselects all filters across all groups', () => {
const groups = createTestGroups(3, 3); const groups = createTestGroups(3, 3);
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: '', queryValue: '',
groups, groups,
}); });
@@ -436,7 +436,7 @@ describe('createFilterManager - deselectAllGlobal() Method', () => {
it('handles deselecting when nothing is selected', () => { it('handles deselecting when nothing is selected', () => {
const groups = createTestGroups(2, 3); const groups = createTestGroups(2, 3);
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: '', queryValue: '',
groups, groups,
}); });
@@ -453,7 +453,7 @@ describe('createFilterManager - deselectAllGlobal() Method', () => {
{ id: 'group-0', label: 'Group 0', properties: [] }, { id: 'group-0', label: 'Group 0', properties: [] },
{ id: 'group-1', label: 'Group 1', properties: [] }, { id: 'group-1', label: 'Group 1', properties: [] },
]; ];
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: '', queryValue: '',
groups, groups,
}); });
@@ -464,7 +464,7 @@ describe('createFilterManager - deselectAllGlobal() Method', () => {
it('can select filters after global deselect', () => { it('can select filters after global deselect', () => {
const groups = createTestGroups(2, 3); const groups = createTestGroups(2, 3);
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: '', queryValue: '',
groups, groups,
}); });
@@ -482,7 +482,7 @@ describe('createFilterManager - deselectAllGlobal() Method', () => {
it('handles partially selected groups', () => { it('handles partially selected groups', () => {
const groups = createTestGroups(3, 5); const groups = createTestGroups(3, 5);
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: '', queryValue: '',
groups, groups,
}); });
@@ -505,7 +505,7 @@ describe('createFilterManager - deselectAllGlobal() Method', () => {
}); });
}); });
describe('createFilterManager - Complex Scenarios', () => { describe('createAppliedFilterStore - Complex Scenarios', () => {
beforeEach(() => { beforeEach(() => {
vi.useFakeTimers(); vi.useFakeTimers();
}); });
@@ -516,7 +516,7 @@ describe('createFilterManager - Complex Scenarios', () => {
it('handles query changes and filter selections together', () => { it('handles query changes and filter selections together', () => {
const groups = createTestGroups(2, 3); const groups = createTestGroups(2, 3);
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: '', queryValue: '',
groups, groups,
}); });
@@ -553,7 +553,7 @@ describe('createFilterManager - Complex Scenarios', () => {
}, },
]; ];
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: '', queryValue: '',
groups, groups,
}); });
@@ -582,7 +582,7 @@ describe('createFilterManager - Complex Scenarios', () => {
it('manages multiple independent filter groups correctly', () => { it('manages multiple independent filter groups correctly', () => {
const groups = createTestGroups(4, 5); const groups = createTestGroups(4, 5);
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: '', queryValue: '',
groups, groups,
}); });
@@ -607,7 +607,7 @@ describe('createFilterManager - Complex Scenarios', () => {
it('handles toggle operations via getGroup', () => { it('handles toggle operations via getGroup', () => {
const groups = createTestGroups(2, 3); const groups = createTestGroups(2, 3);
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: '', queryValue: '',
groups, groups,
}); });
@@ -623,9 +623,9 @@ describe('createFilterManager - Complex Scenarios', () => {
}); });
}); });
describe('createFilterManager - Interface Compliance', () => { describe('createAppliedFilterStore - Interface Compliance', () => {
it('exposes queryValue getter', () => { it('exposes queryValue getter', () => {
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: 'test', queryValue: 'test',
groups: createTestGroups(1), groups: createTestGroups(1),
}); });
@@ -636,7 +636,7 @@ describe('createFilterManager - Interface Compliance', () => {
}); });
it('exposes queryValue setter', () => { it('exposes queryValue setter', () => {
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: 'test', queryValue: 'test',
groups: createTestGroups(1), groups: createTestGroups(1),
}); });
@@ -647,7 +647,7 @@ describe('createFilterManager - Interface Compliance', () => {
}); });
it('exposes debouncedQueryValue getter', () => { it('exposes debouncedQueryValue getter', () => {
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: 'test', queryValue: 'test',
groups: createTestGroups(1), groups: createTestGroups(1),
}); });
@@ -658,7 +658,7 @@ describe('createFilterManager - Interface Compliance', () => {
}); });
it('exposes groups getter', () => { it('exposes groups getter', () => {
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: '', queryValue: '',
groups: createTestGroups(1), groups: createTestGroups(1),
}); });
@@ -669,7 +669,7 @@ describe('createFilterManager - Interface Compliance', () => {
}); });
it('exposes hasAnySelection getter', () => { it('exposes hasAnySelection getter', () => {
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: '', queryValue: '',
groups: createTestGroups(1), groups: createTestGroups(1),
}); });
@@ -680,7 +680,7 @@ describe('createFilterManager - Interface Compliance', () => {
}); });
it('exposes getGroup method', () => { it('exposes getGroup method', () => {
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: '', queryValue: '',
groups: createTestGroups(1), groups: createTestGroups(1),
}); });
@@ -689,7 +689,7 @@ describe('createFilterManager - Interface Compliance', () => {
}); });
it('exposes deselectAllGlobal method', () => { it('exposes deselectAllGlobal method', () => {
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: '', queryValue: '',
groups: createTestGroups(1), groups: createTestGroups(1),
}); });
@@ -698,7 +698,7 @@ describe('createFilterManager - Interface Compliance', () => {
}); });
it('does not expose debouncedQueryValue setter', () => { it('does not expose debouncedQueryValue setter', () => {
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: '', queryValue: '',
groups: createTestGroups(1), groups: createTestGroups(1),
}); });
@@ -708,7 +708,7 @@ describe('createFilterManager - Interface Compliance', () => {
}); });
}); });
describe('createFilterManager - Edge Cases', () => { describe('createAppliedFilterStore - Edge Cases', () => {
it('handles single property groups', () => { it('handles single property groups', () => {
const groups: Array<{ const groups: Array<{
id: string; id: string;
@@ -722,7 +722,7 @@ describe('createFilterManager - Edge Cases', () => {
}, },
]; ];
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: '', queryValue: '',
groups, groups,
}); });
@@ -749,7 +749,7 @@ describe('createFilterManager - Edge Cases', () => {
}, },
]; ];
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: '', queryValue: '',
groups, groups,
}); });
@@ -773,7 +773,7 @@ describe('createFilterManager - Edge Cases', () => {
}, },
]; ];
const manager = createFilterManager({ const manager = createAppliedFilterStore({
queryValue: '', queryValue: '',
groups, groups,
}); });
@@ -6,12 +6,12 @@
* *
* @example * @example
* ```ts * ```ts
* import { filtersStore } from '$features/GetFonts'; * import { availableFilterStore } from '$features/GetFonts';
* *
* // Access filters (reactive) * // Access filters (reactive)
* $: filters = filtersStore.filters; * $: filters = availableFilterStore.filters;
* $: isLoading = filtersStore.isLoading; * $: isLoading = availableFilterStore.isLoading;
* $: error = filtersStore.error; * $: error = availableFilterStore.error;
* ``` * ```
*/ */
@@ -31,7 +31,7 @@ import {
* Fetches and caches filter metadata using fetchProxyFilters() * Fetches and caches filter metadata using fetchProxyFilters()
* Provides reactive access to filter data * Provides reactive access to filter data
*/ */
export class FiltersStore { export class AvailableFilterStore {
/** /**
* TanStack Query result state * TanStack Query result state
*/ */
@@ -125,4 +125,4 @@ export class FiltersStore {
/** /**
* Singleton instance * Singleton instance
*/ */
export const filtersStore = new FiltersStore(); export const availableFilterStore = new AvailableFilterStore();
@@ -9,7 +9,7 @@ import {
} from 'vitest'; } from 'vitest';
import * as filtersApi from '../../../api/filters/filters'; import * as filtersApi from '../../../api/filters/filters';
import type { FilterMetadata } from '../../../api/filters/filters'; import type { FilterMetadata } from '../../../api/filters/filters';
import { FiltersStore } from './filters.svelte'; import { AvailableFilterStore } from './availableFilterStore.svelte';
/** /**
* Build a minimal FilterMetadata fixture for tests. * Build a minimal FilterMetadata fixture for tests.
@@ -29,8 +29,8 @@ function metadata(id: string, optionValues: string[] = []): FilterMetadata {
} as FilterMetadata; } as FilterMetadata;
} }
describe('FiltersStore', () => { describe('AvailableFilterStore', () => {
let store: FiltersStore; let store: AvailableFilterStore;
beforeEach(() => { beforeEach(() => {
queryClient.clear(); queryClient.clear();
@@ -47,13 +47,13 @@ describe('FiltersStore', () => {
describe('initial state', () => { describe('initial state', () => {
it('starts with an empty filter list', () => { it('starts with an empty filter list', () => {
vi.spyOn(filtersApi, 'fetchProxyFilters').mockResolvedValue([]); vi.spyOn(filtersApi, 'fetchProxyFilters').mockResolvedValue([]);
store = new FiltersStore(); store = new AvailableFilterStore();
expect(store.filters).toEqual([]); expect(store.filters).toEqual([]);
}); });
it('reports null error before any failure', () => { it('reports null error before any failure', () => {
vi.spyOn(filtersApi, 'fetchProxyFilters').mockResolvedValue([]); vi.spyOn(filtersApi, 'fetchProxyFilters').mockResolvedValue([]);
store = new FiltersStore(); store = new AvailableFilterStore();
expect(store.error).toBeNull(); expect(store.error).toBeNull();
}); });
}); });
@@ -66,7 +66,7 @@ describe('FiltersStore', () => {
]; ];
vi.spyOn(filtersApi, 'fetchProxyFilters').mockResolvedValue(data); vi.spyOn(filtersApi, 'fetchProxyFilters').mockResolvedValue(data);
store = new FiltersStore(); store = new AvailableFilterStore();
await vi.waitFor(() => expect(store.filters).toEqual(data), { timeout: 1000 }); await vi.waitFor(() => expect(store.filters).toEqual(data), { timeout: 1000 });
expect(store.isError).toBe(false); expect(store.isError).toBe(false);
@@ -75,7 +75,7 @@ describe('FiltersStore', () => {
it('calls fetchProxyFilters exactly once for the initial load', async () => { it('calls fetchProxyFilters exactly once for the initial load', async () => {
const spy = vi.spyOn(filtersApi, 'fetchProxyFilters').mockResolvedValue([]); const spy = vi.spyOn(filtersApi, 'fetchProxyFilters').mockResolvedValue([]);
store = new FiltersStore(); store = new AvailableFilterStore();
await vi.waitFor(() => expect(spy).toHaveBeenCalledTimes(1), { timeout: 1000 }); await vi.waitFor(() => expect(spy).toHaveBeenCalledTimes(1), { timeout: 1000 });
}); });
@@ -84,7 +84,7 @@ describe('FiltersStore', () => {
describe('error handling', () => { describe('error handling', () => {
it('flips isError and exposes the error message on fetch failure', async () => { it('flips isError and exposes the error message on fetch failure', async () => {
vi.spyOn(filtersApi, 'fetchProxyFilters').mockRejectedValue(new Error('boom')); vi.spyOn(filtersApi, 'fetchProxyFilters').mockRejectedValue(new Error('boom'));
store = new FiltersStore(); store = new AvailableFilterStore();
await vi.waitFor(() => expect(store.isError).toBe(true), { timeout: 1000 }); await vi.waitFor(() => expect(store.isError).toBe(true), { timeout: 1000 });
expect(store.error).toBe('boom'); expect(store.error).toBe('boom');
@@ -97,13 +97,13 @@ describe('FiltersStore', () => {
const data = [metadata('providers', ['google'])]; const data = [metadata('providers', ['google'])];
const spy = vi.spyOn(filtersApi, 'fetchProxyFilters').mockResolvedValue(data); const spy = vi.spyOn(filtersApi, 'fetchProxyFilters').mockResolvedValue(data);
store = new FiltersStore(); store = new AvailableFilterStore();
await vi.waitFor(() => expect(store.filters).toEqual(data), { timeout: 1000 }); await vi.waitFor(() => expect(store.filters).toEqual(data), { timeout: 1000 });
expect(spy).toHaveBeenCalledTimes(1); expect(spy).toHaveBeenCalledTimes(1);
// A second observer on the same query key should reuse the cached // A second observer on the same query key should reuse the cached
// result rather than triggering a new request. // result rather than triggering a new request.
const second = new FiltersStore(); const second = new AvailableFilterStore();
try { try {
// Give the new observer a tick to potentially refetch (it shouldn't). // Give the new observer a tick to potentially refetch (it shouldn't).
await new Promise(r => setTimeout(r, 50)); await new Promise(r => setTimeout(r, 50));
@@ -1,5 +1,5 @@
/** /**
* Bridges feature-level UI state (filterManager + sortStore) to the * Bridges feature-level UI state (appliedFilterStore + sortStore) to the
* entity-level fontStore query params. * entity-level fontStore query params.
* *
* Centralizing this here means consumers (Search, FontSearch, * Centralizing this here means consumers (Search, FontSearch,
@@ -11,22 +11,22 @@
import { fontStore } from '$entities/Font'; import { fontStore } from '$entities/Font';
import { untrack } from 'svelte'; import { untrack } from 'svelte';
import { mapManagerToParams } from '../../lib/mapper/mapManagerToParams'; import { mapAppliedFiltersToParams } from '../../lib/mapper/mapAppliedFiltersToParams';
import { filterManager } from './filterManager/filterManager.svelte'; import { appliedFilterStore } from './appliedFilterStore/appliedFilterStore.svelte';
import { filtersStore } from './filters/filters.svelte'; import { availableFilterStore } from './availableFilterStore/availableFilterStore.svelte';
import { sortStore } from './sortStore/sortStore.svelte'; import { sortStore } from './sortStore/sortStore.svelte';
$effect.root(() => { $effect.root(() => {
/** /**
* Populate filterManager groups when backend filter metadata resolves. * Populate appliedFilterStore groups when backend filter metadata resolves.
* filtersStore is async; until it loads, filterManager has empty groups * availableFilterStore is async; until it loads, appliedFilterStore has empty groups
* and the UI renders nothing for them. * and the UI renders nothing for them.
*/ */
$effect(() => { $effect(() => {
const dynamicFilters = filtersStore.filters; const dynamicFilters = availableFilterStore.filters;
if (dynamicFilters.length > 0) { if (dynamicFilters.length > 0) {
filterManager.setGroups( appliedFilterStore.setGroups(
dynamicFilters.map(filter => ({ dynamicFilters.map(filter => ({
id: filter.id, id: filter.id,
label: filter.name, label: filter.name,
@@ -47,7 +47,7 @@ $effect.root(() => {
* into this effect's dependency graph. * into this effect's dependency graph.
*/ */
$effect(() => { $effect(() => {
const params = mapManagerToParams(filterManager); const params = mapAppliedFiltersToParams(appliedFilterStore);
untrack(() => fontStore.setParams(params)); untrack(() => fontStore.setParams(params));
}); });
@@ -9,7 +9,7 @@ const { Story } = defineMeta({
docs: { docs: {
description: { description: {
component: component:
'Renders the full list of filter groups managed by filterManager. Each group maps to a collapsible FilterGroup with checkboxes. No props — reads directly from the filterManager singleton.', 'Renders the full list of filter groups managed by appliedFilterStore. Each group maps to a collapsible FilterGroup with checkboxes. No props — reads directly from the appliedFilterStore singleton.',
}, },
story: { inline: false }, story: { inline: false },
}, },
@@ -4,10 +4,10 @@
--> -->
<script lang="ts"> <script lang="ts">
import { FilterGroup } from '$shared/ui'; import { FilterGroup } from '$shared/ui';
import { filterManager } from '../../model'; import { appliedFilterStore } from '../../model';
</script> </script>
{#each filterManager.groups as group (group.id)} {#each appliedFilterStore.groups as group (group.id)}
<FilterGroup <FilterGroup
displayedLabel={group.label} displayedLabel={group.label}
filter={group.instance} filter={group.instance}
@@ -1,6 +1,6 @@
import { import {
filterManager, appliedFilterStore,
filtersStore, availableFilterStore,
} from '$features/GetFonts'; } from '$features/GetFonts';
import { import {
render, render,
@@ -11,9 +11,9 @@ import Filters from './Filters.svelte';
describe('Filters', () => { describe('Filters', () => {
beforeEach(() => { beforeEach(() => {
// Clear groups and mock filtersStore to be empty so the auto-sync effect doesn't overwrite us // Clear groups and mock availableFilterStore to be empty so the auto-sync effect doesn't overwrite us
filterManager.setGroups([]); appliedFilterStore.setGroups([]);
vi.spyOn(filtersStore, 'filters', 'get').mockReturnValue([]); vi.spyOn(availableFilterStore, 'filters', 'get').mockReturnValue([]);
}); });
afterEach(() => { afterEach(() => {
@@ -28,7 +28,7 @@ describe('Filters', () => {
}); });
it('renders a label for each filter group', () => { it('renders a label for each filter group', () => {
filterManager.setGroups([ appliedFilterStore.setGroups([
{ id: 'cat', label: 'Categories', properties: [] }, { id: 'cat', label: 'Categories', properties: [] },
{ id: 'prov', label: 'Font Providers', properties: [] }, { id: 'prov', label: 'Font Providers', properties: [] },
]); ]);
@@ -38,7 +38,7 @@ describe('Filters', () => {
}); });
it('renders filter properties within groups', () => { it('renders filter properties within groups', () => {
filterManager.setGroups([ appliedFilterStore.setGroups([
{ {
id: 'cat', id: 'cat',
label: 'Category', label: 'Category',
@@ -54,7 +54,7 @@ describe('Filters', () => {
}); });
it('renders multiple groups with their properties', () => { it('renders multiple groups with their properties', () => {
filterManager.setGroups([ appliedFilterStore.setGroups([
{ {
id: 'cat', id: 'cat',
label: 'Category', label: 'Category',
@@ -10,7 +10,7 @@ const { Story } = defineMeta({
docs: { docs: {
description: { description: {
component: component:
'Sort options and Reset_Filters button rendered below the filter list. Reads sort state from sortStore and dispatches resets via filterManager. Requires responsive context — wrap with Providers.', 'Sort options and Reset_Filters button rendered below the filter list. Reads sort state from sortStore and dispatches resets via appliedFilterStore. Requires responsive context — wrap with Providers.',
}, },
story: { inline: false }, story: { inline: false },
}, },
@@ -12,7 +12,7 @@ import RefreshCwIcon from '@lucide/svelte/icons/refresh-cw';
import { getContext } from 'svelte'; import { getContext } from 'svelte';
import { import {
SORT_OPTIONS, SORT_OPTIONS,
filterManager, appliedFilterStore,
sortStore, sortStore,
} from '../../model'; } from '../../model';
@@ -31,7 +31,7 @@ const responsive = getContext<ResponsiveManager>('responsive');
const isMobileOrTabletPortrait = $derived(responsive.isMobile || responsive.isTabletPortrait); const isMobileOrTabletPortrait = $derived(responsive.isMobile || responsive.isTabletPortrait);
function handleReset() { function handleReset() {
filterManager.deselectAllGlobal(); appliedFilterStore.deselectAllGlobal();
} }
</script> </script>
@@ -1,11 +1,11 @@
<!-- <!--
Component: Search Component: Search
Typeface search input for the comparison view. Typeface search input for the comparison view.
Writes through filterManager; the global bridge in $features/GetFonts Writes through appliedFilterStore; the global bridge in $features/GetFonts
propagates the value into fontStore. propagates the value into fontStore.
--> -->
<script lang="ts"> <script lang="ts">
import { filterManager } from '$features/GetFonts'; import { appliedFilterStore } from '$features/GetFonts';
import { SearchBar } from '$shared/ui'; import { SearchBar } from '$shared/ui';
</script> </script>
@@ -15,7 +15,7 @@ import { SearchBar } from '$shared/ui';
class="w-full" class="w-full"
placeholder="Typeface Search" placeholder="Typeface Search"
aria-label="Search typefaces" aria-label="Search typefaces"
bind:value={filterManager.queryValue} bind:value={appliedFilterStore.queryValue}
fullWidth fullWidth
/> />
</div> </div>
@@ -1,4 +1,4 @@
import { filterManager } from '$features/GetFonts'; import { appliedFilterStore } from '$features/GetFonts';
import { import {
render, render,
screen, screen,
@@ -7,7 +7,7 @@ import Search from './Search.svelte';
describe('Search', () => { describe('Search', () => {
beforeEach(() => { beforeEach(() => {
filterManager.queryValue = ''; appliedFilterStore.queryValue = '';
}); });
describe('Rendering', () => { describe('Rendering', () => {
@@ -23,8 +23,8 @@ describe('Search', () => {
}); });
describe('Value binding', () => { describe('Value binding', () => {
it('reflects filterManager.queryValue as initial value', () => { it('reflects appliedFilterStore.queryValue as initial value', () => {
filterManager.queryValue = 'Inter'; appliedFilterStore.queryValue = 'Inter';
render(Search); render(Search);
expect(screen.getByRole('textbox')).toHaveValue('Inter'); expect(screen.getByRole('textbox')).toHaveValue('Inter');
}); });
@@ -6,7 +6,7 @@
import { import {
FilterControls, FilterControls,
Filters, Filters,
filterManager, appliedFilterStore,
} from '$features/GetFonts'; } from '$features/GetFonts';
import { springySlideFade } from '$shared/lib'; import { springySlideFade } from '$shared/lib';
import { import {
@@ -58,7 +58,7 @@ function toggleFilters() {
class="w-full" class="w-full"
placeholder="Typeface Search" placeholder="Typeface Search"
aria-label="Search typefaces" aria-label="Search typefaces"
bind:value={filterManager.queryValue} bind:value={appliedFilterStore.queryValue}
fullWidth fullWidth
/> />