121 lines
3.3 KiB
TypeScript
121 lines
3.3 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState } from "react";
|
|
import { Info } from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
interface HelpTooltipProps {
|
|
/**
|
|
* Contenu du tooltip
|
|
*/
|
|
content: string | React.ReactNode;
|
|
/**
|
|
* Position du tooltip
|
|
* @default "top"
|
|
*/
|
|
position?: "top" | "bottom" | "left" | "right";
|
|
/**
|
|
* Largeur maximale du tooltip
|
|
* @default "250px"
|
|
*/
|
|
maxWidth?: string;
|
|
/**
|
|
* Classe CSS personnalisée pour le wrapper
|
|
*/
|
|
className?: string;
|
|
/**
|
|
* Si vrai, affiche une icône d'information
|
|
* @default true
|
|
*/
|
|
showIcon?: boolean;
|
|
/**
|
|
* Contenu personnalisé pour déclencher le tooltip
|
|
*/
|
|
trigger?: React.ReactNode;
|
|
}
|
|
|
|
export function HelpTooltip({
|
|
content,
|
|
position = "top",
|
|
maxWidth = "250px",
|
|
className,
|
|
showIcon = true,
|
|
trigger,
|
|
}: HelpTooltipProps) {
|
|
const [isVisible, setIsVisible] = useState(false);
|
|
|
|
const positionClasses = {
|
|
top: "bottom-full left-1/2 -translate-x-1/2 mb-2 pb-1",
|
|
bottom: "top-full left-1/2 -translate-x-1/2 mt-2 pt-1",
|
|
left: "right-full top-1/2 -translate-y-1/2 mr-2 pr-1",
|
|
right: "left-full top-1/2 -translate-y-1/2 ml-2 pl-1",
|
|
};
|
|
|
|
const arrowClasses = {
|
|
top: "top-full left-1/2 -translate-x-1/2 border-t-slate-800 border-r-transparent border-b-transparent border-l-transparent",
|
|
bottom: "bottom-full left-1/2 -translate-x-1/2 border-b-slate-800 border-r-transparent border-t-transparent border-l-transparent",
|
|
left: "left-full top-1/2 -translate-y-1/2 border-l-slate-800 border-t-transparent border-r-transparent border-b-transparent",
|
|
right: "right-full top-1/2 -translate-y-1/2 border-r-slate-800 border-t-transparent border-l-transparent border-b-transparent",
|
|
};
|
|
|
|
const defaultTrigger = (
|
|
<button
|
|
type="button"
|
|
className={cn(
|
|
"inline-flex items-center justify-center w-4 h-4 rounded-full",
|
|
"text-slate-400 hover:text-indigo-600 hover:bg-indigo-50",
|
|
"transition-colors duration-150",
|
|
"focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-1",
|
|
className
|
|
)}
|
|
onMouseEnter={() => setIsVisible(true)}
|
|
onMouseLeave={() => setIsVisible(false)}
|
|
onFocus={() => setIsVisible(true)}
|
|
onBlur={() => setIsVisible(false)}
|
|
aria-label="Information"
|
|
>
|
|
<Info className="w-3 h-3" strokeWidth={2.5} />
|
|
</button>
|
|
);
|
|
|
|
return (
|
|
<div className="relative inline-flex">
|
|
<div
|
|
onMouseEnter={() => setIsVisible(true)}
|
|
onMouseLeave={() => setIsVisible(false)}
|
|
>
|
|
{trigger || defaultTrigger}
|
|
</div>
|
|
|
|
{isVisible && (
|
|
<div
|
|
className={cn(
|
|
"absolute z-50 pointer-events-none",
|
|
positionClasses[position]
|
|
)}
|
|
style={{ maxWidth }}
|
|
role="tooltip"
|
|
aria-hidden={!isVisible}
|
|
>
|
|
{/* Arrow */}
|
|
<div
|
|
className={cn(
|
|
"absolute w-0 h-0 border-4",
|
|
arrowClasses[position]
|
|
)}
|
|
/>
|
|
|
|
{/* Content */}
|
|
<div className="relative bg-slate-800 text-slate-100 text-xs rounded-md shadow-lg px-3 py-2 leading-relaxed">
|
|
{typeof content === "string" ? (
|
|
<p>{content}</p>
|
|
) : (
|
|
<div>{content}</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|