Skip to content

Commit 652ae71

Browse files
committed
feat: add light/dark mode toggle with light as default
1 parent 68e3ee4 commit 652ae71

File tree

4 files changed

+153
-56
lines changed

4 files changed

+153
-56
lines changed

src/app/globals.css

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,28 @@
44
@tailwind components;
55
@tailwind utilities;
66

7-
/* Professional Dark Theme */
7+
/* Light Theme (Default) */
88
:root {
9+
--bg-primary: #ffffff;
10+
--bg-secondary: #f8fafc;
11+
--bg-tertiary: #f1f5f9;
12+
--bg-elevated: #e2e8f0;
13+
--text-primary: #1e293b;
14+
--text-secondary: #475569;
15+
--text-muted: #94a3b8;
16+
--accent-primary: #3b82f6;
17+
--accent-secondary: #10b981;
18+
--accent-warning: #f59e0b;
19+
--accent-danger: #ef4444;
20+
--border-color: rgba(0, 0, 0, 0.08);
21+
--border-hover: rgba(0, 0, 0, 0.15);
22+
--glass-bg: rgba(255, 255, 255, 0.9);
23+
--shadow-color: rgba(0, 0, 0, 0.1);
24+
--code-bg: #f8fafc;
25+
}
26+
27+
/* Dark Theme */
28+
[data-theme="dark"] {
929
--bg-primary: #0f1419;
1030
--bg-secondary: #1a1f26;
1131
--bg-tertiary: #242b33;
@@ -19,6 +39,9 @@
1939
--accent-danger: #ef4444;
2040
--border-color: rgba(255, 255, 255, 0.08);
2141
--border-hover: rgba(255, 255, 255, 0.15);
42+
--glass-bg: rgba(26, 31, 38, 0.95);
43+
--shadow-color: rgba(0, 0, 0, 0.3);
44+
--code-bg: #0f1419;
2245
}
2346

