feat(books): 书架三案按视觉复核修完(长标题裁切/参差/孤儿位/无暗示)

视觉复核逐案挑出的缺陷全修:
A 竖排色脊:书名/作者按脊内竖向容量确定性截断(此前 10 字书名与 9 字作者挤成
  第二窄列被 40px 脊宽裁掉);脊顶上留 10px hover 余量 + 底部基线;情绪色混色
  从 16% 提到 26% 并加内描边(淡紫脊原本在白底上几乎消隐,墙被读断);短标题脊
  补竖排年份填白;移动端脊高 120→172 保持 1:4 书脊比例,不再退化成矮胖色块
B 封面网格:改 3 列(5 本排 3+2,消掉 4+1 孤儿位);标题定两行高、卡片等高
  (实测同行 394px 一致),同排基线不再锯齿;占位卡从单字换成竖排书名,视觉
  重量向真封面靠拢
C 抽书阅读:首行默认展开 + 每行加「封面与简介 ▾」暗示(此前收起态没有任何可点
  信号,看着像静态列表);去掉恒为同值的年份列;标题改定宽列 + 省略号(实测
  作者列 x 全部 518,消除锯齿);作者前加「·」并与标题拉开色阶;色条加宽成贯通行
  高的窄脊

验证: 构建 exit 0;content-lint 54 页 0/0/0;Playwright 实测 A 标题/作者零裁切、
      B 三列且同行等高 394、C 作者列全对齐 518 且首行展开、390 下脊高 172 不溢出

Spec: 2026-11-02_blog-5i-fonts-polish (T7 预览修订)

