Spaces:
Sleeping
Sleeping
| /** | |
| * ErrorBanner.tsx — Banner dismissible per errori provider | |
| * | |
| * Legge lastError da providerStore e mostra un banner compatto. | |
| * Auto-dismiss dopo 8s. Non blocca la chat. | |
| * Nessun prop richiesto — si collega allo store da solo. | |
| */ | |
| import { memo, useEffect, useRef } from "react"; | |
| import { useProviderStore, selectLastError } from "@/store/providerStore"; | |
| import { formatErrorBanner } from "@/core/errors"; | |
| import { Z_INDEX } from "@/lib/zindex"; | |
| const SEVERITY_COLORS = { | |
| info: { bg: "rgba(56,189,248,0.08)", border: "rgba(56,189,248,0.22)", text: "#38bdf8" }, | |
| warn: { bg: "rgba(251,191,36,0.08)", border: "rgba(251,191,36,0.22)", text: "#fbbf24" }, | |
| error: { bg: "rgba(248,113,113,0.08)", border: "rgba(248,113,113,0.22)", text: "#f87171" }, | |
| fatal: { bg: "rgba(239,68,68,0.12)", border: "rgba(239,68,68,0.30)", text: "#ef4444" }, | |
| } as const; | |
| const AUTO_DISMISS_MS = 8_000; | |
| const ErrorBanner = memo(function ErrorBanner() { | |
| const error = useProviderStore(selectLastError); | |
| const clearErr = useProviderStore(s => s.clearError); | |
| const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null); | |
| // Auto-dismiss | |
| useEffect(() => { | |
| if (!error) return; | |
| if (timerRef.current) clearTimeout(timerRef.current); | |
| timerRef.current = setTimeout(clearErr, AUTO_DISMISS_MS); | |
| return () => { | |
| if (timerRef.current) clearTimeout(timerRef.current); | |
| }; | |
| }, [error, clearErr]); | |
| if (!error) return null; | |
| const colors = SEVERITY_COLORS[error.severity as keyof typeof SEVERITY_COLORS] ?? SEVERITY_COLORS.warn; | |
| const label = formatErrorBanner(error); | |
| return ( | |
| <div | |
| role="alert" | |
| aria-live="polite" | |
| style={{ | |
| display: "flex", | |
| alignItems: "center", | |
| gap: 8, | |
| padding: "6px 14px", | |
| background: colors.bg, | |
| border: `1px solid ${colors.border}`, | |
| borderLeft: `3px solid ${colors.text}`, | |
| fontSize: "0.76rem", | |
| color: colors.text, | |
| flexShrink: 0, | |
| zIndex: Z_INDEX.BANNER, | |
| // Slide-in animation | |
| animation: "slideDown 0.2s ease", | |
| }} | |
| > | |
| {/* Severity dot */} | |
| <span style={{ | |
| width: 6, height: 6, borderRadius: "50%", | |
| background: colors.text, flexShrink: 0, | |
| }} /> | |
| {/* Message */} | |
| <span style={{ flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}> | |
| {label} | |
| </span> | |
| {/* Retry label se applicabile */} | |
| {error.retryable && ( | |
| <span style={{ color: colors.text, opacity: 0.7, fontSize: "0.68rem", flexShrink: 0 }}> | |
| ritento… | |
| </span> | |
| )} | |
| {/* Dismiss */} | |
| <button | |
| onClick={clearErr} | |
| aria-label="Chiudi" | |
| style={{ | |
| border: "none", background: "transparent", | |
| color: colors.text, cursor: "pointer", | |
| fontSize: 16, lineHeight: 1, padding: "0 2px", | |
| opacity: 0.7, flexShrink: 0, | |
| }} | |
| > | |
| × | |
| </button> | |
| </div> | |
| ); | |
| } | |
| ); | |
| export default ErrorBanner; | |