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:
@@ -51,6 +51,8 @@
|
|||||||
|
|
||||||
每个业务区按“定位、状态、下一步、证据”排列。侧栏内容使用 12px 至 14px 的紧凑间距。复杂原始对象、请求参数、历史和测试资源按需展开。移动端保持单列,不用横向滚动承载业务字段。
|
每个业务区按“定位、状态、下一步、证据”排列。侧栏内容使用 12px 至 14px 的紧凑间距。复杂原始对象、请求参数、历史和测试资源按需展开。移动端保持单列,不用横向滚动承载业务字段。
|
||||||
|
|
||||||
|
地图背景延伸到顶部导航背后,地图控件和分析正文仍预留 56px 导航高度。独立浮层使用完整亚克力材质,阅读面和控件内部的填色不额外叠加模糊;任务堆叠保留完整圆角和每层 7px 的错位。时间轴容器不足 640px 时省略统计摘要,优先保证日期、图例和操作按钮互不重叠。
|
||||||
|
|
||||||
## 6. 层级与深度
|
## 6. 层级与深度
|
||||||
|
|
||||||
保持既有亚克力半透明材质,在透底、模糊、噪点和边缘高光上调整色调。桌面 Agent dock 延续既有实底布局,使用银灰色调。
|
保持既有亚克力半透明材质,在透底、模糊、噪点和边缘高光上调整色调。桌面 Agent dock 延续既有实底布局,使用银灰色调。
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export function AgentCollapsedRail({
|
|||||||
<aside
|
<aside
|
||||||
aria-label="Agent 折叠栏"
|
aria-label="Agent 折叠栏"
|
||||||
className={cn(
|
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
|
<button
|
||||||
@@ -28,7 +28,7 @@ export function AgentCollapsedRail({
|
|||||||
aria-label={expandLabel}
|
aria-label={expandLabel}
|
||||||
title={expandLabel}
|
title={expandLabel}
|
||||||
onClick={onExpand}
|
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} />
|
<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">
|
<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({
|
function ApprovalModeSelect({
|
||||||
presentation,
|
presentation,
|
||||||
value,
|
value,
|
||||||
@@ -847,8 +850,9 @@ function ApprovalModeSelect({
|
|||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className={cn(
|
className={cn(
|
||||||
"items-start rounded-lg border border-transparent px-2.5 py-2 focus:bg-[var(--surface-reading)]",
|
AGENT_SELECT_ITEM_CLASS_NAME,
|
||||||
requestApproval && "border-[var(--surface-border)] bg-[var(--action-soft)] focus:bg-[var(--action-soft)]"
|
"items-start",
|
||||||
|
requestApproval && "border-[var(--surface-border)] bg-[var(--action-soft)]"
|
||||||
)}
|
)}
|
||||||
onSelect={() => onValueChange?.("request")}
|
onSelect={() => onValueChange?.("request")}
|
||||||
>
|
>
|
||||||
@@ -863,9 +867,10 @@ function ApprovalModeSelect({
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className={cn(
|
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 &&
|
!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")}
|
onSelect={() => onValueChange?.("always")}
|
||||||
>
|
>
|
||||||
@@ -1034,10 +1039,8 @@ function AgentModelSelect({
|
|||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
key={model.id}
|
key={model.id}
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-lg border border-transparent px-2.5 py-2 focus:bg-[var(--surface-reading)]",
|
AGENT_SELECT_ITEM_CLASS_NAME,
|
||||||
selected && (presentation === "desktop-dock"
|
selected && "border-[var(--surface-border)] bg-[var(--action-soft)]"
|
||||||
? "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)]")
|
|
||||||
)}
|
)}
|
||||||
onSelect={() => onValueChange?.(model.id)}
|
onSelect={() => onValueChange?.(model.id)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -115,17 +115,16 @@ export function MapScaleLine({
|
|||||||
|
|
||||||
<ScaleLineDivider />
|
<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="tabular-nums text-slate-800">{scale.zoom}</span>
|
||||||
<span className="text-slate-600">EPSG:3857</span>
|
|
||||||
{coordinateReadout ? (
|
{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}
|
{coordinateReadout.mercatorText}
|
||||||
</span>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -296,7 +296,7 @@ export function AgentTaskTicker({
|
|||||||
{isHovered && hasMultipleRunningTasks ? (
|
{isHovered && hasMultipleRunningTasks ? (
|
||||||
<motion.div
|
<motion.div
|
||||||
key="task-indicator"
|
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}
|
variants={tickerIndicatorVariants}
|
||||||
initial="initial"
|
initial="initial"
|
||||||
animate="animate"
|
animate="animate"
|
||||||
@@ -342,7 +342,7 @@ function TickerTaskCard({
|
|||||||
layout="position"
|
layout="position"
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute inset-x-0 top-0 h-16 overflow-hidden rounded-[22px] px-3 py-2",
|
"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" }}
|
style={{ transformOrigin: "center top" }}
|
||||||
custom={transitionDirection}
|
custom={transitionDirection}
|
||||||
|
|||||||
@@ -1256,7 +1256,7 @@ function TaskExecutionStepRow({
|
|||||||
activity={step.status === "current" ? "loading" : "static"}
|
activity={step.status === "current" ? "loading" : "static"}
|
||||||
shape="circle"
|
shape="circle"
|
||||||
size="md"
|
size="md"
|
||||||
className="relative z-10 ring-4 ring-white"
|
className="relative z-10"
|
||||||
>
|
>
|
||||||
{step.status === "done" ? (
|
{step.status === "done" ? (
|
||||||
<CheckCircle2 size={14} />
|
<CheckCircle2 size={14} />
|
||||||
|
|||||||
@@ -918,7 +918,7 @@ function ConditionTimelineSkeleton({ rows }: { rows: number }) {
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : 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>
|
||||||
<span className="surface-reading min-w-0 rounded-xl border border-[var(--surface-divider)]/70 px-2 py-1.5">
|
<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">
|
<span className="flex min-w-0 items-center gap-2">
|
||||||
@@ -1085,10 +1085,7 @@ function ConditionTimelineRow({
|
|||||||
activity={activity}
|
activity={activity}
|
||||||
shape="circle"
|
shape="circle"
|
||||||
data-selected={selected ? "true" : undefined}
|
data-selected={selected ? "true" : undefined}
|
||||||
className={cn(
|
className="relative z-10"
|
||||||
"relative z-10 ring-4 ring-white",
|
|
||||||
selected && "group-focus-visible:ring-[var(--action-focus-ring)]"
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<Icon size={12} />
|
<Icon size={12} />
|
||||||
</SemanticStatusIcon>
|
</SemanticStatusIcon>
|
||||||
|
|||||||
@@ -1254,7 +1254,7 @@ export function MapWorkbenchPage({
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
mapOverlay={
|
mapOverlay={
|
||||||
<div className="pointer-events-none absolute inset-0">
|
<div className="pointer-events-none absolute inset-x-0 bottom-0 top-14">
|
||||||
{!devPanelOpen ? (
|
{!devPanelOpen ? (
|
||||||
<>
|
<>
|
||||||
<div className="pointer-events-auto absolute right-3 top-10 hidden lg:block">
|
<div className="pointer-events-auto absolute right-3 top-10 hidden lg:block">
|
||||||
|
|||||||
@@ -82,14 +82,16 @@ export function OperationalTimeline({
|
|||||||
return (
|
return (
|
||||||
<motion.section
|
<motion.section
|
||||||
aria-label="运行时间轴摘要"
|
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}
|
initial={false}
|
||||||
animate={{ height }}
|
animate={{ height }}
|
||||||
transition={sizeTransition}
|
transition={sizeTransition}
|
||||||
>
|
>
|
||||||
<Clock3 size={15} className="text-blue-600" aria-hidden="true" />
|
<div className="flex h-8 min-w-0 items-center gap-2">
|
||||||
<span className="text-xs font-semibold">{formatDate(date)}</span>
|
<div className="flex min-w-0 items-baseline gap-2 self-start overflow-hidden">
|
||||||
<span className="h-4 w-px bg-slate-300" />
|
<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} />
|
<TimelineSummary summary={summary} />
|
||||||
{selectedEvent ? (
|
{selectedEvent ? (
|
||||||
<button
|
<button
|
||||||
@@ -102,21 +104,29 @@ export function OperationalTimeline({
|
|||||||
) : (
|
) : (
|
||||||
<span className="min-w-0 truncate text-xs text-slate-500">分析视图保持独立时间范围</span>
|
<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
|
<TimelineLegend
|
||||||
visibility={eventVisibility}
|
visibility={eventVisibility}
|
||||||
onVisibilityChange={setEventVisibility}
|
onVisibilityChange={setEventVisibility}
|
||||||
/>
|
/>
|
||||||
<TimelineActions mode={mode} onModeChange={onModeChange} onReturnNow={onReturnNow} />
|
<TimelineActions
|
||||||
|
mode={mode}
|
||||||
|
onModeChange={onModeChange}
|
||||||
|
onReturnNow={onReturnNow}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</motion.section>
|
</motion.section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider delayDuration={180}>
|
<TooltipProvider delayDuration={180} disableHoverableContent>
|
||||||
<motion.section
|
<motion.section
|
||||||
aria-label="运行时间轴"
|
aria-label="运行时间轴"
|
||||||
className={cn(
|
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
|
mobile
|
||||||
? "surface-reading h-full rounded-t-xl border-t"
|
? "surface-reading h-full rounded-t-xl border-t"
|
||||||
: "acrylic-panel rounded-2xl border"
|
: "acrylic-panel rounded-2xl border"
|
||||||
@@ -125,15 +135,10 @@ export function OperationalTimeline({
|
|||||||
animate={{ height }}
|
animate={{ height }}
|
||||||
transition={sizeTransition}
|
transition={sizeTransition}
|
||||||
>
|
>
|
||||||
<div
|
<div className="flex shrink-0 items-center gap-2 border-b border-slate-300/70 p-2">
|
||||||
className={cn(
|
<div className="flex min-w-0 items-baseline gap-2 self-start overflow-hidden">
|
||||||
"flex shrink-0 items-center gap-3 border-b border-slate-300/70 px-3",
|
<Clock3 size={18} className="shrink-0 self-start text-blue-600" aria-hidden="true" />
|
||||||
mode === "compact" && !mobile ? "h-10" : "h-12"
|
<div className="flex shrink-0 items-baseline gap-2">
|
||||||
)}
|
|
||||||
>
|
|
||||||
<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">
|
|
||||||
<p
|
<p
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-500",
|
"text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-500",
|
||||||
@@ -142,10 +147,9 @@ export function OperationalTimeline({
|
|||||||
>
|
>
|
||||||
运行时间轴
|
运行时间轴
|
||||||
</p>
|
</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>
|
</div>
|
||||||
</div>
|
<span className="hidden h-4 w-px shrink-0 self-center bg-slate-300 sm:block" />
|
||||||
<span className="hidden h-5 w-px bg-slate-300 sm:block" />
|
|
||||||
{!mobile ? <TimelineSummary summary={summary} /> : null}
|
{!mobile ? <TimelineSummary summary={summary} /> : null}
|
||||||
{selectedEvent && !mobile ? (
|
{selectedEvent && !mobile ? (
|
||||||
<button
|
<button
|
||||||
@@ -157,11 +161,18 @@ export function OperationalTimeline({
|
|||||||
{formatClock(selectedEvent.cursorTime)} · {selectedEvent.title}
|
{formatClock(selectedEvent.cursorTime)} · {selectedEvent.title}
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className="ml-auto flex h-8 shrink-0 items-center gap-2">
|
||||||
<TimelineLegend
|
<TimelineLegend
|
||||||
visibility={eventVisibility}
|
visibility={eventVisibility}
|
||||||
onVisibilityChange={setEventVisibility}
|
onVisibilityChange={setEventVisibility}
|
||||||
/>
|
/>
|
||||||
<TimelineActions mode={mode} onModeChange={onModeChange} onReturnNow={onReturnNow} />
|
<TimelineActions
|
||||||
|
mode={mode}
|
||||||
|
onModeChange={onModeChange}
|
||||||
|
onReturnNow={onReturnNow}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AnimatePresence initial={false} mode="wait">
|
<AnimatePresence initial={false} mode="wait">
|
||||||
@@ -204,14 +215,14 @@ export function OperationalTimeline({
|
|||||||
) : (
|
) : (
|
||||||
<motion.div
|
<motion.div
|
||||||
key="compact-timeline"
|
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 }}
|
initial={prefersReducedMotion ? false : { opacity: 0, y: 4 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
exit={prefersReducedMotion ? { opacity: 0 } : { opacity: 0, y: 4 }}
|
exit={prefersReducedMotion ? { opacity: 0 } : { opacity: 0, y: 4 }}
|
||||||
transition={prefersReducedMotion ? { duration: 0 } : { duration: 0.14 }}
|
transition={prefersReducedMotion ? { duration: 0 } : { duration: 0.14 }}
|
||||||
>
|
>
|
||||||
<CompactTimeTicks />
|
<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" />
|
<div className="absolute inset-x-0 top-1/2 h-px bg-slate-300" />
|
||||||
{clusters.map((cluster, index) => (
|
{clusters.map((cluster, index) => (
|
||||||
<TimelineMarker
|
<TimelineMarker
|
||||||
@@ -317,7 +328,7 @@ function TimelineMarker({
|
|||||||
side="top"
|
side="top"
|
||||||
sideOffset={10}
|
sideOffset={10}
|
||||||
className={cn(
|
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">
|
<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> }) {
|
function TimelineSummary({ summary }: { summary: ReturnType<typeof getOperationalEventSummary> }) {
|
||||||
return (
|
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>
|
<span>
|
||||||
计划 <strong className="text-blue-700">{summary.planned}</strong>
|
计划 <strong className="text-blue-700">{summary.planned}</strong>
|
||||||
</span>
|
</span>
|
||||||
@@ -419,7 +430,7 @@ function TimelineActions({
|
|||||||
onReturnNow: () => void;
|
onReturnNow: () => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
title="返回现在"
|
title="返回现在"
|
||||||
@@ -463,7 +474,7 @@ function TimelineLegend({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
aria-label="时间轴图例"
|
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) => {
|
{LEGEND_ITEMS.map((item) => {
|
||||||
const visible = visibility[item.id];
|
const visible = visibility[item.id];
|
||||||
@@ -528,7 +539,7 @@ function TimeTicks() {
|
|||||||
|
|
||||||
function CompactTimeTicks() {
|
function CompactTimeTicks() {
|
||||||
return (
|
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) => (
|
{[0, 6, 12, 18, 24].map((hour) => (
|
||||||
<span
|
<span
|
||||||
key={hour}
|
key={hour}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export const ARTIFACT_VIEW_CONTROL_SHELL_CLASS_NAME =
|
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 =
|
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";
|
"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 }
|
||||||
: { duration: 0.24, ease: [0.22, 1, 0.36, 1] as const };
|
: { 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 (
|
return (
|
||||||
<div
|
<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-testid="workbench-main-frame"
|
||||||
data-timeline-mode={timelineMode}
|
data-timeline-mode={timelineMode}
|
||||||
>
|
>
|
||||||
@@ -108,7 +109,7 @@ export function WorkbenchMainFrame({
|
|||||||
aria-label={`${activeArtifact.title}工作区`}
|
aria-label={`${activeArtifact.title}工作区`}
|
||||||
aria-hidden={!showArtifact}
|
aria-hidden={!showArtifact}
|
||||||
className={cn(
|
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]",
|
surfaceMode === "focus" && "lg:pl-[84px]",
|
||||||
showArtifact ? "relative" : "pointer-events-none absolute inset-0"
|
showArtifact ? "relative" : "pointer-events-none absolute inset-0"
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1225,6 +1225,12 @@ button.agent-history-session-trigger:not(:disabled):active {
|
|||||||
touch-action: manipulation;
|
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 {
|
.scheduled-feed-detail-enter {
|
||||||
animation: scheduled-feed-detail-show 140ms ease-out 180ms both;
|
animation: scheduled-feed-detail-show 140ms ease-out 180ms both;
|
||||||
will-change: opacity;
|
will-change: opacity;
|
||||||
|
|||||||
@@ -94,7 +94,8 @@ test("desktop Agent docks beside the workspace with a solid reading surface", as
|
|||||||
const headerBox = await agentHeader.boundingBox();
|
const headerBox = await agentHeader.boundingBox();
|
||||||
const workspaceBox = await workspace.boundingBox();
|
const workspaceBox = await workspace.boundingBox();
|
||||||
expect(headerBox!.height).toBe(topBarBox!.height);
|
expect(headerBox!.height).toBe(topBarBox!.height);
|
||||||
expect(headerBox!.y).toBe(workspaceBox!.y);
|
expect(headerBox!.y).toBe(topBarBox!.y + topBarBox!.height);
|
||||||
|
expect(workspaceBox!.y).toBe(topBarBox!.y);
|
||||||
await expect(topBar).toHaveCSS("box-shadow", "none");
|
await expect(topBar).toHaveCSS("box-shadow", "none");
|
||||||
await expect(agentHeader).toHaveCSS("box-shadow", "none");
|
await expect(agentHeader).toHaveCSS("box-shadow", "none");
|
||||||
await expect(agentHeader.locator(":scope > div").first()).toHaveCSS("background-color", "rgba(0, 0, 0, 0)");
|
await expect(agentHeader.locator(":scope > div").first()).toHaveCSS("background-color", "rgba(0, 0, 0, 0)");
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { expect, test, type Locator } from "@playwright/test";
|
||||||
|
import { mockAgentApi } from "./support/mock-agent-api";
|
||||||
|
import { mockWorkbenchContext } from "./support/mock-workbench-context";
|
||||||
|
import { expectThemeColor } from "./support/theme-colors";
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.emulateMedia({ reducedMotion: "reduce" });
|
||||||
|
await mockWorkbenchContext(page);
|
||||||
|
await mockAgentApi(page);
|
||||||
|
await page.route("**/api/v1/agent/chat/models", (route) => route.fulfill({
|
||||||
|
headers: { "Access-Control-Allow-Origin": "*" },
|
||||||
|
json: {
|
||||||
|
default_model: "test/fast",
|
||||||
|
models: [
|
||||||
|
{ id: "test/fast", label: "快速", description: "快速回答和日常任务处理", icon: "bolt" },
|
||||||
|
{ id: "test/expert", label: "专家", description: "深入分析和复杂任务处理", icon: "sparkle" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const width of [1440, 375]) {
|
||||||
|
test(`model options show hover and keyboard highlight without changing selection at ${width}px`, async ({ page }) => {
|
||||||
|
await page.setViewportSize({ width, height: 900 });
|
||||||
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||||
|
await page.getByRole("button", { name: width < 1024 ? "打开 Agent 面板" : /展开 Agent 助手面板/ }).click();
|
||||||
|
// Radix hides the trigger from the accessibility tree while its modal menu is open.
|
||||||
|
const trigger = page.locator('button[aria-label="选择 Agent 模型"]');
|
||||||
|
await expect(trigger).toHaveAccessibleDescription("当前模型:快速");
|
||||||
|
await trigger.click();
|
||||||
|
await waitForMenuAnimations(page.getByRole("menu"));
|
||||||
|
const fast = page.getByRole("menuitem", { name: /快速回答和日常任务处理/ });
|
||||||
|
const expert = page.getByRole("menuitem", { name: /深入分析和复杂任务处理/ });
|
||||||
|
const box = await expert.boundingBox();
|
||||||
|
for (const item of [fast, expert]) {
|
||||||
|
await item.hover();
|
||||||
|
await expect(item).toHaveAttribute("data-highlighted", "");
|
||||||
|
await expectThemeColor(item, "background-color", "--action-selection-soft-hover");
|
||||||
|
await expectThemeColor(item, "border-color", "--action-border-hover");
|
||||||
|
await expect(trigger).toHaveAttribute("aria-description", "当前模型:快速");
|
||||||
|
}
|
||||||
|
expect(await expert.boundingBox()).toEqual(box);
|
||||||
|
await page.getByRole("menu").screenshot({ path: `test-results/agent-select-menu/model-${width}.png` });
|
||||||
|
await page.keyboard.press("ArrowUp");
|
||||||
|
await expect(fast).toBeFocused();
|
||||||
|
await expectThemeColor(fast, "background-color", "--action-selection-soft-hover");
|
||||||
|
await page.keyboard.press("ArrowDown");
|
||||||
|
await expect(expert).toBeFocused();
|
||||||
|
await page.keyboard.press("Enter");
|
||||||
|
await expect(page.getByRole("menu")).toHaveCount(0);
|
||||||
|
await expect(trigger).toHaveAccessibleDescription("当前模型:专家");
|
||||||
|
await expect(trigger).toBeFocused();
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`approval options highlight both selected and unselected modes at ${width}px`, async ({ page }) => {
|
||||||
|
await page.setViewportSize({ width, height: 900 });
|
||||||
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||||
|
await page.getByRole("button", { name: width < 1024 ? "打开 Agent 面板" : /展开 Agent 助手面板/ }).click();
|
||||||
|
const trigger = page.locator('button[aria-label="权限批准模式"]');
|
||||||
|
await trigger.click();
|
||||||
|
await waitForMenuAnimations(page.getByRole("menu"));
|
||||||
|
const request = page.getByRole("menuitem", { name: /工具权限逐次确认/ });
|
||||||
|
const always = page.getByRole("menuitem", { name: /自动允许本轮权限请求/ });
|
||||||
|
for (const item of [request, always]) {
|
||||||
|
await item.hover();
|
||||||
|
await expectThemeColor(item, "background-color", "--action-selection-soft-hover");
|
||||||
|
await expectThemeColor(item, "border-color", "--action-border-hover");
|
||||||
|
await expect(trigger).toContainText("请求批准");
|
||||||
|
}
|
||||||
|
await page.getByRole("menu").screenshot({ path: `test-results/agent-select-menu/approval-${width}.png` });
|
||||||
|
await always.click();
|
||||||
|
await expect(page.getByRole("menu")).toHaveCount(0);
|
||||||
|
await expect(trigger).toBeFocused();
|
||||||
|
await expect(trigger).toContainText("始终允许");
|
||||||
|
await trigger.click();
|
||||||
|
await waitForMenuAnimations(page.getByRole("menu"));
|
||||||
|
await always.hover();
|
||||||
|
await expectThemeColor(always, "background-color", "--action-selection-soft-hover");
|
||||||
|
await page.keyboard.press("ArrowUp");
|
||||||
|
await expect(request).toBeFocused();
|
||||||
|
await expectThemeColor(request, "background-color", "--action-selection-soft-hover");
|
||||||
|
await page.keyboard.press("Enter");
|
||||||
|
await expect(trigger).toContainText("请求批准");
|
||||||
|
await expect(trigger).toBeFocused();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForMenuAnimations(menu: Locator) {
|
||||||
|
await expect(menu).toBeVisible();
|
||||||
|
await menu.evaluate((element) => Promise.all(
|
||||||
|
element.getAnimations({ subtree: true })
|
||||||
|
.filter((animation) => animation.playState === "running")
|
||||||
|
.map((animation) => animation.finished.catch(() => undefined))
|
||||||
|
));
|
||||||
|
}
|
||||||
@@ -65,6 +65,50 @@ async function waitForMapIdle(page: Page) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test("city buildings render only from zoom 12 in 3D", async ({ page }) => {
|
||||||
|
test.skip(
|
||||||
|
process.env.TJWATER_CITY_SCENE_TEST !== "1",
|
||||||
|
"Requires the generated local city-model scene"
|
||||||
|
);
|
||||||
|
test.setTimeout(60_000);
|
||||||
|
await mockWorkbenchContext(page, undefined, undefined, {}, [], {
|
||||||
|
tiandituToken: "city-scene-tianditu-token"
|
||||||
|
});
|
||||||
|
await page.route("https://t0.tianditu.gov.cn/**", (route) =>
|
||||||
|
route.fulfill({ contentType: "image/png", body: EMPTY_RASTER_TILE })
|
||||||
|
);
|
||||||
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||||
|
const toggle = page
|
||||||
|
.getByRole("group", { name: "地图缩放", exact: true })
|
||||||
|
.locator("button[aria-pressed]");
|
||||||
|
await expect(toggle).toBeEnabled({ timeout: 15_000 });
|
||||||
|
await toggle.click();
|
||||||
|
await page.evaluate(() =>
|
||||||
|
window.__waterNetworkMap!.jumpTo({ center: [121.464, 30.916], zoom: 12, pitch: 55, bearing: 0 })
|
||||||
|
);
|
||||||
|
const renderedBuildings = () => page.evaluate(() => {
|
||||||
|
const map = window.__waterNetworkMap!;
|
||||||
|
return map.getLayer("fengxian-city-buildings")
|
||||||
|
? map.queryRenderedFeatures({ layers: ["fengxian-city-buildings"] }).length
|
||||||
|
: 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect.poll(renderedBuildings, { timeout: 30_000 }).toBeGreaterThan(0);
|
||||||
|
await page.screenshot({ path: "test-results/city-model/zoom-12.png" });
|
||||||
|
for (const zoom of [11.99, 11]) {
|
||||||
|
await page.evaluate((value) => window.__waterNetworkMap!.jumpTo({ zoom: value }), zoom);
|
||||||
|
await expect.poll(renderedBuildings).toBe(0);
|
||||||
|
}
|
||||||
|
await page.screenshot({ path: "test-results/city-model/zoom-11.png" });
|
||||||
|
for (const zoom of [12, 15, 16]) {
|
||||||
|
await page.evaluate((value) => window.__waterNetworkMap!.jumpTo({ zoom: value }), zoom);
|
||||||
|
await expect.poll(renderedBuildings, { timeout: 15_000 }).toBeGreaterThan(0);
|
||||||
|
}
|
||||||
|
await toggle.click();
|
||||||
|
await expect.poll(() => page.evaluate(() => window.__waterNetworkMap!.getPitch())).toBe(0);
|
||||||
|
await expect.poll(renderedBuildings).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
// Uses generated local building data, while unrelated backend services are isolated.
|
// Uses generated local building data, while unrelated backend services are isolated.
|
||||||
test("3D adds opaque buildings without enabling duplicate GIS layers", async ({ page }) => {
|
test("3D adds opaque buildings without enabling duplicate GIS layers", async ({ page }) => {
|
||||||
test.skip(
|
test.skip(
|
||||||
|
|||||||
@@ -301,7 +301,8 @@ async function expectDesktopMainFrameGeometry(page: Page) {
|
|||||||
expect(agentBox!.x).toBe(0);
|
expect(agentBox!.x).toBe(0);
|
||||||
expect(agentBox!.y).toBe(56);
|
expect(agentBox!.y).toBe(56);
|
||||||
expect(workspaceBox!.x).toBe(agentBox!.x + agentBox!.width);
|
expect(workspaceBox!.x).toBe(agentBox!.x + agentBox!.width);
|
||||||
expect(workspaceBox!.y).toBe(agentBox!.y);
|
expect(workspaceBox!.y).toBe(0);
|
||||||
|
expect(workspaceBox!.height).toBe(agentBox!.height + agentBox!.y);
|
||||||
expect(mapBox).toEqual(workspaceBox);
|
expect(mapBox).toEqual(workspaceBox);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user