feat: initialize drainage network frontend
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user