daisy-ui-kit 5.0.0-pre.3 → 5.0.0-pre.30

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 (151) hide show
  1. package/app/components/Accordion.vue +29 -0
  2. package/app/components/Alert.vue +36 -0
  3. package/app/components/Avatar.vue +131 -0
  4. package/app/components/AvatarGroup.vue +22 -0
  5. package/app/components/Badge.vue +72 -0
  6. package/app/components/Breadcrumbs.vue +7 -0
  7. package/app/components/Button.vue +140 -0
  8. package/app/components/Calendar.vue +175 -0
  9. package/app/components/CalendarInput.vue +275 -0
  10. package/app/components/CalendarSkeleton.vue +87 -0
  11. package/app/components/Card.vue +51 -0
  12. package/app/components/CardActions.vue +13 -0
  13. package/app/components/CardBody.vue +13 -0
  14. package/app/components/CardTitle.vue +11 -0
  15. package/app/components/Carousel.vue +24 -0
  16. package/app/components/CarouselItem.vue +5 -0
  17. package/app/components/Chat.vue +26 -0
  18. package/app/components/ChatBubble.vue +31 -0
  19. package/app/components/ChatFooter.vue +5 -0
  20. package/app/components/ChatHeader.vue +5 -0
  21. package/app/components/ChatImage.vue +5 -0
  22. package/app/components/Checkbox.vue +51 -0
  23. package/app/components/Collapse.vue +75 -0
  24. package/app/components/CollapseContent.vue +5 -0
  25. package/app/components/CollapseTitle.vue +15 -0
  26. package/app/components/Countdown.vue +15 -0
  27. package/app/components/CountdownTimers.vue +69 -0
  28. package/app/components/Counter.vue +21 -0
  29. package/app/components/Crumb.vue +5 -0
  30. package/app/components/DaisyLink.vue +56 -0
  31. package/app/components/Diff.vue +11 -0
  32. package/app/components/Divider.vue +43 -0
  33. package/app/components/Dock.vue +57 -0
  34. package/app/components/DockItem.vue +27 -0
  35. package/app/components/DockLabel.vue +5 -0
  36. package/app/components/Drawer.vue +50 -0
  37. package/app/components/DrawerContent.vue +20 -0
  38. package/app/components/DrawerSide.vue +21 -0
  39. package/app/components/Dropdown.vue +106 -0
  40. package/app/components/DropdownButton.vue +23 -0
  41. package/app/components/DropdownContent.vue +127 -0
  42. package/app/components/DropdownTarget.vue +21 -0
  43. package/app/components/Fab.vue +16 -0
  44. package/app/components/FabClose.vue +18 -0
  45. package/app/components/FabMainAction.vue +5 -0
  46. package/app/components/FabTrigger.vue +117 -0
  47. package/app/components/Fieldset.vue +20 -0
  48. package/app/components/FileInput.vue +53 -0
  49. package/app/components/Filter.vue +129 -0
  50. package/app/components/Flex.vue +89 -0
  51. package/app/components/FlexItem.vue +62 -0
  52. package/app/components/Footer.vue +31 -0
  53. package/app/components/FooterTitle.vue +18 -0
  54. package/app/components/FormControl.vue +5 -0
  55. package/app/components/Hero.vue +18 -0
  56. package/app/components/HeroContent.vue +18 -0
  57. package/app/components/HeroOverlay.vue +5 -0
  58. package/app/components/Hover3D.vue +22 -0
  59. package/app/components/HoverGallery.vue +11 -0
  60. package/app/components/Indicator.vue +20 -0
  61. package/app/components/IndicatorItem.vue +43 -0
  62. package/app/components/Input.vue +116 -0
  63. package/app/components/Join.vue +5 -0
  64. package/app/components/Kbd.vue +25 -0
  65. package/app/components/Label.vue +100 -0
  66. package/app/components/List.vue +5 -0
  67. package/app/components/ListColGrow.vue +5 -0
  68. package/app/components/ListColWrap.vue +5 -0
  69. package/app/components/ListRow.vue +5 -0
  70. package/app/components/LoadingBall.vue +42 -0
  71. package/app/components/LoadingBars.vue +42 -0
  72. package/app/components/LoadingDots.vue +42 -0
  73. package/app/components/LoadingInfinity.vue +42 -0
  74. package/app/components/LoadingRing.vue +42 -0
  75. package/app/components/LoadingSpinner.vue +42 -0
  76. package/app/components/Mask.vue +49 -0
  77. package/app/components/Menu.vue +30 -0
  78. package/app/components/MenuExpand.vue +92 -0
  79. package/app/components/MenuExpandToggle.vue +20 -0
  80. package/app/components/MenuItem.vue +39 -0
  81. package/app/components/MenuTitle.vue +5 -0
  82. package/app/components/MockupBrowser.vue +5 -0
  83. package/app/components/MockupBrowserToolbar.vue +5 -0
  84. package/app/components/MockupCode.vue +4 -0
  85. package/app/components/MockupPhone.vue +14 -0
  86. package/app/components/MockupWindow.vue +5 -0
  87. package/app/components/Modal.vue +63 -0
  88. package/app/components/ModalAction.vue +5 -0
  89. package/app/components/ModalBox.vue +5 -0
  90. package/app/components/NavButton.vue +12 -0
  91. package/app/components/Navbar.vue +12 -0
  92. package/app/components/NavbarCenter.vue +11 -0
  93. package/app/components/NavbarEnd.vue +11 -0
  94. package/app/components/NavbarStart.vue +11 -0
  95. package/app/components/Progress.vue +46 -0
  96. package/app/components/Prose.vue +37 -0
  97. package/app/components/RadialProgress.vue +36 -0
  98. package/app/components/Radio.vue +69 -0
  99. package/app/components/RadioGroup.vue +47 -0
  100. package/app/components/Range.vue +201 -0
  101. package/app/components/RangeMeasure.vue +87 -0
  102. package/app/components/RangeMeasureTick.vue +69 -0
  103. package/app/components/Rating.vue +197 -0
  104. package/app/components/Select.vue +101 -0
  105. package/app/components/Skeleton.vue +5 -0
  106. package/app/components/SkeletonText.vue +11 -0
  107. package/app/components/Stack.vue +30 -0
  108. package/app/components/Stat.vue +19 -0
  109. package/app/components/StatActions.vue +5 -0
  110. package/app/components/StatDesc.vue +5 -0
  111. package/app/components/StatFigure.vue +5 -0
  112. package/app/components/StatTitle.vue +5 -0
  113. package/app/components/StatValue.vue +5 -0
  114. package/app/components/Stats.vue +5 -0
  115. package/app/components/Status.vue +43 -0
  116. package/app/components/Step.vue +34 -0
  117. package/app/components/StepIcon.vue +5 -0
  118. package/app/components/Steps.vue +23 -0
  119. package/app/components/Swap.vue +56 -0
  120. package/app/components/Tab.vue +56 -0
  121. package/app/components/TabContent.vue +29 -0
  122. package/app/components/Table.vue +32 -0
  123. package/app/components/Tabs.vue +53 -0
  124. package/app/components/Text.vue +166 -0
  125. package/app/components/TextArea.vue +106 -0
  126. package/app/components/TextRotate.vue +24 -0
  127. package/app/components/ThemeController.vue +45 -0
  128. package/app/components/ThemeProvider.vue +302 -0
  129. package/app/components/ThemeTile.vue +50 -0
  130. package/app/components/Timeline.vue +22 -0
  131. package/app/components/TimelineEnd.vue +14 -0
  132. package/app/components/TimelineItem.vue +5 -0
  133. package/app/components/TimelineLine.vue +29 -0
  134. package/app/components/TimelineMiddle.vue +5 -0
  135. package/app/components/TimelineStart.vue +14 -0
  136. package/app/components/Toast.vue +67 -0
  137. package/app/components/Toggle.vue +60 -0
  138. package/app/components/Tooltip.vue +137 -0
  139. package/app/components/TooltipContent.vue +283 -0
  140. package/app/components/TooltipTarget.vue +20 -0
  141. package/app/components/ValidatorHint.vue +5 -0
  142. package/app/composables/__tests__/use-calendar.test.ts +239 -0
  143. package/app/composables/use-calendar.ts +288 -0
  144. package/app/composables/use-daisy-theme.ts +131 -0
  145. package/app/composables/use-toast.ts +345 -0
  146. package/app/composables/useSearch.ts +22 -0
  147. package/app/utils/drawer-utils.ts +34 -0
  148. package/app/utils/position-area.ts +40 -0
  149. package/nuxt.d.ts +13 -0
  150. package/nuxt.js +31 -0
  151. package/package.json +50 -22
