@datametria/vue-components 2.4.1 → 2.4.2

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.
Files changed (35) hide show
  1. package/README.md +57 -5
  2. package/dist/index.es.js +5812 -2837
  3. package/dist/index.umd.js +670 -10
  4. package/dist/src/components/DatametriaCheckbox.vue.d.ts +2 -0
  5. package/dist/src/components/DatametriaCheckboxGroup.vue.d.ts +6 -0
  6. package/dist/src/components/DatametriaFileUpload.vue.d.ts +5 -0
  7. package/dist/src/components/DatametriaFloatingBar.vue.d.ts +1 -1
  8. package/dist/src/components/DatametriaInput.vue.d.ts +2 -22
  9. package/dist/src/components/DatametriaNavbar.vue.d.ts +1 -1
  10. package/dist/src/components/DatametriaPasswordInput.vue.d.ts +2 -0
  11. package/dist/src/components/DatametriaRadio.vue.d.ts +2 -0
  12. package/dist/src/components/DatametriaRadioGroup.vue.d.ts +6 -0
  13. package/dist/src/components/DatametriaSelect.vue.d.ts +2 -3
  14. package/dist/src/components/DatametriaSidebar.vue.d.ts +1 -1
  15. package/dist/src/components/DatametriaSwitch.vue.d.ts +8 -1
  16. package/dist/src/components/DatametriaTabs.vue.d.ts +2 -2
  17. package/dist/src/components/DatametriaTextarea.vue.d.ts +6 -0
  18. package/dist/src/composables/useAnalytics.d.ts +8 -0
  19. package/dist/src/types/analytics.d.ts +50 -0
  20. package/dist/vue-components.css +1 -1
  21. package/package.json +3 -2
  22. package/src/components/DatametriaButton.vue +196 -195
  23. package/src/components/DatametriaCheckbox.vue +289 -197
  24. package/src/components/DatametriaCheckboxGroup.vue +124 -3
  25. package/src/components/DatametriaFileUpload.vue +493 -414
  26. package/src/components/DatametriaInput.vue +342 -316
  27. package/src/components/DatametriaPasswordInput.vue +433 -446
  28. package/src/components/DatametriaRadio.vue +240 -151
  29. package/src/components/DatametriaRadioGroup.vue +124 -3
  30. package/src/components/DatametriaSelect.vue +409 -313
  31. package/src/components/DatametriaSwitch.vue +319 -146
  32. package/src/components/DatametriaTabs.vue +2 -2
  33. package/src/components/DatametriaTextarea.vue +285 -213
  34. package/src/composables/useAnalytics.ts +70 -0
  35. package/src/types/analytics.ts +59 -0
