vue-toastflow 0.0.1

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.
@@ -0,0 +1,350 @@
1
+ <script setup lang="ts">
2
+ import { computed, onMounted, onUnmounted, ref, watch } from "vue";
3
+ import Toast from "./Toast.vue";
4
+ import type {
5
+ ToastAnimation,
6
+ ToastConfig,
7
+ ToastId,
8
+ ToastInstance,
9
+ ToastPosition,
10
+ ToastStore,
11
+ } from "toastflow-core";
12
+ import { getToastStore } from "../toast";
13
+
14
+ const positions: ToastPosition[] = [
15
+ "top-left",
16
+ "top-center",
17
+ "top-right",
18
+ "bottom-left",
19
+ "bottom-center",
20
+ "bottom-right",
21
+ ];
22
+
23
+ const store: ToastStore = getToastStore();
24
+
25
+ const toasts = ref<ToastInstance[]>([]);
26
+
27
+ // event-driven keys
28
+ const progressResetMap = ref<Record<ToastId, number>>({});
29
+ const duplicateMap = ref<Record<ToastId, number>>({});
30
+ const updateMap = ref<Record<ToastId, number>>({});
31
+
32
+ let unsubscribeState: (() => void) | null = null;
33
+ let unsubscribeEvents: (() => void) | null = null;
34
+
35
+ onMounted(function () {
36
+ unsubscribeState = store.subscribe(function (state) {
37
+ toasts.value = state.toasts;
38
+ });
39
+
40
+ unsubscribeEvents = store.subscribeEvents(function (event) {
41
+ if (event.kind === "timer-reset") {
42
+ progressResetMap.value[event.id] = Math.random();
43
+ }
44
+
45
+ if (event.kind === "duplicate") {
46
+ duplicateMap.value[event.id] = Math.random();
47
+ }
48
+
49
+ if (event.kind === "update") {
50
+ updateMap.value[event.id] = Math.random();
51
+ }
52
+ });
53
+ });
54
+
55
+ onUnmounted(function () {
56
+ if (unsubscribeState) {
57
+ unsubscribeState();
58
+ }
59
+ if (unsubscribeEvents) {
60
+ unsubscribeEvents();
61
+ }
62
+ });
63
+
64
+ function getProgressResetKey(id: ToastId): number {
65
+ return progressResetMap.value[id] ?? 0;
66
+ }
67
+
68
+ function getDuplicateKey(id: ToastId): number {
69
+ return duplicateMap.value[id] ?? 0;
70
+ }
71
+
72
+ function getUpdateKey(id: ToastId): number {
73
+ return updateMap.value[id] ?? 0;
74
+ }
75
+
76
+ const grouped = computed(function () {
77
+ const byPos: Record<ToastPosition, ToastInstance[]> = {
78
+ "top-left": [],
79
+ "top-center": [],
80
+ "top-right": [],
81
+ "bottom-left": [],
82
+ "bottom-center": [],
83
+ "bottom-right": [],
84
+ };
85
+
86
+ for (const toast of toasts.value) {
87
+ byPos[toast.position].push(toast);
88
+ }
89
+
90
+ return byPos;
91
+ });
92
+
93
+ const baseConfig: ToastConfig = store.getConfig();
94
+ const stackConfigs = ref<Record<ToastPosition, ToastConfig>>({
95
+ "top-left": { ...baseConfig, position: "top-left" },
96
+ "top-center": { ...baseConfig, position: "top-center" },
97
+ "top-right": { ...baseConfig, position: "top-right" },
98
+ "bottom-left": { ...baseConfig, position: "bottom-left" },
99
+ "bottom-center": { ...baseConfig, position: "bottom-center" },
100
+ "bottom-right": { ...baseConfig, position: "bottom-right" },
101
+ });
102
+
103
+ const globalZIndex = computed(function () {
104
+ if (!toasts.value.length) {
105
+ return baseConfig.zIndex;
106
+ }
107
+ return Math.max(...toasts.value.map((toast) => toast.zIndex));
108
+ });
109
+
110
+ function stackConfig(position: ToastPosition): ToastConfig {
111
+ return stackConfigs.value[position] ?? { ...baseConfig, position };
112
+ }
113
+
114
+ function stackStyle(position: ToastPosition): Record<string, string> {
115
+ const { offset, width } = stackConfig(position);
116
+ // Lock the stack width, so it doesn't collapse when leaving items get absolute-positioned
117
+ const style: Record<string, string> = { width, maxWidth: "100%" };
118
+
119
+ if (position.startsWith("top-")) {
120
+ style.top = offset;
121
+ }
122
+ if (position.startsWith("bottom-")) {
123
+ style.bottom = offset;
124
+ }
125
+
126
+ if (position.endsWith("left")) {
127
+ style.left = offset;
128
+ } else if (position.endsWith("right")) {
129
+ style.right = offset;
130
+ } else if (position.endsWith("center")) {
131
+ style.left = "50%";
132
+ style.transform = "translateX(-50%)";
133
+ }
134
+
135
+ return style;
136
+ }
137
+
138
+ function stackAlignClass(position: ToastPosition): string {
139
+ if (position.endsWith("left")) {
140
+ return "tf-toast-stack--left";
141
+ }
142
+ if (position.endsWith("center")) {
143
+ return "tf-toast-stack--center";
144
+ }
145
+ return "tf-toast-stack--right";
146
+ }
147
+
148
+ function stackAxisClass(position: ToastPosition): string | null {
149
+ if (position.startsWith("bottom-")) {
150
+ return "tf-toast-stack-inner--bottom";
151
+ }
152
+ return null;
153
+ }
154
+
155
+ function handleDismiss(id: ToastId) {
156
+ store.dismiss(id);
157
+ }
158
+
159
+ function beforeLeave(el: Element) {
160
+ const element = el as HTMLElement;
161
+ const parent = element.parentElement;
162
+ if (!parent || parent.children.length <= 1) {
163
+ return;
164
+ }
165
+
166
+ const top = element.offsetTop;
167
+ const parentHeight = parent.clientHeight;
168
+ const parentWidth = parent.clientWidth;
169
+
170
+ parent.style.minHeight = `${parentHeight}px`;
171
+ element.style.position = "absolute";
172
+ element.style.width = `${parentWidth}px`;
173
+ element.style.left = "0";
174
+ element.style.right = "0";
175
+ element.style.top = `${top}px`;
176
+ }
177
+
178
+ function afterLeave(el: Element) {
179
+ const element = el as HTMLElement;
180
+ const parent = element.parentElement;
181
+ if (parent) {
182
+ parent.style.minHeight = "";
183
+ }
184
+
185
+ element.style.position = "";
186
+ element.style.width = "";
187
+ element.style.left = "";
188
+ element.style.right = "";
189
+ element.style.top = "";
190
+ }
191
+
192
+ watch(
193
+ toasts,
194
+ function (current) {
195
+ const ids = new Set(
196
+ current.map(function (toast) {
197
+ return toast.id;
198
+ }),
199
+ );
200
+
201
+ for (const key of Object.keys(progressResetMap.value)) {
202
+ if (!ids.has(key as ToastId)) {
203
+ delete progressResetMap.value[key as ToastId];
204
+ }
205
+ }
206
+
207
+ for (const key of Object.keys(duplicateMap.value)) {
208
+ if (!ids.has(key as ToastId)) {
209
+ delete duplicateMap.value[key as ToastId];
210
+ }
211
+ }
212
+
213
+ for (const key of Object.keys(updateMap.value)) {
214
+ if (!ids.has(key as ToastId)) {
215
+ delete updateMap.value[key as ToastId];
216
+ }
217
+ }
218
+ },
219
+ { deep: false },
220
+ );
221
+
222
+ function animationForToast(toast: ToastInstance): Partial<ToastAnimation> {
223
+ return {
224
+ ...baseConfig.animation,
225
+ ...toast.animation,
226
+ };
227
+ }
228
+
229
+ watch(
230
+ grouped,
231
+ function (byPos) {
232
+ const next = { ...stackConfigs.value };
233
+
234
+ (Object.keys(byPos) as ToastPosition[]).forEach(function (position) {
235
+ const first = byPos[position][0];
236
+ if (first) {
237
+ next[position] = {
238
+ ...baseConfig,
239
+ ...first,
240
+ position,
241
+ animation: {
242
+ ...baseConfig.animation,
243
+ ...first.animation,
244
+ },
245
+ };
246
+ }
247
+ });
248
+
249
+ stackConfigs.value = next;
250
+ },
251
+ { deep: false },
252
+ );
253
+ </script>
254
+
255
+ <template>
256
+ <div class="tf-toast-root" :style="{ zIndex: globalZIndex }">
257
+ <div
258
+ v-for="position in positions"
259
+ :key="position"
260
+ class="tf-toast-stack"
261
+ :class="stackAlignClass(position)"
262
+ :style="stackStyle(position)"
263
+ >
264
+ <TransitionGroup
265
+ :name="stackConfig(position).animation.name"
266
+ @before-leave="beforeLeave"
267
+ @after-leave="afterLeave"
268
+ tag="div"
269
+ :class="['tf-toast-stack-inner', stackAxisClass(position)]"
270
+ :style="{ gap: stackConfig(position).gap }"
271
+ >
272
+ <div
273
+ v-for="toast in grouped[position]"
274
+ :key="toast.id"
275
+ class="tf-toast-item"
276
+ :style="{ width: toast.width, maxWidth: '100%' }"
277
+ :data-position="toast.position"
278
+ >
279
+ <slot
280
+ v-if="$slots.default"
281
+ :toast="toast"
282
+ :progressResetKey="getProgressResetKey(toast.id)"
283
+ :duplicateKey="getDuplicateKey(toast.id)"
284
+ :updateKey="getUpdateKey(toast.id)"
285
+ :bumpAnimationClass="animationForToast(toast).bump"
286
+ :clearAllAnimationClass="animationForToast(toast).clearAll"
287
+ :updateAnimationClass="animationForToast(toast).update"
288
+ :dismiss="handleDismiss"
289
+ />
290
+
291
+ <Toast
292
+ v-else
293
+ :toast="toast"
294
+ :progressResetKey="getProgressResetKey(toast.id)"
295
+ :duplicateKey="getDuplicateKey(toast.id)"
296
+ :updateKey="getUpdateKey(toast.id)"
297
+ :bumpAnimationClass="animationForToast(toast).bump"
298
+ :clearAllAnimationClass="animationForToast(toast).clearAll"
299
+ :updateAnimationClass="animationForToast(toast).update"
300
+ @dismiss="handleDismiss"
301
+ />
302
+ </div>
303
+ </TransitionGroup>
304
+ </div>
305
+ </div>
306
+ </template>
307
+
308
+ <style scoped>
309
+ .tf-toast-root {
310
+ pointer-events: none;
311
+ position: fixed;
312
+ inset: 0;
313
+ }
314
+
315
+ .tf-toast-stack {
316
+ position: absolute;
317
+ height: 100%;
318
+ display: flex;
319
+ align-items: flex-start;
320
+ }
321
+
322
+ .tf-toast-stack-inner {
323
+ display: flex;
324
+ flex-direction: column;
325
+ position: relative;
326
+ width: 100%;
327
+ }
328
+
329
+ .tf-toast-stack-inner--bottom {
330
+ min-height: 100%;
331
+ justify-content: flex-end;
332
+ }
333
+
334
+ .tf-toast-stack--left {
335
+ justify-content: flex-start;
336
+ }
337
+
338
+ .tf-toast-stack--center {
339
+ justify-content: center;
340
+ }
341
+
342
+ .tf-toast-stack--right {
343
+ justify-content: flex-end;
344
+ }
345
+
346
+ .tf-toast-item {
347
+ pointer-events: auto;
348
+ width: 100%;
349
+ }
350
+ </style>
@@ -0,0 +1,62 @@
1
+ <script setup lang="ts">
2
+ import type {ToastType} from "toastflow-core";
3
+
4
+ const {type} = defineProps<{
5
+ type: ToastType;
6
+ }>();
7
+ </script>
8
+
9
+ <template>
10
+ <div class="tf-toast-progress">
11
+ <div
12
+ class="tf-toast-progress-bar"
13
+ :class="`tf-toast-progress-bar--${type}`"
14
+ />
15
+ </div>
16
+ </template>
17
+
18
+ <style scoped>
19
+ .tf-toast-progress {
20
+ height: var(--tf-toast-progress-height);
21
+ width: 100%;
22
+ border-radius: 0 0 var(--tf-toast-radius) var(--tf-toast-radius);
23
+ background: var(--tf-toast-progress-bg);
24
+ }
25
+
26
+ .tf-toast-progress-bar {
27
+ height: 100%;
28
+ animation-name: tf-toast-progress;
29
+ animation-duration: var(--tf-toast-progress-duration, 5000ms);
30
+ animation-timing-function: linear;
31
+ animation-fill-mode: forwards;
32
+ }
33
+
34
+ .tf-toast-progress-bar--default {
35
+ background: var(--tf-toast-accent-default);
36
+ }
37
+
38
+ .tf-toast-progress-bar--success {
39
+ background: var(--tf-toast-accent-success);
40
+ }
41
+
42
+ .tf-toast-progress-bar--error {
43
+ background: var(--tf-toast-accent-error);
44
+ }
45
+
46
+ .tf-toast-progress-bar--warning {
47
+ background: var(--tf-toast-accent-warning);
48
+ }
49
+
50
+ .tf-toast-progress-bar--info {
51
+ background: var(--tf-toast-accent-info);
52
+ }
53
+
54
+ @keyframes tf-toast-progress {
55
+ from {
56
+ width: 100%;
57
+ }
58
+ to {
59
+ width: 0;
60
+ }
61
+ }
62
+ </style>
@@ -0,0 +1,13 @@
1
+ <template>
2
+ <svg
3
+ xmlns="http://www.w3.org/2000/svg"
4
+ viewBox="0 0 24 24"
5
+ fill="currentColor"
6
+ >
7
+ <path
8
+ fill-rule="evenodd"
9
+ d="M4.755 10.059a7.5 7.5 0 0 1 12.548-3.364l1.903 1.903h-3.183a.75.75 0 1 0 0 1.5h4.992a.75.75 0 0 0 .75-.75V4.356a.75.75 0 0 0-1.5 0v3.18l-1.9-1.9A9 9 0 0 0 3.306 9.67a.75.75 0 1 0 1.45.388Zm15.408 3.352a.75.75 0 0 0-.919.53 7.5 7.5 0 0 1-12.548 3.364l-1.902-1.903h3.183a.75.75 0 0 0 0-1.5H2.984a.75.75 0 0 0-.75.75v4.992a.75.75 0 0 0 1.5 0v-3.18l1.9 1.9a9 9 0 0 0 15.059-4.035.75.75 0 0 0-.53-.918Z"
10
+ clip-rule="evenodd"
11
+ />
12
+ </svg>
13
+ </template>
@@ -0,0 +1,13 @@
1
+ <template>
2
+ <svg
3
+ xmlns="http://www.w3.org/2000/svg"
4
+ viewBox="0 0 24 24"
5
+ fill="currentColor"
6
+ >
7
+ <path
8
+ fill-rule="evenodd"
9
+ d="M5.25 9a6.75 6.75 0 0 1 13.5 0v.75c0 2.123.8 4.057 2.118 5.52a.75.75 0 0 1-.297 1.206c-1.544.57-3.16.99-4.831 1.243a3.75 3.75 0 1 1-7.48 0 24.585 24.585 0 0 1-4.831-1.244.75.75 0 0 1-.298-1.205A8.217 8.217 0 0 0 5.25 9.75V9Zm4.502 8.9a2.25 2.25 0 1 0 4.496 0 25.057 25.057 0 0 1-4.496 0Z"
10
+ clip-rule="evenodd"
11
+ />
12
+ </svg>
13
+ </template>
@@ -0,0 +1,13 @@
1
+ <template>
2
+ <svg
3
+ xmlns="http://www.w3.org/2000/svg"
4
+ viewBox="0 0 24 24"
5
+ fill="currentColor"
6
+ >
7
+ <path
8
+ fill-rule="evenodd"
9
+ d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm13.36-1.814a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z"
10
+ clip-rule="evenodd"
11
+ />
12
+ </svg>
13
+ </template>
@@ -0,0 +1,13 @@
1
+ <template>
2
+ <svg
3
+ xmlns="http://www.w3.org/2000/svg"
4
+ viewBox="0 0 24 24"
5
+ fill="currentColor"
6
+ >
7
+ <path
8
+ fill-rule="evenodd"
9
+ d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm8.706-1.442c1.146-.573 2.437.463 2.126 1.706l-.709 2.836.042-.02a.75.75 0 0 1 .67 1.34l-.04.022c-1.147.573-2.438-.463-2.127-1.706l.71-2.836-.042.02a.75.75 0 1 1-.671-1.34l.041-.022ZM12 9a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z"
10
+ clip-rule="evenodd"
11
+ />
12
+ </svg>
13
+ </template>
@@ -0,0 +1,13 @@
1
+ <template>
2
+ <svg
3
+ xmlns="http://www.w3.org/2000/svg"
4
+ viewBox="0 0 24 24"
5
+ fill="currentColor"
6
+ >
7
+ <path
8
+ fill-rule="evenodd"
9
+ d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm11.378-3.917c-.89-.777-2.366-.777-3.255 0a.75.75 0 0 1-.988-1.129c1.454-1.272 3.776-1.272 5.23 0 1.513 1.324 1.513 3.518 0 4.842a3.75 3.75 0 0 1-.837.552c-.676.328-1.028.774-1.028 1.152v.75a.75.75 0 0 1-1.5 0v-.75c0-1.279 1.06-2.107 1.875-2.502.182-.088.351-.199.503-.331.83-.727.83-1.857 0-2.584ZM12 18a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z"
10
+ clip-rule="evenodd"
11
+ />
12
+ </svg>
13
+ </template>
@@ -0,0 +1,13 @@
1
+ <template>
2
+ <svg
3
+ xmlns="http://www.w3.org/2000/svg"
4
+ viewBox="0 0 24 24"
5
+ fill="currentColor"
6
+ >
7
+ <path
8
+ fill-rule="evenodd"
9
+ d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25Zm-1.72 6.97a.75.75 0 1 0-1.06 1.06L10.94 12l-1.72 1.72a.75.75 0 1 0 1.06 1.06L12 13.06l1.72 1.72a.75.75 0 1 0 1.06-1.06L13.06 12l1.72-1.72a.75.75 0 1 0-1.06-1.06L12 10.94l-1.72-1.72Z"
10
+ clip-rule="evenodd"
11
+ />
12
+ </svg>
13
+ </template>
@@ -0,0 +1,13 @@
1
+ <template>
2
+ <svg
3
+ xmlns="http://www.w3.org/2000/svg"
4
+ viewBox="0 0 24 24"
5
+ fill="currentColor"
6
+ >
7
+ <path
8
+ fill-rule="evenodd"
9
+ d="M5.47 5.47a.75.75 0 0 1 1.06 0L12 10.94l5.47-5.47a.75.75 0 1 1 1.06 1.06L13.06 12l5.47 5.47a.75.75 0 1 1-1.06 1.06L12 13.06l-5.47 5.47a.75.75 0 0 1-1.06-1.06L10.94 12 5.47 6.53a.75.75 0 0 1 0-1.06Z"
10
+ clip-rule="evenodd"
11
+ />
12
+ </svg>
13
+ </template>
package/src/index.ts ADDED
@@ -0,0 +1,17 @@
1
+ import "./styles.css";
2
+
3
+ export * from "toastflow-core";
4
+ export * from "./plugin";
5
+ export * from "./toast";
6
+
7
+ export { default as ToastContainer } from "./components/ToastContainer.vue";
8
+ export { default as Toast } from "./components/Toast.vue";
9
+ export { default as ToastProgress } from "./components/ToastProgress.vue";
10
+
11
+ export { default as ArrowPath } from "./components/icons/ArrowPath.vue";
12
+ export { default as Bell } from "./components/icons/Bell.vue";
13
+ export { default as CheckCircle } from "./components/icons/CheckCircle.vue";
14
+ export { default as InfoCircle } from "./components/icons/InfoCircle.vue";
15
+ export { default as QuestionMarkCircle } from "./components/icons/QuestionMarkCircle.vue";
16
+ export { default as XCircle } from "./components/icons/XCircle.vue";
17
+ export { default as XMark } from "./components/icons/XMark.vue";
package/src/plugin.ts ADDED
@@ -0,0 +1,16 @@
1
+ import type { App, Plugin } from "vue";
2
+ import { createToastStore, type ToastConfig } from "toastflow-core";
3
+ import { toastStoreKey } from "./symbols";
4
+ import { setToastStore } from "./toast";
5
+
6
+ export function createToastflow(config: Partial<ToastConfig> = {}): Plugin {
7
+ const store = createToastStore(config);
8
+
9
+ setToastStore(store);
10
+
11
+ return {
12
+ install(app: App) {
13
+ app.provide(toastStoreKey, store);
14
+ },
15
+ };
16
+ }