дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 11:15:40 +02:00
feat(themes): Built-in Solarized and One light/dark themes (#85)
Adds Solarized Light, Solarized Dark, One Light and One Dark as built-in themes across the workbench, editor, terminal and Mermaid, keeping the original palettes (MIT notices in THIRD_PARTY_LICENSES.md, mapping in docs/themes.md). - Native window colours are read from tokens.css at build time (`theme_frame.rs`) instead of a second colour table. - Startup restores the built-in palette before first paint. - Editor schemes, the terminal and Mermaid refresh on switches between themes of the same kind. - The plugin packages of the same themes are removed. - A test requires every built-in palette theme to define each token (only listed ones may inherit from the base theme), so new tokens can't silently miss them. Co-authored-by: Claude <[email protected]> Claude-Session: https://claude.ai/code/session_01VUYsdXA2nkF5v6zsC93dpn
Цей коміт міститься в:
1 джерело
50308a956d
коміт
3b608355a3
48 files changed
+1267
-1351
No files matched your search
@@ -9,14 +9,11 @@
|
||||
*/
|
||||
import tokensCss from "@/styles/tokens.css?raw";
|
||||
import type { TextAttributesOverride } from "./editorSettings";
|
||||
import { BUILTIN_THEMES, isBuiltinTheme, themeKind, type BuiltinThemeId } from "./themes";
|
||||
|
||||
export type SchemeId = "light" | "dark" | "dark-hc";
|
||||
export type SchemeId = BuiltinThemeId;
|
||||
|
||||
export const SCHEMES: ReadonlyArray<{ id: SchemeId; name: string }> = [
|
||||
{ id: "light", name: "Islands Light" },
|
||||
{ id: "dark", name: "Islands Dark" },
|
||||
{ id: "dark-hc", name: "High Contrast" },
|
||||
];
|
||||
export const SCHEMES: ReadonlyArray<{ id: SchemeId; name: string }> = BUILTIN_THEMES.map((t) => ({ id: t.id, name: t.label }));
|
||||
|
||||
export interface AttributeDef {
|
||||
id: string;
|
||||
@@ -167,7 +164,8 @@ function blocks(css: string): Array<{ selectors: string[]; decls: Vars }> {
|
||||
function themeSelectors(id: SchemeId): string[] {
|
||||
if (id === "light") return [':root[data-theme="light"]'];
|
||||
if (id === "dark-hc") return [':root[data-theme="dark-hc"]', ':root[data-theme="dark"][data-contrast="high"]'];
|
||||
return [':root[data-theme="dark"]'];
|
||||
if (id === "dark") return [':root[data-theme="dark"]'];
|
||||
return [`:root[data-theme="${themeKind(id)}"]`, ":root[data-color-theme]", `:root[data-color-theme="${id}"]`];
|
||||
}
|
||||
|
||||
let source: string = tokensCss;
|
||||
@@ -205,13 +203,14 @@ export function schemeTokens(id: SchemeId, css: string = source): Vars {
|
||||
|
||||
/** The scheme the editor shows for `setting` under the current theme. */
|
||||
export function effectiveScheme(setting: string | undefined, root: HTMLElement | null = typeof document !== "undefined" ? document.documentElement : null): SchemeId {
|
||||
if (setting === "light" || setting === "dark" || setting === "dark-hc") return setting;
|
||||
if (isBuiltinTheme(setting)) return setting;
|
||||
return themeScheme(root);
|
||||
}
|
||||
|
||||
/** Scheme belonging to the active IDE theme (`data-theme` / `data-contrast`). */
|
||||
export function themeScheme(root: HTMLElement | null = typeof document !== "undefined" ? document.documentElement : null): SchemeId {
|
||||
if (!root) return "light";
|
||||
if (isBuiltinTheme(root.dataset.colorTheme)) return root.dataset.colorTheme;
|
||||
if (root.dataset.theme === "light") return "light";
|
||||
return root.dataset.contrast === "high" || root.dataset.theme === "dark-hc" ? "dark-hc" : "dark";
|
||||
}
|
||||
|
||||
@@ -3681,5 +3681,9 @@
|
||||
"omitted for cycle": "因循环依赖而省略",
|
||||
"version managed from {version}": "版本由 {version} 经依赖管理调整",
|
||||
"Cannot find the declaration": "找不到声明",
|
||||
"Cannot exclude {name}": "无法排除 {name}"
|
||||
"Cannot exclude {name}": "无法排除 {name}",
|
||||
"Solarized Light": "Solarized 浅色",
|
||||
"Solarized Dark": "Solarized 深色",
|
||||
"One Light": "One 浅色",
|
||||
"One Dark": "One 深色"
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { BUILTIN_THEMES, isBuiltinTheme, themeKind } from "./themes";
|
||||
import { _setSchemeSource, effectiveScheme, schemeCss, schemeTokens, themeScheme } from "./colorScheme";
|
||||
import { applyThemeAttributes, resolveTheme, resolveThemeId } from "@/workbench/theme";
|
||||
import { validateSettings } from "@/features/settings/model/schema";
|
||||
import { applyPluginTheme } from "@/features/plugins/bridge";
|
||||
|
||||
const css = readFileSync("src/styles/tokens.css", "utf8");
|
||||
_setSchemeSource(css);
|
||||
afterEach(() => {
|
||||
applyPluginTheme(null);
|
||||
delete document.documentElement.dataset.colorTheme;
|
||||
delete document.documentElement.dataset.contrast;
|
||||
});
|
||||
|
||||
describe("built-in palettes", () => {
|
||||
it.each(BUILTIN_THEMES)("preserves $id through validation and system resolution", ({ id, kind }) => {
|
||||
const a = { theme: id, preferredLightTheme: id, preferredDarkTheme: id };
|
||||
expect(validateSettings({ appearance: a }).appearance).toMatchObject(a);
|
||||
expect(resolveThemeId(a, false)).toBe(id);
|
||||
expect(resolveTheme(id, false)).toBe(kind);
|
||||
expect(resolveThemeId({ ...a, syncWithOs: true }, true)).toBe(id);
|
||||
expect(resolveThemeId({ ...a, theme: "system" }, false)).toBe(id);
|
||||
applyThemeAttributes(id);
|
||||
expect(document.documentElement.dataset.theme).toBe(kind);
|
||||
expect(themeScheme()).toBe(id);
|
||||
expect(effectiveScheme(id)).toBe(id);
|
||||
expect(schemeCss("theme-default", undefined)).toBe("");
|
||||
expect(schemeTokens(id).get("--bg-editor")).not.toContain("var(");
|
||||
});
|
||||
|
||||
it("rejects unknown ids and retains legacy fallbacks", () => {
|
||||
expect(isBuiltinTheme("not-a-theme")).toBe(false);
|
||||
expect(resolveThemeId({ theme: "bad" }, true)).toBe("light");
|
||||
expect(resolveThemeId({ theme: "system", preferredDarkTheme: "bad" }, false)).toBe("dark");
|
||||
expect(themeKind("solarized-light")).toBe("light");
|
||||
});
|
||||
|
||||
// Token coverage of every palette theme: settings/__tests__/contrast.test.ts.
|
||||
it("resolves editor scheme tokens from the palettes", () => {
|
||||
expect(schemeTokens("solarized-light").get("--bg-editor")).toBe("#fdf6e3");
|
||||
expect(schemeTokens("solarized-dark").get("--bg-editor")).toBe("#002b36");
|
||||
expect(schemeTokens("solarized-dark").get("--syn-string")).toBe("#2aa198");
|
||||
expect(schemeTokens("solarized-light").get("--ansi-bright-blue")).toBe("#839496");
|
||||
expect(schemeTokens("one-light").get("--syn-keyword")).toBe("hsl(301, 63%, 40%)");
|
||||
expect(schemeTokens("one-dark").get("--syn-string")).toBe("hsl(95, 38%, 62%)");
|
||||
});
|
||||
|
||||
it("keeps explicit editor schemes and per-scheme edits independent of UI palettes", () => {
|
||||
applyThemeAttributes("solarized-light");
|
||||
expect(schemeCss("one-dark", undefined)).toContain("--bg-editor: hsl(220, 13%, 18%) !important");
|
||||
expect(schemeCss("theme-default", { "solarized-light": { keyword: { foreground: "#123456" } } })).toContain(
|
||||
"--syn-keyword: #123456 !important",
|
||||
);
|
||||
applyThemeAttributes("one-light");
|
||||
expect(schemeCss("theme-default", { "solarized-light": { keyword: { foreground: "#123456" } } })).toBe("");
|
||||
});
|
||||
|
||||
it("clears the palette for plugins and restores it when the plugin is removed", () => {
|
||||
const root = document.documentElement;
|
||||
applyThemeAttributes("one-dark", root);
|
||||
root.dataset.pluginThemeKind = "light";
|
||||
applyThemeAttributes("one-dark", root);
|
||||
expect(root.dataset.colorTheme).toBeUndefined();
|
||||
expect(root.dataset.theme).toBe("light");
|
||||
delete root.dataset.pluginThemeKind;
|
||||
applyThemeAttributes("one-dark", root);
|
||||
expect(root.dataset.colorTheme).toBe("one-dark");
|
||||
applyThemeAttributes("dark-hc", root);
|
||||
expect(root.dataset.colorTheme).toBeUndefined();
|
||||
expect(root.dataset.contrast).toBe("high");
|
||||
applyThemeAttributes("solarized-dark", root);
|
||||
expect(root.dataset.contrast).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
/** Stable built-in ids shared by settings, quick switch and editor schemes. */
|
||||
export const BUILTIN_THEMES = [
|
||||
{ id: "light", label: "Islands Light", kind: "light" },
|
||||
{ id: "dark", label: "Islands Dark", kind: "dark" },
|
||||
{ id: "dark-hc", label: "High Contrast", kind: "dark" },
|
||||
{ id: "solarized-light", label: "Solarized Light", kind: "light" },
|
||||
{ id: "solarized-dark", label: "Solarized Dark", kind: "dark" },
|
||||
{ id: "one-light", label: "One Light", kind: "light" },
|
||||
{ id: "one-dark", label: "One Dark", kind: "dark" },
|
||||
] as const;
|
||||
|
||||
export type BuiltinThemeId = (typeof BUILTIN_THEMES)[number]["id"];
|
||||
export const BUILTIN_THEME_IDS: readonly BuiltinThemeId[] = BUILTIN_THEMES.map((t) => t.id);
|
||||
export function isBuiltinTheme(value: unknown): value is BuiltinThemeId {
|
||||
return typeof value === "string" && (BUILTIN_THEME_IDS as readonly string[]).includes(value);
|
||||
}
|
||||
export function themeKind(id: BuiltinThemeId): "light" | "dark" {
|
||||
return BUILTIN_THEMES.find((t) => t.id === id)!.kind;
|
||||
}
|
||||
Reference in new issue
Заблокувати користувача