"use client"; import { useEffect, useRef, useState, type ReactNode } from "react"; import { Hash, ListTree, X } from "lucide-react"; type DrawerSide = "left" | "right"; export interface RailDrawerSide { /** 触发按钮与抽屉标题 */ label: string; /** 抽屉内容(服务端渲染节点) */ content: ReactNode; /** 按钮图标(缺省:左 ListTree / 右 Hash) */ icon?: ReactNode; } /** * 抽屉开合副作用:ESC 关闭 + 锁定背景滚动。 * onClose 经 ref 引用,避免回调身份变化导致反复装卸监听与滚动锁泄漏。 */ export function useDrawerChrome(open: boolean, onClose: () => void) { const closeRef = useRef(onClose); closeRef.current = onClose; useEffect(() => { if (!open) return; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") closeRef.current(); }; window.addEventListener("keydown", onKey); const prev = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { window.removeEventListener("keydown", onKey); document.body.style.overflow = prev; }; }, [open]); } /** * 单侧抽屉面板(lg 以下生效):遮罩 + 侧滑面板。 * - 关闭时保留 DOM 以获得滑出过渡,invisible 延迟切换保证动画完整; * - 抽屉内点击链接(锚点 / 跳转)后自动收起。 */ export function RailDrawerPanel({ side, title, open, onClose, children, }: { side: DrawerSide; title: string; open: boolean; onClose: () => void; children: ReactNode; }) { return (
); } /** * 通用移动端侧栏抽屉(lg 以下生效,桌面仍用常驻侧栏): * - 吸顶工具栏贴在全局 Header 下方,左右各一个抽屉触发按钮,中间可放页面上文; * - 左右两侧内容均可选,两侧皆空时整体不渲染; * - 渐进增强:ESC 关闭、背景滚动锁定、点链接自动收起。 */ export default function MobileRailDrawers({ left, right, center, }: { /** 左抽屉(对应桌面左栏) */ left?: RailDrawerSide; /** 右抽屉(对应桌面右栏) */ right?: RailDrawerSide; /** 工具栏中间内容(如当前文章标题),可省略 */ center?: ReactNode; }) { const [open, setOpen] = useState(null); useDrawerChrome(open !== null, () => setOpen(null)); if (!left && !right) return null; const btn = "relative inline-flex items-center gap-1 rounded-lg px-2.5 py-1.5 text-[12.5px] font-medium shrink-0 transition-colors hover:bg-[var(--panel-2)] active:scale-[0.97] " + // 触控热区上下外扩 8px(视觉不变),移动端更易点中 "before:absolute before:inset-x-0 before:-inset-y-2 before:content-['']"; const sides: { side: DrawerSide; cfg?: RailDrawerSide; fallbackIcon: ReactNode }[] = [ { side: "left", cfg: left, fallbackIcon: }, { side: "right", cfg: right, fallbackIcon: }, ]; return ( <> {/* 吸顶工具栏:与书籍阅读页一致的三段布局——左钮贴左、中间文字居中、右钮贴右; 有中间文字而缺某侧按钮时以等宽占位保持文字不偏心 */}
{left ? ( ) : center ? ( ) : null} {center ?
{center}
: null} {right ? ( ) : center ? ( ) : null}
{sides.map(({ side, cfg }) => cfg ? ( setOpen(null)} > {cfg.content} ) : null )} ); }