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.
- package/LICENSE.md +21 -21
- package/README.md +101 -100
- package/dist/App.svelte +507 -507
- package/dist/Container.svelte +59 -59
- package/dist/app.css +234 -235
- package/dist/app.d.ts +10 -0
- package/dist/lib/Accordion.svelte +155 -155
- package/dist/lib/Badge.svelte +44 -44
- package/dist/lib/Button.svelte +185 -170
- package/dist/lib/Calendar.svelte +384 -384
- package/dist/lib/Card.svelte +103 -103
- package/dist/lib/Carousel.svelte +293 -293
- package/dist/lib/Carousel.svelte.d.ts +1 -1
- package/dist/lib/CheckBox.svelte +210 -210
- package/dist/lib/CodeView.svelte +308 -307
- package/dist/lib/ColorPicker.svelte +159 -159
- package/dist/lib/ContextMenu.svelte +328 -322
- package/dist/lib/DatePicker.svelte +246 -246
- package/dist/lib/Dialog.svelte +233 -233
- package/dist/lib/Field.svelte +299 -299
- package/dist/lib/FilePicker.svelte +295 -240
- package/dist/lib/FilePicker.svelte.d.ts +6 -1
- package/dist/lib/Form.svelte +438 -438
- package/dist/lib/Hamburger.svelte +217 -217
- package/dist/lib/InstallPWA.svelte +94 -94
- package/dist/lib/Menu.svelte +623 -623
- package/dist/lib/NoticeBase.svelte +140 -140
- package/dist/lib/PaginatedCard.svelte +73 -73
- package/dist/lib/Pagination.svelte +119 -119
- package/dist/lib/PrimaryColorSelect.svelte +111 -111
- package/dist/lib/ProgressBar.svelte +141 -141
- package/dist/lib/ProgressCircle.svelte +190 -190
- package/dist/lib/Radio.svelte +189 -189
- package/dist/lib/SearchInput.svelte +104 -104
- package/dist/lib/Select.svelte +524 -524
- package/dist/lib/Slider.svelte +253 -253
- package/dist/lib/Splitter.svelte +159 -150
- package/dist/lib/Switch.svelte +168 -167
- package/dist/lib/Table.svelte +299 -299
- package/dist/lib/Tabs.svelte +213 -213
- package/dist/lib/ThemeToggle.svelte +128 -127
- package/dist/lib/TimePicker.svelte +312 -312
- package/dist/lib/TimePickerNew.svelte +634 -0
- package/dist/lib/TimePickerNew.svelte.d.ts +49 -0
- package/dist/lib/Toast.svelte +123 -123
- package/dist/lib/Tooltip.svelte +110 -110
- package/dist/lib/Topbar.svelte +107 -107
- package/dist/lib/__tests__/Accordion.test.d.ts +1 -0
- package/dist/lib/__tests__/Accordion.test.js +171 -0
- package/dist/lib/__tests__/Badge.test.d.ts +1 -0
- package/dist/lib/__tests__/Badge.test.js +41 -0
- package/dist/lib/__tests__/Button.test.d.ts +1 -0
- package/dist/lib/__tests__/Button.test.js +269 -0
- package/dist/lib/__tests__/Calendar.test.d.ts +1 -0
- package/dist/lib/__tests__/Calendar.test.js +171 -0
- package/dist/lib/__tests__/Card.test.d.ts +1 -0
- package/dist/lib/__tests__/Card.test.js +148 -0
- package/dist/lib/__tests__/Carousel.test.d.ts +1 -0
- package/dist/lib/__tests__/Carousel.test.js +439 -0
- package/dist/lib/__tests__/CheckBox.test.d.ts +1 -0
- package/dist/lib/__tests__/CheckBox.test.js +152 -0
- package/dist/lib/__tests__/CodeView.test.d.ts +1 -0
- package/dist/lib/__tests__/CodeView.test.js +157 -0
- package/dist/lib/__tests__/ColorPicker.test.d.ts +1 -0
- package/dist/lib/__tests__/ColorPicker.test.js +93 -0
- package/dist/lib/__tests__/ContextMenu.test.d.ts +1 -0
- package/dist/lib/__tests__/ContextMenu.test.js +67 -0
- package/dist/lib/__tests__/DatePicker.test.d.ts +1 -0
- package/dist/lib/__tests__/DatePicker.test.js +108 -0
- package/dist/lib/__tests__/Dialog.test.d.ts +1 -0
- package/dist/lib/__tests__/Dialog.test.js +183 -0
- package/dist/lib/__tests__/Field.test.d.ts +1 -0
- package/dist/lib/__tests__/Field.test.js +190 -0
- package/dist/lib/__tests__/FilePicker.test.d.ts +1 -0
- package/dist/lib/__tests__/FilePicker.test.js +179 -0
- package/dist/lib/__tests__/Form.integration.test.d.ts +1 -0
- package/dist/lib/__tests__/Form.integration.test.js +158 -0
- package/dist/lib/__tests__/Form.test.d.ts +1 -0
- package/dist/lib/__tests__/Form.test.js +463 -0
- package/dist/lib/__tests__/Hamburger.test.d.ts +1 -0
- package/dist/lib/__tests__/Hamburger.test.js +161 -0
- package/dist/lib/__tests__/InstallPWA.test.d.ts +1 -0
- package/dist/lib/__tests__/InstallPWA.test.js +15 -0
- package/dist/lib/__tests__/Menu.test.d.ts +1 -0
- package/dist/lib/__tests__/Menu.test.js +285 -0
- package/dist/lib/__tests__/NoticeBase.test.d.ts +1 -0
- package/dist/lib/__tests__/NoticeBase.test.js +60 -0
- package/dist/lib/__tests__/PaginatedCard.test.d.ts +1 -0
- package/dist/lib/__tests__/PaginatedCard.test.js +89 -0
- package/dist/lib/__tests__/Pagination.test.d.ts +1 -0
- package/dist/lib/__tests__/Pagination.test.js +168 -0
- package/dist/lib/__tests__/PrimaryColorSelect.test.d.ts +1 -0
- package/dist/lib/__tests__/PrimaryColorSelect.test.js +92 -0
- package/dist/lib/__tests__/ProgressBar.test.d.ts +1 -0
- package/dist/lib/__tests__/ProgressBar.test.js +69 -0
- package/dist/lib/__tests__/ProgressCircle.test.d.ts +1 -0
- package/dist/lib/__tests__/ProgressCircle.test.js +71 -0
- package/dist/lib/__tests__/Radio.test.d.ts +1 -0
- package/dist/lib/__tests__/Radio.test.js +127 -0
- package/dist/lib/__tests__/SearchInput.test.d.ts +1 -0
- package/dist/lib/__tests__/SearchInput.test.js +80 -0
- package/dist/lib/__tests__/Select.test.d.ts +1 -0
- package/dist/lib/__tests__/Select.test.js +408 -0
- package/dist/lib/__tests__/Slider.test.d.ts +1 -0
- package/dist/lib/__tests__/Slider.test.js +213 -0
- package/dist/lib/__tests__/Splitter.test.d.ts +1 -0
- package/dist/lib/__tests__/Splitter.test.js +87 -0
- package/dist/lib/__tests__/Switch.test.d.ts +1 -0
- package/dist/lib/__tests__/Switch.test.js +97 -0
- package/dist/lib/__tests__/Table.test.d.ts +1 -0
- package/dist/lib/__tests__/Table.test.js +349 -0
- package/dist/lib/__tests__/Tabs.test.d.ts +1 -0
- package/dist/lib/__tests__/Tabs.test.js +262 -0
- package/dist/lib/__tests__/ThemeToggle.test.d.ts +1 -0
- package/dist/lib/__tests__/ThemeToggle.test.js +84 -0
- package/dist/lib/__tests__/TimePicker.test.d.ts +1 -0
- package/dist/lib/__tests__/TimePicker.test.js +146 -0
- package/dist/lib/__tests__/TimePickerNew.test.d.ts +1 -0
- package/dist/lib/__tests__/TimePickerNew.test.js +322 -0
- package/dist/lib/__tests__/Toast.test.d.ts +1 -0
- package/dist/lib/__tests__/Toast.test.js +135 -0
- package/dist/lib/__tests__/Tooltip.test.d.ts +1 -0
- package/dist/lib/__tests__/Tooltip.test.js +171 -0
- package/dist/lib/__tests__/Topbar.test.d.ts +1 -0
- package/dist/lib/__tests__/Topbar.test.js +25 -0
- package/dist/lib/__tests__/setupLangContext.d.ts +1 -0
- package/dist/lib/__tests__/setupLangContext.js +65 -0
- package/dist/lib/__tests__/storage.test.d.ts +1 -0
- package/dist/lib/__tests__/storage.test.js +124 -0
- package/dist/lib/__tests__/utils.test.d.ts +1 -0
- package/dist/lib/__tests__/utils.test.js +11 -0
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/index.js +1 -0
- package/dist/lib/lang.d.ts +4 -0
- package/dist/lib/lang.js +4 -0
- package/dist/styles.css +234 -232
- package/dist/utils/index.js +15 -4
- 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
|
|
22
|
-
*
|
|
23
|
-
* @prop
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* @
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* @
|
|
33
|
-
* @
|
|
34
|
-
*
|
|
35
|
-
* @note
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
);
|
|
89
|
-
const
|
|
90
|
-
value
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
<
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
{
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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`
|
|
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;
|