fix(listing): make the mobile archive reachable and clear the AA misses
Deploy / build (push) Successful in 33s
Deploy / build (push) Successful in 33s
Review pass on the 041471d delivery, measured rather than eyeballed.
/posts/ on a phone put its first archive row 2.36 screens down (featured block
1235 px + 435 px of controls = row at 1996 px at 390x844), so a reader who came
to browse had to scroll past three stacked 16:9 cards. Below 700 px the cards are
now horizontal — thumb 38 %, excerpt dropped, title clamped to three lines — and
the block header carries `All 91 posts ↓` into #archive-list (with scroll-margin
for the sticky header), which doubles as the bypass-block affordance. Block
1235 → 523 px, card 392 → 153 px, first row 1996 → 1309 px.
Two new small-text colours measured under WCAG AA (.feat-min 3.04:1,
.archive-status 3.67:1 at ~12 px) move to --muted → 5.78:1 and 6.99:1. The
featured heading went with them; pre-existing --faint dates (3.67:1) were left
alone rather than restyling 91 rows.
/categories/ still showed the old Gemini case-studies.webp while the category
page had the new topology banner — one category, two art languages. The tile now
uses case-studies-banner-1200.webp (14 KB) letterboxed on #0b1613: a 16:9 centre
crop sliced the outer host panels in half, the letterbox was measured seamless.
`6min read` was emitted without its space (template-literal fix → `6 min read`,
`6 分钟`, verified in dist/). Both /posts/ ledes stopped claiming the page is
purely newest-first.
probe-layout: 72/72 viewport checks, collapse still 425→0→425 (1280) and
523→0→523 (390) in EN and ZH.
This commit is contained in:
1 parent
050d73305d
commit
4e64bfc9ae
7 files changed
+124
-17
No files matched your search
@@ -120,3 +120,18 @@ node scripts/probe-layout.mjs # 退出码 1 = 有横向溢出或折叠
|
||||
(`devops` 出现两次:分类 30 / 话题 6)→ 加 *Category* / *Topic* 标签、≤700px 时标签上移;
|
||||
列表卡片标签数封顶 4 个 + `+N`,修复卡片元信息行高不一致;导航补 `aria-current="page"`
|
||||
(此前四个栏目链接在任何页面都没有当前位置提示)。
|
||||
|
||||
---
|
||||
|
||||
## 复审修复 — 2026-10-27(第二轮)
|
||||
|
||||
独立复核实测发现四处遗漏,已修并重新量过(详见 `project-state.md` §4e-bis):
|
||||
|
||||
| # | 问题 | 实测(前 → 后) | 改法 |
|
||||
| --- | --- | --- | --- |
|
||||
| P1 | 移动端 `/posts/` 首条归档行在 2.36 屏之外 | 精选块 1235→**523px**,卡片 392→**153px**,首行 1996→**1309px(1.55 屏)** | ≤700px 精选卡改左图右文、隐藏摘要、标题 3 行截断;块头加 `All 91 posts ↓`(含 `scroll-margin-top`) |
|
||||
| P2 | 两处新增小字不达 WCAG AA | `.feat-min` 3.04→**5.78**;`.archive-status` 3.67→**6.99**(AA 需 4.5:1) | `--faint` → `--muted`(仅新增元素;既有 91 行日期未动,避免整页重排) |
|
||||
| P3 | 同一分类两套视觉 | `/categories/` 瓦片原用旧 `case-studies.webp` | 改用 `case-studies-banner-1200.webp`(14KB)+ `object-fit:contain` on `#0b1613` 信箱式留边(16:9 居中裁切会切掉两侧主机面板,已实测) |
|
||||
| P4 | `6min read` 缺空格 | `6min read` → **`6 min read` / `6 分钟`** | 改为模板字符串(同款写法在文章页正常,此处在嵌套 span 内被吞掉) |
|
||||
|
||||
另:`/posts/`、`/zh/posts/` 导语不再声称"全部按时间倒序"。验收:`probe-layout` **72/72**,折叠在 EN/ZH × 1280/390 仍为 425→0→425 / 523→0→523。
|
||||
@@ -464,6 +464,33 @@ reports **72/72** page×viewport checks with `scrollWidth == clientWidth`, plus
|
||||
stops breaking; the nav gained `aria-current="page"` (it had no active state at all).
|
||||
- **Docs:** `scripts/probe-layout.mjs` is committed as the repeatable gate for this class of bug.
|
||||
|
||||
### 4e-bis. Review pass on the delivery (same day) — what measurement changed
|
||||
|
||||
An independent read of the shipped pages turned up four things the first pass missed. Fixed, remeasured:
|
||||
|
||||
- **Mobile archive was 2.4 screens away from its own list.** At 390×844 the featured block ran
|
||||
688→1923 px (3 × 392 px stacked cards) plus 435 px of controls, so the first `2026` row sat at
|
||||
**1996 px = 2.36 screens** — a reader who came to browse had to scroll past everything.
|
||||
Below 700 px each featured card is now horizontal (thumb 38 %, excerpt dropped, title clamped to
|
||||
three lines): card 392 → **153 px**, block 1235 → **523 px**, first row at **1309 px = 1.55
|
||||
screens**. The block header gained `All 91 posts ↓` (→ `#archive-list`, `scroll-margin-top`
|
||||
clears the sticky header), which doubles as the bypass-block affordance.
|
||||
- **Two new small-text colours failed WCAG AA.** Measured against their own backgrounds:
|
||||
`.feat-min` **3.04:1** and `.archive-status` **3.67:1** (`--faint`), where AA wants 4.5:1 under
|
||||
18.66 px. Both moved to `--muted` → **5.78:1** and **6.99:1**; the featured block's own heading
|
||||
moved with them. Pre-existing `--faint` dates (3.67:1) were left alone rather than restyling 91 rows.
|
||||
- **One category, two art languages.** `/categories/` still showed the old Gemini
|
||||
`/categories/case-studies.webp` while the category page had the new 21:9 topology banner.
|
||||
The tile now points at `case-studies-banner-1200.webp` (14 KB) and letterboxes it
|
||||
(`object-fit: contain` on `#0b1613`, measured seamless) instead of centre-cropping — a 16:9 crop
|
||||
sliced the outer host panels in half.
|
||||
- **`6min read` had no space.** `{f.minutes} {labels.minutes}` inside the nested `feat-cta` span is
|
||||
emitted without the separating space (the same pattern elsewhere in the repo *does* keep it, so
|
||||
this is compiler-adjacent, not universal). Replaced with a template literal → `6 min read` /
|
||||
`6 分钟`, verified in `dist/`.
|
||||
|
||||
`/posts/` and `/zh/posts/` ledes no longer claim the page is purely newest-first.
|
||||
|
||||
## 5. Conventions (non-negotiable)
|
||||
|
||||
- Push git.hoelee.com first, then GitHub
|
||||
|
||||
@@ -56,6 +56,7 @@ const labels = isZh
|
||||
readArticle: '阅读文章',
|
||||
readCase: '阅读案例复盘',
|
||||
minutes: '分钟',
|
||||
skip: `全部 ${posts.length} 篇 ↓`,
|
||||
statusAll: `共 ${posts.length} 篇`,
|
||||
statusSome: `命中 {n} / ${posts.length} 篇`,
|
||||
clear: '清除筛选',
|
||||
@@ -70,6 +71,7 @@ const labels = isZh
|
||||
readArticle: 'Read article',
|
||||
readCase: 'Read case study',
|
||||
minutes: 'min read',
|
||||
skip: `All ${posts.length} posts ↓`,
|
||||
statusAll: `All ${posts.length} posts`,
|
||||
statusSome: `Showing {n} of ${posts.length} posts`,
|
||||
clear: 'Clear filters',
|
||||
@@ -131,9 +133,15 @@ const ctaLabel = (category: string) =>
|
||||
data-collapsed="false"
|
||||
>
|
||||
<div class="archive-featured-inner">
|
||||
<h2 id="archive-featured-title" class="archive-featured-title">
|
||||
<span class="af-dot" aria-hidden="true"></span>{labels.featured}
|
||||
</h2>
|
||||
<div class="af-head">
|
||||
<h2 id="archive-featured-title" class="archive-featured-title">
|
||||
<span class="af-dot" aria-hidden="true"></span>{labels.featured}
|
||||
</h2>
|
||||
{/* Jump past the cards. On a phone the block used to put the first archive
|
||||
row ~2.4 screens down, so a reader who came to browse had no way out of the
|
||||
flagships; this is also the bypass-block affordance for keyboard users. */}
|
||||
<a class="af-skip" href="#archive-list">{labels.skip}</a>
|
||||
</div>
|
||||
<div class="archive-featured-grid">
|
||||
{featured.map((f) => (
|
||||
<a class="feat-card" href={f.href}>
|
||||
@@ -157,7 +165,7 @@ const ctaLabel = (category: string) =>
|
||||
<span class="feat-title">{f.title}</span>
|
||||
<span class="feat-desc">{f.description}</span>
|
||||
<span class="feat-cta">
|
||||
{ctaLabel(f.category)} → <span class="feat-min">{f.minutes} {labels.minutes}</span>
|
||||
{ctaLabel(f.category)} → <span class="feat-min">{`${f.minutes} ${labels.minutes}`}</span>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
@@ -314,6 +322,12 @@ const ctaLabel = (category: string) =>
|
||||
pointer-events: none;
|
||||
}
|
||||
.archive-featured-inner { overflow: hidden; min-height: 0; }
|
||||
.af-head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.6rem;
|
||||
margin: 0.2rem 0 0.7rem;
|
||||
}
|
||||
.archive-featured-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -321,9 +335,19 @@ const ctaLabel = (category: string) =>
|
||||
font-size: 0.78rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--faint);
|
||||
margin: 0.2rem 0 0.7rem;
|
||||
/* --faint measured 3.67:1 at this size; AA needs 4.5:1 for small text. */
|
||||
color: var(--muted);
|
||||
margin: 0;
|
||||
}
|
||||
.af-skip {
|
||||
margin-left: auto;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.72rem;
|
||||
color: var(--muted);
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.af-skip:hover { color: var(--brand); text-decoration: underline; }
|
||||
.af-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
@@ -393,7 +417,35 @@ const ctaLabel = (category: string) =>
|
||||
gap: 0.35rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.feat-min { color: var(--faint); font-weight: 400; }
|
||||
/* --faint measured 3.04:1 at 12 px; AA needs 4.5:1 for small text. */
|
||||
.feat-min { color: var(--muted); font-weight: 400; }
|
||||
|
||||
/* Phones: three stacked 16:9 cards put the first archive row 2.4 screens down
|
||||
(measured 1235 px of featured + 435 px of controls at 390x844). Lay each card out
|
||||
horizontally instead and drop the excerpt, so the block costs ~1/3 of the height
|
||||
and the year list is reachable in about one screen. */
|
||||
@media (max-width: 700px) {
|
||||
.feat-card { flex-direction: row; align-items: stretch; }
|
||||
.feat-thumb {
|
||||
flex: 0 0 38%;
|
||||
aspect-ratio: auto;
|
||||
align-self: stretch;
|
||||
border-right: 1px solid var(--border);
|
||||
}
|
||||
.feat-thumb img { height: 100%; }
|
||||
.feat-body { flex: 1 1 auto; min-width: 0; padding: 0.55rem 0.65rem 0.6rem; gap: 0.2rem; }
|
||||
.feat-desc { display: none; }
|
||||
.feat-title {
|
||||
font-size: 0.9rem;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.feat-cta { padding-top: 0.15rem; font-size: 0.72rem; }
|
||||
.feat-top { gap: 0.3rem; }
|
||||
.feat-metric { font-size: 0.64rem; }
|
||||
}
|
||||
|
||||
.archive-status {
|
||||
display: flex;
|
||||
@@ -402,7 +454,8 @@ const ctaLabel = (category: string) =>
|
||||
flex-wrap: wrap;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.74rem;
|
||||
color: var(--faint);
|
||||
/* --faint measured 3.67:1 at 11.8 px; AA needs 4.5:1 for small text. */
|
||||
color: var(--muted);
|
||||
margin: 0 0 0.5rem;
|
||||
min-height: 28px;
|
||||
}
|
||||
@@ -419,7 +472,7 @@ const ctaLabel = (category: string) =>
|
||||
}
|
||||
.archive-clear:hover { border-color: var(--brand); }
|
||||
|
||||
.archive { list-style: none; padding: 0; margin: 0; }
|
||||
.archive { list-style: none; padding: 0; margin: 0; scroll-margin-top: 5.5rem; }
|
||||
.archive-year {
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.08em;
|
||||
|
||||
@@ -141,14 +141,18 @@ const LINE_COLORS: Record<Line['t'], string> = {
|
||||
|
||||
{
|
||||
hasImage ? (
|
||||
// case-studies has its own 21:9 topology banner (the portfolio hero art). At the
|
||||
// 16:9 tile ratio a centre crop would slice the outer host panels in half, so the
|
||||
// card letterboxes it on the same dark ground instead — measured invisible at
|
||||
// #0b1613. One art language for one category, no clipped hardware.
|
||||
<img
|
||||
src={`/categories/${slug}.webp`}
|
||||
src={slug === 'case-studies' ? '/images/categories/case-studies-banner-1200.webp' : `/categories/${slug}.webp`}
|
||||
alt={alt}
|
||||
width="1024"
|
||||
height="576"
|
||||
width={slug === 'case-studies' ? 1200 : 1024}
|
||||
height={slug === 'case-studies' ? 514 : 576}
|
||||
loading={loading}
|
||||
decoding="async"
|
||||
class="category-banner-img"
|
||||
class:list={['category-banner-img', { 'is-wide': slug === 'case-studies' }]}
|
||||
/>
|
||||
) : (
|
||||
<svg
|
||||
|
||||
@@ -26,8 +26,9 @@ const featured = pickFeatured(posts, 'en', 3);
|
||||
>
|
||||
<h1>Posts</h1>
|
||||
<p class="archive-lead">
|
||||
{posts.length} posts, newest first. Filter by title, tag or category below — or read a
|
||||
whole topic through its <a href="/tags/docker/">tag page</a>.
|
||||
{posts.length} posts: three flagship write-ups first, then every post newest-first.
|
||||
Filter by title, tag or category below — or read a whole topic through its
|
||||
<a href="/tags/docker/">tag page</a>.
|
||||
</p>
|
||||
<Archive posts={posts} lang="en" featured={featured} />
|
||||
</BaseLayout>
|
||||
@@ -23,8 +23,8 @@ const featured = pickFeatured(posts, 'zh', 3);
|
||||
>
|
||||
<h1>文章</h1>
|
||||
<p class="archive-lead">
|
||||
共 {posts.length} 篇,按时间倒序。下方可按标题、标签或分类即时筛选,也可以走
|
||||
<a href="/zh/tags/docker/">标签页</a> 把一个主题读完。
|
||||
共 {posts.length} 篇:顶部是 3 篇旗舰复盘,下方全量按时间倒序。可按标题、标签或分类即时筛选,
|
||||
也可以走 <a href="/zh/tags/docker/">标签页</a> 把一个主题读完。
|
||||
</p>
|
||||
<Archive posts={posts} lang="zh" featured={featured} />
|
||||
</BaseLayout>
|
||||
@@ -1461,6 +1461,13 @@ a.category-card:hover { border-color: var(--brand); }
|
||||
display: block;
|
||||
transition: transform 0.25s ease;
|
||||
}
|
||||
/* case-studies ships a 21:9 topology banner. Cropping it to the tile ratio would slice
|
||||
the outer host panels, so it letterboxes on its own dark ground (#0b1613 matches the
|
||||
art's gradient at both edges — verified, no visible seam). */
|
||||
.category-card .art img.is-wide {
|
||||
object-fit: contain;
|
||||
background: #0b1613;
|
||||
}
|
||||
a.category-card:hover .art img {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user