Created detailed markdown plans for all items in todo.md: 1. 01-playwright-scaffolding.md - Base Playwright infrastructure 2. 02-magnitude-tests-comprehensive.md - Complete test coverage 3. 03-stream-ai-to-deepgram-tts.md - TTS latency optimization 4. 04-fix-galaxy-node-clicking.md - Galaxy navigation bugs 5. 05-dark-light-mode-theme.md - Dark/light mode with dynamic favicons 6. 06-fix-double-border-desktop.md - UI polish 7. 07-delete-backup-files.md - Code cleanup 8. 08-ai-transition-to-edit.md - Intelligent node creation flow 9. 09-umap-minimum-nodes-analysis.md - Technical analysis Each plan includes: - Detailed problem analysis - Proposed solutions with code examples - Manual Playwright MCP testing strategy - Magnitude test specifications - Implementation steps - Success criteria Ready to implement in sequence. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
464 lines
12 KiB
Markdown
464 lines
12 KiB
Markdown
# Plan: Add Dark/Light Mode with Dynamic Favicons
|
|
|
|
**Priority:** MEDIUM
|
|
**Dependencies:** None
|
|
**Affects:** User experience, accessibility, branding
|
|
|
|
## Overview
|
|
|
|
Add full dark mode / light mode support throughout the app, including:
|
|
- Dynamic theme switching
|
|
- Persistent user preference
|
|
- System preference detection
|
|
- Mode-specific favicons
|
|
- Smooth transitions
|
|
|
|
## Current State
|
|
|
|
- App uses a minimal grayscale theme
|
|
- No dark mode support
|
|
- Single favicon for all modes
|
|
- No theme toggle UI
|
|
|
|
## Proposed Implementation
|
|
|
|
### 1. Mantine Color Scheme Support
|
|
|
|
Mantine has built-in dark mode support. We'll leverage `@mantine/core` ColorSchemeScript and hooks.
|
|
|
|
#### Update `app/layout.tsx`
|
|
```typescript
|
|
import {
|
|
ColorSchemeScript,
|
|
MantineProvider,
|
|
} from '@mantine/core';
|
|
|
|
export default function RootLayout({
|
|
children,
|
|
}: {
|
|
children: React.ReactNode;
|
|
}) {
|
|
return (
|
|
<html lang="en">
|
|
<head>
|
|
<ColorSchemeScript defaultColorScheme="auto" />
|
|
{/* Dynamic favicon based on theme */}
|
|
<link
|
|
rel="icon"
|
|
href="/favicon-light.svg"
|
|
media="(prefers-color-scheme: light)"
|
|
/>
|
|
<link
|
|
rel="icon"
|
|
href="/favicon-dark.svg"
|
|
media="(prefers-color-scheme: dark)"
|
|
/>
|
|
</head>
|
|
<body>
|
|
<MantineProvider defaultColorScheme="auto">
|
|
{children}
|
|
</MantineProvider>
|
|
</body>
|
|
</html>
|
|
);
|
|
}
|
|
```
|
|
|
|
### 2. Update Theme Configuration
|
|
|
|
#### `app/theme.ts`
|
|
```typescript
|
|
import { createTheme, MantineColorsTuple } from '@mantine/core';
|
|
|
|
const gray: MantineColorsTuple = [
|
|
'#f8f9fa',
|
|
'#f1f3f5',
|
|
'#e9ecef',
|
|
'#dee2e6',
|
|
'#ced4da',
|
|
'#adb5bd',
|
|
'#868e96',
|
|
'#495057',
|
|
'#343a40',
|
|
'#212529',
|
|
];
|
|
|
|
export const theme = createTheme({
|
|
fontFamily: 'Inter, system-ui, sans-serif',
|
|
primaryColor: 'gray',
|
|
colors: {
|
|
gray,
|
|
},
|
|
|
|
// Dark mode specific colors
|
|
black: '#0a0a0a',
|
|
white: '#ffffff',
|
|
|
|
// Component-specific dark mode overrides
|
|
components: {
|
|
AppShell: {
|
|
styles: (theme) => ({
|
|
main: {
|
|
backgroundColor: theme.colorScheme === 'dark'
|
|
? theme.colors.dark[8]
|
|
: theme.white,
|
|
},
|
|
}),
|
|
},
|
|
|
|
Paper: {
|
|
styles: (theme) => ({
|
|
root: {
|
|
backgroundColor: theme.colorScheme === 'dark'
|
|
? theme.colors.dark[7]
|
|
: theme.white,
|
|
borderColor: theme.colorScheme === 'dark'
|
|
? theme.colors.dark[5]
|
|
: theme.colors.gray[3],
|
|
},
|
|
}),
|
|
},
|
|
|
|
Modal: {
|
|
styles: (theme) => ({
|
|
content: {
|
|
backgroundColor: theme.colorScheme === 'dark'
|
|
? theme.colors.dark[7]
|
|
: theme.white,
|
|
},
|
|
header: {
|
|
backgroundColor: theme.colorScheme === 'dark'
|
|
? theme.colors.dark[6]
|
|
: theme.colors.gray[0],
|
|
},
|
|
}),
|
|
},
|
|
},
|
|
});
|
|
```
|
|
|
|
### 3. Theme Toggle Component
|
|
|
|
#### `components/ThemeToggle.tsx`
|
|
```typescript
|
|
'use client';
|
|
|
|
import { ActionIcon, useMantineColorScheme, useComputedColorScheme } from '@mantine/core';
|
|
import { IconSun, IconMoon } from '@tabler/icons-react';
|
|
|
|
export function ThemeToggle() {
|
|
const { setColorScheme } = useMantineColorScheme();
|
|
const computedColorScheme = useComputedColorScheme('light');
|
|
|
|
return (
|
|
<ActionIcon
|
|
onClick={() => setColorScheme(computedColorScheme === 'dark' ? 'light' : 'dark')}
|
|
variant="subtle"
|
|
size="lg"
|
|
aria-label="Toggle color scheme"
|
|
>
|
|
{computedColorScheme === 'dark' ? (
|
|
<IconSun size={20} />
|
|
) : (
|
|
<IconMoon size={20} />
|
|
)}
|
|
</ActionIcon>
|
|
);
|
|
}
|
|
```
|
|
|
|
### 4. Add Toggle to Desktop Navigation
|
|
|
|
#### `components/DesktopNav.tsx`
|
|
```typescript
|
|
import { ThemeToggle } from './ThemeToggle';
|
|
|
|
export function DesktopNav() {
|
|
return (
|
|
<AppShell.Navbar p="md">
|
|
<Stack justify="space-between" h="100%">
|
|
<Stack>
|
|
{/* Navigation items */}
|
|
</Stack>
|
|
|
|
<Stack>
|
|
<ThemeToggle />
|
|
{/* Profile menu */}
|
|
</Stack>
|
|
</Stack>
|
|
</AppShell.Navbar>
|
|
);
|
|
}
|
|
```
|
|
|
|
### 5. Create Mode-Specific Favicons
|
|
|
|
#### Design Requirements
|
|
- **Light mode favicon:** Dark icon on transparent background
|
|
- **Dark mode favicon:** Light icon on transparent background
|
|
- Both should be SVG for scalability
|
|
- Simple, recognizable icon representing "thoughts" or "ponderants"
|
|
|
|
#### `public/favicon-light.svg`
|
|
```svg
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
|
<!-- Dark icon for light mode -->
|
|
<circle cx="50" cy="50" r="40" fill="#212529" />
|
|
<circle cx="35" cy="45" r="5" fill="#ffffff" />
|
|
<circle cx="65" cy="45" r="5" fill="#ffffff" />
|
|
<path d="M 30 65 Q 50 75 70 65" stroke="#ffffff" stroke-width="3" fill="none" />
|
|
</svg>
|
|
```
|
|
|
|
#### `public/favicon-dark.svg`
|
|
```svg
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
|
<!-- Light icon for dark mode -->
|
|
<circle cx="50" cy="50" r="40" fill="#f8f9fa" />
|
|
<circle cx="35" cy="45" r="5" fill="#212529" />
|
|
<circle cx="65" cy="45" r="5" fill="#212529" />
|
|
<path d="M 30 65 Q 50 75 70 65" stroke="#212529" stroke-width="3" fill="none" />
|
|
</svg>
|
|
```
|
|
|
|
### 6. Update Galaxy Visualization for Dark Mode
|
|
|
|
The 3D galaxy needs to look good in both modes:
|
|
|
|
#### `components/galaxy/ThoughtGalaxy.tsx`
|
|
```typescript
|
|
'use client';
|
|
|
|
import { useComputedColorScheme } from '@mantine/core';
|
|
|
|
export function ThoughtGalaxy() {
|
|
const colorScheme = useComputedColorScheme('light');
|
|
const isDark = colorScheme === 'dark';
|
|
|
|
return (
|
|
<Canvas
|
|
style={{
|
|
background: isDark
|
|
? 'linear-gradient(to bottom, #0a0a0a, #1a1a1a)'
|
|
: 'linear-gradient(to bottom, #f8f9fa, #e9ecef)',
|
|
}}
|
|
>
|
|
<ambientLight intensity={isDark ? 0.3 : 0.5} />
|
|
<pointLight position={[10, 10, 10]} intensity={isDark ? 0.8 : 1} />
|
|
|
|
{/* Node spheres - adjust color based on theme */}
|
|
<NodeSphere color={isDark ? '#f8f9fa' : '#212529'} />
|
|
|
|
{/* Link lines */}
|
|
<LinkLine color={isDark ? 'rgba(248, 249, 250, 0.3)' : 'rgba(33, 37, 41, 0.3)'} />
|
|
</Canvas>
|
|
);
|
|
}
|
|
```
|
|
|
|
### 7. Persist User Preference
|
|
|
|
Mantine automatically stores the preference in localStorage with the key `mantine-color-scheme-value`.
|
|
|
|
For server-side rendering, we can add a cookie fallback:
|
|
|
|
#### `middleware.ts`
|
|
```typescript
|
|
import { NextResponse } from 'next/server';
|
|
import type { NextRequest } from 'next/server';
|
|
|
|
export function middleware(request: NextRequest) {
|
|
const response = NextResponse.next();
|
|
|
|
// Set color scheme cookie if not present
|
|
if (!request.cookies.has('mantine-color-scheme')) {
|
|
response.cookies.set('mantine-color-scheme', 'auto');
|
|
}
|
|
|
|
return response;
|
|
}
|
|
```
|
|
|
|
## Testing Strategy
|
|
|
|
### Manual Playwright MCP Testing
|
|
|
|
#### Test 1: Theme Toggle
|
|
```typescript
|
|
test('User can toggle between light and dark mode', async ({ page }) => {
|
|
await page.goto('/');
|
|
|
|
// Check initial theme
|
|
const initialTheme = await page.evaluate(() => {
|
|
return document.documentElement.getAttribute('data-mantine-color-scheme');
|
|
});
|
|
|
|
// Click theme toggle
|
|
await page.click('[aria-label="Toggle color scheme"]');
|
|
|
|
// Check theme changed
|
|
const newTheme = await page.evaluate(() => {
|
|
return document.documentElement.getAttribute('data-mantine-color-scheme');
|
|
});
|
|
|
|
expect(newTheme).not.toBe(initialTheme);
|
|
});
|
|
```
|
|
|
|
#### Test 2: Theme Persistence
|
|
```typescript
|
|
test('Theme preference persists across page refreshes', async ({ page }) => {
|
|
await page.goto('/');
|
|
|
|
// Set to dark mode
|
|
await page.click('[aria-label="Toggle color scheme"]');
|
|
await page.waitForTimeout(100);
|
|
|
|
const darkTheme = await page.evaluate(() => {
|
|
return document.documentElement.getAttribute('data-mantine-color-scheme');
|
|
});
|
|
|
|
// Refresh page
|
|
await page.reload();
|
|
|
|
// Check theme persisted
|
|
const persistedTheme = await page.evaluate(() => {
|
|
return document.documentElement.getAttribute('data-mantine-color-scheme');
|
|
});
|
|
|
|
expect(persistedTheme).toBe(darkTheme);
|
|
});
|
|
```
|
|
|
|
#### Test 3: Favicon Changes
|
|
```typescript
|
|
test('Favicon changes with theme', async ({ page }) => {
|
|
await page.goto('/');
|
|
|
|
// Get favicon in light mode
|
|
const lightFavicon = await page.evaluate(() => {
|
|
const link = document.querySelector('link[rel="icon"]');
|
|
return link?.getAttribute('href');
|
|
});
|
|
|
|
// Switch to dark mode
|
|
await page.click('[aria-label="Toggle color scheme"]');
|
|
await page.waitForTimeout(100);
|
|
|
|
// Get favicon in dark mode
|
|
const darkFavicon = await page.evaluate(() => {
|
|
const link = document.querySelector('link[rel="icon"]');
|
|
return link?.getAttribute('href');
|
|
});
|
|
|
|
expect(lightFavicon).toContain('light');
|
|
expect(darkFavicon).toContain('dark');
|
|
});
|
|
```
|
|
|
|
### Magnitude Tests
|
|
|
|
```typescript
|
|
import { test } from 'magnitude-test';
|
|
|
|
test('User can switch between light and dark mode', async (agent) => {
|
|
await agent.open('http://localhost:3000');
|
|
await agent.check('Page loads in default theme');
|
|
|
|
await agent.act('Click theme toggle button');
|
|
await agent.check('Theme switches to opposite mode');
|
|
await agent.check('All UI elements are visible in new theme');
|
|
});
|
|
|
|
test('Dark mode looks good throughout app', async (agent) => {
|
|
await agent.open('http://localhost:3000');
|
|
await agent.act('Switch to dark mode');
|
|
|
|
await agent.act('Navigate to /chat');
|
|
await agent.check('Chat interface is readable in dark mode');
|
|
|
|
await agent.act('Navigate to /galaxy');
|
|
await agent.check('Galaxy visualization looks good in dark mode');
|
|
|
|
await agent.act('Navigate to /edit');
|
|
await agent.check('Editor is readable in dark mode');
|
|
});
|
|
|
|
test('Theme preference persists', async (agent) => {
|
|
await agent.open('http://localhost:3000');
|
|
await agent.act('Switch to dark mode');
|
|
await agent.check('App is in dark mode');
|
|
|
|
await agent.act('Refresh page');
|
|
await agent.check('App remains in dark mode');
|
|
});
|
|
```
|
|
|
|
## Implementation Steps
|
|
|
|
1. **Update Mantine provider and layout**
|
|
- Add ColorSchemeScript to head
|
|
- Add dynamic favicon links
|
|
- Update MantineProvider with defaultColorScheme
|
|
|
|
2. **Update theme configuration**
|
|
- Add dark mode color overrides
|
|
- Update component styles for both modes
|
|
|
|
3. **Create ThemeToggle component**
|
|
- Add sun/moon icon toggle
|
|
- Wire up to Mantine color scheme hooks
|
|
|
|
4. **Add toggle to navigation**
|
|
- Add to DesktopNav
|
|
- Add to mobile nav if applicable
|
|
|
|
5. **Create favicons**
|
|
- Design light mode favicon (dark icon)
|
|
- Design dark mode favicon (light icon)
|
|
- Export as SVG
|
|
- Add to public folder
|
|
|
|
6. **Update galaxy visualization**
|
|
- Add theme-aware colors
|
|
- Test in both modes
|
|
|
|
7. **Test with Playwright MCP**
|
|
- Test toggle functionality
|
|
- Test persistence
|
|
- Test favicon changes
|
|
- Test all pages in both modes
|
|
|
|
8. **Add Magnitude tests**
|
|
- Write comprehensive theme tests
|
|
- Ensure all pass
|
|
|
|
9. **Commit and push (NO DEPLOY)**
|
|
|
|
## Success Criteria
|
|
|
|
- ✅ User can toggle between light and dark mode
|
|
- ✅ Theme preference persists across sessions
|
|
- ✅ Favicon changes based on active theme
|
|
- ✅ All UI elements are readable in both modes
|
|
- ✅ Galaxy visualization looks good in both modes
|
|
- ✅ System preference is detected and respected
|
|
- ✅ Smooth transitions between modes
|
|
- ✅ All Playwright MCP tests pass
|
|
- ✅ All Magnitude tests pass
|
|
|
|
## Files to Create
|
|
|
|
1. `components/ThemeToggle.tsx` - Toggle component
|
|
2. `public/favicon-light.svg` - Light mode favicon
|
|
3. `public/favicon-dark.svg` - Dark mode favicon
|
|
4. `tests/playwright/theme.spec.ts` - Manual tests
|
|
5. `tests/magnitude/theme.mag.ts` - Magnitude tests
|
|
|
|
## Files to Update
|
|
|
|
1. `app/layout.tsx` - Add ColorSchemeScript and dynamic favicons
|
|
2. `app/theme.ts` - Add dark mode color overrides
|
|
3. `components/DesktopNav.tsx` - Add ThemeToggle
|
|
4. `components/galaxy/ThoughtGalaxy.tsx` - Add theme-aware colors
|