fix: animate analysis workspace transitions
This commit is contained in:
@@ -7,37 +7,29 @@ import {
|
||||
Clock3,
|
||||
Database,
|
||||
FileSearch,
|
||||
Focus,
|
||||
Minimize2,
|
||||
ShieldCheck,
|
||||
Sparkles,
|
||||
Trash2,
|
||||
X
|
||||
} from "lucide-react";
|
||||
import { motion, useReducedMotion } from "motion/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { showMapNotice } from "@/features/map/core";
|
||||
import { cn } from "@/shared/ui/cn";
|
||||
import {
|
||||
ARTIFACT_DANGER_ICON_BUTTON_CLASS_NAME,
|
||||
ARTIFACT_ICON_BUTTON_CLASS_NAME,
|
||||
ARTIFACT_PRIMARY_ACTION_BUTTON_CLASS_NAME,
|
||||
ARTIFACT_VIEW_CONTROL_BUTTON_CLASS_NAME,
|
||||
ARTIFACT_VIEW_CONTROL_SHELL_CLASS_NAME
|
||||
ARTIFACT_PRIMARY_ACTION_BUTTON_CLASS_NAME
|
||||
} from "./artifact-view-control-styles";
|
||||
import type {
|
||||
AnalysisArtifact,
|
||||
AnalysisArtifactAction,
|
||||
AnalysisBlock,
|
||||
AnalysisMetric,
|
||||
ArtifactRequestedView,
|
||||
WorkbenchSurfaceMode
|
||||
AnalysisMetric
|
||||
} from "./workspace-model";
|
||||
|
||||
type AnalysisArtifactWorkspaceProps = {
|
||||
artifact: AnalysisArtifact;
|
||||
surfaceMode: WorkbenchSurfaceMode;
|
||||
onSetView: (view: ArtifactRequestedView) => void;
|
||||
onCollapse: () => void;
|
||||
onDestroy: () => void;
|
||||
};
|
||||
@@ -46,8 +38,6 @@ type EvidenceBlock = Exclude<AnalysisBlock, { kind: "metric-grid" }>;
|
||||
|
||||
export function AnalysisArtifactWorkspace({
|
||||
artifact,
|
||||
surfaceMode,
|
||||
onSetView,
|
||||
onCollapse,
|
||||
onDestroy
|
||||
}: AnalysisArtifactWorkspaceProps) {
|
||||
@@ -77,10 +67,7 @@ export function AnalysisArtifactWorkspace({
|
||||
className="acrylic-panel relative m-3 grid h-[calc(100%-1.5rem)] min-h-0 grid-rows-[auto_minmax(0,1fr)_auto] overflow-hidden rounded-2xl border text-slate-900"
|
||||
lang="zh-CN"
|
||||
>
|
||||
<ArtifactHeader
|
||||
artifact={artifact}
|
||||
reserveViewControlSpace={surfaceMode === "focus" && artifact.mapRelation !== "none"}
|
||||
/>
|
||||
<ArtifactHeader artifact={artifact} />
|
||||
|
||||
<div className="min-h-0 overflow-y-auto overscroll-contain px-4 py-4 xl:px-5">
|
||||
<section aria-labelledby="artifact-summary-heading">
|
||||
@@ -123,8 +110,6 @@ export function AnalysisArtifactWorkspace({
|
||||
|
||||
<ArtifactActionBar
|
||||
artifact={artifact}
|
||||
surfaceMode={surfaceMode}
|
||||
onSetView={onSetView}
|
||||
onCollapse={onCollapse}
|
||||
onDestroy={onDestroy}
|
||||
/>
|
||||
@@ -140,20 +125,9 @@ export function AnalysisArtifactWorkspace({
|
||||
);
|
||||
}
|
||||
|
||||
function ArtifactHeader({
|
||||
artifact,
|
||||
reserveViewControlSpace
|
||||
}: {
|
||||
artifact: AnalysisArtifact;
|
||||
reserveViewControlSpace: boolean;
|
||||
}) {
|
||||
function ArtifactHeader({ artifact }: { artifact: AnalysisArtifact }) {
|
||||
return (
|
||||
<header
|
||||
className={cn(
|
||||
"surface-control relative z-10 border-b px-4 pb-4 xl:px-5",
|
||||
reserveViewControlSpace ? "pt-14" : "pt-4"
|
||||
)}
|
||||
>
|
||||
<header className="surface-control relative z-10 border-b px-4 py-4 xl:px-5">
|
||||
<div className="flex items-start justify-between gap-5">
|
||||
<div className="min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs font-medium text-slate-500">
|
||||
@@ -448,36 +422,16 @@ function formatAnalyticalTimeRange(range: AnalysisArtifact["analyticalTimeRange"
|
||||
|
||||
function ArtifactActionBar({
|
||||
artifact,
|
||||
surfaceMode,
|
||||
onSetView,
|
||||
onCollapse,
|
||||
onDestroy
|
||||
}: {
|
||||
artifact: AnalysisArtifact;
|
||||
surfaceMode: WorkbenchSurfaceMode;
|
||||
onSetView: (view: ArtifactRequestedView) => void;
|
||||
onCollapse: () => void;
|
||||
onDestroy: () => void;
|
||||
}) {
|
||||
const prefersReducedMotion = useReducedMotion();
|
||||
|
||||
return (
|
||||
<footer className="surface-control relative z-20 mb-[calc(var(--workbench-timeline-height)+0.25rem)] flex min-h-14 items-center justify-between gap-3 border-t px-3">
|
||||
<div className="flex min-w-0 items-center gap-1">
|
||||
{artifact.mapRelation !== "none" && surfaceMode === "map_split" ? (
|
||||
<motion.div
|
||||
className={ARTIFACT_VIEW_CONTROL_SHELL_CLASS_NAME}
|
||||
initial={prefersReducedMotion ? false : { opacity: 0, y: 4, scale: 0.98 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
transition={prefersReducedMotion ? { duration: 0 } : { duration: 0.18, ease: [0.22, 1, 0.36, 1] }}
|
||||
>
|
||||
<button type="button" onClick={() => onSetView("focus")} className={ARTIFACT_VIEW_CONTROL_BUTTON_CLASS_NAME}>
|
||||
<Focus size={13} aria-hidden="true" />
|
||||
专注分析
|
||||
</button>
|
||||
</motion.div>
|
||||
) : null}
|
||||
</div>
|
||||
<span />
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
{artifact.actions.slice(0, 1).map((action) => <ArtifactPrimaryAction key={action.id} action={action} artifact={artifact} />)}
|
||||
<button type="button" aria-label="将成果收起为预览" title="收起为预览" onClick={onCollapse} className={ARTIFACT_ICON_BUTTON_CLASS_NAME}><Minimize2 size={15} aria-hidden="true" /></button>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ArrowLeft, Map as MapIcon } from "lucide-react";
|
||||
import { ArrowLeft, Focus, Map as MapIcon } from "lucide-react";
|
||||
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
||||
import type { ReactNode } from "react";
|
||||
import { cn } from "@/shared/ui/cn";
|
||||
@@ -50,19 +50,28 @@ export function WorkbenchMainFrame({
|
||||
}: WorkbenchMainFrameProps) {
|
||||
const prefersReducedMotion = useReducedMotion();
|
||||
const showMap = surfaceMode !== "focus";
|
||||
const showArtifact = activeArtifact && surfaceMode !== "map_only";
|
||||
const showArtifact = Boolean(activeArtifact && surfaceMode !== "map_only");
|
||||
const showArtifactViewControl = Boolean(
|
||||
activeArtifact &&
|
||||
activeArtifact.mapRelation !== "none" &&
|
||||
(surfaceMode === "focus" || surfaceMode === "map_only")
|
||||
activeArtifact.mapRelation !== "none"
|
||||
);
|
||||
const mapOnly = surfaceMode === "map_only";
|
||||
const mapSplit = surfaceMode === "map_split";
|
||||
const viewControlLabel = mapOnly
|
||||
? "返回分析成果"
|
||||
: mapSplit
|
||||
? "专注分析"
|
||||
: "显示地图";
|
||||
const viewControlLeft = mapSplit ? "calc(50% + 24px)" : "24px";
|
||||
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 };
|
||||
const artifactTransition = prefersReducedMotion
|
||||
? { duration: 0 }
|
||||
: { duration: 0.24, ease: [0.22, 1, 0.36, 1] as const };
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -92,25 +101,39 @@ export function WorkbenchMainFrame({
|
||||
{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}
|
||||
<AnimatePresence initial={false} mode="popLayout">
|
||||
{activeArtifact ? (
|
||||
<motion.section
|
||||
key={activeArtifact.id}
|
||||
id="artifact-workspace"
|
||||
aria-label={`${activeArtifact.title}工作区`}
|
||||
aria-hidden={!showArtifact}
|
||||
className={cn(
|
||||
"z-30 min-h-0 min-w-0 overflow-hidden",
|
||||
showArtifact ? "relative" : "pointer-events-none absolute inset-0"
|
||||
)}
|
||||
initial={prefersReducedMotion ? false : { opacity: 0, y: 10, scale: 0.992 }}
|
||||
animate={showArtifact
|
||||
? { opacity: 1, y: 0, scale: 1, visibility: "visible" }
|
||||
: {
|
||||
opacity: 0,
|
||||
y: 8,
|
||||
scale: 0.994,
|
||||
transitionEnd: { visibility: "hidden" }
|
||||
}}
|
||||
exit={prefersReducedMotion
|
||||
? { opacity: 0 }
|
||||
: { opacity: 0, y: 10, scale: 0.992 }}
|
||||
transition={artifactTransition}
|
||||
>
|
||||
<AnalysisArtifactWorkspace
|
||||
artifact={activeArtifact}
|
||||
onCollapse={onCollapseArtifact}
|
||||
onDestroy={onDestroyArtifact}
|
||||
/>
|
||||
</motion.section>
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
|
||||
{pendingArtifact ? (
|
||||
<ArtifactPeekBar
|
||||
@@ -125,31 +148,43 @@ export function WorkbenchMainFrame({
|
||||
{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)))]",
|
||||
"pointer-events-auto absolute bottom-[72px] z-40 lg:bottom-6",
|
||||
ARTIFACT_VIEW_CONTROL_SHELL_CLASS_NAME
|
||||
)}
|
||||
initial={prefersReducedMotion ? false : { opacity: 0, y: 6, scale: 0.98 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
initial={prefersReducedMotion
|
||||
? false
|
||||
: { opacity: 0, y: 6, scale: 0.98, left: viewControlLeft }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1, left: viewControlLeft }}
|
||||
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")}
|
||||
aria-label={viewControlLabel}
|
||||
onClick={() => onSetArtifactView(
|
||||
surfaceMode === "focus"
|
||||
? mapSplitAvailable ? "map_split" : "map_only"
|
||||
: "focus"
|
||||
)}
|
||||
className={ARTIFACT_VIEW_CONTROL_BUTTON_CLASS_NAME}
|
||||
>
|
||||
<AnimatePresence initial={false} mode="wait">
|
||||
<motion.span
|
||||
key={mapOnly ? "return-analysis" : "show-map"}
|
||||
key={surfaceMode}
|
||||
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 ? "返回分析成果" : "显示地图"}
|
||||
{mapOnly ? (
|
||||
<ArrowLeft size={13} aria-hidden="true" />
|
||||
) : mapSplit ? (
|
||||
<Focus size={13} aria-hidden="true" />
|
||||
) : (
|
||||
<MapIcon size={13} aria-hidden="true" />
|
||||
)}
|
||||
{viewControlLabel}
|
||||
</motion.span>
|
||||
</AnimatePresence>
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user