@akshatbuilds/sonix 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 +21 -0
- package/README.md +123 -0
- package/app/globals.css +110 -0
- package/app/layout.tsx +54 -0
- package/app/page.tsx +40 -0
- package/app/usage/page.tsx +9 -0
- package/cli/index.mjs +151 -0
- package/components/sound-card.tsx +595 -0
- package/components/sounds-gallery.tsx +137 -0
- package/components/theme-provider.tsx +11 -0
- package/components/theme-toggle.tsx +82 -0
- package/components/ui/button.tsx +57 -0
- package/components/ui/checkbox.tsx +30 -0
- package/components/ui/slider.tsx +28 -0
- package/components/ui/switch.tsx +29 -0
- package/components/ui/tooltip.tsx +30 -0
- package/components/usage-guide.tsx +155 -0
- package/components.json +21 -0
- package/lib/sounds.ts +329 -0
- package/lib/utils.ts +6 -0
- package/next-env.d.ts +6 -0
- package/next.config.mjs +5 -0
- package/package.json +96 -0
- package/postcss.config.mjs +8 -0
- package/public/click1.mp3 +0 -0
- package/public/click2.mp3 +0 -0
- package/public/registry/index.json +92 -0
- package/public/registry/sounds/button-click-secondary.json +13 -0
- package/public/registry/sounds/button-click.json +13 -0
- package/public/registry/sounds/error-beep.json +10 -0
- package/public/registry/sounds/error-buzz.json +10 -0
- package/public/registry/sounds/hover-blip.json +10 -0
- package/public/registry/sounds/hover-soft.json +10 -0
- package/public/registry/sounds/key-press.json +10 -0
- package/public/registry/sounds/notification-ping.json +10 -0
- package/public/registry/sounds/notification-subtle.json +10 -0
- package/public/registry/sounds/pop.json +10 -0
- package/public/registry/sounds/slider-tick.json +10 -0
- package/public/registry/sounds/success-bell.json +10 -0
- package/public/registry/sounds/success-chime.json +10 -0
- package/public/registry/sounds/swoosh.json +10 -0
- package/scripts/build-registry.mjs +293 -0
- package/tailwind.config.ts +100 -0
- package/tsconfig.json +33 -0
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { Button } from '@/components/ui/button';
|
|
5
|
+
import { Slider } from '@/components/ui/slider';
|
|
6
|
+
import { Switch } from '@/components/ui/switch';
|
|
7
|
+
import { Checkbox } from '@/components/ui/checkbox';
|
|
8
|
+
import {
|
|
9
|
+
Tooltip,
|
|
10
|
+
TooltipContent,
|
|
11
|
+
TooltipProvider,
|
|
12
|
+
TooltipTrigger,
|
|
13
|
+
} from '@/components/ui/tooltip';
|
|
14
|
+
import type { SoundName } from '@/lib/sounds';
|
|
15
|
+
import { playSound } from '@/lib/sounds';
|
|
16
|
+
|
|
17
|
+
// Standalone copy-paste code for each sound
|
|
18
|
+
const soundSnippets: Record<SoundName, string> = {
|
|
19
|
+
'button-click': `// Requires click1.mp3 in your public folder
|
|
20
|
+
const audio = new Audio('/click1.mp3');
|
|
21
|
+
audio.volume = 0.5;
|
|
22
|
+
audio.play();`,
|
|
23
|
+
|
|
24
|
+
'button-click-secondary': `// Requires click2.mp3 in your public folder
|
|
25
|
+
const audio = new Audio('/click2.mp3');
|
|
26
|
+
audio.volume = 0.5;
|
|
27
|
+
audio.play();`,
|
|
28
|
+
|
|
29
|
+
'hover-blip': `const ctx = new AudioContext();
|
|
30
|
+
const now = ctx.currentTime;
|
|
31
|
+
const osc = ctx.createOscillator();
|
|
32
|
+
const gain = ctx.createGain();
|
|
33
|
+
osc.type = 'sine';
|
|
34
|
+
osc.frequency.setValueAtTime(1200, now);
|
|
35
|
+
osc.frequency.exponentialRampToValueAtTime(2400, now + 0.05);
|
|
36
|
+
gain.gain.setValueAtTime(0.15, now);
|
|
37
|
+
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.05);
|
|
38
|
+
osc.connect(gain).connect(ctx.destination);
|
|
39
|
+
osc.start(now);
|
|
40
|
+
osc.stop(now + 0.05);`,
|
|
41
|
+
|
|
42
|
+
'hover-soft': `const ctx = new AudioContext();
|
|
43
|
+
const now = ctx.currentTime;
|
|
44
|
+
const osc = ctx.createOscillator();
|
|
45
|
+
const gain = ctx.createGain();
|
|
46
|
+
osc.type = 'sine';
|
|
47
|
+
osc.frequency.setValueAtTime(900, now);
|
|
48
|
+
gain.gain.setValueAtTime(0.08, now);
|
|
49
|
+
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.06);
|
|
50
|
+
osc.connect(gain).connect(ctx.destination);
|
|
51
|
+
osc.start(now);
|
|
52
|
+
osc.stop(now + 0.06);`,
|
|
53
|
+
|
|
54
|
+
'success-chime': `const ctx = new AudioContext();
|
|
55
|
+
const now = ctx.currentTime;
|
|
56
|
+
// Note 1
|
|
57
|
+
const osc1 = ctx.createOscillator();
|
|
58
|
+
const gain1 = ctx.createGain();
|
|
59
|
+
osc1.type = 'sine';
|
|
60
|
+
osc1.frequency.setValueAtTime(800, now);
|
|
61
|
+
gain1.gain.setValueAtTime(0.25, now);
|
|
62
|
+
gain1.gain.exponentialRampToValueAtTime(0.001, now + 0.15);
|
|
63
|
+
osc1.connect(gain1).connect(ctx.destination);
|
|
64
|
+
osc1.start(now);
|
|
65
|
+
osc1.stop(now + 0.15);
|
|
66
|
+
// Note 2
|
|
67
|
+
const osc2 = ctx.createOscillator();
|
|
68
|
+
const gain2 = ctx.createGain();
|
|
69
|
+
osc2.type = 'sine';
|
|
70
|
+
osc2.frequency.setValueAtTime(1200, now + 0.1);
|
|
71
|
+
gain2.gain.setValueAtTime(0.001, now);
|
|
72
|
+
gain2.gain.setValueAtTime(0.25, now + 0.1);
|
|
73
|
+
gain2.gain.exponentialRampToValueAtTime(0.001, now + 0.3);
|
|
74
|
+
osc2.connect(gain2).connect(ctx.destination);
|
|
75
|
+
osc2.start(now + 0.1);
|
|
76
|
+
osc2.stop(now + 0.3);`,
|
|
77
|
+
|
|
78
|
+
'success-bell': `const ctx = new AudioContext();
|
|
79
|
+
const now = ctx.currentTime;
|
|
80
|
+
[1000, 1250, 1500].forEach((freq, i) => {
|
|
81
|
+
const osc = ctx.createOscillator();
|
|
82
|
+
const gain = ctx.createGain();
|
|
83
|
+
osc.type = 'sine';
|
|
84
|
+
osc.frequency.setValueAtTime(freq, now + i * 0.08);
|
|
85
|
+
gain.gain.setValueAtTime(0.001, now);
|
|
86
|
+
gain.gain.setValueAtTime(0.2, now + i * 0.08);
|
|
87
|
+
gain.gain.exponentialRampToValueAtTime(0.001, now + i * 0.08 + 0.15);
|
|
88
|
+
osc.connect(gain).connect(ctx.destination);
|
|
89
|
+
osc.start(now + i * 0.08);
|
|
90
|
+
osc.stop(now + i * 0.08 + 0.15);
|
|
91
|
+
});`,
|
|
92
|
+
|
|
93
|
+
'error-buzz': `const ctx = new AudioContext();
|
|
94
|
+
const now = ctx.currentTime;
|
|
95
|
+
const osc = ctx.createOscillator();
|
|
96
|
+
const gain = ctx.createGain();
|
|
97
|
+
osc.type = 'sawtooth';
|
|
98
|
+
osc.frequency.setValueAtTime(150, now);
|
|
99
|
+
gain.gain.setValueAtTime(0.2, now);
|
|
100
|
+
gain.gain.setValueAtTime(0.001, now + 0.08);
|
|
101
|
+
gain.gain.setValueAtTime(0.2, now + 0.1);
|
|
102
|
+
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.2);
|
|
103
|
+
osc.connect(gain).connect(ctx.destination);
|
|
104
|
+
osc.start(now);
|
|
105
|
+
osc.stop(now + 0.2);`,
|
|
106
|
+
|
|
107
|
+
'error-beep': `const ctx = new AudioContext();
|
|
108
|
+
const now = ctx.currentTime;
|
|
109
|
+
[500, 350].forEach((freq, i) => {
|
|
110
|
+
const osc = ctx.createOscillator();
|
|
111
|
+
const gain = ctx.createGain();
|
|
112
|
+
osc.type = 'square';
|
|
113
|
+
osc.frequency.setValueAtTime(freq, now + i * 0.12);
|
|
114
|
+
gain.gain.setValueAtTime(0.001, now);
|
|
115
|
+
gain.gain.setValueAtTime(0.15, now + i * 0.12);
|
|
116
|
+
gain.gain.exponentialRampToValueAtTime(0.001, now + i * 0.12 + 0.1);
|
|
117
|
+
osc.connect(gain).connect(ctx.destination);
|
|
118
|
+
osc.start(now + i * 0.12);
|
|
119
|
+
osc.stop(now + i * 0.12 + 0.1);
|
|
120
|
+
});`,
|
|
121
|
+
|
|
122
|
+
'notification-ping': `const ctx = new AudioContext();
|
|
123
|
+
const now = ctx.currentTime;
|
|
124
|
+
const osc = ctx.createOscillator();
|
|
125
|
+
const gain = ctx.createGain();
|
|
126
|
+
osc.type = 'sine';
|
|
127
|
+
osc.frequency.setValueAtTime(1100, now);
|
|
128
|
+
osc.frequency.exponentialRampToValueAtTime(1800, now + 0.05);
|
|
129
|
+
osc.frequency.exponentialRampToValueAtTime(1100, now + 0.1);
|
|
130
|
+
gain.gain.setValueAtTime(0.25, now);
|
|
131
|
+
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.15);
|
|
132
|
+
osc.connect(gain).connect(ctx.destination);
|
|
133
|
+
osc.start(now);
|
|
134
|
+
osc.stop(now + 0.15);`,
|
|
135
|
+
|
|
136
|
+
'notification-subtle': `const ctx = new AudioContext();
|
|
137
|
+
const now = ctx.currentTime;
|
|
138
|
+
const osc = ctx.createOscillator();
|
|
139
|
+
const gain = ctx.createGain();
|
|
140
|
+
osc.type = 'sine';
|
|
141
|
+
osc.frequency.setValueAtTime(700, now);
|
|
142
|
+
gain.gain.setValueAtTime(0.1, now);
|
|
143
|
+
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.12);
|
|
144
|
+
osc.connect(gain).connect(ctx.destination);
|
|
145
|
+
osc.start(now);
|
|
146
|
+
osc.stop(now + 0.12);`,
|
|
147
|
+
|
|
148
|
+
'swoosh': `const ctx = new AudioContext();
|
|
149
|
+
const now = ctx.currentTime;
|
|
150
|
+
const bufferSize = Math.floor(ctx.sampleRate * 0.4);
|
|
151
|
+
const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
|
|
152
|
+
const data = buffer.getChannelData(0);
|
|
153
|
+
for (let i = 0; i < bufferSize; i++) {
|
|
154
|
+
data[i] = Math.random() * 2 - 1;
|
|
155
|
+
}
|
|
156
|
+
const noise = ctx.createBufferSource();
|
|
157
|
+
noise.buffer = buffer;
|
|
158
|
+
const lp = ctx.createBiquadFilter();
|
|
159
|
+
lp.type = 'lowpass';
|
|
160
|
+
lp.frequency.setValueAtTime(350, now);
|
|
161
|
+
lp.frequency.linearRampToValueAtTime(500, now + 0.15);
|
|
162
|
+
lp.frequency.linearRampToValueAtTime(250, now + 0.38);
|
|
163
|
+
lp.Q.setValueAtTime(0.5, now);
|
|
164
|
+
const noiseGain = ctx.createGain();
|
|
165
|
+
noiseGain.gain.setValueAtTime(0.001, now);
|
|
166
|
+
noiseGain.gain.linearRampToValueAtTime(0.06, now + 0.16);
|
|
167
|
+
noiseGain.gain.linearRampToValueAtTime(0.05, now + 0.24);
|
|
168
|
+
noiseGain.gain.exponentialRampToValueAtTime(0.001, now + 0.38);
|
|
169
|
+
noise.connect(lp).connect(noiseGain).connect(ctx.destination);
|
|
170
|
+
noise.start(now);
|
|
171
|
+
noise.stop(now + 0.4);`,
|
|
172
|
+
|
|
173
|
+
'pop': `const ctx = new AudioContext();
|
|
174
|
+
const now = ctx.currentTime;
|
|
175
|
+
const osc = ctx.createOscillator();
|
|
176
|
+
const gain = ctx.createGain();
|
|
177
|
+
osc.type = 'sine';
|
|
178
|
+
osc.frequency.setValueAtTime(950, now);
|
|
179
|
+
osc.frequency.exponentialRampToValueAtTime(150, now + 0.08);
|
|
180
|
+
gain.gain.setValueAtTime(0.35, now);
|
|
181
|
+
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.08);
|
|
182
|
+
osc.connect(gain).connect(ctx.destination);
|
|
183
|
+
osc.start(now);
|
|
184
|
+
osc.stop(now + 0.08);`,
|
|
185
|
+
|
|
186
|
+
'slider-tick': `const ctx = new AudioContext();
|
|
187
|
+
const now = ctx.currentTime;
|
|
188
|
+
const osc = ctx.createOscillator();
|
|
189
|
+
const gain = ctx.createGain();
|
|
190
|
+
osc.type = 'square';
|
|
191
|
+
osc.frequency.setValueAtTime(5000, now);
|
|
192
|
+
osc.frequency.exponentialRampToValueAtTime(1200, now + 0.005);
|
|
193
|
+
gain.gain.setValueAtTime(0.08, now);
|
|
194
|
+
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.01);
|
|
195
|
+
osc.connect(gain).connect(ctx.destination);
|
|
196
|
+
osc.start(now);
|
|
197
|
+
osc.stop(now + 0.01);`,
|
|
198
|
+
|
|
199
|
+
'key-press': `// Usage: element.onkeydown = (e) => { if (!e.repeat) playKey(); }
|
|
200
|
+
function playKey() {
|
|
201
|
+
const ctx = new AudioContext();
|
|
202
|
+
const now = ctx.currentTime;
|
|
203
|
+
// Lubed Cherry MX Brown — deep muted thock, no click
|
|
204
|
+
const v = () => 0.94 + Math.random() * 0.12;
|
|
205
|
+
const drift = Math.random() * 0.0015;
|
|
206
|
+
// Layer 1: bottom-out thock (main sound)
|
|
207
|
+
const thud = ctx.createOscillator();
|
|
208
|
+
const thudGain = ctx.createGain();
|
|
209
|
+
const thudFilter = ctx.createBiquadFilter();
|
|
210
|
+
thud.type = 'sine';
|
|
211
|
+
thud.frequency.setValueAtTime(95 * v(), now + drift);
|
|
212
|
+
thud.frequency.exponentialRampToValueAtTime(38, now + drift + 0.04);
|
|
213
|
+
thudFilter.type = 'lowpass';
|
|
214
|
+
thudFilter.frequency.setValueAtTime(300, now + drift);
|
|
215
|
+
thudFilter.Q.setValueAtTime(0.5, now + drift);
|
|
216
|
+
thudGain.gain.setValueAtTime(0.22, now + drift);
|
|
217
|
+
thudGain.gain.exponentialRampToValueAtTime(0.001, now + drift + 0.055);
|
|
218
|
+
thud.connect(thudFilter).connect(thudGain).connect(ctx.destination);
|
|
219
|
+
thud.start(now + drift); thud.stop(now + drift + 0.06);
|
|
220
|
+
// Layer 2: tactile bump (barely audible)
|
|
221
|
+
const bump = ctx.createOscillator();
|
|
222
|
+
const bumpGain = ctx.createGain();
|
|
223
|
+
const bumpFilter = ctx.createBiquadFilter();
|
|
224
|
+
bump.type = 'triangle';
|
|
225
|
+
bump.frequency.setValueAtTime(420 * v(), now);
|
|
226
|
+
bump.frequency.exponentialRampToValueAtTime(180, now + 0.008);
|
|
227
|
+
bumpFilter.type = 'lowpass';
|
|
228
|
+
bumpFilter.frequency.setValueAtTime(600, now);
|
|
229
|
+
bumpGain.gain.setValueAtTime(0.03, now);
|
|
230
|
+
bumpGain.gain.exponentialRampToValueAtTime(0.001, now + 0.012);
|
|
231
|
+
bump.connect(bumpFilter).connect(bumpGain).connect(ctx.destination);
|
|
232
|
+
bump.start(now); bump.stop(now + 0.012);
|
|
233
|
+
// Layer 3: dampened noise (soft low puff, lube kills scratchiness)
|
|
234
|
+
const bufLen = Math.floor(ctx.sampleRate * 0.03);
|
|
235
|
+
const buf = ctx.createBuffer(1, bufLen, ctx.sampleRate);
|
|
236
|
+
const d = buf.getChannelData(0);
|
|
237
|
+
for (let i = 0; i < bufLen; i++) d[i] = Math.random() * 2 - 1;
|
|
238
|
+
const noise = ctx.createBufferSource();
|
|
239
|
+
noise.buffer = buf;
|
|
240
|
+
const noiseLp = ctx.createBiquadFilter();
|
|
241
|
+
noiseLp.type = 'lowpass';
|
|
242
|
+
noiseLp.frequency.setValueAtTime(400 * v(), now + drift);
|
|
243
|
+
noiseLp.Q.setValueAtTime(0.3, now + drift);
|
|
244
|
+
const noiseGain = ctx.createGain();
|
|
245
|
+
noiseGain.gain.setValueAtTime(0.025, now + drift);
|
|
246
|
+
noiseGain.gain.exponentialRampToValueAtTime(0.001, now + drift + 0.025);
|
|
247
|
+
noise.connect(noiseLp).connect(noiseGain).connect(ctx.destination);
|
|
248
|
+
noise.start(now + drift); noise.stop(now + drift + 0.03);
|
|
249
|
+
}`,
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
interface SoundCardProps {
|
|
253
|
+
name: SoundName;
|
|
254
|
+
label: string;
|
|
255
|
+
category: string;
|
|
256
|
+
description: string;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function InteractivePreview({ name }: { name: SoundName }) {
|
|
260
|
+
const [switchOn, setSwitchOn] = useState(false);
|
|
261
|
+
const [checked, setChecked] = useState(false);
|
|
262
|
+
const [sliderVal, setSliderVal] = useState([50]);
|
|
263
|
+
const [notifVisible, setNotifVisible] = useState(false);
|
|
264
|
+
const [successVisible, setSuccessVisible] = useState(false);
|
|
265
|
+
const [errorVisible, setErrorVisible] = useState(false);
|
|
266
|
+
|
|
267
|
+
switch (name) {
|
|
268
|
+
case 'button-click':
|
|
269
|
+
return (
|
|
270
|
+
<Button
|
|
271
|
+
size="sm"
|
|
272
|
+
onClick={() => playSound('button-click')}
|
|
273
|
+
className="w-full"
|
|
274
|
+
>
|
|
275
|
+
Click Me
|
|
276
|
+
</Button>
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
case 'button-click-secondary':
|
|
280
|
+
return (
|
|
281
|
+
<Button
|
|
282
|
+
size="sm"
|
|
283
|
+
variant="secondary"
|
|
284
|
+
onClick={() => playSound('button-click-secondary')}
|
|
285
|
+
className="w-full"
|
|
286
|
+
>
|
|
287
|
+
Secondary Action
|
|
288
|
+
</Button>
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
case 'hover-blip':
|
|
292
|
+
return (
|
|
293
|
+
<div
|
|
294
|
+
onMouseEnter={() => playSound('hover-blip')}
|
|
295
|
+
className="flex w-full cursor-pointer items-center justify-center rounded-md border border-dashed border-border py-3 text-xs text-muted-foreground transition-colors hover:border-primary hover:bg-accent hover:text-foreground"
|
|
296
|
+
>
|
|
297
|
+
Hover over me
|
|
298
|
+
</div>
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
case 'hover-soft':
|
|
302
|
+
return (
|
|
303
|
+
<div className="flex w-full gap-2">
|
|
304
|
+
{['Link 1', 'Link 2', 'Link 3'].map((label) => (
|
|
305
|
+
<span
|
|
306
|
+
key={label}
|
|
307
|
+
onMouseEnter={() => playSound('hover-soft')}
|
|
308
|
+
className="flex-1 cursor-pointer rounded-md py-2 text-center text-xs text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
|
309
|
+
>
|
|
310
|
+
{label}
|
|
311
|
+
</span>
|
|
312
|
+
))}
|
|
313
|
+
</div>
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
case 'success-chime':
|
|
317
|
+
return (
|
|
318
|
+
<div className="flex w-full flex-col gap-2">
|
|
319
|
+
<Button
|
|
320
|
+
size="sm"
|
|
321
|
+
variant="outline"
|
|
322
|
+
onClick={() => {
|
|
323
|
+
playSound('success-chime');
|
|
324
|
+
setSuccessVisible(true);
|
|
325
|
+
setTimeout(() => setSuccessVisible(false), 1500);
|
|
326
|
+
}}
|
|
327
|
+
className="w-full"
|
|
328
|
+
>
|
|
329
|
+
Save Changes
|
|
330
|
+
</Button>
|
|
331
|
+
{successVisible && (
|
|
332
|
+
<p className="text-center text-xs text-green-600 dark:text-green-400" aria-live="polite">✓ Saved successfully!</p>
|
|
333
|
+
)}
|
|
334
|
+
</div>
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
case 'success-bell':
|
|
338
|
+
return (
|
|
339
|
+
<div className="flex w-full flex-col gap-2">
|
|
340
|
+
<div className="flex items-center gap-2">
|
|
341
|
+
<Checkbox
|
|
342
|
+
checked={checked}
|
|
343
|
+
onCheckedChange={() => {
|
|
344
|
+
setChecked(!checked);
|
|
345
|
+
if (!checked) playSound('success-bell');
|
|
346
|
+
}}
|
|
347
|
+
/>
|
|
348
|
+
<label className="text-xs text-card-foreground cursor-pointer" onClick={() => {
|
|
349
|
+
setChecked(!checked);
|
|
350
|
+
if (!checked) playSound('success-bell');
|
|
351
|
+
}}>
|
|
352
|
+
Mark as complete
|
|
353
|
+
</label>
|
|
354
|
+
</div>
|
|
355
|
+
</div>
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
case 'error-buzz':
|
|
359
|
+
return (
|
|
360
|
+
<div className="flex w-full flex-col gap-2">
|
|
361
|
+
<Button
|
|
362
|
+
size="sm"
|
|
363
|
+
variant="destructive"
|
|
364
|
+
onClick={() => {
|
|
365
|
+
playSound('error-buzz');
|
|
366
|
+
setErrorVisible(true);
|
|
367
|
+
setTimeout(() => setErrorVisible(false), 1500);
|
|
368
|
+
}}
|
|
369
|
+
className="w-full"
|
|
370
|
+
>
|
|
371
|
+
Delete Item
|
|
372
|
+
</Button>
|
|
373
|
+
{errorVisible && (
|
|
374
|
+
<p className="text-center text-xs text-red-600 dark:text-red-400" aria-live="polite">✗ Action failed!</p>
|
|
375
|
+
)}
|
|
376
|
+
</div>
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
case 'error-beep':
|
|
380
|
+
return (
|
|
381
|
+
<Button
|
|
382
|
+
size="sm"
|
|
383
|
+
variant="outline"
|
|
384
|
+
onClick={() => playSound('error-beep')}
|
|
385
|
+
className="w-full border-destructive text-destructive hover:bg-destructive/10"
|
|
386
|
+
>
|
|
387
|
+
Invalid Input ✗
|
|
388
|
+
</Button>
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
case 'notification-ping':
|
|
392
|
+
return (
|
|
393
|
+
<div className="flex w-full flex-col gap-2">
|
|
394
|
+
<Button
|
|
395
|
+
size="sm"
|
|
396
|
+
variant="outline"
|
|
397
|
+
onClick={() => {
|
|
398
|
+
playSound('notification-ping');
|
|
399
|
+
setNotifVisible(true);
|
|
400
|
+
setTimeout(() => setNotifVisible(false), 2000);
|
|
401
|
+
}}
|
|
402
|
+
className="w-full"
|
|
403
|
+
>
|
|
404
|
+
Send Notification
|
|
405
|
+
</Button>
|
|
406
|
+
{notifVisible && (
|
|
407
|
+
<div className="rounded-md bg-primary px-3 py-2 text-xs text-primary-foreground text-center animate-in fade-in slide-in-from-top-1" aria-live="polite">
|
|
408
|
+
New message received
|
|
409
|
+
</div>
|
|
410
|
+
)}
|
|
411
|
+
</div>
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
case 'notification-subtle':
|
|
415
|
+
return (
|
|
416
|
+
<div className="flex w-full items-center justify-between">
|
|
417
|
+
<span className="text-xs text-card-foreground">Enable notifications</span>
|
|
418
|
+
<Switch
|
|
419
|
+
checked={switchOn}
|
|
420
|
+
onCheckedChange={(val) => {
|
|
421
|
+
setSwitchOn(val);
|
|
422
|
+
playSound('notification-subtle');
|
|
423
|
+
}}
|
|
424
|
+
/>
|
|
425
|
+
</div>
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
case 'swoosh':
|
|
429
|
+
return (
|
|
430
|
+
<div className="flex w-full gap-2">
|
|
431
|
+
{['Tab 1', 'Tab 2', 'Tab 3'].map((label, i) => (
|
|
432
|
+
<button
|
|
433
|
+
key={label}
|
|
434
|
+
onClick={() => playSound('swoosh')}
|
|
435
|
+
className={`flex-1 rounded-md py-2 text-xs font-medium transition-colors ${
|
|
436
|
+
i === 0
|
|
437
|
+
? 'bg-primary text-primary-foreground'
|
|
438
|
+
: 'text-muted-foreground hover:bg-accent hover:text-foreground'
|
|
439
|
+
}`}
|
|
440
|
+
>
|
|
441
|
+
{label}
|
|
442
|
+
</button>
|
|
443
|
+
))}
|
|
444
|
+
</div>
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
case 'pop':
|
|
448
|
+
return (
|
|
449
|
+
<TooltipProvider>
|
|
450
|
+
<Tooltip onOpenChange={(open) => open && playSound('pop')}>
|
|
451
|
+
<TooltipTrigger asChild>
|
|
452
|
+
<Button
|
|
453
|
+
size="sm"
|
|
454
|
+
variant="outline"
|
|
455
|
+
className="w-full"
|
|
456
|
+
>
|
|
457
|
+
Hover for Tooltip 💬
|
|
458
|
+
</Button>
|
|
459
|
+
</TooltipTrigger>
|
|
460
|
+
<TooltipContent>
|
|
461
|
+
<p className="text-xs">This is a tooltip!</p>
|
|
462
|
+
</TooltipContent>
|
|
463
|
+
</Tooltip>
|
|
464
|
+
</TooltipProvider>
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
case 'slider-tick':
|
|
468
|
+
return (
|
|
469
|
+
<div className="flex w-full flex-col gap-2 px-1">
|
|
470
|
+
<Slider
|
|
471
|
+
value={sliderVal}
|
|
472
|
+
onValueChange={(val) => {
|
|
473
|
+
setSliderVal(val);
|
|
474
|
+
playSound('slider-tick');
|
|
475
|
+
}}
|
|
476
|
+
max={100}
|
|
477
|
+
step={5}
|
|
478
|
+
/>
|
|
479
|
+
<p className="text-center text-xs text-muted-foreground">
|
|
480
|
+
Volume: {sliderVal[0]}%
|
|
481
|
+
</p>
|
|
482
|
+
</div>
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
case 'key-press':
|
|
486
|
+
return (
|
|
487
|
+
<input
|
|
488
|
+
type="text"
|
|
489
|
+
placeholder="Type something…"
|
|
490
|
+
onKeyDown={(e) => { if (!e.repeat) playSound('key-press'); }}
|
|
491
|
+
className="w-full rounded-md border border-border bg-background px-3 py-2 text-xs text-foreground outline-none placeholder:text-muted-foreground focus:ring-1 focus:ring-ring"
|
|
492
|
+
/>
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
default:
|
|
496
|
+
return (
|
|
497
|
+
<Button
|
|
498
|
+
size="sm"
|
|
499
|
+
variant="outline"
|
|
500
|
+
onClick={() => playSound(name)}
|
|
501
|
+
className="w-full"
|
|
502
|
+
>
|
|
503
|
+
Play Sound
|
|
504
|
+
</Button>
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function CopyIcon({ className }: { className?: string }) {
|
|
510
|
+
return (
|
|
511
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
|
|
512
|
+
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
|
|
513
|
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
|
514
|
+
</svg>
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function CheckIcon({ className }: { className?: string }) {
|
|
519
|
+
return (
|
|
520
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
|
|
521
|
+
<polyline points="20 6 9 17 4 12" />
|
|
522
|
+
</svg>
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function DownloadIcon({ className }: { className?: string }) {
|
|
527
|
+
return (
|
|
528
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
|
|
529
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
530
|
+
<polyline points="7 10 12 15 17 10" />
|
|
531
|
+
<line x1="12" y1="15" x2="12" y2="3" />
|
|
532
|
+
</svg>
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const mp3Map: Partial<Record<SoundName, string>> = {
|
|
537
|
+
'button-click': '/click1.mp3',
|
|
538
|
+
'button-click-secondary': '/click2.mp3',
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
export function SoundCard({
|
|
542
|
+
name,
|
|
543
|
+
label,
|
|
544
|
+
category,
|
|
545
|
+
description,
|
|
546
|
+
}: SoundCardProps) {
|
|
547
|
+
const [copied, setCopied] = useState(false);
|
|
548
|
+
const isMp3 = name === 'button-click' || name === 'button-click-secondary';
|
|
549
|
+
|
|
550
|
+
const handleCopyCode = () => {
|
|
551
|
+
if (isMp3) return;
|
|
552
|
+
playSound('notification-subtle');
|
|
553
|
+
navigator.clipboard.writeText(soundSnippets[name]).then(() => {
|
|
554
|
+
setCopied(true);
|
|
555
|
+
setTimeout(() => setCopied(false), 2000);
|
|
556
|
+
});
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
const handleDownload = () => {
|
|
560
|
+
playSound('notification-subtle');
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
return (
|
|
564
|
+
<div className="relative flex flex-col gap-3 rounded-lg border border-border bg-card p-4 transition-colors">
|
|
565
|
+
{isMp3 ? (
|
|
566
|
+
<a
|
|
567
|
+
href={mp3Map[name]}
|
|
568
|
+
download
|
|
569
|
+
aria-label="Download mp3"
|
|
570
|
+
onClick={handleDownload}
|
|
571
|
+
className="absolute right-3 top-3 rounded-md border border-border bg-muted p-1.5 text-muted-foreground transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
572
|
+
>
|
|
573
|
+
<DownloadIcon />
|
|
574
|
+
</a>
|
|
575
|
+
) : (
|
|
576
|
+
<button
|
|
577
|
+
onClick={handleCopyCode}
|
|
578
|
+
aria-label={copied ? 'Copied' : 'Copy code'}
|
|
579
|
+
className="absolute right-3 top-3 rounded-md border border-border bg-muted p-1.5 text-muted-foreground transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
580
|
+
>
|
|
581
|
+
{copied ? <CheckIcon /> : <CopyIcon />}
|
|
582
|
+
</button>
|
|
583
|
+
)}
|
|
584
|
+
|
|
585
|
+
<div className="flex flex-col gap-1 pr-10">
|
|
586
|
+
<h3 className="font-sans text-sm font-semibold text-card-foreground">{label}</h3>
|
|
587
|
+
<p className="text-xs text-muted-foreground">{category}</p>
|
|
588
|
+
</div>
|
|
589
|
+
|
|
590
|
+
<p className="text-xs leading-relaxed text-muted-foreground">{description}</p>
|
|
591
|
+
|
|
592
|
+
<InteractivePreview name={name} />
|
|
593
|
+
</div>
|
|
594
|
+
);
|
|
595
|
+
}
|