220 lines
7.0 KiB
TypeScript
220 lines
7.0 KiB
TypeScript
import type { LucideIcon } from "lucide-react";
|
|
import type { ReactNode } from "react";
|
|
import { cn } from "@/shared/ui/cn";
|
|
import {
|
|
mapOptionInteractionVariants,
|
|
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;
|
|
contentClassName?: string;
|
|
visible?: boolean;
|
|
children: ReactNode;
|
|
};
|
|
|
|
export function MapControlPanel({
|
|
title,
|
|
description,
|
|
icon: Icon,
|
|
widthClassName = "w-[292px]",
|
|
contentClassName,
|
|
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-[var(--action-selection-soft)] text-[var(--action-blue-hover)]", 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-[var(--ink-muted)]">{description}</p> : null}
|
|
</div>
|
|
</div>
|
|
<div className={cn("relative space-y-3", contentClassName)}>{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-[var(--ink-muted)]">{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"
|
|
data-map-option=""
|
|
aria-pressed={active}
|
|
disabled={disabled}
|
|
onClick={onClick}
|
|
className={cn(
|
|
"flex h-14 flex-col items-center justify-center gap-1 border border-transparent text-xs font-semibold",
|
|
MAP_COMPACT_RADIUS_CLASS_NAME,
|
|
active ? "bg-[var(--action-blue)] text-white" : "surface-control text-slate-600",
|
|
mapOptionInteractionVariants({ state: active ? "selected" : "default" })
|
|
)}
|
|
>
|
|
<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"
|
|
data-map-option=""
|
|
disabled={disabled}
|
|
onClick={onClick}
|
|
className={cn(
|
|
"flex h-10 items-center justify-center gap-1.5 text-xs font-semibold text-slate-700",
|
|
MAP_COMPACT_RADIUS_CLASS_NAME,
|
|
MAP_READABLE_SURFACE_CLASS_NAME,
|
|
mapOptionInteractionVariants()
|
|
)}
|
|
>
|
|
<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"
|
|
data-map-option=""
|
|
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",
|
|
MAP_COMPACT_RADIUS_CLASS_NAME,
|
|
isPrimary
|
|
? "border-[var(--action-blue)] bg-[var(--action-blue)] text-white shadow-lg shadow-[color:var(--action-blue)]/25"
|
|
: strong
|
|
? "border-[var(--surface-border)] bg-[var(--action-soft)]/95 text-[var(--action-blue-pressed)]"
|
|
: "surface-control border-transparent text-slate-700",
|
|
selected && !isPrimary && "border-[var(--surface-border)] bg-[var(--action-soft)] text-[var(--action-blue-pressed)]",
|
|
mapOptionInteractionVariants({ state: isDanger ? "danger" : isPrimary ? "primary" : selected || strong ? "selected" : "default" }),
|
|
muted && "opacity-70",
|
|
disabled && "shadow-none"
|
|
)}
|
|
>
|
|
<span
|
|
className={cn(
|
|
"grid h-8 w-8 shrink-0 place-items-center",
|
|
MAP_ICON_CELL_RADIUS_CLASS_NAME,
|
|
isPrimary
|
|
? "bg-[var(--surface-reading)]/16 text-white"
|
|
: selected || strong
|
|
? "bg-[var(--action-blue)] text-white"
|
|
: "bg-[var(--surface-control)] text-inherit",
|
|
isDanger && "bg-[var(--status-danger-soft)] text-[var(--status-danger-foreground)]"
|
|
)}
|
|
>
|
|
<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-[var(--palette-paper)]" : "text-[var(--ink-muted)]")}>{description}</span>
|
|
</span>
|
|
{status ? (
|
|
<span
|
|
className={cn(
|
|
"shrink-0 rounded-full px-2 py-0.5 text-xs font-semibold",
|
|
isPrimary ? "bg-[var(--surface-reading)]/15 text-white" : "bg-[var(--surface-control)] text-[var(--ink-muted)]"
|
|
)}
|
|
>
|
|
{status}
|
|
</span>
|
|
) : null}
|
|
</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-[var(--ink-muted)]">{label}</div>
|
|
<div className="mt-1 truncate text-sm font-semibold text-slate-900">{value}</div>
|
|
</div>
|
|
);
|
|
}
|