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

@@ -0,0 +1,126 @@
import { test, expect } from './fixtures';
test.describe('Theme Switching', () => {
test('user can toggle between light and dark mode', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
// Get initial theme
const initialTheme = await page.evaluate(() => {
return document.documentElement.getAttribute('data-mantine-color-scheme');
});
// Click theme toggle button
await page.click('[aria-label="Toggle color scheme"]');
await page.waitForTimeout(500); // Wait for theme transition
// Check theme changed
const newTheme = await page.evaluate(() => {
return document.documentElement.getAttribute('data-mantine-color-scheme');
});
expect(newTheme).not.toBe(initialTheme);
expect(['light', 'dark']).toContain(newTheme);
});
test('theme preference persists across page refreshes', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
// Set to a specific mode (light)
await page.evaluate(() => {
document.documentElement.setAttribute('data-mantine-color-scheme', 'light');
window.localStorage.setItem('mantine-color-scheme-value', 'light');
});
// Get the theme before reload
const themeBeforeReload = await page.evaluate(() => {
return window.localStorage.getItem('mantine-color-scheme-value');
});
// Refresh page
await page.reload();
await page.waitForLoadState('networkidle');
// Check theme persisted
const persistedTheme = await page.evaluate(() => {
return document.documentElement.getAttribute('data-mantine-color-scheme');
});
expect(persistedTheme).toBe(themeBeforeReload);
});
test('theme toggle icon changes based on current theme', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
// The button should be visible
const toggleButton = page.locator('[aria-label="Toggle color scheme"]');
await expect(toggleButton).toBeVisible();
// Icon should exist (either sun or moon)
const icon = toggleButton.locator('img, svg').first();
await expect(icon).toBeVisible();
});
});
test.describe('Theme Visual Appearance', () => {
test('light mode has light background', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
// Force light mode
await page.evaluate(() => {
document.documentElement.setAttribute('data-mantine-color-scheme', 'light');
window.localStorage.setItem('mantine-color-scheme-value', 'light');
});
// Reload to apply theme
await page.reload();
await page.waitForLoadState('networkidle');
// Check that we're in light mode
const colorScheme = await page.evaluate(() => {
return document.documentElement.getAttribute('data-mantine-color-scheme');
});
expect(colorScheme).toBe('light');
});
test('dark mode has dark background', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
// Force dark mode
await page.evaluate(() => {
document.documentElement.setAttribute('data-mantine-color-scheme', 'dark');
window.localStorage.setItem('mantine-color-scheme-value', 'dark');
});
// Reload to apply theme
await page.reload();
await page.waitForLoadState('networkidle');
// Check that we're in dark mode
const colorScheme = await page.evaluate(() => {
return document.documentElement.getAttribute('data-mantine-color-scheme');
});
expect(colorScheme).toBe('dark');
});
});
test.describe('Theme Auto-Detection', () => {
test('respects system color scheme preference when set to auto', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
// The app should detect and apply a theme (either light or dark)
const appliedTheme = await page.evaluate(() => {
return document.documentElement.getAttribute('data-mantine-color-scheme');
});
// Should be either light or dark, not null
expect(['light', 'dark']).toContain(appliedTheme);
});
});