дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 03:05: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
@@ -28,7 +28,7 @@ export function installColorScheme(): () => void {
|
||||
const observer = typeof MutationObserver !== "undefined" ? new MutationObserver(apply) : null;
|
||||
observer?.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ["data-theme", "data-contrast", "data-plugin-theme"],
|
||||
attributeFilter: ["data-theme", "data-contrast", "data-color-theme", "data-plugin-theme"],
|
||||
});
|
||||
return () => {
|
||||
unsubscribe();
|
||||
|
||||
@@ -144,8 +144,8 @@ describe("store", () => {
|
||||
|
||||
it("selects and clears a plugin theme", async () => {
|
||||
await refreshPlugins();
|
||||
await selectPluginTheme("theme-solarized", "solarized-dark");
|
||||
expect(usePlugins.getState().activeTheme?.theme.id).toBe("solarized-dark");
|
||||
await selectPluginTheme("theme-harbor-night", "harbor-night");
|
||||
expect(usePlugins.getState().activeTheme?.theme.id).toBe("harbor-night");
|
||||
await selectPluginTheme(null, null);
|
||||
expect(usePlugins.getState().activeTheme).toBeNull();
|
||||
});
|
||||
@@ -238,8 +238,8 @@ 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 Themes"));
|
||||
await user.click(screen.getByRole("button", { name: "Apply theme Solarized Dark" }));
|
||||
await user.click(within(list).getByText("Harbor Night Theme"));
|
||||
await user.click(screen.getByRole("button", { name: "Apply theme Harbor Night" }));
|
||||
await waitFor(() => expect(screen.getByRole("button", { name: "Use Default Theme" })).toBeInTheDocument());
|
||||
|
||||
await user.type(screen.getByLabelText("Filter plugins"), "toy");
|
||||
@@ -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"]);
|
||||
expect(filterCatalog(sampleCatalog(), "harbor").map((c) => c.id)).toEqual(["theme-harbor-night"]);
|
||||
});
|
||||
|
||||
it("disables and re-enables all downloaded plugins, leaving dev plugins alone", async () => {
|
||||
|
||||
@@ -76,35 +76,21 @@ function ensureSubscribed() {
|
||||
// ---------------------------------------------------------------------------
|
||||
// Browser simulation: the bundled sample plugins, with `hello` implemented in JS.
|
||||
|
||||
const SOLARIZED: ActiveTheme["theme"][] = [
|
||||
// templates/theme-plugin, abridged.
|
||||
const SAMPLE_THEMES: ActiveTheme["theme"][] = [
|
||||
{
|
||||
id: "solarized-light",
|
||||
name: "Solarized Light",
|
||||
kind: "light",
|
||||
tokens: {
|
||||
"--bg-app": "#fdf6e3",
|
||||
"--bg-island": "#fdf6e3",
|
||||
"--bg-frame": "#eee8d5",
|
||||
"--bg-elev": "#eee8d5",
|
||||
"--fg": "#657b83",
|
||||
"--accent": "#268bd2",
|
||||
"--syn-keyword": "#859900",
|
||||
"--syn-string": "#2aa198",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "solarized-dark",
|
||||
name: "Solarized Dark",
|
||||
id: "harbor-night",
|
||||
name: "Harbor Night",
|
||||
kind: "dark",
|
||||
tokens: {
|
||||
"--bg-app": "#002b36",
|
||||
"--bg-island": "#002b36",
|
||||
"--bg-frame": "#073642",
|
||||
"--bg-elev": "#073642",
|
||||
"--fg": "#839496",
|
||||
"--accent": "#268bd2",
|
||||
"--syn-keyword": "#859900",
|
||||
"--syn-string": "#2aa198",
|
||||
"--bg-app": "#14171c",
|
||||
"--bg-island": "#14171c",
|
||||
"--bg-frame": "#1f252e",
|
||||
"--bg-elev": "#1b2027",
|
||||
"--fg": "#c9d1dc",
|
||||
"--accent": "#2b9e93",
|
||||
"--syn-keyword": "#e0a46e",
|
||||
"--syn-string": "#9ccc7a",
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -140,14 +126,14 @@ function samplePlugins(): PluginInfo[] {
|
||||
},
|
||||
{
|
||||
...base,
|
||||
id: "theme-solarized",
|
||||
name: "Solarized Themes",
|
||||
id: "theme-harbor-night",
|
||||
name: "Harbor Night Theme",
|
||||
version: "1.0.0",
|
||||
description: "Solarized Light and Solarized Dark: Ethan Schoonover's Solarized palette.",
|
||||
path: "~/Library/Application Support/ai.l0ng.openidea/plugins/theme-solarized",
|
||||
description: "Example dark theme: slate surfaces, a teal accent and a warm syntax palette.",
|
||||
path: "~/Library/Application Support/ai.l0ng.openidea/plugins/theme-harbor-night",
|
||||
status: "static",
|
||||
hasCode: false,
|
||||
themes: SOLARIZED.map(({ id, name, kind }) => ({ id, name, kind })),
|
||||
themes: SAMPLE_THEMES.map(({ id, name, kind }) => ({ id, name, kind })),
|
||||
},
|
||||
{
|
||||
...base,
|
||||
@@ -201,7 +187,7 @@ export function createMemoryPluginsBackend(
|
||||
const activeTheme = (): ActiveTheme | null => {
|
||||
if (!theme) return null;
|
||||
const p = plugins.find((x) => x.id === theme?.pluginId);
|
||||
const t = SOLARIZED.find((x) => x.id === theme?.themeId);
|
||||
const t = SAMPLE_THEMES.find((x) => x.id === theme?.themeId);
|
||||
if (!p?.enabled || !t) return null;
|
||||
return { pluginId: p.id, theme: t };
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { contrastRatio, isColorValue, readTokens } from "../theme/contrast";
|
||||
import { BUILTIN_THEMES } from "@/platform/themes";
|
||||
|
||||
const css = readFileSync(`${process.cwd()}/src/styles/tokens.css`, "utf8");
|
||||
const dark = readTokens(css, ":root");
|
||||
@@ -116,3 +117,49 @@ describe("theme tokens", () => {
|
||||
expect(contrastRatio("#000", "#fff")).toBeCloseTo(21, 5);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Tokens the palette themes (Solarized, One) take from their light / dark base
|
||||
* on purpose: colours the user assigns (project and data source colours), the
|
||||
* platform's caption close button, and the tint strength.
|
||||
*/
|
||||
const INHERITED_FROM_BASE = /^--(project-color-|db-color-|caption-close-|fg-caption-close-|tint-strength$)/;
|
||||
|
||||
describe("palette themes", () => {
|
||||
const ISLANDS = ["light", "dark", "dark-hc"];
|
||||
const palettes = BUILTIN_THEMES.filter((t) => !ISLANDS.includes(t.id));
|
||||
const shared = readTokens(css, ":root[data-color-theme]");
|
||||
// Every themeable token (the Islands Light block lists them all) and every plain colour of :root.
|
||||
const required = [
|
||||
...new Set([
|
||||
...Object.keys(readTokens(css, ':root[data-theme="light"]')),
|
||||
...Object.entries(dark)
|
||||
.filter(([, v]) => isColorValue(v) && !v.includes("var("))
|
||||
.map(([k]) => k),
|
||||
]),
|
||||
];
|
||||
|
||||
it("have a tokens.css block for every palette theme id, and no other", () => {
|
||||
const blocks = [...css.matchAll(/:root\[data-color-theme="([a-z0-9-]+)"\]/g)].map((m) => m[1]);
|
||||
expect([...new Set(blocks)].sort()).toEqual(palettes.map((t) => t.id).sort());
|
||||
expect(palettes.length).toBeGreaterThanOrEqual(4);
|
||||
});
|
||||
|
||||
it.each(palettes)("$id defines every token or inherits it from its $kind base on purpose", ({ id, kind }) => {
|
||||
// A failure here means tokens.css gained a token: map it in the :root[data-color-theme]
|
||||
// block (or the theme's own), or add it to INHERITED_FROM_BASE if the base's value is right.
|
||||
const own = { ...shared, ...readTokens(css, `:root[data-color-theme="${id}"]`) };
|
||||
expect(required.length).toBeGreaterThan(200);
|
||||
expect(required.filter((k) => !(k in own) && !INHERITED_FROM_BASE.test(k))).toEqual([]);
|
||||
const base = kind === "light" ? light : dark;
|
||||
expect(required.filter((k) => INHERITED_FROM_BASE.test(k) && !(k in base))).toEqual([]);
|
||||
// Every palette role the shared mapping uses is set by the theme.
|
||||
const refs = Object.values(own)
|
||||
.join(" ")
|
||||
.matchAll(/var\((--palette-[a-z0-9-]+)/g);
|
||||
const roles = new Set([...refs].map((m) => m[1]!));
|
||||
expect([...roles].filter((r) => !(r in own))).toEqual([]);
|
||||
for (const t of ["--chart-series", "--bg-frame", "--fg-muted", "--accent", "--ansi-red", "--syn-keyword"])
|
||||
expect(own[t], `${id} ${t}`).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -4,11 +4,13 @@
|
||||
* `ide-core::settings` defaults; validation never throws.
|
||||
*/
|
||||
|
||||
import { BUILTIN_THEME_IDS, type BuiltinThemeId } from "@/platform/themes";
|
||||
|
||||
export type Antialiasing = "subpixel" | "greyscale" | "none";
|
||||
export type OpenProjectIn = "ask" | "new" | "same";
|
||||
export type NotificationDisplay = "balloon" | "sticky" | "none";
|
||||
/** Themes selectable as "preferred" when syncing with the OS. */
|
||||
export type ConcreteTheme = "light" | "dark" | "dark-hc";
|
||||
export type ConcreteTheme = BuiltinThemeId;
|
||||
|
||||
export interface AppearanceExtras {
|
||||
syncWithOs: boolean;
|
||||
@@ -102,7 +104,7 @@ const int = (v: unknown, d: number, { min, max }: { min: number; max: number })
|
||||
typeof v === "number" && Number.isFinite(v) ? Math.round(Math.min(max, Math.max(min, v))) : d;
|
||||
|
||||
const AA: readonly Antialiasing[] = ["subpixel", "greyscale", "none"];
|
||||
const CONCRETE: readonly ConcreteTheme[] = ["light", "dark", "dark-hc"];
|
||||
const CONCRETE = BUILTIN_THEME_IDS;
|
||||
const DISPLAYS: readonly NotificationDisplay[] = ["balloon", "sticky", "none"];
|
||||
|
||||
export function validateAppearanceExtras(raw: unknown): AppearanceExtras {
|
||||
|
||||
@@ -33,8 +33,9 @@ import {
|
||||
type GeneralExtras,
|
||||
type NotificationsValid,
|
||||
} from "./behaviorSchema";
|
||||
import { BUILTIN_THEME_IDS, type BuiltinThemeId } from "@/platform/themes";
|
||||
|
||||
export type ThemeId = "dark" | "light" | "system" | "dark-hc";
|
||||
export type ThemeId = BuiltinThemeId | "system";
|
||||
export type LocaleId = "en" | "zh";
|
||||
|
||||
/** A user keymap (Keymap › Duplicate…): its own overrides on top of a bundled preset. */
|
||||
@@ -155,7 +156,7 @@ export const LIMITS = {
|
||||
localHistoryMaxMb: { min: 16, max: 16384 },
|
||||
} as const;
|
||||
|
||||
const THEMES: readonly ThemeId[] = ["dark", "light", "system", "dark-hc"];
|
||||
const THEMES: readonly ThemeId[] = [...BUILTIN_THEME_IDS, "system"];
|
||||
const LOCALES: readonly LocaleId[] = ["en", "zh"];
|
||||
export const HEX_COLOR = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;
|
||||
|
||||
|
||||
@@ -29,13 +29,11 @@ import type { PageSpec } from "./spec";
|
||||
import styles from "./Behavior.module.css";
|
||||
import { useT } from "@/platform/i18n";
|
||||
|
||||
import { BUILTIN_THEMES } from "@/platform/themes";
|
||||
|
||||
export const APPEARANCE_GROUP = "Appearance & Behavior";
|
||||
|
||||
const THEMES: SelectOption<ConcreteTheme>[] = [
|
||||
{ value: "light", label: "Islands Light" },
|
||||
{ value: "dark", label: "Islands Dark" },
|
||||
{ value: "dark-hc", label: "High Contrast" },
|
||||
];
|
||||
const THEMES: SelectOption<ConcreteTheme>[] = BUILTIN_THEMES.map((t) => ({ value: t.id, label: t.label }));
|
||||
|
||||
/** Checkbox bound to a key, optionally disabled. */
|
||||
function BoundToggle({ k, label, hint, disabled }: { k: string; label: string; hint?: ReactNode; disabled?: boolean }) {
|
||||
@@ -500,7 +498,7 @@ export const BEHAVIOR_PAGES: PageSpec[] = [
|
||||
{
|
||||
kind: "custom",
|
||||
label: "Theme",
|
||||
keywords: ["Sync with OS", "Islands Light", "Islands Dark", "High Contrast", "preferred theme"],
|
||||
keywords: ["Sync with OS", ...BUILTIN_THEMES.map((t) => t.label), "preferred theme"],
|
||||
component: ThemeRow,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -195,10 +195,10 @@ export function activate(): () => void {
|
||||
terminalRuntime()?.applyThemeToAll();
|
||||
terminalRuntime()?.applyThemeToEmbedded();
|
||||
});
|
||||
// A plugin theme switch keeps data-theme when both themes share a light / dark base.
|
||||
// A palette or plugin theme switch keeps data-theme when both themes share a light / dark base.
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ["data-theme", "data-contrast", "data-plugin-theme", "style"],
|
||||
attributeFilter: ["data-theme", "data-contrast", "data-color-theme", "data-plugin-theme", "style"],
|
||||
});
|
||||
disposers.push(() => observer.disconnect());
|
||||
}
|
||||
|
||||
@@ -108,16 +108,38 @@ describe("mermaid in the markdown preview", () => {
|
||||
expect(ids.size).toBe(2);
|
||||
});
|
||||
|
||||
it("re-renders when switching between built-in palette themes of the same light / dark base", async () => {
|
||||
const root = document.documentElement;
|
||||
const previous = root.dataset.colorTheme;
|
||||
const changed = vi.fn();
|
||||
const off = subscribeTheme(changed);
|
||||
try {
|
||||
root.dataset.colorTheme = "one-dark";
|
||||
const firstKey = themeKey();
|
||||
await renderMermaidIn(preview(SEQ)).done;
|
||||
root.dataset.colorTheme = "solarized-dark";
|
||||
await Promise.resolve();
|
||||
expect(changed).toHaveBeenCalled();
|
||||
expect(themeKey()).not.toBe(firstKey);
|
||||
await renderMermaidIn(preview(SEQ)).done;
|
||||
expect(m.render).toHaveBeenCalledTimes(2);
|
||||
} finally {
|
||||
off();
|
||||
if (previous === undefined) delete root.dataset.colorTheme;
|
||||
else root.dataset.colorTheme = previous;
|
||||
}
|
||||
});
|
||||
|
||||
it("re-renders when switching between plugin themes of the same light / dark base", async () => {
|
||||
const root = document.documentElement;
|
||||
const changed = vi.fn();
|
||||
const off = subscribeTheme(changed);
|
||||
try {
|
||||
root.dataset.theme = "dark";
|
||||
root.dataset.pluginTheme = "theme-one/one-dark";
|
||||
root.dataset.pluginTheme = "acme-themes/night";
|
||||
const firstKey = themeKey();
|
||||
await renderMermaidIn(preview(SEQ)).done;
|
||||
root.dataset.pluginTheme = "theme-solarized/solarized-dark";
|
||||
root.dataset.pluginTheme = "acme-themes/dusk";
|
||||
await Promise.resolve();
|
||||
expect(changed).toHaveBeenCalled();
|
||||
expect(themeKey()).not.toBe(firstKey);
|
||||
|
||||
@@ -30,8 +30,8 @@ function loadMermaid(): Promise<Mermaid> {
|
||||
/** Theme identity: anything that changes the colours read below. */
|
||||
export function themeKey(): string {
|
||||
const root = document.documentElement;
|
||||
const { theme = "", contrast = "", pluginTheme = "" } = root.dataset;
|
||||
return `${theme}|${contrast}|${pluginTheme}|${root.style.getPropertyValue("--accent")}`;
|
||||
const { theme = "", contrast = "", colorTheme = "", pluginTheme = "" } = root.dataset;
|
||||
return `${theme}|${contrast}|${colorTheme}|${pluginTheme}|${root.style.getPropertyValue("--accent")}`;
|
||||
}
|
||||
|
||||
/** Calls `cb` when the theme attributes (a plugin theme's included) or the accent on the root change. */
|
||||
@@ -40,7 +40,7 @@ export function subscribeTheme(cb: () => void): () => void {
|
||||
const observer = new MutationObserver(cb);
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ["data-theme", "data-contrast", "data-plugin-theme", "style"],
|
||||
attributeFilter: ["data-theme", "data-contrast", "data-color-theme", "data-plugin-theme", "style"],
|
||||
});
|
||||
return () => observer.disconnect();
|
||||
}
|
||||
|
||||
@@ -13,14 +13,11 @@ import { notifications } from "@/workbench/contributions";
|
||||
import { ACCENT_PRESETS, DEFAULT_ACCENT } from "@/workbench/theme";
|
||||
import { reopenEnabled, setReopenPreference, useSession } from "./session";
|
||||
import styles from "./Welcome.module.css";
|
||||
import { BUILTIN_THEMES, isBuiltinTheme, type BuiltinThemeId } from "@/platform/themes";
|
||||
|
||||
type Theme = "light" | "dark" | "dark-hc";
|
||||
type Theme = BuiltinThemeId;
|
||||
|
||||
export const THEME_OPTIONS: SelectOption<Theme>[] = [
|
||||
{ value: "light", label: "Islands Light" },
|
||||
{ value: "dark", label: "Islands Dark" },
|
||||
{ value: "dark-hc", label: "High Contrast" },
|
||||
];
|
||||
export const THEME_OPTIONS: SelectOption<Theme>[] = BUILTIN_THEMES.map((t) => ({ value: t.id, label: t.label }));
|
||||
|
||||
const DEFAULT_UI_FONT_SIZE = 13;
|
||||
const DEFAULT_UI_FONT = "Inter";
|
||||
@@ -52,7 +49,7 @@ interface AppearanceView {
|
||||
/** The values Customize shows, from (possibly partial / legacy) settings. */
|
||||
export function customizeView(settings: ReturnType<typeof useProject.getState>["settings"]): AppearanceView {
|
||||
const a = (settings?.appearance ?? {}) as Record<string, unknown>;
|
||||
const theme = a.theme === "dark" || a.theme === "dark-hc" ? a.theme : "light";
|
||||
const theme = isBuiltinTheme(a.theme) ? a.theme : "light";
|
||||
const custom = a.useCustomFont === true;
|
||||
const size = typeof a.uiFontSize === "number" ? a.uiFontSize : DEFAULT_UI_FONT_SIZE;
|
||||
return {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -824,6 +824,402 @@ html {
|
||||
--bg-progress-track: #55585f;
|
||||
}
|
||||
|
||||
/* Solarized and Atom One adaptations. Exact upstream palettes and MIT notices:
|
||||
* docs/themes.md and THIRD_PARTY_LICENSES.md. The shared mapping covers every
|
||||
* colour token; only palette roles below differ. UI-only states use explicit
|
||||
* mixes of original colours where the source has no equivalent component.
|
||||
* Keep before appearance options so accessibility preferences still win.
|
||||
*/
|
||||
:root[data-color-theme] {
|
||||
--bg-app: var(--palette-bg);
|
||||
--bg-island: var(--palette-bg);
|
||||
--bg-input: var(--palette-bg);
|
||||
--bg-field: var(--palette-bg);
|
||||
--bg-checkbox: var(--palette-bg);
|
||||
--bg-control: var(--palette-bg);
|
||||
--bg-button: var(--palette-bg);
|
||||
--bg-dialog: var(--palette-bg);
|
||||
--bg-frame: var(--palette-panel);
|
||||
--bg-elev: var(--palette-panel);
|
||||
--bg-backdrop: var(--palette-panel);
|
||||
--bg-popup-inline: var(--palette-panel);
|
||||
--bg-popup-advertiser: var(--palette-panel);
|
||||
--bg-tab-selected-inactive: var(--palette-panel);
|
||||
--bg-toggle-off: var(--palette-panel);
|
||||
--bg-editor-inline: var(--palette-panel);
|
||||
--bg-inlay: var(--palette-panel);
|
||||
--bg-fold-placeholder: var(--palette-panel);
|
||||
--bg-popup: var(--palette-popup);
|
||||
--bg-tooltip: var(--palette-popup);
|
||||
--bg-hover: var(--palette-hover);
|
||||
--bg-control-hover: var(--palette-hover);
|
||||
--bg-active: var(--palette-hover);
|
||||
--bg-tree-selected-inactive: var(--palette-hover);
|
||||
--bg-progress-track: var(--palette-hover);
|
||||
--bg-tab-hover: var(--palette-hover);
|
||||
--bg-tree-hover: var(--palette-hover);
|
||||
--bg-toolbar-hover: var(--palette-hover);
|
||||
--bg-stripe-hover: var(--palette-hover);
|
||||
--bg-selected: var(--palette-selection);
|
||||
--bg-selected-strong: var(--palette-selection);
|
||||
--bg-tree-selected: var(--palette-selection);
|
||||
--bg-tab-selected: var(--palette-selection);
|
||||
--bg-toolbar-selected: var(--palette-selection);
|
||||
--bg-toolbar-selected-hover: var(--palette-selection);
|
||||
--bg-toolbar-pressed: var(--palette-selection);
|
||||
--bg-stripe-selected: var(--palette-selection);
|
||||
--bg-editor-selection: var(--palette-selection);
|
||||
--bg-editor-selection-inactive: var(--palette-selection);
|
||||
--bg-matched-brace: var(--palette-selection);
|
||||
--bg-matched-tag: var(--palette-selection);
|
||||
--bg-identifier-under-caret: var(--palette-selection);
|
||||
--bg-current-line: var(--palette-line);
|
||||
--border: var(--palette-border);
|
||||
--border-gutter: var(--palette-border);
|
||||
--border-indent-guide: var(--palette-border);
|
||||
--border-right-margin: var(--palette-border);
|
||||
--border-editor-tabs: var(--palette-border);
|
||||
--border-tab-selected-inactive: var(--palette-border);
|
||||
--border-button: var(--palette-border);
|
||||
--border-window: var(--palette-border);
|
||||
--border-popup: var(--palette-border);
|
||||
--border-tooltip: var(--palette-border);
|
||||
--border-strong: var(--palette-muted);
|
||||
--control-off: var(--palette-muted);
|
||||
--border-checkbox: var(--palette-muted);
|
||||
--border-toggle: var(--palette-muted);
|
||||
--bg-toggle-knob: var(--palette-muted);
|
||||
--fg: var(--palette-fg);
|
||||
--fg-emphasis: var(--palette-fg);
|
||||
--fg-strong: var(--palette-fg);
|
||||
--fg-icon: var(--palette-fg);
|
||||
--fg-tooltip: var(--palette-fg);
|
||||
--fg-editor: var(--palette-fg);
|
||||
--fg-fold-placeholder: var(--palette-fg);
|
||||
--fg-line-number-current: var(--palette-fg);
|
||||
--fg-stripe-label: var(--palette-fg);
|
||||
--fg-toggle-off: var(--palette-fg);
|
||||
--fg-secondary: var(--palette-muted);
|
||||
--fg-muted: var(--palette-muted);
|
||||
--fg-subtle: var(--palette-muted);
|
||||
--fg-faint: var(--palette-muted);
|
||||
--fg-gutter: var(--palette-muted);
|
||||
--fg-line-number: var(--palette-muted);
|
||||
--fg-fold-marker: var(--palette-muted);
|
||||
--fg-whitespace: var(--palette-muted);
|
||||
--fg-unused: var(--palette-muted);
|
||||
--fg-inlay: var(--palette-muted);
|
||||
--fg-weak-warning: var(--palette-muted);
|
||||
--syn-inline-value: var(--palette-muted);
|
||||
--accent: var(--palette-accent);
|
||||
--caret: var(--palette-accent);
|
||||
--fg-on-accent: var(--palette-on-accent);
|
||||
--fg-button-primary: var(--palette-on-accent);
|
||||
--fg-stripe-selected-active: var(--palette-on-accent);
|
||||
--fg-toggle-on: var(--palette-on-accent);
|
||||
--fg-counter: var(--palette-on-accent);
|
||||
--link: var(--palette-blue);
|
||||
--fg-completion-match: var(--palette-blue);
|
||||
--info: var(--palette-blue);
|
||||
--vcs-modified: var(--palette-blue);
|
||||
--gutter-modified: var(--palette-blue);
|
||||
--syn-link: var(--palette-blue);
|
||||
--link-hover: var(--palette-cyan);
|
||||
--danger: var(--palette-red);
|
||||
--vcs-untracked: var(--palette-red);
|
||||
--vcs-conflict: var(--palette-red);
|
||||
--gutter-deleted: var(--palette-red);
|
||||
--wave-error: var(--palette-red);
|
||||
--syn-invalid: var(--palette-red);
|
||||
--chart-series: var(--palette-red);
|
||||
--warning: var(--palette-yellow);
|
||||
--wave-warning: var(--palette-yellow);
|
||||
--fg-bulb: var(--palette-yellow);
|
||||
--vcs-ignored: var(--palette-yellow);
|
||||
--success: var(--palette-green);
|
||||
--vcs-added: var(--palette-green);
|
||||
--gutter-added: var(--palette-green);
|
||||
--db-connected: var(--palette-green);
|
||||
--bg-toggle-on: var(--palette-green);
|
||||
--db-connected-ring: var(--palette-green);
|
||||
--vcs-deleted: var(--palette-muted);
|
||||
--bg-search-match: color-mix(in srgb, var(--palette-yellow) 22%, var(--palette-bg));
|
||||
--bg-text-search-result: color-mix(in srgb, var(--palette-yellow) 22%, var(--palette-bg));
|
||||
--bg-search-match-current: color-mix(in srgb, var(--palette-yellow) 36%, var(--palette-bg));
|
||||
--bg-text-search-result-current: color-mix(in srgb, var(--palette-yellow) 36%, var(--palette-bg));
|
||||
--fg-search-match: var(--palette-fg);
|
||||
--border-tab-selected: var(--palette-accent);
|
||||
--border-indent-guide-active: var(--palette-accent);
|
||||
--focus-ring-soft: var(--palette-accent);
|
||||
--bg-counter: var(--palette-accent);
|
||||
--bg-button-primary-hover: color-mix(in srgb, var(--accent) 86%, var(--palette-fg));
|
||||
--scrollbar-thumb: color-mix(in srgb, var(--palette-fg) 24%, transparent);
|
||||
--scrollbar-thumb-hover: color-mix(in srgb, var(--palette-fg) 42%, transparent);
|
||||
--bg-scrim: color-mix(in srgb, var(--palette-bg) 68%, transparent);
|
||||
--bg-debug-line: color-mix(in srgb, var(--palette-blue) 12%, var(--palette-bg));
|
||||
--bg-editor-banner: color-mix(in srgb, var(--palette-blue) 12%, var(--palette-bg));
|
||||
--bg-feedback-info: color-mix(in srgb, var(--palette-blue) 12%, var(--palette-bg));
|
||||
--bg-breakpoint-line: color-mix(in srgb, var(--palette-red) 12%, var(--palette-bg));
|
||||
--bg-identifier-under-caret-write: color-mix(in srgb, var(--palette-red) 12%, var(--palette-bg));
|
||||
--bg-feedback-error: color-mix(in srgb, var(--palette-red) 12%, var(--palette-bg));
|
||||
--bg-feedback-warning: color-mix(in srgb, var(--palette-yellow) 12%, var(--palette-bg));
|
||||
--editor-warning-background: color-mix(in srgb, var(--palette-yellow) 12%, var(--palette-bg));
|
||||
--bg-feedback-success: color-mix(in srgb, var(--palette-green) 12%, var(--palette-bg));
|
||||
--border-feedback-info: color-mix(in srgb, var(--palette-blue) 55%, var(--palette-bg));
|
||||
--border-feedback-error: color-mix(in srgb, var(--palette-red) 55%, var(--palette-bg));
|
||||
--border-feedback-warning: color-mix(in srgb, var(--palette-yellow) 55%, var(--palette-bg));
|
||||
--border-feedback-success: color-mix(in srgb, var(--palette-green) 55%, var(--palette-bg));
|
||||
--editor-error-decoration: underline wavy var(--wave-error);
|
||||
--editor-warning-decoration: underline wavy var(--wave-warning);
|
||||
--matched-brace-weight: var(--fw-code-bold);
|
||||
--shadow-popup: 0 4px 16px #00000024;
|
||||
--shadow-tooltip: 0 4px 16px #00000024;
|
||||
--shadow-dialog: 0 10px 36px #00000033;
|
||||
--shadow-balloon: 0 10px 36px #00000033;
|
||||
--traffic-off: var(--palette-hover);
|
||||
--traffic-off-border: var(--palette-border);
|
||||
--traffic-close: #ff5f57;
|
||||
--traffic-close-border: #e0443e;
|
||||
--traffic-close-pressed: #bf4942;
|
||||
--traffic-glyph: #4d0000;
|
||||
--syn-keyword: var(--palette-keyword);
|
||||
--syn-string: var(--palette-green);
|
||||
--syn-type: var(--palette-type);
|
||||
--syn-function: var(--palette-blue);
|
||||
--syn-function-call: var(--palette-blue);
|
||||
--syn-comment: var(--palette-comment);
|
||||
--syn-doc-comment: var(--palette-comment);
|
||||
--syn-docstring: var(--palette-green);
|
||||
--syn-annotation: var(--palette-orange);
|
||||
--syn-decorator: var(--palette-orange);
|
||||
--syn-number: var(--palette-number);
|
||||
--syn-punct: var(--palette-fg);
|
||||
--syn-variable: var(--palette-variable);
|
||||
--syn-param: var(--palette-fg);
|
||||
--syn-property: var(--palette-property);
|
||||
--syn-constant: var(--palette-number);
|
||||
--syn-builtin: var(--palette-cyan);
|
||||
--syn-self: var(--palette-red);
|
||||
--syn-keyword-arg: var(--palette-red);
|
||||
--syn-predefined: var(--palette-cyan);
|
||||
--syn-escape: var(--palette-cyan);
|
||||
--syn-regexp: var(--palette-cyan);
|
||||
--syn-tag: var(--palette-red);
|
||||
--syn-attribute: var(--palette-orange);
|
||||
--syn-todo: var(--palette-magenta);
|
||||
--syn-markup-heading: var(--palette-blue);
|
||||
--syn-markup-code: var(--palette-green);
|
||||
--syn-comment-style: italic;
|
||||
--syn-rainbow-1: var(--palette-cyan);
|
||||
--syn-rainbow-2: var(--palette-orange);
|
||||
--syn-rainbow-3: var(--palette-green);
|
||||
--syn-rainbow-4: var(--palette-magenta);
|
||||
--syn-rainbow-5: var(--palette-blue);
|
||||
--syn-bracket-1: var(--palette-yellow);
|
||||
--syn-bracket-2: var(--palette-magenta);
|
||||
--syn-bracket-3: var(--palette-blue);
|
||||
--ansi-black: var(--palette-ansi-black);
|
||||
--ansi-bright-black: var(--palette-ansi-bright-black);
|
||||
--ansi-red: var(--palette-ansi-red);
|
||||
--ansi-bright-red: var(--palette-ansi-bright-red);
|
||||
--ansi-green: var(--palette-ansi-green);
|
||||
--ansi-bright-green: var(--palette-ansi-bright-green);
|
||||
--ansi-yellow: var(--palette-ansi-yellow);
|
||||
--ansi-bright-yellow: var(--palette-ansi-bright-yellow);
|
||||
--ansi-blue: var(--palette-ansi-blue);
|
||||
--ansi-bright-blue: var(--palette-ansi-bright-blue);
|
||||
--ansi-magenta: var(--palette-ansi-magenta);
|
||||
--ansi-bright-magenta: var(--palette-ansi-bright-magenta);
|
||||
--ansi-cyan: var(--palette-ansi-cyan);
|
||||
--ansi-bright-cyan: var(--palette-ansi-bright-cyan);
|
||||
--ansi-white: var(--palette-ansi-white);
|
||||
--ansi-bright-white: var(--palette-ansi-bright-white);
|
||||
}
|
||||
|
||||
:root[data-color-theme="solarized-light"] {
|
||||
--palette-yellow: #b58900;
|
||||
--palette-orange: #cb4b16;
|
||||
--palette-red: #dc322f;
|
||||
--palette-magenta: #d33682;
|
||||
--palette-violet: #6c71c4;
|
||||
--palette-blue: #268bd2;
|
||||
--palette-cyan: #2aa198;
|
||||
--palette-green: #859900;
|
||||
--palette-bg: #fdf6e3;
|
||||
--palette-panel: #eee8d5;
|
||||
--palette-popup: #fdf6e3;
|
||||
--palette-fg: #657b83;
|
||||
--palette-muted: #657b83;
|
||||
--palette-border: #eee8d5;
|
||||
--palette-hover: #eee8d5;
|
||||
--palette-selection: #eee8d5;
|
||||
--palette-line: #eee8d5;
|
||||
--palette-accent: #268bd2;
|
||||
--palette-on-accent: #002b36;
|
||||
--palette-keyword: #859900;
|
||||
--palette-type: #b58900;
|
||||
--palette-number: #2aa198;
|
||||
--palette-variable: #268bd2;
|
||||
--palette-property: #268bd2;
|
||||
--palette-comment: #93a1a1;
|
||||
--palette-ansi-black: #073642;
|
||||
--palette-ansi-red: #dc322f;
|
||||
--palette-ansi-green: #859900;
|
||||
--palette-ansi-yellow: #b58900;
|
||||
--palette-ansi-blue: #268bd2;
|
||||
--palette-ansi-magenta: #d33682;
|
||||
--palette-ansi-cyan: #2aa198;
|
||||
--palette-ansi-white: #eee8d5;
|
||||
--palette-ansi-bright-black: #002b36;
|
||||
--palette-ansi-bright-red: #cb4b16;
|
||||
--palette-ansi-bright-green: #586e75;
|
||||
--palette-ansi-bright-yellow: #657b83;
|
||||
--palette-ansi-bright-blue: #839496;
|
||||
--palette-ansi-bright-magenta: #6c71c4;
|
||||
--palette-ansi-bright-cyan: #93a1a1;
|
||||
--palette-ansi-bright-white: #fdf6e3;
|
||||
--syn-string: #2aa198;
|
||||
--syn-docstring: #2aa198;
|
||||
--syn-markup-code: #2aa198;
|
||||
--syn-tag: #268bd2;
|
||||
--syn-escape: #dc322f;
|
||||
}
|
||||
|
||||
:root[data-color-theme="solarized-dark"] {
|
||||
--palette-yellow: #b58900;
|
||||
--palette-orange: #cb4b16;
|
||||
--palette-red: #dc322f;
|
||||
--palette-magenta: #d33682;
|
||||
--palette-violet: #6c71c4;
|
||||
--palette-blue: #268bd2;
|
||||
--palette-cyan: #2aa198;
|
||||
--palette-green: #859900;
|
||||
--palette-bg: #002b36;
|
||||
--palette-panel: #073642;
|
||||
--palette-popup: #073642;
|
||||
--palette-fg: #839496;
|
||||
--palette-muted: #839496;
|
||||
--palette-border: #073642;
|
||||
--palette-hover: #073642;
|
||||
--palette-selection: #073642;
|
||||
--palette-line: #073642;
|
||||
--palette-accent: #268bd2;
|
||||
--palette-on-accent: #002b36;
|
||||
--palette-keyword: #859900;
|
||||
--palette-type: #b58900;
|
||||
--palette-number: #2aa198;
|
||||
--palette-variable: #268bd2;
|
||||
--palette-property: #268bd2;
|
||||
--palette-comment: #586e75;
|
||||
--palette-ansi-black: #073642;
|
||||
--palette-ansi-red: #dc322f;
|
||||
--palette-ansi-green: #859900;
|
||||
--palette-ansi-yellow: #b58900;
|
||||
--palette-ansi-blue: #268bd2;
|
||||
--palette-ansi-magenta: #d33682;
|
||||
--palette-ansi-cyan: #2aa198;
|
||||
--palette-ansi-white: #eee8d5;
|
||||
--palette-ansi-bright-black: #002b36;
|
||||
--palette-ansi-bright-red: #cb4b16;
|
||||
--palette-ansi-bright-green: #586e75;
|
||||
--palette-ansi-bright-yellow: #657b83;
|
||||
--palette-ansi-bright-blue: #839496;
|
||||
--palette-ansi-bright-magenta: #6c71c4;
|
||||
--palette-ansi-bright-cyan: #93a1a1;
|
||||
--palette-ansi-bright-white: #fdf6e3;
|
||||
--syn-string: #2aa198;
|
||||
--syn-docstring: #2aa198;
|
||||
--syn-markup-code: #2aa198;
|
||||
--syn-tag: #268bd2;
|
||||
--syn-escape: #dc322f;
|
||||
}
|
||||
|
||||
:root[data-color-theme="one-light"] {
|
||||
--palette-bg: hsl(230, 1%, 98%);
|
||||
--palette-panel: hsl(230, 1%, 92%);
|
||||
--palette-popup: hsl(230, 1%, 95%);
|
||||
--palette-fg: hsl(230, 8%, 24%);
|
||||
--palette-muted: hsl(230, 6%, 44%);
|
||||
--palette-border: hsl(230, 1%, 86%);
|
||||
--palette-hover: hsl(230, 1%, 90%);
|
||||
--palette-selection: hsl(230, 1%, 90%);
|
||||
--palette-line: color-mix(in srgb, hsl(230, 8%, 24%) 5%, hsl(230, 1%, 98%));
|
||||
--palette-accent: hsl(230, 100%, 66%);
|
||||
--palette-on-accent: #ffffff;
|
||||
--palette-cyan: hsl(198, 99%, 37%);
|
||||
--palette-blue: hsl(221, 87%, 60%);
|
||||
--palette-magenta: hsl(301, 63%, 40%);
|
||||
--palette-green: hsl(119, 34%, 47%);
|
||||
--palette-red: hsl(5, 74%, 59%);
|
||||
--palette-orange: hsl(41, 99%, 30%);
|
||||
--palette-yellow: hsl(41, 99%, 38%);
|
||||
--palette-comment: hsl(230, 4%, 64%);
|
||||
--palette-keyword: hsl(301, 63%, 40%);
|
||||
--palette-type: hsl(41, 99%, 38%);
|
||||
--palette-number: hsl(41, 99%, 30%);
|
||||
--palette-variable: hsl(5, 74%, 59%);
|
||||
--palette-property: hsl(230, 8%, 24%);
|
||||
--palette-ansi-black: hsl(230, 8%, 24%);
|
||||
--palette-ansi-red: hsl(5, 74%, 59%);
|
||||
--palette-ansi-green: hsl(119, 34%, 47%);
|
||||
--palette-ansi-yellow: hsl(41, 99%, 38%);
|
||||
--palette-ansi-blue: hsl(221, 87%, 60%);
|
||||
--palette-ansi-magenta: hsl(301, 63%, 40%);
|
||||
--palette-ansi-cyan: hsl(198, 99%, 37%);
|
||||
--palette-ansi-white: hsl(230, 6%, 44%);
|
||||
--palette-ansi-bright-black: hsl(230, 6%, 44%);
|
||||
--palette-ansi-bright-red: hsl(5, 74%, 59%);
|
||||
--palette-ansi-bright-green: hsl(119, 34%, 47%);
|
||||
--palette-ansi-bright-yellow: hsl(41, 99%, 38%);
|
||||
--palette-ansi-bright-blue: hsl(221, 87%, 60%);
|
||||
--palette-ansi-bright-magenta: hsl(301, 63%, 40%);
|
||||
--palette-ansi-bright-cyan: hsl(198, 99%, 37%);
|
||||
--palette-ansi-bright-white: hsl(230, 8%, 24%);
|
||||
}
|
||||
|
||||
:root[data-color-theme="one-dark"] {
|
||||
--palette-bg: hsl(220, 13%, 18%);
|
||||
--palette-panel: hsl(220, 13%, 15%);
|
||||
--palette-popup: hsl(220, 13%, 16.5%);
|
||||
--palette-fg: hsl(220, 14%, 71%);
|
||||
--palette-muted: hsl(220, 9%, 55%);
|
||||
--palette-border: hsl(220, 13%, 10.8%);
|
||||
--palette-hover: hsl(220, 13%, 22%);
|
||||
--palette-selection: hsl(220, 13%, 28%);
|
||||
--palette-line: color-mix(in srgb, hsl(220, 100%, 80%) 4%, hsl(220, 13%, 18%));
|
||||
--palette-accent: hsl(220, 100%, 66%);
|
||||
--palette-on-accent: #000000;
|
||||
--palette-cyan: hsl(187, 47%, 55%);
|
||||
--palette-blue: hsl(207, 82%, 66%);
|
||||
--palette-magenta: hsl(286, 60%, 67%);
|
||||
--palette-green: hsl(95, 38%, 62%);
|
||||
--palette-red: hsl(355, 65%, 65%);
|
||||
--palette-orange: hsl(29, 54%, 61%);
|
||||
--palette-yellow: hsl(39, 67%, 69%);
|
||||
--palette-comment: hsl(220, 10%, 40%);
|
||||
--palette-keyword: hsl(286, 60%, 67%);
|
||||
--palette-type: hsl(39, 67%, 69%);
|
||||
--palette-number: hsl(29, 54%, 61%);
|
||||
--palette-variable: hsl(355, 65%, 65%);
|
||||
--palette-property: hsl(220, 14%, 71%);
|
||||
--palette-ansi-black: hsl(220, 13%, 15%);
|
||||
--palette-ansi-red: hsl(355, 65%, 65%);
|
||||
--palette-ansi-green: hsl(95, 38%, 62%);
|
||||
--palette-ansi-yellow: hsl(39, 67%, 69%);
|
||||
--palette-ansi-blue: hsl(207, 82%, 66%);
|
||||
--palette-ansi-magenta: hsl(286, 60%, 67%);
|
||||
--palette-ansi-cyan: hsl(187, 47%, 55%);
|
||||
--palette-ansi-white: hsl(220, 14%, 71%);
|
||||
--palette-ansi-bright-black: hsl(220, 9%, 55%);
|
||||
--palette-ansi-bright-red: hsl(355, 65%, 65%);
|
||||
--palette-ansi-bright-green: hsl(95, 38%, 62%);
|
||||
--palette-ansi-bright-yellow: hsl(39, 67%, 69%);
|
||||
--palette-ansi-bright-blue: hsl(207, 82%, 66%);
|
||||
--palette-ansi-bright-magenta: hsl(286, 60%, 67%);
|
||||
--palette-ansi-bright-cyan: hsl(187, 47%, 55%);
|
||||
--palette-ansi-bright-white: #ffffff;
|
||||
}
|
||||
|
||||
/*
|
||||
* Appearance options (Settings › Appearance), applied as attributes on :root
|
||||
* by the settings runtime.
|
||||
|
||||
@@ -4,9 +4,10 @@ import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||
import { create } from "zustand";
|
||||
import { call, commands, isTauri } from "@/platform/ipc";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { BUILTIN_THEMES, isBuiltinTheme, themeKind, type BuiltinThemeId } from "@/platform/themes";
|
||||
import { resolveTokenColor, toHex } from "@/platform/cssColor";
|
||||
|
||||
export type ThemeSetting = "dark" | "light" | "system" | "dark-hc";
|
||||
export type ThemeSetting = BuiltinThemeId | "system";
|
||||
/** PyCharm's brand blue: the accent both Islands themes ship with. */
|
||||
export const DEFAULT_ACCENT = "#3871e1";
|
||||
/** Accent presets (Settings › Appearance, Welcome › Customize); the first is the Islands default. */
|
||||
@@ -44,8 +45,8 @@ interface AppearanceLike {
|
||||
* Concrete theme id: the chosen theme, or with "Sync with OS" (legacy
|
||||
* `theme = "system"`) the preferred light / dark theme for the OS mode.
|
||||
*/
|
||||
export function resolveThemeId(a: AppearanceLike | undefined, systemLight: boolean): "dark" | "light" | "dark-hc" {
|
||||
const concrete = (v: string | undefined, d: "dark" | "light") => (v === "dark" || v === "light" || v === "dark-hc" ? v : d);
|
||||
export function resolveThemeId(a: AppearanceLike | undefined, systemLight: boolean): BuiltinThemeId {
|
||||
const concrete = (v: string | undefined, d: "dark" | "light") => (isBuiltinTheme(v) ? v : d);
|
||||
if (a?.syncWithOs || a?.theme === "system")
|
||||
return systemLight ? concrete(a?.preferredLightTheme, "light") : concrete(a?.preferredDarkTheme, "dark");
|
||||
return concrete(a?.theme, "light");
|
||||
@@ -53,7 +54,7 @@ export function resolveThemeId(a: AppearanceLike | undefined, systemLight: boole
|
||||
|
||||
/** `dark-hc` resolves to `dark` plus `data-contrast="high"`. */
|
||||
export function resolveTheme(setting: string | undefined, systemLight: boolean): "dark" | "light" {
|
||||
return resolveThemeId({ theme: setting }, systemLight) === "light" ? "light" : "dark";
|
||||
return themeKind(resolveThemeId({ theme: setting }, systemLight));
|
||||
}
|
||||
|
||||
const ACCENT_RE = /^#[0-9a-f]{3,8}$/i;
|
||||
@@ -82,17 +83,21 @@ export function useApplyTheme() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets `data-theme` / `data-contrast` for a built-in theme id. While a plugin
|
||||
* Sets `data-theme` / `data-contrast` / `data-color-theme` (the palette themes
|
||||
* over a light / dark base, tokens.css) for a built-in theme id. While a plugin
|
||||
* theme is active (`data-plugin-theme-kind`), its light / dark base wins.
|
||||
*/
|
||||
export function applyThemeAttributes(id: "dark" | "light" | "dark-hc", root: HTMLElement = document.documentElement) {
|
||||
export function applyThemeAttributes(id: BuiltinThemeId, root: HTMLElement = document.documentElement) {
|
||||
const pluginKind = root.dataset.pluginThemeKind;
|
||||
if (pluginKind === "light" || pluginKind === "dark") {
|
||||
delete root.dataset.colorTheme;
|
||||
root.dataset.theme = pluginKind;
|
||||
delete root.dataset.contrast;
|
||||
return;
|
||||
}
|
||||
root.dataset.theme = id === "light" ? "light" : "dark";
|
||||
if (id === "light" || id === "dark" || id === "dark-hc") delete root.dataset.colorTheme;
|
||||
else root.dataset.colorTheme = id;
|
||||
root.dataset.theme = themeKind(id);
|
||||
if (id === "dark-hc") root.dataset.contrast = "high";
|
||||
else delete root.dataset.contrast;
|
||||
}
|
||||
@@ -135,6 +140,7 @@ export function rememberStartupTheme(root: HTMLElement = document.documentElemen
|
||||
JSON.stringify({
|
||||
theme: root.dataset.theme,
|
||||
contrast: root.dataset.contrast,
|
||||
colorTheme: root.dataset.colorTheme,
|
||||
frame: hex("--bg-frame"),
|
||||
fg: hex("--fg-muted"),
|
||||
accent: hex("--accent"),
|
||||
@@ -162,18 +168,14 @@ export function syncWindowBackground() {
|
||||
}
|
||||
|
||||
export function currentTheme(): "dark" | "light" {
|
||||
return currentThemeId() === "light" ? "light" : "dark";
|
||||
return themeKind(currentThemeId());
|
||||
}
|
||||
|
||||
/** Themes offered by quick switch menus, in IDEA's order. */
|
||||
export const THEME_CHOICES: ReadonlyArray<{ id: "light" | "dark" | "dark-hc"; label: string }> = [
|
||||
{ id: "light", label: "Islands Light" },
|
||||
{ id: "dark", label: "Islands Dark" },
|
||||
{ id: "dark-hc", label: "High Contrast" },
|
||||
];
|
||||
export const THEME_CHOICES = BUILTIN_THEMES;
|
||||
|
||||
/** The concrete theme in effect (`dark-hc` kept distinct). */
|
||||
export function currentThemeId(): "dark" | "light" | "dark-hc" {
|
||||
export function currentThemeId(): BuiltinThemeId {
|
||||
const override = useThemeOverride.getState().theme;
|
||||
return override
|
||||
? resolveThemeId({ theme: override }, prefersLight())
|
||||
|
||||
Reference in new issue
Заблокувати користувача