- Added dark mode styles to TaskStatusDropdown, TaskStatusDropdownDebug, and TaskStatusDropdownSimple components. - Introduced ThemeProvider and useTheme hook for managing theme state. - Updated Button, Card, Input, Loading, Navigation, PageContainer, PageHeader, ProjectCalendarWidget, ProjectMap, SearchBar, States, Tooltip, and other UI components to support dark mode. - Created ThemeToggle component for switching between light and dark modes. - Enhanced i18n translations for settings related to theme and language preferences. - Configured Tailwind CSS to support dark mode with class-based toggling.
96 lines
2.6 KiB
JavaScript
96 lines
2.6 KiB
JavaScript
"use client";
|
|
|
|
import { useState, useRef, useEffect } from "react";
|
|
import { createPortal } from "react-dom";
|
|
|
|
export default function Tooltip({ children, content, className = "" }) {
|
|
const [isVisible, setIsVisible] = useState(false);
|
|
const [position, setPosition] = useState({ top: 0, left: 0 });
|
|
const triggerRef = useRef(null);
|
|
const tooltipRef = useRef(null);
|
|
|
|
const updatePosition = () => {
|
|
if (triggerRef.current && tooltipRef.current) {
|
|
const triggerRect = triggerRef.current.getBoundingClientRect();
|
|
const tooltipRect = tooltipRef.current.getBoundingClientRect();
|
|
const scrollY = window.scrollY;
|
|
const scrollX = window.scrollX;
|
|
|
|
// Calculate position (above the trigger by default)
|
|
let top = triggerRect.top + scrollY - tooltipRect.height - 8;
|
|
let left = triggerRect.left + scrollX + (triggerRect.width / 2) - (tooltipRect.width / 2);
|
|
|
|
// Keep tooltip within viewport
|
|
if (left < 10) left = 10;
|
|
if (left + tooltipRect.width > window.innerWidth - 10) {
|
|
left = window.innerWidth - tooltipRect.width - 10;
|
|
}
|
|
|
|
// If tooltip would go above viewport, show below instead
|
|
if (top < scrollY + 10) {
|
|
top = triggerRect.bottom + scrollY + 8;
|
|
}
|
|
|
|
setPosition({ top, left });
|
|
}
|
|
};
|
|
|
|
const handleMouseEnter = () => {
|
|
setIsVisible(true);
|
|
};
|
|
|
|
const handleMouseLeave = () => {
|
|
setIsVisible(false);
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (isVisible) {
|
|
// Small delay to ensure tooltip is rendered before positioning
|
|
const timer = setTimeout(() => {
|
|
updatePosition();
|
|
}, 10);
|
|
|
|
const handleScroll = () => updatePosition();
|
|
const handleResize = () => updatePosition();
|
|
|
|
window.addEventListener("scroll", handleScroll, true);
|
|
window.addEventListener("resize", handleResize);
|
|
|
|
return () => {
|
|
clearTimeout(timer);
|
|
window.removeEventListener("scroll", handleScroll, true);
|
|
window.removeEventListener("resize", handleResize);
|
|
};
|
|
}
|
|
}, [isVisible]);
|
|
|
|
const tooltip = isVisible && (
|
|
<div
|
|
ref={tooltipRef}
|
|
className={`fixed z-50 px-3 py-2 text-sm bg-gray-900 dark:bg-gray-700 text-white rounded-lg shadow-lg border border-gray-700 dark:border-gray-600 max-w-sm ${className}`}
|
|
style={{
|
|
top: position.top,
|
|
left: position.left,
|
|
}}
|
|
>
|
|
{content}
|
|
{/* Arrow pointing down */}
|
|
<div className="absolute top-full left-1/2 transform -translate-x-1/2 border-4 border-transparent border-t-gray-900 dark:border-t-gray-700"></div>
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<>
|
|
<span
|
|
ref={triggerRef}
|
|
onMouseEnter={handleMouseEnter}
|
|
onMouseLeave={handleMouseLeave}
|
|
className="inline-block"
|
|
>
|
|
{children}
|
|
</span>
|
|
{typeof document !== "undefined" && createPortal(tooltip, document.body)}
|
|
</>
|
|
);
|
|
}
|