feat(ui): refine gem-blue acrylic palette and focus borders

Derive the gem-blue palette with OKLCH tonal and complementary-color
rules while preserving acrylic opacity, blur and surface hierarchy.

Darker focus tokens exposed stacked field rings, fallback outlines and
persistent basemap selection rings. Keep fallback focus styles in the
base layer and use one themed field boundary or inset card stroke.
Preserve keyboard focus, invalid states and native input semantics.

Add browser regression coverage for pointer and keyboard focus, all
basemap presentations, narrow layouts and rendered contrast.

Validation in an isolated checkout: pnpm lint, pnpm typecheck,
pnpm test (393 tests), focused browser suites and pnpm build.
This commit is contained in:
2026-10-09 10:57:06 +08:00
parent d2d705ed4c
commit 617ba4d87c
69 changed files with 1239 additions and 765 deletions
@@ -51,10 +51,10 @@ const FEATURE_LAYER_META: Partial<Record<
}
>> = {
pipes: { icon: PipeFeatureIcon, label: "管线", headerClassName: "bg-teal-50/70", labelClassName: "text-teal-800", iconClassName: "bg-teal-700 text-white ring-teal-900/10" },
junctions: { icon: JunctionFeatureIcon, label: "节点", headerClassName: "bg-blue-50/70", labelClassName: "text-blue-800", iconClassName: "bg-sky-700 text-white ring-sky-900/10" },
valves: { icon: ValveFeatureIcon, label: "阀门", headerClassName: "bg-violet-50/70", labelClassName: "text-violet-800", iconClassName: "bg-violet-600 text-white ring-violet-900/10" },
junctions: { icon: JunctionFeatureIcon, label: "节点", headerClassName: "bg-[var(--action-soft)]/70", labelClassName: "text-[var(--action-blue-pressed)]", iconClassName: "bg-sky-700 text-white ring-sky-900/10" },
valves: { icon: ValveFeatureIcon, label: "阀门", headerClassName: "bg-[var(--action-soft)]/70", labelClassName: "text-[var(--action-blue-pressed)]", iconClassName: "bg-[var(--action-blue)] text-white ring-[var(--action-blue)]/10" },
reservoirs: { icon: ReservoirFeatureIcon, label: "水库", headerClassName: "bg-sky-50/70", labelClassName: "text-sky-900", iconClassName: "bg-sky-800 text-white ring-sky-900/10" },
scada: { icon: ScadaFeatureIcon, label: "SCADA", headerClassName: "bg-slate-50/80", labelClassName: "text-slate-800", iconClassName: "bg-slate-700 text-white ring-slate-900/10" },
scada: { icon: ScadaFeatureIcon, label: "SCADA", headerClassName: "bg-[var(--surface-reading)]/80", labelClassName: "text-slate-800", iconClassName: "bg-slate-700 text-white ring-slate-900/10" },
pumps: { icon: PumpFeatureIcon, label: "泵", headerClassName: "bg-cyan-50/70", labelClassName: "text-cyan-800", iconClassName: "bg-cyan-800 text-white ring-cyan-900/10" },
tanks: { icon: ReservoirFeatureIcon, label: "水箱", headerClassName: "bg-teal-50/70", labelClassName: "text-teal-800", iconClassName: "bg-teal-700 text-white ring-teal-900/10" },
water_quality_monitoring_points: { icon: ScadaFeatureIcon, label: "水质监测点", headerClassName: "bg-teal-50/70", labelClassName: "text-teal-800", iconClassName: "bg-teal-700 text-white ring-teal-900/10" }
@@ -63,7 +63,7 @@ const FEATURE_LAYER_META: Partial<Record<
const SOURCE_GIS_LAYER_META = {
icon: JunctionFeatureIcon,
label: "GIS 交付要素",
headerClassName: "bg-slate-50/80",
headerClassName: "bg-[var(--surface-reading)]/80",
labelClassName: "text-slate-700",
iconClassName: "bg-slate-600 text-white ring-slate-900/10"
};
@@ -164,12 +164,12 @@ export function FeaturePopover({
<div className="flex items-center gap-0.5">
{canCopy ? (
<button type="button" aria-label={copied ? "编号已复制" : "复制编号"} title={copied ? "编号已复制" : "复制编号"} onClick={() => void handleCopyId()} className="relative grid h-10 w-10 shrink-0 place-items-center rounded-xl text-slate-500 [@media(hover:hover)]:hover:bg-white/70 [@media(hover:hover)]:hover:text-slate-950">
<button type="button" aria-label={copied ? "编号已复制" : "复制编号"} title={copied ? "编号已复制" : "复制编号"} onClick={() => void handleCopyId()} className="relative grid h-10 w-10 shrink-0 place-items-center rounded-xl text-[var(--ink-muted)] [@media(hover:hover)]:hover:bg-[var(--surface-reading)]/70 [@media(hover:hover)]:hover:text-slate-950">
<Copy size={15} aria-hidden="true" className={cn("absolute transition-[opacity,transform] duration-150", copied ? "scale-90 opacity-0" : "scale-100 opacity-100")} />
<Check size={16} aria-hidden="true" className={cn("status-tone-success absolute text-[var(--status-foreground)] transition-[opacity,transform] duration-150", copied ? "scale-100 opacity-100" : "scale-90 opacity-0")} />
</button>
) : null}
<button type="button" aria-label="关闭要素信息" title="关闭要素信息" onClick={onClose} className="grid h-10 w-10 shrink-0 place-items-center rounded-xl text-slate-500 [@media(hover:hover)]:hover:bg-white/70 [@media(hover:hover)]:hover:text-slate-950">
<button type="button" aria-label="关闭要素信息" title="关闭要素信息" onClick={onClose} className="grid h-10 w-10 shrink-0 place-items-center rounded-xl text-[var(--ink-muted)] [@media(hover:hover)]:hover:bg-[var(--surface-reading)]/70 [@media(hover:hover)]:hover:text-slate-950">
<X size={16} aria-hidden="true" />
</button>
</div>
@@ -179,20 +179,20 @@ export function FeaturePopover({
<div className="min-h-0 overflow-y-auto overscroll-contain px-4 py-3">
{panelModel.attributes.length > 0 ? (
<>
<div className="mb-2 flex items-center gap-2 text-xs font-semibold text-slate-500">
<div className="mb-2 flex items-center gap-2 text-xs font-semibold text-[var(--ink-muted)]">
<Database size={14} aria-hidden="true" />
设施属性
</div>
<FeaturePropertyList entries={panelModel.attributes} variant="popover" />
</>
) : (
<p className="py-3 text-center text-sm text-slate-500">暂无可展示属性</p>
<p className="py-3 text-center text-sm text-[var(--ink-muted)]">暂无可展示属性</p>
)}
{gisProperties.groups.length > 0 || gisProperties.status === "error" ? (
<Disclosure
key={featureKey}
className="mt-2 border-t border-slate-100 pt-1"
className="mt-2 border-t border-[var(--surface-divider)] pt-1"
onToggle={(event) => {
if (event.target === event.currentTarget) {
setExpandedGisFeatureKey(event.currentTarget.open ? featureKey : null);
@@ -201,7 +201,7 @@ export function FeaturePopover({
label={(
<span className="flex min-w-0 flex-1 items-center justify-between gap-3">
<span>GIS 台账属性</span>
<span className="shrink-0 text-[11px] font-normal text-slate-400">
<span className="shrink-0 text-[11px] font-normal text-[var(--ink-muted)]">
{gisProperties.status === "error"
? "读取失败"
: `${gisProperties.groups.length} 份`}
@@ -217,30 +217,30 @@ export function FeaturePopover({
return (
<Disclosure
key={`${group.layer_key}:${group.source_fid}`}
className="border-b border-slate-100 last:border-b-0"
className="border-b border-[var(--surface-divider)] last:border-b-0"
label={(
<span className="min-w-0">
<span className="flex min-w-0 items-center gap-2">
<span className="truncate text-slate-700">{group.layer_name}</span>
<span className="shrink-0 rounded-full bg-slate-100 px-1.5 py-0.5 text-[10px] font-medium text-slate-500">
<span className="shrink-0 rounded-full bg-[var(--surface-control)] px-1.5 py-0.5 text-[10px] font-medium text-[var(--ink-muted)]">
{presentation.datasetRole}
</span>
</span>
<span className="block text-[11px] font-normal text-slate-400 tabular-nums">
<span className="block text-[11px] font-normal text-[var(--ink-muted)] tabular-nums">
{group.source_date} · {presentation.usefulCount} 个有效字段
</span>
</span>
)}
>
<div className="space-y-2 pb-3 pl-6">
<p className="text-[11px] leading-5 text-slate-500">
<p className="text-[11px] leading-5 text-[var(--ink-muted)]">
{presentation.description}
</p>
{presentation.summary.length > 0 ? (
<dl className="grid grid-cols-2 gap-1.5 sm:grid-cols-3">
{presentation.summary.map((entry) => (
<div key={entry.key} className="min-w-0 rounded-lg border border-slate-100 bg-white/70 px-2.5 py-2">
<dt className="truncate text-[10px] font-medium text-slate-400">{entry.label}</dt>
<div key={entry.key} className="min-w-0 rounded-lg border border-[var(--surface-divider)] bg-[var(--surface-reading)]/70 px-2.5 py-2">
<dt className="truncate text-[10px] font-medium text-[var(--ink-muted)]">{entry.label}</dt>
<dd className="mt-0.5 truncate text-xs font-semibold text-slate-800" title={entry.value}>{entry.value}</dd>
</div>
))}
@@ -249,7 +249,7 @@ export function FeaturePopover({
{presentation.sections.map((section) => (
<Disclosure
key={section.id}
className="border-t border-slate-100"
className="border-t border-[var(--surface-divider)]"
label={`${section.label} · ${section.entries.length} 项`}
>
<div className="pb-2 pl-6">
@@ -258,11 +258,11 @@ export function FeaturePopover({
</Disclosure>
))}
<Disclosure
className="border-t border-slate-100"
className="border-t border-[var(--surface-divider)]"
label={`全部原始字段 · ${presentation.rawCount} 项`}
>
<div className="pb-2 pl-6">
<p className="pb-2 text-[11px] leading-5 text-slate-400">
<p className="pb-2 text-[11px] leading-5 text-[var(--ink-muted)]">
包含源系统编码、空值和兼容字段,仅用于核对原始数据。
</p>
<FeaturePropertyList entries={presentation.rawEntries} variant="popover" />