From 129aced1a2a06c02b6db073b3e7dcee2991ef72c Mon Sep 17 00:00:00 2001 From: Gigi Date: Tue, 14 Oct 2025 09:39:13 +0200 Subject: [PATCH] feat(theme): add color theme variants for light and dark modes - Add darkColorTheme: black, midnight (default), charcoal - Add lightColorTheme: paper-white (default), sepia, ivory - Extend UserSettings with color theme fields - Update ThemeSettings UI to show color options - Add CSS variables for all color theme variants - Sepia and Ivory have warm, reading-friendly palettes - Black offers true black for OLED screens - All color themes sync via Nostr (NIP-78) --- src/components/Settings/ThemeSettings.tsx | 65 +++++++++++- src/hooks/useSettings.ts | 8 +- src/services/settingsService.ts | 2 + src/styles/base/variables.css | 121 ++++++++++++++++++++++ src/utils/theme.ts | 25 ++++- 5 files changed, 215 insertions(+), 6 deletions(-) diff --git a/src/components/Settings/ThemeSettings.tsx b/src/components/Settings/ThemeSettings.tsx index 10e7c990..22cbb662 100644 --- a/src/components/Settings/ThemeSettings.tsx +++ b/src/components/Settings/ThemeSettings.tsx @@ -10,6 +10,12 @@ interface ThemeSettingsProps { const ThemeSettings: React.FC = ({ settings, onUpdate }) => { const currentTheme = settings.theme ?? 'system' + const currentDarkColor = settings.darkColorTheme ?? 'midnight' + const currentLightColor = settings.lightColorTheme ?? 'paper-white' + + // Determine which color picker to show based on current theme + const showDarkColors = currentTheme === 'dark' || currentTheme === 'system' + const showLightColors = currentTheme === 'light' || currentTheme === 'system' return (
@@ -41,9 +47,66 @@ const ThemeSettings: React.FC = ({ settings, onUpdate }) => />
+ + {showDarkColors && ( +
+ +
+ + + +
+
+ )} + + {showLightColors && ( +
+ +
+ + + +
+
+ )} ) } export default ThemeSettings - diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index 616f21f2..b2d1e36e 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -50,8 +50,12 @@ export function useSettings({ relayPool, eventStore, pubkey, accountManager }: U console.log('🎨 Applying settings styles:', { fontKey, fontSize: settings.fontSize, theme: settings.theme }) - // Apply theme (defaults to 'system' if not set) - applyTheme(settings.theme ?? 'system') + // Apply theme with color variants (defaults to 'system' if not set) + applyTheme( + settings.theme ?? 'system', + settings.darkColorTheme ?? 'midnight', + settings.lightColorTheme ?? 'paper-white' + ) // Load font first and wait for it to be ready if (fontKey !== 'system') { diff --git a/src/services/settingsService.ts b/src/services/settingsService.ts index f8f4be97..187e0e29 100644 --- a/src/services/settingsService.ts +++ b/src/services/settingsService.ts @@ -49,6 +49,8 @@ export interface UserSettings { autoCollapseSidebarOnMobile?: boolean // Auto-collapse sidebar on mobile (default: true) // Theme preference theme?: 'dark' | 'light' | 'system' // default: system + darkColorTheme?: 'black' | 'midnight' | 'charcoal' // default: midnight + lightColorTheme?: 'paper-white' | 'sepia' | 'ivory' // default: paper-white } export async function loadSettings( diff --git a/src/styles/base/variables.css b/src/styles/base/variables.css index cea7cd74..5c6c8070 100644 --- a/src/styles/base/variables.css +++ b/src/styles/base/variables.css @@ -110,4 +110,125 @@ } } +/* Dark Color Theme Variants */ +/* Midnight (default) - current zinc palette */ +:root.dark-midnight { + --color-bg: #18181b; /* zinc-900 */ + --color-bg-elevated: #27272a; /* zinc-800 */ + --color-bg-subtle: #1e1e1e; + --color-border: #3f3f46; /* zinc-700 */ + --color-border-subtle: #52525b; /* zinc-600 */ +} + +/* Black - true black for OLED */ +:root.dark-black { + --color-bg: #000000; /* true black */ + --color-bg-elevated: #0a0a0a; /* very dark gray */ + --color-bg-subtle: #050505; + --color-border: #1a1a1a; + --color-border-subtle: #2a2a2a; +} + +/* Charcoal - warmer, softer dark */ +:root.dark-charcoal { + --color-bg: #1c1c1e; /* warmer dark */ + --color-bg-elevated: #2c2c2e; + --color-bg-subtle: #242426; + --color-border: #3a3a3c; + --color-border-subtle: #48484a; +} + +/* Light Color Theme Variants */ +/* Paper White (default) - pure white */ +:root.light-paper-white { + --color-bg: #ffffff; /* white */ + --color-bg-elevated: #f5f5f5; /* gray-100 */ + --color-bg-subtle: #fafafa; /* gray-50 */ + --color-border: #e5e7eb; /* gray-200 */ + --color-border-subtle: #d1d5db; /* gray-300 */ +} + +/* Sepia - warm, reading-friendly */ +:root.light-sepia { + --color-bg: #f4f1ea; /* warm beige */ + --color-bg-elevated: #ebe6db; /* darker beige */ + --color-bg-subtle: #f9f6f0; /* lighter beige */ + --color-border: #d4cfc4; /* warm gray border */ + --color-border-subtle: #c4bfb4; + --color-text: #2d2a24; /* warm dark brown */ + --color-text-secondary: #5d5a54; + --color-text-muted: #8d8a84; +} + +/* Ivory - soft, creamy */ +:root.light-ivory { + --color-bg: #fffff0; /* ivory */ + --color-bg-elevated: #faf8f0; /* cream */ + --color-bg-subtle: #fefef8; + --color-border: #e8e6de; + --color-border-subtle: #d8d6ce; + --color-text: #1a1a18; /* near black with warm tint */ + --color-text-secondary: #4a4a48; + --color-text-muted: #7a7a78; +} + +/* System theme color variants */ +@media (prefers-color-scheme: dark) { + :root.theme-system.dark-midnight { + --color-bg: #18181b; + --color-bg-elevated: #27272a; + --color-bg-subtle: #1e1e1e; + --color-border: #3f3f46; + --color-border-subtle: #52525b; + } + + :root.theme-system.dark-black { + --color-bg: #000000; + --color-bg-elevated: #0a0a0a; + --color-bg-subtle: #050505; + --color-border: #1a1a1a; + --color-border-subtle: #2a2a2a; + } + + :root.theme-system.dark-charcoal { + --color-bg: #1c1c1e; + --color-bg-elevated: #2c2c2e; + --color-bg-subtle: #242426; + --color-border: #3a3a3c; + --color-border-subtle: #48484a; + } +} + +@media (prefers-color-scheme: light) { + :root.theme-system.light-paper-white { + --color-bg: #ffffff; + --color-bg-elevated: #f5f5f5; + --color-bg-subtle: #fafafa; + --color-border: #e5e7eb; + --color-border-subtle: #d1d5db; + } + + :root.theme-system.light-sepia { + --color-bg: #f4f1ea; + --color-bg-elevated: #ebe6db; + --color-bg-subtle: #f9f6f0; + --color-border: #d4cfc4; + --color-border-subtle: #c4bfb4; + --color-text: #2d2a24; + --color-text-secondary: #5d5a54; + --color-text-muted: #8d8a84; + } + + :root.theme-system.light-ivory { + --color-bg: #fffff0; + --color-bg-elevated: #faf8f0; + --color-bg-subtle: #fefef8; + --color-border: #e8e6de; + --color-border-subtle: #d8d6ce; + --color-text: #1a1a18; + --color-text-secondary: #4a4a48; + --color-text-muted: #7a7a78; + } +} + diff --git a/src/utils/theme.ts b/src/utils/theme.ts index 30c90d1d..9bbc6796 100644 --- a/src/utils/theme.ts +++ b/src/utils/theme.ts @@ -1,4 +1,6 @@ export type Theme = 'dark' | 'light' | 'system' +export type DarkColorTheme = 'black' | 'midnight' | 'charcoal' +export type LightColorTheme = 'paper-white' | 'sepia' | 'ivory' let mediaQueryListener: ((e: MediaQueryListEvent) => void) | null = null @@ -11,14 +13,21 @@ export function getSystemTheme(): 'dark' | 'light' { } /** - * Apply theme to the document root element + * Apply theme and color variant to the document root element * Handles 'system' theme by listening to OS preference changes */ -export function applyTheme(theme: Theme): void { +export function applyTheme( + theme: Theme, + darkColorTheme: DarkColorTheme = 'midnight', + lightColorTheme: LightColorTheme = 'paper-white' +): void { const root = document.documentElement // Remove existing theme classes root.classList.remove('theme-dark', 'theme-light', 'theme-system') + // Remove existing color theme classes + root.classList.remove('dark-black', 'dark-midnight', 'dark-charcoal') + root.classList.remove('light-paper-white', 'light-sepia', 'light-ivory') // Clean up previous media query listener if exists if (mediaQueryListener) { @@ -29,6 +38,10 @@ export function applyTheme(theme: Theme): void { if (theme === 'system') { root.classList.add('theme-system') + // Apply color themes for system mode (CSS will handle media query) + root.classList.add(`dark-${darkColorTheme}`) + root.classList.add(`light-${lightColorTheme}`) + // Listen for system theme changes mediaQueryListener = (e: MediaQueryListEvent) => { console.log('🎨 System theme changed to:', e.matches ? 'dark' : 'light') @@ -38,7 +51,13 @@ export function applyTheme(theme: Theme): void { window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', mediaQueryListener) } else { root.classList.add(`theme-${theme}`) + // Apply appropriate color theme based on light/dark + if (theme === 'dark') { + root.classList.add(`dark-${darkColorTheme}`) + } else { + root.classList.add(`light-${lightColorTheme}`) + } } - console.log('🎨 Applied theme:', theme) + console.log('🎨 Applied theme:', theme, 'with colors:', { dark: darkColorTheme, light: lightColorTheme }) }