@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
@@ -56,8 +56,8 @@ const OPAQUE_ALPHA_PATTERN = /^(#[0-9A-Fa-f]{6})[fF]{2}$/
56
56
 
57
57
  <script lang="ts" setup generic="P extends string">
58
58
  import { computed, ref, watch } from 'vue'
59
- import NexxtTooltip from '../../../atoms/Tooltip/Tooltip.vue'
60
59
  import NexxtColorPicker from '../../../organisms/ColorPicker/ColorPicker.vue'
60
+ import NexxtThemeRoleSwatches from './_ThemeRoleSwatches.vue'
61
61
  import type { ColorPickerGradient } from '../../../organisms/ColorPicker/ColorPicker.vue'
62
62
  import { clampCenter } from '../../../organisms/ColorPicker/gradientGeometry'
63
63
  import { COLOR_ROLES, ROLE_HEX_PATTERN, isHex, parseColorClass } from './colorClass'
@@ -412,34 +412,12 @@ watch(
412
412
  @close="onClose"
413
413
  >
414
414
  <template v-if="offeredRoles.length" #presets="{ preset, selectPreset }">
415
- <div class="flex flex-col gap-1">
416
- <span class="extra-small-normal text-gray-500">{{ themeLabel }}</span>
417
- <!--
418
- Same swatches and row as the picker's suggestions. Two roles often
419
- share a hex (heading and text), so the tooltip is what tells them
420
- apart; it repeats the accessible name rather than adding copy.
421
- -->
422
- <div class="flex flex-wrap gap-1.5">
423
- <NexxtTooltip v-for="entry in offeredRoles" :key="entry.role" disable-closing-trigger>
424
- <button
425
- type="button"
426
- :aria-label="entry.label"
427
- :aria-pressed="isRolePressed(entry.role, preset)"
428
- class="cursor-pointer rounded-md p-0.5 ring-gray-200 transition-shadow hover:ring-2 hover:ring-cornflower-blue-400 focus-visible:ring-2 focus-visible:ring-cornflower-blue-500 focus-visible:outline-none aria-pressed:ring-2 aria-pressed:ring-cornflower-blue-500"
429
- @click="onRoleClick(entry, selectPreset)"
430
- >
431
- <span
432
- aria-hidden="true"
433
- class="block size-6 rounded-sm shadow-[inset_0_0_0_1px_rgba(0,0,0,0.1)]"
434
- :style="{ backgroundColor: entry.hex }"
435
- />
436
- </button>
437
- <template #tooltip>
438
- <span class="small-normal text-gray-900">{{ entry.label }}</span>
439
- </template>
440
- </NexxtTooltip>
441
- </div>
442
- </div>
415
+ <NexxtThemeRoleSwatches
416
+ :roles="offeredRoles"
417
+ :label="themeLabel"
418
+ :is-pressed="(entry) => isRolePressed(entry.role, preset)"
419
+ @pick="(entry) => onRoleClick(entry, selectPreset)"
420
+ />
443
421
  </template>
444
422
  </NexxtColorPicker>
445
423
  </template>
@@ -0,0 +1,57 @@
1
+ <script lang="ts" setup>
2
+ import NexxtTooltip from '../../../atoms/Tooltip/Tooltip.vue'
3
+ import type { EditorColorThemeRole } from './colorClass'
4
+
5
+ /**
6
+ * The theme roles as one row of swatches under a section title, for the
7
+ * `presets` slot of NexxtColorPicker. What a pick writes, and when a swatch is
8
+ * pressed, is up to the field that renders it: NexxtEditorColor writes the role
9
+ * class, NexxtEditorBorder the role's hex.
10
+ */
11
+ defineOptions({ name: 'NexxtThemeRoleSwatches' })
12
+
13
+ defineProps<{
14
+ /** The roles to show, already stripped of entries with an unreadable hex. */
15
+ roles: EditorColorThemeRole[]
16
+ /** Section title above the swatches. */
17
+ label: string
18
+ /** Whether the swatch of a role shows as selected. */
19
+ isPressed: (entry: EditorColorThemeRole) => boolean
20
+ }>()
21
+
22
+ const emit = defineEmits<{
23
+ /** A swatch was clicked, or activated with the keyboard. */
24
+ pick: [entry: EditorColorThemeRole]
25
+ }>()
26
+ </script>
27
+
28
+ <template>
29
+ <div class="flex flex-col gap-1">
30
+ <span class="extra-small-normal text-gray-500">{{ label }}</span>
31
+ <!--
32
+ Same swatches and row as the picker's suggestions. Two roles often
33
+ share a hex (heading and text), so the tooltip is what tells them
34
+ apart; it repeats the accessible name rather than adding copy.
35
+ -->
36
+ <div class="flex flex-wrap gap-1.5">
37
+ <NexxtTooltip v-for="entry in roles" :key="entry.role" disable-closing-trigger>
38
+ <button
39
+ type="button"
40
+ :aria-label="entry.label"
41
+ :aria-pressed="isPressed(entry)"
42
+ class="cursor-pointer rounded-md p-0.5 ring-gray-200 transition-shadow hover:ring-2 hover:ring-cornflower-blue-400 focus-visible:ring-2 focus-visible:ring-cornflower-blue-500 focus-visible:outline-none aria-pressed:ring-2 aria-pressed:ring-cornflower-blue-500"
43
+ @click="emit('pick', entry)"
44
+ >
45
+ <span
46
+ aria-hidden="true"
47
+ class="block size-6 rounded-sm shadow-[inset_0_0_0_1px_rgba(0,0,0,0.1)]"
48
+ :style="{ backgroundColor: entry.hex }"
49
+ />
50
+ </button>
51
+ <template #tooltip>
52
+ <span class="small-normal text-gray-900">{{ entry.label }}</span>
53
+ </template>
54
+ </NexxtTooltip>
55
+ </div>
56
+ </div>
57
+ </template>
@@ -0,0 +1,303 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import {
3
+ gradientClassToCss,
4
+ looksLikeGradientClass,
5
+ parseGradientClass,
6
+ serializeGradientClass,
7
+ type GradientClass,
8
+ } from './gradientClass'
9
+ import type { EditorColorRole } from './colorClass'
10
+
11
+ describe('parseGradientClass', () => {
12
+ it('reads a linear gradient with two stops at their default positions', () => {
13
+ expect(parseGradientClass('bg-linear-135 from-[#7f56d9] to-[#432e73]')).toEqual({
14
+ type: 'linear',
15
+ angle: 135,
16
+ stops: [
17
+ { color: { hex: '#7f56d9' }, position: 0 },
18
+ { color: { hex: '#432e73' }, position: 100 },
19
+ ],
20
+ })
21
+ })
22
+
23
+ it('reads a via stop, roles and explicit positions', () => {
24
+ expect(
25
+ parseGradientClass(
26
+ 'bg-linear-0 from-theme-main from-10% via-[#ffffff80] via-40% to-theme-text to-90%',
27
+ ),
28
+ ).toEqual({
29
+ type: 'linear',
30
+ angle: 0,
31
+ stops: [
32
+ { color: { role: 'main' }, position: 10 },
33
+ { color: { hex: '#ffffff80' }, position: 40 },
34
+ { color: { role: 'text' }, position: 90 },
35
+ ],
36
+ })
37
+ })
38
+
39
+ it('reads a centred radial gradient and one with a centre elsewhere', () => {
40
+ expect(parseGradientClass('bg-radial from-[#000000] to-[#ffffff]')).toMatchObject({
41
+ type: 'radial',
42
+ x: 50,
43
+ y: 50,
44
+ })
45
+ expect(parseGradientClass('bg-radial-[at_30%_70%] from-[#000000] to-[#ffffff]')).toMatchObject({
46
+ type: 'radial',
47
+ x: 30,
48
+ y: 70,
49
+ })
50
+ })
51
+
52
+ it('normalizes hexes: lower case, an opaque ff dropped', () => {
53
+ expect(parseGradientClass('bg-linear-90 from-[#AABBCCFF] to-[#AABBCC80]')?.stops).toEqual([
54
+ { color: { hex: '#aabbcc' }, position: 0 },
55
+ { color: { hex: '#aabbcc80' }, position: 100 },
56
+ ])
57
+ })
58
+
59
+ it('accepts the classes in another order', () => {
60
+ expect(parseGradientClass('to-[#ffffff] from-[#000000] bg-linear-45')).toMatchObject({
61
+ angle: 45,
62
+ })
63
+ })
64
+
65
+ it.each([
66
+ ['empty', ''],
67
+ ['undefined', undefined],
68
+ ['a solid colour', 'bg-[#ffffff]'],
69
+ ['no type', 'from-[#000000] to-[#ffffff]'],
70
+ ['no from', 'bg-linear-90 to-[#ffffff]'],
71
+ ['no to', 'bg-linear-90 from-[#000000]'],
72
+ ['an unknown role', 'bg-linear-90 from-theme-brand to-[#ffffff]'],
73
+ ['an angle of 360', 'bg-linear-360 from-[#000000] to-[#ffffff]'],
74
+ ['an angle with a leading zero', 'bg-linear-045 from-[#000000] to-[#ffffff]'],
75
+ ['a position above 100', 'bg-linear-90 from-[#000000] to-[#ffffff] to-101%'],
76
+ ['a via position without via', 'bg-linear-90 from-[#000000] via-40% to-[#ffffff]'],
77
+ ['two types', 'bg-linear-90 bg-radial from-[#000000] to-[#ffffff]'],
78
+ ['a stop given twice', 'bg-linear-90 from-[#000000] from-[#111111] to-[#ffffff]'],
79
+ ['an unknown class', 'bg-linear-90 from-[#000000] to-[#ffffff] rounded'],
80
+ ['a short hex', 'bg-linear-90 from-[#000] to-[#ffffff]'],
81
+ ['positions that run backwards', 'bg-linear-90 from-[#000000] from-80% to-[#ffffff] to-20%'],
82
+ ['oklab in the radial value', 'bg-radial-[at_30%_70%_in_oklab] from-[#000000] to-[#ffffff]'],
83
+ ['a centre above 100', 'bg-radial-[at_130%_70%] from-[#000000] to-[#ffffff]'],
84
+ ['a direction keyword', 'bg-linear-to-b from-[#000000] to-[#ffffff]'],
85
+ ])('returns null for %s', (_, value) => {
86
+ expect(parseGradientClass(value)).toBeNull()
87
+ })
88
+ })
89
+
90
+ describe('serializeGradientClass', () => {
91
+ it('writes the fixed order and leaves default positions out', () => {
92
+ expect(
93
+ serializeGradientClass({
94
+ type: 'linear',
95
+ angle: 135,
96
+ stops: [
97
+ { color: { hex: '#7f56d9' }, position: 0 },
98
+ { color: { role: 'main' }, position: 40 },
99
+ { color: { hex: '#432e73cc' }, position: 100 },
100
+ ],
101
+ }),
102
+ ).toBe('bg-linear-135 from-[#7f56d9] via-theme-main via-40% to-[#432e73cc]')
103
+ })
104
+
105
+ it('writes every position that differs from 0, 50 and 100', () => {
106
+ expect(
107
+ serializeGradientClass({
108
+ type: 'linear',
109
+ angle: 90,
110
+ stops: [
111
+ { color: { hex: '#000000' }, position: 5 },
112
+ { color: { hex: '#888888' }, position: 50 },
113
+ { color: { hex: '#ffffff' }, position: 95 },
114
+ ],
115
+ }),
116
+ ).toBe('bg-linear-90 from-[#000000] from-5% via-[#888888] to-[#ffffff] to-95%')
117
+ })
118
+
119
+ it('writes hexes in lower case without an opaque ff', () => {
120
+ expect(
121
+ serializeGradientClass({
122
+ type: 'linear',
123
+ angle: 0,
124
+ stops: [
125
+ { color: { hex: '#AABBCCFF' }, position: 0 },
126
+ { color: { hex: '#AABBCC00' }, position: 100 },
127
+ ],
128
+ }),
129
+ ).toBe('bg-linear-0 from-[#aabbcc] to-[#aabbcc00]')
130
+ })
131
+
132
+ it('writes bg-radial for a centred radial gradient, the arbitrary value otherwise', () => {
133
+ const stops = [
134
+ { color: { hex: '#000000' as const }, position: 0 },
135
+ { color: { hex: '#ffffff' as const }, position: 100 },
136
+ ]
137
+ expect(serializeGradientClass({ type: 'radial', x: 50, y: 50, stops })).toBe(
138
+ 'bg-radial from-[#000000] to-[#ffffff]',
139
+ )
140
+ expect(serializeGradientClass({ type: 'radial', x: 30, y: 70, stops })).toBe(
141
+ 'bg-radial-[at_30%_70%] from-[#000000] to-[#ffffff]',
142
+ )
143
+ })
144
+
145
+ it('rounds, clamps and wraps out-of-range numbers', () => {
146
+ expect(
147
+ serializeGradientClass({
148
+ type: 'linear',
149
+ angle: -10.4,
150
+ stops: [
151
+ { color: { hex: '#000000' }, position: -3 },
152
+ { color: { hex: '#ffffff' }, position: 99.6 },
153
+ ],
154
+ }),
155
+ ).toBe('bg-linear-350 from-[#000000] to-[#ffffff]')
156
+ expect(
157
+ serializeGradientClass({
158
+ type: 'linear',
159
+ angle: 360,
160
+ stops: [
161
+ { color: { hex: '#000000' }, position: 0 },
162
+ { color: { hex: '#ffffff' }, position: 100 },
163
+ ],
164
+ }),
165
+ ).toMatch(/^bg-linear-0 /)
166
+ })
167
+
168
+ it('never writes var() or a free linear-gradient', () => {
169
+ const value = serializeGradientClass({
170
+ type: 'linear',
171
+ angle: 45,
172
+ stops: [
173
+ { color: { role: 'accent' }, position: 0 },
174
+ { color: { role: 'background' }, position: 100 },
175
+ ],
176
+ })
177
+ expect(value).not.toMatch(/var\(|linear-gradient/)
178
+ })
179
+ })
180
+
181
+ describe('round trip', () => {
182
+ const cases: GradientClass[] = [
183
+ {
184
+ type: 'linear',
185
+ angle: 0,
186
+ stops: [
187
+ { color: { hex: '#000000' }, position: 0 },
188
+ { color: { hex: '#ffffff' }, position: 100 },
189
+ ],
190
+ },
191
+ {
192
+ type: 'linear',
193
+ angle: 359,
194
+ stops: [
195
+ { color: { role: 'heading' }, position: 20 },
196
+ { color: { hex: '#12345678' }, position: 20 },
197
+ { color: { role: 'text' }, position: 20 },
198
+ ],
199
+ },
200
+ {
201
+ type: 'radial',
202
+ x: 0,
203
+ y: 100,
204
+ stops: [
205
+ { color: { hex: '#7f56d900' }, position: 0 },
206
+ { color: { hex: '#7f56d9' }, position: 50 },
207
+ { color: { hex: '#432e73' }, position: 100 },
208
+ ],
209
+ },
210
+ {
211
+ type: 'radial',
212
+ x: 50,
213
+ y: 50,
214
+ stops: [
215
+ { color: { role: 'main' }, position: 33 },
216
+ { color: { role: 'main' }, position: 67 },
217
+ ],
218
+ },
219
+ ]
220
+
221
+ it.each(cases.map((value, index) => [index, value] as const))(
222
+ 'parse(serialize(x)) equals x for case %i',
223
+ (_, value) => {
224
+ expect(parseGradientClass(serializeGradientClass(value))).toEqual(value)
225
+ },
226
+ )
227
+
228
+ it.each([
229
+ 'bg-linear-135 from-[#7f56d9] via-theme-main via-40% to-[#432e73cc]',
230
+ 'bg-radial-[at_30%_70%] from-theme-accent to-[#ffffff00] to-80%',
231
+ 'bg-radial from-[#000000] from-10% to-[#ffffff]',
232
+ ])('serialize(parse(s)) equals s for %s', (value) => {
233
+ const parsed = parseGradientClass(value)
234
+ expect(parsed && serializeGradientClass(parsed)).toBe(value)
235
+ })
236
+ })
237
+
238
+ describe('looksLikeGradientClass', () => {
239
+ it('is true for a value led by a gradient type, readable or not', () => {
240
+ expect(looksLikeGradientClass('bg-linear-90 from-[#000000] to-[#ffffff]')).toBe(true)
241
+ expect(looksLikeGradientClass('bg-linear-999 garbage')).toBe(true)
242
+ expect(looksLikeGradientClass('bg-radial')).toBe(true)
243
+ expect(looksLikeGradientClass('bg-radial-[at_1%_2%] x')).toBe(true)
244
+ })
245
+
246
+ it('is false for a solid colour or nothing', () => {
247
+ expect(looksLikeGradientClass('bg-[#ffffff]')).toBe(false)
248
+ expect(looksLikeGradientClass('bg-theme-main')).toBe(false)
249
+ expect(looksLikeGradientClass('bg-radialx')).toBe(false)
250
+ expect(looksLikeGradientClass(undefined)).toBe(false)
251
+ })
252
+ })
253
+
254
+ describe('gradientClassToCss', () => {
255
+ const roles = (role: EditorColorRole) => `var(--color-theme-${role})`
256
+ const css = (value: string) => {
257
+ const parsed = parseGradientClass(value)
258
+ if (!parsed) throw new Error(`unreadable: ${value}`)
259
+ return gradientClassToCss(parsed, roles)
260
+ }
261
+
262
+ it('writes a linear gradient with the angle and every position', () => {
263
+ expect(css('bg-linear-135 from-[#7f56d9] to-[#432e73]')).toBe(
264
+ 'linear-gradient(135deg in oklab, #7f56d9 0%, #432e73 100%)',
265
+ )
266
+ })
267
+
268
+ it('writes a centred radial gradient without a position', () => {
269
+ expect(css('bg-radial from-[#000000] to-[#ffffff] to-80%')).toBe(
270
+ 'radial-gradient(in oklab, #000000 0%, #ffffff 80%)',
271
+ )
272
+ })
273
+
274
+ it('writes the centre before the interpolation method off centre, in oklab', () => {
275
+ expect(css('bg-radial-[at_30%_70%] from-[#000000] to-[#ffffff]')).toBe(
276
+ 'radial-gradient(at 30% 70% in oklab, #000000 0%, #ffffff 100%)',
277
+ )
278
+ })
279
+
280
+ it('writes three stops with the via position, also the default 50 %', () => {
281
+ expect(css('bg-linear-90 from-[#000000] via-[#888888] to-[#ffffff]')).toBe(
282
+ 'linear-gradient(90deg in oklab, #000000 0%, #888888 50%, #ffffff 100%)',
283
+ )
284
+ expect(css('bg-linear-90 from-[#000000] from-10% via-[#888888] via-40% to-[#ffffff]')).toBe(
285
+ 'linear-gradient(90deg in oklab, #000000 10%, #888888 40%, #ffffff 100%)',
286
+ )
287
+ })
288
+
289
+ it('resolves a role and keeps a hex with alpha', () => {
290
+ expect(css('bg-linear-0 from-theme-main to-[#432E73CC]')).toBe(
291
+ 'linear-gradient(0deg in oklab, var(--color-theme-main) 0%, #432e73cc 100%)',
292
+ )
293
+ })
294
+
295
+ it('round-trips: serialize, parse and draw give the same CSS as the parsed value', () => {
296
+ const value = 'bg-radial-[at_20%_80%] from-theme-accent via-[#ffffff80] via-35% to-[#000000]'
297
+ const parsed = parseGradientClass(value)
298
+ expect(parsed && serializeGradientClass(parsed)).toBe(value)
299
+ expect(css(value)).toBe(
300
+ 'radial-gradient(at 20% 80% in oklab, var(--color-theme-accent) 0%, #ffffff80 35%, #000000 100%)',
301
+ )
302
+ })
303
+ })
@@ -0,0 +1,72 @@
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 EditorDashPattern from './EditorDashPattern.vue'
5
+ import type { EditorDashPatternValue } from './EditorDashPattern.vue'
6
+
7
+ export default {
8
+ title: 'Components/Editor Components/Molecules/Editor Dash Pattern',
9
+ component: EditorDashPattern,
10
+ decorators: [() => ({ template: '<div class="w-100 mx-auto"><story /></div>' })],
11
+ argTypes: {
12
+ label: { control: 'text' },
13
+ },
14
+ } satisfies Meta<typeof EditorDashPattern>
15
+
16
+ type Story = StoryObj<typeof EditorDashPattern>
17
+
18
+ /** Shows the model as-is: two whole px values. */
19
+ const render = (args: Story['args'], initial: EditorDashPatternValue) => ({
20
+ components: { EditorDashPattern },
21
+ setup() {
22
+ const pattern = ref(initial)
23
+ return { args, pattern }
24
+ },
25
+ template: `
26
+ <div>
27
+ <EditorDashPattern v-bind="args" v-model="pattern" />
28
+ <p data-testid="dash-pattern-value" class="small-normal text-gray-700">
29
+ {{ pattern.dash }} / {{ pattern.gap }}
30
+ </p>
31
+ </div>
32
+ `,
33
+ })
34
+
35
+ const readout = (canvasElement: HTMLElement) =>
36
+ canvasElement.querySelector('[data-testid="dash-pattern-value"]')!.textContent!.trim()
37
+
38
+ export const Default: Story = {
39
+ render: (args) => render(args, { dash: 8, gap: 4 }),
40
+ }
41
+
42
+ /**
43
+ * The field never goes empty: clearing it and leaving puts the previous value
44
+ * back. Values outside 1-64 are clamped, with the field's snap animation.
45
+ */
46
+ export const NeverEmpty: Story = {
47
+ render: (args) => render(args, { dash: 8, gap: 4 }),
48
+ play: async ({ canvasElement }) => {
49
+ const canvas = within(canvasElement)
50
+ const dash = canvas.getByRole('spinbutton', { name: 'Lengte (px)' })
51
+ const gap = canvas.getByRole('spinbutton', { name: 'Tussenruimte (px)' })
52
+
53
+ await userEvent.clear(dash)
54
+ await userEvent.tab()
55
+ await waitFor(() => expect(dash).toHaveValue(8))
56
+ expect(readout(canvasElement)).toBe('8 / 4')
57
+
58
+ await userEvent.clear(gap)
59
+ await userEvent.type(gap, '100')
60
+ await userEvent.tab()
61
+ await waitFor(() => expect(gap).toHaveValue(64))
62
+ expect(readout(canvasElement)).toBe('8 / 64')
63
+ },
64
+ }
65
+
66
+ export const CustomMessages: Story = {
67
+ args: {
68
+ label: 'Dash',
69
+ messages: { dash: 'Length', gap: 'Gap', unit: 'px' },
70
+ },
71
+ render: (args) => render(args, { dash: 12, gap: 6 }),
72
+ }
@@ -0,0 +1,124 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { flushPromises, mount, type DOMWrapper, type VueWrapper } from '@vue/test-utils'
3
+ import EditorDashPattern, { EDITOR_DASH_PATTERN_LIMITS } from './EditorDashPattern.vue'
4
+ import type { EditorDashPatternValue } from './EditorDashPattern.vue'
5
+
6
+ const render = (modelValue: EditorDashPatternValue, props: Record<string, unknown> = {}) =>
7
+ mount(EditorDashPattern, { props: { modelValue, ...props } })
8
+
9
+ const inputs = (wrapper: VueWrapper) => wrapper.findAll('input')
10
+
11
+ const values = (wrapper: VueWrapper) =>
12
+ inputs(wrapper).map((input) => (input.element as HTMLInputElement).value)
13
+
14
+ const lastEmit = (wrapper: VueWrapper) => wrapper.emitted('update:modelValue')?.at(-1)?.[0]
15
+
16
+ /** Type and confirm, the way leaving the field does. */
17
+ const commit = async (input: DOMWrapper<Element>, typed: string) => {
18
+ await input.setValue(typed)
19
+ await input.trigger('blur')
20
+ await flushPromises()
21
+ }
22
+
23
+ describe('EditorDashPattern', () => {
24
+ // No assertions on typography utilities: copy is found by text or by
25
+ // accessible name, so restyling cannot turn these red.
26
+ it('renders the Dutch default label', () => {
27
+ expect(render({ dash: 8, gap: 4 }).find('p').text()).toBe('Streep')
28
+ })
29
+
30
+ it('renders a custom label', () => {
31
+ expect(render({ dash: 8, gap: 4 }, { label: 'Dash' }).find('p').text()).toBe('Dash')
32
+ })
33
+
34
+ it('shows the two values in two fields side by side', () => {
35
+ const wrapper = render({ dash: 8, gap: 4 })
36
+
37
+ expect(values(wrapper)).toEqual(['8', '4'])
38
+ expect(wrapper.get('.grid-cols-2').classes()).toEqual(expect.arrayContaining(['grid', 'gap-2']))
39
+ })
40
+
41
+ it('captions each field visibly and names it with the caption plus unit', () => {
42
+ const wrapper = render({ dash: 8, gap: 4 })
43
+
44
+ expect(wrapper.text()).toContain('Lengte')
45
+ expect(wrapper.text()).toContain('Tussenruimte')
46
+ expect(inputs(wrapper).map((input) => input.attributes('aria-label'))).toEqual([
47
+ 'Lengte (px)',
48
+ 'Tussenruimte (px)',
49
+ ])
50
+ })
51
+
52
+ it('offers px only, so it renders no unit selector', () => {
53
+ expect(render({ dash: 8, gap: 4 }).find('select').exists()).toBe(false)
54
+ })
55
+
56
+ it('merges custom messages per key', () => {
57
+ const wrapper = render({ dash: 8, gap: 4 }, { messages: { dash: 'Length', unit: 'pixels' } })
58
+
59
+ expect(inputs(wrapper).map((input) => input.attributes('aria-label'))).toEqual([
60
+ 'Length (pixels)',
61
+ 'Tussenruimte (pixels)',
62
+ ])
63
+ })
64
+
65
+ it('writes the dash and keeps the gap', async () => {
66
+ const wrapper = render({ dash: 8, gap: 4 })
67
+
68
+ await commit(inputs(wrapper)[0], '12')
69
+
70
+ expect(lastEmit(wrapper)).toEqual({ dash: 12, gap: 4 })
71
+ })
72
+
73
+ it('writes the gap and keeps the dash', async () => {
74
+ const wrapper = render({ dash: 8, gap: 4 })
75
+
76
+ await commit(inputs(wrapper)[1], '6')
77
+
78
+ expect(lastEmit(wrapper)).toEqual({ dash: 8, gap: 6 })
79
+ })
80
+
81
+ it('passes on commit from the field that finished', async () => {
82
+ const wrapper = render({ dash: 8, gap: 4 })
83
+
84
+ await commit(inputs(wrapper)[1], '6')
85
+
86
+ expect(wrapper.emitted('commit')).toHaveLength(1)
87
+ })
88
+
89
+ it('clamps the dash to the limits', async () => {
90
+ const wrapper = render({ dash: 8, gap: 4 })
91
+
92
+ await commit(inputs(wrapper)[0], '500')
93
+
94
+ expect(lastEmit(wrapper)).toEqual({ dash: 64, gap: 4 })
95
+ })
96
+
97
+ it('clamps the gap to the limits', async () => {
98
+ const wrapper = render({ dash: 8, gap: 4 })
99
+
100
+ await commit(inputs(wrapper)[1], '0')
101
+
102
+ expect(lastEmit(wrapper)).toEqual({ dash: 8, gap: 1 })
103
+ })
104
+
105
+ it('is never empty: an emptied field puts the previous value back', async () => {
106
+ const wrapper = render({ dash: 8, gap: 4 })
107
+
108
+ await commit(inputs(wrapper)[0], '')
109
+
110
+ expect(wrapper.emitted('update:modelValue')).toBeUndefined()
111
+ expect(wrapper.emitted('commit')).toBeUndefined()
112
+ expect(values(wrapper)).toEqual(['8', '4'])
113
+ })
114
+ })
115
+
116
+ // The consumer's matcher uses the same bounds, so they are a contract.
117
+ describe('EDITOR_DASH_PATTERN_LIMITS', () => {
118
+ it('bounds both values to 1-64 px', () => {
119
+ expect(EDITOR_DASH_PATTERN_LIMITS).toEqual({
120
+ dash: { min: 1, max: 64 },
121
+ gap: { min: 1, max: 64 },
122
+ })
123
+ })
124
+ })
@@ -0,0 +1,46 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import { ref } from 'vue'
3
+ import EditorFlexDirection from './EditorFlexDirection.vue'
4
+
5
+ export default {
6
+ title: 'Components/Editor Components/Molecules/Editor Flex Direction',
7
+ component: EditorFlexDirection,
8
+ decorators: [() => ({ template: '<div class="w-100 mx-auto"><story /></div>' })],
9
+ argTypes: {
10
+ label: { control: 'text' },
11
+ },
12
+ } satisfies Meta<typeof EditorFlexDirection>
13
+
14
+ type Story = StoryObj<typeof EditorFlexDirection>
15
+
16
+ export const Default: Story = {
17
+ render: (args) => ({
18
+ components: { EditorFlexDirection },
19
+ setup() {
20
+ const direction = ref<'flex-row' | 'flex-col'>('flex-row')
21
+ return { args, direction }
22
+ },
23
+ // The value is shown as-is: it is the Tailwind class the editor applies.
24
+ template: `
25
+ <div>
26
+ <EditorFlexDirection v-bind="args" v-model="direction" />
27
+ <p class="small-normal text-gray-700">{{ direction }}</p>
28
+ </div>
29
+ `,
30
+ }),
31
+ }
32
+
33
+ export const CustomMessages: Story = {
34
+ args: {
35
+ label: 'Direction',
36
+ messages: { horizontal: 'Horizontal', vertical: 'Vertical' },
37
+ },
38
+ render: (args) => ({
39
+ components: { EditorFlexDirection },
40
+ setup() {
41
+ const direction = ref<'flex-row' | 'flex-col'>('flex-row')
42
+ return { args, direction }
43
+ },
44
+ template: `<EditorFlexDirection v-bind="args" v-model="direction" />`,
45
+ }),
46
+ }