import React, { useState, useRef, useEffect } from "react"; // --------------------------------------------------------------------------- // Calendar utilities (no external date library) // --------------------------------------------------------------------------- const MONTH_NAMES = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December", ]; const SHORT_MONTH_NAMES = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", ]; const DAY_NAMES = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"]; function getDaysInMonth(year: number, month: number): number { return new Date(year, month + 1, 0).getDate(); } function getMonthStartOffset(year: number, month: number): number { const day = new Date(year, month, 1).getDay(); return day === 0 ? 6 : day - 1; } function isSameDay(a: Date | null, b: Date | null): boolean { if (!a || !b) return false; return ( a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate() ); } function isToday(date: Date): boolean { return isSameDay(date, new Date()); } function isBetween(date: Date, start: Date | null, end: Date | null): boolean { if (!start || !end) return false; const t = date.getTime(); const s = new Date(start.getFullYear(), start.getMonth(), start.getDate()).getTime(); const e = new Date(end.getFullYear(), end.getMonth(), end.getDate()).getTime(); return t > s && t < e; } function formatInputValue(date: Date | null): string { if (!date) return ""; const y = date.getFullYear(); const m = String(date.getMonth() + 1).padStart(2, "0"); const d = String(date.getDate()).padStart(2, "0"); return `${d} / ${m} / ${y}`; } function formatRangeDisplay(start: Date | null, end: Date | null): string { if (!start) return ""; const s = `${SHORT_MONTH_NAMES[start.getMonth()]} ${start.getDate()}`; if (!end) return `${s} –`; const e = `${SHORT_MONTH_NAMES[end.getMonth()]} ${end.getDate()}`; return `${s} – ${e}`; } // --------------------------------------------------------------------------- // Inject popover keyframes once // --------------------------------------------------------------------------- function usePopoverKeyframes() { useEffect(() => { const id = "flux-datepicker-keyframes"; if (document.getElementById(id)) return; const s = document.createElement("style"); s.id = id; s.textContent = ` @keyframes flux-pop-in { from { opacity: 0; transform: translateY(-6px) scale(0.98); } to { opacity: 1; transform: translateY(0) scale(1); } } `; document.head.appendChild(s); }, []); } // --------------------------------------------------------------------------- // Shared style helpers // --------------------------------------------------------------------------- const labelStyle: React.CSSProperties = { display: "block", fontSize: "0.875rem", fontWeight: 500, color: "var(--flux-black)", marginBottom: "4px", }; function triggerStyle(open: boolean, minWidth = 200): React.CSSProperties { return { display: "flex", alignItems: "center", gap: "8px", padding: "8px 12px", minWidth, background: "var(--flux-surface)", border: open ? "1px solid var(--flux-primary-400)" : "1px solid var(--flux-grey-200)", borderRadius: "var(--flux-radius-sm)", cursor: "pointer", fontSize: "0.875rem", fontFamily: "inherit", transition: "border-color 150ms, box-shadow 150ms", boxShadow: open ? "0 0 0 1px var(--flux-primary-300)" : "none", outline: "none", }; } const popoverStyle: React.CSSProperties = { position: "absolute", top: "calc(100% + 6px)", left: 0, background: "var(--flux-surface)", border: "1px solid var(--flux-grey-100)", borderRadius: "var(--flux-radius-lg)", boxShadow: "var(--flux-shadow-lg)", padding: "16px", zIndex: 50, minWidth: "268px", animation: "flux-pop-in 160ms cubic-bezier(0.16, 1, 0.3, 1) forwards", }; const clearLinkStyle: React.CSSProperties = { fontSize: "0.75rem", fontWeight: 500, color: "var(--flux-grey-400, #9ca3af)", background: "none", border: "none", cursor: "pointer", padding: "0", fontFamily: "inherit", textDecoration: "underline", textUnderlineOffset: "2px", }; // --------------------------------------------------------------------------- // CalendarIcon // --------------------------------------------------------------------------- function CalendarIcon() { return ( ); } // --------------------------------------------------------------------------- // Shared CalendarGrid // --------------------------------------------------------------------------- interface CalendarGridProps { year: number; month: number; selected?: Date | null; rangeStart?: Date | null; rangeEnd?: Date | null; hovered?: Date | null; onSelect: (date: Date) => void; onHover?: (date: Date | null) => void; } function CalendarGrid({ year, month, selected, rangeStart, rangeEnd, hovered, onSelect, onHover, }: CalendarGridProps) { const daysInMonth = getDaysInMonth(year, month); const offset = getMonthStartOffset(year, month); const cells: (number | null)[] = [ ...Array(offset).fill(null), ...Array.from({ length: daysInMonth }, (_, i) => i + 1), ]; while (cells.length % 7 !== 0) cells.push(null); const cellSize = 34; return (
{DAY_NAMES.map((d) => (
{d}
))}
{cells.map((day, idx) => { if (!day) { return
; } const thisDate = new Date(year, month, day); const isSelected = isSameDay(thisDate, selected ?? null); const isRangeStart = isSameDay(thisDate, rangeStart ?? null); const isRangeEnd = isSameDay(thisDate, rangeEnd ?? null); const effectiveEnd = rangeEnd ?? (rangeStart && hovered ? hovered : null); const isInRange = isBetween(thisDate, rangeStart ?? null, effectiveEnd); const today = isToday(thisDate); const marked = isSelected || isRangeStart || isRangeEnd; let bg = "transparent"; if (marked) bg = "var(--flux-primary-400)"; else if (isInRange) bg = "var(--flux-primary-100)"; let textColor = "var(--flux-black)"; if (marked) textColor = "var(--flux-white)"; else if (today) textColor = "var(--flux-primary-400)"; let borderRadius: string; if (marked || (!isInRange)) { borderRadius = "var(--flux-radius-full)"; } else { if (isRangeStart) borderRadius = "var(--flux-radius-full) 0 0 var(--flux-radius-full)"; else if (isRangeEnd) borderRadius = "0 var(--flux-radius-full) var(--flux-radius-full) 0"; else borderRadius = "0"; } return ( ); })}
); } // --------------------------------------------------------------------------- // MonthNav // --------------------------------------------------------------------------- interface MonthNavProps { year: number; month: number; onChange: (year: number, month: number) => void; } function MonthNav({ year, month, onChange }: MonthNavProps) { const prev = () => { if (month === 0) onChange(year - 1, 11); else onChange(year, month - 1); }; const next = () => { if (month === 11) onChange(year + 1, 0); else onChange(year, month + 1); }; const navBtnStyle: React.CSSProperties = { width: 26, height: 26, display: "flex", alignItems: "center", justifyContent: "center", borderRadius: "var(--flux-radius-sm)", border: "none", background: "none", cursor: "pointer", color: "var(--flux-grey-400, #9ca3af)", transition: "background 120ms, color 120ms", fontFamily: "inherit", }; return (
{MONTH_NAMES[month]} {year}
); } // --------------------------------------------------------------------------- // Section wrapper // --------------------------------------------------------------------------- function Section({ title, children }: { title: string; children: React.ReactNode }) { return (

