@a-vision-software/vue-input-components 1.3.2 → 1.3.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a-vision-software/vue-input-components",
3
- "version": "1.3.2",
3
+ "version": "1.3.4",
4
4
  "description": "A collection of reusable Vue 3 input components with TypeScript support",
5
5
  "author": "A-Vision Software",
6
6
  "license": "MIT",
@@ -4,13 +4,14 @@
4
4
  'dropdown--disabled': disabled,
5
5
  'dropdown--multiple': multiple,
6
6
  'dropdown--large-icon': iconSize === 'large',
7
+ 'dropdown--has-error': error,
7
8
  }" :style="{
8
- '--dropdown-color': color,
9
+ '--dropdown-color': error ? 'var(--danger-color)' : color,
9
10
  '--dropdown-hover-color': hoverColor,
10
11
  '--dropdown-active-color': activeColor,
11
12
  '--dropdown-disabled-color': disabledColor,
12
13
  '--dropdown-background-color': backgroundColor,
13
- '--dropdown-border-radius': borderRadius,
14
+ '--dropdown-border-radius': error ? `${borderRadius} ${borderRadius} 0 0` : borderRadius,
14
15
  '--dropdown-padding': padding,
15
16
  '--dropdown-max-height': maxHeight,
16
17
  '--dropdown-width': width,
@@ -41,6 +42,14 @@
41
42
  @click.stop="clearSelection" />
42
43
  <font-awesome-icon icon="chevron-down" class="dropdown__arrow" :class="{ 'dropdown__arrow--open': isOpen }" />
43
44
  </div>
45
+ <span v-if="required && !showSaved && !showChanged" class="status-indicator required-indicator">required</span>
46
+ <transition name="fade">
47
+ <span v-if="showSaved && !error" class="status-indicator saved-indicator">saved</span>
48
+ </transition>
49
+ <transition name="fade">
50
+ <span v-if="showChanged && !error" class="status-indicator changed-indicator">changed</span>
51
+ </transition>
52
+ <div v-if="error" class="error-message">{{ error }}</div>
44
53
  </div>
45
54
 
46
55
  <div v-if="isOpen" class="dropdown__content">
@@ -62,7 +71,7 @@
62
71
  </template>
63
72
 
64
73
  <script setup lang="ts">
65
- import { ref, computed, watch, nextTick } from 'vue'
74
+ import { ref, computed, watch, nextTick, onUnmounted } from 'vue'
66
75
  import type { DropdownProps, DropdownOption } from '../types/dropdown'
67
76
 
68
77
  const props = withDefaults(defineProps<DropdownProps>(), {
@@ -81,10 +90,14 @@ const props = withDefaults(defineProps<DropdownProps>(), {
81
90
  padding: '0.5rem',
82
91
  icon: '',
83
92
  iconSize: 'normal',
93
+ required: false,
94
+ error: '',
84
95
  })
85
96
 
86
97
  const emit = defineEmits<{
87
98
  (e: 'update:modelValue', value: string | string[]): void
99
+ (e: 'changed'): void
100
+ (e: 'saved'): void
88
101
  }>()
89
102
 
90
103
  const isOpen = ref(false)
@@ -144,8 +157,10 @@ const toggleOption = (option: DropdownOption) => {
144
157
  ? currentValue.filter((id) => id !== option.id)
145
158
  : [...currentValue, option.id]
146
159
  emit('update:modelValue', newValue)
160
+ debounceAutosave(newValue)
147
161
  } else {
148
162
  emit('update:modelValue', option.id)
163
+ debounceAutosave(option.id)
149
164
  closeDropdown()
150
165
  }
151
166
  }
@@ -179,6 +194,66 @@ watch(isOpen, (newValue) => {
179
194
  document.removeEventListener('click', handleClickOutside)
180
195
  }
181
196
  })
197
+
198
+ const showSaved = ref(false)
199
+ const showChanged = ref(false)
200
+ const isChanged = ref(false)
201
+ const debounceTimer = ref<number | null>(null)
202
+ const changedTimer = ref<number | null>(null)
203
+
204
+ const handleAutosave = async (value: string | string[]) => {
205
+ if (props.autosave) {
206
+ try {
207
+ await props.autosave(value)
208
+ if (!props.error) {
209
+ emit('saved')
210
+ showSaved.value = true
211
+ showChanged.value = false
212
+ setTimeout(() => {
213
+ showSaved.value = false
214
+ }, 3000)
215
+ }
216
+ } catch (error) {
217
+ console.error('Autosave failed:', error)
218
+ }
219
+ }
220
+ }
221
+
222
+ const debounceAutosave = (value: string | string[]) => {
223
+ // Clear existing timers
224
+ if (debounceTimer.value) {
225
+ clearTimeout(debounceTimer.value)
226
+ }
227
+ if (changedTimer.value) {
228
+ clearTimeout(changedTimer.value)
229
+ }
230
+
231
+ // Show changed indicator immediately
232
+ if (!props.error) {
233
+ showChanged.value = true
234
+ }
235
+
236
+ // Trigger changed event after 500ms
237
+ changedTimer.value = window.setTimeout(() => {
238
+ emit('changed')
239
+ isChanged.value = true
240
+ }, 500)
241
+
242
+ // Trigger autosave after 1500ms
243
+ debounceTimer.value = window.setTimeout(() => {
244
+ handleAutosave(value)
245
+ }, 1500)
246
+ }
247
+
248
+ // Cleanup timers on unmount
249
+ onUnmounted(() => {
250
+ if (debounceTimer.value) {
251
+ clearTimeout(debounceTimer.value)
252
+ }
253
+ if (changedTimer.value) {
254
+ clearTimeout(changedTimer.value)
255
+ }
256
+ })
182
257
  </script>
183
258
 
184
259
  <style scoped>
@@ -197,6 +272,7 @@ watch(isOpen, (newValue) => {
197
272
  }
198
273
 
199
274
  .dropdown__selected {
275
+ position: relative;
200
276
  display: grid;
201
277
  grid-template-columns: auto 1fr auto;
202
278
  align-items: center;
@@ -397,4 +473,55 @@ watch(isOpen, (newValue) => {
397
473
  justify-content: center;
398
474
  padding: 0.75rem 0;
399
475
  }
476
+
477
+ .dropdown__selected.has-error {
478
+ border-color: var(--danger-color);
479
+ border-bottom-left-radius: 0;
480
+ border-bottom-right-radius: 0;
481
+ }
482
+
483
+ .status-indicator {
484
+ position: absolute;
485
+ top: -1px;
486
+ line-height: 1px;
487
+ right: 0.5rem;
488
+ font-size: 0.75rem;
489
+ color: var(--text-muted);
490
+ background-color: var(--dropdown-background-color);
491
+ padding: 0 0.25rem;
492
+ }
493
+
494
+ .saved-indicator {
495
+ color: var(--success-color);
496
+ }
497
+
498
+ .changed-indicator {
499
+ color: var(--warning-color);
500
+ }
501
+
502
+ .error-message {
503
+ position: absolute;
504
+ bottom: 0;
505
+ left: 0;
506
+ right: 0;
507
+ padding: 0.25rem 0.75rem;
508
+ background-color: var(--danger-color);
509
+ color: white;
510
+ font-size: 0.75rem;
511
+ border-radius: 0 0 0.5rem 0.5rem;
512
+ transform: translateY(100%);
513
+ transition: transform 0.2s ease;
514
+ line-height: var(--line-height);
515
+ z-index: 1;
516
+ }
517
+
518
+ .fade-enter-active,
519
+ .fade-leave-active {
520
+ transition: opacity 0.2s ease;
521
+ }
522
+
523
+ .fade-enter-from,
524
+ .fade-leave-to {
525
+ opacity: 0;
526
+ }
400
527
  </style>
@@ -17,7 +17,7 @@
17
17
  'has-error': error,
18
18
  'has-icon': icon,
19
19
  }">
20
- <div v-if="icon" class="icon-wrapper" :class="{ 'has-error': error }" @click="focusInput">
20
+ <div v-if="icon" class="icon-wrapper" @click="focusInput">
21
21
  <font-awesome-icon :icon="icon" class="icon" />
22
22
  </div>
23
23
  <Datepicker v-if="type === 'date'" :id="id" v-model="dateValue" :placeholder="placeholder" :disabled="disabled"
@@ -279,22 +279,23 @@ defineExpose({
279
279
  border-color: var(--danger-color);
280
280
  border-bottom-left-radius: 0;
281
281
  border-bottom-right-radius: 0;
282
+
283
+ .icon {
284
+ color: var(--danger-color);
285
+ }
282
286
  }
283
287
 
284
288
  .icon-wrapper {
285
- position: absolute;
286
- left: 0.75rem;
287
- top: 50%;
288
- transform: translateY(-50%);
289
- display: flex;
290
- align-items: center;
291
- justify-content: center;
292
- color: var(--text-color);
289
+ display: grid;
290
+ place-items: start;
291
+ padding: 1rem;
292
+ border-right: 1px solid rgb(from var(--border-color) r g b / 20%);
293
293
  cursor: pointer;
294
+ overflow: hidden;
294
295
  }
295
296
 
296
- .icon-wrapper.has-error {
297
- color: var(--error-color);
297
+ .icon-wrapper:hover {
298
+ background-color: var(--input-bg-hover);
298
299
  }
299
300
 
300
301
  .icon {
@@ -22,4 +22,7 @@ export interface DropdownProps {
22
22
  padding?: string
23
23
  icon?: string
24
24
  iconSize?: 'normal' | 'large'
25
+ required?: boolean
26
+ error?: string
27
+ autosave?: (value: string | string[]) => Promise<void>
25
28
  }
@@ -6,7 +6,7 @@
6
6
 
7
7
  <div class="dropdown-test__section">
8
8
  <h2>Single Select Dropdown</h2>
9
- <Dropdown v-model="selectedSingle" :options="options" placeholder="Select a color" filterable
9
+ <Dropdown v-model="selectedSingle" :options="options" placeholder="Select a color" filterable required
10
10
  @update:modelValue="handleSingleChange" />
11
11
  <div v-if="selectedSingle" class="selection-info">
12
12
  Selected: {{ getOptionLabel(selectedSingle) }}
@@ -25,7 +25,7 @@
25
25
  <div class="dropdown-test__section">
26
26
  <h2>Dropdown with Icon</h2>
27
27
  <Dropdown v-model="selectedWithIcon" :options="options" placeholder="Select with icon" icon="house"
28
- @update:modelValue="handleIconChange" />
28
+ @update:modelValue="handleIconChange" error="This is an error" />
29
29
  <div v-if="selectedWithIcon" class="selection-info">
30
30
  Selected: {{ getOptionLabel(selectedWithIcon) }}
31
31
  </div>