cynx-lib 1.3.74 → 1.3.75
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/package.json +1 -1
- package/ui/components/DatePicker.jsx +172 -16
package/package.json
CHANGED
|
@@ -41,8 +41,24 @@ function toKey(y, m, d) { return `${y}-${pad(m+1)}-${pad(d)}`; }
|
|
|
41
41
|
function toDate(val) {
|
|
42
42
|
if (!val) return null;
|
|
43
43
|
if (val instanceof Date) return isNaN(val.getTime()) ? null : val;
|
|
44
|
+
if (typeof val === 'number') {
|
|
45
|
+
if (val >= 1000 && val <= 9999) return new Date(val, 0, 1);
|
|
46
|
+
const d = new Date(val);
|
|
47
|
+
return isNaN(d.getTime()) ? null : d;
|
|
48
|
+
}
|
|
44
49
|
if (typeof val === 'string') {
|
|
45
|
-
const
|
|
50
|
+
const trimmed = val.trim();
|
|
51
|
+
if (/^\d{4}$/.test(trimmed)) {
|
|
52
|
+
const y = parseInt(trimmed, 10);
|
|
53
|
+
if (!isNaN(y)) return new Date(y, 0, 1);
|
|
54
|
+
}
|
|
55
|
+
const ym = trimmed.split('-');
|
|
56
|
+
if (ym.length === 2) {
|
|
57
|
+
const y = parseInt(ym[0], 10);
|
|
58
|
+
const m = parseInt(ym[1], 10) - 1;
|
|
59
|
+
if (!isNaN(y) && !isNaN(m)) return new Date(y, m, 1);
|
|
60
|
+
}
|
|
61
|
+
const parts = trimmed.split('-');
|
|
46
62
|
if (parts.length === 3) {
|
|
47
63
|
const y = parseInt(parts[0], 10);
|
|
48
64
|
const m = parseInt(parts[1], 10) - 1;
|
|
@@ -72,8 +88,10 @@ const SIZES = {
|
|
|
72
88
|
export function DatePicker({
|
|
73
89
|
value,
|
|
74
90
|
onChange,
|
|
91
|
+
type = 'date',
|
|
92
|
+
mode,
|
|
75
93
|
label,
|
|
76
|
-
placeholder
|
|
94
|
+
placeholder,
|
|
77
95
|
badges = {},
|
|
78
96
|
minDate,
|
|
79
97
|
maxDate,
|
|
@@ -97,8 +115,13 @@ export function DatePicker({
|
|
|
97
115
|
const DAYS = loc.days;
|
|
98
116
|
const MONTHS = loc.months;
|
|
99
117
|
const s = SIZES[size] || SIZES.md;
|
|
118
|
+
const pickerType = type || mode || 'date';
|
|
119
|
+
const initialView = pickerType === 'month' ? 'month' : pickerType === 'year' ? 'year' : 'day';
|
|
120
|
+
const defaultPlaceholder = pickerType === 'month' ? 'Select month' : pickerType === 'year' ? 'Select year' : 'Select date';
|
|
121
|
+
const effectivePlaceholder = placeholder !== undefined ? placeholder : defaultPlaceholder;
|
|
122
|
+
|
|
100
123
|
const [open, setOpen] = useState(false);
|
|
101
|
-
const [view, setView] = useState(
|
|
124
|
+
const [view, setView] = useState(initialView); // 'day' | 'month' | 'year'
|
|
102
125
|
const [cursor, setCursor] = useState(() => new Date());
|
|
103
126
|
const [pos, setPos] = useState({ top: 0, left: 0, width: 0 });
|
|
104
127
|
const [rangeStart, setRangeStart] = useState(null);
|
|
@@ -108,11 +131,46 @@ export function DatePicker({
|
|
|
108
131
|
const btnRef = useRef(null);
|
|
109
132
|
const dropRef = useRef(null);
|
|
110
133
|
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
setView(initialView);
|
|
136
|
+
}, [pickerType]);
|
|
137
|
+
|
|
111
138
|
const changeDay = (delta) => {
|
|
112
139
|
if (disabled) return;
|
|
113
140
|
const current = toDate(value) || new Date();
|
|
114
|
-
|
|
141
|
+
if (pickerType === 'year') {
|
|
142
|
+
const next = new Date(current.getFullYear() + delta, 0, 1);
|
|
143
|
+
const minD = toDate(minDate);
|
|
144
|
+
const maxD = toDate(maxDate);
|
|
145
|
+
if (minD && next.getFullYear() < minD.getFullYear()) return;
|
|
146
|
+
if (maxD && next.getFullYear() > maxD.getFullYear()) return;
|
|
147
|
+
if (typeof value === 'number') {
|
|
148
|
+
onChange?.(next.getFullYear());
|
|
149
|
+
} else if (typeof value === 'string') {
|
|
150
|
+
onChange?.(`${next.getFullYear()}`);
|
|
151
|
+
} else {
|
|
152
|
+
onChange?.(next);
|
|
153
|
+
}
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (pickerType === 'month') {
|
|
157
|
+
const next = new Date(current.getFullYear(), current.getMonth() + delta, 1);
|
|
158
|
+
const minD = toDate(minDate);
|
|
159
|
+
const maxD = toDate(maxDate);
|
|
160
|
+
const nextEnd = new Date(next.getFullYear(), next.getMonth() + 1, 0, 23, 59, 59, 999);
|
|
161
|
+
if (minD && nextEnd < new Date(new Date(minD).setHours(0, 0, 0, 0))) return;
|
|
162
|
+
if (maxD && next > new Date(new Date(maxD).setHours(23, 59, 59, 999))) return;
|
|
163
|
+
if (typeof value === 'string') {
|
|
164
|
+
const y = next.getFullYear();
|
|
165
|
+
const m = pad(next.getMonth() + 1);
|
|
166
|
+
onChange?.(`${y}-${m}`);
|
|
167
|
+
} else {
|
|
168
|
+
onChange?.(next);
|
|
169
|
+
}
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
115
172
|
|
|
173
|
+
const next = new Date(current.getFullYear(), current.getMonth(), current.getDate() + delta);
|
|
116
174
|
const minD = toDate(minDate);
|
|
117
175
|
const maxD = toDate(maxDate);
|
|
118
176
|
if (minD && next < new Date(new Date(minD).setHours(0, 0, 0, 0))) return;
|
|
@@ -137,13 +195,13 @@ export function DatePicker({
|
|
|
137
195
|
if (e.target?.closest?.('.cynx-timepicker-drop') || e.target?.closest?.('.cynx-timepicker-cell')) return;
|
|
138
196
|
if (document.querySelector('.cynx-timepicker-drop')) return;
|
|
139
197
|
if (typeof window !== 'undefined') window.__cynx_last_portal_closed_at = Date.now();
|
|
140
|
-
setOpen(false); setView(
|
|
198
|
+
setOpen(false); setView(initialView);
|
|
141
199
|
}
|
|
142
200
|
function onKey(e) {
|
|
143
201
|
if (e.key === 'Escape') {
|
|
144
202
|
if (document.querySelector('.cynx-timepicker-drop')) return;
|
|
145
203
|
if (typeof window !== 'undefined') window.__cynx_last_portal_closed_at = Date.now();
|
|
146
|
-
setOpen(false); setView(
|
|
204
|
+
setOpen(false); setView(initialView);
|
|
147
205
|
}
|
|
148
206
|
}
|
|
149
207
|
document.addEventListener('mousedown', close, true);
|
|
@@ -152,7 +210,7 @@ export function DatePicker({
|
|
|
152
210
|
document.removeEventListener('mousedown', close, true);
|
|
153
211
|
document.removeEventListener('keydown', onKey);
|
|
154
212
|
};
|
|
155
|
-
}, [open]);
|
|
213
|
+
}, [open, initialView]);
|
|
156
214
|
|
|
157
215
|
const [stepperPosState, setStepperPosState] = useState(() => typeof window !== 'undefined' ? localStorage.getItem('datepicker_stepper_position') : null);
|
|
158
216
|
|
|
@@ -225,6 +283,7 @@ export function DatePicker({
|
|
|
225
283
|
if (disabled) return;
|
|
226
284
|
if (!open) {
|
|
227
285
|
calcPos();
|
|
286
|
+
setView(initialView);
|
|
228
287
|
}
|
|
229
288
|
setOpen(o => !o);
|
|
230
289
|
};
|
|
@@ -417,6 +476,10 @@ export function DatePicker({
|
|
|
417
476
|
const formattedTime = showTime && timeValue ? ` ${timeValue}` : '';
|
|
418
477
|
const formatted = range
|
|
419
478
|
? (rangeStart && rangeEnd ? `${formatDateStr(rangeStart)} — ${formatDateStr(rangeEnd)}` : rangeStart ? `${formatDateStr(rangeStart)} — ...` : null)
|
|
479
|
+
: pickerType === 'year'
|
|
480
|
+
? (valDate ? `${valDate.getFullYear()}` : (value !== null && value !== undefined && value !== '' ? `${value}` : null))
|
|
481
|
+
: pickerType === 'month'
|
|
482
|
+
? (valDate ? `${MONTHS[valDate.getMonth()]} ${valDate.getFullYear()}` : (typeof value === 'string' && value ? `${value}` : null))
|
|
420
483
|
: valDate
|
|
421
484
|
? `${MONTHS[valDate.getMonth()]} ${valDate.getDate()}, ${valDate.getFullYear()}${formattedTime}`
|
|
422
485
|
: (typeof value === 'string' && value ? `${value}${formattedTime}` : null);
|
|
@@ -555,7 +618,18 @@ export function DatePicker({
|
|
|
555
618
|
const c = new Date(cursor);
|
|
556
619
|
c.setMonth(i);
|
|
557
620
|
setCursor(c);
|
|
558
|
-
|
|
621
|
+
if (pickerType === 'month') {
|
|
622
|
+
if (typeof value === 'string') {
|
|
623
|
+
const ym = `${y}-${pad(i + 1)}`;
|
|
624
|
+
onChange?.(ym);
|
|
625
|
+
} else {
|
|
626
|
+
onChange?.(c);
|
|
627
|
+
}
|
|
628
|
+
setOpen(false);
|
|
629
|
+
setView('month');
|
|
630
|
+
} else {
|
|
631
|
+
setView('day');
|
|
632
|
+
}
|
|
559
633
|
}}
|
|
560
634
|
style={{
|
|
561
635
|
textAlign: 'center',
|
|
@@ -605,7 +679,21 @@ export function DatePicker({
|
|
|
605
679
|
const c = new Date(cursor);
|
|
606
680
|
c.setFullYear(yr);
|
|
607
681
|
setCursor(c);
|
|
608
|
-
|
|
682
|
+
if (pickerType === 'year') {
|
|
683
|
+
if (typeof value === 'number') {
|
|
684
|
+
onChange?.(yr);
|
|
685
|
+
} else if (typeof value === 'string') {
|
|
686
|
+
onChange?.(`${yr}`);
|
|
687
|
+
} else {
|
|
688
|
+
onChange?.(c);
|
|
689
|
+
}
|
|
690
|
+
setOpen(false);
|
|
691
|
+
setView('year');
|
|
692
|
+
} else if (pickerType === 'month') {
|
|
693
|
+
setView('month');
|
|
694
|
+
} else {
|
|
695
|
+
setView('day');
|
|
696
|
+
}
|
|
609
697
|
}}
|
|
610
698
|
style={{
|
|
611
699
|
textAlign: 'center',
|
|
@@ -641,6 +729,13 @@ export function DatePicker({
|
|
|
641
729
|
const maxD = toDate(maxDate);
|
|
642
730
|
if (!maxD) return false;
|
|
643
731
|
const current = toDate(value) || new Date();
|
|
732
|
+
if (pickerType === 'year') {
|
|
733
|
+
return (current.getFullYear() + 1) > maxD.getFullYear();
|
|
734
|
+
}
|
|
735
|
+
if (pickerType === 'month') {
|
|
736
|
+
const nextMonthStart = new Date(current.getFullYear(), current.getMonth() + 1, 1);
|
|
737
|
+
return nextMonthStart > new Date(new Date(maxD).setHours(23, 59, 59, 999));
|
|
738
|
+
}
|
|
644
739
|
const next = new Date(current.getFullYear(), current.getMonth(), current.getDate() + 1);
|
|
645
740
|
return next > new Date(new Date(maxD).setHours(23, 59, 59, 999));
|
|
646
741
|
};
|
|
@@ -650,6 +745,13 @@ export function DatePicker({
|
|
|
650
745
|
const minD = toDate(minDate);
|
|
651
746
|
if (!minD) return false;
|
|
652
747
|
const current = toDate(value) || new Date();
|
|
748
|
+
if (pickerType === 'year') {
|
|
749
|
+
return (current.getFullYear() - 1) < minD.getFullYear();
|
|
750
|
+
}
|
|
751
|
+
if (pickerType === 'month') {
|
|
752
|
+
const prevMonthEnd = new Date(current.getFullYear(), current.getMonth(), 0, 23, 59, 59, 999);
|
|
753
|
+
return prevMonthEnd < new Date(new Date(minD).setHours(0, 0, 0, 0));
|
|
754
|
+
}
|
|
653
755
|
const prev = new Date(current.getFullYear(), current.getMonth(), current.getDate() - 1);
|
|
654
756
|
return prev < new Date(new Date(minD).setHours(0, 0, 0, 0));
|
|
655
757
|
};
|
|
@@ -717,7 +819,7 @@ export function DatePicker({
|
|
|
717
819
|
onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border, #e2e8f0)'; }}>
|
|
718
820
|
<Calendar size={s.iconSize} style={{ color: 'var(--muted, #797b8d)', flexShrink: 0 }} />
|
|
719
821
|
<span style={{ flex: 1, textAlign: 'left', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', minWidth: 0 }}>
|
|
720
|
-
{formatted ||
|
|
822
|
+
{formatted || effectivePlaceholder}
|
|
721
823
|
</span>
|
|
722
824
|
{value && (
|
|
723
825
|
<span
|
|
@@ -783,7 +885,7 @@ export function DatePicker({
|
|
|
783
885
|
animation: 'dp-in .12s ease',
|
|
784
886
|
}}>
|
|
785
887
|
{/* Calendar header */}
|
|
786
|
-
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8 }}>
|
|
888
|
+
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8, gap: 4 }}>
|
|
787
889
|
<button
|
|
788
890
|
type="button"
|
|
789
891
|
disabled={isNavPrevDisabled()}
|
|
@@ -800,11 +902,65 @@ export function DatePicker({
|
|
|
800
902
|
>
|
|
801
903
|
<ChevronLeft size={15} />
|
|
802
904
|
</button>
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
905
|
+
|
|
906
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 4, minWidth: 0 }}>
|
|
907
|
+
{view === 'day' && (
|
|
908
|
+
<>
|
|
909
|
+
<button
|
|
910
|
+
type="button"
|
|
911
|
+
onClick={() => setView('month')}
|
|
912
|
+
style={{
|
|
913
|
+
background: 'none', border: 'none', cursor: 'pointer',
|
|
914
|
+
fontSize: '.82rem', fontWeight: 700, color: 'var(--primary-text, #0f172a)',
|
|
915
|
+
padding: '4px 6px', borderRadius: 'var(--radxs, 6px)',
|
|
916
|
+
fontFamily: 'var(--sans, inherit)', transition: 'background .12s'
|
|
917
|
+
}}
|
|
918
|
+
onMouseEnter={e => e.currentTarget.style.background = 'var(--grey-1, #f5f5f8)'}
|
|
919
|
+
onMouseLeave={e => e.currentTarget.style.background = 'none'}
|
|
920
|
+
>
|
|
921
|
+
{MONTHS[cursor.getMonth()]}
|
|
922
|
+
</button>
|
|
923
|
+
<button
|
|
924
|
+
type="button"
|
|
925
|
+
onClick={() => setView('year')}
|
|
926
|
+
style={{
|
|
927
|
+
background: 'none', border: 'none', cursor: 'pointer',
|
|
928
|
+
fontSize: '.82rem', fontWeight: 700, color: 'var(--primary-text, #0f172a)',
|
|
929
|
+
padding: '4px 6px', borderRadius: 'var(--radxs, 6px)',
|
|
930
|
+
fontFamily: 'var(--sans, inherit)', transition: 'background .12s'
|
|
931
|
+
}}
|
|
932
|
+
onMouseEnter={e => e.currentTarget.style.background = 'var(--grey-1, #f5f5f8)'}
|
|
933
|
+
onMouseLeave={e => e.currentTarget.style.background = 'none'}
|
|
934
|
+
>
|
|
935
|
+
{cursor.getFullYear()}
|
|
936
|
+
</button>
|
|
937
|
+
</>
|
|
938
|
+
)}
|
|
939
|
+
|
|
940
|
+
{view === 'month' && (
|
|
941
|
+
<button
|
|
942
|
+
type="button"
|
|
943
|
+
onClick={() => setView('year')}
|
|
944
|
+
style={{
|
|
945
|
+
background: 'none', border: 'none', cursor: 'pointer',
|
|
946
|
+
fontSize: '.82rem', fontWeight: 700, color: 'var(--primary-text, #0f172a)',
|
|
947
|
+
padding: '4px 8px', borderRadius: 'var(--radxs, 6px)',
|
|
948
|
+
fontFamily: 'var(--sans, inherit)', transition: 'background .12s'
|
|
949
|
+
}}
|
|
950
|
+
onMouseEnter={e => e.currentTarget.style.background = 'var(--grey-1, #f5f5f8)'}
|
|
951
|
+
onMouseLeave={e => e.currentTarget.style.background = 'none'}
|
|
952
|
+
>
|
|
953
|
+
{cursor.getFullYear()}
|
|
954
|
+
</button>
|
|
955
|
+
)}
|
|
956
|
+
|
|
957
|
+
{view === 'year' && (
|
|
958
|
+
<span style={{ fontSize: '.82rem', fontWeight: 700, color: 'var(--primary-text, #0f172a)', padding: '4px 8px' }}>
|
|
959
|
+
{Math.floor(cursor.getFullYear() / 12) * 12} – {Math.floor(cursor.getFullYear() / 12) * 12 + 11}
|
|
960
|
+
</span>
|
|
961
|
+
)}
|
|
962
|
+
</div>
|
|
963
|
+
|
|
808
964
|
<button
|
|
809
965
|
type="button"
|
|
810
966
|
disabled={isNavNextDisabled()}
|