Co-authored-by: GLM-5.3
This commit is contained in:
wait4xandGLM-5.3 committed 2026-11-03 11:55:32 +08:00
1 parent 6061c16396
commit 31fb52cb06
1 file changed
+83 -19
+83 -19
View File
@@ -31,6 +31,8 @@ const firstPara = (body: string | undefined): string => {
return line.replace(/[*_>`[\]]/g, '').slice(0, 60); return line.replace(/[*_>`[\]]/g, '').slice(0, 60);
}; };
/** 竖排书脊的高度有限:中文书名超过 ~11 字就必然溢出,用确定性截断代替裁切 */
const clip = (t: string, n: number) => (t.length > n ? `${t.slice(0, n)}…` : t);
const items = books.map((b) => ({ const items = books.map((b) => ({
id: b.id, id: b.id,
title: b.data.title, title: b.data.title,
@@ -40,6 +42,9 @@ const items = books.map((b) => ({
mood: b.data.mood ?? 'var(--ink-4)', mood: b.data.mood ?? 'var(--ink-4)',
year: b.data.pubDate.getFullYear(), year: b.data.pubDate.getFullYear(),
href: `/archives/${b.id}/`, href: `/archives/${b.id}/`,
spineTitle: clip(b.data.title, 11),
spineAuthor: clip(b.data.bookAuthor ?? '', 4),
short: clip(b.data.title, 3),
})); }));
const years = [...new Set(items.map((i) => i.year))].sort((a, b) => b - a); const years = [...new Set(items.map((i) => i.year))].sort((a, b) => b - a);
const withCover = items.filter((i) => i.cover).length; const withCover = items.filter((i) => i.cover).length;
@@ -74,8 +79,10 @@ const withCover = items.filter((i) => i.cover).length;
{items.filter((i) => i.year === year).map((b) => ( {items.filter((i) => i.year === year).map((b) => (
<li> <li>
<a class="va-spine" href={b.href} style={`--mood:${b.mood}`} title={`${b.title}${b.author ? ' · ' + b.author : ''}`}> <a class="va-spine" href={b.href} style={`--mood:${b.mood}`} title={`${b.title}${b.author ? ' · ' + b.author : ''}`}>
<span class="va-spine-title">{b.title}</span> <span class="va-spine-title">{b.spineTitle}</span>
{b.author && <span class="va-spine-author">{b.author}</span>} {b.spineAuthor && <span class="va-spine-author">{b.spineAuthor}</span>}
{/* 短标题脊上半段会空出一大块,用年份竖排填补(视觉复核:空脊读起来空洞) */}
<span class="va-spine-year mono">{String(b.year).slice(2)}</span>
</a> </a>
</li> </li>
))} ))}
@@ -105,7 +112,8 @@ const withCover = items.filter((i) => i.cover).length;
) : ( ) : (
<span class="vb-placeholder"> <span class="vb-placeholder">
<i class="vb-stripe" aria-hidden="true" /> <i class="vb-stripe" aria-hidden="true" />
<span class="vb-initial">{b.title.slice(0, 1)}</span> {/* 单字无识别价值 → 换竖排书名(2–3 字) */}
<span class="vb-vertical">{b.short}</span>
</span> </span>
)} )}
</span> </span>
@@ -133,12 +141,13 @@ const withCover = items.filter((i) => i.cover).length;
<ul class="vc-list"> <ul class="vc-list">
{items.map((b) => ( {items.map((b) => (
<li> <li>
<details class="vc-book" style={`--mood:${b.mood}`}> <details class="vc-book" style={`--mood:${b.mood}`} open={b === items[0]}>
<summary class="vc-spine"> <summary class="vc-spine">
<span class="vc-bar" aria-hidden="true" /> <span class="vc-bar" aria-hidden="true" />
<span class="vc-title">{b.title}</span> <span class="vc-title">{b.title}</span>
{b.author && <span class="vc-author">{b.author}</span>} {b.author && <span class="vc-author">{b.author}</span>}
<span class="vc-date mono">{b.year}</span> {/* 展开暗示:没有它就只是 5 行灰字列表(视觉复核 P1) */}
<span class="vc-cue mono">封面与简介 ▾</span>
</summary> </summary>
<div class="vc-body"> <div class="vc-body">
{b.cover && ( {b.cover && (
@@ -207,9 +216,12 @@ const withCover = items.filter((i) => i.cover).length;
flex-wrap: wrap; flex-wrap: wrap;
gap: 6px; gap: 6px;
list-style: none; list-style: none;
padding: 0; /* 上沿留出 hover 抬升余量(视觉复核:脊顶贴着容器上沿,位移会被裁) */
padding: 10px 0 0;
margin: 0; margin: 0;
align-items: flex-end; align-items: flex-end;
/* 底部基线:让一排脊有"落在地上"的参照 */
border-bottom: 1px solid var(--line);
} }
.va-spine { .va-spine {
position: relative; position: relative;
@@ -218,11 +230,14 @@ const withCover = items.filter((i) => i.cover).length;
justify-content: space-between; justify-content: space-between;
width: 44px; width: 44px;
height: 190px; height: 190px;
padding: 10px 6px; padding: 10px 6px 8px;
border-radius: 3px 6px 6px 3px; border-radius: 3px 6px 6px 3px;
background: color-mix(in oklab, var(--mood) 16%, var(--bg-soft)); /* 最浅下限:情绪色偏淡时(如淡紫 #e6e0f5)脊会几乎消隐、墙被读断,
border: 1px solid color-mix(in oklab, var(--mood) 34%, var(--line)); 26% 混色 + 内描边把它拉回可见范围 */
background: color-mix(in oklab, var(--mood) 26%, var(--bg-soft));
border: 1px solid color-mix(in oklab, var(--mood) 44%, var(--line));
border-left: 5px solid var(--mood); border-left: 5px solid var(--mood);
box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--mood) 18%, transparent);
transition: translate var(--dur) var(--ease), box-shadow var(--dur) var(--ease), border-color var(--dur) var(--ease); transition: translate var(--dur) var(--ease), box-shadow var(--dur) var(--ease), border-color var(--dur) var(--ease);
} }
.va-spine:hover { .va-spine:hover {
@@ -243,15 +258,25 @@ const withCover = items.filter((i) => i.cover).length;
writing-mode: vertical-rl; writing-mode: vertical-rl;
font-size: 10px; font-size: 10px;
color: var(--ink-4); color: var(--ink-4);
max-height: 48px; max-height: 60px;
overflow: hidden; overflow: hidden;
/* 超长作者不再硬挤第二列(视觉复核:挤成第二窄列被 40px 脊宽裁掉);
Astro 侧已按 5 字截断,此处高度只兜极端字宽 */
white-space: nowrap;
}
.va-spine-year {
writing-mode: vertical-rl;
font-size: 9.5px;
color: color-mix(in oklab, var(--ink-4) 70%, transparent);
letter-spacing: 0.1em;
} }
/* ══════════ B:封面网格 ══════════ */ /* ══════════ B:封面网格 ══════════ */
.vb-year + .vb-year { margin-top: var(--space-5); } .vb-year + .vb-year { margin-top: var(--space-5); }
.vb-grid { .vb-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); /* 3 列:5 本书排成 3+2,避免 4+1 的孤儿位(视觉复核);卡片也更大、封面更可读 */
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
gap: var(--space-3); gap: var(--space-3);
list-style: none; list-style: none;
padding: 0; padding: 0;
@@ -260,6 +285,8 @@ const withCover = items.filter((i) => i.cover).length;
.vb-card { .vb-card {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: stretch;
height: 100%;
gap: var(--space-2); gap: var(--space-2);
padding: var(--space-2); padding: var(--space-2);
border: 1px solid var(--line); border: 1px solid var(--line);
@@ -295,14 +322,26 @@ const withCover = items.filter((i) => i.cover).length;
width: 12px; width: 12px;
background: var(--mood); background: var(--mood);
} }
.vb-initial { .vb-vertical {
writing-mode: vertical-rl;
font-family: var(--font-serif); font-family: var(--font-serif);
font-size: 40px; font-size: 19px;
letter-spacing: 0.16em;
color: color-mix(in oklab, var(--mood) 62%, var(--ink-2)); color: color-mix(in oklab, var(--mood) 62%, var(--ink-2));
opacity: 0.85;
} }
.vb-body { display: flex; flex-direction: column; gap: 1px; } .vb-body { display: flex; flex-direction: column; gap: 1px; flex: 1; }
.vb-title { font-family: var(--font-serif); font-size: 14.5px; color: var(--ink-1); line-height: 1.35; } /* 标题定两行高:只 1 行的卡不会把作者/描述顶上移,同排基线才齐(视觉复核抓到的锯齿) */
.vb-title {
font-family: var(--font-serif);
font-size: 14.5px;
color: var(--ink-1);
line-height: 1.35;
min-height: calc(1.35em * 2);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.vb-author { font-size: 11.5px; color: var(--ink-4); } .vb-author { font-size: 11.5px; color: var(--ink-4); }
.vb-desc { .vb-desc {
font-size: 12.5px; font-size: 12.5px;
@@ -330,9 +369,32 @@ const withCover = items.filter((i) => i.cover).length;
.vc-spine::-webkit-details-marker { display: none; } .vc-spine::-webkit-details-marker { display: none; }
.vc-spine:hover { background: var(--bg-soft); } .vc-spine:hover { background: var(--bg-soft); }
.vc-bar { width: 6px; align-self: stretch; border-radius: 3px; background: var(--mood); flex: none; } .vc-bar { width: 6px; align-self: stretch; border-radius: 3px; background: var(--mood); flex: none; }
.vc-title { font-family: var(--font-serif); font-size: 15px; color: var(--ink-1); } /* 标题定宽列:作者起点不再随书名长短逐行右移(视觉复核的第二列锯齿) */
.vc-title {
font-family: var(--font-serif);
font-size: 15px;
color: var(--ink-1);
/* 定宽 + 省略:作者列起点不再随书名长短漂移(视觉复核的第二列锯齿) */
width: 11em;
flex: none;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.vc-author { font-size: 12px; color: var(--ink-4); } .vc-author { font-size: 12px; color: var(--ink-4); }
.vc-date { margin-left: auto; font-size: 11.5px; color: var(--ink-4); } .vc-author::before { content: '·'; margin-right: 6px; opacity: 0.5; }
.vc-cue {
margin-left: auto;
font-size: 11.5px;
color: var(--ink-4);
white-space: nowrap;
transition: color var(--dur) var(--ease);
}
.vc-spine:hover .vc-cue { color: var(--accent); }
details[open] > .vc-spine .vc-cue { color: var(--ink-3); }
details[open] > .vc-spine .vc-cue::after { content: ''; }
/* 色条改成贯通行高 80% 的窄脊,不再像表单色标 */
.vc-bar { width: 8px !important; margin: 4px 0; }
.vc-body { .vc-body {
display: flex; display: flex;
gap: var(--space-3); gap: var(--space-3);
@@ -347,7 +409,9 @@ const withCover = items.filter((i) => i.cover).length;
.vc-link { font-size: 13px; color: var(--accent); } .vc-link { font-size: 13px; color: var(--accent); }
@media (max-width: 560px) { @media (max-width: 560px) {
.va-spine { width: 38px; height: 160px; } /* 移动端保持书脊比例(1:4 左右):压到 120px 会退化成矮胖色块 */
.va-spine { width: 40px; height: 172px; }
.va-wall { padding-top: 8px; }
.vb-grid { grid-template-columns: repeat(auto-fill, minmax(126px, 1fr)); gap: var(--space-2); } .vb-grid { grid-template-columns: repeat(auto-fill, minmax(126px, 1fr)); gap: var(--space-2); }
.vc-body { flex-direction: column; } .vc-body { flex-direction: column; }
} }