cynx-lib 1.3.67 → 1.3.68
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 +163 -23
- package/vue/components/Badge.vue +8 -0
- package/vue/components/DatePicker.vue +978 -0
- package/vue/index.js +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useState, useRef, useEffect, useLayoutEffect, useCallback } from 'react';
|
|
2
2
|
import { createPortal } from 'react-dom';
|
|
3
|
-
import { ChevronLeft, ChevronRight, Calendar } from 'lucide-react';
|
|
3
|
+
import { ChevronLeft, ChevronRight, Calendar, Clock } from 'lucide-react';
|
|
4
|
+
import { TimePicker } from './TimePicker.jsx';
|
|
4
5
|
|
|
5
6
|
// DatePicker — reusable date picker component
|
|
6
7
|
// Props:
|
|
@@ -12,9 +13,28 @@ import { ChevronLeft, ChevronRight, Calendar } from 'lucide-react';
|
|
|
12
13
|
// maxDate – Date object, days after this are disabled
|
|
13
14
|
// disabled – boolean
|
|
14
15
|
// style – container style overrides
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
// showTime – boolean, enables integrated TimePicker in dropdown
|
|
17
|
+
// timeValue – string, time string e.g. "14:30"
|
|
18
|
+
// onTimeChange – function(timeString)
|
|
19
|
+
// timeFormat – "24h" | "12h"
|
|
20
|
+
// showSeconds – boolean
|
|
21
|
+
|
|
22
|
+
const LOCALES = {
|
|
23
|
+
en: {
|
|
24
|
+
days: ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'],
|
|
25
|
+
months: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
|
|
26
|
+
today: 'Today',
|
|
27
|
+
time: 'Time:',
|
|
28
|
+
ok: 'OK',
|
|
29
|
+
},
|
|
30
|
+
ru: {
|
|
31
|
+
days: ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'],
|
|
32
|
+
months: ['Янв', 'Фев', 'Мар', 'Апр', 'Май', 'Июн', 'Июл', 'Авг', 'Сен', 'Окт', 'Ноя', 'Дек'],
|
|
33
|
+
today: 'Сегодня',
|
|
34
|
+
time: 'Время:',
|
|
35
|
+
ok: 'OK',
|
|
36
|
+
},
|
|
37
|
+
};
|
|
18
38
|
|
|
19
39
|
function pad(n) { return String(n).padStart(2, '0'); }
|
|
20
40
|
function toKey(y, m, d) { return `${y}-${pad(m+1)}-${pad(d)}`; }
|
|
@@ -42,7 +62,40 @@ function isSameDay(a, b) {
|
|
|
42
62
|
}
|
|
43
63
|
function isToday(y, m, d) { const t = new Date(); return t.getFullYear()===y && t.getMonth()===m && t.getDate()===d; }
|
|
44
64
|
|
|
45
|
-
|
|
65
|
+
const SIZES = {
|
|
66
|
+
xs: { height: 24, fontSize: 11, paddingH: 6, iconSize: 11, btnSize: 24, closeSize: 11 },
|
|
67
|
+
sm: { height: 28, fontSize: 11.5, paddingH: 8, iconSize: 12, btnSize: 28, closeSize: 12 },
|
|
68
|
+
md: { height: 32, fontSize: 13, paddingH: 10, iconSize: 13, btnSize: 32, closeSize: 13 },
|
|
69
|
+
lg: { height: 36, fontSize: 14, paddingH: 12, iconSize: 14, btnSize: 36, closeSize: 14 },
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export function DatePicker({
|
|
73
|
+
value,
|
|
74
|
+
onChange,
|
|
75
|
+
label,
|
|
76
|
+
placeholder = 'Select date',
|
|
77
|
+
badges = {},
|
|
78
|
+
minDate,
|
|
79
|
+
maxDate,
|
|
80
|
+
disabled,
|
|
81
|
+
size = 'md',
|
|
82
|
+
showStepper = false,
|
|
83
|
+
stepperPosition = 'off',
|
|
84
|
+
locale = 'en',
|
|
85
|
+
style,
|
|
86
|
+
fullWidth = false,
|
|
87
|
+
showTime = false,
|
|
88
|
+
timeValue,
|
|
89
|
+
onTimeChange,
|
|
90
|
+
timeFormat = '24h',
|
|
91
|
+
showSeconds = false,
|
|
92
|
+
timePlaceholder = '00:00',
|
|
93
|
+
timeProps = {},
|
|
94
|
+
}) {
|
|
95
|
+
const loc = typeof locale === 'object' && locale !== null ? locale : (LOCALES[locale] || LOCALES.en);
|
|
96
|
+
const DAYS = loc.days;
|
|
97
|
+
const MONTHS = loc.months;
|
|
98
|
+
const s = SIZES[size] || SIZES.md;
|
|
46
99
|
const [open, setOpen] = useState(false);
|
|
47
100
|
const [view, setView] = useState('day'); // 'day' | 'month' | 'year'
|
|
48
101
|
const [cursor, setCursor] = useState(() => toDate(value) || new Date());
|
|
@@ -77,10 +130,24 @@ export function DatePicker({ value, onChange, label, placeholder = 'Select date'
|
|
|
77
130
|
function close(e) {
|
|
78
131
|
if (ref.current?.contains(e.target)) return;
|
|
79
132
|
if (dropRef.current?.contains(e.target)) return;
|
|
133
|
+
if (e.target?.closest?.('.cynx-timepicker-drop') || e.target?.closest?.('.cynx-timepicker-cell')) return;
|
|
134
|
+
if (document.querySelector('.cynx-timepicker-drop')) return;
|
|
135
|
+
if (typeof window !== 'undefined') window.__cynx_last_portal_closed_at = Date.now();
|
|
80
136
|
setOpen(false); setView('day');
|
|
81
137
|
}
|
|
138
|
+
function onKey(e) {
|
|
139
|
+
if (e.key === 'Escape') {
|
|
140
|
+
if (document.querySelector('.cynx-timepicker-drop')) return;
|
|
141
|
+
if (typeof window !== 'undefined') window.__cynx_last_portal_closed_at = Date.now();
|
|
142
|
+
setOpen(false); setView('day');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
82
145
|
document.addEventListener('mousedown', close, true);
|
|
83
|
-
|
|
146
|
+
document.addEventListener('keydown', onKey);
|
|
147
|
+
return () => {
|
|
148
|
+
document.removeEventListener('mousedown', close, true);
|
|
149
|
+
document.removeEventListener('keydown', onKey);
|
|
150
|
+
};
|
|
84
151
|
}, [open]);
|
|
85
152
|
|
|
86
153
|
const [stepperPosState, setStepperPosState] = useState(() => typeof window !== 'undefined' ? localStorage.getItem('datepicker_stepper_position') : null);
|
|
@@ -153,8 +220,10 @@ export function DatePicker({ value, onChange, label, placeholder = 'Select date'
|
|
|
153
220
|
} else {
|
|
154
221
|
onChange?.(date);
|
|
155
222
|
}
|
|
156
|
-
|
|
157
|
-
|
|
223
|
+
if (!showTime) {
|
|
224
|
+
setOpen(false);
|
|
225
|
+
setView('day');
|
|
226
|
+
}
|
|
158
227
|
}
|
|
159
228
|
|
|
160
229
|
const isNavNextDisabled = () => {
|
|
@@ -212,9 +281,10 @@ export function DatePicker({ value, onChange, label, placeholder = 'Select date'
|
|
|
212
281
|
}
|
|
213
282
|
|
|
214
283
|
const valDate = toDate(value);
|
|
284
|
+
const formattedTime = showTime && timeValue ? ` ${timeValue}` : '';
|
|
215
285
|
const formatted = valDate
|
|
216
|
-
? `${MONTHS[valDate.getMonth()]} ${valDate.getDate()}, ${valDate.getFullYear()}`
|
|
217
|
-
: (typeof value === 'string' ? value : null);
|
|
286
|
+
? `${MONTHS[valDate.getMonth()]} ${valDate.getDate()}, ${valDate.getFullYear()}${formattedTime}`
|
|
287
|
+
: (typeof value === 'string' && value ? `${value}${formattedTime}` : null);
|
|
218
288
|
|
|
219
289
|
// ── Day grid ─────────────────────────────────────────────────
|
|
220
290
|
function renderDays() {
|
|
@@ -450,7 +520,7 @@ export function DatePicker({ value, onChange, label, placeholder = 'Select date'
|
|
|
450
520
|
onClick={(e) => { e.stopPropagation(); changeDay(-1); }}
|
|
451
521
|
style={{
|
|
452
522
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
453
|
-
width:
|
|
523
|
+
width: s.btnSize, height: s.btnSize, borderRadius: 'var(--rads, 8px)',
|
|
454
524
|
border: '1px solid var(--border, #e2e8f0)', background: 'var(--surface, #ffffff)',
|
|
455
525
|
cursor: dis ? 'not-allowed' : 'pointer', opacity: dis ? 0.35 : 1,
|
|
456
526
|
color: 'var(--muted, #797b8d)', transition: 'all .15s', boxShadow: 'var(--shadow, 0 1px 3px rgba(0,0,0,.08), 0 1px 2px rgba(0,0,0,.06))',
|
|
@@ -460,7 +530,7 @@ export function DatePicker({ value, onChange, label, placeholder = 'Select date'
|
|
|
460
530
|
onMouseEnter={e => { if (!dis) { e.currentTarget.style.borderColor = 'var(--accent, #f99e2c)'; e.currentTarget.style.color = 'var(--accent, #f99e2c)'; } }}
|
|
461
531
|
onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border, #e2e8f0)'; e.currentTarget.style.color = 'var(--muted, #797b8d)'; }}
|
|
462
532
|
>
|
|
463
|
-
<ChevronLeft size={
|
|
533
|
+
<ChevronLeft size={s.iconSize + 2} />
|
|
464
534
|
</button>
|
|
465
535
|
);
|
|
466
536
|
};
|
|
@@ -474,7 +544,7 @@ export function DatePicker({ value, onChange, label, placeholder = 'Select date'
|
|
|
474
544
|
onClick={(e) => { e.stopPropagation(); changeDay(1); }}
|
|
475
545
|
style={{
|
|
476
546
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
477
|
-
width:
|
|
547
|
+
width: s.btnSize, height: s.btnSize, borderRadius: 'var(--rads, 8px)',
|
|
478
548
|
border: '1px solid var(--border, #e2e8f0)', background: 'var(--surface, #ffffff)',
|
|
479
549
|
cursor: dis ? 'not-allowed' : 'pointer', opacity: dis ? 0.35 : 1,
|
|
480
550
|
color: 'var(--muted, #797b8d)', transition: 'all .15s', boxShadow: 'var(--shadow, 0 1px 3px rgba(0,0,0,.08), 0 1px 2px rgba(0,0,0,.06))',
|
|
@@ -484,7 +554,7 @@ export function DatePicker({ value, onChange, label, placeholder = 'Select date'
|
|
|
484
554
|
onMouseEnter={e => { if (!dis) { e.currentTarget.style.borderColor = 'var(--accent, #f99e2c)'; e.currentTarget.style.color = 'var(--accent, #f99e2c)'; } }}
|
|
485
555
|
onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border, #e2e8f0)'; e.currentTarget.style.color = 'var(--muted, #797b8d)'; }}
|
|
486
556
|
>
|
|
487
|
-
<ChevronRight size={
|
|
557
|
+
<ChevronRight size={s.iconSize + 2} />
|
|
488
558
|
</button>
|
|
489
559
|
);
|
|
490
560
|
};
|
|
@@ -492,21 +562,24 @@ export function DatePicker({ value, onChange, label, placeholder = 'Select date'
|
|
|
492
562
|
const renderTriggerBtn = () => (
|
|
493
563
|
<button ref={btnRef} type="button" onClick={toggleOpen} style={{
|
|
494
564
|
display: 'flex', alignItems: 'center', gap: 7,
|
|
495
|
-
padding:
|
|
565
|
+
padding: `0 ${s.paddingH}px`, borderRadius: 'var(--rads, 8px)',
|
|
496
566
|
border: '1px solid var(--border, #e2e8f0)', background: 'var(--surface, #ffffff)',
|
|
497
567
|
cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? 0.5 : 1,
|
|
498
|
-
fontSize:
|
|
568
|
+
fontSize: s.fontSize, color: formatted ? 'var(--primary-text, #0f172a)' : 'var(--muted, #797b8d)',
|
|
499
569
|
fontFamily: 'var(--sans, inherit)', transition: 'border-color .15s',
|
|
500
|
-
whiteSpace: 'nowrap', minWidth:
|
|
570
|
+
whiteSpace: 'nowrap', minWidth: 0, flex: 1, height: s.height, boxShadow: 'var(--shadow, 0 1px 3px rgba(0,0,0,.08), 0 1px 2px rgba(0,0,0,.06))',
|
|
571
|
+
overflow: 'hidden'
|
|
501
572
|
}}
|
|
502
573
|
onMouseEnter={e => { if (!disabled) e.currentTarget.style.borderColor = 'var(--accent, #f99e2c)'; }}
|
|
503
574
|
onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border, #e2e8f0)'; }}>
|
|
504
|
-
<Calendar size={
|
|
505
|
-
<span style={{ flex: 1, textAlign: 'left'
|
|
575
|
+
<Calendar size={s.iconSize} style={{ color: 'var(--muted, #797b8d)', flexShrink: 0 }} />
|
|
576
|
+
<span style={{ flex: 1, textAlign: 'left', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', minWidth: 0 }}>
|
|
577
|
+
{formatted || placeholder}
|
|
578
|
+
</span>
|
|
506
579
|
{value && (
|
|
507
580
|
<span
|
|
508
581
|
onClick={e => { e.stopPropagation(); onChange?.(null); }}
|
|
509
|
-
style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--muted, #797b8d)', cursor: 'pointer', flexShrink: 0, fontSize:
|
|
582
|
+
style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--muted, #797b8d)', cursor: 'pointer', flexShrink: 0, fontSize: s.closeSize, lineHeight: 1, padding: '0 2px' }}
|
|
510
583
|
onMouseEnter={e => { e.currentTarget.style.color = 'var(--danger, #f64747)'; }}
|
|
511
584
|
onMouseLeave={e => { e.currentTarget.style.color = 'var(--muted, #797b8d)'; }}>
|
|
512
585
|
✕
|
|
@@ -522,13 +595,30 @@ export function DatePicker({ value, onChange, label, placeholder = 'Select date'
|
|
|
522
595
|
: 'around';
|
|
523
596
|
|
|
524
597
|
return (
|
|
525
|
-
<div style={{
|
|
598
|
+
<div style={{
|
|
599
|
+
display: fullWidth || style?.display === 'flex' ? 'flex' : 'inline-flex',
|
|
600
|
+
flexDirection: 'column',
|
|
601
|
+
position: 'relative',
|
|
602
|
+
width: style?.width || (fullWidth ? '100%' : 'auto'),
|
|
603
|
+
maxWidth: style?.maxWidth || (fullWidth ? '100%' : 320),
|
|
604
|
+
boxSizing: 'border-box',
|
|
605
|
+
minWidth: 0,
|
|
606
|
+
...style,
|
|
607
|
+
}}>
|
|
526
608
|
{label && (
|
|
527
609
|
<label style={{ fontSize: 11, fontWeight: 600, color: 'var(--t2, var(--muted, #797b8d))', textTransform: 'uppercase', letterSpacing: '.08em', display: 'block', marginBottom: 4 }}>
|
|
528
610
|
{label}
|
|
529
611
|
</label>
|
|
530
612
|
)}
|
|
531
|
-
<div ref={ref} style={{
|
|
613
|
+
<div ref={ref} style={{
|
|
614
|
+
position: 'relative',
|
|
615
|
+
display: 'inline-flex',
|
|
616
|
+
alignItems: 'center',
|
|
617
|
+
gap: 4,
|
|
618
|
+
width: '100%',
|
|
619
|
+
maxWidth: '100%',
|
|
620
|
+
minWidth: 0,
|
|
621
|
+
}}>
|
|
532
622
|
{showStepperEffective && posMode === 'around' && renderPrevBtn()}
|
|
533
623
|
{showStepperEffective && posMode === 'left' && renderPrevBtn()}
|
|
534
624
|
{showStepperEffective && posMode === 'left' && renderNextBtn()}
|
|
@@ -542,7 +632,7 @@ export function DatePicker({ value, onChange, label, placeholder = 'Select date'
|
|
|
542
632
|
|
|
543
633
|
{/* Dropdown via portal */}
|
|
544
634
|
{open && createPortal(
|
|
545
|
-
<div ref={dropRef} style={{
|
|
635
|
+
<div ref={dropRef} className="cynx-datepicker-drop" style={{
|
|
546
636
|
position: 'fixed', top: pos.top, left: pos.left, width: pos.width, zIndex: 9000,
|
|
547
637
|
background: 'var(--surface, #ffffff)', border: '1px solid var(--border, #e2e8f0)',
|
|
548
638
|
borderRadius: 'var(--rad, 10px)', boxShadow: 'var(--shadow-lg, 0 10px 15px -3px rgba(0,0,0,.1), 0 4px 6px -4px rgba(0,0,0,.06))',
|
|
@@ -595,6 +685,56 @@ export function DatePicker({ value, onChange, label, placeholder = 'Select date'
|
|
|
595
685
|
{view === 'month' && renderMonths()}
|
|
596
686
|
{view === 'year' && renderYears()}
|
|
597
687
|
|
|
688
|
+
{/* Integrated Time Picker footer */}
|
|
689
|
+
{showTime && (
|
|
690
|
+
<div
|
|
691
|
+
style={{
|
|
692
|
+
borderTop: '1px solid var(--border-light, #ebebf0)',
|
|
693
|
+
marginTop: 8,
|
|
694
|
+
paddingTop: 8,
|
|
695
|
+
display: 'flex',
|
|
696
|
+
alignItems: 'center',
|
|
697
|
+
justifyContent: 'space-between',
|
|
698
|
+
gap: 8,
|
|
699
|
+
}}
|
|
700
|
+
>
|
|
701
|
+
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 5, fontSize: '.76rem', color: 'var(--muted, #797b8d)', fontWeight: 600 }}>
|
|
702
|
+
<Clock size={13} style={{ opacity: 0.85 }} />
|
|
703
|
+
<span>{loc.time || 'Time:'}</span>
|
|
704
|
+
</div>
|
|
705
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
|
706
|
+
<TimePicker
|
|
707
|
+
value={timeValue || ''}
|
|
708
|
+
onChange={(t) => onTimeChange?.(t)}
|
|
709
|
+
format={timeFormat}
|
|
710
|
+
showSeconds={showSeconds}
|
|
711
|
+
placeholder={timePlaceholder}
|
|
712
|
+
size={timeProps.size || size || 'md'}
|
|
713
|
+
{...timeProps}
|
|
714
|
+
/>
|
|
715
|
+
<button
|
|
716
|
+
type="button"
|
|
717
|
+
onClick={() => { setOpen(false); setView('day'); }}
|
|
718
|
+
style={{
|
|
719
|
+
padding: '4px 10px',
|
|
720
|
+
borderRadius: 'var(--radxs, 6px)',
|
|
721
|
+
background: 'var(--accent, #f99e2c)',
|
|
722
|
+
color: 'var(--def-btn-text, #ffffff)',
|
|
723
|
+
border: 'none',
|
|
724
|
+
fontSize: '11.5px',
|
|
725
|
+
fontWeight: 600,
|
|
726
|
+
cursor: 'pointer',
|
|
727
|
+
transition: 'opacity 0.15s ease',
|
|
728
|
+
}}
|
|
729
|
+
onMouseEnter={(e) => { e.currentTarget.style.opacity = '0.9'; }}
|
|
730
|
+
onMouseLeave={(e) => { e.currentTarget.style.opacity = '1'; }}
|
|
731
|
+
>
|
|
732
|
+
{loc.ok || 'OK'}
|
|
733
|
+
</button>
|
|
734
|
+
</div>
|
|
735
|
+
</div>
|
|
736
|
+
)}
|
|
737
|
+
|
|
598
738
|
</div>,
|
|
599
739
|
document.body
|
|
600
740
|
)}
|
package/vue/components/Badge.vue
CHANGED
|
@@ -3,11 +3,19 @@ import { computed } from "vue";
|
|
|
3
3
|
|
|
4
4
|
const VARIANTS: Record<string, any> = {
|
|
5
5
|
ok: { bg: "var(--success-bg, rgba(39,174,96,.1))", color: "var(--success, #16a34a)", border: "var(--sd, rgba(39,174,96,.2))", dot: "var(--success, #16a34a)" },
|
|
6
|
+
success: { bg: "var(--success-bg, rgba(39,174,96,.1))", color: "var(--success, #16a34a)", border: "var(--sd, rgba(39,174,96,.2))", dot: "var(--success, #16a34a)" },
|
|
6
7
|
err: { bg: "var(--danger-bg, rgba(246,71,71,.1))", color: "var(--danger, #ef4444)", border: "var(--dd, rgba(246,71,71,.2))", dot: "var(--danger, #ef4444)" },
|
|
8
|
+
danger: { bg: "var(--danger-bg, rgba(246,71,71,.1))", color: "var(--danger, #ef4444)", border: "var(--dd, rgba(246,71,71,.2))", dot: "var(--danger, #ef4444)" },
|
|
9
|
+
error: { bg: "var(--danger-bg, rgba(246,71,71,.1))", color: "var(--danger, #ef4444)", border: "var(--dd, rgba(246,71,71,.2))", dot: "var(--danger, #ef4444)" },
|
|
7
10
|
warn: { bg: "var(--accent-bg, rgba(249,158,44,.1))", color: "var(--accent, #f59e0b)", border: "var(--accent-border, rgba(249,158,44,.25))", dot: "var(--accent, #f59e0b)" },
|
|
11
|
+
warning: { bg: "var(--accent-bg, rgba(249,158,44,.1))", color: "var(--accent, #f59e0b)", border: "var(--accent-border, rgba(249,158,44,.25))", dot: "var(--accent, #f59e0b)" },
|
|
8
12
|
blue: { bg: "var(--blue-bg, rgba(39,130,228,.1))", color: "var(--blue, #2563eb)", border: "var(--infd, rgba(39,130,228,.2))", dot: "var(--blue, #2563eb)" },
|
|
13
|
+
info: { bg: "var(--blue-bg, rgba(39,130,228,.1))", color: "var(--blue, #2563eb)", border: "var(--infd, rgba(39,130,228,.2))", dot: "var(--blue, #2563eb)" },
|
|
9
14
|
teal: { bg: "rgba(18,103,93,.1)", color: "var(--accent, #12675D)", border: "rgba(18,103,93,.25)", dot: "var(--accent, #12675D)" },
|
|
15
|
+
primary: { bg: "rgba(18,103,93,.1)", color: "var(--accent, #12675D)", border: "rgba(18,103,93,.25)", dot: "var(--accent, #12675D)" },
|
|
10
16
|
dim: { bg: "var(--grey-1, #f5f5f8)", color: "var(--grey-4, #64748b)", border: "var(--border, #e2e8f0)", dot: "var(--grey-4, #64748b)" },
|
|
17
|
+
neutral: { bg: "var(--grey-1, #f5f5f8)", color: "var(--grey-4, #64748b)", border: "var(--border, #e2e8f0)", dot: "var(--grey-4, #64748b)" },
|
|
18
|
+
secondary: { bg: "var(--grey-1, #f5f5f8)", color: "var(--grey-4, #64748b)", border: "var(--border, #e2e8f0)", dot: "var(--grey-4, #64748b)" },
|
|
11
19
|
ghost: { bg: "transparent", color: "var(--t2, #64748b)", border: "var(--bd, #e2e8f0)", dot: "var(--t3, #94a3b8)" },
|
|
12
20
|
};
|
|
13
21
|
|
|
@@ -0,0 +1,978 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
ref,
|
|
4
|
+
computed,
|
|
5
|
+
watch,
|
|
6
|
+
onMounted,
|
|
7
|
+
onBeforeUnmount,
|
|
8
|
+
nextTick,
|
|
9
|
+
useSlots,
|
|
10
|
+
} from "vue";
|
|
11
|
+
import { ChevronLeft, ChevronRight, Calendar, Clock } from "lucide-vue-next";
|
|
12
|
+
|
|
13
|
+
export interface DatePickerBadge {
|
|
14
|
+
dot?: boolean;
|
|
15
|
+
color?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface DatePickerLocale {
|
|
19
|
+
days: string[];
|
|
20
|
+
months: string[];
|
|
21
|
+
today?: string;
|
|
22
|
+
time?: string;
|
|
23
|
+
ok?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const LOCALES: Record<string, DatePickerLocale> = {
|
|
27
|
+
en: {
|
|
28
|
+
days: ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"],
|
|
29
|
+
months: [
|
|
30
|
+
"Jan",
|
|
31
|
+
"Feb",
|
|
32
|
+
"Mar",
|
|
33
|
+
"Apr",
|
|
34
|
+
"May",
|
|
35
|
+
"Jun",
|
|
36
|
+
"Jul",
|
|
37
|
+
"Aug",
|
|
38
|
+
"Sep",
|
|
39
|
+
"Oct",
|
|
40
|
+
"Nov",
|
|
41
|
+
"Dec",
|
|
42
|
+
],
|
|
43
|
+
today: "Today",
|
|
44
|
+
time: "Time:",
|
|
45
|
+
ok: "OK",
|
|
46
|
+
},
|
|
47
|
+
ru: {
|
|
48
|
+
days: ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"],
|
|
49
|
+
months: [
|
|
50
|
+
"Янв",
|
|
51
|
+
"Фев",
|
|
52
|
+
"Мар",
|
|
53
|
+
"Апр",
|
|
54
|
+
"Май",
|
|
55
|
+
"Июн",
|
|
56
|
+
"Июл",
|
|
57
|
+
"Авг",
|
|
58
|
+
"Сен",
|
|
59
|
+
"Окт",
|
|
60
|
+
"Ноя",
|
|
61
|
+
"Дек",
|
|
62
|
+
],
|
|
63
|
+
today: "Сегодня",
|
|
64
|
+
time: "Время:",
|
|
65
|
+
ok: "OK",
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const SIZES: Record<
|
|
70
|
+
string,
|
|
71
|
+
{
|
|
72
|
+
height: number;
|
|
73
|
+
fontSize: number;
|
|
74
|
+
paddingH: number;
|
|
75
|
+
iconSize: number;
|
|
76
|
+
btnSize: number;
|
|
77
|
+
closeSize: number;
|
|
78
|
+
}
|
|
79
|
+
> = {
|
|
80
|
+
xs: { height: 24, fontSize: 11, paddingH: 6, iconSize: 11, btnSize: 24, closeSize: 11 },
|
|
81
|
+
sm: { height: 28, fontSize: 11.5, paddingH: 8, iconSize: 12, btnSize: 28, closeSize: 12 },
|
|
82
|
+
md: { height: 32, fontSize: 13, paddingH: 10, iconSize: 13, btnSize: 32, closeSize: 13 },
|
|
83
|
+
lg: { height: 36, fontSize: 14, paddingH: 12, iconSize: 14, btnSize: 36, closeSize: 14 },
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
function pad(n: number) {
|
|
87
|
+
return String(n).padStart(2, "0");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function toKey(y: number, m: number, d: number) {
|
|
91
|
+
return `${y}-${pad(m + 1)}-${pad(d)}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function toDate(val: any): Date | null {
|
|
95
|
+
if (!val) return null;
|
|
96
|
+
if (val instanceof Date) return isNaN(val.getTime()) ? null : val;
|
|
97
|
+
if (typeof val === "string") {
|
|
98
|
+
const parts = val.split("-");
|
|
99
|
+
if (parts.length === 3) {
|
|
100
|
+
const y = parseInt(parts[0], 10);
|
|
101
|
+
const m = parseInt(parts[1], 10) - 1;
|
|
102
|
+
const d = parseInt(parts[2], 10);
|
|
103
|
+
if (!isNaN(y) && !isNaN(m) && !isNaN(d)) {
|
|
104
|
+
return new Date(y, m, d);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const d = new Date(val);
|
|
109
|
+
return isNaN(d.getTime()) ? null : d;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function isSameDay(a: any, b: any) {
|
|
113
|
+
const da = toDate(a);
|
|
114
|
+
const db = toDate(b);
|
|
115
|
+
return (
|
|
116
|
+
Boolean(da && db) &&
|
|
117
|
+
da!.getFullYear() === db!.getFullYear() &&
|
|
118
|
+
da!.getMonth() === db!.getMonth() &&
|
|
119
|
+
da!.getDate() === db!.getDate()
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function isToday(y: number, m: number, d: number) {
|
|
124
|
+
const t = new Date();
|
|
125
|
+
return t.getFullYear() === y && t.getMonth() === m && t.getDate() === d;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const STYLES = `
|
|
129
|
+
@keyframes dpFadeIn { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: translateY(0); } }
|
|
130
|
+
`;
|
|
131
|
+
const _id = "cynx-datepicker-styles";
|
|
132
|
+
function ensureStyles() {
|
|
133
|
+
if (typeof document === "undefined") return;
|
|
134
|
+
if (document.getElementById(_id)) return;
|
|
135
|
+
const tag = document.createElement("style");
|
|
136
|
+
tag.id = _id;
|
|
137
|
+
tag.textContent = STYLES;
|
|
138
|
+
document.head.appendChild(tag);
|
|
139
|
+
}
|
|
140
|
+
ensureStyles();
|
|
141
|
+
|
|
142
|
+
const props = withDefaults(
|
|
143
|
+
defineProps<{
|
|
144
|
+
modelValue?: any;
|
|
145
|
+
value?: any;
|
|
146
|
+
label?: string;
|
|
147
|
+
placeholder?: string;
|
|
148
|
+
badges?: Record<string, DatePickerBadge>;
|
|
149
|
+
minDate?: any;
|
|
150
|
+
maxDate?: any;
|
|
151
|
+
disabled?: boolean;
|
|
152
|
+
size?: "xs" | "sm" | "md" | "lg";
|
|
153
|
+
showStepper?: boolean;
|
|
154
|
+
stepperPosition?: "off" | "around" | "left" | "right" | "start" | "end";
|
|
155
|
+
locale?: "en" | "ru" | DatePickerLocale | string;
|
|
156
|
+
clearable?: boolean;
|
|
157
|
+
fullWidth?: boolean;
|
|
158
|
+
style?: Record<string, any>;
|
|
159
|
+
showTime?: boolean;
|
|
160
|
+
timeValue?: string;
|
|
161
|
+
timeFormat?: "24h" | "12h";
|
|
162
|
+
showSeconds?: boolean;
|
|
163
|
+
timePlaceholder?: string;
|
|
164
|
+
}>(),
|
|
165
|
+
{
|
|
166
|
+
modelValue: undefined,
|
|
167
|
+
value: undefined,
|
|
168
|
+
label: "",
|
|
169
|
+
placeholder: "Select date",
|
|
170
|
+
badges: () => ({}),
|
|
171
|
+
minDate: undefined,
|
|
172
|
+
maxDate: undefined,
|
|
173
|
+
disabled: false,
|
|
174
|
+
size: "md",
|
|
175
|
+
showStepper: false,
|
|
176
|
+
stepperPosition: "off",
|
|
177
|
+
locale: "en",
|
|
178
|
+
clearable: true,
|
|
179
|
+
fullWidth: false,
|
|
180
|
+
style: () => ({}),
|
|
181
|
+
showTime: false,
|
|
182
|
+
timeValue: "",
|
|
183
|
+
timeFormat: "24h",
|
|
184
|
+
showSeconds: false,
|
|
185
|
+
timePlaceholder: "00:00",
|
|
186
|
+
}
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
const emit = defineEmits<{
|
|
190
|
+
(e: "update:modelValue", value: any): void;
|
|
191
|
+
(e: "update:value", value: any): void;
|
|
192
|
+
(e: "change", value: any): void;
|
|
193
|
+
(e: "update:timeValue", value: string): void;
|
|
194
|
+
(e: "time-change", value: string): void;
|
|
195
|
+
(e: "clear"): void;
|
|
196
|
+
}>();
|
|
197
|
+
|
|
198
|
+
const currentVal = computed(() => {
|
|
199
|
+
return props.modelValue !== undefined ? props.modelValue : props.value;
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const loc = computed<DatePickerLocale>(() => {
|
|
203
|
+
if (typeof props.locale === "object" && props.locale !== null) {
|
|
204
|
+
return props.locale;
|
|
205
|
+
}
|
|
206
|
+
return LOCALES[props.locale as string] || LOCALES.en;
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const s = computed(() => SIZES[props.size] || SIZES.md);
|
|
210
|
+
const open = ref(false);
|
|
211
|
+
const view = ref<"day" | "month" | "year">("day");
|
|
212
|
+
const cursor = ref<Date>(toDate(currentVal.value) || new Date());
|
|
213
|
+
const pos = ref({ top: 0, left: 0, width: 0 });
|
|
214
|
+
|
|
215
|
+
const wrapRef = ref<HTMLElement | null>(null);
|
|
216
|
+
const btnRef = ref<HTMLElement | null>(null);
|
|
217
|
+
const dropRef = ref<HTMLElement | null>(null);
|
|
218
|
+
|
|
219
|
+
watch(
|
|
220
|
+
currentVal,
|
|
221
|
+
(newVal) => {
|
|
222
|
+
const d = toDate(newVal);
|
|
223
|
+
if (d) cursor.value = d;
|
|
224
|
+
},
|
|
225
|
+
{ immediate: true }
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
const calcPos = () => {
|
|
229
|
+
if (!btnRef.value) return;
|
|
230
|
+
const r = btnRef.value.getBoundingClientRect();
|
|
231
|
+
const dropW = Math.max(r.width, 260);
|
|
232
|
+
const dropH = dropRef.value ? dropRef.value.offsetHeight : 280;
|
|
233
|
+
|
|
234
|
+
let left = r.left;
|
|
235
|
+
if (left + dropW > window.innerWidth - 12) {
|
|
236
|
+
left = Math.max(12, r.right - dropW);
|
|
237
|
+
}
|
|
238
|
+
if (left < 12) left = 12;
|
|
239
|
+
|
|
240
|
+
let top = r.bottom + 6;
|
|
241
|
+
if (top + dropH > window.innerHeight - 12 && r.top - 6 - dropH > 12) {
|
|
242
|
+
top = r.top - 6 - dropH;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
pos.value = { top, left, width: dropW };
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const handleWindowClick = (e: MouseEvent) => {
|
|
249
|
+
if (!open.value) return;
|
|
250
|
+
const target = e.target as Node;
|
|
251
|
+
if (wrapRef.value?.contains(target)) return;
|
|
252
|
+
if (dropRef.value?.contains(target)) return;
|
|
253
|
+
open.value = false;
|
|
254
|
+
view.value = "day";
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
258
|
+
if (e.key === "Escape" && open.value) {
|
|
259
|
+
open.value = false;
|
|
260
|
+
view.value = "day";
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
watch(open, (isOpen) => {
|
|
265
|
+
if (isOpen) {
|
|
266
|
+
calcPos();
|
|
267
|
+
window.addEventListener("scroll", calcPos, true);
|
|
268
|
+
window.addEventListener("resize", calcPos);
|
|
269
|
+
document.addEventListener("mousedown", handleWindowClick, true);
|
|
270
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
271
|
+
nextTick(calcPos);
|
|
272
|
+
} else {
|
|
273
|
+
window.removeEventListener("scroll", calcPos, true);
|
|
274
|
+
window.removeEventListener("resize", calcPos);
|
|
275
|
+
document.removeEventListener("mousedown", handleWindowClick, true);
|
|
276
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
onBeforeUnmount(() => {
|
|
281
|
+
window.removeEventListener("scroll", calcPos, true);
|
|
282
|
+
window.removeEventListener("resize", calcPos);
|
|
283
|
+
document.removeEventListener("mousedown", handleWindowClick, true);
|
|
284
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const toggleOpen = () => {
|
|
288
|
+
if (props.disabled) return;
|
|
289
|
+
if (!open.value) calcPos();
|
|
290
|
+
open.value = !open.value;
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
function pick(date: Date) {
|
|
294
|
+
if (!date) return;
|
|
295
|
+
const minD = toDate(props.minDate);
|
|
296
|
+
const maxD = toDate(props.maxDate);
|
|
297
|
+
if (minD && date < new Date(new Date(minD).setHours(0, 0, 0, 0))) return;
|
|
298
|
+
if (maxD && date > new Date(new Date(maxD).setHours(23, 59, 59, 999))) return;
|
|
299
|
+
|
|
300
|
+
const y = date.getFullYear();
|
|
301
|
+
const m = pad(date.getMonth() + 1);
|
|
302
|
+
const d = pad(date.getDate());
|
|
303
|
+
const strVal = `${y}-${m}-${d}`;
|
|
304
|
+
|
|
305
|
+
if (typeof currentVal.value === "string" || currentVal.value === undefined) {
|
|
306
|
+
emit("update:modelValue", strVal);
|
|
307
|
+
emit("update:value", strVal);
|
|
308
|
+
emit("change", strVal);
|
|
309
|
+
} else {
|
|
310
|
+
emit("update:modelValue", date);
|
|
311
|
+
emit("update:value", date);
|
|
312
|
+
emit("change", date);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (!props.showTime) {
|
|
316
|
+
open.value = false;
|
|
317
|
+
view.value = "day";
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function handleClear(e: MouseEvent) {
|
|
322
|
+
e.stopPropagation();
|
|
323
|
+
emit("update:modelValue", null);
|
|
324
|
+
emit("update:value", null);
|
|
325
|
+
emit("change", null);
|
|
326
|
+
emit("clear");
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const changeDay = (delta: number) => {
|
|
330
|
+
if (props.disabled) return;
|
|
331
|
+
const current = toDate(currentVal.value) || new Date();
|
|
332
|
+
const next = new Date(
|
|
333
|
+
current.getFullYear(),
|
|
334
|
+
current.getMonth(),
|
|
335
|
+
current.getDate() + delta
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
const minD = toDate(props.minDate);
|
|
339
|
+
const maxD = toDate(props.maxDate);
|
|
340
|
+
if (minD && next < new Date(new Date(minD).setHours(0, 0, 0, 0))) return;
|
|
341
|
+
if (maxD && next > new Date(new Date(maxD).setHours(23, 59, 59, 999))) return;
|
|
342
|
+
|
|
343
|
+
pick(next);
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const isNavNextDisabled = () => {
|
|
347
|
+
const maxD = toDate(props.maxDate);
|
|
348
|
+
if (!maxD) return false;
|
|
349
|
+
const y = cursor.value.getFullYear();
|
|
350
|
+
const m = cursor.value.getMonth();
|
|
351
|
+
if (view.value === "day") {
|
|
352
|
+
const nextMonthStart = new Date(y, m + 1, 1);
|
|
353
|
+
return nextMonthStart > new Date(new Date(maxD).setHours(23, 59, 59, 999));
|
|
354
|
+
}
|
|
355
|
+
if (view.value === "month") {
|
|
356
|
+
const nextYearStart = new Date(y + 1, 0, 1);
|
|
357
|
+
return nextYearStart > new Date(new Date(maxD).setHours(23, 59, 59, 999));
|
|
358
|
+
}
|
|
359
|
+
if (view.value === "year") {
|
|
360
|
+
const base = Math.floor(y / 12) * 12;
|
|
361
|
+
const nextBlockStart = new Date(base + 12, 0, 1);
|
|
362
|
+
return nextBlockStart > new Date(new Date(maxD).setHours(23, 59, 59, 999));
|
|
363
|
+
}
|
|
364
|
+
return false;
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
const isNavPrevDisabled = () => {
|
|
368
|
+
const minD = toDate(props.minDate);
|
|
369
|
+
if (!minD) return false;
|
|
370
|
+
const y = cursor.value.getFullYear();
|
|
371
|
+
const m = cursor.value.getMonth();
|
|
372
|
+
if (view.value === "day") {
|
|
373
|
+
const prevMonthEnd = new Date(y, m, 0, 23, 59, 59, 999);
|
|
374
|
+
return prevMonthEnd < new Date(new Date(minD).setHours(0, 0, 0, 0));
|
|
375
|
+
}
|
|
376
|
+
if (view.value === "month") {
|
|
377
|
+
const prevYearEnd = new Date(y - 1, 11, 31, 23, 59, 59, 999);
|
|
378
|
+
return prevYearEnd < new Date(new Date(minD).setHours(0, 0, 0, 0));
|
|
379
|
+
}
|
|
380
|
+
if (view.value === "year") {
|
|
381
|
+
const base = Math.floor(y / 12) * 12;
|
|
382
|
+
const prevBlockEnd = new Date(base - 1, 11, 31, 23, 59, 59, 999);
|
|
383
|
+
return prevBlockEnd < new Date(new Date(minD).setHours(0, 0, 0, 0));
|
|
384
|
+
}
|
|
385
|
+
return false;
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
function nav(dir: number) {
|
|
389
|
+
if (dir > 0 && isNavNextDisabled()) return;
|
|
390
|
+
if (dir < 0 && isNavPrevDisabled()) return;
|
|
391
|
+
const c = new Date(cursor.value);
|
|
392
|
+
if (view.value === "day") c.setMonth(c.getMonth() + dir);
|
|
393
|
+
if (view.value === "month") c.setFullYear(c.getFullYear() + dir);
|
|
394
|
+
if (view.value === "year") c.setFullYear(c.getFullYear() + dir * 12);
|
|
395
|
+
cursor.value = c;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function cycleView() {
|
|
399
|
+
view.value = view.value === "day" ? "month" : "year";
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const valDate = computed(() => toDate(currentVal.value));
|
|
403
|
+
|
|
404
|
+
const formatted = computed(() => {
|
|
405
|
+
const vd = valDate.value;
|
|
406
|
+
const formattedTime =
|
|
407
|
+
props.showTime && props.timeValue ? ` ${props.timeValue}` : "";
|
|
408
|
+
if (vd) {
|
|
409
|
+
const mName = loc.value.months[vd.getMonth()] || "";
|
|
410
|
+
return `${mName} ${vd.getDate()}, ${vd.getFullYear()}${formattedTime}`;
|
|
411
|
+
}
|
|
412
|
+
if (typeof currentVal.value === "string" && currentVal.value) {
|
|
413
|
+
return `${currentVal.value}${formattedTime}`;
|
|
414
|
+
}
|
|
415
|
+
return null;
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
const headerLabel = computed(() => {
|
|
419
|
+
const y = cursor.value.getFullYear();
|
|
420
|
+
const m = cursor.value.getMonth();
|
|
421
|
+
if (view.value === "day") {
|
|
422
|
+
return `${loc.value.months[m]} ${y}`;
|
|
423
|
+
}
|
|
424
|
+
if (view.value === "month") {
|
|
425
|
+
return `${y}`;
|
|
426
|
+
}
|
|
427
|
+
const base = Math.floor(y / 12) * 12;
|
|
428
|
+
return `${base} – ${base + 11}`;
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
const daysCells = computed(() => {
|
|
432
|
+
const y = cursor.value.getFullYear();
|
|
433
|
+
const m = cursor.value.getMonth();
|
|
434
|
+
const first = new Date(y, m, 1).getDay(); // 0=Sun
|
|
435
|
+
const offset = first === 0 ? 6 : first === 1 ? 7 : first - 1;
|
|
436
|
+
const daysInMonth = new Date(y, m + 1, 0).getDate();
|
|
437
|
+
const prevDays = new Date(y, m, 0).getDate();
|
|
438
|
+
|
|
439
|
+
const cells: Array<{ day: number; cur: boolean; next: boolean; date: Date }> =
|
|
440
|
+
[];
|
|
441
|
+
for (let i = offset - 1; i >= 0; i--) {
|
|
442
|
+
const d = prevDays - i;
|
|
443
|
+
cells.push({ day: d, cur: false, next: false, date: new Date(y, m - 1, d) });
|
|
444
|
+
}
|
|
445
|
+
for (let d = 1; d <= daysInMonth; d++) {
|
|
446
|
+
cells.push({ day: d, cur: true, next: false, date: new Date(y, m, d) });
|
|
447
|
+
}
|
|
448
|
+
const totalSoFar = cells.length;
|
|
449
|
+
let remaining = (7 - (totalSoFar % 7)) % 7;
|
|
450
|
+
if (remaining === 0) remaining = 7;
|
|
451
|
+
for (let d = 1; d <= remaining; d++) {
|
|
452
|
+
cells.push({ day: d, cur: false, next: true, date: new Date(y, m + 1, d) });
|
|
453
|
+
}
|
|
454
|
+
return cells;
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
const yearsList = computed(() => {
|
|
458
|
+
const base = Math.floor(cursor.value.getFullYear() / 12) * 12;
|
|
459
|
+
return Array.from({ length: 12 }, (_, i) => base + i);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
const isCellDisabled = (date: Date) => {
|
|
463
|
+
const minD = toDate(props.minDate);
|
|
464
|
+
const maxD = toDate(props.maxDate);
|
|
465
|
+
const disMin = minD && date < new Date(new Date(minD).setHours(0, 0, 0, 0));
|
|
466
|
+
const disMax = maxD && date > new Date(new Date(maxD).setHours(23, 59, 59, 999));
|
|
467
|
+
return Boolean(disMin || disMax);
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
const isMonthDisabled = (monthIndex: number) => {
|
|
471
|
+
const y = cursor.value.getFullYear();
|
|
472
|
+
const minD = toDate(props.minDate);
|
|
473
|
+
const maxD = toDate(props.maxDate);
|
|
474
|
+
const monthStart = new Date(y, monthIndex, 1);
|
|
475
|
+
const monthEnd = new Date(y, monthIndex + 1, 0, 23, 59, 59, 999);
|
|
476
|
+
const disMin = minD && monthEnd < new Date(new Date(minD).setHours(0, 0, 0, 0));
|
|
477
|
+
const disMax = maxD && monthStart > new Date(new Date(maxD).setHours(23, 59, 59, 999));
|
|
478
|
+
return Boolean(disMin || disMax);
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
const isYearDisabled = (yr: number) => {
|
|
482
|
+
const minD = toDate(props.minDate);
|
|
483
|
+
const maxD = toDate(props.maxDate);
|
|
484
|
+
const yrStart = new Date(yr, 0, 1);
|
|
485
|
+
const yrEnd = new Date(yr, 11, 31, 23, 59, 59, 999);
|
|
486
|
+
const disMin = minD && yrEnd < new Date(new Date(minD).setHours(0, 0, 0, 0));
|
|
487
|
+
const disMax = maxD && yrStart > new Date(new Date(maxD).setHours(23, 59, 59, 999));
|
|
488
|
+
return Boolean(disMin || disMax);
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
const showStepperEffective = computed(
|
|
492
|
+
() => props.showStepper && props.stepperPosition !== "off"
|
|
493
|
+
);
|
|
494
|
+
const posMode = computed(() => {
|
|
495
|
+
const p = props.stepperPosition;
|
|
496
|
+
if (p === "left" || p === "start") return "left";
|
|
497
|
+
if (p === "right" || p === "end") return "right";
|
|
498
|
+
return "around";
|
|
499
|
+
});
|
|
500
|
+
</script>
|
|
501
|
+
|
|
502
|
+
<template>
|
|
503
|
+
<div
|
|
504
|
+
ref="wrapRef"
|
|
505
|
+
:style="{
|
|
506
|
+
display: fullWidth || style?.display === 'flex' ? 'flex' : 'inline-flex',
|
|
507
|
+
flexDirection: 'column',
|
|
508
|
+
position: 'relative',
|
|
509
|
+
width: style?.width || (fullWidth ? '100%' : 'auto'),
|
|
510
|
+
maxWidth: style?.maxWidth || (fullWidth ? '100%' : '320px'),
|
|
511
|
+
boxSizing: 'border-box',
|
|
512
|
+
minWidth: 0,
|
|
513
|
+
...style,
|
|
514
|
+
}"
|
|
515
|
+
>
|
|
516
|
+
<label
|
|
517
|
+
v-if="label"
|
|
518
|
+
style="
|
|
519
|
+
font-size: 11px;
|
|
520
|
+
font-weight: 600;
|
|
521
|
+
color: var(--t2, var(--muted, #797b8d));
|
|
522
|
+
text-transform: uppercase;
|
|
523
|
+
letter-spacing: .08em;
|
|
524
|
+
display: block;
|
|
525
|
+
margin-bottom: 4px;
|
|
526
|
+
user-select: none;
|
|
527
|
+
"
|
|
528
|
+
>
|
|
529
|
+
{{ label }}
|
|
530
|
+
</label>
|
|
531
|
+
|
|
532
|
+
<div
|
|
533
|
+
style="
|
|
534
|
+
position: relative;
|
|
535
|
+
display: inline-flex;
|
|
536
|
+
align-items: center;
|
|
537
|
+
gap: 4px;
|
|
538
|
+
width: 100%;
|
|
539
|
+
max-width: 100%;
|
|
540
|
+
min-width: 0;
|
|
541
|
+
"
|
|
542
|
+
>
|
|
543
|
+
<!-- Left stepper buttons -->
|
|
544
|
+
<button
|
|
545
|
+
v-if="showStepperEffective && (posMode === 'around' || posMode === 'left')"
|
|
546
|
+
type="button"
|
|
547
|
+
:disabled="disabled || isCellDisabled(new Date(new Date(valDate || new Date()).setDate((valDate || new Date()).getDate() - 1)))"
|
|
548
|
+
style="
|
|
549
|
+
display: flex;
|
|
550
|
+
align-items: center;
|
|
551
|
+
justify-content: center;
|
|
552
|
+
border-radius: var(--rads, 8px);
|
|
553
|
+
border: 1px solid var(--border, #e2e8f0);
|
|
554
|
+
background: var(--surface, #ffffff);
|
|
555
|
+
color: var(--muted, #797b8d);
|
|
556
|
+
box-shadow: var(--shadow, 0 1px 3px rgba(0,0,0,.08));
|
|
557
|
+
flex-shrink: 0;
|
|
558
|
+
cursor: pointer;
|
|
559
|
+
transition: all .15s;
|
|
560
|
+
"
|
|
561
|
+
:style="{
|
|
562
|
+
width: `${s.btnSize}px`,
|
|
563
|
+
height: `${s.btnSize}px`,
|
|
564
|
+
opacity: disabled ? 0.35 : 1,
|
|
565
|
+
}"
|
|
566
|
+
title="Previous day (-1)"
|
|
567
|
+
@click.stop="changeDay(-1)"
|
|
568
|
+
>
|
|
569
|
+
<ChevronLeft :size="s.iconSize + 2" />
|
|
570
|
+
</button>
|
|
571
|
+
|
|
572
|
+
<!-- Trigger button -->
|
|
573
|
+
<button
|
|
574
|
+
ref="btnRef"
|
|
575
|
+
type="button"
|
|
576
|
+
:disabled="disabled"
|
|
577
|
+
style="
|
|
578
|
+
display: flex;
|
|
579
|
+
align-items: center;
|
|
580
|
+
gap: 7px;
|
|
581
|
+
border-radius: var(--rads, 8px);
|
|
582
|
+
border: 1px solid var(--border, #e2e8f0);
|
|
583
|
+
background: var(--surface, #ffffff);
|
|
584
|
+
font-family: var(--sans, inherit);
|
|
585
|
+
transition: border-color .15s;
|
|
586
|
+
white-space: nowrap;
|
|
587
|
+
min-width: 0;
|
|
588
|
+
flex: 1;
|
|
589
|
+
box-shadow: var(--shadow, 0 1px 3px rgba(0,0,0,.08));
|
|
590
|
+
overflow: hidden;
|
|
591
|
+
"
|
|
592
|
+
:style="{
|
|
593
|
+
height: `${s.height}px`,
|
|
594
|
+
padding: `0 ${s.paddingH}px`,
|
|
595
|
+
fontSize: `${s.fontSize}px`,
|
|
596
|
+
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
597
|
+
opacity: disabled ? 0.5 : 1,
|
|
598
|
+
color: formatted ? 'var(--primary-text, #0f172a)' : 'var(--muted, #797b8d)',
|
|
599
|
+
borderColor: open ? 'var(--accent, #12675D)' : 'var(--border, #e2e8f0)',
|
|
600
|
+
}"
|
|
601
|
+
@click="toggleOpen"
|
|
602
|
+
>
|
|
603
|
+
<Calendar
|
|
604
|
+
:size="s.iconSize"
|
|
605
|
+
style="color: var(--muted, #797b8d); flex-shrink: 0;"
|
|
606
|
+
/>
|
|
607
|
+
<span
|
|
608
|
+
style="
|
|
609
|
+
flex: 1;
|
|
610
|
+
text-align: left;
|
|
611
|
+
overflow: hidden;
|
|
612
|
+
text-overflow: ellipsis;
|
|
613
|
+
white-space: nowrap;
|
|
614
|
+
min-width: 0;
|
|
615
|
+
"
|
|
616
|
+
>
|
|
617
|
+
{{ formatted || placeholder }}
|
|
618
|
+
</span>
|
|
619
|
+
<span
|
|
620
|
+
v-if="currentVal && clearable"
|
|
621
|
+
title="Clear"
|
|
622
|
+
style="
|
|
623
|
+
display: flex;
|
|
624
|
+
align-items: center;
|
|
625
|
+
justify-content: center;
|
|
626
|
+
color: var(--muted, #797b8d);
|
|
627
|
+
cursor: pointer;
|
|
628
|
+
flex-shrink: 0;
|
|
629
|
+
line-height: 1;
|
|
630
|
+
padding: 0 2px;
|
|
631
|
+
"
|
|
632
|
+
:style="{ fontSize: `${s.closeSize}px` }"
|
|
633
|
+
@click.stop="handleClear"
|
|
634
|
+
>
|
|
635
|
+
✕
|
|
636
|
+
</span>
|
|
637
|
+
</button>
|
|
638
|
+
|
|
639
|
+
<!-- Right stepper buttons -->
|
|
640
|
+
<button
|
|
641
|
+
v-if="showStepperEffective && (posMode === 'around' || posMode === 'right')"
|
|
642
|
+
type="button"
|
|
643
|
+
:disabled="disabled || isCellDisabled(new Date(new Date(valDate || new Date()).setDate((valDate || new Date()).getDate() + 1)))"
|
|
644
|
+
style="
|
|
645
|
+
display: flex;
|
|
646
|
+
align-items: center;
|
|
647
|
+
justify-content: center;
|
|
648
|
+
border-radius: var(--rads, 8px);
|
|
649
|
+
border: 1px solid var(--border, #e2e8f0);
|
|
650
|
+
background: var(--surface, #ffffff);
|
|
651
|
+
color: var(--muted, #797b8d);
|
|
652
|
+
box-shadow: var(--shadow, 0 1px 3px rgba(0,0,0,.08));
|
|
653
|
+
flex-shrink: 0;
|
|
654
|
+
cursor: pointer;
|
|
655
|
+
transition: all .15s;
|
|
656
|
+
"
|
|
657
|
+
:style="{
|
|
658
|
+
width: `${s.btnSize}px`,
|
|
659
|
+
height: `${s.btnSize}px`,
|
|
660
|
+
opacity: disabled ? 0.35 : 1,
|
|
661
|
+
}"
|
|
662
|
+
title="Next day (+1)"
|
|
663
|
+
@click.stop="changeDay(1)"
|
|
664
|
+
>
|
|
665
|
+
<ChevronRight :size="s.iconSize + 2" />
|
|
666
|
+
</button>
|
|
667
|
+
</div>
|
|
668
|
+
|
|
669
|
+
<!-- Dropdown via Teleport -->
|
|
670
|
+
<Teleport to="body">
|
|
671
|
+
<div
|
|
672
|
+
v-if="open"
|
|
673
|
+
ref="dropRef"
|
|
674
|
+
class="cynx-datepicker-drop"
|
|
675
|
+
:style="{
|
|
676
|
+
position: 'fixed',
|
|
677
|
+
top: `${pos.top}px`,
|
|
678
|
+
left: `${pos.left}px`,
|
|
679
|
+
width: `${pos.width}px`,
|
|
680
|
+
zIndex: 9000,
|
|
681
|
+
background: 'var(--surface, #ffffff)',
|
|
682
|
+
border: '1px solid var(--border, #e2e8f0)',
|
|
683
|
+
borderRadius: 'var(--rad, 10px)',
|
|
684
|
+
boxShadow: 'var(--shadow-lg, 0 10px 15px -3px rgba(0,0,0,.1), 0 4px 6px -4px rgba(0,0,0,.06))',
|
|
685
|
+
padding: '10px 12px',
|
|
686
|
+
animation: 'dpFadeIn .12s ease',
|
|
687
|
+
boxSizing: 'border-box',
|
|
688
|
+
}"
|
|
689
|
+
@mousedown.stop
|
|
690
|
+
>
|
|
691
|
+
<!-- Calendar Header -->
|
|
692
|
+
<div
|
|
693
|
+
style="
|
|
694
|
+
display: flex;
|
|
695
|
+
align-items: center;
|
|
696
|
+
justify-content: space-between;
|
|
697
|
+
margin-bottom: 8px;
|
|
698
|
+
"
|
|
699
|
+
>
|
|
700
|
+
<button
|
|
701
|
+
type="button"
|
|
702
|
+
:disabled="isNavPrevDisabled()"
|
|
703
|
+
style="
|
|
704
|
+
background: none;
|
|
705
|
+
border: none;
|
|
706
|
+
padding: 4px;
|
|
707
|
+
border-radius: var(--radxs, 6px);
|
|
708
|
+
color: var(--muted, #797b8d);
|
|
709
|
+
display: flex;
|
|
710
|
+
align-items: center;
|
|
711
|
+
cursor: pointer;
|
|
712
|
+
transition: background .12s;
|
|
713
|
+
"
|
|
714
|
+
:style="{
|
|
715
|
+
opacity: isNavPrevDisabled() ? 0.35 : 1,
|
|
716
|
+
cursor: isNavPrevDisabled() ? 'not-allowed' : 'pointer',
|
|
717
|
+
}"
|
|
718
|
+
@click="nav(-1)"
|
|
719
|
+
>
|
|
720
|
+
<ChevronLeft :size="15" />
|
|
721
|
+
</button>
|
|
722
|
+
|
|
723
|
+
<button
|
|
724
|
+
type="button"
|
|
725
|
+
style="
|
|
726
|
+
background: none;
|
|
727
|
+
border: none;
|
|
728
|
+
cursor: pointer;
|
|
729
|
+
font-size: .82rem;
|
|
730
|
+
font-weight: 700;
|
|
731
|
+
color: var(--primary-text, #0f172a);
|
|
732
|
+
padding: 4px 8px;
|
|
733
|
+
border-radius: var(--radxs, 6px);
|
|
734
|
+
font-family: var(--sans, inherit);
|
|
735
|
+
"
|
|
736
|
+
@click="cycleView"
|
|
737
|
+
>
|
|
738
|
+
{{ headerLabel }}
|
|
739
|
+
</button>
|
|
740
|
+
|
|
741
|
+
<button
|
|
742
|
+
type="button"
|
|
743
|
+
:disabled="isNavNextDisabled()"
|
|
744
|
+
style="
|
|
745
|
+
background: none;
|
|
746
|
+
border: none;
|
|
747
|
+
padding: 4px;
|
|
748
|
+
border-radius: var(--radxs, 6px);
|
|
749
|
+
color: var(--muted, #797b8d);
|
|
750
|
+
display: flex;
|
|
751
|
+
align-items: center;
|
|
752
|
+
cursor: pointer;
|
|
753
|
+
transition: background .12s;
|
|
754
|
+
"
|
|
755
|
+
:style="{
|
|
756
|
+
opacity: isNavNextDisabled() ? 0.35 : 1,
|
|
757
|
+
cursor: isNavNextDisabled() ? 'not-allowed' : 'pointer',
|
|
758
|
+
}"
|
|
759
|
+
@click="nav(1)"
|
|
760
|
+
>
|
|
761
|
+
<ChevronRight :size="15" />
|
|
762
|
+
</button>
|
|
763
|
+
</div>
|
|
764
|
+
|
|
765
|
+
<!-- Day View -->
|
|
766
|
+
<div v-if="view === 'day'">
|
|
767
|
+
<div
|
|
768
|
+
style="
|
|
769
|
+
display: grid;
|
|
770
|
+
grid-template-columns: repeat(7, 1fr);
|
|
771
|
+
margin-bottom: 4px;
|
|
772
|
+
"
|
|
773
|
+
>
|
|
774
|
+
<div
|
|
775
|
+
v-for="d in loc.days"
|
|
776
|
+
:key="d"
|
|
777
|
+
style="
|
|
778
|
+
text-align: center;
|
|
779
|
+
font-size: .68rem;
|
|
780
|
+
font-weight: 700;
|
|
781
|
+
color: var(--muted, #797b8d);
|
|
782
|
+
padding: 4px 0;
|
|
783
|
+
font-family: var(--mono, ui-monospace, monospace);
|
|
784
|
+
"
|
|
785
|
+
>
|
|
786
|
+
{{ d }}
|
|
787
|
+
</div>
|
|
788
|
+
</div>
|
|
789
|
+
|
|
790
|
+
<div
|
|
791
|
+
style="
|
|
792
|
+
display: grid;
|
|
793
|
+
grid-template-columns: repeat(7, 1fr);
|
|
794
|
+
gap: 1px;
|
|
795
|
+
"
|
|
796
|
+
>
|
|
797
|
+
<div
|
|
798
|
+
v-for="(c, i) in daysCells"
|
|
799
|
+
:key="i"
|
|
800
|
+
:style="{
|
|
801
|
+
position: 'relative',
|
|
802
|
+
textAlign: 'center',
|
|
803
|
+
padding: '6px 2px',
|
|
804
|
+
fontSize: c.cur ? '.8rem' : '.78rem',
|
|
805
|
+
fontWeight: isSameDay(c.date, currentVal) ? 700 : isToday(c.date.getFullYear(), c.date.getMonth(), c.date.getDate()) ? 600 : 400,
|
|
806
|
+
borderRadius: 'var(--radxs, 6px)',
|
|
807
|
+
cursor: isCellDisabled(c.date) ? 'not-allowed' : 'pointer',
|
|
808
|
+
opacity: isCellDisabled(c.date) ? 0.35 : c.cur ? 1 : 0.65,
|
|
809
|
+
background: isSameDay(c.date, currentVal) ? 'var(--accent, #12675D)' : 'transparent',
|
|
810
|
+
color: isSameDay(c.date, currentVal)
|
|
811
|
+
? 'var(--def-btn-text, #ffffff)'
|
|
812
|
+
: isCellDisabled(c.date)
|
|
813
|
+
? 'var(--border, #e2e8f0)'
|
|
814
|
+
: isToday(c.date.getFullYear(), c.date.getMonth(), c.date.getDate())
|
|
815
|
+
? 'var(--accent, #12675D)'
|
|
816
|
+
: c.cur
|
|
817
|
+
? 'var(--primary-text, #0f172a)'
|
|
818
|
+
: 'var(--muted, #797b8d)',
|
|
819
|
+
border: isToday(c.date.getFullYear(), c.date.getMonth(), c.date.getDate()) && !isSameDay(c.date, currentVal)
|
|
820
|
+
? '1.5px solid var(--accent, #12675D)'
|
|
821
|
+
: '1.5px solid transparent',
|
|
822
|
+
transition: 'background .12s, color .12s',
|
|
823
|
+
}"
|
|
824
|
+
@click="!isCellDisabled(c.date) && pick(c.date)"
|
|
825
|
+
>
|
|
826
|
+
{{ c.day }}
|
|
827
|
+
<span
|
|
828
|
+
v-if="badges[toKey(c.date.getFullYear(), c.date.getMonth(), c.day)]"
|
|
829
|
+
:style="{
|
|
830
|
+
position: 'absolute',
|
|
831
|
+
bottom: '2px',
|
|
832
|
+
left: '50%',
|
|
833
|
+
transform: 'translateX(-50%)',
|
|
834
|
+
width: '4px',
|
|
835
|
+
height: '4px',
|
|
836
|
+
borderRadius: '50%',
|
|
837
|
+
background: badges[toKey(c.date.getFullYear(), c.date.getMonth(), c.day)]?.color || 'var(--accent, #12675D)',
|
|
838
|
+
}"
|
|
839
|
+
/>
|
|
840
|
+
</div>
|
|
841
|
+
</div>
|
|
842
|
+
</div>
|
|
843
|
+
|
|
844
|
+
<!-- Month View -->
|
|
845
|
+
<div
|
|
846
|
+
v-else-if="view === 'month'"
|
|
847
|
+
style="
|
|
848
|
+
display: grid;
|
|
849
|
+
grid-template-columns: repeat(3, 1fr);
|
|
850
|
+
gap: 4px;
|
|
851
|
+
padding: 4px 0;
|
|
852
|
+
"
|
|
853
|
+
>
|
|
854
|
+
<div
|
|
855
|
+
v-for="(mName, idx) in loc.months"
|
|
856
|
+
:key="idx"
|
|
857
|
+
:style="{
|
|
858
|
+
textAlign: 'center',
|
|
859
|
+
padding: '10px 4px',
|
|
860
|
+
borderRadius: 'var(--radxs, 6px)',
|
|
861
|
+
fontSize: '.82rem',
|
|
862
|
+
fontWeight: 600,
|
|
863
|
+
cursor: isMonthDisabled(idx) ? 'not-allowed' : 'pointer',
|
|
864
|
+
opacity: isMonthDisabled(idx) ? 0.35 : 1,
|
|
865
|
+
background: valDate && valDate.getFullYear() === cursor.getFullYear() && valDate.getMonth() === idx
|
|
866
|
+
? 'var(--accent, #12675D)'
|
|
867
|
+
: 'transparent',
|
|
868
|
+
color: valDate && valDate.getFullYear() === cursor.getFullYear() && valDate.getMonth() === idx
|
|
869
|
+
? 'var(--def-btn-text, #ffffff)'
|
|
870
|
+
: isMonthDisabled(idx)
|
|
871
|
+
? 'var(--border, #e2e8f0)'
|
|
872
|
+
: 'var(--primary-text, #0f172a)',
|
|
873
|
+
transition: 'background .12s',
|
|
874
|
+
}"
|
|
875
|
+
@click="
|
|
876
|
+
() => {
|
|
877
|
+
if (isMonthDisabled(idx)) return;
|
|
878
|
+
const c = new Date(cursor);
|
|
879
|
+
c.setMonth(idx);
|
|
880
|
+
cursor = c;
|
|
881
|
+
view = 'day';
|
|
882
|
+
}
|
|
883
|
+
"
|
|
884
|
+
>
|
|
885
|
+
{{ mName }}
|
|
886
|
+
</div>
|
|
887
|
+
</div>
|
|
888
|
+
|
|
889
|
+
<!-- Year View -->
|
|
890
|
+
<div
|
|
891
|
+
v-else-if="view === 'year'"
|
|
892
|
+
style="
|
|
893
|
+
display: grid;
|
|
894
|
+
grid-template-columns: repeat(3, 1fr);
|
|
895
|
+
gap: 4px;
|
|
896
|
+
padding: 4px 0;
|
|
897
|
+
"
|
|
898
|
+
>
|
|
899
|
+
<div
|
|
900
|
+
v-for="yr in yearsList"
|
|
901
|
+
:key="yr"
|
|
902
|
+
:style="{
|
|
903
|
+
textAlign: 'center',
|
|
904
|
+
padding: '10px 4px',
|
|
905
|
+
borderRadius: 'var(--radxs, 6px)',
|
|
906
|
+
fontSize: '.82rem',
|
|
907
|
+
fontWeight: 600,
|
|
908
|
+
cursor: isYearDisabled(yr) ? 'not-allowed' : 'pointer',
|
|
909
|
+
opacity: isYearDisabled(yr) ? 0.35 : 1,
|
|
910
|
+
background: valDate && valDate.getFullYear() === yr ? 'var(--accent, #12675D)' : 'transparent',
|
|
911
|
+
color: valDate && valDate.getFullYear() === yr
|
|
912
|
+
? 'var(--def-btn-text, #ffffff)'
|
|
913
|
+
: isYearDisabled(yr)
|
|
914
|
+
? 'var(--border, #e2e8f0)'
|
|
915
|
+
: 'var(--primary-text, #0f172a)',
|
|
916
|
+
transition: 'background .12s',
|
|
917
|
+
}"
|
|
918
|
+
@click="
|
|
919
|
+
() => {
|
|
920
|
+
if (isYearDisabled(yr)) return;
|
|
921
|
+
const c = new Date(cursor);
|
|
922
|
+
c.setFullYear(yr);
|
|
923
|
+
cursor = c;
|
|
924
|
+
view = 'month';
|
|
925
|
+
}
|
|
926
|
+
"
|
|
927
|
+
>
|
|
928
|
+
{{ yr }}
|
|
929
|
+
</div>
|
|
930
|
+
</div>
|
|
931
|
+
|
|
932
|
+
<!-- Time Picker Footer (Optional) -->
|
|
933
|
+
<div
|
|
934
|
+
v-if="showTime"
|
|
935
|
+
style="
|
|
936
|
+
border-top: 1px solid var(--border-light, #ebebf0);
|
|
937
|
+
margin-top: 8px;
|
|
938
|
+
padding-top: 8px;
|
|
939
|
+
display: flex;
|
|
940
|
+
align-items: center;
|
|
941
|
+
justify-content: space-between;
|
|
942
|
+
gap: 8px;
|
|
943
|
+
"
|
|
944
|
+
>
|
|
945
|
+
<div
|
|
946
|
+
style="
|
|
947
|
+
display: inline-flex;
|
|
948
|
+
align-items: center;
|
|
949
|
+
gap: 5px;
|
|
950
|
+
font-size: .76rem;
|
|
951
|
+
color: var(--muted, #797b8d);
|
|
952
|
+
fontWeight: 600;
|
|
953
|
+
"
|
|
954
|
+
>
|
|
955
|
+
<Clock :size="13" style="opacity: 0.85;" />
|
|
956
|
+
<span>{{ loc.time || 'Time:' }}</span>
|
|
957
|
+
</div>
|
|
958
|
+
<button
|
|
959
|
+
type="button"
|
|
960
|
+
style="
|
|
961
|
+
padding: 4px 10px;
|
|
962
|
+
border-radius: var(--radxs, 6px);
|
|
963
|
+
background: var(--accent, #12675D);
|
|
964
|
+
color: var(--def-btn-text, #ffffff);
|
|
965
|
+
border: none;
|
|
966
|
+
font-size: 11.5px;
|
|
967
|
+
font-weight: 600;
|
|
968
|
+
cursor: pointer;
|
|
969
|
+
"
|
|
970
|
+
@click="open = false; view = 'day';"
|
|
971
|
+
>
|
|
972
|
+
{{ loc.ok || 'OK' }}
|
|
973
|
+
</button>
|
|
974
|
+
</div>
|
|
975
|
+
</div>
|
|
976
|
+
</Teleport>
|
|
977
|
+
</div>
|
|
978
|
+
</template>
|
package/vue/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import Skeleton from "./components/Skeleton.vue";
|
|
|
13
13
|
import Modal from "./components/Modal.vue";
|
|
14
14
|
import Drawer from "./components/Drawer.vue";
|
|
15
15
|
import Toggle from "./components/Toggle.vue";
|
|
16
|
+
import DatePicker from "./components/DatePicker.vue";
|
|
16
17
|
import { XTable, XThead, XTbody, XTr, XTh, XTd } from "./components/XTable.vue";
|
|
17
18
|
import { useAutoSkeletonRows } from "./composables/useAutoSkeletonRows";
|
|
18
19
|
|
|
@@ -31,6 +32,7 @@ export {
|
|
|
31
32
|
Modal,
|
|
32
33
|
Drawer,
|
|
33
34
|
Toggle,
|
|
35
|
+
DatePicker,
|
|
34
36
|
XTable,
|
|
35
37
|
XThead,
|
|
36
38
|
XTbody,
|
|
@@ -55,6 +57,7 @@ export default {
|
|
|
55
57
|
Modal,
|
|
56
58
|
Drawer,
|
|
57
59
|
Toggle,
|
|
60
|
+
DatePicker,
|
|
58
61
|
XTable,
|
|
59
62
|
XThead,
|
|
60
63
|
XTbody,
|