refactor(ui): migrar componente DatePicker a HeroUI
This commit is contained in:
1 parent
952f30ea19
commit
cc8111275e
1 file changed
+317
-251
+317
-251
@@ -1,289 +1,355 @@
|
||||
"use client";
|
||||
import { useState, useRef, useEffect, useCallback } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { FiCalendar, FiChevronLeft, FiChevronRight, FiX } from "react-icons/fi";
|
||||
|
||||
interface DatePickerProps {
|
||||
import React, { useMemo } from "react";
|
||||
import {
|
||||
DatePicker as HeroUIDatePicker,
|
||||
DateField as HeroUIDateField,
|
||||
Calendar as HeroUICalendar,
|
||||
Label,
|
||||
FieldError,
|
||||
} from "@heroui/react";
|
||||
|
||||
import type { DateValue } from "@internationalized/date";
|
||||
import { parseDate as parseInternationalDate } from "@internationalized/date";
|
||||
|
||||
import {
|
||||
FiCalendar,
|
||||
FiChevronLeft,
|
||||
FiChevronRight,
|
||||
FiX,
|
||||
} from "react-icons/fi";
|
||||
|
||||
export interface DatePickerProps {
|
||||
label?: string;
|
||||
value?: string; // "YYYY-MM-DD"
|
||||
onChange: (value: string | undefined) => void;
|
||||
value?: string; //Fecha en formato YYYY-MM-DD o undefined cuando se limpia
|
||||
onChange: (value: string | undefined) => void; //Devuelve la fecha en formato YYYY-MM-DD o undefined cuando se limpia.
|
||||
placeholder?: string;
|
||||
required?: boolean;
|
||||
maxDate?: Date;
|
||||
maxDate?: Date; //Fecha máxima permitida.
|
||||
className?: string;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
const MONTHS = [
|
||||
"Enero","Febrero","Marzo","Abril","Mayo","Junio",
|
||||
"Julio","Agosto","Septiembre","Octubre","Noviembre","Diciembre",
|
||||
];
|
||||
const DAYS = ["Do","Lu","Ma","Mi","Ju","Vi","Sa"];
|
||||
const DROPDOWN_H = 320; // px estimado del dropdown
|
||||
|
||||
function parseDate(value?: string): Date | undefined {
|
||||
if (!value) return undefined;
|
||||
const d = new Date(value + "T12:00:00");
|
||||
return isNaN(d.getTime()) ? undefined : d;
|
||||
function toCalendarDate(dateStr?: string): DateValue | undefined {
|
||||
if (!dateStr) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function formatDisplay(value?: string): string {
|
||||
const d = parseDate(value);
|
||||
if (!d) return "";
|
||||
return d.toLocaleDateString("es-MX", { day: "numeric", month: "long", year: "numeric" });
|
||||
try {
|
||||
return parseInternationalDate(dateStr);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function DatePicker({
|
||||
function toDateString(date: DateValue): string {
|
||||
const yyyy = date.year;
|
||||
const mm = String(date.month).padStart(2, "0");
|
||||
const dd = String(date.day).padStart(2, "0");
|
||||
|
||||
return `${yyyy}-${mm}-${dd}`;
|
||||
}
|
||||
|
||||
export const DatePicker = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
DatePickerProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
placeholder = "Selecciona una fecha",
|
||||
required,
|
||||
maxDate = new Date(),
|
||||
}: DatePickerProps) {
|
||||
const selected = parseDate(value);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [view, setView] = useState<"days" | "months" | "years">("days");
|
||||
const [cursor, setCursor] = useState(() => {
|
||||
const d = selected ?? new Date();
|
||||
return { month: d.getMonth(), year: d.getFullYear() };
|
||||
});
|
||||
const [pos, setPos] = useState({ top: 0, left: 0, width: 0, openUp: false });
|
||||
placeholder,
|
||||
required = false,
|
||||
maxDate,
|
||||
className = "",
|
||||
errorMessage,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const dateValue = useMemo(
|
||||
() => toCalendarDate(value),
|
||||
[value]
|
||||
);
|
||||
|
||||
const triggerRef = useRef<HTMLButtonElement>(null);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const maxDateValue = useMemo<DateValue | undefined>(() => {
|
||||
if (!maxDate) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const calcPos = useCallback(() => {
|
||||
if (!triggerRef.current) return;
|
||||
const r = triggerRef.current.getBoundingClientRect();
|
||||
const spaceBelow = window.innerHeight - r.bottom;
|
||||
const openUp = spaceBelow < DROPDOWN_H + 8 && r.top > DROPDOWN_H + 8;
|
||||
setPos({
|
||||
top: openUp ? r.top - 8 : r.bottom + 8,
|
||||
left: r.left,
|
||||
width: r.width,
|
||||
openUp,
|
||||
});
|
||||
}, []);
|
||||
try {
|
||||
const yyyy = maxDate.getFullYear();
|
||||
const mm = String(maxDate.getMonth() + 1).padStart(2, "0");
|
||||
const dd = String(maxDate.getDate()).padStart(2, "0");
|
||||
|
||||
const openPicker = () => {
|
||||
if (selected) setCursor({ month: selected.getMonth(), year: selected.getFullYear() });
|
||||
setView("days");
|
||||
calcPos();
|
||||
setOpen(true);
|
||||
return parseInternationalDate(`${yyyy}-${mm}-${dd}`);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}, [maxDate]);
|
||||
|
||||
const handleChange = (newDate: DateValue | null) => {
|
||||
if (newDate) {
|
||||
onChange(toDateString(newDate));
|
||||
} else {
|
||||
onChange(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const onScroll = () => calcPos();
|
||||
const onResize = () => calcPos();
|
||||
const onMouse = (e: MouseEvent) => {
|
||||
if (
|
||||
triggerRef.current?.contains(e.target as Node) ||
|
||||
dropdownRef.current?.contains(e.target as Node)
|
||||
) return;
|
||||
setOpen(false);
|
||||
const handleClear = () => {
|
||||
onChange(undefined);
|
||||
};
|
||||
window.addEventListener("scroll", onScroll, true);
|
||||
window.addEventListener("resize", onResize);
|
||||
document.addEventListener("mousedown", onMouse);
|
||||
return () => {
|
||||
window.removeEventListener("scroll", onScroll, true);
|
||||
window.removeEventListener("resize", onResize);
|
||||
document.removeEventListener("mousedown", onMouse);
|
||||
};
|
||||
}, [open, calcPos]);
|
||||
|
||||
const selectDay = (day: number) => {
|
||||
const d = new Date(cursor.year, cursor.month, day);
|
||||
const iso = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
||||
onChange(iso);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const daysInMonth = new Date(cursor.year, cursor.month + 1, 0).getDate();
|
||||
const firstDow = new Date(cursor.year, cursor.month, 1).getDay();
|
||||
const today = new Date();
|
||||
|
||||
const isDisabled = (day: number) => new Date(cursor.year, cursor.month, day) > maxDate;
|
||||
const isSelected = (day: number) =>
|
||||
selected?.getFullYear() === cursor.year &&
|
||||
selected?.getMonth() === cursor.month &&
|
||||
selected?.getDate() === day;
|
||||
const isToday = (day: number) =>
|
||||
today.getFullYear() === cursor.year &&
|
||||
today.getMonth() === cursor.month &&
|
||||
today.getDate() === day;
|
||||
|
||||
const yearRange = Array.from({ length: 101 }, (_, i) => today.getFullYear() - 100 + i).reverse();
|
||||
|
||||
const dropdown = open ? (
|
||||
<div
|
||||
ref={dropdownRef}
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: pos.openUp ? "auto" : pos.top,
|
||||
bottom: pos.openUp ? window.innerHeight - pos.top : "auto",
|
||||
left: pos.left,
|
||||
width: Math.max(pos.width, 288),
|
||||
zIndex: 9999,
|
||||
}}
|
||||
className="rounded-2xl shadow-2xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 overflow-hidden"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-slate-100 dark:border-slate-800">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => view === "days" && setCursor(c => {
|
||||
const d = new Date(c.year, c.month - 1, 1);
|
||||
return { month: d.getMonth(), year: d.getFullYear() };
|
||||
})}
|
||||
className="p-1.5 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition text-slate-500 dark:text-slate-400"
|
||||
>
|
||||
<FiChevronLeft />
|
||||
</button>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setView(v => v === "months" ? "days" : "months")}
|
||||
className="text-sm font-semibold text-slate-800 dark:text-slate-100 hover:text-violet-600 dark:hover:text-violet-400 transition"
|
||||
>
|
||||
{MONTHS[cursor.month]}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setView(v => v === "years" ? "days" : "years")}
|
||||
className="text-sm font-semibold text-slate-800 dark:text-slate-100 hover:text-violet-600 dark:hover:text-violet-400 transition"
|
||||
>
|
||||
{cursor.year}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => view === "days" && setCursor(c => {
|
||||
const d = new Date(c.year, c.month + 1, 1);
|
||||
return { month: d.getMonth(), year: d.getFullYear() };
|
||||
})}
|
||||
className="p-1.5 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition text-slate-500 dark:text-slate-400"
|
||||
>
|
||||
<FiChevronRight />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Days */}
|
||||
{view === "days" && (
|
||||
<div className="p-3">
|
||||
<div className="grid grid-cols-7 mb-1">
|
||||
{DAYS.map(d => (
|
||||
<div key={d} className="text-center text-[10px] font-semibold text-slate-400 dark:text-slate-500 py-1">{d}</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-7 gap-y-0.5">
|
||||
{Array.from({ length: firstDow }).map((_, i) => <div key={`e-${i}`} />)}
|
||||
{Array.from({ length: daysInMonth }, (_, i) => i + 1).map(day => (
|
||||
<button
|
||||
key={day}
|
||||
type="button"
|
||||
disabled={isDisabled(day)}
|
||||
onClick={() => selectDay(day)}
|
||||
className={`
|
||||
aspect-square w-full flex items-center justify-center rounded-lg text-sm transition-all
|
||||
${isSelected(day)
|
||||
? "bg-violet-600 text-white font-semibold"
|
||||
: isToday(day)
|
||||
? "border border-violet-400 text-violet-600 dark:text-violet-400 font-semibold"
|
||||
: "text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800"}
|
||||
${isDisabled(day) ? "opacity-30 cursor-not-allowed" : ""}
|
||||
`}
|
||||
>
|
||||
{day}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Months */}
|
||||
{view === "months" && (
|
||||
<div className="grid grid-cols-3 gap-2 p-3">
|
||||
{MONTHS.map((m, i) => (
|
||||
<button
|
||||
key={m}
|
||||
type="button"
|
||||
onClick={() => { setCursor(c => ({ ...c, month: i })); setView("days"); }}
|
||||
className={`py-2 rounded-xl text-sm font-medium transition ${
|
||||
cursor.month === i
|
||||
? "bg-violet-600 text-white"
|
||||
: "text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800"
|
||||
}`}
|
||||
>
|
||||
{m.slice(0, 3)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Years */}
|
||||
{view === "years" && (
|
||||
<div className="h-52 overflow-y-auto p-2 grid grid-cols-3 gap-1 content-start">
|
||||
{yearRange.map(y => (
|
||||
<button
|
||||
key={y}
|
||||
type="button"
|
||||
onClick={() => { setCursor(c => ({ ...c, year: y })); setView("days"); }}
|
||||
className={`py-2 rounded-xl text-sm font-medium transition ${
|
||||
cursor.year === y
|
||||
? "bg-violet-600 text-white"
|
||||
: "text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800"
|
||||
}`}
|
||||
>
|
||||
{y}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div className="w-full space-y-1.5">
|
||||
<HeroUIDatePicker.Root
|
||||
ref={ref}
|
||||
value={dateValue ?? null}
|
||||
onChange={handleChange}
|
||||
maxValue={maxDateValue}
|
||||
isRequired={required}
|
||||
isInvalid={Boolean(errorMessage)}
|
||||
className={`w-full flex flex-col gap-1.5 ${className}`}
|
||||
>
|
||||
{/* Label */}
|
||||
{label && (
|
||||
<label className="block text-xs font-semibold text-slate-700 dark:text-slate-300">
|
||||
<Label className="block text-xs font-semibold text-slate-700 dark:text-slate-300">
|
||||
{label}
|
||||
{required && <span className="ml-1 text-rose-500 font-bold">*</span>}
|
||||
</label>
|
||||
|
||||
{required && (
|
||||
<span className="ml-1 font-bold text-rose-500">
|
||||
*
|
||||
</span>
|
||||
)}
|
||||
</Label>
|
||||
)}
|
||||
|
||||
<div className="relative flex items-center">
|
||||
<div className="absolute left-3.5 pointer-events-none text-slate-400 dark:text-slate-500">
|
||||
<FiCalendar className="text-base" />
|
||||
</div>
|
||||
<button
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
onClick={openPicker}
|
||||
className={`
|
||||
w-full pl-10 pr-10 py-2.5 text-sm rounded-xl text-left transition-all duration-200 outline-none
|
||||
bg-white dark:bg-slate-900/90
|
||||
border border-slate-300 dark:border-slate-800
|
||||
hover:border-slate-400 dark:hover:border-slate-700
|
||||
focus:border-cyan-500 focus:ring-4 focus:ring-cyan-500/15
|
||||
dark:focus:border-cyan-500 dark:focus:ring-cyan-500/25
|
||||
${value ? "text-slate-900 dark:text-slate-100" : "text-slate-400 dark:text-slate-500"}
|
||||
`}
|
||||
{/* Campo de fecha */}
|
||||
<HeroUIDateField.Group
|
||||
className={[
|
||||
"bg-white dark:bg-slate-900/90",
|
||||
"border border-slate-300 dark:border-slate-800",
|
||||
"hover:border-slate-400 dark:hover:border-slate-700",
|
||||
"data-[focus-within=true]:border-cyan-500",
|
||||
"data-[focus-within=true]:ring-4",
|
||||
"data-[focus-within=true]:ring-cyan-500/15",
|
||||
"rounded-xl flex items-center shadow-sm",
|
||||
"transition-all duration-200 h-10",
|
||||
errorMessage
|
||||
? "border-rose-500 dark:border-rose-500"
|
||||
: "",
|
||||
].join(" ")}
|
||||
>
|
||||
{value ? formatDisplay(value) : placeholder}
|
||||
</button>
|
||||
{/* Icono izquierdo */}
|
||||
<HeroUIDateField.Prefix
|
||||
className="pl-3.5 pr-1.5 text-slate-400 dark:text-slate-500"
|
||||
>
|
||||
<FiCalendar className="text-base" />
|
||||
</HeroUIDateField.Prefix>
|
||||
|
||||
{/* Input segmentado */}
|
||||
<HeroUIDateField.Input
|
||||
className="flex flex-1 items-center text-sm text-slate-900 outline-none dark:text-slate-100"
|
||||
>
|
||||
{(segment) => (
|
||||
<HeroUIDateField.Segment
|
||||
segment={segment}
|
||||
className={[
|
||||
"min-w-5 rounded px-0.5 text-center outline-none",
|
||||
"data-[placeholder=true]:text-slate-400",
|
||||
"dark:data-[placeholder=true]:text-slate-500",
|
||||
"focus:bg-cyan-500/20",
|
||||
"focus:text-cyan-700",
|
||||
"dark:focus:text-cyan-400",
|
||||
].join(" ")}
|
||||
/>
|
||||
)}
|
||||
</HeroUIDateField.Input>
|
||||
|
||||
{!value && placeholder && (
|
||||
<span className="pointer-events-none absolute ml-10 text-sm text-slate-400 dark:text-slate-500">
|
||||
{placeholder}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<HeroUIDateField.Suffix className="flex items-center">
|
||||
{/* Botón limpiar */}
|
||||
{value && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onChange(undefined)}
|
||||
className="absolute right-3 p-1 rounded-md text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-800 transition"
|
||||
onClick={handleClear}
|
||||
aria-label="Limpiar fecha"
|
||||
className={[
|
||||
"mr-1 rounded-md p-1",
|
||||
"text-slate-400",
|
||||
"hover:bg-slate-100 hover:text-slate-700",
|
||||
"dark:hover:bg-slate-800 dark:hover:text-slate-200",
|
||||
"outline-none transition",
|
||||
].join(" ")}
|
||||
>
|
||||
<FiX className="text-sm" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{typeof window !== "undefined" && createPortal(dropdown, document.body)}
|
||||
{/* Botón calendario */}
|
||||
<HeroUIDatePicker.Trigger
|
||||
className={[
|
||||
"rounded-r-xl pl-1.5 pr-3",
|
||||
"text-slate-400",
|
||||
"hover:text-slate-700",
|
||||
"dark:hover:text-slate-200",
|
||||
"outline-none",
|
||||
"focus-visible:text-cyan-500",
|
||||
].join(" ")}
|
||||
>
|
||||
<HeroUIDatePicker.TriggerIndicator />
|
||||
</HeroUIDatePicker.Trigger>
|
||||
</HeroUIDateField.Suffix>
|
||||
</HeroUIDateField.Group>
|
||||
|
||||
{/* Error */}
|
||||
{errorMessage && (
|
||||
<FieldError className="text-xs text-rose-500">
|
||||
{errorMessage}
|
||||
</FieldError>
|
||||
)}
|
||||
|
||||
{/* Calendario */}
|
||||
<HeroUIDatePicker.Popover
|
||||
className={[
|
||||
"min-w-[288px]",
|
||||
"overflow-hidden rounded-2xl",
|
||||
"border border-slate-200 dark:border-slate-700",
|
||||
"bg-white dark:bg-slate-900",
|
||||
"shadow-2xl",
|
||||
"outline-none",
|
||||
].join(" ")}
|
||||
>
|
||||
<HeroUICalendar.Root aria-label={label ?? "Seleccionar fecha"}>
|
||||
{/* Header */}
|
||||
<HeroUICalendar.Header
|
||||
className={[
|
||||
"flex items-center justify-between",
|
||||
"border-b border-slate-100 dark:border-slate-800",
|
||||
"px-4 py-3",
|
||||
].join(" ")}
|
||||
>
|
||||
{/* Mes anterior */}
|
||||
<HeroUICalendar.NavButton
|
||||
slot="previous"
|
||||
className={[
|
||||
"rounded-lg p-1.5",
|
||||
"text-slate-500 dark:text-slate-400",
|
||||
"hover:bg-slate-100 dark:hover:bg-slate-800",
|
||||
"transition",
|
||||
"outline-none",
|
||||
"focus-visible:ring-2 focus-visible:ring-cyan-500",
|
||||
].join(" ")}
|
||||
>
|
||||
<FiChevronLeft className="text-lg" />
|
||||
</HeroUICalendar.NavButton>
|
||||
|
||||
{/* Mes / año */}
|
||||
<HeroUICalendar.Heading
|
||||
className={[
|
||||
"text-sm font-semibold",
|
||||
"text-slate-800 dark:text-slate-100",
|
||||
].join(" ")}
|
||||
/>
|
||||
|
||||
{/* Mes siguiente */}
|
||||
<HeroUICalendar.NavButton
|
||||
slot="next"
|
||||
className={[
|
||||
"rounded-lg p-1.5",
|
||||
"text-slate-500 dark:text-slate-400",
|
||||
"hover:bg-slate-100 dark:hover:bg-slate-800",
|
||||
"transition",
|
||||
"outline-none",
|
||||
"focus-visible:ring-2 focus-visible:ring-cyan-500",
|
||||
].join(" ")}
|
||||
>
|
||||
<FiChevronRight className="text-lg" />
|
||||
</HeroUICalendar.NavButton>
|
||||
</HeroUICalendar.Header>
|
||||
|
||||
{/* Grid del calendario */}
|
||||
<div className="p-3">
|
||||
<HeroUICalendar.Grid className="w-full border-collapse">
|
||||
{/* Días de la semana */}
|
||||
<HeroUICalendar.GridHeader>
|
||||
{(day) => (
|
||||
<HeroUICalendar.HeaderCell
|
||||
className={[
|
||||
"pb-2 text-center",
|
||||
"text-[10px] font-semibold",
|
||||
"text-slate-400 dark:text-slate-500",
|
||||
].join(" ")}
|
||||
>
|
||||
{day}
|
||||
</HeroUICalendar.HeaderCell>
|
||||
)}
|
||||
</HeroUICalendar.GridHeader>
|
||||
|
||||
{/* Días */}
|
||||
<HeroUICalendar.GridBody>
|
||||
{(date) => (
|
||||
<HeroUICalendar.Cell
|
||||
date={date}
|
||||
className="p-0.5 outline-none"
|
||||
>
|
||||
{({
|
||||
formattedDate,
|
||||
isSelected,
|
||||
isDisabled,
|
||||
isToday,
|
||||
}) => (
|
||||
<HeroUICalendar.CellIndicator
|
||||
className={[
|
||||
"flex aspect-square w-full min-w-8",
|
||||
"items-center justify-center",
|
||||
"rounded-lg text-sm transition-all",
|
||||
"outline-none",
|
||||
"focus-visible:ring-2",
|
||||
"focus-visible:ring-cyan-500",
|
||||
"focus-visible:ring-offset-1",
|
||||
"focus-visible:ring-offset-white",
|
||||
"dark:focus-visible:ring-offset-slate-900",
|
||||
|
||||
isSelected
|
||||
? "bg-violet-600 font-semibold text-white shadow-sm"
|
||||
: isToday
|
||||
? [
|
||||
"border border-violet-400",
|
||||
"font-semibold text-violet-600",
|
||||
"hover:bg-slate-100",
|
||||
"dark:text-violet-400",
|
||||
"dark:hover:bg-slate-800",
|
||||
].join(" ")
|
||||
: [
|
||||
"text-slate-700",
|
||||
"hover:bg-slate-100",
|
||||
"dark:text-slate-300",
|
||||
"dark:hover:bg-slate-800",
|
||||
].join(" "),
|
||||
|
||||
isDisabled
|
||||
? "cursor-not-allowed opacity-30 hover:bg-transparent"
|
||||
: "",
|
||||
].join(" ")}
|
||||
>
|
||||
{formattedDate}
|
||||
</HeroUICalendar.CellIndicator>
|
||||
)}
|
||||
</HeroUICalendar.Cell>
|
||||
)}
|
||||
</HeroUICalendar.GridBody>
|
||||
</HeroUICalendar.Grid>
|
||||
</div>
|
||||
</HeroUICalendar.Root>
|
||||
</HeroUIDatePicker.Popover>
|
||||
</HeroUIDatePicker.Root>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
DatePicker.displayName = "DatePicker";
|
||||
Reference in new issue
Block a user