Files
blog/src/components/Header.astro
T
wait4xandGLM-5.3 d4fe273812 fix(visual): 修视觉终审的折行/豆腐块与灰阶纪律违规
视觉终审(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
2026-11-02 20:56:00 +08:00

170 lines
7.3 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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>