blob: e76de46ead156ae559e507ecb5ea2d60b1f38d1a [file] [log] [blame]
bankseancdb08a52025-07-02 20:28:29 +00001import { html } from "lit";
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -07002import { customElement, property, state } from "lit/decorators.js";
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -07003import { TodoList, TodoItem } from "../types.js";
bankseancdb08a52025-07-02 20:28:29 +00004import { SketchTailwindElement } from "./sketch-tailwind-element.js";
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -07005
6@customElement("sketch-todo-panel")
bankseancdb08a52025-07-02 20:28:29 +00007export class SketchTodoPanel extends SketchTailwindElement {
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -07008 @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 updateTodoContent(content: string) {
30 try {
31 if (!content.trim()) {
32 this.todoList = null;
33 } else {
34 this.todoList = JSON.parse(content) as TodoList;
35 }
36 this.loading = false;
37 this.error = "";
38 } catch (error) {
39 console.error("Failed to parse todo content:", error);
40 this.error = "Failed to parse todo data";
41 this.todoList = null;
42 this.loading = false;
43 }
44 }
45
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -070046 private renderTodoItem(item: TodoItem) {
Autoformatter71c73b52025-05-29 20:18:43 +000047 const statusIcon =
48 {
49 queued: "⚪",
50 "in-progress": "🦉",
51 completed: "✅",
52 }[item.status] || "?";
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -070053
Josh Bleecher Snyderbd52faf2025-06-02 21:21:37 +000054 // Only show comment button for non-completed items
55 const showCommentButton = item.status !== "completed";
56
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -070057 return html`
bankseancdb08a52025-07-02 20:28:29 +000058 <div
59 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"
60 >
61 <div class="text-sm mt-0.5 flex-shrink-0">${statusIcon}</div>
62 <div class="flex items-start justify-between w-full min-h-5">
63 <div class="flex-1 min-w-0 pr-2">
64 <div class="text-xs leading-snug text-gray-800 break-words">
65 ${item.task}
66 </div>
Josh Bleecher Snyderbd52faf2025-06-02 21:21:37 +000067 </div>
bankseancdb08a52025-07-02 20:28:29 +000068 <div class="flex-shrink-0 flex items-start w-6 justify-center">
Autoformatter457dfd12025-06-03 00:18:36 +000069 ${showCommentButton
70 ? html`
71 <button
bankseancdb08a52025-07-02 20:28:29 +000072 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"
Autoformatter457dfd12025-06-03 00:18:36 +000073 @click="${() => this.openCommentBox(item)}"
74 title="Add comment about this TODO item"
75 >
76 💬
77 </button>
78 `
79 : ""}
Josh Bleecher Snyderbd52faf2025-06-02 21:21:37 +000080 </div>
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -070081 </div>
82 </div>
83 `;
84 }
85
86 render() {
87 if (!this.visible) {
88 return html``;
89 }
90
91 const todoIcon = html`
Autoformatter71c73b52025-05-29 20:18:43 +000092 <svg
bankseancdb08a52025-07-02 20:28:29 +000093 class="w-3.5 h-3.5 text-gray-500"
Autoformatter71c73b52025-05-29 20:18:43 +000094 xmlns="http://www.w3.org/2000/svg"
95 viewBox="0 0 24 24"
96 fill="none"
97 stroke="currentColor"
98 stroke-width="2"
99 stroke-linecap="round"
100 stroke-linejoin="round"
101 >
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700102 <path d="M9 11l3 3L22 4"></path>
103 <path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"></path>
104 </svg>
105 `;
106
107 let contentElement;
108 if (this.loading) {
109 contentElement = html`
bankseancdb08a52025-07-02 20:28:29 +0000110 <div
111 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"
112 >
113 <div
114 class="w-5 h-5 border-2 border-gray-200 border-t-blue-500 rounded-full animate-spin mr-2"
115 ></div>
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700116 Loading todos...
117 </div>
118 `;
119 } else if (this.error) {
120 contentElement = html`
bankseancdb08a52025-07-02 20:28:29 +0000121 <div
122 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"
123 >
124 Error: ${this.error}
125 </div>
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700126 `;
Autoformatter71c73b52025-05-29 20:18:43 +0000127 } else if (
128 !this.todoList ||
129 !this.todoList.items ||
130 this.todoList.items.length === 0
131 ) {
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700132 contentElement = html`
bankseancdb08a52025-07-02 20:28:29 +0000133 <div
134 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"
135 >
136 No todos available
137 </div>
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700138 `;
139 } else {
140 const totalCount = this.todoList.items.length;
Autoformatter71c73b52025-05-29 20:18:43 +0000141 const completedCount = this.todoList.items.filter(
142 (item) => item.status === "completed",
143 ).length;
philip.zeyliger26bc6592025-06-30 20:15:30 -0700144 const _inProgressCount = this.todoList.items.filter(
Autoformatter71c73b52025-05-29 20:18:43 +0000145 (item) => item.status === "in-progress",
146 ).length;
147
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700148 contentElement = html`
bankseancdb08a52025-07-02 20:28:29 +0000149 <div
150 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"
151 >
152 <div class="flex items-center gap-1.5">
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700153 ${todoIcon}
154 <span>Sketching...</span>
bankseancdb08a52025-07-02 20:28:29 +0000155 <span
156 class="bg-gray-300 text-gray-500 px-1.5 py-0.5 rounded-full text-xs font-normal"
157 >${completedCount}/${totalCount}</span
158 >
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700159 </div>
160 </div>
bankseancdb08a52025-07-02 20:28:29 +0000161 <div
162 class="flex-1 overflow-y-auto p-2 pb-5 text-xs leading-relaxed min-h-0"
163 >
Autoformatter71c73b52025-05-29 20:18:43 +0000164 ${this.todoList.items.map((item) => this.renderTodoItem(item))}
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700165 </div>
166 `;
167 }
168
Josh Bleecher Snyderbd52faf2025-06-02 21:21:37 +0000169 return html`
bankseancdb08a52025-07-02 20:28:29 +0000170 <div class="flex flex-col h-full bg-transparent overflow-hidden">
171 ${contentElement}
172 </div>
173 ${this.showCommentBox ? this.renderCommentBox() : ""}
Josh Bleecher Snyderbd52faf2025-06-02 21:21:37 +0000174 `;
175 }
176
177 private renderCommentBox() {
178 if (!this.commentingItem) return "";
179
Autoformatter457dfd12025-06-03 00:18:36 +0000180 const statusText =
181 {
182 queued: "Queued",
183 "in-progress": "In Progress",
184 completed: "Completed",
185 }[this.commentingItem.status] || this.commentingItem.status;
Josh Bleecher Snyderbd52faf2025-06-02 21:21:37 +0000186
187 return html`
bankseancdb08a52025-07-02 20:28:29 +0000188 <style>
189 @keyframes fadeIn {
190 from {
191 opacity: 0;
192 }
193 to {
194 opacity: 1;
195 }
196 }
197 .animate-fade-in {
198 animation: fadeIn 0.2s ease-in-out;
199 }
200 </style>
201 <div
202 class="fixed inset-0 bg-black/30 z-[10000] flex items-center justify-center animate-fade-in"
203 @click="${this.handleOverlayClick}"
204 >
205 <div
206 class="bg-white border border-gray-300 rounded-md shadow-lg p-4 w-96 max-w-[90vw] max-h-[80vh] overflow-y-auto"
207 @click="${this.stopPropagation}"
208 >
209 <div class="flex justify-between items-center mb-3">
210 <h3 class="m-0 text-sm font-medium">Comment on TODO Item</h3>
211 <button
212 class="bg-transparent border-none cursor-pointer text-lg text-gray-500 px-1.5 py-0.5 hover:text-gray-800"
213 @click="${this.closeCommentBox}"
214 >
Josh Bleecher Snyderbd52faf2025-06-02 21:21:37 +0000215 ×
216 </button>
217 </div>
Autoformatter457dfd12025-06-03 00:18:36 +0000218
bankseancdb08a52025-07-02 20:28:29 +0000219 <div
220 class="bg-gray-50 border border-gray-200 rounded p-2 mb-3 text-xs"
221 >
222 <div class="font-medium text-gray-500 mb-1">
223 Status: ${statusText}
224 </div>
225 <div class="text-gray-800">${this.commentingItem.task}</div>
Josh Bleecher Snyderbd52faf2025-06-02 21:21:37 +0000226 </div>
Autoformatter457dfd12025-06-03 00:18:36 +0000227
Josh Bleecher Snyderbd52faf2025-06-02 21:21:37 +0000228 <textarea
bankseancdb08a52025-07-02 20:28:29 +0000229 class="w-full min-h-20 p-2 border border-gray-300 rounded resize-y text-xs mb-3 box-border"
Josh Bleecher Snyderbd52faf2025-06-02 21:21:37 +0000230 placeholder="Type your comment about this TODO item..."
231 .value="${this.commentText}"
232 @input="${this.handleCommentInput}"
233 ></textarea>
Autoformatter457dfd12025-06-03 00:18:36 +0000234
bankseancdb08a52025-07-02 20:28:29 +0000235 <div class="flex justify-end gap-2">
236 <button
237 class="px-3 py-1.5 rounded cursor-pointer text-xs bg-transparent border border-gray-300 text-gray-500 hover:bg-gray-100"
238 @click="${this.closeCommentBox}"
239 >
Josh Bleecher Snyderbd52faf2025-06-02 21:21:37 +0000240 Cancel
241 </button>
bankseancdb08a52025-07-02 20:28:29 +0000242 <button
243 class="px-3 py-1.5 rounded cursor-pointer text-xs bg-blue-500 text-white border-none hover:bg-blue-600"
244 @click="${this.submitComment}"
245 >
Josh Bleecher Snyderbd52faf2025-06-02 21:21:37 +0000246 Add Comment
247 </button>
248 </div>
249 </div>
250 </div>
251 `;
252 }
253
254 private openCommentBox(item: TodoItem) {
255 this.commentingItem = item;
256 this.commentText = "";
257 this.showCommentBox = true;
258 }
259
260 private closeCommentBox() {
261 this.showCommentBox = false;
262 this.commentingItem = null;
263 this.commentText = "";
264 }
265
philip.zeyliger26bc6592025-06-30 20:15:30 -0700266 private handleOverlayClick(_e: Event) {
Josh Bleecher Snyderbd52faf2025-06-02 21:21:37 +0000267 // Close when clicking outside the comment box
268 this.closeCommentBox();
269 }
270
271 private stopPropagation(e: Event) {
272 // Prevent clicks inside the comment box from closing it
273 e.stopPropagation();
274 }
275
276 private handleCommentInput(e: Event) {
277 const target = e.target as HTMLTextAreaElement;
278 this.commentText = target.value;
279 }
280
281 private submitComment() {
282 if (!this.commentingItem || !this.commentText.trim()) {
283 return;
284 }
285
286 // Format the comment similar to diff comments
Autoformatter457dfd12025-06-03 00:18:36 +0000287 const statusText =
288 {
289 queued: "Queued",
290 "in-progress": "In Progress",
291 completed: "Completed",
292 }[this.commentingItem.status] || this.commentingItem.status;
Josh Bleecher Snyderbd52faf2025-06-02 21:21:37 +0000293
294 const formattedComment = `\`\`\`
295TODO Item (${statusText}): ${this.commentingItem.task}
296\`\`\`
297
298${this.commentText}`;
299
300 // Dispatch a custom event similar to diff comments
301 const event = new CustomEvent("todo-comment", {
302 detail: { comment: formattedComment },
303 bubbles: true,
304 composed: true,
305 });
306
307 this.dispatchEvent(event);
Autoformatter457dfd12025-06-03 00:18:36 +0000308
Josh Bleecher Snyderbd52faf2025-06-02 21:21:37 +0000309 // Close the comment box
310 this.closeCommentBox();
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700311 }
312}
313
314declare global {
315 interface HTMLElementTagNameMap {
316 "sketch-todo-panel": SketchTodoPanel;
317 }
Autoformatter71c73b52025-05-29 20:18:43 +0000318}