svelte-comp 1.3.3 → 1.3.6

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 (138) hide show
  1. package/LICENSE.md +21 -21
  2. package/README.md +101 -100
  3. package/dist/App.svelte +507 -507
  4. package/dist/Container.svelte +59 -59
  5. package/dist/app.css +234 -235
  6. package/dist/app.d.ts +10 -0
  7. package/dist/lib/Accordion.svelte +155 -155
  8. package/dist/lib/Badge.svelte +44 -44
  9. package/dist/lib/Button.svelte +185 -170
  10. package/dist/lib/Calendar.svelte +384 -384
  11. package/dist/lib/Card.svelte +103 -103
  12. package/dist/lib/Carousel.svelte +293 -293
  13. package/dist/lib/Carousel.svelte.d.ts +1 -1
  14. package/dist/lib/CheckBox.svelte +210 -210
  15. package/dist/lib/CodeView.svelte +308 -307
  16. package/dist/lib/ColorPicker.svelte +159 -159
  17. package/dist/lib/ContextMenu.svelte +328 -322
  18. package/dist/lib/DatePicker.svelte +246 -246
  19. package/dist/lib/Dialog.svelte +233 -233
  20. package/dist/lib/Field.svelte +299 -299
  21. package/dist/lib/FilePicker.svelte +295 -240
  22. package/dist/lib/FilePicker.svelte.d.ts +6 -1
  23. package/dist/lib/Form.svelte +438 -438
  24. package/dist/lib/Hamburger.svelte +217 -217
  25. package/dist/lib/InstallPWA.svelte +94 -94
  26. package/dist/lib/Menu.svelte +623 -623
  27. package/dist/lib/NoticeBase.svelte +140 -140
  28. package/dist/lib/PaginatedCard.svelte +73 -73
  29. package/dist/lib/Pagination.svelte +119 -119
  30. package/dist/lib/PrimaryColorSelect.svelte +111 -111
  31. package/dist/lib/ProgressBar.svelte +141 -141
  32. package/dist/lib/ProgressCircle.svelte +190 -190
  33. package/dist/lib/Radio.svelte +189 -189
  34. package/dist/lib/SearchInput.svelte +104 -104
  35. package/dist/lib/Select.svelte +524 -524
  36. package/dist/lib/Slider.svelte +253 -253
  37. package/dist/lib/Splitter.svelte +159 -150
  38. package/dist/lib/Switch.svelte +168 -167
  39. package/dist/lib/Table.svelte +299 -299
  40. package/dist/lib/Tabs.svelte +213 -213
  41. package/dist/lib/ThemeToggle.svelte +128 -127
  42. package/dist/lib/TimePicker.svelte +312 -312
  43. package/dist/lib/TimePickerNew.svelte +634 -0
  44. package/dist/lib/TimePickerNew.svelte.d.ts +49 -0
  45. package/dist/lib/Toast.svelte +123 -123
  46. package/dist/lib/Tooltip.svelte +110 -110
  47. package/dist/lib/Topbar.svelte +107 -107
  48. package/dist/lib/__tests__/Accordion.test.d.ts +1 -0
  49. package/dist/lib/__tests__/Accordion.test.js +171 -0
  50. package/dist/lib/__tests__/Badge.test.d.ts +1 -0
  51. package/dist/lib/__tests__/Badge.test.js +41 -0
  52. package/dist/lib/__tests__/Button.test.d.ts +1 -0
  53. package/dist/lib/__tests__/Button.test.js +269 -0
  54. package/dist/lib/__tests__/Calendar.test.d.ts +1 -0
  55. package/dist/lib/__tests__/Calendar.test.js +171 -0
  56. package/dist/lib/__tests__/Card.test.d.ts +1 -0
  57. package/dist/lib/__tests__/Card.test.js +148 -0
  58. package/dist/lib/__tests__/Carousel.test.d.ts +1 -0
  59. package/dist/lib/__tests__/Carousel.test.js +439 -0
  60. package/dist/lib/__tests__/CheckBox.test.d.ts +1 -0
  61. package/dist/lib/__tests__/CheckBox.test.js +152 -0
  62. package/dist/lib/__tests__/CodeView.test.d.ts +1 -0
  63. package/dist/lib/__tests__/CodeView.test.js +157 -0
  64. package/dist/lib/__tests__/ColorPicker.test.d.ts +1 -0
  65. package/dist/lib/__tests__/ColorPicker.test.js +93 -0
  66. package/dist/lib/__tests__/ContextMenu.test.d.ts +1 -0
  67. package/dist/lib/__tests__/ContextMenu.test.js +67 -0
  68. package/dist/lib/__tests__/DatePicker.test.d.ts +1 -0
  69. package/dist/lib/__tests__/DatePicker.test.js +108 -0
  70. package/dist/lib/__tests__/Dialog.test.d.ts +1 -0
  71. package/dist/lib/__tests__/Dialog.test.js +183 -0
  72. package/dist/lib/__tests__/Field.test.d.ts +1 -0
  73. package/dist/lib/__tests__/Field.test.js +190 -0
  74. package/dist/lib/__tests__/FilePicker.test.d.ts +1 -0
  75. package/dist/lib/__tests__/FilePicker.test.js +179 -0
  76. package/dist/lib/__tests__/Form.integration.test.d.ts +1 -0
  77. package/dist/lib/__tests__/Form.integration.test.js +158 -0
  78. package/dist/lib/__tests__/Form.test.d.ts +1 -0
  79. package/dist/lib/__tests__/Form.test.js +463 -0
  80. package/dist/lib/__tests__/Hamburger.test.d.ts +1 -0
  81. package/dist/lib/__tests__/Hamburger.test.js +161 -0
  82. package/dist/lib/__tests__/InstallPWA.test.d.ts +1 -0
  83. package/dist/lib/__tests__/InstallPWA.test.js +15 -0
  84. package/dist/lib/__tests__/Menu.test.d.ts +1 -0
  85. package/dist/lib/__tests__/Menu.test.js +285 -0
  86. package/dist/lib/__tests__/NoticeBase.test.d.ts +1 -0
  87. package/dist/lib/__tests__/NoticeBase.test.js +60 -0
  88. package/dist/lib/__tests__/PaginatedCard.test.d.ts +1 -0
  89. package/dist/lib/__tests__/PaginatedCard.test.js +89 -0
  90. package/dist/lib/__tests__/Pagination.test.d.ts +1 -0
  91. package/dist/lib/__tests__/Pagination.test.js +168 -0
  92. package/dist/lib/__tests__/PrimaryColorSelect.test.d.ts +1 -0
  93. package/dist/lib/__tests__/PrimaryColorSelect.test.js +92 -0
  94. package/dist/lib/__tests__/ProgressBar.test.d.ts +1 -0
  95. package/dist/lib/__tests__/ProgressBar.test.js +69 -0
  96. package/dist/lib/__tests__/ProgressCircle.test.d.ts +1 -0
  97. package/dist/lib/__tests__/ProgressCircle.test.js +71 -0
  98. package/dist/lib/__tests__/Radio.test.d.ts +1 -0
  99. package/dist/lib/__tests__/Radio.test.js +127 -0
  100. package/dist/lib/__tests__/SearchInput.test.d.ts +1 -0
  101. package/dist/lib/__tests__/SearchInput.test.js +80 -0
  102. package/dist/lib/__tests__/Select.test.d.ts +1 -0
  103. package/dist/lib/__tests__/Select.test.js +408 -0
  104. package/dist/lib/__tests__/Slider.test.d.ts +1 -0
  105. package/dist/lib/__tests__/Slider.test.js +213 -0
  106. package/dist/lib/__tests__/Splitter.test.d.ts +1 -0
  107. package/dist/lib/__tests__/Splitter.test.js +87 -0
  108. package/dist/lib/__tests__/Switch.test.d.ts +1 -0
  109. package/dist/lib/__tests__/Switch.test.js +97 -0
  110. package/dist/lib/__tests__/Table.test.d.ts +1 -0
  111. package/dist/lib/__tests__/Table.test.js +349 -0
  112. package/dist/lib/__tests__/Tabs.test.d.ts +1 -0
  113. package/dist/lib/__tests__/Tabs.test.js +262 -0
  114. package/dist/lib/__tests__/ThemeToggle.test.d.ts +1 -0
  115. package/dist/lib/__tests__/ThemeToggle.test.js +84 -0
  116. package/dist/lib/__tests__/TimePicker.test.d.ts +1 -0
  117. package/dist/lib/__tests__/TimePicker.test.js +146 -0
  118. package/dist/lib/__tests__/TimePickerNew.test.d.ts +1 -0
  119. package/dist/lib/__tests__/TimePickerNew.test.js +322 -0
  120. package/dist/lib/__tests__/Toast.test.d.ts +1 -0
  121. package/dist/lib/__tests__/Toast.test.js +135 -0
  122. package/dist/lib/__tests__/Tooltip.test.d.ts +1 -0
  123. package/dist/lib/__tests__/Tooltip.test.js +171 -0
  124. package/dist/lib/__tests__/Topbar.test.d.ts +1 -0
  125. package/dist/lib/__tests__/Topbar.test.js +25 -0
  126. package/dist/lib/__tests__/setupLangContext.d.ts +1 -0
  127. package/dist/lib/__tests__/setupLangContext.js +65 -0
  128. package/dist/lib/__tests__/storage.test.d.ts +1 -0
  129. package/dist/lib/__tests__/storage.test.js +124 -0
  130. package/dist/lib/__tests__/utils.test.d.ts +1 -0
  131. package/dist/lib/__tests__/utils.test.js +11 -0
  132. package/dist/lib/index.d.ts +1 -0
  133. package/dist/lib/index.js +1 -0
  134. package/dist/lib/lang.d.ts +4 -0
  135. package/dist/lib/lang.js +4 -0
  136. package/dist/styles.css +234 -232
  137. package/dist/utils/index.js +15 -4
  138. package/package.json +52 -52
