@rkosafo/cai.components 0.0.82 → 0.0.83

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.
@@ -233,7 +233,7 @@
233
233
  <div
234
234
  bind:this={ref}
235
235
  use:focusTrap
236
- class="fixed inset-0 z-40 flex items-center justify-center p-4"
236
+ class="fixed inset-0 z-40 overflow-y-auto p-4"
237
237
  onclick={_onclick}
238
238
  onkeydown={(ev) => {
239
239
  if (ev.key === 'Escape' && !permanent) {
@@ -253,22 +253,22 @@
253
253
  class: clsx(
254
254
  theme?.base,
255
255
  className,
256
- 'relative z-10',
256
+ fullscreen ? 'z-10' : 'absolute z-10',
257
257
  !modal && 'shadow-xl'
258
258
  )
259
259
  })}
260
- tabindex="-1"
261
- onsubmit={_onsubmit}
262
- transition:transition|global={paramsOptions as ParamsType}
263
- {...restProps}
264
- >
265
- {#if form}
266
- <form method="dialog" class={formCls({ class: clsx(theme?.form) })}>
260
+ tabindex="-1"
261
+ onsubmit={_onsubmit}
262
+ transition:transition|global={paramsOptions as ParamsType}
263
+ {...restProps}
264
+ >
265
+ {#if form}
266
+ <form method="dialog" class={formCls({ class: clsx(theme?.form) })}>
267
+ {@render content()}
268
+ </form>
269
+ {:else}
267
270
  {@render content()}
268
- </form>
269
- {:else}
270
- {@render content()}
271
- {/if}
271
+ {/if}
272
272
  </div>
273
273
  </div>
274
274
  {/if}
@@ -34,6 +34,15 @@
34
34
  export type CustomToastOptions = ToastOptions;
35
35
  export type ToastTheme = Record<string, never>;
36
36
 
37
+ export type ToastSoundPreset =
38
+ | 'chime'
39
+ | 'ding'
40
+ | 'bell'
41
+ | 'soft'
42
+ | 'pop'
43
+ | 'pulse'
44
+ | 'airy';
45
+
37
46
  export interface ToasterProps {
38
47
  position?: ToastPosition;
39
48
  duration?: number;
@@ -43,6 +52,8 @@
43
52
  containerStyle?: string;
44
53
  containerClassName?: string;
45
54
  fireWithSound?: boolean;
55
+ /** Web Audio preset when `fireWithSound` is true. Default `chime`. */
56
+ soundPreset?: ToastSoundPreset;
46
57
  // Kept for backward compatibility (no-op in wrapper mode)
47
58
  className?: string;
48
59
  theme?: ToastTheme;
@@ -58,58 +69,188 @@
58
69
 
59
70
  let currentDuration = 4000;
60
71
  let currentFireWithSound = false;
72
+ let currentSoundPreset: ToastSoundPreset = 'chime';
61
73
 
62
- function playToastSound() {
63
- if (!currentFireWithSound || typeof window === 'undefined') return;
74
+ let sharedAudioContext: AudioContext | null = null;
64
75
 
76
+ function getSharedAudioContext(): AudioContext | null {
77
+ if (typeof window === 'undefined') return null;
65
78
  try {
66
- const AudioContextClass = window.AudioContext || (window as Window & { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
67
- if (!AudioContextClass) return;
68
-
69
- const context = new AudioContextClass();
70
- const oscillator = context.createOscillator();
71
- const gain = context.createGain();
72
- oscillator.type = 'sine';
73
- oscillator.frequency.setValueAtTime(880, context.currentTime);
74
- gain.gain.setValueAtTime(0.0001, context.currentTime);
75
- gain.gain.exponentialRampToValueAtTime(0.06, context.currentTime + 0.01);
76
- gain.gain.exponentialRampToValueAtTime(0.0001, context.currentTime + 0.18);
77
- oscillator.connect(gain);
78
- gain.connect(context.destination);
79
- oscillator.start();
80
- oscillator.stop(context.currentTime + 0.2);
79
+ if (sharedAudioContext && sharedAudioContext.state !== 'closed') {
80
+ return sharedAudioContext;
81
+ }
82
+ sharedAudioContext = null;
83
+ const AudioContextClass =
84
+ window.AudioContext || (window as Window & { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
85
+ if (!AudioContextClass) return null;
86
+ sharedAudioContext = new AudioContextClass();
87
+ return sharedAudioContext;
81
88
  } catch {
82
- // Ignore audio errors.
89
+ return null;
90
+ }
91
+ }
92
+
93
+ function envelopeAttackRelease(
94
+ gain: GainNode,
95
+ ctx: AudioContext,
96
+ t: number,
97
+ peak: number,
98
+ attack: number,
99
+ release: number
100
+ ) {
101
+ gain.gain.setValueAtTime(0.0001, t);
102
+ gain.gain.exponentialRampToValueAtTime(peak, t + attack);
103
+ gain.gain.exponentialRampToValueAtTime(0.0001, t + attack + release);
104
+ }
105
+
106
+ function playPresetSound(ctx: AudioContext, preset: ToastSoundPreset) {
107
+ const t0 = ctx.currentTime;
108
+
109
+ switch (preset) {
110
+ case 'chime': {
111
+ const freqs = [523.25, 659.25];
112
+ for (let i = 0; i < freqs.length; i++) {
113
+ const start = t0 + i * 0.065;
114
+ const osc = ctx.createOscillator();
115
+ const g = ctx.createGain();
116
+ osc.type = 'sine';
117
+ osc.frequency.setValueAtTime(freqs[i], start);
118
+ envelopeAttackRelease(g, ctx, start, 0.07, 0.012, 0.11);
119
+ osc.connect(g);
120
+ g.connect(ctx.destination);
121
+ osc.start(start);
122
+ osc.stop(start + 0.2);
123
+ }
124
+ break;
125
+ }
126
+ case 'ding': {
127
+ const osc = ctx.createOscillator();
128
+ const g = ctx.createGain();
129
+ osc.type = 'sine';
130
+ osc.frequency.setValueAtTime(1046.5, t0);
131
+ envelopeAttackRelease(g, ctx, t0, 0.08, 0.004, 0.14);
132
+ osc.connect(g);
133
+ g.connect(ctx.destination);
134
+ osc.start(t0);
135
+ osc.stop(t0 + 0.2);
136
+ break;
137
+ }
138
+ case 'bell': {
139
+ const osc = ctx.createOscillator();
140
+ const g = ctx.createGain();
141
+ osc.type = 'triangle';
142
+ osc.frequency.setValueAtTime(740, t0);
143
+ envelopeAttackRelease(g, ctx, t0, 0.06, 0.006, 0.22);
144
+ osc.connect(g);
145
+ g.connect(ctx.destination);
146
+ osc.start(t0);
147
+ osc.stop(t0 + 0.28);
148
+ break;
149
+ }
150
+ case 'soft': {
151
+ const osc = ctx.createOscillator();
152
+ const g = ctx.createGain();
153
+ osc.type = 'sine';
154
+ osc.frequency.setValueAtTime(392, t0);
155
+ envelopeAttackRelease(g, ctx, t0, 0.05, 0.02, 0.2);
156
+ osc.connect(g);
157
+ g.connect(ctx.destination);
158
+ osc.start(t0);
159
+ osc.stop(t0 + 0.26);
160
+ break;
161
+ }
162
+ case 'pop': {
163
+ const osc = ctx.createOscillator();
164
+ const g = ctx.createGain();
165
+ osc.type = 'sine';
166
+ osc.frequency.setValueAtTime(320, t0);
167
+ envelopeAttackRelease(g, ctx, t0, 0.06, 0.002, 0.045);
168
+ osc.connect(g);
169
+ g.connect(ctx.destination);
170
+ osc.start(t0);
171
+ osc.stop(t0 + 0.08);
172
+ break;
173
+ }
174
+ case 'pulse': {
175
+ const osc = ctx.createOscillator();
176
+ const g = ctx.createGain();
177
+ osc.type = 'sine';
178
+ osc.frequency.setValueAtTime(620, t0);
179
+ osc.frequency.exponentialRampToValueAtTime(920, t0 + 0.07);
180
+ envelopeAttackRelease(g, ctx, t0, 0.07, 0.003, 0.08);
181
+ osc.connect(g);
182
+ g.connect(ctx.destination);
183
+ osc.start(t0);
184
+ osc.stop(t0 + 0.12);
185
+ break;
186
+ }
187
+ case 'airy':
188
+ default: {
189
+ const osc = ctx.createOscillator();
190
+ const g = ctx.createGain();
191
+ osc.type = 'sine';
192
+ osc.frequency.setValueAtTime(1760, t0);
193
+ envelopeAttackRelease(g, ctx, t0, 0.025, 0.002, 0.07);
194
+ osc.connect(g);
195
+ g.connect(ctx.destination);
196
+ osc.start(t0);
197
+ osc.stop(t0 + 0.1);
198
+ break;
199
+ }
83
200
  }
84
201
  }
85
202
 
203
+ /** Schedules sound after the current frame so the toast paints first; handles suspended AudioContext. */
204
+ function scheduleToastSound() {
205
+ if (!currentFireWithSound || typeof window === 'undefined') return;
206
+
207
+ queueMicrotask(() => {
208
+ void (async () => {
209
+ const ctx = getSharedAudioContext();
210
+ if (!ctx) return;
211
+ try {
212
+ if (ctx.state === 'suspended') await ctx.resume();
213
+ playPresetSound(ctx, currentSoundPreset);
214
+ } catch {
215
+ // Ignore audio errors.
216
+ }
217
+ })();
218
+ });
219
+ }
220
+
86
221
  export function updateToastConfig(config: {
87
222
  duration?: number;
88
223
  fireWithSound?: boolean;
224
+ soundPreset?: ToastSoundPreset;
89
225
  theme?: ToastTheme;
90
226
  richColors?: boolean;
91
227
  }) {
92
228
  if (config.duration !== undefined) currentDuration = config.duration;
93
229
  if (config.fireWithSound !== undefined) currentFireWithSound = config.fireWithSound;
230
+ if (config.soundPreset !== undefined) currentSoundPreset = config.soundPreset;
94
231
  }
95
232
 
96
233
  export const toast = Object.assign(
97
234
  (message: Renderable, options?: ToastOptions) => {
98
- playToastSound();
99
- return originalToast(message, { duration: currentDuration, ...options });
235
+ const id = originalToast(message, { duration: currentDuration, ...options });
236
+ scheduleToastSound();
237
+ return id;
100
238
  },
101
239
  {
102
240
  success: (message: Renderable, options?: ToastOptions) => {
103
- playToastSound();
104
- return originalToast.success(message, { duration: currentDuration, ...options });
241
+ const id = originalToast.success(message, { duration: currentDuration, ...options });
242
+ scheduleToastSound();
243
+ return id;
105
244
  },
106
245
  error: (message: Renderable, options?: ToastOptions) => {
107
- playToastSound();
108
- return originalToast.error(message, { duration: currentDuration, ...options });
246
+ const id = originalToast.error(message, { duration: currentDuration, ...options });
247
+ scheduleToastSound();
248
+ return id;
109
249
  },
110
250
  loading: (message: Renderable, options?: ToastOptions) => {
111
- playToastSound();
112
- return originalToast.loading(message, { duration: currentDuration, ...options });
251
+ const id = originalToast.loading(message, { duration: currentDuration, ...options });
252
+ scheduleToastSound();
253
+ return id;
113
254
  },
114
255
  promise: <T,>(
115
256
  promise: Promise<T>,
@@ -120,8 +261,9 @@
120
261
  },
121
262
  opts?: DefaultToastOptions
122
263
  ) => {
123
- playToastSound();
124
- return originalToast.promise(promise, msgs, opts);
264
+ const out = originalToast.promise(promise, msgs, opts);
265
+ scheduleToastSound();
266
+ return out;
125
267
  },
126
268
  dismiss: originalToast.dismiss.bind(originalToast),
127
269
  remove: originalToast.remove.bind(originalToast),
@@ -154,7 +296,8 @@
154
296
  expand = false,
155
297
  gap = 8,
156
298
  offset = '32px',
157
- fireWithSound = false
299
+ fireWithSound = false,
300
+ soundPreset = 'chime'
158
301
  }: ToasterProps = $props();
159
302
 
160
303
  $effect(() => {
@@ -162,7 +305,7 @@
162
305
  void theme;
163
306
  void richColors;
164
307
  void customIcons;
165
- updateToastConfig({ duration, fireWithSound });
308
+ updateToastConfig({ duration, fireWithSound, soundPreset });
166
309
  });
167
310
  </script>
168
311
 
@@ -4,6 +4,7 @@ export { resolveValue };
4
4
  export type { DefaultToastOptions, IconTheme, Toast, ToastOptions, ToastPosition, ToastType, Renderable, ValueFunction, ValueOrFunction } from 'svelte-french-toast';
5
5
  export type CustomToastOptions = ToastOptions;
6
6
  export type ToastTheme = Record<string, never>;
7
+ export type ToastSoundPreset = 'chime' | 'ding' | 'bell' | 'soft' | 'pop' | 'pulse' | 'airy';
7
8
  export interface ToasterProps {
8
9
  position?: ToastPosition;
9
10
  duration?: number;
@@ -13,6 +14,8 @@ export interface ToasterProps {
13
14
  containerStyle?: string;
14
15
  containerClassName?: string;
15
16
  fireWithSound?: boolean;
17
+ /** Web Audio preset when `fireWithSound` is true. Default `chime`. */
18
+ soundPreset?: ToastSoundPreset;
16
19
  className?: string;
17
20
  theme?: ToastTheme;
18
21
  richColors?: boolean;
@@ -27,6 +30,7 @@ export interface ToasterProps {
27
30
  export declare function updateToastConfig(config: {
28
31
  duration?: number;
29
32
  fireWithSound?: boolean;
33
+ soundPreset?: ToastSoundPreset;
30
34
  theme?: ToastTheme;
31
35
  richColors?: boolean;
32
36
  }): void;
@@ -1,2 +1,2 @@
1
1
  export { default as Toaster, toast, originalToast, updateToastConfig } from './Toast.svelte';
2
- export type { ToasterProps, ToastType, CustomToastOptions, ToastTheme, ToastOptions } from './Toast.svelte';
2
+ export type { ToasterProps, ToastSoundPreset, ToastType, CustomToastOptions, ToastTheme, ToastOptions } from './Toast.svelte';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rkosafo/cai.components",
3
- "version": "0.0.82",
3
+ "version": "0.0.83",
4
4
  "files": [
5
5
  "dist",
6
6
  "!dist/**/*.test.*",