twintrinsic 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.
- package/LICENSE +674 -0
- package/README.md +150 -0
- package/dist/App/App.svelte +54 -0
- package/dist/App/App.svelte.d.ts +65 -0
- package/dist/Section.svelte +25 -0
- package/dist/Section.svelte.d.ts +34 -0
- package/dist/actions/clickOutside.d.ts +9 -0
- package/dist/actions/clickOutside.js +19 -0
- package/dist/actions/index.d.ts +1 -0
- package/dist/actions/index.js +1 -0
- package/dist/components/Accordion/Accordion.svelte +75 -0
- package/dist/components/Accordion/Accordion.svelte.d.ts +39 -0
- package/dist/components/Accordion/AccordionItem.svelte +150 -0
- package/dist/components/Accordion/AccordionItem.svelte.d.ts +30 -0
- package/dist/components/App/App.story.md +8 -0
- package/dist/components/App/App.story.svelte +170 -0
- package/dist/components/App/App.story.svelte.d.ts +22 -0
- package/dist/components/App/App.svelte +77 -0
- package/dist/components/App/App.svelte.d.ts +66 -0
- package/dist/components/App/Split.svelte +346 -0
- package/dist/components/App/Split.svelte.d.ts +54 -0
- package/dist/components/App/index.d.ts +2 -0
- package/dist/components/App/index.js +3 -0
- package/dist/components/AppHeader/AppHeader.svelte +439 -0
- package/dist/components/AppHeader/AppHeader.svelte.d.ts +24 -0
- package/dist/components/Avatar/Avatar.svelte +300 -0
- package/dist/components/Avatar/Avatar.svelte.d.ts +48 -0
- package/dist/components/Avatar/AvatarGroup.svelte +185 -0
- package/dist/components/Avatar/AvatarGroup.svelte.d.ts +46 -0
- package/dist/components/Badge/Badge.svelte +186 -0
- package/dist/components/Badge/Badge.svelte.d.ts +51 -0
- package/dist/components/BottomBar/BottomBar.svelte +146 -0
- package/dist/components/BottomBar/BottomBar.svelte.d.ts +38 -0
- package/dist/components/Breadcrumb/Breadcrumb.svelte +77 -0
- package/dist/components/Breadcrumb/Breadcrumb.svelte.d.ts +42 -0
- package/dist/components/Breadcrumb/BreadcrumbItem.svelte +171 -0
- package/dist/components/Breadcrumb/BreadcrumbItem.svelte.d.ts +38 -0
- package/dist/components/Button/Button.svelte +252 -0
- package/dist/components/Button/Button.svelte.d.ts +80 -0
- package/dist/components/Button/ButtonGroup.svelte +127 -0
- package/dist/components/Button/ButtonGroup.svelte.d.ts +44 -0
- package/dist/components/Card/Card.svelte +152 -0
- package/dist/components/Card/Card.svelte.d.ts +55 -0
- package/dist/components/Carousel/Carousel.svelte +461 -0
- package/dist/components/Carousel/Carousel.svelte.d.ts +79 -0
- package/dist/components/Carousel/CarouselItem.svelte +149 -0
- package/dist/components/Carousel/CarouselItem.svelte.d.ts +35 -0
- package/dist/components/Chip/Chip.svelte +288 -0
- package/dist/components/Chip/Chip.svelte.d.ts +71 -0
- package/dist/components/Chip/ChipGroup.svelte +190 -0
- package/dist/components/Chip/ChipGroup.svelte.d.ts +71 -0
- package/dist/components/CodeBlock/CodeBlock.svelte +356 -0
- package/dist/components/CodeBlock/CodeBlock.svelte.d.ts +44 -0
- package/dist/components/CodeBlock/index.d.ts +1 -0
- package/dist/components/CodeBlock/index.js +1 -0
- package/dist/components/CodeBlockSpeed/CodeBlockSpeed.svelte +145 -0
- package/dist/components/CodeBlockSpeed/CodeBlockSpeed.svelte.d.ts +44 -0
- package/dist/components/CodeEditor/CodeEditor.svelte +229 -0
- package/dist/components/CodeEditor/CodeEditor.svelte.d.ts +23 -0
- package/dist/components/Combobox/Combobox.svelte +279 -0
- package/dist/components/Combobox/Combobox.svelte.d.ts +34 -0
- package/dist/components/Container/Container.svelte +45 -0
- package/dist/components/Container/Container.svelte.d.ts +36 -0
- package/dist/components/DataTable/DataTable.svelte +879 -0
- package/dist/components/DataTable/DataTable.svelte.d.ts +102 -0
- package/dist/components/Form/AutoComplete.svelte +357 -0
- package/dist/components/Form/AutoComplete.svelte.d.ts +73 -0
- package/dist/components/Form/Calendar.svelte +429 -0
- package/dist/components/Form/Calendar.svelte.d.ts +53 -0
- package/dist/components/Form/Checkbox.svelte +196 -0
- package/dist/components/Form/Checkbox.svelte.d.ts +50 -0
- package/dist/components/Form/ColorPicker.svelte +396 -0
- package/dist/components/Form/ColorPicker.svelte.d.ts +43 -0
- package/dist/components/Form/Combobox.svelte +645 -0
- package/dist/components/Form/Combobox.svelte.d.ts +93 -0
- package/dist/components/Form/Dropdown.svelte +773 -0
- package/dist/components/Form/Dropdown.svelte.d.ts +81 -0
- package/dist/components/Form/FileUpload.svelte +796 -0
- package/dist/components/Form/FileUpload.svelte.d.ts +78 -0
- package/dist/components/Form/FloatLabel.svelte +245 -0
- package/dist/components/Form/FloatLabel.svelte.d.ts +44 -0
- package/dist/components/Form/Form.svelte +281 -0
- package/dist/components/Form/Form.svelte.d.ts +54 -0
- package/dist/components/Form/FormField.svelte +218 -0
- package/dist/components/Form/FormField.svelte.d.ts +47 -0
- package/dist/components/Form/Input.svelte +340 -0
- package/dist/components/Form/Input.svelte.d.ts +79 -0
- package/dist/components/Form/InputSwitch.svelte +189 -0
- package/dist/components/Form/InputSwitch.svelte.d.ts +46 -0
- package/dist/components/Form/InvalidState.svelte +97 -0
- package/dist/components/Form/InvalidState.svelte.d.ts +37 -0
- package/dist/components/Form/Knob.svelte +537 -0
- package/dist/components/Form/Knob.svelte.d.ts +78 -0
- package/dist/components/Form/ListInput.svelte +469 -0
- package/dist/components/Form/ListInput.svelte.d.ts +70 -0
- package/dist/components/Form/Listbox.svelte +513 -0
- package/dist/components/Form/Listbox.svelte.d.ts +74 -0
- package/dist/components/Form/NumberInput.svelte +452 -0
- package/dist/components/Form/NumberInput.svelte.d.ts +82 -0
- package/dist/components/Form/Radio.svelte +192 -0
- package/dist/components/Form/Radio.svelte.d.ts +53 -0
- package/dist/components/Form/RadioGroup.svelte +155 -0
- package/dist/components/Form/RadioGroup.svelte.d.ts +48 -0
- package/dist/components/Form/Rating.svelte +380 -0
- package/dist/components/Form/Rating.svelte.d.ts +64 -0
- package/dist/components/Form/Select.svelte +436 -0
- package/dist/components/Form/Select.svelte.d.ts +49 -0
- package/dist/components/Form/SelectGroup.svelte +34 -0
- package/dist/components/Form/SelectGroup.svelte.d.ts +33 -0
- package/dist/components/Form/Slider.svelte +622 -0
- package/dist/components/Form/Slider.svelte.d.ts +73 -0
- package/dist/components/Form/Switch.svelte +192 -0
- package/dist/components/Form/Switch.svelte.d.ts +46 -0
- package/dist/components/Form/TextInput.svelte +274 -0
- package/dist/components/Form/TextInput.svelte.d.ts +74 -0
- package/dist/components/Form/Textarea.svelte +207 -0
- package/dist/components/Form/Textarea.svelte.d.ts +62 -0
- package/dist/components/Icon/Icon.svelte +140 -0
- package/dist/components/Icon/Icon.svelte.d.ts +25 -0
- package/dist/components/Icon/index.d.ts +1 -0
- package/dist/components/Icon/index.js +1 -0
- package/dist/components/Lazy/Lazy.svelte +158 -0
- package/dist/components/Lazy/Lazy.svelte.d.ts +42 -0
- package/dist/components/Masonry/Masonry.svelte +299 -0
- package/dist/components/Masonry/Masonry.svelte.d.ts +55 -0
- package/dist/components/Menu/Menu/Menu.svelte +65 -0
- package/dist/components/Menu/Menu/Menu.svelte.d.ts +17 -0
- package/dist/components/Menu/Menu/MenuItem.svelte +90 -0
- package/dist/components/Menu/Menu/MenuItem.svelte.d.ts +27 -0
- package/dist/components/Modal/Modal.svelte +334 -0
- package/dist/components/Modal/Modal.svelte.d.ts +55 -0
- package/dist/components/Panel/Card.svelte +141 -0
- package/dist/components/Panel/Card.svelte.d.ts +52 -0
- package/dist/components/Panel/Hero/Hero.story.md +9 -0
- package/dist/components/Panel/Hero/Hero.story.svelte +49 -0
- package/dist/components/Panel/Hero/Hero.story.svelte.d.ts +21 -0
- package/dist/components/Panel/Hero/Hero.svelte +24 -0
- package/dist/components/Panel/Hero/Hero.svelte.d.ts +32 -0
- package/dist/components/Panel/LazyPanel.svelte +110 -0
- package/dist/components/Panel/LazyPanel.svelte.d.ts +46 -0
- package/dist/components/Panel/Panel.svelte +205 -0
- package/dist/components/Panel/Panel.svelte.d.ts +23 -0
- package/dist/components/Progress/Progress.svelte +220 -0
- package/dist/components/Progress/Progress.svelte.d.ts +61 -0
- package/dist/components/Separator/Separator.svelte +109 -0
- package/dist/components/Separator/Separator.svelte.d.ts +35 -0
- package/dist/components/Sidebar/Sidebar.svelte +213 -0
- package/dist/components/Sidebar/Sidebar.svelte.d.ts +60 -0
- package/dist/components/Skeleton/Skeleton.svelte +170 -0
- package/dist/components/Skeleton/Skeleton.svelte.d.ts +48 -0
- package/dist/components/Stepper/Stepper.svelte +111 -0
- package/dist/components/Stepper/Stepper.svelte.d.ts +54 -0
- package/dist/components/Stepper/StepperStep.svelte +369 -0
- package/dist/components/Stepper/StepperStep.svelte.d.ts +63 -0
- package/dist/components/Table/Table.svelte +167 -0
- package/dist/components/Table/Table.svelte.d.ts +56 -0
- package/dist/components/Table/TableBody.svelte +41 -0
- package/dist/components/Table/TableBody.svelte.d.ts +33 -0
- package/dist/components/Table/TableCell.svelte +76 -0
- package/dist/components/Table/TableCell.svelte.d.ts +36 -0
- package/dist/components/Table/TableHead.svelte +41 -0
- package/dist/components/Table/TableHead.svelte.d.ts +32 -0
- package/dist/components/Table/TableHeader.svelte +148 -0
- package/dist/components/Table/TableHeader.svelte.d.ts +42 -0
- package/dist/components/Table/TableRow.svelte +99 -0
- package/dist/components/Table/TableRow.svelte.d.ts +40 -0
- package/dist/components/Tabs/Tab.svelte +145 -0
- package/dist/components/Tabs/Tab.svelte.d.ts +36 -0
- package/dist/components/Tabs/TabList.svelte +60 -0
- package/dist/components/Tabs/TabList.svelte.d.ts +32 -0
- package/dist/components/Tabs/TabPanel.svelte +118 -0
- package/dist/components/Tabs/TabPanel.svelte.d.ts +38 -0
- package/dist/components/Tabs/Tabs.svelte +287 -0
- package/dist/components/Tabs/Tabs.svelte.d.ts +50 -0
- package/dist/components/Tag/Tag.svelte +260 -0
- package/dist/components/Tag/Tag.svelte.d.ts +54 -0
- package/dist/components/Tag/TagGroup.svelte +147 -0
- package/dist/components/Tag/TagGroup.svelte.d.ts +62 -0
- package/dist/components/ThemeToggle/ThemeToggle.svelte +93 -0
- package/dist/components/ThemeToggle/ThemeToggle.svelte.d.ts +12 -0
- package/dist/components/Timeline/Timeline.svelte +144 -0
- package/dist/components/Timeline/Timeline.svelte.d.ts +48 -0
- package/dist/components/Timeline/TimelineItem.svelte +391 -0
- package/dist/components/Timeline/TimelineItem.svelte.d.ts +63 -0
- package/dist/components/Toast/Toast.svelte +313 -0
- package/dist/components/Toast/Toast.svelte.d.ts +44 -0
- package/dist/components/Toast/toastStore.d.ts +40 -0
- package/dist/components/Toast/toastStore.js +293 -0
- package/dist/components/Tooltip/Tooltip.svelte +282 -0
- package/dist/components/Tooltip/Tooltip.svelte.d.ts +55 -0
- package/dist/components/Tree/Tree.svelte +129 -0
- package/dist/components/Tree/Tree.svelte.d.ts +61 -0
- package/dist/components/Tree/TreeNode.svelte +332 -0
- package/dist/components/Tree/TreeNode.svelte.d.ts +55 -0
- package/dist/components/icons/TwintrinsicLogo.svelte +73 -0
- package/dist/components/icons/TwintrinsicLogo.svelte.d.ts +17 -0
- package/dist/components/icons/twintrinsic-source.svg +73 -0
- package/dist/components/icons/twintrinsic.svg +38 -0
- package/dist/docs/EventsTable.svelte +86 -0
- package/dist/docs/EventsTable.svelte.d.ts +27 -0
- package/dist/docs/PropsTable.svelte +103 -0
- package/dist/docs/PropsTable.svelte.d.ts +28 -0
- package/dist/docs/index.d.ts +2 -0
- package/dist/docs/index.js +2 -0
- package/dist/helpers/detectLanguage.d.ts +6 -0
- package/dist/helpers/detectLanguage.js +60 -0
- package/dist/helpers/index.d.ts +1 -0
- package/dist/helpers/index.js +1 -0
- package/dist/index.d.ts +86 -0
- package/dist/index.js +94 -0
- package/dist/twintrinsic.css +347 -0
- package/package.json +98 -0
|
@@ -0,0 +1,796 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
FileUpload - A component for uploading files with drag and drop support.
|
|
4
|
+
Provides consistent styling, accessibility features, and various display options.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
```svelte
|
|
8
|
+
<FileUpload
|
|
9
|
+
onchange={handleFiles}
|
|
10
|
+
accept="image/*"
|
|
11
|
+
multiple
|
|
12
|
+
/>
|
|
13
|
+
|
|
14
|
+
<FileUpload
|
|
15
|
+
value={existingFiles}
|
|
16
|
+
maxFiles={5}
|
|
17
|
+
maxSize={5242880}
|
|
18
|
+
onchange={handleFiles}
|
|
19
|
+
onerror={handleError}
|
|
20
|
+
>
|
|
21
|
+
<div slot="dropzone">
|
|
22
|
+
<p>Drag files here or click to browse</p>
|
|
23
|
+
</div>
|
|
24
|
+
</FileUpload>
|
|
25
|
+
```
|
|
26
|
+
-->
|
|
27
|
+
<script>
|
|
28
|
+
const {
|
|
29
|
+
/** @type {string} - Additional CSS classes */
|
|
30
|
+
class: className = "",
|
|
31
|
+
|
|
32
|
+
/** @type {string} - HTML id for accessibility */
|
|
33
|
+
id = crypto.randomUUID(),
|
|
34
|
+
|
|
35
|
+
/** @type {string} - Name attribute for the input */
|
|
36
|
+
name,
|
|
37
|
+
|
|
38
|
+
/** @type {Array} - Current value (array of files) */
|
|
39
|
+
value = [],
|
|
40
|
+
|
|
41
|
+
/** @type {string} - Accepted file types */
|
|
42
|
+
accept,
|
|
43
|
+
|
|
44
|
+
/** @type {boolean} - Whether multiple files can be selected */
|
|
45
|
+
multiple = false,
|
|
46
|
+
|
|
47
|
+
/** @type {boolean} - Whether the upload is disabled */
|
|
48
|
+
disabled = false,
|
|
49
|
+
|
|
50
|
+
/** @type {number} - Maximum number of files allowed */
|
|
51
|
+
maxFiles,
|
|
52
|
+
|
|
53
|
+
/** @type {number} - Maximum file size in bytes */
|
|
54
|
+
maxSize,
|
|
55
|
+
|
|
56
|
+
/** @type {boolean} - Whether to show file previews */
|
|
57
|
+
showPreviews = true,
|
|
58
|
+
|
|
59
|
+
/** @type {boolean} - Whether to auto upload files */
|
|
60
|
+
autoUpload = false,
|
|
61
|
+
|
|
62
|
+
/** @type {string} - Upload URL for auto upload */
|
|
63
|
+
uploadUrl,
|
|
64
|
+
|
|
65
|
+
/** @type {Object} - Upload headers for auto upload */
|
|
66
|
+
uploadHeaders,
|
|
67
|
+
|
|
68
|
+
/** @type {string} - Label for the browse button */
|
|
69
|
+
browseLabel = "Browse",
|
|
70
|
+
|
|
71
|
+
/** @type {string} - Label for the dropzone */
|
|
72
|
+
dropzoneLabel = "Drag files here or click to browse",
|
|
73
|
+
|
|
74
|
+
/** @type {string} - ARIA label for the file input */
|
|
75
|
+
ariaLabel = "File upload",
|
|
76
|
+
|
|
77
|
+
/** @type {(event: CustomEvent) => void} - Change event handler */
|
|
78
|
+
onchange,
|
|
79
|
+
/** @type {(event: CustomEvent) => void} - Error event handler */
|
|
80
|
+
onerror,
|
|
81
|
+
/** @type {(event: CustomEvent) => void} - Progress event handler */
|
|
82
|
+
onprogress,
|
|
83
|
+
/** @type {(event: CustomEvent) => void} - Success event handler */
|
|
84
|
+
onsuccess,
|
|
85
|
+
|
|
86
|
+
dropzone,
|
|
87
|
+
previews,
|
|
88
|
+
} = $props()
|
|
89
|
+
|
|
90
|
+
// Component state
|
|
91
|
+
let files = $state(Array.isArray(value) ? [...value] : [])
|
|
92
|
+
let inputElement
|
|
93
|
+
let dropzoneElement
|
|
94
|
+
let isDragging = $state(false)
|
|
95
|
+
let errors = $state([])
|
|
96
|
+
let uploading = $state(false)
|
|
97
|
+
let uploadProgress = $state({})
|
|
98
|
+
|
|
99
|
+
// Update files when value prop changes
|
|
100
|
+
$effect(() => {
|
|
101
|
+
if (value !== files) {
|
|
102
|
+
files = Array.isArray(value) ? [...value] : []
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Validates files before adding them
|
|
108
|
+
* @param {FileList|Array} newFiles - Files to validate
|
|
109
|
+
* @returns {Array} - Valid files
|
|
110
|
+
*/
|
|
111
|
+
function validateFiles(newFiles) {
|
|
112
|
+
const fileArray = Array.from(newFiles)
|
|
113
|
+
const validFiles = []
|
|
114
|
+
const newErrors = []
|
|
115
|
+
|
|
116
|
+
// Check max files
|
|
117
|
+
if (maxFiles && files.length + fileArray.length > maxFiles) {
|
|
118
|
+
newErrors.push({
|
|
119
|
+
type: "maxFiles",
|
|
120
|
+
message: `Maximum of ${maxFiles} files allowed`,
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
// Only take as many files as we can
|
|
124
|
+
fileArray.splice(0, maxFiles - files.length)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Validate each file
|
|
128
|
+
for (const file of fileArray) {
|
|
129
|
+
// Check file size
|
|
130
|
+
if (maxSize && file.size > maxSize) {
|
|
131
|
+
newErrors.push({
|
|
132
|
+
type: "maxSize",
|
|
133
|
+
file,
|
|
134
|
+
message: `File "${file.name}" exceeds maximum size of ${formatBytes(maxSize)}`,
|
|
135
|
+
})
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Check file type if accept is specified
|
|
140
|
+
if (accept) {
|
|
141
|
+
const acceptTypes = accept.split(",").map((type) => type.trim())
|
|
142
|
+
const fileType = file.type
|
|
143
|
+
const fileExtension = `.${file.name.split(".").pop()}`
|
|
144
|
+
|
|
145
|
+
const isAccepted = acceptTypes.some((type) => {
|
|
146
|
+
if (type.startsWith(".")) {
|
|
147
|
+
// Extension match
|
|
148
|
+
return type === fileExtension
|
|
149
|
+
} else if (type.endsWith("/*")) {
|
|
150
|
+
// MIME type category match (e.g., image/*)
|
|
151
|
+
const category = type.replace("/*", "")
|
|
152
|
+
return fileType.startsWith(`${category}/`)
|
|
153
|
+
} else {
|
|
154
|
+
// Exact MIME type match
|
|
155
|
+
return type === fileType
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
if (!isAccepted) {
|
|
160
|
+
newErrors.push({
|
|
161
|
+
type: "accept",
|
|
162
|
+
file,
|
|
163
|
+
message: `File "${file.name}" has an invalid file type`,
|
|
164
|
+
})
|
|
165
|
+
continue
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// File is valid
|
|
170
|
+
validFiles.push(file)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Dispatch errors if any
|
|
174
|
+
if (newErrors.length > 0) {
|
|
175
|
+
errors = [...errors, ...newErrors]
|
|
176
|
+
onerror?.(new CustomEvent("error", { detail: { errors: newErrors } }))
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return validFiles
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Handles file input change
|
|
184
|
+
* @param {Event} event - Change event
|
|
185
|
+
*/
|
|
186
|
+
function handleInputChange(event) {
|
|
187
|
+
if (disabled) return
|
|
188
|
+
|
|
189
|
+
const inputFiles = event.target.files
|
|
190
|
+
if (!inputFiles || inputFiles.length === 0) return
|
|
191
|
+
|
|
192
|
+
addFiles(inputFiles)
|
|
193
|
+
|
|
194
|
+
// Reset input value so the same file can be selected again
|
|
195
|
+
if (inputElement) {
|
|
196
|
+
inputElement.value = ""
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Adds files to the current selection
|
|
202
|
+
* @param {FileList|Array} newFiles - Files to add
|
|
203
|
+
*/
|
|
204
|
+
function addFiles(newFiles) {
|
|
205
|
+
const validFiles = validateFiles(newFiles)
|
|
206
|
+
|
|
207
|
+
if (validFiles.length > 0) {
|
|
208
|
+
// Add new files
|
|
209
|
+
const updatedFiles = multiple ? [...files, ...validFiles] : validFiles
|
|
210
|
+
files = updatedFiles
|
|
211
|
+
|
|
212
|
+
// Dispatch change event
|
|
213
|
+
onchange?.(new CustomEvent("change", { detail: { files: updatedFiles } }))
|
|
214
|
+
|
|
215
|
+
// Auto upload if enabled
|
|
216
|
+
if (autoUpload && uploadUrl) {
|
|
217
|
+
uploadFiles(validFiles)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Removes a file from the selection
|
|
224
|
+
* @param {number} index - Index of the file to remove
|
|
225
|
+
*/
|
|
226
|
+
function removeFile(index) {
|
|
227
|
+
if (disabled) return
|
|
228
|
+
|
|
229
|
+
const removedFile = files[index]
|
|
230
|
+
files = files.filter((_, i) => i !== index)
|
|
231
|
+
|
|
232
|
+
// Dispatch change event
|
|
233
|
+
onchange?.(new CustomEvent("change", { detail: { files } }))
|
|
234
|
+
|
|
235
|
+
// Dispatch remove event
|
|
236
|
+
onremove?.(new CustomEvent("remove", { detail: { file: removedFile, index } }))
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Handles drag enter event
|
|
241
|
+
* @param {DragEvent} event - Drag event
|
|
242
|
+
*/
|
|
243
|
+
function handleDragEnter(event) {
|
|
244
|
+
if (disabled) return
|
|
245
|
+
|
|
246
|
+
event.preventDefault()
|
|
247
|
+
event.stopPropagation()
|
|
248
|
+
isDragging = true
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Handles drag over event
|
|
253
|
+
* @param {DragEvent} event - Drag event
|
|
254
|
+
*/
|
|
255
|
+
function handleDragOver(event) {
|
|
256
|
+
if (disabled) return
|
|
257
|
+
|
|
258
|
+
event.preventDefault()
|
|
259
|
+
event.stopPropagation()
|
|
260
|
+
isDragging = true
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Handles drag leave event
|
|
265
|
+
* @param {DragEvent} event - Drag event
|
|
266
|
+
*/
|
|
267
|
+
function handleDragLeave(event) {
|
|
268
|
+
if (disabled) return
|
|
269
|
+
|
|
270
|
+
event.preventDefault()
|
|
271
|
+
event.stopPropagation()
|
|
272
|
+
|
|
273
|
+
// Only set isDragging to false if we're leaving the dropzone
|
|
274
|
+
// and not entering a child element
|
|
275
|
+
const rect = dropzoneElement.getBoundingClientRect()
|
|
276
|
+
const x = event.clientX
|
|
277
|
+
const y = event.clientY
|
|
278
|
+
|
|
279
|
+
if (x < rect.left || x >= rect.right || y < rect.top || y >= rect.bottom) {
|
|
280
|
+
isDragging = false
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Handles drop event
|
|
286
|
+
* @param {DragEvent} event - Drop event
|
|
287
|
+
*/
|
|
288
|
+
function handleDrop(event) {
|
|
289
|
+
if (disabled) return
|
|
290
|
+
|
|
291
|
+
event.preventDefault()
|
|
292
|
+
event.stopPropagation()
|
|
293
|
+
isDragging = false
|
|
294
|
+
|
|
295
|
+
const droppedFiles = event.dataTransfer.files
|
|
296
|
+
if (!droppedFiles || droppedFiles.length === 0) return
|
|
297
|
+
|
|
298
|
+
addFiles(droppedFiles)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Opens the file browser
|
|
303
|
+
*/
|
|
304
|
+
function browse(evt) {
|
|
305
|
+
evt.stopPropagation()
|
|
306
|
+
if (disabled) return
|
|
307
|
+
|
|
308
|
+
if (inputElement) {
|
|
309
|
+
inputElement.click()
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Uploads files to the server
|
|
315
|
+
* @param {Array} filesToUpload - Files to upload
|
|
316
|
+
*/
|
|
317
|
+
async function uploadFiles(filesToUpload) {
|
|
318
|
+
if (!uploadUrl || !filesToUpload.length) return
|
|
319
|
+
|
|
320
|
+
uploading = true
|
|
321
|
+
|
|
322
|
+
// Create FormData
|
|
323
|
+
const formData = new FormData()
|
|
324
|
+
|
|
325
|
+
// Add files to FormData
|
|
326
|
+
filesToUpload.forEach((file, index) => {
|
|
327
|
+
formData.append(name || "files", file, file.name)
|
|
328
|
+
uploadProgress[file.name] = 0
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
try {
|
|
332
|
+
// Create upload request
|
|
333
|
+
const xhr = new XMLHttpRequest()
|
|
334
|
+
|
|
335
|
+
// Track progress
|
|
336
|
+
xhr.upload.addEventListener("progress", (event) => {
|
|
337
|
+
if (event.lengthComputable) {
|
|
338
|
+
const progress = Math.round((event.loaded / event.total) * 100)
|
|
339
|
+
|
|
340
|
+
// Update progress for all files in this batch
|
|
341
|
+
filesToUpload.forEach((file) => {
|
|
342
|
+
uploadProgress[file.name] = progress
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
// Force update
|
|
346
|
+
uploadProgress = { ...uploadProgress }
|
|
347
|
+
|
|
348
|
+
// Dispatch progress event
|
|
349
|
+
onprogress?.(new CustomEvent("progress", { detail: { progress, files: filesToUpload } }))
|
|
350
|
+
}
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
// Set up completion handler
|
|
354
|
+
xhr.addEventListener("load", () => {
|
|
355
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
356
|
+
// Success
|
|
357
|
+
filesToUpload.forEach((file) => {
|
|
358
|
+
uploadProgress[file.name] = 100
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
// Force update
|
|
362
|
+
uploadProgress = { ...uploadProgress }
|
|
363
|
+
|
|
364
|
+
// Dispatch success event
|
|
365
|
+
onsuccess?.(new CustomEvent("success", {
|
|
366
|
+
detail: {
|
|
367
|
+
response: xhr.response,
|
|
368
|
+
files: filesToUpload,
|
|
369
|
+
}
|
|
370
|
+
}))
|
|
371
|
+
} else {
|
|
372
|
+
// Error
|
|
373
|
+
const error = {
|
|
374
|
+
type: "upload",
|
|
375
|
+
status: xhr.status,
|
|
376
|
+
message: `Upload failed with status ${xhr.status}`,
|
|
377
|
+
response: xhr.response,
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
errors = [...errors, error]
|
|
381
|
+
|
|
382
|
+
// Dispatch error event
|
|
383
|
+
onerror?.(new CustomEvent("error", { detail: { errors: [error] } }))
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
uploading = false
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
// Set up error handler
|
|
390
|
+
xhr.addEventListener("error", () => {
|
|
391
|
+
const error = {
|
|
392
|
+
type: "upload",
|
|
393
|
+
message: "Network error during upload",
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
errors = [...errors, error]
|
|
397
|
+
|
|
398
|
+
// Dispatch error event
|
|
399
|
+
dispatch("error", { errors: [error] })
|
|
400
|
+
|
|
401
|
+
uploading = false
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
// Open and send request
|
|
405
|
+
xhr.open("POST", uploadUrl)
|
|
406
|
+
|
|
407
|
+
// Add headers if provided
|
|
408
|
+
if (uploadHeaders) {
|
|
409
|
+
Object.entries(uploadHeaders).forEach(([key, value]) => {
|
|
410
|
+
xhr.setRequestHeader(key, value)
|
|
411
|
+
})
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
xhr.send(formData)
|
|
415
|
+
} catch (error) {
|
|
416
|
+
const errorObj = {
|
|
417
|
+
type: "upload",
|
|
418
|
+
message: error.message || "Error during upload",
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
errors = [...errors, errorObj]
|
|
422
|
+
|
|
423
|
+
// Dispatch error event
|
|
424
|
+
dispatch("error", { errors: [errorObj] })
|
|
425
|
+
|
|
426
|
+
uploading = false
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Formats bytes to human-readable size
|
|
432
|
+
* @param {number} bytes - Bytes to format
|
|
433
|
+
* @returns {string} - Formatted size
|
|
434
|
+
*/
|
|
435
|
+
function formatBytes(bytes) {
|
|
436
|
+
if (bytes === 0) return "0 Bytes"
|
|
437
|
+
|
|
438
|
+
const k = 1024
|
|
439
|
+
const sizes = ["Bytes", "KB", "MB", "GB", "TB"]
|
|
440
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
441
|
+
|
|
442
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Gets file icon based on file type
|
|
447
|
+
* @param {File} file - File to get icon for
|
|
448
|
+
* @returns {string} - Icon HTML
|
|
449
|
+
*/
|
|
450
|
+
function getFileIcon(file) {
|
|
451
|
+
const type = file.type
|
|
452
|
+
|
|
453
|
+
if (type.startsWith("image/")) {
|
|
454
|
+
return `
|
|
455
|
+
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
456
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
|
457
|
+
</svg>
|
|
458
|
+
`
|
|
459
|
+
} else if (type.startsWith("video/")) {
|
|
460
|
+
return `
|
|
461
|
+
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
462
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
|
|
463
|
+
</svg>
|
|
464
|
+
`
|
|
465
|
+
} else if (type.startsWith("audio/")) {
|
|
466
|
+
return `
|
|
467
|
+
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
468
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3"></path>
|
|
469
|
+
</svg>
|
|
470
|
+
`
|
|
471
|
+
} else if (type === "application/pdf") {
|
|
472
|
+
return `
|
|
473
|
+
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
474
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"></path>
|
|
475
|
+
</svg>
|
|
476
|
+
`
|
|
477
|
+
} else {
|
|
478
|
+
return `
|
|
479
|
+
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
480
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
|
481
|
+
</svg>
|
|
482
|
+
`
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Creates an image preview URL
|
|
488
|
+
* @param {File} file - File to preview
|
|
489
|
+
* @returns {string} - Preview URL
|
|
490
|
+
*/
|
|
491
|
+
function createPreviewUrl(file) {
|
|
492
|
+
if (!file) return ""
|
|
493
|
+
|
|
494
|
+
if (file.type.startsWith("image/")) {
|
|
495
|
+
return URL.createObjectURL(file)
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return ""
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Clean up object URLs on component destroy
|
|
502
|
+
onDestroy(() => {
|
|
503
|
+
files.forEach((file) => {
|
|
504
|
+
if (file.preview) {
|
|
505
|
+
URL.revokeObjectURL(file.preview)
|
|
506
|
+
}
|
|
507
|
+
})
|
|
508
|
+
})
|
|
509
|
+
</script>
|
|
510
|
+
|
|
511
|
+
<div
|
|
512
|
+
class="
|
|
513
|
+
file-upload
|
|
514
|
+
{isDragging ? 'file-upload-dragging' : ''}
|
|
515
|
+
{disabled ? 'file-upload-disabled' : ''}
|
|
516
|
+
{className}
|
|
517
|
+
"
|
|
518
|
+
>
|
|
519
|
+
<div
|
|
520
|
+
class="file-upload-dropzone"
|
|
521
|
+
ondragenter={handleDragEnter}
|
|
522
|
+
ondragover={handleDragOver}
|
|
523
|
+
ondragleave={handleDragLeave}
|
|
524
|
+
ondrop={handleDrop}
|
|
525
|
+
onclick={browse}
|
|
526
|
+
bind:this={dropzoneElement}
|
|
527
|
+
role="button"
|
|
528
|
+
tabindex={disabled ? undefined : 0}
|
|
529
|
+
aria-label={ariaLabel}
|
|
530
|
+
>
|
|
531
|
+
{#if dropzone}
|
|
532
|
+
{@render dropzone?.()}
|
|
533
|
+
{:else}
|
|
534
|
+
<div class="file-upload-content">
|
|
535
|
+
<svg class="file-upload-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
536
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path>
|
|
537
|
+
</svg>
|
|
538
|
+
<div class="file-upload-text">
|
|
539
|
+
<p>{dropzoneLabel}</p>
|
|
540
|
+
{#if maxFiles}
|
|
541
|
+
<p class="file-upload-hint">Maximum {maxFiles} file{maxFiles === 1 ? '' : 's'}</p>
|
|
542
|
+
{/if}
|
|
543
|
+
{#if maxSize}
|
|
544
|
+
<p class="file-upload-hint">Maximum size: {formatBytes(maxSize)}</p>
|
|
545
|
+
{/if}
|
|
546
|
+
{#if accept}
|
|
547
|
+
<p class="file-upload-hint">Accepted types: {accept}</p>
|
|
548
|
+
{/if}
|
|
549
|
+
</div>
|
|
550
|
+
<button
|
|
551
|
+
type="button"
|
|
552
|
+
class="file-upload-browse"
|
|
553
|
+
onclick={browse}
|
|
554
|
+
disabled={disabled}
|
|
555
|
+
>
|
|
556
|
+
{browseLabel}
|
|
557
|
+
</button>
|
|
558
|
+
</div>
|
|
559
|
+
{/if}
|
|
560
|
+
</div>
|
|
561
|
+
|
|
562
|
+
<input
|
|
563
|
+
{id}
|
|
564
|
+
type="file"
|
|
565
|
+
{name}
|
|
566
|
+
{accept}
|
|
567
|
+
{multiple}
|
|
568
|
+
{disabled}
|
|
569
|
+
class="file-upload-input"
|
|
570
|
+
onchange={handleInputChange}
|
|
571
|
+
bind:this={inputElement}
|
|
572
|
+
aria-hidden="true"
|
|
573
|
+
tabindex="-1"
|
|
574
|
+
/>
|
|
575
|
+
|
|
576
|
+
{#if showPreviews && files.length > 0}
|
|
577
|
+
<div class="file-upload-previews">
|
|
578
|
+
{#if previews}
|
|
579
|
+
{@render previews?.({ files, removeFile })}
|
|
580
|
+
{:else}
|
|
581
|
+
<ul class="file-upload-preview-list" role="list">
|
|
582
|
+
{#each files as file, i}
|
|
583
|
+
<li class="file-upload-preview-item">
|
|
584
|
+
<div class="file-upload-preview-content">
|
|
585
|
+
{#if file.type.startsWith('image/')}
|
|
586
|
+
<div class="file-upload-preview-image">
|
|
587
|
+
<img src={createPreviewUrl(file)} alt={file.name} />
|
|
588
|
+
</div>
|
|
589
|
+
{:else}
|
|
590
|
+
<div class="file-upload-preview-icon">
|
|
591
|
+
{@html getFileIcon(file)}
|
|
592
|
+
</div>
|
|
593
|
+
{/if}
|
|
594
|
+
|
|
595
|
+
<div class="file-upload-preview-info">
|
|
596
|
+
<div class="file-upload-preview-name" title={file.name}>
|
|
597
|
+
{file.name}
|
|
598
|
+
</div>
|
|
599
|
+
<div class="file-upload-preview-size">
|
|
600
|
+
{formatBytes(file.size)}
|
|
601
|
+
</div>
|
|
602
|
+
|
|
603
|
+
{#if uploadProgress[file.name] !== undefined}
|
|
604
|
+
<div class="file-upload-preview-progress">
|
|
605
|
+
<div
|
|
606
|
+
class="file-upload-preview-progress-bar"
|
|
607
|
+
style="width: {uploadProgress[file.name]}%"
|
|
608
|
+
></div>
|
|
609
|
+
</div>
|
|
610
|
+
{/if}
|
|
611
|
+
</div>
|
|
612
|
+
|
|
613
|
+
<button
|
|
614
|
+
type="button"
|
|
615
|
+
class="file-upload-preview-remove"
|
|
616
|
+
aria-label={`Remove ${file.name}`}
|
|
617
|
+
onclick={() => removeFile(i)}
|
|
618
|
+
disabled={disabled || uploading}
|
|
619
|
+
>
|
|
620
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
621
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
622
|
+
</svg>
|
|
623
|
+
</button>
|
|
624
|
+
</div>
|
|
625
|
+
</li>
|
|
626
|
+
{/each}
|
|
627
|
+
</ul>
|
|
628
|
+
{/if}
|
|
629
|
+
</div>
|
|
630
|
+
{/if}
|
|
631
|
+
|
|
632
|
+
{#if errors.length > 0}
|
|
633
|
+
<div class="file-upload-errors" role="alert">
|
|
634
|
+
<ul class="file-upload-error-list">
|
|
635
|
+
{#each errors as error}
|
|
636
|
+
<li class="file-upload-error-item">
|
|
637
|
+
{error.message}
|
|
638
|
+
</li>
|
|
639
|
+
{/each}
|
|
640
|
+
</ul>
|
|
641
|
+
</div>
|
|
642
|
+
{/if}
|
|
643
|
+
</div>
|
|
644
|
+
|
|
645
|
+
<style>
|
|
646
|
+
@reference "../../twintrinsic.css";
|
|
647
|
+
|
|
648
|
+
.file-upload {
|
|
649
|
+
@apply w-full;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
.file-upload-dropzone {
|
|
653
|
+
@apply border-2 border-dashed border-border dark:border-border;
|
|
654
|
+
@apply rounded-lg;
|
|
655
|
+
@apply bg-surface dark:bg-surface;
|
|
656
|
+
@apply p-6;
|
|
657
|
+
@apply cursor-pointer;
|
|
658
|
+
@apply transition-colors duration-150;
|
|
659
|
+
@apply focus:outline-none focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
.file-upload-dragging {
|
|
663
|
+
@apply border-primary-500 dark:border-primary-500;
|
|
664
|
+
@apply bg-primary-50 dark:bg-primary-900/20;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
.file-upload-disabled {
|
|
668
|
+
@apply opacity-50 cursor-not-allowed;
|
|
669
|
+
@apply pointer-events-none;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
.file-upload-content {
|
|
673
|
+
@apply flex flex-col items-center justify-center;
|
|
674
|
+
@apply text-center;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
.file-upload-icon {
|
|
678
|
+
@apply w-12 h-12 mb-4;
|
|
679
|
+
@apply text-muted dark:text-muted;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
.file-upload-text {
|
|
683
|
+
@apply mb-4;
|
|
684
|
+
@apply text-text dark:text-text;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
.file-upload-hint {
|
|
688
|
+
@apply text-sm text-muted dark:text-muted;
|
|
689
|
+
@apply mt-1;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
.file-upload-browse {
|
|
693
|
+
@apply px-4 py-2;
|
|
694
|
+
@apply bg-primary-500 dark:bg-primary-500;
|
|
695
|
+
@apply text-white dark:text-white;
|
|
696
|
+
@apply rounded-md;
|
|
697
|
+
@apply font-medium;
|
|
698
|
+
@apply hover:bg-primary-600 dark:hover:bg-primary-600;
|
|
699
|
+
@apply focus:outline-none focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400 focus:ring-offset-2;
|
|
700
|
+
@apply transition-colors duration-150;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
.file-upload-input {
|
|
704
|
+
@apply hidden;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
.file-upload-previews {
|
|
708
|
+
@apply mt-4;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
.file-upload-preview-list {
|
|
712
|
+
@apply space-y-2;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
.file-upload-preview-item {
|
|
716
|
+
@apply bg-surface dark:bg-surface;
|
|
717
|
+
@apply border border-border dark:border-border;
|
|
718
|
+
@apply rounded-md;
|
|
719
|
+
@apply overflow-hidden;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
.file-upload-preview-content {
|
|
723
|
+
@apply flex items-center;
|
|
724
|
+
@apply p-3;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
.file-upload-preview-icon {
|
|
728
|
+
@apply flex-shrink-0 mr-3;
|
|
729
|
+
@apply text-muted dark:text-muted;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
.file-upload-preview-image {
|
|
733
|
+
@apply w-12 h-12 mr-3;
|
|
734
|
+
@apply rounded-md overflow-hidden;
|
|
735
|
+
@apply bg-muted/10 dark:bg-muted/10;
|
|
736
|
+
@apply flex items-center justify-center;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
.file-upload-preview-image img {
|
|
740
|
+
@apply w-full h-full object-cover;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
.file-upload-preview-info {
|
|
744
|
+
@apply flex-grow min-w-0;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
.file-upload-preview-name {
|
|
748
|
+
@apply font-medium;
|
|
749
|
+
@apply truncate;
|
|
750
|
+
@apply text-text dark:text-text;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
.file-upload-preview-size {
|
|
754
|
+
@apply text-sm;
|
|
755
|
+
@apply text-muted dark:text-muted;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
.file-upload-preview-progress {
|
|
759
|
+
@apply mt-1 w-full h-1;
|
|
760
|
+
@apply bg-muted/10 dark:bg-muted/10;
|
|
761
|
+
@apply rounded-full overflow-hidden;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
.file-upload-preview-progress-bar {
|
|
765
|
+
@apply h-full;
|
|
766
|
+
@apply bg-primary-500 dark:bg-primary-500;
|
|
767
|
+
@apply transition-all duration-150;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
.file-upload-preview-remove {
|
|
771
|
+
@apply flex-shrink-0 ml-3;
|
|
772
|
+
@apply p-1 rounded-full;
|
|
773
|
+
@apply text-muted dark:text-muted;
|
|
774
|
+
@apply hover:text-error-500 dark:hover:text-error-500;
|
|
775
|
+
@apply hover:bg-error-50 dark:hover:bg-error-900/20;
|
|
776
|
+
@apply focus:outline-none focus:ring-2 focus:ring-error-500 dark:focus:ring-error-400;
|
|
777
|
+
@apply transition-colors duration-150;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
.file-upload-errors {
|
|
781
|
+
@apply mt-4;
|
|
782
|
+
@apply p-3;
|
|
783
|
+
@apply bg-error-50 dark:bg-error-900/20;
|
|
784
|
+
@apply border border-error-200 dark:border-error-800;
|
|
785
|
+
@apply rounded-md;
|
|
786
|
+
@apply text-error-700 dark:text-error-300;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
.file-upload-error-list {
|
|
790
|
+
@apply list-disc pl-5;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
.file-upload-error-item {
|
|
794
|
+
@apply text-sm;
|
|
795
|
+
}
|
|
796
|
+
</style>
|