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>
This commit is contained in:
165
website/components/compliance/GlossaryTooltip.tsx
Normal file
165
website/components/compliance/GlossaryTooltip.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user