@nexxtmove/ui 0.1.22 → 0.1.24

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 (67) hide show
  1. package/dist/index.d.ts +76 -8
  2. package/dist/index.js +2758 -252
  3. package/dist/nuxt.d.ts +1 -0
  4. package/dist/nuxt.js +49 -0
  5. package/package.json +24 -13
  6. package/src/assets/images/Template_aanvragen.jpg +0 -0
  7. package/src/assets/svg/type_post.svg +1 -0
  8. package/src/assets/svg/type_reel.svg +54 -0
  9. package/src/assets/svg/type_story.svg +46 -0
  10. package/src/assets/svg/type_tiktok.svg +1 -0
  11. package/src/assets/video/Template_aanvragen.mp4 +0 -0
  12. package/src/components/AnimatedNumber/AnimatedNumber.stories.ts +15 -0
  13. package/src/components/AnimatedNumber/AnimatedNumber.test.ts +56 -0
  14. package/src/components/AnimatedNumber/AnimatedNumber.vue +61 -0
  15. package/src/components/Button/Button.stories.ts +212 -0
  16. package/src/components/Button/Button.test.ts +318 -0
  17. package/src/components/Button/Button.vue +67 -0
  18. package/src/components/Calendar/Calendar.stories.ts +91 -0
  19. package/src/components/Calendar/Calendar.test.ts +279 -0
  20. package/src/components/Calendar/Calendar.vue +239 -0
  21. package/src/components/Calendar/_CalendarDayView.test.ts +104 -0
  22. package/src/components/Calendar/_CalendarDayView.vue +169 -0
  23. package/src/components/Calendar/_CalendarHeader.test.ts +86 -0
  24. package/src/components/Calendar/_CalendarHeader.vue +123 -0
  25. package/src/components/Calendar/_CalendarMonthView.test.ts +52 -0
  26. package/src/components/Calendar/_CalendarMonthView.vue +79 -0
  27. package/src/components/Calendar/_CalendarYearView.test.ts +56 -0
  28. package/src/components/Calendar/_CalendarYearView.vue +83 -0
  29. package/src/components/Calendar/calendar.types.ts +11 -0
  30. package/src/components/Chip/Chip.stories.ts +42 -0
  31. package/src/components/Chip/Chip.test.ts +51 -0
  32. package/src/components/Chip/Chip.vue +37 -0
  33. package/src/components/DatePicker/DatePicker.stories.ts +149 -0
  34. package/src/components/DatePicker/DatePicker.test.ts +191 -0
  35. package/src/components/DatePicker/DatePicker.vue +145 -0
  36. package/src/components/Header/Header.stories.ts +48 -0
  37. package/src/components/Header/Header.test.ts +169 -0
  38. package/src/components/Header/Header.vue +42 -0
  39. package/src/components/Icon/Icon.stories.ts +50 -0
  40. package/src/components/Icon/Icon.test.ts +73 -0
  41. package/src/components/Icon/Icon.vue +20 -0
  42. package/src/components/InfoBlock/InfoBlock.stories.ts +90 -0
  43. package/src/components/InfoBlock/InfoBlock.test.ts +101 -0
  44. package/src/components/InfoBlock/InfoBlock.vue +70 -0
  45. package/src/components/ProgressBar/ProgressBar.stories.ts +30 -0
  46. package/src/components/ProgressBar/ProgressBar.test.ts +314 -0
  47. package/src/components/ProgressBar/ProgressBar.vue +102 -0
  48. package/src/components/SocialIcons/SocialIcons.stories.ts +34 -0
  49. package/src/components/SocialIcons/SocialIcons.test.ts +58 -0
  50. package/src/components/SocialIcons/SocialIcons.vue +58 -0
  51. package/src/components/SocialMediaCustomTemplate/SocialMediaCustomTemplate.stories.ts +11 -0
  52. package/src/components/SocialMediaCustomTemplate/SocialMediaCustomTemplate.test.ts +131 -0
  53. package/src/components/SocialMediaCustomTemplate/SocialMediaCustomTemplate.vue +55 -0
  54. package/src/components/SocialMediaTemplate/SocialMediaTemplate.stories.ts +71 -0
  55. package/src/components/SocialMediaTemplate/SocialMediaTemplate.test.ts +466 -0
  56. package/src/components/SocialMediaTemplate/SocialMediaTemplate.vue +130 -0
  57. package/src/components/SocialMediaType/SocialMediaType.stories.ts +43 -0
  58. package/src/components/SocialMediaType/SocialMediaType.test.ts +126 -0
  59. package/src/components/SocialMediaType/SocialMediaType.vue +117 -0
  60. package/src/components/StepperHeader/StepperHeader.stories.ts +47 -0
  61. package/src/components/StepperHeader/StepperHeader.test.ts +244 -0
  62. package/src/components/StepperHeader/StepperHeader.vue +37 -0
  63. package/src/components.json +16 -0
  64. package/src/env.d.ts +23 -0
  65. package/src/index.css +2 -0
  66. package/src/index.ts +15 -0
  67. package/src/nuxt.ts +50 -0
