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.
Files changed (212) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +150 -0
  3. package/dist/App/App.svelte +54 -0
  4. package/dist/App/App.svelte.d.ts +65 -0
  5. package/dist/Section.svelte +25 -0
  6. package/dist/Section.svelte.d.ts +34 -0
  7. package/dist/actions/clickOutside.d.ts +9 -0
  8. package/dist/actions/clickOutside.js +19 -0
  9. package/dist/actions/index.d.ts +1 -0
  10. package/dist/actions/index.js +1 -0
  11. package/dist/components/Accordion/Accordion.svelte +75 -0
  12. package/dist/components/Accordion/Accordion.svelte.d.ts +39 -0
  13. package/dist/components/Accordion/AccordionItem.svelte +150 -0
  14. package/dist/components/Accordion/AccordionItem.svelte.d.ts +30 -0
  15. package/dist/components/App/App.story.md +8 -0
  16. package/dist/components/App/App.story.svelte +170 -0
  17. package/dist/components/App/App.story.svelte.d.ts +22 -0
  18. package/dist/components/App/App.svelte +77 -0
  19. package/dist/components/App/App.svelte.d.ts +66 -0
  20. package/dist/components/App/Split.svelte +346 -0
  21. package/dist/components/App/Split.svelte.d.ts +54 -0
  22. package/dist/components/App/index.d.ts +2 -0
  23. package/dist/components/App/index.js +3 -0
  24. package/dist/components/AppHeader/AppHeader.svelte +439 -0
  25. package/dist/components/AppHeader/AppHeader.svelte.d.ts +24 -0
  26. package/dist/components/Avatar/Avatar.svelte +300 -0
  27. package/dist/components/Avatar/Avatar.svelte.d.ts +48 -0
  28. package/dist/components/Avatar/AvatarGroup.svelte +185 -0
  29. package/dist/components/Avatar/AvatarGroup.svelte.d.ts +46 -0
  30. package/dist/components/Badge/Badge.svelte +186 -0
  31. package/dist/components/Badge/Badge.svelte.d.ts +51 -0
  32. package/dist/components/BottomBar/BottomBar.svelte +146 -0
  33. package/dist/components/BottomBar/BottomBar.svelte.d.ts +38 -0
  34. package/dist/components/Breadcrumb/Breadcrumb.svelte +77 -0
  35. package/dist/components/Breadcrumb/Breadcrumb.svelte.d.ts +42 -0
  36. package/dist/components/Breadcrumb/BreadcrumbItem.svelte +171 -0
  37. package/dist/components/Breadcrumb/BreadcrumbItem.svelte.d.ts +38 -0
  38. package/dist/components/Button/Button.svelte +252 -0
  39. package/dist/components/Button/Button.svelte.d.ts +80 -0
  40. package/dist/components/Button/ButtonGroup.svelte +127 -0
  41. package/dist/components/Button/ButtonGroup.svelte.d.ts +44 -0
  42. package/dist/components/Card/Card.svelte +152 -0
  43. package/dist/components/Card/Card.svelte.d.ts +55 -0
  44. package/dist/components/Carousel/Carousel.svelte +461 -0
  45. package/dist/components/Carousel/Carousel.svelte.d.ts +79 -0
  46. package/dist/components/Carousel/CarouselItem.svelte +149 -0
  47. package/dist/components/Carousel/CarouselItem.svelte.d.ts +35 -0
  48. package/dist/components/Chip/Chip.svelte +288 -0
  49. package/dist/components/Chip/Chip.svelte.d.ts +71 -0
  50. package/dist/components/Chip/ChipGroup.svelte +190 -0
  51. package/dist/components/Chip/ChipGroup.svelte.d.ts +71 -0
  52. package/dist/components/CodeBlock/CodeBlock.svelte +356 -0
  53. package/dist/components/CodeBlock/CodeBlock.svelte.d.ts +44 -0
  54. package/dist/components/CodeBlock/index.d.ts +1 -0
  55. package/dist/components/CodeBlock/index.js +1 -0
  56. package/dist/components/CodeBlockSpeed/CodeBlockSpeed.svelte +145 -0
  57. package/dist/components/CodeBlockSpeed/CodeBlockSpeed.svelte.d.ts +44 -0
  58. package/dist/components/CodeEditor/CodeEditor.svelte +229 -0
  59. package/dist/components/CodeEditor/CodeEditor.svelte.d.ts +23 -0
  60. package/dist/components/Combobox/Combobox.svelte +279 -0
  61. package/dist/components/Combobox/Combobox.svelte.d.ts +34 -0
  62. package/dist/components/Container/Container.svelte +45 -0
  63. package/dist/components/Container/Container.svelte.d.ts +36 -0
  64. package/dist/components/DataTable/DataTable.svelte +879 -0
  65. package/dist/components/DataTable/DataTable.svelte.d.ts +102 -0
  66. package/dist/components/Form/AutoComplete.svelte +357 -0
  67. package/dist/components/Form/AutoComplete.svelte.d.ts +73 -0
  68. package/dist/components/Form/Calendar.svelte +429 -0
  69. package/dist/components/Form/Calendar.svelte.d.ts +53 -0
  70. package/dist/components/Form/Checkbox.svelte +196 -0
  71. package/dist/components/Form/Checkbox.svelte.d.ts +50 -0
  72. package/dist/components/Form/ColorPicker.svelte +396 -0
  73. package/dist/components/Form/ColorPicker.svelte.d.ts +43 -0
  74. package/dist/components/Form/Combobox.svelte +645 -0
  75. package/dist/components/Form/Combobox.svelte.d.ts +93 -0
  76. package/dist/components/Form/Dropdown.svelte +773 -0
  77. package/dist/components/Form/Dropdown.svelte.d.ts +81 -0
  78. package/dist/components/Form/FileUpload.svelte +796 -0
  79. package/dist/components/Form/FileUpload.svelte.d.ts +78 -0
  80. package/dist/components/Form/FloatLabel.svelte +245 -0
  81. package/dist/components/Form/FloatLabel.svelte.d.ts +44 -0
  82. package/dist/components/Form/Form.svelte +281 -0
  83. package/dist/components/Form/Form.svelte.d.ts +54 -0
  84. package/dist/components/Form/FormField.svelte +218 -0
  85. package/dist/components/Form/FormField.svelte.d.ts +47 -0
  86. package/dist/components/Form/Input.svelte +340 -0
  87. package/dist/components/Form/Input.svelte.d.ts +79 -0
  88. package/dist/components/Form/InputSwitch.svelte +189 -0
  89. package/dist/components/Form/InputSwitch.svelte.d.ts +46 -0
  90. package/dist/components/Form/InvalidState.svelte +97 -0
  91. package/dist/components/Form/InvalidState.svelte.d.ts +37 -0
  92. package/dist/components/Form/Knob.svelte +537 -0
  93. package/dist/components/Form/Knob.svelte.d.ts +78 -0
  94. package/dist/components/Form/ListInput.svelte +469 -0
  95. package/dist/components/Form/ListInput.svelte.d.ts +70 -0
  96. package/dist/components/Form/Listbox.svelte +513 -0
  97. package/dist/components/Form/Listbox.svelte.d.ts +74 -0
  98. package/dist/components/Form/NumberInput.svelte +452 -0
  99. package/dist/components/Form/NumberInput.svelte.d.ts +82 -0
  100. package/dist/components/Form/Radio.svelte +192 -0
  101. package/dist/components/Form/Radio.svelte.d.ts +53 -0
  102. package/dist/components/Form/RadioGroup.svelte +155 -0
  103. package/dist/components/Form/RadioGroup.svelte.d.ts +48 -0
  104. package/dist/components/Form/Rating.svelte +380 -0
  105. package/dist/components/Form/Rating.svelte.d.ts +64 -0
  106. package/dist/components/Form/Select.svelte +436 -0
  107. package/dist/components/Form/Select.svelte.d.ts +49 -0
  108. package/dist/components/Form/SelectGroup.svelte +34 -0
  109. package/dist/components/Form/SelectGroup.svelte.d.ts +33 -0
  110. package/dist/components/Form/Slider.svelte +622 -0
  111. package/dist/components/Form/Slider.svelte.d.ts +73 -0
  112. package/dist/components/Form/Switch.svelte +192 -0
  113. package/dist/components/Form/Switch.svelte.d.ts +46 -0
  114. package/dist/components/Form/TextInput.svelte +274 -0
  115. package/dist/components/Form/TextInput.svelte.d.ts +74 -0
  116. package/dist/components/Form/Textarea.svelte +207 -0
  117. package/dist/components/Form/Textarea.svelte.d.ts +62 -0
  118. package/dist/components/Icon/Icon.svelte +140 -0
  119. package/dist/components/Icon/Icon.svelte.d.ts +25 -0
  120. package/dist/components/Icon/index.d.ts +1 -0
  121. package/dist/components/Icon/index.js +1 -0
  122. package/dist/components/Lazy/Lazy.svelte +158 -0
  123. package/dist/components/Lazy/Lazy.svelte.d.ts +42 -0
  124. package/dist/components/Masonry/Masonry.svelte +299 -0
  125. package/dist/components/Masonry/Masonry.svelte.d.ts +55 -0
  126. package/dist/components/Menu/Menu/Menu.svelte +65 -0
  127. package/dist/components/Menu/Menu/Menu.svelte.d.ts +17 -0
  128. package/dist/components/Menu/Menu/MenuItem.svelte +90 -0
  129. package/dist/components/Menu/Menu/MenuItem.svelte.d.ts +27 -0
  130. package/dist/components/Modal/Modal.svelte +334 -0
  131. package/dist/components/Modal/Modal.svelte.d.ts +55 -0
  132. package/dist/components/Panel/Card.svelte +141 -0
  133. package/dist/components/Panel/Card.svelte.d.ts +52 -0
  134. package/dist/components/Panel/Hero/Hero.story.md +9 -0
  135. package/dist/components/Panel/Hero/Hero.story.svelte +49 -0
  136. package/dist/components/Panel/Hero/Hero.story.svelte.d.ts +21 -0
  137. package/dist/components/Panel/Hero/Hero.svelte +24 -0
  138. package/dist/components/Panel/Hero/Hero.svelte.d.ts +32 -0
  139. package/dist/components/Panel/LazyPanel.svelte +110 -0
  140. package/dist/components/Panel/LazyPanel.svelte.d.ts +46 -0
  141. package/dist/components/Panel/Panel.svelte +205 -0
  142. package/dist/components/Panel/Panel.svelte.d.ts +23 -0
  143. package/dist/components/Progress/Progress.svelte +220 -0
  144. package/dist/components/Progress/Progress.svelte.d.ts +61 -0
  145. package/dist/components/Separator/Separator.svelte +109 -0
  146. package/dist/components/Separator/Separator.svelte.d.ts +35 -0
  147. package/dist/components/Sidebar/Sidebar.svelte +213 -0
  148. package/dist/components/Sidebar/Sidebar.svelte.d.ts +60 -0
  149. package/dist/components/Skeleton/Skeleton.svelte +170 -0
  150. package/dist/components/Skeleton/Skeleton.svelte.d.ts +48 -0
  151. package/dist/components/Stepper/Stepper.svelte +111 -0
  152. package/dist/components/Stepper/Stepper.svelte.d.ts +54 -0
  153. package/dist/components/Stepper/StepperStep.svelte +369 -0
  154. package/dist/components/Stepper/StepperStep.svelte.d.ts +63 -0
  155. package/dist/components/Table/Table.svelte +167 -0
  156. package/dist/components/Table/Table.svelte.d.ts +56 -0
  157. package/dist/components/Table/TableBody.svelte +41 -0
  158. package/dist/components/Table/TableBody.svelte.d.ts +33 -0
  159. package/dist/components/Table/TableCell.svelte +76 -0
  160. package/dist/components/Table/TableCell.svelte.d.ts +36 -0
  161. package/dist/components/Table/TableHead.svelte +41 -0
  162. package/dist/components/Table/TableHead.svelte.d.ts +32 -0
  163. package/dist/components/Table/TableHeader.svelte +148 -0
  164. package/dist/components/Table/TableHeader.svelte.d.ts +42 -0
  165. package/dist/components/Table/TableRow.svelte +99 -0
  166. package/dist/components/Table/TableRow.svelte.d.ts +40 -0
  167. package/dist/components/Tabs/Tab.svelte +145 -0
  168. package/dist/components/Tabs/Tab.svelte.d.ts +36 -0
  169. package/dist/components/Tabs/TabList.svelte +60 -0
  170. package/dist/components/Tabs/TabList.svelte.d.ts +32 -0
  171. package/dist/components/Tabs/TabPanel.svelte +118 -0
  172. package/dist/components/Tabs/TabPanel.svelte.d.ts +38 -0
  173. package/dist/components/Tabs/Tabs.svelte +287 -0
  174. package/dist/components/Tabs/Tabs.svelte.d.ts +50 -0
  175. package/dist/components/Tag/Tag.svelte +260 -0
  176. package/dist/components/Tag/Tag.svelte.d.ts +54 -0
  177. package/dist/components/Tag/TagGroup.svelte +147 -0
  178. package/dist/components/Tag/TagGroup.svelte.d.ts +62 -0
  179. package/dist/components/ThemeToggle/ThemeToggle.svelte +93 -0
  180. package/dist/components/ThemeToggle/ThemeToggle.svelte.d.ts +12 -0
  181. package/dist/components/Timeline/Timeline.svelte +144 -0
  182. package/dist/components/Timeline/Timeline.svelte.d.ts +48 -0
  183. package/dist/components/Timeline/TimelineItem.svelte +391 -0
  184. package/dist/components/Timeline/TimelineItem.svelte.d.ts +63 -0
  185. package/dist/components/Toast/Toast.svelte +313 -0
  186. package/dist/components/Toast/Toast.svelte.d.ts +44 -0
  187. package/dist/components/Toast/toastStore.d.ts +40 -0
  188. package/dist/components/Toast/toastStore.js +293 -0
  189. package/dist/components/Tooltip/Tooltip.svelte +282 -0
  190. package/dist/components/Tooltip/Tooltip.svelte.d.ts +55 -0
  191. package/dist/components/Tree/Tree.svelte +129 -0
  192. package/dist/components/Tree/Tree.svelte.d.ts +61 -0
  193. package/dist/components/Tree/TreeNode.svelte +332 -0
  194. package/dist/components/Tree/TreeNode.svelte.d.ts +55 -0
  195. package/dist/components/icons/TwintrinsicLogo.svelte +73 -0
  196. package/dist/components/icons/TwintrinsicLogo.svelte.d.ts +17 -0
  197. package/dist/components/icons/twintrinsic-source.svg +73 -0
  198. package/dist/components/icons/twintrinsic.svg +38 -0
  199. package/dist/docs/EventsTable.svelte +86 -0
  200. package/dist/docs/EventsTable.svelte.d.ts +27 -0
  201. package/dist/docs/PropsTable.svelte +103 -0
  202. package/dist/docs/PropsTable.svelte.d.ts +28 -0
  203. package/dist/docs/index.d.ts +2 -0
  204. package/dist/docs/index.js +2 -0
  205. package/dist/helpers/detectLanguage.d.ts +6 -0
  206. package/dist/helpers/detectLanguage.js +60 -0
  207. package/dist/helpers/index.d.ts +1 -0
  208. package/dist/helpers/index.js +1 -0
  209. package/dist/index.d.ts +86 -0
  210. package/dist/index.js +94 -0
  211. package/dist/twintrinsic.css +347 -0
  212. 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>