chore: move store creators to separate directories
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
type FilterModel,
|
type FilterModel,
|
||||||
createFilterStore,
|
createFilterStore,
|
||||||
} from '$shared/store/createFilterStore';
|
} from '$shared/lib/store/createFilterStore/createFilterStore';
|
||||||
import { FONT_CATEGORIES } from '../const/const';
|
import { FONT_CATEGORIES } from '../const/const';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
type FilterModel,
|
type FilterModel,
|
||||||
createFilterStore,
|
createFilterStore,
|
||||||
} from '$shared/store/createFilterStore';
|
} from '$shared/lib/store/createFilterStore/createFilterStore';
|
||||||
import { FONT_PROVIDERS } from '../const/const';
|
import { FONT_PROVIDERS } from '../const/const';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
type FilterModel,
|
type FilterModel,
|
||||||
createFilterStore,
|
createFilterStore,
|
||||||
} from '$shared/store/createFilterStore';
|
} from '$shared/lib/store/createFilterStore/createFilterStore';
|
||||||
import { FONT_SUBSETS } from '../const/const';
|
import { FONT_SUBSETS } from '../const/const';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
type ControlModel,
|
type ControlModel,
|
||||||
createControlStore,
|
createControlStore,
|
||||||
} from '$shared/store/createControlStore';
|
} from '$shared/lib/store/createControlStore/createControlStore';
|
||||||
import {
|
import {
|
||||||
DEFAULT_FONT_SIZE,
|
DEFAULT_FONT_SIZE,
|
||||||
MAX_FONT_SIZE,
|
MAX_FONT_SIZE,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
type ControlModel,
|
type ControlModel,
|
||||||
createControlStore,
|
createControlStore,
|
||||||
} from '$shared/store/createControlStore';
|
} from '$shared/lib/store/createControlStore/createControlStore';
|
||||||
import {
|
import {
|
||||||
DEFAULT_FONT_WEIGHT,
|
DEFAULT_FONT_WEIGHT,
|
||||||
FONT_WEIGHT_STEP,
|
FONT_WEIGHT_STEP,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
type ControlModel,
|
type ControlModel,
|
||||||
createControlStore,
|
createControlStore,
|
||||||
} from '$shared/store/createControlStore';
|
} from '$shared/lib/store/createControlStore/createControlStore';
|
||||||
import {
|
import {
|
||||||
DEFAULT_LINE_HEIGHT,
|
DEFAULT_LINE_HEIGHT,
|
||||||
LINE_HEIGHT_STEP,
|
LINE_HEIGHT_STEP,
|
||||||
|
|||||||
117
src/shared/lib/store/createControlStore/createControlStore.ts
Normal file
117
src/shared/lib/store/createControlStore/createControlStore.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
226
src/shared/lib/store/createFilterStore/createFilterStore.ts
Normal file
226
src/shared/lib/store/createFilterStore/createFilterStore.ts
Normal 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 })),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user