@nexxtmove/ui 2.6.0 → 2.7.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 (223) hide show
  1. package/dist/index.js +49 -126
  2. package/dist/nuxt.js +0 -1
  3. package/package.json +38 -36
  4. package/src/attribute-contract.test.ts +151 -0
  5. package/src/components/atoms/AnimatedNumber/AnimatedNumber.stories.ts +36 -0
  6. package/src/components/atoms/AnimatedNumber/AnimatedNumber.test.ts +21 -0
  7. package/src/components/atoms/Avatar/Avatar.stories.ts +122 -0
  8. package/src/components/atoms/Avatar/Avatar.test.ts +134 -0
  9. package/src/components/atoms/Button/Button.stories.ts +290 -0
  10. package/src/components/atoms/Button/Button.test.ts +406 -0
  11. package/src/components/atoms/Card/Card.stories.ts +45 -0
  12. package/src/components/atoms/Card/Card.test.ts +46 -0
  13. package/src/components/atoms/Checkbox/Checkbox.stories.ts +35 -0
  14. package/src/components/atoms/Checkbox/Checkbox.test.ts +55 -0
  15. package/src/components/atoms/Chip/Chip.stories.ts +274 -0
  16. package/src/components/atoms/Chip/Chip.test.ts +230 -0
  17. package/src/components/atoms/EnergyLabel/EnergyLabel.stories.ts +189 -0
  18. package/src/components/atoms/EnergyLabel/EnergyLabel.test.ts +100 -0
  19. package/src/components/atoms/FloatingPanel/FloatingPanel.stories.ts +677 -0
  20. package/src/components/atoms/FloatingPanel/FloatingPanel.test.ts +864 -0
  21. package/src/components/atoms/Icon/Icon.stories.ts +63 -0
  22. package/src/components/atoms/Icon/Icon.test.ts +89 -0
  23. package/src/components/atoms/Loader/Loader.stories.ts +36 -0
  24. package/src/components/atoms/Loader/Loader.test.ts +40 -0
  25. package/src/components/atoms/PickerItem/PickerItem.stories.ts +172 -0
  26. package/src/components/atoms/PickerItem/PickerItem.test.ts +142 -0
  27. package/src/components/atoms/PickerPreview/PickerPreview.stories.ts +183 -0
  28. package/src/components/atoms/PickerPreview/PickerPreview.test.ts +137 -0
  29. package/src/components/atoms/PresenceIndicator/PresenceIndicator.stories.ts +21 -0
  30. package/src/components/atoms/PropertyListing/PropertyListing.stories.ts +38 -0
  31. package/src/components/atoms/PropertyListing/PropertyListing.test.ts +46 -0
  32. package/src/components/atoms/SelectableOption/SelectableOption.stories.ts +131 -0
  33. package/src/components/atoms/SelectableOption/SelectableOption.test.ts +96 -0
  34. package/src/components/atoms/Skeleton/Skeleton.stories.ts +27 -0
  35. package/src/components/atoms/Skeleton/Skeleton.test.ts +28 -0
  36. package/src/components/atoms/Slider/Slider.stories.ts +151 -0
  37. package/src/components/atoms/Slider/Slider.test.ts +191 -0
  38. package/src/components/atoms/Squircle/Squircle.stories.ts +114 -0
  39. package/src/components/atoms/Squircle/Squircle.test.ts +70 -0
  40. package/src/components/atoms/Switch/Switch.stories.ts +91 -0
  41. package/src/components/atoms/Switch/Switch.test.ts +183 -0
  42. package/src/components/atoms/Tooltip/Tooltip.stories.ts +158 -0
  43. package/src/components/atoms/Tooltip/Tooltip.test.ts +134 -0
  44. package/src/components/editor/atoms/EditorDimensionField/EditorDimensionField.stories.ts +124 -0
  45. package/src/components/editor/atoms/EditorDimensionField/EditorDimensionField.test.ts +464 -0
  46. package/src/components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.stories.ts +266 -0
  47. package/src/components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.test.ts +421 -0
  48. package/src/components/editor/atoms/EditorSettingsGroup/EditorSettingsGroup.stories.ts +365 -0
  49. package/src/components/editor/atoms/EditorSettingsGroup/EditorSettingsGroup.test.ts +484 -0
  50. package/src/components/editor/composables/useRadioGrid.test.ts +261 -0
  51. package/src/components/editor/molecules/EditorAlignItems/EditorAlignItems.stories.ts +73 -0
  52. package/src/components/editor/molecules/EditorAlignItems/EditorAlignItems.test.ts +189 -0
  53. package/src/components/editor/molecules/EditorAlignmentGrid/EditorAlignmentGrid.stories.ts +309 -0
  54. package/src/components/editor/molecules/EditorAlignmentGrid/EditorAlignmentGrid.test.ts +359 -0
  55. package/src/components/editor/molecules/EditorAnchor/EditorAnchor.stories.ts +138 -0
  56. package/src/components/editor/molecules/EditorAnchor/EditorAnchor.test.ts +344 -0
  57. package/src/components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.stories.ts +87 -0
  58. package/src/components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.test.ts +262 -0
  59. package/src/components/editor/molecules/EditorBlur/EditorBlur.stories.ts +115 -0
  60. package/src/components/editor/molecules/EditorBlur/EditorBlur.test.ts +243 -0
  61. package/src/components/editor/molecules/EditorBorder/EditorBorder.stories.ts +187 -0
  62. package/src/components/editor/molecules/EditorBorder/EditorBorder.test.ts +456 -0
  63. package/src/components/editor/molecules/EditorColor/EditorColor.stories.ts +563 -0
  64. package/src/components/editor/molecules/EditorColor/EditorColor.test.ts +689 -0
  65. package/src/components/editor/molecules/EditorColor/EditorColorGradient.test.ts +793 -0
  66. package/src/components/editor/molecules/EditorColor/gradientClass.test.ts +303 -0
  67. package/src/components/editor/molecules/EditorDashPattern/EditorDashPattern.stories.ts +72 -0
  68. package/src/components/editor/molecules/EditorDashPattern/EditorDashPattern.test.ts +124 -0
  69. package/src/components/editor/molecules/EditorFlexDirection/EditorFlexDirection.stories.ts +46 -0
  70. package/src/components/editor/molecules/EditorFlexDirection/EditorFlexDirection.test.ts +97 -0
  71. package/src/components/editor/molecules/EditorFontSize/EditorFontSize.stories.ts +248 -0
  72. package/src/components/editor/molecules/EditorFontSize/EditorFontSize.test.ts +288 -0
  73. package/src/components/editor/molecules/EditorGap/EditorGap.stories.ts +129 -0
  74. package/src/components/editor/molecules/EditorGap/EditorGap.test.ts +253 -0
  75. package/src/components/editor/molecules/EditorHeadingType/EditorHeadingType.stories.ts +68 -0
  76. package/src/components/editor/molecules/EditorHeadingType/EditorHeadingType.test.ts +150 -0
  77. package/src/components/editor/molecules/EditorJustifyContent/EditorJustifyContent.stories.ts +71 -0
  78. package/src/components/editor/molecules/EditorJustifyContent/EditorJustifyContent.test.ts +161 -0
  79. package/src/components/editor/molecules/EditorLetterSpacing/EditorLetterSpacing.stories.ts +204 -0
  80. package/src/components/editor/molecules/EditorLetterSpacing/EditorLetterSpacing.test.ts +238 -0
  81. package/src/components/editor/molecules/EditorLineHeight/EditorLineHeight.stories.ts +257 -0
  82. package/src/components/editor/molecules/EditorLineHeight/EditorLineHeight.test.ts +286 -0
  83. package/src/components/editor/molecules/EditorLineStyle/EditorLineStyle.stories.ts +135 -0
  84. package/src/components/editor/molecules/EditorLineStyle/EditorLineStyle.test.ts +230 -0
  85. package/src/components/editor/molecules/EditorMaxWidth/EditorMaxWidth.stories.ts +154 -0
  86. package/src/components/editor/molecules/EditorMaxWidth/EditorMaxWidth.test.ts +112 -0
  87. package/src/components/editor/molecules/EditorOverrideStatus/EditorOverrideStatus.stories.ts +206 -0
  88. package/src/components/editor/molecules/EditorOverrideStatus/EditorOverrideStatus.test.ts +332 -0
  89. package/src/components/editor/molecules/EditorPadding/EditorPadding.stories.ts +374 -0
  90. package/src/components/editor/molecules/EditorPadding/EditorPadding.test.ts +712 -0
  91. package/src/components/editor/molecules/EditorRadius/EditorRadius.stories.ts +64 -0
  92. package/src/components/editor/molecules/EditorRadius/EditorRadius.test.ts +137 -0
  93. package/src/components/editor/molecules/EditorShadow/EditorShadow.stories.ts +103 -0
  94. package/src/components/editor/molecules/EditorShadow/EditorShadow.test.ts +168 -0
  95. package/src/components/editor/molecules/EditorSize/EditorSize.stories.ts +84 -0
  96. package/src/components/editor/molecules/EditorSize/EditorSize.test.ts +253 -0
  97. package/src/components/editor/molecules/EditorSizeLimit/EditorSizeLimit.stories.ts +49 -0
  98. package/src/components/editor/molecules/EditorSizeLimit/EditorSizeLimit.test.ts +43 -0
  99. package/src/components/editor/molecules/EditorTextAlign/EditorTextAlign.stories.ts +69 -0
  100. package/src/components/editor/molecules/EditorTextAlign/EditorTextAlign.test.ts +155 -0
  101. package/src/components/molecules/Announcement/Announcement.stories.ts +224 -0
  102. package/src/components/molecules/Announcement/Announcement.test.ts +161 -0
  103. package/src/components/molecules/ContactInfoCard/ContactInfoCard.stories.ts +99 -0
  104. package/src/components/molecules/ContactInfoCard/ContactInfoCard.test.ts +70 -0
  105. package/src/components/molecules/CustomerJourneyTile/CustomerJourneyTile.stories.ts +148 -0
  106. package/src/components/molecules/CustomerJourneyTile/CustomerJourneyTile.test.ts +271 -0
  107. package/src/components/molecules/DoughnutChart/DoughnutChart.stories.ts +45 -0
  108. package/src/components/molecules/DoughnutChart/DoughnutChart.test.ts +205 -0
  109. package/src/components/molecules/InfoBlock/InfoBlock.stories.ts +84 -0
  110. package/src/components/molecules/InfoBlock/InfoBlock.test.ts +101 -0
  111. package/src/components/molecules/InputField/InputField.stories.ts +117 -0
  112. package/src/components/molecules/InputField/InputField.test.ts +138 -0
  113. package/src/components/molecules/InputSelect/InputSelect.stories.ts +116 -0
  114. package/src/components/molecules/InputSelect/InputSelect.test.ts +127 -0
  115. package/src/components/molecules/ModalFooter/ModalFooter.stories.ts +204 -0
  116. package/src/components/molecules/ModalFooter/ModalFooter.test.ts +276 -0
  117. package/src/components/molecules/PageCard/PageCard.stories.ts +100 -0
  118. package/src/components/molecules/PageCard/PageCard.test.ts +91 -0
  119. package/src/components/molecules/PageContainer/PageContainer.stories.ts +90 -0
  120. package/src/components/molecules/PageContainer/PageContainer.test.ts +50 -0
  121. package/src/components/molecules/PageMockup/PageMockup.stories.ts +38 -0
  122. package/src/components/molecules/PageMockup/PageMockup.test.ts +20 -0
  123. package/src/components/molecules/PageTypeTile/PageTypeTile.stories.ts +63 -0
  124. package/src/components/molecules/PageTypeTile/PageTypeTile.test.ts +41 -0
  125. package/src/components/molecules/Pagination/Pagination.stories.ts +72 -0
  126. package/src/components/molecules/Pagination/Pagination.test.ts +130 -0
  127. package/src/components/molecules/PickerGroup/PickerGroup.stories.ts +90 -0
  128. package/src/components/molecules/PickerGroup/PickerGroup.test.ts +145 -0
  129. package/src/components/molecules/PickerList/PickerList.stories.ts +436 -0
  130. package/src/components/molecules/PickerList/PickerList.test.ts +870 -0
  131. package/src/components/molecules/PlaceholderInput/PlaceholderInput.stories.ts +48 -0
  132. package/src/components/molecules/PlaceholderInput/PlaceholderInput.test.ts +107 -0
  133. package/src/components/molecules/PropertyCard/PropertyCard.stories.ts +46 -0
  134. package/src/components/molecules/PropertyCard/PropertyCard.test.ts +59 -0
  135. package/src/components/molecules/Select/Select.stories.ts +264 -0
  136. package/src/components/molecules/Select/Select.test.ts +315 -0
  137. package/src/components/molecules/SidebarMenuItem/SidebarMenuItem.stories.ts +84 -0
  138. package/src/components/molecules/SidebarMenuItem/SidebarMenuItem.test.ts +103 -0
  139. package/src/components/molecules/SkeletonPropertyCard/SkeletonPropertyCard.stories.ts +11 -0
  140. package/src/components/molecules/SkeletonPropertyCard/SkeletonPropertyCard.test.ts +38 -0
  141. package/src/components/molecules/SocialIcons/SocialIcons.stories.ts +28 -0
  142. package/src/components/molecules/SocialIcons/SocialIcons.test.ts +58 -0
  143. package/src/components/molecules/SocialMediaCustomTemplate/SocialMediaCustomTemplate.stories.ts +11 -0
  144. package/src/components/molecules/SocialMediaCustomTemplate/SocialMediaCustomTemplate.test.ts +116 -0
  145. package/src/components/molecules/SocialMediaType/SocialMediaType.stories.ts +37 -0
  146. package/src/components/molecules/SocialMediaType/SocialMediaType.test.ts +126 -0
  147. package/src/components/molecules/Source/Source.stories.ts +35 -0
  148. package/src/components/molecules/Source/Source.test.ts +45 -0
  149. package/src/components/molecules/TabbedContent/TabbedContent.stories.ts +96 -0
  150. package/src/components/molecules/TabbedContent/TabbedContent.test.ts +109 -0
  151. package/src/components/molecules/Tabs/Tabs.stories.ts +230 -0
  152. package/src/components/molecules/Tabs/Tabs.test.ts +280 -0
  153. package/src/components/molecules/TemplateCard/TemplateCard.stories.ts +327 -0
  154. package/src/components/molecules/TemplateCard/TemplateCard.test.ts +339 -0
  155. package/src/components/molecules/Textarea/Textarea.stories.ts +216 -0
  156. package/src/components/molecules/Textarea/Textarea.test.ts +191 -0
  157. package/src/components/molecules/TimelineEvent/TimelineEvent.stories.ts +319 -0
  158. package/src/components/molecules/TimelineEvent/TimelineEvent.test.ts +224 -0
  159. package/src/components/molecules/TimelineLanes/TimelineLanes.stories.ts +29 -0
  160. package/src/components/molecules/TimelineLanes/TimelineLanes.test.ts +30 -0
  161. package/src/components/molecules/Toast/Toast.stories.ts +110 -0
  162. package/src/components/molecules/Toast/Toast.test.ts +103 -0
  163. package/src/components/molecules/ToggleGroup/ToggleGroup.stories.ts +297 -0
  164. package/src/components/molecules/ToggleGroup/ToggleGroup.test.ts +578 -0
  165. package/src/components/molecules/Tree/Tree.stories.ts +884 -0
  166. package/src/components/molecules/Tree/Tree.test.ts +1247 -0
  167. package/src/components/molecules/Tree/tree-drop.test.ts +223 -0
  168. package/src/components/organisms/Calendar/Calendar.stories.ts +108 -0
  169. package/src/components/organisms/Calendar/Calendar.test.ts +279 -0
  170. package/src/components/organisms/Calendar/_CalendarDayView.test.ts +104 -0
  171. package/src/components/organisms/Calendar/_CalendarHeader.test.ts +86 -0
  172. package/src/components/organisms/Calendar/_CalendarMonthView.test.ts +52 -0
  173. package/src/components/organisms/Calendar/_CalendarTimePicker.test.ts +89 -0
  174. package/src/components/organisms/Calendar/_CalendarYearView.test.ts +56 -0
  175. package/src/components/organisms/ColorPicker/ColorPicker.stories.ts +229 -0
  176. package/src/components/organisms/ColorPicker/ColorPicker.test.ts +638 -0
  177. package/src/components/organisms/ColorPicker/gradient.test.ts +67 -0
  178. package/src/components/organisms/ColorPicker/gradientGeometry.test.ts +206 -0
  179. package/src/components/organisms/Combobox/Combobox.stories.ts +234 -0
  180. package/src/components/organisms/Combobox/Combobox.test.ts +512 -0
  181. package/src/components/organisms/Combobox/Combobox.vue +1 -1
  182. package/src/components/organisms/ComboboxPanel/ComboboxPanel.stories.ts +158 -0
  183. package/src/components/organisms/ComboboxPanel/ComboboxPanel.test.ts +524 -0
  184. package/src/components/organisms/ComboboxPanel/ComboboxPanel.vue +1 -1
  185. package/src/components/organisms/CountrySelect/CountrySelect.stories.ts +139 -0
  186. package/src/components/organisms/CountrySelect/CountrySelect.test.ts +149 -0
  187. package/src/components/organisms/DatePicker/DatePicker.stories.ts +163 -0
  188. package/src/components/organisms/DatePicker/DatePicker.test.ts +214 -0
  189. package/src/components/organisms/DropdownButton/DropdownButton.stories.ts +123 -0
  190. package/src/components/organisms/DropdownButton/DropdownButton.test.ts +379 -0
  191. package/src/components/organisms/FontPicker/FontPicker.stories.ts +476 -0
  192. package/src/components/organisms/FontPicker/FontPicker.test.ts +873 -0
  193. package/src/components/organisms/Header/Header.stories.ts +75 -0
  194. package/src/components/organisms/Header/Header.test.ts +233 -0
  195. package/src/components/organisms/IconPicker/IconPicker.stories.ts +734 -0
  196. package/src/components/organisms/IconPicker/IconPicker.test.ts +669 -0
  197. package/src/components/organisms/IconPicker/_IconPickerGrid.test.ts +184 -0
  198. package/src/components/organisms/IconPicker/_IconPickerRail.test.ts +102 -0
  199. package/src/components/organisms/IconPicker/useIconPickerNavigation.test.ts +215 -0
  200. package/src/components/organisms/Modal/Modal.stories.ts +350 -0
  201. package/src/components/organisms/Modal/Modal.test.ts +275 -0
  202. package/src/components/organisms/PickerPanel/PickerPanel.stories.ts +452 -0
  203. package/src/components/organisms/PickerPanel/PickerPanel.test.ts +358 -0
  204. package/src/components/organisms/ProgressBar/ProgressBar.stories.ts +100 -0
  205. package/src/components/organisms/ProgressBar/ProgressBar.test.ts +417 -0
  206. package/src/components/organisms/ReferenceCard/ReferenceCard.stories.ts +191 -0
  207. package/src/components/organisms/ReferenceCard/ReferenceCard.test.ts +165 -0
  208. package/src/components/organisms/SocialMediaTemplate/SocialMediaTemplate.stories.ts +65 -0
  209. package/src/components/organisms/SocialMediaTemplate/SocialMediaTemplate.test.ts +466 -0
  210. package/src/components/organisms/StepperHeader/StepperHeader.stories.ts +91 -0
  211. package/src/components/organisms/StepperHeader/StepperHeader.test.ts +279 -0
  212. package/src/components/organisms/Table/Table.stories.ts +332 -0
  213. package/src/components/organisms/Table/Table.test.ts +320 -0
  214. package/src/components/organisms/TimelinePhaseblock/TimelinePhaseblock.stories.ts +198 -0
  215. package/src/components/organisms/TimelinePhaseblock/TimelinePhaseblock.test.ts +283 -0
  216. package/src/components/organisms/TimelineTrack/TimelineTrack.stories.ts +120 -0
  217. package/src/components/organisms/TimelineTrack/TimelineTrack.test.ts +91 -0
  218. package/src/components/organisms/Toaster/Toaster.stories.ts +51 -0
  219. package/src/components/organisms/Toaster/Toaster.test.ts +75 -0
  220. package/src/components/organisms/VideoControls/VideoControls.stories.ts +792 -0
  221. package/src/components/organisms/VideoControls/VideoControls.test.ts +1056 -0
  222. package/src/components/organisms/VideoPlayer/VideoPlayer.stories.ts +327 -0
  223. package/src/components/organisms/VideoPlayer/VideoPlayer.test.ts +439 -0
