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)
This commit is contained in:
Gigi
2025-10-14 09:39:13 +02:00
parent 69febf4356
commit 129aced1a2
5 changed files with 215 additions and 6 deletions

View File

@@ -10,6 +10,12 @@ interface ThemeSettingsProps {
const ThemeSettings: React.FC<ThemeSettingsProps> = ({ 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 (
<div className="settings-section">
@@ -41,9 +47,66 @@ const ThemeSettings: React.FC<ThemeSettingsProps> = ({ settings, onUpdate }) =>
/>
</div>
</div>
{showDarkColors && (
<div className="setting-group setting-inline">
<label>Dark Color Theme</label>
<div className="setting-buttons">
<button
onClick={() => onUpdate({ darkColorTheme: 'black' })}
className={`font-size-btn ${currentDarkColor === 'black' ? 'active' : ''}`}
title="Black"
>
Black
</button>
<button
onClick={() => onUpdate({ darkColorTheme: 'midnight' })}
className={`font-size-btn ${currentDarkColor === 'midnight' ? 'active' : ''}`}
title="Midnight"
>
Midnight
</button>
<button
onClick={() => onUpdate({ darkColorTheme: 'charcoal' })}
className={`font-size-btn ${currentDarkColor === 'charcoal' ? 'active' : ''}`}
title="Charcoal"
>
Charcoal
</button>
</div>
</div>
)}
{showLightColors && (
<div className="setting-group setting-inline">
<label>Light Color Theme</label>
<div className="setting-buttons">
<button
onClick={() => onUpdate({ lightColorTheme: 'paper-white' })}
className={`font-size-btn ${currentLightColor === 'paper-white' ? 'active' : ''}`}
title="Paper White"
>
Paper
</button>
<button
onClick={() => onUpdate({ lightColorTheme: 'sepia' })}
className={`font-size-btn ${currentLightColor === 'sepia' ? 'active' : ''}`}
title="Sepia"
>
Sepia
</button>
<button
onClick={() => onUpdate({ lightColorTheme: 'ivory' })}
className={`font-size-btn ${currentLightColor === 'ivory' ? 'active' : ''}`}
title="Ivory"
>
Ivory
</button>
</div>
</div>
)}
</div>
)
}
export default ThemeSettings

View File

@@ -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') {

View File

@@ -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(

View File

@@ -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;
}
}

View File

@@ -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 })
}