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

+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 = {
font: [
{ cls: 'font-serif', label: '思源宋(书评正文)' },
{ cls: 'font-kai', label: '霞鹜文楷(金句)' },
{ cls: 'font-sans', label: '思源黑(UI/技术)' },
{ cls: 'font-mono', label: 'JetBrains Mono(等宽)' },
{ cls: 'font-song', label: '宋体(系统)' },
{ cls: 'font-kai-sys', label: '楷体(系统)' },
{ cls: 'font-hei-sys', label: '黑体(系统)' },
{ cls: 'font-fangsong', label: '仿宋(系统)' },
{ cls: 'font-serif', label: '思源宋(书评正文)', family: '"Noto Serif SC", Georgia, serif', kind: '自托管' },
{ cls: 'font-kai', label: '霞鹜文楷(金句)', family: '"LXGW WenKai Screen", "Songti SC", serif', kind: '自托管' },
{ cls: 'font-sans', label: '思源黑(UI/技术)', family: '"Noto Sans SC", system-ui, sans-serif', kind: '自托管' },
{ cls: 'font-mono', label: 'JetBrains Mono(等宽)', family: '"JetBrains Mono", ui-monospace, monospace', kind: '自托管' },
{ cls: 'font-song', label: '宋体族', family: '"Noto Serif SC", "Songti SC", SimSun, serif', kind: '自托管优先' },
{ cls: 'font-kai-sys', label: '楷体族', family: '"LXGW WenKai Screen", "Kaiti SC", STKaiti, serif', kind: '自托管优先' },
{ cls: 'font-hei-sys', label: '黑体族', family: '"Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif', kind: '自托管优先' },
{ cls: 'font-fangsong', label: '仿宋族', family: 'FangSong, STFangsong, serif', kind: '系统回退' },
],
color: [
{ cls: 'ink-1', label: '最重(标题级)' },
{ cls: 'ink-2', label: '正文' },
{ cls: 'ink-3', label: '次要(日期/元信息)' },
{ cls: 'ink-muted', label: '最淡(注释感)' },
{ cls: 'ink-accent', label: '强调(链接色)' },
{ cls: 'ink-accent-soft', label: '弱强调' },
{ cls: 'ink-warm', label: '暖调正文' },
{ cls: 'ink-cool', label: '冷调正文' },
{ cls: 'ink-faint', label: '最淡可读档' },
{ cls: 'ink-1', label: '最重(标题级)', swatch: '#111113' },
{ cls: 'ink-2', label: '正文', swatch: '#33343a' },
{ cls: 'ink-3', label: '次要(日期/元信息)', swatch: '#6b6e78' },
{ cls: 'ink-muted', label: '最淡(注释感)', swatch: '#70747c' },
{ cls: 'ink-warm', label: '暖调正文', swatch: '#3f362e' },
{ cls: 'ink-cool', label: '冷调正文', swatch: '#2f3742' },
{ cls: 'ink-faint', label: '最淡可读档', swatch: '#70747c' },
{ cls: 'ink-accent', label: '强调(链接色)', swatch: '#3454d1' },
{ 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);
const buttons = [];
const addGroup = (name, list) => {
const addGroup = (name, list, withSwatch) => {
const h = document.createElement('p');
h.textContent = name;
Object.assign(h.style, { margin: '12px 0 6px', fontSize: '12px', color: '#6b6e78' });
@@ -47,22 +47,45 @@
for (const item of list) {
const b = document.createElement('button');
b.type = 'button';
b.textContent = item.label;
b.dataset.cls = item.cls;
b.className = item.cls === current ? 'active' : '';
Object.assign(b.style, {
display: 'inline-flex', alignItems: 'center', gap: '8px',
padding: '6px 12px', borderRadius: '8px', cursor: 'pointer',
border: '1px solid ' + (item.cls === current ? '#3454d1' : '#e6e7eb'),
background: item.cls === current ? '#eef1fd' : '#fff',
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);
buttons.push(b);
}
card.appendChild(row);
};
addGroup('字体', CLASSES.font);
addGroup('颜色', CLASSES.color);
addGroup('字体', CLASSES.font, false);
addGroup('颜色', CLASSES.color, true);
const foot = document.createElement('div');
Object.assign(foot.style, { marginTop: '16px', display: 'flex', justifyContent: 'flex-end', gap: '8px' });