refactor(createTypographyControl): createControlStore rewrote to runes
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
import {
|
||||
clampNumber,
|
||||
roundToStepPrecision,
|
||||
} from '$shared/lib/utils';
|
||||
|
||||
export interface ControlDataModel {
|
||||
value: number;
|
||||
min: number;
|
||||
max: number;
|
||||
step: number;
|
||||
}
|
||||
|
||||
export interface ControlModel extends ControlDataModel {
|
||||
id: string;
|
||||
increaseLabel: string;
|
||||
decreaseLabel: string;
|
||||
controlLabel: string;
|
||||
}
|
||||
|
||||
export function createTypographyControl<T extends ControlDataModel>(
|
||||
initialState: T,
|
||||
) {
|
||||
let value = $state(initialState.value);
|
||||
let max = $state(initialState.max);
|
||||
let min = $state(initialState.min);
|
||||
let step = $state(initialState.step);
|
||||
|
||||
const { isAtMax, isAtMin } = $derived({
|
||||
isAtMax: value >= max,
|
||||
isAtMin: value <= min,
|
||||
});
|
||||
|
||||
return {
|
||||
get value() {
|
||||
return value;
|
||||
},
|
||||
set value(newValue) {
|
||||
value = roundToStepPrecision(
|
||||
clampNumber(newValue, min, max),
|
||||
step,
|
||||
);
|
||||
},
|
||||
get max() {
|
||||
return max;
|
||||
},
|
||||
get min() {
|
||||
return min;
|
||||
},
|
||||
get step() {
|
||||
return step;
|
||||
},
|
||||
get isAtMax() {
|
||||
return isAtMax;
|
||||
},
|
||||
get isAtMin() {
|
||||
return isAtMin;
|
||||
},
|
||||
increase() {
|
||||
value = roundToStepPrecision(
|
||||
clampNumber(value + step, min, max),
|
||||
step,
|
||||
);
|
||||
},
|
||||
decrease() {
|
||||
value = roundToStepPrecision(
|
||||
clampNumber(value - step, min, max),
|
||||
step,
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export type TypographyControl = ReturnType<typeof createTypographyControl>;
|
||||
@@ -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<typeof createControlStore<number>>;
|
||||
|
||||
beforeEach(() => {
|
||||
const initialState: ControlModel<number> = {
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -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<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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user