146 lines
3.2 KiB
TypeScript
146 lines
3.2 KiB
TypeScript
import { AlertTriangle, 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}>
|
|
<MapNoticeIcon tone={tone} icon={Icon} />
|
|
<MapNoticeBody
|
|
title={title}
|
|
message={message}
|
|
action={
|
|
actionLabel && onAction ? (
|
|
<MapNoticeAction label={actionLabel} onClick={onAction} />
|
|
) : 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"}
|
|
className={cn(
|
|
"glass-transient relative flex w-full items-start gap-2.5 overflow-hidden border py-2.5 pl-3 pr-10 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,
|
|
action
|
|
}: {
|
|
title?: string;
|
|
message: string;
|
|
action?: ReactNode;
|
|
}) {
|
|
return (
|
|
<span className="min-w-0 flex-1">
|
|
{title ? (
|
|
<span className="block text-sm font-semibold leading-5 text-slate-950">{title}</span>
|
|
) : null}
|
|
<span className={cn("block text-sm leading-5 text-slate-600", title && "mt-0.5")}>
|
|
{message}
|
|
</span>
|
|
{action}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
function MapNoticeAction({ label, onClick }: { label: string; onClick: () => void }) {
|
|
return (
|
|
<Button
|
|
type="button"
|
|
onClick={onClick}
|
|
size="sm"
|
|
variant="outline"
|
|
className={cn("mt-2 border-blue-100 bg-blue-50 text-blue-700", MAP_COMPACT_RADIUS_CLASS_NAME)}
|
|
>
|
|
{label}
|
|
</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;
|
|
}
|