/** * 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 { /** * Initial or current numeric value */ value: number; /** * Lower inclusive bound */ min: number; /** * Upper inclusive bound */ max: number; /** * Precision for increment/decrement operations */ step: number; } /** * Full control model including accessibility labels * * @template T - Type for the control identifier */ export interface ControlModel extends ControlDataModel { /** * Unique string identifier for the control */ id: T; /** * Label used by screen readers for the increase button */ increaseLabel?: string; /** * Label used by screen readers for the decrease button */ decreaseLabel?: string; /** * Overall label describing the control's purpose */ 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( initialState: T, ) { let value = $state(initialState.value); let max = $state(initialState.max); 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 { /** * Clamped and rounded control value (reactive) */ get value() { return value; }, set value(newValue) { const rounded = roundToStepPrecision(clampNumber(newValue, min, max), step); if (value !== rounded) { value = rounded; } }, /** * Upper limit for the control value */ get max() { return max; }, /** * Lower limit for the control value */ get min() { return min; }, /** * Configured step increment */ get step() { return step; }, /** * True if current value is equal to or greater than max */ get isAtMax() { return isAtMax; }, /** * True if current value is equal to or less than min */ 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), step, ); }, }; } /** * Type representing a typography control instance */ export type TypographyControl = ReturnType;