fix: refine mobile workbench sheets
This commit is contained in:
@@ -2,7 +2,6 @@ import {
|
|||||||
Activity,
|
Activity,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
ChevronsUpDown,
|
ChevronsUpDown,
|
||||||
ChevronDown,
|
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
DatabaseZap,
|
DatabaseZap,
|
||||||
History,
|
History,
|
||||||
@@ -16,7 +15,8 @@ import {
|
|||||||
Shield,
|
Shield,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
Square,
|
Square,
|
||||||
Volume2
|
Volume2,
|
||||||
|
X
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { AnimatePresence, MotionConfig, motion, useReducedMotion } from "motion/react";
|
import { AnimatePresence, MotionConfig, motion, useReducedMotion } from "motion/react";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
||||||
@@ -292,19 +292,29 @@ export function AgentCommandPanel({
|
|||||||
>
|
>
|
||||||
<History size={17} aria-hidden="true" />
|
<History size={17} aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
{presentation === "mobile-sheet" ? (
|
||||||
type="button"
|
<button
|
||||||
aria-label={presentation === "mobile-sheet" ? "关闭 Agent 面板" : "折叠 Agent 面板"}
|
type="button"
|
||||||
title={presentation === "mobile-sheet" ? "关闭 Agent 面板" : "折叠 Agent 面板"}
|
aria-label="关闭 Agent 面板"
|
||||||
onClick={onCollapse}
|
title="关闭 Agent 面板"
|
||||||
className="agent-panel-icon-button grid h-9 w-9 shrink-0 place-items-center rounded-xl text-slate-600 transition"
|
onClick={onCollapse}
|
||||||
>
|
className="group grid h-10 w-10 shrink-0 place-items-center rounded-full text-slate-500"
|
||||||
{presentation === "mobile-sheet" ? (
|
>
|
||||||
<ChevronDown size={17} aria-hidden="true" />
|
<span className="surface-control grid h-9 w-9 place-items-center rounded-full shadow-[inset_0_0_0_1px_rgba(148,163,184,0.26),0_4px_12px_rgba(15,33,55,0.10)] transition-[color,background-color,scale] group-active:scale-95 [@media(hover:hover)]:group-hover:bg-blue-50 [@media(hover:hover)]:group-hover:text-blue-700">
|
||||||
) : (
|
<X size={17} strokeWidth={2.25} aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label="折叠 Agent 面板"
|
||||||
|
title="折叠 Agent 面板"
|
||||||
|
onClick={onCollapse}
|
||||||
|
className="agent-panel-icon-button grid h-9 w-9 shrink-0 place-items-center rounded-xl text-slate-600 transition"
|
||||||
|
>
|
||||||
<ChevronLeft size={17} aria-hidden="true" />
|
<ChevronLeft size={17} aria-hidden="true" />
|
||||||
)}
|
</button>
|
||||||
</button>
|
)}
|
||||||
</div>
|
</div>
|
||||||
<AnimatePresence initial={false}>
|
<AnimatePresence initial={false}>
|
||||||
{historyOpen ? (
|
{historyOpen ? (
|
||||||
|
|||||||
@@ -1,21 +1,17 @@
|
|||||||
import { WORKBENCH_LAYOUT } from "../layout/workbench-layout";
|
import { WORKBENCH_LAYOUT } from "../layout/workbench-layout";
|
||||||
|
|
||||||
export type MobileWorkbenchSheetSnap = "peek" | "half" | "full";
|
export type MobileWorkbenchSheetSnap = "half" | "full";
|
||||||
|
|
||||||
export function getMobileWorkbenchSheetHeight(
|
export function getMobileWorkbenchSheetHeight(
|
||||||
snap: MobileWorkbenchSheetSnap,
|
snap: MobileWorkbenchSheetSnap,
|
||||||
viewportHeight: number
|
viewportHeight: number
|
||||||
) {
|
) {
|
||||||
if (snap === "peek") {
|
|
||||||
return WORKBENCH_LAYOUT.mobileSheet.peekHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (snap === "half") {
|
if (snap === "half") {
|
||||||
return Math.round(viewportHeight * WORKBENCH_LAYOUT.mobileSheet.halfViewportRatio);
|
return Math.round(viewportHeight * WORKBENCH_LAYOUT.mobileSheet.halfViewportRatio);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Math.max(
|
return Math.max(
|
||||||
WORKBENCH_LAYOUT.mobileSheet.peekHeight,
|
Math.round(viewportHeight * WORKBENCH_LAYOUT.mobileSheet.halfViewportRatio),
|
||||||
viewportHeight - WORKBENCH_LAYOUT.mobileSheet.fullTopGap
|
viewportHeight - WORKBENCH_LAYOUT.mobileSheet.fullTopGap
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ import { describe, expect, it } from "vitest";
|
|||||||
import { getMobileWorkbenchSheetHeight } from "./mobile-workbench-sheet-layout";
|
import { getMobileWorkbenchSheetHeight } from "./mobile-workbench-sheet-layout";
|
||||||
|
|
||||||
describe("mobile workbench sheet heights", () => {
|
describe("mobile workbench sheet heights", () => {
|
||||||
it("keeps the three deterministic snap heights", () => {
|
it("keeps the two deterministic snap heights", () => {
|
||||||
expect(getMobileWorkbenchSheetHeight("peek", 844)).toBe(96);
|
|
||||||
expect(getMobileWorkbenchSheetHeight("half", 844)).toBe(439);
|
expect(getMobileWorkbenchSheetHeight("half", 844)).toBe(439);
|
||||||
expect(getMobileWorkbenchSheetHeight("full", 844)).toBe(776);
|
expect(getMobileWorkbenchSheetHeight("full", 844)).toBe(776);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
type PointerEvent,
|
type PointerEvent,
|
||||||
type ReactNode
|
type ReactNode
|
||||||
} from "react";
|
} from "react";
|
||||||
import { X } from "lucide-react";
|
|
||||||
import { cn } from "@/shared/ui/cn";
|
import { cn } from "@/shared/ui/cn";
|
||||||
import {
|
import {
|
||||||
getMobileWorkbenchSheetHeight,
|
getMobileWorkbenchSheetHeight,
|
||||||
@@ -22,11 +21,9 @@ type MobileWorkbenchSheetProps = {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
closing?: boolean;
|
|
||||||
onCloseComplete?: () => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const SNAP_ORDER: MobileWorkbenchSheetSnap[] = ["peek", "half", "full"];
|
const SNAP_ORDER: MobileWorkbenchSheetSnap[] = ["half", "full"];
|
||||||
|
|
||||||
export function MobileWorkbenchSheet({
|
export function MobileWorkbenchSheet({
|
||||||
label,
|
label,
|
||||||
@@ -34,9 +31,7 @@ export function MobileWorkbenchSheet({
|
|||||||
onSnapChange,
|
onSnapChange,
|
||||||
onClose,
|
onClose,
|
||||||
children,
|
children,
|
||||||
className,
|
className
|
||||||
closing = false,
|
|
||||||
onCloseComplete
|
|
||||||
}: MobileWorkbenchSheetProps) {
|
}: MobileWorkbenchSheetProps) {
|
||||||
const pointerStartRef = useRef<{ y: number; height: number } | null>(null);
|
const pointerStartRef = useRef<{ y: number; height: number } | null>(null);
|
||||||
const draggedRef = useRef(false);
|
const draggedRef = useRef(false);
|
||||||
@@ -87,7 +82,7 @@ export function MobileWorkbenchSheet({
|
|||||||
draggedRef.current = true;
|
draggedRef.current = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const minHeight = getMobileWorkbenchSheetHeight("peek", window.innerHeight);
|
const minHeight = getMobileWorkbenchSheetHeight("half", window.innerHeight);
|
||||||
const maxHeight = getMobileWorkbenchSheetHeight("full", window.innerHeight);
|
const maxHeight = getMobileWorkbenchSheetHeight("full", window.innerHeight);
|
||||||
setDragHeight(Math.min(maxHeight, Math.max(minHeight, start.height + delta)));
|
setDragHeight(Math.min(maxHeight, Math.max(minHeight, start.height + delta)));
|
||||||
}
|
}
|
||||||
@@ -134,7 +129,7 @@ export function MobileWorkbenchSheet({
|
|||||||
|
|
||||||
if (event.key === "Home") {
|
if (event.key === "Home") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
onSnapChange("peek");
|
onSnapChange("half");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,17 +145,11 @@ export function MobileWorkbenchSheet({
|
|||||||
data-snap={snap}
|
data-snap={snap}
|
||||||
className={cn(
|
className={cn(
|
||||||
"mobile-workbench-sheet acrylic-panel pointer-events-auto absolute bottom-0 left-0 right-0 z-40 flex min-h-0 flex-col overflow-hidden rounded-t-[20px] border-x border-t transition-[height] duration-200 ease-[cubic-bezier(0.16,1,0.3,1)] motion-reduce:transition-none lg:hidden",
|
"mobile-workbench-sheet acrylic-panel pointer-events-auto absolute bottom-0 left-0 right-0 z-40 flex min-h-0 flex-col overflow-hidden rounded-t-[20px] border-x border-t transition-[height] duration-200 ease-[cubic-bezier(0.16,1,0.3,1)] motion-reduce:transition-none lg:hidden",
|
||||||
closing && "pointer-events-none",
|
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
style={{ height: closing ? 0 : (dragHeight ?? restingHeight) }}
|
style={{ height: dragHeight ?? restingHeight }}
|
||||||
onTransitionEnd={(event) => {
|
|
||||||
if (closing && event.propertyName === "height" && event.target === event.currentTarget) {
|
|
||||||
onCloseComplete?.();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div className="relative flex h-7 shrink-0 items-center">
|
<div className="flex h-7 shrink-0 items-center">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={`${label}高度,当前${getSnapLabel(snap)}`}
|
aria-label={`${label}高度,当前${getSnapLabel(snap)}`}
|
||||||
@@ -173,14 +162,6 @@ export function MobileWorkbenchSheet({
|
|||||||
>
|
>
|
||||||
<span className="h-1 w-10 rounded-full bg-slate-400/70 transition-[width,background-color] group-focus-visible:w-12 group-focus-visible:bg-blue-600" />
|
<span className="h-1 w-10 rounded-full bg-slate-400/70 transition-[width,background-color] group-focus-visible:w-12 group-focus-visible:bg-blue-600" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
aria-label={`关闭${label}`}
|
|
||||||
className="absolute right-2 grid h-7 w-7 place-items-center rounded-lg text-slate-500 transition hover:bg-blue-50 hover:text-blue-700"
|
|
||||||
onClick={onClose}
|
|
||||||
>
|
|
||||||
<X size={15} aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="min-h-0 flex-1 overscroll-contain">{children}</div>
|
<div className="min-h-0 flex-1 overscroll-contain">{children}</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -205,14 +186,10 @@ function getClosestSnap(height: number, viewportHeight: number): MobileWorkbench
|
|||||||
getMobileWorkbenchSheetHeight(closest, viewportHeight) - height
|
getMobileWorkbenchSheetHeight(closest, viewportHeight) - height
|
||||||
);
|
);
|
||||||
return candidateDistance < closestDistance ? candidate : closest;
|
return candidateDistance < closestDistance ? candidate : closest;
|
||||||
}, "peek");
|
}, "half");
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSnapLabel(snap: MobileWorkbenchSheetSnap) {
|
function getSnapLabel(snap: MobileWorkbenchSheetSnap) {
|
||||||
if (snap === "peek") {
|
|
||||||
return "预览档";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (snap === "half") {
|
if (snap === "half") {
|
||||||
return "半屏档";
|
return "半屏档";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
ChevronRight,
|
ChevronRight,
|
||||||
ClipboardList,
|
ClipboardList,
|
||||||
History,
|
History,
|
||||||
|
X,
|
||||||
XCircle
|
XCircle
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { AnimatePresence, motion } from "motion/react";
|
import { AnimatePresence, motion } from "motion/react";
|
||||||
@@ -58,6 +59,7 @@ type ScheduledConditionFeedProps = {
|
|||||||
onFocusRequestHandled?: (requestId: number) => void;
|
onFocusRequestHandled?: (requestId: number) => void;
|
||||||
onSelectedConditionChange?: (conditionId: string | null) => void;
|
onSelectedConditionChange?: (conditionId: string | null) => void;
|
||||||
onExpandAgent: () => void;
|
onExpandAgent: () => void;
|
||||||
|
onClose?: () => void;
|
||||||
onLoadHistorySession: (sessionId: string) => void | Promise<void>;
|
onLoadHistorySession: (sessionId: string) => void | Promise<void>;
|
||||||
onSubmitPrompt: (prompt: string) => void | Promise<void>;
|
onSubmitPrompt: (prompt: string) => void | Promise<void>;
|
||||||
};
|
};
|
||||||
@@ -79,6 +81,7 @@ export function ScheduledConditionFeed({
|
|||||||
onFocusRequestHandled,
|
onFocusRequestHandled,
|
||||||
onSelectedConditionChange,
|
onSelectedConditionChange,
|
||||||
onExpandAgent,
|
onExpandAgent,
|
||||||
|
onClose,
|
||||||
onLoadHistorySession,
|
onLoadHistorySession,
|
||||||
onSubmitPrompt
|
onSubmitPrompt
|
||||||
}: ScheduledConditionFeedProps) {
|
}: ScheduledConditionFeedProps) {
|
||||||
@@ -325,6 +328,19 @@ export function ScheduledConditionFeed({
|
|||||||
/>
|
/>
|
||||||
<span className="sr-only">{expanded ? "收起工况任务" : "展开工况任务"}</span>
|
<span className="sr-only">{expanded ? "收起工况任务" : "展开工况任务"}</span>
|
||||||
</button>
|
</button>
|
||||||
|
{mobileSheet && onClose ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label="关闭工况任务"
|
||||||
|
title="关闭工况任务"
|
||||||
|
onClick={onClose}
|
||||||
|
className="group grid h-10 w-10 shrink-0 place-items-center rounded-full text-slate-500"
|
||||||
|
>
|
||||||
|
<span className="surface-control grid h-9 w-9 place-items-center rounded-full shadow-[inset_0_0_0_1px_rgba(148,163,184,0.26),0_4px_12px_rgba(15,33,55,0.10)] transition-[color,background-color,scale] group-active:scale-95 [@media(hover:hover)]:group-hover:bg-blue-50 [@media(hover:hover)]:group-hover:text-blue-700">
|
||||||
|
<X size={17} strokeWidth={2.25} aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ export const WORKBENCH_LAYOUT = {
|
|||||||
tickerWidth: 460
|
tickerWidth: 460
|
||||||
},
|
},
|
||||||
mobileSheet: {
|
mobileSheet: {
|
||||||
peekHeight: 96,
|
|
||||||
halfViewportRatio: 0.52,
|
halfViewportRatio: 0.52,
|
||||||
fullTopGap: 68
|
fullTopGap: 68
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,7 +107,6 @@ export function MapWorkbenchPage() {
|
|||||||
const [conditionFeedExpanded, setConditionFeedExpanded] = useState(false);
|
const [conditionFeedExpanded, setConditionFeedExpanded] = useState(false);
|
||||||
const [mobileSheet, setMobileSheet] = useState<"agent" | "condition" | null>(null);
|
const [mobileSheet, setMobileSheet] = useState<"agent" | "condition" | null>(null);
|
||||||
const [mobileSheetSnap, setMobileSheetSnap] = useState<MobileWorkbenchSheetSnap>("half");
|
const [mobileSheetSnap, setMobileSheetSnap] = useState<MobileWorkbenchSheetSnap>("half");
|
||||||
const [mobileSheetClosing, setMobileSheetClosing] = useState(false);
|
|
||||||
const [agentCollapsedForCondition, setAgentCollapsedForCondition] = useState(false);
|
const [agentCollapsedForCondition, setAgentCollapsedForCondition] = useState(false);
|
||||||
const prefersReducedMotion = useReducedMotion();
|
const prefersReducedMotion = useReducedMotion();
|
||||||
const [taskTickerVisible, setTaskTickerVisible] = useState(true);
|
const [taskTickerVisible, setTaskTickerVisible] = useState(true);
|
||||||
@@ -149,7 +148,6 @@ export function MapWorkbenchPage() {
|
|||||||
setViewportWidth(window.innerWidth);
|
setViewportWidth(window.innerWidth);
|
||||||
if (mediaQuery.matches) {
|
if (mediaQuery.matches) {
|
||||||
setMobileSheet(null);
|
setMobileSheet(null);
|
||||||
setMobileSheetClosing(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -193,24 +191,12 @@ export function MapWorkbenchPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setMobileSheetClosing(false);
|
|
||||||
setMobileSheet("agent");
|
setMobileSheet("agent");
|
||||||
setMobileSheetSnap("half");
|
setMobileSheetSnap("half");
|
||||||
}
|
}
|
||||||
|
|
||||||
const closeMobileSheet = useCallback(() => {
|
const closeMobileSheet = useCallback(() => {
|
||||||
if (prefersReducedMotion) {
|
|
||||||
setMobileSheet(null);
|
|
||||||
setMobileSheetClosing(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setMobileSheetClosing(true);
|
|
||||||
}, [prefersReducedMotion]);
|
|
||||||
|
|
||||||
const completeMobileSheetClose = useCallback(() => {
|
|
||||||
setMobileSheet(null);
|
setMobileSheet(null);
|
||||||
setMobileSheetClosing(false);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
function handleConditionExpandedChange(nextExpanded: boolean) {
|
function handleConditionExpandedChange(nextExpanded: boolean) {
|
||||||
@@ -255,7 +241,6 @@ export function MapWorkbenchPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setConditionFeedExpanded(false);
|
setConditionFeedExpanded(false);
|
||||||
setMobileSheetClosing(false);
|
|
||||||
setMobileSheetSnap("half");
|
setMobileSheetSnap("half");
|
||||||
setMobileSheet("condition");
|
setMobileSheet("condition");
|
||||||
}
|
}
|
||||||
@@ -577,7 +562,6 @@ export function MapWorkbenchPage() {
|
|||||||
setConditionFeedVisible(true);
|
setConditionFeedVisible(true);
|
||||||
setActiveToolId(null);
|
setActiveToolId(null);
|
||||||
if (!isLargeScreen) {
|
if (!isLargeScreen) {
|
||||||
setMobileSheetClosing(false);
|
|
||||||
setMobileSheet("condition");
|
setMobileSheet("condition");
|
||||||
setMobileSheetSnap("half");
|
setMobileSheetSnap("half");
|
||||||
}
|
}
|
||||||
@@ -998,17 +982,21 @@ export function MapWorkbenchPage() {
|
|||||||
aria-label="打开 Agent 面板"
|
aria-label="打开 Agent 面板"
|
||||||
title="打开 Agent 面板"
|
title="打开 Agent 面板"
|
||||||
onClick={openAgentPanelForViewport}
|
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"
|
||||||
>
|
>
|
||||||
<AgentPersona className="pointer-events-none h-9 w-9" state={agent.personaState} />
|
<AgentPersona className="pointer-events-none h-8 w-8" state={agent.personaState} />
|
||||||
</button>
|
</button>
|
||||||
<MapToolbar items={toolbarItems} orientation="horizontal" className="justify-center" />
|
<MapToolbar
|
||||||
|
items={toolbarItems}
|
||||||
|
orientation="horizontal"
|
||||||
|
className="h-11 justify-center"
|
||||||
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="打开工况任务"
|
aria-label="打开工况任务"
|
||||||
title="打开工况任务"
|
title="打开工况任务"
|
||||||
onClick={toggleConditionFeedForViewport}
|
onClick={toggleConditionFeedForViewport}
|
||||||
className="acrylic-control grid h-12 w-12 shrink-0 place-items-center rounded-xl border text-blue-700 active:scale-95"
|
className="acrylic-control grid h-11 w-11 shrink-0 place-items-center rounded-xl border text-blue-700 active:scale-95"
|
||||||
>
|
>
|
||||||
<CalendarClock size={20} aria-hidden="true" />
|
<CalendarClock size={20} aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
@@ -1022,8 +1010,6 @@ export function MapWorkbenchPage() {
|
|||||||
snap={mobileSheetSnap}
|
snap={mobileSheetSnap}
|
||||||
onSnapChange={setMobileSheetSnap}
|
onSnapChange={setMobileSheetSnap}
|
||||||
onClose={closeMobileSheet}
|
onClose={closeMobileSheet}
|
||||||
closing={mobileSheetClosing}
|
|
||||||
onCloseComplete={completeMobileSheetClose}
|
|
||||||
>
|
>
|
||||||
{mobileSheet === "agent" ? (
|
{mobileSheet === "agent" ? (
|
||||||
<AgentCommandPanel
|
<AgentCommandPanel
|
||||||
@@ -1044,6 +1030,7 @@ export function MapWorkbenchPage() {
|
|||||||
onFocusRequestHandled={handleConditionFocusRequestHandled}
|
onFocusRequestHandled={handleConditionFocusRequestHandled}
|
||||||
onSelectedConditionChange={setSelectedConditionId}
|
onSelectedConditionChange={setSelectedConditionId}
|
||||||
onExpandAgent={openAgentPanelForViewport}
|
onExpandAgent={openAgentPanelForViewport}
|
||||||
|
onClose={closeMobileSheet}
|
||||||
onLoadHistorySession={handleLoadAgentHistorySession}
|
onLoadHistorySession={handleLoadAgentHistorySession}
|
||||||
onSubmitPrompt={agent.submitPrompt}
|
onSubmitPrompt={agent.submitPrompt}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,14 +3,40 @@ import { expect, test } from "playwright/test";
|
|||||||
test.describe("mobile workbench sheet", () => {
|
test.describe("mobile workbench sheet", () => {
|
||||||
test.use({ viewport: { width: 390, height: 844 } });
|
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.goto("/", { waitUntil: "domcontentloaded" });
|
||||||
await page.getByRole("button", { name: "打开 Agent 面板" }).click();
|
await page.getByRole("button", { name: "打开 Agent 面板" }).click();
|
||||||
|
|
||||||
const sheet = page.getByRole("region", { name: "Agent 工作台抽屉" });
|
const sheet = page.getByRole("region", { name: "Agent 工作台抽屉" });
|
||||||
const handle = page.getByRole("button", { name: /Agent 工作台抽屉高度/ });
|
const handle = page.getByRole("button", { name: /Agent 工作台抽屉高度/ });
|
||||||
|
const closeButton = page.getByRole("button", { name: "关闭 Agent 面板" });
|
||||||
|
|
||||||
await expect(sheet).toBeVisible();
|
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(sheet).toHaveAttribute("data-snap", "half");
|
||||||
await expect.poll(async () => Math.round((await sheet.boundingBox())?.height ?? 0)).toBe(439);
|
await expect.poll(async () => Math.round((await sheet.boundingBox())?.height ?? 0)).toBe(439);
|
||||||
await expect
|
await expect
|
||||||
@@ -42,10 +68,12 @@ test.describe("mobile workbench sheet", () => {
|
|||||||
|
|
||||||
await handle.focus();
|
await handle.focus();
|
||||||
await page.keyboard.press("Home");
|
await page.keyboard.press("Home");
|
||||||
await expect(sheet).toHaveAttribute("data-snap", "peek");
|
await expect(sheet).toHaveAttribute("data-snap", "half");
|
||||||
await expect.poll(async () => Math.round((await sheet.boundingBox())?.height ?? 0)).toBe(96);
|
await expect.poll(async () => Math.round((await sheet.boundingBox())?.height ?? 0)).toBe(439);
|
||||||
|
|
||||||
await page.keyboard.press("ArrowUp");
|
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 expect(sheet).toHaveAttribute("data-snap", "half");
|
||||||
await page.keyboard.press("End");
|
await page.keyboard.press("End");
|
||||||
await expect(sheet).toHaveAttribute("data-snap", "full");
|
await expect(sheet).toHaveAttribute("data-snap", "full");
|
||||||
@@ -62,11 +90,19 @@ test.describe("mobile workbench sheet", () => {
|
|||||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||||
|
|
||||||
await page.getByRole("button", { name: "打开 Agent 面板" }).click();
|
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.keyboard.press("Escape");
|
||||||
|
|
||||||
await page.getByRole("button", { name: "打开工况任务" }).click();
|
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(
|
await expect(
|
||||||
page.locator('aside[aria-label="Agent 命令面板"]').filter({ visible: true })
|
page.locator('aside[aria-label="Agent 命令面板"]').filter({ visible: true })
|
||||||
).toHaveCount(0);
|
).toHaveCount(0);
|
||||||
@@ -78,8 +114,8 @@ test.describe("mobile workbench sheet", () => {
|
|||||||
|
|
||||||
const sheet = page.getByRole("region", { name: "工况任务抽屉" });
|
const sheet = page.getByRole("region", { name: "工况任务抽屉" });
|
||||||
await expect(sheet).toBeVisible();
|
await expect(sheet).toBeVisible();
|
||||||
await page.getByRole("button", { name: "关闭工况任务抽屉" }).click();
|
await page.getByRole("button", { name: "关闭工况任务" }).click();
|
||||||
await expect(sheet).toBeHidden();
|
expect(await sheet.count()).toBe(0);
|
||||||
await expect(page.getByRole("button", { name: "打开工况任务" })).toBeVisible();
|
await expect(page.getByRole("button", { name: "打开工况任务" })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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 }) => {
|
test("mobile light basemap shows the Agent half sheet", async ({ page }) => {
|
||||||
await page.setViewportSize({ width: 390, height: 844 });
|
await page.setViewportSize({ width: 390, height: 844 });
|
||||||
await openWorkbench(page);
|
await openWorkbench(page);
|
||||||
|
|||||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 141 KiB |
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 96 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
Reference in New Issue
Block a user