Initial commit: Data Analysis application with FastAPI backend and Next.js frontend
This commit is contained in:
120
frontend/src/features/help/components/HelpTooltip.tsx
Normal file
120
frontend/src/features/help/components/HelpTooltip.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user