vue-toastflow 0.0.1

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.
@@ -0,0 +1 @@
1
+ :root{--tf-toast-font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;--tf-toast-radius: .75rem;--tf-toast-padding: 1rem;--normal-bg: #fff;--normal-border: #e6e8eb;--normal-text: #11181c;--loading-bg: hsl(48, 100%, 96%);--loading-border: hsl(46, 100%, 88%);--loading-text: hsl(40, 100%, 32%);--tf-toast-bg: var(--normal-bg);--tf-toast-color: var(--normal-text);--tf-toast-border-color: var(--normal-border);--success-bg: hsl(143, 85%, 96%);--success-border: hsl(145, 92%, 87%);--success-text: hsl(140, 100%, 27%);--info-bg: hsl(208, 100%, 97%);--info-border: hsl(221, 91%, 93%);--info-text: hsl(210, 92%, 45%);--warning-bg: hsl(49, 100%, 97%);--warning-border: hsl(49, 91%, 84%);--warning-text: hsl(31, 92%, 45%);--error-bg: hsl(359, 100%, 97%);--error-border: hsl(359, 100%, 94%);--error-text: hsl(360, 100%, 45%);--tf-toast-title-font-size: .85rem;--tf-toast-title-font-weight: 600;--tf-toast-description-font-size: .8rem;--tf-toast-description-color: var(--tf-toast-color);--tf-toast-created-at-font-size: .7rem;--tf-toast-gap: .8rem;--tf-toast-accent-loading: var(--loading-text);--tf-toast-accent-default: var(--normal-text);--tf-toast-accent-success: var(--success-text);--tf-toast-accent-error: var(--error-text);--tf-toast-accent-warning: var(--warning-text);--tf-toast-accent-info: var(--info-text);--tf-toast-icon-size: 24px;--tf-toast-icon-loading: var(--loading-text);--tf-toast-icon-default: var(--normal-text);--tf-toast-icon-success: var(--success-text);--tf-toast-icon-error: var(--error-text);--tf-toast-icon-warning: var(--warning-text);--tf-toast-icon-info: var(--info-text);--tf-toast-close-size: 18px;--tf-toast-close-border-color: var(--tf-toast-border-color);--tf-toast-close-color: var(--tf-toast-color);--tf-toast-close-bg: var(--tf-toast-bg);--tf-toast-close-ring-color: var(--tf-toast-border-color);--tf-toast-progress-height: 4px;--tf-toast-progress-bg: transparent;--tf-toast-progress-bar-bg: var(--tf-toast-accent-success);--tf-toast-progress-duration: 5s;--tf-toast-motion-offset: 10px}.tf-toast--paused .tf-toast-progress-bar{animation-play-state:paused}.tf-toast-item[data-position^=top-]{--tf-toast-motion-offset: -10px}.tf-toast-item[data-position^=bottom-]{--tf-toast-motion-offset: 10px}.Toastflow__animation-enter-active{animation:Toastflow__enter-kf .26s cubic-bezier(.5,1,.25,1);animation-fill-mode:both}.Toastflow__animation-leave-active{animation:Toastflow__leave-kf .22s ease;animation-fill-mode:both}.Toastflow__animation-clearAll{animation:Toastflow__clearAll-kf .26s cubic-bezier(.22,1,.36,1);animation-fill-mode:both}.Toastflow__animation-move{transition:transform .26s cubic-bezier(.5,1,.25,1);will-change:transform}.Toastflow__animation-bump{animation:Toastflow__bump-kf .14s ease-out;animation-fill-mode:both}.Toastflow__animation-update{animation:Toastflow__update-kf .32s cubic-bezier(.33,1,.68,1);animation-fill-mode:both}@keyframes Toastflow__enter-kf{0%{transform:translate3d(0,var(--tf-toast-motion-offset),0) scale(.9);opacity:0}to{transform:translateZ(0) scale(1);opacity:1}}@keyframes Toastflow__leave-kf{0%{transform:translateZ(0) scale(1);opacity:1}to{transform:translate3d(0,var(--tf-toast-motion-offset),0) scale(.9);opacity:0}}@keyframes Toastflow__clearAll-kf{0%{opacity:1}to{opacity:0}}@keyframes Toastflow__bump-kf{0%{transform:scale(1)}40%{transform:scale(1.01)}to{transform:scale(1)}}@keyframes Toastflow__update-kf{0%{opacity:.4;transform:scale(.985)}40%{opacity:1;transform:scale(1.01)}to{opacity:1;transform:scale(1)}}.tf-toast-progress[data-v-ca0da064]{height:var(--tf-toast-progress-height);width:100%;border-radius:0 0 var(--tf-toast-radius) var(--tf-toast-radius);background:var(--tf-toast-progress-bg)}.tf-toast-progress-bar[data-v-ca0da064]{height:100%;animation-name:tf-toast-progress-ca0da064;animation-duration:var(--tf-toast-progress-duration, 5s);animation-timing-function:linear;animation-fill-mode:forwards}.tf-toast-progress-bar--default[data-v-ca0da064]{background:var(--tf-toast-accent-default)}.tf-toast-progress-bar--success[data-v-ca0da064]{background:var(--tf-toast-accent-success)}.tf-toast-progress-bar--error[data-v-ca0da064]{background:var(--tf-toast-accent-error)}.tf-toast-progress-bar--warning[data-v-ca0da064]{background:var(--tf-toast-accent-warning)}.tf-toast-progress-bar--info[data-v-ca0da064]{background:var(--tf-toast-accent-info)}@keyframes tf-toast-progress-ca0da064{0%{width:100%}to{width:0}}.tf-toast-wrapper[data-v-edee5727]{pointer-events:auto;width:100%}.tf-toast[data-v-edee5727]{cursor:pointer;position:relative;width:100%;--tf-toast-close-bg: var(--tf-toast-bg);--tf-toast-close-color: var(--tf-toast-color);--tf-toast-close-border-color: var(--tf-toast-border-color)}.tf-toast-accent--loading[data-v-edee5727]{--tf-toast-bg: var(--loading-bg);--tf-toast-border-color: var(--loading-border);--tf-toast-color: var(--loading-text);--tf-toast-description-color: var(--loading-text);--tf-toast-progress-bg: color-mix( in srgb, var(--loading-text) 20%, transparent )}.tf-toast-accent--default[data-v-edee5727]{--tf-toast-bg: var(--normal-bg);--tf-toast-border-color: var(--normal-border);--tf-toast-color: var(--normal-text);--tf-toast-description-color: var(--normal-text);--tf-toast-progress-bg: color-mix( in srgb, var(--normal-text) 20%, transparent )}.tf-toast-accent--success[data-v-edee5727]{--tf-toast-bg: var(--success-bg);--tf-toast-border-color: var(--success-border);--tf-toast-color: var(--success-text);--tf-toast-description-color: var(--success-text);--tf-toast-progress-bg: color-mix( in srgb, var(--success-text) 20%, transparent )}.tf-toast-accent--error[data-v-edee5727]{--tf-toast-bg: var(--error-bg);--tf-toast-border-color: var(--error-border);--tf-toast-color: var(--error-text);--tf-toast-description-color: var(--error-text);--tf-toast-progress-bg: color-mix( in srgb, var(--error-text) 20%, transparent )}.tf-toast-accent--warning[data-v-edee5727]{--tf-toast-bg: var(--warning-bg);--tf-toast-border-color: var(--warning-border);--tf-toast-color: var(--warning-text);--tf-toast-description-color: var(--warning-text);--tf-toast-progress-bg: color-mix( in srgb, var(--warning-text) 20%, transparent )}.tf-toast-accent--info[data-v-edee5727]{--tf-toast-bg: var(--info-bg);--tf-toast-border-color: var(--info-border);--tf-toast-color: var(--info-text);--tf-toast-description-color: var(--info-text);--tf-toast-progress-bg: color-mix(in srgb, var(--info-text) 20%, transparent)}.tf-toast-surface[data-v-edee5727]{position:relative;display:block;min-width:0;padding:var(--tf-toast-padding);border-radius:var(--tf-toast-radius);background-color:var(--tf-toast-bg);color:var(--tf-toast-color);border:1px solid var(--tf-toast-border-color);font-family:var(--tf-toast-font-family);overflow:hidden}.tf-toast-main[data-v-edee5727]{display:flex;align-items:center;gap:var(--tf-toast-gap)}.tf-toast-icon-spin[data-v-edee5727]{display:inline-block;animation:tf-toast-spin-edee5727 .8s linear infinite;transform-origin:center}@keyframes tf-toast-spin-edee5727{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.tf-toast-icon-svg[data-v-edee5727]{width:var(--tf-toast-icon-size);height:var(--tf-toast-icon-size)}.tf-toast-icon--loading .tf-toast-icon-svg[data-v-edee5727]{color:var(--tf-toast-icon-loading)}.tf-toast-icon--default .tf-toast-icon-svg[data-v-edee5727]{color:var(--tf-toast-icon-default)}.tf-toast-icon--success .tf-toast-icon-svg[data-v-edee5727]{color:var(--tf-toast-icon-success)}.tf-toast-icon--error .tf-toast-icon-svg[data-v-edee5727]{color:var(--tf-toast-icon-error)}.tf-toast-icon--warning .tf-toast-icon-svg[data-v-edee5727]{color:var(--tf-toast-icon-warning)}.tf-toast-icon--info .tf-toast-icon-svg[data-v-edee5727]{color:var(--tf-toast-icon-info)}.tf-toast-body[data-v-edee5727]{position:relative;z-index:1;min-width:0;flex:1 1 auto;display:flex;flex-direction:column}.tf-toast-created-at[data-v-edee5727]{position:absolute;bottom:0;right:0;color:var(--tf-toast-description-color);font-size:var(--tf-toast-created-at-font-size);font-style:italic}.tf-toast-title[data-v-edee5727]{margin:0;font-size:var(--tf-toast-title-font-size);font-weight:var(--tf-toast-title-font-weight);line-height:1.25;white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.tf-toast-description[data-v-edee5727]{margin:0;font-size:var(--tf-toast-description-font-size);color:var(--tf-toast-description-color)}.tf-toast-close[data-v-edee5727]{position:absolute;top:0;right:0;transform:translate(40%,-40%);height:var(--tf-toast-close-size);width:var(--tf-toast-close-size);border-radius:999px;border:1px solid var(--tf-toast-close-border-color);background:var(--tf-toast-close-bg);color:var(--tf-toast-close-color);font-size:11px;display:inline-flex;align-items:center;justify-content:center;padding:0;cursor:pointer;z-index:2}.tf-toast-close-icon[data-v-edee5727]{width:12px;height:12px}.tf-toast-close[data-v-edee5727]:focus-visible{outline:2px solid var(--tf-toast-close-ring-color);outline-offset:2px}.tf-toast-progress-wrapper[data-v-edee5727]{position:absolute;left:0;right:0;bottom:0;margin:0}.tf-toast-root[data-v-b00384d2]{pointer-events:none;position:fixed;inset:0}.tf-toast-stack[data-v-b00384d2]{position:absolute;height:100%;display:flex;align-items:flex-start}.tf-toast-stack-inner[data-v-b00384d2]{display:flex;flex-direction:column;position:relative;width:100%}.tf-toast-stack-inner--bottom[data-v-b00384d2]{min-height:100%;justify-content:flex-end}.tf-toast-stack--left[data-v-b00384d2]{justify-content:flex-start}.tf-toast-stack--center[data-v-b00384d2]{justify-content:center}.tf-toast-stack--right[data-v-b00384d2]{justify-content:flex-end}.tf-toast-item[data-v-b00384d2]{pointer-events:auto;width:100%}
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "vue-toastflow",
3
+ "version": "0.0.1",
4
+ "main": "src/index.ts",
5
+ "module": "src/index.ts",
6
+ "types": "src/index.ts",
7
+ "scripts": {
8
+ "build": "vite build",
9
+ "test": "vitest"
10
+ },
11
+ "peerDependencies": {
12
+ "vue": "^3.4.0"
13
+ },
14
+ "dependencies": {
15
+ "toastflow-core": "workspace:*"
16
+ },
17
+ "devDependencies": {
18
+ "@vitejs/plugin-vue": "^6.0.2",
19
+ "typescript": "^5.9.3",
20
+ "vite": "^7.2.4",
21
+ "vitest": "^4.0.13",
22
+ "vue": "^3.5.24"
23
+ }
24
+ }
@@ -0,0 +1,660 @@
1
+ <script setup lang="ts">
2
+ import { computed, type CSSProperties, inject, ref, watch } from "vue";
3
+ import ToastProgress from "./ToastProgress.vue";
4
+ import type {
5
+ ToastContext,
6
+ ToastId,
7
+ ToastInstance,
8
+ ToastStore,
9
+ ToastType,
10
+ } from "toastflow-core";
11
+ import { toastStoreKey } from "../symbols";
12
+ import CheckCircle from "./icons/CheckCircle.vue";
13
+ import XCircle from "./icons/XCircle.vue";
14
+ import Bell from "./icons/Bell.vue";
15
+ import InfoCircle from "./icons/InfoCircle.vue";
16
+ import QuestionMarkCircle from "./icons/QuestionMarkCircle.vue";
17
+ import ArrowPath from "./icons/ArrowPath.vue";
18
+ import XMark from "./icons/XMark.vue";
19
+
20
+ const {
21
+ toast,
22
+ progressResetKey,
23
+ duplicateKey,
24
+ updateKey,
25
+ bumpAnimationClass,
26
+ clearAllAnimationClass,
27
+ updateAnimationClass,
28
+ } = defineProps<{
29
+ toast: ToastInstance;
30
+ progressResetKey?: number;
31
+ duplicateKey?: number;
32
+ updateKey?: number;
33
+ bumpAnimationClass?: string;
34
+ clearAllAnimationClass?: string;
35
+ updateAnimationClass?: string;
36
+ }>();
37
+
38
+ const emit = defineEmits<{
39
+ (e: "dismiss", id: ToastId): void;
40
+ }>();
41
+
42
+ const injectedStore = inject<ToastStore | null>(toastStoreKey, null);
43
+ if (!injectedStore) {
44
+ throw new Error("[vue-toastflow] Plugin not installed");
45
+ }
46
+ const store: ToastStore = injectedStore;
47
+
48
+ const isHovered = ref(false);
49
+ const isBumped = ref(false);
50
+ const isUpdated = ref(false);
51
+
52
+ const typeMeta: Record<
53
+ ToastType,
54
+ {
55
+ accent: string;
56
+ icon: string;
57
+ close: string;
58
+ component: any;
59
+ }
60
+ > = {
61
+ success: {
62
+ accent: "tf-toast-accent--success",
63
+ icon: "tf-toast-icon--success",
64
+ close: "tf-toast-close--success",
65
+ component: CheckCircle,
66
+ },
67
+ error: {
68
+ accent: "tf-toast-accent--error",
69
+ icon: "tf-toast-icon--error",
70
+ close: "tf-toast-close--error",
71
+ component: XCircle,
72
+ },
73
+ warning: {
74
+ accent: "tf-toast-accent--warning",
75
+ icon: "tf-toast-icon--warning",
76
+ close: "tf-toast-close--warning",
77
+ component: Bell,
78
+ },
79
+ info: {
80
+ accent: "tf-toast-accent--info",
81
+ icon: "tf-toast-icon--info",
82
+ close: "tf-toast-close--info",
83
+ component: InfoCircle,
84
+ },
85
+ default: {
86
+ accent: "tf-toast-accent--default",
87
+ icon: "tf-toast-icon--default",
88
+ close: "tf-toast-close--default",
89
+ component: QuestionMarkCircle,
90
+ },
91
+ loading: {
92
+ accent: "tf-toast-accent--loading",
93
+ icon: "tf-toast-icon--loading",
94
+ close: "tf-toast-close--loading",
95
+ component: ArrowPath,
96
+ },
97
+ };
98
+
99
+ const role = computed(function () {
100
+ return assertiveTypes.has(toast.type) ? "alert" : "status";
101
+ });
102
+
103
+ const ariaLive = computed(function () {
104
+ return assertiveTypes.has(toast.type) ? "assertive" : "polite";
105
+ });
106
+
107
+ const accentClass = computed(function () {
108
+ return typeMeta[toast.type].accent;
109
+ });
110
+
111
+ const iconWrapperClass = computed(function () {
112
+ return typeMeta[toast.type].icon;
113
+ });
114
+
115
+ const closeWrapperClass = computed(function () {
116
+ return typeMeta[toast.type].close;
117
+ });
118
+
119
+ const defaultIconComponent = computed(function () {
120
+ return typeMeta[toast.type].component;
121
+ });
122
+
123
+ const assertiveTypes = new Set<ToastType>(["error", "warning"]);
124
+
125
+ const progressStyle = computed(function (): CSSProperties {
126
+ return {
127
+ "--tf-toast-progress-duration": `${toast.duration}ms`,
128
+ };
129
+ });
130
+
131
+ const defaultCreatedAtFormatter = function (createdAt: number): string {
132
+ return new Date(createdAt).toLocaleTimeString([], {
133
+ hour: "2-digit",
134
+ minute: "2-digit",
135
+ });
136
+ };
137
+
138
+ const hasCreatedAt = computed(function () {
139
+ return Boolean(toast.showCreatedAt && Number.isFinite(toast.createdAt));
140
+ });
141
+
142
+ const createdAtText = computed(function () {
143
+ if (!hasCreatedAt.value) {
144
+ return "";
145
+ }
146
+
147
+ const formatter =
148
+ typeof toast.createdAtFormatter === "function"
149
+ ? toast.createdAtFormatter
150
+ : defaultCreatedAtFormatter;
151
+
152
+ try {
153
+ return formatter(toast.createdAt);
154
+ } catch (error) {
155
+ return defaultCreatedAtFormatter(toast.createdAt);
156
+ }
157
+ });
158
+
159
+ const createdAtAriaLabel = computed(function () {
160
+ if (!createdAtText.value) {
161
+ return "";
162
+ }
163
+ return `Sent at ${createdAtText.value}`;
164
+ });
165
+
166
+ const titleAriaLabel = computed(function () {
167
+ return toAriaText(toast.title);
168
+ });
169
+
170
+ const descriptionAriaLabel = computed(function () {
171
+ return toAriaText(toast.description);
172
+ });
173
+
174
+ const toastAriaLabel = computed(function () {
175
+ const parts = [];
176
+
177
+ if (titleAriaLabel.value) {
178
+ parts.push(titleAriaLabel.value);
179
+ }
180
+
181
+ if (descriptionAriaLabel.value) {
182
+ parts.push(descriptionAriaLabel.value);
183
+ }
184
+
185
+ if (hasCreatedAt.value && createdAtAriaLabel.value) {
186
+ parts.push(createdAtAriaLabel.value);
187
+ }
188
+
189
+ if (!parts.length) {
190
+ parts.push(`${toast.type} notification`);
191
+ }
192
+
193
+ return parts.join(". ");
194
+ });
195
+
196
+ function toAriaText(value: string): string {
197
+ if (!value) {
198
+ return "";
199
+ }
200
+ return stripHtmlToText(value);
201
+ }
202
+
203
+ function stripHtmlToText(value: string): string {
204
+ const fallback = normalizeWhitespace(value.replace(/<[^>]*>/g, " "));
205
+
206
+ if (typeof window === "undefined" || !window.document) {
207
+ return fallback;
208
+ }
209
+
210
+ try {
211
+ const container = window.document.createElement("div");
212
+ container.innerHTML = value;
213
+ return normalizeWhitespace(container.textContent ?? "");
214
+ } catch (error) {
215
+ return fallback;
216
+ }
217
+ }
218
+
219
+ function normalizeWhitespace(value: string): string {
220
+ return value.replace(/\s+/g, " ").trim();
221
+ }
222
+
223
+ function createContext(): ToastContext {
224
+ return {
225
+ id: toast.id,
226
+ position: toast.position,
227
+ type: toast.type,
228
+ title: toast.title,
229
+ description: toast.description,
230
+ createdAt: toast.createdAt,
231
+ };
232
+ }
233
+
234
+ function handleClick(event: MouseEvent) {
235
+ const context = createContext();
236
+
237
+ if (toast.onClick) {
238
+ toast.onClick(context, event);
239
+ }
240
+
241
+ if (toast.closeOnClick) {
242
+ emit("dismiss", toast.id);
243
+ }
244
+ }
245
+
246
+ function handleCloseClick() {
247
+ emit("dismiss", toast.id);
248
+ }
249
+
250
+ function handleMouseEnter() {
251
+ if (!toast.pauseOnHover) {
252
+ return;
253
+ }
254
+ isHovered.value = true;
255
+ store.pause(toast.id);
256
+ }
257
+
258
+ function handleMouseLeave() {
259
+ if (!toast.pauseOnHover) {
260
+ return;
261
+ }
262
+ isHovered.value = false;
263
+ store.resume(toast.id);
264
+ }
265
+
266
+ const progressKeyLocal = ref(0);
267
+
268
+ watch(
269
+ () => progressResetKey,
270
+ function () {
271
+ if (progressResetKey == null) {
272
+ return;
273
+ }
274
+ progressKeyLocal.value += 1;
275
+ },
276
+ );
277
+
278
+ watch(
279
+ () => duplicateKey,
280
+ function () {
281
+ if (duplicateKey == null) {
282
+ return;
283
+ }
284
+
285
+ progressKeyLocal.value += 1;
286
+
287
+ triggerBump();
288
+ },
289
+ );
290
+
291
+ watch(
292
+ () => updateKey,
293
+ function () {
294
+ if (updateKey == null) {
295
+ return;
296
+ }
297
+
298
+ triggerUpdate();
299
+ },
300
+ );
301
+
302
+ function triggerBump() {
303
+ isBumped.value = false;
304
+ requestAnimationFrame(function () {
305
+ isBumped.value = true;
306
+ });
307
+ }
308
+
309
+ function triggerUpdate() {
310
+ isUpdated.value = false;
311
+ requestAnimationFrame(function () {
312
+ isUpdated.value = true;
313
+ });
314
+ }
315
+ </script>
316
+
317
+ <template>
318
+ <div :role="role" :aria-live="ariaLive" class="tf-toast-wrapper">
319
+ <div
320
+ class="tf-toast"
321
+ :aria-label="toastAriaLabel || undefined"
322
+ :class="[
323
+ accentClass,
324
+ isBumped &&
325
+ toast.phase !== 'leaving' &&
326
+ toast.phase !== 'clear-all' &&
327
+ bumpAnimationClass,
328
+ isUpdated &&
329
+ toast.phase !== 'leaving' &&
330
+ toast.phase !== 'clear-all' &&
331
+ updateAnimationClass,
332
+ toast.phase === 'clear-all' && clearAllAnimationClass,
333
+ isHovered && 'tf-toast--paused',
334
+ ]"
335
+ @click="handleClick"
336
+ @mouseenter="handleMouseEnter"
337
+ @mouseleave="handleMouseLeave"
338
+ >
339
+ <div class="tf-toast-surface">
340
+ <!-- main row -->
341
+ <div class="tf-toast-main">
342
+ <!-- icon (slot + lucide defaults) -->
343
+ <div
344
+ class="tf-toast-icon"
345
+ :class="iconWrapperClass"
346
+ aria-hidden="true"
347
+ >
348
+ <slot name="icon" :toast="toast">
349
+ <component
350
+ :is="defaultIconComponent"
351
+ class="tf-toast-icon-svg"
352
+ :class="[toast.type === 'loading' && 'tf-toast-icon-spin']"
353
+ aria-hidden="true"
354
+ />
355
+ </slot>
356
+ </div>
357
+
358
+ <!-- content -->
359
+ <div class="tf-toast-body">
360
+ <div class="tf-toast-text">
361
+ <p
362
+ v-if="toast.title && !toast.supportHtml"
363
+ :aria-label="titleAriaLabel || undefined"
364
+ class="tf-toast-title"
365
+ >
366
+ {{ toast.title }}
367
+ </p>
368
+ <p
369
+ v-else-if="toast.title && toast.supportHtml"
370
+ class="tf-toast-title"
371
+ :aria-label="titleAriaLabel || undefined"
372
+ v-html="toast.title"
373
+ ></p>
374
+
375
+ <p
376
+ v-if="toast.description && !toast.supportHtml"
377
+ :aria-label="descriptionAriaLabel || undefined"
378
+ class="tf-toast-description"
379
+ >
380
+ {{ toast.description }}
381
+ </p>
382
+ <p
383
+ v-else-if="toast.description && toast.supportHtml"
384
+ class="tf-toast-description"
385
+ :aria-label="descriptionAriaLabel || undefined"
386
+ v-html="toast.description"
387
+ ></p>
388
+ </div>
389
+
390
+ <slot :toast="toast" />
391
+
392
+ <div v-if="hasCreatedAt" class="tf-toast-created-at">
393
+ <slot name="created-at" :toast="toast" :formatted="createdAtText">
394
+ <span :aria-label="createdAtAriaLabel || undefined">
395
+ {{ createdAtText }}
396
+ </span>
397
+ </slot>
398
+ </div>
399
+ </div>
400
+ </div>
401
+
402
+ <!-- bottom progress -->
403
+ <div
404
+ v-if="toast.progressBar"
405
+ class="tf-toast-progress-wrapper"
406
+ :style="progressStyle"
407
+ >
408
+ <slot name="progress" :toast="toast">
409
+ <ToastProgress :key="progressKeyLocal" :type="toast.type" />
410
+ </slot>
411
+ </div>
412
+ </div>
413
+
414
+ <!-- close button floating top-right -->
415
+ <button
416
+ v-if="toast.closeButton"
417
+ type="button"
418
+ class="tf-toast-close"
419
+ :class="closeWrapperClass"
420
+ aria-label="Close notification"
421
+ @click.stop="handleCloseClick"
422
+ >
423
+ <slot name="close-icon" :toast="toast">
424
+ <XMark class="tf-toast-close-icon" aria-hidden="true" />
425
+ </slot>
426
+ </button>
427
+ </div>
428
+ </div>
429
+ </template>
430
+
431
+ <style scoped>
432
+ .tf-toast-wrapper {
433
+ pointer-events: auto;
434
+ width: 100%;
435
+ }
436
+
437
+ .tf-toast {
438
+ cursor: pointer;
439
+ position: relative;
440
+ width: 100%;
441
+ --tf-toast-close-bg: var(--tf-toast-bg);
442
+ --tf-toast-close-color: var(--tf-toast-color);
443
+ --tf-toast-close-border-color: var(--tf-toast-border-color);
444
+ }
445
+
446
+ .tf-toast-accent--loading {
447
+ --tf-toast-bg: var(--loading-bg);
448
+ --tf-toast-border-color: var(--loading-border);
449
+ --tf-toast-color: var(--loading-text);
450
+ --tf-toast-description-color: var(--loading-text);
451
+ --tf-toast-progress-bg: color-mix(
452
+ in srgb,
453
+ var(--loading-text) 20%,
454
+ transparent
455
+ );
456
+ }
457
+
458
+ .tf-toast-accent--default {
459
+ --tf-toast-bg: var(--normal-bg);
460
+ --tf-toast-border-color: var(--normal-border);
461
+ --tf-toast-color: var(--normal-text);
462
+ --tf-toast-description-color: var(--normal-text);
463
+ --tf-toast-progress-bg: color-mix(
464
+ in srgb,
465
+ var(--normal-text) 20%,
466
+ transparent
467
+ );
468
+ }
469
+
470
+ .tf-toast-accent--success {
471
+ --tf-toast-bg: var(--success-bg);
472
+ --tf-toast-border-color: var(--success-border);
473
+ --tf-toast-color: var(--success-text);
474
+ --tf-toast-description-color: var(--success-text);
475
+ --tf-toast-progress-bg: color-mix(
476
+ in srgb,
477
+ var(--success-text) 20%,
478
+ transparent
479
+ );
480
+ }
481
+
482
+ .tf-toast-accent--error {
483
+ --tf-toast-bg: var(--error-bg);
484
+ --tf-toast-border-color: var(--error-border);
485
+ --tf-toast-color: var(--error-text);
486
+ --tf-toast-description-color: var(--error-text);
487
+ --tf-toast-progress-bg: color-mix(
488
+ in srgb,
489
+ var(--error-text) 20%,
490
+ transparent
491
+ );
492
+ }
493
+
494
+ .tf-toast-accent--warning {
495
+ --tf-toast-bg: var(--warning-bg);
496
+ --tf-toast-border-color: var(--warning-border);
497
+ --tf-toast-color: var(--warning-text);
498
+ --tf-toast-description-color: var(--warning-text);
499
+ --tf-toast-progress-bg: color-mix(
500
+ in srgb,
501
+ var(--warning-text) 20%,
502
+ transparent
503
+ );
504
+ }
505
+
506
+ .tf-toast-accent--info {
507
+ --tf-toast-bg: var(--info-bg);
508
+ --tf-toast-border-color: var(--info-border);
509
+ --tf-toast-color: var(--info-text);
510
+ --tf-toast-description-color: var(--info-text);
511
+ --tf-toast-progress-bg: color-mix(in srgb, var(--info-text) 20%, transparent);
512
+ }
513
+
514
+ /* card */
515
+
516
+ .tf-toast-surface {
517
+ position: relative;
518
+ display: block;
519
+ min-width: 0;
520
+ padding: var(--tf-toast-padding);
521
+ border-radius: var(--tf-toast-radius);
522
+ background-color: var(--tf-toast-bg);
523
+ color: var(--tf-toast-color);
524
+ border: 1px solid var(--tf-toast-border-color);
525
+ font-family: var(--tf-toast-font-family);
526
+ overflow: hidden;
527
+ }
528
+
529
+ .tf-toast-main {
530
+ display: flex;
531
+ align-items: center;
532
+ gap: var(--tf-toast-gap);
533
+ }
534
+
535
+ /* icon */
536
+
537
+ .tf-toast-icon-spin {
538
+ display: inline-block;
539
+ animation: tf-toast-spin 0.8s linear infinite;
540
+ transform-origin: center;
541
+ }
542
+
543
+ @keyframes tf-toast-spin {
544
+ from {
545
+ transform: rotate(0deg);
546
+ }
547
+ to {
548
+ transform: rotate(360deg);
549
+ }
550
+ }
551
+
552
+ .tf-toast-icon-svg {
553
+ width: var(--tf-toast-icon-size);
554
+ height: var(--tf-toast-icon-size);
555
+ }
556
+
557
+ /* per-type icon color */
558
+
559
+ .tf-toast-icon--loading .tf-toast-icon-svg {
560
+ color: var(--tf-toast-icon-loading);
561
+ }
562
+
563
+ .tf-toast-icon--default .tf-toast-icon-svg {
564
+ color: var(--tf-toast-icon-default);
565
+ }
566
+
567
+ .tf-toast-icon--success .tf-toast-icon-svg {
568
+ color: var(--tf-toast-icon-success);
569
+ }
570
+
571
+ .tf-toast-icon--error .tf-toast-icon-svg {
572
+ color: var(--tf-toast-icon-error);
573
+ }
574
+
575
+ .tf-toast-icon--warning .tf-toast-icon-svg {
576
+ color: var(--tf-toast-icon-warning);
577
+ }
578
+
579
+ .tf-toast-icon--info .tf-toast-icon-svg {
580
+ color: var(--tf-toast-icon-info);
581
+ }
582
+
583
+ /* body */
584
+
585
+ .tf-toast-body {
586
+ position: relative;
587
+ z-index: 1;
588
+ min-width: 0;
589
+ flex: 1 1 auto;
590
+ display: flex;
591
+ flex-direction: column;
592
+ }
593
+
594
+ .tf-toast-created-at {
595
+ position: absolute;
596
+ bottom: 0;
597
+ right: 0;
598
+ color: var(--tf-toast-description-color);
599
+ font-size: var(--tf-toast-created-at-font-size);
600
+ font-style: italic;
601
+ }
602
+
603
+ .tf-toast-title {
604
+ margin: 0;
605
+ font-size: var(--tf-toast-title-font-size);
606
+ font-weight: var(--tf-toast-title-font-weight);
607
+ line-height: 1.25;
608
+ white-space: nowrap;
609
+ text-overflow: ellipsis;
610
+ overflow: hidden;
611
+ }
612
+
613
+ .tf-toast-description {
614
+ margin: 0;
615
+ font-size: var(--tf-toast-description-font-size);
616
+ color: var(--tf-toast-description-color);
617
+ }
618
+
619
+ /* floating close button */
620
+
621
+ .tf-toast-close {
622
+ position: absolute;
623
+ top: 0;
624
+ right: 0;
625
+ transform: translate(40%, -40%);
626
+ height: var(--tf-toast-close-size);
627
+ width: var(--tf-toast-close-size);
628
+ border-radius: 999px;
629
+ border: 1px solid var(--tf-toast-close-border-color);
630
+ background: var(--tf-toast-close-bg);
631
+ color: var(--tf-toast-close-color);
632
+ font-size: 11px;
633
+ display: inline-flex;
634
+ align-items: center;
635
+ justify-content: center;
636
+ padding: 0;
637
+ cursor: pointer;
638
+ z-index: 2;
639
+ }
640
+
641
+ .tf-toast-close-icon {
642
+ width: 12px;
643
+ height: 12px;
644
+ }
645
+
646
+ .tf-toast-close:focus-visible {
647
+ outline: 2px solid var(--tf-toast-close-ring-color);
648
+ outline-offset: 2px;
649
+ }
650
+
651
+ /* bottom progress wrapper */
652
+
653
+ .tf-toast-progress-wrapper {
654
+ position: absolute;
655
+ left: 0;
656
+ right: 0;
657
+ bottom: 0;
658
+ margin: 0;
659
+ }
660
+ </style>