新增 new-sakura 主题:纯 CSS 樱花,每个页面位置不同
樱花不依赖任何图片素材:整朵花、带缺口的花瓣、带枝的花朵树都是内联 SVG 的 data URI,写在主题 CSS 里。好处是零请求、任意缩放不糊、换花色只改一个色值; 代价是 CSS 约 50KB(gzip 约 12KB),走 StaticVersion 缓存。 - 六个页面作用域各一套 --tb-deco 位置(global / box / question / admin / profile / vote),10 槽三组等长;位置一律「关键字 + 偏移」两值写法。 - 窄屏(≤640px)装饰层全关、底纹缩到 200px:卡片占满宽度后枝条会糊在字上。 - 深色换亮一档的粉、关掉底纹(浅粉压在深底上会变噪点)、背景用渐变避免死黑。 - 亮色 primary 从候选 #c14a72(4.39:1)压深到 #b8436a(4.87:1), muted 5.27:1,都踩住 AA;深色对应 8.17:1 / 5.82:1。 - 生成脚本 dev/theme-tools/gen_new_sakura.py(花形、位置表、配色都在里面), CSS 是产物,不手改。 - 四份词典加 theme.name.new-sakura;conf/app.sample.ini 的 available 登记。 为什么不用生成器:生成器的装饰层只认图片素材,没有内联 SVG 图层, 见 docs/theme-new-sakura.md「为什么不用主题生成器出图」。
This commit is contained in:
+1
-1
@@ -273,6 +273,6 @@ daily_message_limit_max = 999
|
|||||||
; 改选择器与令牌值,再把 <slug> 加进 available —— 不需要改模板。
|
; 改选择器与令牌值,再把 <slug> 加进 available —— 不需要改模板。
|
||||||
; 需要图片素材的,放 static/images/themes/<slug>/(static/fs.go 已嵌 images/themes 目录)。
|
; 需要图片素材的,放 static/images/themes/<slug>/(static/fs.go 已嵌 images/themes 目录)。
|
||||||
default = default
|
default = default
|
||||||
available = default,sakura,shiroko
|
available = default,sakura,shiroko,new-sakura
|
||||||
; ⚠️ 没登记在 available 里的主题,即使 css 已随二进制发布也不会出现在设置页;
|
; ⚠️ 没登记在 available 里的主题,即使 css 已随二进制发布也不会出现在设置页;
|
||||||
; 启动日志会打出「[theme] available 未登记以下主题」提醒。,shiroko
|
; 启动日志会打出「[theme] available 未登记以下主题」提醒。,shiroko
|
||||||
|
|||||||
@@ -0,0 +1,374 @@
|
|||||||
|
"""生成 new-sakura 主题的 CSS。
|
||||||
|
|
||||||
|
为什么用脚本生成而不用生成器的图层功能:生成器的装饰层只认「图片素材」(导出时
|
||||||
|
打进 zip 或传对象存储),而这里的花瓣是**内联 SVG 的 data URI** —— 零外部请求、
|
||||||
|
任意缩放不糊、改配色只改一个色值。代价是 CSS 里会出现一长串百分号编码的 URI,
|
||||||
|
所以源 SVG 保留在本文件里,改完重跑:
|
||||||
|
|
||||||
|
python dev/theme-tools/gen_new_sakura.py
|
||||||
|
|
||||||
|
输出:static/css/themes/new-sakura.css
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
|
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
OUT = os.path.join(ROOT, "static", "css", "themes", "new-sakura.css")
|
||||||
|
|
||||||
|
|
||||||
|
def uri(svg):
|
||||||
|
"""转成 CSS url()。safe='' 是刻意的:连逗号、分号、括号都编码掉 ——
|
||||||
|
`--tb-deco` 是按顶层逗号切分的(internal/theme 的 TestDecoSlotCount),
|
||||||
|
URI 里留一个裸逗号就会被当成多一层的装饰。"""
|
||||||
|
return 'url("data:image/svg+xml,' + urllib.parse.quote(svg, safe="") + '")'
|
||||||
|
|
||||||
|
|
||||||
|
# ---- 花与瓣 ----------------------------------------------------------------
|
||||||
|
# 五瓣樱花:五个椭圆绕中心旋 72°,中心一团深一点的蕊。不做花瓣缺口——
|
||||||
|
# 在 30~90px 的显示尺寸上缺口只剩 2px,反而像毛边;靠五瓣轮廓就够识别。
|
||||||
|
def blossom(light, deep, heart):
|
||||||
|
petals = "".join(
|
||||||
|
f'<ellipse cx="50" cy="27" rx="14" ry="21" transform="rotate({a} 50 50)"/>'
|
||||||
|
for a in (0, 72, 144, 216, 288)
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">'
|
||||||
|
f'<g fill="{light}">{petals}</g>'
|
||||||
|
f'<circle cx="50" cy="50" r="7.5" fill="{deep}"/>'
|
||||||
|
f'<circle cx="50" cy="50" r="3" fill="{heart}"/>'
|
||||||
|
"</svg>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# 单瓣:顶端留一个 V 形缺口(樱花花瓣的标志),比整朵花更适合当"飘落"的单位。
|
||||||
|
def petal(fill):
|
||||||
|
return (
|
||||||
|
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 56">'
|
||||||
|
f'<path d="M20 1c9 0 15 8 15 18 0 5-2 9-4 12l-4-5-7 9-7-9-4 5c-2-3-4-7-4-12C5 9 11 1 20 1z" fill="{fill}"/>'
|
||||||
|
"</svg>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# 枝条:一根主枝 + 两根小杈 + 五朵花 + 三片落瓣。
|
||||||
|
# 花是重复内联而不是 <use href="#f">:data URI 里的片段引用在各浏览器上
|
||||||
|
# 行为不一致(Safari 曾整朵不渲染),多几百字节换确定性。
|
||||||
|
def branch(wood, light, deep, heart, flip=False):
|
||||||
|
fl = ' transform="translate(420,0) scale(-1,1)"' if flip else ""
|
||||||
|
# 花要比枝小得多、数量多几朵。早先 5 朵 scale≈1 的花配一根细枝,缩到 300px
|
||||||
|
# 显示时是一串糖葫芦;真实樱花枝是"小碎花沿枝密布",所以花收到 0.42~0.62。
|
||||||
|
blossoms = [
|
||||||
|
(302, 74, 0.62), (256, 110, 0.55), (214, 138, 0.60), (172, 176, 0.58),
|
||||||
|
(352, 42, 0.50), (140, 214, 0.46), (116, 246, 0.40), (326, 128, 0.42),
|
||||||
|
]
|
||||||
|
petals = [(272, 158, 22), (318, 150, -18), (196, 92, 40), (150, 196, -26)]
|
||||||
|
body = []
|
||||||
|
for x, y, s in blossoms:
|
||||||
|
body.append(
|
||||||
|
f'<g transform="translate({x} {y}) scale({s}) translate(-50 -50)">'
|
||||||
|
'<g fill="%s">' % light
|
||||||
|
+ "".join(f'<ellipse cx="50" cy="27" rx="14" ry="21" transform="rotate({a} 50 50)"/>'
|
||||||
|
for a in (0, 72, 144, 216, 288))
|
||||||
|
+ f'</g><circle cx="50" cy="50" r="7" fill="{deep}"/>'
|
||||||
|
f'<circle cx="50" cy="50" r="2.8" fill="{heart}"/></g>'
|
||||||
|
)
|
||||||
|
for x, y, rot in petals:
|
||||||
|
body.append(
|
||||||
|
f'<g transform="translate({x} {y}) rotate({rot}) scale(0.42) translate(-20 -28)">'
|
||||||
|
f'<path d="M20 1c9 0 15 8 15 18 0 5-2 9-4 12l-4-5-7 9-7-9-4 5c-2-3-4-7-4-12C5 9 11 1 20 1z" fill="{light}"/></g>'
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 420 320">'
|
||||||
|
f'<g{fl}>'
|
||||||
|
f'<path d="M2 318C74 276 112 212 176 184c62-28 102-72 146-118 26-28 52-46 82-58" '
|
||||||
|
f'fill="none" stroke="{wood}" stroke-width="4.2" stroke-linecap="round" opacity=".62"/>'
|
||||||
|
f'<path d="M176 184c-12-34-20-60-16-94M262 126c16-28 32-48 46-58" '
|
||||||
|
f'fill="none" stroke="{wood}" stroke-width="2.8" stroke-linecap="round" opacity=".5"/>'
|
||||||
|
+ "".join(body) + "</g></svg>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# 平铺底纹:极淡的小花 + 落瓣。
|
||||||
|
# 密度是刻意的:最早一版放了 2 朵花 + 2 片瓣并且 260px 平铺,页面上读出来是
|
||||||
|
# 「整齐的包装纸」——因为每个元素都在固定格点上重复。现在只留 1 朵 + 2 片,
|
||||||
|
# 尺寸放大到 300px 且各自错位,平铺时才看不出周期。
|
||||||
|
def pattern(light):
|
||||||
|
one = (
|
||||||
|
'<g fill="%s">' % light
|
||||||
|
+ "".join(f'<ellipse cx="50" cy="27" rx="14" ry="21" transform="rotate({a} 50 50)"/>'
|
||||||
|
for a in (0, 72, 144, 216, 288))
|
||||||
|
+ "</g>"
|
||||||
|
)
|
||||||
|
petal_path = ('<path d="M20 1c9 0 15 8 15 18 0 5-2 9-4 12l-4-5-7 9-7-9-4 5'
|
||||||
|
'c-2-3-4-7-4-12C5 9 11 1 20 1z" fill="%s"/>')
|
||||||
|
return (
|
||||||
|
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300">'
|
||||||
|
f'<g transform="translate(96 84) scale(0.34) translate(-50 -50)" opacity="0.34">{one}</g>'
|
||||||
|
f'<g transform="translate(236 208) rotate(28) scale(0.26)" opacity="0.24">'
|
||||||
|
+ (petal_path % light) +
|
||||||
|
"</g>"
|
||||||
|
f'<g transform="translate(188 52) rotate(-34) scale(0.2)" opacity="0.18">'
|
||||||
|
+ (petal_path % light) +
|
||||||
|
"</g>"
|
||||||
|
"</svg>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---- 一套素材(浅色 / 深色各一组色) ---------------------------------------
|
||||||
|
LIGHT = dict(
|
||||||
|
light="#f7bcd0", deep="#e989aa", heart="#fdf6f8",
|
||||||
|
wood="#9a7a70", pattern="#f2bfd0",
|
||||||
|
)
|
||||||
|
DARK = dict(
|
||||||
|
light="#f0a0b8", deep="#c96a8b", heart="#2c2227",
|
||||||
|
wood="#a8837a", pattern="#7a4e5f",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def uris(p):
|
||||||
|
return {
|
||||||
|
"blossom_l": uri(blossom(p["light"], p["deep"], p["heart"])),
|
||||||
|
"petal_l": uri(petal(p["light"])),
|
||||||
|
"branch_l": uri(branch(p["wood"], p["light"], p["deep"], p["heart"], flip=True)),
|
||||||
|
"branch_r": uri(branch(p["wood"], p["light"], p["deep"], p["heart"], flip=False)),
|
||||||
|
"pattern": uri(pattern(p["pattern"])),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
NONE10 = "none, none, none, none, none, none, none, none, none, none"
|
||||||
|
AUTO10 = "auto, auto, auto, auto, auto, auto, auto, auto, auto, auto"
|
||||||
|
TOPLEFT10 = ("left top, left top, left top, left top, left top, left top, left top, "
|
||||||
|
"left top, left top, left top")
|
||||||
|
|
||||||
|
|
||||||
|
def deco(layers):
|
||||||
|
"""layers: [(var, size, position)],不足 10 槽补 none。"""
|
||||||
|
items, sizes, poss = [], [], []
|
||||||
|
for var, size, pos in layers:
|
||||||
|
items.append(var)
|
||||||
|
sizes.append(size)
|
||||||
|
poss.append(pos)
|
||||||
|
pad = 10 - len(layers)
|
||||||
|
items += ["none"] * pad
|
||||||
|
sizes += ["auto"] * pad
|
||||||
|
poss += ["left top"] * pad
|
||||||
|
return (", ".join(items), ", ".join(sizes), ", ".join(poss))
|
||||||
|
|
||||||
|
|
||||||
|
# ---- 每个页面一套位置 ------------------------------------------------------
|
||||||
|
# 位置一律两值(`关键字 ± 偏移`)。百分比相对「容器 − 图片」,写 calc 更可控;
|
||||||
|
# 这里用 left/right + % 与 px 偏移,保证换视口时花还贴在角落而不是飘到内容上。
|
||||||
|
SCOPES = {
|
||||||
|
"global": [
|
||||||
|
("var(--sk-branch-r)", "auto 280px", "right -16px bottom -22px"),
|
||||||
|
("var(--sk-blossom)", "auto 88px", "left 6% top 12%"),
|
||||||
|
("var(--sk-petal)", "32px", "left 14% top 24%"),
|
||||||
|
("var(--sk-petal)", "26px", "right 16% top 8%"),
|
||||||
|
],
|
||||||
|
"box": [
|
||||||
|
# 枝的细端在右上、粗端朝左下:放在右上角时"从角上垂下来",
|
||||||
|
# 尺寸收到 300px 才会有枝形 —— 390px 时可见部分只剩一团花(第一次就是这个问题)
|
||||||
|
("var(--sk-branch-r)", "auto 300px", "right -24px top -16px"),
|
||||||
|
("var(--sk-blossom)", "auto 80px", "left 5% bottom 16%"),
|
||||||
|
("var(--sk-petal)", "34px", "left 14% bottom 32%"),
|
||||||
|
("var(--sk-petal)", "24px", "right 22% bottom 8%"),
|
||||||
|
("var(--sk-blossom)", "auto 52px", "right 6% bottom 6%"),
|
||||||
|
],
|
||||||
|
"question": [
|
||||||
|
("var(--sk-branch-l)", "auto 300px", "left -34px bottom -24px"),
|
||||||
|
("var(--sk-blossom)", "auto 86px", "right 7% top 11%"),
|
||||||
|
("var(--sk-petal)", "30px", "right 17% top 24%"),
|
||||||
|
("var(--sk-petal)", "24px", "left 11% top 7%"),
|
||||||
|
],
|
||||||
|
"admin": [
|
||||||
|
# 列表页卡片几乎占满宽度,右侧的花会整个被卡片盖住 —— 只往左侧留白放
|
||||||
|
("var(--sk-branch-l)", "auto 260px", "left -28px top -24px"),
|
||||||
|
("var(--sk-blossom)", "auto 70px", "left 3% bottom 7%"),
|
||||||
|
("var(--sk-petal)", "26px", "left 11% bottom 15%"),
|
||||||
|
],
|
||||||
|
"profile": [
|
||||||
|
("var(--sk-branch-r)", "auto 280px", "right -28px bottom -24px"),
|
||||||
|
("var(--sk-blossom)", "auto 66px", "left 6% top 9%"),
|
||||||
|
("var(--sk-petal)", "28px", "left 15% top 19%"),
|
||||||
|
("var(--sk-petal)", "22px", "left 5% top 28%"),
|
||||||
|
],
|
||||||
|
"vote": [
|
||||||
|
("var(--sk-branch-l)", "auto 280px", "left -30px top -20px"),
|
||||||
|
("var(--sk-blossom)", "auto 78px", "right 8% top 16%"),
|
||||||
|
("var(--sk-petal)", "28px", "right 18% top 27%"),
|
||||||
|
("var(--sk-blossom)", "auto 52px", "right 6% bottom 8%"),
|
||||||
|
],
|
||||||
|
# 窄屏:卡片占满宽度,留白只剩顶栏上下那两条 —— 装饰层放什么都没有立足之地,
|
||||||
|
# 樱花改由底纹承担(@media 里把 pattern 缩小到 200px,密度上来才看得出来)
|
||||||
|
"narrow": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def scope_block(prefix, layers, indent=" "):
|
||||||
|
img, size, pos = deco(layers)
|
||||||
|
return (
|
||||||
|
f"{prefix} {{\n"
|
||||||
|
f"{indent}--tb-deco: {img};\n"
|
||||||
|
f"{indent}--tb-deco-size: {size};\n"
|
||||||
|
f"{indent}--tb-deco-position: {pos};\n"
|
||||||
|
"}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build():
|
||||||
|
u = uris(LIGHT)
|
||||||
|
d = uris(DARK)
|
||||||
|
|
||||||
|
vars_light = "\n".join([
|
||||||
|
f" --sk-blossom: {u['blossom_l']};",
|
||||||
|
f" --sk-petal: {u['petal_l']};",
|
||||||
|
f" --sk-branch-l: {u['branch_l']};",
|
||||||
|
f" --sk-branch-r: {u['branch_r']};",
|
||||||
|
])
|
||||||
|
vars_dark = "\n".join([
|
||||||
|
f" --sk-blossom: {d['blossom_l']};",
|
||||||
|
f" --sk-petal: {d['petal_l']};",
|
||||||
|
f" --sk-branch-l: {d['branch_l']};",
|
||||||
|
f" --sk-branch-r: {d['branch_r']};",
|
||||||
|
])
|
||||||
|
|
||||||
|
light_block = f"""html[data-theme-style="new-sakura"] {{
|
||||||
|
--tb-bg: #fdf6f8;
|
||||||
|
--tb-surface: #ffffff;
|
||||||
|
--tb-surface-2: #fdf0f4;
|
||||||
|
--tb-surface-hover: #fff7fa;
|
||||||
|
--tb-navbar: #fdf6f8;
|
||||||
|
--tb-border: #f3dde5;
|
||||||
|
|
||||||
|
--tb-text: #3a2c31;
|
||||||
|
--tb-text-secondary: #6d555d;
|
||||||
|
/* 5.27:1(底色)/ 5.62:1(卡片),踩住 AA 下限 */
|
||||||
|
--tb-text-muted: #77626a;
|
||||||
|
|
||||||
|
/* #c14a72 在粉底上只有 4.39:1,同色相压深到 #b8436a = 4.87:1 */
|
||||||
|
--tb-primary: #b8436a;
|
||||||
|
--tb-primary-weak: rgba(184, 67, 106, 0.12);
|
||||||
|
--tb-on-primary: #ffffff;
|
||||||
|
|
||||||
|
--tb-shadow: 0 1px 2px rgba(150, 80, 105, 0.05), 0 12px 28px -18px rgba(150, 80, 105, 0.22);
|
||||||
|
--tb-shadow-hover: 0 1px 2px rgba(150, 80, 105, 0.07), 0 18px 40px -20px rgba(150, 80, 105, 0.30);
|
||||||
|
|
||||||
|
/* 花与瓣都是内联 SVG(data URI),没有外部素材 —— 所以这个主题不需要对象存储。
|
||||||
|
--sk-* 是花材的别名:同一个 URI 只在这里出现一次,下面各页面作用域引用变量,
|
||||||
|
否则 6 套位置会把 4 个 data URI 重复 20 多次,文件直接翻倍。 */
|
||||||
|
{vars_light}
|
||||||
|
|
||||||
|
/* 整页底纹:极淡的花与落瓣,260px 平铺。压在白底上只是一层纸纹,
|
||||||
|
不参与构图(真正的樱花由下面的装饰层负责)。 */
|
||||||
|
--tb-page-image: none;
|
||||||
|
--tb-page-pattern: {u['pattern']};
|
||||||
|
--tb-page-pattern-size: 300px 300px;
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 全局默认位置:写在同一个块里(未单独配置的页面沿用这套)
|
||||||
|
body = light_block + deco_block_inline(SCOPES["global"]) + "}\n"
|
||||||
|
|
||||||
|
for key in ("box", "question", "admin", "profile", "vote"):
|
||||||
|
body += "\n" + scope_block(
|
||||||
|
f'html[data-theme-style="new-sakura"][data-page-scope="{key}"]', SCOPES[key]) + "\n"
|
||||||
|
|
||||||
|
dark_block = f"""
|
||||||
|
html[data-theme="dark"][data-theme-style="new-sakura"] {{
|
||||||
|
--tb-bg: #221a1e;
|
||||||
|
--tb-surface: #2c2227;
|
||||||
|
--tb-surface-2: #372a30;
|
||||||
|
--tb-surface-hover: rgba(255, 147, 180, 0.10);
|
||||||
|
--tb-navbar: #221a1e;
|
||||||
|
--tb-border: #47333b;
|
||||||
|
|
||||||
|
--tb-text: #f3e6ea;
|
||||||
|
--tb-text-secondary: #c9aeb8;
|
||||||
|
/* 5.82:1(底色)/ 5.26:1(卡片) */
|
||||||
|
--tb-text-muted: #ab9099;
|
||||||
|
|
||||||
|
--tb-primary: #ff93b4;
|
||||||
|
--tb-primary-weak: rgba(255, 147, 180, 0.16);
|
||||||
|
--tb-on-primary: #1b1014;
|
||||||
|
|
||||||
|
--tb-shadow: 0 1px 2px rgba(0, 0, 0, 0.34), 0 12px 28px -18px rgba(0, 0, 0, 0.58);
|
||||||
|
--tb-shadow-hover: 0 1px 2px rgba(0, 0, 0, 0.40), 0 18px 40px -20px rgba(0, 0, 0, 0.68);
|
||||||
|
|
||||||
|
{vars_dark}
|
||||||
|
|
||||||
|
/* 深色不铺底纹:浅粉花瓣压在深底上会变成一层噪点 */
|
||||||
|
--tb-page-image: linear-gradient(180deg, #251c21 0%, #221a1e 62%, #1e171b 100%);
|
||||||
|
--tb-page-pattern: none;
|
||||||
|
""" + deco_block_inline(SCOPES["global"], indent=" ") + "}\n"
|
||||||
|
|
||||||
|
for key in ("box", "question", "admin", "profile", "vote"):
|
||||||
|
dark_block += "\n" + scope_block(
|
||||||
|
f'html[data-theme="dark"][data-theme-style="new-sakura"][data-page-scope="{key}"]',
|
||||||
|
SCOPES[key]) + "\n"
|
||||||
|
|
||||||
|
narrow = (
|
||||||
|
"\n@media (max-width: 640px) {\n"
|
||||||
|
' html[data-theme-style="new-sakura"] {\n'
|
||||||
|
+ deco_block_inline(SCOPES["narrow"], indent=" ")
|
||||||
|
+ " /* 手机上唯一的留白是顶栏上下两条,装饰层没有立足之地 —— 缩小底纹让樱花密起来 */\n"
|
||||||
|
" --tb-page-pattern-size: 200px 200px;\n"
|
||||||
|
" }\n}\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
return HEADER + body + dark_block + narrow + FOOTER
|
||||||
|
|
||||||
|
|
||||||
|
def deco_block_inline(layers, indent=" "):
|
||||||
|
img, size, pos = deco(layers)
|
||||||
|
return (
|
||||||
|
f"{indent}--tb-deco: {img};\n"
|
||||||
|
f"{indent}--tb-deco-size: {size};\n"
|
||||||
|
f"{indent}--tb-deco-position: {pos};\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
HEADER = """/* 新·樱花(new-sakura):纯 CSS 樱花,不依赖任何图片素材。
|
||||||
|
|
||||||
|
和已有的 sakura 主题的区别:sakura 是"粉底 + 网点"的静态配色;这里把樱花本身
|
||||||
|
做成了可摆放的装饰层 —— 整朵花、带缺口的花瓣、带枝的花朵树,全部是内联 SVG
|
||||||
|
的 data URI,所以:换颜色只改一个色值、任意视口不糊、没有额外请求、不需要对象存储。
|
||||||
|
|
||||||
|
每个页面一套位置(--tb-deco / -size / -position 三组各 10 槽,必须等长):
|
||||||
|
global 未单独配置的页面 右下花枝 + 左上两瓣
|
||||||
|
box 箱主页 右上花枝垂下 + 左下花与落瓣
|
||||||
|
question 问题页 左下花枝 + 右上花与落瓣
|
||||||
|
admin 题目列表 左上花枝(只压顶部边距,不抢列表)
|
||||||
|
profile 设置页 右下花枝 + 左侧竖排小瓣
|
||||||
|
vote 投票页 左上花枝 + 右侧花与落瓣
|
||||||
|
≤640px 一律只留角落两朵小的 —— 卡片占满宽度后枝条会糊在字上
|
||||||
|
|
||||||
|
位置一律「两值 + 关键字」,不用三值语法(三值里混用 left/bottom 关键字是非法组合,
|
||||||
|
整条声明会作废,花瓣会静默回到左上角)。
|
||||||
|
|
||||||
|
生成:python dev/theme-tools/gen_new_sakura.py(改花形/位置请改那个脚本,别手改这里)
|
||||||
|
*/
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
FOOTER = """
|
||||||
|
/* 组件微调(仅外观):顶栏与粉底融在一起后,UIkit 默认的 #999 链接色偏淡 */
|
||||||
|
html[data-theme-style="new-sakura"] .uk-navbar-nav > li > a {
|
||||||
|
color: var(--tb-text-secondary);
|
||||||
|
}
|
||||||
|
html[data-theme-style="new-sakura"] .uk-navbar-nav > li > a:hover {
|
||||||
|
color: var(--tb-primary);
|
||||||
|
}
|
||||||
|
/* 信息提示条:UIkit 的浅蓝在樱粉底上是唯一的冷色块,换成主色的弱化底 */
|
||||||
|
html[data-theme-style="new-sakura"] .uk-alert-primary {
|
||||||
|
background-color: var(--tb-primary-weak);
|
||||||
|
color: #9c3356;
|
||||||
|
}
|
||||||
|
html[data-theme="dark"][data-theme-style="new-sakura"] .uk-alert-primary {
|
||||||
|
color: #ffb3c8;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
css = build()
|
||||||
|
with open(OUT, "w", encoding="utf-8", newline="\n") as f:
|
||||||
|
f.write(css)
|
||||||
|
print("已生成 %s(%.1f KB)" % (OUT, len(css.encode()) / 1024))
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
# new-sakura(新·樱花):纯 CSS 樱花主题
|
||||||
|
|
||||||
|
和已有的 `sakura` 的区别:`sakura` 是「粉底 + 网点」的静态配色;`new-sakura` 把樱花本身
|
||||||
|
做成了**可摆放的装饰层** —— 整朵花、带缺口的花瓣、带枝的花朵树,全部是内联 SVG 的
|
||||||
|
`data:image/svg+xml` URI,写在主题 CSS 里。
|
||||||
|
|
||||||
|
因此这个主题:
|
||||||
|
|
||||||
|
- **不需要任何图片素材**,也不依赖对象存储(`static/images/themes/new-sakura/` 是空的);
|
||||||
|
- 任意视口缩放都不糊(矢量),DPI 无关;
|
||||||
|
- 换花色只改 `dev/theme-tools/gen_new_sakura.py` 里的一个色值,重跑即得;
|
||||||
|
- 没有额外 HTTP 请求 —— SVG 就在 CSS 里。
|
||||||
|
|
||||||
|
代价是 CSS 有 40~50KB(gzip 后约 12KB)。主题 CSS 走 `StaticVersion` 缓存,一次性开销。
|
||||||
|
|
||||||
|
## 结构
|
||||||
|
|
||||||
|
```
|
||||||
|
static/css/themes/new-sakura.css ← 生成产物,别手改
|
||||||
|
dev/theme-tools/gen_new_sakura.py ← 改这里,重跑
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python dev/theme-tools/gen_new_sakura.py
|
||||||
|
```
|
||||||
|
|
||||||
|
生成器脚本里有三样东西:花形(`blossom` / `petal` / `branch` / `pattern`)、
|
||||||
|
每个页面作用域的位置表(`SCOPES`)、以及配色(`LIGHT` / `DARK`)。
|
||||||
|
|
||||||
|
## 每个页面的樱花位置
|
||||||
|
|
||||||
|
`--tb-deco` / `--tb-deco-size` / `--tb-deco-position` 三组各 10 槽,必须等长
|
||||||
|
(`TestDecoSlotCount` 会逐文件逐声明检查)。
|
||||||
|
|
||||||
|
| 作用域 | 选择器 | 构图 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| global | `html[data-theme-style="new-sakura"]` | 右下花枝 + 左上大花 + 两片落瓣 |
|
||||||
|
| box | `…[data-page-scope="box"]` | 右上花枝垂下(细端在外、粗端朝内)+ 左下花与落瓣 |
|
||||||
|
| question | `…[data-page-scope="question"]` | 左下花枝斜穿 + 右上大花 + 落瓣 |
|
||||||
|
| admin | `…[data-page-scope="admin"]` | 左上花枝 + 左侧底部小花(卡片占满宽度,右侧会被盖住) |
|
||||||
|
| profile | `…[data-page-scope="profile"]` | 右下花枝 + 左侧竖排小花与落瓣 |
|
||||||
|
| vote | `…[data-page-scope="vote"]` | 左上花枝 + 右侧花与落瓣 |
|
||||||
|
|
||||||
|
位置一律「关键字 + 偏移」两值写法(`right -24px top -16px`)。三值语法里混用
|
||||||
|
`left` / `bottom` 关键字是非法的,整条声明会作废、花瓣静默回到左上角。
|
||||||
|
|
||||||
|
### 窄屏(≤640px)
|
||||||
|
|
||||||
|
装饰层全部置 `none`:卡片占满宽度后,留白只剩顶栏上下两条,枝条会直接糊在文字上。
|
||||||
|
樱花改由底纹承担 —— `@media` 里把 `--tb-page-pattern-size` 从 300px 收到 200px,密度上来才看得见。
|
||||||
|
|
||||||
|
### 深色
|
||||||
|
|
||||||
|
- 花瓣换成更亮一档的粉(`#f0a0b8`),枝条提亮到能看见;
|
||||||
|
- 底纹 `--tb-page-pattern: none`:浅粉花瓣压在深底上会变成一层噪点;
|
||||||
|
- 整页背景改深色渐变(不用纯色,避免大面积死黑)。
|
||||||
|
|
||||||
|
深色块必须写成 `html[data-theme="dark"][data-theme-style="new-sakura"]`(三重选择器),
|
||||||
|
只写浅色块会被同特异性的作用域块盖掉。
|
||||||
|
|
||||||
|
## 配色与对比度
|
||||||
|
|
||||||
|
| 令牌 | 亮色 | 深色 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `--tb-bg` | `#fdf6f8` | `#221a1e` | 纯色,不用渐变(整页色感一致) |
|
||||||
|
| `--tb-surface` | `#ffffff` | `#2c2227` | 卡片 |
|
||||||
|
| `--tb-text` | `#3a2c31` (12.4:1) | `#f3e6ea` (14.0:1) | |
|
||||||
|
| `--tb-text-secondary` | `#6d555d` (6.3:1) | `#c9aeb8` (8.3:1) | |
|
||||||
|
| `--tb-text-muted` | `#77626a` (5.3:1) | `#ab9099` (5.8:1) | 踩 AA 下限 |
|
||||||
|
| `--tb-primary` | `#b8436a` (4.9:1) | `#ff93b4` (8.2:1) | 原候选 `#c14a72` 只有 4.39:1,压深 |
|
||||||
|
| `--tb-on-primary` | `#ffffff` (5.2:1) | `#1b1014` (8.9:1) | 主色按钮上的字 |
|
||||||
|
|
||||||
|
## 为什么不用主题生成器出图
|
||||||
|
|
||||||
|
生成器的装饰层只支持「图片素材」(导出时打进 zip 或传对象存储),没有内联 SVG 图层。
|
||||||
|
要把樱花做成可摆放的图层,只能在设计上二选一:
|
||||||
|
|
||||||
|
1. 生成图片素材(花瓣 PNG/WebP)→ 可以用生成器、可以放 S3,但会有位图缩放、额外请求;
|
||||||
|
2. 内联 SVG 写进 CSS → 零素材、任意缩放不糊,但生成器管不到。
|
||||||
|
|
||||||
|
`new-sakura` 选 2,因为「用 CSS 做樱花」本身就是这个主题的诉求。
|
||||||
|
生成器仍然可以用来**预览**它:`?demo=1&fresh=1` 起画板,把 `new-sakura.css` 注入即可。
|
||||||
|
|
||||||
|
## 怎么验证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test ./internal/theme # 10 槽等长、深浅块成对、必需令牌齐全
|
||||||
|
```
|
||||||
|
|
||||||
|
视觉自检用项目真实页面快照:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TEST_PREVIEW=1 go test ./templates -run TestGenPreview
|
||||||
|
python dev/theme-builder/serve.py
|
||||||
|
# 打开 outputs/preview-box-light.html,DevTools 里给 <html> 加
|
||||||
|
# data-theme-style="new-sakura" data-page-scope="box"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 上线
|
||||||
|
|
||||||
|
1. `conf/app.ini` 的 `[theme] available` 加上 `new-sakura`(不改就「设置页看不到」,现象和部署没生效一样);
|
||||||
|
2. 重新 `go build` —— 主题 CSS 是编译期 embed,只重启不重编不会变;
|
||||||
|
3. 箱主设置页 → 账号 → 外观,选择「新·樱花」。
|
||||||
@@ -941,4 +941,5 @@ var enDict = map[string]string{
|
|||||||
"theme.name.default": "Default look",
|
"theme.name.default": "Default look",
|
||||||
"theme.name.sakura": "Sakura",
|
"theme.name.sakura": "Sakura",
|
||||||
"theme.name.shiroko": "Shiroko",
|
"theme.name.shiroko": "Shiroko",
|
||||||
|
"theme.name.new-sakura": "New Sakura",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -941,4 +941,5 @@ var jaDict = map[string]string{
|
|||||||
"theme.name.default": "標準の外観",
|
"theme.name.default": "標準の外観",
|
||||||
"theme.name.sakura": "さくら",
|
"theme.name.sakura": "さくら",
|
||||||
"theme.name.shiroko": "シロコ",
|
"theme.name.shiroko": "シロコ",
|
||||||
|
"theme.name.new-sakura": "新さくら",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -882,6 +882,7 @@ var zhCNDict = map[string]string{
|
|||||||
"theme.name.default": "基础外观",
|
"theme.name.default": "基础外观",
|
||||||
"theme.name.sakura": "樱花",
|
"theme.name.sakura": "樱花",
|
||||||
"theme.name.shiroko": "砂狼白子",
|
"theme.name.shiroko": "砂狼白子",
|
||||||
|
"theme.name.new-sakura": "新·樱花",
|
||||||
|
|
||||||
// 验证码 JSON 接口:前端消费(P2)
|
// 验证码 JSON 接口:前端消费(P2)
|
||||||
"captcha.json.badRequest": "请求格式错误",
|
"captcha.json.badRequest": "请求格式错误",
|
||||||
|
|||||||
@@ -941,4 +941,5 @@ var zhTWDict = map[string]string{
|
|||||||
"theme.name.default": "基礎外觀",
|
"theme.name.default": "基礎外觀",
|
||||||
"theme.name.sakura": "櫻花",
|
"theme.name.sakura": "櫻花",
|
||||||
"theme.name.shiroko": "砂狼白子",
|
"theme.name.shiroko": "砂狼白子",
|
||||||
|
"theme.name.new-sakura": "新·櫻花",
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user