Files
next-tjwater-drainage-frontend/features/map/core/components/control-panel.tsx
T

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" : "surface-control 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"
: "surface-control border-transparent 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>
);
}