fix: coordinate mobile workbench overlays
Independent fixed offsets caused the timeline, map dock, tool panel, and zoom controls to overlap. Derive them from one layout function and scope collapse to the expanded timeline state.
This commit is contained in:
@@ -5,7 +5,8 @@ import {
|
||||
getAgentPanelMaxWidth,
|
||||
getWorkbenchBasemapTone,
|
||||
getWorkbenchCameraPadding,
|
||||
getWorkbenchViewportLayout
|
||||
getWorkbenchViewportLayout,
|
||||
resolveMobileWorkbenchOverlayLayout
|
||||
} from "./workbench-layout";
|
||||
|
||||
describe("workbench basemap surface tone", () => {
|
||||
@@ -20,6 +21,19 @@ describe("workbench basemap surface tone", () => {
|
||||
});
|
||||
|
||||
describe("workbench floating panels", () => {
|
||||
it("derives mobile dock and map control offsets from one overlay stack", () => {
|
||||
expect(resolveMobileWorkbenchOverlayLayout(true)).toEqual({
|
||||
timelineBottom: 12,
|
||||
dockBottom: 68,
|
||||
mapControlBottom: 120
|
||||
});
|
||||
expect(resolveMobileWorkbenchOverlayLayout(false)).toEqual({
|
||||
timelineBottom: 12,
|
||||
dockBottom: 12,
|
||||
mapControlBottom: 64
|
||||
});
|
||||
});
|
||||
|
||||
it("uses bounded desktop and wide panel widths", () => {
|
||||
expect(getWorkbenchViewportLayout(1440)).toMatchObject({
|
||||
agentWidth: 500,
|
||||
|
||||
@@ -26,9 +26,29 @@ export const WORKBENCH_LAYOUT = {
|
||||
mobileSheet: {
|
||||
halfViewportRatio: 0.52,
|
||||
fullTopGap: 68
|
||||
},
|
||||
mobileOverlay: {
|
||||
edgeInset: 12,
|
||||
gap: 8,
|
||||
dockHeight: 44,
|
||||
timelineSummaryHeight: 48
|
||||
}
|
||||
} as const;
|
||||
|
||||
export function resolveMobileWorkbenchOverlayLayout(timelineVisible: boolean) {
|
||||
const { edgeInset, gap, dockHeight, timelineSummaryHeight } =
|
||||
WORKBENCH_LAYOUT.mobileOverlay;
|
||||
const dockBottom = timelineVisible
|
||||
? edgeInset + timelineSummaryHeight + gap
|
||||
: edgeInset;
|
||||
|
||||
return {
|
||||
timelineBottom: edgeInset,
|
||||
dockBottom,
|
||||
mapControlBottom: dockBottom + dockHeight + gap
|
||||
};
|
||||
}
|
||||
|
||||
export function clampAgentPanelWidth(
|
||||
width: number,
|
||||
viewportWidth: number,
|
||||
|
||||
@@ -78,7 +78,8 @@ import {
|
||||
import {
|
||||
WORKBENCH_LAYOUT,
|
||||
getWorkbenchBasemapTone,
|
||||
getWorkbenchLayoutCssVariables
|
||||
getWorkbenchLayoutCssVariables,
|
||||
resolveMobileWorkbenchOverlayLayout
|
||||
} from "./layout/workbench-layout";
|
||||
import { getResponsiveWorkbenchPadding } from "./map/camera";
|
||||
import { exportMapViewImage } from "./map/export-view";
|
||||
@@ -220,9 +221,10 @@ export function MapWorkbenchPage({
|
||||
timelineMode,
|
||||
timelineVisible
|
||||
);
|
||||
const mobileOverlayLayout = resolveMobileWorkbenchOverlayLayout(timelineVisible);
|
||||
const timelineBottom = isLargeScreen
|
||||
? timelineScaleLayout.timelineBottom
|
||||
: 12;
|
||||
: mobileOverlayLayout.timelineBottom;
|
||||
|
||||
useEffect(() => {
|
||||
setTimelineMode((current) => {
|
||||
@@ -987,19 +989,30 @@ export function MapWorkbenchPage({
|
||||
onReturnNow={handleReturnOperationalNow}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={openTimelineForViewport}
|
||||
className="acrylic-control flex h-12 w-full items-center gap-2 rounded-2xl border px-3 text-left text-xs text-slate-700 lg:hidden"
|
||||
<div
|
||||
role="region"
|
||||
aria-label="移动端运行时间轴摘要"
|
||||
className="acrylic-control flex h-12 w-full items-center rounded-2xl border p-1 text-xs text-slate-700 lg:hidden"
|
||||
>
|
||||
<Clock3 size={15} className="text-blue-600" aria-hidden="true" />
|
||||
<span className="font-semibold">运行时间轴</span>
|
||||
<span className="text-slate-500">计划 {operationalSummary.planned}</span>
|
||||
<span className="text-slate-500">执行 {operationalSummary.active}</span>
|
||||
<span className="text-amber-700">关注 {operationalSummary.attention}</span>
|
||||
<span className="text-rose-700">异常 {operationalSummary.exceptions}</span>
|
||||
<span className="ml-auto text-slate-500">查看全天</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="打开完整运行时间轴"
|
||||
onClick={openTimelineForViewport}
|
||||
className="flex h-10 min-w-0 flex-1 items-center gap-1.5 rounded-xl px-2 text-left transition-colors hover:bg-white/55 active:bg-white/75"
|
||||
>
|
||||
<Clock3 size={15} className="shrink-0 text-blue-600" aria-hidden="true" />
|
||||
<span className="shrink-0 font-semibold">运行时间轴</span>
|
||||
<span className="flex min-w-0 items-center gap-1.5 whitespace-nowrap text-[10px]">
|
||||
<span className="text-slate-500">计划 {operationalSummary.planned}</span>
|
||||
<span className="text-slate-500">执行 {operationalSummary.active}</span>
|
||||
<span className="text-amber-700">关注 {operationalSummary.attention}</span>
|
||||
<span className="text-rose-700">异常 {operationalSummary.exceptions}</span>
|
||||
</span>
|
||||
<span className="ml-auto hidden shrink-0 text-slate-500 min-[360px]:inline">
|
||||
查看全天
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</> : null
|
||||
)}
|
||||
mapContent={(
|
||||
@@ -1019,10 +1032,13 @@ export function MapWorkbenchPage({
|
||||
<ToolbarPanel {...toolbarPanelProps} />
|
||||
</div>
|
||||
<div
|
||||
className="absolute bottom-0 right-0 flex flex-col items-end gap-2"
|
||||
style={{ bottom: timelineScaleLayout.scaleBottom }}
|
||||
className="absolute right-3 flex flex-col items-end gap-2 bottom-[var(--mobile-map-control-bottom)] transition-[bottom] duration-200 ease-out motion-reduce:transition-none lg:right-0 lg:bottom-[var(--desktop-map-scale-bottom)]"
|
||||
style={{
|
||||
"--mobile-map-control-bottom": `${mobileOverlayLayout.mapControlBottom}px`,
|
||||
"--desktop-map-scale-bottom": `${timelineScaleLayout.scaleBottom}px`
|
||||
} as CSSProperties}
|
||||
>
|
||||
<div className="flex items-end gap-2 pr-2">
|
||||
<div className="flex items-end gap-2 lg:pr-2">
|
||||
<MapZoom mapRef={mapRef} mapReady={mapReady} onHome={fitNetworkBounds} />
|
||||
</div>
|
||||
<MapScaleLine
|
||||
@@ -1077,7 +1093,10 @@ export function MapWorkbenchPage({
|
||||
) : null}
|
||||
|
||||
{mobileSheet === null && artifactWorkspace.surfaceMode !== "focus" ? (
|
||||
<div className="absolute bottom-16 left-3 right-3 z-30 lg:hidden">
|
||||
<div
|
||||
className="absolute left-3 right-3 z-30 transition-[bottom] duration-200 ease-out motion-reduce:transition-none lg:hidden"
|
||||
style={{ bottom: mobileOverlayLayout.mapControlBottom }}
|
||||
>
|
||||
<ToolbarPanel
|
||||
{...toolbarPanelProps}
|
||||
panelWidthClassName="w-full max-h-[52dvh] overflow-y-auto"
|
||||
@@ -1108,7 +1127,10 @@ export function MapWorkbenchPage({
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
|
||||
<div className={`absolute z-30 lg:hidden ${timelineVisible ? "bottom-[4.25rem]" : "bottom-3"} ${artifactWorkspace.surfaceMode === "focus" ? "right-3" : "left-3 right-3"}`}>
|
||||
<div
|
||||
className={`absolute z-30 transition-[bottom] duration-200 ease-out motion-reduce:transition-none lg:hidden ${artifactWorkspace.surfaceMode === "focus" ? "right-3" : "left-3 right-3"}`}
|
||||
style={{ bottom: mobileOverlayLayout.dockBottom }}
|
||||
>
|
||||
{mobileSheet === null ? (
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<button
|
||||
@@ -1167,6 +1189,7 @@ export function MapWorkbenchPage({
|
||||
events={operationalEvents}
|
||||
mode="expanded"
|
||||
selectedEventId={workspaceContext.operational.selectedEventId}
|
||||
onModeChange={closeMobileSheet}
|
||||
onSelectEvent={handleSelectOperationalEvent}
|
||||
onClearSelection={handleClearOperationalSelection}
|
||||
onReturnNow={handleReturnOperationalNow}
|
||||
|
||||
Reference in New Issue
Block a user