webui: convert SketchTimeline to use TailwindElement and Tailwind CSS classes

Convert SketchTimeline component from Lit CSS-in-JS styles to TailwindElement
inheritance with Tailwind utility classes, replacing shadow DOM styling with
global Tailwind CSS classes.

Problems Solved:

CSS Inconsistency:
- SketchTimeline used shadow DOM with CSS-in-JS styles while other components use TailwindElement
- Component styling was isolated from global design system
- Difficult to maintain consistent visual appearance across components
- No access to global Tailwind utility classes within shadow DOM

Test Brittleness:
- Tests relied on CSS class selectors that were implementation details
- Complex CSS class selectors made tests fragile to styling changes
- No standardized approach for testing UI elements across components

Missing Demo Infrastructure:
- SketchTimeline had no TypeScript demo module for component development
- Component not included in demo runner system for iterative development
- Only had static HTML demo without interactive controls

Solution Implementation:

TailwindElement Conversion:
- Changed inheritance from LitElement to SketchTailwindElement to disable shadow DOM
- Replaced all CSS-in-JS styles with equivalent Tailwind utility classes
- Added custom CSS for complex animations (thinking dots, loading spinner) that can't be easily replicated with Tailwind
- Maintained all existing visual styling and behavior while using Tailwind classes

CSS Class Mapping:
- .timeline-container → w-full relative max-w-full mx-auto px-[15px] box-border overflow-x-hidden flex-1 min-h-[100px]
- .welcome-box → my-8 mx-auto max-w-[90%] w-[90%] p-8 border-2 border-gray-300 rounded-lg shadow-sm bg-white text-center
- .thinking-indicator → pl-[85px] mt-1.5 mb-4 flex
- .loading-indicator → flex items-center justify-center p-5 text-gray-600 text-sm gap-2.5 opacity-100
- Added print: utility variants for print styling support

Test Infrastructure Updates:
- Replaced CSS class selectors with data-testid attributes for reliable element targeting
- Updated all test selectors to use [data-testid='element-name'] pattern
- Added test IDs to welcome-box, timeline-container, thinking-indicator, loading-indicator, thinking-bubble, thinking-dots, and thinking-dot elements
- Maintained all existing test functionality while improving test reliability

Demo Module Creation:
- Created sketch-timeline.demo.ts with comprehensive interactive demo
- Implemented basic timeline, loading states, thinking states, and interactive controls
- Added mock message generation with various message types and tool calls
- Included controls for adding messages, toggling thinking state, compact padding, and reset functionality
- Added SketchTimeline to knownComponents list in demo-runner.ts

Custom Styling Architecture:
- Added addCustomStyles() method to inject necessary CSS that can't be replicated with Tailwind
- Created thinking-pulse keyframe animation for thinking dots
- Added loading-spin animation for spinner elements
- Implemented compact-padding responsive styling
- Used document.head.appendChild for global style injection with duplicate prevention

Implementation Details:

Component Structure:
- Maintained all existing properties, methods, and component lifecycle
- Preserved scroll handling, viewport management, and loading operations
- Added data-testid attributes without affecting visual presentation
- Kept all existing functionality while changing only the styling approach

Styling Consistency:
- All colors, spacing, borders, and animations maintained visual parity
- Print styles converted to Tailwind print: variants
- Hover and active states preserved with Tailwind state variants
- Responsive design maintained with existing breakpoint behavior

Test Reliability:
- Test selectors now target semantic element roles rather than implementation details
- More robust element identification reduces test flakiness
- Consistent testing pattern across all timeline-related components
- Better separation between styling and testing concerns

Demo Development:
- Interactive demo supports real-time component behavior testing
- Mock data factory functions for consistent test data generation
- Multiple demo scenarios covering empty state, populated timeline, and various loading states
- Control buttons for testing user interactions and state changes

Files Modified:
- sketch/webui/src/web-components/sketch-timeline.ts: TailwindElement inheritance and Tailwind class conversion
- sketch/webui/src/web-components/sketch-timeline.test.ts: Updated test selectors to use data-testid attributes
- sketch/webui/src/web-components/demo/sketch-timeline.demo.ts: New interactive demo module
- sketch/webui/src/web-components/demo/demo-framework/demo-runner.ts: Added sketch-timeline to knownComponents

The conversion maintains complete visual and functional parity while enabling
consistent styling across the component library and improving test reliability
through semantic element targeting.

