Files
next-tjwater-frontend/src/features/map/core/components/notice-presentation.tsx
T

150 lines
3.8 KiB
TypeScript

import { AlertTriangle, ArrowUpRight, CheckCircle2, Info, Loader2, X, XCircle } from "lucide-react";
import type { ReactNode } from "react";
import { cn } from "@/shared/ui/cn";
import { Button } from "@/shared/ui/button";
import {
StatusIcon as SemanticStatusIcon,
type StatusTone
} from "@/shared/ui/status";
import {
MAP_COMPACT_RADIUS_CLASS_NAME,
MAP_ICON_CELL_RADIUS_CLASS_NAME,
MAP_READABLE_RADIUS_CLASS_NAME
} from "./map-control-styles";
import type { MapNoticeOptions, MapNoticeTone } from "./notice-types";
const noticeTones = {
info: "info",
success: "success",
warning: "warning",
error: "danger",
loading: "info"
} satisfies Record<MapNoticeTone, StatusTone>;
export function MapNoticeContent({
title,
message,
tone = "info",
actionLabel,
onAction
}: MapNoticeOptions) {
const Icon = getNoticeIcon(tone);
return (
<MapNoticeFrame tone={tone}>
<div data-slot="map-notice-summary" className="flex items-start gap-3 py-3.5 pl-4 pr-12">
<MapNoticeIcon tone={tone} icon={Icon} />
<MapNoticeBody title={title} message={message} />
</div>
{actionLabel && onAction ? (
<div data-slot="map-notice-actions" className="flex flex-wrap items-center justify-between gap-x-3 gap-y-1 pb-3 pl-14 pr-3">
<span className="text-xs leading-5 text-[var(--ink-muted)]">保留至手动关闭</span>
<MapNoticeAction label={actionLabel} onClick={onAction} />
</div>
) : null}
</MapNoticeFrame>
);
}
export function MapNoticeCloseIcon() {
return (
<>
<span className="sr-only">关闭通知</span>
<X size={15} aria-hidden="true" />
</>
);
}
function MapNoticeFrame({ tone, children }: { tone: MapNoticeTone; children: ReactNode }) {
return (
<div
role={tone === "error" || tone === "warning" ? "alert" : "status"}
// Sonner restores its saved focus on blur. Persistent action buttons must
// let the user move directly to another workbench control instead.
onFocus={(event) => event.stopPropagation()}
className={cn(
"glass-transient relative w-full overflow-hidden border text-sm text-slate-900",
MAP_READABLE_RADIUS_CLASS_NAME,
)}
>
{children}
</div>
);
}
function MapNoticeIcon({
tone,
icon: Icon
}: {
tone: MapNoticeTone;
icon: ReturnType<typeof getNoticeIcon>;
}) {
return (
<SemanticStatusIcon
tone={noticeTones[tone]}
activity={tone === "loading" ? "loading" : "static"}
size="md"
className={cn("mt-0.5", MAP_ICON_CELL_RADIUS_CLASS_NAME)}
>
<Icon size={17} />
</SemanticStatusIcon>
);
}
function MapNoticeBody({
title,
message
}: {
title?: string;
message: string;
}) {
return (
<div className="min-w-0 flex-1">
{title ? (
<span className="block break-words text-sm font-semibold leading-5 text-slate-950">{title}</span>
) : null}
<span className={cn("block break-words text-sm leading-6 text-slate-600", title && "mt-1")}>
{message}
</span>
</div>
);
}
function MapNoticeAction({ label, onClick }: { label: string; onClick: () => void }) {
return (
<Button
type="button"
onClick={onClick}
size="sm"
variant="outline"
className={cn(
"pointer-events-auto h-9 max-w-full gap-1.5 whitespace-normal border-[var(--surface-border)] bg-[var(--action-soft)] px-2.5 text-xs text-[var(--action-blue-hover)]",
MAP_COMPACT_RADIUS_CLASS_NAME
)}
>
{label}
<ArrowUpRight size={14} aria-hidden="true" />
</Button>
);
}
function getNoticeIcon(tone: MapNoticeTone) {
if (tone === "success") {
return CheckCircle2;
}
if (tone === "warning") {
return AlertTriangle;
}
if (tone === "error") {
return XCircle;
}
if (tone === "loading") {
return Loader2;
}
return Info;
}