blob: a937f6f5a9f9aa50e32161e8070544e0ba89f159 [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 {
banksean3eaa4332025-07-19 02:19:06 +000026 content: ["./src/**/*.{js,ts,jsx,tsx,html}", "./src/test-theme.html"],
27 darkMode: "selector", // Enable selector-based dark mode
bankseanae3724e2025-07-18 16:52:37 +000028 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
banksean3eaa4332025-07-19 02:19:06 +000063#### 2. Theme Management Service (Already Implemented)
bankseanae3724e2025-07-18 16:52:37 +000064
65```typescript
66// src/web-components/theme-service.ts
banksean3eaa4332025-07-19 02:19:06 +000067export type ThemeMode = "light" | "dark" | "system";
68
bankseanae3724e2025-07-18 16:52:37 +000069export class ThemeService {
70 private static instance: ThemeService;
banksean3eaa4332025-07-19 02:19:06 +000071 private systemPrefersDark = false;
72 private systemMediaQuery: MediaQueryList;
bankseanae3724e2025-07-18 16:52:37 +000073
74 static getInstance(): ThemeService {
75 if (!this.instance) {
76 this.instance = new ThemeService();
77 }
78 return this.instance;
79 }
80
banksean3eaa4332025-07-19 02:19:06 +000081 /**
82 * Cycle through theme modes: light -> dark -> system -> light
83 */
bankseanae3724e2025-07-18 16:52:37 +000084 toggleTheme(): void {
banksean3eaa4332025-07-19 02:19:06 +000085 const currentTheme = this.getTheme();
86 let nextTheme: ThemeMode;
87
88 switch (currentTheme) {
89 case "light":
90 nextTheme = "dark";
91 break;
92 case "dark":
93 nextTheme = "system";
94 break;
95 case "system":
96 nextTheme = "light";
97 break;
98 default:
99 nextTheme = "light";
100 }
101
102 this.setTheme(nextTheme);
bankseanae3724e2025-07-18 16:52:37 +0000103 }
104
banksean3eaa4332025-07-19 02:19:06 +0000105 setTheme(theme: ThemeMode): void {
106 // Store the theme preference
107 if (theme === "system") {
108 localStorage.removeItem("theme");
109 } else {
110 localStorage.setItem("theme", theme);
111 }
112
113 // Apply the theme
114 this.applyTheme();
bankseanae3724e2025-07-18 16:52:37 +0000115
116 // Dispatch event for components that need to react
117 document.dispatchEvent(
118 new CustomEvent("theme-changed", {
banksean3eaa4332025-07-19 02:19:06 +0000119 detail: {
120 theme,
121 effectiveTheme: this.getEffectiveTheme(),
122 systemPrefersDark: this.systemPrefersDark,
123 },
bankseanae3724e2025-07-18 16:52:37 +0000124 }),
125 );
126 }
127
banksean3eaa4332025-07-19 02:19:06 +0000128 getTheme(): ThemeMode {
129 const saved = localStorage.getItem("theme");
130 if (saved === "light" || saved === "dark") {
131 return saved;
132 }
133 return "system";
134 }
135
136 getEffectiveTheme(): "light" | "dark" {
137 const theme = this.getTheme();
138 if (theme === "system") {
139 return this.systemPrefersDark ? "dark" : "light";
140 }
141 return theme;
bankseanae3724e2025-07-18 16:52:37 +0000142 }
143
144 initializeTheme(): void {
banksean3eaa4332025-07-19 02:19:06 +0000145 this.applyTheme();
bankseanae3724e2025-07-18 16:52:37 +0000146 }
147}
148```
149
banksean3eaa4332025-07-19 02:19:06 +0000150#### 3. Theme Toggle Component (Already Implemented)
bankseanae3724e2025-07-18 16:52:37 +0000151
152```typescript
banksean3eaa4332025-07-19 02:19:06 +0000153// src/web-components/sketch-theme-toggle.ts
bankseanae3724e2025-07-18 16:52:37 +0000154import { html } from "lit";
155import { customElement, state } from "lit/decorators.js";
156import { SketchTailwindElement } from "./sketch-tailwind-element.js";
banksean3eaa4332025-07-19 02:19:06 +0000157import { ThemeService, ThemeMode } from "./theme-service.js";
bankseanae3724e2025-07-18 16:52:37 +0000158
banksean3eaa4332025-07-19 02:19:06 +0000159@customElement("sketch-theme-toggle")
160export class SketchThemeToggle extends SketchTailwindElement {
161 @state() private currentTheme: ThemeMode = "system";
162 @state() private effectiveTheme: "light" | "dark" = "light";
bankseanae3724e2025-07-18 16:52:37 +0000163
164 private themeService = ThemeService.getInstance();
165
166 connectedCallback() {
167 super.connectedCallback();
banksean3eaa4332025-07-19 02:19:06 +0000168 this.updateThemeState();
bankseanae3724e2025-07-18 16:52:37 +0000169
170 // Listen for theme changes from other sources
171 document.addEventListener("theme-changed", this.handleThemeChange);
172 }
173
174 disconnectedCallback() {
175 super.disconnectedCallback();
176 document.removeEventListener("theme-changed", this.handleThemeChange);
177 }
178
179 private handleThemeChange = (e: CustomEvent) => {
banksean3eaa4332025-07-19 02:19:06 +0000180 this.currentTheme = e.detail.theme;
181 this.effectiveTheme = e.detail.effectiveTheme;
bankseanae3724e2025-07-18 16:52:37 +0000182 };
183
184 private toggleTheme() {
185 this.themeService.toggleTheme();
186 }
187
banksean3eaa4332025-07-19 02:19:06 +0000188 private getThemeIcon(): string {
189 switch (this.currentTheme) {
190 case "light":
191 return "☀️"; // Sun
192 case "dark":
193 return "🌙"; // Moon
194 case "system":
195 return "💻"; // Computer/Laptop
196 default:
197 return "💻";
198 }
199 }
200
bankseanae3724e2025-07-18 16:52:37 +0000201 render() {
202 return html`
203 <button
204 @click=${this.toggleTheme}
banksean3eaa4332025-07-19 02:19:06 +0000205 class="p-2 rounded-md border border-gray-300 dark:border-gray-600
bankseanae3724e2025-07-18 16:52:37 +0000206 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-200
207 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors
208 focus:outline-none focus:ring-2 focus:ring-blue-500"
banksean3eaa4332025-07-19 02:19:06 +0000209 title="${this.currentTheme} theme - Click to cycle themes"
210 aria-label="Cycle between light, dark, and system theme"
bankseanae3724e2025-07-18 16:52:37 +0000211 >
banksean3eaa4332025-07-19 02:19:06 +0000212 ${this.getThemeIcon()}
bankseanae3724e2025-07-18 16:52:37 +0000213 </button>
214 `;
215 }
216}
217
218declare global {
219 interface HTMLElementTagNameMap {
banksean3eaa4332025-07-19 02:19:06 +0000220 "sketch-theme-toggle": SketchThemeToggle;
bankseanae3724e2025-07-18 16:52:37 +0000221 }
222}
223```
224
225#### 4. Initialize Theme in App Shell
226
banksean3eaa4332025-07-19 02:19:06 +0000227Add theme initialization to the main app shell component. This needs to be implemented:
bankseanae3724e2025-07-18 16:52:37 +0000228
229```typescript
banksean3eaa4332025-07-19 02:19:06 +0000230// In sketch-app-shell.ts or sketch-app-shell-base.ts
bankseanae3724e2025-07-18 16:52:37 +0000231import { ThemeService } from "./theme-service.js";
232
233connectedCallback() {
234 super.connectedCallback();
235 ThemeService.getInstance().initializeTheme();
236}
237```
238
banksean3eaa4332025-07-19 02:19:06 +0000239**Note**: This initialization is not yet implemented in the app shell components.
240
bankseanae3724e2025-07-18 16:52:37 +0000241### Phase 2: Component Updates
242
243#### Systematic Component Audit
244
2451. **Identify all components using color classes**
246
247 - Search for `bg-`, `text-`, `border-`, `ring-`, `divide-` classes
248 - Document current color usage patterns
249
2502. **Add dark mode variants systematically**
251
252 - Start with core components (app shell, chat input, etc.)
253 - Update classes following the pattern:
254
255 ```typescript
256 // Before:
257 class="bg-white text-gray-900 border-gray-200"
258
259 // After:
260 class="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 border-gray-200 dark:border-gray-700"
261 ```
262
2633. **Priority order for component updates:**
264 1. `sketch-app-shell` - Main container
265 2. `sketch-chat-input` - Primary interaction component
266 3. `sketch-container-status` - Status indicators
267 4. `sketch-call-status` - Call indicators
268 5. Demo components and other secondary components
269
270#### Common Dark Mode Color Mappings
271
272```scss
273// Light -> Dark mappings
274bg-white -> bg-gray-900
275bg-gray-50 -> bg-gray-800
276bg-gray-100 -> bg-gray-800
277bg-gray-200 -> bg-gray-700
278
279text-gray-900 -> text-gray-100
280text-gray-800 -> text-gray-200
281text-gray-700 -> text-gray-300
282text-gray-600 -> text-gray-400
283text-gray-500 -> text-gray-500 (neutral)
284
285border-gray-200 -> border-gray-700
286border-gray-300 -> border-gray-600
287
288ring-gray-300 -> ring-gray-600
289```
290
291### Phase 3: Polish and Testing
292
293#### 1. Smooth Transitions
294
295- Add `transition-colors` to interactive elements
296- Consider adding a global transition class for theme changes
297
298#### 2. Accessibility
299
300- Ensure sufficient contrast ratios in both modes
301- Test with screen readers
302- Verify focus indicators work in both themes
303- Add proper ARIA labels to theme toggle
304
305#### 3. Testing Checklist
306
307- [ ] Theme persists across page reloads
308- [ ] System preference detection works
309- [ ] All components render correctly in both modes
310- [ ] Interactive states (hover, focus, active) work in both themes
311- [ ] No flash of unstyled content (FOUC)
312- [ ] Works across different screen sizes
313- [ ] Performance impact is minimal
314
315## File Structure
316
317```
318src/web-components/
banksean3eaa4332025-07-19 02:19:06 +0000319├── theme-service.ts # Theme management service (✅ implemented)
320├── sketch-theme-toggle.ts # Theme toggle component (✅ implemented)
321├── sketch-tailwind-element.ts # Base class (✅ existing)
322└── [other components].ts # Need dark mode variants added
bankseanae3724e2025-07-18 16:52:37 +0000323```
324
325## Benefits of This Approach
326
327- **Incremental**: Can be implemented component by component
328- **Standard**: Uses Tailwind's built-in dark mode features
329- **Performant**: Class-based approach is efficient
330- **Maintainable**: Clear separation of concerns with theme service
331- **Accessible**: Respects system preferences by default
332- **Consistent**: Follows Sketch's existing component patterns
333
banksean3eaa4332025-07-19 02:19:06 +0000334## Current Implementation Status
bankseanae3724e2025-07-18 16:52:37 +0000335
banksean3eaa4332025-07-19 02:19:06 +0000336### ✅ Completed:
337
338- Tailwind configuration with dark mode enabled
339- Theme management service with light/dark/system modes
340- Theme toggle component with cycling behavior
341- Base `SketchTailwindElement` class
342
343### 🚧 Partially Complete:
344
345- Some components may have dark mode classes
346
347### ❌ Still Needed:
348
349- Theme initialization in app shell components
350- Systematic audit and update of all components for dark mode
351- Testing and accessibility verification
352
353## Next Steps Timeline
354
3551. Phase 1 - Add theme initialization to app shell
3562. Phase 2 - Core component updates (systematic audit)
3573. Phase 2 - Secondary component updates
3584. Phase 3 - Polish, testing, and accessibility
bankseanae3724e2025-07-18 16:52:37 +0000359
360## Notes
361
banksean3eaa4332025-07-19 02:19:06 +0000362- Components extend `SketchTailwindElement` (not `LitElement`) ✅
363- No Shadow DOM usage allows for global Tailwind classes ✅
364- Theme service uses singleton pattern for consistency ✅
365- Theme service supports three modes: light, dark, and system ✅
366- Event system allows components to react to theme changes ✅
367- LocalStorage preserves user preference across sessions ✅
368- Theme toggle cycles through all three modes ✅