aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
424 lines (421 loc) • 14.6 kB
JavaScript
'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