72bd43965f
把"手搓内联 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>
65 lines
2.0 KiB
TypeScript
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>
|
|
);
|
|
}
|