@nexxtmove/ui 2.6.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (229) hide show
  1. package/dist/components/editor/molecules/EditorColor/EditorColor.vue.d.ts +7 -5
  2. package/dist/components/editor/molecules/EditorColor/_ColorField.vue.d.ts +4 -1
  3. package/dist/components/organisms/ColorPicker/ColorPicker.vue.d.ts +3 -1
  4. package/dist/index.js +95 -172
  5. package/dist/nuxt.js +0 -1
  6. package/package.json +38 -36
  7. package/src/attribute-contract.test.ts +151 -0
  8. package/src/components/atoms/AnimatedNumber/AnimatedNumber.stories.ts +36 -0
  9. package/src/components/atoms/AnimatedNumber/AnimatedNumber.test.ts +21 -0
  10. package/src/components/atoms/Avatar/Avatar.stories.ts +122 -0
  11. package/src/components/atoms/Avatar/Avatar.test.ts +134 -0
  12. package/src/components/atoms/Button/Button.stories.ts +290 -0
  13. package/src/components/atoms/Button/Button.test.ts +406 -0
  14. package/src/components/atoms/Card/Card.stories.ts +45 -0
  15. package/src/components/atoms/Card/Card.test.ts +46 -0
  16. package/src/components/atoms/Checkbox/Checkbox.stories.ts +35 -0
  17. package/src/components/atoms/Checkbox/Checkbox.test.ts +55 -0
  18. package/src/components/atoms/Chip/Chip.stories.ts +274 -0
  19. package/src/components/atoms/Chip/Chip.test.ts +230 -0
  20. package/src/components/atoms/EnergyLabel/EnergyLabel.stories.ts +189 -0
  21. package/src/components/atoms/EnergyLabel/EnergyLabel.test.ts +100 -0
  22. package/src/components/atoms/FloatingPanel/FloatingPanel.stories.ts +677 -0
  23. package/src/components/atoms/FloatingPanel/FloatingPanel.test.ts +864 -0
  24. package/src/components/atoms/Icon/Icon.stories.ts +63 -0
  25. package/src/components/atoms/Icon/Icon.test.ts +89 -0
  26. package/src/components/atoms/Loader/Loader.stories.ts +36 -0
  27. package/src/components/atoms/Loader/Loader.test.ts +40 -0
  28. package/src/components/atoms/PickerItem/PickerItem.stories.ts +172 -0
  29. package/src/components/atoms/PickerItem/PickerItem.test.ts +142 -0
  30. package/src/components/atoms/PickerPreview/PickerPreview.stories.ts +183 -0
  31. package/src/components/atoms/PickerPreview/PickerPreview.test.ts +137 -0
  32. package/src/components/atoms/PresenceIndicator/PresenceIndicator.stories.ts +21 -0
  33. package/src/components/atoms/PropertyListing/PropertyListing.stories.ts +38 -0
  34. package/src/components/atoms/PropertyListing/PropertyListing.test.ts +46 -0
  35. package/src/components/atoms/SelectableOption/SelectableOption.stories.ts +131 -0
  36. package/src/components/atoms/SelectableOption/SelectableOption.test.ts +96 -0
  37. package/src/components/atoms/Skeleton/Skeleton.stories.ts +27 -0
  38. package/src/components/atoms/Skeleton/Skeleton.test.ts +28 -0
  39. package/src/components/atoms/Slider/Slider.stories.ts +151 -0
  40. package/src/components/atoms/Slider/Slider.test.ts +191 -0
  41. package/src/components/atoms/Squircle/Squircle.stories.ts +114 -0
  42. package/src/components/atoms/Squircle/Squircle.test.ts +70 -0
  43. package/src/components/atoms/Switch/Switch.stories.ts +91 -0
  44. package/src/components/atoms/Switch/Switch.test.ts +183 -0
  45. package/src/components/atoms/Tooltip/Tooltip.stories.ts +158 -0
  46. package/src/components/atoms/Tooltip/Tooltip.test.ts +134 -0
  47. package/src/components/editor/atoms/EditorDimensionField/EditorDimensionField.stories.ts +124 -0
  48. package/src/components/editor/atoms/EditorDimensionField/EditorDimensionField.test.ts +464 -0
  49. package/src/components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.stories.ts +266 -0
  50. package/src/components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.test.ts +421 -0
  51. package/src/components/editor/atoms/EditorSettingsGroup/EditorSettingsGroup.stories.ts +365 -0
  52. package/src/components/editor/atoms/EditorSettingsGroup/EditorSettingsGroup.test.ts +484 -0
  53. package/src/components/editor/composables/useRadioGrid.test.ts +261 -0
  54. package/src/components/editor/molecules/EditorAlignItems/EditorAlignItems.stories.ts +73 -0
  55. package/src/components/editor/molecules/EditorAlignItems/EditorAlignItems.test.ts +189 -0
  56. package/src/components/editor/molecules/EditorAlignmentGrid/EditorAlignmentGrid.stories.ts +309 -0
  57. package/src/components/editor/molecules/EditorAlignmentGrid/EditorAlignmentGrid.test.ts +359 -0
  58. package/src/components/editor/molecules/EditorAnchor/EditorAnchor.stories.ts +138 -0
  59. package/src/components/editor/molecules/EditorAnchor/EditorAnchor.test.ts +344 -0
  60. package/src/components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.stories.ts +87 -0
  61. package/src/components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.test.ts +262 -0
  62. package/src/components/editor/molecules/EditorBlur/EditorBlur.stories.ts +115 -0
  63. package/src/components/editor/molecules/EditorBlur/EditorBlur.test.ts +243 -0
  64. package/src/components/editor/molecules/EditorBorder/EditorBorder.stories.ts +187 -0
  65. package/src/components/editor/molecules/EditorBorder/EditorBorder.test.ts +456 -0
  66. package/src/components/editor/molecules/EditorColor/EditorColor.stories.ts +633 -0
  67. package/src/components/editor/molecules/EditorColor/EditorColor.test.ts +776 -0
  68. package/src/components/editor/molecules/EditorColor/EditorColor.vue +7 -5
  69. package/src/components/editor/molecules/EditorColor/EditorColorGradient.test.ts +832 -0
  70. package/src/components/editor/molecules/EditorColor/_ColorField.vue +20 -15
  71. package/src/components/editor/molecules/EditorColor/gradientClass.test.ts +303 -0
  72. package/src/components/editor/molecules/EditorDashPattern/EditorDashPattern.stories.ts +72 -0
  73. package/src/components/editor/molecules/EditorDashPattern/EditorDashPattern.test.ts +124 -0
  74. package/src/components/editor/molecules/EditorFlexDirection/EditorFlexDirection.stories.ts +46 -0
  75. package/src/components/editor/molecules/EditorFlexDirection/EditorFlexDirection.test.ts +97 -0
  76. package/src/components/editor/molecules/EditorFontSize/EditorFontSize.stories.ts +248 -0
  77. package/src/components/editor/molecules/EditorFontSize/EditorFontSize.test.ts +288 -0
  78. package/src/components/editor/molecules/EditorGap/EditorGap.stories.ts +129 -0
  79. package/src/components/editor/molecules/EditorGap/EditorGap.test.ts +253 -0
  80. package/src/components/editor/molecules/EditorHeadingType/EditorHeadingType.stories.ts +68 -0
  81. package/src/components/editor/molecules/EditorHeadingType/EditorHeadingType.test.ts +150 -0
  82. package/src/components/editor/molecules/EditorJustifyContent/EditorJustifyContent.stories.ts +71 -0
  83. package/src/components/editor/molecules/EditorJustifyContent/EditorJustifyContent.test.ts +161 -0
  84. package/src/components/editor/molecules/EditorLetterSpacing/EditorLetterSpacing.stories.ts +204 -0
  85. package/src/components/editor/molecules/EditorLetterSpacing/EditorLetterSpacing.test.ts +238 -0
  86. package/src/components/editor/molecules/EditorLineHeight/EditorLineHeight.stories.ts +257 -0
  87. package/src/components/editor/molecules/EditorLineHeight/EditorLineHeight.test.ts +286 -0
  88. package/src/components/editor/molecules/EditorLineStyle/EditorLineStyle.stories.ts +135 -0
  89. package/src/components/editor/molecules/EditorLineStyle/EditorLineStyle.test.ts +230 -0
  90. package/src/components/editor/molecules/EditorMaxWidth/EditorMaxWidth.stories.ts +154 -0
  91. package/src/components/editor/molecules/EditorMaxWidth/EditorMaxWidth.test.ts +112 -0
  92. package/src/components/editor/molecules/EditorOverrideStatus/EditorOverrideStatus.stories.ts +206 -0
  93. package/src/components/editor/molecules/EditorOverrideStatus/EditorOverrideStatus.test.ts +332 -0
  94. package/src/components/editor/molecules/EditorPadding/EditorPadding.stories.ts +374 -0
  95. package/src/components/editor/molecules/EditorPadding/EditorPadding.test.ts +712 -0
  96. package/src/components/editor/molecules/EditorRadius/EditorRadius.stories.ts +64 -0
  97. package/src/components/editor/molecules/EditorRadius/EditorRadius.test.ts +137 -0
  98. package/src/components/editor/molecules/EditorShadow/EditorShadow.stories.ts +103 -0
  99. package/src/components/editor/molecules/EditorShadow/EditorShadow.test.ts +168 -0
  100. package/src/components/editor/molecules/EditorSize/EditorSize.stories.ts +84 -0
  101. package/src/components/editor/molecules/EditorSize/EditorSize.test.ts +253 -0
  102. package/src/components/editor/molecules/EditorSizeLimit/EditorSizeLimit.stories.ts +49 -0
  103. package/src/components/editor/molecules/EditorSizeLimit/EditorSizeLimit.test.ts +43 -0
  104. package/src/components/editor/molecules/EditorTextAlign/EditorTextAlign.stories.ts +69 -0
  105. package/src/components/editor/molecules/EditorTextAlign/EditorTextAlign.test.ts +155 -0
  106. package/src/components/molecules/Announcement/Announcement.stories.ts +224 -0
  107. package/src/components/molecules/Announcement/Announcement.test.ts +161 -0
  108. package/src/components/molecules/ContactInfoCard/ContactInfoCard.stories.ts +99 -0
  109. package/src/components/molecules/ContactInfoCard/ContactInfoCard.test.ts +70 -0
  110. package/src/components/molecules/CustomerJourneyTile/CustomerJourneyTile.stories.ts +148 -0
  111. package/src/components/molecules/CustomerJourneyTile/CustomerJourneyTile.test.ts +271 -0
  112. package/src/components/molecules/DoughnutChart/DoughnutChart.stories.ts +45 -0
  113. package/src/components/molecules/DoughnutChart/DoughnutChart.test.ts +205 -0
  114. package/src/components/molecules/InfoBlock/InfoBlock.stories.ts +84 -0
  115. package/src/components/molecules/InfoBlock/InfoBlock.test.ts +101 -0
  116. package/src/components/molecules/InputField/InputField.stories.ts +117 -0
  117. package/src/components/molecules/InputField/InputField.test.ts +138 -0
  118. package/src/components/molecules/InputSelect/InputSelect.stories.ts +116 -0
  119. package/src/components/molecules/InputSelect/InputSelect.test.ts +127 -0
  120. package/src/components/molecules/ModalFooter/ModalFooter.stories.ts +204 -0
  121. package/src/components/molecules/ModalFooter/ModalFooter.test.ts +276 -0
  122. package/src/components/molecules/PageCard/PageCard.stories.ts +100 -0
  123. package/src/components/molecules/PageCard/PageCard.test.ts +91 -0
  124. package/src/components/molecules/PageContainer/PageContainer.stories.ts +90 -0
  125. package/src/components/molecules/PageContainer/PageContainer.test.ts +50 -0
  126. package/src/components/molecules/PageMockup/PageMockup.stories.ts +38 -0
  127. package/src/components/molecules/PageMockup/PageMockup.test.ts +20 -0
  128. package/src/components/molecules/PageTypeTile/PageTypeTile.stories.ts +63 -0
  129. package/src/components/molecules/PageTypeTile/PageTypeTile.test.ts +41 -0
  130. package/src/components/molecules/Pagination/Pagination.stories.ts +72 -0
  131. package/src/components/molecules/Pagination/Pagination.test.ts +130 -0
  132. package/src/components/molecules/PickerGroup/PickerGroup.stories.ts +90 -0
  133. package/src/components/molecules/PickerGroup/PickerGroup.test.ts +145 -0
  134. package/src/components/molecules/PickerList/PickerList.stories.ts +436 -0
  135. package/src/components/molecules/PickerList/PickerList.test.ts +870 -0
  136. package/src/components/molecules/PlaceholderInput/PlaceholderInput.stories.ts +48 -0
  137. package/src/components/molecules/PlaceholderInput/PlaceholderInput.test.ts +107 -0
  138. package/src/components/molecules/PropertyCard/PropertyCard.stories.ts +46 -0
  139. package/src/components/molecules/PropertyCard/PropertyCard.test.ts +59 -0
  140. package/src/components/molecules/Select/Select.stories.ts +264 -0
  141. package/src/components/molecules/Select/Select.test.ts +315 -0
  142. package/src/components/molecules/SidebarMenuItem/SidebarMenuItem.stories.ts +84 -0
  143. package/src/components/molecules/SidebarMenuItem/SidebarMenuItem.test.ts +103 -0
  144. package/src/components/molecules/SkeletonPropertyCard/SkeletonPropertyCard.stories.ts +11 -0
  145. package/src/components/molecules/SkeletonPropertyCard/SkeletonPropertyCard.test.ts +38 -0
  146. package/src/components/molecules/SocialIcons/SocialIcons.stories.ts +28 -0
  147. package/src/components/molecules/SocialIcons/SocialIcons.test.ts +58 -0
  148. package/src/components/molecules/SocialMediaCustomTemplate/SocialMediaCustomTemplate.stories.ts +11 -0
  149. package/src/components/molecules/SocialMediaCustomTemplate/SocialMediaCustomTemplate.test.ts +116 -0
  150. package/src/components/molecules/SocialMediaType/SocialMediaType.stories.ts +37 -0
  151. package/src/components/molecules/SocialMediaType/SocialMediaType.test.ts +126 -0
  152. package/src/components/molecules/Source/Source.stories.ts +35 -0
  153. package/src/components/molecules/Source/Source.test.ts +45 -0
  154. package/src/components/molecules/TabbedContent/TabbedContent.stories.ts +96 -0
  155. package/src/components/molecules/TabbedContent/TabbedContent.test.ts +109 -0
  156. package/src/components/molecules/Tabs/Tabs.stories.ts +230 -0
  157. package/src/components/molecules/Tabs/Tabs.test.ts +280 -0
  158. package/src/components/molecules/TemplateCard/TemplateCard.stories.ts +327 -0
  159. package/src/components/molecules/TemplateCard/TemplateCard.test.ts +339 -0
  160. package/src/components/molecules/Textarea/Textarea.stories.ts +216 -0
  161. package/src/components/molecules/Textarea/Textarea.test.ts +191 -0
  162. package/src/components/molecules/TimelineEvent/TimelineEvent.stories.ts +319 -0
  163. package/src/components/molecules/TimelineEvent/TimelineEvent.test.ts +224 -0
  164. package/src/components/molecules/TimelineLanes/TimelineLanes.stories.ts +29 -0
  165. package/src/components/molecules/TimelineLanes/TimelineLanes.test.ts +30 -0
  166. package/src/components/molecules/Toast/Toast.stories.ts +110 -0
  167. package/src/components/molecules/Toast/Toast.test.ts +103 -0
  168. package/src/components/molecules/ToggleGroup/ToggleGroup.stories.ts +297 -0
  169. package/src/components/molecules/ToggleGroup/ToggleGroup.test.ts +578 -0
  170. package/src/components/molecules/Tree/Tree.stories.ts +884 -0
  171. package/src/components/molecules/Tree/Tree.test.ts +1247 -0
  172. package/src/components/molecules/Tree/tree-drop.test.ts +223 -0
  173. package/src/components/organisms/Calendar/Calendar.stories.ts +108 -0
  174. package/src/components/organisms/Calendar/Calendar.test.ts +279 -0
  175. package/src/components/organisms/Calendar/_CalendarDayView.test.ts +104 -0
  176. package/src/components/organisms/Calendar/_CalendarHeader.test.ts +86 -0
  177. package/src/components/organisms/Calendar/_CalendarMonthView.test.ts +52 -0
  178. package/src/components/organisms/Calendar/_CalendarTimePicker.test.ts +89 -0
  179. package/src/components/organisms/Calendar/_CalendarYearView.test.ts +56 -0
  180. package/src/components/organisms/ColorPicker/ColorPicker.stories.ts +229 -0
  181. package/src/components/organisms/ColorPicker/ColorPicker.test.ts +696 -0
  182. package/src/components/organisms/ColorPicker/ColorPicker.vue +12 -3
  183. package/src/components/organisms/ColorPicker/gradient.test.ts +67 -0
  184. package/src/components/organisms/ColorPicker/gradientGeometry.test.ts +206 -0
  185. package/src/components/organisms/Combobox/Combobox.stories.ts +234 -0
  186. package/src/components/organisms/Combobox/Combobox.test.ts +512 -0
  187. package/src/components/organisms/Combobox/Combobox.vue +1 -1
  188. package/src/components/organisms/ComboboxPanel/ComboboxPanel.stories.ts +158 -0
  189. package/src/components/organisms/ComboboxPanel/ComboboxPanel.test.ts +524 -0
  190. package/src/components/organisms/ComboboxPanel/ComboboxPanel.vue +1 -1
  191. package/src/components/organisms/CountrySelect/CountrySelect.stories.ts +139 -0
  192. package/src/components/organisms/CountrySelect/CountrySelect.test.ts +149 -0
  193. package/src/components/organisms/DatePicker/DatePicker.stories.ts +163 -0
  194. package/src/components/organisms/DatePicker/DatePicker.test.ts +214 -0
  195. package/src/components/organisms/DropdownButton/DropdownButton.stories.ts +123 -0
  196. package/src/components/organisms/DropdownButton/DropdownButton.test.ts +379 -0
  197. package/src/components/organisms/FontPicker/FontPicker.stories.ts +476 -0
  198. package/src/components/organisms/FontPicker/FontPicker.test.ts +873 -0
  199. package/src/components/organisms/Header/Header.stories.ts +75 -0
  200. package/src/components/organisms/Header/Header.test.ts +233 -0
  201. package/src/components/organisms/IconPicker/IconPicker.stories.ts +734 -0
  202. package/src/components/organisms/IconPicker/IconPicker.test.ts +669 -0
  203. package/src/components/organisms/IconPicker/_IconPickerGrid.test.ts +184 -0
  204. package/src/components/organisms/IconPicker/_IconPickerRail.test.ts +102 -0
  205. package/src/components/organisms/IconPicker/useIconPickerNavigation.test.ts +215 -0
  206. package/src/components/organisms/Modal/Modal.stories.ts +350 -0
  207. package/src/components/organisms/Modal/Modal.test.ts +275 -0
  208. package/src/components/organisms/PickerPanel/PickerPanel.stories.ts +452 -0
  209. package/src/components/organisms/PickerPanel/PickerPanel.test.ts +358 -0
  210. package/src/components/organisms/ProgressBar/ProgressBar.stories.ts +100 -0
  211. package/src/components/organisms/ProgressBar/ProgressBar.test.ts +417 -0
  212. package/src/components/organisms/ReferenceCard/ReferenceCard.stories.ts +191 -0
  213. package/src/components/organisms/ReferenceCard/ReferenceCard.test.ts +165 -0
  214. package/src/components/organisms/SocialMediaTemplate/SocialMediaTemplate.stories.ts +65 -0
  215. package/src/components/organisms/SocialMediaTemplate/SocialMediaTemplate.test.ts +466 -0
  216. package/src/components/organisms/StepperHeader/StepperHeader.stories.ts +91 -0
  217. package/src/components/organisms/StepperHeader/StepperHeader.test.ts +279 -0
  218. package/src/components/organisms/Table/Table.stories.ts +332 -0
  219. package/src/components/organisms/Table/Table.test.ts +320 -0
  220. package/src/components/organisms/TimelinePhaseblock/TimelinePhaseblock.stories.ts +198 -0
  221. package/src/components/organisms/TimelinePhaseblock/TimelinePhaseblock.test.ts +283 -0
  222. package/src/components/organisms/TimelineTrack/TimelineTrack.stories.ts +120 -0
  223. package/src/components/organisms/TimelineTrack/TimelineTrack.test.ts +91 -0
  224. package/src/components/organisms/Toaster/Toaster.stories.ts +51 -0
  225. package/src/components/organisms/Toaster/Toaster.test.ts +75 -0
  226. package/src/components/organisms/VideoControls/VideoControls.stories.ts +792 -0
  227. package/src/components/organisms/VideoControls/VideoControls.test.ts +1056 -0
  228. package/src/components/organisms/VideoPlayer/VideoPlayer.stories.ts +327 -0
  229. package/src/components/organisms/VideoPlayer/VideoPlayer.test.ts +439 -0
