@ -3,11 +3,24 @@ import midnightThemeContent from "../themes/midnight.css?raw";
import paperThemeContent from "../themes/paper.css?raw" ;
import whitewallThemeContent from "../themes/whitewall.css?raw" ;
// ============================================================================
// Types and Constants
// ============================================================================
const VALID_THEMES = [ "system" , "default" , "default-dark" , "midnight" , "paper" , "whitewall" ] as const ;
type ValidTheme = ( typeof VALID_THEMES ) [ number ] ;
const THEME_CONTENT : Record < ValidTheme , string | null > = {
system : null , // System theme dynamically chooses between default and default-dark
export type Theme = ( typeof VALID_THEMES ) [ number ] ;
export type ResolvedTheme = Exclude < Theme , " system " > ;
export interface ThemeOption {
value : string ;
label : string ;
}
const STORAGE_KEY = "memos-theme" ;
const STYLE_ELEMENT_ID = "instance-theme" ;
const THEME_CONTENT : Record < ResolvedTheme , string | null > = {
default : null ,
"default-dark" : defaultDarkThemeContent ,
midnight : midnightThemeContent ,
@ -15,11 +28,6 @@ const THEME_CONTENT: Record<ValidTheme, string | null> = {
whitewall : whitewallThemeContent ,
} ;
export interface ThemeOption {
value : string ;
label : string ;
}
export const THEME_OPTIONS : ThemeOption [ ] = [
{ value : "system" , label : "Sync with system" } ,
{ value : "default" , label : "Light" } ,
@ -29,102 +37,201 @@ export const THEME_OPTIONS: ThemeOption[] = [
{ value : "whitewall" , label : "Whitewall" } ,
] ;
const validateTheme = ( theme : string ) : ValidTheme = > {
return VALID_THEMES . includes ( theme as ValidTheme ) ? ( theme as ValidTheme ) : "default" ;
// ============================================================================
// Theme Validation and Detection
// ============================================================================
/ * *
* Validates and normalizes a theme string to a valid theme .
* Falls back to "default" for invalid themes .
* /
const validateTheme = ( theme : string ) : Theme = > {
return VALID_THEMES . includes ( theme as Theme ) ? ( theme as Theme ) : "default" ;
} ;
export const getSystemTheme = ( ) : "default" | "default-dark" = > {
if ( typeof window !== "undefined" && window . matchMedia ) {
return window . matchMedia ( "(prefers-color-scheme: dark)" ) . matches ? "default-dark" : "default" ;
/ * *
* Detects the system ' s preferred color scheme .
* @returns "default-dark" for dark mode , "default" for light mode
* /
export const getSystemTheme = ( ) : ResolvedTheme = > {
if ( typeof window !== "undefined" && window . matchMedia ? . ( "(prefers-color-scheme: dark)" ) . matches ) {
return "default-dark" ;
}
return "default" ;
} ;
// Resolves "system" to actual theme based on OS preference
export const resolveTheme = ( theme : string ) : "default" | "default-dark" | "midnight" | "paper" | "whitewall" = > {
if ( theme === "system" ) {
return getSystemTheme ( ) ;
}
/ * *
* Resolves "system" theme to the actual theme based on OS preference .
* Other themes are returned as - is after validation .
* /
export const resolveTheme = ( theme : string ) : ResolvedTheme = > {
const validTheme = validateTheme ( theme ) ;
return validTheme === "system" ? getSystemTheme ( ) : validTheme ;
} ;
// Gets the theme that should be applied on initial load
export const getInitialTheme = ( ) : ValidTheme = > {
// Try to get stored theme from localStorage
// ============================================================================
// LocalStorage Helpers
// ============================================================================
/ * *
* Safely reads the theme from localStorage .
* @returns The stored theme , or null if not found or unavailable
* /
const getStoredTheme = ( ) : Theme | null = > {
try {
const storedTheme = localStorage . getItem ( "memos-theme" ) ;
if ( storedTheme && VALID_THEMES . includes ( storedTheme as ValidTheme ) ) {
return storedTheme as ValidTheme ;
}
const stored = localStorage . getItem ( STORAGE_KEY ) ;
return stored && VALID_THEMES . includes ( stored as Theme ) ? ( stored as Theme ) : null ;
} catch {
// localStorage might not be available
return null ;
}
} ;
/ * *
* Safely stores the theme to localStorage .
* /
const setStoredTheme = ( theme : Theme ) : void = > {
try {
localStorage . setItem ( STORAGE_KEY , theme ) ;
} catch {
// localStorage might not be available (SSR, private browsing, etc.)
}
} ;
// ============================================================================
// Theme Selection with Fallbacks
// ============================================================================
/ * *
* Gets the theme for initial page load ( before user settings are available ) .
* Priority : localStorage - > system preference
* /
export const getInitialTheme = ( ) : Theme = > {
return getStoredTheme ( ) ? ? "system" ;
} ;
/ * *
* Gets the theme with full fallback chain .
* Priority :
* 1 . User setting ( if logged in and has preference )
* 2 . localStorage ( from previous session )
* 3 . System preference
* /
export const getThemeWithFallback = ( userTheme? : string ) : Theme = > {
// Priority 1: User setting
if ( userTheme && VALID_THEMES . includes ( userTheme as Theme ) ) {
return userTheme as Theme ;
}
// Priority 2: localStorage
const stored = getStoredTheme ( ) ;
if ( stored ) {
return stored ;
}
// Priority 3: System preference
return "system" ;
} ;
// Applies the theme early to prevent flash of wrong theme
export const applyThemeEarly = ( ) : void = > {
const theme = getInitialTheme ( ) ;
loadTheme ( theme ) ;
// ============================================================================
// DOM Manipulation
// ============================================================================
/ * *
* Removes the existing theme style element from the DOM .
* /
const removeThemeStyle = ( ) : void = > {
document . getElementById ( STYLE_ELEMENT_ID ) ? . remove ( ) ;
} ;
export const loadTheme = ( themeName : string ) : void = > {
const validTheme = validateTheme ( themeName ) ;
/ * *
* Injects theme CSS into the document head .
* Skips injection for the default theme ( uses base CSS ) .
* /
const injectThemeStyle = ( theme : ResolvedTheme ) : void = > {
removeThemeStyle ( ) ;
// Resolve "system" to actual theme based on OS preference
const resolvedTheme = resolveTheme ( validTheme ) ;
if ( theme === "default" ) {
return ; // Use base CSS for default theme
}
// Remove existing theme
document . getElementById ( "instance-theme" ) ? . remove ( ) ;
// Apply theme (skip for default)
if ( resolvedTheme !== "default" ) {
const css = THEME_CONTENT [ resolvedTheme ] ;
if ( css ) {
const style = document . createElement ( "style" ) ;
style . id = "instance-theme" ;
style . textContent = css ;
document . head . appendChild ( style ) ;
}
const css = THEME_CONTENT [ theme ] ;
if ( css ) {
const style = document . createElement ( "style" ) ;
style . id = STYLE_ELEMENT_ID ;
style . textContent = css ;
document . head . appendChild ( style ) ;
}
} ;
/ * *
* Sets the data - theme attribute on the document element .
* This allows CSS to react to the current theme .
* /
const setThemeAttribute = ( theme : ResolvedTheme ) : void = > {
document . documentElement . setAttribute ( "data-theme" , theme ) ;
} ;
// Set data attribute with resolved theme
document . documentElement . setAttribute ( "data-theme" , resolvedTheme ) ;
// ============================================================================
// Main Theme Loading
// ============================================================================
/ * *
* Loads and applies a theme .
* This function :
* 1 . Validates the theme
* 2 . Resolves "system" to actual theme
* 3 . Injects theme CSS
* 4 . Sets data - theme attribute
* 5 . Persists to localStorage
* /
export const loadTheme = ( themeName : string ) : void = > {
const validTheme = validateTheme ( themeName ) ;
const resolvedTheme = resolveTheme ( validTheme ) ;
// Store theme preference (original, not resolved) for future loads
try {
localStorage . setItem ( "memos-theme" , validTheme ) ;
} catch {
// localStorage might not be available
}
injectThemeStyle ( resolvedTheme ) ;
setThemeAttribute ( resolvedTheme ) ;
setStoredTheme ( validTheme ) ; // Store original theme preference (not resolved)
} ;
// Sets up a listener for system theme preference changes
/ * *
* Applies theme early during initial page load to prevent FOUC .
* Uses only localStorage and system preference ( no user settings yet ) .
* /
export const applyThemeEarly = ( ) : void = > {
const theme = getInitialTheme ( ) ;
loadTheme ( theme ) ;
} ;
// ============================================================================
// System Theme Listener
// ============================================================================
/ * *
* Sets up a listener for OS - level theme preference changes .
* Supports both modern ( addEventListener ) and legacy ( addListener ) APIs .
*
* @param onThemeChange - Callback invoked when system theme changes
* @returns Cleanup function to remove the listener
* /
export const setupSystemThemeListener = ( onThemeChange : ( ) = > void ) : ( ( ) = > void ) = > {
// Guard against SSR
if ( typeof window === "undefined" || ! window . matchMedia ) {
return ( ) = > { } ; // No-op cleanup
return ( ) = > { } ;
}
const mediaQuery = window . matchMedia ( "(prefers-color-scheme: dark)" ) ;
// Handle theme change
const handleChange = ( ) = > {
onThemeChange ( ) ;
} ;
// Modern API (addEventListener)
// Modern API (preferred)
if ( mediaQuery . addEventListener ) {
mediaQuery . addEventListener ( "change" , handleChange ) ;
return ( ) = > mediaQuery . removeEventListener ( "change" , handleChange ) ;
mediaQuery . addEventListener ( "change" , onThemeChange ) ;
return ( ) = > mediaQuery . removeEventListener ( "change" , onThemeChange ) ;
}
// Legacy API (addListener) - for older browsers
// Legacy API ( Safari < 14)
if ( mediaQuery . addListener ) {
mediaQuery . addListener ( handl eChange) ;
return ( ) = > mediaQuery . removeListener ( handl eChange) ;
mediaQuery . addListener ( onThem eChange) ;
return ( ) = > mediaQuery . removeListener ( onThem eChange) ;
}
return ( ) = > { } ; // No-op cleanup
return ( ) = > { } ;
} ;