220 lines
7.0 KiB
TypeScript
220 lines
7.0 KiB
TypeScript
import { ChevronRight, type LucideIcon } from "lucide-react";
|
|
import type { ReactNode } from "react";
|
|
import { cn } from "@/lib/utils";
|
|
import {
|
|
MAP_COMPACT_RADIUS_CLASS_NAME,
|
|
MAP_ICON_CELL_RADIUS_CLASS_NAME,
|
|
MAP_MAJOR_PANEL_RADIUS_CLASS_NAME,
|
|
MAP_READABLE_RADIUS_CLASS_NAME,
|
|
MAP_READABLE_SURFACE_CLASS_NAME,
|
|
MAP_READABLE_SURFACE_STRONG_CLASS_NAME,
|
|
MAP_TOOL_PANEL_SURFACE_CLASS_NAME
|
|
} from "./map-control-styles";
|
|
|
|
export type MapControlPanelProps = {
|
|
title: string;
|
|
description?: string;
|
|
icon: LucideIcon;
|
|
widthClassName?: string;
|
|
visible?: boolean;
|
|
children: ReactNode;
|
|
};
|
|
|
|
export function MapControlPanel({
|
|
title,
|
|
description,
|
|
icon: Icon,
|
|
widthClassName = "w-[292px]",
|
|
visible = true,
|
|
children
|
|
}: MapControlPanelProps) {
|
|
return (
|
|
<section
|
|
aria-label={title}
|
|
aria-hidden={!visible}
|
|
className={cn(
|
|
"relative origin-top-right p-3 transition-[opacity,transform] duration-150 ease-out motion-reduce:transition-none",
|
|
visible
|
|
? "pointer-events-auto opacity-100 translate-x-0 scale-100 motion-safe:animate-in motion-safe:fade-in-0 motion-safe:slide-in-from-right-2 motion-safe:zoom-in-95 motion-safe:duration-150 motion-safe:ease-out"
|
|
: "pointer-events-none translate-x-2 scale-[0.98] opacity-0",
|
|
MAP_MAJOR_PANEL_RADIUS_CLASS_NAME,
|
|
MAP_TOOL_PANEL_SURFACE_CLASS_NAME,
|
|
widthClassName
|
|
)}
|
|
>
|
|
<div className={cn("relative mb-3 flex items-center gap-2.5 px-2.5 py-2", MAP_READABLE_RADIUS_CLASS_NAME, MAP_READABLE_SURFACE_CLASS_NAME)}>
|
|
<span className={cn("grid h-8 w-8 shrink-0 place-items-center bg-blue-100 text-blue-700", MAP_ICON_CELL_RADIUS_CLASS_NAME)}>
|
|
<Icon 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>
|
|
{description ? <p className="mt-0.5 truncate text-xs text-slate-500">{description}</p> : null}
|
|
</div>
|
|
</div>
|
|
<div className="relative space-y-3">{children}</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
export function MapPanelSection({ title, children }: { title: string; children: ReactNode }) {
|
|
return (
|
|
<section>
|
|
<div className="mb-1.5 px-1 text-xs font-semibold text-slate-500">{title}</div>
|
|
<div className="space-y-1.5">{children}</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
export type MapModeButtonProps = {
|
|
icon: LucideIcon;
|
|
label: string;
|
|
active?: boolean;
|
|
disabled?: boolean;
|
|
onClick?: () => void;
|
|
};
|
|
|
|
export function MapModeButton({ icon: Icon, label, active = false, disabled = false, onClick }: MapModeButtonProps) {
|
|
return (
|
|
<button
|
|
type="button"
|
|
aria-pressed={active}
|
|
disabled={disabled}
|
|
onClick={onClick}
|
|
className={cn(
|
|
"flex h-14 flex-col items-center justify-center gap-1 text-xs font-semibold transition duration-150",
|
|
MAP_COMPACT_RADIUS_CLASS_NAME,
|
|
active ? "bg-blue-600 text-white shadow-sm" : "bg-white/95 text-slate-600 hover:text-blue-700",
|
|
disabled && "cursor-not-allowed opacity-55 hover:text-slate-600"
|
|
)}
|
|
>
|
|
<Icon size={17} aria-hidden="true" />
|
|
<span>{label}</span>
|
|
</button>
|
|
);
|
|
}
|
|
|
|
export function MapActionChip({
|
|
icon: Icon,
|
|
label,
|
|
disabled = false,
|
|
onClick
|
|
}: {
|
|
icon: LucideIcon;
|
|
label: string;
|
|
disabled?: boolean;
|
|
onClick?: () => void;
|
|
}) {
|
|
return (
|
|
<button
|
|
type="button"
|
|
disabled={disabled}
|
|
onClick={onClick}
|
|
className={cn(
|
|
"flex h-10 items-center justify-center gap-1.5 text-xs font-semibold text-slate-700 transition duration-150 hover:border-blue-200 hover:text-blue-700",
|
|
MAP_COMPACT_RADIUS_CLASS_NAME,
|
|
MAP_READABLE_SURFACE_CLASS_NAME,
|
|
disabled && "cursor-not-allowed opacity-55 hover:border-white/70 hover:text-slate-700"
|
|
)}
|
|
>
|
|
<Icon size={14} aria-hidden="true" />
|
|
<span>{label}</span>
|
|
</button>
|
|
);
|
|
}
|
|
|
|
export type MapActionRowProps = {
|
|
icon: LucideIcon;
|
|
label: string;
|
|
description: string;
|
|
status?: string;
|
|
strong?: boolean;
|
|
variant?: "default" | "primary";
|
|
selected?: boolean;
|
|
muted?: boolean;
|
|
disabled?: boolean;
|
|
tone?: "default" | "danger";
|
|
onClick?: () => void;
|
|
};
|
|
|
|
export function MapActionRow({
|
|
icon: Icon,
|
|
label,
|
|
description,
|
|
status,
|
|
strong = false,
|
|
variant = "default",
|
|
selected = false,
|
|
muted = false,
|
|
disabled = false,
|
|
tone = "default",
|
|
onClick
|
|
}: MapActionRowProps) {
|
|
const isDanger = tone === "danger";
|
|
const isPrimary = variant === "primary";
|
|
|
|
return (
|
|
<button
|
|
type="button"
|
|
disabled={disabled}
|
|
onClick={onClick}
|
|
className={cn(
|
|
"flex min-h-12 w-full items-center gap-2.5 border px-2.5 py-2 text-left transition duration-150 active:translate-y-px active:scale-[0.99]",
|
|
MAP_COMPACT_RADIUS_CLASS_NAME,
|
|
isPrimary
|
|
? "border-blue-600 bg-blue-600 text-white shadow-lg shadow-blue-600/25 hover:bg-blue-700"
|
|
: strong
|
|
? "border-blue-100 bg-blue-50/95 text-blue-800"
|
|
: "border-white/70 bg-white/95 text-slate-700 hover:border-blue-100 hover:bg-white hover:text-blue-700",
|
|
isDanger && "hover:border-red-100 hover:text-red-700",
|
|
selected && !isPrimary && "border-blue-200 bg-blue-50 text-blue-800",
|
|
muted && "opacity-70",
|
|
disabled &&
|
|
(isPrimary
|
|
? "cursor-not-allowed opacity-55 hover:border-blue-600 hover:bg-blue-600 hover:text-white"
|
|
: "cursor-not-allowed opacity-55 hover:border-white/70 hover:bg-white/95 hover:text-slate-700"),
|
|
disabled && "active:translate-y-0 active:scale-100"
|
|
)}
|
|
>
|
|
<span
|
|
className={cn(
|
|
"grid h-8 w-8 shrink-0 place-items-center",
|
|
MAP_ICON_CELL_RADIUS_CLASS_NAME,
|
|
isPrimary
|
|
? "bg-white/16 text-white"
|
|
: selected || strong
|
|
? "bg-blue-600 text-white"
|
|
: "bg-slate-100 text-slate-500",
|
|
isDanger && "bg-red-50 text-red-600"
|
|
)}
|
|
>
|
|
<Icon size={15} aria-hidden="true" />
|
|
</span>
|
|
<span className="min-w-0 flex-1">
|
|
<span className="block truncate text-sm font-semibold">{label}</span>
|
|
<span className={cn("block truncate text-xs", isPrimary ? "text-blue-100" : "text-slate-500")}>{description}</span>
|
|
</span>
|
|
{status ? (
|
|
<span
|
|
className={cn(
|
|
"shrink-0 rounded-full px-2 py-0.5 text-xs font-semibold",
|
|
isPrimary ? "bg-white/15 text-white" : "bg-slate-100 text-slate-500"
|
|
)}
|
|
>
|
|
{status}
|
|
</span>
|
|
) : (
|
|
<ChevronRight size={15} aria-hidden="true" className={cn("shrink-0", isPrimary ? "text-blue-100" : "text-slate-400")} />
|
|
)}
|
|
</button>
|
|
);
|
|
}
|
|
|
|
export function MapMetricTile({ label, value }: { label: string; value: string }) {
|
|
return (
|
|
<div className={cn("px-2.5 py-2", MAP_READABLE_RADIUS_CLASS_NAME, MAP_READABLE_SURFACE_STRONG_CLASS_NAME)}>
|
|
<div className="text-xs text-slate-500">{label}</div>
|
|
<div className="mt-1 truncate text-sm font-semibold text-slate-900">{value}</div>
|
|
</div>
|
|
);
|
|
}
|