daisy-ui-kit 5.0.0-pre.24 → 5.0.0-pre.26
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/Calendar.vue +149 -63
- package/app/components/CalendarInput.vue +229 -128
- package/app/composables/__tests__/use-calendar.test.ts +239 -0
- package/app/composables/use-calendar.ts +288 -0
- package/nuxt.d.ts +13 -0
- package/nuxt.js +12 -9
- package/package.json +11 -5
- package/app/composables/use-pikaday.ts +0 -35
|
@@ -1,89 +1,175 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import CalendarSkeleton from './CalendarSkeleton.vue'
|
|
2
|
+
import type { CalendarOptions } from '../composables/use-calendar'
|
|
3
|
+
import { computed, watch } from 'vue'
|
|
4
|
+
import { useCalendar } from '../composables/use-calendar'
|
|
6
5
|
|
|
7
6
|
const props = defineProps<{
|
|
8
7
|
/** Bound value: Date object or ISO string or null */
|
|
9
|
-
modelValue
|
|
10
|
-
/**
|
|
11
|
-
options?:
|
|
8
|
+
modelValue?: Date | string | null
|
|
9
|
+
/** Calendar options */
|
|
10
|
+
options?: CalendarOptions
|
|
12
11
|
/** If true, default to today when no value provided */
|
|
13
12
|
autoDefault?: boolean
|
|
14
13
|
}>()
|
|
14
|
+
|
|
15
15
|
const emit = defineEmits<{
|
|
16
16
|
(e: 'update:modelValue', v: Date | null): void
|
|
17
17
|
}>()
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
// Parse initial date from modelValue
|
|
20
|
+
function parseDate(value: Date | string | null | undefined): Date | null {
|
|
21
|
+
if (!value) return null
|
|
22
|
+
if (value instanceof Date) return value
|
|
23
|
+
const d = new Date(value)
|
|
24
|
+
return Number.isNaN(d.getTime()) ? null : d
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
if (props.
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
// Try to parse modelValue if it's a string
|
|
33
|
-
if (typeof props.modelValue === 'string') {
|
|
34
|
-
return new Date(props.modelValue)
|
|
35
|
-
}
|
|
36
|
-
// Use defaultDate from options if provided
|
|
37
|
-
if (props.options?.defaultDate instanceof Date) {
|
|
38
|
-
return props.options.defaultDate
|
|
39
|
-
}
|
|
40
|
-
// Fall back to today
|
|
41
|
-
return new Date()
|
|
27
|
+
const initialDate = computed(() => {
|
|
28
|
+
const parsed = parseDate(props.modelValue)
|
|
29
|
+
if (parsed) return parsed
|
|
30
|
+
if (props.autoDefault) return new Date()
|
|
31
|
+
return null
|
|
42
32
|
})
|
|
43
33
|
|
|
44
|
-
|
|
34
|
+
const {
|
|
35
|
+
selectedDate,
|
|
36
|
+
viewMonth,
|
|
37
|
+
viewYear,
|
|
38
|
+
monthName,
|
|
39
|
+
weekdayHeaders,
|
|
40
|
+
weekdayHeadersFull,
|
|
41
|
+
calendarDays,
|
|
42
|
+
prevMonth,
|
|
43
|
+
nextMonth,
|
|
44
|
+
selectDate,
|
|
45
|
+
goToDate,
|
|
46
|
+
} = useCalendar(initialDate.value, props.options)
|
|
47
|
+
|
|
48
|
+
// Sync selectedDate back to modelValue
|
|
49
|
+
watch(selectedDate, date => {
|
|
45
50
|
emit('update:modelValue', date)
|
|
46
|
-
}
|
|
51
|
+
})
|
|
47
52
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
// Watch for external modelValue changes
|
|
54
|
+
watch(
|
|
55
|
+
() => props.modelValue,
|
|
56
|
+
newValue => {
|
|
57
|
+
const parsed = parseDate(newValue)
|
|
58
|
+
if (parsed && (!selectedDate.value || parsed.getTime() !== selectedDate.value.getTime())) {
|
|
59
|
+
selectDate(parsed)
|
|
60
|
+
goToDate(parsed)
|
|
61
|
+
}
|
|
53
62
|
},
|
|
54
|
-
handleSelect,
|
|
55
63
|
)
|
|
56
64
|
|
|
57
|
-
|
|
58
|
-
if (
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
containerRef.value.appendChild(picker.el)
|
|
62
|
-
}
|
|
63
|
-
loading.value = false
|
|
64
|
-
}
|
|
65
|
-
})
|
|
65
|
+
function handleDayClick(day: (typeof calendarDays.value)[0]) {
|
|
66
|
+
if (day.isDisabled) return
|
|
67
|
+
selectDate(day.date)
|
|
68
|
+
}
|
|
66
69
|
</script>
|
|
67
70
|
|
|
68
71
|
<template>
|
|
69
|
-
<div class="
|
|
70
|
-
<div class="
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
72
|
+
<div class="pika-single">
|
|
73
|
+
<div class="pika-lendar">
|
|
74
|
+
<!-- Header with navigation -->
|
|
75
|
+
<div class="pika-title">
|
|
76
|
+
<div class="pika-label">
|
|
77
|
+
{{ monthName }}
|
|
78
|
+
<select
|
|
79
|
+
class="pika-select pika-select-month"
|
|
80
|
+
:value="viewMonth"
|
|
81
|
+
@change="
|
|
82
|
+
e => {
|
|
83
|
+
const target = e.target as HTMLSelectElement
|
|
84
|
+
const newMonth = parseInt(target.value, 10)
|
|
85
|
+
const d = new Date(viewYear, newMonth, 1)
|
|
86
|
+
goToDate(d)
|
|
87
|
+
}
|
|
88
|
+
"
|
|
89
|
+
>
|
|
90
|
+
<option
|
|
91
|
+
v-for="(m, i) in [
|
|
92
|
+
'January',
|
|
93
|
+
'February',
|
|
94
|
+
'March',
|
|
95
|
+
'April',
|
|
96
|
+
'May',
|
|
97
|
+
'June',
|
|
98
|
+
'July',
|
|
99
|
+
'August',
|
|
100
|
+
'September',
|
|
101
|
+
'October',
|
|
102
|
+
'November',
|
|
103
|
+
'December',
|
|
104
|
+
]"
|
|
105
|
+
:key="i"
|
|
106
|
+
:value="i"
|
|
107
|
+
>
|
|
108
|
+
{{ m }}
|
|
109
|
+
</option>
|
|
110
|
+
</select>
|
|
111
|
+
</div>
|
|
112
|
+
<div class="pika-label">
|
|
113
|
+
{{ viewYear }}
|
|
114
|
+
<select
|
|
115
|
+
class="pika-select pika-select-year"
|
|
116
|
+
:value="viewYear"
|
|
117
|
+
@change="
|
|
118
|
+
e => {
|
|
119
|
+
const target = e.target as HTMLSelectElement
|
|
120
|
+
const newYear = parseInt(target.value, 10)
|
|
121
|
+
const d = new Date(newYear, viewMonth, 1)
|
|
122
|
+
goToDate(d)
|
|
123
|
+
}
|
|
124
|
+
"
|
|
125
|
+
>
|
|
126
|
+
<option v-for="y in Array.from({ length: 21 }, (_, i) => viewYear - 10 + i)" :key="y" :value="y">
|
|
127
|
+
{{ y }}
|
|
128
|
+
</option>
|
|
129
|
+
</select>
|
|
130
|
+
</div>
|
|
131
|
+
<button type="button" class="pika-prev" @click="prevMonth">Previous Month</button>
|
|
132
|
+
<button type="button" class="pika-next" @click="nextMonth">Next Month</button>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<!-- Calendar grid -->
|
|
136
|
+
<table class="pika-table" role="grid">
|
|
137
|
+
<thead>
|
|
138
|
+
<tr>
|
|
139
|
+
<th v-for="(day, i) in weekdayHeaders" :key="i" scope="col">
|
|
140
|
+
<abbr :title="weekdayHeadersFull[i]">{{ day }}</abbr>
|
|
141
|
+
</th>
|
|
142
|
+
</tr>
|
|
143
|
+
</thead>
|
|
144
|
+
<tbody>
|
|
145
|
+
<tr v-for="week in 6" :key="week" class="pika-row">
|
|
146
|
+
<td
|
|
147
|
+
v-for="dayIndex in 7"
|
|
148
|
+
:key="dayIndex"
|
|
149
|
+
:class="{
|
|
150
|
+
'is-today': calendarDays[(week - 1) * 7 + dayIndex - 1]?.isToday,
|
|
151
|
+
'is-selected': calendarDays[(week - 1) * 7 + dayIndex - 1]?.isSelected,
|
|
152
|
+
'is-disabled': calendarDays[(week - 1) * 7 + dayIndex - 1]?.isDisabled,
|
|
153
|
+
'is-outside-current-month': calendarDays[(week - 1) * 7 + dayIndex - 1]?.isOutsideMonth,
|
|
154
|
+
}"
|
|
155
|
+
:aria-selected="calendarDays[(week - 1) * 7 + dayIndex - 1]?.isSelected"
|
|
156
|
+
:data-day="calendarDays[(week - 1) * 7 + dayIndex - 1]?.day"
|
|
157
|
+
>
|
|
158
|
+
<button
|
|
159
|
+
type="button"
|
|
160
|
+
class="pika-button pika-day"
|
|
161
|
+
:disabled="calendarDays[(week - 1) * 7 + dayIndex - 1]?.isDisabled"
|
|
162
|
+
:data-pika-year="calendarDays[(week - 1) * 7 + dayIndex - 1]?.year"
|
|
163
|
+
:data-pika-month="calendarDays[(week - 1) * 7 + dayIndex - 1]?.month"
|
|
164
|
+
:data-pika-day="calendarDays[(week - 1) * 7 + dayIndex - 1]?.day"
|
|
165
|
+
@click="handleDayClick(calendarDays[(week - 1) * 7 + dayIndex - 1]!)"
|
|
166
|
+
>
|
|
167
|
+
{{ calendarDays[(week - 1) * 7 + dayIndex - 1]?.day }}
|
|
168
|
+
</button>
|
|
169
|
+
</td>
|
|
170
|
+
</tr>
|
|
171
|
+
</tbody>
|
|
172
|
+
</table>
|
|
77
173
|
</div>
|
|
78
|
-
<div ref="containerRef" class="absolute inset-0 inline-block w-full h-full" />
|
|
79
|
-
<span class="pika-single hidden" />
|
|
80
174
|
</div>
|
|
81
175
|
</template>
|
|
82
|
-
|
|
83
|
-
<style>
|
|
84
|
-
.has-event {
|
|
85
|
-
.pika-button {
|
|
86
|
-
color: var(--color-primary);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
</style>
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type {
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
import { usePikaday } from '../composables/use-pikaday'
|
|
2
|
+
import type { CalendarOptions } from '../composables/use-calendar'
|
|
3
|
+
import { computed, ref, watch } from 'vue'
|
|
4
|
+
import { useCalendar } from '../composables/use-calendar'
|
|
5
|
+
import { randomString } from '../utils/random-string'
|
|
7
6
|
|
|
8
7
|
const props = defineProps<{
|
|
9
8
|
/** Bound value: Date object or ISO string or null */
|
|
10
|
-
modelValue
|
|
11
|
-
/**
|
|
12
|
-
options?:
|
|
9
|
+
modelValue?: Date | string | number | null
|
|
10
|
+
/** Calendar options */
|
|
11
|
+
options?: CalendarOptions
|
|
13
12
|
/** If true, default to today when no value provided */
|
|
14
13
|
autoDefault?: boolean
|
|
15
14
|
placeholder?: string
|
|
@@ -32,143 +31,245 @@ const props = defineProps<{
|
|
|
32
31
|
sm?: boolean
|
|
33
32
|
xs?: boolean
|
|
34
33
|
}>()
|
|
34
|
+
|
|
35
35
|
const emit = defineEmits<{
|
|
36
36
|
(e: 'update:modelValue', v: Date | null): void
|
|
37
37
|
(e: 'update:inputValue', v: string | null): void
|
|
38
38
|
}>()
|
|
39
|
-
const inputRef = ref<ComponentPublicInstance | null>(null)
|
|
40
|
-
const inputValue = ref<string | null>(null)
|
|
41
|
-
const visible = ref(false)
|
|
42
39
|
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
const popoverId = `calendar-popover-${randomString()}`
|
|
41
|
+
const anchorName = `--calendar-anchor-${randomString()}`
|
|
42
|
+
const inputRef = ref<HTMLInputElement | null>(null)
|
|
43
|
+
const popoverRef = ref<HTMLElement | null>(null)
|
|
46
44
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
},
|
|
54
|
-
handleSelect,
|
|
55
|
-
handleOpen,
|
|
56
|
-
handleClose,
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
function handleSelect(date: Date) {
|
|
60
|
-
emit('update:modelValue', date)
|
|
61
|
-
inputValue.value = picker.value?.toString() ?? ''
|
|
62
|
-
emit('update:inputValue', inputValue.value)
|
|
63
|
-
}
|
|
64
|
-
function handleOpen() {
|
|
65
|
-
visible.value = true
|
|
66
|
-
}
|
|
67
|
-
function handleClose() {
|
|
68
|
-
visible.value = false
|
|
45
|
+
// Parse date from various input types
|
|
46
|
+
function parseDate(value: Date | string | number | null | undefined): Date | null {
|
|
47
|
+
if (!value) return null
|
|
48
|
+
if (value instanceof Date) return value
|
|
49
|
+
const d = new Date(value)
|
|
50
|
+
return Number.isNaN(d.getTime()) ? null : d
|
|
69
51
|
}
|
|
70
52
|
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
if (props.
|
|
75
|
-
|
|
76
|
-
if (props.modelValue instanceof Date) {
|
|
77
|
-
d = props.modelValue
|
|
78
|
-
} else if (typeof props.modelValue === 'string') {
|
|
79
|
-
const tmp = new Date(props.modelValue)
|
|
80
|
-
if (!Number.isNaN(tmp.getTime())) {
|
|
81
|
-
d = tmp
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
if (d) {
|
|
85
|
-
const day = d.getDate()
|
|
86
|
-
const month = d.toLocaleString('en-US', { month: 'short' })
|
|
87
|
-
const year = d.getFullYear()
|
|
88
|
-
inputValue.value = `${day} ${month} ${year}`
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
if (inputEl) {
|
|
92
|
-
picker.value = await createPicker(inputEl)
|
|
93
|
-
// Do not call setDate here
|
|
94
|
-
if (picker.value && (picker.value as any).config?.bound === false) {
|
|
95
|
-
picker.value.hide()
|
|
96
|
-
}
|
|
97
|
-
}
|
|
53
|
+
const initialDate = computed(() => {
|
|
54
|
+
const parsed = parseDate(props.modelValue)
|
|
55
|
+
if (parsed) return parsed
|
|
56
|
+
if (props.autoDefault) return new Date()
|
|
57
|
+
return null
|
|
98
58
|
})
|
|
99
59
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
60
|
+
const {
|
|
61
|
+
selectedDate,
|
|
62
|
+
viewMonth,
|
|
63
|
+
viewYear,
|
|
64
|
+
monthName,
|
|
65
|
+
weekdayHeaders,
|
|
66
|
+
weekdayHeadersFull,
|
|
67
|
+
calendarDays,
|
|
68
|
+
prevMonth,
|
|
69
|
+
nextMonth,
|
|
70
|
+
selectDate,
|
|
71
|
+
goToDate,
|
|
72
|
+
formatDate,
|
|
73
|
+
} = useCalendar(initialDate.value, props.options)
|
|
74
|
+
|
|
75
|
+
// Input display value
|
|
76
|
+
const inputValue = computed(() => formatDate('D MMM YYYY'))
|
|
114
77
|
|
|
78
|
+
// Sync selectedDate back to modelValue
|
|
79
|
+
watch(selectedDate, date => {
|
|
80
|
+
emit('update:modelValue', date)
|
|
81
|
+
emit('update:inputValue', formatDate('D MMM YYYY'))
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
// Watch for external modelValue changes
|
|
115
85
|
watch(
|
|
116
86
|
() => props.modelValue,
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
inputValue.value = picker.value.toString()
|
|
125
|
-
} else if (typeof val === 'string') {
|
|
126
|
-
const d = new Date(val)
|
|
127
|
-
if (!Number.isNaN(d.getTime())) {
|
|
128
|
-
picker.value.setDate(d, true)
|
|
129
|
-
inputValue.value = picker.value.toString()
|
|
130
|
-
}
|
|
131
|
-
} else {
|
|
132
|
-
picker.value.setDate(null, true)
|
|
133
|
-
inputValue.value = ''
|
|
134
|
-
}
|
|
87
|
+
newValue => {
|
|
88
|
+
const parsed = parseDate(newValue)
|
|
89
|
+
if (parsed && (!selectedDate.value || parsed.getTime() !== selectedDate.value.getTime())) {
|
|
90
|
+
selectDate(parsed)
|
|
91
|
+
goToDate(parsed)
|
|
92
|
+
} else if (!newValue && selectedDate.value) {
|
|
93
|
+
selectedDate.value = null
|
|
135
94
|
}
|
|
136
95
|
},
|
|
137
96
|
)
|
|
97
|
+
|
|
98
|
+
function handleDayClick(day: (typeof calendarDays.value)[0]) {
|
|
99
|
+
if (day.isDisabled) return
|
|
100
|
+
selectDate(day.date)
|
|
101
|
+
popoverRef.value?.hidePopover()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function handleClick() {
|
|
105
|
+
// Sync view to selected date when opening
|
|
106
|
+
if (selectedDate.value) {
|
|
107
|
+
goToDate(selectedDate.value)
|
|
108
|
+
}
|
|
109
|
+
popoverRef.value?.togglePopover()
|
|
110
|
+
}
|
|
138
111
|
</script>
|
|
139
112
|
|
|
140
113
|
<template>
|
|
141
|
-
<
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
114
|
+
<div class="relative inline-block">
|
|
115
|
+
<input
|
|
116
|
+
ref="inputRef"
|
|
117
|
+
type="text"
|
|
118
|
+
readonly
|
|
119
|
+
:value="inputValue"
|
|
120
|
+
:placeholder="props.placeholder"
|
|
121
|
+
:disabled="props.disabled"
|
|
122
|
+
class="input cursor-pointer"
|
|
123
|
+
:class="[
|
|
124
|
+
{ validator: props.validator },
|
|
125
|
+
{ 'input-primary': props.primary || props.color === 'primary' },
|
|
126
|
+
{ 'input-secondary': props.secondary || props.color === 'secondary' },
|
|
127
|
+
{ 'input-accent': props.accent || props.color === 'accent' },
|
|
128
|
+
{ 'input-info': props.info || props.color === 'info' },
|
|
129
|
+
{ 'input-success': props.success || props.color === 'success' },
|
|
130
|
+
{ 'input-warning': props.warning || props.color === 'warning' },
|
|
131
|
+
{ 'input-error': props.error || props.color === 'error' },
|
|
132
|
+
{ 'input-ghost': props.ghost },
|
|
133
|
+
{ 'input-xl': props.xl || props.size === 'xl' },
|
|
134
|
+
{ 'input-lg': props.lg || props.size === 'lg' },
|
|
135
|
+
{ 'input-md': props.md || props.size === 'md' },
|
|
136
|
+
{ 'input-sm': props.sm || props.size === 'sm' },
|
|
137
|
+
{ 'input-xs': props.xs || props.size === 'xs' },
|
|
138
|
+
{ 'join-item': props.join },
|
|
139
|
+
]"
|
|
140
|
+
:style="{ 'anchor-name': anchorName } as any"
|
|
141
|
+
v-bind="$attrs"
|
|
142
|
+
@click="handleClick"
|
|
143
|
+
/>
|
|
144
|
+
|
|
145
|
+
<!-- Dropdown calendar using Popover API -->
|
|
146
|
+
<div
|
|
147
|
+
:id="popoverId"
|
|
148
|
+
ref="popoverRef"
|
|
149
|
+
popover="auto"
|
|
150
|
+
class="pika-single calendar-popover"
|
|
151
|
+
:style="{ 'position-anchor': anchorName } as any"
|
|
152
|
+
>
|
|
153
|
+
<div class="pika-lendar">
|
|
154
|
+
<!-- Header with navigation -->
|
|
155
|
+
<div class="pika-title">
|
|
156
|
+
<div class="pika-label">
|
|
157
|
+
{{ monthName }}
|
|
158
|
+
<select
|
|
159
|
+
class="pika-select pika-select-month"
|
|
160
|
+
:value="viewMonth"
|
|
161
|
+
@change="
|
|
162
|
+
e => {
|
|
163
|
+
const target = e.target as HTMLSelectElement
|
|
164
|
+
const newMonth = parseInt(target.value, 10)
|
|
165
|
+
const d = new Date(viewYear, newMonth, 1)
|
|
166
|
+
goToDate(d)
|
|
167
|
+
}
|
|
168
|
+
"
|
|
169
|
+
>
|
|
170
|
+
<option
|
|
171
|
+
v-for="(m, i) in [
|
|
172
|
+
'January',
|
|
173
|
+
'February',
|
|
174
|
+
'March',
|
|
175
|
+
'April',
|
|
176
|
+
'May',
|
|
177
|
+
'June',
|
|
178
|
+
'July',
|
|
179
|
+
'August',
|
|
180
|
+
'September',
|
|
181
|
+
'October',
|
|
182
|
+
'November',
|
|
183
|
+
'December',
|
|
184
|
+
]"
|
|
185
|
+
:key="i"
|
|
186
|
+
:value="i"
|
|
187
|
+
>
|
|
188
|
+
{{ m }}
|
|
189
|
+
</option>
|
|
190
|
+
</select>
|
|
191
|
+
</div>
|
|
192
|
+
<div class="pika-label">
|
|
193
|
+
{{ viewYear }}
|
|
194
|
+
<select
|
|
195
|
+
class="pika-select pika-select-year"
|
|
196
|
+
:value="viewYear"
|
|
197
|
+
@change="
|
|
198
|
+
e => {
|
|
199
|
+
const target = e.target as HTMLSelectElement
|
|
200
|
+
const newYear = parseInt(target.value, 10)
|
|
201
|
+
const d = new Date(newYear, viewMonth, 1)
|
|
202
|
+
goToDate(d)
|
|
203
|
+
}
|
|
204
|
+
"
|
|
205
|
+
>
|
|
206
|
+
<option v-for="y in Array.from({ length: 21 }, (_, i) => viewYear - 10 + i)" :key="y" :value="y">
|
|
207
|
+
{{ y }}
|
|
208
|
+
</option>
|
|
209
|
+
</select>
|
|
210
|
+
</div>
|
|
211
|
+
<button type="button" class="pika-prev" @click="prevMonth">Previous Month</button>
|
|
212
|
+
<button type="button" class="pika-next" @click="nextMonth">Next Month</button>
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
<!-- Calendar grid -->
|
|
216
|
+
<table class="pika-table" role="grid">
|
|
217
|
+
<thead>
|
|
218
|
+
<tr>
|
|
219
|
+
<th v-for="(day, i) in weekdayHeaders" :key="i" scope="col">
|
|
220
|
+
<abbr :title="weekdayHeadersFull[i]">{{ day }}</abbr>
|
|
221
|
+
</th>
|
|
222
|
+
</tr>
|
|
223
|
+
</thead>
|
|
224
|
+
<tbody>
|
|
225
|
+
<tr v-for="week in 6" :key="week" class="pika-row">
|
|
226
|
+
<td
|
|
227
|
+
v-for="dayIndex in 7"
|
|
228
|
+
:key="dayIndex"
|
|
229
|
+
:class="{
|
|
230
|
+
'is-today': calendarDays[(week - 1) * 7 + dayIndex - 1]?.isToday,
|
|
231
|
+
'is-selected': calendarDays[(week - 1) * 7 + dayIndex - 1]?.isSelected,
|
|
232
|
+
'is-disabled': calendarDays[(week - 1) * 7 + dayIndex - 1]?.isDisabled,
|
|
233
|
+
'is-outside-current-month': calendarDays[(week - 1) * 7 + dayIndex - 1]?.isOutsideMonth,
|
|
234
|
+
}"
|
|
235
|
+
:aria-selected="calendarDays[(week - 1) * 7 + dayIndex - 1]?.isSelected"
|
|
236
|
+
>
|
|
237
|
+
<button
|
|
238
|
+
type="button"
|
|
239
|
+
class="pika-button pika-day"
|
|
240
|
+
:disabled="calendarDays[(week - 1) * 7 + dayIndex - 1]?.isDisabled"
|
|
241
|
+
@click="handleDayClick(calendarDays[(week - 1) * 7 + dayIndex - 1]!)"
|
|
242
|
+
>
|
|
243
|
+
{{ calendarDays[(week - 1) * 7 + dayIndex - 1]?.day }}
|
|
244
|
+
</button>
|
|
245
|
+
</td>
|
|
246
|
+
</tr>
|
|
247
|
+
</tbody>
|
|
248
|
+
</table>
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
174
252
|
</template>
|
|
253
|
+
|
|
254
|
+
<style>
|
|
255
|
+
.calendar-popover[popover] {
|
|
256
|
+
position-area: block-end span-inline-end;
|
|
257
|
+
position-try-fallbacks:
|
|
258
|
+
flip-block,
|
|
259
|
+
flip-inline,
|
|
260
|
+
flip-block flip-inline;
|
|
261
|
+
margin: 0;
|
|
262
|
+
margin-top: 0.25rem;
|
|
263
|
+
/* Reset default popover styles */
|
|
264
|
+
border: none;
|
|
265
|
+
inset: auto;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.calendar-popover[popover]:popover-open {
|
|
269
|
+
position: fixed;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.calendar-popover[popover]:not(:popover-open) {
|
|
273
|
+
display: none;
|
|
274
|
+
}
|
|
275
|
+
</style>
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { useCalendar } from '../use-calendar'
|
|
3
|
+
|
|
4
|
+
describe('useCalendar', () => {
|
|
5
|
+
describe('initialization', () => {
|
|
6
|
+
it('initializes with null date by default', () => {
|
|
7
|
+
const { selectedDate } = useCalendar()
|
|
8
|
+
expect(selectedDate.value).toBeNull()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('initializes with provided date', () => {
|
|
12
|
+
const date = new Date(2025, 5, 15) // June 15, 2025
|
|
13
|
+
const { selectedDate } = useCalendar(date)
|
|
14
|
+
expect(selectedDate.value?.getFullYear()).toBe(2025)
|
|
15
|
+
expect(selectedDate.value?.getMonth()).toBe(5)
|
|
16
|
+
expect(selectedDate.value?.getDate()).toBe(15)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('sets viewDate to today when no initial date provided', () => {
|
|
20
|
+
const { viewDate } = useCalendar()
|
|
21
|
+
const today = new Date()
|
|
22
|
+
expect(viewDate.value.getFullYear()).toBe(today.getFullYear())
|
|
23
|
+
expect(viewDate.value.getMonth()).toBe(today.getMonth())
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('sets viewDate to initial date when provided', () => {
|
|
27
|
+
const date = new Date(2025, 5, 15)
|
|
28
|
+
const { viewDate } = useCalendar(date)
|
|
29
|
+
expect(viewDate.value.getFullYear()).toBe(2025)
|
|
30
|
+
expect(viewDate.value.getMonth()).toBe(5)
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
describe('weekday headers', () => {
|
|
35
|
+
it('returns weekday headers starting with Sunday by default', () => {
|
|
36
|
+
const { weekdayHeaders } = useCalendar()
|
|
37
|
+
expect(weekdayHeaders.value).toEqual(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'])
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('returns weekday headers starting with Monday when firstDay is 1', () => {
|
|
41
|
+
const { weekdayHeaders } = useCalendar(null, { firstDay: 1 })
|
|
42
|
+
expect(weekdayHeaders.value).toEqual(['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'])
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('returns full weekday names for accessibility', () => {
|
|
46
|
+
const { weekdayHeadersFull } = useCalendar()
|
|
47
|
+
expect(weekdayHeadersFull.value).toEqual([
|
|
48
|
+
'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday',
|
|
49
|
+
])
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe('month display', () => {
|
|
54
|
+
it('returns correct month name', () => {
|
|
55
|
+
const date = new Date(2025, 5, 15) // June
|
|
56
|
+
const { monthName } = useCalendar(date)
|
|
57
|
+
expect(monthName.value).toBe('June')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('returns correct short month name', () => {
|
|
61
|
+
const date = new Date(2025, 11, 25) // December
|
|
62
|
+
const { monthNameShort } = useCalendar(date)
|
|
63
|
+
expect(monthNameShort.value).toBe('Dec')
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
describe('calendar days', () => {
|
|
68
|
+
it('generates 42 days (6 weeks)', () => {
|
|
69
|
+
const { calendarDays } = useCalendar(new Date(2025, 5, 15))
|
|
70
|
+
expect(calendarDays.value).toHaveLength(42)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('marks days outside current month', () => {
|
|
74
|
+
const { calendarDays } = useCalendar(new Date(2025, 5, 15)) // June 2025
|
|
75
|
+
const outsideDays = calendarDays.value.filter(d => d.isOutsideMonth)
|
|
76
|
+
expect(outsideDays.length).toBeGreaterThan(0)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('marks the selected date', () => {
|
|
80
|
+
const date = new Date(2025, 5, 15)
|
|
81
|
+
const { calendarDays } = useCalendar(date)
|
|
82
|
+
const selectedDays = calendarDays.value.filter(d => d.isSelected)
|
|
83
|
+
expect(selectedDays).toHaveLength(1)
|
|
84
|
+
expect(selectedDays[0]?.day).toBe(15)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('marks today correctly', () => {
|
|
88
|
+
const today = new Date()
|
|
89
|
+
const { calendarDays, goToDate } = useCalendar()
|
|
90
|
+
goToDate(today)
|
|
91
|
+
const todayDays = calendarDays.value.filter(d => d.isToday && !d.isOutsideMonth)
|
|
92
|
+
expect(todayDays).toHaveLength(1)
|
|
93
|
+
expect(todayDays[0]?.day).toBe(today.getDate())
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
describe('navigation', () => {
|
|
98
|
+
it('navigates to next month', () => {
|
|
99
|
+
const { viewMonth, viewYear, nextMonth } = useCalendar(new Date(2025, 5, 15))
|
|
100
|
+
expect(viewMonth.value).toBe(5)
|
|
101
|
+
nextMonth()
|
|
102
|
+
expect(viewMonth.value).toBe(6)
|
|
103
|
+
expect(viewYear.value).toBe(2025)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('navigates to previous month', () => {
|
|
107
|
+
const { viewMonth, viewYear, prevMonth } = useCalendar(new Date(2025, 5, 15))
|
|
108
|
+
expect(viewMonth.value).toBe(5)
|
|
109
|
+
prevMonth()
|
|
110
|
+
expect(viewMonth.value).toBe(4)
|
|
111
|
+
expect(viewYear.value).toBe(2025)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('handles year rollover when navigating forward', () => {
|
|
115
|
+
const { viewMonth, viewYear, nextMonth } = useCalendar(new Date(2025, 11, 15))
|
|
116
|
+
expect(viewMonth.value).toBe(11)
|
|
117
|
+
nextMonth()
|
|
118
|
+
expect(viewMonth.value).toBe(0)
|
|
119
|
+
expect(viewYear.value).toBe(2026)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('handles year rollover when navigating backward', () => {
|
|
123
|
+
const { viewMonth, viewYear, prevMonth } = useCalendar(new Date(2025, 0, 15))
|
|
124
|
+
expect(viewMonth.value).toBe(0)
|
|
125
|
+
prevMonth()
|
|
126
|
+
expect(viewMonth.value).toBe(11)
|
|
127
|
+
expect(viewYear.value).toBe(2024)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('goes to specific month', () => {
|
|
131
|
+
const { viewMonth, goToMonth } = useCalendar(new Date(2025, 5, 15))
|
|
132
|
+
goToMonth(10)
|
|
133
|
+
expect(viewMonth.value).toBe(10)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('goes to specific year', () => {
|
|
137
|
+
const { viewYear, goToYear } = useCalendar(new Date(2025, 5, 15))
|
|
138
|
+
goToYear(2030)
|
|
139
|
+
expect(viewYear.value).toBe(2030)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('goes to today', () => {
|
|
143
|
+
const { viewMonth, viewYear, goToToday } = useCalendar(new Date(2020, 0, 1))
|
|
144
|
+
const today = new Date()
|
|
145
|
+
goToToday()
|
|
146
|
+
expect(viewMonth.value).toBe(today.getMonth())
|
|
147
|
+
expect(viewYear.value).toBe(today.getFullYear())
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
describe('selection', () => {
|
|
152
|
+
it('selects a date', () => {
|
|
153
|
+
const { selectedDate, selectDate } = useCalendar()
|
|
154
|
+
const date = new Date(2025, 5, 20)
|
|
155
|
+
selectDate(date)
|
|
156
|
+
expect(selectedDate.value?.getDate()).toBe(20)
|
|
157
|
+
expect(selectedDate.value?.getMonth()).toBe(5)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('clears selection', () => {
|
|
161
|
+
const { selectedDate, clearSelection } = useCalendar(new Date(2025, 5, 15))
|
|
162
|
+
expect(selectedDate.value).not.toBeNull()
|
|
163
|
+
clearSelection()
|
|
164
|
+
expect(selectedDate.value).toBeNull()
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('does not select disabled dates', () => {
|
|
168
|
+
const minDate = new Date(2025, 5, 10)
|
|
169
|
+
const { selectedDate, selectDate } = useCalendar(null, { minDate })
|
|
170
|
+
selectDate(new Date(2025, 5, 5)) // Before minDate
|
|
171
|
+
expect(selectedDate.value).toBeNull()
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
describe('date constraints', () => {
|
|
176
|
+
it('marks dates before minDate as disabled', () => {
|
|
177
|
+
const minDate = new Date(2025, 5, 15)
|
|
178
|
+
const { calendarDays } = useCalendar(new Date(2025, 5, 20), { minDate })
|
|
179
|
+
const june14 = calendarDays.value.find(d => d.day === 14 && d.month === 5)
|
|
180
|
+
const june15 = calendarDays.value.find(d => d.day === 15 && d.month === 5)
|
|
181
|
+
expect(june14?.isDisabled).toBe(true)
|
|
182
|
+
expect(june15?.isDisabled).toBe(false)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('marks dates after maxDate as disabled', () => {
|
|
186
|
+
const maxDate = new Date(2025, 5, 15)
|
|
187
|
+
const { calendarDays } = useCalendar(new Date(2025, 5, 10), { maxDate })
|
|
188
|
+
const june15 = calendarDays.value.find(d => d.day === 15 && d.month === 5)
|
|
189
|
+
const june16 = calendarDays.value.find(d => d.day === 16 && d.month === 5)
|
|
190
|
+
expect(june15?.isDisabled).toBe(false)
|
|
191
|
+
expect(june16?.isDisabled).toBe(true)
|
|
192
|
+
})
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
describe('formatting', () => {
|
|
196
|
+
it('formats date with default format', () => {
|
|
197
|
+
const { formatDate } = useCalendar(new Date(2025, 5, 15))
|
|
198
|
+
expect(formatDate()).toBe('15 Jun 2025')
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('formats date with custom format', () => {
|
|
202
|
+
const { formatDate } = useCalendar(new Date(2025, 5, 15))
|
|
203
|
+
expect(formatDate('YYYY-MM-DD')).toBe('2025-06-15')
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('formats date with full month name', () => {
|
|
207
|
+
const { formatDate } = useCalendar(new Date(2025, 5, 15))
|
|
208
|
+
expect(formatDate('MMMM D, YYYY')).toBe('June 15, 2025')
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('returns empty string when no date selected', () => {
|
|
212
|
+
const { formatDate } = useCalendar()
|
|
213
|
+
expect(formatDate()).toBe('')
|
|
214
|
+
})
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
describe('utility functions', () => {
|
|
218
|
+
it('isSameDay returns true for same day', () => {
|
|
219
|
+
const { isSameDay } = useCalendar()
|
|
220
|
+
const a = new Date(2025, 5, 15, 10, 30)
|
|
221
|
+
const b = new Date(2025, 5, 15, 20, 45)
|
|
222
|
+
expect(isSameDay(a, b)).toBe(true)
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('isSameDay returns false for different days', () => {
|
|
226
|
+
const { isSameDay } = useCalendar()
|
|
227
|
+
const a = new Date(2025, 5, 15)
|
|
228
|
+
const b = new Date(2025, 5, 16)
|
|
229
|
+
expect(isSameDay(a, b)).toBe(false)
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('isSameDay handles null values', () => {
|
|
233
|
+
const { isSameDay } = useCalendar()
|
|
234
|
+
expect(isSameDay(null, new Date())).toBe(false)
|
|
235
|
+
expect(isSameDay(new Date(), null)).toBe(false)
|
|
236
|
+
expect(isSameDay(null, null)).toBe(false)
|
|
237
|
+
})
|
|
238
|
+
})
|
|
239
|
+
})
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { computed, ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
export interface CalendarOptions {
|
|
4
|
+
/** First day of week: 0 = Sunday, 1 = Monday, etc. */
|
|
5
|
+
firstDay?: number
|
|
6
|
+
/** Minimum selectable date */
|
|
7
|
+
minDate?: Date | null
|
|
8
|
+
/** Maximum selectable date */
|
|
9
|
+
maxDate?: Date | null
|
|
10
|
+
/** Locale for formatting (default: 'en-US') */
|
|
11
|
+
locale?: string
|
|
12
|
+
/** Short month names */
|
|
13
|
+
monthsShort?: string[]
|
|
14
|
+
/** Full month names */
|
|
15
|
+
months?: string[]
|
|
16
|
+
/** Short weekday names */
|
|
17
|
+
weekdaysShort?: string[]
|
|
18
|
+
/** Full weekday names */
|
|
19
|
+
weekdays?: string[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CalendarDay {
|
|
23
|
+
date: Date
|
|
24
|
+
day: number
|
|
25
|
+
month: number
|
|
26
|
+
year: number
|
|
27
|
+
isToday: boolean
|
|
28
|
+
isSelected: boolean
|
|
29
|
+
isDisabled: boolean
|
|
30
|
+
isOutsideMonth: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const DEFAULT_MONTHS = [
|
|
34
|
+
'January',
|
|
35
|
+
'February',
|
|
36
|
+
'March',
|
|
37
|
+
'April',
|
|
38
|
+
'May',
|
|
39
|
+
'June',
|
|
40
|
+
'July',
|
|
41
|
+
'August',
|
|
42
|
+
'September',
|
|
43
|
+
'October',
|
|
44
|
+
'November',
|
|
45
|
+
'December',
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
const DEFAULT_MONTHS_SHORT = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
|
|
49
|
+
|
|
50
|
+
const DEFAULT_WEEKDAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
|
|
51
|
+
|
|
52
|
+
const DEFAULT_WEEKDAYS_SHORT = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
|
53
|
+
|
|
54
|
+
export function useCalendar(initialDate: Date | null = null, options: CalendarOptions = {}) {
|
|
55
|
+
const {
|
|
56
|
+
firstDay = 0,
|
|
57
|
+
minDate = null,
|
|
58
|
+
maxDate = null,
|
|
59
|
+
months = DEFAULT_MONTHS,
|
|
60
|
+
monthsShort = DEFAULT_MONTHS_SHORT,
|
|
61
|
+
weekdays = DEFAULT_WEEKDAYS,
|
|
62
|
+
weekdaysShort = DEFAULT_WEEKDAYS_SHORT,
|
|
63
|
+
} = options
|
|
64
|
+
|
|
65
|
+
// The currently selected date
|
|
66
|
+
const selectedDate = ref<Date | null>(initialDate)
|
|
67
|
+
|
|
68
|
+
// The month/year currently being viewed
|
|
69
|
+
const viewDate = ref(initialDate ? new Date(initialDate) : new Date())
|
|
70
|
+
|
|
71
|
+
// Current view month/year
|
|
72
|
+
const viewMonth = computed(() => viewDate.value.getMonth())
|
|
73
|
+
const viewYear = computed(() => viewDate.value.getFullYear())
|
|
74
|
+
|
|
75
|
+
// Month/year display strings
|
|
76
|
+
const monthName = computed(() => months[viewMonth.value])
|
|
77
|
+
const monthNameShort = computed(() => monthsShort[viewMonth.value])
|
|
78
|
+
|
|
79
|
+
// Weekday headers adjusted for firstDay (short names for display)
|
|
80
|
+
const weekdayHeaders = computed(() => {
|
|
81
|
+
const headers: string[] = []
|
|
82
|
+
for (let i = 0; i < 7; i++) {
|
|
83
|
+
headers.push(weekdaysShort[(i + firstDay) % 7] ?? '')
|
|
84
|
+
}
|
|
85
|
+
return headers
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
// Full weekday names for accessibility (title attributes, screen readers)
|
|
89
|
+
const weekdayHeadersFull = computed(() => {
|
|
90
|
+
const headers: string[] = []
|
|
91
|
+
for (let i = 0; i < 7; i++) {
|
|
92
|
+
headers.push(weekdays[(i + firstDay) % 7] ?? '')
|
|
93
|
+
}
|
|
94
|
+
return headers
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
// Get days in a month
|
|
98
|
+
function getDaysInMonth(year: number, month: number): number {
|
|
99
|
+
return new Date(year, month + 1, 0).getDate()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Check if two dates are the same day
|
|
103
|
+
function isSameDay(a: Date | null, b: Date | null): boolean {
|
|
104
|
+
if (!a || !b) return false
|
|
105
|
+
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Check if a date is today
|
|
109
|
+
function isToday(date: Date): boolean {
|
|
110
|
+
return isSameDay(date, new Date())
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Check if a date is disabled
|
|
114
|
+
function isDisabled(date: Date): boolean {
|
|
115
|
+
if (minDate && date < minDate) return true
|
|
116
|
+
if (maxDate && date > maxDate) return true
|
|
117
|
+
return false
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Generate calendar days for the current view
|
|
121
|
+
const calendarDays = computed<CalendarDay[]>(() => {
|
|
122
|
+
const year = viewYear.value
|
|
123
|
+
const month = viewMonth.value
|
|
124
|
+
const days: CalendarDay[] = []
|
|
125
|
+
|
|
126
|
+
// First day of the month
|
|
127
|
+
const firstOfMonth = new Date(year, month, 1)
|
|
128
|
+
const startDayOfWeek = firstOfMonth.getDay()
|
|
129
|
+
|
|
130
|
+
// Calculate offset based on firstDay option
|
|
131
|
+
const offset = (startDayOfWeek - firstDay + 7) % 7
|
|
132
|
+
|
|
133
|
+
// Days from previous month
|
|
134
|
+
const prevMonth = month === 0 ? 11 : month - 1
|
|
135
|
+
const prevYear = month === 0 ? year - 1 : year
|
|
136
|
+
const daysInPrevMonth = getDaysInMonth(prevYear, prevMonth)
|
|
137
|
+
|
|
138
|
+
for (let i = offset - 1; i >= 0; i--) {
|
|
139
|
+
const day = daysInPrevMonth - i
|
|
140
|
+
const date = new Date(prevYear, prevMonth, day)
|
|
141
|
+
days.push({
|
|
142
|
+
date,
|
|
143
|
+
day,
|
|
144
|
+
month: prevMonth,
|
|
145
|
+
year: prevYear,
|
|
146
|
+
isToday: isToday(date),
|
|
147
|
+
isSelected: isSameDay(date, selectedDate.value),
|
|
148
|
+
isDisabled: isDisabled(date),
|
|
149
|
+
isOutsideMonth: true,
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Days in current month
|
|
154
|
+
const daysInMonth = getDaysInMonth(year, month)
|
|
155
|
+
for (let day = 1; day <= daysInMonth; day++) {
|
|
156
|
+
const date = new Date(year, month, day)
|
|
157
|
+
days.push({
|
|
158
|
+
date,
|
|
159
|
+
day,
|
|
160
|
+
month,
|
|
161
|
+
year,
|
|
162
|
+
isToday: isToday(date),
|
|
163
|
+
isSelected: isSameDay(date, selectedDate.value),
|
|
164
|
+
isDisabled: isDisabled(date),
|
|
165
|
+
isOutsideMonth: false,
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Days from next month to fill the grid (always 6 rows = 42 cells)
|
|
170
|
+
const nextMonth = month === 11 ? 0 : month + 1
|
|
171
|
+
const nextYear = month === 11 ? year + 1 : year
|
|
172
|
+
const remainingDays = 42 - days.length
|
|
173
|
+
|
|
174
|
+
for (let day = 1; day <= remainingDays; day++) {
|
|
175
|
+
const date = new Date(nextYear, nextMonth, day)
|
|
176
|
+
days.push({
|
|
177
|
+
date,
|
|
178
|
+
day,
|
|
179
|
+
month: nextMonth,
|
|
180
|
+
year: nextYear,
|
|
181
|
+
isToday: isToday(date),
|
|
182
|
+
isSelected: isSameDay(date, selectedDate.value),
|
|
183
|
+
isDisabled: isDisabled(date),
|
|
184
|
+
isOutsideMonth: true,
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return days
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
// Navigation
|
|
192
|
+
function prevMonth() {
|
|
193
|
+
const d = new Date(viewDate.value)
|
|
194
|
+
d.setMonth(d.getMonth() - 1)
|
|
195
|
+
viewDate.value = d
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function nextMonth() {
|
|
199
|
+
const d = new Date(viewDate.value)
|
|
200
|
+
d.setMonth(d.getMonth() + 1)
|
|
201
|
+
viewDate.value = d
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function goToMonth(month: number) {
|
|
205
|
+
const d = new Date(viewDate.value)
|
|
206
|
+
d.setMonth(month)
|
|
207
|
+
viewDate.value = d
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function goToYear(year: number) {
|
|
211
|
+
const d = new Date(viewDate.value)
|
|
212
|
+
d.setFullYear(year)
|
|
213
|
+
viewDate.value = d
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function goToDate(date: Date) {
|
|
217
|
+
viewDate.value = new Date(date)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function goToToday() {
|
|
221
|
+
viewDate.value = new Date()
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Selection
|
|
225
|
+
function selectDate(date: Date) {
|
|
226
|
+
if (isDisabled(date)) return
|
|
227
|
+
selectedDate.value = new Date(date)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function clearSelection() {
|
|
231
|
+
selectedDate.value = null
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Format selected date
|
|
235
|
+
function formatDate(format: string = 'D MMM YYYY'): string {
|
|
236
|
+
if (!selectedDate.value) return ''
|
|
237
|
+
|
|
238
|
+
const d = selectedDate.value
|
|
239
|
+
const day = d.getDate()
|
|
240
|
+
const month = d.getMonth()
|
|
241
|
+
const year = d.getFullYear()
|
|
242
|
+
|
|
243
|
+
return format
|
|
244
|
+
.replace('YYYY', String(year))
|
|
245
|
+
.replace('YY', String(year).slice(-2))
|
|
246
|
+
.replace('MMMM', months[month] ?? '')
|
|
247
|
+
.replace('MMM', monthsShort[month] ?? '')
|
|
248
|
+
.replace('MM', String(month + 1).padStart(2, '0'))
|
|
249
|
+
.replace('M', String(month + 1))
|
|
250
|
+
.replace('DD', String(day).padStart(2, '0'))
|
|
251
|
+
.replace('D', String(day))
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
// State
|
|
256
|
+
selectedDate,
|
|
257
|
+
viewDate,
|
|
258
|
+
viewMonth,
|
|
259
|
+
viewYear,
|
|
260
|
+
|
|
261
|
+
// Display
|
|
262
|
+
monthName,
|
|
263
|
+
monthNameShort,
|
|
264
|
+
weekdayHeaders,
|
|
265
|
+
weekdayHeadersFull,
|
|
266
|
+
calendarDays,
|
|
267
|
+
|
|
268
|
+
// Navigation
|
|
269
|
+
prevMonth,
|
|
270
|
+
nextMonth,
|
|
271
|
+
goToMonth,
|
|
272
|
+
goToYear,
|
|
273
|
+
goToDate,
|
|
274
|
+
goToToday,
|
|
275
|
+
|
|
276
|
+
// Selection
|
|
277
|
+
selectDate,
|
|
278
|
+
clearSelection,
|
|
279
|
+
|
|
280
|
+
// Formatting
|
|
281
|
+
formatDate,
|
|
282
|
+
|
|
283
|
+
// Utilities
|
|
284
|
+
isSameDay,
|
|
285
|
+
isToday,
|
|
286
|
+
isDisabled,
|
|
287
|
+
}
|
|
288
|
+
}
|
package/nuxt.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { NuxtModule } from '@nuxt/schema'
|
|
2
|
+
|
|
3
|
+
export interface ModuleOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Prefix for all DaisyUI Kit components
|
|
6
|
+
* @default ''
|
|
7
|
+
*/
|
|
8
|
+
prefix?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
declare const module: NuxtModule<ModuleOptions>
|
|
12
|
+
|
|
13
|
+
export default module
|
package/nuxt.js
CHANGED
|
@@ -1,28 +1,31 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { defineNuxtModule } from '@nuxt/kit'
|
|
1
|
+
import { addImports, createResolver, defineNuxtModule } from '@nuxt/kit'
|
|
3
2
|
|
|
4
3
|
export default defineNuxtModule({
|
|
5
4
|
meta: {
|
|
6
5
|
name: 'daisy-ui-kit',
|
|
7
6
|
configKey: 'daisy',
|
|
7
|
+
compatibility: {
|
|
8
|
+
nuxt: '>=3.0.0',
|
|
9
|
+
},
|
|
8
10
|
},
|
|
9
11
|
defaults: {
|
|
10
12
|
prefix: '',
|
|
11
13
|
},
|
|
12
14
|
setup(moduleOptions, nuxt) {
|
|
13
15
|
const { prefix } = moduleOptions
|
|
16
|
+
const { resolve } = createResolver(import.meta.url)
|
|
14
17
|
|
|
15
|
-
nuxt.hook('components:dirs',
|
|
18
|
+
nuxt.hook('components:dirs', dirs => {
|
|
16
19
|
dirs.push({
|
|
17
|
-
path:
|
|
20
|
+
path: resolve('./app/components'),
|
|
18
21
|
prefix,
|
|
19
22
|
})
|
|
20
23
|
})
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
25
|
+
addImports([
|
|
26
|
+
{ name: 'useCalendar', from: resolve('./app/composables/use-calendar') },
|
|
27
|
+
{ name: 'useDaisyTheme', from: resolve('./app/composables/use-daisy-theme') },
|
|
28
|
+
{ name: 'useToast', from: resolve('./app/composables/use-toast') },
|
|
29
|
+
])
|
|
27
30
|
},
|
|
28
31
|
})
|
package/package.json
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "daisy-ui-kit",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "5.0.0-pre.
|
|
4
|
+
"version": "5.0.0-pre.26",
|
|
5
5
|
"packageManager": "pnpm@10.10.0",
|
|
6
6
|
"author": "feathers.dev",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": {
|
|
9
|
+
"types": "./nuxt.d.ts",
|
|
9
10
|
"import": "./nuxt.js",
|
|
10
11
|
"require": "./nuxt.js"
|
|
11
12
|
},
|
|
12
13
|
"./nuxt": {
|
|
14
|
+
"types": "./nuxt.d.ts",
|
|
13
15
|
"import": "./nuxt.js",
|
|
14
16
|
"require": "./nuxt.js"
|
|
15
17
|
},
|
|
@@ -19,11 +21,13 @@
|
|
|
19
21
|
},
|
|
20
22
|
"main": "./nuxt.js",
|
|
21
23
|
"module": "./nuxt.js",
|
|
24
|
+
"types": "./nuxt.d.ts",
|
|
22
25
|
"files": [
|
|
23
26
|
"app/components/*.vue",
|
|
24
27
|
"app/composables/*",
|
|
25
28
|
"app/utils/*",
|
|
26
|
-
"nuxt.js"
|
|
29
|
+
"nuxt.js",
|
|
30
|
+
"nuxt.d.ts"
|
|
27
31
|
],
|
|
28
32
|
"scripts": {
|
|
29
33
|
"import-d1-dumps": "./import-d1-dumps.sh",
|
|
@@ -60,7 +64,6 @@
|
|
|
60
64
|
"@vueuse/integrations": "^13.1.0",
|
|
61
65
|
"focus-trap": "^7.6.4",
|
|
62
66
|
"nuxt": "^4.2.1",
|
|
63
|
-
"pikaday": "^1.8.2",
|
|
64
67
|
"shiki": "^3.3.0",
|
|
65
68
|
"typescript": "^5.8.3",
|
|
66
69
|
"vue": "^3.5.13",
|
|
@@ -72,11 +75,14 @@
|
|
|
72
75
|
"@stylistic/eslint-plugin": "^4.2.0",
|
|
73
76
|
"@tailwindcss/typography": "^0.5.16",
|
|
74
77
|
"@tailwindcss/vite": "^4.1.5",
|
|
75
|
-
"@
|
|
78
|
+
"@vitejs/plugin-vue": "^6.0.3",
|
|
79
|
+
"@vue/test-utils": "^2.4.6",
|
|
76
80
|
"daisyui": "^5.5.5",
|
|
77
81
|
"eslint": "^9.26.0",
|
|
78
82
|
"eslint-config-prettier": "^10.1.8",
|
|
79
83
|
"eslint-plugin-vue": "^10.5.1",
|
|
80
|
-
"
|
|
84
|
+
"happy-dom": "^20.0.11",
|
|
85
|
+
"tailwindcss": "^4.1.5",
|
|
86
|
+
"vitest": "^4.0.16"
|
|
81
87
|
}
|
|
82
88
|
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import type Pikaday from 'pikaday'
|
|
2
|
-
import type { PikadayOptions } from 'pikaday'
|
|
3
|
-
import { onBeforeUnmount, ref } from 'vue'
|
|
4
|
-
|
|
5
|
-
export function usePikaday(
|
|
6
|
-
options: PikadayOptions,
|
|
7
|
-
onSelect: (date: Date) => void,
|
|
8
|
-
onOpen?: () => void,
|
|
9
|
-
onClose?: () => void,
|
|
10
|
-
) {
|
|
11
|
-
const picker = ref<Pikaday | null>(null)
|
|
12
|
-
|
|
13
|
-
async function createPicker(fieldOrContainer: HTMLElement) {
|
|
14
|
-
const { default: PikadayLib } = await import('pikaday')
|
|
15
|
-
picker.value = new PikadayLib({
|
|
16
|
-
...options,
|
|
17
|
-
field: options.bound !== false ? fieldOrContainer : undefined,
|
|
18
|
-
container: options.bound === false ? fieldOrContainer : undefined,
|
|
19
|
-
onSelect,
|
|
20
|
-
onOpen,
|
|
21
|
-
onClose,
|
|
22
|
-
})
|
|
23
|
-
return picker.value
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
onBeforeUnmount(() => {
|
|
27
|
-
picker.value?.destroy()
|
|
28
|
-
picker.value = null
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
return {
|
|
32
|
-
picker,
|
|
33
|
-
createPicker,
|
|
34
|
-
}
|
|
35
|
-
}
|