Files
breakpilot-lehrer/website/components/compliance/GlossaryTooltip.tsx
Benjamin Boenisch 5a31f52310 Initial commit: breakpilot-lehrer - Lehrer KI Platform
Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website,
Klausur-Service, School-Service, Voice-Service, Geo-Service,
BreakPilot Drive, Agent-Core

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:26 +01:00

166 lines
4.6 KiB
TypeScript

'use client'
/**
* GlossaryTooltip Component
*
* Displays a term with a hover tooltip that explains the compliance concept.
* Supports bilingual content (DE/EN) from the i18n system.
*/
import { useState, useRef, useEffect } from 'react'
import { getTerm, getDescription, Language } from '@/lib/compliance-i18n'
interface GlossaryTooltipProps {
termKey: string
lang?: Language
children?: React.ReactNode
className?: string
showIcon?: boolean
}
export default function GlossaryTooltip({
termKey,
lang = 'de',
children,
className = '',
showIcon = true,
}: GlossaryTooltipProps) {
const [isVisible, setIsVisible] = useState(false)
const [position, setPosition] = useState<'top' | 'bottom'>('top')
const triggerRef = useRef<HTMLSpanElement>(null)
const tooltipRef = useRef<HTMLDivElement>(null)
const term = getTerm(lang, termKey)
const description = getDescription(lang, termKey)
useEffect(() => {
if (isVisible && triggerRef.current && tooltipRef.current) {
const triggerRect = triggerRef.current.getBoundingClientRect()
const tooltipHeight = tooltipRef.current.offsetHeight
const spaceAbove = triggerRect.top
const spaceBelow = window.innerHeight - triggerRect.bottom
// Position tooltip where there's more space
if (spaceAbove < tooltipHeight + 10 && spaceBelow > spaceAbove) {
setPosition('bottom')
} else {
setPosition('top')
}
}
}, [isVisible])
if (!description) {
// No description available, just render the term
return <span className={className}>{children || term}</span>
}
return (
<span
ref={triggerRef}
className={`relative inline-flex items-center gap-1 cursor-help ${className}`}
onMouseEnter={() => setIsVisible(true)}
onMouseLeave={() => setIsVisible(false)}
onFocus={() => setIsVisible(true)}
onBlur={() => setIsVisible(false)}
tabIndex={0}
>
{children || term}
{showIcon && (
<svg
className="w-3.5 h-3.5 text-slate-400 hover:text-slate-600 transition-colors"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
)}
{/* Tooltip */}
{isVisible && (
<div
ref={tooltipRef}
className={`
absolute z-50 w-64 p-3 text-sm
bg-slate-900 text-white rounded-lg shadow-xl
transition-opacity duration-150
${position === 'top' ? 'bottom-full mb-2' : 'top-full mt-2'}
left-1/2 -translate-x-1/2
`}
role="tooltip"
>
{/* Arrow */}
<div
className={`
absolute left-1/2 -translate-x-1/2 w-0 h-0
border-l-8 border-r-8 border-transparent
${position === 'top'
? 'top-full border-t-8 border-t-slate-900'
: 'bottom-full border-b-8 border-b-slate-900'
}
`}
/>
{/* Content */}
<div className="font-semibold text-white mb-1">{term}</div>
<div className="text-slate-300 text-xs leading-relaxed">{description}</div>
</div>
)}
</span>
)
}
/**
* InfoIcon Component
*
* A standalone info icon that shows a tooltip on hover.
*/
interface InfoIconProps {
text: string
className?: string
}
export function InfoIcon({ text, className = '' }: InfoIconProps) {
const [isVisible, setIsVisible] = useState(false)
return (
<span
className={`relative inline-flex items-center cursor-help ${className}`}
onMouseEnter={() => setIsVisible(true)}
onMouseLeave={() => setIsVisible(false)}
>
<svg
className="w-4 h-4 text-slate-400 hover:text-slate-600 transition-colors"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
{isVisible && (
<div
className="
absolute z-50 w-56 p-2 text-xs
bg-slate-800 text-slate-200 rounded-lg shadow-lg
bottom-full mb-2 left-1/2 -translate-x-1/2
"
>
{text}
<div className="absolute left-1/2 -translate-x-1/2 top-full w-0 h-0 border-l-6 border-r-6 border-transparent border-t-6 border-t-slate-800" />
</div>
)}
</span>
)
}