@datametria/vue-components 1.2.0 → 2.1.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.
Files changed (103) hide show
  1. package/README.md +554 -657
  2. package/dist/index.es.js +2570 -1433
  3. package/dist/index.umd.js +10 -10
  4. package/dist/vue-components.css +1 -1
  5. package/package.json +102 -98
  6. package/src/components/DatametriaAlert.vue +137 -137
  7. package/src/components/DatametriaAutocomplete.vue +184 -138
  8. package/src/components/DatametriaAvatar.vue +177 -33
  9. package/src/components/DatametriaBadge.vue +98 -98
  10. package/src/components/DatametriaBreadcrumb.vue +21 -21
  11. package/src/components/DatametriaButton.vue +177 -165
  12. package/src/components/DatametriaCard.vue +12 -12
  13. package/src/components/DatametriaCheckbox.vue +8 -8
  14. package/src/components/DatametriaChip.vue +145 -149
  15. package/src/components/DatametriaContainer.vue +4 -4
  16. package/src/components/DatametriaDatePicker.vue +686 -68
  17. package/src/components/DatametriaDivider.vue +13 -13
  18. package/src/components/DatametriaFileUpload.vue +272 -140
  19. package/src/components/DatametriaFloatingBar.vue +126 -0
  20. package/src/components/DatametriaGrid.vue +3 -3
  21. package/src/components/DatametriaInput.vue +15 -15
  22. package/src/components/DatametriaMenu.vue +604 -619
  23. package/src/components/DatametriaModal.vue +16 -16
  24. package/src/components/DatametriaNavbar.vue +230 -252
  25. package/src/components/DatametriaPasswordInput.vue +430 -0
  26. package/src/components/DatametriaProgress.vue +18 -18
  27. package/src/components/DatametriaRadio.vue +20 -20
  28. package/src/components/DatametriaSelect.vue +15 -15
  29. package/src/components/DatametriaSidebar.vue +230 -0
  30. package/src/components/DatametriaSkeleton.vue +243 -239
  31. package/src/components/DatametriaSlider.vue +395 -407
  32. package/src/components/DatametriaSortableTable.vue +585 -0
  33. package/src/components/DatametriaSpinner.vue +7 -7
  34. package/src/components/DatametriaSwitch.vue +16 -16
  35. package/src/components/DatametriaTable.vue +14 -14
  36. package/src/components/DatametriaTabs.vue +150 -29
  37. package/src/components/DatametriaTextarea.vue +28 -28
  38. package/src/components/DatametriaTimePicker.vue +285 -285
  39. package/src/components/DatametriaToast.vue +176 -176
  40. package/src/components/DatametriaTooltip.vue +408 -408
  41. package/src/components/__tests__/DatametriaAlert.test.js +35 -35
  42. package/src/components/__tests__/DatametriaAlert.test.ts +190 -0
  43. package/src/components/__tests__/DatametriaAutocomplete.test.ts +180 -0
  44. package/src/components/__tests__/DatametriaAvatar.test.ts +152 -0
  45. package/src/components/__tests__/DatametriaBadge.test.js +29 -29
  46. package/src/components/__tests__/DatametriaBadge.test.ts +167 -0
  47. package/src/components/__tests__/DatametriaBreadcrumb.test.ts +75 -0
  48. package/src/components/__tests__/DatametriaButton.test.js +30 -30
  49. package/src/components/__tests__/DatametriaButton.test.ts +283 -0
  50. package/src/components/__tests__/DatametriaCard.test.ts +201 -0
  51. package/src/components/__tests__/DatametriaCheckbox.test.ts +47 -0
  52. package/src/components/__tests__/DatametriaChip.test.js +38 -38
  53. package/src/components/__tests__/DatametriaContainer.test.ts +52 -0
  54. package/src/components/__tests__/DatametriaDatePicker.test.ts +234 -0
  55. package/src/components/__tests__/DatametriaDivider.test.ts +54 -0
  56. package/src/components/__tests__/DatametriaFileUpload.test.ts +291 -0
  57. package/src/components/__tests__/DatametriaFloatingBar.test.ts +137 -0
  58. package/src/components/__tests__/DatametriaGrid.test.ts +31 -0
  59. package/src/components/__tests__/DatametriaInput.test.ts +72 -0
  60. package/src/components/__tests__/DatametriaMenu.test.ts +366 -0
  61. package/src/components/__tests__/DatametriaModal.test.ts +86 -0
  62. package/src/components/__tests__/DatametriaNavbar.test.js +48 -48
  63. package/src/components/__tests__/DatametriaNavbar.test.ts +203 -0
  64. package/src/components/__tests__/DatametriaPasswordInput.test.js +305 -0
  65. package/src/components/__tests__/DatametriaProgress.test.ts +90 -0
  66. package/src/components/__tests__/DatametriaRadio.test.ts +77 -0
  67. package/src/components/__tests__/DatametriaSelect.test.ts +77 -0
  68. package/src/components/__tests__/DatametriaSidebar.test.ts +169 -0
  69. package/src/components/__tests__/DatametriaSlider.test.ts +261 -0
  70. package/src/components/__tests__/DatametriaSortableTable.test.js +168 -0
  71. package/src/components/__tests__/DatametriaSpinner.test.ts +156 -0
  72. package/src/components/__tests__/DatametriaSwitch.test.ts +64 -0
  73. package/src/components/__tests__/DatametriaTable.test.ts +97 -0
  74. package/src/components/__tests__/DatametriaTabs.test.ts +232 -0
  75. package/src/components/__tests__/DatametriaTextarea.test.ts +66 -0
  76. package/src/components/__tests__/DatametriaToast.test.js +48 -48
  77. package/src/components/__tests__/DatametriaToast.test.ts +99 -0
  78. package/src/composables/useAccessibilityScale.ts +94 -94
  79. package/src/composables/useBreakpoints.ts +82 -82
  80. package/src/composables/useHapticFeedback.ts +439 -439
  81. package/src/composables/useRipple.ts +218 -218
  82. package/src/index.ts +70 -61
  83. package/src/stories/Variants.stories.js +95 -95
  84. package/src/styles/design-tokens.css +623 -623
  85. package/src/theme/ThemeProvider.vue +96 -0
  86. package/src/theme/__tests__/ThemeProvider.test.ts +208 -0
  87. package/src/theme/__tests__/constants.test.ts +31 -0
  88. package/src/theme/__tests__/presets.test.ts +166 -0
  89. package/src/theme/__tests__/tokens.test.ts +155 -0
  90. package/src/theme/__tests__/types.test.ts +153 -0
  91. package/src/theme/__tests__/useTheme.test.ts +146 -0
  92. package/src/theme/constants.ts +14 -0
  93. package/src/theme/index.ts +12 -0
  94. package/src/theme/presets/datametria.ts +94 -0
  95. package/src/theme/presets/default.ts +94 -0
  96. package/src/theme/presets/index.ts +8 -0
  97. package/src/theme/tokens/colors.ts +28 -0
  98. package/src/theme/tokens/index.ts +47 -0
  99. package/src/theme/tokens/spacing.ts +21 -0
  100. package/src/theme/tokens/typography.ts +35 -0
  101. package/src/theme/types.ts +111 -0
  102. package/src/theme/useTheme.ts +28 -0
  103. package/src/types/index.ts +19 -0
