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
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. ❤️
|