@umbra.ui/core 0.1.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.
- package/dist/components/controls/Dropdown/types.d.ts +5 -0
- package/dist/components/controls/Dropdown/types.d.ts.map +1 -0
- package/dist/components/controls/Dropdown/types.js +1 -0
- package/dist/components/controls/SegmentedControl/types.d.ts +6 -0
- package/dist/components/controls/SegmentedControl/types.d.ts.map +1 -0
- package/dist/components/controls/SegmentedControl/types.js +1 -0
- package/dist/components/dialogs/Alert/types.d.ts +7 -0
- package/dist/components/dialogs/Alert/types.d.ts.map +1 -0
- package/dist/components/dialogs/Alert/types.js +1 -0
- package/dist/components/dialogs/Toast/types.d.ts +34 -0
- package/dist/components/dialogs/Toast/types.d.ts.map +1 -0
- package/dist/components/dialogs/Toast/types.js +10 -0
- package/dist/components/dialogs/Toast/useToast.d.ts +36 -0
- package/dist/components/dialogs/Toast/useToast.d.ts.map +1 -0
- package/dist/components/dialogs/Toast/useToast.js +90 -0
- package/dist/components/indicators/Tooltip/tooltip.d.ts +3 -0
- package/dist/components/indicators/Tooltip/tooltip.d.ts.map +1 -0
- package/dist/components/indicators/Tooltip/tooltip.js +33 -0
- package/dist/components/indicators/Tooltip/types.d.ts +14 -0
- package/dist/components/indicators/Tooltip/types.d.ts.map +1 -0
- package/dist/components/indicators/Tooltip/types.js +1 -0
- package/dist/components/indicators/Tooltip/useTooltip.d.ts +18 -0
- package/dist/components/indicators/Tooltip/useTooltip.d.ts.map +1 -0
- package/dist/components/indicators/Tooltip/useTooltip.js +57 -0
- package/dist/components/inputs/Tags/tag-bar-styles.d.ts +14 -0
- package/dist/components/inputs/Tags/tag-bar-styles.d.ts.map +1 -0
- package/dist/components/inputs/Tags/tag-bar-styles.js +313 -0
- package/dist/components/inputs/Tags/types.d.ts +93 -0
- package/dist/components/inputs/Tags/types.d.ts.map +1 -0
- package/dist/components/inputs/Tags/types.js +216 -0
- package/dist/components/inputs/search/types.d.ts +9 -0
- package/dist/components/inputs/search/types.d.ts.map +1 -0
- package/dist/components/inputs/search/types.js +1 -0
- package/dist/components/navigation/adaptive/types.d.ts +16 -0
- package/dist/components/navigation/adaptive/types.d.ts.map +1 -0
- package/dist/components/navigation/adaptive/types.js +1 -0
- package/dist/components/navigation/adaptive/useAdaptiveLayout.d.ts +27 -0
- package/dist/components/navigation/adaptive/useAdaptiveLayout.d.ts.map +1 -0
- package/dist/components/navigation/adaptive/useAdaptiveLayout.js +40 -0
- package/dist/components/navigation/adaptive/useBreakpoints.d.ts +6 -0
- package/dist/components/navigation/adaptive/useBreakpoints.d.ts.map +1 -0
- package/dist/components/navigation/adaptive/useBreakpoints.js +37 -0
- package/dist/components/navigation/adaptive/useContainerMonitor.d.ts +93 -0
- package/dist/components/navigation/adaptive/useContainerMonitor.d.ts.map +1 -0
- package/dist/components/navigation/adaptive/useContainerMonitor.js +145 -0
- package/dist/components/navigation/adaptive/useViewAnimation.d.ts +31 -0
- package/dist/components/navigation/adaptive/useViewAnimation.d.ts.map +1 -0
- package/dist/components/navigation/adaptive/useViewAnimation.js +591 -0
- package/dist/components/navigation/adaptive/useViewResize.d.ts +52 -0
- package/dist/components/navigation/adaptive/useViewResize.d.ts.map +1 -0
- package/dist/components/navigation/adaptive/useViewResize.js +146 -0
- package/dist/components/navigation/navstack/useNavigationStack.d.ts +25 -0
- package/dist/components/navigation/navstack/useNavigationStack.d.ts.map +1 -0
- package/dist/components/navigation/navstack/useNavigationStack.js +133 -0
- package/dist/components/navigation/slideover/useSlideoverController.d.ts +20 -0
- package/dist/components/navigation/slideover/useSlideoverController.d.ts.map +1 -0
- package/dist/components/navigation/slideover/useSlideoverController.js +267 -0
- package/dist/components/navigation/splitview/useSplitViewController.d.ts +20 -0
- package/dist/components/navigation/splitview/useSplitViewController.d.ts.map +1 -0
- package/dist/components/navigation/splitview/useSplitViewController.js +325 -0
- package/dist/components/navigation/tabcontroller/types.d.ts +21 -0
- package/dist/components/navigation/tabcontroller/types.d.ts.map +1 -0
- package/dist/components/navigation/tabcontroller/types.js +1 -0
- package/dist/components/navigation/tabcontroller/useTabController.d.ts +5 -0
- package/dist/components/navigation/tabcontroller/useTabController.d.ts.map +1 -0
- package/dist/components/navigation/tabcontroller/useTabController.js +10 -0
- package/dist/components/navigation/types.d.ts +8 -0
- package/dist/components/navigation/types.d.ts.map +1 -0
- package/dist/components/navigation/types.js +1 -0
- package/dist/components/pickers/CollectionPicker/types.d.ts +11 -0
- package/dist/components/pickers/CollectionPicker/types.d.ts.map +1 -0
- package/dist/components/pickers/CollectionPicker/types.js +1 -0
- package/dist/components/pickers/ColorPicker/colors.d.ts +13 -0
- package/dist/components/pickers/ColorPicker/colors.d.ts.map +1 -0
- package/dist/components/pickers/ColorPicker/colors.js +266 -0
- package/dist/components/pickers/FilePicker/types.d.ts +10 -0
- package/dist/components/pickers/FilePicker/types.d.ts.map +1 -0
- package/dist/components/pickers/FilePicker/types.js +1 -0
- package/dist/index.d.ts +91 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +196 -0
- package/dist/theme.d.ts +73 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +279 -0
- package/dist/themes/blank.d.ts +7 -0
- package/dist/themes/blank.d.ts.map +1 -0
- package/dist/themes/blank.js +543 -0
- package/dist/themes/crimson-dark.d.ts +4 -0
- package/dist/themes/crimson-dark.d.ts.map +1 -0
- package/dist/themes/crimson-dark.js +552 -0
- package/dist/themes/cyan-light.d.ts +4 -0
- package/dist/themes/cyan-light.d.ts.map +1 -0
- package/dist/themes/cyan-light.js +552 -0
- package/dist/themes/dark.d.ts +4 -0
- package/dist/themes/dark.d.ts.map +1 -0
- package/dist/themes/dark.js +551 -0
- package/dist/themes/gold-dark.d.ts +4 -0
- package/dist/themes/gold-dark.d.ts.map +1 -0
- package/dist/themes/gold-dark.js +552 -0
- package/dist/themes/grass-dark.d.ts +4 -0
- package/dist/themes/grass-dark.d.ts.map +1 -0
- package/dist/themes/grass-dark.js +552 -0
- package/dist/themes/indigo.d.ts +4 -0
- package/dist/themes/indigo.d.ts.map +1 -0
- package/dist/themes/indigo.js +552 -0
- package/dist/themes/light.d.ts +4 -0
- package/dist/themes/light.d.ts.map +1 -0
- package/dist/themes/light.js +551 -0
- package/dist/themes/orange-dark.d.ts +4 -0
- package/dist/themes/orange-dark.d.ts.map +1 -0
- package/dist/themes/orange-dark.js +551 -0
- package/dist/themes/orange-light.d.ts +4 -0
- package/dist/themes/orange-light.d.ts.map +1 -0
- package/dist/themes/orange-light.js +551 -0
- package/package.json +62 -0
- package/src/components/controls/Button/Button.vue +417 -0
- package/src/components/controls/Button/README.md +348 -0
- package/src/components/controls/Button/theme.css +200 -0
- package/src/components/controls/Checkbox/Checkbox.vue +164 -0
- package/src/components/controls/Checkbox/README.md +441 -0
- package/src/components/controls/Checkbox/theme.css +36 -0
- package/src/components/controls/Dropdown/Dropdown.vue +476 -0
- package/src/components/controls/Dropdown/README.md +370 -0
- package/src/components/controls/Dropdown/theme.css +50 -0
- package/src/components/controls/Dropdown/types.ts +6 -0
- package/src/components/controls/IconButton/IconButton.vue +267 -0
- package/src/components/controls/IconButton/README.md +502 -0
- package/src/components/controls/IconButton/theme.css +89 -0
- package/src/components/controls/Radio/README.md +591 -0
- package/src/components/controls/Radio/Radio.vue +89 -0
- package/src/components/controls/Radio/theme.css +14 -0
- package/src/components/controls/RangeSlider/README.md +608 -0
- package/src/components/controls/RangeSlider/RangeSlider.vue +535 -0
- package/src/components/controls/RangeSlider/theme.css +80 -0
- package/src/components/controls/SegmentedControl/README.md +587 -0
- package/src/components/controls/SegmentedControl/SegmentedControl.vue +284 -0
- package/src/components/controls/SegmentedControl/theme.css +60 -0
- package/src/components/controls/SegmentedControl/types.ts +5 -0
- package/src/components/controls/Slider/README.md +627 -0
- package/src/components/controls/Slider/Slider.vue +260 -0
- package/src/components/controls/Slider/theme.css +74 -0
- package/src/components/controls/Stepper/README.md +601 -0
- package/src/components/controls/Stepper/Stepper.vue +103 -0
- package/src/components/controls/Stepper/theme.css +53 -0
- package/src/components/controls/Switch/README.md +667 -0
- package/src/components/controls/Switch/Switch.vue +127 -0
- package/src/components/controls/Switch/theme.css +42 -0
- package/src/components/dialogs/Alert/Alert.vue +218 -0
- package/src/components/dialogs/Alert/README.md +450 -0
- package/src/components/dialogs/Alert/theme.css +44 -0
- package/src/components/dialogs/Alert/types.ts +11 -0
- package/src/components/dialogs/Toast/README.md +522 -0
- package/src/components/dialogs/Toast/Toast.vue +296 -0
- package/src/components/dialogs/Toast/ToastContainer.vue +330 -0
- package/src/components/dialogs/Toast/theme.css +44 -0
- package/src/components/dialogs/Toast/types.ts +46 -0
- package/src/components/dialogs/Toast/useToast.ts +127 -0
- package/src/components/indicators/ProgressBar/ProgressBar.vue +98 -0
- package/src/components/indicators/ProgressBar/README.md +744 -0
- package/src/components/indicators/ProgressBar/theme.css +36 -0
- package/src/components/indicators/Tooltip/README.md +723 -0
- package/src/components/indicators/Tooltip/TooltipProvider.vue +142 -0
- package/src/components/indicators/Tooltip/theme.css +18 -0
- package/src/components/indicators/Tooltip/tooltip.ts +48 -0
- package/src/components/indicators/Tooltip/types.ts +15 -0
- package/src/components/indicators/Tooltip/useTooltip.ts +71 -0
- package/src/components/inputs/AutogrowTextView/AutogrowTextView.vue +110 -0
- package/src/components/inputs/AutogrowTextView/README.md +643 -0
- package/src/components/inputs/AutogrowTextView/theme.css +28 -0
- package/src/components/inputs/InputCard/InputCard.vue +600 -0
- package/src/components/inputs/InputCard/README.md +636 -0
- package/src/components/inputs/InputEmail/InputEmail.vue +698 -0
- package/src/components/inputs/InputEmail/README.md +764 -0
- package/src/components/inputs/InputNumber/InputNumber.vue +300 -0
- package/src/components/inputs/InputNumber/README.md +749 -0
- package/src/components/inputs/InputPhone/InputPhone.vue +645 -0
- package/src/components/inputs/InputPhone/README.md +636 -0
- package/src/components/inputs/InputSecure/InputSecure.vue +646 -0
- package/src/components/inputs/InputSecure/README.md +771 -0
- package/src/components/inputs/InputText/InputText.vue +225 -0
- package/src/components/inputs/InputText/README.md +844 -0
- package/src/components/inputs/OTP/OTP.vue +349 -0
- package/src/components/inputs/OTP/README.md +736 -0
- package/src/components/inputs/OTP/theme.css +50 -0
- package/src/components/inputs/StringCapture/README.md +718 -0
- package/src/components/inputs/StringCapture/StringCapture.vue +315 -0
- package/src/components/inputs/StringCapture/theme.css +86 -0
- package/src/components/inputs/Tags/README.md +897 -0
- package/src/components/inputs/Tags/TagBar.vue +793 -0
- package/src/components/inputs/Tags/TagCreation.vue +219 -0
- package/src/components/inputs/Tags/TagPicker.vue +380 -0
- package/src/components/inputs/Tags/tag-bar-styles.ts +354 -0
- package/src/components/inputs/Tags/theme.css +121 -0
- package/src/components/inputs/Tags/types.ts +346 -0
- package/src/components/inputs/search/README.md +759 -0
- package/src/components/inputs/search/SearchBar.vue +394 -0
- package/src/components/inputs/search/SearchResults.vue +310 -0
- package/src/components/inputs/search/theme.css +187 -0
- package/src/components/inputs/search/types.ts +8 -0
- package/src/components/inputs/theme.css +102 -0
- package/src/components/menus/ActionMenu/ActionMenu.vue +383 -0
- package/src/components/menus/ActionMenu/README.md +825 -0
- package/src/components/menus/ActionMenu/theme.css +93 -0
- package/src/components/models/Popover/Popover.vue +551 -0
- package/src/components/models/Popover/README.md +885 -0
- package/src/components/models/Popover/theme.css +52 -0
- package/src/components/models/Sheet/README.md +1159 -0
- package/src/components/models/Sheet/Sheet.vue +465 -0
- package/src/components/models/Sheet/theme.css +72 -0
- package/src/components/models/Sidebar/README.md +1228 -0
- package/src/components/models/Sidebar/Sidebar.vue +480 -0
- package/src/components/models/Sidebar/theme.css +90 -0
- package/src/components/navigation/adaptive/AdaptiveLayout.vue +779 -0
- package/src/components/navigation/adaptive/AdaptiveLayoutBreadcrumbs.vue +192 -0
- package/src/components/navigation/adaptive/AdaptiveLayoutMenuButton.vue +149 -0
- package/src/components/navigation/adaptive/README.md +768 -0
- package/src/components/navigation/adaptive/types.ts +19 -0
- package/src/components/navigation/adaptive/useAdaptiveLayout.ts +89 -0
- package/src/components/navigation/adaptive/useBreakpoints.ts +41 -0
- package/src/components/navigation/adaptive/useContainerMonitor.ts +214 -0
- package/src/components/navigation/adaptive/useViewAnimation.ts +721 -0
- package/src/components/navigation/adaptive/useViewResize.ts +211 -0
- package/src/components/navigation/navstack/NavigationStack.vue +180 -0
- package/src/components/navigation/navstack/README.md +994 -0
- package/src/components/navigation/navstack/useNavigationStack.ts +164 -0
- package/src/components/navigation/slideover/README.md +1275 -0
- package/src/components/navigation/slideover/SlideoverController.vue +287 -0
- package/src/components/navigation/slideover/useSlideoverController.ts +320 -0
- package/src/components/navigation/splitview/README.md +1115 -0
- package/src/components/navigation/splitview/SplitViewController.vue +176 -0
- package/src/components/navigation/splitview/useSplitViewController.ts +388 -0
- package/src/components/navigation/tabcontroller/README.md +919 -0
- package/src/components/navigation/tabcontroller/TabController.vue +307 -0
- package/src/components/navigation/tabcontroller/TabItem.vue +57 -0
- package/src/components/navigation/tabcontroller/types.ts +24 -0
- package/src/components/navigation/tabcontroller/useTabController.ts +18 -0
- package/src/components/navigation/theme.css +91 -0
- package/src/components/navigation/types.ts +7 -0
- package/src/components/pickers/CollectionPicker/CollectionPicker.vue +398 -0
- package/src/components/pickers/CollectionPicker/README.md +1115 -0
- package/src/components/pickers/CollectionPicker/theme.css +14 -0
- package/src/components/pickers/CollectionPicker/types.ts +11 -0
- package/src/components/pickers/ColorPicker/ColorPicker.vue +376 -0
- package/src/components/pickers/ColorPicker/README.md +1439 -0
- package/src/components/pickers/ColorPicker/colors.ts +299 -0
- package/src/components/pickers/ColorPicker/theme.css +32 -0
- package/src/components/pickers/DatePicker/DatePicker.vue +660 -0
- package/src/components/pickers/DatePicker/README.md +1195 -0
- package/src/components/pickers/DatePicker/theme.css +22 -0
- package/src/components/pickers/FilePicker/FilePicker.vue +534 -0
- package/src/components/pickers/FilePicker/README.md +1542 -0
- package/src/components/pickers/FilePicker/theme.css +48 -0
- package/src/components/pickers/FilePicker/types.ts +10 -0
- package/src/components/pickers/IconPicker/IconPicker.vue +327 -0
- package/src/components/pickers/IconPicker/README.md +1161 -0
- package/src/components/pickers/IconPicker/theme.css +28 -0
- package/src/components/pickers/theme.css +82 -0
- package/src/components/views/MarkdownViewer/MarkdownViewer.vue +442 -0
- package/src/components/views/MarkdownViewer/README.md +833 -0
- package/src/components/views/MarkdownViewer/theme.css +130 -0
- package/src/index.ts +263 -0
- package/src/theme.ts +378 -0
- package/src/themes/crimson-dark.ts +556 -0
- package/src/themes/cyan-light.ts +556 -0
- package/src/themes/dark.ts +557 -0
- package/src/themes/gold-dark.ts +556 -0
- package/src/themes/grass-dark.ts +556 -0
- package/src/themes/indigo.ts +556 -0
- package/src/themes/light.ts +557 -0
- package/src/themes/orange-dark.ts +557 -0
- package/src/themes/orange-light.ts +557 -0
- package/src/vue.d.ts +45 -0
|
@@ -0,0 +1,1542 @@
|
|
|
1
|
+
# FilePicker
|
|
2
|
+
|
|
3
|
+
A comprehensive file picker component built with Vue 3 Composition API and TypeScript. The FilePicker provides an intuitive drag-and-drop interface for file selection with support for file validation, directory traversal, multiple file handling, and smooth animations using GSAP.
|
|
4
|
+
|
|
5
|
+
## Installation/Import
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { FilePicker } from "@umbra-ui/core";
|
|
9
|
+
import type { FileError, DropState } from "@umbra-ui/core";
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
**Dependencies:**
|
|
13
|
+
|
|
14
|
+
- Vue 3.x
|
|
15
|
+
- GSAP (for animations)
|
|
16
|
+
- @umbra-ui/icons (for icons)
|
|
17
|
+
|
|
18
|
+
## Basic Usage
|
|
19
|
+
|
|
20
|
+
```vue
|
|
21
|
+
<script setup lang="ts">
|
|
22
|
+
import { ref } from "vue";
|
|
23
|
+
import { FilePicker } from "@umbra-ui/core";
|
|
24
|
+
import type { FileError } from "@umbra-ui/core";
|
|
25
|
+
|
|
26
|
+
const selectedFiles = ref<File[]>([]);
|
|
27
|
+
const fileErrors = ref<FileError[]>([]);
|
|
28
|
+
|
|
29
|
+
const handleFilesUpdate = (files: File[]) => {
|
|
30
|
+
console.log("Files updated:", files);
|
|
31
|
+
selectedFiles.value = files;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const handleFileDrop = (files: File[]) => {
|
|
35
|
+
console.log("New files dropped:", files);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const handleFileErrors = (errors: FileError[]) => {
|
|
39
|
+
console.log("File errors:", errors);
|
|
40
|
+
fileErrors.value = errors;
|
|
41
|
+
};
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<template>
|
|
45
|
+
<div class="app">
|
|
46
|
+
<h2>Upload Files</h2>
|
|
47
|
+
|
|
48
|
+
<FilePicker
|
|
49
|
+
v-model:files="selectedFiles"
|
|
50
|
+
accept="image/*,.pdf,.doc,.docx"
|
|
51
|
+
:max-size="5 * 1024 * 1024"
|
|
52
|
+
:max-files="10"
|
|
53
|
+
@update:files="handleFilesUpdate"
|
|
54
|
+
@drop="handleFileDrop"
|
|
55
|
+
@error="handleFileErrors"
|
|
56
|
+
/>
|
|
57
|
+
|
|
58
|
+
<div v-if="selectedFiles.length > 0" class="file-list">
|
|
59
|
+
<h3>Selected Files ({{ selectedFiles.length }})</h3>
|
|
60
|
+
<div v-for="file in selectedFiles" :key="file.name" class="file-item">
|
|
61
|
+
<span class="file-name">{{ file.name }}</span>
|
|
62
|
+
<span class="file-size">{{ formatFileSize(file.size) }}</span>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<div v-if="fileErrors.length > 0" class="error-list">
|
|
67
|
+
<h3>Errors</h3>
|
|
68
|
+
<div v-for="error in fileErrors" :key="error.file" class="error-item">
|
|
69
|
+
<strong>{{ error.file }}:</strong> {{ error.error }}
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</template>
|
|
74
|
+
|
|
75
|
+
<style module>
|
|
76
|
+
.app {
|
|
77
|
+
padding: 24px;
|
|
78
|
+
max-width: 600px;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.file-list {
|
|
82
|
+
margin-top: 20px;
|
|
83
|
+
padding: 16px;
|
|
84
|
+
background: #f8f9fa;
|
|
85
|
+
border-radius: 8px;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.file-item {
|
|
89
|
+
display: flex;
|
|
90
|
+
justify-content: space-between;
|
|
91
|
+
align-items: center;
|
|
92
|
+
padding: 8px 0;
|
|
93
|
+
border-bottom: 1px solid #e9ecef;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.file-item:last-child {
|
|
97
|
+
border-bottom: none;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.file-name {
|
|
101
|
+
font-weight: 500;
|
|
102
|
+
color: #495057;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.file-size {
|
|
106
|
+
color: #6c757d;
|
|
107
|
+
font-size: 14px;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.error-list {
|
|
111
|
+
margin-top: 20px;
|
|
112
|
+
padding: 16px;
|
|
113
|
+
background: #f8d7da;
|
|
114
|
+
border-radius: 8px;
|
|
115
|
+
border: 1px solid #f5c6cb;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.error-item {
|
|
119
|
+
color: #721c24;
|
|
120
|
+
margin-bottom: 8px;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.error-item:last-child {
|
|
124
|
+
margin-bottom: 0;
|
|
125
|
+
}
|
|
126
|
+
</style>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Props
|
|
130
|
+
|
|
131
|
+
| Prop Name | Type | Required | Default | Description |
|
|
132
|
+
| ------------- | -------------------- | -------- | ---------- | ---------------------------------------------------- |
|
|
133
|
+
| `accept` | `string \| string[]` | No | `"*/*"` | Accepted file types (MIME types or extensions) |
|
|
134
|
+
| `multiple` | `boolean` | No | `true` | Whether to allow multiple file selection |
|
|
135
|
+
| `disabled` | `boolean` | No | `false` | Whether the picker is disabled |
|
|
136
|
+
| `maxSize` | `number` | No | `Infinity` | Maximum file size in bytes |
|
|
137
|
+
| `maxFiles` | `number` | No | `Infinity` | Maximum number of files allowed |
|
|
138
|
+
| `recursive` | `boolean` | No | `true` | Whether to traverse directories recursively |
|
|
139
|
+
| `appendFiles` | `boolean` | No | `true` | Whether to append new files or replace existing ones |
|
|
140
|
+
|
|
141
|
+
## Events
|
|
142
|
+
|
|
143
|
+
| Event Name | Payload Type | Description |
|
|
144
|
+
| -------------- | ------------- | ----------------------------------------------- |
|
|
145
|
+
| `update:files` | `File[]` | Emitted when the files list is updated |
|
|
146
|
+
| `error` | `FileError[]` | Emitted when file validation errors occur |
|
|
147
|
+
| `drop` | `File[]` | Emitted when files are dropped (only new files) |
|
|
148
|
+
|
|
149
|
+
## FileError Interface
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
interface FileError {
|
|
153
|
+
file: string;
|
|
154
|
+
error: string;
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### FileError Properties
|
|
159
|
+
|
|
160
|
+
| Property | Type | Description |
|
|
161
|
+
| -------- | -------- | -------------------------------------- |
|
|
162
|
+
| `file` | `string` | Name of the file that caused the error |
|
|
163
|
+
| `error` | `string` | Description of the error |
|
|
164
|
+
|
|
165
|
+
## DropState Interface
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
interface DropState {
|
|
169
|
+
isDragging: boolean;
|
|
170
|
+
isProcessing: boolean;
|
|
171
|
+
dragCount: number;
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### DropState Properties
|
|
176
|
+
|
|
177
|
+
| Property | Type | Description |
|
|
178
|
+
| -------------- | --------- | ------------------------------------------------------------ |
|
|
179
|
+
| `isDragging` | `boolean` | Whether files are currently being dragged over the drop zone |
|
|
180
|
+
| `isProcessing` | `boolean` | Whether files are currently being processed |
|
|
181
|
+
| `dragCount` | `number` | Number of drag enter/leave events (for nested elements) |
|
|
182
|
+
|
|
183
|
+
## Exposed Methods
|
|
184
|
+
|
|
185
|
+
| Method | Parameters | Description |
|
|
186
|
+
| ---------------- | ---------- | ---------------------------------------------- |
|
|
187
|
+
| `openFileDialog` | None | Opens the native file selection dialog |
|
|
188
|
+
| `clearFiles` | None | Clears all selected files |
|
|
189
|
+
| `files` | None | Reactive reference to the current files array |
|
|
190
|
+
| `errors` | None | Reactive reference to the current errors array |
|
|
191
|
+
|
|
192
|
+
## CSS Customization
|
|
193
|
+
|
|
194
|
+
### Layout Variables
|
|
195
|
+
|
|
196
|
+
```css
|
|
197
|
+
.file-picker {
|
|
198
|
+
--filepicker-container-bg: #ffffff;
|
|
199
|
+
--filepicker-container-border: #d1d5db;
|
|
200
|
+
--filepicker-container-hover-bg: #f9fafb;
|
|
201
|
+
--filepicker-container-hover-border: #9ca3af;
|
|
202
|
+
--filepicker-container-active-bg: #e5e7eb;
|
|
203
|
+
--filepicker-container-active-border: #0090ff;
|
|
204
|
+
--filepicker-container-active-text: #000000;
|
|
205
|
+
--filepicker-overlay-bg: rgba(255, 255, 255, 0.9);
|
|
206
|
+
--filepicker-overlay-text: #000000;
|
|
207
|
+
--filepicker-spinner-border: #d1d5db;
|
|
208
|
+
--filepicker-spinner-border-top: #6366f1;
|
|
209
|
+
--filepicker-disabled-opacity: 0.5;
|
|
210
|
+
--filepicker-icon-color: #6b7280;
|
|
211
|
+
--filepicker-title-color: #374151;
|
|
212
|
+
--filepicker-subtitle-color: #6b7280;
|
|
213
|
+
--filepicker-processing-bg: rgba(255, 255, 255, 0.8);
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Container Styling
|
|
218
|
+
|
|
219
|
+
```css
|
|
220
|
+
.file-picker .container {
|
|
221
|
+
position: relative;
|
|
222
|
+
width: 100%;
|
|
223
|
+
min-height: 200px;
|
|
224
|
+
border: 1px dashed var(--filepicker-container-border);
|
|
225
|
+
border-radius: 0.706rem;
|
|
226
|
+
background-color: var(--filepicker-container-bg);
|
|
227
|
+
transition: all 0.2s ease;
|
|
228
|
+
overflow: hidden;
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Overlay Styling
|
|
233
|
+
|
|
234
|
+
```css
|
|
235
|
+
.file-picker .overlay {
|
|
236
|
+
position: absolute;
|
|
237
|
+
inset: 0;
|
|
238
|
+
display: flex;
|
|
239
|
+
align-items: center;
|
|
240
|
+
justify-content: center;
|
|
241
|
+
background-color: var(--filepicker-overlay-bg);
|
|
242
|
+
backdrop-filter: blur(4px);
|
|
243
|
+
opacity: 0;
|
|
244
|
+
scale: 0.95;
|
|
245
|
+
pointer-events: none;
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Examples
|
|
250
|
+
|
|
251
|
+
### Image Uploader
|
|
252
|
+
|
|
253
|
+
```vue
|
|
254
|
+
<script setup lang="ts">
|
|
255
|
+
import { ref, computed } from "vue";
|
|
256
|
+
import { FilePicker } from "@umbra-ui/core";
|
|
257
|
+
import type { FileError } from "@umbra-ui/core";
|
|
258
|
+
|
|
259
|
+
const images = ref<File[]>([]);
|
|
260
|
+
const errors = ref<FileError[]>([]);
|
|
261
|
+
|
|
262
|
+
const totalSize = computed(() => {
|
|
263
|
+
return images.value.reduce((total, file) => total + file.size, 0);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const formatFileSize = (bytes: number): string => {
|
|
267
|
+
if (bytes === 0) return "0 Bytes";
|
|
268
|
+
const k = 1024;
|
|
269
|
+
const sizes = ["Bytes", "KB", "MB", "GB"];
|
|
270
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
271
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const handleImagesUpdate = (files: File[]) => {
|
|
275
|
+
images.value = files;
|
|
276
|
+
console.log("Images updated:", files);
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const handleImageErrors = (fileErrors: FileError[]) => {
|
|
280
|
+
errors.value = fileErrors;
|
|
281
|
+
console.log("Image errors:", fileErrors);
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const removeImage = (index: number) => {
|
|
285
|
+
images.value.splice(index, 1);
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const clearAllImages = () => {
|
|
289
|
+
images.value = [];
|
|
290
|
+
errors.value = [];
|
|
291
|
+
};
|
|
292
|
+
</script>
|
|
293
|
+
|
|
294
|
+
<template>
|
|
295
|
+
<div class="image-uploader">
|
|
296
|
+
<div class="uploader-header">
|
|
297
|
+
<h3>Image Gallery</h3>
|
|
298
|
+
<p>Upload your images (max 5MB each, 10 images total)</p>
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
<FilePicker
|
|
302
|
+
v-model:files="images"
|
|
303
|
+
accept="image/*"
|
|
304
|
+
:max-size="5 * 1024 * 1024"
|
|
305
|
+
:max-files="10"
|
|
306
|
+
@update:files="handleImagesUpdate"
|
|
307
|
+
@error="handleImageErrors"
|
|
308
|
+
>
|
|
309
|
+
<div class="custom-content">
|
|
310
|
+
<div class="upload-icon">📸</div>
|
|
311
|
+
<div class="upload-text">
|
|
312
|
+
<h4>Drop images here or click to browse</h4>
|
|
313
|
+
<p>Supports JPG, PNG, GIF, WebP</p>
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
</FilePicker>
|
|
317
|
+
|
|
318
|
+
<div v-if="images.length > 0" class="image-gallery">
|
|
319
|
+
<div class="gallery-header">
|
|
320
|
+
<h4>Uploaded Images ({{ images.length }})</h4>
|
|
321
|
+
<div class="gallery-stats">
|
|
322
|
+
<span>Total size: {{ formatFileSize(totalSize) }}</span>
|
|
323
|
+
<button @click="clearAllImages" class="clear-btn">Clear All</button>
|
|
324
|
+
</div>
|
|
325
|
+
</div>
|
|
326
|
+
|
|
327
|
+
<div class="image-grid">
|
|
328
|
+
<div
|
|
329
|
+
v-for="(image, index) in images"
|
|
330
|
+
:key="image.name"
|
|
331
|
+
class="image-item"
|
|
332
|
+
>
|
|
333
|
+
<div class="image-preview">
|
|
334
|
+
<img
|
|
335
|
+
:src="URL.createObjectURL(image)"
|
|
336
|
+
:alt="image.name"
|
|
337
|
+
class="preview-img"
|
|
338
|
+
/>
|
|
339
|
+
<button @click="removeImage(index)" class="remove-btn">×</button>
|
|
340
|
+
</div>
|
|
341
|
+
<div class="image-info">
|
|
342
|
+
<p class="image-name">{{ image.name }}</p>
|
|
343
|
+
<p class="image-size">{{ formatFileSize(image.size) }}</p>
|
|
344
|
+
</div>
|
|
345
|
+
</div>
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
348
|
+
|
|
349
|
+
<div v-if="errors.length > 0" class="error-section">
|
|
350
|
+
<h4>Upload Errors</h4>
|
|
351
|
+
<div v-for="error in errors" :key="error.file" class="error-item">
|
|
352
|
+
<strong>{{ error.file }}:</strong> {{ error.error }}
|
|
353
|
+
</div>
|
|
354
|
+
</div>
|
|
355
|
+
</div>
|
|
356
|
+
</template>
|
|
357
|
+
|
|
358
|
+
<style module>
|
|
359
|
+
.image-uploader {
|
|
360
|
+
padding: 24px;
|
|
361
|
+
max-width: 800px;
|
|
362
|
+
background: #ffffff;
|
|
363
|
+
border-radius: 12px;
|
|
364
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
.uploader-header {
|
|
368
|
+
margin-bottom: 24px;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.uploader-header h3 {
|
|
372
|
+
font-size: 24px;
|
|
373
|
+
font-weight: 600;
|
|
374
|
+
color: #1a202c;
|
|
375
|
+
margin: 0 0 8px 0;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.uploader-header p {
|
|
379
|
+
color: #718096;
|
|
380
|
+
margin: 0;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.custom-content {
|
|
384
|
+
display: flex;
|
|
385
|
+
flex-direction: column;
|
|
386
|
+
align-items: center;
|
|
387
|
+
gap: 16px;
|
|
388
|
+
text-align: center;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
.upload-icon {
|
|
392
|
+
font-size: 48px;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
.upload-text h4 {
|
|
396
|
+
font-size: 18px;
|
|
397
|
+
font-weight: 600;
|
|
398
|
+
color: #2d3748;
|
|
399
|
+
margin: 0 0 8px 0;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.upload-text p {
|
|
403
|
+
color: #718096;
|
|
404
|
+
margin: 0;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
.image-gallery {
|
|
408
|
+
margin-top: 32px;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
.gallery-header {
|
|
412
|
+
display: flex;
|
|
413
|
+
justify-content: space-between;
|
|
414
|
+
align-items: center;
|
|
415
|
+
margin-bottom: 20px;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
.gallery-header h4 {
|
|
419
|
+
font-size: 18px;
|
|
420
|
+
font-weight: 500;
|
|
421
|
+
color: #2d3748;
|
|
422
|
+
margin: 0;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
.gallery-stats {
|
|
426
|
+
display: flex;
|
|
427
|
+
align-items: center;
|
|
428
|
+
gap: 16px;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
.gallery-stats span {
|
|
432
|
+
color: #6b7280;
|
|
433
|
+
font-size: 14px;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
.clear-btn {
|
|
437
|
+
padding: 6px 12px;
|
|
438
|
+
background: #ef4444;
|
|
439
|
+
color: white;
|
|
440
|
+
border: none;
|
|
441
|
+
border-radius: 4px;
|
|
442
|
+
font-size: 12px;
|
|
443
|
+
font-weight: 500;
|
|
444
|
+
cursor: pointer;
|
|
445
|
+
transition: background 0.2s ease;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
.clear-btn:hover {
|
|
449
|
+
background: #dc2626;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
.image-grid {
|
|
453
|
+
display: grid;
|
|
454
|
+
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
455
|
+
gap: 16px;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
.image-item {
|
|
459
|
+
border: 1px solid #e5e7eb;
|
|
460
|
+
border-radius: 8px;
|
|
461
|
+
overflow: hidden;
|
|
462
|
+
background: #f9fafb;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
.image-preview {
|
|
466
|
+
position: relative;
|
|
467
|
+
aspect-ratio: 1;
|
|
468
|
+
overflow: hidden;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.preview-img {
|
|
472
|
+
width: 100%;
|
|
473
|
+
height: 100%;
|
|
474
|
+
object-fit: cover;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
.remove-btn {
|
|
478
|
+
position: absolute;
|
|
479
|
+
top: 8px;
|
|
480
|
+
right: 8px;
|
|
481
|
+
width: 24px;
|
|
482
|
+
height: 24px;
|
|
483
|
+
background: rgba(239, 68, 68, 0.9);
|
|
484
|
+
color: white;
|
|
485
|
+
border: none;
|
|
486
|
+
border-radius: 50%;
|
|
487
|
+
font-size: 16px;
|
|
488
|
+
font-weight: bold;
|
|
489
|
+
cursor: pointer;
|
|
490
|
+
display: flex;
|
|
491
|
+
align-items: center;
|
|
492
|
+
justify-content: center;
|
|
493
|
+
transition: background 0.2s ease;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
.remove-btn:hover {
|
|
497
|
+
background: rgba(220, 38, 38, 0.9);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
.image-info {
|
|
501
|
+
padding: 12px;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
.image-name {
|
|
505
|
+
font-size: 14px;
|
|
506
|
+
font-weight: 500;
|
|
507
|
+
color: #374151;
|
|
508
|
+
margin: 0 0 4px 0;
|
|
509
|
+
white-space: nowrap;
|
|
510
|
+
overflow: hidden;
|
|
511
|
+
text-overflow: ellipsis;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
.image-size {
|
|
515
|
+
font-size: 12px;
|
|
516
|
+
color: #6b7280;
|
|
517
|
+
margin: 0;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
.error-section {
|
|
521
|
+
margin-top: 24px;
|
|
522
|
+
padding: 16px;
|
|
523
|
+
background: #fef2f2;
|
|
524
|
+
border: 1px solid #fecaca;
|
|
525
|
+
border-radius: 8px;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
.error-section h4 {
|
|
529
|
+
font-size: 16px;
|
|
530
|
+
font-weight: 500;
|
|
531
|
+
color: #dc2626;
|
|
532
|
+
margin: 0 0 12px 0;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
.error-item {
|
|
536
|
+
color: #991b1b;
|
|
537
|
+
margin-bottom: 8px;
|
|
538
|
+
font-size: 14px;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
.error-item:last-child {
|
|
542
|
+
margin-bottom: 0;
|
|
543
|
+
}
|
|
544
|
+
</style>
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
### Document Uploader
|
|
548
|
+
|
|
549
|
+
```vue
|
|
550
|
+
<script setup lang="ts">
|
|
551
|
+
import { ref } from "vue";
|
|
552
|
+
import { FilePicker } from "@umbra-ui/core";
|
|
553
|
+
import type { FileError } from "@umbra-ui/core";
|
|
554
|
+
|
|
555
|
+
const documents = ref<File[]>([]);
|
|
556
|
+
const errors = ref<FileError[]>([]);
|
|
557
|
+
|
|
558
|
+
const acceptedTypes = [
|
|
559
|
+
"application/pdf",
|
|
560
|
+
"application/msword",
|
|
561
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
562
|
+
"text/plain",
|
|
563
|
+
".pdf",
|
|
564
|
+
".doc",
|
|
565
|
+
".docx",
|
|
566
|
+
".txt",
|
|
567
|
+
];
|
|
568
|
+
|
|
569
|
+
const formatFileSize = (bytes: number): string => {
|
|
570
|
+
if (bytes === 0) return "0 Bytes";
|
|
571
|
+
const k = 1024;
|
|
572
|
+
const sizes = ["Bytes", "KB", "MB", "GB"];
|
|
573
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
574
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
const getFileIcon = (file: File) => {
|
|
578
|
+
if (file.type.includes("pdf")) return "📄";
|
|
579
|
+
if (file.type.includes("word")) return "📝";
|
|
580
|
+
if (file.type.includes("text")) return "📃";
|
|
581
|
+
return "📁";
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
const handleDocumentsUpdate = (files: File[]) => {
|
|
585
|
+
documents.value = files;
|
|
586
|
+
console.log("Documents updated:", files);
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
const handleDocumentErrors = (fileErrors: FileError[]) => {
|
|
590
|
+
errors.value = fileErrors;
|
|
591
|
+
console.log("Document errors:", fileErrors);
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
const removeDocument = (index: number) => {
|
|
595
|
+
documents.value.splice(index, 1);
|
|
596
|
+
};
|
|
597
|
+
</script>
|
|
598
|
+
|
|
599
|
+
<template>
|
|
600
|
+
<div class="document-uploader">
|
|
601
|
+
<div class="uploader-header">
|
|
602
|
+
<h3>Document Upload</h3>
|
|
603
|
+
<p>Upload your documents (PDF, Word, Text files)</p>
|
|
604
|
+
</div>
|
|
605
|
+
|
|
606
|
+
<FilePicker
|
|
607
|
+
v-model:files="documents"
|
|
608
|
+
:accept="acceptedTypes"
|
|
609
|
+
:max-size="10 * 1024 * 1024"
|
|
610
|
+
:max-files="5"
|
|
611
|
+
:multiple="true"
|
|
612
|
+
@update:files="handleDocumentsUpdate"
|
|
613
|
+
@error="handleDocumentErrors"
|
|
614
|
+
>
|
|
615
|
+
<div class="custom-content">
|
|
616
|
+
<div class="upload-icon">📁</div>
|
|
617
|
+
<div class="upload-text">
|
|
618
|
+
<h4>Drop documents here or click to browse</h4>
|
|
619
|
+
<p>Supports PDF, DOC, DOCX, TXT (max 10MB each)</p>
|
|
620
|
+
</div>
|
|
621
|
+
</div>
|
|
622
|
+
</FilePicker>
|
|
623
|
+
|
|
624
|
+
<div v-if="documents.length > 0" class="document-list">
|
|
625
|
+
<h4>Uploaded Documents ({{ documents.length }})</h4>
|
|
626
|
+
<div class="document-items">
|
|
627
|
+
<div
|
|
628
|
+
v-for="(document, index) in documents"
|
|
629
|
+
:key="document.name"
|
|
630
|
+
class="document-item"
|
|
631
|
+
>
|
|
632
|
+
<div class="document-icon">{{ getFileIcon(document) }}</div>
|
|
633
|
+
<div class="document-info">
|
|
634
|
+
<p class="document-name">{{ document.name }}</p>
|
|
635
|
+
<p class="document-details">
|
|
636
|
+
{{ formatFileSize(document.size) }} • {{ document.type }}
|
|
637
|
+
</p>
|
|
638
|
+
</div>
|
|
639
|
+
<button @click="removeDocument(index)" class="remove-btn">×</button>
|
|
640
|
+
</div>
|
|
641
|
+
</div>
|
|
642
|
+
</div>
|
|
643
|
+
|
|
644
|
+
<div v-if="errors.length > 0" class="error-section">
|
|
645
|
+
<h4>Upload Errors</h4>
|
|
646
|
+
<div v-for="error in errors" :key="error.file" class="error-item">
|
|
647
|
+
<strong>{{ error.file }}:</strong> {{ error.error }}
|
|
648
|
+
</div>
|
|
649
|
+
</div>
|
|
650
|
+
</div>
|
|
651
|
+
</template>
|
|
652
|
+
|
|
653
|
+
<style module>
|
|
654
|
+
.document-uploader {
|
|
655
|
+
padding: 24px;
|
|
656
|
+
max-width: 600px;
|
|
657
|
+
background: #ffffff;
|
|
658
|
+
border-radius: 12px;
|
|
659
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
.uploader-header {
|
|
663
|
+
margin-bottom: 24px;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
.uploader-header h3 {
|
|
667
|
+
font-size: 24px;
|
|
668
|
+
font-weight: 600;
|
|
669
|
+
color: #1a202c;
|
|
670
|
+
margin: 0 0 8px 0;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
.uploader-header p {
|
|
674
|
+
color: #718096;
|
|
675
|
+
margin: 0;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
.custom-content {
|
|
679
|
+
display: flex;
|
|
680
|
+
flex-direction: column;
|
|
681
|
+
align-items: center;
|
|
682
|
+
gap: 16px;
|
|
683
|
+
text-align: center;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
.upload-icon {
|
|
687
|
+
font-size: 48px;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
.upload-text h4 {
|
|
691
|
+
font-size: 18px;
|
|
692
|
+
font-weight: 600;
|
|
693
|
+
color: #2d3748;
|
|
694
|
+
margin: 0 0 8px 0;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
.upload-text p {
|
|
698
|
+
color: #718096;
|
|
699
|
+
margin: 0;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
.document-list {
|
|
703
|
+
margin-top: 32px;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
.document-list h4 {
|
|
707
|
+
font-size: 18px;
|
|
708
|
+
font-weight: 500;
|
|
709
|
+
color: #2d3748;
|
|
710
|
+
margin: 0 0 16px 0;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
.document-items {
|
|
714
|
+
display: flex;
|
|
715
|
+
flex-direction: column;
|
|
716
|
+
gap: 12px;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
.document-item {
|
|
720
|
+
display: flex;
|
|
721
|
+
align-items: center;
|
|
722
|
+
gap: 12px;
|
|
723
|
+
padding: 16px;
|
|
724
|
+
background: #f9fafb;
|
|
725
|
+
border: 1px solid #e5e7eb;
|
|
726
|
+
border-radius: 8px;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
.document-icon {
|
|
730
|
+
font-size: 24px;
|
|
731
|
+
width: 40px;
|
|
732
|
+
height: 40px;
|
|
733
|
+
display: flex;
|
|
734
|
+
align-items: center;
|
|
735
|
+
justify-content: center;
|
|
736
|
+
background: #e5e7eb;
|
|
737
|
+
border-radius: 8px;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
.document-info {
|
|
741
|
+
flex: 1;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
.document-name {
|
|
745
|
+
font-size: 14px;
|
|
746
|
+
font-weight: 500;
|
|
747
|
+
color: #374151;
|
|
748
|
+
margin: 0 0 4px 0;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
.document-details {
|
|
752
|
+
font-size: 12px;
|
|
753
|
+
color: #6b7280;
|
|
754
|
+
margin: 0;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
.remove-btn {
|
|
758
|
+
width: 24px;
|
|
759
|
+
height: 24px;
|
|
760
|
+
background: #ef4444;
|
|
761
|
+
color: white;
|
|
762
|
+
border: none;
|
|
763
|
+
border-radius: 50%;
|
|
764
|
+
font-size: 16px;
|
|
765
|
+
font-weight: bold;
|
|
766
|
+
cursor: pointer;
|
|
767
|
+
display: flex;
|
|
768
|
+
align-items: center;
|
|
769
|
+
justify-content: center;
|
|
770
|
+
transition: background 0.2s ease;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
.remove-btn:hover {
|
|
774
|
+
background: #dc2626;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
.error-section {
|
|
778
|
+
margin-top: 24px;
|
|
779
|
+
padding: 16px;
|
|
780
|
+
background: #fef2f2;
|
|
781
|
+
border: 1px solid #fecaca;
|
|
782
|
+
border-radius: 8px;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
.error-section h4 {
|
|
786
|
+
font-size: 16px;
|
|
787
|
+
font-weight: 500;
|
|
788
|
+
color: #dc2626;
|
|
789
|
+
margin: 0 0 12px 0;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
.error-item {
|
|
793
|
+
color: #991b1b;
|
|
794
|
+
margin-bottom: 8px;
|
|
795
|
+
font-size: 14px;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
.error-item:last-child {
|
|
799
|
+
margin-bottom: 0;
|
|
800
|
+
}
|
|
801
|
+
</style>
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
### Single File Upload
|
|
805
|
+
|
|
806
|
+
```vue
|
|
807
|
+
<script setup lang="ts">
|
|
808
|
+
import { ref } from "vue";
|
|
809
|
+
import { FilePicker } from "@umbra-ui/core";
|
|
810
|
+
import type { FileError } from "@umbra-ui/core";
|
|
811
|
+
|
|
812
|
+
const selectedFile = ref<File | null>(null);
|
|
813
|
+
const errors = ref<FileError[]>([]);
|
|
814
|
+
|
|
815
|
+
const formatFileSize = (bytes: number): string => {
|
|
816
|
+
if (bytes === 0) return "0 Bytes";
|
|
817
|
+
const k = 1024;
|
|
818
|
+
const sizes = ["Bytes", "KB", "MB", "GB"];
|
|
819
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
820
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
|
821
|
+
};
|
|
822
|
+
|
|
823
|
+
const handleFileUpdate = (files: File[]) => {
|
|
824
|
+
selectedFile.value = files[0] || null;
|
|
825
|
+
console.log("File updated:", selectedFile.value);
|
|
826
|
+
};
|
|
827
|
+
|
|
828
|
+
const handleFileErrors = (fileErrors: FileError[]) => {
|
|
829
|
+
errors.value = fileErrors;
|
|
830
|
+
console.log("File errors:", fileErrors);
|
|
831
|
+
};
|
|
832
|
+
|
|
833
|
+
const clearFile = () => {
|
|
834
|
+
selectedFile.value = null;
|
|
835
|
+
errors.value = [];
|
|
836
|
+
};
|
|
837
|
+
</script>
|
|
838
|
+
|
|
839
|
+
<template>
|
|
840
|
+
<div class="single-file-uploader">
|
|
841
|
+
<div class="uploader-header">
|
|
842
|
+
<h3>Single File Upload</h3>
|
|
843
|
+
<p>Upload one file at a time</p>
|
|
844
|
+
</div>
|
|
845
|
+
|
|
846
|
+
<FilePicker
|
|
847
|
+
v-model:files="selectedFile ? [selectedFile] : []"
|
|
848
|
+
accept="*/*"
|
|
849
|
+
:max-size="50 * 1024 * 1024"
|
|
850
|
+
:max-files="1"
|
|
851
|
+
:multiple="false"
|
|
852
|
+
:append-files="false"
|
|
853
|
+
@update:files="handleFileUpdate"
|
|
854
|
+
@error="handleFileErrors"
|
|
855
|
+
>
|
|
856
|
+
<div class="custom-content">
|
|
857
|
+
<div class="upload-icon">📎</div>
|
|
858
|
+
<div class="upload-text">
|
|
859
|
+
<h4>Drop a file here or click to browse</h4>
|
|
860
|
+
<p>Any file type, max 50MB</p>
|
|
861
|
+
</div>
|
|
862
|
+
</div>
|
|
863
|
+
</FilePicker>
|
|
864
|
+
|
|
865
|
+
<div v-if="selectedFile" class="file-preview">
|
|
866
|
+
<h4>Selected File</h4>
|
|
867
|
+
<div class="file-card">
|
|
868
|
+
<div class="file-icon">📄</div>
|
|
869
|
+
<div class="file-details">
|
|
870
|
+
<p class="file-name">{{ selectedFile.name }}</p>
|
|
871
|
+
<p class="file-info">
|
|
872
|
+
{{ formatFileSize(selectedFile.size) }} •
|
|
873
|
+
{{ selectedFile.type || "Unknown type" }}
|
|
874
|
+
</p>
|
|
875
|
+
<p class="file-date">
|
|
876
|
+
Last modified:
|
|
877
|
+
{{ new Date(selectedFile.lastModified).toLocaleDateString() }}
|
|
878
|
+
</p>
|
|
879
|
+
</div>
|
|
880
|
+
<button @click="clearFile" class="clear-btn">Clear</button>
|
|
881
|
+
</div>
|
|
882
|
+
</div>
|
|
883
|
+
|
|
884
|
+
<div v-if="errors.length > 0" class="error-section">
|
|
885
|
+
<h4>Upload Error</h4>
|
|
886
|
+
<div v-for="error in errors" :key="error.file" class="error-item">
|
|
887
|
+
<strong>{{ error.file }}:</strong> {{ error.error }}
|
|
888
|
+
</div>
|
|
889
|
+
</div>
|
|
890
|
+
</div>
|
|
891
|
+
</template>
|
|
892
|
+
|
|
893
|
+
<style module>
|
|
894
|
+
.single-file-uploader {
|
|
895
|
+
padding: 24px;
|
|
896
|
+
max-width: 500px;
|
|
897
|
+
background: #ffffff;
|
|
898
|
+
border-radius: 12px;
|
|
899
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
.uploader-header {
|
|
903
|
+
margin-bottom: 24px;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
.uploader-header h3 {
|
|
907
|
+
font-size: 24px;
|
|
908
|
+
font-weight: 600;
|
|
909
|
+
color: #1a202c;
|
|
910
|
+
margin: 0 0 8px 0;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
.uploader-header p {
|
|
914
|
+
color: #718096;
|
|
915
|
+
margin: 0;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
.custom-content {
|
|
919
|
+
display: flex;
|
|
920
|
+
flex-direction: column;
|
|
921
|
+
align-items: center;
|
|
922
|
+
gap: 16px;
|
|
923
|
+
text-align: center;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
.upload-icon {
|
|
927
|
+
font-size: 48px;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
.upload-text h4 {
|
|
931
|
+
font-size: 18px;
|
|
932
|
+
font-weight: 600;
|
|
933
|
+
color: #2d3748;
|
|
934
|
+
margin: 0 0 8px 0;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
.upload-text p {
|
|
938
|
+
color: #718096;
|
|
939
|
+
margin: 0;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
.file-preview {
|
|
943
|
+
margin-top: 32px;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
.file-preview h4 {
|
|
947
|
+
font-size: 18px;
|
|
948
|
+
font-weight: 500;
|
|
949
|
+
color: #2d3748;
|
|
950
|
+
margin: 0 0 16px 0;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
.file-card {
|
|
954
|
+
display: flex;
|
|
955
|
+
align-items: center;
|
|
956
|
+
gap: 16px;
|
|
957
|
+
padding: 20px;
|
|
958
|
+
background: #f9fafb;
|
|
959
|
+
border: 1px solid #e5e7eb;
|
|
960
|
+
border-radius: 8px;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
.file-icon {
|
|
964
|
+
font-size: 32px;
|
|
965
|
+
width: 60px;
|
|
966
|
+
height: 60px;
|
|
967
|
+
display: flex;
|
|
968
|
+
align-items: center;
|
|
969
|
+
justify-content: center;
|
|
970
|
+
background: #e5e7eb;
|
|
971
|
+
border-radius: 12px;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
.file-details {
|
|
975
|
+
flex: 1;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
.file-name {
|
|
979
|
+
font-size: 16px;
|
|
980
|
+
font-weight: 600;
|
|
981
|
+
color: #374151;
|
|
982
|
+
margin: 0 0 8px 0;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
.file-info {
|
|
986
|
+
font-size: 14px;
|
|
987
|
+
color: #6b7280;
|
|
988
|
+
margin: 0 0 4px 0;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
.file-date {
|
|
992
|
+
font-size: 12px;
|
|
993
|
+
color: #9ca3af;
|
|
994
|
+
margin: 0;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
.clear-btn {
|
|
998
|
+
padding: 8px 16px;
|
|
999
|
+
background: #ef4444;
|
|
1000
|
+
color: white;
|
|
1001
|
+
border: none;
|
|
1002
|
+
border-radius: 6px;
|
|
1003
|
+
font-size: 14px;
|
|
1004
|
+
font-weight: 500;
|
|
1005
|
+
cursor: pointer;
|
|
1006
|
+
transition: background 0.2s ease;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
.clear-btn:hover {
|
|
1010
|
+
background: #dc2626;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
.error-section {
|
|
1014
|
+
margin-top: 24px;
|
|
1015
|
+
padding: 16px;
|
|
1016
|
+
background: #fef2f2;
|
|
1017
|
+
border: 1px solid #fecaca;
|
|
1018
|
+
border-radius: 8px;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
.error-section h4 {
|
|
1022
|
+
font-size: 16px;
|
|
1023
|
+
font-weight: 500;
|
|
1024
|
+
color: #dc2626;
|
|
1025
|
+
margin: 0 0 12px 0;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
.error-item {
|
|
1029
|
+
color: #991b1b;
|
|
1030
|
+
margin-bottom: 8px;
|
|
1031
|
+
font-size: 14px;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
.error-item:last-child {
|
|
1035
|
+
margin-bottom: 0;
|
|
1036
|
+
}
|
|
1037
|
+
</style>
|
|
1038
|
+
```
|
|
1039
|
+
|
|
1040
|
+
### Custom Overlay
|
|
1041
|
+
|
|
1042
|
+
```vue
|
|
1043
|
+
<script setup lang="ts">
|
|
1044
|
+
import { ref } from "vue";
|
|
1045
|
+
import { FilePicker } from "@umbra-ui/core";
|
|
1046
|
+
import type { FileError } from "@umbra-ui/core";
|
|
1047
|
+
|
|
1048
|
+
const files = ref<File[]>([]);
|
|
1049
|
+
const errors = ref<FileError[]>([]);
|
|
1050
|
+
|
|
1051
|
+
const handleFilesUpdate = (newFiles: File[]) => {
|
|
1052
|
+
files.value = newFiles;
|
|
1053
|
+
console.log("Files updated:", newFiles);
|
|
1054
|
+
};
|
|
1055
|
+
|
|
1056
|
+
const handleFileErrors = (fileErrors: FileError[]) => {
|
|
1057
|
+
errors.value = fileErrors;
|
|
1058
|
+
console.log("File errors:", fileErrors);
|
|
1059
|
+
};
|
|
1060
|
+
</script>
|
|
1061
|
+
|
|
1062
|
+
<template>
|
|
1063
|
+
<div class="custom-overlay-uploader">
|
|
1064
|
+
<div class="uploader-header">
|
|
1065
|
+
<h3>Custom Overlay Uploader</h3>
|
|
1066
|
+
<p>Drag and drop with custom overlay animation</p>
|
|
1067
|
+
</div>
|
|
1068
|
+
|
|
1069
|
+
<FilePicker
|
|
1070
|
+
v-model:files="files"
|
|
1071
|
+
accept="image/*,video/*"
|
|
1072
|
+
:max-size="100 * 1024 * 1024"
|
|
1073
|
+
:max-files="20"
|
|
1074
|
+
@update:files="handleFilesUpdate"
|
|
1075
|
+
@error="handleFileErrors"
|
|
1076
|
+
>
|
|
1077
|
+
<div class="custom-content">
|
|
1078
|
+
<div class="upload-icon">🎬</div>
|
|
1079
|
+
<div class="upload-text">
|
|
1080
|
+
<h4>Drop media files here</h4>
|
|
1081
|
+
<p>Images and videos up to 100MB each</p>
|
|
1082
|
+
</div>
|
|
1083
|
+
</div>
|
|
1084
|
+
|
|
1085
|
+
<template #overlay>
|
|
1086
|
+
<div class="custom-overlay">
|
|
1087
|
+
<div class="overlay-icon">⬇️</div>
|
|
1088
|
+
<div class="overlay-text">
|
|
1089
|
+
<h4>Release to upload</h4>
|
|
1090
|
+
<p>Drop your files now</p>
|
|
1091
|
+
</div>
|
|
1092
|
+
</div>
|
|
1093
|
+
</template>
|
|
1094
|
+
|
|
1095
|
+
<template #processing>
|
|
1096
|
+
<div class="custom-processing">
|
|
1097
|
+
<div class="processing-spinner"></div>
|
|
1098
|
+
<p>Processing files...</p>
|
|
1099
|
+
</div>
|
|
1100
|
+
</template>
|
|
1101
|
+
</FilePicker>
|
|
1102
|
+
|
|
1103
|
+
<div v-if="files.length > 0" class="files-summary">
|
|
1104
|
+
<h4>Uploaded Files ({{ files.length }})</h4>
|
|
1105
|
+
<div class="files-grid">
|
|
1106
|
+
<div v-for="file in files" :key="file.name" class="file-item">
|
|
1107
|
+
<div class="file-icon">
|
|
1108
|
+
{{ file.type.startsWith("image/") ? "🖼️" : "🎥" }}
|
|
1109
|
+
</div>
|
|
1110
|
+
<div class="file-info">
|
|
1111
|
+
<p class="file-name">{{ file.name }}</p>
|
|
1112
|
+
<p class="file-size">
|
|
1113
|
+
{{ (file.size / 1024 / 1024).toFixed(2) }} MB
|
|
1114
|
+
</p>
|
|
1115
|
+
</div>
|
|
1116
|
+
</div>
|
|
1117
|
+
</div>
|
|
1118
|
+
</div>
|
|
1119
|
+
</div>
|
|
1120
|
+
</template>
|
|
1121
|
+
|
|
1122
|
+
<style module>
|
|
1123
|
+
.custom-overlay-uploader {
|
|
1124
|
+
padding: 24px;
|
|
1125
|
+
max-width: 700px;
|
|
1126
|
+
background: #ffffff;
|
|
1127
|
+
border-radius: 12px;
|
|
1128
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
.uploader-header {
|
|
1132
|
+
margin-bottom: 24px;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
.uploader-header h3 {
|
|
1136
|
+
font-size: 24px;
|
|
1137
|
+
font-weight: 600;
|
|
1138
|
+
color: #1a202c;
|
|
1139
|
+
margin: 0 0 8px 0;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
.uploader-header p {
|
|
1143
|
+
color: #718096;
|
|
1144
|
+
margin: 0;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
.custom-content {
|
|
1148
|
+
display: flex;
|
|
1149
|
+
flex-direction: column;
|
|
1150
|
+
align-items: center;
|
|
1151
|
+
gap: 16px;
|
|
1152
|
+
text-align: center;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
.upload-icon {
|
|
1156
|
+
font-size: 48px;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
.upload-text h4 {
|
|
1160
|
+
font-size: 18px;
|
|
1161
|
+
font-weight: 600;
|
|
1162
|
+
color: #2d3748;
|
|
1163
|
+
margin: 0 0 8px 0;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
.upload-text p {
|
|
1167
|
+
color: #718096;
|
|
1168
|
+
margin: 0;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
.custom-overlay {
|
|
1172
|
+
display: flex;
|
|
1173
|
+
flex-direction: column;
|
|
1174
|
+
align-items: center;
|
|
1175
|
+
gap: 16px;
|
|
1176
|
+
text-align: center;
|
|
1177
|
+
background: rgba(59, 130, 246, 0.1);
|
|
1178
|
+
border: 2px dashed #3b82f6;
|
|
1179
|
+
border-radius: 12px;
|
|
1180
|
+
padding: 40px;
|
|
1181
|
+
margin: 20px;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
.overlay-icon {
|
|
1185
|
+
font-size: 48px;
|
|
1186
|
+
animation: bounce 1s infinite;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
.overlay-text h4 {
|
|
1190
|
+
font-size: 20px;
|
|
1191
|
+
font-weight: 600;
|
|
1192
|
+
color: #1e40af;
|
|
1193
|
+
margin: 0 0 8px 0;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
.overlay-text p {
|
|
1197
|
+
color: #3b82f6;
|
|
1198
|
+
margin: 0;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
@keyframes bounce {
|
|
1202
|
+
0%,
|
|
1203
|
+
20%,
|
|
1204
|
+
50%,
|
|
1205
|
+
80%,
|
|
1206
|
+
100% {
|
|
1207
|
+
transform: translateY(0);
|
|
1208
|
+
}
|
|
1209
|
+
40% {
|
|
1210
|
+
transform: translateY(-10px);
|
|
1211
|
+
}
|
|
1212
|
+
60% {
|
|
1213
|
+
transform: translateY(-5px);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
.custom-processing {
|
|
1218
|
+
display: flex;
|
|
1219
|
+
flex-direction: column;
|
|
1220
|
+
align-items: center;
|
|
1221
|
+
gap: 16px;
|
|
1222
|
+
text-align: center;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
.processing-spinner {
|
|
1226
|
+
width: 40px;
|
|
1227
|
+
height: 40px;
|
|
1228
|
+
border: 4px solid #e5e7eb;
|
|
1229
|
+
border-top-color: #3b82f6;
|
|
1230
|
+
border-radius: 50%;
|
|
1231
|
+
animation: spin 1s linear infinite;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
@keyframes spin {
|
|
1235
|
+
to {
|
|
1236
|
+
transform: rotate(360deg);
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
.custom-processing p {
|
|
1241
|
+
color: #6b7280;
|
|
1242
|
+
font-weight: 500;
|
|
1243
|
+
margin: 0;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
.files-summary {
|
|
1247
|
+
margin-top: 32px;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
.files-summary h4 {
|
|
1251
|
+
font-size: 18px;
|
|
1252
|
+
font-weight: 500;
|
|
1253
|
+
color: #2d3748;
|
|
1254
|
+
margin: 0 0 16px 0;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
.files-grid {
|
|
1258
|
+
display: grid;
|
|
1259
|
+
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
|
1260
|
+
gap: 12px;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
.file-item {
|
|
1264
|
+
display: flex;
|
|
1265
|
+
flex-direction: column;
|
|
1266
|
+
align-items: center;
|
|
1267
|
+
gap: 8px;
|
|
1268
|
+
padding: 16px;
|
|
1269
|
+
background: #f9fafb;
|
|
1270
|
+
border: 1px solid #e5e7eb;
|
|
1271
|
+
border-radius: 8px;
|
|
1272
|
+
text-align: center;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
.file-icon {
|
|
1276
|
+
font-size: 24px;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
.file-info {
|
|
1280
|
+
width: 100%;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
.file-name {
|
|
1284
|
+
font-size: 12px;
|
|
1285
|
+
font-weight: 500;
|
|
1286
|
+
color: #374151;
|
|
1287
|
+
margin: 0 0 4px 0;
|
|
1288
|
+
white-space: nowrap;
|
|
1289
|
+
overflow: hidden;
|
|
1290
|
+
text-overflow: ellipsis;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
.file-size {
|
|
1294
|
+
font-size: 11px;
|
|
1295
|
+
color: #6b7280;
|
|
1296
|
+
margin: 0;
|
|
1297
|
+
}
|
|
1298
|
+
</style>
|
|
1299
|
+
```
|
|
1300
|
+
|
|
1301
|
+
## Advanced Usage
|
|
1302
|
+
|
|
1303
|
+
### Programmatic Control
|
|
1304
|
+
|
|
1305
|
+
```vue
|
|
1306
|
+
<script setup lang="ts">
|
|
1307
|
+
import { ref } from "vue";
|
|
1308
|
+
import { FilePicker } from "@umbra-ui/core";
|
|
1309
|
+
import type { FileError } from "@umbra-ui/core";
|
|
1310
|
+
|
|
1311
|
+
const filePickerRef = ref<InstanceType<typeof FilePicker>>();
|
|
1312
|
+
const files = ref<File[]>([]);
|
|
1313
|
+
const errors = ref<FileError[]>([]);
|
|
1314
|
+
|
|
1315
|
+
const handleFilesUpdate = (newFiles: File[]) => {
|
|
1316
|
+
files.value = newFiles;
|
|
1317
|
+
};
|
|
1318
|
+
|
|
1319
|
+
const handleFileErrors = (fileErrors: FileError[]) => {
|
|
1320
|
+
errors.value = fileErrors;
|
|
1321
|
+
};
|
|
1322
|
+
|
|
1323
|
+
const openFileDialog = () => {
|
|
1324
|
+
filePickerRef.value?.openFileDialog();
|
|
1325
|
+
};
|
|
1326
|
+
|
|
1327
|
+
const clearAllFiles = () => {
|
|
1328
|
+
filePickerRef.value?.clearFiles();
|
|
1329
|
+
};
|
|
1330
|
+
|
|
1331
|
+
const getCurrentFiles = () => {
|
|
1332
|
+
return filePickerRef.value?.files || [];
|
|
1333
|
+
};
|
|
1334
|
+
|
|
1335
|
+
const getCurrentErrors = () => {
|
|
1336
|
+
return filePickerRef.value?.errors || [];
|
|
1337
|
+
};
|
|
1338
|
+
</script>
|
|
1339
|
+
|
|
1340
|
+
<template>
|
|
1341
|
+
<div class="programmatic-control">
|
|
1342
|
+
<div class="control-header">
|
|
1343
|
+
<h3>Programmatic Control</h3>
|
|
1344
|
+
<p>Control the file picker programmatically</p>
|
|
1345
|
+
</div>
|
|
1346
|
+
|
|
1347
|
+
<div class="control-buttons">
|
|
1348
|
+
<button @click="openFileDialog" class="control-btn">
|
|
1349
|
+
Open File Dialog
|
|
1350
|
+
</button>
|
|
1351
|
+
<button @click="clearAllFiles" class="control-btn">
|
|
1352
|
+
Clear All Files
|
|
1353
|
+
</button>
|
|
1354
|
+
<button @click="getCurrentFiles" class="control-btn">
|
|
1355
|
+
Get Current Files
|
|
1356
|
+
</button>
|
|
1357
|
+
<button @click="getCurrentErrors" class="control-btn">
|
|
1358
|
+
Get Current Errors
|
|
1359
|
+
</button>
|
|
1360
|
+
</div>
|
|
1361
|
+
|
|
1362
|
+
<FilePicker
|
|
1363
|
+
ref="filePickerRef"
|
|
1364
|
+
v-model:files="files"
|
|
1365
|
+
accept="*/*"
|
|
1366
|
+
:max-size="10 * 1024 * 1024"
|
|
1367
|
+
:max-files="5"
|
|
1368
|
+
@update:files="handleFilesUpdate"
|
|
1369
|
+
@error="handleFileErrors"
|
|
1370
|
+
/>
|
|
1371
|
+
|
|
1372
|
+
<div v-if="files.length > 0" class="files-info">
|
|
1373
|
+
<h4>Current Files ({{ files.length }})</h4>
|
|
1374
|
+
<ul>
|
|
1375
|
+
<li v-for="file in files" :key="file.name">
|
|
1376
|
+
{{ file.name }} ({{ (file.size / 1024).toFixed(2) }} KB)
|
|
1377
|
+
</li>
|
|
1378
|
+
</ul>
|
|
1379
|
+
</div>
|
|
1380
|
+
|
|
1381
|
+
<div v-if="errors.length > 0" class="errors-info">
|
|
1382
|
+
<h4>Current Errors ({{ errors.length }})</h4>
|
|
1383
|
+
<ul>
|
|
1384
|
+
<li v-for="error in errors" :key="error.file">
|
|
1385
|
+
{{ error.file }}: {{ error.error }}
|
|
1386
|
+
</li>
|
|
1387
|
+
</ul>
|
|
1388
|
+
</div>
|
|
1389
|
+
</div>
|
|
1390
|
+
</template>
|
|
1391
|
+
|
|
1392
|
+
<style module>
|
|
1393
|
+
.programmatic-control {
|
|
1394
|
+
padding: 24px;
|
|
1395
|
+
max-width: 600px;
|
|
1396
|
+
background: #ffffff;
|
|
1397
|
+
border-radius: 12px;
|
|
1398
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
.control-header {
|
|
1402
|
+
margin-bottom: 24px;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
.control-header h3 {
|
|
1406
|
+
font-size: 24px;
|
|
1407
|
+
font-weight: 600;
|
|
1408
|
+
color: #1a202c;
|
|
1409
|
+
margin: 0 0 8px 0;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
.control-header p {
|
|
1413
|
+
color: #718096;
|
|
1414
|
+
margin: 0;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
.control-buttons {
|
|
1418
|
+
display: flex;
|
|
1419
|
+
flex-wrap: wrap;
|
|
1420
|
+
gap: 12px;
|
|
1421
|
+
margin-bottom: 24px;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
.control-btn {
|
|
1425
|
+
padding: 8px 16px;
|
|
1426
|
+
background: #3b82f6;
|
|
1427
|
+
color: white;
|
|
1428
|
+
border: none;
|
|
1429
|
+
border-radius: 6px;
|
|
1430
|
+
font-size: 14px;
|
|
1431
|
+
font-weight: 500;
|
|
1432
|
+
cursor: pointer;
|
|
1433
|
+
transition: background 0.2s ease;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
.control-btn:hover {
|
|
1437
|
+
background: #2563eb;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
.files-info,
|
|
1441
|
+
.errors-info {
|
|
1442
|
+
margin-top: 24px;
|
|
1443
|
+
padding: 16px;
|
|
1444
|
+
background: #f9fafb;
|
|
1445
|
+
border-radius: 8px;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
.files-info h4,
|
|
1449
|
+
.errors-info h4 {
|
|
1450
|
+
font-size: 16px;
|
|
1451
|
+
font-weight: 500;
|
|
1452
|
+
color: #2d3748;
|
|
1453
|
+
margin: 0 0 12px 0;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
.files-info ul,
|
|
1457
|
+
.errors-info ul {
|
|
1458
|
+
margin: 0;
|
|
1459
|
+
padding-left: 20px;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
.files-info li,
|
|
1463
|
+
.errors-info li {
|
|
1464
|
+
color: #4a5568;
|
|
1465
|
+
margin-bottom: 4px;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
.files-info li:last-child,
|
|
1469
|
+
.errors-info li:last-child {
|
|
1470
|
+
margin-bottom: 0;
|
|
1471
|
+
}
|
|
1472
|
+
</style>
|
|
1473
|
+
```
|
|
1474
|
+
|
|
1475
|
+
## Performance Considerations
|
|
1476
|
+
|
|
1477
|
+
- **GSAP Animations**: Smooth drag-and-drop animations with optimized performance
|
|
1478
|
+
- **File Processing**: Asynchronous file processing with proper error handling
|
|
1479
|
+
- **Memory Management**: Efficient file handling with proper cleanup
|
|
1480
|
+
- **Directory Traversal**: Recursive directory processing with depth control
|
|
1481
|
+
- **Validation**: Client-side validation before file processing
|
|
1482
|
+
|
|
1483
|
+
## Accessibility
|
|
1484
|
+
|
|
1485
|
+
- **Keyboard Navigation**: Support for keyboard interaction
|
|
1486
|
+
- **Screen Reader Support**: Proper ARIA labels and semantic structure
|
|
1487
|
+
- **Focus Management**: Focus is properly managed during interactions
|
|
1488
|
+
- **Drag and Drop**: Accessible drag-and-drop with proper feedback
|
|
1489
|
+
- **Error Reporting**: Clear error messages for validation failures
|
|
1490
|
+
|
|
1491
|
+
## Browser Support
|
|
1492
|
+
|
|
1493
|
+
- **Modern Browsers**: Chrome 88+, Firefox 85+, Safari 14+, Edge 88+
|
|
1494
|
+
- **Mobile Browsers**: iOS Safari 14+, Chrome Mobile 88+
|
|
1495
|
+
- **File API**: Requires File API support for drag-and-drop
|
|
1496
|
+
- **Directory API**: Requires File System Access API for directory traversal
|
|
1497
|
+
|
|
1498
|
+
## Troubleshooting
|
|
1499
|
+
|
|
1500
|
+
### Common Issues
|
|
1501
|
+
|
|
1502
|
+
1. **Files not uploading**: Check file size limits and accepted types
|
|
1503
|
+
2. **Drag and drop not working**: Ensure proper event handling and browser support
|
|
1504
|
+
3. **Directory traversal issues**: Verify File System Access API support
|
|
1505
|
+
4. **Animation not smooth**: Check GSAP installation and configuration
|
|
1506
|
+
|
|
1507
|
+
### Debug Mode
|
|
1508
|
+
|
|
1509
|
+
```vue
|
|
1510
|
+
<script setup lang="ts">
|
|
1511
|
+
import { ref, watch } from "vue";
|
|
1512
|
+
import { FilePicker } from "@umbra-ui/core";
|
|
1513
|
+
|
|
1514
|
+
const files = ref<File[]>([]);
|
|
1515
|
+
const errors = ref<FileError[]>([]);
|
|
1516
|
+
|
|
1517
|
+
// Watch for changes
|
|
1518
|
+
watch(files, (newFiles) => {
|
|
1519
|
+
console.log("Files changed:", newFiles);
|
|
1520
|
+
});
|
|
1521
|
+
|
|
1522
|
+
watch(errors, (newErrors) => {
|
|
1523
|
+
console.log("Errors changed:", newErrors);
|
|
1524
|
+
});
|
|
1525
|
+
</script>
|
|
1526
|
+
```
|
|
1527
|
+
|
|
1528
|
+
## Migration Guide
|
|
1529
|
+
|
|
1530
|
+
### From v1 to v2
|
|
1531
|
+
|
|
1532
|
+
- `v-model` prop is now `v-model:files`
|
|
1533
|
+
- Event names have been updated to use kebab-case
|
|
1534
|
+
- File validation system has been improved
|
|
1535
|
+
- GSAP integration has been enhanced
|
|
1536
|
+
|
|
1537
|
+
### Breaking Changes
|
|
1538
|
+
|
|
1539
|
+
- Removed `value` prop in favor of `v-model:files`
|
|
1540
|
+
- Changed event names from camelCase to kebab-case
|
|
1541
|
+
- Updated file prop to use File[] array instead of single File
|
|
1542
|
+
- Modified CSS class naming convention
|