@@ -0,0 +1,104 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import CalendarDayView from './_CalendarDayView.vue'
4
+ import { nl } from 'date-fns/locale'
5
+ import { startOfMonth, endOfMonth, startOfWeek, endOfWeek, eachDayOfInterval } from 'date-fns'
6
+
7
+ const VIEW_DATE = new Date(2026, 2, 20) // March 20, 2026
8
+ const start = startOfWeek(startOfMonth(VIEW_DATE), { weekStartsOn: 1 })
9
+ const end = endOfWeek(endOfMonth(VIEW_DATE), { weekStartsOn: 1 })
10
+ const DAYS = eachDayOfInterval({ start, end })
11
+
12
+ const defaults = {
13
+ viewDate: VIEW_DATE,
14
+ calendarDays: DAYS,
15
+ modelValue: null,
16
+ markedDates: [],
17
+ locale: nl,
18
+ isDisabled: () => false,
19
+ timezone: 'UTC',
20
+ }
21
+
22
+ const mountDayView = (props: Record<string, unknown> = {}) =>
23
+ mount(CalendarDayView, {
24
+ props: { ...defaults, ...props },
25
+ })
26
+
27
+ describe('CalendarDayView', () => {
28
+ it('renders the correct number of days', () => {
29
+ const wrapper = mountDayView()
30
+ const dayButtons = wrapper.findAll('button')
31
+ expect(dayButtons.length).toBe(DAYS.length)
32
+ })
33
+
34
+ it('highlights the selected day', () => {
35
+ const selectedDate = new Date(2026, 2, 15)
36
+ const wrapper = mountDayView({
37
+ modelValue: selectedDate,
38
+ })
39
+ const selectedCell = wrapper.find('[role="gridcell"][aria-selected="true"]')
40
+ expect(selectedCell.exists()).toBe(true)
41
+ expect(selectedCell.text()).toBe('15')
42
+ })
43
+
44
+ it('marks dates correctly', () => {
45
+ const markedDates = [new Date(2026, 2, 10)]
46
+ const wrapper = mountDayView({
47
+ markedDates,
48
+ })
49
+ // In our component, isMarked(day) is used to show a small dot.
50
+ // The dot is a span with bg-cornflower-blue-500 class.
51
+ expect(wrapper.find('.bg-cornflower-blue-500').exists()).toBe(true)
52
+ })
53
+
54
+ it('disables dates based on isDisabled prop', () => {
55
+ const wrapper = mountDayView({
56
+ isDisabled: (day: Date) => day.getDate() === 15,
57
+ })
58
+ const disabledButton = wrapper.find('button[disabled]')
59
+ expect(disabledButton.exists()).toBe(true)
60
+ expect(disabledButton.text()).toBe('15')
61
+ })
62
+
63
+ it('emits select event when a day is clicked', async () => {
64
+ const wrapper = mountDayView()
65
+ const button = wrapper.findAll('button')[10]
66
+ if (button) {
67
+ await button.trigger('click')
68
+ expect(wrapper.emitted('select')).toBeTruthy()
69
+ expect(wrapper.emitted('select')?.[0]?.[0]).toBeInstanceOf(Date)
70
+ }
71
+ })
72
+
73
+ it('handles keyboard navigation (ArrowRight)', async () => {
74
+ const wrapper = mountDayView({
75
+ attachTo: document.body,
76
+ })
77
+ const button = wrapper.findAll('button')[0]
78
+ if (button) {
79
+ await button.trigger('keydown', { key: 'ArrowRight' })
80
+ }
81
+ // In vitest/jsdom, focus might not move as easily, so we check if the method logic runs
82
+ // For unit tests, we can also check if emit('select') is called on Enter/Space
83
+ })
84
+
85
+ it('emits select when Enter or Space is pressed', async () => {
86
+ const wrapper = mountDayView()
87
+ const button = wrapper.findAll('button')[0]
88
+ if (button) {
89
+ await button.trigger('keydown', { key: 'Enter' })
90
+ expect(wrapper.emitted('select')).toBeTruthy()
91
+ await button.trigger('keydown', { key: ' ' })
92
+ expect(wrapper.emitted('select')?.[1]).toBeTruthy()
93
+ }
94
+ })
95
+
96
+ it('does not emit select for other keys', async () => {
97
+ const wrapper = mountDayView()
98
+ const button = wrapper.findAll('button')[0]
99
+ if (button) {
100
+ await button.trigger('keydown', { key: 'Tab' })
101
+ expect(wrapper.emitted('select')).toBeFalsy()
102
+ }
103
+ })
104
+ })
@@ -0,0 +1,86 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import CalendarHeader from './_CalendarHeader.vue'
4
+ import { nl } from 'date-fns/locale'
5
+
6
+ const VIEW_DATE = new Date(2026, 2, 20) // March 20, 2026
7
+
8
+ describe('CalendarHeader', () => {
9
+ it('renders the correct month and year label', () => {
10
+ const wrapper = mount(CalendarHeader, {
11
+ props: {
12
+ viewDate: VIEW_DATE,
13
+ pickerView: 'days',
14
+ monthYearPicker: false,
15
+ locale: nl,
16
+ canGoPrev: true,
17
+ canGoNext: true,
18
+ },
19
+ })
20
+ expect(wrapper.text()).toContain('Mrt.')
21
+ expect(wrapper.text()).toContain('2026')
22
+ })
23
+
24
+ it('renders month and year as buttons if monthYearPicker is true', () => {
25
+ const wrapper = mount(CalendarHeader, {
26
+ props: {
27
+ viewDate: VIEW_DATE,
28
+ pickerView: 'days',
29
+ monthYearPicker: true,
30
+ locale: nl,
31
+ canGoPrev: true,
32
+ canGoNext: true,
33
+ },
34
+ })
35
+ const buttons = wrapper.findAll('.flex.items-center.gap-1 button')
36
+ expect(buttons.length).toBe(2)
37
+ })
38
+
39
+ it('emits openMonthPicker when month label button is clicked', async () => {
40
+ const wrapper = mount(CalendarHeader, {
41
+ props: {
42
+ viewDate: VIEW_DATE,
43
+ pickerView: 'days',
44
+ monthYearPicker: true,
45
+ locale: nl,
46
+ canGoPrev: true,
47
+ canGoNext: true,
48
+ },
49
+ })
50
+ await wrapper.findAll('.flex.items-center.gap-1 button')[0].trigger('click')
51
+ expect(wrapper.emitted('openMonthPicker')).toBeTruthy()
52
+ })
53
+
54
+ it('emits prev and next events', async () => {
55
+ const wrapper = mount(CalendarHeader, {
56
+ props: {
57
+ viewDate: VIEW_DATE,
58
+ pickerView: 'days',
59
+ monthYearPicker: false,
60
+ locale: nl,
61
+ canGoPrev: true,
62
+ canGoNext: true,
63
+ },
64
+ })
65
+ const navButtons = wrapper.findAll('button[aria-label]')
66
+ await navButtons[0].trigger('click')
67
+ expect(wrapper.emitted('prev')).toBeTruthy()
68
+ await navButtons[1].trigger('click')
69
+ expect(wrapper.emitted('next')).toBeTruthy()
70
+ })
71
+
72
+ it('shows back arrow instead of chevron when in years view', () => {
73
+ const wrapper = mount(CalendarHeader, {
74
+ props: {
75
+ viewDate: VIEW_DATE,
76
+ pickerView: 'years',
77
+ monthYearPicker: true,
78
+ locale: nl,
79
+ canGoPrev: false,
80
+ canGoNext: false,
81
+ },
82
+ })
83
+ const backButton = wrapper.find('button[aria-label="Terug naar maanden"]')
84
+ expect(backButton.exists()).toBe(true)
85
+ })
86
+ })
@@ -0,0 +1,52 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import CalendarMonthView from './_CalendarMonthView.vue'
4
+ import { nl } from 'date-fns/locale'
5
+
6
+ const VIEW_YEAR = 2026
7
+ const CURRENT_MONTH = 2 // March
8
+
9
+ const mountMonthView = (props: Record<string, unknown>) =>
10
+ mount(CalendarMonthView, {
11
+ props: {
12
+ viewYear: VIEW_YEAR,
13
+ modelValue: null,
14
+ currentMonth: CURRENT_MONTH,
15
+ locale: nl,
16
+ timezone: 'UTC',
17
+ ...(props as Record<string, unknown>),
18
+ },
19
+ })
20
+
21
+ describe('CalendarMonthView', () => {
22
+ it('renders all 12 months', () => {
23
+ const wrapper = mountMonthView({})
24
+ const buttons = wrapper.findAll('button')
25
+ expect(buttons.length).toBe(12)
26
+ })
27
+
28
+ it('highlights the selected month', () => {
29
+ const selectedDate = new Date(Date.UTC(VIEW_YEAR, 5, 15)) // June
30
+ const wrapper = mountMonthView({ modelValue: selectedDate })
31
+ const selectedButton = wrapper.find('button[aria-selected="true"]')
32
+ expect(selectedButton.text()).toBe('Jun')
33
+ })
34
+
35
+ it('disables months based on minDate and maxDate', () => {
36
+ const minDate = new Date(Date.UTC(VIEW_YEAR, 2, 1)) // March 1st
37
+ const wrapper = mountMonthView({ minDate })
38
+ // January (0) and February (1) should be disabled
39
+ const disabledButtons = wrapper.findAll('button[disabled]')
40
+ expect(disabledButtons.length).toBe(2)
41
+ expect(disabledButtons[0]?.text()).toBe('Jan')
42
+ expect(disabledButtons[1]?.text()).toBe('Feb')
43
+ })
44
+
45
+ it('emits select event when a month is clicked', async () => {
46
+ const wrapper = mountMonthView({})
47
+ await wrapper.findAll('button')[5]?.trigger('click')
48
+ const emitted = wrapper.emitted('select')
49
+ expect(emitted).toBeTruthy()
50
+ expect(emitted?.[0]?.[0]).toBe(5)
51
+ })
52
+ })
@@ -0,0 +1,89 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import { getHours, getMinutes } from 'date-fns'
4
+ import TimePicker from './_CalendarTimePicker.vue'
5
+
6
+ describe('CalendarTimePicker', () => {
7
+ it('renders correctly with modelValue', () => {
8
+ const date = new Date(2024, 0, 1, 10, 30)
9
+ const wrapper = mount(TimePicker, {
10
+ props: {
11
+ modelValue: date,
12
+ },
13
+ })
14
+
15
+ const displays = wrapper.findAll('.tabular-nums')
16
+ expect(displays[0].text()).toBe('10')
17
+ expect(displays[1].text()).toBe('30')
18
+ })
19
+
20
+ it('updates hours when buttons are clicked', async () => {
21
+ const date = new Date(2024, 0, 1, 10, 30)
22
+ const wrapper = mount(TimePicker, {
23
+ props: {
24
+ modelValue: date,
25
+ },
26
+ })
27
+
28
+ const hourButtons = wrapper.findAll('button')
29
+ // First button is increment hours
30
+ await hourButtons[0].trigger('click')
31
+
32
+ const emitted = wrapper.emitted('update:modelValue')
33
+ expect(emitted).toBeTruthy()
34
+ const updatedDate = emitted![0][0] as Date
35
+ expect(getHours(updatedDate)).toBe(11)
36
+ expect(getMinutes(updatedDate)).toBe(30)
37
+ })
38
+
39
+ it('updates minutes when buttons are clicked', async () => {
40
+ const date = new Date(2024, 0, 1, 10, 30)
41
+ const wrapper = mount(TimePicker, {
42
+ props: {
43
+ modelValue: date,
44
+ },
45
+ })
46
+
47
+ const minuteButtons = wrapper.findAll('button')
48
+ // Third button is increment minutes (0: inc hours, 1: dec hours, 2: inc minutes, 3: dec minutes)
49
+ await minuteButtons[2].trigger('click')
50
+
51
+ const emitted = wrapper.emitted('update:modelValue')
52
+ expect(emitted).toBeTruthy()
53
+ const updatedDate = emitted![0][0] as Date
54
+ expect(getHours(updatedDate)).toBe(10)
55
+ expect(getMinutes(updatedDate)).toBe(31)
56
+ })
57
+
58
+ it('handles null modelValue by using current time as default', () => {
59
+ const now = new Date()
60
+ const wrapper = mount(TimePicker, {
61
+ props: {
62
+ modelValue: null,
63
+ },
64
+ })
65
+
66
+ const displays = wrapper.findAll('.tabular-nums')
67
+ expect(displays[0].text()).toBe(now.getHours().toString().padStart(2, '0'))
68
+ expect(displays[1].text()).toBe(now.getMinutes().toString().padStart(2, '0'))
69
+ })
70
+
71
+ it('emits current date and time when buttons clicked with null modelValue', async () => {
72
+ const wrapper = mount(TimePicker, {
73
+ props: {
74
+ modelValue: null,
75
+ },
76
+ })
77
+
78
+ const hourButtons = wrapper.findAll('button')
79
+ await hourButtons[0].trigger('click') // increment hours
80
+
81
+ const emitted = wrapper.emitted('update:modelValue')
82
+ expect(emitted).toBeTruthy()
83
+ const updatedDate = emitted![0][0] as Date
84
+ expect(updatedDate).toBeInstanceOf(Date)
85
+ // Check that it roughly matches current time + 1 hour
86
+ const expectedHour = (new Date().getHours() + 1) % 24
87
+ expect(getHours(updatedDate)).toBe(expectedHour)
88
+ })
89
+ })
@@ -0,0 +1,56 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import CalendarYearView from './_CalendarYearView.vue'
4
+
5
+ describe('CalendarYearView', () => {
6
+ it('renders a range of years', () => {
7
+ const wrapper = mount(CalendarYearView, {
8
+ props: {
9
+ viewYear: 2024,
10
+ timezone: 'UTC',
11
+ },
12
+ })
13
+ const items = wrapper.findAll('button')
14
+ expect(items.length).toBeGreaterThan(100) // Default range is viewYear - 100 to + 20
15
+ })
16
+
17
+ it('respects minDate and maxDate', () => {
18
+ const wrapper = mount(CalendarYearView, {
19
+ props: {
20
+ viewYear: 2024,
21
+ minDate: new Date(Date.UTC(2020, 0, 1)),
22
+ maxDate: new Date(Date.UTC(2025, 0, 1)),
23
+ timezone: 'UTC',
24
+ },
25
+ })
26
+ const items = wrapper.findAll('button')
27
+ // 2020 to 2025 inclusive is 6 years
28
+ expect(items.length).toBe(6)
29
+ expect(items[0]?.text()).toBe('2020')
30
+ expect(items[5]?.text()).toBe('2025')
31
+ })
32
+
33
+ it('emits select when a year is clicked', async () => {
34
+ const wrapper = mount(CalendarYearView, {
35
+ props: {
36
+ viewYear: 2024,
37
+ timezone: 'UTC',
38
+ },
39
+ })
40
+ const year2024 = wrapper.findAll('button').find((i) => i.text() === '2024')
41
+ await year2024?.trigger('click')
42
+ expect(wrapper.emitted('select')).toBeTruthy()
43
+ expect(wrapper.emitted('select')?.[0]?.[0]).toBe(2024)
44
+ })
45
+
46
+ it('highlights the current year', async () => {
47
+ const wrapper = mount(CalendarYearView, {
48
+ props: {
49
+ viewYear: 2024,
50
+ timezone: 'UTC',
51
+ },
52
+ })
53
+ const activeYear = wrapper.find('button.text-cornflower-blue-500')
54
+ expect(activeYear.text()).toBe('2024')
55
+ })
56
+ })
@@ -0,0 +1,229 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import { expect, userEvent, waitFor, within } from 'storybook/test'
3
+ import { ref } from 'vue'
4
+ import type { ColorPickerGradient, ColorPickerFillMode } from './ColorPicker.vue'
5
+ import NexxtColorPicker from './ColorPicker.vue'
6
+
7
+ export default {
8
+ title: 'Components/Organisms/Color picker',
9
+ component: NexxtColorPicker,
10
+ } satisfies Meta<typeof NexxtColorPicker>
11
+
12
+ type Story = StoryObj<typeof NexxtColorPicker>
13
+
14
+ const render = (args: Story['args']) => ({
15
+ components: { NexxtColorPicker },
16
+ setup() {
17
+ return { args }
18
+ },
19
+ template: `
20
+ <NexxtColorPicker
21
+ v-bind="args"
22
+ :modelValue="args.modelValue"
23
+ @update:modelValue="args.modelValue = $event"
24
+ />
25
+ `,
26
+ })
27
+
28
+ export const Default: Story = {
29
+ render,
30
+ args: {
31
+ label: '',
32
+ placeholder: 'Kies kleur...',
33
+ modelValue: '',
34
+ error: false,
35
+ errorMessage: '',
36
+ required: false,
37
+ showAlpha: false,
38
+ formats: ['hex'],
39
+ },
40
+ }
41
+
42
+ export const WithLabel: Story = {
43
+ render,
44
+ args: {
45
+ ...Default.args,
46
+ label: 'Merkkleur',
47
+ },
48
+ }
49
+
50
+ export const WithValue: Story = {
51
+ render,
52
+ args: {
53
+ ...Default.args,
54
+ label: 'Achtergrondkleur',
55
+ modelValue: '#6366f1',
56
+ },
57
+ }
58
+ /** A value with a name of its own shows that name in the trigger instead of the colour. */
59
+ export const WithValueLabel: Story = {
60
+ render,
61
+ args: {
62
+ ...Default.args,
63
+ label: 'Achtergrondkleur',
64
+ modelValue: '#1777ea',
65
+ valueLabel: 'Hoofdkleur',
66
+ },
67
+ }
68
+ export const WithSuggestions: Story = {
69
+ render,
70
+ args: {
71
+ ...Default.args,
72
+ label: 'Achtergrondkleur',
73
+ suggestions: { hex: ['#1777ea', '#f0f4f8', '#1f2937'] },
74
+ },
75
+ }
76
+
77
+ export const Error: Story = {
78
+ render,
79
+ args: {
80
+ ...Default.args,
81
+ label: 'Kleur',
82
+ error: true,
83
+ errorMessage: 'Kies een kleur om door te gaan',
84
+ },
85
+ }
86
+
87
+ export const Required: Story = {
88
+ render,
89
+ args: {
90
+ ...Default.args,
91
+ label: 'Primaire kleur',
92
+ required: true,
93
+ },
94
+ }
95
+
96
+ /**
97
+ * The opacity slider is named by its visible label. The play runs in Chromium,
98
+ * where the name is computed from the rendered label rather than read off an
99
+ * attribute.
100
+ */
101
+ export const WithAlpha: Story = {
102
+ render,
103
+ args: {
104
+ ...Default.args,
105
+ label: 'Overlay kleur',
106
+ modelValue: '#3b82f680',
107
+ showAlpha: true,
108
+ },
109
+ play: async ({ canvasElement, args }) => {
110
+ const canvas = within(canvasElement)
111
+
112
+ await userEvent.click(canvas.getByRole('button', { name: 'Overlay kleur' }))
113
+
114
+ // The name is matched exactly: the label alone, once, and not reka's "Alpha".
115
+ const slider = await canvas.findByRole('slider', { name: 'Dekking' })
116
+ expect(slider).not.toHaveAttribute('aria-label')
117
+
118
+ // `#…80` is 50.196% to reka; the slider announces it whole, the colour stays.
119
+ expect(slider).toHaveAttribute('aria-valuenow', '50')
120
+ expect(slider).toHaveAttribute('aria-valuetext', '50%')
121
+ expect(args.modelValue).toBe('#3b82f680')
122
+ },
123
+ }
124
+
125
+ export const HslFormat: Story = {
126
+ render,
127
+ args: {
128
+ ...Default.args,
129
+ label: 'HSL kleur',
130
+ modelValue: '#6366f1',
131
+ formats: ['hsl'],
132
+ },
133
+ }
134
+
135
+ export const RgbFormat: Story = {
136
+ render,
137
+ args: {
138
+ ...Default.args,
139
+ label: 'RGB kleur',
140
+ modelValue: '#6366f1',
141
+ formats: ['rgb'],
142
+ },
143
+ }
144
+
145
+ export const AllFormats: Story = {
146
+ render,
147
+ args: {
148
+ ...Default.args,
149
+ label: 'Alle formaten',
150
+ formats: ['hex', 'hsl', 'rgb'],
151
+ },
152
+ }
153
+
154
+ export const FullWidth: Story = {
155
+ render,
156
+ args: {
157
+ ...Default.args,
158
+ label: 'Volledige breedte',
159
+ modelValue: '#6366f1',
160
+ fullWidth: true,
161
+ },
162
+ }
163
+
164
+ export const MatchWidth: Story = {
165
+ render,
166
+ args: {
167
+ ...Default.args,
168
+ label: 'Panel op triggerbreedte',
169
+ modelValue: '#6366f1',
170
+ fullWidth: true,
171
+ matchWidth: true,
172
+ },
173
+ }
174
+
175
+ /**
176
+ * The gradient mode on its own: the picker edits plain data (type, angle,
177
+ * centre, stops) and knows no class names; NexxtEditorColor turns it into
178
+ * Tailwind classes. Rendered at the editor column's width.
179
+ */
180
+ export const Gradient: Story = {
181
+ args: { modes: ['solid', 'gradient'], suggestions: { hex: ['#7f56d9', '#432e73'] } },
182
+ render: (args) => ({
183
+ components: { NexxtColorPicker },
184
+ setup() {
185
+ const mode = ref<ColorPickerFillMode>('gradient')
186
+ const gradient = ref<ColorPickerGradient>({
187
+ type: 'linear',
188
+ angle: 135,
189
+ centerX: 50,
190
+ centerY: 50,
191
+ stops: [
192
+ { color: '#7f56d9', position: 0 },
193
+ { color: '#432e73cc', position: 100 },
194
+ ],
195
+ })
196
+ return { args, mode, gradient }
197
+ },
198
+ template: `
199
+ <div class="w-64">
200
+ <NexxtColorPicker
201
+ v-bind="args"
202
+ v-model:mode="mode"
203
+ v-model:gradient-value="gradient"
204
+ full-width
205
+ match-width
206
+ />
207
+ </div>
208
+ `,
209
+ }),
210
+ play: async ({ canvasElement }) => {
211
+ const canvas = within(canvasElement)
212
+ const trigger = canvas.getByRole('button', { name: /Kies kleur/ })
213
+ expect(trigger).toHaveTextContent('Lineair verloop')
214
+ await userEvent.click(trigger)
215
+ await waitFor(() => expect(canvasElement.querySelector('[data-gradient-bar]')).not.toBeNull())
216
+
217
+ const panel = canvasElement
218
+ .querySelector('[data-gradient-bar]')!
219
+ .closest<HTMLElement>('div[style*="position: fixed"]')!
220
+ expect(within(panel).getByRole('slider', { name: 'Hoek' })).toHaveAttribute(
221
+ 'aria-valuetext',
222
+ '135 graden',
223
+ )
224
+ expect(within(panel).getAllByRole('slider', { name: /^Kleurstop \d van 2$/ })).toHaveLength(2)
225
+ const surface = panel.querySelector<HTMLElement>('.rounded-xl')!
226
+ expect(surface.offsetWidth).toBe(trigger.offsetWidth)
227
+ expect(surface.scrollHeight).toBeLessThanOrEqual(surface.clientHeight)
228
+ },
229
+ }