144 lines
3.9 KiB
TypeScript
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(" ");
|
|
}
|