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
Цей коміт міститься в:
l0ng-aiтаbghtyu authored and GitHub committed 2026-10-27 17:53:07 +08:00
1 джерело 9d8280104f
коміт 24330192c0
43 files changed
+2035 -180

No files matched your search

+44
Переглянути файл
@@ -0,0 +1,44 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { rememberStartupTheme, STARTUP_THEME_KEY, windowBackgroundColor, windowBackgroundHex } from "./theme";
const mockFrame = (token: string, resolved = "") =>
vi
.spyOn(globalThis, "getComputedStyle")
.mockReturnValue({ getPropertyValue: () => token, color: resolved } as unknown as CSSStyleDeclaration);
afterEach(() => {
vi.restoreAllMocks();
localStorage.clear();
});
describe("window background", () => {
it("resolves the frame to Tauri's RGBA tuple instead of passing CSS hsl() or mixes", () => {
mockFrame("#26282c");
expect(windowBackgroundColor()).toEqual([38, 40, 44, 255]);
mockFrame("hsl(230, 1%, 92%)", "rgb(234, 234, 235)");
expect(windowBackgroundColor()).toEqual([234, 234, 235, 255]);
expect(windowBackgroundHex()).toBe("#eaeaeb");
mockFrame("color-mix(in srgb, #8040bf 80%, transparent)", "color(srgb 0.5 0.25 0.75 / 0.8)");
expect(windowBackgroundColor()).toEqual([128, 64, 191, 204]);
expect(windowBackgroundHex()).toBe("#8040bfcc");
mockFrame("", "");
expect(windowBackgroundColor()).toBeNull();
expect(windowBackgroundHex()).toBeNull();
});
it("remembers the base and the resolved splash colours for the next window", () => {
const root = document.documentElement;
root.dataset.theme = "light";
const values: Record<string, string> = { "--bg-frame": "#eee8d5", "--fg-muted": "#657b83", "--accent": "#268bd2" };
vi.spyOn(globalThis, "getComputedStyle").mockReturnValue({
getPropertyValue: (n: string) => values[n] ?? "",
} as unknown as CSSStyleDeclaration);
rememberStartupTheme(root);
expect(JSON.parse(localStorage.getItem(STARTUP_THEME_KEY)!)).toEqual({
theme: "light",
frame: "#eee8d5",
fg: "#657b83",
accent: "#268bd2",
});
});
});
+54 -9
Переглянути файл
@@ -4,6 +4,7 @@ import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import { create } from "zustand";
import { call, commands, isTauri } from "@/platform/ipc";
import { useProject } from "@/platform/project";
import { resolveTokenColor, toHex } from "@/platform/cssColor";
export type ThemeSetting = "dark" | "light" | "system" | "dark-hc";
/** PyCharm's brand blue: the accent both Islands themes ship with. */
@@ -64,7 +65,6 @@ export function useApplyTheme() {
const id = override
? resolveThemeId({ theme: override }, systemLight)
: resolveThemeId(appearance as AppearanceLike | undefined, systemLight);
const theme = id === "light" ? "light" : "dark";
const accent = appearance?.accent;
// Until settings arrive, keep the theme public/boot.js restored rather than the light default.
@@ -77,13 +77,8 @@ export function useApplyTheme() {
const custom = !!accent && ACCENT_RE.test(accent) && !THEME_ACCENTS.has(accent.toLowerCase());
if (custom) root.style.setProperty("--accent", accent);
else root.style.removeProperty("--accent");
try {
localStorage.setItem("openidea.theme", JSON.stringify({ theme, contrast: root.dataset.contrast }));
} catch {
/* storage unavailable */
}
syncWindowBackground();
}, [pending, theme, id, accent]);
}, [pending, id, accent]);
}
/**
@@ -102,14 +97,64 @@ export function applyThemeAttributes(id: "dark" | "light" | "dark-hc", root: HTM
else delete root.dataset.contrast;
}
/**
* The frame colour as Tauri's `[r, g, b, a]` (0–255). Tauri parses neither
* `hsl()` nor `color-mix()`, which themes may use, so it is resolved here.
*/
export function windowBackgroundColor(
root: HTMLElement = document.documentElement,
): [number, number, number, number] | null {
const c = resolveTokenColor("--bg-frame", root);
if (!c) return null;
const [r, g, b, a] = c;
return [Math.round(r), Math.round(g), Math.round(b), Math.round(a * 255)];
}
/** The frame colour as `#rrggbb[aa]`, for native window commands that take a string. */
export function windowBackgroundHex(root: HTMLElement = document.documentElement): string | null {
const c = resolveTokenColor("--bg-frame", root);
return c ? toHex(c) : null;
}
/** localStorage key read by public/boot.js. */
export const STARTUP_THEME_KEY = "openidea.theme";
/**
* Remembers the theme for public/boot.js, which applies it to the next window
* before any stylesheet loads: the light / dark base, and the resolved frame,
* text and accent colours its splash is painted in.
*/
export function rememberStartupTheme(root: HTMLElement = document.documentElement) {
const hex = (name: string) => {
const c = resolveTokenColor(name, root);
return c ? toHex(c) : undefined;
};
try {
localStorage.setItem(
STARTUP_THEME_KEY,
JSON.stringify({
theme: root.dataset.theme,
contrast: root.dataset.contrast,
frame: hex("--bg-frame"),
fg: hex("--fg-muted"),
accent: hex("--accent"),
}),
);
} catch {
/* storage unavailable */
}
}
/**
* Where a live resize outruns the page, the native window and the webview (WebView2's default
* background) show their own colours; match both to the frame. The webview one is only set at
* window creation otherwise, so a theme switched later left it light.
* window creation otherwise, so a theme switched later left it light. Also remembers the theme
* for the next window's start-up.
*/
export function syncWindowBackground() {
rememberStartupTheme();
if (!isTauri()) return;
const color = getComputedStyle(document.documentElement).getPropertyValue("--bg-frame").trim();
const color = windowBackgroundColor();
if (!color) return;
getCurrentWebviewWindow()
.setBackgroundColor(color)