| banksean | ae3724e | 2025-07-18 16:52:37 +0000 | [diff] [blame] | 1 | # Dark Mode Implementation Plan |
| 2 | |
| 3 | ## Overview |
| 4 | |
| 5 | This 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 | |
| 9 | Sketch'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 |
| 25 | export 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 |
| 67 | export 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 |
| 124 | import { html } from "lit"; |
| 125 | import { customElement, state } from "lit/decorators.js"; |
| 126 | import { SketchTailwindElement } from "./sketch-tailwind-element.js"; |
| 127 | import { ThemeService } from "./theme-service.js"; |
| 128 | |
| 129 | @customElement("theme-toggle") |
| 130 | export 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 | |
| 173 | declare global { |
| 174 | interface HTMLElementTagNameMap { |
| 175 | "theme-toggle": ThemeToggle; |
| 176 | } |
| 177 | } |
| 178 | ``` |
| 179 | |
| 180 | #### 4. Initialize Theme in App Shell |
| 181 | |
| 182 | Add theme initialization to the main app shell component: |
| 183 | |
| 184 | ```typescript |
| 185 | // In sketch-app-shell.ts or similar |
| 186 | import { ThemeService } from "./theme-service.js"; |
| 187 | |
| 188 | connectedCallback() { |
| 189 | super.connectedCallback(); |
| 190 | ThemeService.getInstance().initializeTheme(); |
| 191 | } |
| 192 | ``` |
| 193 | |
| 194 | ### Phase 2: Component Updates |
| 195 | |
| 196 | #### Systematic Component Audit |
| 197 | |
| 198 | 1. **Identify all components using color classes** |
| 199 | |
| 200 | - Search for `bg-`, `text-`, `border-`, `ring-`, `divide-` classes |
| 201 | - Document current color usage patterns |
| 202 | |
| 203 | 2. **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 | |
| 216 | 3. **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 |
| 227 | bg-white -> bg-gray-900 |
| 228 | bg-gray-50 -> bg-gray-800 |
| 229 | bg-gray-100 -> bg-gray-800 |
| 230 | bg-gray-200 -> bg-gray-700 |
| 231 | |
| 232 | text-gray-900 -> text-gray-100 |
| 233 | text-gray-800 -> text-gray-200 |
| 234 | text-gray-700 -> text-gray-300 |
| 235 | text-gray-600 -> text-gray-400 |
| 236 | text-gray-500 -> text-gray-500 (neutral) |
| 237 | |
| 238 | border-gray-200 -> border-gray-700 |
| 239 | border-gray-300 -> border-gray-600 |
| 240 | |
| 241 | ring-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 | ``` |
| 271 | src/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 | |
| 289 | 1. **Week 1**: Phase 1 - Foundation (config, service, toggle) |
| 290 | 2. **Week 2**: Phase 2 - Core component updates |
| 291 | 3. **Week 3**: Phase 2 - Secondary component updates |
| 292 | 4. **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 |