@soft-toast/vue 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 +31 -0
- package/README.md +210 -0
- package/dist/animations/gsapConfig.d.ts +42 -0
- package/dist/animations/gsapConfig.d.ts.map +1 -0
- package/dist/composables/useFlash.d.ts +41 -0
- package/dist/composables/useFlash.d.ts.map +1 -0
- package/dist/composables/useFlash.test.d.ts +2 -0
- package/dist/composables/useFlash.test.d.ts.map +1 -0
- package/dist/composables/useToast.d.ts +53 -0
- package/dist/composables/useToast.d.ts.map +1 -0
- package/dist/composables/useToast.test.d.ts +2 -0
- package/dist/composables/useToast.test.d.ts.map +1 -0
- package/dist/exports.test.d.ts +2 -0
- package/dist/exports.test.d.ts.map +1 -0
- package/dist/icons.d.ts +2 -0
- package/dist/icons.d.ts.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2100 -0
- package/dist/plugin.d.ts +7 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/stores/toastStore.d.ts +25 -0
- package/dist/stores/toastStore.d.ts.map +1 -0
- package/dist/stores/toastStore.test.d.ts +2 -0
- package/dist/stores/toastStore.test.d.ts.map +1 -0
- package/dist/style.css +1 -0
- package/dist/types/index.d.ts +107 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/utils/sound.d.ts +9 -0
- package/dist/utils/sound.d.ts.map +1 -0
- package/package.json +70 -0
- package/src/animations/gsapConfig.ts +303 -0
- package/src/components/ToastContainer.vue +36 -0
- package/src/components/ToastIcon.vue +33 -0
- package/src/components/ToastItem.vue +342 -0
- package/src/components/ToastProgress.vue +50 -0
- package/src/components/ToastRegion.vue +381 -0
- package/src/composables/useFlash.test.ts +164 -0
- package/src/composables/useFlash.ts +118 -0
- package/src/composables/useToast.test.ts +230 -0
- package/src/composables/useToast.ts +95 -0
- package/src/exports.test.ts +72 -0
- package/src/icons.ts +38 -0
- package/src/index.ts +25 -0
- package/src/plugin.ts +85 -0
- package/src/stores/toastStore.test.ts +129 -0
- package/src/stores/toastStore.ts +288 -0
- package/src/styles/toast.css +353 -0
- package/src/styles/variables.css +83 -0
- package/src/types/index.ts +115 -0
- package/src/utils/sound.ts +140 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/* Soft Toast CSS Variables - Light Theme (Default) */
|
|
2
|
+
:root {
|
|
3
|
+
/* Colors */
|
|
4
|
+
--st-bg-default: #ffffff;
|
|
5
|
+
--st-bg-success: #dcfce7;
|
|
6
|
+
--st-bg-error: #fee2e2;
|
|
7
|
+
--st-bg-warning: #fef3c7;
|
|
8
|
+
--st-bg-info: #dbeafe;
|
|
9
|
+
|
|
10
|
+
--st-text-default: #1f2937;
|
|
11
|
+
--st-text-success: #166534;
|
|
12
|
+
--st-text-error: #991b1b;
|
|
13
|
+
--st-text-warning: #92400e;
|
|
14
|
+
--st-text-info: #1e40af;
|
|
15
|
+
|
|
16
|
+
--st-border-default: #e5e7eb;
|
|
17
|
+
--st-border-success: #86efac;
|
|
18
|
+
--st-border-error: #fca5a5;
|
|
19
|
+
--st-border-warning: #fcd34d;
|
|
20
|
+
--st-border-info: #93c5fd;
|
|
21
|
+
|
|
22
|
+
--st-icon-success: #22c55e;
|
|
23
|
+
--st-icon-error: #ef4444;
|
|
24
|
+
--st-icon-warning: #f59e0b;
|
|
25
|
+
--st-icon-info: #3b82f6;
|
|
26
|
+
|
|
27
|
+
/* Shadows */
|
|
28
|
+
--st-shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
|
29
|
+
--st-shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
|
30
|
+
--st-shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
|
31
|
+
--st-shadow-glow: 0 0 20px rgb(0 0 0 / 0.05);
|
|
32
|
+
|
|
33
|
+
/* Dimensions */
|
|
34
|
+
--st-toast-min-width: 320px;
|
|
35
|
+
--st-toast-max-width: 420px;
|
|
36
|
+
--st-toast-padding: 16px;
|
|
37
|
+
--st-toast-gap: 12px;
|
|
38
|
+
--st-border-radius: 9999px;
|
|
39
|
+
--st-border-radius-blob: 24px;
|
|
40
|
+
|
|
41
|
+
/* Typography */
|
|
42
|
+
--st-font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
43
|
+
--st-font-size-sm: 12px;
|
|
44
|
+
--st-font-size-base: 14px;
|
|
45
|
+
--st-font-size-lg: 16px;
|
|
46
|
+
--st-font-weight-normal: 400;
|
|
47
|
+
--st-font-weight-medium: 500;
|
|
48
|
+
--st-font-weight-semibold: 600;
|
|
49
|
+
|
|
50
|
+
/* Transitions */
|
|
51
|
+
--st-transition-fast: 150ms;
|
|
52
|
+
--st-transition-base: 300ms;
|
|
53
|
+
--st-transition-slow: 500ms;
|
|
54
|
+
|
|
55
|
+
/* Z-index */
|
|
56
|
+
--st-z-index: 9999;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* Dark Theme */
|
|
60
|
+
[data-soft-toast-theme="dark"] {
|
|
61
|
+
--st-bg-default: #1f2937;
|
|
62
|
+
--st-bg-success: #14532d;
|
|
63
|
+
--st-bg-error: #7f1d1d;
|
|
64
|
+
--st-bg-warning: #713f12;
|
|
65
|
+
--st-bg-info: #1e3a8a;
|
|
66
|
+
|
|
67
|
+
--st-text-default: #f9fafb;
|
|
68
|
+
--st-text-success: #86efac;
|
|
69
|
+
--st-text-error: #fca5a5;
|
|
70
|
+
--st-text-warning: #fcd34d;
|
|
71
|
+
--st-text-info: #93c5fd;
|
|
72
|
+
|
|
73
|
+
--st-border-default: #374151;
|
|
74
|
+
--st-border-success: #166534;
|
|
75
|
+
--st-border-error: #991b1b;
|
|
76
|
+
--st-border-warning: #92400e;
|
|
77
|
+
--st-border-info: #1e40af;
|
|
78
|
+
|
|
79
|
+
--st-shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3);
|
|
80
|
+
--st-shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.4), 0 2px 4px -2px rgb(0 0 0 / 0.3);
|
|
81
|
+
--st-shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.4), 0 4px 6px -4px rgb(0 0 0 / 0.3);
|
|
82
|
+
--st-shadow-glow: 0 0 30px rgb(0 0 0 / 0.3);
|
|
83
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { VNode, Component } from 'vue'
|
|
2
|
+
|
|
3
|
+
export type ToastType = 'default' | 'success' | 'error' | 'warning' | 'info' | 'promise'
|
|
4
|
+
export type ToastPosition = 'top' | 'bottom' | 'left' | 'right' | 'center' | 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'
|
|
5
|
+
export type AnimationPreset = 'smooth' | 'bouncy' | 'subtle' | 'snappy'
|
|
6
|
+
export type QueueOverflow = 'drop-oldest' | 'drop-newest'
|
|
7
|
+
|
|
8
|
+
export interface ToastAction {
|
|
9
|
+
label: string
|
|
10
|
+
onClick: () => void | Promise<void>
|
|
11
|
+
successLabel?: string
|
|
12
|
+
primary?: boolean
|
|
13
|
+
class?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ToastPromiseMessages {
|
|
17
|
+
loading: string
|
|
18
|
+
success: string
|
|
19
|
+
error: string
|
|
20
|
+
description?: {
|
|
21
|
+
success?: string
|
|
22
|
+
error?: string
|
|
23
|
+
}
|
|
24
|
+
action?: {
|
|
25
|
+
error?: ToastAction
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ToastOptions {
|
|
30
|
+
id?: string
|
|
31
|
+
type?: ToastType
|
|
32
|
+
title?: string
|
|
33
|
+
description?: string | VNode
|
|
34
|
+
action?: ToastAction | ToastAction[]
|
|
35
|
+
icon?: string | VNode | Component
|
|
36
|
+
duration?: number
|
|
37
|
+
position?: ToastPosition
|
|
38
|
+
classNames?: ToastClassNames
|
|
39
|
+
fillColor?: string
|
|
40
|
+
borderColor?: string
|
|
41
|
+
borderWidth?: number
|
|
42
|
+
preset?: AnimationPreset
|
|
43
|
+
bounce?: number
|
|
44
|
+
spring?: boolean
|
|
45
|
+
showTimestamp?: boolean
|
|
46
|
+
showProgress?: boolean
|
|
47
|
+
closeButton?: boolean | 'top-left' | 'top-right'
|
|
48
|
+
// Sound
|
|
49
|
+
sound?: boolean | string // true = built-in tone, string = custom audio URL
|
|
50
|
+
soundVolume?: number // 0–1, default 0.5
|
|
51
|
+
onDismiss?: (id: string) => void
|
|
52
|
+
onAutoClose?: (id: string) => void
|
|
53
|
+
promise?: Promise<unknown>
|
|
54
|
+
promiseMessages?: ToastPromiseMessages
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface ToastClassNames {
|
|
58
|
+
wrapper?: string
|
|
59
|
+
content?: string
|
|
60
|
+
header?: string
|
|
61
|
+
title?: string
|
|
62
|
+
icon?: string
|
|
63
|
+
description?: string
|
|
64
|
+
actionWrapper?: string
|
|
65
|
+
actionButton?: string
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface Toast extends Required<Pick<ToastOptions, 'id' | 'type' | 'duration' | 'position' | 'showTimestamp' | 'showProgress' | 'spring'>> {
|
|
69
|
+
title?: string
|
|
70
|
+
description?: string | VNode
|
|
71
|
+
action?: ToastAction | ToastAction[]
|
|
72
|
+
icon?: string | VNode | Component
|
|
73
|
+
classNames?: ToastClassNames
|
|
74
|
+
fillColor?: string
|
|
75
|
+
borderColor?: string
|
|
76
|
+
borderWidth?: number
|
|
77
|
+
closeButton?: boolean | 'top-left' | 'top-right'
|
|
78
|
+
preset: AnimationPreset
|
|
79
|
+
bounce: number
|
|
80
|
+
createdAt: number
|
|
81
|
+
remainingTime: number
|
|
82
|
+
isPaused: boolean
|
|
83
|
+
isExpanded: boolean
|
|
84
|
+
isLeaving: boolean
|
|
85
|
+
promise?: Promise<unknown>
|
|
86
|
+
promiseMessages?: ToastPromiseMessages
|
|
87
|
+
onDismiss?: (id: string) => void
|
|
88
|
+
onAutoClose?: (id: string) => void
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface ToastContainerProps {
|
|
92
|
+
position?: ToastPosition
|
|
93
|
+
duration?: number
|
|
94
|
+
gap?: number
|
|
95
|
+
offset?: number | string
|
|
96
|
+
theme?: 'light' | 'dark'
|
|
97
|
+
toastOptions?: Partial<ToastOptions>
|
|
98
|
+
spring?: boolean
|
|
99
|
+
bounce?: number
|
|
100
|
+
preset?: AnimationPreset
|
|
101
|
+
closeOnEscape?: boolean
|
|
102
|
+
closeButton?: boolean | 'top-left' | 'top-right'
|
|
103
|
+
showProgress?: boolean
|
|
104
|
+
showTimestamp?: boolean
|
|
105
|
+
sound?: boolean // enable built-in sounds globally
|
|
106
|
+
soundVolume?: number // global volume 0–1
|
|
107
|
+
maxQueue?: number
|
|
108
|
+
queueOverflow?: QueueOverflow
|
|
109
|
+
dir?: 'ltr' | 'rtl'
|
|
110
|
+
swipeToDismiss?: boolean
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface ToastPluginOptions extends ToastContainerProps {
|
|
114
|
+
teleportTarget?: string
|
|
115
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @soft-toast/vue — Sound Engine
|
|
3
|
+
* Uses the Web Audio API to synthesize tones — zero external file dependencies.
|
|
4
|
+
* Silently no-ops in SSR, when autoplay policy blocks, or when sound is disabled.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ToastType } from '../types'
|
|
8
|
+
|
|
9
|
+
// Web Audio context (lazy, shared)
|
|
10
|
+
let ctx: AudioContext | null = null
|
|
11
|
+
|
|
12
|
+
// Browser autoplay policy: audio only plays after a user gesture
|
|
13
|
+
let userHasInteracted = false
|
|
14
|
+
|
|
15
|
+
const trackInteraction = () => {
|
|
16
|
+
userHasInteracted = true
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (typeof window !== 'undefined') {
|
|
20
|
+
;(['click', 'keydown', 'pointerdown', 'touchstart'] as const).forEach((evt) =>
|
|
21
|
+
window.addEventListener(evt, trackInteraction, { once: true, passive: true })
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const getCtx = (): AudioContext | null => {
|
|
26
|
+
if (typeof window === 'undefined' || typeof AudioContext === 'undefined') return null
|
|
27
|
+
if (!ctx) ctx = new AudioContext()
|
|
28
|
+
// Resume if suspended (some browsers suspend after inactivity)
|
|
29
|
+
if (ctx.state === 'suspended') ctx.resume().catch(() => {})
|
|
30
|
+
return ctx
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Play a single synthesised note.
|
|
35
|
+
* @param freq Frequency in Hz
|
|
36
|
+
* @param duration Duration in seconds
|
|
37
|
+
* @param volume Amplitude 0–1
|
|
38
|
+
* @param type Oscillator wave type
|
|
39
|
+
* @param delay Start offset in seconds (from now)
|
|
40
|
+
*/
|
|
41
|
+
const playNote = (
|
|
42
|
+
freq: number,
|
|
43
|
+
duration: number,
|
|
44
|
+
volume: number,
|
|
45
|
+
type: OscillatorType = 'sine',
|
|
46
|
+
delay = 0
|
|
47
|
+
) => {
|
|
48
|
+
const audio = getCtx()
|
|
49
|
+
if (!audio) return
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const osc = audio.createOscillator()
|
|
53
|
+
const gain = audio.createGain()
|
|
54
|
+
|
|
55
|
+
osc.connect(gain)
|
|
56
|
+
gain.connect(audio.destination)
|
|
57
|
+
|
|
58
|
+
osc.type = type
|
|
59
|
+
osc.frequency.setValueAtTime(freq, audio.currentTime + delay)
|
|
60
|
+
|
|
61
|
+
// Soft attack + exponential decay for a natural feel
|
|
62
|
+
gain.gain.setValueAtTime(0, audio.currentTime + delay)
|
|
63
|
+
gain.gain.linearRampToValueAtTime(volume, audio.currentTime + delay + 0.01)
|
|
64
|
+
gain.gain.exponentialRampToValueAtTime(0.001, audio.currentTime + delay + duration)
|
|
65
|
+
|
|
66
|
+
osc.start(audio.currentTime + delay)
|
|
67
|
+
osc.stop(audio.currentTime + delay + duration + 0.02)
|
|
68
|
+
} catch {
|
|
69
|
+
// AudioContext can throw in certain sandboxed environments
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Built-in tone profiles — tuned to feel contextually appropriate.
|
|
75
|
+
* success : rising triad (positive, resolved)
|
|
76
|
+
* error : falling minor (tense, alarming)
|
|
77
|
+
* warning : double pulse (attention-grabbing but not harsh)
|
|
78
|
+
* info : soft chime (neutral, informational)
|
|
79
|
+
* default : single soft pop
|
|
80
|
+
*/
|
|
81
|
+
const profiles: Record<ToastType, (vol: number) => void> = {
|
|
82
|
+
success: (vol) => {
|
|
83
|
+
playNote(523.25, 0.12, vol * 0.45, 'sine', 0) // C5
|
|
84
|
+
playNote(659.25, 0.14, vol * 0.50, 'sine', 0.08) // E5
|
|
85
|
+
playNote(783.99, 0.20, vol * 0.55, 'sine', 0.16) // G5
|
|
86
|
+
},
|
|
87
|
+
error: (vol) => {
|
|
88
|
+
playNote(329.63, 0.18, vol * 0.60, 'triangle', 0) // E4
|
|
89
|
+
playNote(277.18, 0.28, vol * 0.55, 'triangle', 0.12) // C#4
|
|
90
|
+
},
|
|
91
|
+
warning: (vol) => {
|
|
92
|
+
playNote(440.00, 0.14, vol * 0.50, 'sine', 0) // A4
|
|
93
|
+
playNote(440.00, 0.18, vol * 0.40, 'sine', 0.22) // A4 (double pulse)
|
|
94
|
+
},
|
|
95
|
+
info: (vol) => {
|
|
96
|
+
playNote(659.25, 0.12, vol * 0.38, 'sine', 0) // E5
|
|
97
|
+
playNote(880.00, 0.18, vol * 0.42, 'sine', 0.10) // A5
|
|
98
|
+
},
|
|
99
|
+
default: (vol) => {
|
|
100
|
+
playNote(523.25, 0.15, vol * 0.35, 'sine', 0) // C5
|
|
101
|
+
},
|
|
102
|
+
// promise → same as info while loading
|
|
103
|
+
promise: (vol) => {
|
|
104
|
+
playNote(523.25, 0.12, vol * 0.30, 'sine', 0)
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Play a sound for the given toast.
|
|
110
|
+
* @param toastType The semantic toast type
|
|
111
|
+
* @param sound true = built-in tone, string = custom audio URL, false = silence
|
|
112
|
+
* @param volume 0–1 (clamped)
|
|
113
|
+
*/
|
|
114
|
+
export const playToastSound = (
|
|
115
|
+
toastType: ToastType,
|
|
116
|
+
sound: boolean | string | undefined,
|
|
117
|
+
volume = 0.5
|
|
118
|
+
): void => {
|
|
119
|
+
if (!sound) return
|
|
120
|
+
if (typeof window === 'undefined') return
|
|
121
|
+
if (!userHasInteracted) return // respect autoplay policy
|
|
122
|
+
|
|
123
|
+
const vol = Math.max(0, Math.min(1, volume))
|
|
124
|
+
|
|
125
|
+
if (typeof sound === 'string') {
|
|
126
|
+
// Custom audio URL — user supplies their own sound file
|
|
127
|
+
try {
|
|
128
|
+
const audio = new Audio(sound)
|
|
129
|
+
audio.volume = vol
|
|
130
|
+
audio.play().catch(() => {}) // silent fail if policy blocks
|
|
131
|
+
} catch {
|
|
132
|
+
/* noop */
|
|
133
|
+
}
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Built-in synthesised tone
|
|
138
|
+
const profile = profiles[toastType] ?? profiles.default
|
|
139
|
+
profile(vol)
|
|
140
|
+
}
|