From fa003016ef6c8f00ce276592f27d126017d79980 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Tue, 3 Nov 2026 14:46:46 +0800 Subject: [PATCH] feat(plugins): theme variables for html view frames, kept current without reload - Inject `data-theme` and stable `--openidea-*` colour and font variables (concrete values; the frame has no IDE stylesheet) into html view bodies. - Post theme changes to the loaded frame instead of rebuilding srcdoc, so a theme switch keeps the frame's state; re-send on load to close the race. - Expose `openidea.theme` / `openidea.onThemeChange` to frame scripts. - Move `subscribeTheme` / `themeKey` from the Markdown mermaid module to `src/platform/themeWatch.ts`, shared by both. - Document the variables in plugins/README.md. --- plugins/README.md | 17 +++ src/features/plugins/__tests__/views.test.tsx | 93 +++++++++++++++- src/features/plugins/views/PluginView.tsx | 36 +++++- src/features/plugins/views/frame.ts | 103 ++++++++++++++++-- .../viewers/markdown/MarkdownEditor.tsx | 3 +- src/features/viewers/markdown/mermaid.test.ts | 3 +- src/features/viewers/markdown/mermaid.ts | 21 +--- src/platform/themeWatch.ts | 25 +++++ 8 files changed, 265 insertions(+), 36 deletions(-) create mode 100644 src/platform/themeWatch.ts diff --git a/plugins/README.md b/plugins/README.md index c9d97c5f..4765e421 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -384,6 +384,23 @@ inline scripts and styles and `data:` images, no network, no IDE API — and talks to the plugin only through `openidea.postMessage(text)` / `openidea.onMessage(fn)` (`ViewEvent::Message`, `host::post_view_message`). +The frame follows the IDE theme without reloading: its root carries +`data-theme="light|dark"` and these CSS variables, with concrete colour +values (the body already uses the text colour, UI font and size): + +| Variable | Meaning | +| ----------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | +| `--openidea-bg`, `--openidea-bg-elevated`, `--openidea-bg-hover`, `--openidea-bg-selected`, `--openidea-bg-input` | Surfaces (the frame itself is transparent) | +| `--openidea-fg`, `--openidea-fg-strong`, `--openidea-fg-muted` | Text | +| `--openidea-border`, `--openidea-focus-ring`, `--openidea-link` | Lines, focus, links | +| `--openidea-accent`, `--openidea-fg-on-accent` | Accent and text on it | +| `--openidea-danger`, `--openidea-warning`, `--openidea-success`, `--openidea-info` | Status colours | +| `--openidea-font-ui`, `--openidea-font-mono`, `--openidea-font-size` | Fonts (web fonts the IDE bundles are not available in the frame) | + +Scripts get the same as `openidea.theme` (`{ dark, vars }`) and +`openidea.onThemeChange(fn)`, for drawing on a canvas, say. Variables are only +ever added. + ### Settings ```toml diff --git a/src/features/plugins/__tests__/views.test.tsx b/src/features/plugins/__tests__/views.test.tsx index a78fce47..8d197d15 100644 --- a/src/features/plugins/__tests__/views.test.tsx +++ b/src/features/plugins/__tests__/views.test.tsx @@ -9,7 +9,7 @@ import { defineBuiltinUiPoints } from "../points"; import { clearUiContributions, syncUiContributions } from "../projection"; import { pluginMenuItems, withPluginSetting } from "../uiPoints"; import { matchesPattern } from "../views/customEditor"; -import { FRAME_CSP, frameDocument } from "../views/frame"; +import { FRAME_CSP, FRAME_TAG, frameDocument, frameTheme, type FrameTheme } from "../views/frame"; import { PluginView, treeNodes, wantedViewport } from "../views/PluginView"; import { bumpView } from "../views/viewStore"; @@ -56,7 +56,7 @@ describe("view helpers", () => { }); it("confines HTML bodies: CSP without network, a message bridge", () => { - const doc = frameDocument("
hi
", true); + const doc = frameDocument("hi
", { dark: true, vars: {} }); expect(doc).toContain(`content="${FRAME_CSP}"`); expect(FRAME_CSP).toContain("default-src 'none'"); expect(FRAME_CSP).not.toMatch(/https?:|connect-src/); @@ -64,6 +64,71 @@ describe("view helpers", () => { expect(doc.indexOf("Content-Security-Policy")).toBeLessThan(doc.indexOf("hi
")); }); + it("reads the frame theme from the IDE tokens, resolved and sanitised", () => { + const root = document.documentElement; + const before = root.dataset.theme; + root.style.setProperty("--fg", "#112233"); + root.style.setProperty("--font-ui", "Inter, sans-serif"); + root.style.setProperty("--font-mono", "x}"); + try { + root.dataset.theme = "light"; + const light = frameTheme(); + expect(light.dark).toBe(false); + expect(light.vars["--openidea-fg"]).toBe("rgb(17, 34, 51)"); + expect(light.vars["--openidea-font-ui"]).toBe("Inter, sans-serif"); + expect(light.vars).not.toHaveProperty("--openidea-font-mono"); + root.dataset.theme = "dark"; + root.dataset.contrast = "high"; + expect(frameTheme().dark).toBe(true); + } finally { + for (const p of ["--fg", "--font-ui", "--font-mono"]) root.style.removeProperty(p); + delete root.dataset.contrast; + if (before === undefined) delete root.dataset.theme; + else root.dataset.theme = before; + } + }); + + it("puts the theme into the frame document without letting a value end the script", () => { + const doc = frameDocument("hi
", { dark: true, vars: { "--openidea-fg": "" } }); + expect(doc).toContain(''); + expect(doc).not.toContain(""); + expect(doc).toContain("\\u003c/script>"); + }); + + it("applies posted themes inside the frame, replacing the previous variables", () => { + const doc = frameDocument("", { + dark: false, + vars: { "--openidea-fg": "rgb(1, 2, 3)", "--openidea-accent": "red" }, + }); + const script = /`, + ``, + ``, "", html, "