@nexxtmove/ui 2.6.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (229) hide show
  1. package/dist/components/editor/molecules/EditorColor/EditorColor.vue.d.ts +7 -5
  2. package/dist/components/editor/molecules/EditorColor/_ColorField.vue.d.ts +4 -1
  3. package/dist/components/organisms/ColorPicker/ColorPicker.vue.d.ts +3 -1
  4. package/dist/index.js +95 -172
  5. package/dist/nuxt.js +0 -1
  6. package/package.json +38 -36
  7. package/src/attribute-contract.test.ts +151 -0
  8. package/src/components/atoms/AnimatedNumber/AnimatedNumber.stories.ts +36 -0
  9. package/src/components/atoms/AnimatedNumber/AnimatedNumber.test.ts +21 -0
  10. package/src/components/atoms/Avatar/Avatar.stories.ts +122 -0
  11. package/src/components/atoms/Avatar/Avatar.test.ts +134 -0
  12. package/src/components/atoms/Button/Button.stories.ts +290 -0
  13. package/src/components/atoms/Button/Button.test.ts +406 -0
  14. package/src/components/atoms/Card/Card.stories.ts +45 -0
  15. package/src/components/atoms/Card/Card.test.ts +46 -0
  16. package/src/components/atoms/Checkbox/Checkbox.stories.ts +35 -0
  17. package/src/components/atoms/Checkbox/Checkbox.test.ts +55 -0
  18. package/src/components/atoms/Chip/Chip.stories.ts +274 -0
  19. package/src/components/atoms/Chip/Chip.test.ts +230 -0
  20. package/src/components/atoms/EnergyLabel/EnergyLabel.stories.ts +189 -0
  21. package/src/components/atoms/EnergyLabel/EnergyLabel.test.ts +100 -0
  22. package/src/components/atoms/FloatingPanel/FloatingPanel.stories.ts +677 -0
  23. package/src/components/atoms/FloatingPanel/FloatingPanel.test.ts +864 -0
  24. package/src/components/atoms/Icon/Icon.stories.ts +63 -0
  25. package/src/components/atoms/Icon/Icon.test.ts +89 -0
  26. package/src/components/atoms/Loader/Loader.stories.ts +36 -0
  27. package/src/components/atoms/Loader/Loader.test.ts +40 -0
  28. package/src/components/atoms/PickerItem/PickerItem.stories.ts +172 -0
  29. package/src/components/atoms/PickerItem/PickerItem.test.ts +142 -0
  30. package/src/components/atoms/PickerPreview/PickerPreview.stories.ts +183 -0
  31. package/src/components/atoms/PickerPreview/PickerPreview.test.ts +137 -0
  32. package/src/components/atoms/PresenceIndicator/PresenceIndicator.stories.ts +21 -0
  33. package/src/components/atoms/PropertyListing/PropertyListing.stories.ts +38 -0
  34. package/src/components/atoms/PropertyListing/PropertyListing.test.ts +46 -0
  35. package/src/components/atoms/SelectableOption/SelectableOption.stories.ts +131 -0
  36. package/src/components/atoms/SelectableOption/SelectableOption.test.ts +96 -0
  37. package/src/components/atoms/Skeleton/Skeleton.stories.ts +27 -0
  38. package/src/components/atoms/Skeleton/Skeleton.test.ts +28 -0
  39. package/src/components/atoms/Slider/Slider.stories.ts +151 -0
  40. package/src/components/atoms/Slider/Slider.test.ts +191 -0
  41. package/src/components/atoms/Squircle/Squircle.stories.ts +114 -0
  42. package/src/components/atoms/Squircle/Squircle.test.ts +70 -0
  43. package/src/components/atoms/Switch/Switch.stories.ts +91 -0
  44. package/src/components/atoms/Switch/Switch.test.ts +183 -0
  45. package/src/components/atoms/Tooltip/Tooltip.stories.ts +158 -0
  46. package/src/components/atoms/Tooltip/Tooltip.test.ts +134 -0
  47. package/src/components/editor/atoms/EditorDimensionField/EditorDimensionField.stories.ts +124 -0
  48. package/src/components/editor/atoms/EditorDimensionField/EditorDimensionField.test.ts +464 -0
  49. package/src/components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.stories.ts +266 -0
  50. package/src/components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.test.ts +421 -0
  51. package/src/components/editor/atoms/EditorSettingsGroup/EditorSettingsGroup.stories.ts +365 -0
  52. package/src/components/editor/atoms/EditorSettingsGroup/EditorSettingsGroup.test.ts +484 -0
  53. package/src/components/editor/composables/useRadioGrid.test.ts +261 -0
  54. package/src/components/editor/molecules/EditorAlignItems/EditorAlignItems.stories.ts +73 -0
  55. package/src/components/editor/molecules/EditorAlignItems/EditorAlignItems.test.ts +189 -0
  56. package/src/components/editor/molecules/EditorAlignmentGrid/EditorAlignmentGrid.stories.ts +309 -0
  57. package/src/components/editor/molecules/EditorAlignmentGrid/EditorAlignmentGrid.test.ts +359 -0
  58. package/src/components/editor/molecules/EditorAnchor/EditorAnchor.stories.ts +138 -0
  59. package/src/components/editor/molecules/EditorAnchor/EditorAnchor.test.ts +344 -0
  60. package/src/components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.stories.ts +87 -0
  61. package/src/components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.test.ts +262 -0
  62. package/src/components/editor/molecules/EditorBlur/EditorBlur.stories.ts +115 -0
  63. package/src/components/editor/molecules/EditorBlur/EditorBlur.test.ts +243 -0
  64. package/src/components/editor/molecules/EditorBorder/EditorBorder.stories.ts +187 -0
  65. package/src/components/editor/molecules/EditorBorder/EditorBorder.test.ts +456 -0
  66. package/src/components/editor/molecules/EditorColor/EditorColor.stories.ts +633 -0
  67. package/src/components/editor/molecules/EditorColor/EditorColor.test.ts +776 -0
  68. package/src/components/editor/molecules/EditorColor/EditorColor.vue +7 -5
  69. package/src/components/editor/molecules/EditorColor/EditorColorGradient.test.ts +832 -0
  70. package/src/components/editor/molecules/EditorColor/_ColorField.vue +20 -15
  71. package/src/components/editor/molecules/EditorColor/gradientClass.test.ts +303 -0
  72. package/src/components/editor/molecules/EditorDashPattern/EditorDashPattern.stories.ts +72 -0
  73. package/src/components/editor/molecules/EditorDashPattern/EditorDashPattern.test.ts +124 -0
  74. package/src/components/editor/molecules/EditorFlexDirection/EditorFlexDirection.stories.ts +46 -0
  75. package/src/components/editor/molecules/EditorFlexDirection/EditorFlexDirection.test.ts +97 -0
  76. package/src/components/editor/molecules/EditorFontSize/EditorFontSize.stories.ts +248 -0
  77. package/src/components/editor/molecules/EditorFontSize/EditorFontSize.test.ts +288 -0
  78. package/src/components/editor/molecules/EditorGap/EditorGap.stories.ts +129 -0
  79. package/src/components/editor/molecules/EditorGap/EditorGap.test.ts +253 -0
  80. package/src/components/editor/molecules/EditorHeadingType/EditorHeadingType.stories.ts +68 -0
  81. package/src/components/editor/molecules/EditorHeadingType/EditorHeadingType.test.ts +150 -0
  82. package/src/components/editor/molecules/EditorJustifyContent/EditorJustifyContent.stories.ts +71 -0
  83. package/src/components/editor/molecules/EditorJustifyContent/EditorJustifyContent.test.ts +161 -0
  84. package/src/components/editor/molecules/EditorLetterSpacing/EditorLetterSpacing.stories.ts +204 -0
  85. package/src/components/editor/molecules/EditorLetterSpacing/EditorLetterSpacing.test.ts +238 -0
  86. package/src/components/editor/molecules/EditorLineHeight/EditorLineHeight.stories.ts +257 -0
  87. package/src/components/editor/molecules/EditorLineHeight/EditorLineHeight.test.ts +286 -0
  88. package/src/components/editor/molecules/EditorLineStyle/EditorLineStyle.stories.ts +135 -0
  89. package/src/components/editor/molecules/EditorLineStyle/EditorLineStyle.test.ts +230 -0
  90. package/src/components/editor/molecules/EditorMaxWidth/EditorMaxWidth.stories.ts +154 -0
  91. package/src/components/editor/molecules/EditorMaxWidth/EditorMaxWidth.test.ts +112 -0
  92. package/src/components/editor/molecules/EditorOverrideStatus/EditorOverrideStatus.stories.ts +206 -0
  93. package/src/components/editor/molecules/EditorOverrideStatus/EditorOverrideStatus.test.ts +332 -0
  94. package/src/components/editor/molecules/EditorPadding/EditorPadding.stories.ts +374 -0
  95. package/src/components/editor/molecules/EditorPadding/EditorPadding.test.ts +712 -0
  96. package/src/components/editor/molecules/EditorRadius/EditorRadius.stories.ts +64 -0
  97. package/src/components/editor/molecules/EditorRadius/EditorRadius.test.ts +137 -0
  98. package/src/components/editor/molecules/EditorShadow/EditorShadow.stories.ts +103 -0
  99. package/src/components/editor/molecules/EditorShadow/EditorShadow.test.ts +168 -0
  100. package/src/components/editor/molecules/EditorSize/EditorSize.stories.ts +84 -0
  101. package/src/components/editor/molecules/EditorSize/EditorSize.test.ts +253 -0
  102. package/src/components/editor/molecules/EditorSizeLimit/EditorSizeLimit.stories.ts +49 -0
  103. package/src/components/editor/molecules/EditorSizeLimit/EditorSizeLimit.test.ts +43 -0
  104. package/src/components/editor/molecules/EditorTextAlign/EditorTextAlign.stories.ts +69 -0
  105. package/src/components/editor/molecules/EditorTextAlign/EditorTextAlign.test.ts +155 -0
  106. package/src/components/molecules/Announcement/Announcement.stories.ts +224 -0
  107. package/src/components/molecules/Announcement/Announcement.test.ts +161 -0
  108. package/src/components/molecules/ContactInfoCard/ContactInfoCard.stories.ts +99 -0
  109. package/src/components/molecules/ContactInfoCard/ContactInfoCard.test.ts +70 -0
  110. package/src/components/molecules/CustomerJourneyTile/CustomerJourneyTile.stories.ts +148 -0
  111. package/src/components/molecules/CustomerJourneyTile/CustomerJourneyTile.test.ts +271 -0
  112. package/src/components/molecules/DoughnutChart/DoughnutChart.stories.ts +45 -0
  113. package/src/components/molecules/DoughnutChart/DoughnutChart.test.ts +205 -0
  114. package/src/components/molecules/InfoBlock/InfoBlock.stories.ts +84 -0
  115. package/src/components/molecules/InfoBlock/InfoBlock.test.ts +101 -0
  116. package/src/components/molecules/InputField/InputField.stories.ts +117 -0
  117. package/src/components/molecules/InputField/InputField.test.ts +138 -0
  118. package/src/components/molecules/InputSelect/InputSelect.stories.ts +116 -0
  119. package/src/components/molecules/InputSelect/InputSelect.test.ts +127 -0
  120. package/src/components/molecules/ModalFooter/ModalFooter.stories.ts +204 -0
  121. package/src/components/molecules/ModalFooter/ModalFooter.test.ts +276 -0
  122. package/src/components/molecules/PageCard/PageCard.stories.ts +100 -0
  123. package/src/components/molecules/PageCard/PageCard.test.ts +91 -0
  124. package/src/components/molecules/PageContainer/PageContainer.stories.ts +90 -0
  125. package/src/components/molecules/PageContainer/PageContainer.test.ts +50 -0
  126. package/src/components/molecules/PageMockup/PageMockup.stories.ts +38 -0
  127. package/src/components/molecules/PageMockup/PageMockup.test.ts +20 -0
  128. package/src/components/molecules/PageTypeTile/PageTypeTile.stories.ts +63 -0
  129. package/src/components/molecules/PageTypeTile/PageTypeTile.test.ts +41 -0
  130. package/src/components/molecules/Pagination/Pagination.stories.ts +72 -0
  131. package/src/components/molecules/Pagination/Pagination.test.ts +130 -0
  132. package/src/components/molecules/PickerGroup/PickerGroup.stories.ts +90 -0
  133. package/src/components/molecules/PickerGroup/PickerGroup.test.ts +145 -0
  134. package/src/components/molecules/PickerList/PickerList.stories.ts +436 -0
  135. package/src/components/molecules/PickerList/PickerList.test.ts +870 -0
  136. package/src/components/molecules/PlaceholderInput/PlaceholderInput.stories.ts +48 -0
  137. package/src/components/molecules/PlaceholderInput/PlaceholderInput.test.ts +107 -0
  138. package/src/components/molecules/PropertyCard/PropertyCard.stories.ts +46 -0
  139. package/src/components/molecules/PropertyCard/PropertyCard.test.ts +59 -0
  140. package/src/components/molecules/Select/Select.stories.ts +264 -0
  141. package/src/components/molecules/Select/Select.test.ts +315 -0
  142. package/src/components/molecules/SidebarMenuItem/SidebarMenuItem.stories.ts +84 -0
  143. package/src/components/molecules/SidebarMenuItem/SidebarMenuItem.test.ts +103 -0
  144. package/src/components/molecules/SkeletonPropertyCard/SkeletonPropertyCard.stories.ts +11 -0
  145. package/src/components/molecules/SkeletonPropertyCard/SkeletonPropertyCard.test.ts +38 -0
  146. package/src/components/molecules/SocialIcons/SocialIcons.stories.ts +28 -0
  147. package/src/components/molecules/SocialIcons/SocialIcons.test.ts +58 -0
  148. package/src/components/molecules/SocialMediaCustomTemplate/SocialMediaCustomTemplate.stories.ts +11 -0
  149. package/src/components/molecules/SocialMediaCustomTemplate/SocialMediaCustomTemplate.test.ts +116 -0
  150. package/src/components/molecules/SocialMediaType/SocialMediaType.stories.ts +37 -0
  151. package/src/components/molecules/SocialMediaType/SocialMediaType.test.ts +126 -0
  152. package/src/components/molecules/Source/Source.stories.ts +35 -0
  153. package/src/components/molecules/Source/Source.test.ts +45 -0
  154. package/src/components/molecules/TabbedContent/TabbedContent.stories.ts +96 -0
  155. package/src/components/molecules/TabbedContent/TabbedContent.test.ts +109 -0
  156. package/src/components/molecules/Tabs/Tabs.stories.ts +230 -0
  157. package/src/components/molecules/Tabs/Tabs.test.ts +280 -0
  158. package/src/components/molecules/TemplateCard/TemplateCard.stories.ts +327 -0
  159. package/src/components/molecules/TemplateCard/TemplateCard.test.ts +339 -0
  160. package/src/components/molecules/Textarea/Textarea.stories.ts +216 -0
  161. package/src/components/molecules/Textarea/Textarea.test.ts +191 -0
  162. package/src/components/molecules/TimelineEvent/TimelineEvent.stories.ts +319 -0
  163. package/src/components/molecules/TimelineEvent/TimelineEvent.test.ts +224 -0
  164. package/src/components/molecules/TimelineLanes/TimelineLanes.stories.ts +29 -0
  165. package/src/components/molecules/TimelineLanes/TimelineLanes.test.ts +30 -0
  166. package/src/components/molecules/Toast/Toast.stories.ts +110 -0
  167. package/src/components/molecules/Toast/Toast.test.ts +103 -0
  168. package/src/components/molecules/ToggleGroup/ToggleGroup.stories.ts +297 -0
  169. package/src/components/molecules/ToggleGroup/ToggleGroup.test.ts +578 -0
  170. package/src/components/molecules/Tree/Tree.stories.ts +884 -0
  171. package/src/components/molecules/Tree/Tree.test.ts +1247 -0
  172. package/src/components/molecules/Tree/tree-drop.test.ts +223 -0
  173. package/src/components/organisms/Calendar/Calendar.stories.ts +108 -0
  174. package/src/components/organisms/Calendar/Calendar.test.ts +279 -0
  175. package/src/components/organisms/Calendar/_CalendarDayView.test.ts +104 -0
  176. package/src/components/organisms/Calendar/_CalendarHeader.test.ts +86 -0
  177. package/src/components/organisms/Calendar/_CalendarMonthView.test.ts +52 -0
  178. package/src/components/organisms/Calendar/_CalendarTimePicker.test.ts +89 -0
  179. package/src/components/organisms/Calendar/_CalendarYearView.test.ts +56 -0
  180. package/src/components/organisms/ColorPicker/ColorPicker.stories.ts +229 -0
  181. package/src/components/organisms/ColorPicker/ColorPicker.test.ts +696 -0
  182. package/src/components/organisms/ColorPicker/ColorPicker.vue +12 -3
  183. package/src/components/organisms/ColorPicker/gradient.test.ts +67 -0
  184. package/src/components/organisms/ColorPicker/gradientGeometry.test.ts +206 -0
  185. package/src/components/organisms/Combobox/Combobox.stories.ts +234 -0
  186. package/src/components/organisms/Combobox/Combobox.test.ts +512 -0
  187. package/src/components/organisms/Combobox/Combobox.vue +1 -1
  188. package/src/components/organisms/ComboboxPanel/ComboboxPanel.stories.ts +158 -0
  189. package/src/components/organisms/ComboboxPanel/ComboboxPanel.test.ts +524 -0
  190. package/src/components/organisms/ComboboxPanel/ComboboxPanel.vue +1 -1
  191. package/src/components/organisms/CountrySelect/CountrySelect.stories.ts +139 -0
  192. package/src/components/organisms/CountrySelect/CountrySelect.test.ts +149 -0
  193. package/src/components/organisms/DatePicker/DatePicker.stories.ts +163 -0
  194. package/src/components/organisms/DatePicker/DatePicker.test.ts +214 -0
  195. package/src/components/organisms/DropdownButton/DropdownButton.stories.ts +123 -0
  196. package/src/components/organisms/DropdownButton/DropdownButton.test.ts +379 -0
  197. package/src/components/organisms/FontPicker/FontPicker.stories.ts +476 -0
  198. package/src/components/organisms/FontPicker/FontPicker.test.ts +873 -0
  199. package/src/components/organisms/Header/Header.stories.ts +75 -0
  200. package/src/components/organisms/Header/Header.test.ts +233 -0
  201. package/src/components/organisms/IconPicker/IconPicker.stories.ts +734 -0
  202. package/src/components/organisms/IconPicker/IconPicker.test.ts +669 -0
  203. package/src/components/organisms/IconPicker/_IconPickerGrid.test.ts +184 -0
  204. package/src/components/organisms/IconPicker/_IconPickerRail.test.ts +102 -0
  205. package/src/components/organisms/IconPicker/useIconPickerNavigation.test.ts +215 -0
  206. package/src/components/organisms/Modal/Modal.stories.ts +350 -0
  207. package/src/components/organisms/Modal/Modal.test.ts +275 -0
  208. package/src/components/organisms/PickerPanel/PickerPanel.stories.ts +452 -0
  209. package/src/components/organisms/PickerPanel/PickerPanel.test.ts +358 -0
  210. package/src/components/organisms/ProgressBar/ProgressBar.stories.ts +100 -0
  211. package/src/components/organisms/ProgressBar/ProgressBar.test.ts +417 -0
  212. package/src/components/organisms/ReferenceCard/ReferenceCard.stories.ts +191 -0
  213. package/src/components/organisms/ReferenceCard/ReferenceCard.test.ts +165 -0
  214. package/src/components/organisms/SocialMediaTemplate/SocialMediaTemplate.stories.ts +65 -0
  215. package/src/components/organisms/SocialMediaTemplate/SocialMediaTemplate.test.ts +466 -0
  216. package/src/components/organisms/StepperHeader/StepperHeader.stories.ts +91 -0
  217. package/src/components/organisms/StepperHeader/StepperHeader.test.ts +279 -0
  218. package/src/components/organisms/Table/Table.stories.ts +332 -0
  219. package/src/components/organisms/Table/Table.test.ts +320 -0
  220. package/src/components/organisms/TimelinePhaseblock/TimelinePhaseblock.stories.ts +198 -0
  221. package/src/components/organisms/TimelinePhaseblock/TimelinePhaseblock.test.ts +283 -0
  222. package/src/components/organisms/TimelineTrack/TimelineTrack.stories.ts +120 -0
  223. package/src/components/organisms/TimelineTrack/TimelineTrack.test.ts +91 -0
  224. package/src/components/organisms/Toaster/Toaster.stories.ts +51 -0
  225. package/src/components/organisms/Toaster/Toaster.test.ts +75 -0
  226. package/src/components/organisms/VideoControls/VideoControls.stories.ts +792 -0
  227. package/src/components/organisms/VideoControls/VideoControls.test.ts +1056 -0
  228. package/src/components/organisms/VideoPlayer/VideoPlayer.stories.ts +327 -0
  229. package/src/components/organisms/VideoPlayer/VideoPlayer.test.ts +439 -0
