@datametria/vue-components 2.3.0 → 2.4.0

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 (128) hide show
  1. package/README.md +625 -594
  2. package/dist/index.es.js +1962 -1887
  3. package/dist/index.umd.js +6 -6
  4. package/dist/src/components/DatametriaForm.vue.d.ts +1 -1
  5. package/dist/src/components/DatametriaInput.vue.d.ts +9 -1
  6. package/dist/src/components/DatametriaSelect.vue.d.ts +10 -1
  7. package/dist/vue-components.css +1 -1
  8. package/package.json +103 -105
  9. package/src/components/DatametriaAlert.vue +151 -133
  10. package/src/components/DatametriaAutocomplete.vue +250 -229
  11. package/src/components/DatametriaAvatar.vue +256 -238
  12. package/src/components/DatametriaBadge.vue +101 -96
  13. package/src/components/DatametriaBreadcrumb.vue +132 -128
  14. package/src/components/DatametriaButton.vue +191 -173
  15. package/src/components/DatametriaCard.vue +84 -66
  16. package/src/components/DatametriaCheckbox.vue +197 -193
  17. package/src/components/DatametriaCheckboxGroup.vue +56 -38
  18. package/src/components/DatametriaChip.vue +159 -141
  19. package/src/components/DatametriaContainer.vue +70 -52
  20. package/src/components/DatametriaDataTable.vue +318 -300
  21. package/src/components/DatametriaDatePicker.vue +396 -378
  22. package/src/components/DatametriaDialog.vue +297 -293
  23. package/src/components/DatametriaDivider.vue +105 -98
  24. package/src/components/DatametriaDropdown.vue +356 -350
  25. package/src/components/DatametriaEmpty.vue +155 -151
  26. package/src/components/DatametriaFileUpload.vue +413 -395
  27. package/src/components/DatametriaFloatingBar.vue +144 -126
  28. package/src/components/DatametriaForm.vue +174 -156
  29. package/src/components/DatametriaFormItem.vue +183 -179
  30. package/src/components/DatametriaGrid.vue +55 -37
  31. package/src/components/DatametriaInput.vue +314 -263
  32. package/src/components/DatametriaMenu.vue +618 -600
  33. package/src/components/DatametriaModal.vue +147 -129
  34. package/src/components/DatametriaNavbar.vue +277 -223
  35. package/src/components/DatametriaPagination.vue +375 -371
  36. package/src/components/DatametriaPasswordInput.vue +444 -426
  37. package/src/components/DatametriaPopconfirm.vue +240 -234
  38. package/src/components/DatametriaProgress.vue +228 -224
  39. package/src/components/DatametriaRadio.vue +151 -147
  40. package/src/components/DatametriaRadioGroup.vue +55 -37
  41. package/src/components/DatametriaResult.vue +135 -131
  42. package/src/components/DatametriaSelect.vue +311 -211
  43. package/src/components/DatametriaSidebar.vue +294 -222
  44. package/src/components/DatametriaSkeleton.vue +257 -234
  45. package/src/components/DatametriaSlider.vue +409 -391
  46. package/src/components/DatametriaSortableTable.vue +820 -802
  47. package/src/components/DatametriaSpinner.vue +114 -110
  48. package/src/components/DatametriaSteps.vue +318 -312
  49. package/src/components/DatametriaSwitch.vue +146 -142
  50. package/src/components/DatametriaTabPane.vue +94 -76
  51. package/src/components/DatametriaTable.vue +118 -100
  52. package/src/components/DatametriaTabs.vue +315 -297
  53. package/src/components/DatametriaTextarea.vue +213 -195
  54. package/src/components/DatametriaTimePicker.vue +317 -299
  55. package/src/components/DatametriaToast.vue +176 -176
  56. package/src/components/DatametriaTooltip.vue +421 -400
  57. package/src/components/DatametriaTree.vue +126 -122
  58. package/src/components/DatametriaTreeNode.vue +176 -172
  59. package/src/components/DatametriaUpload.vue +379 -361
  60. package/src/components/__tests__/DatametriaAlert.test.js +35 -35
  61. package/src/components/__tests__/DatametriaAlert.test.ts +190 -190
  62. package/src/components/__tests__/DatametriaAvatar.test.ts +151 -151
  63. package/src/components/__tests__/DatametriaBadge.test.js +29 -29
  64. package/src/components/__tests__/DatametriaBadge.test.ts +167 -167
  65. package/src/components/__tests__/DatametriaBreadcrumb.test.ts +187 -0
  66. package/src/components/__tests__/DatametriaButton.test.js +30 -30
  67. package/src/components/__tests__/DatametriaButton.test.ts +283 -283
  68. package/src/components/__tests__/DatametriaCard.test.ts +201 -201
  69. package/src/components/__tests__/DatametriaCheckbox.test.ts +204 -0
  70. package/src/components/__tests__/DatametriaChip.test.js +38 -38
  71. package/src/components/__tests__/DatametriaContainer.test.ts +52 -52
  72. package/src/components/__tests__/DatametriaDialog.test.ts +338 -0
  73. package/src/components/__tests__/DatametriaDivider.test.ts +54 -54
  74. package/src/components/__tests__/DatametriaDropdown.test.ts +357 -0
  75. package/src/components/__tests__/DatametriaEmpty.test.ts +261 -0
  76. package/src/components/__tests__/DatametriaFileUpload.test.ts +290 -290
  77. package/src/components/__tests__/DatametriaFloatingBar.test.ts +137 -137
  78. package/src/components/__tests__/DatametriaForm.test.ts +96 -0
  79. package/src/components/__tests__/DatametriaFormItem.test.ts +58 -0
  80. package/src/components/__tests__/DatametriaGrid.test.ts +31 -31
  81. package/src/components/__tests__/DatametriaInput.test.ts +72 -72
  82. package/src/components/__tests__/DatametriaMenu.test.ts +366 -366
  83. package/src/components/__tests__/DatametriaModal.test.ts +86 -86
  84. package/src/components/__tests__/DatametriaNavbar.test.js +48 -48
  85. package/src/components/__tests__/DatametriaNavbar.test.ts +203 -203
  86. package/src/components/__tests__/DatametriaPasswordInput.test.js +305 -305
  87. package/src/components/__tests__/DatametriaRadio.test.ts +195 -0
  88. package/src/components/__tests__/DatametriaSelect.test.ts +77 -77
  89. package/src/components/__tests__/DatametriaSidebar.test.ts +169 -169
  90. package/src/components/__tests__/DatametriaSlider.test.ts +261 -261
  91. package/src/components/__tests__/DatametriaSortableTable.test.js +168 -168
  92. package/src/components/__tests__/DatametriaSpinner.test.ts +156 -156
  93. package/src/components/__tests__/DatametriaSteps.test.ts +211 -0
  94. package/src/components/__tests__/DatametriaSwitch.test.ts +129 -0
  95. package/src/components/__tests__/DatametriaTabPane.test.ts +205 -0
  96. package/src/components/__tests__/DatametriaTable.test.ts +97 -97
  97. package/src/components/__tests__/DatametriaTabs.test.ts +232 -232
  98. package/src/components/__tests__/DatametriaToast.test.js +48 -48
  99. package/src/components/__tests__/DatametriaToast.test.ts +99 -99
  100. package/src/components/__tests__/DatametriaTree.test.ts +376 -0
  101. package/src/components/__tests__/index.test.ts +48 -0
  102. package/src/composables/useAccessibilityScale.ts +94 -94
  103. package/src/composables/useBreakpoints.ts +82 -82
  104. package/src/composables/useHapticFeedback.ts +439 -439
  105. package/src/composables/useRipple.ts +218 -218
  106. package/src/composables/useTheme.ts +5 -1
  107. package/src/index.ts +84 -84
  108. package/src/stories/Variants.stories.js +95 -95
  109. package/src/styles/design-tokens.css +623 -623
  110. package/src/theme/ThemeProvider.vue +96 -96
  111. package/src/theme/__tests__/ThemeProvider.test.ts +208 -208
  112. package/src/theme/__tests__/constants.test.ts +31 -31
  113. package/src/theme/__tests__/presets.test.ts +166 -166
  114. package/src/theme/__tests__/tokens.test.ts +155 -155
  115. package/src/theme/__tests__/types.test.ts +153 -153
  116. package/src/theme/__tests__/useTheme.test.ts +146 -146
  117. package/src/theme/constants.ts +14 -14
  118. package/src/theme/index.ts +12 -12
  119. package/src/theme/presets/datametria.ts +94 -94
  120. package/src/theme/presets/default.ts +94 -94
  121. package/src/theme/presets/index.ts +8 -8
  122. package/src/theme/tokens/colors.ts +28 -28
  123. package/src/theme/tokens/index.ts +47 -47
  124. package/src/theme/tokens/spacing.ts +21 -21
  125. package/src/theme/tokens/typography.ts +35 -35
  126. package/src/theme/types.ts +111 -111
  127. package/src/theme/useTheme.ts +28 -28
  128. package/src/types/index.ts +55 -55
