移动端美化:悬浮磨砂导航菜单 + 保守档字号

菜单(site-header.astro)
- 全屏遮罩居中大链接 -> 仿 Firefly float-panel 的悬浮磨砂卡片:
  fixed top-[76px] 左右各留 12px,rounded-2xl,浅阴影,max-h 兜底防横屏溢出
- 菜单项 30px -> 18px,行高 44px,圆角 hover 底色;外链右侧加 ↗
- 当前页在构建期用 Astro.url.pathname 判定,高亮为主色文字 + 淡主色底
  + 正文标题那道红色短竖线(滑入/当前项显示)
- 入场动画改为「面板落下 + 逐项上浮」,节奏从 420ms 收到 300ms
- 遮罩从纯色全屏改为 background/45% + 极轻模糊,点它关闭
- JS 未改:data-open / ESC / body 滚动锁 / astro:before-swap 复位 / aria 全部沿用
- 补 :focus-visible 自定义焦点环(替掉浏览器默认那圈)、prefers-reduced-motion、
  prefers-reduced-transparency、@supports 回退

磨砂
- global.css 新增 --glass-bg / --glass-border / --glass-blur / --glass-saturate,
  由 --card 派生所以亮暗自动跟随
- 移动端页眉仍保持关闭常驻磨砂(原来就是有意为之),只有点开的菜单面板用

字号(只动 <768px,保守档)
- 正文/引用/列表 16.5px/1.85 -> 15.5px/1.78,行高密度约省 10%
- 文章 h1 30 -> 27px;正文 h2 30 -> 25;h3 24 -> 21;h4 20 -> 18
- 列表区标题 24 -> 21px
- 桌面端用 md: 前缀原样恢复

