@soft-toast/vue 1.0.1 → 1.1.0
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 +49 -14
- package/dist/animations/gsapConfig.d.ts +2 -1
- package/dist/animations/gsapConfig.d.ts.map +1 -1
- package/dist/composables/{useToast.d.ts → useSoftToast.d.ts} +6 -6
- package/dist/composables/useSoftToast.d.ts.map +1 -0
- package/dist/composables/useSoftToast.test.d.ts +2 -0
- package/dist/composables/useSoftToast.test.d.ts.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1139 -848
- package/dist/plugin.d.ts.map +1 -1
- package/dist/stores/toastStore.d.ts +1 -1
- package/dist/stores/toastStore.d.ts.map +1 -1
- package/dist/style.css +1 -1
- package/dist/types/index.d.ts +8 -6
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +15 -2
- package/src/animations/gsapConfig.ts +53 -19
- package/src/components/ToastContainer.vue +2 -1
- package/src/components/ToastItem.vue +291 -65
- package/src/components/ToastRegion.vue +364 -87
- package/src/composables/useFlash.ts +1 -1
- package/src/composables/{useToast.test.ts → useSoftToast.test.ts} +54 -54
- package/src/composables/{useToast.ts → useSoftToast.ts} +5 -5
- package/src/exports.test.ts +39 -22
- package/src/index.ts +1 -2
- package/src/plugin.ts +8 -6
- package/src/stores/toastStore.ts +71 -21
- package/src/styles/toast.css +52 -13
- package/src/types/index.ts +8 -6
- package/dist/composables/useToast.d.ts.map +0 -1
- package/dist/composables/useToast.test.d.ts +0 -2
- package/dist/composables/useToast.test.d.ts.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from 'bun:test'
|
|
2
|
-
import {
|
|
2
|
+
import { useSoftToast, softToast } from './useSoftToast'
|
|
3
3
|
import { toastStore } from '../stores/toastStore'
|
|
4
4
|
|
|
5
5
|
// Reset store state before each test
|
|
@@ -7,11 +7,11 @@ beforeEach(() => {
|
|
|
7
7
|
toastStore.clearAll()
|
|
8
8
|
})
|
|
9
9
|
|
|
10
|
-
// ───
|
|
10
|
+
// ─── useSoftToast() composable ────────────────────────────────────────────────────
|
|
11
11
|
|
|
12
|
-
describe('
|
|
12
|
+
describe('useSoftToast()', () => {
|
|
13
13
|
it('returns an object with all API methods', () => {
|
|
14
|
-
const t =
|
|
14
|
+
const t = useSoftToast()
|
|
15
15
|
expect(typeof t.default).toBe('function')
|
|
16
16
|
expect(typeof t.success).toBe('function')
|
|
17
17
|
expect(typeof t.error).toBe('function')
|
|
@@ -27,7 +27,7 @@ describe('useToast()', () => {
|
|
|
27
27
|
})
|
|
28
28
|
|
|
29
29
|
it('default() adds a toast with type "default"', () => {
|
|
30
|
-
const t =
|
|
30
|
+
const t = useSoftToast()
|
|
31
31
|
const id = t.default('Hello')
|
|
32
32
|
const toasts = toastStore.toasts.value
|
|
33
33
|
expect(toasts.length).toBe(1)
|
|
@@ -37,32 +37,32 @@ describe('useToast()', () => {
|
|
|
37
37
|
})
|
|
38
38
|
|
|
39
39
|
it('success() adds a toast with type "success"', () => {
|
|
40
|
-
const t =
|
|
40
|
+
const t = useSoftToast()
|
|
41
41
|
t.success('Done!')
|
|
42
42
|
expect(toastStore.toasts.value[0].type).toBe('success')
|
|
43
43
|
expect(toastStore.toasts.value[0].title).toBe('Done!')
|
|
44
44
|
})
|
|
45
45
|
|
|
46
46
|
it('error() adds a toast with type "error"', () => {
|
|
47
|
-
const t =
|
|
47
|
+
const t = useSoftToast()
|
|
48
48
|
t.error('Oops!')
|
|
49
49
|
expect(toastStore.toasts.value[0].type).toBe('error')
|
|
50
50
|
})
|
|
51
51
|
|
|
52
52
|
it('warning() adds a toast with type "warning"', () => {
|
|
53
|
-
const t =
|
|
53
|
+
const t = useSoftToast()
|
|
54
54
|
t.warning('Watch out!')
|
|
55
55
|
expect(toastStore.toasts.value[0].type).toBe('warning')
|
|
56
56
|
})
|
|
57
57
|
|
|
58
58
|
it('info() adds a toast with type "info"', () => {
|
|
59
|
-
const t =
|
|
59
|
+
const t = useSoftToast()
|
|
60
60
|
t.info('FYI')
|
|
61
61
|
expect(toastStore.toasts.value[0].type).toBe('info')
|
|
62
62
|
})
|
|
63
63
|
|
|
64
64
|
it('custom() passes options through directly', () => {
|
|
65
|
-
const t =
|
|
65
|
+
const t = useSoftToast()
|
|
66
66
|
t.custom({ type: 'success', title: 'Custom', description: 'My desc', duration: 9999 })
|
|
67
67
|
const added = toastStore.toasts.value[0]
|
|
68
68
|
expect(added.title).toBe('Custom')
|
|
@@ -71,7 +71,7 @@ describe('useToast()', () => {
|
|
|
71
71
|
})
|
|
72
72
|
|
|
73
73
|
it('update() changes an existing toast', () => {
|
|
74
|
-
const t =
|
|
74
|
+
const t = useSoftToast()
|
|
75
75
|
const id = t.default('Before')
|
|
76
76
|
t.update(id, { title: 'After', type: 'success' })
|
|
77
77
|
const updated = toastStore.toasts.value.find(x => x.id === id)
|
|
@@ -80,7 +80,7 @@ describe('useToast()', () => {
|
|
|
80
80
|
})
|
|
81
81
|
|
|
82
82
|
it('dismiss() marks toast as isLeaving', () => {
|
|
83
|
-
const t =
|
|
83
|
+
const t = useSoftToast()
|
|
84
84
|
const id = t.default('Bye')
|
|
85
85
|
t.dismiss(id)
|
|
86
86
|
const toast = toastStore.toasts.value.find(x => x.id === id)
|
|
@@ -88,7 +88,7 @@ describe('useToast()', () => {
|
|
|
88
88
|
})
|
|
89
89
|
|
|
90
90
|
it('pause() and resume() toggle isPaused', () => {
|
|
91
|
-
const t =
|
|
91
|
+
const t = useSoftToast()
|
|
92
92
|
const id = t.default('Pausing')
|
|
93
93
|
t.pause(id)
|
|
94
94
|
expect(toastStore.toasts.value[0].isPaused).toBe(true)
|
|
@@ -97,7 +97,7 @@ describe('useToast()', () => {
|
|
|
97
97
|
})
|
|
98
98
|
|
|
99
99
|
it('forward options like duration, description, position', () => {
|
|
100
|
-
const t =
|
|
100
|
+
const t = useSoftToast()
|
|
101
101
|
t.success('With opts', {
|
|
102
102
|
description: 'More info',
|
|
103
103
|
duration: 1234,
|
|
@@ -110,75 +110,75 @@ describe('useToast()', () => {
|
|
|
110
110
|
})
|
|
111
111
|
})
|
|
112
112
|
|
|
113
|
-
// ───
|
|
113
|
+
// ─── softToast (static object) ────────────────────────────────────────────────
|
|
114
114
|
|
|
115
|
-
describe('
|
|
116
|
-
it('
|
|
117
|
-
|
|
115
|
+
describe('softToast (static API)', () => {
|
|
116
|
+
it('softToast.success() adds a success toast', () => {
|
|
117
|
+
softToast.success('Static success')
|
|
118
118
|
expect(toastStore.toasts.value[0].type).toBe('success')
|
|
119
119
|
expect(toastStore.toasts.value[0].title).toBe('Static success')
|
|
120
120
|
})
|
|
121
121
|
|
|
122
|
-
it('
|
|
123
|
-
|
|
122
|
+
it('softToast.error() adds an error toast', () => {
|
|
123
|
+
softToast.error('Static error')
|
|
124
124
|
expect(toastStore.toasts.value[0].type).toBe('error')
|
|
125
125
|
})
|
|
126
126
|
|
|
127
|
-
it('
|
|
128
|
-
|
|
127
|
+
it('softToast.warning() adds a warning toast', () => {
|
|
128
|
+
softToast.warning('Careful')
|
|
129
129
|
expect(toastStore.toasts.value[0].type).toBe('warning')
|
|
130
130
|
})
|
|
131
131
|
|
|
132
|
-
it('
|
|
133
|
-
|
|
132
|
+
it('softToast.info() adds an info toast', () => {
|
|
133
|
+
softToast.info('Just so you know')
|
|
134
134
|
expect(toastStore.toasts.value[0].type).toBe('info')
|
|
135
135
|
})
|
|
136
136
|
|
|
137
|
-
it('
|
|
138
|
-
|
|
137
|
+
it('softToast.default() adds a default toast', () => {
|
|
138
|
+
softToast.default('Hello world')
|
|
139
139
|
expect(toastStore.toasts.value[0].type).toBe('default')
|
|
140
140
|
})
|
|
141
141
|
|
|
142
|
-
it('
|
|
143
|
-
|
|
142
|
+
it('softToast.custom() adds toast with arbitrary options', () => {
|
|
143
|
+
softToast.custom({ title: 'Custom static', type: 'info', id: 'static-1' })
|
|
144
144
|
expect(toastStore.toasts.value[0].id).toBe('static-1')
|
|
145
145
|
expect(toastStore.toasts.value[0].title).toBe('Custom static')
|
|
146
146
|
})
|
|
147
147
|
|
|
148
|
-
it('
|
|
149
|
-
const id =
|
|
150
|
-
|
|
148
|
+
it('softToast.update() modifies a toast', () => {
|
|
149
|
+
const id = softToast.default('Initial')
|
|
150
|
+
softToast.update(id, { title: 'Updated' })
|
|
151
151
|
expect(toastStore.toasts.value.find(x => x.id === id)?.title).toBe('Updated')
|
|
152
152
|
})
|
|
153
153
|
|
|
154
|
-
it('
|
|
155
|
-
const id =
|
|
156
|
-
|
|
154
|
+
it('softToast.pause() and softToast.resume() work', () => {
|
|
155
|
+
const id = softToast.info('Pausable')
|
|
156
|
+
softToast.pause(id)
|
|
157
157
|
expect(toastStore.toasts.value[0].isPaused).toBe(true)
|
|
158
|
-
|
|
158
|
+
softToast.resume(id)
|
|
159
159
|
expect(toastStore.toasts.value[0].isPaused).toBe(false)
|
|
160
160
|
})
|
|
161
161
|
|
|
162
|
-
it('
|
|
163
|
-
const id =
|
|
164
|
-
|
|
162
|
+
it('softToast.dismiss() marks toast as leaving', () => {
|
|
163
|
+
const id = softToast.success('Going away')
|
|
164
|
+
softToast.dismiss(id)
|
|
165
165
|
expect(toastStore.toasts.value.find(x => x.id === id)?.isLeaving).toBe(true)
|
|
166
166
|
})
|
|
167
167
|
|
|
168
168
|
it('multiple toasts are ordered newest-first', () => {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
169
|
+
softToast.default('First')
|
|
170
|
+
softToast.success('Second')
|
|
171
|
+
softToast.error('Third')
|
|
172
172
|
const titles = toastStore.toasts.value.map(t => t.title)
|
|
173
173
|
expect(titles).toEqual(['Third', 'Second', 'First'])
|
|
174
174
|
})
|
|
175
175
|
})
|
|
176
176
|
|
|
177
|
-
// ───
|
|
177
|
+
// ─── softToast.promise() ─────────────────────────────────────────────────────────
|
|
178
178
|
|
|
179
|
-
describe('
|
|
179
|
+
describe('softToast.promise()', () => {
|
|
180
180
|
it('resolves: updates toast to success', async () => {
|
|
181
|
-
const id = await
|
|
181
|
+
const id = await softToast.promise(
|
|
182
182
|
Promise.resolve('ok'),
|
|
183
183
|
{ loading: 'Loading…', success: 'Done!', error: 'Failed' }
|
|
184
184
|
)
|
|
@@ -191,7 +191,7 @@ describe('toast.promise()', () => {
|
|
|
191
191
|
it('rejects: updates toast to error and re-throws', async () => {
|
|
192
192
|
let caught: unknown
|
|
193
193
|
try {
|
|
194
|
-
await
|
|
194
|
+
await softToast.promise(
|
|
195
195
|
Promise.reject(new Error('boom')),
|
|
196
196
|
{ loading: 'Loading…', success: 'Done!', error: 'Something failed' }
|
|
197
197
|
)
|
|
@@ -206,25 +206,25 @@ describe('toast.promise()', () => {
|
|
|
206
206
|
|
|
207
207
|
// ─── Flash & sound API surface ────────────────────────────────────────────────
|
|
208
208
|
|
|
209
|
-
describe('
|
|
209
|
+
describe('useSoftToast() — flash API methods', () => {
|
|
210
210
|
it('returns flash, showFlashes, hasFlashes methods', () => {
|
|
211
|
-
const t =
|
|
211
|
+
const t = useSoftToast()
|
|
212
212
|
expect(typeof t.flash).toBe('function')
|
|
213
213
|
expect(typeof t.showFlashes).toBe('function')
|
|
214
214
|
expect(typeof t.hasFlashes).toBe('function')
|
|
215
215
|
})
|
|
216
216
|
})
|
|
217
217
|
|
|
218
|
-
describe('
|
|
219
|
-
it('
|
|
220
|
-
expect(typeof
|
|
218
|
+
describe('softToast (static) — flash API methods', () => {
|
|
219
|
+
it('softToast.flash is a function', () => {
|
|
220
|
+
expect(typeof softToast.flash).toBe('function')
|
|
221
221
|
})
|
|
222
222
|
|
|
223
|
-
it('
|
|
224
|
-
expect(typeof
|
|
223
|
+
it('softToast.showFlashes is a function', () => {
|
|
224
|
+
expect(typeof softToast.showFlashes).toBe('function')
|
|
225
225
|
})
|
|
226
226
|
|
|
227
|
-
it('
|
|
228
|
-
expect(typeof
|
|
227
|
+
it('softToast.hasFlashes is a function', () => {
|
|
228
|
+
expect(typeof softToast.hasFlashes).toBe('function')
|
|
229
229
|
})
|
|
230
230
|
})
|
|
@@ -4,7 +4,7 @@ import { queueFlash, consumeFlashes, hasPendingFlashes } from './useFlash'
|
|
|
4
4
|
|
|
5
5
|
// ─── Composable API ───────────────────────────────────────────────────────────
|
|
6
6
|
|
|
7
|
-
export const
|
|
7
|
+
export const useSoftToast = () => ({
|
|
8
8
|
default: (title: string, options?: Omit<ToastOptions, 'type' | 'title'>) =>
|
|
9
9
|
toastStore.add({ ...options, type: 'default', title }),
|
|
10
10
|
|
|
@@ -25,7 +25,7 @@ export const useToast = () => ({
|
|
|
25
25
|
|
|
26
26
|
promise: <T>(
|
|
27
27
|
promiseFn: Promise<T>,
|
|
28
|
-
messages: ToastPromiseMessages
|
|
28
|
+
messages: ToastPromiseMessages<T>,
|
|
29
29
|
options?: Omit<ToastOptions, 'type' | 'promise' | 'promiseMessages'>
|
|
30
30
|
): Promise<T> => toastStore.promise(promiseFn, messages, options),
|
|
31
31
|
|
|
@@ -44,7 +44,7 @@ export const useToast = () => ({
|
|
|
44
44
|
* Perfect for the "submit → redirect → show success" pattern.
|
|
45
45
|
*
|
|
46
46
|
* @example
|
|
47
|
-
* const { flash } =
|
|
47
|
+
* const { flash } = useSoftToast()
|
|
48
48
|
* await api.save()
|
|
49
49
|
* flash('Saved!', { type: 'success' })
|
|
50
50
|
* router.push('/dashboard')
|
|
@@ -64,7 +64,7 @@ export const useToast = () => ({
|
|
|
64
64
|
|
|
65
65
|
// ─── Singleton API (usable outside components) ────────────────────────────────
|
|
66
66
|
|
|
67
|
-
export const
|
|
67
|
+
export const softToast = {
|
|
68
68
|
default: (title: string, options?: Omit<ToastOptions, 'type' | 'title'>) =>
|
|
69
69
|
toastStore.add({ ...options, type: 'default', title }),
|
|
70
70
|
success: (title: string, options?: Omit<ToastOptions, 'type' | 'title'>) =>
|
|
@@ -79,7 +79,7 @@ export const toast = {
|
|
|
79
79
|
toastStore.loading(title, options),
|
|
80
80
|
promise: <T>(
|
|
81
81
|
promiseFn: Promise<T>,
|
|
82
|
-
messages: ToastPromiseMessages
|
|
82
|
+
messages: ToastPromiseMessages<T>,
|
|
83
83
|
options?: Omit<ToastOptions, 'type' | 'promise' | 'promiseMessages'>
|
|
84
84
|
): Promise<T> => toastStore.promise(promiseFn, messages, options),
|
|
85
85
|
custom: (options: ToastOptions) => toastStore.add(options),
|
package/src/exports.test.ts
CHANGED
|
@@ -4,6 +4,23 @@ import { describe, it, expect } from 'bun:test'
|
|
|
4
4
|
// This acts as a "contract test" — if something is accidentally removed, CI will catch it.
|
|
5
5
|
|
|
6
6
|
describe('Package exports', () => {
|
|
7
|
+
it('exports the expected public API', async () => {
|
|
8
|
+
const api = await import('./index')
|
|
9
|
+
expect(Object.keys(api).sort()).toEqual([
|
|
10
|
+
'SoftToastPlugin',
|
|
11
|
+
'ToastContainer',
|
|
12
|
+
'ToastItem',
|
|
13
|
+
'consumeFlashes',
|
|
14
|
+
'getToastOptions',
|
|
15
|
+
'hasPendingFlashes',
|
|
16
|
+
'queueFlash',
|
|
17
|
+
'softToast',
|
|
18
|
+
'toastStore',
|
|
19
|
+
'useFlash',
|
|
20
|
+
'useSoftToast',
|
|
21
|
+
])
|
|
22
|
+
})
|
|
23
|
+
|
|
7
24
|
it('exports SoftToastPlugin with install()', async () => {
|
|
8
25
|
const { SoftToastPlugin } = await import('./index')
|
|
9
26
|
expect(SoftToastPlugin).toBeDefined()
|
|
@@ -20,33 +37,33 @@ describe('Package exports', () => {
|
|
|
20
37
|
expect(opts).toHaveProperty('theme')
|
|
21
38
|
})
|
|
22
39
|
|
|
23
|
-
it('exports
|
|
24
|
-
const {
|
|
25
|
-
expect(typeof
|
|
26
|
-
const api =
|
|
40
|
+
it('exports useSoftToast()', async () => {
|
|
41
|
+
const { useSoftToast } = await import('./index')
|
|
42
|
+
expect(typeof useSoftToast).toBe('function')
|
|
43
|
+
const api = useSoftToast()
|
|
27
44
|
expect(typeof api.success).toBe('function')
|
|
28
45
|
expect(typeof api.error).toBe('function')
|
|
29
46
|
})
|
|
30
47
|
|
|
31
|
-
it('exports
|
|
32
|
-
const {
|
|
33
|
-
expect(
|
|
34
|
-
expect(typeof
|
|
35
|
-
expect(typeof
|
|
36
|
-
expect(typeof
|
|
37
|
-
expect(typeof
|
|
38
|
-
expect(typeof
|
|
39
|
-
expect(typeof
|
|
40
|
-
expect(typeof
|
|
41
|
-
expect(typeof
|
|
42
|
-
expect(typeof
|
|
43
|
-
expect(typeof
|
|
44
|
-
expect(typeof
|
|
45
|
-
expect(typeof
|
|
48
|
+
it('exports softToast object', async () => {
|
|
49
|
+
const { softToast } = await import('./index')
|
|
50
|
+
expect(softToast).toBeDefined()
|
|
51
|
+
expect(typeof softToast.success).toBe('function')
|
|
52
|
+
expect(typeof softToast.error).toBe('function')
|
|
53
|
+
expect(typeof softToast.warning).toBe('function')
|
|
54
|
+
expect(typeof softToast.info).toBe('function')
|
|
55
|
+
expect(typeof softToast.default).toBe('function')
|
|
56
|
+
expect(typeof softToast.promise).toBe('function')
|
|
57
|
+
expect(typeof softToast.custom).toBe('function')
|
|
58
|
+
expect(typeof softToast.update).toBe('function')
|
|
59
|
+
expect(typeof softToast.dismiss).toBe('function')
|
|
60
|
+
expect(typeof softToast.dismissAll).toBe('function')
|
|
61
|
+
expect(typeof softToast.pause).toBe('function')
|
|
62
|
+
expect(typeof softToast.resume).toBe('function')
|
|
46
63
|
// Flash & sound API
|
|
47
|
-
expect(typeof
|
|
48
|
-
expect(typeof
|
|
49
|
-
expect(typeof
|
|
64
|
+
expect(typeof softToast.flash).toBe('function')
|
|
65
|
+
expect(typeof softToast.showFlashes).toBe('function')
|
|
66
|
+
expect(typeof softToast.hasFlashes).toBe('function')
|
|
50
67
|
})
|
|
51
68
|
|
|
52
69
|
it('exports toastStore with all methods', async () => {
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Main exports
|
|
2
2
|
export { SoftToastPlugin, getToastOptions } from './plugin'
|
|
3
|
-
export {
|
|
3
|
+
export { useSoftToast, softToast } from './composables/useSoftToast'
|
|
4
4
|
export { useFlash, queueFlash, consumeFlashes, hasPendingFlashes } from './composables/useFlash'
|
|
5
5
|
export { toastStore } from './stores/toastStore'
|
|
6
6
|
|
|
@@ -22,4 +22,3 @@ export type {
|
|
|
22
22
|
AnimationPreset,
|
|
23
23
|
QueueOverflow
|
|
24
24
|
} from './types'
|
|
25
|
-
|
package/src/plugin.ts
CHANGED
|
@@ -19,7 +19,8 @@ let pluginOptions: ToastPluginOptions = {
|
|
|
19
19
|
queueOverflow: 'drop-oldest',
|
|
20
20
|
dir: 'ltr',
|
|
21
21
|
swipeToDismiss: true,
|
|
22
|
-
teleportTarget: 'body'
|
|
22
|
+
teleportTarget: 'body',
|
|
23
|
+
autoMount: true
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
// Flag to check if container is mounted
|
|
@@ -30,8 +31,10 @@ export const SoftToastPlugin = {
|
|
|
30
31
|
// Merge options
|
|
31
32
|
pluginOptions = { ...pluginOptions, ...options }
|
|
32
33
|
|
|
34
|
+
app.component('SoftToastContainer', ToastContainer)
|
|
35
|
+
|
|
33
36
|
// Only mount container once
|
|
34
|
-
if (!isContainerMounted && typeof window !== 'undefined') {
|
|
37
|
+
if (pluginOptions.autoMount !== false && !isContainerMounted && typeof window !== 'undefined') {
|
|
35
38
|
// Create a container div
|
|
36
39
|
const containerId = 'soft-toast-global-container'
|
|
37
40
|
let container = document.getElementById(containerId)
|
|
@@ -42,9 +45,6 @@ export const SoftToastPlugin = {
|
|
|
42
45
|
document.body.appendChild(container)
|
|
43
46
|
}
|
|
44
47
|
|
|
45
|
-
// Mount ToastContainer
|
|
46
|
-
app.component('SoftToastContainer', ToastContainer)
|
|
47
|
-
|
|
48
48
|
// Create a mini-app instance just for the toast container
|
|
49
49
|
const toastContainerApp = createApp({
|
|
50
50
|
render: () => h(ToastContainer, {
|
|
@@ -62,7 +62,9 @@ export const SoftToastPlugin = {
|
|
|
62
62
|
maxQueue: pluginOptions.maxQueue,
|
|
63
63
|
queueOverflow: pluginOptions.queueOverflow,
|
|
64
64
|
dir: pluginOptions.dir,
|
|
65
|
-
swipeToDismiss: pluginOptions.swipeToDismiss
|
|
65
|
+
swipeToDismiss: pluginOptions.swipeToDismiss,
|
|
66
|
+
showTimestamp: pluginOptions.showTimestamp,
|
|
67
|
+
slotFilter: pluginOptions.slotFilter
|
|
66
68
|
})
|
|
67
69
|
})
|
|
68
70
|
|
package/src/stores/toastStore.ts
CHANGED
|
@@ -26,6 +26,17 @@ 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
|
+
|
|
32
|
+
const resetTimer = (id: string, duration: number, isPaused = false) => {
|
|
33
|
+
if (duration === Infinity) {
|
|
34
|
+
timerMap.delete(id)
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
timerMap.set(id, { remainingTime: duration, isPaused })
|
|
38
|
+
}
|
|
39
|
+
|
|
29
40
|
// ─── Computed ─────────────────────────────────────────────────────────────────
|
|
30
41
|
|
|
31
42
|
const getToastsByPosition = (position: ToastPosition) =>
|
|
@@ -37,7 +48,7 @@ const allToasts = computed(() => toasts.value)
|
|
|
37
48
|
|
|
38
49
|
/**
|
|
39
50
|
* Add a toast — or UPDATE an existing one if the same `id` is already visible.
|
|
40
|
-
* This automatic deduplication means calling
|
|
51
|
+
* This automatic deduplication means calling softToast.error('Oops', { id: 'network-error' })
|
|
41
52
|
* five times in a row results in ONE toast that refreshes its content each time.
|
|
42
53
|
*/
|
|
43
54
|
const add = (options: ToastOptions): string => {
|
|
@@ -57,6 +68,7 @@ const add = (options: ToastOptions): string => {
|
|
|
57
68
|
isPaused: existing.isPaused,
|
|
58
69
|
isLeaving: false,
|
|
59
70
|
}
|
|
71
|
+
resetTimer(id, options.duration ?? existing.duration, existing.isPaused)
|
|
60
72
|
// Play sound again if configured (content changed)
|
|
61
73
|
const sound = options.sound
|
|
62
74
|
const vol = options.soundVolume ?? 0.5
|
|
@@ -65,12 +77,13 @@ const add = (options: ToastOptions): string => {
|
|
|
65
77
|
}
|
|
66
78
|
|
|
67
79
|
// ── New toast ──
|
|
80
|
+
const duration = options.duration ?? defaultOptions.duration
|
|
68
81
|
const toast: Toast = {
|
|
69
82
|
...defaultOptions,
|
|
70
83
|
...options,
|
|
71
84
|
id,
|
|
72
85
|
createdAt: Date.now(),
|
|
73
|
-
remainingTime:
|
|
86
|
+
remainingTime: duration,
|
|
74
87
|
isPaused: false,
|
|
75
88
|
isExpanded: true,
|
|
76
89
|
isLeaving: false,
|
|
@@ -81,6 +94,9 @@ const add = (options: ToastOptions): string => {
|
|
|
81
94
|
showProgress: options.showProgress ?? defaultOptions.showProgress,
|
|
82
95
|
}
|
|
83
96
|
|
|
97
|
+
// Register in non-reactive timer map
|
|
98
|
+
resetTimer(id, duration, false)
|
|
99
|
+
|
|
84
100
|
toasts.value.unshift(toast)
|
|
85
101
|
startTickLoop()
|
|
86
102
|
|
|
@@ -95,13 +111,25 @@ const add = (options: ToastOptions): string => {
|
|
|
95
111
|
const update = (id: string, options: Partial<ToastOptions>) => {
|
|
96
112
|
const index = toasts.value.findIndex((t) => t.id === id)
|
|
97
113
|
if (index !== -1) {
|
|
98
|
-
|
|
114
|
+
const existing = toasts.value[index]
|
|
115
|
+
const nextDuration = options.duration ?? existing.duration
|
|
116
|
+
const nextRemainingTime =
|
|
117
|
+
options.duration === undefined ? existing.remainingTime : nextDuration
|
|
118
|
+
toasts.value[index] = {
|
|
119
|
+
...existing,
|
|
120
|
+
...options,
|
|
121
|
+
remainingTime: nextRemainingTime,
|
|
122
|
+
}
|
|
123
|
+
if (options.duration !== undefined) {
|
|
124
|
+
resetTimer(id, nextDuration, existing.isPaused)
|
|
125
|
+
}
|
|
99
126
|
}
|
|
100
127
|
}
|
|
101
128
|
|
|
102
129
|
const dismiss = (id?: string | { type?: ToastType | ToastType[] }) => {
|
|
103
130
|
if (!id) {
|
|
104
131
|
toasts.value.forEach((t) => { t.isLeaving = true })
|
|
132
|
+
timerMap.clear()
|
|
105
133
|
setTimeout(() => { toasts.value = [] }, 400)
|
|
106
134
|
return
|
|
107
135
|
}
|
|
@@ -111,6 +139,7 @@ const dismiss = (id?: string | { type?: ToastType | ToastType[] }) => {
|
|
|
111
139
|
if (toast) {
|
|
112
140
|
toast.isLeaving = true
|
|
113
141
|
toast.onDismiss?.(id)
|
|
142
|
+
timerMap.delete(id)
|
|
114
143
|
setTimeout(() => {
|
|
115
144
|
toasts.value = toasts.value.filter((t) => t.id !== id)
|
|
116
145
|
}, 400)
|
|
@@ -121,6 +150,7 @@ const dismiss = (id?: string | { type?: ToastType | ToastType[] }) => {
|
|
|
121
150
|
if (types.includes(t.type)) {
|
|
122
151
|
t.isLeaving = true
|
|
123
152
|
t.onDismiss?.(t.id)
|
|
153
|
+
timerMap.delete(t.id)
|
|
124
154
|
}
|
|
125
155
|
})
|
|
126
156
|
setTimeout(() => {
|
|
@@ -130,11 +160,15 @@ const dismiss = (id?: string | { type?: ToastType | ToastType[] }) => {
|
|
|
130
160
|
}
|
|
131
161
|
|
|
132
162
|
const pause = (id: string) => {
|
|
163
|
+
const timer = timerMap.get(id)
|
|
164
|
+
if (timer) timer.isPaused = true
|
|
133
165
|
const toast = toasts.value.find((t) => t.id === id)
|
|
134
166
|
if (toast) toast.isPaused = true
|
|
135
167
|
}
|
|
136
168
|
|
|
137
169
|
const resume = (id: string) => {
|
|
170
|
+
const timer = timerMap.get(id)
|
|
171
|
+
if (timer) timer.isPaused = false
|
|
138
172
|
const toast = toasts.value.find((t) => t.id === id)
|
|
139
173
|
if (toast) toast.isPaused = false
|
|
140
174
|
}
|
|
@@ -163,19 +197,31 @@ const startTickLoop = () => {
|
|
|
163
197
|
const delta = currentTime - lastTime
|
|
164
198
|
lastTime = currentTime
|
|
165
199
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
200
|
+
// Tick non-reactive timer map — zero Vue reactivity cost per frame
|
|
201
|
+
const expiredIds: string[] = []
|
|
202
|
+
timerMap.forEach((timer, id) => {
|
|
203
|
+
if (timer.isPaused) return
|
|
204
|
+
const toast = toasts.value.find((t) => t.id === id)
|
|
205
|
+
if (!toast || toast.isLeaving || toast.duration === Infinity) return
|
|
206
|
+
timer.remainingTime -= delta
|
|
207
|
+
// Sync remainingTime back to toast only for progress bar consumers
|
|
208
|
+
if (toast.showProgress) toast.remainingTime = timer.remainingTime
|
|
209
|
+
if (timer.remainingTime <= 0) expiredIds.push(id)
|
|
177
210
|
})
|
|
178
211
|
|
|
212
|
+
// Mutate Vue state only when a toast actually expires
|
|
213
|
+
for (const id of expiredIds) {
|
|
214
|
+
timerMap.delete(id)
|
|
215
|
+
const toast = toasts.value.find((t) => t.id === id)
|
|
216
|
+
if (toast && !toast.isLeaving) {
|
|
217
|
+
toast.isLeaving = true
|
|
218
|
+
toast.onAutoClose?.(id)
|
|
219
|
+
setTimeout(() => {
|
|
220
|
+
toasts.value = toasts.value.filter((t) => t.id !== id)
|
|
221
|
+
}, 400)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
179
225
|
if (toasts.value.length > 0) {
|
|
180
226
|
rafId = requestAnimationFrame(loop)
|
|
181
227
|
} else {
|
|
@@ -206,7 +252,7 @@ const loading = (title: string, options?: Omit<ToastOptions, 'type' | 'title'>)
|
|
|
206
252
|
|
|
207
253
|
const promise = async <T>(
|
|
208
254
|
promiseFn: Promise<T>,
|
|
209
|
-
messages: ToastPromiseMessages
|
|
255
|
+
messages: ToastPromiseMessages<T>,
|
|
210
256
|
options?: Omit<ToastOptions, 'type' | 'promise' | 'promiseMessages'>
|
|
211
257
|
): Promise<T> => {
|
|
212
258
|
const id = add({
|
|
@@ -220,16 +266,16 @@ const promise = async <T>(
|
|
|
220
266
|
const result = await promiseFn
|
|
221
267
|
update(id, {
|
|
222
268
|
type: 'success',
|
|
223
|
-
title: messages.success,
|
|
224
|
-
description: messages.description?.success,
|
|
269
|
+
title: typeof messages.success === 'function' ? messages.success(result) : messages.success,
|
|
270
|
+
description: typeof messages.description?.success === 'function' ? messages.description.success(result) : messages.description?.success,
|
|
225
271
|
duration: 4000,
|
|
226
272
|
})
|
|
227
273
|
return result
|
|
228
274
|
} catch (err) {
|
|
229
275
|
update(id, {
|
|
230
276
|
type: 'error',
|
|
231
|
-
title: messages.error,
|
|
232
|
-
description: messages.description?.error,
|
|
277
|
+
title: typeof messages.error === 'function' ? messages.error(err) : messages.error,
|
|
278
|
+
description: typeof messages.description?.error === 'function' ? messages.description.error(err) : messages.description?.error,
|
|
233
279
|
action: messages.action?.error,
|
|
234
280
|
duration: 6000,
|
|
235
281
|
})
|
|
@@ -237,9 +283,13 @@ const promise = async <T>(
|
|
|
237
283
|
}
|
|
238
284
|
}
|
|
239
285
|
|
|
240
|
-
const clearAll = () => {
|
|
286
|
+
const clearAll = () => {
|
|
287
|
+
timerMap.clear()
|
|
288
|
+
toasts.value = []
|
|
289
|
+
}
|
|
241
290
|
|
|
242
291
|
const remove = (id: string) => {
|
|
292
|
+
timerMap.delete(id)
|
|
243
293
|
toasts.value = toasts.value.filter((t) => t.id !== id)
|
|
244
294
|
}
|
|
245
295
|
|
|
@@ -262,7 +312,7 @@ export interface ToastStore {
|
|
|
262
312
|
warning: (title: string, options?: Omit<ToastOptions, 'type' | 'title'>) => string
|
|
263
313
|
info: (title: string, options?: Omit<ToastOptions, 'type' | 'title'>) => string
|
|
264
314
|
loading: (title: string, options?: Omit<ToastOptions, 'type' | 'title'>) => string
|
|
265
|
-
promise: <T>(promiseFn: Promise<T>, messages: ToastPromiseMessages
|
|
315
|
+
promise: <T>(promiseFn: Promise<T>, messages: ToastPromiseMessages<T>, options?: Omit<ToastOptions, 'type' | 'promise' | 'promiseMessages'>) => Promise<T>
|
|
266
316
|
clearAll: () => void
|
|
267
317
|
remove: (id: string) => void
|
|
268
318
|
}
|