188 lines
6.7 KiB
TypeScript
188 lines
6.7 KiB
TypeScript
import { ArrowLeft, Map as MapIcon } 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";
|
|
import { AnalysisArtifactWorkspace } from "./analysis-document-view";
|
|
import { ArtifactPeekBar } from "./artifact-peek-bar";
|
|
import {
|
|
ARTIFACT_VIEW_CONTROL_BUTTON_CLASS_NAME,
|
|
ARTIFACT_VIEW_CONTROL_SHELL_CLASS_NAME
|
|
} from "./artifact-view-control-styles";
|
|
import type {
|
|
AnalysisArtifact,
|
|
ArtifactRequestedView,
|
|
WorkbenchSurfaceMode
|
|
} from "./workspace-model";
|
|
|
|
type WorkbenchMainFrameProps = {
|
|
mapContent: ReactNode;
|
|
mapOverlay?: ReactNode;
|
|
timelineContent: ReactNode;
|
|
timelineMode: "compact" | "expanded" | "summary";
|
|
timelineBottom: number;
|
|
activeArtifact: AnalysisArtifact | null;
|
|
pendingArtifact: AnalysisArtifact | null;
|
|
surfaceMode: WorkbenchSurfaceMode;
|
|
mapSplitAvailable: boolean;
|
|
onOpenPendingArtifact: () => void;
|
|
onDestroyPendingArtifact: () => void;
|
|
onSetArtifactView: (view: ArtifactRequestedView) => void;
|
|
onCollapseArtifact: () => void;
|
|
onDestroyArtifact: () => void;
|
|
};
|
|
|
|
export function WorkbenchMainFrame({
|
|
mapContent,
|
|
mapOverlay,
|
|
timelineContent,
|
|
timelineMode,
|
|
timelineBottom,
|
|
activeArtifact,
|
|
pendingArtifact,
|
|
surfaceMode,
|
|
mapSplitAvailable,
|
|
onOpenPendingArtifact,
|
|
onDestroyPendingArtifact,
|
|
onSetArtifactView,
|
|
onCollapseArtifact,
|
|
onDestroyArtifact
|
|
}: WorkbenchMainFrameProps) {
|
|
const prefersReducedMotion = useReducedMotion();
|
|
const showMap = surfaceMode !== "focus";
|
|
const showArtifact = activeArtifact && surfaceMode !== "map_only";
|
|
const showArtifactViewControl = Boolean(
|
|
activeArtifact &&
|
|
activeArtifact.mapRelation !== "none" &&
|
|
(surfaceMode === "focus" || surfaceMode === "map_only")
|
|
);
|
|
const mapOnly = surfaceMode === "map_only";
|
|
const timelineTransition = prefersReducedMotion
|
|
? { duration: 0 }
|
|
: { duration: 0.28, ease: [0.22, 1, 0.36, 1] as const };
|
|
const viewControlTransition = prefersReducedMotion
|
|
? { duration: 0 }
|
|
: { duration: 0.18, ease: [0.22, 1, 0.36, 1] as const };
|
|
|
|
return (
|
|
<div
|
|
className="workbench-main-frame pointer-events-none absolute inset-x-0 bottom-0 top-14 min-h-0 lg:left-[var(--workbench-agent-current-width)]"
|
|
data-testid="workbench-main-frame"
|
|
data-timeline-mode={timelineMode}
|
|
>
|
|
<div
|
|
id="main-workspace"
|
|
data-layout-mode={surfaceMode}
|
|
className={cn(
|
|
"pointer-events-auto absolute inset-0 grid min-h-0 overflow-hidden bg-[#dbe3eb]",
|
|
surfaceMode === "map_split" && "grid-cols-2 gap-px bg-slate-300",
|
|
surfaceMode !== "map_split" && "grid-cols-1"
|
|
)}
|
|
>
|
|
<section
|
|
id="workspace-map"
|
|
aria-label="供水管网地图工作区"
|
|
aria-hidden={!showMap}
|
|
className={cn(
|
|
"relative min-h-0 min-w-0 overflow-hidden bg-[var(--canvas-map)]",
|
|
!showMap && "hidden"
|
|
)}
|
|
>
|
|
{mapContent}
|
|
{mapOverlay}
|
|
</section>
|
|
|
|
{activeArtifact ? (
|
|
<section
|
|
id="artifact-workspace"
|
|
aria-label={`${activeArtifact.title}工作区`}
|
|
aria-hidden={!showArtifact}
|
|
className={cn(
|
|
"relative z-30 min-h-0 min-w-0 overflow-hidden",
|
|
!showArtifact && "hidden"
|
|
)}
|
|
>
|
|
<AnalysisArtifactWorkspace
|
|
artifact={activeArtifact}
|
|
surfaceMode={surfaceMode}
|
|
onSetView={onSetArtifactView}
|
|
onCollapse={onCollapseArtifact}
|
|
onDestroy={onDestroyArtifact}
|
|
/>
|
|
</section>
|
|
) : null}
|
|
|
|
{pendingArtifact ? (
|
|
<ArtifactPeekBar
|
|
artifact={pendingArtifact}
|
|
surfaceMode={surfaceMode}
|
|
onOpen={onOpenPendingArtifact}
|
|
onDestroy={onDestroyPendingArtifact}
|
|
/>
|
|
) : null}
|
|
|
|
<AnimatePresence initial={false}>
|
|
{showArtifactViewControl ? (
|
|
<motion.div
|
|
className={cn(
|
|
"pointer-events-auto absolute left-6 top-4 z-40 lg:left-[max(1.5rem,calc(6rem-var(--workbench-agent-current-width)))]",
|
|
ARTIFACT_VIEW_CONTROL_SHELL_CLASS_NAME
|
|
)}
|
|
initial={prefersReducedMotion ? false : { opacity: 0, y: 6, scale: 0.98 }}
|
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
exit={prefersReducedMotion ? { opacity: 0 } : { opacity: 0, y: 5, scale: 0.98 }}
|
|
transition={viewControlTransition}
|
|
>
|
|
<button
|
|
type="button"
|
|
aria-label={mapOnly ? "返回分析成果" : "显示地图"}
|
|
onClick={() => onSetArtifactView(mapOnly ? "focus" : mapSplitAvailable ? "map_split" : "map_only")}
|
|
className={ARTIFACT_VIEW_CONTROL_BUTTON_CLASS_NAME}
|
|
>
|
|
<AnimatePresence initial={false} mode="wait">
|
|
<motion.span
|
|
key={mapOnly ? "return-analysis" : "show-map"}
|
|
className="inline-flex items-center gap-1.5"
|
|
initial={prefersReducedMotion ? false : { opacity: 0, x: mapOnly ? 5 : -5 }}
|
|
animate={{ opacity: 1, x: 0 }}
|
|
exit={prefersReducedMotion ? { opacity: 0 } : { opacity: 0, x: mapOnly ? -5 : 5 }}
|
|
transition={viewControlTransition}
|
|
>
|
|
{mapOnly ? <ArrowLeft size={13} aria-hidden="true" /> : <MapIcon size={13} aria-hidden="true" />}
|
|
{mapOnly ? "返回分析成果" : "显示地图"}
|
|
</motion.span>
|
|
</AnimatePresence>
|
|
</button>
|
|
</motion.div>
|
|
) : null}
|
|
</AnimatePresence>
|
|
</div>
|
|
|
|
<motion.div
|
|
className="pointer-events-auto absolute left-3 z-20 mx-auto min-h-0"
|
|
initial={false}
|
|
animate={{
|
|
bottom: timelineBottom,
|
|
maxWidth: OPERATIONAL_TIMELINE_LAYOUT[timelineMode].maxWidth,
|
|
right: 12
|
|
}}
|
|
transition={timelineTransition}
|
|
>
|
|
<AnimatePresence initial={false}>
|
|
{timelineContent ? (
|
|
<motion.div
|
|
key="operational-timeline"
|
|
initial={prefersReducedMotion ? false : { opacity: 0, y: 14, scale: 0.985 }}
|
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
exit={prefersReducedMotion ? { opacity: 0 } : { opacity: 0, y: 12, scale: 0.985 }}
|
|
transition={timelineTransition}
|
|
>
|
|
{timelineContent}
|
|
</motion.div>
|
|
) : null}
|
|
</AnimatePresence>
|
|
</motion.div>
|
|
</div>
|
|
);
|
|
}
|