验证(headless Chrome 131,390x844 + 1280/800/700)
- 390px:正文 15.5/27.59、h1 27、菜单项 18;面板 445px 高、底边 511 < 844
- 1280/800px:正文 16.5/30.525、h1 48px,与改动前一致
- 亮/暗两套截图正常,无横向溢出
This commit is contained in:
dsh
2026-09-15 09:03:24 +08:00
parent c841fa2047
commit 9d9f87a013
5 changed files with 205 additions and 48 deletions
+8 -8
View File
@@ -35,7 +35,7 @@ const primaryTagHref = primaryTag ? `/?tag=${encodeURIComponent(primaryTag)}#hom
<header class="mb-8 xl:mb-12">
<h1
class="font-serif text-3xl sm:text-4xl md:text-5xl font-bold text-foreground leading-tight mb-6 flex items-start gap-3"
class="font-serif text-[27px] sm:text-4xl md:text-5xl font-bold text-foreground leading-tight mb-6 flex items-start gap-3"
data-pagefind-meta="title"
>
<span class="w-1.5 h-8 sm:h-10 shrink-0 bg-primary rounded-full mt-[0.15em] sm:mt-[0.3em]"></span>
@@ -119,17 +119,17 @@ const primaryTagHref = primaryTag ? `/?tag=${encodeURIComponent(primaryTag)}#hom
<article
class="prose prose-neutral dark:prose-invert max-w-[44rem] mx-auto
prose-headings:font-serif prose-headings:font-bold
prose-h2:text-3xl prose-h2:mt-12 prose-h2:mb-4 prose-h2:flex prose-h2:items-center prose-h2:gap-3
prose-h3:text-2xl prose-h3:mt-8 prose-h3:mb-3
prose-h4:text-xl
prose-p:text-[16.5px] prose-p:leading-[1.85] prose-p:my-5 prose-p:text-foreground/90
prose-h2:text-[25px] md:prose-h2:text-3xl prose-h2:mt-12 prose-h2:mb-4 prose-h2:flex prose-h2:items-center prose-h2:gap-3
prose-h3:text-[21px] md:prose-h3:text-2xl prose-h3:mt-8 prose-h3:mb-3
prose-h4:text-[18px] md:prose-h4:text-xl
prose-p:text-[15.5px] md:prose-p:text-[16.5px] prose-p:leading-[1.78] md:prose-p:leading-[1.85] prose-p:my-5 prose-p:text-foreground/90
prose-a:text-primary prose-a:no-underline prose-a:hover:underline
prose-blockquote:border-l-primary prose-blockquote:border-l-2 prose-blockquote:pl-4 prose-blockquote:italic prose-blockquote:text-[16.5px] prose-blockquote:leading-[1.85] prose-blockquote:text-content-secondary prose-blockquote:font-normal
prose-blockquote:border-l-primary prose-blockquote:border-l-2 prose-blockquote:pl-4 prose-blockquote:italic prose-blockquote:text-[15.5px] md:prose-blockquote:text-[16.5px] prose-blockquote:leading-[1.78] md:prose-blockquote:leading-[1.85] prose-blockquote:text-content-secondary prose-blockquote:font-normal
prose-strong:text-foreground prose-strong:font-semibold
prose-code:text-primary prose-code:bg-secondary prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded prose-code:text-sm prose-code:before:content-none prose-code:after:content-none
prose-pre:bg-secondary prose-pre:border prose-pre:border-border
prose-ol:text-[16.5px] prose-ul:text-[16.5px] prose-ol:text-foreground/90 prose-ul:text-foreground/90 prose-ol:my-5 prose-ul:my-5
prose-li:marker:text-primary prose-li:my-1.5 prose-li:leading-[1.85]
prose-ol:text-[15.5px] md:prose-ol:text-[16.5px] prose-ul:text-[15.5px] md:prose-ul:text-[16.5px] prose-ol:text-foreground/90 prose-ul:text-foreground/90 prose-ol:my-5 prose-ul:my-5
prose-li:marker:text-primary prose-li:my-1.5 prose-li:leading-[1.78] md:prose-li:leading-[1.85]
prose-table:text-sm
prose-th:bg-secondary prose-th:px-4 prose-th:py-2 prose-th:text-left prose-th:font-medium
prose-td:px-4 prose-td:py-2 prose-td:border-t prose-td:border-border
+2 -2
View File
@@ -312,11 +312,11 @@ export function ArticleList({
style={{ animation: "fade-in-up 220ms ease-out" }}
>
{activeCategory === "all" && !activeTag ? (
<h2 className="font-serif text-2xl font-bold leading-tight tracking-tight text-foreground sm:text-3xl lg:text-[2.625rem]">
<h2 className="font-serif text-[21px] font-bold leading-tight tracking-tight text-foreground sm:text-3xl lg:text-[2.625rem]">
{headerMeta.heading}
</h2>
) : (
<h2 className="flex flex-wrap items-end gap-x-2 gap-y-1 font-serif text-2xl font-bold leading-tight tracking-tight sm:text-3xl lg:text-[2.625rem]">
<h2 className="flex flex-wrap items-end gap-x-2 gap-y-1 font-serif text-[21px] font-bold leading-tight tracking-tight sm:text-3xl lg:text-[2.625rem]">
<span className="text-foreground/68">{headerMeta.eyebrow}</span>
<span className="text-foreground/35">/</span>
<span className="text-primary">{headerMeta.heading}</span>
+155 -38
View File
@@ -15,6 +15,12 @@ const navItems: { name: string; href: string; external?: boolean }[] = [
{ name: "关于", href: "/about/" },
{ name: "开往", href: "https://www.travellings.cn/go.html", external: true },
]
// 静态站:当前页在构建时就知道,直接拿来高亮菜单项。
// 切页后页眉会随新页面一起被换掉,所以这个标记始终对应当前页。
const currentPath = Astro.url.pathname
const isCurrent = (href: string) =>
href === "/#home-main" ? currentPath === "/" : currentPath.startsWith(href)
---
<header class="site-header fixed top-0 left-0 right-0 z-50 bg-background/80 backdrop-blur-xl border-b border-border/50 py-4">
@@ -74,63 +80,119 @@ const navItems: { name: string; href: string; external?: boolean }[] = [
<div
id="mobile-menu"
class="mobile-menu md:hidden fixed inset-0 z-[60] opacity-0 pointer-events-none transition-opacity duration-300 ease-out"
class="mobile-menu md:hidden fixed inset-0 z-[60] pointer-events-none"
aria-hidden="true"
>
<div
class="mobile-menu-bg absolute inset-0 bg-background/95 backdrop-blur-xl"
data-dismiss
aria-hidden="true"
></div>
<div class="mobile-menu-bg absolute inset-0" data-dismiss aria-hidden="true"></div>
<div class="relative h-full flex flex-col">
<div class="flex items-center justify-end px-6 py-4">
<div
class="mobile-menu-panel absolute left-3 right-3 top-[76px] p-2 rounded-2xl border max-h-[calc(100dvh-96px)] overflow-y-auto overscroll-contain"
role="dialog"
aria-modal="true"
aria-label="站点导航"
>
<div class="flex items-center justify-between pl-4 pr-1 pb-1.5">
<span class="text-[11px] font-medium uppercase tracking-[0.18em] text-muted-foreground">导航</span>
<button
type="button"
class="p-2 -mr-2 text-foreground"
class="p-2 rounded-lg text-muted-foreground transition-colors duration-200 hover:bg-secondary hover:text-foreground"
aria-label="关闭菜单"
data-dismiss
>
<span class="relative block w-5 h-5">
<span class="absolute left-0 top-1/2 w-full h-px -translate-y-1/2 rotate-45 bg-current" />
<span class="absolute left-0 top-1/2 w-full h-px -translate-y-1/2 -rotate-45 bg-current" />
</span>
<svg
class="w-4 h-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.75"
stroke-linecap="round"
aria-hidden="true"
>
<path d="M6 6l12 12M18 6L6 18" />
</svg>
</button>
</div>
<nav class="flex-1 flex flex-col items-center justify-center gap-7 px-8">
<nav class="flex flex-col">
{navItems.map((item, i) => (
<a
href={item.href}
target={item.external ? "_blank" : undefined}
rel={item.external ? "noreferrer" : undefined}
data-astro-prefetch={item.external ? undefined : ""}
class="mobile-nav-item group text-3xl font-medium tracking-tight text-foreground"
style={`--stagger: ${60 + i * 40}ms`}
aria-current={isCurrent(item.href) ? "page" : undefined}
class:list={[
"mobile-nav-item group relative flex items-center gap-3 h-11 pl-4 pr-3 rounded-xl text-[18px] font-medium tracking-tight transition-colors duration-200",
isCurrent(item.href) ? "is-current text-primary" : "text-foreground hover:text-primary",
]}
style={`--stagger: ${40 + i * 30}ms`}
>
<span class="relative inline-block">
{item.name}
<span class="absolute left-0 -bottom-1 h-px w-full origin-right scale-x-0 bg-primary transition-transform duration-300 group-hover:origin-left group-hover:scale-x-100" />
</span>
<span class="mobile-nav-marker" aria-hidden="true" />
<span class="flex-1">{item.name}</span>
{item.external && (
<svg
class="w-3.5 h-3.5 text-muted-foreground/70 transition-transform duration-200 group-hover:-translate-y-0.5 group-hover:translate-x-0.5"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.75"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M7 17L17 7M9 7h8v8" />
</svg>
)}
</a>
))}
</nav>
</div>
</div>
<style>
/* 容器只负责铺满屏幕、拦截点击和定位,透明度交给遮罩层和面板各自过渡 */
.mobile-menu[data-open="true"] {
opacity: 1;
pointer-events: auto;
}
/* 遮罩:轻轻压暗 + 极轻的模糊,让人一眼看出下面的内容被“压住了”,点它关闭 */
.mobile-menu-bg {
opacity: 0;
background-color: color-mix(in oklch, var(--background) 45%, transparent);
transition: opacity 200ms ease-out;
}
.mobile-menu[data-open="true"] .mobile-menu-bg {
opacity: 1;
}
/* 悬浮面板:磨砂卡片,从顶部轻轻落下 */
.mobile-menu-panel {
opacity: 0;
transform: translateY(-10px) scale(0.985);
transform-origin: top center;
background-color: var(--glass-bg);
border-color: var(--glass-border);
box-shadow: 0 18px 48px -14px oklch(0 0 0 / 0.24);
backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
-webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
transition:
opacity 180ms ease-out,
transform 260ms cubic-bezier(0.22, 1, 0.36, 1);
}
.mobile-menu[data-open="true"] .mobile-menu-panel {
opacity: 1;
transform: none;
}
/* 逐项入场 */
.mobile-menu .mobile-nav-item {
opacity: 0;
transform: translateY(14px);
transform: translateY(8px);
transition:
opacity 420ms cubic-bezier(0.22, 1, 0.36, 1),
transform 420ms cubic-bezier(0.22, 1, 0.36, 1);
opacity 300ms cubic-bezier(0.22, 1, 0.36, 1),
transform 300ms cubic-bezier(0.22, 1, 0.36, 1),
background-color 200ms ease-out,
color 200ms ease-out;
transition-delay: 0ms;
}
.mobile-menu[data-open="true"] .mobile-nav-item {
@@ -139,28 +201,83 @@ const navItems: { name: string; href: string; external?: boolean }[] = [
transition-delay: var(--stagger, 0ms);
}
@media (prefers-reduced-motion: reduce) {
.mobile-menu,
.mobile-menu .mobile-nav-item {
transition-duration: 1ms !important;
transform: none !important;
.mobile-nav-item:hover {
background-color: color-mix(in oklch, var(--secondary) 70%, transparent);
}
.mobile-nav-item.is-current {
background-color: color-mix(in oklch, var(--primary) 8%, transparent);
}
/* 键盘 Tab 进来时的焦点环,比浏览器默认那圈细一点、柔一点 */
.mobile-nav-item:focus-visible,
.mobile-menu-panel button:focus-visible {
outline: 2px solid color-mix(in oklch, var(--primary) 45%, transparent);
outline-offset: -2px;
}
/* 朱墨留白的那道红色短竖线,复用正文标题的语言 */
.mobile-nav-marker {
position: absolute;
left: 0;
top: 50%;
width: 3px;
height: 1rem;
border-radius: 9999px;
background-color: var(--primary);
opacity: 0;
transform: translateY(-50%) scaleY(0);
transition:
opacity 200ms ease-out,
transform 200ms cubic-bezier(0.22, 1, 0.36, 1);
}
.mobile-nav-item:hover .mobile-nav-marker,
.mobile-nav-item.is-current .mobile-nav-marker {
opacity: 1;
transform: translateY(-50%) scaleY(1);
}
/* 不支持 backdrop-filter 的浏览器:退回不透明卡片,别让文字糊在内容上 */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
.mobile-menu-panel {
background-color: var(--card);
}
}
/* Drop the heavy backdrop-filter on small viewports. The fixed header keeps
blurring everything underneath while scrolling, which makes mobile compositing
expensive. Use a slightly more opaque background instead. */
/* 系统开启“减少透明度”时直接关掉磨砂 */
@media (prefers-reduced-transparency: reduce) {
.mobile-menu-panel {
background-color: var(--card);
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
}
@media (prefers-reduced-motion: reduce) {
.mobile-menu,
.mobile-menu-panel,
.mobile-menu-bg,
.mobile-menu .mobile-nav-item,
.mobile-nav-marker {
transition-duration: 1ms !important;
}
.mobile-menu-panel,
.mobile-menu .mobile-nav-item {
transform: none !important;
}
.mobile-nav-marker,
.mobile-nav-item.is-current .mobile-nav-marker,
.mobile-nav-item:hover .mobile-nav-marker {
transform: translateY(-50%) scaleY(1) !important;
}
}
/* 小屏上页眉不再常驻 backdrop-filter:固定元素持续模糊会让移动端合成开销变大,
顶部时用实色反而更省更快。菜单面板是点开才出现的、面积也小,保留磨砂。 */
@media (max-width: 767px) {
.site-header {
backdrop-filter: none !important;
-webkit-backdrop-filter: none !important;
background-color: var(--background) !important;
}
.mobile-menu-bg {
backdrop-filter: none !important;
-webkit-backdrop-filter: none !important;
background-color: var(--background) !important;
}
}
</style>
+10
View File
@@ -48,6 +48,16 @@
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--radius: 0.5rem;
/*
* 磨砂玻璃 token —— 只给「浮在内容上方、面积不大」的元素用
* (移动端菜单、页眉、搜索面板…)。不要给文章卡片大面积用,
* 一屏十几个元素同时 backdrop-filter 会明显掉帧。
* 由 --card 派生,所以亮/暗色自动跟随。
*/
--glass-bg: color-mix(in oklch, var(--card) 72%, transparent);
--glass-border: color-mix(in oklch, var(--border) 75%, transparent);
--glass-blur: 14px;
--glass-saturate: 1.5;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.55 0.22 var(--primary-hue, 25));