ui: fix theme and locale review regressions

This commit is contained in:
Val Alexander 2026-03-09 20:02:05 -05:00
parent 7bdf866403
commit 37a914bb88
No known key found for this signature in database
9 changed files with 119 additions and 13 deletions

View File

@ -2,6 +2,7 @@ import type { TranslationMap } from "../lib/types.ts";
export const en: TranslationMap = {
common: {
version: "Version",
health: "Health",
ok: "OK",
offline: "Offline",

View File

@ -11,6 +11,7 @@ export const pt_BR: TranslationMap = {
disabled: "Desativado",
na: "n/a",
docs: "Docs",
theme: "Tema",
resources: "Recursos",
search: "Pesquisar",
},

View File

@ -11,6 +11,7 @@ export const zh_CN: TranslationMap = {
disabled: "已禁用",
na: "不适用",
docs: "文档",
theme: "主题",
resources: "资源",
search: "搜索",
},

View File

@ -11,6 +11,7 @@ export const zh_TW: TranslationMap = {
disabled: "已禁用",
na: "不適用",
docs: "文檔",
theme: "主題",
resources: "資源",
search: "搜尋",
},

View File

@ -128,11 +128,13 @@ describe("loadSettings default gateway URL derivation", () => {
gatewayUrl: "wss://gateway.example:8443/openclaw",
sessionKey: "agent",
lastActiveSessionKey: "agent",
theme: "system",
theme: "claw",
themeMode: "system",
chatFocusMode: false,
chatShowThinking: true,
splitRatio: 0.6,
navCollapsed: false,
navWidth: 220,
navGroupsCollapsed: {},
});
expect(sessionStorage.length).toBe(0);
@ -151,11 +153,13 @@ describe("loadSettings default gateway URL derivation", () => {
token: "session-token",
sessionKey: "main",
lastActiveSessionKey: "main",
theme: "system",
theme: "claw",
themeMode: "system",
chatFocusMode: false,
chatShowThinking: true,
splitRatio: 0.6,
navCollapsed: false,
navWidth: 220,
navGroupsCollapsed: {},
});
@ -178,11 +182,13 @@ describe("loadSettings default gateway URL derivation", () => {
token: "gateway-a-token",
sessionKey: "main",
lastActiveSessionKey: "main",
theme: "system",
theme: "claw",
themeMode: "system",
chatFocusMode: false,
chatShowThinking: true,
splitRatio: 0.6,
navCollapsed: false,
navWidth: 220,
navGroupsCollapsed: {},
});
@ -192,11 +198,13 @@ describe("loadSettings default gateway URL derivation", () => {
gatewayUrl: "wss://other-gateway.example:8443/openclaw",
sessionKey: "main",
lastActiveSessionKey: "main",
theme: "system",
theme: "claw",
themeMode: "system",
chatFocusMode: false,
chatShowThinking: true,
splitRatio: 0.6,
navCollapsed: false,
navWidth: 220,
navGroupsCollapsed: {},
}),
);
@ -220,11 +228,13 @@ describe("loadSettings default gateway URL derivation", () => {
token: "memory-only-token",
sessionKey: "main",
lastActiveSessionKey: "main",
theme: "system",
theme: "claw",
themeMode: "system",
chatFocusMode: false,
chatShowThinking: true,
splitRatio: 0.6,
navCollapsed: false,
navWidth: 220,
navGroupsCollapsed: {},
});
expect(loadSettings()).toMatchObject({
@ -236,11 +246,13 @@ describe("loadSettings default gateway URL derivation", () => {
gatewayUrl: "wss://gateway.example:8443/openclaw",
sessionKey: "main",
lastActiveSessionKey: "main",
theme: "system",
theme: "claw",
themeMode: "system",
chatFocusMode: false,
chatShowThinking: true,
splitRatio: 0.6,
navCollapsed: false,
navWidth: 220,
navGroupsCollapsed: {},
});
expect(sessionStorage.length).toBe(1);
@ -259,11 +271,13 @@ describe("loadSettings default gateway URL derivation", () => {
token: "stale-token",
sessionKey: "main",
lastActiveSessionKey: "main",
theme: "system",
theme: "claw",
themeMode: "system",
chatFocusMode: false,
chatShowThinking: true,
splitRatio: 0.6,
navCollapsed: false,
navWidth: 220,
navGroupsCollapsed: {},
});
saveSettings({
@ -271,15 +285,47 @@ describe("loadSettings default gateway URL derivation", () => {
token: "",
sessionKey: "main",
lastActiveSessionKey: "main",
theme: "system",
theme: "claw",
themeMode: "system",
chatFocusMode: false,
chatShowThinking: true,
splitRatio: 0.6,
navCollapsed: false,
navWidth: 220,
navGroupsCollapsed: {},
});
expect(loadSettings().token).toBe("");
expect(sessionStorage.length).toBe(0);
});
it("persists themeMode and navWidth alongside the selected theme", async () => {
setTestLocation({
protocol: "https:",
host: "gateway.example:8443",
pathname: "/",
});
const { saveSettings } = await import("./storage.ts");
saveSettings({
gatewayUrl: "wss://gateway.example:8443/openclaw",
token: "",
sessionKey: "main",
lastActiveSessionKey: "main",
theme: "dash",
themeMode: "light",
chatFocusMode: false,
chatShowThinking: true,
splitRatio: 0.6,
navCollapsed: false,
navWidth: 320,
navGroupsCollapsed: {},
});
expect(JSON.parse(localStorage.getItem("openclaw.control.settings.v1") ?? "{}")).toMatchObject({
theme: "dash",
themeMode: "light",
navWidth: 320,
});
});
});

View File

@ -19,7 +19,7 @@ export type UiSettings = {
chatShowThinking: boolean;
splitRatio: number; // Sidebar split ratio (0.4 to 0.7, default 0.6)
navCollapsed: boolean; // Collapsible sidebar state
navWidth: number; // Sidebar width when expanded (240400px)
navWidth: number; // Sidebar width when expanded (200400px)
navGroupsCollapsed: Record<string, boolean>; // Which nav groups are collapsed
locale?: string;
};
@ -194,10 +194,12 @@ function persistSettings(next: UiSettings) {
sessionKey: next.sessionKey,
lastActiveSessionKey: next.lastActiveSessionKey,
theme: next.theme,
themeMode: next.themeMode,
chatFocusMode: next.chatFocusMode,
chatShowThinking: next.chatShowThinking,
splitRatio: next.splitRatio,
navCollapsed: next.navCollapsed,
navWidth: next.navWidth,
navGroupsCollapsed: next.navGroupsCollapsed,
...(next.locale ? { locale: next.locale } : {}),
};

View File

@ -9,6 +9,8 @@ export type ThemeTransitionContext = {
export type ThemeTransitionOptions = {
nextTheme: ResolvedTheme;
applyTheme: () => void;
// Retained so callers from stacked slices can keep passing pointer metadata
// while theme switching remains an immediate, non-animated update here.
context?: ThemeTransitionContext;
currentTheme?: ResolvedTheme | null;
};

33
ui/src/ui/theme.test.ts Normal file
View File

@ -0,0 +1,33 @@
import { describe, expect, it, vi } from "vitest";
import { parseThemeSelection, resolveTheme } from "./theme.ts";
describe("resolveTheme", () => {
it("keeps the legacy mode-only signature working for existing callers", () => {
expect(resolveTheme("dark")).toBe("dark");
expect(resolveTheme("light")).toBe("light");
});
it("resolves named theme families when mode is provided", () => {
expect(resolveTheme("knot", "dark")).toBe("openknot");
expect(resolveTheme("dash", "light")).toBe("dash-light");
});
it("uses system preference when a named theme omits mode", () => {
vi.stubGlobal("matchMedia", vi.fn().mockReturnValue({ matches: true }));
expect(resolveTheme("knot")).toBe("openknot-light");
vi.unstubAllGlobals();
});
});
describe("parseThemeSelection", () => {
it("maps legacy stored values onto theme + mode", () => {
expect(parseThemeSelection("system", undefined)).toEqual({
theme: "claw",
mode: "system",
});
expect(parseThemeSelection("fieldmanual", undefined)).toEqual({
theme: "dash",
mode: "dark",
});
});
});

View File

@ -62,12 +62,31 @@ function resolveMode(mode: ThemeMode): "light" | "dark" {
return mode;
}
export function resolveTheme(theme: ThemeName, mode: ThemeMode): ResolvedTheme {
const resolvedMode = resolveMode(mode);
if (theme === "claw") {
function normalizeThemeArgs(
themeOrMode: ThemeName | ThemeMode,
mode: ThemeMode | undefined,
): { theme: ThemeName; mode: ThemeMode } {
if (VALID_THEME_NAMES.has(themeOrMode as ThemeName)) {
return {
theme: themeOrMode as ThemeName,
mode: mode ?? "system",
};
}
return {
theme: "claw",
mode: themeOrMode as ThemeMode,
};
}
export function resolveTheme(mode: ThemeMode): ResolvedTheme;
export function resolveTheme(theme: ThemeName, mode?: ThemeMode): ResolvedTheme;
export function resolveTheme(themeOrMode: ThemeName | ThemeMode, mode?: ThemeMode): ResolvedTheme {
const normalized = normalizeThemeArgs(themeOrMode, mode);
const resolvedMode = resolveMode(normalized.mode);
if (normalized.theme === "claw") {
return resolvedMode === "light" ? "light" : "dark";
}
if (theme === "knot") {
if (normalized.theme === "knot") {
return resolvedMode === "light" ? "openknot-light" : "openknot";
}
return resolvedMode === "light" ? "dash-light" : "dash";