- packages/webui: Vite + React + Tailwind v4, single-file bundle - Dark industrial theme, Space Grotesk + JetBrains Mono - Config table with scope/flag badges, masked values, search/filter - Admin panel for agent management - scripts/build-ui.sh generates worker/src/ui.ts from webui build - Worker serves UI at GET / before auth check
32 lines
1.1 KiB
TypeScript
32 lines
1.1 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import type { Toast as ToastType } from "../types";
|
|
|
|
export default function Toast({ toasts, remove }: { toasts: ToastType[]; remove: (id: number) => void }) {
|
|
return (
|
|
<div className="fixed top-4 right-4 z-50 flex flex-col gap-2">
|
|
{toasts.map((t) => (
|
|
<ToastItem key={t.id} toast={t} remove={remove} />
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function ToastItem({ toast, remove }: { toast: ToastType; remove: (id: number) => void }) {
|
|
const [show, setShow] = useState(false);
|
|
useEffect(() => {
|
|
requestAnimationFrame(() => setShow(true));
|
|
const timer = setTimeout(() => { setShow(false); setTimeout(() => remove(toast.id), 300); }, 3000);
|
|
return () => clearTimeout(timer);
|
|
}, [toast.id, remove]);
|
|
|
|
return (
|
|
<div
|
|
className={`px-4 py-3 rounded-lg border text-sm font-medium transition-all duration-300 ${
|
|
show ? "opacity-100 translate-x-0" : "opacity-0 translate-x-4"
|
|
} ${toast.type === "error" ? "bg-red-500/10 border-red-500/30 text-red-400" : "bg-green-500/10 border-green-500/30 text-green-400"}`}
|
|
>
|
|
{toast.message}
|
|
</div>
|
|
);
|
|
}
|