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
@@ -8,6 +8,7 @@ import {
MAP_MAJOR_PANEL_RADIUS_CLASS_NAME,
MAP_READABLE_RADIUS_CLASS_NAME,
MAP_READABLE_SURFACE_CLASS_NAME,
MAP_SELECTION_CARD_FOCUS_CLASS_NAME,
MAP_TOOL_PANEL_SURFACE_CLASS_NAME
} from "./map-control-styles";
@@ -85,12 +86,12 @@ export function BaseLayersControl({
)}
>
<div className="flex items-center gap-2 px-1">
<span className={cn("grid h-8 w-8 shrink-0 place-items-center bg-blue-50 text-blue-700", MAP_ICON_CELL_RADIUS_CLASS_NAME)}>
<span className={cn("grid h-8 w-8 shrink-0 place-items-center bg-[var(--action-soft)] text-[var(--action-blue-hover)]", MAP_ICON_CELL_RADIUS_CLASS_NAME)}>
<Map size={17} aria-hidden="true" />
</span>
<div className="min-w-0">
<h2 className="truncate text-sm font-semibold leading-tight text-slate-950">{title}</h2>
<p className="mt-0.5 truncate text-xs text-slate-500">
<p className="mt-0.5 truncate text-xs text-[var(--ink-muted)]">
当前 {activeLayer?.label ?? "--"}
</p>
</div>
@@ -110,10 +111,11 @@ export function BaseLayersControl({
className={cn(
"flex min-h-[76px] flex-col p-1 text-left transition duration-150",
MAP_READABLE_RADIUS_CLASS_NAME,
MAP_SELECTION_CARD_FOCUS_CLASS_NAME,
MAP_READABLE_SURFACE_CLASS_NAME,
active
? "border-blue-500 text-blue-700 shadow-xs ring-2 ring-blue-100"
: "text-slate-700 hover:border-blue-300 hover:bg-blue-50/40",
? "border-[var(--action-blue)] text-[var(--action-blue-hover)] shadow-xs"
: "text-slate-700 hover:border-[var(--palette-brand)] hover:bg-[var(--action-soft)]/40",
option.disabled && "cursor-not-allowed opacity-50"
)}
>
@@ -163,8 +165,9 @@ export function BaseLayersControl({
className={cn(
"flex h-[84px] w-[84px] flex-col p-1 text-center transition duration-150",
MAP_READABLE_RADIUS_CLASS_NAME,
MAP_SELECTION_CARD_FOCUS_CLASS_NAME,
MAP_READABLE_SURFACE_CLASS_NAME,
active ? "border-blue-500 shadow-xs ring-2 ring-blue-100" : "border-slate-200 hover:border-blue-300 hover:bg-blue-50/40",
active ? "border-[var(--action-blue)] shadow-xs" : "border-[var(--surface-divider)] hover:border-[var(--palette-brand)] hover:bg-[var(--action-soft)]/40",
option.disabled && "cursor-not-allowed opacity-50"
)}
>
@@ -184,11 +187,11 @@ export function BaseLayersControl({
type="button"
aria-label={`${title}:当前 ${activeLayer?.label ?? ""}`}
title={`${title}:当前 ${activeLayer?.label ?? ""}`}
className={cn("grid h-[84px] w-[84px] place-items-center p-1 text-slate-700 shadow-xs transition hover:border-blue-300 hover:text-blue-700", MAP_READABLE_RADIUS_CLASS_NAME, MAP_READABLE_SURFACE_CLASS_NAME)}
className={cn("grid h-[84px] w-[84px] place-items-center p-1 text-slate-700 shadow-xs transition hover:border-[var(--palette-brand)] hover:text-[var(--action-blue-hover)]", MAP_READABLE_RADIUS_CLASS_NAME, MAP_READABLE_SURFACE_CLASS_NAME, MAP_SELECTION_CARD_FOCUS_CLASS_NAME)}
>
<span className={cn("relative h-full w-full overflow-hidden border border-white shadow-inner", MAP_ICON_CELL_RADIUS_CLASS_NAME)}>
{activeLayer ? (
<span className={cn("absolute inset-0", activeLayer.swatchClassName ?? "bg-slate-100")} />
<span className={cn("absolute inset-0", activeLayer.swatchClassName ?? "bg-[var(--surface-control)]")} />
) : (
<Map size={18} aria-hidden="true" className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
)}
@@ -213,11 +216,11 @@ function BaseLayerSwatch({ option, active }: BaseLayerSwatchProps) {
className={cn(
"relative block min-h-0 flex-1 overflow-hidden border border-white shadow-inner",
MAP_ICON_CELL_RADIUS_CLASS_NAME,
option.swatchClassName ?? "bg-slate-100"
option.swatchClassName ?? "bg-[var(--surface-control)]"
)}
>
{active ? (
<span className="absolute right-0.5 top-0.5 grid h-4 w-4 place-items-center rounded-full bg-blue-600 text-white shadow-xs">
<span className="absolute right-0.5 top-0.5 grid h-4 w-4 place-items-center rounded-full bg-[var(--action-blue)] text-white shadow-xs">
<Check size={10} aria-hidden="true" />
</span>
) : null}