主题体系跟上设计令牌化:派生文字色按对比度下限、主题 muted 补到 AA

生成器(dev/theme-builder):
- paletteVars 的 text-secondary/text-muted 从「往底色混 36%/55%」改成 mixToContrast(5.5/4.5):二分找最淡且达标的位置,文字本身不达标就原样返回
- 半透明卡片先 flatten 压平到底色再算对比度;参考背景取「页面底色 + 压平后的卡片」中更不利的
- 配色面板加读数(正文/次要/淡文字/主色),低于 4.5 变橙;默认 primary #4a7fc1(3.69) → #3d6fac(4.63)
- 新增 ?dump=palette 四种配色回归;README 补配色一节

主题/base(含用户本轮 base.css 改动):
- sakura muted #a08c94(2.8:1) → #78696e(4.61)、shiroko #8794a7(2.76:1) → #656e7a(4.63):主题覆盖令牌,基座的 AA 修复到不了主题页
- 深色顶栏链接补 secondary 覆盖:亮色块已是次要文字色,深色块原来 !important 定成正文色,同一顶栏权重跳一档
- _scaffold.css 令牌清单 23 → 29(补 fs 阶梯与 page-position),muted 说明 AA 下限
- StaticVersion 20261021a → 20261021b
This commit is contained in:
tamakyi
2026-10-21 17:42:43 +08:00
parent 54df166d4e
commit db4b3fa095
11 changed files with 317 additions and 21 deletions
+99
View File
@@ -346,3 +346,102 @@ CLI 端到端手动验证:`honeypot hits` / `--ip` / `--show-password` 输出
- `docs/cli.md`:§0 补「替换二进制必须重启」「strings 用 grep -ac」;
命令总览加 honeypot;新增 §7 honeypot 命令(原 7/8/9 顺延为 8/9/10);
排错表加 5 条。
# 2026-10-21
## 新旧前端对比(正式 vs 测试环境),只测量未改码
**方法**:curl 抓两环境 4 个页面 → 本地注入脚本取计算样式 / 资源体积 / 对比度 / 点按区;
无头 Chrome 截图(`--headless=old`)后用 Pillow 算像素指标。
⚠️ 本模型读不了图片,所以结论全部来自数值与结构,不靠"看截图"。
**踩坑**:file:// 打开抓下来的页面时,`href="/static/css/base.css"` 会解析成 `file:///static/...` → CSS 不加载,
于是 dev 页量出来全是 UIkit 旧样式(圆角 0、旧阴影)。改法:抓下来后把 `/static/` 重写成 dev 域名绝对地址,
再重新采集。**以后本地复现线上页面必须先重写站内绝对路径,否则测的是"无 CSS 的裸 HTML"。**
另:`body.scrollWidth` 会被隐藏面板撑大(dev explore 1499),判断横向溢出要看 `documentElement.scrollWidth`(实测 1249 = 无溢出)。
**实测差异(1280 宽)**
| 指标 | 正式(旧) | 测试(新) |
|---|---|---|
| 首页 HTML | 42.9KB | 34.2KB |
| 探索页 HTML | 77.4KB | 53.4KB |
| `<style>` 块 | 9–10 | 2 |
| 行内 style 属性 | 8–16 | 3–5 |
| 卡片圆角 / 阴影 | 0px / 单层硬阴影 | 12px / 双层柔阴影 |
| 圆角取值种类 | 2px×73、50%、8、6、4、9 | 6、8、12、14、5、50% |
| 正文色 | #333/#666/#999 混用 | 令牌 #1f2329/#5c6370/#8a919c |
| 字体栈 | UIkit 默认 | system-ui + 苹方/雅黑 |
| 图标 | emoji(🌙☀️) | 内联 SVG |
| 首页截图像素:色彩数 / 边缘密度 | 2206 / 10.3% | 1174 / 5.0% |
**共同短板(新旧都没解决)**
- `--tb-text-muted #8a919c` 在 #fff 上只有 **3.18:1**(AA 需 4.5);旧版 #999 更低(2.85)。
建议改 #6b7280(4.83:1);暗色 muted #8b93a1 在 #242830 上 4.77 已达标。
- 顶栏导航链接(UIkit rgba(0,0,0,.5))2.68–2.85:1,同样是低对比。
- 移动端点按区:两版都有大量 19px 高的纯文字链接(不满足 44px 建议值)。
**新版自身的毛病(我写的,待修)**
- `templates/home.html` 字号用了 0.75/0.78/0.86/0.9/0.95/0.98 rem → 7 个肉眼难辨的档位;
`static/css/base.css` 里还有 0.78/0.8/0.82/0.84/0.88/0.95。**排版层级比旧版更碎**,应收敛到 4 档。
- 残留圆角 5px(header 验证码框、sponsor)、9px(profile-tabs,list.html/explore.html 各一处)。
## 按上轮结论改代码(字号收敛 / 对比度 / 圆角收敛)
先确认过:本地 `static/css/base.css` 与 dev 部署版 **diff 0 行**,本地就是新版基线。
1. **新增字号阶梯令牌**(base.css `:root`):`--tb-fs-xs/md/lg/xl/hero` = 12/14/16/24/28px。
`lg` 取 **1rem 而非 17px** —— UIkit 的 `.uk-card-title`/h4 就是 16px,顺着它才能让自定义标题
与组件标题同档,否则会留下 16 与 17 差 1px 的两档并存(第一版就是这么写的,实测发现后改回)。
2. **替换范围**:home.html 全部 7 档 → 5 档;explore / question-list / question-list(list.html) 三页
的 0.7/0.72/0.82/0.84/0.85/0.9/0.98/1/1.15/1.2 全换令牌。实测 explore 由 **9 档 → 5 档**。
3. **`--tb-text-muted` #8a919c → #6b7280**(#fff 上 4.83:1)。**关键坑**:
UIkit 自带 `.uk-text-muted{color:#999!important}`,我原先的普通声明**从未生效**,全站仍是 #999(2.85:1)。
必须 `!important` 才能压过。改完 box 页 fails 15→11、explore 14→9。
4. **顶栏导航**:UIkit 默认 rgba(0,0,0,.5) = 2.68:1 → 改 `var(--tb-text-secondary)` = **6.05:1**,
hover/active 走主色;暗色分支原有 `!important` 规则自然接管(实测 10.75:1)。
5. **圆角全量收敛**:9px/5px/4px/2px/7px/3px 以及等值的 6/8/10/12/14px 全部换成
`var(--tb-radius[-sm|-xs])`。全站(排除邮件模板)现在只剩 3 处 `999px`(胶囊,语义正确)。
6. `conf.StaticVersion` 20261020g → **20261021a**(base.css 改了必须 +1)。
**验证**:go vet / go test(templates+i18n+conf)/ go build 全绿;预览页内联本地 base.css 后用无头 Chrome
量计算样式,首页 12/14/16/24/28、explore 12/14/16/24/32,nav 6.05:1。
**遗留(未改,属设计取舍)**:主色链接 `#1e87f0` 在纯白上 3.37–3.42:1(<4.5)。改深会影响品牌观感,
需用户决定;Waline 第三方赞数绿色 2.87 不在本站控制内。
**注意**:工作区有 4 个**非本次改动**的删除(`static/css/themes/test01.css`、`static/images/shiroko-cutout.png`、
`static/images/themes/test01/*.webp`),是之前就存在的未提交删除,提交时勿一并 add。
## 17:4x 用户前端设计令牌化 → 主题/生成器的连带影响
用户改了 12 个模板 + base.css(未提交):`--tb-text-muted` #8a919c→#6b7280(AA 4.83:1)、
新增 `--tb-fs-xs/md/lg/xl/hero` 阶梯、圆角改走 `--tb-radius(-sm/-xs)`、顶栏链接改 `--tb-text-secondary`、
`.uk-text-muted` 加 `!important`(压 UIkit 的 `#999!important`)。
**关键结论:主题会把这套修复盖回原样。** 主题 css 覆盖同名令牌 → 页面用主题的值,不是 base.css 的。
实测(worse of bg / 压平后的 surface):
- default.css 不定义文字色 → 跟随 base ✓;sakura muted `#a08c94` **2.80:1**、shiroko `#8794a7` **2.76:1**(都看不见)。
- 已调到 AA 下限:sakura `#78696e`(4.61)、shiroko `#656e7a`(4.63),同色相只压深;深色块两个主题本来就 ≥4.7,不动。
- 主色仍不达标(未改,等用户定):base/shiroko `#4a7fc1` 3.69、base `#1e87f0` 3.26、sakura `#e5678f` 2.80。
候选:`#3d6fac`(4.63)、`#0f6ac6`(4.83)、`#b53d61`(4.91)。
**生成器的必修项**:`paletteVars` 原来 `text-muted = mix(text,bg,0.55)` 硬算 → 浅底掉到 2.9:1,
等于每导一次主题就把 base.css 的修复覆盖掉。改成 `mixToContrast(text, [bg, flatten(surface,bg)], 4.5)`:
二分找「最淡但达标」的比例;文字本身不达标时原样返回(宁可不够淡,不能看不见)。
- 半透明卡片(`rgba(255,255,255,0.86)`)必须**压平到底色**再算,否则按纯白算会偏低。
- 次要 5.5 / 弱化 4.5 两档,保证层级不倒挂。
- 配色面板加读数 `正文 12.5:1 · 次要 5.5:1 · 淡文字 4.6:1 · 主色 4.6:1`,<4.5 变橙(手填主色只提醒不拦)。
- 默认 primary 由 `#4a7fc1`(3.69) 压到 `#3d6fac`(4.63) —— 默认值不该一上手就踩线。
- 新增 `?dump=palette`(四种配色:默认亮/默认深/低对比手填/白卡彩底),`ok:true`。
**顺手补的一致性**:深色块顶栏原来 `!important` 定成 `--tb-text`(正文色),亮色块新的 secondary 规则
让同一顶栏在深浅两模式权重跳一档 → 加 `html[data-theme="dark"] .uk-navbar-nav > li > a { secondary !important }`。
实测计算值:无主题 nav #5c6370 / muted #6b7280;shiroko #51607a / #656e7a;sakura #6b545c / #78696e。
**⚠️ 用户删了 test01(css + 2 张图)与 `shiroko-cutout.png`,未提交,我没动。** 若测试站 `[theme] available`
还登记着 test01,启动日志会打「主题文件不存在,已从可用列表忽略」,设置页不再出现,箱主已选 test01 的会静默回退站点默认。
`shiroko-cutout.png` 只在 `docs/theme-shiroko.md` 里作参考图引用,不参与 embed。
**令牌化的残留**(只提醒,没改):`templates/user/profile.html` 还有 16 处 `font-size: 0.XXrem`;
`base.css` 自身 11 处(0.78/0.82/0.88/0.95/1.1 —— 正是被点名要消掉的那档);圆角只剩 50%/999px(语义正确)。
`--tb-fs-*` 在 base.css 里只用了 3 处(主题格、chip),阶梯主要落在模板层。
`StaticVersion` 20261021a → **20261021b**(主题 css 内容变了,必须换键)。
+8 -1
View File
@@ -20,7 +20,14 @@
⚠️ `@media` 里**每个独立配置的 scope 都要写一条** `[data-page-scope="x"]`:页面覆盖块特异性 (0,2,1),
只写 `html[data-theme-style]`(0,1,1) 的话独立配置页在手机上完全不生效。
⚠️ 竖版图只替换**本来有背景图**的页面(`cfg.bg` 非空),否则等于给纯色页塞了张图。
- 自检钩子:`?dump=css|zip|validate|install|fx|fxpipe|fxkey|layers|fit|quality`,`?demo=1&fresh=1&rows=N&scope=&loadurl=`
- ⚠️ **派生文字色必须走对比度下限,不能按固定比例混**:`paletteVars` 里
`text-secondary = mixToContrast(text, backdrops, 5.5)`、`text-muted = 4.5`(WCAG AA)。
旧的 `mix(text,bg,0.55)` 在浅底只有 2.9:1 —— 每导一次主题就把 base.css 的 AA 修复覆盖回去。
- 半透明卡片(`--tb-surface: rgba(255,255,255,0.86)`)必须 `flatten()` 压到底色再算,否则按纯白算偏低;
- 参考背景取「页面底色 + 压平后的卡片」两者更不利的那个;
- 手填正文本身不达标时**原样返回**(宁可不够淡,不能产出看不见的字)。
- 配色面板有读数(正文/次要/淡文字/主色),<4.5 变橙;默认 primary 已压到 `#3d6fac`(4.63)。
- 自检钩子:`?dump=css|zip|validate|install|fx|fxpipe|fxkey|layers|fit|quality|palette`,`?demo=1&fresh=1&rows=N&scope=&loadurl=`
(`?demo=1` 已含 fit 层 + 竖版背景,`@media` 才会生成)。用法:
`chrome --headless=old --virtual-time-budget=12000 --dump-dom "http://127.0.0.1:8777/dev/theme-builder/index.html?demo=1&fresh=1&dump=fit"`
抓 `<pre id="dump">`(正则要带属性:`<pre id="dump"[^>]*>`)。
+13
View File
@@ -13,6 +13,19 @@
- ⚠️ **UIkit 3.3.3 的 `.uk-flex::before/::after{display:none}`**:给带 `uk-flex` 的元素加伪元素必须写
复合选择器(`.tb-cover.uk-flex::after`),否则静默失效。
- 分类/题型标签一律 `.tb-chip`;彩色 `uk-label` 只留给语义状态(置顶/私密/被举报/已读、投票四态)。
- **字号阶梯 5 档**:`--tb-fs-xs/md/lg/xl/hero`(12/14/16/24/28)。散落的 `.9/.95/.98rem` 一律映射到档位,
别新增第 6 个值。⚠️ 已令牌化的是模板层;`base.css` 自身仍留 11 处硬编码(0.78/0.82/0.88/0.95/1.1),
`templates/user/profile.html` 还留 16 处 —— 想彻底统一得先扫这两处。
- 圆角只用 `--tb-radius(-sm/-xs)`;`50%` 与 `999px` 是语义(头像、胶囊),**不要**换成令牌。
- ⚠️ `.uk-text-muted/.uk-text-meta` 必须带 `!important`:UIkit 3.3.3 自带 `color:#999!important`,
普通声明压不过(现象:规则写了但全站还是 #999)。
- **文字色下限 WCAG AA 4.5:1**(正文级)。`--tb-text-muted` = `#6b7280`(白底 4.83)。
旧值 `#8a919c` 3.18、`#999` 2.85 —— 肉眼"淡一点"其实已经不合格。
主色还没达标(`#1e87f0` 3.26 / `#4a7fc1` 3.69 / sakura `#e5678f` 2.80),要动得连主题一起改。
- ⚠️ **改 base.css 的配色令牌必须同步检查 `static/css/themes/*.css`**:主题覆盖同名令牌,
基座修了但主题页纹丝不动(sakura/shiroko 的 muted 就曾是 2.8:1,是这次才补到 4.6)。
- 顶栏链接:亮色用 `--tb-text-secondary`、hover/当前页 `--tb-primary`;深色块原本 `!important` 定成
`--tb-text`,会让同一顶栏在深浅两模式权重跳一档 → 已加 dark secondary 覆盖。
- 视觉自检:`TEST_PREVIEW=1 go test ./templates -run TestGenPreview` → `outputs/preview-*.html`;
截图必须 `chrome --headless=old --force-device-scale-factor=1`(新版 headless 本机挂住)。
⚠️ `--window-size` ≠ CSS 视口;要精确视口就用 iframe 指定 width/height。验证 CSS 靠**像素比对**
+8
View File
@@ -42,3 +42,11 @@
- ⚠️ **抠白底容差 16**:背景 253、白袜子 237 只差 16,容差 22 以上会把袜子吃穿。手内侧/两腿之间是
**闭合**白区,四边 floodfill 进不去,得按连通域单独掏(面积 ≥24),再 1px 收边。
- ⚠️ `ruleBody` 取令牌前要剥 `/* */`:注释会和后面的令牌粘成一个键,色块预览静默失效。
- ⚠️ **主题会覆盖 base.css 的同名令牌 —— 基座改配色必须同步改主题**。改 `--tb-text-muted` 时
sakura `#a08c94`(2.8:1)、shiroko `#8794a7`(2.76:1) 都还没跟上,"修了但主题页没变"就是这么来的。
现已调到 `#78696e`(4.61) / `#656e7a`(4.63),同色相只压深。主色仍不达标(见 topics/style.md)。
量对比度时半透明 `--tb-surface` 要**压平到 --tb-bg** 再算,别当纯白。
- 主题选择页的色块由浅色块的 `--tb-bg` + `--tb-primary` 生成(`swatchOf`),注释里别写令牌名。
- `test01` 是用户 2026-10-21 自建又删掉的验证主题(css + 2 张 webp 已移除,未提交)。
删主题只需删文件:`build()` 读不到 css 会打 warning 并忽略,已选该主题的用户静默回退站点默认。
`static/images/shiroko-cutout.png` 只是 `docs/theme-shiroko.md` 的生成参考图,不进 embed。
+18
View File
@@ -133,6 +133,23 @@ http://127.0.0.1:8777/dev/theme-builder/
生成 `--tb-page-position`(`base.css` 里以 `var(--tb-page-position, center center)` 兜底,
旧主题不写这个令牌也不会坏)。
**配色** —— 只填 6 个基础色(底色 / 卡片 / 顶栏 / 正文 / 主色 / 边框),
其余 4 个派生:`surface-2` 往正文靠、`surface-hover` 往主色靠、`primary-weak` 加 12% 透明、
`text-secondary` / `text-muted` 往底色淡出。
⚠️ **派生文字色有对比度下限,不是固定比例**。正文往背景淡出时,二分找到「最淡但仍 ≥ 目标」的位置:
次要文字 5.5:1、弱化文字 4.5:1(WCAG AA 正文)。旧版按「混 55%」硬算,
在浅底上直接掉到 2.9:1 —— `base.css` 特意把 `--tb-text-muted` 从 `#8a919c` 提到 `#6b7280`(4.83:1),
生成器按固定比例算等于每导出一次就把那个修复覆盖回去。
- 卡片是半透明(`rgba(255,255,255,0.86)`)时先**压平到页面底色**再算,否则白卡会被当成纯白、对比度偏低;
- 同时检查页面底色与卡片两个背景,取更不利的那个;
- 手填的正文本身就达不到目标时**原样返回**(不再往淡里调)——宁可不够淡,不能产出看不见的字;
- 面板底部有读数 `正文 12.5:1 · 次要 5.5:1 · 淡文字 4.6:1 · 主色 3.4:1`,低于 4.5 变橙(手填色只提醒不拦)。
顺带:这套字号阶梯(`--tb-fs-xs…hero`)来自 `base.css`,生成器**不碰字号** ——
主题的职责是配色与背景,改字号会连带挤坏所有页面排版。
**图片池** —— 上传任意张,浏览器本地转 WebP。导出时只打包实际用到的图。
每张图左上角有 `fx` 按钮,进处理面板(处理过的会高亮)。
@@ -252,6 +269,7 @@ INSTALL-<slug>.txt
?dump=layers 图层/作用域回归(加层默认不绑图、行内换图、自动切独立、新建清空)
?dump=fit 尺寸自适应回归(五档视口占用、溢出检测、fitw、旧工程兼容、@media 覆盖各作用域)
?dump=quality 画质回归(q 单调性、q=1 被夹到 0.95、长边上限真的压像素)
?dump=palette 派生文字色的对比度回归(四种配色下 ≥AA、层级不倒挂、手填低对比不继续变淡)
?dump=fxkey 端到端去白底:载入白底图,统计透明像素比例
?loadurl=/static/images/x.webp&fx=x
直接把项目里的图当素材载入(不必先下载再上传),fx= 打开处理面板
+113 -3
View File
@@ -326,8 +326,10 @@ const MOB_SCALE_DEFAULT = 0.62;
/* 默认配色/窄屏抽出来,「新建」时能原样还原;S 里存的是副本,不会被重置污染 */
const D = {
palette: {
/* primary 从 #4a7fc1 压深到 #3d6fac:前者在 #eef3f9 上只有 3.69:1,
14px 的链接/按钮文字不达 AA。默认值应当是可用的,不能一上手就踩红线。 */
bg:'#eef3f9', surface:'rgba(255,255,255,0.86)', navbar:'rgba(255,255,255,0.52)',
text:'#222d3b', primary:'#4a7fc1', border:'#d6e0ec',
text:'#222d3b', primary:'#3d6fac', border:'#d6e0ec',
},
/* 窄屏:手机是「卡片占满宽度」,桌面那套 16:9 场景会被 cover 裁成一条无意义纹理。
这一份是全局的(不分页面)—— 窄屏的问题是空间不够,各页面诉求差别不大。
@@ -641,8 +643,50 @@ function withAlpha(c, a){
const A = parseColor(c);
return `rgba(${A[0]}, ${A[1]}, ${A[2]}, ${a})`;
}
/** 半透明色压到某个不透明底色上,得到「实际看到」的颜色。
卡片的 --tb-surface 常写成 rgba(255,255,255,0.86),直接拿它算对比度是错的。 */
function flatten(c, under){
const A = parseColor(c), B = parseColor(under);
if (A[3] >= 1) return `rgb(${A[0]}, ${A[1]}, ${A[2]})`;
const m = i => Math.round(A[i] * A[3] + B[i] * (1 - A[3]));
return `rgb(${m(0)}, ${m(1)}, ${m(2)})`;
}
/* WCAG 相对亮度 / 对比度。文字色的下限不是审美问题,是硬指标:
正文 4.5:1(AA)。base.css 已经按这个标准重算过 --tb-text-muted,
主题若按「往底色混 55%」拍脑袋派生,等于每次生成都把那个修复覆盖回去。 */
function relLum(c){
const A = parseColor(c);
const f = v => {
v /= 255;
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
};
return 0.2126 * f(A[0]) + 0.7152 * f(A[1]) + 0.0722 * f(A[2]);
}
function contrastRatio(a, b){
const la = relLum(a), lb = relLum(b);
return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
}
/** 在 text→bg 的连线上取「最淡但仍达标」的点:二分找出刚好达标的混合比例。
文字本来就不达标(底色与文字太接近)时原样返回文字色 —— 宁可不够淡,
也不能产出一行看不见的字。bgs 传多个时按最不利的那个算(卡片叠在底色上)。 */
function mixToContrast(text, bgs, target){
const worst = t => Math.min(...bgs.map(b => contrastRatio(mixRGB(text, bgs[0], t), b)));
if (worst(0) <= target) return text;
let lo = 0, hi = 0.92;
for (let i = 0; i < 14; i++){
const mid = (lo + hi) / 2;
if (worst(mid) >= target) lo = mid; else hi = mid;
}
// 往下收半档:浮点四舍五入后掉回 4.49:1 这种「差一点」最难查
return mixRGB(text, bgs[0], Math.floor(lo * 100) / 100);
}
/** 文字要同时落在页面底色和卡片上:把卡片的不透明度压平后,取两者中更不利的那个。 */
function textBackdrops(p){
return [p.bg, flatten(p.surface, p.bg)];
}
function paletteVars(p){
const back = textBackdrops(p);
return [
` --tb-bg: ${p.bg};`,
` --tb-surface: ${p.surface};`,
@@ -651,8 +695,8 @@ function paletteVars(p){
` --tb-navbar: ${p.navbar};`,
` --tb-border: ${p.border};`,
` --tb-text: ${p.text};`,
` --tb-text-secondary: ${mixRGB(p.text, p.bg, 0.36)};`,
` --tb-text-muted: ${mixRGB(p.text, p.bg, 0.55)};`,
` --tb-text-secondary: ${mixToContrast(p.text, back, 5.5)};`,
` --tb-text-muted: ${mixToContrast(p.text, back, 4.5)};`,
` --tb-primary: ${p.primary};`,
` --tb-primary-weak: ${withAlpha(p.primary, 0.12)};`,
].join('\n');
@@ -1547,6 +1591,25 @@ function renderPalette(){
r.append(s, c, t);
wrap.appendChild(r);
});
// 对比度读数:主题能改文字色,也就有能力把页面改到看不清。
// 派生值现在有下限(不达标就自动收淡),但用户手填的 text / primary 没有 —— 只提醒、不拦。
const back = textBackdrops(S.palette);
const worstOf = c => Math.min(...back.map(b => contrastRatio(c, b)));
const sec = mixToContrast(S.palette.text, back, 5.5);
const mut = mixToContrast(S.palette.text, back, 4.5);
const crL = document.createElement('p');
crL.className = 'hint';
const items = [
['正文', worstOf(S.palette.text), 4.5],
['次要', worstOf(sec), 4.5],
['淡文字', worstOf(mut), 4.5],
['主色', worstOf(S.palette.primary), 4.5],
];
crL.innerHTML = items.map(([n, v, lim]) =>
`<span style="color:${v < lim ? '#e0a33e' : 'inherit'}">${n} ${v.toFixed(1)}:1</span>`
).join(' · ') + '<br>低于 4.5:1(WCAG AA 正文)会变橙。次要/淡文字由正文自动派生并保底。';
wrap.appendChild(crL);
const note = document.createElement('p');
note.className = 'hint';
note.textContent = '深色配色写在生成结果里(深色不加载图片);要改深色请直接改 CSS。';
@@ -2969,6 +3032,53 @@ function boot(){
}
pre.textContent = JSON.stringify(r, null, 1);
})();
} else if (dump === 'palette'){
// 派生色的对比度下限:base.css 把 --tb-text-muted 按 WCAG AA(4.5:1) 重算过,
// 生成器若还按固定比例往底色混,等于每次导出都把那个修复覆盖回去。
const r = { cases: {} };
try {
const cases = {
'默认亮色': { bg:'#eef3f9', surface:'rgba(255,255,255,0.86)', navbar:'rgba(255,255,255,0.52)', text:'#222d3b', primary:'#4a7fc1', border:'#d6e0ec' },
'默认深色': { bg:'#141a22', surface:'#1d2530', navbar:'#141a22', text:'#e3eaf4', primary:'#6fa3e0', border:'#334052' },
// 极端:文字色本身就不达标 → 必须原样返回,不能继续往底色淡
'低对比手填': { bg:'#f4f4f4', surface:'#ffffff', navbar:'#ffffff', text:'#c8ccd2', primary:'#dddddd', border:'#e5e5e5' },
// 纯白卡片压在彩色底上:卡片侧才是更不利的那个
'白卡彩底': { bg:'#f6d9e4', surface:'#ffffff', navbar:'#f6d9e4', text:'#3b2b33', primary:'#c2557d', border:'#efd3de' },
};
let allOk = true;
for (const name in cases){
const p = cases[name];
const back = textBackdrops(p);
const sec = mixToContrast(p.text, back, 5.5);
const mut = mixToContrast(p.text, back, 4.5);
const worst = c => Math.min(...back.map(b => contrastRatio(c, b)));
const row = {
secondary: +worst(sec).toFixed(2),
muted: +worst(mut).toFixed(2),
rawText: +worst(p.text).toFixed(2),
mutedDarkerThanSecondary: worst(mut) <= worst(sec), // 层级:淡文字必须比次要更淡
// 关键断言:只要有 4.5 的空间就必须达标;本来就不达标的不算错
ok: (worst(p.text) < 4.5) ? (mut === p.text) : (worst(mut) >= 4.5 && worst(sec) >= 4.5),
};
// 卡片半透明时必须按压平后的颜色算,否则白卡 86% 会被当成 #fff
row.surfaceFlat = flatten(p.surface, p.bg);
r.cases[name] = row;
if (!row.ok) allOk = false;
}
// 阶梯:淡文字绝不能比次要文字更醒目
for (const name in r.cases){
if (!r.cases[name].mutedDarkerThanSecondary) allOk = false;
}
// 把生成结果里的派生值也打出来,方便和 base.css 的取值对照
const css = buildCSS(false);
r.mutedLine = (css.match(/--tb-text-muted:[^;]+;/) || [''])[0];
r.secondaryLine = (css.match(/--tb-text-secondary:[^;]+;/) || [''])[0];
r.ok = allOk;
} catch (e){
r.error = e.message + ' | ' + (e.stack || '').split('\n')[1];
r.ok = false;
}
pre.textContent = JSON.stringify(r, null, 1);
} else {
const files = [{ name: `${S.slug}.css`, data: enc(buildCSS(false)) }];
const bytes = zipStore(files);
+1 -1
View File
@@ -10,7 +10,7 @@ var (
BuildCommit = "dev"
// StaticVersion 静态资源缓存版本号:base.css 与主题 css 共用,改样式只改这一处。
StaticVersion = "20261020g"
StaticVersion = "20261021b"
)
// 文本审核通道取值([security] text_censor_source)。定义在 conf 侧,
+39 -11
View File
@@ -16,7 +16,10 @@
--tb-border: #e8eaee;
--tb-text: #1f2329;
--tb-text-secondary: #5c6370;
--tb-text-muted: #8a919c;
/* 次要文字:必须是 #6b7280 而不是更浅的灰。实测 #8a919c 在纯白上只有 3.18:1、
旧的 #999 只有 2.85:1,都低于 WCAG AA 正文要求的 4.5:1;
这个值在 #fff 上 4.83:1、在 --tb-surface-2 上 4.6:1,是能兼顾「淡」和「看得清」的下限。 */
--tb-text-muted: #6b7280;
--tb-primary: #1e87f0;
--tb-primary-weak: rgba(30, 135, 240, 0.10);
--tb-radius: 12px;
@@ -25,6 +28,15 @@
--tb-shadow: 0 1px 2px rgba(16, 24, 40, 0.04), 0 12px 28px -18px rgba(16, 24, 40, 0.20);
--tb-shadow-hover: 0 1px 2px rgba(16, 24, 40, 0.05), 0 18px 40px -20px rgba(16, 24, 40, 0.28);
--tb-font: system-ui, -apple-system, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Noto Sans CJK SC", "Microsoft YaHei", sans-serif;
/* 字号阶梯:只留 5 档。之前页面里散落 0.7/0.72/0.78/0.8/0.82/0.84/0.85/0.86/0.88/0.9/0.95/0.98
十几个值,14px 上下挤了五六档,肉眼分不出来、只会显得排版乱。
lg 取 1rem 而不是 17px:UIkit 原生的 .uk-card-title / h4 就是 16px,
顺着它才能让自定义标题与组件标题落在同一档,而不是差 1px 的两档并存。 */
--tb-fs-xs: 0.75rem; /* 12px 标签、脚注、meta */
--tb-fs-md: 0.875rem; /* 14px 正文、列表、按钮 */
--tb-fs-lg: 1rem; /* 16px 区块小标题、卡片标题 */
--tb-fs-xl: 1.5rem; /* 24px 页面标题、大数字 */
--tb-fs-hero: 1.75rem; /* 28px 首页主标题 */
/* 整页背景:主题可覆盖。图案层叠在背景图之上,两者都不用时保持纯色底。 */
--tb-page-image: none;
--tb-page-pattern: none;
@@ -163,6 +175,11 @@
html[data-theme="dark"] .uk-list a {
color: var(--tb-text) !important;
}
/* 顶栏链接在深色下也用次要文字色:亮色块已经改成 secondary,
这里若保持 --tb-text(正文色),同一个顶栏在深浅两种模式下权重会跳一档。 */
html[data-theme="dark"] .uk-navbar-nav > li > a {
color: var(--tb-text-secondary) !important;
}
html[data-theme="dark"] .uk-navbar-nav > li > a:hover,
html[data-theme="dark"] .uk-card a:hover,
html[data-theme="dark"] .uk-navbar-dropdown-nav > li > a:hover,
@@ -328,35 +345,46 @@
border-radius: var(--tb-radius-xs);
}
.uk-alert {
border-radius: 10px;
border-radius: var(--tb-radius-sm);
}
.uk-modal-dialog {
border-radius: 14px;
border-radius: var(--tb-radius);
}
.uk-dropdown,
.uk-navbar-dropdown {
border-radius: 10px;
border-radius: var(--tb-radius-sm);
box-shadow: var(--tb-shadow);
}
.uk-navbar-container:not(.uk-navbar-transparent) {
background-color: var(--tb-navbar, var(--tb-surface));
}
/* 顶栏导航:UIkit 默认用 rgba(0,0,0,.5),在纯白上实测只有 2.68:1(AA 需 4.5)。
改用次要文字色(6.05:1),hover / 当前页走主色;暗色由下方 dark 分支统一接管。 */
.uk-navbar-nav > li > a {
color: var(--tb-text-secondary);
}
.uk-navbar-nav > li > a:hover,
.uk-navbar-nav > li.uk-active > a {
color: var(--tb-primary);
}
/* 键盘可达性:仅在键盘聚焦时出现描边,鼠标点击不出现 */
.uk-button:focus-visible,
.uk-navbar-nav > li > a:focus-visible {
outline: 2px solid var(--tb-primary);
outline-offset: 2px;
}
/* 次要文字统一走令牌,避免 #999 / #8a919c 混用 */
/* 次要文字统一走令牌,避免 #999 / #8a919c 混用。
⚠️ 必须 !important:UIkit 3.3.3 自带 .uk-text-muted{color:#999!important},
普通声明一律压不过它(踩过:规则写了但全站仍是 #999,实测 2.85:1)。 */
.uk-text-muted,
.uk-text-meta {
color: var(--tb-text-muted);
color: var(--tb-text-muted) !important;
}
/* 主题选择器(设置页):色块 + 名称。纯展示,不参与主题令牌,所以放在基础层。 */
.tb-theme-hint {
margin: 0 0 6px;
font-size: 0.8rem;
font-size: var(--tb-fs-xs);
color: var(--tb-text-muted);
}
.tb-theme-grid {
@@ -394,7 +422,7 @@
outline-offset: 1px;
}
.tb-theme-name {
font-size: 0.84rem;
font-size: var(--tb-fs-xs);
line-height: 1.3;
}
.tb-theme-opt input:checked ~ .tb-theme-name {
@@ -450,7 +478,7 @@ a:hover, .uk-link:hover {
border-radius: var(--tb-radius-xs);
background-color: var(--tb-surface-2);
color: var(--tb-text-secondary);
font-size: 0.72rem;
font-size: var(--tb-fs-xs);
line-height: 1.7;
white-space: nowrap;
}
@@ -690,14 +718,14 @@ a.tb-chip:hover {
.md-body pre {
background: #f5f5f5;
padding: 0.8em 1em;
border-radius: 6px;
border-radius: var(--tb-radius-xs);
overflow-x: auto;
margin: 0.6em 0;
}
.md-body code {
background: #f0f0f0;
padding: 0.1em 0.35em;
border-radius: 4px;
border-radius: var(--tb-radius-xs);
font-size: 0.9em;
}
.md-body pre code { background: none; padding: 0; }
+14 -3
View File
@@ -16,7 +16,7 @@
display / flex / grid、position 一律不动 —— 主题不是换皮模板。
3. 本文件不参与构建,仅作参考;真正生效的是同目录下的 <slug>.css。
── 可用的 23 个令牌 ──────────────────────────────────────────────
── 可用的 29 个令牌 ──────────────────────────────────────────────
底色层 --tb-bg 页面底色(html/body)
--tb-surface 卡片、输入框等前景面板
--tb-surface-2 次级面(标签底、进度条轨道、代码块)
@@ -25,13 +25,22 @@
页面底色不是白时建议覆盖成 bg 或它的同色,否则顶栏会成为一条白带)
线条 --tb-border 边框、分隔线
文字 --tb-text 正文
--tb-text-secondary 次要文字(说明、meta)
--tb-text-secondary 次要文字(说明、meta、顶栏链接)
--tb-text-muted 弱化文字(占位、时间戳)
⚠️ 这三个都要满足 WCAG AA 的 4.5:1(对 --tb-bg 与压平后的 --tb-surface 都算)。
往浅调很容易掉到 3:1 以下(旧的 #8a919c 是 3.18:1),肉眼"淡一点"其实已经看不清了。
强调 --tb-primary 主色:链接、主按钮、进度条
--tb-primary-weak 主色 10% 左右的浅底(选中态、标签)
形状 --tb-radius 卡片 12px / --tb-radius-sm 8px / --tb-radius-xs 6px
阴影 --tb-shadow 卡片默认 / --tb-shadow-hover 悬停
字体 --tb-font 字体栈(一般不改)
--tb-fs-xs 12px 标签、脚注、meta
--tb-fs-md 14px 正文、列表、按钮
--tb-fs-lg 16px 区块小标题、卡片标题
--tb-fs-xl 24px 页面标题、大数字
--tb-fs-hero 28px 首页主标题
这套阶梯 base.css 已统一定义,主题一般不要覆盖:
改一档会连带挤坏所有页面的排版,且与主题要解决的问题无关。
页面背景 --tb-page-image 整页背景图(cover,fixed)
--tb-page-pattern 整页图案(平铺,可叠加在背景图之上)
--tb-page-pattern-size 图案平铺尺寸(默认 auto)
@@ -72,7 +81,7 @@ html[data-theme-style="SLUG"] {
--tb-text: #1f2329;
--tb-text-secondary: #5c6370;
--tb-text-muted: #8a919c;
--tb-text-muted: #6b7280; /* AA 下限:白底 4.83:1。别再往浅调 */
--tb-primary: #1e87f0;
--tb-primary-weak: rgba(30, 135, 240, 0.10);
@@ -90,6 +99,7 @@ html[data-theme-style="SLUG"] {
--tb-page-image: none;
--tb-page-pattern: none;
--tb-page-pattern-size: auto;
--tb-page-position: center center; /* cover 裁切时保留哪一侧 */
/* 装饰层(放人物/摆件/光)。固定 10 个槽位,必须写满,缺的填 none——
值一多一少,多背景各列表就错位(会出现整页重复同一张图)。
@@ -126,6 +136,7 @@ html[data-theme="dark"][data-theme-style="SLUG"] {
--tb-page-image: none;
--tb-page-pattern: none;
--tb-page-pattern-size: auto;
--tb-page-position: center center; /* cover 裁切时保留哪一侧 */
}
/* 3) 按页面作用域覆盖(可选)
+2 -1
View File
@@ -14,7 +14,8 @@ html[data-theme-style="sakura"] {
--tb-text: #3a2b30;
--tb-text-secondary: #6b545c;
--tb-text-muted: #a08c94;
/* 弱化文字:旧值 #a08c94 只有 2.8:1(看不清),#78696e 是 4.6:1,踩住 AA 下限 */
--tb-text-muted: #78696e;
--tb-primary: #e5678f;
--tb-primary-weak: rgba(229, 103, 143, 0.12);
+2 -1
View File
@@ -21,7 +21,8 @@ html[data-theme-style="shiroko"] {
--tb-text: #222d3b;
--tb-text-secondary: #51607a;
--tb-text-muted: #8794a7;
/* 弱化文字:旧值 #8794a7 只有 2.76:1(看不清),#656e7a 是 4.63:1,踩住 AA 下限 */
--tb-text-muted: #656e7a;
--tb-primary: #4a7fc1;
--tb-primary-weak: rgba(74, 127, 193, 0.11);