chore: move store creators to separate directories

This commit is contained in:
Ilia Mashkov
2026-01-06 21:33:30 +03:00
parent d1f035a6ad
commit bea3f7ae7f
8 changed files with 349 additions and 6 deletions

View File

@@ -1,7 +1,7 @@
import {
type FilterModel,
createFilterStore,
} from '$shared/store/createFilterStore';
} from '$shared/lib/store/createFilterStore/createFilterStore';
import { FONT_CATEGORIES } from '../const/const';
/**

View File

@@ -1,7 +1,7 @@
import {
type FilterModel,
createFilterStore,
} from '$shared/store/createFilterStore';
} from '$shared/lib/store/createFilterStore/createFilterStore';
import { FONT_PROVIDERS } from '../const/const';
/**

View File

@@ -1,7 +1,7 @@
import {
type FilterModel,
createFilterStore,
} from '$shared/store/createFilterStore';
} from '$shared/lib/store/createFilterStore/createFilterStore';
import { FONT_SUBSETS } from '../const/const';
/**

View File

@@ -1,7 +1,7 @@
import {
type ControlModel,
createControlStore,
} from '$shared/store/createControlStore';
} from '$shared/lib/store/createControlStore/createControlStore';
import {
DEFAULT_FONT_SIZE,
MAX_FONT_SIZE,

View File

@@ -1,7 +1,7 @@
import {
type ControlModel,
createControlStore,
} from '$shared/store/createControlStore';
} from '$shared/lib/store/createControlStore/createControlStore';
import {
DEFAULT_FONT_WEIGHT,
FONT_WEIGHT_STEP,

View File

@@ -1,7 +1,7 @@
import {
type ControlModel,
createControlStore,
} from '$shared/store/createControlStore';
} from '$shared/lib/store/createControlStore/createControlStore';
import {
DEFAULT_LINE_HEIGHT,
LINE_HEIGHT_STEP,

View File

@@ -0,0 +1,117 @@
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<ControlModel<TValue>>
& {
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<TValue>,
): ControlStoreModel<TValue> {
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,
};
}

View File

@@ -0,0 +1,226 @@
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<T extends FilterModel> extends Writable<T> {
/**
* Get the store.
* @returns Readable store with filter data
*/
getStore: () => Readable<T>;
/**
* Get all properties.
* @returns Readable store with properties
*/
getAllProperties: () => Readable<Property[]>;
/**
* Get the selected properties.
* @returns Readable store with selected properties
*/
getSelectedProperties: () => Readable<Property[]>;
/**
* Get the filtered properties.
* @returns Readable store with filtered properties
*/
getFilteredProperties: () => Readable<Property[]>;
/**
* 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<T>
*/
export function createFilterStore<T extends FilterModel>(
initialState?: T,
): FilterStore<T> {
const { subscribe, set, update } = writable<T>(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 })),
}));
},
};
}