fix: align scale info with floating timeline
This commit is contained in:
@@ -32,6 +32,7 @@ import {
|
|||||||
} from "@/features/map/core";
|
} from "@/features/map/core";
|
||||||
import { env } from "@/shared/config/env";
|
import { env } from "@/shared/config/env";
|
||||||
import type { AccessTokenProvider } from "@/shared/auth/keycloak-auth";
|
import type { AccessTokenProvider } from "@/shared/auth/keycloak-auth";
|
||||||
|
import { cn } from "@/shared/ui/cn";
|
||||||
import { AgentTaskTicker } from "./components/agent-task-ticker";
|
import { AgentTaskTicker } from "./components/agent-task-ticker";
|
||||||
import { MapDevPanel } from "./components/map-dev-panel";
|
import { MapDevPanel } from "./components/map-dev-panel";
|
||||||
import { MobileWorkbenchSheet } from "./components/mobile-workbench-sheet";
|
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 { WorkbenchAgentPanels } from "./components/workbench-agent-panels";
|
||||||
import { OperationalTimeline } from "./operational-timeline/operational-timeline";
|
import { OperationalTimeline } from "./operational-timeline/operational-timeline";
|
||||||
import {
|
import {
|
||||||
|
canDockMapScaleBesideTimeline,
|
||||||
createOperationalEvents,
|
createOperationalEvents,
|
||||||
getOperationalEventSummary,
|
getOperationalEventSummary,
|
||||||
|
OPERATIONAL_TIMELINE_LAYOUT,
|
||||||
type OperationalEvent,
|
type OperationalEvent,
|
||||||
type OperationalTimelineMode
|
type OperationalTimelineMode
|
||||||
} from "./operational-timeline/operational-timeline-model";
|
} from "./operational-timeline/operational-timeline-model";
|
||||||
@@ -213,6 +216,11 @@ export function MapWorkbenchPage({
|
|||||||
() => getOperationalEventSummary(operationalEvents),
|
() => getOperationalEventSummary(operationalEvents),
|
||||||
[operationalEvents]
|
[operationalEvents]
|
||||||
);
|
);
|
||||||
|
const scaleInfoBesideTimeline = canDockMapScaleBesideTimeline(
|
||||||
|
businessWorkspaceWidth,
|
||||||
|
timelineMode,
|
||||||
|
timelineVisible
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTimelineMode((current) => {
|
setTimelineMode((current) => {
|
||||||
@@ -919,12 +927,8 @@ export function MapWorkbenchPage({
|
|||||||
!timelineVisible
|
!timelineVisible
|
||||||
? 0
|
? 0
|
||||||
: !isLargeScreen
|
: !isLargeScreen
|
||||||
? 48
|
? OPERATIONAL_TIMELINE_LAYOUT.mobileSummaryHeight
|
||||||
: timelineMode === "expanded"
|
: OPERATIONAL_TIMELINE_LAYOUT[timelineMode].height
|
||||||
? 240
|
|
||||||
: timelineMode === "summary"
|
|
||||||
? 52
|
|
||||||
: 72
|
|
||||||
}px`
|
}px`
|
||||||
} as CSSProperties
|
} as CSSProperties
|
||||||
}
|
}
|
||||||
@@ -971,6 +975,7 @@ export function MapWorkbenchPage({
|
|||||||
onCollapseArtifact={artifactWorkspace.collapseArtifact}
|
onCollapseArtifact={artifactWorkspace.collapseArtifact}
|
||||||
onDestroyArtifact={artifactWorkspace.destroyArtifact}
|
onDestroyArtifact={artifactWorkspace.destroyArtifact}
|
||||||
timelineMode={timelineMode}
|
timelineMode={timelineMode}
|
||||||
|
reserveMapScaleSpace={scaleInfoBesideTimeline}
|
||||||
timelineContent={(
|
timelineContent={(
|
||||||
timelineVisible ? <>
|
timelineVisible ? <>
|
||||||
<div className="hidden lg:block">
|
<div className="hidden lg:block">
|
||||||
@@ -1016,7 +1021,14 @@ export function MapWorkbenchPage({
|
|||||||
<div className="pointer-events-auto absolute right-16 top-3 hidden lg:block">
|
<div className="pointer-events-auto absolute right-16 top-3 hidden lg:block">
|
||||||
<ToolbarPanel {...toolbarPanelProps} />
|
<ToolbarPanel {...toolbarPanelProps} />
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute bottom-[calc(var(--workbench-timeline-height)+1rem)] right-0 flex flex-col items-end gap-2">
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute right-0 flex flex-col items-end gap-2",
|
||||||
|
scaleInfoBesideTimeline
|
||||||
|
? "bottom-3"
|
||||||
|
: "bottom-[calc(var(--workbench-timeline-height)+1rem)]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="flex items-end gap-2 pr-2">
|
<div className="flex items-end gap-2 pr-2">
|
||||||
<MapZoom mapRef={mapRef} mapReady={mapReady} onHome={fitNetworkBounds} />
|
<MapZoom mapRef={mapRef} mapReady={mapReady} onHome={fitNetworkBounds} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import type { ScheduledConditionItem } from "../types";
|
import type { ScheduledConditionItem } from "../types";
|
||||||
import { clusterOperationalEvents, createOperationalEvents } from "./operational-timeline-model";
|
import {
|
||||||
|
canDockMapScaleBesideTimeline,
|
||||||
|
clusterOperationalEvents,
|
||||||
|
createOperationalEvents
|
||||||
|
} from "./operational-timeline-model";
|
||||||
|
|
||||||
const workOrder: ScheduledConditionItem = {
|
const workOrder: ScheduledConditionItem = {
|
||||||
id: "work-1", kind: "work_order", code: "WO-1", scheduledAt: "2026-08-19T10:00:00.000Z",
|
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).toHaveLength(2);
|
||||||
expect(clusters.find((cluster) => cluster.lane === "plan")?.events).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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -30,6 +30,28 @@ export type OperationalEventCluster = {
|
|||||||
events: OperationalEvent[];
|
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[] {
|
export function createOperationalEvents(items: ScheduledConditionItem[]): OperationalEvent[] {
|
||||||
return items.flatMap((item) =>
|
return items.flatMap((item) =>
|
||||||
item.kind === "work_order" ? createWorkOrderEvents(item) : createConditionEvents(item)
|
item.kind === "work_order" ? createWorkOrderEvents(item) : createConditionEvents(item)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/shar
|
|||||||
import {
|
import {
|
||||||
clusterOperationalEvents,
|
clusterOperationalEvents,
|
||||||
getOperationalEventSummary,
|
getOperationalEventSummary,
|
||||||
|
OPERATIONAL_TIMELINE_LAYOUT,
|
||||||
type OperationalEvent,
|
type OperationalEvent,
|
||||||
type OperationalEventCluster,
|
type OperationalEventCluster,
|
||||||
type OperationalEventLane,
|
type OperationalEventLane,
|
||||||
@@ -54,7 +55,7 @@ export function OperationalTimeline({
|
|||||||
const clusters = clusterOperationalEvents(events, mobile ? 60 : 10);
|
const clusters = clusterOperationalEvents(events, mobile ? 60 : 10);
|
||||||
const selectedEvent = events.find((event) => event.id === selectedEventId) ?? null;
|
const selectedEvent = events.find((event) => event.id === selectedEventId) ?? null;
|
||||||
const cursorPosition = getDayPosition(cursorTime);
|
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) {
|
if (mode === "summary" && !mobile) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ArrowLeft, FileChartColumnIncreasing } from "lucide-react";
|
import { ArrowLeft, FileChartColumnIncreasing } from "lucide-react";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { cn } from "@/shared/ui/cn";
|
import { cn } from "@/shared/ui/cn";
|
||||||
|
import { OPERATIONAL_TIMELINE_LAYOUT } from "../operational-timeline/operational-timeline-model";
|
||||||
import { AnalysisArtifactWorkspace } from "./analysis-document-view";
|
import { AnalysisArtifactWorkspace } from "./analysis-document-view";
|
||||||
import { ArtifactPeekBar } from "./artifact-peek-bar";
|
import { ArtifactPeekBar } from "./artifact-peek-bar";
|
||||||
import type {
|
import type {
|
||||||
@@ -14,6 +15,7 @@ type WorkbenchMainFrameProps = {
|
|||||||
mapOverlay?: ReactNode;
|
mapOverlay?: ReactNode;
|
||||||
timelineContent: ReactNode;
|
timelineContent: ReactNode;
|
||||||
timelineMode: "compact" | "expanded" | "summary";
|
timelineMode: "compact" | "expanded" | "summary";
|
||||||
|
reserveMapScaleSpace: boolean;
|
||||||
activeArtifact: AnalysisArtifact | null;
|
activeArtifact: AnalysisArtifact | null;
|
||||||
pendingArtifact: AnalysisArtifact | null;
|
pendingArtifact: AnalysisArtifact | null;
|
||||||
surfaceMode: WorkbenchSurfaceMode;
|
surfaceMode: WorkbenchSurfaceMode;
|
||||||
@@ -30,6 +32,7 @@ export function WorkbenchMainFrame({
|
|||||||
mapOverlay,
|
mapOverlay,
|
||||||
timelineContent,
|
timelineContent,
|
||||||
timelineMode,
|
timelineMode,
|
||||||
|
reserveMapScaleSpace,
|
||||||
activeArtifact,
|
activeArtifact,
|
||||||
pendingArtifact,
|
pendingArtifact,
|
||||||
surfaceMode,
|
surfaceMode,
|
||||||
@@ -111,12 +114,14 @@ export function WorkbenchMainFrame({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className="pointer-events-auto absolute bottom-3 left-3 z-40 mx-auto min-h-0"
|
||||||
"pointer-events-auto absolute bottom-3 left-3 right-3 z-40 mx-auto min-h-0 w-[calc(100%-1.5rem)]",
|
style={{
|
||||||
timelineMode === "expanded" && "max-w-[1180px]",
|
maxWidth: OPERATIONAL_TIMELINE_LAYOUT[timelineMode].maxWidth,
|
||||||
timelineMode === "compact" && "max-w-[980px]",
|
right: reserveMapScaleSpace
|
||||||
timelineMode === "summary" && "max-w-[760px]"
|
? OPERATIONAL_TIMELINE_LAYOUT.mapScaleSafeWidth +
|
||||||
)}
|
OPERATIONAL_TIMELINE_LAYOUT.floatingGap
|
||||||
|
: 12
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{timelineContent}
|
{timelineContent}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user