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-runner.html b/webui/src/web-components/demo/demo-runner.html
new file mode 100644
index 0000000..94af314
--- /dev/null
+++ b/webui/src/web-components/demo/demo-runner.html
@@ -0,0 +1,328 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Sketch Web Components Demo Runner</title>
+    <link rel="stylesheet" href="demo.css" />
+    <link rel="stylesheet" href="/dist/tailwind.css" />
+    <style>
+      :root {
+        --demo-primary: #0969da;
+        --demo-secondary: #656d76;
+        --demo-background: #f6f8fa;
+        --demo-border: #d1d9e0;
+      }
+
+      .demo-runner {
+        display: flex;
+        height: 100vh;
+        font-family:
+          -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+      }
+
+      .demo-sidebar {
+        width: 280px;
+        background: var(--demo-background);
+        border-right: 1px solid var(--demo-border);
+        padding: 20px;
+        overflow-y: auto;
+      }
+
+      .demo-content {
+        flex: 1;
+        padding: 20px;
+        overflow-y: auto;
+      }
+
+      .demo-nav {
+        list-style: none;
+        padding: 0;
+        margin: 0;
+      }
+
+      .demo-nav li {
+        margin-bottom: 4px;
+      }
+
+      .demo-nav button {
+        width: 100%;
+        text-align: left;
+        padding: 8px 12px;
+        background: transparent;
+        border: 1px solid transparent;
+        border-radius: 6px;
+        cursor: pointer;
+        font-size: 14px;
+        color: var(--demo-secondary);
+        transition: all 0.2s;
+      }
+
+      .demo-nav button:hover {
+        background: #ffffff;
+        border-color: var(--demo-border);
+        color: var(--demo-primary);
+      }
+
+      .demo-nav button.active {
+        background: var(--demo-primary);
+        color: white;
+      }
+
+      .demo-header {
+        margin-bottom: 20px;
+        padding-bottom: 15px;
+        border-bottom: 1px solid var(--demo-border);
+      }
+
+      .demo-title {
+        font-size: 24px;
+        font-weight: 600;
+        margin: 0 0 8px 0;
+        color: #24292f;
+      }
+
+      .demo-description {
+        color: var(--demo-secondary);
+        margin: 0;
+        font-size: 14px;
+      }
+
+      .demo-container {
+        background: white;
+        border: 1px solid var(--demo-border);
+        border-radius: 8px;
+        min-height: 400px;
+        padding: 20px;
+      }
+
+      .demo-loading {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        height: 200px;
+        color: var(--demo-secondary);
+      }
+
+      .demo-welcome {
+        text-align: center;
+        padding: 60px 20px;
+        color: var(--demo-secondary);
+      }
+
+      .demo-welcome h2 {
+        margin-bottom: 10px;
+        color: #24292f;
+      }
+
+      .search-box {
+        width: 100%;
+        padding: 8px 12px;
+        margin-bottom: 16px;
+        border: 1px solid var(--demo-border);
+        border-radius: 6px;
+        font-size: 14px;
+      }
+
+      .search-box:focus {
+        outline: none;
+        border-color: var(--demo-primary);
+      }
+
+      .demo-error {
+        padding: 20px;
+        background: #ffeaea;
+        border: 1px solid #ffcccc;
+        border-radius: 6px;
+        color: #d73a49;
+      }
+    </style>
+  </head>
+  <body>
+    <div class="demo-runner">
+      <nav class="demo-sidebar">
+        <h1 style="font-size: 18px; margin: 0 0 20px 0; color: #24292f">
+          Component Demos
+        </h1>
+
+        <input
+          type="text"
+          class="search-box"
+          placeholder="Search components..."
+          id="demo-search"
+        />
+
+        <ul class="demo-nav" id="demo-nav">
+          <!-- Component list will be populated dynamically -->
+        </ul>
+      </nav>
+
+      <main class="demo-content">
+        <div class="demo-header" id="demo-header" style="display: none">
+          <h1 class="demo-title" id="demo-title"></h1>
+          <p class="demo-description" id="demo-description"></p>
+        </div>
+
+        <div class="demo-container" id="demo-container">
+          <div class="demo-welcome">
+            <h2>Welcome to Sketch Component Demos</h2>
+            <p>Select a component from the sidebar to view its demo.</p>
+          </div>
+        </div>
+      </main>
+    </div>
+
+    <script type="module">
+      import { DemoRunner } from "./demo-framework/demo-runner.ts";
+
+      class DemoRunnerApp {
+        constructor() {
+          this.demoRunner = new DemoRunner({
+            container: document.getElementById("demo-container"),
+            onDemoChange: this.onDemoChange.bind(this),
+          });
+
+          this.searchBox = document.getElementById("demo-search");
+          this.navList = document.getElementById("demo-nav");
+          this.demoHeader = document.getElementById("demo-header");
+          this.demoTitle = document.getElementById("demo-title");
+          this.demoDescription = document.getElementById("demo-description");
+
+          this.currentComponent = null;
+          this.availableComponents = [];
+
+          this.init();
+        }
+
+        async init() {
+          try {
+            // Load available components
+            this.availableComponents =
+              await this.demoRunner.getAvailableComponents();
+            this.renderNavigation();
+
+            // Set up search
+            this.searchBox.addEventListener(
+              "input",
+              this.handleSearch.bind(this),
+            );
+
+            // Handle URL hash for direct linking
+            this.handleHashChange();
+            window.addEventListener(
+              "hashchange",
+              this.handleHashChange.bind(this),
+            );
+          } catch (error) {
+            console.error("Failed to initialize demo runner:", error);
+            this.showError("Failed to load demo components");
+          }
+        }
+
+        renderNavigation(filter = "") {
+          const filteredComponents = this.availableComponents.filter(
+            (component) =>
+              component.toLowerCase().includes(filter.toLowerCase()),
+          );
+
+          this.navList.innerHTML = "";
+
+          filteredComponents.forEach((component) => {
+            const li = document.createElement("li");
+            const button = document.createElement("button");
+            button.textContent = this.formatComponentName(component);
+            button.addEventListener("click", () =>
+              this.loadComponent(component),
+            );
+
+            if (component === this.currentComponent) {
+              button.classList.add("active");
+            }
+
+            li.appendChild(button);
+            this.navList.appendChild(li);
+          });
+        }
+
+        formatComponentName(component) {
+          return component
+            .replace(/^sketch-/, "")
+            .replace(/-/g, " ")
+            .replace(/\b\w/g, (l) => l.toUpperCase());
+        }
+
+        async loadComponent(componentName) {
+          if (this.currentComponent === componentName) {
+            return;
+          }
+
+          try {
+            this.showLoading();
+            await this.demoRunner.loadDemo(componentName);
+            this.currentComponent = componentName;
+
+            // Update URL hash
+            window.location.hash = componentName;
+
+            // Update navigation
+            this.renderNavigation(this.searchBox.value);
+          } catch (error) {
+            console.error(`Failed to load demo for ${componentName}:`, error);
+            this.showError(`Failed to load demo for ${componentName}`);
+          }
+        }
+
+        onDemoChange(componentName, demo) {
+          // Update header
+          this.demoTitle.textContent = demo.title;
+          this.demoDescription.textContent = demo.description || "";
+
+          if (demo.description) {
+            this.demoDescription.style.display = "block";
+          } else {
+            this.demoDescription.style.display = "none";
+          }
+
+          this.demoHeader.style.display = "block";
+        }
+
+        handleSearch(event) {
+          this.renderNavigation(event.target.value);
+        }
+
+        handleHashChange() {
+          const hash = window.location.hash.slice(1);
+          if (hash && this.availableComponents.includes(hash)) {
+            this.loadComponent(hash);
+          }
+        }
+
+        showLoading() {
+          document.getElementById("demo-container").innerHTML = `
+            <div class="demo-loading">
+              Loading demo...
+            </div>
+          `;
+        }
+
+        showError(message) {
+          document.getElementById("demo-container").innerHTML = `
+            <div class="demo-error">
+              <strong>Error:</strong> ${message}
+            </div>
+          `;
+        }
+      }
+
+      // Initialize the demo runner when DOM is ready
+      if (document.readyState === "loading") {
+        document.addEventListener(
+          "DOMContentLoaded",
+          () => new DemoRunnerApp(),
+        );
+      } else {
+        new DemoRunnerApp();
+      }
+    </script>
+  </body>
+</html>