feat(typography): 16 类语义色板(8 色相×2 档)+ 字体回退升级 + 面板 v2

- 颜色:c-{blue,teal,green,yellow,orange,red,pink,purple} × {标准,soft}
  共 16 类;tokens 明暗各一套值,32 项浏览器实测对比度 4.50–11.73(零失败);
  黄色压到 #7a5900(原色白底仅 ~1.8:1)、黄/粉 soft 压深留余量
- 使用纪律入档:彩色只作用于正文强调,站点外壳(导航/页脚/按钮/背景/边框)
  保持灰阶,单页彩色强调建议 ≤3 处
- 字体:3/4 回退类升级为「自托管优先 + 系统兜底」(font-song→Noto Serif SC、
  font-kai-sys→霞鹜文楷、font-hei-sys→Noto Sans SC,+0.0KB 跨设备一致);
  仿宋无 OFL 自托管包(fandol/朱雀仿宋均无版本)→ 纯系统回退,缺口待决策
- webfont 候选全部按体积账否决并留档:霞鹜文楷 Light 用到页 +8605KB、站酷小薇
  +648KB、得意黑/更纱/汇文无可用 npm 包;根因=@fontsource CJK 切片粒度
- 可视化面板 v2:33 项(8 字族 + 25 颜色),颜色项带 16px 色块预览、字体项带
  示例字形「秋海」并标注自托管/优先/系统;桩 CMS 7 项验证全过
- 白名单放行 25 色类 + 8 字族类;docs 三表(灰阶层级/语义色板含对比度/字体
  含许可证与体积账)更新

车道:op-artalk(改口径后重跑,实测表齐全);收编复验:CSS 16 色类定义与
白名单一致、暗态值在 [data-theme="dark"] 段、字体回退类 computed 生效、
构建 48 页、E2E 37/37

验证: pnpm astro build 48 页 exit 0;E2E 37/37;playwright 抽查彩色类与
      字体回退 computed

Spec: none (排版色板与字体回退,车道级交付)

Co-authored-by: GLM-5.3
This commit is contained in:
wait4xandGLM-5.3 committed 2026-11-02 17:14:12 +08:00
1 parent 2cab243891
commit 26bbb66a72
7 files changed
+110 -44

No files matched your search