2447
* {
@@ -65,17 +88,19 @@ body {
6588

6689
/* Glass Morphism - Simplified */
6790
.glass {
68-
background: rgba(26, 31, 38, 0.95);
91+
background: var(--glass-bg);
6992
backdrop-filter: blur(12px);
7093
-webkit-backdrop-filter: blur(12px);
7194
border: 1px solid var(--border-color);
95+
box-shadow: 0 4px 6px -1px var(--shadow-color);
7296
}
7397

7498
.glass-strong {
75-
background: rgba(26, 31, 38, 0.98);
99+
background: var(--glass-bg);
76100
backdrop-filter: blur(20px);
77101
-webkit-backdrop-filter: blur(20px);
78102
border: 1px solid var(--border-color);
103+
box-shadow: 0 4px 6px -1px var(--shadow-color);
79104
}
80105

81106
/* Professional Button Styles */
@@ -107,18 +132,28 @@ body {
107132
font-family: 'JetBrains Mono', monospace;
108133
font-size: 0.8125rem;
109134
line-height: 1.6;
110-
background: var(--bg-primary);
135+
background: var(--code-bg);
111136
border: 1px solid var(--border-color);
112137
border-radius: 8px;
113138
}
114139

115-
.code-block .keyword { color: #c792ea; }
116-
.code-block .function { color: #82aaff; }
117-
.code-block .string { color: #c3e88d; }
118-
.code-block .number { color: #f78c6c; }
119-
.code-block .comment { color: #546e7a; }
120-
.code-block .class { color: #ffcb6b; }
121-
.code-block .operator { color: #89ddff; }
140+
/* Light mode code syntax */
141+
.code-block .keyword { color: #7c3aed; }
142+
.code-block .function { color: #2563eb; }
143+
.code-block .string { color: #059669; }
144+
.code-block .number { color: #dc2626; }
145+
.code-block .comment { color: #6b7280; }
146+
.code-block .class { color: #d97706; }
147+
.code-block .operator { color: #0891b2; }
148+
149+
/* Dark mode code syntax */
150+
[data-theme="dark"] .code-block .keyword { color: #c792ea; }
151+
[data-theme="dark"] .code-block .function { color: #82aaff; }
152+
[data-theme="dark"] .code-block .string { color: #c3e88d; }
153+
[data-theme="dark"] .code-block .number { color: #f78c6c; }
154+
[data-theme="dark"] .code-block .comment { color: #546e7a; }
155+
[data-theme="dark"] .code-block .class { color: #ffcb6b; }
156+
[data-theme="dark"] .code-block .operator { color: #89ddff; }
122157

123158
/* KaTeX */
124159
.katex {

src/components/3d/NetworkVisualization.tsx

Lines changed: 64 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ function Connection({
111111
}
112112

113113
// Layer visualization
114-
function LayerVisualization({
114+
function LayerVisualization({
115115
layerIndex,
116116
neurons,
117117
type,
@@ -131,59 +131,59 @@ function LayerVisualization({
131131
const displayNeurons = Math.min(neurons, 8);
132132
const spacing = 0.7;
133133
const startY = ((displayNeurons - 1) * spacing) / 2;
134-
134+
135135
const positions: [number, number, number][] = useMemo(() => {
136136
return Array.from({ length: displayNeurons }, (_, i) => [
137137
xPosition,
138138
startY - i * spacing,
139139
0
140140
] as [number, number, number]);
141141
}, [displayNeurons, xPosition, startY, spacing]);
142-
142+
143143
return (
144144
<group>
145145
{positions.map((pos, i) => (
146-
<Neuron
147-
key={i}
148-
position={pos}
146+
<Neuron
147+
key={i}
148+
position={pos}
149149
color={color}
150150
size={0.25}
151151
pulseSpeed={2 + layerIndex * 0.5}
152152
/>
153153
))}
154-
154+
155155
{/* Layer label using Html */}
156156
<Html
157157
position={[xPosition, startY + 1, 0]}
158158
center
159-
style={{
159+
style={{
160160
pointerEvents: 'none',
161161
userSelect: 'none'
162162
}}
163163
>
164164
<div className="text-center whitespace-nowrap">
165-
<div className={`text-sm font-bold ${isSelected ? 'text-cyan-400' : 'text-white'}`}>
165+
<div className={`text-sm font-bold ${isSelected ? 'text-cyan-400' : 'text-[var(--text-primary)]'}`}>
166166
{name}
167167
</div>
168-
<div className="text-xs text-gray-400">
169-
{type === 'dense' ? `${neurons} units` :
168+
<div className="text-xs text-[var(--text-muted)]">
169+
{type === 'dense' ? `${neurons} units` :
170170
type === 'conv2d' ? `${neurons} filters` :
171171
type === 'input' ? 'Input' : ''}
172172
</div>
173173
</div>
174174
</Html>
175-
175+
176176
{/* Ellipsis for more neurons */}
177177
{neurons > 8 && (
178178
<Html
179179
position={[xPosition, -startY - 0.8, 0]}
180180
center
181181
style={{ pointerEvents: 'none' }}
182182
>
183-
<div className="text-gray-400 text-lg"></div>
183+
<div className="text-[var(--text-muted)] text-lg"></div>
184184
</Html>
185185
)}
186-
186+
187187
{/* Selection ring */}
188188
{isSelected && (
189189
<mesh rotation={[Math.PI / 2, 0, 0]} position={[xPosition, 0, 0]}>
@@ -299,64 +299,89 @@ function NetworkScene() {
299299
);
300300
}
301301

302-
// Grid floor
302+
// Grid floor - adapts to theme
303303
function GridFloor() {
304+
const theme = useNetworkStore(state => state.ui.theme);
305+
const gridColor = theme === 'dark' ? '#0a0a0f' : '#e2e8f0';
306+
const opacity = theme === 'dark' ? 0.3 : 0.5;
307+
304308
return (
305309
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -3, 0]} receiveShadow>
306310
<planeGeometry args={[50, 50, 50, 50]} />
307-
<meshStandardMaterial
308-
color="#0a0a0f"
311+
<meshStandardMaterial
312+
color={gridColor}
309313
wireframe
310314
transparent
311-
opacity={0.3}
315+
opacity={opacity}
312316
/>
313317
</mesh>
314318
);
315319
}
316320

321+
// Background stars - only shown in dark mode
322+
function BackgroundStars() {
323+
const theme = useNetworkStore(state => state.ui.theme);
324+
325+
// Use useMemo to generate consistent star positions (must be called before any early returns)
326+
const stars = useMemo(() => {
327+
return Array.from({ length: 100 }).map((_, i) => ({
328+
position: [
329+
(Math.sin(i * 1.234) * 0.5) * 40,
330+
(Math.cos(i * 2.345) * 0.5) * 40,
331+
-20 - (Math.sin(i * 3.456) * 0.5 + 0.5) * 20
332+
] as [number, number, number],
333+
size: 0.02 + (Math.sin(i * 4.567) * 0.5 + 0.5) * 0.03,
334+
opacity: 0.5 + (Math.cos(i * 5.678) * 0.5 + 0.5) * 0.5
335+
}));
336+
}, []);
337+
338+
if (theme !== 'dark') return null;
339+
340+
return (
341+
<>
342+
{stars.map((star, i) => (
343+
<mesh key={i} position={star.position}>
344+
<sphereGeometry args={[star.size, 8, 8]} />
345+
<meshBasicMaterial color="#ffffff" transparent opacity={star.opacity} />
346+
</mesh>
347+
))}
348+
</>
349+
);
350+
}
351+
317352
// Main component
318353
export default function NetworkVisualization() {
354+
const theme = useNetworkStore(state => state.ui.theme);
355+
319356
return (
320357
<div className="w-full h-full">
321358
<Canvas
322359
camera={{ position: [0, 3, 12], fov: 50 }}
323360
gl={{ antialias: true, alpha: true }}
324361
style={{ background: 'transparent' }}
325362
>
326-
{/* Lighting */}
327-
<ambientLight intensity={0.5} />
328-
<pointLight position={[10, 10, 10]} intensity={1} color="#ffffff" />
363+
{/* Lighting - adjusted for theme */}
364+
<ambientLight intensity={theme === 'dark' ? 0.5 : 0.8} />
365+
<pointLight position={[10, 10, 10]} intensity={theme === 'dark' ? 1 : 0.8} color="#ffffff" />
329366
<pointLight position={[-10, 5, -10]} intensity={0.5} color="#a855f7" />
330367
<pointLight position={[0, -5, 5]} intensity={0.3} color="#00d4ff" />
331-
368+
332369
{/* Controls */}
333-
<OrbitControls
370+
<OrbitControls
334371
enablePan={true}
335372
enableZoom={true}
336373
enableRotate={true}
337374
minDistance={5}
338375
maxDistance={30}
339376
target={[0, 0, 0]}
340377
/>
341-
378+
342379
{/* Scene */}
343380
<NetworkScene />
344381
<GridFloor />
345-
346-
{/* Background stars effect */}
347-
{Array.from({ length: 100 }).map((_, i) => (
348-
<mesh
349-
key={i}
350-
position={[
351-
(Math.random() - 0.5) * 40,
352-
(Math.random() - 0.5) * 40,
353-
-20 - Math.random() * 20
354-
]}
355-
>
356-
<sphereGeometry args={[0.02 + Math.random() * 0.03, 8, 8]} />
357-
<meshBasicMaterial color="#ffffff" transparent opacity={0.5 + Math.random() * 0.5} />
358-
</mesh>
359-
))}
382+
383+
{/* Background stars effect - dark mode only */}
384+
<BackgroundStars />
360385
</Canvas>
361386
</div>
362387
);

src/components/NeuralNetworkVisualizer.tsx

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,14 @@ function HeaderBar({ isMobile }: { isMobile: boolean }) {
5252
training,
5353
toggleLeftPanel,
5454
toggleRightPanel,
55-
getTotalParameters
55+
getTotalParameters,
56+
setTheme
5657
} = useNetworkStore();
5758

59+
const toggleTheme = () => {
60+
setTheme(ui.theme === 'light' ? 'dark' : 'light');
61+
};
62+
5863
return (
5964
<header className="fixed top-0 left-0 right-0 h-14 glass z-30 flex items-center justify-between px-2 sm:px-4">
6065
{/* Left: Toggle sidebar */}
@@ -115,12 +120,30 @@ function HeaderBar({ isMobile }: { isMobile: boolean }) {
115120
)}
116121
</div>
117122

118-
{/* Right: Toggle right panel + Credit */}
119-
<div className="flex items-center gap-3">
123+
{/* Right: Theme toggle + GitHub + Toggle right panel */}
124+
<div className="flex items-center gap-2 sm:gap-3">
120125
<span className="hidden lg:block text-xs text-[var(--text-muted)]">
121126
ML Engineer Portfolio Project
122127
</span>
123-
128+
129+
{/* Theme Toggle */}
130+
<button
131+
onClick={toggleTheme}
132+
className="p-2 rounded-lg hover:bg-[var(--bg-tertiary)] transition-colors text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
133+
aria-label={`Switch to ${ui.theme === 'light' ? 'dark' : 'light'} mode`}
134+
title={`Switch to ${ui.theme === 'light' ? 'dark' : 'light'} mode`}
135+
>
136+
{ui.theme === 'light' ? (
137+
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
138+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
139+
</svg>
140+
) : (
141+
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
142+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
143+
</svg>
144+
)}
145+
</button>
146+
124147
<a
125148
href="https://github.com/nolancacheux"
126149
target="_blank"
@@ -218,13 +241,27 @@ function useKeyboardShortcuts() {
218241
}, [handleKeyDown]);
219242
}
220243

244+
// Hook to apply theme to document
245+
function useTheme() {
246+
const theme = useNetworkStore(state => state.ui.theme);
247+
248+
useEffect(() => {
249+
if (theme === 'dark') {
250+
document.documentElement.setAttribute('data-theme', 'dark');
251+
} else {
252+
document.documentElement.removeAttribute('data-theme');
253+
}
254+
}, [theme]);
255+
}
256+
221257
// Main component
222258
export default function NeuralNetworkVisualizer() {
223259
const { ui, toggleLeftPanel, toggleRightPanel } = useNetworkStore();
224260
const isMobile = useIsMobile();
225261

226-
// Register keyboard shortcuts
262+
// Register keyboard shortcuts and theme
227263
useKeyboardShortcuts();
264+
useTheme();
228265

229266
// Calculate main content margins based on panel states (desktop only)
230267
const mainStyles = isMobile ? {} : {

src/store/networkStore.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ const initialState = {
268268
rightPanelTab: 'parameters' as const,
269269
isTourActive: false,
270270
currentTourStep: 0,
271-
theme: 'dark' as const
271+
theme: 'light' as const
272272
}
273273
};
274274

0 commit comments

Comments
 (0)