This commit is contained in:
2025-10-14 13:17:09 +05:00
4 changed files with 166 additions and 3 deletions
+2
View File
@@ -23,3 +23,5 @@ dist-ssr
*.njsproj
*.sln
*.sw?
.env
@@ -30,7 +30,7 @@ function ParticipantItem({ id }: { id: string }) {
return (
<div className="flex items-center justify-between w-full h-[2.5vw]">
<div className="flex items-center gap-[0.833vw]">
<Avatar size="medium" />
<Avatar size="medium" status="caution" />
<div className="flex flex-col gap-[0.278vw]">
<span className="button-m">Иван Иванович {id}</span>
<span className="caption-s text-[#CCCCCC]">Роль</span>
+11 -2
View File
@@ -1,6 +1,7 @@
import clsx from "clsx";
import Warning from "../indicators/Warning";
import Admin from "../indicators/Admin";
import Tooltip from "./Tooltip";
interface AvatarProps {
size: "small" | "medium" | "large";
@@ -28,8 +29,16 @@ export default function Avatar({ size, status, src, name }: AvatarProps) {
size === "large" && "bottom-[2.5vw] left-[2.5vw]"
)}
>
{status === "caution" && <Warning type="caution" />}
{status === "admin" && <Admin />}
{status === "caution" && (
<Tooltip label="Проблема с соединением" position="top">
<Warning type="caution" />
</Tooltip>
)}
{status === "admin" && (
<Tooltip label="Администратор" position="top">
<Admin />
</Tooltip>
)}
</div>
</div>
);
+152
View File
@@ -0,0 +1,152 @@
import { motion } from "motion/react";
import React, { useEffect, useRef, useState } from "react";
interface TooltipProps {
label: string;
children: React.ReactNode;
position?: "top" | "bottom" | "left" | "right" | "cursor";
showDelay?: number;
}
interface TooltipPosition {
top?: number | string;
left?: number | string;
bottom?: number | string;
right?: number | string;
transform?: string;
}
export default function Tooltip({
label,
children,
position = "top",
showDelay = 500,
}: TooltipProps) {
const [isVisible, setIsVisible] = useState(false);
const [tooltipPosition, setTooltipPosition] = useState<TooltipPosition>({
top: 0,
left: 0,
transform: "none",
});
const tooltipWrapperRef = useRef<HTMLDivElement>(null);
const tooltipRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!tooltipWrapperRef.current) return;
const current = tooltipWrapperRef.current;
current.addEventListener("mouseenter", () => setIsVisible(true));
current.addEventListener("mouseleave", () => setIsVisible(false));
return () => {
current?.removeEventListener("mouseenter", () => setIsVisible(true));
current?.removeEventListener("mouseleave", () => setIsVisible(false));
};
}, []);
useEffect(() => {
if (!isVisible || !tooltipRef.current || !tooltipWrapperRef.current) return;
const pos = calculatePosition(position, tooltipRef, tooltipWrapperRef);
setTooltipPosition(pos);
}, [isVisible, position]);
return (
<div ref={tooltipWrapperRef} className="relative hover:cursor-pointer">
{children}
{isVisible && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: showDelay / 1000, duration: 0 }}
>
<TooltipContent
label={label}
position={tooltipPosition || {}}
ref={tooltipRef as React.RefObject<HTMLDivElement>}
/>
</motion.div>
)}
</div>
);
}
function TooltipContent({
label,
position,
ref,
}: {
label: string;
position: TooltipPosition;
ref: React.RefObject<HTMLDivElement>;
}) {
return (
<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"
>
{label}
</div>
);
}
function calculatePosition(
position: TooltipProps["position"],
tooltipRef: React.RefObject<HTMLDivElement | null>,
tooltipWrapperRef: React.RefObject<HTMLDivElement | null>
) {
if (!tooltipWrapperRef.current || !tooltipRef.current) return {};
const wrapperRect = tooltipWrapperRef.current.getBoundingClientRect();
const tooltipRect = tooltipRef.current.getBoundingClientRect();
const GAP = 8;
function adjustHorizontal() {
let left = wrapperRect.width / 2;
let transform = "translateX(-50%)";
const tooltipLeft = wrapperRect.left + left - tooltipRect.width / 2;
const tooltipRight = tooltipLeft + tooltipRect.width;
// Если не влезает горизонтально, то сдвигаем
if (tooltipLeft < GAP) {
left = GAP - wrapperRect.left;
transform = "none";
} else if (tooltipRight > window.innerWidth - GAP) {
left = window.innerWidth - GAP - wrapperRect.left - tooltipRect.width;
transform = "none";
}
return { left, transform };
}
function adjustVertical() {
return { top: wrapperRect.height / 2, transform: "translateY(-50%)" };
}
switch (position) {
case "top": {
return wrapperRect.bottom + tooltipRect.height + GAP < 0
? { bottom: -tooltipRect.height - GAP, ...adjustHorizontal() }
: { top: -tooltipRect.height - GAP, ...adjustHorizontal() };
}
case "bottom": {
return wrapperRect.bottom + tooltipRect.height + GAP > window.innerHeight
? { top: -tooltipRect.height - GAP, ...adjustHorizontal() }
: { bottom: -tooltipRect.height - GAP, ...adjustHorizontal() };
}
case "left": {
return wrapperRect.left - tooltipRect.width - GAP < 0
? { right: -tooltipRect.width - GAP, ...adjustVertical() }
: { left: -tooltipRect.width - GAP, ...adjustVertical() };
}
case "right": {
return wrapperRect.right + tooltipRect.width + GAP > window.innerWidth
? { left: -tooltipRect.width - GAP, ...adjustVertical() }
: { right: -tooltipRect.width - GAP, ...adjustVertical() };
}
case "cursor": {
return { transform: "none" };
}
}
return {};
}