feat: add persistent light/dark mode toggle

- Add CSS custom property token system to globals.css (dark + light variants)
- Add anti-FOUC inline script in layout.tsx head to apply stored theme before hydration
- Create useTheme hook (src/hooks/useTheme.ts) backed by usePersistentState
- Add Sun/Moon toggle button to LeftToolbar above Settings
- Wire useTheme into ThreePanelLayout and pass theme/onThemeToggle to LeftToolbar
- Migrate hardcoded hex values to CSS vars in LeftToolbar and ThreePanelLayout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
“BeeRad”
2026-03-17 19:32:06 +11:00
co-authored by Claude Sonnet 4.6
parent 4802187f5e
commit bf175ad125
5 changed files with 144 additions and 13 deletions
+18 -5
View File
@@ -13,8 +13,11 @@ import {
Settings,
PanelLeftClose,
PanelLeftOpen,
Sun,
Moon,
} from 'lucide-react';
import type { PaneType } from '../panes/types';
import type { Theme } from '@/hooks/useTheme';
interface LeftToolbarProps {
onSearchClick: () => void;
@@ -26,6 +29,8 @@ interface LeftToolbarProps {
onToggleExpanded: () => void;
openTabTypes: Set<PaneType>;
activeTabType: PaneType | null;
theme: Theme;
onThemeToggle: () => void;
}
const NAV_WIDTH_COLLAPSED = 50;
@@ -57,7 +62,7 @@ function NavButton({
activeTone?: 'neutral' | 'green';
}) {
const [hovered, setHovered] = useState(false);
const activeColor = activeTone === 'green' ? '#22c55e' : '#f0f0f0';
const activeColor = activeTone === 'green' ? 'var(--rah-accent-green)' : 'var(--rah-text-active)';
return (
<button
@@ -70,8 +75,8 @@ function NavButton({
height: '36px',
borderRadius: '8px',
border: 'none',
background: active ? '#151515' : (hovered ? '#121212' : 'transparent'),
color: active ? activeColor : (hovered ? '#c7c7c7' : '#7a7a7a'),
background: active ? 'var(--rah-bg-hover)' : (hovered ? 'var(--rah-bg-subtle)' : 'transparent'),
color: active ? activeColor : (hovered ? 'var(--rah-text-secondary)' : 'var(--rah-text-muted)'),
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
@@ -104,6 +109,8 @@ export default function LeftToolbar({
onToggleExpanded,
openTabTypes,
activeTabType,
theme,
onThemeToggle,
}: LeftToolbarProps) {
return (
<div
@@ -134,7 +141,7 @@ export default function LeftToolbar({
<NavButton icon={RefreshCw} label="Refresh" expanded={isExpanded} onClick={onRefreshClick} />
</div>
<div style={{ borderTop: '1px solid #1a1a1a', paddingTop: '14px', marginTop: '10px' }}>
<div style={{ borderTop: '1px solid var(--rah-border)', paddingTop: '14px', marginTop: '10px' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px', paddingTop: '4px' }}>
{VIEW_ITEMS.map((item) => (
<NavButton
@@ -151,7 +158,13 @@ export default function LeftToolbar({
</div>
</div>
<div style={{ borderTop: '1px solid #1a1a1a', paddingTop: '8px' }}>
<div style={{ borderTop: '1px solid var(--rah-border)', paddingTop: '8px' }}>
<NavButton
icon={theme === 'dark' ? Sun : Moon}
label={theme === 'dark' ? 'Light Mode' : 'Dark Mode'}
expanded={isExpanded}
onClick={onThemeToggle}
/>
<NavButton icon={Settings} label="Settings" expanded={isExpanded} onClick={onSettingsClick} />
</div>
</div>
+6 -1
View File
@@ -6,6 +6,7 @@ import SearchModal from '../nodes/SearchModal';
import { Node } from '@/types/database';
import { DatabaseEvent } from '@/services/events';
import { usePersistentState } from '@/hooks/usePersistentState';
import { useTheme } from '@/hooks/useTheme';
// ChatMessage import removed - chat disabled in rah-light
// Stub type for delegation (delegation system removed in rah-light)
@@ -40,6 +41,8 @@ import QuickAddInput from '../agents/QuickAddInput';
import type { PaneType, SlotState, PaneAction } from '../panes/types';
export default function ThreePanelLayout() {
const [theme, toggleTheme] = useTheme();
// Container ref for resize calculations
const containerRef = useRef<HTMLDivElement>(null);
@@ -1014,7 +1017,7 @@ export default function ThreePanelLayout() {
display: 'flex',
height: '100vh',
width: '100vw',
background: '#0a0a0a',
background: 'var(--rah-bg-base)',
overflow: 'hidden'
}}
>
@@ -1032,6 +1035,8 @@ export default function ThreePanelLayout() {
openTabTypes={new Set([slotA?.type, slotB?.type].filter((t): t is PaneType => t != null))}
activeTabType={activePane === 'A' ? slotA?.type ?? null : slotB?.type ?? null}
onRefreshClick={handleRefreshAll}
theme={theme}
onThemeToggle={toggleTheme}
/>
{/* Main content area */}