体验: toast改macOS通知风圆徽毛玻璃卡片,移除明暗主题切换仅保留亮色
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,20 +1,12 @@
|
||||
import { FolderCog, Monitor, Moon, PanelBottom, PanelLeft, PanelRight, Sun, Unplug } from "lucide-react"
|
||||
import { FolderCog, PanelBottom, PanelLeft, PanelRight, Unplug } from "lucide-react"
|
||||
|
||||
import { Button } from "~/components/ui/button"
|
||||
import { Separator } from "~/components/ui/separator"
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "~/components/ui/tooltip"
|
||||
import { useTheme, type ThemePreference } from "~/hooks/use-theme"
|
||||
import { isMacElectron } from "~/lib/platform"
|
||||
import type { WorkspaceSession } from "~/lib/tfs/session"
|
||||
import { cn } from "~/lib/utils"
|
||||
|
||||
// 主题三态对应的图标与说明文案。
|
||||
const THEME_VISUALS: Record<ThemePreference, { icon: typeof Monitor; label: string }> = {
|
||||
system: { icon: Monitor, label: "主题:跟随系统" },
|
||||
light: { icon: Sun, label: "主题:亮色" },
|
||||
dark: { icon: Moon, label: "主题:暗色" },
|
||||
}
|
||||
|
||||
// 三个可折叠面板的显隐状态,由 WorkspaceShell 持有。
|
||||
export interface PanelVisibility {
|
||||
tree: boolean
|
||||
@@ -41,9 +33,6 @@ export function TopBar({
|
||||
}) {
|
||||
// 隐藏式标题栏下顶栏整体作为拖拽区,并为左上角红绿灯预留空间。
|
||||
const macInset = isMacElectron()
|
||||
const theme = useTheme()
|
||||
const themeVisual = THEME_VISUALS[theme.preference]
|
||||
const ThemeIcon = themeVisual.icon
|
||||
return (
|
||||
<header
|
||||
className={cn(
|
||||
@@ -91,14 +80,6 @@ export function TopBar({
|
||||
<PanelBottom />
|
||||
</PanelToggle>
|
||||
<Separator orientation="vertical" className="mx-1 h-4!" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button size="icon-sm" variant="ghost" onClick={theme.cycle} aria-label={themeVisual.label}>
|
||||
<ThemeIcon />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">{themeVisual.label}(点击切换)</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button size="icon-sm" variant="ghost" onClick={onManageWorkspace} aria-label="工作区与映射管理">
|
||||
|
||||
@@ -167,7 +167,7 @@ export function ConflictDialog({
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<Badge variant="secondary" className="rounded-md bg-red-500/10 text-red-700 dark:text-red-400">
|
||||
<Badge variant="secondary" className="rounded-md bg-red-500/10 text-red-700">
|
||||
{conflicts.length} 个冲突
|
||||
</Badge>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
|
||||
@@ -46,13 +46,6 @@ function splitDiffSides(raw: string[]): { original: string; modified: string } {
|
||||
return { original: original.join("\n"), modified: modified.join("\n") }
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前应用是否处于暗色主题(class 方案),用于选择 Monaco 主题。
|
||||
*/
|
||||
function isDarkTheme(): boolean {
|
||||
return typeof document !== "undefined" && document.documentElement.classList.contains("dark")
|
||||
}
|
||||
|
||||
/**
|
||||
* 文本 Diff 弹窗:基于 Monaco DiffEditor 渲染,支持 本地 vs 服务器 latest
|
||||
* 与 两个历史版本 对比。提供统一 / 左右分栏视图、上一处 / 下一处差异导航、
|
||||
@@ -126,7 +119,6 @@ export function DiffDialog({
|
||||
let diffEditor: MonacoEditor.IStandaloneDiffEditor | null = null
|
||||
let originalModel: MonacoEditor.ITextModel | null = null
|
||||
let modifiedModel: MonacoEditor.ITextModel | null = null
|
||||
let themeObserver: MutationObserver | null = null
|
||||
void (async () => {
|
||||
const { monaco, detectLanguage } = await import("~/lib/monaco")
|
||||
if (disposed || !containerRef.current) {
|
||||
@@ -144,7 +136,7 @@ export function DiffDialog({
|
||||
// 统一视图下隐藏旧版行号列,避免出现两列紧贴的行号。
|
||||
compactMode: viewMode === "unified",
|
||||
hideUnchangedRegions: { enabled: onlyChanges },
|
||||
theme: isDarkTheme() ? "mactfs-dark" : "mactfs-light",
|
||||
theme: "mactfs-light",
|
||||
fontSize: 12,
|
||||
lineHeight: 20,
|
||||
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, "Courier New", monospace',
|
||||
@@ -168,16 +160,10 @@ export function DiffDialog({
|
||||
setBlockCount(diffEditor?.getLineChanges()?.length ?? 0)
|
||||
})
|
||||
editorRef.current = diffEditor
|
||||
// 弹窗打开期间应用切换明暗主题时,同步更新 Monaco 主题。
|
||||
themeObserver = new MutationObserver(() => {
|
||||
monaco.editor.setTheme(isDarkTheme() ? "mactfs-dark" : "mactfs-light")
|
||||
})
|
||||
themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] })
|
||||
})()
|
||||
return () => {
|
||||
disposed = true
|
||||
editorRef.current = null
|
||||
themeObserver?.disconnect()
|
||||
diffEditor?.dispose()
|
||||
originalModel?.dispose()
|
||||
modifiedModel?.dispose()
|
||||
@@ -257,10 +243,10 @@ export function DiffDialog({
|
||||
<div className="flex shrink-0 flex-wrap items-center gap-2 text-xs">
|
||||
{diff && (
|
||||
<>
|
||||
<Badge variant="secondary" className="rounded-md bg-red-500/10 text-red-700 dark:text-red-400">
|
||||
<Badge variant="secondary" className="rounded-md bg-red-500/10 text-red-700">
|
||||
− {diff.sourceLabel}
|
||||
</Badge>
|
||||
<Badge variant="secondary" className="rounded-md bg-green-500/10 text-green-700 dark:text-green-400">
|
||||
<Badge variant="secondary" className="rounded-md bg-green-500/10 text-green-700">
|
||||
+ {diff.targetLabel}
|
||||
</Badge>
|
||||
<span className="text-muted-foreground">{changedCount} 行差异</span>
|
||||
|
||||
@@ -21,42 +21,42 @@ const FILE_ICON_RULES: { exts: string[]; icon: LucideIcon; className: string }[]
|
||||
{
|
||||
exts: ["ts", "tsx", "js", "jsx", "java", "cs", "py", "go", "rs", "c", "cpp", "h", "vue", "css", "scss", "html"],
|
||||
icon: FileCode,
|
||||
className: "text-blue-600 dark:text-blue-400",
|
||||
className: "text-blue-600",
|
||||
},
|
||||
{
|
||||
exts: ["json", "xml", "yml", "yaml", "toml", "config", "properties", "gradle"],
|
||||
icon: FileJson,
|
||||
className: "text-amber-600 dark:text-amber-400",
|
||||
className: "text-amber-600",
|
||||
},
|
||||
{
|
||||
exts: ["md", "txt", "doc", "docx", "pdf", "rtf"],
|
||||
icon: FileText,
|
||||
className: "text-zinc-500 dark:text-zinc-400",
|
||||
className: "text-zinc-500",
|
||||
},
|
||||
{
|
||||
exts: ["sql", "db"],
|
||||
icon: Database,
|
||||
className: "text-violet-600 dark:text-violet-400",
|
||||
className: "text-violet-600",
|
||||
},
|
||||
{
|
||||
exts: ["png", "jpg", "jpeg", "gif", "svg", "webp", "ico", "icns"],
|
||||
icon: FileImage,
|
||||
className: "text-emerald-600 dark:text-emerald-400",
|
||||
className: "text-emerald-600",
|
||||
},
|
||||
{
|
||||
exts: ["zip", "rar", "7z", "tar", "gz", "jar"],
|
||||
icon: FileArchive,
|
||||
className: "text-orange-600 dark:text-orange-400",
|
||||
className: "text-orange-600",
|
||||
},
|
||||
{
|
||||
exts: ["xls", "xlsx", "csv"],
|
||||
icon: FileSpreadsheet,
|
||||
className: "text-green-600 dark:text-green-400",
|
||||
className: "text-green-600",
|
||||
},
|
||||
{
|
||||
exts: ["sh", "bat", "cmd", "ps1", "zsh"],
|
||||
icon: FileTerminal,
|
||||
className: "text-zinc-600 dark:text-zinc-300",
|
||||
className: "text-zinc-600",
|
||||
},
|
||||
]
|
||||
|
||||
@@ -91,7 +91,7 @@ export function FileIcon({
|
||||
<Icon
|
||||
className={cn(
|
||||
"size-3.5 shrink-0",
|
||||
mapped ? "text-primary" : "text-sky-600 dark:text-sky-400",
|
||||
mapped ? "text-primary" : "text-sky-600",
|
||||
className,
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -185,7 +185,7 @@ export function WorkspaceManageDialog({
|
||||
) : (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="bg-red-500/10 text-red-700 dark:text-red-400"
|
||||
className="bg-red-500/10 text-red-700"
|
||||
>
|
||||
不存在
|
||||
</Badge>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import { AlertCircle, CheckCircle2, Loader2 } from "lucide-react"
|
||||
import { Check, Loader2, X } from "lucide-react"
|
||||
|
||||
import { cn } from "~/lib/utils"
|
||||
|
||||
@@ -70,34 +70,15 @@ export function showToast(input: { id?: string; kind: ToastKind; text: string })
|
||||
return id
|
||||
}
|
||||
|
||||
// toast 类型对应的图标、强调条与边框配色:彩色左侧条 + 着色边框提高辨识度。
|
||||
const KIND_VISUALS: Record<
|
||||
ToastKind,
|
||||
{ icon: typeof CheckCircle2; iconClass: string; barClass: string; borderClass: string }
|
||||
> = {
|
||||
success: {
|
||||
icon: CheckCircle2,
|
||||
iconClass: "text-emerald-600 dark:text-emerald-400",
|
||||
barClass: "bg-emerald-500",
|
||||
borderClass: "border-emerald-500/35",
|
||||
},
|
||||
error: {
|
||||
icon: AlertCircle,
|
||||
iconClass: "text-destructive",
|
||||
barClass: "bg-destructive",
|
||||
borderClass: "border-destructive/40",
|
||||
},
|
||||
loading: {
|
||||
icon: Loader2,
|
||||
iconClass: "animate-spin text-primary",
|
||||
barClass: "bg-primary",
|
||||
borderClass: "border-primary/35",
|
||||
},
|
||||
// toast 类型对应的状态徽标:macOS 通知风的彩色小圆徽(成功绿 / 失败红),loading 用旋转图标。
|
||||
const KIND_BADGES: Record<"success" | "error", { icon: typeof Check; chipClass: string }> = {
|
||||
success: { icon: Check, chipClass: "bg-emerald-500" },
|
||||
error: { icon: X, chipClass: "bg-destructive" },
|
||||
}
|
||||
|
||||
/**
|
||||
* toast 挂载点:固定在窗口右上角(顶栏下方),新条目自上滑入,点击可关闭。
|
||||
* 彩色强调条 + 着色边框 + 毛玻璃底,保证进度与结果提示足够显眼。
|
||||
* 视觉对齐 macOS 系统通知:白色毛玻璃圆角卡片 + 彩色状态圆徽 + 柔和投影,无彩色边框。
|
||||
*/
|
||||
export function Toaster() {
|
||||
const [items, setItems] = useState<ToastItem[]>([])
|
||||
@@ -116,26 +97,42 @@ export function Toaster() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pointer-events-none fixed top-14 right-3 z-[60] flex w-[380px] flex-col gap-2">
|
||||
{items.map((item) => {
|
||||
const visual = KIND_VISUALS[item.kind]
|
||||
const Icon = visual.icon
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
onClick={() => dismissToast(item.id)}
|
||||
className={cn(
|
||||
"animate-in fade-in slide-in-from-top-3 ease-out-quart pointer-events-auto relative flex items-start gap-2.5 overflow-hidden rounded-xl border bg-popover/95 py-3 pr-3.5 pl-4 text-left text-sm font-medium shadow-overlay backdrop-blur-md duration-250",
|
||||
visual.borderClass,
|
||||
)}
|
||||
>
|
||||
<span className={cn("absolute inset-y-0 left-0 w-1", visual.barClass)} />
|
||||
<Icon className={cn("mt-0.5 size-4.5 shrink-0", visual.iconClass)} />
|
||||
<span className="min-w-0 break-words">{item.text}</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
<div className="pointer-events-none fixed top-14 right-4 z-[60] flex flex-col items-end gap-2.5">
|
||||
{items.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
onClick={() => dismissToast(item.id)}
|
||||
className="animate-in fade-in slide-in-from-top-2 ease-out-quart pointer-events-auto flex w-fit max-w-[360px] min-w-[200px] items-center gap-2.5 rounded-xl border border-black/8 bg-popover/90 py-2.5 pr-4 pl-3 text-left shadow-[0_8px_24px_rgb(0_0_0/0.12),0_2px_6px_rgb(0_0_0/0.06)] backdrop-blur-xl duration-300"
|
||||
>
|
||||
{item.kind === "loading" ? (
|
||||
<Loader2 className="size-4.5 shrink-0 animate-spin text-primary" />
|
||||
) : (
|
||||
<ToastBadge kind={item.kind} />
|
||||
)}
|
||||
<span className="min-w-0 text-[13px] leading-snug font-medium break-words text-foreground">
|
||||
{item.text}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功 / 失败的彩色圆形徽标:实色圆底 + 白色粗描边图标。
|
||||
*/
|
||||
function ToastBadge({ kind }: { kind: "success" | "error" }) {
|
||||
const badge = KIND_BADGES[kind]
|
||||
const Icon = badge.icon
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"flex size-5 shrink-0 items-center justify-center rounded-full",
|
||||
badge.chipClass,
|
||||
)}
|
||||
>
|
||||
<Icon className="size-3 text-white" strokeWidth={3.2} />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user