@14ch/svelte-ui 0.0.1

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 (109) hide show
  1. package/README.md +359 -0
  2. package/dist/assets/styles/README.md +144 -0
  3. package/dist/assets/styles/core.scss +61 -0
  4. package/dist/assets/styles/import.scss +11 -0
  5. package/dist/assets/styles/optional/fonts.scss +23 -0
  6. package/dist/assets/styles/optional/reset.scss +230 -0
  7. package/dist/assets/styles/variables.scss +805 -0
  8. package/dist/components/Button.svelte +574 -0
  9. package/dist/components/Button.svelte.d.ts +56 -0
  10. package/dist/components/COMPONENT_DESIGN_GUIDELINES.md +127 -0
  11. package/dist/components/Checkbox.svelte +523 -0
  12. package/dist/components/Checkbox.svelte.d.ts +42 -0
  13. package/dist/components/CheckboxGroup.svelte +82 -0
  14. package/dist/components/CheckboxGroup.svelte.d.ts +13 -0
  15. package/dist/components/ColorPicker.svelte +496 -0
  16. package/dist/components/ColorPicker.svelte.d.ts +45 -0
  17. package/dist/components/Combobox.svelte +576 -0
  18. package/dist/components/Combobox.svelte.d.ts +52 -0
  19. package/dist/components/ConfirmDialog.svelte +116 -0
  20. package/dist/components/ConfirmDialog.svelte.d.ts +20 -0
  21. package/dist/components/Datepicker.svelte +578 -0
  22. package/dist/components/Datepicker.svelte.d.ts +72 -0
  23. package/dist/components/DatepickerCalendar.svelte +925 -0
  24. package/dist/components/DatepickerCalendar.svelte.d.ts +31 -0
  25. package/dist/components/Dialog.svelte +245 -0
  26. package/dist/components/Dialog.svelte.d.ts +38 -0
  27. package/dist/components/Drawer.svelte +383 -0
  28. package/dist/components/Drawer.svelte.d.ts +39 -0
  29. package/dist/components/Fab.svelte +486 -0
  30. package/dist/components/Fab.svelte.d.ts +51 -0
  31. package/dist/components/FileUploader.svelte +456 -0
  32. package/dist/components/FileUploader.svelte.d.ts +36 -0
  33. package/dist/components/Icon.svelte +167 -0
  34. package/dist/components/Icon.svelte.d.ts +21 -0
  35. package/dist/components/IconButton.svelte +557 -0
  36. package/dist/components/IconButton.svelte.d.ts +60 -0
  37. package/dist/components/ImageUploader.svelte +516 -0
  38. package/dist/components/ImageUploader.svelte.d.ts +37 -0
  39. package/dist/components/ImageUploaderPreview.svelte +157 -0
  40. package/dist/components/ImageUploaderPreview.svelte.d.ts +13 -0
  41. package/dist/components/Input.svelte +885 -0
  42. package/dist/components/Input.svelte.d.ts +75 -0
  43. package/dist/components/LoadingSpinner.svelte +116 -0
  44. package/dist/components/LoadingSpinner.svelte.d.ts +10 -0
  45. package/dist/components/Modal.svelte +313 -0
  46. package/dist/components/Modal.svelte.d.ts +34 -0
  47. package/dist/components/Pagination.svelte +276 -0
  48. package/dist/components/Pagination.svelte.d.ts +14 -0
  49. package/dist/components/Popup.svelte +676 -0
  50. package/dist/components/Popup.svelte.d.ts +40 -0
  51. package/dist/components/PopupMenu.svelte +421 -0
  52. package/dist/components/PopupMenu.svelte.d.ts +24 -0
  53. package/dist/components/PopupMenuButton.svelte +365 -0
  54. package/dist/components/PopupMenuButton.svelte.d.ts +42 -0
  55. package/dist/components/Radio.svelte +548 -0
  56. package/dist/components/Radio.svelte.d.ts +42 -0
  57. package/dist/components/RadioGroup.svelte +74 -0
  58. package/dist/components/RadioGroup.svelte.d.ts +14 -0
  59. package/dist/components/Select.svelte +479 -0
  60. package/dist/components/Select.svelte.d.ts +47 -0
  61. package/dist/components/Slider.svelte +473 -0
  62. package/dist/components/Slider.svelte.d.ts +46 -0
  63. package/dist/components/Snackbar.svelte +124 -0
  64. package/dist/components/Snackbar.svelte.d.ts +9 -0
  65. package/dist/components/SnackbarItem.svelte +423 -0
  66. package/dist/components/SnackbarItem.svelte.d.ts +21 -0
  67. package/dist/components/Switch.svelte +454 -0
  68. package/dist/components/Switch.svelte.d.ts +40 -0
  69. package/dist/components/Tab.svelte +193 -0
  70. package/dist/components/Tab.svelte.d.ts +14 -0
  71. package/dist/components/TabItem.svelte +140 -0
  72. package/dist/components/TabItem.svelte.d.ts +17 -0
  73. package/dist/components/Textarea.svelte +702 -0
  74. package/dist/components/Textarea.svelte.d.ts +64 -0
  75. package/dist/components/skeleton/Skeleton.svelte +235 -0
  76. package/dist/components/skeleton/Skeleton.svelte.d.ts +13 -0
  77. package/dist/components/skeleton/SkeletonAvatar.svelte +97 -0
  78. package/dist/components/skeleton/SkeletonAvatar.svelte.d.ts +8 -0
  79. package/dist/components/skeleton/SkeletonBox.svelte +105 -0
  80. package/dist/components/skeleton/SkeletonBox.svelte.d.ts +12 -0
  81. package/dist/components/skeleton/SkeletonButton.svelte +71 -0
  82. package/dist/components/skeleton/SkeletonButton.svelte.d.ts +8 -0
  83. package/dist/components/skeleton/SkeletonHeading.svelte +49 -0
  84. package/dist/components/skeleton/SkeletonHeading.svelte.d.ts +8 -0
  85. package/dist/components/skeleton/SkeletonMedia.svelte +115 -0
  86. package/dist/components/skeleton/SkeletonMedia.svelte.d.ts +9 -0
  87. package/dist/components/skeleton/SkeletonText.svelte +75 -0
  88. package/dist/components/skeleton/SkeletonText.svelte.d.ts +8 -0
  89. package/dist/index.d.ts +42 -0
  90. package/dist/index.js +43 -0
  91. package/dist/types/icon.d.ts +4 -0
  92. package/dist/types/icon.js +2 -0
  93. package/dist/types/menuItem.d.ts +8 -0
  94. package/dist/types/menuItem.js +1 -0
  95. package/dist/types/options.d.ts +6 -0
  96. package/dist/types/options.js +4 -0
  97. package/dist/types/skeleton.d.ts +77 -0
  98. package/dist/types/skeleton.js +19 -0
  99. package/dist/utils/accessibility.d.ts +48 -0
  100. package/dist/utils/accessibility.js +87 -0
  101. package/dist/utils/formatText.d.ts +4 -0
  102. package/dist/utils/formatText.js +44 -0
  103. package/dist/utils/mobile.d.ts +9 -0
  104. package/dist/utils/mobile.js +47 -0
  105. package/dist/utils/snackbar.svelte.d.ts +51 -0
  106. package/dist/utils/snackbar.svelte.js +107 -0
  107. package/dist/utils/style.d.ts +17 -0
  108. package/dist/utils/style.js +22 -0
  109. package/package.json +102 -0
