UNPKG

aura-glass

Version:

A comprehensive glassmorphism design system for React applications with 142+ production-ready components

424 lines (421 loc) 14.6 kB
'use client'; import { jsxs, jsx } from 'react/jsx-runtime'; import { cn } from '../../lib/utilsComprehensive.js'; import React, { forwardRef, useState, useRef, useEffect } from 'react'; import { GlassCore } from '../../primitives/GlassCore.js'; import '../../primitives/glass/GlassAdvanced.js'; import '../../primitives/OptimizedGlassCore.js'; import '../../primitives/glass/OptimizedGlassAdvanced.js'; import '../../primitives/MotionNative.js'; import { MotionFramer } from '../../primitives/motion/MotionFramer.js'; import { GlassButton, IconButton } from '../button/GlassButton.js'; import { GlassInput } from './GlassInput.js'; /** * GlassDatePicker component * Calendar interface with glassmorphism styling and comprehensive date selection */ const GlassDatePicker = /*#__PURE__*/forwardRef(({ value, defaultValue, onChange, mode = "single", rangeValue, defaultRangeValue, onRangeChange, minDate, maxDate, disabledDates, format = "MM/dd/yyyy", placeholder = "Select date...", size = "md", disabled = false, required = false, error = false, helperText, errorMessage, showWeekNumbers = false, firstDayOfWeek = 0, showTodayButton = true, showClearButton = true, renderDate, locale = "en-US", className, ...props }, ref) => { // Internal state for uncontrolled mode const [internalValue, setInternalValue] = useState(defaultValue || null); const [internalRangeValue, setInternalRangeValue] = useState(defaultRangeValue || { from: null, to: null }); // Calendar state const [isOpen, setIsOpen] = useState(false); const [currentMonth, setCurrentMonth] = useState(new Date()); const [inputValue, setInputValue] = useState(""); // Refs const containerRef = useRef(null); const inputRef = useRef(null); // Determine current values const currentDate = value !== undefined ? value : internalValue; const currentRange = rangeValue !== undefined ? rangeValue : internalRangeValue; // Format date const formatDate = date => { return new Intl.DateTimeFormat(locale, { year: "numeric", month: "2-digit", day: "2-digit" }).format(date); }; // Parse date from string const parseDate = dateString => { const date = new Date(dateString); return isNaN(date.getTime()) ? null : date; }; // Update input value when date changes useEffect(() => { if (mode === "single") { setInputValue(currentDate ? formatDate(currentDate) : ""); } else { const { from, to } = currentRange; if (from && to) { setInputValue(`${formatDate(from)} - ${formatDate(to)}`); } else if (from) { setInputValue(formatDate(from)); } else { setInputValue(""); } } }, [currentDate, currentRange, mode, locale]); // Handle date selection const handleDateSelect = date => { if (isDateDisabled(date)) return; if (mode === "single") { const newValue = date; if (value === undefined) { setInternalValue(newValue); } onChange?.(newValue); setIsOpen(false); } else { const { from, to } = currentRange; if (!from || from && to) { // Start new range const newRange = { from: date, to: null }; if (rangeValue === undefined) { setInternalRangeValue(newRange); } onRangeChange?.(newRange); } else { // Complete range const newRange = date < from ? { from: date, to: from } : { from, to: date }; if (rangeValue === undefined) { setInternalRangeValue(newRange); } onRangeChange?.(newRange); setIsOpen(false); } } }; // Check if date is disabled const isDateDisabled = date => { if (minDate && date < minDate) return true; if (maxDate && date > maxDate) return true; if (Array.isArray(disabledDates)) { return disabledDates.some(disabledDate => date.toDateString() === disabledDate.toDateString()); } if (typeof disabledDates === "function") { return disabledDates(date); } return false; }; // Check if date is selected const isDateSelected = date => { if (mode === "single") { return currentDate ? date.toDateString() === currentDate.toDateString() : false; } else { const { from, to } = currentRange; if (from && to) { return date >= from && date <= to; } return from ? date.toDateString() === from.toDateString() : false; } }; // Check if date is in range (for range mode) const isDateInRange = date => { if (mode !== "range") return false; const { from, to } = currentRange; return from && to ? date > from && date < to : false; }; // Handle input change const handleInputChange = e => { const newValue = e.target.value; setInputValue(newValue); if (mode === "single") { const parsedDate = parseDate(newValue); if (parsedDate && !isDateDisabled(parsedDate)) { if (value === undefined) { setInternalValue(parsedDate); } onChange?.(parsedDate); setCurrentMonth(parsedDate); } } }; // Handle clear const handleClear = () => { if (mode === "single") { if (value === undefined) { setInternalValue(null); } onChange?.(null); } else { const newRange = { from: null, to: null }; if (rangeValue === undefined) { setInternalRangeValue(newRange); } onRangeChange?.(newRange); } setInputValue(""); }; // Handle today button const handleToday = () => { const today = new Date(); today.setHours(0, 0, 0, 0); if (!isDateDisabled(today)) { handleDateSelect(today); setCurrentMonth(today); } }; // Generate calendar days const generateCalendarDays = () => { const startOfMonth = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), 1); new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 0); const startOfWeek = new Date(startOfMonth); startOfWeek.setDate(startOfMonth.getDate() - (startOfMonth.getDay() - firstDayOfWeek + 7) % 7); const days = []; const current = new Date(startOfWeek); // Generate 6 weeks (42 days) to ensure consistent calendar size for (let i = 0; i < 42; i++) { days.push(new Date(current)); current.setDate(current.getDate() + 1); } return days; }; // Get month names const getMonthNames = () => { return Array.from({ length: 12 }, (_, i) => new Intl.DateTimeFormat(locale, { month: "long" }).format(new Date(2000, i, 1))); }; // Get day names const getDayNames = () => { const baseDate = new Date(2000, 0, 2); // A Sunday return Array.from({ length: 7 }, (_, i) => { const date = new Date(baseDate); date.setDate(baseDate.getDate() + (i + firstDayOfWeek) % 7); return new Intl.DateTimeFormat(locale, { weekday: "short" }).format(date); }); }; // Navigate months const navigateMonth = direction => { setCurrentMonth(prev => { const newMonth = new Date(prev); newMonth.setMonth(prev.getMonth() + (direction === "next" ? 1 : -1)); return newMonth; }); }; // Close on outside click useEffect(() => { const handleClickOutside = event => { if (containerRef.current && !containerRef.current.contains(event.target)) { setIsOpen(false); } }; if (isOpen) { document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); } }, [isOpen]); // Keyboard navigation const handleKeyDown = e => { if (e.key === "Escape") { setIsOpen(false); inputRef.current?.focus(); } else if (e.key === "Enter") { setIsOpen(true); } }; const calendarDays = generateCalendarDays(); const monthNames = getMonthNames(); const dayNames = getDayNames(); return jsxs("div", { "data-glass-component": true, ref: containerRef, className: cn("glass-datepicker relative", className), ...props, children: [jsx("div", { ref: ref, children: jsx(GlassInput, { ref: inputRef, value: inputValue, onChange: handleInputChange, onKeyDown: handleKeyDown, placeholder: placeholder, size: size, disabled: disabled, required: required, state: error ? "error" : "default", helperText: error && errorMessage ? errorMessage : helperText, rightIcon: jsx(GlassButton, { type: "button", className: 'glass-p-1 glass-radius-md hover:glass-surface-subtle transition-colors', onClick: e => setIsOpen(!isOpen), disabled: disabled, children: jsx("svg", { className: 'w-4 h-4', fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" }) }) }), clearable: showClearButton && (mode === "single" ? !!currentDate : !!(currentRange.from || currentRange.to)), onClear: handleClear }) }), isOpen && jsx(MotionFramer, { className: 'absolute top-full left-0 z-50 glass-mt-2', children: jsxs(GlassCore, { className: 'w-80 glass-border glass-border-glass-border/20 glass-p-4 glass-radius-lg', children: [jsxs("div", { className: 'glass-flex glass-items-center glass-justify-between mb-4', children: [jsx(IconButton, { icon: "\u2039", variant: "ghost", size: "sm", onClick: e => navigateMonth("prev"), "aria-label": "Previous month" }), jsxs("div", { className: "glass-flex glass-items-center glass-gap-2", children: [jsx("select", { value: currentMonth.getMonth(), onChange: e => setCurrentMonth(new Date(currentMonth.getFullYear(), parseInt(e.target.value), 1)), className: 'bg-transparent glass-border glass-border-glass-border/20 glass-radius-md glass-px-2 glass-py-1 glass-text-sm focus:ring-2 focus:ring-primary/20', children: monthNames.map((month, index) => jsx("option", { value: index, children: month }, month)) }), jsx("select", { value: currentMonth.getFullYear(), onChange: e => setCurrentMonth(new Date(parseInt(e.target.value), currentMonth.getMonth(), 1)), className: 'bg-transparent glass-border glass-border-glass-border/20 glass-radius-md glass-px-2 glass-py-1 glass-text-sm focus:ring-2 focus:ring-primary/20', children: Array.from({ length: 201 }, (_, i) => 1900 + i).map(year => jsx("option", { value: year, children: year }, year)) })] }), jsx(IconButton, { icon: "\u203A", variant: "ghost", size: "sm", onClick: e => navigateMonth("next"), "aria-label": "Next month" })] }), jsxs("div", { className: 'glass-grid glass-grid-cols-7 glass-gap-1 mb-2', children: [showWeekNumbers && jsx("div", { className: 'glass-text-xs glass-text-secondary glass-p-2 text-center', children: "Wk" }), dayNames.map(day => jsx("div", { className: 'glass-text-xs glass-text-secondary glass-p-2 text-center font-medium', children: day }, day))] }), jsx("div", { className: "glass-grid glass-grid-cols-7 glass-gap-1", children: Array.from({ length: 6 }, (_, weekIndex) => jsxs(React.Fragment, { children: [showWeekNumbers && jsx("div", { className: 'glass-text-xs glass-text-secondary glass-p-2 text-center', children: weekIndex + 1 }), calendarDays.slice(weekIndex * 7, (weekIndex + 1) * 7).map(date => { const isCurrentMonth = date.getMonth() === currentMonth.getMonth(); const isSelected = isDateSelected(date); const isDisabled = isDateDisabled(date); const isInRange = isDateInRange(date); const isToday = date.toDateString() === new Date().toDateString(); return jsxs(GlassButton, { type: "button", className: cn("glass-p-2 glass-text-sm glass-radius-md transition-colors relative", "hover:bg-muted/20 focus:bg-muted/20 focus:outline-none focus:ring-2 focus:ring-primary/20", { "glass-text-secondary": !isCurrentMonth, "bg-primary text-primary-foreground": isSelected, "bg-primary/20": isInRange, "opacity-50 cursor-not-allowed": isDisabled, "font-bold": isToday }), onClick: e => handleDateSelect(date), disabled: isDisabled, "aria-label": formatDate(date), children: [renderDate ? renderDate(date, isSelected, isDisabled) : date.getDate(), isToday && !isSelected && jsx("div", { className: 'absolute bottom-1 left-1/2 transform -translate-x-1/2 w-1 h-1 glass-surface-primary glass-radius-full' })] }, date.toISOString()); })] }, weekIndex)) }), (showTodayButton || showClearButton) && jsxs("div", { className: 'glass-flex glass-items-center glass-justify-between glass-mt-4 pt-4 glass-border-t glass-border-glass-border/20', children: [showTodayButton && jsx(GlassButton, { variant: "ghost", size: "sm", onClick: handleToday, children: "Today" }), showClearButton && jsx(GlassButton, { variant: "ghost", size: "sm", onClick: handleClear, children: "Clear" })] })] }) })] }); }); GlassDatePicker.displayName = "GlassDatePicker"; export { GlassDatePicker }; //# sourceMappingURL=GlassDatePicker.js.map