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;
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}`);
+ }
+});
diff --git a/webui/src/web-components/sketch-todo-panel.ts b/webui/src/web-components/sketch-todo-panel.ts
index 7760faf..e76de46 100644
--- a/webui/src/web-components/sketch-todo-panel.ts
+++ b/webui/src/web-components/sketch-todo-panel.ts
@@ -1,10 +1,10 @@
-import { css, html, LitElement } from "lit";
+import { html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
-// import { unsafeHTML } from "lit/directives/unsafe-html.js"; // Unused import
import { TodoList, TodoItem } from "../types.js";
+import { SketchTailwindElement } from "./sketch-tailwind-element.js";
@customElement("sketch-todo-panel")
-export class SketchTodoPanel extends LitElement {
+export class SketchTodoPanel extends SketchTailwindElement {
@property()
visible: boolean = false;
@@ -26,322 +26,6 @@
@state()
private commentText: string = "";
- static styles = css`
- :host {
- display: flex;
- flex-direction: column;
- height: 100%;
- background-color: transparent; /* Let parent handle background */
- overflow: hidden; /* Ensure proper clipping */
- }
-
- .todo-header {
- padding: 8px 12px;
- border-bottom: 1px solid #e0e0e0;
- background-color: #f5f5f5;
- font-weight: 600;
- font-size: 13px;
- color: #333;
- display: flex;
- align-items: center;
- gap: 6px;
- }
-
- .todo-icon {
- width: 14px;
- height: 14px;
- color: #666;
- }
-
- .todo-content {
- flex: 1;
- overflow-y: auto;
- padding: 8px;
- padding-bottom: 20px; /* Extra bottom padding for better scrolling */
- font-family:
- system-ui,
- -apple-system,
- BlinkMacSystemFont,
- "Segoe UI",
- sans-serif;
- font-size: 12px;
- line-height: 1.4;
- /* Ensure scrollbar is always accessible */
- min-height: 0;
- }
-
- .todo-content.loading {
- display: flex;
- align-items: center;
- justify-content: center;
- color: #666;
- }
-
- .todo-content.error {
- color: #d32f2f;
- display: flex;
- align-items: center;
- justify-content: center;
- }
-
- .todo-content.empty {
- color: #999;
- font-style: italic;
- display: flex;
- align-items: center;
- justify-content: center;
- }
-
- /* Todo item styling */
- .todo-item {
- display: flex;
- align-items: flex-start;
- padding: 8px;
- margin-bottom: 6px;
- border-radius: 4px;
- background-color: #fff;
- border: 1px solid #e0e0e0;
- gap: 8px;
- min-height: 24px; /* Ensure consistent height */
- }
-
- .todo-item.queued {
- border-left: 3px solid #e0e0e0;
- }
-
- .todo-item.in-progress {
- border-left: 3px solid #e0e0e0;
- }
-
- .todo-item.completed {
- border-left: 3px solid #e0e0e0;
- }
-
- .todo-status-icon {
- font-size: 14px;
- margin-top: 1px;
- flex-shrink: 0;
- }
-
- .todo-main {
- flex: 1;
- min-width: 0;
- }
-
- .todo-content-text {
- font-size: 12px;
- line-height: 1.3;
- color: #333;
- word-wrap: break-word;
- }
-
- .todo-item-content {
- display: flex;
- align-items: flex-start;
- justify-content: space-between;
- width: 100%;
- min-height: 20px; /* Ensure consistent height */
- }
-
- .todo-text-section {
- flex: 1;
- min-width: 0;
- padding-right: 8px; /* Space between text and button column */
- }
-
- .todo-actions-column {
- flex-shrink: 0;
- display: flex;
- align-items: flex-start;
- width: 24px; /* Fixed width for button column */
- justify-content: center;
- }
-
- .comment-button {
- background: none;
- border: none;
- cursor: pointer;
- font-size: 14px;
- padding: 2px;
- color: #666;
- opacity: 0.7;
- transition: opacity 0.2s ease;
- width: 20px;
- height: 20px;
- display: flex;
- align-items: center;
- justify-content: center;
- }
-
- .comment-button:hover {
- opacity: 1;
- background-color: rgba(0, 0, 0, 0.05);
- border-radius: 3px;
- }
-
- /* Comment box overlay */
- .comment-overlay {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background-color: rgba(0, 0, 0, 0.3);
- z-index: 10000;
- display: flex;
- align-items: center;
- justify-content: center;
- animation: fadeIn 0.2s ease-in-out;
- }
-
- .comment-box {
- background-color: white;
- border: 1px solid #ddd;
- border-radius: 6px;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
- padding: 16px;
- width: 400px;
- max-width: 90vw;
- max-height: 80vh;
- overflow-y: auto;
- }
-
- .comment-box-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 12px;
- }
-
- .comment-box-header h3 {
- margin: 0;
- font-size: 14px;
- font-weight: 500;
- }
-
- .close-button {
- background: none;
- border: none;
- cursor: pointer;
- font-size: 18px;
- color: #666;
- padding: 2px 6px;
- }
-
- .close-button:hover {
- color: #333;
- }
-
- .todo-context {
- background-color: #f8f9fa;
- border: 1px solid #e9ecef;
- border-radius: 4px;
- padding: 8px;
- margin-bottom: 12px;
- font-size: 12px;
- }
-
- .todo-context-status {
- font-weight: 500;
- color: #666;
- margin-bottom: 4px;
- }
-
- .todo-context-task {
- color: #333;
- }
-
- .comment-textarea {
- width: 100%;
- min-height: 80px;
- padding: 8px;
- border: 1px solid #ddd;
- border-radius: 4px;
- resize: vertical;
- font-family: inherit;
- font-size: 12px;
- margin-bottom: 12px;
- box-sizing: border-box;
- }
-
- .comment-actions {
- display: flex;
- justify-content: flex-end;
- gap: 8px;
- }
-
- .comment-actions button {
- padding: 6px 12px;
- border-radius: 4px;
- cursor: pointer;
- font-size: 12px;
- }
-
- .cancel-button {
- background-color: transparent;
- border: 1px solid #ddd;
- color: #666;
- }
-
- .cancel-button:hover {
- background-color: #f5f5f5;
- }
-
- .submit-button {
- background-color: #4285f4;
- color: white;
- border: none;
- }
-
- .submit-button:hover {
- background-color: #3367d6;
- }
-
- @keyframes fadeIn {
- from {
- opacity: 0;
- }
- to {
- opacity: 1;
- }
- }
-
- .todo-header-text {
- display: flex;
- align-items: center;
- gap: 6px;
- }
-
- .todo-count {
- background-color: #e0e0e0;
- color: #666;
- padding: 2px 6px;
- border-radius: 10px;
- font-size: 10px;
- font-weight: normal;
- }
-
- /* Loading spinner */
- .spinner {
- width: 20px;
- height: 20px;
- border: 2px solid #f3f3f3;
- border-top: 2px solid #3498db;
- border-radius: 50%;
- animation: spin 1s linear infinite;
- margin-right: 8px;
- }
-
- @keyframes spin {
- 0% {
- transform: rotate(0deg);
- }
- 100% {
- transform: rotate(360deg);
- }
- }
- `;
-
updateTodoContent(content: string) {
try {
if (!content.trim()) {
@@ -371,17 +55,21 @@
const showCommentButton = item.status !== "completed";
return html`
- <div class="todo-item ${item.status}">
- <div class="todo-status-icon">${statusIcon}</div>
- <div class="todo-item-content">
- <div class="todo-text-section">
- <div class="todo-content-text">${item.task}</div>
+ <div
+ class="flex items-start p-2 mb-1.5 rounded bg-white border border-gray-300 gap-2 min-h-6 border-l-[3px] border-l-gray-300"
+ >
+ <div class="text-sm mt-0.5 flex-shrink-0">${statusIcon}</div>
+ <div class="flex items-start justify-between w-full min-h-5">
+ <div class="flex-1 min-w-0 pr-2">
+ <div class="text-xs leading-snug text-gray-800 break-words">
+ ${item.task}
+ </div>
</div>
- <div class="todo-actions-column">
+ <div class="flex-shrink-0 flex items-start w-6 justify-center">
${showCommentButton
? html`
<button
- class="comment-button"
+ class="bg-transparent border-none cursor-pointer text-sm p-0.5 text-gray-500 opacity-70 transition-opacity duration-200 w-5 h-5 flex items-center justify-center hover:opacity-100 hover:bg-black/5 hover:bg-opacity-5 hover:rounded-sm"
@click="${() => this.openCommentBox(item)}"
title="Add comment about this TODO item"
>
@@ -402,7 +90,7 @@
const todoIcon = html`
<svg
- class="todo-icon"
+ class="w-3.5 h-3.5 text-gray-500"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
@@ -419,14 +107,22 @@
let contentElement;
if (this.loading) {
contentElement = html`
- <div class="todo-content loading">
- <div class="spinner"></div>
+ <div
+ class="flex-1 overflow-y-auto p-2 pb-5 text-xs leading-relaxed min-h-0 flex items-center justify-center text-gray-500"
+ >
+ <div
+ class="w-5 h-5 border-2 border-gray-200 border-t-blue-500 rounded-full animate-spin mr-2"
+ ></div>
Loading todos...
</div>
`;
} else if (this.error) {
contentElement = html`
- <div class="todo-content error">Error: ${this.error}</div>
+ <div
+ class="flex-1 overflow-y-auto p-2 pb-5 text-xs leading-relaxed min-h-0 text-red-600 flex items-center justify-center"
+ >
+ Error: ${this.error}
+ </div>
`;
} else if (
!this.todoList ||
@@ -434,7 +130,11 @@
this.todoList.items.length === 0
) {
contentElement = html`
- <div class="todo-content empty">No todos available</div>
+ <div
+ class="flex-1 overflow-y-auto p-2 pb-5 text-xs leading-relaxed min-h-0 text-gray-400 italic flex items-center justify-center"
+ >
+ No todos available
+ </div>
`;
} else {
const totalCount = this.todoList.items.length;
@@ -446,21 +146,31 @@
).length;
contentElement = html`
- <div class="todo-header">
- <div class="todo-header-text">
+ <div
+ class="py-2 px-3 border-b border-gray-300 bg-gray-100 font-semibold text-xs text-gray-800 flex items-center gap-1.5"
+ >
+ <div class="flex items-center gap-1.5">
${todoIcon}
<span>Sketching...</span>
- <span class="todo-count">${completedCount}/${totalCount}</span>
+ <span
+ class="bg-gray-300 text-gray-500 px-1.5 py-0.5 rounded-full text-xs font-normal"
+ >${completedCount}/${totalCount}</span
+ >
</div>
</div>
- <div class="todo-content">
+ <div
+ class="flex-1 overflow-y-auto p-2 pb-5 text-xs leading-relaxed min-h-0"
+ >
${this.todoList.items.map((item) => this.renderTodoItem(item))}
</div>
`;
}
return html`
- ${contentElement} ${this.showCommentBox ? this.renderCommentBox() : ""}
+ <div class="flex flex-col h-full bg-transparent overflow-hidden">
+ ${contentElement}
+ </div>
+ ${this.showCommentBox ? this.renderCommentBox() : ""}
`;
}
@@ -475,32 +185,64 @@
}[this.commentingItem.status] || this.commentingItem.status;
return html`
- <div class="comment-overlay" @click="${this.handleOverlayClick}">
- <div class="comment-box" @click="${this.stopPropagation}">
- <div class="comment-box-header">
- <h3>Comment on TODO Item</h3>
- <button class="close-button" @click="${this.closeCommentBox}">
+ <style>
+ @keyframes fadeIn {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+ }
+ .animate-fade-in {
+ animation: fadeIn 0.2s ease-in-out;
+ }
+ </style>
+ <div
+ class="fixed inset-0 bg-black/30 z-[10000] flex items-center justify-center animate-fade-in"
+ @click="${this.handleOverlayClick}"
+ >
+ <div
+ class="bg-white border border-gray-300 rounded-md shadow-lg p-4 w-96 max-w-[90vw] max-h-[80vh] overflow-y-auto"
+ @click="${this.stopPropagation}"
+ >
+ <div class="flex justify-between items-center mb-3">
+ <h3 class="m-0 text-sm font-medium">Comment on TODO Item</h3>
+ <button
+ class="bg-transparent border-none cursor-pointer text-lg text-gray-500 px-1.5 py-0.5 hover:text-gray-800"
+ @click="${this.closeCommentBox}"
+ >
×
</button>
</div>
- <div class="todo-context">
- <div class="todo-context-status">Status: ${statusText}</div>
- <div class="todo-context-task">${this.commentingItem.task}</div>
+ <div
+ class="bg-gray-50 border border-gray-200 rounded p-2 mb-3 text-xs"
+ >
+ <div class="font-medium text-gray-500 mb-1">
+ Status: ${statusText}
+ </div>
+ <div class="text-gray-800">${this.commentingItem.task}</div>
</div>
<textarea
- class="comment-textarea"
+ class="w-full min-h-20 p-2 border border-gray-300 rounded resize-y text-xs mb-3 box-border"
placeholder="Type your comment about this TODO item..."
.value="${this.commentText}"
@input="${this.handleCommentInput}"
></textarea>
- <div class="comment-actions">
- <button class="cancel-button" @click="${this.closeCommentBox}">
+ <div class="flex justify-end gap-2">
+ <button
+ class="px-3 py-1.5 rounded cursor-pointer text-xs bg-transparent border border-gray-300 text-gray-500 hover:bg-gray-100"
+ @click="${this.closeCommentBox}"
+ >
Cancel
</button>
- <button class="submit-button" @click="${this.submitComment}">
+ <button
+ class="px-3 py-1.5 rounded cursor-pointer text-xs bg-blue-500 text-white border-none hover:bg-blue-600"
+ @click="${this.submitComment}"
+ >
Add Comment
</button>
</div>