svelora 3.0.1 → 3.0.2

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 (107) hide show
  1. package/dist/Accordion/Accordion.svelte +66 -97
  2. package/dist/Alert/Alert.svelte +39 -64
  3. package/dist/Alert/Alert.svelte.d.ts +1 -1
  4. package/dist/Avatar/Avatar.svelte +35 -75
  5. package/dist/AvatarGroup/AvatarGroup.svelte +38 -55
  6. package/dist/Badge/Badge.svelte +28 -50
  7. package/dist/Banner/Banner.svelte +46 -41
  8. package/dist/Banner/Banner.svelte.d.ts +1 -1
  9. package/dist/Breadcrumb/Breadcrumb.svelte +32 -26
  10. package/dist/Button/Button.svelte +70 -138
  11. package/dist/Calendar/Calendar.svelte +94 -157
  12. package/dist/Calendar/Calendar.svelte.d.ts +1 -1
  13. package/dist/Card/Card.svelte +18 -31
  14. package/dist/Carousel/Carousel.svelte +118 -173
  15. package/dist/Checkbox/Checkbox.svelte +52 -97
  16. package/dist/CheckboxGroup/CheckboxGroup.svelte +62 -107
  17. package/dist/CheckboxGroup/CheckboxGroup.svelte.d.ts +1 -1
  18. package/dist/Chip/Chip.svelte +22 -34
  19. package/dist/CodeBlock/CodeBlock.svelte +42 -59
  20. package/dist/Collapsible/Collapsible.svelte +22 -38
  21. package/dist/Collapsible/Collapsible.svelte.d.ts +1 -1
  22. package/dist/Collapsible/CollapsibleTestWrapper.svelte +2 -5
  23. package/dist/Collapsible/CollapsibleTestWrapper.svelte.d.ts +1 -1
  24. package/dist/Command/Command.svelte +40 -77
  25. package/dist/Command/Command.svelte.d.ts +1 -1
  26. package/dist/Command/CommandTestWrapper.svelte +2 -10
  27. package/dist/Command/CommandTestWrapper.svelte.d.ts +1 -1
  28. package/dist/Container/Container.svelte +11 -14
  29. package/dist/ContextMenu/ContextMenu.svelte +51 -114
  30. package/dist/ContextMenu/ContextMenu.svelte.d.ts +1 -1
  31. package/dist/Drawer/Drawer.svelte +72 -110
  32. package/dist/Drawer/DrawerTriggerTestWrapper.svelte +1 -2
  33. package/dist/DropdownMenu/DropdownMenu.svelte +63 -124
  34. package/dist/DropdownMenu/DropdownMenu.svelte.d.ts +1 -1
  35. package/dist/DropdownMenu/DropdownMenuTriggerTestWrapper.svelte +2 -5
  36. package/dist/Editor/Editor.svelte +441 -576
  37. package/dist/Editor/Editor.svelte.d.ts +1 -1
  38. package/dist/Editor/EditorUrlPrompt.svelte +40 -53
  39. package/dist/Editor/SlashPopup.svelte +12 -24
  40. package/dist/Empty/Empty.svelte +32 -63
  41. package/dist/FieldGroup/FieldGroup.svelte +23 -38
  42. package/dist/FileUpload/FileUpload.svelte +242 -320
  43. package/dist/FileUpload/FileUpload.svelte.d.ts +1 -1
  44. package/dist/Fonts/Fonts.svelte +15 -37
  45. package/dist/Form/Form.svelte +112 -170
  46. package/dist/FormField/FormField.svelte +102 -135
  47. package/dist/Icon/Icon.svelte +7 -32
  48. package/dist/Input/Input.svelte +71 -141
  49. package/dist/Input/Input.svelte.d.ts +2 -2
  50. package/dist/Kbd/Kbd.svelte +18 -34
  51. package/dist/Link/Link.svelte +129 -196
  52. package/dist/LocaleButton/LocaleButton.svelte +165 -0
  53. package/dist/LocaleButton/LocaleButton.svelte.d.ts +5 -0
  54. package/dist/LocaleButton/index.d.ts +2 -0
  55. package/dist/LocaleButton/index.js +1 -0
  56. package/dist/LocaleButton/locale-button.types.d.ts +182 -0
  57. package/dist/LocaleButton/locale-button.types.js +1 -0
  58. package/dist/LocaleButton/locale-button.variants.d.ts +61 -0
  59. package/dist/LocaleButton/locale-button.variants.js +34 -0
  60. package/dist/Modal/Modal.svelte +52 -106
  61. package/dist/Modal/ModalTriggerTestWrapper.svelte +1 -2
  62. package/dist/Pagination/Pagination.svelte +48 -92
  63. package/dist/Pagination/pagination.variants.d.ts +1 -1
  64. package/dist/PinInput/PinInput.svelte +57 -111
  65. package/dist/PinInput/PinInput.svelte.d.ts +1 -1
  66. package/dist/Popover/Popover.svelte +28 -61
  67. package/dist/Popover/Popover.svelte.d.ts +1 -1
  68. package/dist/Progress/Progress.svelte +75 -94
  69. package/dist/RadioGroup/RadioGroup.svelte +54 -99
  70. package/dist/RadioGroup/RadioGroup.svelte.d.ts +1 -1
  71. package/dist/Select/Select.svelte +112 -269
  72. package/dist/Select/Select.svelte.d.ts +1 -1
  73. package/dist/SelectMenu/SelectMenu.svelte +211 -409
  74. package/dist/SelectMenu/SelectMenu.svelte.d.ts +1 -1
  75. package/dist/SelectMenu/SelectMenuFormFieldTestWrapper.svelte +3 -6
  76. package/dist/Separator/Separator.svelte +29 -44
  77. package/dist/Skeleton/Skeleton.svelte +11 -23
  78. package/dist/Slideover/Slideover.svelte +52 -106
  79. package/dist/Slideover/SlideoverTriggerTestWrapper.svelte +1 -2
  80. package/dist/Slider/Slider.svelte +48 -84
  81. package/dist/Slider/Slider.svelte.d.ts +1 -1
  82. package/dist/Stepper/Stepper.svelte +139 -132
  83. package/dist/Stepper/Stepper.svelte.d.ts +1 -1
  84. package/dist/Switch/Switch.svelte +62 -98
  85. package/dist/Table/Table.svelte +232 -283
  86. package/dist/Table/table.variants.d.ts +1 -1
  87. package/dist/Tabs/Tabs.svelte +96 -129
  88. package/dist/Tabs/Tabs.svelte.d.ts +1 -1
  89. package/dist/Textarea/Textarea.svelte +90 -173
  90. package/dist/Textarea/Textarea.svelte.d.ts +1 -1
  91. package/dist/ThemeModeButton/ThemeModeButton.svelte +16 -38
  92. package/dist/Timeline/Timeline.svelte +75 -54
  93. package/dist/Toast/Toaster.svelte +8 -25
  94. package/dist/Tooltip/Tooltip.svelte +34 -66
  95. package/dist/Tooltip/Tooltip.svelte.d.ts +1 -1
  96. package/dist/Tooltip/TooltipTestWrapper.svelte +2 -5
  97. package/dist/User/User.svelte +33 -49
  98. package/dist/docs/navigation.js +6 -0
  99. package/dist/hooks/HookContextProbe.svelte +2 -4
  100. package/dist/hooks/HookContextProvider.svelte +8 -6
  101. package/dist/hooks/HookEmitProbe.svelte +8 -11
  102. package/dist/i18n.d.ts +2 -0
  103. package/dist/i18n.js +19 -0
  104. package/dist/index.d.ts +1 -0
  105. package/dist/index.js +1 -0
  106. package/dist/mcp/svelora-docs.data.json +4 -2
  107. package/package.json +16 -8
