@fivem-nui/svelte-components 0.0.0-stage → 1.0.0
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/README.md +101 -2
- package/dist/components/actions/NuiButton.svelte +292 -0
- package/dist/components/actions/NuiButton.svelte.d.ts +60 -0
- package/dist/components/data/NuiBadge.svelte +277 -0
- package/dist/components/data/NuiBadge.svelte.d.ts +74 -0
- package/dist/components/data/NuiCodeBlock.svelte +256 -0
- package/dist/components/data/NuiCodeBlock.svelte.d.ts +49 -0
- package/dist/components/data/NuiList.svelte +341 -0
- package/dist/components/data/NuiList.svelte.d.ts +52 -0
- package/dist/components/data/NuiProgressBar.svelte +264 -0
- package/dist/components/data/NuiProgressBar.svelte.d.ts +51 -0
- package/dist/components/data/NuiRichText.svelte +260 -0
- package/dist/components/data/NuiRichText.svelte.d.ts +33 -0
- package/dist/components/data/NuiTable.svelte +470 -0
- package/dist/components/data/NuiTable.svelte.d.ts +53 -0
- package/dist/components/data/chart/NuiChart.svelte +384 -0
- package/dist/components/data/chart/NuiChart.svelte.d.ts +49 -0
- package/dist/components/data/chart/color.d.ts +9 -0
- package/dist/components/data/chart/color.js +78 -0
- package/dist/components/data/chart/index.d.ts +10 -0
- package/dist/components/data/chart/index.js +10 -0
- package/dist/components/data/chart/registry.d.ts +14 -0
- package/dist/components/data/chart/registry.js +53 -0
- package/dist/components/data/chart/renderers/bar.d.ts +8 -0
- package/dist/components/data/chart/renderers/bar.js +79 -0
- package/dist/components/data/chart/renderers/base.d.ts +18 -0
- package/dist/components/data/chart/renderers/base.js +10 -0
- package/dist/components/data/chart/renderers/line.d.ts +8 -0
- package/dist/components/data/chart/renderers/line.js +89 -0
- package/dist/components/data/chart/renderers/pie.d.ts +10 -0
- package/dist/components/data/chart/renderers/pie.js +55 -0
- package/dist/components/data/chart/scale.d.ts +22 -0
- package/dist/components/data/chart/scale.js +101 -0
- package/dist/components/data/chart/types.d.ts +84 -0
- package/dist/components/data/chart/types.js +1 -0
- package/dist/components/data/chart/validator.d.ts +10 -0
- package/dist/components/data/chart/validator.js +82 -0
- package/dist/components/forms/NuiCheckbox.svelte +218 -0
- package/dist/components/forms/NuiCheckbox.svelte.d.ts +37 -0
- package/dist/components/forms/NuiDatepicker.svelte +755 -0
- package/dist/components/forms/NuiDatepicker.svelte.d.ts +34 -0
- package/dist/components/forms/NuiFileInput.svelte +1047 -0
- package/dist/components/forms/NuiFileInput.svelte.d.ts +93 -0
- package/dist/components/forms/NuiFileInput.types.d.ts +28 -0
- package/dist/components/forms/NuiFileInput.types.js +1 -0
- package/dist/components/forms/NuiForm.svelte +391 -0
- package/dist/components/forms/NuiForm.svelte.d.ts +69 -0
- package/dist/components/forms/NuiInput.svelte +296 -0
- package/dist/components/forms/NuiInput.svelte.d.ts +62 -0
- package/dist/components/forms/NuiRadioGroup.svelte +238 -0
- package/dist/components/forms/NuiRadioGroup.svelte.d.ts +47 -0
- package/dist/components/forms/NuiSelect.svelte +819 -0
- package/dist/components/forms/NuiSelect.svelte.d.ts +49 -0
- package/dist/components/layout/NuiCard.svelte +314 -0
- package/dist/components/layout/NuiCard.svelte.d.ts +48 -0
- package/dist/components/layout/NuiCardGrid.svelte +98 -0
- package/dist/components/layout/NuiCardGrid.svelte.d.ts +22 -0
- package/dist/components/layout/NuiFlex.svelte +121 -0
- package/dist/components/layout/NuiFlex.svelte.d.ts +23 -0
- package/dist/components/layout/NuiGrid.svelte +66 -0
- package/dist/components/layout/NuiGrid.svelte.d.ts +14 -0
- package/dist/components/layout/NuiStack.svelte +138 -0
- package/dist/components/layout/NuiStack.svelte.d.ts +26 -0
- package/dist/components/navigation/NuiAccordion.svelte +323 -0
- package/dist/components/navigation/NuiAccordion.svelte.d.ts +38 -0
- package/dist/components/navigation/NuiBreadcrumbs.svelte +133 -0
- package/dist/components/navigation/NuiBreadcrumbs.svelte.d.ts +19 -0
- package/dist/components/navigation/NuiMenu.svelte +596 -0
- package/dist/components/navigation/NuiMenu.svelte.d.ts +28 -0
- package/dist/components/navigation/NuiPagination.svelte +306 -0
- package/dist/components/navigation/NuiPagination.svelte.d.ts +40 -0
- package/dist/components/navigation/NuiSidebar.svelte +186 -0
- package/dist/components/navigation/NuiSidebar.svelte.d.ts +26 -0
- package/dist/components/navigation/NuiTabs.svelte +249 -0
- package/dist/components/navigation/NuiTabs.svelte.d.ts +31 -0
- package/dist/components/overlay/NuiDialog.svelte +118 -0
- package/dist/components/overlay/NuiDialog.svelte.d.ts +34 -0
- package/dist/components/overlay/NuiModal.svelte +992 -0
- package/dist/components/overlay/NuiModal.svelte.d.ts +72 -0
- package/dist/components/overlay/NuiTooltip.svelte +159 -0
- package/dist/components/overlay/NuiTooltip.svelte.d.ts +13 -0
- package/dist/composables/useClickOutside.d.ts +43 -0
- package/dist/composables/useClickOutside.js +73 -0
- package/dist/composables/useFormValidation.d.ts +23 -0
- package/dist/composables/useFormValidation.js +72 -0
- package/dist/composables/useIdle.svelte.d.ts +18 -0
- package/dist/composables/useIdle.svelte.js +144 -0
- package/dist/composables/useKeyboardShortcut.svelte.d.ts +114 -0
- package/dist/composables/useKeyboardShortcut.svelte.js +153 -0
- package/dist/composables/useLocalStorage.svelte.d.ts +22 -0
- package/dist/composables/useLocalStorage.svelte.js +105 -0
- package/dist/composables/useMediaQuery.svelte.d.ts +51 -0
- package/dist/composables/useMediaQuery.svelte.js +80 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +42 -0
- package/package.json +58 -3
- package/src/components/actions/NuiButton.svelte +292 -0
- package/src/components/data/NuiBadge.svelte +277 -0
- package/src/components/data/NuiCodeBlock.svelte +256 -0
- package/src/components/data/NuiList.svelte +341 -0
- package/src/components/data/NuiProgressBar.svelte +264 -0
- package/src/components/data/NuiRichText.svelte +260 -0
- package/src/components/data/NuiTable.svelte +470 -0
- package/src/components/data/chart/NuiChart.svelte +384 -0
- package/src/components/data/chart/color.ts +83 -0
- package/src/components/data/chart/index.ts +10 -0
- package/src/components/data/chart/registry.ts +71 -0
- package/src/components/data/chart/renderers/bar.ts +90 -0
- package/src/components/data/chart/renderers/base.ts +32 -0
- package/src/components/data/chart/renderers/line.ts +99 -0
- package/src/components/data/chart/renderers/pie.ts +66 -0
- package/src/components/data/chart/scale.ts +139 -0
- package/src/components/data/chart/types.ts +98 -0
- package/src/components/data/chart/validator.ts +98 -0
- package/src/components/forms/NuiCheckbox.svelte +218 -0
- package/src/components/forms/NuiDatepicker.svelte +755 -0
- package/src/components/forms/NuiFileInput.svelte +1047 -0
- package/src/components/forms/NuiFileInput.types.ts +29 -0
- package/src/components/forms/NuiForm.svelte +391 -0
- package/src/components/forms/NuiInput.svelte +296 -0
- package/src/components/forms/NuiRadioGroup.svelte +238 -0
- package/src/components/forms/NuiSelect.svelte +819 -0
- package/src/components/layout/NuiCard.svelte +314 -0
- package/src/components/layout/NuiCardGrid.svelte +98 -0
- package/src/components/layout/NuiFlex.svelte +121 -0
- package/src/components/layout/NuiGrid.svelte +66 -0
- package/src/components/layout/NuiStack.svelte +138 -0
- package/src/components/navigation/NuiAccordion.svelte +323 -0
- package/src/components/navigation/NuiBreadcrumbs.svelte +133 -0
- package/src/components/navigation/NuiMenu.svelte +596 -0
- package/src/components/navigation/NuiPagination.svelte +306 -0
- package/src/components/navigation/NuiSidebar.svelte +186 -0
- package/src/components/navigation/NuiTabs.svelte +249 -0
- package/src/components/overlay/NuiDialog.svelte +118 -0
- package/src/components/overlay/NuiModal.svelte +992 -0
- package/src/components/overlay/NuiTooltip.svelte +159 -0
- package/src/composables/useClickOutside.ts +132 -0
- package/src/composables/useFormValidation.ts +105 -0
- package/src/composables/useIdle.svelte.ts +191 -0
- package/src/composables/useKeyboardShortcut.svelte.ts +278 -0
- package/src/composables/useLocalStorage.svelte.ts +137 -0
- package/src/composables/useMediaQuery.svelte.ts +102 -0
- package/src/index.ts +66 -0
|
@@ -0,0 +1,755 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component NuiDatepicker
|
|
3
|
+
Accessible date picker component conforming to WAI-ARIA grid design pattern,
|
|
4
|
+
supporting full keyboard navigation (arrows, PageUp/PageDown, Home/End, Escape),
|
|
5
|
+
strict ISO date validation, min/max constraints, today highlighting, and FiveM NUI event integration.
|
|
6
|
+
-->
|
|
7
|
+
<script lang="ts">
|
|
8
|
+
import { nui } from '@fivem-nui/svelte';
|
|
9
|
+
import type { SvelteNuiClient } from '@fivem-nui/svelte';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
/** Optional HTML id attribute. Auto-generated via $props.id() if omitted. */
|
|
13
|
+
id?: string;
|
|
14
|
+
/** Selected date string in YYYY-MM-DD format. Supports two-way binding. */
|
|
15
|
+
value?: string;
|
|
16
|
+
/** Visual label text displayed above the control. */
|
|
17
|
+
label?: string;
|
|
18
|
+
/** Placeholder text displayed when no date is selected. */
|
|
19
|
+
placeholder?: string;
|
|
20
|
+
/** Whether the datepicker is disabled. */
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
/** Minimum allowed date in YYYY-MM-DD format. */
|
|
23
|
+
min?: string;
|
|
24
|
+
/** Maximum allowed date in YYYY-MM-DD format. */
|
|
25
|
+
max?: string;
|
|
26
|
+
/** Accessible label for screen readers when visual label is omitted. */
|
|
27
|
+
ariaLabel?: string;
|
|
28
|
+
/** Optional NUI event name sent on date selection with `{ value: dateStr }`. */
|
|
29
|
+
eventName?: string;
|
|
30
|
+
/** Callback fired when a valid date is selected. */
|
|
31
|
+
onSelect?: (date: string) => void;
|
|
32
|
+
/** SvelteNuiClient instance for event transport. */
|
|
33
|
+
client?: SvelteNuiClient;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const autoId = $props.id();
|
|
37
|
+
|
|
38
|
+
let {
|
|
39
|
+
id,
|
|
40
|
+
value = $bindable(''),
|
|
41
|
+
label = '',
|
|
42
|
+
placeholder = 'Select date...',
|
|
43
|
+
disabled = false,
|
|
44
|
+
min = '',
|
|
45
|
+
max = '',
|
|
46
|
+
ariaLabel,
|
|
47
|
+
eventName,
|
|
48
|
+
onSelect,
|
|
49
|
+
client = nui
|
|
50
|
+
}: Props = $props();
|
|
51
|
+
|
|
52
|
+
let isOpen = $state(false);
|
|
53
|
+
let containerEl = $state<HTMLElement | null>(null);
|
|
54
|
+
let triggerEl = $state<HTMLButtonElement | null>(null);
|
|
55
|
+
let calendarEl = $state<HTMLDivElement | null>(null);
|
|
56
|
+
|
|
57
|
+
const controlId = $derived(id || autoId);
|
|
58
|
+
const labelId = $derived(label ? `${controlId}-label` : undefined);
|
|
59
|
+
const calendarId = $derived(`${controlId}-calendar`);
|
|
60
|
+
const resolvedAriaLabel = $derived(!label ? (ariaLabel || 'Select date') : undefined);
|
|
61
|
+
|
|
62
|
+
/** Validates whether a string is a genuine, calendar-valid YYYY-MM-DD date. */
|
|
63
|
+
function isValidDateString(dateStr: string | undefined): dateStr is string {
|
|
64
|
+
if (!dateStr || !/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) return false;
|
|
65
|
+
const [y, m, d] = dateStr.split('-').map(Number);
|
|
66
|
+
if (y < 1000 || y > 9999 || m < 1 || m > 12 || d < 1 || d > 31) return false;
|
|
67
|
+
const dt = new Date(y, m - 1, d);
|
|
68
|
+
return dt.getFullYear() === y && dt.getMonth() === m - 1 && dt.getDate() === d;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function parseDateString(dateStr: string): Date | null {
|
|
72
|
+
if (!isValidDateString(dateStr)) return null;
|
|
73
|
+
const [y, m, d] = dateStr.split('-').map(Number);
|
|
74
|
+
return new Date(y, m - 1, d);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function formatDateString(date: Date): string {
|
|
78
|
+
const y = date.getFullYear();
|
|
79
|
+
const m = String(date.getMonth() + 1).padStart(2, '0');
|
|
80
|
+
const d = String(date.getDate()).padStart(2, '0');
|
|
81
|
+
return `${y}-${m}-${d}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const today = new Date();
|
|
85
|
+
const todayStr = formatDateString(today);
|
|
86
|
+
|
|
87
|
+
const validMin = $derived(isValidDateString(min) ? min : undefined);
|
|
88
|
+
const validMax = $derived(isValidDateString(max) ? max : undefined);
|
|
89
|
+
|
|
90
|
+
// Validate range order and warn if min > max
|
|
91
|
+
const effectiveMin = $derived(validMin);
|
|
92
|
+
const effectiveMax = $derived.by(() => {
|
|
93
|
+
if (validMin && validMax && validMin > validMax) {
|
|
94
|
+
console.warn(
|
|
95
|
+
`[fivem-nui-components] NuiDatepicker: min (${validMin}) is greater than max (${validMax}). Disregarding max constraint.`
|
|
96
|
+
);
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
return validMax;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const validValue = $derived(isValidDateString(value) ? value : '');
|
|
103
|
+
const isValueMalformed = $derived(Boolean(value && !isValidDateString(value)));
|
|
104
|
+
const isValueOutOfRange = $derived.by(() => {
|
|
105
|
+
if (!validValue) return false;
|
|
106
|
+
if (effectiveMin && validValue < effectiveMin) return true;
|
|
107
|
+
if (effectiveMax && validValue > effectiveMax) return true;
|
|
108
|
+
return false;
|
|
109
|
+
});
|
|
110
|
+
const isValueInvalid = $derived(isValueMalformed || isValueOutOfRange);
|
|
111
|
+
|
|
112
|
+
// Development warnings for invalid props
|
|
113
|
+
$effect(() => {
|
|
114
|
+
if (value && !isValidDateString(value)) {
|
|
115
|
+
console.warn(
|
|
116
|
+
`[fivem-nui-components] NuiDatepicker: value '${value}' is not a valid ISO-8601 date (YYYY-MM-DD).`
|
|
117
|
+
);
|
|
118
|
+
} else if (validValue && isValueOutOfRange) {
|
|
119
|
+
console.warn(
|
|
120
|
+
`[fivem-nui-components] NuiDatepicker: value '${validValue}' is outside allowed range [${effectiveMin ?? '-inf'}, ${effectiveMax ?? '+inf'}].`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
$effect(() => {
|
|
126
|
+
if (min && !isValidDateString(min)) {
|
|
127
|
+
console.warn(
|
|
128
|
+
`[fivem-nui-components] NuiDatepicker: min prop '${min}' is not a valid ISO-8601 date (YYYY-MM-DD).`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
if (max && !isValidDateString(max)) {
|
|
132
|
+
console.warn(
|
|
133
|
+
`[fivem-nui-components] NuiDatepicker: max prop '${max}' is not a valid ISO-8601 date (YYYY-MM-DD).`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
let viewYear = $state(today.getFullYear());
|
|
139
|
+
let viewMonth = $state(today.getMonth());
|
|
140
|
+
let focusedDateStr = $state<string>(todayStr);
|
|
141
|
+
|
|
142
|
+
function clampToAllowedRange(dateStr: string): string {
|
|
143
|
+
if (effectiveMin && dateStr < effectiveMin) return effectiveMin;
|
|
144
|
+
if (effectiveMax && dateStr > effectiveMax) return effectiveMax;
|
|
145
|
+
return dateStr;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Sync calendar view with valid, in-range incoming value
|
|
149
|
+
$effect(() => {
|
|
150
|
+
if (validValue && !isValueOutOfRange) {
|
|
151
|
+
const parts = validValue.split('-').map(Number);
|
|
152
|
+
viewYear = parts[0];
|
|
153
|
+
viewMonth = parts[1] - 1;
|
|
154
|
+
focusedDateStr = validValue;
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const daysOfWeek = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
|
159
|
+
const monthNames = [
|
|
160
|
+
'January', 'February', 'March', 'April', 'May', 'June',
|
|
161
|
+
'July', 'August', 'September', 'October', 'November', 'December'
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
const canPrevMonth = $derived.by(() => {
|
|
165
|
+
if (!effectiveMin) return true;
|
|
166
|
+
const prevMonthEnd = new Date(viewYear, viewMonth, 0);
|
|
167
|
+
return formatDateString(prevMonthEnd) >= effectiveMin;
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const canNextMonth = $derived.by(() => {
|
|
171
|
+
if (!effectiveMax) return true;
|
|
172
|
+
const nextMonthStart = new Date(viewYear, viewMonth + 1, 1);
|
|
173
|
+
return formatDateString(nextMonthStart) <= effectiveMax;
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
function prevMonth() {
|
|
177
|
+
if (!canPrevMonth) return;
|
|
178
|
+
viewMonth--;
|
|
179
|
+
if (viewMonth < 0) {
|
|
180
|
+
viewMonth = 11;
|
|
181
|
+
viewYear--;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function nextMonth() {
|
|
186
|
+
if (!canNextMonth) return;
|
|
187
|
+
viewMonth++;
|
|
188
|
+
if (viewMonth > 11) {
|
|
189
|
+
viewMonth = 0;
|
|
190
|
+
viewYear++;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
interface CalendarCell {
|
|
195
|
+
empty: boolean;
|
|
196
|
+
day?: number;
|
|
197
|
+
dateStr?: string;
|
|
198
|
+
disabled?: boolean;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const calendarWeeks = $derived.by((): CalendarCell[][] => {
|
|
202
|
+
const firstDay = new Date(viewYear, viewMonth, 1).getDay();
|
|
203
|
+
// Monday-based offset (0 = Mon, 6 = Sun)
|
|
204
|
+
const offset = (firstDay + 6) % 7;
|
|
205
|
+
const daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate();
|
|
206
|
+
|
|
207
|
+
const cells: CalendarCell[] = [];
|
|
208
|
+
|
|
209
|
+
for (let i = 0; i < offset; i++) {
|
|
210
|
+
cells.push({ empty: true });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
for (let d = 1; d <= daysInMonth; d++) {
|
|
214
|
+
const mm = String(viewMonth + 1).padStart(2, '0');
|
|
215
|
+
const dd = String(d).padStart(2, '0');
|
|
216
|
+
const dateStr = `${viewYear}-${mm}-${dd}`;
|
|
217
|
+
const isBeforeMin = effectiveMin ? dateStr < effectiveMin : false;
|
|
218
|
+
const isAfterMax = effectiveMax ? dateStr > effectiveMax : false;
|
|
219
|
+
|
|
220
|
+
cells.push({
|
|
221
|
+
empty: false,
|
|
222
|
+
day: d,
|
|
223
|
+
dateStr,
|
|
224
|
+
disabled: isBeforeMin || isAfterMax
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Pad trailing cells to complete the final 7-day row
|
|
229
|
+
const remainder = (7 - (cells.length % 7)) % 7;
|
|
230
|
+
for (let i = 0; i < remainder; i++) {
|
|
231
|
+
cells.push({ empty: true });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const weeks: CalendarCell[][] = [];
|
|
235
|
+
for (let i = 0; i < cells.length; i += 7) {
|
|
236
|
+
weeks.push(cells.slice(i, i + 7));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return weeks;
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
function selectDate(dateStr: string) {
|
|
243
|
+
if (!isValidDateString(dateStr)) return;
|
|
244
|
+
if (effectiveMin && dateStr < effectiveMin) return;
|
|
245
|
+
if (effectiveMax && dateStr > effectiveMax) return;
|
|
246
|
+
|
|
247
|
+
value = dateStr;
|
|
248
|
+
closeCalendar(true);
|
|
249
|
+
|
|
250
|
+
if (eventName) {
|
|
251
|
+
try {
|
|
252
|
+
client.send(eventName, { value: dateStr });
|
|
253
|
+
} catch (err) {
|
|
254
|
+
console.error(`[fivem-nui-components] Failed to send datepicker event '${eventName}':`, err);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
onSelect?.(dateStr);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function openCalendar() {
|
|
262
|
+
if (disabled) return;
|
|
263
|
+
isOpen = true;
|
|
264
|
+
|
|
265
|
+
// Ensure initial focus lands strictly on an in-range, enabled day
|
|
266
|
+
const candidate =
|
|
267
|
+
validValue &&
|
|
268
|
+
(!effectiveMin || validValue >= effectiveMin) &&
|
|
269
|
+
(!effectiveMax || validValue <= effectiveMax)
|
|
270
|
+
? validValue
|
|
271
|
+
: clampToAllowedRange(todayStr);
|
|
272
|
+
|
|
273
|
+
focusedDateStr = candidate;
|
|
274
|
+
const activeDate = parseDateString(candidate) || new Date();
|
|
275
|
+
viewYear = activeDate.getFullYear();
|
|
276
|
+
viewMonth = activeDate.getMonth();
|
|
277
|
+
|
|
278
|
+
requestAnimationFrame(() => {
|
|
279
|
+
calendarEl?.querySelector<HTMLButtonElement>(`button[data-date="${focusedDateStr}"]`)?.focus();
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function closeCalendar(restoreFocus = true) {
|
|
284
|
+
isOpen = false;
|
|
285
|
+
if (restoreFocus) {
|
|
286
|
+
triggerEl?.focus();
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function toggleCalendar() {
|
|
291
|
+
if (isOpen) {
|
|
292
|
+
closeCalendar(true);
|
|
293
|
+
} else {
|
|
294
|
+
openCalendar();
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function changeFocusedDate(daysDelta: number) {
|
|
299
|
+
const current = parseDateString(focusedDateStr) || new Date(viewYear, viewMonth, 1);
|
|
300
|
+
const nextDate = new Date(current.getFullYear(), current.getMonth(), current.getDate() + daysDelta);
|
|
301
|
+
let nextStr = formatDateString(nextDate);
|
|
302
|
+
|
|
303
|
+
if (effectiveMin && nextStr < effectiveMin) nextStr = effectiveMin;
|
|
304
|
+
if (effectiveMax && nextStr > effectiveMax) nextStr = effectiveMax;
|
|
305
|
+
|
|
306
|
+
focusedDateStr = nextStr;
|
|
307
|
+
const resolved = parseDateString(nextStr);
|
|
308
|
+
if (resolved) {
|
|
309
|
+
viewYear = resolved.getFullYear();
|
|
310
|
+
viewMonth = resolved.getMonth();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
requestAnimationFrame(() => {
|
|
314
|
+
calendarEl?.querySelector<HTMLButtonElement>(`button[data-date="${nextStr}"]`)?.focus();
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function changeFocusedMonth(monthsDelta: number) {
|
|
319
|
+
const current = parseDateString(focusedDateStr) || new Date(viewYear, viewMonth, 1);
|
|
320
|
+
const nextDate = new Date(current.getFullYear(), current.getMonth() + monthsDelta, current.getDate());
|
|
321
|
+
if (nextDate.getDate() !== current.getDate()) {
|
|
322
|
+
nextDate.setDate(0);
|
|
323
|
+
}
|
|
324
|
+
let nextStr = formatDateString(nextDate);
|
|
325
|
+
|
|
326
|
+
if (effectiveMin && nextStr < effectiveMin) nextStr = effectiveMin;
|
|
327
|
+
if (effectiveMax && nextStr > effectiveMax) nextStr = effectiveMax;
|
|
328
|
+
|
|
329
|
+
focusedDateStr = nextStr;
|
|
330
|
+
const resolved = parseDateString(nextStr);
|
|
331
|
+
if (resolved) {
|
|
332
|
+
viewYear = resolved.getFullYear();
|
|
333
|
+
viewMonth = resolved.getMonth();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
requestAnimationFrame(() => {
|
|
337
|
+
calendarEl?.querySelector<HTMLButtonElement>(`button[data-date="${nextStr}"]`)?.focus();
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function moveToWeekBoundary(boundary: 'start' | 'end') {
|
|
342
|
+
const current = parseDateString(focusedDateStr) || new Date(viewYear, viewMonth, 1);
|
|
343
|
+
const dayOfWeek = (current.getDay() + 6) % 7; // Monday = 0, Sunday = 6
|
|
344
|
+
const delta = boundary === 'start' ? -dayOfWeek : 6 - dayOfWeek;
|
|
345
|
+
const nextDate = new Date(current.getFullYear(), current.getMonth(), current.getDate() + delta);
|
|
346
|
+
let targetStr = formatDateString(nextDate);
|
|
347
|
+
|
|
348
|
+
if (effectiveMin && targetStr < effectiveMin) targetStr = effectiveMin;
|
|
349
|
+
if (effectiveMax && targetStr > effectiveMax) targetStr = effectiveMax;
|
|
350
|
+
|
|
351
|
+
focusedDateStr = targetStr;
|
|
352
|
+
const resolved = parseDateString(targetStr);
|
|
353
|
+
if (resolved) {
|
|
354
|
+
viewYear = resolved.getFullYear();
|
|
355
|
+
viewMonth = resolved.getMonth();
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
requestAnimationFrame(() => {
|
|
359
|
+
calendarEl?.querySelector<HTMLButtonElement>(`button[data-date="${targetStr}"]`)?.focus();
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Keyboard handler for the calendar grid days (Arrow keys, PageUp/Down, Home/End). */
|
|
364
|
+
function handleGridKeyDown(event: KeyboardEvent) {
|
|
365
|
+
const isDayButton = (event.target as HTMLElement)?.classList.contains('nui-datepicker__day');
|
|
366
|
+
if (!isDayButton) return;
|
|
367
|
+
|
|
368
|
+
switch (event.key) {
|
|
369
|
+
case 'ArrowLeft':
|
|
370
|
+
event.preventDefault();
|
|
371
|
+
changeFocusedDate(-1);
|
|
372
|
+
break;
|
|
373
|
+
case 'ArrowRight':
|
|
374
|
+
event.preventDefault();
|
|
375
|
+
changeFocusedDate(1);
|
|
376
|
+
break;
|
|
377
|
+
case 'ArrowUp':
|
|
378
|
+
event.preventDefault();
|
|
379
|
+
changeFocusedDate(-7);
|
|
380
|
+
break;
|
|
381
|
+
case 'ArrowDown':
|
|
382
|
+
event.preventDefault();
|
|
383
|
+
changeFocusedDate(7);
|
|
384
|
+
break;
|
|
385
|
+
case 'PageUp':
|
|
386
|
+
event.preventDefault();
|
|
387
|
+
if (event.shiftKey) {
|
|
388
|
+
changeFocusedMonth(-12);
|
|
389
|
+
} else {
|
|
390
|
+
changeFocusedMonth(-1);
|
|
391
|
+
}
|
|
392
|
+
break;
|
|
393
|
+
case 'PageDown':
|
|
394
|
+
event.preventDefault();
|
|
395
|
+
if (event.shiftKey) {
|
|
396
|
+
changeFocusedMonth(12);
|
|
397
|
+
} else {
|
|
398
|
+
changeFocusedMonth(1);
|
|
399
|
+
}
|
|
400
|
+
break;
|
|
401
|
+
case 'Home':
|
|
402
|
+
event.preventDefault();
|
|
403
|
+
moveToWeekBoundary('start');
|
|
404
|
+
break;
|
|
405
|
+
case 'End':
|
|
406
|
+
event.preventDefault();
|
|
407
|
+
moveToWeekBoundary('end');
|
|
408
|
+
break;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** Dialog-level keyboard handler for Escape key dismissal. */
|
|
413
|
+
function handleDialogKeyDown(event: KeyboardEvent) {
|
|
414
|
+
if (event.key === 'Escape') {
|
|
415
|
+
event.preventDefault();
|
|
416
|
+
event.stopPropagation();
|
|
417
|
+
closeCalendar(true);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function handleWindowPointerDown(event: PointerEvent) {
|
|
422
|
+
if (isOpen && containerEl && !containerEl.contains(event.target as Node)) {
|
|
423
|
+
closeCalendar(false);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
</script>
|
|
427
|
+
|
|
428
|
+
<svelte:window onpointerdown={handleWindowPointerDown} />
|
|
429
|
+
|
|
430
|
+
<div class="nui-datepicker" bind:this={containerEl}>
|
|
431
|
+
{#if label}
|
|
432
|
+
<label class="nui-datepicker__label" id={labelId} for={controlId}>
|
|
433
|
+
{label}
|
|
434
|
+
</label>
|
|
435
|
+
{/if}
|
|
436
|
+
|
|
437
|
+
<button
|
|
438
|
+
bind:this={triggerEl}
|
|
439
|
+
id={controlId}
|
|
440
|
+
type="button"
|
|
441
|
+
class="nui-datepicker__control"
|
|
442
|
+
class:is-active={isOpen}
|
|
443
|
+
class:is-invalid={isValueInvalid}
|
|
444
|
+
{disabled}
|
|
445
|
+
onclick={toggleCalendar}
|
|
446
|
+
aria-haspopup="dialog"
|
|
447
|
+
aria-expanded={isOpen}
|
|
448
|
+
aria-controls={isOpen ? calendarId : undefined}
|
|
449
|
+
aria-labelledby={labelId}
|
|
450
|
+
aria-label={resolvedAriaLabel}
|
|
451
|
+
>
|
|
452
|
+
<span class="nui-datepicker__text" class:is-placeholder={!validValue}>
|
|
453
|
+
{validValue || placeholder}
|
|
454
|
+
</span>
|
|
455
|
+
<span class="nui-datepicker__icon" aria-hidden="true">📅</span>
|
|
456
|
+
</button>
|
|
457
|
+
|
|
458
|
+
{#if isOpen}
|
|
459
|
+
<div
|
|
460
|
+
bind:this={calendarEl}
|
|
461
|
+
id={calendarId}
|
|
462
|
+
class="nui-datepicker__calendar"
|
|
463
|
+
role="dialog"
|
|
464
|
+
aria-modal="false"
|
|
465
|
+
aria-label={label ? `${label} calendar` : 'Calendar'}
|
|
466
|
+
tabindex="-1"
|
|
467
|
+
onkeydown={handleDialogKeyDown}
|
|
468
|
+
>
|
|
469
|
+
<div class="nui-datepicker__header">
|
|
470
|
+
<button
|
|
471
|
+
type="button"
|
|
472
|
+
class="nui-datepicker__nav"
|
|
473
|
+
disabled={!canPrevMonth}
|
|
474
|
+
onclick={prevMonth}
|
|
475
|
+
aria-label="Previous month"
|
|
476
|
+
>
|
|
477
|
+
‹
|
|
478
|
+
</button>
|
|
479
|
+
<span class="nui-datepicker__month" aria-live="polite">
|
|
480
|
+
{monthNames[viewMonth]} {viewYear}
|
|
481
|
+
</span>
|
|
482
|
+
<button
|
|
483
|
+
type="button"
|
|
484
|
+
class="nui-datepicker__nav"
|
|
485
|
+
disabled={!canNextMonth}
|
|
486
|
+
onclick={nextMonth}
|
|
487
|
+
aria-label="Next month"
|
|
488
|
+
>
|
|
489
|
+
›
|
|
490
|
+
</button>
|
|
491
|
+
</div>
|
|
492
|
+
|
|
493
|
+
<table
|
|
494
|
+
class="nui-datepicker__table"
|
|
495
|
+
role="grid"
|
|
496
|
+
aria-label="{monthNames[viewMonth]} {viewYear}"
|
|
497
|
+
onkeydown={handleGridKeyDown}
|
|
498
|
+
>
|
|
499
|
+
<thead>
|
|
500
|
+
<tr>
|
|
501
|
+
{#each daysOfWeek as day}
|
|
502
|
+
<th class="nui-datepicker__day-header" scope="col">{day}</th>
|
|
503
|
+
{/each}
|
|
504
|
+
</tr>
|
|
505
|
+
</thead>
|
|
506
|
+
<tbody>
|
|
507
|
+
{#each calendarWeeks as week, wIdx (`week-${wIdx}`)}
|
|
508
|
+
<tr>
|
|
509
|
+
{#each week as cell, cIdx (`cell-${wIdx}-${cIdx}`)}
|
|
510
|
+
{#if cell.empty}
|
|
511
|
+
<td class="nui-datepicker__day-empty" role="gridcell" aria-hidden="true"></td>
|
|
512
|
+
{:else}
|
|
513
|
+
{@const isSelected = validValue === cell.dateStr}
|
|
514
|
+
{@const isToday = cell.dateStr === todayStr}
|
|
515
|
+
{@const isFocused = cell.dateStr === focusedDateStr}
|
|
516
|
+
<td class="nui-datepicker__cell" role="gridcell" aria-selected={isSelected ? 'true' : 'false'}>
|
|
517
|
+
<button
|
|
518
|
+
type="button"
|
|
519
|
+
class="nui-datepicker__day"
|
|
520
|
+
class:nui-datepicker__day--selected={isSelected}
|
|
521
|
+
class:nui-datepicker__day--today={isToday}
|
|
522
|
+
class:nui-datepicker__day--focused={isFocused}
|
|
523
|
+
data-date={cell.dateStr}
|
|
524
|
+
disabled={cell.disabled}
|
|
525
|
+
tabindex={isFocused ? 0 : -1}
|
|
526
|
+
aria-current={isToday ? 'date' : undefined}
|
|
527
|
+
onclick={() => cell.dateStr && selectDate(cell.dateStr)}
|
|
528
|
+
>
|
|
529
|
+
{cell.day}
|
|
530
|
+
</button>
|
|
531
|
+
</td>
|
|
532
|
+
{/if}
|
|
533
|
+
{/each}
|
|
534
|
+
</tr>
|
|
535
|
+
{/each}
|
|
536
|
+
</tbody>
|
|
537
|
+
</table>
|
|
538
|
+
</div>
|
|
539
|
+
{/if}
|
|
540
|
+
</div>
|
|
541
|
+
|
|
542
|
+
<style>
|
|
543
|
+
.nui-datepicker {
|
|
544
|
+
position: relative;
|
|
545
|
+
display: flex;
|
|
546
|
+
flex-direction: column;
|
|
547
|
+
gap: 0.35rem;
|
|
548
|
+
font-family: inherit;
|
|
549
|
+
width: 100%;
|
|
550
|
+
box-sizing: border-box;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
.nui-datepicker__label {
|
|
554
|
+
font-size: 0.8125rem;
|
|
555
|
+
font-weight: 600;
|
|
556
|
+
color: var(--nui-fg, #fafafa);
|
|
557
|
+
opacity: 0.85;
|
|
558
|
+
cursor: pointer;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
.nui-datepicker__control {
|
|
562
|
+
display: flex;
|
|
563
|
+
align-items: center;
|
|
564
|
+
gap: 0.5rem;
|
|
565
|
+
padding: 0.5rem 0.75rem;
|
|
566
|
+
border: 1px solid var(--nui-border, #3f3f46);
|
|
567
|
+
border-radius: var(--nui-radius, 0.375rem);
|
|
568
|
+
background: var(--nui-surface-alt, #101012);
|
|
569
|
+
color: var(--nui-fg, #fafafa);
|
|
570
|
+
cursor: pointer;
|
|
571
|
+
font: inherit;
|
|
572
|
+
font-size: 0.875rem;
|
|
573
|
+
box-sizing: border-box;
|
|
574
|
+
transition: border-color 0.15s ease, background 0.15s ease;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
.nui-datepicker__control:hover:not(:disabled),
|
|
578
|
+
.nui-datepicker__control.is-active {
|
|
579
|
+
border-color: var(--nui-primary, #3b82f6);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
.nui-datepicker__control.is-invalid {
|
|
583
|
+
border-color: var(--nui-danger, #ef4444);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
.nui-datepicker__control.is-invalid:focus-visible {
|
|
587
|
+
outline-color: var(--nui-danger, #ef4444);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
.nui-datepicker__control:focus-visible {
|
|
591
|
+
outline: 2px solid var(--nui-primary, #3b82f6);
|
|
592
|
+
outline-offset: 2px;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
.nui-datepicker__control:disabled {
|
|
596
|
+
opacity: 0.5;
|
|
597
|
+
cursor: not-allowed;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
.nui-datepicker__text {
|
|
601
|
+
flex: 1;
|
|
602
|
+
text-align: left;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
.nui-datepicker__text.is-placeholder {
|
|
606
|
+
color: var(--nui-fg-secondary, #a1a1aa);
|
|
607
|
+
opacity: 0.7;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
.nui-datepicker__icon {
|
|
611
|
+
font-size: 0.95rem;
|
|
612
|
+
opacity: 0.75;
|
|
613
|
+
user-select: none;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
.nui-datepicker__calendar {
|
|
617
|
+
position: absolute;
|
|
618
|
+
top: calc(100% + 0.35rem);
|
|
619
|
+
left: 0;
|
|
620
|
+
padding: 0.875rem;
|
|
621
|
+
border: 1px solid var(--nui-border, #3f3f46);
|
|
622
|
+
border-radius: var(--nui-radius, 0.5rem);
|
|
623
|
+
background: var(--nui-surface, #18181b);
|
|
624
|
+
box-shadow: 0 10px 25px rgb(0 0 0 / 0.45);
|
|
625
|
+
z-index: var(--nui-z-dropdown, 100);
|
|
626
|
+
width: 17.5rem;
|
|
627
|
+
box-sizing: border-box;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
.nui-datepicker__calendar:focus-visible {
|
|
631
|
+
outline: none;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
.nui-datepicker__header {
|
|
635
|
+
display: flex;
|
|
636
|
+
justify-content: space-between;
|
|
637
|
+
align-items: center;
|
|
638
|
+
margin-bottom: 0.75rem;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
.nui-datepicker__nav {
|
|
642
|
+
background: transparent;
|
|
643
|
+
border: 1px solid var(--nui-border, #3f3f46);
|
|
644
|
+
border-radius: 50%;
|
|
645
|
+
width: 1.75rem;
|
|
646
|
+
height: 1.75rem;
|
|
647
|
+
font-size: 1rem;
|
|
648
|
+
color: var(--nui-fg, #fafafa);
|
|
649
|
+
cursor: pointer;
|
|
650
|
+
display: flex;
|
|
651
|
+
align-items: center;
|
|
652
|
+
justify-content: center;
|
|
653
|
+
transition: background 0.15s ease, border-color 0.15s ease;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
.nui-datepicker__nav:hover:not(:disabled) {
|
|
657
|
+
background: var(--nui-hover, rgb(255 255 255 / 0.08));
|
|
658
|
+
border-color: var(--nui-border-hover, #52525b);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
.nui-datepicker__nav:focus-visible {
|
|
662
|
+
outline: 2px solid var(--nui-primary, #3b82f6);
|
|
663
|
+
outline-offset: 1px;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
.nui-datepicker__nav:disabled {
|
|
667
|
+
opacity: 0.25;
|
|
668
|
+
cursor: not-allowed;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
.nui-datepicker__month {
|
|
672
|
+
font-weight: 600;
|
|
673
|
+
font-size: 0.875rem;
|
|
674
|
+
color: var(--nui-fg, #fafafa);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
.nui-datepicker__table {
|
|
678
|
+
width: 100%;
|
|
679
|
+
border-collapse: collapse;
|
|
680
|
+
border-spacing: 0;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
.nui-datepicker__day-header {
|
|
684
|
+
color: var(--nui-fg-secondary, #a1a1aa);
|
|
685
|
+
font-size: 0.75rem;
|
|
686
|
+
font-weight: 600;
|
|
687
|
+
text-align: center;
|
|
688
|
+
padding: 0.25rem 0;
|
|
689
|
+
user-select: none;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
.nui-datepicker__cell {
|
|
693
|
+
padding: 0.1rem;
|
|
694
|
+
text-align: center;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
.nui-datepicker__day-empty {
|
|
698
|
+
padding: 0.1rem;
|
|
699
|
+
width: 2rem;
|
|
700
|
+
height: 2rem;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
.nui-datepicker__day {
|
|
704
|
+
width: 2rem;
|
|
705
|
+
height: 2rem;
|
|
706
|
+
padding: 0;
|
|
707
|
+
border: 1px solid transparent;
|
|
708
|
+
border-radius: var(--nui-radius, 0.25rem);
|
|
709
|
+
background: transparent;
|
|
710
|
+
color: var(--nui-fg, #fafafa);
|
|
711
|
+
font: inherit;
|
|
712
|
+
font-size: 0.8125rem;
|
|
713
|
+
cursor: pointer;
|
|
714
|
+
display: inline-flex;
|
|
715
|
+
align-items: center;
|
|
716
|
+
justify-content: center;
|
|
717
|
+
transition: background 0.15s ease, border-color 0.15s ease;
|
|
718
|
+
box-sizing: border-box;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
.nui-datepicker__day:hover:not(:disabled):not(.nui-datepicker__day--selected) {
|
|
722
|
+
background: var(--nui-hover, rgb(255 255 255 / 0.1));
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
.nui-datepicker__day:focus-visible {
|
|
726
|
+
outline: 2px solid var(--nui-primary, #3b82f6);
|
|
727
|
+
outline-offset: 1px;
|
|
728
|
+
z-index: 1;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
.nui-datepicker__day:disabled {
|
|
732
|
+
opacity: 0.25;
|
|
733
|
+
cursor: not-allowed;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
.nui-datepicker__day--today:not(.nui-datepicker__day--selected) {
|
|
737
|
+
border-color: var(--nui-primary, #3b82f6);
|
|
738
|
+
font-weight: 600;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
.nui-datepicker__day--selected {
|
|
742
|
+
background: var(--nui-primary, #3b82f6) !important;
|
|
743
|
+
border-color: var(--nui-primary, #3b82f6) !important;
|
|
744
|
+
color: #fff !important;
|
|
745
|
+
font-weight: 700;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
@media (prefers-reduced-motion: reduce) {
|
|
749
|
+
.nui-datepicker__control,
|
|
750
|
+
.nui-datepicker__nav,
|
|
751
|
+
.nui-datepicker__day {
|
|
752
|
+
transition: none;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
</style>
|