vue-toast-kit 1.0.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/LICENSE +21 -0
- package/README.md +1145 -0
- package/dist/composables/useToast.d.ts +28 -0
- package/dist/composables/useToastContext.d.ts +6 -0
- package/dist/composables/useToastState.d.ts +7 -0
- package/dist/core/GroupManager.d.ts +16 -0
- package/dist/core/ToastBuffer.d.ts +19 -0
- package/dist/core/ToastQueue.d.ts +55 -0
- package/dist/core/UndoTimer.d.ts +23 -0
- package/dist/core/types.d.ts +114 -0
- package/dist/index.d.ts +23 -0
- package/dist/module.d.ts +1 -0
- package/dist/nuxt/module.cjs +2 -0
- package/dist/nuxt/module.cjs.map +1 -0
- package/dist/nuxt/module.d.ts +1 -0
- package/dist/nuxt/module.js +34 -0
- package/dist/nuxt/module.js.map +1 -0
- package/dist/plugin.d.ts +6 -0
- package/dist/style.css +1 -0
- package/dist/testing.d.ts +14 -0
- package/dist/vue-toast-kit.cjs +2 -0
- package/dist/vue-toast-kit.cjs.map +1 -0
- package/dist/vue-toast-kit.d.ts +540 -0
- package/dist/vue-toast-kit.js +1000 -0
- package/dist/vue-toast-kit.js.map +1 -0
- package/package.json +89 -0
- package/src/components/Toast.vue +222 -0
- package/src/components/ToastActions.vue +34 -0
- package/src/components/ToastContainer.vue +257 -0
- package/src/components/ToastIcon.vue +53 -0
- package/src/components/ToastProgressBar.vue +18 -0
- package/src/composables/useToast.ts +152 -0
- package/src/composables/useToastContext.ts +63 -0
- package/src/composables/useToastState.ts +18 -0
- package/src/core/GroupManager.ts +105 -0
- package/src/core/ToastBuffer.ts +45 -0
- package/src/core/ToastQueue.ts +377 -0
- package/src/core/UndoTimer.ts +90 -0
- package/src/core/types.ts +142 -0
- package/src/env.d.ts +7 -0
- package/src/index.ts +51 -0
- package/src/nuxt/composables.ts +13 -0
- package/src/nuxt/module.ts +52 -0
- package/src/nuxt/plugin.ts +8 -0
- package/src/plugin.ts +18 -0
- package/src/styles/animations.css +106 -0
- package/src/styles/base.css +201 -0
- package/src/styles/themes/dark.css +30 -0
- package/src/styles/themes/light.css +30 -0
- package/src/styles/themes/system.css +32 -0
- package/src/styles/tokens.css +74 -0
- package/src/testing.ts +81 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { VNode } from 'vue'
|
|
2
|
+
import type { ToastOptions } from './types'
|
|
3
|
+
|
|
4
|
+
interface BufferedToast {
|
|
5
|
+
message: string | VNode
|
|
6
|
+
options: ToastOptions
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const isServer = typeof window === 'undefined'
|
|
10
|
+
|
|
11
|
+
export class ToastBuffer {
|
|
12
|
+
private buffer: BufferedToast[] = []
|
|
13
|
+
private flushed = false
|
|
14
|
+
private flushCallbacks: Array<(items: BufferedToast[]) => void> = []
|
|
15
|
+
|
|
16
|
+
push(message: string | VNode, options: ToastOptions): void {
|
|
17
|
+
if (this.flushed) return
|
|
18
|
+
this.buffer.push({ message, options })
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
onFlush(cb: (items: BufferedToast[]) => void): void {
|
|
22
|
+
this.flushCallbacks.push(cb)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
flush(): void {
|
|
26
|
+
if (this.flushed) return
|
|
27
|
+
this.flushed = true
|
|
28
|
+
const items = [...this.buffer]
|
|
29
|
+
this.buffer = []
|
|
30
|
+
for (const cb of this.flushCallbacks) {
|
|
31
|
+
cb(items)
|
|
32
|
+
}
|
|
33
|
+
this.flushCallbacks = []
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
isFlushed(): boolean {
|
|
37
|
+
return this.flushed
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get size(): number {
|
|
41
|
+
return this.buffer.length
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const globalBuffer = new ToastBuffer()
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import { ref, reactive, shallowReactive, type VNode } from 'vue'
|
|
2
|
+
import { GroupManager } from './GroupManager'
|
|
3
|
+
import { UndoTimer } from './UndoTimer'
|
|
4
|
+
import { PRIORITY_ORDER, DEFAULT_OPTIONS, type ToastItem, type ToastOptions, type ToastPosition } from './types'
|
|
5
|
+
|
|
6
|
+
let idCounter = 0
|
|
7
|
+
function generateId(): string {
|
|
8
|
+
return `vtk-${Date.now()}-${++idCounter}`
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const PERSIST_KEY = 'vtk-persist'
|
|
12
|
+
|
|
13
|
+
type EventListener<T extends unknown[]> = (...args: T) => void
|
|
14
|
+
|
|
15
|
+
export class ToastQueue {
|
|
16
|
+
active = shallowReactive<ToastItem[]>([])
|
|
17
|
+
pending = shallowReactive<ToastItem[]>([])
|
|
18
|
+
|
|
19
|
+
private maxVisible: number
|
|
20
|
+
private hiddenItems = reactive(new Set<string>())
|
|
21
|
+
private timers = new Map<string, UndoTimer>()
|
|
22
|
+
private groupManager: GroupManager
|
|
23
|
+
|
|
24
|
+
private rateLimit: number
|
|
25
|
+
private rateLimitWindowMs: number
|
|
26
|
+
private recentAddTimes: number[] = []
|
|
27
|
+
|
|
28
|
+
private persistStorage: boolean
|
|
29
|
+
|
|
30
|
+
private addListeners = new Set<EventListener<[ToastItem]>>()
|
|
31
|
+
private dismissListeners = new Set<EventListener<[string]>>()
|
|
32
|
+
private updateListeners = new Set<EventListener<[string, Partial<ToastOptions>]>>()
|
|
33
|
+
|
|
34
|
+
constructor(maxVisible = 5, options: { rateLimit?: number; rateLimitWindowMs?: number; persistStorage?: boolean } = {}) {
|
|
35
|
+
this.maxVisible = maxVisible
|
|
36
|
+
this.rateLimit = options.rateLimit ?? 0
|
|
37
|
+
this.rateLimitWindowMs = options.rateLimitWindowMs ?? 1000
|
|
38
|
+
this.persistStorage = options.persistStorage ?? false
|
|
39
|
+
|
|
40
|
+
this.groupManager = new GroupManager(
|
|
41
|
+
(ids) => [...this.active, ...this.pending].filter(t => ids.includes(t.id)),
|
|
42
|
+
(id) => this.hiddenItems.add(id),
|
|
43
|
+
(id) => this.hiddenItems.delete(id),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
if (this.persistStorage) {
|
|
47
|
+
this.restoreFromStorage()
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Subscribe to toast add events. Returns an unsubscribe function. */
|
|
52
|
+
onAdd(fn: EventListener<[ToastItem]>): () => void {
|
|
53
|
+
this.addListeners.add(fn)
|
|
54
|
+
return () => this.addListeners.delete(fn)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Subscribe to toast dismiss events. Returns an unsubscribe function. */
|
|
58
|
+
onDismiss(fn: EventListener<[string]>): () => void {
|
|
59
|
+
this.dismissListeners.add(fn)
|
|
60
|
+
return () => this.dismissListeners.delete(fn)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Subscribe to toast update events. Returns an unsubscribe function. */
|
|
64
|
+
onUpdate(fn: EventListener<[string, Partial<ToastOptions>]>): () => void {
|
|
65
|
+
this.updateListeners.add(fn)
|
|
66
|
+
return () => this.updateListeners.delete(fn)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private emit<T extends unknown[]>(set: Set<EventListener<T>>, ...args: T): void {
|
|
70
|
+
set.forEach(fn => fn(...args))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private isRateLimited(): boolean {
|
|
74
|
+
if (!this.rateLimit) return false
|
|
75
|
+
const now = Date.now()
|
|
76
|
+
this.recentAddTimes = this.recentAddTimes.filter(t => now - t < this.rateLimitWindowMs)
|
|
77
|
+
if (this.recentAddTimes.length >= this.rateLimit) return true
|
|
78
|
+
this.recentAddTimes.push(now)
|
|
79
|
+
return false
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private restoreFromStorage(): void {
|
|
83
|
+
if (typeof localStorage === 'undefined') return
|
|
84
|
+
try {
|
|
85
|
+
const raw = localStorage.getItem(PERSIST_KEY)
|
|
86
|
+
if (!raw) return
|
|
87
|
+
const items: Array<{ id: string; message: string; options: ToastOptions }> = JSON.parse(raw)
|
|
88
|
+
for (const item of items) {
|
|
89
|
+
this.add(item.message, { ...item.options, id: item.id })
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
localStorage.removeItem(PERSIST_KEY)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private saveToStorage(item: ToastItem): void {
|
|
97
|
+
if (typeof localStorage === 'undefined') return
|
|
98
|
+
try {
|
|
99
|
+
const raw = localStorage.getItem(PERSIST_KEY)
|
|
100
|
+
const items: Array<{ id: string; message: string; options: ToastOptions }> = raw ? JSON.parse(raw) : []
|
|
101
|
+
if (!items.find(i => i.id === item.id)) {
|
|
102
|
+
items.push({ id: item.id, message: item.message as string, options: item.options })
|
|
103
|
+
localStorage.setItem(PERSIST_KEY, JSON.stringify(items))
|
|
104
|
+
}
|
|
105
|
+
} catch { /* noop */ }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private removeFromStorage(id: string): void {
|
|
109
|
+
if (typeof localStorage === 'undefined') return
|
|
110
|
+
try {
|
|
111
|
+
const raw = localStorage.getItem(PERSIST_KEY)
|
|
112
|
+
if (!raw) return
|
|
113
|
+
const items: Array<{ id: string }> = JSON.parse(raw)
|
|
114
|
+
const filtered = items.filter(i => i.id !== id)
|
|
115
|
+
if (filtered.length) {
|
|
116
|
+
localStorage.setItem(PERSIST_KEY, JSON.stringify(filtered))
|
|
117
|
+
} else {
|
|
118
|
+
localStorage.removeItem(PERSIST_KEY)
|
|
119
|
+
}
|
|
120
|
+
} catch { /* noop */ }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
get visibleActive(): ToastItem[] {
|
|
124
|
+
return this.active.filter(t => !this.hiddenItems.has(t.id))
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
isHidden(id: string): boolean {
|
|
128
|
+
return this.hiddenItems.has(id)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
add(message: string | VNode, options: ToastOptions = {}): string {
|
|
132
|
+
if (this.isRateLimited()) return ''
|
|
133
|
+
|
|
134
|
+
const id = options.id ?? generateId()
|
|
135
|
+
|
|
136
|
+
const existing = this.active.find(t => t.id === id)
|
|
137
|
+
if (existing) {
|
|
138
|
+
this.mergeOptions(existing, options)
|
|
139
|
+
return id
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const item = this.createItem(id, message, options)
|
|
143
|
+
|
|
144
|
+
if (this.visibleActive.length < this.maxVisible) {
|
|
145
|
+
this.active.push(item)
|
|
146
|
+
this.startTimer(item)
|
|
147
|
+
if (options.groupKey) this.groupManager.add(id, options.groupKey)
|
|
148
|
+
if (this.persistStorage && item.options.persist) this.saveToStorage(item)
|
|
149
|
+
this.emit(this.addListeners, item)
|
|
150
|
+
return id
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const priority = options.priority ?? 'normal'
|
|
154
|
+
if (PRIORITY_ORDER[priority] > PRIORITY_ORDER['normal']) {
|
|
155
|
+
const lowestIdx = this.findLowestPriorityIndex()
|
|
156
|
+
if (lowestIdx !== -1) {
|
|
157
|
+
const evicted = this.active[lowestIdx]
|
|
158
|
+
if (PRIORITY_ORDER[priority] > PRIORITY_ORDER[evicted.options.priority]) {
|
|
159
|
+
this.stopTimer(evicted.id)
|
|
160
|
+
this.active.splice(lowestIdx, 1)
|
|
161
|
+
this.pending.unshift(evicted)
|
|
162
|
+
this.active.push(item)
|
|
163
|
+
this.startTimer(item)
|
|
164
|
+
if (options.groupKey) this.groupManager.add(id, options.groupKey)
|
|
165
|
+
if (this.persistStorage && item.options.persist) this.saveToStorage(item)
|
|
166
|
+
this.emit(this.addListeners, item)
|
|
167
|
+
return id
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
this.pending.push(item)
|
|
173
|
+
this.sortPending()
|
|
174
|
+
if (options.groupKey) this.groupManager.add(id, options.groupKey)
|
|
175
|
+
if (this.persistStorage && item.options.persist) this.saveToStorage(item)
|
|
176
|
+
this.emit(this.addListeners, item)
|
|
177
|
+
return id
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
remove(id: string): void {
|
|
181
|
+
this.stopTimer(id)
|
|
182
|
+
|
|
183
|
+
const activeIdx = this.active.findIndex(t => t.id === id)
|
|
184
|
+
if (activeIdx !== -1) {
|
|
185
|
+
const item = this.active[activeIdx]
|
|
186
|
+
this.active.splice(activeIdx, 1)
|
|
187
|
+
this.hiddenItems.delete(id)
|
|
188
|
+
|
|
189
|
+
if (item.options.groupKey) this.groupManager.remove(id, item.options.groupKey)
|
|
190
|
+
if (this.persistStorage) this.removeFromStorage(id)
|
|
191
|
+
|
|
192
|
+
if (this.pending.length > 0) {
|
|
193
|
+
const next = this.pending.shift()!
|
|
194
|
+
this.active.push(next)
|
|
195
|
+
this.startTimer(next)
|
|
196
|
+
}
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const pendingIdx = this.pending.findIndex(t => t.id === id)
|
|
201
|
+
if (pendingIdx !== -1) {
|
|
202
|
+
const item = this.pending[pendingIdx]
|
|
203
|
+
this.pending.splice(pendingIdx, 1)
|
|
204
|
+
this.hiddenItems.delete(id)
|
|
205
|
+
if (item.options.groupKey) this.groupManager.remove(id, item.options.groupKey)
|
|
206
|
+
if (this.persistStorage) this.removeFromStorage(id)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
update(id: string, partial: Partial<ToastOptions>): void {
|
|
211
|
+
const item = [...this.active, ...this.pending].find(t => t.id === id)
|
|
212
|
+
if (!item) return
|
|
213
|
+
this.mergeOptions(item, partial)
|
|
214
|
+
this.emit(this.updateListeners, id, partial)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
dismiss(id?: string): void {
|
|
218
|
+
if (id === undefined) {
|
|
219
|
+
const ids = [...this.active, ...this.pending].map(t => t.id)
|
|
220
|
+
ids.forEach(i => this.remove(i))
|
|
221
|
+
return
|
|
222
|
+
}
|
|
223
|
+
const item = [...this.active, ...this.pending].find(t => t.id === id)
|
|
224
|
+
if (item) {
|
|
225
|
+
item.options.onClose?.()
|
|
226
|
+
this.remove(id)
|
|
227
|
+
this.emit(this.dismissListeners, id)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
dismissAll(position?: ToastPosition): void {
|
|
232
|
+
const targets = [...this.active, ...this.pending].filter(
|
|
233
|
+
t => !position || t.options.position === position,
|
|
234
|
+
)
|
|
235
|
+
targets.forEach(t => {
|
|
236
|
+
t.options.onClose?.()
|
|
237
|
+
this.remove(t.id)
|
|
238
|
+
this.emit(this.dismissListeners, t.id)
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
isActive(id: string): boolean {
|
|
243
|
+
return this.active.some(t => t.id === id)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
pauseAll(): void {
|
|
247
|
+
this.active.forEach(t => t.pause())
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
resumeAll(): void {
|
|
251
|
+
this.active.forEach(t => t.resume())
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
setMaxVisible(n: number): void {
|
|
255
|
+
this.maxVisible = n
|
|
256
|
+
while (this.visibleActive.length < n && this.pending.length > 0) {
|
|
257
|
+
const next = this.pending.shift()!
|
|
258
|
+
this.active.push(next)
|
|
259
|
+
this.startTimer(next)
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
toggleGroupExpand(groupKey: string): void {
|
|
264
|
+
this.groupManager.toggleExpand(groupKey)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
isGroupExpanded(groupKey: string): boolean {
|
|
268
|
+
return this.groupManager.isExpanded(groupKey)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
destroy(): void {
|
|
272
|
+
this.timers.forEach(t => t.destroy())
|
|
273
|
+
this.timers.clear()
|
|
274
|
+
this.groupManager.clear()
|
|
275
|
+
this.active.splice(0)
|
|
276
|
+
this.pending.splice(0)
|
|
277
|
+
this.hiddenItems.clear()
|
|
278
|
+
this.addListeners.clear()
|
|
279
|
+
this.dismissListeners.clear()
|
|
280
|
+
this.updateListeners.clear()
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private createItem(id: string, message: string | VNode, options: ToastOptions): ToastItem {
|
|
284
|
+
const remaining = ref(1)
|
|
285
|
+
const isPaused = ref(false)
|
|
286
|
+
const groupCount = ref(1)
|
|
287
|
+
const isGrouped = ref(false)
|
|
288
|
+
|
|
289
|
+
const mergedOptions = {
|
|
290
|
+
...DEFAULT_OPTIONS,
|
|
291
|
+
...options,
|
|
292
|
+
type: options.type ?? 'info',
|
|
293
|
+
priority: options.priority ?? 'normal',
|
|
294
|
+
duration: options.duration ?? 4000,
|
|
295
|
+
closable: options.closable ?? true,
|
|
296
|
+
pauseOnHover: options.pauseOnHover ?? true,
|
|
297
|
+
pauseOnFocusLoss: options.pauseOnFocusLoss ?? true,
|
|
298
|
+
swipeToDismiss: options.swipeToDismiss ?? true,
|
|
299
|
+
persist: options.persist ?? false,
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const item: ToastItem = {
|
|
303
|
+
id,
|
|
304
|
+
message,
|
|
305
|
+
options: mergedOptions as ToastItem['options'],
|
|
306
|
+
createdAt: Date.now(),
|
|
307
|
+
remaining,
|
|
308
|
+
isPaused,
|
|
309
|
+
groupCount,
|
|
310
|
+
isGrouped,
|
|
311
|
+
pause: () => {
|
|
312
|
+
isPaused.value = true
|
|
313
|
+
this.timers.get(id)?.pause()
|
|
314
|
+
},
|
|
315
|
+
resume: () => {
|
|
316
|
+
isPaused.value = false
|
|
317
|
+
this.timers.get(id)?.resume()
|
|
318
|
+
},
|
|
319
|
+
dismiss: () => this.dismiss(id),
|
|
320
|
+
update: (opts) => this.update(id, opts),
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return item
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private startTimer(item: ToastItem): void {
|
|
327
|
+
const duration = item.options.undo?.duration ?? item.options.duration
|
|
328
|
+
if (!duration) return
|
|
329
|
+
|
|
330
|
+
const timer = new UndoTimer(
|
|
331
|
+
duration,
|
|
332
|
+
() => {
|
|
333
|
+
item.options.onAutoClose?.()
|
|
334
|
+
this.remove(item.id)
|
|
335
|
+
},
|
|
336
|
+
(r) => {
|
|
337
|
+
item.remaining.value = r
|
|
338
|
+
},
|
|
339
|
+
)
|
|
340
|
+
this.timers.set(item.id, timer)
|
|
341
|
+
timer.start()
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
private stopTimer(id: string): void {
|
|
345
|
+
this.timers.get(id)?.destroy()
|
|
346
|
+
this.timers.delete(id)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
private mergeOptions(item: ToastItem, partial: Partial<ToastOptions>): void {
|
|
350
|
+
Object.assign(item.options, partial)
|
|
351
|
+
if ('message' in partial) {
|
|
352
|
+
item.message = (partial as Record<string, unknown>).message as string | VNode
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
private findLowestPriorityIndex(): number {
|
|
357
|
+
let lowestIdx = -1
|
|
358
|
+
let lowestPriority = 999
|
|
359
|
+
this.active.forEach((t, i) => {
|
|
360
|
+
const p = PRIORITY_ORDER[t.options.priority]
|
|
361
|
+
if (p < lowestPriority) {
|
|
362
|
+
lowestPriority = p
|
|
363
|
+
lowestIdx = i
|
|
364
|
+
}
|
|
365
|
+
})
|
|
366
|
+
return lowestIdx
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private sortPending(): void {
|
|
370
|
+
this.pending.sort((a, b) => {
|
|
371
|
+
const pa = PRIORITY_ORDER[a.options.priority]
|
|
372
|
+
const pb = PRIORITY_ORDER[b.options.priority]
|
|
373
|
+
if (pb !== pa) return pb - pa
|
|
374
|
+
return a.createdAt - b.createdAt
|
|
375
|
+
})
|
|
376
|
+
}
|
|
377
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
export class UndoTimer {
|
|
2
|
+
private timerId: ReturnType<typeof setTimeout> | null = null
|
|
3
|
+
private tickId: ReturnType<typeof setInterval> | null = null
|
|
4
|
+
private elapsed = 0
|
|
5
|
+
private startTime = 0
|
|
6
|
+
private _remaining = 1
|
|
7
|
+
private _isPaused = false
|
|
8
|
+
|
|
9
|
+
private static readonly TICK_INTERVAL = 50
|
|
10
|
+
|
|
11
|
+
readonly duration: number
|
|
12
|
+
readonly onExpire: () => void
|
|
13
|
+
readonly onTick?: (remaining: number) => void
|
|
14
|
+
|
|
15
|
+
constructor(duration: number, onExpire: () => void, onTick?: (remaining: number) => void) {
|
|
16
|
+
this.duration = duration
|
|
17
|
+
this.onExpire = onExpire
|
|
18
|
+
this.onTick = onTick
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get remaining(): number {
|
|
22
|
+
return this._remaining
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get isPaused(): boolean {
|
|
26
|
+
return this._isPaused
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
start(): void {
|
|
30
|
+
this.elapsed = 0
|
|
31
|
+
this._remaining = 1
|
|
32
|
+
this.startTime = Date.now()
|
|
33
|
+
this.scheduleExpiry(this.duration)
|
|
34
|
+
if (this.onTick) this.startTick()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
pause(): void {
|
|
38
|
+
if (this._isPaused) return
|
|
39
|
+
this._isPaused = true
|
|
40
|
+
this.elapsed += Date.now() - this.startTime
|
|
41
|
+
this.clearExpiry()
|
|
42
|
+
this.clearTick()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
resume(): void {
|
|
46
|
+
if (!this._isPaused) return
|
|
47
|
+
this._isPaused = false
|
|
48
|
+
const remainingMs = this.duration - this.elapsed
|
|
49
|
+
this.startTime = Date.now()
|
|
50
|
+
this.scheduleExpiry(remainingMs)
|
|
51
|
+
if (this.onTick) this.startTick()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
destroy(): void {
|
|
55
|
+
this.clearExpiry()
|
|
56
|
+
this.clearTick()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private scheduleExpiry(delay: number): void {
|
|
60
|
+
this.timerId = setTimeout(() => {
|
|
61
|
+
this.clearTick()
|
|
62
|
+
this._remaining = 0
|
|
63
|
+
this.onTick?.(0)
|
|
64
|
+
this.timerId = null
|
|
65
|
+
this.onExpire()
|
|
66
|
+
}, delay)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private clearExpiry(): void {
|
|
70
|
+
if (this.timerId !== null) {
|
|
71
|
+
clearTimeout(this.timerId)
|
|
72
|
+
this.timerId = null
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private startTick(): void {
|
|
77
|
+
this.tickId = setInterval(() => {
|
|
78
|
+
const totalElapsed = this.elapsed + (Date.now() - this.startTime)
|
|
79
|
+
this._remaining = Math.max(0, 1 - totalElapsed / this.duration)
|
|
80
|
+
this.onTick?.(this._remaining)
|
|
81
|
+
}, UndoTimer.TICK_INTERVAL)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private clearTick(): void {
|
|
85
|
+
if (this.tickId !== null) {
|
|
86
|
+
clearInterval(this.tickId)
|
|
87
|
+
this.tickId = null
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { Component, VNode, Ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
export type ToastType = 'info' | 'success' | 'warning' | 'error' | 'loading' | 'custom'
|
|
4
|
+
export type ToastPriority = 'critical' | 'high' | 'normal' | 'low'
|
|
5
|
+
export type ToastPosition =
|
|
6
|
+
| 'top-left' | 'top-center' | 'top-right'
|
|
7
|
+
| 'bottom-left' | 'bottom-center' | 'bottom-right'
|
|
8
|
+
|
|
9
|
+
export interface ToastAction {
|
|
10
|
+
label: string
|
|
11
|
+
onClick: () => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ToastUndo {
|
|
15
|
+
label?: string
|
|
16
|
+
onUndo: () => void | Promise<void>
|
|
17
|
+
duration?: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ToastDesignTokens {
|
|
21
|
+
colorBg?: string
|
|
22
|
+
colorText?: string
|
|
23
|
+
colorBorder?: string
|
|
24
|
+
colorSuccess?: string
|
|
25
|
+
colorError?: string
|
|
26
|
+
colorWarning?: string
|
|
27
|
+
colorInfo?: string
|
|
28
|
+
colorLoading?: string
|
|
29
|
+
fontFamily?: string
|
|
30
|
+
fontSize?: string
|
|
31
|
+
fontWeight?: string
|
|
32
|
+
lineHeight?: string
|
|
33
|
+
borderRadius?: string
|
|
34
|
+
borderWidth?: string
|
|
35
|
+
shadow?: string
|
|
36
|
+
paddingX?: string
|
|
37
|
+
paddingY?: string
|
|
38
|
+
iconSize?: string
|
|
39
|
+
progressHeight?: string
|
|
40
|
+
maxWidth?: string
|
|
41
|
+
minWidth?: string
|
|
42
|
+
transitionDuration?: string
|
|
43
|
+
transitionEasing?: string
|
|
44
|
+
zIndex?: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ToastOptions {
|
|
48
|
+
id?: string
|
|
49
|
+
type?: ToastType
|
|
50
|
+
priority?: ToastPriority
|
|
51
|
+
duration?: number
|
|
52
|
+
position?: ToastPosition
|
|
53
|
+
closable?: boolean
|
|
54
|
+
groupKey?: string
|
|
55
|
+
icon?: Component | string | false
|
|
56
|
+
action?: ToastAction
|
|
57
|
+
undo?: ToastUndo
|
|
58
|
+
onClose?: () => void
|
|
59
|
+
onAutoClose?: () => void
|
|
60
|
+
pauseOnHover?: boolean
|
|
61
|
+
pauseOnFocusLoss?: boolean
|
|
62
|
+
swipeToDismiss?: boolean
|
|
63
|
+
persist?: boolean
|
|
64
|
+
component?: Component
|
|
65
|
+
componentProps?: Record<string, unknown>
|
|
66
|
+
ariaLive?: 'assertive' | 'polite'
|
|
67
|
+
theme?: 'light' | 'dark' | 'system' | ToastDesignTokens
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface ToastItem {
|
|
71
|
+
id: string
|
|
72
|
+
message: string | VNode
|
|
73
|
+
options: Required<Omit<ToastOptions, 'component' | 'componentProps' | 'icon' | 'action' | 'undo' | 'theme' | 'position'>> & {
|
|
74
|
+
position?: ToastPosition
|
|
75
|
+
component?: Component
|
|
76
|
+
componentProps?: Record<string, unknown>
|
|
77
|
+
icon?: Component | string | false
|
|
78
|
+
action?: ToastAction
|
|
79
|
+
undo?: ToastUndo
|
|
80
|
+
theme?: 'light' | 'dark' | 'system' | ToastDesignTokens
|
|
81
|
+
}
|
|
82
|
+
createdAt: number
|
|
83
|
+
remaining: Ref<number>
|
|
84
|
+
isPaused: Ref<boolean>
|
|
85
|
+
groupCount: Ref<number>
|
|
86
|
+
isGrouped: Ref<boolean>
|
|
87
|
+
pause(): void
|
|
88
|
+
resume(): void
|
|
89
|
+
dismiss(): void
|
|
90
|
+
update(opts: Partial<ToastOptions>): void
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface PromiseToastMessages<T = unknown> {
|
|
94
|
+
loading: string
|
|
95
|
+
success: string | ((data: T) => string)
|
|
96
|
+
error: string | ((err: unknown) => string)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface ToastContext {
|
|
100
|
+
queue: import('./ToastQueue').ToastQueue
|
|
101
|
+
addToast(message: string | VNode, options?: ToastOptions): string
|
|
102
|
+
dismiss(id?: string): void
|
|
103
|
+
update(id: string, options: Partial<ToastOptions>): void
|
|
104
|
+
isActive(id: string): boolean
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface GlobalToastOptions {
|
|
108
|
+
position?: ToastPosition
|
|
109
|
+
maxVisible?: number
|
|
110
|
+
duration?: number
|
|
111
|
+
theme?: 'light' | 'dark' | 'system'
|
|
112
|
+
ignoreSSR?: boolean
|
|
113
|
+
pauseOnHover?: boolean
|
|
114
|
+
pauseOnFocusLoss?: boolean
|
|
115
|
+
closable?: boolean
|
|
116
|
+
/** Max toasts added within rateLimitWindowMs before extras are dropped. */
|
|
117
|
+
rateLimit?: number
|
|
118
|
+
/** Window in ms for rateLimit (default: 1000). */
|
|
119
|
+
rateLimitWindowMs?: number
|
|
120
|
+
/** Enable automatic localStorage persist/restore for toasts with persist:true. */
|
|
121
|
+
persistStorage?: boolean
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export const PRIORITY_ORDER: Record<ToastPriority, number> = {
|
|
125
|
+
critical: 3,
|
|
126
|
+
high: 2,
|
|
127
|
+
normal: 1,
|
|
128
|
+
low: 0,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export const DEFAULT_OPTIONS: Required<Omit<ToastOptions, 'id' | 'component' | 'componentProps' | 'icon' | 'action' | 'undo' | 'groupKey' | 'theme' | 'onClose' | 'onAutoClose' | 'ariaLive' | 'position'>> = {
|
|
132
|
+
type: 'info',
|
|
133
|
+
priority: 'normal',
|
|
134
|
+
duration: 4000,
|
|
135
|
+
closable: true,
|
|
136
|
+
pauseOnHover: true,
|
|
137
|
+
pauseOnFocusLoss: true,
|
|
138
|
+
swipeToDismiss: true,
|
|
139
|
+
persist: false,
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export const TOAST_CONTEXT_KEY = Symbol('vue-toast-kit-context')
|
package/src/env.d.ts
ADDED
package/src/index.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Styles
|
|
2
|
+
import './styles/tokens.css'
|
|
3
|
+
import './styles/base.css'
|
|
4
|
+
import './styles/animations.css'
|
|
5
|
+
import './styles/themes/light.css'
|
|
6
|
+
import './styles/themes/dark.css'
|
|
7
|
+
import './styles/themes/system.css'
|
|
8
|
+
|
|
9
|
+
// Core types
|
|
10
|
+
export type {
|
|
11
|
+
ToastType,
|
|
12
|
+
ToastPriority,
|
|
13
|
+
ToastPosition,
|
|
14
|
+
ToastOptions,
|
|
15
|
+
ToastItem,
|
|
16
|
+
ToastAction,
|
|
17
|
+
ToastUndo,
|
|
18
|
+
PromiseToastMessages,
|
|
19
|
+
ToastContext,
|
|
20
|
+
GlobalToastOptions,
|
|
21
|
+
ToastDesignTokens,
|
|
22
|
+
} from './core/types'
|
|
23
|
+
|
|
24
|
+
export { TOAST_CONTEXT_KEY, PRIORITY_ORDER, DEFAULT_OPTIONS } from './core/types'
|
|
25
|
+
|
|
26
|
+
// Core classes (for advanced use)
|
|
27
|
+
export { ToastQueue } from './core/ToastQueue'
|
|
28
|
+
export { UndoTimer } from './core/UndoTimer'
|
|
29
|
+
export { GroupManager } from './core/GroupManager'
|
|
30
|
+
export { globalBuffer, isServer } from './core/ToastBuffer'
|
|
31
|
+
|
|
32
|
+
// Composables
|
|
33
|
+
export { useToast, toast } from './composables/useToast'
|
|
34
|
+
export type { ToastApi } from './composables/useToast'
|
|
35
|
+
export { useToastState } from './composables/useToastState'
|
|
36
|
+
export {
|
|
37
|
+
useToastContext,
|
|
38
|
+
createToastContext,
|
|
39
|
+
getOrCreateGlobalContext,
|
|
40
|
+
} from './composables/useToastContext'
|
|
41
|
+
|
|
42
|
+
// Components
|
|
43
|
+
export { default as ToastContainer } from './components/ToastContainer.vue'
|
|
44
|
+
export { default as Toast } from './components/Toast.vue'
|
|
45
|
+
export { default as ToastIcon } from './components/ToastIcon.vue'
|
|
46
|
+
export { default as ToastProgressBar } from './components/ToastProgressBar.vue'
|
|
47
|
+
export { default as ToastActions } from './components/ToastActions.vue'
|
|
48
|
+
|
|
49
|
+
// Plugin
|
|
50
|
+
export { VueToastPlugin } from './plugin'
|
|
51
|
+
export type { VueToastPluginOptions } from './plugin'
|