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, Bot,
CheckCircle2, CheckCircle2,
ChevronDown, ChevronDown,
ChevronRight,
Columns2, Columns2,
FileText, FileText,
Keyboard, Keyboard,
@@ -56,6 +55,11 @@ const menuSurfaceClassName = cn(
); );
const menuReadableRowClassName = const menuReadableRowClassName =
"surface-well focus:bg-[var(--action-soft)] focus:text-slate-950"; "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({ export function AnalysisViewMenu({
open, open,
onOpenChange, onOpenChange,
@@ -224,23 +228,34 @@ export function AlertMenu({
transition={{ duration: skipRemovalMotion ? 0 : 0.18, ease: [0.4, 0, 1, 1] }} transition={{ duration: skipRemovalMotion ? 0 : 0.18, ease: [0.4, 0, 1, 1] }}
className={cn( className={cn(
tone === "warning" ? "status-tone-warning" : "status-tone-danger", 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, MAP_COMPACT_RADIUS_CLASS_NAME,
menuReadableRowClassName menuReadableRowClassName
)} )}
> >
<span className="flex min-w-0 items-center gap-2 text-xs text-[var(--ink-muted)]"> <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}`}
/>
</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" /> <StatusDot tone={tone} size="md" />
<span className="font-mono tabular-nums">{alert.time}</span> <span className="font-mono tabular-nums">{alert.time}</span>
</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 pr-10 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> <p className="mt-0.5 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"> </div>
{onRemoveAlert ? ( {onRemoveAlert ? (
<DropdownMenuItem <DropdownMenuItem
aria-label={`移除通知:${alert.title}`} aria-label={`移除通知:${alert.title}`}
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)} className={cn(alertActionClassName, "absolute right-1 top-1 z-10")}
onSelect={(event) => { onSelect={(event) => {
event.preventDefault(); event.preventDefault();
onRemoveAlert(alert); onRemoveAlert(alert);
@@ -249,15 +264,6 @@ export function AlertMenu({
<X size={14} aria-hidden="true" /> <X size={14} aria-hidden="true" />
</DropdownMenuItem> </DropdownMenuItem>
) : null} ) : 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> </motion.div>
); );
}) : ( }) : (
+23 -6
View File
@@ -42,13 +42,30 @@ for (const { width, reducedMotion } of [{ width: 1280, reducedMotion: false }, {
await expect(removeTask).toBeVisible(); await expect(removeTask).toBeVisible();
const openTask = page.getByRole("menuitem", { name: `打开通知:${notification.title}` }); const openTask = page.getByRole("menuitem", { name: `打开通知:${notification.title}` });
await expect(openTask).toHaveText(""); await expect(openTask).toHaveText("");
const actions = await openTask.evaluate((element) => { const notificationRow = page.locator(`[data-alert-id="operational-task-notification:${notification.notification_id}"]`);
const open = element.getBoundingClientRect(); const titleBounds = await notificationRow.locator("p").first().boundingBox();
const remove = element.previousElementSibling!.getBoundingClientRect(); const rowBounds = await notificationRow.boundingBox();
return { centerDifference: Math.abs((open.left + open.width / 2) - (remove.left + remove.width / 2)), gap: open.top - remove.bottom }; 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(removePosition.top).toBeLessThanOrEqual(6);
expect(actions.gap).toBeGreaterThanOrEqual(0); expect(removePosition.right).toBeLessThanOrEqual(6);
const box = await menu.boundingBox(); const box = await menu.boundingBox();
expect(box!.x).toBeGreaterThanOrEqual(0); expect(box!.x).toBeGreaterThanOrEqual(0);
expect(box!.x + box!.width).toBeLessThanOrEqual(width); 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(page.getByRole("region", { name: "工况任务", exact: true }).getByRole("heading", { name: task.title })).toBeVisible();
await expect(popup).toBeVisible(); await expect(popup).toBeVisible();
await menu.click(); 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 expect(page.getByRole("region", { name: "工况任务", exact: true }).getByRole("heading", { name: task.title })).toBeVisible();
await emit(); await emit();
await expect(popup).toBeVisible(); await expect(popup).toBeVisible();