@nexxtmove/ui 2.5.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 (228) hide show
  1. package/dist/components/editor/molecules/EditorBorder/EditorBorder.vue.d.ts +12 -0
  2. package/dist/components/editor/molecules/EditorColor/_ThemeRoleSwatches.vue.d.ts +16 -0
  3. package/dist/index.js +1094 -1135
  4. package/dist/nuxt.js +0 -1
  5. package/package.json +38 -36
  6. package/src/attribute-contract.test.ts +151 -0
  7. package/src/components/atoms/AnimatedNumber/AnimatedNumber.stories.ts +36 -0
  8. package/src/components/atoms/AnimatedNumber/AnimatedNumber.test.ts +21 -0
  9. package/src/components/atoms/Avatar/Avatar.stories.ts +122 -0
  10. package/src/components/atoms/Avatar/Avatar.test.ts +134 -0
  11. package/src/components/atoms/Button/Button.stories.ts +290 -0
  12. package/src/components/atoms/Button/Button.test.ts +406 -0
  13. package/src/components/atoms/Card/Card.stories.ts +45 -0
  14. package/src/components/atoms/Card/Card.test.ts +46 -0
  15. package/src/components/atoms/Checkbox/Checkbox.stories.ts +35 -0
  16. package/src/components/atoms/Checkbox/Checkbox.test.ts +55 -0
  17. package/src/components/atoms/Chip/Chip.stories.ts +274 -0
  18. package/src/components/atoms/Chip/Chip.test.ts +230 -0
  19. package/src/components/atoms/EnergyLabel/EnergyLabel.stories.ts +189 -0
  20. package/src/components/atoms/EnergyLabel/EnergyLabel.test.ts +100 -0
  21. package/src/components/atoms/FloatingPanel/FloatingPanel.stories.ts +677 -0
  22. package/src/components/atoms/FloatingPanel/FloatingPanel.test.ts +864 -0
  23. package/src/components/atoms/Icon/Icon.stories.ts +63 -0
  24. package/src/components/atoms/Icon/Icon.test.ts +89 -0
  25. package/src/components/atoms/Loader/Loader.stories.ts +36 -0
  26. package/src/components/atoms/Loader/Loader.test.ts +40 -0
  27. package/src/components/atoms/PickerItem/PickerItem.stories.ts +172 -0
  28. package/src/components/atoms/PickerItem/PickerItem.test.ts +142 -0
  29. package/src/components/atoms/PickerPreview/PickerPreview.stories.ts +183 -0
  30. package/src/components/atoms/PickerPreview/PickerPreview.test.ts +137 -0
  31. package/src/components/atoms/PresenceIndicator/PresenceIndicator.stories.ts +21 -0
  32. package/src/components/atoms/PropertyListing/PropertyListing.stories.ts +38 -0
  33. package/src/components/atoms/PropertyListing/PropertyListing.test.ts +46 -0
  34. package/src/components/atoms/SelectableOption/SelectableOption.stories.ts +131 -0
  35. package/src/components/atoms/SelectableOption/SelectableOption.test.ts +96 -0
  36. package/src/components/atoms/Skeleton/Skeleton.stories.ts +27 -0
  37. package/src/components/atoms/Skeleton/Skeleton.test.ts +28 -0
  38. package/src/components/atoms/Slider/Slider.stories.ts +151 -0
  39. package/src/components/atoms/Slider/Slider.test.ts +191 -0
  40. package/src/components/atoms/Squircle/Squircle.stories.ts +114 -0
  41. package/src/components/atoms/Squircle/Squircle.test.ts +70 -0
  42. package/src/components/atoms/Switch/Switch.stories.ts +91 -0
  43. package/src/components/atoms/Switch/Switch.test.ts +183 -0
  44. package/src/components/atoms/Tooltip/Tooltip.stories.ts +158 -0
  45. package/src/components/atoms/Tooltip/Tooltip.test.ts +134 -0
  46. package/src/components/editor/atoms/EditorDimensionField/EditorDimensionField.stories.ts +124 -0
  47. package/src/components/editor/atoms/EditorDimensionField/EditorDimensionField.test.ts +464 -0
  48. package/src/components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.stories.ts +266 -0
  49. package/src/components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.test.ts +421 -0
  50. package/src/components/editor/atoms/EditorSettingsGroup/EditorSettingsGroup.stories.ts +365 -0
  51. package/src/components/editor/atoms/EditorSettingsGroup/EditorSettingsGroup.test.ts +484 -0
  52. package/src/components/editor/composables/useRadioGrid.test.ts +261 -0
  53. package/src/components/editor/molecules/EditorAlignItems/EditorAlignItems.stories.ts +73 -0
  54. package/src/components/editor/molecules/EditorAlignItems/EditorAlignItems.test.ts +189 -0
  55. package/src/components/editor/molecules/EditorAlignmentGrid/EditorAlignmentGrid.stories.ts +309 -0
  56. package/src/components/editor/molecules/EditorAlignmentGrid/EditorAlignmentGrid.test.ts +359 -0
  57. package/src/components/editor/molecules/EditorAnchor/EditorAnchor.stories.ts +138 -0
  58. package/src/components/editor/molecules/EditorAnchor/EditorAnchor.test.ts +344 -0
  59. package/src/components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.stories.ts +87 -0
  60. package/src/components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.test.ts +262 -0
  61. package/src/components/editor/molecules/EditorBlur/EditorBlur.stories.ts +115 -0
  62. package/src/components/editor/molecules/EditorBlur/EditorBlur.test.ts +243 -0
  63. package/src/components/editor/molecules/EditorBorder/EditorBorder.stories.ts +187 -0
  64. package/src/components/editor/molecules/EditorBorder/EditorBorder.test.ts +456 -0
  65. package/src/components/editor/molecules/EditorBorder/EditorBorder.vue +58 -2
  66. package/src/components/editor/molecules/EditorColor/EditorColor.stories.ts +563 -0
  67. package/src/components/editor/molecules/EditorColor/EditorColor.test.ts +689 -0
  68. package/src/components/editor/molecules/EditorColor/EditorColorGradient.test.ts +793 -0
  69. package/src/components/editor/molecules/EditorColor/_ColorField.vue +7 -29
  70. package/src/components/editor/molecules/EditorColor/_ThemeRoleSwatches.vue +57 -0
  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 +638 -0
  182. package/src/components/organisms/ColorPicker/gradient.test.ts +67 -0
  183. package/src/components/organisms/ColorPicker/gradientGeometry.test.ts +206 -0
  184. package/src/components/organisms/Combobox/Combobox.stories.ts +234 -0
  185. package/src/components/organisms/Combobox/Combobox.test.ts +512 -0
  186. package/src/components/organisms/Combobox/Combobox.vue +1 -1
  187. package/src/components/organisms/ComboboxPanel/ComboboxPanel.stories.ts +158 -0
  188. package/src/components/organisms/ComboboxPanel/ComboboxPanel.test.ts +524 -0
  189. package/src/components/organisms/ComboboxPanel/ComboboxPanel.vue +1 -1
  190. package/src/components/organisms/CountrySelect/CountrySelect.stories.ts +139 -0
  191. package/src/components/organisms/CountrySelect/CountrySelect.test.ts +149 -0
  192. package/src/components/organisms/DatePicker/DatePicker.stories.ts +163 -0
  193. package/src/components/organisms/DatePicker/DatePicker.test.ts +214 -0
  194. package/src/components/organisms/DropdownButton/DropdownButton.stories.ts +123 -0
  195. package/src/components/organisms/DropdownButton/DropdownButton.test.ts +379 -0
  196. package/src/components/organisms/FontPicker/FontPicker.stories.ts +476 -0
  197. package/src/components/organisms/FontPicker/FontPicker.test.ts +873 -0
  198. package/src/components/organisms/Header/Header.stories.ts +75 -0
  199. package/src/components/organisms/Header/Header.test.ts +233 -0
  200. package/src/components/organisms/IconPicker/IconPicker.stories.ts +734 -0
  201. package/src/components/organisms/IconPicker/IconPicker.test.ts +669 -0
  202. package/src/components/organisms/IconPicker/_IconPickerGrid.test.ts +184 -0
  203. package/src/components/organisms/IconPicker/_IconPickerRail.test.ts +102 -0
  204. package/src/components/organisms/IconPicker/useIconPickerNavigation.test.ts +215 -0
  205. package/src/components/organisms/Modal/Modal.stories.ts +350 -0
  206. package/src/components/organisms/Modal/Modal.test.ts +275 -0
  207. package/src/components/organisms/PickerPanel/PickerPanel.stories.ts +452 -0
  208. package/src/components/organisms/PickerPanel/PickerPanel.test.ts +358 -0
  209. package/src/components/organisms/ProgressBar/ProgressBar.stories.ts +100 -0
  210. package/src/components/organisms/ProgressBar/ProgressBar.test.ts +417 -0
  211. package/src/components/organisms/ReferenceCard/ReferenceCard.stories.ts +191 -0
  212. package/src/components/organisms/ReferenceCard/ReferenceCard.test.ts +165 -0
  213. package/src/components/organisms/SocialMediaTemplate/SocialMediaTemplate.stories.ts +65 -0
  214. package/src/components/organisms/SocialMediaTemplate/SocialMediaTemplate.test.ts +466 -0
  215. package/src/components/organisms/StepperHeader/StepperHeader.stories.ts +91 -0
  216. package/src/components/organisms/StepperHeader/StepperHeader.test.ts +279 -0
  217. package/src/components/organisms/Table/Table.stories.ts +332 -0
  218. package/src/components/organisms/Table/Table.test.ts +320 -0
  219. package/src/components/organisms/TimelinePhaseblock/TimelinePhaseblock.stories.ts +198 -0
  220. package/src/components/organisms/TimelinePhaseblock/TimelinePhaseblock.test.ts +283 -0
  221. package/src/components/organisms/TimelineTrack/TimelineTrack.stories.ts +120 -0
  222. package/src/components/organisms/TimelineTrack/TimelineTrack.test.ts +91 -0
  223. package/src/components/organisms/Toaster/Toaster.stories.ts +51 -0
  224. package/src/components/organisms/Toaster/Toaster.test.ts +75 -0
  225. package/src/components/organisms/VideoControls/VideoControls.stories.ts +792 -0
  226. package/src/components/organisms/VideoControls/VideoControls.test.ts +1056 -0
  227. package/src/components/organisms/VideoPlayer/VideoPlayer.stories.ts +327 -0
  228. package/src/components/organisms/VideoPlayer/VideoPlayer.test.ts +439 -0
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import type { EditorColorThemeRole } from '../EditorColor/colorClass'
3
+
2
4
  /**
3
5
  * The widths the slider can land on: the Tailwind class and the pixels it
4
6
  * renders. These are Tailwind's own defaults (`border` is 1px, then 2, 4 and 8)
@@ -29,6 +31,8 @@ export interface EditorBorderMessages {
29
31
  placeholder?: string
30
32
  /** Accessible name of the width slider. Defaults to `'Dikte'`. */
