@a-vision-software/vue-input-components 1.1.30 → 1.1.32

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.
@@ -0,0 +1,116 @@
1
+ <template>
2
+ <div class="text-area-input">
3
+ <label v-if="label" :class="{ required }">{{ label }}</label>
4
+ <textarea
5
+ :value="modelValue"
6
+ @input="handleInput"
7
+ :placeholder="placeholder"
8
+ :disabled="disabled"
9
+ :required="required"
10
+ :maxlength="maxlength"
11
+ :rows="rows"
12
+ :class="{ error: !!error }"
13
+ ></textarea>
14
+ <div v-if="error" class="error-message">{{ error }}</div>
15
+ </div>
16
+ </template>
17
+
18
+ <script setup lang="ts">
19
+ import { defineProps, defineEmits } from 'vue'
20
+
21
+ defineProps({
22
+ modelValue: {
23
+ type: String,
24
+ required: true,
25
+ },
26
+ label: {
27
+ type: String,
28
+ default: '',
29
+ },
30
+ placeholder: {
31
+ type: String,
32
+ default: '',
33
+ },
34
+ error: {
35
+ type: String,
36
+ default: '',
37
+ },
38
+ disabled: {
39
+ type: Boolean,
40
+ default: false,
41
+ },
42
+ required: {
43
+ type: Boolean,
44
+ default: false,
45
+ },
46
+ maxlength: {
47
+ type: Number,
48
+ default: undefined,
49
+ },
50
+ rows: {
51
+ type: Number,
52
+ default: 4,
53
+ },
54
+ })
55
+
56
+ const emit = defineEmits(['update:modelValue'])
57
+
58
+ const handleInput = (event: Event) => {
59
+ const target = event.target as HTMLTextAreaElement
60
+ emit('update:modelValue', target.value)
61
+ }
62
+ </script>
63
+
64
+ <style scoped>
65
+ .text-area-input {
66
+ display: flex;
67
+ flex-direction: column;
68
+ gap: 0.5rem;
69
+ width: 100%;
70
+ }
71
+
72
+ label {
73
+ font-size: 0.875rem;
74
+ font-weight: 500;
75
+ color: var(--text-color);
76
+ }
77
+
78
+ label.required::after {
79
+ content: '*';
80
+ color: var(--error-color);
81
+ margin-left: 0.25rem;
82
+ }
83
+
84
+ textarea {
85
+ width: 100%;
86
+ padding: 0.5rem;
87
+ border: 1px solid var(--border-color);
88
+ border-radius: 0.375rem;
89
+ font-size: 0.875rem;
90
+ line-height: 1.5;
91
+ color: var(--text-color);
92
+ background-color: var(--bg-color);
93
+ transition: border-color 0.2s ease-in-out;
94
+ resize: vertical;
95
+ min-height: 2.5rem;
96
+ }
97
+
98
+ textarea:focus {
99
+ outline: none;
100
+ border-color: var(--primary-color);
101
+ }
102
+
103
+ textarea:disabled {
104
+ background-color: var(--disabled-bg-color);
105
+ cursor: not-allowed;
106
+ }
107
+
108
+ textarea.error {
109
+ border-color: var(--error-color);
110
+ }
111
+
112
+ .error-message {
113
+ font-size: 0.75rem;
114
+ color: var(--error-color);
115
+ }
116
+ </style>
@@ -0,0 +1,368 @@
1
+ <template>
2
+ <div
3
+ class="text-input"
4
+ :class="{
5
+ [`label-${labelPosition}`]: label,
6
+ [`label-align-${labelAlign}`]: label,
7
+ }"
8
+ :style="[
9
+ { width: totalWidth || '100%' },
10
+ labelStyle,
11
+ {
12
+ '--max-textarea-height': props.maxHeight || props.height || '14rem',
13
+ '--textarea-height': props.height || '5.5rem',
14
+ },
15
+ ]"
16
+ >
17
+ <label v-if="label" :for="id" class="label">
18
+ {{ label }}
19
+ </label>
20
+ <div
21
+ class="input-wrapper"
22
+ :class="{
23
+ 'has-error': error,
24
+ 'has-icon': icon,
25
+ }"
26
+ >
27
+ <div v-if="icon" class="icon-wrapper" @click="focusInput">
28
+ <font-awesome-icon :icon="icon" class="icon" />
29
+ </div>
30
+ <input
31
+ v-if="!isTextarea"
32
+ :id="id"
33
+ :type="type"
34
+ :value="modelValue"
35
+ :placeholder="placeholder"
36
+ :required="required"
37
+ :disabled="disabled"
38
+ class="input"
39
+ @input="handleInput"
40
+ ref="inputRef"
41
+ />
42
+ <textarea
43
+ v-else
44
+ :id="id"
45
+ :value="modelValue"
46
+ :placeholder="placeholder"
47
+ :required="required"
48
+ :disabled="disabled"
49
+ class="input"
50
+ @input="handleInput"
51
+ ref="inputRef"
52
+ ></textarea>
53
+ <span
54
+ v-if="required && !showSaved && !showChanged"
55
+ class="status-indicator required-indicator"
56
+ >required</span
57
+ >
58
+ <transition name="fade">
59
+ <span v-if="showSaved && !error" class="status-indicator saved-indicator">saved</span>
60
+ </transition>
61
+ <transition name="fade">
62
+ <span v-if="showChanged && !error" class="status-indicator changed-indicator">changed</span>
63
+ </transition>
64
+ <div v-if="error" class="error-message">{{ error }}</div>
65
+ <span v-if="success" class="message success-message">{{ success }}</span>
66
+ </div>
67
+ </div>
68
+ </template>
69
+
70
+ <script setup lang="ts">
71
+ import { computed, ref, onUnmounted } from 'vue'
72
+
73
+ const props = defineProps<{
74
+ modelValue: string
75
+ label?: string
76
+ type?: string
77
+ icon?: string
78
+ placeholder?: string
79
+ required?: boolean
80
+ disabled?: boolean
81
+ error?: string
82
+ success?: string
83
+ labelPosition?: 'top' | 'left'
84
+ labelAlign?: 'left' | 'right' | 'center'
85
+ totalWidth?: string
86
+ inputWidth?: string
87
+ labelWidth?: string
88
+ autosave?: (value: string) => Promise<void>
89
+ isTextarea?: boolean
90
+ maxHeight?: string
91
+ height?: string
92
+ }>()
93
+
94
+ const emit = defineEmits<{
95
+ (e: 'update:modelValue', value: string): void
96
+ (e: 'changed'): void
97
+ (e: 'saved'): void
98
+ }>()
99
+
100
+ const id = computed(() => `input-${Math.random().toString(36).substr(2, 9)}`)
101
+ const showSaved = ref(false)
102
+ const showChanged = ref(false)
103
+ const isChanged = ref(false)
104
+ const debounceTimer = ref<number | null>(null)
105
+ const changedTimer = ref<number | null>(null)
106
+ const inputRef = ref<HTMLInputElement | null>(null)
107
+
108
+ const labelStyle = computed(() => {
109
+ if (!props.label) return {}
110
+ if (props.labelPosition === 'left' && props.labelWidth) {
111
+ return {
112
+ 'grid-template-columns': `${props.labelWidth} 1fr`,
113
+ }
114
+ }
115
+ return {}
116
+ })
117
+
118
+ const handleAutosave = async (value: string) => {
119
+ if (props.autosave) {
120
+ try {
121
+ await props.autosave(value)
122
+ if (!props.error) {
123
+ emit('saved')
124
+ showSaved.value = true
125
+ showChanged.value = false
126
+ setTimeout(() => {
127
+ showSaved.value = false
128
+ }, 3000)
129
+ }
130
+ } catch (error) {
131
+ console.error('Autosave failed:', error)
132
+ }
133
+ }
134
+ }
135
+
136
+ const debounceAutosave = (value: string) => {
137
+ // Clear existing timers
138
+ if (debounceTimer.value) {
139
+ clearTimeout(debounceTimer.value)
140
+ }
141
+ if (changedTimer.value) {
142
+ clearTimeout(changedTimer.value)
143
+ }
144
+
145
+ // Show changed indicator immediately
146
+ if (!props.error) {
147
+ showChanged.value = true
148
+ }
149
+
150
+ // Trigger changed event after 500ms
151
+ changedTimer.value = window.setTimeout(() => {
152
+ emit('changed')
153
+ isChanged.value = true
154
+ }, 500)
155
+
156
+ // Trigger autosave after 1500ms
157
+ debounceTimer.value = window.setTimeout(() => {
158
+ handleAutosave(value)
159
+ }, 1500)
160
+ }
161
+
162
+ const focusInput = () => {
163
+ inputRef.value?.focus()
164
+ }
165
+
166
+ const adjustHeight = (element: HTMLTextAreaElement) => {
167
+ element.style.height = 'auto' // Reset height to auto to calculate new height
168
+ element.style.height = `${element.scrollHeight}px` // Set height to scrollHeight
169
+ }
170
+
171
+ const handleInput = (event: Event) => {
172
+ const value = (event.target as HTMLTextAreaElement).value
173
+ emit('update:modelValue', value)
174
+ debounceAutosave(value)
175
+ adjustHeight(event.target as HTMLTextAreaElement) // Adjust height on input
176
+ }
177
+
178
+ // Cleanup timers on unmount
179
+ onUnmounted(() => {
180
+ if (debounceTimer.value) {
181
+ clearTimeout(debounceTimer.value)
182
+ }
183
+ if (changedTimer.value) {
184
+ clearTimeout(changedTimer.value)
185
+ }
186
+ })
187
+ </script>
188
+
189
+ <style scoped>
190
+ .text-input {
191
+ display: grid;
192
+ gap: 0.5rem;
193
+ width: 100%;
194
+ margin-top: 0.7rem;
195
+ }
196
+
197
+ .text-input.label-top {
198
+ grid-template-rows: auto 1fr;
199
+ }
200
+
201
+ .text-input.label-left {
202
+ grid-template-columns: 30% 1fr;
203
+ align-items: start;
204
+ gap: 1rem;
205
+ }
206
+
207
+ .text-input.label-left .label {
208
+ padding-top: 0.75rem;
209
+ width: 100%;
210
+ }
211
+
212
+ .label {
213
+ font-weight: 500;
214
+ color: var(--text-color);
215
+ text-align: left;
216
+ }
217
+
218
+ .label-align-left .label {
219
+ text-align: left;
220
+ }
221
+
222
+ .label-align-right .label {
223
+ text-align: right;
224
+ }
225
+
226
+ .label-align-center .label {
227
+ text-align: center;
228
+ }
229
+
230
+ .required {
231
+ color: var(--danger-color);
232
+ margin-left: 0.25rem;
233
+ }
234
+
235
+ .input-wrapper {
236
+ position: relative;
237
+ display: grid;
238
+ grid-template-columns: 1fr;
239
+ border: 1px solid var(--border-color);
240
+ border-radius: 0.5rem;
241
+ transition: all 0.2s ease;
242
+ width: 100%;
243
+ min-height: 2rem;
244
+ background: var(--input-bg-color);
245
+ }
246
+
247
+ .input-wrapper.has-icon {
248
+ grid-template-columns: auto 1fr;
249
+ }
250
+
251
+ .input-wrapper:focus-within {
252
+ border-color: var(--primary-color);
253
+ box-shadow: 0 0 0 3px var(--shadow-color);
254
+ }
255
+
256
+ .input-wrapper.has-error {
257
+ border-color: var(--danger-color);
258
+ border-bottom-left-radius: 0;
259
+ border-bottom-right-radius: 0;
260
+ }
261
+
262
+ .icon-wrapper {
263
+ display: grid;
264
+ place-items: start;
265
+ padding: 1rem;
266
+ border-right: 1px solid var(--border-color);
267
+ cursor: pointer;
268
+ overflow: hidden;
269
+ }
270
+
271
+ .icon-wrapper:hover {
272
+ background-color: var(--input-bg-hover);
273
+ }
274
+
275
+ .icon {
276
+ color: var(--text-muted);
277
+ font-size: 1rem;
278
+ }
279
+
280
+ .input {
281
+ padding: 0.75rem 1rem;
282
+ border: none;
283
+ outline: none;
284
+ font-size: 1rem;
285
+ color: var(--text-color);
286
+ background: transparent;
287
+ width: 100%;
288
+ line-height: var(--line-height);
289
+ }
290
+
291
+ .input::placeholder {
292
+ color: var(--text-muted);
293
+ }
294
+
295
+ .input:disabled {
296
+ background-color: var(--input-bg-disabled);
297
+ cursor: not-allowed;
298
+ }
299
+
300
+ .message {
301
+ position: absolute;
302
+ bottom: -1.5rem;
303
+ left: 0;
304
+ font-size: 0.75rem;
305
+ white-space: nowrap;
306
+ }
307
+
308
+ .error-message {
309
+ position: absolute;
310
+ bottom: 0;
311
+ left: 0;
312
+ right: 0;
313
+ padding: 0.25rem 0.75rem;
314
+ background-color: var(--danger-color);
315
+ color: white;
316
+ font-size: 0.75rem;
317
+ border-radius: 0 0 0.5rem 0.5rem;
318
+ transform: translateY(100%);
319
+ transition: transform 0.2s ease;
320
+ line-height: var(--line-height);
321
+ z-index: 1;
322
+ }
323
+
324
+ .success-message {
325
+ position: absolute;
326
+ bottom: -1.5rem;
327
+ left: 0;
328
+ color: var(--success-color);
329
+ font-size: 0.75rem;
330
+ line-height: var(--line-height);
331
+ }
332
+
333
+ .status-indicator {
334
+ position: absolute;
335
+ top: -0.1rem;
336
+ right: 0.5rem;
337
+ font-size: 0.75rem;
338
+ color: var(--text-muted);
339
+ line-height: 0px;
340
+ background-color: var(--input-bg-color);
341
+ padding: 0 0.25rem;
342
+ }
343
+
344
+ .saved-indicator {
345
+ color: var(--success-color);
346
+ }
347
+
348
+ .changed-indicator {
349
+ color: var(--warning-color);
350
+ }
351
+
352
+ .fade-enter-active,
353
+ .fade-leave-active {
354
+ transition: opacity 0.2s ease;
355
+ }
356
+
357
+ .fade-enter-from,
358
+ .fade-leave-to {
359
+ opacity: 0;
360
+ }
361
+
362
+ textarea {
363
+ min-height: var(--textarea-height, 5.5rem);
364
+ max-height: var(--max-textarea-height, 14rem);
365
+ overflow-y: auto;
366
+ resize: none;
367
+ }
368
+ </style>
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ import TextInput from './components/TextInput.vue'
2
+ import TextAreaInput from './components/TextAreaInput.vue'
3
+ import FileUpload from './components/FileUpload.vue'
4
+
5
+ export { TextInput, TextAreaInput, FileUpload }
6
+
7
+ // Export types
8
+ export * from './types'
package/src/main.ts ADDED
@@ -0,0 +1,21 @@
1
+ import { createApp } from 'vue'
2
+ import { library } from '@fortawesome/fontawesome-svg-core'
3
+ import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
4
+ import { fas } from '@fortawesome/free-solid-svg-icons'
5
+ import { far } from '@fortawesome/free-regular-svg-icons'
6
+
7
+ import App from './App.vue'
8
+ import router from './router'
9
+
10
+ import './assets/colors.css'
11
+ import './assets/main.css'
12
+
13
+ // Add all solid icons to the library
14
+ library.add(fas, far)
15
+
16
+ const app = createApp(App)
17
+
18
+ app.use(router)
19
+ app.component('font-awesome-icon', FontAwesomeIcon)
20
+
21
+ app.mount('#app')
@@ -0,0 +1,27 @@
1
+ import { createRouter, createWebHashHistory } from 'vue-router'
2
+ import DashboardView from '../views/DashboardView.vue'
3
+ import TextInputTestView from '../views/TextInputTestView.vue'
4
+ import FileUploadTestView from '../views/FileUploadTestView.vue'
5
+
6
+ const router = createRouter({
7
+ history: createWebHashHistory(),
8
+ routes: [
9
+ {
10
+ path: '/',
11
+ name: 'dashboard',
12
+ component: DashboardView,
13
+ },
14
+ {
15
+ path: '/text-input-test',
16
+ name: 'text-input-test',
17
+ component: TextInputTestView,
18
+ },
19
+ {
20
+ path: '/file-upload-test',
21
+ name: 'file-upload-test',
22
+ component: FileUploadTestView,
23
+ },
24
+ ],
25
+ })
26
+
27
+ export default router
package/src/types.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ declare module '@a-vision-software/vue-input-components' {
2
+ import { DefineComponent } from 'vue'
3
+
4
+ export const TextInput: DefineComponent<{
5
+ modelValue?: string
6
+ label?: string
7
+ placeholder?: string
8
+ required?: boolean
9
+ disabled?: boolean
10
+ error?: string
11
+ }>
12
+
13
+ export const FileUpload: DefineComponent<{
14
+ modelValue?: File[]
15
+ maxFiles?: number
16
+ maxFileSize?: number
17
+ accept?: string
18
+ uploadUrl?: string
19
+ required?: boolean
20
+ disabled?: boolean
21
+ error?: string
22
+ }>
23
+ }
package/src/types.ts ADDED
@@ -0,0 +1,48 @@
1
+ import type { Component } from 'vue'
2
+
3
+ export interface TextInputProps {
4
+ modelValue: string
5
+ label?: string
6
+ placeholder?: string
7
+ error?: string
8
+ disabled?: boolean
9
+ required?: boolean
10
+ maxlength?: number
11
+ }
12
+
13
+ export interface TextAreaInputProps {
14
+ modelValue: string
15
+ label?: string
16
+ placeholder?: string
17
+ error?: string
18
+ disabled?: boolean
19
+ required?: boolean
20
+ maxlength?: number
21
+ rows?: number
22
+ }
23
+
24
+ export interface FileUploadProps {
25
+ modelValue: File[]
26
+ label?: string
27
+ placeholder?: string
28
+ error?: string
29
+ disabled?: boolean
30
+ required?: boolean
31
+ multiple?: boolean
32
+ accept?: string
33
+ maxSize?: number
34
+ uploadUrl?: string
35
+ }
36
+
37
+ export interface FileUploadEmits {
38
+ (e: 'update:modelValue', files: File[]): void
39
+ (e: 'files-selected', files: File[]): void
40
+ (e: 'start-upload', files: File[]): void
41
+ (e: 'upload-progress', progress: number): void
42
+ (e: 'upload-success', response: any): void
43
+ (e: 'upload-error', error: Error): void
44
+ }
45
+
46
+ export type TextInputComponent = Component<TextInputProps>
47
+ export type TextAreaInputComponent = Component<TextAreaInputProps>
48
+ export type FileUploadComponent = Component<FileUploadProps>
@@ -0,0 +1,57 @@
1
+ <template>
2
+ <div class="dashboard">
3
+ <router-link to="/text-input-test" class="tile">
4
+ <font-awesome-icon icon="keyboard" size="2x" />
5
+ <span>Text Inputs Test</span>
6
+ </router-link>
7
+ <router-link to="/file-upload-test" class="tile">
8
+ <font-awesome-icon icon="upload" size="2x" />
9
+ <span>File Upload Test</span>
10
+ </router-link>
11
+ </div>
12
+ </template>
13
+
14
+ <script setup lang="ts"></script>
15
+
16
+ <style scoped>
17
+ .dashboard {
18
+ min-height: 100vh;
19
+ display: grid;
20
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
21
+ gap: 2rem;
22
+ place-items: center;
23
+ padding: 2rem;
24
+ }
25
+
26
+ .tile {
27
+ display: grid;
28
+ grid-template-rows: auto auto;
29
+ gap: 1rem;
30
+ justify-items: center;
31
+ align-items: center;
32
+ padding: 2rem;
33
+ background: #f8f9fa;
34
+ border: 1px solid #e9ecef;
35
+ border-radius: 0.75rem;
36
+ cursor: pointer;
37
+ transition: all 0.2s ease;
38
+ width: 200px;
39
+ text-decoration: none;
40
+ }
41
+
42
+ .tile:hover {
43
+ background: #e9ecef;
44
+ transform: translateY(-2px);
45
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
46
+ }
47
+
48
+ .tile svg {
49
+ color: #495057;
50
+ }
51
+
52
+ .tile span {
53
+ font-weight: 500;
54
+ color: #495057;
55
+ text-align: center;
56
+ }
57
+ </style>