From f1cc68a4ba2820056fea89bfec3c5f6a9711db59 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Mon, 14 Sep 2026 14:42:59 +0800 Subject: [PATCH] fix(plugin): prevent theme flash in embedded workbenches --- .../plugins/PluginWorkbenchHost.vue | 2 +- .../src/lib/plugins/pluginHostBridge.spec.ts | 12 ++++++++ .../src/lib/plugins/pluginHostBridge.ts | 11 ++++++-- plugins/sdk/dev-host/ui/App.vue | 28 ++++++++++++++++++- 4 files changed, 49 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/components/plugins/PluginWorkbenchHost.vue b/apps/desktop/src/components/plugins/PluginWorkbenchHost.vue index bf8242238c..3e1b4bb892 100644 --- a/apps/desktop/src/components/plugins/PluginWorkbenchHost.vue +++ b/apps/desktop/src/components/plugins/PluginWorkbenchHost.vue @@ -124,7 +124,7 @@ async function loadWorkbench() { const bytes = Uint8Array.from(atob(asset.dataBase64), (character) => character.charCodeAt(0)); const html = await inlineLocalUiAssets(new TextDecoder().decode(bytes), props.plugin.manifest.id); if (disposed || generation !== loadGeneration) return; - source.value = pluginSandboxDocument(html, props.plugin.manifest.permissions); + source.value = pluginSandboxDocument(html, props.plugin.manifest.permissions, currentBridgeTheme()); await nextTick(); if (disposed || generation !== loadGeneration) return; createBridge(); diff --git a/apps/desktop/src/lib/plugins/pluginHostBridge.spec.ts b/apps/desktop/src/lib/plugins/pluginHostBridge.spec.ts index c851a41fef..c85bf8f726 100644 --- a/apps/desktop/src/lib/plugins/pluginHostBridge.spec.ts +++ b/apps/desktop/src/lib/plugins/pluginHostBridge.spec.ts @@ -297,6 +297,18 @@ describe("PluginHostBridge", () => { expect(document).toContain("var(--color-background"); }); + it("bootstraps the initial theme before the plugin document paints", () => { + const document = pluginSandboxDocument("", [], { + appearance: "dark", + tokens: { "--color-background": "rgb(19 20 22)", "--color-destructive": "rgb(243 98 95)" }, + }); + + expect(document).toContain('root.dataset.dbxTheme = theme.appearance === "dark" ? "dark" : "light"'); + expect(document).toContain('root.style.colorScheme = theme.appearance === "dark" ? "dark" : "light"'); + expect(document).toContain('"--color-background":"rgb(19 20 22)"'); + expect(document).toContain('"--color-destructive":"rgb(243 98 95)"'); + }); + it("forwards the plugin close-tab shortcut to the host", () => { const target = { postMessage: vi.fn() } as unknown as Window; const closeTab = vi.fn(); diff --git a/apps/desktop/src/lib/plugins/pluginHostBridge.ts b/apps/desktop/src/lib/plugins/pluginHostBridge.ts index fd08349d29..61061ba1ba 100644 --- a/apps/desktop/src/lib/plugins/pluginHostBridge.ts +++ b/apps/desktop/src/lib/plugins/pluginHostBridge.ts @@ -217,17 +217,24 @@ export function pluginNetworkOrigins(permissions: readonly string[] | undefined) return [...origins]; } -export function pluginSandboxDocument(html: string, permissions?: readonly string[]): string { +export function pluginSandboxDocument(html: string, permissions?: readonly string[], theme?: PluginBridgeTheme): string { const networkOrigins = pluginNetworkOrigins(permissions); const connectSrc = networkOrigins.length > 0 ? `connect-src ${networkOrigins.join(" ")};` : "connect-src 'none';"; const csp = ``; const sdk = ``; const uiKit = ``; - const injection = `${csp}${uiKit}${sdk}`; + const themeBootstrap = pluginThemeBootstrap(theme); + const injection = `${csp}${uiKit}${themeBootstrap}${sdk}`; if (/]*)?>/i.test(html)) return html.replace(/]*)?>/i, (head) => `${head}${injection}`); return `${injection}${html}`; } +function pluginThemeBootstrap(theme?: PluginBridgeTheme): string { + if (!theme) return ""; + const serializedTheme = JSON.stringify(theme).replace(/(() => { const theme = ${serializedTheme}; const root = document.documentElement; root.dataset.dbxTheme = theme.appearance === "dark" ? "dark" : "light"; root.style.colorScheme = theme.appearance === "dark" ? "dark" : "light"; for (const [name, value] of Object.entries(theme.tokens || {})) { if (/^--[a-z0-9-]+$/i.test(name) && typeof value === "string") root.style.setProperty(name, value); } })();`; +} + /** * Minimal official component kit for plugin workbenches. Every class is built * on the DBX design tokens the host pushes through the bridge, so plugin UI diff --git a/plugins/sdk/dev-host/ui/App.vue b/plugins/sdk/dev-host/ui/App.vue index 909865633b..cea9cb1170 100644 --- a/plugins/sdk/dev-host/ui/App.vue +++ b/plugins/sdk/dev-host/ui/App.vue @@ -93,11 +93,37 @@ const themes = () => ({ ? { "--color-background": "#18181b", "--color-foreground": "#f4f4f5", + "--color-card": "#1b1b1f", + "--color-card-foreground": "#f4f4f5", "--color-muted": "#27272a", + "--color-muted-foreground": "#a1a1aa", "--color-border": "#3f3f46", + "--color-input": "#3f3f46", + "--color-ring": "#71717a", "--color-primary": "#60a5fa", + "--color-primary-foreground": "#18181b", + "--color-destructive": "#f3625f", + "--color-destructive-foreground": "#18181b", + "--radius-md": "6px", + "--radius-lg": "8px", } - : { "--color-background": "#ffffff", "--color-foreground": "#27272a", "--color-muted": "#f4f4f5", "--color-border": "#e4e4e7", "--color-primary": "#2563eb" }, + : { + "--color-background": "#ffffff", + "--color-foreground": "#27272a", + "--color-card": "#ffffff", + "--color-card-foreground": "#27272a", + "--color-muted": "#f4f4f5", + "--color-muted-foreground": "#71717a", + "--color-border": "#e4e4e7", + "--color-input": "#e4e4e7", + "--color-ring": "#93c5fd", + "--color-primary": "#2563eb", + "--color-primary-foreground": "#ffffff", + "--color-destructive": "#e7000b", + "--color-destructive-foreground": "#ffffff", + "--radius-md": "6px", + "--radius-lg": "8px", + }, }); function post(frame, message) { const snapshot = hostMessage(frame.channel, message);