daisy-ui-kit 5.0.0-pre.21 → 5.0.0-pre.25

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.
@@ -1,89 +1,175 @@
1
1
  <script setup lang="ts">
2
- import type { PikadayOptions } from 'pikaday'
3
- import { onMounted, ref } from 'vue'
4
- import { usePikaday } from '~/composables/use-pikaday'
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: Date | string | null
10
- /** All Pikaday options (except `container` and `bound`) */
11
- options?: Omit<PikadayOptions, 'container' | 'bound'>
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
- const containerRef = ref<HTMLElement | null>(null)
20
- const loading = ref(true)
21
-
22
- const defaultOptions: Partial<PikadayOptions> = {
23
- format: 'D MMM YYYY',
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
- // Determine the date to use for the skeleton
27
- const skeletonDate = computed(() => {
28
- // Try to use modelValue if it's a Date
29
- if (props.modelValue instanceof Date) {
30
- return props.modelValue
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
- function handleSelect(date: Date) {
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
- const { createPicker } = usePikaday(
49
- {
50
- ...defaultOptions,
51
- ...props.options,
52
- bound: false,
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
- onMounted(async () => {
58
- if (containerRef.value) {
59
- const picker = await createPicker(containerRef.value)
60
- if (picker && picker.el && !containerRef.value.contains(picker.el)) {
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="relative w-[270px] h-[270px]">
70
- <div class="absolute inset-0">
71
- <CalendarSkeleton
72
- :number-of-months="options?.numberOfMonths ?? 1"
73
- :date="skeletonDate"
74
- :first-day="options?.firstDay"
75
- :class="loading ? '' : 'opacity-0 pointer-events-none transition-opacity duration-300'"
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 { PikadayOptions } from 'pikaday'
3
- import type { ComponentPublicInstance } from 'vue'
4
- import { onMounted, ref, watch } from 'vue'
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: Date | string | number | null
11
- /** All Pikaday options (except `field`) */
12
- options?: Omit<PikadayOptions, 'field'>
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 defaultOptions: Partial<PikadayOptions> = {
44
- format: 'D MMM YYYY',
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
- const { picker, createPicker } = usePikaday(
48
- {
49
- ...defaultOptions,
50
- ...props.options,
51
- bound: true,
52
- field: undefined, // will be set below
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
- onMounted(async () => {
72
- const inputEl = inputRef.value as HTMLInputElement | null
73
- // Format and set the initial input value BEFORE initializing Pikaday
74
- if (props.modelValue) {
75
- let d: Date | null = null
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
- function onFocus() {
101
- if (picker.value) {
102
- let d: Date | null = null
103
- if (props.modelValue instanceof Date) {
104
- d = props.modelValue
105
- } else if (typeof props.modelValue === 'string') {
106
- const tmp = new Date(props.modelValue)
107
- if (!Number.isNaN(tmp.getTime())) {
108
- d = tmp
109
- }
110
- }
111
- picker.value.setDate(d, true)
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
- val => {
118
- if (!picker.value) {
119
- return
120
- }
121
- if (!visible.value) {
122
- if (val instanceof Date) {
123
- picker.value.setDate(val, true)
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
- <input
142
- ref="inputRef"
143
- type="text"
144
- :value="inputValue"
145
- :placeholder="props.placeholder"
146
- :disabled="props.disabled"
147
- class="input"
148
- :class="[
149
- { validator: props.validator },
150
- { 'input-primary': props.primary || props.color === 'primary' },
151
- { 'input-secondary': props.secondary || props.color === 'secondary' },
152
- { 'input-accent': props.accent || props.color === 'accent' },
153
- { 'input-info': props.info || props.color === 'info' },
154
- { 'input-success': props.success || props.color === 'success' },
155
- { 'input-warning': props.warning || props.color === 'warning' },
156
- { 'input-error': props.error || props.color === 'error' },
157
- { 'input-ghost': props.ghost },
158
- { 'input-xl': props.xl || props.size === 'xl' },
159
- { 'input-lg': props.lg || props.size === 'lg' },
160
- { 'input-md': props.md || props.size === 'md' },
161
- { 'input-sm': props.sm || props.size === 'sm' },
162
- { 'input-xs': props.xs || props.size === 'xs' },
163
- { 'join-item': props.join },
164
- ]"
165
- v-bind="$attrs"
166
- @focus="onFocus"
167
- @input="
168
- e => {
169
- const val = (e.target as HTMLInputElement).value
170
- emit('update:inputValue', val)
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>
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
- import type { Toast } from '~/composables/use-toast'
2
+ import type { Toast } from '../composables/use-toast'
3
3
  import { computed } from 'vue'
4
- import { useToast } from '~/composables/use-toast'
4
+ import { useToast } from '../composables/use-toast'
5
5
 
6
6
  // Explicit slot typing (Vue 3.4+ / Volar)
7
7
  interface ToastSlotProps {