@nexxtmove/ui 0.1.22 → 0.1.23
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.d.ts +75 -8
- package/dist/index.js +2408 -252
- package/dist/nuxt.d.ts +1 -0
- package/dist/nuxt.js +49 -0
- package/package.json +23 -13
- package/src/assets/images/Template_aanvragen.jpg +0 -0
- package/src/assets/svg/type_post.svg +1 -0
- package/src/assets/svg/type_reel.svg +54 -0
- package/src/assets/svg/type_story.svg +46 -0
- package/src/assets/svg/type_tiktok.svg +1 -0
- package/src/assets/video/Template_aanvragen.mp4 +0 -0
- package/src/components/AnimatedNumber/AnimatedNumber.stories.ts +15 -0
- package/src/components/AnimatedNumber/AnimatedNumber.test.ts +56 -0
- package/src/components/AnimatedNumber/AnimatedNumber.vue +61 -0
- package/src/components/Button/Button.stories.ts +212 -0
- package/src/components/Button/Button.test.ts +318 -0
- package/src/components/Button/Button.vue +67 -0
- package/src/components/Calendar/Calendar.stories.ts +91 -0
- package/src/components/Calendar/Calendar.test.ts +269 -0
- package/src/components/Calendar/Calendar.vue +221 -0
- package/src/components/Calendar/_CalendarDayView.test.ts +145 -0
- package/src/components/Calendar/_CalendarDayView.vue +156 -0
- package/src/components/Calendar/_CalendarHeader.test.ts +86 -0
- package/src/components/Calendar/_CalendarHeader.vue +123 -0
- package/src/components/Calendar/_CalendarMonthView.test.ts +68 -0
- package/src/components/Calendar/_CalendarMonthView.vue +70 -0
- package/src/components/Calendar/_CalendarYearView.vue +77 -0
- package/src/components/Calendar/calendar.types.ts +10 -0
- package/src/components/Chip/Chip.stories.ts +42 -0
- package/src/components/Chip/Chip.test.ts +51 -0
- package/src/components/Chip/Chip.vue +37 -0
- package/src/components/DatePicker/DatePicker.stories.ts +149 -0
- package/src/components/DatePicker/DatePicker.test.ts +191 -0
- package/src/components/DatePicker/DatePicker.vue +142 -0
- package/src/components/Header/Header.stories.ts +48 -0
- package/src/components/Header/Header.test.ts +169 -0
- package/src/components/Header/Header.vue +42 -0
- package/src/components/Icon/Icon.stories.ts +50 -0
- package/src/components/Icon/Icon.test.ts +73 -0
- package/src/components/Icon/Icon.vue +20 -0
- package/src/components/InfoBlock/InfoBlock.stories.ts +90 -0
- package/src/components/InfoBlock/InfoBlock.test.ts +101 -0
- package/src/components/InfoBlock/InfoBlock.vue +70 -0
- package/src/components/ProgressBar/ProgressBar.stories.ts +30 -0
- package/src/components/ProgressBar/ProgressBar.test.ts +314 -0
- package/src/components/ProgressBar/ProgressBar.vue +102 -0
- package/src/components/SocialIcons/SocialIcons.stories.ts +34 -0
- package/src/components/SocialIcons/SocialIcons.test.ts +58 -0
- package/src/components/SocialIcons/SocialIcons.vue +58 -0
- package/src/components/SocialMediaCustomTemplate/SocialMediaCustomTemplate.stories.ts +11 -0
- package/src/components/SocialMediaCustomTemplate/SocialMediaCustomTemplate.test.ts +131 -0
- package/src/components/SocialMediaCustomTemplate/SocialMediaCustomTemplate.vue +55 -0
- package/src/components/SocialMediaTemplate/SocialMediaTemplate.stories.ts +71 -0
- package/src/components/SocialMediaTemplate/SocialMediaTemplate.test.ts +466 -0
- package/src/components/SocialMediaTemplate/SocialMediaTemplate.vue +130 -0
- package/src/components/SocialMediaType/SocialMediaType.stories.ts +43 -0
- package/src/components/SocialMediaType/SocialMediaType.test.ts +126 -0
- package/src/components/SocialMediaType/SocialMediaType.vue +117 -0
- package/src/components/StepperHeader/StepperHeader.stories.ts +47 -0
- package/src/components/StepperHeader/StepperHeader.test.ts +244 -0
- package/src/components/StepperHeader/StepperHeader.vue +37 -0
- package/src/components.json +16 -0
- package/src/env.d.ts +23 -0
- package/src/index.css +2 -0
- package/src/index.ts +15 -0
- package/src/nuxt.ts +50 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
|
|
4
|
+
interface NexxtChipProps {
|
|
5
|
+
variant?: 'default' | 'warning' | 'success' | 'danger'
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
defineOptions({
|
|
9
|
+
name: 'NexxtChip',
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
const { variant = 'default' } = defineProps<NexxtChipProps>()
|
|
13
|
+
|
|
14
|
+
const backgroundColor = computed(() => {
|
|
15
|
+
switch (variant) {
|
|
16
|
+
case 'warning':
|
|
17
|
+
return 'bg-orange-500'
|
|
18
|
+
case 'success':
|
|
19
|
+
return 'bg-green-500'
|
|
20
|
+
case 'danger':
|
|
21
|
+
return 'bg-brick-500'
|
|
22
|
+
default:
|
|
23
|
+
return 'bg-cornflower-blue-600'
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<template>
|
|
29
|
+
<span
|
|
30
|
+
class="inline-flex h-5 flex-col items-center justify-center gap-2.5 rounded-[200px] px-4"
|
|
31
|
+
:class="backgroundColor"
|
|
32
|
+
>
|
|
33
|
+
<span class="justify-start text-center extra-small-semibold text-white">
|
|
34
|
+
<slot>Chip</slot>
|
|
35
|
+
</span>
|
|
36
|
+
</span>
|
|
37
|
+
</template>
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import DatePicker from './DatePicker.vue'
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
title: 'Components/Molecules/DatePicker',
|
|
7
|
+
component: DatePicker,
|
|
8
|
+
decorators: [
|
|
9
|
+
() => ({ template: '<div style="min-height: 380px; padding: 16px;"><story /></div>' }),
|
|
10
|
+
],
|
|
11
|
+
argTypes: {
|
|
12
|
+
anchor: {
|
|
13
|
+
control: 'select',
|
|
14
|
+
options: ['bottom-right', 'bottom-left', 'top-right', 'top-left'],
|
|
15
|
+
},
|
|
16
|
+
monthYearPicker: { control: 'boolean' },
|
|
17
|
+
markedDates: { control: 'object' },
|
|
18
|
+
disabledDates: { control: 'object' },
|
|
19
|
+
minDate: { control: 'date' },
|
|
20
|
+
maxDate: { control: 'date' },
|
|
21
|
+
locale: { control: false },
|
|
22
|
+
},
|
|
23
|
+
parameters: {
|
|
24
|
+
design: {
|
|
25
|
+
type: 'figma',
|
|
26
|
+
url: 'https://www.figma.com/design/CdbFJ7qUga6mtjagcPDvYK/Design-System?node-id=713-355&t=CbHvOQfEMIr7M5mO-4',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
} satisfies Meta<typeof DatePicker>
|
|
30
|
+
|
|
31
|
+
type Story = StoryObj<typeof DatePicker>
|
|
32
|
+
|
|
33
|
+
export const Default: Story = {
|
|
34
|
+
args: {
|
|
35
|
+
placeholder: 'Selecteer datum',
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const AnchorBottomRight: Story = {
|
|
40
|
+
render: (args) => ({
|
|
41
|
+
components: { DatePicker },
|
|
42
|
+
setup() {
|
|
43
|
+
return { args }
|
|
44
|
+
},
|
|
45
|
+
template: `
|
|
46
|
+
<div style="min-height: 380px; display: flex; justify-content: flex-end; align-items: flex-start; padding: 16px;">
|
|
47
|
+
<DatePicker v-bind="args" />
|
|
48
|
+
</div>
|
|
49
|
+
`,
|
|
50
|
+
}),
|
|
51
|
+
args: {
|
|
52
|
+
placeholder: 'Selecteer datum',
|
|
53
|
+
anchor: 'bottom-right',
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const AnchorTopLeft: Story = {
|
|
58
|
+
render: (args) => ({
|
|
59
|
+
components: { DatePicker },
|
|
60
|
+
setup() {
|
|
61
|
+
return { args }
|
|
62
|
+
},
|
|
63
|
+
template: `
|
|
64
|
+
<div style="min-height: 380px; display: flex; align-items: flex-end; padding: 16px;">
|
|
65
|
+
<DatePicker v-bind="args" />
|
|
66
|
+
</div>
|
|
67
|
+
`,
|
|
68
|
+
}),
|
|
69
|
+
args: {
|
|
70
|
+
placeholder: 'Selecteer datum',
|
|
71
|
+
anchor: 'top-left',
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const AnchorTopRight: Story = {
|
|
76
|
+
render: (args) => ({
|
|
77
|
+
components: { DatePicker },
|
|
78
|
+
setup() {
|
|
79
|
+
return { args }
|
|
80
|
+
},
|
|
81
|
+
template: `
|
|
82
|
+
<div style="min-height: 380px; display: flex; justify-content: flex-end; align-items: flex-end; padding: 16px;">
|
|
83
|
+
<DatePicker v-bind="args" />
|
|
84
|
+
</div>
|
|
85
|
+
`,
|
|
86
|
+
}),
|
|
87
|
+
args: {
|
|
88
|
+
placeholder: 'Selecteer datum',
|
|
89
|
+
anchor: 'top-right',
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const WithoutAutoClose: Story = {
|
|
94
|
+
args: {
|
|
95
|
+
placeholder: 'Selecteer datum',
|
|
96
|
+
autoClose: false,
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export const CustomTrigger: Story = {
|
|
101
|
+
render: (args) => ({
|
|
102
|
+
components: { DatePicker },
|
|
103
|
+
setup() {
|
|
104
|
+
const selected = ref(args.modelValue)
|
|
105
|
+
return { args, selected }
|
|
106
|
+
},
|
|
107
|
+
template: `
|
|
108
|
+
<div style="min-height: 380px; padding: 16px;">
|
|
109
|
+
<p style="font-size: 13px; color: #555; margin-bottom: 12px;">Selected: {{ selected ? selected.toLocaleDateString() : 'None' }}</p>
|
|
110
|
+
<DatePicker v-bind="args" v-model="selected">
|
|
111
|
+
<template #trigger="{ toggle, isOpen }">
|
|
112
|
+
<button
|
|
113
|
+
type="button"
|
|
114
|
+
@click="toggle"
|
|
115
|
+
class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors shadow-sm font-medium"
|
|
116
|
+
>
|
|
117
|
+
{{ isOpen ? 'Sluiten' : 'Kies een datum uit de lijst' }}
|
|
118
|
+
</button>
|
|
119
|
+
</template>
|
|
120
|
+
</DatePicker>
|
|
121
|
+
</div>
|
|
122
|
+
`,
|
|
123
|
+
}),
|
|
124
|
+
args: {
|
|
125
|
+
modelValue: null,
|
|
126
|
+
},
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export const Playground: Story = {
|
|
130
|
+
render: (args) => ({
|
|
131
|
+
components: { DatePicker },
|
|
132
|
+
setup() {
|
|
133
|
+
const selected = ref<Date | null>(null)
|
|
134
|
+
return { args, selected }
|
|
135
|
+
},
|
|
136
|
+
template: `
|
|
137
|
+
<div style="min-height: 380px; padding: 16px;">
|
|
138
|
+
<DatePicker v-bind="args" v-model="selected" />
|
|
139
|
+
<p v-if="selected" style="margin-top: 12px; font-size: 13px; color: #555;">
|
|
140
|
+
Geselecteerd: {{ selected.toLocaleDateString('nl-NL') }}
|
|
141
|
+
</p>
|
|
142
|
+
</div>
|
|
143
|
+
`,
|
|
144
|
+
}),
|
|
145
|
+
args: {
|
|
146
|
+
placeholder: 'Selecteer datum',
|
|
147
|
+
monthYearPicker: true,
|
|
148
|
+
},
|
|
149
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from 'vitest'
|
|
2
|
+
import { mount, type VueWrapper } from '@vue/test-utils'
|
|
3
|
+
import { nextTick } from 'vue'
|
|
4
|
+
import DatePicker from './DatePicker.vue'
|
|
5
|
+
|
|
6
|
+
const YEAR = 2026
|
|
7
|
+
const MONTH = 2 // March (0-indexed)
|
|
8
|
+
const TODAY = new Date(YEAR, MONTH, 20)
|
|
9
|
+
|
|
10
|
+
const mountDatePicker = (props = {}) => mount(DatePicker, { props, attachTo: document.body })
|
|
11
|
+
|
|
12
|
+
const openPicker = async (wrapper: VueWrapper) => {
|
|
13
|
+
await wrapper.find('button').trigger('click')
|
|
14
|
+
await nextTick()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('DatePicker', () => {
|
|
18
|
+
let wrapper: VueWrapper
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
wrapper?.unmount()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
// ── Trigger button ───────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
it('renders trigger button with placeholder', () => {
|
|
27
|
+
wrapper = mountDatePicker({ placeholder: 'Kies een datum' })
|
|
28
|
+
expect(wrapper.text()).toContain('Kies een datum')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('uses default placeholder when none provided', () => {
|
|
32
|
+
wrapper = mountDatePicker()
|
|
33
|
+
expect(wrapper.text()).toContain('Selecteer datum')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
// ── Open / close ─────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
it('calendar is closed by default', () => {
|
|
39
|
+
wrapper = mountDatePicker()
|
|
40
|
+
expect(wrapper.find('[class*="rounded-xl"]').exists()).toBe(false)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('opens when trigger button is clicked', async () => {
|
|
44
|
+
wrapper = mountDatePicker()
|
|
45
|
+
await openPicker(wrapper)
|
|
46
|
+
expect(wrapper.find('[class*="rounded-xl"]').exists()).toBe(true)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('closes when trigger button is clicked again', async () => {
|
|
50
|
+
wrapper = mountDatePicker()
|
|
51
|
+
await openPicker(wrapper)
|
|
52
|
+
await wrapper.find('button').trigger('click')
|
|
53
|
+
await nextTick()
|
|
54
|
+
expect(wrapper.find('[class*="rounded-xl"]').exists()).toBe(false)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('closes when clicking outside', async () => {
|
|
58
|
+
wrapper = mountDatePicker()
|
|
59
|
+
await openPicker(wrapper)
|
|
60
|
+
document.body.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }))
|
|
61
|
+
await nextTick()
|
|
62
|
+
expect(wrapper.find('[class*="rounded-xl"]').exists()).toBe(false)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('closes on Escape key', async () => {
|
|
66
|
+
wrapper = mountDatePicker()
|
|
67
|
+
await openPicker(wrapper)
|
|
68
|
+
await wrapper.trigger('keydown', { key: 'Escape' })
|
|
69
|
+
await nextTick()
|
|
70
|
+
expect(wrapper.find('[class*="rounded-xl"]').exists()).toBe(false)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
// ── Day selection + autoClose ─────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
it('closes after selecting a day (autoClose default)', async () => {
|
|
76
|
+
wrapper = mountDatePicker({ modelValue: TODAY })
|
|
77
|
+
await openPicker(wrapper)
|
|
78
|
+
|
|
79
|
+
const dayButtons = wrapper
|
|
80
|
+
.findAll('button[type="button"]')
|
|
81
|
+
.filter((b) => /^\d{1,2}$/.test(b.text().trim()))
|
|
82
|
+
await dayButtons[10]!.trigger('click')
|
|
83
|
+
await nextTick()
|
|
84
|
+
|
|
85
|
+
expect(wrapper.find('[class*="rounded-xl"]').exists()).toBe(false)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('stays open after selecting a day if autoClose is false', async () => {
|
|
89
|
+
wrapper = mountDatePicker({ modelValue: TODAY, autoClose: false })
|
|
90
|
+
await openPicker(wrapper)
|
|
91
|
+
|
|
92
|
+
const dayButtons = wrapper
|
|
93
|
+
.findAll('button[type="button"]')
|
|
94
|
+
.filter((b) => /^\d{1,2}$/.test(b.text().trim()))
|
|
95
|
+
await dayButtons[10]!.trigger('click')
|
|
96
|
+
await nextTick()
|
|
97
|
+
|
|
98
|
+
expect(wrapper.find('[class*="rounded-xl"]').exists()).toBe(true)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('emits update:modelValue with the clicked date', async () => {
|
|
102
|
+
wrapper = mountDatePicker({ modelValue: TODAY })
|
|
103
|
+
await openPicker(wrapper)
|
|
104
|
+
|
|
105
|
+
const dayButtons = wrapper
|
|
106
|
+
.findAll('button[type="button"]')
|
|
107
|
+
.filter((b) => /^\d{1,2}$/.test(b.text().trim()))
|
|
108
|
+
await dayButtons[10]!.trigger('click')
|
|
109
|
+
await nextTick()
|
|
110
|
+
|
|
111
|
+
expect(wrapper.emitted('update:modelValue')).toBeTruthy()
|
|
112
|
+
expect(wrapper.emitted('update:modelValue')![0]![0]).toBeInstanceOf(Date)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
// ── Resets to days view on reopen ─────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
it('resets to days view when closed and reopened', async () => {
|
|
118
|
+
wrapper = mountDatePicker({ modelValue: TODAY, monthYearPicker: true })
|
|
119
|
+
await openPicker(wrapper)
|
|
120
|
+
|
|
121
|
+
// open month picker
|
|
122
|
+
const header = wrapper.find('[class*="cornflower-blue-600"]')
|
|
123
|
+
await header.find('button').trigger('click')
|
|
124
|
+
await nextTick()
|
|
125
|
+
expect(wrapper.find('[class*="grid-cols-3"]').exists()).toBe(true)
|
|
126
|
+
|
|
127
|
+
// close and reopen (v-if remounts Calendar with fresh pickerView = 'days')
|
|
128
|
+
await wrapper.find('button').trigger('click')
|
|
129
|
+
await nextTick()
|
|
130
|
+
await openPicker(wrapper)
|
|
131
|
+
expect(wrapper.find('[class*="grid-cols-7"]').exists()).toBe(true)
|
|
132
|
+
expect(wrapper.find('[class*="grid-cols-3"]').exists()).toBe(false)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
// ── Custom trigger slot ───────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
it('renders custom trigger through slot', () => {
|
|
138
|
+
wrapper = mount(DatePicker, {
|
|
139
|
+
slots: {
|
|
140
|
+
trigger: `
|
|
141
|
+
<template #trigger="{ toggle }">
|
|
142
|
+
<button class="custom-trigger" @click="toggle">Custom Button</button>
|
|
143
|
+
</template>
|
|
144
|
+
`,
|
|
145
|
+
},
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
expect(wrapper.find('.custom-trigger').exists()).toBe(true)
|
|
149
|
+
expect(wrapper.text()).toContain('Custom Button')
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('toggles picker via custom trigger toggle function', async () => {
|
|
153
|
+
wrapper = mount(DatePicker, {
|
|
154
|
+
slots: {
|
|
155
|
+
trigger: `
|
|
156
|
+
<template #trigger="{ toggle }">
|
|
157
|
+
<button class="custom-trigger" @click="toggle">Toggle</button>
|
|
158
|
+
</template>
|
|
159
|
+
`,
|
|
160
|
+
},
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
expect(wrapper.find('[class*="rounded-xl"]').exists()).toBe(false)
|
|
164
|
+
|
|
165
|
+
await wrapper.find('.custom-trigger').trigger('click')
|
|
166
|
+
await nextTick()
|
|
167
|
+
expect(wrapper.find('[class*="rounded-xl"]').exists()).toBe(true)
|
|
168
|
+
|
|
169
|
+
await wrapper.find('.custom-trigger').trigger('click')
|
|
170
|
+
await nextTick()
|
|
171
|
+
expect(wrapper.find('[class*="rounded-xl"]').exists()).toBe(false)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('provides isOpen state to custom trigger', async () => {
|
|
175
|
+
wrapper = mount(DatePicker, {
|
|
176
|
+
slots: {
|
|
177
|
+
trigger: `
|
|
178
|
+
<template #trigger="{ isOpen, toggle }">
|
|
179
|
+
<button class="custom-trigger" @click="toggle">{{ isOpen ? 'OPEN' : 'CLOSED' }}</button>
|
|
180
|
+
</template>
|
|
181
|
+
`,
|
|
182
|
+
},
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
expect(wrapper.find('.custom-trigger').text()).toBe('CLOSED')
|
|
186
|
+
|
|
187
|
+
await wrapper.find('.custom-trigger').trigger('click')
|
|
188
|
+
await nextTick()
|
|
189
|
+
expect(wrapper.find('.custom-trigger').text()).toBe('OPEN')
|
|
190
|
+
})
|
|
191
|
+
})
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
|
|
3
|
+
import NexxtButton from '../Button/Button.vue'
|
|
4
|
+
import NexxtCalendar from '../Calendar/Calendar.vue'
|
|
5
|
+
import type { NexxtCalendarProps } from '../Calendar/calendar.types'
|
|
6
|
+
|
|
7
|
+
export type CalendarAnchor = 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'
|
|
8
|
+
|
|
9
|
+
interface NexxtDatePickerProps extends NexxtCalendarProps {
|
|
10
|
+
placeholder?: string
|
|
11
|
+
anchor?: CalendarAnchor
|
|
12
|
+
autoClose?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
defineOptions({ name: 'NexxtDatePicker' })
|
|
16
|
+
|
|
17
|
+
const model = defineModel<Date | null>({ default: null })
|
|
18
|
+
|
|
19
|
+
const props = withDefaults(defineProps<NexxtDatePickerProps>(), {
|
|
20
|
+
placeholder: 'Selecteer datum',
|
|
21
|
+
anchor: 'bottom-left',
|
|
22
|
+
autoClose: true,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const PICKER_ONLY_KEYS = new Set(['placeholder', 'anchor', 'autoClose'])
|
|
26
|
+
|
|
27
|
+
const calendarProps = computed(
|
|
28
|
+
(): NexxtCalendarProps =>
|
|
29
|
+
Object.fromEntries(
|
|
30
|
+
Object.entries(props).filter(([key]) => !PICKER_ONLY_KEYS.has(key)),
|
|
31
|
+
) as NexxtCalendarProps,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
// ── State ─────────────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
const isOpen = ref(false)
|
|
37
|
+
const wrapperRef = ref<HTMLElement | null>(null)
|
|
38
|
+
const triggerRef = ref<HTMLElement | null>(null)
|
|
39
|
+
const popoverRef = ref<HTMLElement | null>(null)
|
|
40
|
+
|
|
41
|
+
// ── Watchers ──────────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
watch(isOpen, async (val) => {
|
|
44
|
+
if (!val) {
|
|
45
|
+
await nextTick()
|
|
46
|
+
const focusable = triggerRef.value?.querySelector<HTMLElement>(
|
|
47
|
+
'button, input, [tabindex]:not([tabindex="-1"])',
|
|
48
|
+
)
|
|
49
|
+
focusable?.focus()
|
|
50
|
+
} else {
|
|
51
|
+
await nextTick()
|
|
52
|
+
popoverRef.value?.focus()
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
// ── Handlers ──────────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
const handleSelect = () => {
|
|
59
|
+
if (props.autoClose) {
|
|
60
|
+
isOpen.value = false
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const handleKeydown = (event: KeyboardEvent) => {
|
|
65
|
+
if (!isOpen.value) return
|
|
66
|
+
if (event.key === 'Escape') {
|
|
67
|
+
isOpen.value = false
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
72
|
+
if (wrapperRef.value && !wrapperRef.value.contains(event.target as Node)) {
|
|
73
|
+
isOpen.value = false
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
onMounted(() => document.addEventListener('mousedown', handleClickOutside))
|
|
78
|
+
onUnmounted(() => document.removeEventListener('mousedown', handleClickOutside))
|
|
79
|
+
|
|
80
|
+
// ── Popover placement ─────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
const popoverClasses = computed(() => {
|
|
83
|
+
const map: Record<CalendarAnchor, string> = {
|
|
84
|
+
'bottom-right': 'top-full right-0 mt-2 origin-top-right',
|
|
85
|
+
'bottom-left': 'top-full left-0 mt-2 origin-top-left',
|
|
86
|
+
'top-right': 'bottom-full right-0 mb-2 origin-bottom-right',
|
|
87
|
+
'top-left': 'bottom-full left-0 mb-2 origin-bottom-left',
|
|
88
|
+
}
|
|
89
|
+
return map[props.anchor]
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const transitionClasses = computed(() => {
|
|
93
|
+
const isBottom = props.anchor.startsWith('bottom')
|
|
94
|
+
|
|
95
|
+
const origins: Record<CalendarAnchor, string> = {
|
|
96
|
+
'bottom-left': 'origin-top-left',
|
|
97
|
+
'bottom-right': 'origin-top-right',
|
|
98
|
+
'top-left': 'origin-bottom-left',
|
|
99
|
+
'top-right': 'origin-bottom-right',
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const translateClass = isBottom ? '-translate-y-3' : 'translate-y-3'
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
enterActiveClass: `transition-all duration-200 ease-out ${origins[props.anchor]}`,
|
|
106
|
+
leaveActiveClass: `transition-all duration-150 ease-in ${origins[props.anchor]}`,
|
|
107
|
+
enterFromClass: `opacity-0 scale-95 ${translateClass}`,
|
|
108
|
+
leaveToClass: `opacity-0 scale-95 ${translateClass}`,
|
|
109
|
+
enterToClass: 'opacity-100 scale-100 translate-y-0',
|
|
110
|
+
leaveFromClass: 'opacity-100 scale-100 translate-y-0',
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
</script>
|
|
114
|
+
|
|
115
|
+
<template>
|
|
116
|
+
<div ref="wrapperRef" class="relative inline-block" @keydown="handleKeydown">
|
|
117
|
+
<span ref="triggerRef">
|
|
118
|
+
<slot name="trigger" :is-open="isOpen" :toggle="() => (isOpen = !isOpen)">
|
|
119
|
+
<NexxtButton
|
|
120
|
+
icon="calendar"
|
|
121
|
+
variant="secondary"
|
|
122
|
+
:aria-expanded="isOpen"
|
|
123
|
+
aria-haspopup="grid"
|
|
124
|
+
@click="isOpen = !isOpen"
|
|
125
|
+
>
|
|
126
|
+
{{ props.placeholder }}
|
|
127
|
+
</NexxtButton>
|
|
128
|
+
</slot>
|
|
129
|
+
</span>
|
|
130
|
+
|
|
131
|
+
<Transition v-bind="transitionClasses">
|
|
132
|
+
<div
|
|
133
|
+
v-if="isOpen"
|
|
134
|
+
ref="popoverRef"
|
|
135
|
+
tabindex="-1"
|
|
136
|
+
:class="['absolute z-50 outline-none', popoverClasses]"
|
|
137
|
+
>
|
|
138
|
+
<NexxtCalendar v-model="model" v-bind="calendarProps" @select="handleSelect" />
|
|
139
|
+
</div>
|
|
140
|
+
</Transition>
|
|
141
|
+
</div>
|
|
142
|
+
</template>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import Header from './Header.vue'
|
|
3
|
+
import Button from '../Button/Button.vue'
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
title: 'Components/Atoms/Header',
|
|
7
|
+
component: Header,
|
|
8
|
+
parameters: {
|
|
9
|
+
design: {
|
|
10
|
+
type: 'figma',
|
|
11
|
+
url: 'https://www.figma.com/design/CdbFJ7qUga6mtjagcPDvYK/Design-System?node-id=441-280&t=CbHvOQfEMIr7M5mO-4',
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
} satisfies Meta<typeof Header>
|
|
15
|
+
|
|
16
|
+
type Story = StoryObj<typeof Header>
|
|
17
|
+
|
|
18
|
+
export const Default: Story = {
|
|
19
|
+
args: {
|
|
20
|
+
title: 'Header Title',
|
|
21
|
+
backButtonText: 'Back',
|
|
22
|
+
backButtonAction: () => console.log('Back button clicked'),
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const WithSlots: Story = {
|
|
27
|
+
args: {
|
|
28
|
+
title: 'Header Title',
|
|
29
|
+
backButtonText: 'Go Back',
|
|
30
|
+
backButtonAction: () => console.log('Back clicked'),
|
|
31
|
+
},
|
|
32
|
+
render: (args) => ({
|
|
33
|
+
components: { Header, Button },
|
|
34
|
+
setup() {
|
|
35
|
+
return { args }
|
|
36
|
+
},
|
|
37
|
+
template: `
|
|
38
|
+
<Header v-bind="args">
|
|
39
|
+
<template #center>
|
|
40
|
+
<span class="font-bold">Centered Content</span>
|
|
41
|
+
</template>
|
|
42
|
+
<template #right>
|
|
43
|
+
<Button variant="primary" icon="arrow-right" :icon-right="true">Next</Button>
|
|
44
|
+
</template>
|
|
45
|
+
</Header>
|
|
46
|
+
`,
|
|
47
|
+
}),
|
|
48
|
+
}
|