@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.
Files changed (51) hide show
  1. package/LICENSE +31 -0
  2. package/README.md +210 -0
  3. package/dist/animations/gsapConfig.d.ts +42 -0
  4. package/dist/animations/gsapConfig.d.ts.map +1 -0
  5. package/dist/composables/useFlash.d.ts +41 -0
  6. package/dist/composables/useFlash.d.ts.map +1 -0
  7. package/dist/composables/useFlash.test.d.ts +2 -0
  8. package/dist/composables/useFlash.test.d.ts.map +1 -0
  9. package/dist/composables/useToast.d.ts +53 -0
  10. package/dist/composables/useToast.d.ts.map +1 -0
  11. package/dist/composables/useToast.test.d.ts +2 -0
  12. package/dist/composables/useToast.test.d.ts.map +1 -0
  13. package/dist/exports.test.d.ts +2 -0
  14. package/dist/exports.test.d.ts.map +1 -0
  15. package/dist/icons.d.ts +2 -0
  16. package/dist/icons.d.ts.map +1 -0
  17. package/dist/index.d.ts +8 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +2100 -0
  20. package/dist/plugin.d.ts +7 -0
  21. package/dist/plugin.d.ts.map +1 -0
  22. package/dist/stores/toastStore.d.ts +25 -0
  23. package/dist/stores/toastStore.d.ts.map +1 -0
  24. package/dist/stores/toastStore.test.d.ts +2 -0
  25. package/dist/stores/toastStore.test.d.ts.map +1 -0
  26. package/dist/style.css +1 -0
  27. package/dist/types/index.d.ts +107 -0
  28. package/dist/types/index.d.ts.map +1 -0
  29. package/dist/utils/sound.d.ts +9 -0
  30. package/dist/utils/sound.d.ts.map +1 -0
  31. package/package.json +70 -0
  32. package/src/animations/gsapConfig.ts +303 -0
  33. package/src/components/ToastContainer.vue +36 -0
  34. package/src/components/ToastIcon.vue +33 -0
  35. package/src/components/ToastItem.vue +342 -0
  36. package/src/components/ToastProgress.vue +50 -0
  37. package/src/components/ToastRegion.vue +381 -0
  38. package/src/composables/useFlash.test.ts +164 -0
  39. package/src/composables/useFlash.ts +118 -0
  40. package/src/composables/useToast.test.ts +230 -0
  41. package/src/composables/useToast.ts +95 -0
  42. package/src/exports.test.ts +72 -0
  43. package/src/icons.ts +38 -0
  44. package/src/index.ts +25 -0
  45. package/src/plugin.ts +85 -0
  46. package/src/stores/toastStore.test.ts +129 -0
  47. package/src/stores/toastStore.ts +288 -0
  48. package/src/styles/toast.css +353 -0
  49. package/src/styles/variables.css +83 -0
  50. package/src/types/index.ts +115 -0
  51. 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
+ }