@@ -1,81 +1,536 @@
1
1
  <template>
2
- <div class="dm-datepicker">
2
+ <div class="dm-datepicker" ref="datepickerRef">
3
3
  <label v-if="label" :for="inputId" class="dm-datepicker__label">
4
4
  {{ label }}
5
5
  <span v-if="required" class="dm-datepicker__required">*</span>
6
6
  </label>
7
+
7
8
  <div class="dm-datepicker__wrapper">
8
9
  <input
9
10
  :id="inputId"
10
- type="date"
11
- v-model="internalValue"
11
+ v-model="displayValue"
12
+ type="text"
12
13
  class="dm-datepicker__input"
13
- :class="{ 'dm-datepicker__input--error': error }"
14
+ :class="{ 'dm-datepicker__input--error': errorMessage }"
15
+ :placeholder="placeholder"
14
16
  :disabled="disabled"
17
+ :readonly="readonly"
15
18
  :required="required"
16
- :min="min"
17
- :max="max"
18
- :aria-label="ariaLabel"
19
- :aria-describedby="error ? `${inputId}-error` : undefined"
20
- :aria-invalid="!!error"
21
- @change="handleChange"
19
+ :aria-label="label || 'Date picker'"
20
+ :aria-invalid="!!errorMessage"
21
+ :aria-describedby="errorMessage ? `${inputId}-error` : undefined"
22
+ @click="toggleCalendar"
23
+ @focus="handleFocus"
24
+ @keydown="handleKeydown"
22
25
  />
