@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 type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import { expect, userEvent, waitFor, within } from 'storybook/test'
3
+ import { computed, defineComponent, ref } from 'vue'
4
+ import NexxtButton from '../Button/Button.vue'
5
+ import NexxtIcon, { type NexxtIconName } from '../Icon/Icon.vue'
6
+ import PickerPreview from './PickerPreview.vue'
7
+
8
+ /** A neutral stand-in for real preview art: one plane, one outline, the icon in the middle. */
9
+ const DummyArt = defineComponent({
10
+ components: { NexxtIcon },
11
+ props: { icon: { type: String as () => NexxtIconName, required: true } },
12
+ template: `
13
+ <div class="relative size-full">
14
+ <svg viewBox="0 0 320 200" width="100%" height="100%" preserveAspectRatio="xMidYMid meet" aria-hidden="true" focusable="false" class="absolute inset-0">
15
+ <rect x="16" y="16" width="288" height="168" rx="6" fill="var(--color-gray-100)" stroke="var(--color-gray-300)" stroke-width="2" vector-effect="non-scaling-stroke" />
16
+ </svg>
17
+ <div class="absolute inset-0 grid place-items-center text-5xl text-cornflower-blue-500">
18
+ <NexxtIcon :name="icon" aria-hidden="true" />
19
+ </div>
20
+ </div>
21
+ `,
22
+ })
23
+
24
+ export default {
25
+ title: 'Components/Atoms/PickerPreview',
26
+ component: PickerPreview,
27
+ decorators: [() => ({ template: '<div class="w-86"><story /></div>' })],
28
+ args: {
29
+ title: 'Afbeelding',
30
+ description: 'Een foto of illustratie over de volle breedte van de pagina.',
31
+ emptyText: 'Kies een onderdeel om een voorbeeld te zien.',
32
+ },
33
+ } satisfies Meta<typeof PickerPreview>
34
+
35
+ type Story = StoryObj<typeof PickerPreview>
36
+
37
+ const frameOf = (canvasElement: HTMLElement) =>
38
+ canvasElement.querySelector<HTMLElement>('[class*="aspect-"]')!
39
+
40
+ export const WithContent: Story = {
41
+ render: (args) => ({
42
+ components: { PickerPreview, DummyArt },
43
+ setup: () => ({ args }),
44
+ template: `<PickerPreview v-bind="args"><DummyArt icon="image" /></PickerPreview>`,
45
+ }),
46
+ play: async ({ canvasElement }) => {
47
+ const root = canvasElement.querySelector('[aria-hidden="true"]')!
48
+
49
+ await expect(root.querySelectorAll('button, a, input, [tabindex]')).toHaveLength(0)
50
+ await expect(frameOf(canvasElement).querySelector('svg')).not.toBeNull()
51
+ },
52
+ }
53
+
54
+ /** Nothing highlighted: the frame keeps its size and centres the text. */
55
+ export const EmptyWithText: Story = {
56
+ args: { title: undefined, description: undefined },
57
+ play: async ({ canvasElement }) => {
58
+ const frame = frameOf(canvasElement)
59
+ const { width, height } = frame.getBoundingClientRect()
60
+
61
+ await expect(frame).toHaveTextContent('Kies een onderdeel om een voorbeeld te zien.')
62
+ await expect(width / height).toBeCloseTo(1.6, 1)
63
+ },
64
+ }
65
+
66
+ export const EmptyWithSlot: Story = {
67
+ args: { title: undefined, description: undefined },
68
+ render: (args) => ({
69
+ components: { PickerPreview, NexxtIcon },
70
+ setup: () => ({ args }),
71
+ template: `
72
+ <PickerPreview v-bind="args">
73
+ <template #empty>
74
+ <div class="flex flex-col items-center gap-2 text-gray-700">
75
+ <NexxtIcon name="hand-pointer" class="text-2xl" aria-hidden="true" />
76
+ <p class="small-normal">Wijs een onderdeel aan.</p>
77
+ </div>
78
+ </template>
79
+ </PickerPreview>
80
+ `,
81
+ }),
82
+ }
83
+
84
+ /** The explanation stops after two lines; the title after one. */
85
+ export const LongDescription: Story = {
86
+ args: {
87
+ title: 'Een titel die veel te lang is voor de kolom van het voorbeeld',
88
+ description:
89
+ 'Een uitleg die zo lang is dat hij niet in twee regels past. Hij loopt door en door, zodat te zien is dat de rest wordt afgekapt in plaats van dat de kolom groeit en de hoogte van het paneel verschuift.',
90
+ },
91
+ render: (args) => ({
92
+ components: { PickerPreview, DummyArt },
93
+ setup: () => ({ args }),
94
+ template: `<PickerPreview v-bind="args"><DummyArt icon="font" /></PickerPreview>`,
95
+ }),
96
+ play: async ({ canvasElement }) => {
97
+ const [title, description] = Array.from(canvasElement.querySelectorAll<HTMLElement>('p'))
98
+ const lineHeight = parseFloat(getComputedStyle(description).lineHeight)
99
+
100
+ await expect(title.scrollWidth).toBeGreaterThan(title.clientWidth)
101
+ await expect(description.clientHeight).toBeLessThanOrEqual(lineHeight * 2 + 1)
102
+ },
103
+ }
104
+
105
+ /** 280, 344 and 520 wide: the frame stays 16:10 at every width. */
106
+ export const Widths: Story = {
107
+ decorators: [() => ({ template: '<story />' })],
108
+ render: (args) => ({
109
+ components: { PickerPreview, DummyArt },
110
+ setup: () => ({ args, widths: [280, 344, 520] }),
111
+ template: `
112
+ <div class="flex items-start gap-6">
113
+ <div v-for="width in widths" :key="width" class="shrink-0" :style="{ width: width + 'px' }">
114
+ <PickerPreview v-bind="args"><DummyArt icon="image" /></PickerPreview>
115
+ </div>
116
+ </div>
117
+ `,
118
+ }),
119
+ play: async ({ canvasElement }) => {
120
+ const frames = Array.from(canvasElement.querySelectorAll<HTMLElement>('[class*="aspect-"]'))
121
+
122
+ await expect(frames.map((frame) => Math.round(frame.getBoundingClientRect().width))).toEqual([
123
+ 280, 344, 520,
124
+ ])
125
+ for (const frame of frames) {
126
+ const { width, height } = frame.getBoundingClientRect()
127
+ await expect(width / height).toBeCloseTo(1.6, 1)
128
+ await expect(getComputedStyle(frame).overflow).toBe('hidden')
129
+ }
130
+ },
131
+ }
132
+
133
+ const SEQUENCE = [
134
+ { key: 'image', icon: 'image', title: 'Afbeelding', description: 'Een foto of illustratie.' },
135
+ { key: 'title', icon: 'heading', title: 'Titel', description: 'Een kop boven een onderdeel.' },
136
+ { key: 'text', icon: 'font', title: 'Tekst', description: 'Een alinea lopende tekst.' },
137
+ ] as const
138
+
139
+ /** Each click shows the next item: the new content fades in, the old one is gone at once. */
140
+ export const Swap: Story = {
141
+ render: () => ({
142
+ components: { PickerPreview, DummyArt, NexxtButton },
143
+ setup() {
144
+ const index = ref(0)
145
+ const current = computed(() => SEQUENCE[index.value % SEQUENCE.length])
146
+ return { current, next: () => index.value++ }
147
+ },
148
+ template: `
149
+ <div class="flex flex-col gap-4">
150
+ <NexxtButton variant="secondary" @click="next">Volgende</NexxtButton>
151
+ <PickerPreview :content-key="current.key" :title="current.title" :description="current.description">
152
+ <DummyArt :icon="current.icon" />
153
+ </PickerPreview>
154
+ </div>
155
+ `,
156
+ }),
157
+ play: async ({ canvasElement }) => {
158
+ const frame = frameOf(canvasElement)
159
+ const fading: string[] = []
160
+ frame.addEventListener('transitionrun', (event) => {
161
+ fading.push((event as TransitionEvent).propertyName)
162
+ })
163
+
164
+ await userEvent.click(within(canvasElement).getByRole('button', { name: 'Volgende' }))
165
+
166
+ // no crossfade: only the incoming layer is ever in the frame
167
+ await expect(frame.children).toHaveLength(1)
168
+ await expect(canvasElement.querySelector('p')).toHaveTextContent('Titel')
169
+ await waitFor(() => expect(fading).toContain('opacity'))
170
+
171
+ // swaps 30ms apart, like a held arrow key, show at full opacity instead of restarting the fade
172
+ const button = within(canvasElement).getByRole('button', { name: 'Volgende' })
173
+ await new Promise((resolve) => setTimeout(resolve, 300))
174
+ button.click()
175
+ await new Promise((resolve) => setTimeout(resolve, 30))
176
+ button.click()
177
+ await new Promise((resolve) => setTimeout(resolve, 20))
178
+ await expect(getComputedStyle(frame.children[0]).opacity).toBe('1')
179
+ await expect(getComputedStyle(canvasElement.querySelector('p')!.parentElement!).opacity).toBe(
180
+ '1',
181
+ )
182
+ },
183
+ }
@@ -0,0 +1,137 @@
1
+ import { afterEach, describe, it, expect, vi } from 'vitest'
2
+ import { nextTick } from 'vue'
3
+ import { mount } from '@vue/test-utils'
4
+ import PickerPreview from './PickerPreview.vue'
5
+
6
+ const frameOf = (wrapper: ReturnType<typeof mount>) => wrapper.find('[class*="aspect-"]')
7
+
8
+ const stubReducedMotion = (reduce: boolean) =>
9
+ vi.stubGlobal('matchMedia', (query: string) => ({
10
+ matches: reduce && query.includes('reduce'),
11
+ addEventListener: () => {},
12
+ removeEventListener: () => {},
13
+ }))
14
+
15
+ describe('PickerPreview', () => {
16
+ afterEach(() => {
17
+ vi.unstubAllGlobals()
18
+ })
19
+
20
+ it('hides the whole preview from assistive tech and holds nothing focusable', () => {
21
+ const wrapper = mount(PickerPreview, {
22
+ props: { title: 'Afbeelding', description: 'Een foto' },
23
+ slots: { default: '<svg></svg>' },
24
+ })
25
+
26
+ expect(wrapper.attributes('aria-hidden')).toBe('true')
27
+ expect(wrapper.find('button, a, input, [tabindex]').exists()).toBe(false)
28
+ })
29
+
30
+ it('keeps a 16:10 frame that clips its content on the radius', () => {
31
+ const wrapper = mount(PickerPreview)
32
+
33
+ expect(frameOf(wrapper).classes()).toEqual(
34
+ expect.arrayContaining(['aspect-16/10', 'w-full', 'overflow-hidden', 'rounded-xl']),
35
+ )
36
+ })
37
+
38
+ it('shows the empty text in the frame when there is no content and no title', () => {
39
+ const wrapper = mount(PickerPreview, { props: { emptyText: 'Kies een item' } })
40
+
41
+ expect(frameOf(wrapper).text()).toBe('Kies een item')
42
+ expect(wrapper.findAll('p')).toHaveLength(1)
43
+ })
44
+
45
+ it('lets the empty slot replace the empty text', () => {
46
+ const wrapper = mount(PickerPreview, {
47
+ props: { emptyText: 'Kies een item' },
48
+ slots: { empty: '<em>Niets gekozen</em>' },
49
+ })
50
+
51
+ expect(frameOf(wrapper).text()).toBe('Niets gekozen')
52
+ })
53
+
54
+ it('keeps the frame when empty without any copy', () => {
55
+ const wrapper = mount(PickerPreview)
56
+
57
+ expect(frameOf(wrapper).exists()).toBe(true)
58
+ expect(wrapper.text()).toBe('')
59
+ })
60
+
61
+ it('renders title and description under an empty frame without slot content', () => {
62
+ const wrapper = mount(PickerPreview, {
63
+ props: { title: 'Titel', description: 'Een kop', emptyText: 'Kies een item' },
64
+ })
65
+
66
+ expect(frameOf(wrapper).text()).toBe('')
67
+ expect(wrapper.findAll('p').map((p) => p.text())).toEqual(['Titel', 'Een kop'])
68
+ })
69
+
70
+ it('renders the default slot in the frame', () => {
71
+ const wrapper = mount(PickerPreview, {
72
+ props: { title: 'Afbeelding' },
73
+ slots: { default: '<svg data-test="art"></svg>' },
74
+ })
75
+
76
+ expect(frameOf(wrapper).find('[data-test="art"]').exists()).toBe(true)
77
+ })
78
+
79
+ it('fades new content in and drops the old content at once', async () => {
80
+ stubReducedMotion(false)
81
+ const wrapper = mount(PickerPreview, {
82
+ props: { title: 'Een', contentKey: 'a' },
83
+ slots: { default: '<span>art</span>' },
84
+ global: { stubs: { transition: false } },
85
+ })
86
+
87
+ await wrapper.setProps({ title: 'Twee', contentKey: 'b' })
88
+
89
+ const layers = frameOf(wrapper).element.children
90
+ expect(layers).toHaveLength(1)
91
+ expect(layers[0].className).toContain('opacity-0')
92
+ expect(layers[0].className).toContain('motion-reduce:transition-none')
93
+ expect(wrapper.findAll('p').map((p) => p.text())).toEqual(['Twee'])
94
+ })
95
+
96
+ it('skips the fade for a swap within 150ms of the previous one, as with a held arrow key', async () => {
97
+ vi.useFakeTimers()
98
+ stubReducedMotion(false)
99
+ const wrapper = mount(PickerPreview, {
100
+ props: { title: 'Een', contentKey: 'a' },
101
+ slots: { default: '<span>art</span>' },
102
+ global: { stubs: { transition: false } },
103
+ })
104
+ const layer = () => frameOf(wrapper).element.children[0]
105
+
106
+ await wrapper.setProps({ title: 'Twee', contentKey: 'b' })
107
+ expect(layer().className).toContain('opacity-0')
108
+
109
+ vi.advanceTimersByTime(30)
110
+ await wrapper.setProps({ title: 'Drie', contentKey: 'c' })
111
+ expect(frameOf(wrapper).element.children).toHaveLength(1)
112
+ expect(layer().className).not.toContain('opacity-0')
113
+ expect(wrapper.findAll('p').at(-1)!.element.parentElement!.className).not.toContain('opacity-0')
114
+
115
+ // after a pause, a single swap fades again
116
+ vi.advanceTimersByTime(200)
117
+ await wrapper.setProps({ title: 'Vier', contentKey: 'd' })
118
+ expect(layer().className).toContain('opacity-0')
119
+ vi.useRealTimers()
120
+ })
121
+
122
+ it('swaps without a fade under reduced motion', async () => {
123
+ stubReducedMotion(true)
124
+ const wrapper = mount(PickerPreview, {
125
+ props: { title: 'Een', contentKey: 'a' },
126
+ slots: { default: '<span>art</span>' },
127
+ global: { stubs: { transition: false } },
128
+ })
129
+
130
+ await wrapper.setProps({ title: 'Twee', contentKey: 'b' })
131
+ await nextTick()
132
+
133
+ const layers = frameOf(wrapper).element.children
134
+ expect(layers).toHaveLength(1)
135
+ expect(layers[0].className).not.toContain('opacity-0')
136
+ })
137
+ })
@@ -0,0 +1,21 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import PresenceIndicator from './PresenceIndicator.vue'
3
+
4
+ export default {
5
+ title: 'Components/Atoms/Presence indicator',
6
+ component: PresenceIndicator,
7
+ } satisfies Meta<typeof PresenceIndicator>
8
+
9
+ type Story = StoryObj<typeof PresenceIndicator>
10
+
11
+ export const Online: Story = {
12
+ args: {
13
+ online: true,
14
+ },
15
+ }
16
+
17
+ export const Offline: Story = {
18
+ args: {
19
+ online: false,
20
+ },
21
+ }
@@ -0,0 +1,38 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import NexxtPropertyListing from './PropertyListing.vue'
3
+
4
+ export default {
5
+ title: 'Components/Atoms/Property Listing',
6
+ component: NexxtPropertyListing,
7
+ } satisfies Meta<typeof NexxtPropertyListing>
8
+
9
+ type Story = StoryObj<typeof NexxtPropertyListing>
10
+
11
+ export const Default: Story = {
12
+ args: {
13
+ price: 520000,
14
+ city: 'Hollandscheveld',
15
+ text: 'Van Echtenstraat 62b',
16
+ },
17
+ }
18
+
19
+ export const CityOnly: Story = {
20
+ args: {
21
+ city: 'Hollandscheveld',
22
+ },
23
+ }
24
+
25
+ export const WithLongText: Story = {
26
+ args: {
27
+ price: 520000,
28
+ city: 'Hollandscheveld',
29
+ text: 'Van Echtenstraat 62b is a spacious family home located in the heart of Hollandscheveld. This charming property features a large living area, modern kitchen, and a sunny backyard, making it perfect for comfortable family living.',
30
+ },
31
+ }
32
+
33
+ export const OverflowingCity: Story = {
34
+ args: {
35
+ price: 1250000,
36
+ city: 'Nieuw-Amsterdam en Veenoord',
37
+ },
38
+ }
@@ -0,0 +1,46 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import NexxtPropertyListing from './PropertyListing.vue'
4
+
5
+ describe('NexxtPropertyListing', () => {
6
+ it('renders price with euro sign and city', () => {
7
+ const wrapper = mount(NexxtPropertyListing, {
8
+ props: { price: 520000, city: 'Hollandscheveld' },
9
+ })
10
+ expect(wrapper.text()).toContain('€')
11
+ expect(wrapper.text()).toContain('520.000')
12
+ expect(wrapper.text()).toContain('Hollandscheveld')
13
+ expect(wrapper.text()).toContain('|')
14
+ })
15
+
16
+ it('renders city as semibold when no price', () => {
17
+ const wrapper = mount(NexxtPropertyListing, {
18
+ props: { city: 'Hollandscheveld' },
19
+ })
20
+ expect(wrapper.text()).not.toContain('|')
21
+ expect(wrapper.find('.body-semibold').text()).toContain('Hollandscheveld')
22
+ })
23
+
24
+ it('does not render separator when price is absent', () => {
25
+ const wrapper = mount(NexxtPropertyListing, {
26
+ props: { city: 'Hollandscheveld' },
27
+ })
28
+ expect(wrapper.text()).not.toContain('|')
29
+ })
30
+
31
+ it('renders text when provided', () => {
32
+ const wrapper = mount(NexxtPropertyListing, {
33
+ props: { city: 'Hollandscheveld', text: 'Van Echtenstraat 62b' },
34
+ })
35
+ expect(wrapper.text()).toContain('Van Echtenstraat 62b')
36
+ })
37
+
38
+ it('renders long text', () => {
39
+ const longText =
40
+ 'Van Echtenstraat 62b is a spacious family home located in the heart of Hollandscheveld. This charming property features a large living area, modern kitchen, and a sunny backyard, making it perfect for comfortable family living.'
41
+ const wrapper = mount(NexxtPropertyListing, {
42
+ props: { price: 520000, city: 'Hollandscheveld', text: longText },
43
+ })
44
+ expect(wrapper.text()).toContain(longText)
45
+ })
46
+ })
@@ -0,0 +1,131 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import { ref } from 'vue'
3
+ import SelectableOption from './SelectableOption.vue'
4
+
5
+ export default {
6
+ title: 'Components/Atoms/SelectableOption',
7
+ component: SelectableOption,
8
+ argTypes: {
9
+ rounded: { control: 'select', options: ['lg', 'xl', '3xl'] },
10
+ },
11
+ } satisfies Meta<typeof SelectableOption>
12
+
13
+ type Story = StoryObj<typeof SelectableOption>
14
+
15
+ const single = (args: Story['args']) => ({
16
+ components: { SelectableOption },
17
+ setup: () => ({ args }),
18
+ template: `<SelectableOption v-bind="args" class="px-5 py-2.5">30 minuten</SelectableOption>`,
19
+ })
20
+
21
+ export const Default: Story = {
22
+ args: { selected: false },
23
+ render: single,
24
+ }
25
+
26
+ export const Selected: Story = {
27
+ args: { selected: true },
28
+ render: single,
29
+ }
30
+
31
+ export const Disabled: Story = {
32
+ args: { selected: false, disabled: true },
33
+ render: single,
34
+ }
35
+
36
+ export const DisabledSelected: Story = {
37
+ args: { selected: true, disabled: true },
38
+ render: single,
39
+ }
40
+
41
+ /** Cards get a check badge in the corner; it only appears while selected. */
42
+ export const WithCheckmark: Story = {
43
+ args: { selected: true, checkmark: true },
44
+ render: (args) => ({
45
+ components: { SelectableOption },
46
+ setup: () => ({ args }),
47
+ template: `<SelectableOption v-bind="args" class="w-48 p-6 text-center">Kaart</SelectableOption>`,
48
+ }),
49
+ }
50
+
51
+ /**
52
+ * The three radii side by side. `lg` is for compact rows of choices, `xl` for
53
+ * tile pickers, `3xl` for the large content-type tiles.
54
+ */
55
+ export const Radii: Story = {
56
+ render: () => ({
57
+ components: { SelectableOption },
58
+ setup: () => ({ radii: ['lg', 'xl', '3xl'] as const }),
59
+ template: `
60
+ <div class="flex items-center gap-4">
61
+ <SelectableOption v-for="r in radii" :key="r" :rounded="r" selected class="px-5 py-2.5">
62
+ {{ r }}
63
+ </SelectableOption>
64
+ </div>
65
+ `,
66
+ }),
67
+ }
68
+
69
+ /**
70
+ * A single-choice group, the way the planner wizard uses it: the parent owns
71
+ * the selection and every option reads its own `selected` from it.
72
+ */
73
+ export const SingleChoiceGroup: Story = {
74
+ render: () => ({
75
+ components: { SelectableOption },
76
+ setup: () => {
77
+ const options = [15, 30, 45, 60]
78
+ const chosen = ref(30)
79
+ return { options, chosen }
80
+ },
81
+ template: `
82
+ <div class="flex flex-wrap items-center gap-3">
83
+ <SelectableOption
84
+ v-for="option in options"
85
+ :key="option"
86
+ rounded="lg"
87
+ :selected="chosen === option"
88
+ class="px-5 py-2.5"
89
+ @click="chosen = option"
90
+ >
91
+ {{ option }} minuten
92
+ </SelectableOption>
93
+ </div>
94
+ `,
95
+ }),
96
+ }
97
+
98
+ /**
99
+ * The slot receives `selected`, so content can colour itself along with the
100
+ * option instead of re-deriving the state. This is the employee-card pattern.
101
+ */
102
+ export const SlotReceivesSelected: Story = {
103
+ render: () => ({
104
+ components: { SelectableOption },
105
+ setup: () => {
106
+ const chosen = ref('anna')
107
+ const people = [
108
+ { id: 'anna', name: 'Anna de Vries', email: 'anna@nexxtmove.nl' },
109
+ { id: 'bram', name: 'Bram Jansen', email: 'bram@nexxtmove.nl' },
110
+ ]
111
+ return { chosen, people }
112
+ },
113
+ template: `
114
+ <div class="grid w-[520px] grid-cols-2 gap-4">
115
+ <SelectableOption
116
+ v-for="person in people"
117
+ :key="person.id"
118
+ v-slot="{ selected }"
119
+ :selected="chosen === person.id"
120
+ class="p-6 text-center"
121
+ @click="chosen = person.id"
122
+ >
123
+ <p class="font-semibold" :class="selected ? 'text-cornflower-blue-500' : 'text-neutral-900'">
124
+ {{ person.name }}
125
+ </p>
126
+ <p class="text-sm text-neutral-500">{{ person.email }}</p>
127
+ </SelectableOption>
128
+ </div>
129
+ `,
130
+ }),
131
+ }
@@ -0,0 +1,96 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import SelectableOption from './SelectableOption.vue'
4
+
5
+ describe('SelectableOption', () => {
6
+ it('renders a button that never submits a form', () => {
7
+ const wrapper = mount(SelectableOption, { slots: { default: 'Optie' } })
8
+
9
+ expect(wrapper.element.tagName).toBe('BUTTON')
10
+ expect(wrapper.attributes('type')).toBe('button')
11
+ })
12
+
13
+ it('renders slot content', () => {
14
+ const wrapper = mount(SelectableOption, { slots: { default: 'Optie' } })
15
+
16
+ expect(wrapper.text()).toBe('Optie')
17
+ })
18
+
19
+ it('is not selected by default', () => {
20
+ const wrapper = mount(SelectableOption)
21
+
22
+ expect(wrapper.attributes('aria-pressed')).toBe('false')
23
+ expect(wrapper.classes()).toContain('border-gray-200')
24
+ expect(wrapper.classes()).not.toContain('border-cornflower-blue-500')
25
+ })
26
+
27
+ it('exposes the selected state to assistive tech and styles it', () => {
28
+ const wrapper = mount(SelectableOption, { props: { selected: true } })
29
+
30
+ expect(wrapper.attributes('aria-pressed')).toBe('true')
31
+ expect(wrapper.classes()).toContain('border-cornflower-blue-500')
32
+ expect(wrapper.classes()).toContain('bg-cornflower-blue-50')
33
+ expect(wrapper.classes()).not.toContain('border-gray-200')
34
+ })
35
+
36
+ it('hands selected to the slot', () => {
37
+ const wrapper = mount(SelectableOption, {
38
+ props: { selected: true },
39
+ slots: {
40
+ default: `<template #default="{ selected }"><span>{{ selected ? 'aan' : 'uit' }}</span></template>`,
41
+ },
42
+ })
43
+
44
+ expect(wrapper.text()).toBe('aan')
45
+ })
46
+
47
+ it('uses the xl radius by default', () => {
48
+ const wrapper = mount(SelectableOption)
49
+
50
+ expect(wrapper.classes()).toContain('rounded-xl')
51
+ })
52
+
53
+ it.each(['lg', 'xl', '3xl'] as const)('applies the %s radius', (rounded) => {
54
+ const wrapper = mount(SelectableOption, { props: { rounded } })
55
+
56
+ expect(wrapper.classes()).toContain(`rounded-${rounded}`)
57
+ })
58
+
59
+ it('disables the button when disabled', () => {
60
+ const wrapper = mount(SelectableOption, { props: { disabled: true } })
61
+
62
+ expect((wrapper.element as HTMLButtonElement).disabled).toBe(true)
63
+ })
64
+
65
+ it('emits a native click when chosen', async () => {
66
+ const wrapper = mount(SelectableOption)
67
+
68
+ await wrapper.trigger('click')
69
+
70
+ expect(wrapper.emitted('click')).toHaveLength(1)
71
+ })
72
+
73
+ it('shows no check badge by default', () => {
74
+ const wrapper = mount(SelectableOption, { props: { selected: true } })
75
+
76
+ expect(wrapper.find('i.fa-check').exists()).toBe(false)
77
+ })
78
+
79
+ it('shows the check badge only while selected', async () => {
80
+ const wrapper = mount(SelectableOption, { props: { checkmark: true, selected: false } })
81
+
82
+ expect(wrapper.find('i.fa-check').exists()).toBe(false)
83
+ expect(wrapper.classes()).toContain('relative')
84
+
85
+ await wrapper.setProps({ selected: true })
86
+
87
+ expect(wrapper.find('i.fa-check').exists()).toBe(true)
88
+ })
89
+
90
+ it('forwards attributes and classes to the button', () => {
91
+ const wrapper = mount(SelectableOption, { attrs: { class: 'p-6', 'data-test': 'optie' } })
92
+
93
+ expect(wrapper.classes()).toContain('p-6')
94
+ expect(wrapper.attributes('data-test')).toBe('optie')
95
+ })
96
+ })
@@ -0,0 +1,27 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import Skeleton from './Skeleton.vue'
3
+
4
+ export default {
5
+ title: 'Components/Atoms/Skeleton',
6
+ component: Skeleton,
7
+ } satisfies Meta<typeof Skeleton>
8
+
9
+ type Story = StoryObj<typeof Skeleton>
10
+
11
+ export const Default: Story = {
12
+ args: {
13
+ class: 'h-4 w-48',
14
+ },
15
+ }
16
+
17
+ export const Circle: Story = {
18
+ args: {
19
+ class: 'h-12 w-12 rounded-full',
20
+ },
21
+ }
22
+
23
+ export const ImagePlaceholder: Story = {
24
+ args: {
25
+ class: 'h-40 w-80',
26
+ },
27
+ }