@witchcraft/ui 0.3.2 → 0.3.3
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/animations.css +1 -0
- package/dist/runtime/assets/base.css +1 -1
- package/dist/runtime/assets/tailwind.css +1 -1
- package/dist/runtime/components/LibNotifications/LibNotification.d.vue.ts +15 -2
- package/dist/runtime/components/LibNotifications/LibNotification.vue +84 -25
- package/dist/runtime/components/LibNotifications/LibNotification.vue.d.ts +15 -2
- package/dist/runtime/components/LibNotifications/LibNotifications.vue +110 -87
- package/dist/runtime/components/LibPopup/LibPopup.vue +2 -6
- package/dist/runtime/components/LibProgressBar/LibProgressBar.d.vue.ts +1 -0
- package/dist/runtime/components/LibProgressBar/LibProgressBar.vue +1 -1
- package/dist/runtime/components/LibProgressBar/LibProgressBar.vue.d.ts +1 -0
- package/dist/runtime/components/LibRecorder/LibRecorder.vue +1 -1
- package/dist/runtime/composables/index.d.ts +2 -0
- package/dist/runtime/composables/index.js +2 -0
- package/dist/runtime/composables/useSlotVars.d.ts +32 -0
- package/dist/runtime/composables/useSlotVars.js +12 -0
- package/dist/runtime/helpers/NotificationHandler.d.ts +11 -4
- package/dist/runtime/helpers/NotificationHandler.js +34 -16
- package/package.json +1 -1
- package/src/runtime/assets/animations.css +75 -0
- package/src/runtime/assets/base.css +0 -27
- package/src/runtime/assets/tailwind.css +1 -0
- package/src/runtime/components/LibColorPicker/LibColorPicker.stories.ts +1 -1
- package/src/runtime/components/LibNotifications/LibNotification.vue +86 -25
- package/src/runtime/components/LibNotifications/LibNotifications.stories.ts +4 -4
- package/src/runtime/components/LibNotifications/LibNotifications.vue +112 -90
- package/src/runtime/components/LibPopup/LibPopup.vue +2 -6
- package/src/runtime/components/LibProgressBar/LibProgressBar.vue +2 -1
- package/src/runtime/components/LibRecorder/LibRecorder.vue +1 -1
- package/src/runtime/composables/index.ts +2 -0
- package/src/runtime/composables/useSlotVars.ts +41 -0
- package/src/runtime/helpers/NotificationHandler.ts +44 -22
|
@@ -4,12 +4,13 @@ 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
8
|
export class NotificationHandler {
|
|
8
9
|
timeout = 5e3;
|
|
9
10
|
debug = false;
|
|
10
11
|
id = 0;
|
|
11
|
-
queue
|
|
12
|
-
history
|
|
12
|
+
queue;
|
|
13
|
+
history;
|
|
13
14
|
maxHistory = 100;
|
|
14
15
|
listeners = [];
|
|
15
16
|
stringifier;
|
|
@@ -18,6 +19,8 @@ export class NotificationHandler {
|
|
|
18
19
|
stringifier,
|
|
19
20
|
maxHistory
|
|
20
21
|
} = {}) {
|
|
22
|
+
this.queue = reactive([]);
|
|
23
|
+
this.history = reactive([]);
|
|
21
24
|
if (timeout) this.timeout = timeout;
|
|
22
25
|
if (maxHistory) this.maxHistory = maxHistory;
|
|
23
26
|
if (stringifier) this.stringifier = stringifier;
|
|
@@ -102,9 +105,10 @@ export class NotificationHandler {
|
|
|
102
105
|
entry.resolve = _resolve;
|
|
103
106
|
});
|
|
104
107
|
if (entry.timeout !== void 0) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
+
entry._timer = {
|
|
109
|
+
elapsedBeforePause: 0
|
|
110
|
+
};
|
|
111
|
+
this.resume(entry);
|
|
108
112
|
}
|
|
109
113
|
this.queue.push(entry);
|
|
110
114
|
for (const listener of this.listeners) {
|
|
@@ -115,8 +119,10 @@ export class NotificationHandler {
|
|
|
115
119
|
for (const listener of this.listeners) {
|
|
116
120
|
listener(entry, "resolved");
|
|
117
121
|
}
|
|
122
|
+
;
|
|
118
123
|
this.history.push(entry);
|
|
119
124
|
if (this.history.length > this.maxHistory) {
|
|
125
|
+
;
|
|
120
126
|
this.history.splice(0, 1);
|
|
121
127
|
for (const listener of this.listeners) {
|
|
122
128
|
listener(entry, "deleted");
|
|
@@ -126,6 +132,29 @@ export class NotificationHandler {
|
|
|
126
132
|
return res;
|
|
127
133
|
});
|
|
128
134
|
}
|
|
135
|
+
pause(notification) {
|
|
136
|
+
if (notification.timeout === void 0) {
|
|
137
|
+
throw new Error(`Cannot pause notification with no timeout: ${notification.id}`);
|
|
138
|
+
}
|
|
139
|
+
if (notification.isPaused) {
|
|
140
|
+
throw new Error(`Cannot pause notification that is already paused: ${notification.id}`);
|
|
141
|
+
}
|
|
142
|
+
notification.isPaused = true;
|
|
143
|
+
clearTimeout(notification._timer.id);
|
|
144
|
+
notification._timer.elapsedBeforePause += Date.now() - notification.startTime;
|
|
145
|
+
}
|
|
146
|
+
resume(notification) {
|
|
147
|
+
if (notification.timeout === void 0) {
|
|
148
|
+
throw new Error(`Cannot resume notification with no timeout: ${notification.id}`);
|
|
149
|
+
}
|
|
150
|
+
notification.isPaused = false;
|
|
151
|
+
notification.startTime = Date.now();
|
|
152
|
+
const remaining = notification.timeout - notification._timer.elapsedBeforePause;
|
|
153
|
+
clearTimeout(notification._timer.id);
|
|
154
|
+
notification._timer.id = setTimeout(() => {
|
|
155
|
+
notification.resolve(notification.cancellable);
|
|
156
|
+
}, remaining);
|
|
157
|
+
}
|
|
129
158
|
static resolveToDefault(notification) {
|
|
130
159
|
notification.resolve(notification.default);
|
|
131
160
|
}
|
|
@@ -148,15 +177,4 @@ export class NotificationHandler {
|
|
|
148
177
|
clear() {
|
|
149
178
|
setReadOnly(this, "history", []);
|
|
150
179
|
}
|
|
151
|
-
addNotificationListener(cb) {
|
|
152
|
-
this.listeners.push(cb);
|
|
153
|
-
}
|
|
154
|
-
removeNotificationListener(cb) {
|
|
155
|
-
const exists = this.listeners.indexOf(cb);
|
|
156
|
-
if (exists > -1) {
|
|
157
|
-
this.listeners.splice(exists, 1);
|
|
158
|
-
} else {
|
|
159
|
-
throw new Error(`Listener does not exist: ${cb.toString()}`);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
180
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
@theme {
|
|
2
|
+
--animate-blinkInf: blink 1s linear-infinite;
|
|
3
|
+
@keyframes blink {
|
|
4
|
+
0% {
|
|
5
|
+
opacity: 0;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
25% {
|
|
9
|
+
opacity: 1;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
75% {
|
|
13
|
+
opacity: 1;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
100% {
|
|
17
|
+
opacity: 0;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
--animate-slideBgInf: slide-bg 10s linear-infinite;
|
|
22
|
+
@keyframes slide {
|
|
23
|
+
0% {
|
|
24
|
+
background-position:0%;
|
|
25
|
+
}
|
|
26
|
+
100% {
|
|
27
|
+
background-position: 100%;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
--animate-hide: hide 500ms ease-in;
|
|
32
|
+
@keyframes hide {
|
|
33
|
+
from {
|
|
34
|
+
opacity: 1;
|
|
35
|
+
}
|
|
36
|
+
to {
|
|
37
|
+
opacity: 0;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
--animate-slideIn: slideIn 500ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
42
|
+
@keyframes slideIn {
|
|
43
|
+
from {
|
|
44
|
+
transform: translateX(100%);
|
|
45
|
+
opacity: 0;
|
|
46
|
+
}
|
|
47
|
+
to {
|
|
48
|
+
transform: translateX(0);
|
|
49
|
+
opacity: 1;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
--animate-overlayShow: overlayShow 500ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
54
|
+
@keyframes overlayShow {
|
|
55
|
+
from {
|
|
56
|
+
opacity: 0;
|
|
57
|
+
}
|
|
58
|
+
to {
|
|
59
|
+
opacity: 1;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
--animate-contentShow: contentShow 500ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
64
|
+
@keyframes contentShow {
|
|
65
|
+
from {
|
|
66
|
+
opacity: 0;
|
|
67
|
+
transform: translate(0, -10%) scale(0.96);
|
|
68
|
+
}
|
|
69
|
+
to {
|
|
70
|
+
opacity: 1;
|
|
71
|
+
transform: scale(1);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
@@ -34,33 +34,6 @@ animations can be calculated correctly. */
|
|
|
34
34
|
user-select: none;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
/* Animations */
|
|
38
|
-
@keyframes blink {
|
|
39
|
-
0% {
|
|
40
|
-
opacity: 0;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
25% {
|
|
44
|
-
opacity: 1;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
75% {
|
|
48
|
-
opacity: 1;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
100% {
|
|
52
|
-
opacity: 0;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
@keyframes slide {
|
|
57
|
-
0% {
|
|
58
|
-
background-position:0%;
|
|
59
|
-
}
|
|
60
|
-
100% {
|
|
61
|
-
background-position: 100%;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
37
|
|
|
65
38
|
a {
|
|
66
39
|
@apply link-like;
|
|
@@ -23,7 +23,7 @@ export const Primary: Story = {
|
|
|
23
23
|
components,
|
|
24
24
|
setup: () => {
|
|
25
25
|
const color = ref({ r: 0, g: 0, b: 0/* , a: 0.5 */, ...(args.modelValue) })
|
|
26
|
-
delete args.modelValue
|
|
26
|
+
delete (args as any).modelValue
|
|
27
27
|
const handleChange = (e: any) => {
|
|
28
28
|
color.value = { ...e }
|
|
29
29
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div
|
|
3
|
-
|
|
3
|
+
v-if="notification"
|
|
4
|
+
:class="twMerge(`
|
|
5
|
+
notification
|
|
4
6
|
max-w-700px
|
|
5
7
|
bg-neutral-50
|
|
6
8
|
dark:bg-neutral-900
|
|
@@ -8,11 +10,15 @@
|
|
|
8
10
|
dark:text-bg
|
|
9
11
|
border
|
|
10
12
|
border-neutral-400
|
|
13
|
+
dark:border-neutral-700
|
|
11
14
|
rounded-sm
|
|
12
15
|
focus-outline
|
|
13
|
-
flex
|
|
16
|
+
flex
|
|
17
|
+
flex-col
|
|
14
18
|
gap-2
|
|
15
|
-
p-
|
|
19
|
+
p-1
|
|
20
|
+
text-sm
|
|
21
|
+
focus:border-accent-500
|
|
16
22
|
`,
|
|
17
23
|
($attrs as any).class)"
|
|
18
24
|
v-bind="{ ...$attrs, class: undefined }"
|
|
@@ -20,30 +26,59 @@
|
|
|
20
26
|
ref="notificationEl"
|
|
21
27
|
@keydown.enter.self="NotificationHandler.resolveToDefault(notification)"
|
|
22
28
|
>
|
|
23
|
-
<
|
|
24
|
-
|
|
29
|
+
<slot
|
|
30
|
+
name="top"
|
|
31
|
+
:notification="notification"
|
|
32
|
+
/>
|
|
33
|
+
<div
|
|
34
|
+
class="
|
|
35
|
+
notification--header
|
|
36
|
+
flex-reverse
|
|
37
|
+
flex
|
|
38
|
+
justify-between
|
|
39
|
+
items-center
|
|
40
|
+
"
|
|
41
|
+
>
|
|
42
|
+
<slot
|
|
25
43
|
v-if="notification.title"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
44
|
+
name="title"
|
|
45
|
+
v-bind="setSlotVar('title', {
|
|
46
|
+
title: notification.title,
|
|
47
|
+
class: `
|
|
48
|
+
notification--title
|
|
49
|
+
focus-outline
|
|
50
|
+
rounded-sm
|
|
51
|
+
font-bold
|
|
52
|
+
`,
|
|
53
|
+
tabindex: 0
|
|
54
|
+
})"
|
|
32
55
|
>
|
|
33
|
-
|
|
34
|
-
|
|
56
|
+
<div
|
|
57
|
+
v-bind="slotVars.title"
|
|
58
|
+
>
|
|
59
|
+
{{ notification.title }}
|
|
60
|
+
</div>
|
|
61
|
+
</slot>
|
|
35
62
|
<div class="notification--spacer flex-1"/>
|
|
36
63
|
<div class="actions flex">
|
|
37
64
|
<LibButton
|
|
38
65
|
:border="false"
|
|
39
|
-
class="
|
|
66
|
+
class="
|
|
67
|
+
notification--title-button
|
|
68
|
+
notification--copy-button
|
|
69
|
+
text-neutral-700
|
|
70
|
+
dark:text-neutral-300
|
|
71
|
+
"
|
|
40
72
|
@click="copy(handler ? handler.stringify(notification) : JSON.stringify(notification))"
|
|
41
73
|
>
|
|
42
74
|
<icon><i-fa6-regular-copy/></icon>
|
|
43
75
|
</LibButton>
|
|
44
76
|
<lib-button
|
|
45
77
|
v-if="notification.cancellable"
|
|
46
|
-
class="
|
|
78
|
+
class="
|
|
79
|
+
notification--title-button
|
|
80
|
+
notification--cancel-button
|
|
81
|
+
"
|
|
47
82
|
:border="false"
|
|
48
83
|
@click="NotificationHandler.dismiss(notification)"
|
|
49
84
|
>
|
|
@@ -51,12 +86,27 @@
|
|
|
51
86
|
</lib-button>
|
|
52
87
|
</div>
|
|
53
88
|
</div>
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
89
|
+
<slot
|
|
90
|
+
v-if="notification.message"
|
|
91
|
+
name="message"
|
|
92
|
+
v-bind="setSlotVar('message', {
|
|
93
|
+
class: `
|
|
94
|
+
notification--message
|
|
95
|
+
whitespace-pre-wrap
|
|
96
|
+
text-neutral-800
|
|
97
|
+
dark:text-neutral-200
|
|
98
|
+
mb-1
|
|
99
|
+
`,
|
|
100
|
+
message: notification.message,
|
|
101
|
+
tabindex: 0
|
|
102
|
+
})"
|
|
57
103
|
>
|
|
58
|
-
|
|
59
|
-
|
|
104
|
+
<div
|
|
105
|
+
v-bind="slotVars.message"
|
|
106
|
+
>
|
|
107
|
+
{{ notification.message }}
|
|
108
|
+
</div>
|
|
109
|
+
</slot>
|
|
60
110
|
<div class="notification--footer flex items-end justify-between">
|
|
61
111
|
<div
|
|
62
112
|
v-if="notification.code"
|
|
@@ -67,19 +117,23 @@
|
|
|
67
117
|
<div class="notification--footer-spacer flex-1 py-1"/>
|
|
68
118
|
<div
|
|
69
119
|
v-if="notification.options"
|
|
70
|
-
class="
|
|
71
|
-
|
|
120
|
+
class="
|
|
121
|
+
notification--options
|
|
122
|
+
flex
|
|
123
|
+
flex-wrap
|
|
124
|
+
justify-end
|
|
72
125
|
gap-2
|
|
73
126
|
"
|
|
74
127
|
>
|
|
75
128
|
<lib-button
|
|
76
129
|
:label="option"
|
|
77
130
|
:class="twMerge(`
|
|
131
|
+
notification--button
|
|
78
132
|
notification--option-button
|
|
133
|
+
px-2
|
|
79
134
|
`,
|
|
80
|
-
|
|
135
|
+
notification.default === option && `notification--default`
|
|
81
136
|
)"
|
|
82
|
-
:border="buttonColors[i] !== 'secondary'"
|
|
83
137
|
:color="buttonColors[i]"
|
|
84
138
|
v-for="option, i in notification.options"
|
|
85
139
|
:key="option"
|
|
@@ -91,11 +145,12 @@
|
|
|
91
145
|
</template>
|
|
92
146
|
|
|
93
147
|
<script setup lang="ts">
|
|
94
|
-
import { computed, type HTMLAttributes, ref, useAttrs } from "vue"
|
|
148
|
+
import { computed, type HTMLAttributes, onMounted, ref, useAttrs } from "vue"
|
|
95
149
|
|
|
96
150
|
import IFa6RegularCopy from "~icons/fa6-regular/copy"
|
|
97
151
|
import IFa6SolidXmark from "~icons/fa6-solid/xmark"
|
|
98
152
|
|
|
153
|
+
import { useSlotVars } from "../../composables/useSlotVars.js"
|
|
99
154
|
import { copy } from "../../helpers/copy.js"
|
|
100
155
|
import { type NotificationEntry, NotificationHandler } from "../../helpers/NotificationHandler.js"
|
|
101
156
|
import { twMerge } from "../../utils/twMerge.js"
|
|
@@ -109,6 +164,9 @@ defineOptions({
|
|
|
109
164
|
})
|
|
110
165
|
const $attrs = useAttrs()
|
|
111
166
|
|
|
167
|
+
const { setSlotVar, slotVars } = useSlotVars()
|
|
168
|
+
|
|
169
|
+
|
|
112
170
|
const props = withDefaults(defineProps<Props>(), {
|
|
113
171
|
handler: undefined
|
|
114
172
|
})
|
|
@@ -120,6 +178,9 @@ const getColor = (notification: NotificationEntry, option: string): "ok" | "prim
|
|
|
120
178
|
const buttonColors = computed(() => props.notification.options.map((option: any /* what ??? */) => getColor(props.notification, option)))
|
|
121
179
|
|
|
122
180
|
const notificationEl = ref<null | HTMLElement>(null)
|
|
181
|
+
onMounted(() => {
|
|
182
|
+
notificationEl.value?.focus()
|
|
183
|
+
})
|
|
123
184
|
defineExpose({
|
|
124
185
|
focus: () => {
|
|
125
186
|
notificationEl.value?.focus()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/naming-convention */
|
|
2
2
|
import type { Meta, StoryObj } from "@storybook/vue3"
|
|
3
|
-
import {
|
|
3
|
+
import { ref } from "vue"
|
|
4
4
|
|
|
5
5
|
import LibNotifications from "./LibNotifications.vue"
|
|
6
6
|
|
|
@@ -23,7 +23,7 @@ export const Primary: Story = {
|
|
|
23
23
|
render: args => ({
|
|
24
24
|
components,
|
|
25
25
|
setup() {
|
|
26
|
-
const handler =
|
|
26
|
+
const handler = new NotificationHandler({})
|
|
27
27
|
|
|
28
28
|
let count = 0
|
|
29
29
|
|
|
@@ -76,7 +76,7 @@ export const Primary: Story = {
|
|
|
76
76
|
void handler.notify({
|
|
77
77
|
title: withTitle.value ? `Notification(${count})` : undefined,
|
|
78
78
|
message: `This is a notification. No action required.`,
|
|
79
|
-
timeout: disableTimeout.value ? false :
|
|
79
|
+
timeout: disableTimeout.value ? false : 5000
|
|
80
80
|
})
|
|
81
81
|
}
|
|
82
82
|
return {
|
|
@@ -97,7 +97,7 @@ export const Primary: Story = {
|
|
|
97
97
|
backgrounds: { disable: true },
|
|
98
98
|
// <lib-debug>{{args.handler}}</lib-debug>
|
|
99
99
|
template: `
|
|
100
|
-
<lib-root :outline="args.outline">
|
|
100
|
+
<lib-root :outline="args.outline" :notification-handler="handler">
|
|
101
101
|
<lib-button :label="'Notify Timeoutable'" @click="notifyTimeoutable()"></lib-button>
|
|
102
102
|
<lib-button :label="'Notify RequiresAction'" @click="notifyRequiresAction()"></lib-button>
|
|
103
103
|
<lib-button :label="'Notify Non-Cancellable that RequiresAction'" @click="notifyNonCancellableRequiresAction()"></lib-button>
|
|
@@ -1,69 +1,131 @@
|
|
|
1
1
|
<template>
|
|
2
|
+
<!-- using custom toasts, reka-ui toasts still have issues, like like of control over pause, and I can't get the leave event to animate or transition with vue transitions to work -->
|
|
2
3
|
<TransitionGroup
|
|
3
4
|
name="list"
|
|
4
5
|
tag="div"
|
|
5
|
-
:class="twMerge(`
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
6
|
+
:class="twMerge(`
|
|
7
|
+
notifications
|
|
8
|
+
[--notification-width:300px]
|
|
9
|
+
fixed
|
|
10
|
+
top-0
|
|
11
|
+
z-50
|
|
12
|
+
right-[calc(var(--notification-width)*-1)]
|
|
13
|
+
py-2
|
|
14
|
+
w-[calc(var(--spacing)*2+var(--notification-width)*2)]
|
|
15
|
+
[&_.notification]:w-[var(--notification-width)]
|
|
16
|
+
max-h-[100dvh]
|
|
17
|
+
flex
|
|
18
|
+
flex-col
|
|
19
|
+
[&_.notification]:shrink-0
|
|
20
|
+
gap-1
|
|
21
|
+
list-none
|
|
22
|
+
outline-none
|
|
23
|
+
overflow-y-auto
|
|
24
|
+
overflow-x-clip
|
|
25
|
+
scrollbar-hidden
|
|
26
|
+
`, ($attrs as any).class)"
|
|
15
27
|
v-bind="{ ...$attrs, class: undefined }"
|
|
16
28
|
>
|
|
17
29
|
<lib-notification
|
|
18
|
-
class="pointer-events-auto"
|
|
19
30
|
:handler="handler"
|
|
20
31
|
tabindex="0"
|
|
21
32
|
:notification="notification"
|
|
33
|
+
class="overflow-hidden"
|
|
22
34
|
v-for="notification of notifications"
|
|
23
35
|
:key="notification.id"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
<Transition>
|
|
27
|
-
<div
|
|
28
|
-
v-show="topNotifications.length > 0"
|
|
29
|
-
:class="twMerge(`notifications--none`, ($attrs as any).class)"
|
|
30
|
-
/>
|
|
31
|
-
</Transition>
|
|
32
|
-
<Transition>
|
|
33
|
-
<dialog
|
|
34
|
-
v-show="topNotifications.length > 0"
|
|
35
|
-
:id="id"
|
|
36
|
-
:class="twMerge(`notifications-modal
|
|
37
|
-
bg-transparent
|
|
38
|
-
p-0
|
|
39
|
-
backdrop:bg-black/50
|
|
40
|
-
backdrop:p-5
|
|
41
|
-
`, ($attrs as any).class)"
|
|
42
|
-
ref="dialogEl"
|
|
43
|
-
@click.self.prevent="topNotifications[0] && NotificationHandler.dismiss(topNotifications[0])"
|
|
36
|
+
@pointerenter="notification.timeout && !notification.isPaused && handler.pause(notification)"
|
|
37
|
+
@blur="notification.timeout && notification.isPaused && handler.resume(notification)"
|
|
44
38
|
>
|
|
45
|
-
<
|
|
39
|
+
<template #top>
|
|
40
|
+
<LibProgressBar
|
|
41
|
+
v-if="notification.timeout !== undefined"
|
|
42
|
+
class="
|
|
43
|
+
w-full
|
|
44
|
+
h-1
|
|
45
|
+
before:duration-[10ms]
|
|
46
|
+
-mt-1
|
|
47
|
+
-mx-[calc(var(--spacing)*2+2px)]
|
|
48
|
+
rounded-none
|
|
49
|
+
"
|
|
50
|
+
:progress="100 - (((notification.isPaused ? (notification._timer.elapsedBeforePause): (notification._timer.elapsedBeforePause + (time - notification.startTime))) / notification.timeout) * 100)"
|
|
51
|
+
/>
|
|
52
|
+
</template>
|
|
53
|
+
</lib-notification>
|
|
54
|
+
</TransitionGroup>
|
|
55
|
+
<!-- we don't need to worry about the user accidentally closing a non-closable dialog as keeping open=true (which the handler handles when the component tries to close) is enough to keep it open without issues -->
|
|
56
|
+
<AlertDialogRoot
|
|
57
|
+
:open="topNotifications.length > 0 && topNotifications[0] !== undefined"
|
|
58
|
+
@update:open="topNotifications[0] && NotificationHandler.dismiss(topNotifications[0])"
|
|
59
|
+
>
|
|
60
|
+
<AlertDialogPortal :to="'#root'">
|
|
61
|
+
<AlertDialogOverlay
|
|
62
|
+
class="
|
|
63
|
+
fixed inset-0 z-30
|
|
64
|
+
bg-neutral-950/20
|
|
65
|
+
data-[state=open]:animate-overlayShow
|
|
66
|
+
"
|
|
67
|
+
/>
|
|
68
|
+
<AlertDialogContent
|
|
69
|
+
class="
|
|
70
|
+
data-[state=open]:animate-contentShow
|
|
71
|
+
fixed
|
|
72
|
+
top-[50%]
|
|
73
|
+
left-[50%]
|
|
74
|
+
translate-x-[-50%]
|
|
75
|
+
translate-y-[-50%]
|
|
76
|
+
max-h-[80dvh]
|
|
77
|
+
max-w-[700px]
|
|
78
|
+
z-100
|
|
79
|
+
"
|
|
80
|
+
>
|
|
46
81
|
<lib-notification
|
|
47
|
-
|
|
82
|
+
class="
|
|
83
|
+
top-notification
|
|
84
|
+
text-md
|
|
85
|
+
gap-2
|
|
86
|
+
p-2
|
|
87
|
+
[&_.notification--button]:p-2
|
|
88
|
+
[&_.notification--button]:py-1
|
|
89
|
+
[&_.notification--header]:text-lg
|
|
90
|
+
[&_.notification--message]:py-3
|
|
91
|
+
"
|
|
48
92
|
:handler="handler"
|
|
49
|
-
|
|
50
|
-
:notification="topNotifications[0]"
|
|
93
|
+
:notification="topNotifications[0]!"
|
|
51
94
|
ref="topNotificationComp"
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
95
|
+
>
|
|
96
|
+
<template #title="slotProps">
|
|
97
|
+
<AlertDialogTitle v-bind="slotProps">
|
|
98
|
+
{{ slotProps.title }}
|
|
99
|
+
</AlertDialogTitle>
|
|
100
|
+
</template>
|
|
101
|
+
<template #message="slotProps">
|
|
102
|
+
<AlertDialogDescription v-bind="slotProps">
|
|
103
|
+
{{ slotProps.message }}
|
|
104
|
+
</AlertDialogDescription>
|
|
105
|
+
</template>
|
|
106
|
+
</lib-notification>
|
|
107
|
+
</AlertDialogContent>
|
|
108
|
+
</AlertDialogPortal>
|
|
109
|
+
</AlertDialogRoot>
|
|
56
110
|
</template>
|
|
57
111
|
|
|
58
112
|
<script setup lang="ts">
|
|
59
|
-
import {
|
|
60
|
-
|
|
113
|
+
import {
|
|
114
|
+
AlertDialogContent,
|
|
115
|
+
AlertDialogDescription,
|
|
116
|
+
AlertDialogOverlay,
|
|
117
|
+
AlertDialogPortal,
|
|
118
|
+
AlertDialogRoot,
|
|
119
|
+
AlertDialogTitle
|
|
120
|
+
} from "reka-ui"
|
|
121
|
+
import { computed, ref } from "vue"
|
|
61
122
|
|
|
62
123
|
import LibNotification from "./LibNotification.vue"
|
|
63
124
|
|
|
64
125
|
import { useNotificationHandler } from "../../composables/useNotificationHandler.js"
|
|
65
|
-
import {
|
|
126
|
+
import { NotificationHandler } from "../../helpers/NotificationHandler.js"
|
|
66
127
|
import { twMerge } from "../../utils/twMerge.js"
|
|
128
|
+
import LibProgressBar from "../LibProgressBar/LibProgressBar.vue"
|
|
67
129
|
import type { LinkableByIdProps, TailwindClassProp } from "../shared/props.js"
|
|
68
130
|
|
|
69
131
|
defineOptions({
|
|
@@ -73,58 +135,18 @@ defineOptions({
|
|
|
73
135
|
|
|
74
136
|
const props = defineProps<Props>()
|
|
75
137
|
|
|
76
|
-
const
|
|
138
|
+
const topNotifications = computed(() => handler.queue.filter(entry => entry.requiresAction).reverse())
|
|
139
|
+
const notifications = computed(() => handler.queue.filter(entry => !entry.requiresAction))
|
|
77
140
|
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
dialogEl.value!.showModal()
|
|
85
|
-
isOpen.value = true
|
|
86
|
-
})
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
const close = () => {
|
|
90
|
-
if (isOpen.value && topNotifications.length === 0) {
|
|
91
|
-
dialogEl.value!.close()
|
|
92
|
-
isOpen.value = false
|
|
93
|
-
}
|
|
94
|
-
}
|
|
141
|
+
const time = ref(Date.now())
|
|
142
|
+
setInterval(() => {
|
|
143
|
+
requestAnimationFrame(() => {
|
|
144
|
+
time.value = Date.now()
|
|
145
|
+
})
|
|
146
|
+
}, 50)
|
|
95
147
|
|
|
96
|
-
const addNotification = (entry: NotificationEntry) => {
|
|
97
|
-
if (entry.resolution === undefined) {
|
|
98
|
-
if (entry.requiresAction) {
|
|
99
|
-
topNotifications.push(entry)
|
|
100
|
-
open()
|
|
101
|
-
entry.promise.then(() => {
|
|
102
|
-
removeIfIn(topNotifications, entry)
|
|
103
|
-
close()
|
|
104
|
-
})
|
|
105
|
-
} else {
|
|
106
|
-
notifications.splice(0, 0, entry)
|
|
107
|
-
entry.promise.then(() => {
|
|
108
|
-
removeIfIn(notifications, entry)
|
|
109
|
-
})
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const notificationListener = (entry: NotificationEntry, type: "added" | "resolved" | "deleted"): void => {
|
|
115
|
-
if (type === "added") {
|
|
116
|
-
addNotification(entry)
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
148
|
|
|
120
149
|
const handler = props.handler ?? useNotificationHandler()
|
|
121
|
-
|
|
122
|
-
handler.addNotificationListener(notificationListener)
|
|
123
|
-
|
|
124
|
-
for (const entry of handler.queue) { addNotification(entry) }
|
|
125
|
-
onBeforeUnmount(() => {
|
|
126
|
-
handler.removeNotificationListener(notificationListener)
|
|
127
|
-
})
|
|
128
150
|
</script>
|
|
129
151
|
|
|
130
152
|
<script lang="ts">
|