@@ -1,400 +1,418 @@
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
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-neutral-300, #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-neutral-50, #f9fafb);
254
+ }
255
+
256
+ .dm-file-upload__area:hover {
257
+ border-color: var(--dm-primary, #0072ce);
258
+ background-color: var(--dm-neutral-100, #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-neutral-400, #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-neutral-700, #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-neutral-500, #6b7280);
302
+ margin: 0;
303
+ }
304
+
305
+ .dm-file-upload__spinner {
306
+ width: 2rem;
307
+ height: 2rem;
308
+ border: 3px solid var(--dm-neutral-300, #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-neutral-200, #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-neutral-600, #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-neutral-50, #f9fafb);
358
+ border: 1px solid var(--dm-neutral-200, #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-neutral-700, #374151);
372
+ }
373
+
374
+ .dm-file-upload__file-size {
375
+ font-size: 0.875rem;
376
+ color: var(--dm-neutral-500, #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);
146
409
  }
147
410
  }
148
411
 
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-neutral-300, #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-neutral-50, #f9fafb);
254
- }
255
-
256
- .dm-file-upload__area:hover {
257
- border-color: var(--dm-primary, #0072ce);
258
- background-color: var(--dm-neutral-100, #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-neutral-400, #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-neutral-700, #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-neutral-500, #6b7280);
302
- margin: 0;
303
- }
304
-
305
- .dm-file-upload__spinner {
306
- width: 2rem;
307
- height: 2rem;
308
- border: 3px solid var(--dm-neutral-300, #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-neutral-200, #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-neutral-600, #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-neutral-50, #f9fafb);
358
- border: 1px solid var(--dm-neutral-200, #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-neutral-700, #374151);
372
- }
373
-
374
- .dm-file-upload__file-size {
375
- font-size: 0.875rem;
376
- color: var(--dm-neutral-500, #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;
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);
399
417
  }
400
418
  </style>