or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

dom-events.mddom-utilities.mdfocus-accessibility.mdfunction-utilities.mdindex.mdmath-utilities.mdobject-manipulation.mdreact-utilities.mdresponsive.mdtype-checking.md
tile.json

math-utilities.mddocs/

Mathematical Utilities

Number manipulation utilities including precision control, percentage calculations, value clamping, and step-based rounding. These utilities are specifically designed for UI controls like sliders, numeric inputs, progress bars, and other interactive components that require precise mathematical operations.

Capabilities

Precision Control

Utilities for controlling number precision and decimal places.

/**
 * Converts a number to a string with specified precision
 * @param value - The number to format
 * @param precision - Number of decimal places (optional)
 * @returns String representation with specified precision
 */
function toPrecision(value: number, precision?: number): string;

/**
 * Counts the number of decimal places in a number
 * @param value - The number to analyze
 * @returns Number of decimal places
 */
function countDecimalPlaces(value: number): number;

Usage Examples:

import { toPrecision, countDecimalPlaces } from "@chakra-ui/utils";

// Number formatting for display
const userInput = 3.14159265359;
const formatted = toPrecision(userInput, 2); // "3.14"
const scientific = toPrecision(0.000123, 3); // "1.23e-4"

// Decimal place counting for validation
const decimals1 = countDecimalPlaces(3.14159); // 5
const decimals2 = countDecimalPlaces(100);     // 0
const decimals3 = countDecimalPlaces(0.5);     // 1

// Slider component with precision control
interface SliderProps {
  min: number;
  max: number;
  step?: number;
  precision?: number;
  value: number;
  onChange: (value: number) => void;
}

function PrecisionSlider({ min, max, step = 1, precision, value, onChange }: SliderProps) {
  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const rawValue = parseFloat(event.target.value);
    const precisionToUse = precision ?? countDecimalPlaces(step);
    const formattedValue = parseFloat(toPrecision(rawValue, precisionToUse));
    onChange(formattedValue);
  };
  
  return (
    <div className="slider">
      <input
        type="range"
        min={min}
        max={max}
        step={step}
        value={value}
        onChange={handleChange}
      />
      <span className="slider-value">
        {toPrecision(value, precision)}
      </span>
    </div>
  );
}

// Currency formatter with precision
function formatCurrency(amount: number, currency = "USD"): string {
  const precision = currency === "JPY" ? 0 : 2; // Japanese Yen has no decimals
  const formatted = toPrecision(amount, precision);
  
  return new Intl.NumberFormat("en-US", {
    style: "currency",
    currency,
    minimumFractionDigits: precision,
    maximumFractionDigits: precision
  }).format(parseFloat(formatted));
}

Value Clamping

Utilities for constraining values within specified ranges.

/**
 * Clamps a value between minimum and maximum bounds
 * @param value - The value to clamp
 * @param min - Minimum allowed value
 * @param max - Maximum allowed value  
 * @returns Clamped value within the specified range
 */
function clampValue(value: number, min: number, max: number): number;

Usage Examples:

import { clampValue } from "@chakra-ui/utils";

// Basic clamping
const clamped1 = clampValue(150, 0, 100);   // 100 (clamped to max)
const clamped2 = clampValue(-10, 0, 100);   // 0 (clamped to min)
const clamped3 = clampValue(50, 0, 100);    // 50 (within range)

// Progress bar component
interface ProgressBarProps {
  value: number;
  min?: number;
  max?: number;
  className?: string;
}

function ProgressBar({ value, min = 0, max = 100, className }: ProgressBarProps) {
  const clampedValue = clampValue(value, min, max);
  const percentage = ((clampedValue - min) / (max - min)) * 100;
  
  return (
    <div className={cx("progress-bar", className)}>
      <div 
        className="progress-bar__fill"
        style={{ width: `${percentage}%` }}
        role="progressbar"
        aria-valuenow={clampedValue}
        aria-valuemin={min}
        aria-valuemax={max}
      />
    </div>
  );
}

