webui: implement modular demo system with TypeScript and shared fixtures

Replace hand-written HTML demo pages with TypeScript demo modules and
automated infrastructure to reduce maintenance overhead and improve
developer experience with type safety and shared code.

Problems Solved:

Demo Maintenance Overhead:
- Hand-written HTML demo pages contained extensive boilerplate duplication
- No type checking for demo setup code or component data
- Manual maintenance of demo/index.html with available demos
- Difficult to share common fake data between demo pages
- No hot module replacement for demo development

Code Quality and Consistency:
- Demo setup code written in plain JavaScript without type safety
- No validation that demo data matches component interfaces
- Inconsistent styling and structure across demo pages
- Duplicated fake data declarations in each demo file

Solution Architecture:

TypeScript Demo Module System:
- Created DemoModule interface for standardized demo structure
- Demo modules export title, description, imports, and setup functions
- Full TypeScript compilation with type checking for demo code
- Dynamic import system for on-demand demo loading with Vite integration

Shared Demo Infrastructure:
- demo-framework/ with types.ts and demo-runner.ts for core functionality
- DemoRunner class handles dynamic loading, cleanup, and error handling
- Single demo-runner.html page loads any demo module dynamically
- Supports URL hash routing for direct demo links

Centralized Fake Data:
- demo-fixtures/ directory with shared TypeScript data files
- sampleToolCalls, sampleTimelineMessages, and sampleContainerState
- Type-safe imports ensure demo data matches component interfaces
- demoUtils with helper functions for consistent demo UI creation

Auto-generated Index Page:
- generate-index.ts scans for *.demo.ts files and extracts metadata
- Creates index-generated.html with links to all available demos
- Automatically includes demo titles and descriptions
- Eliminates manual maintenance of demo listing

Implementation Details:

Demo Framework:
- DemoRunner.loadDemo() uses dynamic imports with Vite ignore comments
- Automatic component import based on demo module configuration
- Support for demo-specific CSS and cleanup functions
- Error handling with detailed error display for debugging

Demo Module Structure:
- sketch-chat-input.demo.ts: Interactive chat with message history
- sketch-container-status.demo.ts: Status variations with real-time updates
- sketch-tool-calls.demo.ts: Multiple tool call examples with progressive loading
- All use shared fixtures and utilities for consistent experience

Vite Integration:
- Hot Module Replacement works for demo modules and shared fixtures
- TypeScript compilation on-the-fly for immediate feedback
- Dynamic imports work seamlessly with Vite's module system
- @vite-ignore comments prevent import analysis warnings

Testing and Validation:
- Tested demo runner loads and displays available components
- Verified component discovery and dynamic import functionality
- Confirmed shared fixture imports work correctly
- Validated auto-generated index creation and content

Files Modified:
- demo-framework/types.ts: TypeScript interfaces for demo system
- demo-framework/demo-runner.ts: Core demo loading and execution logic
- demo-fixtures/: Shared fake data (tool-calls.ts, timeline-messages.ts, container-status.ts, index.ts)
- demo-runner.html: Interactive demo browser with sidebar navigation
- generate-index.ts: Auto-generation script for demo index
- sketch-chat-input.demo.ts: Converted chat input demo to TypeScript
- sketch-container-status.demo.ts: Container status demo with variations
- sketch-tool-calls.demo.ts: Tool calls demo with interactive examples
- readme.md: Comprehensive documentation for new demo system

Benefits:
- Developers get full TypeScript type checking for demo code
- Shared fake data ensures consistency and reduces duplication
- Hot module replacement provides instant feedback during development
- Auto-generated index eliminates manual maintenance
- Modular architecture makes it easy to add new demos
- Vite integration provides fast development iteration

The new system reduces demo maintenance overhead while providing
better developer experience through TypeScript, shared code, and
automated infrastructure.

