diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index f30b6b4..34fe7cc 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -24,7 +24,7 @@ export default function RootLayout({
return (
diff --git a/src/components/settings-dialog.tsx b/src/components/settings-dialog.tsx
index 6ffa439..201bf4d 100644
--- a/src/components/settings-dialog.tsx
+++ b/src/components/settings-dialog.tsx
@@ -187,6 +187,17 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
}
}, []);
+ // Apply or clear custom theme live without restart
+ // Defer application until Save
+ const _applyCustomTheme = useCallback((vars: Record) => {
+ const root = document.documentElement;
+ Object.entries(vars).forEach(([k, v]) => root.style.setProperty(k, v));
+ }, []);
+ const _clearCustomTheme = useCallback(() => {
+ const root = document.documentElement;
+ THEME_VARIABLES.forEach(({ key }) => root.style.removeProperty(key));
+ }, []);
+
const loadPermissions = useCallback(async () => {
setIsLoadingPermissions(true);
try {
@@ -281,6 +292,26 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
try {
await invoke("save_app_settings", { settings });
setTheme(settings.theme === "custom" ? "dark" : settings.theme);
+ // Apply or clear custom variables only on Save
+ if (settings.theme === "custom") {
+ if (settings.custom_theme) {
+ try {
+ const root = document.documentElement;
+ // Clear any previous custom vars first
+ THEME_VARIABLES.forEach(({ key }) =>
+ root.style.removeProperty(key),
+ );
+ Object.entries(settings.custom_theme).forEach(([k, v]) =>
+ root.style.setProperty(k, v),
+ );
+ } catch {}
+ }
+ } else {
+ try {
+ const root = document.documentElement;
+ THEME_VARIABLES.forEach(({ key }) => root.style.removeProperty(key));
+ } catch {}
+ }
setOriginalSettings(settings);
onClose();
} catch (error) {
@@ -418,7 +449,7 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
@@ -432,17 +463,12 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
onColorChange={([r, g, b, a]) => {
const next = Color({ r, g, b }).alpha(a);
const nextStr = next.hexa();
- updateSetting("custom_theme", {
+ const nextTheme = {
...(settings.custom_theme ?? {}),
[key]: nextStr,
- });
- // Live preview
- try {
- document.documentElement.style.setProperty(
- key,
- nextStr,
- );
- } catch {}
+ } as Record;
+ updateSetting("custom_theme", nextTheme);
+ // No live preview; applied on Save
}}
>
diff --git a/src/components/theme-provider.tsx b/src/components/theme-provider.tsx
index 3501b7f..4a0f7be 100644
--- a/src/components/theme-provider.tsx
+++ b/src/components/theme-provider.tsx
@@ -29,16 +29,7 @@ export function CustomThemeProvider({ children }: CustomThemeProviderProps) {
const { invoke } = await import("@tauri-apps/api/core");
const settings = await invoke("get_app_settings");
const themeValue = settings?.theme ?? "system";
- if (themeValue === "custom") {
- setDefaultTheme("light");
- const vars = settings.custom_theme ?? {};
- try {
- const root = document.documentElement;
- Object.entries(vars).forEach(([k, v]) => {
- root.style.setProperty(k, v);
- });
- } catch {}
- } else if (
+ if (
themeValue === "light" ||
themeValue === "dark" ||
themeValue === "system"
diff --git a/src/components/ui/color-picker.tsx b/src/components/ui/color-picker.tsx
index 914b4ff..188370e 100644
--- a/src/components/ui/color-picker.tsx
+++ b/src/components/ui/color-picker.tsx
@@ -70,41 +70,74 @@ export const ColorPicker = ({
children,
...props
}: ColorPickerProps) => {
- const selectedColor = Color(value);
+ const selectedColor = Color(value ?? defaultValue);
const defaultColor = Color(defaultValue);
- const [hue, setHue] = useState(
- selectedColor.hue() || defaultColor.hue() || 0,
- );
- const [saturation, setSaturation] = useState(
- selectedColor.saturationl() || defaultColor.saturationl() || 100,
- );
- const [lightness, setLightness] = useState(
- selectedColor.lightness() || defaultColor.lightness() || 50,
- );
- const [alpha, setAlpha] = useState(
- selectedColor.alpha() * 100 || defaultColor.alpha() * 100,
- );
+ const initialHue = Number.isFinite(selectedColor.hue())
+ ? selectedColor.hue()
+ : Number.isFinite(defaultColor.hue())
+ ? defaultColor.hue()
+ : 0;
+ const initialSaturation = Number.isFinite(selectedColor.saturationl())
+ ? selectedColor.saturationl()
+ : Number.isFinite(defaultColor.saturationl())
+ ? defaultColor.saturationl()
+ : 100;
+ const initialLightness = Number.isFinite(selectedColor.lightness())
+ ? selectedColor.lightness()
+ : Number.isFinite(defaultColor.lightness())
+ ? defaultColor.lightness()
+ : 50;
+ const initialAlpha = Number.isFinite(selectedColor.alpha())
+ ? Math.round(selectedColor.alpha() * 100)
+ : Math.round(defaultColor.alpha() * 100);
+
+ const [hue, setHue] = useState(initialHue);
+ const [saturation, setSaturation] = useState(initialSaturation);
+ const [lightness, setLightness] = useState(initialLightness);
+ const [alpha, setAlpha] = useState(initialAlpha);
const [mode, setMode] = useState("hex");
+ const lastEmittedRef = useRef(
+ `${Math.round(initialHue)}|${Math.round(initialSaturation)}|${Math.round(initialLightness)}|${Math.round(initialAlpha)}`,
+ );
// Update color when controlled value changes
useEffect(() => {
- if (value) {
- const color = Color.rgb(value).rgb().object();
+ if (value !== undefined) {
+ const c = Color(value).hsl();
+ const nextHue = Number.isFinite(c.hue()) ? c.hue() : 0;
+ const nextSat = Number.isFinite(c.saturationl()) ? c.saturationl() : 0;
+ const nextLight = Number.isFinite(c.lightness()) ? c.lightness() : 0;
+ const nextAlpha = Math.round(
+ (Number.isFinite(c.alpha()) ? c.alpha() : 1) * 100,
+ );
- setHue(color.r);
- setSaturation(color.g);
- setLightness(color.b);
- setAlpha(color.a);
+ // Only update internal state if it actually changed
+ if (
+ Math.round(nextHue) !== Math.round(hue) ||
+ Math.round(nextSat) !== Math.round(saturation) ||
+ Math.round(nextLight) !== Math.round(lightness) ||
+ Math.round(nextAlpha) !== Math.round(alpha)
+ ) {
+ setHue(nextHue);
+ setSaturation(nextSat);
+ setLightness(nextLight);
+ setAlpha(nextAlpha);
+ }
}
- }, [value]);
+ }, [value, alpha, hue, lightness, saturation]);
// Notify parent of changes
useEffect(() => {
if (onColorChange) {
+ const key = `${Math.round(hue)}|${Math.round(saturation)}|${Math.round(lightness)}|${Math.round(alpha)}`;
+ if (key === lastEmittedRef.current) {
+ return;
+ }
+ lastEmittedRef.current = key;
+
const color = Color.hsl(hue, saturation, lightness).alpha(alpha / 100);
const rgba = color.rgb().array();
-
onColorChange([rgba[0], rgba[1], rgba[2], alpha / 100]);
}
}, [hue, saturation, lightness, alpha, onColorChange]);
@@ -191,7 +224,7 @@ export const ColorPickerSelection = memo(
return (
{
e.preventDefault();
setIsDragging(true);