+
-
{children}
@@ -205,14 +186,10 @@ function getClosestSnap(height: number, viewportHeight: number): MobileWorkbench
getMobileWorkbenchSheetHeight(closest, viewportHeight) - height
);
return candidateDistance < closestDistance ? candidate : closest;
- }, "peek");
+ }, "half");
}
function getSnapLabel(snap: MobileWorkbenchSheetSnap) {
- if (snap === "peek") {
- return "预览档";
- }
-
if (snap === "half") {
return "半屏档";
}
diff --git a/src/features/workbench/components/scheduled-condition-feed.tsx b/src/features/workbench/components/scheduled-condition-feed.tsx
index a0aea5f..89591c6 100644
--- a/src/features/workbench/components/scheduled-condition-feed.tsx
+++ b/src/features/workbench/components/scheduled-condition-feed.tsx
@@ -4,6 +4,7 @@ import {
ChevronRight,
ClipboardList,
History,
+ X,
XCircle
} from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
@@ -58,6 +59,7 @@ type ScheduledConditionFeedProps = {
onFocusRequestHandled?: (requestId: number) => void;
onSelectedConditionChange?: (conditionId: string | null) => void;
onExpandAgent: () => void;
+ onClose?: () => void;
onLoadHistorySession: (sessionId: string) => void | Promise
;
onSubmitPrompt: (prompt: string) => void | Promise;
};
@@ -79,6 +81,7 @@ export function ScheduledConditionFeed({
onFocusRequestHandled,
onSelectedConditionChange,
onExpandAgent,
+ onClose,
onLoadHistorySession,
onSubmitPrompt
}: ScheduledConditionFeedProps) {
@@ -325,6 +328,19 @@ export function ScheduledConditionFeed({
/>
{expanded ? "收起工况任务" : "展开工况任务"}
+ {mobileSheet && onClose ? (
+
+ ) : null}
(null);
const [mobileSheetSnap, setMobileSheetSnap] = useState
("half");
- const [mobileSheetClosing, setMobileSheetClosing] = useState(false);
const [agentCollapsedForCondition, setAgentCollapsedForCondition] = useState(false);
const prefersReducedMotion = useReducedMotion();
const [taskTickerVisible, setTaskTickerVisible] = useState(true);
@@ -149,7 +148,6 @@ export function MapWorkbenchPage() {
setViewportWidth(window.innerWidth);
if (mediaQuery.matches) {
setMobileSheet(null);
- setMobileSheetClosing(false);
}
};
@@ -193,24 +191,12 @@ export function MapWorkbenchPage() {
return;
}
- setMobileSheetClosing(false);
setMobileSheet("agent");
setMobileSheetSnap("half");
}
const closeMobileSheet = useCallback(() => {
- if (prefersReducedMotion) {
- setMobileSheet(null);
- setMobileSheetClosing(false);
- return;
- }
-
- setMobileSheetClosing(true);
- }, [prefersReducedMotion]);
-
- const completeMobileSheetClose = useCallback(() => {
setMobileSheet(null);
- setMobileSheetClosing(false);
}, []);
function handleConditionExpandedChange(nextExpanded: boolean) {
@@ -255,7 +241,6 @@ export function MapWorkbenchPage() {
}
setConditionFeedExpanded(false);
- setMobileSheetClosing(false);
setMobileSheetSnap("half");
setMobileSheet("condition");
}
@@ -577,7 +562,6 @@ export function MapWorkbenchPage() {
setConditionFeedVisible(true);
setActiveToolId(null);
if (!isLargeScreen) {
- setMobileSheetClosing(false);
setMobileSheet("condition");
setMobileSheetSnap("half");
}
@@ -998,17 +982,21 @@ export function MapWorkbenchPage() {
aria-label="打开 Agent 面板"
title="打开 Agent 面板"
onClick={openAgentPanelForViewport}
- className="acrylic-control grid h-12 w-12 shrink-0 place-items-center rounded-xl border text-violet-700 active:scale-95"
+ className="acrylic-control grid h-11 w-11 shrink-0 place-items-center rounded-xl border text-violet-700 active:scale-95"
>
-
+
-
+
@@ -1022,8 +1010,6 @@ export function MapWorkbenchPage() {
snap={mobileSheetSnap}
onSnapChange={setMobileSheetSnap}
onClose={closeMobileSheet}
- closing={mobileSheetClosing}
- onCloseComplete={completeMobileSheetClose}
>
{mobileSheet === "agent" ? (
diff --git a/tests/browser/mobile-workbench-sheet.e2e.ts b/tests/browser/mobile-workbench-sheet.e2e.ts
index 62bc0d6..72effe9 100644
--- a/tests/browser/mobile-workbench-sheet.e2e.ts
+++ b/tests/browser/mobile-workbench-sheet.e2e.ts
@@ -3,14 +3,40 @@ import { expect, test } from "playwright/test";
test.describe("mobile workbench sheet", () => {
test.use({ viewport: { width: 390, height: 844 } });
- test("supports all snap heights, keyboard control, and Escape close", async ({ page }) => {
+ test("keeps the bottom launcher controls at one height", async ({ page }) => {
+ await page.goto("/", { waitUntil: "domcontentloaded" });
+
+ const agentButton = page.getByRole("button", { name: "打开 Agent 面板" });
+ const mapToolbar = page.locator('nav[aria-label="地图工具"]:visible');
+ const conditionButton = page.getByRole("button", { name: "打开工况任务" });
+ const boxes = await Promise.all([
+ agentButton.boundingBox(),
+ mapToolbar.boundingBox(),
+ conditionButton.boundingBox()
+ ]);
+
+ boxes.forEach((box) => expect(box).not.toBeNull());
+ expect(boxes.map((box) => Math.round(box!.height))).toEqual([44, 44, 44]);
+ expect(boxes.map((box) => Math.round(box!.y))).toEqual([
+ Math.round(boxes[0]!.y),
+ Math.round(boxes[0]!.y),
+ Math.round(boxes[0]!.y)
+ ]);
+ });
+
+ test("supports half and full heights, keyboard control, and Escape close", async ({ page }) => {
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByRole("button", { name: "打开 Agent 面板" }).click();
const sheet = page.getByRole("region", { name: "Agent 工作台抽屉" });
const handle = page.getByRole("button", { name: /Agent 工作台抽屉高度/ });
+ const closeButton = page.getByRole("button", { name: "关闭 Agent 面板" });
await expect(sheet).toBeVisible();
+ await expect(closeButton).toHaveCount(1);
+ await expect.poll(async () => Math.round((await closeButton.boundingBox())?.height ?? 0)).toBe(
+ 40
+ );
await expect(sheet).toHaveAttribute("data-snap", "half");
await expect.poll(async () => Math.round((await sheet.boundingBox())?.height ?? 0)).toBe(439);
await expect
@@ -42,10 +68,12 @@ test.describe("mobile workbench sheet", () => {
await handle.focus();
await page.keyboard.press("Home");
- await expect(sheet).toHaveAttribute("data-snap", "peek");
- await expect.poll(async () => Math.round((await sheet.boundingBox())?.height ?? 0)).toBe(96);
+ await expect(sheet).toHaveAttribute("data-snap", "half");
+ await expect.poll(async () => Math.round((await sheet.boundingBox())?.height ?? 0)).toBe(439);
await page.keyboard.press("ArrowUp");
+ await expect(sheet).toHaveAttribute("data-snap", "full");
+ await page.keyboard.press("ArrowDown");
await expect(sheet).toHaveAttribute("data-snap", "half");
await page.keyboard.press("End");
await expect(sheet).toHaveAttribute("data-snap", "full");
@@ -62,11 +90,19 @@ test.describe("mobile workbench sheet", () => {
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByRole("button", { name: "打开 Agent 面板" }).click();
- await expect(page.getByRole("region", { name: "Agent 工作台抽屉" })).toBeVisible();
+ const agentSheet = page.getByRole("region", { name: "Agent 工作台抽屉" });
+ await expect(agentSheet).toBeVisible();
+ await expect.poll(async () => Math.round((await agentSheet.boundingBox())?.height ?? 0)).toBe(
+ 439
+ );
await page.keyboard.press("Escape");
await page.getByRole("button", { name: "打开工况任务" }).click();
- await expect(page.getByRole("region", { name: "工况任务抽屉" })).toBeVisible();
+ const conditionSheet = page.getByRole("region", { name: "工况任务抽屉" });
+ await expect(conditionSheet).toBeVisible();
+ await expect.poll(async () =>
+ Math.round((await conditionSheet.boundingBox())?.height ?? 0)
+ ).toBe(439);
await expect(
page.locator('aside[aria-label="Agent 命令面板"]').filter({ visible: true })
).toHaveCount(0);
@@ -78,8 +114,8 @@ test.describe("mobile workbench sheet", () => {
const sheet = page.getByRole("region", { name: "工况任务抽屉" });
await expect(sheet).toBeVisible();
- await page.getByRole("button", { name: "关闭工况任务抽屉" }).click();
- await expect(sheet).toBeHidden();
+ await page.getByRole("button", { name: "关闭工况任务" }).click();
+ expect(await sheet.count()).toBe(0);
await expect(page.getByRole("button", { name: "打开工况任务" })).toBeVisible();
});
diff --git a/tests/browser/workbench-visual.e2e.ts b/tests/browser/workbench-visual.e2e.ts
index 4c22697..0b0d9f9 100644
--- a/tests/browser/workbench-visual.e2e.ts
+++ b/tests/browser/workbench-visual.e2e.ts
@@ -70,6 +70,18 @@ test.describe("neutral blue mist workbench", () => {
});
});
+ test("mobile launcher keeps Agent, map tools, and conditions visually balanced", async ({
+ page
+ }) => {
+ await page.setViewportSize({ width: 390, height: 844 });
+ await openWorkbench(page);
+
+ await expect(page).toHaveScreenshot("workbench-mobile-launcher-light.png", {
+ animations: "disabled",
+ maxDiffPixelRatio: 0.01
+ });
+ });
+
test("mobile light basemap shows the Agent half sheet", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await openWorkbench(page);
diff --git a/tests/browser/workbench-visual.e2e.ts-snapshots/workbench-mobile-agent-light-chromium-linux.png b/tests/browser/workbench-visual.e2e.ts-snapshots/workbench-mobile-agent-light-chromium-linux.png
index 034b066..69290dc 100644
Binary files a/tests/browser/workbench-visual.e2e.ts-snapshots/workbench-mobile-agent-light-chromium-linux.png and b/tests/browser/workbench-visual.e2e.ts-snapshots/workbench-mobile-agent-light-chromium-linux.png differ
diff --git a/tests/browser/workbench-visual.e2e.ts-snapshots/workbench-mobile-condition-satellite-chromium-linux.png b/tests/browser/workbench-visual.e2e.ts-snapshots/workbench-mobile-condition-satellite-chromium-linux.png
index 14f9102..3b92104 100644
Binary files a/tests/browser/workbench-visual.e2e.ts-snapshots/workbench-mobile-condition-satellite-chromium-linux.png and b/tests/browser/workbench-visual.e2e.ts-snapshots/workbench-mobile-condition-satellite-chromium-linux.png differ
diff --git a/tests/browser/workbench-visual.e2e.ts-snapshots/workbench-mobile-launcher-light-chromium-linux.png b/tests/browser/workbench-visual.e2e.ts-snapshots/workbench-mobile-launcher-light-chromium-linux.png
new file mode 100644
index 0000000..db19fa9
Binary files /dev/null and b/tests/browser/workbench-visual.e2e.ts-snapshots/workbench-mobile-launcher-light-chromium-linux.png differ