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";
|
"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;
|
label?: string;
|
||||||
value?: string; // "YYYY-MM-DD"
|
value?: string; //Fecha en formato YYYY-MM-DD o undefined cuando se limpia
|
||||||
onChange: (value: string | undefined) => void;
|
onChange: (value: string | undefined) => void; //Devuelve la fecha en formato YYYY-MM-DD o undefined cuando se limpia.
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
maxDate?: Date;
|
maxDate?: Date; //Fecha máxima permitida.
|
||||||
|
className?: string;
|
||||||
|
errorMessage?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MONTHS = [
|
function toCalendarDate(dateStr?: string): DateValue | undefined {
|
||||||
"Enero","Febrero","Marzo","Abril","Mayo","Junio",
|
if (!dateStr) {
|
||||||
"Julio","Agosto","Septiembre","Octubre","Noviembre","Diciembre",
|
return undefined;
|
||||||
];
|
|
||||||
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 formatDisplay(value?: string): string {
|
try {
|
||||||
const d = parseDate(value);
|
return parseInternationalDate(dateStr);
|
||||||
if (!d) return "";
|
} catch {
|
||||||
return d.toLocaleDateString("es-MX", { day: "numeric", month: "long", year: "numeric" });
|
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,
|
label,
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
placeholder = "Selecciona una fecha",
|
placeholder,
|
||||||
required,
|
required = false,
|
||||||
maxDate = new Date(),
|
maxDate,
|
||||||
}: DatePickerProps) {
|
className = "",
|
||||||
const selected = parseDate(value);
|
errorMessage,
|
||||||
const [open, setOpen] = useState(false);
|
},
|
||||||
const [view, setView] = useState<"days" | "months" | "years">("days");
|
ref
|
||||||
const [cursor, setCursor] = useState(() => {
|
) => {
|
||||||
const d = selected ?? new Date();
|
const dateValue = useMemo(
|
||||||
return { month: d.getMonth(), year: d.getFullYear() };
|
() => toCalendarDate(value),
|
||||||
});
|
[value]
|
||||||
const [pos, setPos] = useState({ top: 0, left: 0, width: 0, openUp: false });
|
);
|
||||||
|
|
||||||
const triggerRef = useRef<HTMLButtonElement>(null);
|
const maxDateValue = useMemo<DateValue | undefined>(() => {
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
if (!maxDate) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const calcPos = useCallback(() => {
|
try {
|
||||||
if (!triggerRef.current) return;
|
const yyyy = maxDate.getFullYear();
|
||||||
const r = triggerRef.current.getBoundingClientRect();
|
const mm = String(maxDate.getMonth() + 1).padStart(2, "0");
|
||||||
const spaceBelow = window.innerHeight - r.bottom;
|
const dd = String(maxDate.getDate()).padStart(2, "0");
|
||||||
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,
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const openPicker = () => {
|
return parseInternationalDate(`${yyyy}-${mm}-${dd}`);
|
||||||
if (selected) setCursor({ month: selected.getMonth(), year: selected.getFullYear() });
|
} catch {
|
||||||
setView("days");
|
return undefined;
|
||||||
calcPos();
|
}
|
||||||
setOpen(true);
|
}, [maxDate]);
|
||||||
|
|
||||||
|
const handleChange = (newDate: DateValue | null) => {
|
||||||
|
if (newDate) {
|
||||||
|
onChange(toDateString(newDate));
|
||||||
|
} else {
|
||||||
|
onChange(undefined);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const handleClear = () => {
|
||||||
if (!open) return;
|
onChange(undefined);
|
||||||
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);
|
|
||||||
};
|
};
|
||||||
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 (
|
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 && (
|
||||||
<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}
|
{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">
|
{/* Campo de fecha */}
|
||||||
<div className="absolute left-3.5 pointer-events-none text-slate-400 dark:text-slate-500">
|
<HeroUIDateField.Group
|
||||||
<FiCalendar className="text-base" />
|
className={[
|
||||||
</div>
|
"bg-white dark:bg-slate-900/90",
|
||||||
<button
|
"border border-slate-300 dark:border-slate-800",
|
||||||
ref={triggerRef}
|
"hover:border-slate-400 dark:hover:border-slate-700",
|
||||||
type="button"
|
"data-[focus-within=true]:border-cyan-500",
|
||||||
onClick={openPicker}
|
"data-[focus-within=true]:ring-4",
|
||||||
className={`
|
"data-[focus-within=true]:ring-cyan-500/15",
|
||||||
w-full pl-10 pr-10 py-2.5 text-sm rounded-xl text-left transition-all duration-200 outline-none
|
"rounded-xl flex items-center shadow-sm",
|
||||||
bg-white dark:bg-slate-900/90
|
"transition-all duration-200 h-10",
|
||||||
border border-slate-300 dark:border-slate-800
|
errorMessage
|
||||||
hover:border-slate-400 dark:hover:border-slate-700
|
? "border-rose-500 dark:border-rose-500"
|
||||||
focus:border-cyan-500 focus:ring-4 focus:ring-cyan-500/15
|
: "",
|
||||||
dark:focus:border-cyan-500 dark:focus:ring-cyan-500/25
|
].join(" ")}
|
||||||
${value ? "text-slate-900 dark:text-slate-100" : "text-slate-400 dark:text-slate-500"}
|
|
||||||
`}
|
|
||||||
>
|
>
|
||||||
{value ? formatDisplay(value) : placeholder}
|
{/* Icono izquierdo */}
|
||||||
</button>
|
<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 && (
|
{value && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onChange(undefined)}
|
onClick={handleClear}
|
||||||
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"
|
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" />
|
<FiX className="text-sm" />
|
||||||
</button>
|
</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>
|
</div>
|
||||||
|
</HeroUICalendar.Root>
|
||||||
|
</HeroUIDatePicker.Popover>
|
||||||
|
</HeroUIDatePicker.Root>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
DatePicker.displayName = "DatePicker";
|
||||||
Reference in new issue
Block a user