@@ -1,240 +1,295 @@
1
- <!-- src/lib/FilePicker.svelte -->
2
- <script lang="ts">
3
- /**
4
- * @component FilePicker
5
- * @description Lightweight file selector with click support and drag-and-drop. Internally uses a hidden `<input type="file">` plus a drop zone.
6
- *
7
- * @prop accept {string} - Accepted file types
8
- * @default "*\\/*"
9
- *
10
- * @prop multiple {boolean} - Allows selecting multiple files
11
- * @default false
12
- *
13
- * @prop label {string} - Button label; falls back to localized text
14
- *
15
- * @prop disabled {boolean} - Disables all interactions
16
- * @default false
17
- *
18
- * @prop clearable {boolean} - Shows a clear button to reset selection
19
- * @default true
20
- *
21
- * @prop placeholder {string} - Placeholder text for the drop zone
22
- *
23
- * @prop value {FileList | null} - Controlled selected files (bindable)
24
- * @default null
25
- *
26
- * @prop onFilesSelected {(files: FileList | null) => void} - Fired when files are chosen
27
- *
28
- * @prop class {string} - Additional classes for the wrapper
29
- * @default ""
30
- *
31
- * @note The entire area is clickable and supports drag-and-drop.
32
- * @note After a selection, the underlying input resets its value, so choosing the same file twice still triggers updates.
33
- * @note `accept` does not apply to dropped files, only to the picker UI; validate files inside `onFilesSelected`.
34
- * @note When `clearable=true`, the user can clear selected files and the callback receives `null`.
35
- * @note When `disabled=true`, clicks, drag events, focus, and keyboard input are blocked.
36
- */
37
- import type { HTMLAttributes } from "svelte/elements";
38
- import Button from "./Button.svelte";
39
- import { cx, formatFileSize } from "../utils";
40
- import { getComponentText, getLangContext, getLangKey } from "./lang-context";
41
-
42
- type Props = HTMLAttributes<HTMLDivElement> & {
43
- accept?: string;
44
- multiple?: boolean;
45
- label?: string;
46
- disabled?: boolean;
47
- clearable?: boolean;
48
- placeholder?: string;
49
- value?: FileList | null;
50
- onFilesSelected?: (files: FileList | null) => void;
51
- onError?: (error: string) => void;
52
- class?: string;
53
- };
54
-
55
- let {
56
- accept = "*/*",
57
- multiple = false,
58
- label,
59
- disabled = false,
60
- clearable = true,
61
- placeholder,
62
- value = $bindable<FileList | null>(null),
63
- onFilesSelected,
64
- class: externalClass = "",
65
- ...rest
66
- }: Props = $props();
67
-
68
- const langCtx = getLangContext();
69
- const langKey = $derived(getLangKey(langCtx));
70
- const L = $derived(getComponentText("filePicker", langKey));
71
-
72
- const labelFinal = $derived(label ?? L.text);
73
- const placeholderFinal = $derived(placeholder ?? L.placeholder);
74
-
75
- let inputEl: HTMLInputElement;
76
- let isDragOver = $state(false);
77
-
78
- const base = "inline-block w-full";
79
- const pickerClass = $derived(cx(base, externalClass));
80
-
81
- const hasValue = $derived(Boolean(value && value.length > 0));
82
- const fileNames = $derived(
83
- value
84
- ? Array.from(value)
85
- .map((file) => file.name)
86
- .join(", ")
87
- : ""
88
- );
89
- const totalBytes = $derived(
90
- value ? Array.from(value).reduce((acc, file) => acc + file.size, 0) : 0
91
- );
92
-
93
- function handleButtonClick() {
94
- if (disabled) return;
95
- inputEl?.click();
96
- }
97
-
98
- function handleFileChange(event: Event) {
99
- const target = event.target as HTMLInputElement;
100
- const files = target.files;
101
- value = files;
102
- if (files && files.length > 0) {
103
- onFilesSelected?.(files);
104
- }
105
- if (inputEl) {
106
- inputEl.value = "";
107
- }
108
- }
109
-
110
- function handleDrop(event: DragEvent) {
111
- event.preventDefault();
112
- isDragOver = false;
113
- if (disabled) return;
114
- const files = event.dataTransfer?.files;
115
- value = files || null;
116
- if (files && files.length > 0) {
117
- onFilesSelected?.(files);
118
- }
119
- if (inputEl) {
120
- inputEl.value = "";
121
- }
122
- }
123
-
124
- function handleDragOver(event: DragEvent) {
125
- event.preventDefault();
126
- }
127
-
128
- function handleDragEnter(event: DragEvent) {
129
- event.preventDefault();
130
- if (!disabled) {
131
- isDragOver = true;
132
- }
133
- }
134
-
135
- function handleDragLeave(event: DragEvent) {
136
- event.preventDefault();
137
- isDragOver = false;
138
- }
139
-
140
- function handleKeyDown(event: KeyboardEvent) {
141
- if (disabled) return;
142
- if (event.key === "Enter" || event.key === " ") {
143
- event.preventDefault();
144
- handleButtonClick();
145
- }
146
- }
147
-
148
- function clearSelection() {
149
- if (!clearable) return;
150
- value = null;
151
- if (inputEl) {
152
- inputEl.value = "";
153
- }
154
- onFilesSelected?.(null);
155
- }
156
- </script>
157
-
158
- <div class={pickerClass} {...rest}>
159
- <input
160
- bind:this={inputEl}
161
- type="file"
162
- {accept}
163
- {multiple}
164
- class="hidden"
165
- onchange={handleFileChange}
166
- />
167
-
168
- <div class="flex flex-wrap items-center gap-x-3 gap-y-2">
169
- <Button {disabled} onClick={handleButtonClick} class="relative" sz="xs">
170
- {labelFinal}
171
- </Button>
172
-
173
- {#if clearable}
174
- <Button
175
- onClick={clearSelection}
176
- variant="danger"
177
- disabled={!hasValue || disabled}
178
- sz="xs"
179
- >
180
- {L.clear}
181
- </Button>
182
- {/if}
183
- </div>
184
-
185
- <div
186
- class="mt-2 p-4 border-2 border-dashed rounded-[var(--radius-md)] text-center transition-colors duration-200"
187
- class:border-[var(--color-primary)]={isDragOver && !disabled}
188
- class:border-[var(--border-color-default)]={!isDragOver || disabled}
189
- class:bg-[var(--color-bg-hover)]={isDragOver && !disabled}
190
- class:cursor-pointer={!disabled}
191
- class:opacity-[var(--opacity-disabled)]={disabled}
192
- class:cursor-not-allowed={disabled}
193
- class:cursor-copy={isDragOver && !disabled}
194
- role="button"
195
- tabindex={disabled ? -1 : 0}
196
- aria-disabled={disabled}
197
- ondrop={handleDrop}
198
- ondragover={handleDragOver}
199
- ondragenter={handleDragEnter}
200
- ondragleave={handleDragLeave}
201
- onclick={handleButtonClick}
202
- onkeydown={handleKeyDown}
203
- >
204
- <p class="text-sm [color:var(--color-text-muted)]">
205
- {L.dragDrop}
206
- </p>
207
- {#if accept !== "*/*"}
208
- <p class="text-xs mt-1 [color:var(--color-text-muted)]">
209
- {L.accepted}: {accept}
210
- </p>
211
- {/if}
212
- </div>
213
-
214
- <div
215
- class="mt-3 p-4 bg-[var(--color-bg-surface)] text-center"
216
- aria-live="polite"
217
- >
218
- <p class="text-xs uppercase tracking-wide [color:var(--color-text-muted)]">
219
- {L.selectedFiles}
220
- </p>
221
- <p
222
- class="text-sm font-semibold mt-1 [color:var(--color-text-default)] break-words"
223
- >
224
- {#if hasValue}
225
- {fileNames}
226
- {:else}
227
- {placeholderFinal}
228
- {/if}
229
- </p>
230
- {#if hasValue && value}
231
- <p class="text-sm mt-1 [color:var(--color-text-muted)]">
232
- {L.fileCount.replace("{n}", String(value.length))}
233
-
234
- {#if multiple && value.length > 1}
235
- - {L.totalSize}: {formatFileSize(totalBytes)}
236
- {/if}
237
- </p>
238
- {/if}
239
- </div>
240
- </div>
1
+ <!-- src/lib/FilePicker.svelte -->
2
+ <script lang="ts">
3
+ /**
4
+ * @component FilePicker
5
+ * @description Lightweight file selector with click support and drag-and-drop. Internally uses a hidden `<input type="file">` plus a drop zone.
6
+ *
7
+ * @prop accept {string} - Accepted file types
8
+ * @default "*\\/*"
9
+ *
10
+ * @prop multiple {boolean} - Allows selecting multiple files
11
+ * @default false
12
+ *
13
+ * @prop label {string} - Button label; falls back to localized text
14
+ *
15
+ * @prop disabled {boolean} - Disables all interactions
16
+ * @default false
17
+ *
18
+ * @prop clearable {boolean} - Shows a clear button to reset selection
19
+ * @default true
20
+ *
21
+ * @prop maxBytes {number} - Maximum allowed file size in bytes
22
+ *
23
+ * @prop onError {(error: string) => void} - Fired when selected files are rejected
24
+ *
25
+ * @prop placeholder {string} - Placeholder text for the drop zone
26
+ *
27
+ * @prop value {FileList | null} - Controlled selected files (bindable)
28
+ * @default null
29
+ *
30
+ * @prop onFilesSelected {(files: FileList | null) => void} - Fired when files are chosen
31
+ *
32
+ * @prop class {string} - Additional classes for the wrapper
33
+ * @default ""
34
+ *
35
+ * @note The entire area is clickable and supports drag-and-drop.
36
+ * @note After a selection, the underlying input resets its value, so choosing the same file twice still triggers updates.
37
+ * @note `accept` and `maxBytes` are enforced for both input and dropped files.
38
+ * @note When `clearable=true`, the user can clear selected files and the callback receives `null`.
39
+ * @note When `disabled=true`, clicks, drag events, focus, and keyboard input are blocked.
40
+ */
41
+ import type { HTMLAttributes } from "svelte/elements";
42
+ import Button from "./Button.svelte";
43
+ import { cx, formatFileSize } from "../utils";
44
+ import { getComponentText, getLangContext, getLangKey } from "./lang-context";
45
+
46
+ type Props = HTMLAttributes<HTMLDivElement> & {
47
+ accept?: string;
48
+ multiple?: boolean;
49
+ label?: string;
50
+ disabled?: boolean;
51
+ clearable?: boolean;
52
+ placeholder?: string;
53
+ value?: FileList | null;
54
+ maxBytes?: number;
55
+ onFilesSelected?: (files: FileList | null) => void;
56
+ onError?: (error: string) => void;
57
+ class?: string;
58
+ };
59
+
60
+ let {
61
+ accept = "*/*",
62
+ multiple = false,
63
+ label,
64
+ disabled = false,
65
+ clearable = true,
66
+ placeholder,
67
+ value = $bindable<FileList | null>(null),
68
+ maxBytes = Number.POSITIVE_INFINITY,
69
+ onFilesSelected,
70
+ onError,
71
+ class: externalClass = "",
72
+ ...rest
73
+ }: Props = $props();
74
+
75
+ const langCtx = getLangContext();
76
+ const langKey = $derived(getLangKey(langCtx));
77
+ const L = $derived(getComponentText("filePicker", langKey));
78
+
79
+ const labelFinal = $derived(label ?? L.text);
80
+ const placeholderFinal = $derived(placeholder ?? L.placeholder);
81
+
82
+ let inputEl: HTMLInputElement;
83
+ let isDragOver = $state(false);
84
+
85
+ const base = "inline-block w-full";
86
+ const pickerClass = $derived(cx(base, externalClass));
87
+
88
+ const hasValue = $derived(Boolean(value && value.length > 0));
89
+ const fileNames = $derived(
90
+ value
91
+ ? Array.from(value)
92
+ .map((file) => file.name)
93
+ .join(", ")
94
+ : ""
95
+ );
96
+ const totalBytes = $derived(
97
+ value ? Array.from(value).reduce((acc, file) => acc + file.size, 0) : 0
98
+ );
99
+
100
+ function handleButtonClick() {
101
+ if (disabled) return;
102
+ inputEl?.click();
103
+ }
104
+
105
+ function handleFileChange(event: Event) {
106
+ const target = event.target as HTMLInputElement;
107
+ selectFiles(target.files);
108
+ if (inputEl) {
109
+ inputEl.value = "";
110
+ }
111
+ }
112
+
113
+ function handleDrop(event: DragEvent) {
114
+ event.preventDefault();
115
+ isDragOver = false;
116
+ if (disabled) return;
117
+ selectFiles(event.dataTransfer?.files ?? null);
118
+ if (inputEl) {
119
+ inputEl.value = "";
120
+ }
121
+ }
122
+
123
+ function handleDragOver(event: DragEvent) {
124
+ event.preventDefault();
125
+ }
126
+
127
+ function handleDragEnter(event: DragEvent) {
128
+ event.preventDefault();
129
+ if (!disabled) {
130
+ isDragOver = true;
131
+ }
132
+ }
133
+
134
+ function handleDragLeave(event: DragEvent) {
135
+ event.preventDefault();
136
+ isDragOver = false;
137
+ }
138
+
139
+ function handleKeyDown(event: KeyboardEvent) {
140
+ if (disabled) return;
141
+ if (event.key === "Enter" || event.key === " ") {
142
+ event.preventDefault();
143
+ handleButtonClick();
144
+ }
145
+ }
146
+
147
+ function clearSelection() {
148
+ if (!clearable) return;
149
+ value = null;
150
+ if (inputEl) {
151
+ inputEl.value = "";
152
+ }
153
+ onFilesSelected?.(null);
154
+ }
155
+
156
+ function selectFiles(files: FileList | null) {
157
+ const acceptedFiles = filterFiles(files);
158
+ value = acceptedFiles;
159
+ if (acceptedFiles && acceptedFiles.length > 0) {
160
+ onFilesSelected?.(acceptedFiles);
161
+ }
162
+ }
163
+
164
+ function filterFiles(files: FileList | null) {
165
+ if (!files || files.length === 0) return null;
166
+
167
+ const selected = Array.from(files);
168
+ const accepted = selected.filter(isAllowedFile);
169
+
170
+ if (accepted.length !== selected.length) {
171
+ onError?.("Some files were rejected by type or size constraints.");
172
+ }
173
+
174
+ if (accepted.length === 0) return null;
175
+ if (accepted.length === selected.length) return files;
176
+
177
+ return toFileList(accepted);
178
+ }
179
+
180
+ function isAllowedFile(file: File) {
181
+ if (Number.isFinite(maxBytes) && file.size > maxBytes) return false;
182
+ return matchesAccept(file, accept);
183
+ }
184
+
185
+ function matchesAccept(file: File, acceptValue: string) {
186
+ const rules = acceptValue
187
+ .split(",")
188
+ .map((rule) => rule.trim().toLowerCase())
189
+ .filter(Boolean);
190
+
191
+ if (rules.length === 0 || rules.includes("*/*")) return true;
192
+
193
+ const fileName = file.name.toLowerCase();
194
+ const fileType = file.type.toLowerCase();
195
+
196
+ return rules.some((rule) => {
197
+ if (rule.startsWith(".")) return fileName.endsWith(rule);
198
+ if (rule.endsWith("/*")) return fileType.startsWith(rule.slice(0, -1));
199
+ return fileType === rule;
200
+ });
201
+ }
202
+
203
+ function toFileList(files: File[]) {
204
+ if (typeof DataTransfer === "undefined") return null;
205
+ const transfer = new DataTransfer();
206
+ for (const file of files) {
207
+ transfer.items.add(file);
208
+ }
209
+ return transfer.files;
210
+ }
211
+ </script>
212
+
213
+ <div class={pickerClass} {...rest}>
214
+ <input
215
+ bind:this={inputEl}
216
+ type="file"
217
+ {accept}
218
+ {multiple}
219
+ class="hidden"
220
+ onchange={handleFileChange}
221
+ />
222
+
223
+ <div class="flex flex-wrap items-center gap-x-3 gap-y-2">
224
+ <Button {disabled} onClick={handleButtonClick} class="relative" sz="xs">
225
+ {labelFinal}
226
+ </Button>
227
+
228
+ {#if clearable}
229
+ <Button
230
+ onClick={clearSelection}
231
+ variant="danger"
232
+ disabled={!hasValue || disabled}
233
+ sz="xs"
234
+ >
235
+ {L.clear}
236
+ </Button>
237
+ {/if}
238
+ </div>
239
+
240
+ <div
241
+ class="mt-2 p-4 border-2 border-dashed rounded-[var(--radius-md)] text-center transition-colors duration-200"
242
+ class:border-[var(--color-primary)]={isDragOver && !disabled}
243
+ class:border-[var(--border-color-default)]={!isDragOver || disabled}
244
+ class:bg-[var(--color-bg-hover)]={isDragOver && !disabled}
245
+ class:cursor-pointer={!disabled}
246
+ class:opacity-[var(--opacity-disabled)]={disabled}
247
+ class:cursor-not-allowed={disabled}
248
+ class:cursor-copy={isDragOver && !disabled}
249
+ role="button"
250
+ tabindex={disabled ? -1 : 0}
251
+ aria-disabled={disabled}
252
+ ondrop={handleDrop}
253
+ ondragover={handleDragOver}
254
+ ondragenter={handleDragEnter}
255
+ ondragleave={handleDragLeave}
256
+ onclick={handleButtonClick}
257
+ onkeydown={handleKeyDown}
258
+ >
259
+ <p class="text-sm [color:var(--color-text-muted)]">
260
+ {L.dragDrop}
261
+ </p>
262
+ {#if accept !== "*/*"}
263
+ <p class="text-xs mt-1 [color:var(--color-text-muted)]">
264
+ {L.accepted}: {accept}
265
+ </p>
266
+ {/if}
267
+ </div>
268
+
269
+ <div
270
+ class="mt-3 p-4 bg-[var(--color-bg-surface)] text-center"
271
+ aria-live="polite"
272
+ >
273
+ <p class="text-xs uppercase tracking-wide [color:var(--color-text-muted)]">
274
+ {L.selectedFiles}
275
+ </p>
276
+ <p
277
+ class="text-sm font-semibold mt-1 [color:var(--color-text-default)] break-words"
278
+ >
279
+ {#if hasValue}
280
+ {fileNames}
281
+ {:else}
282
+ {placeholderFinal}
283
+ {/if}
284
+ </p>
285
+ {#if hasValue && value}
286
+ <p class="text-sm mt-1 [color:var(--color-text-muted)]">
287
+ {L.fileCount.replace("{n}", String(value.length))}
288
+
289
+ {#if multiple && value.length > 1}
290
+ - {L.totalSize}: {formatFileSize(totalBytes)}
291
+ {/if}
292
+ </p>
293
+ {/if}
294
+ </div>
295
+ </div>
@@ -16,6 +16,10 @@
16
16
  * @prop clearable {boolean} - Shows a clear button to reset selection
17
17
  * @default true
18
18
  *
19
+ * @prop maxBytes {number} - Maximum allowed file size in bytes
20
+ *
21
+ * @prop onError {(error: string) => void} - Fired when selected files are rejected
22
+ *
19
23
  * @prop placeholder {string} - Placeholder text for the drop zone
20
24
  *
21
25
  * @prop value {FileList | null} - Controlled selected files (bindable)
@@ -28,7 +32,7 @@
28
32
  *
29
33
  * @note The entire area is clickable and supports drag-and-drop.
30
34
  * @note After a selection, the underlying input resets its value, so choosing the same file twice still triggers updates.
31
- * @note `accept` does not apply to dropped files, only to the picker UI; validate files inside `onFilesSelected`.
35
+ * @note `accept` and `maxBytes` are enforced for both input and dropped files.
32
36
  * @note When `clearable=true`, the user can clear selected files and the callback receives `null`.
33
37
  * @note When `disabled=true`, clicks, drag events, focus, and keyboard input are blocked.
34
38
  */
@@ -41,6 +45,7 @@ type Props = HTMLAttributes<HTMLDivElement> & {
41
45
  clearable?: boolean;
42
46
  placeholder?: string;
43
47
  value?: FileList | null;
48
+ maxBytes?: number;
44
49
  onFilesSelected?: (files: FileList | null) => void;
45
50
  onError?: (error: string) => void;
46
51
  class?: string;