diff --git a/src/App.tsx b/src/App.tsx index 6e1ab8d..4c495f8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -328,6 +328,9 @@ function App() { const [showShortcutsModal, setShowShortcutsModal] = useState(false); const [showAboutModal, setShowAboutModal] = useState(false); const [zoom, setZoom] = useState(100); + const [theme, setTheme] = useState<'dark' | 'light'>(() => { + return (localStorage.getItem('vesper-theme') as 'dark' | 'light') || 'dark'; + }); const [uiZoom, setUiZoom] = useState(() => { const saved = localStorage.getItem('vesper-ui-zoom'); return saved ? parseInt(saved, 10) : 100; @@ -863,6 +866,11 @@ function App() { localStorage.setItem('vesper-ui-zoom', String(Math.round(uiZoom))); }, [uiZoom]); + // Persist theme to localStorage + useEffect(() => { + localStorage.setItem('vesper-theme', theme); + }, [theme]); + // Menu bar keyboard navigation (WAI-ARIA menu pattern) const handleMenuBarKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'ArrowRight') { @@ -1080,7 +1088,7 @@ function App() { }; return ( -
+
Skip to content {isDraggingOver && ( @@ -1133,6 +1141,7 @@ function App() { +
e.stopPropagation()}> UI Scale diff --git a/src/styles.css b/src/styles.css index 2d157ca..0f04fd8 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1674,6 +1674,128 @@ input:focus-visible, z-index: 1; } +/* ============================================ + Light theme overrides — WCAG 1.4.8 + ============================================ */ +.theme-light { + --color-bg-base: #F5F6F8; + --color-bg-surface: #EBEDF0; + --color-bg-elevated: #FFFFFF; + --color-bg-overlay: #FFFFFF; + --color-bg-hover: #E2E4E8; + --color-bg-active: #D5D8DD; + + --color-text-primary: #1A1D24; + --color-text-secondary: #4A5060; + --color-text-tertiary: #6B7280; + --color-text-disabled: #9CA3AF; + + --color-accent: #3B66E0; + --color-accent-hover: #2B52CC; + --color-accent-muted: rgba(59, 102, 224, 0.10); + --color-accent-subtle: rgba(59, 102, 224, 0.05); + --color-accent-small: #2B52CC; + + --color-border-subtle: rgba(0, 0, 0, 0.06); + --color-border: rgba(0, 0, 0, 0.10); + --color-border-strong: rgba(0, 0, 0, 0.16); + + /* Markdown typography — light variants */ + --color-md-h1: #111318; + --color-md-h2: #1A1D24; + --color-md-h3: #252A31; + --color-md-h4: #333942; + --color-md-h5: #404754; + --color-md-h6: #4A5060; + --color-md-heading: #1A1D24; + --color-md-paragraph: #333942; + --color-md-link: #2B5BC2; + --color-md-link-underline: rgba(43, 91, 194, 0.3); + --color-md-link-hover: #1E4AAE; + --color-md-link-hover-underline: rgba(30, 74, 174, 0.5); + --color-md-marker: #3B66E0; + --color-md-blockquote-border: #3B66E0; + --color-md-blockquote-bg: rgba(59, 102, 224, 0.04); + --color-md-blockquote-text: #4A5060; + --color-md-code-inline: #B35C2A; + --color-md-code-inline-bg: rgba(0, 0, 0, 0.04); + --color-md-code-block-bg: #F0F1F3; + --color-md-code-block-border: rgba(0, 0, 0, 0.08); + --color-md-code-text: #1A1D24; + --color-md-table-header-bg: rgba(59, 102, 224, 0.06); + --color-md-table-border: rgba(0, 0, 0, 0.08); + --color-md-table-cell: #333942; + --color-md-table-stripe: rgba(0, 0, 0, 0.02); + --color-md-table-hover: rgba(59, 102, 224, 0.04); + --color-md-hr: rgba(0, 0, 0, 0.1); + --color-md-mark-bg: rgba(251, 191, 36, 0.25); + --color-md-mark-text: #92650B; + --color-md-comment: #6B7280; + --color-md-highlight-bg: rgba(251, 191, 36, 0.3); + --color-md-highlight-active-bg: rgba(251, 191, 36, 0.5); + --color-md-highlight-active-outline: rgba(200, 150, 20, 0.7); + --color-md-welcome-btn: #3B66E0; +} + +/* Light theme — noise overlay should be darker */ +.theme-light.app-container::before { + opacity: 0.025; + mix-blend-mode: multiply; +} + +/* Light theme — title bar close button */ +.theme-light .title-bar-button.close:hover { + background-color: #DC2626; + color: white; +} + +/* Light theme — scrollbar colors */ +.theme-light .os-theme-dark { + --os-handle-bg: rgba(0, 0, 0, 0.15); + --os-handle-bg-hover: var(--color-accent); + --os-handle-bg-active: var(--color-accent-hover); +} + +/* Light theme — sidebar resize handle */ +.theme-light .sidebar-resize-handle { + background: rgba(0, 0, 0, 0.06); +} + +.theme-light .sidebar-resize-handle:hover, +.theme-light .sidebar-resize-handle:focus-visible { + background-color: var(--color-accent); +} + +/* Light theme syntax highlighting */ +.theme-light .hljs { color: #1A1D24; } +.theme-light .hljs-keyword, +.theme-light .hljs-selector-tag, +.theme-light .hljs-literal, +.theme-light .hljs-section, +.theme-light .hljs-link { color: #7C3AED; font-weight: 500; } + +.theme-light .hljs-string, +.theme-light .hljs-title, +.theme-light .hljs-name, +.theme-light .hljs-type, +.theme-light .hljs-attribute, +.theme-light .hljs-symbol, +.theme-light .hljs-bullet, +.theme-light .hljs-addition, +.theme-light .hljs-variable, +.theme-light .hljs-template-tag, +.theme-light .hljs-template-variable { color: #16652B; } + +.theme-light .hljs-comment, +.theme-light .hljs-quote, +.theme-light .hljs-deletion, +.theme-light .hljs-meta { color: var(--color-md-comment); font-style: italic; } + +.theme-light .hljs-number { color: #C2410C; font-weight: 500; } +.theme-light .hljs-built_in { color: #2563EB; } +.theme-light .hljs-class .hljs-title { color: #B45309; } +.theme-light .hljs-attr { color: #B45309; } + /* Reduced motion */ @media (prefers-reduced-motion: reduce) { *,