blob: 82bd65b5f41a9a254f2b634a9ae71bc9d2331c26 [file] [log] [blame] [view]
bankseanae3724e2025-07-18 16:52:37 +00001# Dark Mode Implementation Plan
2
3## Overview
4
5This document outlines the plan to implement dark mode for Sketch's web UI using Tailwind CSS's built-in dark mode capabilities.
6
7## Current State Analysis
8
9Sketch's web UI currently uses:
10
11- Tailwind CSS with basic configuration
12- Lit web components extending `SketchTailwindElement`
13- Components avoid Shadow DOM (`createRenderRoot() { return this; }`)
14- Standard Tailwind classes like `bg-white`, `text-gray-600`, `border-gray-200`
15- Global CSS approach, making dark mode implementation straightforward
16
17## Implementation Strategy: Class-Based Dark Mode
18
19### Phase 1: Foundation
20
21#### 1. Update Tailwind Configuration
22
23```javascript
24// tailwind.config.js
25export default {
26 content: ["./src/**/*.{js,ts,jsx,tsx,html}"],
27 darkMode: "class", // Enable class-based dark mode
28 plugins: ["@tailwindcss/container-queries"],
29 theme: {
30 extend: {
31 // Custom colors for better dark mode support
32 colors: {
33 // Define semantic color tokens
34 surface: {
35 DEFAULT: "#ffffff",
36 dark: "#1f2937",
37 },
38 "surface-secondary": {
39 DEFAULT: "#f9fafb",
40 dark: "#374151",
41 },
42 },
43 animation: {
44 "fade-in": "fadeIn 0.3s ease-in-out",
45 },
46 keyframes: {
47 fadeIn: {
48 "0%": {
49 opacity: "0",
50 transform: "translateX(-50%) translateY(10px)",
51 },
52 "100%": {
53 opacity: "1",
54 transform: "translateX(-50%) translateY(0)",
55 },
56 },
57 },
58 },
59 },
60};
61```
62
63#### 2. Create Theme Management Service
64
65```typescript
66// src/web-components/theme-service.ts
67export class ThemeService {
68 private static instance: ThemeService;
69
70 static getInstance(): ThemeService {
71 if (!this.instance) {
72 this.instance = new ThemeService();
73 }
74 return this.instance;
75 }
76
77 toggleTheme(): void {
78 const isDark = document.documentElement.classList.contains("dark");
79 this.setTheme(isDark ? "light" : "dark");
80 }
81
82 setTheme(theme: "light" | "dark"): void {
83 document.documentElement.classList.toggle("dark", theme === "dark");
84 localStorage.setItem("theme", theme);
85
86 // Dispatch event for components that need to react
87 document.dispatchEvent(
88 new CustomEvent("theme-changed", {
89 detail: { theme },
90 }),
91 );
92 }
93
94 getTheme(): "light" | "dark" {
95 return document.documentElement.classList.contains("dark")
96 ? "dark"
97 : "light";
98 }
99
100 initializeTheme(): void {
101 const saved = localStorage.getItem("theme");
102 const prefersDark = window.matchMedia(
103 "(prefers-color-scheme: dark)",
104 ).matches;
105 const theme = saved || (prefersDark ? "dark" : "light");
106 this.setTheme(theme as "light" | "dark");
107
108 // Listen for system theme changes
109 window
110 .matchMedia("(prefers-color-scheme: dark)")
111 .addEventListener("change", (e) => {
112 if (!localStorage.getItem("theme")) {
113 this.setTheme(e.matches ? "dark" : "light");
114 }
115 });
116 }
117}
118```
119
120#### 3. Theme Toggle Component
121
122```typescript
123// src/web-components/theme-toggle.ts
124import { html } from "lit";
125import { customElement, state } from "lit/decorators.js";
126import { SketchTailwindElement } from "./sketch-tailwind-element.js";
127import { ThemeService } from "./theme-service.js";
128
129@customElement("theme-toggle")
130export class ThemeToggle extends SketchTailwindElement {
131 @state() private isDark = false;
132
133 private themeService = ThemeService.getInstance();
134
135 connectedCallback() {
136 super.connectedCallback();
137 this.isDark = document.documentElement.classList.contains("dark");
138
139 // Listen for theme changes from other sources
140 document.addEventListener("theme-changed", this.handleThemeChange);
141 }
142
143 disconnectedCallback() {
144 super.disconnectedCallback();
145 document.removeEventListener("theme-changed", this.handleThemeChange);
146 }
147
148 private handleThemeChange = (e: CustomEvent) => {
149 this.isDark = e.detail.theme === "dark";
150 };
151
152 private toggleTheme() {
153 this.themeService.toggleTheme();
154 }
155
156 render() {
157 return html`
158 <button
159 @click=${this.toggleTheme}
160 class="p-2 rounded-md border border-gray-300 dark:border-gray-600
161 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-200
162 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors
163 focus:outline-none focus:ring-2 focus:ring-blue-500"
164 title="Toggle theme"
165 aria-label="Toggle between light and dark mode"
166 >
167 ${this.isDark ? "☀️" : "🌙"}
168 </button>
169 `;
170 }
171}
172
173declare global {
174 interface HTMLElementTagNameMap {
175 "theme-toggle": ThemeToggle;
176 }
177}
178```
179
180#### 4. Initialize Theme in App Shell
181
182Add theme initialization to the main app shell component:
183
184```typescript
185// In sketch-app-shell.ts or similar
186import { ThemeService } from "./theme-service.js";
187
188connectedCallback() {
189 super.connectedCallback();
190 ThemeService.getInstance().initializeTheme();
191}
192```
193
194### Phase 2: Component Updates
195
196#### Systematic Component Audit
197
1981. **Identify all components using color classes**
199
200 - Search for `bg-`, `text-`, `border-`, `ring-`, `divide-` classes
201 - Document current color usage patterns
202
2032. **Add dark mode variants systematically**
204
205 - Start with core components (app shell, chat input, etc.)
206 - Update classes following the pattern:
207
208 ```typescript
209 // Before:
210 class="bg-white text-gray-900 border-gray-200"
211
212 // After:
213 class="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 border-gray-200 dark:border-gray-700"
214 ```
215
2163. **Priority order for component updates:**
217 1. `sketch-app-shell` - Main container
218 2. `sketch-chat-input` - Primary interaction component
219 3. `sketch-container-status` - Status indicators
220 4. `sketch-call-status` - Call indicators
221 5. Demo components and other secondary components
222
223#### Common Dark Mode Color Mappings
224
225```scss
226// Light -> Dark mappings
227bg-white -> bg-gray-900
228bg-gray-50 -> bg-gray-800
229bg-gray-100 -> bg-gray-800
230bg-gray-200 -> bg-gray-700
231
232text-gray-900 -> text-gray-100
233text-gray-800 -> text-gray-200
234text-gray-700 -> text-gray-300
235text-gray-600 -> text-gray-400
236text-gray-500 -> text-gray-500 (neutral)
237
238border-gray-200 -> border-gray-700
239border-gray-300 -> border-gray-600
240
241ring-gray-300 -> ring-gray-600
242```
243
244### Phase 3: Polish and Testing
245
246#### 1. Smooth Transitions
247
248- Add `transition-colors` to interactive elements
249- Consider adding a global transition class for theme changes
250
251#### 2. Accessibility
252
253- Ensure sufficient contrast ratios in both modes
254- Test with screen readers
255- Verify focus indicators work in both themes
256- Add proper ARIA labels to theme toggle
257
258#### 3. Testing Checklist
259
260- [ ] Theme persists across page reloads
261- [ ] System preference detection works
262- [ ] All components render correctly in both modes
263- [ ] Interactive states (hover, focus, active) work in both themes
264- [ ] No flash of unstyled content (FOUC)
265- [ ] Works across different screen sizes
266- [ ] Performance impact is minimal
267
268## File Structure
269
270```
271src/web-components/
272├── theme-service.ts # Theme management service
273├── theme-toggle.ts # Theme toggle component
274├── sketch-tailwind-element.ts # Base class (existing)
275└── [other components].ts # Updated with dark mode variants
276```
277
278## Benefits of This Approach
279
280- **Incremental**: Can be implemented component by component
281- **Standard**: Uses Tailwind's built-in dark mode features
282- **Performant**: Class-based approach is efficient
283- **Maintainable**: Clear separation of concerns with theme service
284- **Accessible**: Respects system preferences by default
285- **Consistent**: Follows Sketch's existing component patterns
286
287## Implementation Timeline
288
2891. **Week 1**: Phase 1 - Foundation (config, service, toggle)
2902. **Week 2**: Phase 2 - Core component updates
2913. **Week 3**: Phase 2 - Secondary component updates
2924. **Week 4**: Phase 3 - Polish, testing, and accessibility
293
294## Notes
295
296- Components extend `SketchTailwindElement` (not `LitElement`)
297- No Shadow DOM usage allows for global Tailwind classes
298- Theme service uses singleton pattern for consistency
299- Event system allows components to react to theme changes
300- LocalStorage preserves user preference across sessions