diff --git a/index.html b/index.html index b6ff2136..c7ca0399 100644 --- a/index.html +++ b/index.html @@ -25,6 +25,23 @@ + + +
diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx index e343fad6..c7f3e5cc 100644 --- a/src/components/Settings.tsx +++ b/src/components/Settings.tsx @@ -4,6 +4,7 @@ import { RelayPool } from 'applesauce-relay' import { UserSettings } from '../services/settingsService' import IconButton from './IconButton' import { loadFont } from '../utils/fontLoader' +import ThemeSettings from './Settings/ThemeSettings' import ReadingDisplaySettings from './Settings/ReadingDisplaySettings' import LayoutNavigationSettings from './Settings/LayoutNavigationSettings' import StartupPreferencesSettings from './Settings/StartupPreferencesSettings' @@ -159,6 +160,7 @@ const Settings: React.FC = ({ settings, onSave, onClose, relayPoo
+ diff --git a/src/components/Settings/ThemeSettings.tsx b/src/components/Settings/ThemeSettings.tsx new file mode 100644 index 00000000..10e7c990 --- /dev/null +++ b/src/components/Settings/ThemeSettings.tsx @@ -0,0 +1,49 @@ +import React from 'react' +import { faSun, faMoon, faDesktop } from '@fortawesome/free-solid-svg-icons' +import { UserSettings } from '../../services/settingsService' +import IconButton from '../IconButton' + +interface ThemeSettingsProps { + settings: UserSettings + onUpdate: (updates: Partial) => void +} + +const ThemeSettings: React.FC = ({ settings, onUpdate }) => { + const currentTheme = settings.theme ?? 'system' + + return ( +
+

Theme

+ +
+ +
+ onUpdate({ theme: 'light' })} + title="Light theme" + ariaLabel="Light theme" + variant={currentTheme === 'light' ? 'primary' : 'ghost'} + /> + onUpdate({ theme: 'dark' })} + title="Dark theme" + ariaLabel="Dark theme" + variant={currentTheme === 'dark' ? 'primary' : 'ghost'} + /> + onUpdate({ theme: 'system' })} + title="Use system preference" + ariaLabel="Use system preference" + variant={currentTheme === 'system' ? 'primary' : 'ghost'} + /> +
+
+
+ ) +} + +export default ThemeSettings + diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index b78d03da..616f21f2 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -5,6 +5,7 @@ import { EventFactory } from 'applesauce-factory' import { AccountManager } from 'applesauce-accounts' import { UserSettings, loadSettings, saveSettings, watchSettings } from '../services/settingsService' import { loadFont, getFontFamily } from '../utils/fontLoader' +import { applyTheme } from '../utils/theme' import { RELAYS } from '../config/relays' interface UseSettingsParams { @@ -47,7 +48,10 @@ export function useSettings({ relayPool, eventStore, pubkey, accountManager }: U const root = document.documentElement.style const fontKey = settings.readingFont || 'system' - console.log('🎨 Applying settings styles:', { fontKey, fontSize: settings.fontSize }) + console.log('🎨 Applying settings styles:', { fontKey, fontSize: settings.fontSize, theme: settings.theme }) + + // Apply theme (defaults to 'system' if not set) + applyTheme(settings.theme ?? 'system') // 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 8b03ef12..f8f4be97 100644 --- a/src/services/settingsService.ts +++ b/src/services/settingsService.ts @@ -47,6 +47,8 @@ export interface UserSettings { imageCacheSizeMB?: number // Maximum cache size in megabytes (default: 210MB) // Mobile settings autoCollapseSidebarOnMobile?: boolean // Auto-collapse sidebar on mobile (default: true) + // Theme preference + theme?: 'dark' | 'light' | 'system' // default: system } export async function loadSettings( diff --git a/src/styles/base/global.css b/src/styles/base/global.css index 3bb2bcef..f394f898 100644 --- a/src/styles/base/global.css +++ b/src/styles/base/global.css @@ -22,7 +22,7 @@ body.mobile-sidebar-open { justify-content: center; text-align: center; padding: 2rem; - color: rgb(212 212 216); /* zinc-300 */ + color: var(--color-text); } diff --git a/src/styles/base/variables.css b/src/styles/base/variables.css index e536fae2..cea7cd74 100644 --- a/src/styles/base/variables.css +++ b/src/styles/base/variables.css @@ -4,10 +4,6 @@ line-height: 1.5; font-weight: 400; - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; @@ -47,10 +43,70 @@ --safe-area-right: env(safe-area-inset-right, 0px); } +/* Dark theme (default) */ +:root.theme-dark { + color-scheme: dark; + + --color-bg: #18181b; /* zinc-900 */ + --color-bg-elevated: #27272a; /* zinc-800 */ + --color-bg-subtle: #1e1e1e; /* between zinc-800 and zinc-900 */ + --color-border: #3f3f46; /* zinc-700 */ + --color-border-subtle: #52525b; /* zinc-600 */ + --color-text: #e4e4e7; /* zinc-200 */ + --color-text-secondary: #a1a1aa; /* zinc-400 */ + --color-text-muted: #71717a; /* zinc-500 */ + --color-primary: #6366f1; /* indigo-500 */ + --color-primary-hover: #4f46e5; /* indigo-600 */ +} + +/* Light theme */ +:root.theme-light { + color-scheme: light; + + --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 */ + --color-text: #111827; /* gray-900 */ + --color-text-secondary: #374151; /* gray-700 */ + --color-text-muted: #6b7280; /* gray-500 */ + --color-primary: #4f46e5; /* indigo-600 */ + --color-primary-hover: #4338ca; /* indigo-700 */ +} + +/* System theme - follow OS preference */ +:root.theme-system { + color-scheme: light dark; +} + +@media (prefers-color-scheme: dark) { + :root.theme-system { + --color-bg: #18181b; + --color-bg-elevated: #27272a; + --color-bg-subtle: #1e1e1e; + --color-border: #3f3f46; + --color-border-subtle: #52525b; + --color-text: #e4e4e7; + --color-text-secondary: #a1a1aa; + --color-text-muted: #71717a; + --color-primary: #6366f1; + --color-primary-hover: #4f46e5; + } +} + @media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; + :root.theme-system { + --color-bg: #ffffff; + --color-bg-elevated: #f5f5f5; + --color-bg-subtle: #fafafa; + --color-border: #e5e7eb; + --color-border-subtle: #d1d5db; + --color-text: #111827; + --color-text-secondary: #374151; + --color-text-muted: #6b7280; + --color-primary: #4f46e5; + --color-primary-hover: #4338ca; } } diff --git a/src/styles/components/cards.css b/src/styles/components/cards.css index 480791cd..d83a53c7 100644 --- a/src/styles/components/cards.css +++ b/src/styles/components/cards.css @@ -1,14 +1,14 @@ /* Bookmark item and blog post cards */ -.bookmark-item { background: rgb(24 24 27); /* zinc-900 */ padding: 1.5rem; border-radius: 12px; transition: all 0.2s ease; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } +.bookmark-item { background: var(--color-bg); padding: 1.5rem; border-radius: 12px; transition: all 0.2s ease; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .bookmark-item:hover { transform: translateY(-2px); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); } -.bookmark-item h3 { margin: 0 0 0.5rem 0; color: rgb(255 255 255); /* white */ font-size: 1.2rem; } -.bookmark-url { color: rgb(99 102 241); /* indigo-500 */ text-decoration: none; display: block; margin-bottom: 0.5rem; word-break: break-all; background: none; border: none; padding: 0; font: inherit; cursor: pointer; text-align: left; width: 100%; } +.bookmark-item h3 { margin: 0 0 0.5rem 0; color: var(--color-text); font-size: 1.2rem; } +.bookmark-url { color: var(--color-primary); text-decoration: none; display: block; margin-bottom: 0.5rem; word-break: break-all; background: none; border: none; padding: 0; font: inherit; cursor: pointer; text-align: left; width: 100%; } .bookmark-url:hover { text-decoration: underline; } -.bookmark-content { color: rgb(212 212 216); /* zinc-300 */ margin: 0.5rem 0; line-height: 1.4; word-wrap: break-word; overflow-wrap: break-word; word-break: break-word; } -.bookmark-meta { color: rgb(161 161 170); /* zinc-400 */ font-size: 0.9rem; margin-top: 0.5rem; } +.bookmark-content { color: var(--color-text); margin: 0.5rem 0; line-height: 1.4; word-wrap: break-word; overflow-wrap: break-word; word-break: break-word; } +.bookmark-meta { color: var(--color-text-secondary); font-size: 0.9rem; margin-top: 0.5rem; } .individual-bookmarks { margin: 1rem 0; } -.individual-bookmarks h4 { margin: 0 0 1rem 0; font-size: 1rem; color: rgb(255 255 255); /* white */ } +.individual-bookmarks h4 { margin: 0 0 1rem 0; font-size: 1rem; color: var(--color-text); } .bookmarks-grid { display: flex; flex-direction: column; gap: 1rem; width: 100%; max-width: 100%; } .bookmarks-grid.bookmarks-compact { gap: 0.5rem; } @@ -19,75 +19,75 @@ .bookmarks-grid.bookmarks-large { gap: 1rem; } } -.individual-bookmark { background: transparent; padding: 1rem; border-radius: 8px; transition: all 0.2s ease; border: 1px solid rgb(39 39 42); /* zinc-800 */ word-wrap: break-word; overflow-wrap: break-word; word-break: break-word; overflow: hidden; } -.individual-bookmark:hover { border-color: rgb(63 63 70); /* zinc-700 */ background: rgb(39 39 42); /* zinc-800 */ } +.individual-bookmark { background: transparent; padding: 1rem; border-radius: 8px; transition: all 0.2s ease; border: 1px solid var(--color-bg-elevated); word-wrap: break-word; overflow-wrap: break-word; word-break: break-word; overflow: hidden; } +.individual-bookmark:hover { border-color: var(--color-border); background: var(--color-bg-elevated); } /* Compact view */ -.individual-bookmark.compact { padding: 0.5rem 0.5rem; background: transparent; border: none; border-bottom: 1px solid rgb(39 39 42); /* zinc-800 */ border-radius: 0; box-shadow: none; width: 100%; max-width: 100%; overflow: hidden; } -.individual-bookmark.compact:hover { background: rgb(39 39 42); /* zinc-800 */ border-bottom-color: rgb(63 63 70); /* zinc-700 */ transform: none; box-shadow: none; } +.individual-bookmark.compact { padding: 0.5rem 0.5rem; background: transparent; border: none; border-bottom: 1px solid var(--color-bg-elevated); border-radius: 0; box-shadow: none; width: 100%; max-width: 100%; overflow: hidden; } +.individual-bookmark.compact:hover { background: var(--color-bg-elevated); border-bottom-color: var(--color-border); transform: none; box-shadow: none; } .compact-row { display: flex; align-items: center; gap: 0.5rem; height: 28px; width: 100%; min-width: 0; overflow: hidden; } -.compact-thumbnail { width: 24px; height: 24px; flex-shrink: 0; border-radius: 4px; overflow: hidden; background: rgb(39 39 42); /* zinc-800 */ display: flex; align-items: center; justify-content: center; } +.compact-thumbnail { width: 24px; height: 24px; flex-shrink: 0; border-radius: 4px; overflow: hidden; background: var(--color-bg-elevated); display: flex; align-items: center; justify-content: center; } .compact-thumbnail img { width: 100%; height: 100%; object-fit: cover; } .compact-row.clickable { cursor: pointer; } .compact-row.clickable:active { opacity: 0.8; } -.bookmark-type-compact { display: flex; align-items: center; gap: 0.25rem; color: rgb(99 102 241); /* indigo-500 */ font-size: 0.85rem; flex-shrink: 0; } -.compact-text { flex: 1; min-width: 0; color: rgb(212 212 216); /* zinc-300 */ font-size: 0.85rem; line-height: 1.2; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.bookmark-date-compact { font-size: 0.7rem; color: rgb(113 113 122); /* zinc-500 */ flex-shrink: 0; white-space: nowrap; } -.compact-read-btn { background: transparent; color: rgb(161 161 170); /* zinc-400 */ border: none; padding: 0; border-radius: 4px; cursor: pointer; font-size: 0.75rem; display: flex; align-items: center; justify-content: center; width: 24px; height: 22px; flex-shrink: 0; transition: color 0.2s ease; } -.compact-read-btn:hover { color: rgb(212 212 216); /* zinc-300 */ } +.bookmark-type-compact { display: flex; align-items: center; gap: 0.25rem; color: var(--color-primary); font-size: 0.85rem; flex-shrink: 0; } +.compact-text { flex: 1; min-width: 0; color: var(--color-text); font-size: 0.85rem; line-height: 1.2; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.bookmark-date-compact { font-size: 0.7rem; color: var(--color-text-muted); flex-shrink: 0; white-space: nowrap; } +.compact-read-btn { background: transparent; color: var(--color-text-secondary); border: none; padding: 0; border-radius: 4px; cursor: pointer; font-size: 0.75rem; display: flex; align-items: center; justify-content: center; width: 24px; height: 22px; flex-shrink: 0; transition: color 0.2s ease; } +.compact-read-btn:hover { color: var(--color-text); } .compact-read-btn:active { transform: translateY(1px); } .bookmark-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem; flex-wrap: wrap; gap: 0.5rem; } -.bookmark-type { color: rgb(99 102 241); /* indigo-500 */ font-size: 0.9rem; display: flex; align-items: center; gap: 0.35rem; } -.bookmark-id { font-family: monospace; font-size: 0.8rem; color: rgb(161 161 170); /* zinc-400 */ background: rgb(24 24 27); /* zinc-900 */ padding: 0.25rem 0.5rem; border-radius: 4px; } -.bookmark-date { font-size: 0.8rem; color: rgb(113 113 122); /* zinc-500 */ } -.bookmark-date-link { font-size: 0.8rem; color: rgb(113 113 122); /* zinc-500 */ text-decoration: none; transition: color 0.2s ease; } -.bookmark-date-link:hover { color: rgb(96 165 250); /* blue-400 */ text-decoration: underline; } -.individual-bookmark .bookmark-content { margin: 0.75rem 0; color: rgb(212 212 216); /* zinc-300 */ line-height: 1.6; font-size: 0.9rem; word-wrap: break-word; overflow-wrap: break-word; word-break: break-word; } -.expand-toggle { margin: 0.25rem 0; background: transparent; border: none; color: rgb(161 161 170); /* zinc-400 */ cursor: pointer; width: 100%; height: 22px; display: flex; align-items: center; justify-content: center; } -.expand-toggle:hover { color: rgb(212 212 216); /* zinc-300 */ } +.bookmark-type { color: var(--color-primary); font-size: 0.9rem; display: flex; align-items: center; gap: 0.35rem; } +.bookmark-id { font-family: monospace; font-size: 0.8rem; color: var(--color-text-secondary); background: var(--color-bg); padding: 0.25rem 0.5rem; border-radius: 4px; } +.bookmark-date { font-size: 0.8rem; color: var(--color-text-muted); } +.bookmark-date-link { font-size: 0.8rem; color: var(--color-text-muted); text-decoration: none; transition: color 0.2s ease; } +.bookmark-date-link:hover { color: var(--color-primary); text-decoration: underline; } +.individual-bookmark .bookmark-content { margin: 0.75rem 0; color: var(--color-text); line-height: 1.6; font-size: 0.9rem; word-wrap: break-word; overflow-wrap: break-word; word-break: break-word; } +.expand-toggle { margin: 0.25rem 0; background: transparent; border: none; color: var(--color-text-secondary); cursor: pointer; width: 100%; height: 22px; display: flex; align-items: center; justify-content: center; } +.expand-toggle:hover { color: var(--color-text); } .bookmark-footer { display: flex; justify-content: space-between; align-items: center; margin-top: 0.75rem; gap: 0.75rem; } -.bookmark-meta-minimal { font-size: 0.8rem; color: rgb(161 161 170); /* zinc-400 */ } -.author-link-minimal { color: rgb(161 161 170); /* zinc-400 */ text-decoration: none; transition: color 0.2s ease; } -.author-link-minimal:hover { color: rgb(212 212 216); /* zinc-300 */ } -.read-now-button-minimal { background: rgb(99 102 241); /* indigo-500 */ color: white; border: none; padding: 0.5rem 1rem; border-radius: 6px; cursor: pointer; font-weight: 600; font-size: 0.85rem; transition: all 0.2s ease; white-space: nowrap; } -.read-now-button-minimal:hover { background: rgb(79 70 229); /* indigo-600 */ } -.expand-toggle-urls { margin-top: 0.5rem; background: transparent; border: none; color: rgb(99 102 241); /* indigo-500 */ cursor: pointer; font-size: 0.8rem; padding: 0.25rem 0; text-decoration: underline; } -.expand-toggle-urls:hover { color: rgb(129 140 248); /* indigo-400 */ } +.bookmark-meta-minimal { font-size: 0.8rem; color: var(--color-text-secondary); } +.author-link-minimal { color: var(--color-text-secondary); text-decoration: none; transition: color 0.2s ease; } +.author-link-minimal:hover { color: var(--color-text); } +.read-now-button-minimal { background: var(--color-primary); color: white; border: none; padding: 0.5rem 1rem; border-radius: 6px; cursor: pointer; font-weight: 600; font-size: 0.85rem; transition: all 0.2s ease; white-space: nowrap; } +.read-now-button-minimal:hover { background: var(--color-primary-hover); } +.expand-toggle-urls { margin-top: 0.5rem; background: transparent; border: none; color: var(--color-primary); cursor: pointer; font-size: 0.8rem; padding: 0.25rem 0; text-decoration: underline; } +.expand-toggle-urls:hover { color: var(--color-primary-hover); } /* Large preview view */ -.individual-bookmark.large { padding: 0; display: flex; flex-direction: column; overflow: hidden; border: 1px solid rgb(39 39 42); /* zinc-800 */ } -.large-preview-image { width: 100%; height: 180px; background: rgb(24 24 27); /* zinc-900 */ background-size: cover; background-position: center; background-repeat: no-repeat; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.2s ease; border-bottom: 1px solid rgb(63 63 70); /* zinc-700 */ position: relative; } +.individual-bookmark.large { padding: 0; display: flex; flex-direction: column; overflow: hidden; border: 1px solid var(--color-bg-elevated); } +.large-preview-image { width: 100%; height: 180px; background: var(--color-bg); background-size: cover; background-position: center; background-repeat: no-repeat; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.2s ease; border-bottom: 1px solid var(--color-border); position: relative; } .large-preview-image:hover { opacity: 0.9; } .large-preview-image::after { content: ''; position: absolute; inset: 0; background: linear-gradient(to bottom, transparent 60%, rgba(0,0,0,0.3) 100%); pointer-events: none; } -.preview-placeholder { font-size: 3rem; color: rgb(82 82 91); /* zinc-600 */ } +.preview-placeholder { font-size: 3rem; color: var(--color-border-subtle); } .large-content { padding: 1.25rem; } -.large-text { color: rgb(212 212 216); /* zinc-300 */ font-size: 0.95rem; line-height: 1.6; margin-bottom: 1rem; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; } -.large-footer { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; font-size: 0.8rem; color: rgb(161 161 170); /* zinc-400 */ padding-top: 0.75rem; border-top: 1px solid rgb(63 63 70); /* zinc-700 */ } +.large-text { color: var(--color-text); font-size: 0.95rem; line-height: 1.6; margin-bottom: 1rem; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; } +.large-footer { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; font-size: 0.8rem; color: var(--color-text-secondary); padding-top: 0.75rem; border-top: 1px solid var(--color-border); } .large-author { flex: 1; } -.large-read-button { background: rgb(99 102 241); /* indigo-500 */ color: white; border: none; padding: 0.5rem 1rem; border-radius: 6px; cursor: pointer; font-weight: 600; font-size: 0.85rem; transition: all 0.2s ease; display: flex; align-items: center; gap: 0.5rem; } -.large-read-button:hover { background: rgb(79 70 229); /* indigo-600 */ } +.large-read-button { background: var(--color-primary); color: white; border: none; padding: 0.5rem 1rem; border-radius: 6px; cursor: pointer; font-weight: 600; font-size: 0.85rem; transition: all 0.2s ease; display: flex; align-items: center; gap: 0.5rem; } +.large-read-button:hover { background: var(--color-primary-hover); } /* Blog cards (Explore) */ .explore-container { padding: 2rem; max-width: 1400px; margin: 0 auto; min-height: 100vh; } .explore-header { text-align: center; margin-bottom: 3rem; } -.explore-header h1 { font-size: 2.5rem; margin: 0 0 1rem 0; color: rgb(99 102 241); /* indigo-500 */ display: flex; align-items: center; justify-content: center; gap: 1rem; } -.explore-subtitle { font-size: 1.125rem; color: rgba(255, 255, 255, 0.7); margin: 0; } -.explore-loading, .explore-error, .explore-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 1rem; color: rgba(255, 255, 255, 0.7); } +.explore-header h1 { font-size: 2.5rem; margin: 0 0 1rem 0; color: var(--color-primary); display: flex; align-items: center; justify-content: center; gap: 1rem; } +.explore-subtitle { font-size: 1.125rem; color: var(--color-text-secondary); margin: 0; } +.explore-loading, .explore-error, .explore-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 1rem; color: var(--color-text-secondary); } .explore-loading { min-height: 0; padding: 0.25rem 0; } .explore-error { color: rgb(239 68 68); /* red-500 */ } -.explore-empty { color: rgb(161 161 170); /* zinc-400 */ } +.explore-empty { color: var(--color-text-secondary); } .explore-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 2rem; margin-top: 2rem; } -.blog-post-card { background: rgb(24 24 27); /* zinc-900 */ border: 1px solid rgb(63 63 70); /* zinc-700 */ border-radius: 12px; overflow: hidden; transition: all 0.3s ease; cursor: pointer; display: flex; flex-direction: column; height: 100%; } -.blog-post-card:hover { border-color: rgb(99 102 241); /* indigo-500 */ transform: translateY(-4px); box-shadow: 0 8px 24px rgba(99, 102, 241, 0.15); } -.blog-post-card-image { width: 100%; height: 200px; overflow: hidden; background: rgb(9 9 11); /* zinc-950 */ display: flex; align-items: center; justify-content: center; } +.blog-post-card { background: var(--color-bg); border: 1px solid var(--color-border); border-radius: 12px; overflow: hidden; transition: all 0.3s ease; cursor: pointer; display: flex; flex-direction: column; height: 100%; } +.blog-post-card:hover { border-color: var(--color-primary); transform: translateY(-4px); box-shadow: 0 8px 24px rgba(99, 102, 241, 0.15); } +.blog-post-card-image { width: 100%; height: 200px; overflow: hidden; background: var(--color-bg-subtle); display: flex; align-items: center; justify-content: center; } .blog-post-card-image img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.3s ease; } .blog-post-card:hover .blog-post-card-image img { transform: scale(1.05); } -.blog-post-image-placeholder { font-size: 3rem; color: rgb(82 82 91); /* zinc-600 */ display: flex; align-items: center; justify-content: center; } +.blog-post-image-placeholder { font-size: 3rem; color: var(--color-border-subtle); display: flex; align-items: center; justify-content: center; } .blog-post-card-content { padding: 1.5rem; display: flex; flex-direction: column; gap: 1rem; flex: 1; } -.blog-post-card-title { font-size: 1.25rem; font-weight: 600; margin: 0; color: rgba(255, 255, 255, 0.95); line-height: 1.4; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; } -.blog-post-card-summary { font-size: 0.875rem; color: rgba(255, 255, 255, 0.6); margin: 0; line-height: 1.6; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; flex: 1; } -.blog-post-card-meta { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding-top: 0.75rem; border-top: 1px solid rgb(63 63 70); /* zinc-700 */ font-size: 0.75rem; color: rgba(255, 255, 255, 0.5); flex-wrap: wrap; } +.blog-post-card-title { font-size: 1.25rem; font-weight: 600; margin: 0; color: var(--color-text); line-height: 1.4; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; } +.blog-post-card-summary { font-size: 0.875rem; color: var(--color-text-secondary); margin: 0; line-height: 1.6; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; flex: 1; } +.blog-post-card-meta { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding-top: 0.75rem; border-top: 1px solid var(--color-border); font-size: 0.75rem; color: var(--color-text-muted); flex-wrap: wrap; } .blog-post-card-author, .blog-post-card-date { display: flex; align-items: center; gap: 0.5rem; } .blog-post-card-author svg, .blog-post-card-date svg { opacity: 0.7; } @media (max-width: 768px) { @@ -98,4 +98,3 @@ .blog-post-card-content { padding: 1rem; } } - diff --git a/src/styles/components/forms.css b/src/styles/components/forms.css index 3d219513..acd972fd 100644 --- a/src/styles/components/forms.css +++ b/src/styles/components/forms.css @@ -4,28 +4,28 @@ .setting-label { text-align: left; flex: 1; } .setting-control { display: flex; justify-content: flex-end; align-items: center; } .setting-group.setting-inline label { margin-bottom: 0; } -.setting-group label { display: block; margin-bottom: 0.5rem; color: rgb(212 212 216); /* zinc-300 */ font-weight: 500; text-align: left; } +.setting-group label { display: block; margin-bottom: 0.5rem; color: var(--color-text); font-weight: 500; text-align: left; } .setting-buttons { display: flex; align-items: center; gap: 0.5rem; } .color-picker { display: flex; align-items: center; gap: 0.5rem; } -.color-swatch { width: 33px; height: 33px; border: 1px solid rgb(82 82 91); /* zinc-600 */ border-radius: 6px; cursor: pointer; transition: all 0.2s; position: relative; } -.color-swatch:hover { border-color: rgb(161 161 170); /* zinc-400 */ } -.color-swatch.active { border-color: rgb(99 102 241); /* indigo-500 */ box-shadow: 0 0 0 2px rgb(99 102 241); /* indigo-500 */ } -.color-swatch.active::after { content: '✓'; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: rgb(0 0 0); /* black */ font-size: 0.875rem; font-weight: bold; text-shadow: 0 0 2px rgb(255 255 255); /* white */ } -.font-size-btn { min-width: 33px; height: 33px; padding: 0; background: transparent; border: 1px solid rgb(82 82 91); /* zinc-600 */ border-radius: 6px; color: rgb(212 212 216); /* zinc-300 */ cursor: pointer; transition: all 0.2s; font-weight: bold; display: flex; align-items: center; justify-content: center; } -.font-size-btn:hover { background: rgb(63 63 70); /* zinc-700 */ border-color: rgb(113 113 122); /* zinc-500 */ } -.font-size-btn.active { background: rgb(99 102 241); /* indigo-500 */ border-color: rgb(99 102 241); /* indigo-500 */ color: white; } +.color-swatch { width: 33px; height: 33px; border: 1px solid var(--color-border-subtle); border-radius: 6px; cursor: pointer; transition: all 0.2s; position: relative; } +.color-swatch:hover { border-color: var(--color-text-secondary); } +.color-swatch.active { border-color: var(--color-primary); box-shadow: 0 0 0 2px var(--color-primary); } +.color-swatch.active::after { content: '✓'; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: rgb(0 0 0); font-size: 0.875rem; font-weight: bold; text-shadow: 0 0 2px rgb(255 255 255); } +.font-size-btn { min-width: 33px; height: 33px; padding: 0; background: transparent; border: 1px solid var(--color-border-subtle); border-radius: 6px; color: var(--color-text); cursor: pointer; transition: all 0.2s; font-weight: bold; display: flex; align-items: center; justify-content: center; } +.font-size-btn:hover { background: var(--color-border); border-color: var(--color-text-muted); } +.font-size-btn.active { background: var(--color-primary); border-color: var(--color-primary); color: white; } .setting-preview { margin: 1.5rem 0; padding: 1rem; - background: rgb(24 24 27); /* zinc-900 */ - border: 1px solid rgb(63 63 70); /* zinc-700 */ + background: var(--color-bg); + border: 1px solid var(--color-border); border-radius: 8px; max-width: 100%; overflow: hidden; } -.preview-label { font-size: 0.875rem; color: rgb(161 161 170); /* zinc-400 */ margin-bottom: 0.75rem; font-weight: 500; text-transform: uppercase; letter-spacing: 0.05em; } +.preview-label { font-size: 0.875rem; color: var(--color-text-secondary); margin-bottom: 0.75rem; font-weight: 500; text-transform: uppercase; letter-spacing: 0.05em; } .preview-content { - color: rgb(228 228 231); /* zinc-200 */ + color: var(--color-text); line-height: 1.7; max-width: 100%; overflow-wrap: break-word; @@ -35,20 +35,20 @@ .preview-content h3 { margin: 0 0 1rem 0; font-size: 1.5em; - color: rgb(255 255 255); /* white */ + color: var(--color-text); word-wrap: break-word; } .preview-content p { margin: 0.75rem 0; word-wrap: break-word; } -.setting-select { width: 100%; padding: 0.5rem; background: rgb(39 39 42); /* zinc-800 */ border: 1px solid rgb(82 82 91); /* zinc-600 */ border-radius: 4px; color: rgb(255 255 255); /* white */ font-size: 1rem; } +.setting-select { width: 100%; padding: 0.5rem; background: var(--color-bg-elevated); border: 1px solid var(--color-border-subtle); border-radius: 4px; color: var(--color-text); font-size: 1rem; } .setting-inline .setting-select { width: auto; min-width: 200px; flex: 1; } -.setting-select:focus { outline: none; border-color: rgb(99 102 241); /* indigo-500 */ } +.setting-select:focus { outline: none; border-color: var(--color-primary); } .font-select option { padding: 0.5rem; font-size: 1rem; } .checkbox-label { display: flex !important; align-items: center; gap: 0.75rem; cursor: pointer; user-select: none; text-align: left; justify-content: flex-start; margin-bottom: 0 !important; font-weight: normal !important; } -.setting-checkbox { width: 18px; height: 18px; cursor: pointer; flex-shrink: 0; margin: 0; accent-color: rgb(99 102 241); /* indigo-500 */ } -.checkbox-label span { color: rgb(228 228 231); /* zinc-200 */ text-align: left; font-weight: 500; } +.setting-checkbox { width: 18px; height: 18px; cursor: pointer; flex-shrink: 0; margin: 0; accent-color: var(--color-primary); } +.checkbox-label span { color: var(--color-text); text-align: left; font-weight: 500; } /* Mobile responsive styles */ @media (max-width: 768px) { @@ -81,4 +81,3 @@ } } - diff --git a/src/styles/layout/app.css b/src/styles/layout/app.css index 6b5ff94a..a1dbc9b3 100644 --- a/src/styles/layout/app.css +++ b/src/styles/layout/app.css @@ -111,7 +111,7 @@ max-width: 320px; height: 100vh; height: 100dvh; - background: rgb(24 24 27); /* zinc-900 */ + background: var(--color-bg); z-index: 1001; /* Above backdrop */ transition: transform 0.3s ease; box-shadow: none; diff --git a/src/utils/theme.ts b/src/utils/theme.ts new file mode 100644 index 00000000..44907f17 --- /dev/null +++ b/src/utils/theme.ts @@ -0,0 +1,64 @@ +export type Theme = 'dark' | 'light' | 'system' + +let mediaQueryListener: ((e: MediaQueryListEvent) => void) | null = null + +/** + * Get the system's current theme preference + */ +export function getSystemTheme(): 'dark' | 'light' { + if (typeof window === 'undefined') return 'dark' + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' +} + +/** + * Apply theme to the document root element + * Handles 'system' theme by listening to OS preference changes + */ +export function applyTheme(theme: Theme): void { + const root = document.documentElement + + // Remove existing theme classes + root.classList.remove('theme-dark', 'theme-light', 'theme-system') + + // Clean up previous media query listener if exists + if (mediaQueryListener) { + window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', mediaQueryListener) + mediaQueryListener = null + } + + if (theme === 'system') { + root.classList.add('theme-system') + + // Listen for system theme changes + mediaQueryListener = (e: MediaQueryListEvent) => { + console.log('🎨 System theme changed to:', e.matches ? 'dark' : 'light') + // The CSS media query handles the color changes + // We just need to update localStorage for no-FOUC on next load + localStorage.setItem('theme-system-current', e.matches ? 'dark' : 'light') + } + + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', mediaQueryListener) + + // Store current system theme for no-FOUC on next boot + localStorage.setItem('theme-system-current', getSystemTheme()) + } else { + root.classList.add(`theme-${theme}`) + } + + // Persist to localStorage for early boot application + localStorage.setItem('theme', theme) + + console.log('🎨 Applied theme:', theme) +} + +/** + * Get the current theme from localStorage or default to 'system' + */ +export function getStoredTheme(): Theme { + const stored = localStorage.getItem('theme') + if (stored === 'dark' || stored === 'light' || stored === 'system') { + return stored + } + return 'system' +} +