OpenProgram / Web UI

主题是一份完整数据,组件只读取语义 token

回答三个问题:主题由哪里选择和持久化;一次设置会影响哪些界面;怎样保证新增或修改主题时不会只改变部分组件、部分窗口或部分状态。

受众:Web / Desktop 实现者状态:当前规范范围:颜色、字体、跨窗口主题

设计边界

唯一运行时仍是 CSS custom properties。 不引入 JS theme object、CSS-in-JS 或第二套组件皮肤。内置主题必须提供相同 token 集;组件不得按 darklight 等具体主题名写分支。

包含

  • Settings 中的 Appearance 与 Font
  • 首帧、React 水合后及系统明暗变化
  • 主窗口、桌面浮层和新建窗口
  • 表面、文字、边框、状态、focus、composer 和 DAG

不包含

  • 改变组件尺寸、布局或交互逻辑
  • 自动改写用户的 Custom CSS
  • 以主题名为条件的业务逻辑
  • 把语法高亮或网站内容强制套用应用主题

设置如何影响组件

1 · 选择

Settings

THEME_PREFS 生成主题卡片;字体选项来自 font-pref.ts

2 · 持久化

Browser preference

agentic_theme 写入 localStorage;字体同时维护 cookie 以保证首帧。

3 · 应用

HTML attributes

首帧脚本和 hook 都只设置 <html data-theme>--font-sans

4 · 解析

完整 token 文件

每个内置主题定义同一 token 契约;:root 只负责未知值和 Custom 的回退。

5 · 消费

全部组件

Tailwind bridge、CSS modules、桌面浮层全部读取 token,不判断具体主题名。

完整 token 契约

主题文件负责“值”;组件负责“用途”。同一用途在所有主题中使用同一个 token 名。

Surface

--bg-primary / secondary / tertiary--bg-input / hover / selected--surface-popover / tooltip

Text

--text-bright / primary--text-secondary / muted--nav-color / hover

Boundary & depth

--border / border-light--border-popover--shadow-* / --scrim-*

Action & status

--theme-accent--theme-accent-fill / fill-hover--accent-green / red / yellow--success-soft / warning-soft / danger-soft

Interaction

--selection-bg / --focus-ring--chip-bg / --chip-ring--meter-fill / --meter-track

Component-owned roles

--composer-surface / shadow / filter--provider-icon-bg--dag-ghost
设置直接状态影响范围禁止
Appearancedata-theme背景、文字、边框、shadow、状态色、tab focus、composer、popover、DAG、desktop overlay组件内写 [data-theme="light"]
Font--font-sansbody、button、input、select、textarea、optgroup;显式 mono 区域除外普通组件重新声明系统字体栈
Custom CSS#user-custom-css同一 token 契约,未覆写项从默认主题回退新增只有 Custom 才认识的组件分支
主题族主题主色作用范围
Neutraldark / light蓝色Default Button、主操作填充、选中态、链接与强调边界
Warmbeige-dark / beige-light橙色 / 珊瑚色同一套组件角色,只替换主题值
Auroraaurora青绿色同一套组件角色,并使用深色 foreground 保证填充对比
Customcustom用户定义未定义时使用基础蓝色回退

五套内置主题共享同一组件结构

New chatBrowserTerminal
同一 tab、同一文字层级、同一按钮用途主题只替换 token 值,不替换组件结构或行为。

实现方案比较

方案采用原因与边界当前缺失能力
CSS custom properties + data-theme采用原生级联覆盖 Web、CSS modules、Tailwind arbitrary values 和桌面同源浮层。CSS 本身不能验证每个主题是否漏 token,因此增加静态契约检查。
Tailwind @theme作为 bridge只把 CSS token 暴露给 utility 名,不保存另一份色值。不能动态生成 CSS import,主题文件与 TS id 由检查脚本对齐。
JavaScript theme object / Context拒绝会复制色值且无法自然覆盖原生 CSS、首帧和 Electron overlay。不提供运行时逐组件主题 props。
组件级 [data-theme] 分支拒绝新增主题时必然漏改;改为主题文件提供组件角色 token。组件不得知道内置主题 id。
light-dark() 仅按 color-scheme拒绝只能表达明暗两类,不能区分 beige、neutral、aurora 和 custom。仍使用各主题显式值。

规范依据:CSS Custom Properties Level 1CSS Color Adjustment 的 color-scheme。现有实现入口见 web/lib/prefs/theme-pref.tsweb/app/globals.cssweb/app/styles/themes/

验收标准

  • 主题列表设置页、CSS imports、主题文件、桌面浮层允许值由检查脚本证明一致。
  • Token 完整五个内置主题提供相同 token 集;缺一个即失败。
  • 组件隔离theme 文件之外不存在按具体内置主题名分支的组件 CSS。
  • 跨窗口一致beige-dark、beige-light、dark、light、aurora、custom 都原样传给 desktop overlay。
  • 字体一致普通文本和原生表单控件继承同一 --font-sans;mono 区域显式例外。
  • 主色矩阵Neutral 深浅主题为蓝色,Warm 深浅主题为橙色,Aurora 为青绿色;常态、填充与 hover 必须属于同一主题色族。
  • 可见验收默认安装版在深色、浅色和 aurora 下检查主窗口、tab、composer、popover 与桌面浮层。

实现状态

设计与代码实施已完成。

THEME_PREFS 现在是主题入口的唯一清单;五套内置主题各自完整定义 58 个契约 token;composer、provider icon 与 DAG 只消费角色 token,不再判断具体主题 ID;desktop menu overlay 接受并传播全部主题 ID。共享按钮只消费 --primary,由主题层统一映射主色,不在组件内维护主题分支。主题契约检查已纳入 Web 总检查,安装版验收结果记录在本节下方。

  • npm --prefix web run check:通过,包括 check:theme-contract
  • web/node_modules/.bin/tsc -p web/tsconfig.json --noEmit:通过。
  • npm --prefix web run build:通过。
  • 默认安装版 /Applications/OpenProgram.app:五套内置主题均读取到完整 58 个 token,页面背景均按主题变化,原生按钮继承字体设置;Aurora 下 desktop main-menu overlay 收到 theme=aurora 并使用对应 token。
  • 默认安装版共享按钮实测:dark / light 使用蓝色,beige-dark / beige-light 使用橙色,aurora 使用青绿色;常态、填充、hover 均保持各自主题色族。Custom 主题原有 --accent-orange--accent-fill--accent-orange-hover 覆盖方式继续有效。