31
33
  width?: string
34
+ /** Label shown above the theme roles. Defaults to `'Huisstijl'`. */
35
+ themeLabel?: string
32
36
  }
33
37
 
34
38
  export interface NexxtEditorBorderProps {
@@ -52,12 +56,22 @@ export interface NexxtEditorBorderProps {
52
56
  * component.
53
57
  */
54
58
  messages?: EditorBorderMessages
59
+ /**
60
+ * Theme roles offered as swatches in the colour panel, the same ones
61
+ * NexxtEditorColor takes. A role is a shortcut to its hex, not a binding: a
62
+ * pick writes `border-[#hex]` exactly as a picked colour does, and a swatch
63
+ * shows as selected while the border's hex equals the role's, whatever the
64
+ * case. An entry with an unreadable hex is left out. Defaults to `[]`, which
65
+ * renders no theme section.
66
+ */
67
+ themeRoles?: EditorColorThemeRole[]
55
68
  }
56
69
 
57
70
  const DEFAULT_MESSAGES = {
58
71
  unit: 'px',
59
72
  placeholder: 'Kies kleur...',
60
73
  width: 'Dikte',
74
+ themeLabel: 'Huisstijl',
61
75
  } satisfies Required<EditorBorderMessages>
62
76
 
63
77
  /**
@@ -73,10 +87,17 @@ import { computed } from 'vue'
73
87
  import NexxtEditorSettingsBlock from '../../atoms/EditorSettingsBlock/EditorSettingsBlock.vue'
74
88
  import NexxtSlider from '../../../atoms/Slider/Slider.vue'
75
89
  import NexxtColorPicker from '../../../organisms/ColorPicker/ColorPicker.vue'
90
+ import NexxtThemeRoleSwatches from '../EditorColor/_ThemeRoleSwatches.vue'
91
+ import { ROLE_HEX_PATTERN } from '../EditorColor/colorClass'
76
92
 
77
93
  defineOptions({ name: 'NexxtEditorBorder' })
78
94
 
79
- const { label = 'Rand', switchable = true, messages } = defineProps<NexxtEditorBorderProps>()
95
+ const {
96
+ label = 'Rand',
97
+ switchable = true,
98
+ messages,
99
+ themeRoles = [],
100
+ } = defineProps<NexxtEditorBorderProps>()
80
101
 
81
102
  /**
82
103
  * The border as the editor needs it: one class string that carries both halves
@@ -164,6 +185,32 @@ const picked = computed({
164
185
  },
165
186
  })
166
187
 
188
+ /** The picker throws on a colour it cannot read, so a role's hex is checked first. */
189
+ const offeredRoles = computed(() => themeRoles.filter((entry) => ROLE_HEX_PATTERN.test(entry.hex)))
190
+
191
+ /**
192
+ * A colour without its alpha pair, as `#rrggbb`. The picker is opened without
193
+ * an alpha channel here, so a role's alpha would be stored but never shown;
194
+ * the role is offered, written and compared by its opaque colour instead.
195
+ */
196
+ const opaque = (value: string) => value.slice(0, 7).toLowerCase()
197
+
198
+ /**
199
+ * A role is only a shortcut to a hex here, so its swatch is pressed by colour:
200
+ * two roles that share a hex are pressed together, and a hand-picked colour
201
+ * equal to a role's presses that role too.
202
+ */
203
+ const isRolePressed = (entry: EditorColorThemeRole) =>
204
+ hex.value !== '' && opaque(hex.value) === opaque(entry.hex)
205
+
206
+ /**
207
+ * Written through the picker's own path, so a role's hex is stored as any
208
+ * picked hex is: six digits, in the case the consumer passed it.
209
+ */
210
+ const pickRole = (entry: EditorColorThemeRole) => {
211
+ picked.value = entry.hex.slice(0, 7)
212
+ }
213
+
167
214
  const displayValue = computed(() => `${EDITOR_BORDER_STEPS[stepIndex.value].px} ${copy.value.unit}`)
168
215
  </script>
169
216
 
@@ -216,7 +263,16 @@ const displayValue = computed(() => `${EDITOR_BORDER_STEPS[stepIndex.value].px}
216
263
  :formats="['hex']"
217
264
  :show-alpha="false"
218
265
  :placeholder="copy.placeholder"
219
- />
266
+ >
267
+ <template v-if="offeredRoles.length" #presets>
268
+ <NexxtThemeRoleSwatches
269
+ :roles="offeredRoles"
270
+ :label="copy.themeLabel"
271
+ :is-pressed="isRolePressed"
272
+ @pick="pickRole"
273
+ />
274
+ </template>
275
+ </NexxtColorPicker>
220
276
  </div>
221
277
  </NexxtEditorSettingsBlock>
222
278
  </template>
@@ -0,0 +1,563 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import { expect, screen, userEvent, waitFor, within } from 'storybook/test'
3
+ import { ref } from 'vue'
4
+ import EditorColor from './EditorColor.vue'
5
+ import type { EditorColorThemeRole, EditorColorValue } from './EditorColor.vue'
6
+
7
+ /**
8
+ * The collapsing wrapper, found by the grid row it animates rather than by any
9
+ * styling class, so restyling the block cannot quietly break the assertions.
10
+ */
11
+ const contentWrapper = (canvasElement: HTMLElement) =>
12
+ canvasElement.querySelector('[class*="grid-rows-"]') as HTMLElement
13
+
14
+ /**
15
+ * The roles as a consumer would pass them: its own labels, and the hex each role
16
+ * has in the customer's theme right now. Heading and text share a hex, so only
17
+ * the tooltip tells their swatches apart.
18
+ */
19
+ const THEME_ROLES: EditorColorThemeRole[] = [
20
+ { role: 'main', label: 'Hoofdkleur', hex: '#1777EA' },
21
+ { role: 'accent', label: 'Accentkleur', hex: '#F59E0B' },
22
+ { role: 'background', label: 'Achtergrond', hex: '#F0F4F8' },
23
+ { role: 'heading', label: 'Koppen', hex: '#1F2937' },
24
+ { role: 'text', label: 'Tekst', hex: '#1F2937' },
25
+ ]
26
+
27
+ export default {
28
+ title: 'Components/Editor Components/Molecules/Editor Color',
29
+ component: EditorColor,
30
+ decorators: [() => ({ template: '<div class="w-100 mx-auto"><story /></div>' })],
31
+ argTypes: {
32
+ label: { control: 'text' },
33
+ switchable: { control: 'boolean' },
34
+ property: { control: 'select', options: ['bg', 'text'] },
35
+ showAlpha: { control: 'boolean' },
36
+ },
37
+ } satisfies Meta<typeof EditorColor>
38
+
39
+ type Story = StoryObj<typeof EditorColor>
40
+
41
+ export const Default: Story = {
42
+ render: (args) => ({
43
+ components: { EditorColor },
44
+ setup() {
45
+ const color = ref<EditorColorValue>('bg-[#1777EA]')
46
+ return { args, color }
47
+ },
48
+ // The value is shown as-is: it is the Tailwind class the editor applies.
49
+ template: `
50
+ <div>
51
+ <EditorColor v-bind="args" v-model="color" />
52
+ <p class="small-normal text-gray-700">{{ color }}</p>
53
+ </div>
54
+ `,
55
+ }),
56
+ // The panel defaults to a fixed w-70, which is narrower than the full-width
57
+ // trigger in a settings column. jsdom has no layout, so the two widths can
58
+ // only be compared in a real browser.
59
+ play: async ({ canvasElement }) => {
60
+ const canvas = within(canvasElement)
61
+ const trigger = canvas.getByRole('button')
62
+
63
+ await userEvent.click(trigger)
64
+
65
+ // The floating container is found by the inline position it is given, not by
66
+ // a styling class: it is the only fixed-positioned ancestor of the panel.
67
+ await waitFor(() => {
68
+ const panel = canvasElement
69
+ .querySelector('[placeholder="#000000"]')!
70
+ .closest<HTMLElement>('div[style*="position: fixed"]')!
71
+
72
+ expect(panel.getBoundingClientRect().width).toBe(trigger.getBoundingClientRect().width)
73
+ })
74
+ },
75
+ }
76
+ export const WithSuggestions: Story = {
77
+ args: {
78
+ suggestions: ['#1777EA', '#F0F4F8', '#1F2937'],
79
+ },
80
+ render: (args) => ({
81
+ components: { EditorColor },
82
+ setup() {
83
+ const color = ref<EditorColorValue>('bg-[#1777EA]')
84
+ return { args, color }
85
+ },
86
+ template: `
87
+ <EditorColor v-bind="args" v-model="color" />
88
+ `,
89
+ }),
90
+ }
91
+
92
+ /** Picking a role writes the role class for the current property, not its hex. */
93
+ export const WithThemeRoles: Story = {
94
+ args: { themeRoles: THEME_ROLES },
95
+ render: (args) => ({
96
+ components: { EditorColor },
97
+ setup() {
98
+ const color = ref<EditorColorValue>('bg-[#1777EA]')
99
+ return { args, color }
100
+ },
101
+ template: `
102
+ <div>
103
+ <EditorColor v-bind="args" v-model="color" />
104
+ <p class="small-normal text-gray-700">{{ color }}</p>
105
+ </div>
106
+ `,
107
+ }),
108
+ play: async ({ canvasElement }) => {
109
+ const canvas = within(canvasElement)
110
+
111
+ // The trigger is named after its placeholder; a role button after its label.
112
+ await userEvent.click(canvas.getByRole('button', { name: 'Kies kleur...' }))
113
+
114
+ const main = await canvas.findByRole('button', { name: 'Hoofdkleur' })
115
+
116
+ // The literal has the hex of the main colour, and that selects nothing.
117
+ expect(main).toHaveAttribute('aria-pressed', 'false')
118
+
119
+ // The swatch carries no visible label; hovering it names the role. The
120
+ // tooltip is portalled to the body, outside the canvas, and reka hides the
121
+ // node that carries the role from the accessibility tree: the bubble around
122
+ // it is what the user sees.
123
+ await userEvent.hover(main)
124
+ const bubble = (await screen.findByRole('tooltip', { hidden: true })).parentElement
125
+ expect(bubble).toBeVisible()
126
+ expect(bubble).toHaveTextContent('Hoofdkleur')
127
+
128
+ await userEvent.click(main)
129
+
130
+ await waitFor(() => {
131
+ expect(canvas.getByText('bg-theme-main')).toBeInTheDocument()
132
+ expect(main).toHaveAttribute('aria-pressed', 'true')
133
+ expect(canvas.getByRole('button', { name: 'Kies kleur...' })).toHaveTextContent('Hoofdkleur')
134
+ })
135
+ },
136
+ }
137
+
138
+ /** A stored role class is shown by its label, with the swatch of the role's current hex. */
139
+ export const ThemeRoleSelected: Story = {
140
+ args: { themeRoles: THEME_ROLES, property: 'text', label: 'Tekstkleur' },
141
+ render: (args) => ({
142
+ components: { EditorColor },
143
+ setup() {
144
+ const color = ref<EditorColorValue>('text-theme-heading')
145
+ return { args, color }
146
+ },
147
+ template: `
148
+ <div>
149
+ <EditorColor v-bind="args" v-model="color" />
150
+ <p class="small-normal text-gray-700">{{ color }}</p>
151
+ </div>
152
+ `,
153
+ }),
154
+ play: async ({ canvasElement }) => {
155
+ const canvas = within(canvasElement)
156
+ const trigger = canvas.getByRole('button', { name: 'Kies kleur...' })
157
+
158
+ expect(trigger).toHaveTextContent('Koppen')
159
+
160
+ await userEvent.click(trigger)
161
+
162
+ // `Tekst` has the same hex as `Koppen`; only the stored role is selected.
163
+ expect(await canvas.findByRole('button', { name: 'Koppen' })).toHaveAttribute(
164
+ 'aria-pressed',
165
+ 'true',
166
+ )
167
+ expect(canvas.getByRole('button', { name: 'Tekst' })).toHaveAttribute('aria-pressed', 'false')
168
+
169
+ // The picker is handed the role's hex with an alpha pair; the field must not show it.
170
+ expect(canvasElement.querySelector('[placeholder="#000000"]')).toHaveValue('#1f2937')
171
+ },
172
+ }
173
+
174
+ /** The theme section sits above the suggestions; a suggestion still writes a literal colour. */
175
+ export const ThemeRolesWithSuggestions: Story = {
176
+ args: {
177
+ themeRoles: THEME_ROLES,
178
+ suggestions: ['#0F766E', '#F0F4F8', '#1F2937'],
179
+ },
180
+ render: (args) => ({
181
+ components: { EditorColor },
182
+ setup() {
183
+ const color = ref<EditorColorValue>('bg-theme-main')
184
+ return { args, color }
185
+ },
186
+ template: `
187
+ <div>
188
+ <EditorColor v-bind="args" v-model="color" />
189
+ <p class="small-normal text-gray-700">{{ color }}</p>
190
+ </div>
191
+ `,
192
+ }),
193
+ play: async ({ canvasElement }) => {
194
+ const canvas = within(canvasElement)
195
+
196
+ await userEvent.click(canvas.getByRole('button', { name: 'Kies kleur...' }))
197
+
198
+ const title = await canvas.findByText('Huisstijl')
199
+ const suggestionsTitle = canvas.getByText('Gebruikte kleuren')
200
+
201
+ expect(
202
+ title.compareDocumentPosition(suggestionsTitle) & Node.DOCUMENT_POSITION_FOLLOWING,
203
+ ).toBeTruthy()
204
+
205
+ await userEvent.click(canvas.getByRole('button', { name: 'Kies kleur #0f766e' }))
206
+
207
+ await waitFor(() => {
208
+ expect(canvas.getByText('bg-[#0f766e]')).toBeInTheDocument()
209
+ expect(canvas.getByRole('button', { name: 'Hoofdkleur' })).toHaveAttribute(
210
+ 'aria-pressed',
211
+ 'false',
212
+ )
213
+ })
214
+ },
215
+ }
216
+
217
+ /**
218
+ * With `showAlpha` the picker offers an opacity slider. Below full opacity the
219
+ * class carries an 8-digit hex; a theme role has no opacity, so picking one
220
+ * hides the slider.
221
+ */
222
+ export const WithAlpha: Story = {
223
+ args: { showAlpha: true, themeRoles: THEME_ROLES },
224
+ render: (args) => ({
225
+ components: { EditorColor },
226
+ setup() {
227
+ const color = ref<EditorColorValue>('bg-[#1777EA80]')
228
+ return { args, color }
229
+ },
230
+ template: `
231
+ <div>
232
+ <EditorColor v-bind="args" v-model="color" />
233
+ <p class="small-normal text-gray-700">{{ color }}</p>
234
+ </div>
235
+ `,
236
+ }),
237
+ play: async ({ canvasElement }) => {
238
+ const canvas = within(canvasElement)
239
+
240
+ await userEvent.click(canvas.getByRole('button', { name: 'Kies kleur...' }))
241
+
242
+ // The slider is named by its visible label and reads the opacity out as a percentage.
243
+ const alpha = await canvas.findByRole('slider', { name: 'Dekking' })
244
+ expect(alpha).toHaveAttribute('aria-valuetext', '50%')
245
+
246
+ alpha.focus()
247
+ await userEvent.keyboard('{ArrowRight}')
248
+
249
+ // 51% of 255 rounds to 0x82; the picker writes hex in lower case.
250
+ await waitFor(() => expect(canvas.getByText('bg-[#1777ea82]')).toBeInTheDocument())
251
+
252
+ await userEvent.click(canvas.getByRole('button', { name: 'Hoofdkleur' }))
253
+
254
+ await waitFor(() => {
255
+ expect(canvas.getByText('bg-theme-main')).toBeInTheDocument()
256
+ expect(canvas.queryByRole('slider', { name: 'Dekking' })).not.toBeInTheDocument()
257
+ })
258
+ },
259
+ }
260
+
261
+ /**
262
+ * The flow from the editor: a colour picked, made translucent with the slider,
263
+ * then replaced by a theme role in one click.
264
+ */
265
+ export const WithAlphaRolePicked: Story = {
266
+ args: { showAlpha: true, switchable: true, themeRoles: THEME_ROLES, suggestions: ['#00aa00'] },
267
+ render: (args) => ({
268
+ components: { EditorColor },
269
+ setup() {
270
+ const color = ref<EditorColorValue>('bg-[#1777EA]')
271
+ const enabled = ref(true)
272
+ return { args, color, enabled }
273
+ },
274
+ template: `
275
+ <div>
276
+ <EditorColor v-bind="args" v-model="color" v-model:enabled="enabled" />
277
+ <p class="small-normal text-gray-700">{{ color }}</p>
278
+ </div>
279
+ `,
280
+ }),
281
+ play: async ({ canvasElement }) => {
282
+ const canvas = within(canvasElement)
283
+
284
+ await userEvent.click(canvas.getByRole('button', { name: 'Kies kleur...' }))
285
+
286
+ await userEvent.click(await canvas.findByRole('button', { name: 'Kies kleur #00aa00' }))
287
+ await waitFor(() => expect(canvas.getByText('bg-[#00aa00]')).toBeInTheDocument())
288
+
289
+ const alpha = canvas.getByRole('slider', { name: 'Dekking' })
290
+ alpha.focus()
291
+ for (let step = 0; step < 10; step++) await userEvent.keyboard('{ArrowLeft}')
292
+ await waitFor(() => expect(canvas.getByText('bg-[#00aa00e6]')).toBeInTheDocument())
293
+
294
+ await userEvent.click(canvas.getByRole('button', { name: 'Hoofdkleur' }))
295
+
296
+ await waitFor(() => {
297
+ expect(canvas.getByText('bg-theme-main')).toBeInTheDocument()
298
+ expect(canvas.queryByRole('slider', { name: 'Dekking' })).not.toBeInTheDocument()
299
+ })
300
+ },
301
+ }
302
+
303
+ export const Switchable: Story = {
304
+ args: { switchable: true },
305
+ render: (args) => ({
306
+ components: { EditorColor },
307
+ setup() {
308
+ const color = ref<EditorColorValue>('bg-[#1777EA]')
309
+ const enabled = ref(true)
310
+ return { args, color, enabled }
311
+ },
312
+ template: `
313
+ <div>
314
+ <EditorColor v-bind="args" v-model="color" v-model:enabled="enabled" />
315
+ <p class="small-normal text-gray-700">{{ color }}</p>
316
+ <p class="small-normal text-gray-700">{{ enabled }}</p>
317
+ </div>
318
+ `,
319
+ }),
320
+ play: async ({ canvasElement }) => {
321
+ const canvas = within(canvasElement)
322
+ const content = contentWrapper(canvasElement)
323
+
324
+ expect(content).not.toHaveAttribute('inert')
325
+
326
+ await userEvent.click(canvas.getByRole('switch'))
327
+
328
+ await waitFor(() => {
329
+ expect(content).toHaveAttribute('inert')
330
+ expect(canvas.getByText('false')).toBeInTheDocument()
331
+ })
332
+
333
+ // The colour survives the collapse: only the switch state changed.
334
+ expect(canvas.getByText('bg-[#1777EA]')).toBeInTheDocument()
335
+ },
336
+ }
337
+
338
+ export const SwitchableOff: Story = {
339
+ args: { switchable: true },
340
+ render: (args) => ({
341
+ components: { EditorColor },
342
+ setup() {
343
+ const color = ref<EditorColorValue>('bg-[#1777EA]')
344
+ const enabled = ref(false)
345
+ return { args, color, enabled }
346
+ },
347
+ template: `
348
+ <div>
349
+ <EditorColor v-bind="args" v-model="color" v-model:enabled="enabled" />
350
+ <p class="small-normal text-gray-700">{{ color }}</p>
351
+ </div>
352
+ `,
353
+ }),
354
+ }
355
+
356
+ export const TextProperty: Story = {
357
+ args: { property: 'text', label: 'Tekstkleur' },
358
+ render: (args) => ({
359
+ components: { EditorColor },
360
+ setup() {
361
+ const color = ref<EditorColorValue>('text-[#1777EA]')
362
+ return { args, color }
363
+ },
364
+ template: `
365
+ <div>
366
+ <EditorColor v-bind="args" v-model="color" />
367
+ <p class="small-normal text-gray-700">{{ color }}</p>
368
+ </div>
369
+ `,
370
+ }),
371
+ }
372
+
373
+ /** A stored value the component cannot parse shows the placeholder instead. */
374
+ export const Empty: Story = {
375
+ render: (args) => ({
376
+ components: { EditorColor },
377
+ setup() {
378
+ const color = ref('bg-red-500' as EditorColorValue)
379
+ return { args, color }
380
+ },
381
+ template: `
382
+ <div>
383
+ <EditorColor v-bind="args" v-model="color" />
384
+ <p class="small-normal text-gray-700">{{ color }}</p>
385
+ </div>
386
+ `,
387
+ }),
388
+ }
389
+
390
+ /**
391
+ * The gradient stories render in a 256px column, the width of the editor's
392
+ * settings column, so the panel's content is the 224px the layout is made for.
393
+ */
394
+ const gradientRender = (initial: EditorColorValue) => (args: Story['args']) => ({
395
+ components: { EditorColor },
396
+ setup() {
397
+ const color = ref<EditorColorValue>(initial)
398
+ return { args, color }
399
+ },
400
+ template: `
401
+ <div class="w-64">
402
+ <EditorColor v-bind="args" v-model="color" />
403
+ <p class="mt-2 extra-small-normal break-all text-gray-700">{{ color }}</p>
404
+ </div>
405
+ `,
406
+ })
407
+
408
+ /** The panel, found by the stop bar it always contains in the gradient mode. */
409
+ const gradientPanel = (canvasElement: HTMLElement) =>
410
+ canvasElement
411
+ .querySelector('[data-gradient-bar]')!
412
+ .closest<HTMLElement>('div[style*="position: fixed"]')!
413
+
414
+ /** Opens the picker and checks that the panel fits: no scrolling, nothing sticking out. */
415
+ const openAndMeasure = async (canvasElement: HTMLElement) => {
416
+ const trigger = within(canvasElement).getAllByRole('button')[0]!
417
+ await userEvent.click(trigger)
418
+ await waitFor(() => expect(canvasElement.querySelector('[data-gradient-bar]')).not.toBeNull())
419
+ const panel = gradientPanel(canvasElement)
420
+ const surface = panel.querySelector<HTMLElement>('.rounded-xl')!
421
+ // Layout pixels, not the bounding box: the test canvas may be zoomed. The
422
+ // spec expects ~560px at a content width of 224px, well within 720px; the
423
+ // measured height is the guard, so the panel cannot quietly grow.
424
+ expect(surface.offsetHeight).toBeLessThanOrEqual(PANEL_HEIGHT)
425
+ expect(surface.scrollHeight).toBeLessThanOrEqual(surface.clientHeight)
426
+ // The panel is exactly as wide as the trigger: nothing in it is wider.
427
+ expect(surface.offsetWidth).toBe(trigger.offsetWidth)
428
+ expect(surface.scrollWidth).toBeLessThanOrEqual(surface.clientWidth)
429
+ // The preview square takes the full content width: 224px in this column.
430
+ const square = panel.querySelector<HTMLElement>('[data-gradient-preview]')!
431
+ expect(square.offsetWidth).toBe(224)
432
+ expect(square.offsetHeight).toBe(224)
433
+ // Every handle lies in the square; half a handle may cross its edge.
434
+ const box = square.getBoundingClientRect()
435
+ square
436
+ .querySelectorAll<HTMLElement>('[data-stop-point], [data-gradient-center]')
437
+ .forEach((handle) => {
438
+ const r = handle.getBoundingClientRect()
439
+ const x = r.left + r.width / 2
440
+ const y = r.top + r.height / 2
441
+ expect(x).toBeGreaterThanOrEqual(box.left - 0.5)
442
+ expect(x).toBeLessThanOrEqual(box.right + 0.5)
443
+ expect(y).toBeGreaterThanOrEqual(box.top - 0.5)
444
+ expect(y).toBeLessThanOrEqual(box.bottom + 0.5)
445
+ })
446
+ // No field in a row overflows or clips its value. A role name longer than
447
+ // "Hoofdkleur" -- the one the layout is measured with -- may truncate.
448
+ const fields = panel.querySelectorAll<HTMLInputElement>('[data-gradient-row] input')
449
+ const measured = [...fields].filter((input) => !input.readOnly || input.value === 'Hoofdkleur')
450
+ measured.forEach((input) => {
451
+ expect(input.scrollWidth, input.getAttribute('aria-label') ?? '').toBeLessThanOrEqual(
452
+ input.clientWidth,
453
+ )
454
+ })
455
+ return panel
456
+ }
457
+
458
+ /** Measured height of the tallest gradient panel (three stops, a role) in the 224px column. */
459
+ const PANEL_HEIGHT = 544
460
+
461
+ /**
462
+ * A linear gradient: the to-point in the preview sets the angle, two stops in
463
+ * the bar and the list.
464
+ */
465
+ export const GradientLinear: Story = {
466
+ // Axe runs on the open panel, after the play function.
467
+ parameters: { a11y: { test: 'error' } },
468
+ args: {
469
+ modes: ['solid', 'gradient'],
470
+ themeRoles: THEME_ROLES,
471
+ suggestions: ['#7F56D9', '#432E73'],
472
+ },
473
+ render: gradientRender('bg-linear-135 from-[#7f56d9] to-[#432e73cc]'),
474
+ play: async ({ canvasElement }) => {
475
+ const panel = await openAndMeasure(canvasElement)
476
+ const angle = within(panel).getByRole('slider', { name: 'Hoek' })
477
+ expect(angle).toHaveAttribute('aria-valuetext', '135 graden')
478
+ expect(within(panel).getAllByRole('group', { name: /^Kleurstop \d van 2$/ })).toHaveLength(2)
479
+ },
480
+ }
481
+
482
+ /** A radial gradient with its centre off the middle: the centre point and the stops on one ray. */
483
+ export const GradientRadial: Story = {
484
+ // Axe runs on the open panel, after the play function.
485
+ parameters: { a11y: { test: 'error' } },
486
+ args: { modes: ['solid', 'gradient'], themeRoles: THEME_ROLES },
487
+ render: gradientRender('bg-radial-[at_30%_70%] from-theme-accent to-[#ffffff00]'),
488
+ play: async ({ canvasElement }) => {
489
+ const panel = await openAndMeasure(canvasElement)
490
+ expect(within(panel).getByRole('slider', { name: 'Middelpunt' })).toHaveAttribute(
491
+ 'aria-valuetext',
492
+ '30 % van links, 70 % van boven',
493
+ )
494
+ // The first stop is a theme role: in its popover the opacity is locked, with the hint.
495
+ const [swatch] = within(panel).getAllByRole('button', { name: 'Kleur kiezen' })
496
+ await userEvent.click(swatch!)
497
+ const dialog = await within(panel).findByRole('dialog', { name: 'Kleur van kleurstop 1' })
498
+ const opacity = within(dialog).getByRole('slider', { name: 'Dekking' })
499
+ expect(opacity).toHaveAttribute('aria-disabled', 'true')
500
+ expect(opacity).toHaveAccessibleDescription('Huisstijlkleuren zijn altijd dekkend.')
501
+ },
502
+ }
503
+
504
+ /**
505
+ * Three stops, the most the gradient takes: adding is unavailable, with the
506
+ * reason, and removing is possible. The middle stop is a theme role and the
507
+ * last one translucent, so its hex has eight digits. This is the tallest panel,
508
+ * and it carries the height check. The play also opens a stop's colour popover
509
+ * and closes it with Escape.
510
+ */
511
+ export const GradientThreeStops: Story = {
512
+ // Axe runs on the open panel, after the play function.
513
+ parameters: { a11y: { test: 'error' } },
514
+ args: {
515
+ modes: ['solid', 'gradient'],
516
+ themeRoles: THEME_ROLES,
517
+ suggestions: ['#7F56D9', '#432E73', '#F0F4F8', '#1F2937', '#F59E0B', '#1777EA'],
518
+ },
519
+ render: gradientRender('bg-linear-90 from-[#7f56d9] via-theme-main via-40% to-[#432e73cc]'),
520
+ play: async ({ canvasElement }) => {
521
+ const panel = await openAndMeasure(canvasElement)
522
+ const add = within(panel).getByRole('button', { name: 'Kleurstop toevoegen' })
523
+ expect(add).toHaveAttribute('aria-disabled', 'true')
524
+ expect(add).toHaveAccessibleDescription('Een verloop heeft maximaal 3 kleurstops.')
525
+ within(panel)
526
+ .getAllByRole('button', { name: 'Verwijderen' })
527
+ .forEach((remove) => expect(remove).toBeEnabled())
528
+ expect(within(panel).getAllByRole('slider', { name: /^Kleurstop \d van 3$/ })).toHaveLength(3)
529
+
530
+ const [swatch] = within(panel).getAllByRole('button', { name: 'Kleur kiezen' })
531
+ await userEvent.click(swatch!)
532
+ const dialog = await within(panel).findByRole('dialog', { name: 'Kleur van kleurstop 1' })
533
+ expect(within(dialog).getByRole('textbox', { name: 'Hex' })).toBeInTheDocument()
534
+ await userEvent.keyboard('{Escape}')
535
+ await waitFor(() => expect(within(panel).queryByRole('dialog')).toBeNull())
536
+ expect(swatch).toHaveFocus()
537
+ expect(canvasElement.querySelector('[data-gradient-bar]')).not.toBeNull()
538
+ },
539
+ }
540
+
541
+ /**
542
+ * Only the gradient, without the Effen | Verloop switch. The stored value is a
543
+ * solid colour, so the panel opens on its fade -- the colour to the same hex
544
+ * at `00` -- and nothing is written until the first change.
545
+ */
546
+ export const GradientOnly: Story = {
547
+ // Axe runs on the open panel, after the play function.
548
+ parameters: { a11y: { test: 'error' } },
549
+ args: { modes: ['gradient'], themeRoles: THEME_ROLES },
550
+ render: gradientRender('bg-[#7f56d9]'),
551
+ play: async ({ canvasElement }) => {
552
+ const panel = await openAndMeasure(canvasElement)
553
+ expect(within(panel).queryByRole('button', { name: 'Effen' })).toBeNull()
554
+ const hexes = within(panel).getAllByRole('textbox', { name: 'Hex' })
555
+ expect(hexes.map((input) => (input as HTMLInputElement).value)).toEqual([
556
+ '#7f56d9',
557
+ '#7f56d900',
558
+ ])
559
+ expect(canvasElement).toHaveTextContent('bg-[#7f56d9]')
560
+ // Until the first change the trigger shows the stored colour, not the placeholder.
561
+ expect(within(canvasElement).getAllByRole('button')[0]).toHaveTextContent('#7f56d9')
562
+ },
563
+ }