@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.
- package/package.json +2 -1
- package/src/App.vue +7 -0
- package/src/assets/colors.css +67 -0
- package/src/assets/logo.svg +1 -0
- package/src/assets/main.css +160 -0
- package/src/components/Action.vue +208 -0
- package/src/components/FileUpload.vue +310 -0
- package/src/components/Navigation.vue +634 -0
- package/src/components/TextInput.vue +503 -0
- package/src/env.d.ts +9 -0
- package/src/index.ts +8 -0
- package/src/main.ts +21 -0
- package/src/router/index.ts +44 -0
- package/src/types/index.ts +3 -0
- package/src/types/navigation.ts +32 -0
- package/src/types.d.ts +23 -0
- package/src/types.ts +108 -0
- package/src/views/ActionTestView.vue +307 -0
- package/src/views/DashboardView.vue +98 -0
- package/src/views/FileUploadTestView.vue +177 -0
- package/src/views/NavigationTestView.vue +319 -0
- package/src/views/TextInputTestView.vue +372 -0
|
@@ -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>
|