Enhance Modal and Popup components with drag-and-drop functionality for mobile responsiveness. Implement drag handling in ModalContainer and PopupContainer, and adjust layout in ModalWrapper and PopupWrapper for better user experience. Update Tooltip and ActionsPopover for improved positioning and visibility. Clean up unused code and comments in HomePage and popupStore.

This commit is contained in:
2025-10-22 14:23:15 +05:00
parent 8d05e938be
commit 941b775034
15 changed files with 242 additions and 199 deletions
+32 -6
View File
@@ -2,14 +2,17 @@
import { useEffect, useRef } from "react";
import useModalStore from "../store/modalStore";
import clsx from "clsx";
import { AnimatePresence, motion } from "motion/react";
import { AnimatePresence, motion, type PanInfo } from "motion/react";
function ModalContainer() {
const { modal, setModal, position } = useModalStore();
const divRef = useRef<HTMLDivElement>(null);
const backdropRef = useRef<HTMLDivElement>(null);
const modalRef = useRef<HTMLDivElement>(null);
const isMobile = innerWidth < 640;
function handleResize() {
if (!modalRef.current) return;
@@ -25,6 +28,16 @@ function ModalContainer() {
setModal(null);
}
const handleDragEnd = (
_event: MouseEvent | TouchEvent | PointerEvent,
info: PanInfo
) => {
// Закрываем модалку если свайпнули вниз больше чем на 100px или со скоростью > 500
if (info.offset.y > 100 || info.velocity.y > 500) {
setModal(null);
}
};
useEffect(() => {
window.addEventListener("resize", handleResize);
window.addEventListener("keydown", handleKeydown);
@@ -47,19 +60,32 @@ function ModalContainer() {
<div
ref={modalRef}
className={clsx(
"bg-black/70 max-md:top-14 flex overflow-y-auto fixed inset-0 z-10 items-center",
position === "center" ? "justify-center" : "justify-end"
"bg-black/70 max-md:top-14 flex overflow-y-auto fixed inset-0 z-10",
isMobile ? "items-end" : "items-center",
!isMobile &&
(position === "center" ? "justify-center" : "justify-end")
)}
>
<div className="max-h-full">
<div ref={divRef} className="2xl:p-[1.111vw]">
<div className={clsx("max-h-full", isMobile && "w-full")}>
<motion.div
ref={divRef}
className="2xl:p-[1.111vw]"
initial={isMobile ? { y: "100%" } : undefined}
animate={isMobile ? { y: 0 } : undefined}
exit={isMobile ? { y: "100%" } : undefined}
transition={{ bounce: 0, ease: "easeInOut" }}
drag={isMobile ? "y" : false}
dragConstraints={{ top: 0, bottom: 0 }}
dragElastic={{ top: 0, bottom: 0.2 }}
onDragEnd={isMobile ? handleDragEnd : undefined}
>
<div
ref={backdropRef}
className="absolute inset-0 cursor-pointer"
onClick={() => setModal(null)}
/>
{modal}
</div>
</motion.div>
</div>
</div>
</motion.div>
+12 -2
View File
@@ -17,12 +17,22 @@ function ModalWrapper({
return (
<div
className={clsx(
"bg-white 2xl:rounded-[2.222vw] rounded-[32px] relative",
"bg-white 2xl:rounded-[2.222vw] rounded-[32px] relative max-sm:rounded-b-none max-sm:max-h-[97.5dvh]",
className
)}
>
{/* Полоска-ручка для свайпа на мобильных */}
<div className="hidden absolute -top-3 left-1/2 justify-center pt-1 pb-1 -translate-x-1/2 max-sm:flex">
<div className="w-8 h-1 bg-[#141414] rounded-full opacity-50" />
</div>
<ModalHeader title={title} leftButton={leftButton} />
<div className={clsx("2xl:p-[1.389vw] p-5", !title && "!pt-0")}>
<div
className={clsx(
"2xl:p-[1.389vw] p-5 overflow-y-auto min-h-full bg-white",
!title && "!pt-0"
)}
>
{children}
</div>
</div>
+12 -6
View File
@@ -1,15 +1,18 @@
import { AnimatePresence, motion } from "motion/react";
import { AnimatePresence, motion, type PanInfo } from "motion/react";
import usePopupStore from "../store/popupStore";
import { useEffect } from "react";
function PopupContainer() {
const { popup } = usePopupStore();
const { popup, setPopup } = usePopupStore();
const isMobile = innerWidth < 640;
useEffect(() => {
console.log(popup);
}, [popup]);
const handleDragEnd = (
_event: MouseEvent | TouchEvent | PointerEvent,
info: PanInfo
) => {
// Закрываем попап если свайпнули вниз больше чем на 100px или со скоростью > 500
if (info.offset.y > 100 || info.velocity.y > 500) setPopup(null);
};
return (
<AnimatePresence>
@@ -20,6 +23,9 @@ function PopupContainer() {
animate={{ opacity: 1, y: isMobile ? "0%" : undefined }}
exit={{ opacity: 0, y: isMobile ? "100%" : undefined }}
transition={{ bounce: 0, ease: "easeInOut" }}
drag={isMobile ? "y" : false}
dragConstraints={{ top: 0, bottom: 0 }}
onDragEnd={handleDragEnd}
>
{popup}
</motion.div>
+1 -1
View File
@@ -18,7 +18,7 @@ function PopupWrapper({
return (
<div
className={clsx(
"2xl:rounded-[2.222vw] relative bg-white shadow-[0_4px_40px_0_rgba(15,16,17,0.1)] 2xl:w-[21.667vw] sm:rounded-[32px] max-sm:w-screen max-sm:rounded-t-[32px]",
"2xl:rounded-[2.222vw] bg-white shadow-[0_4px_40px_0_rgba(15,16,17,0.1)] 2xl:w-[21.667vw] sm:rounded-[32px] max-sm:w-screen max-sm:rounded-t-[32px]",
className
)}
>
+1 -1
View File
@@ -10,7 +10,7 @@ export default function Warning({
return (
<div
className={clsx(
"2xl:size-[1.111vw] size-[4.444vw] border-[1px] border-white rounded-full flex items-center justify-center ",
"2xl:size-[1.111vw] size-4 border-[1px] border-white rounded-full flex items-center justify-center ",
type === "caution" && "bg-[#F9A530]",
type === "critical" && "bg-[#FF4517]",
className
@@ -354,10 +354,7 @@ function SettingsModal() {
};
return (
<ModalWrapper
title="Настройки"
className="2xl:max-w-[27.778vw] max-w-[400px]"
>
<ModalWrapper title="Настройки" className="2xl:max-w-[27.778vw]">
<div className="2xl:space-y-[1.389vw] space-y-5 2xl:-mt-[1.389vw] -mt-5">
<div className="flex">
<Button
@@ -556,7 +553,7 @@ function SettingsModal() {
className="2xl:rounded-[1.111vw] rounded-2xl 2xl:w-[25vw] w-[360px] aspect-[360/202] object-cover"
/>
{!isVideoTesting && (
<div className="bg-[#F3F3F3] 2xl:w-[25vw] w-[360px] aspect-[360/202] flex justify-center items-center 2xl:rounded-[1.111vw] rounded-2xl">
<div className="bg-[#F3F3F3] 2xl:w-[25vw] w-[360px] max-sm:w-full aspect-[360/202] flex justify-center items-center 2xl:rounded-[1.111vw] rounded-2xl">
{cameras.length === 0 && !isLoadingCameras ? (
<div className="2xl:space-y-[0.556vw] space-y-2 text-center 2xl:px-[1.111vw] px-4">
<p className="title-s font-medium">Камеры не найдены</p>
+3 -3
View File
@@ -40,7 +40,7 @@ export default function ChatPopup() {
initialPosition={{ right: "5vw" }}
>
<PopupWrapper title="Чат" className="sm:overflow-hidden">
<div className="flex flex-col 2xl:h-[19.444vw] max-sm:h-[97.5dvh] 2xl:-m-[1.389vw] -m-5">
<div className="flex flex-col 2xl:h-[19.444vw] max-sm:h-[87.5dvh] 2xl:-m-[1.389vw] -m-5">
<MessageFeed messages={messages} />
<MessageInput onMessageSend={onMessageSend} />
</div>
@@ -59,7 +59,7 @@ function MessageFeed({ messages }: { messages: MessageItemProps[] }) {
return (
<div
className="flex flex-col w-full 2xl:h-[calc(100%-4.444vw)] bg-[#F0F0F0] 2xl:p-[1.111vw] p-4 pb-0 overflow-y-auto [-webkit-scrollbar]:hidden"
className="flex flex-col w-full 2xl:h-[calc(100%-4.444vw)] h-full bg-[#F0F0F0] 2xl:p-[1.111vw] p-4 pb-0 overflow-y-auto [-webkit-scrollbar]:hidden"
style={{ scrollbarWidth: "none", msOverflowStyle: "none" }}
>
{messages.length === 0 ? (
@@ -165,7 +165,7 @@ function MessageInput({
return (
<div
onClick={() => textareaRef.current?.focus()}
className="flex w-full 2xl:min-h-[4.444vw] min-h-16 2xl:p-[1.111vw] p-4 items-end justify-between absolute bottom-0 left-0 bg-white"
className="flex w-full 2xl:min-h-[4.444vw] min-h-16 2xl:p-[1.111vw] p-4 items-end justify-between bg-white"
>
<textarea
ref={textareaRef}
@@ -23,17 +23,15 @@ export default function ParticipantsPopup() {
initialPosition={{ right: "5vw" }}
>
<PopupWrapper title="Участники">
<div className="flex flex-col 2xl:gap-[1.667vw] gap-6 relative 2xl:-mt-[1.389vw] -mt-5">
<ul className="flex flex-col gap-0 2xl:gap-[1.111vw] 2xl:max-h-[calc(11.944vw+1.389vw)] overflow-y-auto 2xl:pt-[1.389vw] pt-5">
<div className="flex flex-col 2xl:gap-[1.667vw] gap-6 2xl:-mt-[1.389vw] -mt-5">
<div className="flex flex-col gap-4 2xl:gap-[1.111vw] 2xl:max-h-[calc(11.944vw+1.389vw)] max-h-[73.75dvh] overflow-y-auto 2xl:pt-[1.389vw] pt-5">
{participants.map((participant, index) => (
<Fragment key={index}>
<ParticipantItem id={participant.toString()} />
{index !== participants.length - 1 && (
<div className="w-full h-[1px] bg-[#F6F6F6]" />
)}
<hr className="w-full 2xl:h-[0.69vw] h-px border-[#F6F6F6] last:hidden" />
</Fragment>
))}
</ul>
</div>
<Button
variant="primary"
onClick={() => {
@@ -62,31 +60,27 @@ export default function ParticipantsPopup() {
function ParticipantItem({ id }: { id: string }) {
const isMuted = true;
const isNotControlling = true;
const isMobile = window.innerWidth <= 360;
const parentRef = useRef<HTMLDivElement>(null);
return (
<div
ref={parentRef}
onTouchEnd={(ev) => ev.stopPropagation()}
className="flex items-center justify-between w-full relative py-[2.222vw] 2xl:p-0"
>
<div className="flex items-center 2xl:gap-[0.833vw] gap-[3.333vw]">
<div ref={parentRef} className="flex items-center justify-between w-full">
<div className="flex items-center 2xl:gap-[0.833vw] gap-3">
<Avatar size="medium" status="caution" />
<div className="flex flex-col gap-[0.278vw]">
<div className="flex flex-col 2xl:gap-[0.278vw] gap-1">
<span className="button-m">Иван Иванович {id}</span>
<span className="caption-s text-[#CCCCCC]">Роль</span>
</div>
</div>
<div className="flex 2xl:gap-[0.556vw] gap-[2.222vw] items-center">
<div className="flex 2xl:gap-[0.556vw] gap-2 items-center">
{isNotControlling && (
<div className="2xl:size-[1.111vw] size-[4.444vw] text-[#FF4517]">
<div className="2xl:size-[1.111vw] size-4 text-[#FF4517]">
<HandRaisedOffFilledIcon />
</div>
)}
{isMuted && (
<div className="2xl:size-[1.111vw] size-[4.444vw] text-[#FF4517]">
<div className="2xl:size-[1.111vw] size-4 text-[#FF4517]">
<MicrophoneOffFilledIcon />
</div>
)}
@@ -115,8 +109,6 @@ function ParticipantItem({ id }: { id: string }) {
onClick: () => {},
},
]}
parentRef={isMobile ? parentRef : undefined}
className={isMobile ? "left-0" : undefined}
/>
</div>
</div>
+20 -59
View File
@@ -1,8 +1,9 @@
import { useState, useRef, useEffect } from "react";
import { useState, useRef } from "react";
import Button from "./Button";
import MoreIcon from "../icons/MoreIcon";
import PopoverWrapper from "./PopoverWrapper";
import clsx from "clsx";
import { useClickAway } from "@uidotdev/usehooks";
interface ActionsPopoverProps {
options: {
@@ -12,77 +13,39 @@ interface ActionsPopoverProps {
disabled?: boolean;
}[];
isOpened?: boolean;
parentRef?: React.RefObject<HTMLDivElement | null>;
className?: string;
}
/**
* @param parentRef - Если передан, то кнопка активации опций не будет отображаться, а сам Popover будет отображаться по клику на parentRef.
*/
export default function ActionsPopover({
options,
isOpened = false,
parentRef,
className,
}: ActionsPopoverProps) {
const [opened, setOpened] = useState(isOpened);
const popoverRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
let isHandling = false;
const handleClickOutside = (event: MouseEvent | TouchEvent) => {
// Предотвращаем двойное срабатывание mousedown и touchstart
if (isHandling) return;
isHandling = true;
setTimeout(() => {
isHandling = false;
}, 200);
const tgt = event.target as Node;
const clickedInsideParentElement = parentRef?.current?.contains(tgt);
const clickedInsidePopover = popoverRef.current?.contains(tgt);
if (clickedInsideParentElement && !clickedInsidePopover) {
//
} else {
// Если нет parentRef - закрытие только по клику вне popover и вне кнопки
const clickedInsideButton = buttonRef.current?.contains(tgt);
if (!clickedInsidePopover && !clickedInsideButton) {
setOpened(false);
}
}
};
document.addEventListener("mousedown", handleClickOutside);
document.addEventListener("touchstart", handleClickOutside);
parentRef?.current?.addEventListener("touchend", () => setOpened(!opened));
return () => {
document.removeEventListener("mousedown", handleClickOutside);
document.removeEventListener("touchstart", handleClickOutside);
parentRef?.current?.addEventListener("touchend", () =>
setOpened(!opened)
);
};
}, [parentRef]);
const ref = useClickAway<HTMLDivElement>(() => setOpened(false));
return (
<div className={!parentRef ? "relative" : ""} ref={popoverRef}>
{!parentRef && (
<button
ref={buttonRef}
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={() => setOpened(!opened)}
>
<div className="2xl:size-[1.111vw] size-4 2xl:rounded-[0.556vw] rounded-2xl">
<MoreIcon />
</div>
</button>
)}
<div ref={ref} className="max-sm:max-h-6">
<button
ref={buttonRef}
className={clsx(
"2xl:rounded-[0.556vw] rounded-lg 2xl:p-[0.278vw] p-1 text-[#CCCCCC] 2xl:hover:text-[#7D7D7D] 2xl:hover:bg-[#F3F3F3] active:text-[#141414]",
opened && "max-sm:bg-[#F3F3F3]"
)}
onClick={() => setOpened(!opened)}
>
<div className="2xl:size-[1.111vw] size-4">
<MoreIcon />
</div>
</button>
<PopoverWrapper
isOpened={opened}
parentElRef={parentRef ? parentRef : buttonRef}
parentElRef={buttonRef}
position="vertical"
className={clsx("2xl:w-[17.222vw] w-[53.333vw]", className)}
>
{options.map((option) => (
@@ -93,9 +56,7 @@ export default function ActionsPopover({
onClick={option.onClick}
disabled={option.disabled}
>
<div className="2xl:size-[1.111vw] size-[4.444vw]">
{option.icon}
</div>
<div className="2xl:size-[1.111vw] size-4">{option.icon}</div>
{option.label}
</Button>
))}
+3 -3
View File
@@ -15,9 +15,9 @@ export default function Avatar({ size, status, src, name }: AvatarProps) {
<div
className={clsx(
"rounded-full text-white relative",
size === "small" && "2xl:size-[2.222vw] size-[8.889vw] button-s",
size === "medium" && "2xl:size-[2.5vw] size-[10vw] button-m",
size === "large" && "2xl:size-[3.333vw] size-[13.333vw] title-s"
size === "small" && "2xl:size-[2.222vw] size-8 button-s",
size === "medium" && "2xl:size-[2.5vw] size-9 button-m",
size === "large" && "2xl:size-[3.333vw] size-12 title-s"
)}
>
{GetAvatarImage(src, name)}
+25 -30
View File
@@ -1,7 +1,7 @@
import FloatingActionButton from "./FloatingActionButton";
import PopoverWrapper from "./PopoverWrapper";
import MoreIcon from "../icons/MoreIcon";
import { useEffect, useRef, useState } from "react";
import { useRef, useState } from "react";
import Button from "./Button";
import ChatFilledIcon from "../icons/ChatFilledIcon";
import UsersFilledIcon from "../icons/UsersFilledIcon";
@@ -14,44 +14,40 @@ import ParticipantsPopup from "../popups/ParticipantsPopup";
import SharePopup from "../popups/SharePopup";
import SettingsModal from "../modals/SettingsModal";
import clsx from "clsx";
import { useClickAway } from "@uidotdev/usehooks";
function ControlsPopover() {
const [isOpened, setIsOpened] = useState(false);
const buttonRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent | TouchEvent) => {
if (
buttonRef.current &&
!buttonRef.current.contains(event.target as Node)
) {
setIsOpened(false);
}
};
const ref = useClickAway<HTMLDivElement>(() => setIsOpened(false));
document.addEventListener("mousedown", handleClickOutside);
document.addEventListener("touchstart", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
document.removeEventListener("touchstart", handleClickOutside);
};
}, []);
const { popup, setPopup } = usePopupStore();
const { setPopup } = usePopupStore();
const { setModal } = useModalStore();
function handleClickOpenChatPopup() {
console.log("handleClickOpenChatPopup");
setIsOpened(false);
setPopup(<ChatPopup />);
}
useEffect(() => {
console.log(popup);
}, [popup]);
function handleClickOpenParticipantsPopup() {
setIsOpened(false);
setPopup(<ParticipantsPopup />);
}
function handleClickOpenSharePopup() {
setIsOpened(false);
setPopup(<SharePopup link="https://estate.stream/ahdy12jdco1" />);
}
function handleClickOpenSettingsModal() {
setIsOpened(false);
setModal(<SettingsModal />);
}
return (
<div className="relative order-3 2xl:hidden">
<div className="order-3 2xl:hidden" ref={ref}>
<FloatingActionButton
ref={buttonRef}
className={clsx(isOpened && "!bg-[#7B60F3]")}
@@ -64,7 +60,8 @@ function ControlsPopover() {
<PopoverWrapper
isOpened={isOpened}
parentElRef={buttonRef}
className="w-[248px] !bottom-[72px] min-h-full !fixed left-1/2 -translate-x-1/2"
position="center"
className="w-[248px]"
>
<Button
variant="tertiary"
@@ -79,7 +76,7 @@ function ControlsPopover() {
<Button
variant="tertiary"
className="w-full !justify-start"
onClick={() => setPopup(<ParticipantsPopup />)}
onClick={handleClickOpenParticipantsPopup}
>
<div className="size-4">
<UsersFilledIcon />
@@ -89,9 +86,7 @@ function ControlsPopover() {
<Button
variant="tertiary"
className="w-full !justify-start"
onClick={() =>
setPopup(<SharePopup link="https://estate.stream/ahdy12jdco1" />)
}
onClick={handleClickOpenSharePopup}
>
<div className="size-4">
<ShareFilledIcon />
@@ -101,7 +96,7 @@ function ControlsPopover() {
<Button
variant="tertiary"
className="w-full !justify-start"
onClick={() => setModal(<SettingsModal />)}
onClick={handleClickOpenSettingsModal}
>
<div className="size-4">
<CogFilledIcon />
+45 -4
View File
@@ -7,6 +7,7 @@ interface PopoverProps {
parentElRef: React.RefObject<HTMLButtonElement | HTMLDivElement | null>;
children: React.ReactNode;
className?: string;
position?: "side" | "center" | "vertical"; // side - сбоку от элемента, center - по центру экрана, vertical - сверху/снизу
}
function PopoverWrapper({
@@ -14,18 +15,56 @@ function PopoverWrapper({
parentElRef,
children,
className,
position = "side",
}: PopoverProps) {
const [openUpwards, setOpenUpwards] = useState(false);
const [menuHeight, setMenuHeight] = useState(0);
useEffect(() => {
if (isOpened && parentElRef.current) {
if (
isOpened &&
parentElRef.current &&
(position === "side" || position === "vertical")
) {
const buttonRect = parentElRef.current.getBoundingClientRect();
const spaceBelow = window.innerHeight - buttonRect.bottom;
const spaceAbove = buttonRect.top;
setOpenUpwards(spaceBelow < menuHeight && spaceAbove > menuHeight);
}
}, [parentElRef, isOpened, menuHeight]);
}, [parentElRef, isOpened, menuHeight, position]);
const getPositionStyles = () => {
if (!parentElRef.current) return {};
const buttonRect = parentElRef.current.getBoundingClientRect();
if (position === "center") {
// Позиционирование по центру экрана
return {
left: "50%",
bottom: buttonRect.height + 16, // 16px отступ от кнопки
transform: "translateX(-50%)",
};
}
if (position === "vertical") {
// Позиционирование сверху или снизу от элемента, выравнивание по правому краю
return {
left: buttonRect.right,
top: openUpwards
? buttonRect.top - menuHeight - 4
: buttonRect.bottom + 4,
};
}
// Позиционирование сбоку (существующая логика)
return {
left: buttonRect.left + buttonRect.width,
top: openUpwards
? buttonRect.top - menuHeight - 4
: buttonRect.bottom + 4,
};
};
return (
<AnimatePresence>
@@ -37,9 +76,11 @@ function PopoverWrapper({
ref={(el) => {
if (el) setMenuHeight(el.offsetHeight);
}}
style={getPositionStyles()}
className={clsx(
"absolute z-10 right-0 shadow-[0_4px_40px_0_rgba(15,16,17,0.1)] overflow-hidden bg-white 2xl:rounded-[1.111vw] rounded-2xl",
openUpwards ? "bottom-full" : "top-full",
"fixed z-10 shadow-[0_4px_40px_0_rgba(15,16,17,0.1)] overflow-hidden bg-white 2xl:rounded-[1.111vw] rounded-2xl",
position === "side" && "-translate-x-full",
position === "vertical" && "-translate-x-full",
className
)}
>
+73 -27
View File
@@ -30,7 +30,7 @@ export default function Tooltip({
useEffect(() => {
if (!tooltipWrapperRef.current) return;
const current = tooltipWrapperRef.current;
current.addEventListener("mouseenter", () => setIsVisible(true));
current.addEventListener("mouseleave", () => setIsVisible(false));
return () => {
@@ -41,18 +41,36 @@ export default function Tooltip({
useEffect(() => {
if (!isVisible || !tooltipRef.current || !tooltipWrapperRef.current) return;
const pos = calculatePosition(position, tooltipRef, tooltipWrapperRef);
setTooltipPosition(pos);
const updatePosition = () => {
const pos = calculatePosition(position, tooltipRef, tooltipWrapperRef);
setTooltipPosition(pos);
};
const handleScroll = () => {
setIsVisible(false);
};
updatePosition();
// Скрываем tooltip при скролле, обновляем позицию при ресайзе
window.addEventListener("scroll", handleScroll, true);
window.addEventListener("resize", updatePosition);
return () => {
window.removeEventListener("scroll", handleScroll, true);
window.removeEventListener("resize", updatePosition);
};
}, [isVisible, position]);
return (
<div ref={tooltipWrapperRef} className="relative hover:cursor-pointer">
<div ref={tooltipWrapperRef} className="cursor-pointer">
{children}
{isVisible && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: showDelay / 1000, duration: 0 }}
transition={{ delay: showDelay / 1000, duration: 0.3 }}
>
<TooltipContent
label={label}
@@ -78,7 +96,7 @@ function TooltipContent({
<div
ref={ref}
style={{ ...position, transform: position.transform || "none" }}
className="z-[9999] absolute px-[0.694vw] py-[0.417vw] bg-[#00000073] caption-s text-white rounded-[0.556vw] whitespace-nowrap backdrop-blur-[4px] shadow-[0_2px_4px_0_#00000059] cursor-default"
className="z-[9999] fixed 2xl:px-[0.694vw] 2xl:py-[0.417vw] px-[10px] py-[6px] bg-[#00000073] caption-s text-white 2xl:rounded-[0.556vw] rounded-lg whitespace-nowrap backdrop-blur-[4px] shadow-[0_2px_4px_0_#00000059] cursor-default"
>
{label}
</div>
@@ -96,49 +114,77 @@ function calculatePosition(
const tooltipRect = tooltipRef.current.getBoundingClientRect();
const GAP = 8;
function adjustHorizontal() {
let left = wrapperRect.width / 2;
function adjustHorizontal(baseLeft: number) {
let left = baseLeft;
let transform = "translateX(-50%)";
const tooltipLeft = wrapperRect.left + left - tooltipRect.width / 2;
const tooltipLeft = left - tooltipRect.width / 2;
const tooltipRight = tooltipLeft + tooltipRect.width;
// Если не влезает горизонтально, то сдвигаем
if (tooltipLeft < GAP) {
left = GAP - wrapperRect.left;
transform = "none";
left = GAP + tooltipRect.width / 2;
transform = "translateX(-50%)";
} else if (tooltipRight > window.innerWidth - GAP) {
left = window.innerWidth - GAP - wrapperRect.left - tooltipRect.width;
transform = "none";
left = window.innerWidth - GAP - tooltipRect.width / 2;
transform = "translateX(-50%)";
}
return { left, transform };
}
function adjustVertical() {
return { top: wrapperRect.height / 2, transform: "translateY(-50%)" };
function adjustVertical(baseTop: number) {
return { top: baseTop, transform: "translateY(-50%)" };
}
switch (position) {
case "top": {
return wrapperRect.bottom + tooltipRect.height + GAP < 0
? { bottom: -tooltipRect.height - GAP, ...adjustHorizontal() }
: { top: -tooltipRect.height - GAP, ...adjustHorizontal() };
const top = wrapperRect.top - tooltipRect.height - GAP;
const left = wrapperRect.left + wrapperRect.width / 2;
// Если не влезает сверху, показываем снизу
if (top < GAP) {
const bottomTop = wrapperRect.bottom + GAP;
return { top: bottomTop, ...adjustHorizontal(left) };
}
return { top, ...adjustHorizontal(left) };
}
case "bottom": {
return wrapperRect.bottom + tooltipRect.height + GAP > window.innerHeight
? { top: -tooltipRect.height - GAP, ...adjustHorizontal() }
: { bottom: -tooltipRect.height - GAP, ...adjustHorizontal() };
const top = wrapperRect.bottom + GAP;
const left = wrapperRect.left + wrapperRect.width / 2;
// Если не влезает снизу, показываем сверху
if (top + tooltipRect.height > window.innerHeight - GAP) {
const topTop = wrapperRect.top - tooltipRect.height - GAP;
return { top: topTop, ...adjustHorizontal(left) };
}
return { top, ...adjustHorizontal(left) };
}
case "left": {
return wrapperRect.left - tooltipRect.width - GAP < 0
? { right: -tooltipRect.width - GAP, ...adjustVertical() }
: { left: -tooltipRect.width - GAP, ...adjustVertical() };
const left = wrapperRect.left - tooltipRect.width - GAP;
const top = wrapperRect.top + wrapperRect.height / 2;
// Если не влезает слева, показываем справа
if (left < GAP) {
const rightLeft = wrapperRect.right + GAP;
return { left: rightLeft, ...adjustVertical(top) };
}
return { left, ...adjustVertical(top) };
}
case "right": {
return wrapperRect.right + tooltipRect.width + GAP > window.innerWidth
? { left: -tooltipRect.width - GAP, ...adjustVertical() }
: { right: -tooltipRect.width - GAP, ...adjustVertical() };
const left = wrapperRect.right + GAP;
const top = wrapperRect.top + wrapperRect.height / 2;
// Если не влезает справа, показываем слева
if (left + tooltipRect.width > window.innerWidth - GAP) {
const leftLeft = wrapperRect.left - tooltipRect.width - GAP;
return { left: leftLeft, ...adjustVertical(top) };
}
return { left, ...adjustVertical(top) };
}
case "cursor": {
return { transform: "none" };
+2 -29
View File
@@ -7,12 +7,10 @@ import usePopupStore from "../store/popupStore";
import SettingsModal from "../components/modals/SettingsModal";
import useModalStore from "../store/modalStore";
import CogFilledIcon from "../components/icons/CogFilledIcon";
// import SessionUsersPanel from "../components/SessionUsersPanel";
// import { useEffect } from "react";
// import useToastsStore from "../store/toastsStore";
import ChatPopup from "../components/popups/ChatPopup";
import ChatFilledIcon from "../components/icons/ChatFilledIcon";
import ParticipantsPopup from "../components/popups/ParticipantsPopup";
import ControlsPopover from "../components/ui/ControlsPopover";
function HomePage() {
const { data: user } = useMe();
@@ -27,31 +25,6 @@ function HomePage() {
const { setPopup } = usePopupStore();
const { setModal } = useModalStore();
// -------------------------------- Тосты --------------------------------
// const { addToast } = useToastsStore();
// useEffect(() => {
// addToast({
// id: crypto.randomUUID(),
// type: "warning",
// title: "Тестовое предупреждение",
// message: "Это тестовое предупреждение",
// onDeny: () => {},
// onAllow: () => {},
// });
// const timer = setTimeout(() => {
// addToast({
// id: crypto.randomUUID(),
// type: "notification",
// title: "Тестовое уведомление",
// message: "Это тестовое уведомление",
// onDeny: () => {},
// onAllow: () => {},
// });
// }, 1000);
// return () => clearTimeout(timer);
// }, []);
return (
<div className="py-8 min-h-screen bg-gray-50">
<div className="px-4 mx-auto max-w-4xl">
@@ -76,7 +49,6 @@ function HomePage() {
<ChatFilledIcon />
</div>
</FloatingActionButton>
{/* <SessionUsersPanel /> */}
<FloatingActionButton
variant="default"
@@ -86,6 +58,7 @@ function HomePage() {
<CogFilledIcon />
</div>
</FloatingActionButton>
<ControlsPopover />
<div className="space-y-4">
<div className="p-4 bg-blue-50 rounded-lg border border-blue-200">
-4
View File
@@ -4,15 +4,11 @@ import type { ReactNode } from "react";
interface PopupState {
popup: ReactNode | null;
setPopup: (popup: ReactNode | null) => void;
// position: { x: number; y: number };
// setPosition: (position: { x: number; y: number }) => void;
}
const usePopupStore = create<PopupState>()((set) => ({
popup: null,
setPopup: (popup) => set({ popup }),
// position: { x: 0, y: 0 },
// setPosition: (position) => set({ position }),
}));
export default usePopupStore;