@manas-dev/sound-lab 1.0.1
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.
Potentially problematic release.
This version of @manas-dev/sound-lab might be problematic. Click here for more details.
- package/README.md +84 -0
- package/assets/preview.svg +57 -0
- package/dist/index.cjs.js +305 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.esm.js +301 -0
- package/dist/index.esm.js.map +1 -0
- package/package.json +36 -0
- package/postcss.config.js +6 -0
- package/rollup.config.js +27 -0
- package/src/index.tsx +211 -0
- package/tailwind.config.js +9 -0
- package/tsconfig.json +32 -0
package/src/index.tsx
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
2
|
+
import { Music, X, Sparkles, Keyboard, Gamepad2, Box, Headphones, Zap, Droplets } from 'lucide-react';
|
|
3
|
+
|
|
4
|
+
export interface SoundProfile {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
icon: any; // Lucide icon component
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const DEFAULT_SOUNDS: SoundProfile[] = [
|
|
11
|
+
{ id: 'glass', name: 'Glass', icon: Sparkles },
|
|
12
|
+
{ id: 'thock', name: 'Thock', icon: Keyboard },
|
|
13
|
+
{ id: 'console', name: 'Console', icon: Gamepad2 },
|
|
14
|
+
{ id: 'minecraft', name: 'Minecraft', icon: Box },
|
|
15
|
+
{ id: 'retro', name: 'Retro', icon: Gamepad2 },
|
|
16
|
+
{ id: 'lofi', name: 'Lofi', icon: Headphones },
|
|
17
|
+
{ id: 'mechanical', name: 'Clicky', icon: Keyboard },
|
|
18
|
+
{ id: 'scifi', name: 'Sci-Fi', icon: Zap },
|
|
19
|
+
{ id: 'bubble', name: 'Bubble', icon: Droplets },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
export interface SoundLabProps {
|
|
23
|
+
isSoundEnabled: boolean;
|
|
24
|
+
setIsSoundEnabled: (enabled: boolean) => void;
|
|
25
|
+
volume: number;
|
|
26
|
+
setVolume: (volume: number) => void;
|
|
27
|
+
soundProfile: string;
|
|
28
|
+
setSoundProfile: (profileId: string) => void;
|
|
29
|
+
isDark: boolean;
|
|
30
|
+
playSound?: (type: 'click' | 'hover' | 'switch') => void;
|
|
31
|
+
primaryColor?: string; // Hex color for active states
|
|
32
|
+
className?: string;
|
|
33
|
+
sounds?: SoundProfile[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const SoundLab: React.FC<SoundLabProps> = ({
|
|
37
|
+
isSoundEnabled,
|
|
38
|
+
setIsSoundEnabled,
|
|
39
|
+
volume,
|
|
40
|
+
setVolume,
|
|
41
|
+
soundProfile,
|
|
42
|
+
setSoundProfile,
|
|
43
|
+
isDark,
|
|
44
|
+
playSound = () => { },
|
|
45
|
+
primaryColor = '#3b82f6', // Default blue-500
|
|
46
|
+
className = '',
|
|
47
|
+
sounds = DEFAULT_SOUNDS
|
|
48
|
+
}) => {
|
|
49
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
50
|
+
const [toastMessage, setToastMessage] = useState<string | null>(null);
|
|
51
|
+
const [showToast, setShowToast] = useState(false);
|
|
52
|
+
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
53
|
+
|
|
54
|
+
const showNotification = (message: string) => {
|
|
55
|
+
setToastMessage(message);
|
|
56
|
+
setShowToast(true);
|
|
57
|
+
setTimeout(() => setShowToast(false), 2000);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const handleMouseEnter = () => {
|
|
61
|
+
if (timeoutRef.current) {
|
|
62
|
+
clearTimeout(timeoutRef.current);
|
|
63
|
+
timeoutRef.current = null;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const handleMouseLeave = () => {
|
|
68
|
+
timeoutRef.current = setTimeout(() => {
|
|
69
|
+
setIsOpen(false);
|
|
70
|
+
}, 1000);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
return () => {
|
|
75
|
+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
76
|
+
};
|
|
77
|
+
}, []);
|
|
78
|
+
|
|
79
|
+
// Helper for Hex transparency
|
|
80
|
+
const hexToRgba = (hex: string, alpha: number) => {
|
|
81
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
82
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
83
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
84
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<div className={`relative ${className}`}>
|
|
89
|
+
{/* Toast Notification */}
|
|
90
|
+
{showToast && (
|
|
91
|
+
<div className={`fixed top-20 right-4 px-4 py-2 rounded-xl shadow-lg backdrop-blur-md border animate-in slide-in-from-top-2 fade-in duration-300 z-[60] whitespace-nowrap
|
|
92
|
+
${isDark ? 'bg-zinc-800/90 border-zinc-700 text-white' : 'bg-white/90 border-slate-200 text-slate-800'}`}>
|
|
93
|
+
<span className="text-xs font-bold">{toastMessage}</span>
|
|
94
|
+
</div>
|
|
95
|
+
)}
|
|
96
|
+
|
|
97
|
+
{/* Trigger Button */}
|
|
98
|
+
<button
|
|
99
|
+
onClick={() => { setIsOpen(!isOpen); playSound('click'); }}
|
|
100
|
+
onMouseEnter={() => playSound('hover')}
|
|
101
|
+
className={`p-2 md:p-3 rounded-full transition-all duration-300
|
|
102
|
+
${isOpen || isSoundEnabled
|
|
103
|
+
? (isDark ? `bg-zinc-800` : `bg-white`)
|
|
104
|
+
: (isDark ? 'text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800/50' : 'text-slate-400 hover:text-slate-600 hover:bg-slate-50')
|
|
105
|
+
}`}
|
|
106
|
+
style={isOpen || isSoundEnabled ? { color: primaryColor } : {}}
|
|
107
|
+
aria-label="Open sound menu"
|
|
108
|
+
>
|
|
109
|
+
{isSoundEnabled ? <Music size={20} className="md:w-6 md:h-6 animate-pulse" /> : <Music size={20} className="md:w-6 md:h-6" />}
|
|
110
|
+
</button>
|
|
111
|
+
|
|
112
|
+
{/* Sound Lab Popup */}
|
|
113
|
+
{isOpen && (
|
|
114
|
+
<div
|
|
115
|
+
onMouseEnter={handleMouseEnter}
|
|
116
|
+
onMouseLeave={handleMouseLeave}
|
|
117
|
+
className={`absolute top-full right-0 mt-4 p-4 rounded-3xl border w-[280px] backdrop-blur-xl shadow-2xl animate-in slide-in-from-top-5 fade-in z-50
|
|
118
|
+
${isDark ? 'bg-zinc-900/95 border-zinc-800' : 'bg-white/95 border-white/60'}`}>
|
|
119
|
+
|
|
120
|
+
<div className="flex justify-between items-center mb-4">
|
|
121
|
+
<div className="flex items-center gap-2">
|
|
122
|
+
<Music size={14} className={isDark ? 'text-zinc-400' : 'text-slate-400'} />
|
|
123
|
+
<span className={`text-xs font-bold uppercase tracking-wider ${isDark ? 'text-zinc-500' : 'text-slate-500'}`}>Sound Lab</span>
|
|
124
|
+
</div>
|
|
125
|
+
<button
|
|
126
|
+
onClick={() => { setIsOpen(false); playSound('click'); }}
|
|
127
|
+
className={`p-1 rounded-full ${isDark ? 'hover:bg-zinc-800' : 'hover:bg-slate-100'}`}
|
|
128
|
+
aria-label="Close sound menu"
|
|
129
|
+
>
|
|
130
|
+
<X size={14} className={isDark ? 'text-zinc-400' : 'text-slate-400'} />
|
|
131
|
+
</button>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
{/* Master Toggle */}
|
|
135
|
+
<div className={`flex items-center justify-between p-3 rounded-xl mb-4 transition-colors
|
|
136
|
+
${isDark ? 'bg-zinc-800/50' : 'bg-slate-50'}`}>
|
|
137
|
+
<span className={`text-xs font-medium ${isDark ? 'text-zinc-300' : 'text-slate-600'}`}>Master Audio</span>
|
|
138
|
+
<button
|
|
139
|
+
onClick={() => {
|
|
140
|
+
setIsSoundEnabled(!isSoundEnabled);
|
|
141
|
+
if (!isSoundEnabled) { // If turning ON, play a sound
|
|
142
|
+
setTimeout(() => playSound('switch'), 50);
|
|
143
|
+
showNotification('Sound Enabled');
|
|
144
|
+
} else {
|
|
145
|
+
showNotification('Sound Disabled');
|
|
146
|
+
}
|
|
147
|
+
}}
|
|
148
|
+
className={`relative w-10 h-6 rounded-full transition-colors duration-300 ${isDark ? 'bg-zinc-700' : 'bg-slate-300'}`}
|
|
149
|
+
style={{ backgroundColor: isSoundEnabled ? primaryColor : undefined }}
|
|
150
|
+
aria-label={isSoundEnabled ? "Disable sound" : "Enable sound"}
|
|
151
|
+
>
|
|
152
|
+
<span className={`absolute top-1 left-1 w-4 h-4 rounded-full bg-white transition-transform duration-300 ${isSoundEnabled ? 'translate-x-4' : 'translate-x-0'}`}></span>
|
|
153
|
+
</button>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
{/* Volume Slider */}
|
|
157
|
+
<div className={`p-3 rounded-xl mb-4 transition-colors ${isDark ? 'bg-zinc-800/50' : 'bg-slate-50'}`}>
|
|
158
|
+
<div className="flex justify-between items-center mb-2">
|
|
159
|
+
<span className={`text-xs font-medium ${isDark ? 'text-zinc-300' : 'text-slate-600'}`}>Volume</span>
|
|
160
|
+
<span className="text-xs font-bold" style={{ color: primaryColor }}>{Math.round(volume * 100)}%</span>
|
|
161
|
+
</div>
|
|
162
|
+
<input
|
|
163
|
+
type="range"
|
|
164
|
+
min="0"
|
|
165
|
+
max="1"
|
|
166
|
+
step="0.05"
|
|
167
|
+
value={volume}
|
|
168
|
+
onChange={(e) => {
|
|
169
|
+
setVolume(parseFloat(e.target.value));
|
|
170
|
+
if (!isSoundEnabled) setIsSoundEnabled(true);
|
|
171
|
+
}}
|
|
172
|
+
className={`w-full h-1.5 rounded-full appearance-none cursor-pointer ${isDark ? 'bg-zinc-700' : 'bg-slate-300'} accent-current`}
|
|
173
|
+
style={{ accentColor: primaryColor }}
|
|
174
|
+
aria-label="Volume control"
|
|
175
|
+
/>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<div className="grid grid-cols-3 gap-2">
|
|
179
|
+
{sounds.map((profile) => (
|
|
180
|
+
<button
|
|
181
|
+
key={profile.id}
|
|
182
|
+
onClick={() => {
|
|
183
|
+
setSoundProfile(profile.id);
|
|
184
|
+
if (!isSoundEnabled) setIsSoundEnabled(true);
|
|
185
|
+
setTimeout(() => playSound('click'), 50);
|
|
186
|
+
showNotification(`Profile: ${profile.name}`);
|
|
187
|
+
}}
|
|
188
|
+
className={`flex flex-col items-center gap-1 p-2 rounded-xl border transition-all duration-300
|
|
189
|
+
${isDark ? 'bg-transparent border-transparent hover:bg-zinc-800' : 'bg-transparent border-transparent hover:bg-slate-100'}`}
|
|
190
|
+
style={soundProfile === profile.id ? {
|
|
191
|
+
backgroundColor: hexToRgba(primaryColor, 0.1),
|
|
192
|
+
borderColor: hexToRgba(primaryColor, 0.5)
|
|
193
|
+
} : {}}
|
|
194
|
+
>
|
|
195
|
+
<profile.icon size={16}
|
|
196
|
+
className={isDark ? 'text-zinc-500' : 'text-slate-400'}
|
|
197
|
+
style={soundProfile === profile.id ? { color: primaryColor } : {}}
|
|
198
|
+
/>
|
|
199
|
+
<span className={`text-[10px] font-medium ${isDark ? 'text-zinc-500' : 'text-slate-500'}`}
|
|
200
|
+
style={soundProfile === profile.id ? { color: isDark ? 'white' : primaryColor } : {}}
|
|
201
|
+
>
|
|
202
|
+
{profile.name}
|
|
203
|
+
</span>
|
|
204
|
+
</button>
|
|
205
|
+
))}
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
)}
|
|
209
|
+
</div>
|
|
210
|
+
);
|
|
211
|
+
};
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es5",
|
|
4
|
+
"lib": [
|
|
5
|
+
"dom",
|
|
6
|
+
"dom.iterable",
|
|
7
|
+
"esnext"
|
|
8
|
+
],
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"allowSyntheticDefaultImports": true,
|
|
13
|
+
"strict": true,
|
|
14
|
+
"forceConsistentCasingInFileNames": true,
|
|
15
|
+
"noFallthroughCasesInSwitch": true,
|
|
16
|
+
"module": "esnext",
|
|
17
|
+
"moduleResolution": "node",
|
|
18
|
+
"resolveJsonModule": true,
|
|
19
|
+
"isolatedModules": true,
|
|
20
|
+
"jsx": "react-jsx",
|
|
21
|
+
"declaration": true,
|
|
22
|
+
"rootDir": "src",
|
|
23
|
+
"outDir": "dist"
|
|
24
|
+
},
|
|
25
|
+
"include": [
|
|
26
|
+
"src"
|
|
27
|
+
],
|
|
28
|
+
"exclude": [
|
|
29
|
+
"node_modules",
|
|
30
|
+
"dist"
|
|
31
|
+
]
|
|
32
|
+
}
|