26
+
27
+ <button
28
+ v-if="clearable && hasValue"
29
+ type="button"
30
+ class="dm-datepicker__clear"
31
+ @click="clearDate"
32
+ aria-label="Clear date"
33
+ >
34
+ ×
35
+ </button>
36
+ </div>
37
+
38
+ <div
39
+ v-if="isOpen"
40
+ class="dm-datepicker__calendar"
41
+ role="dialog"
42
+ aria-label="Calendar"
43
+ @click.stop
44
+ >
45
+ <div class="dm-datepicker__header">
46
+ <button
47
+ type="button"
48
+ class="dm-datepicker__nav dm-datepicker__nav-prev"
49
+ @click="previousMonth"
50
+ aria-label="Previous month"
51
+ >
52
+
53
+ </button>
54
+
55
+ <button
56
+ type="button"
57
+ class="dm-datepicker__month-year"
58
+ @click="toggleYearPicker"
59
+ :aria-label="`${currentMonthYear}, click to select year`"
60
+ >
61
+ {{ currentMonthYear }}
62
+ </button>
63
+
64
+ <button
65
+ type="button"
66
+ class="dm-datepicker__nav dm-datepicker__nav-next"
67
+ @click="nextMonth"
68
+ aria-label="Next month"
69
+ >
70
+
71
+ </button>
72
+ </div>
73
+
74
+ <div v-if="showShortcuts && shortcuts.length > 0" class="dm-datepicker__shortcuts">
75
+ <button
76
+ v-for="shortcut in shortcuts"
77
+ :key="shortcut.label"
78
+ type="button"
79
+ class="dm-datepicker__shortcut"
80
+ @click="applyShortcut(shortcut)"
81
+ >
82
+ {{ shortcut.label }}
83
+ </button>
84
+ </div>
85
+
86
+ <div v-if="showYearPicker" class="dm-datepicker__year-picker">
87
+ <div
88
+ v-for="year in availableYears"
89
+ :key="year"
90
+ class="dm-datepicker__year-option"
91
+ :class="{ 'dm-datepicker__year-option--current': year === currentDate.getFullYear() }"
92
+ @click="selectYear(year)"
93
+ >
94
+ {{ year }}
95
+ </div>
96
+ </div>
97
+
98
+ <div v-else class="dm-datepicker__grid">
99
+ <div class="dm-datepicker__weekdays">
100
+ <div
101
+ v-for="day in weekdays"
102
+ :key="day"
103
+ class="dm-datepicker__weekday"
104
+ >
105
+ {{ day }}
106
+ </div>
107
+ </div>
108
+
109
+ <div class="dm-datepicker__days">
110
+ <button
111
+ v-for="day in calendarDays"
112
+ :key="`${day.date}-${day.month}`"
113
+ type="button"
114
+ class="dm-datepicker__day"
115
+ :class="{
116
+ 'dm-datepicker__day--other-month': day.otherMonth,
117
+ 'dm-datepicker__day--disabled': day.disabled,
118
+ 'dm-datepicker__day--selected': day.selected,
119
+ 'dm-datepicker__day--today': day.today,
120
+ 'dm-datepicker__day--in-range': day.inRange,
121
+ 'dm-datepicker__day--range-start': day.rangeStart,
122
+ 'dm-datepicker__day--range-end': day.rangeEnd
123
+ }"
124
+ :data-date="day.dateString"
125
+ :disabled="day.disabled"
126
+ :aria-label="getDateAriaLabel(day)"
127
+ :aria-current="day.today ? 'date' : undefined"
128
+ @click="selectDate(day)"
129
+ >
130
+ {{ day.date }}
131
+ </button>
132
+ </div>
133
+ </div>
134
+
135
+ <div v-if="showToday" class="dm-datepicker__footer">
136
+ <button
137
+ type="button"
138
+ class="dm-datepicker__today"
139
+ @click="selectToday"
140
+ >
141
+ Today
142
+ </button>
143
+ </div>
144
+ </div>
145
+
146
+ <div v-if="errorMessage" :id="`${inputId}-error`" class="dm-datepicker__error">
147
+ {{ errorMessage }}
23
148
  </div>
24
- <p v-if="error" :id="`${inputId}-error`" class="dm-datepicker__error">{{ error }}</p>
25
149
  </div>
26
150
  </template>
27
151
 
28
152
  <script setup lang="ts">
29
- import { ref, watch } from 'vue'
153
+ import { ref, computed, onMounted, onUnmounted } from 'vue'
154
+
155
+ type DateValue = string | { start: string; end: string } | string[]
156
+
157
+ interface Shortcut {
158
+ label: string
159
+ value: string | { start: string; end: string } | string[]
160
+ }
30
161
 
