diff --git a/src/features/agent/components/agent-command-panel.tsx b/src/features/agent/components/agent-command-panel.tsx index 4a65d9d..535571b 100644 --- a/src/features/agent/components/agent-command-panel.tsx +++ b/src/features/agent/components/agent-command-panel.tsx @@ -23,7 +23,7 @@ import { AnimatePresence, MotionConfig, motion, useReducedMotion } from "motion/ import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; import { createPortal } from "react-dom"; import { useStickToBottomContext } from "use-stick-to-bottom"; -import { showMapNotice } from "@/features/map/core/components/notice"; +import { showMapNotice } from "@/features/map/core/components/notice-actions"; import { Agent, AgentContent } from "@/shared/ai-elements/agent"; import { Conversation, diff --git a/src/features/map/core/components/notice-actions.tsx b/src/features/map/core/components/notice-actions.tsx new file mode 100644 index 0000000..548302e --- /dev/null +++ b/src/features/map/core/components/notice-actions.tsx @@ -0,0 +1,60 @@ +"use client"; + +import type { ComponentProps } from "react"; +import { toast, Toaster } from "sonner"; +import { cn } from "@/lib/utils"; +import { MAP_ICON_CELL_RADIUS_CLASS_NAME } from "./map-control-styles"; +import { MapNoticeCloseIcon, MapNoticeContent } from "./notice-presentation"; +import type { MapNoticeOptions, MapNoticePosition } from "./notice-types"; + +type SonnerPosition = NonNullable["position"]>; + +const DEFAULT_POSITION: MapNoticePosition = "top-center"; + +const positionClassNames: Record = { + "top-center": "left-1/2! right-auto! top-[84px] [translate:-50%_0]", + "top-right": "top-[84px] right-[72px]", + "bottom-left": "bottom-12 left-4", + "bottom-right": "bottom-12 right-4" +}; + +const sonnerPositions: Record = { + "top-center": "top-center", + "top-right": "top-right", + "bottom-left": "bottom-left", + "bottom-right": "bottom-right" +}; + +export function showMapNotice(options: MapNoticeOptions) { + const tone = options.tone ?? "info"; + const position = options.position ?? DEFAULT_POSITION; + const duration = options.duration ?? (tone === "info" || tone === "success" ? 3200 : Infinity); + const dismissible = options.dismissible ?? tone !== "loading"; + + return toast.custom( + () => , + { + id: options.id, + duration, + position: sonnerPositions[position], + dismissible, + cancel: dismissible + ? { + label: , + onClick: (event) => event.stopPropagation() + } + : undefined, + classNames: { + cancelButton: cn( + "absolute right-2 top-2 z-10 grid h-7 w-7 place-items-center border border-transparent bg-transparent p-0 text-slate-400 transition hover:border-slate-200 hover:bg-slate-50 hover:text-slate-700 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-600/25", + MAP_ICON_CELL_RADIUS_CLASS_NAME + ) + }, + className: cn("fixed! z-[60]! w-[min(420px,calc(100vw-24px))]!", positionClassNames[position]) + } + ); +} + +export function dismissMapNotice(id?: string | number) { + toast.dismiss(id); +} diff --git a/src/features/map/core/components/notice-presentation.tsx b/src/features/map/core/components/notice-presentation.tsx new file mode 100644 index 0000000..97a2b7c --- /dev/null +++ b/src/features/map/core/components/notice-presentation.tsx @@ -0,0 +1,154 @@ +"use client"; + +import { AlertTriangle, CheckCircle2, Info, Loader2, X, XCircle } 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_READABLE_RADIUS_CLASS_NAME +} from "./map-control-styles"; +import type { MapNoticeOptions, MapNoticeTone } from "./notice-types"; + +const toneClassNames: Record = { + info: "acrylic-panel text-slate-900", + success: "acrylic-panel text-slate-900", + warning: "acrylic-panel text-slate-900", + error: "acrylic-panel text-slate-900", + loading: "acrylic-panel text-slate-900" +}; + +const iconClassNames: Record = { + info: "text-blue-600", + success: "text-green-600", + warning: "text-orange-500", + error: "text-red-500", + loading: "text-blue-600" +}; + +const noticeAccentClassNames: Record = { + info: "bg-blue-600", + success: "bg-green-500", + warning: "bg-orange-500", + error: "bg-red-500", + loading: "bg-blue-600" +}; + +const noticeIconSurfaceClassNames: Record = { + info: "border-blue-100 bg-blue-50", + success: "border-green-100 bg-green-50", + warning: "border-orange-100 bg-orange-50", + error: "border-red-100 bg-red-50", + loading: "border-blue-100 bg-blue-50" +}; + +export function MapNoticeContent({ + title, + message, + tone = "info", + actionLabel, + onAction +}: MapNoticeOptions) { + const Icon = getNoticeIcon(tone); + + return ( + + + : null} + /> + + ); +} + +export function MapNoticeCloseIcon() { + return ( + <> + 关闭通知 +