- Header: Ajout 3ème ligne de statut (Caisses & orga.) avec descriptions détaillées - Tooltips: Affichage riche avec titre, voyant coloré et contenu markdown formaté - Éditeur markdown: Nouveau composant RichTextEditor avec toolbar (gras, italique, liens, listes) - Modal staff: StatusEditModal étendu avec onglets et éditeur de descriptions - Migration: Ajout colonnes status_*_description dans maintenance_status - API: Routes GET/PUT enrichies pour gérer les 9 champs de statut - Navigation: Redirection /compte/securite → /securite (nouvelle page centralisée) - Breadcrumb: Support contrats RG/CDDU multi + labels dynamiques salariés - UX Documents: Bouton 'Nouvel onglet / Télécharger' au lieu de téléchargement forcé - Contrats staff: Pagination paies (6/page) pour RG et CDDU multi-mois avec vue compacte - PayslipCard: Bouton cliquable 'Ouvrir le PDF' pour accès direct aux bulletins
122 lines
No EOL
3.8 KiB
TypeScript
122 lines
No EOL
3.8 KiB
TypeScript
"use client"
|
|
|
|
import * as React from "react"
|
|
import { createPortal } from "react-dom"
|
|
import { cn } from "@/lib/utils"
|
|
|
|
interface TooltipProps {
|
|
children: React.ReactNode
|
|
content: string
|
|
className?: string
|
|
side?: 'top' | 'bottom' | 'left' | 'right'
|
|
asChild?: boolean
|
|
allowHTML?: boolean
|
|
}
|
|
|
|
export function Tooltip({ children, content, className, side = 'top', asChild = false, allowHTML = false }: TooltipProps) {
|
|
const [isVisible, setIsVisible] = React.useState(false)
|
|
const [triggerRect, setTriggerRect] = React.useState<DOMRect | null>(null)
|
|
const triggerRef = React.useRef<HTMLDivElement>(null)
|
|
|
|
const updatePosition = () => {
|
|
if (triggerRef.current) {
|
|
setTriggerRect(triggerRef.current.getBoundingClientRect())
|
|
}
|
|
}
|
|
|
|
const handleMouseEnter = () => {
|
|
updatePosition()
|
|
setIsVisible(true)
|
|
}
|
|
|
|
const handleMouseLeave = () => {
|
|
setIsVisible(false)
|
|
}
|
|
|
|
const getTooltipStyle = (): React.CSSProperties => {
|
|
if (!triggerRect) return {}
|
|
|
|
const offset = 8
|
|
let left = 0
|
|
let top = 0
|
|
|
|
switch (side) {
|
|
case 'top':
|
|
left = triggerRect.left + triggerRect.width / 2
|
|
top = triggerRect.top - offset
|
|
break
|
|
case 'bottom':
|
|
left = triggerRect.left + triggerRect.width / 2
|
|
top = triggerRect.bottom + offset
|
|
break
|
|
case 'left':
|
|
left = triggerRect.left - offset
|
|
top = triggerRect.top + triggerRect.height / 2
|
|
break
|
|
case 'right':
|
|
left = triggerRect.right + offset
|
|
top = triggerRect.top + triggerRect.height / 2
|
|
break
|
|
}
|
|
|
|
// Position the tooltip relative to the computed anchor point
|
|
// For 'top', anchor is at the bottom-center of the tooltip (so translateY(-100%))
|
|
// For 'bottom', anchor is at the top-center (no vertical translation)
|
|
// For 'left'/'right', anchor centered vertically.
|
|
let transform = ''
|
|
if (side === 'top') transform = 'translate(-50%, -100%)'
|
|
else if (side === 'bottom') transform = 'translate(-50%, 0)'
|
|
else if (side === 'left') transform = 'translate(-100%, -50%)'
|
|
else transform = 'translate(0, -50%)'
|
|
|
|
return {
|
|
position: 'fixed',
|
|
left: `${left}px`,
|
|
top: `${top}px`,
|
|
transform,
|
|
zIndex: 9999
|
|
}
|
|
}
|
|
|
|
const arrowClasses = {
|
|
top: 'top-full left-1/2 transform -translate-x-1/2 border-t-white border-t-4 border-x-transparent border-x-4 border-b-0',
|
|
bottom: 'bottom-full left-1/2 transform -translate-x-1/2 border-b-white border-b-4 border-x-transparent border-x-4 border-t-0',
|
|
left: 'left-full top-1/2 transform -translate-y-1/2 border-l-white border-l-4 border-y-transparent border-y-4 border-r-0',
|
|
right: 'right-full top-1/2 transform -translate-y-1/2 border-r-white border-r-4 border-y-transparent border-y-4 border-l-0'
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<div
|
|
ref={triggerRef}
|
|
className={asChild ? "block w-full" : "inline-block"}
|
|
onMouseEnter={handleMouseEnter}
|
|
onMouseLeave={handleMouseLeave}
|
|
onFocus={handleMouseEnter}
|
|
onBlur={handleMouseLeave}
|
|
>
|
|
{children}
|
|
</div>
|
|
|
|
{isVisible && triggerRect && typeof window !== 'undefined' && createPortal(
|
|
<div
|
|
style={getTooltipStyle()}
|
|
className={cn(
|
|
"px-4 py-3 text-sm rounded-xl shadow-2xl pointer-events-none relative max-w-md min-w-[280px]",
|
|
"bg-white text-slate-900 border border-slate-200",
|
|
"animate-in fade-in-0 zoom-in-95 duration-200",
|
|
className
|
|
)}
|
|
>
|
|
{allowHTML ? (
|
|
<div dangerouslySetInnerHTML={{ __html: content }} className="prose prose-sm max-w-none" />
|
|
) : (
|
|
content
|
|
)}
|
|
<div className={cn("absolute w-0 h-0", arrowClasses[side])} />
|
|
</div>,
|
|
document.body
|
|
)}
|
|
</>
|
|
)
|
|
} |