Co-Authored-By: sketch <hello@sketch.dev>
Change-ID: s3d91894eb7c4a79fk
diff --git a/webui/src/web-components/demo/demo-framework/demo-runner.ts b/webui/src/web-components/demo/demo-framework/demo-runner.ts
new file mode 100644
index 0000000..53f9fba
--- /dev/null
+++ b/webui/src/web-components/demo/demo-framework/demo-runner.ts
@@ -0,0 +1,219 @@
+/**
+ * Demo runner that dynamically loads and executes demo modules
+ */
+
+import {
+  DemoModule,
+  DemoRegistry,
+  DemoRunnerOptions,
+  DemoNavigationEvent,
+} from "./types";
+
+export class DemoRunner {
+  private container: HTMLElement;
+  private basePath: string;
+  private currentDemo: DemoModule | null = null;
+  private currentComponentName: string | null = null;
+  private onDemoChange?: (componentName: string, demo: DemoModule) => void;
+
+  constructor(options: DemoRunnerOptions) {
+    this.container = options.container;
+    this.basePath = options.basePath || "../";
+    this.onDemoChange = options.onDemoChange;
+  }
+
+  /**
+   * Load and display a demo for the specified component
+   */
+  async loadDemo(componentName: string): Promise<void> {
+    try {
+      // Cleanup current demo if any
+      await this.cleanup();
+
+      // Dynamically import the demo module
+      const demoModule = await import(
+        /* @vite-ignore */ `../${componentName}.demo.ts`
+      );
+      const demo: DemoModule = demoModule.default;
+
+      if (!demo) {
+        throw new Error(
+          `Demo module for ${componentName} does not export a default DemoModule`,
+        );
+      }
+
+      // Clear container
+      this.container.innerHTML = "";
+
+      // Load additional styles if specified
+      if (demo.styles) {
+        for (const styleUrl of demo.styles) {
+          await this.loadStylesheet(styleUrl);
+        }
+      }
+
+      // Add custom styles if specified
+      if (demo.customStyles) {
+        this.addCustomStyles(demo.customStyles, componentName);
+      }
+
+      // Import required component modules
+      if (demo.imports) {
+        for (const importPath of demo.imports) {
+          await import(/* @vite-ignore */ this.basePath + importPath);
+        }
+      }
+
+      // Set up the demo
+      await demo.setup(this.container);
+
+      // Update current state
+      this.currentDemo = demo;
+      this.currentComponentName = componentName;
+
+      // Notify listeners
+      if (this.onDemoChange) {
+        this.onDemoChange(componentName, demo);
+      }
+
+      // Dispatch navigation event
+      const event: DemoNavigationEvent = new CustomEvent("demo-navigation", {
+        detail: { componentName, demo },
+      });
+      document.dispatchEvent(event);
+    } catch (error) {
+      console.error(`Failed to load demo for ${componentName}:`, error);
+      this.showError(`Failed to load demo for ${componentName}`, error);
+    }
+  }
+
+  /**
+   * Get list of available demo components by scanning for .demo.ts files
+   */
+  async getAvailableComponents(): Promise<string[]> {
+    // For now, we'll maintain a registry of known demo components
+    // This could be improved with build-time generation
+    const knownComponents = [
+      "sketch-chat-input",
+      "sketch-container-status",
+      "sketch-tool-calls",
+    ];
+
+    // Filter to only components that actually have demo files
+    const availableComponents: string[] = [];
+    for (const component of knownComponents) {
+      try {
+        // Test if the demo module exists by attempting to import it
+        const demoModule = await import(
+          /* @vite-ignore */ `../${component}.demo.ts`
+        );
+        if (demoModule.default) {
+          availableComponents.push(component);
+        }
+      } catch (error) {
+        console.warn(`Demo not available for ${component}:`, error);
+        // Component demo doesn't exist, skip it
+      }
+    }
+
+    return availableComponents;
+  }
+
+  /**
+   * Cleanup current demo
+   */
+  private async cleanup(): Promise<void> {
+    if (this.currentDemo?.cleanup) {
+      await this.currentDemo.cleanup();
+    }
+
+    // Remove custom styles
+    if (this.currentComponentName) {
+      this.removeCustomStyles(this.currentComponentName);
+    }
+
+    this.currentDemo = null;
+    this.currentComponentName = null;
+  }
+
+  /**
+   * Load a CSS stylesheet dynamically
+   */
+  private async loadStylesheet(url: string): Promise<void> {
+    return new Promise((resolve, reject) => {
+      const link = document.createElement("link");
+      link.rel = "stylesheet";
+      link.href = url;
+      link.onload = () => resolve();
+      link.onerror = () =>
+        reject(new Error(`Failed to load stylesheet: ${url}`));
+      document.head.appendChild(link);
+    });
+  }
+
+  /**
+   * Add custom CSS styles for a demo
+   */
+  private addCustomStyles(css: string, componentName: string): void {
+    const styleId = `demo-custom-styles-${componentName}`;
+
+    // Remove existing styles for this component
+    const existing = document.getElementById(styleId);
+    if (existing) {
+      existing.remove();
+    }
+
+    // Add new styles
+    const style = document.createElement("style");
+    style.id = styleId;
+    style.textContent = css;
+    document.head.appendChild(style);
+  }
+
+  /**
+   * Remove custom styles for a component
+   */
+  private removeCustomStyles(componentName: string): void {
+    const styleId = `demo-custom-styles-${componentName}`;
+    const existing = document.getElementById(styleId);
+    if (existing) {
+      existing.remove();
+    }
+  }
+
+  /**
+   * Show error message in the demo container
+   */
+  private showError(message: string, error: any): void {
+    this.container.innerHTML = `
+      <div style="
+        padding: 20px;
+        background: #fee;
+        border: 1px solid #fcc;
+        border-radius: 4px;
+        color: #800;
+        font-family: monospace;
+      ">
+        <h3>Demo Error</h3>
+        <p><strong>${message}</strong></p>
+        <details>
+          <summary>Error Details</summary>
+          <pre>${error.stack || error.message || error}</pre>
+        </details>
+      </div>
+    `;
+  }
+
+  /**
+   * Get current demo info
+   */
+  getCurrentDemo(): { componentName: string; demo: DemoModule } | null {
+    if (this.currentComponentName && this.currentDemo) {
+      return {
+        componentName: this.currentComponentName,
+        demo: this.currentDemo,
+      };
+    }
+    return null;
+  }
+}