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 -33
View File
@@ -1,6 +1,7 @@
import { expect, test, type Page } from "@playwright/test";
import { mockAgentApi } from "./support/mock-agent-api";
import { mockScadaApi } from "./support/mock-scada-api";
import { readRenderedContrast, readThemeColor } from "./support/theme-colors";
const EMPTY_RASTER_TILE = Buffer.from(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=",
@@ -23,6 +24,7 @@ test.describe("Main Frame workbench", () => {
await expectSolidMainFrameMaterials(page);
await expectLoadedChineseFont(page);
await expectBodyContrast(page);
await page.screenshot({ path: "test-results/palette/workbench-desktop-light.png", animations: "disabled" });
await expect(page).toHaveScreenshot("workbench-desktop-light.png", {
animations: "disabled",
maxDiffPixelRatio: 0.01
@@ -38,6 +40,7 @@ test.describe("Main Frame workbench", () => {
await expect(page.locator("main")).toHaveAttribute("data-basemap-tone", "satellite");
await expectDesktopMainFrameGeometry(page);
await expectSolidMainFrameMaterials(page);
await page.screenshot({ path: "test-results/palette/workbench-desktop-satellite.png", animations: "disabled" });
await expect(page).toHaveScreenshot("workbench-desktop-satellite.png", {
animations: "disabled",
maxDiffPixelRatio: 0.01
@@ -81,7 +84,7 @@ test.describe("Main Frame workbench", () => {
expect(await canvas.evaluate((element) => element.matches(":focus-visible"))).toBe(true);
await expect(canvas).toHaveCSS("outline-style", "solid");
await expect(canvas).toHaveCSS("outline-width", "1px");
await expect(canvas).toHaveCSS("outline-color", "rgba(37, 99, 235, 0.34)");
await expect(canvas).toHaveCSS("outline-color", await readThemeColor(canvas, "--action-focus-ring"));
await expect(canvas).toHaveCSS("outline-offset", "-1px");
await page.screenshot({ path: "test-results/agent-history-ui/map-pointer-focus.png" });
});
@@ -112,6 +115,7 @@ test.describe("Main Frame workbench", () => {
.poll(async () => Math.round((await workspace.boundingBox())?.width ?? 0))
.toBe(1920);
expect((await workspace.boundingBox())!.width).toBeGreaterThan(expandedWorkspace!.width);
await page.screenshot({ path: "test-results/palette/workbench-desktop-wide-max-agent.png", animations: "disabled" });
await expect(page).toHaveScreenshot("workbench-desktop-wide-max-agent.png", {
animations: "disabled",
maxDiffPixelRatio: 0.01
@@ -124,6 +128,7 @@ test.describe("Main Frame workbench", () => {
await page.setViewportSize({ width: 390, height: 844 });
await openWorkbench(page);
await page.screenshot({ path: "test-results/palette/workbench-mobile-launcher-light.png", animations: "disabled" });
await expect(page).toHaveScreenshot("workbench-mobile-launcher-light.png", {
animations: "disabled",
maxDiffPixelRatio: 0.01
@@ -138,6 +143,7 @@ test.describe("Main Frame workbench", () => {
const sheet = page.getByRole("region", { name: "Agent 工作台抽屉" });
await expect(sheet).toHaveAttribute("data-snap", "half");
await expect.poll(async () => Math.round((await sheet.boundingBox())?.height ?? 0)).toBe(439);
await page.screenshot({ path: "test-results/palette/workbench-mobile-agent-light.png", animations: "disabled" });
await expect(page).toHaveScreenshot("workbench-mobile-agent-light.png", {
animations: "disabled",
maxDiffPixelRatio: 0.01
@@ -153,6 +159,7 @@ test.describe("Main Frame workbench", () => {
const sheet = page.getByRole("region", { name: "工况任务抽屉" });
await expect(sheet).toHaveAttribute("data-snap", "half");
await expect(page.locator("main")).toHaveAttribute("data-basemap-tone", "satellite");
await page.screenshot({ path: "test-results/palette/workbench-mobile-condition-satellite.png", animations: "disabled" });
await expect(page).toHaveScreenshot("workbench-mobile-condition-satellite.png", {
animations: "disabled",
maxDiffPixelRatio: 0.01
@@ -308,7 +315,9 @@ async function expectSolidMainFrameMaterials(page: Page) {
};
});
expect(materials.agentBackdrop).toBe("none");
expect(materials.agentBackground).toBe("oklch(0.972 0.005 250)");
expect(materials.agentBackground).toBe(
await readThemeColor(page.getByRole("complementary", { name: "Agent 命令面板" }), "--agent-dock-surface")
);
expect(materials.agentShadow).toBe("none");
}
@@ -321,35 +330,6 @@ async function expectLoadedChineseFont(page: Page) {
}
async function expectBodyContrast(page: Page) {
const contrast = await page
.getByText("直接说出你想调查的问题,我会整理监测与空间证据,并将分析结果带回地图。")
.evaluate((element) => {
const foreground = parseColor(getComputedStyle(element).color);
const background = { r: 250, g: 252, b: 254 };
return contrastRatio(foreground, background);
function parseColor(color: string) {
const channels = color.match(/\d+(?:\.\d+)?/g)?.map(Number) ?? [0, 0, 0];
return { r: channels[0] ?? 0, g: channels[1] ?? 0, b: channels[2] ?? 0 };
}
function contrastRatio(
first: { r: number; g: number; b: number },
second: { r: number; g: number; b: number }
) {
const lighter = Math.max(luminance(first), luminance(second));
const darker = Math.min(luminance(first), luminance(second));
return (lighter + 0.05) / (darker + 0.05);
}
function luminance(color: { r: number; g: number; b: number }) {
const [r, g, b] = [color.r, color.g, color.b].map((channel) => {
const normalized = channel / 255;
return normalized <= 0.03928 ? normalized / 12.92 : ((normalized + 0.055) / 1.055) ** 2.4;
});
return 0.2126 * (r ?? 0) + 0.7152 * (g ?? 0) + 0.0722 * (b ?? 0);
}
});
expect(contrast).toBeGreaterThanOrEqual(4.5);
const body = page.getByText("直接说出你想调查的问题,我会整理监测与空间证据,并将分析结果带回地图。");
expect(await readRenderedContrast(body)).toBeGreaterThanOrEqual(4.5);
}