@rkosafo/cai.components 0.0.82 → 0.0.84

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.
@@ -199,9 +199,6 @@
199
199
  type={form ? 'submit' : 'button'}
200
200
  formnovalidate
201
201
  class={clsx(styling.close)}
202
- onclick={() => {
203
- if (!form) close();
204
- }}
205
202
  />
206
203
  {/if}
207
204
  {:else if header}
@@ -222,9 +219,6 @@
222
219
  type={form ? 'submit' : 'button'}
223
220
  formnovalidate
224
221
  class={closeCls({ class: clsx(theme?.close, styling.close) })}
225
- onclick={() => {
226
- if (!form) close();
227
- }}
228
222
  />
229
223
  {/if}
230
224
  {/snippet}
@@ -233,7 +227,7 @@
233
227
  <div
234
228
  bind:this={ref}
235
229
  use:focusTrap
236
- class="fixed inset-0 z-40 flex items-center justify-center p-4"
230
+ class="fixed inset-0 z-40 overflow-y-auto p-4"
237
231
  onclick={_onclick}
238
232
  onkeydown={(ev) => {
239
233
  if (ev.key === 'Escape' && !permanent) {
@@ -253,22 +247,22 @@
253
247
  class: clsx(
254
248
  theme?.base,
255
249
  className,
256
- 'relative z-10',
250
+ fullscreen ? 'z-10' : 'absolute z-10',
257
251
  !modal && 'shadow-xl'
258
252
  )
259
253
  })}
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) })}>
254
+ tabindex="-1"
255
+ onsubmit={_onsubmit}
256
+ transition:transition|global={paramsOptions as ParamsType}
257
+ {...restProps}
258
+ >
259
+ {#if form}
260
+ <form method="dialog" class={formCls({ class: clsx(theme?.form) })}>
261
+ {@render content()}
262
+ </form>
263
+ {:else}
267
264
  {@render content()}
268
- </form>
269
- {:else}
270
- {@render content()}
271
- {/if}
265
+ {/if}
272
266
  </div>
273
267
  </div>
274
268
  {/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.84",
4
4
  "files": [
5
5
  "dist",
6
6
  "!dist/**/*.test.*",