@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,213 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { render, screen } from '@testing-library/vue'
3
+ import userEvent from '@testing-library/user-event'
4
+ import Input from './Input.vue'
5
+
6
+ describe('Input', () => {
7
+ describe('rendering', () => {
8
+ it('renders with label', () => {
9
+ render(Input, {
10
+ props: {
11
+ label: 'Email'
12
+ }
13
+ })
14
+
15
+ expect(screen.getByText('Email')).toBeInTheDocument()
16
+ })
17
+
18
+ it('renders without label', () => {
19
+ const { container } = render(Input)
20
+
21
+ expect(container.querySelector('.input-label')).not.toBeInTheDocument()
22
+ })
23
+
24
+ it('renders placeholder', () => {
25
+ render(Input, {
26
+ props: {
27
+ placeholder: 'Enter text'
28
+ }
29
+ })
30
+
31
+ expect(screen.getByPlaceholderText('Enter text')).toBeInTheDocument()
32
+ })
33
+
34
+ it('renders error message', () => {
35
+ render(Input, {
36
+ props: {
37
+ error: 'This field is required'
38
+ }
39
+ })
40
+
41
+ expect(screen.getByText('This field is required')).toBeInTheDocument()
42
+ })
43
+
44
+ it('renders hint text', () => {
45
+ render(Input, {
46
+ props: {
47
+ hint: 'Must be at least 8 characters'
48
+ }
49
+ })
50
+
51
+ expect(screen.getByText('Must be at least 8 characters')).toBeInTheDocument()
52
+ })
53
+
54
+ it('does not show hint when error is present', () => {
55
+ render(Input, {
56
+ props: {
57
+ hint: 'Helper text',
58
+ error: 'Error message'
59
+ }
60
+ })
61
+
62
+ expect(screen.queryByText('Helper text')).not.toBeInTheDocument()
63
+ expect(screen.getByText('Error message')).toBeInTheDocument()
64
+ })
65
+
66
+ it('shows required asterisk when required', () => {
67
+ render(Input, {
68
+ props: {
69
+ label: 'Name',
70
+ required: true
71
+ }
72
+ })
73
+
74
+ expect(screen.getByText('*')).toBeInTheDocument()
75
+ })
76
+
77
+ it('applies error class when error prop is set', () => {
78
+ render(Input, {
79
+ props: {
80
+ error: 'Error'
81
+ }
82
+ })
83
+
84
+ const input = screen.getByRole('textbox')
85
+ expect(input).toHaveClass('input-field--error')
86
+ })
87
+ })
88
+
89
+ describe('input types', () => {
90
+ it('renders as text input by default', () => {
91
+ render(Input)
92
+
93
+ const input = screen.getByRole('textbox')
94
+ expect(input).toHaveAttribute('type', 'text')
95
+ })
96
+
97
+ it('renders as email input', () => {
98
+ render(Input, {
99
+ props: {
100
+ type: 'email'
101
+ }
102
+ })
103
+
104
+ const input = screen.getByRole('textbox')
105
+ expect(input).toHaveAttribute('type', 'email')
106
+ })
107
+
108
+ it('renders as password input', () => {
109
+ render(Input, {
110
+ props: {
111
+ type: 'password'
112
+ }
113
+ })
114
+
115
+ const input = document.querySelector('input[type="password"]')
116
+ expect(input).toBeInTheDocument()
117
+ })
118
+
119
+ it('renders as number input', () => {
120
+ render(Input, {
121
+ props: {
122
+ type: 'number'
123
+ }
124
+ })
125
+
126
+ const input = screen.getByRole('spinbutton')
127
+ expect(input).toHaveAttribute('type', 'number')
128
+ })
129
+ })
130
+
131
+ describe('v-model', () => {
132
+ it('displays initial value', () => {
133
+ render(Input, {
134
+ props: {
135
+ modelValue: 'Initial value'
136
+ }
137
+ })
138
+
139
+ const input = screen.getByRole('textbox') as HTMLInputElement
140
+ expect(input.value).toBe('Initial value')
141
+ })
142
+
143
+ it('emits update:modelValue on input', async () => {
144
+ const user = userEvent.setup()
145
+ const { emitted } = render(Input)
146
+
147
+ const input = screen.getByRole('textbox')
148
+ await user.type(input, 'test')
149
+
150
+ expect(emitted()['update:modelValue']).toBeTruthy()
151
+ expect(emitted()['update:modelValue'].length).toBeGreaterThan(0)
152
+ })
153
+ })
154
+
155
+ describe('disabled state', () => {
156
+ it('disables the input when disabled prop is true', () => {
157
+ render(Input, {
158
+ props: {
159
+ disabled: true
160
+ }
161
+ })
162
+
163
+ const input = screen.getByRole('textbox')
164
+ expect(input).toBeDisabled()
165
+ })
166
+
167
+ it('applies disabled class', () => {
168
+ render(Input, {
169
+ props: {
170
+ disabled: true
171
+ }
172
+ })
173
+
174
+ const input = screen.getByRole('textbox')
175
+ expect(input).toHaveClass('input-field--disabled')
176
+ })
177
+ })
178
+
179
+ describe('required attribute', () => {
180
+ it('sets required attribute when required prop is true', () => {
181
+ render(Input, {
182
+ props: {
183
+ required: true
184
+ }
185
+ })
186
+
187
+ const input = screen.getByRole('textbox')
188
+ expect(input).toBeRequired()
189
+ })
190
+
191
+ it('does not set required attribute by default', () => {
192
+ render(Input)
193
+
194
+ const input = screen.getByRole('textbox')
195
+ expect(input).not.toBeRequired()
196
+ })
197
+ })
198
+
199
+ describe('accessibility', () => {
200
+ it('associates label with input using htmlFor', () => {
201
+ render(Input, {
202
+ props: {
203
+ label: 'Username'
204
+ }
205
+ })
206
+
207
+ const label = screen.getByText('Username') as HTMLLabelElement
208
+ const input = screen.getByRole('textbox')
209
+
210
+ expect(label.htmlFor).toBe(input.id)
211
+ })
212
+ })
213
+ })
@@ -0,0 +1,121 @@
1
+ <template>
2
+ <div class="input-wrapper">
3
+ <label v-if="label" :for="inputId" class="input-label">
4
+ {{ label }}
5
+ </label>
6
+ <input
7
+ :id="inputId"
8
+ :type="type"
9
+ :value="modelValue"
10
+ :placeholder="placeholder"
11
+ :disabled="disabled"
12
+ :required="required"
13
+ :class="['input-field', { 'input-field--error': error, 'input-field--disabled': disabled }]"
14
+ @input="handleInput"
15
+ />
16
+ <span v-if="error" class="input-error">{{ error }}</span>
17
+ <span v-if="hint && !error" class="input-hint">{{ hint }}</span>
18
+ </div>
19
+ </template>
20
+
21
+ <script setup lang="ts">
22
+ import { computed } from 'vue'
23
+
24
+ let componentIdCounter = 0
25
+
26
+ export interface InputProps {
27
+ modelValue?: string | number
28
+ id?: string
29
+ label?: string
30
+ type?: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url' | 'time'
31
+ placeholder?: string
32
+ disabled?: boolean
33
+ required?: boolean
34
+ error?: string
35
+ hint?: string
36
+ }
37
+
38
+ const props = withDefaults(defineProps<InputProps>(), {
39
+ type: 'text',
40
+ disabled: false,
41
+ required: false
42
+ })
43
+
44
+ const emit = defineEmits<{
45
+ 'update:modelValue': [value: string]
46
+ }>()
47
+
48
+ const generatedId = `input-${++componentIdCounter}`
49
+ const inputId = computed(() => props.id || generatedId)
50
+
51
+ const handleInput = (event: Event) => {
52
+ const target = event.target as HTMLInputElement
53
+ emit('update:modelValue', target.value)
54
+ }
55
+ </script>
56
+
57
+ <style scoped>
58
+ .input-wrapper {
59
+ display: flex;
60
+ flex-direction: column;
61
+ gap: 0.375rem;
62
+ }
63
+
64
+ .input-label {
65
+ font-size: 0.875rem;
66
+ font-weight: 500;
67
+ color: #374151;
68
+ }
69
+
70
+
71
+ .input-field {
72
+ padding: 0.75rem 0.875rem;
73
+ font-size: 1rem;
74
+ line-height: 1.5;
75
+ height: 48px;
76
+ border: 1px solid #d1d5db;
77
+ border-radius: 0.375rem;
78
+ outline: none;
79
+ transition: all 0.2s ease-in-out;
80
+ background: white;
81
+ color: #1f2937;
82
+ box-sizing: border-box;
83
+ }
84
+
85
+ .input-field::placeholder {
86
+ color: #9ca3af;
87
+ }
88
+
89
+ .input-field:focus {
90
+ border-color: #3b82f6;
91
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
92
+ }
93
+
94
+ .input-field--error {
95
+ border-color: #fca5a5;
96
+ border-bottom: 2px solid #dc2626;
97
+ background: #fef2f2;
98
+ }
99
+
100
+ .input-field--error:focus {
101
+ border-color: #fca5a5;
102
+ border-bottom: 2px solid #dc2626;
103
+ box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.12);
104
+ }
105
+
106
+ .input-field--disabled {
107
+ background: #f3f4f6;
108
+ cursor: not-allowed;
109
+ opacity: 0.6;
110
+ }
111
+
112
+ .input-error {
113
+ font-size: 0.75rem;
114
+ color: #ef4444;
115
+ }
116
+
117
+ .input-hint {
118
+ font-size: 0.75rem;
119
+ color: #6b7280;
120
+ }
121
+ </style>
@@ -0,0 +1,11 @@
1
+ export interface InputProps {
2
+ modelValue?: string | number
3
+ id?: string
4
+ label?: string
5
+ type?: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url' | 'time'
6
+ placeholder?: string
7
+ disabled?: boolean
8
+ required?: boolean
9
+ error?: string
10
+ hint?: string
11
+ }
@@ -0,0 +1,341 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3'
2
+ import { ref } from 'vue'
3
+ import Modal from './Modal.vue'
4
+
5
+ const meta = {
6
+ title: 'Components/Modal',
7
+ component: Modal,
8
+ tags: ['autodocs'],
9
+ argTypes: {
10
+ modelValue: {
11
+ control: 'boolean',
12
+ description: 'Modal visibility state'
13
+ },
14
+ title: {
15
+ control: 'text',
16
+ description: 'Modal title'
17
+ },
18
+ size: {
19
+ control: 'select',
20
+ options: ['small', 'medium', 'large'],
21
+ description: 'Modal size'
22
+ },
23
+ closable: {
24
+ control: 'boolean',
25
+ description: 'Show close button'
26
+ },
27
+ closeOnOverlay: {
28
+ control: 'boolean',
29
+ description: 'Close when clicking overlay'
30
+ }
31
+ },
32
+ args: {
33
+ title: 'Modal Title',
34
+ size: 'medium',
35
+ closable: true,
36
+ closeOnOverlay: true
37
+ }
38
+ } satisfies Meta<typeof Modal>
39
+
40
+ export default meta
41
+ type Story = StoryObj<typeof meta>
42
+
43
+ export const Default: Story = {
44
+ render: (args: any) => ({
45
+ components: { Modal },
46
+ setup() {
47
+ const isOpen = ref(false)
48
+ return { isOpen, args }
49
+ },
50
+ template: `
51
+ <div>
52
+ <button
53
+ @click="isOpen = true"
54
+ style="padding: 0.625rem 1.25rem; background: #667eea; color: white; border: none; border-radius: 0.375rem; cursor: pointer; font-weight: 600;"
55
+ >
56
+ Open Modal
57
+ </button>
58
+ <Modal v-model="isOpen" v-bind="args">
59
+ <p>This is the modal content. You can put any content here.</p>
60
+ </Modal>
61
+ </div>
62
+ `
63
+ })
64
+ }
65
+
66
+ export const WithFooter: Story = {
67
+ render: (args: any) => ({
68
+ components: { Modal },
69
+ setup() {
70
+ const isOpen = ref(false)
71
+ return { isOpen, args }
72
+ },
73
+ template: `
74
+ <div>
75
+ <button
76
+ @click="isOpen = true"
77
+ style="padding: 0.625rem 1.25rem; background: #667eea; color: white; border: none; border-radius: 0.375rem; cursor: pointer; font-weight: 600;"
78
+ >
79
+ Open Modal
80
+ </button>
81
+ <Modal v-model="isOpen" title="Confirm Action" v-bind="args">
82
+ <p>Are you sure you want to continue with this action?</p>
83
+ <template #footer>
84
+ <div style="display: flex; gap: 0.75rem; justify-content: flex-end;">
85
+ <button
86
+ @click="isOpen = false"
87
+ style="padding: 0.5rem 1rem; background: #e5e7eb; color: #374151; border: none; border-radius: 0.375rem; cursor: pointer; font-weight: 500;"
88
+ >
89
+ Cancel
90
+ </button>
91
+ <button
92
+ @click="isOpen = false"
93
+ style="padding: 0.5rem 1rem; background: #667eea; color: white; border: none; border-radius: 0.375rem; cursor: pointer; font-weight: 500;"
94
+ >
95
+ Confirm
96
+ </button>
97
+ </div>
98
+ </template>
99
+ </Modal>
100
+ </div>
101
+ `
102
+ })
103
+ }
104
+
105
+ export const Small: Story = {
106
+ args: {
107
+ size: 'small',
108
+ title: 'Small Modal'
109
+ },
110
+ render: (args: any) => ({
111
+ components: { Modal },
112
+ setup() {
113
+ const isOpen = ref(false)
114
+ return { isOpen, args }
115
+ },
116
+ template: `
117
+ <div>
118
+ <button
119
+ @click="isOpen = true"
120
+ style="padding: 0.625rem 1.25rem; background: #667eea; color: white; border: none; border-radius: 0.375rem; cursor: pointer; font-weight: 600;"
121
+ >
122
+ Open Small Modal
123
+ </button>
124
+ <Modal v-model="isOpen" v-bind="args">
125
+ <p>This is a small modal with limited content.</p>
126
+ </Modal>
127
+ </div>
128
+ `
129
+ })
130
+ }
131
+
132
+ export const Large: Story = {
133
+ args: {
134
+ size: 'large',
135
+ title: 'Large Modal'
136
+ },
137
+ render: (args: any) => ({
138
+ components: { Modal },
139
+ setup() {
140
+ const isOpen = ref(false)
141
+ return { isOpen, args }
142
+ },
143
+ template: `
144
+ <div>
145
+ <button
146
+ @click="isOpen = true"
147
+ style="padding: 0.625rem 1.25rem; background: #667eea; color: white; border: none; border-radius: 0.375rem; cursor: pointer; font-weight: 600;"
148
+ >
149
+ Open Large Modal
150
+ </button>
151
+ <Modal v-model="isOpen" v-bind="args">
152
+ <div style="min-height: 400px;">
153
+ <h3>Large Content Area</h3>
154
+ <p>This modal has more space for content.</p>
155
+ <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
156
+ <p>Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
157
+ </div>
158
+ </Modal>
159
+ </div>
160
+ `
161
+ })
162
+ }
163
+
164
+ export const NoClose: Story = {
165
+ args: {
166
+ closable: false,
167
+ closeOnOverlay: false,
168
+ title: 'Important Message'
169
+ },
170
+ render: (args: any) => ({
171
+ components: { Modal },
172
+ setup() {
173
+ const isOpen = ref(false)
174
+ return { isOpen, args }
175
+ },
176
+ template: `
177
+ <div>
178
+ <button
179
+ @click="isOpen = true"
180
+ style="padding: 0.625rem 1.25rem; background: #667eea; color: white; border: none; border-radius: 0.375rem; cursor: pointer; font-weight: 600;"
181
+ >
182
+ Open Modal (No Close)
183
+ </button>
184
+ <Modal v-model="isOpen" v-bind="args">
185
+ <p>This modal cannot be closed by clicking outside or using the X button.</p>
186
+ <template #footer>
187
+ <button
188
+ @click="isOpen = false"
189
+ style="padding: 0.5rem 1rem; background: #667eea; color: white; border: none; border-radius: 0.375rem; cursor: pointer; font-weight: 500;"
190
+ >
191
+ I Understand
192
+ </button>
193
+ </template>
194
+ </Modal>
195
+ </div>
196
+ `
197
+ })
198
+ }
199
+
200
+ export const CustomHeader: Story = {
201
+ render: (args: any) => ({
202
+ components: { Modal },
203
+ setup() {
204
+ const isOpen = ref(false)
205
+ return { isOpen, args }
206
+ },
207
+ template: `
208
+ <div>
209
+ <button
210
+ @click="isOpen = true"
211
+ style="padding: 0.625rem 1.25rem; background: #667eea; color: white; border: none; border-radius: 0.375rem; cursor: pointer; font-weight: 600;"
212
+ >
213
+ Open Modal
214
+ </button>
215
+ <Modal v-model="isOpen">
216
+ <template #header>
217
+ <div style="display: flex; align-items: center; gap: 0.75rem;">
218
+ <span style="font-size: 1.5rem;">✨</span>
219
+ <h3 style="margin: 0; font-size: 1.25rem; color: #667eea;">Custom Header</h3>
220
+ </div>
221
+ </template>
222
+ <p>This modal has a custom header with an icon and styled text.</p>
223
+ </Modal>
224
+ </div>
225
+ `
226
+ })
227
+ }
228
+
229
+ export const FormModal: Story = {
230
+ render: () => ({
231
+ components: { Modal },
232
+ setup() {
233
+ const isOpen = ref(false)
234
+ const formData = ref({ name: '', email: '' })
235
+
236
+ const handleSubmit = () => {
237
+ console.log('Form submitted:', formData.value)
238
+ isOpen.value = false
239
+ }
240
+
241
+ return { isOpen, formData, handleSubmit }
242
+ },
243
+ template: `
244
+ <div>
245
+ <button
246
+ @click="isOpen = true"
247
+ style="padding: 0.625rem 1.25rem; background: #667eea; color: white; border: none; border-radius: 0.375rem; cursor: pointer; font-weight: 600;"
248
+ >
249
+ Add User
250
+ </button>
251
+ <Modal v-model="isOpen" title="Add New User">
252
+ <form @submit.prevent="handleSubmit">
253
+ <div style="margin-bottom: 1rem;">
254
+ <label style="display: block; margin-bottom: 0.5rem; font-weight: 600;">Name</label>
255
+ <input
256
+ v-model="formData.name"
257
+ type="text"
258
+ placeholder="Enter name"
259
+ required
260
+ style="width: 100%; padding: 0.5rem; border: 1px solid #d1d5db; border-radius: 0.375rem;"
261
+ />
262
+ </div>
263
+ <div style="margin-bottom: 1rem;">
264
+ <label style="display: block; margin-bottom: 0.5rem; font-weight: 600;">Email</label>
265
+ <input
266
+ v-model="formData.email"
267
+ type="email"
268
+ placeholder="Enter email"
269
+ required
270
+ style="width: 100%; padding: 0.5rem; border: 1px solid #d1d5db; border-radius: 0.375rem;"
271
+ />
272
+ </div>
273
+ </form>
274
+ <template #footer>
275
+ <div style="display: flex; gap: 0.75rem; justify-content: flex-end;">
276
+ <button
277
+ @click="isOpen = false"
278
+ type="button"
279
+ style="padding: 0.5rem 1rem; background: #e5e7eb; color: #374151; border: none; border-radius: 0.375rem; cursor: pointer; font-weight: 500;"
280
+ >
281
+ Cancel
282
+ </button>
283
+ <button
284
+ @click="handleSubmit"
285
+ type="submit"
286
+ style="padding: 0.5rem 1rem; background: #667eea; color: white; border: none; border-radius: 0.375rem; cursor: pointer; font-weight: 500;"
287
+ >
288
+ Add User
289
+ </button>
290
+ </div>
291
+ </template>
292
+ </Modal>
293
+ </div>
294
+ `
295
+ })
296
+ }
297
+
298
+ export const ConfirmDialog: Story = {
299
+ render: () => ({
300
+ components: { Modal },
301
+ setup() {
302
+ const isOpen = ref(false)
303
+
304
+ const handleDelete = () => {
305
+ console.log('Item deleted')
306
+ isOpen.value = false
307
+ }
308
+
309
+ return { isOpen, handleDelete }
310
+ },
311
+ template: `
312
+ <div>
313
+ <button
314
+ @click="isOpen = true"
315
+ style="padding: 0.625rem 1.25rem; background: #dc2626; color: white; border: none; border-radius: 0.375rem; cursor: pointer; font-weight: 600;"
316
+ >
317
+ Delete Item
318
+ </button>
319
+ <Modal v-model="isOpen" title="Confirm Deletion" size="small">
320
+ <p style="margin: 0;">Are you sure you want to delete this item? This action cannot be undone.</p>
321
+ <template #footer>
322
+ <div style="display: flex; gap: 0.75rem; justify-content: flex-end;">
323
+ <button
324
+ @click="isOpen = false"
325
+ style="padding: 0.5rem 1rem; background: #e5e7eb; color: #374151; border: none; border-radius: 0.375rem; cursor: pointer; font-weight: 500;"
326
+ >
327
+ Cancel
328
+ </button>
329
+ <button
330
+ @click="handleDelete"
331
+ style="padding: 0.5rem 1rem; background: #dc2626; color: white; border: none; border-radius: 0.375rem; cursor: pointer; font-weight: 500;"
332
+ >
333
+ Delete
334
+ </button>
335
+ </div>
336
+ </template>
337
+ </Modal>
338
+ </div>
339
+ `
340
+ })
341
+ }