дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 11:15:40 +02:00
feat(themes): Solarized and One theme plugins (#92)
Solarized Light/Dark and One Light/Dark ship as theme plugins (plugins/theme-solarized, plugins/theme-one) with complete UI tokens, editor schemes and terminal colours, and the original palettes' MIT notices. Installing a plugin now also copies its LICENSE/NOTICE/COPYING. Theme handling that benefits every plugin theme: - The last plugin theme is restored before first paint (splash, workbench); the native window background follows the theme. - HSL / color-mix() / var() colours are resolved to RGB for native windows, the Settings window and Mermaid. - Mermaid, the terminal and editor schemes follow switches between two themes of the same kind. - An explicitly chosen editor scheme wins over a plugin theme's editor colours. Supersedes #85. Co-authored-by: bghtyu <[email protected]> Claude-Session: https://claude.ai/code/session_01VUYsdXA2nkF5v6zsC93dpn
Цей коміт міститься в:
1 джерело
9d8280104f
коміт
24330192c0
43 files changed
+2035
-180
No files matched your search
@@ -144,7 +144,7 @@ describe("store", () => {
|
||||
|
||||
it("selects and clears a plugin theme", async () => {
|
||||
await refreshPlugins();
|
||||
await selectPluginTheme("theme-solarized-dark", "solarized-dark");
|
||||
await selectPluginTheme("theme-solarized", "solarized-dark");
|
||||
expect(usePlugins.getState().activeTheme?.theme.id).toBe("solarized-dark");
|
||||
await selectPluginTheme(null, null);
|
||||
expect(usePlugins.getState().activeTheme).toBeNull();
|
||||
@@ -238,7 +238,7 @@ describe("PluginsPage", () => {
|
||||
await waitFor(() => expect(screen.getByRole("checkbox", { name: "Enable Hello" })).toBeInTheDocument());
|
||||
expect(screen.getByRole("button", { name: "Run Hello: Insert Date" })).toBeDisabled();
|
||||
|
||||
await user.click(within(list).getByText("Solarized Dark Theme"));
|
||||
await user.click(within(list).getByText("Solarized Themes"));
|
||||
await user.click(screen.getByRole("button", { name: "Apply theme Solarized Dark" }));
|
||||
await waitFor(() => expect(screen.getByRole("button", { name: "Use Default Theme" })).toBeInTheDocument());
|
||||
|
||||
@@ -309,7 +309,7 @@ describe("catalog helpers", () => {
|
||||
expect(catalogState(entry, [plugin({ id: "hello", version: "0.0.9" })])).toBe("update");
|
||||
expect(catalogState(entry, [plugin({ id: "hello", version: "0.1.0" })])).toBe("installed");
|
||||
expect(catalogState(entry, [plugin({ id: "hello", source: "dev" })])).toBe("dev");
|
||||
expect(filterCatalog(sampleCatalog(), "solarized").map((c) => c.id)).toEqual(["theme-solarized-dark"]);
|
||||
expect(filterCatalog(sampleCatalog(), "solarized").map((c) => c.id)).toEqual(["theme-solarized"]);
|
||||
});
|
||||
|
||||
it("disables and re-enables all downloaded plugins, leaving dev plugins alone", async () => {
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ActiveTheme } from "@/bindings";
|
||||
import { STARTUP_THEME_KEY, syncWindowBackground } from "@/workbench/theme";
|
||||
import { createMemoryPluginsBackend, setPluginsBackend, type PluginsBackend } from "../backend";
|
||||
import {
|
||||
applyPluginTheme,
|
||||
PLUGIN_THEME_CACHE_KEY,
|
||||
PLUGIN_THEME_STYLE_ID,
|
||||
trackPluginTheme,
|
||||
type CachedPluginTheme,
|
||||
} from "../bridge";
|
||||
import { usePlugins } from "../store";
|
||||
|
||||
const BOOT_JS = readFileSync("public/boot.js", "utf8");
|
||||
/** Runs public/boot.js as the next window would, before any app code. */
|
||||
const boot = () => new Function(BOOT_JS)();
|
||||
|
||||
const ONE_DARK: ActiveTheme = {
|
||||
pluginId: "theme-one",
|
||||
theme: {
|
||||
id: "one-dark",
|
||||
name: "One Dark",
|
||||
kind: "dark",
|
||||
tokens: {
|
||||
"--bg-frame": "hsl(220, 13%, 15%)",
|
||||
"--fg-muted": "hsl(220, 9%, 55%)",
|
||||
"--accent": "hsl(220, 100%, 66%)",
|
||||
},
|
||||
},
|
||||
};
|
||||
const SOLARIZED_LIGHT: ActiveTheme = {
|
||||
pluginId: "theme-solarized",
|
||||
theme: { id: "solarized-light", name: "Solarized Light", kind: "light", tokens: { "--bg-frame": "#eee8d5" } },
|
||||
};
|
||||
|
||||
/** Computed style as a browser would resolve the active plugin theme. */
|
||||
function computedColors(values: Record<string, string>) {
|
||||
return vi.spyOn(globalThis, "getComputedStyle").mockImplementation(
|
||||
() =>
|
||||
({
|
||||
getPropertyValue: (name: string) => values[name] ?? "",
|
||||
color: "",
|
||||
}) as unknown as CSSStyleDeclaration,
|
||||
);
|
||||
}
|
||||
|
||||
function resetRoot() {
|
||||
const root = document.documentElement;
|
||||
for (const key of Object.keys(root.dataset)) delete root.dataset[key];
|
||||
document.getElementById(PLUGIN_THEME_STYLE_ID)?.remove();
|
||||
document.getElementById("openidea-boot-colors")?.remove();
|
||||
}
|
||||
|
||||
let backend: PluginsBackend;
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
resetRoot();
|
||||
backend = createMemoryPluginsBackend();
|
||||
setPluginsBackend(backend);
|
||||
usePlugins.setState({ plugins: [], activeTheme: null, loaded: false, error: null });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
applyPluginTheme(null);
|
||||
setPluginsBackend(null);
|
||||
localStorage.clear();
|
||||
resetRoot();
|
||||
});
|
||||
|
||||
describe("plugin theme at start-up", () => {
|
||||
it("restores the last plugin theme and splash colours before the app loads", () => {
|
||||
const style = computedColors({ "--bg-frame": "#21252b", "--fg-muted": "#828997", "--accent": "#5c8aff" });
|
||||
const stop = trackPluginTheme();
|
||||
usePlugins.setState({ loaded: true, activeTheme: ONE_DARK });
|
||||
syncWindowBackground();
|
||||
stop();
|
||||
style.mockRestore();
|
||||
resetRoot();
|
||||
|
||||
boot();
|
||||
const root = document.documentElement;
|
||||
expect(root.dataset.pluginTheme).toBe("theme-one/one-dark");
|
||||
expect(root.dataset.pluginThemeKind).toBe("dark");
|
||||
expect(root.dataset.theme).toBe("dark");
|
||||
expect(document.getElementById(PLUGIN_THEME_STYLE_ID)?.textContent).toContain("--bg-frame: hsl(220, 13%, 15%);");
|
||||
expect(document.getElementById("openidea-boot-colors")?.textContent).toBe(
|
||||
":root { --boot-frame: #21252b; --boot-fg: #828997; --boot-accent: #5c8aff; }",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps the restored theme until the plugin list arrives, then follows it", () => {
|
||||
const cached: CachedPluginTheme = {
|
||||
id: "theme-one/one-dark",
|
||||
kind: "dark",
|
||||
css: ':root[data-plugin-theme]:not([data-plugin-theme=""]) {\n --bg-frame: #21252b;\n}\n',
|
||||
};
|
||||
localStorage.setItem(PLUGIN_THEME_CACHE_KEY, JSON.stringify(cached));
|
||||
boot();
|
||||
const root = document.documentElement;
|
||||
const stop = trackPluginTheme();
|
||||
// Nothing listed yet: the boot theme stays.
|
||||
expect(root.dataset.pluginTheme).toBe("theme-one/one-dark");
|
||||
expect(document.getElementById(PLUGIN_THEME_STYLE_ID)).not.toBeNull();
|
||||
// A failed listing doesn't drop it either.
|
||||
usePlugins.setState({ loaded: true, error: "backend unavailable" });
|
||||
expect(root.dataset.pluginTheme).toBe("theme-one/one-dark");
|
||||
|
||||
usePlugins.setState({ error: null, activeTheme: SOLARIZED_LIGHT });
|
||||
expect(root.dataset.pluginTheme).toBe("theme-solarized/solarized-light");
|
||||
expect(root.dataset.theme).toBe("light");
|
||||
expect(JSON.parse(localStorage.getItem(PLUGIN_THEME_CACHE_KEY)!)).toMatchObject({
|
||||
id: "theme-solarized/solarized-light",
|
||||
kind: "light",
|
||||
});
|
||||
|
||||
// The plugin was disabled or uninstalled meanwhile.
|
||||
usePlugins.setState({ activeTheme: null });
|
||||
expect(root.dataset.pluginTheme).toBeUndefined();
|
||||
expect(document.getElementById(PLUGIN_THEME_STYLE_ID)).toBeNull();
|
||||
expect(localStorage.getItem(PLUGIN_THEME_CACHE_KEY)).toBeNull();
|
||||
stop();
|
||||
});
|
||||
|
||||
it("ignores a cached stylesheet that could load resources", () => {
|
||||
localStorage.setItem(
|
||||
PLUGIN_THEME_CACHE_KEY,
|
||||
JSON.stringify({ id: "p/t", kind: "dark", css: ":root { --bg-app: url(https://example.com/x) }" }),
|
||||
);
|
||||
localStorage.setItem(STARTUP_THEME_KEY, JSON.stringify({ theme: "light", frame: "red; color: blue" }));
|
||||
boot();
|
||||
expect(document.documentElement.dataset.pluginTheme).toBeUndefined();
|
||||
expect(document.documentElement.dataset.theme).toBe("light");
|
||||
expect(document.getElementById(PLUGIN_THEME_STYLE_ID)).toBeNull();
|
||||
expect(document.getElementById("openidea-boot-colors")).toBeNull();
|
||||
});
|
||||
|
||||
it("reports the resolved frame colour once per selected theme", () => {
|
||||
const setFrame = vi.spyOn(backend, "setThemeFrame");
|
||||
computedColors({ "--bg-frame": "rgb(33, 37, 43)" });
|
||||
const stop = trackPluginTheme();
|
||||
usePlugins.setState({ loaded: true, activeTheme: ONE_DARK });
|
||||
usePlugins.setState({ activeTheme: { ...ONE_DARK } });
|
||||
expect(setFrame.mock.calls).toEqual([["#21252b"]]);
|
||||
// Selecting it again after the default theme reports it again: the backend forgot it.
|
||||
usePlugins.setState({ activeTheme: null });
|
||||
usePlugins.setState({ activeTheme: ONE_DARK });
|
||||
expect(setFrame).toHaveBeenCalledTimes(2);
|
||||
stop();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Заблокувати користувача