Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | 1x 1x 1x 18x 18x 18x 18x 2x 2x 2x 16x 16x 18x 18x 16x 16x 16x 18x 18x 18x 18x 18x 18x 18x 5x 5x 5x 5x 11x 11x 11x 18x 4x 4x 4x 4x 4x 4x 18x 18x 18x 5x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 18x 18x | import { useEffect, useState } from "react";
import { Download, X, RefreshCw } from "lucide-react";
import type { UpdateState } from "@/hooks/useUpdateChecker";
interface UpdateBannerProps {
state: UpdateState;
hidden?: boolean;
}
export const UpdateBanner = ({ state, hidden }: UpdateBannerProps) => {
const { update, installing, progress, error, packageInstall, dismiss, install } = state;
const [visible, setVisible] = useState(false);
useEffect(() => {
if (!update || hidden) {
setVisible(false);
return;
}
const id = requestAnimationFrame(() => setVisible(true));
return () => cancelAnimationFrame(id);
}, [update, hidden]);
if (!update || hidden) return null;
return (
<div
className={`fixed bottom-4 right-4 z-50 flex items-center gap-3 bg-card border border-border rounded-xl px-4 py-3 shadow-2xl max-w-sm transition-all duration-300 ease-out ${
visible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-4"
}`}
>
<div className="flex-1 min-w-0">
<p className="text-sm font-semibold leading-tight">
Update available — v{update.version}
</p>
{installing ? (
<p className="text-xs text-muted-foreground mt-0.5">
{progress !== null ? `Downloading… ${progress}%` : "Installing…"}
{packageInstall && progress === 100 && " (password required)"}
</p>
) : (
<p className="text-xs text-muted-foreground mt-0.5 truncate">
{update.body ?? "A new version is ready to install."}
</p>
)}
{installing && progress !== null && (
<div className="mt-1.5 h-1 w-full rounded-full bg-secondary overflow-hidden">
<div
className="h-full bg-primary transition-all duration-200"
style={{ width: `${progress}%` }}
/>
</div>
)}
</div>
{error && <p className="text-xs text-destructive mt-1">{error}</p>}
{installing ? (
<RefreshCw className="h-4 w-4 text-muted-foreground animate-spin shrink-0" />
) : (
<>
<button
onClick={install}
className="flex items-center gap-1.5 text-xs font-semibold bg-primary text-primary-foreground px-3 py-1.5 rounded-lg hover:bg-primary/90 transition-colors shrink-0"
>
<Download className="h-3 w-3" />
Install
</button>
<button
onClick={dismiss}
aria-label="Dismiss update"
className="text-muted-foreground hover:text-foreground transition-colors shrink-0"
>
<X className="h-4 w-4" />
</button>
</>
)}
</div>
);
};
|