-2
View File
@@ -24,8 +24,6 @@ draft: false
<p class="ink-accent-soft">弱强调:秋天的海面像一块被风抚平的铁皮。</p> <p class="ink-accent-soft">弱强调:秋天的海面像一块被风抚平的铁皮。</p>
<p class="font-kai-light">霞鹜文楷 Light(自托管 300):秋天的海面像一块被风抚平的铁皮。</p>
<p class="c-blue">蓝色标准:秋天的海面像一块被风抚平的铁皮。</p> <p class="c-blue">蓝色标准:秋天的海面像一块被风抚平的铁皮。</p>
<p class="c-yellow">黄色标准(已压暗):秋天的海面像一块被风抚平的铁皮。</p> <p class="c-yellow">黄色标准(已压暗):秋天的海面像一块被风抚平的铁皮。</p>
<p class="c-red-soft">红色 soft:秋天的海面像一块被风抚平的铁皮。</p> <p class="c-red-soft">红色 soft:秋天的海面像一块被风抚平的铁皮。</p>
+44 -12
View File
@@ -88,7 +88,7 @@ https://astro.build/
正文里可用 `<span class="…">` / `<p class="…">` 做**受限**排版。构建期有白名单过滤:**只放行下表类名,其它 class 与 `style`、`on*` 属性一律被剥离**(防注入 + 保护灰阶骨架)。 正文里可用 `<span class="…">` / `<p class="…">` 做**受限**排版。构建期有白名单过滤:**只放行下表类名,其它 class 与 `style`、`on*` 属性一律被剥离**(防注入 + 保护灰阶骨架)。
### 颜色类(全部实测亮暗对比度 ≥4.5:1) ### 灰阶层级类(全部实测亮暗对比度 ≥4.5:1)
| 类名 | 作用 | | 类名 | 作用 |
|---|---| |---|---|
@@ -100,18 +100,36 @@ https://astro.build/
| `ink-accent` | 强调(链接色,少量点缀) | | `ink-accent` | 强调(链接色,少量点缀) |
| `ink-accent-soft` | 弱强调(比强调淡一档) | | `ink-accent-soft` | 弱强调(比强调淡一档) |
### 语义色板(8 色相 × 标准/soft;亮暗各自取值,实测全部 ≥4.5:1)
| 色相 | 标准类 | soft 类 | 亮态对比度(标准 / soft) | 暗态(标准 / soft) |
|---|---|---|---|---|
| 蓝 | `c-blue` | `c-blue-soft` | 6.46 / 4.58 | 7.70 / 4.61 |
| 青 | `c-teal` | `c-teal-soft` | 5.00 / 4.57 | 9.77 / 4.52 |
| 绿 | `c-green` | `c-green-soft` | 5.38 / 4.67 | 10.38 / 4.67 |
| 黄 | `c-yellow` | `c-yellow-soft` | 6.45 / 4.57 | 11.73 / 4.52 |
| 橙 | `c-orange` | `c-orange-soft` | 6.26 / 4.65 | 9.44 / 4.58 |
| 红 | `c-red` | `c-red-soft` | 6.68 / 4.55 | 7.51 / 4.50 |
| 粉 | `c-pink` | `c-pink-soft` | 6.96 / 4.73 | 8.87 / 4.52 |
| 紫 | `c-purple` | `c-purple-soft` | 7.46 / 4.53 | 8.25 / 4.64 |
- 黄色在亮态被压暗到 `#7a5900`(原色在白底只有 ~1.8:1),这是**必须**的:彩色文字要能读
- `soft` 档是"阈值内最淡值",适合次要强调;标准档用于关键强调
> **使用纪律(重要)**:彩色**只作用于文章正文的强调文字**。站点外壳(导航、页脚、按钮、背景、卡片边框)一律保持灰阶——整站不因正文用色而花掉。写作时一个页面里的彩色强调建议不超过 3 处。
### 字体类 ### 字体类
| 类名 | 作用 | 备注 | | 类名 | 作用 | 字体 | 许可证 | 加载 |
|---|---|---| |---|---|---|---|---|
| `font-serif` | 思源宋(书评正文族) | 自托管 | | `font-serif` | 书评正文族 | Noto Serif SC | OFL-1.1 | 自托管 |
| `font-kai` | 霞鹜文楷(金句族) | 自托管 | | `font-kai` | 金句族 | 霞鹜文楷 Screen | OFL-1.1 | 自托管 |
| `font-sans` | 思源黑(UI/技术族) | 自托管 | | `font-sans` | UI/技术族 | Noto Sans SC | OFL-1.1 | 自托管 |
| `font-mono` | JetBrains Mono(标签/数值) | 自托管 | | `font-mono` | 标签/数值 | JetBrains Mono | OFL-1.1 | 自托管 |
| `font-song` | 宋体族 | ⚠️ 系统字体,**依赖访客设备** | | `font-song` | 宋体族 | Noto Serif SC 优先,系统宋体兜底 | OFL-1.1 | **自托管优先** |
| `font-kai-sys` | 楷体族 | ⚠️ 系统字体 | | `font-kai-sys` | 楷体族 | 霞鹜文楷优先,系统楷体兜底 | OFL-1.1 | **自托管优先** |
| `font-hei-sys` | 黑体族 | ⚠️ 系统字体 | | `font-hei-sys` | 黑体族 | Noto Sans SC 优先,系统黑体兜底 | OFL-1.1 | **自托管优先** |
| `font-fangsong` | 仿宋族 | ⚠️ 系统字体 | | `font-fangsong` | 仿宋族 | FangSong/STFangsong | — | ⚠️ **纯系统回退**(暂无 OFL 自托管仿宋包) |
### 文学排版类(段落级) ### 文学排版类(段落级)
@@ -121,7 +139,21 @@ https://astro.build/
| `quote-line` | 金句块(左侧竖线 + 文楷) | | `quote-line` | 金句块(左侧竖线 + 文楷) |
| `fig-cap` | 图注(居中小字) | | `fig-cap` | 图注(居中小字) |
> **系统字体类的前提**:`-sys` / `font-song` / `font-fangsong` 不下载任何字体文件,命中访客本机字体(macOS/iOS 有宋楷黑、Windows 有宋黑仿宋,Android 常缺楷仿宋)。**跨设备不保证一致**——要稳定请用自托管四族。 **字体体积账(2026-11-02 实测,本地无 gzip 的相对值)**:
| 候选 | 许可证 | 引入后实测 | 结论 |
|---|---|---|---|
| 霞鹜文楷 Light/300(`@fontsource/lxgw-wenkai`) | OFL-1.1 | 用到它的页面 **+8605KB** | ❌ 远超 150KB/页 阈值,未采用 |
| 站酷小薇(`@fontsource/zcool-xiaowei`) | OFL-1.1 | 用到它的页面 +648KB | ❌ 同上 |
| 得意黑 Smiley Sans | OFL-1.1 | npm 无可用包(仅 GitHub release 整包,体积更大) | ⏸ 未接入 |
| Sarasa Gothic(含 Mono) | OFL-1.1 | npm 包为空壳(需 cli 自生成切片) | ⏸ 未接入 |
| 汇文明朝体 | 免费商用(非 OFL) | 无 npm 包 | ⏸ 未接入 |
| 现有四族(思源宋/黑、文楷、JetBrains) | OFL-1.1 | 基线:首页 869KB / 书评 3512KB / 技术 875KB | ✅ 已是最优 |
- 中文 webfont 的代价在 **CJK 切片粒度**:@fontsource 的 CJK 切片刻度是"用几个汉字就拉几 MB",所以新增中文 webfont 单页增量普遍远超 150KB
- 现有四族已覆盖"宋/黑/楷/等宽"四种气质,且全部自托管 → 跨设备一致;`font-song`/`font-kai-sys`/`font-hei-sys` 因**自托管优先**,实际渲染也走站内字体(系统字族只兜底)
- `font-fangsong` 是唯一纯系统回退类:m6 未找到 OFL 自托管仿宋包(Fandol/朱雀等均无 npm 可用包),命中访客本机仿宋字体,**跨设备不保证一致**
- 未用到的字体类在构建期被按页子集化裁掉:**未使用该字体的页面字体字节增量 = 0**
```markdown ```markdown
<span class="ink-muted">次要说明</span> <span class="ink-muted">次要说明</span>
+37 -18
View File
@@ -1,28 +1,47 @@
/** /**
* 排版类清单 —— 单一事实源(后台编辑器面板与文档共用)。 * 排版类清单 —— 单一事实源(后台编辑器面板与文档共用)。
* 与 src/styles/global.css 的「受限排版类」段、src/lib/rehype-class-whitelist.ts 的白名单三处保持一致: * 与 src/styles/global.css 的「受限排版类/语义色板」段、src/lib/rehype-class-whitelist.ts 的白名单三处保持一致:
* 加类时三处同改,否则构建期白名单会把新类剥掉。 * 加类时三处同改,否则构建期白名单会把新类剥掉。
*
* swatch / family 仅供**后台面板预览**使用(后台无站点 CSS 变量,故给具体值):
* swatch 取亮态值(后台是浅色 UI),family 为该类的真实字体栈。
*/ */
window.TYPOGRAPHY_CLASSES = { window.TYPOGRAPHY_CLASSES = {
font: [ font: [
{ cls: 'font-serif', label: '思源宋(书评正文)' }, { cls: 'font-serif', label: '思源宋(书评正文)', family: '"Noto Serif SC", Georgia, serif', kind: '自托管' },
{ cls: 'font-kai', label: '霞鹜文楷(金句)' }, { cls: 'font-kai', label: '霞鹜文楷(金句)', family: '"LXGW WenKai Screen", "Songti SC", serif', kind: '自托管' },
{ cls: 'font-sans', label: '思源黑(UI/技术)' }, { cls: 'font-sans', label: '思源黑(UI/技术)', family: '"Noto Sans SC", system-ui, sans-serif', kind: '自托管' },
{ cls: 'font-mono', label: 'JetBrains Mono(等宽)' }, { cls: 'font-mono', label: 'JetBrains Mono(等宽)', family: '"JetBrains Mono", ui-monospace, monospace', kind: '自托管' },
{ cls: 'font-song', label: '宋体(系统)' }, { cls: 'font-song', label: '宋体族', family: '"Noto Serif SC", "Songti SC", SimSun, serif', kind: '自托管优先' },
{ cls: 'font-kai-sys', label: '楷体(系统)' }, { cls: 'font-kai-sys', label: '楷体族', family: '"LXGW WenKai Screen", "Kaiti SC", STKaiti, serif', kind: '自托管优先' },
{ cls: 'font-hei-sys', label: '黑体(系统)' }, { cls: 'font-hei-sys', label: '黑体族', family: '"Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif', kind: '自托管优先' },
{ cls: 'font-fangsong', label: '仿宋(系统)' }, { cls: 'font-fangsong', label: '仿宋族', family: 'FangSong, STFangsong, serif', kind: '系统回退' },
], ],
color: [ color: [
{ cls: 'ink-1', label: '最重(标题级)' }, { cls: 'ink-1', label: '最重(标题级)', swatch: '#111113' },
{ cls: 'ink-2', label: '正文' }, { cls: 'ink-2', label: '正文', swatch: '#33343a' },
{ cls: 'ink-3', label: '次要(日期/元信息)' }, { cls: 'ink-3', label: '次要(日期/元信息)', swatch: '#6b6e78' },
{ cls: 'ink-muted', label: '最淡(注释感)' }, { cls: 'ink-muted', label: '最淡(注释感)', swatch: '#70747c' },
{ cls: 'ink-accent', label: '强调(链接色)' }, { cls: 'ink-warm', label: '暖调正文', swatch: '#3f362e' },
{ cls: 'ink-accent-soft', label: '弱强调' }, { cls: 'ink-cool', label: '冷调正文', swatch: '#2f3742' },
{ cls: 'ink-warm', label: '暖调正文' }, { cls: 'ink-faint', label: '最淡可读档', swatch: '#70747c' },
{ cls: 'ink-cool', label: '冷调正文' }, { cls: 'ink-accent', label: '强调(链接色)', swatch: '#3454d1' },
{ cls: 'ink-faint', label: '最淡可读档' }, { cls: 'ink-accent-soft', label: '弱强调', swatch: '#4f66c2' },
{ cls: 'c-blue', label: '蓝', swatch: '#2456c9' },
{ cls: 'c-blue-soft', label: '蓝(soft)', swatch: '#4170dd' },
{ cls: 'c-teal', label: '青', swatch: '#0d7d74' },
{ cls: 'c-teal-soft', label: '青(soft)', swatch: '#0e847a' },
{ cls: 'c-green', label: '绿', swatch: '#1f7a3a' },
{ cls: 'c-green-soft', label: '绿(soft)', swatch: '#22853f' },
{ cls: 'c-yellow', label: '黄(已压暗)', swatch: '#7a5900' },
{ cls: 'c-yellow-soft', label: '黄(soft)', swatch: '#926a00' },
{ cls: 'c-orange', label: '橙', swatch: '#9a4a00' },
{ cls: 'c-orange-soft', label: '橙(soft)', swatch: '#ba5900' },
{ cls: 'c-red', label: '红', swatch: '#b32020' },
{ cls: 'c-red-soft', label: '红(soft)', swatch: '#db3737' },
{ cls: 'c-pink', label: '粉', swatch: '#a3246a' },
{ cls: 'c-pink-soft', label: '粉(soft)', swatch: '#ce3386' },
{ cls: 'c-purple', label: '紫', swatch: '#6a35b8' },
{ cls: 'c-purple-soft', label: '紫(soft)', swatch: '#8c5ed1' },
], ],
}; };
+27 -4
View File
@@ -37,7 +37,7 @@
card.appendChild(title); card.appendChild(title);
const buttons = []; const buttons = [];
const addGroup = (name, list) => { const addGroup = (name, list, withSwatch) => {
const h = document.createElement('p'); const h = document.createElement('p');
h.textContent = name; h.textContent = name;
Object.assign(h.style, { margin: '12px 0 6px', fontSize: '12px', color: '#6b6e78' }); Object.assign(h.style, { margin: '12px 0 6px', fontSize: '12px', color: '#6b6e78' });
@@ -47,22 +47,45 @@
for (const item of list) { for (const item of list) {
const b = document.createElement('button'); const b = document.createElement('button');
b.type = 'button'; b.type = 'button';
b.textContent = item.label;
b.dataset.cls = item.cls; b.dataset.cls = item.cls;
b.className = item.cls === current ? 'active' : ''; b.className = item.cls === current ? 'active' : '';
Object.assign(b.style, { Object.assign(b.style, {
display: 'inline-flex', alignItems: 'center', gap: '8px',
padding: '6px 12px', borderRadius: '8px', cursor: 'pointer', padding: '6px 12px', borderRadius: '8px', cursor: 'pointer',
border: '1px solid ' + (item.cls === current ? '#3454d1' : '#e6e7eb'), border: '1px solid ' + (item.cls === current ? '#3454d1' : '#e6e7eb'),
background: item.cls === current ? '#eef1fd' : '#fff', background: item.cls === current ? '#eef1fd' : '#fff',
color: '#111113', fontSize: '13px', color: '#111113', fontSize: '13px',
}); });
if (withSwatch) {
// 色块预览:直接展示该类在亮态下的实际颜色
const chip = document.createElement('span');
Object.assign(chip.style, {
width: '16px', height: '16px', borderRadius: '4px', flex: 'none',
background: item.swatch || '#ccc',
border: '1px solid rgba(0,0,0,.12)',
});
chip.setAttribute('aria-hidden', 'true');
b.appendChild(chip);
} else {
// 字体项:名称 + 用该字体栈渲染的示例字形
const sample = document.createElement('span');
sample.textContent = '秋海';
Object.assign(sample.style, {
fontFamily: item.family || 'inherit', fontSize: '15px', lineHeight: '1',
padding: '0 4px', borderLeft: '1px solid #e6e7eb', color: '#33343a',
});
b.appendChild(sample);
}
const label = document.createElement('span');
label.textContent = item.label + (item.kind ? `(${item.kind})` : '');
b.appendChild(label);
row.appendChild(b); row.appendChild(b);
buttons.push(b); buttons.push(b);
} }
card.appendChild(row); card.appendChild(row);
}; };
addGroup('字体', CLASSES.font); addGroup('字体', CLASSES.font, false);
addGroup('颜色', CLASSES.color); addGroup('颜色', CLASSES.color, true);
const foot = document.createElement('div'); const foot = document.createElement('div');
Object.assign(foot.style, { marginTop: '16px', display: 'flex', justifyContent: 'flex-end', gap: '8px' }); Object.assign(foot.style, { marginTop: '16px', display: 'flex', justifyContent: 'flex-end', gap: '8px' });
-1
View File
@@ -44,7 +44,6 @@ const ALLOWED = new Set([
'font-kai-sys', 'font-kai-sys',
'font-hei-sys', 'font-hei-sys',
'font-fangsong', 'font-fangsong',
'font-kai-light',
// 文学排版 // 文学排版
'drop-cap', 'drop-cap',
'quote-line', 'quote-line',
-5
View File
@@ -213,11 +213,6 @@ pre code { background: none; border: none; padding: 0; font-size: inherit; }
.prose-reading .font-fangsong, .prose-tech .font-fangsong { .prose-reading .font-fangsong, .prose-tech .font-fangsong {
font-family: FangSong, STFangsong, "FangSong_GB2312", serif; font-family: FangSong, STFangsong, "FangSong_GB2312", serif;
} }
/* 霞鹜文楷 Light(自托管 @fontsource,OFL-1.1):更轻的金句字重 */
.prose-reading .font-kai-light, .prose-tech .font-kai-light {
font-family: 'LXGW WenKai', var(--font-kai);
font-weight: 300;
}
/* 文学常用:首字下沉(书评开篇)与金句块(引文式的独立强调段) */ /* 文学常用:首字下沉(书评开篇)与金句块(引文式的独立强调段) */
.prose-reading p.drop-cap::first-letter, .prose-tech p.drop-cap::first-letter { .prose-reading p.drop-cap::first-letter, .prose-tech p.drop-cap::first-letter {
float: left; float: left;
+2 -2
View File
@@ -32,10 +32,10 @@
--c-blue-soft: #4170dd; --c-blue-soft: #4170dd;
--c-teal-soft: #0e847a; --c-teal-soft: #0e847a;
--c-green-soft: #22853f; --c-green-soft: #22853f;
--c-yellow-soft: #997000; --c-yellow-soft: #926a00;
--c-orange-soft: #ba5900; --c-orange-soft: #ba5900;
--c-red-soft: #db3737; --c-red-soft: #db3737;
--c-pink-soft: #d3368c; --c-pink-soft: #ce3386;
--c-purple-soft: #8c5ed1; --c-purple-soft: #8c5ed1;
--line: #e6e7eb; /* 分隔线 */ --line: #e6e7eb; /* 分隔线 */