feat: add light theme with full WCAG AAA contrast compliance
This commit is contained in:
11
src/App.tsx
11
src/App.tsx
@@ -328,6 +328,9 @@ function App() {
|
|||||||
const [showShortcutsModal, setShowShortcutsModal] = useState(false);
|
const [showShortcutsModal, setShowShortcutsModal] = useState(false);
|
||||||
const [showAboutModal, setShowAboutModal] = useState(false);
|
const [showAboutModal, setShowAboutModal] = useState(false);
|
||||||
const [zoom, setZoom] = useState(100);
|
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 [uiZoom, setUiZoom] = useState(() => {
|
||||||
const saved = localStorage.getItem('vesper-ui-zoom');
|
const saved = localStorage.getItem('vesper-ui-zoom');
|
||||||
return saved ? parseInt(saved, 10) : 100;
|
return saved ? parseInt(saved, 10) : 100;
|
||||||
@@ -863,6 +866,11 @@ function App() {
|
|||||||
localStorage.setItem('vesper-ui-zoom', String(Math.round(uiZoom)));
|
localStorage.setItem('vesper-ui-zoom', String(Math.round(uiZoom)));
|
||||||
}, [uiZoom]);
|
}, [uiZoom]);
|
||||||
|
|
||||||
|
// Persist theme to localStorage
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem('vesper-theme', theme);
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
// Menu bar keyboard navigation (WAI-ARIA menu pattern)
|
// Menu bar keyboard navigation (WAI-ARIA menu pattern)
|
||||||
const handleMenuBarKeyDown = (e: React.KeyboardEvent) => {
|
const handleMenuBarKeyDown = (e: React.KeyboardEvent) => {
|
||||||
if (e.key === 'ArrowRight') {
|
if (e.key === 'ArrowRight') {
|
||||||
@@ -1080,7 +1088,7 @@ function App() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`app-container ${focusMode ? 'focus-mode' : ''}`} style={{ zoom: `${uiZoom}%` }}>
|
<div className={`app-container ${focusMode ? 'focus-mode' : ''} ${theme === 'light' ? 'theme-light' : ''}`} style={{ zoom: `${uiZoom}%` }}>
|
||||||
<a href="#main-content" className="skip-link">Skip to content</a>
|
<a href="#main-content" className="skip-link">Skip to content</a>
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{isDraggingOver && (
|
{isDraggingOver && (
|
||||||
@@ -1133,6 +1141,7 @@ function App() {
|
|||||||
<button className="menu-dropdown-item" role="menuitem" onClick={() => { setShowSearch(s => !s); setMenuOpen(null); }}>Toggle Search <span className="menu-shortcut">Ctrl+F</span></button>
|
<button className="menu-dropdown-item" role="menuitem" onClick={() => { setShowSearch(s => !s); setMenuOpen(null); }}>Toggle Search <span className="menu-shortcut">Ctrl+F</span></button>
|
||||||
<button className="menu-dropdown-item" role="menuitem" onClick={() => { setShowSidebar(s => !s); setMenuOpen(null); }}>Toggle Sidebar <span className="menu-shortcut">Ctrl+Shift+S</span></button>
|
<button className="menu-dropdown-item" role="menuitem" onClick={() => { setShowSidebar(s => !s); setMenuOpen(null); }}>Toggle Sidebar <span className="menu-shortcut">Ctrl+Shift+S</span></button>
|
||||||
<button className="menu-dropdown-item" role="menuitem" onClick={() => { setFocusMode(f => !f); setMenuOpen(null); }}>Focus Mode <span className="menu-shortcut">F11</span></button>
|
<button className="menu-dropdown-item" role="menuitem" onClick={() => { setFocusMode(f => !f); setMenuOpen(null); }}>Focus Mode <span className="menu-shortcut">F11</span></button>
|
||||||
|
<button className="menu-dropdown-item" role="menuitem" onClick={() => { setTheme(t => t === 'dark' ? 'light' : 'dark'); setMenuOpen(null); }}>Theme: {theme === 'dark' ? 'Dark' : 'Light'} <span className="menu-shortcut">{theme === 'dark' ? 'Switch to Light' : 'Switch to Dark'}</span></button>
|
||||||
<div className="menu-separator"></div>
|
<div className="menu-separator"></div>
|
||||||
<div className="menu-dropdown-zoom" onClick={e => e.stopPropagation()}>
|
<div className="menu-dropdown-zoom" onClick={e => e.stopPropagation()}>
|
||||||
<span>UI Scale</span>
|
<span>UI Scale</span>
|
||||||
|
|||||||
122
src/styles.css
122
src/styles.css
@@ -1674,6 +1674,128 @@ input:focus-visible,
|
|||||||
z-index: 1;
|
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 */
|
/* Reduced motion */
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
*,
|
*,
|
||||||
|
|||||||
Reference in New Issue
Block a user