@a-vision-software/vue-input-components 1.2.4 → 1.2.6

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,503 @@
1
+ <template>
2
+ <div
3
+ class="text-input"
4
+ :class="{
5
+ [`label-${labelPosition}`]: label,
6
+ [`label-align-${labelAlign}`]: label,
7
+ }"
8
+ :style="[
9
+ { width: type === 'date' ? totalWidth || '12rem' : 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
+ <Datepicker
31
+ v-if="type === 'date'"
32
+ :id="id"
33
+ v-model="dateValue"
34
+ :placeholder="placeholder"
35
+ :disabled="disabled"
36
+ :readonly="readonly"
37
+ :min-date="min"
38
+ :max-date="max"
39
+ :format="dateFormat"
40
+ :enable-time-picker="false"
41
+ :auto-apply="true"
42
+ :close-on-auto-apply="true"
43
+ :clearable="true"
44
+ :input-class-name="['input', { 'has-icon': icon }]"
45
+ @update:model-value="handleDateChange"
46
+ @focus="handleFocus"
47
+ @blur="handleBlur"
48
+ />
49
+ <input
50
+ v-else-if="!isTextarea"
51
+ :id="id"
52
+ :type="type"
53
+ :value="modelValue"
54
+ :placeholder="placeholder"
55
+ :required="required"
56
+ :disabled="disabled"
57
+ :readonly="readonly"
58
+ :maxlength="maxlength"
59
+ class="input"
60
+ @input="handleInput"
61
+ @focus="handleFocus"
62
+ @blur="handleBlur"
63
+ @keydown="handleKeydown"
64
+ ref="inputRef"
65
+ />
66
+ <textarea
67
+ v-else
68
+ :id="id"
69
+ :value="modelValue"
70
+ :placeholder="placeholder"
71
+ :required="required"
72
+ :disabled="disabled"
73
+ class="input"
74
+ @input="handleInput"
75
+ ref="inputRef"
76
+ ></textarea>
77
+ <span
78
+ v-if="required && !showSaved && !showChanged"
79
+ class="status-indicator required-indicator"
80
+ >required</span
81
+ >
82
+ <transition name="fade">
83
+ <span v-if="showSaved && !error" class="status-indicator saved-indicator">saved</span>
84
+ </transition>
85
+ <transition name="fade">
86
+ <span v-if="showChanged && !error" class="status-indicator changed-indicator">changed</span>
87
+ </transition>
88
+ <div v-if="error" class="error-message">{{ error }}</div>
89
+ <span v-if="success" class="message success-message">{{ success }}</span>
90
+ </div>
91
+ </div>
92
+ </template>
93
+
94
+ <script setup lang="ts">
95
+ import { computed, ref, onUnmounted, onMounted } from 'vue'
96
+ import { TextInputProps } from '../types'
97
+ import Datepicker from '@vuepic/vue-datepicker'
98
+ import '@vuepic/vue-datepicker/dist/main.css'
99
+
100
+ const props = withDefaults(defineProps<TextInputProps>(), {
101
+ modelValue: '',
102
+ type: 'text',
103
+ placeholder: '',
104
+ label: '',
105
+ icon: undefined,
106
+ disabled: false,
107
+ readonly: false,
108
+ maxlength: undefined,
109
+ error: '',
110
+ min: undefined,
111
+ max: undefined,
112
+ })
113
+
114
+ const emit = defineEmits<{
115
+ (e: 'update:modelValue', value: string): void
116
+ (e: 'changed'): void
117
+ (e: 'saved'): void
118
+ (e: 'focus'): void
119
+ (e: 'blur'): void
120
+ (e: 'keydown', event: KeyboardEvent): void
121
+ }>()
122
+
123
+ const id = ref<string>('')
124
+ const showSaved = ref(false)
125
+ const showChanged = ref(false)
126
+ const isChanged = ref(false)
127
+ const debounceTimer = ref<number | null>(null)
128
+ const changedTimer = ref<number | null>(null)
129
+ const inputRef = ref<HTMLInputElement | null>(null)
130
+ const dateValue = ref<Date | null>(null)
131
+
132
+ const dateFormat = 'dd/MM/yyyy'
133
+
134
+ const labelStyle = computed(() => {
135
+ if (!props.label) return {}
136
+ if (props.labelPosition === 'left' && props.labelWidth) {
137
+ return {
138
+ 'grid-template-columns': `${props.labelWidth} 1fr`,
139
+ }
140
+ }
141
+ return {}
142
+ })
143
+
144
+ const formatDateForModel = (date: Date | null): string => {
145
+ if (!date) return ''
146
+ const day = String(date.getDate()).padStart(2, '0')
147
+ const month = String(date.getMonth() + 1).padStart(2, '0')
148
+ const year = date.getFullYear()
149
+ return `${day}/${month}/${year}`
150
+ }
151
+
152
+ const parseDateFromModel = (dateStr: string): Date | null => {
153
+ if (!dateStr) return null
154
+ const [day, month, year] = dateStr.split('/').map(Number)
155
+ return new Date(year, month - 1, day)
156
+ }
157
+
158
+ const handleAutosave = async (value: string) => {
159
+ if (props.autosave) {
160
+ try {
161
+ await props.autosave(value)
162
+ if (!props.error) {
163
+ emit('saved')
164
+ showSaved.value = true
165
+ showChanged.value = false
166
+ setTimeout(() => {
167
+ showSaved.value = false
168
+ }, 3000)
169
+ }
170
+ } catch (error) {
171
+ console.error('Autosave failed:', error)
172
+ }
173
+ }
174
+ }
175
+
176
+ const debounceAutosave = (value: string) => {
177
+ // Clear existing timers
178
+ if (debounceTimer.value) {
179
+ clearTimeout(debounceTimer.value)
180
+ }
181
+ if (changedTimer.value) {
182
+ clearTimeout(changedTimer.value)
183
+ }
184
+
185
+ // Show changed indicator immediately
186
+ if (!props.error) {
187
+ showChanged.value = true
188
+ }
189
+
190
+ // Trigger changed event after 500ms
191
+ changedTimer.value = window.setTimeout(() => {
192
+ emit('changed')
193
+ isChanged.value = true
194
+ }, 500)
195
+
196
+ // Trigger autosave after 1500ms
197
+ debounceTimer.value = window.setTimeout(() => {
198
+ handleAutosave(value)
199
+ }, 1500)
200
+ }
201
+
202
+ const focusInput = () => {
203
+ inputRef.value?.focus()
204
+ }
205
+
206
+ const adjustHeight = (element: HTMLTextAreaElement) => {
207
+ element.style.height = 'auto' // Reset height to auto to calculate new height
208
+ element.style.height = `${element.scrollHeight}px` // Set height to scrollHeight
209
+ }
210
+
211
+ const handleInput = (event: Event) => {
212
+ const value = (event.target as HTMLTextAreaElement).value
213
+ emit('update:modelValue', value)
214
+ debounceAutosave(value)
215
+ adjustHeight(event.target as HTMLTextAreaElement) // Adjust height on input
216
+ }
217
+
218
+ const handleFocus = () => {
219
+ emit('focus')
220
+ }
221
+
222
+ const handleBlur = () => {
223
+ emit('blur')
224
+ }
225
+
226
+ const handleKeydown = (event: KeyboardEvent) => {
227
+ emit('keydown', event)
228
+ }
229
+
230
+ const handleDateChange = (date: Date | null) => {
231
+ const formattedDate = formatDateForModel(date)
232
+ emit('update:modelValue', formattedDate)
233
+ debounceAutosave(formattedDate)
234
+ }
235
+
236
+ // Cleanup timers on unmount
237
+ onUnmounted(() => {
238
+ if (debounceTimer.value) {
239
+ clearTimeout(debounceTimer.value)
240
+ }
241
+ if (changedTimer.value) {
242
+ clearTimeout(changedTimer.value)
243
+ }
244
+ })
245
+
246
+ onMounted(() => {
247
+ id.value = `text-input-${Math.random().toString(36).substr(2, 9)}`
248
+ if (props.type === 'date' && props.modelValue) {
249
+ dateValue.value = parseDateFromModel(props.modelValue)
250
+ }
251
+ })
252
+
253
+ defineExpose({
254
+ focus: () => inputRef.value?.focus(),
255
+ blur: () => inputRef.value?.blur(),
256
+ })
257
+ </script>
258
+
259
+ <style scoped>
260
+ .text-input {
261
+ display: grid;
262
+ gap: 0.5rem;
263
+ width: 100%;
264
+ margin-top: 0.7rem;
265
+ }
266
+
267
+ .text-input.label-top {
268
+ grid-template-rows: auto 1fr;
269
+ }
270
+
271
+ .text-input.label-left {
272
+ grid-template-columns: 30% 1fr;
273
+ align-items: start;
274
+ gap: 1rem;
275
+ }
276
+
277
+ .text-input.label-left .label {
278
+ padding-top: 0.75rem;
279
+ width: 100%;
280
+ }
281
+
282
+ .label {
283
+ font-weight: 500;
284
+ color: var(--text-color);
285
+ text-align: left;
286
+ }
287
+
288
+ .label-align-left .label {
289
+ text-align: left;
290
+ }
291
+
292
+ .label-align-right .label {
293
+ text-align: right;
294
+ }
295
+
296
+ .label-align-center .label {
297
+ text-align: center;
298
+ }
299
+
300
+ .required {
301
+ color: var(--danger-color);
302
+ margin-left: 0.25rem;
303
+ }
304
+
305
+ .input-wrapper {
306
+ position: relative;
307
+ display: grid;
308
+ grid-template-columns: 1fr;
309
+ border: 1px solid var(--border-color);
310
+ border-radius: 0.5rem;
311
+ transition: all 0.2s ease;
312
+ width: 100%;
313
+ min-height: 2rem;
314
+ background: var(--input-bg-color);
315
+ }
316
+
317
+ .input-wrapper.has-icon {
318
+ grid-template-columns: auto 1fr;
319
+ }
320
+
321
+ .input-wrapper:focus-within {
322
+ border-color: var(--primary-color);
323
+ box-shadow: 0 0 0 3px var(--shadow-color);
324
+ }
325
+
326
+ .input-wrapper.has-error {
327
+ border-color: var(--danger-color);
328
+ border-bottom-left-radius: 0;
329
+ border-bottom-right-radius: 0;
330
+ }
331
+
332
+ .icon-wrapper {
333
+ display: grid;
334
+ place-items: start;
335
+ padding: 1rem;
336
+ border-right: 1px solid var(--border-color);
337
+ cursor: pointer;
338
+ overflow: hidden;
339
+ }
340
+
341
+ .icon-wrapper:hover {
342
+ background-color: var(--input-bg-hover);
343
+ }
344
+
345
+ .icon {
346
+ color: var(--text-muted);
347
+ font-size: 1rem;
348
+ }
349
+
350
+ .input {
351
+ padding: 0.75rem 1rem;
352
+ border: none;
353
+ outline: none;
354
+ font-size: 1rem;
355
+ color: var(--text-color);
356
+ background: transparent;
357
+ width: 100%;
358
+ line-height: var(--line-height);
359
+ }
360
+
361
+ .input::placeholder {
362
+ color: var(--text-muted);
363
+ }
364
+
365
+ .input:disabled {
366
+ background-color: var(--input-bg-disabled);
367
+ cursor: not-allowed;
368
+ }
369
+
370
+ .message {
371
+ position: absolute;
372
+ bottom: -1.5rem;
373
+ left: 0;
374
+ font-size: 0.75rem;
375
+ white-space: nowrap;
376
+ }
377
+
378
+ .error-message {
379
+ position: absolute;
380
+ bottom: 0;
381
+ left: 0;
382
+ right: 0;
383
+ padding: 0.25rem 0.75rem;
384
+ background-color: var(--danger-color);
385
+ color: white;
386
+ font-size: 0.75rem;
387
+ border-radius: 0 0 0.5rem 0.5rem;
388
+ transform: translateY(100%);
389
+ transition: transform 0.2s ease;
390
+ line-height: var(--line-height);
391
+ z-index: 1;
392
+ }
393
+
394
+ .success-message {
395
+ position: absolute;
396
+ bottom: -1.5rem;
397
+ left: 0;
398
+ color: var(--success-color);
399
+ font-size: 0.75rem;
400
+ line-height: var(--line-height);
401
+ }
402
+
403
+ .status-indicator {
404
+ position: absolute;
405
+ top: -0.1rem;
406
+ right: 0.5rem;
407
+ font-size: 0.75rem;
408
+ color: var(--text-muted);
409
+ line-height: 0px;
410
+ background-color: var(--input-bg-color);
411
+ padding: 0 0.25rem;
412
+ }
413
+
414
+ .saved-indicator {
415
+ color: var(--success-color);
416
+ }
417
+
418
+ .changed-indicator {
419
+ color: var(--warning-color);
420
+ }
421
+
422
+ .fade-enter-active,
423
+ .fade-leave-active {
424
+ transition: opacity 0.2s ease;
425
+ }
426
+
427
+ .fade-enter-from,
428
+ .fade-leave-to {
429
+ opacity: 0;
430
+ }
431
+
432
+ textarea {
433
+ min-height: var(--textarea-height, 5.5rem);
434
+ max-height: var(--max-textarea-height, 14rem);
435
+ overflow-y: auto;
436
+ resize: none;
437
+ }
438
+
439
+ :deep(.dp__input) {
440
+ padding: 0.75rem 1rem;
441
+ border: none;
442
+ outline: none;
443
+ font-size: 1rem;
444
+ color: var(--text-color);
445
+ background: transparent;
446
+ width: 100%;
447
+ line-height: var(--line-height);
448
+ }
449
+
450
+ :deep(.dp__input::placeholder) {
451
+ color: var(--text-muted);
452
+ }
453
+
454
+ :deep(.dp__input:disabled) {
455
+ background-color: var(--input-bg-disabled);
456
+ cursor: not-allowed;
457
+ }
458
+
459
+ :deep(.dp__input.has-icon) {
460
+ padding-left: 2.5rem;
461
+ }
462
+
463
+ :deep(.dp__input_icon) {
464
+ display: none;
465
+ }
466
+
467
+ :deep(.dp__input_icon_pad) {
468
+ padding-right: 0.75rem;
469
+ }
470
+
471
+ :deep(.dp__menu) {
472
+ background-color: var(--input-bg-color);
473
+ border: 1px solid var(--border-color);
474
+ border-radius: 0.5rem;
475
+ box-shadow:
476
+ 0 4px 6px -1px rgba(0, 0, 0, 0.1),
477
+ 0 2px 4px -1px rgba(0, 0, 0, 0.06);
478
+ }
479
+
480
+ :deep(.dp__cell_inner) {
481
+ color: var(--text-color);
482
+ }
483
+
484
+ :deep(.dp__today) {
485
+ border-color: var(--primary-color);
486
+ }
487
+
488
+ :deep(.dp__active_date) {
489
+ background-color: var(--primary-color);
490
+ color: white;
491
+ }
492
+
493
+ :deep(.dp__range_start),
494
+ :deep(.dp__range_end) {
495
+ background-color: var(--primary-color);
496
+ color: white;
497
+ }
498
+
499
+ :deep(.dp__range_between) {
500
+ background-color: var(--primary-color-light);
501
+ color: var(--text-color);
502
+ }
503
+ </style>
package/src/env.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ interface ImportMetaEnv {
4
+ readonly BASE_URL: string
5
+ }
6
+
7
+ interface ImportMeta {
8
+ readonly env: ImportMetaEnv
9
+ }
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ import TextInput from './components/TextInput.vue'
2
+ import FileUpload from './components/FileUpload.vue'
3
+ import Navigation from './components/Navigation.vue'
4
+ import Action from './components/Action.vue'
5
+
6
+ export { TextInput, FileUpload, Navigation, Action }
7
+
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,44 @@
1
+ import { createRouter, createWebHistory } from 'vue-router'
2
+ import TextInputTestView from '../views/TextInputTestView.vue'
3
+ import FileUploadTestView from '../views/FileUploadTestView.vue'
4
+ import ActionTestView from '../views/ActionTestView.vue'
5
+ import DashboardView from '../views/DashboardView.vue'
6
+ import NavigationTestView from '../views/NavigationTestView.vue'
7
+
8
+ const router = createRouter({
9
+ history: createWebHistory(import.meta.env.BASE_URL),
10
+ routes: [
11
+ {
12
+ path: '/',
13
+ name: 'dashboard',
14
+ component: DashboardView,
15
+ meta: { title: 'Dashboard' },
16
+ },
17
+ {
18
+ path: '/text-input',
19
+ name: 'text-input',
20
+ component: TextInputTestView,
21
+ meta: { title: 'Text Input Test' },
22
+ },
23
+ {
24
+ path: '/file-upload',
25
+ name: 'file-upload',
26
+ component: FileUploadTestView,
27
+ meta: { title: 'File Upload Test' },
28
+ },
29
+ {
30
+ path: '/action',
31
+ name: 'action',
32
+ component: ActionTestView,
33
+ meta: { title: 'Action Test' },
34
+ },
35
+ {
36
+ path: '/navigation',
37
+ name: 'navigation',
38
+ component: NavigationTestView,
39
+ meta: { title: 'Navigation Test' },
40
+ },
41
+ ],
42
+ })
43
+
44
+ export default router
@@ -0,0 +1,3 @@
1
+ import type { NavigationProps, NavigationItem } from './navigation'
2
+
3
+ export type { NavigationProps, NavigationItem }
@@ -0,0 +1,32 @@
1
+ export interface NavigationItem {
2
+ id: string
3
+ label: string
4
+ url?: string
5
+ icon?: string
6
+ disabled?: boolean
7
+ alignment?: 'left' | 'right' | 'start' | 'end'
8
+ width?: string
9
+ children?: NavigationItem[]
10
+ }
11
+
12
+ export interface NavigationProps {
13
+ items: NavigationItem[]
14
+ type?: 'tiles' | 'dropdowns'
15
+ orientation?: 'horizontal' | 'vertical'
16
+ activeItem?: string
17
+ color?: string
18
+ hoverColor?: string
19
+ activeColor?: string
20
+ disabledColor?: string
21
+ gap?: string
22
+ padding?: string
23
+ borderRadius?: string
24
+ height?: string
25
+ width?: string
26
+ backgroundColor?: string
27
+ activeBackgroundColor?: string
28
+ activeItemAlignment?: 'left' | 'right' | 'top' | 'bottom'
29
+ showBottomBorder?: boolean
30
+ bottomBorderColor?: string
31
+ iconSize?: 'normal' | 'large'
32
+ }
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
+ }