@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,78 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3'
2
+ import Tag from './Tag.vue'
3
+
4
+ const meta = {
5
+ title: 'Components/Tag',
6
+ component: Tag,
7
+ tags: ['autodocs'],
8
+ argTypes: {
9
+ variant: {
10
+ control: 'select',
11
+ options: ['default', 'primary', 'success', 'warning', 'danger', 'info']
12
+ },
13
+ size: {
14
+ control: 'select',
15
+ options: ['small', 'medium', 'large']
16
+ }
17
+ },
18
+ args: {
19
+ variant: 'default',
20
+ size: 'medium'
21
+ }
22
+ } satisfies Meta<typeof Tag>
23
+
24
+ export default meta
25
+ type Story = StoryObj<typeof meta>
26
+
27
+ export const Default: Story = {
28
+ render: (args: any) => ({
29
+ components: { Tag },
30
+ setup() {
31
+ return { args }
32
+ },
33
+ template: '<Tag v-bind="args">Default Tag</Tag>'
34
+ })
35
+ }
36
+
37
+ export const Variants: Story = {
38
+ render: () => ({
39
+ components: { Tag },
40
+ template: `
41
+ <div style="display: flex; gap: 1rem; flex-wrap: wrap;">
42
+ <Tag variant="default">Default</Tag>
43
+ <Tag variant="primary">Primary</Tag>
44
+ <Tag variant="success">Success</Tag>
45
+ <Tag variant="warning">Warning</Tag>
46
+ <Tag variant="danger">Danger</Tag>
47
+ <Tag variant="info">Info</Tag>
48
+ </div>
49
+ `
50
+ })
51
+ }
52
+
53
+ export const Sizes: Story = {
54
+ render: () => ({
55
+ components: { Tag },
56
+ template: `
57
+ <div style="display: flex; gap: 1rem; align-items: center;">
58
+ <Tag size="small" variant="primary">Small</Tag>
59
+ <Tag size="medium" variant="primary">Medium</Tag>
60
+ <Tag size="large" variant="primary">Large</Tag>
61
+ </div>
62
+ `
63
+ })
64
+ }
65
+
66
+ export const SkillTags: Story = {
67
+ render: () => ({
68
+ components: { Tag },
69
+ template: `
70
+ <div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
71
+ <Tag variant="primary" size="small">Unity</Tag>
72
+ <Tag variant="success" size="small">C#</Tag>
73
+ <Tag variant="info" size="small">Game Design</Tag>
74
+ <Tag variant="warning" size="small">Blender</Tag>
75
+ </div>
76
+ `
77
+ })
78
+ }
@@ -0,0 +1,50 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import Tag from './Tag.vue'
4
+
5
+ describe('Tag', () => {
6
+ it('renders slot content', () => {
7
+ const wrapper = mount(Tag, {
8
+ slots: {
9
+ default: 'Test Tag'
10
+ }
11
+ })
12
+ expect(wrapper.text()).toBe('Test Tag')
13
+ })
14
+
15
+ it('applies default variant class', () => {
16
+ const wrapper = mount(Tag)
17
+ expect(wrapper.classes()).toContain('tag--default')
18
+ })
19
+
20
+ it('applies custom variant class', () => {
21
+ const wrapper = mount(Tag, {
22
+ props: { variant: 'primary' }
23
+ })
24
+ expect(wrapper.classes()).toContain('tag--primary')
25
+ })
26
+
27
+ it('applies default size class', () => {
28
+ const wrapper = mount(Tag)
29
+ expect(wrapper.classes()).toContain('tag--medium')
30
+ })
31
+
32
+ it('applies custom size class', () => {
33
+ const wrapper = mount(Tag, {
34
+ props: { size: 'small' }
35
+ })
36
+ expect(wrapper.classes()).toContain('tag--small')
37
+ })
38
+
39
+ it('applies multiple classes correctly', () => {
40
+ const wrapper = mount(Tag, {
41
+ props: {
42
+ variant: 'success',
43
+ size: 'large'
44
+ }
45
+ })
46
+ expect(wrapper.classes()).toContain('tag')
47
+ expect(wrapper.classes()).toContain('tag--success')
48
+ expect(wrapper.classes()).toContain('tag--large')
49
+ })
50
+ })
@@ -0,0 +1,71 @@
1
+ <template>
2
+ <span :class="['tag', `tag--${variant}`, `tag--${size}`]">
3
+ <slot />
4
+ </span>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import type { TagProps } from './types'
9
+
10
+ withDefaults(defineProps<TagProps>(), {
11
+ variant: 'default',
12
+ size: 'medium'
13
+ })
14
+ </script>
15
+
16
+ <style scoped>
17
+ .tag {
18
+ display: inline-flex;
19
+ align-items: center;
20
+ font-weight: 600;
21
+ border-radius: 0.375rem;
22
+ transition: all 0.2s ease-in-out;
23
+ }
24
+
25
+ /* Sizes */
26
+ .tag--small {
27
+ padding: 0.25rem 0.5rem;
28
+ font-size: 0.75rem;
29
+ }
30
+
31
+ .tag--medium {
32
+ padding: 0.375rem 0.75rem;
33
+ font-size: 0.875rem;
34
+ }
35
+
36
+ .tag--large {
37
+ padding: 0.5rem 1rem;
38
+ font-size: 1rem;
39
+ }
40
+
41
+ /* Variants */
42
+ .tag--default {
43
+ background-color: #f3f4f6;
44
+ color: #374151;
45
+ }
46
+
47
+ .tag--primary {
48
+ background-color: #dbeafe;
49
+ color: #1e40af;
50
+ }
51
+
52
+ .tag--success {
53
+ background-color: #d1fae5;
54
+ color: #065f46;
55
+ }
56
+
57
+ .tag--warning {
58
+ background-color: #fef3c7;
59
+ color: #92400e;
60
+ }
61
+
62
+ .tag--danger {
63
+ background-color: #fee2e2;
64
+ color: #991b1b;
65
+ }
66
+
67
+ .tag--info {
68
+ background-color: #e0e7ff;
69
+ color: #3730a3;
70
+ }
71
+ </style>
@@ -0,0 +1,4 @@
1
+ export interface TagProps {
2
+ variant?: 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'info'
3
+ size?: 'small' | 'medium' | 'large'
4
+ }
@@ -0,0 +1,171 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3'
2
+ import { ref } from 'vue'
3
+ import TextArea from './TextArea.vue'
4
+
5
+ const meta = {
6
+ title: 'Form Fields/TextArea',
7
+ component: TextArea,
8
+ tags: ['autodocs'],
9
+ argTypes: {
10
+ label: {
11
+ control: 'text',
12
+ description: 'Label text'
13
+ },
14
+ placeholder: {
15
+ control: 'text',
16
+ description: 'Placeholder text'
17
+ },
18
+ rows: {
19
+ control: 'number',
20
+ description: 'Number of visible text lines'
21
+ },
22
+ disabled: {
23
+ control: 'boolean',
24
+ description: 'Disabled state'
25
+ },
26
+ required: {
27
+ control: 'boolean',
28
+ description: 'Required field'
29
+ },
30
+ error: {
31
+ control: 'text',
32
+ description: 'Error message'
33
+ },
34
+ hint: {
35
+ control: 'text',
36
+ description: 'Hint text'
37
+ }
38
+ },
39
+ args: {
40
+ rows: 4
41
+ }
42
+ } satisfies Meta<typeof TextArea>
43
+
44
+ export default meta
45
+ type Story = StoryObj<typeof meta>
46
+
47
+ export const Default: Story = {
48
+ args: {
49
+ label: 'Message',
50
+ placeholder: 'Enter your message'
51
+ },
52
+ render: (args: any) => ({
53
+ components: { TextArea },
54
+ setup() {
55
+ const value = ref('')
56
+ return { args, value }
57
+ },
58
+ template: '<TextArea v-bind="args" v-model="value" />'
59
+ })
60
+ }
61
+
62
+ export const WithValue: Story = {
63
+ args: {
64
+ label: 'Description',
65
+ modelValue: 'This is a sample description that spans multiple lines.\n\nIt can contain paragraphs and line breaks.'
66
+ },
67
+ render: (args: any) => ({
68
+ components: { TextArea },
69
+ setup() {
70
+ const value = ref(args.modelValue ?? '')
71
+ return { args, value }
72
+ },
73
+ template: '<TextArea v-bind="args" v-model="value" />'
74
+ })
75
+ }
76
+
77
+ export const WithHint: Story = {
78
+ args: {
79
+ label: 'Comments',
80
+ placeholder: 'Add your comments',
81
+ hint: 'Maximum 500 characters'
82
+ },
83
+ render: (args: any) => ({
84
+ components: { TextArea },
85
+ setup() {
86
+ const value = ref('')
87
+ return { args, value }
88
+ },
89
+ template: '<TextArea v-bind="args" v-model="value" />'
90
+ })
91
+ }
92
+
93
+ export const WithError: Story = {
94
+ args: {
95
+ label: 'Feedback',
96
+ modelValue: 'Too short',
97
+ error: 'Feedback must be at least 50 characters'
98
+ },
99
+ render: (args: any) => ({
100
+ components: { TextArea },
101
+ setup() {
102
+ const value = ref(args.modelValue ?? '')
103
+ return { args, value }
104
+ },
105
+ template: '<TextArea v-bind="args" v-model="value" />'
106
+ })
107
+ }
108
+
109
+ export const Required: Story = {
110
+ args: {
111
+ label: 'Description',
112
+ placeholder: 'Describe the issue',
113
+ required: true
114
+ },
115
+ render: (args: any) => ({
116
+ components: { TextArea },
117
+ setup() {
118
+ const value = ref('')
119
+ return { args, value }
120
+ },
121
+ template: '<TextArea v-bind="args" v-model="value" />'
122
+ })
123
+ }
124
+
125
+ export const Disabled: Story = {
126
+ args: {
127
+ label: 'Read Only',
128
+ modelValue: 'This content cannot be edited',
129
+ disabled: true
130
+ },
131
+ render: (args: any) => ({
132
+ components: { TextArea },
133
+ setup() {
134
+ const value = ref(args.modelValue ?? '')
135
+ return { args, value }
136
+ },
137
+ template: '<TextArea v-bind="args" v-model="value" />'
138
+ })
139
+ }
140
+
141
+ export const CustomRows: Story = {
142
+ args: {
143
+ label: 'Short Note',
144
+ placeholder: 'Brief note',
145
+ rows: 2
146
+ },
147
+ render: (args: any) => ({
148
+ components: { TextArea },
149
+ setup() {
150
+ const value = ref('')
151
+ return { args, value }
152
+ },
153
+ template: '<TextArea v-bind="args" v-model="value" />'
154
+ })
155
+ }
156
+
157
+ export const LargeTextArea: Story = {
158
+ args: {
159
+ label: 'Article Content',
160
+ placeholder: 'Write your article content here',
161
+ rows: 10
162
+ },
163
+ render: (args: any) => ({
164
+ components: { TextArea },
165
+ setup() {
166
+ const value = ref('')
167
+ return { args, value }
168
+ },
169
+ template: '<TextArea v-bind="args" v-model="value" />'
170
+ })
171
+ }
@@ -0,0 +1,202 @@
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 TextArea from './TextArea.vue'
5
+
6
+ describe('TextArea', () => {
7
+ describe('rendering', () => {
8
+ it('renders with label', () => {
9
+ render(TextArea, {
10
+ props: {
11
+ label: 'Message'
12
+ }
13
+ })
14
+
15
+ expect(screen.getByText('Message')).toBeInTheDocument()
16
+ })
17
+
18
+ it('renders without label', () => {
19
+ const { container } = render(TextArea)
20
+
21
+ expect(container.querySelector('.textarea-label')).not.toBeInTheDocument()
22
+ })
23
+
24
+ it('renders placeholder', () => {
25
+ render(TextArea, {
26
+ props: {
27
+ placeholder: 'Enter your message'
28
+ }
29
+ })
30
+
31
+ expect(screen.getByPlaceholderText('Enter your message')).toBeInTheDocument()
32
+ })
33
+
34
+ it('renders error message', () => {
35
+ render(TextArea, {
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(TextArea, {
46
+ props: {
47
+ hint: 'Maximum 500 characters'
48
+ }
49
+ })
50
+
51
+ expect(screen.getByText('Maximum 500 characters')).toBeInTheDocument()
52
+ })
53
+
54
+ it('does not show hint when error is present', () => {
55
+ render(TextArea, {
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(TextArea, {
68
+ props: {
69
+ label: 'Comments',
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(TextArea, {
79
+ props: {
80
+ error: 'Error'
81
+ }
82
+ })
83
+
84
+ const textarea = screen.getByRole('textbox')
85
+ expect(textarea).toHaveClass('textarea-field--error')
86
+ })
87
+
88
+ it('sets custom rows attribute', () => {
89
+ render(TextArea, {
90
+ props: {
91
+ rows: 8
92
+ }
93
+ })
94
+
95
+ const textarea = screen.getByRole('textbox')
96
+ expect(textarea).toHaveAttribute('rows', '8')
97
+ })
98
+
99
+ it('uses default rows value of 4', () => {
100
+ render(TextArea)
101
+
102
+ const textarea = screen.getByRole('textbox')
103
+ expect(textarea).toHaveAttribute('rows', '4')
104
+ })
105
+ })
106
+
107
+ describe('v-model', () => {
108
+ it('displays initial value', () => {
109
+ render(TextArea, {
110
+ props: {
111
+ modelValue: 'Initial text'
112
+ }
113
+ })
114
+
115
+ const textarea = screen.getByRole('textbox') as HTMLTextAreaElement
116
+ expect(textarea.value).toBe('Initial text')
117
+ })
118
+
119
+ it('emits update:modelValue on input', async () => {
120
+ const user = userEvent.setup()
121
+ const { emitted } = render(TextArea)
122
+
123
+ const textarea = screen.getByRole('textbox')
124
+ await user.type(textarea, 'test')
125
+
126
+ expect(emitted()['update:modelValue']).toBeTruthy()
127
+ expect(emitted()['update:modelValue'].length).toBeGreaterThan(0)
128
+ })
129
+
130
+ it('handles multiline input', async () => {
131
+ const user = userEvent.setup()
132
+ const { emitted } = render(TextArea)
133
+
134
+ const textarea = screen.getByRole('textbox')
135
+ await user.type(textarea, 'Line 1{Enter}Line 2')
136
+
137
+ const emissions = emitted()['update:modelValue'] as any[]
138
+ expect(emissions).toBeTruthy()
139
+ const lastEmission = emissions[emissions.length - 1][0] as string
140
+ expect(lastEmission).toContain('\n')
141
+ })
142
+ })
143
+
144
+ describe('disabled state', () => {
145
+ it('disables the textarea when disabled prop is true', () => {
146
+ render(TextArea, {
147
+ props: {
148
+ disabled: true
149
+ }
150
+ })
151
+
152
+ const textarea = screen.getByRole('textbox')
153
+ expect(textarea).toBeDisabled()
154
+ })
155
+
156
+ it('applies disabled class', () => {
157
+ render(TextArea, {
158
+ props: {
159
+ disabled: true
160
+ }
161
+ })
162
+
163
+ const textarea = screen.getByRole('textbox')
164
+ expect(textarea).toHaveClass('textarea-field--disabled')
165
+ })
166
+ })
167
+
168
+ describe('required attribute', () => {
169
+ it('sets required attribute when required prop is true', () => {
170
+ render(TextArea, {
171
+ props: {
172
+ required: true
173
+ }
174
+ })
175
+
176
+ const textarea = screen.getByRole('textbox')
177
+ expect(textarea).toBeRequired()
178
+ })
179
+
180
+ it('does not set required attribute by default', () => {
181
+ render(TextArea)
182
+
183
+ const textarea = screen.getByRole('textbox')
184
+ expect(textarea).not.toBeRequired()
185
+ })
186
+ })
187
+
188
+ describe('accessibility', () => {
189
+ it('associates label with textarea using htmlFor', () => {
190
+ render(TextArea, {
191
+ props: {
192
+ label: 'Message'
193
+ }
194
+ })
195
+
196
+ const label = screen.getByText('Message') as HTMLLabelElement
197
+ const textarea = screen.getByRole('textbox')
198
+
199
+ expect(label.htmlFor).toBe(textarea.id)
200
+ })
201
+ })
202
+ })
@@ -0,0 +1,122 @@
1
+ <template>
2
+ <div class="textarea-wrapper">
3
+ <label v-if="label" :for="textareaId" class="textarea-label">
4
+ {{ label }}
5
+ </label>
6
+ <textarea
7
+ :id="textareaId"
8
+ :value="modelValue"
9
+ :placeholder="placeholder"
10
+ :disabled="disabled"
11
+ :required="required"
12
+ :rows="rows"
13
+ :class="['textarea-field', { 'textarea-field--error': error, 'textarea-field--disabled': disabled }]"
14
+ @input="handleInput"
15
+ />
16
+ <span v-if="error" class="textarea-error">{{ error }}</span>
17
+ <span v-if="hint && !error" class="textarea-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 TextAreaProps {
27
+ modelValue?: string
28
+ id?: string
29
+ label?: string
30
+ placeholder?: string
31
+ disabled?: boolean
32
+ required?: boolean
33
+ rows?: number
34
+ error?: string
35
+ hint?: string
36
+ }
37
+
38
+ const props = withDefaults(defineProps<TextAreaProps>(), {
39
+ rows: 4,
40
+ disabled: false,
41
+ required: false
42
+ })
43
+
44
+ const emit = defineEmits<{
45
+ 'update:modelValue': [value: string]
46
+ }>()
47
+
48
+ const generatedId = `textarea-${++componentIdCounter}`
49
+ const textareaId = computed(() => props.id || generatedId)
50
+
51
+ const handleInput = (event: Event) => {
52
+ const target = event.target as HTMLTextAreaElement
53
+ emit('update:modelValue', target.value)
54
+ }
55
+ </script>
56
+
57
+ <style scoped>
58
+ .textarea-wrapper {
59
+ display: flex;
60
+ flex-direction: column;
61
+ gap: 0.375rem;
62
+ }
63
+
64
+ .textarea-label {
65
+ font-size: 0.875rem;
66
+ font-weight: 500;
67
+ color: #374151;
68
+ }
69
+
70
+
71
+ .textarea-field {
72
+ padding: 0.625rem 0.875rem;
73
+ font-size: 1rem;
74
+ font-family: inherit;
75
+ border: 1px solid #d1d5db;
76
+ border-radius: 0.375rem;
77
+ outline: none;
78
+ transition: all 0.2s ease-in-out;
79
+ background: white;
80
+ color: #1f2937;
81
+ resize: vertical;
82
+ min-height: 2.5rem;
83
+ }
84
+
85
+ .textarea-field::placeholder {
86
+ color: #9ca3af;
87
+ }
88
+
89
+ .textarea-field:focus {
90
+ border-color: #3b82f6;
91
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
92
+ }
93
+
94
+ .textarea-field--error {
95
+ border-color: #fca5a5;
96
+ border-bottom: 2px solid #dc2626;
97
+ background: #fef2f2;
98
+ }
99
+
100
+ .textarea-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
+ .textarea-field--disabled {
107
+ background: #f3f4f6;
108
+ cursor: not-allowed;
109
+ opacity: 0.6;
110
+ resize: none;
111
+ }
112
+
113
+ .textarea-error {
114
+ font-size: 0.75rem;
115
+ color: #ef4444;
116
+ }
117
+
118
+ .textarea-hint {
119
+ font-size: 0.75rem;
120
+ color: #6b7280;
121
+ }
122
+ </style>
@@ -0,0 +1,11 @@
1
+ export interface TextAreaProps {
2
+ modelValue?: string
3
+ id?: string
4
+ label?: string
5
+ placeholder?: string
6
+ disabled?: boolean
7
+ required?: boolean
8
+ rows?: number
9
+ error?: string
10
+ hint?: string
11
+ }