blob: dd23276d0c503c8bfa6fdac63b964f5b9935d1fc [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
Josh Bleecher Snyderbd52faf2025-06-02 21:21:37 +000020 @state()
21 private showCommentBox: boolean = false;
22
23 @state()
24 private commentingItem: TodoItem | null = null;
25
26 @state()
27 private commentText: string = "";
28
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -070029 static styles = css`
30 :host {
31 display: flex;
32 flex-direction: column;
33 height: 100%;
34 background-color: transparent; /* Let parent handle background */
35 overflow: hidden; /* Ensure proper clipping */
36 }
37
38 .todo-header {
39 padding: 8px 12px;
40 border-bottom: 1px solid #e0e0e0;
41 background-color: #f5f5f5;
42 font-weight: 600;
43 font-size: 13px;
44 color: #333;
45 display: flex;
46 align-items: center;
47 gap: 6px;
48 }
49
50 .todo-icon {
51 width: 14px;
52 height: 14px;
53 color: #666;
54 }
55
56 .todo-content {
57 flex: 1;
58 overflow-y: auto;
59 padding: 8px;
60 padding-bottom: 20px; /* Extra bottom padding for better scrolling */
Autoformatter71c73b52025-05-29 20:18:43 +000061 font-family:
62 system-ui,
63 -apple-system,
64 BlinkMacSystemFont,
65 "Segoe UI",
66 sans-serif;
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -070067 font-size: 12px;
68 line-height: 1.4;
69 /* Ensure scrollbar is always accessible */
70 min-height: 0;
71 }
72
73 .todo-content.loading {
74 display: flex;
75 align-items: center;
76 justify-content: center;
77 color: #666;
78 }
79
80 .todo-content.error {
81 color: #d32f2f;
82 display: flex;
83 align-items: center;
84 justify-content: center;
85 }
86
87 .todo-content.empty {
88 color: #999;
89 font-style: italic;
90 display: flex;
91 align-items: center;
92 justify-content: center;
93 }
94
95 /* Todo item styling */
96 .todo-item {
97 display: flex;
98 align-items: flex-start;
99 padding: 8px;
100 margin-bottom: 6px;
101 border-radius: 4px;
102 background-color: #fff;
103 border: 1px solid #e0e0e0;
104 gap: 8px;
Josh Bleecher Snyderbd52faf2025-06-02 21:21:37 +0000105 min-height: 24px; /* Ensure consistent height */
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700106 }
107
108 .todo-item.queued {
109 border-left: 3px solid #e0e0e0;
110 }
111
112 .todo-item.in-progress {
113 border-left: 3px solid #e0e0e0;
114 }
115
116 .todo-item.completed {
117 border-left: 3px solid #e0e0e0;
118 }
119
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700120 .todo-status-icon {
121 font-size: 14px;
122 margin-top: 1px;
123 flex-shrink: 0;
124 }
125
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700126 .todo-main {
127 flex: 1;
128 min-width: 0;
129 }
130
131 .todo-content-text {
132 font-size: 12px;
133 line-height: 1.3;
134 color: #333;
135 word-wrap: break-word;
136 }
137
Josh Bleecher Snyderbd52faf2025-06-02 21:21:37 +0000138 .todo-item-content {
139 display: flex;
140 align-items: flex-start;
141 justify-content: space-between;
142 width: 100%;
143 min-height: 20px; /* Ensure consistent height */
144 }
145
146 .todo-text-section {
147 flex: 1;
148 min-width: 0;
149 padding-right: 8px; /* Space between text and button column */
150 }
151
152 .todo-actions-column {
153 flex-shrink: 0;
154 display: flex;
155 align-items: flex-start;
156 width: 24px; /* Fixed width for button column */
157 justify-content: center;
158 }
159
160 .comment-button {
161 background: none;
162 border: none;
163 cursor: pointer;
164 font-size: 14px;
165 padding: 2px;
166 color: #666;
167 opacity: 0.7;
168 transition: opacity 0.2s ease;
169 width: 20px;
170 height: 20px;
171 display: flex;
172 align-items: center;
173 justify-content: center;
174 }
175
176 .comment-button:hover {
177 opacity: 1;
178 background-color: rgba(0, 0, 0, 0.05);
179 border-radius: 3px;
180 }
181
182 /* Comment box overlay */
183 .comment-overlay {
184 position: fixed;
185 top: 0;
186 left: 0;
187 right: 0;
188 bottom: 0;
189 background-color: rgba(0, 0, 0, 0.3);
190 z-index: 10000;
191 display: flex;
192 align-items: center;
193 justify-content: center;
194 animation: fadeIn 0.2s ease-in-out;
195 }
196
197 .comment-box {
198 background-color: white;
199 border: 1px solid #ddd;
200 border-radius: 6px;
201 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
202 padding: 16px;
203 width: 400px;
204 max-width: 90vw;
205 max-height: 80vh;
206 overflow-y: auto;
207 }
208
209 .comment-box-header {
210 display: flex;
211 justify-content: space-between;
212 align-items: center;
213 margin-bottom: 12px;
214 }
215
216 .comment-box-header h3 {
217 margin: 0;
218 font-size: 14px;
219 font-weight: 500;
220 }
221
222 .close-button {
223 background: none;
224 border: none;
225 cursor: pointer;
226 font-size: 18px;
227 color: #666;
228 padding: 2px 6px;
229 }
230
231 .close-button:hover {
232 color: #333;
233 }
234
235 .todo-context {
236 background-color: #f8f9fa;
237 border: 1px solid #e9ecef;
238 border-radius: 4px;
239 padding: 8px;
240 margin-bottom: 12px;
241 font-size: 12px;
242 }
243
244 .todo-context-status {
245 font-weight: 500;
246 color: #666;
247 margin-bottom: 4px;
248 }
249
250 .todo-context-task {
251 color: #333;
252 }
253
254 .comment-textarea {
255 width: 100%;
256 min-height: 80px;
257 padding: 8px;
258 border: 1px solid #ddd;
259 border-radius: 4px;
260 resize: vertical;
261 font-family: inherit;
262 font-size: 12px;
263 margin-bottom: 12px;
264 box-sizing: border-box;
265 }
266
267 .comment-actions {
268 display: flex;
269 justify-content: flex-end;
270 gap: 8px;
271 }
272
273 .comment-actions button {
274 padding: 6px 12px;
275 border-radius: 4px;
276 cursor: pointer;
277 font-size: 12px;
278 }
279
280 .cancel-button {
281 background-color: transparent;
282 border: 1px solid #ddd;
283 color: #666;
284 }
285
286 .cancel-button:hover {
287 background-color: #f5f5f5;
288 }
289
290 .submit-button {
291 background-color: #4285f4;
292 color: white;
293 border: none;
294 }
295
296 .submit-button:hover {
297 background-color: #3367d6;
298 }
299
300 @keyframes fadeIn {
301 from {
302 opacity: 0;
303 }
304 to {
305 opacity: 1;
306 }
307 }
308
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700309 .todo-header-text {
310 display: flex;
311 align-items: center;
312 gap: 6px;
313 }
314
315 .todo-count {
316 background-color: #e0e0e0;
317 color: #666;
318 padding: 2px 6px;
319 border-radius: 10px;
320 font-size: 10px;
321 font-weight: normal;
322 }
323
324 /* Loading spinner */
325 .spinner {
326 width: 20px;
327 height: 20px;
328 border: 2px solid #f3f3f3;
329 border-top: 2px solid #3498db;
330 border-radius: 50%;
331 animation: spin 1s linear infinite;
332 margin-right: 8px;
333 }
334
335 @keyframes spin {
Autoformatter71c73b52025-05-29 20:18:43 +0000336 0% {
337 transform: rotate(0deg);
338 }
339 100% {
340 transform: rotate(360deg);
341 }
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700342 }
343 `;
344
345 updateTodoContent(content: string) {
346 try {
347 if (!content.trim()) {
348 this.todoList = null;
349 } else {
350 this.todoList = JSON.parse(content) as TodoList;
351 }
352 this.loading = false;
353 this.error = "";
354 } catch (error) {
355 console.error("Failed to parse todo content:", error);
356 this.error = "Failed to parse todo data";
357 this.todoList = null;
358 this.loading = false;
359 }
360 }
361
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700362 private renderTodoItem(item: TodoItem) {
Autoformatter71c73b52025-05-29 20:18:43 +0000363 const statusIcon =
364 {
365 queued: "⚪",
366 "in-progress": "🦉",
367 completed: "✅",
368 }[item.status] || "?";
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700369
Josh Bleecher Snyderbd52faf2025-06-02 21:21:37 +0000370 // Only show comment button for non-completed items
371 const showCommentButton = item.status !== "completed";
372
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700373 return html`
374 <div class="todo-item ${item.status}">
375 <div class="todo-status-icon">${statusIcon}</div>
Josh Bleecher Snyderbd52faf2025-06-02 21:21:37 +0000376 <div class="todo-item-content">
377 <div class="todo-text-section">
378 <div class="todo-content-text">${item.task}</div>
379 </div>
380 <div class="todo-actions-column">
Autoformatter457dfd12025-06-03 00:18:36 +0000381 ${showCommentButton
382 ? html`
383 <button
384 class="comment-button"
385 @click="${() => this.openCommentBox(item)}"
386 title="Add comment about this TODO item"
387 >
388 💬
389 </button>
390 `
391 : ""}
Josh Bleecher Snyderbd52faf2025-06-02 21:21:37 +0000392 </div>
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700393 </div>
394 </div>
395 `;
396 }
397
398 render() {
399 if (!this.visible) {
400 return html``;
401 }
402
403 const todoIcon = html`
Autoformatter71c73b52025-05-29 20:18:43 +0000404 <svg
405 class="todo-icon"
406 xmlns="http://www.w3.org/2000/svg"
407 viewBox="0 0 24 24"
408 fill="none"
409 stroke="currentColor"
410 stroke-width="2"
411 stroke-linecap="round"
412 stroke-linejoin="round"
413 >
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700414 <path d="M9 11l3 3L22 4"></path>
415 <path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"></path>
416 </svg>
417 `;
418
419 let contentElement;
420 if (this.loading) {
421 contentElement = html`
422 <div class="todo-content loading">
423 <div class="spinner"></div>
424 Loading todos...
425 </div>
426 `;
427 } else if (this.error) {
428 contentElement = html`
Autoformatter71c73b52025-05-29 20:18:43 +0000429 <div class="todo-content error">Error: ${this.error}</div>
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700430 `;
Autoformatter71c73b52025-05-29 20:18:43 +0000431 } else if (
432 !this.todoList ||
433 !this.todoList.items ||
434 this.todoList.items.length === 0
435 ) {
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700436 contentElement = html`
Autoformatter71c73b52025-05-29 20:18:43 +0000437 <div class="todo-content empty">No todos available</div>
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700438 `;
439 } else {
440 const totalCount = this.todoList.items.length;
Autoformatter71c73b52025-05-29 20:18:43 +0000441 const completedCount = this.todoList.items.filter(
442 (item) => item.status === "completed",
443 ).length;
444 const inProgressCount = this.todoList.items.filter(
445 (item) => item.status === "in-progress",
446 ).length;
447
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700448 contentElement = html`
449 <div class="todo-header">
450 <div class="todo-header-text">
451 ${todoIcon}
452 <span>Sketching...</span>
453 <span class="todo-count">${completedCount}/${totalCount}</span>
454 </div>
455 </div>
456 <div class="todo-content">
Autoformatter71c73b52025-05-29 20:18:43 +0000457 ${this.todoList.items.map((item) => this.renderTodoItem(item))}
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700458 </div>
459 `;
460 }
461
Josh Bleecher Snyderbd52faf2025-06-02 21:21:37 +0000462 return html`
Autoformatter457dfd12025-06-03 00:18:36 +0000463 ${contentElement} ${this.showCommentBox ? this.renderCommentBox() : ""}
Josh Bleecher Snyderbd52faf2025-06-02 21:21:37 +0000464 `;
465 }
466
467 private renderCommentBox() {
468 if (!this.commentingItem) return "";
469
Autoformatter457dfd12025-06-03 00:18:36 +0000470 const statusText =
471 {
472 queued: "Queued",
473 "in-progress": "In Progress",
474 completed: "Completed",
475 }[this.commentingItem.status] || this.commentingItem.status;
Josh Bleecher Snyderbd52faf2025-06-02 21:21:37 +0000476
477 return html`
478 <div class="comment-overlay" @click="${this.handleOverlayClick}">
479 <div class="comment-box" @click="${this.stopPropagation}">
480 <div class="comment-box-header">
481 <h3>Comment on TODO Item</h3>
482 <button class="close-button" @click="${this.closeCommentBox}">
483 ×
484 </button>
485 </div>
Autoformatter457dfd12025-06-03 00:18:36 +0000486
Josh Bleecher Snyderbd52faf2025-06-02 21:21:37 +0000487 <div class="todo-context">
488 <div class="todo-context-status">Status: ${statusText}</div>
489 <div class="todo-context-task">${this.commentingItem.task}</div>
490 </div>
Autoformatter457dfd12025-06-03 00:18:36 +0000491
Josh Bleecher Snyderbd52faf2025-06-02 21:21:37 +0000492 <textarea
493 class="comment-textarea"
494 placeholder="Type your comment about this TODO item..."
495 .value="${this.commentText}"
496 @input="${this.handleCommentInput}"
497 ></textarea>
Autoformatter457dfd12025-06-03 00:18:36 +0000498
Josh Bleecher Snyderbd52faf2025-06-02 21:21:37 +0000499 <div class="comment-actions">
500 <button class="cancel-button" @click="${this.closeCommentBox}">
501 Cancel
502 </button>
503 <button class="submit-button" @click="${this.submitComment}">
504 Add Comment
505 </button>
506 </div>
507 </div>
508 </div>
509 `;
510 }
511
512 private openCommentBox(item: TodoItem) {
513 this.commentingItem = item;
514 this.commentText = "";
515 this.showCommentBox = true;
516 }
517
518 private closeCommentBox() {
519 this.showCommentBox = false;
520 this.commentingItem = null;
521 this.commentText = "";
522 }
523
524 private handleOverlayClick(e: Event) {
525 // Close when clicking outside the comment box
526 this.closeCommentBox();
527 }
528
529 private stopPropagation(e: Event) {
530 // Prevent clicks inside the comment box from closing it
531 e.stopPropagation();
532 }
533
534 private handleCommentInput(e: Event) {
535 const target = e.target as HTMLTextAreaElement;
536 this.commentText = target.value;
537 }
538
539 private submitComment() {
540 if (!this.commentingItem || !this.commentText.trim()) {
541 return;
542 }
543
544 // Format the comment similar to diff comments
Autoformatter457dfd12025-06-03 00:18:36 +0000545 const statusText =
546 {
547 queued: "Queued",
548 "in-progress": "In Progress",
549 completed: "Completed",
550 }[this.commentingItem.status] || this.commentingItem.status;
Josh Bleecher Snyderbd52faf2025-06-02 21:21:37 +0000551
552 const formattedComment = `\`\`\`
553TODO Item (${statusText}): ${this.commentingItem.task}
554\`\`\`
555
556${this.commentText}`;
557
558 // Dispatch a custom event similar to diff comments
559 const event = new CustomEvent("todo-comment", {
560 detail: { comment: formattedComment },
561 bubbles: true,
562 composed: true,
563 });
564
565 this.dispatchEvent(event);
Autoformatter457dfd12025-06-03 00:18:36 +0000566
Josh Bleecher Snyderbd52faf2025-06-02 21:21:37 +0000567 // Close the comment box
568 this.closeCommentBox();
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700569 }
570}
571
572declare global {
573 interface HTMLElementTagNameMap {
574 "sketch-todo-panel": SketchTodoPanel;
575 }
Autoformatter71c73b52025-05-29 20:18:43 +0000576}