webui: add dark mode implementation plan
Also implements phase 1 of the plan, which just lays the foundation
for implementing the user-visible changes. This does not include
any dark-mode theme settings for the rest of the web UI, and
while it does inlcude a "sketch-theme-toggle" element, this is
only included in the demo:runner vite server for interactive testing.
It's not included in the app shell base yet.
-SM
---
Documents comprehensive strategy for implementing dark mode in Sketch's
web UI using Tailwind CSS class-based approach.
The plan covers:
- Foundation setup (Tailwind config, theme service, toggle component)
- Systematic component updates with dark mode variants
- Accessibility considerations and testing checklist
- 4-week implementation timeline
Key technical decisions:
- Uses SketchTailwindElement base class following existing patterns
- Singleton theme service with event system for component coordination
- Respects system preferences while allowing user override
- Persistent theme storage in localStorage
This provides a roadmap for adding dark mode support while maintaining
consistency with Sketch's existing web component architecture.
Co-Authored-By: sketch <hello@sketch.dev>
Change-ID: s6b69ad95a4394f98k
diff --git a/webui/DARK_MODE.md b/webui/DARK_MODE.md
new file mode 100644
index 0000000..82bd65b
--- /dev/null
+++ b/webui/DARK_MODE.md
@@ -0,0 +1,300 @@
+# 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