Core@paxeer/core/numeric-stepperimplemented
Numeric stepper
Increment/decrement field composed from Button + Input.
Install
$
pnpm exec designx add @paxeer/core/numeric-stepperPreview
Loading preview...
Source code
components/designx/core/numeric-stepper.tsxtsx
"use client";
import { type ComponentProps, useId } from "react";
import { cn } from "../lib/cn";
import { Button } from "./button";
import { Input } from "./input";
export interface NumericStepperProps
extends Omit<ComponentProps<typeof Input>, "onChange" | "type" | "value"> {
value: number;
onValueChange: (value: number) => void;
step?: number;
min?: number;
max?: number;
label?: string;
}
export function NumericStepper({
value,
onValueChange,
step = 1,
min,
max,
label,
className,
id,
disabled,
...props
}: NumericStepperProps) {
const generatedId = useId();
const inputId = id ?? generatedId;
function clamp(next: number) {
let result = next;
if (typeof min === "number") result = Math.max(min, result);
if (typeof max === "number") result = Math.min(max, result);
return Number(Number(result).toPrecision(12));
}
function bump(direction: 1 | -1) {
onValueChange(clamp(value + direction * step));
}
return (
<div className={cn("inline-flex flex-col gap-1", className)}>
{label ? (
<label
htmlFor={inputId}
className="text-[length:var(--md-sys-typescale-label-medium-size)] text-on-surface-variant"
>
{label}
</label>
) : null}
<div className="inline-flex overflow-hidden rounded-[var(--md-sys-shape-corner-small)] border border-outline-variant">
<Button
variant="ghost"
size="dense"
aria-label="Decrease"
disabled={disabled || (typeof min === "number" && value <= min)}
className="rounded-none"
onClick={() => bump(-1)}
>
−
</Button>
<Input
{...props}
id={inputId}
type="number"
inputMode="decimal"
disabled={disabled}
step={step}
min={min}
max={max}
value={Number.isFinite(value) ? value : ""}
onChange={(event) => {
const next = Number(event.target.value);
if (Number.isNaN(next)) return;
onValueChange(clamp(next));
}}
className="h-10 w-20 rounded-none border-y-0 border-x-outline-variant text-center font-[family-name:var(--md-ref-typeface-mono)]"
/>
<Button
variant="ghost"
size="dense"
aria-label="Increase"
disabled={disabled || (typeof max === "number" && value >= max)}
className="rounded-none"
onClick={() => bump(1)}
>
+
</Button>
</div>
</div>
);
}
Dependencies
Registry dependencies