@nexxtmove/ui 2.6.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/dist/index.js +49 -126
  2. package/dist/nuxt.js +0 -1
  3. package/package.json +38 -36
  4. package/src/attribute-contract.test.ts +151 -0
  5. package/src/components/atoms/AnimatedNumber/AnimatedNumber.stories.ts +36 -0
  6. package/src/components/atoms/AnimatedNumber/AnimatedNumber.test.ts +21 -0
  7. package/src/components/atoms/Avatar/Avatar.stories.ts +122 -0
  8. package/src/components/atoms/Avatar/Avatar.test.ts +134 -0
  9. package/src/components/atoms/Button/Button.stories.ts +290 -0
  10. package/src/components/atoms/Button/Button.test.ts +406 -0
  11. package/src/components/atoms/Card/Card.stories.ts +45 -0
  12. package/src/components/atoms/Card/Card.test.ts +46 -0
  13. package/src/components/atoms/Checkbox/Checkbox.stories.ts +35 -0
  14. package/src/components/atoms/Checkbox/Checkbox.test.ts +55 -0
  15. package/src/components/atoms/Chip/Chip.stories.ts +274 -0
  16. package/src/components/atoms/Chip/Chip.test.ts +230 -0
  17. package/src/components/atoms/EnergyLabel/EnergyLabel.stories.ts +189 -0
  18. package/src/components/atoms/EnergyLabel/EnergyLabel.test.ts +100 -0
  19. package/src/components/atoms/FloatingPanel/FloatingPanel.stories.ts +677 -0
  20. package/src/components/atoms/FloatingPanel/FloatingPanel.test.ts +864 -0
  21. package/src/components/atoms/Icon/Icon.stories.ts +63 -0
  22. package/src/components/atoms/Icon/Icon.test.ts +89 -0
  23. package/src/components/atoms/Loader/Loader.stories.ts +36 -0
  24. package/src/components/atoms/Loader/Loader.test.ts +40 -0
  25. package/src/components/atoms/PickerItem/PickerItem.stories.ts +172 -0
  26. package/src/components/atoms/PickerItem/PickerItem.test.ts +142 -0
  27. package/src/components/atoms/PickerPreview/PickerPreview.stories.ts +183 -0
  28. package/src/components/atoms/PickerPreview/PickerPreview.test.ts +137 -0
  29. package/src/components/atoms/PresenceIndicator/PresenceIndicator.stories.ts +21 -0
  30. package/src/components/atoms/PropertyListing/PropertyListing.stories.ts +38 -0
  31. package/src/components/atoms/PropertyListing/PropertyListing.test.ts +46 -0
  32. package/src/components/atoms/SelectableOption/SelectableOption.stories.ts +131 -0
  33. package/src/components/atoms/SelectableOption/SelectableOption.test.ts +96 -0
  34. package/src/components/atoms/Skeleton/Skeleton.stories.ts +27 -0
  35. package/src/components/atoms/Skeleton/Skeleton.test.ts +28 -0
  36. package/src/components/atoms/Slider/Slider.stories.ts +151 -0
  37. package/src/components/atoms/Slider/Slider.test.ts +191 -0
  38. package/src/components/atoms/Squircle/Squircle.stories.ts +114 -0
  39. package/src/components/atoms/Squircle/Squircle.test.ts +70 -0
  40. package/src/components/atoms/Switch/Switch.stories.ts +91 -0
  41. package/src/components/atoms/Switch/Switch.test.ts +183 -0
  42. package/src/components/atoms/Tooltip/Tooltip.stories.ts +158 -0
  43. package/src/components/atoms/Tooltip/Tooltip.test.ts +134 -0
  44. package/src/components/editor/atoms/EditorDimensionField/EditorDimensionField.stories.ts +124 -0
  45. package/src/components/editor/atoms/EditorDimensionField/EditorDimensionField.test.ts +464 -0
  46. package/src/components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.stories.ts +266 -0
  47. package/src/components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.test.ts +421 -0
  48. package/src/components/editor/atoms/EditorSettingsGroup/EditorSettingsGroup.stories.ts +365 -0
  49. package/src/components/editor/atoms/EditorSettingsGroup/EditorSettingsGroup.test.ts +484 -0
  50. package/src/components/editor/composables/useRadioGrid.test.ts +261 -0
  51. package/src/components/editor/molecules/EditorAlignItems/EditorAlignItems.stories.ts +73 -0
  52. package/src/components/editor/molecules/EditorAlignItems/EditorAlignItems.test.ts +189 -0
  53. package/src/components/editor/molecules/EditorAlignmentGrid/EditorAlignmentGrid.stories.ts +309 -0
  54. package/src/components/editor/molecules/EditorAlignmentGrid/EditorAlignmentGrid.test.ts +359 -0
  55. package/src/components/editor/molecules/EditorAnchor/EditorAnchor.stories.ts +138 -0
  56. package/src/components/editor/molecules/EditorAnchor/EditorAnchor.test.ts +344 -0
  57. package/src/components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.stories.ts +87 -0
  58. package/src/components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.test.ts +262 -0
  59. package/src/components/editor/molecules/EditorBlur/EditorBlur.stories.ts +115 -0
  60. package/src/components/editor/molecules/EditorBlur/EditorBlur.test.ts +243 -0
  61. package/src/components/editor/molecules/EditorBorder/EditorBorder.stories.ts +187 -0
  62. package/src/components/editor/molecules/EditorBorder/EditorBorder.test.ts +456 -0
  63. package/src/components/editor/molecules/EditorColor/EditorColor.stories.ts +563 -0
  64. package/src/components/editor/molecules/EditorColor/EditorColor.test.ts +689 -0
  65. package/src/components/editor/molecules/EditorColor/EditorColorGradient.test.ts +793 -0
  66. package/src/components/editor/molecules/EditorColor/gradientClass.test.ts +303 -0
  67. package/src/components/editor/molecules/EditorDashPattern/EditorDashPattern.stories.ts +72 -0
  68. package/src/components/editor/molecules/EditorDashPattern/EditorDashPattern.test.ts +124 -0
  69. package/src/components/editor/molecules/EditorFlexDirection/EditorFlexDirection.stories.ts +46 -0
  70. package/src/components/editor/molecules/EditorFlexDirection/EditorFlexDirection.test.ts +97 -0
  71. package/src/components/editor/molecules/EditorFontSize/EditorFontSize.stories.ts +248 -0
  72. package/src/components/editor/molecules/EditorFontSize/EditorFontSize.test.ts +288 -0
  73. package/src/components/editor/molecules/EditorGap/EditorGap.stories.ts +129 -0
  74. package/src/components/editor/molecules/EditorGap/EditorGap.test.ts +253 -0
  75. package/src/components/editor/molecules/EditorHeadingType/EditorHeadingType.stories.ts +68 -0
  76. package/src/components/editor/molecules/EditorHeadingType/EditorHeadingType.test.ts +150 -0
  77. package/src/components/editor/molecules/EditorJustifyContent/EditorJustifyContent.stories.ts +71 -0
  78. package/src/components/editor/molecules/EditorJustifyContent/EditorJustifyContent.test.ts +161 -0
  79. package/src/components/editor/molecules/EditorLetterSpacing/EditorLetterSpacing.stories.ts +204 -0
  80. package/src/components/editor/molecules/EditorLetterSpacing/EditorLetterSpacing.test.ts +238 -0
  81. package/src/components/editor/molecules/EditorLineHeight/EditorLineHeight.stories.ts +257 -0
  82. package/src/components/editor/molecules/EditorLineHeight/EditorLineHeight.test.ts +286 -0
  83. package/src/components/editor/molecules/EditorLineStyle/EditorLineStyle.stories.ts +135 -0
  84. package/src/components/editor/molecules/EditorLineStyle/EditorLineStyle.test.ts +230 -0
  85. package/src/components/editor/molecules/EditorMaxWidth/EditorMaxWidth.stories.ts +154 -0
  86. package/src/components/editor/molecules/EditorMaxWidth/EditorMaxWidth.test.ts +112 -0
  87. package/src/components/editor/molecules/EditorOverrideStatus/EditorOverrideStatus.stories.ts +206 -0
  88. package/src/components/editor/molecules/EditorOverrideStatus/EditorOverrideStatus.test.ts +332 -0
  89. package/src/components/editor/molecules/EditorPadding/EditorPadding.stories.ts +374 -0
  90. package/src/components/editor/molecules/EditorPadding/EditorPadding.test.ts +712 -0
  91. package/src/components/editor/molecules/EditorRadius/EditorRadius.stories.ts +64 -0
  92. package/src/components/editor/molecules/EditorRadius/EditorRadius.test.ts +137 -0
  93. package/src/components/editor/molecules/EditorShadow/EditorShadow.stories.ts +103 -0
  94. package/src/components/editor/molecules/EditorShadow/EditorShadow.test.ts +168 -0
  95. package/src/components/editor/molecules/EditorSize/EditorSize.stories.ts +84 -0
  96. package/src/components/editor/molecules/EditorSize/EditorSize.test.ts +253 -0
  97. package/src/components/editor/molecules/EditorSizeLimit/EditorSizeLimit.stories.ts +49 -0
  98. package/src/components/editor/molecules/EditorSizeLimit/EditorSizeLimit.test.ts +43 -0
  99. package/src/components/editor/molecules/EditorTextAlign/EditorTextAlign.stories.ts +69 -0
  100. package/src/components/editor/molecules/EditorTextAlign/EditorTextAlign.test.ts +155 -0
  101. package/src/components/molecules/Announcement/Announcement.stories.ts +224 -0
  102. package/src/components/molecules/Announcement/Announcement.test.ts +161 -0
  103. package/src/components/molecules/ContactInfoCard/ContactInfoCard.stories.ts +99 -0
  104. package/src/components/molecules/ContactInfoCard/ContactInfoCard.test.ts +70 -0
  105. package/src/components/molecules/CustomerJourneyTile/CustomerJourneyTile.stories.ts +148 -0
  106. package/src/components/molecules/CustomerJourneyTile/CustomerJourneyTile.test.ts +271 -0
  107. package/src/components/molecules/DoughnutChart/DoughnutChart.stories.ts +45 -0
  108. package/src/components/molecules/DoughnutChart/DoughnutChart.test.ts +205 -0
  109. package/src/components/molecules/InfoBlock/InfoBlock.stories.ts +84 -0
  110. package/src/components/molecules/InfoBlock/InfoBlock.test.ts +101 -0
  111. package/src/components/molecules/InputField/InputField.stories.ts +117 -0
  112. package/src/components/molecules/InputField/InputField.test.ts +138 -0
  113. package/src/components/molecules/InputSelect/InputSelect.stories.ts +116 -0
  114. package/src/components/molecules/InputSelect/InputSelect.test.ts +127 -0
  115. package/src/components/molecules/ModalFooter/ModalFooter.stories.ts +204 -0
  116. package/src/components/molecules/ModalFooter/ModalFooter.test.ts +276 -0
  117. package/src/components/molecules/PageCard/PageCard.stories.ts +100 -0
  118. package/src/components/molecules/PageCard/PageCard.test.ts +91 -0
  119. package/src/components/molecules/PageContainer/PageContainer.stories.ts +90 -0
  120. package/src/components/molecules/PageContainer/PageContainer.test.ts +50 -0
  121. package/src/components/molecules/PageMockup/PageMockup.stories.ts +38 -0
  122. package/src/components/molecules/PageMockup/PageMockup.test.ts +20 -0
  123. package/src/components/molecules/PageTypeTile/PageTypeTile.stories.ts +63 -0
  124. package/src/components/molecules/PageTypeTile/PageTypeTile.test.ts +41 -0
  125. package/src/components/molecules/Pagination/Pagination.stories.ts +72 -0
  126. package/src/components/molecules/Pagination/Pagination.test.ts +130 -0
  127. package/src/components/molecules/PickerGroup/PickerGroup.stories.ts +90 -0
  128. package/src/components/molecules/PickerGroup/PickerGroup.test.ts +145 -0
  129. package/src/components/molecules/PickerList/PickerList.stories.ts +436 -0
  130. package/src/components/molecules/PickerList/PickerList.test.ts +870 -0
  131. package/src/components/molecules/PlaceholderInput/PlaceholderInput.stories.ts +48 -0
  132. package/src/components/molecules/PlaceholderInput/PlaceholderInput.test.ts +107 -0
  133. package/src/components/molecules/PropertyCard/PropertyCard.stories.ts +46 -0
  134. package/src/components/molecules/PropertyCard/PropertyCard.test.ts +59 -0
  135. package/src/components/molecules/Select/Select.stories.ts +264 -0
  136. package/src/components/molecules/Select/Select.test.ts +315 -0
  137. package/src/components/molecules/SidebarMenuItem/SidebarMenuItem.stories.ts +84 -0
  138. package/src/components/molecules/SidebarMenuItem/SidebarMenuItem.test.ts +103 -0
  139. package/src/components/molecules/SkeletonPropertyCard/SkeletonPropertyCard.stories.ts +11 -0
  140. package/src/components/molecules/SkeletonPropertyCard/SkeletonPropertyCard.test.ts +38 -0
  141. package/src/components/molecules/SocialIcons/SocialIcons.stories.ts +28 -0
  142. package/src/components/molecules/SocialIcons/SocialIcons.test.ts +58 -0
  143. package/src/components/molecules/SocialMediaCustomTemplate/SocialMediaCustomTemplate.stories.ts +11 -0
  144. package/src/components/molecules/SocialMediaCustomTemplate/SocialMediaCustomTemplate.test.ts +116 -0
  145. package/src/components/molecules/SocialMediaType/SocialMediaType.stories.ts +37 -0
  146. package/src/components/molecules/SocialMediaType/SocialMediaType.test.ts +126 -0
  147. package/src/components/molecules/Source/Source.stories.ts +35 -0
  148. package/src/components/molecules/Source/Source.test.ts +45 -0
  149. package/src/components/molecules/TabbedContent/TabbedContent.stories.ts +96 -0
  150. package/src/components/molecules/TabbedContent/TabbedContent.test.ts +109 -0
  151. package/src/components/molecules/Tabs/Tabs.stories.ts +230 -0
  152. package/src/components/molecules/Tabs/Tabs.test.ts +280 -0
  153. package/src/components/molecules/TemplateCard/TemplateCard.stories.ts +327 -0
  154. package/src/components/molecules/TemplateCard/TemplateCard.test.ts +339 -0
  155. package/src/components/molecules/Textarea/Textarea.stories.ts +216 -0
  156. package/src/components/molecules/Textarea/Textarea.test.ts +191 -0
  157. package/src/components/molecules/TimelineEvent/TimelineEvent.stories.ts +319 -0
  158. package/src/components/molecules/TimelineEvent/TimelineEvent.test.ts +224 -0
  159. package/src/components/molecules/TimelineLanes/TimelineLanes.stories.ts +29 -0
  160. package/src/components/molecules/TimelineLanes/TimelineLanes.test.ts +30 -0
  161. package/src/components/molecules/Toast/Toast.stories.ts +110 -0
  162. package/src/components/molecules/Toast/Toast.test.ts +103 -0
  163. package/src/components/molecules/ToggleGroup/ToggleGroup.stories.ts +297 -0
  164. package/src/components/molecules/ToggleGroup/ToggleGroup.test.ts +578 -0
  165. package/src/components/molecules/Tree/Tree.stories.ts +884 -0
  166. package/src/components/molecules/Tree/Tree.test.ts +1247 -0
  167. package/src/components/molecules/Tree/tree-drop.test.ts +223 -0
  168. package/src/components/organisms/Calendar/Calendar.stories.ts +108 -0
  169. package/src/components/organisms/Calendar/Calendar.test.ts +279 -0
  170. package/src/components/organisms/Calendar/_CalendarDayView.test.ts +104 -0
  171. package/src/components/organisms/Calendar/_CalendarHeader.test.ts +86 -0
  172. package/src/components/organisms/Calendar/_CalendarMonthView.test.ts +52 -0
  173. package/src/components/organisms/Calendar/_CalendarTimePicker.test.ts +89 -0
  174. package/src/components/organisms/Calendar/_CalendarYearView.test.ts +56 -0
  175. package/src/components/organisms/ColorPicker/ColorPicker.stories.ts +229 -0
  176. package/src/components/organisms/ColorPicker/ColorPicker.test.ts +638 -0
  177. package/src/components/organisms/ColorPicker/gradient.test.ts +67 -0
  178. package/src/components/organisms/ColorPicker/gradientGeometry.test.ts +206 -0
  179. package/src/components/organisms/Combobox/Combobox.stories.ts +234 -0
  180. package/src/components/organisms/Combobox/Combobox.test.ts +512 -0
  181. package/src/components/organisms/Combobox/Combobox.vue +1 -1
  182. package/src/components/organisms/ComboboxPanel/ComboboxPanel.stories.ts +158 -0
  183. package/src/components/organisms/ComboboxPanel/ComboboxPanel.test.ts +524 -0
  184. package/src/components/organisms/ComboboxPanel/ComboboxPanel.vue +1 -1
  185. package/src/components/organisms/CountrySelect/CountrySelect.stories.ts +139 -0
  186. package/src/components/organisms/CountrySelect/CountrySelect.test.ts +149 -0
  187. package/src/components/organisms/DatePicker/DatePicker.stories.ts +163 -0
  188. package/src/components/organisms/DatePicker/DatePicker.test.ts +214 -0
  189. package/src/components/organisms/DropdownButton/DropdownButton.stories.ts +123 -0
  190. package/src/components/organisms/DropdownButton/DropdownButton.test.ts +379 -0
  191. package/src/components/organisms/FontPicker/FontPicker.stories.ts +476 -0
  192. package/src/components/organisms/FontPicker/FontPicker.test.ts +873 -0
  193. package/src/components/organisms/Header/Header.stories.ts +75 -0
  194. package/src/components/organisms/Header/Header.test.ts +233 -0
  195. package/src/components/organisms/IconPicker/IconPicker.stories.ts +734 -0
  196. package/src/components/organisms/IconPicker/IconPicker.test.ts +669 -0
  197. package/src/components/organisms/IconPicker/_IconPickerGrid.test.ts +184 -0
  198. package/src/components/organisms/IconPicker/_IconPickerRail.test.ts +102 -0
  199. package/src/components/organisms/IconPicker/useIconPickerNavigation.test.ts +215 -0
  200. package/src/components/organisms/Modal/Modal.stories.ts +350 -0
  201. package/src/components/organisms/Modal/Modal.test.ts +275 -0
  202. package/src/components/organisms/PickerPanel/PickerPanel.stories.ts +452 -0
  203. package/src/components/organisms/PickerPanel/PickerPanel.test.ts +358 -0
  204. package/src/components/organisms/ProgressBar/ProgressBar.stories.ts +100 -0
  205. package/src/components/organisms/ProgressBar/ProgressBar.test.ts +417 -0
  206. package/src/components/organisms/ReferenceCard/ReferenceCard.stories.ts +191 -0
  207. package/src/components/organisms/ReferenceCard/ReferenceCard.test.ts +165 -0
  208. package/src/components/organisms/SocialMediaTemplate/SocialMediaTemplate.stories.ts +65 -0
  209. package/src/components/organisms/SocialMediaTemplate/SocialMediaTemplate.test.ts +466 -0
  210. package/src/components/organisms/StepperHeader/StepperHeader.stories.ts +91 -0
  211. package/src/components/organisms/StepperHeader/StepperHeader.test.ts +279 -0
  212. package/src/components/organisms/Table/Table.stories.ts +332 -0
  213. package/src/components/organisms/Table/Table.test.ts +320 -0
  214. package/src/components/organisms/TimelinePhaseblock/TimelinePhaseblock.stories.ts +198 -0
  215. package/src/components/organisms/TimelinePhaseblock/TimelinePhaseblock.test.ts +283 -0
  216. package/src/components/organisms/TimelineTrack/TimelineTrack.stories.ts +120 -0
  217. package/src/components/organisms/TimelineTrack/TimelineTrack.test.ts +91 -0
  218. package/src/components/organisms/Toaster/Toaster.stories.ts +51 -0
  219. package/src/components/organisms/Toaster/Toaster.test.ts +75 -0
  220. package/src/components/organisms/VideoControls/VideoControls.stories.ts +792 -0
  221. package/src/components/organisms/VideoControls/VideoControls.test.ts +1056 -0
  222. package/src/components/organisms/VideoPlayer/VideoPlayer.stories.ts +327 -0
  223. package/src/components/organisms/VideoPlayer/VideoPlayer.test.ts +439 -0
