@functionalcms/svelte-components 4.16.1 → 4.19.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.
- package/dist/components/form/Button.svelte +118 -129
- package/dist/components/form/Button.svelte.d.ts +0 -3
- package/dist/components/form/ChoiceInput.svelte +37 -80
- package/dist/components/form/ChoiceInput.svelte.d.ts +1 -17
- package/dist/components/form/Dropzone.svelte +183 -0
- package/dist/components/form/Dropzone.svelte.d.ts +4 -0
- package/dist/components/form/Form.svelte +43 -95
- package/dist/components/form/Form.svelte.d.ts +8 -14
- package/dist/components/form/Input.svelte +3 -3
- package/dist/components/form/Input.svelte.d.ts +3 -3
- package/dist/components/form/Select.svelte +39 -73
- package/dist/components/form/Switch.svelte +17 -19
- package/dist/components/form/dropzone.d.ts +49 -0
- package/dist/components/form/dropzone.js +18 -0
- package/dist/components/form/form.d.ts +32 -11
- package/dist/components/form/form.js +29 -33
- package/dist/components/layouts/Tabs.svelte +3 -3
- package/dist/components/layouts/TwoColumnsLayout.svelte +1 -1
- package/dist/components/layouts/TwoColumnsLayout.svelte.d.ts +3 -2
- package/dist/components/menu/CollapsibleMenu.svelte +4 -1
- package/dist/components/presentation/Carousel.svelte +4 -2
- package/dist/index-server.d.ts +1 -1
- package/dist/index-server.js +1 -1
- package/dist/index.d.ts +2 -9
- package/dist/index.js +2 -8
- package/dist/index.server.d.ts +1 -1
- package/dist/index.server.js +1 -1
- package/dist/server-side/getRedirectPipeline.d.ts +6 -0
- package/dist/server-side/getRedirectPipeline.js +16 -0
- package/package.json +82 -86
- package/dist/components/form/dropzone/DefaultDropzone.svelte +0 -37
- package/dist/components/form/dropzone/DefaultDropzone.svelte.d.ts +0 -8
- package/dist/components/form/dropzone/Dropzone.svelte +0 -306
- package/dist/components/form/dropzone/Dropzone.svelte.d.ts +0 -4
- package/dist/components/form/dropzone/UseDropzone.d.ts +0 -3
- package/dist/components/form/dropzone/UseDropzone.js +0 -19
- package/dist/components/form/dropzone/attr-accept.d.ts +0 -12
- package/dist/components/form/dropzone/attr-accept.js +0 -29
- package/dist/components/form/dropzone/default.d.ts +0 -31
- package/dist/components/form/dropzone/default.js +0 -78
- package/dist/components/form/dropzone/types.d.ts +0 -62
- package/dist/components/form/dropzone/types.js +0 -1
- package/dist/components/form/utils.d.ts +0 -13
- package/dist/components/form/utils.js +0 -1
- package/dist/server-side/redirection.d.ts +0 -6
- package/dist/server-side/redirection.js +0 -16
- package/dist/translations/translator.d.ts +0 -2
- package/dist/translations/translator.js +0 -11
|
@@ -1,131 +1,120 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { cn } from '../../utils.js';
|
|
3
|
-
import type { Snippet } from 'svelte';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
{
|
|
90
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
disabled={isDisabled}
|
|
119
|
-
onclick={click}
|
|
120
|
-
onkeydown={keydown}
|
|
121
|
-
data-sveltekit-preload-data={dataPreload}
|
|
122
|
-
data-sveltekit-preload-code={codePreload}
|
|
123
|
-
{...restProps}
|
|
124
|
-
>
|
|
125
|
-
{@render children?.()}
|
|
126
|
-
</button>
|
|
127
|
-
{/if}
|
|
128
|
-
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../../utils.js';
|
|
3
|
+
import type { Snippet } from 'svelte';
|
|
4
|
+
import type { EventHandler } from 'svelte/elements';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
children: Snippet;
|
|
8
|
+
css: string;
|
|
9
|
+
style: string;
|
|
10
|
+
type?: 'submit' | 'reset' | 'button' | 'link';
|
|
11
|
+
href: string;
|
|
12
|
+
mode?: string;
|
|
13
|
+
size?: string;
|
|
14
|
+
isPrimary: boolean;
|
|
15
|
+
isBordered?: boolean;
|
|
16
|
+
isCapsule?: boolean;
|
|
17
|
+
isGrouped?: boolean;
|
|
18
|
+
isBlock?: boolean;
|
|
19
|
+
isLink?: boolean;
|
|
20
|
+
isBlank?: boolean;
|
|
21
|
+
isDisabled?: boolean;
|
|
22
|
+
role?: string;
|
|
23
|
+
isCircle?: boolean;
|
|
24
|
+
isRounded?: boolean;
|
|
25
|
+
isSkinned?: boolean;
|
|
26
|
+
ariaSelected?: boolean;
|
|
27
|
+
ariaControls?: string;
|
|
28
|
+
tabIndex?: number;
|
|
29
|
+
ariaLabel: string;
|
|
30
|
+
click?: (event: MouseEvent) => void;
|
|
31
|
+
keydown?: (event: KeyboardEvent) => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let {
|
|
35
|
+
children,
|
|
36
|
+
css = '',
|
|
37
|
+
style = '',
|
|
38
|
+
href = '',
|
|
39
|
+
mode = undefined,
|
|
40
|
+
size = undefined,
|
|
41
|
+
type = 'button',
|
|
42
|
+
isPrimary = false,
|
|
43
|
+
isBordered = false,
|
|
44
|
+
isCapsule = false,
|
|
45
|
+
isGrouped = false,
|
|
46
|
+
isBlock = false,
|
|
47
|
+
isLink = false,
|
|
48
|
+
isBlank = false,
|
|
49
|
+
isDisabled = false,
|
|
50
|
+
role = undefined,
|
|
51
|
+
isCircle = false,
|
|
52
|
+
isRounded = false,
|
|
53
|
+
isSkinned = true,
|
|
54
|
+
ariaSelected = undefined,
|
|
55
|
+
ariaControls = undefined,
|
|
56
|
+
tabIndex = 0,
|
|
57
|
+
click = undefined,
|
|
58
|
+
keydown = undefined,
|
|
59
|
+
ariaLabel = '',
|
|
60
|
+
...restProps
|
|
61
|
+
}: Partial<Props> = $props();
|
|
62
|
+
|
|
63
|
+
let klasses = $derived(
|
|
64
|
+
cn(
|
|
65
|
+
isSkinned ? 'btn' : 'btn-base',
|
|
66
|
+
mode ? `btn-${mode}` : '',
|
|
67
|
+
size ? `btn-${size}` : '',
|
|
68
|
+
isBordered ? 'btn-bordered' : '',
|
|
69
|
+
isCapsule ? 'btn-capsule ' : '',
|
|
70
|
+
isGrouped ? 'btn-grouped' : '',
|
|
71
|
+
isBlock ? 'btn-block' : '',
|
|
72
|
+
isCircle ? 'btn-circle' : '',
|
|
73
|
+
isRounded ? 'btn-rounded' : '',
|
|
74
|
+
isDisabled ? 'disabled' : '',
|
|
75
|
+
isBlank ? 'btn-blank' : '',
|
|
76
|
+
isLink ? 'btn-link' : '',
|
|
77
|
+
css ? `${css}` : ''
|
|
78
|
+
)
|
|
79
|
+
);
|
|
80
|
+
</script>
|
|
81
|
+
|
|
82
|
+
{#if type == 'link'}
|
|
83
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
84
|
+
<a
|
|
85
|
+
class={klasses}
|
|
86
|
+
{href}
|
|
87
|
+
{style}
|
|
88
|
+
{role}
|
|
89
|
+
aria-selected={ariaSelected}
|
|
90
|
+
aria-controls={ariaControls}
|
|
91
|
+
aria-label={ariaLabel}
|
|
92
|
+
tabindex={tabIndex}
|
|
93
|
+
onclick={click}
|
|
94
|
+
onkeydown={keydown}
|
|
95
|
+
{...restProps}
|
|
96
|
+
>
|
|
97
|
+
{@render children?.()}
|
|
98
|
+
</a>
|
|
99
|
+
{:else}
|
|
100
|
+
<button
|
|
101
|
+
{type}
|
|
102
|
+
class={klasses}
|
|
103
|
+
{style}
|
|
104
|
+
{role}
|
|
105
|
+
aria-selected={ariaSelected}
|
|
106
|
+
aria-controls={ariaControls}
|
|
107
|
+
aria-label={ariaLabel}
|
|
108
|
+
tabindex={tabIndex}
|
|
109
|
+
disabled={isDisabled}
|
|
110
|
+
onclick={click}
|
|
111
|
+
onkeydown={keydown}
|
|
112
|
+
{...restProps}
|
|
113
|
+
>
|
|
114
|
+
{@render children?.()}
|
|
115
|
+
</button>
|
|
116
|
+
{/if}
|
|
117
|
+
|
|
129
118
|
<style>.btn-base {
|
|
130
119
|
display: inline-flex;
|
|
131
120
|
align-items: center;
|
|
@@ -397,4 +386,4 @@ on the side padding. As such, these have a good bit less then regular buttons. *
|
|
|
397
386
|
|
|
398
387
|
.btn-link:hover {
|
|
399
388
|
cursor: pointer;
|
|
400
|
-
}</style>
|
|
389
|
+
}</style>
|
|
@@ -23,9 +23,6 @@ declare const Button: import("svelte").Component<Partial<{
|
|
|
23
23
|
ariaControls?: string;
|
|
24
24
|
tabIndex?: number;
|
|
25
25
|
ariaLabel: string;
|
|
26
|
-
noStyle: boolean;
|
|
27
|
-
dataPreload: "hover" | "tap";
|
|
28
|
-
codePreload: "eager" | "viewport" | "hover" | "tap";
|
|
29
26
|
click?: (event: MouseEvent) => void;
|
|
30
27
|
keydown?: (event: KeyboardEvent) => void;
|
|
31
28
|
}>, {}, "">;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { cn } from '../../utils.js';
|
|
3
|
-
import { Orientation } from '../Styling.js';
|
|
4
3
|
import type { ChoiceInputOption, ChoiceInputSize, ChoiceInputType, HtmlParts } from './utils.js';
|
|
5
4
|
|
|
6
5
|
interface Props {
|
|
@@ -16,7 +15,6 @@
|
|
|
16
15
|
label: string;
|
|
17
16
|
size: ChoiceInputSize;
|
|
18
17
|
checked: string[];
|
|
19
|
-
orientation: Orientation;
|
|
20
18
|
}
|
|
21
19
|
|
|
22
20
|
let {
|
|
@@ -38,8 +36,7 @@
|
|
|
38
36
|
size = '',
|
|
39
37
|
// Provides bind:checked capabilities that consumer can use
|
|
40
38
|
checked = $bindable([]),
|
|
41
|
-
|
|
42
|
-
...restProps
|
|
39
|
+
...restProps
|
|
43
40
|
}: Partial<Props & HtmlParts> = $props();
|
|
44
41
|
|
|
45
42
|
let labelClasses = $derived(
|
|
@@ -99,41 +96,37 @@
|
|
|
99
96
|
};
|
|
100
97
|
</script>
|
|
101
98
|
|
|
102
|
-
<
|
|
103
|
-
<
|
|
104
|
-
|
|
105
|
-
<
|
|
106
|
-
{
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
{/each}
|
|
126
|
-
</div>
|
|
127
|
-
</fieldset>
|
|
128
|
-
</div>
|
|
99
|
+
<fieldset class={fieldsetClasses}>
|
|
100
|
+
<legend class={legendClasses}>{label}</legend>
|
|
101
|
+
{#each options as { value, label }, index}
|
|
102
|
+
<label
|
|
103
|
+
class={labelClasses}
|
|
104
|
+
class:disabled={isDisabled || disabledOptions.includes(value) || undefined}
|
|
105
|
+
>
|
|
106
|
+
<input
|
|
107
|
+
class={inputClasses}
|
|
108
|
+
id="{id}-{name}-{index}"
|
|
109
|
+
{type}
|
|
110
|
+
{name}
|
|
111
|
+
{value}
|
|
112
|
+
disabled={isDisabled || disabledOptions.includes(value)}
|
|
113
|
+
checked={checkedOptions.includes(value)}
|
|
114
|
+
onchange={onChange}
|
|
115
|
+
{...restProps}
|
|
116
|
+
/>
|
|
117
|
+
<span class={labelSpanClasses} aria-hidden="true"></span>
|
|
118
|
+
<span class={labelCopyClasses}>{label}</span>
|
|
119
|
+
</label>
|
|
120
|
+
{/each}
|
|
121
|
+
</fieldset>
|
|
129
122
|
|
|
130
123
|
<style>
|
|
131
124
|
/**
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
125
|
+
* These radio and checkbox customizations are an amalgamation of various resources I've
|
|
126
|
+
* found on the internets; from Heydon Pickering's radio article (and his Inclusive Components
|
|
127
|
+
* book), to Sara Soueidan, Scott O'Hara, MDO, and Adrian Roselli's research on the matter
|
|
128
|
+
* of inclusive hiding and custom radio/checkbox inputs.
|
|
129
|
+
*/
|
|
137
130
|
.checkbox-group,
|
|
138
131
|
.radio-group {
|
|
139
132
|
--width-28: calc(7 * var(--fluid-4)); /* 1.75rem/28px */
|
|
@@ -157,13 +150,13 @@
|
|
|
157
150
|
}
|
|
158
151
|
|
|
159
152
|
/* Hiding technique from https://www.sarasoueidan.com/blog/inclusively-hiding-and-styling-checkboxes-and-radio-buttons/
|
|
160
|
-
|
|
153
|
+
*/
|
|
161
154
|
.checkbox,
|
|
162
155
|
.radio {
|
|
163
156
|
position: absolute;
|
|
164
157
|
width: var(--fluid-14);
|
|
165
158
|
height: var(--fluid-14);
|
|
166
|
-
|
|
159
|
+
opacity: 0%;
|
|
167
160
|
}
|
|
168
161
|
|
|
169
162
|
.checkbox-small,
|
|
@@ -243,7 +236,7 @@
|
|
|
243
236
|
}
|
|
244
237
|
|
|
245
238
|
/* Since we build up the radio size outwardly, it's naturally larger then the checkboxes
|
|
246
|
-
|
|
239
|
+
so we add a multiplyer to even those out initially */
|
|
247
240
|
.checkbox-label::before {
|
|
248
241
|
border: 2px solid var(--agnostic-checkbox-border-color, var(--agnostic-gray-light));
|
|
249
242
|
width: var(--fluid-16);
|
|
@@ -333,9 +326,9 @@
|
|
|
333
326
|
}
|
|
334
327
|
|
|
335
328
|
/**
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
329
|
+
* Consumer styles <legend> themselves, and can opt to use the .screenreader-only from
|
|
330
|
+
* utilities.css if they're design requires it.
|
|
331
|
+
*/
|
|
339
332
|
.checkbox-group-hidden,
|
|
340
333
|
.radio-group-hidden {
|
|
341
334
|
border: 0;
|
|
@@ -350,8 +343,8 @@
|
|
|
350
343
|
}
|
|
351
344
|
|
|
352
345
|
/* Targets both the label container and the span label that is used
|
|
353
|
-
to style the custom radio / checkbox. Note it does NOT target the input
|
|
354
|
-
itself. */
|
|
346
|
+
to style the custom radio / checkbox. Note it does NOT target the input
|
|
347
|
+
itself. */
|
|
355
348
|
.checkbox[disabled] ~ .checkbox-label-copy,
|
|
356
349
|
.radio[disabled] ~ .radio-label-copy,
|
|
357
350
|
.checkbox-label-wrap[class='disabled'],
|
|
@@ -379,40 +372,4 @@ itself. */
|
|
|
379
372
|
outline-offset: -2px;
|
|
380
373
|
}
|
|
381
374
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
.field-help,
|
|
385
|
-
.field-help-large,
|
|
386
|
-
.field-help-small,
|
|
387
|
-
.field-error,
|
|
388
|
-
.field-error-large,
|
|
389
|
-
.field-error-small,
|
|
390
|
-
.label-skin,
|
|
391
|
-
.label,
|
|
392
|
-
.input-addon-container,
|
|
393
|
-
.input-small,
|
|
394
|
-
.input-large,
|
|
395
|
-
.input-skin,
|
|
396
|
-
.input-underlined,
|
|
397
|
-
.input-underlined-bg,
|
|
398
|
-
.input {
|
|
399
|
-
color: var(--font-color, var(--dark));
|
|
400
|
-
font-family: var(--font-family-body);
|
|
401
|
-
font-weight: var(--font-weight, 300);
|
|
402
|
-
font-size: var(--font-size, 1rem);
|
|
403
|
-
line-height: var(--line-height, var(--fluid-20, 1.25rem));
|
|
404
|
-
width: 100%;
|
|
405
|
-
max-width: 100%;
|
|
406
|
-
}
|
|
407
|
-
.label {
|
|
408
|
-
display: inline-block;
|
|
409
|
-
|
|
410
|
-
/* Provided --input-vertical-pad isn't overriden we'll get 20px
|
|
411
|
-
label w/a 6px margin then a 38px input = 64 which is on the 8pt grid */
|
|
412
|
-
margin-block-start: 0;
|
|
413
|
-
margin-inline-start: 0;
|
|
414
|
-
margin-inline-end: 0;
|
|
415
|
-
margin-block-end: var(--input-label-pad, 0.375rem);
|
|
416
|
-
vertical-align: initial;
|
|
417
|
-
}
|
|
418
375
|
</style>
|
|
@@ -1,19 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { ChoiceInputOption, ChoiceInputSize, ChoiceInputType, HtmlParts } from './utils.js';
|
|
3
|
-
declare const ChoiceInput: import("svelte").Component<Partial<{
|
|
4
|
-
isSkinned: boolean;
|
|
5
|
-
isFieldset: boolean;
|
|
6
|
-
isInline: boolean;
|
|
7
|
-
isDisabled: boolean;
|
|
8
|
-
isInvalid: boolean;
|
|
9
|
-
options: ChoiceInputOption[];
|
|
10
|
-
disabledOptions: string[];
|
|
11
|
-
checkedOptions: string[];
|
|
12
|
-
type: ChoiceInputType;
|
|
13
|
-
label: string;
|
|
14
|
-
size: ChoiceInputSize;
|
|
15
|
-
checked: string[];
|
|
16
|
-
orientation: Orientation;
|
|
17
|
-
} & HtmlParts>, {}, "checked">;
|
|
1
|
+
declare const ChoiceInput: import("svelte").Component<any, {}, "checked">;
|
|
18
2
|
type ChoiceInput = ReturnType<typeof ChoiceInput>;
|
|
19
3
|
export default ChoiceInput;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { displaySize, type FileDropZoneProps, type FileRejectedReason } from './dropzone.js';
|
|
3
|
+
import { cn } from '../../utils.js';
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
id,
|
|
7
|
+
children,
|
|
8
|
+
maxFiles,
|
|
9
|
+
maxFileSize,
|
|
10
|
+
fileCount,
|
|
11
|
+
disabled = false,
|
|
12
|
+
onUpload,
|
|
13
|
+
onFileRejected,
|
|
14
|
+
accept,
|
|
15
|
+
class: css,
|
|
16
|
+
...rest
|
|
17
|
+
}: FileDropZoneProps = $props();
|
|
18
|
+
|
|
19
|
+
if (maxFiles !== undefined && fileCount === undefined) {
|
|
20
|
+
console.warn(
|
|
21
|
+
'Make sure to provide FileDropZone with `fileCount` when using the `maxFiles` prompt'
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let uploading = $state(false);
|
|
26
|
+
|
|
27
|
+
const drop = async (
|
|
28
|
+
e: DragEvent & {
|
|
29
|
+
currentTarget: EventTarget & HTMLLabelElement;
|
|
30
|
+
}
|
|
31
|
+
) => {
|
|
32
|
+
if (disabled || !canUploadFiles) return;
|
|
33
|
+
|
|
34
|
+
e.preventDefault();
|
|
35
|
+
|
|
36
|
+
const droppedFiles = Array.from(e.dataTransfer?.files ?? []);
|
|
37
|
+
|
|
38
|
+
await upload(droppedFiles);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const change = async (
|
|
42
|
+
e: Event & {
|
|
43
|
+
currentTarget: EventTarget & HTMLInputElement;
|
|
44
|
+
}
|
|
45
|
+
) => {
|
|
46
|
+
if (disabled) return;
|
|
47
|
+
|
|
48
|
+
const selectedFiles = e.currentTarget.files;
|
|
49
|
+
|
|
50
|
+
if (!selectedFiles) return;
|
|
51
|
+
|
|
52
|
+
await upload(Array.from(selectedFiles));
|
|
53
|
+
|
|
54
|
+
// this if a file fails and we upload the same file again we still get feedback
|
|
55
|
+
(e.target as HTMLInputElement).value = '';
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const shouldAcceptFile = (file: File, fileNumber: number): FileRejectedReason | undefined => {
|
|
59
|
+
if (maxFileSize !== undefined && file.size > maxFileSize) return 'Maximum file size exceeded';
|
|
60
|
+
|
|
61
|
+
if (maxFiles !== undefined && fileNumber > maxFiles) return 'Maximum files uploaded';
|
|
62
|
+
|
|
63
|
+
if (!accept) return undefined;
|
|
64
|
+
|
|
65
|
+
const acceptedTypes = accept.split(',').map((a) => a.trim().toLowerCase());
|
|
66
|
+
const fileType = file.type.toLowerCase();
|
|
67
|
+
const fileName = file.name.toLowerCase();
|
|
68
|
+
|
|
69
|
+
const isAcceptable = acceptedTypes.some((pattern) => {
|
|
70
|
+
// check extension like .mp4
|
|
71
|
+
if (fileType.startsWith('.')) {
|
|
72
|
+
return fileName.endsWith(pattern);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// if pattern has wild card like video/*
|
|
76
|
+
if (pattern.endsWith('/*')) {
|
|
77
|
+
const baseType = pattern.slice(0, pattern.indexOf('/*'));
|
|
78
|
+
return fileType.startsWith(baseType + '/');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// otherwise it must be a specific type like video/mp4
|
|
82
|
+
return fileType === pattern;
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
if (!isAcceptable) return 'File type not allowed';
|
|
86
|
+
|
|
87
|
+
return undefined;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const upload = async (uploadFiles: File[]) => {
|
|
91
|
+
uploading = true;
|
|
92
|
+
|
|
93
|
+
const validFiles: File[] = [];
|
|
94
|
+
|
|
95
|
+
for (let i = 0; i < uploadFiles.length; i++) {
|
|
96
|
+
const file = uploadFiles[i];
|
|
97
|
+
|
|
98
|
+
const rejectedReason = shouldAcceptFile(file, (fileCount ?? 0) + i + 1);
|
|
99
|
+
|
|
100
|
+
if (rejectedReason) {
|
|
101
|
+
onFileRejected?.({ file, reason: rejectedReason });
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
validFiles.push(file);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
await onUpload(validFiles);
|
|
109
|
+
|
|
110
|
+
uploading = false;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const canUploadFiles = $derived(
|
|
114
|
+
!disabled &&
|
|
115
|
+
!uploading &&
|
|
116
|
+
!(maxFiles !== undefined && fileCount !== undefined && fileCount >= maxFiles)
|
|
117
|
+
);
|
|
118
|
+
</script>
|
|
119
|
+
|
|
120
|
+
<label
|
|
121
|
+
ondragover={(e) => e.preventDefault()}
|
|
122
|
+
ondrop={drop}
|
|
123
|
+
for={id}
|
|
124
|
+
aria-disabled={!canUploadFiles}
|
|
125
|
+
class={cn(
|
|
126
|
+
'flex h-48 w-full place-items-center justify-center rounded-lg border-2 border-dashed border-border p-6 transition-all hover:cursor-pointer hover:bg-accent/25 aria-disabled:opacity-50 aria-disabled:hover:cursor-not-allowed',
|
|
127
|
+
className
|
|
128
|
+
)}
|
|
129
|
+
>
|
|
130
|
+
{#if children}
|
|
131
|
+
{@render children()}
|
|
132
|
+
{:else}
|
|
133
|
+
<div class="flex flex-col place-items-center justify-center gap-2">
|
|
134
|
+
<div
|
|
135
|
+
class="flex size-14 place-items-center justify-center rounded-full border border-dashed border-border text-muted-foreground"
|
|
136
|
+
>
|
|
137
|
+
<svg
|
|
138
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
139
|
+
width="24"
|
|
140
|
+
height="24"
|
|
141
|
+
viewBox="0 0 24 24"
|
|
142
|
+
fill="none"
|
|
143
|
+
stroke="currentColor"
|
|
144
|
+
stroke-width="2"
|
|
145
|
+
stroke-linecap="round"
|
|
146
|
+
stroke-linejoin="round"
|
|
147
|
+
class="lucide lucide-upload-icon lucide-upload"
|
|
148
|
+
><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><polyline
|
|
149
|
+
points="17 8 12 3 7 8"
|
|
150
|
+
/><line x1="12" x2="12" y1="3" y2="15" /></svg
|
|
151
|
+
>
|
|
152
|
+
</div>
|
|
153
|
+
<div class="flex flex-col gap-0.5 text-center">
|
|
154
|
+
<span class="font-medium text-muted-foreground">
|
|
155
|
+
Drag 'n' drop files here, or click to select files
|
|
156
|
+
</span>
|
|
157
|
+
{#if maxFiles || maxFileSize}
|
|
158
|
+
<span class="text-sm text-muted-foreground/75">
|
|
159
|
+
{#if maxFiles}
|
|
160
|
+
<span>You can upload {maxFiles} files</span>
|
|
161
|
+
{/if}
|
|
162
|
+
{#if maxFiles && maxFileSize}
|
|
163
|
+
<span>(up to {displaySize(maxFileSize)} each)</span>
|
|
164
|
+
{/if}
|
|
165
|
+
{#if maxFileSize && !maxFiles}
|
|
166
|
+
<span>Maximum size {displaySize(maxFileSize)}</span>
|
|
167
|
+
{/if}
|
|
168
|
+
</span>
|
|
169
|
+
{/if}
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
{/if}
|
|
173
|
+
<input
|
|
174
|
+
{...rest}
|
|
175
|
+
disabled={!canUploadFiles}
|
|
176
|
+
{id}
|
|
177
|
+
{accept}
|
|
178
|
+
multiple={maxFiles === undefined || maxFiles - (fileCount ?? 0) > 1}
|
|
179
|
+
type="file"
|
|
180
|
+
onchange={change}
|
|
181
|
+
class="hidden"
|
|
182
|
+
/>
|
|
183
|
+
</label>
|