主题系统
配置 App 的外观模式(亮色/暗色/跟随系统)与主题系列
主题系统
App 端的主题由两个独立维度组合而成:外观模式(亮色 / 暗色 / 跟随系统)和主题系列(Theme Family)。两者组合后生成一个活跃主题名称,由 Uniwind 驱动实际样式渲染。
两个维度
外观模式(ThemeModePreference)
控制应用使用亮色还是暗色配色:
| 值 | 说明 |
|---|---|
system | 跟随设备系统设置(默认) |
light | 固定亮色模式 |
dark | 固定暗色模式 |
主题系列(ThemeFamily)
控制应用的整体色调风格,内置 4 套:
| 值 | 风格 |
|---|---|
alpha | 默认主题系列 |
lavender | 薰衣草紫 |
mint | 薄荷绿 |
sky | 天空蓝 |
活跃主题名称
两个维度组合后生成活跃主题名:{themeFamily}-{resolvedThemeMode}
例如,用户选择 lavender 系列 + 深色模式,则活跃主题为 lavender-dark。该名称通过 Uniwind.setTheme() 驱动组件库的样式切换。
核心文件
| 文件 | 说明 |
|---|---|
apps/native/providers/theme-provider.tsx | 主题状态管理、持久化读写、Uniwind 同步 |
apps/native/configs/app-config.ts | 存储键名配置 |
用户偏好存储
主题选择通过 AsyncStorage 持久化到设备本地:
存储键(来自 app-config.ts) | 内容 |
|---|---|
{AppName}_theme_preference | 外观模式:system / light / dark |
{AppName}_theme_family | 主题系列:alpha / lavender / mint / sky |
其中 AppName 来自 packages/app-config 中配置的应用名称。
更改默认主题
ThemeProvider 初始化时,若 AsyncStorage 中没有存储值,则使用代码中定义的默认值:
const [themeModePreference, setThemeModePreferenceState] =
useState<ThemeModePreference>("system"); // 默认:跟随系统
const [themeFamily, setThemeFamilyState] = useState<ThemeFamily>("alpha"); // 默认:alpha修改这两行的初始值,即可更改新用户首次打开 App 时的默认主题。
新增主题系列
在类型中添加新系列
编辑 theme-provider.tsx,将新系列加入 THEME_FAMILIES 常量和 ThemeFamily 类型:
const THEME_FAMILIES = ["alpha", "lavender", "mint", "sky", "ocean"] as const;
// ↑ 新增
export type ThemeFamily = "alpha" | "lavender" | "mint" | "sky" | "ocean";在 Uniwind 中注册对应主题
参照项目中已有的 alpha、lavender 等系列,新系列需要三步:新建 CSS 文件、在 global.css 中导入、在 metro.config.js 中注册。
1. 新建 apps/native/themes/ocean.css
参照 themes/alpha.css 的结构,为亮色和暗色各写一套变量:
@layer theme {
:root {
@variant ocean-light {
--radius: 0.5rem;
--background: oklch(0.97 0.01 220);
--foreground: oklch(0.15 0.03 220);
--surface: oklch(0.97 0.01 220);
--surface-foreground: var(--foreground);
--surface-secondary: oklch(0.93 0.02 220);
--surface-secondary-foreground: var(--foreground);
--surface-tertiary: oklch(0.90 0.02 220);
--surface-tertiary-foreground: var(--foreground);
--overlay: oklch(0.97 0.01 220);
--overlay-foreground: var(--foreground);
--muted: var(--color-neutral-500);
--default: oklch(0.92 0.02 220);
--default-foreground: oklch(0.15 0.03 220);
--accent: oklch(0.45 0.15 220);
--accent-foreground: var(--snow);
--field-background: var(--default);
--field-foreground: var(--foreground);
--field-placeholder: var(--muted);
--field-border: transparent;
--success: oklch(0.55 0.12 154);
--success-foreground: var(--snow);
--warning: oklch(0.72 0.15 65);
--warning-foreground: var(--eclipse);
--danger: oklch(0.63 0.19 29);
--danger-foreground: var(--snow);
--segment: oklch(0.97 0.01 220);
--segment-foreground: var(--eclipse);
--border: oklch(0.85 0.03 220);
--separator: oklch(0.75 0.03 220);
--focus: var(--accent);
--link: var(--foreground);
--surface-shadow:
0 2px 4px 0 rgba(0, 0, 0, 0.04), 0 1px 2px 0 rgba(0, 0, 0, 0.06),
0 0 1px 0 rgba(0, 0, 0, 0.06);
--overlay-shadow:
0 2px 8px 0 rgba(0, 0, 0, 0.02), 0 14px 28px 0 rgba(0, 0, 0, 0.03);
--field-shadow:
0 2px 4px 0 rgba(0, 0, 0, 0.04), 0 1px 2px 0 rgba(0, 0, 0, 0.06),
0 0 1px 0 rgba(0, 0, 0, 0.06);
}
@variant ocean-dark {
--radius: 0.5rem;
--background: oklch(0.12 0.03 220);
--foreground: oklch(0.92 0.02 220);
--surface: oklch(0.17 0.03 220);
--surface-foreground: var(--foreground);
--surface-secondary: oklch(0.22 0.03 220);
--surface-secondary-foreground: var(--foreground);
--surface-tertiary: oklch(0.25 0.03 220);
--surface-tertiary-foreground: var(--foreground);
--overlay: oklch(0.20 0.03 220);
--overlay-foreground: var(--foreground);
--muted: var(--color-neutral-400);
--default: oklch(0.20 0.03 220);
--default-foreground: var(--snow);
--accent: oklch(0.65 0.15 220);
--accent-foreground: var(--eclipse);
--field-background: var(--default);
--field-foreground: var(--foreground);
--field-placeholder: var(--muted);
--field-border: transparent;
--success: oklch(0.55 0.12 154);
--success-foreground: var(--snow);
--warning: oklch(0.85 0.14 78);
--warning-foreground: var(--eclipse);
--danger: oklch(0.58 0.16 31);
--danger-foreground: var(--snow);
--segment: oklch(0.20 0.03 220);
--segment-foreground: var(--foreground);
--border: oklch(0.25 0.03 220);
--separator: oklch(0.35 0.03 220);
--focus: var(--accent);
--link: var(--foreground);
--surface-shadow: 0 0 0 0 transparent inset;
--overlay-shadow: 0 0 1px 0 rgba(255, 255, 255, 0.2) inset;
--field-shadow: 0 0 0 0 transparent inset;
}
}
}每个主题必须声明完全相同的变量名,参照
alpha.css核对变量列表。
2. 在 global.css 中导入
@import "./themes/alpha.css";
@import "./themes/lavander.css";
@import "./themes/mint.css";
@import "./themes/sky.css";
@import "./themes/ocean.css"; /* 新增 */3. 在 metro.config.js 中注册
module.exports = withUniwindConfig(config, {
cssEntryFile: "./global.css",
dtsFile: "./uniwind-types.d.ts",
extraThemes: [
"alpha-light", "alpha-dark",
"lavender-light", "lavender-dark",
"mint-light", "mint-dark",
"sky-light", "sky-dark",
"ocean-light", "ocean-dark", // 新增
],
});修改 metro.config.js 后需要重启 Metro。如遇缓存异常,运行 npx expo start --clear。
添加 i18n 翻译(可选)
在 packages/i18n/messages/native/ 下的各语言文件中,为新系列补充显示名称和描述:
{
"settings": {
"themeFamilyOptions": {
"ocean": "海洋"
},
"themeFamilyDescriptions": {
"ocean": "深邃的海洋蓝调"
}
}
}