@@ -0,0 +1,925 @@
1
+ <!-- DatepickerCalendar.svelte -->
2
+
3
+ <script lang="ts">
4
+ import dayjs from 'dayjs';
5
+ import localeData from 'dayjs/plugin/localeData';
6
+ import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
7
+ import isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
8
+ import 'dayjs/locale/ja';
9
+ import 'dayjs/locale/en';
10
+ import 'dayjs/locale/fr';
11
+ import 'dayjs/locale/de';
12
+ import 'dayjs/locale/es';
13
+ import 'dayjs/locale/zh-cn';
14
+ import IconButton from './IconButton.svelte';
15
+ import { onMount } from 'svelte';
16
+ import { t } from '../i18n';
17
+
18
+ dayjs.extend(localeData);
19
+ dayjs.extend(isSameOrBefore);
20
+ dayjs.extend(isSameOrAfter);
21
+
22
+ // =========================================================================
23
+ // Props, States & Constants
24
+ // =========================================================================
25
+ let {
26
+ // 基本プロパティ
27
+ value = $bindable(),
28
+ locale = 'en',
29
+ minDate,
30
+ maxDate,
31
+
32
+ // HTML属性系
33
+ id,
34
+
35
+ // 状態/動作
36
+ mode = 'single',
37
+
38
+ // 入力イベント
39
+ onchange = () => {}, // No params for type inference
40
+ onOpen,
41
+ onClose
42
+ }: {
43
+ // 基本プロパティ
44
+ value: Date | { start: Date; end: Date } | undefined;
45
+ locale?: 'en' | 'ja' | 'fr' | 'de' | 'es' | 'zh-cn';
46
+ minDate?: Date;
47
+ maxDate?: Date;
48
+
49
+ // HTML属性系
50
+ id?: string;
51
+
52
+ // 状態/動作
53
+ mode?: 'single' | 'range';
54
+
55
+ // 入力イベント
56
+ onchange: (value: Date | { start: Date; end: Date } | undefined) => void;
57
+ onOpen?: Function;
58
+ onClose?: Function;
59
+ } = $props();
60
+
61
+ let month: dayjs.Dayjs = $state(dayjs());
62
+ let viewMode: 'date' | 'month' = $state('date');
63
+ let selectedYearMonth: dayjs.Dayjs = $state(dayjs().startOf('month'));
64
+ let focusedDate: dayjs.Dayjs = $state(dayjs());
65
+ let focusedMonth: number = $state(dayjs().month());
66
+ let isKeyboardActive: boolean = $state(false);
67
+ let isSelectingStart: boolean = $state(true);
68
+ let hoveredDate: dayjs.Dayjs | null = $state(null);
69
+ let calendarRef: HTMLDivElement | undefined = $state();
70
+
71
+ const localeConfig = {
72
+ en: {
73
+ monthFormat: 'MMMM YYYY',
74
+ yearFormat: 'YYYY',
75
+ prevMonthLabel: t('datepicker.prevMonth'),
76
+ nextMonthLabel: t('datepicker.nextMonth'),
77
+ todayLabel: t('datepicker.today'),
78
+ selectedLabel: t('datepicker.selected')
79
+ },
80
+ ja: {
81
+ monthFormat: 'YYYY年M月',
82
+ yearFormat: 'YYYY年',
83
+ prevMonthLabel: t('datepicker.prevMonth'),
84
+ nextMonthLabel: t('datepicker.nextMonth'),
85
+ todayLabel: t('datepicker.today'),
86
+ selectedLabel: t('datepicker.selected')
87
+ },
88
+ fr: {
89
+ monthFormat: 'MMMM YYYY',
90
+ yearFormat: 'YYYY',
91
+ prevMonthLabel: 'Mois précédent',
92
+ nextMonthLabel: 'Mois suivant',
93
+ todayLabel: " aujourd'hui",
94
+ selectedLabel: ' sélectionné'
95
+ },
96
+ de: {
97
+ monthFormat: 'MMMM YYYY',
98
+ yearFormat: 'YYYY',
99
+ prevMonthLabel: 'Vorheriger Monat',
100
+ nextMonthLabel: 'Nächster Monat',
101
+ todayLabel: ' heute',
102
+ selectedLabel: ' ausgewählt'
103
+ },
104
+ es: {
105
+ monthFormat: 'MMMM YYYY',
106
+ yearFormat: 'YYYY',
107
+ prevMonthLabel: 'Mes anterior',
108
+ nextMonthLabel: 'Mes siguiente',
109
+ todayLabel: ' hoy',
110
+ selectedLabel: ' seleccionado'
111
+ },
112
+ 'zh-cn': {
113
+ monthFormat: 'YYYY年M月',
114
+ yearFormat: 'YYYY年',
115
+ prevMonthLabel: '上个月',
116
+ nextMonthLabel: '下个月',
117
+ todayLabel: ' 今天',
118
+ selectedLabel: ' 已选择'
119
+ }
120
+ };
121
+
122
+ const generateDateArray = (startDate: dayjs.Dayjs, endDate: dayjs.Dayjs) => {
123
+ let dates = [];
124
+ let currentDate = startDate;
125
+ while (currentDate.isBefore(endDate) || currentDate.isSame(endDate, 'day')) {
126
+ dates.push(currentDate);
127
+ currentDate = currentDate.add(1, 'day');
128
+ }
129
+ return dates;
130
+ };
131
+
132
+ // =========================================================================
133
+ // Lifecycle
134
+ // =========================================================================
135
+ onMount(() => {
136
+ reset();
137
+ selectedYearMonth = month.startOf('month');
138
+ isKeyboardActive = false;
139
+
140
+ if (value) {
141
+ if (mode === 'range' && value && 'start' in value && 'end' in value) {
142
+ focusedDate = dayjs(value.start);
143
+ } else if (mode === 'single' && value instanceof Date) {
144
+ focusedDate = dayjs(value);
145
+ }
146
+ } else {
147
+ focusedDate = dayjs();
148
+ }
149
+ });
150
+
151
+ // =========================================================================
152
+ // Effects
153
+ // =========================================================================
154
+ $effect(() => {
155
+ dayjs.locale(locale);
156
+ });
157
+
158
+ // =========================================================================
159
+ // Methods
160
+ // =========================================================================
161
+ export const reset = () => {
162
+ if (mode === 'range' && value && 'start' in value && 'end' in value) {
163
+ month = value ? dayjs(value.start).startOf('month') : dayjs().startOf('month');
164
+ focusedDate = value ? dayjs(value.start) : dayjs();
165
+ } else if (mode === 'single' && value && value instanceof Date) {
166
+ month = value ? dayjs(value).startOf('month') : dayjs().startOf('month');
167
+ focusedDate = value ? dayjs(value) : dayjs();
168
+ } else {
169
+ month = dayjs().startOf('month');
170
+ focusedDate = dayjs();
171
+ }
172
+ selectedYearMonth = month.startOf('month');
173
+ };
174
+
175
+ export const focusCalendar = () => {
176
+ isKeyboardActive = true;
177
+ };
178
+
179
+ const handleCalendarOpen = () => {
180
+ document.addEventListener('keydown', handleKeyDown);
181
+ onOpen?.();
182
+ };
183
+
184
+ const handleCalendarClose = () => {
185
+ document.removeEventListener('keydown', handleKeyDown);
186
+ isKeyboardActive = false;
187
+ onClose?.();
188
+ };
189
+
190
+ export const handlePopupOpen = handleCalendarOpen;
191
+ export const handlePopupClose = handleCalendarClose;
192
+
193
+ const toggleMonthMode = () => {
194
+ viewMode = viewMode === 'month' ? 'date' : 'month';
195
+ if (viewMode === 'month') {
196
+ focusedMonth = month.month();
197
+ }
198
+ };
199
+
200
+ const selectMonth = (monthIndex: number) => {
201
+ month = month.month(monthIndex);
202
+ focusedDate = focusedDate.month(monthIndex);
203
+ selectedYearMonth = month.startOf('month');
204
+ viewMode = 'date';
205
+ };
206
+
207
+ const changeYearInMonthMode = (direction: number) => {
208
+ month = month.add(direction, 'year');
209
+ focusedDate = focusedDate.add(direction, 'year');
210
+ };
211
+
212
+ const moveDay = (direction: number) => {
213
+ const newDate = focusedDate.add(direction, 'day');
214
+ focusedDate = newDate;
215
+
216
+ if (newDate.month() !== month.month()) {
217
+ month = newDate.startOf('month');
218
+ }
219
+ };
220
+
221
+ const moveWeek = (direction: number) => {
222
+ const newDate = focusedDate.add(direction * 7, 'day');
223
+ focusedDate = newDate;
224
+
225
+ if (newDate.month() !== month.month()) {
226
+ month = newDate.startOf('month');
227
+ }
228
+ };
229
+
230
+ const moveMonth = (direction: number) => {
231
+ const newDate = focusedDate.add(direction, 'month');
232
+ focusedDate = newDate;
233
+ month = newDate.startOf('month');
234
+ };
235
+
236
+ const moveMonthInSelection = (direction: number) => {
237
+ let newMonth = focusedMonth + direction;
238
+ let yearChange = 0;
239
+
240
+ if (newMonth < 0) {
241
+ yearChange = Math.floor(newMonth / 12);
242
+ newMonth = newMonth % 12;
243
+ if (newMonth < 0) {
244
+ newMonth = 12 + newMonth;
245
+ }
246
+ } else if (newMonth > 11) {
247
+ yearChange = Math.floor(newMonth / 12);
248
+ newMonth = newMonth % 12;
249
+ }
250
+
251
+ focusedMonth = newMonth;
252
+
253
+ if (yearChange !== 0) {
254
+ month = month.add(yearChange, 'year');
255
+ focusedDate = focusedDate.add(yearChange, 'year');
256
+ }
257
+ };
258
+
259
+ const selectFocusedDate = () => {
260
+ if (isOutOfRange(focusedDate)) return;
261
+ selectDate(focusedDate);
262
+ };
263
+
264
+ const handleKeyDown = (event: KeyboardEvent) => {
265
+ if (
266
+ !isKeyboardActive &&
267
+ (event.key === 'ArrowUp' ||
268
+ event.key === 'ArrowDown' ||
269
+ event.key === 'ArrowLeft' ||
270
+ event.key === 'ArrowRight' ||
271
+ event.key === 'PageUp' ||
272
+ event.key === 'PageDown' ||
273
+ event.key === 'Home' ||
274
+ event.key === 'End')
275
+ ) {
276
+ isKeyboardActive = true;
277
+ }
278
+
279
+ if (viewMode === 'month') {
280
+ switch (event.key) {
281
+ case 'ArrowUp':
282
+ event.preventDefault();
283
+ moveMonthInSelection(-3);
284
+ break;
285
+ case 'ArrowDown':
286
+ event.preventDefault();
287
+ moveMonthInSelection(3);
288
+ break;
289
+ case 'ArrowLeft':
290
+ event.preventDefault();
291
+ moveMonthInSelection(-1);
292
+ break;
293
+ case 'ArrowRight':
294
+ event.preventDefault();
295
+ moveMonthInSelection(1);
296
+ break;
297
+ case 'Enter':
298
+ case ' ':
299
+ event.preventDefault();
300
+ selectMonth(focusedMonth);
301
+ break;
302
+ case 'Escape':
303
+ event.preventDefault();
304
+ viewMode = 'date';
305
+ break;
306
+ }
307
+ } else if (viewMode === 'date') {
308
+ switch (event.key) {
309
+ case 'ArrowUp':
310
+ event.preventDefault();
311
+ moveWeek(-1);
312
+ break;
313
+ case 'ArrowDown':
314
+ event.preventDefault();
315
+ moveWeek(1);
316
+ break;
317
+ case 'ArrowLeft':
318
+ event.preventDefault();
319
+ moveDay(-1);
320
+ break;
321
+ case 'ArrowRight':
322
+ event.preventDefault();
323
+ moveDay(1);
324
+ break;
325
+ case 'PageUp':
326
+ event.preventDefault();
327
+ moveMonth(-1);
328
+ break;
329
+ case 'PageDown':
330
+ event.preventDefault();
331
+ moveMonth(1);
332
+ break;
333
+ case 'Home':
334
+ event.preventDefault();
335
+ focusedDate = focusedDate.startOf('week');
336
+ break;
337
+ case 'End':
338
+ event.preventDefault();
339
+ focusedDate = focusedDate.endOf('week');
340
+ break;
341
+ case 'Enter':
342
+ case ' ':
343
+ event.preventDefault();
344
+ selectFocusedDate();
345
+ break;
346
+ case 'Escape':
347
+ event.preventDefault();
348
+ break;
349
+ }
350
+ }
351
+ };
352
+
353
+ const goPrev = () => {
354
+ if (viewMode === 'month') {
355
+ changeYearInMonthMode(-1);
356
+ } else {
357
+ month = month.subtract(1, 'month');
358
+ }
359
+ };
360
+
361
+ const goNext = () => {
362
+ if (viewMode === 'month') {
363
+ changeYearInMonthMode(1);
364
+ } else {
365
+ month = month.add(1, 'month');
366
+ }
367
+ };
368
+
369
+ const isSelected = (date: dayjs.Dayjs) => {
370
+ if (mode === 'range' && value && 'start' in value && 'end' in value) {
371
+ return (
372
+ dayjs(date).isSameOrAfter(dayjs(value.start).startOf('day')) &&
373
+ dayjs(date).isSameOrBefore(dayjs(value.end).startOf('day'))
374
+ );
375
+ } else if (mode === 'single' && value && value instanceof Date) {
376
+ return dayjs(date).isSame(dayjs(value).startOf('day'));
377
+ }
378
+ return false;
379
+ };
380
+
381
+ const isRangeStart = (date: dayjs.Dayjs) => {
382
+ if (mode !== 'range' || !value || !('start' in value && 'end' in value)) return false;
383
+ if (isRangePreviewActive) return false;
384
+ return dayjs(date).isSame(dayjs(value.start).startOf('day'));
385
+ };
386
+
387
+ const isRangeEnd = (date: dayjs.Dayjs) => {
388
+ if (mode !== 'range' || !value || !('start' in value && 'end' in value)) return false;
389
+ if (isRangePreviewActive) return false;
390
+ return dayjs(date).isSame(dayjs(value.end).startOf('day'));
391
+ };
392
+
393
+ const isRangeMiddle = (date: dayjs.Dayjs) => {
394
+ if (mode !== 'range' || !value || !('start' in value && 'end' in value)) return false;
395
+ if (isRangePreviewActive) return false;
396
+ return isSelected(date) && !isRangeStart(date) && !isRangeEnd(date);
397
+ };
398
+
399
+ const isRangeSingle = (date: dayjs.Dayjs) => {
400
+ if (mode !== 'range' || !value || !('start' in value && 'end' in value)) return false;
401
+ if (isRangePreviewActive) return false;
402
+ return isRangeStart(date) && isRangeEnd(date);
403
+ };
404
+
405
+ const isRangePreviewStart = (date: dayjs.Dayjs) => {
406
+ if (mode !== 'range' || !hoveredDate || !value || !('start' in value && 'end' in value))
407
+ return false;
408
+ if (isSelectingStart) return false;
409
+
410
+ const startDate = dayjs(value.start).startOf('day');
411
+ const endDate = hoveredDate.startOf('day');
412
+
413
+ if (startDate.isSame(endDate)) return false;
414
+
415
+ const actualStart = startDate.isSameOrBefore(endDate) ? startDate : endDate;
416
+
417
+ return dayjs(date).isSame(actualStart);
418
+ };
419
+
420
+ const isRangePreviewEnd = (date: dayjs.Dayjs) => {
421
+ if (mode !== 'range' || !hoveredDate || !value || !('start' in value && 'end' in value))
422
+ return false;
423
+ if (isSelectingStart) return false;
424
+
425
+ const startDate = dayjs(value.start).startOf('day');
426
+ const endDate = hoveredDate.startOf('day');
427
+
428
+ if (startDate.isSame(endDate)) return false;
429
+
430
+ const actualEnd = startDate.isSameOrBefore(endDate) ? endDate : startDate;
431
+
432
+ return dayjs(date).isSame(actualEnd);
433
+ };
434
+
435
+ const isRangePreviewMiddle = (date: dayjs.Dayjs) => {
436
+ if (mode !== 'range' || !hoveredDate || !value || !('start' in value && 'end' in value))
437
+ return false;
438
+ if (isSelectingStart) return false;
439
+
440
+ const startDate = dayjs(value.start).startOf('day');
441
+ const endDate = hoveredDate.startOf('day');
442
+
443
+ if (startDate.isSame(endDate)) return false;
444
+
445
+ const actualStart = startDate.isSameOrBefore(endDate) ? startDate : endDate;
446
+ const actualEnd = startDate.isSameOrBefore(endDate) ? endDate : startDate;
447
+
448
+ return dayjs(date).isAfter(actualStart) && dayjs(date).isBefore(actualEnd);
449
+ };
450
+
451
+ const isRangePreviewSingle = (date: dayjs.Dayjs) => {
452
+ if (mode !== 'range' || !hoveredDate || !value || !('start' in value && 'end' in value))
453
+ return false;
454
+ if (isSelectingStart) return false;
455
+
456
+ const startDate = dayjs(value.start).startOf('day');
457
+ const endDate = hoveredDate.startOf('day');
458
+
459
+ return startDate.isSame(endDate) && dayjs(date).isSame(startDate);
460
+ };
461
+
462
+ const isOutOfMonth = (date: dayjs.Dayjs) => {
463
+ return date.month() !== month.month();
464
+ };
465
+
466
+ const isOutOfRange = (date: dayjs.Dayjs) => {
467
+ return (
468
+ (minDate && date.startOf('day').isBefore(dayjs(minDate).startOf('day'))) ||
469
+ (maxDate && date.startOf('day').isAfter(dayjs(maxDate).startOf('day')))
470
+ );
471
+ };
472
+
473
+ const isToday = (date: dayjs.Dayjs) => {
474
+ return date.startOf('day').isSame(dayjs().startOf('day'));
475
+ };
476
+
477
+ const isFocused = (date: dayjs.Dayjs) => {
478
+ const dateKey = date.startOf('day').format('YYYY-MM-DD');
479
+ const result = focusedDateKey === dateKey;
480
+ return result;
481
+ };
482
+
483
+ const getDateId = (date: dayjs.Dayjs) => {
484
+ return `calendar-date-${date.format('YYYY-MM-DD')}`;
485
+ };
486
+
487
+ const handleMouseEnter = (date: dayjs.Dayjs) => {
488
+ if (mode !== 'range' || isOutOfRange(date)) return;
489
+ hoveredDate = date;
490
+ };
491
+
492
+ const handleMouseLeave = () => {
493
+ hoveredDate = null;
494
+ };
495
+
496
+ const selectDate = (date: dayjs.Dayjs) => {
497
+ if (mode === 'range') {
498
+ if (value && 'start' in value && 'end' in value) {
499
+ if (isSelectingStart) {
500
+ value = { start: date.toDate(), end: date.toDate() };
501
+ isSelectingStart = false;
502
+ } else {
503
+ if (date.isSameOrAfter(value.start)) {
504
+ value = { start: value.start, end: date.toDate() };
505
+ } else {
506
+ value = { start: date.toDate(), end: value.start };
507
+ }
508
+ isSelectingStart = true;
509
+ onchange(value);
510
+ }
511
+ } else {
512
+ value = { start: date.toDate(), end: date.toDate() };
513
+ isSelectingStart = false;
514
+ }
515
+ } else {
516
+ value = date.toDate();
517
+ onchange(value);
518
+ }
519
+ };
520
+
521
+ // =========================================================================
522
+ // $derived
523
+ // =========================================================================
524
+ const currentLocaleConfig = $derived(localeConfig[locale]);
525
+ const startDate = $derived(month.startOf('month').startOf('week'));
526
+ const endDate = $derived(month.endOf('month').endOf('week'));
527
+ const dates: dayjs.Dayjs[] = $derived(generateDateArray(startDate, endDate));
528
+ const monthNames = $derived.by(() => {
529
+ dayjs.locale(locale);
530
+ return dayjs.months();
531
+ });
532
+ const DAY_ARRAY = $derived.by(() => {
533
+ dayjs.locale(locale);
534
+ return dayjs.weekdaysMin();
535
+ });
536
+ const isRangePreviewActive = $derived(
537
+ mode === 'range' &&
538
+ !isSelectingStart &&
539
+ hoveredDate &&
540
+ value &&
541
+ 'start' in value &&
542
+ 'end' in value
543
+ );
544
+ const focusedDateKey = $derived(
545
+ isKeyboardActive ? focusedDate.startOf('day').format('YYYY-MM-DD') : null
546
+ );
547
+ </script>
548
+
549
+ <div
550
+ bind:this={calendarRef}
551
+ class="datepicker-calendar"
552
+ role="grid"
553
+ aria-label={month.locale(locale).format(currentLocaleConfig.monthFormat)}
554
+ {id}
555
+ data-testid="datepicker-calendar"
556
+ >
557
+ <div class="datepicker-calendar__header">
558
+ <div class="prev-button-block">
559
+ <IconButton size={36} ariaLabel={currentLocaleConfig.prevMonthLabel} onclick={goPrev}>
560
+ chevron_left
561
+ </IconButton>
562
+ </div>
563
+ <button
564
+ class="datepicker-calendar__header__month-label-button"
565
+ aria-live="polite"
566
+ aria-atomic="true"
567
+ onclick={(event) => {
568
+ event.stopPropagation();
569
+ isKeyboardActive = false;
570
+ toggleMonthMode();
571
+ }}
572
+ >
573
+ {#if viewMode === 'month'}
574
+ {month.locale(locale).format(currentLocaleConfig.yearFormat)}
575
+ {:else}
576
+ {month.locale(locale).format(currentLocaleConfig.monthFormat)}
577
+ {/if}
578
+ </button>
579
+ <div class="datepicker-calendar__header__next-button-block">
580
+ <IconButton size={36} ariaLabel={currentLocaleConfig.nextMonthLabel} onclick={goNext}
581
+ >chevron_right</IconButton
582
+ >
583
+ </div>
584
+ </div>
585
+
586
+ {#if viewMode === 'month'}
587
+ <div class="datepicker-calendar__month-selection">
588
+ {#each monthNames as monthName, index}
589
+ <button
590
+ class="datepicker-calendar__month-button"
591
+ class:datepicker-calendar__month-button--current={index === dayjs().month() &&
592
+ month.year() === dayjs().year()}
593
+ class:datepicker-calendar__month-button--selected={month
594
+ .month(index)
595
+ .startOf('month')
596
+ .isSame(selectedYearMonth, 'month')}
597
+ class:datepicker-calendar__month-button--focused={isKeyboardActive &&
598
+ index === focusedMonth}
599
+ onclick={(event) => {
600
+ event.stopPropagation();
601
+ focusedMonth = index;
602
+ isKeyboardActive = false;
603
+ selectMonth(index);
604
+ }}
605
+ >
606
+ {monthName}
607
+ </button>
608
+ {/each}
609
+ </div>
610
+ {:else}
611
+ <div class="datepicker-calendar__date-selection" role="grid" aria-labelledby="month-label">
612
+ <div class="datepicker-calendar__day-list" role="row">
613
+ {#each DAY_ARRAY as day}
614
+ <div class="datepicker-calendar__day-item" role="columnheader">
615
+ {day}
616
+ </div>
617
+ {/each}
618
+ </div>
619
+ <div class="datepicker-calendar__date-list">
620
+ {#each dates as date}
621
+ <div
622
+ class="datepicker-calendar__date-item"
623
+ class:datepicker-calendar__date-item--selected={mode === 'single' && isSelected(date)}
624
+ class:datepicker-calendar__date-item--range-start={isRangeStart(date)}
625
+ class:datepicker-calendar__date-item--range-end={isRangeEnd(date)}
626
+ class:datepicker-calendar__date-item--range-middle={isRangeMiddle(date)}
627
+ class:datepicker-calendar__date-item--range-single={isRangeSingle(date)}
628
+ class:datepicker-calendar__date-item--range-preview-start={isRangePreviewStart(date)}
629
+ class:datepicker-calendar__date-item--range-preview-end={isRangePreviewEnd(date)}
630
+ class:datepicker-calendar__date-item--range-preview-middle={isRangePreviewMiddle(date)}
631
+ class:datepicker-calendar__date-item--range-preview-single={isRangePreviewSingle(date)}
632
+ class:datepicker-calendar__date-item--out-of-month={isOutOfMonth(date)}
633
+ class:datepicker-calendar__date-item--out-of-range={isOutOfRange(date)}
634
+ class:datepicker-calendar__date-item--today={isToday(date)}
635
+ class:datepicker-calendar__date-item--focused={focusedDateKey ===
636
+ date.startOf('day').format('YYYY-MM-DD')}
637
+ role="gridcell"
638
+ >
639
+ <button
640
+ id={getDateId(date)}
641
+ class="datepicker-calendar__date-button"
642
+ aria-current={isToday(date) ? 'date' : undefined}
643
+ aria-pressed={isSelected(date)}
644
+ aria-label={`${date.locale(locale).format('YYYY/MM/DD')}${isToday(date) ? currentLocaleConfig.todayLabel : ''}${isSelected(date) ? currentLocaleConfig.selectedLabel : ''}`}
645
+ aria-disabled={isOutOfRange(date)}
646
+ onclick={() => {
647
+ focusedDate = date;
648
+ isKeyboardActive = false;
649
+ selectDate(date);
650
+ }}
651
+ onmouseenter={() => handleMouseEnter(date)}
652
+ onmouseleave={handleMouseLeave}
653
+ >
654
+ {date.format('D')}
655
+ </button>
656
+ </div>
657
+ {/each}
658
+ </div>
659
+ </div>
660
+ {/if}
661
+ </div>
662
+
663
+ <style>@charset "UTF-8";
664
+ .datepicker-calendar {
665
+ display: flex;
666
+ flex-direction: column;
667
+ gap: 8px;
668
+ width: 320px;
669
+ padding: 16px;
670
+ }
671
+
672
+ .datepicker-calendar__header {
673
+ display: flex;
674
+ justify-content: space-between;
675
+ align-items: center;
676
+ }
677
+
678
+ .datepicker-calendar__header__month-label-button {
679
+ font-size: 1.4rem;
680
+ font-weight: bold;
681
+ color: var(--svelte-ui-datepicker-date-color);
682
+ background: none;
683
+ border: none;
684
+ padding: 8px 16px;
685
+ border-radius: var(--svelte-ui-border-radius);
686
+ cursor: pointer;
687
+ transition: background-color var(--svelte-ui-transition-duration);
688
+ }
689
+ @media (hover: hover) {
690
+ .datepicker-calendar__header__month-label-button:hover {
691
+ background-color: var(--svelte-ui-hover-overlay);
692
+ }
693
+ }
694
+ .datepicker-calendar__header__month-label-button:focus-visible {
695
+ outline: var(--svelte-ui-focus-outline-outer);
696
+ outline-offset: var(--svelte-ui-focus-outline-offset-outer);
697
+ }
698
+
699
+ .datepicker-calendar__month-selection {
700
+ display: grid;
701
+ grid-template-columns: repeat(3, 1fr);
702
+ gap: 8px;
703
+ }
704
+
705
+ .datepicker-calendar__month-button {
706
+ padding: 8px;
707
+ border-radius: var(--svelte-ui-border-radius);
708
+ background-color: var(--svelte-ui-surface-color);
709
+ color: var(--svelte-ui-datepicker-date-color);
710
+ font-size: 1rem;
711
+ cursor: pointer;
712
+ transition: background-color var(--svelte-ui-transition-duration);
713
+ }
714
+ @media (hover: hover) {
715
+ .datepicker-calendar__month-button:hover {
716
+ background-color: var(--svelte-ui-hover-overlay);
717
+ }
718
+ }
719
+ .datepicker-calendar__month-button:focus-visible {
720
+ outline: var(--svelte-ui-focus-outline-outer);
721
+ outline-offset: var(--svelte-ui-focus-outline-offset-outer);
722
+ }
723
+ .datepicker-calendar__month-button--current {
724
+ font-weight: bold;
725
+ color: var(--svelte-ui-datepicker-current-color);
726
+ }
727
+ .datepicker-calendar__month-button--selected {
728
+ background-color: var(--svelte-ui-datepicker-selected-color);
729
+ color: var(--svelte-ui-datepicker-selected-text-color);
730
+ border-color: var(--svelte-ui-datepicker-selected-color);
731
+ }
732
+ .datepicker-calendar__month-button--focused {
733
+ outline: var(--svelte-ui-focus-outline-outer);
734
+ outline-offset: var(--svelte-ui-focus-outline-offset-outer);
735
+ }
736
+
737
+ .datepicker-calendar__date-selection {
738
+ display: flex;
739
+ flex-direction: column;
740
+ gap: 8px;
741
+ }
742
+
743
+ .datepicker-calendar__day-list {
744
+ display: flex;
745
+ place-items: center stretch;
746
+ }
747
+
748
+ .datepicker-calendar__date-list {
749
+ display: flex;
750
+ flex-wrap: wrap;
751
+ }
752
+
753
+ .datepicker-calendar__day-item,
754
+ .datepicker-calendar__date-item {
755
+ display: flex;
756
+ justify-content: center;
757
+ padding: 2px 0;
758
+ font-size: 1rem;
759
+ width: 14.2857142857%;
760
+ }
761
+
762
+ .datepicker-calendar__day-item {
763
+ background-color: var(--svelte-ui-datepicker-day-label-bg);
764
+ color: var(--svelte-ui-datepicker-day-label-color);
765
+ }
766
+
767
+ .datepicker-calendar__day-item:first-of-type {
768
+ border-radius: var(--svelte-ui-datepicker-day-label-border-radius) 0 0 var(--svelte-ui-datepicker-day-label-border-radius);
769
+ }
770
+
771
+ .datepicker-calendar__day-item:last-of-type {
772
+ border-radius: 0 var(--svelte-ui-datepicker-day-label-border-radius) var(--svelte-ui-datepicker-day-label-border-radius) 0;
773
+ }
774
+
775
+ .datepicker-calendar__date-item .datepicker-calendar__date-button {
776
+ color: var(--svelte-ui-datepicker-date-color);
777
+ }
778
+
779
+ .datepicker-calendar__date-item--out-of-month .datepicker-calendar__date-button {
780
+ color: var(--svelte-ui-datepicker-out-of-month-text-color);
781
+ }
782
+
783
+ .datepicker-calendar__date-item--today .datepicker-calendar__date-button {
784
+ font-weight: bold;
785
+ color: var(--svelte-ui-datepicker-today-color);
786
+ }
787
+
788
+ .datepicker-calendar__date-item--selected .datepicker-calendar__date-button {
789
+ background-color: var(--svelte-ui-datepicker-selected-color);
790
+ color: var(--svelte-ui-datepicker-selected-text-color);
791
+ }
792
+
793
+ @media (hover: hover) {
794
+ .datepicker-calendar__date-button:hover {
795
+ background: var(--svelte-ui-hover-overlay);
796
+ }
797
+ }
798
+ /* 期間選択の帯状表示 */
799
+ .datepicker-calendar__date-item--range-start,
800
+ .datepicker-calendar__date-item--range-end,
801
+ .datepicker-calendar__date-item--range-middle,
802
+ .datepicker-calendar__date-item--range-single,
803
+ .datepicker-calendar__date-item--range-preview-start,
804
+ .datepicker-calendar__date-item--range-preview-end,
805
+ .datepicker-calendar__date-item--range-preview-middle,
806
+ .datepicker-calendar__date-item--range-preview-single {
807
+ position: relative;
808
+ }
809
+
810
+ .datepicker-calendar__date-item--range-start::before,
811
+ .datepicker-calendar__date-item--range-end::before,
812
+ .datepicker-calendar__date-item--range-middle::before,
813
+ .datepicker-calendar__date-item--range-preview-start::before,
814
+ .datepicker-calendar__date-item--range-preview-end::before,
815
+ .datepicker-calendar__date-item--range-preview-middle::before {
816
+ content: "";
817
+ position: absolute;
818
+ top: 50%;
819
+ transform: translateY(-50%);
820
+ height: 36px;
821
+ background-color: var(--svelte-ui-select-overlay);
822
+ z-index: 0;
823
+ }
824
+
825
+ /* 範囲プレビュー用の背景色調整 */
826
+ .datepicker-calendar__date-item--range-preview-start::before,
827
+ .datepicker-calendar__date-item--range-preview-end::before,
828
+ .datepicker-calendar__date-item--range-preview-middle::before,
829
+ .datepicker-calendar__date-item--range-preview-single::before {
830
+ background-color: var(--svelte-ui-hover-overlay);
831
+ }
832
+
833
+ /* 範囲開始日 */
834
+ .datepicker-calendar__date-item--range-start::before,
835
+ .datepicker-calendar__date-item--range-preview-start::before {
836
+ left: 50%;
837
+ right: 0;
838
+ }
839
+
840
+ /* 範囲終了日 */
841
+ .datepicker-calendar__date-item--range-end::before,
842
+ .datepicker-calendar__date-item--range-preview-end::before {
843
+ left: 0;
844
+ right: 50%;
845
+ }
846
+
847
+ /* 範囲中間日 */
848
+ .datepicker-calendar__date-item--range-middle::before,
849
+ .datepicker-calendar__date-item--range-preview-middle::before {
850
+ left: 0;
851
+ right: 0;
852
+ border-radius: 0;
853
+ }
854
+
855
+ /* 単一日選択(開始日と終了日が同じ) */
856
+ .datepicker-calendar__date-item--range-single::before,
857
+ .datepicker-calendar__date-item--range-preview-single::before {
858
+ display: none;
859
+ }
860
+
861
+ /* 範囲内の日付ボタンスタイル */
862
+ .datepicker-calendar__date-item--out-of-range {
863
+ background: var(--svelte-ui-datepicker-out-of-range-bg);
864
+ pointer-events: none;
865
+ }
866
+ .datepicker-calendar__date-item--out-of-range .datepicker-calendar__date-button {
867
+ color: var(--svelte-ui-datepicker-out-of-range-text-color);
868
+ }
869
+ .datepicker-calendar__date-item--out-of-range.datepicker-calendar__date-item--selected .datepicker-calendar__date-button {
870
+ color: var(--svelte-ui-datepicker-selected-text-color);
871
+ }
872
+
873
+ /* レンジ選択のスタイル(out-of-rangeより後に定義して優先度を上げる) */
874
+ .datepicker-calendar__date-item--range-start .datepicker-calendar__date-button,
875
+ .datepicker-calendar__date-item--range-end .datepicker-calendar__date-button,
876
+ .datepicker-calendar__date-item--range-single .datepicker-calendar__date-button {
877
+ background-color: var(--svelte-ui-datepicker-range-color);
878
+ color: var(--svelte-ui-datepicker-selected-text-color);
879
+ font-weight: bold;
880
+ z-index: 1;
881
+ position: relative;
882
+ }
883
+
884
+ .datepicker-calendar__date-item--range-middle .datepicker-calendar__date-button {
885
+ background-color: transparent;
886
+ color: var(--svelte-ui-datepicker-date-color);
887
+ z-index: 1;
888
+ position: relative;
889
+ }
890
+
891
+ /* 範囲プレビュー用のボタンスタイル */
892
+ .datepicker-calendar__date-item--range-preview-start .datepicker-calendar__date-button,
893
+ .datepicker-calendar__date-item--range-preview-end .datepicker-calendar__date-button,
894
+ .datepicker-calendar__date-item--range-preview-single .datepicker-calendar__date-button {
895
+ background-color: var(--svelte-ui-datepicker-range-color);
896
+ color: var(--svelte-ui-datepicker-selected-text-color);
897
+ font-weight: bold;
898
+ z-index: 1;
899
+ position: relative;
900
+ }
901
+
902
+ .datepicker-calendar__date-item--range-preview-middle .datepicker-calendar__date-button {
903
+ background-color: transparent;
904
+ z-index: 1;
905
+ position: relative;
906
+ }
907
+
908
+ .datepicker-calendar__date-button:focus-visible {
909
+ outline: var(--svelte-ui-focus-outline-outer);
910
+ outline-offset: var(--svelte-ui-focus-outline-offset-outer);
911
+ }
912
+
913
+ .datepicker-calendar__date-item--focused .datepicker-calendar__date-button {
914
+ outline: var(--svelte-ui-focus-outline-outer);
915
+ outline-offset: var(--svelte-ui-focus-outline-offset-outer);
916
+ }
917
+
918
+ .datepicker-calendar__date-button {
919
+ width: 36px;
920
+ height: 36px;
921
+ padding: 0;
922
+ background: transparent;
923
+ border: none;
924
+ border-radius: 18px;
925
+ }</style>