@rkosafo/cai.components 0.0.78 → 0.0.80

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.
Files changed (103) hide show
  1. package/README.md +8 -8
  2. package/dist/baseEditor/index.svelte +32 -32
  3. package/dist/builders/filters/FilterBuilder.svelte +641 -641
  4. package/dist/forms/FormCheckbox/FormCheckbox.svelte +53 -53
  5. package/dist/forms/FormClEditor/ClEdito.svelte +68 -68
  6. package/dist/forms/FormDatepicker/FormDatepicker.svelte +159 -159
  7. package/dist/forms/FormFileUpload/FormFileUplad.svelte +134 -134
  8. package/dist/forms/FormInput/FormInput.svelte +87 -87
  9. package/dist/forms/FormRadio/FormRadio.svelte +53 -53
  10. package/dist/forms/FormSelect/FormSelect.svelte +88 -88
  11. package/dist/forms/FormTextarea/FormTextarea.svelte +78 -78
  12. package/dist/forms/button-toggle/ButtonToggle.svelte +119 -119
  13. package/dist/forms/button-toggle/CheckIcon.svelte +28 -28
  14. package/dist/forms/checkbox/Checkbox.svelte +82 -82
  15. package/dist/forms/checkbox/CheckboxButton.svelte +92 -92
  16. package/dist/forms/datepicker/Datepicker.svelte +707 -707
  17. package/dist/forms/form/Form.svelte +69 -69
  18. package/dist/forms/input/Input.svelte +363 -363
  19. package/dist/forms/label/Label.svelte +38 -38
  20. package/dist/forms/radio/Radio.svelte +48 -48
  21. package/dist/forms/radio/RadioButton.svelte +22 -22
  22. package/dist/forms/select/Select.svelte +56 -56
  23. package/dist/forms/textarea/Textarea.svelte +165 -165
  24. package/dist/forms/toggle/Toggle.svelte +70 -70
  25. package/dist/layout/Chat/CategorySelector.svelte +52 -52
  26. package/dist/layout/Chat/ChatEntry.svelte +246 -246
  27. package/dist/layout/Chat/ChatEntrySkeleton.svelte +81 -81
  28. package/dist/layout/Chat/ChatHeader.svelte +172 -172
  29. package/dist/layout/Chat/ChatInput.svelte +207 -207
  30. package/dist/layout/Chat/DraggableWindow.svelte +230 -230
  31. package/dist/layout/Chat/PreviewPage.svelte +182 -182
  32. package/dist/layout/Chat/RichText.svelte +216 -216
  33. package/dist/layout/ComponentCanvas/Canvas.svelte +40 -40
  34. package/dist/layout/ComponentCanvas/ComponentRenderer.svelte +85 -85
  35. package/dist/layout/TF/Content/Content.svelte +21 -21
  36. package/dist/layout/TF/Header/Header.svelte +166 -166
  37. package/dist/layout/TF/Sidebar/Sidebar.svelte +148 -148
  38. package/dist/layout/TF/Wrapper/Wrapper.svelte +17 -17
  39. package/dist/layout/mailing/MailPaginator.svelte +36 -36
  40. package/dist/layout/mailing/MailSidebar.svelte +39 -39
  41. package/dist/layout/mailing/MailToolBar.svelte +174 -174
  42. package/dist/layout/mailing/MailingContent.svelte +10 -10
  43. package/dist/layout/mailing/MailingHeader.svelte +55 -55
  44. package/dist/layout/mailing/MailingMessageCard.svelte +112 -112
  45. package/dist/layout/mailing/MailingMessageViewer.svelte +87 -87
  46. package/dist/layout/mailing/MailingModule.svelte +448 -448
  47. package/dist/styles/docs.css +615 -615
  48. package/dist/styles/tf-layout.css +185 -185
  49. package/dist/themes/ThemeProvider.svelte +20 -20
  50. package/dist/types/index.d.ts +2 -0
  51. package/dist/typography/heading/Heading.svelte +35 -35
  52. package/dist/ui/accordion/Accordion.svelte +49 -49
  53. package/dist/ui/accordion/AccordionItem.svelte +173 -173
  54. package/dist/ui/alert/Alert.svelte +83 -83
  55. package/dist/ui/alertDialog/AlertDialog.svelte +40 -40
  56. package/dist/ui/avatar/Avatar.svelte +77 -77
  57. package/dist/ui/box/Box.svelte +28 -28
  58. package/dist/ui/breadcrumb/Breadcrumb.svelte +39 -39
  59. package/dist/ui/buttons/ActionButton.svelte +234 -234
  60. package/dist/ui/buttons/Button.svelte +102 -102
  61. package/dist/ui/buttons/GradientButton.svelte +59 -59
  62. package/dist/ui/datatable/Datatable.svelte +525 -525
  63. package/dist/ui/drawer/Drawer.svelte +300 -300
  64. package/dist/ui/dropdown/Dropdown.svelte +36 -36
  65. package/dist/ui/dropdown/DropdownDivider.svelte +11 -11
  66. package/dist/ui/dropdown/DropdownGroup.svelte +14 -14
  67. package/dist/ui/dropdown/DropdownHeader.svelte +14 -14
  68. package/dist/ui/dropdown/DropdownItem.svelte +52 -52
  69. package/dist/ui/footer/Footer.svelte +15 -15
  70. package/dist/ui/footer/FooterBrand.svelte +37 -37
  71. package/dist/ui/footer/FooterCopyright.svelte +45 -45
  72. package/dist/ui/footer/FooterIcon.svelte +22 -22
  73. package/dist/ui/footer/FooterLink.svelte +33 -33
  74. package/dist/ui/footer/FooterLinkGroup.svelte +13 -13
  75. package/dist/ui/icons/IconifyIcon.svelte +7 -7
  76. package/dist/ui/indicator/Indicator.svelte +42 -42
  77. package/dist/ui/modal/Modal.svelte +265 -265
  78. package/dist/ui/notificationList/NotificationList.svelte +123 -123
  79. package/dist/ui/pageLoader/PageLoader.svelte +14 -14
  80. package/dist/ui/pageLoader/PageLoader2.svelte +99 -0
  81. package/dist/ui/pageLoader/PageLoader2.svelte.d.ts +24 -0
  82. package/dist/ui/pageLoader/index.d.ts +2 -1
  83. package/dist/ui/pageLoader/index.js +2 -1
  84. package/dist/ui/paginate/Paginate.svelte +96 -96
  85. package/dist/ui/speedDial/SpeedDial.svelte +77 -77
  86. package/dist/ui/speedDial/SpeedDialButton.svelte +75 -75
  87. package/dist/ui/speedDial/SpeedDialTrigger.svelte +79 -79
  88. package/dist/ui/tab/Tab.svelte +93 -67
  89. package/dist/ui/table/Table.svelte +396 -396
  90. package/dist/ui/tableLoader/TableLoader.svelte +24 -24
  91. package/dist/ui/toast/Toast.svelte +337 -337
  92. package/dist/ui/toast/Toast.svelte.d.ts +10 -10
  93. package/dist/ui/toolbar/Toolbar.svelte +59 -59
  94. package/dist/ui/toolbar/ToolbarButton.svelte +56 -56
  95. package/dist/ui/toolbar/ToolbarGroup.svelte +43 -43
  96. package/dist/ui/tooltip/Tooltip.svelte +51 -51
  97. package/dist/utils/Popper.svelte +257 -257
  98. package/dist/utils/closeButton/CloseButton.svelte +88 -88
  99. package/dist/utils/index.d.ts +2 -2
  100. package/dist/utils/index.js +3 -3
  101. package/dist/utils/singleSelection.svelte.js +48 -48
  102. package/dist/youtube/index.svelte +12 -12
  103. package/package.json +1 -1
