Files
tamabox/docs/theme-page-scope.md
T
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

3.4 KiB
Raw Blame History

主题的页面作用域(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。

选择器与特异性

/* 全局 */
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"] 相同, 而覆盖块写在文件后面,会盖掉深色块。所以深色必须再写一条三重选择器:

html[data-theme="dark"][data-theme-style="SLUG"][data-page-scope="question"] { … }

不写的话,深色模式下会拿到浅色的覆盖块 —— 典型表现是「深色页面里突然出现一块白天场景」。

覆盖块写什么

通常只覆盖这三组:

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():

--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。