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

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.2.4",
3
+ "version": "1.2.5",
4
4
  "description": "A collection of reusable Vue 3 input components with TypeScript support",
5
5
  "author": "A-Vision Software",
6
6
  "license": "MIT",
@@ -22,6 +22,7 @@
22
22
  ],
23
23
  "type": "module",
24
24
  "files": [
25
+ "src/components",
25
26
  "dist"
26
27
  ],
27
28
  "main": "./dist/vue-input-components.cjs.js",
@@ -0,0 +1,208 @@
1
+ <template>
2
+ <component
3
+ :is="isLink ? 'a' : 'button'"
4
+ :href="isLink ? href : undefined"
5
+ :type="isLink ? undefined : type"
6
+ :class="[
7
+ 'action',
8
+ {
9
+ 'action--icon-only': icon && !label,
10
+ 'action--label-only': !icon && label,
11
+ 'action--icon-label': icon && label,
12
+ 'action--disabled': disabled,
13
+ 'action--link': isLink,
14
+ 'action--small': size === 'small',
15
+ 'action--large': size === 'large',
16
+ 'action--transparent': variant === 'transparent',
17
+ },
18
+ ]"
19
+ :style="buttonStyle"
20
+ :disabled="disabled"
21
+ @click="handleClick"
22
+ >
23
+ <font-awesome-icon v-if="icon" :icon="icon" class="action__icon" />
24
+ <span v-if="label" class="action__label">{{ label }}</span>
25
+ </component>
26
+ </template>
27
+
28
+ <script setup lang="ts">
29
+ import { computed } from 'vue'
30
+
31
+ interface ActionProps {
32
+ icon?: string
33
+ label?: string
34
+ href?: string
35
+ type?: 'button' | 'submit' | 'reset'
36
+ disabled?: boolean
37
+ color?: string
38
+ size?: 'small' | 'regular' | 'large'
39
+ variant?: 'solid' | 'transparent'
40
+ }
41
+
42
+ const props = withDefaults(defineProps<ActionProps>(), {
43
+ icon: undefined,
44
+ label: undefined,
45
+ href: undefined,
46
+ type: 'button',
47
+ disabled: false,
48
+ color: 'var(--primary)',
49
+ size: 'regular',
50
+ variant: 'solid',
51
+ })
52
+
53
+ const emit = defineEmits<{
54
+ (e: 'click', event: MouseEvent): void
55
+ }>()
56
+
57
+ const isLink = computed(() => !!props.href)
58
+
59
+ const buttonStyle = computed(() => {
60
+ if (!props.color) return {}
61
+
62
+ if (props.variant === 'transparent') {
63
+ return {
64
+ backgroundColor: 'rgba(255, 255, 255, 0.1)',
65
+ color: props.color,
66
+ borderColor: props.color,
67
+ }
68
+ }
69
+
70
+ return props.href
71
+ ? {
72
+ color: props.color,
73
+ borderColor: 'transparent',
74
+ backgroundColor: 'transparent',
75
+ }
76
+ : {
77
+ color: 'rgba(255, 255, 255, 0.8)',
78
+ borderColor: props.color,
79
+ backgroundColor: props.color,
80
+ }
81
+ })
82
+
83
+ const handleClick = (event: MouseEvent) => {
84
+ if (!props.disabled) {
85
+ emit('click', event)
86
+ }
87
+ }
88
+ </script>
89
+
90
+ <style scoped>
91
+ .action {
92
+ display: inline-flex;
93
+ align-items: center;
94
+ justify-content: center;
95
+ gap: 0.5rem;
96
+ padding: 0.5rem 1rem;
97
+ border: 1px solid;
98
+ border-radius: 0.375rem;
99
+ font-size: 1rem;
100
+ font-weight: 500;
101
+ text-decoration: none;
102
+ cursor: pointer;
103
+ transition: all 0.2s ease-in-out;
104
+ }
105
+
106
+ /* Size variants */
107
+ .action--small {
108
+ padding: 0.25rem 0.5rem;
109
+ font-size: 0.8rem;
110
+ border-radius: 0.25rem;
111
+ gap: 0.25rem;
112
+ min-height: 1.5rem;
113
+ }
114
+
115
+ .action--small.action--icon-only {
116
+ padding: 0.375rem;
117
+ min-width: 1.5rem;
118
+ }
119
+
120
+ .action--small .action__icon {
121
+ font-size: 0.9rem;
122
+ width: 0.9rem;
123
+ height: 0.9rem;
124
+ }
125
+
126
+ .action--large {
127
+ padding: 0.75rem 1.5rem;
128
+ font-size: 1.25rem;
129
+ border-radius: 0.5rem;
130
+ gap: 0.625rem;
131
+ min-height: 2.5rem;
132
+ }
133
+
134
+ .action--large.action--icon-only {
135
+ padding: 1rem;
136
+ min-width: 2.5rem;
137
+ }
138
+
139
+ .action--large .action__icon {
140
+ font-size: 1.35rem;
141
+ width: 1.35rem;
142
+ height: 1.35rem;
143
+ }
144
+
145
+ .action:hover:not(.action--disabled) {
146
+ opacity: 0.9;
147
+ transform: translateY(-1px);
148
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
149
+ }
150
+
151
+ .action:active:not(.action--disabled) {
152
+ transform: translateY(0);
153
+ box-shadow: none;
154
+ }
155
+
156
+ .action--icon-only {
157
+ padding: 0.75rem;
158
+ border-radius: 50%;
159
+ aspect-ratio: 1;
160
+ }
161
+
162
+ .action--label-only {
163
+ padding: 0.5rem 1rem;
164
+ }
165
+
166
+ .action--icon-label {
167
+ padding: 0.5rem 1rem;
168
+ }
169
+
170
+ .action--disabled {
171
+ opacity: 0.6;
172
+ cursor: not-allowed;
173
+ }
174
+
175
+ .action--link {
176
+ background-color: transparent !important;
177
+ border: none !important;
178
+ box-shadow: none !important;
179
+ transform: none !important;
180
+ }
181
+
182
+ .action--link:hover:not(.action--disabled) {
183
+ opacity: 0.8;
184
+ transform: none;
185
+ box-shadow: none;
186
+ }
187
+
188
+ .action--link .action__label {
189
+ text-decoration: underline;
190
+ }
191
+
192
+ .action--transparent:hover:not(.action--disabled) {
193
+ background-color: rgba(255, 255, 255, 0.15);
194
+ }
195
+
196
+ .action__icon {
197
+ font-size: 1.25rem;
198
+ width: 1.25rem;
199
+ height: 1.25rem;
200
+ display: flex;
201
+ align-items: center;
202
+ justify-content: center;
203
+ }
204
+
205
+ .action__label {
206
+ white-space: nowrap;
207
+ }
208
+ </style>
@@ -0,0 +1,310 @@
1
+ <template>
2
+ <div class="file-upload">
3
+ <div
4
+ class="upload-area"
5
+ :class="{ 'is-dragging': isDragging, 'has-files': files.length > 0 }"
6
+ @dragenter.prevent="handleDragEnter"
7
+ @dragleave.prevent="handleDragLeave"
8
+ @dragover.prevent
9
+ @drop.prevent="handleDrop"
10
+ @click="triggerFileInput"
11
+ >
12
+ <input ref="fileInput" type="file" multiple class="file-input" @change="handleFileSelect" />
13
+ <div class="upload-content">
14
+ <font-awesome-icon :icon="['fas', icon || 'upload']" />
15
+ <p v-if="files.length === 0">Drag & drop files here or click to select</p>
16
+ <div v-else class="selected-files">
17
+ <p>{{ files.length }} file(s) selected</p>
18
+ <div v-for="(file, index) in files" :key="index" class="file-info">
19
+ <span class="file-name">{{ file.name }}</span>
20
+ <span class="file-size">{{ formatFileSize(file.size) }}</span>
21
+ </div>
22
+ </div>
23
+ </div>
24
+ </div>
25
+ <div v-if="error" class="error-message">{{ error }}</div>
26
+ <div v-if="uploadProgress > 0 && uploadProgress < 100" class="progress-bar">
27
+ <div class="progress" :style="{ width: `${uploadProgress}%` }"></div>
28
+ </div>
29
+ <div v-if="uploadStatus" class="status-message" :class="uploadStatus.type">
30
+ {{ uploadStatus.message }}
31
+ </div>
32
+ <button v-if="files.length > 0 && !uploadUrl" class="upload-button" @click="handleStartUpload">
33
+ Upload Files
34
+ </button>
35
+ </div>
36
+ </template>
37
+
38
+ <script setup lang="ts">
39
+ import { ref, watch } from 'vue'
40
+
41
+ const props = defineProps<{
42
+ icon?: string
43
+ uploadUrl?: string
44
+ }>()
45
+
46
+ const emit = defineEmits<{
47
+ (e: 'upload-complete', files: File[]): void
48
+ (e: 'upload-error', error: string): void
49
+ (e: 'files-selected', files: File[]): void
50
+ (e: 'start-upload', files: File[]): void
51
+ }>()
52
+
53
+ const MAX_FILE_SIZE = 20 * 1024 * 1024 // 20MB in bytes
54
+ const fileInput = ref<HTMLInputElement | null>(null)
55
+ const files = ref<File[]>([])
56
+ const isDragging = ref(false)
57
+ const uploadProgress = ref(0)
58
+ const error = ref('')
59
+ const uploadStatus = ref<{ type: 'success' | 'error'; message: string } | null>(null)
60
+
61
+ const formatFileSize = (bytes: number): string => {
62
+ if (bytes === 0) return '0 Bytes'
63
+ const k = 1024
64
+ const sizes = ['Bytes', 'KB', 'MB', 'GB']
65
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
66
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
67
+ }
68
+
69
+ const validateFileSize = (file: File): boolean => {
70
+ if (file.size > MAX_FILE_SIZE) {
71
+ error.value = `File "${file.name}" exceeds the maximum size of 20MB`
72
+ return false
73
+ }
74
+ return true
75
+ }
76
+
77
+ const handleDragEnter = () => {
78
+ isDragging.value = true
79
+ }
80
+
81
+ const handleDragLeave = () => {
82
+ isDragging.value = false
83
+ }
84
+
85
+ const handleDrop = (e: DragEvent) => {
86
+ isDragging.value = false
87
+ error.value = ''
88
+ if (e.dataTransfer?.files) {
89
+ const newFiles = Array.from(e.dataTransfer.files)
90
+ if (newFiles.every(validateFileSize)) {
91
+ files.value = [...files.value, ...newFiles]
92
+ }
93
+ }
94
+ }
95
+
96
+ const triggerFileInput = () => {
97
+ fileInput.value?.click()
98
+ }
99
+
100
+ const handleFileSelect = (e: Event) => {
101
+ error.value = ''
102
+ const input = e.target as HTMLInputElement
103
+ if (input.files) {
104
+ const newFiles = Array.from(input.files)
105
+ if (newFiles.every(validateFileSize)) {
106
+ files.value = [...files.value, ...newFiles]
107
+ }
108
+ }
109
+ input.value = ''
110
+ }
111
+
112
+ const uploadFiles = async () => {
113
+ if (!props.uploadUrl) {
114
+ error.value = 'No upload URL provided'
115
+ return
116
+ }
117
+
118
+ if (files.value.length === 0) {
119
+ error.value = 'No files selected'
120
+ return
121
+ }
122
+
123
+ const formData = new FormData()
124
+ files.value.forEach((file) => {
125
+ formData.append('files', file)
126
+ })
127
+
128
+ try {
129
+ const xhr = new XMLHttpRequest()
130
+ xhr.upload.addEventListener('progress', (e) => {
131
+ if (e.lengthComputable) {
132
+ uploadProgress.value = (e.loaded / e.total) * 100
133
+ }
134
+ })
135
+
136
+ xhr.addEventListener('load', () => {
137
+ if (xhr.status >= 200 && xhr.status < 300) {
138
+ uploadStatus.value = {
139
+ type: 'success',
140
+ message: 'Upload completed successfully',
141
+ }
142
+ emit('upload-complete', files.value)
143
+ files.value = []
144
+ uploadProgress.value = 0
145
+ } else {
146
+ throw new Error(`Upload failed with status ${xhr.status}`)
147
+ }
148
+ })
149
+
150
+ xhr.addEventListener('error', () => {
151
+ throw new Error('Upload failed')
152
+ })
153
+
154
+ xhr.open('POST', props.uploadUrl)
155
+ xhr.send(formData)
156
+ } catch (err) {
157
+ const errorMessage = err instanceof Error ? err.message : 'Upload failed'
158
+ error.value = errorMessage
159
+ uploadStatus.value = {
160
+ type: 'error',
161
+ message: errorMessage,
162
+ }
163
+ emit('upload-error', errorMessage)
164
+ }
165
+ }
166
+
167
+ const handleStartUpload = () => {
168
+ emit('start-upload', files.value)
169
+ }
170
+
171
+ // Watch for changes in files and automatically upload when files are selected
172
+ watch(files, (newFiles) => {
173
+ if (newFiles.length > 0) {
174
+ if (props.uploadUrl) {
175
+ uploadFiles()
176
+ } else {
177
+ emit('files-selected', newFiles)
178
+ }
179
+ }
180
+ })
181
+ </script>
182
+
183
+ <style scoped>
184
+ .file-upload {
185
+ width: 100%;
186
+ max-width: 600px;
187
+ margin: 0 auto;
188
+ }
189
+
190
+ .upload-area {
191
+ border: 2px dashed var(--upload-border-color);
192
+ border-radius: 0.75rem;
193
+ padding: 2rem;
194
+ text-align: center;
195
+ cursor: pointer;
196
+ transition: all 0.3s ease;
197
+ background-color: var(--upload-bg-color);
198
+ }
199
+
200
+ .upload-area.is-dragging {
201
+ border-color: var(--upload-dragging-border-color);
202
+ background-color: var(--upload-dragging-bg-color);
203
+ }
204
+
205
+ .upload-area.has-files {
206
+ border-color: var(--upload-has-files-border-color);
207
+ background-color: var(--upload-has-files-bg-color);
208
+ }
209
+
210
+ .file-input {
211
+ display: none;
212
+ }
213
+
214
+ .upload-content {
215
+ display: flex;
216
+ flex-direction: column;
217
+ align-items: center;
218
+ gap: 1rem;
219
+ }
220
+
221
+ .upload-content :deep(svg) {
222
+ font-size: 2rem;
223
+ color: var(--upload-icon-color);
224
+ }
225
+
226
+ .selected-files {
227
+ width: 100%;
228
+ text-align: left;
229
+ max-height: 200px;
230
+ overflow-y: auto;
231
+ font-size: 0.75rem;
232
+ color: var(--upload-text-color);
233
+ }
234
+
235
+ .file-info {
236
+ display: flex;
237
+ justify-content: space-between;
238
+ align-items: center;
239
+ padding: 0.125rem 0;
240
+ border-radius: 0.25rem;
241
+ gap: 0.5rem;
242
+ font-size: 0.75rem;
243
+ }
244
+
245
+ .file-name {
246
+ flex: 1;
247
+ overflow: hidden;
248
+ text-overflow: ellipsis;
249
+ white-space: nowrap;
250
+ margin-right: 0.5rem;
251
+ }
252
+
253
+ .file-size {
254
+ font-size: 0.7rem;
255
+ flex-shrink: 0;
256
+ }
257
+
258
+ .error-message {
259
+ color: var(--error-text-color);
260
+ margin-top: 1rem;
261
+ font-size: 0.875rem;
262
+ }
263
+
264
+ .progress-bar {
265
+ height: 0.5rem;
266
+ background-color: var(--progress-bg-color);
267
+ border-radius: 0.25rem;
268
+ margin-top: 1rem;
269
+ overflow: hidden;
270
+ }
271
+
272
+ .progress {
273
+ height: 100%;
274
+ background-color: var(--progress-color);
275
+ transition: width 0.3s ease;
276
+ }
277
+
278
+ .status-message {
279
+ margin-top: 1rem;
280
+ padding: 0.5rem;
281
+ border-radius: 0.25rem;
282
+ font-size: 0.875rem;
283
+ }
284
+
285
+ .status-message.success {
286
+ background-color: var(--success-bg-color);
287
+ color: var(--success-text-color);
288
+ }
289
+
290
+ .status-message.error {
291
+ background-color: var(--error-bg-color);
292
+ color: var(--error-text-color);
293
+ }
294
+
295
+ .upload-button {
296
+ margin-top: 1rem;
297
+ padding: 0.5rem 1rem;
298
+ background-color: var(--primary-color);
299
+ color: white;
300
+ border: none;
301
+ border-radius: 0.25rem;
302
+ cursor: pointer;
303
+ font-size: 0.875rem;
304
+ transition: background-color 0.3s ease;
305
+ }
306
+
307
+ .upload-button:hover {
308
+ background-color: var(--primary-color-light);
309
+ }
310
+ </style>