chore(repo): initialize planarchy workspace
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
|
||||
interface InfoTooltipProps {
|
||||
content: React.ReactNode;
|
||||
/** Position relative to the trigger icon. Default: "top" */
|
||||
position?: "top" | "bottom";
|
||||
/** Extra width class, e.g. "w-72". Default: "w-60" */
|
||||
width?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Small ℹ icon that shows a tooltip on hover / focus.
|
||||
* Rendered via a portal into document.body so it's never clipped by
|
||||
* ancestor overflow:hidden containers (table cells, widget cards, etc.).
|
||||
*/
|
||||
export function InfoTooltip({ content, position = "top", width = "w-60" }: InfoTooltipProps) {
|
||||
const [show, setShow] = useState(false);
|
||||
const [coords, setCoords] = useState({ top: 0, left: 0 });
|
||||
const btnRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
function computeCoords() {
|
||||
if (!btnRef.current) return;
|
||||
const rect = btnRef.current.getBoundingClientRect();
|
||||
if (position === "top") {
|
||||
setCoords({
|
||||
top: rect.top + window.scrollY - 8, // 8px gap + arrow
|
||||
left: rect.left + window.scrollX + rect.width / 2,
|
||||
});
|
||||
} else {
|
||||
setCoords({
|
||||
top: rect.bottom + window.scrollY + 8,
|
||||
left: rect.left + window.scrollX + rect.width / 2,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleShow() {
|
||||
computeCoords();
|
||||
setShow(true);
|
||||
}
|
||||
|
||||
// Recompute on scroll/resize while shown so tooltip follows the trigger
|
||||
useEffect(() => {
|
||||
if (!show) return;
|
||||
const update = () => computeCoords();
|
||||
window.addEventListener("scroll", update, true);
|
||||
window.addEventListener("resize", update);
|
||||
return () => {
|
||||
window.removeEventListener("scroll", update, true);
|
||||
window.removeEventListener("resize", update);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [show]);
|
||||
|
||||
const tooltipStyle: React.CSSProperties =
|
||||
position === "top"
|
||||
? { position: "fixed", top: coords.top, left: coords.left, transform: "translate(-50%, -100%)" }
|
||||
: { position: "fixed", top: coords.top, left: coords.left, transform: "translateX(-50%)" };
|
||||
|
||||
const arrowClass =
|
||||
position === "top"
|
||||
? "top-full border-t-gray-900 border-l-transparent border-r-transparent border-b-transparent border-l-4 border-r-4 border-t-4 border-b-0"
|
||||
: "bottom-full border-b-gray-900 border-l-transparent border-r-transparent border-t-transparent border-l-4 border-r-4 border-b-4 border-t-0";
|
||||
|
||||
return (
|
||||
<span className="relative inline-flex items-center">
|
||||
<button
|
||||
ref={btnRef}
|
||||
type="button"
|
||||
onMouseEnter={handleShow}
|
||||
onMouseLeave={() => setShow(false)}
|
||||
onFocus={handleShow}
|
||||
onBlur={() => setShow(false)}
|
||||
className="ml-1 w-3.5 h-3.5 rounded-full bg-gray-200 dark:bg-gray-600 text-gray-500 dark:text-gray-300 text-[9px] font-bold flex items-center justify-center hover:bg-gray-300 dark:hover:bg-gray-500 cursor-help flex-shrink-0 leading-none"
|
||||
aria-label="More information"
|
||||
>
|
||||
i
|
||||
</button>
|
||||
|
||||
{show &&
|
||||
createPortal(
|
||||
<div
|
||||
style={tooltipStyle}
|
||||
className={`z-[9999] ${width} bg-gray-900 text-white text-xs rounded-lg px-3 py-2 shadow-xl pointer-events-none`}
|
||||
>
|
||||
{content}
|
||||
<span className={`absolute left-1/2 -translate-x-1/2 w-0 h-0 ${arrowClass}`} />
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user