From 5532c35064b08b14b2611d7d51da0550360e6202 Mon Sep 17 00:00:00 2001 From: dsh Date: Tue, 15 Sep 2026 09:37:14 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B5=85=E8=89=B2=E6=94=B9=E4=B8=BA=E6=9A=96?= =?UTF-8?q?=E7=BA=B8=E5=BA=95=20+=20=E7=BA=B8=E7=BA=B9=E5=BA=95=20+=20?= =?UTF-8?q?=E7=A7=BB=E5=8A=A8=E7=AB=AF=E5=88=86=E7=B1=BB=E8=83=B6=E5=9B=8A?= =?UTF-8?q?=E6=9D=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 浅色配色(global.css,暗色未动) - 纯白底在屏幕上偏刺眼、大面积纯色没有视觉落点,整套浅色 token 压到 L*≈96 并给暖黄彩度(oklch hue 84),墨色换成暖褐而非纯黑 - background #f8f3e9 / surface-subtle #f4eee3 / card #fcf8f0(比页面底亮, 靠明度差拉层次)/ foreground #221d18 / border #dfd9ce - 对比度仍留足:正文 15:1、content-secondary 7.5:1、muted-foreground 5.3:1、 主色链接 4.9:1(AA 要求 4.5:1) - secondary / sidebar / popover 等原来偏中性的 token 一并转暖,避免冷暖打架 纸纹底 - --paper-noise 用内联 SVG feTurbulence 分形噪声,无需图片文件; feColorMatrix saturate=0 去饱和得到带随机 alpha 的灰颗粒, 因此不需要 mix-blend-mode 就能亮暗通用 - body::before 铺成 position:fixed 整屏颗粒,z-index:1(压在正文上、 页眉 z-50 / 菜单 z-60 之下),pointer-events:none - 颗粒浓度:亮色 0.2 / 暗色 0.22。实测亮度标准差 0.05→0.7、0.2→1.9、 0.3→2.9(0=完全平整),选 2 左右这个「看得出是纸但不觉得脏」的档位 - 顺带让 backdrop-filter 有细节可模糊,此前纯色底上磨砂等于不存在 移动端分类胶囊条(article-list.tsx + sidebar.tsx) - 移动端侧栏被排在文章列表下面(实测首篇 y=999、侧栏 y=3212), 分类筛选基本摸不到;现在列表顶部多一条吸附胶囊条 - 复用同一份 categories / activeCategory / handleCategoryChange,不写第二套; 末尾胶囊在「分类 / 标签」间切换;两端渐隐提示可横向滑;滚动条藏掉 - 把当前项自动滚到中间,只操作这条横向容器,不动页面纵向位置 - sidebar.tsx 的分类/标签块加 hidden lg:block,避免同页出现两份 其他 - 新增 --header-h: 69px(页眉实测高度),移动端菜单面板和吸附胶囊条 都改用它定位,不再各处硬编码 69/76 - README 补「浅色配色」「纸纹底」「移动端分类胶囊条」三节 验证(headless Chrome 131) - 390px:胶囊条可见、侧栏两块 display:none;点「思考」→ ?category=思考、 列表 10→4 条、当前项高亮;切到标签模式列出 35 个标签;无横向溢出 - 1280px:胶囊条隐藏、侧栏分类恢复显示(display:block) - 亮/暗两套截图正常,书架、文章页均无异常 --- README.md | 47 +++++++++++- src/components/article-list.tsx | 124 +++++++++++++++++++++++++++++++ src/components/sidebar.tsx | 8 +- src/components/site-header.astro | 2 +- src/styles/global.css | 90 ++++++++++++++++------ 5 files changed, 244 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 12899a2..acf4d08 100644 --- a/README.md +++ b/README.md @@ -176,8 +176,42 @@ epigraph: 题记 # 可选 代码风格:Astro/TS/TSX 一律 2 空格缩进。组件命名 `PascalCase` 与 `kebab-case` 混用,跟着所在目录的既有风格走即可。项目没有配 formatter,改动尽量贴合周围代码。 -### 移动端字号 +### 浅色配色:暖纸底,不用亮白 +浅色模式**刻意不用纯白**。纯白(L\*≈99)在屏幕上偏刺眼,而且大面积纯色没有视觉落点,看久了发死。整套浅色 token 压到 L\*≈96 并给了一点暖黄彩度(oklch hue 84 ≈ 米黄),墨色也换成暖褐而非纯黑: + +| token | 值 | 说明 | +|---|---|---| +| `--background` | `oklch(0.964 0.014 84)` | 页面底 `#f8f3e9` | +| `--surface-subtle` | `oklch(0.952 0.016 84)` | 区块底 `#f4eee3`,比页面底再深一档 | +| `--card` | `oklch(0.98 0.011 84)` | 卡片**比页面底亮**,靠明度差拉出层次 `#fcf8f0` | +| `--foreground` | `oklch(0.235 0.013 68)` | 暖墨 `#221d18`,不是 `#000` | +| `--muted-foreground` | `oklch(0.505 0.012 70)` | 元信息,对比度 5.3:1 | +| `--border` / `--input` | `oklch(0.888 0.016 84)` | | + +对比度都还留得很足(正文 15:1、次要文字 7.5:1、主色链接 4.9:1,WCAG AA 要求 4.5:1),所以是「柔和」不是「发灰」。**暗色模式没动。** + +调暖 / 调冷就改这几个数字的**色相**(84 = 米黄,70 更偏橙,95 更偏柠檬);想更白就把 L 往上推、彩度往下压。 + +### 纸纹底 + +`--paper-noise` 是一段内联 SVG 的 `feTurbulence` 分形噪声,不需要任何图片文件: + +```css +--paper-noise: url("data:image/svg+xml,...feTurbulence baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'..."); +--paper-noise-opacity: 0.2; /* 暗色 0.22 */ +``` + +由 `body::before` 铺成一层 `position: fixed` 的整屏颗粒(`z-index: 1`、`pointer-events: none`)。它有两个作用:给纯色底一点纸的质感;让 `backdrop-filter` 有细节可模糊 —— 背后是纯色时磨砂等于不存在。 + +调参: + +- **颗粒粗细**看 `baseFrequency`,越大越细(0.7–0.9 是细砂纸,0.3 会变成明显斑块) +- **浓淡**看 `--paper-noise-opacity`。实测颗粒亮度标准差:0.05 → 0.7,0.2 → 1.9,0.3 → 2.9(0 = 完全平整)。2 左右是「看得出是纸、但不觉得脏」的档位 +- `stitchTiles='stitch'` 不能删,否则平铺有接缝 +- 伪元素用的是**灰阶 alpha 噪点**(`feColorMatrix saturate=0`),所以不需要 `mix-blend-mode`,亮暗通用 + +### 移动端字号 移动端(<768px)的字号比桌面端小一档,用 Tailwind 的移动优先写法表达 —— 基础值是手机用的,`md:` 前缀是桌面恢复值: ```html @@ -188,6 +222,17 @@ prose-p:leading-[1.78] md:prose-p:leading-[1.85] 改动集中在 `ArticleContent.astro`(正文、标题、引用、列表)和 `article-list.tsx`(列表区标题)。**不要动 `html` 的根字号**,那会连带缩放所有 rem 间距,容易连锁出问题。 +### 移动端分类胶囊条 + +手机端侧栏(`sidebar.tsx`)被排在文章列表**下面**(实测第一篇文章 y=999、侧栏 y=3212),分类筛选基本摸不到。所以 `article-list.tsx` 在列表顶部额外渲染了一条吸附胶囊条: + +- `sticky top-[var(--header-h)]`,跟在固定页眉下面;`-mx-6` 拉通全宽,内部再 `px-6` 对齐正文 +- 左端第一个胶囊是当前模式,末尾一个胶囊用来在「分类 / 标签」之间切换 +- 两端渐隐提示可以横向滑(靠 `filterEdges` 状态控制显隐),滚动条用 `.no-scrollbar` 藏掉 +- **复用同一份** `categories` / `activeCategory` / `handleCategoryChange`,不写第二套逻辑;同时把 `sidebar.tsx` 里那两个块加了 `hidden lg:block`,避免同页出现两份 + +`--header-h: 69px` 是固定页眉的实测高度,移动端菜单和这条胶囊条都靠它定位。**改页眉内边距或头像尺寸时记得同步改它**,否则吸附位置会错。 + ### 磨砂玻璃 `global.css` 里有四个 token: diff --git a/src/components/article-list.tsx b/src/components/article-list.tsx index 7970598..a9739f9 100644 --- a/src/components/article-list.tsx +++ b/src/components/article-list.tsx @@ -301,9 +301,133 @@ export function ArticleList({ setCurrentPage(page) } + /* ---------- 移动端分类胶囊条 ---------- */ + + // 移动端侧栏被推到文章列表下面(实测第一篇文章 y=999、侧栏 y=3212), + // 所以手机上看不到分类筛选。这里在页眉下面再挂一条吸附的胶囊条, + // 复用同一份 categories / activeCategory / handleCategoryChange,不写第二套逻辑。 + const [filterMode, setFilterMode] = useState<"categories" | "tags">(activeTag ? "tags" : "categories") + const filterBarRef = useRef(null) + const [filterEdges, setFilterEdges] = useState({ left: false, right: false }) + + const updateFilterEdges = useCallback(() => { + const el = filterBarRef.current + if (!el) return + const max = el.scrollWidth - el.clientWidth + setFilterEdges({ + left: el.scrollLeft > 4, + right: max > 4 && el.scrollLeft < max - 4, + }) + }, []) + + // 把当前项滚到中间,只操作这一条横向滚动容器,不碰页面纵向位置 + useEffect(() => { + const el = filterBarRef.current + if (!el) return + const active = el.querySelector('[data-filter-active="true"]') + if (!active) { + updateFilterEdges() + return + } + const target = active.offsetLeft - (el.clientWidth - active.offsetWidth) / 2 + el.scrollTo({ left: Math.max(0, target), behavior: "smooth" }) + const timer = window.setTimeout(updateFilterEdges, 350) + return () => window.clearTimeout(timer) + }, [activeCategory, activeTag, filterMode, updateFilterEdges]) + + useEffect(() => { + const el = filterBarRef.current + if (!el) return + updateFilterEdges() + el.addEventListener("scroll", updateFilterEdges, { passive: true }) + window.addEventListener("resize", updateFilterEdges) + return () => { + el.removeEventListener("scroll", updateFilterEdges) + window.removeEventListener("resize", updateFilterEdges) + } + }, [updateFilterEdges, filterMode, categories.length, tags.length]) + + const filterPill = + "shrink-0 inline-flex items-center h-8 px-3 rounded-full border text-[13px] leading-none transition-colors duration-150" + const filterPillOn = "bg-primary border-primary text-primary-foreground" + const filterPillOff = "border-border text-muted-foreground hover:border-primary hover:text-primary" + + const filterBar = ( +
+
+ {filterMode === "categories" ? ( + <> + {categories.map((category) => ( + + ))} +
+ + {/* 两端渐隐,提示还能横向滑 */} +
+ ) + return (
+ {filterBar} {/* Section header */}
- {/* Categories */} -
+ {/* Categories —— 移动端由 article-list 顶部的吸附胶囊条代替,这里只在 lg 以上显示 */} +

分类

@@ -120,8 +120,8 @@ export function Sidebar({ )}
- {/* Tags */} -
+ {/* Tags —— 同上,移动端在胶囊条里切到「标签」即可 */} +

标签

diff --git a/src/components/site-header.astro b/src/components/site-header.astro index d9342e5..b142743 100644 --- a/src/components/site-header.astro +++ b/src/components/site-header.astro @@ -86,7 +86,7 @@ const isCurrent = (href: string) =>