webui: convert sketch-todo-panel to SketchTailwindElement with comprehensive test suite

Convert sketch-todo-panel component from LitElement with CSS-in-JS to SketchTailwindElement
inheritance using Tailwind utility classes, and add complete testing infrastructure with
TypeScript demo module and comprehensive test coverage.

Component Conversion:
- Replace LitElement with SketchTailwindElement inheritance to disable shadow DOM
- Remove 200+ lines of CSS-in-JS styles in favor of Tailwind utility classes
- Convert all styling to Tailwind class compositions while maintaining visual parity
- Add inline fadeIn animation using <style> tag following established patterns
- Preserve all existing functionality: todo rendering, comment system, loading states

CSS-to-Tailwind Mapping:
- Main container: flex flex-col h-full bg-transparent overflow-hidden
- Header section: py-2 px-3 border-b border-gray-300 bg-gray-100 font-semibold text-xs
- Content area: flex-1 overflow-y-auto p-2 pb-5 text-xs leading-relaxed min-h-0
- Todo items: flex items-start p-2 mb-1.5 rounded bg-white border border-gray-300 gap-2
- Loading state: animate-spin with proper Tailwind spinner classes
- Comment modal: fixed inset-0 bg-black bg-opacity-30 z-[10000] with centered content
- Status icons: ✅ completed, 🦉 in-progress, ⚪ queued with proper sizing
- Interactive buttons: hover states and transitions using Tailwind utility classes

Test Infrastructure:
- Create sketch-todo-panel.test.ts with 14 comprehensive test cases
- Test initialization, visibility, state management (loading/error/empty states)
- Test todo rendering: status icons, task descriptions, progress counts
- Test comment system: button visibility, modal interactions, event dispatch
- Test error handling: invalid JSON parsing, empty content scenarios
- Test Tailwind integration: proper class usage, shadow DOM disabled
- Test scrollable interface: large todo lists render and scroll correctly
- Use @sand4rt/experimental-ct-web framework following established patterns
- Include helper functions for mock TodoItem creation and test utilities

Demo Module Integration:
- Create sketch-todo-panel.demo.ts following established TypeScript demo pattern
- Add comprehensive demo scenarios: basic usage, loading/error/empty states
- Include large scrollable list demonstration with multiple todo items
- Add interactive comment functionality testing with event logging
- Add sketch-todo-panel to demo-runner.ts knownComponents registry
- Demonstrate all component states and user interactions comprehensively

TypeScript Compatibility:
- Fix property access for private @state() properties using component.evaluate()
- Use type assertions for addEventListener/removeEventListener on SketchTailwindElement
- Address interface compatibility issues with proper TypeScript patterns
- Remove unused imports and helper functions to maintain ESLint compliance
- Follow established patterns from other SketchTailwindElement components

Files Modified:
- sketch/webui/src/web-components/sketch-todo-panel.ts: TailwindElement conversion with complete CSS removal
- sketch/webui/src/web-components/demo/demo-framework/demo-runner.ts: Added component to registry

Files Added:
- sketch/webui/src/web-components/sketch-todo-panel.test.ts: Comprehensive test suite with 14 test cases
- sketch/webui/src/web-components/demo/sketch-todo-panel.demo.ts: Interactive TypeScript demo module

The conversion maintains complete functional parity while enabling consistent
Tailwind-based styling, comprehensive test coverage, and improved development
experience through integrated demo infrastructure.

