@witchcraft/ui 0.3.10 → 0.3.12
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/module.json +1 -1
- package/dist/runtime/assets/utils.css +1 -1
- package/dist/runtime/components/LibFileInput/LibFileInput.d.vue.ts +5 -3
- package/dist/runtime/components/LibFileInput/LibFileInput.vue +141 -84
- package/dist/runtime/components/LibFileInput/LibFileInput.vue.d.ts +5 -3
- package/dist/runtime/components/LibNotifications/LibNotification.d.vue.ts +7 -1
- package/dist/runtime/components/LibNotifications/LibNotification.vue +20 -1
- package/dist/runtime/components/LibNotifications/LibNotification.vue.d.ts +7 -1
- package/dist/runtime/components/LibNotifications/LibNotifications.vue +2 -2
- package/package.json +2 -2
- package/src/runtime/assets/utils.css +4 -4
- package/src/runtime/components/LibFileInput/LibFileInput.stories.ts +13 -3
- package/src/runtime/components/LibFileInput/LibFileInput.vue +154 -92
- package/src/runtime/components/LibNotifications/LibNotification.vue +28 -1
- package/src/runtime/components/LibNotifications/LibNotifications.vue +2 -2
package/dist/module.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
@custom-variant dark (&:where(.dark, .dark *));@utility focus-outline-within{@
|
|
1
|
+
@custom-variant dark (&:where(.dark, .dark *));@utility focus-outline-within{@apply outlined-within:outline-2 outlined-within:outline-accent-500 outlined-within:outline-offset-2}@utility focus-outline{@apply outlined:outline-2 outlined:outline-accent-500 outlined:outline-offset-2}@utility focus-outline-no-offset{@apply outlined:outline-2 outlined:outline-accent-500}@utility focus-outline-hidden{@apply outlined:outline-none}@utility bg-squares-gradient{--_square:var(--squareSize,5px);--_double_square:calc(var(--_square)*2);--_light_square:var(--lightSquare,var(--color-white));--_dark_square:var(--darkSquare,var(--color-black));background-color:var(--_light_square);background:repeating-conic-gradient(var(--_dark_square) 0 25%,var(--_light_square) 0 50%) 50% /var(--_double_square) var(--_double_square)}@utility square-light-*{--lightSquare:--value(--color- *)}@utility square-dark-*{--darkSquare:--value(--color- *)}@utility square-size-*{--squareSize:calc(--value(integer) * 1px)}@utility bg-bars-gradient{--_bg_color:var(--bars-bg-color,var(--color-accent-700));--_fg_color:var(--bars-fg-color,var(--color-accent-800));--_angle:var(--bars-angle,45deg);--_fg_width:var(--bars-fg-width,50%);--_bg_width:calc(100% - var(--_fg_width));background-color:var(--_bg_color);--_pos_1:calc(var(--_bg_width)/2);--_pos_2:calc(var(--_bg_width)/2 + var(--_fg_width)/2);--_pos_3:calc(var(--_bg_width) + var(--_fg_width)/2);background-image:repeating-linear-gradient(var(--_angle),var(--_bg_color),var(--_bg_color) var(--_pos_1),var(--_fg_color) var(--_pos_1),var(--_fg_color) var(--_pos_2),var(--_bg_color) var(--_pos_2),var(--_bg_color) var(--_pos_3),var(--_fg_color) var(--_pos_3),var(--_fg_color))}@utility bars-angle-*{--bars-angle:var(--value(integer) * 1deg)}@utility bars-fg-*{--bars-fg-color:--value(--color-*)}@utility bars-bg-*{--bars-bg-color:--value(--color-*)}@utility bars-w-*{--bars-fg-width:calc(--value(integer) * 1%, 50%)}@utility scrollbar-hidden{-ms-overflow-style:none;scrollbar-width:none;&::-webkit-scrollbar{display:none}}@utility styled-scrollbar{--_scrollbar_width:var(--scrollbar-width,calc(var(--spacing)*3));--_scrollbar_border_width:var(--scrollbar-border-width,calc(var(--spacing)/2));--_scrollbar_color:var(--scrollbar-color,--alpha(var(--color-accent-500)/40%));--_scrollbar_hover_color:var(--scrollbar-hover-color,--alpha(var(--color-accent-500)/80%));--_scrollbar_bg_color:var(--scrollbar-bg-color,var(--color-bg));.dark &{--_scrollbar_bg_color:var(--scrollbar-bg-color,var(--color-fg))}&::-webkit-scrollbar{height:var(--_scrollbar_width);width:var(--_scrollbar_width)}&::-webkit-scrollbar-corner,&::-webkit-scrollbar-track{background-color:transparent}&::-webkit-scrollbar-thumb,&::-webkit-scrollbar-track{border-radius:var(--_scrollbar_width)}&::-webkit-scrollbar-thumb{background-color:var(--_scrollbar_color);border:var(--_scrollbar_border_width) solid var(--_scrollbar_bg_color)}&::-webkit-scrollbar-thumb:hover{cursor:pointer}&::-webkit-scrollbar-thumb:active,&::-webkit-scrollbar-thumb:hover{background-color:var(--_scrollbar_hover_color);border-radius:var(--_scrollbar_width)}}@utility styled-scrollbar-w-*{--scrollbar-width:--value(integer)}@utility styled-scrollbar-border-w-*{--scrollbar-border-width:--value(integer)}@utility styled-scrollbar-*{--scrollbar-color:--value(--color-*)}@utility styled-scrollbar-bg-*{--scrollbar-bg-color:--value(--color-*)}@utility styled-resizer{--_resizer_width:var(--resizer-width,8px);--_resizer_color:var(--resizer-color,var(--color-neutral-300));.dark &{--_resizer_color:var(--resizer-color,var(--color-neutral-700))}&::-webkit-resizer{border-bottom-color:var(--_resizer_color);border-left-color:transparent;border-right-color:var(--_resizer_color);border-style:solid;border-top-color:transparent;border-width:var(--_resizer_width)}}@utility styled-resizer-w-*{--resizer-width:--value(integer)}@utility styled-resizer-color-*{--resizer-color:--value(--color-*)}@utility content-vertical-holder{--tw-content:"\200b";content:var(--tw-content)}@utility no-touch-action{touch-action:none}@utility bg-transparency-squares{@apply bg-squares-gradient square-light-white square-dark-neutral-300 square-size-6}@utility link-like{@apply cursor-pointer hover:text-accent-500}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { type HTMLAttributes, type InputHTMLAttributes } from "vue";
|
|
2
2
|
import type { FileInputError } from "../../types/index.js";
|
|
3
3
|
import { type LinkableByIdProps, type TailwindClassProp, type WrapperProps } from "../shared/props.js";
|
|
4
|
-
declare const _default: __VLS_WithSlots<import("vue").DefineComponent<Props, {
|
|
5
|
-
|
|
4
|
+
declare const _default: __VLS_WithSlots<import("vue").DefineComponent<Props, {
|
|
5
|
+
clearFiles: () => void;
|
|
6
|
+
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
7
|
+
input: (val: File[], clearFiles: () => void) => any;
|
|
6
8
|
errors: (val: FileInputError[]) => any;
|
|
7
9
|
}, string, import("vue").PublicProps, Readonly<Props> & Readonly<{
|
|
8
|
-
onInput?: ((val: File[]) => any) | undefined;
|
|
10
|
+
onInput?: ((val: File[], clearFiles: () => void) => any) | undefined;
|
|
9
11
|
onErrors?: ((val: FileInputError[]) => any) | undefined;
|
|
10
12
|
}>, {
|
|
11
13
|
multiple: boolean;
|
|
@@ -2,30 +2,52 @@
|
|
|
2
2
|
<!-- todo aria errors -->
|
|
3
3
|
<div
|
|
4
4
|
:class="twMerge(
|
|
5
|
-
`
|
|
5
|
+
`
|
|
6
|
+
file-input
|
|
6
7
|
justify-center
|
|
7
8
|
border-2
|
|
8
9
|
border-dashed
|
|
9
10
|
border-accent-500/80
|
|
10
11
|
focus-outline-within
|
|
11
12
|
transition-[border-color,box-shadow]
|
|
12
|
-
ease-out
|
|
13
|
+
ease-out
|
|
14
|
+
hover:bg-accent-500/10
|
|
15
|
+
outlined-focus-within
|
|
16
|
+
`,
|
|
13
17
|
compact && `rounded-sm`,
|
|
14
|
-
!compact && `
|
|
18
|
+
!compact && `
|
|
19
|
+
flex
|
|
20
|
+
w-full
|
|
21
|
+
flex-col
|
|
22
|
+
items-stretch
|
|
23
|
+
gap-2
|
|
24
|
+
rounded-xl
|
|
25
|
+
p-2
|
|
26
|
+
`,
|
|
15
27
|
errors.length > 0 && errorFlashing && `border-danger-400`,
|
|
28
|
+
isHovered && `bg-accent-500/10`,
|
|
16
29
|
$.wrapperAttrs.class
|
|
17
30
|
)"
|
|
18
31
|
v-bind="{ ...$.wrapperAttrs, class: void 0 }"
|
|
32
|
+
@drop="onDrop"
|
|
33
|
+
@dragover.prevent="isHovered = true"
|
|
34
|
+
@dragleave="isHovered = false"
|
|
19
35
|
>
|
|
20
36
|
<div
|
|
21
37
|
:class="twMerge(
|
|
22
38
|
`
|
|
23
|
-
|
|
24
|
-
|
|
39
|
+
file-input--wrapper
|
|
40
|
+
relative
|
|
41
|
+
justify-center
|
|
42
|
+
@container
|
|
43
|
+
`,
|
|
25
44
|
compact && `flex gap-2`,
|
|
26
|
-
!compact && `
|
|
27
|
-
|
|
28
|
-
|
|
45
|
+
!compact && `
|
|
46
|
+
file-input
|
|
47
|
+
flex
|
|
48
|
+
flex-col
|
|
49
|
+
items-center
|
|
50
|
+
`
|
|
29
51
|
)"
|
|
30
52
|
>
|
|
31
53
|
<label
|
|
@@ -36,7 +58,9 @@
|
|
|
36
58
|
flex
|
|
37
59
|
gap-1
|
|
38
60
|
items-center
|
|
61
|
+
justify-center
|
|
39
62
|
whitespace-nowrap
|
|
63
|
+
max-w-full
|
|
40
64
|
`)"
|
|
41
65
|
>
|
|
42
66
|
<slot
|
|
@@ -46,23 +70,37 @@
|
|
|
46
70
|
<icon><i-fa6-solid-arrow-up-from-bracket/></icon>
|
|
47
71
|
</slot>
|
|
48
72
|
<slot name="label">
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
73
|
+
<div class="text-ellipsis overflow-hidden shrink-1 hidden @min-[15ch]:block">
|
|
74
|
+
{{
|
|
75
|
+
compact ? multiple ? t("file-input.compact-choose-file-plural") : t("file-input.compact-choose-file") : multiple ? t("file-input.non-compact-choose-file-plural") : t("file-input.non-compact-choose-file")
|
|
76
|
+
}}
|
|
77
|
+
</div>
|
|
52
78
|
</slot>
|
|
53
|
-
<
|
|
79
|
+
<div
|
|
54
80
|
v-if="compact && multiple"
|
|
55
81
|
class="file-input--label-count"
|
|
56
82
|
>
|
|
57
83
|
{{ ` (${files.length})` }}
|
|
58
|
-
</
|
|
84
|
+
</div>
|
|
85
|
+
<div
|
|
86
|
+
v-if="compact && !multiple && files.length > 0"
|
|
87
|
+
class="file-input--label-name text-ellipsis overflow-hidden shrink-9999 hidden @3xs:block"
|
|
88
|
+
>
|
|
89
|
+
{{ ` (${files[0]?.file.name})` }}
|
|
90
|
+
</div>
|
|
91
|
+
<div
|
|
92
|
+
v-if="compact && !multiple && files.length > 0"
|
|
93
|
+
class="file-input--label-name text-ellipsis overflow-hidden shrink-9999 @3xs:hidden"
|
|
94
|
+
>
|
|
95
|
+
{{ ` (...)` }}
|
|
96
|
+
</div>
|
|
59
97
|
</label>
|
|
60
98
|
<label
|
|
61
99
|
v-if="!compact && formats?.length > 0"
|
|
62
|
-
class="file-input--formats-label flex
|
|
100
|
+
class="file-input--formats-label flex-col items-center text-sm max-w-full hidden @min-[15ch]:flex"
|
|
63
101
|
>
|
|
64
|
-
<slot name="formats">{{ t("file-input.accepted-formats") }}
|
|
65
|
-
<div class="file-input--formats-list">
|
|
102
|
+
<slot name="formats"><div class="text-ellipsis overflow-hidden max-w-full">{{ t("file-input.accepted-formats") }}:</div> </slot>
|
|
103
|
+
<div class="file-input--formats-list overflow-hidden text-ellipsis max-w-full">
|
|
66
104
|
{{ extensions.join(", ") }}
|
|
67
105
|
</div>
|
|
68
106
|
</label>
|
|
@@ -70,14 +108,14 @@
|
|
|
70
108
|
:id="id ?? fallbackId"
|
|
71
109
|
:class="twMerge(
|
|
72
110
|
`
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
111
|
+
file-input--input
|
|
112
|
+
absolute
|
|
113
|
+
inset-[calc(var(--spacing)*-2)]
|
|
114
|
+
cursor-pointer
|
|
115
|
+
z-0
|
|
116
|
+
text-[0]
|
|
117
|
+
opacity-0
|
|
118
|
+
`,
|
|
81
119
|
$.inputAttrs?.class
|
|
82
120
|
)"
|
|
83
121
|
type="file"
|
|
@@ -94,7 +132,7 @@
|
|
|
94
132
|
v-if="!compact && files.length > 0"
|
|
95
133
|
:class="twMerge(
|
|
96
134
|
`file-input--previews
|
|
97
|
-
flex items-stretch justify-center gap-
|
|
135
|
+
flex items-stretch justify-center gap-4 flex-wrap
|
|
98
136
|
`,
|
|
99
137
|
multiple && `
|
|
100
138
|
w-full
|
|
@@ -102,32 +140,47 @@
|
|
|
102
140
|
$.previewsAttrs?.class
|
|
103
141
|
)"
|
|
104
142
|
>
|
|
105
|
-
<div class="file-input--preview-spacer flex-1"/>
|
|
106
143
|
<div
|
|
107
|
-
class="
|
|
144
|
+
class="
|
|
145
|
+
file-input--preview-wrapper
|
|
108
146
|
z-1
|
|
109
147
|
relative
|
|
110
148
|
flex
|
|
111
149
|
min-w-0
|
|
112
150
|
max-w-[150px]
|
|
113
151
|
flex-initial
|
|
114
|
-
flex-
|
|
152
|
+
flex-col
|
|
115
153
|
items-center
|
|
116
|
-
gap-
|
|
117
|
-
|
|
118
|
-
|
|
154
|
+
gap-1
|
|
155
|
+
p-1
|
|
156
|
+
rounded-sm
|
|
157
|
+
border
|
|
158
|
+
border-neutral-300
|
|
159
|
+
dark:border-neutral-800
|
|
160
|
+
shadow-md
|
|
161
|
+
shadow-neutral-800/30
|
|
162
|
+
bg-neutral-100
|
|
163
|
+
dark:bg-neutral-900
|
|
164
|
+
[&:hover_.file-input--remove-button]:opacity-100
|
|
119
165
|
"
|
|
120
166
|
v-for="entry of files"
|
|
121
167
|
:key="entry.file.name"
|
|
122
168
|
>
|
|
123
|
-
<div class="
|
|
169
|
+
<div class="flex flex-initial basis-full justify-start items-center max-w-full gap-2 px-1">
|
|
124
170
|
<lib-button
|
|
125
171
|
:border="false"
|
|
172
|
+
class="file-input--remove-button rounded-full p-0"
|
|
126
173
|
:aria-label="`Remove file ${entry.file.name}`"
|
|
127
174
|
@click="removeFile(entry)"
|
|
128
175
|
>
|
|
129
176
|
<icon><i-fa6-solid-xmark/></icon>
|
|
130
177
|
</lib-button>
|
|
178
|
+
<div
|
|
179
|
+
class="file-input--preview-filename min-w-0 flex-1 basis-0 truncate break-all rounded-sm text-sm"
|
|
180
|
+
:title="entry.file.name"
|
|
181
|
+
>
|
|
182
|
+
{{ entry.file.name }}
|
|
183
|
+
</div>
|
|
131
184
|
</div>
|
|
132
185
|
|
|
133
186
|
<div class="file-input--preview flex flex-initial basis-full justify-center">
|
|
@@ -154,25 +207,7 @@
|
|
|
154
207
|
<icon><i-fa6-regular-file class="text-4xl opacity-50"/></icon>
|
|
155
208
|
</div>
|
|
156
209
|
</div>
|
|
157
|
-
<div
|
|
158
|
-
class="
|
|
159
|
-
file-input--preview-filename
|
|
160
|
-
min-w-0
|
|
161
|
-
flex-1
|
|
162
|
-
basis-0
|
|
163
|
-
truncate
|
|
164
|
-
break-all
|
|
165
|
-
rounded-sm
|
|
166
|
-
p-1
|
|
167
|
-
text-sm
|
|
168
|
-
"
|
|
169
|
-
:title="entry.file.name"
|
|
170
|
-
>
|
|
171
|
-
{{ entry.file.name }}
|
|
172
|
-
</div>
|
|
173
210
|
</div>
|
|
174
|
-
|
|
175
|
-
<div class="flex-1"/>
|
|
176
211
|
</div>
|
|
177
212
|
</div>
|
|
178
213
|
</template>
|
|
@@ -193,8 +228,13 @@ const el = ref(null);
|
|
|
193
228
|
const files = shallowReactive([]);
|
|
194
229
|
const errors = shallowReactive([]);
|
|
195
230
|
const errorFlashing = ref(false);
|
|
231
|
+
const isHovered = ref(false);
|
|
232
|
+
function clearFiles() {
|
|
233
|
+
el.value.value = "";
|
|
234
|
+
files.splice(0, files.length);
|
|
235
|
+
}
|
|
196
236
|
watch(files, () => {
|
|
197
|
-
emits("input", files.map((entry) => entry.file));
|
|
237
|
+
emits("input", files.map((entry) => entry.file), clearFiles);
|
|
198
238
|
});
|
|
199
239
|
watch(errors, () => {
|
|
200
240
|
if (errors.length > 0) {
|
|
@@ -202,7 +242,8 @@ watch(errors, () => {
|
|
|
202
242
|
setTimeout(() => {
|
|
203
243
|
errorFlashing.value = false;
|
|
204
244
|
}, 500);
|
|
205
|
-
emits("errors", errors);
|
|
245
|
+
emits("errors", [...errors]);
|
|
246
|
+
errors.splice(0, errors.length);
|
|
206
247
|
}
|
|
207
248
|
});
|
|
208
249
|
defineOptions({
|
|
@@ -229,44 +270,60 @@ const removeFile = (entry) => {
|
|
|
229
270
|
files.splice(index, 1);
|
|
230
271
|
};
|
|
231
272
|
const extensionsList = computed(() => extensions.value.join(", "));
|
|
232
|
-
|
|
273
|
+
function onDrop(e) {
|
|
274
|
+
if ("dataTransfer" in e && e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
|
275
|
+
el.value.files = e.dataTransfer.files;
|
|
276
|
+
e.preventDefault();
|
|
277
|
+
isHovered.value = false;
|
|
278
|
+
return updateFiles(el.value.files);
|
|
279
|
+
}
|
|
280
|
+
return void 0;
|
|
281
|
+
}
|
|
282
|
+
async function inputFile(e) {
|
|
233
283
|
e.preventDefault();
|
|
234
284
|
if (el.value.files) {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
} else {
|
|
257
|
-
files.splice(0, files.length, { file, isImg });
|
|
258
|
-
}
|
|
259
|
-
}
|
|
285
|
+
return updateFiles(el.value.files);
|
|
286
|
+
}
|
|
287
|
+
return void 0;
|
|
288
|
+
}
|
|
289
|
+
function updateFiles(filesList) {
|
|
290
|
+
const errs = [];
|
|
291
|
+
for (const file of filesList) {
|
|
292
|
+
const isImg = file.type.startsWith("image");
|
|
293
|
+
const byPassValidation = props.formats.length === 0;
|
|
294
|
+
const isValidMimeType = mimeTypes.value.find((_) => _.endsWith("/*") ? file.type.startsWith(_.slice(0, -2)) : _ === file.type) !== void 0;
|
|
295
|
+
const isValidExtension = extensions.value.find((_) => file.name.endsWith(_)) !== void 0;
|
|
296
|
+
if (!byPassValidation && (!isValidMimeType || !isValidExtension)) {
|
|
297
|
+
const extension = file.name.match(/.*(\..*)/)?.[1] ?? "Unknown";
|
|
298
|
+
const type = file.type === "" ? "" : ` (${file.type})`;
|
|
299
|
+
const message = `File type ${extension}${type} is not allowed. Allowed file types are: ${extensionsList.value}.`;
|
|
300
|
+
const err = new Error(message);
|
|
301
|
+
err.file = file;
|
|
302
|
+
err.isValidExtension = isValidExtension;
|
|
303
|
+
err.isValidMimeType = isValidMimeType;
|
|
304
|
+
errs.push(err);
|
|
305
|
+
continue;
|
|
260
306
|
}
|
|
261
|
-
if (errs.length > 0)
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
307
|
+
if (errs.length > 0) continue;
|
|
308
|
+
if (!files.find((_) => _.file === file)) {
|
|
309
|
+
if (props.multiple || files.length < 1) {
|
|
310
|
+
files.push({ file, isImg });
|
|
311
|
+
} else {
|
|
312
|
+
files.splice(0, files.length, { file, isImg });
|
|
313
|
+
}
|
|
266
314
|
}
|
|
267
315
|
}
|
|
316
|
+
if (errs.length > 0) {
|
|
317
|
+
errors.splice(0, errors.length, ...errs);
|
|
318
|
+
return false;
|
|
319
|
+
} else if (errors.length > 0) {
|
|
320
|
+
errors.splice(0, errors.length);
|
|
321
|
+
}
|
|
268
322
|
return void 0;
|
|
269
|
-
}
|
|
323
|
+
}
|
|
324
|
+
defineExpose({
|
|
325
|
+
clearFiles
|
|
326
|
+
});
|
|
270
327
|
</script>
|
|
271
328
|
|
|
272
329
|
<script>
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { type HTMLAttributes, type InputHTMLAttributes } from "vue";
|
|
2
2
|
import type { FileInputError } from "../../types/index.js";
|
|
3
3
|
import { type LinkableByIdProps, type TailwindClassProp, type WrapperProps } from "../shared/props.js";
|
|
4
|
-
declare const _default: __VLS_WithSlots<import("vue").DefineComponent<Props, {
|
|
5
|
-
|
|
4
|
+
declare const _default: __VLS_WithSlots<import("vue").DefineComponent<Props, {
|
|
5
|
+
clearFiles: () => void;
|
|
6
|
+
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
7
|
+
input: (val: File[], clearFiles: () => void) => any;
|
|
6
8
|
errors: (val: FileInputError[]) => any;
|
|
7
9
|
}, string, import("vue").PublicProps, Readonly<Props> & Readonly<{
|
|
8
|
-
onInput?: ((val: File[]) => any) | undefined;
|
|
10
|
+
onInput?: ((val: File[], clearFiles: () => void) => any) | undefined;
|
|
9
11
|
onErrors?: ((val: FileInputError[]) => any) | undefined;
|
|
10
12
|
}>, {
|
|
11
13
|
multiple: boolean;
|
|
@@ -11,7 +11,13 @@ Partial<Omit<HTMLAttributes, "class"> & TailwindClassProp>, RealProps {
|
|
|
11
11
|
}
|
|
12
12
|
declare const _default: __VLS_WithSlots<import("vue").DefineComponent<Props, {
|
|
13
13
|
focus: () => void;
|
|
14
|
-
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}
|
|
14
|
+
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
15
|
+
pause: (notification: NotificationEntry) => any;
|
|
16
|
+
resume: (notification: NotificationEntry) => any;
|
|
17
|
+
}, string, import("vue").PublicProps, Readonly<Props> & Readonly<{
|
|
18
|
+
onPause?: ((notification: NotificationEntry) => any) | undefined;
|
|
19
|
+
onResume?: ((notification: NotificationEntry) => any) | undefined;
|
|
20
|
+
}>, {
|
|
15
21
|
handler: NotificationHandler;
|
|
16
22
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>, {
|
|
17
23
|
top?: (props: {
|
|
@@ -20,13 +20,16 @@
|
|
|
20
20
|
p-1
|
|
21
21
|
text-sm
|
|
22
22
|
focus:border-accent-500
|
|
23
|
+
focus-within:border-accent-500
|
|
23
24
|
`,
|
|
24
25
|
$attrs.class
|
|
25
26
|
)"
|
|
26
27
|
v-bind="{ ...$attrs, class: void 0 }"
|
|
27
28
|
tabindex="0"
|
|
29
|
+
:data-id="notification.id"
|
|
28
30
|
ref="notificationEl"
|
|
29
31
|
@keydown.enter.self="NotificationHandler.resolveToDefault(notification)"
|
|
32
|
+
@pointerenter="notification.timeout && !notification.isPaused && emit('pause', notification)"
|
|
30
33
|
>
|
|
31
34
|
<slot
|
|
32
35
|
name="top"
|
|
@@ -148,7 +151,7 @@
|
|
|
148
151
|
</template>
|
|
149
152
|
|
|
150
153
|
<script setup>
|
|
151
|
-
import { computed, onMounted, ref, useAttrs } from "vue";
|
|
154
|
+
import { computed, onBeforeUnmount, onMounted, ref, useAttrs } from "vue";
|
|
152
155
|
import IFa6RegularCopy from "~icons/fa6-regular/copy";
|
|
153
156
|
import IFa6SolidXmark from "~icons/fa6-solid/xmark";
|
|
154
157
|
import { useSlotVars } from "../../composables/useSlotVars.js";
|
|
@@ -167,13 +170,29 @@ const props = defineProps({
|
|
|
167
170
|
notification: { type: null, required: true },
|
|
168
171
|
handler: { type: Object, required: false, default: void 0 }
|
|
169
172
|
});
|
|
173
|
+
const emit = defineEmits(["pause", "resume"]);
|
|
170
174
|
const getColor = (notification, option) => {
|
|
171
175
|
return notification.dangerous.includes(option) ? "danger" : notification.default === option ? "primary" : "secondary";
|
|
172
176
|
};
|
|
173
177
|
const buttonColors = computed(() => props.notification.options.map((option) => getColor(props.notification, option)));
|
|
174
178
|
const notificationEl = ref(null);
|
|
179
|
+
const mousedownAbortController = new AbortController();
|
|
175
180
|
onMounted(() => {
|
|
176
181
|
notificationEl.value?.focus();
|
|
182
|
+
if (props.notification.timeout) {
|
|
183
|
+
window.addEventListener("pointerdown", (e) => {
|
|
184
|
+
if (!e.target || !(e.target instanceof HTMLElement)) return;
|
|
185
|
+
if (e.target === notificationEl.value || notificationEl.value?.contains(e.target)) {
|
|
186
|
+
if (props.notification.isPaused) return;
|
|
187
|
+
emit("pause", props.notification);
|
|
188
|
+
} else {
|
|
189
|
+
emit("resume", props.notification);
|
|
190
|
+
}
|
|
191
|
+
}, { signal: mousedownAbortController.signal });
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
onBeforeUnmount(() => {
|
|
195
|
+
mousedownAbortController.abort();
|
|
177
196
|
});
|
|
178
197
|
defineExpose({
|
|
179
198
|
focus: () => {
|
|
@@ -11,7 +11,13 @@ Partial<Omit<HTMLAttributes, "class"> & TailwindClassProp>, RealProps {
|
|
|
11
11
|
}
|
|
12
12
|
declare const _default: __VLS_WithSlots<import("vue").DefineComponent<Props, {
|
|
13
13
|
focus: () => void;
|
|
14
|
-
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}
|
|
14
|
+
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
15
|
+
pause: (notification: NotificationEntry) => any;
|
|
16
|
+
resume: (notification: NotificationEntry) => any;
|
|
17
|
+
}, string, import("vue").PublicProps, Readonly<Props> & Readonly<{
|
|
18
|
+
onPause?: ((notification: NotificationEntry) => any) | undefined;
|
|
19
|
+
onResume?: ((notification: NotificationEntry) => any) | undefined;
|
|
20
|
+
}>, {
|
|
15
21
|
handler: NotificationHandler;
|
|
16
22
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>, {
|
|
17
23
|
top?: (props: {
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
class="overflow-hidden my-2"
|
|
33
33
|
v-for="notification of notifications"
|
|
34
34
|
:key="notification.id"
|
|
35
|
-
@
|
|
36
|
-
@
|
|
35
|
+
@pause="handler.pause(notification)"
|
|
36
|
+
@resume="handler.resume(notification)"
|
|
37
37
|
>
|
|
38
38
|
<template #top>
|
|
39
39
|
<LibProgressBar
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@witchcraft/ui",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.12",
|
|
4
4
|
"description": "Vue component library.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/runtime/main.lib.js",
|
|
@@ -121,7 +121,7 @@
|
|
|
121
121
|
"husky": "^9.1.7",
|
|
122
122
|
"indexit": "2.1.0-beta.3",
|
|
123
123
|
"madge": "^7.0.0",
|
|
124
|
-
"nuxt": "^4.
|
|
124
|
+
"nuxt": "^4.2.1",
|
|
125
125
|
"playwright": "=1.54.0",
|
|
126
126
|
"playwright-core": "=1.54.0",
|
|
127
127
|
"semantic-release": "^24.2.7",
|
|
@@ -37,17 +37,17 @@
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
@utility focus-outline-within {
|
|
40
|
-
@
|
|
40
|
+
@apply outlined-within:outline-2 outlined-within:outline-accent-500 outlined-within:outline-offset-2;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
@utility focus-outline {
|
|
44
|
-
@
|
|
44
|
+
@apply outlined:outline-2 outlined:outline-accent-500 outlined:outline-offset-2;
|
|
45
45
|
}
|
|
46
46
|
@utility focus-outline-no-offset {
|
|
47
|
-
@
|
|
47
|
+
@apply outlined:outline-2 outlined:outline-accent-500;
|
|
48
48
|
}
|
|
49
49
|
@utility focus-outline-hidden {
|
|
50
|
-
@
|
|
50
|
+
@apply outlined:outline-none;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
/* .bg-squares-gradient { */
|
|
@@ -21,14 +21,24 @@ export const SingleFile: Story = {
|
|
|
21
21
|
render: args => ({
|
|
22
22
|
components,
|
|
23
23
|
setup: () => {
|
|
24
|
-
|
|
24
|
+
const errors = ref([])
|
|
25
|
+
function errorHandler(errs: any) {
|
|
25
26
|
// eslint-disable-next-line no-console
|
|
26
|
-
console.log(
|
|
27
|
+
console.log(errs)
|
|
28
|
+
errors.value = errs
|
|
27
29
|
}
|
|
28
|
-
return { args, errorHandler }
|
|
30
|
+
return { args, errorHandler, errors }
|
|
29
31
|
},
|
|
30
32
|
template: `
|
|
31
33
|
<lib-file-input v-bind="{...args}" @errors="errorHandler"></lib-file-input>
|
|
34
|
+
<div
|
|
35
|
+
v-if="errors.length > 0"
|
|
36
|
+
class="border-2 border-red-500 rounded-lg p-2 mt-2 w-full"
|
|
37
|
+
>
|
|
38
|
+
<div v-for="err of errors" :key="err">
|
|
39
|
+
{{err}}
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
32
42
|
`
|
|
33
43
|
})
|
|
34
44
|
}
|
|
@@ -1,29 +1,51 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<!-- todo aria errors -->
|
|
3
3
|
<div
|
|
4
|
-
:class="twMerge(`
|
|
4
|
+
:class="twMerge(`
|
|
5
|
+
file-input
|
|
5
6
|
justify-center
|
|
6
7
|
border-2
|
|
7
8
|
border-dashed
|
|
8
9
|
border-accent-500/80
|
|
9
10
|
focus-outline-within
|
|
10
11
|
transition-[border-color,box-shadow]
|
|
11
|
-
ease-out
|
|
12
|
+
ease-out
|
|
13
|
+
hover:bg-accent-500/10
|
|
14
|
+
outlined-focus-within
|
|
15
|
+
`,
|
|
12
16
|
compact && `rounded-sm`,
|
|
13
|
-
!compact && `
|
|
17
|
+
!compact && `
|
|
18
|
+
flex
|
|
19
|
+
w-full
|
|
20
|
+
flex-col
|
|
21
|
+
items-stretch
|
|
22
|
+
gap-2
|
|
23
|
+
rounded-xl
|
|
24
|
+
p-2
|
|
25
|
+
`,
|
|
14
26
|
errors.length > 0 && errorFlashing && `border-danger-400`,
|
|
27
|
+
isHovered && `bg-accent-500/10`,
|
|
15
28
|
($.wrapperAttrs as any).class
|
|
16
29
|
)"
|
|
17
30
|
v-bind="{ ...$.wrapperAttrs, class: undefined }"
|
|
31
|
+
@drop="onDrop"
|
|
32
|
+
@dragover.prevent="isHovered = true"
|
|
33
|
+
@dragleave="isHovered = false"
|
|
18
34
|
>
|
|
19
35
|
<div
|
|
20
36
|
:class="twMerge(`
|
|
21
|
-
|
|
22
|
-
|
|
37
|
+
file-input--wrapper
|
|
38
|
+
relative
|
|
39
|
+
justify-center
|
|
40
|
+
@container
|
|
41
|
+
`,
|
|
23
42
|
compact && `flex gap-2`,
|
|
24
|
-
!compact && `
|
|
25
|
-
|
|
26
|
-
|
|
43
|
+
!compact && `
|
|
44
|
+
file-input
|
|
45
|
+
flex
|
|
46
|
+
flex-col
|
|
47
|
+
items-center
|
|
48
|
+
`
|
|
27
49
|
)"
|
|
28
50
|
>
|
|
29
51
|
<label
|
|
@@ -34,7 +56,9 @@
|
|
|
34
56
|
flex
|
|
35
57
|
gap-1
|
|
36
58
|
items-center
|
|
59
|
+
justify-center
|
|
37
60
|
whitespace-nowrap
|
|
61
|
+
max-w-full
|
|
38
62
|
`)"
|
|
39
63
|
>
|
|
40
64
|
<slot
|
|
@@ -44,44 +68,58 @@
|
|
|
44
68
|
<icon><i-fa6-solid-arrow-up-from-bracket/></icon>
|
|
45
69
|
</slot>
|
|
46
70
|
<slot name="label">
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
?
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
71
|
+
<div class="text-ellipsis overflow-hidden shrink-1 hidden @min-[15ch]:block">
|
|
72
|
+
{{
|
|
73
|
+
(compact
|
|
74
|
+
? multiple
|
|
75
|
+
? t("file-input.compact-choose-file-plural")
|
|
76
|
+
: t("file-input.compact-choose-file")
|
|
77
|
+
: multiple
|
|
78
|
+
? t("file-input.non-compact-choose-file-plural")
|
|
79
|
+
: t("file-input.non-compact-choose-file")
|
|
80
|
+
)
|
|
81
|
+
}}
|
|
82
|
+
</div>
|
|
57
83
|
</slot>
|
|
58
|
-
<
|
|
84
|
+
<div
|
|
59
85
|
v-if="compact && multiple"
|
|
60
86
|
class="file-input--label-count"
|
|
61
87
|
>
|
|
62
88
|
{{ ` (${files.length})` }}
|
|
63
|
-
</
|
|
89
|
+
</div>
|
|
90
|
+
<div
|
|
91
|
+
v-if="compact && !multiple && files.length > 0"
|
|
92
|
+
class="file-input--label-name text-ellipsis overflow-hidden shrink-9999 hidden @3xs:block"
|
|
93
|
+
>
|
|
94
|
+
{{ ` (${files[0]?.file.name})` }}
|
|
95
|
+
</div>
|
|
96
|
+
<div
|
|
97
|
+
v-if="compact && !multiple && files.length > 0"
|
|
98
|
+
class="file-input--label-name text-ellipsis overflow-hidden shrink-9999 @3xs:hidden"
|
|
99
|
+
>
|
|
100
|
+
{{ ` (...)` }}
|
|
101
|
+
</div>
|
|
64
102
|
</label>
|
|
65
103
|
<label
|
|
66
104
|
v-if="!compact && formats?.length > 0"
|
|
67
|
-
class="file-input--formats-label flex
|
|
105
|
+
class="file-input--formats-label flex-col items-center text-sm max-w-full hidden @min-[15ch]:flex"
|
|
68
106
|
>
|
|
69
|
-
<slot name="formats">{{ t("file-input.accepted-formats") }}
|
|
70
|
-
<div class="file-input--formats-list">
|
|
107
|
+
<slot name="formats"><div class="text-ellipsis overflow-hidden max-w-full">{{ t("file-input.accepted-formats") }}:</div> </slot>
|
|
108
|
+
<div class="file-input--formats-list overflow-hidden text-ellipsis max-w-full">
|
|
71
109
|
{{ extensions.join(", ") }}
|
|
72
110
|
</div>
|
|
73
111
|
</label>
|
|
74
112
|
<input
|
|
75
113
|
:id="id ?? fallbackId"
|
|
76
114
|
:class="twMerge(`
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
115
|
+
file-input--input
|
|
116
|
+
absolute
|
|
117
|
+
inset-[calc(var(--spacing)*-2)]
|
|
118
|
+
cursor-pointer
|
|
119
|
+
z-0
|
|
120
|
+
text-[0]
|
|
121
|
+
opacity-0
|
|
122
|
+
`,
|
|
85
123
|
($.inputAttrs as any)?.class
|
|
86
124
|
)"
|
|
87
125
|
type="file"
|
|
@@ -97,7 +135,7 @@
|
|
|
97
135
|
<div
|
|
98
136
|
v-if="!compact && files.length > 0"
|
|
99
137
|
:class="twMerge(`file-input--previews
|
|
100
|
-
flex items-stretch justify-center gap-
|
|
138
|
+
flex items-stretch justify-center gap-4 flex-wrap
|
|
101
139
|
`,
|
|
102
140
|
multiple && `
|
|
103
141
|
w-full
|
|
@@ -105,32 +143,47 @@
|
|
|
105
143
|
($.previewsAttrs as any)?.class
|
|
106
144
|
)"
|
|
107
145
|
>
|
|
108
|
-
<div class="file-input--preview-spacer flex-1"/>
|
|
109
146
|
<div
|
|
110
|
-
class="
|
|
147
|
+
class="
|
|
148
|
+
file-input--preview-wrapper
|
|
111
149
|
z-1
|
|
112
150
|
relative
|
|
113
151
|
flex
|
|
114
152
|
min-w-0
|
|
115
153
|
max-w-[150px]
|
|
116
154
|
flex-initial
|
|
117
|
-
flex-
|
|
155
|
+
flex-col
|
|
118
156
|
items-center
|
|
119
|
-
gap-
|
|
120
|
-
|
|
121
|
-
|
|
157
|
+
gap-1
|
|
158
|
+
p-1
|
|
159
|
+
rounded-sm
|
|
160
|
+
border
|
|
161
|
+
border-neutral-300
|
|
162
|
+
dark:border-neutral-800
|
|
163
|
+
shadow-md
|
|
164
|
+
shadow-neutral-800/30
|
|
165
|
+
bg-neutral-100
|
|
166
|
+
dark:bg-neutral-900
|
|
167
|
+
[&:hover_.file-input--remove-button]:opacity-100
|
|
122
168
|
"
|
|
123
169
|
v-for="entry of files"
|
|
124
170
|
:key="entry.file.name"
|
|
125
171
|
>
|
|
126
|
-
<div class="
|
|
172
|
+
<div class="flex flex-initial basis-full justify-start items-center max-w-full gap-2 px-1">
|
|
127
173
|
<lib-button
|
|
128
174
|
:border="false"
|
|
175
|
+
class="file-input--remove-button rounded-full p-0"
|
|
129
176
|
:aria-label="`Remove file ${entry.file.name}`"
|
|
130
177
|
@click="removeFile(entry)"
|
|
131
178
|
>
|
|
132
179
|
<icon><i-fa6-solid-xmark/></icon>
|
|
133
180
|
</lib-button>
|
|
181
|
+
<div
|
|
182
|
+
class="file-input--preview-filename min-w-0 flex-1 basis-0 truncate break-all rounded-sm text-sm"
|
|
183
|
+
:title="entry.file.name"
|
|
184
|
+
>
|
|
185
|
+
{{ entry.file.name }}
|
|
186
|
+
</div>
|
|
134
187
|
</div>
|
|
135
188
|
|
|
136
189
|
<div class="file-input--preview flex flex-initial basis-full justify-center">
|
|
@@ -157,25 +210,7 @@
|
|
|
157
210
|
<icon><i-fa6-regular-file class="text-4xl opacity-50"/></icon>
|
|
158
211
|
</div>
|
|
159
212
|
</div>
|
|
160
|
-
<div
|
|
161
|
-
class="
|
|
162
|
-
file-input--preview-filename
|
|
163
|
-
min-w-0
|
|
164
|
-
flex-1
|
|
165
|
-
basis-0
|
|
166
|
-
truncate
|
|
167
|
-
break-all
|
|
168
|
-
rounded-sm
|
|
169
|
-
p-1
|
|
170
|
-
text-sm
|
|
171
|
-
"
|
|
172
|
-
:title="entry.file.name"
|
|
173
|
-
>
|
|
174
|
-
{{ entry.file.name }}
|
|
175
|
-
</div>
|
|
176
213
|
</div>
|
|
177
|
-
|
|
178
|
-
<div class="flex-1"/>
|
|
179
214
|
</div>
|
|
180
215
|
</div>
|
|
181
216
|
</template>
|
|
@@ -202,9 +237,16 @@ type Entry = { file: File, isImg: boolean }
|
|
|
202
237
|
const files = shallowReactive<(Entry)[]>([])
|
|
203
238
|
const errors = shallowReactive<(FileInputError)[]>([])
|
|
204
239
|
const errorFlashing = ref(false)
|
|
240
|
+
const isHovered = ref(false)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
function clearFiles() {
|
|
244
|
+
el.value!.value = ""
|
|
245
|
+
files.splice(0, files.length)
|
|
246
|
+
}
|
|
205
247
|
|
|
206
248
|
watch(files, () => {
|
|
207
|
-
emits("input", files.map(entry => entry.file))
|
|
249
|
+
emits("input", files.map(entry => entry.file), clearFiles)
|
|
208
250
|
})
|
|
209
251
|
watch(errors, () => {
|
|
210
252
|
if (errors.length > 0) {
|
|
@@ -212,7 +254,8 @@ watch(errors, () => {
|
|
|
212
254
|
setTimeout(() => {
|
|
213
255
|
errorFlashing.value = false
|
|
214
256
|
}, 500)
|
|
215
|
-
emits("errors", errors)
|
|
257
|
+
emits("errors", [...errors])
|
|
258
|
+
errors.splice(0, errors.length)
|
|
216
259
|
}
|
|
217
260
|
})
|
|
218
261
|
|
|
@@ -223,7 +266,7 @@ defineOptions({
|
|
|
223
266
|
const $ = useDivideAttrs(["wrapper", "input", "previews"] as const)
|
|
224
267
|
|
|
225
268
|
const emits = defineEmits<{
|
|
226
|
-
(e: "input", val: File[]): void
|
|
269
|
+
(e: "input", val: File[], clearFiles: () => void): void
|
|
227
270
|
(e: "errors", val: FileInputError[]): void
|
|
228
271
|
}>()
|
|
229
272
|
|
|
@@ -247,46 +290,65 @@ const removeFile = (entry: Entry) => {
|
|
|
247
290
|
files.splice(index, 1)
|
|
248
291
|
}
|
|
249
292
|
const extensionsList = computed(() => extensions.value.join(", "))
|
|
250
|
-
|
|
293
|
+
|
|
294
|
+
function onDrop(e: DragEvent) {
|
|
295
|
+
if ("dataTransfer" in e && e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
|
296
|
+
el.value!.files = e.dataTransfer.files
|
|
297
|
+
e.preventDefault()
|
|
298
|
+
isHovered.value = false
|
|
299
|
+
return updateFiles(el.value!.files)
|
|
300
|
+
}
|
|
301
|
+
return undefined
|
|
302
|
+
}
|
|
303
|
+
async function inputFile(e: InputEvent): Promise<undefined | boolean> {
|
|
251
304
|
e.preventDefault()
|
|
252
305
|
if (el.value!.files) {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
306
|
+
return updateFiles(el.value!.files)
|
|
307
|
+
}
|
|
308
|
+
return undefined
|
|
309
|
+
}
|
|
256
310
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
files.push({ file, isImg })
|
|
276
|
-
} else {
|
|
277
|
-
files.splice(0, files.length, { file, isImg })
|
|
278
|
-
}
|
|
279
|
-
}
|
|
311
|
+
function updateFiles(filesList: FileList): boolean | undefined {
|
|
312
|
+
const errs = []
|
|
313
|
+
for (const file of filesList) {
|
|
314
|
+
const isImg = file.type.startsWith("image")
|
|
315
|
+
|
|
316
|
+
const byPassValidation = props.formats.length === 0
|
|
317
|
+
const isValidMimeType = mimeTypes.value.find(_ => _.endsWith("/*") ? file.type.startsWith(_.slice(0, -2)) : _ === file.type) !== undefined
|
|
318
|
+
const isValidExtension = extensions.value.find(_ => file.name.endsWith(_)) !== undefined
|
|
319
|
+
if (!byPassValidation && (!isValidMimeType || !isValidExtension)) {
|
|
320
|
+
const extension = file.name.match(/.*(\..*)/)?.[1] ?? "Unknown"
|
|
321
|
+
const type = file.type === "" ? "" : ` (${file.type})`
|
|
322
|
+
const message = `File type ${extension}${type} is not allowed. Allowed file types are: ${extensionsList.value}.`
|
|
323
|
+
const err = new Error(message) as FileInputError
|
|
324
|
+
err.file = file
|
|
325
|
+
err.isValidExtension = isValidExtension
|
|
326
|
+
err.isValidMimeType = isValidMimeType
|
|
327
|
+
errs.push(err)
|
|
328
|
+
continue
|
|
280
329
|
}
|
|
281
|
-
if (errs.length > 0)
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
330
|
+
if (errs.length > 0) continue
|
|
331
|
+
if (!files.find(_ => _.file === file)) {
|
|
332
|
+
if ((props.multiple || files.length < 1)
|
|
333
|
+
) {
|
|
334
|
+
files.push({ file, isImg })
|
|
335
|
+
} else {
|
|
336
|
+
files.splice(0, files.length, { file, isImg })
|
|
337
|
+
}
|
|
286
338
|
}
|
|
287
339
|
}
|
|
340
|
+
if (errs.length > 0) {
|
|
341
|
+
errors.splice(0, errors.length, ...errs)
|
|
342
|
+
return false
|
|
343
|
+
} else if (errors.length > 0) {
|
|
344
|
+
errors.splice(0, errors.length)
|
|
345
|
+
}
|
|
288
346
|
return undefined
|
|
289
347
|
}
|
|
348
|
+
|
|
349
|
+
defineExpose({
|
|
350
|
+
clearFiles
|
|
351
|
+
})
|
|
290
352
|
</script>
|
|
291
353
|
|
|
292
354
|
<script lang="ts">
|
|
@@ -19,12 +19,15 @@
|
|
|
19
19
|
p-1
|
|
20
20
|
text-sm
|
|
21
21
|
focus:border-accent-500
|
|
22
|
+
focus-within:border-accent-500
|
|
22
23
|
`,
|
|
23
24
|
($attrs as any).class)"
|
|
24
25
|
v-bind="{ ...$attrs, class: undefined }"
|
|
25
26
|
tabindex="0"
|
|
27
|
+
:data-id="notification.id"
|
|
26
28
|
ref="notificationEl"
|
|
27
29
|
@keydown.enter.self="NotificationHandler.resolveToDefault(notification)"
|
|
30
|
+
@pointerenter="notification.timeout && !notification.isPaused && emit('pause', notification)"
|
|
28
31
|
>
|
|
29
32
|
<slot
|
|
30
33
|
name="top"
|
|
@@ -145,7 +148,7 @@
|
|
|
145
148
|
</template>
|
|
146
149
|
|
|
147
150
|
<script setup lang="ts">
|
|
148
|
-
import { computed, type HTMLAttributes, onMounted, ref, useAttrs } from "vue"
|
|
151
|
+
import { computed, type HTMLAttributes, onBeforeUnmount, onMounted, ref, useAttrs } from "vue"
|
|
149
152
|
|
|
150
153
|
import IFa6RegularCopy from "~icons/fa6-regular/copy"
|
|
151
154
|
import IFa6SolidXmark from "~icons/fa6-solid/xmark"
|
|
@@ -171,6 +174,11 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
171
174
|
handler: undefined
|
|
172
175
|
})
|
|
173
176
|
|
|
177
|
+
const emit = defineEmits<{
|
|
178
|
+
(e: "pause", notification: NotificationEntry): void
|
|
179
|
+
(e: "resume", notification: NotificationEntry): void
|
|
180
|
+
}>()
|
|
181
|
+
|
|
174
182
|
const getColor = (notification: NotificationEntry, option: string): "ok" | "primary" | "danger" | "secondary" => {
|
|
175
183
|
return notification.dangerous.includes(option)
|
|
176
184
|
? "danger"
|
|
@@ -184,9 +192,28 @@ const getColor = (notification: NotificationEntry, option: string): "ok" | "prim
|
|
|
184
192
|
const buttonColors = computed(() => props.notification.options.map((option: any /* what ??? */) => getColor(props.notification, option)))
|
|
185
193
|
|
|
186
194
|
const notificationEl = ref<null | HTMLElement>(null)
|
|
195
|
+
|
|
196
|
+
const mousedownAbortController = new AbortController()
|
|
197
|
+
|
|
187
198
|
onMounted(() => {
|
|
188
199
|
notificationEl.value?.focus()
|
|
200
|
+
if (props.notification.timeout) {
|
|
201
|
+
window.addEventListener("pointerdown", e => {
|
|
202
|
+
if (!e.target || !(e.target instanceof HTMLElement)) return
|
|
203
|
+
if (e.target === notificationEl.value || notificationEl.value?.contains(e.target)) {
|
|
204
|
+
if (props.notification.isPaused) return
|
|
205
|
+
emit("pause", props.notification)
|
|
206
|
+
} else {
|
|
207
|
+
emit("resume", props.notification)
|
|
208
|
+
}
|
|
209
|
+
}, { signal: mousedownAbortController.signal })
|
|
210
|
+
}
|
|
189
211
|
})
|
|
212
|
+
|
|
213
|
+
onBeforeUnmount(() => {
|
|
214
|
+
mousedownAbortController.abort()
|
|
215
|
+
})
|
|
216
|
+
|
|
190
217
|
defineExpose({
|
|
191
218
|
focus: () => {
|
|
192
219
|
notificationEl.value?.focus()
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
class="overflow-hidden my-2"
|
|
33
33
|
v-for="notification of notifications"
|
|
34
34
|
:key="notification.id"
|
|
35
|
-
@
|
|
36
|
-
@
|
|
35
|
+
@pause="handler.pause(notification)"
|
|
36
|
+
@resume="handler.resume(notification)"
|
|
37
37
|
>
|
|
38
38
|
<template #top>
|
|
39
39
|
<LibProgressBar
|