Add framer-motion and motion dependencies; implement animations in ModalContainer and PopupContainer components; enhance PopupWrapper for touch support; update SoundCheckModal and VoiceCheckModal for improved audio testing; refactor Select and ActionsPopover components for animation support.

This commit is contained in:
2025-10-13 15:52:05 +05:00
parent 0e3ad8e065
commit 90e9786ec9
11 changed files with 328 additions and 241 deletions
+34 -30
View File
@@ -1,6 +1,7 @@
import { useState, useRef, useEffect } from "react";
import Button from "./Button";
import MoreIcon from "../icons/MoreIcon";
import { AnimatePresence, motion } from "motion/react";
interface ActionsPopoverProps {
options: {
@@ -19,7 +20,7 @@ export default function ActionsPopover({ options }: ActionsPopoverProps) {
const [menuHeight, setMenuHeight] = useState(0);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
const handleClickOutside = (event: MouseEvent | TouchEvent) => {
if (
popoverRef.current &&
!popoverRef.current.contains(event.target as Node)
@@ -29,8 +30,10 @@ export default function ActionsPopover({ options }: ActionsPopoverProps) {
};
document.addEventListener("mousedown", handleClickOutside);
document.addEventListener("touchstart", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
document.removeEventListener("touchstart", handleClickOutside);
};
}, []);
@@ -39,11 +42,7 @@ export default function ActionsPopover({ options }: ActionsPopoverProps) {
const buttonRect = buttonRef.current.getBoundingClientRect();
const spaceBelow = window.innerHeight - buttonRect.bottom;
const spaceAbove = buttonRect.top;
if (spaceBelow < menuHeight && spaceAbove > menuHeight) {
setOpenUpwards(true);
} else {
setOpenUpwards(false);
}
setOpenUpwards(spaceBelow < menuHeight && spaceAbove > menuHeight);
}
}, [isOpened, menuHeight, options.length]);
@@ -54,34 +53,39 @@ export default function ActionsPopover({ options }: ActionsPopoverProps) {
className="flex items-center justify-center gap-[0.139vw] size-[1.667vw] rounded-[0.556vw] text-[#CCCCCC] hover:text-[#7D7D7D] hover:bg-[#F3F3F3] active:text-[#141414] "
onClick={() => setIsOpened(!isOpened)}
>
<div className="size-[1.111vw] rounded-[0.556vw]">
<div className="2xl:size-[1.111vw] size-4 2xl:rounded-[0.556vw] rounded-2xl">
<MoreIcon />
</div>
</button>
{isOpened && (
<div
ref={(el) => {
setMenuHeight(el?.offsetHeight || 0);
}}
className={`absolute z-10 right-0 w-[13.333vw] bg-white rounded-[1.111vw] shadow-[0_4px_40px_0_rgba(15,16,17,0.1)] overflow-hidden ${
openUpwards ? "bottom-[100%]" : "top-[100%]"
}`}
>
{options.map((option) => (
<Button
variant="tertiary"
className="p-[0.833vw] button-s w-full flex items-center !rounded-none !justify-start gap-[0.556vw]"
key={option.label}
onClick={option.onClick}
disabled={option.disabled}
>
<div className="size-[1.111vw] ">{option.icon}</div>
{option.label}
</Button>
))}
</div>
)}
<AnimatePresence>
{isOpened && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
ref={(el) => {
setMenuHeight(el?.offsetHeight || 0);
}}
className={`absolute z-10 right-0 w-[13.333vw] bg-white rounded-[1.111vw] shadow-[0_4px_40px_0_rgba(15,16,17,0.1)] overflow-hidden ${
openUpwards ? "bottom-[100%]" : "top-[100%]"
}`}
>
{options.map((option) => (
<Button
variant="tertiary"
className="p-[0.833vw] button-s w-full flex items-center !rounded-none !justify-start gap-[0.556vw]"
key={option.label}
onClick={option.onClick}
disabled={option.disabled}
>
<div className="size-[1.111vw] ">{option.icon}</div>
{option.label}
</Button>
))}
</motion.div>
)}
</AnimatePresence>
</div>
);
}
+17 -8
View File
@@ -20,20 +20,25 @@ function RangeInput({
useEffect(() => {
addEventListener("mouseup", () => setMouseDown(false));
addEventListener("mousemove", handleMouseMove);
addEventListener("touchend", () => setMouseDown(false));
addEventListener("mousemove", handleMove);
addEventListener("touchmove", handleMove);
return () => {
removeEventListener("mouseup", () => setMouseDown(false));
removeEventListener("mousemove", handleMouseMove);
removeEventListener("touchend", () => setMouseDown(false));
removeEventListener("mousemove", handleMove);
removeEventListener("touchmove", handleMove);
};
}, [handleMouseMove]);
}, [handleMove]);
function handleMouseMove(e: MouseEvent) {
function handleMove(e: MouseEvent | TouchEvent) {
if (mouseDown && ref.current) {
onChange(
Math.min(
Math.max(
min,
((e.clientX - ref.current.getBoundingClientRect().left) /
((("clientX" in e ? e.clientX : e.touches[0].clientX) -
ref.current.getBoundingClientRect().left) /
ref.current.clientWidth) *
(max - min) +
min
@@ -44,14 +49,17 @@ function RangeInput({
}
}
function handleMouseDown(e: React.MouseEvent<HTMLDivElement>) {
function handleMouseDownOrTouchStart(
e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>
) {
e.preventDefault();
if (ref.current) {
onChange(
Math.min(
Math.max(
min,
((e.clientX - ref.current.getBoundingClientRect().left) /
((("clientX" in e ? e.clientX : e.touches[0].clientX) -
ref.current.getBoundingClientRect().left) /
ref.current.clientWidth) *
(max - min) +
min
@@ -67,7 +75,8 @@ function RangeInput({
<div
ref={ref}
className="2xl:w-[21.111vw] w-[304px] 2xl:h-[0.139vw] h-[2px] relative bg-[#F0F0F0] 2xl:rounded-[0.556vw] rounded-lg cursor-grab active:cursor-grabbing"
onMouseDown={handleMouseDown}
onMouseDown={handleMouseDownOrTouchStart}
onTouchStart={handleMouseDownOrTouchStart}
>
<div
className="bg-[#7B60F3] 2xl:rounded-[0.556vw] h-full absolute left-0 top-0 rounded-lg"
+32 -26
View File
@@ -3,6 +3,7 @@ import clsx from "clsx";
import { useEffect, useRef, useState } from "react";
import ChevronDownIcon from "../icons/ChevronDownIcon";
import CheckIcon from "../icons/CheckIcon";
import { AnimatePresence, motion } from "motion/react";
interface SelectProps {
options: string[];
@@ -68,35 +69,40 @@ function Select({
</button>
{/* Dropdown Menu */}
{isOpen && (
<div
ref={dropDownRef}
className="absolute left-0 2xl:top-[calc(100%+0.556vw)] top-[calc(100%+8px)] z-10 w-full bg-white 2xl:rounded-[1.111vw] rounded-2xl shadow-[0px_4px_40px_0px_rgba(0,0,0,0.05),0px_2px_2px_0px_rgba(0,0,0,0.05)] 2xl:p-[0.833vw] p-3 2xl:space-y-[0.278vw] space-y-1 overflow-auto"
>
{options.map((option) => (
<button
key={option}
className={clsx(
"w-full flex items-center 2xl:gap-[0.278vw] gap-1 2xl:p-[0.833vw] p-3 2xl:rounded-[0.556vw] rounded-lg text-left transition-colors hover:bg-[#F3F3F3]"
)}
onClick={() => {
setSelectedOption(option);
setIsOpen(false);
}}
>
<div
<AnimatePresence>
{isOpen && (
<motion.div
ref={dropDownRef}
className="absolute left-0 2xl:top-[calc(100%+0.556vw)] top-[calc(100%+8px)] z-10 w-full bg-white 2xl:rounded-[1.111vw] rounded-2xl shadow-[0px_4px_40px_0px_rgba(0,0,0,0.05),0px_2px_2px_0px_rgba(0,0,0,0.05)] 2xl:p-[0.833vw] p-3 2xl:space-y-[0.278vw] space-y-1 overflow-auto"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
{options.map((option) => (
<button
key={option}
className={clsx(
"2xl:size-[1.111vw] size-4 shrink-0 text-[#7B60F3]",
option !== selectedOption && "opacity-0"
"w-full flex items-center 2xl:gap-[0.278vw] gap-1 2xl:p-[0.833vw] p-3 2xl:rounded-[0.556vw] rounded-lg text-left transition-colors hover:bg-[#F3F3F3]"
)}
onClick={() => {
setSelectedOption(option);
setIsOpen(false);
}}
>
<CheckIcon />
</div>
<span className="text-s">{option}</span>
</button>
))}
</div>
)}
<div
className={clsx(
"2xl:size-[1.111vw] size-4 shrink-0 text-[#7B60F3]",
option !== selectedOption && "opacity-0"
)}
>
<CheckIcon />
</div>
<span className="text-s">{option}</span>
</button>
))}
</motion.div>
)}
</AnimatePresence>
</div>
);
}