Files
next-tjwater-frontend/src/features/workbench/layout/workbench-layout.ts
T
jiang 74bf7c64c4 fix: align map control hover spacing
Let control surfaces size to their 40px buttons, padding and borders. Reserve the resulting 50px mobile dock height to keep the map controls clear.

The initial alignment check covered only horizontal spacing and missed the mobile height constraint. Check all four outer edges at desktop and mobile widths, plus the dock gap, to prevent recurrence.
2026-11-02 15:35:11 +08:00

175 lines
5.5 KiB
TypeScript

export const WORKBENCH_LAYOUT = {
headerHeight: 56,
desktopMinWidth: 1024,
persistentConditionMinWidth: 1280,
wideMinWidth: 1536,
collapsedAgentWidth: 0,
collapsedAgentRailWidth: 64,
minAgentWidth: 400,
maxAgentWidth: 640,
agentPanelLeftInset: 12,
conditionPanelRightInset: 64,
minimumAgentMapCorridor: 256,
mapEdgeGap: 24,
desktop: {
agentWidth: 500,
conditionWidth: 432,
conditionExpandedWidth: 880,
toolbarWidth: 48,
tickerWidth: 420
},
wide: {
agentWidth: 540,
conditionWidth: 432,
conditionExpandedWidth: 960,
toolbarWidth: 48,
tickerWidth: 460
},
mobileSheet: {
halfViewportRatio: 0.52,
fullTopGap: 68
},
mobileOverlay: {
edgeInset: 12,
gap: 8,
dockHeight: 50, // 40px buttons + 4px padding and 1px border on each side.
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,
conditionExpanded = false
) {
const maxWidth = getAgentPanelMaxWidth(viewportWidth, conditionExpanded);
return Math.round(
Math.min(Math.max(width, WORKBENCH_LAYOUT.minAgentWidth), maxWidth)
);
}
export function getAgentPanelDefaultWidth(viewportWidth: number, conditionExpanded = false) {
const layout = getWorkbenchViewportLayout(viewportWidth);
if (viewportWidth < WORKBENCH_LAYOUT.desktopMinWidth) {
return layout.agentWidth;
}
const conditionWidth = conditionExpanded ? layout.conditionExpandedWidth : layout.conditionWidth;
const collisionSafeWidth =
viewportWidth -
WORKBENCH_LAYOUT.agentPanelLeftInset -
WORKBENCH_LAYOUT.mapEdgeGap -
conditionWidth -
WORKBENCH_LAYOUT.mapEdgeGap -
layout.toolbarWidth;
return Math.min(layout.agentWidth, collisionSafeWidth);
}
export function getAgentPanelMaxWidth(viewportWidth: number, conditionExpanded = false) {
const layout = getWorkbenchViewportLayout(viewportWidth);
const conditionWidth = conditionExpanded ? layout.conditionExpandedWidth : layout.conditionWidth;
const widthWithMapCorridor =
viewportWidth -
WORKBENCH_LAYOUT.agentPanelLeftInset -
WORKBENCH_LAYOUT.conditionPanelRightInset -
conditionWidth -
WORKBENCH_LAYOUT.minimumAgentMapCorridor;
return Math.min(WORKBENCH_LAYOUT.maxAgentWidth, Math.max(layout.agentWidth, widthWithMapCorridor));
}
export type WorkbenchPanelState = {
agentOpen: boolean;
agentWidth?: number;
conditionOpen: boolean;
conditionExpanded?: boolean;
};
export type WorkbenchBasemapTone = "light" | "satellite" | "none";
export type WorkbenchViewportLayout = {
agentWidth: number;
conditionWidth: number;
conditionExpandedWidth: number;
toolbarWidth: number;
tickerWidth: number;
};
export function getWorkbenchViewportLayout(viewportWidth: number): WorkbenchViewportLayout {
return viewportWidth >= WORKBENCH_LAYOUT.wideMinWidth
? WORKBENCH_LAYOUT.wide
: WORKBENCH_LAYOUT.desktop;
}
export function getWorkbenchBasemapTone(activeBaseLayerId: string): WorkbenchBasemapTone {
if (["mapbox-light", "tianditu-vector"].includes(activeBaseLayerId)) {
return "light";
}
if (["mapbox-satellite", "tianditu-image"].includes(activeBaseLayerId)) {
return "satellite";
}
return "none";
}
export function getWorkbenchCameraPadding(viewportWidth: number, panels: WorkbenchPanelState) {
if (viewportWidth < WORKBENCH_LAYOUT.desktopMinWidth) {
return { top: 72, right: 24, bottom: 72, left: 24 };
}
const layout = getWorkbenchViewportLayout(viewportWidth);
const agentWidth = panels.agentOpen
? panels.agentWidth === undefined
? getAgentPanelDefaultWidth(
viewportWidth,
panels.conditionOpen && Boolean(panels.conditionExpanded)
)
: clampAgentPanelWidth(
panels.agentWidth,
viewportWidth,
panels.conditionOpen && Boolean(panels.conditionExpanded)
)
: WORKBENCH_LAYOUT.collapsedAgentWidth;
const conditionWidth = panels.conditionOpen
? panels.conditionExpanded
? layout.conditionExpandedWidth
: layout.conditionWidth
: 0;
return {
top: WORKBENCH_LAYOUT.headerHeight + 16,
right: WORKBENCH_LAYOUT.mapEdgeGap + layout.toolbarWidth + conditionWidth,
bottom: 32,
left: WORKBENCH_LAYOUT.mapEdgeGap + agentWidth
};
}
export function getWorkbenchLayoutCssVariables() {
return {
"--workbench-agent-width": `${WORKBENCH_LAYOUT.desktop.agentWidth}px`,
"--workbench-agent-width-wide": `${WORKBENCH_LAYOUT.wide.agentWidth}px`,
"--workbench-agent-rail-width": `${WORKBENCH_LAYOUT.collapsedAgentRailWidth}px`,
"--workbench-condition-width": `${WORKBENCH_LAYOUT.desktop.conditionWidth}px`,
"--workbench-condition-width-wide": `${WORKBENCH_LAYOUT.wide.conditionWidth}px`,
"--workbench-condition-expanded-width": `${WORKBENCH_LAYOUT.desktop.conditionExpandedWidth}px`,
"--workbench-condition-expanded-width-wide": `${WORKBENCH_LAYOUT.wide.conditionExpandedWidth}px`,
"--workbench-toolbar-width": `${WORKBENCH_LAYOUT.desktop.toolbarWidth}px`,
"--workbench-ticker-width": `${WORKBENCH_LAYOUT.desktop.tickerWidth}px`,
"--workbench-ticker-width-wide": `${WORKBENCH_LAYOUT.wide.tickerWidth}px`
} as const;
}