cynx-ui 1.2.39 → 1.2.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/DatePicker.jsx +117 -24
- package/package.json +1 -1
|
@@ -42,15 +42,50 @@ function isSameDay(a, b) {
|
|
|
42
42
|
}
|
|
43
43
|
function isToday(y, m, d) { const t = new Date(); return t.getFullYear()===y && t.getMonth()===m && t.getDate()===d; }
|
|
44
44
|
|
|
45
|
-
export function DatePicker({ value, onChange, placeholder = 'Select date', badges = {}, minDate, maxDate, disabled, style }) {
|
|
45
|
+
export function DatePicker({ value, onChange, placeholder = 'Select date', badges = {}, minDate, maxDate, disabled, showStepper = true, stepperPosition, style }) {
|
|
46
46
|
const [open, setOpen] = useState(false);
|
|
47
47
|
const [view, setView] = useState('day'); // 'day' | 'month' | 'year'
|
|
48
48
|
const [cursor, setCursor] = useState(() => toDate(value) || new Date());
|
|
49
49
|
const [pos, setPos] = useState({ top: 0, left: 0, width: 0 });
|
|
50
|
+
const [localStepperPos, setLocalStepperPos] = useState(() => {
|
|
51
|
+
return typeof window !== 'undefined' ? (localStorage.getItem('datepicker_stepper_position') || 'around') : 'around';
|
|
52
|
+
});
|
|
50
53
|
const ref = useRef(null);
|
|
51
54
|
const btnRef = useRef(null);
|
|
52
55
|
const dropRef = useRef(null);
|
|
53
56
|
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
function syncPos() {
|
|
59
|
+
if (typeof window !== 'undefined') {
|
|
60
|
+
setLocalStepperPos(localStorage.getItem('datepicker_stepper_position') || 'around');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
window.addEventListener('storage', syncPos);
|
|
64
|
+
window.addEventListener('datepicker-pos-changed', syncPos);
|
|
65
|
+
return () => {
|
|
66
|
+
window.removeEventListener('storage', syncPos);
|
|
67
|
+
window.removeEventListener('datepicker-pos-changed', syncPos);
|
|
68
|
+
};
|
|
69
|
+
}, []);
|
|
70
|
+
|
|
71
|
+
const changeDay = (delta) => {
|
|
72
|
+
if (disabled) return;
|
|
73
|
+
const current = toDate(value) || new Date();
|
|
74
|
+
const next = new Date(current.getFullYear(), current.getMonth(), current.getDate() + delta);
|
|
75
|
+
|
|
76
|
+
if (minDate && next < new Date(new Date(minDate).setHours(0, 0, 0, 0))) return;
|
|
77
|
+
if (maxDate && next > new Date(new Date(maxDate).setHours(23, 59, 59, 999))) return;
|
|
78
|
+
|
|
79
|
+
if (typeof value === 'string') {
|
|
80
|
+
const y = next.getFullYear();
|
|
81
|
+
const m = pad(next.getMonth() + 1);
|
|
82
|
+
const d = pad(next.getDate());
|
|
83
|
+
onChange?.(`${y}-${m}-${d}`);
|
|
84
|
+
} else {
|
|
85
|
+
onChange?.(next);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
54
89
|
// Close on outside click
|
|
55
90
|
useEffect(() => {
|
|
56
91
|
if (!open) return;
|
|
@@ -272,32 +307,90 @@ export function DatePicker({ value, onChange, placeholder = 'Select date', badge
|
|
|
272
307
|
? `${cursor.getFullYear()}`
|
|
273
308
|
: `${Math.floor(cursor.getFullYear()/12)*12} – ${Math.floor(cursor.getFullYear()/12)*12+11}`;
|
|
274
309
|
|
|
275
|
-
|
|
276
|
-
<
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
310
|
+
const renderPrevBtn = () => (
|
|
311
|
+
<button
|
|
312
|
+
type="button"
|
|
313
|
+
disabled={disabled}
|
|
314
|
+
onClick={(e) => { e.stopPropagation(); changeDay(-1); }}
|
|
315
|
+
style={{
|
|
316
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
317
|
+
width: 32, height: 32, borderRadius: 'var(--rads)',
|
|
318
|
+
border: '1px solid var(--border)', background: 'var(--surface)',
|
|
319
|
+
cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? 0.5 : 1,
|
|
320
|
+
color: 'var(--muted)', transition: 'all .15s', boxShadow: 'var(--shadow)',
|
|
321
|
+
flexShrink: 0
|
|
322
|
+
}}
|
|
323
|
+
title="Previous day (-1)"
|
|
324
|
+
onMouseEnter={e => { if (!disabled) { e.currentTarget.style.borderColor = 'var(--accent)'; e.currentTarget.style.color = 'var(--accent)'; } }}
|
|
325
|
+
onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border)'; e.currentTarget.style.color = 'var(--muted)'; }}
|
|
326
|
+
>
|
|
327
|
+
<ChevronLeft size={15} />
|
|
328
|
+
</button>
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
const renderNextBtn = () => (
|
|
332
|
+
<button
|
|
333
|
+
type="button"
|
|
334
|
+
disabled={disabled}
|
|
335
|
+
onClick={(e) => { e.stopPropagation(); changeDay(1); }}
|
|
336
|
+
style={{
|
|
337
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
338
|
+
width: 32, height: 32, borderRadius: 'var(--rads)',
|
|
281
339
|
border: '1px solid var(--border)', background: 'var(--surface)',
|
|
282
340
|
cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? 0.5 : 1,
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
whiteSpace: 'nowrap', minWidth: 140, height: 32, boxShadow: 'var(--shadow)',
|
|
341
|
+
color: 'var(--muted)', transition: 'all .15s', boxShadow: 'var(--shadow)',
|
|
342
|
+
flexShrink: 0
|
|
286
343
|
}}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
344
|
+
title="Next day (+1)"
|
|
345
|
+
onMouseEnter={e => { if (!disabled) { e.currentTarget.style.borderColor = 'var(--accent)'; e.currentTarget.style.color = 'var(--accent)'; } }}
|
|
346
|
+
onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border)'; e.currentTarget.style.color = 'var(--muted)'; }}
|
|
347
|
+
>
|
|
348
|
+
<ChevronRight size={15} />
|
|
349
|
+
</button>
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
const renderTriggerBtn = () => (
|
|
353
|
+
<button ref={btnRef} type="button" onClick={toggleOpen} style={{
|
|
354
|
+
display: 'flex', alignItems: 'center', gap: 7,
|
|
355
|
+
padding: '5px 10px', borderRadius: 'var(--rads)',
|
|
356
|
+
border: '1px solid var(--border)', background: 'var(--surface)',
|
|
357
|
+
cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? 0.5 : 1,
|
|
358
|
+
fontSize: '.8rem', color: formatted ? 'var(--primary-text)' : 'var(--muted)',
|
|
359
|
+
fontFamily: 'var(--sans)', transition: 'border-color .15s',
|
|
360
|
+
whiteSpace: 'nowrap', minWidth: 140, height: 32, boxShadow: 'var(--shadow)',
|
|
361
|
+
}}
|
|
362
|
+
onMouseEnter={e => { if (!disabled) e.currentTarget.style.borderColor = 'var(--accent)'; }}
|
|
363
|
+
onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border)'; }}>
|
|
364
|
+
<Calendar size={13} style={{ color: 'var(--muted)', flexShrink: 0 }} />
|
|
365
|
+
<span style={{ flex: 1, textAlign: 'left' }}>{formatted || placeholder}</span>
|
|
366
|
+
{value && (
|
|
367
|
+
<span
|
|
368
|
+
onClick={e => { e.stopPropagation(); onChange?.(null); }}
|
|
369
|
+
style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--muted)', cursor: 'pointer', flexShrink: 0, fontSize: 13, lineHeight: 1, padding: '0 2px' }}
|
|
370
|
+
onMouseEnter={e => { e.currentTarget.style.color = 'var(--danger)'; }}
|
|
371
|
+
onMouseLeave={e => { e.currentTarget.style.color = 'var(--muted)'; }}>
|
|
372
|
+
✕
|
|
373
|
+
</span>
|
|
374
|
+
)}
|
|
375
|
+
</button>
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
const actualPos = stepperPosition || localStepperPos || 'around';
|
|
379
|
+
const posMode = actualPos === 'left' || actualPos === 'start' ? 'left'
|
|
380
|
+
: actualPos === 'right' || actualPos === 'end' ? 'right'
|
|
381
|
+
: 'around';
|
|
382
|
+
|
|
383
|
+
return (
|
|
384
|
+
<div ref={ref} style={{ position: 'relative', display: 'inline-flex', alignItems: 'center', gap: 4, ...style }}>
|
|
385
|
+
{showStepper && posMode === 'around' && renderPrevBtn()}
|
|
386
|
+
{showStepper && posMode === 'left' && renderPrevBtn()}
|
|
387
|
+
{showStepper && posMode === 'left' && renderNextBtn()}
|
|
388
|
+
|
|
389
|
+
{renderTriggerBtn()}
|
|
390
|
+
|
|
391
|
+
{showStepper && posMode === 'around' && renderNextBtn()}
|
|
392
|
+
{showStepper && posMode === 'right' && renderPrevBtn()}
|
|
393
|
+
{showStepper && posMode === 'right' && renderNextBtn()}
|
|
301
394
|
|
|
302
395
|
{/* Dropdown via portal */}
|
|
303
396
|
{open && createPortal(
|