@@ -0,0 +1,67 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import {
3
+ colorAt,
4
+ gradientCss,
5
+ stopBarCss,
6
+ mixColors,
7
+ normalizeHex,
8
+ wrapAngle,
9
+ clampPercent,
10
+ DEFAULT_GRADIENT,
11
+ } from './gradient'
12
+
13
+ describe('gradient helpers', () => {
14
+ it('wraps angles and clamps percentages to whole numbers', () => {
15
+ expect(wrapAngle(360)).toBe(0)
16
+ expect(wrapAngle(-10)).toBe(350)
17
+ expect(wrapAngle(719.6)).toBe(0)
18
+ expect(clampPercent(-4)).toBe(0)
19
+ expect(clampPercent(100.4)).toBe(100)
20
+ expect(clampPercent(40.5)).toBe(41)
21
+ })
22
+
23
+ it('normalizes hexes to lower case without an opaque ff', () => {
24
+ expect(normalizeHex('#AABBCCFF')).toBe('#aabbcc')
25
+ expect(normalizeHex('#AABBCC80')).toBe('#aabbcc80')
26
+ })
27
+
28
+ it('renders in oklab always, as the app draws it, also off centre', () => {
29
+ expect(gradientCss(DEFAULT_GRADIENT)).toBe(
30
+ 'linear-gradient(180deg in oklab, #00000000 0%, #00000099 100%)',
31
+ )
32
+ const radial = { ...DEFAULT_GRADIENT, type: 'radial' as const }
33
+ expect(gradientCss(radial)).toBe('radial-gradient(in oklab, #00000000 0%, #00000099 100%)')
34
+ const offCentre = { ...radial, centerX: 30 }
35
+ expect(gradientCss(offCentre)).toBe(
36
+ 'radial-gradient(at 30% 50% in oklab, #00000000 0%, #00000099 100%)',
37
+ )
38
+ expect(stopBarCss(offCentre)).toBe(
39
+ 'linear-gradient(to right in oklab, #00000000 0%, #00000099 100%)',
40
+ )
41
+ })
42
+
43
+ it('mixes with premultiplied alpha, so a fade from transparent keeps the hue', () => {
44
+ expect(mixColors('#00000000', '#ff0000', 0.5, 'srgb')).toBe('#ff000080')
45
+ expect(mixColors('#00000000', '#ff0000', 0.5, 'oklab')).toBe('#ff000080')
46
+ })
47
+
48
+ it('mixes two opaque colours in oklab differently from srgb', () => {
49
+ const oklab = mixColors('#0000ff', '#ffffff', 0.5, 'oklab')
50
+ const srgb = mixColors('#0000ff', '#ffffff', 0.5, 'srgb')
51
+ expect(srgb).toBe('#8080ff')
52
+ expect(oklab).not.toBe(srgb)
53
+ })
54
+
55
+ it('returns the colour at a position, flat beyond the ends', () => {
56
+ const stops = [
57
+ { color: '#000000', position: 20 },
58
+ { color: '#ffffff', position: 80 },
59
+ ]
60
+ expect(colorAt(stops, 0)).toBe('#000000')
61
+ expect(colorAt(stops, 100)).toBe('#ffffff')
62
+ // halfway in oklab, which is lighter than the srgb #808080
63
+ expect(colorAt(stops, 50)).toBe(mixColors('#000000', '#ffffff', 0.5, 'oklab'))
64
+ expect(colorAt(stops, 50)).not.toBe('#808080')
65
+ expect(colorAt(DEFAULT_GRADIENT.stops, 50)).toBe('#0000004d')
66
+ })
67
+ })
@@ -0,0 +1,206 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import {
3
+ angleBetween,
4
+ clampToSquare,
5
+ isInSquare,
6
+ linearHandlePoint,
7
+ farthestCornerAngle,
8
+ linearFromPoints,
9
+ linearLength,
10
+ linearPoint,
11
+ linearPosition,
12
+ projectOnSegment,
13
+ radialEllipse,
14
+ radialPoint,
15
+ radialPosition,
16
+ radialRay,
17
+ radialT,
18
+ radialAngle,
19
+ viaFraction,
20
+ lerpPoint,
21
+ } from './gradientGeometry'
22
+
23
+ const S = 176
24
+
25
+ describe('linear geometry', () => {
26
+ it('has the CSS gradient line length', () => {
27
+ expect(linearLength(0, S)).toBeCloseTo(S)
28
+ expect(linearLength(90, S)).toBeCloseTo(S)
29
+ expect(linearLength(45, S)).toBeCloseTo(S * Math.SQRT2)
30
+ expect(linearLength(135, S)).toBeCloseTo(S * Math.SQRT2)
31
+ })
32
+
33
+ it('puts 0 % and 100 % where CSS does', () => {
34
+ // 0° runs upwards: 0 % at the bottom edge, 100 % at the top edge.
35
+ expect(linearPoint(0, 0, S)).toMatchObject({ x: S / 2, y: S })
36
+ expect(linearPoint(0, 100, S).y).toBeCloseTo(0)
37
+ // 90° runs to the right.
38
+ expect(linearPoint(90, 0, S).x).toBeCloseTo(0)
39
+ expect(linearPoint(90, 100, S).x).toBeCloseTo(S)
40
+ // 45° and 135° end exactly on the corners of the square.
41
+ expect(linearPoint(45, 100, S).x).toBeCloseTo(S)
42
+ expect(linearPoint(45, 100, S).y).toBeCloseTo(0)
43
+ expect(linearPoint(135, 100, S).x).toBeCloseTo(S)
44
+ expect(linearPoint(135, 100, S).y).toBeCloseTo(S)
45
+ })
46
+
47
+ it('sticks out of the square by at most 0.104·S at 22.5°', () => {
48
+ const end = linearPoint(22.5, 100, S)
49
+ expect(-end.y / S).toBeCloseTo(0.104, 2)
50
+ })
51
+
52
+ it.each([0, 22, 45, 90, 135, 300])('round-trips angle and positions at %i°', (angle) => {
53
+ const positions = { from: 10, via: 37, to: 85 }
54
+ const from = linearPoint(angle, positions.from, S)
55
+ const to = linearPoint(angle, positions.to, S)
56
+ const f = viaFraction(positions.from, positions.via, positions.to)
57
+ expect(
58
+ linearFromPoints({ from, to, viaFraction: f, previousAngle: 999, size: S, minDistance: 8 }),
59
+ ).toEqual({ angle, ...positions })
60
+ })
61
+
62
+ it('projects a point beside the middle line onto the same position', () => {
63
+ const onLine = linearPoint(90, 30, S)
64
+ expect(linearPosition(90, { x: onLine.x, y: onLine.y + 40 }, S)).toBe(30)
65
+ })
66
+
67
+ it('keeps the previous angle for points closer than the minimum distance', () => {
68
+ expect(angleBetween({ x: 10, y: 10 }, { x: 14, y: 14 }, 77, 8)).toBe(77)
69
+ expect(angleBetween({ x: 10, y: 10 }, { x: 10, y: 0 }, 77, 8)).toBe(0)
70
+ expect(angleBetween({ x: 10, y: 10 }, { x: 0, y: 10 }, 77, 8)).toBe(270)
71
+ })
72
+
73
+ it('clamps positions of points beyond the square', () => {
74
+ expect(linearPosition(90, { x: -30, y: 50 }, S)).toBe(0)
75
+ expect(linearPosition(90, { x: S + 30, y: 50 }, S)).toBe(100)
76
+ })
77
+
78
+ it('projects the via point onto the segment, clamped', () => {
79
+ const a = { x: 0, y: 0 }
80
+ const b = { x: 100, y: 0 }
81
+ expect(projectOnSegment({ x: 25, y: 40 }, a, b)).toBeCloseTo(0.25)
82
+ expect(projectOnSegment({ x: -50, y: 0 }, a, b)).toBe(0)
83
+ expect(projectOnSegment({ x: 150, y: 0 }, a, b)).toBe(1)
84
+ expect(lerpPoint(a, b, 0.25)).toEqual({ x: 25, y: 0 })
85
+ })
86
+ })
87
+
88
+ describe('handles inside the square', () => {
89
+ it('clamps a dragged pointer onto the square', () => {
90
+ expect(clampToSquare({ x: -30, y: 200 }, S)).toEqual({ x: 0, y: S })
91
+ expect(clampToSquare({ x: 50, y: 60 }, S)).toEqual({ x: 50, y: 60 })
92
+ })
93
+
94
+ it('draws 0 % and 100 % at 22° on the edge, along the gradient line', () => {
95
+ const outside = linearPoint(22, 100, S)
96
+ expect(isInSquare(outside, S)).toBe(false)
97
+ const end = linearHandlePoint(22, 100, S)
98
+ const start = linearHandlePoint(22, 0, S)
99
+ expect(isInSquare(end, S)).toBe(true)
100
+ expect(isInSquare(start, S)).toBe(true)
101
+ // on the edge: the top for 100 %, the bottom for 0 %
102
+ expect(end.y).toBeCloseTo(0)
103
+ expect(start.y).toBeCloseTo(S)
104
+ // still on the line through the middle at 22°
105
+ expect(angleBetween(start, end, -1, 1)).toBe(22)
106
+ })
107
+
108
+ it('leaves a canonical point that is inside where it is', () => {
109
+ expect(linearHandlePoint(90, 100, S)).toEqual(linearPoint(90, 100, S))
110
+ expect(linearHandlePoint(22, 50, S)).toEqual(linearPoint(22, 50, S))
111
+ const inner = linearHandlePoint(45, 80, S)
112
+ const canonical = linearPoint(45, 80, S)
113
+ expect(inner.x).toBeCloseTo(canonical.x)
114
+ expect(inner.y).toBeCloseTo(canonical.y)
115
+ })
116
+ })
117
+
118
+ describe('radial geometry', () => {
119
+ it('runs a centred ellipse through the corners', () => {
120
+ const ellipse = radialEllipse(50, 50, S)
121
+ expect(ellipse.rx).toBeCloseTo((S / 2) * Math.SQRT2)
122
+ expect(ellipse.ry).toBeCloseTo((S / 2) * Math.SQRT2)
123
+ expect(radialPosition(ellipse, { x: S, y: S })).toBe(100)
124
+ expect(radialPosition(ellipse, { x: S / 2, y: S / 2 })).toBe(0)
125
+ })
126
+
127
+ it('keeps the closest-side proportions and reaches the farthest corner off centre', () => {
128
+ const ellipse = radialEllipse(20, 80, S)
129
+ // closest sides: 20 % horizontally and 20 % vertically, so a circle
130
+ expect(ellipse.rx / ellipse.ry).toBeCloseTo(1)
131
+ // farthest corner is top-right
132
+ expect(radialT(ellipse, { x: S, y: 0 })).toBeCloseTo(1)
133
+ const skewed = radialEllipse(30, 50, S)
134
+ expect(skewed.rx / skewed.ry).toBeCloseTo(0.3 / 0.5)
135
+ expect(radialT(skewed, { x: S, y: 0 })).toBeCloseTo(1)
136
+ expect(radialT(skewed, { x: S, y: S })).toBeCloseTo(1)
137
+ })
138
+
139
+ it('stays within 100 % inside the square', () => {
140
+ const ellipse = radialEllipse(20, 80, S)
141
+ for (const corner of [
142
+ { x: 0, y: 0 },
143
+ { x: S, y: 0 },
144
+ { x: 0, y: S },
145
+ { x: S, y: S },
146
+ ]) {
147
+ expect(radialT(ellipse, corner)).toBeLessThanOrEqual(1 + 1e-9)
148
+ }
149
+ })
150
+
151
+ it('round-trips positions along the ray to the farthest corner', () => {
152
+ const ellipse = radialEllipse(20, 80, S)
153
+ const psi = farthestCornerAngle(ellipse, S)
154
+ const end = radialPoint(ellipse, psi, 100)
155
+ expect(end.x).toBeCloseTo(S)
156
+ expect(end.y).toBeCloseTo(0)
157
+ for (const position of [0, 13, 50, 87, 100]) {
158
+ expect(radialPosition(ellipse, radialPoint(ellipse, psi, position))).toBe(position)
159
+ }
160
+ })
161
+
162
+ it('clamps the centre to 1–99 so the ellipse never degenerates', () => {
163
+ const ellipse = radialEllipse(0, 100, S)
164
+ expect(Number.isFinite(ellipse.rx)).toBe(true)
165
+ expect(ellipse.rx).toBeGreaterThan(0)
166
+ })
167
+
168
+ describe('radialRay', () => {
169
+ it('falls back to the farthest corner when the dragged ray puts the end outside', () => {
170
+ // Repro: centre 30/70, via dragged to (60, 100) would put to = 100 % at (104, -43).
171
+ const ellipse = radialEllipse(30, 70, S)
172
+ const dragged = radialAngle(ellipse, { x: 60, y: 100 })
173
+ const outside = radialPoint(ellipse, dragged, 100)
174
+ expect(outside.x).toBeCloseTo(104, 0)
175
+ expect(outside.y).toBeCloseTo(-43, 0)
176
+ const ray = radialRay(ellipse, dragged, 100, S)
177
+ expect(ray).toBe(farthestCornerAngle(ellipse, S))
178
+ const end = radialPoint(ellipse, ray, 100)
179
+ expect(end.x).toBeCloseTo(S)
180
+ expect(end.y).toBeCloseTo(0)
181
+ })
182
+
183
+ it('falls back when a kept ray no longer fits after the centre moved', () => {
184
+ const before = radialEllipse(30, 70, S)
185
+ const dragged = radialAngle(before, { x: 20, y: 170 })
186
+ const position = radialPosition(before, { x: 20, y: 170 })
187
+ expect(radialRay(before, dragged, position, S)).toBe(dragged)
188
+
189
+ const after = radialEllipse(80, 80, S)
190
+ expect(radialPoint(after, dragged, position).y).toBeGreaterThan(S)
191
+ const ray = radialRay(after, dragged, position, S)
192
+ const end = radialPoint(after, ray, position)
193
+ expect(end.x).toBeGreaterThanOrEqual(0)
194
+ expect(end.y).toBeGreaterThanOrEqual(0)
195
+ expect(end.x).toBeLessThanOrEqual(S)
196
+ expect(end.y).toBeLessThanOrEqual(S)
197
+ })
198
+
199
+ it('keeps a dragged ray while the farthest stop stays inside', () => {
200
+ const ellipse = radialEllipse(50, 50, S)
201
+ const dragged = radialAngle(ellipse, { x: 30, y: 88 })
202
+ expect(radialRay(ellipse, dragged, 40, S)).toBe(dragged)
203
+ expect(radialRay(ellipse, null, 40, S)).toBe(farthestCornerAngle(ellipse, S))
204
+ })
205
+ })
206
+ })
@@ -0,0 +1,234 @@
1
+ import { ref } from 'vue'
2
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
3
+ import Combobox from './Combobox.vue'
4
+ import type { ComboboxOption } from '../ComboboxPanel/combobox-types'
5
+
6
+ // Flat args type used for Meta/StoryObj — avoids the discriminated union in
7
+ // ComboboxOptions causing TypeScript to erase props to Record<string,any>.
8
+ type ComboboxStoryArgs = {
9
+ variant?: 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'link'
10
+ icon?: string
11
+ placeholder?: string
12
+ placement?: 'left' | 'right'
13
+ matchWidth?: boolean
14
+ showChips?: boolean
15
+ fullWidth?: boolean
16
+ singleSelect?: boolean
17
+ options: ComboboxOption[]
18
+ allowNew?: boolean
19
+ }
20
+
21
+ const CITIES = [
22
+ { label: 'Almere', value: 'ALM' },
23
+ { label: 'Amersfoort', value: 'AMF' },
24
+ { label: 'Amsterdam', value: 'AMS' },
25
+ { label: 'Apeldoorn', value: 'APD' },
26
+ { label: 'Arnhem', value: 'ARN' },
27
+ { label: 'Bolsward', value: 'BOL' },
28
+ { label: 'Breda', value: 'BRD' },
29
+ { label: 'Den Haag', value: 'DHG' },
30
+ { label: 'Dokkum', value: 'DOK' },
31
+ { label: 'Dordrecht', value: 'DOR' },
32
+ { label: 'Eindhoven', value: 'EHV' },
33
+ { label: 'Enschede', value: 'ENS' },
34
+ { label: 'Franeker', value: 'FRK' },
35
+ { label: 'Groningen', value: 'GRQ' },
36
+ { label: 'Haarlem', value: 'HAA' },
37
+ { label: 'Harlingen', value: 'HRL' },
38
+ { label: 'Hindeloopen', value: 'HIN' },
39
+ { label: 'IJlst', value: 'IJL' },
40
+ { label: 'Leeuwarden', value: 'LWR' },
41
+ { label: 'Leiden', value: 'LEI' },
42
+ { label: 'Maastricht', value: 'MST' },
43
+ { label: 'Nijmegen', value: 'NJM' },
44
+ { label: 'Rotterdam', value: 'RTM' },
45
+ { label: 'Sloten', value: 'SLT' },
46
+ { label: 'Sneek', value: 'SNK' },
47
+ { label: 'Stavoren', value: 'STV' },
48
+ { label: "'s-Hertogenbosch", value: 'HTG' },
49
+ { label: 'Tilburg', value: 'TLB' },
50
+ { label: 'Utrecht', value: 'UTC' },
51
+ { label: 'Workum', value: 'WRK' },
52
+ { label: 'Zaanstad', value: 'ZAA' },
53
+ { label: 'Zwolle', value: 'ZWO' },
54
+ ]
55
+
56
+ export default {
57
+ title: 'Components/Organisms/Combobox',
58
+ // Cast needed: withDefaults + discriminated union props cause Storybook's
59
+ // component type check to fail; ComboboxStoryArgs is used for args instead.
60
+ component: Combobox as unknown as Meta<ComboboxStoryArgs>['component'],
61
+ decorators: [
62
+ () => ({
63
+ template:
64
+ '<div style="min-height: 450px; padding: 16px; display: flex; align-items: start; justify-content: center; position: relative;"><story /></div>',
65
+ }),
66
+ ],
67
+ } satisfies Meta<ComboboxStoryArgs>
68
+
69
+ type Story = StoryObj<ComboboxStoryArgs>
70
+
71
+ export const StringOptions: Story = {
72
+ render: (args) => ({
73
+ components: { Combobox },
74
+ setup() {
75
+ const selected = ref<string[]>([])
76
+ const remove = (val: string) => {
77
+ selected.value = selected.value.filter((v) => v !== val)
78
+ }
79
+ return { args, selected, remove }
80
+ },
81
+ template: `
82
+ <Combobox v-bind="args" v-model="selected">Steden</Combobox>
83
+ <div style="position: absolute; top: 0; right: 0; font-size: 12px; color: #6b7280;">
84
+ <div v-for="val in selected" :key="val" style="cursor: pointer;" @click="remove(val)">{{ val }}</div>
85
+ <em v-if="!selected.length">Geen selectie</em>
86
+ </div>
87
+ `,
88
+ }),
89
+ args: {
90
+ placeholder: 'Zoeken...',
91
+ options: CITIES.map((city) => city.label),
92
+ },
93
+ }
94
+
95
+ export const AllowNew: Story = {
96
+ render: (args) => ({
97
+ components: { Combobox },
98
+ setup() {
99
+ const selected = ref<(string | number)[]>([])
100
+ const remove = (val: string | number) => {
101
+ selected.value = selected.value.filter((v) => v !== val)
102
+ }
103
+ return { args, selected, remove }
104
+ },
105
+ template: `
106
+ <Combobox v-bind="args" v-model="selected">Tags</Combobox>
107
+ <div style="position: absolute; top: 0; right: 0; font-size: 12px; color: #6b7280;">
108
+ <div v-for="val in selected" :key="val" style="cursor: pointer;" @click="remove(val)">{{ val }}</div>
109
+ <em v-if="!selected.length">Geen selectie</em>
110
+ </div>
111
+ `,
112
+ }),
113
+ args: {
114
+ placeholder: 'Zoeken of toevoegen...',
115
+ options: CITIES.map((city) => city.label),
116
+ allowNew: true,
117
+ },
118
+ }
119
+
120
+ export const ObjectOptions: Story = {
121
+ render: (args) => ({
122
+ components: { Combobox },
123
+ setup() {
124
+ const selected = ref<(string | number)[]>([])
125
+ const remove = (val: string | number) => {
126
+ selected.value = selected.value.filter((v) => v !== val)
127
+ }
128
+ return { args, selected, remove }
129
+ },
130
+ template: `
131
+ <Combobox v-bind="args" v-model="selected">Steden</Combobox>
132
+ <div style="position: absolute; top: 0; right: 0; font-size: 12px; color: #6b7280;">
133
+ <div v-for="val in selected" :key="val" style="cursor: pointer;" @click="remove(val)">{{ val }}</div>
134
+ <em v-if="!selected.length">Geen selectie</em>
135
+ </div>
136
+ `,
137
+ }),
138
+ args: {
139
+ placeholder: 'Selecteer een stad...',
140
+ options: CITIES,
141
+ },
142
+ }
143
+
144
+ export const SingleSelect: Story = {
145
+ render: (args) => ({
146
+ components: { Combobox },
147
+ setup() {
148
+ const selected = ref<(string | number)[]>([])
149
+ return { args, selected }
150
+ },
151
+ template: `
152
+ <Combobox v-bind="args" v-model="selected">{{ selected[0] ?? 'Kies een stad' }}</Combobox>
153
+ <div style="position: absolute; top: 0; right: 0; font-size: 12px; color: #6b7280;">
154
+ <span>{{ selected[0] ?? '—' }}</span>
155
+ </div>
156
+ `,
157
+ }),
158
+ args: {
159
+ placeholder: 'Zoeken...',
160
+ options: CITIES,
161
+ singleSelect: true,
162
+ },
163
+ }
164
+
165
+ export const MatchWidth: Story = {
166
+ render: (args) => ({
167
+ components: { Combobox },
168
+ setup() {
169
+ const selected = ref<(string | number)[]>([])
170
+ return { args, selected }
171
+ },
172
+ template: `
173
+ <div style="width: 320px;">
174
+ <Combobox v-bind="args" v-model="selected" style="width: 100%;" full-width match-width>Kies een stad</Combobox>
175
+ </div>
176
+ `,
177
+ }),
178
+ args: {
179
+ placeholder: 'Zoeken...',
180
+ options: CITIES,
181
+ matchWidth: true,
182
+ fullWidth: true,
183
+ },
184
+ }
185
+
186
+ export const FullWidth: Story = {
187
+ decorators: [
188
+ () => ({
189
+ template:
190
+ '<div style="min-height: 450px; padding: 16px; display: flex; flex-direction: column; gap: 8px; position: relative;"><story /></div>',
191
+ }),
192
+ ],
193
+ render: (args) => ({
194
+ components: { Combobox },
195
+ setup() {
196
+ const selected = ref<(string | number)[]>([])
197
+ return { args, selected }
198
+ },
199
+ template: `
200
+ <p style="font-size: 12px; color: #6b7280; margin: 0;">The combobox below stretches to fill its container width.</p>
201
+ <div style="width: 100%; border: 1px dashed #d1d5db; padding: 12px; border-radius: 8px; box-sizing: border-box;">
202
+ <Combobox v-bind="args" v-model="selected">Kies een stad</Combobox>
203
+ </div>
204
+ `,
205
+ }),
206
+ args: {
207
+ placeholder: 'Zoeken...',
208
+ options: CITIES,
209
+ fullWidth: true,
210
+ matchWidth: true,
211
+ },
212
+ }
213
+
214
+ export const HideChips: Story = {
215
+ render: (args) => ({
216
+ components: { Combobox },
217
+ setup() {
218
+ const selected = ref<(string | number)[]>([])
219
+ return { args, selected }
220
+ },
221
+ template: `
222
+ <Combobox v-bind="args" v-model="selected">Steden</Combobox>
223
+ <div style="position: absolute; top: 0; right: 0; font-size: 12px; color: #6b7280;">
224
+ <div v-for="val in selected" :key="val">{{ val }}</div>
225
+ <em v-if="!selected.length">Geen selectie</em>
226
+ </div>
227
+ `,
228
+ }),
229
+ args: {
230
+ placeholder: 'Zoeken...',
231
+ options: CITIES,
232
+ showChips: false,
233
+ },
234
+ }