Add input variant to DatePicker for typeable date fields
DatePicker now supports variant="button" (default, registration DOB) and variant="input" (typeable text input + calendar icon trigger). Input variant lets users type dates manually while the calendar icon opens the same popup picker. Smart blur management prevents onBlur from firing when focus moves between input, icon, and popup. 9 non-registration usages updated to variant="input". Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
013f9ec010
commit
4dc3c856b0
@ -167,6 +167,7 @@ export default function PersonForm({ person, categories, onClose }: PersonFormPr
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="birthday">Birthday</Label>
|
||||
<DatePicker
|
||||
variant="input"
|
||||
id="birthday"
|
||||
value={formData.birthday}
|
||||
onChange={(v) => set('birthday', v)}
|
||||
|
||||
@ -123,6 +123,7 @@ export default function ProjectForm({ project, onClose }: ProjectFormProps) {
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="due_date">Due Date</Label>
|
||||
<DatePicker
|
||||
variant="input"
|
||||
id="due_date"
|
||||
value={formData.due_date}
|
||||
onChange={(v) => setFormData({ ...formData, due_date: v })}
|
||||
|
||||
@ -352,6 +352,7 @@ export default function TaskDetailPanel({
|
||||
</div>
|
||||
{isEditing ? (
|
||||
<DatePicker
|
||||
variant="input"
|
||||
value={editState.due_date}
|
||||
onChange={(v) => setEditState((s) => ({ ...s, due_date: v }))}
|
||||
className="h-8 text-xs"
|
||||
|
||||
@ -156,6 +156,7 @@ export default function TaskForm({ projectId, task, parentTaskId, defaultDueDate
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="due_date">Due Date</Label>
|
||||
<DatePicker
|
||||
variant="input"
|
||||
id="due_date"
|
||||
value={formData.due_date}
|
||||
onChange={(v) => setFormData({ ...formData, due_date: v })}
|
||||
|
||||
@ -342,6 +342,7 @@ export default function ReminderDetailPanel({
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="reminder-at">Remind At</Label>
|
||||
<DatePicker
|
||||
variant="input"
|
||||
id="reminder-at"
|
||||
mode="datetime"
|
||||
value={editState.remind_at}
|
||||
|
||||
@ -98,6 +98,7 @@ export default function ReminderForm({ reminder, onClose }: ReminderFormProps) {
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="remind_at">Remind At</Label>
|
||||
<DatePicker
|
||||
variant="input"
|
||||
id="remind_at"
|
||||
mode="datetime"
|
||||
value={formData.remind_at}
|
||||
|
||||
@ -355,6 +355,7 @@ export default function SettingsPage() {
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="date_of_birth">Date of Birth</Label>
|
||||
<DatePicker
|
||||
variant="input"
|
||||
id="date_of_birth"
|
||||
value={dateOfBirth}
|
||||
onChange={(v) => setDateOfBirth(v)}
|
||||
|
||||
@ -387,6 +387,7 @@ export default function TodoDetailPanel({
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="todo-due-date">Due Date</Label>
|
||||
<DatePicker
|
||||
variant="input"
|
||||
id="todo-due-date"
|
||||
value={editState.due_date}
|
||||
onChange={(v) => updateField('due_date', v)}
|
||||
|
||||
@ -131,6 +131,7 @@ export default function TodoForm({ todo, onClose }: TodoFormProps) {
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="due_date">Due Date</Label>
|
||||
<DatePicker
|
||||
variant="input"
|
||||
id="due_date"
|
||||
value={formData.due_date}
|
||||
onChange={(v) => setFormData({ ...formData, due_date: v })}
|
||||
|
||||
@ -31,6 +31,7 @@ const DAY_HEADERS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
|
||||
// ── Props ──
|
||||
|
||||
export interface DatePickerProps {
|
||||
variant?: 'button' | 'input';
|
||||
mode?: 'date' | 'datetime';
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
@ -53,6 +54,7 @@ export interface DatePickerProps {
|
||||
const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
|
||||
(
|
||||
{
|
||||
variant = 'button',
|
||||
mode = 'date',
|
||||
value,
|
||||
onChange,
|
||||
@ -99,19 +101,22 @@ const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
|
||||
const [hour, setHour] = React.useState(parsed?.hour ?? 0);
|
||||
const [minute, setMinute] = React.useState(parsed?.minute ?? 0);
|
||||
|
||||
const triggerRef = React.useRef<HTMLButtonElement>(null);
|
||||
// Refs
|
||||
const triggerRef = React.useRef<HTMLButtonElement>(null); // button variant
|
||||
const wrapperRef = React.useRef<HTMLDivElement>(null); // input variant
|
||||
const inputElRef = React.useRef<HTMLInputElement>(null); // input variant
|
||||
const popupRef = React.useRef<HTMLDivElement>(null);
|
||||
const [pos, setPos] = React.useState<{ top: number; left: number; flipped: boolean }>({
|
||||
top: 0,
|
||||
left: 0,
|
||||
flipped: false,
|
||||
});
|
||||
const blurTimeoutRef = React.useRef<ReturnType<typeof setTimeout>>();
|
||||
|
||||
// Merge forwarded ref with internal ref
|
||||
const [pos, setPos] = React.useState<{ top: number; left: number }>({ top: 0, left: 0 });
|
||||
|
||||
// Merge forwarded ref with internal ref (button variant only)
|
||||
React.useImperativeHandle(ref, () => triggerRef.current!);
|
||||
|
||||
// Sync internal state when value changes externally
|
||||
// Sync internal state when value changes (only when popup is closed to avoid
|
||||
// jumping the calendar view while the user is navigating months or typing)
|
||||
React.useEffect(() => {
|
||||
if (open) return;
|
||||
const p = parseDateValue();
|
||||
if (p) {
|
||||
setViewYear(p.year);
|
||||
@ -120,12 +125,13 @@ const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
|
||||
setMinute(p.minute);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [value]);
|
||||
}, [value, open]);
|
||||
|
||||
// Position popup
|
||||
// Position popup relative to trigger (button) or wrapper (input)
|
||||
const updatePosition = React.useCallback(() => {
|
||||
if (!triggerRef.current) return;
|
||||
const rect = triggerRef.current.getBoundingClientRect();
|
||||
const el = variant === 'input' ? wrapperRef.current : triggerRef.current;
|
||||
if (!el) return;
|
||||
const rect = el.getBoundingClientRect();
|
||||
const popupHeight = mode === 'datetime' ? 370 : 320;
|
||||
const spaceBelow = window.innerHeight - rect.bottom;
|
||||
const flipped = spaceBelow < popupHeight && rect.top > popupHeight;
|
||||
@ -133,9 +139,8 @@ const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
|
||||
setPos({
|
||||
top: flipped ? rect.top - popupHeight - 4 : rect.bottom + 4,
|
||||
left: Math.min(rect.left, window.innerWidth - 290),
|
||||
flipped,
|
||||
});
|
||||
}, [mode]);
|
||||
}, [mode, variant]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!open) return;
|
||||
@ -148,21 +153,33 @@ const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
|
||||
};
|
||||
}, [open, updatePosition]);
|
||||
|
||||
// Close popup. refocusTrigger=true returns focus to the trigger/input
|
||||
// (used for day selection, Escape, Done). false lets focus go wherever the
|
||||
// user clicked (used for outside-click dismiss).
|
||||
const closePopup = React.useCallback(
|
||||
(refocusTrigger = true) => {
|
||||
setOpen(false);
|
||||
if (variant === 'button') {
|
||||
onBlur?.();
|
||||
} else if (refocusTrigger) {
|
||||
setTimeout(() => inputElRef.current?.focus(), 0);
|
||||
}
|
||||
},
|
||||
[variant, onBlur]
|
||||
);
|
||||
|
||||
// Dismiss on click outside
|
||||
React.useEffect(() => {
|
||||
if (!open) return;
|
||||
const handler = (e: MouseEvent) => {
|
||||
if (
|
||||
popupRef.current?.contains(e.target as Node) ||
|
||||
triggerRef.current?.contains(e.target as Node)
|
||||
)
|
||||
return;
|
||||
closePopup();
|
||||
if (popupRef.current?.contains(e.target as Node)) return;
|
||||
if (variant === 'button' && triggerRef.current?.contains(e.target as Node)) return;
|
||||
if (variant === 'input' && wrapperRef.current?.contains(e.target as Node)) return;
|
||||
closePopup(false);
|
||||
};
|
||||
document.addEventListener('mousedown', handler);
|
||||
return () => document.removeEventListener('mousedown', handler);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open]);
|
||||
}, [open, variant, closePopup]);
|
||||
|
||||
// Dismiss on Escape
|
||||
React.useEffect(() => {
|
||||
@ -170,19 +187,34 @@ const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
e.stopPropagation();
|
||||
closePopup();
|
||||
closePopup(true);
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', handler);
|
||||
return () => document.removeEventListener('keydown', handler);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open]);
|
||||
}, [open, closePopup]);
|
||||
|
||||
const closePopup = () => {
|
||||
setOpen(false);
|
||||
// Fire onBlur once when popup closes
|
||||
onBlur?.();
|
||||
};
|
||||
// Input variant: smart blur — only fires onBlur when focus truly leaves the
|
||||
// component group (input + icon + popup). Uses a short timeout to let focus
|
||||
// settle on the new target before checking.
|
||||
const handleInputBlur = React.useCallback(() => {
|
||||
if (blurTimeoutRef.current) clearTimeout(blurTimeoutRef.current);
|
||||
blurTimeoutRef.current = setTimeout(() => {
|
||||
const active = document.activeElement;
|
||||
if (
|
||||
popupRef.current?.contains(active) ||
|
||||
wrapperRef.current?.contains(active)
|
||||
) return;
|
||||
onBlur?.();
|
||||
}, 10);
|
||||
}, [onBlur]);
|
||||
|
||||
// Cleanup blur timeout on unmount
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
if (blurTimeoutRef.current) clearTimeout(blurTimeoutRef.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const openPopup = () => {
|
||||
if (disabled) return;
|
||||
@ -198,7 +230,7 @@ const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
|
||||
};
|
||||
|
||||
const togglePopup = () => {
|
||||
if (open) closePopup();
|
||||
if (open) closePopup(true);
|
||||
else openPopup();
|
||||
};
|
||||
|
||||
@ -221,7 +253,7 @@ const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
|
||||
onChange(`${dateStr}T${pad(hour)}:${pad(minute)}`);
|
||||
} else {
|
||||
onChange(dateStr);
|
||||
closePopup();
|
||||
closePopup(true);
|
||||
}
|
||||
};
|
||||
|
||||
@ -266,7 +298,7 @@ const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
|
||||
const isSelected = (d: number) =>
|
||||
parsed !== null && d === parsed.day && viewMonth === parsed.month && viewYear === parsed.year;
|
||||
|
||||
// Display text
|
||||
// Display text (button variant only)
|
||||
const displayText = (() => {
|
||||
if (!parsed) return '';
|
||||
const monthName = MONTH_NAMES[parsed.month];
|
||||
@ -281,6 +313,185 @@ const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
|
||||
const yearOptions: number[] = [];
|
||||
for (let y = startYear; y <= endYear; y++) yearOptions.push(y);
|
||||
|
||||
// ── Shared popup ──
|
||||
const popup = open
|
||||
? createPortal(
|
||||
<div
|
||||
ref={popupRef}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
style={{ position: 'fixed', top: pos.top, left: pos.left, zIndex: 60 }}
|
||||
className="w-[280px] rounded-lg border border-input bg-card shadow-lg animate-fade-in"
|
||||
>
|
||||
{/* Month/Year Nav */}
|
||||
<div className="flex items-center justify-between px-3 pt-3 pb-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={prevMonth}
|
||||
className="p-1 rounded-md hover:bg-accent/10 transition-colors"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
<div className="flex items-center gap-1.5">
|
||||
<select
|
||||
value={viewMonth}
|
||||
onChange={(e) => setViewMonth(parseInt(e.target.value, 10))}
|
||||
className="appearance-none bg-transparent text-sm font-medium cursor-pointer hover:text-accent focus:outline-none pr-1"
|
||||
>
|
||||
{MONTH_NAMES.map((n, i) => (
|
||||
<option key={i} value={i} className="bg-card text-foreground">
|
||||
{n}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
value={viewYear}
|
||||
onChange={(e) => setViewYear(parseInt(e.target.value, 10))}
|
||||
className="appearance-none bg-transparent text-sm font-medium cursor-pointer hover:text-accent focus:outline-none"
|
||||
>
|
||||
{yearOptions.map((y) => (
|
||||
<option key={y} value={y} className="bg-card text-foreground">
|
||||
{y}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={nextMonth}
|
||||
className="p-1 rounded-md hover:bg-accent/10 transition-colors"
|
||||
>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Day headers */}
|
||||
<div className="grid grid-cols-7 px-3 pb-1">
|
||||
{DAY_HEADERS.map((d) => (
|
||||
<div
|
||||
key={d}
|
||||
className="text-center text-[11px] font-medium text-muted-foreground py-1"
|
||||
>
|
||||
{d}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Day grid */}
|
||||
<div className="grid grid-cols-7 px-3 pb-3">
|
||||
{cells.map((day, i) =>
|
||||
day === null ? (
|
||||
<div key={`empty-${i}`} />
|
||||
) : (
|
||||
<button
|
||||
key={day}
|
||||
type="button"
|
||||
disabled={isDayDisabled(viewYear, viewMonth, day)}
|
||||
onClick={() => selectDay(day)}
|
||||
className={cn(
|
||||
'h-8 w-full rounded-md text-sm transition-colors',
|
||||
'hover:bg-accent/10 focus:outline-none focus-visible:ring-1 focus-visible:ring-ring',
|
||||
isSelected(day) && 'bg-accent text-accent-foreground font-medium',
|
||||
isToday(day) && !isSelected(day) && 'border border-accent/50 text-accent',
|
||||
isDayDisabled(viewYear, viewMonth, day) &&
|
||||
'opacity-30 cursor-not-allowed hover:bg-transparent'
|
||||
)}
|
||||
>
|
||||
{day}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Time selectors (datetime mode only) */}
|
||||
{mode === 'datetime' && (
|
||||
<div className="flex items-center gap-2 px-3 pb-3 border-t border-border pt-2">
|
||||
<Clock className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
||||
<select
|
||||
value={hour}
|
||||
onChange={(e) => handleTimeChange(parseInt(e.target.value, 10), minute)}
|
||||
className="flex-1 appearance-none bg-secondary rounded-md px-2 py-1 text-sm focus:outline-none focus-visible:ring-1 focus-visible:ring-ring cursor-pointer"
|
||||
>
|
||||
{Array.from({ length: 24 }, (_, i) => (
|
||||
<option key={i} value={i} className="bg-card text-foreground">
|
||||
{pad(i)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<span className="text-muted-foreground font-medium">:</span>
|
||||
<select
|
||||
value={minute}
|
||||
onChange={(e) => handleTimeChange(hour, parseInt(e.target.value, 10))}
|
||||
className="flex-1 appearance-none bg-secondary rounded-md px-2 py-1 text-sm focus:outline-none focus-visible:ring-1 focus-visible:ring-ring cursor-pointer"
|
||||
>
|
||||
{Array.from({ length: 60 }, (_, i) => (
|
||||
<option key={i} value={i} className="bg-card text-foreground">
|
||||
{pad(i)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => closePopup(true)}
|
||||
className="ml-auto px-2 py-1 text-xs font-medium rounded-md bg-accent text-accent-foreground hover:bg-accent/90 transition-colors"
|
||||
>
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>,
|
||||
document.body
|
||||
)
|
||||
: null;
|
||||
|
||||
// ── Input variant: typeable input + calendar icon trigger ──
|
||||
if (variant === 'input') {
|
||||
return (
|
||||
<>
|
||||
<div ref={wrapperRef} className="relative">
|
||||
<input
|
||||
ref={inputElRef}
|
||||
type="text"
|
||||
id={id}
|
||||
name={name}
|
||||
autoComplete={autoComplete}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
onBlur={handleInputBlur}
|
||||
onKeyDown={(e) => {
|
||||
if (open && e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
onKeyDown?.(e);
|
||||
}}
|
||||
required={required}
|
||||
disabled={disabled}
|
||||
placeholder={placeholder || (mode === 'datetime' ? 'YYYY-MM-DDThh:mm' : 'YYYY-MM-DD')}
|
||||
className={cn(
|
||||
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 pr-9 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className
|
||||
)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={togglePopup}
|
||||
className="absolute right-2.5 top-1/2 -translate-y-1/2 rounded p-0.5 hover:bg-accent/10 transition-colors disabled:opacity-50"
|
||||
tabIndex={-1}
|
||||
aria-label="Open calendar"
|
||||
>
|
||||
<Calendar className="h-4 w-4 opacity-70" />
|
||||
</button>
|
||||
</div>
|
||||
{popup}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Button variant: non-editable trigger (registration DOB) ──
|
||||
return (
|
||||
<>
|
||||
{/* Hidden input for native form validation + autofill */}
|
||||
@ -331,141 +542,7 @@ const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
|
||||
<Calendar className="h-4 w-4 shrink-0 opacity-70" />
|
||||
</button>
|
||||
|
||||
{/* Popup (portalled) */}
|
||||
{open &&
|
||||
createPortal(
|
||||
<div
|
||||
ref={popupRef}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: pos.top,
|
||||
left: pos.left,
|
||||
zIndex: 60,
|
||||
}}
|
||||
className="w-[280px] rounded-lg border border-input bg-card shadow-lg animate-fade-in"
|
||||
>
|
||||
{/* Month/Year Nav */}
|
||||
<div className="flex items-center justify-between px-3 pt-3 pb-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={prevMonth}
|
||||
className="p-1 rounded-md hover:bg-accent/10 transition-colors"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
<div className="flex items-center gap-1.5">
|
||||
<select
|
||||
value={viewMonth}
|
||||
onChange={(e) => setViewMonth(parseInt(e.target.value, 10))}
|
||||
className="appearance-none bg-transparent text-sm font-medium cursor-pointer hover:text-accent focus:outline-none pr-1"
|
||||
>
|
||||
{MONTH_NAMES.map((name, i) => (
|
||||
<option key={i} value={i} className="bg-card text-foreground">
|
||||
{name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
value={viewYear}
|
||||
onChange={(e) => setViewYear(parseInt(e.target.value, 10))}
|
||||
className="appearance-none bg-transparent text-sm font-medium cursor-pointer hover:text-accent focus:outline-none"
|
||||
>
|
||||
{yearOptions.map((y) => (
|
||||
<option key={y} value={y} className="bg-card text-foreground">
|
||||
{y}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={nextMonth}
|
||||
className="p-1 rounded-md hover:bg-accent/10 transition-colors"
|
||||
>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Day headers */}
|
||||
<div className="grid grid-cols-7 px-3 pb-1">
|
||||
{DAY_HEADERS.map((d) => (
|
||||
<div
|
||||
key={d}
|
||||
className="text-center text-[11px] font-medium text-muted-foreground py-1"
|
||||
>
|
||||
{d}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Day grid */}
|
||||
<div className="grid grid-cols-7 px-3 pb-3">
|
||||
{cells.map((day, i) =>
|
||||
day === null ? (
|
||||
<div key={`empty-${i}`} />
|
||||
) : (
|
||||
<button
|
||||
key={day}
|
||||
type="button"
|
||||
disabled={isDayDisabled(viewYear, viewMonth, day)}
|
||||
onClick={() => selectDay(day)}
|
||||
className={cn(
|
||||
'h-8 w-full rounded-md text-sm transition-colors',
|
||||
'hover:bg-accent/10 focus:outline-none focus-visible:ring-1 focus-visible:ring-ring',
|
||||
isSelected(day) && 'bg-accent text-accent-foreground font-medium',
|
||||
isToday(day) && !isSelected(day) && 'border border-accent/50 text-accent',
|
||||
isDayDisabled(viewYear, viewMonth, day) &&
|
||||
'opacity-30 cursor-not-allowed hover:bg-transparent'
|
||||
)}
|
||||
>
|
||||
{day}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Time selectors (datetime mode only) */}
|
||||
{mode === 'datetime' && (
|
||||
<div className="flex items-center gap-2 px-3 pb-3 border-t border-border pt-2">
|
||||
<Clock className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
||||
<select
|
||||
value={hour}
|
||||
onChange={(e) => handleTimeChange(parseInt(e.target.value, 10), minute)}
|
||||
className="flex-1 appearance-none bg-secondary rounded-md px-2 py-1 text-sm focus:outline-none focus-visible:ring-1 focus-visible:ring-ring cursor-pointer"
|
||||
>
|
||||
{Array.from({ length: 24 }, (_, i) => (
|
||||
<option key={i} value={i} className="bg-card text-foreground">
|
||||
{pad(i)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<span className="text-muted-foreground font-medium">:</span>
|
||||
<select
|
||||
value={minute}
|
||||
onChange={(e) => handleTimeChange(hour, parseInt(e.target.value, 10))}
|
||||
className="flex-1 appearance-none bg-secondary rounded-md px-2 py-1 text-sm focus:outline-none focus-visible:ring-1 focus-visible:ring-ring cursor-pointer"
|
||||
>
|
||||
{Array.from({ length: 60 }, (_, i) => (
|
||||
<option key={i} value={i} className="bg-card text-foreground">
|
||||
{pad(i)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
onClick={closePopup}
|
||||
className="ml-auto px-2 py-1 text-xs font-medium rounded-md bg-accent text-accent-foreground hover:bg-accent/90 transition-colors"
|
||||
>
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
{popup}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user