fix: refine floating workspace controls
Keep ScaleInfo pinned to the map corner and move the centered timeline above collisions, preventing the previous conditional offset from lifting the scale. Remove collapsed Agent rail width reservation and add timeline state transitions.
This commit is contained in:
@@ -1,13 +1,14 @@
|
||||
import {
|
||||
CalendarClock,
|
||||
Check,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Clock3,
|
||||
OctagonAlert,
|
||||
PanelBottomClose,
|
||||
PanelBottomOpen,
|
||||
RotateCcw,
|
||||
TriangleAlert
|
||||
} from "lucide-react";
|
||||
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
||||
import { cn } from "@/shared/ui/cn";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/shared/ui/tooltip";
|
||||
import {
|
||||
@@ -51,18 +52,24 @@ export function OperationalTimeline({
|
||||
onClearSelection,
|
||||
onReturnNow
|
||||
}: OperationalTimelineProps) {
|
||||
const prefersReducedMotion = useReducedMotion();
|
||||
const summary = getOperationalEventSummary(events);
|
||||
const clusters = clusterOperationalEvents(events, mobile ? 60 : 10);
|
||||
const selectedEvent = events.find((event) => event.id === selectedEventId) ?? null;
|
||||
const cursorPosition = getDayPosition(cursorTime);
|
||||
const height = mobile ? "100%" : OPERATIONAL_TIMELINE_LAYOUT[mode].height;
|
||||
const sizeTransition = prefersReducedMotion
|
||||
? { duration: 0 }
|
||||
: { duration: 0.3, ease: [0.22, 1, 0.36, 1] as const };
|
||||
|
||||
if (mode === "summary" && !mobile) {
|
||||
return (
|
||||
<section
|
||||
<motion.section
|
||||
aria-label="运行时间轴摘要"
|
||||
className="acrylic-panel flex h-full items-center gap-3 rounded-2xl border px-3 text-slate-900"
|
||||
style={{ height }}
|
||||
initial={false}
|
||||
animate={{ height }}
|
||||
transition={sizeTransition}
|
||||
>
|
||||
<Clock3 size={15} className="text-blue-600" aria-hidden="true" />
|
||||
<span className="text-xs font-semibold">{formatDate(date)}</span>
|
||||
@@ -80,13 +87,13 @@ export function OperationalTimeline({
|
||||
<span className="min-w-0 truncate text-xs text-slate-500">分析视图保持独立时间范围</span>
|
||||
)}
|
||||
<TimelineActions mode={mode} onModeChange={onModeChange} onReturnNow={onReturnNow} />
|
||||
</section>
|
||||
</motion.section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipProvider delayDuration={180}>
|
||||
<section
|
||||
<motion.section
|
||||
aria-label="运行时间轴"
|
||||
className={cn(
|
||||
"flex min-h-0 flex-col overflow-hidden text-slate-900",
|
||||
@@ -94,12 +101,14 @@ export function OperationalTimeline({
|
||||
? "surface-reading h-full rounded-t-xl border-t"
|
||||
: "acrylic-panel rounded-2xl border"
|
||||
)}
|
||||
style={{ height }}
|
||||
initial={false}
|
||||
animate={{ height }}
|
||||
transition={sizeTransition}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex shrink-0 items-center gap-3 border-b border-slate-300/70 px-3",
|
||||
mode === "compact" && !mobile ? "h-8" : "h-12"
|
||||
mode === "compact" && !mobile ? "h-10" : "h-12"
|
||||
)}
|
||||
>
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
@@ -124,8 +133,16 @@ export function OperationalTimeline({
|
||||
<TimelineActions mode={mode} onModeChange={onModeChange} onReturnNow={onReturnNow} />
|
||||
</div>
|
||||
|
||||
{mode === "expanded" || mobile ? (
|
||||
<div className="grid min-h-0 flex-1 grid-cols-[52px_minmax(0,1fr)] px-3 pb-2 pt-1.5 sm:grid-cols-[76px_minmax(0,1fr)]">
|
||||
<AnimatePresence initial={false} mode="wait">
|
||||
{mode === "expanded" || mobile ? (
|
||||
<motion.div
|
||||
key="expanded-timeline"
|
||||
className="grid min-h-0 flex-1 grid-cols-[52px_minmax(0,1fr)] px-3 pb-2 pt-1.5 sm:grid-cols-[76px_minmax(0,1fr)]"
|
||||
initial={prefersReducedMotion ? false : { opacity: 0, y: 5 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={prefersReducedMotion ? { opacity: 0 } : { opacity: 0, y: -4 }}
|
||||
transition={prefersReducedMotion ? { duration: 0 } : { duration: 0.14 }}
|
||||
>
|
||||
<div className="grid grid-rows-[18px_repeat(3,1fr)] pr-2 text-[10px] text-slate-500">
|
||||
<span />
|
||||
{LANES.map((lane) => (
|
||||
@@ -150,11 +167,18 @@ export function OperationalTimeline({
|
||||
<span className="absolute -left-1 -top-1 h-2 w-2 rotate-45 bg-blue-500" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
) : (
|
||||
<div className="relative min-h-0 flex-1 px-3">
|
||||
<motion.div
|
||||
key="compact-timeline"
|
||||
className="relative min-h-0 flex-1 px-3"
|
||||
initial={prefersReducedMotion ? false : { opacity: 0, y: 4 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={prefersReducedMotion ? { opacity: 0 } : { opacity: 0, y: 4 }}
|
||||
transition={prefersReducedMotion ? { duration: 0 } : { duration: 0.14 }}
|
||||
>
|
||||
<CompactTimeTicks />
|
||||
<div className="absolute bottom-1 left-3 right-3 top-3.5">
|
||||
<div className="absolute bottom-2 left-3 right-3 top-4">
|
||||
<div className="absolute inset-x-0 top-1/2 h-px bg-slate-300" />
|
||||
{clusters.map((cluster, index) => (
|
||||
<TimelineMarker
|
||||
@@ -171,9 +195,10 @@ export function OperationalTimeline({
|
||||
style={{ left: `${cursorPosition}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</section>
|
||||
</AnimatePresence>
|
||||
</motion.section>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
@@ -316,26 +341,35 @@ function TimelineActions({
|
||||
onReturnNow: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="ml-auto flex shrink-0 items-center gap-1">
|
||||
<div className="surface-control ml-auto flex shrink-0 items-center rounded-xl border border-white/70 p-0.5 shadow-[0_4px_14px_rgba(15,23,42,0.08)]">
|
||||
<button
|
||||
type="button"
|
||||
title="返回现在"
|
||||
aria-label="返回现在"
|
||||
onClick={onReturnNow}
|
||||
className="surface-control grid h-8 w-8 place-items-center rounded-lg border text-slate-500 hover:text-blue-700"
|
||||
className="inline-flex h-7 items-center gap-1.5 rounded-lg px-2 text-[10px] font-medium text-slate-500 transition-colors hover:bg-white/80 hover:text-blue-700"
|
||||
>
|
||||
<RotateCcw size={14} aria-hidden="true" />
|
||||
<RotateCcw size={13} aria-hidden="true" />
|
||||
<span className="hidden sm:inline">现在</span>
|
||||
</button>
|
||||
{onModeChange ? (
|
||||
<button
|
||||
type="button"
|
||||
title={mode === "expanded" ? "收起时间轴" : "展开时间轴"}
|
||||
aria-label={mode === "expanded" ? "收起时间轴" : "展开时间轴"}
|
||||
onClick={() => onModeChange(mode === "expanded" ? "compact" : "expanded")}
|
||||
className="surface-control grid h-8 w-8 place-items-center rounded-lg border text-slate-500 hover:text-blue-700"
|
||||
>
|
||||
{mode === "expanded" ? <ChevronDown size={15} /> : <ChevronUp size={15} />}
|
||||
</button>
|
||||
<>
|
||||
<span className="mx-0.5 h-4 w-px bg-slate-300/80" aria-hidden="true" />
|
||||
<button
|
||||
type="button"
|
||||
title={mode === "expanded" ? "收起时间轴" : "展开时间轴"}
|
||||
aria-label={mode === "expanded" ? "收起时间轴" : "展开时间轴"}
|
||||
onClick={() => onModeChange(mode === "expanded" ? "compact" : "expanded")}
|
||||
className="inline-flex h-7 items-center gap-1.5 rounded-lg px-2 text-[10px] font-medium text-slate-500 transition-colors hover:bg-white/80 hover:text-blue-700"
|
||||
>
|
||||
{mode === "expanded" ? (
|
||||
<PanelBottomClose size={14} aria-hidden="true" />
|
||||
) : (
|
||||
<PanelBottomOpen size={14} aria-hidden="true" />
|
||||
)}
|
||||
<span>{mode === "expanded" ? "收起" : "展开"}</span>
|
||||
</button>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user