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/demo/demo-framework/demo-runner.ts b/webui/src/web-components/demo/demo-framework/demo-runner.ts
index b2b947a..c4c6362 100644
--- a/webui/src/web-components/demo/demo-framework/demo-runner.ts
+++ b/webui/src/web-components/demo/demo-framework/demo-runner.ts
@@ -96,6 +96,7 @@
"sketch-container-status",
"sketch-timeline",
"sketch-timeline-message",
+ "sketch-todo-panel",
"sketch-tool-calls",
"sketch-view-mode-select",
];
diff --git a/webui/src/web-components/demo/sketch-todo-panel.demo.ts b/webui/src/web-components/demo/sketch-todo-panel.demo.ts
new file mode 100644
index 0000000..a65cb82
--- /dev/null
+++ b/webui/src/web-components/demo/sketch-todo-panel.demo.ts
@@ -0,0 +1,218 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+/**
+ * Demo module for sketch-todo-panel component
+ */
+
+import { DemoModule } from "./demo-framework/types";
+import { demoUtils } from "./demo-fixtures/index";
+
+// Sample todo data
+const sampleTodoList = {
+ items: [
+ {
+ id: "task-1",
+ status: "completed" as const,
+ task: "Convert sketch-todo-panel.ts to inherit from SketchTailwindElement",
+ },
+ {
+ id: "task-2",
+ status: "in-progress" as const,
+ task: "Test the converted element to ensure it works correctly",
+ },
+ {
+ id: "task-3",
+ status: "queued" as const,
+ task: "Add unit tests for the todo panel component",
+ },
+ {
+ id: "task-4",
+ status: "queued" as const,
+ task: "Update documentation with new implementation details",
+ },
+ ],
+};
+
+const largeTodoList = {
+ items: [
+ {
+ id: "task-1",
+ status: "completed" as const,
+ task: "Implement authentication system with JWT tokens",
+ },
+ {
+ id: "task-2",
+ status: "completed" as const,
+ task: "Set up database migrations and schema",
+ },
+ {
+ id: "task-3",
+ status: "in-progress" as const,
+ task: "Build responsive dashboard with real-time updates and complex data visualization components",
+ },
+ {
+ id: "task-4",
+ status: "queued" as const,
+ task: "Add file upload functionality with drag and drop support",
+ },
+ {
+ id: "task-5",
+ status: "queued" as const,
+ task: "Implement comprehensive test suite including unit, integration, and end-to-end tests",
+ },
+ {
+ id: "task-6",
+ status: "queued" as const,
+ task: "Deploy to production environment with monitoring and logging",
+ },
+ {
+ id: "task-7",
+ status: "queued" as const,
+ task: "Create user documentation and API guides",
+ },
+ ],
+};
+
+const demo: DemoModule = {
+ title: "Todo Panel Demo",
+ description:
+ "Interactive todo list panel showing task progress and allowing comments",
+ imports: ["../sketch-todo-panel"],
+ styles: ["/dist/tailwind.css"],
+
+ setup: async (container: HTMLElement) => {
+ // Create demo sections
+ const basicSection = demoUtils.createDemoSection(
+ "Basic Todo Panel",
+ "Shows a typical todo list with different task statuses",
+ );
+
+ const statesSection = demoUtils.createDemoSection(
+ "Different States",
+ "Loading, error, and empty states",
+ );
+
+ const largeListSection = demoUtils.createDemoSection(
+ "Large Todo List",
+ "Scrollable list with longer task descriptions",
+ );
+
+ // Basic todo panel with sample data
+ const basicPanel = document.createElement("sketch-todo-panel") as any;
+ basicPanel.id = "basic-panel";
+ basicPanel.visible = true;
+ basicPanel.style.cssText =
+ "height: 300px; border: 1px solid #e0e0e0; display: block;";
+
+ // Set the data after a short delay to show it populating
+ setTimeout(() => {
+ basicPanel.updateTodoContent(JSON.stringify(sampleTodoList));
+ }, 100);
+
+ // Loading state panel
+ const loadingPanel = document.createElement("sketch-todo-panel") as any;
+ loadingPanel.id = "loading-panel";
+ loadingPanel.visible = true;
+ loadingPanel.loading = true;
+ loadingPanel.style.cssText =
+ "height: 150px; border: 1px solid #e0e0e0; display: block; margin-right: 10px; flex: 1;";
+
+ // Error state panel
+ const errorPanel = document.createElement("sketch-todo-panel") as any;
+ errorPanel.id = "error-panel";
+ errorPanel.visible = true;
+ errorPanel.error = "Failed to load todo data";
+ errorPanel.style.cssText =
+ "height: 150px; border: 1px solid #e0e0e0; display: block; margin-right: 10px; flex: 1;";
+
+ // Empty state panel
+ const emptyPanel = document.createElement("sketch-todo-panel") as any;
+ emptyPanel.id = "empty-panel";
+ emptyPanel.visible = true;
+ emptyPanel.updateTodoContent("");
+ emptyPanel.style.cssText =
+ "height: 150px; border: 1px solid #e0e0e0; display: block; flex: 1;";
+
+ // Large list panel
+ const largePanel = document.createElement("sketch-todo-panel") as any;
+ largePanel.id = "large-panel";
+ largePanel.visible = true;
+ largePanel.style.cssText =
+ "height: 400px; border: 1px solid #e0e0e0; display: block;";
+ largePanel.updateTodoContent(JSON.stringify(largeTodoList));
+
+ // Create states container
+ const statesContainer = document.createElement("div");
+ statesContainer.style.cssText = "display: flex; gap: 10px; margin: 10px 0;";
+ statesContainer.appendChild(loadingPanel);
+ statesContainer.appendChild(errorPanel);
+ statesContainer.appendChild(emptyPanel);
+
+ // Add state labels
+ const loadingLabel = document.createElement("div");
+ loadingLabel.textContent = "Loading State";
+ loadingLabel.style.cssText =
+ "font-weight: bold; margin-bottom: 8px; flex: 1; text-align: center;";
+
+ const errorLabel = document.createElement("div");
+ errorLabel.textContent = "Error State";
+ errorLabel.style.cssText =
+ "font-weight: bold; margin-bottom: 8px; flex: 1; text-align: center;";
+
+ const emptyLabel = document.createElement("div");
+ emptyLabel.textContent = "Empty State";
+ emptyLabel.style.cssText =
+ "font-weight: bold; margin-bottom: 8px; flex: 1; text-align: center;";
+
+ const labelsContainer = document.createElement("div");
+ labelsContainer.style.cssText =
+ "display: flex; gap: 10px; margin-bottom: 5px;";
+ labelsContainer.appendChild(loadingLabel);
+ labelsContainer.appendChild(errorLabel);
+ labelsContainer.appendChild(emptyLabel);
+
+ // Add event listener for comment events
+ const eventLog = document.createElement("div");
+ eventLog.style.cssText =
+ "margin-top: 20px; padding: 10px; background: #f5f5f5; border-radius: 4px; font-family: monospace; font-size: 12px;";
+ eventLog.innerHTML =
+ "<strong>Event Log:</strong> (try clicking the 💬 button on in-progress or queued items)<br>";
+
+ const logEvent = (message: string) => {
+ const timestamp = new Date().toLocaleTimeString();
+ eventLog.innerHTML += `<div>[${timestamp}] ${message}</div>`;
+ eventLog.scrollTop = eventLog.scrollHeight;
+ };
+
+ // Listen for todo comment events
+ [basicPanel, largePanel].forEach((panel) => {
+ panel.addEventListener("todo-comment", (event: any) => {
+ logEvent(
+ `Todo comment received: "${event.detail.comment.substring(0, 50)}..."`,
+ );
+ });
+ });
+
+ // Assemble the demo
+ basicSection.appendChild(basicPanel);
+
+ statesSection.appendChild(labelsContainer);
+ statesSection.appendChild(statesContainer);
+
+ largeListSection.appendChild(largePanel);
+
+ container.appendChild(basicSection);
+ container.appendChild(statesSection);
+ container.appendChild(largeListSection);
+ container.appendChild(eventLog);
+ },
+
+ cleanup: () => {
+ // Remove any event listeners if needed
+ const panels = document.querySelectorAll("sketch-todo-panel");
+ panels.forEach((panel) => {
+ (panel as any).removeEventListener("todo-comment", () => {});
+ });
+ },
+};
+
+export default demo;