Files
next-tjwater-frontend/src/features/map/core/components/toolbar.tsx
T
jiang 617ba4d87c 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.
2026-10-09 10:57:06 +08:00

124 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { LucideIcon } from "lucide-react";
import { cn } from "@/shared/ui/cn";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger
} from "@/shared/ui/tooltip";
import {
MAP_CONTROL_HOVER_CLASS_NAME,
MAP_CONTROL_RADIUS_CLASS_NAME,
MAP_ICON_CELL_RADIUS_CLASS_NAME,
MAP_CONTROL_SURFACE_CLASS_NAME
} from "./map-control-styles";
export type MapToolbarItem = {
id: string;
label: string;
description?: string;
icon: LucideIcon;
active?: boolean;
disabled?: boolean;
badge?: string | number;
onClick?: () => void;
};
type MapToolbarProps = {
items: MapToolbarItem[];
label?: string;
orientation?: "vertical" | "horizontal";
className?: string;
};
export function MapToolbar({
items,
label = "地图工具",
orientation = "vertical",
className = ""
}: MapToolbarProps) {
const isVertical = orientation === "vertical";
return (
<TooltipProvider delayDuration={180}>
<nav
aria-label={label}
className={cn(
"pointer-events-auto p-1",
MAP_CONTROL_RADIUS_CLASS_NAME,
MAP_CONTROL_SURFACE_CLASS_NAME,
isVertical ? "flex w-12 flex-col" : "inline-flex",
className
)}
>
{items.map((tool, index) => {
const Icon = tool.icon;
const title = tool.description ? `${tool.label}:${tool.description}` : tool.label;
return (
<div key={tool.id} className={cn(isVertical ? "contents" : "flex items-center")}>
{index > 0 ? <ToolbarDivider vertical={isVertical} /> : null}
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
aria-label={title}
aria-pressed={tool.active}
disabled={tool.disabled}
onClick={tool.onClick}
className={cn(
"relative grid h-10 w-10 shrink-0 place-items-center border border-transparent text-slate-700",
MAP_ICON_CELL_RADIUS_CLASS_NAME,
tool.active
? "border-[var(--action-blue)] bg-[var(--action-blue)] text-white shadow-[var(--action-shadow-primary)]"
: MAP_CONTROL_HOVER_CLASS_NAME,
tool.disabled && "text-[var(--ink-muted)] opacity-45 hover:bg-transparent hover:text-[var(--ink-muted)] hover:ring-0"
)}
>
<Icon size={20} strokeWidth={2.15} aria-hidden="true" />
<span className="sr-only">{tool.label}</span>
{tool.badge ? (
<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}
</button>
</TooltipTrigger>
<TooltipContent
side={isVertical ? "left" : "top"}
sideOffset={8}
className="border border-[var(--surface-divider)] bg-[var(--surface-reading)] px-2.5 py-1.5 text-xs font-medium text-slate-800 shadow-lg"
>
{title}
</TooltipContent>
</Tooltip>
</div>
);
})}
</nav>
</TooltipProvider>
);
}
function ToolbarDivider({ vertical }: { vertical: boolean }) {
if (vertical) {
return (
<div
className="my-0.5 flex h-px w-full items-center justify-center"
aria-hidden="true"
>
<span className="block h-px w-3.5 bg-slate-300/70" />
</div>
);
}
return (
<div
className="mx-0.5 flex h-full w-px items-center justify-center"
aria-hidden="true"
>
<span className="block h-3.5 w-px bg-slate-300/70" />
</div>
);
}