抽取配色css (#79)

Co-authored-by: 段舒康 <kdletters@qq.com>
Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/79
Reviewed-by: 段舒康 <kdletters@qq.com>
Co-authored-by: kvtodev <kvtodev@outlook.com>
Co-committed-by: kvtodev <kvtodev@outlook.com>
This commit was merged in pull request #79.
This commit is contained in:
2026-07-14 17:01:39 +08:00
committed by 段舒康
parent 0baa8cf8cc
commit d66b17b855
6 changed files with 621 additions and 540 deletions
@@ -2649,7 +2649,7 @@
# 2026-05-20 陶泥儿主视觉配色回收为暖白/陶土橙
- 背景:用户要求只替换产品各界面的 UI 颜色,不改布局,并以两张陶泥儿主视觉图作为配色依据。
- 决策:平台亮色主题的主色回收到暖白 / 米杏底、陶土橙主按钮、深棕正文与浅杏边框;后台管理也同步切换到同一暖橙体系。主题变量优先通过 `src/index.css``--platform-*` token 统一控制,零散组件只做必要的局部替换。
- 决策:平台亮色主题的主色回收到暖白 / 米杏底、陶土橙主按钮、深棕正文与浅杏边框;后台管理也同步切换到同一暖橙体系。主题变量和平台字体栈优先通过 `packages/shared/src/theme.css``--platform-*` token 统一控制,具体全局字体选择器留在消费端原有层叠位置,零散组件只做必要的局部替换。
- 影响范围:主站平台壳层、常用表单 / 按钮 / 卡片 / 背景、后台管理 UI、业务进度条和小游戏结果条的通用强调色。
- 验证方式:优先检查 `src/index.css``apps/admin-web/src/styles/admin.css` 是否还存在旧粉色主色;再用编码检查和可执行的本地 typecheck / build 验证。
- 关联文档:`docs/【项目基线】当前产品与工程约束-2026-05-15.md`
@@ -108,7 +108,7 @@ server-rs + Axum + SpacetimeDB
12. “我的”页泥点余额、累计游玩、已玩游戏三张统计卡只展示各自标签和值,三个统计 icon 使用小尺寸普通 UI 档位,内容不换行,不在统计区底部展示“更新于”时间;移动端昵称、常用功能和法律信息也应保持 `10px``14px` 的普通 UI 字号区间,避免展示级字号挤压内容。
13. 移动端“我的”页需要兼容窄屏:头像 / 昵称 / 陶泥号、三张统计卡、五项常用功能和法律信息都必须能在底部固定 TabBar 上方完整滚动露出,不得与底部 dock、刘海 safe-area 或相邻 UI 元素遮挡重叠。
14. RPG 等运行态的战斗飘字、血量变化和即时反馈必须在暗色、噪声高的场景背景上保持可读:使用高亮文字、深色描边、强阴影或小面积半透明底,不只依赖红/绿文字本身表达伤害或治疗。
15. 平台亮色 UI 配色以陶泥儿主视觉为准:暖白 / 米杏底、陶土橙主按钮、深棕正文与浅杏边框;新增界面优先复用 `src/index.css``--platform-*` 主题变量和 `apps/admin-web/src/styles/admin.css` 的同系色值,不再引入粉红、蓝绿等独立主色方案。
15. 平台亮色 UI 配色以陶泥儿主视觉为准:暖白 / 米杏底、陶土橙主按钮、深棕正文与浅杏边框;新增界面优先复用 `packages/shared/src/theme.css``--platform-*` 主题变量和平台字体栈、`apps/admin-web/src/styles/admin.css` 的同系色值,不再引入粉红、蓝绿等独立主色方案;具体全局字体选择器保留在消费端原有层叠位置,避免共享样式导入改变既有视觉
## 文案与编码
File diff suppressed because it is too large Load Diff
+53
View File
@@ -0,0 +1,53 @@
import fs from 'node:fs';
import path from 'node:path';
import {describe, expect, it} from 'vitest';
const themePath = path.resolve(
process.cwd(),
'packages/shared/src/theme.css',
);
function getCssBlock(source: string, selector: string) {
const selectorIndex = source.indexOf(selector);
expect(selectorIndex, `${selector} should exist`).toBeGreaterThanOrEqual(0);
const openBraceIndex = source.indexOf('{', selectorIndex);
let depth = 0;
for (let index = openBraceIndex; index < source.length; index += 1) {
const char = source[index];
if (char === '{') {
depth += 1;
} else if (char === '}') {
depth -= 1;
if (depth === 0) {
return source.slice(openBraceIndex + 1, index);
}
}
}
throw new Error(`${selector} block is not closed`);
}
describe('shared platform theme', () => {
it('exposes the shared platform font stack without fixing its cascade position', () => {
const css = fs.readFileSync(themePath, 'utf8');
const platformTheme = getCssBlock(css, '.platform-theme');
expect(platformTheme).toContain("--platform-font-family: 'Inter'");
expect(css).not.toContain('.platform-theme,\n.platform-theme *');
});
it('keeps the light and dark semantic color contracts', () => {
const css = fs.readFileSync(themePath, 'utf8');
const light = getCssBlock(css, '.platform-theme--light');
const dark = getCssBlock(css, '.platform-theme--dark');
expect(light).toContain('--platform-accent: #c7653d;');
expect(light).toContain('linear-gradient(135deg, #df7f40, #b95d3a)');
expect(light).toContain('--platform-text-strong: #3d1f10;');
expect(dark).toContain('--platform-accent: #5b6cff;');
expect(dark).toContain('linear-gradient(135deg, #5b6cff, #3dd9ff)');
expect(dark).toContain('--platform-text-strong: #ffffff;');
});
});
+2 -534
View File
File diff suppressed because it is too large Load Diff
+30 -4
View File
@@ -7,6 +7,13 @@ function readIndexCss() {
return fs.readFileSync(path.resolve(process.cwd(), 'src/index.css'), 'utf8');
}
function readPlatformThemeCss() {
return fs.readFileSync(
path.resolve(process.cwd(), 'packages/shared/src/theme.css'),
'utf8',
);
}
function getCssBlock(source: string, selector: string) {
const selectorIndex = source.indexOf(selector);
expect(selectorIndex, `${selector} should exist in src/index.css`).toBeGreaterThanOrEqual(0);
@@ -194,11 +201,16 @@ describe('index stylesheet unread dots', () => {
it('uses warm brown tokens for draft unread markers instead of red literals', () => {
const css = readIndexCss();
const themeCss = readPlatformThemeCss();
expect(css).toContain('--platform-unread-dot-fill: #8b5a3d;');
expect(css).toContain('--platform-unread-dot-glow: rgba(139, 90, 61, 0.34);');
expect(css).toContain('--platform-unread-dot-fill: #d6a27c;');
expect(css).toContain('--platform-unread-dot-glow: rgba(214, 162, 124, 0.24);');
expect(themeCss).toContain('--platform-unread-dot-fill: #8b5a3d;');
expect(themeCss).toContain(
'--platform-unread-dot-glow: rgba(139, 90, 61, 0.34);',
);
expect(themeCss).toContain('--platform-unread-dot-fill: #d6a27c;');
expect(themeCss).toContain(
'--platform-unread-dot-glow: rgba(214, 162, 124, 0.24);',
);
for (const selector of [
'.creation-work-card__unread-dot',
@@ -213,6 +225,20 @@ describe('index stylesheet unread dots', () => {
}
});
it('keeps platform font overrides after branded title typography', () => {
const css = readIndexCss();
const themeCss = readPlatformThemeCss();
const titleRuleIndex = css.indexOf('.selection-hero-brand__title');
const platformThemeRuleIndex = css.indexOf('.platform-theme,\n.platform-theme *');
expect(titleRuleIndex).toBeGreaterThanOrEqual(0);
expect(platformThemeRuleIndex).toBeGreaterThan(titleRuleIndex);
expect(getCssBlock(css, '.platform-theme,\n.platform-theme *')).toContain(
'font-family: var(--platform-font-family) !important;',
);
expect(themeCss).not.toContain('.platform-theme,\n.platform-theme *');
});
it('keeps the creation shelf share button on a visible surface', () => {
const css = readIndexCss();
const block = getCssBlock(css, '.creation-work-card__quick-action-button');