主题体系跟上设计令牌化:派生文字色按对比度下限、主题 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:
@@ -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 内容变了,必须换键)。
|
||||
|
||||
@@ -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,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 靠**像素比对**
|
||||
|
||||
@@ -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。
|
||||
|
||||
@@ -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= 打开处理面板
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -10,7 +10,7 @@ var (
|
||||
BuildCommit = "dev"
|
||||
|
||||
// StaticVersion 静态资源缓存版本号:base.css 与主题 css 共用,改样式只改这一处。
|
||||
StaticVersion = "20261020g"
|
||||
StaticVersion = "20261021b"
|
||||
)
|
||||
|
||||
// 文本审核通道取值([security] text_censor_source)。定义在 conf 侧,
|
||||
|
||||
+39
-11
@@ -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; }
|
||||
|
||||
@@ -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) 按页面作用域覆盖(可选)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user