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.
Files changed (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1145 -0
  3. package/dist/composables/useToast.d.ts +28 -0
  4. package/dist/composables/useToastContext.d.ts +6 -0
  5. package/dist/composables/useToastState.d.ts +7 -0
  6. package/dist/core/GroupManager.d.ts +16 -0
  7. package/dist/core/ToastBuffer.d.ts +19 -0
  8. package/dist/core/ToastQueue.d.ts +55 -0
  9. package/dist/core/UndoTimer.d.ts +23 -0
  10. package/dist/core/types.d.ts +114 -0
  11. package/dist/index.d.ts +23 -0
  12. package/dist/module.d.ts +1 -0
  13. package/dist/nuxt/module.cjs +2 -0
  14. package/dist/nuxt/module.cjs.map +1 -0
  15. package/dist/nuxt/module.d.ts +1 -0
  16. package/dist/nuxt/module.js +34 -0
  17. package/dist/nuxt/module.js.map +1 -0
  18. package/dist/plugin.d.ts +6 -0
  19. package/dist/style.css +1 -0
  20. package/dist/testing.d.ts +14 -0
  21. package/dist/vue-toast-kit.cjs +2 -0
  22. package/dist/vue-toast-kit.cjs.map +1 -0
  23. package/dist/vue-toast-kit.d.ts +540 -0
  24. package/dist/vue-toast-kit.js +1000 -0
  25. package/dist/vue-toast-kit.js.map +1 -0
  26. package/package.json +89 -0
  27. package/src/components/Toast.vue +222 -0
  28. package/src/components/ToastActions.vue +34 -0
  29. package/src/components/ToastContainer.vue +257 -0
  30. package/src/components/ToastIcon.vue +53 -0
  31. package/src/components/ToastProgressBar.vue +18 -0
  32. package/src/composables/useToast.ts +152 -0
  33. package/src/composables/useToastContext.ts +63 -0
  34. package/src/composables/useToastState.ts +18 -0
  35. package/src/core/GroupManager.ts +105 -0
  36. package/src/core/ToastBuffer.ts +45 -0
  37. package/src/core/ToastQueue.ts +377 -0
  38. package/src/core/UndoTimer.ts +90 -0
  39. package/src/core/types.ts +142 -0
  40. package/src/env.d.ts +7 -0
  41. package/src/index.ts +51 -0
  42. package/src/nuxt/composables.ts +13 -0
  43. package/src/nuxt/module.ts +52 -0
  44. package/src/nuxt/plugin.ts +8 -0
  45. package/src/plugin.ts +18 -0
  46. package/src/styles/animations.css +106 -0
  47. package/src/styles/base.css +201 -0
  48. package/src/styles/themes/dark.css +30 -0
  49. package/src/styles/themes/light.css +30 -0
  50. package/src/styles/themes/system.css +32 -0
  51. package/src/styles/tokens.css +74 -0
  52. package/src/testing.ts +81 -0
package/README.md ADDED
@@ -0,0 +1,1145 @@
1
+ <div align="center" style="background:#111827;border-radius:20px;padding:28px 20px 20px;margin-bottom:32px">
2
+ <h1 style="color:#f9fafb;margin:0 0 32px;font-size:2.2em;letter-spacing:-0.03em;font-weight:700;font-family:sans-serif">
3
+ vue-toast-kit
4
+ </h1>
5
+ <img
6
+ src="https://s3.twcstorage.ru/c9a2cc89-780f97fd-311d-4a1a-b86f-c25665c9dc46/images/npm/vue-toast-kit.webp"
7
+ alt="vue-virtual-scroller-kit"
8
+ style="max-width:100%;width:auto;height:300px;border-radius:12px"
9
+ />
10
+ </div>
11
+
12
+ Promise-API with auto type switching, priority queue with preemption, undo-actions with progress timer, toast grouping, headless mode, and a full design system via CSS custom properties — all with a single peer dependency (Vue 3).
13
+
14
+ ---
15
+
16
+ ## Contents
17
+
18
+ - [Features](#features)
19
+ - [Installation](#installation)
20
+ - [Demo](#demo)
21
+ - [Quick start — Vue 3](#quick-start--vue-3)
22
+ - [Quick start — Nuxt 3](#quick-start--nuxt-3)
23
+ - [useToast](#usetoast)
24
+ - [toast.promise](#toastpromise)
25
+ - [toast.undo](#toastundo)
26
+ - [Toast grouping](#toast-grouping)
27
+ - [ToastContainer](#toastcontainer)
28
+ - [useToastState — headless mode](#usetoaststate--headless-mode)
29
+ - [createToastContext — multi-instance](#createtoastcontext--multi-instance)
30
+ - [Stack mode (Sonner-style)](#stack-mode-sonner-style)
31
+ - [Event emitter](#event-emitter)
32
+ - [Rate limiting & localStorage persist](#rate-limiting--localstorage-persist)
33
+ - [Testing utilities](#testing-utilities)
34
+ - [Design System](#design-system)
35
+ - [Vue plugin](#vue-plugin)
36
+ - [Nuxt module](#nuxt-module)
37
+ - [TypeScript types](#typescript-types)
38
+ - [SSR compatibility](#ssr-compatibility)
39
+ - [Architecture](#architecture)
40
+ - [Bundle size & peer dependencies](#bundle-size--peer-dependencies)
41
+ - [Migration from vue-toastification / vue-sonner](#migration-from-vue-toastification--vue-sonner)
42
+
43
+ ---
44
+
45
+ ## Features
46
+
47
+ - **Promise API** — `toast.promise(promise, messages)` automatically switches `loading → success / error` based on the result; returns the original promise unmodified
48
+ - **Priority queue** — four levels (`critical / high / normal / low`); when the visible limit is reached, high-priority toasts preempt low-priority ones; the preempted toast moves to a pending queue and reappears when space frees up
49
+ - **Undo actions** — `toast.undo(message, { undo: { onUndo, duration } })` renders a progress-bar timer; clicking "Undo" calls the callback and closes the toast; when the timer expires the action is confirmed silently
50
+ - **Grouping** — toasts with the same `groupKey` are stacked into one with a `+N` counter; clicking the counter expands the group
51
+ - **Headless mode** — `useToastState()` returns raw reactive queue data; render with any UI framework or fully custom markup
52
+ - **Multi-instance** — `createToastContext()` produces an isolated queue; pass it to `useToast(ctx)` and `<ToastContainer :context="ctx" />` for micro-frontends or scoped notification zones
53
+ - **Design System** — 30+ CSS custom properties (`--vtk-*`) covering colors, typography, shape, shadows, animations, and z-index; three built-in themes (`light`, `dark`, `system`); inline token override via the `theme` prop on `<ToastContainer>`
54
+ - **SSR-safe** — core has no browser API; toasts fired before `<ToastContainer>` mounts are buffered and flushed after mount (100 ms delay)
55
+ - **Accessibility** — `role="alert"` for `error / critical`, `role="status"` for others; `aria-live="assertive"` for critical; `Escape` closes the focused toast; focus returns to the previously active element on dismiss
56
+ - **Touch support** — swipe left or right to dismiss (configurable 40 % threshold)
57
+ - **RTL support** — CSS logical properties (`margin-inline-start`, `padding-inline`) adapt the layout automatically when `dir="rtl"` is set on `<html>`
58
+ - **Pause on hover / focus loss** — timers freeze automatically; `visibilitychange` stops all timers when the tab goes to the background
59
+ - **Animations** — CSS-only slide + fade per position; `prefers-reduced-motion` degrades to fade-only
60
+ - **Vue Plugin + Nuxt Module** — `app.use(VueToastPlugin)` for Vue 3; `modules: ['vue-toast-kit/nuxt']` for Nuxt 3 with auto-imports
61
+ - **Zero external runtime dependencies** — only Vue 3 as peer dep; full ESM + CJS, tree-shakeable
62
+
63
+ ---
64
+
65
+ ## Installation
66
+
67
+ ```bash
68
+ npm install vue-toast-kit
69
+ ```
70
+
71
+ Peer dependency:
72
+
73
+ ```bash
74
+ npm install vue@>=3.3
75
+ ```
76
+
77
+ ---
78
+
79
+ ## Demo
80
+
81
+ An interactive demo application is included in the `demo/` directory covering every feature in a tabbed interface.
82
+
83
+ ```bash
84
+ git clone https://github.com/macrulezru/vue-toast-kit.git
85
+ cd vue-toast-kit
86
+ npm install
87
+ npm run demo
88
+ ```
89
+
90
+ `npm run demo` installs demo dependencies automatically and starts the dev server.
91
+ Opens `http://localhost:5173`.
92
+
93
+ | Script | Description |
94
+ |---|---|
95
+ | `npm run demo` | Install demo deps (if needed) + start dev server |
96
+ | `npm run demo:dev` | Start dev server only (deps already installed) |
97
+ | `npm run demo:build` | Build demo for production |
98
+
99
+ | Tab | What it shows |
100
+ |---|---|
101
+ | 🔔 Basic | All toast types, positions, priorities, action buttons |
102
+ | ⚡ Promise | `toast.promise()` with resolve / reject simulation |
103
+ | ↩️ Undo | `toast.undo()` with progress timer, event log |
104
+ | 📦 Group | Stacking toasts by `groupKey`, expand on click |
105
+ | 🎨 Headless | `useToastState()` with fully custom render — zero package styles |
106
+ | 🔀 Multi-instance | `createToastContext()` — two isolated zones on one page |
107
+ | 🎨 Design System | Live CSS token editor with preset themes and CSS export |
108
+ | ✨ Animations | All 6 positions, animated grid picker |
109
+
110
+ ---
111
+
112
+ ## Quick start — Vue 3
113
+
114
+ **1. Register the plugin**
115
+
116
+ ```ts
117
+ // main.ts
118
+ import { createApp } from 'vue'
119
+ import { VueToastPlugin } from 'vue-toast-kit'
120
+ import 'vue-toast-kit/style'
121
+ import App from './App.vue'
122
+
123
+ const app = createApp(App)
124
+ app.use(VueToastPlugin, { position: 'bottom-right', theme: 'system' })
125
+ app.mount('#app')
126
+ ```
127
+
128
+ **2. Add the container**
129
+
130
+ ```vue
131
+ <!-- App.vue -->
132
+ <template>
133
+ <RouterView />
134
+ <ToastContainer />
135
+ </template>
136
+ ```
137
+
138
+ `<ToastContainer>` is registered globally by the plugin. No import needed.
139
+
140
+ **3. Fire toasts from anywhere**
141
+
142
+ ```vue
143
+ <script setup lang="ts">
144
+ import { useToast } from 'vue-toast-kit'
145
+
146
+ const toast = useToast()
147
+ </script>
148
+
149
+ <template>
150
+ <button @click="toast.success('Saved!')">Save</button>
151
+ <button @click="toast.error('Something went wrong')">Fail</button>
152
+ </template>
153
+ ```
154
+
155
+ Or use the named singleton outside components (Pinia stores, axios interceptors, route guards):
156
+
157
+ ```ts
158
+ import { toast } from 'vue-toast-kit'
159
+
160
+ axios.interceptors.response.use(null, (err) => {
161
+ toast.error(`Network error: ${err.message}`)
162
+ return Promise.reject(err)
163
+ })
164
+ ```
165
+
166
+ ---
167
+
168
+ ## Quick start — Nuxt 3
169
+
170
+ **1. Add the module**
171
+
172
+ ```ts
173
+ // nuxt.config.ts
174
+ export default defineNuxtConfig({
175
+ modules: ['vue-toast-kit/nuxt'],
176
+ vueToastKit: {
177
+ position: 'top-right',
178
+ theme: 'system',
179
+ maxVisible: 5,
180
+ },
181
+ })
182
+ ```
183
+
184
+ **2. Add the container to your layout**
185
+
186
+ ```vue
187
+ <!-- layouts/default.vue -->
188
+ <template>
189
+ <div>
190
+ <slot />
191
+ <ToastContainer /> <!-- auto-imported -->
192
+ </div>
193
+ </template>
194
+ ```
195
+
196
+ **3. Use in pages and composables**
197
+
198
+ ```vue
199
+ <script setup lang="ts">
200
+ // useToast and toast are auto-imported — no explicit import needed
201
+ const toast = useToast()
202
+
203
+ async function save() {
204
+ await toast.promise(
205
+ $fetch('/api/save', { method: 'POST', body: form }),
206
+ { loading: 'Saving…', success: 'Saved!', error: (e) => e.message },
207
+ )
208
+ }
209
+ </script>
210
+ ```
211
+
212
+ ---
213
+
214
+ ## useToast
215
+
216
+ The main composable. Returns a `ToastApi` object. Works inside and outside Vue components.
217
+
218
+ ```ts
219
+ const toast = useToast(context?: ToastContext): ToastApi
220
+ ```
221
+
222
+ When called without arguments inside a component, it uses the injected context (set up by the plugin). When called outside a component it falls back to the global singleton. Pass a `ToastContext` from `createToastContext()` to use an isolated queue.
223
+
224
+ ### Methods
225
+
226
+ | Method | Signature | Description |
227
+ |---|---|---|
228
+ | `toast()` | `(message, options?) → id` | Show an `info` toast |
229
+ | `toast.success()` | `(message, options?) → id` | Show a `success` toast |
230
+ | `toast.error()` | `(message, options?) → id` | Show an `error` toast (priority: `high` by default) |
231
+ | `toast.warning()` | `(message, options?) → id` | Show a `warning` toast |
232
+ | `toast.info()` | `(message, options?) → id` | Show an `info` toast |
233
+ | `toast.loading()` | `(message, options?) → id` | Show a `loading` toast (no auto-dismiss, not closable by default) |
234
+ | `toast.custom()` | `(component, options?) → id` | Replace the toast body with a Vue component |
235
+ | `toast.promise()` | `(promise, messages, options?) → Promise` | See [toast.promise](#toastpromise) |
236
+ | `toast.undo()` | `(message, options) → id` | See [toast.undo](#toastundo) |
237
+ | `toast.update()` | `(id, partial) → void` | Merge options (and optionally the message) into an existing toast |
238
+ | `toast.updateMessage()` | `(id, message) → void` | Update only the message text without touching options |
239
+ | `toast.dismiss()` | `(id?) → void` | Close a toast by id; omit id to close all |
240
+ | `toast.dismissAll()` | `(position?) → void` | Close all toasts, optionally filtered by position |
241
+ | `toast.isActive()` | `(id) → boolean` | Check if a toast is still visible |
242
+ | `toast.pauseAll()` | `() → void` | Pause all timers |
243
+ | `toast.resumeAll()` | `() → void` | Resume all timers |
244
+
245
+ ### ToastOptions
246
+
247
+ | Option | Type | Default | Description |
248
+ |---|---|---|---|
249
+ | `id` | `string` | auto | Unique id; if the same id is already active the toast is updated |
250
+ | `type` | `ToastType` | `'info'` | Visual style; one of `info / success / warning / error / loading / custom` |
251
+ | `priority` | `ToastPriority` | `'normal'` | Queue priority; one of `critical / high / normal / low` |
252
+ | `duration` | `number` | `4000` | Auto-dismiss delay in ms; `0` = sticky (never auto-closes) |
253
+ | `position` | `ToastPosition` | container default | Render this toast at a specific position, regardless of the container's `position` prop |
254
+ | `closable` | `boolean` | `true` | Show the close button |
255
+ | `groupKey` | `string` | — | Group toasts with the same key into a stack |
256
+ | `icon` | `Component \| string \| false` | type default | SVG component, emoji string, or `false` to hide |
257
+ | `action` | `{ label, onClick }` | — | Extra action button inside the toast |
258
+ | `undo` | `{ label?, onUndo, duration? }` | — | Undo button with timer; see [toast.undo](#toastundo) |
259
+ | `onClose` | `() => void` | — | Called when the toast is closed (any reason) |
260
+ | `onAutoClose` | `() => void` | — | Called only when the timer expires |
261
+ | `pauseOnHover` | `boolean` | `true` | Pause the timer on mouse enter |
262
+ | `pauseOnFocusLoss` | `boolean` | `true` | Pause the timer when the tab goes to background |
263
+ | `swipeToDismiss` | `boolean` | `true` | Enable swipe left / right to dismiss on touch devices |
264
+ | `persist` | `boolean` | `false` | Restore from `localStorage` after reload (only for toasts without callbacks) |
265
+ | `component` | `Component` | — | Replace the entire toast body with a Vue component |
266
+ | `componentProps` | `Record<string, unknown>` | — | Props forwarded to `component` |
267
+ | `ariaLive` | `'assertive' \| 'polite'` | auto | Override the automatic aria-live value |
268
+ | `theme` | `'light' \| 'dark' \| 'system' \| ToastDesignTokens` | — | Per-toast theme or token overrides |
269
+
270
+ ### Examples
271
+
272
+ **All toast types:**
273
+
274
+ ```ts
275
+ toast.info('Sync complete')
276
+ toast.success('File uploaded')
277
+ toast.warning('Disk almost full (92 %)')
278
+ toast.error('Connection refused')
279
+ toast.loading('Fetching data…')
280
+ ```
281
+
282
+ **Custom duration and position:**
283
+
284
+ ```ts
285
+ toast.success('Copied to clipboard', {
286
+ duration: 2000,
287
+ position: 'top-center',
288
+ })
289
+ ```
290
+
291
+ **With an action button:**
292
+
293
+ ```ts
294
+ toast.info('New message from Alex', {
295
+ action: {
296
+ label: 'Open',
297
+ onClick: () => router.push('/messages'),
298
+ },
299
+ })
300
+ ```
301
+
302
+ **Emoji icon:**
303
+
304
+ ```ts
305
+ toast.success('Backup complete', { icon: '💾' })
306
+ ```
307
+
308
+ **Sticky until manually dismissed:**
309
+
310
+ ```ts
311
+ const id = toast.error('Server is down', { duration: 0, closable: true })
312
+ // Later:
313
+ toast.dismiss(id)
314
+ ```
315
+
316
+ **Update an existing toast:**
317
+
318
+ ```ts
319
+ const id = toast.loading('Uploading…')
320
+ // Update message only (no option changes):
321
+ toast.updateMessage(id, 'Processing…')
322
+ // Or update message + options together:
323
+ toast.update(id, { message: 'Almost done…', duration: 3000 })
324
+ ```
325
+
326
+ **Rich content via Vue component:**
327
+
328
+ ```ts
329
+ import RichCard from './RichCard.vue'
330
+
331
+ toast.custom(RichCard, {
332
+ componentProps: { title: 'Hello', body: 'World' },
333
+ duration: 0,
334
+ closable: true,
335
+ })
336
+ ```
337
+
338
+ ---
339
+
340
+ ## toast.promise
341
+
342
+ Automatically switches a `loading` toast to `success` or `error` based on the promise result. Returns the original promise so you can `await` it.
343
+
344
+ ```ts
345
+ toast.promise<T>(
346
+ promise: Promise<T>,
347
+ messages: PromiseToastMessages<T>,
348
+ options?: ToastOptions,
349
+ ): Promise<T>
350
+ ```
351
+
352
+ ### PromiseToastMessages
353
+
354
+ | Field | Type | Description |
355
+ |---|---|---|
356
+ | `loading` | `string` | Message while the promise is pending |
357
+ | `success` | `string \| (data: T) => string` | Message on resolve; receives the resolved value |
358
+ | `error` | `string \| (err: unknown) => string` | Message on reject; receives the error |
359
+
360
+ ### Examples
361
+
362
+ **Static messages:**
363
+
364
+ ```ts
365
+ await toast.promise(
366
+ fetch('/api/deploy').then(r => r.json()),
367
+ {
368
+ loading: 'Deploying…',
369
+ success: 'Deployed successfully!',
370
+ error: 'Deployment failed',
371
+ },
372
+ )
373
+ ```
374
+
375
+ **Dynamic messages from data / error:**
376
+
377
+ ```ts
378
+ const user = await toast.promise(
379
+ fetchUser(id),
380
+ {
381
+ loading: 'Loading user…',
382
+ success: (u) => `Welcome, ${u.name}!`,
383
+ error: (e) => `Could not load user: ${(e as Error).message}`,
384
+ },
385
+ )
386
+ ```
387
+
388
+ **In a Pinia action:**
389
+
390
+ ```ts
391
+ // stores/files.ts
392
+ import { toast } from 'vue-toast-kit'
393
+
394
+ export const useFileStore = defineStore('files', {
395
+ actions: {
396
+ async upload(file: File) {
397
+ return toast.promise(
398
+ uploadAPI(file),
399
+ {
400
+ loading: `Uploading ${file.name}…`,
401
+ success: (res) => `${res.name} uploaded (${res.size} KB)`,
402
+ error: (e) => `Upload failed: ${(e as Error).message}`,
403
+ },
404
+ )
405
+ },
406
+ },
407
+ })
408
+ ```
409
+
410
+ The promise `reject` is re-thrown after updating the toast, so your `try / catch` or `.catch()` still fires normally.
411
+
412
+ ---
413
+
414
+ ## toast.undo
415
+
416
+ Creates a toast with a countdown progress bar. When the user clicks the undo button, `onUndo()` is called and the toast closes immediately. When the timer runs out, the toast closes silently (action confirmed).
417
+
418
+ ```ts
419
+ toast.undo(message: string, options: ToastOptions & {
420
+ undo: {
421
+ onUndo: () => void | Promise<void>
422
+ label?: string // default: 'Отменить'
423
+ duration?: number // ms, default: 5000
424
+ }
425
+ }): string
426
+ ```
427
+
428
+ ### Examples
429
+
430
+ **Delete with undo:**
431
+
432
+ ```ts
433
+ function deleteFile(id: string) {
434
+ markForDeletion(id)
435
+
436
+ toast.undo(`File "${fileName}" deleted`, {
437
+ undo: {
438
+ label: 'Restore',
439
+ duration: 6000,
440
+ onUndo: () => {
441
+ restoreFile(id)
442
+ toast.success('File restored')
443
+ },
444
+ },
445
+ onAutoClose: () => permanentlyDelete(id),
446
+ })
447
+ }
448
+ ```
449
+
450
+ **Archive email:**
451
+
452
+ ```ts
453
+ toast.undo('Email archived', {
454
+ icon: '📨',
455
+ undo: {
456
+ onUndo: () => moveToInbox(emailId),
457
+ },
458
+ })
459
+ ```
460
+
461
+ **Async undo:**
462
+
463
+ ```ts
464
+ toast.undo('Record deleted', {
465
+ undo: {
466
+ onUndo: async () => {
467
+ await api.restore(recordId)
468
+ toast.success('Record restored!')
469
+ },
470
+ },
471
+ })
472
+ ```
473
+
474
+ ---
475
+
476
+ ## Toast grouping
477
+
478
+ Toasts with the same `groupKey` are collapsed into a single toast with a `+N` counter. Clicking the counter toggles the expanded state.
479
+
480
+ ```ts
481
+ // All three calls produce one visible toast with "+2"
482
+ toast.info('New message from Alice', { groupKey: 'messages' })
483
+ toast.info('New message from Bob', { groupKey: 'messages' })
484
+ toast.info('New message from Carol', { groupKey: 'messages' })
485
+ ```
486
+
487
+ The leader toast (first in the group) stays visible; subsequent toasts are hidden but tracked. When the leader is dismissed, the next toast becomes the leader automatically.
488
+
489
+ ### Grouping options
490
+
491
+ | Option | Behaviour |
492
+ |---|---|
493
+ | `groupKey: 'my-key'` | Enable grouping for this toast |
494
+ | No `groupKey` | Toast is always shown individually |
495
+
496
+ ---
497
+
498
+ ## ToastContainer
499
+
500
+ The Vue component that renders toasts. Place it once in `App.vue` (or in your Nuxt layout). Uses `<Teleport to="body">` internally.
501
+
502
+ ```vue
503
+ <ToastContainer
504
+ position="bottom-right"
505
+ :max-visible="5"
506
+ :gap="8"
507
+ :offset-x="16"
508
+ :offset-y="16"
509
+ :z-index="9999"
510
+ theme="system"
511
+ />
512
+ ```
513
+
514
+ ### Props
515
+
516
+ | Prop | Type | Default | Description |
517
+ |---|---|---|---|
518
+ | `position` | `ToastPosition` | `'bottom-right'` | Default container position (per-toast `position` option overrides this) |
519
+ | `maxVisible` | `number` | `5` | Maximum number of toasts shown at once; extras wait in a pending queue |
520
+ | `gap` | `number` | `8` | Vertical gap between toasts in pixels |
521
+ | `offsetX` | `number` | `16` | Horizontal distance from the screen edge in pixels |
522
+ | `offsetY` | `number` | `16` | Vertical distance from the screen edge in pixels |
523
+ | `zIndex` | `number` | `9999` | CSS z-index of the container |
524
+ | `expand` | `boolean` | `false` | Expand all groups immediately (skip collapsed state) |
525
+ | `teleportTo` | `string` | `'body'` | CSS selector passed to `<Teleport>` |
526
+ | `context` | `ToastContext` | global | Pass an isolated context from `createToastContext()` |
527
+ | `theme` | `'light' \| 'dark' \| 'system' \| ToastDesignTokens` | — | Theme name or inline token overrides |
528
+ | `stackMode` | `boolean` | `false` | Sonner-style stack: inactive toasts collapse behind the front one; hover expands |
529
+
530
+ ### Multiple containers
531
+
532
+ A single `<ToastContainer>` handles all positions automatically — each toast is rendered at its own `position` option, falling back to the container's `position` prop:
533
+
534
+ ```ts
535
+ toast.success('Saved', { position: 'top-right' })
536
+ toast.error('Failed', { position: 'bottom-center' })
537
+ // Both appear in their respective corners from one <ToastContainer>
538
+ ```
539
+
540
+ For fully isolated queues (separate notification zones), use `createToastContext()` with a dedicated container:
541
+
542
+ ```vue
543
+ <template>
544
+ <!-- Default global queue — bottom right -->
545
+ <ToastContainer position="bottom-right" />
546
+
547
+ <!-- Critical alerts — top center, separate queue -->
548
+ <ToastContainer position="top-center" :context="alertCtx" :z-index="10000" />
549
+ </template>
550
+
551
+ <script setup lang="ts">
552
+ import { createToastContext, useToast } from 'vue-toast-kit'
553
+ const alertCtx = createToastContext()
554
+ const alertToast = useToast(alertCtx)
555
+ </script>
556
+ ```
557
+
558
+ ### Slots
559
+
560
+ Override any part of the toast without losing the queue logic:
561
+
562
+ ```vue
563
+ <ToastContainer>
564
+ <!-- Replace the entire toast -->
565
+ <template #toast="{ toast, dismiss }">
566
+ <MyCustomToast :data="toast" @close="dismiss(toast.id)" />
567
+ </template>
568
+ </ToastContainer>
569
+ ```
570
+
571
+ | Slot | Props | Description |
572
+ |---|---|---|
573
+ | `#toast` | `{ toast, dismiss }` | Full replacement of one toast (skips all sub-slots) |
574
+ | `#toast-icon` | `{ toast }` | Replace the icon only; falls back to `ToastIcon` |
575
+ | `#toast-content` | `{ toast }` | Replace the entire message + actions area |
576
+ | `#toast-action` | `{ toast }` | Replace the action / undo buttons; falls back to `ToastActions` |
577
+ | `#toast-close` | `{ toast, dismiss }` | Replace the close button |
578
+ | `#toast-undo` | `{ toast, remaining }` | Replace the progress bar at the bottom of the toast |
579
+
580
+ ---
581
+
582
+ ## useToastState — headless mode
583
+
584
+ Returns raw reactive data from the queue. Use it to build a completely custom notification UI — `<ToastContainer>` is not needed.
585
+
586
+ ```ts
587
+ const { active, pending, count, has } = useToastState(context?: ToastContext)
588
+ ```
589
+
590
+ ### Return value
591
+
592
+ | Property | Type | Description |
593
+ |---|---|---|
594
+ | `active` | `ComputedRef<ToastItem[]>` | Currently visible toasts (excluding hidden grouped ones) |
595
+ | `pending` | `ComputedRef<ToastItem[]>` | Toasts waiting for a slot |
596
+ | `count` | `ComputedRef<number>` | `active.value.length` |
597
+ | `has(id)` | `(id: string) → boolean` | Check if a toast is active |
598
+
599
+ ### Example — fully custom render
600
+
601
+ ```vue
602
+ <script setup lang="ts">
603
+ import { useToast, useToastState } from 'vue-toast-kit'
604
+
605
+ const toast = useToast()
606
+ const { active } = useToastState()
607
+ </script>
608
+
609
+ <template>
610
+ <!-- No <ToastContainer> — render entirely from scratch -->
611
+ <div class="my-notifications">
612
+ <div
613
+ v-for="t in active"
614
+ :key="t.id"
615
+ :class="`notification notification--${t.options.type}`"
616
+ @mouseenter="t.pause()"
617
+ @mouseleave="t.resume()"
618
+ >
619
+ <span>{{ t.message }}</span>
620
+ <button @click="t.dismiss()">✕</button>
621
+ <div class="progress" :style="{ width: `${t.remaining.value * 100}%` }" />
622
+ </div>
623
+ </div>
624
+ </template>
625
+ ```
626
+
627
+ Each `ToastItem` in `active` is fully reactive:
628
+
629
+ | Property / Method | Type | Description |
630
+ |---|---|---|
631
+ | `id` | `string` | Unique id |
632
+ | `message` | `string \| VNode` | Toast content |
633
+ | `options` | `ToastOptions` (required) | Merged options |
634
+ | `createdAt` | `number` | `Date.now()` at creation |
635
+ | `remaining` | `Ref<number>` | 0–1, fraction of timer remaining |
636
+ | `isPaused` | `Ref<boolean>` | Timer is paused |
637
+ | `groupCount` | `Ref<number>` | 1 normally; >1 when grouping is active |
638
+ | `pause()` | `() → void` | Pause the timer |
639
+ | `resume()` | `() → void` | Resume the timer |
640
+ | `dismiss()` | `() → void` | Close the toast |
641
+ | `update(opts)` | `(Partial<ToastOptions>) → void` | Merge new options |
642
+
643
+ ---
644
+
645
+ ## createToastContext — multi-instance
646
+
647
+ Creates an isolated queue instance. Pass it to `useToast(ctx)` and `<ToastContainer :context="ctx" />` to completely separate the notification scope from the global one.
648
+
649
+ ```ts
650
+ const ctx = createToastContext(options?: GlobalToastOptions): ToastContext
651
+ ```
652
+
653
+ Use cases:
654
+ - Micro-frontend shells where each MFE manages its own notifications
655
+ - Modal dialogs with local status toasts that must not interfere with the app-level queue
656
+ - Multiple separate notification zones on one page
657
+
658
+ ### Example
659
+
660
+ ```vue
661
+ <script setup lang="ts">
662
+ import { createToastContext, useToast, ToastContainer } from 'vue-toast-kit'
663
+
664
+ const modalCtx = createToastContext({ maxVisible: 3 })
665
+ const modalToast = useToast(modalCtx)
666
+
667
+ function save() {
668
+ modalToast.success('Changes saved inside the modal')
669
+ }
670
+ </script>
671
+
672
+ <template>
673
+ <div class="modal">
674
+ <button @click="save">Save</button>
675
+
676
+ <!-- This container only shows toasts from modalCtx -->
677
+ <ToastContainer
678
+ :context="modalCtx"
679
+ position="top-right"
680
+ :z-index="10001"
681
+ />
682
+ </div>
683
+ </template>
684
+ ```
685
+
686
+ ---
687
+
688
+ ## Design System
689
+
690
+ All visual properties are controlled by CSS custom properties (`--vtk-*`). Override them globally in `:root`, scoped to a container, or pass a `ToastDesignTokens` object as the `theme` prop.
691
+
692
+ ### Built-in themes
693
+
694
+ ```vue
695
+ <!-- Light (default) -->
696
+ <ToastContainer theme="light" />
697
+
698
+ <!-- Dark -->
699
+ <ToastContainer theme="dark" />
700
+
701
+ <!-- Follows prefers-color-scheme -->
702
+ <ToastContainer theme="system" />
703
+ ```
704
+
705
+ ### Inline token overrides
706
+
707
+ Pass a `ToastDesignTokens` object to override specific tokens without touching global CSS:
708
+
709
+ ```vue
710
+ <ToastContainer
711
+ :theme="{
712
+ colorBg: '#1a1a2e',
713
+ colorText: '#e2e8f0',
714
+ colorSuccess: '#00ff88',
715
+ colorError: '#ff4d6d',
716
+ borderRadius: '16px',
717
+ shadow: '0 8px 32px rgba(0, 0, 0, 0.5)',
718
+ maxWidth: '360px',
719
+ }"
720
+ />
721
+ ```
722
+
723
+ ### Full token reference
724
+
725
+ | Token | CSS Variable | Default (light) | Description |
726
+ |---|---|---|---|
727
+ | `colorBg` | `--vtk-color-bg` | `#ffffff` | Toast background |
728
+ | `colorText` | `--vtk-color-text` | `#1a1a1a` | Primary text color |
729
+ | `colorBorder` | `--vtk-color-border` | `rgba(0,0,0,0.08)` | Border color |
730
+ | `colorSuccess` | `--vtk-color-success` | `#16a34a` | Success accent |
731
+ | `colorError` | `--vtk-color-error` | `#dc2626` | Error accent |
732
+ | `colorWarning` | `--vtk-color-warning` | `#d97706` | Warning accent |
733
+ | `colorInfo` | `--vtk-color-info` | `#2563eb` | Info accent |
734
+ | `colorLoading` | `--vtk-color-loading` | `#7c3aed` | Loading accent |
735
+ | `fontFamily` | `--vtk-font-family` | system-ui | Font stack |
736
+ | `fontSize` | `--vtk-font-size` | `0.875rem` | Base font size |
737
+ | `fontWeight` | `--vtk-font-weight` | `400` | Base font weight |
738
+ | `lineHeight` | `--vtk-line-height` | `1.4` | Line height |
739
+ | `borderRadius` | `--vtk-border-radius` | `10px` | Corner radius |
740
+ | `borderWidth` | `--vtk-border-width` | `1px` | Border width |
741
+ | `shadow` | `--vtk-shadow` | multi-layer | Box shadow |
742
+ | `paddingX` | `--vtk-padding-x` | `1rem` | Horizontal padding |
743
+ | `paddingY` | `--vtk-padding-y` | `0.75rem` | Vertical padding |
744
+ | `iconSize` | `--vtk-icon-size` | `1.25rem` | Icon size |
745
+ | `progressHeight` | `--vtk-progress-height` | `3px` | Progress bar height |
746
+ | `maxWidth` | `--vtk-max-width` | `400px` | Maximum toast width |
747
+ | `minWidth` | `--vtk-min-width` | `280px` | Minimum toast width |
748
+ | `transitionDuration` | `--vtk-transition-duration` | `300ms` | Animation duration |
749
+ | `transitionEasing` | `--vtk-transition-easing` | ease | Animation easing |
750
+ | `zIndex` | `--vtk-z-index` | `9999` | Container z-index |
751
+
752
+ ### Global CSS override
753
+
754
+ ```css
755
+ /* styles/toasts.css */
756
+ :root {
757
+ --vtk-border-radius: 6px;
758
+ --vtk-font-family: 'Inter', sans-serif;
759
+ --vtk-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
760
+ --vtk-max-width: 360px;
761
+ }
762
+ ```
763
+
764
+ ---
765
+
766
+ ## Vue plugin
767
+
768
+ ```ts
769
+ import { VueToastPlugin } from 'vue-toast-kit'
770
+ import 'vue-toast-kit/style'
771
+
772
+ app.use(VueToastPlugin, {
773
+ position: 'bottom-right',
774
+ maxVisible: 5,
775
+ duration: 4000,
776
+ theme: 'system',
777
+ closable: true,
778
+ pauseOnHover: true,
779
+ pauseOnFocusLoss: true,
780
+ registerComponent: true, // auto-register <ToastContainer> globally
781
+ })
782
+ ```
783
+
784
+ ### VueToastPluginOptions
785
+
786
+ | Option | Type | Default | Description |
787
+ |---|---|---|---|
788
+ | `position` | `ToastPosition` | `'bottom-right'` | Default container position |
789
+ | `maxVisible` | `number` | `5` | Default maximum visible toasts |
790
+ | `duration` | `number` | `4000` | Default auto-dismiss duration in ms |
791
+ | `theme` | `'light' \| 'dark' \| 'system'` | — | Default theme for all containers |
792
+ | `closable` | `boolean` | `true` | Show close button by default |
793
+ | `pauseOnHover` | `boolean` | `true` | Pause on hover by default |
794
+ | `pauseOnFocusLoss` | `boolean` | `true` | Pause on tab background by default |
795
+ | `ignoreSSR` | `boolean` | `false` | Disable SSR buffering |
796
+ | `registerComponent` | `boolean` | `true` | Globally register `<ToastContainer>` |
797
+ | `rateLimit` | `number` | — | Max toasts added within `rateLimitWindowMs`; extras are silently dropped |
798
+ | `rateLimitWindowMs` | `number` | `1000` | Window in ms for rate limiting |
799
+ | `persistStorage` | `boolean` | `false` | Enable localStorage persist/restore for toasts with `persist: true` |
800
+
801
+ ---
802
+
803
+ ## Nuxt module
804
+
805
+ ```ts
806
+ // nuxt.config.ts
807
+ export default defineNuxtConfig({
808
+ modules: ['vue-toast-kit/nuxt'],
809
+
810
+ vueToastKit: {
811
+ position: 'bottom-right',
812
+ maxVisible: 5,
813
+ duration: 4000,
814
+ theme: 'system',
815
+ closable: true,
816
+ pauseOnHover: true,
817
+ pauseOnFocusLoss: true,
818
+ registerComponent: true,
819
+ },
820
+ })
821
+ ```
822
+
823
+ The module automatically:
824
+ - Adds the Vue plugin on the client
825
+ - **Auto-imports** `useToast`, `useToastState`, `createToastContext`, and the `toast` singleton — no explicit import needed
826
+ - **Auto-imports** `<ToastContainer>` as a global component (when `registerComponent: true`)
827
+ - Injects `vue-toast-kit/style.css` into the Nuxt CSS pipeline
828
+
829
+ CSS is loaded automatically — no manual import of `vue-toast-kit/style` required in Nuxt.
830
+
831
+ ---
832
+
833
+ ## TypeScript types
834
+
835
+ All public types are exported from the package root:
836
+
837
+ ```ts
838
+ import type {
839
+ ToastType, // 'info' | 'success' | 'warning' | 'error' | 'loading' | 'custom'
840
+ ToastPriority, // 'critical' | 'high' | 'normal' | 'low'
841
+ ToastPosition, // 'top-left' | 'top-center' | 'top-right' | 'bottom-*'
842
+
843
+ ToastOptions, // Full options object
844
+ ToastItem, // Internal reactive toast item (used in headless mode)
845
+ ToastAction, // { label: string; onClick: () => void }
846
+ ToastUndo, // { label?: string; onUndo: () => void | Promise<void>; duration?: number }
847
+ ToastDesignTokens, // All CSS token keys typed
848
+
849
+ PromiseToastMessages,// { loading, success, error }
850
+
851
+ ToastContext, // Isolated queue context
852
+ GlobalToastOptions, // Plugin / module options
853
+
854
+ ToastApi, // Return type of useToast()
855
+ } from 'vue-toast-kit'
856
+ ```
857
+
858
+ **Working with `ToastItem` in headless mode:**
859
+
860
+ ```ts
861
+ import type { ToastItem } from 'vue-toast-kit'
862
+
863
+ function renderCustomToast(t: ToastItem) {
864
+ // t.remaining.value — number 0–1
865
+ // t.isPaused.value — boolean
866
+ // t.groupCount.value — number
867
+ // t.options.type, t.options.priority, etc.
868
+ }
869
+ ```
870
+
871
+ **Typed token override:**
872
+
873
+ ```ts
874
+ import type { ToastDesignTokens } from 'vue-toast-kit'
875
+
876
+ const darkGlass: ToastDesignTokens = {
877
+ colorBg: 'rgba(15, 15, 20, 0.85)',
878
+ colorText: '#f0f0f0',
879
+ borderRadius: '14px',
880
+ shadow: '0 8px 32px rgba(0,0,0,0.6)',
881
+ }
882
+ ```
883
+
884
+ ---
885
+
886
+ ## SSR compatibility
887
+
888
+ | Scenario | Behaviour |
889
+ |---|---|
890
+ | `typeof window === 'undefined'` | `toast()` calls are buffered in `ToastBuffer`; no browser API is touched |
891
+ | `<ToastContainer>` mounts on the client | Buffer is flushed after 100 ms with all pending toasts |
892
+ | `ignoreSSR: true` | Buffer is disabled; SSR-fired toasts are discarded silently |
893
+ | Nuxt hydration | Plugin runs client-side only; SSR render produces no toast HTML |
894
+
895
+ ```ts
896
+ // nuxt.config.ts — disable SSR buffer if you never fire toasts on the server
897
+ vueToastKit: { ignoreSSR: true }
898
+ ```
899
+
900
+ ---
901
+
902
+ ## Architecture
903
+
904
+ ```
905
+ useToast() / toast (singleton)
906
+
907
+ ├── buildToastApi(context)
908
+ │ toast(), toast.success/error/warning/info/loading/custom()
909
+ │ toast.promise() — updates type + restarts timer
910
+ │ toast.undo() — wraps options.undo
911
+ │ toast.dismiss() — proxies to queue.dismiss()
912
+
913
+
914
+ ToastContext
915
+ │ addToast() → isServer ? ToastBuffer : ToastQueue.add()
916
+ │ dismiss() → ToastQueue.dismiss()
917
+ │ update() → ToastQueue.update()
918
+
919
+
920
+ ToastQueue GroupManager
921
+ active: ToastItem[] ◄───────────────────┐
922
+ pending: ToastItem[] add(id, key) │
923
+ timers: Map<id, UndoTimer> remove(id, key)│
924
+ toggleExpand() │
925
+ add() — dedup / preempt / sort pending │
926
+ remove() — free slot, promote from pending│
927
+ update() — merge options │
928
+ dismiss() — calls onClose, remove │
929
+
930
+ UndoTimer │
931
+ setTimeout/setInterval, pause/resume │
932
+ remaining: number (0–1) ────────────────►│ ToastItem.remaining.value
933
+ onExpire: () => queue.remove(id) │
934
+
935
+ ToastBuffer (SSR) │
936
+ push() — store before window exists │
937
+ flush() — replay into queue at mount │
938
+ onFlush() — called by ToastContainer │
939
+
940
+ ToastContainer.vue │
941
+ Teleport → body │
942
+ TransitionGroup (slide + fade per position)│
943
+ hover → queue.pauseAll() / resumeAll() │
944
+ visibilitychange → pause/resume │
945
+ slot: #toast / #toast-icon / … │
946
+ │ │
947
+ └── Toast.vue │
948
+ swipe (touch) │
949
+ aria role + aria-live │
950
+ ToastIcon.vue (SVG + spinner) │
951
+ ToastProgressBar.vue (scaleX) │
952
+ action / undo buttons │
953
+ group counter (click → toggleExpand)│
954
+
955
+ Plugin (VueToastPlugin) Nuxt Module
956
+ app.use() → installContext() defineNuxtModule()
957
+ provide(TOAST_CONTEXT_KEY, ctx) addPlugin(), addImports()
958
+ app.component('ToastContainer', …) addComponent(), css inject
959
+ ```
960
+
961
+ ---
962
+
963
+ ## Bundle size & peer dependencies
964
+
965
+ | Entry point | Size (gzip) | Peer deps |
966
+ |---|---|---|
967
+ | `vue-toast-kit` (JS) | ~9.2 KB | `vue ^3.3` |
968
+ | `vue-toast-kit/style` (CSS) | ~2.4 KB | — |
969
+ | `vue-toast-kit/nuxt` | ~0.6 KB | `vue ^3.3`, `@nuxt/kit` |
970
+
971
+ Ships as tree-shakeable ESM (`vue-toast-kit.js`) and CommonJS (`vue-toast-kit.cjs`).
972
+
973
+ ---
974
+
975
+ ## Stack mode (Sonner-style)
976
+
977
+ Enable `stackMode` on `<ToastContainer>` to collapse multiple toasts into a visual stack. The front toast is fully visible; behind it you see up to 2 ghost cards, slightly scaled and offset. Hovering the container expands them back to the normal stacked list.
978
+
979
+ ```vue
980
+ <ToastContainer :stack-mode="true" position="bottom-right" />
981
+ ```
982
+
983
+ Hover to expand, mouse-leave to collapse back.
984
+
985
+ ---
986
+
987
+ ## Event emitter
988
+
989
+ Subscribe to queue events for analytics integration (Sentry, Amplitude, etc.). All listeners return an unsubscribe function.
990
+
991
+ ```ts
992
+ import { getOrCreateGlobalContext } from 'vue-toast-kit'
993
+
994
+ const queue = getOrCreateGlobalContext().queue
995
+
996
+ const off = queue.onAdd((item) => {
997
+ analytics.track('toast_shown', { type: item.options.type, message: item.message })
998
+ })
999
+
1000
+ queue.onDismiss((id) => {
1001
+ analytics.track('toast_dismissed', { id })
1002
+ })
1003
+
1004
+ queue.onUpdate((id, partial) => {
1005
+ analytics.track('toast_updated', { id, ...partial })
1006
+ })
1007
+
1008
+ // Unsubscribe when done:
1009
+ off()
1010
+ ```
1011
+
1012
+ ---
1013
+
1014
+ ## Rate limiting & localStorage persist
1015
+
1016
+ ```ts
1017
+ import { createToastContext } from 'vue-toast-kit'
1018
+
1019
+ // Max 3 toasts per second; extras are silently dropped
1020
+ const ctx = createToastContext({ rateLimit: 3, rateLimitWindowMs: 1000 })
1021
+
1022
+ // Restore toasts with persist:true after page reload
1023
+ const ctx2 = createToastContext({ persistStorage: true })
1024
+ ```
1025
+
1026
+ Or configure globally via the plugin:
1027
+
1028
+ ```ts
1029
+ app.use(VueToastPlugin, {
1030
+ rateLimit: 5,
1031
+ persistStorage: true,
1032
+ })
1033
+ ```
1034
+
1035
+ Mark individual toasts as persistent:
1036
+
1037
+ ```ts
1038
+ toast.info('Maintenance window tonight', { persist: true, duration: 0 })
1039
+ // This toast survives a page reload
1040
+ ```
1041
+
1042
+ ---
1043
+
1044
+ ## Testing utilities
1045
+
1046
+ ```ts
1047
+ import { createMockToast, mockUseToast } from 'vue-toast-kit/testing'
1048
+
1049
+ // In a Vitest / Jest test:
1050
+ describe('MyComponent', () => {
1051
+ it('calls toast.success on save', async () => {
1052
+ const mockToast = mockUseToast()
1053
+ vi.mock('vue-toast-kit', () => ({ useToast: () => mockToast }))
1054
+
1055
+ // render component, trigger save…
1056
+
1057
+ expect(mockToast.success).toHaveBeenCalledWith('Saved!')
1058
+ })
1059
+ })
1060
+
1061
+ // Create a minimal ToastItem stub:
1062
+ const item = createMockToast({
1063
+ message: 'Upload complete',
1064
+ options: { type: 'success', duration: 3000 },
1065
+ })
1066
+ ```
1067
+
1068
+ ---
1069
+
1070
+ ## Migration from vue-toastification / vue-sonner
1071
+
1072
+ ### API compatibility table
1073
+
1074
+ | vue-toastification | vue-sonner | vue-toast-kit |
1075
+ |---|---|---|
1076
+ | `useToast()` | — | `useToast()` |
1077
+ | `toast(msg, { type: TYPE.SUCCESS })` | `toast.success(msg)` | `toast.success(msg)` |
1078
+ | `toast(msg, { type: TYPE.ERROR })` | `toast.error(msg)` | `toast.error(msg)` |
1079
+ | `toast(msg, { type: TYPE.WARNING })` | — | `toast.warning(msg)` |
1080
+ | `toast(msg, { type: TYPE.INFO })` | `toast(msg)` | `toast.info(msg)` |
1081
+ | `toast.loading(msg)` | `toast.loading(msg)` | `toast.loading(msg)` |
1082
+ | `POSITION.BOTTOM_RIGHT` | — | `'bottom-right'` |
1083
+ | `POSITION.TOP_CENTER` | — | `'top-center'` |
1084
+ | `toast.dismiss(id)` | `toast.dismiss(id)` | `toast.dismiss(id)` |
1085
+ | `toast.update(id, opts)` | — | `toast.update(id, opts)` |
1086
+ | — | `toast.promise()` | `toast.promise()` |
1087
+ | — | — | `toast.undo()` |
1088
+ | — | — | Priority queue |
1089
+ | — | — | Grouping |
1090
+ | — | — | `useToastState()` headless |
1091
+ | — | — | `createToastContext()` |
1092
+
1093
+ ### Migrating from vue-toastification
1094
+
1095
+ ```ts
1096
+ // Before
1097
+ import { useToast, TYPE, POSITION } from 'vue-toastification'
1098
+ const toast = useToast()
1099
+ toast('Hello', { type: TYPE.SUCCESS, position: POSITION.BOTTOM_RIGHT })
1100
+
1101
+ // After
1102
+ import { useToast } from 'vue-toast-kit'
1103
+ const toast = useToast()
1104
+ toast.success('Hello') // position is set globally in the plugin
1105
+ ```
1106
+
1107
+ ### Migrating from vue-sonner
1108
+
1109
+ `toast.success()`, `toast.error()`, `toast.promise()`, and `toast.dismiss()` are identical. The only difference is that `<ToastContainer />` replaces `<Toaster />`:
1110
+
1111
+ ```vue
1112
+ <!-- Before (vue-sonner) -->
1113
+ <Toaster position="bottom-right" />
1114
+
1115
+ <!-- After (vue-toast-kit) -->
1116
+ <ToastContainer position="bottom-right" />
1117
+ ```
1118
+
1119
+ ---
1120
+
1121
+ ## License
1122
+
1123
+ MIT
1124
+
1125
+ ---
1126
+
1127
+ ## Author
1128
+
1129
+ macrulezru
1130
+
1131
+ GitHub: [macrulezru](https://github.com/macrulezru) · Website: [macrulez.ru/en](https://macrulez.ru/en)
1132
+
1133
+ Bugs and questions — [issues](https://github.com/macrulezru/vue-toast-kit/issues)
1134
+
1135
+ ---
1136
+
1137
+ ## 💖 Support the project
1138
+
1139
+ Open source takes time and effort. If this package saves you time or brings value, consider supporting further development.
1140
+
1141
+ <a href="https://donate.cryptocloud.plus/M6O34NIN" target="_blank">
1142
+ <img src="https://img.shields.io/badge/Donate-CryptoCloud-8A2BE2?style=for-the-badge&logo=cryptocurrency&logoColor=white" alt="Donate via CryptoCloud">
1143
+ </a>
1144
+
1145
+ Thank you for being part of this journey. ❤️