Co-Authored-By: sketch <hello@sketch.dev>
Change-ID: s0621383cac6304dek
diff --git a/webui/src/web-components/demo/test-timeline-manual.html b/webui/src/web-components/demo/test-timeline-manual.html
new file mode 100644
index 0000000..67107cd
--- /dev/null
+++ b/webui/src/web-components/demo/test-timeline-manual.html
@@ -0,0 +1,188 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Sketch Timeline Manual Test</title>
+    <link rel="stylesheet" href="/dist/tailwind.css" />
+    <style>
+      body {
+        font-family:
+          system-ui,
+          -apple-system,
+          sans-serif;
+        margin: 0;
+        padding: 20px;
+      }
+      .test-container {
+        max-width: 800px;
+        margin: 0 auto;
+      }
+      .test-section {
+        margin: 40px 0;
+        padding: 20px;
+        border: 1px solid #e1e5e9;
+        border-radius: 8px;
+      }
+      .test-section h3 {
+        margin: 0 0 15px 0;
+        color: #24292f;
+      }
+      .timeline-wrapper {
+        height: 400px;
+        border: 1px solid #ccc;
+        border-radius: 6px;
+        margin: 15px 0;
+      }
+    </style>
+  </head>
+  <body>
+    <div class="test-container">
+      <h1>Sketch Timeline Component Test</h1>
+      <p>
+        This page tests the converted SketchTimeline component using Tailwind
+        CSS.
+      </p>
+
+      <div class="test-section">
+        <h3>Empty Timeline (Welcome State)</h3>
+        <div class="timeline-wrapper">
+          <sketch-timeline id="empty-timeline"></sketch-timeline>
+        </div>
+      </div>
+
+      <div class="test-section">
+        <h3>Timeline with Messages</h3>
+        <div class="timeline-wrapper">
+          <sketch-timeline id="messages-timeline"></sketch-timeline>
+        </div>
+      </div>
+
+      <div class="test-section">
+        <h3>Timeline with Thinking State</h3>
+        <div class="timeline-wrapper">
+          <sketch-timeline id="thinking-timeline"></sketch-timeline>
+        </div>
+      </div>
+
+      <div class="test-section">
+        <h3>Controls</h3>
+        <button id="add-message">Add Message</button>
+        <button id="toggle-thinking">Toggle Thinking</button>
+        <button id="clear-messages">Clear Messages</button>
+      </div>
+    </div>
+
+    <script type="module">
+      // Import the timeline component
+      import "../sketch-timeline.js";
+
+      // Mock messages
+      function createMockMessage(props = {}) {
+        return {
+          idx: props.idx || 0,
+          type: props.type || "agent",
+          content: props.content || "Hello world",
+          timestamp: props.timestamp || "2023-05-15T12:00:00Z",
+          elapsed: props.elapsed || 1500000000,
+          end_of_turn: props.end_of_turn || false,
+          conversation_id: props.conversation_id || "conv123",
+          tool_calls: props.tool_calls || [],
+          commits: props.commits || [],
+          usage: props.usage,
+          hide_output: props.hide_output || false,
+          ...props,
+        };
+      }
+
+      // Get timeline elements
+      const emptyTimeline = document.getElementById("empty-timeline");
+      const messagesTimeline = document.getElementById("messages-timeline");
+      const thinkingTimeline = document.getElementById("thinking-timeline");
+
+      // Set up messages timeline
+      const messages = [
+        createMockMessage({
+          idx: 0,
+          content: "Hello! I'm a user message.",
+          type: "user",
+          timestamp: "2023-05-15T12:00:00Z",
+        }),
+        createMockMessage({
+          idx: 1,
+          content: "And I'm an agent response with some details.",
+          type: "agent",
+          timestamp: "2023-05-15T12:01:00Z",
+          usage: {
+            input_tokens: 15,
+            output_tokens: 42,
+            cost_usd: 0.001234,
+          },
+        }),
+        createMockMessage({
+          idx: 2,
+          content: "Here's a message with tool calls.",
+          type: "agent",
+          timestamp: "2023-05-15T12:02:00Z",
+          tool_calls: [
+            {
+              name: "bash",
+              input: "echo 'Hello World'",
+              tool_call_id: "call_123",
+              args: '{"command": "echo \'Hello World\'"}',
+              result: "Hello World",
+            },
+          ],
+        }),
+      ];
+
+      messagesTimeline.messages = messages;
+      thinkingTimeline.messages = messages.slice(0, 2);
+
+      // Set initial load complete
+      setTimeout(() => {
+        messagesTimeline.isInitialLoadComplete = true;
+        thinkingTimeline.isInitialLoadComplete = true;
+        messagesTimeline.requestUpdate();
+        thinkingTimeline.requestUpdate();
+      }, 100);
+
+      // Set thinking state
+      thinkingTimeline.llmCalls = 1;
+      thinkingTimeline.toolCalls = ["bash"];
+
+      // Control buttons
+      let messageCount = messages.length;
+      document.getElementById("add-message").addEventListener("click", () => {
+        const newMessage = createMockMessage({
+          idx: messageCount++,
+          content: `New message added at ${new Date().toLocaleTimeString()}`,
+          type: messageCount % 2 === 0 ? "user" : "agent",
+          timestamp: new Date().toISOString(),
+        });
+        messages.push(newMessage);
+        messagesTimeline.messages = [...messages];
+      });
+
+      document
+        .getElementById("toggle-thinking")
+        .addEventListener("click", () => {
+          if (thinkingTimeline.llmCalls > 0) {
+            thinkingTimeline.llmCalls = 0;
+            thinkingTimeline.toolCalls = [];
+          } else {
+            thinkingTimeline.llmCalls = 1;
+            thinkingTimeline.toolCalls = ["bash"];
+          }
+        });
+
+      document
+        .getElementById("clear-messages")
+        .addEventListener("click", () => {
+          messages.length = 0;
+          messagesTimeline.messages = [];
+          messageCount = 0;
+        });
+    </script>
+  </body>
+</html>