@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.
- package/dist/index.js +49 -126
- package/dist/nuxt.js +0 -1
- package/package.json +38 -36
- package/src/attribute-contract.test.ts +151 -0
- package/src/components/atoms/AnimatedNumber/AnimatedNumber.stories.ts +36 -0
- package/src/components/atoms/AnimatedNumber/AnimatedNumber.test.ts +21 -0
- package/src/components/atoms/Avatar/Avatar.stories.ts +122 -0
- package/src/components/atoms/Avatar/Avatar.test.ts +134 -0
- package/src/components/atoms/Button/Button.stories.ts +290 -0
- package/src/components/atoms/Button/Button.test.ts +406 -0
- package/src/components/atoms/Card/Card.stories.ts +45 -0
- package/src/components/atoms/Card/Card.test.ts +46 -0
- package/src/components/atoms/Checkbox/Checkbox.stories.ts +35 -0
- package/src/components/atoms/Checkbox/Checkbox.test.ts +55 -0
- package/src/components/atoms/Chip/Chip.stories.ts +274 -0
- package/src/components/atoms/Chip/Chip.test.ts +230 -0
- package/src/components/atoms/EnergyLabel/EnergyLabel.stories.ts +189 -0
- package/src/components/atoms/EnergyLabel/EnergyLabel.test.ts +100 -0
- package/src/components/atoms/FloatingPanel/FloatingPanel.stories.ts +677 -0
- package/src/components/atoms/FloatingPanel/FloatingPanel.test.ts +864 -0
- package/src/components/atoms/Icon/Icon.stories.ts +63 -0
- package/src/components/atoms/Icon/Icon.test.ts +89 -0
- package/src/components/atoms/Loader/Loader.stories.ts +36 -0
- package/src/components/atoms/Loader/Loader.test.ts +40 -0
- package/src/components/atoms/PickerItem/PickerItem.stories.ts +172 -0
- package/src/components/atoms/PickerItem/PickerItem.test.ts +142 -0
- package/src/components/atoms/PickerPreview/PickerPreview.stories.ts +183 -0
- package/src/components/atoms/PickerPreview/PickerPreview.test.ts +137 -0
- package/src/components/atoms/PresenceIndicator/PresenceIndicator.stories.ts +21 -0
- package/src/components/atoms/PropertyListing/PropertyListing.stories.ts +38 -0
- package/src/components/atoms/PropertyListing/PropertyListing.test.ts +46 -0
- package/src/components/atoms/SelectableOption/SelectableOption.stories.ts +131 -0
- package/src/components/atoms/SelectableOption/SelectableOption.test.ts +96 -0
- package/src/components/atoms/Skeleton/Skeleton.stories.ts +27 -0
- package/src/components/atoms/Skeleton/Skeleton.test.ts +28 -0
- package/src/components/atoms/Slider/Slider.stories.ts +151 -0
- package/src/components/atoms/Slider/Slider.test.ts +191 -0
- package/src/components/atoms/Squircle/Squircle.stories.ts +114 -0
- package/src/components/atoms/Squircle/Squircle.test.ts +70 -0
- package/src/components/atoms/Switch/Switch.stories.ts +91 -0
- package/src/components/atoms/Switch/Switch.test.ts +183 -0
- package/src/components/atoms/Tooltip/Tooltip.stories.ts +158 -0
- package/src/components/atoms/Tooltip/Tooltip.test.ts +134 -0
- package/src/components/editor/atoms/EditorDimensionField/EditorDimensionField.stories.ts +124 -0
- package/src/components/editor/atoms/EditorDimensionField/EditorDimensionField.test.ts +464 -0
- package/src/components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.stories.ts +266 -0
- package/src/components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.test.ts +421 -0
- package/src/components/editor/atoms/EditorSettingsGroup/EditorSettingsGroup.stories.ts +365 -0
- package/src/components/editor/atoms/EditorSettingsGroup/EditorSettingsGroup.test.ts +484 -0
- package/src/components/editor/composables/useRadioGrid.test.ts +261 -0
- package/src/components/editor/molecules/EditorAlignItems/EditorAlignItems.stories.ts +73 -0
- package/src/components/editor/molecules/EditorAlignItems/EditorAlignItems.test.ts +189 -0
- package/src/components/editor/molecules/EditorAlignmentGrid/EditorAlignmentGrid.stories.ts +309 -0
- package/src/components/editor/molecules/EditorAlignmentGrid/EditorAlignmentGrid.test.ts +359 -0
- package/src/components/editor/molecules/EditorAnchor/EditorAnchor.stories.ts +138 -0
- package/src/components/editor/molecules/EditorAnchor/EditorAnchor.test.ts +344 -0
- package/src/components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.stories.ts +87 -0
- package/src/components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.test.ts +262 -0
- package/src/components/editor/molecules/EditorBlur/EditorBlur.stories.ts +115 -0
- package/src/components/editor/molecules/EditorBlur/EditorBlur.test.ts +243 -0
- package/src/components/editor/molecules/EditorBorder/EditorBorder.stories.ts +187 -0
- package/src/components/editor/molecules/EditorBorder/EditorBorder.test.ts +456 -0
- package/src/components/editor/molecules/EditorColor/EditorColor.stories.ts +563 -0
- package/src/components/editor/molecules/EditorColor/EditorColor.test.ts +689 -0
- package/src/components/editor/molecules/EditorColor/EditorColorGradient.test.ts +793 -0
- package/src/components/editor/molecules/EditorColor/gradientClass.test.ts +303 -0
- package/src/components/editor/molecules/EditorDashPattern/EditorDashPattern.stories.ts +72 -0
- package/src/components/editor/molecules/EditorDashPattern/EditorDashPattern.test.ts +124 -0
- package/src/components/editor/molecules/EditorFlexDirection/EditorFlexDirection.stories.ts +46 -0
- package/src/components/editor/molecules/EditorFlexDirection/EditorFlexDirection.test.ts +97 -0
- package/src/components/editor/molecules/EditorFontSize/EditorFontSize.stories.ts +248 -0
- package/src/components/editor/molecules/EditorFontSize/EditorFontSize.test.ts +288 -0
- package/src/components/editor/molecules/EditorGap/EditorGap.stories.ts +129 -0
- package/src/components/editor/molecules/EditorGap/EditorGap.test.ts +253 -0
- package/src/components/editor/molecules/EditorHeadingType/EditorHeadingType.stories.ts +68 -0
- package/src/components/editor/molecules/EditorHeadingType/EditorHeadingType.test.ts +150 -0
- package/src/components/editor/molecules/EditorJustifyContent/EditorJustifyContent.stories.ts +71 -0
- package/src/components/editor/molecules/EditorJustifyContent/EditorJustifyContent.test.ts +161 -0
- package/src/components/editor/molecules/EditorLetterSpacing/EditorLetterSpacing.stories.ts +204 -0
- package/src/components/editor/molecules/EditorLetterSpacing/EditorLetterSpacing.test.ts +238 -0
- package/src/components/editor/molecules/EditorLineHeight/EditorLineHeight.stories.ts +257 -0
- package/src/components/editor/molecules/EditorLineHeight/EditorLineHeight.test.ts +286 -0
- package/src/components/editor/molecules/EditorLineStyle/EditorLineStyle.stories.ts +135 -0
- package/src/components/editor/molecules/EditorLineStyle/EditorLineStyle.test.ts +230 -0
- package/src/components/editor/molecules/EditorMaxWidth/EditorMaxWidth.stories.ts +154 -0
- package/src/components/editor/molecules/EditorMaxWidth/EditorMaxWidth.test.ts +112 -0
- package/src/components/editor/molecules/EditorOverrideStatus/EditorOverrideStatus.stories.ts +206 -0
- package/src/components/editor/molecules/EditorOverrideStatus/EditorOverrideStatus.test.ts +332 -0
- package/src/components/editor/molecules/EditorPadding/EditorPadding.stories.ts +374 -0
- package/src/components/editor/molecules/EditorPadding/EditorPadding.test.ts +712 -0
- package/src/components/editor/molecules/EditorRadius/EditorRadius.stories.ts +64 -0
- package/src/components/editor/molecules/EditorRadius/EditorRadius.test.ts +137 -0
- package/src/components/editor/molecules/EditorShadow/EditorShadow.stories.ts +103 -0
- package/src/components/editor/molecules/EditorShadow/EditorShadow.test.ts +168 -0
- package/src/components/editor/molecules/EditorSize/EditorSize.stories.ts +84 -0
- package/src/components/editor/molecules/EditorSize/EditorSize.test.ts +253 -0
- package/src/components/editor/molecules/EditorSizeLimit/EditorSizeLimit.stories.ts +49 -0
- package/src/components/editor/molecules/EditorSizeLimit/EditorSizeLimit.test.ts +43 -0
- package/src/components/editor/molecules/EditorTextAlign/EditorTextAlign.stories.ts +69 -0
- package/src/components/editor/molecules/EditorTextAlign/EditorTextAlign.test.ts +155 -0
- package/src/components/molecules/Announcement/Announcement.stories.ts +224 -0
- package/src/components/molecules/Announcement/Announcement.test.ts +161 -0
- package/src/components/molecules/ContactInfoCard/ContactInfoCard.stories.ts +99 -0
- package/src/components/molecules/ContactInfoCard/ContactInfoCard.test.ts +70 -0
- package/src/components/molecules/CustomerJourneyTile/CustomerJourneyTile.stories.ts +148 -0
- package/src/components/molecules/CustomerJourneyTile/CustomerJourneyTile.test.ts +271 -0
- package/src/components/molecules/DoughnutChart/DoughnutChart.stories.ts +45 -0
- package/src/components/molecules/DoughnutChart/DoughnutChart.test.ts +205 -0
- package/src/components/molecules/InfoBlock/InfoBlock.stories.ts +84 -0
- package/src/components/molecules/InfoBlock/InfoBlock.test.ts +101 -0
- package/src/components/molecules/InputField/InputField.stories.ts +117 -0
- package/src/components/molecules/InputField/InputField.test.ts +138 -0
- package/src/components/molecules/InputSelect/InputSelect.stories.ts +116 -0
- package/src/components/molecules/InputSelect/InputSelect.test.ts +127 -0
- package/src/components/molecules/ModalFooter/ModalFooter.stories.ts +204 -0
- package/src/components/molecules/ModalFooter/ModalFooter.test.ts +276 -0
- package/src/components/molecules/PageCard/PageCard.stories.ts +100 -0
- package/src/components/molecules/PageCard/PageCard.test.ts +91 -0
- package/src/components/molecules/PageContainer/PageContainer.stories.ts +90 -0
- package/src/components/molecules/PageContainer/PageContainer.test.ts +50 -0
- package/src/components/molecules/PageMockup/PageMockup.stories.ts +38 -0
- package/src/components/molecules/PageMockup/PageMockup.test.ts +20 -0
- package/src/components/molecules/PageTypeTile/PageTypeTile.stories.ts +63 -0
- package/src/components/molecules/PageTypeTile/PageTypeTile.test.ts +41 -0
- package/src/components/molecules/Pagination/Pagination.stories.ts +72 -0
- package/src/components/molecules/Pagination/Pagination.test.ts +130 -0
- package/src/components/molecules/PickerGroup/PickerGroup.stories.ts +90 -0
- package/src/components/molecules/PickerGroup/PickerGroup.test.ts +145 -0
- package/src/components/molecules/PickerList/PickerList.stories.ts +436 -0
- package/src/components/molecules/PickerList/PickerList.test.ts +870 -0
- package/src/components/molecules/PlaceholderInput/PlaceholderInput.stories.ts +48 -0
- package/src/components/molecules/PlaceholderInput/PlaceholderInput.test.ts +107 -0
- package/src/components/molecules/PropertyCard/PropertyCard.stories.ts +46 -0
- package/src/components/molecules/PropertyCard/PropertyCard.test.ts +59 -0
- package/src/components/molecules/Select/Select.stories.ts +264 -0
- package/src/components/molecules/Select/Select.test.ts +315 -0
- package/src/components/molecules/SidebarMenuItem/SidebarMenuItem.stories.ts +84 -0
- package/src/components/molecules/SidebarMenuItem/SidebarMenuItem.test.ts +103 -0
- package/src/components/molecules/SkeletonPropertyCard/SkeletonPropertyCard.stories.ts +11 -0
- package/src/components/molecules/SkeletonPropertyCard/SkeletonPropertyCard.test.ts +38 -0
- package/src/components/molecules/SocialIcons/SocialIcons.stories.ts +28 -0
- package/src/components/molecules/SocialIcons/SocialIcons.test.ts +58 -0
- package/src/components/molecules/SocialMediaCustomTemplate/SocialMediaCustomTemplate.stories.ts +11 -0
- package/src/components/molecules/SocialMediaCustomTemplate/SocialMediaCustomTemplate.test.ts +116 -0
- package/src/components/molecules/SocialMediaType/SocialMediaType.stories.ts +37 -0
- package/src/components/molecules/SocialMediaType/SocialMediaType.test.ts +126 -0
- package/src/components/molecules/Source/Source.stories.ts +35 -0
- package/src/components/molecules/Source/Source.test.ts +45 -0
- package/src/components/molecules/TabbedContent/TabbedContent.stories.ts +96 -0
- package/src/components/molecules/TabbedContent/TabbedContent.test.ts +109 -0
- package/src/components/molecules/Tabs/Tabs.stories.ts +230 -0
- package/src/components/molecules/Tabs/Tabs.test.ts +280 -0
- package/src/components/molecules/TemplateCard/TemplateCard.stories.ts +327 -0
- package/src/components/molecules/TemplateCard/TemplateCard.test.ts +339 -0
- package/src/components/molecules/Textarea/Textarea.stories.ts +216 -0
- package/src/components/molecules/Textarea/Textarea.test.ts +191 -0
- package/src/components/molecules/TimelineEvent/TimelineEvent.stories.ts +319 -0
- package/src/components/molecules/TimelineEvent/TimelineEvent.test.ts +224 -0
- package/src/components/molecules/TimelineLanes/TimelineLanes.stories.ts +29 -0
- package/src/components/molecules/TimelineLanes/TimelineLanes.test.ts +30 -0
- package/src/components/molecules/Toast/Toast.stories.ts +110 -0
- package/src/components/molecules/Toast/Toast.test.ts +103 -0
- package/src/components/molecules/ToggleGroup/ToggleGroup.stories.ts +297 -0
- package/src/components/molecules/ToggleGroup/ToggleGroup.test.ts +578 -0
- package/src/components/molecules/Tree/Tree.stories.ts +884 -0
- package/src/components/molecules/Tree/Tree.test.ts +1247 -0
- package/src/components/molecules/Tree/tree-drop.test.ts +223 -0
- package/src/components/organisms/Calendar/Calendar.stories.ts +108 -0
- package/src/components/organisms/Calendar/Calendar.test.ts +279 -0
- package/src/components/organisms/Calendar/_CalendarDayView.test.ts +104 -0
- package/src/components/organisms/Calendar/_CalendarHeader.test.ts +86 -0
- package/src/components/organisms/Calendar/_CalendarMonthView.test.ts +52 -0
- package/src/components/organisms/Calendar/_CalendarTimePicker.test.ts +89 -0
- package/src/components/organisms/Calendar/_CalendarYearView.test.ts +56 -0
- package/src/components/organisms/ColorPicker/ColorPicker.stories.ts +229 -0
- package/src/components/organisms/ColorPicker/ColorPicker.test.ts +638 -0
- package/src/components/organisms/ColorPicker/gradient.test.ts +67 -0
- package/src/components/organisms/ColorPicker/gradientGeometry.test.ts +206 -0
- package/src/components/organisms/Combobox/Combobox.stories.ts +234 -0
- package/src/components/organisms/Combobox/Combobox.test.ts +512 -0
- package/src/components/organisms/Combobox/Combobox.vue +1 -1
- package/src/components/organisms/ComboboxPanel/ComboboxPanel.stories.ts +158 -0
- package/src/components/organisms/ComboboxPanel/ComboboxPanel.test.ts +524 -0
- package/src/components/organisms/ComboboxPanel/ComboboxPanel.vue +1 -1
- package/src/components/organisms/CountrySelect/CountrySelect.stories.ts +139 -0
- package/src/components/organisms/CountrySelect/CountrySelect.test.ts +149 -0
- package/src/components/organisms/DatePicker/DatePicker.stories.ts +163 -0
- package/src/components/organisms/DatePicker/DatePicker.test.ts +214 -0
- package/src/components/organisms/DropdownButton/DropdownButton.stories.ts +123 -0
- package/src/components/organisms/DropdownButton/DropdownButton.test.ts +379 -0
- package/src/components/organisms/FontPicker/FontPicker.stories.ts +476 -0
- package/src/components/organisms/FontPicker/FontPicker.test.ts +873 -0
- package/src/components/organisms/Header/Header.stories.ts +75 -0
- package/src/components/organisms/Header/Header.test.ts +233 -0
- package/src/components/organisms/IconPicker/IconPicker.stories.ts +734 -0
- package/src/components/organisms/IconPicker/IconPicker.test.ts +669 -0
- package/src/components/organisms/IconPicker/_IconPickerGrid.test.ts +184 -0
- package/src/components/organisms/IconPicker/_IconPickerRail.test.ts +102 -0
- package/src/components/organisms/IconPicker/useIconPickerNavigation.test.ts +215 -0
- package/src/components/organisms/Modal/Modal.stories.ts +350 -0
- package/src/components/organisms/Modal/Modal.test.ts +275 -0
- package/src/components/organisms/PickerPanel/PickerPanel.stories.ts +452 -0
- package/src/components/organisms/PickerPanel/PickerPanel.test.ts +358 -0
- package/src/components/organisms/ProgressBar/ProgressBar.stories.ts +100 -0
- package/src/components/organisms/ProgressBar/ProgressBar.test.ts +417 -0
- package/src/components/organisms/ReferenceCard/ReferenceCard.stories.ts +191 -0
- package/src/components/organisms/ReferenceCard/ReferenceCard.test.ts +165 -0
- package/src/components/organisms/SocialMediaTemplate/SocialMediaTemplate.stories.ts +65 -0
- package/src/components/organisms/SocialMediaTemplate/SocialMediaTemplate.test.ts +466 -0
- package/src/components/organisms/StepperHeader/StepperHeader.stories.ts +91 -0
- package/src/components/organisms/StepperHeader/StepperHeader.test.ts +279 -0
- package/src/components/organisms/Table/Table.stories.ts +332 -0
- package/src/components/organisms/Table/Table.test.ts +320 -0
- package/src/components/organisms/TimelinePhaseblock/TimelinePhaseblock.stories.ts +198 -0
- package/src/components/organisms/TimelinePhaseblock/TimelinePhaseblock.test.ts +283 -0
- package/src/components/organisms/TimelineTrack/TimelineTrack.stories.ts +120 -0
- package/src/components/organisms/TimelineTrack/TimelineTrack.test.ts +91 -0
- package/src/components/organisms/Toaster/Toaster.stories.ts +51 -0
- package/src/components/organisms/Toaster/Toaster.test.ts +75 -0
- package/src/components/organisms/VideoControls/VideoControls.stories.ts +792 -0
- package/src/components/organisms/VideoControls/VideoControls.test.ts +1056 -0
- package/src/components/organisms/VideoPlayer/VideoPlayer.stories.ts +327 -0
- package/src/components/organisms/VideoPlayer/VideoPlayer.test.ts +439 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest'
|
|
2
|
+
import { mount, type VueWrapper } from '@vue/test-utils'
|
|
3
|
+
import EditorFlexDirection, { EDITOR_FLEX_DIRECTION_OPTIONS } from './EditorFlexDirection.vue'
|
|
4
|
+
|
|
5
|
+
// ToggleGroup awaits document.fonts.ready on mount, which jsdom does not ship.
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
Object.defineProperty(document, 'fonts', {
|
|
8
|
+
value: { ready: Promise.resolve() },
|
|
9
|
+
configurable: true,
|
|
10
|
+
})
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
/** Option labels, read from the interactive layer rather than the overlay copy. */
|
|
14
|
+
const optionLabels = (wrapper: VueWrapper) =>
|
|
15
|
+
wrapper.findAll('button').map((button) => button.text())
|
|
16
|
+
|
|
17
|
+
describe('EditorFlexDirection', () => {
|
|
18
|
+
// The options sit in a ToggleGroup toolbar; its name is the block label, so
|
|
19
|
+
// a screen reader announces which setting the pressed option belongs to.
|
|
20
|
+
it('names the options toolbar with the block label', async () => {
|
|
21
|
+
const wrapper = mount(EditorFlexDirection, { props: { modelValue: 'flex-row' } })
|
|
22
|
+
expect(wrapper.get('[role="toolbar"]').attributes('aria-label')).toBe('Richting')
|
|
23
|
+
|
|
24
|
+
await wrapper.setProps({ label: 'Custom' })
|
|
25
|
+
expect(wrapper.get('[role="toolbar"]').attributes('aria-label')).toBe('Custom')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
// No assertions on typography utilities: labels are found by text or by
|
|
29
|
+
// structure, so restyling the block or the toggle group cannot turn these red.
|
|
30
|
+
it('renders the Dutch default label', () => {
|
|
31
|
+
const wrapper = mount(EditorFlexDirection, { props: { modelValue: 'flex-row' } })
|
|
32
|
+
|
|
33
|
+
expect(wrapper.find('p').text()).toBe('Richting')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('renders the Dutch default option labels', () => {
|
|
37
|
+
const wrapper = mount(EditorFlexDirection, { props: { modelValue: 'flex-row' } })
|
|
38
|
+
|
|
39
|
+
expect(optionLabels(wrapper)).toEqual(['Horizontaal', 'Verticaal'])
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('renders a custom label', () => {
|
|
43
|
+
const wrapper = mount(EditorFlexDirection, {
|
|
44
|
+
props: { modelValue: 'flex-row', label: 'Direction' },
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
expect(wrapper.find('p').text()).toBe('Direction')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('renders custom option labels', () => {
|
|
51
|
+
const wrapper = mount(EditorFlexDirection, {
|
|
52
|
+
props: {
|
|
53
|
+
modelValue: 'flex-row',
|
|
54
|
+
messages: { horizontal: 'Horizontal', vertical: 'Vertical' },
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
expect(optionLabels(wrapper)).toEqual(['Horizontal', 'Vertical'])
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('keeps the untouched default when only one message is overridden', () => {
|
|
62
|
+
const wrapper = mount(EditorFlexDirection, {
|
|
63
|
+
props: { modelValue: 'flex-row', messages: { vertical: 'Vertical' } },
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
expect(optionLabels(wrapper)).toEqual(['Horizontaal', 'Vertical'])
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('emits update:modelValue with the Tailwind class of the clicked option', async () => {
|
|
70
|
+
const wrapper = mount(EditorFlexDirection, { props: { modelValue: 'flex-row' } })
|
|
71
|
+
|
|
72
|
+
await wrapper.findAll('button')[1].trigger('click')
|
|
73
|
+
|
|
74
|
+
expect(wrapper.emitted('update:modelValue')).toEqual([['flex-col']])
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('renders an icon for each option', () => {
|
|
78
|
+
const wrapper = mount(EditorFlexDirection, { props: { modelValue: 'flex-row' } })
|
|
79
|
+
|
|
80
|
+
// Each icon is rendered twice: once per toggle group layer.
|
|
81
|
+
expect(wrapper.findAll('i')).toHaveLength(4)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('is always visible, so it renders no switch', () => {
|
|
85
|
+
const wrapper = mount(EditorFlexDirection, { props: { modelValue: 'flex-row' } })
|
|
86
|
+
|
|
87
|
+
expect(wrapper.find('input[type="checkbox"]').exists()).toBe(false)
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
// The consumer derives its class matchers from this list, so the list is a
|
|
92
|
+
// contract: any change here changes what the app recognises.
|
|
93
|
+
describe('EDITOR_FLEX_DIRECTION_OPTIONS', () => {
|
|
94
|
+
it('offers exactly the agreed options', () => {
|
|
95
|
+
expect(EDITOR_FLEX_DIRECTION_OPTIONS).toEqual([{ token: 'row' }, { token: 'col' }])
|
|
96
|
+
})
|
|
97
|
+
})
|