Co-Authored-By: sketch <hello@sketch.dev>
Change-ID: seada6841c7c375e5k
diff --git a/webui/src/web-components/sketch-todo-panel.test.ts b/webui/src/web-components/sketch-todo-panel.test.ts
new file mode 100644
index 0000000..50131dd
--- /dev/null
+++ b/webui/src/web-components/sketch-todo-panel.test.ts
@@ -0,0 +1,345 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { test, expect } from "@sand4rt/experimental-ct-web";
+import { SketchTodoPanel } from "./sketch-todo-panel";
+import { TodoItem } from "../types";
+
+// Helper function to create mock todo items
+function createMockTodoItem(props: Partial<TodoItem> = {}): TodoItem {
+  return {
+    id: props.id || "task-1",
+    status: props.status || "queued",
+    task: props.task || "Sample task description",
+    ...props,
+  };
+}
+
+test("initializes with default properties", async ({ mount }) => {
+  const component = await mount(SketchTodoPanel, {});
+
+  // Check default properties
+  const visible = await component.evaluate((el: SketchTodoPanel) => el.visible);
+  expect(visible).toBe(false);
+
+  // When not visible, component should not render content
+  const content = await component.textContent();
+  expect(content?.trim()).toBe("");
+});
+
+test("displays empty state when visible but no data", async ({ mount }) => {
+  const component = await mount(SketchTodoPanel, {
+    props: {
+      visible: true,
+    },
+  });
+
+  // Should show empty state message
+  await expect(component).toContainText("No todos available");
+});
+
+test("displays loading state correctly", async ({ mount }) => {
+  const component = await mount(SketchTodoPanel, {
+    props: {
+      visible: true,
+    },
+  });
+
+  // Set loading state through component method
+  await component.evaluate((el: SketchTodoPanel) => {
+    (el as any).loading = true;
+  });
+
+  // Should show loading message and spinner
+  await expect(component).toContainText("Loading todos...");
+  // Check for spinner element (it has animate-spin class)
+  await expect(component.locator(".animate-spin")).toBeVisible();
+});
+
+test("displays error state correctly", async ({ mount }) => {
+  const component = await mount(SketchTodoPanel, {
+    props: {
+      visible: true,
+    },
+  });
+
+  // Set error state through component method
+  await component.evaluate((el: SketchTodoPanel) => {
+    (el as any).error = "Failed to load todo data";
+  });
+
+  // Should show error message
+  await expect(component).toContainText("Error: Failed to load todo data");
+  // Error text should have red color
+  await expect(component.locator(".text-red-600")).toBeVisible();
+});
+
+test("renders todo items correctly", async ({ mount }) => {
+  const mockTodos = [
+    createMockTodoItem({
+      id: "task-1",
+      status: "completed",
+      task: "Complete the first task",
+    }),
+    createMockTodoItem({
+      id: "task-2",
+      status: "in-progress",
+      task: "Work on the second task",
+    }),
+    createMockTodoItem({
+      id: "task-3",
+      status: "queued",
+      task: "Start the third task",
+    }),
+  ];
+
+  const component = await mount(SketchTodoPanel, {
+    props: {
+      visible: true,
+    },
+  });
+
+  // Update component with todo data
+  await component.evaluate((el: SketchTodoPanel, todos) => {
+    el.updateTodoContent(JSON.stringify({ items: todos }));
+  }, mockTodos);
+
+  // Check that all tasks are rendered
+  await expect(component).toContainText("Complete the first task");
+  await expect(component).toContainText("Work on the second task");
+  await expect(component).toContainText("Start the third task");
+
+  // Check that status icons are present (emojis)
+  await expect(component).toContainText("✅"); // completed
+  await expect(component).toContainText("🦉"); // in-progress
+  await expect(component).toContainText("⚪"); // queued
+});
+
+test("displays correct todo count in header", async ({ mount }) => {
+  const mockTodos = [
+    createMockTodoItem({ id: "task-1", status: "completed" }),
+    createMockTodoItem({ id: "task-2", status: "completed" }),
+    createMockTodoItem({ id: "task-3", status: "in-progress" }),
+    createMockTodoItem({ id: "task-4", status: "queued" }),
+  ];
+
+  const component = await mount(SketchTodoPanel, {
+    props: {
+      visible: true,
+    },
+  });
+
+  await component.evaluate((el: SketchTodoPanel, todos) => {
+    el.updateTodoContent(JSON.stringify({ items: todos }));
+  }, mockTodos);
+
+  // Should show "2/4" (2 completed out of 4 total)
+  await expect(component).toContainText("2/4");
+  await expect(component).toContainText("Sketching...");
+});
+
+test("shows comment button only for non-completed items", async ({ mount }) => {
+  const mockTodos = [
+    createMockTodoItem({ id: "task-1", status: "completed" }),
+    createMockTodoItem({ id: "task-2", status: "in-progress" }),
+    createMockTodoItem({ id: "task-3", status: "queued" }),
+  ];
+
+  const component = await mount(SketchTodoPanel, {
+    props: {
+      visible: true,
+    },
+  });
+
+  await component.evaluate((el: SketchTodoPanel, todos) => {
+    el.updateTodoContent(JSON.stringify({ items: todos }));
+  }, mockTodos);
+
+  // Comment buttons (💬) should only appear for in-progress and queued items
+  const commentButtons = component.locator('button[title*="Add comment"]');
+  await expect(commentButtons).toHaveCount(2); // Only for in-progress and queued
+});
+
+test("opens comment box when comment button is clicked", async ({ mount }) => {
+  const mockTodos = [
+    createMockTodoItem({
+      id: "task-1",
+      status: "in-progress",
+      task: "Work on important task",
+    }),
+  ];
+
+  const component = await mount(SketchTodoPanel, {
+    props: {
+      visible: true,
+    },
+  });
+
+  await component.evaluate((el: SketchTodoPanel, todos) => {
+    el.updateTodoContent(JSON.stringify({ items: todos }));
+  }, mockTodos);
+
+  // Click the comment button
+  await component.locator('button[title*="Add comment"]').click();
+
+  // Comment overlay should be visible
+  await expect(component.locator(".fixed.inset-0")).toBeVisible();
+  await expect(component).toContainText("Comment on TODO Item");
+  await expect(component).toContainText("Status: In Progress");
+  await expect(component).toContainText("Work on important task");
+});
+
+test("closes comment box when cancel is clicked", async ({ mount }) => {
+  const mockTodos = [createMockTodoItem({ id: "task-1", status: "queued" })];
+
+  const component = await mount(SketchTodoPanel, {
+    props: {
+      visible: true,
+    },
+  });
+
+  await component.evaluate((el: SketchTodoPanel, todos) => {
+    el.updateTodoContent(JSON.stringify({ items: todos }));
+  }, mockTodos);
+
+  // Open comment box
+  await component.locator('button[title*="Add comment"]').click();
+  await expect(component.locator(".fixed.inset-0")).toBeVisible();
+
+  // Click cancel
+  await component.locator('button:has-text("Cancel")').click();
+
+  // Comment overlay should be hidden
+  await expect(component.locator(".fixed.inset-0")).not.toBeVisible();
+});
+
+test("dispatches todo-comment event when comment is submitted", async ({
+  mount,
+}) => {
+  const mockTodos = [
+    createMockTodoItem({
+      id: "task-1",
+      status: "in-progress",
+      task: "Important task",
+    }),
+  ];
+
+  const component = await mount(SketchTodoPanel, {
+    props: {
+      visible: true,
+    },
+  });
+
+  await component.evaluate((el: SketchTodoPanel, todos) => {
+    el.updateTodoContent(JSON.stringify({ items: todos }));
+  }, mockTodos);
+
+  // Set up event listener
+  const eventPromise = component.evaluate((el: SketchTodoPanel) => {
+    return new Promise((resolve) => {
+      (el as any).addEventListener(
+        "todo-comment",
+        (e: any) => {
+          resolve(e.detail.comment);
+        },
+        { once: true },
+      );
+    });
+  });
+
+  // Open comment box
+  await component.locator('button[title*="Add comment"]').click();
+
+  // Fill in comment
+  await component
+    .locator('textarea[placeholder*="Type your comment"]')
+    .fill("This is a test comment");
+
+  // Submit comment
+  await component.locator('button:has-text("Add Comment")').click();
+
+  // Wait for event and check content
+  const eventDetail = await eventPromise;
+  expect(eventDetail).toContain("This is a test comment");
+  expect(eventDetail).toContain("Important task");
+  expect(eventDetail).toContain("In Progress");
+
+  // Comment box should be closed after submission
+  await expect(component.locator(".fixed.inset-0")).not.toBeVisible();
+});
+
+test("handles invalid JSON gracefully", async ({ mount }) => {
+  const component = await mount(SketchTodoPanel, {
+    props: {
+      visible: true,
+    },
+  });
+
+  // Update with invalid JSON
+  await component.evaluate((el: SketchTodoPanel) => {
+    el.updateTodoContent("{invalid json}");
+  });
+
+  // Should show error state
+  await expect(component).toContainText("Error: Failed to parse todo data");
+});
+
+test("handles empty content gracefully", async ({ mount }) => {
+  const component = await mount(SketchTodoPanel, {
+    props: {
+      visible: true,
+    },
+  });
+
+  // Update with empty content
+  await component.evaluate((el: SketchTodoPanel) => {
+    el.updateTodoContent("");
+  });
+
+  // Should show empty state
+  await expect(component).toContainText("No todos available");
+});
+
+test("renders with proper Tailwind classes", async ({ mount }) => {
+  const component = await mount(SketchTodoPanel, {
+    props: {
+      visible: true,
+    },
+  });
+
+  // Check main container has correct Tailwind classes
+  const container = component.locator(".flex.flex-col.h-full");
+  await expect(container).toBeVisible();
+
+  // Check that it's using Tailwind instead of shadow DOM styling
+  const shadowRoot = await component.evaluate((el) => el.shadowRoot);
+  expect(shadowRoot).toBeNull(); // SketchTailwindElement disables shadow DOM
+});
+
+test("displays todos in scrollable container", async ({ mount }) => {
+  const mockTodos = Array.from({ length: 10 }, (_, i) =>
+    createMockTodoItem({
+      id: `task-${i + 1}`,
+      status:
+        i % 3 === 0 ? "completed" : i % 3 === 1 ? "in-progress" : "queued",
+      task: `Task number ${i + 1} with some description text`,
+    }),
+  );
+
+  const component = await mount(SketchTodoPanel, {
+    props: {
+      visible: true,
+    },
+  });
+
+  await component.evaluate((el: SketchTodoPanel, todos) => {
+    el.updateTodoContent(JSON.stringify({ items: todos }));
+  }, mockTodos);
+
+  // Check that scrollable container exists
+  const scrollContainer = component.locator(".overflow-y-auto");
+  await expect(scrollContainer).toBeVisible();
+
+  // All tasks should be rendered
+  for (let i = 1; i <= 10; i++) {
+    await expect(component).toContainText(`Task number ${i}`);
+  }
+});