style(workbench): refine notification cards and hit targets

This commit is contained in:
2026-11-07 20:04:28 +08:00
parent ea3b62573c
commit e0436217a8
3 changed files with 64 additions and 36 deletions
@@ -5,7 +5,6 @@ import {
Bot,
CheckCircle2,
ChevronDown,
ChevronRight,
Columns2,
FileText,
Keyboard,
@@ -56,6 +55,11 @@ const menuSurfaceClassName = cn(
);
const menuReadableRowClassName =
"surface-well focus:bg-[var(--action-soft)] focus:text-slate-950";
const alertActionClassName = cn(
MAP_COMPACT_RADIUS_CLASS_NAME,
"h-10 w-10 shrink-0 justify-center border border-transparent bg-transparent p-0 text-[var(--ink-muted)] transition-colors hover:border-[var(--action-blue)] hover:bg-[var(--action-blue)] hover:text-white focus:border-[var(--action-blue)] focus:bg-[var(--action-blue)] focus:text-white data-[highlighted]:border-[var(--action-blue)] data-[highlighted]:bg-[var(--action-blue)] data-[highlighted]:text-white active:scale-95"
);
export function AnalysisViewMenu({
open,
onOpenChange,
@@ -224,40 +228,42 @@ export function AlertMenu({
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)]",
"relative my-1 min-h-[76px] border border-transparent px-3 py-2 transition-colors hover:border-[var(--action-border-hover)] hover:bg-[var(--action-selection-soft)] focus-within:border-[var(--action-border-hover)] focus-within:bg-[var(--action-selection-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>
<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 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);
}}
>
<X size={14} aria-hidden="true" />
</DropdownMenuItem>
) : null}
<DropdownMenuItem
<DropdownMenuItem
asChild
className="absolute inset-0 z-0 h-full w-full cursor-pointer rounded-[inherit] border-0 bg-transparent p-0 hover:bg-transparent focus:bg-transparent focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--action-focus-ring)]"
onSelect={() => onSelectAlert(alert)}
>
<button
type="button"
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>
/>
</DropdownMenuItem>
<div className="pointer-events-none relative">
<span className="flex min-w-0 items-center gap-2 pr-10 text-xs text-[var(--ink-muted)]">
<StatusDot tone={tone} size="md" />
<span className="font-mono tabular-nums">{alert.time}</span>
</span>
<p className="mt-1 break-words pr-10 text-sm font-semibold leading-5 text-slate-950">{alert.title}</p>
<p className="mt-0.5 break-words text-xs leading-5 text-[var(--ink-muted)]">{alert.description}</p>
</div>
{onRemoveAlert ? (
<DropdownMenuItem
aria-label={`移除通知:${alert.title}`}
title="移除通知"
className={cn(alertActionClassName, "absolute right-1 top-1 z-10")}
onSelect={(event) => {
event.preventDefault();
onRemoveAlert(alert);
}}
>
<X size={14} aria-hidden="true" />
</DropdownMenuItem>
) : null}
</motion.div>
);
}) : (
+23 -6
View File
@@ -42,13 +42,30 @@ for (const { width, reducedMotion } of [{ width: 1280, reducedMotion: false }, {
await expect(removeTask).toBeVisible();
const openTask = page.getByRole("menuitem", { name: `打开通知:${notification.title}` });
await expect(openTask).toHaveText("");
const actions = await openTask.evaluate((element) => {
const open = element.getBoundingClientRect();
const remove = element.previousElementSibling!.getBoundingClientRect();
return { centerDifference: Math.abs((open.left + open.width / 2) - (remove.left + remove.width / 2)), gap: open.top - remove.bottom };
const notificationRow = page.locator(`[data-alert-id="operational-task-notification:${notification.notification_id}"]`);
const titleBounds = await notificationRow.locator("p").first().boundingBox();
const rowBounds = await notificationRow.boundingBox();
expect(rowBounds!.height).toBeLessThanOrEqual(82);
const hit = await page.evaluate(({ x, y }) => document.elementFromPoint(x, y)?.closest("button")?.getAttribute("aria-label"),
{ x: titleBounds!.x + titleBounds!.width / 2, y: titleBounds!.y + titleBounds!.height / 2 });
expect(hit).toBe(`打开通知:${notification.title}`);
await openTask.hover();
await expect(notificationRow).toHaveCSS("background-color", "rgb(218, 229, 253)");
await expect(notificationRow).toHaveCSS("border-color", "rgb(66, 99, 199)");
for (const action of [removeTask]) {
await action.hover();
await expect(action).toHaveCSS("background-color", "rgb(66, 99, 199)");
await expect(action).toHaveCSS("color", "rgb(255, 255, 255)");
}
await testInfo.attach(`alert-hover-${width}`, { body: await page.screenshot({ path: testInfo.outputPath("alert-hover.png"), style: "[data-sonner-toaster] { visibility: hidden !important; }" }), contentType: "image/png" });
await page.mouse.move(0, 0);
const removePosition = await removeTask.evaluate((element) => {
const remove = element.getBoundingClientRect();
const row = element.closest("[data-alert-id]")!.getBoundingClientRect();
return { top: remove.top - row.top, right: row.right - remove.right };
});
expect(actions.centerDifference).toBeLessThan(1);
expect(actions.gap).toBeGreaterThanOrEqual(0);
expect(removePosition.top).toBeLessThanOrEqual(6);
expect(removePosition.right).toBeLessThanOrEqual(6);
const box = await menu.boundingBox();
expect(box!.x).toBeGreaterThanOrEqual(0);
expect(box!.x + box!.width).toBeLessThanOrEqual(width);
+6 -1
View File
@@ -93,7 +93,12 @@ for (const width of [1280, 375, 320]) {
await expect(page.getByRole("region", { name: "工况任务", exact: true }).getByRole("heading", { name: task.title })).toBeVisible();
await expect(popup).toBeVisible();
await menu.click();
await page.getByRole("menuitem", { name: `打开通知:${alert.title}` }).click();
const alertRow = page.locator(`[data-alert-id="${alert.id}"]`);
const titleBox = await alertRow.locator("p").first().boundingBox();
const rowBox = await alertRow.boundingBox();
await alertRow.click({ position: { x: titleBox!.x - rowBox!.x + titleBox!.width / 2,
y: titleBox!.y - rowBox!.y + titleBox!.height / 2 } });
await expect(page.getByRole("menu")).toHaveCount(0);
await expect(page.getByRole("region", { name: "工况任务", exact: true }).getByRole("heading", { name: task.title })).toBeVisible();
await emit();
await expect(popup).toBeVisible();