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>
166 lines
4.6 KiB
TypeScript
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>
|
|
)
|
|
}
|