blob: 82bd65b5f41a9a254f2b634a9ae71bc9d2331c26 [file] [log] [blame] [view]
# Dark Mode Implementation Plan
## Overview
This document outlines the plan to implement dark mode for Sketch's web UI using Tailwind CSS's built-in dark mode capabilities.
## Current State Analysis
Sketch's web UI currently uses:
- Tailwind CSS with basic configuration
- Lit web components extending `SketchTailwindElement`
- Components avoid Shadow DOM (`createRenderRoot() { return this; }`)
- Standard Tailwind classes like `bg-white`, `text-gray-600`, `border-gray-200`
- Global CSS approach, making dark mode implementation straightforward
## Implementation Strategy: Class-Based Dark Mode
### Phase 1: Foundation
#### 1. Update Tailwind Configuration
```javascript
// tailwind.config.js
export default {
content: ["./src/**/*.{js,ts,jsx,tsx,html}"],
darkMode: "class", // Enable class-based dark mode
plugins: ["@tailwindcss/container-queries"],
theme: {
extend: {
// Custom colors for better dark mode support
colors: {
// Define semantic color tokens
surface: {
DEFAULT: "#ffffff",
dark: "#1f2937",
},
"surface-secondary": {
DEFAULT: "#f9fafb",
dark: "#374151",
},
},
animation: {
"fade-in": "fadeIn 0.3s ease-in-out",
},
keyframes: {
fadeIn: {
"0%": {
opacity: "0",
transform: "translateX(-50%) translateY(10px)",
},
"100%": {
opacity: "1",
transform: "translateX(-50%) translateY(0)",
},
},
},
},
},
};
```
#### 2. Create Theme Management Service
```typescript
// src/web-components/theme-service.ts
export class ThemeService {
private static instance: ThemeService;
static getInstance(): ThemeService {
if (!this.instance) {
this.instance = new ThemeService();
}
return this.instance;
}
toggleTheme(): void {
const isDark = document.documentElement.classList.contains("dark");
this.setTheme(isDark ? "light" : "dark");
}
setTheme(theme: "light" | "dark"): void {
document.documentElement.classList.toggle("dark", theme === "dark");
localStorage.setItem("theme", theme);
// Dispatch event for components that need to react
document.dispatchEvent(
new CustomEvent("theme-changed", {
detail: { theme },
}),
);
}
getTheme(): "light" | "dark" {
return document.documentElement.classList.contains("dark")
? "dark"
: "light";
}
initializeTheme(): void {
const saved = localStorage.getItem("theme");
const prefersDark = window.matchMedia(
"(prefers-color-scheme: dark)",
).matches;
const theme = saved || (prefersDark ? "dark" : "light");
this.setTheme(theme as "light" | "dark");
// Listen for system theme changes
window
.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", (e) => {
if (!localStorage.getItem("theme")) {
this.setTheme(e.matches ? "dark" : "light");
}
});
}
}
```
#### 3. Theme Toggle Component
```typescript
// src/web-components/theme-toggle.ts
import { html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { SketchTailwindElement } from "./sketch-tailwind-element.js";
import { ThemeService } from "./theme-service.js";
@customElement("theme-toggle")
export class ThemeToggle extends SketchTailwindElement {
@state() private isDark = false;
private themeService = ThemeService.getInstance();
connectedCallback() {
super.connectedCallback();
this.isDark = document.documentElement.classList.contains("dark");
// Listen for theme changes from other sources
document.addEventListener("theme-changed", this.handleThemeChange);
}
disconnectedCallback() {
super.disconnectedCallback();
document.removeEventListener("theme-changed", this.handleThemeChange);
}
private handleThemeChange = (e: CustomEvent) => {
this.isDark = e.detail.theme === "dark";
};
private toggleTheme() {
this.themeService.toggleTheme();
}
render() {
return html`
<button
@click=${this.toggleTheme}
class="p-2 rounded-md border border-gray-300 dark:border-gray-600
bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-200
hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors
focus:outline-none focus:ring-2 focus:ring-blue-500"
title="Toggle theme"
aria-label="Toggle between light and dark mode"
>
${this.isDark ? "☀️" : "🌙"}
</button>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"theme-toggle": ThemeToggle;
}
}
```
#### 4. Initialize Theme in App Shell
Add theme initialization to the main app shell component:
```typescript
// In sketch-app-shell.ts or similar
import { ThemeService } from "./theme-service.js";
connectedCallback() {
super.connectedCallback();
ThemeService.getInstance().initializeTheme();
}
```
### Phase 2: Component Updates
#### Systematic Component Audit
1. **Identify all components using color classes**
- Search for `bg-`, `text-`, `border-`, `ring-`, `divide-` classes
- Document current color usage patterns
2. **Add dark mode variants systematically**
- Start with core components (app shell, chat input, etc.)
- Update classes following the pattern:
```typescript
// Before:
class="bg-white text-gray-900 border-gray-200"
// After:
class="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 border-gray-200 dark:border-gray-700"
```
3. **Priority order for component updates:**
1. `sketch-app-shell` - Main container
2. `sketch-chat-input` - Primary interaction component
3. `sketch-container-status` - Status indicators
4. `sketch-call-status` - Call indicators
5. Demo components and other secondary components
#### Common Dark Mode Color Mappings
```scss
// Light -> Dark mappings
bg-white -> bg-gray-900
bg-gray-50 -> bg-gray-800
bg-gray-100 -> bg-gray-800
bg-gray-200 -> bg-gray-700
text-gray-900 -> text-gray-100
text-gray-800 -> text-gray-200
text-gray-700 -> text-gray-300
text-gray-600 -> text-gray-400
text-gray-500 -> text-gray-500 (neutral)
border-gray-200 -> border-gray-700
border-gray-300 -> border-gray-600
ring-gray-300 -> ring-gray-600
```
### Phase 3: Polish and Testing
#### 1. Smooth Transitions
- Add `transition-colors` to interactive elements
- Consider adding a global transition class for theme changes
#### 2. Accessibility
- Ensure sufficient contrast ratios in both modes
- Test with screen readers
- Verify focus indicators work in both themes
- Add proper ARIA labels to theme toggle
#### 3. Testing Checklist
- [ ] Theme persists across page reloads
- [ ] System preference detection works
- [ ] All components render correctly in both modes
- [ ] Interactive states (hover, focus, active) work in both themes
- [ ] No flash of unstyled content (FOUC)
- [ ] Works across different screen sizes
- [ ] Performance impact is minimal
## File Structure
```
src/web-components/
├── theme-service.ts # Theme management service
├── theme-toggle.ts # Theme toggle component
├── sketch-tailwind-element.ts # Base class (existing)
└── [other components].ts # Updated with dark mode variants
```
## Benefits of This Approach
- **Incremental**: Can be implemented component by component
- **Standard**: Uses Tailwind's built-in dark mode features
- **Performant**: Class-based approach is efficient
- **Maintainable**: Clear separation of concerns with theme service
- **Accessible**: Respects system preferences by default
- **Consistent**: Follows Sketch's existing component patterns
## Implementation Timeline
1. **Week 1**: Phase 1 - Foundation (config, service, toggle)
2. **Week 2**: Phase 2 - Core component updates
3. **Week 3**: Phase 2 - Secondary component updates
4. **Week 4**: Phase 3 - Polish, testing, and accessibility
## Notes
- Components extend `SketchTailwindElement` (not `LitElement`)
- No Shadow DOM usage allows for global Tailwind classes
- Theme service uses singleton pattern for consistency
- Event system allows components to react to theme changes
- LocalStorage preserves user preference across sessions