@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.
Files changed (95) hide show
  1. package/.storybook/main.ts +18 -0
  2. package/.storybook/preview.ts +14 -0
  3. package/package.json +1 -4
  4. package/src/components/Badge/Badge.stories.ts +147 -0
  5. package/src/components/Badge/Badge.test.ts +57 -0
  6. package/src/components/Badge/Badge.vue +79 -0
  7. package/src/components/Button/Button.stories.ts +80 -0
  8. package/src/components/Button/Button.test.ts +145 -0
  9. package/src/components/Button/Button.vue +108 -0
  10. package/src/components/Button/types.ts +4 -0
  11. package/src/components/Calendar/Calendar.stories.ts +261 -0
  12. package/src/components/Calendar/Calendar.test.ts +119 -0
  13. package/src/components/Calendar/Calendar.vue +528 -0
  14. package/src/components/Calendar/types.ts +20 -0
  15. package/src/components/Card/Card.stories.ts +88 -0
  16. package/src/components/Card/Card.test.ts +173 -0
  17. package/src/components/Card/Card.vue +59 -0
  18. package/{dist/Card/types.d.ts → src/components/Card/types.ts} +1 -1
  19. package/src/components/Checkbox/Checkbox.stories.ts +126 -0
  20. package/src/components/Checkbox/Checkbox.test.ts +155 -0
  21. package/src/components/Checkbox/Checkbox.vue +121 -0
  22. package/src/components/Checkbox/types.ts +7 -0
  23. package/src/components/DataTable/DataTable.stories.ts +156 -0
  24. package/src/components/DataTable/DataTable.test.ts +185 -0
  25. package/src/components/DataTable/DataTable.vue +177 -0
  26. package/src/components/DataTable/types.ts +12 -0
  27. package/src/components/DatePicker/DatePicker.stories.ts +172 -0
  28. package/src/components/DatePicker/DatePicker.test.ts +87 -0
  29. package/src/components/DatePicker/DatePicker.vue +302 -0
  30. package/src/components/Dropdown/Dropdown.stories.ts +231 -0
  31. package/src/components/Dropdown/Dropdown.vue +314 -0
  32. package/src/components/Dropdown/types.ts +14 -0
  33. package/src/components/EmptyState/EmptyState.stories.ts +189 -0
  34. package/src/components/EmptyState/EmptyState.vue +215 -0
  35. package/src/components/EmptyState/types.ts +8 -0
  36. package/src/components/ErrorSummary/ErrorSummary.vue +78 -0
  37. package/src/components/ErrorSummary/types.ts +4 -0
  38. package/src/components/FormGroup/FormGroup.stories.ts +264 -0
  39. package/src/components/FormGroup/FormGroup.test.ts +63 -0
  40. package/src/components/FormGroup/FormGroup.vue +58 -0
  41. package/src/components/Heading/Heading.stories.ts +121 -0
  42. package/src/components/Heading/Heading.test.ts +184 -0
  43. package/src/components/Heading/Heading.vue +95 -0
  44. package/src/components/Heading/types.ts +6 -0
  45. package/src/components/Input/Input.stories.ts +172 -0
  46. package/src/components/Input/Input.test.ts +213 -0
  47. package/src/components/Input/Input.vue +121 -0
  48. package/src/components/Input/types.ts +11 -0
  49. package/src/components/Modal/Modal.stories.ts +341 -0
  50. package/src/components/Modal/Modal.test.ts +99 -0
  51. package/src/components/Modal/Modal.vue +278 -0
  52. package/src/components/ProgressBar/ProgressBar.stories.ts +313 -0
  53. package/src/components/ProgressBar/ProgressBar.test.ts +98 -0
  54. package/src/components/ProgressBar/ProgressBar.vue +117 -0
  55. package/src/components/Select/Select.stories.ts +177 -0
  56. package/src/components/Select/Select.test.ts +225 -0
  57. package/src/components/Select/Select.vue +147 -0
  58. package/src/components/Select/types.ts +16 -0
  59. package/src/components/StatCard/StatCard.stories.ts +274 -0
  60. package/src/components/StatCard/StatCard.vue +226 -0
  61. package/src/components/StatCard/types.ts +12 -0
  62. package/src/components/Tag/Tag.stories.ts +78 -0
  63. package/src/components/Tag/Tag.test.ts +50 -0
  64. package/src/components/Tag/Tag.vue +71 -0
  65. package/src/components/Tag/types.ts +4 -0
  66. package/src/components/TextArea/TextArea.stories.ts +171 -0
  67. package/src/components/TextArea/TextArea.test.ts +202 -0
  68. package/src/components/TextArea/TextArea.vue +122 -0
  69. package/src/components/TextArea/types.ts +11 -0
  70. package/src/components/index.ts +5 -0
  71. package/src/test/setup.ts +1 -0
  72. package/src/vite-env.d.ts +6 -0
  73. package/tsconfig.json +29 -0
  74. package/vite.config.ts +33 -0
  75. package/vitest.config.ts +28 -0
  76. package/dist/Button/types.d.ts +0 -4
  77. package/dist/Calendar/types.d.ts +0 -22
  78. package/dist/Checkbox/types.d.ts +0 -7
  79. package/dist/DataTable/types.d.ts +0 -11
  80. package/dist/Dropdown/types.d.ts +0 -13
  81. package/dist/EmptyState/types.d.ts +0 -8
  82. package/dist/ErrorSummary/types.d.ts +0 -4
  83. package/dist/Heading/types.d.ts +0 -6
  84. package/dist/Input/types.d.ts +0 -11
  85. package/dist/Select/types.d.ts +0 -15
  86. package/dist/StatCard/types.d.ts +0 -12
  87. package/dist/Tag/types.d.ts +0 -4
  88. package/dist/TextArea/types.d.ts +0 -11
  89. package/dist/core.css +0 -1
  90. package/dist/core.js +0 -24
  91. package/dist/core.js.map +0 -1
  92. package/dist/core.umd.cjs +0 -2
  93. package/dist/core.umd.cjs.map +0 -1
  94. package/dist/index.d.ts +0 -2
  95. 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
+ }