@@ -1,418 +1,497 @@
1
- <template>
2
- <div class="dm-file-upload" :class="{ 'dm-file-upload--disabled': disabled, 'dm-file-upload--loading': loading }">
3
- <div
4
- class="dm-file-upload__area"
5
- :class="{ 'dm-file-upload__area--dragover': isDragOver }"
6
- @click="triggerFileInput"
7
- @drop="handleDrop"
8
- @dragover="handleDragOver"
9
- @dragenter="handleDragEnter"
10
- @dragleave="handleDragLeave"
11
- >
12
- <input
13
- ref="fileInputRef"
14
- type="file"
15
- class="dm-file-upload__input"
16
- :accept="accept"
17
- :multiple="multiple"
18
- :disabled="disabled"
19
- @change="handleFileSelect"
20
- />
21
-
22
- <div v-if="loading" class="dm-file-upload__spinner"></div>
23
-
24
- <div v-else class="dm-file-upload__content">
25
- <div class="dm-file-upload__icon">
26
- <svg viewBox="0 0 24 24" fill="currentColor">
27
- <path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z"/>
28
- </svg>
29
- </div>
30
-
31
- <div class="dm-file-upload__text">
32
- <p class="dm-file-upload__primary-text">
33
- {{ uploadText || 'Click to upload or drag and drop' }}
34
- </p>
35
- <p class="dm-file-upload__secondary-text">
36
- {{ accept ? `Supported formats: ${accept}` : 'All file types supported' }}
37
- </p>
38
- </div>
39
- </div>
40
- </div>
41
-
42
- <div v-if="progress !== undefined" class="dm-file-upload__progress">
43
- <div class="dm-file-upload__progress-track">
44
- <div
45
- class="dm-file-upload__progress-bar"
46
- :style="{ width: `${progress}%` }"
47
- ></div>
48
- </div>
49
- <span class="dm-file-upload__progress-text">{{ progress }}%</span>
50
- </div>
51
-
52
- <div v-if="selectedFiles.length > 0" class="dm-file-upload__files">
53
- <div
54
- v-for="(file, index) in selectedFiles"
55
- :key="`${file.name}-${index}`"
56
- class="dm-file-upload__file"
57
- >
58
- <div class="dm-file-upload__file-info">
59
- <span class="dm-file-upload__file-name">{{ file.name }}</span>
60
- <span class="dm-file-upload__file-size">{{ formatFileSize(file.size) }}</span>
61
- </div>
62
- <button
63
- type="button"
64
- class="dm-file-upload__remove"
65
- @click="removeFile(index)"
66
- >
67
- ×
68
- </button>
69
- </div>
70
- </div>
71
-
72
- <div v-if="computedError" class="dm-file-upload__error">
73
- {{ computedError }}
74
- </div>
75
- </div>
76
- </template>
77
-
78
- <script setup lang="ts">
79
- import { ref, computed } from 'vue'
80
-
81
- interface Props {
82
- modelValue?: File | File[]
83
- accept?: string
84
- multiple?: boolean
85
- maxSize?: number // in bytes
86
- maxFiles?: number
87
- disabled?: boolean
88
- loading?: boolean
89
- progress?: number
90
- uploadText?: string
91
- error?: string
92
- }
93
-
94
- const props = withDefaults(defineProps<Props>(), {
95
- multiple: false,
96
- disabled: false,
97
- loading: false
98
- })
99
-
100
- const emit = defineEmits<{
101
- 'update:modelValue': [value: File | File[]]
102
- 'file-added': [file: File]
103
- 'file-removed': [file: File, index: number]
104
- }>()
105
-
106
- const fileInputRef = ref<HTMLInputElement>()
107
- const isDragOver = ref(false)
108
- const selectedFiles = ref<File[]>([])
109
- const validationError = ref('')
110
-
111
- const computedError = computed(() => props.error || validationError.value)
112
-
113
- const triggerFileInput = () => {
114
- if (!props.disabled && fileInputRef.value) {
115
- fileInputRef.value.click()
116
- }
117
- }
118
-
119
- const handleFileSelect = (event: Event) => {
120
- const target = event.target as HTMLInputElement
121
- if (target.files) {
122
- processFiles(Array.from(target.files))
123
- }
124
- }
125
-
126
- const handleDrop = (event: DragEvent) => {
127
- event.preventDefault()
128
- isDragOver.value = false
129
-
130
- if (props.disabled) return
131
-
132
- const files = event.dataTransfer?.files
133
- if (files) {
134
- processFiles(Array.from(files))
135
- }
136
- }
137
-
138
- const handleDragOver = (event: DragEvent) => {
139
- event.preventDefault()
140
- }
141
-
142
- const handleDragEnter = (event: DragEvent) => {
143
- event.preventDefault()
144
- if (!props.disabled) {
145
- isDragOver.value = true
146
- }
147
- }
148
-
149
- const handleDragLeave = (event: DragEvent) => {
150
- event.preventDefault()
151
- // Only set to false if we're leaving the drop area entirely
152
- if (!(event.currentTarget as Element)?.contains(event.relatedTarget as Node)) {
153
- isDragOver.value = false
154
- }
155
- }
156
-
157
- const processFiles = (files: File[]) => {
158
- validationError.value = ''
159
-
160
- // Validate file types
161
- if (props.accept) {
162
- const acceptedTypes = props.accept.split(',').map(type => type.trim())
163
- const invalidFiles = files.filter(file => {
164
- return !acceptedTypes.some(type => {
165
- if (type.startsWith('.')) {
166
- return file.name.toLowerCase().endsWith(type.toLowerCase())
167
- }
168
- return file.type.match(type.replace('*', '.*'))
169
- })
170
- })
171
-
172
- if (invalidFiles.length > 0) {
173
- validationError.value = `Invalid file type(s): ${invalidFiles.map(f => f.name).join(', ')}`
174
- return
175
- }
176
- }
177
-
178
- // Validate file sizes
179
- if (props.maxSize) {
180
- const oversizedFiles = files.filter(file => file.size > props.maxSize!)
181
- if (oversizedFiles.length > 0) {
182
- validationError.value = `File(s) too large: ${oversizedFiles.map(f => f.name).join(', ')}`
183
- return
184
- }
185
- }
186
-
187
- // Validate number of files
188
- if (props.multiple) {
189
- const totalFiles = selectedFiles.value.length + files.length
190
- if (props.maxFiles && totalFiles > props.maxFiles) {
191
- validationError.value = `Maximum ${props.maxFiles} files allowed`
192
- return
193
- }
194
-
195
- selectedFiles.value.push(...files)
196
- emit('update:modelValue', selectedFiles.value)
197
- } else {
198
- selectedFiles.value = [files[0]]
199
- emit('update:modelValue', files[0])
200
- }
201
-
202
- // Emit file-added events
203
- files.forEach(file => {
204
- emit('file-added', file)
205
- })
206
- }
207
-
208
- const removeFile = (index: number) => {
209
- const removedFile = selectedFiles.value[index]
210
- selectedFiles.value.splice(index, 1)
211
-
212
- if (props.multiple) {
213
- emit('update:modelValue', selectedFiles.value)
214
- } else {
215
- emit('update:modelValue', selectedFiles.value[0] || null)
216
- }
217
-
218
- emit('file-removed', removedFile, index)
219
- }
220
-
221
- const formatFileSize = (bytes: number): string => {
222
- if (bytes === 0) return '0 Bytes'
223
-
224
- const k = 1024
225
- const sizes = ['Bytes', 'KB', 'MB', 'GB']
226
- const i = Math.floor(Math.log(bytes) / Math.log(k))
227
-
228
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
229
- }
230
- </script>
231
-
232
- <style scoped>
233
- .dm-file-upload {
234
- width: 100%;
235
- }
236
-
237
- .dm-file-upload--disabled {
238
- opacity: 0.6;
239
- pointer-events: none;
240
- }
241
-
242
- .dm-file-upload--loading {
243
- opacity: 0.8;
244
- }
245
-
246
- .dm-file-upload__area {
247
- border: 2px dashed var(--dm-border-color, #d1d5db);
248
- border-radius: var(--dm-radius-lg, 0.5rem);
249
- padding: 2rem;
250
- text-align: center;
251
- cursor: pointer;
252
- transition: all 0.2s;
253
- background-color: var(--dm-bg-secondary, #f9fafb);
254
- }
255
-
256
- .dm-file-upload__area:hover {
257
- border-color: var(--dm-primary, #0072ce);
258
- background-color: var(--dm-bg-hover, #f3f4f6);
259
- }
260
-
261
- .dm-file-upload__area--dragover {
262
- border-color: var(--dm-primary, #0072ce);
263
- background-color: rgba(0, 114, 206, 0.05);
264
- }
265
-
266
- .dm-file-upload__input {
267
- display: none;
268
- }
269
-
270
- .dm-file-upload__content {
271
- display: flex;
272
- flex-direction: column;
273
- align-items: center;
274
- gap: 1rem;
275
- }
276
-
277
- .dm-file-upload__icon {
278
- width: 3rem;
279
- height: 3rem;
280
- color: var(--dm-text-secondary, #9ca3af);
281
- }
282
-
283
- .dm-file-upload__icon svg {
284
- width: 100%;
285
- height: 100%;
286
- }
287
-
288
- .dm-file-upload__text {
289
- text-align: center;
290
- }
291
-
292
- .dm-file-upload__primary-text {
293
- font-size: 1.125rem;
294
- font-weight: 500;
295
- color: var(--dm-text-primary, #374151);
296
- margin: 0 0 0.5rem 0;
297
- }
298
-
299
- .dm-file-upload__secondary-text {
300
- font-size: 0.875rem;
301
- color: var(--dm-text-secondary, #6b7280);
302
- margin: 0;
303
- }
304
-
305
- .dm-file-upload__spinner {
306
- width: 2rem;
307
- height: 2rem;
308
- border: 3px solid var(--dm-border-color, #d1d5db);
309
- border-top: 3px solid var(--dm-primary, #0072ce);
310
- border-radius: 50%;
311
- animation: spin 1s linear infinite;
312
- }
313
-
314
- @keyframes spin {
315
- 0% { transform: rotate(0deg); }
316
- 100% { transform: rotate(360deg); }
317
- }
318
-
319
- .dm-file-upload__progress {
320
- margin-top: 1rem;
321
- display: flex;
322
- align-items: center;
323
- gap: 1rem;
324
- }
325
-
326
- .dm-file-upload__progress-track {
327
- flex: 1;
328
- height: 0.5rem;
329
- background-color: var(--dm-bg-secondary, #e5e7eb);
330
- border-radius: var(--dm-radius-full, 9999px);
331
- overflow: hidden;
332
- }
333
-
334
- .dm-file-upload__progress-bar {
335
- height: 100%;
336
- background-color: var(--dm-primary, #0072ce);
337
- transition: width 0.3s ease;
338
- }
339
-
340
- .dm-file-upload__progress-text {
341
- font-size: 0.875rem;
342
- font-weight: 500;
343
- color: var(--dm-text-secondary, #4b5563);
344
- min-width: 3rem;
345
- }
346
-
347
- .dm-file-upload__files {
348
- margin-top: 1rem;
349
- space-y: 0.5rem;
350
- }
351
-
352
- .dm-file-upload__file {
353
- display: flex;
354
- align-items: center;
355
- justify-content: space-between;
356
- padding: 0.75rem;
357
- background-color: var(--dm-bg-secondary, #f9fafb);
358
- border: 1px solid var(--dm-border-color, #e5e7eb);
359
- border-radius: var(--dm-radius-md, 0.375rem);
360
- margin-bottom: 0.5rem;
361
- }
362
-
363
- .dm-file-upload__file-info {
364
- display: flex;
365
- flex-direction: column;
366
- gap: 0.25rem;
367
- }
368
-
369
- .dm-file-upload__file-name {
370
- font-weight: 500;
371
- color: var(--dm-text-primary, #374151);
372
- }
373
-
374
- .dm-file-upload__file-size {
375
- font-size: 0.875rem;
376
- color: var(--dm-text-secondary, #6b7280);
377
- }
378
-
379
- .dm-file-upload__remove {
380
- background: none;
381
- border: none;
382
- color: var(--dm-error, #ef4444);
383
- cursor: pointer;
384
- font-size: 1.25rem;
385
- padding: 0.25rem;
386
- border-radius: var(--dm-radius-sm, 0.25rem);
387
- transition: background-color 0.2s;
388
- }
389
-
390
- .dm-file-upload__remove:hover {
391
- background-color: var(--dm-error, #ef4444);
392
- color: white;
393
- }
394
-
395
- .dm-file-upload__error {
396
- margin-top: 0.5rem;
397
- color: var(--dm-error, #ef4444);
398
- font-size: 0.875rem;
399
- }
400
-
401
- /* Dark Mode Support - Hybrid Approach */
402
-
403
- /* Fallback automático (sem JS) */
404
- @media (prefers-color-scheme: dark) {
405
- .dm-file-upload {
406
- background: var(--dm-bg-color-dark, #1e1e1e);
407
- color: var(--dm-text-primary-dark, #e0e0e0);
408
- border-color: var(--dm-border-color-dark, #404040);
1
+ <template>
2
+ <div class="datametria-file-upload" :class="uploadClasses">
3
+ <label v-if="label" :for="uploadId" class="datametria-file-upload__label">
4
+ {{ label }}
5
+ <span v-if="required" class="datametria-file-upload__required">*</span>
6
+ </label>
7
+
8
+ <div
9
+ class="datametria-file-upload__area"
10
+ :class="{ 'is-dragover': isDragOver }"
11
+ role="button"
12
+ :aria-label="uploadText || 'Upload de arquivo'"
13
+ :aria-disabled="disabled"
14
+ tabindex="0"
15
+ @click="triggerFileInput"
16
+ @keydown.enter.prevent="triggerFileInput"
17
+ @keydown.space.prevent="triggerFileInput"
18
+ @drop="handleDrop"
19
+ @dragover="handleDragOver"
20
+ @dragenter="handleDragEnter"
21
+ @dragleave="handleDragLeave"
22
+ >
23
+ <input
24
+ :id="uploadId"
25
+ ref="fileInputRef"
26
+ type="file"
27
+ class="datametria-file-upload__input"
28
+ :accept="accept"
29
+ :multiple="multiple"
30
+ :disabled="disabled"
31
+ :aria-label="label || uploadText"
32
+ :aria-required="required"
33
+ :aria-invalid="!!computedError"
34
+ :aria-describedby="computedError ? `${uploadId}-error` : undefined"
35
+ @change="handleFileSelect"
36
+ />
37
+
38
+ <div v-if="loading" class="datametria-file-upload__spinner" role="status" aria-label="Carregando"></div>
39
+
40
+ <div v-else class="datametria-file-upload__content">
41
+ <div class="datametria-file-upload__icon">
42
+ <svg viewBox="0 0 24 24" fill="currentColor">
43
+ <path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z"/>
44
+ </svg>
45
+ </div>
46
+
47
+ <div class="datametria-file-upload__text">
48
+ <p class="datametria-file-upload__primary-text">
49
+ {{ uploadText || 'Clique para enviar ou arraste e solte' }}
50
+ </p>
51
+ <p class="datametria-file-upload__secondary-text">
52
+ {{ accept ? `Formatos: ${accept}` : 'Todos os formatos' }}
53
+ </p>
54
+ </div>
55
+ </div>
56
+ </div>
57
+
58
+ <div v-if="progress !== undefined" class="datametria-file-upload__progress" role="progressbar" :aria-valuenow="progress" aria-valuemin="0" aria-valuemax="100">
59
+ <div class="datametria-file-upload__progress-track">
60
+ <div class="datametria-file-upload__progress-bar" :style="{ width: `${progress}%` }"></div>
61
+ </div>
62
+ <span class="datametria-file-upload__progress-text">{{ progress }}%</span>
63
+ </div>
64
+
65
+ <div v-if="selectedFiles.length > 0" class="datametria-file-upload__files">
66
+ <div v-for="(file, index) in selectedFiles" :key="`${file.name}-${index}`" class="datametria-file-upload__file">
67
+ <div class="datametria-file-upload__file-info">
68
+ <span class="datametria-file-upload__file-name">{{ file.name }}</span>
69
+ <span class="datametria-file-upload__file-size">{{ formatFileSize(file.size) }}</span>
70
+ </div>
71
+ <button type="button" class="datametria-file-upload__remove" aria-label="Remover arquivo" @click="removeFile(index)">×</button>
72
+ </div>
73
+ </div>
74
+
75
+ <div v-if="computedError" :id="`${uploadId}-error`" role="alert" aria-live="polite" class="datametria-file-upload__error">
76
+ {{ computedError }}
77
+ </div>
78
+ </div>
79
+ </template>
80
+
81
+ <script setup lang="ts">
82
+ import { ref, computed } from 'vue'
83
+ import { useAnalytics } from '@/composables/useAnalytics'
84
+
85
+ interface Props {
86
+ modelValue?: File | File[]
87
+ accept?: string
88
+ multiple?: boolean
89
+ maxSize?: number
90
+ maxFiles?: number
91
+ disabled?: boolean
92
+ loading?: boolean
93
+ progress?: number
94
+ uploadText?: string
95
+ error?: string
96
+ label?: string
97
+ required?: boolean
98
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
99
+ }
100
+
101
+ const props = withDefaults(defineProps<Props>(), {
102
+ multiple: false,
103
+ disabled: false,
104
+ loading: false,
105
+ required: false,
106
+ size: 'md'
107
+ })
108
+
109
+ const emit = defineEmits<{
110
+ 'update:modelValue': [value: File | File[]]
111
+ 'file-added': [file: File]
112
+ 'file-removed': [file: File, index: number]
113
+ }>()
114
+
115
+ const { trackEvent } = useAnalytics()
116
+ const fileInputRef = ref<HTMLInputElement>()
117
+ const isDragOver = ref(false)
118
+ const selectedFiles = ref<File[]>([])
119
+ const validationError = ref('')
120
+ const uploadId = `datametria-file-upload-${Math.random().toString(36).substr(2, 9)}`
121
+
122
+ const uploadClasses = computed(() => ({
123
+ 'is-disabled': props.disabled,
124
+ 'is-loading': props.loading,
125
+ 'is-error': !!computedError.value,
126
+ [`datametria-file-upload--${props.size}`]: true
127
+ }))
128
+
129
+ const computedError = computed(() => props.error || validationError.value)
130
+
131
+ const triggerFileInput = () => {
132
+ if (!props.disabled && fileInputRef.value) {
133
+ fileInputRef.value.click()
134
+ }
135
+ }
136
+
137
+ const handleFileSelect = (event: Event) => {
138
+ const target = event.target as HTMLInputElement
139
+ if (target.files) {
140
+ processFiles(Array.from(target.files))
141
+ }
142
+ }
143
+
144
+ const handleDrop = (event: DragEvent) => {
145
+ event.preventDefault()
146
+ isDragOver.value = false
147
+ if (props.disabled) return
148
+ const files = event.dataTransfer?.files
149
+ if (files) {
150
+ processFiles(Array.from(files))
151
+ }
152
+ }
153
+
154
+ const handleDragOver = (event: DragEvent) => {
155
+ event.preventDefault()
156
+ }
157
+
158
+ const handleDragEnter = (event: DragEvent) => {
159
+ event.preventDefault()
160
+ if (!props.disabled) {
161
+ isDragOver.value = true
162
+ }
163
+ }
164
+
165
+ const handleDragLeave = (event: DragEvent) => {
166
+ event.preventDefault()
167
+ if (!(event.currentTarget as Element)?.contains(event.relatedTarget as Node)) {
168
+ isDragOver.value = false
169
+ }
170
+ }
171
+
172
+ const processFiles = (files: File[]) => {
173
+ validationError.value = ''
174
+
175
+ if (props.accept) {
176
+ const acceptedTypes = props.accept.split(',').map(type => type.trim())
177
+ const invalidFiles = files.filter(file => {
178
+ return !acceptedTypes.some(type => {
179
+ if (type.startsWith('.')) {
180
+ return file.name.toLowerCase().endsWith(type.toLowerCase())
181
+ }
182
+ return file.type.match(type.replace('*', '.*'))
183
+ })
184
+ })
185
+ if (invalidFiles.length > 0) {
186
+ validationError.value = `Tipo inválido: ${invalidFiles.map(f => f.name).join(', ')}`
187
+ return
188
+ }
409
189
  }
190
+
191
+ if (props.maxSize) {
192
+ const oversizedFiles = files.filter(file => file.size > props.maxSize!)
193
+ if (oversizedFiles.length > 0) {
194
+ validationError.value = `Arquivo muito grande: ${oversizedFiles.map(f => f.name).join(', ')}`
195
+ return
196
+ }
197
+ }
198
+
199
+ if (props.multiple) {
200
+ const totalFiles = selectedFiles.value.length + files.length
201
+ if (props.maxFiles && totalFiles > props.maxFiles) {
202
+ validationError.value = `Máximo ${props.maxFiles} arquivos`
203
+ return
204
+ }
205
+ selectedFiles.value.push(...files)
206
+ emit('update:modelValue', selectedFiles.value)
207
+ } else {
208
+ selectedFiles.value = [files[0]]
209
+ emit('update:modelValue', files[0])
210
+ }
211
+
212
+ files.forEach(file => {
213
+ emit('file-added', file)
214
+ trackEvent('file_upload_added', {
215
+ component: 'DatametriaFileUpload',
216
+ fileName: file.name,
217
+ fileSize: file.size,
218
+ fileType: file.type
219
+ })
220
+ })
221
+ }
222
+
223
+ const removeFile = (index: number) => {
224
+ const removedFile = selectedFiles.value[index]
225
+ selectedFiles.value.splice(index, 1)
226
+
227
+ if (props.multiple) {
228
+ emit('update:modelValue', selectedFiles.value)
229
+ } else {
230
+ emit('update:modelValue', selectedFiles.value[0] || null)
231
+ }
232
+
233
+ emit('file-removed', removedFile, index)
234
+ trackEvent('file_upload_removed', {
235
+ component: 'DatametriaFileUpload',
236
+ fileName: removedFile.name
237
+ })
238
+ }
239
+
240
+ const formatFileSize = (bytes: number): string => {
241
+ if (bytes === 0) return '0 Bytes'
242
+ const k = 1024
243
+ const sizes = ['Bytes', 'KB', 'MB', 'GB']
244
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
245
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
246
+ }
247
+ </script>
248
+
249
+ <style scoped>
250
+ .datametria-file-upload {
251
+ width: 100%;
252
+ display: flex;
253
+ flex-direction: column;
254
+ gap: 8px;
255
+ }
256
+
257
+ .datametria-file-upload__label {
258
+ font-size: 14px;
259
+ font-weight: 500;
260
+ color: var(--color-text-primary, #1f2937);
261
+ }
262
+
263
+ .datametria-file-upload__required {
264
+ color: var(--color-error, #ef4444);
265
+ }
266
+
267
+ .datametria-file-upload.is-disabled {
268
+ opacity: 0.6;
269
+ pointer-events: none;
270
+ }
271
+
272
+ .datametria-file-upload__area {
273
+ border: 2px dashed var(--color-border, #d1d5db);
274
+ border-radius: 6px;
275
+ padding: 32px;
276
+ text-align: center;
277
+ cursor: pointer;
278
+ transition: all 0.2s;
279
+ background-color: var(--color-background-alt, #f9fafb);
280
+ touch-action: manipulation;
281
+ -webkit-tap-highlight-color: transparent;
282
+ min-height: 44px;
283
+ }
284
+
285
+ .datametria-file-upload__area:hover {
286
+ border-color: var(--color-primary, #3b82f6);
287
+ background-color: var(--color-background-muted, #f3f4f6);
288
+ }
289
+
290
+ .datametria-file-upload__area:focus-visible {
291
+ outline: none;
292
+ border-color: var(--color-primary, #3b82f6);
293
+ box-shadow: 0 0 0 3px var(--color-primary-alpha, rgba(59, 130, 246, 0.1));
294
+ }
295
+
296
+ .datametria-file-upload__area.is-dragover {
297
+ border-color: var(--color-primary, #3b82f6);
298
+ background-color: var(--color-primary-alpha, rgba(59, 130, 246, 0.05));
299
+ }
300
+
301
+ .datametria-file-upload__input {
302
+ display: none;
303
+ }
304
+
305
+ .datametria-file-upload__content {
306
+ display: flex;
307
+ flex-direction: column;
308
+ align-items: center;
309
+ gap: 16px;
310
+ }
311
+
312
+ .datametria-file-upload__icon {
313
+ width: 48px;
314
+ height: 48px;
315
+ color: var(--color-text-secondary, #6b7280);
316
+ }
317
+
318
+ .datametria-file-upload__primary-text {
319
+ font-size: 18px;
320
+ font-weight: 500;
321
+ color: var(--color-text-primary, #1f2937);
322
+ margin: 0 0 8px 0;
323
+ }
324
+
325
+ .datametria-file-upload__secondary-text {
326
+ font-size: 14px;
327
+ color: var(--color-text-secondary, #6b7280);
328
+ margin: 0;
329
+ }
330
+
331
+ .datametria-file-upload__spinner {
332
+ width: 32px;
333
+ height: 32px;
334
+ border: 3px solid var(--color-border, #d1d5db);
335
+ border-top: 3px solid var(--color-primary, #3b82f6);
336
+ border-radius: 50%;
337
+ animation: spin 1s linear infinite;
338
+ }
339
+
340
+ @keyframes spin {
341
+ to { transform: rotate(360deg); }
342
+ }
343
+
344
+ .datametria-file-upload__progress {
345
+ display: flex;
346
+ align-items: center;
347
+ gap: 16px;
348
+ }
349
+
350
+ .datametria-file-upload__progress-track {
351
+ flex: 1;
352
+ height: 8px;
353
+ background-color: var(--color-background-muted, #e5e7eb);
354
+ border-radius: 9999px;
355
+ overflow: hidden;
356
+ }
357
+
358
+ .datametria-file-upload__progress-bar {
359
+ height: 100%;
360
+ background-color: var(--color-primary, #3b82f6);
361
+ transition: width 0.3s;
362
+ }
363
+
364
+ .datametria-file-upload__progress-text {
365
+ font-size: 14px;
366
+ font-weight: 500;
367
+ color: var(--color-text-secondary, #6b7280);
368
+ min-width: 48px;
369
+ }
370
+
371
+ .datametria-file-upload__files {
372
+ display: flex;
373
+ flex-direction: column;
374
+ gap: 8px;
375
+ }
376
+
377
+ .datametria-file-upload__file {
378
+ display: flex;
379
+ align-items: center;
380
+ justify-content: space-between;
381
+ padding: 12px;
382
+ background: var(--color-background-alt, #f9fafb);
383
+ border-radius: 6px;
384
+ border: 1px solid var(--color-border, #d1d5db);
385
+ }
386
+
387
+ .datametria-file-upload__file-info {
388
+ display: flex;
389
+ flex-direction: column;
390
+ gap: 4px;
391
+ flex: 1;
392
+ }
393
+
394
+ .datametria-file-upload__file-name {
395
+ font-size: 14px;
396
+ font-weight: 500;
397
+ color: var(--color-text-primary, #1f2937);
398
+ }
399
+
400
+ .datametria-file-upload__file-size {
401
+ font-size: 12px;
402
+ color: var(--color-text-secondary, #6b7280);
403
+ }
404
+
405
+ .datametria-file-upload__remove {
406
+ background: none;
407
+ border: none;
408
+ font-size: 24px;
409
+ color: var(--color-text-secondary, #6b7280);
410
+ cursor: pointer;
411
+ padding: 8px;
412
+ min-width: 44px;
413
+ min-height: 44px;
414
+ display: flex;
415
+ align-items: center;
416
+ justify-content: center;
417
+ transition: color 0.2s;
418
+ }
419
+
420
+ .datametria-file-upload__remove:hover {
421
+ color: var(--color-error, #ef4444);
422
+ }
423
+
424
+ .datametria-file-upload__error {
425
+ font-size: 12px;
426
+ color: var(--color-error, #ef4444);
427
+ }
428
+
429
+ /* Tamanhos */
430
+ .datametria-file-upload--xs .datametria-file-upload__area {
431
+ padding: 16px;
432
+ }
433
+
434
+ .datametria-file-upload--xs .datametria-file-upload__icon {
435
+ width: 32px;
436
+ height: 32px;
437
+ }
438
+
439
+ .datametria-file-upload--xs .datametria-file-upload__primary-text {
440
+ font-size: 14px;
441
+ }
442
+
443
+ .datametria-file-upload--sm .datametria-file-upload__area {
444
+ padding: 24px;
445
+ }
446
+
447
+ .datametria-file-upload--sm .datametria-file-upload__icon {
448
+ width: 40px;
449
+ height: 40px;
450
+ }
451
+
452
+ .datametria-file-upload--sm .datametria-file-upload__primary-text {
453
+ font-size: 16px;
454
+ }
455
+
456
+ .datametria-file-upload--md .datametria-file-upload__area {
457
+ padding: 32px;
410
458
  }
411
459
 
412
- /* Controle manual via useTheme() */
413
- [data-theme="dark"] .dm-file-upload {
414
- background: var(--dm-bg-color-dark, #1e1e1e);
415
- color: var(--dm-text-primary-dark, #e0e0e0);
416
- border-color: var(--dm-border-color-dark, #404040);
460
+ .datametria-file-upload--md .datametria-file-upload__icon {
461
+ width: 48px;
462
+ height: 48px;
463
+ }
464
+
465
+ .datametria-file-upload--lg .datametria-file-upload__area {
466
+ padding: 40px;
467
+ }
468
+
469
+ .datametria-file-upload--lg .datametria-file-upload__icon {
470
+ width: 56px;
471
+ height: 56px;
472
+ }
473
+
474
+ .datametria-file-upload--lg .datametria-file-upload__primary-text {
475
+ font-size: 20px;
476
+ }
477
+
478
+ .datametria-file-upload--xl .datametria-file-upload__area {
479
+ padding: 48px;
480
+ }
481
+
482
+ .datametria-file-upload--xl .datametria-file-upload__icon {
483
+ width: 64px;
484
+ height: 64px;
485
+ }
486
+
487
+ .datametria-file-upload--xl .datametria-file-upload__primary-text {
488
+ font-size: 22px;
489
+ }
490
+
491
+ @media (max-width: 640px) {
492
+ .datametria-file-upload__area {
493
+ padding: 24px;
494
+ min-height: 48px;
495
+ }
417
496
  }
418
- </style>
497
+ </style>