2026-03-02 22:18:59 +03:00
|
|
|
/**
|
|
|
|
|
* 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)
|
|
|
|
|
* ```
|
|
|
|
|
*/
|
|
|
|
|
|
2026-01-07 16:53:17 +03:00
|
|
|
import {
|
|
|
|
|
clampNumber,
|
|
|
|
|
roundToStepPrecision,
|
|
|
|
|
} from '$shared/lib/utils';
|
|
|
|
|
|
2026-03-02 22:18:59 +03:00
|
|
|
/**
|
|
|
|
|
* Core numeric control configuration
|
|
|
|
|
* Defines the bounds and stepping behavior for a control
|
|
|
|
|
*/
|
2026-01-07 16:53:17 +03:00
|
|
|
export interface ControlDataModel {
|
2026-03-02 22:18:59 +03:00
|
|
|
/** Current numeric value */
|
2026-01-07 16:53:17 +03:00
|
|
|
value: number;
|
2026-03-02 22:18:59 +03:00
|
|
|
/** Minimum allowed value (inclusive) */
|
2026-01-07 16:53:17 +03:00
|
|
|
min: number;
|
2026-03-02 22:18:59 +03:00
|
|
|
/** Maximum allowed value (inclusive) */
|
2026-01-07 16:53:17 +03:00
|
|
|
max: number;
|
2026-03-02 22:18:59 +03:00
|
|
|
/** Step size for increment/decrement operations */
|
2026-01-07 16:53:17 +03:00
|
|
|
step: number;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-02 22:18:59 +03:00
|
|
|
/**
|
|
|
|
|
* Full control model including accessibility labels
|
|
|
|
|
*
|
|
|
|
|
* @template T - Type for the control identifier
|
|
|
|
|
*/
|
2026-02-07 11:26:18 +03:00
|
|
|
export interface ControlModel<T extends string = string> extends ControlDataModel {
|
2026-03-02 22:18:59 +03:00
|
|
|
/** Unique identifier for the control */
|
2026-02-07 11:26:18 +03:00
|
|
|
id: T;
|
2026-03-02 22:18:59 +03:00
|
|
|
/** ARIA label for the increase button */
|
2026-01-21 21:54:48 +03:00
|
|
|
increaseLabel?: string;
|
2026-03-02 22:18:59 +03:00
|
|
|
/** ARIA label for the decrease button */
|
2026-01-21 21:54:48 +03:00
|
|
|
decreaseLabel?: string;
|
2026-03-02 22:18:59 +03:00
|
|
|
/** ARIA label for the control area */
|
2026-01-21 21:54:48 +03:00
|
|
|
controlLabel?: string;
|
2026-01-07 16:53:17 +03:00
|
|
|
}
|
|
|
|
|
|
2026-03-02 22:18:59 +03:00
|
|
|
/**
|
|
|
|
|
* 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)
|
|
|
|
|
* ```
|
|
|
|
|
*/
|
2026-01-07 16:53:17 +03:00
|
|
|
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);
|
|
|
|
|
|
2026-03-02 22:18:59 +03:00
|
|
|
// Derived state for boundary detection
|
2026-01-07 16:53:17 +03:00
|
|
|
const { isAtMax, isAtMin } = $derived({
|
|
|
|
|
isAtMax: value >= max,
|
|
|
|
|
isAtMin: value <= min,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return {
|
2026-03-02 22:18:59 +03:00
|
|
|
/**
|
|
|
|
|
* Current control value (getter/setter)
|
|
|
|
|
* Setting automatically clamps to bounds and rounds to step precision
|
|
|
|
|
*/
|
2026-01-07 16:53:17 +03:00
|
|
|
get value() {
|
|
|
|
|
return value;
|
|
|
|
|
},
|
|
|
|
|
set value(newValue) {
|
2026-02-07 11:26:18 +03:00
|
|
|
const rounded = roundToStepPrecision(clampNumber(newValue, min, max), step);
|
|
|
|
|
if (value !== rounded) {
|
|
|
|
|
value = rounded;
|
|
|
|
|
}
|
2026-01-07 16:53:17 +03:00
|
|
|
},
|
2026-03-02 22:18:59 +03:00
|
|
|
|
|
|
|
|
/** Maximum allowed value */
|
2026-01-07 16:53:17 +03:00
|
|
|
get max() {
|
|
|
|
|
return max;
|
|
|
|
|
},
|
2026-03-02 22:18:59 +03:00
|
|
|
|
|
|
|
|
/** Minimum allowed value */
|
2026-01-07 16:53:17 +03:00
|
|
|
get min() {
|
|
|
|
|
return min;
|
|
|
|
|
},
|
2026-03-02 22:18:59 +03:00
|
|
|
|
|
|
|
|
/** Step increment size */
|
2026-01-07 16:53:17 +03:00
|
|
|
get step() {
|
|
|
|
|
return step;
|
|
|
|
|
},
|
2026-03-02 22:18:59 +03:00
|
|
|
|
|
|
|
|
/** Whether the value is at or exceeds the maximum */
|
2026-01-07 16:53:17 +03:00
|
|
|
get isAtMax() {
|
|
|
|
|
return isAtMax;
|
|
|
|
|
},
|
2026-03-02 22:18:59 +03:00
|
|
|
|
|
|
|
|
/** Whether the value is at or below the minimum */
|
2026-01-07 16:53:17 +03:00
|
|
|
get isAtMin() {
|
|
|
|
|
return isAtMin;
|
|
|
|
|
},
|
2026-03-02 22:18:59 +03:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Increase value by one step (clamped to max)
|
|
|
|
|
*/
|
2026-01-07 16:53:17 +03:00
|
|
|
increase() {
|
|
|
|
|
value = roundToStepPrecision(
|
|
|
|
|
clampNumber(value + step, min, max),
|
|
|
|
|
step,
|
|
|
|
|
);
|
|
|
|
|
},
|
2026-03-02 22:18:59 +03:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Decrease value by one step (clamped to min)
|
|
|
|
|
*/
|
2026-01-07 16:53:17 +03:00
|
|
|
decrease() {
|
|
|
|
|
value = roundToStepPrecision(
|
|
|
|
|
clampNumber(value - step, min, max),
|
|
|
|
|
step,
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-02 22:18:59 +03:00
|
|
|
/**
|
|
|
|
|
* Type representing a typography control instance
|
|
|
|
|
*/
|
2026-01-07 16:53:17 +03:00
|
|
|
export type TypographyControl = ReturnType<typeof createTypographyControl>;
|