@sabrenski/spire-ui 0.0.1

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 (237) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +233 -0
  3. package/dist/index.d.ts +4981 -0
  4. package/dist/spire-ui.css +1 -0
  5. package/dist/spire-ui.es.js +18403 -0
  6. package/dist/spire-ui.umd.js +45 -0
  7. package/package.json +83 -0
  8. package/src/components/Accordion/Accordion.test.ts +218 -0
  9. package/src/components/Accordion/AccordionContent.vue +112 -0
  10. package/src/components/Accordion/AccordionItem.vue +87 -0
  11. package/src/components/Accordion/AccordionRoot.vue +111 -0
  12. package/src/components/Accordion/AccordionTrigger.vue +125 -0
  13. package/src/components/Accordion/index.ts +11 -0
  14. package/src/components/Accordion/keys.ts +23 -0
  15. package/src/components/Avatar/Avatar.test.ts +181 -0
  16. package/src/components/Avatar/Avatar.vue +150 -0
  17. package/src/components/Avatar/index.ts +2 -0
  18. package/src/components/Badge/Badge.test.ts +141 -0
  19. package/src/components/Badge/Badge.vue +133 -0
  20. package/src/components/Badge/index.ts +2 -0
  21. package/src/components/BadgeContainer/BadgeContainer.test.ts +150 -0
  22. package/src/components/BadgeContainer/BadgeContainer.vue +90 -0
  23. package/src/components/BadgeContainer/index.ts +2 -0
  24. package/src/components/Breadcrumb/Breadcrumb.test.ts +342 -0
  25. package/src/components/Breadcrumb/BreadcrumbEllipsis.vue +96 -0
  26. package/src/components/Breadcrumb/BreadcrumbItem.vue +16 -0
  27. package/src/components/Breadcrumb/BreadcrumbLink.vue +67 -0
  28. package/src/components/Breadcrumb/BreadcrumbList.vue +20 -0
  29. package/src/components/Breadcrumb/BreadcrumbPage.vue +25 -0
  30. package/src/components/Breadcrumb/BreadcrumbRoot.vue +41 -0
  31. package/src/components/Breadcrumb/BreadcrumbSeparator.vue +63 -0
  32. package/src/components/Breadcrumb/index.ts +13 -0
  33. package/src/components/Breadcrumb/keys.ts +7 -0
  34. package/src/components/Button/Button.test.ts +231 -0
  35. package/src/components/Button/Button.vue +349 -0
  36. package/src/components/Button/index.ts +2 -0
  37. package/src/components/Callout/Callout.test.ts +260 -0
  38. package/src/components/Callout/Callout.vue +341 -0
  39. package/src/components/Callout/index.ts +2 -0
  40. package/src/components/Card/Card.test.ts +565 -0
  41. package/src/components/Card/Card.vue +209 -0
  42. package/src/components/Card/CardContent.vue +57 -0
  43. package/src/components/Card/CardFooter.vue +72 -0
  44. package/src/components/Card/CardHeader.vue +111 -0
  45. package/src/components/Card/CardImage.vue +124 -0
  46. package/src/components/Card/index.ts +14 -0
  47. package/src/components/Chart/BarChart.vue +208 -0
  48. package/src/components/Chart/BaseChart.vue +444 -0
  49. package/src/components/Chart/Chart.test.ts +359 -0
  50. package/src/components/Chart/DonutChart.vue +283 -0
  51. package/src/components/Chart/LineChart.vue +211 -0
  52. package/src/components/Chart/index.ts +20 -0
  53. package/src/components/Chart/useChartTheme.ts +192 -0
  54. package/src/components/Checkbox/Checkbox.test.ts +209 -0
  55. package/src/components/Checkbox/Checkbox.vue +285 -0
  56. package/src/components/Checkbox/index.ts +2 -0
  57. package/src/components/ChoiceChip/ChoiceChip.test.ts +142 -0
  58. package/src/components/ChoiceChip/ChoiceChip.vue +218 -0
  59. package/src/components/ChoiceChip/index.ts +2 -0
  60. package/src/components/ChoiceChipGroup/ChoiceChipGroup.test.ts +151 -0
  61. package/src/components/ChoiceChipGroup/ChoiceChipGroup.vue +70 -0
  62. package/src/components/ChoiceChipGroup/index.ts +2 -0
  63. package/src/components/ColorPicker/ColorArea.vue +159 -0
  64. package/src/components/ColorPicker/ColorPicker.test.ts +250 -0
  65. package/src/components/ColorPicker/ColorPicker.vue +339 -0
  66. package/src/components/ColorPicker/ColorSlider.vue +191 -0
  67. package/src/components/ColorPicker/index.ts +7 -0
  68. package/src/components/Combobox/Combobox.test.ts +891 -0
  69. package/src/components/Combobox/Combobox.vue +934 -0
  70. package/src/components/Combobox/index.ts +2 -0
  71. package/src/components/DataTable/DataTable.test.ts +1221 -0
  72. package/src/components/DataTable/DataTable.vue +1415 -0
  73. package/src/components/DataTable/index.ts +10 -0
  74. package/src/components/DatePicker/DatePicker.test.ts +625 -0
  75. package/src/components/DatePicker/DatePicker.vue +1586 -0
  76. package/src/components/DatePicker/index.ts +2 -0
  77. package/src/components/Drawer/Drawer.test.ts +336 -0
  78. package/src/components/Drawer/Drawer.vue +466 -0
  79. package/src/components/Drawer/index.ts +2 -0
  80. package/src/components/Dropdown/Dropdown.test.ts +607 -0
  81. package/src/components/Dropdown/Dropdown.vue +807 -0
  82. package/src/components/Dropdown/DropdownItem.vue +227 -0
  83. package/src/components/Dropdown/DropdownSeparator.vue +14 -0
  84. package/src/components/Dropdown/DropdownSub.vue +104 -0
  85. package/src/components/Dropdown/DropdownSubContent.vue +187 -0
  86. package/src/components/Dropdown/DropdownSubTrigger.vue +151 -0
  87. package/src/components/Dropdown/index.ts +14 -0
  88. package/src/components/EmptyState/EmptyState.test.ts +180 -0
  89. package/src/components/EmptyState/EmptyState.vue +137 -0
  90. package/src/components/EmptyState/index.ts +2 -0
  91. package/src/components/FileUpload/FileUpload.test.ts +1151 -0
  92. package/src/components/FileUpload/FileUpload.vue +1042 -0
  93. package/src/components/FileUpload/index.ts +2 -0
  94. package/src/components/Heading/Heading.test.ts +107 -0
  95. package/src/components/Heading/Heading.vue +67 -0
  96. package/src/components/Heading/index.ts +2 -0
  97. package/src/components/Icon/Icon.test.ts +157 -0
  98. package/src/components/Icon/Icon.vue +86 -0
  99. package/src/components/Icon/index.ts +2 -0
  100. package/src/components/Input/Input.test.ts +273 -0
  101. package/src/components/Input/Input.vue +388 -0
  102. package/src/components/Input/index.ts +2 -0
  103. package/src/components/Layout/Container.vue +67 -0
  104. package/src/components/Layout/Grid.vue +159 -0
  105. package/src/components/Layout/GridItem.vue +154 -0
  106. package/src/components/Layout/Layout.test.ts +202 -0
  107. package/src/components/Layout/Stack.vue +128 -0
  108. package/src/components/Layout/index.ts +9 -0
  109. package/src/components/Layout/keys.ts +7 -0
  110. package/src/components/Modal/Modal.test.ts +311 -0
  111. package/src/components/Modal/Modal.vue +336 -0
  112. package/src/components/Modal/index.ts +2 -0
  113. package/src/components/Pagination/Pagination.test.ts +303 -0
  114. package/src/components/Pagination/Pagination.vue +212 -0
  115. package/src/components/Pagination/index.ts +3 -0
  116. package/src/components/Pagination/utils.ts +86 -0
  117. package/src/components/Popover/Popover.test.ts +285 -0
  118. package/src/components/Popover/Popover.vue +441 -0
  119. package/src/components/Popover/index.ts +2 -0
  120. package/src/components/Progress/Progress.test.ts +361 -0
  121. package/src/components/Progress/Progress.vue +363 -0
  122. package/src/components/Progress/index.ts +7 -0
  123. package/src/components/Radio/Radio.test.ts +216 -0
  124. package/src/components/Radio/Radio.vue +214 -0
  125. package/src/components/Radio/index.ts +2 -0
  126. package/src/components/Rating/Rating.test.ts +319 -0
  127. package/src/components/Rating/Rating.vue +247 -0
  128. package/src/components/Rating/index.ts +2 -0
  129. package/src/components/SegmentedControl/SegmentedControl.test.ts +292 -0
  130. package/src/components/SegmentedControl/SegmentedControl.vue +288 -0
  131. package/src/components/SegmentedControl/index.ts +2 -0
  132. package/src/components/Select/Select.test.ts +589 -0
  133. package/src/components/Select/Select.vue +666 -0
  134. package/src/components/Select/index.ts +2 -0
  135. package/src/components/Sidebar/Sidebar.test.ts +301 -0
  136. package/src/components/Sidebar/SidebarGroup.vue +103 -0
  137. package/src/components/Sidebar/SidebarItem.vue +196 -0
  138. package/src/components/Sidebar/SidebarLayout.vue +42 -0
  139. package/src/components/Sidebar/SidebarRoot.vue +122 -0
  140. package/src/components/Sidebar/index.ts +11 -0
  141. package/src/components/Sidebar/keys.ts +14 -0
  142. package/src/components/Skeleton/Skeleton.test.ts +130 -0
  143. package/src/components/Skeleton/Skeleton.vue +104 -0
  144. package/src/components/Skeleton/index.ts +2 -0
  145. package/src/components/Slider/Slider.test.ts +416 -0
  146. package/src/components/Slider/Slider.vue +435 -0
  147. package/src/components/Slider/index.ts +2 -0
  148. package/src/components/Slider/utils.ts +91 -0
  149. package/src/components/Spinner/Spinner.test.ts +79 -0
  150. package/src/components/Spinner/Spinner.vue +159 -0
  151. package/src/components/Spinner/index.ts +2 -0
  152. package/src/components/SpireProvider/SpireProvider.vue +71 -0
  153. package/src/components/SpireProvider/index.ts +11 -0
  154. package/src/components/Stepper/Stepper.test.ts +221 -0
  155. package/src/components/Stepper/StepperContent.vue +51 -0
  156. package/src/components/Stepper/StepperItem.vue +89 -0
  157. package/src/components/Stepper/StepperRoot.vue +101 -0
  158. package/src/components/Stepper/StepperSeparator.vue +52 -0
  159. package/src/components/Stepper/StepperTrigger.vue +144 -0
  160. package/src/components/Stepper/index.ts +11 -0
  161. package/src/components/Stepper/keys.ts +27 -0
  162. package/src/components/Switch/Switch.test.ts +214 -0
  163. package/src/components/Switch/Switch.vue +235 -0
  164. package/src/components/Switch/index.ts +2 -0
  165. package/src/components/Tabs/Tabs.test.ts +363 -0
  166. package/src/components/Tabs/Tabs.vue +318 -0
  167. package/src/components/Tabs/index.ts +2 -0
  168. package/src/components/Text/Text.test.ts +154 -0
  169. package/src/components/Text/Text.vue +100 -0
  170. package/src/components/Text/index.ts +2 -0
  171. package/src/components/Textarea/Textarea.test.ts +432 -0
  172. package/src/components/Textarea/Textarea.vue +411 -0
  173. package/src/components/Textarea/index.ts +2 -0
  174. package/src/components/TimePicker/TimePicker.test.ts +352 -0
  175. package/src/components/TimePicker/TimePicker.vue +569 -0
  176. package/src/components/TimePicker/index.ts +2 -0
  177. package/src/components/Timeline/Timeline.test.ts +193 -0
  178. package/src/components/Timeline/Timeline.vue +111 -0
  179. package/src/components/Timeline/TimelineItem.vue +167 -0
  180. package/src/components/Timeline/index.ts +13 -0
  181. package/src/components/Timeline/keys.ts +21 -0
  182. package/src/components/Toast/ToastItem.test.ts +289 -0
  183. package/src/components/Toast/ToastItem.vue +370 -0
  184. package/src/components/Toast/ToastProvider.test.ts +158 -0
  185. package/src/components/Toast/ToastProvider.vue +181 -0
  186. package/src/components/Toast/index.ts +83 -0
  187. package/src/components/Toast/toastState.test.ts +165 -0
  188. package/src/components/Toast/toastState.ts +161 -0
  189. package/src/components/ToggleButton/ToggleButton.test.ts +166 -0
  190. package/src/components/ToggleButton/ToggleButton.vue +197 -0
  191. package/src/components/ToggleButton/index.ts +2 -0
  192. package/src/components/ToggleGroup/ToggleGroup.test.ts +181 -0
  193. package/src/components/ToggleGroup/ToggleGroup.vue +130 -0
  194. package/src/components/ToggleGroup/index.ts +2 -0
  195. package/src/components/Tooltip/Tooltip.test.ts +238 -0
  196. package/src/components/Tooltip/Tooltip.vue +217 -0
  197. package/src/components/Tooltip/index.ts +2 -0
  198. package/src/components/TreeView/TreeView.test.ts +357 -0
  199. package/src/components/TreeView/TreeView.vue +251 -0
  200. package/src/components/TreeView/TreeViewItem.vue +288 -0
  201. package/src/components/TreeView/index.ts +11 -0
  202. package/src/components/TreeView/keys.ts +35 -0
  203. package/src/composables/index.ts +12 -0
  204. package/src/composables/useClickOutside.ts +36 -0
  205. package/src/composables/useClipboard.ts +35 -0
  206. package/src/composables/useEventListener.ts +48 -0
  207. package/src/composables/useFocusTrap.ts +58 -0
  208. package/src/composables/useHoverReveal.ts +98 -0
  209. package/src/composables/useId.ts +10 -0
  210. package/src/composables/useMagnetic.ts +171 -0
  211. package/src/composables/useRelativePosition.ts +127 -0
  212. package/src/composables/useRipple.ts +146 -0
  213. package/src/composables/useScrollLock.ts +25 -0
  214. package/src/composables/useSpireConfig.ts +27 -0
  215. package/src/composables/useStagger.ts +224 -0
  216. package/src/config/icons.test.ts +115 -0
  217. package/src/config/icons.ts +170 -0
  218. package/src/index.ts +361 -0
  219. package/src/styles/depth.css +129 -0
  220. package/src/styles/effects.css +169 -0
  221. package/src/styles/fallback.css +152 -0
  222. package/src/styles/main.css +25 -0
  223. package/src/styles/mood.css +211 -0
  224. package/src/styles/motion.css +159 -0
  225. package/src/styles/reset.css +97 -0
  226. package/src/styles/theme.css +708 -0
  227. package/src/styles/tokens.css +183 -0
  228. package/src/utils/.gitkeep +0 -0
  229. package/src/utils/color.ts +277 -0
  230. package/src/utils/date.test.ts +522 -0
  231. package/src/utils/date.ts +380 -0
  232. package/src/utils/index.ts +23 -0
  233. package/src/utils/object.test.ts +80 -0
  234. package/src/utils/object.ts +25 -0
  235. package/src/utils/string.test.ts +64 -0
  236. package/src/utils/string.ts +32 -0
  237. package/src/utils/time.ts +156 -0
