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/sketch-theme-toggle.ts b/webui/src/web-components/sketch-theme-toggle.ts
new file mode 100644
index 0000000..7e428fa
--- /dev/null
+++ b/webui/src/web-components/sketch-theme-toggle.ts
@@ -0,0 +1,100 @@
+import { html } from "lit";
+import { customElement, state } from "lit/decorators.js";
+import { SketchTailwindElement } from "./sketch-tailwind-element.js";
+import { ThemeService, ThemeMode } from "./theme-service.js";
+
+@customElement("sketch-theme-toggle")
+export class SketchThemeToggle extends SketchTailwindElement {
+ @state() private currentTheme: ThemeMode = "system";
+ @state() private effectiveTheme: "light" | "dark" = "light";
+
+ private themeService = ThemeService.getInstance();
+
+ connectedCallback() {
+ super.connectedCallback();
+ this.updateThemeState();
+
+ // 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.currentTheme = e.detail.theme;
+ this.effectiveTheme = e.detail.effectiveTheme;
+ };
+
+ private updateThemeState() {
+ this.currentTheme = this.themeService.getTheme();
+ this.effectiveTheme = this.themeService.getEffectiveTheme();
+ }
+
+ private toggleTheme() {
+ this.themeService.toggleTheme();
+ }
+
+ private getThemeIcon(): string {
+ switch (this.currentTheme) {
+ case "light":
+ return "\u2600\ufe0f"; // Sun
+ case "dark":
+ return "\ud83c\udf19"; // Moon
+ case "system":
+ return "\ud83d\udcbb"; // Computer/Laptop
+ default:
+ return "\ud83d\udcbb";
+ }
+ }
+
+ private getThemeLabel(): string {
+ switch (this.currentTheme) {
+ case "light":
+ return "Light mode";
+ case "dark":
+ return "Dark mode";
+ case "system":
+ return `System theme (${this.effectiveTheme})`;
+ default:
+ return "System theme";
+ }
+ }
+
+ private getNextThemeLabel(): string {
+ switch (this.currentTheme) {
+ case "light":
+ return "Switch to dark mode";
+ case "dark":
+ return "Switch to system theme";
+ case "system":
+ return "Switch to light mode";
+ default:
+ return "Switch theme";
+ }
+ }
+
+ 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="${this.getThemeLabel()} - ${this.getNextThemeLabel()}"
+ aria-label="${this.getNextThemeLabel()}"
+ >
+ ${this.getThemeIcon()}
+ </button>
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "sketch-theme-toggle": SketchThemeToggle;
+ }
+}