diff --git a/src/components/theme-customizer/index.tsx b/src/components/theme-customizer/index.tsx index eb14914..054cb84 100644 --- a/src/components/theme-customizer/index.tsx +++ b/src/components/theme-customizer/index.tsx @@ -11,6 +11,7 @@ import { useSidebarConfig } from '@/contexts/sidebar-context' import { tweakcnThemes } from '@/config/theme-data' import { saveThemePrefsAction } from '@/lib/appwrite/theme-prefs-actions' import type { ThemePrefs } from '@/lib/appwrite/theme-prefs-actions' +import { getLocalThemePrefs, saveLocalThemePrefs } from '@/lib/local-theme-prefs' import { ThemeTab } from './theme-tab' import { LayoutTab } from './layout-tab' import { ImportModal } from './import-modal' @@ -29,9 +30,18 @@ export function ThemeCustomizer({ open, onOpenChange, initialPrefs }: ThemeCusto const { config: sidebarConfig, updateConfig: updateSidebarConfig } = useSidebarConfig() const [activeTab, setActiveTab] = React.useState("theme") - const [selectedTheme, setSelectedTheme] = React.useState(initialPrefs.colorTheme ?? "default") - const [selectedTweakcnTheme, setSelectedTweakcnTheme] = React.useState(initialPrefs.tweakcnTheme ?? "") - const [selectedRadius, setSelectedRadius] = React.useState(initialPrefs.radius ?? "0.5rem") + const [selectedTheme, setSelectedTheme] = React.useState(() => { + const local = getLocalThemePrefs() + return local.colorTheme ?? initialPrefs.colorTheme ?? "default" + }) + const [selectedTweakcnTheme, setSelectedTweakcnTheme] = React.useState(() => { + const local = getLocalThemePrefs() + return local.tweakcnTheme ?? initialPrefs.tweakcnTheme ?? "" + }) + const [selectedRadius, setSelectedRadius] = React.useState(() => { + const local = getLocalThemePrefs() + return local.radius ?? initialPrefs.radius ?? "0.5rem" + }) const [importModalOpen, setImportModalOpen] = React.useState(false) const [importedTheme, setImportedTheme] = React.useState(null) @@ -42,10 +52,15 @@ export function ThemeCustomizer({ open, onOpenChange, initialPrefs }: ThemeCusto void saveThemePrefsAction({ theme }) }, [theme]) - // Save sidebar config to Appwrite when it changes (skip first mount) + // Save sidebar config to localStorage + Appwrite when it changes (skip first mount) const sidebarMountRef = React.useRef(false) React.useEffect(() => { if (!sidebarMountRef.current) { sidebarMountRef.current = true; return } + saveLocalThemePrefs({ + sidebarVariant: sidebarConfig.variant, + sidebarCollapsible: sidebarConfig.collapsible, + sidebarSide: sidebarConfig.side, + }) void saveThemePrefsAction({ sidebarVariant: sidebarConfig.variant, sidebarCollapsible: sidebarConfig.collapsible, @@ -62,6 +77,7 @@ export function ThemeCustomizer({ open, onOpenChange, initialPrefs }: ThemeCusto resetTheme() applyRadius("0.5rem") updateSidebarConfig({ variant: "inset", collapsible: "offcanvas", side: "left" }) + saveLocalThemePrefs({ colorTheme: "default", tweakcnTheme: "", radius: "0.5rem", sidebarVariant: "inset", sidebarCollapsible: "offcanvas", sidebarSide: "left" }) void saveThemePrefsAction({ colorTheme: "default", tweakcnTheme: "", radius: "0.5rem" }) } @@ -158,17 +174,20 @@ export function ThemeCustomizer({ open, onOpenChange, initialPrefs }: ThemeCusto setSelectedTheme={(value) => { setSelectedTheme(value) setSelectedTweakcnTheme("") + saveLocalThemePrefs({ colorTheme: value, tweakcnTheme: "" }) void saveThemePrefsAction({ colorTheme: value, tweakcnTheme: "" }) }} selectedTweakcnTheme={selectedTweakcnTheme} setSelectedTweakcnTheme={(value) => { setSelectedTweakcnTheme(value) setSelectedTheme("") + saveLocalThemePrefs({ tweakcnTheme: value, colorTheme: "" }) void saveThemePrefsAction({ tweakcnTheme: value, colorTheme: "" }) }} selectedRadius={selectedRadius} setSelectedRadius={(value) => { setSelectedRadius(value) + saveLocalThemePrefs({ radius: value }) void saveThemePrefsAction({ radius: value }) }} setImportedTheme={setImportedTheme} diff --git a/src/components/theme-customizer/prefs-initializer.tsx b/src/components/theme-customizer/prefs-initializer.tsx index 93d2902..d3c9fbd 100644 --- a/src/components/theme-customizer/prefs-initializer.tsx +++ b/src/components/theme-customizer/prefs-initializer.tsx @@ -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 diff --git a/src/lib/local-theme-prefs.ts b/src/lib/local-theme-prefs.ts new file mode 100644 index 0000000..65f0240 --- /dev/null +++ b/src/lib/local-theme-prefs.ts @@ -0,0 +1,28 @@ +const STORAGE_KEY = "isletmem-theme-prefs" + +export interface LocalThemePrefs { + colorTheme?: string + tweakcnTheme?: string + radius?: string + sidebarVariant?: string + sidebarCollapsible?: string + sidebarSide?: string +} + +export function getLocalThemePrefs(): LocalThemePrefs { + if (typeof window === "undefined") return {} + try { + const raw = localStorage.getItem(STORAGE_KEY) + return raw ? (JSON.parse(raw) as LocalThemePrefs) : {} + } catch { + return {} + } +} + +export function saveLocalThemePrefs(update: Partial): void { + if (typeof window === "undefined") return + try { + const existing = getLocalThemePrefs() + localStorage.setItem(STORAGE_KEY, JSON.stringify({ ...existing, ...update })) + } catch {} +}