fix(ui): refine acrylic controls and timeline interactions
Restore full acrylic materials to stacked task cards and floating controls. Extend the map behind the navigation header while preserving document and control insets. Align timeline and scale readouts, refresh dense-marker hover content, and highlight model and approval menu options. Keep narrow timeline headers clear of their legend and actions. Update coupled geometry checks and retain city zoom-12 and menu regression coverage. Validation: scoped git diff --check and read-only UI geometry inspection. Test suites were not rerun at the user's request.
This commit is contained in:
@@ -20,7 +20,7 @@ export function AgentCollapsedRail({
|
||||
<aside
|
||||
aria-label="Agent 折叠栏"
|
||||
className={cn(
|
||||
"agent-rail-enter surface-dock pointer-events-auto w-[72px] rounded-2xl border p-1.5 shadow-[0_8px_24px_rgba(15,23,42,0.1)]"
|
||||
"agent-rail-enter acrylic-control pointer-events-auto w-[72px] rounded-2xl border p-1.5"
|
||||
)}
|
||||
>
|
||||
<button
|
||||
@@ -28,7 +28,7 @@ export function AgentCollapsedRail({
|
||||
aria-label={expandLabel}
|
||||
title={expandLabel}
|
||||
onClick={onExpand}
|
||||
className="agent-rail-item surface-control group relative flex h-14 w-full items-center justify-center rounded-xl border hover:bg-[var(--surface-reading)]"
|
||||
className="agent-rail-item group relative flex h-14 w-full items-center justify-center rounded-xl border border-transparent bg-transparent hover:bg-[var(--action-selection-soft)]"
|
||||
>
|
||||
<AgentPersona className="h-11 w-11" state={personaState} />
|
||||
<span className="agent-panel-primary-icon absolute bottom-0.5 right-0.5 grid h-5 w-5 place-items-center rounded-md border-0! shadow-xs transition-transform group-hover:translate-x-0.5">
|
||||
|
||||
@@ -802,6 +802,9 @@ function VoiceInputButton({
|
||||
);
|
||||
}
|
||||
|
||||
const AGENT_SELECT_ITEM_CLASS_NAME =
|
||||
"rounded-lg border border-transparent px-2.5 py-2 focus:border-[var(--action-border-hover)] focus:bg-[var(--action-selection-soft-hover)] data-[highlighted]:border-[var(--action-border-hover)] data-[highlighted]:bg-[var(--action-selection-soft-hover)] motion-reduce:transition-none";
|
||||
|
||||
function ApprovalModeSelect({
|
||||
presentation,
|
||||
value,
|
||||
@@ -847,8 +850,9 @@ function ApprovalModeSelect({
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
className={cn(
|
||||
"items-start rounded-lg border border-transparent px-2.5 py-2 focus:bg-[var(--surface-reading)]",
|
||||
requestApproval && "border-[var(--surface-border)] bg-[var(--action-soft)] focus:bg-[var(--action-soft)]"
|
||||
AGENT_SELECT_ITEM_CLASS_NAME,
|
||||
"items-start",
|
||||
requestApproval && "border-[var(--surface-border)] bg-[var(--action-soft)]"
|
||||
)}
|
||||
onSelect={() => onValueChange?.("request")}
|
||||
>
|
||||
@@ -863,9 +867,10 @@ function ApprovalModeSelect({
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className={cn(
|
||||
"mt-1 items-start rounded-lg border border-transparent px-2.5 py-2 focus:bg-[var(--surface-reading)]",
|
||||
AGENT_SELECT_ITEM_CLASS_NAME,
|
||||
"mt-1 items-start",
|
||||
!requestApproval &&
|
||||
"status-tone-success border-[var(--status-border)] bg-[var(--status-soft)] focus:bg-[var(--status-soft)]"
|
||||
"status-tone-success border-[var(--status-border)] bg-[var(--status-soft)]"
|
||||
)}
|
||||
onSelect={() => onValueChange?.("always")}
|
||||
>
|
||||
@@ -1034,10 +1039,8 @@ function AgentModelSelect({
|
||||
<DropdownMenuItem
|
||||
key={model.id}
|
||||
className={cn(
|
||||
"rounded-lg border border-transparent px-2.5 py-2 focus:bg-[var(--surface-reading)]",
|
||||
selected && (presentation === "desktop-dock"
|
||||
? "border-[var(--surface-border)] bg-[var(--action-soft)] focus:bg-[var(--action-soft)]"
|
||||
: "border-[var(--surface-border)] bg-[var(--action-soft)] focus:bg-[var(--action-soft)]")
|
||||
AGENT_SELECT_ITEM_CLASS_NAME,
|
||||
selected && "border-[var(--surface-border)] bg-[var(--action-soft)]"
|
||||
)}
|
||||
onSelect={() => onValueChange?.(model.id)}
|
||||
>
|
||||
|
||||
@@ -115,17 +115,16 @@ export function MapScaleLine({
|
||||
|
||||
<ScaleLineDivider />
|
||||
|
||||
<div className="flex min-w-0 items-center justify-end gap-2.5 overflow-hidden whitespace-nowrap text-slate-700">
|
||||
<div className="flex min-w-0 items-center justify-end gap-2 overflow-hidden whitespace-nowrap text-slate-700">
|
||||
<span className="tabular-nums text-slate-800">{scale.zoom}</span>
|
||||
<span className="text-slate-600">EPSG:3857</span>
|
||||
{coordinateReadout ? (
|
||||
<span className={cn(COORDINATE_READOUT_WIDTH_CLASS_NAME, "min-w-0 truncate text-right tabular-nums text-slate-800")} title={coordinateReadout.lngLatText}>
|
||||
<span className={cn(COORDINATE_READOUT_WIDTH_CLASS_NAME, "min-w-0 truncate text-left tabular-nums text-slate-800")} title={coordinateReadout.lngLatText}>
|
||||
{coordinateReadout.mercatorText}
|
||||
</span>
|
||||
) : (
|
||||
<span className={cn(COORDINATE_READOUT_WIDTH_CLASS_NAME, "min-w-0 truncate text-right tabular-nums")}>{coordinateLabel}</span>
|
||||
<span className={cn(COORDINATE_READOUT_WIDTH_CLASS_NAME, "min-w-0 truncate text-left tabular-nums")}>{coordinateLabel}</span>
|
||||
)}
|
||||
<span className="text-[var(--ink-muted)]">Mapbox/OSM</span>
|
||||
<span className="shrink-0 text-slate-600">EPSG:3857</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -296,7 +296,7 @@ export function AgentTaskTicker({
|
||||
{isHovered && hasMultipleRunningTasks ? (
|
||||
<motion.div
|
||||
key="task-indicator"
|
||||
className="agent-task-ticker-indicator surface-control flex flex-col gap-1 rounded-full border px-1 py-1"
|
||||
className="agent-task-ticker-indicator acrylic-control flex flex-col gap-1 rounded-full border px-1 py-1"
|
||||
variants={tickerIndicatorVariants}
|
||||
initial="initial"
|
||||
animate="animate"
|
||||
@@ -342,7 +342,7 @@ function TickerTaskCard({
|
||||
layout="position"
|
||||
className={cn(
|
||||
"absolute inset-x-0 top-0 h-16 overflow-hidden rounded-[22px] px-3 py-2",
|
||||
isActive ? "glass-transient z-30" : "surface-control pointer-events-none z-10 border"
|
||||
isActive ? "glass-transient z-30" : "acrylic-control pointer-events-none z-10 border"
|
||||
)}
|
||||
style={{ transformOrigin: "center top" }}
|
||||
custom={transitionDirection}
|
||||
|
||||
@@ -1256,7 +1256,7 @@ function TaskExecutionStepRow({
|
||||
activity={step.status === "current" ? "loading" : "static"}
|
||||
shape="circle"
|
||||
size="md"
|
||||
className="relative z-10 ring-4 ring-white"
|
||||
className="relative z-10"
|
||||
>
|
||||
{step.status === "done" ? (
|
||||
<CheckCircle2 size={14} />
|
||||
|
||||
@@ -918,7 +918,7 @@ function ConditionTimelineSkeleton({ rows }: { rows: number }) {
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : null}
|
||||
<span className="relative z-10 h-6 w-6 animate-pulse rounded-full bg-[var(--surface-divider)] ring-4 ring-white" />
|
||||
<span className="relative z-10 h-6 w-6 animate-pulse rounded-full bg-[var(--surface-divider)]" />
|
||||
</span>
|
||||
<span className="surface-reading min-w-0 rounded-xl border border-[var(--surface-divider)]/70 px-2 py-1.5">
|
||||
<span className="flex min-w-0 items-center gap-2">
|
||||
@@ -1085,10 +1085,7 @@ function ConditionTimelineRow({
|
||||
activity={activity}
|
||||
shape="circle"
|
||||
data-selected={selected ? "true" : undefined}
|
||||
className={cn(
|
||||
"relative z-10 ring-4 ring-white",
|
||||
selected && "group-focus-visible:ring-[var(--action-focus-ring)]"
|
||||
)}
|
||||
className="relative z-10"
|
||||
>
|
||||
<Icon size={12} />
|
||||
</SemanticStatusIcon>
|
||||
|
||||
@@ -1254,7 +1254,7 @@ export function MapWorkbenchPage({
|
||||
/>
|
||||
}
|
||||
mapOverlay={
|
||||
<div className="pointer-events-none absolute inset-0">
|
||||
<div className="pointer-events-none absolute inset-x-0 bottom-0 top-14">
|
||||
{!devPanelOpen ? (
|
||||
<>
|
||||
<div className="pointer-events-auto absolute right-3 top-10 hidden lg:block">
|
||||
|
||||
@@ -82,41 +82,51 @@ export function OperationalTimeline({
|
||||
return (
|
||||
<motion.section
|
||||
aria-label="运行时间轴摘要"
|
||||
className="acrylic-panel flex h-full items-center gap-3 rounded-2xl border px-3 text-slate-900"
|
||||
className="@container/timeline acrylic-panel rounded-2xl border p-2 text-slate-900"
|
||||
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>
|
||||
<span className="h-4 w-px bg-slate-300" />
|
||||
<TimelineSummary summary={summary} />
|
||||
{selectedEvent ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClearSelection}
|
||||
className="min-w-0 truncate text-left text-xs text-slate-600 hover:text-blue-700"
|
||||
>
|
||||
已选 {formatClock(selectedEvent.cursorTime)} · {selectedEvent.title}
|
||||
</button>
|
||||
) : (
|
||||
<span className="min-w-0 truncate text-xs text-slate-500">分析视图保持独立时间范围</span>
|
||||
)}
|
||||
<TimelineLegend
|
||||
visibility={eventVisibility}
|
||||
onVisibilityChange={setEventVisibility}
|
||||
/>
|
||||
<TimelineActions mode={mode} onModeChange={onModeChange} onReturnNow={onReturnNow} />
|
||||
<div className="flex h-8 min-w-0 items-center gap-2">
|
||||
<div className="flex min-w-0 items-baseline gap-2 self-start overflow-hidden">
|
||||
<Clock3 size={18} className="shrink-0 self-start text-blue-600" aria-hidden="true" />
|
||||
<span className="shrink-0 whitespace-nowrap text-sm font-semibold leading-5 tabular-nums">{formatDate(date)}</span>
|
||||
<span className="h-4 w-px shrink-0 self-center bg-slate-300" />
|
||||
<TimelineSummary summary={summary} />
|
||||
{selectedEvent ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClearSelection}
|
||||
className="min-w-0 truncate text-left text-xs text-slate-600 hover:text-blue-700"
|
||||
>
|
||||
已选 {formatClock(selectedEvent.cursorTime)} · {selectedEvent.title}
|
||||
</button>
|
||||
) : (
|
||||
<span className="min-w-0 truncate text-xs text-slate-500">分析视图保持独立时间范围</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="ml-auto flex h-8 shrink-0 items-center gap-2">
|
||||
<TimelineLegend
|
||||
visibility={eventVisibility}
|
||||
onVisibilityChange={setEventVisibility}
|
||||
/>
|
||||
<TimelineActions
|
||||
mode={mode}
|
||||
onModeChange={onModeChange}
|
||||
onReturnNow={onReturnNow}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</motion.section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipProvider delayDuration={180}>
|
||||
<TooltipProvider delayDuration={180} disableHoverableContent>
|
||||
<motion.section
|
||||
aria-label="运行时间轴"
|
||||
className={cn(
|
||||
"flex min-h-0 flex-col overflow-hidden text-slate-900",
|
||||
"@container/timeline relative flex min-h-0 flex-col overflow-hidden text-slate-900",
|
||||
mobile
|
||||
? "surface-reading h-full rounded-t-xl border-t"
|
||||
: "acrylic-panel rounded-2xl border"
|
||||
@@ -125,15 +135,10 @@ export function OperationalTimeline({
|
||||
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-10" : "h-12"
|
||||
)}
|
||||
>
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<Clock3 size={15} className="shrink-0 text-blue-600" aria-hidden="true" />
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex shrink-0 items-center gap-2 border-b border-slate-300/70 p-2">
|
||||
<div className="flex min-w-0 items-baseline gap-2 self-start overflow-hidden">
|
||||
<Clock3 size={18} className="shrink-0 self-start text-blue-600" aria-hidden="true" />
|
||||
<div className="flex shrink-0 items-baseline gap-2">
|
||||
<p
|
||||
className={cn(
|
||||
"text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-500",
|
||||
@@ -142,26 +147,32 @@ export function OperationalTimeline({
|
||||
>
|
||||
运行时间轴
|
||||
</p>
|
||||
<p className="text-xs font-semibold text-slate-900">{formatDate(date)}</p>
|
||||
<p className="whitespace-nowrap text-sm font-semibold leading-5 tabular-nums text-slate-900">{formatDate(date)}</p>
|
||||
</div>
|
||||
<span className="hidden h-4 w-px shrink-0 self-center bg-slate-300 sm:block" />
|
||||
{!mobile ? <TimelineSummary summary={summary} /> : null}
|
||||
{selectedEvent && !mobile ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClearSelection}
|
||||
title="清除当前事件选择"
|
||||
className="min-w-0 truncate text-left text-[11px] text-slate-500 hover:text-blue-700"
|
||||
>
|
||||
{formatClock(selectedEvent.cursorTime)} · {selectedEvent.title}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="ml-auto flex h-8 shrink-0 items-center gap-2">
|
||||
<TimelineLegend
|
||||
visibility={eventVisibility}
|
||||
onVisibilityChange={setEventVisibility}
|
||||
/>
|
||||
<TimelineActions
|
||||
mode={mode}
|
||||
onModeChange={onModeChange}
|
||||
onReturnNow={onReturnNow}
|
||||
/>
|
||||
</div>
|
||||
<span className="hidden h-5 w-px bg-slate-300 sm:block" />
|
||||
{!mobile ? <TimelineSummary summary={summary} /> : null}
|
||||
{selectedEvent && !mobile ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClearSelection}
|
||||
title="清除当前事件选择"
|
||||
className="min-w-0 truncate text-left text-[11px] text-slate-500 hover:text-blue-700"
|
||||
>
|
||||
{formatClock(selectedEvent.cursorTime)} · {selectedEvent.title}
|
||||
</button>
|
||||
) : null}
|
||||
<TimelineLegend
|
||||
visibility={eventVisibility}
|
||||
onVisibilityChange={setEventVisibility}
|
||||
/>
|
||||
<TimelineActions mode={mode} onModeChange={onModeChange} onReturnNow={onReturnNow} />
|
||||
</div>
|
||||
|
||||
<AnimatePresence initial={false} mode="wait">
|
||||
@@ -204,14 +215,14 @@ export function OperationalTimeline({
|
||||
) : (
|
||||
<motion.div
|
||||
key="compact-timeline"
|
||||
className="relative min-h-0 flex-1 px-3"
|
||||
className="relative min-h-0 flex-1 px-2"
|
||||
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-2 left-3 right-3 top-4">
|
||||
<div className="absolute bottom-2 left-2 right-2 top-4">
|
||||
<div className="absolute inset-x-0 top-1/2 h-px bg-slate-300" />
|
||||
{clusters.map((cluster, index) => (
|
||||
<TimelineMarker
|
||||
@@ -317,7 +328,7 @@ function TimelineMarker({
|
||||
side="top"
|
||||
sideOffset={10}
|
||||
className={cn(
|
||||
"acrylic-panel w-[min(300px,calc(100vw-24px))] overflow-hidden rounded-2xl border p-1 text-slate-900 shadow-[0_18px_48px_rgba(15,23,42,0.18)]"
|
||||
"operational-timeline-tooltip acrylic-panel w-[min(300px,calc(100vw-24px))] overflow-hidden rounded-2xl border p-1 text-slate-900 shadow-[0_18px_48px_rgba(15,23,42,0.18)]"
|
||||
)}
|
||||
>
|
||||
<div className="surface-reading min-w-0 rounded-[13px] border p-3">
|
||||
@@ -392,7 +403,7 @@ function operationalEventNatureLabel(event: OperationalEvent) {
|
||||
|
||||
function TimelineSummary({ summary }: { summary: ReturnType<typeof getOperationalEventSummary> }) {
|
||||
return (
|
||||
<div className="flex shrink-0 items-center gap-2 text-[10px] text-slate-500 sm:gap-3">
|
||||
<div className="hidden shrink-0 items-baseline gap-2 text-xs leading-5 tabular-nums text-slate-500 @[640px]/timeline:flex">
|
||||
<span>
|
||||
计划 <strong className="text-blue-700">{summary.planned}</strong>
|
||||
</span>
|
||||
@@ -419,7 +430,7 @@ function TimelineActions({
|
||||
onReturnNow: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="surface-control flex shrink-0 items-center rounded-xl border border-white/70 p-0.5 shadow-[0_4px_14px_rgba(15,23,42,0.08)]">
|
||||
<div className="surface-control flex h-8 shrink-0 items-center rounded-xl border border-white/70 p-px shadow-[0_4px_14px_rgba(15,23,42,0.08)]">
|
||||
<button
|
||||
type="button"
|
||||
title="返回现在"
|
||||
@@ -463,7 +474,7 @@ function TimelineLegend({
|
||||
return (
|
||||
<div
|
||||
aria-label="时间轴图例"
|
||||
className="ml-auto flex shrink-0 items-center gap-0.5 px-1"
|
||||
className="flex h-8 shrink-0 items-center gap-0.5 px-1"
|
||||
>
|
||||
{LEGEND_ITEMS.map((item) => {
|
||||
const visible = visibility[item.id];
|
||||
@@ -528,7 +539,7 @@ function TimeTicks() {
|
||||
|
||||
function CompactTimeTicks() {
|
||||
return (
|
||||
<div className="absolute inset-x-3 top-0 text-[8px] text-slate-500">
|
||||
<div className="absolute inset-x-2 top-0 text-[8px] leading-none tabular-nums text-slate-500">
|
||||
{[0, 6, 12, 18, 24].map((hour) => (
|
||||
<span
|
||||
key={hour}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export const ARTIFACT_VIEW_CONTROL_SHELL_CLASS_NAME =
|
||||
"surface-control inline-flex shrink-0 items-center rounded-xl border border-white/70 p-0.5 shadow-[0_4px_14px_rgba(15,23,42,0.08)]";
|
||||
"acrylic-control inline-flex shrink-0 items-center rounded-xl border p-0.5";
|
||||
|
||||
export const ARTIFACT_VIEW_CONTROL_BUTTON_CLASS_NAME =
|
||||
"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 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-500/60";
|
||||
|
||||
@@ -72,9 +72,10 @@ export function WorkbenchMainFrame({
|
||||
? { duration: 0 }
|
||||
: { duration: 0.24, ease: [0.22, 1, 0.36, 1] as const };
|
||||
|
||||
// Map imagery fills the header backdrop; documents and controls reserve the header inset.
|
||||
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)]"
|
||||
className="workbench-main-frame pointer-events-none absolute inset-0 min-h-0 lg:left-[var(--workbench-agent-current-width)]"
|
||||
data-testid="workbench-main-frame"
|
||||
data-timeline-mode={timelineMode}
|
||||
>
|
||||
@@ -108,7 +109,7 @@ export function WorkbenchMainFrame({
|
||||
aria-label={`${activeArtifact.title}工作区`}
|
||||
aria-hidden={!showArtifact}
|
||||
className={cn(
|
||||
"z-30 min-h-0 min-w-0 overflow-hidden",
|
||||
"z-30 mt-14 min-h-0 min-w-0 overflow-hidden",
|
||||
surfaceMode === "focus" && "lg:pl-[84px]",
|
||||
showArtifact ? "relative" : "pointer-events-none absolute inset-0"
|
||||
)}
|
||||
|
||||
@@ -1225,6 +1225,12 @@ button.agent-history-session-trigger:not(:disabled):active {
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
/* The Popper wrapper must also let pointer events reach nearby timeline markers. */
|
||||
.operational-timeline-tooltip,
|
||||
[data-radix-popper-content-wrapper]:has(> .operational-timeline-tooltip) {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.scheduled-feed-detail-enter {
|
||||
animation: scheduled-feed-detail-show 140ms ease-out 180ms both;
|
||||
will-change: opacity;
|
||||
|
||||
Reference in New Issue
Block a user