cynx-lib 1.3.68 → 1.3.70

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.
@@ -1,978 +1,1136 @@
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>
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
+ range?: boolean;
165
+ }>(),
166
+ {
167
+ modelValue: undefined,
168
+ value: undefined,
169
+ label: "",
170
+ placeholder: "Select date",
171
+ badges: () => ({}),
172
+ minDate: undefined,
173
+ maxDate: undefined,
174
+ disabled: false,
175
+ size: "md",
176
+ showStepper: false,
177
+ stepperPosition: "off",
178
+ locale: "en",
179
+ clearable: true,
180
+ fullWidth: false,
181
+ style: () => ({}),
182
+ showTime: false,
183
+ timeValue: "",
184
+ timeFormat: "24h",
185
+ showSeconds: false,
186
+ timePlaceholder: "00:00",
187
+ range: false,
188
+ }
189
+ );
190
+
191
+ const emit = defineEmits<{
192
+ (e: "update:modelValue", value: any): void;
193
+ (e: "update:value", value: any): void;
194
+ (e: "change", value: any): void;
195
+ (e: "update:timeValue", value: string): void;
196
+ (e: "time-change", value: string): void;
197
+ (e: "clear"): void;
198
+ }>();
199
+
200
+ const currentVal = computed(() => {
201
+ return props.modelValue !== undefined ? props.modelValue : props.value;
202
+ });
203
+
204
+ const loc = computed<DatePickerLocale>(() => {
205
+ if (typeof props.locale === "object" && props.locale !== null) {
206
+ return props.locale;
207
+ }
208
+ return LOCALES[props.locale as string] || LOCALES.en;
209
+ });
210
+
211
+ const s = computed(() => SIZES[props.size] || SIZES.md);
212
+ const open = ref(false);
213
+ const view = ref<"day" | "month" | "year">("day");
214
+ const cursor = ref<Date>(new Date());
215
+ const pos = ref({ top: 0, left: 0, width: 0 });
216
+
217
+ const rangeStart = ref<Date | null>(null);
218
+ const rangeEnd = ref<Date | null>(null);
219
+ const hoverDate = ref<Date | null>(null);
220
+
221
+ function formatDateStr(d: Date | null): string {
222
+ if (!d) return "";
223
+ const y = d.getFullYear();
224
+ const m = pad(d.getMonth() + 1);
225
+ const day = pad(d.getDate());
226
+ return `${y}-${m}-${day}`;
227
+ }
228
+
229
+ const wrapRef = ref<HTMLElement | null>(null);
230
+ const btnRef = ref<HTMLElement | null>(null);
231
+ const dropRef = ref<HTMLElement | null>(null);
232
+
233
+ watch(
234
+ currentVal,
235
+ (newVal) => {
236
+ if (props.range) {
237
+ if (Array.isArray(newVal)) {
238
+ rangeStart.value = toDate(newVal[0]);
239
+ rangeEnd.value = toDate(newVal[1]);
240
+ if (rangeStart.value) cursor.value = rangeStart.value;
241
+ } else if (typeof newVal === "string" && newVal.includes(",")) {
242
+ const parts = newVal.split(",");
243
+ rangeStart.value = toDate(parts[0]);
244
+ rangeEnd.value = toDate(parts[1]);
245
+ if (rangeStart.value) cursor.value = rangeStart.value;
246
+ } else {
247
+ rangeStart.value = null;
248
+ rangeEnd.value = null;
249
+ }
250
+ } else {
251
+ const d = toDate(newVal);
252
+ if (d) cursor.value = d;
253
+ }
254
+ },
255
+ { immediate: true }
256
+ );
257
+
258
+ const calcPos = () => {
259
+ if (!btnRef.value) return;
260
+ const r = btnRef.value.getBoundingClientRect();
261
+ const dropW = Math.max(r.width, 260);
262
+ const dropH = dropRef.value ? dropRef.value.offsetHeight : 280;
263
+
264
+ let left = r.left;
265
+ if (left + dropW > window.innerWidth - 12) {
266
+ left = Math.max(12, r.right - dropW);
267
+ }
268
+ if (left < 12) left = 12;
269
+
270
+ let top = r.bottom + 6;
271
+ if (top + dropH > window.innerHeight - 12 && r.top - 6 - dropH > 12) {
272
+ top = r.top - 6 - dropH;
273
+ }
274
+
275
+ pos.value = { top, left, width: dropW };
276
+ };
277
+
278
+ const handleWindowClick = (e: MouseEvent) => {
279
+ if (!open.value) return;
280
+ const target = e.target as Node;
281
+ if (wrapRef.value?.contains(target)) return;
282
+ if (dropRef.value?.contains(target)) return;
283
+ open.value = false;
284
+ view.value = "day";
285
+ };
286
+
287
+ const handleKeyDown = (e: KeyboardEvent) => {
288
+ if (e.key === "Escape" && open.value) {
289
+ open.value = false;
290
+ view.value = "day";
291
+ }
292
+ };
293
+
294
+ watch(open, (isOpen) => {
295
+ if (isOpen) {
296
+ calcPos();
297
+ window.addEventListener("scroll", calcPos, true);
298
+ window.addEventListener("resize", calcPos);
299
+ document.addEventListener("mousedown", handleWindowClick, true);
300
+ document.addEventListener("keydown", handleKeyDown);
301
+ nextTick(calcPos);
302
+ } else {
303
+ window.removeEventListener("scroll", calcPos, true);
304
+ window.removeEventListener("resize", calcPos);
305
+ document.removeEventListener("mousedown", handleWindowClick, true);
306
+ document.removeEventListener("keydown", handleKeyDown);
307
+ }
308
+ });
309
+
310
+ onBeforeUnmount(() => {
311
+ window.removeEventListener("scroll", calcPos, true);
312
+ window.removeEventListener("resize", calcPos);
313
+ document.removeEventListener("mousedown", handleWindowClick, true);
314
+ document.removeEventListener("keydown", handleKeyDown);
315
+ });
316
+
317
+ const toggleOpen = () => {
318
+ if (props.disabled) return;
319
+ if (!open.value) calcPos();
320
+ open.value = !open.value;
321
+ };
322
+
323
+ function pick(date: Date) {
324
+ if (!date) return;
325
+ const minD = toDate(props.minDate);
326
+ const maxD = toDate(props.maxDate);
327
+ if (minD && date < new Date(new Date(minD).setHours(0, 0, 0, 0))) return;
328
+ if (maxD && date > new Date(new Date(maxD).setHours(23, 59, 59, 999))) return;
329
+
330
+ if (props.range) {
331
+ if (!rangeStart.value || (rangeStart.value && rangeEnd.value)) {
332
+ rangeStart.value = date;
333
+ rangeEnd.value = null;
334
+ } else {
335
+ let start = rangeStart.value;
336
+ let end = date;
337
+ if (start > end) {
338
+ const tmp = start;
339
+ start = end;
340
+ end = tmp;
341
+ }
342
+ rangeStart.value = start;
343
+ rangeEnd.value = end;
344
+
345
+ const result = [formatDateStr(start), formatDateStr(end)];
346
+ emit("update:modelValue", result);
347
+ emit("update:value", result);
348
+ emit("change", result);
349
+
350
+ open.value = false;
351
+ view.value = "day";
352
+ }
353
+ return;
354
+ }
355
+
356
+ const y = date.getFullYear();
357
+ const m = pad(date.getMonth() + 1);
358
+ const d = pad(date.getDate());
359
+ const strVal = `${y}-${m}-${d}`;
360
+
361
+ if (typeof currentVal.value === "string" || currentVal.value === undefined) {
362
+ emit("update:modelValue", strVal);
363
+ emit("update:value", strVal);
364
+ emit("change", strVal);
365
+ } else {
366
+ emit("update:modelValue", date);
367
+ emit("update:value", date);
368
+ emit("change", date);
369
+ }
370
+
371
+ if (!props.showTime) {
372
+ open.value = false;
373
+ view.value = "day";
374
+ }
375
+ }
376
+
377
+ function handleClear(e: MouseEvent) {
378
+ e.stopPropagation();
379
+ if (props.range) {
380
+ rangeStart.value = null;
381
+ rangeEnd.value = null;
382
+ }
383
+ emit("update:modelValue", null);
384
+ emit("update:value", null);
385
+ emit("change", null);
386
+ emit("clear");
387
+ }
388
+
389
+ const changeDay = (delta: number) => {
390
+ if (props.disabled) return;
391
+ const current = toDate(currentVal.value) || new Date();
392
+ const next = new Date(
393
+ current.getFullYear(),
394
+ current.getMonth(),
395
+ current.getDate() + delta
396
+ );
397
+
398
+ const minD = toDate(props.minDate);
399
+ const maxD = toDate(props.maxDate);
400
+ if (minD && next < new Date(new Date(minD).setHours(0, 0, 0, 0))) return;
401
+ if (maxD && next > new Date(new Date(maxD).setHours(23, 59, 59, 999))) return;
402
+
403
+ pick(next);
404
+ };
405
+
406
+ const isNavNextDisabled = () => {
407
+ const maxD = toDate(props.maxDate);
408
+ if (!maxD) return false;
409
+ const y = cursor.value.getFullYear();
410
+ const m = cursor.value.getMonth();
411
+ if (view.value === "day") {
412
+ const nextMonthStart = new Date(y, m + 1, 1);
413
+ return nextMonthStart > new Date(new Date(maxD).setHours(23, 59, 59, 999));
414
+ }
415
+ if (view.value === "month") {
416
+ const nextYearStart = new Date(y + 1, 0, 1);
417
+ return nextYearStart > new Date(new Date(maxD).setHours(23, 59, 59, 999));
418
+ }
419
+ if (view.value === "year") {
420
+ const base = Math.floor(y / 12) * 12;
421
+ const nextBlockStart = new Date(base + 12, 0, 1);
422
+ return nextBlockStart > new Date(new Date(maxD).setHours(23, 59, 59, 999));
423
+ }
424
+ return false;
425
+ };
426
+
427
+ const isNavPrevDisabled = () => {
428
+ const minD = toDate(props.minDate);
429
+ if (!minD) return false;
430
+ const y = cursor.value.getFullYear();
431
+ const m = cursor.value.getMonth();
432
+ if (view.value === "day") {
433
+ const prevMonthEnd = new Date(y, m, 0, 23, 59, 59, 999);
434
+ return prevMonthEnd < new Date(new Date(minD).setHours(0, 0, 0, 0));
435
+ }
436
+ if (view.value === "month") {
437
+ const prevYearEnd = new Date(y - 1, 11, 31, 23, 59, 59, 999);
438
+ return prevYearEnd < new Date(new Date(minD).setHours(0, 0, 0, 0));
439
+ }
440
+ if (view.value === "year") {
441
+ const base = Math.floor(y / 12) * 12;
442
+ const prevBlockEnd = new Date(base - 1, 11, 31, 23, 59, 59, 999);
443
+ return prevBlockEnd < new Date(new Date(minD).setHours(0, 0, 0, 0));
444
+ }
445
+ return false;
446
+ };
447
+
448
+ function nav(dir: number) {
449
+ if (dir > 0 && isNavNextDisabled()) return;
450
+ if (dir < 0 && isNavPrevDisabled()) return;
451
+ const c = new Date(cursor.value);
452
+ if (view.value === "day") c.setMonth(c.getMonth() + dir);
453
+ if (view.value === "month") c.setFullYear(c.getFullYear() + dir);
454
+ if (view.value === "year") c.setFullYear(c.getFullYear() + dir * 12);
455
+ cursor.value = c;
456
+ }
457
+
458
+ function cycleView() {
459
+ view.value = view.value === "day" ? "month" : "year";
460
+ }
461
+
462
+ const valDate = computed(() => toDate(currentVal.value));
463
+
464
+ function isRangeStartActive(date: Date) {
465
+ if (!props.range || !rangeStart.value) return false;
466
+ if (rangeEnd.value) {
467
+ const sTime = new Date(rangeStart.value.getFullYear(), rangeStart.value.getMonth(), rangeStart.value.getDate()).getTime();
468
+ const eTime = new Date(rangeEnd.value.getFullYear(), rangeEnd.value.getMonth(), rangeEnd.value.getDate()).getTime();
469
+ const minD = sTime <= eTime ? rangeStart.value : rangeEnd.value;
470
+ return isSameDay(date, minD);
471
+ }
472
+ if (hoverDate.value) {
473
+ const sTime = new Date(rangeStart.value.getFullYear(), rangeStart.value.getMonth(), rangeStart.value.getDate()).getTime();
474
+ const hTime = new Date(hoverDate.value.getFullYear(), hoverDate.value.getMonth(), hoverDate.value.getDate()).getTime();
475
+ const minD = sTime <= hTime ? rangeStart.value : hoverDate.value;
476
+ return isSameDay(date, minD);
477
+ }
478
+ return isSameDay(date, rangeStart.value);
479
+ }
480
+
481
+ function isRangeEndActive(date: Date) {
482
+ if (!props.range || !rangeStart.value) return false;
483
+ if (rangeEnd.value) {
484
+ const sTime = new Date(rangeStart.value.getFullYear(), rangeStart.value.getMonth(), rangeStart.value.getDate()).getTime();
485
+ const eTime = new Date(rangeEnd.value.getFullYear(), rangeEnd.value.getMonth(), rangeEnd.value.getDate()).getTime();
486
+ const maxD = sTime <= eTime ? rangeEnd.value : rangeStart.value;
487
+ return isSameDay(date, maxD);
488
+ }
489
+ if (hoverDate.value) {
490
+ const sTime = new Date(rangeStart.value.getFullYear(), rangeStart.value.getMonth(), rangeStart.value.getDate()).getTime();
491
+ const hTime = new Date(hoverDate.value.getFullYear(), hoverDate.value.getMonth(), hoverDate.value.getDate()).getTime();
492
+ const maxD = sTime <= hTime ? hoverDate.value : rangeStart.value;
493
+ return isSameDay(date, maxD);
494
+ }
495
+ return false;
496
+ }
497
+
498
+ function isRangeEndpoint(date: Date) {
499
+ return isRangeStartActive(date) || isRangeEndActive(date);
500
+ }
501
+
502
+ function isRangeBetween(date: Date) {
503
+ if (!props.range || !rangeStart.value) return false;
504
+ const dTime = new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
505
+ if (rangeEnd.value) {
506
+ const sTime = new Date(rangeStart.value.getFullYear(), rangeStart.value.getMonth(), rangeStart.value.getDate()).getTime();
507
+ const eTime = new Date(rangeEnd.value.getFullYear(), rangeEnd.value.getMonth(), rangeEnd.value.getDate()).getTime();
508
+ const minT = Math.min(sTime, eTime);
509
+ const maxT = Math.max(sTime, eTime);
510
+ return dTime > minT && dTime < maxT;
511
+ }
512
+ if (hoverDate.value) {
513
+ const sTime = new Date(rangeStart.value.getFullYear(), rangeStart.value.getMonth(), rangeStart.value.getDate()).getTime();
514
+ const hTime = new Date(hoverDate.value.getFullYear(), hoverDate.value.getMonth(), hoverDate.value.getDate()).getTime();
515
+ const minT = Math.min(sTime, hTime);
516
+ const maxT = Math.max(sTime, hTime);
517
+ return dTime > minT && dTime < maxT;
518
+ }
519
+ return false;
520
+ }
521
+
522
+ function getRangeRadius(date: Date) {
523
+ if (!props.range) return 'var(--radxs, 6px)';
524
+ const isStart = isRangeStartActive(date);
525
+ const isEnd = isRangeEndActive(date);
526
+ const dayOfWeek = date.getDay();
527
+ const isRowStart = dayOfWeek === 1;
528
+ const isRowEnd = dayOfWeek === 0;
529
+
530
+ if (isStart && isEnd) {
531
+ return 'var(--radxs, 6px)';
532
+ }
533
+ if (isStart) {
534
+ return 'var(--radxs, 6px) 0 0 var(--radxs, 6px)';
535
+ }
536
+ if (isEnd) {
537
+ return '0 var(--radxs, 6px) var(--radxs, 6px) 0';
538
+ }
539
+ if (isRangeBetween(date)) {
540
+ if (isRowStart && isRowEnd) return 'var(--radxs, 6px)';
541
+ if (isRowStart) return 'var(--radxs, 6px) 0 0 var(--radxs, 6px)';
542
+ if (isRowEnd) return '0 var(--radxs, 6px) var(--radxs, 6px) 0';
543
+ return '0';
544
+ }
545
+ return 'var(--radxs, 6px)';
546
+ }
547
+
548
+ const formatted = computed(() => {
549
+ if (props.range) {
550
+ if (rangeStart.value && rangeEnd.value) {
551
+ return `${formatDateStr(rangeStart.value)} — ${formatDateStr(rangeEnd.value)}`;
552
+ }
553
+ if (rangeStart.value) {
554
+ return `${formatDateStr(rangeStart.value)} — ...`;
555
+ }
556
+ return null;
557
+ }
558
+ const vd = valDate.value;
559
+ const formattedTime =
560
+ props.showTime && props.timeValue ? ` ${props.timeValue}` : "";
561
+ if (vd) {
562
+ const mName = loc.value.months[vd.getMonth()] || "";
563
+ return `${mName} ${vd.getDate()}, ${vd.getFullYear()}${formattedTime}`;
564
+ }
565
+ if (typeof currentVal.value === "string" && currentVal.value) {
566
+ return `${currentVal.value}${formattedTime}`;
567
+ }
568
+ return null;
569
+ });
570
+
571
+ const headerLabel = computed(() => {
572
+ const y = cursor.value.getFullYear();
573
+ const m = cursor.value.getMonth();
574
+ if (view.value === "day") {
575
+ return `${loc.value.months[m]} ${y}`;
576
+ }
577
+ if (view.value === "month") {
578
+ return `${y}`;
579
+ }
580
+ const base = Math.floor(y / 12) * 12;
581
+ return `${base} – ${base + 11}`;
582
+ });
583
+
584
+ const daysCells = computed(() => {
585
+ const y = cursor.value.getFullYear();
586
+ const m = cursor.value.getMonth();
587
+ const first = new Date(y, m, 1).getDay(); // 0=Sun
588
+ const offset = first === 0 ? 6 : first === 1 ? 7 : first - 1;
589
+ const daysInMonth = new Date(y, m + 1, 0).getDate();
590
+ const prevDays = new Date(y, m, 0).getDate();
591
+
592
+ const cells: Array<{ day: number; cur: boolean; next: boolean; date: Date }> =
593
+ [];
594
+ for (let i = offset - 1; i >= 0; i--) {
595
+ const d = prevDays - i;
596
+ cells.push({ day: d, cur: false, next: false, date: new Date(y, m - 1, d) });
597
+ }
598
+ for (let d = 1; d <= daysInMonth; d++) {
599
+ cells.push({ day: d, cur: true, next: false, date: new Date(y, m, d) });
600
+ }
601
+ const totalSoFar = cells.length;
602
+ let remaining = (7 - (totalSoFar % 7)) % 7;
603
+ if (remaining === 0) remaining = 7;
604
+ for (let d = 1; d <= remaining; d++) {
605
+ cells.push({ day: d, cur: false, next: true, date: new Date(y, m + 1, d) });
606
+ }
607
+ return cells;
608
+ });
609
+
610
+ const yearsList = computed(() => {
611
+ const base = Math.floor(cursor.value.getFullYear() / 12) * 12;
612
+ return Array.from({ length: 12 }, (_, i) => base + i);
613
+ });
614
+
615
+ const isCellDisabled = (date: Date) => {
616
+ const minD = toDate(props.minDate);
617
+ const maxD = toDate(props.maxDate);
618
+ const disMin = minD && date < new Date(new Date(minD).setHours(0, 0, 0, 0));
619
+ const disMax = maxD && date > new Date(new Date(maxD).setHours(23, 59, 59, 999));
620
+ return Boolean(disMin || disMax);
621
+ };
622
+
623
+ const isMonthDisabled = (monthIndex: number) => {
624
+ const y = cursor.value.getFullYear();
625
+ const minD = toDate(props.minDate);
626
+ const maxD = toDate(props.maxDate);
627
+ const monthStart = new Date(y, monthIndex, 1);
628
+ const monthEnd = new Date(y, monthIndex + 1, 0, 23, 59, 59, 999);
629
+ const disMin = minD && monthEnd < new Date(new Date(minD).setHours(0, 0, 0, 0));
630
+ const disMax = maxD && monthStart > new Date(new Date(maxD).setHours(23, 59, 59, 999));
631
+ return Boolean(disMin || disMax);
632
+ };
633
+
634
+ const isYearDisabled = (yr: number) => {
635
+ const minD = toDate(props.minDate);
636
+ const maxD = toDate(props.maxDate);
637
+ const yrStart = new Date(yr, 0, 1);
638
+ const yrEnd = new Date(yr, 11, 31, 23, 59, 59, 999);
639
+ const disMin = minD && yrEnd < new Date(new Date(minD).setHours(0, 0, 0, 0));
640
+ const disMax = maxD && yrStart > new Date(new Date(maxD).setHours(23, 59, 59, 999));
641
+ return Boolean(disMin || disMax);
642
+ };
643
+
644
+ const showStepperEffective = computed(
645
+ () => props.showStepper && props.stepperPosition !== "off"
646
+ );
647
+ const posMode = computed(() => {
648
+ const p = props.stepperPosition;
649
+ if (p === "left" || p === "start") return "left";
650
+ if (p === "right" || p === "end") return "right";
651
+ return "around";
652
+ });
653
+ </script>
654
+
655
+ <template>
656
+ <div
657
+ ref="wrapRef"
658
+ :style="{
659
+ display: fullWidth || style?.display === 'flex' ? 'flex' : 'inline-flex',
660
+ flexDirection: 'column',
661
+ position: 'relative',
662
+ width: style?.width || (fullWidth ? '100%' : 'auto'),
663
+ maxWidth: style?.maxWidth || (fullWidth ? '100%' : '320px'),
664
+ boxSizing: 'border-box',
665
+ minWidth: 0,
666
+ ...style,
667
+ }"
668
+ >
669
+ <label
670
+ v-if="label"
671
+ style="
672
+ font-size: 11px;
673
+ font-weight: 600;
674
+ color: var(--t2, var(--muted, #797b8d));
675
+ text-transform: uppercase;
676
+ letter-spacing: .08em;
677
+ display: block;
678
+ margin-bottom: 4px;
679
+ user-select: none;
680
+ "
681
+ >
682
+ {{ label }}
683
+ </label>
684
+
685
+ <div
686
+ style="
687
+ position: relative;
688
+ display: inline-flex;
689
+ align-items: center;
690
+ gap: 4px;
691
+ width: 100%;
692
+ max-width: 100%;
693
+ min-width: 0;
694
+ "
695
+ >
696
+ <!-- Left stepper buttons -->
697
+ <button
698
+ v-if="showStepperEffective && (posMode === 'around' || posMode === 'left')"
699
+ type="button"
700
+ :disabled="disabled || isCellDisabled(new Date(new Date(valDate || new Date()).setDate((valDate || new Date()).getDate() - 1)))"
701
+ style="
702
+ display: flex;
703
+ align-items: center;
704
+ justify-content: center;
705
+ border-radius: var(--rads, 8px);
706
+ border: 1px solid var(--border, #e2e8f0);
707
+ background: var(--surface, #ffffff);
708
+ color: var(--muted, #797b8d);
709
+ box-shadow: var(--shadow, 0 1px 3px rgba(0,0,0,.08));
710
+ flex-shrink: 0;
711
+ cursor: pointer;
712
+ transition: all .15s;
713
+ "
714
+ :style="{
715
+ width: `${s.btnSize}px`,
716
+ height: `${s.btnSize}px`,
717
+ opacity: disabled ? 0.35 : 1,
718
+ }"
719
+ title="Previous day (-1)"
720
+ @click.stop="changeDay(-1)"
721
+ >
722
+ <ChevronLeft :size="s.iconSize + 2" />
723
+ </button>
724
+
725
+ <!-- Trigger button -->
726
+ <button
727
+ ref="btnRef"
728
+ type="button"
729
+ :disabled="disabled"
730
+ style="
731
+ display: flex;
732
+ align-items: center;
733
+ gap: 7px;
734
+ border-radius: var(--rads, 8px);
735
+ border: 1px solid var(--border, #e2e8f0);
736
+ background: var(--surface, #ffffff);
737
+ font-family: var(--sans, inherit);
738
+ transition: border-color .15s;
739
+ white-space: nowrap;
740
+ min-width: 0;
741
+ flex: 1;
742
+ box-shadow: var(--shadow, 0 1px 3px rgba(0,0,0,.08));
743
+ overflow: hidden;
744
+ "
745
+ :style="{
746
+ height: `${s.height}px`,
747
+ padding: `0 ${s.paddingH}px`,
748
+ fontSize: `${s.fontSize}px`,
749
+ cursor: disabled ? 'not-allowed' : 'pointer',
750
+ opacity: disabled ? 0.5 : 1,
751
+ color: formatted ? 'var(--primary-text, #0f172a)' : 'var(--muted, #797b8d)',
752
+ borderColor: open ? 'var(--accent, #12675D)' : 'var(--border, #e2e8f0)',
753
+ }"
754
+ @click="toggleOpen"
755
+ >
756
+ <Calendar
757
+ :size="s.iconSize"
758
+ style="color: var(--muted, #797b8d); flex-shrink: 0;"
759
+ />
760
+ <span
761
+ style="
762
+ flex: 1;
763
+ text-align: left;
764
+ overflow: hidden;
765
+ text-overflow: ellipsis;
766
+ white-space: nowrap;
767
+ min-width: 0;
768
+ "
769
+ >
770
+ {{ formatted || placeholder }}
771
+ </span>
772
+ <span
773
+ v-if="currentVal && clearable"
774
+ title="Clear"
775
+ style="
776
+ display: flex;
777
+ align-items: center;
778
+ justify-content: center;
779
+ color: var(--muted, #797b8d);
780
+ cursor: pointer;
781
+ flex-shrink: 0;
782
+ line-height: 1;
783
+ padding: 0 2px;
784
+ "
785
+ :style="{ fontSize: `${s.closeSize}px` }"
786
+ @click.stop="handleClear"
787
+ >
788
+ ✕
789
+ </span>
790
+ </button>
791
+
792
+ <!-- Right stepper buttons -->
793
+ <button
794
+ v-if="showStepperEffective && (posMode === 'around' || posMode === 'right')"
795
+ type="button"
796
+ :disabled="disabled || isCellDisabled(new Date(new Date(valDate || new Date()).setDate((valDate || new Date()).getDate() + 1)))"
797
+ style="
798
+ display: flex;
799
+ align-items: center;
800
+ justify-content: center;
801
+ border-radius: var(--rads, 8px);
802
+ border: 1px solid var(--border, #e2e8f0);
803
+ background: var(--surface, #ffffff);
804
+ color: var(--muted, #797b8d);
805
+ box-shadow: var(--shadow, 0 1px 3px rgba(0,0,0,.08));
806
+ flex-shrink: 0;
807
+ cursor: pointer;
808
+ transition: all .15s;
809
+ "
810
+ :style="{
811
+ width: `${s.btnSize}px`,
812
+ height: `${s.btnSize}px`,
813
+ opacity: disabled ? 0.35 : 1,
814
+ }"
815
+ title="Next day (+1)"
816
+ @click.stop="changeDay(1)"
817
+ >
818
+ <ChevronRight :size="s.iconSize + 2" />
819
+ </button>
820
+ </div>
821
+
822
+ <!-- Dropdown via Teleport -->
823
+ <Teleport to="body">
824
+ <div
825
+ v-if="open"
826
+ ref="dropRef"
827
+ class="cynx-datepicker-drop"
828
+ :style="{
829
+ position: 'fixed',
830
+ top: `${pos.top}px`,
831
+ left: `${pos.left}px`,
832
+ width: `${pos.width}px`,
833
+ zIndex: 9000,
834
+ background: 'var(--surface, #ffffff)',
835
+ border: '1px solid var(--border, #e2e8f0)',
836
+ borderRadius: 'var(--rad, 10px)',
837
+ boxShadow: 'var(--shadow-lg, 0 10px 15px -3px rgba(0,0,0,.1), 0 4px 6px -4px rgba(0,0,0,.06))',
838
+ padding: '10px 12px',
839
+ animation: 'dpFadeIn .12s ease',
840
+ boxSizing: 'border-box',
841
+ }"
842
+ @mousedown.stop
843
+ >
844
+ <!-- Calendar Header -->
845
+ <div
846
+ style="
847
+ display: flex;
848
+ align-items: center;
849
+ justify-content: space-between;
850
+ margin-bottom: 8px;
851
+ "
852
+ >
853
+ <button
854
+ type="button"
855
+ :disabled="isNavPrevDisabled()"
856
+ style="
857
+ background: none;
858
+ border: none;
859
+ padding: 4px;
860
+ border-radius: var(--radxs, 6px);
861
+ color: var(--muted, #797b8d);
862
+ display: flex;
863
+ align-items: center;
864
+ cursor: pointer;
865
+ transition: background .12s;
866
+ "
867
+ :style="{
868
+ opacity: isNavPrevDisabled() ? 0.35 : 1,
869
+ cursor: isNavPrevDisabled() ? 'not-allowed' : 'pointer',
870
+ }"
871
+ @click="nav(-1)"
872
+ >
873
+ <ChevronLeft :size="15" />
874
+ </button>
875
+
876
+ <button
877
+ type="button"
878
+ style="
879
+ background: none;
880
+ border: none;
881
+ cursor: pointer;
882
+ font-size: .82rem;
883
+ font-weight: 700;
884
+ color: var(--primary-text, #0f172a);
885
+ padding: 4px 8px;
886
+ border-radius: var(--radxs, 6px);
887
+ font-family: var(--sans, inherit);
888
+ "
889
+ @click="cycleView"
890
+ >
891
+ {{ headerLabel }}
892
+ </button>
893
+
894
+ <button
895
+ type="button"
896
+ :disabled="isNavNextDisabled()"
897
+ style="
898
+ background: none;
899
+ border: none;
900
+ padding: 4px;
901
+ border-radius: var(--radxs, 6px);
902
+ color: var(--muted, #797b8d);
903
+ display: flex;
904
+ align-items: center;
905
+ cursor: pointer;
906
+ transition: background .12s;
907
+ "
908
+ :style="{
909
+ opacity: isNavNextDisabled() ? 0.35 : 1,
910
+ cursor: isNavNextDisabled() ? 'not-allowed' : 'pointer',
911
+ }"
912
+ @click="nav(1)"
913
+ >
914
+ <ChevronRight :size="15" />
915
+ </button>
916
+ </div>
917
+
918
+ <!-- Day View -->
919
+ <div v-if="view === 'day'">
920
+ <div
921
+ style="
922
+ display: grid;
923
+ grid-template-columns: repeat(7, 1fr);
924
+ margin-bottom: 4px;
925
+ "
926
+ >
927
+ <div
928
+ v-for="d in loc.days"
929
+ :key="d"
930
+ style="
931
+ text-align: center;
932
+ font-size: .68rem;
933
+ font-weight: 700;
934
+ color: var(--muted, #797b8d);
935
+ padding: 4px 0;
936
+ font-family: var(--mono, ui-monospace, monospace);
937
+ "
938
+ >
939
+ {{ d }}
940
+ </div>
941
+ </div>
942
+
943
+ <div
944
+ style="
945
+ display: grid;
946
+ grid-template-columns: repeat(7, 1fr);
947
+ gap: 2px 0;
948
+ "
949
+ >
950
+ <div
951
+ v-for="(c, i) in daysCells"
952
+ :key="i"
953
+ :style="{
954
+ position: 'relative',
955
+ textAlign: 'center',
956
+ padding: '6px 2px',
957
+ fontSize: c.cur ? '.8rem' : '.78rem',
958
+ fontWeight: (props.range ? isRangeEndpoint(c.date) : isSameDay(c.date, currentVal)) ? 700 : isToday(c.date.getFullYear(), c.date.getMonth(), c.date.getDate()) ? 600 : 400,
959
+ borderRadius: getRangeRadius(c.date),
960
+ cursor: isCellDisabled(c.date) ? 'not-allowed' : 'pointer',
961
+ opacity: isCellDisabled(c.date) ? 0.35 : c.cur ? 1 : 0.65,
962
+ background: (props.range ? isRangeEndpoint(c.date) : isSameDay(c.date, currentVal))
963
+ ? 'var(--accent, #12675D)'
964
+ : isRangeBetween(c.date)
965
+ ? 'rgba(18, 103, 93, 0.14)'
966
+ : 'transparent',
967
+ color: (props.range ? isRangeEndpoint(c.date) : isSameDay(c.date, currentVal))
968
+ ? '#ffffff'
969
+ : isCellDisabled(c.date)
970
+ ? 'var(--border, #e2e8f0)'
971
+ : isToday(c.date.getFullYear(), c.date.getMonth(), c.date.getDate())
972
+ ? 'var(--accent, #12675D)'
973
+ : c.cur
974
+ ? 'var(--primary-text, #0f172a)'
975
+ : 'var(--muted, #797b8d)',
976
+ border: isToday(c.date.getFullYear(), c.date.getMonth(), c.date.getDate()) && !(props.range ? isRangeEndpoint(c.date) : isSameDay(c.date, currentVal))
977
+ ? '1.5px solid var(--accent, #12675D)'
978
+ : '1.5px solid transparent',
979
+ transition: 'background .12s, color .12s',
980
+ }"
981
+ @mouseenter="props.range && rangeStart && !rangeEnd && (hoverDate = c.date)"
982
+ @click="!isCellDisabled(c.date) && pick(c.date)"
983
+ >
984
+ {{ c.day }}
985
+ <span
986
+ v-if="badges[toKey(c.date.getFullYear(), c.date.getMonth(), c.day)]"
987
+ :style="{
988
+ position: 'absolute',
989
+ bottom: '2px',
990
+ left: '50%',
991
+ transform: 'translateX(-50%)',
992
+ width: '4px',
993
+ height: '4px',
994
+ borderRadius: '50%',
995
+ background: badges[toKey(c.date.getFullYear(), c.date.getMonth(), c.day)]?.color || 'var(--accent, #12675D)',
996
+ }"
997
+ />
998
+ </div>
999
+ </div>
1000
+ </div>
1001
+
1002
+ <!-- Month View -->
1003
+ <div
1004
+ v-else-if="view === 'month'"
1005
+ style="
1006
+ display: grid;
1007
+ grid-template-columns: repeat(3, 1fr);
1008
+ gap: 4px;
1009
+ padding: 4px 0;
1010
+ "
1011
+ >
1012
+ <div
1013
+ v-for="(mName, idx) in loc.months"
1014
+ :key="idx"
1015
+ :style="{
1016
+ textAlign: 'center',
1017
+ padding: '10px 4px',
1018
+ borderRadius: 'var(--radxs, 6px)',
1019
+ fontSize: '.82rem',
1020
+ fontWeight: 600,
1021
+ cursor: isMonthDisabled(idx) ? 'not-allowed' : 'pointer',
1022
+ opacity: isMonthDisabled(idx) ? 0.35 : 1,
1023
+ background: valDate && valDate.getFullYear() === cursor.getFullYear() && valDate.getMonth() === idx
1024
+ ? 'var(--accent, #12675D)'
1025
+ : 'transparent',
1026
+ color: valDate && valDate.getFullYear() === cursor.getFullYear() && valDate.getMonth() === idx
1027
+ ? '#ffffff'
1028
+ : isMonthDisabled(idx)
1029
+ ? 'var(--border, #e2e8f0)'
1030
+ : 'var(--primary-text, #0f172a)',
1031
+ transition: 'background .12s',
1032
+ }"
1033
+ @click="
1034
+ () => {
1035
+ if (isMonthDisabled(idx)) return;
1036
+ const c = new Date(cursor);
1037
+ c.setMonth(idx);
1038
+ cursor = c;
1039
+ view = 'day';
1040
+ }
1041
+ "
1042
+ >
1043
+ {{ mName }}
1044
+ </div>
1045
+ </div>
1046
+
1047
+ <!-- Year View -->
1048
+ <div
1049
+ v-else-if="view === 'year'"
1050
+ style="
1051
+ display: grid;
1052
+ grid-template-columns: repeat(3, 1fr);
1053
+ gap: 4px;
1054
+ padding: 4px 0;
1055
+ "
1056
+ >
1057
+ <div
1058
+ v-for="yr in yearsList"
1059
+ :key="yr"
1060
+ :style="{
1061
+ textAlign: 'center',
1062
+ padding: '10px 4px',
1063
+ borderRadius: 'var(--radxs, 6px)',
1064
+ fontSize: '.82rem',
1065
+ fontWeight: 600,
1066
+ cursor: isYearDisabled(yr) ? 'not-allowed' : 'pointer',
1067
+ opacity: isYearDisabled(yr) ? 0.35 : 1,
1068
+ background: valDate && valDate.getFullYear() === yr ? 'var(--accent, #12675D)' : 'transparent',
1069
+ color: valDate && valDate.getFullYear() === yr
1070
+ ? '#ffffff'
1071
+ : isYearDisabled(yr)
1072
+ ? 'var(--border, #e2e8f0)'
1073
+ : 'var(--primary-text, #0f172a)',
1074
+ transition: 'background .12s',
1075
+ }"
1076
+ @click="
1077
+ () => {
1078
+ if (isYearDisabled(yr)) return;
1079
+ const c = new Date(cursor);
1080
+ c.setFullYear(yr);
1081
+ cursor = c;
1082
+ view = 'month';
1083
+ }
1084
+ "
1085
+ >
1086
+ {{ yr }}
1087
+ </div>
1088
+ </div>
1089
+
1090
+ <!-- Time Picker Footer (Optional) -->
1091
+ <div
1092
+ v-if="showTime"
1093
+ style="
1094
+ border-top: 1px solid var(--border-light, #ebebf0);
1095
+ margin-top: 8px;
1096
+ padding-top: 8px;
1097
+ display: flex;
1098
+ align-items: center;
1099
+ justify-content: space-between;
1100
+ gap: 8px;
1101
+ "
1102
+ >
1103
+ <div
1104
+ style="
1105
+ display: inline-flex;
1106
+ align-items: center;
1107
+ gap: 5px;
1108
+ font-size: .76rem;
1109
+ color: var(--muted, #797b8d);
1110
+ fontWeight: 600;
1111
+ "
1112
+ >
1113
+ <Clock :size="13" style="opacity: 0.85;" />
1114
+ <span>{{ loc.time || 'Time:' }}</span>
1115
+ </div>
1116
+ <button
1117
+ type="button"
1118
+ style="
1119
+ padding: 4px 10px;
1120
+ border-radius: var(--radxs, 6px);
1121
+ background: var(--accent, #12675D);
1122
+ color: var(--def-btn-text, #ffffff);
1123
+ border: none;
1124
+ font-size: 11.5px;
1125
+ font-weight: 600;
1126
+ cursor: pointer;
1127
+ "
1128
+ @click="open = false; view = 'day';"
1129
+ >
1130
+ {{ loc.ok || 'OK' }}
1131
+ </button>
1132
+ </div>
1133
+ </div>
1134
+ </Teleport>
1135
+ </div>
1136
+ </template>