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:
co-authored by
Claude Sonnet 4.6
parent
4802187f5e
commit
bf175ad125
+67
-6
@@ -36,6 +36,67 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
:root,
|
||||||
|
html[data-theme="dark"] {
|
||||||
|
color-scheme: dark;
|
||||||
|
--rah-bg-base: #0a0a0a;
|
||||||
|
--rah-bg-surface: #111111;
|
||||||
|
--rah-bg-subtle: #121212;
|
||||||
|
--rah-bg-panel: #141414;
|
||||||
|
--rah-bg-hover: #151515;
|
||||||
|
--rah-bg-elevated: #161616;
|
||||||
|
--rah-bg-active: #1a1a1a;
|
||||||
|
--rah-bg-modal: #141414;
|
||||||
|
--rah-border: #1a1a1a;
|
||||||
|
--rah-border-strong: #2a2a2a;
|
||||||
|
--rah-border-stronger: #3a3a3a;
|
||||||
|
--rah-scrollbar: #353535;
|
||||||
|
--rah-scrollbar-hover: #4a4a4a;
|
||||||
|
--rah-text-base: #e5e5e5;
|
||||||
|
--rah-text-secondary: #c7c7c7;
|
||||||
|
--rah-text-muted: #7a7a7a;
|
||||||
|
--rah-text-soft: #a3a3a3;
|
||||||
|
--rah-text-active: #f0f0f0;
|
||||||
|
--rah-text-inverse: #0a0a0a;
|
||||||
|
--rah-accent-green: #22c55e;
|
||||||
|
--rah-accent-green-hover: #16a34a;
|
||||||
|
--rah-accent-green-soft: rgba(34, 197, 94, 0.14);
|
||||||
|
--rah-accent-green-soft-strong: rgba(34, 197, 94, 0.22);
|
||||||
|
--rah-backdrop: rgba(0, 0, 0, 0.85);
|
||||||
|
--rah-shadow-modal: 0 8px 32px rgba(0, 0, 0, 0.8), 0 0 0 1px rgba(255, 255, 255, 0.05);
|
||||||
|
--rah-shadow-floating: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="light"] {
|
||||||
|
color-scheme: light;
|
||||||
|
--rah-bg-base: #ffffff;
|
||||||
|
--rah-bg-surface: #fafafa;
|
||||||
|
--rah-bg-subtle: #f7f7f7;
|
||||||
|
--rah-bg-panel: #f5f5f5;
|
||||||
|
--rah-bg-hover: #f0f0f0;
|
||||||
|
--rah-bg-elevated: #ececec;
|
||||||
|
--rah-bg-active: #e8e8e8;
|
||||||
|
--rah-bg-modal: #ffffff;
|
||||||
|
--rah-border: #e5e5e5;
|
||||||
|
--rah-border-strong: #d4d4d4;
|
||||||
|
--rah-border-stronger: #bdbdbd;
|
||||||
|
--rah-scrollbar: #d4d4d4;
|
||||||
|
--rah-scrollbar-hover: #b5b5b5;
|
||||||
|
--rah-text-base: #111111;
|
||||||
|
--rah-text-secondary: #333333;
|
||||||
|
--rah-text-muted: #6b7280;
|
||||||
|
--rah-text-soft: #4b5563;
|
||||||
|
--rah-text-active: #000000;
|
||||||
|
--rah-text-inverse: #ffffff;
|
||||||
|
--rah-accent-green: #16a34a;
|
||||||
|
--rah-accent-green-hover: #15803d;
|
||||||
|
--rah-accent-green-soft: rgba(22, 163, 74, 0.12);
|
||||||
|
--rah-accent-green-soft-strong: rgba(22, 163, 74, 0.2);
|
||||||
|
--rah-backdrop: rgba(0, 0, 0, 0.5);
|
||||||
|
--rah-shadow-modal: 0 10px 36px rgba(0, 0, 0, 0.18), 0 0 0 1px rgba(0, 0, 0, 0.04);
|
||||||
|
--rah-shadow-floating: 0 6px 20px rgba(0, 0, 0, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
/* Modern Clean Design System - RA-H */
|
/* Modern Clean Design System - RA-H */
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -46,8 +107,8 @@
|
|||||||
html, body {
|
html, body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background-color: #0a0a0a;
|
background-color: var(--rah-bg-base);
|
||||||
color: #e5e5e5;
|
color: var(--rah-text-base);
|
||||||
/* Geist Sans for optimal reading/writing experience */
|
/* Geist Sans for optimal reading/writing experience */
|
||||||
font-family: 'Geist', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
font-family: 'Geist', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -71,21 +132,21 @@ html, body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background: #0a0a0a;
|
background: var(--rah-bg-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: #353535;
|
background: var(--rah-scrollbar);
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: #4a4a4a;
|
background: var(--rah-scrollbar-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Utility classes */
|
/* Utility classes */
|
||||||
.border-divider {
|
.border-divider {
|
||||||
border-color: #1a1a1a;
|
border-color: var(--rah-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-muted {
|
.text-muted {
|
||||||
|
|||||||
+25
-1
@@ -13,7 +13,31 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en" suppressHydrationWarning>
|
||||||
|
<head>
|
||||||
|
<script
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: `
|
||||||
|
(function() {
|
||||||
|
try {
|
||||||
|
var rawTheme = localStorage.getItem('ui.theme');
|
||||||
|
var theme = 'dark';
|
||||||
|
if (rawTheme !== null) {
|
||||||
|
try {
|
||||||
|
theme = JSON.parse(rawTheme) === 'light' ? 'light' : 'dark';
|
||||||
|
} catch (parseError) {
|
||||||
|
theme = rawTheme === 'light' ? 'light' : 'dark';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.documentElement.setAttribute('data-theme', theme);
|
||||||
|
} catch (error) {
|
||||||
|
document.documentElement.setAttribute('data-theme', 'dark');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<DimensionIconsProvider>
|
<DimensionIconsProvider>
|
||||||
<ExternalNavigationManager />
|
<ExternalNavigationManager />
|
||||||
|
|||||||
@@ -13,8 +13,11 @@ import {
|
|||||||
Settings,
|
Settings,
|
||||||
PanelLeftClose,
|
PanelLeftClose,
|
||||||
PanelLeftOpen,
|
PanelLeftOpen,
|
||||||
|
Sun,
|
||||||
|
Moon,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import type { PaneType } from '../panes/types';
|
import type { PaneType } from '../panes/types';
|
||||||
|
import type { Theme } from '@/hooks/useTheme';
|
||||||
|
|
||||||
interface LeftToolbarProps {
|
interface LeftToolbarProps {
|
||||||
onSearchClick: () => void;
|
onSearchClick: () => void;
|
||||||
@@ -26,6 +29,8 @@ interface LeftToolbarProps {
|
|||||||
onToggleExpanded: () => void;
|
onToggleExpanded: () => void;
|
||||||
openTabTypes: Set<PaneType>;
|
openTabTypes: Set<PaneType>;
|
||||||
activeTabType: PaneType | null;
|
activeTabType: PaneType | null;
|
||||||
|
theme: Theme;
|
||||||
|
onThemeToggle: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NAV_WIDTH_COLLAPSED = 50;
|
const NAV_WIDTH_COLLAPSED = 50;
|
||||||
@@ -57,7 +62,7 @@ function NavButton({
|
|||||||
activeTone?: 'neutral' | 'green';
|
activeTone?: 'neutral' | 'green';
|
||||||
}) {
|
}) {
|
||||||
const [hovered, setHovered] = useState(false);
|
const [hovered, setHovered] = useState(false);
|
||||||
const activeColor = activeTone === 'green' ? '#22c55e' : '#f0f0f0';
|
const activeColor = activeTone === 'green' ? 'var(--rah-accent-green)' : 'var(--rah-text-active)';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
@@ -70,8 +75,8 @@ function NavButton({
|
|||||||
height: '36px',
|
height: '36px',
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
background: active ? '#151515' : (hovered ? '#121212' : 'transparent'),
|
background: active ? 'var(--rah-bg-hover)' : (hovered ? 'var(--rah-bg-subtle)' : 'transparent'),
|
||||||
color: active ? activeColor : (hovered ? '#c7c7c7' : '#7a7a7a'),
|
color: active ? activeColor : (hovered ? 'var(--rah-text-secondary)' : 'var(--rah-text-muted)'),
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@@ -104,6 +109,8 @@ export default function LeftToolbar({
|
|||||||
onToggleExpanded,
|
onToggleExpanded,
|
||||||
openTabTypes,
|
openTabTypes,
|
||||||
activeTabType,
|
activeTabType,
|
||||||
|
theme,
|
||||||
|
onThemeToggle,
|
||||||
}: LeftToolbarProps) {
|
}: LeftToolbarProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -134,7 +141,7 @@ export default function LeftToolbar({
|
|||||||
<NavButton icon={RefreshCw} label="Refresh" expanded={isExpanded} onClick={onRefreshClick} />
|
<NavButton icon={RefreshCw} label="Refresh" expanded={isExpanded} onClick={onRefreshClick} />
|
||||||
</div>
|
</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' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px', paddingTop: '4px' }}>
|
||||||
{VIEW_ITEMS.map((item) => (
|
{VIEW_ITEMS.map((item) => (
|
||||||
<NavButton
|
<NavButton
|
||||||
@@ -151,7 +158,13 @@ export default function LeftToolbar({
|
|||||||
</div>
|
</div>
|
||||||
</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} />
|
<NavButton icon={Settings} label="Settings" expanded={isExpanded} onClick={onSettingsClick} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import SearchModal from '../nodes/SearchModal';
|
|||||||
import { Node } from '@/types/database';
|
import { Node } from '@/types/database';
|
||||||
import { DatabaseEvent } from '@/services/events';
|
import { DatabaseEvent } from '@/services/events';
|
||||||
import { usePersistentState } from '@/hooks/usePersistentState';
|
import { usePersistentState } from '@/hooks/usePersistentState';
|
||||||
|
import { useTheme } from '@/hooks/useTheme';
|
||||||
// ChatMessage import removed - chat disabled in rah-light
|
// ChatMessage import removed - chat disabled in rah-light
|
||||||
|
|
||||||
// Stub type for delegation (delegation system removed 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';
|
import type { PaneType, SlotState, PaneAction } from '../panes/types';
|
||||||
|
|
||||||
export default function ThreePanelLayout() {
|
export default function ThreePanelLayout() {
|
||||||
|
const [theme, toggleTheme] = useTheme();
|
||||||
|
|
||||||
// Container ref for resize calculations
|
// Container ref for resize calculations
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@@ -1014,7 +1017,7 @@ export default function ThreePanelLayout() {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
height: '100vh',
|
height: '100vh',
|
||||||
width: '100vw',
|
width: '100vw',
|
||||||
background: '#0a0a0a',
|
background: 'var(--rah-bg-base)',
|
||||||
overflow: 'hidden'
|
overflow: 'hidden'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -1032,6 +1035,8 @@ export default function ThreePanelLayout() {
|
|||||||
openTabTypes={new Set([slotA?.type, slotB?.type].filter((t): t is PaneType => t != null))}
|
openTabTypes={new Set([slotA?.type, slotB?.type].filter((t): t is PaneType => t != null))}
|
||||||
activeTabType={activePane === 'A' ? slotA?.type ?? null : slotB?.type ?? null}
|
activeTabType={activePane === 'A' ? slotA?.type ?? null : slotB?.type ?? null}
|
||||||
onRefreshClick={handleRefreshAll}
|
onRefreshClick={handleRefreshAll}
|
||||||
|
theme={theme}
|
||||||
|
onThemeToggle={toggleTheme}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Main content area */}
|
{/* Main content area */}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { usePersistentState } from './usePersistentState';
|
||||||
|
|
||||||
|
export type Theme = 'dark' | 'light';
|
||||||
|
|
||||||
|
function getInitialTheme(): Theme {
|
||||||
|
if (typeof document === 'undefined') {
|
||||||
|
return 'dark';
|
||||||
|
}
|
||||||
|
|
||||||
|
return document.documentElement.getAttribute('data-theme') === 'light' ? 'light' : 'dark';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTheme(): [Theme, () => void] {
|
||||||
|
const [theme, setTheme] = usePersistentState<Theme>('ui.theme', getInitialTheme());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.documentElement.setAttribute('data-theme', theme);
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
|
const toggleTheme = () => {
|
||||||
|
setTheme((prev) => (prev === 'dark' ? 'light' : 'dark'));
|
||||||
|
};
|
||||||
|
|
||||||
|
return [theme, toggleTheme];
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user