+
{
).toEqual({ top: 72, right: 952, bottom: 32, left: 476 });
});
+ it("does not reserve map width for the collapsed Agent launcher", () => {
+ expect(
+ getWorkbenchCameraPadding(1440, {
+ agentOpen: false,
+ conditionOpen: false
+ }).left
+ ).toBe(24);
+ });
+
it("uses the committed Agent width without exceeding the hard limit", () => {
expect(
getWorkbenchCameraPadding(1920, {
diff --git a/src/features/workbench/layout/workbench-layout.ts b/src/features/workbench/layout/workbench-layout.ts
index 097e222..d3af352 100644
--- a/src/features/workbench/layout/workbench-layout.ts
+++ b/src/features/workbench/layout/workbench-layout.ts
@@ -3,7 +3,7 @@ export const WORKBENCH_LAYOUT = {
desktopMinWidth: 1024,
persistentConditionMinWidth: 1280,
wideMinWidth: 1536,
- collapsedAgentWidth: 72,
+ collapsedAgentWidth: 0,
maxAgentWidth: 720,
agentPanelLeftInset: 12,
conditionPanelRightInset: 64,
diff --git a/src/features/workbench/map-workbench-page.tsx b/src/features/workbench/map-workbench-page.tsx
index 383ea7d..ee5bdf2 100644
--- a/src/features/workbench/map-workbench-page.tsx
+++ b/src/features/workbench/map-workbench-page.tsx
@@ -32,7 +32,6 @@ import {
} from "@/features/map/core";
import { env } from "@/shared/config/env";
import type { AccessTokenProvider } from "@/shared/auth/keycloak-auth";
-import { cn } from "@/shared/ui/cn";
import { AgentTaskTicker } from "./components/agent-task-ticker";
import { MapDevPanel } from "./components/map-dev-panel";
import { MobileWorkbenchSheet } from "./components/mobile-workbench-sheet";
@@ -41,10 +40,10 @@ import { ScheduledConditionFeed } from "./components/scheduled-condition-feed";
import { WorkbenchAgentPanels } from "./components/workbench-agent-panels";
import { OperationalTimeline } from "./operational-timeline/operational-timeline";
import {
- canDockMapScaleBesideTimeline,
createOperationalEvents,
getOperationalEventSummary,
OPERATIONAL_TIMELINE_LAYOUT,
+ resolveTimelineScaleLayout,
type OperationalEvent,
type OperationalTimelineMode
} from "./operational-timeline/operational-timeline-model";
@@ -216,11 +215,14 @@ export function MapWorkbenchPage({
() => getOperationalEventSummary(operationalEvents),
[operationalEvents]
);
- const scaleInfoBesideTimeline = canDockMapScaleBesideTimeline(
+ const timelineScaleLayout = resolveTimelineScaleLayout(
businessWorkspaceWidth,
timelineMode,
timelineVisible
);
+ const timelineBottom = isLargeScreen
+ ? timelineScaleLayout.timelineBottom
+ : 12;
useEffect(() => {
setTimelineMode((current) => {
@@ -927,8 +929,8 @@ export function MapWorkbenchPage({
!timelineVisible
? 0
: !isLargeScreen
- ? OPERATIONAL_TIMELINE_LAYOUT.mobileSummaryHeight
- : OPERATIONAL_TIMELINE_LAYOUT[timelineMode].height
+ ? OPERATIONAL_TIMELINE_LAYOUT.mobileSummaryHeight + timelineBottom
+ : OPERATIONAL_TIMELINE_LAYOUT[timelineMode].height + timelineBottom
}px`
} as CSSProperties
}
@@ -975,7 +977,7 @@ export function MapWorkbenchPage({
onCollapseArtifact={artifactWorkspace.collapseArtifact}
onDestroyArtifact={artifactWorkspace.destroyArtifact}
timelineMode={timelineMode}
- reserveMapScaleSpace={scaleInfoBesideTimeline}
+ timelineBottom={timelineBottom}
timelineContent={(
timelineVisible ? <>
@@ -1022,17 +1024,17 @@ export function MapWorkbenchPage({
)}
diff --git a/src/features/workbench/operational-timeline/operational-timeline-model.test.ts b/src/features/workbench/operational-timeline/operational-timeline-model.test.ts
index 69c1d8a..4159bb2 100644
--- a/src/features/workbench/operational-timeline/operational-timeline-model.test.ts
+++ b/src/features/workbench/operational-timeline/operational-timeline-model.test.ts
@@ -1,9 +1,10 @@
import { describe, expect, it } from "vitest";
import type { ScheduledConditionItem } from "../types";
import {
- canDockMapScaleBesideTimeline,
+ canCenterTimelineBesideMapScale,
clusterOperationalEvents,
- createOperationalEvents
+ createOperationalEvents,
+ resolveTimelineScaleLayout
} from "./operational-timeline-model";
const workOrder: ScheduledConditionItem = {
@@ -44,9 +45,20 @@ describe("operational timeline model", () => {
expect(clusters.find((cluster) => cluster.lane === "plan")?.events).toHaveLength(2);
});
- it("docks scale info beside a floating timeline only when the business area is wide enough", () => {
- expect(canDockMapScaleBesideTimeline(2048, "expanded", true)).toBe(true);
- expect(canDockMapScaleBesideTimeline(1380, "expanded", true)).toBe(false);
- expect(canDockMapScaleBesideTimeline(1380, "expanded", false)).toBe(true);
+ it("keeps a centered timeline beside scale info only when both side margins are wide enough", () => {
+ expect(canCenterTimelineBesideMapScale(2560, "expanded", true)).toBe(true);
+ expect(canCenterTimelineBesideMapScale(2048, "expanded", true)).toBe(false);
+ expect(canCenterTimelineBesideMapScale(1380, "expanded", false)).toBe(true);
+ });
+
+ it("keeps scale info in the map corner and moves the timeline when horizontal space is tight", () => {
+ expect(resolveTimelineScaleLayout(2560, "expanded", true)).toEqual({
+ scaleBottom: 0,
+ timelineBottom: 24
+ });
+ expect(resolveTimelineScaleLayout(2048, "expanded", true)).toEqual({
+ scaleBottom: 0,
+ timelineBottom: 56
+ });
});
});
diff --git a/src/features/workbench/operational-timeline/operational-timeline-model.ts b/src/features/workbench/operational-timeline/operational-timeline-model.ts
index c6e3dab..034441e 100644
--- a/src/features/workbench/operational-timeline/operational-timeline-model.ts
+++ b/src/features/workbench/operational-timeline/operational-timeline-model.ts
@@ -31,15 +31,22 @@ export type OperationalEventCluster = {
};
export const OPERATIONAL_TIMELINE_LAYOUT = {
- compact: { height: 72, maxWidth: 980 },
+ compact: { height: 88, maxWidth: 980 },
expanded: { height: 240, maxWidth: 1180 },
summary: { height: 52, maxWidth: 760 },
mobileSummaryHeight: 48,
mapScaleSafeWidth: 520,
- floatingGap: 48
+ floatingGap: 48,
+ edgeInset: 24,
+ scaleCollisionOffset: 56
} as const;
-export function canDockMapScaleBesideTimeline(
+export type TimelineScaleLayout = {
+ scaleBottom: number;
+ timelineBottom: number;
+};
+
+export function canCenterTimelineBesideMapScale(
businessWorkspaceWidth: number,
mode: OperationalTimelineMode,
visible: boolean
@@ -48,8 +55,30 @@ export function canDockMapScaleBesideTimeline(
const layout = OPERATIONAL_TIMELINE_LAYOUT[mode];
return businessWorkspaceWidth >=
layout.maxWidth +
- OPERATIONAL_TIMELINE_LAYOUT.mapScaleSafeWidth +
- OPERATIONAL_TIMELINE_LAYOUT.floatingGap;
+ 2 * (
+ OPERATIONAL_TIMELINE_LAYOUT.mapScaleSafeWidth +
+ OPERATIONAL_TIMELINE_LAYOUT.floatingGap
+ );
+}
+
+export function resolveTimelineScaleLayout(
+ businessWorkspaceWidth: number,
+ mode: OperationalTimelineMode,
+ visible: boolean
+): TimelineScaleLayout {
+ const scaleClearsCenteredTimeline = canCenterTimelineBesideMapScale(
+ businessWorkspaceWidth,
+ mode,
+ visible
+ );
+
+ return {
+ scaleBottom: 0,
+ timelineBottom:
+ visible && !scaleClearsCenteredTimeline
+ ? OPERATIONAL_TIMELINE_LAYOUT.scaleCollisionOffset
+ : OPERATIONAL_TIMELINE_LAYOUT.edgeInset
+ };
}
export function createOperationalEvents(items: ScheduledConditionItem[]): OperationalEvent[] {
diff --git a/src/features/workbench/operational-timeline/operational-timeline.tsx b/src/features/workbench/operational-timeline/operational-timeline.tsx
index 3238863..9abc7b6 100644
--- a/src/features/workbench/operational-timeline/operational-timeline.tsx
+++ b/src/features/workbench/operational-timeline/operational-timeline.tsx
@@ -1,13 +1,14 @@
import {
CalendarClock,
Check,
- ChevronDown,
- ChevronUp,
Clock3,
OctagonAlert,
+ PanelBottomClose,
+ PanelBottomOpen,
RotateCcw,
TriangleAlert
} from "lucide-react";
+import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { cn } from "@/shared/ui/cn";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/shared/ui/tooltip";
import {
@@ -51,18 +52,24 @@ export function OperationalTimeline({
onClearSelection,
onReturnNow
}: OperationalTimelineProps) {
+ const prefersReducedMotion = useReducedMotion();
const summary = getOperationalEventSummary(events);
const clusters = clusterOperationalEvents(events, mobile ? 60 : 10);
const selectedEvent = events.find((event) => event.id === selectedEventId) ?? null;
const cursorPosition = getDayPosition(cursorTime);
const height = mobile ? "100%" : OPERATIONAL_TIMELINE_LAYOUT[mode].height;
+ const sizeTransition = prefersReducedMotion
+ ? { duration: 0 }
+ : { duration: 0.3, ease: [0.22, 1, 0.36, 1] as const };
if (mode === "summary" && !mobile) {
return (
-
{formatDate(date)}
@@ -80,13 +87,13 @@ export function OperationalTimeline({
分析视图保持独立时间范围
)}
-
+
);
}
return (
-
@@ -124,8 +133,16 @@ export function OperationalTimeline({
- {mode === "expanded" || mobile ? (
-
+
+ {mode === "expanded" || mobile ? (
+
{LANES.map((lane) => (
@@ -150,11 +167,18 @@ export function OperationalTimeline({
-
+
) : (
-
+
-
+
{clusters.map((cluster, index) => (
-
+
)}
-
+
+
);
}
@@ -316,26 +341,35 @@ function TimelineActions({
onReturnNow: () => void;
}) {
return (
-
+
{onModeChange ? (
-
+ <>
+
+
+ >
) : null}
);
diff --git a/src/features/workbench/workspace/workbench-main-frame.tsx b/src/features/workbench/workspace/workbench-main-frame.tsx
index 6704a6d..1a42461 100644
--- a/src/features/workbench/workspace/workbench-main-frame.tsx
+++ b/src/features/workbench/workspace/workbench-main-frame.tsx
@@ -1,4 +1,5 @@
import { ArrowLeft, FileChartColumnIncreasing } from "lucide-react";
+import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import type { ReactNode } from "react";
import { cn } from "@/shared/ui/cn";
import { OPERATIONAL_TIMELINE_LAYOUT } from "../operational-timeline/operational-timeline-model";
@@ -15,7 +16,7 @@ type WorkbenchMainFrameProps = {
mapOverlay?: ReactNode;
timelineContent: ReactNode;
timelineMode: "compact" | "expanded" | "summary";
- reserveMapScaleSpace: boolean;
+ timelineBottom: number;
activeArtifact: AnalysisArtifact | null;
pendingArtifact: AnalysisArtifact | null;
surfaceMode: WorkbenchSurfaceMode;
@@ -32,7 +33,7 @@ export function WorkbenchMainFrame({
mapOverlay,
timelineContent,
timelineMode,
- reserveMapScaleSpace,
+ timelineBottom,
activeArtifact,
pendingArtifact,
surfaceMode,
@@ -43,8 +44,12 @@ export function WorkbenchMainFrame({
onCollapseArtifact,
onDestroyArtifact
}: WorkbenchMainFrameProps) {
+ const prefersReducedMotion = useReducedMotion();
const showMap = surfaceMode !== "focus";
const showArtifact = activeArtifact && surfaceMode !== "map_only";
+ const timelineTransition = prefersReducedMotion
+ ? { duration: 0 }
+ : { duration: 0.28, ease: [0.22, 1, 0.36, 1] as const };
return (
-
- {timelineContent}
-
+
+ {timelineContent ? (
+
+ {timelineContent}
+
+ ) : null}
+
+
);
}