@davidbirchall/core 1.0.6 → 1.0.8
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/.storybook/main.ts +18 -0
- package/.storybook/preview.ts +14 -0
- package/package.json +1 -4
- package/src/components/Badge/Badge.stories.ts +147 -0
- package/src/components/Badge/Badge.test.ts +57 -0
- package/src/components/Badge/Badge.vue +79 -0
- package/src/components/Button/Button.stories.ts +80 -0
- package/src/components/Button/Button.test.ts +145 -0
- package/src/components/Button/Button.vue +108 -0
- package/src/components/Button/types.ts +4 -0
- package/src/components/Calendar/Calendar.stories.ts +261 -0
- package/src/components/Calendar/Calendar.test.ts +119 -0
- package/src/components/Calendar/Calendar.vue +528 -0
- package/src/components/Calendar/types.ts +20 -0
- package/src/components/Card/Card.stories.ts +88 -0
- package/src/components/Card/Card.test.ts +173 -0
- package/src/components/Card/Card.vue +59 -0
- package/{dist/Card/types.d.ts → src/components/Card/types.ts} +1 -1
- package/src/components/Checkbox/Checkbox.stories.ts +126 -0
- package/src/components/Checkbox/Checkbox.test.ts +155 -0
- package/src/components/Checkbox/Checkbox.vue +121 -0
- package/src/components/Checkbox/types.ts +7 -0
- package/src/components/DataTable/DataTable.stories.ts +156 -0
- package/src/components/DataTable/DataTable.test.ts +185 -0
- package/src/components/DataTable/DataTable.vue +177 -0
- package/src/components/DataTable/types.ts +12 -0
- package/src/components/DatePicker/DatePicker.stories.ts +172 -0
- package/src/components/DatePicker/DatePicker.test.ts +87 -0
- package/src/components/DatePicker/DatePicker.vue +302 -0
- package/src/components/Dropdown/Dropdown.stories.ts +231 -0
- package/src/components/Dropdown/Dropdown.vue +314 -0
- package/src/components/Dropdown/types.ts +14 -0
- package/src/components/EmptyState/EmptyState.stories.ts +189 -0
- package/src/components/EmptyState/EmptyState.vue +215 -0
- package/src/components/EmptyState/types.ts +8 -0
- package/src/components/ErrorSummary/ErrorSummary.vue +78 -0
- package/src/components/ErrorSummary/types.ts +4 -0
- package/src/components/FormGroup/FormGroup.stories.ts +264 -0
- package/src/components/FormGroup/FormGroup.test.ts +63 -0
- package/src/components/FormGroup/FormGroup.vue +58 -0
- package/src/components/Heading/Heading.stories.ts +121 -0
- package/src/components/Heading/Heading.test.ts +184 -0
- package/src/components/Heading/Heading.vue +95 -0
- package/src/components/Heading/types.ts +6 -0
- package/src/components/Input/Input.stories.ts +172 -0
- package/src/components/Input/Input.test.ts +213 -0
- package/src/components/Input/Input.vue +121 -0
- package/src/components/Input/types.ts +11 -0
- package/src/components/Modal/Modal.stories.ts +341 -0
- package/src/components/Modal/Modal.test.ts +99 -0
- package/src/components/Modal/Modal.vue +278 -0
- package/src/components/ProgressBar/ProgressBar.stories.ts +313 -0
- package/src/components/ProgressBar/ProgressBar.test.ts +98 -0
- package/src/components/ProgressBar/ProgressBar.vue +117 -0
- package/src/components/Select/Select.stories.ts +177 -0
- package/src/components/Select/Select.test.ts +225 -0
- package/src/components/Select/Select.vue +147 -0
- package/src/components/Select/types.ts +16 -0
- package/src/components/StatCard/StatCard.stories.ts +274 -0
- package/src/components/StatCard/StatCard.vue +226 -0
- package/src/components/StatCard/types.ts +12 -0
- package/src/components/Tag/Tag.stories.ts +78 -0
- package/src/components/Tag/Tag.test.ts +50 -0
- package/src/components/Tag/Tag.vue +71 -0
- package/src/components/Tag/types.ts +4 -0
- package/src/components/TextArea/TextArea.stories.ts +171 -0
- package/src/components/TextArea/TextArea.test.ts +202 -0
- package/src/components/TextArea/TextArea.vue +122 -0
- package/src/components/TextArea/types.ts +11 -0
- package/src/components/index.ts +5 -0
- package/src/test/setup.ts +1 -0
- package/src/vite-env.d.ts +6 -0
- package/tsconfig.json +29 -0
- package/vite.config.ts +33 -0
- package/vitest.config.ts +28 -0
- package/dist/Button/types.d.ts +0 -4
- package/dist/Calendar/types.d.ts +0 -22
- package/dist/Checkbox/types.d.ts +0 -7
- package/dist/DataTable/types.d.ts +0 -11
- package/dist/Dropdown/types.d.ts +0 -13
- package/dist/EmptyState/types.d.ts +0 -8
- package/dist/ErrorSummary/types.d.ts +0 -4
- package/dist/Heading/types.d.ts +0 -6
- package/dist/Input/types.d.ts +0 -11
- package/dist/Select/types.d.ts +0 -15
- package/dist/StatCard/types.d.ts +0 -12
- package/dist/Tag/types.d.ts +0 -4
- package/dist/TextArea/types.d.ts +0 -11
- package/dist/core.css +0 -1
- package/dist/core.js +0 -24
- package/dist/core.js.map +0 -1
- package/dist/core.umd.cjs +0 -2
- package/dist/core.umd.cjs.map +0 -1
- package/dist/index.d.ts +0 -2
- package/dist/package.json +0 -27
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { mount } from '@vue/test-utils'
|
|
3
|
+
import DatePicker from './DatePicker.vue'
|
|
4
|
+
|
|
5
|
+
describe('DatePicker', () => {
|
|
6
|
+
it('renders correctly', () => {
|
|
7
|
+
const wrapper = mount(DatePicker)
|
|
8
|
+
expect(wrapper.find('.date-picker').exists()).toBe(true)
|
|
9
|
+
expect(wrapper.find('.date-input').exists()).toBe(true)
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('shows placeholder', () => {
|
|
13
|
+
const wrapper = mount(DatePicker, {
|
|
14
|
+
props: {
|
|
15
|
+
placeholder: 'Pick a date'
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const input = wrapper.find('.date-input')
|
|
20
|
+
expect(input.attributes('placeholder')).toBe('Pick a date')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('displays selected date', () => {
|
|
24
|
+
const wrapper = mount(DatePicker, {
|
|
25
|
+
props: {
|
|
26
|
+
modelValue: '2026-03-15'
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const input = wrapper.find('.date-input')
|
|
31
|
+
const value = (input.element as HTMLInputElement).value
|
|
32
|
+
expect(value).toContain('Mar')
|
|
33
|
+
expect(value).toContain('15')
|
|
34
|
+
expect(value).toContain('2026')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('opens calendar on click', async () => {
|
|
38
|
+
const wrapper = mount(DatePicker, {
|
|
39
|
+
attachTo: document.body
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
await wrapper.find('.date-picker-input').trigger('click')
|
|
43
|
+
|
|
44
|
+
// Calendar should open
|
|
45
|
+
expect(wrapper.vm.isOpen).toBe(true)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('respects disabled state', async () => {
|
|
49
|
+
const wrapper = mount(DatePicker, {
|
|
50
|
+
props: {
|
|
51
|
+
disabled: true
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const input = wrapper.find('.date-input')
|
|
56
|
+
expect(input.attributes('disabled')).toBeDefined()
|
|
57
|
+
|
|
58
|
+
await wrapper.find('.date-picker-input').trigger('click')
|
|
59
|
+
expect(wrapper.vm.isOpen).toBe(false)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('formats date in long format', () => {
|
|
63
|
+
const wrapper = mount(DatePicker, {
|
|
64
|
+
props: {
|
|
65
|
+
modelValue: '2026-03-15',
|
|
66
|
+
format: 'long'
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const input = wrapper.find('.date-input')
|
|
71
|
+
const value = (input.element as HTMLInputElement).value
|
|
72
|
+
expect(value).toContain('March')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('displays date range', () => {
|
|
76
|
+
const wrapper = mount(DatePicker, {
|
|
77
|
+
props: {
|
|
78
|
+
modelValue: { start: '2026-03-15', end: '2026-03-20' },
|
|
79
|
+
mode: 'range'
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const input = wrapper.find('.date-input')
|
|
84
|
+
const value = (input.element as HTMLInputElement).value
|
|
85
|
+
expect(value).toContain('-')
|
|
86
|
+
})
|
|
87
|
+
})
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="date-picker" ref="datePickerRef">
|
|
3
|
+
<div class="date-picker-input" @click="toggleCalendar">
|
|
4
|
+
<input
|
|
5
|
+
type="text"
|
|
6
|
+
:id="inputId"
|
|
7
|
+
:value="displayValue"
|
|
8
|
+
:placeholder="placeholder"
|
|
9
|
+
readonly
|
|
10
|
+
:disabled="disabled"
|
|
11
|
+
:class="['date-input', { 'date-input--error': error }]"
|
|
12
|
+
/>
|
|
13
|
+
<span class="date-icon">📅</span>
|
|
14
|
+
</div>
|
|
15
|
+
<span v-if="error" class="date-error">{{ error }}</span>
|
|
16
|
+
|
|
17
|
+
<Teleport to="body">
|
|
18
|
+
<div
|
|
19
|
+
v-if="isOpen"
|
|
20
|
+
ref="dropdownRef"
|
|
21
|
+
class="date-picker-dropdown"
|
|
22
|
+
:style="dropdownStyle"
|
|
23
|
+
>
|
|
24
|
+
<Calendar
|
|
25
|
+
v-model="internalValue"
|
|
26
|
+
:mode="mode"
|
|
27
|
+
:min-date="minDate"
|
|
28
|
+
:max-date="maxDate"
|
|
29
|
+
:disabled-dates="disabledDates"
|
|
30
|
+
:first-day-of-week="firstDayOfWeek"
|
|
31
|
+
:initial-date="typeof modelValue === 'string' ? modelValue : undefined"
|
|
32
|
+
@update:model-value="handleDateSelect"
|
|
33
|
+
/>
|
|
34
|
+
</div>
|
|
35
|
+
</Teleport>
|
|
36
|
+
|
|
37
|
+
<div
|
|
38
|
+
v-if="isOpen"
|
|
39
|
+
class="date-picker-overlay"
|
|
40
|
+
@click="closeCalendar"
|
|
41
|
+
></div>
|
|
42
|
+
</div>
|
|
43
|
+
</template>
|
|
44
|
+
|
|
45
|
+
<script setup lang="ts">
|
|
46
|
+
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
|
|
47
|
+
import Calendar from '../Calendar/Calendar.vue'
|
|
48
|
+
import type { CalendarProps } from '../Calendar/types'
|
|
49
|
+
|
|
50
|
+
interface DatePickerProps extends Omit<CalendarProps, 'modelValue'> {
|
|
51
|
+
modelValue?: string | string[] | { start: string; end: string }
|
|
52
|
+
id?: string
|
|
53
|
+
placeholder?: string
|
|
54
|
+
disabled?: boolean
|
|
55
|
+
format?: 'short' | 'long'
|
|
56
|
+
error?: string
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const props = withDefaults(defineProps<DatePickerProps>(), {
|
|
60
|
+
placeholder: 'Select date',
|
|
61
|
+
format: 'short',
|
|
62
|
+
mode: 'single',
|
|
63
|
+
firstDayOfWeek: 0
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const emit = defineEmits<{
|
|
67
|
+
'update:modelValue': [value: string | string[] | { start: string; end: string }]
|
|
68
|
+
}>()
|
|
69
|
+
|
|
70
|
+
const isOpen = ref(false)
|
|
71
|
+
let componentIdCounter = 0
|
|
72
|
+
const generatedId = `date-input-${++componentIdCounter}`
|
|
73
|
+
const inputId = computed(() => props.id || generatedId)
|
|
74
|
+
const datePickerRef = ref<HTMLElement | null>(null)
|
|
75
|
+
const dropdownRef = ref<HTMLElement | null>(null)
|
|
76
|
+
const dropdownStyle = ref<{ top: string; left: string; width: string }>({
|
|
77
|
+
top: '0px',
|
|
78
|
+
left: '0px',
|
|
79
|
+
width: '320px'
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const internalValue = ref(props.modelValue)
|
|
83
|
+
|
|
84
|
+
watch(() => props.modelValue, (newValue) => {
|
|
85
|
+
internalValue.value = newValue
|
|
86
|
+
}, { immediate: true })
|
|
87
|
+
|
|
88
|
+
const displayValue = computed(() => {
|
|
89
|
+
if (!props.modelValue) return ''
|
|
90
|
+
|
|
91
|
+
if (props.mode === 'single' && typeof props.modelValue === 'string') {
|
|
92
|
+
return formatDate(props.modelValue)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (props.mode === 'multiple' && Array.isArray(props.modelValue)) {
|
|
96
|
+
return props.modelValue.map(d => formatDate(d)).join(', ')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (props.mode === 'range' && typeof props.modelValue === 'object' && 'start' in props.modelValue) {
|
|
100
|
+
const { start, end } = props.modelValue
|
|
101
|
+
if (start && end) {
|
|
102
|
+
return `${formatDate(start)} - ${formatDate(end)}`
|
|
103
|
+
}
|
|
104
|
+
if (start) return formatDate(start)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return ''
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
const formatDate = (dateString: string): string => {
|
|
111
|
+
// Parse date string as local date to avoid timezone issues
|
|
112
|
+
const [year, month, day] = dateString.split('-').map(Number)
|
|
113
|
+
const date = new Date(year, month - 1, day)
|
|
114
|
+
|
|
115
|
+
if (props.format === 'long') {
|
|
116
|
+
return date.toLocaleDateString('en-US', {
|
|
117
|
+
year: 'numeric',
|
|
118
|
+
month: 'long',
|
|
119
|
+
day: 'numeric'
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return date.toLocaleDateString('en-US', {
|
|
124
|
+
year: 'numeric',
|
|
125
|
+
month: 'short',
|
|
126
|
+
day: 'numeric'
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const toggleCalendar = () => {
|
|
131
|
+
if (props.disabled) return
|
|
132
|
+
|
|
133
|
+
if (isOpen.value) {
|
|
134
|
+
closeCalendar()
|
|
135
|
+
} else {
|
|
136
|
+
openCalendar()
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const openCalendar = () => {
|
|
141
|
+
isOpen.value = true
|
|
142
|
+
|
|
143
|
+
// Calculate position
|
|
144
|
+
setTimeout(() => {
|
|
145
|
+
if (datePickerRef.value) {
|
|
146
|
+
const rect = datePickerRef.value.getBoundingClientRect()
|
|
147
|
+
const viewportHeight = window.innerHeight
|
|
148
|
+
const spaceBelow = viewportHeight - rect.bottom
|
|
149
|
+
const calendarHeight = 400 // Approximate height
|
|
150
|
+
|
|
151
|
+
if (spaceBelow < calendarHeight && rect.top > calendarHeight) {
|
|
152
|
+
// Show above
|
|
153
|
+
dropdownStyle.value = {
|
|
154
|
+
top: `${rect.top - calendarHeight + window.scrollY}px`,
|
|
155
|
+
left: `${rect.left + window.scrollX}px`,
|
|
156
|
+
width: '320px'
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
// Show below
|
|
160
|
+
dropdownStyle.value = {
|
|
161
|
+
top: `${rect.bottom + 4 + window.scrollY}px`,
|
|
162
|
+
left: `${rect.left + window.scrollX}px`,
|
|
163
|
+
width: '320px'
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}, 0)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const closeCalendar = () => {
|
|
171
|
+
isOpen.value = false
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const handleDateSelect = (value: string | string[] | { start: string; end: string }) => {
|
|
175
|
+
internalValue.value = value
|
|
176
|
+
emit('update:modelValue', value)
|
|
177
|
+
|
|
178
|
+
// Auto-close for single mode
|
|
179
|
+
if (props.mode === 'single') {
|
|
180
|
+
setTimeout(() => {
|
|
181
|
+
closeCalendar()
|
|
182
|
+
}, 200)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Auto-close for range mode when both dates selected
|
|
186
|
+
if (props.mode === 'range' && typeof value === 'object' && 'start' in value && value.end) {
|
|
187
|
+
setTimeout(() => {
|
|
188
|
+
closeCalendar()
|
|
189
|
+
}, 200)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
194
|
+
if (!isOpen.value) return
|
|
195
|
+
|
|
196
|
+
const target = event.target as Node
|
|
197
|
+
|
|
198
|
+
// Check if click is inside the date picker input or dropdown
|
|
199
|
+
if (datePickerRef.value?.contains(target)) return
|
|
200
|
+
if (dropdownRef.value?.contains(target)) return
|
|
201
|
+
|
|
202
|
+
// Click is outside, close the calendar
|
|
203
|
+
closeCalendar()
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
onMounted(() => {
|
|
207
|
+
document.addEventListener('click', handleClickOutside)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
onBeforeUnmount(() => {
|
|
211
|
+
document.removeEventListener('click', handleClickOutside)
|
|
212
|
+
})
|
|
213
|
+
</script>
|
|
214
|
+
|
|
215
|
+
<style scoped>
|
|
216
|
+
.date-picker {
|
|
217
|
+
position: relative;
|
|
218
|
+
width: 100%;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.date-picker-input {
|
|
222
|
+
position: relative;
|
|
223
|
+
display: flex;
|
|
224
|
+
align-items: center;
|
|
225
|
+
cursor: pointer;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.date-input {
|
|
229
|
+
width: 100%;
|
|
230
|
+
padding: 0.75rem 0.875rem;
|
|
231
|
+
padding-right: 2.5rem;
|
|
232
|
+
border: 1px solid #d1d5db;
|
|
233
|
+
border-radius: 0.375rem;
|
|
234
|
+
font-size: 1rem;
|
|
235
|
+
line-height: 1.5;
|
|
236
|
+
height: 48px;
|
|
237
|
+
cursor: pointer;
|
|
238
|
+
background: white;
|
|
239
|
+
color: #1f2937;
|
|
240
|
+
transition: all 0.2s ease-in-out;
|
|
241
|
+
box-sizing: border-box;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.date-input:focus {
|
|
245
|
+
outline: none;
|
|
246
|
+
border-color: #3b82f6;
|
|
247
|
+
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.date-input--error {
|
|
251
|
+
border-color: #fca5a5;
|
|
252
|
+
border-bottom: 2px solid #dc2626;
|
|
253
|
+
background: #fef2f2;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.date-input--error:focus {
|
|
257
|
+
border-color: #fca5a5;
|
|
258
|
+
border-bottom: 2px solid #dc2626;
|
|
259
|
+
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.12);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.date-input:disabled {
|
|
263
|
+
background: #f3f4f6;
|
|
264
|
+
cursor: not-allowed;
|
|
265
|
+
color: #9ca3af;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.date-input::placeholder {
|
|
269
|
+
color: #9ca3af;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.date-error {
|
|
273
|
+
margin-top: 0.35rem;
|
|
274
|
+
color: #dc2626;
|
|
275
|
+
font-size: 0.8rem;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.date-icon {
|
|
279
|
+
position: absolute;
|
|
280
|
+
right: 0.75rem;
|
|
281
|
+
pointer-events: none;
|
|
282
|
+
font-size: 1rem;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.date-picker-overlay {
|
|
286
|
+
position: fixed;
|
|
287
|
+
top: 0;
|
|
288
|
+
left: 0;
|
|
289
|
+
width: 100%;
|
|
290
|
+
height: 100%;
|
|
291
|
+
background: transparent;
|
|
292
|
+
z-index: 9998;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.date-picker-dropdown {
|
|
296
|
+
position: absolute;
|
|
297
|
+
z-index: 9999;
|
|
298
|
+
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
|
299
|
+
border-radius: 0.75rem;
|
|
300
|
+
background: white;
|
|
301
|
+
}
|
|
302
|
+
</style>
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3'
|
|
2
|
+
import Dropdown from './Dropdown.vue'
|
|
3
|
+
import type { DropdownItem } from './types'
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Components/Dropdown',
|
|
7
|
+
component: Dropdown,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
argTypes: {
|
|
10
|
+
placement: {
|
|
11
|
+
control: 'select',
|
|
12
|
+
options: ['bottom-left', 'bottom-right', 'top-left', 'top-right'],
|
|
13
|
+
description: 'Dropdown menu placement'
|
|
14
|
+
},
|
|
15
|
+
closeOnClick: {
|
|
16
|
+
control: 'boolean',
|
|
17
|
+
description: 'Close dropdown when item is clicked'
|
|
18
|
+
},
|
|
19
|
+
disabled: {
|
|
20
|
+
control: 'boolean',
|
|
21
|
+
description: 'Disabled state'
|
|
22
|
+
},
|
|
23
|
+
onSelect: { action: 'selected' }
|
|
24
|
+
}
|
|
25
|
+
} satisfies Meta<typeof Dropdown>
|
|
26
|
+
|
|
27
|
+
export default meta
|
|
28
|
+
type Story = StoryObj<typeof meta>
|
|
29
|
+
|
|
30
|
+
const defaultItems: DropdownItem[] = [
|
|
31
|
+
{ label: 'Profile', value: 'profile', icon: '👤' },
|
|
32
|
+
{ label: 'Settings', value: 'settings', icon: '⚙️' },
|
|
33
|
+
{ label: '', value: 'divider', divider: true },
|
|
34
|
+
{ label: 'Logout', value: 'logout', icon: '🚪' }
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
export const Default: Story = {
|
|
38
|
+
args: {
|
|
39
|
+
items: defaultItems,
|
|
40
|
+
placement: 'bottom-right'
|
|
41
|
+
},
|
|
42
|
+
render: (args: any) => ({
|
|
43
|
+
components: { Dropdown },
|
|
44
|
+
setup() {
|
|
45
|
+
return { args }
|
|
46
|
+
},
|
|
47
|
+
template: `
|
|
48
|
+
render: (args: any) => ({
|
|
49
|
+
<Dropdown v-bind="args" @select="args.onSelect">
|
|
50
|
+
<template #trigger>
|
|
51
|
+
<button style="padding: 0.5rem 1rem; background: #667eea; color: white; border: none; border-radius: 0.375rem; cursor: pointer;">
|
|
52
|
+
Open Menu
|
|
53
|
+
</button>
|
|
54
|
+
</template>
|
|
55
|
+
</Dropdown>
|
|
56
|
+
</div>
|
|
57
|
+
`
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const WithHeader: Story = {
|
|
62
|
+
args: {
|
|
63
|
+
items: [
|
|
64
|
+
{ label: 'Dashboard', value: 'dashboard' },
|
|
65
|
+
{ label: 'Projects', value: 'projects' },
|
|
66
|
+
{ label: 'Team', value: 'team' }
|
|
67
|
+
],
|
|
68
|
+
placement: 'bottom-left'
|
|
69
|
+
},
|
|
70
|
+
render: (args: any) => ({
|
|
71
|
+
components: { Dropdown },
|
|
72
|
+
setup() {
|
|
73
|
+
return { args }
|
|
74
|
+
},
|
|
75
|
+
template: `
|
|
76
|
+
<div style="padding: 100px; display: flex; justify-content: center;">
|
|
77
|
+
<Dropdown v-bind="args" @select="args.onSelect">
|
|
78
|
+
<template #trigger>
|
|
79
|
+
<button style="padding: 0.5rem 1rem; background: #667eea; color: white; border: none; border-radius: 0.375rem; cursor: pointer;">
|
|
80
|
+
Navigation
|
|
81
|
+
</button>
|
|
82
|
+
</template>
|
|
83
|
+
<template #header>
|
|
84
|
+
<div style="padding: 0.75rem; border-bottom: 1px solid #e5e7eb; font-weight: 600;">
|
|
85
|
+
Quick Links
|
|
86
|
+
</div>
|
|
87
|
+
</template>
|
|
88
|
+
</Dropdown>
|
|
89
|
+
</div>
|
|
90
|
+
`
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
export const WithFooter: Story = {
|
|
94
|
+
args: {
|
|
95
|
+
items: [
|
|
96
|
+
{ label: 'New Document', value: 'new' },
|
|
97
|
+
{ label: 'Open', value: 'open' },
|
|
98
|
+
{ label: 'Save', value: 'save' }
|
|
99
|
+
]
|
|
100
|
+
},
|
|
101
|
+
render: (args: any) => ({
|
|
102
|
+
components: { Dropdown },
|
|
103
|
+
setup() {
|
|
104
|
+
return { args }
|
|
105
|
+
},
|
|
106
|
+
template: `
|
|
107
|
+
<div style="padding: 100px; display: flex; justify-content: center;">
|
|
108
|
+
<Dropdown v-bind="args" @select="args.onSelect">
|
|
109
|
+
<template #trigger>
|
|
110
|
+
<button style="padding: 0.5rem 1rem; background: #667eea; color: white; border: none; border-radius: 0.375rem; cursor: pointer;">
|
|
111
|
+
File Menu
|
|
112
|
+
</button>
|
|
113
|
+
</template>
|
|
114
|
+
<template #footer>
|
|
115
|
+
<div style="padding: 0.5rem; border-top: 1px solid #e5e7eb; text-align: center; font-size: 0.75rem; color: #6b7280;">
|
|
116
|
+
v1.0.0
|
|
117
|
+
</div>
|
|
118
|
+
</template>
|
|
119
|
+
</Dropdown>
|
|
120
|
+
</div>
|
|
121
|
+
`
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export const UserMenu: Story = {
|
|
126
|
+
render: () => ({
|
|
127
|
+
components: { Dropdown },
|
|
128
|
+
setup() {
|
|
129
|
+
const items: DropdownItem[] = [
|
|
130
|
+
{ label: 'View Profile', value: 'profile' },
|
|
131
|
+
{ label: 'Account Settings', value: 'settings' },
|
|
132
|
+
{ label: '', value: 'divider1', divider: true },
|
|
133
|
+
{ label: 'Help & Support', value: 'help' },
|
|
134
|
+
{ label: '', value: 'divider2', divider: true },
|
|
135
|
+
{ label: 'Sign Out', value: 'signout' }
|
|
136
|
+
]
|
|
137
|
+
return { items }
|
|
138
|
+
},
|
|
139
|
+
template: `
|
|
140
|
+
<div style="padding: 100px; display: flex; justify-content: center;">
|
|
141
|
+
<Dropdown :items="items" placement="bottom-right">
|
|
142
|
+
<template #trigger>
|
|
143
|
+
<div style="width: 2.5rem; height: 2.5rem; border-radius: 50%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; display: flex; align-items: center; justify-content: center; font-weight: 600; cursor: pointer;">
|
|
144
|
+
JD
|
|
145
|
+
</div>
|
|
146
|
+
</template>
|
|
147
|
+
<template #header>
|
|
148
|
+
<div style="padding: 0.75rem; border-bottom: 1px solid #e5e7eb;">
|
|
149
|
+
<div style="font-weight: 600; font-size: 0.9375rem;">John Doe</div>
|
|
150
|
+
<div style="font-size: 0.875rem; color: #6b7280;">john@example.com</div>
|
|
151
|
+
</div>
|
|
152
|
+
</template>
|
|
153
|
+
</Dropdown>
|
|
154
|
+
</div>
|
|
155
|
+
`
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export const Disabled: Story = {
|
|
160
|
+
args: {
|
|
161
|
+
items: defaultItems,
|
|
162
|
+
disabled: true
|
|
163
|
+
},
|
|
164
|
+
render: (args: any) => ({
|
|
165
|
+
components: { Dropdown },
|
|
166
|
+
setup() {
|
|
167
|
+
return { args }
|
|
168
|
+
},
|
|
169
|
+
template: `
|
|
170
|
+
<div style="padding: 100px; display: flex; justify-content: center;">
|
|
171
|
+
<Dropdown v-bind="args">
|
|
172
|
+
<template #trigger>
|
|
173
|
+
<button style="padding: 0.5rem 1rem; background: #9ca3af; color: white; border: none; border-radius: 0.375rem; cursor: not-allowed;">
|
|
174
|
+
Disabled Menu
|
|
175
|
+
</button>
|
|
176
|
+
</template>
|
|
177
|
+
</Dropdown>
|
|
178
|
+
</div>
|
|
179
|
+
`
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export const Placements: Story = {
|
|
184
|
+
render: () => ({
|
|
185
|
+
components: { Dropdown },
|
|
186
|
+
setup() {
|
|
187
|
+
const items: DropdownItem[] = [
|
|
188
|
+
{ label: 'Option 1', value: '1' },
|
|
189
|
+
{ label: 'Option 2', value: '2' },
|
|
190
|
+
{ label: 'Option 3', value: '3' }
|
|
191
|
+
]
|
|
192
|
+
return { items }
|
|
193
|
+
},
|
|
194
|
+
template: `
|
|
195
|
+
<div style="padding: 150px; display: grid; grid-template-columns: 1fr 1fr; gap: 2rem;">
|
|
196
|
+
<div style="display: flex; flex-direction: column; gap: 1rem;">
|
|
197
|
+
<Dropdown :items="items" placement="bottom-left">
|
|
198
|
+
<template #trigger>
|
|
199
|
+
<button style="padding: 0.5rem 1rem; background: #667eea; color: white; border: none; border-radius: 0.375rem; cursor: pointer; width: 150px;">
|
|
200
|
+
Bottom Left
|
|
201
|
+
</button>
|
|
202
|
+
</template>
|
|
203
|
+
</Dropdown>
|
|
204
|
+
<Dropdown :items="items" placement="top-left">
|
|
205
|
+
<template #trigger>
|
|
206
|
+
<button style="padding: 0.5rem 1rem; background: #667eea; color: white; border: none; border-radius: 0.375rem; cursor: pointer; width: 150px;">
|
|
207
|
+
Top Left
|
|
208
|
+
</button>
|
|
209
|
+
</template>
|
|
210
|
+
</Dropdown>
|
|
211
|
+
</div>
|
|
212
|
+
<div style="display: flex; flex-direction: column; gap: 1rem; align-items: flex-end;">
|
|
213
|
+
<Dropdown :items="items" placement="bottom-right">
|
|
214
|
+
<template #trigger>
|
|
215
|
+
<button style="padding: 0.5rem 1rem; background: #667eea; color: white; border: none; border-radius: 0.375rem; cursor: pointer; width: 150px;">
|
|
216
|
+
Bottom Right
|
|
217
|
+
</button>
|
|
218
|
+
</template>
|
|
219
|
+
</Dropdown>
|
|
220
|
+
<Dropdown :items="items" placement="top-right">
|
|
221
|
+
<template #trigger>
|
|
222
|
+
<button style="padding: 0.5rem 1rem; background: #667eea; color: white; border: none; border-radius: 0.375rem; cursor: pointer; width: 150px;">
|
|
223
|
+
Top Right
|
|
224
|
+
</button>
|
|
225
|
+
</template>
|
|
226
|
+
</Dropdown>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
`
|
|
230
|
+
})
|
|
231
|
+
}
|