import { ArrowLeft } from "lucide-react"; import { createContext, type ReactNode, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { Button } from "~/components/ui/button.tsx"; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "~/components/ui/sheet.tsx"; import { cn } from "~/lib/utils.ts"; /** 桌面宽度:md ≈ 32rem,lg ≈ 44rem,xl ≈ 64rem(内容重的记录:订阅对比、站点);手机一律全宽 */ export type DetailSheetWidth = "md" | "lg" | "xl"; const WIDTH: Record = { md: "sm:max-w-lg", lg: "sm:max-w-2xl", xl: "sm:max-w-5xl", }; /** * 抽屉里再开的 DetailSheet 不叠第二层抽屉(console-ia.md §9):在同一个抽屉里换成子视图, * 顶部是「返回」,关掉子视图回到原来的内容(滚动位置不丢)。 */ type Nest = { host: HTMLElement | null; push: (close: () => void) => () => void; }; const NestContext = createContext(null); type Props = { open: boolean; onOpenChange: (open: boolean) => void; title: ReactNode; description?: ReactNode; /** 标题下面一行的操作按钮(记录抽屉的主操作、更多) */ actions?: ReactNode; children: ReactNode; footer?: ReactNode; width?: DetailSheetWidth; className?: string; }; /** * 右侧详情抽屉(手机全宽)。内容区自带纵向滚动;footer 固定在底部放操作按钮。 * !o && setId(null)} title="详情" description="…"> * * * 从列表打开记录时配合 useRecordSheet(record-sheet.tsx)把打开的记录写进地址栏。 */ export function DetailSheet(props: Props) { const nest = useContext(NestContext); if (nest) return ; return ; } function RootSheet({ open, onOpenChange, title, description, actions, children, footer, width = "md", className, }: Props) { const [host, setHost] = useState(null); const stack = useRef<(() => void)[]>([]); const [depth, setDepth] = useState(0); const push = useCallback((close: () => void) => { stack.current.push(close); setDepth(stack.current.length); return () => { stack.current = stack.current.filter((c) => c !== close); setDepth(stack.current.length); }; }, []); const nest = useMemo(() => ({ host, push }), [host, push]); return ( { e.preventDefault(); (e.currentTarget as HTMLElement | null)?.focus({ preventScroll: true }); }} onEscapeKeyDown={(e) => { // 有子视图时 Esc 先回到上一层 const top = stack.current.at(-1); if (top) { e.preventDefault(); top(); } }} >
0 && "invisible")} aria-hidden={depth > 0}> {children}
); } function SheetFrame({ title, description, actions, footer, children, back, }: Pick & { back?: () => void }) { return ( <> {back ? ( ) : null} {back ? ( // 子视图在同一个对话框里,标题不再占用对话框的 Title / Description <>

{title}

{description ?
{description}
: null} ) : ( <> {title} {description ? (
{description}
) : null} )} {actions ?
{actions}
: null}
{children}
{footer ? (
{footer}
) : null} ); } /** 抽屉里的子视图:渲染进外层抽屉的同一块区域,盖住原内容 */ function NestedView({ open, onOpenChange, nest, ...frame }: Props & { nest: Nest }) { const close = useRef(() => onOpenChange(false)); close.current = () => onOpenChange(false); const { push, host } = nest; useEffect(() => { if (!open) return; return push(() => close.current()); }, [open, push]); if (!open || !host) return null; return createPortal(
close.current()} />
, host, ); } /** 抽屉/卡片内的小节:大写小标签 + 内容 */ export function DetailSection({ title, children, actions, }: { title: string; children: ReactNode; actions?: ReactNode; }) { return (

{title}

{actions}
{children}
); }