{title}

{children}
); } // --------------------------------------------------------------------------- // 1. Single Date Picker (Popover) // --------------------------------------------------------------------------- function SingleDatePicker() { usePopoverKeyframes(); const today = new Date(); const [open, setOpen] = useState(false); const [year, setYear] = useState(today.getFullYear()); const [month, setMonth] = useState(today.getMonth()); const [selected, setSelected] = useState(null); const ref = useRef(null); useEffect(() => { function handler(e: MouseEvent) { if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); } document.addEventListener("mousedown", handler); return () => document.removeEventListener("mousedown", handler); }, []); useEffect(() => { function handler(e: KeyboardEvent) { if (e.key === "Escape") setOpen(false); } document.addEventListener("keydown", handler); return () => document.removeEventListener("keydown", handler); }, []); const handleToggle = () => { if (!open && selected) { setYear(selected.getFullYear()); setMonth(selected.getMonth()); } setOpen((v) => !v); }; const handleSelect = (date: Date) => { setSelected(date); setOpen(false); }; const chevronStyle: React.CSSProperties = { color: "var(--flux-grey-300)", flexShrink: 0, transition: "transform 150ms", transform: open ? "rotate(180deg)" : "rotate(0deg)", }; return (
{open && (
{ setYear(y); setMonth(m); }} /> {selected && (
)}
)}
); } // --------------------------------------------------------------------------- // 2. Date Range Picker (Popover, single calendar) // --------------------------------------------------------------------------- function DateRangePicker() { usePopoverKeyframes(); const today = new Date(); const [open, setOpen] = useState(false); const [year, setYear] = useState(today.getFullYear()); const [month, setMonth] = useState(today.getMonth()); const [rangeStart, setRangeStart] = useState(null); const [rangeEnd, setRangeEnd] = useState(null); const [hovered, setHovered] = useState(null); const ref = useRef(null); useEffect(() => { function handler(e: MouseEvent) { if (ref.current && !ref.current.contains(e.target as Node)) { setOpen(false); setHovered(null); } } document.addEventListener("mousedown", handler); return () => document.removeEventListener("mousedown", handler); }, []); useEffect(() => { function handler(e: KeyboardEvent) { if (e.key === "Escape") { setOpen(false); setHovered(null); } } document.addEventListener("keydown", handler); return () => document.removeEventListener("keydown", handler); }, []); const handleToggle = () => { setOpen((v) => !v); setHovered(null); }; const handleSelect = (date: Date) => { if (!rangeStart || rangeEnd) { setRangeStart(date); setRangeEnd(null); setHovered(null); } else { const t = date.getTime(); const s = rangeStart.getTime(); if (t < s) { setRangeEnd(rangeStart); setRangeStart(date); } else { setRangeEnd(date); } setHovered(null); setOpen(false); } }; const handleClear = () => { setRangeStart(null); setRangeEnd(null); setHovered(null); }; const selectingEnd = rangeStart !== null && rangeEnd === null; const hasSelection = rangeStart !== null; const displayValue = formatRangeDisplay(rangeStart, rangeEnd); const chevronStyle: React.CSSProperties = { color: "var(--flux-grey-300)", flexShrink: 0, transition: "transform 150ms", transform: open ? "rotate(180deg)" : "rotate(0deg)", }; return (
{open && (
Now select end date
{ setYear(y); setMonth(m); }} /> {hasSelection && (
{rangeStart && rangeEnd && ( {Math.round( (rangeEnd.getTime() - rangeStart.getTime()) / 86_400_000 )}{" "} nights )}
)}
)}
); } // --------------------------------------------------------------------------- // Main demo export // --------------------------------------------------------------------------- export default function DatePickerDemo() { return (

Input field that opens a calendar popover on click. Select a date to close automatically. Dismiss with Escape or by clicking outside.

First click sets the start date, second click sets the end date and closes. Range is highlighted as you hover. Shows as "May 27 – Jun 3".

); }