@@ -1,707 +1,707 @@
1
- <script lang="ts">
2
- import { onMount } from 'svelte';
3
- import { fade } from 'svelte/transition';
4
- import clsx from 'clsx';
5
- import { datepicker } from './index.js';
6
- import {
7
- parse,
8
- isValid,
9
- addDays,
10
- startOfMonth,
11
- endOfMonth,
12
- startOfWeek,
13
- endOfWeek,
14
- eachDayOfInterval,
15
- isSameDay,
16
- isWithinInterval
17
- } from 'date-fns';
18
- import { Button, getTheme, ToolbarButton, type DatepickerProps } from '../../index.js';
19
-
20
- let {
21
- value = $bindable(),
22
- defaultDate = null,
23
- range = false,
24
- rangeFrom = $bindable(),
25
- rangeTo = $bindable(),
26
- availableFrom = null,
27
- availableTo = null,
28
- locale = 'default',
29
- translationLocale = locale,
30
- firstDayOfWeek = 0,
31
- dateFormat,
32
- placeholder = 'Select date',
33
- disabled = false,
34
- required = false,
35
- inputClass = '',
36
- color = 'primary',
37
- inline = false,
38
- autohide = true,
39
- showActionButtons = false,
40
- title = '',
41
- onselect,
42
- onclear,
43
- onapply,
44
- btnClass,
45
- inputmode = 'none',
46
- classes,
47
- monthColor = 'alternative',
48
- monthBtnSelected = 'bg-primary-500 text-white',
49
- monthBtn = 'text-gray-700 dark:text-gray-300',
50
- class: className,
51
- elementRef = $bindable(),
52
- name,
53
- // New placement prop
54
- placement = 'bottom-start'
55
- }: DatepickerProps = $props();
56
-
57
- const theme = getTheme('datepicker');
58
-
59
- // If translationLocale is not explicitly provided, it will default to the value of locale. This ensures reactivity as both are directly exposed as props.
60
- translationLocale = translationLocale ?? locale;
61
-
62
- let isOpen: boolean = $state(inline);
63
- let showMonthSelector: boolean = $state(false);
64
- let inputElement: HTMLInputElement | null = $state(null);
65
-
66
- $effect(() => {
67
- if (inputElement) {
68
- elementRef = inputElement;
69
- }
70
- });
71
- let datepickerContainerElement: HTMLDivElement;
72
- let currentMonth: Date = $state(value || defaultDate || new Date());
73
- let focusedDate: Date | null = null;
74
- let calendarRef: HTMLDivElement | null = $state(null);
75
-
76
- let daysInMonth = $derived(getDaysInMonth(currentMonth));
77
-
78
- onMount(() => {
79
- if (!inline) {
80
- datepickerContainerElement?.ownerDocument.addEventListener('click', handleClickOutside);
81
- return () => {
82
- datepickerContainerElement?.ownerDocument.removeEventListener('click', handleClickOutside);
83
- };
84
- }
85
- });
86
-
87
- function getDaysInMonth(date: Date): Date[] {
88
- const monthStart = startOfMonth(date);
89
- const monthEnd = endOfMonth(date);
90
- const calendarStart = startOfWeek(monthStart, { weekStartsOn: firstDayOfWeek });
91
- const calendarEnd = endOfWeek(monthEnd, { weekStartsOn: firstDayOfWeek });
92
-
93
- return eachDayOfInterval({ start: calendarStart, end: calendarEnd });
94
- }
95
-
96
- const getWeekdayNames = (): string[] => {
97
- const referenceDate = new Date(1970, 0, 4 + firstDayOfWeek);
98
- return Array.from({ length: 7 }, (_, i) =>
99
- addDays(referenceDate, i).toLocaleDateString(translationLocale, { weekday: 'short' })
100
- );
101
- };
102
-
103
- let weekdays = $derived(getWeekdayNames());
104
-
105
- const getMonthNames = (): string[] => {
106
- return Array.from({ length: 12 }, (_, i) =>
107
- new Date(2000, i, 1).toLocaleDateString(translationLocale, { month: 'short' })
108
- );
109
- };
110
- let monthNames = $derived(getMonthNames());
111
-
112
- const addDay = (date: Date, increment: number): Date => addDays(date, increment);
113
-
114
- function changeMonth(increment: number) {
115
- currentMonth = new Date(currentMonth.getFullYear(), currentMonth.getMonth() + increment, 1);
116
- }
117
-
118
- function changeYear(increment: number) {
119
- currentMonth = new Date(currentMonth.getFullYear() + increment, currentMonth.getMonth(), 1);
120
- }
121
-
122
- function selectMonth(monthIndex: number, event: MouseEvent) {
123
- event.stopPropagation();
124
- currentMonth = new Date(currentMonth.getFullYear(), monthIndex, 1);
125
- showMonthSelector = false;
126
- }
127
-
128
- function toggleMonthSelector(event: MouseEvent) {
129
- event.stopPropagation();
130
- showMonthSelector = !showMonthSelector;
131
- }
132
-
133
- function isDateAvailable(date: Date): boolean {
134
- const dateOnly = new Date(date.getFullYear(), date.getMonth(), date.getDate());
135
-
136
- if (availableFrom) {
137
- const fromDate = new Date(
138
- availableFrom.getFullYear(),
139
- availableFrom.getMonth(),
140
- availableFrom.getDate()
141
- );
142
- if (dateOnly < fromDate) return false;
143
- }
144
-
145
- if (availableTo) {
146
- const toDate = new Date(
147
- availableTo.getFullYear(),
148
- availableTo.getMonth(),
149
- availableTo.getDate()
150
- );
151
- if (dateOnly > toDate) return false;
152
- }
153
-
154
- return true;
155
- }
156
-
157
- function handleDaySelect(day: Date) {
158
- if (!isDateAvailable(day)) return;
159
-
160
- if (range) {
161
- if (!rangeFrom || (rangeFrom && rangeTo)) {
162
- rangeFrom = day;
163
- rangeTo = undefined;
164
- } else if (day < rangeFrom) {
165
- rangeFrom = day;
166
- rangeTo = rangeFrom;
167
- } else {
168
- rangeTo = day;
169
- }
170
- onselect?.({ from: rangeFrom, to: rangeTo });
171
- } else {
172
- value = day;
173
- onselect?.(value);
174
- if (autohide && !inline) isOpen = false;
175
- }
176
- }
177
-
178
- function handleInputChangeWithDateFns() {
179
- const inputValue = inputElement?.value?.trim();
180
- if (!inputValue) {
181
- rangeFrom = undefined;
182
- rangeTo = undefined;
183
- inputElement?.setCustomValidity('');
184
- return;
185
- }
186
-
187
- inputElement?.setCustomValidity('');
188
-
189
- if (range) {
190
- const parts = inputValue.split(' - ');
191
- if (parts.length === 2) {
192
- const parsedFrom = tryParseDate(parts[0]);
193
- const parsedTo = tryParseDate(parts[1]);
194
-
195
- if (
196
- parsedFrom &&
197
- isValid(parsedFrom) &&
198
- isDateAvailable(parsedFrom) &&
199
- parsedTo &&
200
- isValid(parsedTo) &&
201
- isDateAvailable(parsedTo)
202
- ) {
203
- [rangeFrom, rangeTo] =
204
- parsedFrom > parsedTo ? [parsedTo, parsedFrom] : [parsedFrom, parsedTo];
205
- onselect?.({ from: rangeFrom, to: rangeTo });
206
- return;
207
- } else {
208
- inputElement?.setCustomValidity(
209
- `Please enter date range in format: ${getDateFormatPattern()} - ${getDateFormatPattern()}`
210
- );
211
- return;
212
- }
213
- }
214
- }
215
-
216
- const parsedDate = tryParseDate(inputValue);
217
-
218
- if (!parsedDate || !isValid(parsedDate)) {
219
- const formatPattern = getDateFormatPattern();
220
- inputElement?.setCustomValidity(`Please enter date in format: ${formatPattern}`);
221
- return;
222
- }
223
-
224
- if (!isDateAvailable(parsedDate)) {
225
- inputElement?.setCustomValidity('Selected date is not available');
226
- return;
227
- }
228
-
229
- handleDaySelect(parsedDate);
230
- }
231
-
232
- function tryParseDate(inputValue: string): Date | null {
233
- const formatPattern = getDateFormatPattern();
234
- try {
235
- const parsedDate = parse(inputValue, formatPattern, new Date());
236
- if (isValid(parsedDate)) {
237
- return parsedDate;
238
- }
239
- } catch (error) {
240
- // Continue to next strategy
241
- }
242
-
243
- const commonFormats = [
244
- 'd.M.yyyy', // German: 17.7.2025
245
- 'dd.MM.yyyy', // German: 17.07.2025
246
- 'M/d/yyyy', // US: 7/17/2025
247
- 'MM/dd/yyyy', // US: 07/17/2025
248
- 'd/M/yyyy', // UK: 17/7/2025
249
- 'dd/MM/yyyy', // UK: 17/07/2025
250
- 'yyyy-MM-dd', // ISO: 2025-07-17
251
- 'yyyy-M-d', // ISO: 2025-7-17
252
- 'M-d-yyyy', // US with dashes: 7-17-2025
253
- 'd-M-yyyy' // EU with dashes: 17-7-2025
254
- ];
255
-
256
- for (const format of commonFormats) {
257
- try {
258
- const parsedDate = parse(inputValue, format, new Date());
259
- if (isValid(parsedDate)) {
260
- return parsedDate;
261
- }
262
- } catch (error) {
263
- // Continue to next format
264
- }
265
- }
266
-
267
- try {
268
- const nativeDate = new Date(inputValue);
269
- if (isValid(nativeDate) && !isNaN(nativeDate.getTime())) {
270
- return nativeDate;
271
- }
272
- } catch (error) {
273
- // Continue
274
- }
275
-
276
- return null;
277
- }
278
-
279
- function getDateFormatPattern(): string {
280
- const actualLocale = locale === 'default' ? navigator.language : locale;
281
- const testDate = new Date(2025, 0, 15); // January 15, 2025
282
- const formatted = testDate.toLocaleDateString(
283
- actualLocale,
284
- dateFormat || { year: 'numeric', month: 'numeric', day: 'numeric' }
285
- );
286
-
287
- if (formatted.includes('.')) {
288
- // German/European format with dots
289
- if (formatted.startsWith('15.')) {
290
- return 'd.M.yyyy';
291
- } else if (formatted.startsWith('01.')) {
292
- return 'M.d.yyyy';
293
- }
294
- return 'd.M.yyyy'; // Default to day first
295
- } else if (formatted.includes('/')) {
296
- // US/UK format with slashes
297
- if (formatted.startsWith('1/')) {
298
- return 'M/d/yyyy'; // US format
299
- } else if (formatted.startsWith('15/')) {
300
- return 'd/M/yyyy'; // UK format
301
- }
302
-
303
- const testDate2 = new Date(2025, 11, 3); // December 3, 2025
304
- const formatted2 = testDate2.toLocaleDateString(
305
- actualLocale,
306
- dateFormat || { year: 'numeric', month: 'numeric', day: 'numeric' }
307
- );
308
- if (formatted2.startsWith('3/') || formatted2.startsWith('03/')) {
309
- return 'd/M/yyyy';
310
- } else {
311
- return 'M/d/yyyy';
312
- }
313
- } else if (formatted.includes('-')) {
314
- // ISO or other dash format
315
- if (formatted.startsWith('2025-')) {
316
- return 'yyyy-M-d';
317
- } else if (formatted.startsWith('1-')) {
318
- return 'M-d-yyyy';
319
- } else {
320
- return 'd-M-yyyy';
321
- }
322
- }
323
-
324
- // Default fallback - try to detect based on locale
325
- if (actualLocale.startsWith('en-US')) {
326
- return 'M/d/yyyy';
327
- } else if (
328
- actualLocale.startsWith('de') ||
329
- actualLocale.startsWith('at') ||
330
- actualLocale.startsWith('ch')
331
- ) {
332
- return 'd.M.yyyy';
333
- } else if (actualLocale.startsWith('en-GB') || actualLocale.startsWith('en-AU')) {
334
- return 'd/M/yyyy';
335
- }
336
-
337
- return 'M/d/yyyy';
338
- }
339
-
340
- function handleClickOutside(event: MouseEvent) {
341
- if (
342
- isOpen &&
343
- datepickerContainerElement &&
344
- !datepickerContainerElement.contains(event.target as Node)
345
- ) {
346
- isOpen = false;
347
- showMonthSelector = false;
348
- }
349
- }
350
-
351
- // Use locale for formatting (not translationLocale)
352
- const formatDate = (date?: Date): string => date?.toLocaleDateString(locale, dateFormat) ?? '';
353
- const isSameDate = (date1?: Date, date2?: Date): boolean =>
354
- date1 && date2 ? isSameDay(date1, date2) : false;
355
- const isToday = (day: Date): boolean => isSameDate(day, new Date());
356
- const isInRange = (day: Date): boolean =>
357
- !!(range && rangeFrom && rangeTo && isWithinInterval(day, { start: rangeFrom, end: rangeTo }));
358
-
359
- let isSelected = $derived((day: Date): boolean =>
360
- range ? isSameDate(day, rangeFrom) || isSameDate(day, rangeTo) : isSameDate(day, value)
361
- );
362
-
363
- function handleCalendarKeydown(event: KeyboardEvent) {
364
- if (!isOpen) return;
365
-
366
- if (!focusedDate) {
367
- focusedDate = value || new Date();
368
- }
369
-
370
- switch (event.key) {
371
- case 'ArrowLeft':
372
- focusedDate = addDay(focusedDate, -1);
373
- break;
374
- case 'ArrowRight':
375
- focusedDate = addDay(focusedDate, 1);
376
- break;
377
- case 'ArrowUp':
378
- focusedDate = addDay(focusedDate, -7);
379
- break;
380
- case 'ArrowDown':
381
- focusedDate = addDay(focusedDate, 7);
382
- break;
383
- case 'Enter':
384
- if (range) {
385
- if (rangeFrom && rangeTo) {
386
- if (autohide && !inline) isOpen = false;
387
- } else {
388
- handleDaySelect(focusedDate);
389
- }
390
- } else {
391
- handleDaySelect(focusedDate);
392
- if (autohide && !inline) isOpen = false;
393
- }
394
- break;
395
- case 'Escape':
396
- isOpen = false;
397
- showMonthSelector = false;
398
- inputElement?.focus();
399
- break;
400
- default:
401
- return;
402
- }
403
-
404
- event.preventDefault();
405
- if (focusedDate.getMonth() !== currentMonth.getMonth()) {
406
- currentMonth = new Date(focusedDate.getFullYear(), focusedDate.getMonth(), 1);
407
- }
408
-
409
- // Use translationLocale for aria-label
410
- setTimeout(() => {
411
- const focusedButton = calendarRef?.querySelector(
412
- `button[aria-label="${focusedDate!.toLocaleDateString(translationLocale, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}"]`
413
- ) as HTMLButtonElement | null;
414
- focusedButton?.focus();
415
- }, 0);
416
- }
417
-
418
- function handleInputKeydown(event: KeyboardEvent) {
419
- if (event.key === 'Enter') {
420
- event.preventDefault();
421
- handleInputChangeWithDateFns();
422
- if (autohide && !inline) {
423
- isOpen = false;
424
- }
425
- } else if (event.key === ' ') {
426
- event.preventDefault();
427
- isOpen = !isOpen;
428
- }
429
- }
430
-
431
- function handleClear() {
432
- value = rangeFrom = rangeTo = undefined;
433
- onclear?.();
434
- }
435
-
436
- function handleApply() {
437
- const result = range ? { from: rangeFrom, to: rangeTo } : value;
438
- if (result) onapply?.(result);
439
- if (!inline) isOpen = false;
440
- }
441
-
442
- let {
443
- base,
444
- input,
445
- button,
446
- titleVariant,
447
- actionButtons,
448
- columnHeader,
449
- polite,
450
- grid,
451
- nav,
452
- dayButton,
453
- monthButton
454
- } = datepicker({ placement });
455
- </script>
456
-
457
- {#snippet navButton(forward: boolean)}
458
- <ToolbarButton
459
- color="dark"
460
- onclick={() => changeMonth(forward ? 1 : -1)}
461
- size="lg"
462
- aria-label={forward ? 'Next month' : 'Previous month'}
463
- >
464
- <svg
465
- class="h-3 w-3 rtl:rotate-180"
466
- aria-hidden="true"
467
- xmlns="http://www.w3.org/2000/svg"
468
- fill="none"
469
- viewBox="0 0 14 10"
470
- >
471
- <path
472
- stroke="currentColor"
473
- stroke-linecap="round"
474
- stroke-linejoin="round"
475
- stroke-width="2"
476
- d={forward ? 'M1 5h12m0 0L9 1m4 4L9 9' : 'M13 5H1m0 0 4 4M1 5l4-4'}
477
- ></path>
478
- </svg>
479
- </ToolbarButton>
480
- {/snippet}
481
-
482
- {#snippet yearNavButton(forward: boolean)}
483
- <ToolbarButton
484
- color="dark"
485
- onclick={() => changeYear(forward ? 1 : -1)}
486
- size="lg"
487
- aria-label={forward ? 'Next year' : 'Previous year'}
488
- >
489
- <svg
490
- class="h-3 w-3 rtl:rotate-180"
491
- aria-hidden="true"
492
- xmlns="http://www.w3.org/2000/svg"
493
- fill="none"
494
- viewBox="0 0 14 10"
495
- >
496
- <path
497
- stroke="currentColor"
498
- stroke-linecap="round"
499
- stroke-linejoin="round"
500
- stroke-width="2"
501
- d={forward ? 'M1 5h12m0 0L9 1m4 4L9 9' : 'M13 5H1m0 0 4 4M1 5l4-4'}
502
- ></path>
503
- </svg>
504
- </ToolbarButton>
505
- {/snippet}
506
-
507
- <div bind:this={datepickerContainerElement} class={['relative', inline && 'inline-block']}>
508
- {#if !inline}
509
- <div class="relative">
510
- <input
511
- bind:this={inputElement}
512
- type="text"
513
- class={input({ color, class: clsx(theme?.input, inputClass) })}
514
- {placeholder}
515
- value={range && rangeFrom
516
- ? `${formatDate(rangeFrom)} - ${formatDate(rangeTo)}`
517
- : formatDate(value)}
518
- onfocus={() => (isOpen = true)}
519
- onchange={handleInputChangeWithDateFns}
520
- onkeydown={handleInputKeydown}
521
- {disabled}
522
- {required}
523
- {inputmode}
524
- aria-haspopup="dialog"
525
- {name}
526
- />
527
- <button
528
- type="button"
529
- class={button({ class: clsx(btnClass, theme?.button, classes?.button) })}
530
- onclick={() => (isOpen = !isOpen)}
531
- {disabled}
532
- aria-label={isOpen ? 'Close date picker' : 'Open date picker'}
533
- >
534
- <svg
535
- class="h-4 w-4 text-gray-500 dark:text-gray-400"
536
- aria-hidden="true"
537
- xmlns="http://www.w3.org/2000/svg"
538
- fill="currentColor"
539
- viewBox="0 0 20 20"
540
- >
541
- <path
542
- d="M20 4a2 2 0 0 0-2-2h-2V1a1 1 0 0 0-2 0v1h-3V1a1 1 0 0 0-2 0v1H6V1a1 1 0 0 0-2 0v1H2a2 2 0 0 0-2 2v2h20V4ZM0 18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8H0v10Zm5-8h10a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2Z"
543
- ></path>
544
- </svg>
545
- </button>
546
- </div>
547
- {/if}
548
-
549
- {#if isOpen || inline}
550
- <div
551
- bind:this={calendarRef}
552
- id="datepicker-dropdown"
553
- class={base({ inline, class: clsx(theme?.base, className) })}
554
- transition:fade={{ duration: 100 }}
555
- role="dialog"
556
- aria-label="Calendar"
557
- >
558
- {#if title}
559
- <h2 class={titleVariant({ class: clsx(theme?.titleVariant, classes?.titleVariant) })}>
560
- {title}
561
- </h2>
562
- {/if}
563
-
564
- {#if showMonthSelector}
565
- <!-- Month/Year Selector View -->
566
- <div class={nav({ class: clsx(theme?.nav, classes?.nav) })}>
567
- {@render yearNavButton(false)}
568
- <h3 class={polite({ class: clsx(theme?.polite, classes?.polite) })} aria-live="polite">
569
- {currentMonth.getFullYear()}
570
- </h3>
571
- {@render yearNavButton(true)}
572
- </div>
573
- <div class="grid grid-cols-4 gap-2 p-4">
574
- {#each monthNames as month, index}
575
- <Button
576
- type="button"
577
- color={monthColor}
578
- class={monthButton({
579
- class: clsx(
580
- currentMonth.getMonth() === index ? monthBtnSelected : monthBtn,
581
- classes?.monthButton,
582
- theme?.monthButton
583
- )
584
- })}
585
- onclick={(event: MouseEvent) => selectMonth(index, event)}
586
- >
587
- {month}
588
- </Button>
589
- {/each}
590
- </div>
591
- {:else}
592
- <div class={nav({ class: clsx(classes?.nav) })}>
593
- {@render navButton(false)}
594
- <Button
595
- type="button"
596
- class={polite({
597
- class: clsx(
598
- 'cursor-pointer rounded px-2 py-1 hover:bg-gray-100 dark:hover:bg-gray-700',
599
- classes?.polite
600
- )
601
- })}
602
- aria-live="polite"
603
- onclick={(event: MouseEvent) => toggleMonthSelector(event)}
604
- >
605
- {currentMonth.toLocaleString(translationLocale, { month: 'long', year: 'numeric' })}
606
- </Button>
607
- {@render navButton(true)}
608
- </div>
609
- <div class={grid({ class: clsx(theme?.grid, classes?.grid) })} role="grid">
610
- {#each weekdays as day}
611
- <div
612
- class={columnHeader({ class: clsx(theme?.columnHeader, classes?.columnHeader) })}
613
- role="columnheader"
614
- >
615
- {day}
616
- </div>
617
- {/each}
618
- {#each daysInMonth as day}
619
- {@const current = day.getMonth() !== currentMonth.getMonth()}
620
- {@const available = isDateAvailable(day)}
621
- <Button
622
- type="button"
623
- color={isSelected(day) ? color : 'alternative'}
624
- class={dayButton({
625
- current,
626
- today: isToday(day),
627
- color: isInRange(day) ? color : undefined,
628
- unavailable: !available,
629
- class: clsx(
630
- theme?.dayButton,
631
- classes?.dayButton,
632
- !available && 'cursor-not-allowed opacity-50'
633
- )
634
- })}
635
- onclick={() => handleDaySelect(day)}
636
- onkeydown={handleCalendarKeydown}
637
- aria-label={day.toLocaleDateString(translationLocale, {
638
- weekday: 'long',
639
- year: 'numeric',
640
- month: 'long',
641
- day: 'numeric'
642
- })}
643
- aria-selected={isSelected(day)}
644
- aria-disabled={!available}
645
- disabled={!available}
646
- role="gridcell"
647
- >
648
- {day.getDate()}
649
- </Button>
650
- {/each}
651
- </div>
652
- {/if}
653
-
654
- {#if showActionButtons && !showMonthSelector}
655
- <div class={actionButtons({ class: clsx(theme?.actionButtons, classes?.actionButtons) })}>
656
- <Button
657
- onclick={() => handleDaySelect(new Date())}
658
- {color}
659
- size="sm"
660
- disabled={!isDateAvailable(new Date())}>Today</Button
661
- >
662
- <Button onclick={handleClear} color="red" size="sm">Clear</Button>
663
- <Button onclick={handleApply} {color} size="sm">Apply</Button>
664
- </div>
665
- {/if}
666
- </div>
667
- {/if}
668
- </div>
669
-
670
- <!--
671
- @component
672
- [Go to docs](https://flowbite-svelte.com/)
673
- ## Type
674
- [DatepickerProps](https://github.com/themesberg/flowbite-svelte/blob/main/src/lib/types.ts#L449)
675
- ## Props
676
- @prop value = $bindable()
677
- @prop defaultDate = null
678
- @prop range = false
679
- @prop rangeFrom = $bindable()
680
- @prop rangeTo = $bindable()
681
- @prop availableFrom = null
682
- @prop availableTo = null
683
- @prop locale = "default"
684
- @prop translationLocale = locale
685
- @prop firstDayOfWeek = 0
686
- @prop dateFormat
687
- @prop placeholder = "Select date"
688
- @prop disabled = false
689
- @prop required = false
690
- @prop inputClass = ""
691
- @prop color = "primary"
692
- @prop inline = false
693
- @prop autohide = true
694
- @prop showActionButtons = false
695
- @prop title = ""
696
- @prop onselect
697
- @prop onclear
698
- @prop onapply
699
- @prop btnClass
700
- @prop inputmode = "none"
701
- @prop classes
702
- @prop monthColor = "alternative"
703
- @prop monthBtnSelected = "bg-primary-500 text-white"
704
- @prop monthBtn = "text-gray-700 dark:text-gray-300"
705
- @prop class: className
706
- @prop elementRef = $bindable()
707
- -->
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import { fade } from 'svelte/transition';
4
+ import clsx from 'clsx';
5
+ import { datepicker } from './index.js';
6
+ import {
7
+ parse,
8
+ isValid,
9
+ addDays,
10
+ startOfMonth,
11
+ endOfMonth,
12
+ startOfWeek,
13
+ endOfWeek,
14
+ eachDayOfInterval,
15
+ isSameDay,
16
+ isWithinInterval
17
+ } from 'date-fns';
18
+ import { Button, getTheme, ToolbarButton, type DatepickerProps } from '../../index.js';
19
+
20
+ let {
21
+ value = $bindable(),
22
+ defaultDate = null,
23
+ range = false,
24
+ rangeFrom = $bindable(),
25
+ rangeTo = $bindable(),
26
+ availableFrom = null,
27
+ availableTo = null,
28
+ locale = 'default',
29
+ translationLocale = locale,
30
+ firstDayOfWeek = 0,
31
+ dateFormat,
32
+ placeholder = 'Select date',
33
+ disabled = false,
34
+ required = false,
35
+ inputClass = '',
36
+ color = 'primary',
37
+ inline = false,
38
+ autohide = true,
39
+ showActionButtons = false,
40
+ title = '',
41
+ onselect,
42
+ onclear,
43
+ onapply,
44
+ btnClass,
45
+ inputmode = 'none',
46
+ classes,
47
+ monthColor = 'alternative',
48
+ monthBtnSelected = 'bg-primary-500 text-white',
49
+ monthBtn = 'text-gray-700 dark:text-gray-300',
50
+ class: className,
51
+ elementRef = $bindable(),
52
+ name,
53
+ // New placement prop
54
+ placement = 'bottom-start'
55
+ }: DatepickerProps = $props();
56
+
57
+ const theme = getTheme('datepicker');
58
+
59
+ // If translationLocale is not explicitly provided, it will default to the value of locale. This ensures reactivity as both are directly exposed as props.
60
+ translationLocale = translationLocale ?? locale;
61
+
62
+ let isOpen: boolean = $state(inline);
63
+ let showMonthSelector: boolean = $state(false);
64
+ let inputElement: HTMLInputElement | null = $state(null);
65
+
66
+ $effect(() => {
67
+ if (inputElement) {
68
+ elementRef = inputElement;
69
+ }
70
+ });
71
+ let datepickerContainerElement: HTMLDivElement;
72
+ let currentMonth: Date = $state(value || defaultDate || new Date());
73
+ let focusedDate: Date | null = null;
74
+ let calendarRef: HTMLDivElement | null = $state(null);
75
+
76
+ let daysInMonth = $derived(getDaysInMonth(currentMonth));
77
+
78
+ onMount(() => {
79
+ if (!inline) {
80
+ datepickerContainerElement?.ownerDocument.addEventListener('click', handleClickOutside);
81
+ return () => {
82
+ datepickerContainerElement?.ownerDocument.removeEventListener('click', handleClickOutside);
83
+ };
84
+ }
85
+ });
86
+
87
+ function getDaysInMonth(date: Date): Date[] {
88
+ const monthStart = startOfMonth(date);
89
+ const monthEnd = endOfMonth(date);
90
+ const calendarStart = startOfWeek(monthStart, { weekStartsOn: firstDayOfWeek });
91
+ const calendarEnd = endOfWeek(monthEnd, { weekStartsOn: firstDayOfWeek });
92
+
93
+ return eachDayOfInterval({ start: calendarStart, end: calendarEnd });
94
+ }
95
+
96
+ const getWeekdayNames = (): string[] => {
97
+ const referenceDate = new Date(1970, 0, 4 + firstDayOfWeek);
98
+ return Array.from({ length: 7 }, (_, i) =>
99
+ addDays(referenceDate, i).toLocaleDateString(translationLocale, { weekday: 'short' })
100
+ );
101
+ };
102
+
103
+ let weekdays = $derived(getWeekdayNames());
104
+
105
+ const getMonthNames = (): string[] => {
106
+ return Array.from({ length: 12 }, (_, i) =>
107
+ new Date(2000, i, 1).toLocaleDateString(translationLocale, { month: 'short' })
108
+ );
109
+ };
110
+ let monthNames = $derived(getMonthNames());
111
+
112
+ const addDay = (date: Date, increment: number): Date => addDays(date, increment);
113
+
114
+ function changeMonth(increment: number) {
115
+ currentMonth = new Date(currentMonth.getFullYear(), currentMonth.getMonth() + increment, 1);
116
+ }
117
+
118
+ function changeYear(increment: number) {
119
+ currentMonth = new Date(currentMonth.getFullYear() + increment, currentMonth.getMonth(), 1);
120
+ }
121
+
122
+ function selectMonth(monthIndex: number, event: MouseEvent) {
123
+ event.stopPropagation();
124
+ currentMonth = new Date(currentMonth.getFullYear(), monthIndex, 1);
125
+ showMonthSelector = false;
126
+ }
127
+
128
+ function toggleMonthSelector(event: MouseEvent) {
129
+ event.stopPropagation();
130
+ showMonthSelector = !showMonthSelector;
131
+ }
132
+
133
+ function isDateAvailable(date: Date): boolean {
134
+ const dateOnly = new Date(date.getFullYear(), date.getMonth(), date.getDate());
135
+
136
+ if (availableFrom) {
137
+ const fromDate = new Date(
138
+ availableFrom.getFullYear(),
139
+ availableFrom.getMonth(),
140
+ availableFrom.getDate()
141
+ );
142
+ if (dateOnly < fromDate) return false;
143
+ }
144
+
145
+ if (availableTo) {
146
+ const toDate = new Date(
147
+ availableTo.getFullYear(),
148
+ availableTo.getMonth(),
149
+ availableTo.getDate()
150
+ );
151
+ if (dateOnly > toDate) return false;
152
+ }
153
+
154
+ return true;
155
+ }
156
+
157
+ function handleDaySelect(day: Date) {
158
+ if (!isDateAvailable(day)) return;
159
+
160
+ if (range) {
161
+ if (!rangeFrom || (rangeFrom && rangeTo)) {
162
+ rangeFrom = day;
163
+ rangeTo = undefined;
164
+ } else if (day < rangeFrom) {
165
+ rangeFrom = day;
166
+ rangeTo = rangeFrom;
167
+ } else {
168
+ rangeTo = day;
169
+ }
170
+ onselect?.({ from: rangeFrom, to: rangeTo });
171
+ } else {
172
+ value = day;
173
+ onselect?.(value);
174
+ if (autohide && !inline) isOpen = false;
175
+ }
176
+ }
177
+
178
+ function handleInputChangeWithDateFns() {
179
+ const inputValue = inputElement?.value?.trim();
180
+ if (!inputValue) {
181
+ rangeFrom = undefined;
182
+ rangeTo = undefined;
183
+ inputElement?.setCustomValidity('');
184
+ return;
185
+ }
186
+
187
+ inputElement?.setCustomValidity('');
188
+
189
+ if (range) {
190
+ const parts = inputValue.split(' - ');
191
+ if (parts.length === 2) {
192
+ const parsedFrom = tryParseDate(parts[0]);
193
+ const parsedTo = tryParseDate(parts[1]);
194
+
195
+ if (
196
+ parsedFrom &&
197
+ isValid(parsedFrom) &&
198
+ isDateAvailable(parsedFrom) &&
199
+ parsedTo &&
200
+ isValid(parsedTo) &&
201
+ isDateAvailable(parsedTo)
202
+ ) {
203
+ [rangeFrom, rangeTo] =
204
+ parsedFrom > parsedTo ? [parsedTo, parsedFrom] : [parsedFrom, parsedTo];
205
+ onselect?.({ from: rangeFrom, to: rangeTo });
206
+ return;
207
+ } else {
208
+ inputElement?.setCustomValidity(
209
+ `Please enter date range in format: ${getDateFormatPattern()} - ${getDateFormatPattern()}`
210
+ );
211
+ return;
212
+ }
213
+ }
214
+ }
215
+
216
+ const parsedDate = tryParseDate(inputValue);
217
+
218
+ if (!parsedDate || !isValid(parsedDate)) {
219
+ const formatPattern = getDateFormatPattern();
220
+ inputElement?.setCustomValidity(`Please enter date in format: ${formatPattern}`);
221
+ return;
222
+ }
223
+
224
+ if (!isDateAvailable(parsedDate)) {
225
+ inputElement?.setCustomValidity('Selected date is not available');
226
+ return;
227
+ }
228
+
229
+ handleDaySelect(parsedDate);
230
+ }
231
+
232
+ function tryParseDate(inputValue: string): Date | null {
233
+ const formatPattern = getDateFormatPattern();
234
+ try {
235
+ const parsedDate = parse(inputValue, formatPattern, new Date());
236
+ if (isValid(parsedDate)) {
237
+ return parsedDate;
238
+ }
239
+ } catch (error) {
240
+ // Continue to next strategy
241
+ }
242
+
243
+ const commonFormats = [
244
+ 'd.M.yyyy', // German: 17.7.2025
245
+ 'dd.MM.yyyy', // German: 17.07.2025
246
+ 'M/d/yyyy', // US: 7/17/2025
247
+ 'MM/dd/yyyy', // US: 07/17/2025
248
+ 'd/M/yyyy', // UK: 17/7/2025
249
+ 'dd/MM/yyyy', // UK: 17/07/2025
250
+ 'yyyy-MM-dd', // ISO: 2025-07-17
251
+ 'yyyy-M-d', // ISO: 2025-7-17
252
+ 'M-d-yyyy', // US with dashes: 7-17-2025
253
+ 'd-M-yyyy' // EU with dashes: 17-7-2025
254
+ ];
255
+
256
+ for (const format of commonFormats) {
257
+ try {
258
+ const parsedDate = parse(inputValue, format, new Date());
259
+ if (isValid(parsedDate)) {
260
+ return parsedDate;
261
+ }
262
+ } catch (error) {
263
+ // Continue to next format
264
+ }
265
+ }
266
+
267
+ try {
268
+ const nativeDate = new Date(inputValue);
269
+ if (isValid(nativeDate) && !isNaN(nativeDate.getTime())) {
270
+ return nativeDate;
271
+ }
272
+ } catch (error) {
273
+ // Continue
274
+ }
275
+
276
+ return null;
277
+ }
278
+
279
+ function getDateFormatPattern(): string {
280
+ const actualLocale = locale === 'default' ? navigator.language : locale;
281
+ const testDate = new Date(2025, 0, 15); // January 15, 2025
282
+ const formatted = testDate.toLocaleDateString(
283
+ actualLocale,
284
+ dateFormat || { year: 'numeric', month: 'numeric', day: 'numeric' }
285
+ );
286
+
287
+ if (formatted.includes('.')) {
288
+ // German/European format with dots
289
+ if (formatted.startsWith('15.')) {
290
+ return 'd.M.yyyy';
291
+ } else if (formatted.startsWith('01.')) {
292
+ return 'M.d.yyyy';
293
+ }
294
+ return 'd.M.yyyy'; // Default to day first
295
+ } else if (formatted.includes('/')) {
296
+ // US/UK format with slashes
297
+ if (formatted.startsWith('1/')) {
298
+ return 'M/d/yyyy'; // US format
299
+ } else if (formatted.startsWith('15/')) {
300
+ return 'd/M/yyyy'; // UK format
301
+ }
302
+
303
+ const testDate2 = new Date(2025, 11, 3); // December 3, 2025
304
+ const formatted2 = testDate2.toLocaleDateString(
305
+ actualLocale,
306
+ dateFormat || { year: 'numeric', month: 'numeric', day: 'numeric' }
307
+ );
308
+ if (formatted2.startsWith('3/') || formatted2.startsWith('03/')) {
309
+ return 'd/M/yyyy';
310
+ } else {
311
+ return 'M/d/yyyy';
312
+ }
313
+ } else if (formatted.includes('-')) {
314
+ // ISO or other dash format
315
+ if (formatted.startsWith('2025-')) {
316
+ return 'yyyy-M-d';
317
+ } else if (formatted.startsWith('1-')) {
318
+ return 'M-d-yyyy';
319
+ } else {
320
+ return 'd-M-yyyy';
321
+ }
322
+ }
323
+
324
+ // Default fallback - try to detect based on locale
325
+ if (actualLocale.startsWith('en-US')) {
326
+ return 'M/d/yyyy';
327
+ } else if (
328
+ actualLocale.startsWith('de') ||
329
+ actualLocale.startsWith('at') ||
330
+ actualLocale.startsWith('ch')
331
+ ) {
332
+ return 'd.M.yyyy';
333
+ } else if (actualLocale.startsWith('en-GB') || actualLocale.startsWith('en-AU')) {
334
+ return 'd/M/yyyy';
335
+ }
336
+
337
+ return 'M/d/yyyy';
338
+ }
339
+
340
+ function handleClickOutside(event: MouseEvent) {
341
+ if (
342
+ isOpen &&
343
+ datepickerContainerElement &&
344
+ !datepickerContainerElement.contains(event.target as Node)
345
+ ) {
346
+ isOpen = false;
347
+ showMonthSelector = false;
348
+ }
349
+ }
350
+
351
+ // Use locale for formatting (not translationLocale)
352
+ const formatDate = (date?: Date): string => date?.toLocaleDateString(locale, dateFormat) ?? '';
353
+ const isSameDate = (date1?: Date, date2?: Date): boolean =>
354
+ date1 && date2 ? isSameDay(date1, date2) : false;
355
+ const isToday = (day: Date): boolean => isSameDate(day, new Date());
356
+ const isInRange = (day: Date): boolean =>
357
+ !!(range && rangeFrom && rangeTo && isWithinInterval(day, { start: rangeFrom, end: rangeTo }));
358
+
359
+ let isSelected = $derived((day: Date): boolean =>
360
+ range ? isSameDate(day, rangeFrom) || isSameDate(day, rangeTo) : isSameDate(day, value)
361
+ );
362
+
363
+ function handleCalendarKeydown(event: KeyboardEvent) {
364
+ if (!isOpen) return;
365
+
366
+ if (!focusedDate) {
367
+ focusedDate = value || new Date();
368
+ }
369
+
370
+ switch (event.key) {
371
+ case 'ArrowLeft':
372
+ focusedDate = addDay(focusedDate, -1);
373
+ break;
374
+ case 'ArrowRight':
375
+ focusedDate = addDay(focusedDate, 1);
376
+ break;
377
+ case 'ArrowUp':
378
+ focusedDate = addDay(focusedDate, -7);
379
+ break;
380
+ case 'ArrowDown':
381
+ focusedDate = addDay(focusedDate, 7);
382
+ break;
383
+ case 'Enter':
384
+ if (range) {
385
+ if (rangeFrom && rangeTo) {
386
+ if (autohide && !inline) isOpen = false;
387
+ } else {
388
+ handleDaySelect(focusedDate);
389
+ }
390
+ } else {
391
+ handleDaySelect(focusedDate);
392
+ if (autohide && !inline) isOpen = false;
393
+ }
394
+ break;
395
+ case 'Escape':
396
+ isOpen = false;
397
+ showMonthSelector = false;
398
+ inputElement?.focus();
399
+ break;
400
+ default:
401
+ return;
402
+ }
403
+
404
+ event.preventDefault();
405
+ if (focusedDate.getMonth() !== currentMonth.getMonth()) {
406
+ currentMonth = new Date(focusedDate.getFullYear(), focusedDate.getMonth(), 1);
407
+ }
408
+
409
+ // Use translationLocale for aria-label
410
+ setTimeout(() => {
411
+ const focusedButton = calendarRef?.querySelector(
412
+ `button[aria-label="${focusedDate!.toLocaleDateString(translationLocale, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}"]`
413
+ ) as HTMLButtonElement | null;
414
+ focusedButton?.focus();
415
+ }, 0);
416
+ }
417
+
418
+ function handleInputKeydown(event: KeyboardEvent) {
419
+ if (event.key === 'Enter') {
420
+ event.preventDefault();
421
+ handleInputChangeWithDateFns();
422
+ if (autohide && !inline) {
423
+ isOpen = false;
424
+ }
425
+ } else if (event.key === ' ') {
426
+ event.preventDefault();
427
+ isOpen = !isOpen;
428
+ }
429
+ }
430
+
431
+ function handleClear() {
432
+ value = rangeFrom = rangeTo = undefined;
433
+ onclear?.();
434
+ }
435
+
436
+ function handleApply() {
437
+ const result = range ? { from: rangeFrom, to: rangeTo } : value;
438
+ if (result) onapply?.(result);
439
+ if (!inline) isOpen = false;
440
+ }
441
+
442
+ let {
443
+ base,
444
+ input,
445
+ button,
446
+ titleVariant,
447
+ actionButtons,
448
+ columnHeader,
449
+ polite,
450
+ grid,
451
+ nav,
452
+ dayButton,
453
+ monthButton
454
+ } = datepicker({ placement });
455
+ </script>
456
+
457
+ {#snippet navButton(forward: boolean)}
458
+ <ToolbarButton
459
+ color="dark"
460
+ onclick={() => changeMonth(forward ? 1 : -1)}
461
+ size="lg"
462
+ aria-label={forward ? 'Next month' : 'Previous month'}
463
+ >
464
+ <svg
465
+ class="h-3 w-3 rtl:rotate-180"
466
+ aria-hidden="true"
467
+ xmlns="http://www.w3.org/2000/svg"
468
+ fill="none"
469
+ viewBox="0 0 14 10"
470
+ >
471
+ <path
472
+ stroke="currentColor"
473
+ stroke-linecap="round"
474
+ stroke-linejoin="round"
475
+ stroke-width="2"
476
+ d={forward ? 'M1 5h12m0 0L9 1m4 4L9 9' : 'M13 5H1m0 0 4 4M1 5l4-4'}
477
+ ></path>
478
+ </svg>
479
+ </ToolbarButton>
480
+ {/snippet}
481
+
482
+ {#snippet yearNavButton(forward: boolean)}
483
+ <ToolbarButton
484
+ color="dark"
485
+ onclick={() => changeYear(forward ? 1 : -1)}
486
+ size="lg"
487
+ aria-label={forward ? 'Next year' : 'Previous year'}
488
+ >
489
+ <svg
490
+ class="h-3 w-3 rtl:rotate-180"
491
+ aria-hidden="true"
492
+ xmlns="http://www.w3.org/2000/svg"
493
+ fill="none"
494
+ viewBox="0 0 14 10"
495
+ >
496
+ <path
497
+ stroke="currentColor"
498
+ stroke-linecap="round"
499
+ stroke-linejoin="round"
500
+ stroke-width="2"
501
+ d={forward ? 'M1 5h12m0 0L9 1m4 4L9 9' : 'M13 5H1m0 0 4 4M1 5l4-4'}
502
+ ></path>
503
+ </svg>
504
+ </ToolbarButton>
505
+ {/snippet}
506
+
507
+ <div bind:this={datepickerContainerElement} class={['relative', inline && 'inline-block']}>
508
+ {#if !inline}
509
+ <div class="relative">
510
+ <input
511
+ bind:this={inputElement}
512
+ type="text"
513
+ class={input({ color, class: clsx(theme?.input, inputClass) })}
514
+ {placeholder}
515
+ value={range && rangeFrom
516
+ ? `${formatDate(rangeFrom)} - ${formatDate(rangeTo)}`
517
+ : formatDate(value)}
518
+ onfocus={() => (isOpen = true)}
519
+ onchange={handleInputChangeWithDateFns}
520
+ onkeydown={handleInputKeydown}
521
+ {disabled}
522
+ {required}
523
+ {inputmode}
524
+ aria-haspopup="dialog"
525
+ {name}
526
+ />
527
+ <button
528
+ type="button"
529
+ class={button({ class: clsx(btnClass, theme?.button, classes?.button) })}
530
+ onclick={() => (isOpen = !isOpen)}
531
+ {disabled}
532
+ aria-label={isOpen ? 'Close date picker' : 'Open date picker'}
533
+ >
534
+ <svg
535
+ class="h-4 w-4 text-gray-500 dark:text-gray-400"
536
+ aria-hidden="true"
537
+ xmlns="http://www.w3.org/2000/svg"
538
+ fill="currentColor"
539
+ viewBox="0 0 20 20"
540
+ >
541
+ <path
542
+ d="M20 4a2 2 0 0 0-2-2h-2V1a1 1 0 0 0-2 0v1h-3V1a1 1 0 0 0-2 0v1H6V1a1 1 0 0 0-2 0v1H2a2 2 0 0 0-2 2v2h20V4ZM0 18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8H0v10Zm5-8h10a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2Z"
543
+ ></path>
544
+ </svg>
545
+ </button>
546
+ </div>
547
+ {/if}
548
+
549
+ {#if isOpen || inline}
550
+ <div
551
+ bind:this={calendarRef}
552
+ id="datepicker-dropdown"
553
+ class={base({ inline, class: clsx(theme?.base, className) })}
554
+ transition:fade={{ duration: 100 }}
555
+ role="dialog"
556
+ aria-label="Calendar"
557
+ >
558
+ {#if title}
559
+ <h2 class={titleVariant({ class: clsx(theme?.titleVariant, classes?.titleVariant) })}>
560
+ {title}
561
+ </h2>
562
+ {/if}
563
+
564
+ {#if showMonthSelector}
565
+ <!-- Month/Year Selector View -->
566
+ <div class={nav({ class: clsx(theme?.nav, classes?.nav) })}>
567
+ {@render yearNavButton(false)}
568
+ <h3 class={polite({ class: clsx(theme?.polite, classes?.polite) })} aria-live="polite">
569
+ {currentMonth.getFullYear()}
570
+ </h3>
571
+ {@render yearNavButton(true)}
572
+ </div>
573
+ <div class="grid grid-cols-4 gap-2 p-4">
574
+ {#each monthNames as month, index}
575
+ <Button
576
+ type="button"
577
+ color={monthColor}
578
+ class={monthButton({
579
+ class: clsx(
580
+ currentMonth.getMonth() === index ? monthBtnSelected : monthBtn,
581
+ classes?.monthButton,
582
+ theme?.monthButton
583
+ )
584
+ })}
585
+ onclick={(event: MouseEvent) => selectMonth(index, event)}
586
+ >
587
+ {month}
588
+ </Button>
589
+ {/each}
590
+ </div>
591
+ {:else}
592
+ <div class={nav({ class: clsx(classes?.nav) })}>
593
+ {@render navButton(false)}
594
+ <Button
595
+ type="button"
596
+ class={polite({
597
+ class: clsx(
598
+ 'cursor-pointer rounded px-2 py-1 hover:bg-gray-100 dark:hover:bg-gray-700',
599
+ classes?.polite
600
+ )
601
+ })}
602
+ aria-live="polite"
603
+ onclick={(event: MouseEvent) => toggleMonthSelector(event)}
604
+ >
605
+ {currentMonth.toLocaleString(translationLocale, { month: 'long', year: 'numeric' })}
606
+ </Button>
607
+ {@render navButton(true)}
608
+ </div>
609
+ <div class={grid({ class: clsx(theme?.grid, classes?.grid) })} role="grid">
610
+ {#each weekdays as day}
611
+ <div
612
+ class={columnHeader({ class: clsx(theme?.columnHeader, classes?.columnHeader) })}
613
+ role="columnheader"
614
+ >
615
+ {day}
616
+ </div>
617
+ {/each}
618
+ {#each daysInMonth as day}
619
+ {@const current = day.getMonth() !== currentMonth.getMonth()}
620
+ {@const available = isDateAvailable(day)}
621
+ <Button
622
+ type="button"
623
+ color={isSelected(day) ? color : 'alternative'}
624
+ class={dayButton({
625
+ current,
626
+ today: isToday(day),
627
+ color: isInRange(day) ? color : undefined,
628
+ unavailable: !available,
629
+ class: clsx(
630
+ theme?.dayButton,
631
+ classes?.dayButton,
632
+ !available && 'cursor-not-allowed opacity-50'
633
+ )
634
+ })}
635
+ onclick={() => handleDaySelect(day)}
636
+ onkeydown={handleCalendarKeydown}
637
+ aria-label={day.toLocaleDateString(translationLocale, {
638
+ weekday: 'long',
639
+ year: 'numeric',
640
+ month: 'long',
641
+ day: 'numeric'
642
+ })}
643
+ aria-selected={isSelected(day)}
644
+ aria-disabled={!available}
645
+ disabled={!available}
646
+ role="gridcell"
647
+ >
648
+ {day.getDate()}
649
+ </Button>
650
+ {/each}
651
+ </div>
652
+ {/if}
653
+
654
+ {#if showActionButtons && !showMonthSelector}
655
+ <div class={actionButtons({ class: clsx(theme?.actionButtons, classes?.actionButtons) })}>
656
+ <Button
657
+ onclick={() => handleDaySelect(new Date())}
658
+ {color}
659
+ size="sm"
660
+ disabled={!isDateAvailable(new Date())}>Today</Button
661
+ >
662
+ <Button onclick={handleClear} color="red" size="sm">Clear</Button>
663
+ <Button onclick={handleApply} {color} size="sm">Apply</Button>
664
+ </div>
665
+ {/if}
666
+ </div>
667
+ {/if}
668
+ </div>
669
+
670
+ <!--
671
+ @component
672
+ [Go to docs](https://flowbite-svelte.com/)
673
+ ## Type
674
+ [DatepickerProps](https://github.com/themesberg/flowbite-svelte/blob/main/src/lib/types.ts#L449)
675
+ ## Props
676
+ @prop value = $bindable()
677
+ @prop defaultDate = null
678
+ @prop range = false
679
+ @prop rangeFrom = $bindable()
680
+ @prop rangeTo = $bindable()
681
+ @prop availableFrom = null
682
+ @prop availableTo = null
683
+ @prop locale = "default"
684
+ @prop translationLocale = locale
685
+ @prop firstDayOfWeek = 0
686
+ @prop dateFormat
687
+ @prop placeholder = "Select date"
688
+ @prop disabled = false
689
+ @prop required = false
690
+ @prop inputClass = ""
691
+ @prop color = "primary"
692
+ @prop inline = false
693
+ @prop autohide = true
694
+ @prop showActionButtons = false
695
+ @prop title = ""
696
+ @prop onselect
697
+ @prop onclear
698
+ @prop onapply
699
+ @prop btnClass
700
+ @prop inputmode = "none"
701
+ @prop classes
702
+ @prop monthColor = "alternative"
703
+ @prop monthBtnSelected = "bg-primary-500 text-white"
704
+ @prop monthBtn = "text-gray-700 dark:text-gray-300"
705
+ @prop class: className
706
+ @prop elementRef = $bindable()
707
+ -->