import { create } from 'zustand' import { persist, createJSONStorage } from 'zustand/middleware' import { localStorageKey } from '@/constants/localStorage' import { RgbaColor } from 'react-colorful' import { rgb, oklch, formatCss } from 'culori' import { useTheme } from './useTheme' export type FontSize = '14px' | '15px' | '16px' | '18px' interface AppearanceState { fontSize: FontSize appBgColor: RgbaColor appMainViewBgColor: RgbaColor appPrimaryBgColor: RgbaColor appAccentBgColor: RgbaColor appDestructiveBgColor: RgbaColor appMainViewTextColor: string appPrimaryTextColor: string appAccentTextColor: string appDestructiveTextColor: string appLeftPanelTextColor: string setFontSize: (size: FontSize) => void setAppBgColor: (color: RgbaColor) => void setAppMainViewBgColor: (color: RgbaColor) => void setAppPrimaryBgColor: (color: RgbaColor) => void setAppAccentBgColor: (color: RgbaColor) => void setAppDestructiveBgColor: (color: RgbaColor) => void resetAppearance: () => void } const getBrightness = ({ r, g, b }: RgbaColor) => (r * 299 + g * 587 + b * 114) / 1000 export const fontSizeOptions = [ { label: 'Small', value: '14px' as FontSize }, { label: 'Medium', value: '15px' as FontSize }, { label: 'Large', value: '16px' as FontSize }, { label: 'Extra Large', value: '18px' as FontSize }, ] // Default appearance settings const defaultFontSize: FontSize = '15px' const defaultAppBgColor: RgbaColor = { r: 25, g: 25, b: 25, a: IS_WINDOWS || IS_LINUX || !IS_TAURI ? 1 : 0.4, } const defaultLightAppBgColor: RgbaColor = { r: 255, g: 255, b: 255, a: IS_WINDOWS || IS_LINUX || !IS_TAURI ? 1 : 0.4, } const defaultAppMainViewBgColor: RgbaColor = { r: 25, g: 25, b: 25, a: 1 } const defaultLightAppMainViewBgColor: RgbaColor = { r: 255, g: 255, b: 255, a: 1, } const defaultAppPrimaryBgColor: RgbaColor = { r: 219, g: 88, b: 44, a: 1 } const defaultLightAppPrimaryBgColor: RgbaColor = { r: 219, g: 88, b: 44, a: 1 } const defaultAppAccentBgColor: RgbaColor = { r: 45, g: 120, b: 220, a: 1 } const defaultLightAppAccentBgColor: RgbaColor = { r: 45, g: 120, b: 220, a: 1 } const defaultAppDestructiveBgColor: RgbaColor = { r: 144, g: 60, b: 60, a: 1 } const defaultLightAppDestructiveBgColor: RgbaColor = { r: 217, g: 95, b: 95, a: 1, } const defaultDarkLeftPanelTextColor: string = '#FFF' const defaultLightLeftPanelTextColor: string = '#000' // Helper function to check if two RGBA colors are equal const isColorEqual = (color1: RgbaColor, color2: RgbaColor): boolean => { return ( color1.r === color2.r && color1.g === color2.g && color1.b === color2.b && color1.a === color2.a ) } // Helper function to check if color is default (not customized) export const isDefaultColor = (color: RgbaColor): boolean => { return ( isColorEqual(color, defaultAppBgColor) || isColorEqual(color, defaultLightAppBgColor) ) } export const isDefaultColorMainView = (color: RgbaColor): boolean => { return ( isColorEqual(color, defaultAppMainViewBgColor) || isColorEqual(color, defaultLightAppMainViewBgColor) ) } export const isDefaultColorPrimary = (color: RgbaColor): boolean => { return ( isColorEqual(color, defaultAppPrimaryBgColor) || isColorEqual(color, defaultLightAppPrimaryBgColor) ) } export const isDefaultColorAccent = (color: RgbaColor): boolean => { return ( isColorEqual(color, defaultAppAccentBgColor) || isColorEqual(color, defaultLightAppAccentBgColor) ) } export const isDefaultColorDestructive = (color: RgbaColor): boolean => { return ( isColorEqual(color, defaultAppDestructiveBgColor) || isColorEqual(color, defaultLightAppDestructiveBgColor) ) } // Helper function to get default text color based on theme export const getDefaultTextColor = (isDark: boolean): string => { return isDark ? defaultDarkLeftPanelTextColor : defaultLightLeftPanelTextColor } export const useAppearance = create()( persist( (set) => { return { fontSize: defaultFontSize, appBgColor: defaultAppBgColor, appMainViewBgColor: defaultAppMainViewBgColor, appPrimaryBgColor: defaultAppPrimaryBgColor, appAccentBgColor: defaultAppAccentBgColor, appDestructiveBgColor: defaultAppDestructiveBgColor, appLeftPanelTextColor: getDefaultTextColor(useTheme.getState().isDark), appMainViewTextColor: getDefaultTextColor(useTheme.getState().isDark), appPrimaryTextColor: getDefaultTextColor(useTheme.getState().isDark), appAccentTextColor: getDefaultTextColor(useTheme.getState().isDark), appDestructiveTextColor: '#FFF', resetAppearance: () => { const { isDark } = useTheme.getState() // Reset font size document.documentElement.style.setProperty( '--font-size-base', defaultFontSize ) // Reset app background color const defaultBg = isDark ? defaultAppBgColor : defaultLightAppBgColor const culoriRgbBg = rgb({ mode: 'rgb', r: defaultBg.r / 255, g: defaultBg.g / 255, b: defaultBg.b / 255, alpha: defaultBg.a, }) const oklchBgColor = oklch(culoriRgbBg) document.documentElement.style.setProperty( '--app-bg', formatCss(oklchBgColor) ) // Reset main view background color const defaultMainView = isDark ? defaultAppMainViewBgColor : defaultLightAppMainViewBgColor const culoriRgbMainView = rgb({ mode: 'rgb', r: defaultMainView.r / 255, g: defaultMainView.g / 255, b: defaultMainView.b / 255, alpha: defaultMainView.a, }) const oklchMainViewColor = oklch(culoriRgbMainView) document.documentElement.style.setProperty( '--app-main-view', formatCss(oklchMainViewColor) ) // Reset primary color const defaultPrimary = isDark ? defaultAppPrimaryBgColor : defaultLightAppPrimaryBgColor const culoriRgbPrimary = rgb({ mode: 'rgb', r: defaultPrimary.r / 255, g: defaultPrimary.g / 255, b: defaultPrimary.b / 255, alpha: defaultPrimary.a, }) const oklchPrimaryColor = oklch(culoriRgbPrimary) document.documentElement.style.setProperty( '--app-primary', formatCss(oklchPrimaryColor) ) // Reset accent color const defaultAccent = isDark ? defaultAppAccentBgColor : defaultLightAppAccentBgColor const culoriRgbAccent = rgb({ mode: 'rgb', r: defaultAccent.r / 255, g: defaultAccent.g / 255, b: defaultAccent.b / 255, alpha: defaultAccent.a, }) const oklchAccentColor = oklch(culoriRgbAccent) document.documentElement.style.setProperty( '--app-accent', formatCss(oklchAccentColor) ) // Reset destructive color const defaultDestructive = isDark ? defaultAppDestructiveBgColor : defaultLightAppDestructiveBgColor const culoriRgbDestructive = rgb({ mode: 'rgb', r: defaultDestructive.r / 255, g: defaultDestructive.g / 255, b: defaultDestructive.b / 255, alpha: defaultDestructive.a, }) const oklchDestructiveColor = oklch(culoriRgbDestructive) document.documentElement.style.setProperty( '--app-destructive', formatCss(oklchDestructiveColor) ) // Reset text colors const defaultTextColor = getDefaultTextColor(isDark) document.documentElement.style.setProperty( '--text-color', defaultTextColor ) document.documentElement.style.setProperty( '--app-left-panel-fg', defaultTextColor ) document.documentElement.style.setProperty( '--app-main-view-fg', defaultTextColor ) document.documentElement.style.setProperty('--app-primary-fg', '#FFF') document.documentElement.style.setProperty('--app-accent-fg', '#FFF') document.documentElement.style.setProperty( '--app-destructive-fg', '#FFF' ) // Update state set({ fontSize: defaultFontSize, appBgColor: defaultBg, appMainViewBgColor: defaultMainView, appPrimaryBgColor: defaultPrimary, appAccentBgColor: defaultAccent, appLeftPanelTextColor: defaultTextColor, appMainViewTextColor: defaultTextColor, appPrimaryTextColor: '#FFF', appAccentTextColor: '#FFF', appDestructiveBgColor: defaultDestructive, appDestructiveTextColor: '#FFF', }) }, setFontSize: (size: FontSize) => { // Update CSS variable document.documentElement.style.setProperty('--font-size-base', size) // Update state set({ fontSize: size }) }, setAppBgColor: (color: RgbaColor) => { // Get the current theme state const { isDark } = useTheme.getState() // If color is being set to default, use theme-appropriate default let finalColor = color if (isDefaultColor(color)) { finalColor = isDark ? defaultAppBgColor : defaultLightAppBgColor } // Convert RGBA to a format culori can work with const culoriRgb = rgb({ mode: 'rgb', r: finalColor.r / 255, g: finalColor.g / 255, b: finalColor.b / 255, alpha: finalColor.a, }) // Convert to OKLCH for CSS variable const oklchColor = oklch(culoriRgb) // Update CSS variable with OKLCH document.documentElement.style.setProperty( '--app-bg', formatCss(oklchColor) ) // Calculate text color based on background brightness or use default based on theme let textColor: string if (isDefaultColor(color)) { // If using default background, use default text color based on theme textColor = getDefaultTextColor(isDark) } else { // Otherwise calculate based on brightness textColor = getBrightness(finalColor) > 128 ? '#000' : '#FFF' } // Update CSS variable for text color document.documentElement.style.setProperty('--text-color', textColor) // Store the original RGBA and calculated text color in state set({ appBgColor: finalColor, appLeftPanelTextColor: textColor }) }, setAppMainViewBgColor: (color: RgbaColor) => { // Get the current theme state const { isDark } = useTheme.getState() // If color is being set to default, use theme-appropriate default let finalColorMainView = color if (isDefaultColorMainView(color)) { finalColorMainView = isDark ? defaultAppMainViewBgColor : defaultLightAppMainViewBgColor } // Convert RGBA to a format culori can work with const culoriRgb = rgb({ mode: 'rgb', r: finalColorMainView.r / 255, g: finalColorMainView.g / 255, b: finalColorMainView.b / 255, alpha: finalColorMainView.a, }) // Convert to OKLCH for CSS variable const oklchColor = oklch(culoriRgb) // Update CSS variable with OKLCH document.documentElement.style.setProperty( '--app-main-view', formatCss(oklchColor) ) // Calculate text color based on background brightness or use default based on theme let textColor: string if (isDefaultColor(color)) { // If using default background, use default text color based on theme textColor = getDefaultTextColor(isDark) } else { // Otherwise calculate based on brightness textColor = getBrightness(finalColorMainView) > 128 ? '#000' : '#FFF' } // Update CSS variable for text color document.documentElement.style.setProperty( '--app-main-view-fg', textColor ) // Store the original RGBA and calculated text color in state set({ appMainViewBgColor: finalColorMainView, appMainViewTextColor: textColor, }) }, setAppPrimaryBgColor: (color: RgbaColor) => { // Get the current theme state const { isDark } = useTheme.getState() // If color is being set to default, use theme-appropriate default let finalColorPrimary = color if (isDefaultColorPrimary(color)) { finalColorPrimary = isDark ? defaultAppPrimaryBgColor : defaultLightAppPrimaryBgColor } // Convert RGBA to a format culori can work with const culoriRgb = rgb({ mode: 'rgb', r: finalColorPrimary.r / 255, g: finalColorPrimary.g / 255, b: finalColorPrimary.b / 255, alpha: finalColorPrimary.a, }) // Convert to OKLCH for CSS variable const oklchColor = oklch(culoriRgb) // Update CSS variable with OKLCH document.documentElement.style.setProperty( '--app-primary', formatCss(oklchColor) ) // Calculate text color based on background brightness or use default based on theme let textColor: string if (isDefaultColorPrimary(color)) { // If using default background, use default text color based on theme textColor = '#FFF' } else { // Otherwise calculate based on brightness textColor = getBrightness(finalColorPrimary) > 128 ? '#000' : '#FFF' } // Update CSS variable for text color document.documentElement.style.setProperty( '--app-primary-fg', textColor ) // Store the original RGBA and calculated text color in state set({ appPrimaryBgColor: finalColorPrimary, appPrimaryTextColor: textColor, }) }, setAppAccentBgColor: (color: RgbaColor) => { // Get the current theme state const { isDark } = useTheme.getState() // If color is being set to default, use theme-appropriate default let finalColorAccent = color if (isDefaultColorAccent(color)) { finalColorAccent = isDark ? defaultAppAccentBgColor : defaultLightAppAccentBgColor } // Convert RGBA to a format culori can work with const culoriRgb = rgb({ mode: 'rgb', r: finalColorAccent.r / 255, g: finalColorAccent.g / 255, b: finalColorAccent.b / 255, alpha: finalColorAccent.a, }) // Convert to OKLCH for CSS variable const oklchColor = oklch(culoriRgb) // Update CSS variable with OKLCH document.documentElement.style.setProperty( '--app-accent', formatCss(oklchColor) ) // Calculate text color based on background brightness or use default based on theme let textColor: string if (isDefaultColorAccent(color)) { // If using default background, use default text color based on theme textColor = '#FFF' } else { // Otherwise calculate based on brightness textColor = getBrightness(finalColorAccent) > 128 ? '#000' : '#FFF' } // Update CSS variable for text color document.documentElement.style.setProperty( '--app-accent-fg', textColor ) // Store the original RGBA and calculated text color in state set({ appAccentBgColor: finalColorAccent, appAccentTextColor: textColor, }) }, setAppDestructiveBgColor: (color: RgbaColor) => { // Get the current theme state const { isDark } = useTheme.getState() // If color is being set to default, use theme-appropriate default let finalColorDestructive = color if (isDefaultColorDestructive(color)) { finalColorDestructive = isDark ? defaultAppDestructiveBgColor : defaultLightAppDestructiveBgColor } // Convert RGBA to a format culori can work with const culoriRgb = rgb({ mode: 'rgb', r: finalColorDestructive.r / 255, g: finalColorDestructive.g / 255, b: finalColorDestructive.b / 255, alpha: finalColorDestructive.a, }) // Convert to OKLCH for CSS variable const oklchColor = oklch(culoriRgb) // Update CSS variable with OKLCH document.documentElement.style.setProperty( '--app-destructive', formatCss(oklchColor) ) // Calculate text color based on background brightness or use default based on theme let textColor: string if (isDefaultColorDestructive(color)) { // If using default background, use default text color based on theme textColor = '#FFF' } else { // Otherwise calculate based on brightness textColor = getBrightness(finalColorDestructive) > 128 ? '#000' : '#FFF' } // Update CSS variable for text color document.documentElement.style.setProperty( '--app-destructive-fg', textColor ) // Store the original RGBA and calculated text color in state set({ appDestructiveBgColor: finalColorDestructive, appDestructiveTextColor: textColor, }) }, } }, { name: localStorageKey.settingAppearance, storage: createJSONStorage(() => localStorage), // Apply settings when hydrating from storage onRehydrateStorage: () => (state) => { if (state) { // Apply font size from storage document.documentElement.style.setProperty( '--font-size-base', state.fontSize ) // Get the current theme state const { isDark } = useTheme.getState() // If stored color is default, use theme-appropriate default let finalColor = state.appBgColor if (isDefaultColor(state.appBgColor)) { finalColor = isDark ? defaultAppBgColor : defaultLightAppBgColor } let finalColorMainView = state.appMainViewBgColor if (isDefaultColorMainView(state.appMainViewBgColor)) { finalColorMainView = isDark ? defaultAppMainViewBgColor : defaultLightAppMainViewBgColor } let finalColorPrimary = state.appPrimaryBgColor if (isDefaultColorPrimary(state.appPrimaryBgColor)) { finalColorPrimary = isDark ? defaultAppPrimaryBgColor : defaultLightAppPrimaryBgColor } let finalColorAccent = state.appAccentBgColor if (isDefaultColorAccent(state.appAccentBgColor)) { finalColorAccent = isDark ? defaultAppAccentBgColor : defaultLightAppAccentBgColor } let finalColorDestructive = state.appDestructiveBgColor if (isDefaultColorDestructive(state.appDestructiveBgColor)) { finalColorDestructive = isDark ? defaultAppDestructiveBgColor : defaultLightAppDestructiveBgColor } // Apply app background color from storage // Convert RGBA to a format culori can work with const culoriRgb = rgb({ mode: 'rgb', r: finalColor.r / 255, g: finalColor.g / 255, b: finalColor.b / 255, alpha: finalColor.a, }) const culoriRgbMainViewColor = rgb({ mode: 'rgb', r: finalColorMainView.r / 255, g: finalColorMainView.g / 255, b: finalColorMainView.b / 255, alpha: finalColorMainView.a, }) const culoriRgbPrimaryColor = rgb({ mode: 'rgb', r: finalColorPrimary.r / 255, g: finalColorPrimary.g / 255, b: finalColorPrimary.b / 255, alpha: finalColorPrimary.a, }) const culoriRgbAccentColor = rgb({ mode: 'rgb', r: finalColorAccent.r / 255, g: finalColorAccent.g / 255, b: finalColorAccent.b / 255, alpha: finalColorAccent.a, }) const culoriRgbDestructiveColor = rgb({ mode: 'rgb', r: finalColorDestructive.r / 255, g: finalColorDestructive.g / 255, b: finalColorDestructive.b / 255, alpha: finalColorDestructive.a, }) // Convert to OKLCH for CSS variable const oklchColor = oklch(culoriRgb) const oklchMainViewColor = oklch(culoriRgbMainViewColor) const oklchPrimaryColor = oklch(culoriRgbPrimaryColor) const oklchAccentColor = oklch(culoriRgbAccentColor) const oklchDestructiveColor = oklch(culoriRgbDestructiveColor) document.documentElement.style.setProperty( '--app-bg', formatCss(oklchColor) ) document.documentElement.style.setProperty( '--app-main-view', formatCss(oklchMainViewColor) ) document.documentElement.style.setProperty( '--app-primary', formatCss(oklchPrimaryColor) ) document.documentElement.style.setProperty( '--app-accent', formatCss(oklchAccentColor) ) document.documentElement.style.setProperty( '--app-destructive', formatCss(oklchDestructiveColor) ) // Calculate and apply text color let textColor: string if (isDefaultColor(state.appBgColor)) { // If using default background, use default text color based on theme textColor = getDefaultTextColor(isDark) } else { // Otherwise calculate based on brightness textColor = getBrightness(finalColor) > 128 ? '#000' : '#FFF' } document.documentElement.style.setProperty( '--app-left-panel-fg', textColor ) // Calculate and apply text color for main view let textColorMainView: string if (isDefaultColorMainView(state.appMainViewBgColor)) { // If using default background, use default text color based on theme textColorMainView = getDefaultTextColor(isDark) } else { // Otherwise calculate based on brightness textColorMainView = getBrightness(finalColorMainView) > 128 ? '#000' : '#FFF' } document.documentElement.style.setProperty( '--app-main-view-fg', textColorMainView ) // Calculate and apply text color for primary let textColorPrimary: string if (isDefaultColorPrimary(state.appPrimaryBgColor)) { // If using default background, use default text color based on theme textColorPrimary = getDefaultTextColor(isDark) } else { // Otherwise calculate based on brightness textColorPrimary = getBrightness(finalColorPrimary) > 128 ? '#000' : '#FFF' } document.documentElement.style.setProperty( '--app-primary-fg', textColorPrimary ) // Calculate and apply text color for accent let textColorAccent: string if (isDefaultColorAccent(state.appAccentBgColor)) { // If using default background, use default text color based on theme textColorAccent = getDefaultTextColor(isDark) } else { // Otherwise calculate based on brightness textColorAccent = getBrightness(finalColorAccent) > 128 ? '#000' : '#FFF' } document.documentElement.style.setProperty( '--app-accent-fg', textColorAccent ) // We don't need to update the state here as it will be handled by the store // The state will be updated with the hydrated values automatically } // Return the state to be used for hydration return state }, } ) )