Files
watermaps/apps/web/src/components/NavigationToolRail.tsx
T
BuTzZ 55ce94066e
Test and publish container images / test (push) Successful in 2m20s
Test and publish container images / publish (push) Failing after 4s
Bootssymbol hinzugefügt
2026-07-26 12:31:49 +02:00

144 lines
3.9 KiB
TypeScript

import {
Anchor,
Bell,
CloudSun,
Route as RouteIcon,
Ship,
type LucideIcon
} from "lucide-react";
export const NAVIGATION_TOOL_ORDER = [
"boat",
"anchor",
"conditions",
"upcoming",
"route"
] as const;
export type NavigationToolId = (typeof NAVIGATION_TOOL_ORDER)[number];
export type ActiveTool = NavigationToolId | null;
export type NavigationToolStatus = "idle" | "active" | "caution" | "alarm" | "stale";
type NavigationToolDefinition = {
id: NavigationToolId;
label: string;
Icon: LucideIcon;
};
const NAVIGATION_TOOLS: readonly NavigationToolDefinition[] = [
{ id: "boat", label: "Mein Boot", Icon: Ship },
{ id: "anchor", label: "Ankerwache", Icon: Anchor },
{ id: "conditions", label: "Wetter und Tide", Icon: CloudSun },
{ id: "upcoming", label: "Als Nächstes", Icon: Bell },
{ id: "route", label: "Route", Icon: RouteIcon }
];
export type NavigationToolRailProps = {
activeTool: ActiveTool;
onSelect: (tool: NavigationToolId) => void;
statuses?: Partial<Record<NavigationToolId, NavigationToolStatus>>;
badges?: Partial<Record<NavigationToolId, number | string | null>>;
disabledTools?: Partial<Record<NavigationToolId, boolean>>;
workspaceId?: string;
className?: string;
ariaLabel?: string;
};
export function NavigationToolRail({
activeTool,
onSelect,
statuses = {},
badges = {},
disabledTools = {},
workspaceId = "navigation-workspace",
className,
ariaLabel = "Navigationswerkzeuge"
}: NavigationToolRailProps) {
return (
<nav className={classNames("navigation-tool-rail", className)} aria-label={ariaLabel}>
{NAVIGATION_TOOLS.map(({ id, label, Icon }) => {
const active = activeTool === id;
const status = statuses[id] ?? "idle";
const badge = formatBadge(badges[id]);
const accessibleLabel = toolAriaLabel(label, status, badge, active);
return (
<button
key={id}
className="navigation-tool-rail-button"
type="button"
disabled={Boolean(disabledTools[id])}
data-tool={id}
data-status={status}
data-active={active}
aria-controls={workspaceId}
aria-expanded={active}
aria-pressed={active}
aria-label={accessibleLabel}
title={accessibleLabel}
onClick={() => onSelect(id)}
>
<Icon size={21} aria-hidden="true" />
<span className="navigation-tool-rail-label" aria-hidden="true">
{label}
</span>
{badge && (
<span className="navigation-tool-rail-badge" aria-hidden="true">
{badge}
</span>
)}
</button>
);
})}
</nav>
);
}
export function navigationToolLabel(tool: NavigationToolId) {
return NAVIGATION_TOOLS.find((definition) => definition.id === tool)?.label ?? tool;
}
function toolAriaLabel(
label: string,
status: NavigationToolStatus,
badge: string | null,
active: boolean
) {
const parts = [label, statusLabel(status)];
if (badge) {
parts.push(`${badge} Hinweise`);
}
parts.push(active ? "geöffnet" : "öffnen");
return parts.join(", ");
}
function statusLabel(status: NavigationToolStatus) {
switch (status) {
case "active":
return "aktiv";
case "caution":
return "Warnung";
case "alarm":
return "Alarm";
case "stale":
return "Daten veraltet";
default:
return "bereit";
}
}
function formatBadge(value: number | string | null | undefined) {
if (typeof value === "number") {
if (!Number.isFinite(value) || value <= 0) {
return null;
}
return value > 99 ? "99+" : String(Math.floor(value));
}
const normalized = value?.trim();
return normalized || null;
}
function classNames(...values: Array<string | null | undefined | false>) {
return values.filter(Boolean).join(" ");
}