diff --git a/web/src/components/Layout.tsx b/web/src/components/Layout.tsx index 14e7e79..126c32a 100644 --- a/web/src/components/Layout.tsx +++ b/web/src/components/Layout.tsx @@ -1,20 +1,72 @@ +import { useEffect, useRef, useState } from 'react' import { Link, NavLink, Outlet, useLocation, useNavigate } from 'react-router' -import { Moon, Sun, Monitor, ExternalLink, House, PenLine, Tags, Trash2, Settings, LogOut } from 'lucide-react' +import { + Moon, Sun, Monitor, ExternalLink, House, PenLine, MoreHorizontal, Tags, Trash2, Settings, LogOut, +} from 'lucide-react' import { useSiteTitle } from '../features/site' import { useTheme } from '../lib/theme' import { useAuth } from '../lib/auth' +/* ── 溢出菜单:触发按钮 + 下拉面板(点击外部 / Esc / 选中项关闭) ── */ + +function MoreMenu({ triggerClassName, triggerLabel, children }: { + triggerClassName: string + triggerLabel: string + children: React.ReactNode +}) { + const [open, setOpen] = useState(false) + const ref = useRef(null) + useEffect(() => { + if (!open) return + const onPointer = (e: PointerEvent) => { + if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false) + } + const onKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') setOpen(false) + } + document.addEventListener('pointerdown', onPointer) + document.addEventListener('keydown', onKey) + return () => { + document.removeEventListener('pointerdown', onPointer) + document.removeEventListener('keydown', onKey) + } + }, [open]) + return ( +
+ + {open && ( +
setOpen(false)}> + {children} +
+ )} +
+ ) +} + /* ── 博客端:居中单栏 720px 布局(博客系列设计稿) ─────────── */ -function ThemeButton() { +function ThemeButton({ className = 'pn-site-navlink', tip = 'native' }: { + className?: string + tip?: 'native' | 'css' +}) { const { mode, setMode } = useTheme() const next = mode === 'light' ? 'dark' : mode === 'dark' ? 'system' : 'light' const title = `主题:${mode === 'light' ? '浅色' : mode === 'dark' ? '深色' : '跟随系统'}(点击切换)` return ( + + 返回博客 + + + + ) } diff --git a/web/src/index.css b/web/src/index.css index dadfd1e..57d56a1 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -489,6 +489,11 @@ button:focus-visible { width: 14px; height: 14px; } +/* 博客端溢出菜单触发按钮:<640px 显示(基础规则须在 640px MQ 之前, + 否则同特异性下源码顺序靠后者覆盖 MQ 内的 display:inline-flex) */ +.pn-site-menu-btn { + display: none; +} .pn-site-main { flex: 1; width: 100%; @@ -547,6 +552,9 @@ button:focus-visible { .pn-site-navlink.hide-sm { display: none; } + .pn-site-menu-btn { + display: inline-flex; + } } /* ============================================================ @@ -1658,6 +1666,99 @@ button:focus-visible { .pn-admin-iconbtn:focus-visible::before { opacity: 1; } +/* tooltip 朝下变体(topbar 贴近视口顶部,气泡须向下弹出)。 + 气泡右对齐按钮:居中锚定会越过 topbar 右缘产生页面横向滚动条 */ +.pn-admin-iconbtn.tip-below::after { + bottom: auto; + top: calc(100% + 10px); + left: auto; + right: 0; + transform: translate(0, -2px); +} +.pn-admin-iconbtn.tip-below::before { + bottom: auto; + top: 100%; + border-top-color: transparent; + border-bottom-color: var(--primary); +} +.pn-admin-iconbtn.tip-below:hover::after, +.pn-admin-iconbtn.tip-below:focus-visible::after { + transform: translate(0, 0); +} +/* 侧栏隐藏时的移动端兜底按钮(<768px 显示) */ +.pn-admin-mobile-act { + display: none; +} +/* 溢出菜单:面板与菜单项(博客 header / 后台 topbar 共用) */ +.pn-moremenu { + position: relative; +} +.pn-moremenu-panel { + position: absolute; + top: calc(100% + 8px); + right: 0; + z-index: 60; + min-width: 150px; + display: flex; + flex-direction: column; + gap: 2px; + padding: 6px; + border: 1px solid var(--border); + border-radius: var(--radius-md); + background: var(--popover); + box-shadow: var(--shadow-sm); + animation: pn-moremenu-in 0.15s ease; +} +@keyframes pn-moremenu-in { + from { + opacity: 0; + transform: translateY(-4px); + } + to { + opacity: 1; + transform: none; + } +} +.pn-moremenu-item { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 10px; + border: 0; + border-radius: var(--radius-sm); + background: transparent; + color: var(--foreground); + font-size: 13px; + font-weight: 500; + line-height: 1.4; + text-align: left; + text-decoration: none; + white-space: nowrap; + cursor: pointer; + transition: background-color 0.15s ease; +} +.pn-moremenu-item:hover, +.pn-moremenu-item.active { + background: var(--muted); + color: var(--foreground); +} +.pn-moremenu-item.active { + font-weight: 600; +} +.pn-moremenu-item svg { + width: 15px; + height: 15px; + flex: none; + color: var(--muted-foreground); +} +.pn-moremenu-item.active svg { + color: var(--foreground); +} +/* 无 data-tip 的图标按钮不渲染空 tooltip 气泡(如菜单触发按钮) */ +.pn-admin-iconbtn:not([data-tip])::after, +.pn-admin-iconbtn:not([data-tip])::before { + display: none; +} .pn-admin-avatar { width: 28px; height: 28px; @@ -1799,6 +1900,23 @@ button:focus-visible { } .pn-admin-topbar h1 { font-size: 16px; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .pn-admin-actions { + gap: 6px; + } + .pn-admin-actions .btn { + padding-left: 10px; + padding-right: 10px; + } + .pn-admin-mobile-act { + display: inline-flex; + } + .pn-admin-desktop-act { + display: none; } .pn-admin-search { display: none; @@ -2588,41 +2706,10 @@ textarea.pn-as-input { padding-top: 16px; border-top: 1px solid var(--border); } -.pn-as-save-bar { - position: sticky; - bottom: 20px; - display: flex; - align-items: center; - justify-content: space-between; - gap: 16px; - padding: 12px 16px; - background: var(--card); - border: 1px solid var(--border); - border-radius: var(--radius-md); - box-shadow: var(--shadow-sm); -} -.pn-as-save-hint { - font-size: 12.5px; - color: var(--muted-foreground); -} -.pn-as-save-bar .btn { - min-height: 36px; - padding: 0 16px; - font-size: 13.5px; - flex: none; -} -.pn-as-save-bar .btn svg { - width: 15px; - height: 15px; -} @media (max-width: 767px) { .pn-as-card { padding: 20px; } - .pn-as-save-bar { - bottom: 88px; - flex-wrap: wrap; - } } /* ============================================================ diff --git a/web/src/pages/AdminSettings.tsx b/web/src/pages/AdminSettings.tsx index e4902df..38ed033 100644 --- a/web/src/pages/AdminSettings.tsx +++ b/web/src/pages/AdminSettings.tsx @@ -47,7 +47,17 @@ export default function AdminSettings() { return ( <> - + + +
@@ -104,19 +114,6 @@ export default function AdminSettings() {
- -
- 更改将在保存后应用到公开站点 - -
setToast(null)} />