@saasmakers/ui 2.0.68 → 2.0.70
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/components/bases/BaseChip.vue +125 -0
- package/app/components/fields/FieldCombobox.vue +59 -209
- package/app/components/fields/FieldDate.vue +758 -0
- package/app/components/fields/FieldSelect.vue +2 -2
- package/app/composables/useChipNavigation.ts +178 -0
- package/app/composables/useRelativeDate.ts +97 -0
- package/app/i18n/relativeDate.ts +144 -0
- package/app/types/bases.d.ts +26 -0
- package/app/types/fields.d.ts +24 -0
- package/app/types/global.d.ts +6 -0
- package/app/utils/dateField.ts +104 -0
- package/app/utils/layerIcons.ts +1 -0
- package/package.json +1 -1
- package/uno.config.ts +2 -0
|
@@ -0,0 +1,758 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import type { FieldDate, FieldDatePreset } from '../../types/fields'
|
|
3
|
+
import { getRelativeDateMessage } from '../../i18n/relativeDate'
|
|
4
|
+
import { buildCalendarGrid, formatMonthLabel, formatPresetSub, getNextMonday, getNextSaturday, isBeforeDay, isSameDay, resolveLocaleCode, startOfLocalDay } from '../../utils/dateField'
|
|
5
|
+
|
|
6
|
+
const props = withDefaults(defineProps<Omit<FieldDate, 'modelValue'>>(), {
|
|
7
|
+
background: 'white',
|
|
8
|
+
description: '',
|
|
9
|
+
disabled: false,
|
|
10
|
+
hideError: true,
|
|
11
|
+
label: '',
|
|
12
|
+
labelIcon: undefined,
|
|
13
|
+
min: undefined,
|
|
14
|
+
placeholder: '',
|
|
15
|
+
presets: undefined,
|
|
16
|
+
required: false,
|
|
17
|
+
size: 'base',
|
|
18
|
+
validation: undefined,
|
|
19
|
+
weekStartsOn: 1,
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const emit = defineEmits<{
|
|
23
|
+
close: []
|
|
24
|
+
open: []
|
|
25
|
+
}>()
|
|
26
|
+
|
|
27
|
+
const { locale, t } = useI18n()
|
|
28
|
+
|
|
29
|
+
const localeCode = computed(() => {
|
|
30
|
+
return resolveLocaleCode(locale.value)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const id = useId()
|
|
34
|
+
const chipListId = `${id}-presets`
|
|
35
|
+
const modelValue = defineModel<FieldDate['modelValue']>({ default: null })
|
|
36
|
+
const opened = ref(false)
|
|
37
|
+
const calendarOpen = ref(false)
|
|
38
|
+
const panelRef = ref<HTMLElement>()
|
|
39
|
+
const chipListRef = ref<HTMLElement>()
|
|
40
|
+
const viewMonth = ref(startOfLocalDay(new Date()))
|
|
41
|
+
const focusedDayIndex = ref(0)
|
|
42
|
+
const { absoluteLabel, isOverdue, relativeLabel } = useRelativeDate(modelValue)
|
|
43
|
+
const prefersReducedMotion = usePreferredReducedMotion()
|
|
44
|
+
|
|
45
|
+
const panelAnimation = computed(() => {
|
|
46
|
+
if (prefersReducedMotion.value === 'reduce') {
|
|
47
|
+
return {
|
|
48
|
+
animate: { opacity: 1 },
|
|
49
|
+
initial: { opacity: 1 },
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return fadeIn
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const triggerIconSize = computed<BaseSize>(() => {
|
|
57
|
+
switch (props.size) {
|
|
58
|
+
case 'lg':
|
|
59
|
+
return '2xl'
|
|
60
|
+
case 'sm':
|
|
61
|
+
return 'lg'
|
|
62
|
+
default:
|
|
63
|
+
return 'xl'
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const clearIconSize = computed<BaseSize>(() => {
|
|
68
|
+
switch (props.size) {
|
|
69
|
+
case 'lg':
|
|
70
|
+
return 'lg'
|
|
71
|
+
case 'sm':
|
|
72
|
+
return 'base'
|
|
73
|
+
default:
|
|
74
|
+
return 'base'
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
const today = computed(() => {
|
|
79
|
+
return startOfLocalDay(new Date())
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const computedPresets = computed(() => {
|
|
83
|
+
if (props.presets) {
|
|
84
|
+
return props.presets
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const currentToday = today.value
|
|
88
|
+
const weekday = currentToday.getDay()
|
|
89
|
+
const tomorrow = new Date(currentToday)
|
|
90
|
+
|
|
91
|
+
tomorrow.setDate(tomorrow.getDate() + 1)
|
|
92
|
+
|
|
93
|
+
const tomorrowDate = startOfLocalDay(tomorrow)
|
|
94
|
+
|
|
95
|
+
const items: FieldDatePreset[] = [
|
|
96
|
+
{
|
|
97
|
+
date: currentToday,
|
|
98
|
+
id: 'today',
|
|
99
|
+
label: getRelativeDateMessage(localeCode.value, 'today'),
|
|
100
|
+
sub: formatPresetSub(currentToday, localeCode.value),
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
date: tomorrowDate,
|
|
104
|
+
id: 'tomorrow',
|
|
105
|
+
label: getRelativeDateMessage(localeCode.value, 'tomorrow'),
|
|
106
|
+
sub: formatPresetSub(tomorrowDate, localeCode.value),
|
|
107
|
+
},
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
if (weekday !== 0 && weekday !== 6) {
|
|
111
|
+
const weekendDate = getNextSaturday(currentToday)
|
|
112
|
+
|
|
113
|
+
items.push({
|
|
114
|
+
date: weekendDate,
|
|
115
|
+
id: 'weekend',
|
|
116
|
+
label: t('weekend'),
|
|
117
|
+
sub: formatPresetSub(weekendDate, localeCode.value),
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const nextWeekDate = getNextMonday(currentToday)
|
|
122
|
+
|
|
123
|
+
items.push({
|
|
124
|
+
date: nextWeekDate,
|
|
125
|
+
id: 'nextWeek',
|
|
126
|
+
label: t('nextWeek'),
|
|
127
|
+
sub: formatPresetSub(nextWeekDate, localeCode.value),
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
return items
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
const chooseChipIndex = computed(() => {
|
|
134
|
+
return computedPresets.value.length
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const chipsCount = computed(() => {
|
|
138
|
+
return chooseChipIndex.value + 1
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
const { highlightedIndex, onArrowKey, onHighlight, resetHighlight } = useChipNavigation({
|
|
142
|
+
containerRef: chipListRef,
|
|
143
|
+
count: chipsCount,
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
const calendarCells = computed(() => {
|
|
147
|
+
return buildCalendarGrid({
|
|
148
|
+
viewMonth: viewMonth.value,
|
|
149
|
+
weekStartsOn: props.weekStartsOn,
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
const monthLabel = computed(() => {
|
|
154
|
+
return formatMonthLabel(viewMonth.value, localeCode.value)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
const weekdayLabels = computed(() => {
|
|
158
|
+
const labels: string[] = []
|
|
159
|
+
const referenceMonday = new Date(2024, 0, 1)
|
|
160
|
+
|
|
161
|
+
for (let index = 0; index < 7; index += 1) {
|
|
162
|
+
const date = new Date(referenceMonday)
|
|
163
|
+
|
|
164
|
+
date.setDate(referenceMonday.getDate() + ((props.weekStartsOn + index - 1 + 7) % 7))
|
|
165
|
+
labels.push(new Intl.DateTimeFormat(localeCode.value, { weekday: 'narrow' }).format(date))
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return labels
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
watch(opened, (isOpened) => {
|
|
172
|
+
if (isOpened) {
|
|
173
|
+
emit('open')
|
|
174
|
+
resetHighlight(resolveInitialChipHighlight())
|
|
175
|
+
|
|
176
|
+
nextTick(() => {
|
|
177
|
+
chipListRef.value?.focus()
|
|
178
|
+
|
|
179
|
+
panelRef.value?.scrollIntoView({
|
|
180
|
+
behavior: prefersReducedMotion.value === 'reduce' ? 'auto' : 'smooth',
|
|
181
|
+
block: 'nearest',
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
calendarOpen.value = false
|
|
187
|
+
|
|
188
|
+
emit('close')
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
watch(modelValue, (value) => {
|
|
193
|
+
if (value) {
|
|
194
|
+
viewMonth.value = startOfLocalDay(new Date(value.getFullYear(), value.getMonth(), 1))
|
|
195
|
+
}
|
|
196
|
+
}, { immediate: true })
|
|
197
|
+
|
|
198
|
+
function closePanel() {
|
|
199
|
+
opened.value = false
|
|
200
|
+
calendarOpen.value = false
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function isDayDisabled(date: Date) {
|
|
204
|
+
return isBeforeDay(date, props.min)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function isPresetSelected(preset: FieldDatePreset) {
|
|
208
|
+
return modelValue.value ? isSameDay(preset.date, modelValue.value) : false
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function onChipEnter() {
|
|
212
|
+
const index = highlightedIndex.value
|
|
213
|
+
|
|
214
|
+
if (index === chooseChipIndex.value) {
|
|
215
|
+
onOpenCalendar()
|
|
216
|
+
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const preset = computedPresets.value[index]
|
|
221
|
+
|
|
222
|
+
if (preset) {
|
|
223
|
+
onPresetClick(preset)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function onChipListKeyDown(event: KeyboardEvent) {
|
|
228
|
+
if (event.key === 'ArrowDown') {
|
|
229
|
+
onArrowKey(event, 'down')
|
|
230
|
+
}
|
|
231
|
+
else if (event.key === 'ArrowUp') {
|
|
232
|
+
onArrowKey(event, 'up')
|
|
233
|
+
}
|
|
234
|
+
else if (event.key === 'ArrowLeft') {
|
|
235
|
+
onArrowKey(event, 'left')
|
|
236
|
+
}
|
|
237
|
+
else if (event.key === 'ArrowRight') {
|
|
238
|
+
onArrowKey(event, 'right')
|
|
239
|
+
}
|
|
240
|
+
else if (event.key === 'Enter') {
|
|
241
|
+
event.preventDefault()
|
|
242
|
+
onChipEnter()
|
|
243
|
+
}
|
|
244
|
+
else if (event.key === 'Escape') {
|
|
245
|
+
event.preventDefault()
|
|
246
|
+
closePanel()
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function onChipMouseDown(event: MouseEvent) {
|
|
251
|
+
event.preventDefault()
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function onClearClick(event: MouseEvent) {
|
|
255
|
+
event.stopPropagation()
|
|
256
|
+
|
|
257
|
+
modelValue.value = null
|
|
258
|
+
|
|
259
|
+
closePanel()
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function onCloseCalendar() {
|
|
263
|
+
calendarOpen.value = false
|
|
264
|
+
|
|
265
|
+
resetHighlight(chooseChipIndex.value)
|
|
266
|
+
|
|
267
|
+
nextTick(() => {
|
|
268
|
+
chipListRef.value?.focus()
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function onContainerClick() {
|
|
273
|
+
if (!props.disabled) {
|
|
274
|
+
opened.value = !opened.value
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function onContainerKeyDown(event: KeyboardEvent) {
|
|
279
|
+
if (!props.disabled && ['Enter', 'Space'].includes(event.code)) {
|
|
280
|
+
event.preventDefault()
|
|
281
|
+
|
|
282
|
+
opened.value = !opened.value
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function onDayClick(date: Date) {
|
|
287
|
+
if (!isDayDisabled(date)) {
|
|
288
|
+
selectDate(date)
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function onDayKeyDown(event: KeyboardEvent, date: Date, index: number) {
|
|
293
|
+
if (event.key === 'Escape') {
|
|
294
|
+
event.preventDefault()
|
|
295
|
+
onCloseCalendar()
|
|
296
|
+
|
|
297
|
+
return
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (isDayDisabled(date)) {
|
|
301
|
+
return
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
305
|
+
event.preventDefault()
|
|
306
|
+
selectDate(date)
|
|
307
|
+
}
|
|
308
|
+
else if (event.key === 'ArrowLeft') {
|
|
309
|
+
event.preventDefault()
|
|
310
|
+
|
|
311
|
+
focusedDayIndex.value = Math.max(0, index - 1)
|
|
312
|
+
}
|
|
313
|
+
else if (event.key === 'ArrowRight') {
|
|
314
|
+
event.preventDefault()
|
|
315
|
+
|
|
316
|
+
focusedDayIndex.value = Math.min(calendarCells.value.length - 1, index + 1)
|
|
317
|
+
}
|
|
318
|
+
else if (event.key === 'ArrowUp') {
|
|
319
|
+
event.preventDefault()
|
|
320
|
+
|
|
321
|
+
focusedDayIndex.value = Math.max(0, index - 7)
|
|
322
|
+
}
|
|
323
|
+
else if (event.key === 'ArrowDown') {
|
|
324
|
+
event.preventDefault()
|
|
325
|
+
|
|
326
|
+
focusedDayIndex.value = Math.min(calendarCells.value.length - 1, index + 7)
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function onNextMonth() {
|
|
331
|
+
viewMonth.value = new Date(viewMonth.value.getFullYear(), viewMonth.value.getMonth() + 1, 1)
|
|
332
|
+
|
|
333
|
+
nextTick(() => {
|
|
334
|
+
syncFocusedDayIndex()
|
|
335
|
+
panelRef.value?.querySelector<HTMLElement>('[role="gridcell"][tabindex="0"]')?.focus()
|
|
336
|
+
})
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function onOpenCalendar() {
|
|
340
|
+
calendarOpen.value = true
|
|
341
|
+
|
|
342
|
+
resetHighlight(chooseChipIndex.value)
|
|
343
|
+
syncFocusedDayIndex()
|
|
344
|
+
|
|
345
|
+
nextTick(() => {
|
|
346
|
+
panelRef.value?.scrollIntoView({
|
|
347
|
+
behavior: prefersReducedMotion.value === 'reduce' ? 'auto' : 'smooth',
|
|
348
|
+
block: 'nearest',
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
panelRef.value?.querySelector<HTMLElement>('[role="gridcell"][tabindex="0"]')?.focus()
|
|
352
|
+
})
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function onPresetClick(preset: FieldDatePreset) {
|
|
356
|
+
selectDate(preset.date)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function onPreviousMonth() {
|
|
360
|
+
viewMonth.value = new Date(viewMonth.value.getFullYear(), viewMonth.value.getMonth() - 1, 1)
|
|
361
|
+
|
|
362
|
+
nextTick(() => {
|
|
363
|
+
syncFocusedDayIndex()
|
|
364
|
+
panelRef.value?.querySelector<HTMLElement>('[role="gridcell"][tabindex="0"]')?.focus()
|
|
365
|
+
})
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function resolveInitialChipHighlight() {
|
|
369
|
+
const selectedIndex = computedPresets.value.findIndex(preset => isPresetSelected(preset))
|
|
370
|
+
|
|
371
|
+
if (selectedIndex >= 0) {
|
|
372
|
+
return selectedIndex
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (calendarOpen.value) {
|
|
376
|
+
return chooseChipIndex.value
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return 0
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function selectDate(date: Date) {
|
|
383
|
+
modelValue.value = startOfLocalDay(date)
|
|
384
|
+
|
|
385
|
+
closePanel()
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function syncFocusedDayIndex() {
|
|
389
|
+
const selectedIndex = calendarCells.value.findIndex(cell => modelValue.value && isSameDay(cell.date, modelValue.value))
|
|
390
|
+
|
|
391
|
+
focusedDayIndex.value = selectedIndex >= 0 ? selectedIndex : calendarCells.value.findIndex(cell => cell.inMonth)
|
|
392
|
+
}
|
|
393
|
+
</script>
|
|
394
|
+
|
|
395
|
+
<template>
|
|
396
|
+
<div>
|
|
397
|
+
<FieldLabel
|
|
398
|
+
v-if="label"
|
|
399
|
+
:disabled="disabled"
|
|
400
|
+
:for-field="id"
|
|
401
|
+
has-margin-bottom
|
|
402
|
+
:icon="labelIcon"
|
|
403
|
+
:label="label"
|
|
404
|
+
:required="required"
|
|
405
|
+
:size="size"
|
|
406
|
+
/>
|
|
407
|
+
|
|
408
|
+
<div
|
|
409
|
+
class="select-none overflow-hidden base-box field-uppercase"
|
|
410
|
+
:class="{
|
|
411
|
+
'field-disabled': disabled,
|
|
412
|
+
'base-border-interactive-focus-within': !disabled,
|
|
413
|
+
'bg-gray-100 dark:bg-gray-900': background === 'gray',
|
|
414
|
+
'bg-white dark:bg-gray-900': background === 'white',
|
|
415
|
+
}"
|
|
416
|
+
>
|
|
417
|
+
<div
|
|
418
|
+
:id="id"
|
|
419
|
+
class="flex cursor-pointer items-center gap-2 outline-none"
|
|
420
|
+
:class="{
|
|
421
|
+
'h-10': size === 'sm',
|
|
422
|
+
'h-12': size === 'base',
|
|
423
|
+
'h-14': size === 'lg',
|
|
424
|
+
'pl-3 pr-0': modelValue,
|
|
425
|
+
'px-3': !modelValue,
|
|
426
|
+
}"
|
|
427
|
+
role="button"
|
|
428
|
+
:aria-disabled="disabled"
|
|
429
|
+
:aria-expanded="opened"
|
|
430
|
+
tabindex="0"
|
|
431
|
+
@click="onContainerClick"
|
|
432
|
+
@keydown="onContainerKeyDown"
|
|
433
|
+
>
|
|
434
|
+
<BaseIcon
|
|
435
|
+
class="shrink-0"
|
|
436
|
+
color="gray"
|
|
437
|
+
:icon="getLayerIcon('calendar')"
|
|
438
|
+
:size="triggerIconSize"
|
|
439
|
+
/>
|
|
440
|
+
|
|
441
|
+
<div
|
|
442
|
+
class="min-w-0 flex-1 truncate font-medium leading-normal tracking-tight uppercase"
|
|
443
|
+
:class="{
|
|
444
|
+
'pr-2': modelValue,
|
|
445
|
+
'text-xs': size === 'sm',
|
|
446
|
+
'text-sm': size === 'base',
|
|
447
|
+
'text-base': size === 'lg',
|
|
448
|
+
}"
|
|
449
|
+
>
|
|
450
|
+
<template v-if="modelValue">
|
|
451
|
+
<span
|
|
452
|
+
:class="{
|
|
453
|
+
'text-orange-600 dark:text-orange-400': isOverdue,
|
|
454
|
+
'text-gray-900 dark:text-gray-100': !isOverdue,
|
|
455
|
+
}"
|
|
456
|
+
>{{ relativeLabel }}</span><span class="text-gray-600 font-medium dark:text-gray-400"> · {{ absoluteLabel }}</span>
|
|
457
|
+
</template>
|
|
458
|
+
|
|
459
|
+
<span
|
|
460
|
+
v-else
|
|
461
|
+
class="truncate text-gray-600 dark:text-gray-400"
|
|
462
|
+
>
|
|
463
|
+
{{ placeholder || t('placeholder') }}
|
|
464
|
+
</span>
|
|
465
|
+
</div>
|
|
466
|
+
|
|
467
|
+
<button
|
|
468
|
+
v-if="modelValue"
|
|
469
|
+
:aria-label="t('clear')"
|
|
470
|
+
class="mr-3 inline-flex shrink-0 items-center self-center justify-center rounded-full base-control-hover p-1 text-gray-600 outline-none transition-[transform,colors] hover:scale-105 dark:text-gray-400"
|
|
471
|
+
type="button"
|
|
472
|
+
@click="onClearClick"
|
|
473
|
+
>
|
|
474
|
+
<BaseIcon
|
|
475
|
+
:icon="getLayerIcon('close')"
|
|
476
|
+
:size="clearIconSize"
|
|
477
|
+
/>
|
|
478
|
+
</button>
|
|
479
|
+
</div>
|
|
480
|
+
|
|
481
|
+
<Motion
|
|
482
|
+
v-if="opened"
|
|
483
|
+
ref="panelRef"
|
|
484
|
+
:animate="panelAnimation.animate"
|
|
485
|
+
as="div"
|
|
486
|
+
:initial="panelAnimation.initial"
|
|
487
|
+
>
|
|
488
|
+
<div class="border-t base-border px-1.5 pb-1.5 pt-1">
|
|
489
|
+
<div
|
|
490
|
+
v-if="!calendarOpen"
|
|
491
|
+
:id="chipListId"
|
|
492
|
+
ref="chipListRef"
|
|
493
|
+
class="flex flex-wrap gap-1.5 outline-none"
|
|
494
|
+
:class="{
|
|
495
|
+
'text-xs': size === 'sm',
|
|
496
|
+
'text-sm': size === 'base',
|
|
497
|
+
'text-base': size === 'lg',
|
|
498
|
+
}"
|
|
499
|
+
:aria-activedescendant="`${id}-preset-${highlightedIndex}`"
|
|
500
|
+
role="listbox"
|
|
501
|
+
tabindex="0"
|
|
502
|
+
@keydown="onChipListKeyDown"
|
|
503
|
+
>
|
|
504
|
+
<BaseChip
|
|
505
|
+
v-for="(preset, index) in computedPresets"
|
|
506
|
+
:id="`${id}-preset-${index}`"
|
|
507
|
+
:key="preset.id"
|
|
508
|
+
:active="index === highlightedIndex"
|
|
509
|
+
active-highlight="indigo"
|
|
510
|
+
:aria-selected="index === highlightedIndex"
|
|
511
|
+
as="div"
|
|
512
|
+
hoverable
|
|
513
|
+
max-width
|
|
514
|
+
role="option"
|
|
515
|
+
variant="outline"
|
|
516
|
+
@click="onPresetClick(preset)"
|
|
517
|
+
@mousedown="onChipMouseDown"
|
|
518
|
+
@mouseenter="onHighlight(index)"
|
|
519
|
+
>
|
|
520
|
+
{{ preset.label }}<template v-if="preset.sub">
|
|
521
|
+
· {{ preset.sub }}
|
|
522
|
+
</template>
|
|
523
|
+
</BaseChip>
|
|
524
|
+
|
|
525
|
+
<BaseChip
|
|
526
|
+
:id="`${id}-preset-${chooseChipIndex}`"
|
|
527
|
+
:active="highlightedIndex === chooseChipIndex"
|
|
528
|
+
active-highlight="indigo"
|
|
529
|
+
:aria-selected="highlightedIndex === chooseChipIndex"
|
|
530
|
+
as="div"
|
|
531
|
+
hoverable
|
|
532
|
+
max-width
|
|
533
|
+
role="option"
|
|
534
|
+
variant="dashed"
|
|
535
|
+
@click="onOpenCalendar"
|
|
536
|
+
@mousedown="onChipMouseDown"
|
|
537
|
+
@mouseenter="onHighlight(chooseChipIndex)"
|
|
538
|
+
>
|
|
539
|
+
{{ t('choose') }}
|
|
540
|
+
</BaseChip>
|
|
541
|
+
</div>
|
|
542
|
+
|
|
543
|
+
<div
|
|
544
|
+
v-if="calendarOpen"
|
|
545
|
+
class="px-0.5"
|
|
546
|
+
role="grid"
|
|
547
|
+
:aria-label="monthLabel"
|
|
548
|
+
@keydown.escape.prevent="onCloseCalendar"
|
|
549
|
+
>
|
|
550
|
+
<div class="grid grid-cols-7 mb-0.5 gap-px">
|
|
551
|
+
<button
|
|
552
|
+
:aria-label="t('prevMonth')"
|
|
553
|
+
class="min-h-7 min-w-7 inline-flex cursor-pointer items-center justify-center rounded-md base-bg-hover outline-none transition-colors"
|
|
554
|
+
type="button"
|
|
555
|
+
@click="onPreviousMonth"
|
|
556
|
+
>
|
|
557
|
+
<BaseIcon
|
|
558
|
+
color="black"
|
|
559
|
+
:icon="getLayerIcon('chevronLeft')"
|
|
560
|
+
size="xs"
|
|
561
|
+
/>
|
|
562
|
+
</button>
|
|
563
|
+
|
|
564
|
+
<span class="col-span-5 min-h-7 flex items-center justify-center text-xs text-gray-900 font-bold uppercase dark:text-gray-100">
|
|
565
|
+
{{ monthLabel }}
|
|
566
|
+
</span>
|
|
567
|
+
|
|
568
|
+
<button
|
|
569
|
+
:aria-label="t('nextMonth')"
|
|
570
|
+
class="min-h-7 min-w-7 inline-flex cursor-pointer items-center justify-center rounded-md base-bg-hover outline-none transition-colors"
|
|
571
|
+
type="button"
|
|
572
|
+
@click="onNextMonth"
|
|
573
|
+
>
|
|
574
|
+
<BaseIcon
|
|
575
|
+
color="black"
|
|
576
|
+
:icon="getLayerIcon('chevronRight')"
|
|
577
|
+
size="xs"
|
|
578
|
+
/>
|
|
579
|
+
</button>
|
|
580
|
+
</div>
|
|
581
|
+
|
|
582
|
+
<div
|
|
583
|
+
class="grid grid-cols-7 gap-px"
|
|
584
|
+
role="row"
|
|
585
|
+
>
|
|
586
|
+
<div
|
|
587
|
+
v-for="(weekdayLabel, weekdayIndex) in weekdayLabels"
|
|
588
|
+
:key="`weekday-${weekdayIndex}`"
|
|
589
|
+
class="min-h-7 min-w-7 flex items-center justify-center text-xs text-gray-600 font-medium dark:text-gray-400"
|
|
590
|
+
role="columnheader"
|
|
591
|
+
>
|
|
592
|
+
{{ weekdayLabel }}
|
|
593
|
+
</div>
|
|
594
|
+
</div>
|
|
595
|
+
|
|
596
|
+
<div class="grid grid-cols-7 gap-px">
|
|
597
|
+
<button
|
|
598
|
+
v-for="(cell, index) in calendarCells"
|
|
599
|
+
:key="`${cell.date.toISOString()}-${index}`"
|
|
600
|
+
class="relative min-h-7 min-w-7 inline-flex items-center justify-center border border-transparent rounded-md text-xs font-medium outline-none"
|
|
601
|
+
:class="{
|
|
602
|
+
'cursor-not-allowed text-gray-400 opacity-40 dark:text-gray-600': !cell.inMonth || isDayDisabled(cell.date),
|
|
603
|
+
'base-bg-hover': cell.inMonth && !isDayDisabled(cell.date) && !(modelValue && isSameDay(cell.date, modelValue)),
|
|
604
|
+
'text-indigo-600 font-bold dark:text-indigo-400': cell.inMonth && !isDayDisabled(cell.date) && isSameDay(cell.date, today) && !(modelValue && isSameDay(cell.date, modelValue)),
|
|
605
|
+
'text-gray-600 dark:text-gray-400': cell.inMonth && !isDayDisabled(cell.date) && !isSameDay(cell.date, today) && !(modelValue && isSameDay(cell.date, modelValue)),
|
|
606
|
+
'border-indigo-600/35 bg-indigo-600/[0.08] text-gray-900 dark:border-indigo-400/35 dark:bg-indigo-400/[0.08] dark:text-gray-100': modelValue && isSameDay(cell.date, modelValue),
|
|
607
|
+
'ring-2 ring-indigo-600/35 dark:ring-indigo-400/35': index === focusedDayIndex && calendarOpen && !(modelValue && isSameDay(cell.date, modelValue)),
|
|
608
|
+
}"
|
|
609
|
+
:disabled="!cell.inMonth || isDayDisabled(cell.date)"
|
|
610
|
+
role="gridcell"
|
|
611
|
+
:tabindex="index === focusedDayIndex ? 0 : -1"
|
|
612
|
+
@click="onDayClick(cell.date)"
|
|
613
|
+
@keydown="onDayKeyDown($event, cell.date, index)"
|
|
614
|
+
>
|
|
615
|
+
{{ cell.date.getDate() }}
|
|
616
|
+
|
|
617
|
+
<span
|
|
618
|
+
v-if="isSameDay(cell.date, today) && !(modelValue && isSameDay(cell.date, modelValue))"
|
|
619
|
+
class="absolute bottom-0 h-0.5 w-0.5 rounded-full bg-indigo-600 dark:bg-indigo-400"
|
|
620
|
+
/>
|
|
621
|
+
</button>
|
|
622
|
+
</div>
|
|
623
|
+
</div>
|
|
624
|
+
</div>
|
|
625
|
+
</Motion>
|
|
626
|
+
</div>
|
|
627
|
+
|
|
628
|
+
<FieldMessage
|
|
629
|
+
:description="description"
|
|
630
|
+
:disabled="disabled"
|
|
631
|
+
:hide-error="hideError"
|
|
632
|
+
:size="size"
|
|
633
|
+
:validation="validation"
|
|
634
|
+
/>
|
|
635
|
+
</div>
|
|
636
|
+
</template>
|
|
637
|
+
|
|
638
|
+
<i18n lang="json">
|
|
639
|
+
{
|
|
640
|
+
"de": {
|
|
641
|
+
"choose": "Wählen…",
|
|
642
|
+
"clear": "Datum entfernen",
|
|
643
|
+
"nextMonth": "Nächster Monat",
|
|
644
|
+
"nextWeek": "Nächste Woche",
|
|
645
|
+
"placeholder": "Fälligkeit hinzufügen",
|
|
646
|
+
"prevMonth": "Vorheriger Monat",
|
|
647
|
+
"weekend": "Dieses Wochenende"
|
|
648
|
+
},
|
|
649
|
+
"en": {
|
|
650
|
+
"choose": "Choose…",
|
|
651
|
+
"clear": "Remove date",
|
|
652
|
+
"nextMonth": "Next month",
|
|
653
|
+
"nextWeek": "Next week",
|
|
654
|
+
"placeholder": "Add a due date",
|
|
655
|
+
"prevMonth": "Previous month",
|
|
656
|
+
"weekend": "This weekend"
|
|
657
|
+
},
|
|
658
|
+
"es": {
|
|
659
|
+
"choose": "Elegir…",
|
|
660
|
+
"clear": "Quitar fecha",
|
|
661
|
+
"nextMonth": "Mes siguiente",
|
|
662
|
+
"nextWeek": "Próxima semana",
|
|
663
|
+
"placeholder": "Añadir fecha límite",
|
|
664
|
+
"prevMonth": "Mes anterior",
|
|
665
|
+
"weekend": "Este fin de semana"
|
|
666
|
+
},
|
|
667
|
+
"fr": {
|
|
668
|
+
"choose": "Choisir…",
|
|
669
|
+
"clear": "Retirer la date",
|
|
670
|
+
"nextMonth": "Mois suivant",
|
|
671
|
+
"nextWeek": "Semaine prochaine",
|
|
672
|
+
"placeholder": "Ajouter une échéance",
|
|
673
|
+
"prevMonth": "Mois précédent",
|
|
674
|
+
"weekend": "Ce week-end"
|
|
675
|
+
},
|
|
676
|
+
"id": {
|
|
677
|
+
"choose": "Pilih…",
|
|
678
|
+
"clear": "Hapus tanggal",
|
|
679
|
+
"nextMonth": "Bulan berikutnya",
|
|
680
|
+
"nextWeek": "Minggu depan",
|
|
681
|
+
"placeholder": "Tambahkan tenggat",
|
|
682
|
+
"prevMonth": "Bulan sebelumnya",
|
|
683
|
+
"weekend": "Akhir pekan ini"
|
|
684
|
+
},
|
|
685
|
+
"it": {
|
|
686
|
+
"choose": "Scegli…",
|
|
687
|
+
"clear": "Rimuovi data",
|
|
688
|
+
"nextMonth": "Mese successivo",
|
|
689
|
+
"nextWeek": "Prossima settimana",
|
|
690
|
+
"placeholder": "Aggiungi scadenza",
|
|
691
|
+
"prevMonth": "Mese precedente",
|
|
692
|
+
"weekend": "Questo weekend"
|
|
693
|
+
},
|
|
694
|
+
"ja": {
|
|
695
|
+
"choose": "選択…",
|
|
696
|
+
"clear": "日付を削除",
|
|
697
|
+
"nextMonth": "次の月",
|
|
698
|
+
"nextWeek": "来週",
|
|
699
|
+
"placeholder": "期限を追加",
|
|
700
|
+
"prevMonth": "前の月",
|
|
701
|
+
"weekend": "今週末"
|
|
702
|
+
},
|
|
703
|
+
"ko": {
|
|
704
|
+
"choose": "선택…",
|
|
705
|
+
"clear": "날짜 제거",
|
|
706
|
+
"nextMonth": "다음 달",
|
|
707
|
+
"nextWeek": "다음 주",
|
|
708
|
+
"placeholder": "마감일 추가",
|
|
709
|
+
"prevMonth": "이전 달",
|
|
710
|
+
"weekend": "이번 주말"
|
|
711
|
+
},
|
|
712
|
+
"nl": {
|
|
713
|
+
"choose": "Kiezen…",
|
|
714
|
+
"clear": "Datum verwijderen",
|
|
715
|
+
"nextMonth": "Volgende maand",
|
|
716
|
+
"nextWeek": "Volgende week",
|
|
717
|
+
"placeholder": "Deadline toevoegen",
|
|
718
|
+
"prevMonth": "Vorige maand",
|
|
719
|
+
"weekend": "Dit weekend"
|
|
720
|
+
},
|
|
721
|
+
"pl": {
|
|
722
|
+
"choose": "Wybierz…",
|
|
723
|
+
"clear": "Usuń datę",
|
|
724
|
+
"nextMonth": "Następny miesiąc",
|
|
725
|
+
"nextWeek": "Przyszły tydzień",
|
|
726
|
+
"placeholder": "Dodaj termin",
|
|
727
|
+
"prevMonth": "Poprzedni miesiąc",
|
|
728
|
+
"weekend": "Ten weekend"
|
|
729
|
+
},
|
|
730
|
+
"pt": {
|
|
731
|
+
"choose": "Escolher…",
|
|
732
|
+
"clear": "Remover data",
|
|
733
|
+
"nextMonth": "Mês seguinte",
|
|
734
|
+
"nextWeek": "Próxima semana",
|
|
735
|
+
"placeholder": "Adicionar prazo",
|
|
736
|
+
"prevMonth": "Mês anterior",
|
|
737
|
+
"weekend": "Este fim de semana"
|
|
738
|
+
},
|
|
739
|
+
"pt-BR": {
|
|
740
|
+
"choose": "Escolher…",
|
|
741
|
+
"clear": "Remover data",
|
|
742
|
+
"nextMonth": "Próximo mês",
|
|
743
|
+
"nextWeek": "Próxima semana",
|
|
744
|
+
"placeholder": "Adicionar prazo",
|
|
745
|
+
"prevMonth": "Mês anterior",
|
|
746
|
+
"weekend": "Neste fim de semana"
|
|
747
|
+
},
|
|
748
|
+
"vi": {
|
|
749
|
+
"choose": "Chọn…",
|
|
750
|
+
"clear": "Xóa ngày",
|
|
751
|
+
"nextMonth": "Tháng sau",
|
|
752
|
+
"nextWeek": "Tuần tới",
|
|
753
|
+
"placeholder": "Thêm hạn chót",
|
|
754
|
+
"prevMonth": "Tháng trước",
|
|
755
|
+
"weekend": "Cuối tuần này"
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
</i18n>
|