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

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.3",
4
4
  "description": "A collection of reusable Vue 3 input components with TypeScript support",
5
5
  "author": "A-Vision Software",
6
6
  "license": "MIT",
@@ -41,6 +41,14 @@
41
41
  @click.stop="clearSelection" />
42
42
  <font-awesome-icon icon="chevron-down" class="dropdown__arrow" :class="{ 'dropdown__arrow--open': isOpen }" />
43
43
  </div>
44
+ <span v-if="required && !showSaved && !showChanged" class="status-indicator required-indicator">required</span>
45
+ <transition name="fade">
46
+ <span v-if="showSaved && !error" class="status-indicator saved-indicator">saved</span>
47
+ </transition>
48
+ <transition name="fade">
49
+ <span v-if="showChanged && !error" class="status-indicator changed-indicator">changed</span>
50
+ </transition>
51
+ <div v-if="error" class="error-message">{{ error }}</div>
44
52
  </div>
45
53
 
46
54
  <div v-if="isOpen" class="dropdown__content">
@@ -62,7 +70,7 @@
62
70
  </template>
63
71
 
64
72
  <script setup lang="ts">
65
- import { ref, computed, watch, nextTick } from 'vue'
73
+ import { ref, computed, watch, nextTick, onUnmounted } from 'vue'
66
74
  import type { DropdownProps, DropdownOption } from '../types/dropdown'
67
75
 
68
76
  const props = withDefaults(defineProps<DropdownProps>(), {
@@ -81,10 +89,14 @@ const props = withDefaults(defineProps<DropdownProps>(), {
81
89
  padding: '0.5rem',
82
90
  icon: '',
83
91
  iconSize: 'normal',
92
+ required: false,
93
+ error: '',
84
94
  })
85
95
 
86
96
  const emit = defineEmits<{
87
97
  (e: 'update:modelValue', value: string | string[]): void
98
+ (e: 'changed'): void
99
+ (e: 'saved'): void
88
100
  }>()
89
101
 
90
102
  const isOpen = ref(false)
@@ -144,8 +156,10 @@ const toggleOption = (option: DropdownOption) => {
144
156
  ? currentValue.filter((id) => id !== option.id)
145
157
  : [...currentValue, option.id]
146
158
  emit('update:modelValue', newValue)
159
+ debounceAutosave(newValue)
147
160
  } else {
148
161
  emit('update:modelValue', option.id)
162
+ debounceAutosave(option.id)
149
163
  closeDropdown()
150
164
  }
151
165
  }
@@ -179,6 +193,66 @@ watch(isOpen, (newValue) => {
179
193
  document.removeEventListener('click', handleClickOutside)
180
194
  }
181
195
  })
196
+
197
+ const showSaved = ref(false)
198
+ const showChanged = ref(false)
199
+ const isChanged = ref(false)
200
+ const debounceTimer = ref<number | null>(null)
201
+ const changedTimer = ref<number | null>(null)
202
+
203
+ const handleAutosave = async (value: string | string[]) => {
204
+ if (props.autosave) {
205
+ try {
206
+ await props.autosave(value)
207
+ if (!props.error) {
208
+ emit('saved')
209
+ showSaved.value = true
210
+ showChanged.value = false
211
+ setTimeout(() => {
212
+ showSaved.value = false
213
+ }, 3000)
214
+ }
215
+ } catch (error) {
216
+ console.error('Autosave failed:', error)
217
+ }
218
+ }
219
+ }
220
+
221
+ const debounceAutosave = (value: string | string[]) => {
222
+ // Clear existing timers
223
+ if (debounceTimer.value) {
224
+ clearTimeout(debounceTimer.value)
225
+ }
226
+ if (changedTimer.value) {
227
+ clearTimeout(changedTimer.value)
228
+ }
229
+
230
+ // Show changed indicator immediately
231
+ if (!props.error) {
232
+ showChanged.value = true
233
+ }
234
+
235
+ // Trigger changed event after 500ms
236
+ changedTimer.value = window.setTimeout(() => {
237
+ emit('changed')
238
+ isChanged.value = true
239
+ }, 500)
240
+
241
+ // Trigger autosave after 1500ms
242
+ debounceTimer.value = window.setTimeout(() => {
243
+ handleAutosave(value)
244
+ }, 1500)
245
+ }
246
+
247
+ // Cleanup timers on unmount
248
+ onUnmounted(() => {
249
+ if (debounceTimer.value) {
250
+ clearTimeout(debounceTimer.value)
251
+ }
252
+ if (changedTimer.value) {
253
+ clearTimeout(changedTimer.value)
254
+ }
255
+ })
182
256
  </script>
183
257
 
184
258
  <style scoped>
@@ -197,6 +271,7 @@ watch(isOpen, (newValue) => {
197
271
  }
198
272
 
199
273
  .dropdown__selected {
274
+ position: relative;
200
275
  display: grid;
201
276
  grid-template-columns: auto 1fr auto;
202
277
  align-items: center;
@@ -397,4 +472,55 @@ watch(isOpen, (newValue) => {
397
472
  justify-content: center;
398
473
  padding: 0.75rem 0;
399
474
  }
475
+
476
+ .dropdown__selected.has-error {
477
+ border-color: var(--danger-color);
478
+ border-bottom-left-radius: 0;
479
+ border-bottom-right-radius: 0;
480
+ }
481
+
482
+ .status-indicator {
483
+ position: absolute;
484
+ top: -1px;
485
+ line-height: 1px;
486
+ right: 0.5rem;
487
+ font-size: 0.75rem;
488
+ color: var(--text-muted);
489
+ background-color: var(--dropdown-background-color);
490
+ padding: 0 0.25rem;
491
+ }
492
+
493
+ .saved-indicator {
494
+ color: var(--success-color);
495
+ }
496
+
497
+ .changed-indicator {
498
+ color: var(--warning-color);
499
+ }
500
+
501
+ .error-message {
502
+ position: absolute;
503
+ bottom: 0;
504
+ left: 0;
505
+ right: 0;
506
+ padding: 0.25rem 0.75rem;
507
+ background-color: var(--danger-color);
508
+ color: white;
509
+ font-size: 0.75rem;
510
+ border-radius: 0 0 0.5rem 0.5rem;
511
+ transform: translateY(100%);
512
+ transition: transform 0.2s ease;
513
+ line-height: var(--line-height);
514
+ z-index: 1;
515
+ }
516
+
517
+ .fade-enter-active,
518
+ .fade-leave-active {
519
+ transition: opacity 0.2s ease;
520
+ }
521
+
522
+ .fade-enter-from,
523
+ .fade-leave-to {
524
+ opacity: 0;
525
+ }
400
526
  </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) }}