31
162
  interface Props {
32
- modelValue?: string
163
+ modelValue?: DateValue
164
+ mode?: 'single' | 'range' | 'multiple'
165
+ format?: string
166
+ placeholder?: string
33
167
  label?: string
168
+ errorMessage?: string
34
169
  disabled?: boolean
170
+ readonly?: boolean
35
171
  required?: boolean
36
- error?: string
37
172
  min?: string
38
173
  max?: string
39
- ariaLabel?: string
174
+ disabledDates?: string[]
175
+ disabledWeekdays?: number[]
176
+ enabledDates?: string[]
177
+ showToday?: boolean
178
+ clearable?: boolean
179
+ closeOnSelect?: boolean
180
+ showShortcuts?: boolean
181
+ shortcuts?: Shortcut[]
40
182
  }
41
183
 
42
184
  const props = withDefaults(defineProps<Props>(), {
43
- modelValue: '',
44
- disabled: false,
45
- required: false
185
+ mode: 'single',
186
+ format: 'DD/MM/YYYY',
187
+ placeholder: 'Select date...',
188
+ showToday: false,
189
+ clearable: false,
190
+ closeOnSelect: true,
191
+ showShortcuts: false,
192
+ shortcuts: () => [
193
+ { label: 'Today', value: new Date().toISOString().split('T')[0] },
194
+ { label: 'Tomorrow', value: new Date(Date.now() + 86400000).toISOString().split('T')[0] },
195
+ { label: 'Next Week', value: new Date(Date.now() + 7 * 86400000).toISOString().split('T')[0] }
196
+ ]
46
197
  })
47
198
 
48
199
  const emit = defineEmits<{
49
- 'update:modelValue': [value: string]
200
+ 'update:modelValue': [value: DateValue]
201
+ open: []
202
+ close: []
203
+ clear: []
50
204
  }>()
51
205
 
52
- const inputId = `dm-datepicker-${Math.random().toString(36).substr(2, 9)}`
53
- const internalValue = ref(props.modelValue)
206
+ const datepickerRef = ref<HTMLElement>()
207
+ const isOpen = ref(false)
208
+ const showYearPicker = ref(false)
209
+ const currentDate = ref(new Date())
210
+ const isMobile = ref(false)
211
+
212
+ const inputId = computed(() => `datepicker-${Math.random().toString(36).substr(2, 9)}`)
213
+ const weekdays = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']
214
+
215
+ const hasValue = computed(() => {
216
+ if (props.mode === 'single') return !!props.modelValue
217
+ if (props.mode === 'range') return !!(props.modelValue as any)?.start && !!(props.modelValue as any)?.end
218
+ if (props.mode === 'multiple') return Array.isArray(props.modelValue) && props.modelValue.length > 0
219
+ return false
220
+ })
221
+
222
+ const displayValue = computed(() => {
223
+ if (!props.modelValue) return ''
224
+
225
+ if (props.mode === 'single') {
226
+ const date = new Date(props.modelValue as string)
227
+ return formatDate(date, props.format)
228
+ }
229
+
230
+ if (props.mode === 'range') {
231
+ const range = props.modelValue as { start: string; end: string }
232
+ if (!range.start || !range.end) return ''
233
+ const startDate = formatDate(new Date(range.start), props.format)
234
+ const endDate = formatDate(new Date(range.end), props.format)
235
+ return `${startDate} - ${endDate}`
236
+ }
237
+
238
+ if (props.mode === 'multiple') {
239
+ const dates = props.modelValue as string[]
240
+ if (dates.length === 0) return ''
241
+ if (dates.length === 1) return formatDate(new Date(dates[0]), props.format)
242
+ return `${dates.length} dates selected`
243
+ }
244
+
245
+ return ''
246
+ })
247
+
248
+ const currentMonthYear = computed(() => {
249
+ const month = currentDate.value.toLocaleString('default', { month: 'long' })
250
+ const year = currentDate.value.getFullYear()
251
+ return `${month} ${year}`
252
+ })
54
253
 
