Time Picker
Selecteur d'heure en popover compose de colonnes d'heures et de minutes. Installez Time Picker depuis le registre VLLNT UI avec la CLI shadcn.
Selecteur d'heure en popover compose de colonnes d'heures et de minutes. Fait partie de la famille form dans VLLNT UI, il est publie comme entree de registre lisible par machine — copiez la source directement dans votre application avec la CLI shadcn et possedez-la, sans dependance d'execution a une bibliotheque de composants.
Apercu
Basculez entre clair et sombre pour inspecter l'apercu Storybook integre.
Installation
Ajoutez Time Picker a votre projet avec la CLI shadcn. La source arrive dans votre code, prete a etre adaptee :
pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/time-picker.jsonSource
"use client";
import * as React from "react";
import { Clock } from "lucide-react";
import { cn } from "../../lib/utils";
import { Button } from "../button/button";
import { Popover, PopoverContent, PopoverTrigger } from "../popover";
function pad(value: number): string {
return value.toString().padStart(2, "0");
}
function buildOptions(count: number, step: number): string[] {
return Array.from({ length: Math.ceil(count / step) }, (_unused, index) =>
pad(index * step),
);
}
function splitTime(value: string) {
const [hour = "", minute = ""] = value.split(":");
return { hour, minute };
}
type TimeColumnProps = {
label: string;
onSelect: (value: string) => void;
options: string[];
selected: string;
};
function TimeColumn({ label, onSelect, options, selected }: TimeColumnProps) {
return (
<div
aria-label={label}
className="flex max-h-56 flex-col gap-1 overflow-y-auto px-1"
role="listbox"
>
{options.map((option) => (
<button
aria-selected={option === selected}
className={cn(
"rounded-sm px-3 py-1.5 text-sm tabular-nums outline-none transition-colors hover:bg-accent focus-visible:bg-accent",
option === selected &&
"bg-primary text-primary-foreground hover:bg-primary",
)}
key={option}
onClick={() => {
onSelect(option);
}}
role="option"
type="button"
>
{option}
</button>
))}
</div>
);
}
/** Popover-based time selector built from hour and minute columns. */
export type TimePickerProps = {
className?: string;
defaultValue?: string;
minuteStep?: number;
onValueChange?: (value: string) => void;
placeholder?: string;
value?: string;
};
const TimePicker = ({
className,
defaultValue = "",
minuteStep = 5,
onValueChange,
placeholder = "Select time",
ref,
value,
}: TimePickerProps & { ref?: React.Ref<HTMLButtonElement> }) => {
const [internalValue, setInternalValue] = React.useState(defaultValue);
const currentValue = value ?? internalValue;
const { hour, minute } = splitTime(currentValue);
const commit = (nextHour: string, nextMinute: string) => {
const next = `${nextHour || "00"}:${nextMinute || "00"}`;
if (value === undefined) {
setInternalValue(next);
}
onValueChange?.(next);
};
return (
<Popover>
<PopoverTrigger asChild>
<Button
className={cn(
"w-full justify-start text-left font-normal",
!currentValue && "text-muted-foreground",
className,
)}
ref={ref}
variant="outline"
>
<Clock className="mr-2 size-4" />
{currentValue || placeholder}
</Button>
</PopoverTrigger>
<PopoverContent align="start" className="w-auto p-2">
<div className="flex gap-2">
<TimeColumn
label="Hour"
onSelect={(nextHour) => {
commit(nextHour, minute);
}}
options={buildOptions(24, 1)}
selected={hour}
/>
<TimeColumn
label="Minute"
onSelect={(nextMinute) => {
commit(hour, nextMinute);
}}
options={buildOptions(60, minuteStep)}
selected={minute}
/>
</div>
</PopoverContent>
</Popover>
);
};
TimePicker.displayName = "TimePicker";
export { TimePicker };
Stories
Explorez chaque variante et etat dans le Storybook interactif :
Apercu
Basculez entre clair et sombre pour inspecter l'apercu Storybook integre.