blob: c752b5a40867b539057cbcd870b8fa2c86729e6f [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
banksean3eaa4332025-07-19 02:19:06 +000059 class="flex items-start p-2 mb-1.5 rounded bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 gap-2 min-h-6 border-l-[3px] border-l-gray-300 dark:border-l-gray-600"
bankseancdb08a52025-07-02 20:28:29 +000060 >
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">
banksean3eaa4332025-07-19 02:19:06 +000064 <div
65 class="text-xs leading-snug text-gray-800 dark:text-gray-200 break-words"
66 >
bankseancdb08a52025-07-02 20:28:29 +000067 ${item.task}
68 </div>
Josh Bleecher Snyderbd52faf2025-06-02 21:21:37 +000069 </div>
bankseancdb08a52025-07-02 20:28:29 +000070 <div class="flex-shrink-0 flex items-start w-6 justify-center">
Autoformatter457dfd12025-06-03 00:18:36 +000071 ${showCommentButton
72 ? html`
73 <button
banksean3eaa4332025-07-19 02:19:06 +000074 class="bg-transparent border-none cursor-pointer text-sm p-0.5 text-gray-500 dark:text-gray-400 opacity-70 transition-opacity duration-200 w-5 h-5 flex items-center justify-center hover:opacity-100 hover:bg-black/5 dark:hover:bg-white/10 hover:bg-opacity-5 hover:rounded-sm"
Autoformatter457dfd12025-06-03 00:18:36 +000075 @click="${() => this.openCommentBox(item)}"
76 title="Add comment about this TODO item"
77 >
78 💬
79 </button>
80 `
81 : ""}
Josh Bleecher Snyderbd52faf2025-06-02 21:21:37 +000082 </div>
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -070083 </div>
84 </div>
85 `;
86 }
87
88 render() {
89 if (!this.visible) {
90 return html``;
91 }
92
93 const todoIcon = html`
Autoformatter71c73b52025-05-29 20:18:43 +000094 <svg
banksean3eaa4332025-07-19 02:19:06 +000095 class="w-3.5 h-3.5 text-gray-500 dark:text-gray-400"
Autoformatter71c73b52025-05-29 20:18:43 +000096 xmlns="http://www.w3.org/2000/svg"
97 viewBox="0 0 24 24"
98 fill="none"
99 stroke="currentColor"
100 stroke-width="2"
101 stroke-linecap="round"
102 stroke-linejoin="round"
103 >
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700104 <path d="M9 11l3 3L22 4"></path>
105 <path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"></path>
106 </svg>
107 `;
108
109 let contentElement;
110 if (this.loading) {
111 contentElement = html`
bankseancdb08a52025-07-02 20:28:29 +0000112 <div
banksean3eaa4332025-07-19 02:19:06 +0000113 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 dark:text-gray-400"
bankseancdb08a52025-07-02 20:28:29 +0000114 >
115 <div
banksean3eaa4332025-07-19 02:19:06 +0000116 class="w-5 h-5 border-2 border-gray-200 dark:border-gray-600 border-t-blue-500 rounded-full animate-spin mr-2"
bankseancdb08a52025-07-02 20:28:29 +0000117 ></div>
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700118 Loading todos...
119 </div>
120 `;
121 } else if (this.error) {
122 contentElement = html`
bankseancdb08a52025-07-02 20:28:29 +0000123 <div
banksean3eaa4332025-07-19 02:19:06 +0000124 class="flex-1 overflow-y-auto p-2 pb-5 text-xs leading-relaxed min-h-0 text-red-600 dark:text-red-400 flex items-center justify-center"
bankseancdb08a52025-07-02 20:28:29 +0000125 >
126 Error: ${this.error}
127 </div>
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700128 `;
Autoformatter71c73b52025-05-29 20:18:43 +0000129 } else if (
130 !this.todoList ||
131 !this.todoList.items ||
132 this.todoList.items.length === 0
133 ) {
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700134 contentElement = html`
bankseancdb08a52025-07-02 20:28:29 +0000135 <div
banksean3eaa4332025-07-19 02:19:06 +0000136 class="flex-1 overflow-y-auto p-2 pb-5 text-xs leading-relaxed min-h-0 text-gray-400 dark:text-gray-500 italic flex items-center justify-center"
bankseancdb08a52025-07-02 20:28:29 +0000137 >
138 No todos available
139 </div>
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700140 `;
141 } else {
142 const totalCount = this.todoList.items.length;
Autoformatter71c73b52025-05-29 20:18:43 +0000143 const completedCount = this.todoList.items.filter(
144 (item) => item.status === "completed",
145 ).length;
philip.zeyliger26bc6592025-06-30 20:15:30 -0700146 const _inProgressCount = this.todoList.items.filter(
Autoformatter71c73b52025-05-29 20:18:43 +0000147 (item) => item.status === "in-progress",
148 ).length;
149
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700150 contentElement = html`
bankseancdb08a52025-07-02 20:28:29 +0000151 <div
banksean3eaa4332025-07-19 02:19:06 +0000152 class="py-2 px-3 border-b border-gray-300 dark:border-gray-600 bg-gray-100 dark:bg-gray-700 font-semibold text-xs text-gray-800 dark:text-gray-200 flex items-center gap-1.5"
bankseancdb08a52025-07-02 20:28:29 +0000153 >
154 <div class="flex items-center gap-1.5">
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700155 ${todoIcon}
156 <span>Sketching...</span>
bankseancdb08a52025-07-02 20:28:29 +0000157 <span
banksean3eaa4332025-07-19 02:19:06 +0000158 class="bg-gray-300 dark:bg-gray-600 text-gray-500 dark:text-gray-400 px-1.5 py-0.5 rounded-full text-xs font-normal"
bankseancdb08a52025-07-02 20:28:29 +0000159 >${completedCount}/${totalCount}</span
160 >
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700161 </div>
162 </div>
bankseancdb08a52025-07-02 20:28:29 +0000163 <div
164 class="flex-1 overflow-y-auto p-2 pb-5 text-xs leading-relaxed min-h-0"
165 >
Autoformatter71c73b52025-05-29 20:18:43 +0000166 ${this.todoList.items.map((item) => this.renderTodoItem(item))}
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700167 </div>
168 `;
169 }
170
Josh Bleecher Snyderbd52faf2025-06-02 21:21:37 +0000171 return html`
bankseancdb08a52025-07-02 20:28:29 +0000172 <div class="flex flex-col h-full bg-transparent overflow-hidden">
173 ${contentElement}
174 </div>
175 ${this.showCommentBox ? this.renderCommentBox() : ""}
Josh Bleecher Snyderbd52faf2025-06-02 21:21:37 +0000176 `;
177 }
178
179 private renderCommentBox() {
180 if (!this.commentingItem) return "";
181
Autoformatter457dfd12025-06-03 00:18:36 +0000182 const statusText =
183 {
184 queued: "Queued",
185 "in-progress": "In Progress",
186 completed: "Completed",
187 }[this.commentingItem.status] || this.commentingItem.status;
Josh Bleecher Snyderbd52faf2025-06-02 21:21:37 +0000188
189 return html`
bankseancdb08a52025-07-02 20:28:29 +0000190 <style>
191 @keyframes fadeIn {
192 from {
193 opacity: 0;
194 }
195 to {
196 opacity: 1;
197 }
198 }
199 .animate-fade-in {
200 animation: fadeIn 0.2s ease-in-out;
201 }
202 </style>
203 <div
banksean3eaa4332025-07-19 02:19:06 +0000204 class="fixed inset-0 bg-black/30 dark:bg-black/50 z-[10000] flex items-center justify-center animate-fade-in"
bankseancdb08a52025-07-02 20:28:29 +0000205 @click="${this.handleOverlayClick}"
206 >
207 <div
banksean3eaa4332025-07-19 02:19:06 +0000208 class="bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md shadow-lg p-4 w-96 max-w-[90vw] max-h-[80vh] overflow-y-auto"
bankseancdb08a52025-07-02 20:28:29 +0000209 @click="${this.stopPropagation}"
210 >
211 <div class="flex justify-between items-center mb-3">
banksean3eaa4332025-07-19 02:19:06 +0000212 <h3
213 class="m-0 text-sm font-medium text-gray-900 dark:text-gray-100"
214 >
215 Comment on TODO Item
216 </h3>
bankseancdb08a52025-07-02 20:28:29 +0000217 <button
banksean3eaa4332025-07-19 02:19:06 +0000218 class="bg-transparent border-none cursor-pointer text-lg text-gray-500 dark:text-gray-400 px-1.5 py-0.5 hover:text-gray-800 dark:hover:text-gray-200"
bankseancdb08a52025-07-02 20:28:29 +0000219 @click="${this.closeCommentBox}"
220 >
Josh Bleecher Snyderbd52faf2025-06-02 21:21:37 +0000221 ×
222 </button>
223 </div>
Autoformatter457dfd12025-06-03 00:18:36 +0000224
bankseancdb08a52025-07-02 20:28:29 +0000225 <div
banksean3eaa4332025-07-19 02:19:06 +0000226 class="bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded p-2 mb-3 text-xs"
bankseancdb08a52025-07-02 20:28:29 +0000227 >
banksean3eaa4332025-07-19 02:19:06 +0000228 <div class="font-medium text-gray-500 dark:text-gray-400 mb-1">
bankseancdb08a52025-07-02 20:28:29 +0000229 Status: ${statusText}
230 </div>
banksean3eaa4332025-07-19 02:19:06 +0000231 <div class="text-gray-800 dark:text-gray-200">
232 ${this.commentingItem.task}
233 </div>
Josh Bleecher Snyderbd52faf2025-06-02 21:21:37 +0000234 </div>
Autoformatter457dfd12025-06-03 00:18:36 +0000235
Josh Bleecher Snyderbd52faf2025-06-02 21:21:37 +0000236 <textarea
banksean3eaa4332025-07-19 02:19:06 +0000237 class="w-full min-h-20 p-2 border border-gray-300 dark:border-gray-600 rounded resize-y text-xs mb-3 box-border bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
Josh Bleecher Snyderbd52faf2025-06-02 21:21:37 +0000238 placeholder="Type your comment about this TODO item..."
239 .value="${this.commentText}"
240 @input="${this.handleCommentInput}"
241 ></textarea>
Autoformatter457dfd12025-06-03 00:18:36 +0000242
bankseancdb08a52025-07-02 20:28:29 +0000243 <div class="flex justify-end gap-2">
244 <button
banksean3eaa4332025-07-19 02:19:06 +0000245 class="px-3 py-1.5 rounded cursor-pointer text-xs bg-transparent border border-gray-300 dark:border-gray-600 text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700"
bankseancdb08a52025-07-02 20:28:29 +0000246 @click="${this.closeCommentBox}"
247 >
Josh Bleecher Snyderbd52faf2025-06-02 21:21:37 +0000248 Cancel
249 </button>
bankseancdb08a52025-07-02 20:28:29 +0000250 <button
251 class="px-3 py-1.5 rounded cursor-pointer text-xs bg-blue-500 text-white border-none hover:bg-blue-600"
252 @click="${this.submitComment}"
253 >
Josh Bleecher Snyderbd52faf2025-06-02 21:21:37 +0000254 Add Comment
255 </button>
256 </div>
257 </div>
258 </div>
259 `;
260 }
261
262 private openCommentBox(item: TodoItem) {
263 this.commentingItem = item;
264 this.commentText = "";
265 this.showCommentBox = true;
266 }
267
268 private closeCommentBox() {
269 this.showCommentBox = false;
270 this.commentingItem = null;
271 this.commentText = "";
272 }
273
philip.zeyliger26bc6592025-06-30 20:15:30 -0700274 private handleOverlayClick(_e: Event) {
Josh Bleecher Snyderbd52faf2025-06-02 21:21:37 +0000275 // Close when clicking outside the comment box
276 this.closeCommentBox();
277 }
278
279 private stopPropagation(e: Event) {
280 // Prevent clicks inside the comment box from closing it
281 e.stopPropagation();
282 }
283
284 private handleCommentInput(e: Event) {
285 const target = e.target as HTMLTextAreaElement;
286 this.commentText = target.value;
287 }
288
289 private submitComment() {
290 if (!this.commentingItem || !this.commentText.trim()) {
291 return;
292 }
293
294 // Format the comment similar to diff comments
Autoformatter457dfd12025-06-03 00:18:36 +0000295 const statusText =
296 {
297 queued: "Queued",
298 "in-progress": "In Progress",
299 completed: "Completed",
300 }[this.commentingItem.status] || this.commentingItem.status;
Josh Bleecher Snyderbd52faf2025-06-02 21:21:37 +0000301
302 const formattedComment = `\`\`\`
303TODO Item (${statusText}): ${this.commentingItem.task}
304\`\`\`
305
306${this.commentText}`;
307
308 // Dispatch a custom event similar to diff comments
309 const event = new CustomEvent("todo-comment", {
310 detail: { comment: formattedComment },
311 bubbles: true,
312 composed: true,
313 });
314
315 this.dispatchEvent(event);
Autoformatter457dfd12025-06-03 00:18:36 +0000316
Josh Bleecher Snyderbd52faf2025-06-02 21:21:37 +0000317 // Close the comment box
318 this.closeCommentBox();
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700319 }
320}
321
322declare global {
323 interface HTMLElementTagNameMap {
324 "sketch-todo-panel": SketchTodoPanel;
325 }
Autoformatter71c73b52025-05-29 20:18:43 +0000326}