@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,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
|
+
}
|