@@ -0,0 +1,314 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import ProgressBar from './ProgressBar.vue'
4
+
5
+ describe('ProgressBar', () => {
6
+ beforeEach(() => {
7
+ vi.clearAllMocks()
8
+ vi.stubGlobal('console', { warn: vi.fn() })
9
+ })
10
+
11
+ it('renders correct percentage for first step', () => {
12
+ const wrapper = mount(ProgressBar, {
13
+ props: {
14
+ steps: 5,
15
+ currentStep: 0,
16
+ },
17
+ })
18
+
19
+ const progressbar = wrapper.find('[role="progressbar"]')
20
+ expect(progressbar.attributes('aria-valuenow')).toBe('0')
21
+
22
+ const innerBar = progressbar.find('div')
23
+ expect(innerBar.attributes('style')).toContain('width: 0%')
24
+ })
25
+
26
+ it('renders correct percentage for middle step', () => {
27
+ const wrapper = mount(ProgressBar, {
28
+ props: {
29
+ steps: 5,
30
+ currentStep: 2,
31
+ },
32
+ })
33
+
34
+ const progressbar = wrapper.find('[role="progressbar"]')
35
+ expect(progressbar.attributes('aria-valuenow')).toBe('50')
36
+ expect(progressbar.find('div').attributes('style')).toContain('width: 50%')
37
+ })
38
+
39
+ it('renders correct percentage for last step', () => {
40
+ const wrapper = mount(ProgressBar, {
41
+ props: {
42
+ steps: 5,
43
+ currentStep: 4,
44
+ },
45
+ })
46
+
47
+ const progressbar = wrapper.find('[role="progressbar"]')
48
+ expect(progressbar.attributes('aria-valuenow')).toBe('100')
49
+ expect(progressbar.find('div').attributes('style')).toContain('width: 100%')
50
+ })
51
+
52
+ it('clamps currentStep within range', () => {
53
+ const wrapperLow = mount(ProgressBar, {
54
+ props: { steps: 5, currentStep: -1 },
55
+ })
56
+ expect(wrapperLow.find('[role="progressbar"]').attributes('aria-valuenow')).toBe('0')
57
+
58
+ const wrapperHigh = mount(ProgressBar, {
59
+ props: { steps: 5, currentStep: 10 },
60
+ })
61
+ expect(wrapperHigh.find('[role="progressbar"]').attributes('aria-valuenow')).toBe('100')
62
+ })
63
+
64
+ it('handles edge case of 0 or 1 steps', () => {
65
+ const wrapperZero = mount(ProgressBar, {
66
+ props: { steps: 0, currentStep: 0 },
67
+ })
68
+ expect(wrapperZero.find('[role="progressbar"]').attributes('aria-valuenow')).toBe('0')
69
+
70
+ const wrapperOne = mount(ProgressBar, {
71
+ props: { steps: 1, currentStep: 0 },
72
+ })
73
+ expect(wrapperOne.find('[role="progressbar"]').attributes('aria-valuenow')).toBe('100')
74
+ })
75
+
76
+ it('handles empty stepTitles array', () => {
77
+ const wrapper = mount(ProgressBar, {
78
+ props: { steps: 5, currentStep: 0, stepTitles: [] },
79
+ })
80
+ expect(wrapper.find('ol').exists()).toBe(false)
81
+ expect(wrapper.find('.tabular-nums').exists()).toBe(false)
82
+ })
83
+
84
+ it('displays correct step text without titles', () => {
85
+ const wrapper = mount(ProgressBar, {
86
+ props: { steps: 5, currentStep: 2 },
87
+ })
88
+
89
+ expect(wrapper.find('[role="progressbar"]').attributes('aria-valuetext')).toBe('Step 3 of 5')
90
+ })
91
+
92
+ it('displays correct step text with titles', () => {
93
+ const stepTitles = ['Start', 'Middle', 'End']
94
+ const wrapper = mount(ProgressBar, {
95
+ props: {
96
+ steps: 3,
97
+ currentStep: 1,
98
+ stepTitles,
99
+ },
100
+ })
101
+
102
+ expect(wrapper.find('[role="progressbar"]').attributes('aria-valuetext')).toBe(
103
+ 'Step 2 of 3: Middle',
104
+ )
105
+ })
106
+
107
+ it('renders step titles and marks current step', () => {
108
+ const stepTitles = ['Step 1', 'Step 2', 'Step 3']
109
+ const wrapper = mount(ProgressBar, {
110
+ props: {
111
+ steps: 3,
112
+ currentStep: 1,
113
+ stepTitles,
114
+ },
115
+ })
116
+
117
+ const items = wrapper.findAll('li')
118
+ expect(items).toHaveLength(3)
119
+ expect(items[1]!.attributes('aria-current')).toBe('step')
120
+ expect(items[1]!.classes()).toContain('text-sapphire-500')
121
+ expect(items[0]!.attributes('aria-current')).toBeUndefined()
122
+ })
123
+
124
+ it('uses default progress label', () => {
125
+ const wrapper = mount(ProgressBar, {
126
+ props: {
127
+ steps: 5,
128
+ currentStep: 0,
129
+ },
130
+ })
131
+ expect(wrapper.find('[role="progressbar"]').attributes('aria-label')).toBe('Voortgang')
132
+ })
133
+
134
+ it('uses custom progress label', () => {
135
+ const wrapper = mount(ProgressBar, {
136
+ props: {
137
+ steps: 5,
138
+ currentStep: 0,
139
+ progressLabel: 'My Progress',
140
+ },
141
+ })
142
+ expect(wrapper.find('[role="progressbar"]').attributes('aria-label')).toBe('My Progress')
143
+ })
144
+
145
+ it('warns when stepTitles length mismatch', () => {
146
+ const spy = vi.spyOn(console, 'warn')
147
+ mount(ProgressBar, {
148
+ props: {
149
+ steps: 5,
150
+ currentStep: 0,
151
+ stepTitles: ['Only one'],
152
+ },
153
+ })
154
+ expect(spy).toHaveBeenCalledWith(expect.stringContaining('[NexxtProgressBar]'))
155
+ })
156
+
157
+ it('handles missing title in titles array gracefully', () => {
158
+ const wrapper = mount(ProgressBar, {
159
+ props: {
160
+ steps: 3,
161
+ currentStep: 2, // Last step
162
+ stepTitles: ['Only First'], // Only index 0 exists
163
+ },
164
+ })
165
+ // clampedStep is 2, stepTitles[2] is undefined
166
+ expect(wrapper.find('[role="progressbar"]').attributes('aria-valuetext')).toBe('Step 3 of 3')
167
+ })
168
+
169
+ it('has responsive container query classes', () => {
170
+ const wrapper = mount(ProgressBar, {
171
+ props: {
172
+ steps: 5,
173
+ currentStep: 0,
174
+ stepTitles: ['Step 1', 'Step 2', 'Step 3', 'Step 4', 'Step 5'],
175
+ },
176
+ })
177
+
178
+ // Verify the root is a container
179
+ expect(wrapper.classes()).toContain('@container')
180
+
181
+ // Verify the digit counter hides when the container is large
182
+ const counter = wrapper.find('.tabular-nums')
183
+ expect(counter.classes()).toContain('@md:hidden')
184
+
185
+ // Verify list items hide titles when the container is small
186
+ const firstTitle = wrapper.find('li')
187
+ expect(firstTitle.classes()).toContain('@max-md:hidden')
188
+ })
189
+
190
+ it('updates animation direction correctly based on step change', async () => {
191
+ const wrapper = mount(ProgressBar, {
192
+ props: {
193
+ steps: 5,
194
+ currentStep: 2,
195
+ stepTitles: ['1', '2', '3', '4', '5'],
196
+ },
197
+ global: {
198
+ stubs: {
199
+ // Stubbing Transition lets us inspect the dynamic classes passed to it
200
+ Transition: {
201
+ template: '<slot />',
202
+ props: ['enterFromClass', 'leaveToClass'],
203
+ },
204
+ },
205
+ },
206
+ })
207
+
208
+ // Increment: Step 3 -> 4
209
+ await wrapper.setProps({ currentStep: 3 })
210
+ let transition = wrapper.findComponent({ name: 'Transition' })
211
+ expect(transition.props('enterFromClass')).toBe('translate-y-full')
212
+ expect(transition.props('leaveToClass')).toBe('-translate-y-full')
213
+
214
+ // Decrement: Step 4 -> 2
215
+ await wrapper.setProps({ currentStep: 1 })
216
+ transition = wrapper.findComponent({ name: 'Transition' })
217
+ expect(transition.props('enterFromClass')).toBe('-translate-y-full')
218
+ expect(transition.props('leaveToClass')).toBe('translate-y-full')
219
+ })
220
+
221
+ it('emits update:currentStep when a previous step is clicked', async () => {
222
+ const stepTitles = ['Step 1', 'Step 2', 'Step 3']
223
+ const wrapper = mount(ProgressBar, {
224
+ props: {
225
+ steps: 3,
226
+ currentStep: 2,
227
+ stepTitles,
228
+ },
229
+ })
230
+
231
+ const items = wrapper.findAll('li')
232
+
233
+ // Click first step (index 0, previous)
234
+ await items[0]!.trigger('click')
235
+ expect(wrapper.emitted('update:currentStep')).toBeTruthy()
236
+ expect(wrapper.emitted('update:currentStep')![0]).toEqual([0])
237
+
238
+ // Click second step (index 1, previous)
239
+ await items[1]!.trigger('click')
240
+ expect(wrapper.emitted('update:currentStep')![1]).toEqual([1])
241
+ })
242
+
243
+ it('does not emit update:currentStep when the current or a future step is clicked', async () => {
244
+ const stepTitles = ['Step 1', 'Step 2', 'Step 3']
245
+ const wrapper = mount(ProgressBar, {
246
+ props: {
247
+ steps: 3,
248
+ currentStep: 1,
249
+ stepTitles,
250
+ },
251
+ })
252
+
253
+ const items = wrapper.findAll('li')
254
+
255
+ // Click current step (index 1)
256
+ await items[1]!.trigger('click')
257
+ expect(wrapper.emitted('update:currentStep')).toBeFalsy()
258
+
259
+ // Click future step (index 2)
260
+ await items[2]!.trigger('click')
261
+ expect(wrapper.emitted('update:currentStep')).toBeFalsy()
262
+ })
263
+
264
+ it('applies interactive classes only to previous steps', () => {
265
+ const stepTitles = ['Step 1', 'Step 2', 'Step 3']
266
+ const wrapper = mount(ProgressBar, {
267
+ props: {
268
+ steps: 3,
269
+ currentStep: 1,
270
+ stepTitles,
271
+ },
272
+ })
273
+
274
+ const items = wrapper.findAll('li')
275
+
276
+ // Previous step
277
+ expect(items[0]!.classes()).toContain('cursor-pointer')
278
+ expect(items[0]!.classes()).toContain('hover:text-sapphire-400')
279
+ expect(items[0]!.attributes('role')).toBe('button')
280
+ expect(items[0]!.attributes('tabindex')).toBe('0')
281
+ expect(items[0]!.attributes('aria-label')).toBe('Ga naar Step 1')
282
+
283
+ // Current step
284
+ expect(items[1]!.classes()).not.toContain('cursor-pointer')
285
+ expect(items[1]!.attributes('role')).toBeUndefined()
286
+ expect(items[1]!.attributes('tabindex')).toBeUndefined()
287
+
288
+ // Future step
289
+ expect(items[2]!.classes()).not.toContain('cursor-pointer')
290
+ expect(items[2]!.attributes('role')).toBeUndefined()
291
+ expect(items[2]!.attributes('tabindex')).toBeUndefined()
292
+ })
293
+
294
+ it('emits update:currentStep when Enter or Space is pressed on a previous step', async () => {
295
+ const stepTitles = ['Step 1', 'Step 2', 'Step 3']
296
+ const wrapper = mount(ProgressBar, {
297
+ props: {
298
+ steps: 3,
299
+ currentStep: 2,
300
+ stepTitles,
301
+ },
302
+ })
303
+
304
+ const items = wrapper.findAll('li')
305
+
306
+ // Press Enter on first step
307
+ await items[0]!.trigger('keydown.enter')
308
+ expect(wrapper.emitted('update:currentStep')![0]).toEqual([0])
309
+
310
+ // Press Space on second step
311
+ await items[1]!.trigger('keydown.space')
312
+ expect(wrapper.emitted('update:currentStep')![1]).toEqual([1])
313
+ })
314
+ })
@@ -0,0 +1,102 @@
1
+ <script setup lang="ts">
2
+ import { computed, watchEffect } from 'vue'
3
+ import AnimatedNumber from '../AnimatedNumber/AnimatedNumber.vue'
4
+
5
+ interface NexxtProgressBarProps {
6
+ steps: number
7
+ currentStep: number
8
+ progressLabel?: string
9
+ stepTitles?: string[]
10
+ }
11
+
12
+ defineOptions({
13
+ name: 'NexxtProgressBar',
14
+ })
15
+
16
+ defineEmits(['update:currentStep'])
17
+
18
+ const {
19
+ progressLabel = 'Voortgang',
20
+ stepTitles,
21
+ steps,
22
+ currentStep,
23
+ } = defineProps<NexxtProgressBarProps>()
24
+
25
+ const clampedStep = computed(() => Math.min(Math.max(0, currentStep), Math.max(0, steps - 1)))
26
+ const displayStep = computed(() => (steps > 0 ? clampedStep.value + 1 : 0))
27
+
28
+ const percentage = computed(() => {
29
+ if (steps <= 1) return steps === 1 ? 100 : 0
30
+ return (clampedStep.value / (steps - 1)) * 100
31
+ })
32
+
33
+ const hasTitles = computed(() => Boolean(stepTitles?.length))
34
+
35
+ watchEffect(() => {
36
+ if (stepTitles && stepTitles.length !== steps) {
37
+ console.warn(
38
+ `[NexxtProgressBar] The "stepTitles" array length (${stepTitles.length}) must match the "steps" prop (${steps}).`,
39
+ )
40
+ }
41
+ })
42
+ </script>
43
+
44
+ <template>
45
+ <div class="@container">
46
+ <div class="flex flex-col @md:gap-2">
47
+ <div class="flex items-center justify-between gap-2">
48
+ <div
49
+ class="progress-bar progress-bar-bar h-2 w-full overflow-hidden rounded-full"
50
+ role="progressbar"
51
+ :aria-label="progressLabel"
52
+ aria-valuemin="0"
53
+ aria-valuemax="100"
54
+ :aria-valuenow="Math.round(percentage)"
55
+ :aria-valuetext="
56
+ hasTitles && stepTitles?.[clampedStep]
57
+ ? `Step ${displayStep} of ${steps}: ${stepTitles[clampedStep]}`
58
+ : `Step ${displayStep} of ${steps}`
59
+ "
60
+ >
61
+ <div
62
+ class="progress-bar-progress h-full transition-all duration-500 ease-out"
63
+ :style="{ width: `${percentage}%` }"
64
+ :aria-hidden="true"
65
+ />
66
+ </div>
67
+ <div
68
+ v-if="hasTitles"
69
+ class="flex items-center small-normal tabular-nums @md:hidden"
70
+ aria-hidden="true"
71
+ >
72
+ <AnimatedNumber :value="displayStep" />
73
+ <span>/{{ steps }}</span>
74
+ </div>
75
+ </div>
76
+ <ol
77
+ v-if="hasTitles"
78
+ class="flex justify-between @max-md:order-first @max-md:justify-center"
79
+ aria-label="Progress steps"
80
+ >
81
+ <li
82
+ v-for="(title, index) in stepTitles"
83
+ :key="title"
84
+ class="rounded-xs transition-colors duration-500 focus-visible:outline-3 @max-md:hidden @max-md:body-normal @min-md:small-normal"
85
+ :class="[
86
+ index === clampedStep ? 'text-sapphire-500 @max-md:inline' : '',
87
+ index < clampedStep ? 'cursor-pointer duration-200! hover:text-sapphire-400' : '',
88
+ ]"
89
+ :aria-current="index === clampedStep ? 'step' : undefined"
90
+ :role="index < clampedStep ? 'button' : undefined"
91
+ :tabindex="index < clampedStep ? 0 : undefined"
92
+ :aria-label="index < clampedStep ? `Ga naar ${title}` : undefined"
93
+ @click="index < clampedStep && $emit('update:currentStep', index)"
94
+ @keydown.enter="index < clampedStep && $emit('update:currentStep', index)"
95
+ @keydown.space.prevent="index < clampedStep && $emit('update:currentStep', index)"
96
+ >
97
+ {{ title }}
98
+ </li>
99
+ </ol>
100
+ </div>
101
+ </div>
102
+ </template>
@@ -0,0 +1,34 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import SocialIcons from './SocialIcons.vue'
3
+
4
+ export default {
5
+ title: 'Components/Molecules/Social Icons',
6
+ component: SocialIcons,
7
+ parameters: {
8
+ design: {
9
+ type: 'figma',
10
+ url: 'https://www.figma.com/design/CdbFJ7qUga6mtjagcPDvYK/Design-System?node-id=524-17&t=CbHvOQfEMIr7M5mO-11',
11
+ },
12
+ },
13
+ } satisfies Meta<typeof SocialIcons>
14
+
15
+ type Story = StoryObj<typeof SocialIcons>
16
+
17
+ export const Default: Story = {
18
+ args: {
19
+ facebook: true,
20
+ instagram: true,
21
+ linkedin: true,
22
+ x: true,
23
+ google: true,
24
+ tiktok: true,
25
+ },
26
+ }
27
+
28
+ export const Selective: Story = {
29
+ args: {
30
+ facebook: true,
31
+ linkedin: true,
32
+ instagram: true,
33
+ },
34
+ }
@@ -0,0 +1,58 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import SocialIcons from './SocialIcons.vue'
4
+
5
+ describe('SocialIcons', () => {
6
+ it('renders nothing when no props are set', () => {
7
+ const wrapper = mount(SocialIcons)
8
+ expect(wrapper.findAll('.rounded-full')).toHaveLength(0)
9
+ })
10
+
11
+ it('renders only the requested icons', () => {
12
+ const wrapper = mount(SocialIcons, {
13
+ props: {
14
+ facebook: true,
15
+ instagram: true,
16
+ },
17
+ })
18
+
19
+ const icons = wrapper.findAll('.rounded-full')
20
+ expect(icons).toHaveLength(2)
21
+
22
+ // Check for specific background colors to verify which icons are rendered
23
+ expect(icons[0]!.classes()).toContain('bg-brands-facebook')
24
+ expect(icons[1]!.classes()).toContain('bg-brands-instagram')
25
+ })
26
+
27
+ it('has correct accessibility attributes', () => {
28
+ const wrapper = mount(SocialIcons, {
29
+ props: {
30
+ facebook: true,
31
+ },
32
+ })
33
+
34
+ const list = wrapper.find('ul')
35
+ expect(list.exists()).toBe(true)
36
+ expect(list.attributes('aria-label')).toBe('Sociale media kanalen')
37
+
38
+ const item = wrapper.find('li')
39
+ expect(item.exists()).toBe(true)
40
+ expect(item.attributes('aria-label')).toBe('Facebook')
41
+ expect(item.findComponent({ name: 'NexxtIcon' }).attributes('aria-hidden')).toBe('true')
42
+ })
43
+
44
+ it('renders all icons when all props are true', () => {
45
+ const wrapper = mount(SocialIcons, {
46
+ props: {
47
+ facebook: true,
48
+ instagram: true,
49
+ linkedin: true,
50
+ x: true,
51
+ google: true,
52
+ tiktok: true,
53
+ },
54
+ })
55
+
56
+ expect(wrapper.findAll('.rounded-full')).toHaveLength(6)
57
+ })
58
+ })
@@ -0,0 +1,58 @@
1
+ <script lang="ts" setup>
2
+ import { computed } from 'vue'
3
+ import Icon from '../Icon/Icon.vue'
4
+
5
+ export interface NexxtSocialIconsProps {
6
+ facebook?: boolean
7
+ instagram?: boolean
8
+ linkedin?: boolean
9
+ x?: boolean
10
+ google?: boolean
11
+ tiktok?: boolean
12
+ }
13
+
14
+ defineOptions({
15
+ name: 'NexxtSocialIcons',
16
+ })
17
+
18
+ const props = defineProps<NexxtSocialIconsProps>()
19
+
20
+ const socialPlatforms = [
21
+ { name: 'facebook', label: 'Facebook', icon: 'facebook-f' },
22
+ { name: 'instagram', label: 'Instagram', icon: 'instagram' },
23
+ { name: 'linkedin', label: 'LinkedIn', icon: 'linkedin-in' },
24
+ { name: 'x', label: 'X (voorheen Twitter)', icon: 'x-twitter' },
25
+ { name: 'google', label: 'Google', icon: 'google' },
26
+ { name: 'tiktok', label: 'TikTok', icon: 'tiktok' },
27
+ ] as const
28
+
29
+ const brandColors: Record<string, string> = {
30
+ facebook: 'bg-brands-facebook',
31
+ instagram: 'bg-brands-instagram',
32
+ linkedin: 'bg-brands-linkedin',
33
+ x: 'bg-brands-x',
34
+ google: 'bg-brands-google',
35
+ tiktok: 'bg-brands-tiktok',
36
+ }
37
+
38
+ const activeIcons = computed(() =>
39
+ socialPlatforms.filter((platform) => props[platform.name as keyof NexxtSocialIconsProps]),
40
+ )
41
+ </script>
42
+
43
+ <template>
44
+ <ul
45
+ class="m-0 inline-flex list-none items-start justify-start gap-0.5 p-0"
46
+ aria-label="Sociale media kanalen"
47
+ >
48
+ <li
49
+ v-for="platform in activeIcons"
50
+ :key="platform.name"
51
+ class="inline-flex h-5 w-5 items-center justify-center rounded-full outline-1 outline-white"
52
+ :class="brandColors[platform.name]"
53
+ :aria-label="platform.label"
54
+ >
55
+ <Icon :name="platform.icon" type="brands" class="text-[10px] text-white" aria-hidden="true" />
56
+ </li>
57
+ </ul>
58
+ </template>
@@ -0,0 +1,11 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import SocialMediaCustomTemplate from './SocialMediaCustomTemplate.vue'
3
+
4
+ export default {
5
+ title: 'Components/Molecules/Social Media Custom Template',
6
+ component: SocialMediaCustomTemplate,
7
+ } satisfies Meta<typeof SocialMediaCustomTemplate>
8
+
9
+ type Story = StoryObj<typeof SocialMediaCustomTemplate>
10
+
11
+ export const Default: Story = {}