From 5a1afedfd372e09544f7397aad9e3d2a729f1e7a Mon Sep 17 00:00:00 2001 From: Joywayer Date: Thu, 27 Aug 2026 22:38:26 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20theme=20=E5=A5=91=E7=BA=A6(?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B+=E9=BB=98=E8=AE=A4=E5=80=BC+=E5=90=88?= =?UTF-8?q?=E5=B9=B6+Provider)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 子游戏只写要改的字段, 缺省集中在 DEFAULT_THEME 一处(spec §4.3, 第二准则的唯一例外)。ThemeProvider 未 init 即读抛错, 不兜底。 皮肤构建期定死, 故静态读取不做响应式。 Co-Authored-By: Claude Opus 5 (1M context) --- .../framework/ui/theme/default-theme.ts | 28 ++++++++++++ .../framework/ui/theme/default-theme.ts.meta | 9 ++++ .../framework/ui/theme/resolve-theme.ts | 11 +++++ .../framework/ui/theme/resolve-theme.ts.meta | 9 ++++ .../framework/ui/theme/theme-provider.ts | 23 ++++++++++ .../framework/ui/theme/theme-provider.ts.meta | 9 ++++ .../assets/framework/ui/theme/types.ts | 43 +++++++++++++++++++ .../assets/framework/ui/theme/types.ts.meta | 9 ++++ .../framework-tests/ui/theme.test.ts | 43 +++++++++++++++++++ 9 files changed, 184 insertions(+) create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/ui/theme/default-theme.ts create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/ui/theme/default-theme.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/ui/theme/resolve-theme.ts create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/ui/theme/resolve-theme.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/ui/theme/theme-provider.ts create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/ui/theme/theme-provider.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/ui/theme/types.ts create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/ui/theme/types.ts.meta create mode 100644 cocoscreator_projects/framework-tests/ui/theme.test.ts diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/theme/default-theme.ts b/cocoscreator_projects/YouleNexus/assets/framework/ui/theme/default-theme.ts new file mode 100644 index 0000000..aef78cd --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/theme/default-theme.ts @@ -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, + }, +}; diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/theme/default-theme.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/ui/theme/default-theme.ts.meta new file mode 100644 index 0000000..4a301ee --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/theme/default-theme.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "5f3f9c00-dd28-4c44-91c2-b747b751595c", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/theme/resolve-theme.ts b/cocoscreator_projects/YouleNexus/assets/framework/ui/theme/resolve-theme.ts new file mode 100644 index 0000000..0c78d07 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/theme/resolve-theme.ts @@ -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 }, + }; +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/theme/resolve-theme.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/ui/theme/resolve-theme.ts.meta new file mode 100644 index 0000000..6b03370 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/theme/resolve-theme.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "86f0162e-5db7-47c9-9fe0-a31d1e9dd643", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/theme/theme-provider.ts b/cocoscreator_projects/YouleNexus/assets/framework/ui/theme/theme-provider.ts new file mode 100644 index 0000000..1c461e3 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/theme/theme-provider.ts @@ -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; + }, +}; diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/theme/theme-provider.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/ui/theme/theme-provider.ts.meta new file mode 100644 index 0000000..f83ec04 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/theme/theme-provider.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "105e374f-7644-4114-9ced-5082cc129939", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/theme/types.ts b/cocoscreator_projects/YouleNexus/assets/framework/ui/theme/types.ts new file mode 100644 index 0000000..35bcd5b --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/theme/types.ts @@ -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; + fonts?: Partial; + layout?: Partial; +} + +/** 合并后的完整主题,框架内部消费 */ +export interface ResolvedTheme { + colors: ThemeColors; + fonts: ThemeFonts; + layout: ThemeLayout; +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/theme/types.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/ui/theme/types.ts.meta new file mode 100644 index 0000000..4901464 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/theme/types.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "3974f701-a0a3-4590-ba45-db688cf5a672", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/framework-tests/ui/theme.test.ts b/cocoscreator_projects/framework-tests/ui/theme.test.ts new file mode 100644 index 0000000..7695f76 --- /dev/null +++ b/cocoscreator_projects/framework-tests/ui/theme.test.ts @@ -0,0 +1,43 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { DEFAULT_THEME } from '../../YouleNexus/assets/framework/ui/theme/default-theme.ts'; +import { resolveTheme } from '../../YouleNexus/assets/framework/ui/theme/resolve-theme.ts'; +import { ThemeProvider } from '../../YouleNexus/assets/framework/ui/theme/theme-provider.ts'; + +test('resolveTheme 不传配置时返回默认主题', () => { + const t = resolveTheme(); + assert.deepEqual(t, DEFAULT_THEME); +}); + +test('resolveTheme 只覆盖给出的字段,其余取默认', () => { + const t = resolveTheme({ colors: { primary: '#C8102E' } }); + assert.equal(t.colors.primary, '#C8102E'); + assert.equal(t.colors.accent, DEFAULT_THEME.colors.accent); + assert.equal(t.fonts.sizeBody, DEFAULT_THEME.fonts.sizeBody); + assert.equal(t.layout.myInfoAnchorX, DEFAULT_THEME.layout.myInfoAnchorX); +}); + +test('resolveTheme 分组独立合并,互不影响', () => { + const t = resolveTheme({ layout: { myInfoAnchorX: 130 } }); + assert.equal(t.layout.myInfoAnchorX, 130); + assert.equal(t.layout.nicknameMaxChars, DEFAULT_THEME.layout.nicknameMaxChars); + assert.deepEqual(t.colors, DEFAULT_THEME.colors); +}); + +test('resolveTheme 不修改 DEFAULT_THEME 本身', () => { + const before = DEFAULT_THEME.colors.primary; + resolveTheme({ colors: { primary: '#000000' } }); + assert.equal(DEFAULT_THEME.colors.primary, before); +}); + +test('ThemeProvider 未 init 就读取会抛错(第二准则:不兜底)', () => { + ThemeProvider.reset(); + assert.throws(() => ThemeProvider.get(), /未初始化/); +}); + +test('ThemeProvider init 后可读回同一份主题', () => { + ThemeProvider.reset(); + const t = resolveTheme({ colors: { primary: '#123456' } }); + ThemeProvider.init(t); + assert.equal(ThemeProvider.get().colors.primary, '#123456'); +});