"use client";
import { useId, useState, type CSSProperties } from "react";
export type Choice = {
value: string;
label: string;
description?: string;
disabled?: boolean;
};
type ChoiceProps = {
label: string;
options: readonly Choice[];
value: string;
onValueChange: (value: string) => void;
name?: string;
disabled?: boolean;
className?: string;
};
export function RadioGroup({
label,
options,
value,
onValueChange,
name,
disabled,
className = "",
}: ChoiceProps) {
const id = useId();
return (
{label}
{options.map((option) => (
onValueChange(option.value)}
disabled={option.disabled}
/>
{option.label}
{option.description && {option.description} }
))}
);
}
export function SegmentedControl({
label,
options,
value,
onValueChange,
name,
disabled,
className = "",
}: ChoiceProps) {
const id = useId();
return (
{label}
{options.map((option) => (
onValueChange(option.value)}
/>
{option.label}
))}
);
}
export function NumberField({
label,
value,
onValueChange,
min = -Infinity,
max = Infinity,
step = 1,
disabled = false,
className = "",
}: {
label: string;
value: number;
onValueChange: (value: number) => void;
min?: number;
max?: number;
step?: number;
disabled?: boolean;
className?: string;
}) {
const id = useId();
const [draft, setDraft] = useState(null);
const update = (n: number) => {
if (Number.isFinite(n)) onValueChange(Math.min(max, Math.max(min, n)));
};
const commit = () => {
if (draft === null) return;
if (draft.trim() !== "") update(Number(draft));
setDraft(null);
};
const increment = Math.max(Number.EPSILON, Math.abs(step));
return (
);
}
export function Rating({
label,
value,
onValueChange,
max = 5,
disabled,
className = "",
}: {
label: string;
value: number;
onValueChange: (value: number) => void;
max?: number;
disabled?: boolean;
className?: string;
}) {
const id = useId(),
count = Math.min(10, Math.max(1, Math.floor(max)));
return (
{label}
{Array.from({ length: count }, (_, i) => (
onValueChange(i + 1)}
/>
))}
);
}
export function ColorPicker({
label,
value,
onValueChange,
swatches = ["#ff4d1c", "#2f6bff", "#2e8b70", "#f4f0e8"],
disabled,
className = "",
}: {
label: string;
value: string;
onValueChange: (value: string) => void;
swatches?: readonly string[];
disabled?: boolean;
className?: string;
}) {
const valid = /^#[\da-f]{6}$/i.test(value);
return (
{label}
onValueChange(e.target.value)}
/>
{value}
{swatches
.filter((s) => /^#[\da-f]{6}$/i.test(s))
.map((color) => (
onValueChange(color)}
/>
))}
);
}