feat: Add dark/light mode theme switching with dynamic favicons

Implemented comprehensive dark/light mode support throughout the app:

- Added ColorSchemeScript to layout for auto-detection of system preference
- Updated MantineProvider to use 'auto' color scheme (respects system)
- Updated theme.ts with dynamic Paper component styles based on color scheme
- Created ThemeToggle component with sun/moon icons
- Added toggle to desktop sidebar navigation
- Created theme-specific favicons (favicon-light.svg, favicon-dark.svg)
- Made ThoughtGalaxy 3D visualization theme-aware:
  - Dynamic node colors based on theme
  - Theme-aware lighting intensity
  - Theme-aware link colors
  - Theme-aware text labels
- Added comprehensive Playwright tests for theme functionality
- Theme preference persists via localStorage

Tested manually with Playwright MCP:
-  Theme toggle switches between light and dark modes
-  Theme persists across page reloads
-  Both modes render correctly with appropriate colors
-  Icons change based on current theme (sun/moon)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-09 22:04:26 +00:00
parent 482ec9fff8
commit 9bf16fefaf
8 changed files with 222 additions and 16 deletions

View File

@@ -7,7 +7,7 @@ import {
Text,
} from '@react-three/drei';
import { Suspense, useEffect, useRef, useState } from 'react';
import { Stack, Text as MantineText, Paper, Title, Box, CloseButton, Group, Anchor } from '@mantine/core';
import { Stack, Text as MantineText, Paper, Title, Box, CloseButton, Group, Anchor, useComputedColorScheme } from '@mantine/core';
import { useRouter, usePathname, useSearchParams } from 'next/navigation';
import * as THREE from 'three';
@@ -30,17 +30,24 @@ interface LinkData {
function Node({
node,
isFocused,
onNodeClick
onNodeClick,
isDark
}: {
node: NodeData;
isFocused: boolean;
onNodeClick: (node: NodeData) => void;
isDark: boolean;
}) {
const [hovered, setHovered] = useState(false);
const isExpanded = isFocused || hovered;
const scale = isFocused ? 2.5 : 1;
// Theme-aware colors
const nodeColor = isDark ? '#e9ecef' : '#495057';
const focusColor = '#4dabf7';
const hoverColor = '#90c0ff';
return (
<mesh
position={node.coords_3d}
@@ -57,8 +64,8 @@ function Node({
>
<sphereGeometry args={[0.1, 32, 32]} />
<meshStandardMaterial
color={isFocused ? '#4dabf7' : (hovered ? '#90c0ff' : '#e9ecef')}
emissive={isFocused ? '#4dabf7' : (hovered ? '#90c0ff' : '#e9ecef')}
color={isFocused ? focusColor : (hovered ? hoverColor : nodeColor)}
emissive={isFocused ? focusColor : (hovered ? hoverColor : nodeColor)}
emissiveIntensity={isFocused ? 0.8 : (hovered ? 0.5 : 0.1)}
/>
{/* Show title on hover or focus */}
@@ -66,7 +73,7 @@ function Node({
<Text
position={[0, 0.3 / scale, 0]}
fontSize={0.1 / scale}
color="white"
color={isDark ? 'white' : 'black'}
anchorX="center"
anchorY="middle"
>
@@ -82,6 +89,9 @@ export function ThoughtGalaxy() {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const colorScheme = useComputedColorScheme('light');
const isDark = colorScheme === 'dark';
const [nodes, setNodes] = useState<NodeData[]>([]);
const [links, setLinks] = useState<LinkData[]>([]);
const [selectedNode, setSelectedNode] = useState<NodeData | null>(null);
@@ -353,8 +363,9 @@ export function ThoughtGalaxy() {
}
}}
>
<ambientLight intensity={0.5} />
<pointLight position={[10, 10, 10]} intensity={1} />
{/* Theme-aware lighting */}
<ambientLight intensity={isDark ? 0.3 : 0.5} />
<pointLight position={[10, 10, 10]} intensity={isDark ? 0.8 : 1} />
<CameraControls ref={cameraControlsRef} />
<Suspense fallback={null}>
@@ -366,15 +377,16 @@ export function ThoughtGalaxy() {
node={node}
isFocused={selectedNodeId === node.id || selectedNode?.id === node.id}
onNodeClick={handleNodeClick}
isDark={isDark}
/>
))}
{/* Render all links */}
{/* Render all links - theme-aware colors */}
{linkLines.map((line, i) => (
<Line
key={i}
points={[line.start, line.end]}
color="#495057" // gray
color={isDark ? '#495057' : '#adb5bd'}
lineWidth={1}
/>
))}