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/src/web-components/theme-service.ts b/webui/src/web-components/theme-service.ts
new file mode 100644
index 0000000..f002572
--- /dev/null
+++ b/webui/src/web-components/theme-service.ts
@@ -0,0 +1,126 @@
+export type ThemeMode = "light" | "dark" | "system";
+
+export class ThemeService {
+  private static instance: ThemeService;
+  private systemPrefersDark = false;
+  private systemMediaQuery: MediaQueryList;
+
+  private constructor() {
+    this.systemMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
+    this.systemPrefersDark = this.systemMediaQuery.matches;
+
+    // Listen for system theme changes
+    this.systemMediaQuery.addEventListener("change", (e) => {
+      this.systemPrefersDark = e.matches;
+      // If current theme is 'system', update the applied theme
+      if (this.getTheme() === "system") {
+        this.applyTheme();
+      }
+    });
+  }
+
+  static getInstance(): ThemeService {
+    if (!this.instance) {
+      this.instance = new ThemeService();
+    }
+    return this.instance;
+  }
+
+  /**
+   * Cycle through theme modes: light -> dark -> system -> light
+   */
+  toggleTheme(): void {
+    const currentTheme = this.getTheme();
+    let nextTheme: ThemeMode;
+
+    switch (currentTheme) {
+      case "light":
+        nextTheme = "dark";
+        break;
+      case "dark":
+        nextTheme = "system";
+        break;
+      case "system":
+        nextTheme = "light";
+        break;
+      default:
+        nextTheme = "light";
+    }
+
+    this.setTheme(nextTheme);
+  }
+
+  /**
+   * Set the theme mode
+   */
+  setTheme(theme: ThemeMode): void {
+    // Store the theme preference
+    if (theme === "system") {
+      localStorage.removeItem("theme");
+    } else {
+      localStorage.setItem("theme", theme);
+    }
+
+    // Apply the theme
+    this.applyTheme();
+
+    // Dispatch event for components that need to react
+    document.dispatchEvent(
+      new CustomEvent("theme-changed", {
+        detail: {
+          theme,
+          effectiveTheme: this.getEffectiveTheme(),
+          systemPrefersDark: this.systemPrefersDark,
+        },
+      }),
+    );
+  }
+
+  /**
+   * Get the current theme preference (light, dark, or system)
+   */
+  getTheme(): ThemeMode {
+    const saved = localStorage.getItem("theme");
+    if (saved === "light" || saved === "dark") {
+      return saved;
+    }
+    return "system";
+  }
+
+  /**
+   * Get the effective theme (what is actually applied: light or dark)
+   */
+  getEffectiveTheme(): "light" | "dark" {
+    const theme = this.getTheme();
+    if (theme === "system") {
+      return this.systemPrefersDark ? "dark" : "light";
+    }
+    return theme;
+  }
+
+  /**
+   * Check if dark mode is currently active
+   */
+  isDarkMode(): boolean {
+    return this.getEffectiveTheme() === "dark";
+  }
+
+  /**
+   * Apply the current theme to the DOM
+   */
+  private applyTheme(): void {
+    const effectiveTheme = this.getEffectiveTheme();
+    document.documentElement.classList.toggle(
+      "dark",
+      effectiveTheme === "dark",
+    );
+  }
+
+  /**
+   * Initialize the theme system
+   */
+  initializeTheme(): void {
+    // Apply the initial theme
+    this.applyTheme();
+  }
+}