Unit Input
ridiculous/unit-inputFreeComponent
Ridiculously typed CSS-unit input with built-in deg/%/px/rem/em/vw/vh validators, pointer-locked drag scrubbing, and pluggable custom units.
Install it
npx shadcn@latest add http://ridiculous.turtlesocks.dev/r/unit-input.jsonSource
1"use client"23import * as React from "react"4import { Input } from "@/components/ui/input"5import { cn } from "@/lib/utils"6import type { KnownUnit, UnitStringMap } from "./unit-input.types"78export interface UnitInputProps<9 TUnit extends KnownUnit | (string & {}) = KnownUnit | (string & {}),10> {11 value: TUnit extends KnownUnit ? UnitStringMap[TUnit] | (string & {}) : string12 onChange: (13 next: TUnit extends KnownUnit ? UnitStringMap[TUnit] : string,14 ) => void15 unit: TUnit16 min?: number17 max?: number18 step?: number19 precision?: number20 dragSensitivity?: number21 prefix?: React.ReactNode22 suffix?: React.ReactNode23 disabled?: boolean24 "aria-label"?: string25 className?: string26}2728function parseNumericResult(29 value: string,30 unit: string,31): { value: number; ok: boolean } {32 // Reject a wrong suffix: parseFloat("45px") would silently accept it for unit="deg".33 if (value !== "" && unit !== "" && !value.endsWith(unit)) {34 return { value: 0, ok: false }35 }36 const stripped = value.endsWith(unit) ? value.slice(0, -unit.length) : value37 const n = Number.parseFloat(stripped)38 if (Number.isNaN(n)) return { value: 0, ok: false }39 // Reject trailing garbage that parseFloat would otherwise tolerate.40 if (!/^-?\d+(\.\d+)?$/.test(stripped)) return { value: 0, ok: false }41 return { value: n, ok: true }42}4344function clamp(n: number, min: number | undefined, max: number | undefined) {45 if (min !== undefined && n < min) return min46 if (max !== undefined && n > max) return max47 return n48}4950export function UnitInput<TUnit extends KnownUnit | (string & {})>({51 value,52 onChange,53 unit,54 min,55 max,56 step = 1,57 precision = 0,58 dragSensitivity = 1,59 prefix,60 suffix,61 disabled,62 className,63 "aria-label": ariaLabel,64}: UnitInputProps<TUnit>) {65 const unitStr = String(unit)66 const warnedRef = React.useRef(false)67 const { value: parsedFromValue, ok: parseOk } = parseNumericResult(68 String(value),69 unitStr,70 )71 React.useEffect(() => {72 if (!parseOk && !warnedRef.current) {73 warnedRef.current = true74 console.warn(75 `[UnitInput] could not parse value "${String(value)}" for unit "${unitStr}". Falling back to 0.`,76 )77 }78 }, [parseOk, value, unitStr])79 const [rawDraft, setRawDraft] = React.useState<string | null>(null)80 const displayed = rawDraft ?? parsedFromValue.toFixed(precision)8182 const commit = (raw: string) => {83 const parsed = Number.parseFloat(raw)84 const next = Number.isNaN(parsed)85// … truncated
What it pulls in
Other registry items
Files it writes
More from ridiculous
All 22 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| All ridiculous componentsall | ridiculous | Components | Free | no deps | |
| Anchor Position Editoranchor-position-editor | ridiculous | Components | Free | no deps · 9 files | |
| Background Editorbackground-editor | ridiculous | Components | Free | no deps · 9 files | |
| Box Shadow Editorbox-shadow-editor | ridiculous | Components | Free | no deps · 7 files | |
| Calc Editorcalc-editor | ridiculous | Components | Free | no deps · 4 files | |
| Clip Path Editorclip-path-editor | ridiculous | Components | Free | no deps · 20 files | |
| Color Functioncolor-function | ridiculous | Components | Free | no deps · 10 files | |
| Color Pickercolor-picker | ridiculous | Components | Free | no deps · 11 files |