@@ -0,0 +1,183 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import Switch from './Switch.vue'
4
+
5
+ describe('Switch', () => {
6
+ it('renders a checkbox input', () => {
7
+ const wrapper = mount(Switch)
8
+
9
+ expect(wrapper.find('input[type="checkbox"]').exists()).toBe(true)
10
+ })
11
+
12
+ // A toggle is on/off, not selected/unselected, so it is exposed as a switch.
13
+ // No `aria-checked` rides along: on a native checkbox the browser derives the
14
+ // state, and a hand-written one would be the thing that goes stale.
15
+ it('exposes the input as a switch', () => {
16
+ const wrapper = mount(Switch)
17
+ const input = wrapper.find('input[type="checkbox"]')
18
+
19
+ expect(input.attributes('role')).toBe('switch')
20
+ expect(input.attributes('aria-checked')).toBeUndefined()
21
+ })
22
+
23
+ it('is unchecked by default', () => {
24
+ const wrapper = mount(Switch)
25
+
26
+ expect((wrapper.find('input').element as HTMLInputElement).checked).toBe(false)
27
+ })
28
+
29
+ it('is checked when modelValue is true', () => {
30
+ const wrapper = mount(Switch, { props: { modelValue: true } })
31
+
32
+ expect((wrapper.find('input').element as HTMLInputElement).checked).toBe(true)
33
+ })
34
+
35
+ it('emits update:modelValue when toggled', async () => {
36
+ const wrapper = mount(Switch, { props: { modelValue: false } })
37
+
38
+ await wrapper.find('input').setValue(true)
39
+
40
+ expect(wrapper.emitted('update:modelValue')).toBeTruthy()
41
+ })
42
+
43
+ it('applies blue track color when checked by default', () => {
44
+ const wrapper = mount(Switch, { props: { modelValue: true } })
45
+
46
+ expect(wrapper.find('span.absolute.inset-0').classes()).toContain('bg-cornflower-blue-500')
47
+ })
48
+
49
+ it('applies green track color when checked and color is green', () => {
50
+ const wrapper = mount(Switch, { props: { modelValue: true, color: 'green' } })
51
+
52
+ expect(wrapper.find('span.absolute.inset-0').classes()).toContain('bg-green-400')
53
+ })
54
+
55
+ it('applies gray track color when unchecked', () => {
56
+ const wrapper = mount(Switch, { props: { modelValue: false } })
57
+
58
+ expect(wrapper.find('span.absolute.inset-0').classes()).toContain('bg-gray-200')
59
+ })
60
+
61
+ it('applies the large size by default', () => {
62
+ const wrapper = mount(Switch)
63
+
64
+ expect(wrapper.find('span.relative').classes()).toEqual(expect.arrayContaining(['h-8', 'w-15']))
65
+ })
66
+
67
+ it('applies the medium size classes', () => {
68
+ const wrapper = mount(Switch, { props: { size: 'md' } })
69
+
70
+ expect(wrapper.find('span.relative').classes()).toEqual(expect.arrayContaining(['h-6', 'w-11']))
71
+ })
72
+
73
+ it('applies the small size classes', () => {
74
+ const wrapper = mount(Switch, { props: { size: 'sm' } })
75
+
76
+ expect(wrapper.find('span.relative').classes()).toEqual(expect.arrayContaining(['h-5', 'w-9']))
77
+ })
78
+
79
+ it('never lets the track shrink', () => {
80
+ const wrapper = mount(Switch)
81
+
82
+ expect(wrapper.find('span.relative').classes()).toContain('shrink-0')
83
+ })
84
+
85
+ it('renders a focus ring on the track when the input is focus-visible', () => {
86
+ const wrapper = mount(Switch)
87
+
88
+ expect(wrapper.find('span.absolute.inset-0').classes()).toEqual(
89
+ expect.arrayContaining([
90
+ 'peer-focus-visible:outline-2',
91
+ 'peer-focus-visible:outline-offset-2',
92
+ 'peer-focus-visible:outline-cornflower-blue-500',
93
+ ]),
94
+ )
95
+ })
96
+
97
+ it('keeps the input and the track as siblings so the peer selector resolves', () => {
98
+ const wrapper = mount(Switch)
99
+ const input = wrapper.find('input').element
100
+ const track = wrapper.find('span.absolute.inset-0').element
101
+
102
+ expect(track.parentElement).toBe(input.parentElement)
103
+ expect(input.nextElementSibling).toBe(track)
104
+ })
105
+
106
+ it('translates the knob by the size-specific distance when checked', () => {
107
+ const wrapper = mount(Switch, { props: { modelValue: true, size: 'sm' } })
108
+
109
+ expect(wrapper.find('span.rounded-full').classes()).toContain('translate-x-4')
110
+ })
111
+
112
+ it('does not translate the knob when unchecked', () => {
113
+ const wrapper = mount(Switch, { props: { modelValue: false, size: 'sm' } })
114
+
115
+ expect(wrapper.find('span.rounded-full').classes()).not.toContain('translate-x-4')
116
+ })
117
+
118
+ it('renders iconOn when checked', () => {
119
+ const wrapper = mount(Switch, { props: { modelValue: true, iconOn: 'check' } })
120
+
121
+ expect(wrapper.find('i').exists()).toBe(true)
122
+ })
123
+
124
+ it('renders iconOff when unchecked', () => {
125
+ const wrapper = mount(Switch, { props: { modelValue: false, iconOff: 'xmark' } })
126
+
127
+ expect(wrapper.find('i').exists()).toBe(true)
128
+ })
129
+
130
+ it('does not render iconOn when unchecked', () => {
131
+ const wrapper = mount(Switch, { props: { modelValue: false, iconOn: 'check' } })
132
+
133
+ expect(wrapper.find('i').exists()).toBe(false)
134
+ })
135
+
136
+ it('does not render iconOff when checked', () => {
137
+ const wrapper = mount(Switch, { props: { modelValue: true, iconOff: 'xmark' } })
138
+
139
+ expect(wrapper.find('i').exists()).toBe(false)
140
+ })
141
+
142
+ it('does not render an icon when neither iconOn nor iconOff is set', () => {
143
+ const wrapper = mount(Switch)
144
+
145
+ expect(wrapper.find('i').exists()).toBe(false)
146
+ })
147
+
148
+ it('renders slot content after the toggle by default', () => {
149
+ const wrapper = mount(Switch, {
150
+ props: { labelPosition: 'after' },
151
+ slots: { default: '<span>Enable notifications</span>' },
152
+ })
153
+ const label = wrapper.find('label')
154
+ const toggle = wrapper.find('span.relative')
155
+
156
+ expect(label.html()).toContain('Enable notifications')
157
+ expect(label.element.lastElementChild?.textContent).toContain('Enable notifications')
158
+ expect(toggle.element.compareDocumentPosition(label.element.lastElementChild!)).toBe(
159
+ Node.DOCUMENT_POSITION_FOLLOWING,
160
+ )
161
+ })
162
+
163
+ it('renders slot content before the toggle when labelPosition is before', () => {
164
+ const wrapper = mount(Switch, {
165
+ props: { labelPosition: 'before' },
166
+ slots: { default: '<span>Enable notifications</span>' },
167
+ })
168
+ const label = wrapper.find('label')
169
+ const toggle = wrapper.find('span.relative')
170
+
171
+ expect(label.html()).toContain('Enable notifications')
172
+ expect(label.element.firstElementChild?.textContent).toContain('Enable notifications')
173
+ expect(toggle.element.compareDocumentPosition(label.element.firstElementChild!)).toBe(
174
+ Node.DOCUMENT_POSITION_PRECEDING,
175
+ )
176
+ })
177
+
178
+ it('forwards attributes to the input', () => {
179
+ const wrapper = mount(Switch, { attrs: { 'aria-label': 'Enable notifications' } })
180
+
181
+ expect(wrapper.find('input').attributes('aria-label')).toBe('Enable notifications')
182
+ })
183
+ })
@@ -0,0 +1,158 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import Tooltip from './Tooltip.vue'
3
+
4
+ export default {
5
+ title: 'Components/Atoms/Tooltip',
6
+ component: Tooltip,
7
+ } satisfies Meta<typeof Tooltip>
8
+
9
+ type Story = StoryObj<typeof Tooltip>
10
+
11
+ const trigger = `<button class="rounded border border-gray-300 px-3 py-1 text-sm">Hover me</button>`
12
+
13
+ export const Default: Story = {
14
+ render: () => ({
15
+ components: { Tooltip },
16
+ template: `
17
+ <Tooltip>
18
+ ${trigger}
19
+ <template #tooltip>This is a tooltip</template>
20
+ </Tooltip>
21
+ `,
22
+ }),
23
+ }
24
+
25
+ export const WithDelay: Story = {
26
+ render: () => ({
27
+ components: { Tooltip },
28
+ template: `
29
+ <Tooltip :delay="500">
30
+ <button class="rounded border border-gray-300 px-3 py-1 text-sm">Hover me (500ms delay)</button>
31
+ <template #tooltip>Appears after a delay</template>
32
+ </Tooltip>
33
+ `,
34
+ }),
35
+ }
36
+
37
+ export const SideBottom: Story = {
38
+ render: () => ({
39
+ components: { Tooltip },
40
+ template: `
41
+ <Tooltip side="bottom">
42
+ ${trigger}
43
+ <template #tooltip>Tooltip below</template>
44
+ </Tooltip>
45
+ `,
46
+ }),
47
+ }
48
+
49
+ export const SideLeft: Story = {
50
+ render: () => ({
51
+ components: { Tooltip },
52
+ template: `
53
+ <Tooltip side="left">
54
+ ${trigger}
55
+ <template #tooltip>Tooltip on the left</template>
56
+ </Tooltip>
57
+ `,
58
+ }),
59
+ }
60
+
61
+ export const SideRight: Story = {
62
+ render: () => ({
63
+ components: { Tooltip },
64
+ template: `
65
+ <Tooltip side="right">
66
+ ${trigger}
67
+ <template #tooltip>Tooltip on the right</template>
68
+ </Tooltip>
69
+ `,
70
+ }),
71
+ }
72
+
73
+ export const AlignStart: Story = {
74
+ render: () => ({
75
+ components: { Tooltip },
76
+ template: `
77
+ <Tooltip align="start">
78
+ ${trigger}
79
+ <template #tooltip>Aligned to start</template>
80
+ </Tooltip>
81
+ `,
82
+ }),
83
+ }
84
+
85
+ export const AlignEnd: Story = {
86
+ render: () => ({
87
+ components: { Tooltip },
88
+ template: `
89
+ <Tooltip align="end">
90
+ ${trigger}
91
+ <template #tooltip>Aligned to end</template>
92
+ </Tooltip>
93
+ `,
94
+ }),
95
+ }
96
+
97
+ export const CustomOffset: Story = {
98
+ render: () => ({
99
+ components: { Tooltip },
100
+ template: `
101
+ <Tooltip :side-offset="20">
102
+ ${trigger}
103
+ <template #tooltip>Further away (20px offset)</template>
104
+ </Tooltip>
105
+ `,
106
+ }),
107
+ }
108
+
109
+ export const DisableHoverableContent: Story = {
110
+ render: () => ({
111
+ components: { Tooltip },
112
+ template: `
113
+ <Tooltip :disable-hoverable-content="true">
114
+ ${trigger}
115
+ <template #tooltip>Closes when you move to the tooltip</template>
116
+ </Tooltip>
117
+ `,
118
+ }),
119
+ }
120
+
121
+ export const DisableClosingTrigger: Story = {
122
+ render: () => ({
123
+ components: { Tooltip },
124
+ template: `
125
+ <Tooltip :disable-closing-trigger="true">
126
+ <button class="rounded border border-gray-300 px-3 py-1 text-sm">Click me — tooltip stays open</button>
127
+ <template #tooltip>Stays open after clicking the trigger</template>
128
+ </Tooltip>
129
+ `,
130
+ }),
131
+ }
132
+
133
+ export const RichContent: Story = {
134
+ render: () => ({
135
+ components: { Tooltip },
136
+ template: `
137
+ <Tooltip>
138
+ <button class="rounded border border-gray-300 px-3 py-1 text-sm">Hover for details</button>
139
+ <template #tooltip>
140
+ <strong>Title</strong>
141
+ <p class="mt-1 text-gray-500">Additional description here</p>
142
+ </template>
143
+ </Tooltip>
144
+ `,
145
+ }),
146
+ }
147
+
148
+ export const OnIcon: Story = {
149
+ render: () => ({
150
+ components: { Tooltip },
151
+ template: `
152
+ <Tooltip>
153
+ <span class="inline-flex h-5 w-5 cursor-pointer items-center justify-center rounded-full bg-gray-200 text-xs text-gray-600">?</span>
154
+ <template #tooltip>Helpful information about this field</template>
155
+ </Tooltip>
156
+ `,
157
+ }),
158
+ }
@@ -0,0 +1,134 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import { defineComponent } from 'vue'
4
+ import Tooltip from './Tooltip.vue'
5
+
6
+ // Stub reka-ui components so we can inspect slots and props without portal/hover behaviour
7
+ const TooltipProviderStub = defineComponent({ template: '<div><slot /></div>' })
8
+ const TooltipRootStub = defineComponent({
9
+ props: ['delayDuration', 'disableHoverableContent', 'disableClosingTrigger'],
10
+ template:
11
+ '<div :data-delay="delayDuration" :data-disable-hoverable="disableHoverableContent" :data-disable-closing-trigger="disableClosingTrigger"><slot /></div>',
12
+ })
13
+ const TooltipTriggerStub = defineComponent({ template: '<div><slot /></div>' })
14
+ const TooltipPortalStub = defineComponent({ template: '<div><slot /></div>' })
15
+ const TooltipContentStub = defineComponent({
16
+ props: ['side', 'sideOffset', 'align'],
17
+ template:
18
+ '<div class="tooltip-content" :data-side="side" :data-side-offset="sideOffset" :data-align="align"><slot /></div>',
19
+ })
20
+
21
+ const stubs = {
22
+ TooltipProvider: TooltipProviderStub,
23
+ TooltipRoot: TooltipRootStub,
24
+ TooltipTrigger: TooltipTriggerStub,
25
+ TooltipPortal: TooltipPortalStub,
26
+ TooltipContent: TooltipContentStub,
27
+ }
28
+
29
+ const mountTooltip = (slots = {}, props = {}) => mount(Tooltip, { props, slots, global: { stubs } })
30
+
31
+ describe('Tooltip', () => {
32
+ describe('trigger slot', () => {
33
+ it('renders the default slot as the trigger', () => {
34
+ const wrapper = mountTooltip({ default: '<button>Click me</button>' })
35
+ expect(wrapper.find('button').exists()).toBe(true)
36
+ expect(wrapper.find('button').text()).toBe('Click me')
37
+ })
38
+
39
+ it('renders plain text in the default slot', () => {
40
+ const wrapper = mountTooltip({ default: 'Trigger text' })
41
+ expect(wrapper.text()).toContain('Trigger text')
42
+ })
43
+ })
44
+
45
+ describe('tooltip slot', () => {
46
+ it('renders the tooltip slot content', () => {
47
+ const wrapper = mountTooltip({ tooltip: 'Tooltip message' })
48
+ expect(wrapper.find('.tooltip-content').text()).toContain('Tooltip message')
49
+ })
50
+
51
+ it('renders rich HTML in the tooltip slot', () => {
52
+ const wrapper = mountTooltip({ tooltip: '<strong>Bold text</strong>' })
53
+ expect(wrapper.find('.tooltip-content strong').exists()).toBe(true)
54
+ expect(wrapper.find('.tooltip-content strong').text()).toBe('Bold text')
55
+ })
56
+
57
+ it('renders an empty tooltip content when no tooltip slot is provided', () => {
58
+ const wrapper = mountTooltip()
59
+ expect(wrapper.find('.tooltip-content').text()).toBe('')
60
+ })
61
+ })
62
+
63
+ describe('delay prop', () => {
64
+ it('passes the delay to TooltipRoot', () => {
65
+ const wrapper = mountTooltip({}, { delay: 500 })
66
+ expect(wrapper.findComponent(TooltipRootStub).attributes('data-delay')).toBe('500')
67
+ })
68
+
69
+ it('defaults delay to 0', () => {
70
+ const wrapper = mountTooltip()
71
+ expect(wrapper.findComponent(TooltipRootStub).attributes('data-delay')).toBe('0')
72
+ })
73
+ })
74
+
75
+ describe('side prop', () => {
76
+ it('passes the side to TooltipContent', () => {
77
+ const wrapper = mountTooltip({}, { side: 'bottom' })
78
+ expect(wrapper.find('.tooltip-content').attributes('data-side')).toBe('bottom')
79
+ })
80
+
81
+ it('defaults side to top', () => {
82
+ const wrapper = mountTooltip()
83
+ expect(wrapper.find('.tooltip-content').attributes('data-side')).toBe('top')
84
+ })
85
+ })
86
+
87
+ describe('sideOffset prop', () => {
88
+ it('passes the sideOffset to TooltipContent', () => {
89
+ const wrapper = mountTooltip({}, { sideOffset: 16 })
90
+ expect(wrapper.find('.tooltip-content').attributes('data-side-offset')).toBe('16')
91
+ })
92
+
93
+ it('defaults sideOffset to 8', () => {
94
+ const wrapper = mountTooltip()
95
+ expect(wrapper.find('.tooltip-content').attributes('data-side-offset')).toBe('8')
96
+ })
97
+ })
98
+
99
+ describe('align prop', () => {
100
+ it('passes the align to TooltipContent', () => {
101
+ const wrapper = mountTooltip({}, { align: 'start' })
102
+ expect(wrapper.find('.tooltip-content').attributes('data-align')).toBe('start')
103
+ })
104
+
105
+ it('defaults align to center', () => {
106
+ const wrapper = mountTooltip()
107
+ expect(wrapper.find('.tooltip-content').attributes('data-align')).toBe('center')
108
+ })
109
+ })
110
+
111
+ describe('disableHoverableContent prop', () => {
112
+ it('passes disableHoverableContent to TooltipRoot', () => {
113
+ const wrapper = mountTooltip({}, { disableHoverableContent: true })
114
+ expect(wrapper.findComponent(TooltipRootStub).props('disableHoverableContent')).toBe(true)
115
+ })
116
+
117
+ it('defaults disableHoverableContent to false', () => {
118
+ const wrapper = mountTooltip()
119
+ expect(wrapper.findComponent(TooltipRootStub).props('disableHoverableContent')).toBe(false)
120
+ })
121
+ })
122
+
123
+ describe('disableClosingTrigger prop', () => {
124
+ it('passes disableClosingTrigger to TooltipRoot', () => {
125
+ const wrapper = mountTooltip({}, { disableClosingTrigger: true })
126
+ expect(wrapper.findComponent(TooltipRootStub).props('disableClosingTrigger')).toBe(true)
127
+ })
128
+
129
+ it('defaults disableClosingTrigger to false', () => {
130
+ const wrapper = mountTooltip()
131
+ expect(wrapper.findComponent(TooltipRootStub).props('disableClosingTrigger')).toBe(false)
132
+ })
133
+ })
134
+ })
@@ -0,0 +1,124 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import { ref } from 'vue'
3
+ import { expect, userEvent, waitFor, within } from 'storybook/test'
4
+ import EditorDimensionField from './EditorDimensionField.vue'
5
+ import type { EditorDimensionValue } from './EditorDimensionField.vue'
6
+
7
+ export default {
8
+ title: 'Components/Editor Components/Atoms/Editor Dimension Field',
9
+ component: EditorDimensionField,
10
+ decorators: [() => ({ template: '<div class="w-64 mx-auto"><story /></div>' })],
11
+ args: { label: 'Breedte' },
12
+ argTypes: {
13
+ label: { control: 'text' },
14
+ optional: { control: 'boolean' },
15
+ },
16
+ } satisfies Meta<typeof EditorDimensionField>
17
+
18
+ type Story = StoryObj<typeof EditorDimensionField>
19
+
20
+ /** Shows the model as-is under the field, with the number of finished edits. */
21
+ const render = (args: Story['args'], initial: EditorDimensionValue | null) => ({
22
+ components: { EditorDimensionField },
23
+ setup() {
24
+ const dimension = ref(initial)
25
+ const commits = ref(0)
26
+ return { args, dimension, commits }
27
+ },
28
+ template: `
29
+ <div class="flex flex-col gap-2">
30
+ <EditorDimensionField v-bind="args" v-model="dimension" @commit="commits++" />
31
+ <p data-testid="dimension-value" class="small-normal text-gray-700">
32
+ {{ dimension === null ? 'null' : dimension.value + ' ' + dimension.unit }} · {{ commits }}
33
+ </p>
34
+ </div>
35
+ `,
36
+ })
37
+
38
+ const readout = (canvasElement: HTMLElement) =>
39
+ canvasElement.querySelector('[data-testid="dimension-value"]')!.textContent!.trim()
40
+
41
+ /** px only, the default: the unit is plain text, as in the padding fields. */
42
+ export const Default: Story = {
43
+ render: (args) => render(args, { value: 320, unit: 'px' }),
44
+ play: async ({ canvasElement }) => {
45
+ const input = within(canvasElement).getByRole('spinbutton') as HTMLInputElement
46
+
47
+ // Typing is left alone until it is confirmed, then clamped to 40..1600.
48
+ await userEvent.clear(input)
49
+ await userEvent.type(input, '12')
50
+ expect(readout(canvasElement)).toBe('320 px · 0')
51
+
52
+ await userEvent.keyboard('{Enter}')
53
+ await waitFor(() => expect(input.value).toBe('40'))
54
+ expect(readout(canvasElement)).toBe('40 px · 1')
55
+
56
+ // A stream of arrow steps is one finished edit, closed when the key is let go.
57
+ await userEvent.keyboard('{Shift>}{ArrowUp}{ArrowUp}{/Shift}')
58
+ await waitFor(() => expect(readout(canvasElement)).toBe('60 px · 3'))
59
+
60
+ // Escape undoes the typing, not the value.
61
+ await userEvent.clear(input)
62
+ await userEvent.type(input, '999')
63
+ await userEvent.keyboard('{Escape}')
64
+ await waitFor(() => expect(input.value).toBe('60'))
65
+ },
66
+ }
67
+
68
+ /**
69
+ * px and %: the unit becomes a selector. Switching keeps the number, clamped
70
+ * into the range of the new unit -- 320 px becomes 100 %.
71
+ */
72
+ export const PixelsAndPercent: Story = {
73
+ args: { units: ['px', '%'] },
74
+ render: (args) => render(args, { value: 320, unit: 'px' }),
75
+ play: async ({ canvasElement }) => {
76
+ const canvas = within(canvasElement)
77
+
78
+ await userEvent.selectOptions(canvas.getByRole('combobox', { name: 'Eenheid' }), '%')
79
+
80
+ await waitFor(() => expect(readout(canvasElement)).toBe('100 % · 1'))
81
+ },
82
+ }
83
+
84
+ /** An optional field without a value: empty means none, shown as the placeholder next to the unit. */
85
+ export const OptionalEmpty: Story = {
86
+ args: { optional: true },
87
+ render: (args) => render(args, null),
88
+ }
89
+
90
+ /**
91
+ * Optional with both units: the selector stays in sight on an empty field, so
92
+ * the unit can be picked before a value is typed -- Tab from the input reaches it.
93
+ */
94
+ export const OptionalEmptyWithUnits: Story = {
95
+ args: { optional: true, units: ['px', '%'] },
96
+ render: (args) => render(args, null),
97
+ play: async ({ canvasElement }) => {
98
+ const canvas = within(canvasElement)
99
+
100
+ expect(canvas.getByRole('combobox', { name: 'Eenheid' })).toBeVisible()
101
+
102
+ await userEvent.click(canvas.getByRole('spinbutton'))
103
+ await userEvent.tab()
104
+ const select = canvas.getByRole('combobox', { name: 'Eenheid' })
105
+ expect(select).toHaveFocus()
106
+
107
+ await userEvent.selectOptions(select, '%')
108
+ await userEvent.click(canvas.getByRole('spinbutton'))
109
+ await userEvent.type(canvas.getByRole('spinbutton'), '250')
110
+ await userEvent.keyboard('{Enter}')
111
+
112
+ await waitFor(() => expect(readout(canvasElement)).toBe('100 % · 1'))
113
+ },
114
+ }
115
+
116
+ export const CustomMessages: Story = {
117
+ args: {
118
+ label: 'Width',
119
+ optional: true,
120
+ units: ['px', '%'],
121
+ messages: { placeholder: 'None', unit: 'Unit', px: 'px', percent: '%' },
122
+ },
123
+ render: (args) => render(args, null),
124
+ }