дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +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
@@ -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,
|
||||
},
|
||||
{
|
||||
|
||||
Reference in new issue
Заблокувати користувача