feat(theme): add custom UI color palettes
This commit is contained in:
1 parent
c81466d0db
commit
8996da7d9a
15 files changed
+618
-37
No files matched your search
@@ -107,4 +107,45 @@ describe("useTheme on Linux", () => {
|
||||
await flushDynamicImport();
|
||||
expect(setTheme).toHaveBeenLastCalledWith("light");
|
||||
});
|
||||
|
||||
it("injects custom UI colors as inline CSS variables and removes them when leaving the custom palette", async () => {
|
||||
const theme = await loadTheme();
|
||||
theme.setThemePalette("custom");
|
||||
await flushDynamicImport();
|
||||
theme.setCustomUiColors({ ...theme.customUiColors.value, background: "#123456", primary: "#abcdef" });
|
||||
|
||||
expect(theme.themePalette.value).toBe("custom");
|
||||
expect(document.documentElement.style.getPropertyValue("--background")).toBe("rgb(18 52 86)");
|
||||
expect(document.documentElement.style.getPropertyValue("--primary")).toBe("rgb(171 205 239)");
|
||||
expect(document.documentElement.style.getPropertyValue("--dbx-primary-rgb")).toBe("171, 205, 239");
|
||||
expect(document.documentElement.style.getPropertyValue("--sidebar")).toBe("rgb(250 250 250)");
|
||||
// Derived paired foregrounds are applied and readable against their surfaces.
|
||||
expect(document.documentElement.style.getPropertyValue("--card-foreground")).toBe("rgb(10 10 10)");
|
||||
expect(document.documentElement.style.getPropertyValue("--primary-foreground")).toBe("rgb(10 10 10)");
|
||||
|
||||
theme.setThemePalette("pearl");
|
||||
await flushDynamicImport();
|
||||
expect(document.documentElement.style.getPropertyValue("--background")).toBe("");
|
||||
expect(document.documentElement.style.getPropertyValue("--dbx-primary-rgb")).toBe("");
|
||||
});
|
||||
|
||||
it("applies a separate custom color set in dark mode and switches sets with the mode", async () => {
|
||||
const theme = await loadTheme("dark");
|
||||
theme.setThemePalette("custom");
|
||||
await flushDynamicImport();
|
||||
|
||||
// Dark mode uses the dark custom set by default (not the light defaults).
|
||||
expect(document.documentElement.style.getPropertyValue("--background")).toBe("rgb(19 20 22)");
|
||||
expect(document.documentElement.style.getPropertyValue("--foreground")).toBe("rgb(215 215 219)");
|
||||
|
||||
theme.setCustomUiColors({ ...theme.customUiColorsDark.value, background: "#1a1b1e" });
|
||||
expect(document.documentElement.style.getPropertyValue("--background")).toBe("rgb(26 27 30)");
|
||||
|
||||
// Switching to light mode re-applies the light custom set.
|
||||
theme.setThemeMode("light");
|
||||
await flushDynamicImport();
|
||||
expect(document.documentElement.style.getPropertyValue("--background")).toBe("rgb(255 255 255)");
|
||||
expect(theme.customUiColors.value.background).toBe("#ffffff");
|
||||
expect(theme.customUiColorsDark.value.background).toBe("#1a1b1e");
|
||||
});
|
||||
});
|
||||
@@ -1,16 +1,26 @@
|
||||
import { computed, ref } from "vue";
|
||||
import {
|
||||
APP_CORNER_STYLE_STORAGE_KEY,
|
||||
APP_CUSTOM_UI_COLOR_DEFS,
|
||||
APP_CUSTOM_UI_DARK_STORAGE_KEY,
|
||||
APP_CUSTOM_UI_DERIVED_VAR_NAMES,
|
||||
APP_CUSTOM_UI_STORAGE_KEY,
|
||||
deriveCustomUiColors,
|
||||
APP_THEME_PALETTE_CLASS_NAMES,
|
||||
APP_THEME_PALETTE_STORAGE_KEY,
|
||||
APP_THEME_STORAGE_KEY,
|
||||
DEFAULT_APP_CUSTOM_UI_COLORS,
|
||||
DEFAULT_APP_CUSTOM_UI_COLORS_DARK,
|
||||
appCustomUiColorValue,
|
||||
getAppThemePaletteClass,
|
||||
getTauriThemeForMode,
|
||||
isSystemAppThemeMode,
|
||||
normalizeAppCustomUiColors,
|
||||
normalizeAppThemeMode,
|
||||
normalizeAppCornerStyle,
|
||||
normalizeAppThemePalette,
|
||||
resolveAppThemeAppearance,
|
||||
type AppCustomUiColors,
|
||||
type AppThemeMode,
|
||||
type AppThemePalette,
|
||||
type AppCornerStyle,
|
||||
@@ -26,6 +36,17 @@ const savedThemeMode = safeLocalStorageGet(APP_THEME_STORAGE_KEY);
|
||||
const themeMode = ref<AppThemeMode>(normalizeAppThemeMode(savedThemeMode));
|
||||
const savedThemePalette = safeLocalStorageGet(APP_THEME_PALETTE_STORAGE_KEY);
|
||||
const themePalette = ref<AppThemePalette>(normalizeAppThemePalette(savedThemePalette));
|
||||
function parseStoredCustomUiColors(raw: string | null): AppCustomUiColors | null {
|
||||
if (!raw) return null;
|
||||
try {
|
||||
return normalizeAppCustomUiColors(JSON.parse(raw));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
const customUiColors = ref<AppCustomUiColors>(parseStoredCustomUiColors(safeLocalStorageGet(APP_CUSTOM_UI_STORAGE_KEY)) ?? DEFAULT_APP_CUSTOM_UI_COLORS);
|
||||
const customUiColorsDark = ref<AppCustomUiColors>(parseStoredCustomUiColors(safeLocalStorageGet(APP_CUSTOM_UI_DARK_STORAGE_KEY)) ?? DEFAULT_APP_CUSTOM_UI_COLORS_DARK);
|
||||
const activeCustomUiColors = computed<AppCustomUiColors>(() => (isDark.value ? customUiColorsDark.value : customUiColors.value));
|
||||
const savedCornerStyle = safeLocalStorageGet(APP_CORNER_STYLE_STORAGE_KEY);
|
||||
const cornerStyle = ref<AppCornerStyle>(normalizeAppCornerStyle(savedCornerStyle));
|
||||
if (savedThemeMode && savedThemeMode !== themeMode.value) safeLocalStorageSet(APP_THEME_STORAGE_KEY, themeMode.value);
|
||||
@@ -67,6 +88,7 @@ function applyTheme() {
|
||||
if (paletteClass) doc.classList.add(paletteClass);
|
||||
doc.dataset.cornerStyle = cornerStyle.value;
|
||||
doc.style.colorScheme = dark ? "dark" : "light";
|
||||
applyCustomUiColors();
|
||||
|
||||
// force reflow so the class toggle takes effect before re-enabling transitions
|
||||
doc.offsetHeight; // eslint-disable-line @typescript-eslint/no-unused-expressions
|
||||
@@ -106,6 +128,45 @@ function setThemePalette(palette: AppThemePalette) {
|
||||
applyTheme();
|
||||
}
|
||||
|
||||
function applyCustomUiColors() {
|
||||
if (typeof document === "undefined") return;
|
||||
const doc = document.documentElement;
|
||||
const clear = () => {
|
||||
for (const def of APP_CUSTOM_UI_COLOR_DEFS) {
|
||||
doc.style.removeProperty(def.varName);
|
||||
if (def.rgbVarName) doc.style.removeProperty(def.rgbVarName);
|
||||
}
|
||||
for (const name of APP_CUSTOM_UI_DERIVED_VAR_NAMES) doc.style.removeProperty(name);
|
||||
};
|
||||
if (themePalette.value !== "custom") {
|
||||
clear();
|
||||
return;
|
||||
}
|
||||
for (const def of APP_CUSTOM_UI_COLOR_DEFS) {
|
||||
const { color, rgbTriplet } = appCustomUiColorValue(activeCustomUiColors.value[def.key]);
|
||||
doc.style.setProperty(def.varName, color);
|
||||
if (def.rgbVarName) doc.style.setProperty(def.rgbVarName, rgbTriplet);
|
||||
}
|
||||
const derived = deriveCustomUiColors(activeCustomUiColors.value);
|
||||
for (const name of APP_CUSTOM_UI_DERIVED_VAR_NAMES) doc.style.setProperty(name, derived[name]);
|
||||
}
|
||||
|
||||
function setCustomUiColors(colors: AppCustomUiColors) {
|
||||
const next = normalizeAppCustomUiColors(colors);
|
||||
if (isDark.value) {
|
||||
customUiColorsDark.value = next;
|
||||
safeLocalStorageSet(APP_CUSTOM_UI_DARK_STORAGE_KEY, JSON.stringify(next));
|
||||
} else {
|
||||
customUiColors.value = next;
|
||||
safeLocalStorageSet(APP_CUSTOM_UI_STORAGE_KEY, JSON.stringify(next));
|
||||
}
|
||||
applyCustomUiColors();
|
||||
}
|
||||
|
||||
function resetCustomUiColors() {
|
||||
setCustomUiColors({ ...(isDark.value ? DEFAULT_APP_CUSTOM_UI_COLORS_DARK : DEFAULT_APP_CUSTOM_UI_COLORS) });
|
||||
}
|
||||
|
||||
function setCornerStyle(style: AppCornerStyle) {
|
||||
cornerStyle.value = normalizeAppCornerStyle(style);
|
||||
safeLocalStorageSet(APP_CORNER_STYLE_STORAGE_KEY, cornerStyle.value);
|
||||
@@ -119,5 +180,5 @@ export function useTheme() {
|
||||
setThemeMode(isDark.value ? "light" : "dark");
|
||||
}
|
||||
|
||||
return { isDark, themeMode, themePalette, cornerStyle, applyTheme, setThemeMode, setThemePalette, setCornerStyle, toggleTheme };
|
||||
return { isDark, themeMode, themePalette, customUiColors, customUiColorsDark, activeCustomUiColors, cornerStyle, applyTheme, setThemeMode, setThemePalette, setCustomUiColors, resetCustomUiColors, setCornerStyle, toggleTheme };
|
||||
}
|
||||
Reference in new issue
Block a user