AUDIT / src /components /ErrorBanner.tsx
Arypulka98's picture
feat(audit): deploy full backend cluster node
aea470f verified
Raw
History Blame Contribute Delete
3.04 kB
/**
* 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;