Files
tamabox/dev/theme-builder
tamakyi ddee221de2 feat(theme): 主题支持按页面分别配装饰,生成器改用真实页面画布
原来的生成器只能配「背景 + 人物 + 道具」三件套,预览还是自己画的仿制页面 ——
跟真实页面不像,也没法给不同页面配不同内容。

机制层:
- internal/context.pageScope() 按路径归类,输出 <html data-page-scope>:
  box / question / admin / profile / vote(首页与 /explore 不加载主题,不列)
- /user/questions 单独归为 admin:题目列表信息密,和设置表单的装饰诉求不同
- base.css 装饰槽位 6 -> 10,背景 8 层 -> 12 层;主题按作用域覆盖时同样写满 10 个
- 新增 TestDecoSlotCount:逐文件逐声明检查 10 个值。少写一个不报错,
  只会让图层与 size/position 错位(整页重复同一张图 / 人物跑到左上角),注释守不住

生成器:
- 画板改成 iframe 加载 outputs/preview-*.html(真实模板渲染),配 serve.py 起本地服务
  解决 file:// 下父页面读不到 iframe DOM 的问题
- 八个页面作用域(去掉不生效的首页/探索),每个可独立配背景与最多 10 层装饰
- 图层类型:图片 / 径向光斑 / 线性光;9 宫格锚点 + 像素偏移,可在画板上直接拖
- 位置统一写 calc(0%|50%|100% ± Npx):百分比是相对 (容器-图片),
  不用 left/bottom 关键字混搭,避免三值语法非法导致整条声明作废
- 配色只填 6 个基础色,其余 4 个必需令牌按方向派生(surface-2 往 text 靠,
  浅色变深、深色变亮,方向自动正确)

配套:
- zz_preview_test.go 增加单题页 / 题目列表 / 投票页预览,作为生成器画板
- docs/theme-page-scope.md 记录作用域取值、选择器特异性与 10 槽位约束
- StaticVersion 20261020e(base.css 变了)

验证:生成器导出的 CSS 通过 TestThemeCSSComplete + TestDecoSlotCount;
真实箱主页计算样式为 12 层对齐;临时主题验证「question 覆盖为 none」生效、
未覆盖时回退全局。
2026-10-20 10:53:44 +08:00
..

主题生成器

一个可视化编辑主题的小工具:以项目真实页面为画布,按页面分别摆装饰,导出 zip 解压即用。

它替换了早先那版「单文件、仿制预览」的生成器 —— 那一版的问题很直接:预览是我自己画的 示意图,跟真实页面长得不一样;而且只能配「背景 + 人物 + 道具」三件套。

为什么需要起一个本地服务

生成器要把 outputs/preview-*.html(由 go test 渲染出来的真实页面)放进 iframe 当画板, 再往里注入正在编辑的主题 CSS。file:// 下浏览器不允许父页面访问 iframe 的 DOM(即使同目录), 预览就只能退回示意图。起本地服务后父子同源,拖拽、注入、实时改令牌都能工作。

# 1) 先渲染真实页面快照(改了模板/数据后要重跑)
TEST_PREVIEW=1 go test ./templates -run TestGenPreview

# 2) 起服务(只读,不改项目任何文件)
python dev/theme-builder/serve.py          # 默认 8777

# 3) 打开
http://127.0.0.1:8777/dev/theme-builder/

不想用脚本也行:在项目根跑 python -m http.server 8777,效果一样。

它和线上是同一条链路

环节 生成器 线上
画板 outputs/preview-*.html(真模板渲染) 同一套模板
作用域 iframe 里设 data-page-scope internal/context.pageScope() 输出到 <html>
装饰层 6 槽?不,固定 10 槽 base.css 的 12 层背景(pattern 1 + deco 10 + image 1)
位置 `calc(0% 50%

因为是同一套机制,工具里拖出来的位置换视口不会跑位,也不会出现「预览好看、实机错位」。

三块面板

页面 —— 每个页面作用域可以单独配,各有一份背景与装饰:

作用域 对应路径
全局 所有页面默认
box /_/{domain} 箱主页
question /_/{domain}/{id} 单题页
admin /user/questions 题目列表
profile /user/* 设置页
vote /vote/{token} 投票页

默认全部「跟随全局」;勾掉就变成独立配置,导出时才会产出覆盖块。 没配的页面自动沿用全局,不会出现「某页装饰消失」。

(首页 / 与 /explore 不列出来 —— 全站公开页不注入 ThemeSlug,不加载主题 css,配了也没效果。)

装饰图层 —— 每个作用域最多 10 层,三种类型:

  • 图片:从图片池选,按高度定尺寸(宽度自动等比),选中后可直接在画板上拖动;
  • 光斑:radial-gradient,给空白一点体积,不占位、不会变成贴纸;
  • 线性光:linear-gradient,做窗光/压深。

锚点用 9 宫格 + 像素偏移,生成 calc(0% + 40px) calc(100% - 36px) 这种两值形式。

图片池 —— 上传任意张,浏览器本地转 WebP,画布尺寸 = 原图尺寸(不缩放、不留透明边)。 导出时只打包实际用到的图。

导出

点「导出 zip」得到:

static/css/themes/<slug>.css
static/images/themes/<slug>/*.webp
INSTALL-<slug>.txt

解压到项目根,然后按 INSTALL-<slug>.txt 做三步:

  1. conf/app.ini 的 [theme] available 加上 slug;
  2. internal/conf/static.go 的 StaticVersion +1;
  3. go build(模板与静态资源都是编译期 embed,只重启不重编不会变)。

它不做什么

  • 不抠图:图片必须本身是透明底,白底图导出的主题会带一块白。
  • 不做调色:想统一「雾度」(人物压白 16%、场景 30~38%)得在生成端做, 见 docs/theme-shiroko-assets-spec.md。
  • 不写配置:[theme] available 只给要粘贴的那一行。
  • 不碰深色的图片:深色块一律 --tb-page-image: none + 装饰全关(白天场景压在深底上会脏)。 想在深色下也放图,需要手工加深色块。

自检

没有 UI 也能回归,两个钩子:

?demo=1            载入示例素材(canvas 画的背景 + 剪影人物)
?scope=admin       直接落在某个作用域
?dump=css          把最终 CSS 写进 <pre id="dump">
?dump=zip          把 zip 的 base64 写进 <pre id="dump">
chrome --headless=old --virtual-time-budget=6000 \
  --dump-dom "http://127.0.0.1:8777/dev/theme-builder/?demo=1&dump=css"

把抓到的 CSS 存成 static/css/themes/<slug>.css,可跑:

go test ./internal/theme -run "TestThemeCSSComplete|TestDecoSlotCount"

TestDecoSlotCount 是这次新增的:它会逐个检查 base.css 与所有主题 css 里每一处 --tb-deco* 声明是否都有 10 个值。少写一个不会报错,只会让图层与 size/position 错位 (表现为整页重复同一张插画,或人物跑到左上角),靠注释守不住。