NextUI 跨平台基础层方案
状态: ✅ 阶段 1-3 已完成,阶段 4 进行中
最后更新: 2026-01-13
一、现状分析
1.1 已实现的功能(C#/Blazor 端)
✅ 主题色/重点色支持
- 位置:
src/NextUI.Core/ThemeState.cs - 功能:
PrimarySeedHex: 支持任意颜色作为主题色GeneratePalette(): 生成完整调色板(P50-P900)Mix(),AdjustBrightness(): 颜色计算工具RecentColors: 最近使用的颜色历史
- 状态: ✅ 已实现
✅ 颜色编辑器
- 位置:
src/NextUI.Blazor/Components/SxColorPicker.razor - 功能: 基础颜色选择器(HTML5 color input)
- 状态: ✅ 已实现(基础版,可能缺少高级功能如调色板选择、预设颜色等)
✅ 字体大小调整
- 位置:
src/NextUI.Core/ThemeState.cs(FontScale属性) - 功能: 支持百分比缩放(如 0.8, 1.0, 1.2, 1.5)
- CSS:
src/NextUI.Tokens/Exports/blazor.css中有--ds-font-scale变量 - 状态: ✅ 已实现(但需要确认是否动态注入到页面)
✅ 多语言支持
- 位置:
src/NextUI.Core/LocaleState.cs - 功能:
SetCulture(): 即时切换语言- 持久化到
IUserPreferences - 自动应用
CultureInfo
- 状态: ✅ 已实现
1.2 缺失的部分
❌ HTML/JS 运行时支持
- 当前这些功能只在 C#/Blazor 端实现
- 没有纯 HTML 页面可以使用的 JS 版本
- 没有统一的 CSS 变量注入机制
❌ CSS 变量不完整
sx-tokens.css中缺少主题色调色板变量(只有--sx-colorBrandBackground1,没有 P50-P900)sx-tokens.css中缺少字体缩放变量(blazor.css有--ds-font-scale,但命名不一致)- 没有动态注入机制将
ThemeState的值同步到 CSS 变量
❌ Blazor 桥接缺失
ThemeState.Changed事件触发后,没有自动将新值注入到 CSS- 需要手动在 Blazor 组件中订阅事件并调用 JSInterop
二、目标架构
2.1 核心原则
单一数据源 (Single Source of Truth)
- C# 端:
ThemeState/LocaleState是权威数据源 - HTML 端:localStorage 是持久化存储
- CSS 变量:是视觉表现的唯一出口
- C# 端:
双向同步
- Blazor → CSS:C# 状态变化时自动注入 CSS 变量
- HTML → CSS:JS 运行时从 localStorage 读取并注入
- 跨页面同步:通过 localStorage 事件或共享存储
渐进增强
- 纯 HTML 页面:只依赖 CSS + JS(无 Blazor)
- Blazor 应用:C# 状态优先,JS 作为桥接层
2.2 文件结构
src/NextUI.Tokens/Exports/
├── tokens.css # 静态 CSS 变量定义(基础值)
├── theme-runtime.js # JS 运行时(新增,跨平台资源)
└── tokens.json # 已有
src/NextUI.Blazor/
├── Components/
│ └── SxThemeProvider.razor # Blazor 桥接组件(新增)
└── wwwroot/
├── js/ # Blazor 组件专用 JS(已有)
└── Styles/
└── sx-tokens.css # 需要扩展(添加主题色/字体缩放变量)
docs/
├── DesignTokens.md # 需要同步更新(添加新增 CSS 变量说明)
└── PLATFORM_AGNOSTIC_FOUNDATION.md # 本方案文档(架构文档)
三、详细方案
3.1 CSS 变量层(统一命名与扩展)
3.1.1 统一命名规范
- 前缀:
--sx-*(与现有sx-tokens.css保持一致) - 主题色:
--sx-primary-{shade}(50, 100, 200, ..., 900) - 字体缩放:
--sx-font-scale - 语言:
--sx-locale(可选,主要用于 CSS:lang()选择器)
3.1.2 需要添加的 CSS 变量
在 sx-tokens.css 的 :root 中添加:
/* 主题色系统(动态注入) */
--sx-primary-seed: #3B82F6; /* 种子色 */
--sx-primary-50: #EFF6FF; /* 最浅 */
--sx-primary-100: #DBEAFE;
--sx-primary-200: #BFDBFE;
--sx-primary-300: #93C5FD;
--sx-primary-400: #60A5FA;
--sx-primary-500: #3B82F6; /* 基准色 */
--sx-primary-600: #2563EB;
--sx-primary-700: #1D4ED8;
--sx-primary-800: #1E40AF;
--sx-primary-900: #1E3A8A; /* 最深 */
/* 语义化别名(向后兼容) */
--sx-colorBrandBackground1: var(--sx-primary-500);
--sx-colorBrandBackground2: var(--sx-primary-600);
--sx-colorBrandForeground1: var(--sx-primary-500);
/* 字体缩放(动态注入) */
--sx-font-scale: 1.0;
/* 所有字体大小基于缩放 */
--sx-font-size-base: calc(1rem * var(--sx-font-scale, 1));
--sx-font-size-sm: calc(0.875rem * var(--sx-font-scale, 1));
--sx-font-size-xs: calc(0.75rem * var(--sx-font-scale, 1));
--sx-font-size-lg: calc(1.125rem * var(--sx-font-scale, 1));
--sx-font-size-xl: calc(1.5rem * var(--sx-font-scale, 1));
3.1.3 暗色模式适配
在 [data-theme="dark"] 中,主题色需要自动调整(通过 JS 计算或预设映射)。
3.2 JS 运行时层(theme-runtime.js)
3.2.1 核心职责
- 初始化: 从 localStorage 读取设置,注入到 CSS 变量
- 主题色管理: 生成调色板(P50-P900),注入 CSS
- 字体缩放: 更新
--sx-font-scale - 语言切换: 更新
document.documentElement.lang和--sx-locale - 系统主题监听: 监听
prefers-color-scheme变化
3.2.2 API 设计
// theme-runtime.js (ES Module)
export const ThemeRuntime = {
// 初始化(页面加载时调用)
async init() {
// 1. 从 localStorage 读取
// 2. 注入 CSS 变量
// 3. 监听系统主题变化
},
// 设置主题色(生成调色板并注入)
setPrimaryColor(hex) {
const palette = generatePalette(hex);
injectPaletteToCSS(palette);
saveToLocalStorage('theme_primary_hex', hex);
},
// 设置字体缩放
setFontScale(scale) {
document.documentElement.style.setProperty('--sx-font-scale', scale);
saveToLocalStorage('theme_font_scale', scale);
},
// 设置语言
setLocale(locale) {
document.documentElement.lang = locale;
document.documentElement.style.setProperty('--sx-locale', locale);
saveToLocalStorage('locale_culture', locale);
},
// 设置主题模式(light/dark/system)
setThemeMode(mode) {
// 处理 system 模式:监听 prefers-color-scheme
// 应用 data-theme 属性
}
};
// 颜色工具函数(与 C# ThemeState.GeneratePalette 逻辑一致)
function generatePalette(seedHex) {
// 实现与 C# 端相同的 Mix 算法
// 返回 { 50: '#...', 100: '#...', ..., 900: '#...' }
}
3.2.3 localStorage 键名(与 C# 端保持一致)
theme_mode:"Light" | "Dark" | "System"theme_primary_hex:"#3B82F6"theme_font_scale:1.0locale_culture:"en-US"
3.3 Blazor 桥接层(SxThemeProvider.razor)
3.3.1 组件职责
- 注入
IThemeState和ILocaleState - 订阅
Changed事件 - 通过 JSInterop 调用
ThemeRuntime同步到 CSS - 初始化时从 C# 状态同步到 CSS
3.3.2 实现要点
@inject IThemeState ThemeState
@inject ILocaleState LocaleState
@inject IJSRuntime JS
@implements IDisposable
@code {
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// 初始化:将 C# 状态同步到 CSS
await SyncToCSS();
// 订阅变化事件
ThemeState.Changed += OnThemeChanged;
LocaleState.Changed += OnLocaleChanged;
}
}
private async void OnThemeChanged()
{
await SyncToCSS();
}
private async Task SyncToCSS()
{
// 调用 JS ThemeRuntime
await JS.InvokeVoidAsync("ThemeRuntime.setPrimaryColor", ThemeState.PrimarySeedHex);
await JS.InvokeVoidAsync("ThemeRuntime.setFontScale", ThemeState.FontScale);
await JS.InvokeVoidAsync("ThemeRuntime.setThemeMode", ThemeState.Mode.ToString());
}
}
3.3.3 使用方式
在 Blazor 应用的根组件(如 App.razor 或 MainLayout.razor)中添加:
<SxThemeProvider />
3.4 HTML 页面使用方式
3.4.1 基础用法
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" href="/path/to/sx-tokens.css">
<script type="module">
import { ThemeRuntime } from '/path/to/theme-runtime.js';
ThemeRuntime.init();
</script>
</head>
<body>
<button style="background: var(--sx-primary-500);">
使用主题色
</button>
</body>
</html>
3.4.2 动态修改
import { ThemeRuntime } from './theme-runtime.js';
// 改变主题色
ThemeRuntime.setPrimaryColor('#FF5733');
// 改变字体大小
ThemeRuntime.setFontScale(1.2);
// 切换语言
ThemeRuntime.setLocale('zh-CN');
四、实施步骤
阶段 1:CSS 变量扩展(1-2 天)
- ✅ 在
sx-tokens.css中添加主题色调色板变量(P50-P900) - ✅ 添加
--sx-font-scale变量 - ✅ 更新所有字体大小变量使用
calc()基于--sx-font-scale - ✅ 添加语义化别名(向后兼容)
- ✅ 同步更新
docs/DesignTokens.md:添加新增的 CSS 变量说明,保持文档与代码一致
阶段 2:JS 运行时开发(2-3 天)
- ✅ 创建
src/NextUI.Tokens/Exports/theme-runtime.js - ✅ 实现颜色调色板生成算法(与 C# 端一致)
- ✅ 实现 CSS 变量注入函数
- ✅ 实现 localStorage 读写
- ✅ 实现系统主题监听(
prefers-color-scheme)
阶段 3:Blazor 桥接(1-2 天)
- ✅ 创建
SxThemeProvider.razor组件 - ✅ 实现事件订阅和 JSInterop 调用
- ✅ 在 Workbench 中测试
阶段 4:测试与文档(1 天)
- ✅ 创建纯 HTML 示例页面 (
docs/examples/standalone-html-example.html) - ✅ 验证 Blazor 和 HTML 端的一致性
- ✅ 同步更新
docs/DesignTokens.md:确保所有新增的 CSS 变量都有文档说明 - ✅ 创建
SxThemeProvider组件文档 - ✅ 更新本方案文档(记录完成情况)
五、关键技术决策
5.1 颜色算法一致性
问题: JS 端的 generatePalette() 必须与 C# 端的 ThemeState.GeneratePalette() 逻辑完全一致。
方案:
- 使用相同的混合算法(Mix with weight)
- 可以考虑将算法提取为共享规范文档,或通过测试用例确保一致性
5.2 性能考虑
问题: 每次主题色变化都要生成 10 个颜色并注入 CSS。
方案:
- 颜色计算是轻量操作(纯数学运算)
- CSS 变量注入是同步操作,性能影响可忽略
- 如果担心,可以添加防抖(但通常不需要)
5.3 跨页面同步
问题: 用户在页面 A 修改主题,页面 B 如何同步?
方案:
- 选项 A: 使用
localStorage的storage事件(同源页面自动同步) - 选项 B: 每次页面加载时从 localStorage 读取(简单可靠)
- 推荐: 选项 B(更简单,延迟可接受)
5.4 向后兼容
问题: 现有组件使用 --sx-colorBrandBackground1,新变量是 --sx-primary-500。
方案:
- 在 CSS 中添加别名:
--sx-colorBrandBackground1: var(--sx-primary-500); - 保持现有变量名不变,新代码可以使用新命名
六、风险评估
6.1 低风险
- ✅ CSS 变量扩展:纯添加,不影响现有代码
- ✅ JS 运行时:独立模块,不影响 Blazor 端
6.2 中风险
⚠️ 颜色算法一致性: 如果 JS 和 C# 端不一致,会导致视觉差异
- 缓解: 编写测试用例对比两端的输出
⚠️ 字体缩放影响: 如果某些组件使用固定
px而非rem,缩放可能不生效- 缓解: 代码审查,确保所有尺寸使用相对单位
6.3 需要确认
- ❓ IUserPreferences 实现: 当前
ThemeState依赖IUserPreferences,需要确认是否有实现类,以及是否使用 localStorage - ❓ 颜色编辑器高级功能:
SxColorPicker是否需要扩展(调色板选择、预设颜色等)
6.4 文档同步要求 [MANDATORY]
- ⚠️ DesignTokens.md 必须同步更新: 实施过程中添加的所有 CSS 变量(主题色调色板、字体缩放等)必须同步更新到
docs/DesignTokens.md,保持文档与代码一致 - ⚠️ 文档即真理原则: 任何 CSS 变量变更必须先更新文档,再修改代码
七、后续扩展(第二阶段)
完成基础层后,可以考虑:
- Web Components 组件层: 将常用组件(Button、Input 等)封装为 Web Components
- 主题预设: 提供预设主题色(如 Material Design、Fluent UI 的默认调色板)
- 动画过渡: 主题切换时的颜色过渡动画
- 可访问性增强: 确保主题色满足 WCAG 对比度要求
八、总结
这个方案的核心是建立统一的 CSS 变量层作为视觉表现的唯一出口,然后通过:
- JS 运行时(HTML 端)和
- Blazor 桥接组件(Blazor 端)
将状态同步到 CSS,实现"既支持 Blazor 也支持 HTML"的目标。
优势:
- ✅ 单一数据源,避免不一致
- ✅ 渐进增强,HTML 页面无需 Blazor
- ✅ 向后兼容,现有代码不受影响
工作量估算: 5-8 天(包含测试)
九、实施进度
✅ 已完成(2026-01-13)
阶段 1:CSS 变量扩展
- ✅ 在
sx-tokens.css中添加主题色调色板变量(P50-P900) - ✅ 添加
--sx-font-scale变量 - ✅ 更新所有字体大小变量使用
calc()基于--sx-font-scale - ✅ 添加语义化别名(向后兼容)
- ✅ 同步更新
docs/DesignTokens.md - ✅ 单元测试: 24 个测试全部通过 ✅
阶段 2:JS 运行时开发
- ✅ 创建
src/NextUI.Tokens/Exports/theme-runtime.js - ✅ 实现颜色调色板生成算法(与 C# 端一致)
- ✅ 实现 CSS 变量注入函数
- ✅ 实现 localStorage 读写
- ✅ 实现系统主题监听(
prefers-color-scheme) - ✅ 实现初始化函数和完整 API
- ✅ 复制到
src/NextUI.Blazor/wwwroot/js/作为备用
阶段 3:Blazor 桥接
- ✅ 创建
SxThemeProvider.razor组件 - ✅ 实现事件订阅和 JSInterop 调用
- ✅ 实现初始化同步
- ✅ 创建组件文档
docs/components/SxThemeProvider.md - ✅ 代码无 linter 错误
阶段 4:测试与文档(✅ 已完成)
- ✅ 创建纯 HTML 示例页面 (
docs/examples/standalone-html-example.html) - ✅ 创建测试指南文档 (
docs/PLATFORM_AGNOSTIC_FOUNDATION_TESTING.md) - ✅ 在 Workbench 中集成
SxThemeProvider - ✅ 创建主题测试页面 (
workbench/NextUI.Workbench/Pages/ThemeTestPage.razor) - ✅ 验证主题功能正常工作(测试通过)
注意: 实际运行测试需要在本地环境中进行,因为需要:
- 启动 Workbench 应用
- 在浏览器中打开 HTML 示例页面
- 验证功能是否正常工作
📝 创建的文件清单
src/NextUI.Tokens/Exports/theme-runtime.js- JS 运行时模块(跨平台)src/NextUI.Blazor/Components/SxThemeProvider.razor- Blazor 桥接组件src/NextUI.Blazor/wwwroot/js/theme-runtime.js- 备用副本(开发用)src/NextUI.Core.Tests/- 测试项目(24 个测试全部通过)docs/components/SxThemeProvider.md- 组件文档docs/examples/standalone-html-example.html- HTML 示例页面
🔧 修改的文件清单
src/NextUI.Blazor/wwwroot/Styles/sx-tokens.css- 添加主题色和字体缩放变量docs/DesignTokens.md- 同步更新文档src/NextUI.Tokens/NextUI.Tokens.csproj- 添加 theme-runtime.js 到项目NextUI.sln- 添加测试项目.cursorrules- 扩展目录规范docs/ARCHITECTURE.md- 更新物理结构说明
请确认是否同意此方案,或提出需要调整的地方。