diff --git a/src/features/workbench/map-workbench-page.tsx b/src/features/workbench/map-workbench-page.tsx index a9a0921..383ea7d 100644 --- a/src/features/workbench/map-workbench-page.tsx +++ b/src/features/workbench/map-workbench-page.tsx @@ -32,6 +32,7 @@ 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"; @@ -40,8 +41,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, type OperationalEvent, type OperationalTimelineMode } from "./operational-timeline/operational-timeline-model"; @@ -213,6 +216,11 @@ export function MapWorkbenchPage({ () => getOperationalEventSummary(operationalEvents), [operationalEvents] ); + const scaleInfoBesideTimeline = canDockMapScaleBesideTimeline( + businessWorkspaceWidth, + timelineMode, + timelineVisible + ); useEffect(() => { setTimelineMode((current) => { @@ -919,12 +927,8 @@ export function MapWorkbenchPage({ !timelineVisible ? 0 : !isLargeScreen - ? 48 - : timelineMode === "expanded" - ? 240 - : timelineMode === "summary" - ? 52 - : 72 + ? OPERATIONAL_TIMELINE_LAYOUT.mobileSummaryHeight + : OPERATIONAL_TIMELINE_LAYOUT[timelineMode].height }px` } as CSSProperties } @@ -971,6 +975,7 @@ export function MapWorkbenchPage({ onCollapseArtifact={artifactWorkspace.collapseArtifact} onDestroyArtifact={artifactWorkspace.destroyArtifact} timelineMode={timelineMode} + reserveMapScaleSpace={scaleInfoBesideTimeline} timelineContent={( timelineVisible ? <>
@@ -1016,7 +1021,14 @@ 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 ca0c165..69c1d8a 100644 --- a/src/features/workbench/operational-timeline/operational-timeline-model.test.ts +++ b/src/features/workbench/operational-timeline/operational-timeline-model.test.ts @@ -1,6 +1,10 @@ import { describe, expect, it } from "vitest"; import type { ScheduledConditionItem } from "../types"; -import { clusterOperationalEvents, createOperationalEvents } from "./operational-timeline-model"; +import { + canDockMapScaleBesideTimeline, + clusterOperationalEvents, + createOperationalEvents +} from "./operational-timeline-model"; const workOrder: ScheduledConditionItem = { id: "work-1", kind: "work_order", code: "WO-1", scheduledAt: "2026-08-19T10:00:00.000Z", @@ -39,4 +43,10 @@ describe("operational timeline model", () => { expect(clusters).toHaveLength(2); 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); + }); }); diff --git a/src/features/workbench/operational-timeline/operational-timeline-model.ts b/src/features/workbench/operational-timeline/operational-timeline-model.ts index b7d8882..c6e3dab 100644 --- a/src/features/workbench/operational-timeline/operational-timeline-model.ts +++ b/src/features/workbench/operational-timeline/operational-timeline-model.ts @@ -30,6 +30,28 @@ export type OperationalEventCluster = { events: OperationalEvent[]; }; +export const OPERATIONAL_TIMELINE_LAYOUT = { + compact: { height: 72, maxWidth: 980 }, + expanded: { height: 240, maxWidth: 1180 }, + summary: { height: 52, maxWidth: 760 }, + mobileSummaryHeight: 48, + mapScaleSafeWidth: 520, + floatingGap: 48 +} as const; + +export function canDockMapScaleBesideTimeline( + businessWorkspaceWidth: number, + mode: OperationalTimelineMode, + visible: boolean +) { + if (!visible) return true; + const layout = OPERATIONAL_TIMELINE_LAYOUT[mode]; + return businessWorkspaceWidth >= + layout.maxWidth + + OPERATIONAL_TIMELINE_LAYOUT.mapScaleSafeWidth + + OPERATIONAL_TIMELINE_LAYOUT.floatingGap; +} + export function createOperationalEvents(items: ScheduledConditionItem[]): OperationalEvent[] { return items.flatMap((item) => item.kind === "work_order" ? createWorkOrderEvents(item) : createConditionEvents(item) diff --git a/src/features/workbench/operational-timeline/operational-timeline.tsx b/src/features/workbench/operational-timeline/operational-timeline.tsx index 0f6f321..3238863 100644 --- a/src/features/workbench/operational-timeline/operational-timeline.tsx +++ b/src/features/workbench/operational-timeline/operational-timeline.tsx @@ -13,6 +13,7 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/shar import { clusterOperationalEvents, getOperationalEventSummary, + OPERATIONAL_TIMELINE_LAYOUT, type OperationalEvent, type OperationalEventCluster, type OperationalEventLane, @@ -54,7 +55,7 @@ export function OperationalTimeline({ const clusters = clusterOperationalEvents(events, mobile ? 60 : 10); const selectedEvent = events.find((event) => event.id === selectedEventId) ?? null; const cursorPosition = getDayPosition(cursorTime); - const height = mobile ? "100%" : mode === "expanded" ? 240 : mode === "summary" ? 52 : 72; + const height = mobile ? "100%" : OPERATIONAL_TIMELINE_LAYOUT[mode].height; if (mode === "summary" && !mobile) { return ( diff --git a/src/features/workbench/workspace/workbench-main-frame.tsx b/src/features/workbench/workspace/workbench-main-frame.tsx index f2fb886..6704a6d 100644 --- a/src/features/workbench/workspace/workbench-main-frame.tsx +++ b/src/features/workbench/workspace/workbench-main-frame.tsx @@ -1,6 +1,7 @@ import { ArrowLeft, FileChartColumnIncreasing } from "lucide-react"; import type { ReactNode } from "react"; import { cn } from "@/shared/ui/cn"; +import { OPERATIONAL_TIMELINE_LAYOUT } from "../operational-timeline/operational-timeline-model"; import { AnalysisArtifactWorkspace } from "./analysis-document-view"; import { ArtifactPeekBar } from "./artifact-peek-bar"; import type { @@ -14,6 +15,7 @@ type WorkbenchMainFrameProps = { mapOverlay?: ReactNode; timelineContent: ReactNode; timelineMode: "compact" | "expanded" | "summary"; + reserveMapScaleSpace: boolean; activeArtifact: AnalysisArtifact | null; pendingArtifact: AnalysisArtifact | null; surfaceMode: WorkbenchSurfaceMode; @@ -30,6 +32,7 @@ export function WorkbenchMainFrame({ mapOverlay, timelineContent, timelineMode, + reserveMapScaleSpace, activeArtifact, pendingArtifact, surfaceMode, @@ -111,12 +114,14 @@ export function WorkbenchMainFrame({
{timelineContent}