feat: persist theme prefs in localStorage for reliable same-device persistence

- Add src/lib/local-theme-prefs.ts: read/write color theme, radius, sidebar
  config to localStorage key 'isletmem-theme-prefs'
- ThemeCustomizer: init state from localStorage (falls back to Appwrite prefs),
  save to localStorage on every color/radius/sidebar change in addition to Appwrite
- PrefsInitializer: merge localStorage (wins) with server Appwrite prefs on mount

Appwrite updatePrefs had a silent try/catch so failures were invisible;
dark/light was already in localStorage via ThemeProvider but color theme was not.
This commit is contained in:
kovakmedya
2026-05-08 14:55:29 +03:00
parent e2d09ab138
commit c307865a44
3 changed files with 74 additions and 16 deletions
@@ -7,6 +7,7 @@ import { useTheme } from "@/hooks/use-theme";
import { useThemeManager } from "@/hooks/use-theme-manager";
import { tweakcnThemes } from "@/config/theme-data";
import type { ThemePrefs } from "@/lib/appwrite/theme-prefs-actions";
import { getLocalThemePrefs } from "@/lib/local-theme-prefs";
export function PrefsInitializer({ prefs }: { prefs: ThemePrefs }) {
const { setTheme } = useTheme();
@@ -18,30 +19,40 @@ export function PrefsInitializer({ prefs }: { prefs: ThemePrefs }) {
if (applied.current) return;
applied.current = true;
if (prefs.theme) setTheme(prefs.theme);
// localStorage wins (most recent change on this device); Appwrite prefs are fallback
const local = getLocalThemePrefs();
const effectiveTheme = prefs.theme;
const effectiveColorTheme = local.colorTheme ?? prefs.colorTheme;
const effectiveTweakcnTheme = local.tweakcnTheme ?? prefs.tweakcnTheme;
const effectiveRadius = local.radius ?? prefs.radius;
const effectiveSidebarVariant = (local.sidebarVariant as ThemePrefs["sidebarVariant"]) ?? prefs.sidebarVariant;
const effectiveSidebarCollapsible = (local.sidebarCollapsible as ThemePrefs["sidebarCollapsible"]) ?? prefs.sidebarCollapsible;
const effectiveSidebarSide = (local.sidebarSide as ThemePrefs["sidebarSide"]) ?? prefs.sidebarSide;
if (effectiveTheme) setTheme(effectiveTheme);
const isDark =
prefs.theme === "dark" ||
(prefs.theme !== "light" &&
effectiveTheme === "dark" ||
(effectiveTheme !== "light" &&
typeof window !== "undefined" &&
window.matchMedia("(prefers-color-scheme: dark)").matches);
if (prefs.radius) applyRadius(prefs.radius);
if (effectiveRadius) applyRadius(effectiveRadius);
if (prefs.colorTheme) {
applyTheme(prefs.colorTheme, isDark);
if (effectiveColorTheme) {
applyTheme(effectiveColorTheme, isDark);
}
if (prefs.tweakcnTheme) {
const preset = tweakcnThemes.find((t) => t.value === prefs.tweakcnTheme)?.preset;
if (effectiveTweakcnTheme) {
const preset = tweakcnThemes.find((t) => t.value === effectiveTweakcnTheme)?.preset;
if (preset) applyTweakcnTheme(preset, isDark);
}
if (prefs.sidebarVariant || prefs.sidebarCollapsible || prefs.sidebarSide) {
if (effectiveSidebarVariant || effectiveSidebarCollapsible || effectiveSidebarSide) {
updateConfig({
...(prefs.sidebarVariant && { variant: prefs.sidebarVariant }),
...(prefs.sidebarCollapsible && { collapsible: prefs.sidebarCollapsible }),
...(prefs.sidebarSide && { side: prefs.sidebarSide }),
...(effectiveSidebarVariant && { variant: effectiveSidebarVariant }),
...(effectiveSidebarCollapsible && { collapsible: effectiveSidebarCollapsible }),
...(effectiveSidebarSide && { side: effectiveSidebarSide }),
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps