feat(ui): refine gem-blue acrylic palette and focus borders

Derive the gem-blue palette with OKLCH tonal and complementary-color
rules while preserving acrylic opacity, blur and surface hierarchy.

Darker focus tokens exposed stacked field rings, fallback outlines and
persistent basemap selection rings. Keep fallback focus styles in the
base layer and use one themed field boundary or inset card stroke.
Preserve keyboard focus, invalid states and native input semantics.

Add browser regression coverage for pointer and keyboard focus, all
basemap presentations, narrow layouts and rendered contrast.

Validation in an isolated checkout: pnpm lint, pnpm typecheck,
pnpm test (393 tests), focused browser suites and pnpm build.
This commit is contained in:
2026-10-09 10:57:06 +08:00
parent d2d705ed4c
commit 617ba4d87c
69 changed files with 1239 additions and 765 deletions
+13 -15
View File
@@ -1,6 +1,7 @@
import { expect, test, type Locator } from "@playwright/test";
import { mockAgentApi } from "./support/mock-agent-api";
import { mockWorkbenchContext } from "./support/mock-workbench-context";
import { expectThemeColor, readColorAlpha, readThemeColor } from "./support/theme-colors";
test.beforeEach(async ({ page }) => {
await mockWorkbenchContext(page);
@@ -53,18 +54,18 @@ test("desktop Agent docks beside the workspace with a solid reading surface", as
await waitForAnimations(agentPanel);
await expectAcrylicSurface(topBar);
await expect(agentPanel).toHaveCSS("background-color", "oklch(0.972 0.005 250)");
await expectThemeColor(agentPanel, "background-color", "--agent-dock-surface");
await expect(agentPanel).toHaveCSS("backdrop-filter", "none");
await expect(agentPanel).toHaveCSS("border-radius", "0px");
await expect(agentPanel).toHaveCSS("border-top-width", "0px");
await expect(agentPanel).toHaveCSS("border-bottom-width", "0px");
await expect(agentPanel).toHaveCSS("border-left-width", "0px");
await expect(agentPanel).toHaveCSS("border-right-width", "1px");
await expect(agentPanel).toHaveCSS("border-right-color", "oklch(0.905 0.009 250)");
await expectThemeColor(agentPanel, "border-right-color", "--surface-divider");
await expect(agentPanel).toHaveCSS("box-shadow", "none");
await expect(agentControls).toHaveCSS("border-top-width", "1px");
await expect(agentControls).toHaveCSS("background-color", "oklch(0.985 0.003 250)");
await expect(agentPanel.locator("form.agent-panel-control")).toHaveCSS("background-color", "rgb(255, 255, 255)");
await expectThemeColor(agentControls, "background-color", "--agent-dock-band");
await expectThemeColor(agentPanel.locator("form.agent-panel-control"), "background-color", "--surface-reading");
const capabilities = agentPanel.getByRole("list", { name: "Agent 分析能力" });
await expect(capabilities.getByRole("listitem")).toHaveCount(4);
await expect(capabilities).toHaveCSS("box-shadow", "none");
@@ -137,7 +138,7 @@ test("desktop Agent portal menus keep local material and respect reduced transpa
const menu = page.getByRole("menu");
await expect(menu).toHaveAttribute("data-agent-presentation", "desktop-dock");
expect(await menu.evaluate((element) => element.closest("aside"))).toBeNull();
await expect(menu).toHaveCSS("background-color", "rgba(255, 255, 255, 0.94)");
await expectThemeColor(menu, "background-color", "--agent-dock-overlay-surface");
await expect(menu).toHaveCSS("backdrop-filter", "blur(12px) saturate(1.03)");
await expect(menu).toHaveCSS("background-image", "none");
await page.keyboard.press("Escape");
@@ -151,12 +152,12 @@ test("desktop Agent portal menus keep local material and respect reduced transpa
expect(await page.evaluate(() => matchMedia("(prefers-reduced-transparency: reduce)").matches)).toBe(true);
await panel.getByRole("button", { name: "选择 Agent 模型" }).click();
const menu = page.getByRole("menu");
await expect(menu).toHaveCSS("background-color", "rgb(255, 255, 255)");
await expectThemeColor(menu, "background-color", "--palette-paper");
await expect(menu).toHaveCSS("backdrop-filter", "none");
await page.keyboard.press("Escape");
await panel.getByRole("button", { name: "打开 Agent 历史记录" }).click();
const history = panel.getByRole("region", { name: "Agent 历史记录" });
await expect(history).toHaveCSS("background-color", "rgb(255, 255, 255)");
await expectThemeColor(history, "background-color", "--palette-paper");
await expect(history).toHaveCSS("backdrop-filter", "none");
});
@@ -176,7 +177,7 @@ test("desktop Agent history remains usable inside the docked navigation pane", a
await waitForAnimations(history);
await expect(historyButton).toHaveAttribute("aria-expanded", "true");
await expectUnifiedHistoryStructure(history);
await expect(history).toHaveCSS("background-color", "rgba(255, 255, 255, 0.94)");
await expectThemeColor(history, "background-color", "--agent-dock-overlay-surface");
await expect(history).toHaveCSS("backdrop-filter", "blur(12px) saturate(1.03)");
await history.getByRole("button", { name: "重命名对话" }).first().click();
@@ -186,7 +187,7 @@ test("desktop Agent history remains usable inside the docked navigation pane", a
);
await expect(renameInput).toBeFocused();
await expect(renameInput).toHaveCSS("box-shadow", "none");
await expect(renameInput).toHaveCSS("border-color", "rgb(203, 213, 225)");
await expectThemeColor(renameInput, "border-color", "--action-blue-hover");
await expect(editingItem).toHaveCSS("border-color", "rgba(0, 0, 0, 0)");
await page.screenshot({ path: "test-results/agent-history-ui/desktop-rename.png" });
await history.getByRole("button", { name: "取消重命名" }).click();
@@ -414,6 +415,7 @@ test("long history titles keep rename and explicit delete usable across viewport
await history.getByRole("button", { name: "重命名对话", exact: true }).click();
const input = history.getByRole("textbox");
await expect(input).toBeFocused();
await expectThemeColor(input, "border-color", "--action-blue-hover");
await input.fill(`${originalTitle}(已复核)`);
await expect(history.getByRole("button", { name: "保存对话主题" })).toBeVisible();
await waitForAnimations(history);
@@ -632,9 +634,7 @@ async function expectTransparentExpandedHeader(header: Locator, agentSurface: Lo
boxShadow: style.boxShadow
};
}),
agentSurface.evaluate((element) =>
getComputedStyle(element).getPropertyValue("--acrylic-outline").trim()
)
readThemeColor(agentSurface, "--acrylic-outline")
]);
expect(getAlpha(presentation.backgroundColor)).toBe(0);
@@ -665,9 +665,7 @@ async function getSurfacePresentation(surface: Locator) {
}
function getAlpha(color: string) {
const match = color.match(/^rgba\([^,]+,[^,]+,[^,]+,\s*([\d.]+)\)$/);
return match ? Number(match[1]) : 1;
return readColorAlpha(color);
}
async function expectLightweightMaterialContext(context: Locator) {