@@ -0,0 +1,1042 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
3
+ import Button from '../Button/Button.vue'
4
+ import { useId } from '../../composables'
5
+
6
+ export interface UploadFile {
7
+ /** Unique identifier */
8
+ id: string
9
+ /** Original File object */
10
+ file: File
11
+ /** File name */
12
+ name: string
13
+ /** File size in bytes */
14
+ size: number
15
+ /** MIME type */
16
+ type: string
17
+ /** Preview URL (for images) */
18
+ preview?: string
19
+ /** Upload progress (0-100) */
20
+ progress: number
21
+ /** Upload status */
22
+ status: 'pending' | 'uploading' | 'success' | 'error'
23
+ /** Error message if status is error */
24
+ error?: string
25
+ }
26
+
27
+ export interface FileUploadProps {
28
+ /** Currently selected files (v-model) */
29
+ modelValue?: UploadFile[]
30
+ /** Accepted file types (MIME types or extensions) */
31
+ accept?: string
32
+ /** Allow multiple file selection */
33
+ multiple?: boolean
34
+ /** Maximum file size in bytes */
35
+ maxSize?: number
36
+ /** Maximum number of files */
37
+ maxFiles?: number
38
+ /** Disabled state */
39
+ disabled?: boolean
40
+ /** Label text */
41
+ label?: string
42
+ /** Hint text */
43
+ hint?: string
44
+ /** Error message */
45
+ error?: string
46
+ /** Show image previews */
47
+ showPreviews?: boolean
48
+ /** Compact mode (smaller dropzone) */
49
+ compact?: boolean
50
+ /** Enable paste-to-upload (Ctrl+V) */
51
+ allowPaste?: boolean
52
+ }
53
+
54
+ const props = withDefaults(defineProps<FileUploadProps>(), {
55
+ modelValue: () => [],
56
+ multiple: false,
57
+ disabled: false,
58
+ showPreviews: true,
59
+ compact: false,
60
+ allowPaste: true
61
+ })
62
+
63
+ const emit = defineEmits<{
64
+ (e: 'update:modelValue', files: UploadFile[]): void
65
+ (e: 'files-added', files: UploadFile[]): void
66
+ (e: 'file-removed', file: UploadFile): void
67
+ (e: 'file-rejected', file: File, reason: string): void
68
+ (e: 'files-pasted', files: UploadFile[]): void
69
+ (e: 'file-replaced', oldFile: UploadFile, newFile: UploadFile): void
70
+ }>()
71
+
72
+ const inputRef = ref<HTMLInputElement | null>(null)
73
+ const dropzoneRef = ref<HTMLElement | null>(null)
74
+ const replacingFileId = ref<string | null>(null)
75
+
76
+ const isDragging = ref(false)
77
+ const dragCounter = ref(0)
78
+ const showPasteIndicator = ref(false)
79
+ const isHovered = ref(false)
80
+ const isFocused = ref(false)
81
+
82
+ const uid = useId('file-upload')
83
+ const inputId = computed(() => uid)
84
+ const hintId = computed(() => `${uid}-hint`)
85
+ const errorId = computed(() => `${uid}-error`)
86
+
87
+ const describedBy = computed(() => {
88
+ if (props.error) return errorId.value
89
+ if (props.hint) return hintId.value
90
+ return undefined
91
+ })
92
+
93
+ const previewUrls = ref<Map<string, string>>(new Map())
94
+
95
+ function generateFileId(): string {
96
+ return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`
97
+ }
98
+
99
+ function formatSize(bytes: number): string {
100
+ if (bytes === 0) return '0 B'
101
+ const k = 1024
102
+ const sizes = ['B', 'KB', 'MB', 'GB']
103
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
104
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`
105
+ }
106
+
107
+ function isAcceptedType(file: File): boolean {
108
+ if (!props.accept) return true
109
+
110
+ const acceptedTypes = props.accept.split(',').map(t => t.trim().toLowerCase())
111
+
112
+ for (const accepted of acceptedTypes) {
113
+ if (accepted === file.type.toLowerCase()) return true
114
+
115
+ if (accepted.endsWith('/*')) {
116
+ const category = accepted.slice(0, -2)
117
+ if (file.type.toLowerCase().startsWith(category + '/')) return true
118
+ }
119
+
120
+ if (accepted.startsWith('.')) {
121
+ const ext = file.name.toLowerCase().slice(file.name.lastIndexOf('.'))
122
+ if (ext === accepted) return true
123
+ }
124
+ }
125
+
126
+ return false
127
+ }
128
+
129
+ function validateFile(file: File): string | null {
130
+ if (!isAcceptedType(file)) {
131
+ return `File type "${file.type || 'unknown'}" is not accepted`
132
+ }
133
+
134
+ if (props.maxSize && file.size > props.maxSize) {
135
+ return `File exceeds maximum size of ${formatSize(props.maxSize)}`
136
+ }
137
+
138
+ return null
139
+ }
140
+
141
+ function createPreview(file: File): string | undefined {
142
+ if (!props.showPreviews) return undefined
143
+ if (!file.type.startsWith('image/')) return undefined
144
+
145
+ const url = URL.createObjectURL(file)
146
+ return url
147
+ }
148
+
149
+ /**
150
+ * Process and add files
151
+ * @returns The array of successfully added files
152
+ */
153
+ function processFiles(fileList: FileList | File[]): UploadFile[] {
154
+ const files = Array.from(fileList)
155
+ const currentCount = props.modelValue.length
156
+ const validFiles: UploadFile[] = []
157
+
158
+ for (const file of files) {
159
+ if (props.maxFiles && currentCount + validFiles.length >= props.maxFiles) {
160
+ emit('file-rejected', file, `Maximum ${props.maxFiles} files allowed`)
161
+ continue
162
+ }
163
+
164
+ const error = validateFile(file)
165
+ if (error) {
166
+ emit('file-rejected', file, error)
167
+ continue
168
+ }
169
+
170
+ const id = generateFileId()
171
+ const preview = createPreview(file)
172
+
173
+ if (preview) {
174
+ previewUrls.value.set(id, preview)
175
+ }
176
+
177
+ validFiles.push({
178
+ id,
179
+ file,
180
+ name: file.name,
181
+ size: file.size,
182
+ type: file.type,
183
+ preview,
184
+ progress: 0,
185
+ status: 'pending'
186
+ })
187
+ }
188
+
189
+ if (validFiles.length > 0) {
190
+ const newFiles = props.multiple
191
+ ? [...props.modelValue, ...validFiles]
192
+ : validFiles.slice(0, 1)
193
+
194
+ emit('update:modelValue', newFiles)
195
+ emit('files-added', validFiles)
196
+ }
197
+
198
+ return validFiles
199
+ }
200
+
201
+ function handleInputChange(event: Event) {
202
+ const input = event.target as HTMLInputElement
203
+ if (input.files && input.files.length > 0) {
204
+ if (replacingFileId.value) {
205
+ handleReplaceFile(input.files[0])
206
+ } else {
207
+ processFiles(input.files)
208
+ }
209
+ }
210
+ input.value = ''
211
+ replacingFileId.value = null
212
+ }
213
+
214
+ function handleReplaceFile(newFile: File) {
215
+ if (!replacingFileId.value) return
216
+
217
+ const oldFileIndex = props.modelValue.findIndex(f => f.id === replacingFileId.value)
218
+ if (oldFileIndex === -1) return
219
+
220
+ const oldFile = props.modelValue[oldFileIndex]
221
+
222
+ const error = validateFile(newFile)
223
+ if (error) {
224
+ emit('file-rejected', newFile, error)
225
+ return
226
+ }
227
+
228
+ const oldPreview = previewUrls.value.get(oldFile.id)
229
+ if (oldPreview) {
230
+ URL.revokeObjectURL(oldPreview)
231
+ previewUrls.value.delete(oldFile.id)
232
+ }
233
+
234
+ const id = generateFileId()
235
+ const preview = createPreview(newFile)
236
+
237
+ if (preview) {
238
+ previewUrls.value.set(id, preview)
239
+ }
240
+
241
+ const newUploadFile: UploadFile = {
242
+ id,
243
+ file: newFile,
244
+ name: newFile.name,
245
+ size: newFile.size,
246
+ type: newFile.type,
247
+ preview,
248
+ progress: 0,
249
+ status: 'pending'
250
+ }
251
+
252
+ const newFiles = [...props.modelValue]
253
+ newFiles[oldFileIndex] = newUploadFile
254
+
255
+ emit('update:modelValue', newFiles)
256
+ emit('file-replaced', oldFile, newUploadFile)
257
+ }
258
+
259
+ function removeFile(fileToRemove: UploadFile) {
260
+ const previewUrl = previewUrls.value.get(fileToRemove.id)
261
+ if (previewUrl) {
262
+ URL.revokeObjectURL(previewUrl)
263
+ previewUrls.value.delete(fileToRemove.id)
264
+ }
265
+
266
+ const newFiles = props.modelValue.filter(f => f.id !== fileToRemove.id)
267
+ emit('update:modelValue', newFiles)
268
+ emit('file-removed', fileToRemove)
269
+ }
270
+
271
+ function replaceFile(file: UploadFile) {
272
+ if (props.disabled) return
273
+ replacingFileId.value = file.id
274
+ inputRef.value?.click()
275
+ }
276
+
277
+ function triggerInput() {
278
+ if (props.disabled) return
279
+ inputRef.value?.click()
280
+ }
281
+
282
+ function handleKeydown(event: KeyboardEvent) {
283
+ if (event.key === 'Enter' || event.key === ' ') {
284
+ event.preventDefault()
285
+ triggerInput()
286
+ }
287
+ }
288
+
289
+ function handleDragEnter(event: DragEvent) {
290
+ event.preventDefault()
291
+ if (props.disabled) return
292
+
293
+ dragCounter.value++
294
+ isDragging.value = true
295
+ }
296
+
297
+ function handleDragOver(event: DragEvent) {
298
+ event.preventDefault()
299
+ }
300
+
301
+ function handleDragLeave(event: DragEvent) {
302
+ event.preventDefault()
303
+ if (props.disabled) return
304
+
305
+ dragCounter.value--
306
+ if (dragCounter.value === 0) {
307
+ isDragging.value = false
308
+ }
309
+ }
310
+
311
+ function handleDrop(event: DragEvent) {
312
+ event.preventDefault()
313
+ if (props.disabled) return
314
+
315
+ dragCounter.value = 0
316
+ isDragging.value = false
317
+
318
+ if (event.dataTransfer?.files && event.dataTransfer.files.length > 0) {
319
+ processFiles(event.dataTransfer.files)
320
+ }
321
+ }
322
+
323
+ function handleMouseEnter() {
324
+ isHovered.value = true
325
+ }
326
+
327
+ function handleMouseLeave() {
328
+ isHovered.value = false
329
+ }
330
+
331
+ function handleFocus() {
332
+ isFocused.value = true
333
+ }
334
+
335
+ function handleBlur() {
336
+ isFocused.value = false
337
+ }
338
+
339
+ function handlePaste(event: ClipboardEvent) {
340
+ if (!props.allowPaste || props.disabled) return
341
+ if (!isHovered.value && !isFocused.value) return
342
+
343
+ const items = event.clipboardData?.items
344
+ if (!items) return
345
+
346
+ const files: File[] = []
347
+ for (const item of Array.from(items)) {
348
+ if (item.kind === 'file') {
349
+ const file = item.getAsFile()
350
+ if (file) {
351
+ files.push(file)
352
+ }
353
+ }
354
+ }
355
+
356
+ if (files.length > 0) {
357
+ const addedFiles = processFiles(files)
358
+
359
+ if (addedFiles.length > 0) {
360
+ showPasteIndicator.value = true
361
+ setTimeout(() => {
362
+ showPasteIndicator.value = false
363
+ }, 1500)
364
+
365
+ emit('files-pasted', addedFiles)
366
+ }
367
+ }
368
+ }
369
+
370
+ /**
371
+ * Update file progress (call from parent during upload)
372
+ */
373
+ function updateProgress(fileId: string, progress: number) {
374
+ const file = props.modelValue.find(f => f.id === fileId)
375
+ if (file) {
376
+ file.progress = Math.min(100, Math.max(0, progress))
377
+ if (file.status === 'pending') {
378
+ file.status = 'uploading'
379
+ }
380
+ }
381
+ }
382
+
383
+ /**
384
+ * Mark file as successfully uploaded
385
+ */
386
+ function markSuccess(fileId: string) {
387
+ const file = props.modelValue.find(f => f.id === fileId)
388
+ if (file) {
389
+ file.progress = 100
390
+ file.status = 'success'
391
+ }
392
+ }
393
+
394
+ /**
395
+ * Mark file as failed
396
+ */
397
+ function markError(fileId: string, error: string) {
398
+ const file = props.modelValue.find(f => f.id === fileId)
399
+ if (file) {
400
+ file.status = 'error'
401
+ file.error = error
402
+ }
403
+ }
404
+
405
+ /**
406
+ * Clear all files
407
+ */
408
+ function clearAll() {
409
+ for (const url of previewUrls.value.values()) {
410
+ URL.revokeObjectURL(url)
411
+ }
412
+ previewUrls.value.clear()
413
+
414
+ emit('update:modelValue', [])
415
+ }
416
+
417
+ defineExpose({
418
+ updateProgress,
419
+ markSuccess,
420
+ markError,
421
+ clearAll,
422
+ triggerInput,
423
+ replaceFile
424
+ })
425
+
426
+ onMounted(() => {
427
+ if (props.allowPaste) {
428
+ document.addEventListener('paste', handlePaste)
429
+ }
430
+ })
431
+
432
+ watch(() => props.allowPaste, (enabled) => {
433
+ if (enabled) {
434
+ document.addEventListener('paste', handlePaste)
435
+ } else {
436
+ document.removeEventListener('paste', handlePaste)
437
+ }
438
+ })
439
+
440
+ onBeforeUnmount(() => {
441
+ document.removeEventListener('paste', handlePaste)
442
+
443
+ for (const url of previewUrls.value.values()) {
444
+ URL.revokeObjectURL(url)
445
+ }
446
+ previewUrls.value.clear()
447
+ })
448
+
449
+ watch(() => props.modelValue, (newFiles, oldFiles) => {
450
+ if (!oldFiles) return
451
+
452
+ const newIds = new Set(newFiles.map(f => f.id))
453
+
454
+ for (const oldFile of oldFiles) {
455
+ if (!newIds.has(oldFile.id)) {
456
+ const url = previewUrls.value.get(oldFile.id)
457
+ if (url) {
458
+ URL.revokeObjectURL(url)
459
+ previewUrls.value.delete(oldFile.id)
460
+ }
461
+ }
462
+ }
463
+ }, { deep: true })
464
+
465
+ const hasFiles = computed(() => props.modelValue.length > 0)
466
+ const isUploading = computed(() => props.modelValue.some(f => f.status === 'uploading'))
467
+
468
+ function getFileIcon(type: string): string {
469
+ if (type.startsWith('image/')) return 'image'
470
+ if (type.startsWith('video/')) return 'video'
471
+ if (type.startsWith('audio/')) return 'audio'
472
+ if (type === 'application/pdf') return 'pdf'
473
+ if (type.includes('spreadsheet') || type.includes('excel')) return 'spreadsheet'
474
+ if (type.includes('document') || type.includes('word')) return 'document'
475
+ if (type.includes('zip') || type.includes('compressed')) return 'archive'
476
+ return 'file'
477
+ }
478
+ </script>
479
+
480
+ <template>
481
+ <div
482
+ class="ui-file-upload"
483
+ :class="{
484
+ 'ui-file-upload--disabled': disabled,
485
+ 'ui-file-upload--error': error,
486
+ 'ui-file-upload--compact': compact
487
+ }"
488
+ >
489
+ <label
490
+ v-if="label"
491
+ :for="inputId"
492
+ class="ui-file-upload__label"
493
+ >
494
+ {{ label }}
495
+ </label>
496
+
497
+ <div
498
+ ref="dropzoneRef"
499
+ class="ui-file-upload__dropzone"
500
+ :class="{
501
+ 'ui-file-upload__dropzone--active': isDragging,
502
+ 'ui-file-upload__dropzone--disabled': disabled,
503
+ 'ui-file-upload__dropzone--error': error,
504
+ 'ui-file-upload__dropzone--has-files': hasFiles && !compact
505
+ }"
506
+ role="button"
507
+ tabindex="0"
508
+ :aria-disabled="disabled"
509
+ :aria-describedby="describedBy"
510
+ @click="triggerInput"
511
+ @keydown="handleKeydown"
512
+ @dragenter="handleDragEnter"
513
+ @dragover="handleDragOver"
514
+ @dragleave="handleDragLeave"
515
+ @drop="handleDrop"
516
+ @mouseenter="handleMouseEnter"
517
+ @mouseleave="handleMouseLeave"
518
+ @focus="handleFocus"
519
+ @blur="handleBlur"
520
+ >
521
+ <input
522
+ :id="inputId"
523
+ ref="inputRef"
524
+ type="file"
525
+ class="ui-file-upload__input"
526
+ :accept="accept"
527
+ :multiple="multiple"
528
+ :disabled="disabled"
529
+ tabindex="-1"
530
+ @change="handleInputChange"
531
+ />
532
+
533
+ <div class="ui-file-upload__content">
534
+ <div class="ui-file-upload__icon" aria-hidden="true">
535
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
536
+ <path d="M12 16V4m0 0l-4 4m4-4l4 4" stroke-linecap="round" stroke-linejoin="round" />
537
+ <path d="M3 15v4a2 2 0 002 2h14a2 2 0 002-2v-4" stroke-linecap="round" stroke-linejoin="round" />
538
+ </svg>
539
+ </div>
540
+
541
+ <div class="ui-file-upload__text">
542
+ <span v-if="isDragging" class="ui-file-upload__drop-text">
543
+ Drop files here
544
+ </span>
545
+ <template v-else>
546
+ <span class="ui-file-upload__primary-text">
547
+ <span class="ui-file-upload__link">Click to upload</span>
548
+ or drag and drop
549
+ </span>
550
+ <span v-if="accept || maxSize || allowPaste" class="ui-file-upload__secondary-text">
551
+ <template v-if="accept">{{ accept }}</template>
552
+ <template v-if="accept && (maxSize || allowPaste)"> · </template>
553
+ <template v-if="maxSize">Max {{ formatSize(maxSize) }}</template>
554
+ <template v-if="maxSize && allowPaste"> · </template>
555
+ <template v-if="allowPaste">Paste (Ctrl+V)</template>
556
+ </span>
557
+ </template>
558
+ </div>
559
+ </div>
560
+
561
+ <Transition name="ui-file-upload-fade">
562
+ <div v-if="showPasteIndicator" class="ui-file-upload__paste-indicator">
563
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
564
+ <path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2" />
565
+ <rect x="9" y="3" width="6" height="4" rx="1" />
566
+ </svg>
567
+ <span>Files pasted</span>
568
+ </div>
569
+ </Transition>
570
+ </div>
571
+
572
+ <ul v-if="hasFiles" class="ui-file-upload__list" role="list">
573
+ <li
574
+ v-for="file in modelValue"
575
+ :key="file.id"
576
+ class="ui-file-upload__file"
577
+ :class="{
578
+ 'ui-file-upload__file--uploading': file.status === 'uploading',
579
+ 'ui-file-upload__file--success': file.status === 'success',
580
+ 'ui-file-upload__file--error': file.status === 'error'
581
+ }"
582
+ >
583
+ <div class="ui-file-upload__file-preview">
584
+ <img
585
+ v-if="file.preview"
586
+ :src="file.preview"
587
+ :alt="file.name"
588
+ class="ui-file-upload__file-image"
589
+ />
590
+ <div v-else class="ui-file-upload__file-icon" :data-type="getFileIcon(file.type)">
591
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
592
+ <path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" />
593
+ <polyline points="14 2 14 8 20 8" />
594
+ </svg>
595
+ </div>
596
+ </div>
597
+
598
+ <div class="ui-file-upload__file-info">
599
+ <span class="ui-file-upload__file-name">{{ file.name }}</span>
600
+ <span class="ui-file-upload__file-meta">
601
+ {{ formatSize(file.size) }}
602
+ <template v-if="file.status === 'error' && file.error">
603
+ · <span class="ui-file-upload__file-error">{{ file.error }}</span>
604
+ </template>
605
+ <template v-else-if="file.status === 'success'">
606
+ · <span class="ui-file-upload__file-success">Uploaded</span>
607
+ </template>
608
+ </span>
609
+
610
+ <div
611
+ v-if="file.status === 'uploading'"
612
+ class="ui-file-upload__progress"
613
+ >
614
+ <div
615
+ class="ui-file-upload__progress-bar"
616
+ :style="{ width: `${file.progress}%` }"
617
+ />
618
+ </div>
619
+ </div>
620
+
621
+ <div class="ui-file-upload__file-status">
622
+ <svg
623
+ v-if="file.status === 'success'"
624
+ class="ui-file-upload__status-icon ui-file-upload__status-icon--success"
625
+ viewBox="0 0 24 24"
626
+ fill="none"
627
+ stroke="currentColor"
628
+ stroke-width="2"
629
+ >
630
+ <path d="M5 12l5 5L20 7" />
631
+ </svg>
632
+
633
+ <svg
634
+ v-else-if="file.status === 'error'"
635
+ class="ui-file-upload__status-icon ui-file-upload__status-icon--error"
636
+ viewBox="0 0 24 24"
637
+ fill="none"
638
+ stroke="currentColor"
639
+ stroke-width="2"
640
+ >
641
+ <circle cx="12" cy="12" r="10" />
642
+ <line x1="12" y1="8" x2="12" y2="12" />
643
+ <line x1="12" y1="16" x2="12.01" y2="16" />
644
+ </svg>
645
+
646
+ <div
647
+ v-else-if="file.status === 'uploading'"
648
+ class="ui-file-upload__spinner"
649
+ />
650
+ </div>
651
+
652
+ <div v-if="file.status !== 'uploading'" class="ui-file-upload__actions">
653
+ <Button
654
+ variant="ghost"
655
+ size="xs"
656
+ :aria-label="`Replace ${file.name}`"
657
+ @click.stop="replaceFile(file)"
658
+ >
659
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="16" height="16">
660
+ <path d="M4 12v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l4 4m0 0l-4 4m4-4H9" stroke-linecap="round" stroke-linejoin="round" />
661
+ </svg>
662
+ </Button>
663
+
664
+ <Button
665
+ variant="ghost"
666
+ size="xs"
667
+ class="ui-file-upload__remove-btn"
668
+ :aria-label="`Remove ${file.name}`"
669
+ @click.stop="removeFile(file)"
670
+ >
671
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="16" height="16">
672
+ <path d="M18 6L6 18M6 6l12 12" stroke-linecap="round" stroke-linejoin="round" />
673
+ </svg>
674
+ </Button>
675
+ </div>
676
+ </li>
677
+ </ul>
678
+
679
+ <p
680
+ v-if="error"
681
+ :id="errorId"
682
+ class="ui-file-upload__message ui-file-upload__message--error"
683
+ role="alert"
684
+ >
685
+ {{ error }}
686
+ </p>
687
+ <p
688
+ v-else-if="hint"
689
+ :id="hintId"
690
+ class="ui-file-upload__message ui-file-upload__message--hint"
691
+ >
692
+ {{ hint }}
693
+ </p>
694
+ </div>
695
+ </template>
696
+
697
+ <style scoped>
698
+ .ui-file-upload {
699
+ display: flex;
700
+ flex-direction: column;
701
+ gap: var(--space-2);
702
+ font-family: var(--font-sans);
703
+ }
704
+
705
+ .ui-file-upload__label {
706
+ font-size: var(--text-sm);
707
+ font-weight: var(--font-medium);
708
+ color: var(--input-label);
709
+ line-height: var(--leading-tight);
710
+ }
711
+
712
+ .ui-file-upload__dropzone {
713
+ position: relative;
714
+ display: flex;
715
+ align-items: center;
716
+ justify-content: center;
717
+ min-height: 160px;
718
+ padding: var(--space-6);
719
+ background-color: var(--file-upload-bg);
720
+ border: 2px dashed var(--file-upload-border);
721
+ border-radius: var(--radius-lg);
722
+ cursor: pointer;
723
+ transition:
724
+ border-color var(--duration-fast) var(--ease-default),
725
+ background-color var(--duration-fast) var(--ease-default);
726
+ }
727
+
728
+ .ui-file-upload--compact .ui-file-upload__dropzone {
729
+ min-height: 80px;
730
+ padding: var(--space-4);
731
+ }
732
+
733
+ .ui-file-upload__dropzone:hover:not(.ui-file-upload__dropzone--disabled) {
734
+ border-color: var(--file-upload-border-hover);
735
+ background-color: var(--file-upload-bg-hover);
736
+ }
737
+
738
+ .ui-file-upload__dropzone:focus-visible {
739
+ outline: none;
740
+ border-color: var(--input-border-focus);
741
+ box-shadow: 0 0 0 3px var(--input-ring);
742
+ }
743
+
744
+ .ui-file-upload__dropzone--active {
745
+ border-color: var(--file-upload-active-border);
746
+ border-style: solid;
747
+ background-color: var(--file-upload-active-bg);
748
+ }
749
+
750
+ .ui-file-upload__dropzone--error {
751
+ border-color: var(--file-upload-error-border);
752
+ }
753
+
754
+ .ui-file-upload__dropzone--disabled {
755
+ opacity: 0.5;
756
+ cursor: not-allowed;
757
+ background-color: var(--input-bg-disabled);
758
+ }
759
+
760
+ .ui-file-upload__input {
761
+ position: absolute;
762
+ width: 1px;
763
+ height: 1px;
764
+ padding: 0;
765
+ margin: -1px;
766
+ overflow: hidden;
767
+ clip: rect(0, 0, 0, 0);
768
+ white-space: nowrap;
769
+ border: 0;
770
+ }
771
+
772
+ .ui-file-upload__content {
773
+ display: flex;
774
+ flex-direction: column;
775
+ align-items: center;
776
+ gap: var(--space-3);
777
+ text-align: center;
778
+ pointer-events: none;
779
+ }
780
+
781
+ .ui-file-upload--compact .ui-file-upload__content {
782
+ flex-direction: row;
783
+ gap: var(--space-4);
784
+ }
785
+
786
+ .ui-file-upload__icon {
787
+ display: flex;
788
+ align-items: center;
789
+ justify-content: center;
790
+ width: 48px;
791
+ height: 48px;
792
+ color: var(--file-upload-icon);
793
+ background-color: var(--file-upload-icon-bg);
794
+ border-radius: var(--radius-full);
795
+ }
796
+
797
+ .ui-file-upload--compact .ui-file-upload__icon {
798
+ width: 40px;
799
+ height: 40px;
800
+ }
801
+
802
+ .ui-file-upload__icon svg {
803
+ width: 24px;
804
+ height: 24px;
805
+ }
806
+
807
+ .ui-file-upload--compact .ui-file-upload__icon svg {
808
+ width: 20px;
809
+ height: 20px;
810
+ }
811
+
812
+ .ui-file-upload__dropzone--active .ui-file-upload__icon {
813
+ color: var(--file-upload-active-icon);
814
+ background-color: var(--file-upload-active-icon-bg);
815
+ }
816
+
817
+ .ui-file-upload__text {
818
+ display: flex;
819
+ flex-direction: column;
820
+ gap: var(--space-1);
821
+ }
822
+
823
+ .ui-file-upload--compact .ui-file-upload__text {
824
+ text-align: left;
825
+ }
826
+
827
+ .ui-file-upload__primary-text {
828
+ font-size: var(--text-sm);
829
+ color: var(--text-secondary);
830
+ }
831
+
832
+ .ui-file-upload__link {
833
+ color: var(--action-primary);
834
+ font-weight: var(--font-medium);
835
+ }
836
+
837
+ .ui-file-upload__secondary-text {
838
+ font-size: var(--text-xs);
839
+ color: var(--text-tertiary);
840
+ }
841
+
842
+ .ui-file-upload__drop-text {
843
+ font-size: var(--text-sm);
844
+ font-weight: var(--font-medium);
845
+ color: var(--action-primary);
846
+ }
847
+
848
+ .ui-file-upload__list {
849
+ display: flex;
850
+ flex-direction: column;
851
+ gap: var(--space-2);
852
+ margin: 0;
853
+ padding: 0;
854
+ list-style: none;
855
+ }
856
+
857
+ .ui-file-upload__file {
858
+ display: flex;
859
+ align-items: center;
860
+ gap: var(--space-3);
861
+ padding: var(--space-3);
862
+ background-color: var(--file-upload-file-bg);
863
+ border: 1px solid var(--file-upload-file-border);
864
+ border-radius: var(--radius-md);
865
+ }
866
+
867
+ .ui-file-upload__file--error {
868
+ border-color: var(--file-upload-error-border);
869
+ background-color: var(--file-upload-error-bg);
870
+ }
871
+
872
+ .ui-file-upload__file--success {
873
+ border-color: var(--file-upload-success-border);
874
+ }
875
+
876
+ .ui-file-upload__file-preview {
877
+ flex-shrink: 0;
878
+ width: 40px;
879
+ height: 40px;
880
+ border-radius: var(--radius-md);
881
+ overflow: hidden;
882
+ }
883
+
884
+ .ui-file-upload__file-image {
885
+ width: 100%;
886
+ height: 100%;
887
+ object-fit: cover;
888
+ }
889
+
890
+ .ui-file-upload__file-icon {
891
+ display: flex;
892
+ align-items: center;
893
+ justify-content: center;
894
+ width: 100%;
895
+ height: 100%;
896
+ background-color: var(--file-upload-icon-bg);
897
+ color: var(--file-upload-icon);
898
+ }
899
+
900
+ .ui-file-upload__file-icon svg {
901
+ width: 20px;
902
+ height: 20px;
903
+ }
904
+
905
+ .ui-file-upload__file-info {
906
+ flex: 1;
907
+ min-width: 0;
908
+ display: flex;
909
+ flex-direction: column;
910
+ gap: var(--space-1);
911
+ }
912
+
913
+ .ui-file-upload__file-name {
914
+ font-size: var(--text-sm);
915
+ font-weight: var(--font-medium);
916
+ color: var(--text-primary);
917
+ white-space: nowrap;
918
+ overflow: hidden;
919
+ text-overflow: ellipsis;
920
+ }
921
+
922
+ .ui-file-upload__file-meta {
923
+ font-size: var(--text-xs);
924
+ color: var(--text-tertiary);
925
+ }
926
+
927
+ .ui-file-upload__file-error {
928
+ color: var(--status-error);
929
+ }
930
+
931
+ .ui-file-upload__file-success {
932
+ color: var(--status-success);
933
+ }
934
+
935
+ .ui-file-upload__progress {
936
+ width: 100%;
937
+ height: 4px;
938
+ background-color: var(--file-upload-progress-bg);
939
+ border-radius: var(--radius-full);
940
+ overflow: hidden;
941
+ }
942
+
943
+ .ui-file-upload__progress-bar {
944
+ height: 100%;
945
+ background-color: var(--file-upload-progress);
946
+ border-radius: var(--radius-full);
947
+ transition: width var(--duration-normal) var(--ease-out);
948
+ }
949
+
950
+ .ui-file-upload__file-status {
951
+ flex-shrink: 0;
952
+ width: 20px;
953
+ height: 20px;
954
+ }
955
+
956
+ .ui-file-upload__status-icon {
957
+ width: 20px;
958
+ height: 20px;
959
+ }
960
+
961
+ .ui-file-upload__status-icon--success {
962
+ color: var(--status-success);
963
+ }
964
+
965
+ .ui-file-upload__status-icon--error {
966
+ color: var(--status-error);
967
+ }
968
+
969
+ .ui-file-upload__spinner {
970
+ width: 20px;
971
+ height: 20px;
972
+ border: 2px solid var(--file-upload-progress-bg);
973
+ border-top-color: var(--action-primary);
974
+ border-radius: var(--radius-full);
975
+ animation: ui-file-upload-spin 0.8s linear infinite;
976
+ }
977
+
978
+ @keyframes ui-file-upload-spin {
979
+ to {
980
+ transform: rotate(360deg);
981
+ }
982
+ }
983
+
984
+ .ui-file-upload__actions {
985
+ display: flex;
986
+ gap: var(--space-1);
987
+ flex-shrink: 0;
988
+ }
989
+
990
+ .ui-file-upload__remove-btn:hover {
991
+ color: var(--status-error);
992
+ background-color: var(--file-upload-remove-hover-bg);
993
+ }
994
+
995
+ .ui-file-upload__message {
996
+ font-size: var(--text-xs);
997
+ line-height: var(--leading-normal);
998
+ margin: 0;
999
+ }
1000
+
1001
+ .ui-file-upload__message--hint {
1002
+ color: var(--input-hint);
1003
+ }
1004
+
1005
+ .ui-file-upload__message--error {
1006
+ color: var(--input-error);
1007
+ }
1008
+
1009
+ .ui-file-upload__paste-indicator {
1010
+ position: absolute;
1011
+ top: 50%;
1012
+ left: 50%;
1013
+ transform: translate(-50%, -50%);
1014
+ display: flex;
1015
+ align-items: center;
1016
+ gap: var(--space-2);
1017
+ padding: var(--space-2) var(--space-4);
1018
+ background-color: var(--file-upload-paste-bg);
1019
+ color: var(--file-upload-paste-text);
1020
+ border-radius: var(--radius-full);
1021
+ font-size: var(--text-sm);
1022
+ font-weight: var(--font-medium);
1023
+ box-shadow: var(--shadow-md);
1024
+ pointer-events: none;
1025
+ z-index: 10;
1026
+ }
1027
+
1028
+ .ui-file-upload__paste-indicator svg {
1029
+ width: 16px;
1030
+ height: 16px;
1031
+ }
1032
+
1033
+ .ui-file-upload-fade-enter-active,
1034
+ .ui-file-upload-fade-leave-active {
1035
+ transition: opacity var(--duration-normal) var(--ease-default);
1036
+ }
1037
+
1038
+ .ui-file-upload-fade-enter-from,
1039
+ .ui-file-upload-fade-leave-to {
1040
+ opacity: 0;
1041
+ }
1042
+ </style>