'use client' import { useEffect, useState } from 'react' // Stufe 3 of the Attribution Renderer (Task #23): an inline source // badge that any rendered control/hazard/measure can attach to itself. // // Visually a small license-rule pill (R1/R2/R3); on hover/click it // reveals the underlying regulation, license type, and — for Rule 2 — // the mandatory attribution string. // // Usage: // // // The component lazily fetches /licenses/source-info/{uuid} on first // expand so the surrounding list view stays cheap. type SourceInfo = { control_uuid: string license_rule: number | null license_label_de: string | null attribution_required: boolean render_full_text: boolean regulation_id: string | null regulation_name_de: string | null license_type: string | null attribution: string | null source_url: string | null } const RULE_BADGE: Record = { 1: 'bg-emerald-100 text-emerald-800 border-emerald-300', 2: 'bg-amber-100 text-amber-800 border-amber-300', 3: 'bg-slate-100 text-slate-700 border-slate-300', } const RULE_TITLE: Record = { 1: 'R1 — wörtlich übernehmbar', 2: 'R2 — wörtlich mit Attribution', 3: 'R3 — nur Identifier zitieren', } interface SourceBadgeProps { controlUuid: string /** Optional: skip the fetch and render from already-known data. */ prefetched?: SourceInfo /** Compact mode for tight UI rows (smaller pill). */ compact?: boolean } export function SourceBadge({ controlUuid, prefetched, compact }: SourceBadgeProps) { const [data, setData] = useState(prefetched ?? null) const [open, setOpen] = useState(false) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) useEffect(() => { if (!open || data) return setLoading(true) fetch(`/api/sdk/v1/compliance/licenses/source-info/${controlUuid}`) .then((r) => (r.ok ? r.json() : Promise.reject(`HTTP ${r.status}`))) .then(setData) .catch((e) => setError(String(e))) .finally(() => setLoading(false)) }, [open, data, controlUuid]) const rule = data?.license_rule ?? prefetched?.license_rule ?? null const badgeClass = rule ? RULE_BADGE[rule] ?? RULE_BADGE[3] : 'bg-slate-100 text-slate-500 border-slate-200' const sizeClass = compact ? 'text-[10px] px-1.5 py-0.5' : 'text-xs px-2 py-0.5' return ( setOpen((v) => !v)} className={`inline-flex items-center gap-1 rounded border font-medium ${sizeClass} ${badgeClass} hover:opacity-80 transition`} title={rule ? RULE_TITLE[rule] : 'Lizenz unbekannt'} aria-expanded={open} > {rule ? `R${rule}` : '?'} {open && ( {loading && Lade Quellen-Info…} {error && Fehler: {error}} {data && ( {data.license_label_de ?? 'Lizenz unbekannt'} {data.regulation_name_de && ( Quelle:{' '} {data.regulation_name_de} )} {data.license_type && ( Lizenztyp:{' '} {data.license_type} )} {data.attribution && ( Attribution-Pflicht {data.attribution} )} {!data.render_full_text && ( Volltext wird im Output nicht gerendert — nur Identifier-Verweis. )} {data.source_url && ( Originalquelle öffnen ↗ )} )} )} ) } export default SourceBadge
Lade Quellen-Info…
Fehler: {error}