feat(workbench): refine alert actions and removal transitions

This commit is contained in:
2026-11-07 19:56:14 +08:00
parent eee65ac8fd
commit ea3b62573c
2 changed files with 91 additions and 42 deletions
@@ -18,7 +18,7 @@ import {
type LucideIcon
} from "lucide-react";
import { useRef, useState, type ReactNode } from "react";
import { AnimatePresence } from "motion/react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { ProjectSwitchDialog } from "./project-switch-dialog";
import {
MAP_COMPACT_RADIUS_CLASS_NAME,
@@ -138,6 +138,9 @@ export function AlertMenu({
onToggleConditionFeed: () => void;
onAnalyzeAlertsWithAgent: () => void | Promise<void>;
}) {
const reduceMotion = useReducedMotion();
const keyboardInteraction = useRef(false);
const skipRemovalMotion = reduceMotion === true || keyboardInteraction.current;
const hasAlerts = alerts.length > 0;
const hasCriticalAlerts = alerts.some((alert) => alert.severity !== "warning");
const alertSummaryTone = hasCriticalAlerts ? "danger" : hasAlerts ? "warning" : "neutral";
@@ -190,7 +193,10 @@ export function AlertMenu({
)}
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" sideOffset={8} className={cn("w-[min(392px,calc(100vw-24px))]", menuSurfaceClassName)}>
<DropdownMenuContent align="end" sideOffset={8} className={cn("w-[min(392px,calc(100vw-24px))]", menuSurfaceClassName)}
onKeyDownCapture={() => { keyboardInteraction.current = true; }}
onPointerDownCapture={() => { keyboardInteraction.current = false; }}
>
<AlertQueueSummary
count={alerts.length}
latestAlertTime={latestAlertTime}
@@ -199,33 +205,42 @@ export function AlertMenu({
tone={hasAlerts ? alertSummaryTone : "success"}
/>
<MenuSectionLabel>待处理事项</MenuSectionLabel>
<div className="scheduled-feed-scroll max-h-[min(52dvh,420px)] overflow-y-auto pr-1">
{hasAlerts ? alerts.map((alert) => {
const tone = alert.severity === "warning" ? "warning" : "danger";
return (
<div
key={alert.id}
className={cn("surface-well my-1 px-3 pb-3 pt-1", MAP_COMPACT_RADIUS_CLASS_NAME)}
>
<div className="flex min-w-0 items-center justify-between gap-2">
<div className="scheduled-feed-scroll relative max-h-[min(52dvh,420px)] overflow-x-hidden overflow-y-auto pr-1">
<AnimatePresence initial={false} mode="popLayout" custom={skipRemovalMotion}>
{hasAlerts ? alerts.map((alert) => {
const tone = alert.severity === "warning" ? "warning" : "danger";
return (
<motion.div
key={alert.id}
data-alert-id={alert.id}
layout="position"
initial={false}
animate={{ opacity: 1, x: 0 }}
exit="removed"
variants={{ removed: (skip: boolean) => ({
opacity: 0, x: skip ? 0 : 12, pointerEvents: "none",
transition: { duration: skip ? 0 : 0.18, ease: [0.4, 0, 1, 1] }
}) }}
transition={{ duration: skipRemovalMotion ? 0 : 0.18, ease: [0.4, 0, 1, 1] }}
className={cn(
tone === "warning" ? "status-tone-warning" : "status-tone-danger",
"relative my-1 min-h-[88px] border border-transparent py-3 pl-3 pr-[76px] transition-colors hover:border-[var(--status-border)] hover:bg-[var(--action-soft)] focus-within:border-[var(--status-border)] focus-within:bg-[var(--action-soft)]",
MAP_COMPACT_RADIUS_CLASS_NAME,
menuReadableRowClassName
)}
>
<span className="flex min-w-0 items-center gap-2 text-xs text-[var(--ink-muted)]">
<StatusDot tone={tone} size="md" />
<span className="font-mono tabular-nums">{alert.time}</span>
</span>
<div className="flex shrink-0 items-center gap-0.5">
<DropdownMenuItem
aria-label={`打开通知:${alert.title}`}
className={cn("h-10 justify-center gap-1 px-2 text-xs font-medium text-[var(--action-blue)] active:scale-95", MAP_COMPACT_RADIUS_CLASS_NAME, menuReadableRowClassName)}
onSelect={() => onSelectAlert(alert)}
>
打开
<ChevronRight size={14} aria-hidden="true" />
</DropdownMenuItem>
<p className="mt-2 break-words text-sm font-semibold leading-5 text-slate-950">{alert.title}</p>
<p className="mt-1 break-words text-xs leading-5 text-[var(--ink-muted)]">{alert.description}</p>
<div className="absolute bottom-1 right-1 top-1 flex w-16 flex-col items-center justify-between">
{onRemoveAlert ? (
<DropdownMenuItem
aria-label={`移除通知:${alert.title}`}
title="移除通知"
className={cn("h-10 w-10 justify-center p-0 text-[var(--ink-muted)] active:scale-95", MAP_COMPACT_RADIUS_CLASS_NAME, menuReadableRowClassName)}
className={cn("h-10 w-10 shrink-0 justify-center p-0 text-[var(--ink-muted)] active:scale-95", MAP_COMPACT_RADIUS_CLASS_NAME, menuReadableRowClassName)}
onSelect={(event) => {
event.preventDefault();
onRemoveAlert(alert);
@@ -234,23 +249,29 @@ export function AlertMenu({
<X size={14} aria-hidden="true" />
</DropdownMenuItem>
) : null}
<DropdownMenuItem
aria-label={`打开通知:${alert.title}`}
title="打开通知"
className={cn("h-10 w-10 shrink-0 justify-center p-0 text-[var(--action-blue)] active:scale-95", MAP_COMPACT_RADIUS_CLASS_NAME, menuReadableRowClassName)}
onSelect={() => onSelectAlert(alert)}
>
<ChevronRight size={14} aria-hidden="true" />
</DropdownMenuItem>
</div>
</motion.div>
);
}) : (
<div key="empty-alert-queue" className={cn("surface-reading my-1 grid min-h-[76px] place-items-center border border-dashed border-[var(--surface-divider)] px-3 py-3 text-center", MAP_COMPACT_RADIUS_CLASS_NAME)}>
<div className="min-w-0">
<SemanticStatusIcon tone="success" size="md" className="mx-auto">
<CheckCircle2 size={16} />
</SemanticStatusIcon>
<p className="mt-1 text-xs font-semibold text-slate-700">当前无待处理事项</p>
<p className="mt-0.5 text-xs leading-4 text-[var(--ink-muted)]">新的待办和告警会进入这里。</p>
</div>
<p className="break-words text-sm font-semibold leading-5 text-slate-950">{alert.title}</p>
<p className="mt-1 break-words text-xs leading-5 text-[var(--ink-muted)]">{alert.description}</p>
</div>
);
}) : (
<div className={cn("surface-reading my-1 grid min-h-[76px] place-items-center border border-dashed border-[var(--surface-divider)] px-3 py-3 text-center", MAP_COMPACT_RADIUS_CLASS_NAME)}>
<div className="min-w-0">
<SemanticStatusIcon tone="success" size="md" className="mx-auto">
<CheckCircle2 size={16} />
</SemanticStatusIcon>
<p className="mt-1 text-xs font-semibold text-slate-700">当前无待处理事项</p>
<p className="mt-0.5 text-xs leading-4 text-[var(--ink-muted)]">新的待办和告警会进入这里。</p>
</div>
</div>
)}
)}
</AnimatePresence>
</div>
<div className="mt-2 grid grid-cols-[minmax(0,1fr)_auto] gap-2 border-t border-[var(--surface-divider)]/70 pt-2">
<MenuPlainButton