@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.
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, beforeEach } from 'bun:test'
2
- import { useToast, toast } from './useToast'
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
- // ─── useToast() composable ────────────────────────────────────────────────────
10
+ // ─── useSoftToast() composable ────────────────────────────────────────────────────
11
11
 
12
- describe('useToast()', () => {
12
+ describe('useSoftToast()', () => {
13
13
  it('returns an object with all API methods', () => {
14
- const t = useToast()
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 = useToast()
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 = useToast()
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 = useToast()
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 = useToast()
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 = useToast()
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 = useToast()
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 = useToast()
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 = useToast()
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 = useToast()
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 = useToast()
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
- // ─── toast (static object) ────────────────────────────────────────────────────
113
+ // ─── softToast (static object) ────────────────────────────────────────────────
114
114
 
115
- describe('toast (static API)', () => {
116
- it('toast.success() adds a success toast', () => {
117
- toast.success('Static success')
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('toast.error() adds an error toast', () => {
123
- toast.error('Static error')
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('toast.warning() adds a warning toast', () => {
128
- toast.warning('Careful')
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('toast.info() adds an info toast', () => {
133
- toast.info('Just so you know')
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('toast.default() adds a default toast', () => {
138
- toast.default('Hello world')
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('toast.custom() adds toast with arbitrary options', () => {
143
- toast.custom({ title: 'Custom static', type: 'info', id: 'static-1' })
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('toast.update() modifies a toast', () => {
149
- const id = toast.default('Initial')
150
- toast.update(id, { title: 'Updated' })
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('toast.pause() and toast.resume() work', () => {
155
- const id = toast.info('Pausable')
156
- toast.pause(id)
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
- toast.resume(id)
158
+ softToast.resume(id)
159
159
  expect(toastStore.toasts.value[0].isPaused).toBe(false)
160
160
  })
161
161
 
162
- it('toast.dismiss() marks toast as leaving', () => {
163
- const id = toast.success('Going away')
164
- toast.dismiss(id)
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
- toast.default('First')
170
- toast.success('Second')
171
- toast.error('Third')
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
- // ─── toast.promise() ─────────────────────────────────────────────────────────
177
+ // ─── softToast.promise() ─────────────────────────────────────────────────────────
178
178
 
179
- describe('toast.promise()', () => {
179
+ describe('softToast.promise()', () => {
180
180
  it('resolves: updates toast to success', async () => {
181
- const id = await toast.promise(
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 toast.promise(
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('useToast() — flash API methods', () => {
209
+ describe('useSoftToast() — flash API methods', () => {
210
210
  it('returns flash, showFlashes, hasFlashes methods', () => {
211
- const t = useToast()
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('toast (static) — flash API methods', () => {
219
- it('toast.flash is a function', () => {
220
- expect(typeof toast.flash).toBe('function')
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('toast.showFlashes is a function', () => {
224
- expect(typeof toast.showFlashes).toBe('function')
223
+ it('softToast.showFlashes is a function', () => {
224
+ expect(typeof softToast.showFlashes).toBe('function')
225
225
  })
226
226
 
227
- it('toast.hasFlashes is a function', () => {
228
- expect(typeof toast.hasFlashes).toBe('function')
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 useToast = () => ({
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 } = useToast()
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 toast = {
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),
@@ -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 useToast()', async () => {
24
- const { useToast } = await import('./index')
25
- expect(typeof useToast).toBe('function')
26
- const api = useToast()
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 toast object', async () => {
32
- const { toast } = await import('./index')
33
- expect(toast).toBeDefined()
34
- expect(typeof toast.success).toBe('function')
35
- expect(typeof toast.error).toBe('function')
36
- expect(typeof toast.warning).toBe('function')
37
- expect(typeof toast.info).toBe('function')
38
- expect(typeof toast.default).toBe('function')
39
- expect(typeof toast.promise).toBe('function')
40
- expect(typeof toast.custom).toBe('function')
41
- expect(typeof toast.update).toBe('function')
42
- expect(typeof toast.dismiss).toBe('function')
43
- expect(typeof toast.dismissAll).toBe('function')
44
- expect(typeof toast.pause).toBe('function')
45
- expect(typeof toast.resume).toBe('function')
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 toast.flash).toBe('function')
48
- expect(typeof toast.showFlashes).toBe('function')
49
- expect(typeof toast.hasFlashes).toBe('function')
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 { useToast, toast } from './composables/useToast'
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
 
@@ -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 toast.error('Oops', { id: 'network-error' })
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: options.duration ?? defaultOptions.duration,
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
- toasts.value[index] = { ...toasts.value[index], ...options }
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
- toasts.value.forEach((toast) => {
167
- if (!toast.isPaused && !toast.isLeaving && toast.remainingTime > 0 && toast.duration !== Infinity) {
168
- toast.remainingTime -= delta
169
- if (toast.remainingTime <= 0) {
170
- toast.isLeaving = true
171
- toast.onAutoClose?.(toast.id)
172
- setTimeout(() => {
173
- toasts.value = toasts.value.filter((t) => t.id !== toast.id)
174
- }, 400)
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 = () => { toasts.value = [] }
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, options?: Omit<ToastOptions, 'type' | 'promise' | 'promiseMessages'>) => Promise<T>
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
  }