fix(plugin): prevent theme flash in embedded workbenches
This commit is contained in:
4 files changed
+49
-4
No files matched your search
@@ -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();
|
||||
|
||||
@@ -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("<html><head></head><body></body></html>", [], {
|
||||
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();
|
||||
|
||||
@@ -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 = `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline' blob:; style-src 'unsafe-inline' blob:; img-src data: blob:; font-src data: blob:; ${connectSrc} media-src data: blob:;">`;
|
||||
const sdk = `<script>${pluginSdkSource()}</script>`;
|
||||
const uiKit = `<style>${pluginUiKitCss()}</style>`;
|
||||
const injection = `${csp}${uiKit}${sdk}`;
|
||||
const themeBootstrap = pluginThemeBootstrap(theme);
|
||||
const injection = `${csp}${uiKit}${themeBootstrap}${sdk}`;
|
||||
if (/<head(?:\s[^>]*)?>/i.test(html)) return html.replace(/<head(?:\s[^>]*)?>/i, (head) => `${head}${injection}`);
|
||||
return `<!doctype html><html><head>${injection}</head><body>${html}</body></html>`;
|
||||
}
|
||||
|
||||
function pluginThemeBootstrap(theme?: PluginBridgeTheme): string {
|
||||
if (!theme) return "";
|
||||
const serializedTheme = JSON.stringify(theme).replace(/</g, "\\u003c");
|
||||
return `<script>(() => { 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); } })();</script>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
Reference in new issue
Block a user