@witchcraft/ui 0.3.11 → 0.3.13
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/LibColorInput/LibColorInput.d.vue.ts +2 -2
- package/dist/runtime/components/LibColorInput/LibColorInput.vue.d.ts +2 -2
- package/dist/runtime/components/LibColorPicker/LibColorPicker.d.vue.ts +3 -3
- package/dist/runtime/components/LibColorPicker/LibColorPicker.vue.d.ts +3 -3
- 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/LibInputDeprecated/LibInputDeprecated.d.vue.ts +3 -3
- package/dist/runtime/components/LibInputDeprecated/LibInputDeprecated.vue.d.ts +3 -3
- package/dist/runtime/components/LibNotifications/LibNotification.vue +16 -1
- package/dist/runtime/components/LibNotifications/LibNotificationTestMessageComponent.d.vue.ts +6 -0
- package/dist/runtime/components/LibNotifications/LibNotificationTestMessageComponent.vue +29 -0
- package/dist/runtime/components/LibNotifications/LibNotificationTestMessageComponent.vue.d.ts +6 -0
- package/dist/runtime/components/LibPopup/LibPopup.d.vue.ts +1 -1
- package/dist/runtime/components/LibPopup/LibPopup.vue.d.ts +1 -1
- package/dist/runtime/helpers/NotificationHandler.d.ts +5 -2
- package/dist/runtime/helpers/NotificationHandler.js +2 -1
- package/dist/runtime/types/index.d.ts +4 -0
- package/dist/runtime/utils/notifyIfError.d.ts +3 -1
- package/dist/runtime/utils/notifyIfError.js +4 -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.stories.ts +22 -1
- package/src/runtime/components/LibNotifications/LibNotification.vue +16 -1
- package/src/runtime/components/LibNotifications/LibNotificationTestMessageComponent.vue +27 -0
- package/src/runtime/helpers/NotificationHandler.ts +6 -2
- package/src/runtime/types/index.ts +5 -0
- package/src/runtime/utils/notifyIfError.ts +6 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AnyFunction, MakeRequired } from "@alanscodelog/utils";
|
|
2
|
-
import { type Reactive } from "vue";
|
|
2
|
+
import { type Component, type Reactive } from "vue";
|
|
3
3
|
export declare class NotificationHandler<TRawEntry extends RawNotificationEntry<any, any> = RawNotificationEntry<any, any>, TEntry extends NotificationEntry<TRawEntry> = NotificationEntry<TRawEntry>> {
|
|
4
4
|
timeout: number;
|
|
5
5
|
debug: boolean;
|
|
@@ -26,7 +26,6 @@ export declare class NotificationHandler<TRawEntry extends RawNotificationEntry<
|
|
|
26
26
|
}
|
|
27
27
|
export type NotificationPromise<TOption extends string = string> = Promise<TOption>;
|
|
28
28
|
export type RawNotificationEntry<TOptions extends string[] = ["Ok", "Cancel"], TCancellable extends boolean | TOptions[number] = "Cancel"> = {
|
|
29
|
-
message: string;
|
|
30
29
|
title?: string;
|
|
31
30
|
code?: string;
|
|
32
31
|
/** @default ["Ok", "Cancel"] */
|
|
@@ -41,6 +40,10 @@ export type RawNotificationEntry<TOptions extends string[] = ["Ok", "Cancel"], T
|
|
|
41
40
|
/** @default false if cancellable, otherwise the default timeout */
|
|
42
41
|
timeout?: number | boolean;
|
|
43
42
|
icon?: string;
|
|
43
|
+
message: string;
|
|
44
|
+
component?: string | Component;
|
|
45
|
+
/** By default the component is passed the message and the messageClasses. Both will be overriden if you set them on componentProps. */
|
|
46
|
+
componentProps?: Record<string, any>;
|
|
44
47
|
};
|
|
45
48
|
export type NotificationEntry<TRawEntry extends RawNotificationEntry<any, any> = RawNotificationEntry<any, any>> = Omit<MakeRequired<TRawEntry, "options" | "requiresAction" | "default" | "dangerous">, "cancellable"> & {
|
|
46
49
|
promise: NotificationPromise;
|
|
@@ -4,7 +4,7 @@ import { indent } from "@alanscodelog/utils/indent";
|
|
|
4
4
|
import { isBlank } from "@alanscodelog/utils/isBlank";
|
|
5
5
|
import { pretty } from "@alanscodelog/utils/pretty";
|
|
6
6
|
import { setReadOnly } from "@alanscodelog/utils/setReadOnly";
|
|
7
|
-
import { reactive } from "vue";
|
|
7
|
+
import { markRaw, reactive } from "vue";
|
|
8
8
|
export class NotificationHandler {
|
|
9
9
|
timeout = 5e3;
|
|
10
10
|
debug = false;
|
|
@@ -87,6 +87,7 @@ export class NotificationHandler {
|
|
|
87
87
|
default: "Ok",
|
|
88
88
|
cancellable: rawEntry.cancellable,
|
|
89
89
|
...rawEntry,
|
|
90
|
+
component: rawEntry.component && typeof rawEntry.component !== "string" ? markRaw(rawEntry.component) : void 0,
|
|
90
91
|
dangerous: rawEntry.dangerous ?? [],
|
|
91
92
|
timeout: rawEntry.timeout === true ? this.timeout : rawEntry.timeout !== void 0 && rawEntry.timeout !== false ? rawEntry.timeout : void 0
|
|
92
93
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { NotificationEntry } from "../helpers/NotificationHandler.js";
|
|
1
2
|
/**
|
|
2
3
|
* Notifies the user if the given value is an error. Useful for making non-critical errors don't go unnoticed.
|
|
3
4
|
*
|
|
@@ -5,10 +6,11 @@
|
|
|
5
6
|
*
|
|
6
7
|
* If the value is not an error, it is returned.
|
|
7
8
|
*/
|
|
8
|
-
export declare function notifyIfError<T>(err: T, { logger, ns, force }?: {
|
|
9
|
+
export declare function notifyIfError<T>(err: T, { logger, ns, force, entry }?: {
|
|
9
10
|
logger?: {
|
|
10
11
|
debug: (...args: any[]) => void;
|
|
11
12
|
};
|
|
12
13
|
ns?: string;
|
|
13
14
|
force?: boolean;
|
|
15
|
+
entry?: Partial<NotificationEntry<any>>;
|
|
14
16
|
}): T;
|
|
@@ -3,7 +3,8 @@ import { useNotificationHandler } from "../composables/useNotificationHandler.js
|
|
|
3
3
|
export function notifyIfError(err, {
|
|
4
4
|
logger,
|
|
5
5
|
ns,
|
|
6
|
-
force = false
|
|
6
|
+
force = false,
|
|
7
|
+
entry
|
|
7
8
|
} = {}) {
|
|
8
9
|
if (force || err instanceof Error) {
|
|
9
10
|
const errMessage = {
|
|
@@ -22,7 +23,8 @@ export function notifyIfError(err, {
|
|
|
22
23
|
...errMessage,
|
|
23
24
|
options: ["Ok"],
|
|
24
25
|
cancellable: "Ok",
|
|
25
|
-
timeout: true
|
|
26
|
+
timeout: true,
|
|
27
|
+
...entry
|
|
26
28
|
});
|
|
27
29
|
}
|
|
28
30
|
return err;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@witchcraft/ui",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.13",
|
|
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">
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import type { Meta, StoryObj } from "@storybook/vue3"
|
|
3
3
|
|
|
4
4
|
import LibNotification from "./LibNotification.vue"
|
|
5
|
+
import LibNotificationTestMessageComponent from "./LibNotificationTestMessageComponent.vue"
|
|
5
6
|
|
|
6
7
|
import { NotificationHandler } from "../../helpers/NotificationHandler.js"
|
|
7
8
|
import * as components from "../index.js"
|
|
@@ -21,7 +22,11 @@ type Story = StoryObj<typeof LibNotification>
|
|
|
21
22
|
|
|
22
23
|
export const Primary: Story = {
|
|
23
24
|
render: args => ({
|
|
24
|
-
components: {
|
|
25
|
+
components: {
|
|
26
|
+
...components,
|
|
27
|
+
LibNotification,
|
|
28
|
+
LibNotificationTestMessageComponent
|
|
29
|
+
},
|
|
25
30
|
setup() {
|
|
26
31
|
return { args }
|
|
27
32
|
},
|
|
@@ -136,3 +141,19 @@ export const CustomDefaultAndDangerousOption: Story = {
|
|
|
136
141
|
})
|
|
137
142
|
}
|
|
138
143
|
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
export const CustomMessageComponent: Story = {
|
|
147
|
+
...Primary,
|
|
148
|
+
args: {
|
|
149
|
+
...Primary.args,
|
|
150
|
+
// @ts-expect-error calling protected method
|
|
151
|
+
notification: handler._createEntry({
|
|
152
|
+
...Primary.args!.notification,
|
|
153
|
+
component: LibNotificationTestMessageComponent,
|
|
154
|
+
componentProps: {
|
|
155
|
+
customProp: "Custom Prop"
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
</div>
|
|
91
91
|
</div>
|
|
92
92
|
<slot
|
|
93
|
-
v-if="notification.message"
|
|
93
|
+
v-if="notification.message && !notification.component"
|
|
94
94
|
name="message"
|
|
95
95
|
v-bind="setSlotVar('message', {
|
|
96
96
|
class: `
|
|
@@ -110,6 +110,21 @@
|
|
|
110
110
|
{{ notification.message }}
|
|
111
111
|
</div>
|
|
112
112
|
</slot>
|
|
113
|
+
<Component
|
|
114
|
+
v-if="notification.component"
|
|
115
|
+
:is="notification.component"
|
|
116
|
+
v-bind="{
|
|
117
|
+
message: notification.message,
|
|
118
|
+
messageClasses: `
|
|
119
|
+
notification--message
|
|
120
|
+
whitespace-pre-wrap
|
|
121
|
+
text-neutral-800
|
|
122
|
+
dark:text-neutral-200
|
|
123
|
+
mb-1
|
|
124
|
+
`,
|
|
125
|
+
...(notification.componentProps ?? {})
|
|
126
|
+
}"
|
|
127
|
+
/>
|
|
113
128
|
<div class="notification--footer flex items-end justify-between">
|
|
114
129
|
<div
|
|
115
130
|
v-if="notification.code"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="border-2 border-accent-500 p-1 px-2 rounded-sm bg-accent-500/10">
|
|
3
|
+
<div class="text-lg">
|
|
4
|
+
Custom Message Component
|
|
5
|
+
</div>
|
|
6
|
+
<div class="font-bold">
|
|
7
|
+
Original message:
|
|
8
|
+
</div>
|
|
9
|
+
<div
|
|
10
|
+
:class="props.messageClasses"
|
|
11
|
+
>
|
|
12
|
+
{{ props.message }}
|
|
13
|
+
</div>
|
|
14
|
+
<div class="font-bold">
|
|
15
|
+
Custom Prop:
|
|
16
|
+
</div>
|
|
17
|
+
<div>
|
|
18
|
+
{{ props.customProp }}
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
</template>
|
|
22
|
+
|
|
23
|
+
<script setup lang="ts">
|
|
24
|
+
import type { CustomNotificationComponentProps } from "../../types/index.js"
|
|
25
|
+
|
|
26
|
+
const props = defineProps<CustomNotificationComponentProps & { customProp: string }>()
|
|
27
|
+
</script>
|
|
@@ -5,7 +5,7 @@ import { indent } from "@alanscodelog/utils/indent"
|
|
|
5
5
|
import { isBlank } from "@alanscodelog/utils/isBlank"
|
|
6
6
|
import { pretty } from "@alanscodelog/utils/pretty"
|
|
7
7
|
import { setReadOnly } from "@alanscodelog/utils/setReadOnly"
|
|
8
|
-
import { type Reactive, reactive } from "vue"
|
|
8
|
+
import { type Component, markRaw, type Reactive, reactive } from "vue"
|
|
9
9
|
|
|
10
10
|
export class NotificationHandler<
|
|
11
11
|
TRawEntry extends RawNotificationEntry<any, any> = RawNotificationEntry<any, any>,
|
|
@@ -99,6 +99,7 @@ export class NotificationHandler<
|
|
|
99
99
|
default: "Ok",
|
|
100
100
|
cancellable: rawEntry.cancellable,
|
|
101
101
|
...rawEntry,
|
|
102
|
+
component: rawEntry.component && typeof rawEntry.component !== "string" ? markRaw(rawEntry.component) : undefined,
|
|
102
103
|
dangerous: rawEntry.dangerous ?? [],
|
|
103
104
|
timeout: rawEntry.timeout === true
|
|
104
105
|
? this.timeout
|
|
@@ -211,7 +212,6 @@ export type RawNotificationEntry<
|
|
|
211
212
|
TOptions extends string[] = ["Ok", "Cancel"],
|
|
212
213
|
TCancellable extends boolean | TOptions[number] = "Cancel"
|
|
213
214
|
> = {
|
|
214
|
-
message: string
|
|
215
215
|
title?: string
|
|
216
216
|
code?: string
|
|
217
217
|
/** @default ["Ok", "Cancel"] */
|
|
@@ -226,6 +226,10 @@ export type RawNotificationEntry<
|
|
|
226
226
|
/** @default false if cancellable, otherwise the default timeout */
|
|
227
227
|
timeout?: number | boolean
|
|
228
228
|
icon?: string
|
|
229
|
+
message: string
|
|
230
|
+
component?: string | Component
|
|
231
|
+
/** By default the component is passed the message and the messageClasses. Both will be overriden if you set them on componentProps. */
|
|
232
|
+
componentProps?: Record<string, any>
|
|
229
233
|
}
|
|
230
234
|
|
|
231
235
|
export type NotificationEntry<
|