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

+27 -1
Переглянути файл
@@ -1,6 +1,6 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { act, fireEvent, render, screen, within } from "@testing-library/react";
import type { Settings } from "@/platform/ipc";
import { commands, type Settings } from "@/platform/ipc";
const settingsSet = vi.fn();
const settingsGet = vi.fn();
@@ -282,3 +282,29 @@ describe("Settings dialog", () => {
expect(within(dialog).getByLabelText("UI font size")).toHaveValue("13");
});
});
describe("native Settings window background", () => {
it.each([
["a hex frame", "#26282c", "", "#26282c"],
["an hsl() frame", "hsl(220, 13%, 15%)", "rgb(33, 37, 43)", "#21252b"],
[
"a translucent color-mix() frame",
"color-mix(in srgb, #8040bf 80%, transparent)",
"color(srgb 0.5 0.25 0.75 / 0.8)",
"#8040bfcc",
],
])("sends %s as hex, the colour format the window command parses", async (_name, token, resolved, hex) => {
const native = vi.spyOn(commands, "windowOpenSettings").mockResolvedValue({ status: "ok", data: null });
const computed = vi
.spyOn(globalThis, "getComputedStyle")
.mockReturnValue({ getPropertyValue: () => token, color: resolved } as unknown as CSSStyleDeclaration);
try {
await executeAction("file.settings", "appearance");
expect(native).toHaveBeenCalledWith("appearance", false, hex);
expect(useSettingsDialog.getState().open).toBe(false);
} finally {
computed.mockRestore();
native.mockRestore();
}
});
});
+2 -2
Переглянути файл
@@ -17,6 +17,7 @@ import { call, commands, isTauri } from "@/platform/ipc";
import { isPopupOpen, lazyComponent } from "@/ui";
import { loadSettings, useProject } from "@/platform/project";
import { provideSettingsSearchIndex, registerOverlay } from "@/workbench/contributions";
import { windowBackgroundHex } from "@/workbench/theme";
import { LazySettingsDialog, loadBuiltinPages, unloadBuiltinPages } from "./dialog/LazySettingsDialog";
import {
closeSettings,
@@ -85,8 +86,7 @@ function settingsActions(open: (page?: string, newProjects?: boolean) => void):
async function showSettings(page?: string, newProjects = false) {
if (isTauri()) {
try {
const frame = getComputedStyle(document.documentElement).getPropertyValue("--bg-frame").trim();
await call(commands.windowOpenSettings(page ?? null, newProjects, frame || null));
await call(commands.windowOpenSettings(page ?? null, newProjects, windowBackgroundHex()));
return;
} catch (e) {
console.warn("settings window unavailable, using the dialog", e);