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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cynx-lib",
3
- "version": "1.3.67",
3
+ "version": "1.3.68",
4
4
  "type": "module",
5
5
  "main": "./index.cjs",
6
6
  "module": "./index.js",
@@ -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
- const DAYS = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'];
17
- const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
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
- export function DatePicker({ value, onChange, label, placeholder = 'Select date', badges = {}, minDate, maxDate, disabled, showStepper = true, stepperPosition = 'around', style }) {
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
- return () => document.removeEventListener('mousedown', close, true);
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
- setOpen(false);
157
- setView('day');
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: 32, height: 32, borderRadius: 'var(--rads, 8px)',
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={15} />
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: 32, height: 32, borderRadius: 'var(--rads, 8px)',
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={15} />
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: '5px 10px', borderRadius: 'var(--rads, 8px)',
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: '.8rem', color: formatted ? 'var(--primary-text, #0f172a)' : 'var(--muted, #797b8d)',
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: 140, height: 32, boxShadow: 'var(--shadow, 0 1px 3px rgba(0,0,0,.08), 0 1px 2px rgba(0,0,0,.06))',
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={13} style={{ color: 'var(--muted, #797b8d)', flexShrink: 0 }} />
505
- <span style={{ flex: 1, textAlign: 'left' }}>{formatted || placeholder}</span>
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: 13, lineHeight: 1, padding: '0 2px' }}
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={{ display: 'inline-flex', flexDirection: 'column', position: 'relative', width: style?.width || 'auto', boxSizing: 'border-box', minWidth: 0, ...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={{ position: 'relative', display: 'inline-flex', alignItems: 'center', gap: 4, width: '100%' }}>
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
  )}
@@ -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,