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」生效、
未覆盖时回退全局。
This commit is contained in:
tamakyi
2026-10-20 10:53:44 +08:00
parent feb5b1f78d
commit ddee221de2
14 changed files with 1748 additions and 796 deletions
+86
View File
@@ -0,0 +1,86 @@
# 主题的页面作用域(data-page-scope)
一套主题过去只能有一种装饰:人物永远站在左下角,不管当前是箱主页、单题页还是题目列表。
单题页正文长、题目列表信息密,同一套装饰在三种页面上要同时成立很难。
现在服务端会按请求路径给 `<html>` 打一个 `data-page-scope`,主题可以对每个页面分别配
整页背景与装饰层。
## 取值
| 值 | 路径 | 说明 |
| --- | --- | --- |
| (无属性) | — | 全局默认 |
| `box` | `/_/{domain}`、`/_/{domain}/tag/*` | 箱主页(问题列表) |
| `question` | `/_/{domain}/{qid}` | 单个问题 |
| `admin` | `/user/questions`、`/user/votes/*` | 题目 / 私聊管理列表 |
| `profile` | `/user`、`/user/profile` 等 | 设置页 |
| `vote` | `/vote/{token}` | 投票页(跟随发布者的主题) |
| `home` / `explore` | `/`、`/explore` | Go 侧会返回,但**这两页不加载主题 css** |
来源:`internal/context.pageScope()`,测试在 `internal/context/context_test.go` 的 `TestPageScope`。
## 选择器与特异性
```css
/* 全局 */
html[data-theme-style="SLUG"] { … }
/* 某一页覆盖 */
html[data-theme-style="SLUG"][data-page-scope="question"] { … }
```
特异性 0,2,1 与深色块 `html[data-theme="dark"][data-theme-style="SLUG"]` 相同,
而覆盖块写在文件后面,**会盖掉深色块**。所以深色必须再写一条三重选择器:
```css
html[data-theme="dark"][data-theme-style="SLUG"][data-page-scope="question"] { … }
```
不写的话,深色模式下会拿到浅色的覆盖块 —— 典型表现是「深色页面里突然出现一块白天场景」。
## 覆盖块写什么
通常只覆盖这三组:
```css
html[data-theme-style="SLUG"][data-page-scope="question"] {
--tb-page-image: none; /* 不放整页场景 */
--tb-page-pattern: none;
--tb-deco: …; /* 10 个值 */
--tb-deco-size: …; /* 10 个值 */
--tb-deco-position: …; /* 10 个值 */
}
```
配色(`--tb-bg` / `--tb-surface` / `--tb-text` / `--tb-primary`)一般不要按页面改:
同一个主题在不同页面换配色,用户会觉得走错站了。
## 10 个槽位
`--tb-deco` / `--tb-deco-size` / `--tb-deco-position` 三组必须**等长且写满 10 个**。
少一个不会报错,CSS 会按列表长度循环取值,于是图层与 size/position 整体错位:
- 整页重复同一张插画(第一次踩坑就是这个)
- 人物跑到页面左上角(`background-position` 整条声明非法时回落初始值)
`internal/theme/slots_test.go` 的 `TestDecoSlotCount` 会逐个文件、逐处声明检查,
包括 base.css 与所有主题 css(含作用域覆盖块)。写主题时不用记,跑测试就行。
`background-position` 一律写成两值 `calc()`:
```css
--tb-deco-position: calc(0% + 40px) calc(100% - 36px),
calc(50% + 0px) calc(50% + 0px), …;
```
- 百分比是相对 **(容器 − 图片)** 的剩余空间,不是相对容器
- `0%` 左/上、`50%` 居中、`100%` 右/下
- **不要**混用 `left` / `bottom` 关键字:「水平居中 + 垂直贴底」会变成三值语法,
一旦不合规范整条声明作废,图层静默掉到左上角
## 可视化编辑
手写这些值不现实,用 `dev/theme-builder/`(见同目录 README):以真实页面为画布,
按页面切换、拖拽定位、导出 zip。它生成的 CSS 直接过 `TestThemeCSSComplete` 与
`TestDecoSlotCount`。