@@ -1,325 +1,247 @@
1
- <script lang="ts" module>
2
- import type { FileUploadProps, FileUploadRejection } from './file-upload.types.js'
3
-
4
- export type Props = FileUploadProps
1
+ <script lang="ts" module>export {};
5
2
  </script>
6
3
 
7
- <script lang="ts">
8
- import { useId } from 'bits-ui'
9
- import { untrack } from 'svelte'
10
- import Button from '../Button/Button.svelte'
11
- import { getComponentConfig, iconsDefaults } from '../config.js'
12
- import { useFormField, useFormFieldEmit } from '../hooks/useFormField.svelte.js'
13
- import Icon from '../Icon/Icon.svelte'
14
- import Modal from '../Modal/Modal.svelte'
15
- import { fileUploadDefaults, fileUploadVariants } from './file-upload.variants.js'
16
-
17
- const config = getComponentConfig('fileUpload', fileUploadDefaults)
18
- const icons = getComponentConfig('icons', iconsDefaults)
19
-
20
- let {
21
- ref = $bindable(null),
22
- value = $bindable([]),
23
- onValueChange,
24
- multiple = false,
25
- accept,
26
- maxSize,
27
- maxFiles,
28
- onReject,
29
- dropzone = true,
30
- interactive = true,
31
- label = 'Drop files here or click to upload',
32
- description,
33
- icon = icons.upload,
34
- color = config.defaultVariants.color,
35
- size = config.defaultVariants.size,
36
- variant = config.defaultVariants.variant,
37
- layout = config.defaultVariants.layout,
38
- disabled = false,
39
- loading = false,
40
- loadingIcon = icons.loading,
41
- preview = true,
42
- highlight = false,
43
- required = false,
44
- fileIcon = icons.file,
45
- imagePreview = true,
46
- id,
47
- name,
48
- leadingSlot,
49
- labelSlot,
50
- descriptionSlot,
51
- actionsSlot,
52
- filesSlot,
53
- fileSlot,
54
- children,
55
- class: className,
56
- ui,
57
- ...restProps
58
- }: Props = $props()
59
-
60
- const autoId = useId()
61
-
62
- let inputRef = $state<HTMLInputElement | null>(null)
63
- let dragCounter = $state(0)
64
- let previewOpen = $state(false)
65
- let previewFile = $state<File | null>(null)
66
-
67
- const formFieldContext = useFormField()
68
- const emit = useFormFieldEmit()
69
-
70
- // Stable file identity key with separator to avoid collisions
71
- const fileKey = (f: File) => `${f.name}:${f.size}:${f.lastModified}`
72
-
73
- // Plain Map intentionally urlCache is an internal optimization cache, not reactive UI
74
- // state. SvelteMap mutations would cascade re-renders every time a URL is cached/evicted.
75
- // eslint-disable-next-line svelte/prefer-svelte-reactivity
76
- const urlCache = new Map<string, string>()
77
-
78
- const hasError = $derived(
79
- formFieldContext?.error !== undefined && formFieldContext?.error !== false
80
- )
81
- const resolvedHighlight = $derived(highlight || hasError)
82
- const resolvedId = $derived(id ?? formFieldContext?.ariaId)
83
- const resolvedName = $derived(name ?? formFieldContext?.name)
84
- const ariaDescribedBy = $derived(
85
- !formFieldContext
86
- ? undefined
87
- : hasError
88
- ? `${formFieldContext.ariaId}-error`
89
- : `${formFieldContext.ariaId}-description ${formFieldContext.ariaId}-help`
90
- )
91
-
92
- const isDisabled = $derived(disabled || loading)
93
- const isDragging = $derived(dragCounter > 0)
94
- const isFull = $derived(maxFiles !== undefined && value.length >= maxFiles)
95
- const showFilesInside = $derived(
96
- layout === 'grid' && !multiple && value.length > 0 && preview && variant === 'area'
97
- )
98
-
99
- // Pass booleans directly so compound variants with `false` values match correctly
100
- const slots = $derived(
101
- fileUploadVariants({
102
- color: hasError ? 'error' : color,
103
- size,
104
- variant,
105
- layout,
106
- dropzone,
107
- interactive: interactive && !isDisabled,
108
- highlight: resolvedHighlight,
109
- multiple,
110
- disabled: isDisabled
111
- })
112
- )
113
-
114
- const classes = $derived.by(() => {
115
- const u = ui ?? {}
116
- return {
117
- root: slots.root({ class: [config.slots.root, className, u.root] }),
118
- base: slots.base({ class: [config.slots.base, u.base] }),
119
- wrapper: slots.wrapper({ class: [config.slots.wrapper, u.wrapper] }),
120
- icon: slots.icon({ class: [config.slots.icon, u.icon] }),
121
- label: slots.label({ class: [config.slots.label, u.label] }),
122
- description: slots.description({ class: [config.slots.description, u.description] }),
123
- actions: slots.actions({ class: [config.slots.actions, u.actions] }),
124
- files: slots.files({ class: [config.slots.files, u.files] }),
125
- file: slots.file({ class: [config.slots.file, u.file] }),
126
- fileLeading: slots.fileLeading({ class: [config.slots.fileLeading, u.fileLeading] }),
127
- fileWrapper: slots.fileWrapper({ class: [config.slots.fileWrapper, u.fileWrapper] }),
128
- fileName: slots.fileName({ class: [config.slots.fileName, u.fileName] }),
129
- fileSize: slots.fileSize({ class: [config.slots.fileSize, u.fileSize] }),
130
- fileTrailing: slots.fileTrailing({
131
- class: [config.slots.fileTrailing, u.fileTrailing]
132
- }),
133
- previewContent: slots.previewContent({
134
- class: [config.slots.previewContent, u.previewContent]
135
- }),
136
- previewBody: slots.previewBody({ class: [config.slots.previewBody, u.previewBody] })
137
- }
138
- })
139
-
140
- // Revoke blob URLs for files no longer in value (handles external bind:value resets)
141
- $effect(() => {
142
- const current = new Set(value.map(fileKey))
143
- // Close preview if its file was removed externally — untrack to avoid subscribing to previewFile
144
- const pf = untrack(() => previewFile)
145
- if (pf && !current.has(fileKey(pf))) {
146
- previewOpen = false
147
- previewFile = null
148
- }
149
- for (const [key, url] of urlCache) {
150
- if (!current.has(key)) {
151
- URL.revokeObjectURL(url)
152
- urlCache.delete(key)
153
- }
154
- }
155
- })
156
-
157
- $effect(() => {
158
- return () => {
159
- for (const [, url] of urlCache) URL.revokeObjectURL(url)
160
- urlCache.clear()
161
- }
162
- })
163
-
164
- export function open() {
165
- if (isDisabled) return
166
- inputRef?.click()
167
- }
168
-
169
- function isFileAccepted(file: File): boolean {
170
- if (!accept) return true
171
- return accept
172
- .split(',')
173
- .map((s) => s.trim())
174
- .some((token) => {
175
- if (!token || token === '*') return true
176
- if (token.startsWith('.'))
177
- return file.name.toLowerCase().endsWith(token.toLowerCase())
178
- if (token.endsWith('/*')) return file.type.startsWith(token.slice(0, -1))
179
- return file.type === token
180
- })
181
- }
182
-
183
- function validateIngress(file: File): FileUploadRejection['reason'] | null {
184
- if (accept && !isFileAccepted(file)) return 'accept'
185
- if (maxSize !== undefined && file.size > maxSize) return 'maxSize'
186
- return null
187
- }
188
-
189
- function applyMaxFiles(candidates: File[]): {
190
- accepted: File[]
191
- rejected: FileUploadRejection[]
192
- } {
193
- if (maxFiles === undefined) return { accepted: candidates, rejected: [] }
194
- const remaining = Math.max(0, maxFiles - value.length)
195
- if (candidates.length <= remaining) return { accepted: candidates, rejected: [] }
196
- return {
197
- accepted: candidates.slice(0, remaining),
198
- rejected: candidates.slice(remaining).map((file) => ({ file, reason: 'maxFiles' }))
199
- }
200
- }
201
-
202
- function addFiles(newFiles: File[]) {
203
- if (isDisabled) return
204
-
205
- const rejected: FileUploadRejection[] = []
206
- const passed: File[] = []
207
- for (const file of newFiles) {
208
- const reason = validateIngress(file)
209
- if (reason) rejected.push({ file, reason })
210
- else passed.push(file)
211
- }
212
-
213
- let accepted: File[]
214
- if (!multiple) {
215
- accepted = passed.slice(0, 1)
216
- } else {
217
- const existing = new Set(value.map(fileKey))
218
- const deduped = passed.filter((f) => !existing.has(fileKey(f)))
219
- const result = applyMaxFiles(deduped)
220
- accepted = result.accepted
221
- rejected.push(...result.rejected)
222
- }
223
-
224
- if (accepted.length) {
225
- value = multiple ? [...value, ...accepted] : accepted
226
- onValueChange?.(value)
227
- emit.onChange()
228
- }
229
- if (rejected.length) onReject?.(rejected)
230
- }
231
-
232
- function removeFile(index: number) {
233
- const removed = value[index]
234
- const key = fileKey(removed)
235
- if (previewFile && fileKey(previewFile) === key) {
236
- previewOpen = false
237
- previewFile = null
238
- }
239
- if (urlCache.has(key)) {
240
- URL.revokeObjectURL(urlCache.get(key)!)
241
- urlCache.delete(key)
242
- }
243
- value = value.filter((_, i) => i !== index)
244
- onValueChange?.(value)
245
- emit.onChange()
246
- }
247
-
248
- export function clearAll() {
249
- previewOpen = false
250
- previewFile = null
251
- for (const [, url] of urlCache) URL.revokeObjectURL(url)
252
- urlCache.clear()
253
- value = []
254
- onValueChange?.(value)
255
- emit.onChange()
256
- }
257
-
258
- function handleInputChange(e: Event) {
259
- const input = e.target as HTMLInputElement
260
- if (input.files?.length) {
261
- addFiles(Array.from(input.files))
262
- input.value = ''
263
- }
264
- }
265
-
266
- function handleDrop(e: DragEvent) {
267
- e.preventDefault()
268
- dragCounter = 0
269
- if (!dropzone || isDisabled) return
270
- const files = e.dataTransfer?.files
271
- if (files?.length) addFiles(Array.from(files))
272
- }
273
-
274
- function handleDragEnter(e: DragEvent) {
275
- e.preventDefault()
276
- if (dropzone && !isDisabled) dragCounter++
277
- }
278
-
279
- function handleDragOver(e: DragEvent) {
280
- e.preventDefault()
281
- }
282
-
283
- function handleDragLeave() {
284
- dragCounter = Math.max(0, dragCounter - 1)
285
- }
286
-
287
- function handleAreaClick() {
288
- if (interactive && !isDisabled) open()
289
- }
290
-
291
- function handleKeydown(e: KeyboardEvent) {
292
- if (!interactive) return
293
- if (e.key === 'Enter' || e.key === ' ') {
294
- e.preventDefault()
295
- open()
296
- }
297
- }
298
-
299
- function formatFileSize(bytes: number): string {
300
- if (bytes === 0) return '0 B'
301
- const k = 1024
302
- const sizes = ['B', 'KB', 'MB', 'GB']
303
- const i = Math.floor(Math.log(bytes) / Math.log(k))
304
- return `${(bytes / k ** i).toFixed(1)} ${sizes[i]}`
305
- }
306
-
307
- function isImageFile(file: File): boolean {
308
- return file.type.startsWith('image/')
309
- }
310
-
311
- function openPreview(file: File) {
312
- previewFile = file
313
- previewOpen = true
314
- }
315
-
316
- function getObjectUrl(file: File): string {
317
- const key = fileKey(file)
318
- if (!urlCache.has(key)) {
319
- urlCache.set(key, URL.createObjectURL(file))
320
- }
321
- return urlCache.get(key)!
322
- }
4
+ <script lang="ts">import { useId } from "bits-ui";
5
+ import { untrack } from "svelte";
6
+ import Button from "../Button/Button.svelte";
7
+ import { getComponentConfig, iconsDefaults } from "../config.js";
8
+ import { useFormField, useFormFieldEmit } from "../hooks/useFormField.svelte.js";
9
+ import Icon from "../Icon/Icon.svelte";
10
+ import Modal from "../Modal/Modal.svelte";
11
+ import { fileUploadDefaults, fileUploadVariants } from "./file-upload.variants.js";
12
+ const config = getComponentConfig("fileUpload", fileUploadDefaults);
13
+ const icons = getComponentConfig("icons", iconsDefaults);
14
+ let { ref = $bindable(null), value = $bindable([]), onValueChange, multiple = false, accept, maxSize, maxFiles, onReject, dropzone = true, interactive = true, label = "Drop files here or click to upload", description, icon = icons.upload, color = config.defaultVariants.color, size = config.defaultVariants.size, variant = config.defaultVariants.variant, layout = config.defaultVariants.layout, disabled = false, loading = false, loadingIcon = icons.loading, preview = true, highlight = false, required = false, fileIcon = icons.file, imagePreview = true, id, name, leadingSlot, labelSlot, descriptionSlot, actionsSlot, filesSlot, fileSlot, children, class: className, ui, ...restProps } = $props();
15
+ const autoId = useId();
16
+ let inputRef = $state(null);
17
+ let dragCounter = $state(0);
18
+ let previewOpen = $state(false);
19
+ let previewFile = $state(null);
20
+ const formFieldContext = useFormField();
21
+ const emit = useFormFieldEmit();
22
+ // Stable file identity key with separator to avoid collisions
23
+ const fileKey = (f) => `${f.name}:${f.size}:${f.lastModified}`;
24
+ // Plain Map intentionally — urlCache is an internal optimization cache, not reactive UI
25
+ // state. SvelteMap mutations would cascade re-renders every time a URL is cached/evicted.
26
+ // eslint-disable-next-line svelte/prefer-svelte-reactivity
27
+ const urlCache = new Map();
28
+ const hasError = $derived(formFieldContext?.error !== undefined && formFieldContext?.error !== false);
29
+ const resolvedHighlight = $derived(highlight || hasError);
30
+ const resolvedId = $derived(id ?? formFieldContext?.ariaId);
31
+ const resolvedName = $derived(name ?? formFieldContext?.name);
32
+ const ariaDescribedBy = $derived(!formFieldContext ? undefined : hasError ? `${formFieldContext.ariaId}-error` : `${formFieldContext.ariaId}-description ${formFieldContext.ariaId}-help`);
33
+ const isDisabled = $derived(disabled || loading);
34
+ const isDragging = $derived(dragCounter > 0);
35
+ const isFull = $derived(maxFiles !== undefined && value.length >= maxFiles);
36
+ const showFilesInside = $derived(layout === "grid" && !multiple && value.length > 0 && preview && variant === "area");
37
+ // Pass booleans directly so compound variants with `false` values match correctly
38
+ const slots = $derived(fileUploadVariants({
39
+ color: hasError ? "error" : color,
40
+ size,
41
+ variant,
42
+ layout,
43
+ dropzone,
44
+ interactive: interactive && !isDisabled,
45
+ highlight: resolvedHighlight,
46
+ multiple,
47
+ disabled: isDisabled
48
+ }));
49
+ const classes = $derived.by(() => {
50
+ const u = ui ?? {};
51
+ return {
52
+ root: slots.root({ class: [
53
+ config.slots.root,
54
+ className,
55
+ u.root
56
+ ] }),
57
+ base: slots.base({ class: [config.slots.base, u.base] }),
58
+ wrapper: slots.wrapper({ class: [config.slots.wrapper, u.wrapper] }),
59
+ icon: slots.icon({ class: [config.slots.icon, u.icon] }),
60
+ label: slots.label({ class: [config.slots.label, u.label] }),
61
+ description: slots.description({ class: [config.slots.description, u.description] }),
62
+ actions: slots.actions({ class: [config.slots.actions, u.actions] }),
63
+ files: slots.files({ class: [config.slots.files, u.files] }),
64
+ file: slots.file({ class: [config.slots.file, u.file] }),
65
+ fileLeading: slots.fileLeading({ class: [config.slots.fileLeading, u.fileLeading] }),
66
+ fileWrapper: slots.fileWrapper({ class: [config.slots.fileWrapper, u.fileWrapper] }),
67
+ fileName: slots.fileName({ class: [config.slots.fileName, u.fileName] }),
68
+ fileSize: slots.fileSize({ class: [config.slots.fileSize, u.fileSize] }),
69
+ fileTrailing: slots.fileTrailing({ class: [config.slots.fileTrailing, u.fileTrailing] }),
70
+ previewContent: slots.previewContent({ class: [config.slots.previewContent, u.previewContent] }),
71
+ previewBody: slots.previewBody({ class: [config.slots.previewBody, u.previewBody] })
72
+ };
73
+ });
74
+ // Revoke blob URLs for files no longer in value (handles external bind:value resets)
75
+ $effect(() => {
76
+ const current = new Set(value.map(fileKey));
77
+ // Close preview if its file was removed externally — untrack to avoid subscribing to previewFile
78
+ const pf = untrack(() => previewFile);
79
+ if (pf && !current.has(fileKey(pf))) {
80
+ previewOpen = false;
81
+ previewFile = null;
82
+ }
83
+ for (const [key, url] of urlCache) {
84
+ if (!current.has(key)) {
85
+ URL.revokeObjectURL(url);
86
+ urlCache.delete(key);
87
+ }
88
+ }
89
+ });
90
+ $effect(() => {
91
+ return () => {
92
+ for (const [, url] of urlCache) URL.revokeObjectURL(url);
93
+ urlCache.clear();
94
+ };
95
+ });
96
+ export function open() {
97
+ if (isDisabled) return;
98
+ inputRef?.click();
99
+ }
100
+ function isFileAccepted(file) {
101
+ if (!accept) return true;
102
+ return accept.split(",").map((s) => s.trim()).some((token) => {
103
+ if (!token || token === "*") return true;
104
+ if (token.startsWith(".")) return file.name.toLowerCase().endsWith(token.toLowerCase());
105
+ if (token.endsWith("/*")) return file.type.startsWith(token.slice(0, -1));
106
+ return file.type === token;
107
+ });
108
+ }
109
+ function validateIngress(file) {
110
+ if (accept && !isFileAccepted(file)) return "accept";
111
+ if (maxSize !== undefined && file.size > maxSize) return "maxSize";
112
+ return null;
113
+ }
114
+ function applyMaxFiles(candidates) {
115
+ if (maxFiles === undefined) return {
116
+ accepted: candidates,
117
+ rejected: []
118
+ };
119
+ const remaining = Math.max(0, maxFiles - value.length);
120
+ if (candidates.length <= remaining) return {
121
+ accepted: candidates,
122
+ rejected: []
123
+ };
124
+ return {
125
+ accepted: candidates.slice(0, remaining),
126
+ rejected: candidates.slice(remaining).map((file) => ({
127
+ file,
128
+ reason: "maxFiles"
129
+ }))
130
+ };
131
+ }
132
+ function addFiles(newFiles) {
133
+ if (isDisabled) return;
134
+ const rejected = [];
135
+ const passed = [];
136
+ for (const file of newFiles) {
137
+ const reason = validateIngress(file);
138
+ if (reason) rejected.push({
139
+ file,
140
+ reason
141
+ });
142
+ else passed.push(file);
143
+ }
144
+ let accepted;
145
+ if (!multiple) {
146
+ accepted = passed.slice(0, 1);
147
+ } else {
148
+ const existing = new Set(value.map(fileKey));
149
+ const deduped = passed.filter((f) => !existing.has(fileKey(f)));
150
+ const result = applyMaxFiles(deduped);
151
+ accepted = result.accepted;
152
+ rejected.push(...result.rejected);
153
+ }
154
+ if (accepted.length) {
155
+ value = multiple ? [...value, ...accepted] : accepted;
156
+ onValueChange?.(value);
157
+ emit.onChange();
158
+ }
159
+ if (rejected.length) onReject?.(rejected);
160
+ }
161
+ function removeFile(index) {
162
+ const removed = value[index];
163
+ const key = fileKey(removed);
164
+ if (previewFile && fileKey(previewFile) === key) {
165
+ previewOpen = false;
166
+ previewFile = null;
167
+ }
168
+ if (urlCache.has(key)) {
169
+ URL.revokeObjectURL(urlCache.get(key));
170
+ urlCache.delete(key);
171
+ }
172
+ value = value.filter((_, i) => i !== index);
173
+ onValueChange?.(value);
174
+ emit.onChange();
175
+ }
176
+ export function clearAll() {
177
+ previewOpen = false;
178
+ previewFile = null;
179
+ for (const [, url] of urlCache) URL.revokeObjectURL(url);
180
+ urlCache.clear();
181
+ value = [];
182
+ onValueChange?.(value);
183
+ emit.onChange();
184
+ }
185
+ function handleInputChange(e) {
186
+ const input = e.target;
187
+ if (input.files?.length) {
188
+ addFiles(Array.from(input.files));
189
+ input.value = "";
190
+ }
191
+ }
192
+ function handleDrop(e) {
193
+ e.preventDefault();
194
+ dragCounter = 0;
195
+ if (!dropzone || isDisabled) return;
196
+ const files = e.dataTransfer?.files;
197
+ if (files?.length) addFiles(Array.from(files));
198
+ }
199
+ function handleDragEnter(e) {
200
+ e.preventDefault();
201
+ if (dropzone && !isDisabled) dragCounter++;
202
+ }
203
+ function handleDragOver(e) {
204
+ e.preventDefault();
205
+ }
206
+ function handleDragLeave() {
207
+ dragCounter = Math.max(0, dragCounter - 1);
208
+ }
209
+ function handleAreaClick() {
210
+ if (interactive && !isDisabled) open();
211
+ }
212
+ function handleKeydown(e) {
213
+ if (!interactive) return;
214
+ if (e.key === "Enter" || e.key === " ") {
215
+ e.preventDefault();
216
+ open();
217
+ }
218
+ }
219
+ function formatFileSize(bytes) {
220
+ if (bytes === 0) return "0 B";
221
+ const k = 1024;
222
+ const sizes = [
223
+ "B",
224
+ "KB",
225
+ "MB",
226
+ "GB"
227
+ ];
228
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
229
+ return `${(bytes / k ** i).toFixed(1)} ${sizes[i]}`;
230
+ }
231
+ function isImageFile(file) {
232
+ return file.type.startsWith("image/");
233
+ }
234
+ function openPreview(file) {
235
+ previewFile = file;
236
+ previewOpen = true;
237
+ }
238
+ function getObjectUrl(file) {
239
+ const key = fileKey(file);
240
+ if (!urlCache.has(key)) {
241
+ urlCache.set(key, URL.createObjectURL(file));
242
+ }
243
+ return urlCache.get(key);
244
+ }
323
245
  </script>
