设计系统
Elftia 的视觉设计追求温暖、亲和、高级感,避免冰冷的纯技术风格。
设计哲学
色彩原则
- 暖调中性色:所有表面色带有微暖色调(hue 24-38),而非纯灰度
- 深色模式:暖炭色(warm charcoal),而非纯黑
- 浅色模式:暖奶油色(warm cream),而非纯白
- 禁止冷灰:不使用
hsl(0, 0%, ...)的纯灰色作为背景或边框
圆角原则
| 元素 | 圆角 | Tailwind 类 |
|---|---|---|
| 卡片/容器 | 12px | rounded-xl |
| 输入框 | 12px | rounded-xl |
| 按钮/徽章 | 6px | rounded-md |
| 默认 | 8px | rounded-lg |
禁止使用小于 4px 的圆角(除线条装饰)。
排版原则
| 用途 | 字体 | Tailwind 类 |
|---|---|---|
| 展示/大标题 | Noto Serif / Georgia | font-display |
| 正文/界面 | Inter | font-sans |
| 代码 | JetBrains Mono | font-mono |
大标题使用 font-semibold(而非 font-bold),配合 tracking-tight。
颜色系统
语义化 Token
所有颜色通过 CSS 变量定义,Tailwind 配置映射为实用类。禁止使用硬编码颜色值。
{/* 禁止 */}
<div className="bg-white text-black border-gray-200">
<div style={{ backgroundColor: '#ffffff' }}>
{/* 正确 */}
<div className="bg-surface-0 text-foreground border-border">
<div style={{ backgroundColor: 'var(--surface-0)' }}>
常用 Token 对照表
| 用途 | Tailwind 类 | CSS 变量 |
|---|---|---|
| 页面背景 | bg-background | var(--background) |
| L0 背景 | bg-surface-0 | var(--surface-0) |
| L1 背景(卡片/侧边栏) | bg-surface-1 | var(--surface-1) |
| L2 背景(输入框/次级容器) | bg-surface-2 | var(--surface-2) |
| L3 背景(弹出层) | bg-surface-3 | var(--surface-3) |
| 主文本 | text-foreground | var(--foreground) |
| 次要文本 | text-muted-foreground | var(--muted-foreground) |
| 辅助文本 | text-text-subtle | var(--text-subtle) |
| 边框 | border-border | var(--border) |
| 主题色 | bg-primary / text-primary | var(--primary) |
| 成功 | text-success | var(--success) |
| 错误 | text-destructive | var(--destructive) |
| 警告 | text-warning | var(--warning) |
深色/浅色模式
切换机制
使用 Tailwind 的 class 策略(darkMode: 'class'),通过 ThemeContext 统一管理。
{/* 正确:通过 useTheme 获取主题信息 */}
import { useTheme } from '@/shared/state/themeStore';
function MyComponent() {
const { mode, resolvedMode, userTheme } = useTheme();
}
{/* 禁止:手动检测 */}
const isDark = localStorage.getItem('theme') === 'dark';
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
层级体系
深色模式依靠亮度区分层级:越靠近用户的 UI 元素,背景越亮。
| 层级 | Token | 亮度 | 用途 | 参考色值 |
|---|---|---|---|---|
| L0 | --surface-0 | 7% | 页面主背景 | #121212 |
| L1 | --surface-1 | 12% | 侧边栏、卡片 | #1E1E1E |
| L2 | --surface-2 | 17% | 次级容器、输入框 | #2B2928 |
| L3 | --surface-3 | 22% | 弹出层、Dropdown | #383635 |
层级间亮度差必须 >= 4-5%,确保肉眼可区分。
边框规范
深色模式下人眼对暗部的感知灵敏度降低,边框需要更高可见度:
| 场景 | 浅色模式 | 深色模式 |
|---|---|---|
| 卡片边框 | border-border/30 ~ /40 | border-border/50 ~ /70 |
| 分隔线 | border-border/20 ~ /30 | border-border/40 ~ /50 |
| 输入框 | border-border/40 | border-border/60 ~ border-border |
WCAG 无障碍
基于 WCAG 2.1 标准。
对比度要求
| 元素类型 | 最低对比度 | 说明 |
|---|---|---|
| 普通文本 (< 18pt) | 4.5:1 | 正文、描述、标签 |
| 大号文本 (>= 18pt 或 14pt 加粗) | 3:1 | 标题 |
| UI 控件(图标、边框) | 3:1 | 输入框边框、图标、徽章 |
| 禁用状态 | 豁免,建议 2.5:1 | 避免完全不可见 |
文本 Token 使用规则
| Token | 亮度 | 允许的背景 | 典型用途 |
|---|---|---|---|
text-foreground (93%) | 最高 | 所有 surface | 主标题、正文 |
text-muted-foreground (65%) | 中 | surface-0, surface-1 | 次要文本、描述 |
text-text-subtle (50%) | 低 | 仅 surface-0 | 时间戳、元数据 |
{/* 好:描述文字使用 text-muted */}
<p className="text-muted-foreground">共 5 个模型</p>
{/* 不好:在 surface-1 卡片内使用 text-subtle(对比度不足) */}
<div className="bg-surface-1">
<span className="text-text-subtle">看不清</span>
</div>
颜色传达信息
不要仅依赖颜色传达状态,必须同时配有文字提示或图标:
{/* 不好:仅靠颜色 */}
<div className={status === 'error' ? 'border-red-500' : 'border-border'} />
{/* 好:颜色 + 图标 + 文字 */}
<div className={status === 'error' ? 'border-destructive' : 'border-border'}>
{status === 'error' && <AlertCircle className="text-destructive" />}
<span>{errorMessage}</span>
</div>
焦点状态
使用 focus-visible 为键盘导航用户提供焦点边框:
{/* 好 */}
<button className="focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none">
按钮
</button>
{/* 禁止:移除焦点样式 */}
<button className="outline-none focus:outline-none">按钮</button>
动画偏好
尊重系统的 prefers-reduced-motion 设置:
<div className="motion-safe:animate-fadeIn motion-reduce:animate-none">
内容
</div>
UI 组件规范
禁止使用原生控件
| 原生控件 | 项目组件 | 路径 |
|---|---|---|
<select> | Select | @/components/ui/select |
<input type="text"> | Input | @/components/ui/input |
<input type="checkbox"> | Switch / Checkbox | @/components/ui/switch |
<button> | Button | @/components/ui/button |
window.confirm() | ConfirmDialog | @/components/ui/confirm-dialog |
window.alert() | Toast 组件 | - |
下拉组件
所有下拉组件应支持:
- 视口感知定位(使用
useDropdownPositionHook) - 点击外部区域关闭
- Escape 键关闭
- 项目主题色和样式
壁纸透明度系统
当用户设置壁纸时,body 添加 data-wallpaper-active="true" 属性,触发 CSS 透明度规则。
CSS 规则层级
| 优先级 | 选择器 | 效果 | 用途 |
|---|---|---|---|
| 1 | .bg-background, .bg-surface-0 | 全透明 | 页面主背景 |
| 2 | .wallpaper-blur | 35% 不透明 + blur | 主容器(WorkspaceShell) |
| 3 | .wallpaper-blur .wallpaper-blur | 透明 + blur*0.67 | 嵌套容器(避免叠加) |
| 4 | bg-surface-0/XX 在 .wallpaper-blur 内 | 透明 | 布局面板 |
| 5 | bg-surface-1/XX 在 .wallpaper-blur 内 | 12% + blur | 内容卡片(alpha 变体) |
| 6 | bg-surface-1, bg-popover | 15% + blur | 按钮/卡片(排除 input) |
| 7 | bg-surface-2 | 20% + blur | 次级容器(排除 input) |
| 8 | .wallpaper-panel | 85% + blur*1.33 | 浮动下拉/菜单 |
| 9 | .wallpaper-solid | 不透明 surface-0 | Dialog/Modal |
层级叠加模型
WorkspaceShell (wallpaper-blur, 35%)
+-- Sidebar (bg-surface-0/75 -> transparent) = 35%
+-- Main content (bg-background -> transparent) = 35%
| +-- 内容卡片 (bg-surface-1/80 -> 12%) ~ 43%
| +-- 按钮 (bg-surface-1 -> 15%) ~ 45%
| +-- 输入框 (input -> 实色) = 不透明
| +-- 下拉面板 (wallpaper-panel -> 85%) = 85%
+-- Bottombar (wallpaper-blur -> transparent) = 35%
壁纸 CSS 类使用指南
| 场景 | 推荐做法 |
|---|---|
| 页面主容器 | bg-background 或 bg-surface-0(自动全透明) |
| 按钮/卡片 | bg-surface-1 或 bg-surface-1/80(自动半透明) |
| 次级容器 | bg-surface-2(自动 20% 半透明) |
| 主布局容器 | 添加 wallpaper-blur 类 |
| 浮动下拉/菜单 | 添加 wallpaper-panel 类 |
| Dialog/Modal | 添加 wallpaper-solid 类 |
| 输入框 | <input> / <textarea> + bg-surface-1(自动排除,保持实色) |
已内置壁纸支持的组件
半透明毛玻璃 (wallpaper-panel):
Select下拉面板DropdownMenuContent/DropdownMenuSubContentContextMenuContent/ContextMenuSubContent
完全不透明 (wallpaper-solid):
DialogContent
禁止事项
- 禁止使用裸
bg-surface-0作为卡片背景(壁纸模式下会全透明消失) - 禁止使用
bg-white、bg-black、bg-gray-*、bg-neutral-*等硬编码颜色 - 如需不透明效果,请同时添加
wallpaper-solid类
用户可自定义着色层(0.1.11+)
在上述"基础壁纸透明度系统"之上,壁纸面板暴露了三类用户可调的色彩层,每一类都通过 body 数据属性 + CSS 变量 来驱动,CSS 选择器层叠地覆盖默认 surface 表现。所有写入由 themeUtils.applyWallpaperToDocument 集中管理,组件不要直接 body.style.setProperty。
1. 遮罩层(body::before 伪元素)
| 数据属性 | 触发条件 | CSS 变量 |
|---|---|---|
data-wallpaper-active="true" | 任何壁纸源就绪 | --wp-dimming (0–1),--wp-dim-{h,s,l} |
data-wp-dim-gradient="true" | wallpaperDimmingGradient 已设 | --wp-dim-gradient(覆盖 HSL) |
亮度 fallback:未设 --wp-dim-l 时,浅色模式默认 100%、深色模式默认 0%(对应原来的 white/black 行为)。
2. 元素表面层(侧边栏 / 卡片 / 标签 / 上下文菜单)
| 数据属性 | 触发条件 | CSS 变量 |
|---|---|---|
data-wp-element-tint="true" | wallpaperElementTint 是有效 hex | --wp-elem-{h,s,l} |
data-wp-element-gradient="true" | wallpaperElementGradient 已设 | --wp-elem-gradient-{15,20,35}(按 surface tier alpha 分版本) |
设计要点:每个 surface tier 保留独立 alpha(surface-1 = 15%、surface-2 = 20%、.wallpaper-card = 35%),所以即使整体改成同一色调,视觉层级依然可分辨。Input/Textarea 和 wallpaper-solid / wallpaper-panel 选择器都被 :not(...) 排除——可读性优先于色彩一致性。
3. 消息气泡层(用户 / 助手独立)
| 数据属性 | 触发条件 | CSS 变量 |
|---|---|---|
data-wp-bubble-override="true" | wallpaperBubbleOverride === true | --wp-bubble-alpha (0–1) |
data-wp-bubble-tint-user="true" | 用户气泡 hex 有效 | --wp-bubble-user-{h,s,l} |
data-wp-bubble-tint-assistant="true" | 助手气泡 hex 有效 | --wp-bubble-asst-{h,s,l} |
data-wp-bubble-gradient-{user,assistant}="true" | 对应渐变已设 | --wp-bubble-{user,asst}-gradient |
CSS 选择器是分两层级联的:
/* 第 1 层:override 关闭时,气泡跟随元素 tint(继承) */
body[data-wp-element-tint="true"]:not([data-wp-bubble-override="true"]) .chat-bubble-user,
body[data-wp-element-tint="true"]:not([data-wp-bubble-override="true"]) .chat-bubble-assistant {
background: hsl(var(--wp-elem-h) var(--wp-elem-s) var(--wp-elem-l) / var(--wp-alpha-35, 0.35)) !important;
}
/* 第 2 层:override 开启时,per-side tint + 自定义 alpha 生效 */
body[data-wp-bubble-override="true"][data-wp-bubble-tint-user="true"] .chat-bubble-user {
background: hsl(var(--wp-bubble-user-h) var(--wp-bubble-user-s) var(--wp-bubble-user-l) / var(--wp-bubble-alpha, var(--wp-alpha-35, 0.35))) !important;
}
第 2 层选择器具有更高特异性 + 写在后面,因此 !important 平局时会胜出。
添加新的 user-tint 字段时
- 在
ThemePreferences(settings-types.ts)和ThemePreferencesSchema(configSchema.ts)同时加字段 - 在
ThemeService.setWallpaperPreferences增加 setter 分支 +readPreferences/importProfile/resetTheme/mergePreferences默认值 - 在
ThemeRouter的两处 Zod schema(themeProfileSchema与theme:setWallpaperPreferences的 inline schema)都增加字段 - 在
ThemeContext增加 context value + setWallpaperPreferences 入参 + commitState fallback - 在
themeUtils.applyWallpaperToDocument增加参数 +_prev*缓存 + body 属性/CSS 变量写入 - 在
WallpaperPanel增加 UI(开关/取色盘/滑杆) + i18n 三语 - 在
index.css增加选择器(注意层级顺序与!important优先级) - 透传链:
AppearanceTab→Settings.tsx/ThemeStudioPage.tsx(含 DraftState + effective + Apply 提交) - Agent stubs:
desktop-api.ts(preload 契约)、shared/agent/types/settings.ts(共享接口)、shared/agent/web/theme.ts(HTTP 实现) - 更新本表 +
architecture-indexSKILL.md 的字段速查表 +ipc-channels.md的theme:setWallpaperPreferences字段表 +appearance.md用户文档
主题兼容开发规则
只使用语义化 Token
禁止直接写 #fff/rgb() 或 Tailwind 默认色值。
统一使用 ThemeContext
组件需要主题信息时通过 useTheme() 读取。
尊重可配置字体
文本/代码区域使用 CSS 变量:
<div style={{ fontFamily: 'var(--font-ui)' }}>普通文本</div>
<code style={{ fontFamily: 'var(--font-code)' }}>代码</code>
{/* 或使用 Tailwind 类 */}
<div className="font-ui">普通文本</div>
<code className="font-code">代码</code>
允许 customCss 覆盖
避免 !important 和大段内联样式,优先使用 className + CSS 变量。
自测检查清单
创建新 UI 组件时:
- 只使用语义化 Token(不使用硬编码颜色)
- 通过
useTheme()获取主题信息 - 测试深色/浅色模式切换
- 测试壁纸透明度效果
- 普通文本对比度 >= 4.5:1
- 深色模式边框使用
dark:border-border/50以上 -
text-subtle仅用于surface-0背景 - 交互元素使用语义标签或添加
role+tabIndex - 焦点样式使用
focus-visible:ring-2 - 屏幕亮度 30% 下文字和边框仍可辨认
- 窗口缩放至 200% 时布局不破裂