{isTyping ? (
-
setTextInput(e.target.value)}
- onKeyDown={e => { if (e.key === 'Enter') handleTextSubmit(); if (e.key === 'Escape') setIsTyping(false) }}
+
setTextInput(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter') handleTextSubmit();
+ if (e.key === 'Escape') setIsTyping(false);
+ }}
onBlur={handleTextSubmit}
placeholder="DD.MM.YYYY"
+ aria-label="Enter date manually"
style={{
- width: '100%', padding: '8px 14px', borderRadius: 10, border: '1px solid var(--text-faint)',
- background: 'var(--bg-input)', color: 'var(--text-primary)', fontSize: 'calc(13px * var(--fs-scale-body, 1))', fontFamily: 'inherit', outline: 'none',
- }} />
+ width: '100%',
+ padding: '8px 14px',
+ borderRadius: 10,
+ border: '1px solid var(--text-faint)',
+ background: 'var(--bg-input)',
+ color: 'var(--text-primary)',
+ fontSize: 'calc(13px * var(--fs-scale-body, 1))',
+ fontFamily: 'inherit',
+ outline: 'none',
+ }}
+ />
) : (
-
+
+ {/* Calendar trigger */}
+
+
+ {/* Keyboard / text-input trigger (non-compact only; compact gets it in the popup footer) */}
+ {!compact && !borderless && (
+
+ )}
+
)}
- {open && ReactDOM.createPortal(
-
{
- const r = ref.current?.getBoundingClientRect()
- if (!r) return { top: 0, left: 0 }
- const w = 268, pad = 8, h = 360
- const vw = window.innerWidth
- const vh = window.visualViewport?.height ?? window.innerHeight
- let left = r.left
- let top = r.bottom + 4
- if (left + w > vw - pad) left = Math.max(pad, vw - w - pad)
- if (top + h > vh - pad) top = r.top - h - 4
- top = Math.max(pad, Math.min(top, vh - h - pad))
- if (vw < 360) left = Math.max(pad, (vw - w) / 2)
- return { top, left }
- })(),
- zIndex: 99999,
- background: 'var(--bg-card)', border: '1px solid var(--border-primary)',
- borderRadius: 14, boxShadow: '0 8px 32px rgba(0,0,0,0.12)', padding: 12, width: 268,
- maxWidth: 'calc(100vw - 16px)',
- animation: 'selectIn 0.15s ease-out',
- backdropFilter: 'blur(24px)', WebkitBackdropFilter: 'blur(24px)',
- }}>
- {/* Month nav */}
-
-
- {monthLabel}
-
-
+ {open &&
+ ReactDOM.createPortal(
+
{
+ const r = ref.current?.getBoundingClientRect();
+ if (!r) return { top: 0, left: 0 };
+ const w = 268,
+ pad = 8,
+ h = 360;
+ const vw = window.innerWidth;
+ const vh = window.visualViewport?.height ?? window.innerHeight;
+ let left = r.left;
+ let top = r.bottom + 4;
+ if (left + w > vw - pad) left = Math.max(pad, vw - w - pad);
+ if (top + h > vh - pad) top = r.top - h - 4;
+ top = Math.max(pad, Math.min(top, vh - h - pad));
+ if (vw < 360) left = Math.max(pad, (vw - w) / 2);
+ return { top, left };
+ })(),
+ zIndex: 99999,
+ background: 'var(--bg-card)',
+ border: '1px solid var(--border-primary)',
+ borderRadius: 14,
+ boxShadow: '0 8px 32px rgba(0,0,0,0.12)',
+ padding: 12,
+ width: 268,
+ maxWidth: 'calc(100vw - 16px)',
+ animation: 'selectIn 0.15s ease-out',
+ backdropFilter: 'blur(24px)',
+ WebkitBackdropFilter: 'blur(24px)',
+ }}
+ >
+ {/* ── Header ── */}
+
+
- {/* Weekday headers */}
-
- {weekdays.map((d, i) => (
-
{d}
- ))}
-
-
- {/* Days grid */}
-
- {Array.from({ length: startDay }, (_, i) =>
)}
- {Array.from({ length: days }, (_, i) => {
- const d = i + 1
- const sel = d === selectedDay
- const td = isToday(d)
- return (
-
+ ) : (
+
{headerLabel}
+ )}
- {/* Clear */}
- {value && (
-
- { onChange(''); setOpen(false) }}
- style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 'calc(11px * var(--fs-scale-caption, 1))', color: 'var(--text-faint)', padding: '3px 8px', borderRadius: 6 }}
- onMouseEnter={e => e.currentTarget.style.color = '#ef4444'} onMouseLeave={e => e.currentTarget.style.color = 'var(--text-faint)'}>
- ✕
+ (e.currentTarget.style.color = 'var(--text-primary)')}
+ onMouseLeave={(e) => (e.currentTarget.style.color = 'var(--text-faint)')}
+ >
+
- )}
-
,
- document.body
- )}
+
+ {/* ── Days view ── */}
+ {view === 'days' && (
+ <>
+
+ {weekdays.map((d, i) => (
+
+ {d}
+
+ ))}
+
+
+ {Array.from({ length: startDay }, (_, i) => (
+
+ ))}
+ {Array.from({ length: days }, (_, i) => {
+ const d = i + 1;
+ const sel = d === selectedDay;
+ const td = isToday(d);
+ const isoDate = `${viewYear}-${String(viewMonth + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
+ const ariaLabel = new Date(isoDate + 'T00:00:00Z').toLocaleDateString(locale, {
+ day: 'numeric',
+ month: 'long',
+ year: 'numeric',
+ timeZone: 'UTC',
+ });
+ return (
+
selectDay(d)}
+ aria-label={ariaLabel}
+ aria-pressed={sel}
+ style={{
+ width: 32,
+ height: 32,
+ fontSize: 'calc(12px * var(--fs-scale-body, 1))',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ ...gridCellStyle(sel, td),
+ }}
+ onMouseEnter={(e) => {
+ if (!sel) e.currentTarget.style.background = 'var(--bg-hover)';
+ }}
+ onMouseLeave={(e) => {
+ if (!sel) e.currentTarget.style.background = 'transparent';
+ }}
+ >
+ {d}
+
+ );
+ })}
+
+ >
+ )}
+
+ {/* ── Months view ── */}
+ {view === 'months' && (
+
+ {monthNames.map((name, i) => {
+ const sel = !!(parsed && parsed.getUTCFullYear() === viewYear && parsed.getUTCMonth() === i);
+ const cur = today.getFullYear() === viewYear && today.getMonth() === i;
+ const ariaLabel = new Date(viewYear, i).toLocaleDateString(locale, {
+ month: 'long',
+ year: 'numeric',
+ });
+ return (
+ selectMonth(i)}
+ aria-label={ariaLabel}
+ aria-pressed={sel}
+ style={{
+ padding: '10px 4px',
+ fontSize: 'calc(12px * var(--fs-scale-body, 1))',
+ ...gridCellStyle(sel, cur),
+ }}
+ onMouseEnter={(e) => {
+ if (!sel) e.currentTarget.style.background = 'var(--bg-hover)';
+ }}
+ onMouseLeave={(e) => {
+ if (!sel) (e.currentTarget as HTMLButtonElement).style.background = 'transparent';
+ }}
+ >
+ {name}
+
+ );
+ })}
+
+ )}
+
+ {/* ── Years view ── */}
+ {view === 'years' && (
+
+ {years.map((year) => {
+ const sel = !!(parsed && parsed.getUTCFullYear() === year);
+ const cur = today.getFullYear() === year;
+ return (
+ selectYear(year)}
+ aria-label={String(year)}
+ aria-pressed={sel}
+ style={{
+ padding: '10px 4px',
+ fontSize: 'calc(12px * var(--fs-scale-body, 1))',
+ ...gridCellStyle(sel, cur),
+ }}
+ onMouseEnter={(e) => {
+ if (!sel) e.currentTarget.style.background = 'var(--bg-hover)';
+ }}
+ onMouseLeave={(e) => {
+ if (!sel) (e.currentTarget as HTMLButtonElement).style.background = 'transparent';
+ }}
+ >
+ {year}
+
+ );
+ })}
+
+ )}
+
+ {/* ── Footer: keyboard trigger (compact) + clear ── */}
+
+ {/* In compact/borderless mode, the keyboard icon lives here instead of next to the trigger */}
+ {compact || borderless ? (
+
{
+ setOpen(false);
+ setTextInput(inputValue || '');
+ setIsTyping(true);
+ }}
+ aria-label="Enter date manually"
+ title="Type a date"
+ style={{
+ background: 'none',
+ border: 'none',
+ cursor: 'pointer',
+ padding: '3px 6px',
+ borderRadius: 6,
+ display: 'flex',
+ color: 'var(--text-faint)',
+ }}
+ onMouseEnter={(e) => (e.currentTarget.style.color = 'var(--text-primary)')}
+ onMouseLeave={(e) => (e.currentTarget.style.color = 'var(--text-faint)')}
+ >
+
+
+ ) : (
+
+ )}
+
+ {value && (
+
{
+ onChange('');
+ setOpen(false);
+ }}
+ aria-label="Clear date"
+ style={{
+ background: 'none',
+ border: 'none',
+ cursor: 'pointer',
+ fontSize: 'calc(11px * var(--fs-scale-caption, 1))',
+ color: 'var(--text-faint)',
+ padding: '3px 8px',
+ borderRadius: 6,
+ }}
+ onMouseEnter={(e) => (e.currentTarget.style.color = '#ef4444')}
+ onMouseLeave={(e) => (e.currentTarget.style.color = 'var(--text-faint)')}
+ >
+ ✕
+
+ )}
+
+
,
+ document.body
+ )}
- )
+ );
}
interface CustomDateTimePickerProps {
- value: string
- onChange: (value: string) => void
- placeholder?: string
- style?: React.CSSProperties
+ value: string;
+ onChange: (value: string) => void;
+ placeholder?: string;
+ style?: React.CSSProperties;
}
export function CustomDateTimePicker({ value, onChange, placeholder, style = {} }: CustomDateTimePickerProps) {
- const { locale } = useTranslation()
- const [datePart, timePart] = (value || '').split('T')
+ const { locale } = useTranslation();
+ const [datePart, timePart] = (value || '').split('T');
const handleDateChange = (d: string) => {
- onChange(d ? `${d}T${timePart || '12:00'}` : '')
- }
+ onChange(d ? `${d}T${timePart || '12:00'}` : '');
+ };
const handleTimeChange = (t: string) => {
- const d = datePart || new Date().toISOString().split('T')[0]
- onChange(t ? `${d}T${t}` : d)
- }
+ const d = datePart || new Date().toISOString().split('T')[0];
+ onChange(t ? `${d}T${t}` : d);
+ };
return (
@@ -228,7 +622,7 @@ export function CustomDateTimePicker({ value, onChange, placeholder, style = {}
- )
+ );
}
-import CustomTimePicker from './CustomTimePicker'
+import CustomTimePicker from './CustomTimePicker';