blob: d8e34f0634d6ca87c3e5374a16d9df82d1a3ef6f [file] [log] [blame]
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -07001import { css, html, LitElement } from "lit";
2import { customElement, property, state } from "lit/decorators.js";
3import { unsafeHTML } from "lit/directives/unsafe-html.js";
4import { TodoList, TodoItem } from "../types.js";
5
6@customElement("sketch-todo-panel")
7export class SketchTodoPanel extends LitElement {
8 @property()
9 visible: boolean = false;
10
11 @state()
12 private todoList: TodoList | null = null;
13
14 @state()
15 private loading: boolean = false;
16
17 @state()
18 private error: string = "";
19
20 static styles = css`
21 :host {
22 display: flex;
23 flex-direction: column;
24 height: 100%;
25 background-color: transparent; /* Let parent handle background */
26 overflow: hidden; /* Ensure proper clipping */
27 }
28
29 .todo-header {
30 padding: 8px 12px;
31 border-bottom: 1px solid #e0e0e0;
32 background-color: #f5f5f5;
33 font-weight: 600;
34 font-size: 13px;
35 color: #333;
36 display: flex;
37 align-items: center;
38 gap: 6px;
39 }
40
41 .todo-icon {
42 width: 14px;
43 height: 14px;
44 color: #666;
45 }
46
47 .todo-content {
48 flex: 1;
49 overflow-y: auto;
50 padding: 8px;
51 padding-bottom: 20px; /* Extra bottom padding for better scrolling */
52 font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
53 font-size: 12px;
54 line-height: 1.4;
55 /* Ensure scrollbar is always accessible */
56 min-height: 0;
57 }
58
59 .todo-content.loading {
60 display: flex;
61 align-items: center;
62 justify-content: center;
63 color: #666;
64 }
65
66 .todo-content.error {
67 color: #d32f2f;
68 display: flex;
69 align-items: center;
70 justify-content: center;
71 }
72
73 .todo-content.empty {
74 color: #999;
75 font-style: italic;
76 display: flex;
77 align-items: center;
78 justify-content: center;
79 }
80
81 /* Todo item styling */
82 .todo-item {
83 display: flex;
84 align-items: flex-start;
85 padding: 8px;
86 margin-bottom: 6px;
87 border-radius: 4px;
88 background-color: #fff;
89 border: 1px solid #e0e0e0;
90 gap: 8px;
91 }
92
93 .todo-item.queued {
94 border-left: 3px solid #e0e0e0;
95 }
96
97 .todo-item.in-progress {
98 border-left: 3px solid #e0e0e0;
99 }
100
101 .todo-item.completed {
102 border-left: 3px solid #e0e0e0;
103 }
104
105
106
107 .todo-status-icon {
108 font-size: 14px;
109 margin-top: 1px;
110 flex-shrink: 0;
111 }
112
113
114
115 .todo-main {
116 flex: 1;
117 min-width: 0;
118 }
119
120 .todo-content-text {
121 font-size: 12px;
122 line-height: 1.3;
123 color: #333;
124 word-wrap: break-word;
125 }
126
127
128
129 .todo-header-text {
130 display: flex;
131 align-items: center;
132 gap: 6px;
133 }
134
135 .todo-count {
136 background-color: #e0e0e0;
137 color: #666;
138 padding: 2px 6px;
139 border-radius: 10px;
140 font-size: 10px;
141 font-weight: normal;
142 }
143
144 /* Loading spinner */
145 .spinner {
146 width: 20px;
147 height: 20px;
148 border: 2px solid #f3f3f3;
149 border-top: 2px solid #3498db;
150 border-radius: 50%;
151 animation: spin 1s linear infinite;
152 margin-right: 8px;
153 }
154
155 @keyframes spin {
156 0% { transform: rotate(0deg); }
157 100% { transform: rotate(360deg); }
158 }
159 `;
160
161 updateTodoContent(content: string) {
162 try {
163 if (!content.trim()) {
164 this.todoList = null;
165 } else {
166 this.todoList = JSON.parse(content) as TodoList;
167 }
168 this.loading = false;
169 this.error = "";
170 } catch (error) {
171 console.error("Failed to parse todo content:", error);
172 this.error = "Failed to parse todo data";
173 this.todoList = null;
174 this.loading = false;
175 }
176 }
177
178
179
180 private renderTodoItem(item: TodoItem) {
181 const statusIcon = {
182 queued: '⚪',
183 'in-progress': '🦉',
184 completed: '✅'
185 }[item.status] || '?';
186
187 return html`
188 <div class="todo-item ${item.status}">
189 <div class="todo-status-icon">${statusIcon}</div>
190 <div class="todo-main">
191 <div class="todo-content-text">${item.task}</div>
192
193 </div>
194 </div>
195 `;
196 }
197
198 render() {
199 if (!this.visible) {
200 return html``;
201 }
202
203 const todoIcon = html`
204 <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">
205 <path d="M9 11l3 3L22 4"></path>
206 <path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"></path>
207 </svg>
208 `;
209
210 let contentElement;
211 if (this.loading) {
212 contentElement = html`
213 <div class="todo-content loading">
214 <div class="spinner"></div>
215 Loading todos...
216 </div>
217 `;
218 } else if (this.error) {
219 contentElement = html`
220 <div class="todo-content error">
221 Error: ${this.error}
222 </div>
223 `;
224 } else if (!this.todoList || !this.todoList.items || this.todoList.items.length === 0) {
225 contentElement = html`
226 <div class="todo-content empty">
227 No todos available
228 </div>
229 `;
230 } else {
231 const totalCount = this.todoList.items.length;
232 const completedCount = this.todoList.items.filter(item => item.status === 'completed').length;
233 const inProgressCount = this.todoList.items.filter(item => item.status === 'in-progress').length;
234
235 contentElement = html`
236 <div class="todo-header">
237 <div class="todo-header-text">
238 ${todoIcon}
239 <span>Sketching...</span>
240 <span class="todo-count">${completedCount}/${totalCount}</span>
241 </div>
242 </div>
243 <div class="todo-content">
244 ${this.todoList.items.map(item => this.renderTodoItem(item))}
245 </div>
246 `;
247 }
248
249 return html`
250 ${contentElement}
251 `;
252 }
253}
254
255declare global {
256 interface HTMLElementTagNameMap {
257 "sketch-todo-panel": SketchTodoPanel;
258 }
259}