refactor(helpers): modernize reactive helpers and add tests

This commit is contained in:
Ilia Mashkov
2026-03-02 22:18:59 +03:00
parent 594af924c7
commit ac73fd5044
12 changed files with 1117 additions and 185 deletions
@@ -1,46 +1,98 @@
/**
* Numeric control with bounded values and step precision
*
* Creates a reactive control for numeric values that enforces min/max bounds
* and rounds to a specific step increment. Commonly used for typography controls
* like font size, line height, and letter spacing.
*
* @example
* ```ts
* const fontSize = createTypographyControl({
* value: 16,
* min: 12,
* max: 72,
* step: 1
* });
*
* // Access current value
* fontSize.value; // 16
* fontSize.isAtMin; // false
*
* // Modify value (automatically clamped and rounded)
* fontSize.increase();
* fontSize.value = 100; // Will be clamped to max (72)
* ```
*/
import {
clampNumber,
roundToStepPrecision,
} from '$shared/lib/utils';
/**
* Core numeric control configuration
* Defines the bounds and stepping behavior for a control
*/
export interface ControlDataModel {
/**
* Control value
*/
/** Current numeric value */
value: number;
/**
* Minimal possible value
*/
/** Minimum allowed value (inclusive) */
min: number;
/**
* Maximal possible value
*/
/** Maximum allowed value (inclusive) */
max: number;
/**
* Step size for increase/decrease
*/
/** Step size for increment/decrement operations */
step: number;
}
/**
* Full control model including accessibility labels
*
* @template T - Type for the control identifier
*/
export interface ControlModel<T extends string = string> extends ControlDataModel {
/**
* Control identifier
*/
/** Unique identifier for the control */
id: T;
/**
* Area label for increase button
*/
/** ARIA label for the increase button */
increaseLabel?: string;
/**
* Area label for decrease button
*/
/** ARIA label for the decrease button */
decreaseLabel?: string;
/**
* Control area label
*/
/** ARIA label for the control area */
controlLabel?: string;
}
/**
* Creates a reactive numeric control with bounds and stepping
*
* The control automatically:
* - Clamps values to the min/max range
* - Rounds values to the step precision
* - Tracks whether at min/max bounds
*
* @param initialState - Initial value, bounds, and step configuration
* @returns Typography control instance with reactive state and methods
*
* @example
* ```ts
* // Font size control: 12-72px in 1px increments
* const fontSize = createTypographyControl({
* value: 16,
* min: 12,
* max: 72,
* step: 1
* });
*
* // Line height control: 1.0-2.0 in 0.1 increments
* const lineHeight = createTypographyControl({
* value: 1.5,
* min: 1.0,
* max: 2.0,
* step: 0.1
* });
*
* // Direct assignment (auto-clamped)
* fontSize.value = 100; // Becomes 72 (max)
* ```
*/
export function createTypographyControl<T extends ControlDataModel>(
initialState: T,
) {
@@ -49,12 +101,17 @@ export function createTypographyControl<T extends ControlDataModel>(
let min = $state(initialState.min);
let step = $state(initialState.step);
// Derived state for boundary detection
const { isAtMax, isAtMin } = $derived({
isAtMax: value >= max,
isAtMin: value <= min,
});
return {
/**
* Current control value (getter/setter)
* Setting automatically clamps to bounds and rounds to step precision
*/
get value() {
return value;
},
@@ -64,27 +121,45 @@ export function createTypographyControl<T extends ControlDataModel>(
value = rounded;
}
},
/** Maximum allowed value */
get max() {
return max;
},
/** Minimum allowed value */
get min() {
return min;
},
/** Step increment size */
get step() {
return step;
},
/** Whether the value is at or exceeds the maximum */
get isAtMax() {
return isAtMax;
},
/** Whether the value is at or below the minimum */
get isAtMin() {
return isAtMin;
},
/**
* Increase value by one step (clamped to max)
*/
increase() {
value = roundToStepPrecision(
clampNumber(value + step, min, max),
step,
);
},
/**
* Decrease value by one step (clamped to min)
*/
decrease() {
value = roundToStepPrecision(
clampNumber(value - step, min, max),
@@ -94,4 +169,7 @@ export function createTypographyControl<T extends ControlDataModel>(
};
}
/**
* Type representing a typography control instance
*/
export type TypographyControl = ReturnType<typeof createTypographyControl>;