// Volume control with clamping
function useVolumeControl(initialVolume = 50) {
  const [volume, setVolume] = React.useState(clampValue(initialVolume, 0, 100));
  
  const increaseVolume = (amount = 10) => {
    setVolume(prev => clampValue(prev + amount, 0, 100));
  };
  
  const decreaseVolume = (amount = 10) => {
    setVolume(prev => clampValue(prev - amount, 0, 100));
  };
  
  const setVolumeDirectly = (newVolume: number) => {
    setVolume(clampValue(newVolume, 0, 100));
  };
  
  return { volume, increaseVolume, decreaseVolume, setVolumeDirectly };
}

// Color component validation
interface RGBColor {
  r: number;
  g: number;
  b: number;
}

function createRGBColor(r: number, g: number, b: number): RGBColor {
  return {
    r: clampValue(Math.round(r), 0, 255),
    g: clampValue(Math.round(g), 0, 255),
    b: clampValue(Math.round(b), 0, 255)
  };
}

function rgbToHex({ r, g, b }: RGBColor): string {
  const toHex = (value: number) => clampValue(value, 0, 255).toString(16).padStart(2, '0');
  return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}

Percentage Calculations

Utilities for converting between values and percentages within ranges.

/**
 * Converts a value to a percentage within a given range
 * @param value - The value to convert
 * @param min - Minimum value of the range
 * @param max - Maximum value of the range
 * @returns Percentage (0-100) representing the value's position in range
 */
function valueToPercent(value: number, min: number, max: number): number;

/**
 * Converts a percentage to a value within a given range
 * @param percent - Percentage as decimal (0-1) to convert
 * @param min - Minimum value of the range
 * @param max - Maximum value of the range
 * @returns Value corresponding to the percentage within the range
 */
function percentToValue(percent: number, min: number, max: number): number;

Usage Examples:

import { valueToPercent, percentToValue, clampValue } from "@chakra-ui/utils";

// Basic percentage conversions
const percent1 = valueToPercent(25, 0, 100);    // 25%
const percent2 = valueToPercent(5, 0, 10);      // 50%
const percent3 = valueToPercent(75, 50, 100);   // 50%

const value1 = percentToValue(0.5, 0, 100);     // 50
const value2 = percentToValue(0.25, 0, 200);    // 50
const value3 = percentToValue(0.75, 100, 200);  // 175

// Range input component
interface RangeInputProps {
  min: number;
  max: number;
  value: number;
  onChange: (value: number) => void;
  showPercentage?: boolean;
}

function RangeInput({ min, max, value, onChange, showPercentage }: RangeInputProps) {
  const percentage = valueToPercent(value, min, max);
  
  const handleSliderChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const newValue = parseFloat(event.target.value);
    onChange(newValue);
  };
  
  const handlePercentageChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const newPercentage = parseFloat(event.target.value);
    const newValue = percentToValue(newPercentage / 100, min, max); // Convert 0-100 to 0-1
    onChange(clampValue(newValue, min, max));
  };
  
  return (
    <div className="range-input">
      <input
        type="range"
        min={min}
        max={max}
        value={value}
        onChange={handleSliderChange}
        className="range-input__slider"
      />
      <div className="range-input__displays">
        <input
          type="number"
          value={value}
          onChange={(e) => onChange(clampValue(parseFloat(e.target.value) || min, min, max))}
          className="range-input__value"
        />
        {showPercentage && (
          <input
            type="number"
            value={Math.round(percentage)}
            onChange={handlePercentageChange}
            min={0}
            max={100}
            className="range-input__percentage"
          />
        )}
      </div>
    </div>
  );
}

// Loading progress indicator
interface LoadingProgressProps {
  current: number;
  total: number;
  showText?: boolean;
}

function LoadingProgress({ current, total, showText = true }: LoadingProgressProps) {
  const percentage = valueToPercent(current, 0, total);
  const clampedPercentage = clampValue(percentage, 0, 100);
  
  return (
    <div className="loading-progress">
      <div className="loading-progress__bar">
        <div 
          className="loading-progress__fill"
          style={{ width: `${clampedPercentage}%` }}
        />
      </div>
      {showText && (
        <span className="loading-progress__text">
          {Math.round(clampedPercentage)}% ({current}/{total})
        </span>
      )}
    </div>
  );
}

// Battery level indicator
function BatteryIndicator({ level }: { level: number }) {
  const percentage = clampValue(level * 100, 0, 100); // level is 0-1
  const displayValue = Math.round(percentage);
  
  return (
    <div 
      className={cx(
        "battery",
        percentage <= 20 && "battery--low",
        percentage <= 5 && "battery--critical"
      )}
    >
      <div 
        className="battery__fill"
        style={{ height: `${percentage}%` }}
      />
      <span className="battery__text">{displayValue}%</span>
    </div>
  );
}

