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.
124 lines
3.9 KiB
TypeScript
124 lines
3.9 KiB
TypeScript
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>
|
||
);
|
||
}
|