From eb10d581284b6349598b394a9240f4b6a4f991dc Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Tue, 6 Jan 2026 21:35:49 +0300 Subject: [PATCH] chore: move store creators to separate directories --- src/shared/store/createControlStore.test.ts | 89 -------- src/shared/store/createControlStore.ts | 117 ---------- src/shared/store/createFilterStore.test.ts | 136 ------------ src/shared/store/createFilterStore.ts | 226 -------------------- 4 files changed, 568 deletions(-) delete mode 100644 src/shared/store/createControlStore.test.ts delete mode 100644 src/shared/store/createControlStore.ts delete mode 100644 src/shared/store/createFilterStore.test.ts delete mode 100644 src/shared/store/createFilterStore.ts diff --git a/src/shared/store/createControlStore.test.ts b/src/shared/store/createControlStore.test.ts deleted file mode 100644 index fb9ce03..0000000 --- a/src/shared/store/createControlStore.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { get } from 'svelte/store'; -import { - beforeEach, - describe, - expect, - it, -} from 'vitest'; -import { - type ControlModel, - createControlStore, -} from './createControlStore'; - -describe('createControlStore', () => { - let store: ReturnType>; - - beforeEach(() => { - const initialState: ControlModel = { - value: 10, - min: 0, - max: 100, - step: 5, - }; - store = createControlStore(initialState); - }); - - it('initializes with correct state', () => { - expect(get(store)).toEqual({ - value: 10, - min: 0, - max: 100, - step: 5, - }); - }); - - it('increases value by step', () => { - store.increase(); - expect(get(store).value).toBe(15); - }); - - it('decreases value by step', () => { - store.decrease(); - expect(get(store).value).toBe(5); - }); - - it('clamps value at maximum', () => { - store.setValue(200); - expect(get(store).value).toBe(100); - }); - - it('clamps value at minimum', () => { - store.setValue(-10); - expect(get(store).value).toBe(0); - }); - - it('rounds to step precision', () => { - store.setValue(12.34); - // With step=5, 12.34 is clamped and rounded to nearest integer (0 decimal places) - expect(get(store).value).toBe(12); - }); - - it('handles decimal steps correctly', () => { - const decimalStore = createControlStore({ - value: 1.0, - min: 0, - max: 2, - step: 0.05, - }); - decimalStore.increase(); - expect(get(decimalStore).value).toBe(1.05); - }); - - it('isAtMax returns true when at maximum', () => { - store.setValue(100); - expect(store.isAtMax()).toBe(true); - }); - - it('isAtMax returns false when not at maximum', () => { - expect(store.isAtMax()).toBe(false); - }); - - it('isAtMin returns true when at minimum', () => { - store.setValue(0); - expect(store.isAtMin()).toBe(true); - }); - - it('isAtMin returns false when not at minimum', () => { - expect(store.isAtMin()).toBe(false); - }); -}); diff --git a/src/shared/store/createControlStore.ts b/src/shared/store/createControlStore.ts deleted file mode 100644 index a7e7463..0000000 --- a/src/shared/store/createControlStore.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { - type Writable, - get, - writable, -} from 'svelte/store'; - -/** - * Model for a control value with min/max bounds - */ -export type ControlModel< - TValue extends number = number, -> = { - value: TValue; - min: TValue; - max: TValue; - step?: TValue; -}; - -/** - * Store model with methods for control manipulation - */ -export type ControlStoreModel< - TValue extends number, -> = - & Writable> - & { - increase: () => void; - decrease: () => void; - /** Set a specific value */ - setValue: (newValue: TValue) => void; - isAtMax: () => boolean; - isAtMin: () => boolean; - }; - -/** - * Create a writable store for numeric control values with bounds - * - * @template TValue - The value type (extends number) - * @param initialState - Initial state containing value, min, and max - */ -/** - * Get the number of decimal places in a number - * - * For example: - * - 1 -> 0 - * - 0.1 -> 1 - * - 0.01 -> 2 - * - 0.05 -> 2 - * - * @param step - The step number to analyze - * @returns The number of decimal places - */ -function getDecimalPlaces(step: number): number { - const str = step.toString(); - const decimalPart = str.split('.')[1]; - return decimalPart ? decimalPart.length : 0; -} - -/** - * Round a value to the precision of the given step - * - * This fixes floating-point precision errors that occur with decimal steps. - * For example, with step=0.05, adding it repeatedly can produce values like - * 1.3499999999999999 instead of 1.35. - * - * We use toFixed() to round to the appropriate decimal places instead of - * Math.round(value / step) * step, which doesn't always work correctly - * due to floating-point arithmetic errors. - * - * @param value - The value to round - * @param step - The step to round to (defaults to 1) - * @returns The rounded value - */ -function roundToStepPrecision(value: number, step: number = 1): number { - if (step <= 0) { - return value; - } - const decimals = getDecimalPlaces(step); - return parseFloat(value.toFixed(decimals)); -} - -export function createControlStore< - TValue extends number = number, ->( - initialState: ControlModel, -): ControlStoreModel { - const store = writable(initialState); - const { subscribe, set, update } = store; - - const clamp = (value: number): TValue => { - return Math.max(initialState.min, Math.min(value, initialState.max)) as TValue; - }; - - return { - subscribe, - set, - update, - increase: () => - update(m => { - const step = m.step ?? 1; - const newValue = clamp(m.value + step); - return { ...m, value: roundToStepPrecision(newValue, step) as TValue }; - }), - decrease: () => - update(m => { - const step = m.step ?? 1; - const newValue = clamp(m.value - step); - return { ...m, value: roundToStepPrecision(newValue, step) as TValue }; - }), - setValue: (v: TValue) => { - const step = initialState.step ?? 1; - update(m => ({ ...m, value: roundToStepPrecision(clamp(v), step) as TValue })); - }, - isAtMin: () => get(store).value === initialState.min, - isAtMax: () => get(store).value === initialState.max, - }; -} diff --git a/src/shared/store/createFilterStore.test.ts b/src/shared/store/createFilterStore.test.ts deleted file mode 100644 index 3b94235..0000000 --- a/src/shared/store/createFilterStore.test.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { get } from 'svelte/store'; -import { - beforeEach, - describe, - expect, - it, -} from 'vitest'; -import { - type FilterModel, - type Property, - createFilterStore, -} from './createFilterStore'; - -describe('createFilterStore', () => { - const mockProperties: Property[] = [ - { id: '1', name: 'Sans-serif', selected: false }, - { id: '2', name: 'Serif', selected: false }, - { id: '3', name: 'Display', selected: false }, - ]; - - let store: ReturnType; - - beforeEach(() => { - const initialState: FilterModel = { - searchQuery: '', - properties: mockProperties, - }; - store = createFilterStore(initialState); - }); - - it('initializes with correct state', () => { - const state = get(store); - expect(state).toEqual({ - searchQuery: '', - properties: mockProperties, - }); - }); - - it('sets search query', () => { - store.setSearchQuery('serif'); - const state = get(store); - expect(state.searchQuery).toBe('serif'); - }); - - it('clears search query', () => { - store.setSearchQuery('test'); - store.clearSearchQuery(); - const state = get(store); - expect(state.searchQuery).toBeUndefined(); - }); - - it('selects a property', () => { - store.selectProperty('1'); - const state = get(store); - const property = state.properties.find(p => p.id === '1'); - expect(property?.selected).toBe(true); - }); - - it('deselects a property', () => { - store.selectProperty('1'); - store.deselectProperty('1'); - const state = get(store); - const property = state.properties.find(p => p.id === '1'); - expect(property?.selected).toBe(false); - }); - - it('toggles property from unselected to selected', () => { - store.toggleProperty('1'); - const state = get(store); - const property = state.properties.find(p => p.id === '1'); - expect(property?.selected).toBe(true); - }); - - it('toggles property from selected to unselected', () => { - store.selectProperty('1'); - store.toggleProperty('1'); - const state = get(store); - const property = state.properties.find(p => p.id === '1'); - expect(property?.selected).toBe(false); - }); - - it('selects all properties', () => { - store.selectAllProperties(); - const state = get(store); - expect(state.properties.every(p => p.selected)).toBe(true); - }); - - it('deselects all properties', () => { - store.selectAllProperties(); - store.deselectAllProperties(); - const state = get(store); - expect(state.properties.every(p => !p.selected)).toBe(true); - }); - - it('gets all properties', () => { - const allProps = store.getAllProperties(); - const props = get(allProps); - expect(props).toEqual(mockProperties); - }); - - it('gets selected properties', () => { - store.selectProperty('1'); - store.selectProperty('3'); - const selectedProps = store.getSelectedProperties(); - const props = get(selectedProps); - expect(props).toHaveLength(2); - expect(props?.[0].id).toBe('1'); - expect(props?.[1].id).toBe('3'); - }); - - it('filters properties by search query', () => { - store.setSearchQuery('serif'); - const filteredProps = store.getFilteredProperties(); - const props = get(filteredProps); - // 'serif' is a substring of 'Sans-serif' (case-sensitive match) - expect(props).toHaveLength(1); - expect(props?.[0].id).toBe('1'); - }); - - it('filter is case-sensitive', () => { - store.setSearchQuery('San'); - const filteredProps = store.getFilteredProperties(); - const props = get(filteredProps); - // 'San' matches 'Sans-serif' exactly (case-sensitive) - expect(props).toHaveLength(1); - expect(props?.[0].id).toBe('1'); - }); - - it('filter returns all properties when query is empty', () => { - store.setSearchQuery(''); - const filteredProps = store.getFilteredProperties(); - let props: Property[] | undefined = undefined; - filteredProps.subscribe(p => (props = p))(); - expect(props).toHaveLength(3); - }); -}); diff --git a/src/shared/store/createFilterStore.ts b/src/shared/store/createFilterStore.ts deleted file mode 100644 index a15ab0f..0000000 --- a/src/shared/store/createFilterStore.ts +++ /dev/null @@ -1,226 +0,0 @@ -import { - type Readable, - type Writable, - derived, - writable, -} from 'svelte/store'; - -export interface Property { - /** - * Property identifier - */ - id: string; - /** - * Property name - */ - name: string; - /** - * Property selected state - */ - selected?: boolean; -} - -export interface FilterModel { - /** - * Search query - */ - searchQuery?: string; - /** - * Properties - */ - properties: Property[]; -} - -/** - * Model for reusable filter store with search support and property selection - */ -export interface FilterStore extends Writable { - /** - * Get the store. - * @returns Readable store with filter data - */ - getStore: () => Readable; - /** - * Get all properties. - * @returns Readable store with properties - */ - getAllProperties: () => Readable; - /** - * Get the selected properties. - * @returns Readable store with selected properties - */ - getSelectedProperties: () => Readable; - /** - * Get the filtered properties. - * @returns Readable store with filtered properties - */ - getFilteredProperties: () => Readable; - /** - * Update the search query filter. - * - * @param searchQuery - Search text (undefined to clear) - */ - setSearchQuery: (searchQuery: string | undefined) => void; - /** - * Clear the search query filter. - */ - clearSearchQuery: () => void; - /** - * Select a property. - * - * @param property - Property to select - */ - selectProperty: (propertyId: string) => void; - /** - * Deselect a property. - * - * @param property - Property to deselect - */ - deselectProperty: (propertyId: string) => void; - /** - * Toggle a property. - * - * @param propertyId - Property ID - */ - toggleProperty: (propertyId: string) => void; - /** - * Select all properties. - */ - selectAllProperties: () => void; - /** - * Deselect all properties. - */ - deselectAllProperties: () => void; -} - -/** - * Create a filter store. - * @param initialState - Initial state of the filter store - * @returns FilterStore - */ -export function createFilterStore( - initialState?: T, -): FilterStore { - const { subscribe, set, update } = writable(initialState); - - return { - /* - * Expose subscribe, set, and update from Writable. - * This makes FilterStore compatible with Writable interface. - */ - subscribe, - set, - update, - /** - * Get the current state of the filter store. - */ - getStore: () => { - return { - subscribe, - }; - }, - /** - * Get the filtered properties. - */ - getAllProperties: () => { - return derived({ subscribe }, $store => { - return $store.properties; - }); - }, - /** - * Get the selected properties. - */ - getSelectedProperties: () => { - return derived({ subscribe }, $store => { - return $store.properties.filter(property => property.selected); - }); - }, - /** - * Get the filtered properties. - */ - getFilteredProperties: () => { - return derived({ subscribe }, $store => { - return $store.properties.filter(property => - property.name.includes($store.searchQuery || '') - ); - }); - }, - /** - * Update the search query filter. - * - * @param searchQuery - Search text (undefined to clear) - */ - setSearchQuery: (searchQuery: string | undefined) => { - update(state => ({ - ...state, - searchQuery: searchQuery || undefined, - })); - }, - /** - * Clear the search query filter. - */ - clearSearchQuery: () => { - update(state => ({ - ...state, - searchQuery: undefined, - })); - }, - /** - * Select a property. - * - * @param propertyId - Property ID - */ - selectProperty: (propertyId: string) => { - update(state => ({ - ...state, - properties: state.properties.map(c => - c.id === propertyId ? { ...c, selected: true } : c - ), - })); - }, - /** - * Deselect a property. - * - * @param propertyId - Property ID - */ - deselectProperty: (propertyId: string) => { - update(state => ({ - ...state, - properties: state.properties.map(c => - c.id === propertyId ? { ...c, selected: false } : c - ), - })); - }, - /** - * Toggle a property. - * - * @param propertyId - Property ID - */ - toggleProperty: (propertyId: string) => { - update(state => ({ - ...state, - properties: state.properties.map(c => - c.id === propertyId ? { ...c, selected: !c.selected } : c - ), - })); - }, - /** - * Select all properties - */ - selectAllProperties: () => { - update(state => ({ - ...state, - properties: state.properties.map(c => ({ ...c, selected: true })), - })); - }, - /** - * Deselect all properties - */ - deselectAllProperties: () => { - update(state => ({ - ...state, - properties: state.properties.map(c => ({ ...c, selected: false })), - })); - }, - }; -}