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:
@@ -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<ImportedTheme | null>(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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<LocalThemePrefs>): void {
|
||||
if (typeof window === "undefined") return
|
||||
try {
|
||||
const existing = getLocalThemePrefs()
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify({ ...existing, ...update }))
|
||||
} catch {}
|
||||
}
|
||||
Reference in New Issue
Block a user