Files
sundynix-agentix/sundynix-desktop/frontend/src/ui/Toast.tsx
T
Blizzard 72bd43965f feat(desktop): 工业化升级 A —— 设计系统地基(primitives + lucide + 语义令牌)
把"手搓内联 class + Unicode 字符图标"换成统一组件与真实图标,为后续工业化打底。

- 依赖:装 lucide-react(描线图标,按需 tree-shake)
- 令牌:tailwind.config 加语义色 brand/accent/success/warn/danger + 圆角档位;
  强调色字面量(violet/cyan/emerald…)收敛到令牌,便于整体换肤
- primitives(src/ui,零重依赖自建):Button/Input/Textarea/Select/Field/Card/Panel/
  Badge/Dot/Tabs/Skeleton/EmptyState/Dialog/Toast(+useToast)/cn,桶文件统一引入
- 迁移:TopBar/LeftNav/BottomDrawer + Home/Report/Runs/Kb/Placeholder/ExecTrace/
  MemoryPanel/StudioView 全部换 primitives + lucide 图标;导航/能力卡/按钮告别
  ▤◆▣▦ 等 Unicode 字符;错误改用全局 Toast;空状态用 EmptyState
- App 包 ToastProvider

验证:tsc + vite build 通过;浏览器(Preview)走查工作台/报告页——真实图标、统一卡片/
按钮/输入;跑报告端到端正常(执行轨迹 lucide 状态图标点亮、章节耗时/字数/检索片段、
完成弹 Toast + 下载 Word)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 16:39:42 +08:00

65 lines
2.0 KiB
TypeScript

import { createContext, useCallback, useContext, useRef, useState, type ReactNode } from "react";
import { CheckCircle2, AlertTriangle, Info, X } from "lucide-react";
import { cn } from "./cn";
type ToastTone = "success" | "error" | "info";
interface Toast {
id: number;
tone: ToastTone;
msg: string;
}
interface ToastCtx {
push: (tone: ToastTone, msg: string) => void;
}
const Ctx = createContext<ToastCtx>({ push: () => {} });
// useToast 在任意组件里弹出全局通知。
export function useToast() {
return useContext(Ctx);
}
const icons = { success: CheckCircle2, error: AlertTriangle, info: Info };
const accent = {
success: "text-success",
error: "text-danger",
info: "text-accent-400",
};
export function ToastProvider({ children }: { children: ReactNode }) {
const [toasts, setToasts] = useState<Toast[]>([]);
const seq = useRef(0);
const push = useCallback((tone: ToastTone, msg: string) => {
const id = ++seq.current;
setToasts((t) => [...t, { id, tone, msg }]);
setTimeout(() => setToasts((t) => t.filter((x) => x.id !== id)), 4200);
}, []);
const dismiss = (id: number) => setToasts((t) => t.filter((x) => x.id !== id));
return (
<Ctx.Provider value={{ push }}>
{children}
<div className="pointer-events-none fixed bottom-4 right-4 z-50 flex w-80 flex-col gap-2">
{toasts.map((t) => {
const Icon = icons[t.tone];
return (
<div
key={t.id}
className="pointer-events-auto flex items-start gap-2.5 rounded-lg border border-line bg-ink-850 px-3 py-2.5 shadow-card"
>
<Icon className={cn("mt-0.5 h-4 w-4 shrink-0", accent[t.tone])} strokeWidth={2} />
<span className="flex-1 text-xs leading-relaxed text-slate-200">{t.msg}</span>
<button onClick={() => dismiss(t.id)} className="text-slate-600 hover:text-slate-300">
<X className="h-3.5 w-3.5" />
</button>
</div>
);
})}
</div>
</Ctx.Provider>
);
}