@soft-toast/vue 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -6
- package/dist/animations/gsapConfig.d.ts.map +1 -1
- package/dist/index.js +1130 -877
- package/dist/plugin.d.ts.map +1 -1
- package/dist/stores/toastStore.d.ts.map +1 -1
- package/dist/style.css +1 -1
- package/dist/types/index.d.ts +3 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/animations/gsapConfig.ts +11 -8
- package/src/components/ToastContainer.vue +2 -1
- package/src/components/ToastItem.vue +258 -63
- package/src/components/ToastRegion.vue +360 -87
- package/src/plugin.ts +8 -6
- package/src/stores/toastStore.ts +43 -13
- package/src/styles/toast.css +36 -6
- package/src/types/index.ts +3 -1
package/src/stores/toastStore.ts
CHANGED
|
@@ -26,6 +26,9 @@ const defaultOptions: Required<Pick<
|
|
|
26
26
|
|
|
27
27
|
const toasts = ref<Toast[]>([])
|
|
28
28
|
|
|
29
|
+
// Non-reactive timer state — lives outside Vue reactivity to avoid per-frame re-renders
|
|
30
|
+
const timerMap = new Map<string, { remainingTime: number; isPaused: boolean }>()
|
|
31
|
+
|
|
29
32
|
// ─── Computed ─────────────────────────────────────────────────────────────────
|
|
30
33
|
|
|
31
34
|
const getToastsByPosition = (position: ToastPosition) =>
|
|
@@ -65,12 +68,13 @@ const add = (options: ToastOptions): string => {
|
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
// ── New toast ──
|
|
71
|
+
const duration = options.duration ?? defaultOptions.duration
|
|
68
72
|
const toast: Toast = {
|
|
69
73
|
...defaultOptions,
|
|
70
74
|
...options,
|
|
71
75
|
id,
|
|
72
76
|
createdAt: Date.now(),
|
|
73
|
-
remainingTime:
|
|
77
|
+
remainingTime: duration,
|
|
74
78
|
isPaused: false,
|
|
75
79
|
isExpanded: true,
|
|
76
80
|
isLeaving: false,
|
|
@@ -81,6 +85,9 @@ const add = (options: ToastOptions): string => {
|
|
|
81
85
|
showProgress: options.showProgress ?? defaultOptions.showProgress,
|
|
82
86
|
}
|
|
83
87
|
|
|
88
|
+
// Register in non-reactive timer map
|
|
89
|
+
timerMap.set(id, { remainingTime: duration, isPaused: false })
|
|
90
|
+
|
|
84
91
|
toasts.value.unshift(toast)
|
|
85
92
|
startTickLoop()
|
|
86
93
|
|
|
@@ -102,6 +109,7 @@ const update = (id: string, options: Partial<ToastOptions>) => {
|
|
|
102
109
|
const dismiss = (id?: string | { type?: ToastType | ToastType[] }) => {
|
|
103
110
|
if (!id) {
|
|
104
111
|
toasts.value.forEach((t) => { t.isLeaving = true })
|
|
112
|
+
timerMap.clear()
|
|
105
113
|
setTimeout(() => { toasts.value = [] }, 400)
|
|
106
114
|
return
|
|
107
115
|
}
|
|
@@ -111,6 +119,7 @@ const dismiss = (id?: string | { type?: ToastType | ToastType[] }) => {
|
|
|
111
119
|
if (toast) {
|
|
112
120
|
toast.isLeaving = true
|
|
113
121
|
toast.onDismiss?.(id)
|
|
122
|
+
timerMap.delete(id)
|
|
114
123
|
setTimeout(() => {
|
|
115
124
|
toasts.value = toasts.value.filter((t) => t.id !== id)
|
|
116
125
|
}, 400)
|
|
@@ -121,6 +130,7 @@ const dismiss = (id?: string | { type?: ToastType | ToastType[] }) => {
|
|
|
121
130
|
if (types.includes(t.type)) {
|
|
122
131
|
t.isLeaving = true
|
|
123
132
|
t.onDismiss?.(t.id)
|
|
133
|
+
timerMap.delete(t.id)
|
|
124
134
|
}
|
|
125
135
|
})
|
|
126
136
|
setTimeout(() => {
|
|
@@ -130,11 +140,15 @@ const dismiss = (id?: string | { type?: ToastType | ToastType[] }) => {
|
|
|
130
140
|
}
|
|
131
141
|
|
|
132
142
|
const pause = (id: string) => {
|
|
143
|
+
const timer = timerMap.get(id)
|
|
144
|
+
if (timer) timer.isPaused = true
|
|
133
145
|
const toast = toasts.value.find((t) => t.id === id)
|
|
134
146
|
if (toast) toast.isPaused = true
|
|
135
147
|
}
|
|
136
148
|
|
|
137
149
|
const resume = (id: string) => {
|
|
150
|
+
const timer = timerMap.get(id)
|
|
151
|
+
if (timer) timer.isPaused = false
|
|
138
152
|
const toast = toasts.value.find((t) => t.id === id)
|
|
139
153
|
if (toast) toast.isPaused = false
|
|
140
154
|
}
|
|
@@ -163,19 +177,31 @@ const startTickLoop = () => {
|
|
|
163
177
|
const delta = currentTime - lastTime
|
|
164
178
|
lastTime = currentTime
|
|
165
179
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
180
|
+
// Tick non-reactive timer map — zero Vue reactivity cost per frame
|
|
181
|
+
const expiredIds: string[] = []
|
|
182
|
+
timerMap.forEach((timer, id) => {
|
|
183
|
+
if (timer.isPaused) return
|
|
184
|
+
const toast = toasts.value.find((t) => t.id === id)
|
|
185
|
+
if (!toast || toast.isLeaving || toast.duration === Infinity) return
|
|
186
|
+
timer.remainingTime -= delta
|
|
187
|
+
// Sync remainingTime back to toast only for progress bar consumers
|
|
188
|
+
if (toast.showProgress) toast.remainingTime = timer.remainingTime
|
|
189
|
+
if (timer.remainingTime <= 0) expiredIds.push(id)
|
|
177
190
|
})
|
|
178
191
|
|
|
192
|
+
// Mutate Vue state only when a toast actually expires
|
|
193
|
+
for (const id of expiredIds) {
|
|
194
|
+
timerMap.delete(id)
|
|
195
|
+
const toast = toasts.value.find((t) => t.id === id)
|
|
196
|
+
if (toast && !toast.isLeaving) {
|
|
197
|
+
toast.isLeaving = true
|
|
198
|
+
toast.onAutoClose?.(id)
|
|
199
|
+
setTimeout(() => {
|
|
200
|
+
toasts.value = toasts.value.filter((t) => t.id !== id)
|
|
201
|
+
}, 400)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
179
205
|
if (toasts.value.length > 0) {
|
|
180
206
|
rafId = requestAnimationFrame(loop)
|
|
181
207
|
} else {
|
|
@@ -237,9 +263,13 @@ const promise = async <T>(
|
|
|
237
263
|
}
|
|
238
264
|
}
|
|
239
265
|
|
|
240
|
-
const clearAll = () => {
|
|
266
|
+
const clearAll = () => {
|
|
267
|
+
timerMap.clear()
|
|
268
|
+
toasts.value = []
|
|
269
|
+
}
|
|
241
270
|
|
|
242
271
|
const remove = (id: string) => {
|
|
272
|
+
timerMap.delete(id)
|
|
243
273
|
toasts.value = toasts.value.filter((t) => t.id !== id)
|
|
244
274
|
}
|
|
245
275
|
|
package/src/styles/toast.css
CHANGED
|
@@ -28,7 +28,6 @@
|
|
|
28
28
|
.soft-toast-container[data-position="bottom"] { bottom: 32px; left: 50%; transform: translateX(-50%); }
|
|
29
29
|
.soft-toast-container[data-position="left"] { top: 50%; left: 32px; transform: translateY(-50%); }
|
|
30
30
|
.soft-toast-container[data-position="right"] { top: 50%; right: 32px; transform: translateY(-50%); }
|
|
31
|
-
.soft-toast-container[data-position="center"] { top: 50%; left: 50%; transform: translate(-50%, -50%); }
|
|
32
31
|
|
|
33
32
|
/* ---- Toast Item ---- */
|
|
34
33
|
.soft-toast-item {
|
|
@@ -45,8 +44,7 @@
|
|
|
45
44
|
line-height: 1.45;
|
|
46
45
|
cursor: default;
|
|
47
46
|
user-select: none;
|
|
48
|
-
|
|
49
|
-
touch-action: pan-y; /* allow vertical scroll, intercept horizontal for swipe */
|
|
47
|
+
touch-action: pan-y;
|
|
50
48
|
|
|
51
49
|
/* Clean modern white card */
|
|
52
50
|
background: rgba(255, 255, 255, 0.98);
|
|
@@ -56,6 +54,11 @@
|
|
|
56
54
|
inset 0 1px 0 rgba(255, 255, 255, 1);
|
|
57
55
|
}
|
|
58
56
|
|
|
57
|
+
.soft-toast-item[data-interactive="false"] {
|
|
58
|
+
pointer-events: none;
|
|
59
|
+
touch-action: none;
|
|
60
|
+
}
|
|
61
|
+
|
|
59
62
|
/* Show grab cursor on swipeable toasts to hint at the gesture */
|
|
60
63
|
.soft-toast-item--swipeable {
|
|
61
64
|
cursor: grab;
|
|
@@ -101,6 +104,8 @@
|
|
|
101
104
|
|
|
102
105
|
/* ---- Content layout ---- */
|
|
103
106
|
.soft-toast-content {
|
|
107
|
+
position: relative;
|
|
108
|
+
z-index: 1;
|
|
104
109
|
display: flex;
|
|
105
110
|
align-items: flex-start;
|
|
106
111
|
gap: 11px;
|
|
@@ -111,7 +116,7 @@
|
|
|
111
116
|
flex-shrink: 0;
|
|
112
117
|
width: 24px;
|
|
113
118
|
height: 24px;
|
|
114
|
-
margin-top:
|
|
119
|
+
margin-top: 0;
|
|
115
120
|
display: flex;
|
|
116
121
|
align-items: center;
|
|
117
122
|
justify-content: center;
|
|
@@ -155,6 +160,7 @@
|
|
|
155
160
|
display: flex;
|
|
156
161
|
align-items: center;
|
|
157
162
|
gap: 12px;
|
|
163
|
+
min-height: 24px;
|
|
158
164
|
}
|
|
159
165
|
|
|
160
166
|
.soft-toast-title {
|
|
@@ -162,8 +168,8 @@
|
|
|
162
168
|
font-size: 14px;
|
|
163
169
|
color: #111827;
|
|
164
170
|
margin: 0;
|
|
165
|
-
line-height: 1.
|
|
166
|
-
letter-spacing:
|
|
171
|
+
line-height: 1.3;
|
|
172
|
+
letter-spacing: 0;
|
|
167
173
|
/* Slight right padding by default so text doesn't bump against close button on hover */
|
|
168
174
|
padding-right: 4px;
|
|
169
175
|
}
|
|
@@ -217,6 +223,7 @@
|
|
|
217
223
|
width: 100%;
|
|
218
224
|
padding: 8px 16px;
|
|
219
225
|
border-radius: 12px;
|
|
226
|
+
background: rgba(100, 116, 139, 0.12); /* fallback for Safari < 16.2 */
|
|
220
227
|
background: color-mix(in srgb, var(--st-accent) 15%, transparent);
|
|
221
228
|
color: var(--st-icon-color);
|
|
222
229
|
border: none;
|
|
@@ -229,6 +236,7 @@
|
|
|
229
236
|
text-align: center;
|
|
230
237
|
}
|
|
231
238
|
.soft-toast-action-button:hover {
|
|
239
|
+
background: rgba(100, 116, 139, 0.22); /* fallback for Safari < 16.2 */
|
|
232
240
|
background: color-mix(in srgb, var(--st-accent) 25%, transparent);
|
|
233
241
|
transform: scale(1.02);
|
|
234
242
|
}
|
|
@@ -247,6 +255,7 @@
|
|
|
247
255
|
/* Close Button */
|
|
248
256
|
.soft-toast-close {
|
|
249
257
|
position: absolute;
|
|
258
|
+
z-index: 3;
|
|
250
259
|
top: 10px;
|
|
251
260
|
right: 10px;
|
|
252
261
|
width: 22px;
|
|
@@ -269,6 +278,10 @@
|
|
|
269
278
|
transform: scale(1.1);
|
|
270
279
|
}
|
|
271
280
|
.soft-toast-item:hover .soft-toast-close { opacity: 1; }
|
|
281
|
+
/* iOS/touch: no hover state — always show close button */
|
|
282
|
+
@media (hover: none) {
|
|
283
|
+
.soft-toast-close { opacity: 0.7; }
|
|
284
|
+
}
|
|
272
285
|
.soft-toast-close[data-position="top-left"] { right: auto; left: 10px; }
|
|
273
286
|
|
|
274
287
|
/* Morph X to Minus on Hover */
|
|
@@ -344,6 +357,23 @@
|
|
|
344
357
|
.soft-toast-close { opacity: 1; }
|
|
345
358
|
}
|
|
346
359
|
|
|
360
|
+
/* ---- Collapsed stack: only front toast is touchable on mobile ---- */
|
|
361
|
+
/* Prevents hidden stacked toasts from intercepting touch/scroll on the page */
|
|
362
|
+
@media (hover: none) {
|
|
363
|
+
.soft-toast-container:not([data-expanded="true"]) .soft-toast-item[data-interactive="false"] {
|
|
364
|
+
pointer-events: none;
|
|
365
|
+
touch-action: none;
|
|
366
|
+
}
|
|
367
|
+
/* Front toast in collapsed stack: no swipe gesture — tap to expand only */
|
|
368
|
+
.soft-toast-container:not([data-expanded="true"]) .soft-toast-item[data-st-index="0"] {
|
|
369
|
+
touch-action: manipulation;
|
|
370
|
+
}
|
|
371
|
+
/* Expanded: all toasts can swipe */
|
|
372
|
+
.soft-toast-container[data-expanded="true"] .soft-toast-item[data-interactive="true"] {
|
|
373
|
+
touch-action: pan-y;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
347
377
|
/* ---- Reduced Motion ---- */
|
|
348
378
|
@media (prefers-reduced-motion: reduce) {
|
|
349
379
|
.soft-toast-item,
|
package/src/types/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { VNode, Component } from 'vue'
|
|
2
2
|
|
|
3
3
|
export type ToastType = 'default' | 'success' | 'error' | 'warning' | 'info' | 'promise'
|
|
4
|
-
export type ToastPosition = 'top' | 'bottom' | 'left' | 'right' | '
|
|
4
|
+
export type ToastPosition = 'top' | 'bottom' | 'left' | 'right' | 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'
|
|
5
5
|
export type AnimationPreset = 'smooth' | 'bouncy' | 'subtle' | 'snappy'
|
|
6
6
|
export type QueueOverflow = 'drop-oldest' | 'drop-newest'
|
|
7
7
|
|
|
@@ -108,8 +108,10 @@ export interface ToastContainerProps {
|
|
|
108
108
|
queueOverflow?: QueueOverflow
|
|
109
109
|
dir?: 'ltr' | 'rtl'
|
|
110
110
|
swipeToDismiss?: boolean
|
|
111
|
+
slotFilter?: (toast: Toast) => boolean
|
|
111
112
|
}
|
|
112
113
|
|
|
113
114
|
export interface ToastPluginOptions extends ToastContainerProps {
|
|
114
115
|
teleportTarget?: string
|
|
116
|
+
autoMount?: boolean
|
|
115
117
|
}
|