Files
next-tjwater-frontend/src/features/map/core/components/control-panel.tsx
T
jiang 0ae0a41e61
Generic Container CI/CD / test-build-publish (push) Successful in 2m7s
Frontend CI/CD / build-test-publish-and-deploy (push) Successful in 2m8s
feat(charts): manage investigation charts in a scrolling workspace
2026-11-10 17:56:23 +08:00

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>
);
}