blob: 540397b45aa52d0d06cf7250053c9290d45158f3 [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 */
Autoformatter71c73b52025-05-29 20:18:43 +000052 font-family:
53 system-ui,
54 -apple-system,
55 BlinkMacSystemFont,
56 "Segoe UI",
57 sans-serif;
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -070058 font-size: 12px;
59 line-height: 1.4;
60 /* Ensure scrollbar is always accessible */
61 min-height: 0;
62 }
63
64 .todo-content.loading {
65 display: flex;
66 align-items: center;
67 justify-content: center;
68 color: #666;
69 }
70
71 .todo-content.error {
72 color: #d32f2f;
73 display: flex;
74 align-items: center;
75 justify-content: center;
76 }
77
78 .todo-content.empty {
79 color: #999;
80 font-style: italic;
81 display: flex;
82 align-items: center;
83 justify-content: center;
84 }
85
86 /* Todo item styling */
87 .todo-item {
88 display: flex;
89 align-items: flex-start;
90 padding: 8px;
91 margin-bottom: 6px;
92 border-radius: 4px;
93 background-color: #fff;
94 border: 1px solid #e0e0e0;
95 gap: 8px;
96 }
97
98 .todo-item.queued {
99 border-left: 3px solid #e0e0e0;
100 }
101
102 .todo-item.in-progress {
103 border-left: 3px solid #e0e0e0;
104 }
105
106 .todo-item.completed {
107 border-left: 3px solid #e0e0e0;
108 }
109
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700110 .todo-status-icon {
111 font-size: 14px;
112 margin-top: 1px;
113 flex-shrink: 0;
114 }
115
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700116 .todo-main {
117 flex: 1;
118 min-width: 0;
119 }
120
121 .todo-content-text {
122 font-size: 12px;
123 line-height: 1.3;
124 color: #333;
125 word-wrap: break-word;
126 }
127
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700128 .todo-header-text {
129 display: flex;
130 align-items: center;
131 gap: 6px;
132 }
133
134 .todo-count {
135 background-color: #e0e0e0;
136 color: #666;
137 padding: 2px 6px;
138 border-radius: 10px;
139 font-size: 10px;
140 font-weight: normal;
141 }
142
143 /* Loading spinner */
144 .spinner {
145 width: 20px;
146 height: 20px;
147 border: 2px solid #f3f3f3;
148 border-top: 2px solid #3498db;
149 border-radius: 50%;
150 animation: spin 1s linear infinite;
151 margin-right: 8px;
152 }
153
154 @keyframes spin {
Autoformatter71c73b52025-05-29 20:18:43 +0000155 0% {
156 transform: rotate(0deg);
157 }
158 100% {
159 transform: rotate(360deg);
160 }
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700161 }
162 `;
163
164 updateTodoContent(content: string) {
165 try {
166 if (!content.trim()) {
167 this.todoList = null;
168 } else {
169 this.todoList = JSON.parse(content) as TodoList;
170 }
171 this.loading = false;
172 this.error = "";
173 } catch (error) {
174 console.error("Failed to parse todo content:", error);
175 this.error = "Failed to parse todo data";
176 this.todoList = null;
177 this.loading = false;
178 }
179 }
180
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700181 private renderTodoItem(item: TodoItem) {
Autoformatter71c73b52025-05-29 20:18:43 +0000182 const statusIcon =
183 {
184 queued: "⚪",
185 "in-progress": "🦉",
186 completed: "✅",
187 }[item.status] || "?";
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700188
189 return html`
190 <div class="todo-item ${item.status}">
191 <div class="todo-status-icon">${statusIcon}</div>
192 <div class="todo-main">
193 <div class="todo-content-text">${item.task}</div>
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700194 </div>
195 </div>
196 `;
197 }
198
199 render() {
200 if (!this.visible) {
201 return html``;
202 }
203
204 const todoIcon = html`
Autoformatter71c73b52025-05-29 20:18:43 +0000205 <svg
206 class="todo-icon"
207 xmlns="http://www.w3.org/2000/svg"
208 viewBox="0 0 24 24"
209 fill="none"
210 stroke="currentColor"
211 stroke-width="2"
212 stroke-linecap="round"
213 stroke-linejoin="round"
214 >
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700215 <path d="M9 11l3 3L22 4"></path>
216 <path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"></path>
217 </svg>
218 `;
219
220 let contentElement;
221 if (this.loading) {
222 contentElement = html`
223 <div class="todo-content loading">
224 <div class="spinner"></div>
225 Loading todos...
226 </div>
227 `;
228 } else if (this.error) {
229 contentElement = html`
Autoformatter71c73b52025-05-29 20:18:43 +0000230 <div class="todo-content error">Error: ${this.error}</div>
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700231 `;
Autoformatter71c73b52025-05-29 20:18:43 +0000232 } else if (
233 !this.todoList ||
234 !this.todoList.items ||
235 this.todoList.items.length === 0
236 ) {
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700237 contentElement = html`
Autoformatter71c73b52025-05-29 20:18:43 +0000238 <div class="todo-content empty">No todos available</div>
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700239 `;
240 } else {
241 const totalCount = this.todoList.items.length;
Autoformatter71c73b52025-05-29 20:18:43 +0000242 const completedCount = this.todoList.items.filter(
243 (item) => item.status === "completed",
244 ).length;
245 const inProgressCount = this.todoList.items.filter(
246 (item) => item.status === "in-progress",
247 ).length;
248
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700249 contentElement = html`
250 <div class="todo-header">
251 <div class="todo-header-text">
252 ${todoIcon}
253 <span>Sketching...</span>
254 <span class="todo-count">${completedCount}/${totalCount}</span>
255 </div>
256 </div>
257 <div class="todo-content">
Autoformatter71c73b52025-05-29 20:18:43 +0000258 ${this.todoList.items.map((item) => this.renderTodoItem(item))}
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700259 </div>
260 `;
261 }
262
Autoformatter71c73b52025-05-29 20:18:43 +0000263 return html` ${contentElement} `;
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700264 }
265}
266
267declare global {
268 interface HTMLElementTagNameMap {
269 "sketch-todo-panel": SketchTodoPanel;
270 }
Autoformatter71c73b52025-05-29 20:18:43 +0000271}