55
- watch(() => props.modelValue, (newValue) => {
56
- internalValue.value = newValue
254
+ const availableYears = computed(() => {
255
+ const currentYear = new Date().getFullYear()
256
+ const years = []
257
+ for (let i = currentYear - 50; i <= currentYear + 10; i++) {
258
+ years.push(i)
259
+ }
260
+ return years
57
261
  })
58
262
 
59
- const handleChange = () => {
60
- emit('update:modelValue', internalValue.value)
263
+ const calendarDays = computed(() => {
264
+ const year = currentDate.value.getFullYear()
265
+ const month = currentDate.value.getMonth()
266
+
267
+ const firstDay = new Date(year, month, 1)
268
+ const startDate = new Date(firstDay)
269
+ startDate.setDate(startDate.getDate() - firstDay.getDay())
270
+
271
+ const days = []
272
+ const today = new Date()
273
+
274
+ for (let i = 0; i < 42; i++) {
275
+ const date = new Date(startDate)
276
+ date.setDate(startDate.getDate() + i)
277
+
278
+ const dateString = date.toISOString().split('T')[0]
279
+ const isOtherMonth = date.getMonth() !== month
280
+ const isDisabled = isDateDisabled(date)
281
+ const isSelected = isDateSelected(dateString)
282
+ const isToday = isSameDate(date, today)
283
+
284
+ days.push({
285
+ date: date.getDate(),
286
+ month: date.getMonth(),
287
+ year: date.getFullYear(),
288
+ dateString,
289
+ otherMonth: isOtherMonth,
290
+ disabled: isDisabled,
291
+ selected: isSelected,
292
+ today: isToday,
293
+ inRange: false,
294
+ rangeStart: false,
295
+ rangeEnd: false
296
+ })
297
+ }
298
+
299
+ return days
300
+ })
301
+
302
+ const formatDate = (date: Date, format: string): string => {
303
+ const day = date.getDate().toString().padStart(2, '0')
304
+ const month = (date.getMonth() + 1).toString().padStart(2, '0')
305
+ const year = date.getFullYear().toString()
306
+
307
+ return format
308
+ .replace('DD', day)
309
+ .replace('MM', month)
310
+ .replace('YYYY', year)
311
+ }
312
+
313
+ const isSameDate = (date1: Date, date2: Date): boolean => {
314
+ return date1.getFullYear() === date2.getFullYear() &&
315
+ date1.getMonth() === date2.getMonth() &&
316
+ date1.getDate() === date2.getDate()
317
+ }
318
+
319
+ const isDateDisabled = (date: Date): boolean => {
320
+ const dateString = date.toISOString().split('T')[0]
321
+
322
+ if (props.min && date < new Date(props.min)) return true
323
+ if (props.max && date > new Date(props.max)) return true
324
+ if (props.disabledDates?.includes(dateString)) return true
325
+ if (props.disabledWeekdays?.includes(date.getDay())) return true
326
+ if (props.enabledDates && !props.enabledDates.includes(dateString)) return true
327
+
328
+ return false
329
+ }
330
+
331
+ const isDateSelected = (dateString: string): boolean => {
332
+ if (props.mode === 'single') {
333
+ return props.modelValue === dateString
334
+ }
335
+
336
+ if (props.mode === 'range') {
337
+ const range = props.modelValue as { start: string; end: string }
338
+ return range?.start === dateString || range?.end === dateString
339
+ }
340
+
341
+ if (props.mode === 'multiple') {
342
+ const dates = props.modelValue as string[]
343
+ return dates?.includes(dateString) || false
344
+ }
345
+
346
+ return false
347
+ }
348
+
349
+ const getDateAriaLabel = (day: any): string => {
350
+ const date = new Date(day.year, day.month, day.date)
351
+ const dateString = date.toLocaleDateString('en-US', {
352
+ weekday: 'long',
353
+ year: 'numeric',
354
+ month: 'long',
355
+ day: 'numeric'
356
+ })
357
+
358
+ let label = dateString
359
+ if (day.today) label += ', today'
360
+ if (day.selected) label += ', selected'
361
+ if (day.disabled) label += ', disabled'
362
+
363
+ return label
364
+ }
365
+
366
+ const toggleCalendar = () => {
367
+ if (!props.disabled && !props.readonly) {
368
+ isOpen.value = !isOpen.value
369
+ if (isOpen.value) {
370
+ emit('open')
371
+ } else {
372
+ emit('close')
373
+ }
374
+ }
375
+ }
376
+
377
+ const handleFocus = () => {
378
+ if (!props.disabled && !props.readonly) {
379
+ isOpen.value = true
380
+ emit('open')
381
+ }
382
+ }
383
+
384
+ const handleKeydown = (event: KeyboardEvent) => {
385
+ if (event.key === 'Enter' || event.key === ' ') {
386
+ event.preventDefault()
387
+ toggleCalendar()
388
+ } else if (event.key === 'Escape') {
389
+ isOpen.value = false
390
+ emit('close')
391
+ }
392
+ }
393
+
394
+ const previousMonth = () => {
395
+ currentDate.value = new Date(currentDate.value.getFullYear(), currentDate.value.getMonth() - 1, 1)
396
+ }
397
+
398
+ const nextMonth = () => {
399
+ currentDate.value = new Date(currentDate.value.getFullYear(), currentDate.value.getMonth() + 1, 1)
400
+ }
401
+
402
+ const toggleYearPicker = () => {
403
+ showYearPicker.value = !showYearPicker.value
404
+ }
405
+
406
+ const selectYear = (year: number) => {
407
+ currentDate.value = new Date(year, currentDate.value.getMonth(), 1)
408
+ showYearPicker.value = false
409
+ }
410
+
411
+ const selectDate = (day: any) => {
412
+ if (day.disabled) return
413
+
414
+ const dateString = day.dateString
415
+
416
+ if (props.mode === 'single') {
417
+ emit('update:modelValue', dateString)
418
+ if (props.closeOnSelect) {
419
+ isOpen.value = false
420
+ emit('close')
421
+ }
422
+ }
423
+
424
+ if (props.mode === 'range') {
425
+ const currentRange = (props.modelValue as { start: string; end: string }) || { start: '', end: '' }
426
+
427
+ if (!currentRange.start || (currentRange.start && currentRange.end)) {
428
+ emit('update:modelValue', { start: dateString, end: '' })
429
+ } else {
430
+ const start = new Date(currentRange.start)
431
+ const selected = new Date(dateString)
432
+
433
+ if (selected < start) {
434
+ emit('update:modelValue', { start: dateString, end: currentRange.start })
435
+ } else {
436
+ emit('update:modelValue', { start: currentRange.start, end: dateString })
437
+ }
438
+
439
+ if (props.closeOnSelect) {
440
+ isOpen.value = false
441
+ emit('close')
442
+ }
443
+ }
444
+ }
445
+
446
+ if (props.mode === 'multiple') {
447
+ const currentDates = (props.modelValue as string[]) || []
448
+ const index = currentDates.indexOf(dateString)
449
+
450
+ if (index >= 0) {
451
+ const newDates = [...currentDates]
452
+ newDates.splice(index, 1)
453
+ emit('update:modelValue', newDates)
454
+ } else {
455
+ emit('update:modelValue', [...currentDates, dateString])
456
+ }
457
+ }
458
+ }
459
+
460
+ const selectToday = () => {
461
+ const today = new Date().toISOString().split('T')[0]
462
+
463
+ if (props.mode === 'single') {
464
+ emit('update:modelValue', today)
465
+ } else if (props.mode === 'range') {
466
+ emit('update:modelValue', { start: today, end: today })
467
+ } else if (props.mode === 'multiple') {
468
+ const currentDates = (props.modelValue as string[]) || []
469
+ if (!currentDates.includes(today)) {
470
+ emit('update:modelValue', [...currentDates, today])
471
+ }
472
+ }
473
+
474
+ isOpen.value = false
475
+ emit('close')
476
+ }
477
+
478
+ const applyShortcut = (shortcut: Shortcut) => {
479
+ emit('update:modelValue', shortcut.value as DateValue)
480
+ isOpen.value = false
481
+ emit('close')
482
+ }
483
+
484
+ const clearDate = () => {
485
+ if (props.mode === 'single') {
486
+ emit('update:modelValue', '')
487
+ } else if (props.mode === 'range') {
488
+ emit('update:modelValue', { start: '', end: '' })
489
+ } else if (props.mode === 'multiple') {
490
+ emit('update:modelValue', [])
491
+ }
492
+ emit('clear')
493
+ }
494
+
495
+ const handleClickOutside = (event: Event) => {
496
+ if (datepickerRef.value && !datepickerRef.value.contains(event.target as Node)) {
497
+ isOpen.value = false
498
+ showYearPicker.value = false
499
+ emit('close')
500
+ }
61
501
  }
502
+
503
+ const checkMobile = () => {
504
+ isMobile.value = window.innerWidth <= 640
505
+ }
506
+
507
+ onMounted(() => {
508
+ document.addEventListener('click', handleClickOutside)
509
+ window.addEventListener('resize', checkMobile)
510
+ checkMobile()
511
+ })
512
+
513
+ onUnmounted(() => {
514
+ document.removeEventListener('click', handleClickOutside)
515
+ window.removeEventListener('resize', checkMobile)
516
+ })
62
517
  </script>
63
518
 
64
519
  <style scoped>
65
520
  .dm-datepicker {
66
- display: flex;
67
- flex-direction: column;
68
- gap: var(--dm-space-2);
521
+ position: relative;
522
+ width: 100%;
69
523
  }
70
524
 
71
525
  .dm-datepicker__label {
72
- color: var(--dm-text-primary);
73
- font-size: var(--dm-text-sm);
526
+ display: block;
527
+ margin-bottom: 0.5rem;
74
528
  font-weight: 500;
529
+ color: var(--dm-neutral-700, #374151);
75
530
  }
76
531
 
77
532
  .dm-datepicker__required {
78
- color: var(--dm-error);
533
+ color: var(--dm-error, #ef4444);
79
534
  }
80
535
 
81
536
  .dm-datepicker__wrapper {
@@ -84,57 +539,220 @@ const handleChange = () => {
84
539
 
85
540
  .dm-datepicker__input {
86
541
  width: 100%;
87
- min-height: 44px;
88
- padding: var(--dm-space-3);
89
- border: 1px solid var(--dm-gray-300);
90
- border-radius: var(--dm-radius);
91
- font-size: var(--dm-text-base);
92
- color: var(--dm-text-primary);
93
- background: var(--dm-white);
94
- transition: var(--dm-transition);
95
- font-family: inherit;
542
+ padding: 0.75rem;
543
+ border: 1px solid var(--dm-neutral-300, #d1d5db);
544
+ border-radius: var(--dm-radius-md, 0.375rem);
545
+ font-size: 1rem;
546
+ cursor: pointer;
547
+ transition: border-color 0.2s;
96
548
  }
97
549
 
98
- .dm-datepicker__input:hover:not(:disabled) {
99
- border-color: var(--dm-gray-400);
550
+ .dm-datepicker__input:focus {
551
+ outline: none;
552
+ border-color: var(--dm-primary, #0072ce);
553
+ box-shadow: 0 0 0 3px rgba(0, 114, 206, 0.1);
100
554
  }
101
555
 
102
- .dm-datepicker__input:focus {
103
- outline: var(--dm-focus-ring);
104
- outline-offset: 0;
105
- border-color: var(--dm-primary);
556
+ .dm-datepicker__input--error {
557
+ border-color: var(--dm-error, #ef4444);
558
+ }
559
+
560
+ .dm-datepicker__clear {
561
+ position: absolute;
562
+ right: 0.75rem;
563
+ top: 50%;
564
+ transform: translateY(-50%);
565
+ background: none;
566
+ border: none;
567
+ font-size: 1.25rem;
568
+ cursor: pointer;
569
+ color: var(--dm-neutral-500, #6b7280);
570
+ }
571
+
572
+ .dm-datepicker__calendar {
573
+ position: absolute;
574
+ top: 100%;
575
+ left: 0;
576
+ z-index: 1000;
577
+ background: white;
578
+ border: 1px solid var(--dm-neutral-300, #d1d5db);
579
+ border-radius: var(--dm-radius-md, 0.375rem);
580
+ box-shadow: var(--dm-shadow-lg, 0 10px 15px -3px rgba(0, 0, 0, 0.1));
581
+ padding: 1rem;
582
+ min-width: 280px;
583
+ }
584
+
585
+ @media (max-width: 640px) {
586
+ .dm-datepicker__calendar {
587
+ position: fixed;
588
+ top: 50%;
589
+ left: 50%;
590
+ transform: translate(-50%, -50%);
591
+ width: 90vw;
592
+ max-width: 320px;
593
+ }
594
+ }
595
+
596
+ .dm-datepicker__header {
597
+ display: flex;
598
+ align-items: center;
599
+ justify-content: space-between;
600
+ margin-bottom: 1rem;
601
+ }
602
+
603
+ .dm-datepicker__nav {
604
+ background: none;
605
+ border: none;
606
+ font-size: 1.25rem;
607
+ cursor: pointer;
608
+ padding: 0.25rem;
609
+ border-radius: var(--dm-radius-sm, 0.25rem);
610
+ transition: background-color 0.2s;
106
611
  }
107
612
 
108
- .dm-datepicker__input:disabled {
109
- background: var(--dm-gray-100);
613
+ .dm-datepicker__nav:hover {
614
+ background-color: var(--dm-neutral-100, #f3f4f6);
615
+ }
616
+
617
+ .dm-datepicker__month-year {
618
+ background: none;
619
+ border: none;
620
+ font-weight: 600;
621
+ cursor: pointer;
622
+ padding: 0.5rem;
623
+ border-radius: var(--dm-radius-sm, 0.25rem);
624
+ transition: background-color 0.2s;
625
+ }
626
+
627
+ .dm-datepicker__month-year:hover {
628
+ background-color: var(--dm-neutral-100, #f3f4f6);
629
+ }
630
+
631
+ .dm-datepicker__shortcuts {
632
+ display: flex;
633
+ gap: 0.5rem;
634
+ margin-bottom: 1rem;
635
+ flex-wrap: wrap;
636
+ }
637
+
638
+ .dm-datepicker__shortcut {
639
+ background: none;
640
+ border: 1px solid var(--dm-neutral-300, #d1d5db);
641
+ padding: 0.25rem 0.5rem;
642
+ border-radius: var(--dm-radius-sm, 0.25rem);
643
+ cursor: pointer;
644
+ font-size: 0.875rem;
645
+ transition: all 0.2s;
646
+ }
647
+
648
+ .dm-datepicker__shortcut:hover {
649
+ background-color: var(--dm-neutral-100, #f3f4f6);
650
+ }
651
+
652
+ .dm-datepicker__year-picker {
653
+ display: grid;
654
+ grid-template-columns: repeat(4, 1fr);
655
+ gap: 0.5rem;
656
+ max-height: 200px;
657
+ overflow-y: auto;
658
+ }
659
+
660
+ .dm-datepicker__year-option {
661
+ padding: 0.5rem;
662
+ text-align: center;
663
+ cursor: pointer;
664
+ border-radius: var(--dm-radius-sm, 0.25rem);
665
+ transition: background-color 0.2s;
666
+ }
667
+
668
+ .dm-datepicker__year-option:hover {
669
+ background-color: var(--dm-neutral-100, #f3f4f6);
670
+ }
671
+
672
+ .dm-datepicker__year-option--current {
673
+ background-color: var(--dm-primary, #0072ce);
674
+ color: white;
675
+ }
676
+
677
+ .dm-datepicker__weekdays {
678
+ display: grid;
679
+ grid-template-columns: repeat(7, 1fr);
680
+ gap: 0.25rem;
681
+ margin-bottom: 0.5rem;
682
+ }
683
+
684
+ .dm-datepicker__weekday {
685
+ padding: 0.5rem;
686
+ text-align: center;
687
+ font-size: 0.875rem;
688
+ font-weight: 600;
689
+ color: var(--dm-neutral-600, #4b5563);
690
+ }
691
+
692
+ .dm-datepicker__days {
693
+ display: grid;
694
+ grid-template-columns: repeat(7, 1fr);
695
+ gap: 0.25rem;
696
+ }
697
+
698
+ .dm-datepicker__day {
699
+ padding: 0.5rem;
700
+ text-align: center;
701
+ cursor: pointer;
702
+ border: none;
703
+ background: none;
704
+ border-radius: var(--dm-radius-sm, 0.25rem);
705
+ transition: background-color 0.2s;
706
+ }
707
+
708
+ .dm-datepicker__day:hover:not(.dm-datepicker__day--disabled) {
709
+ background-color: var(--dm-neutral-100, #f3f4f6);
710
+ }
711
+
712
+ .dm-datepicker__day--other-month {
713
+ color: var(--dm-neutral-400, #9ca3af);
714
+ }
715
+
716
+ .dm-datepicker__day--disabled {
717
+ color: var(--dm-neutral-300, #d1d5db);
110
718
  cursor: not-allowed;
111
- opacity: 0.6;
112
719
  }
113
720
 
114
- .dm-datepicker__input--error {
115
- border-color: var(--dm-error);
721
+ .dm-datepicker__day--selected {
722
+ background-color: var(--dm-primary, #0072ce);
723
+ color: white;
116
724
  }
117
725
 
118
- .dm-datepicker__input--error:focus {
119
- outline-color: var(--dm-error);
726
+ .dm-datepicker__day--today {
727
+ font-weight: 600;
728
+ color: var(--dm-primary, #0072ce);
120
729
  }
121
730
 
122
- .dm-datepicker__error {
123
- color: var(--dm-error);
124
- font-size: var(--dm-text-sm);
125
- margin: 0;
731
+ .dm-datepicker__footer {
732
+ margin-top: 1rem;
733
+ padding-top: 1rem;
734
+ border-top: 1px solid var(--dm-neutral-200, #e5e7eb);
735
+ text-align: center;
126
736
  }
127
737
 
128
- @media (prefers-color-scheme: dark) {
129
- .dm-datepicker__input {
130
- background: var(--dm-gray-800);
131
- border-color: var(--dm-gray-600);
132
- color: var(--dm-white);
133
- color-scheme: dark;
134
- }
738
+ .dm-datepicker__today {
739
+ background: none;
740
+ border: 1px solid var(--dm-primary, #0072ce);
741
+ color: var(--dm-primary, #0072ce);
742
+ padding: 0.5rem 1rem;
743
+ border-radius: var(--dm-radius-sm, 0.25rem);
744
+ cursor: pointer;
745
+ transition: all 0.2s;
746
+ }
135
747
 
136
- .dm-datepicker__input:disabled {
137
- background: var(--dm-gray-900);
138
- }
748
+ .dm-datepicker__today:hover {
749
+ background-color: var(--dm-primary, #0072ce);
750
+ color: white;
751
+ }
752
+
753
+ .dm-datepicker__error {
754
+ margin-top: 0.25rem;
755
+ color: var(--dm-error, #ef4444);
756
+ font-size: 0.875rem;
139
757
  }
140
- </style>
758
+ </style>