style: refine acrylic workbench surfaces

This commit is contained in:
2026-07-15 17:07:32 +08:00
parent 8978f2d167
commit 16ecb69d00
41 changed files with 838 additions and 606 deletions
@@ -20,23 +20,18 @@ type TickerTransitionDirection = -1 | 1;
const tickerEase = [0.22, 1, 0.36, 1] as const;
const tickerSwitchAnimationMs = 380;
const tickerCardSurfaceClassName =
"glass-control border";
const tickerCardVariants: Variants = {
initial: (direction: TickerTransitionDirection) => ({
opacity: 0.62,
x: 0,
y: direction > 0 ? 36 : -28,
rotate: 0,
filter: "brightness(0.955) saturate(0.95)"
rotate: 0
}),
exit: (direction: TickerTransitionDirection) => ({
opacity: [1, 1, 0],
x: 0,
y: direction > 0 ? 36 : -28,
rotate: 0,
filter: "brightness(1.02) saturate(1)",
transition: {
duration: 0.3,
times: [0, 0.65, 1],
@@ -261,7 +256,7 @@ export function AgentTaskTicker({
{isHovered && hasMultipleRunningTasks ? (
<motion.div
key="task-indicator"
className="agent-task-ticker-indicator flex flex-col gap-1 rounded-full bg-[var(--glass-menu)] px-1 py-1 shadow-sm shadow-blue-950/10"
className="agent-task-ticker-indicator surface-control flex flex-col gap-1 rounded-full border px-1 py-1"
variants={tickerIndicatorVariants}
initial="initial"
animate="animate"
@@ -307,8 +302,7 @@ function TickerTaskCard({
layout="position"
className={cn(
"absolute inset-x-0 top-0 h-16 overflow-hidden rounded-[22px] px-3 py-2",
tickerCardSurfaceClassName,
isActive ? "z-30" : "pointer-events-none z-10"
isActive ? "glass-transient z-30" : "surface-control pointer-events-none z-10 border"
)}
style={{ transformOrigin: "center top" }}
custom={transitionDirection}
@@ -319,7 +313,6 @@ function TickerTaskCard({
x: stackStyle.x,
y: stackStyle.y,
rotate: stackStyle.rotate,
filter: stackStyle.filter,
transition: {
duration: isActive ? 0.34 : 0.3,
ease: tickerEase
@@ -449,8 +442,7 @@ function getTickerStackStyle(stackIndex: number) {
opacity: 1,
x: 0,
y: 7,
rotate: 0,
filter: "brightness(0.985) saturate(0.98)"
rotate: 0
};
}
@@ -459,8 +451,7 @@ function getTickerStackStyle(stackIndex: number) {
opacity: 1,
x: 0,
y: 14,
rotate: 0,
filter: "brightness(0.965) saturate(0.96)"
rotate: 0
};
}
@@ -468,7 +459,6 @@ function getTickerStackStyle(stackIndex: number) {
opacity: 1,
x: 0,
y: 0,
rotate: 0,
filter: "brightness(1) saturate(1)"
rotate: 0
};
}
@@ -77,7 +77,7 @@ export function FeatureInsightPanel({ feature, onClose }: FeatureInsightPanelPro
}, [feature]);
return (
<aside className="glass-readable pointer-events-auto flex h-full min-h-0 flex-col rounded border">
<aside className="acrylic-panel pointer-events-auto flex h-full min-h-0 flex-col rounded border">
<div className="border-b border-slate-200/80 p-4">
<div className="flex items-start justify-between gap-3">
<div>
@@ -91,7 +91,7 @@ export function FeatureInsightPanel({ feature, onClose }: FeatureInsightPanelPro
aria-label="关闭详情"
title="关闭详情"
onClick={onClose}
className="grid h-9 w-9 place-items-center rounded border border-slate-200 bg-white text-slate-500 hover:text-slate-900"
className="surface-control grid h-9 w-9 place-items-center rounded border text-slate-500 hover:text-slate-900"
>
<X size={16} aria-hidden="true" />
</button>
@@ -106,7 +106,7 @@ export function FeatureInsightPanel({ feature, onClose }: FeatureInsightPanelPro
<>
<div className="grid grid-cols-2 gap-2">
{metrics.map(([label, value]) => (
<div key={label} className="rounded border border-slate-200 bg-white p-3">
<div key={label} className="surface-reading rounded border p-3">
<p className="text-xs text-slate-500">{label}</p>
<p className="mt-1 truncate text-sm font-semibold text-slate-900">{value}</p>
</div>
@@ -126,7 +126,7 @@ export function FeatureInsightPanel({ feature, onClose }: FeatureInsightPanelPro
</section>
) : null}
<section className="glass-warning mt-4 rounded border border-orange-100 p-3">
<section className="material-tone-warning mt-4 rounded border border-orange-100 p-3">
<div className="flex items-center gap-2 text-sm font-semibold text-orange-900">
<TriangleAlert size={16} aria-hidden="true" />
Agent
@@ -138,7 +138,7 @@ export function FeatureInsightPanel({ feature, onClose }: FeatureInsightPanelPro
<section className="mt-4">
<h3 className="text-sm font-semibold text-slate-900"></h3>
<dl className="mt-2 divide-y divide-slate-100 rounded border border-slate-200 bg-white">
<dl className="surface-reading mt-2 divide-y divide-slate-100 rounded border">
{propertyEntries.map((entry) => (
<div key={entry.key} className="grid grid-cols-[110px_1fr] gap-2 px-3 py-2 text-sm">
<dt className="truncate text-slate-500">{entry.label}</dt>
@@ -150,7 +150,7 @@ export function FeatureInsightPanel({ feature, onClose }: FeatureInsightPanelPro
</>
) : (
<div className="flex h-full flex-col items-center justify-center text-center">
<div className="grid h-12 w-12 place-items-center rounded border border-slate-200 bg-white text-slate-500">
<div className="surface-reading grid h-12 w-12 place-items-center rounded border text-slate-500">
<Target size={22} aria-hidden="true" />
</div>
<p className="mt-3 text-sm font-medium text-slate-800"></p>
@@ -166,8 +166,8 @@ export function FeatureInsightPanel({ feature, onClose }: FeatureInsightPanelPro
function getScadaPresentation(rawTypeId: unknown) {
const typeId = Number(rawTypeId);
if (typeId === 72) return { iconPath: SCADA_ICON_PATHS.waterQuality, sectionClassName: "glass-normal border-teal-200", labelClassName: "text-teal-800" };
if (typeId === 58) return { iconPath: SCADA_ICON_PATHS.radarLevel, sectionClassName: "glass-info border-blue-200", labelClassName: "text-blue-800" };
if (typeId === 45) return { iconPath: SCADA_ICON_PATHS.ultrasonicFlow, sectionClassName: "glass-warning border-orange-200", labelClassName: "text-orange-800" };
return { iconPath: SCADA_ICON_PATHS.unknown, sectionClassName: "border-slate-200 bg-slate-50", labelClassName: "text-slate-800" };
if (typeId === 72) return { iconPath: SCADA_ICON_PATHS.waterQuality, sectionClassName: "material-tone-normal border-teal-200", labelClassName: "text-teal-800" };
if (typeId === 58) return { iconPath: SCADA_ICON_PATHS.radarLevel, sectionClassName: "material-tone-info border-blue-200", labelClassName: "text-blue-800" };
if (typeId === 45) return { iconPath: SCADA_ICON_PATHS.ultrasonicFlow, sectionClassName: "material-tone-warning border-orange-200", labelClassName: "text-orange-800" };
return { iconPath: SCADA_ICON_PATHS.unknown, sectionClassName: "surface-control border-transparent", labelClassName: "text-slate-800" };
}
@@ -11,7 +11,10 @@ import {
import Image from "next/image";
import type { ComponentType } from "react";
import { useEffect, useRef, useState } from "react";
import { MAP_MAJOR_PANEL_RADIUS_CLASS_NAME } from "@/features/map/core/components/map-control-styles";
import {
MAP_FOCUS_SURFACE_CLASS_NAME,
MAP_MAJOR_PANEL_RADIUS_CLASS_NAME
} from "@/features/map/core/components/map-control-styles";
import { showMapNotice } from "@/features/map/core/components/notice";
import { cn } from "@/lib/utils";
import { SCADA_ICON_PATHS } from "../map/scada";
@@ -128,7 +131,8 @@ export function FeaturePopover({ feature, onClose }: FeaturePopoverProps) {
return (
<aside
className={cn(
"pointer-events-auto absolute left-1/2 top-[92px] z-20 w-[min(380px,calc(100vw-24px))] -translate-x-1/2 overflow-hidden bg-white/[0.94] shadow-2xl shadow-blue-950/20 ring-1 ring-white/80 backdrop-blur-xl",
"pointer-events-auto absolute left-1/2 top-[92px] z-20 w-[min(380px,calc(100vw-24px))] -translate-x-1/2 overflow-hidden",
MAP_FOCUS_SURFACE_CLASS_NAME,
MAP_MAJOR_PANEL_RADIUS_CLASS_NAME
)}
>
@@ -173,7 +177,7 @@ export function FeaturePopover({ feature, onClose }: FeaturePopoverProps) {
<Database size={14} aria-hidden="true" />
</div>
<dl className="divide-y divide-slate-100 border-y border-slate-200/80">
<dl className="surface-reading divide-y divide-slate-100 rounded-xl px-3">
{panelModel.attributes.map((entry) => (
<div key={entry.key} className="grid min-h-10 grid-cols-[96px_minmax(0,1fr)] items-baseline gap-3 py-2.5">
<dt className="text-xs font-medium leading-5 text-slate-500">{entry.label}</dt>
@@ -218,7 +222,8 @@ function ScadaFeaturePopover({ feature, copied, canCopy, onCopy, onClose }: Scad
return (
<aside
className={cn(
"pointer-events-auto absolute left-1/2 top-[92px] z-20 w-[min(380px,calc(100vw-24px))] bg-white/[0.94] shadow-2xl shadow-blue-950/20 ring-1 ring-white/80 backdrop-blur-xl",
"pointer-events-auto absolute left-1/2 top-[92px] z-20 w-[min(380px,calc(100vw-24px))]",
MAP_FOCUS_SURFACE_CLASS_NAME,
"-translate-x-1/2 overflow-hidden",
MAP_MAJOR_PANEL_RADIUS_CLASS_NAME
)}
@@ -264,7 +269,7 @@ function ScadaFeaturePopover({ feature, copied, canCopy, onCopy, onClose }: Scad
<Radio size={14} aria-hidden="true" />
</div>
<dl className="divide-y divide-slate-100 border-y border-slate-200/80">
<dl className="surface-reading divide-y divide-slate-100 rounded-xl px-3">
{attributes.map(([label, value]) => (
<div key={label} className="grid min-h-10 grid-cols-[92px_minmax(0,1fr)] items-baseline gap-3 py-2.5">
<dt className="text-xs font-medium text-slate-500">{label}</dt>
@@ -273,7 +278,7 @@ function ScadaFeaturePopover({ feature, copied, canCopy, onCopy, onClose }: Scad
))}
</dl>
<div className="mt-3 grid grid-cols-[18px_1fr] gap-2 rounded-xl bg-slate-50 px-3 py-2.5 text-xs leading-5 text-slate-600 ring-1 ring-inset ring-slate-200/70">
<div className="surface-reading mt-3 grid grid-cols-[18px_1fr] gap-2 rounded-xl px-3 py-2.5 text-xs leading-5 text-slate-600">
<MapPin size={15} className="mt-0.5 text-slate-500" aria-hidden="true" />
<p></p>
</div>
@@ -15,7 +15,7 @@ export function IconButton({ label, children, onClick, active = false }: IconBut
className={`grid h-10 w-10 place-items-center rounded border transition ${
active
? "border-slate-800 bg-slate-900 text-white"
: "border-transparent bg-[var(--glass-readable)] text-slate-700 shadow-sm hover:bg-white"
: "surface-control border-transparent text-slate-700 hover:bg-white"
}`}
>
{children}
@@ -49,9 +49,9 @@ import { getConditionReportRiskLevel } from "./condition-report-risk";
const RUNNING_WORKFLOW_TICK_MS = 1_000;
const STATUS_PILL_CLASS_NAME = "rounded-full border px-2 py-0.5 text-xs font-semibold leading-4";
const NEUTRAL_SURFACE_CLASS_NAME = "rounded-xl border border-transparent bg-[var(--glass-readable)]";
const INSET_SURFACE_CLASS_NAME = "rounded-xl border border-slate-200/70 bg-[var(--glass-menu)]";
const REPORT_META_TILE_CLASS_NAME = "rounded-lg border border-slate-200/70 bg-[var(--glass-menu)] px-2 py-1.5";
const NEUTRAL_SURFACE_CLASS_NAME = "surface-reading rounded-xl border border-slate-200/70";
const INSET_SURFACE_CLASS_NAME = "surface-control rounded-xl border border-slate-200/70";
const REPORT_META_TILE_CLASS_NAME = "surface-control rounded-lg border border-slate-200/70 px-2 py-1.5";
const riskIndicatorClassNames: Record<ScheduledConditionRiskLevel, string> = {
normal: "border-slate-200 bg-slate-50 text-slate-600",
@@ -136,9 +136,9 @@ export function ConditionDetailPanel({ condition, onContinueConversation }: Cond
<motion.div
key={detailKey}
className="min-h-0"
initial={{ opacity: 0, y: 8, filter: "blur(2px)" }}
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
exit={{ opacity: 0, y: -6, filter: "blur(1px)" }}
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -6 }}
transition={{ duration: 0.22, ease: [0.22, 1, 0.36, 1] }}
>
{content}
@@ -185,9 +185,9 @@ function ConditionRecordDetailPanel({
<motion.div
key={bodyKey}
className="space-y-2.5"
initial={{ opacity: 0, y: 10, scale: 0.992, filter: "blur(2px)" }}
animate={{ opacity: 1, y: 0, scale: 1, filter: "blur(0px)" }}
exit={{ opacity: 0, y: -8, scale: 0.996, filter: "blur(1px)" }}
initial={{ opacity: 0, y: 10, scale: 0.992 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -8, scale: 0.996 }}
transition={{ duration: 0.26, ease: [0.22, 1, 0.36, 1] }}
>
{condition.status === "running" ? (
@@ -345,7 +345,7 @@ type DetailShellProps = {
function DetailShell({ children }: DetailShellProps) {
return (
<div className={cn(NEUTRAL_SURFACE_CLASS_NAME, "overflow-hidden ring-1 ring-white/80")}>
<div className={cn(NEUTRAL_SURFACE_CLASS_NAME, "overflow-hidden")}>
{children}
</div>
);
@@ -444,7 +444,7 @@ function SolutionOptionsList({
{options.map((option, index) => (
<article key={option.id} className="px-2.5 py-2">
<div className="flex items-center gap-2">
<span className="grid h-5 w-5 shrink-0 place-items-center rounded-lg bg-white text-xs font-semibold text-slate-600 ring-1 ring-slate-200">
<span className="surface-reading grid h-5 w-5 shrink-0 place-items-center rounded-lg border border-slate-200/70 text-xs font-semibold text-slate-600">
{index + 1}
</span>
<h4 className="min-w-0 truncate text-xs font-semibold leading-4 text-slate-900">{option.title}</h4>
@@ -576,7 +576,7 @@ function EscalationCriteriaList({ items }: { items: string[] }) {
<ol className={cn(INSET_SURFACE_CLASS_NAME, "mt-2 divide-y divide-slate-200/70 overflow-hidden")}>
{items.map((item, index) => (
<li key={item} className="grid grid-cols-[18px_1fr] gap-2 px-2 py-1.5 text-xs leading-5 text-slate-700">
<span className="mt-0.5 grid h-[18px] w-[18px] place-items-center rounded-lg bg-white text-xs font-semibold text-slate-600 ring-1 ring-slate-200">
<span className="surface-reading mt-0.5 grid h-[18px] w-[18px] place-items-center rounded-lg border border-slate-200/70 text-xs font-semibold text-slate-600">
{index + 1}
</span>
<span>{item}</span>
@@ -711,7 +711,7 @@ function ReportSection({
function KpiTile({ kpi }: { kpi: ScheduledConditionKpi }) {
return (
<div className={cn("min-w-0 rounded-xl border px-2.5 py-2", getKpiSurfaceClassName(kpi.status))}>
<div className={cn("surface-reading min-w-0 rounded-xl border px-2.5 py-2", getKpiSurfaceClassName(kpi.status))}>
<div className="flex items-start justify-between gap-2">
<div className="min-w-0">
<div className="truncate text-xs font-semibold text-slate-500" title={kpi.label}>
@@ -739,14 +739,14 @@ function KpiTile({ kpi }: { kpi: ScheduledConditionKpi }) {
function getKpiSurfaceClassName(riskLevel: ScheduledConditionRiskLevel) {
if (riskLevel === "critical") {
return "glass-danger border-red-200/80";
return "border-red-200/80";
}
if (riskLevel === "attention") {
return "glass-warning border-orange-200/80";
return "border-orange-200/80";
}
return "border-slate-200/70 bg-slate-50/80";
return "border-slate-200/70";
}
function DecisionActionStrip({
@@ -782,7 +782,7 @@ function DecisionActionStrip({
"flex min-h-9 shrink-0 items-center justify-center gap-1.5 px-3 text-xs font-semibold leading-4 transition-colors min-[1500px]:w-[104px]",
"rounded-xl",
actionDisabled
? "cursor-not-allowed bg-white/70 text-slate-500 opacity-80"
? "surface-control cursor-not-allowed border border-slate-200/70 text-slate-500 opacity-80"
: "bg-blue-600 text-white hover:bg-blue-700"
)}
>
@@ -842,14 +842,14 @@ function getTaskExecutionStepIconClassName(status: ExecutionStepStatus) {
function getTaskExecutionStepSurfaceClassName(status: ExecutionStepStatus) {
if (status === "current") {
return "glass-info border-blue-100";
return "surface-reading border-blue-100";
}
if (status === "confirmation") {
return "glass-warning border-orange-100";
return "surface-reading border-orange-100";
}
return "border-slate-200/70 bg-slate-50/80";
return "surface-reading border-slate-200/70";
}
function getTaskExecutionStepStatusClassName(status: ExecutionStepStatus) {
@@ -973,7 +973,7 @@ function DetailSectionCard({
className?: string;
}) {
return (
<div className={cn("rounded-xl border border-slate-200/80 bg-slate-50/90 px-2.5 py-2.5 text-slate-500", className)}>
<div className={cn("surface-reading rounded-xl border border-slate-200/70 px-2.5 py-2.5 text-slate-500", className)}>
{children}
</div>
);
@@ -1131,7 +1131,7 @@ function WorkOrderReplyRequirements({ condition }: { condition: ScheduledWorkOrd
<ul className="mt-2 space-y-1.5">
{condition.replyRequirements.map((item, index) => (
<li key={item} className="grid grid-cols-[18px_1fr] gap-2 text-xs leading-5 text-slate-700">
<span className="mt-0.5 grid h-[18px] w-[18px] place-items-center rounded-lg bg-white text-xs font-semibold text-blue-700 ring-1 ring-blue-100">
<span className="surface-reading mt-0.5 grid h-[18px] w-[18px] place-items-center rounded-lg border border-blue-100 text-xs font-semibold text-blue-700">
{index + 1}
</span>
<span>{item}</span>
@@ -1150,7 +1150,7 @@ function RunningEvidencePreview({ items }: { items: string[] }) {
{items.length > 0 ? (
items.map((item, index) => (
<li key={item} className="grid grid-cols-[18px_1fr_auto] items-start gap-2 text-xs leading-5 text-slate-700">
<span className="mt-0.5 grid h-[18px] w-[18px] place-items-center rounded-lg bg-blue-50 text-xs font-semibold text-blue-700 ring-1 ring-blue-100">
<span className="surface-reading mt-0.5 grid h-[18px] w-[18px] place-items-center rounded-lg border border-blue-100 text-xs font-semibold text-blue-700">
{index + 1}
</span>
<span className="line-clamp-1">{item}</span>
@@ -1173,7 +1173,7 @@ function EvidenceList({ items }: { items: string[] }) {
{items.length > 0 ? (
items.map((item, index) => (
<li key={item} className="grid grid-cols-[18px_1fr] gap-2 text-xs leading-5 text-slate-700">
<span className="mt-0.5 grid h-[18px] w-[18px] place-items-center rounded-lg bg-white text-xs font-semibold text-blue-700 ring-1 ring-blue-100">
<span className="surface-reading mt-0.5 grid h-[18px] w-[18px] place-items-center rounded-lg border border-blue-100 text-xs font-semibold text-blue-700">
{index + 1}
</span>
<span>{item}</span>
@@ -10,14 +10,12 @@ import {
} from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { cn } from "@/lib/utils";
import {
MAP_READABLE_SURFACE_STRONG_CLASS_NAME
} from "@/features/map/core/components/map-control-styles";
import { MAP_TOOL_PANEL_SURFACE_CLASS_NAME } from "@/features/map/core/components/map-control-styles";
import { showMapNotice } from "@/features/map/core/components/notice";
import { isGeneratedScheduledConditionSessionId } from "@/features/workbench/data/scheduled-conditions";
import { createConditionConversationPrompt } from "@/features/workbench/utils/scheduled-condition-prompts";
import type { ScheduledConditionItem } from "@/features/workbench/types";
import { createConditionConversationPrompt } from "@/features/workbench/utils/scheduled-condition-prompts";
import { cn } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
@@ -41,11 +39,15 @@ import {
type ConditionTaskFilterValue
} from "./scheduled-condition-feed-utils";
const CONDITION_CONTENT_SURFACE_CLASS_NAME = "surface-reading rounded-xl border border-slate-200/70";
const CONDITION_CONTROL_SURFACE_CLASS_NAME = "surface-control rounded-xl border border-slate-200/70";
type ScheduledConditionFeedProps = {
conditions?: ScheduledConditionItem[];
expanded?: boolean;
focusRequest?: ScheduledConditionFeedFocusRequest | null;
loading?: boolean;
visible?: boolean;
selectedConditionId?: string | null;
onExpandedChange?: (expanded: boolean) => void;
onFocusRequestHandled?: (requestId: number) => void;
@@ -65,6 +67,7 @@ export function ScheduledConditionFeed({
expanded: controlledExpanded,
focusRequest,
loading = false,
visible = true,
selectedConditionId: controlledSelectedConditionId,
onExpandedChange,
onFocusRequestHandled,
@@ -115,7 +118,7 @@ export function ScheduledConditionFeed({
? `筛选 ${filteredConditionCount}/${totalConditionCount} 条 · 预约 ${futureWorkOrders.length}/${allFutureWorkOrders.length}`
: expanded
? `最近 ${recentConditions.length} 条 · 预约 ${futureWorkOrders.length}`
: `最近 ${recentConditions.length}`;
: `最近 ${recentConditions.length} 条 · 预约 ${futureWorkOrders.length}`;
const selectedCondition =
timelineConditions.find((condition) => condition.id === selectedConditionId) ??
recentConditions[0] ??
@@ -238,14 +241,16 @@ export function ScheduledConditionFeed({
return (
<section
aria-label="工况任务"
aria-hidden={!visible}
className={cn(
"scheduled-feed-panel-shell pointer-events-auto max-w-[calc(100vw-6rem)] overflow-hidden p-3 motion-reduce:transition-none",
visible ? "scheduled-feed-shell-enter" : "scheduled-feed-shell-exit",
MAP_TOOL_PANEL_SURFACE_CLASS_NAME,
expanded ? "flex max-h-[calc(100dvh-8rem)] w-[880px] flex-col 2xl:w-[960px]" : "w-[432px]",
"rounded-2xl border",
expanded ? "glass-panel" : "glass-control"
"rounded-2xl"
)}
>
<header className={cn("flex items-center gap-2.5 px-2.5 py-2", "rounded-xl", expanded ? "bg-white/30" : MAP_READABLE_SURFACE_STRONG_CLASS_NAME)}>
<header className={cn("flex items-center gap-2.5 px-2.5 py-2", CONDITION_CONTROL_SURFACE_CLASS_NAME)}>
<span className={cn("grid h-8 w-8 shrink-0 place-items-center bg-blue-50 text-blue-700", "rounded-lg")}>
<CalendarClock size={17} aria-hidden="true" />
</span>
@@ -366,10 +371,7 @@ function ConditionTimelinePanel({
className={cn(
"flex min-h-0 flex-col overflow-hidden p-2.5",
expanded ? "h-[calc(100dvh-14rem)] max-h-[calc(100dvh-14rem)]" : "h-[420px] max-h-[420px]",
"rounded-xl",
expanded
? "border border-white/30 bg-white/10"
: MAP_READABLE_SURFACE_STRONG_CLASS_NAME
CONDITION_CONTENT_SURFACE_CLASS_NAME
)}
>
{expanded && !loading ? (
@@ -387,7 +389,7 @@ function ConditionTimelinePanel({
/>
) : null}
{expanded && !loading && futureWorkOrders.length > 0 ? (
<section className="mb-2 rounded-xl border border-blue-100 bg-blue-50/55 p-2">
<section className="mb-2">
<ConditionGroupHeader icon={ClipboardList} title="预约任务" count={futureWorkOrders.length} compact />
<div className="scheduled-feed-scroll max-h-[132px] overflow-y-auto pr-1">
<ConditionRows
@@ -447,7 +449,7 @@ function ConditionFilterBar({
const filterActive = taskFilter !== CONDITION_FILTER_ALL || statusFilter !== CONDITION_FILTER_ALL;
return (
<div className="mb-2 rounded-xl border border-slate-200/70 bg-slate-50/95 p-2 ring-1 ring-white/80">
<div className={cn(CONDITION_CONTENT_SURFACE_CLASS_NAME, "mb-2 p-2")}>
<div className="mb-2 flex items-center justify-between gap-2 px-1">
<span className="text-xs font-semibold text-slate-700"></span>
<span className="shrink-0 text-xs font-semibold text-slate-400">
@@ -478,7 +480,7 @@ function ConditionFilterBar({
<button
type="button"
onClick={onResetFilters}
className="mt-2 inline-flex h-7 w-full items-center justify-center gap-1.5 rounded-lg border border-blue-100 bg-white px-2 text-xs font-semibold text-blue-700 shadow-sm transition hover:border-blue-200 hover:bg-blue-50"
className="surface-control mt-2 inline-flex h-8 w-full items-center justify-center gap-1.5 rounded-lg border border-slate-200/70 px-2 text-xs font-semibold text-blue-700 transition-[background-color,transform] hover:bg-blue-50 active:scale-95"
>
<XCircle size={12} aria-hidden="true" />
@@ -515,7 +517,7 @@ function ConditionFilterDropdown<TValue extends string>({
<button
type="button"
className={cn(
"group flex h-9 w-full min-w-0 items-center gap-2 border border-white/80 bg-white px-2 text-left text-xs shadow-sm outline-none transition hover:border-blue-100 hover:bg-blue-50/35 focus-visible:border-blue-200 focus-visible:ring-2 focus-visible:ring-blue-100 data-[state=open]:border-blue-200 data-[state=open]:bg-blue-50/60 data-[state=open]:ring-2 data-[state=open]:ring-blue-100",
"surface-reading group flex h-9 w-full min-w-0 items-center gap-2 border border-slate-200/70 px-2 text-left text-xs outline-none transition-colors hover:bg-blue-50 focus-visible:border-blue-200 focus-visible:ring-2 focus-visible:ring-blue-100 data-[state=open]:border-blue-200 data-[state=open]:bg-blue-50 data-[state=open]:ring-2 data-[state=open]:ring-blue-100",
"rounded-xl"
)}
>
@@ -535,7 +537,7 @@ function ConditionFilterDropdown<TValue extends string>({
<DropdownMenuContent
align="start"
sideOffset={6}
className="glass-menu scheduled-feed-scroll z-50 max-h-[280px] w-[var(--radix-dropdown-menu-trigger-width)] overflow-y-auto rounded-xl border p-1.5 text-slate-700"
className="surface-reading scheduled-feed-scroll z-50 max-h-[280px] w-[var(--radix-dropdown-menu-trigger-width)] overflow-y-auto rounded-xl border border-slate-200/70 p-1.5 text-slate-700 shadow-md shadow-slate-900/10"
>
<DropdownMenuRadioGroup value={value} onValueChange={(nextValue) => onValueChange(nextValue as TValue)}>
{options.map((option) => (
@@ -543,12 +545,12 @@ function ConditionFilterDropdown<TValue extends string>({
key={option.value}
value={option.value}
className={cn(
"my-0.5 min-h-9 gap-2 border border-transparent py-1.5 pl-7 pr-2 text-xs transition focus:bg-blue-50/80 focus:text-blue-800 data-[state=checked]:border-blue-100 data-[state=checked]:bg-blue-50/90 data-[state=checked]:text-blue-800",
"my-0.5 min-h-9 gap-2 border border-transparent bg-transparent py-1.5 pl-7 pr-2 text-xs transition focus:bg-blue-50 focus:text-blue-800 data-[state=checked]:border-blue-100 data-[state=checked]:bg-blue-50 data-[state=checked]:text-blue-800",
"rounded-xl"
)}
>
<span className="min-w-0 flex-1 truncate font-semibold">{option.label}</span>
<span className="shrink-0 rounded-full border border-slate-200 bg-white px-1.5 py-0.5 text-xs font-semibold leading-4 text-slate-500">
<span className="surface-control shrink-0 rounded-full border px-1.5 py-0.5 text-xs font-semibold leading-4 text-slate-500">
{option.count}
</span>
</DropdownMenuRadioItem>
@@ -562,7 +564,7 @@ function ConditionFilterDropdown<TValue extends string>({
function ConditionEmptyState({ title, description }: { title: string; description: string }) {
return (
<div className="flex min-h-full flex-col items-center justify-center rounded-xl border border-dashed border-slate-200 bg-[var(--glass-readable)] px-4 py-5 text-center">
<div className="surface-reading flex min-h-full flex-col items-center justify-center rounded-xl border border-dashed border-slate-200/80 px-4 py-5 text-center">
<span className="grid h-8 w-8 place-items-center rounded-lg bg-slate-100 text-slate-400">
<ChevronRight size={15} aria-hidden="true" />
</span>
@@ -587,7 +589,7 @@ function ConditionTimelineSkeleton({ rows }: { rows: number }) {
) : null}
<span className="relative z-10 h-6 w-6 animate-pulse rounded-full bg-slate-200 ring-4 ring-white" />
</span>
<span className="min-w-0 rounded-xl border border-transparent bg-[var(--glass-menu)] px-2 py-1.5">
<span className="surface-reading min-w-0 rounded-xl border border-slate-200/70 px-2 py-1.5">
<span className="flex min-w-0 items-center gap-2">
<span className="min-w-0 flex-1 space-y-1.5">
<span className="block h-3.5 w-24 animate-pulse rounded-full bg-slate-200/90" />
@@ -607,13 +609,12 @@ function ConditionDetailSkeleton() {
<DetailScroll>
<div
className={cn(
"relative min-h-[420px] overflow-hidden border border-slate-200/60 bg-white/30 px-4 py-3",
"surface-reading relative min-h-[420px] overflow-hidden border border-slate-200/70 px-4 py-3",
"rounded-xl"
)}
role="status"
aria-label="工况详情加载中"
>
<div className="absolute inset-y-0 left-0 w-1 bg-blue-300" aria-hidden="true" />
<div className="flex gap-2.5">
<span className={cn("h-9 w-9 shrink-0 animate-pulse bg-slate-100", "rounded-lg")} />
<div className="min-w-0 flex-1 space-y-2">
@@ -628,7 +629,7 @@ function ConditionDetailSkeleton() {
</div>
<div className="mt-4 grid gap-2">
{Array.from({ length: 5 }).map((_, index) => (
<div key={index} className="rounded-xl border border-slate-200/70 bg-slate-50/80 px-3 py-3">
<div key={index} className="surface-reading rounded-xl border border-slate-200/70 px-3 py-3">
<span className="block h-3.5 w-28 animate-pulse rounded-full bg-slate-200" />
<span className="mt-2 block h-3 w-full animate-pulse rounded-full bg-slate-100" />
<span className="mt-1.5 block h-3 w-3/4 animate-pulse rounded-full bg-slate-100" />
@@ -706,8 +707,8 @@ function ConditionTimelineRow({ condition, selected, isLast, onSelect }: Conditi
aria-pressed={selected}
onClick={onSelect}
className={cn(
"group grid min-h-[60px] w-full grid-cols-[42px_24px_minmax(0,1fr)] gap-2 rounded-xl px-1 py-1 text-left outline-none transition-colors focus-visible:bg-blue-50/40 focus-visible:ring-2 focus-visible:ring-blue-200/80 focus-visible:ring-offset-1 focus-visible:ring-offset-white",
selected ? "bg-blue-50/70" : "hover:bg-blue-50/45"
"group grid min-h-[60px] w-full grid-cols-[42px_24px_minmax(0,1fr)] gap-2 rounded-xl border px-1 py-1 text-left outline-none transition-colors focus-visible:bg-blue-50 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-blue-200",
selected ? "surface-reading border-blue-100 text-blue-900" : "border-transparent hover:bg-slate-50"
)}
>
<span className="pt-2 font-mono text-xs font-semibold leading-4 text-slate-500">{formatTime(condition.scheduledAt)}</span>
@@ -735,10 +736,10 @@ function ConditionTimelineRow({ condition, selected, isLast, onSelect }: Conditi
</span>
<span
className={cn(
"min-w-0 rounded-xl border px-2 py-1.5 transition-colors group-focus-visible:border-blue-300 group-focus-visible:bg-white",
"min-w-0 px-2 py-1.5 transition-colors",
selected
? "border-blue-300 bg-white text-blue-900 shadow-sm shadow-blue-600/10 ring-1 ring-blue-300/40"
: "border-transparent bg-[var(--glass-menu)] text-slate-700 group-hover:border-blue-100 group-hover:bg-white"
? "text-blue-900"
: "text-slate-700"
)}
>
<span className="flex min-w-0 items-center gap-2">
@@ -46,10 +46,10 @@ const scenarioStatusLabels: Record<WorkbenchScenario["status"], string> = {
const menuSurfaceClassName = cn(
MAP_MAJOR_PANEL_RADIUS_CLASS_NAME,
"glass-menu border p-2 text-slate-900"
"acrylic-panel border p-2 text-slate-900"
);
const menuReadableRowClassName =
"bg-[var(--glass-readable)] focus:bg-blue-50/95 focus:text-slate-950";
"surface-well focus:bg-blue-50 focus:text-slate-950";
const headerControlButtonClassName =
"inline-flex h-8 items-center gap-2 rounded-full border border-transparent bg-transparent text-sm font-semibold text-slate-700 transition-colors hover:bg-white/70 hover:text-blue-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-600/20 focus-visible:ring-offset-2 data-[state=open]:bg-white/90 data-[state=open]:text-blue-700";
const headerControlIconClassName =
@@ -220,7 +220,7 @@ export function AlertMenu({
</span>
</DropdownMenuItem>
)) : (
<div className={cn("my-1 grid min-h-[76px] place-items-center border border-dashed border-slate-200 bg-[var(--glass-readable)] px-3 py-3 text-center", MAP_COMPACT_RADIUS_CLASS_NAME)}>
<div className={cn("surface-reading my-1 grid min-h-[76px] place-items-center border border-dashed border-slate-200 px-3 py-3 text-center", MAP_COMPACT_RADIUS_CLASS_NAME)}>
<div className="min-w-0">
<CheckCircle2 size={18} className="mx-auto text-emerald-600" aria-hidden="true" />
<p className="mt-1 text-xs font-semibold text-slate-700"></p>
@@ -315,7 +315,7 @@ function AlertQueueSummary({
const hasAlerts = count > 0;
return (
<div className={cn("overflow-hidden bg-[var(--glass-menu)] px-3 py-3 ring-1 ring-white/80", MAP_READABLE_RADIUS_CLASS_NAME)}>
<div className={cn("surface-reading overflow-hidden px-3 py-3", MAP_READABLE_RADIUS_CLASS_NAME)}>
<div className="flex items-start justify-between gap-3">
<div className="flex min-w-0 items-start gap-2.5">
<span
@@ -423,7 +423,7 @@ function MenuHeader({
icon?: LucideIcon;
}) {
return (
<div className={cn("flex items-center justify-between gap-3 border border-transparent bg-[var(--glass-menu)] px-2 py-2", MAP_READABLE_RADIUS_CLASS_NAME)}>
<div className={cn("surface-control flex items-center justify-between gap-3 border px-2 py-2", MAP_READABLE_RADIUS_CLASS_NAME)}>
<div className="min-w-0">
<div className="flex min-w-0 items-center gap-1.5">
{Icon ? <Icon size={13} className="shrink-0 text-slate-500" aria-hidden="true" /> : null}
@@ -78,7 +78,7 @@ export function WorkbenchTopBar({
};
return (
<header className="glass-navigation pointer-events-auto absolute left-0 right-0 top-0 z-30 flex h-14 select-none items-center justify-between border-b px-3 sm:px-4 md:px-5">
<header className="acrylic-navigation pointer-events-auto absolute left-0 right-0 top-0 z-30 flex h-14 select-none items-center justify-between border-b px-3 sm:px-4 md:px-5">
<div className="flex min-w-0 items-center gap-2.5">
<div className={cn("grid h-8 w-8 shrink-0 place-items-center bg-blue-600 text-white shadow-lg shadow-blue-600/20", MAP_ICON_CELL_RADIUS_CLASS_NAME)}>
<Droplets size={18} aria-hidden="true" />