cynx-lib 1.3.67 → 1.3.69
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/ui/components/DatePicker.jsx +335 -52
- package/vue/components/Badge.vue +8 -0
- package/vue/components/DatePicker.vue +1136 -0
- package/vue/components/LinearProgress.vue +268 -0
- package/vue/components/Sidebar.vue +4 -3
- package/vue/components/StatusTag.vue +60 -0
- package/vue/index.js +9 -0
|
@@ -0,0 +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
|
+
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>
|