aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
463 lines (460 loc) • 16.7 kB
JavaScript
'use client';
import { jsxs, jsx } from 'react/jsx-runtime';
import { useReducedMotion } from '../../hooks/useReducedMotion.js';
import { forwardRef, useState, useRef, useEffect } from 'react';
import { AnimatePresence, motion } from 'framer-motion';
import '../../primitives/GlassCore.js';
import '../../primitives/glass/GlassAdvanced.js';
import { OptimizedGlassCore } from '../../primitives/OptimizedGlassCore.js';
import '../../primitives/glass/OptimizedGlassAdvanced.js';
import '../../primitives/MotionNative.js';
import '../../primitives/motion/MotionFramer.js';
import { useGlassSound } from '../../utils/soundDesign.js';
import { useA11yId } from '../../utils/a11y.js';
import { useMotionPreference } from '../../hooks/useMotionPreference.js';
import { createGlassStyle } from '../../utils/createGlassStyle.js';
const activityColors = {
low: "var(--glass-gray-600)",
medium: "var(--glass-color-success)",
high: "var(--glass-color-warning)",
peak: "var(--glass-color-danger)"
};
const rainbowColors = ["#FF6B6B", "#4ECDC4", "#45B7D1", "#96CEB4", "#FECA57", "#FF9FF3", "#54A0FF", "#5F27CD", "#00D2D3", "#FF9F43"];
const GlassVoiceWaveform = /*#__PURE__*/forwardRef(({
participants,
currentUserId,
showAvatars = true,
showNames = true,
showMuteStatus = true,
showConnectionStatus = true,
waveformStyle = "bars",
sensitivity = 1,
smoothing = 0.8,
colorMode = "participant",
maxBars = 32,
animationSpeed = 1,
realTimeMode = false,
soundVisualization = true,
showVoiceActivity = true,
compactMode = false,
onParticipantClick,
onMuteToggle,
className = "",
...props
}, ref) => {
const prefersReducedMotion = useReducedMotion();
const [audioData, setAudioData] = useState({});
const [speakingHistory, setSpeakingHistory] = useState({});
const [simulatedParticipants, setSimulatedParticipants] = useState(participants);
const animationFrameRef = useRef();
const participantsRef = useRef(simulatedParticipants);
const settingsRef = useRef({
maxBars,
smoothing
});
const {
play
} = useGlassSound();
useA11yId("glass-voice-waveform");
const {
shouldAnimate
} = useMotionPreference();
// Generate realistic audio waveform data
const generateWaveformData = (audioLevel, bars) => {
const data = [];
const baseLevel = audioLevel * sensitivity;
for (let i = 0; i < bars; i++) {
const frequency = i / bars * Math.PI * 4;
let amplitude = Math.sin(frequency) * baseLevel;
// Add some randomness for more realistic waveform
amplitude += (Math.random() - 0.5) * baseLevel * 0.3;
// Apply frequency-based falloff (higher frequencies typically lower amplitude)
amplitude *= Math.max(0.1, 1 - i / bars * 0.7);
data.push(Math.max(0, Math.min(1, amplitude)));
}
return data;
};
// Simulated real-time audio activity
useEffect(() => {
if (!realTimeMode) return;
const interval = setInterval(() => {
setSimulatedParticipants(prev => prev.map(participant => {
if (!participant.isConnected) return participant;
const wasSpeaking = participant.isSpeaking;
const newIsSpeaking = Math.random() < 0.3;
const newAudioLevel = newIsSpeaking ? Math.random() * 0.8 + 0.2 : Math.random() * 0.1;
// Sound notification when someone starts speaking
if (!wasSpeaking && newIsSpeaking && soundVisualization) {
play("notification");
}
return {
...participant,
isSpeaking: newIsSpeaking,
audioLevel: newAudioLevel,
lastActivity: newIsSpeaking ? Date.now() : participant.lastActivity
};
}));
}, 200);
return () => clearInterval(interval);
}, [realTimeMode, soundVisualization, play]);
// Keep refs in sync to avoid effect restarts while RAF loop runs
useEffect(() => {
participantsRef.current = simulatedParticipants;
}, [simulatedParticipants]);
useEffect(() => {
settingsRef.current = {
maxBars,
smoothing
};
}, [maxBars, smoothing]);
// Update audio data for waveforms (single RAF loop gated by realTimeMode)
useEffect(() => {
if (!realTimeMode) return;
let mounted = true;
const updateAudioData = () => {
if (!mounted) return;
const {
maxBars: bars,
smoothing: smooth
} = settingsRef.current;
const snapshot = participantsRef.current;
setAudioData(prev => {
const updated = {};
snapshot.forEach(participant => {
const prevData = prev[participant.id] || new Array(bars).fill(0);
if (participant.isSpeaking && participant.audioLevel > 0.1) {
const nextData = generateWaveformData(participant.audioLevel, bars);
const smoothed = prevData.map((current, index) => current * smooth + nextData[index] * (1 - smooth));
updated[participant.id] = smoothed;
} else {
updated[participant.id] = prevData.map(v => v * 0.9);
}
});
return updated;
});
animationFrameRef.current = requestAnimationFrame(updateAudioData);
};
animationFrameRef.current = requestAnimationFrame(updateAudioData);
return () => {
mounted = false;
if (animationFrameRef.current) cancelAnimationFrame(animationFrameRef.current);
};
}, [realTimeMode]);
// Track speaking history for voice activity
useEffect(() => {
if (!showVoiceActivity) return;
setSpeakingHistory(prev => {
const newHistory = {
...prev
};
const now = Date.now();
simulatedParticipants.forEach(participant => {
if (!newHistory[participant.id]) {
newHistory[participant.id] = [];
}
const history = newHistory[participant.id];
if (participant.isSpeaking) {
history.push(now);
}
// Keep only last 30 seconds of history
newHistory[participant.id] = history.filter(timestamp => now - timestamp < 30000);
});
return newHistory;
});
}, [simulatedParticipants, showVoiceActivity]);
const getParticipantColor = (participant, index) => {
switch (colorMode) {
case "activity":
if (!participant.isSpeaking) return activityColors.low;
if (participant.audioLevel < 0.3) return activityColors.medium;
if (participant.audioLevel < 0.6) return activityColors.high;
return activityColors.peak;
case "rainbow":
return rainbowColors[index % rainbowColors.length];
default:
return participant.color;
}
};
const VoiceActivityIndicator = ({
participant
}) => {
const history = speakingHistory[participant.id] || [];
const activityLevel = history.length / 10; // Normalize to 0-1 range
return jsxs("div", {
className: 'glass-flex glass-items-center space-x-1',
children: [jsx("div", {
className: 'glass-flex space-x-0.5',
children: [0, 1, 2, 3, 4].map(level => jsx("div", {
className: `
w-1 h-3 rounded-full transition-all duration-300
${activityLevel > level * 0.2 ? "bg-green-400" : "bg-white/20"}
`
}, level))
}), jsx("span", {
className: 'glass-text-xs text-primary/60',
children: history.length > 0 ? `${history.length}` : "0"
})]
});
};
const WaveformBars = ({
participant,
data
}) => jsx("div", {
className: 'glass-flex glass-items-end space-x-1 h-12',
children: data.map((amplitude, index) => jsx(motion.div, {
className: 'bg-transparent glass-radius-full',
style: {
width: compactMode ? "2px" : "3px",
color: getParticipantColor(participant, participants.indexOf(participant)),
opacity: participant.isSpeaking ? 0.8 : 0.3
},
animate: {
height: `${Math.max(2, amplitude * 48)}px`,
opacity: participant.isSpeaking ? 0.8 : 0.3
},
transition: shouldAnimate ? {
duration: 0.1 * animationSpeed,
ease: "easeOut"
} : {
duration: 0
}
}, index))
});
const WaveformWaves = ({
participant,
data
}) => jsx("svg", {
width: compactMode ? 120 : 200,
height: 48,
className: 'overflow-visible',
children: jsx("path", {
d: `M 0 24 ${data.map((amplitude, index) => `L ${index / (data.length - 1) * (compactMode ? 120 : 200)} ${24 - amplitude * 20}`).join(" ")} L ${compactMode ? 120 : 200} 24`,
fill: "none",
stroke: getParticipantColor(participant, participants.indexOf(participant)),
strokeWidth: "2",
opacity: participant.isSpeaking ? 0.8 : 0.3
})
});
const WaveformCircular = ({
participant,
data
}) => {
const radius = compactMode ? 20 : 30;
const centerX = radius + 5;
const centerY = radius + 5;
return jsx("svg", {
width: (radius + 5) * 2,
height: (radius + 5) * 2,
className: 'overflow-visible',
children: data.map((amplitude, index) => {
const angle = index / data.length * Math.PI * 2 - Math.PI / 2;
const innerRadius = radius * 0.6;
const outerRadius = innerRadius + amplitude * radius * 0.4;
const x1 = centerX + Math.cos(angle) * innerRadius;
const y1 = centerY + Math.sin(angle) * innerRadius;
const x2 = centerX + Math.cos(angle) * outerRadius;
const y2 = centerY + Math.sin(angle) * outerRadius;
return jsx("line", {
x1: x1,
y1: y1,
x2: x2,
y2: y2,
stroke: getParticipantColor(participant, participants.indexOf(participant)),
strokeWidth: "2",
opacity: participant.isSpeaking ? 0.8 : 0.3
}, index);
})
});
};
const WaveformSpectrum = ({
participant,
data
}) => jsx("div", {
className: 'glass-flex glass-items-end glass-justify-center space-x-px h-12 w-32',
children: data.map((amplitude, index) => jsx(motion.div, {
className: 'bg-transparent',
style: {
width: `${100 / data.length}%`,
color: getParticipantColor(participant, participants.indexOf(participant)),
opacity: participant.isSpeaking ? 0.8 : 0.3
},
animate: {
height: `${Math.max(1, amplitude * 48)}px`
},
transition: shouldAnimate ? {
duration: 0.1 * animationSpeed
} : {
duration: 0
}
}, index))
});
const renderWaveform = participant => {
const data = audioData[participant.id] || new Array(maxBars).fill(0);
switch (waveformStyle) {
case "waves":
return jsx(WaveformWaves, {
participant: participant,
data: data
});
case "circular":
return jsx(WaveformCircular, {
participant: participant,
data: data
});
case "spectrum":
return jsx(WaveformSpectrum, {
participant: participant,
data: data
});
default:
return jsx(WaveformBars, {
participant: participant,
data: data
});
}
};
const ParticipantCard = ({
participant,
index
}) => jsxs(motion.div, {
className: `
flex items-center space-x-3 p-3 rounded-lg cursor-pointer transition-colors duration-200
${createGlassStyle({
blur: "sm",
opacity: 0.8
}).background}
${participant.isSpeaking ? "ring-2 ring-green-400/50" : ""}
hover:bg-white/10
`,
onClick: () => onParticipantClick?.(participant.id),
whileHover: {
scale: 1.02
},
whileTap: {
scale: 0.98
},
children: [showAvatars && jsxs("div", {
className: 'relative',
children: [jsx("div", {
className: `
${compactMode ? "w-8 h-8" : "w-12 h-12"}
rounded-full bg-gradient-to-br from-gray-300 to-gray-500
flex items-center justify-center text-white font-semibold
${createGlassStyle({
blur: "sm",
opacity: 0.8
}).background}
`,
children: participant.avatar ? jsx("img", {
src: participant.avatar,
alt: participant.name,
className: 'glass-w-full glass-h-full glass-radius-full object-cover'
}) : participant.name.charAt(0).toUpperCase()
}), showConnectionStatus && jsx("div", {
className: `
absolute -bottom-1 -right-1 w-3 h-3 rounded-full border-2 border-white
${participant.isConnected ? "bg-green-400" : "bg-red-400"}
`
}), participant.isSpeaking && jsx(motion.div, {
className: 'absolute glass-top-1 -right-1 w-4 h-4 glass-surface-green glass-radius-full',
animate: prefersReducedMotion ? {} : {
scale: [1, 1.2, 1]
},
transition: shouldAnimate ? {
duration: 0.8,
repeat: Infinity
} : {
duration: 0
}
})]
}), jsxs("div", {
className: "glass-flex-1 glass-min-w-0",
children: [showNames && jsxs("div", {
className: 'glass-flex glass-items-center space-x-2',
children: [jsxs("p", {
className: `
font-medium text-white/90 truncate
${compactMode ? "text-sm" : "text-base"}
`,
children: [participant.name, participant.id === currentUserId && " (You)"]
}), showMuteStatus && participant.isMuted && jsx("span", {
className: 'text-primary glass-text-xs',
children: "\uD83D\uDD07"
})]
}), showVoiceActivity && !compactMode && jsx(VoiceActivityIndicator, {
participant: participant
})]
}), jsxs("div", {
className: 'glass-flex glass-items-center space-x-2',
children: [renderWaveform(participant), showMuteStatus && !compactMode && jsx("button", {
onClick: e => {
e.stopPropagation();
onMuteToggle?.(participant.id);
},
className: `
p-1 rounded text-sm transition-colors duration-200 glass-focus glass-touch-target glass-contrast-guard
${participant.isMuted ? "text-red-400 hover:text-red-300" : "text-white/60 hover:text-white"}
`,
children: participant.isMuted ? "🔇" : "🎤"
})]
})]
});
const activeSpeakers = simulatedParticipants.filter(p => p.isSpeaking).length;
const connectedParticipants = simulatedParticipants.filter(p => p.isConnected).length;
return jsxs(OptimizedGlassCore, {
ref: ref,
intensity: "subtle",
className: `p-4 space-y-4 ${className}`,
...props,
children: [jsxs("div", {
className: "glass-flex glass-items-center glass-justify-between",
children: [jsxs("h3", {
className: 'glass-text-lg font-semibold text-primary/90',
children: ["Voice Chat (", connectedParticipants, ")"]
}), jsxs("div", {
className: 'glass-flex glass-items-center space-x-4 glass-text-sm text-primary/60',
children: [activeSpeakers > 0 && jsxs("span", {
className: 'glass-flex glass-items-center space-x-1',
children: [jsx("div", {
className: 'w-2 h-2 glass-surface-green glass-radius-full animate-pulse'
}), jsxs("span", {
children: [activeSpeakers, " speaking"]
})]
}), realTimeMode && jsxs("span", {
className: 'glass-flex glass-items-center space-x-1',
children: [jsx("div", {
className: 'w-2 h-2 glass-surface-blue glass-radius-full animate-pulse'
}), jsx("span", {
children: "Live"
})]
})]
})]
}), jsx("div", {
className: `
space-y-2
${compactMode ? "max-h-64 overflow-y-auto" : ""}
`,
children: jsx(AnimatePresence, {
children: simulatedParticipants.sort((a, b) => {
if (a.isSpeaking && !b.isSpeaking) return -1;
if (!a.isSpeaking && b.isSpeaking) return 1;
if (a.isConnected && !b.isConnected) return -1;
if (!a.isConnected && b.isConnected) return 1;
return a.name.localeCompare(b.name);
}).map((participant, index) => jsx(ParticipantCard, {
participant: participant,
index: index
}, participant.id))
})
}), !compactMode && jsxs("div", {
className: 'pt-3 glass-border-t glass-border-white/10 glass-flex glass-justify-between glass-items-center glass-text-xs text-primary/50',
children: [jsxs("span", {
children: ["Waveform: ", waveformStyle]
}), jsxs("span", {
children: ["Sensitivity: ", sensitivity]
})]
})]
});
});
export { GlassVoiceWaveform };
//# sourceMappingURL=GlassVoiceWaveform.js.map