视觉终审(glm-5.3-flash:max)判 BLOCK,逐条修完并复测。
P1 阻断:
- 瞬间页日期折成两行(`2026-` / `04-29`):日期列复用 var(--space-5)=40px,
而 10 字符 mono 日期需 ≈75px 且缺 nowrap。抽 --tl-rail(5.4rem) 统一驱动
轨道宽度与两处偏移,加 nowrap;实测日期单行(高度 20px)且圆点与卡片对齐
P2 交付可见破形:
- 关于页「关于我」行首 emoji 在无彩色 emoji 字体的环境渲染成黑方块:
正文行首 emoji 全部去掉(改纯文字段落),并给 --font-serif/--font-sans
链尾补 Apple/Segoe/Noto emoji 回退——用户内容里的 emoji(瞬间、书评)
同样不再落豆腐块
移动端导航两字折行(开/源、文/章):
- ≤450px 的 overflow-x 内滚从未生效:flex 子项默认可压缩,中文词被压到就地
折行。给 nav ul > li 加 flex:none + .nav-link nowrap;实测 360/390/430
三档全部单行(Range.getClientRects 计数逐项验证)
灰阶纪律:
- 项目墙原来按 GitHub linguist 给每个仓染语言色点,开源页铺 15 个仓 = 15 种
彩色,破「外壳灰阶、彩色只作正文强调」红线 → 统一改灰阶点,语言靠文字承载
- 语言占比并列(Python/TypeScript 各 36%)只有第一名着色,读起来像谁领先 →
判定改为「百分比等于最大值者一并着色」
版式与文案细节:
- prose 标题到正文比 tracks 多一档(16+16=32 vs 16)→ .sec-title + .prose-body
margin-top: 0
- 开源页 bio 破折号贴链接(`blog—— Astro`)→ 破折号前补空格
- 页脚 built from<hash> 缺空格(Astro 吃掉换行处空白)→ 显式 {' '}
- 新增一条真实瞬间(含青色楷体富文本)替代只有占位串的样本,让评论区与标签行
的间距可在真实数据下验收
验证: 构建 51 页 exit 0;content-lint 51 页 0/0/0;E2E **46/46 PASS**;
390px 四页 scrollWidth == innerWidth(无横向滚动);
导航 360/390/430 全部单行;日期单行;config-check 全过
Spec: 2026-11-02_blog-5h-content-social (视觉修复)
Co-authored-by: GLM-5.3
170 lines
7.3 KiB
Plaintext
170 lines
7.3 KiB
Plaintext
---
|
||
import { localeOf, localePath } from '../lib/locale';
|
||
import { t } from '../i18n/ui';
|
||
|
||
// 顶部导航:单行纪律 + 双语整轨(zh 无前缀 ↔ /en/,locale 构建期按页自推断——
|
||
// 曾只把 locale 喂给词条/语言钮而 href 裸奔 zh 路径,en 页点导航全部跌回中文轨)
|
||
const path = Astro.url.pathname;
|
||
const curLocale = localeOf(path);
|
||
const prefix = curLocale === 'en' ? '/en' : '';
|
||
const isCurrent = (href: string) =>
|
||
path === href || path === href.replace(/\/$/, '') || path.startsWith(href);
|
||
|
||
// 顺序:开源 · 文章 · 书架 · 图库 · 瞬间 · 关于(书架=阅读轨延伸紧跟文章;瞬间新进,开源前置)
|
||
const nav = [
|
||
{ href: `${prefix}/opensource/`, label: t('nav_opensource', curLocale) },
|
||
{ href: `${prefix}/archives/`, label: t('nav_posts', curLocale) },
|
||
{ href: `${prefix}/books/`, label: t('nav_books', curLocale) },
|
||
{ href: `${prefix}/gallery/`, label: t('nav_gallery', curLocale) },
|
||
{ href: `${prefix}/moments/`, label: t('nav_moments', curLocale) },
|
||
{ href: `${prefix}/about/`, label: t('nav_about', curLocale) },
|
||
];
|
||
const homeHref = curLocale === 'en' ? '/en/' : '/';
|
||
const switchHref = localePath(path, curLocale === 'zh' ? 'en' : 'zh');
|
||
const switchLabel = curLocale === 'zh' ? 'EN' : '中';
|
||
---
|
||
|
||
<header class="site-header safe-area">
|
||
<div class="container-wide header-inner">
|
||
<a href={homeHref} class="brand" aria-label={`wait4x ${t('nav_home', curLocale)}`}>wait4x</a>
|
||
<div class="nav-side">
|
||
<nav aria-label="主导航">
|
||
<ul>
|
||
{nav.map((item) => (
|
||
<li>
|
||
<a
|
||
href={item.href}
|
||
class="tap nav-link"
|
||
aria-current={isCurrent(item.href) ? 'page' : undefined}
|
||
>
|
||
{item.label}
|
||
</a>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</nav>
|
||
<!-- 图标钮出舱(车道 G):6 文本项后移动端内滚只波及导航词,三个功能钮任何宽度常驻可见
|
||
(实测 390 en 第 6 项后文本区滚 ~116px,旧结构会把主题钮一并推出首屏) -->
|
||
<div class="nav-actions">
|
||
<button class="tap nav-link" type="button" data-search-trigger aria-label="搜索">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||
<circle cx="11" cy="11" r="7"></circle>
|
||
<path d="m20 20-3.5-3.5"></path>
|
||
</svg>
|
||
</button>
|
||
<a href={switchHref} class="tap nav-link nav-lang" hreflang={curLocale === 'zh' ? 'en' : 'zh'}>{switchLabel}</a>
|
||
<button class="tap nav-link" type="button" data-theme-toggle aria-label="切换明暗">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||
<circle cx="12" cy="12" r="4"></circle>
|
||
<path d="M12 2v2m0 16v2M4.9 4.9l1.4 1.4m11.4 11.4 1.4 1.4M2 12h2m16 0h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"></path>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<script>
|
||
// document 级委托:ClientRouter 软导航会整体换 DOM,元素级监听在换页后
|
||
// 全部丢失(明暗按钮"失效"的根因);document 挂载点跨导航持久
|
||
document.addEventListener('click', (e) => {
|
||
// 用 Element 基类:按钮内是 SVG 图标,SVGElement 不是 HTMLElement 的实例
|
||
//(E2E 实测抓到的坑:instanceof HTMLElement 会拦掉所有点在图标上的点击)
|
||
if (!(e.target instanceof Element)) return;
|
||
if (!e.target.closest('[data-theme-toggle]')) return;
|
||
const root = document.documentElement;
|
||
const next = root.dataset.theme === 'dark' ? 'light' : 'dark';
|
||
root.dataset.theme = next;
|
||
localStorage.setItem('theme', next);
|
||
});
|
||
</script>
|
||
|
||
<style>
|
||
.site-header {
|
||
border-bottom: 1px solid var(--line);
|
||
position: sticky;
|
||
top: 0;
|
||
background: color-mix(in oklab, var(--bg) 88%, transparent);
|
||
backdrop-filter: blur(8px);
|
||
z-index: 10;
|
||
}
|
||
.header-inner {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
height: 56px;
|
||
}
|
||
.brand {
|
||
font-family: var(--font-mono);
|
||
font-weight: 700;
|
||
font-size: 18px;
|
||
color: var(--ink-1);
|
||
letter-spacing: 0.02em;
|
||
}
|
||
nav ul {
|
||
display: flex;
|
||
gap: var(--space-2);
|
||
list-style: none;
|
||
padding: 0;
|
||
margin: 0;
|
||
}
|
||
/* nav-side:导航+功能钮右聚;nav 可收缩(min-width:0)——内滚只发生在文本区 */
|
||
.nav-side {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: clamp(2px, 0.8vw, 8px);
|
||
min-width: 0;
|
||
margin-left: auto;
|
||
}
|
||
nav { min-width: 0; }
|
||
.nav-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: clamp(2px, 0.8vw, 8px);
|
||
flex: none;
|
||
}
|
||
.nav-link {
|
||
font-size: 15px;
|
||
color: var(--ink-3);
|
||
padding-inline: var(--space-2);
|
||
border-radius: 6px;
|
||
transition: color var(--dur) var(--ease);
|
||
}
|
||
.nav-link:hover { color: var(--ink-1); }
|
||
.nav-link[aria-current='page'] { color: var(--accent); }
|
||
.nav-lang { font-family: var(--font-mono); font-size: 13px; }
|
||
/* 暗色下图标钮发灰难辨(视觉复核 02/12):图标链接统一提一档并去底块感 */
|
||
.nav-link:has(svg) { color: var(--ink-2); background: transparent; }
|
||
.nav-link:has(svg):hover { color: var(--ink-1); }
|
||
@media (max-width: 560px) {
|
||
.nav-link { font-size: 14px; padding-inline: var(--space-1); }
|
||
}
|
||
/* 移动端审计 B1:7 项一行在 390px 溢出 50-88px(主题钮被推出屏)——
|
||
更窄一档整体收紧,估算总宽 ~344px < 360 */
|
||
/* 流式自适应(用户设计诉求):440→360px 区间字号/间距/图标钮连续收缩
|
||
(clamp = min~max 随视口线性插值),替代阶梯断点——任何宽度都合身,
|
||
不再需要逐档补丁;触控目标同步流式但下限钉 28px(WCAG 24 之上) */
|
||
.header-inner { gap: clamp(6px, 1.6vw, 12px); padding-inline: clamp(10px, 3vw, 20px); }
|
||
.brand { font-size: clamp(15px, 4vw, 18px); }
|
||
.nav-link { font-size: clamp(12px, 3.2vw, 14px); padding-inline: clamp(2px, 1vw, 8px); }
|
||
nav ul { gap: clamp(2px, 0.8vw, 8px); }
|
||
.header-inner .tap { min-width: clamp(28px, 8vw, 44px); min-height: clamp(28px, 8vw, 44px); }
|
||
.nav-lang { font-size: clamp(11.5px, 3vw, 13px); }
|
||
/* 超窄兜底:导航内滚代替页面晃动——页面级 scrollWidth 恒等于视口,任何机型不破版。
|
||
阈值 350→450(E 车道 6 项后 en 词长实测:360-430px 页溢 42-51px;zh 无感知) */
|
||
@media (max-width: 450px) {
|
||
nav ul { overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch; }
|
||
/* 视觉终审 P1:flex 子项默认可压缩,中文词被压到就地折成两行(开/源),
|
||
overflow-x 永远不触发。钉死不许压缩 + 不许换行,内滚才真正接管 */
|
||
nav ul > li { flex: none; }
|
||
.nav-link { white-space: nowrap; }
|
||
nav { min-width: 0; }
|
||
.header-inner { flex-wrap: nowrap; }
|
||
/* 中段再收一档地板(en 五词在 360-430 差 55-61px):字号 11px 下限仍可读,
|
||
目标是常规视口零内滚、内滚只作极端巪余 */
|
||
.nav-link { font-size: clamp(11px, 3.2vw, 14px); padding-inline: clamp(1px, 1vw, 8px); }
|
||
nav ul { gap: clamp(1px, 0.8vw, 8px); }
|
||
.header-inner .tap { min-width: clamp(26px, 8vw, 44px); }
|
||
}
|
||
</style>
|