324
246
 
325
247
  <div {...restProps} bind:this={ref} class={classes.root} data-full={isFull ? '' : undefined}>
@@ -3,6 +3,6 @@ export type Props = FileUploadProps;
3
3
  declare const FileUpload: import("svelte").Component<FileUploadProps, {
4
4
  open: () => void;
5
5
  clearAll: () => void;
6
- }, "value" | "ref">;
6
+ }, "ref" | "value">;
7
7
  type FileUpload = ReturnType<typeof FileUpload>;
8
8
  export default FileUpload;
@@ -1,40 +1,19 @@
1
- <script lang="ts" module>
2
- import type { FontsProps } from './fonts.types.js'
3
-
4
- export type Props = FontsProps
1
+ <script lang="ts" module>export {};
5
2
  </script>
6
3
 
7
- <script lang="ts">
8
- import { getConfig } from '../config.js'
9
- import {
10
- buildFontVariablesCss,
11
- buildGoogleFontsUrl,
12
- buildLocalFontFaceCss,
13
- fontsDefaults,
14
- resolveFontsOptions
15
- } from './fonts.js'
16
-
17
- const globalConfig = getConfig()
18
- const config = resolveFontsOptions(globalConfig.fonts)
19
-
20
- let {
21
- fonts,
22
- families,
23
- display,
24
- preconnect
25
- }: Props = $props()
26
-
27
- const resolvedFamilies = $derived(families ?? fonts ?? (config === false ? [] : config.families ?? []))
28
- const resolvedDisplay = $derived(display ?? (config === false ? fontsDefaults.display : config.display))
29
- const resolvedPreconnect = $derived(
30
- preconnect ?? (config === false ? fontsDefaults.preconnect : config.preconnect)
31
- )
32
-
33
- const href = $derived(buildGoogleFontsUrl(resolvedFamilies, resolvedDisplay))
34
- const localFontFaceCss = $derived(buildLocalFontFaceCss(resolvedFamilies, resolvedDisplay))
35
- const variableCss = $derived(buildFontVariablesCss(resolvedFamilies))
36
- const inlineCss = $derived([localFontFaceCss, variableCss].filter((value) => value.length > 0).join('\n\n'))
37
- const hasFonts = $derived(href.length > 0 || inlineCss.length > 0)
4
+ <script lang="ts">import { getConfig } from "../config.js";
5
+ import { buildFontVariablesCss, buildGoogleFontsUrl, buildLocalFontFaceCss, fontsDefaults, resolveFontsOptions } from "./fonts.js";
6
+ const globalConfig = getConfig();
7
+ const config = resolveFontsOptions(globalConfig.fonts);
8
+ let { fonts, families, display, preconnect } = $props();
9
+ const resolvedFamilies = $derived(families ?? fonts ?? (config === false ? [] : config.families ?? []));
10
+ const resolvedDisplay = $derived(display ?? (config === false ? fontsDefaults.display : config.display));
11
+ const resolvedPreconnect = $derived(preconnect ?? (config === false ? fontsDefaults.preconnect : config.preconnect));
12
+ const href = $derived(buildGoogleFontsUrl(resolvedFamilies, resolvedDisplay));
13
+ const localFontFaceCss = $derived(buildLocalFontFaceCss(resolvedFamilies, resolvedDisplay));
14
+ const variableCss = $derived(buildFontVariablesCss(resolvedFamilies));
15
+ const inlineCss = $derived([localFontFaceCss, variableCss].filter((value) => value.length > 0).join("\n\n"));
16
+ const hasFonts = $derived(href.length > 0 || inlineCss.length > 0);
38
17
  </script>
39
18
 
40
19
  <svelte:head>
@@ -47,8 +26,7 @@
47
26
  <link rel="stylesheet" href={href} />
48
27
  {/if}
49
28
  {#if inlineCss}
50
- <!-- eslint-disable-next-line svelte/no-at-html-tags -->
51
- {@html `<style>${inlineCss}</style>`}
29
+ <svelte:element this={'style'}>{inlineCss}</svelte:element>
52
30
  {/if}
53
31
  {/if}
54
32
  </svelte:head>