feat: refine workbench visuals and map controls

Unify the Agent history extension with the header acrylic surface, preserve the full conversation body, and consolidate shared control and status styling.

Restore map flow and SCADA controller behavior, remove obsolete rendering paths, and extend regression coverage. Button press coverage now releases outside the target so state assertions cannot accidentally toggle the control.
This commit is contained in:
2026-07-28 16:39:36 +08:00
parent ade88b4fbf
commit f4318a9b9c
96 changed files with 5480 additions and 1875 deletions
@@ -158,7 +158,7 @@ export function MapActionRow({
disabled={disabled}
onClick={onClick}
className={cn(
"flex min-h-12 w-full items-center gap-2.5 border px-2.5 py-2 text-left transition duration-150 active:translate-y-px active:scale-[0.99]",
"flex min-h-12 w-full items-center gap-2.5 border px-2.5 py-2 text-left",
MAP_COMPACT_RADIUS_CLASS_NAME,
isPrimary
? "border-blue-600 bg-blue-600 text-white shadow-lg shadow-blue-600/25 hover:bg-blue-700"
@@ -172,7 +172,7 @@ export function MapActionRow({
(isPrimary
? "cursor-not-allowed opacity-55 hover:border-blue-600 hover:bg-blue-600 hover:text-white"
: "cursor-not-allowed opacity-55 hover:border-white/70 hover:bg-white/95 hover:text-slate-700"),
disabled && "active:translate-y-0 active:scale-100"
disabled && "shadow-none"
)}
>
<span
@@ -53,7 +53,7 @@ export function MapDrawToolbar({ items, label = "绘制工具", className = "",
"flex min-h-10 items-center gap-2 border px-2.5 py-2 text-left text-xs font-semibold transition duration-150",
MAP_COMPACT_RADIUS_CLASS_NAME,
item.active
? "border-blue-600 bg-blue-600 text-white shadow-md shadow-blue-600/20"
? "border-blue-600 bg-blue-600 text-white shadow-[var(--action-shadow-primary)]"
: "text-slate-700 hover:border-blue-100 hover:bg-white hover:text-blue-700",
!item.active && MAP_READABLE_SURFACE_CLASS_NAME,
isDanger && "hover:border-red-100 hover:text-red-700",
@@ -85,7 +85,7 @@ export function MapDrawToolbar({ items, label = "绘制工具", className = "",
role="toolbar"
aria-label={label}
className={cn(
"pointer-events-auto inline-flex p-1",
"pointer-events-auto inline-flex gap-1 p-1",
MAP_COMPACT_RADIUS_CLASS_NAME,
MAP_CONTROL_SURFACE_CLASS_NAME,
className
@@ -105,14 +105,13 @@ export function MapDrawToolbar({ items, label = "绘制工具", className = "",
disabled={item.disabled}
onClick={item.onClick}
className={cn(
"grid h-8 w-8 shrink-0 place-items-center text-slate-600 transition duration-150",
"grid h-10 w-10 shrink-0 place-items-center border border-transparent text-slate-600",
MAP_ICON_CELL_RADIUS_CLASS_NAME,
"focus-visible:outline focus-visible:outline-2 focus-visible:outline-slate-900 focus-visible:outline-offset-2",
item.active
? "bg-blue-600 text-white shadow-xs"
? "border-blue-600 bg-blue-600 text-white shadow-[var(--action-shadow-primary)]"
: "hover:bg-blue-50 hover:text-blue-700",
isDanger && "hover:bg-red-50 hover:text-red-700",
item.disabled && "cursor-not-allowed opacity-45 hover:bg-transparent hover:text-slate-600"
item.disabled && "opacity-45 hover:bg-transparent hover:text-slate-600"
)}
>
<Icon size={16} aria-hidden="true" />
@@ -61,7 +61,7 @@ function LayerVisibilitySection({ items, onToggleLayer }: LayerVisibilitySection
title={item.description}
onClick={() => onToggleLayer(item.id, !item.visible)}
className={cn(
"relative flex min-h-[76px] w-full flex-col items-center justify-center gap-1.5 border px-1.5 py-2 text-center transition duration-150 active:translate-y-px active:scale-[0.98]",
"relative flex min-h-[76px] w-full flex-col items-center justify-center gap-1.5 border px-1.5 py-2 text-center",
MAP_COMPACT_RADIUS_CLASS_NAME,
item.visible
? "border-blue-200 bg-blue-50/90 text-blue-950 shadow-xs shadow-blue-950/5"
+2 -1
View File
@@ -1,5 +1,6 @@
import type { LucideIcon } from "lucide-react";
import { cn } from "@/shared/ui/cn";
import { StatusDot } from "@/shared/ui/status";
import {
MAP_ICON_CELL_RADIUS_CLASS_NAME,
MAP_MAJOR_PANEL_RADIUS_CLASS_NAME,
@@ -66,7 +67,7 @@ export function MapLegendList({ items, className = "", showStatusDot = false }:
<span className="block truncate font-semibold">{item.label}</span>
{item.description ? <span className="block truncate text-xs text-slate-500">{item.description}</span> : null}
</span>
{showStatusDot ? <span className="h-2 w-2 rounded-full bg-emerald-500" aria-hidden="true" /> : null}
{showStatusDot ? <StatusDot tone="success" size="md" /> : null}
</li>
);
})}
@@ -1,3 +1,5 @@
import { buttonInteractionVariants } from "@/shared/ui/button-interaction";
export const MAP_MAJOR_PANEL_RADIUS_CLASS_NAME = "rounded-2xl";
export const MAP_CONTROL_RADIUS_CLASS_NAME = "rounded-xl";
@@ -24,4 +26,4 @@ export const MAP_READABLE_SURFACE_STRONG_CLASS_NAME =
"surface-control border";
export const MAP_CONTROL_HOVER_CLASS_NAME =
"hover:bg-blue-100/70 hover:text-blue-700 hover:ring-1 hover:ring-blue-200/80 active:bg-blue-100";
buttonInteractionVariants({ tone: "neutral" });
@@ -117,7 +117,7 @@ export function MapMeasurePanel({
type="button"
onClick={() => onSelectUnit?.(unit.id)}
className={cn(
"h-7 text-xs font-semibold transition",
"h-10 text-xs font-semibold",
MAP_COMPACT_RADIUS_CLASS_NAME,
unit.id === activeUnitId
? "bg-blue-600 text-white"
@@ -44,7 +44,7 @@ export function showMapNotice(options: MapNoticeOptions) {
: undefined,
classNames: {
cancelButton: cn(
"absolute right-2 top-2 z-10 grid h-7 w-7 place-items-center border border-transparent bg-transparent p-0 text-slate-400 transition hover:border-slate-200 hover:bg-slate-50 hover:text-slate-700 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-600/25",
"absolute right-2 top-2 z-10 grid h-8 w-8 place-items-center border border-transparent bg-transparent p-0 text-slate-400 after:absolute after:-inset-1 after:content-[''] hover:border-slate-200 hover:bg-slate-50 hover:text-slate-700",
MAP_ICON_CELL_RADIUS_CLASS_NAME
)
},
@@ -1,6 +1,11 @@
import { AlertTriangle, CheckCircle2, Info, Loader2, X, XCircle } from "lucide-react";
import type { ReactNode } from "react";
import { cn } from "@/shared/ui/cn";
import { Button } from "@/shared/ui/button";
import {
StatusIcon as SemanticStatusIcon,
type StatusTone
} from "@/shared/ui/status";
import {
MAP_COMPACT_RADIUS_CLASS_NAME,
MAP_ICON_CELL_RADIUS_CLASS_NAME,
@@ -8,37 +13,13 @@ import {
} from "./map-control-styles";
import type { MapNoticeOptions, MapNoticeTone } from "./notice-types";
const toneClassNames: Record<MapNoticeTone, string> = {
info: "glass-transient text-slate-900",
success: "glass-transient text-slate-900",
warning: "glass-transient text-slate-900",
error: "glass-transient text-slate-900",
loading: "glass-transient text-slate-900"
};
const iconClassNames: Record<MapNoticeTone, string> = {
info: "text-blue-600",
success: "text-green-600",
warning: "text-orange-500",
error: "text-red-500",
loading: "text-blue-600"
};
const noticeAccentClassNames: Record<MapNoticeTone, string> = {
info: "bg-blue-600",
success: "bg-green-500",
warning: "bg-orange-500",
error: "bg-red-500",
loading: "bg-blue-600"
};
const noticeIconSurfaceClassNames: Record<MapNoticeTone, string> = {
info: "border-blue-100 bg-blue-50",
success: "border-green-100 bg-green-50",
warning: "border-orange-100 bg-orange-50",
error: "border-red-100 bg-red-50",
loading: "border-blue-100 bg-blue-50"
};
const noticeTones = {
info: "info",
success: "success",
warning: "warning",
error: "danger",
loading: "info"
} satisfies Record<MapNoticeTone, StatusTone>;
export function MapNoticeContent({
title,
@@ -79,15 +60,10 @@ function MapNoticeFrame({ tone, children }: { tone: MapNoticeTone; children: Rea
<div
role={tone === "error" || tone === "warning" ? "alert" : "status"}
className={cn(
"relative flex w-full items-start gap-2.5 overflow-hidden border py-2.5 pl-3 pr-10 text-sm",
"glass-transient relative flex w-full items-start gap-2.5 overflow-hidden border py-2.5 pl-3 pr-10 text-sm text-slate-900",
MAP_READABLE_RADIUS_CLASS_NAME,
toneClassNames[tone]
)}
>
<span
className={cn("absolute bottom-0 left-0 top-0 w-1", noticeAccentClassNames[tone])}
aria-hidden="true"
/>
{children}
</div>
);
@@ -101,20 +77,14 @@ function MapNoticeIcon({
icon: ReturnType<typeof getNoticeIcon>;
}) {
return (
<span
className={cn(
"mt-0.5 grid h-7 w-7 shrink-0 place-items-center border",
MAP_ICON_CELL_RADIUS_CLASS_NAME,
iconClassNames[tone],
noticeIconSurfaceClassNames[tone]
)}
<SemanticStatusIcon
tone={noticeTones[tone]}
activity={tone === "loading" ? "loading" : "static"}
size="md"
className={cn("mt-0.5", MAP_ICON_CELL_RADIUS_CLASS_NAME)}
>
<Icon
size={17}
aria-hidden="true"
className={tone === "loading" ? "animate-spin" : undefined}
/>
</span>
<Icon size={17} />
</SemanticStatusIcon>
);
}
@@ -142,16 +112,15 @@ function MapNoticeBody({
function MapNoticeAction({ label, onClick }: { label: string; onClick: () => void }) {
return (
<button
<Button
type="button"
onClick={onClick}
className={cn(
"mt-2 inline-flex h-7 items-center justify-center border border-blue-100 bg-blue-50 px-2.5 text-xs font-semibold text-blue-700 transition hover:bg-blue-100",
MAP_COMPACT_RADIUS_CLASS_NAME
)}
size="sm"
variant="outline"
className={cn("mt-2 border-blue-100 bg-blue-50 text-blue-700", MAP_COMPACT_RADIUS_CLASS_NAME)}
>
{label}
</button>
</Button>
);
}
+1 -1
View File
@@ -17,7 +17,7 @@ export function MapToaster() {
classNames: {
toast: "group pointer-events-auto",
closeButton:
"absolute right-2 top-2 grid h-6 w-6 place-items-center rounded-lg border border-transparent text-slate-400 transition hover:bg-white/70 hover:text-slate-700"
"absolute right-2 top-2 grid h-8 w-8 place-items-center rounded-lg border border-transparent text-slate-400 after:absolute after:-inset-1 after:content-[''] hover:bg-white/70 hover:text-slate-700"
}
}}
/>
+6 -7
View File
@@ -44,10 +44,10 @@ export function MapToolbar({
<nav
aria-label={label}
className={cn(
"pointer-events-auto p-[3px]",
"pointer-events-auto p-1",
MAP_CONTROL_RADIUS_CLASS_NAME,
MAP_CONTROL_SURFACE_CLASS_NAME,
isVertical ? "flex w-[46px] flex-col" : "inline-flex",
isVertical ? "flex w-12 flex-col" : "inline-flex",
className
)}
>
@@ -67,19 +67,18 @@ export function MapToolbar({
disabled={tool.disabled}
onClick={tool.onClick}
className={cn(
"relative grid h-[38px] w-[38px] shrink-0 place-items-center text-slate-700 transition duration-150",
"relative grid h-10 w-10 shrink-0 place-items-center border border-transparent text-slate-700",
MAP_ICON_CELL_RADIUS_CLASS_NAME,
"focus-visible:z-10 focus-visible:outline focus-visible:outline-2 focus-visible:outline-slate-900 focus-visible:outline-offset-2",
tool.active
? "bg-blue-50 text-blue-800 ring-1 ring-blue-300/70 hover:bg-blue-100 hover:text-blue-900"
? "border-blue-600 bg-blue-600 text-white shadow-[var(--action-shadow-primary)]"
: MAP_CONTROL_HOVER_CLASS_NAME,
tool.disabled && "cursor-not-allowed text-slate-400 opacity-60 hover:bg-transparent hover:text-slate-400 hover:ring-0 active:bg-transparent"
tool.disabled && "text-slate-400 opacity-45 hover:bg-transparent hover:text-slate-400 hover:ring-0"
)}
>
<Icon size={20} strokeWidth={2.15} aria-hidden="true" />
<span className="sr-only">{tool.label}</span>
{tool.badge ? (
<span className="absolute -right-1 -top-1 grid min-w-4 place-items-center rounded-full bg-red-500 px-1 text-xs font-semibold leading-4 text-white ring-2 ring-white">
<span className="status-tone-danger absolute -right-1 -top-1 grid min-w-4 place-items-center rounded-full bg-[var(--status-mark)] px-1 text-xs font-semibold leading-4 text-white ring-2 ring-white">
{tool.badge}
</span>
) : null}
+5 -6
View File
@@ -37,13 +37,13 @@ export function MapZoom({ mapRef, mapReady, onHome, className = "" }: MapZoomPro
role="group"
aria-label="地图缩放"
className={cn(
"pointer-events-auto flex w-[46px] flex-col gap-2",
"pointer-events-auto flex w-12 flex-col gap-2",
className
)}
>
<div
className={cn(
"flex flex-col p-[3px]",
"flex flex-col p-1",
MAP_CONTROL_RADIUS_CLASS_NAME,
MAP_CONTROL_SURFACE_CLASS_NAME
)}
@@ -65,7 +65,7 @@ export function MapZoom({ mapRef, mapReady, onHome, className = "" }: MapZoomPro
<div
className={cn(
"p-[3px]",
"p-1",
MAP_CONTROL_RADIUS_CLASS_NAME,
MAP_CONTROL_SURFACE_CLASS_NAME
)}
@@ -99,13 +99,12 @@ function ControlButton({ icon: Icon, label, disabled, onClick }: ControlButtonPr
disabled={disabled}
onClick={onClick}
className={cn(
"grid h-[38px] w-[38px] place-items-center transition duration-150",
"grid h-10 w-10 place-items-center border border-transparent",
MAP_ICON_CELL_RADIUS_CLASS_NAME,
"focus-visible:outline focus-visible:outline-2 focus-visible:outline-slate-900 focus-visible:outline-offset-2",
"bg-transparent text-slate-700",
MAP_CONTROL_HOVER_CLASS_NAME,
disabled &&
"cursor-not-allowed text-slate-400 opacity-55 hover:bg-transparent hover:text-slate-400 active:bg-transparent"
"text-slate-400 opacity-45 hover:bg-transparent hover:text-slate-400"
)}
>
<Icon size={18} strokeWidth={2.2} aria-hidden="true" />