| import { css, html, LitElement } from "lit"; |
| import { customElement, property, state } from "lit/decorators.js"; |
| import { unsafeHTML } from "lit/directives/unsafe-html.js"; |
| import { TodoList, TodoItem } from "../types.js"; |
| |
| @customElement("sketch-todo-panel") |
| export class SketchTodoPanel extends LitElement { |
| @property() |
| visible: boolean = false; |
| |
| @state() |
| private todoList: TodoList | null = null; |
| |
| @state() |
| private loading: boolean = false; |
| |
| @state() |
| private error: 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; |
| } |
| |
| .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-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()) { |
| this.todoList = null; |
| } else { |
| this.todoList = JSON.parse(content) as TodoList; |
| } |
| this.loading = false; |
| this.error = ""; |
| } catch (error) { |
| console.error("Failed to parse todo content:", error); |
| this.error = "Failed to parse todo data"; |
| this.todoList = null; |
| this.loading = false; |
| } |
| } |
| |
| |
| |
| private renderTodoItem(item: TodoItem) { |
| const statusIcon = { |
| queued: '⚪', |
| 'in-progress': '🦉', |
| completed: '✅' |
| }[item.status] || '?'; |
| |
| return html` |
| <div class="todo-item ${item.status}"> |
| <div class="todo-status-icon">${statusIcon}</div> |
| <div class="todo-main"> |
| <div class="todo-content-text">${item.task}</div> |
| |
| </div> |
| </div> |
| `; |
| } |
| |
| render() { |
| if (!this.visible) { |
| return html``; |
| } |
| |
| const todoIcon = html` |
| <svg class="todo-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
| <path d="M9 11l3 3L22 4"></path> |
| <path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"></path> |
| </svg> |
| `; |
| |
| let contentElement; |
| if (this.loading) { |
| contentElement = html` |
| <div class="todo-content loading"> |
| <div class="spinner"></div> |
| Loading todos... |
| </div> |
| `; |
| } else if (this.error) { |
| contentElement = html` |
| <div class="todo-content error"> |
| Error: ${this.error} |
| </div> |
| `; |
| } else if (!this.todoList || !this.todoList.items || this.todoList.items.length === 0) { |
| contentElement = html` |
| <div class="todo-content empty"> |
| No todos available |
| </div> |
| `; |
| } else { |
| const totalCount = this.todoList.items.length; |
| const completedCount = this.todoList.items.filter(item => item.status === 'completed').length; |
| const inProgressCount = this.todoList.items.filter(item => item.status === 'in-progress').length; |
| |
| contentElement = html` |
| <div class="todo-header"> |
| <div class="todo-header-text"> |
| ${todoIcon} |
| <span>Sketching...</span> |
| <span class="todo-count">${completedCount}/${totalCount}</span> |
| </div> |
| </div> |
| <div class="todo-content"> |
| ${this.todoList.items.map(item => this.renderTodoItem(item))} |
| </div> |
| `; |
| } |
| |
| return html` |
| ${contentElement} |
| `; |
| } |
| } |
| |
| declare global { |
| interface HTMLElementTagNameMap { |
| "sketch-todo-panel": SketchTodoPanel; |
| } |
| } |