@@ -31,7 +31,10 @@ export interface ColorFieldProps<P extends string> {
31
31
  suggestions?: string[]
32
32
  /** Theme roles offered above the suggestions; an entry with an unreadable hex is left out. */
33
33
  themeRoles?: EditorColorThemeRole[]
34
- /** Offer an opacity slider, hidden while a theme role is selected. */
34
+ /**
35
+ * Offer an opacity slider, also while a theme role is selected: it then stands
36
+ * at 100%, and moving it turns the role into its hex with that opacity.
37
+ */
35
38
  showAlpha?: boolean
36
39
  /**
37
40
  * The fills offered: `['solid']` (the default), `['solid', 'gradient']` or
@@ -122,25 +125,27 @@ const offeredRoles = computed(() => themeRoles.filter((entry) => ROLE_HEX_PATTER
122
125
  const activeRole = computed(() => offeredRoles.value.find(({ role }) => role === storedRole.value))
123
126
 
124
127
  /**
128
+ * A role's hex as the picker gets it: opaque, in the 8-digit form.
129
+ *
125
130
  * Picking the role's own colour -- from the suggestions, or typed -- is how a
126
131
  * colour is detached from the theme without changing how it looks. The picker
127
- * reports nothing when it is told to pick the string it already has, so a role's
128
- * hex is handed over in its 8-digit form: with `show-alpha` off the picker never
129
- * reports an alpha pair, which makes every pick a change.
132
+ * reports nothing when it is told to pick the string it already has, so the hex
133
+ * is handed over with an `ff` pair: the picker writes an opaque colour as six
134
+ * digits and an alpha pair only below full opacity, so it never writes this
135
+ * string back, and every pick is a change. That holds with the opacity slider
136
+ * shown as well.
137
+ *
138
+ * A role has no opacity, so an alpha pair in the role's own hex is replaced by
139
+ * `ff`: the slider of an active role stands at 100%. Moving it below that writes
140
+ * the role's hex with the alpha pair, which detaches the role the same way;
141
+ * back at 100% it stays a literal (`OPAQUE_ALPHA_PATTERN`).
130
142
  *
131
- * A role has no opacity here, so an alpha pair in the role's own hex is replaced
132
- * by `ff`. Handed over translucent, the picker would drop that alpha the moment
133
- * the role turns `show-alpha` off, and write the opaque hex over the role.
143
+ * Opening the panel, or a role becoming active, writes nothing: the picker's
144
+ * primitives only report a change the user makes, and a key that leaves the
145
+ * slider where it is (ArrowRight at 100%) is no change.
134
146
  */
135
147
  const pickerHex = (roleHex: string) => `${roleHex.slice(0, 7)}ff`
136
148
 
137
- /**
138
- * A role class has no opacity, so the slider is only offered for a literal.
139
- * Hiding it is also what keeps the trick in `pickerHex` working: without the
140
- * alpha channel the picker reads the role's hex as opaque.
141
- */
142
- const alphaEnabled = computed(() => showAlpha && !activeRole.value)
143
-
144
149
  // An unparseable value -- empty, a token class such as `bg-red-500`, or copy
145
150
  // from before this component -- leaves the picker empty and its placeholder
146
151
  // visible, rather than being reported as a problem: the first pick writes a
@@ -401,7 +406,7 @@ watch(
401
406
  :label="label"
402
407
  :describedby="describedby"
403
408
  :formats="['hex']"
404
- :show-alpha="alphaEnabled"
409
+ :show-alpha="showAlpha"
405
410
  :placeholder="placeholder"
406
411
  :suggestions="{ hex: suggestions }"
407
412
  :suggestions-label="suggestionsLabel"
@@ -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
+ }