Update API URL to localhost in .env; add equalizer_bars.svg for audio visualization; refactor PopupWrapper and SessionUsersPanel for improved responsiveness; enhance SoundCheckModal and VoiceCheckModal with audio playback and visualization features; update SharePopup to handle sharing functionality; improve UI components for better interaction and consistency.

This commit is contained in:
2025-10-17 18:01:44 +05:00
parent b8bdbc94f9
commit ff5d8386ed
14 changed files with 335 additions and 182 deletions
+2 -2
View File
@@ -44,11 +44,11 @@ function PopupWrapper({
setPosition({
x: Math.min(
Math.max(0, position.x + x - mouseDownPosition.x),
window.innerWidth - wrapperRef.current.clientWidth
innerWidth - wrapperRef.current.clientWidth
),
y: Math.min(
Math.max(0, position.y + y - mouseDownPosition.y),
window.innerHeight - wrapperRef.current.clientHeight
innerHeight - wrapperRef.current.clientHeight
),
});
setMouseDownPosition({ x, y });
+1 -1
View File
@@ -115,7 +115,7 @@ export default function SessionUsersPanel() {
transform: `translate(${isLeft ? "0" : "-100%"}, ${
isTop ? "0" : "-100%"
})`,
transition: "all 0.3s ease-out",
transition: "all 0.3s ease-in-out",
};
};
@@ -1,4 +1,4 @@
import { useRef, useEffect } from "react";
import { useRef, useEffect, useState } from "react";
import ModalWrapper from "../ModalWrapper";
import Button from "../ui/Button";
import RestartIcon from "../icons/RestartIcon";
@@ -21,10 +21,46 @@ function SoundCheckModal({
}: SoundCheckModalProps) {
const { setModal } = useModalStore();
const audioContextRef = useRef<AudioContext | null>(null);
const animationFrameRef = useRef<number | null>(null);
const [isPlaying, setIsPlaying] = useState(false);
const [playProgress, setPlayProgress] = useState(0); // Прогресс воспроизведения 0-1
const playStartTimeRef = useRef<number>(0);
// Высоты баров эквалайзера (16 баров как в дизайне)
const barCount = 16;
const barHeights = [
3, 12, 16, 20, 44, 28, 34, 60, 8, 40, 18, 36, 24, 13, 10, 6,
];
// Обновление прогресса воспроизведения
useEffect(() => {
if (!isPlaying) return;
const updateProgress = () => {
const elapsed = Date.now() - playStartTimeRef.current;
const progress = Math.min(elapsed / 3000, 1); // 3 секунды
setPlayProgress(progress);
if (progress < 1) {
animationFrameRef.current = requestAnimationFrame(updateProgress);
}
};
updateProgress();
return () => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
};
}, [isPlaying]);
// Очистка AudioContext при размонтировании
useEffect(() => {
return () => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
if (audioContextRef.current) {
audioContextRef.current.close();
audioContextRef.current = null;
@@ -42,6 +78,7 @@ function SoundCheckModal({
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
// Подключаем напрямую
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
@@ -54,12 +91,23 @@ function SoundCheckModal({
gainNode.gain.setValueAtTime(baseVolume * 0.3, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(
0.01,
audioContext.currentTime + 1
audioContext.currentTime + 3
);
// Играем звук 1 секунду
// Запускаем анимацию прогресса
playStartTimeRef.current = Date.now();
setIsPlaying(true);
setPlayProgress(0);
// Играем звук 3 секунды
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 1);
oscillator.stop(audioContext.currentTime + 3);
// Останавливаем анимацию после окончания звука
setTimeout(() => {
setIsPlaying(false);
setPlayProgress(0);
}, 3000);
};
return (
@@ -76,20 +124,28 @@ function SoundCheckModal({
<div className="flex flex-col items-center 2xl:gap-[0.833vw] gap-3">
<div className="flex items-center justify-center 2xl:gap-[0.208vw] gap-[3px] 2xl:h-[4.167vw] h-[60px]">
{[3, 12, 16, 20, 44, 28, 34, 60, 8, 40, 18, 36, 24, 13, 10, 6].map(
(height, index) => (
{barHeights.map((height, index) => {
// Определяем, заполнен ли бар синим (прогресс слева направо)
const barProgress = (index + 1) / barCount;
const isActivated = playProgress >= barProgress || !isPlaying;
return (
<div
key={index}
className="2xl:w-[0.208vw] w-[3px] bg-[#D6D6D6] 2xl:rounded-[0.556vw] rounded-lg"
style={{ height: `${(height / 1440) * innerWidth}px` }}
className="2xl:w-[0.208vw] w-[3px] 2xl:rounded-[0.556vw] rounded-lg transition-all duration-150 ease-out"
style={{
height: `${height}px`,
backgroundColor: isActivated ? "#7B60F3" : "#D6D6D6",
}}
/>
)
)}
);
})}
</div>
<button
onClick={playTestSound}
className="2xl:size-[2.778vw] size-10 rounded-full bg-[#7B60F3] hover:bg-[#9184F6] active:bg-[#B3AAF9] flex items-center justify-center text-white transition-colors"
disabled={isPlaying}
className="2xl:size-[2.778vw] size-10 rounded-full bg-[#7B60F3] hover:bg-[#9184F6] active:bg-[#B3AAF9] disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center text-white transition-colors"
>
<div className="2xl:size-[1.111vw] size-4">
<RestartIcon />
+123 -75
View File
@@ -24,13 +24,11 @@ function VoiceCheckModal({
}: VoiceCheckModalProps) {
const { setModal } = useModalStore();
const [audioLevel, setAudioLevel] = useState(0);
const [status, setStatus] = useState<"default" | "success" | "error">(
"default"
);
const [isTestRunning, setIsTestRunning] = useState(false);
const [soundDetected, setSoundDetected] = useState(false);
const [maxAudioLevel, setMaxAudioLevel] = useState(0);
const audioContextRef = useRef<AudioContext | null>(null);
const analyserRef = useRef<AnalyserNode | null>(null);
@@ -41,30 +39,6 @@ function VoiceCheckModal({
const testTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const statusRef = useRef<"default" | "success" | "error">("default");
function detectAudioLevel() {
if (!analyserRef.current) return;
const dataArray = new Uint8Array(analyserRef.current.frequencyBinCount);
analyserRef.current.getByteFrequencyData(dataArray);
const average = dataArray.reduce((a, b) => a + b) / dataArray.length;
setAudioLevel(average);
// Обновляем максимальный уровень звука
setMaxAudioLevel((prev) => Math.max(prev, average));
// Если звук обнаружен и статус ещё не success, устанавливаем success
if (average > 5) {
if (statusRef.current !== "success") {
statusRef.current = "success";
setStatus("success");
}
setSoundDetected(true);
}
animationFrameRef.current = requestAnimationFrame(detectAudioLevel);
}
async function startMicrophoneTest() {
// Проверяем доступность API
if (!isMediaDevicesSupported()) {
@@ -94,7 +68,10 @@ function VoiceCheckModal({
audioContextRef.current = audioContext;
const analyser = audioContext.createAnalyser();
analyser.fftSize = 256;
analyser.fftSize = 2048; // Увеличиваем для более точного анализа
analyser.smoothingTimeConstant = 0.85; // Увеличиваем сглаживание для плавных волн
analyser.minDecibels = -90; // Минимальный уровень для лучшей чувствительности
analyser.maxDecibels = -10; // Максимальный уровень
analyserRef.current = analyser;
const gainNode = audioContext.createGain();
@@ -113,21 +90,10 @@ function VoiceCheckModal({
statusRef.current = "default";
setStatus("default");
setSoundDetected(false);
setAudioLevel(0);
setMaxAudioLevel(0);
setIsTestRunning(true);
// Запускаем проверку звука
detectAudioLevel();
// Останавливаем проверку через 3 секунды и устанавливаем результат
testTimeoutRef.current = setTimeout(() => {
// Останавливаем анимацию
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
animationFrameRef.current = null;
}
// Устанавливаем финальный статус
if (statusRef.current === "default") {
statusRef.current = "error";
@@ -203,7 +169,6 @@ function VoiceCheckModal({
cleanupAudioResources();
// НЕ сбрасываем audioLevel и maxAudioLevel - они сохраняются до нового теста
setSoundDetected(false);
setIsTestRunning(false);
}
@@ -227,20 +192,94 @@ function VoiceCheckModal({
}
}, [microphoneVolume]);
// Генерируем высоты для баров на основе уровня звука
function generateBarHeights() {
const baseHeights = [
3, 12, 16, 20, 44, 28, 34, 60, 8, 40, 18, 36, 24, 13, 10, 6,
];
// Используем maxAudioLevel для сохранения максимальной высоты
const levelToUse = isTestRunning ? audioLevel : maxAudioLevel;
const multiplier = Math.min(levelToUse / 5);
return baseHeights.map((h) => Math.max(3, Math.min(60, h * multiplier)));
}
// Состояние для баров эквалайзера (40 баров для плавных волн)
const barCount = 40;
const [barHeights, setBarHeights] = useState<number[]>(
new Array(barCount).fill(3)
);
const barHeights = generateBarHeights();
// Обновляем бары эквалайзера в режиме реального времени
useEffect(() => {
if (!isTestRunning || !analyserRef.current) return;
console.log(barHeights);
const updateBars = () => {
if (!analyserRef.current) return;
const bufferLength = analyserRef.current.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
analyserRef.current.getByteFrequencyData(dataArray);
// Сначала создаём меньше опорных точек для интерполяции
const keyPoints = Math.floor(barCount / 2.5); // ~16 опорных точек
const keyHeights: number[] = [];
for (let i = 0; i < keyPoints; i++) {
// Логарифмическое распределение частот
const startFreq = Math.pow(2, (i / keyPoints) * 10);
const endFreq = Math.pow(2, ((i + 1) / keyPoints) * 10);
const startIndex = Math.floor((startFreq / 1024) * bufferLength);
const endIndex = Math.floor((endFreq / 1024) * bufferLength);
let sum = 0;
let count = 0;
for (let j = startIndex; j < endIndex && j < bufferLength; j++) {
sum += dataArray[j];
count++;
}
const average = count > 0 ? sum / count : 0;
// Нормализуем к диапазону 3-60 пикселей
const normalizedHeight = Math.min(
60,
Math.max(3, (average / 255) * 60 * 2.5)
);
keyHeights.push(normalizedHeight);
}
// Интерполируем между опорными точками для плавных волн
const newBarHeights: number[] = [];
for (let i = 0; i < barCount; i++) {
const position = (i / barCount) * (keyPoints - 1);
const index = Math.floor(position);
const fraction = position - index;
if (index >= keyPoints - 1) {
newBarHeights.push(keyHeights[keyPoints - 1]);
} else {
// Линейная интерполяция между двумя опорными точками
const interpolated =
keyHeights[index] * (1 - fraction) +
keyHeights[index + 1] * fraction;
newBarHeights.push(interpolated);
}
}
setBarHeights(newBarHeights);
// Определяем наличие звука по максимальному бару
const maxBar = Math.max(...newBarHeights);
if (maxBar > 10) {
// Если есть бары выше 10px, значит есть звук
if (statusRef.current !== "success") {
statusRef.current = "success";
setStatus("success");
}
setSoundDetected(true);
}
animationFrameRef.current = requestAnimationFrame(updateBars);
};
updateBars();
return () => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
};
}, [isTestRunning, barCount]);
return (
<ModalWrapper className="2xl:max-w-[21.111vw] max-w-[304px]">
@@ -255,31 +294,40 @@ function VoiceCheckModal({
/>
</div>
{/* Визуализация уровня звука */}
<div className="flex flex-col items-center 2xl:gap-[0.556vw] gap-2">
<div className="flex items-center justify-center 2xl:gap-[0.208vw] gap-[3px] 2xl:h-[4.167vw] h-[60px]">
{barHeights.map((height, index) => (
<div
key={index}
className={clsx(
`2xl:rounded-[0.556vw] rounded-lg transition-all duration-150 2xl:w-[0.208vw] w-[3px]`,
soundDetected ? "bg-[#7B60F3]" : "bg-[#D6D6D6]"
)}
style={{ height: `${(height / 1440) * innerWidth}px` }}
/>
))}
{/* Эквалайзер */}
<div className="flex flex-col 2xl:gap-[0.833vw] gap-3">
{/* Визуализация эквалайзера */}
<div className="flex flex-col items-center 2xl:gap-[0.556vw] gap-2">
<div className="flex items-center justify-center 2xl:gap-[0.208vw] gap-[3px] 2xl:h-[4.167vw] h-[60px]">
{barHeights.map((height, index) => (
<div
key={index}
className={clsx(
"2xl:w-[0.208vw] w-[3px] 2xl:rounded-[0.556vw] rounded-lg transition-all duration-150 ease-out",
soundDetected ? "bg-[#7B60F3]" : "bg-[#D6D6D6]"
)}
style={{
height: `${Math.max(3, height)}px`,
}}
/>
))}
</div>
{/* Статус */}
{!isTestRunning && status === "success" && (
<p className="caption-s text-[#29AF61] font-medium">Звук есть!</p>
)}
{!isTestRunning && status === "error" && (
<p className="caption-s text-[#FF4517] font-medium">
Звук не обнаружен
</p>
)}
{isTestRunning && (
<p className="caption-s text-[#7D7D7D] font-medium">
Проверка...
</p>
)}
</div>
{!isTestRunning && status === "success" && (
<p className="caption-s text-[#29AF61] font-medium">Звук есть!</p>
)}
{!isTestRunning && status === "error" && (
<p className="caption-s text-[#FF4517] font-medium">
Звук не обнаружен
</p>
)}
{isTestRunning && (
<p className="caption-s text-[#7D7D7D] font-medium">Проверка...</p>
)}
</div>
<div className="flex flex-col items-center 2xl:gap-[0.556vw] gap-2 text-center">
@@ -14,6 +14,7 @@ function QRCodePopup({ link }: QRCodePopupProps) {
return (
<PopupWrapper
className="w-[21.667vw]"
draggable
leftButton={
<Button
+19 -2
View File
@@ -9,11 +9,23 @@ import QRCodePopup from "./QRCodePopup";
function SharePopup({ link }: { link: string }) {
const { setPopup } = usePopupStore();
function handleShare() {
if (navigator.share) {
navigator.share({
title: "Пригласить",
text: "Присоединяйся к моей встрече",
url: link,
});
} else {
navigator.clipboard.writeText(link);
}
}
return (
<PopupWrapper
title="Пригласить"
draggable
className="max-w-[21.667vw]"
className="w-[21.667vw]"
leftButton={
<Button
variant="secondary"
@@ -30,7 +42,12 @@ function SharePopup({ link }: { link: string }) {
<p className="title-s mb-[0.833vw] font-medium">Скопировать ссылку</p>
<LinkShare link={link} />
</div>
<Button variant="primary" size="large" className="w-full">
<Button
variant="primary"
size="large"
className="w-full"
onClick={handleShare}
>
Отправить
<div className="2xl:size-[1.111vw] size-4">
<ShareFilledIcon />
+1 -1
View File
@@ -29,7 +29,7 @@ function Button({
onClick?.(e);
}}
className={clsx(
"transition-all select-none cursor-pointer disabled:!cursor-default flex outline-none gap-2 items-center justify-center font-medium disabled:bg-[#F6F6F6] disabled:!text-[#D6D6D6]",
"transition-colors select-none cursor-pointer disabled:!cursor-default flex outline-none gap-2 items-center justify-center font-medium disabled:bg-[#F6F6F6] disabled:!text-[#D6D6D6]",
isActive && "bg-[#F3F1FD] !text-[#7B60F3]",
variant === "menu" &&
"text-[#7D7D7D] hover:bg-[#F3F3F3] active:bg-[#F3F1FD] active:text-[#7B60F3]",
+4 -9
View File
@@ -4,12 +4,10 @@ interface ControlButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
size: "small" | "large";
icon: React.ReactNode;
enabled: boolean;
}
function ControlButton({
size,
enabled,
icon,
className,
onClick,
@@ -20,13 +18,10 @@ function ControlButton({
onClick={onClick}
{...props}
className={clsx(
"backdrop-blur-[10px] rounded-full transition-all cursor-pointer disabled:!cursor-default outline-none",
size === "large" ? "2xl:p-[0.833vw] p-3" : "2xl:p-[0.417vw] p-[6px]",
!enabled
? "bg-[#FF4517] hover:bg-[#FF4517]/85"
: size === "large"
? "bg-[#FFFFFF]/15 hover:bg-[#FFFFFF]/25"
: "bg-[#141414]/15 hover:bg-[#141414]/25",
"backdrop-blur-[10px] rounded-full transition-colors cursor-pointer disabled:!cursor-default outline-none disabled:bg-[#FF4517] disabled:hover:bg-[#FF4517]/85",
size === "large"
? "2xl:p-[0.833vw] p-3 enabled:bg-[#FFFFFF]/15 enabled:hover:bg-[#FFFFFF]/25"
: "2xl:p-[0.417vw] p-[6px] enabled:bg-[#141414]/15 enabled:hover:bg-[#141414]/25",
className
)}
>
+3 -1
View File
@@ -97,7 +97,9 @@ function Select({
>
<CheckIcon />
</div>
<span className="text-s">{option}</span>
<span className="text-s line-clamp-1 text-ellipsis text-nowrap">
{option}
</span>
</button>
))}
</motion.div>
+59 -72
View File
@@ -1,6 +1,4 @@
import { AnimatePresence, motion } from "motion/react";
import { useState } from "react";
import { useEffect, useRef } from "react";
import HandRaisedOffFilledIcon from "../icons/HandRaisedOffFilledIcon";
import HandRaisedFilledIcon from "../icons/HandRaisedFilledIcon";
import MicrophoneFilledIcon from "../icons/MicrophoneFilledIcon";
@@ -23,7 +21,7 @@ interface UserCameraControlsProps {
interface UserCameraProps extends UserCameraControlsProps {
isAdmin?: boolean;
name?: string;
mediaStream?: string;
mediaStream?: MediaStream | null;
isSpeaking?: boolean;
}
@@ -37,23 +35,25 @@ export default function UserCamera({
isSpeaking = false,
isAdmin = false,
name = "Гость",
mediaStream = "",
mediaStream = null,
}: UserCameraProps) {
const [isHover, setIsHover] = useState(false);
const ref = useRef<HTMLVideoElement>(null);
useEffect(() => {
if (ref.current) {
ref.current.srcObject = mediaStream;
}
}, [mediaStream]);
return (
<motion.div
onMouseEnter={() => setIsHover(true)}
onMouseLeave={() => setIsHover(false)}
animate={{
width: isHover ? "10.833vw" : "6.944vw",
border: isSpeaking
? "0.139vw solid #7B60F3"
: "0.139vw solid #FFFFFF4D",
}}
<div
className={clsx(
"aspect-square group rounded-[1.667vw] bg-yellow-500 relative flex-shrink-0 transition-all duration-300 pointer-events-auto hover:w-[10.833vw]w-[6.944vw] shadow-[0_4px_40px_0_rgba(15,16,17,0.1),0_2px_2px_0_rgba(0,0,0,0.06)]",
isAdmin && "order-3"
"aspect-square group 2xl:rounded-[1.667vw] rounded-2xl relative flex-shrink-0 transition-[width,box-shadow,background-color] duration-300 pointer-events-auto hover:w-[10.833vw] w-[6.944vw] shadow-[0_4px_40px_0_rgba(15,16,17,0.1),0_2px_2px_0_rgba(0,0,0,0.06)]",
isAdmin && "order-3",
isSpeaking
? "ring-[0.139vw] ring-[#7B60F3]"
: "ring-[0.069vw] ring-[#FFFFFF4D]",
isVideoOff ? "bg-green-500" : "bg-yellow-500"
)}
>
{isAdmin && <Admin className="absolute top-0 right-0" />}
@@ -66,7 +66,7 @@ export default function UserCamera({
</div>
<video
src={mediaStream}
ref={ref}
className="size-full object-cover"
autoPlay
muted={isMuted}
@@ -77,12 +77,11 @@ export default function UserCamera({
isMuted={isMuted}
isVideoOff={isVideoOff}
isControlDisabled={isControlDisabled}
isHover={isHover}
onMute={onMute}
onVideoOff={onVideoOff}
onCanControl={onCanControl}
/>
</motion.div>
</div>
);
}
@@ -90,63 +89,51 @@ function UserCameraControls({
isMuted,
isVideoOff,
isControlDisabled,
isHover,
onMute,
onVideoOff,
onCanControl,
}: UserCameraControlsProps & { isHover: boolean }) {
}: UserCameraControlsProps) {
return (
<div className="absolute bottom-[0.278vw] left-1/2 translate-x-[-50%]">
<AnimatePresence mode="wait">
{isHover ? (
<motion.div
key="controls"
className="flex gap-[0.278vw] mb-[0.278vw]"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
>
<ControlButton
icon={isMuted ? <MicrophoneOffIcon /> : <MicrophoneFilledIcon />}
size={"small"}
enabled={!isMuted}
onClick={onMute}
/>
<ControlButton
icon={isVideoOff ? <VideoOffFilledIcon /> : <VideoFilledIcon />}
size={"small"}
enabled={!isVideoOff}
onClick={onVideoOff}
/>
<ControlButton
icon={
isControlDisabled ? (
<HandRaisedOffFilledIcon />
) : (
<HandRaisedFilledIcon />
)
}
size={"small"}
enabled={!isControlDisabled}
onClick={onCanControl}
/>
</motion.div>
) : (
<motion.div
key="controls-muted"
className="size-[1.667vw] bg-[#14141426] backdrop-blur-[4px] rounded-full flex items-center justify-center z-10"
initial={{ opacity: 0 }}
animate={{ opacity: isMuted ? 1 : 0 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
>
<div className="size-[0.972vw] text-white">
<MicrophoneOffIcon />
</div>
</motion.div>
<div className="absolute transition-[bottom] duration-300 2xl:bottom-[0.278vw] 2xl:group-hover:bottom-[0.556vw] group-hover:bottom-2 bottom-1 left-1/2 -translate-x-1/2">
<div
className={clsx(
"2xl:size-[1.667vw] size-6 bg-[#14141426] backdrop-blur-[4px] transition-opacity duration-300 rounded-full flex items-center justify-center z-10a absolute left-1/2 -translate-x-1/2 2xl:bottom-0 [0.278vw] group-hover:opacity-0",
isMuted ? "opacity-100" : "opacity-0"
)}
</AnimatePresence>
>
<div className="size-[0.972vw] text-white">
<MicrophoneOffIcon />
</div>
</div>
<div
className="flex gap-[0.278vw] mb-[0.278vw] group-hover:opacity-100 opacity-0 transition-opacity duration-300"
onMouseDown={(e) => e.stopPropagation()}
>
<ControlButton
icon={isMuted ? <MicrophoneOffIcon /> : <MicrophoneFilledIcon />}
size={"small"}
disabled={isMuted}
onClick={onMute}
/>
<ControlButton
icon={isVideoOff ? <VideoOffFilledIcon /> : <VideoFilledIcon />}
size={"small"}
disabled={isVideoOff}
onClick={onVideoOff}
/>
<ControlButton
icon={
isControlDisabled ? (
<HandRaisedOffFilledIcon />
) : (
<HandRaisedFilledIcon />
)
}
size={"small"}
disabled={isControlDisabled}
onClick={onCanControl}
/>
</div>
</div>
);
}
@@ -23,29 +23,29 @@ export default function UserDevicesControls() {
}
return (
<div className="hidden order-4 2xl:grid grid-cols-2 gap-[0.278vw] aspect-square p-[0.556vw] flex-wrap justify-between items-center size-[6.944vw] rounded-[1.667vw] bg-[#00000040] shadow-[0_4px_40px_0_#0F10111A] backdrop-blur-[10px] pointer-events-auto">
<div className="hidden order-4 2xl:grid grid-cols-2 gap-[0.278vw] aspect-square p-[0.556vw] flex-wrap justify-between items-center size-[6.944vw] rounded-[1.667vw] bg-[#00000040] shadow-[0_4px_40px_0_#0F10111A] backdrop-blur-[10px] pointer-events-auto">
<ControlButton
onMouseDown={(e) => e.stopPropagation()}
size="large"
icon={<MicrophoneFilledIcon />}
onClick={ToggleAudioDevice}
enabled={true}
/>
<ControlButton
onMouseDown={(e) => e.stopPropagation()}
size="large"
icon={<VideoFilledIcon />}
enabled={true}
onClick={ToggleVideoDevice}
/>
<ControlButton
onMouseDown={(e) => e.stopPropagation()}
size="large"
icon={<HandRaisedFilledIcon />}
onClick={ToggleCanControl}
enabled={true}
/>
<ControlButton
onMouseDown={(e) => e.stopPropagation()}
size="large"
icon={<CogFilledIcon />}
enabled={true}
onClick={ToggleSettings}
/>
</div>