"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
)}
>
);
}