дзеркало
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
@@ -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",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Заблокувати користувача