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();
+ }
+}