Step-Based Rounding

Utilities for rounding values to specific increments or steps.

/**
 * Rounds a value to the nearest step increment from a starting point
 * @param value - The value to round
 * @param from - The starting point for step calculation
 * @param step - The step increment to round to
 * @returns String representation of the rounded value
 */
function roundValueToStep(value: number, from: number, step: number): string;

Usage Examples:

import { roundValueToStep, toPrecision } from "@chakra-ui/utils";

// Basic step rounding
const rounded1 = roundValueToStep(23, 0, 5);     // "25" (rounds to nearest 5)
const rounded2 = roundValueToStep(17.3, 10, 2); // "18" (rounds to nearest 2 from 10)
const rounded3 = roundValueToStep(3.14, 0, 0.5); // "3" (rounds to nearest 0.5)

// Stepper input component
interface StepperInputProps {
  value: number;
  min: number;
  max: number;
  step: number;
  onChange: (value: number) => void;
}

function StepperInput({ value, min, max, step, onChange }: StepperInputProps) {
  const roundedValue = parseFloat(roundValueToStep(value, min, step));
  
  const increment = () => {
    const newValue = clampValue(roundedValue + step, min, max);
    const steppedValue = parseFloat(roundValueToStep(newValue, min, step));
    onChange(steppedValue);
  };
  
  const decrement = () => {
    const newValue = clampValue(roundedValue - step, min, max);
    const steppedValue = parseFloat(roundValueToStep(newValue, min, step));
    onChange(steppedValue);
  };
  
  const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const inputValue = parseFloat(event.target.value) || min;
    const clampedValue = clampValue(inputValue, min, max);
    const steppedValue = parseFloat(roundValueToStep(clampedValue, min, step));
    onChange(steppedValue);
  };
  
  return (
    <div className="stepper-input">
      <button onClick={decrement} disabled={roundedValue <= min}>
        -
      </button>
      <input
        type="number"
        value={roundedValue}
        onChange={handleInputChange}
        min={min}
        max={max}
        step={step}
      />
      <button onClick={increment} disabled={roundedValue >= max}>
        +
      </button>
    </div>
  );
}

// Price rounding for e-commerce
function roundPrice(price: number, currency = "USD"): number {
  const roundingMap = {
    USD: 0.01,  // Round to cents
    EUR: 0.01,  // Round to cents
    JPY: 1,     // Round to whole yen
    GBP: 0.01   // Round to pence
  };
  
  const step = roundingMap[currency as keyof typeof roundingMap] || 0.01;
  return parseFloat(roundValueToStep(price, 0, step));
}

// Grid snap functionality
interface GridSnapOptions {
  gridSize: number;
  origin?: { x: number; y: number };
}

function snapToGrid(
  point: { x: number; y: number }, 
  { gridSize, origin = { x: 0, y: 0 } }: GridSnapOptions
) {
  return {
    x: parseFloat(roundValueToStep(point.x, origin.x, gridSize)),
    y: parseFloat(roundValueToStep(point.y, origin.y, gridSize))
  };
}

// Usage in draggable component
function useDraggableWithSnap(gridSize = 10) {
  const [position, setPosition] = React.useState({ x: 0, y: 0 });
  
  const updatePosition = (newPosition: { x: number; y: number }) => {
    const snappedPosition = snapToGrid(newPosition, { gridSize });
    setPosition(snappedPosition);
  };
  
  return { position, updatePosition };
}

// Time rounding for scheduling
function roundToTimeIncrement(minutes: number, increment = 15): number {
  return parseFloat(roundValueToStep(minutes, 0, increment));
}

function formatTimeSlot(minutes: number): string {
  const roundedMinutes = roundToTimeIncrement(minutes, 15);
  const hours = Math.floor(roundedMinutes / 60);
  const mins = roundedMinutes % 60;
  
  return `${hours.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}`;
}

// Usage
const timeSlots = [480, 495, 510, 525]; // 8:00, 8:15, 8:30, 8:45
const formattedSlots = timeSlots.map(formatTimeSlot);
// ["08:00", "08:15", "08:30", "08:45"]