feat(plugins): add theme contributions
Adds plugin-contributed light and dark app themes, including persistence, host-aware coalescing, fallback behavior, documentation, examples, and browser/iOS QA.
This commit is contained in:
@@ -17,6 +17,7 @@ import {
|
||||
DEFAULT_CLIENT_SETTINGS,
|
||||
DEFAULT_CODE_FONT_SIZE,
|
||||
DEFAULT_TERMINAL_SCROLLBACK_LINES,
|
||||
DEFAULT_THEME_PREFERENCE,
|
||||
DEFAULT_UI_BASE_FONT_SIZE,
|
||||
MAX_CODE_FONT_SIZE,
|
||||
MAX_TERMINAL_SCROLLBACK_LINES,
|
||||
@@ -49,6 +50,7 @@ export {
|
||||
DEFAULT_CLIENT_SETTINGS,
|
||||
DEFAULT_CODE_FONT_SIZE,
|
||||
DEFAULT_TERMINAL_SCROLLBACK_LINES,
|
||||
DEFAULT_THEME_PREFERENCE,
|
||||
DEFAULT_UI_BASE_FONT_SIZE,
|
||||
MAX_CODE_FONT_SIZE,
|
||||
MAX_TERMINAL_SCROLLBACK_LINES,
|
||||
|
||||
@@ -403,6 +403,21 @@ describe("saveAppSettings", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("persists a selected plugin theme", async () => {
|
||||
const deps = makeDeps();
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
await saveAppSettings({
|
||||
queryClient,
|
||||
updates: { theme: "plugin", pluginThemeId: "catppuccin/theme/mocha" },
|
||||
deps,
|
||||
});
|
||||
|
||||
const loaded = await loadAppSettingsFromStorage(deps);
|
||||
expect(loaded.theme).toBe("plugin");
|
||||
expect(loaded.pluginThemeId).toBe("catppuccin/theme/mocha");
|
||||
});
|
||||
|
||||
// The row items are written as one object through one strict schema, so an item the schema
|
||||
// does not know does not just fail to persist itself — it takes every sibling toggle with it.
|
||||
it.each(SIDEBAR_ROW_ITEMS)("persists the %s row item being switched off", async (item) => {
|
||||
|
||||
@@ -15,7 +15,12 @@ import {
|
||||
type SidebarRowItems,
|
||||
} from "@/components/sidebar/display-preferences/row-items";
|
||||
import { isNative } from "@/constants/platform";
|
||||
import { FONT_SIZE, THEME_OPTIONS, type ThemePreference } from "@/styles/theme";
|
||||
import {
|
||||
FONT_SIZE,
|
||||
PLUGIN_THEME_PREFERENCE,
|
||||
THEME_OPTIONS,
|
||||
type ThemePreference,
|
||||
} from "@/styles/theme";
|
||||
import { z } from "zod";
|
||||
import { readValidatedJson } from "@/storage/validated-storage";
|
||||
import { APP_SETTINGS_KEY, LEGACY_SETTINGS_KEY } from "./keys";
|
||||
@@ -32,8 +37,13 @@ export type WorkspaceTitleSource = "title" | "branch";
|
||||
export type SidebarWorkspaceTrailing = "diff" | "timestamp" | "none";
|
||||
export type ToolCallDetailLevel = "overview" | "detailed";
|
||||
|
||||
const VALID_THEMES = new Set<string>(THEME_OPTIONS.map((option) => option.name));
|
||||
const ThemePreferenceSchema = z.enum(THEME_OPTIONS.map((option) => option.name));
|
||||
const ThemePreferenceSchema = z.enum([
|
||||
...THEME_OPTIONS.map((option) => option.name),
|
||||
PLUGIN_THEME_PREFERENCE,
|
||||
]);
|
||||
const VALID_THEMES = new Set<string>(ThemePreferenceSchema.options);
|
||||
/** Where the theme picker lands when the persisted preference cannot be honoured. */
|
||||
export const DEFAULT_THEME_PREFERENCE = "auto" satisfies ThemePreference;
|
||||
const VALID_SERVICE_URL_BEHAVIORS = new Set<ServiceUrlBehavior>(["ask", "in-app", "external"]);
|
||||
const VALID_WORKSPACE_TITLE_SOURCES = new Set<WorkspaceTitleSource>(["title", "branch"]);
|
||||
const VALID_SIDEBAR_WORKSPACE_TRAILINGS = new Set<SidebarWorkspaceTrailing>([
|
||||
@@ -59,6 +69,8 @@ export const MAX_FONT_FAMILY_LENGTH = 200;
|
||||
|
||||
export interface AppSettings {
|
||||
theme: ThemePreference;
|
||||
/** Which contributed theme `theme: "plugin"` selects. */
|
||||
pluginThemeId: string | null;
|
||||
language: AppLanguage;
|
||||
sendBehavior: SendBehavior;
|
||||
serviceUrlBehavior: ServiceUrlBehavior;
|
||||
@@ -103,6 +115,7 @@ const SidebarRowItemsSchema = z.strictObject({
|
||||
|
||||
const StoredAppSettingsSchema = z.strictObject({
|
||||
theme: ThemePreferenceSchema.optional(),
|
||||
pluginThemeId: z.string().nullish(),
|
||||
language: z
|
||||
.enum(["system", "ar", "en", "es", "fr", "ja", "ko", "pt-BR", "ru", "zh-CN"])
|
||||
.optional(),
|
||||
@@ -137,7 +150,8 @@ const LegacyRendererSettingsSchema = StoredAppSettingsSchema;
|
||||
type StoredAppSettings = z.infer<typeof StoredAppSettingsSchema>;
|
||||
|
||||
export const DEFAULT_CLIENT_SETTINGS: AppSettings = {
|
||||
theme: "auto",
|
||||
theme: DEFAULT_THEME_PREFERENCE,
|
||||
pluginThemeId: null,
|
||||
language: "system",
|
||||
sendBehavior: "steer",
|
||||
serviceUrlBehavior: "ask",
|
||||
@@ -369,6 +383,9 @@ function pickEnumAppSettings(stored: StoredAppSettings): Partial<AppSettings> {
|
||||
function pickAppSettings(stored: StoredAppSettings): Partial<AppSettings> {
|
||||
const result: Partial<AppSettings> = {};
|
||||
Object.assign(result, pickEnumAppSettings(stored));
|
||||
if (typeof stored.pluginThemeId === "string") {
|
||||
result.pluginThemeId = stored.pluginThemeId;
|
||||
}
|
||||
if (stored.sidebarRowItems !== undefined) {
|
||||
result.sidebarRowItems = parseSidebarRowItems(stored.sidebarRowItems);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user