feat(ui): theme 契约(类型+默认值+合并+Provider)

子游戏只写要改的字段, 缺省集中在 DEFAULT_THEME 一处(spec §4.3,
第二准则的唯一例外)。ThemeProvider 未 init 即读抛错, 不兜底。
皮肤构建期定死, 故静态读取不做响应式。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-27 22:38:26 +08:00
co-authored by Claude Opus 5
parent cd064756d6
commit 5a1afedfd3
9 changed files with 184 additions and 0 deletions
@@ -0,0 +1,28 @@
import type { ResolvedTheme } from './types.ts';
/**
* 主题缺省值的**唯一来源**(spec §4.3)。
* 这是 CLAUDE.md 第二准则明文列出的唯一例外——缺省集中在来源处,下游子游戏零兜底。
* 不得以此为先例:协议数据缺失仍须显式暴露,不得 ?? 抹平。
*/
export const DEFAULT_THEME: ResolvedTheme = {
colors: {
primary: '#1E7A46',
accent: '#F2B233',
textPrimary: '#FFFFFF',
textSecondary: '#C8D6CE',
disabled: '#8A8A8A',
warning: '#D94F3D',
},
fonts: {
familyPath: null,
sizeTitle: 32,
sizeBody: 24,
sizeCaption: 20,
},
layout: {
myInfoAnchorX: 130,
nicknameMaxChars: 8,
infoNicknameMaxChars: 16,
},
};
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "5f3f9c00-dd28-4c44-91c2-b747b751595c",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -0,0 +1,11 @@
import type { ResolvedTheme, ThemeConfig } from './types.ts';
import { DEFAULT_THEME } from './default-theme.ts';
/** 把子游戏的部分主题配置合并到默认主题上。分组独立合并,不改动 DEFAULT_THEME。 */
export function resolveTheme(game: ThemeConfig = {}): ResolvedTheme {
return {
colors: { ...DEFAULT_THEME.colors, ...game.colors },
fonts: { ...DEFAULT_THEME.fonts, ...game.fonts },
layout: { ...DEFAULT_THEME.layout, ...game.layout },
};
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "86f0162e-5db7-47c9-9fe0-a31d1e9dd643",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -0,0 +1,23 @@
import type { ResolvedTheme } from './types.ts';
let current: ResolvedTheme | null = null;
/**
* 主题持有者。皮肤在构建期即定死,运行时不变,故为静态读取、**不做响应式**(spec §4.5)。
* 子游戏在启动时调 init 注册自己的主题——框架不 import 子游戏(零耦合规则)。
*/
export const ThemeProvider = {
init(theme: ResolvedTheme): void {
current = theme;
},
get(): ResolvedTheme {
if (!current) {
throw new Error('ThemeProvider 未初始化:请在启动时调用 ThemeProvider.init(resolveTheme(theme))');
}
return current;
},
/** 仅供测试复位 */
reset(): void {
current = null;
},
};
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "105e374f-7644-4114-9ced-5082cc129939",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -0,0 +1,43 @@
/**
* 主题契约。见 spec `2026-08-27-ui-asset-and-skin-design.md` §4。
* 字段集不预先穷举——随框架 UI 抽象逐项浮现;加字段非破坏性,删字段/改语义为 breaking change。
*/
export interface ThemeColors {
primary: string;
accent: string;
textPrimary: string;
textSecondary: string;
disabled: string;
warning: string;
}
export interface ThemeFonts {
/** 字体资源逻辑路径;null = 用引擎默认字体 */
familyPath: string | null;
sizeTitle: number;
sizeBody: number;
sizeCaption: number;
}
export interface ThemeLayout {
/** 自己信息的对齐点 x(旧 Game_Config.Info.myPosition) */
myInfoAnchorX: number;
/** 主界面昵称最大字符数(旧 Game_Config.Info.Mainnickname) */
nicknameMaxChars: number;
/** 个人信息页昵称最大字符数(旧 Game_Config.Info.Infonickname) */
infoNicknameMaxChars: number;
}
/** 子游戏声明用:各组均可只写要改的字段 */
export interface ThemeConfig {
colors?: Partial<ThemeColors>;
fonts?: Partial<ThemeFonts>;
layout?: Partial<ThemeLayout>;
}
/** 合并后的完整主题,框架内部消费 */
export interface ResolvedTheme {
colors: ThemeColors;
fonts: ThemeFonts;
layout: ThemeLayout;
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "3974f701-a0a3-4590-ba45-db688cf5a672",
"files": [],
"subMetas": {},
"userData": {}
}