@@ -0,0 +1,137 @@
1
+ <script setup lang="ts">
2
+ import { useId } from 'vue'
3
+ import { useElementHover } from '@vueuse/core'
4
+ import { computed, onMounted, provide, ref, watch } from 'vue'
5
+
6
+ const props = withDefaults(
7
+ defineProps<{
8
+ tip?: string | number
9
+ open?: boolean
10
+
11
+ color?: string
12
+ neutral?: boolean
13
+ primary?: boolean
14
+ secondary?: boolean
15
+ accent?: boolean
16
+ info?: boolean
17
+ success?: boolean
18
+ warning?: boolean
19
+ error?: boolean
20
+
21
+ position?: 'top' | 'right' | 'bottom' | 'left'
22
+ top?: boolean
23
+ right?: boolean
24
+ bottom?: boolean
25
+ left?: boolean
26
+
27
+ // Popover mode props
28
+ delayEnter?: number
29
+ delayLeave?: number
30
+ }>(),
31
+ {
32
+ delayEnter: 200,
33
+ delayLeave: 100,
34
+ },
35
+ )
36
+
37
+ // Detect if using popover mode (when TooltipTarget/TooltipContent are used)
38
+ const isPopoverMode = computed(() => {
39
+ return !props.tip
40
+ })
41
+
42
+ // Popover mode setup
43
+ const isOpen = defineModel('open', { default: false })
44
+ provide('isTooltipOpen', isOpen)
45
+
46
+ const uniqueId = useId()
47
+ const id = `tooltip-${uniqueId}`
48
+ provide('tooltipId', id)
49
+
50
+ // Compute placement for CSS anchor positioning
51
+ const placement = computed(() => {
52
+ if (props.top || props.position === 'top') return 'top'
53
+ if (props.right || props.position === 'right') return 'right'
54
+ if (props.left || props.position === 'left') return 'left'
55
+ return 'bottom' // default
56
+ })
57
+ provide('tooltipPlacement', placement)
58
+
59
+ // Provide color for TooltipContent
60
+ const color = computed(() => {
61
+ if (props.color) return props.color
62
+ if (props.neutral) return 'neutral'
63
+ if (props.primary) return 'primary'
64
+ if (props.secondary) return 'secondary'
65
+ if (props.accent) return 'accent'
66
+ if (props.info) return 'info'
67
+ if (props.success) return 'success'
68
+ if (props.warning) return 'warning'
69
+ if (props.error) return 'error'
70
+ return null
71
+ })
72
+ provide('tooltipColor', color)
73
+
74
+ // References
75
+ const targetEl = ref(null)
76
+ const contentEl = ref(null)
77
+ provide('targetEl', targetEl)
78
+ provide('contentEl', contentEl)
79
+
80
+ // Visibility utils
81
+ function open() {
82
+ isOpen.value = true
83
+ }
84
+ function close() {
85
+ isOpen.value = false
86
+ }
87
+ provide('openTooltip', open)
88
+ provide('closeTooltip', close)
89
+
90
+ const tooltipWrapper = ref(null)
91
+
92
+ onMounted(() => {
93
+ if (isPopoverMode.value) {
94
+ const hover = useElementHover(tooltipWrapper, {
95
+ delayLeave: props.delayLeave,
96
+ delayEnter: props.delayEnter,
97
+ })
98
+
99
+ watch(hover, newValue => {
100
+ isOpen.value = newValue
101
+ })
102
+ }
103
+ })
104
+ </script>
105
+
106
+ <template>
107
+ <!-- CSS-only mode (when tip prop is provided) -->
108
+ <div
109
+ v-if="!isPopoverMode"
110
+ :data-tip="tip"
111
+ class="tooltip"
112
+ :class="{
113
+ 'tooltip-open': props.open,
114
+
115
+ 'tooltip-top': props.top || props.position === 'top',
116
+ 'tooltip-bottom': props.bottom || props.position === 'bottom',
117
+ 'tooltip-left': props.left || props.position === 'left',
118
+ 'tooltip-right': props.right || props.position === 'right',
119
+
120
+ 'tooltip-neutral': props.neutral || props.color === 'neutral',
121
+ 'tooltip-primary': props.primary || props.color === 'primary',
122
+ 'tooltip-secondary': props.secondary || props.color === 'secondary',
123
+ 'tooltip-accent': props.accent || props.color === 'accent',
124
+ 'tooltip-info': props.info || props.color === 'info',
125
+ 'tooltip-success': props.success || props.color === 'success',
126
+ 'tooltip-warning': props.warning || props.color === 'warning',
127
+ 'tooltip-error': props.error || props.color === 'error',
128
+ }"
129
+ >
130
+ <slot />
131
+ </div>
132
+
133
+ <!-- Popover API mode (when using TooltipTarget/TooltipContent) -->
134
+ <div v-else ref="tooltipWrapper" class="tooltip-wrapper inline-block">
135
+ <slot />
136
+ </div>
137
+ </template>
@@ -0,0 +1,283 @@
1
+ <script setup>
2
+ import { computed, inject, ref, watch } from 'vue'
3
+ import { getPositionArea, getPositionFallbacks } from '../utils/position-area'
4
+
5
+ const id = inject('tooltipId', null)
6
+ const isOpen = inject('isTooltipOpen', ref(false))
7
+ const contentEl = inject('contentEl', ref(null))
8
+ const placement = inject('tooltipPlacement', ref('bottom'))
9
+ const color = inject('tooltipColor', ref(null))
10
+
11
+ // Check if we're in popover mode (inside a Tooltip wrapper with context)
12
+ const isPopoverMode = computed(() => id !== null)
13
+
14
+ // Compute CSS position-area value based on placement
15
+ const positionArea = computed(() => getPositionArea(placement.value))
16
+ const positionFallbacks = computed(() => getPositionFallbacks(placement.value))
17
+
18
+ // Color classes for the tooltip
19
+ const colorClass = computed(() => {
20
+ if (!color.value) return ''
21
+ const colorMap = {
22
+ neutral: 'tooltip-neutral',
23
+ primary: 'tooltip-primary',
24
+ secondary: 'tooltip-secondary',
25
+ accent: 'tooltip-accent',
26
+ info: 'tooltip-info',
27
+ success: 'tooltip-success',
28
+ warning: 'tooltip-warning',
29
+ error: 'tooltip-error',
30
+ }
31
+ return colorMap[color.value] || ''
32
+ })
33
+
34
+ // Sync popover state with isOpen model
35
+ watch(
36
+ isOpen,
37
+ newValue => {
38
+ if (!isPopoverMode.value || !contentEl.value) return
39
+
40
+ try {
41
+ const isPopoverOpen = contentEl.value.matches(':popover-open')
42
+
43
+ if (newValue && !isPopoverOpen) {
44
+ contentEl.value.showPopover()
45
+ } else if (!newValue && isPopoverOpen) {
46
+ contentEl.value.hidePopover()
47
+ }
48
+ } catch (e) {
49
+ console.warn('Popover API not supported:', e)
50
+ }
51
+ },
52
+ { flush: 'post' },
53
+ )
54
+
55
+ // Listen to popover toggle events to sync back to isOpen model
56
+ function handleToggle(event) {
57
+ const newState = event.newState === 'open'
58
+ if (isOpen.value !== newState) {
59
+ isOpen.value = newState
60
+ }
61
+ }
62
+ </script>
63
+
64
+ <template>
65
+ <!-- Popover mode -->
66
+ <div
67
+ v-if="isPopoverMode"
68
+ :id="`${id}-content`"
69
+ ref="contentEl"
70
+ :anchor="id"
71
+ :data-placement="placement"
72
+ role="tooltip"
73
+ popover="manual"
74
+ class="tooltip-popover"
75
+ :class="colorClass"
76
+ :style="{
77
+ 'position-anchor': `--${id}`,
78
+ 'position-area': positionArea,
79
+ 'position-try-fallbacks': positionFallbacks,
80
+ }"
81
+ @toggle="handleToggle"
82
+ >
83
+ <slot />
84
+ </div>
85
+
86
+ <!-- Simple mode (no Tooltip wrapper context) -->
87
+ <div v-else class="tooltip-content">
88
+ <slot />
89
+ </div>
90
+ </template>
91
+
92
+ <style>
93
+ @layer components {
94
+ .tooltip-popover[popover] {
95
+ border: none;
96
+ overflow: visible;
97
+ inset: auto;
98
+ margin: 0;
99
+ padding: 0.25rem 0.5rem;
100
+ font-size: 0.875rem;
101
+ line-height: 1.25rem;
102
+ border-radius: var(--radius-selector, 0.25rem);
103
+ background-color: var(--color-neutral);
104
+ color: var(--color-neutral-content);
105
+ max-width: 20rem;
106
+ }
107
+
108
+ .tooltip-popover[popover]:popover-open {
109
+ position: fixed;
110
+ }
111
+
112
+ /* Arrow base styles */
113
+ .tooltip-popover[popover]::before {
114
+ content: '';
115
+ position: absolute;
116
+ border: 6px solid transparent;
117
+ }
118
+
119
+ /* Bottom placement - arrow points up */
120
+ .tooltip-popover[data-placement='bottom'] {
121
+ margin-top: 6px;
122
+ }
123
+ .tooltip-popover[data-placement='bottom']::before {
124
+ bottom: 100%;
125
+ left: 50%;
126
+ transform: translateX(-50%);
127
+ border-bottom-color: var(--color-neutral);
128
+ }
129
+
130
+ /* Top placement - arrow points down */
131
+ .tooltip-popover[data-placement='top'] {
132
+ margin-bottom: 6px;
133
+ }
134
+ .tooltip-popover[data-placement='top']::before {
135
+ top: 100%;
136
+ left: 50%;
137
+ transform: translateX(-50%);
138
+ border-top-color: var(--color-neutral);
139
+ }
140
+
141
+ /* Right placement - arrow points left */
142
+ .tooltip-popover[data-placement='right'] {
143
+ margin-left: 6px;
144
+ }
145
+ .tooltip-popover[data-placement='right']::before {
146
+ right: 100%;
147
+ top: 50%;
148
+ transform: translateY(-50%);
149
+ border-right-color: var(--color-neutral);
150
+ }
151
+
152
+ /* Left placement - arrow points right */
153
+ .tooltip-popover[data-placement='left'] {
154
+ margin-right: 6px;
155
+ }
156
+ .tooltip-popover[data-placement='left']::before {
157
+ left: 100%;
158
+ top: 50%;
159
+ transform: translateY(-50%);
160
+ border-left-color: var(--color-neutral);
161
+ }
162
+
163
+ /* Color variants */
164
+ .tooltip-popover.tooltip-primary {
165
+ background-color: var(--color-primary);
166
+ color: var(--color-primary-content);
167
+ }
168
+ .tooltip-popover.tooltip-primary[data-placement='bottom']::before {
169
+ border-bottom-color: var(--color-primary);
170
+ }
171
+ .tooltip-popover.tooltip-primary[data-placement='top']::before {
172
+ border-top-color: var(--color-primary);
173
+ }
174
+ .tooltip-popover.tooltip-primary[data-placement='right']::before {
175
+ border-right-color: var(--color-primary);
176
+ }
177
+ .tooltip-popover.tooltip-primary[data-placement='left']::before {
178
+ border-left-color: var(--color-primary);
179
+ }
180
+
181
+ .tooltip-popover.tooltip-secondary {
182
+ background-color: var(--color-secondary);
183
+ color: var(--color-secondary-content);
184
+ }
185
+ .tooltip-popover.tooltip-secondary[data-placement='bottom']::before {
186
+ border-bottom-color: var(--color-secondary);
187
+ }
188
+ .tooltip-popover.tooltip-secondary[data-placement='top']::before {
189
+ border-top-color: var(--color-secondary);
190
+ }
191
+ .tooltip-popover.tooltip-secondary[data-placement='right']::before {
192
+ border-right-color: var(--color-secondary);
193
+ }
194
+ .tooltip-popover.tooltip-secondary[data-placement='left']::before {
195
+ border-left-color: var(--color-secondary);
196
+ }
197
+
198
+ .tooltip-popover.tooltip-accent {
199
+ background-color: var(--color-accent);
200
+ color: var(--color-accent-content);
201
+ }
202
+ .tooltip-popover.tooltip-accent[data-placement='bottom']::before {
203
+ border-bottom-color: var(--color-accent);
204
+ }
205
+ .tooltip-popover.tooltip-accent[data-placement='top']::before {
206
+ border-top-color: var(--color-accent);
207
+ }
208
+ .tooltip-popover.tooltip-accent[data-placement='right']::before {
209
+ border-right-color: var(--color-accent);
210
+ }
211
+ .tooltip-popover.tooltip-accent[data-placement='left']::before {
212
+ border-left-color: var(--color-accent);
213
+ }
214
+
215
+ .tooltip-popover.tooltip-info {
216
+ background-color: var(--color-info);
217
+ color: var(--color-info-content);
218
+ }
219
+ .tooltip-popover.tooltip-info[data-placement='bottom']::before {
220
+ border-bottom-color: var(--color-info);
221
+ }
222
+ .tooltip-popover.tooltip-info[data-placement='top']::before {
223
+ border-top-color: var(--color-info);
224
+ }
225
+ .tooltip-popover.tooltip-info[data-placement='right']::before {
226
+ border-right-color: var(--color-info);
227
+ }
228
+ .tooltip-popover.tooltip-info[data-placement='left']::before {
229
+ border-left-color: var(--color-info);
230
+ }
231
+
232
+ .tooltip-popover.tooltip-success {
233
+ background-color: var(--color-success);
234
+ color: var(--color-success-content);
235
+ }
236
+ .tooltip-popover.tooltip-success[data-placement='bottom']::before {
237
+ border-bottom-color: var(--color-success);
238
+ }
239
+ .tooltip-popover.tooltip-success[data-placement='top']::before {
240
+ border-top-color: var(--color-success);
241
+ }
242
+ .tooltip-popover.tooltip-success[data-placement='right']::before {
243
+ border-right-color: var(--color-success);
244
+ }
245
+ .tooltip-popover.tooltip-success[data-placement='left']::before {
246
+ border-left-color: var(--color-success);
247
+ }
248
+
249
+ .tooltip-popover.tooltip-warning {
250
+ background-color: var(--color-warning);
251
+ color: var(--color-warning-content);
252
+ }
253
+ .tooltip-popover.tooltip-warning[data-placement='bottom']::before {
254
+ border-bottom-color: var(--color-warning);
255
+ }
256
+ .tooltip-popover.tooltip-warning[data-placement='top']::before {
257
+ border-top-color: var(--color-warning);
258
+ }
259
+ .tooltip-popover.tooltip-warning[data-placement='right']::before {
260
+ border-right-color: var(--color-warning);
261
+ }
262
+ .tooltip-popover.tooltip-warning[data-placement='left']::before {
263
+ border-left-color: var(--color-warning);
264
+ }
265
+
266
+ .tooltip-popover.tooltip-error {
267
+ background-color: var(--color-error);
268
+ color: var(--color-error-content);
269
+ }
270
+ .tooltip-popover.tooltip-error[data-placement='bottom']::before {
271
+ border-bottom-color: var(--color-error);
272
+ }
273
+ .tooltip-popover.tooltip-error[data-placement='top']::before {
274
+ border-top-color: var(--color-error);
275
+ }
276
+ .tooltip-popover.tooltip-error[data-placement='right']::before {
277
+ border-right-color: var(--color-error);
278
+ }
279
+ .tooltip-popover.tooltip-error[data-placement='left']::before {
280
+ border-left-color: var(--color-error);
281
+ }
282
+ }
283
+ </style>
@@ -0,0 +1,20 @@
1
+ <script setup>
2
+ import { inject } from 'vue'
3
+
4
+ const id = inject('tooltipId')
5
+ const targetEl = inject('targetEl')
6
+ </script>
7
+
8
+ <template>
9
+ <div
10
+ :id="id"
11
+ ref="targetEl"
12
+ :aria-describedby="`${id}-content`"
13
+ :popovertarget="`${id}-content`"
14
+ popovertargetaction="toggle"
15
+ :style="{ 'anchor-name': `--${id}` }"
16
+ class="tooltip-target inline-block"
17
+ >
18
+ <slot />
19
+ </div>
20
+ </template>
@@ -0,0 +1,5 @@
1
+ <template>
2
+ <span class="validator-hint">
3
+ <slot />
4
+ </span>
5
+ </template>
@@ -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
+ })