diff --git a/MiniApp/Demo/git-graph/source/build.js b/MiniApp/Demo/git-graph/source/build.js index 846ef1c08a..7a3f49b111 100644 --- a/MiniApp/Demo/git-graph/source/build.js +++ b/MiniApp/Demo/git-graph/source/build.js @@ -10,7 +10,7 @@ const ROOT = path.dirname(SOURCE_DIR); const UI_ORDER = [ 'ui/state.js', - 'ui/theme.js', + 'ui/appearance.js', 'ui/graph/layout.js', 'ui/graph/renderRowSvg.js', 'ui/services/gitClient.js', diff --git a/MiniApp/Demo/git-graph/source/ui.js b/MiniApp/Demo/git-graph/source/ui.js index c751a603d0..cf7c1802b5 100644 --- a/MiniApp/Demo/git-graph/source/ui.js +++ b/MiniApp/Demo/git-graph/source/ui.js @@ -113,9 +113,9 @@ })(); -/* ui/theme.js */ +/* ui/appearance.js */ /** - * Git Graph MiniApp — theme adapter: read --branch-* and node stroke from CSS for graph colors. + * Git Graph MiniApp — appearance adapter: read --branch-* and node stroke from CSS for graph colors. */ (function () { window.__GG = window.__GG || {}; @@ -125,7 +125,7 @@ return getComputedStyle(root).getPropertyValue(name).trim() || null; } - /** Returns array of 7 branch/lane colors from CSS variables (theme-aware). */ + /** Returns array of 7 branch/lane colors from CSS variables (appearance-aware). */ window.__GG.getGraphColors = function () { const colors = []; for (let i = 1; i <= 7; i++) { @@ -434,7 +434,7 @@ /* ui/graph/renderRowSvg.js */ /** - * Git Graph MiniApp — build SVG for one commit row (theme-aware colors). + * Git Graph MiniApp — build SVG for one commit row (appearance-aware colors). */ (function () { window.__GG = window.__GG || {}; @@ -1787,7 +1787,7 @@ /* ui/bootstrap.js */ /** - * Git Graph MiniApp — bootstrap: bind events, init resizer, restore last repo, theme subscription. + * Git Graph MiniApp — bootstrap: bind events, init resizer, restore last repo, appearance subscription. */ (function () { window.__GG = window.__GG || {}; @@ -1843,8 +1843,8 @@ }); } - if (window.app && typeof window.app.onThemeChange === 'function') { - window.app.onThemeChange(function () { + if (window.app && typeof window.app.onAppearanceChange === 'function') { + window.app.onAppearanceChange(function () { if (state.cwd && $('commit-list').children.length) { window.__GG.renderCommitList(); } diff --git a/MiniApp/Demo/git-graph/source/ui/theme.js b/MiniApp/Demo/git-graph/source/ui/appearance.js similarity index 87% rename from MiniApp/Demo/git-graph/source/ui/theme.js rename to MiniApp/Demo/git-graph/source/ui/appearance.js index a1e3c479db..239ee2e055 100644 --- a/MiniApp/Demo/git-graph/source/ui/theme.js +++ b/MiniApp/Demo/git-graph/source/ui/appearance.js @@ -1,5 +1,5 @@ /** - * Git Graph MiniApp — theme adapter: read --branch-* and node stroke from CSS for graph colors. + * Git Graph MiniApp — appearance adapter: read --branch-* and node stroke from CSS for graph colors. */ (function () { window.__GG = window.__GG || {}; @@ -9,7 +9,7 @@ return getComputedStyle(root).getPropertyValue(name).trim() || null; } - /** Returns array of 7 branch/lane colors from CSS variables (theme-aware). */ + /** Returns array of 7 branch/lane colors from CSS variables (appearance-aware). */ window.__GG.getGraphColors = function () { const colors = []; for (let i = 1; i <= 7; i++) { diff --git a/MiniApp/Demo/git-graph/source/ui/bootstrap.js b/MiniApp/Demo/git-graph/source/ui/bootstrap.js index 9021c56045..25006645f9 100644 --- a/MiniApp/Demo/git-graph/source/ui/bootstrap.js +++ b/MiniApp/Demo/git-graph/source/ui/bootstrap.js @@ -1,5 +1,5 @@ /** - * Git Graph MiniApp — bootstrap: bind events, init resizer, restore last repo, theme subscription. + * Git Graph MiniApp — bootstrap: bind events, init resizer, restore last repo, appearance subscription. */ (function () { window.__GG = window.__GG || {}; @@ -55,8 +55,8 @@ }); } - if (window.app && typeof window.app.onThemeChange === 'function') { - window.app.onThemeChange(function () { + if (window.app && typeof window.app.onAppearanceChange === 'function') { + window.app.onAppearanceChange(function () { if (state.cwd && $('commit-list').children.length) { window.__GG.renderCommitList(); } diff --git a/MiniApp/Demo/git-graph/source/ui/graph/renderRowSvg.js b/MiniApp/Demo/git-graph/source/ui/graph/renderRowSvg.js index a8da6a3da0..9d1cb5cd0f 100644 --- a/MiniApp/Demo/git-graph/source/ui/graph/renderRowSvg.js +++ b/MiniApp/Demo/git-graph/source/ui/graph/renderRowSvg.js @@ -1,5 +1,5 @@ /** - * Git Graph MiniApp — build SVG for one commit row (theme-aware colors). + * Git Graph MiniApp — build SVG for one commit row (appearance-aware colors). */ (function () { window.__GG = window.__GG || {}; diff --git a/MiniApp/Demo/icon-design-system/source/index.html b/MiniApp/Demo/icon-design-system/source/index.html index 8e3ffc08f1..509bc39a00 100644 --- a/MiniApp/Demo/icon-design-system/source/index.html +++ b/MiniApp/Demo/icon-design-system/source/index.html @@ -1,5 +1,5 @@ - + diff --git a/MiniApp/Skills/miniapp-dev/SKILL.md b/MiniApp/Skills/miniapp-dev/SKILL.md index e98a466cec..8e291f9b4f 100644 --- a/MiniApp/Skills/miniapp-dev/SKILL.md +++ b/MiniApp/Skills/miniapp-dev/SKILL.md @@ -197,7 +197,7 @@ MiniApp 框架**只暴露下列能力**,没有任何"通用 BitFun 后端通 | 悬浮会话气泡 | `app.chat.claimComposer / releaseComposer / focusSession / setComposerDraft / onUserMessage` | 受 `permissions.agent.enabled` 限制;把内容和提交路由注册进右下角的标准悬浮聊天窗(输入器、附件、模型、权限、停止等仍由宿主共享组件拥有),并展示小应用自己的 Agent 过程(Agentic MiniApp 模式,样板间:`builtin-ppt-live`) | | 幻灯片栅格化 | `app.deck.renderPage` | 在隐藏宿主 WebView 中渲染单页 HTML,返回 base64 PNG/PDF(导出用) | | 自定义后端 | `app.call('xxx', …)` + `worker.js` | 仅 `node.enabled = true` 时可用,自己实现业务逻辑 | -| 主题 / i18n | `app.theme` / `app.locale` / `app.onThemeChange` / `app.onLocaleChange` / `app.t(...)` | 见对应章节 | +| 主题 / i18n | `app.appearanceMode` / `app.locale` / `app.onAppearanceChange` / `app.onLocaleChange` / `app.t(...)` | 见对应章节 | ### 框架**不**直接暴露的 BitFun 后端能力(截至本文档) @@ -242,16 +242,16 @@ MiniApp 在 iframe 中运行时自动与主应用主题同步,避免界面风 | 成员 | 说明 | |------|------| -| `app.theme` | 当前主题类型字符串:`'dark'` 或 `'light'`(随主应用切换更新) | -| `app.onThemeChange(fn)` | 注册主题变更回调,参数为 payload:`{ type, id, vars }` | +| `app.appearanceMode` | 当前主题类型字符串:`'dark'` 或 `'light'`(随主应用切换更新) | +| `app.onAppearanceChange(fn)` | 注册主题变更回调,参数为 payload:`{ mode, id, vars }` | -### data-theme-type 属性 +### data-bf-appearance-mode 属性 -编译后的 HTML 根元素 `` 带有 `data-theme-type="dark"` 或 `"light"`,便于用 CSS 按主题写样式,例如: +编译后的 HTML 根元素 `` 带有 `data-bf-appearance-mode="dark"` 或 `"light"`,便于用 CSS 按主题写样式,例如: ```css -[data-theme-type="light"] .panel { background: #f5f5f5; } -[data-theme-type="dark"] .panel { background: #1a1a1a; } +[data-bf-appearance-mode="light"] .panel { background: #f5f5f5; } +[data-bf-appearance-mode="dark"] .panel { background: #1a1a1a; } ``` ### --bitfun-* CSS 变量 @@ -307,8 +307,8 @@ body { ### 同步时机 -- iframe 加载后 bridge 会向宿主发送 `bitfun/request-theme`,宿主回推当前主题变量,iframe 内 `_applyThemeVars` 写入 `:root`。 -- 主应用切换主题时,宿主会向 iframe 发送 `themeChange` 事件,bridge 更新变量并触发 `onThemeChange` 回调。 +- iframe 加载后 bridge 会向宿主发送 `bitfun/request-appearance`,宿主回推当前主题变量,iframe 内 `_applyAppearanceVars` 写入 `:root`。 +- 主应用切换主题时,宿主会向 iframe 发送 `appearanceChange` 事件,bridge 更新变量并触发 `onAppearanceChange` 回调。 ## 国际化(i18n) diff --git a/MiniApp/Skills/miniapp-dev/api-reference.md b/MiniApp/Skills/miniapp-dev/api-reference.md index ba65c17ed1..b62f1c1e20 100644 --- a/MiniApp/Skills/miniapp-dev/api-reference.md +++ b/MiniApp/Skills/miniapp-dev/api-reference.md @@ -17,7 +17,7 @@ MiniApp **能且只能**用以下 API,没有任何"通用 BitFun 后端通道" - `app.dialog.open/save/message` —— 文件对话框 - `app.clipboard.readText/writeText` —— 剪贴板 - `app.call('xxx', ...)` + `worker.js` —— 自定义 Node 后端(仅 `node.enabled = true` 时) -- `app.theme / locale / on*` —— 主题与 i18n +- `app.appearanceMode / locale / on*` —— 主题与 i18n **框架不暴露**的 BitFun 后端能力(截至当前版本):WorkspaceService(结构化搜索 / 索引)、GitService(结构化 status/diff/blame)、TerminalService、Session/AgenticSystem、LSP / Snapshot / Mermaid / Skills / Browser / Computer Use / Config 等。需要这些能力时: @@ -108,7 +108,7 @@ MiniApp 中所有与宿主通信的 API 均通过 `window.app` 暴露。 app.appId // string — 当前 MiniApp 的 ID app.appDataDir // string — 应用数据目录绝对路径 app.workspaceDir // string — 当前工作区路径 -app.theme // 'dark' | 'light' — 当前主题 +app.appearanceMode // 'dark' | 'light' — 当前主题 app.locale // string — 当前语言 ID(如 'zh-CN' / 'en-US'),随宿主切换更新 app.platform // 'win32' | 'darwin' | 'linux' app.mode // 'hosted' @@ -289,8 +289,8 @@ const text = await app.clipboard.readText(); ```javascript app.onActivate(() => { /* Tab 变为活跃状态 */ }); app.onDeactivate(() => { /* Tab 切走 */ }); -app.onThemeChange((payload) => { - // payload: { type: 'dark'|'light', vars: { '--bitfun-bg': '...', ... } } +app.onAppearanceChange((payload) => { + // payload: { mode: 'dark'|'light', vars: { '--bitfun-bg': '...', ... } } }); app.onLocaleChange((locale) => { // locale: 新的语言 ID 字符串(如 'zh-CN' / 'en-US') diff --git a/MiniApp/Skills/miniapp-dev/design-playbook.md b/MiniApp/Skills/miniapp-dev/design-playbook.md index f240fdc020..edad9de8ae 100644 --- a/MiniApp/Skills/miniapp-dev/design-playbook.md +++ b/MiniApp/Skills/miniapp-dev/design-playbook.md @@ -100,7 +100,7 @@ 1. **首选**:直接 `var(--bitfun-*)` 系列,让小应用与宿主主题协同。 2. **fallback**:每个 `var()` 都带 fallback,用于导出为独立应用时仍可用。 -3. **主题区分**:所有颜色都要在 light / dark 各测一次。可以利用 `[data-theme-type="light"]` 选择器做差异化覆写。 +3. **主题区分**:所有颜色都要在 light / dark 各测一次。可以利用 `[data-bf-appearance-mode="light"]` 选择器做差异化覆写。 4. **辅助色板**(仅当用户明确需要"专属配色"时使用,否则默认走主题)——参考下方 10 套从内容出发的配色: | 主题感觉 | 主色 | 辅助 | 强调 | 适合的小应用 | diff --git a/docs/architecture/appearance-package-system.md b/docs/architecture/appearance-package-system.md new file mode 100644 index 0000000000..4f89442307 --- /dev/null +++ b/docs/architecture/appearance-package-system.md @@ -0,0 +1,473 @@ +# BitFun Appearance 外观包系统 + +本文定义 Web UI 的唯一外观运行时。原 `ThemeService + SkinService + DeepSkinComponents.scss` +双系统已经废弃;`bitfun.skin`、`.bitfun-skin`、旧 IndexedDB 数据和旧 manifest 均不兼容、 +不迁移。 + +## 目标 + +Appearance 必须允许外观包高自由度控制所有可见宿主表面,同时保持以下边界: + +- 外观包只提交强类型数据,不提交 CSS、DOM 选择器、HTML、JavaScript 或 React 代码。 +- 外观包不能引用外部 URL,不能携带字体、SVG 或可执行资源;视频只允许作为宿主管理的顶层动态背景。 +- 组件、场景、Portal 和独立渲染器的可定制边界由宿主注册。 +- DOM 选择器与最终 CSS 只由宿主编译器生成。 +- 所有可见表面最终由同一个 `AppearanceRuntime` 快照和 revision 驱动。 +- 激活是事务:CSS、资源和 renderer adapter 要么一起提交,要么回滚到旧快照。 + +## 目录与所有权 + +```text +src/web-ui/src/infrastructure/appearance/ +├── types/ # schema、结构化样式值、resolved snapshot +├── registry/ # component / scene / renderer 宿主契约 +├── schema/ # manifest 校验和 ZIP 解析 +├── compiler/ # token、materials、选择器、CSS、结构与对比度诊断 +├── runtime/ # 唯一快照、revision、事务、OverlayHost、服务 API +├── storage/ # 外观包和 catalog 的 IndexedDB 命名空间 +├── builtins/ # 内置外观 +├── adapters/ # CSS token 与独立渲染器 adapter +└── hooks/ # React 消费入口 +``` + +`AppearanceRuntime` 是唯一视觉 owner。内置外观和导入外观使用同一个 `AppearancePackage` +契约、同一个 compiler、同一个事务提交路径。选择值由正式配置键 `appearance.selection` +持久化;IndexedDB `bitfun-appearance` 只保存导入包和 catalog,不保存当前选择。 + +包可以只声明需要覆盖的字段,但进入 runtime 前必须通过 `composeAppearancePackage` 与同模式的 +完整内置基准组合。compiler 会按“内置基准普通层、导入包覆盖层”分别生成 surface 规则,避免 +导入包的 `cascade: override` 将未声明的基准属性一起提升为 `!important`;renderer、token 和 +materials 仍组成完整快照,因此稀疏包不会把未声明能力清空。 + +当前唯一 manifest 版本仍是 `schemaVersion: 1`,当前宿主 registry 是该版本的唯一权威契约。 +系统不兼容已经删除或改名的 component / scene / part,也不兼容旧的单数 `material` 字段或旧 +material 结构;外观包必须按本文和当前 registry 重新生成。 + +导入和编译失败必须保留 `AppearanceValidationIssue` 的错误码、路径与 surface 上下文,并由产品界面 +按组件、场景或配置区域分组展示;禁止把全部校验问题压平成单行 toast。对于未知 part,诊断同时 +提供该 surface 当前注册的 part 列表,外观包制作工具应据此重新生成 manifest。 + +## 包格式 + +文件扩展名为 `.bitfun-appearance`,内容为 ZIP,根目录必须包含 `appearance.json`: + +```text +aurora.bitfun-appearance +├── appearance.json +└── assets/ + ├── preview.webp + ├── background.webp + ├── background.webm + └── pattern.png +``` + +最小 manifest: + +```json +{ + "schema": "bitfun.appearance", + "schemaVersion": 1, + "id": "example.aurora", + "name": "Aurora", + "version": "1.0.0", + "mode": "dark" +} +``` + +完整顶层结构: + +```json +{ + "schema": "bitfun.appearance", + "schemaVersion": 1, + "id": "example.aurora", + "name": "Aurora", + "author": "Example Studio", + "description": "A device-local visual appearance.", + "version": "1.0.0", + "mode": "dark", + "preview": { "kind": "asset", "assetId": "preview" }, + "backgroundMedia": { + "kind": "video", + "assetId": "background-video", + "posterAssetId": "background-poster", + "fit": "cover", + "position": "center" + }, + "requiredCapabilities": ["components.v1", "scenes.v1", "assets.v1", "background-media.v1"], + "globals": {}, + "materials": {}, + "components": {}, + "scenes": {}, + "renderers": {}, + "assets": {}, + "integrity": { "sha256": {} } +} +``` + +`preview` 是设置页外观包卡片使用的图片资源。未声明时,宿主依次尝试名为 `preview`、 +`background` 的资源,再回退到包内第一张图片;完全没有图片时显示占位图标。预览必须是图片, +不能引用视频。 + +`backgroundMedia` 是唯一的视频入口。它必须引用一个 MP4/WebM 视频资源和一个静态图片 poster, +并声明 `background-media.v1`。宿主在 `AppLayout` 的固定背景层渲染 `