agkan 2.12.2 → 2.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/dist/board/boardConfig.d.ts +3 -0
  2. package/dist/board/boardConfig.d.ts.map +1 -1
  3. package/dist/board/boardConfig.js +7 -1
  4. package/dist/board/boardConfig.js.map +1 -1
  5. package/dist/board/boardRenderer.d.ts +1 -1
  6. package/dist/board/boardRenderer.d.ts.map +1 -1
  7. package/dist/board/boardRenderer.js +33 -5
  8. package/dist/board/boardRenderer.js.map +1 -1
  9. package/dist/board/boardRoutes.d.ts +1 -1
  10. package/dist/board/boardRoutes.d.ts.map +1 -1
  11. package/dist/board/boardRoutes.js +76 -5
  12. package/dist/board/boardRoutes.js.map +1 -1
  13. package/dist/board/boardStyles.d.ts +1 -1
  14. package/dist/board/boardStyles.d.ts.map +1 -1
  15. package/dist/board/boardStyles.js +135 -83
  16. package/dist/board/boardStyles.js.map +1 -1
  17. package/dist/board/client/board.js +901 -369
  18. package/dist/cli/commands/block/add.d.ts.map +1 -1
  19. package/dist/cli/commands/block/add.js +156 -110
  20. package/dist/cli/commands/block/add.js.map +1 -1
  21. package/dist/cli/commands/block/list.d.ts.map +1 -1
  22. package/dist/cli/commands/block/list.js +2 -3
  23. package/dist/cli/commands/block/list.js.map +1 -1
  24. package/dist/cli/commands/block/remove.d.ts.map +1 -1
  25. package/dist/cli/commands/block/remove.js +2 -3
  26. package/dist/cli/commands/block/remove.js.map +1 -1
  27. package/dist/cli/commands/comment/add.d.ts.map +1 -1
  28. package/dist/cli/commands/comment/add.js +2 -3
  29. package/dist/cli/commands/comment/add.js.map +1 -1
  30. package/dist/cli/commands/comment/delete.js +2 -2
  31. package/dist/cli/commands/comment/delete.js.map +1 -1
  32. package/dist/cli/commands/comment/list.d.ts.map +1 -1
  33. package/dist/cli/commands/comment/list.js +2 -3
  34. package/dist/cli/commands/comment/list.js.map +1 -1
  35. package/dist/cli/commands/export.d.ts +7 -0
  36. package/dist/cli/commands/export.d.ts.map +1 -0
  37. package/dist/cli/commands/export.js +30 -0
  38. package/dist/cli/commands/export.js.map +1 -0
  39. package/dist/cli/commands/import.d.ts +7 -0
  40. package/dist/cli/commands/import.d.ts.map +1 -0
  41. package/dist/cli/commands/import.js +44 -0
  42. package/dist/cli/commands/import.js.map +1 -0
  43. package/dist/cli/commands/meta/delete.d.ts.map +1 -1
  44. package/dist/cli/commands/meta/delete.js +2 -3
  45. package/dist/cli/commands/meta/delete.js.map +1 -1
  46. package/dist/cli/commands/meta/get.d.ts.map +1 -1
  47. package/dist/cli/commands/meta/get.js +2 -3
  48. package/dist/cli/commands/meta/get.js.map +1 -1
  49. package/dist/cli/commands/meta/list.d.ts.map +1 -1
  50. package/dist/cli/commands/meta/list.js +2 -3
  51. package/dist/cli/commands/meta/list.js.map +1 -1
  52. package/dist/cli/commands/meta/set.d.ts.map +1 -1
  53. package/dist/cli/commands/meta/set.js +2 -3
  54. package/dist/cli/commands/meta/set.js.map +1 -1
  55. package/dist/cli/commands/tag/add.js +2 -2
  56. package/dist/cli/commands/tag/add.js.map +1 -1
  57. package/dist/cli/commands/tag/attach.d.ts.map +1 -1
  58. package/dist/cli/commands/tag/attach.js +2 -4
  59. package/dist/cli/commands/tag/attach.js.map +1 -1
  60. package/dist/cli/commands/tag/delete.js +2 -2
  61. package/dist/cli/commands/tag/delete.js.map +1 -1
  62. package/dist/cli/commands/tag/detach.d.ts.map +1 -1
  63. package/dist/cli/commands/tag/detach.js +2 -4
  64. package/dist/cli/commands/tag/detach.js.map +1 -1
  65. package/dist/cli/commands/tag/list.d.ts.map +1 -1
  66. package/dist/cli/commands/tag/list.js +2 -3
  67. package/dist/cli/commands/tag/list.js.map +1 -1
  68. package/dist/cli/commands/tag/rename.js +2 -2
  69. package/dist/cli/commands/tag/rename.js.map +1 -1
  70. package/dist/cli/commands/tag/show.d.ts.map +1 -1
  71. package/dist/cli/commands/tag/show.js +2 -3
  72. package/dist/cli/commands/tag/show.js.map +1 -1
  73. package/dist/cli/commands/task/add.d.ts.map +1 -1
  74. package/dist/cli/commands/task/add.js +3 -3
  75. package/dist/cli/commands/task/add.js.map +1 -1
  76. package/dist/cli/commands/task/count.js +2 -2
  77. package/dist/cli/commands/task/count.js.map +1 -1
  78. package/dist/cli/commands/task/delete.js +2 -2
  79. package/dist/cli/commands/task/delete.js.map +1 -1
  80. package/dist/cli/commands/task/find.d.ts.map +1 -1
  81. package/dist/cli/commands/task/find.js +2 -3
  82. package/dist/cli/commands/task/find.js.map +1 -1
  83. package/dist/cli/commands/task/get.d.ts.map +1 -1
  84. package/dist/cli/commands/task/get.js +204 -209
  85. package/dist/cli/commands/task/get.js.map +1 -1
  86. package/dist/cli/commands/task/list.d.ts.map +1 -1
  87. package/dist/cli/commands/task/list.js +324 -161
  88. package/dist/cli/commands/task/list.js.map +1 -1
  89. package/dist/cli/commands/task/purge.js +2 -2
  90. package/dist/cli/commands/task/purge.js.map +1 -1
  91. package/dist/cli/commands/task/update-helpers.d.ts +42 -0
  92. package/dist/cli/commands/task/update-helpers.d.ts.map +1 -0
  93. package/dist/cli/commands/task/update-helpers.js +154 -0
  94. package/dist/cli/commands/task/update-helpers.js.map +1 -0
  95. package/dist/cli/commands/task/update-parent.js +2 -2
  96. package/dist/cli/commands/task/update-parent.js.map +1 -1
  97. package/dist/cli/commands/task/update.d.ts.map +1 -1
  98. package/dist/cli/commands/task/update.js +81 -196
  99. package/dist/cli/commands/task/update.js.map +1 -1
  100. package/dist/cli/index.js +6 -0
  101. package/dist/cli/index.js.map +1 -1
  102. package/dist/cli/utils/error-handler.js +3 -3
  103. package/dist/cli/utils/error-handler.js.map +1 -1
  104. package/dist/cli/utils/output-formatter.d.ts +14 -0
  105. package/dist/cli/utils/output-formatter.d.ts.map +1 -1
  106. package/dist/cli/utils/output-formatter.js +36 -0
  107. package/dist/cli/utils/output-formatter.js.map +1 -1
  108. package/dist/cli/utils/service-container.d.ts +25 -0
  109. package/dist/cli/utils/service-container.d.ts.map +1 -0
  110. package/dist/cli/utils/service-container.js +26 -0
  111. package/dist/cli/utils/service-container.js.map +1 -0
  112. package/dist/services/ExportImportService.d.ts +84 -0
  113. package/dist/services/ExportImportService.d.ts.map +1 -0
  114. package/dist/services/ExportImportService.js +222 -0
  115. package/dist/services/ExportImportService.js.map +1 -0
  116. package/dist/services/ProcessService.d.ts +54 -0
  117. package/dist/services/ProcessService.d.ts.map +1 -0
  118. package/dist/services/ProcessService.js +147 -0
  119. package/dist/services/ProcessService.js.map +1 -0
  120. package/dist/services/TaskService.d.ts +14 -0
  121. package/dist/services/TaskService.d.ts.map +1 -1
  122. package/dist/services/TaskService.js +48 -29
  123. package/dist/services/TaskService.js.map +1 -1
  124. package/dist/services/TmuxService.d.ts +2 -0
  125. package/dist/services/TmuxService.d.ts.map +1 -0
  126. package/dist/services/TmuxService.js +7 -0
  127. package/dist/services/TmuxService.js.map +1 -0
  128. package/dist/services/index.d.ts +2 -0
  129. package/dist/services/index.d.ts.map +1 -1
  130. package/dist/services/index.js +3 -1
  131. package/dist/services/index.js.map +1 -1
  132. package/dist/utils/input-validators.d.ts.map +1 -1
  133. package/dist/utils/input-validators.js +64 -69
  134. package/dist/utils/input-validators.js.map +1 -1
  135. package/package.json +3 -3
@@ -149,106 +149,6 @@
149
149
  document.addEventListener("dragend", stopAutoScroll);
150
150
  }
151
151
 
152
- // src/board/client/addTaskModal.ts
153
- function initAddTaskModal() {
154
- const addModal = document.getElementById("add-modal");
155
- const addTitle = document.getElementById("add-title");
156
- const addBody = document.getElementById("add-body");
157
- const addPriority = document.getElementById("add-priority");
158
- const addStatus = document.getElementById("add-status");
159
- document.querySelectorAll(".add-btn").forEach((btn) => {
160
- btn.addEventListener("click", (e) => {
161
- e.stopPropagation();
162
- addStatus.value = btn.dataset.status;
163
- addTitle.value = "";
164
- addBody.value = "";
165
- addPriority.value = "";
166
- addModal.classList.add("show");
167
- addTitle.focus();
168
- });
169
- });
170
- document.getElementById("add-cancel")?.addEventListener("click", () => {
171
- addModal.classList.remove("show");
172
- });
173
- addModal.addEventListener("click", (e) => {
174
- if (e.target === addModal) addModal.classList.remove("show");
175
- });
176
- addTitle.addEventListener("keydown", (e) => {
177
- if (e.key === "Enter" && !e.isComposing) {
178
- e.preventDefault();
179
- document.getElementById("add-submit").click();
180
- }
181
- });
182
- document.getElementById("add-submit")?.addEventListener("click", async () => {
183
- const title = addTitle.value.trim();
184
- if (!title) {
185
- addTitle.focus();
186
- return;
187
- }
188
- const status = addStatus.value;
189
- addModal.classList.remove("show");
190
- try {
191
- const res = await fetch("/api/tasks", {
192
- method: "POST",
193
- headers: { "Content-Type": "application/json" },
194
- body: JSON.stringify({
195
- title,
196
- body: addBody.value.trim() || null,
197
- status,
198
- priority: addPriority.value || null
199
- })
200
- });
201
- if (!res.ok) throw new Error("Server error");
202
- location.reload();
203
- } catch {
204
- showToast("Failed to add task");
205
- }
206
- });
207
- }
208
-
209
- // src/board/client/contextMenu.ts
210
- function initContextMenu() {
211
- const ctxMenu = document.getElementById("context-menu");
212
- let ctxTargetCard = null;
213
- document.addEventListener("contextmenu", (e) => {
214
- const card = e.target.closest(".card");
215
- if (!card) {
216
- ctxMenu.style.display = "none";
217
- return;
218
- }
219
- e.preventDefault();
220
- ctxTargetCard = card;
221
- ctxMenu.style.left = e.clientX + "px";
222
- ctxMenu.style.top = e.clientY + "px";
223
- ctxMenu.style.display = "block";
224
- });
225
- document.addEventListener("click", (e) => {
226
- if (!e.target.closest("#context-menu")) {
227
- ctxMenu.style.display = "none";
228
- ctxTargetCard = null;
229
- }
230
- });
231
- document.getElementById("ctx-delete")?.addEventListener("click", async (e) => {
232
- e.stopPropagation();
233
- ctxMenu.style.display = "none";
234
- if (!ctxTargetCard) return;
235
- const card = ctxTargetCard;
236
- ctxTargetCard = null;
237
- const taskId = card.dataset.id;
238
- const status = card.dataset.status;
239
- if (!confirm("Delete task #" + taskId + "?")) return;
240
- card.remove();
241
- updateCount(status);
242
- try {
243
- const res = await fetch("/api/tasks/" + taskId, { method: "DELETE" });
244
- if (!res.ok) throw new Error("Server error");
245
- } catch {
246
- location.reload();
247
- showToast("Failed to delete task");
248
- }
249
- });
250
- }
251
-
252
152
  // src/board/client/tags.ts
253
153
  var allAvailableTags = [];
254
154
  var _getDetailTaskId = null;
@@ -425,7 +325,7 @@
425
325
  function setLastUpdatedAt(val) {
426
326
  lastUpdatedAt = val;
427
327
  }
428
- var activeFilters = { tagIds: [], priorities: [], assignee: "" };
328
+ var activeFilters = { tagIds: [], priorities: [], assignee: "", searchText: "" };
429
329
  function buildFilterParams() {
430
330
  const params = new URLSearchParams();
431
331
  if (activeFilters.priorities.length > 0) {
@@ -437,17 +337,103 @@
437
337
  if (activeFilters.assignee) {
438
338
  params.set("assignee", activeFilters.assignee);
439
339
  }
340
+ if (activeFilters.searchText) {
341
+ params.set("search", activeFilters.searchText);
342
+ }
440
343
  return params;
441
344
  }
442
345
  var _openTaskDetail = null;
443
346
  var _renderDetailPanel = null;
444
347
  var _showUpdateWarning = null;
445
348
  var _getDetailTaskId2 = null;
349
+ var _setActiveCard = null;
446
350
  function registerDetailPanelCallbacks(callbacks) {
447
351
  _openTaskDetail = callbacks.openTaskDetail;
448
352
  _renderDetailPanel = callbacks.renderDetailPanel;
449
353
  _showUpdateWarning = callbacks.showUpdateWarning;
450
354
  _getDetailTaskId2 = callbacks.getDetailTaskId;
355
+ _setActiveCard = callbacks.setActiveCard;
356
+ }
357
+ function attachCardListeners(body) {
358
+ body.querySelectorAll(".card").forEach((card) => {
359
+ attachDragListeners(card);
360
+ card.addEventListener("click", async (e) => {
361
+ if (e.defaultPrevented) return;
362
+ if (_openTaskDetail) await _openTaskDetail(card.dataset.id);
363
+ });
364
+ });
365
+ }
366
+ function applyIncrementalCardUpdate(body, newHtml) {
367
+ const template = document.createElement("div");
368
+ template.innerHTML = newHtml;
369
+ const newCards = Array.from(template.querySelectorAll(".card"));
370
+ const existingCards = /* @__PURE__ */ new Map();
371
+ body.querySelectorAll(".card").forEach((card) => {
372
+ const id = card.dataset.id;
373
+ if (id) existingCards.set(id, card);
374
+ });
375
+ const newCardIds = /* @__PURE__ */ new Set();
376
+ newCards.forEach((card) => {
377
+ const id = card.dataset.id;
378
+ if (id) newCardIds.add(id);
379
+ });
380
+ existingCards.forEach((card, id) => {
381
+ if (!newCardIds.has(id)) {
382
+ card.remove();
383
+ }
384
+ });
385
+ newCards.forEach((newCard, index) => {
386
+ const id = newCard.dataset.id;
387
+ const newUpdatedAt = newCard.dataset.updatedAt;
388
+ const existing = id ? existingCards.get(id) : void 0;
389
+ if (existing) {
390
+ const existingUpdatedAt = existing.dataset.updatedAt;
391
+ let activeCard;
392
+ if (newUpdatedAt !== existingUpdatedAt) {
393
+ existing.replaceWith(newCard);
394
+ activeCard = newCard;
395
+ } else {
396
+ activeCard = existing;
397
+ }
398
+ const currentChild = body.children[index];
399
+ if (currentChild !== activeCard) {
400
+ body.insertBefore(activeCard, currentChild || null);
401
+ }
402
+ } else {
403
+ const currentChild = body.children[index];
404
+ body.insertBefore(newCard, currentChild || null);
405
+ }
406
+ });
407
+ }
408
+ function updateColumnHtml(col) {
409
+ const body = document.getElementById("col-" + col.status);
410
+ if (!body) return;
411
+ applyIncrementalCardUpdate(body, col.html);
412
+ const colEl = body.closest(".column");
413
+ if (colEl) {
414
+ const countEl = colEl.querySelector(".column-count");
415
+ if (countEl) countEl.textContent = String(col.count);
416
+ }
417
+ attachCardListeners(body);
418
+ attachAutoScrollToBody(body);
419
+ }
420
+ function isEditingDetailPanel() {
421
+ const editableFields = ["detail-edit-title", "detail-edit-body", "detail-edit-status", "detail-edit-priority"];
422
+ return editableFields.some((id) => document.activeElement && document.activeElement.id === id);
423
+ }
424
+ async function refreshOpenDetailPanel(detailTaskId2) {
425
+ if (isEditingDetailPanel()) {
426
+ if (_showUpdateWarning) _showUpdateWarning();
427
+ return;
428
+ }
429
+ try {
430
+ const taskRes = await fetch("/api/tasks/" + detailTaskId2);
431
+ if (taskRes.ok) {
432
+ const taskData = await taskRes.json();
433
+ if (_renderDetailPanel) _renderDetailPanel(taskData);
434
+ }
435
+ } catch {
436
+ }
451
437
  }
452
438
  async function refreshBoardCards() {
453
439
  const filterParams = buildFilterParams();
@@ -456,41 +442,13 @@
456
442
  const res = await fetch(url);
457
443
  if (!res.ok) return;
458
444
  const data = await res.json();
459
- const columns = data.columns;
460
- columns.forEach((col) => {
461
- const body = document.getElementById("col-" + col.status);
462
- if (!body) return;
463
- body.innerHTML = col.html;
464
- const colEl = body.closest(".column");
465
- if (colEl) {
466
- const countEl = colEl.querySelector(".column-count");
467
- if (countEl) countEl.textContent = String(col.count);
468
- }
469
- body.querySelectorAll(".card").forEach((card) => {
470
- attachDragListeners(card);
471
- card.addEventListener("click", async (e) => {
472
- if (e.defaultPrevented) return;
473
- if (_openTaskDetail) await _openTaskDetail(card.dataset.id);
474
- });
475
- });
476
- attachAutoScrollToBody(body);
477
- });
445
+ data.columns.forEach(updateColumnHtml);
478
446
  const detailTaskId2 = _getDetailTaskId2 ? _getDetailTaskId2() : null;
447
+ if (detailTaskId2 !== null && _setActiveCard) {
448
+ _setActiveCard(detailTaskId2);
449
+ }
479
450
  if (detailTaskId2 !== null) {
480
- const editableFields = ["detail-edit-title", "detail-edit-body", "detail-edit-status", "detail-edit-priority"];
481
- const isEditing = editableFields.some((id) => document.activeElement && document.activeElement.id === id);
482
- if (isEditing) {
483
- if (_showUpdateWarning) _showUpdateWarning();
484
- } else {
485
- try {
486
- const taskRes = await fetch("/api/tasks/" + detailTaskId2);
487
- if (taskRes.ok) {
488
- const taskData = await taskRes.json();
489
- if (_renderDetailPanel) _renderDetailPanel(taskData);
490
- }
491
- } catch {
492
- }
493
- }
451
+ await refreshOpenDetailPanel(detailTaskId2);
494
452
  }
495
453
  } catch {
496
454
  }
@@ -502,16 +460,11 @@
502
460
  if (!res.ok) return;
503
461
  const data = await res.json();
504
462
  const ts = data.updatedAt;
505
- const detailPanel = document.getElementById("detail-panel");
506
463
  if (lastUpdatedAt === null) {
507
464
  lastUpdatedAt = ts;
508
465
  } else if (ts !== lastUpdatedAt) {
509
466
  lastUpdatedAt = ts;
510
- if (detailPanel.classList.contains("open")) {
511
- await refreshBoardCards();
512
- } else {
513
- location.reload();
514
- }
467
+ await refreshBoardCards();
515
468
  }
516
469
  } catch {
517
470
  }
@@ -521,16 +474,316 @@
521
474
  pollBoardUpdates();
522
475
  }
523
476
 
477
+ // src/board/client/addTaskModal.ts
478
+ var selectedTags = [];
479
+ var tagInputValue = "";
480
+ var tagFocusedIndex = -1;
481
+ function getFilteredAddTags() {
482
+ const selectedIds = new Set(selectedTags.map((t) => t.id));
483
+ const available = allAvailableTags.filter((t) => !selectedIds.has(t.id));
484
+ if (!tagInputValue.trim()) return available;
485
+ const q = tagInputValue.toLowerCase();
486
+ return available.filter((t) => t.name.toLowerCase().includes(q));
487
+ }
488
+ function renderAddTagPills(control, input) {
489
+ control.querySelectorAll(".tag-pill").forEach((p) => p.remove());
490
+ selectedTags.forEach((t) => {
491
+ const pill = document.createElement("span");
492
+ pill.className = "tag-pill";
493
+ pill.dataset.tagId = String(t.id);
494
+ const label = document.createTextNode(t.name);
495
+ const removeBtn = document.createElement("button");
496
+ removeBtn.className = "tag-pill-remove";
497
+ removeBtn.title = "Remove tag";
498
+ removeBtn.innerHTML = "×";
499
+ removeBtn.addEventListener("click", (e) => {
500
+ e.stopPropagation();
501
+ const idx = selectedTags.findIndex((x) => x.id === t.id);
502
+ if (idx !== -1) selectedTags.splice(idx, 1);
503
+ renderAddTagPills(control, input);
504
+ });
505
+ pill.appendChild(label);
506
+ pill.appendChild(removeBtn);
507
+ control.insertBefore(pill, input);
508
+ });
509
+ input.placeholder = selectedTags.length === 0 ? "Add tags..." : "";
510
+ }
511
+ function renderAddTagDropdown(dropdown) {
512
+ const filtered = getFilteredAddTags();
513
+ dropdown.innerHTML = "";
514
+ tagFocusedIndex = -1;
515
+ if (filtered.length === 0) {
516
+ const noOpt = document.createElement("div");
517
+ noOpt.className = "tag-select-no-options";
518
+ noOpt.textContent = tagInputValue ? "No matching tags" : "No tags available";
519
+ dropdown.appendChild(noOpt);
520
+ } else {
521
+ filtered.forEach((t, i) => {
522
+ const opt = document.createElement("div");
523
+ opt.className = "tag-select-option";
524
+ opt.dataset.tagId = String(t.id);
525
+ opt.textContent = t.name;
526
+ opt.addEventListener("mouseover", () => setAddTagFocused(dropdown, i));
527
+ opt.addEventListener("mousedown", (e) => {
528
+ e.preventDefault();
529
+ selectAddTag(t.id, dropdown, document.getElementById("add-tag-input"));
530
+ });
531
+ dropdown.appendChild(opt);
532
+ });
533
+ }
534
+ }
535
+ function setAddTagFocused(dropdown, index) {
536
+ const opts = dropdown.querySelectorAll(".tag-select-option");
537
+ opts.forEach((o, i) => o.classList.toggle("focused", i === index));
538
+ tagFocusedIndex = index;
539
+ }
540
+ function selectAddTag(tagId, dropdown, input) {
541
+ const tag = allAvailableTags.find((t) => t.id === tagId);
542
+ if (!tag) return;
543
+ selectedTags.push(tag);
544
+ input.value = "";
545
+ tagInputValue = "";
546
+ const control = document.getElementById("add-tag-select-control");
547
+ renderAddTagPills(control, input);
548
+ renderAddTagDropdown(dropdown);
549
+ }
550
+ function initAddTagSelector() {
551
+ const control = document.getElementById("add-tag-select-control");
552
+ const dropdown = document.getElementById("add-tag-select-dropdown");
553
+ if (!control || !dropdown) return;
554
+ const input = document.createElement("input");
555
+ input.className = "tag-select-input";
556
+ input.id = "add-tag-input";
557
+ input.type = "text";
558
+ input.autocomplete = "off";
559
+ control.appendChild(input);
560
+ control.addEventListener("click", () => input.focus());
561
+ input.addEventListener("focus", () => {
562
+ renderAddTagDropdown(dropdown);
563
+ dropdown.classList.add("open");
564
+ });
565
+ input.addEventListener(
566
+ "blur",
567
+ () => setTimeout(() => {
568
+ dropdown.classList.remove("open");
569
+ tagFocusedIndex = -1;
570
+ }, 150)
571
+ );
572
+ input.addEventListener("input", () => {
573
+ tagInputValue = input.value;
574
+ renderAddTagDropdown(dropdown);
575
+ if (!dropdown.classList.contains("open")) dropdown.classList.add("open");
576
+ });
577
+ input.addEventListener("keydown", (e) => {
578
+ const filtered = getFilteredAddTags();
579
+ const opts = dropdown.querySelectorAll(".tag-select-option");
580
+ if (e.key === "ArrowDown") {
581
+ e.preventDefault();
582
+ setAddTagFocused(dropdown, Math.min(tagFocusedIndex + 1, opts.length - 1));
583
+ } else if (e.key === "ArrowUp") {
584
+ e.preventDefault();
585
+ setAddTagFocused(dropdown, Math.max(tagFocusedIndex - 1, 0));
586
+ } else if (e.key === "Enter") {
587
+ e.preventDefault();
588
+ if (tagFocusedIndex >= 0 && filtered[tagFocusedIndex]) {
589
+ selectAddTag(filtered[tagFocusedIndex].id, dropdown, input);
590
+ }
591
+ } else if (e.key === "Escape") {
592
+ dropdown.classList.remove("open");
593
+ input.blur();
594
+ } else if (e.key === "Backspace" && input.value === "" && selectedTags.length > 0) {
595
+ e.preventDefault();
596
+ selectedTags.splice(selectedTags.length - 1, 1);
597
+ renderAddTagPills(control, input);
598
+ renderAddTagDropdown(dropdown);
599
+ }
600
+ });
601
+ }
602
+ function addMetadataRow(container) {
603
+ const row = document.createElement("div");
604
+ row.className = "metadata-row";
605
+ const keyInput = document.createElement("input");
606
+ keyInput.type = "text";
607
+ keyInput.className = "metadata-row-key";
608
+ keyInput.placeholder = "Key";
609
+ const valueInput = document.createElement("input");
610
+ valueInput.type = "text";
611
+ valueInput.className = "metadata-row-value";
612
+ valueInput.placeholder = "Value";
613
+ const removeBtn = document.createElement("button");
614
+ removeBtn.type = "button";
615
+ removeBtn.className = "metadata-row-remove";
616
+ removeBtn.title = "Remove";
617
+ removeBtn.innerHTML = "×";
618
+ removeBtn.addEventListener("click", () => {
619
+ row.remove();
620
+ });
621
+ row.appendChild(keyInput);
622
+ row.appendChild(valueInput);
623
+ row.appendChild(removeBtn);
624
+ container.appendChild(row);
625
+ }
626
+ function collectMetadata(container) {
627
+ const rows = container.querySelectorAll(".metadata-row");
628
+ const result = [];
629
+ rows.forEach((row) => {
630
+ const key = (row.querySelector(".metadata-row-key")?.value ?? "").trim();
631
+ const value = (row.querySelector(".metadata-row-value")?.value ?? "").trim();
632
+ if (key) result.push({ key, value });
633
+ });
634
+ return result;
635
+ }
636
+ function resetAddModal(elements) {
637
+ elements.addTitle.value = "";
638
+ elements.addBody.value = "";
639
+ elements.addPriority.value = "";
640
+ selectedTags = [];
641
+ tagInputValue = "";
642
+ tagFocusedIndex = -1;
643
+ const control = document.getElementById("add-tag-select-control");
644
+ const input = document.getElementById("add-tag-input");
645
+ if (control && input) {
646
+ renderAddTagPills(control, input);
647
+ }
648
+ elements.addMetadataRows.innerHTML = "";
649
+ }
650
+ function openAddModal(elements, status) {
651
+ elements.addStatus.value = status;
652
+ resetAddModal(elements);
653
+ elements.addModal.classList.add("show");
654
+ elements.addTitle.focus();
655
+ }
656
+ async function submitAddTask(elements) {
657
+ const title = elements.addTitle.value.trim();
658
+ if (!title) {
659
+ elements.addTitle.focus();
660
+ return;
661
+ }
662
+ const status = elements.addStatus.value;
663
+ elements.addModal.classList.remove("show");
664
+ const tags = selectedTags.map((t) => t.id);
665
+ const metadata = collectMetadata(elements.addMetadataRows);
666
+ try {
667
+ const res = await fetch("/api/tasks", {
668
+ method: "POST",
669
+ headers: { "Content-Type": "application/json" },
670
+ body: JSON.stringify({
671
+ title,
672
+ body: elements.addBody.value.trim() || null,
673
+ status,
674
+ priority: elements.addPriority.value || null,
675
+ tags: tags.length > 0 ? tags : void 0,
676
+ metadata: metadata.length > 0 ? metadata : void 0
677
+ })
678
+ });
679
+ if (!res.ok) throw new Error("Server error");
680
+ await refreshBoardCards();
681
+ } catch {
682
+ showToast("Failed to add task");
683
+ }
684
+ }
685
+ function initAddTaskModal() {
686
+ const elements = {
687
+ addModal: document.getElementById("add-modal"),
688
+ addTitle: document.getElementById("add-title"),
689
+ addBody: document.getElementById("add-body"),
690
+ addPriority: document.getElementById("add-priority"),
691
+ addStatus: document.getElementById("add-status"),
692
+ addTagControl: document.getElementById("add-tag-select-control"),
693
+ addTagDropdown: document.getElementById("add-tag-select-dropdown"),
694
+ addMetadataRows: document.getElementById("add-metadata-rows")
695
+ };
696
+ initAddTagSelector();
697
+ document.querySelectorAll(".add-btn").forEach((btn) => {
698
+ btn.addEventListener("click", (e) => {
699
+ e.stopPropagation();
700
+ openAddModal(elements, btn.dataset.status);
701
+ });
702
+ });
703
+ document.getElementById("add-cancel")?.addEventListener("click", () => {
704
+ elements.addModal.classList.remove("show");
705
+ });
706
+ elements.addModal.addEventListener("click", (e) => {
707
+ if (e.target === elements.addModal) elements.addModal.classList.remove("show");
708
+ });
709
+ elements.addTitle.addEventListener("keydown", (e) => {
710
+ if (e.key === "Enter" && !e.isComposing) {
711
+ e.preventDefault();
712
+ document.getElementById("add-submit").click();
713
+ }
714
+ });
715
+ document.getElementById("add-metadata-add-row")?.addEventListener("click", () => {
716
+ addMetadataRow(elements.addMetadataRows);
717
+ });
718
+ document.getElementById("add-submit")?.addEventListener("click", () => submitAddTask(elements));
719
+ }
720
+
721
+ // src/board/client/contextMenu.ts
722
+ async function deleteCard(card) {
723
+ const taskId = card.dataset.id;
724
+ const status = card.dataset.status;
725
+ if (!confirm("Delete task #" + taskId + "?")) return;
726
+ card.remove();
727
+ updateCount(status);
728
+ try {
729
+ const res = await fetch("/api/tasks/" + taskId, { method: "DELETE" });
730
+ if (!res.ok) throw new Error("Server error");
731
+ } catch {
732
+ location.reload();
733
+ showToast("Failed to delete task");
734
+ }
735
+ }
736
+ function initContextMenu() {
737
+ const ctxMenu = document.getElementById("context-menu");
738
+ let ctxTargetCard = null;
739
+ document.addEventListener("contextmenu", (e) => {
740
+ const card = e.target.closest(".card");
741
+ if (!card) {
742
+ ctxMenu.style.display = "none";
743
+ return;
744
+ }
745
+ e.preventDefault();
746
+ ctxTargetCard = card;
747
+ ctxMenu.style.left = e.clientX + "px";
748
+ ctxMenu.style.top = e.clientY + "px";
749
+ ctxMenu.style.display = "block";
750
+ });
751
+ document.addEventListener("click", (e) => {
752
+ if (!e.target.closest("#context-menu")) {
753
+ ctxMenu.style.display = "none";
754
+ ctxTargetCard = null;
755
+ }
756
+ });
757
+ document.getElementById("ctx-delete")?.addEventListener("click", async (e) => {
758
+ e.stopPropagation();
759
+ ctxMenu.style.display = "none";
760
+ if (!ctxTargetCard) return;
761
+ const card = ctxTargetCard;
762
+ ctxTargetCard = null;
763
+ await deleteCard(card);
764
+ });
765
+ }
766
+
524
767
  // src/board/client/detailPanel.ts
525
768
  var detailTaskId = null;
526
769
  var lastTab = "details";
527
770
  function getDetailTaskId() {
528
771
  return detailTaskId;
529
772
  }
773
+ function setActiveCard(taskId) {
774
+ document.querySelectorAll(".card.active").forEach((card) => {
775
+ card.classList.remove("active");
776
+ });
777
+ if (taskId !== null) {
778
+ const card = document.querySelector('.card[data-id="' + taskId + '"]');
779
+ if (card) card.classList.add("active");
780
+ }
781
+ }
530
782
  function closeDetailPanel() {
531
783
  const detailPanel = document.getElementById("detail-panel");
532
784
  detailPanel.classList.remove("open");
533
785
  detailPanel.style.width = "";
786
+ setActiveCard(null);
534
787
  detailTaskId = null;
535
788
  }
536
789
  function switchTab(tabName) {
@@ -560,44 +813,56 @@
560
813
  if (pane) pane.innerHTML = '<div style="padding:20px;font-size:12px;color:#94a3b8;">Failed to load comments</div>';
561
814
  }
562
815
  }
816
+ function renderCommentItemHtml(comment, taskId) {
817
+ const authorText = comment.author ? escapeHtmlClient(comment.author) : "Anonymous";
818
+ const dateRel = relativeTime(comment.created_at);
819
+ const dateAbs = escapeHtmlClient(comment.created_at);
820
+ const contentText = escapeHtmlClient(comment.content);
821
+ let html = '<div class="comment-item" data-comment-id="' + comment.id + '">';
822
+ html += '<div class="comment-meta">';
823
+ html += '<span class="comment-author">' + authorText + "</span>";
824
+ html += '<span class="comment-date" title="' + dateAbs + '">' + dateRel + "</span>";
825
+ html += '<span class="comment-actions">';
826
+ html += '<button class="comment-action-btn" title="Edit" data-action="start-comment-edit" data-comment-id="' + comment.id + '">&#9998;</button>';
827
+ html += '<button class="comment-action-btn danger" title="Delete" data-action="delete-comment" data-comment-id="' + comment.id + '" data-task-id="' + taskId + '">&#128465;</button>';
828
+ html += "</span></div>";
829
+ html += '<div class="comment-content" id="comment-content-' + comment.id + '">' + contentText + "</div>";
830
+ html += '<div id="comment-edit-' + comment.id + '" style="display:none;">';
831
+ html += '<textarea class="comment-edit-area" id="comment-edit-area-' + comment.id + '">' + contentText + "</textarea>";
832
+ html += '<div class="comment-edit-actions">';
833
+ html += '<button class="comment-btn" data-action="save-comment-edit" data-comment-id="' + comment.id + '" data-task-id="' + taskId + '">Save</button>';
834
+ html += '<button class="comment-btn" data-action="cancel-comment-edit" data-comment-id="' + comment.id + '">Cancel</button>';
835
+ html += "</div></div></div>";
836
+ return html;
837
+ }
838
+ function renderAddCommentFormHtml(taskId) {
839
+ let html = '<button class="add-comment-trigger" id="add-comment-trigger" data-action="open-add-comment">+ Add comment...</button>';
840
+ html += '<div class="add-comment-form" id="add-comment-form">';
841
+ html += '<textarea class="add-comment-textarea" id="add-comment-text" placeholder="Write a comment..."></textarea>';
842
+ html += "<div>";
843
+ html += '<button class="add-comment-submit" data-action="submit-comment" data-task-id="' + taskId + '">Add Comment</button>';
844
+ html += '<button class="add-comment-cancel" data-action="close-add-comment">Cancel</button>';
845
+ html += "</div></div>";
846
+ return html;
847
+ }
563
848
  function renderComments(taskId, comments) {
564
849
  const pane = document.getElementById("detail-tab-content-comments");
565
850
  if (!pane) return;
566
851
  pane.style.padding = "16px 20px";
567
852
  let html = "";
568
- comments.forEach(function(comment) {
569
- const authorText = comment.author ? escapeHtmlClient(comment.author) : "Anonymous";
570
- const dateRel = relativeTime(comment.created_at);
571
- const dateAbs = escapeHtmlClient(comment.created_at);
572
- const contentText = escapeHtmlClient(comment.content);
573
- html += '<div class="comment-item" data-comment-id="' + comment.id + '">';
574
- html += '<div class="comment-meta">';
575
- html += '<span class="comment-author">' + authorText + "</span>";
576
- html += '<span class="comment-date" title="' + dateAbs + '">' + dateRel + "</span>";
577
- html += '<span class="comment-actions">';
578
- html += '<button class="comment-action-btn" title="Edit" onclick="startCommentEdit(' + comment.id + ')">&#9998;</button>';
579
- html += '<button class="comment-action-btn danger" title="Delete" onclick="deleteComment(' + comment.id + "," + taskId + ')">&#128465;</button>';
580
- html += "</span>";
581
- html += "</div>";
582
- html += '<div class="comment-content" id="comment-content-' + comment.id + '">' + contentText + "</div>";
583
- html += '<div id="comment-edit-' + comment.id + '" style="display:none;">';
584
- html += '<textarea class="comment-edit-area" id="comment-edit-area-' + comment.id + '">' + contentText + "</textarea>";
585
- html += '<div class="comment-edit-actions">';
586
- html += '<button class="comment-btn" onclick="saveCommentEdit(' + comment.id + "," + taskId + ')">Save</button>';
587
- html += '<button class="comment-btn" onclick="cancelCommentEdit(' + comment.id + ')">Cancel</button>';
588
- html += "</div></div>";
589
- html += "</div>";
853
+ comments.forEach((comment) => {
854
+ html += renderCommentItemHtml(comment, taskId);
590
855
  });
591
- html += '<button class="add-comment-trigger" id="add-comment-trigger" onclick="openAddCommentForm()">+ Add comment...</button>';
592
- html += '<div class="add-comment-form" id="add-comment-form">';
593
- html += '<textarea class="add-comment-textarea" id="add-comment-text" placeholder="Write a comment..."></textarea>';
594
- html += "<div>";
595
- html += '<button class="add-comment-submit" onclick="submitComment(' + taskId + ')">Add Comment</button>';
596
- html += '<button class="add-comment-cancel" onclick="closeAddCommentForm()">Cancel</button>';
597
- html += "</div></div>";
856
+ html += renderAddCommentFormHtml(taskId);
598
857
  pane.innerHTML = html;
858
+ const paneEl = pane;
859
+ if (paneEl._commentActionHandler) {
860
+ paneEl.removeEventListener("click", paneEl._commentActionHandler);
861
+ }
862
+ paneEl._commentActionHandler = handleCommentAction;
863
+ paneEl.addEventListener("click", paneEl._commentActionHandler);
599
864
  }
600
- window.openAddCommentForm = function() {
865
+ function openAddCommentForm() {
601
866
  const trigger = document.getElementById("add-comment-trigger");
602
867
  const form = document.getElementById("add-comment-form");
603
868
  if (trigger) trigger.style.display = "none";
@@ -605,8 +870,8 @@
605
870
  form.classList.add("open");
606
871
  form.querySelector("textarea").focus();
607
872
  }
608
- };
609
- window.closeAddCommentForm = function() {
873
+ }
874
+ function closeAddCommentForm() {
610
875
  const trigger = document.getElementById("add-comment-trigger");
611
876
  const form = document.getElementById("add-comment-form");
612
877
  if (trigger) trigger.style.display = "";
@@ -614,22 +879,22 @@
614
879
  form.classList.remove("open");
615
880
  form.querySelector("textarea").value = "";
616
881
  }
617
- };
618
- window.startCommentEdit = function(commentId) {
882
+ }
883
+ function startCommentEdit(commentId) {
619
884
  const contentEl = document.getElementById("comment-content-" + commentId);
620
885
  const editWrapper = document.getElementById("comment-edit-" + commentId);
621
886
  if (contentEl) contentEl.style.display = "none";
622
887
  if (editWrapper) editWrapper.style.display = "block";
623
888
  const area = document.getElementById("comment-edit-area-" + commentId);
624
889
  if (area) area.focus();
625
- };
626
- window.cancelCommentEdit = function(commentId) {
890
+ }
891
+ function cancelCommentEdit(commentId) {
627
892
  const contentEl = document.getElementById("comment-content-" + commentId);
628
893
  const editWrapper = document.getElementById("comment-edit-" + commentId);
629
894
  if (contentEl) contentEl.style.display = "";
630
895
  if (editWrapper) editWrapper.style.display = "none";
631
- };
632
- window.saveCommentEdit = async function(commentId, taskId) {
896
+ }
897
+ async function saveCommentEdit(commentId, taskId) {
633
898
  const area = document.getElementById("comment-edit-area-" + commentId);
634
899
  if (!area) return;
635
900
  const content = area.value.trim();
@@ -648,8 +913,8 @@
648
913
  } catch {
649
914
  showToast("Failed to update comment");
650
915
  }
651
- };
652
- window.deleteComment = async function(commentId, taskId) {
916
+ }
917
+ async function deleteComment(commentId, taskId) {
653
918
  if (!confirm("Delete this comment?")) return;
654
919
  try {
655
920
  const res = await fetch("/api/comments/" + commentId, { method: "DELETE" });
@@ -658,8 +923,8 @@
658
923
  } catch {
659
924
  showToast("Failed to delete comment");
660
925
  }
661
- };
662
- window.submitComment = async function(taskId) {
926
+ }
927
+ async function submitComment(taskId) {
663
928
  const textarea = document.getElementById("add-comment-text");
664
929
  if (!textarea) return;
665
930
  const content = textarea.value.trim();
@@ -678,99 +943,161 @@
678
943
  } catch {
679
944
  showToast("Failed to add comment");
680
945
  }
681
- };
682
- function renderDetailPanel(data) {
683
- document.getElementById("detail-panel-update-warning")?.remove();
684
- const detailPanelTitle = document.getElementById("detail-panel-title");
685
- const task = data.task;
686
- const tags = data.tags || [];
687
- const metadata = data.metadata || [];
688
- const blockedBy = data.blockedBy || [];
689
- const blocking = data.blocking || [];
690
- const parent = data.parent || null;
691
- detailTaskId = task.id;
692
- detailPanelTitle.textContent = "#" + task.id;
693
- const win = window;
694
- const _allStatuses = win.allStatuses;
695
- const _statusLabels = win.statusLabels;
696
- const _allPriorities = win.allPriorities;
697
- let html = "";
698
- html += '<div class="detail-field">';
946
+ }
947
+ function dispatchCommentAction(action, commentId, taskId) {
948
+ switch (action) {
949
+ case "open-add-comment":
950
+ openAddCommentForm();
951
+ break;
952
+ case "close-add-comment":
953
+ closeAddCommentForm();
954
+ break;
955
+ case "start-comment-edit":
956
+ startCommentEdit(commentId);
957
+ break;
958
+ case "cancel-comment-edit":
959
+ cancelCommentEdit(commentId);
960
+ break;
961
+ case "save-comment-edit":
962
+ void saveCommentEdit(commentId, taskId);
963
+ break;
964
+ case "delete-comment":
965
+ void deleteComment(commentId, taskId);
966
+ break;
967
+ case "submit-comment":
968
+ void submitComment(taskId);
969
+ break;
970
+ }
971
+ }
972
+ function handleCommentAction(e) {
973
+ const target = e.target.closest("[data-action]");
974
+ if (!target) return;
975
+ const action = target.dataset.action ?? "";
976
+ const commentId = target.dataset.commentId ? Number(target.dataset.commentId) : NaN;
977
+ const taskId = target.dataset.taskId ? Number(target.dataset.taskId) : NaN;
978
+ dispatchCommentAction(action, commentId, taskId);
979
+ }
980
+ function renderStatusField(currentStatus, allStatuses, statusLabels) {
981
+ let html = '<div class="detail-field">';
699
982
  html += '<div class="detail-field-label">Status</div>';
700
983
  html += '<select id="detail-edit-status" class="detail-edit-select">';
701
- _allStatuses.forEach((s) => {
702
- const selected = s === task.status ? " selected" : "";
703
- html += '<option value="' + s + '"' + selected + ">" + _statusLabels[s] + "</option>";
984
+ allStatuses.forEach((s) => {
985
+ const selected = s === currentStatus ? " selected" : "";
986
+ html += '<option value="' + s + '"' + selected + ">" + statusLabels[s] + "</option>";
704
987
  });
705
- html += "</select>";
706
- html += "</div>";
707
- html += '<div class="detail-field">';
988
+ html += "</select></div>";
989
+ return html;
990
+ }
991
+ function renderPriorityField(currentPriority, allPriorities) {
992
+ let html = '<div class="detail-field">';
708
993
  html += '<div class="detail-field-label">Priority</div>';
709
994
  html += '<select id="detail-edit-priority" class="detail-edit-select">';
710
995
  html += '<option value="">None</option>';
711
- _allPriorities.forEach((p) => {
712
- const selected = task.priority === p ? " selected" : "";
996
+ allPriorities.forEach((p) => {
997
+ const selected = currentPriority === p ? " selected" : "";
713
998
  html += '<option value="' + p + '"' + selected + ">" + p.charAt(0).toUpperCase() + p.slice(1) + "</option>";
714
999
  });
715
- html += "</select>";
716
- html += "</div>";
717
- html += '<div class="detail-field">';
718
- html += '<div class="detail-field-label">Tags</div>';
719
- html += '<div id="detail-tags-container"></div>';
720
- html += "</div>";
721
- const hasRelations = parent || blockedBy.length > 0 || blocking.length > 0;
722
- if (hasRelations) {
723
- html += '<div class="detail-relations">';
724
- if (parent) {
725
- html += '<div class="detail-relation-row">';
726
- html += '<span class="detail-relation-label">Parent</span>';
727
- html += '<div class="detail-relation-ids"><span class="detail-relation-id">#' + parent.id + " " + escapeHtmlClient(parent.title) + "</span></div>";
728
- html += "</div>";
729
- }
730
- if (blockedBy.length > 0) {
731
- html += '<div class="detail-relation-row">';
732
- html += '<span class="detail-relation-label">Blocked by</span>';
733
- html += '<div class="detail-relation-ids">';
734
- blockedBy.forEach((t) => {
735
- html += '<span class="detail-relation-id">#' + t.id + "</span>";
736
- });
737
- html += "</div></div>";
738
- }
739
- if (blocking.length > 0) {
740
- html += '<div class="detail-relation-row">';
741
- html += '<span class="detail-relation-label">Blocking</span>';
742
- html += '<div class="detail-relation-ids">';
743
- blocking.forEach((t) => {
744
- html += '<span class="detail-relation-id">#' + t.id + "</span>";
745
- });
746
- html += "</div></div>";
747
- }
1000
+ html += "</select></div>";
1001
+ return html;
1002
+ }
1003
+ function renderRelationsHtml(parent, blockedBy, blocking) {
1004
+ let html = '<div class="detail-relations">';
1005
+ if (parent) {
1006
+ html += '<div class="detail-relation-row">';
1007
+ html += '<span class="detail-relation-label">Parent</span>';
1008
+ html += '<div class="detail-relation-ids"><span class="detail-relation-id">#' + parent.id + " " + escapeHtmlClient(parent.title) + "</span></div>";
748
1009
  html += "</div>";
749
1010
  }
750
- html += '<div class="detail-field">';
751
- html += '<div class="detail-field-label">Title</div>';
1011
+ if (blockedBy.length > 0) {
1012
+ html += '<div class="detail-relation-row"><span class="detail-relation-label">Blocked by</span>';
1013
+ html += '<div class="detail-relation-ids">';
1014
+ blockedBy.forEach((t) => {
1015
+ html += '<span class="detail-relation-id">#' + t.id + "</span>";
1016
+ });
1017
+ html += "</div></div>";
1018
+ }
1019
+ if (blocking.length > 0) {
1020
+ html += '<div class="detail-relation-row"><span class="detail-relation-label">Blocking</span>';
1021
+ html += '<div class="detail-relation-ids">';
1022
+ blocking.forEach((t) => {
1023
+ html += '<span class="detail-relation-id">#' + t.id + "</span>";
1024
+ });
1025
+ html += "</div></div>";
1026
+ }
1027
+ html += "</div>";
1028
+ return html;
1029
+ }
1030
+ function autoResizeTextarea(el) {
1031
+ el.style.height = "auto";
1032
+ el.style.height = el.scrollHeight + "px";
1033
+ }
1034
+ function renderMetadataTable(metadata) {
1035
+ const otherMeta = metadata.filter((m) => m.key !== "priority");
1036
+ if (otherMeta.length === 0) return "";
1037
+ let html = '<div class="detail-field"><div class="detail-field-label">Metadata</div>';
1038
+ html += '<table class="detail-meta-table">';
1039
+ otherMeta.forEach((m) => {
1040
+ html += "<tr><td>" + escapeHtmlClient(m.key) + "</td><td>" + escapeHtmlClient(m.value) + "</td></tr>";
1041
+ });
1042
+ html += "</table></div>";
1043
+ return html;
1044
+ }
1045
+ function renderEditableTextFields(task) {
1046
+ let html = '<div class="detail-field"><div class="detail-field-label">Title</div>';
752
1047
  html += '<input id="detail-edit-title" class="detail-edit-input" type="text" value="' + escapeHtmlClient(task.title) + '">';
753
1048
  html += "</div>";
754
- html += '<div class="detail-field description-field-wrapper">';
755
- html += '<div class="detail-field-label">Description</div>';
1049
+ html += '<div class="detail-field description-field-wrapper"><div class="detail-field-label">Description</div>';
756
1050
  html += '<textarea id="detail-edit-body" class="detail-edit-textarea">' + escapeHtmlClient(task.body || "") + "</textarea>";
757
1051
  html += "</div>";
758
- const otherMeta = metadata.filter((m) => m.key !== "priority");
759
- if (otherMeta.length > 0) {
760
- html += '<div class="detail-field">';
761
- html += '<div class="detail-field-label">Metadata</div>';
762
- html += '<table class="detail-meta-table">';
763
- otherMeta.forEach((m) => {
764
- html += "<tr><td>" + escapeHtmlClient(m.key) + "</td><td>" + escapeHtmlClient(m.value) + "</td></tr>";
765
- });
766
- html += "</table></div>";
1052
+ return html;
1053
+ }
1054
+ function renderDetailPanelHtml(data) {
1055
+ const task = data.task;
1056
+ const metadata = data.metadata || [];
1057
+ const blockedBy = data.blockedBy || [];
1058
+ const blocking = data.blocking || [];
1059
+ const parent = data.parent || null;
1060
+ const win = window;
1061
+ const allStatuses = win.allStatuses;
1062
+ const statusLabels = win.statusLabels;
1063
+ const allPriorities = win.allPriorities;
1064
+ let html = "";
1065
+ html += renderStatusField(task.status, allStatuses, statusLabels);
1066
+ html += renderPriorityField(task.priority, allPriorities);
1067
+ html += '<div class="detail-field"><div class="detail-field-label">Tags</div>';
1068
+ html += '<div id="detail-tags-container"></div></div>';
1069
+ const hasRelations = parent || blockedBy.length > 0 || blocking.length > 0;
1070
+ if (hasRelations) {
1071
+ html += renderRelationsHtml(parent, blockedBy, blocking);
767
1072
  }
768
- html += '<div class="detail-timestamp">created ' + relativeTime(task.created_at) + " &middot; updated " + relativeTime(task.updated_at) + "</div>";
1073
+ html += renderMetadataTable(metadata);
1074
+ html += renderEditableTextFields(task);
1075
+ return html;
1076
+ }
1077
+ function renderDetailPanel(data) {
1078
+ document.getElementById("detail-panel-update-warning")?.remove();
1079
+ const detailPanelTitle = document.getElementById("detail-panel-title");
1080
+ const task = data.task;
1081
+ const tags = data.tags || [];
1082
+ detailTaskId = task.id;
1083
+ detailPanelTitle.textContent = "#" + task.id;
769
1084
  const detailsPane = document.getElementById("detail-tab-content-details");
770
1085
  if (detailsPane) {
771
- detailsPane.innerHTML = html;
1086
+ detailsPane.innerHTML = renderDetailPanelHtml(data);
772
1087
  detailsPane.style.padding = "20px";
773
1088
  }
1089
+ const footer = document.getElementById("detail-panel-footer");
1090
+ if (footer) {
1091
+ footer.innerHTML = '<span class="detail-footer-timestamp">created ' + relativeTime(task.created_at) + " &middot; updated " + relativeTime(task.updated_at) + '</span><button id="detail-save-btn">Save</button>';
1092
+ document.getElementById("detail-save-btn")?.addEventListener("click", saveDetailTask);
1093
+ }
1094
+ const textarea = document.getElementById("detail-edit-body");
1095
+ if (textarea) {
1096
+ autoResizeTextarea(textarea);
1097
+ textarea.addEventListener("input", () => {
1098
+ autoResizeTextarea(textarea);
1099
+ });
1100
+ }
774
1101
  loadAllTags().then(() => renderTagsSection([...tags])).catch((err) => {
775
1102
  console.error("[agkan] renderDetailPanel tags failed", err);
776
1103
  });
@@ -779,14 +1106,15 @@
779
1106
  }
780
1107
  async function openTaskDetail(taskId) {
781
1108
  const detailPanel = document.getElementById("detail-panel");
782
- const PANEL_DEFAULT_WIDTH = 400;
1109
+ const PANEL_DEFAULT_WIDTH2 = 400;
783
1110
  try {
784
1111
  const res = await fetch("/api/tasks/" + taskId);
785
1112
  if (!res.ok) throw new Error("Server error");
786
1113
  const data = await res.json();
787
1114
  renderDetailPanel(data);
1115
+ setActiveCard(Number(taskId));
788
1116
  if (!detailPanel.classList.contains("open")) {
789
- const preferredWidth = detailPanel.dataset.preferredWidth || String(PANEL_DEFAULT_WIDTH);
1117
+ const preferredWidth = detailPanel.dataset.preferredWidth || String(PANEL_DEFAULT_WIDTH2);
790
1118
  detailPanel.style.width = preferredWidth + "px";
791
1119
  detailPanel.classList.add("open");
792
1120
  }
@@ -805,55 +1133,100 @@
805
1133
  const msgSpan = document.createElement("span");
806
1134
  msgSpan.style.cssText = "flex: 1;";
807
1135
  msgSpan.textContent = "This task has been updated in the database. Save or discard your changes to see the latest version.";
808
- const reloadBtn = document.createElement("button");
809
- reloadBtn.title = "Reload latest data";
810
- reloadBtn.textContent = "\u21BA";
811
- reloadBtn.style.cssText = "background: none; border: none; cursor: pointer; font-size: 1.1em; color: red; padding: 0 2px; line-height: 1; flex-shrink: 0;";
812
- reloadBtn.addEventListener("click", async () => {
813
- try {
814
- const taskRes = await fetch("/api/tasks/" + detailTaskId);
815
- if (taskRes.ok) {
816
- const taskData = await taskRes.json();
817
- renderDetailPanel(taskData);
818
- }
819
- } catch {
820
- }
821
- });
1136
+ const reloadBtn = buildUpdateWarningReloadBtn();
822
1137
  warningEl.appendChild(msgSpan);
823
1138
  warningEl.appendChild(reloadBtn);
824
1139
  detailPanelBody.insertBefore(warningEl, detailPanelBody.firstChild);
825
1140
  }
826
1141
  }
827
- function initDetailPanel() {
828
- const boardContainer = document.querySelector(".board-container");
829
- const detailPanelHtml = '<div class="detail-panel" id="detail-panel"><div class="detail-panel-resize-handle" id="detail-panel-resize-handle"></div><div class="detail-panel-header"><h2 id="detail-panel-title">Task Detail</h2><button class="detail-panel-close" id="detail-panel-close" title="Close">&times;</button></div><div class="detail-tabs" id="detail-tabs"><button class="detail-tab active" data-tab="details">Details</button><button class="detail-tab" data-tab="comments" id="detail-tab-comments">Comments</button></div><div class="detail-panel-body" id="detail-panel-body"><div class="detail-tab-content active" id="detail-tab-content-details"></div><div class="detail-tab-content" id="detail-tab-content-comments"></div></div><div class="detail-panel-footer" id="detail-panel-footer"><button id="detail-save-btn">Save</button></div></div>';
830
- boardContainer.insertAdjacentHTML("beforeend", detailPanelHtml);
831
- const detailPanel = document.getElementById("detail-panel");
832
- document.getElementById("detail-panel-close")?.addEventListener("click", closeDetailPanel);
833
- document.getElementById("detail-tabs")?.addEventListener("click", (e) => {
834
- const btn = e.target.closest(".detail-tab");
835
- if (!btn) return;
836
- switchTab(btn.dataset.tab);
837
- });
838
- const resizeHandle = document.getElementById("detail-panel-resize-handle");
839
- const PANEL_MIN_WIDTH = 280;
840
- const PANEL_MAX_WIDTH = 800;
841
- const PANEL_DEFAULT_WIDTH = 400;
842
- (async function initPanelWidth() {
843
- let targetWidth = PANEL_DEFAULT_WIDTH;
1142
+ function buildUpdateWarningReloadBtn() {
1143
+ const reloadBtn = document.createElement("button");
1144
+ reloadBtn.title = "Reload latest data";
1145
+ reloadBtn.textContent = "\u21BA";
1146
+ reloadBtn.style.cssText = "background: none; border: none; cursor: pointer; font-size: 1.1em; color: red; padding: 0 2px; line-height: 1; flex-shrink: 0;";
1147
+ reloadBtn.addEventListener("click", async () => {
844
1148
  try {
845
- const res = await fetch("/api/config");
846
- if (res.ok) {
847
- const data = await res.json();
848
- const savedWidth = data && data.board && data.board.detailPaneWidth;
849
- if (typeof savedWidth === "number" && savedWidth >= PANEL_MIN_WIDTH && savedWidth <= PANEL_MAX_WIDTH) {
850
- targetWidth = savedWidth;
851
- }
1149
+ const taskRes = await fetch("/api/tasks/" + detailTaskId);
1150
+ if (taskRes.ok) {
1151
+ const taskData = await taskRes.json();
1152
+ renderDetailPanel(taskData);
852
1153
  }
853
1154
  } catch {
854
1155
  }
855
- detailPanel.dataset.preferredWidth = String(targetWidth);
856
- })();
1156
+ });
1157
+ return reloadBtn;
1158
+ }
1159
+ function collectEditedTaskFields() {
1160
+ const titleInput = document.getElementById("detail-edit-title");
1161
+ const title = titleInput ? titleInput.value.trim() : "";
1162
+ if (!title) {
1163
+ if (titleInput) titleInput.focus();
1164
+ return null;
1165
+ }
1166
+ const bodyEl = document.getElementById("detail-edit-body");
1167
+ const statusEl = document.getElementById("detail-edit-status");
1168
+ const priorityEl = document.getElementById("detail-edit-priority");
1169
+ return {
1170
+ title,
1171
+ body: bodyEl ? bodyEl.value.trim() || null : null,
1172
+ status: statusEl ? statusEl.value : void 0,
1173
+ priority: priorityEl ? priorityEl.value || null : null
1174
+ };
1175
+ }
1176
+ async function patchAndReloadDetail(taskId, fields) {
1177
+ const res = await fetch("/api/tasks/" + taskId, {
1178
+ method: "PATCH",
1179
+ headers: { "Content-Type": "application/json" },
1180
+ body: JSON.stringify(fields)
1181
+ });
1182
+ if (!res.ok) throw new Error("Server error");
1183
+ const getRes = await fetch("/api/tasks/" + taskId);
1184
+ if (!getRes.ok) throw new Error("Failed to fetch updated task");
1185
+ const data = await getRes.json();
1186
+ renderDetailPanel(data);
1187
+ }
1188
+ async function saveDetailTask() {
1189
+ if (detailTaskId === null) return;
1190
+ const fields = collectEditedTaskFields();
1191
+ if (!fields) return;
1192
+ try {
1193
+ await patchAndReloadDetail(detailTaskId, fields);
1194
+ showToast("Task saved successfully");
1195
+ await syncTimestampAfterSave();
1196
+ refreshBoardCards();
1197
+ } catch {
1198
+ showToast("Failed to update task");
1199
+ }
1200
+ }
1201
+ async function syncTimestampAfterSave() {
1202
+ try {
1203
+ const tsRes = await fetch("/api/board/updated-at");
1204
+ if (tsRes.ok) {
1205
+ const tsData = await tsRes.json();
1206
+ setLastUpdatedAt(tsData.updatedAt);
1207
+ }
1208
+ } catch {
1209
+ }
1210
+ }
1211
+ var PANEL_MIN_WIDTH = 280;
1212
+ var PANEL_MAX_WIDTH = 800;
1213
+ var PANEL_DEFAULT_WIDTH = 400;
1214
+ async function initPanelWidthFromConfig(detailPanel) {
1215
+ let targetWidth = PANEL_DEFAULT_WIDTH;
1216
+ try {
1217
+ const res = await fetch("/api/config");
1218
+ if (res.ok) {
1219
+ const data = await res.json();
1220
+ const savedWidth = data && data.board && data.board.detailPaneWidth;
1221
+ if (typeof savedWidth === "number" && savedWidth >= PANEL_MIN_WIDTH && savedWidth <= PANEL_MAX_WIDTH) {
1222
+ targetWidth = savedWidth;
1223
+ }
1224
+ }
1225
+ } catch {
1226
+ }
1227
+ detailPanel.dataset.preferredWidth = String(targetWidth);
1228
+ }
1229
+ function attachResizeMousedown(resizeHandle, detailPanel) {
857
1230
  resizeHandle.addEventListener("mousedown", function(e) {
858
1231
  e.preventDefault();
859
1232
  if (!detailPanel.classList.contains("open")) return;
@@ -863,8 +1236,8 @@
863
1236
  document.body.style.userSelect = "none";
864
1237
  document.body.style.cursor = "col-resize";
865
1238
  detailPanel.style.transition = "none";
866
- function onMouseMove(e2) {
867
- const delta = startX - e2.clientX;
1239
+ function onMouseMove(ev) {
1240
+ const delta = startX - ev.clientX;
868
1241
  const newWidth = Math.min(PANEL_MAX_WIDTH, Math.max(PANEL_MIN_WIDTH, startWidth + delta));
869
1242
  detailPanel.style.width = newWidth + "px";
870
1243
  }
@@ -887,47 +1260,31 @@
887
1260
  document.addEventListener("mousemove", onMouseMove);
888
1261
  document.addEventListener("mouseup", onMouseUp);
889
1262
  });
890
- document.getElementById("detail-save-btn")?.addEventListener("click", async () => {
891
- if (detailTaskId === null) return;
892
- const titleInput = document.getElementById("detail-edit-title");
893
- const title = titleInput ? titleInput.value.trim() : "";
894
- if (!title) {
895
- if (titleInput) titleInput.focus();
896
- return;
897
- }
898
- const bodyEl = document.getElementById("detail-edit-body");
899
- const statusEl = document.getElementById("detail-edit-status");
900
- const priorityEl = document.getElementById("detail-edit-priority");
901
- try {
902
- const res = await fetch("/api/tasks/" + detailTaskId, {
903
- method: "PATCH",
904
- headers: { "Content-Type": "application/json" },
905
- body: JSON.stringify({
906
- title,
907
- body: bodyEl ? bodyEl.value.trim() || null : null,
908
- status: statusEl ? statusEl.value : void 0,
909
- priority: priorityEl ? priorityEl.value || null : null
910
- })
911
- });
912
- if (!res.ok) throw new Error("Server error");
913
- const getRes = await fetch("/api/tasks/" + detailTaskId);
914
- if (!getRes.ok) throw new Error("Failed to fetch updated task");
915
- const data = await getRes.json();
916
- renderDetailPanel(data);
917
- showToast("Task saved successfully");
918
- try {
919
- const tsRes = await fetch("/api/board/updated-at");
920
- if (tsRes.ok) {
921
- const tsData = await tsRes.json();
922
- setLastUpdatedAt(tsData.updatedAt);
923
- }
924
- } catch {
925
- }
926
- refreshBoardCards();
927
- } catch {
928
- showToast("Failed to update task");
1263
+ }
1264
+ function initPanelResize(detailPanel) {
1265
+ const resizeHandle = document.getElementById("detail-panel-resize-handle");
1266
+ initPanelWidthFromConfig(detailPanel);
1267
+ attachResizeMousedown(resizeHandle, detailPanel);
1268
+ }
1269
+ function buildDetailPanelHtml() {
1270
+ return '<div class="detail-panel" id="detail-panel"><div class="detail-panel-resize-handle" id="detail-panel-resize-handle"></div><div class="detail-panel-header"><h2 id="detail-panel-title">Task Detail</h2><button class="detail-panel-close" id="detail-panel-close" title="Close">&times;</button></div><div class="detail-tabs" id="detail-tabs"><button class="detail-tab active" data-tab="details">Details</button><button class="detail-tab" data-tab="comments" id="detail-tab-comments">Comments</button></div><div class="detail-panel-body" id="detail-panel-body"><div class="detail-tab-content active" id="detail-tab-content-details"></div><div class="detail-tab-content" id="detail-tab-content-comments"></div></div><div class="detail-panel-footer" id="detail-panel-footer"><button id="detail-save-btn">Save</button></div></div>';
1271
+ }
1272
+ function initDetailPanel() {
1273
+ const boardContainer = document.querySelector(".board-container");
1274
+ boardContainer.insertAdjacentHTML("beforeend", buildDetailPanelHtml());
1275
+ const detailPanel = document.getElementById("detail-panel");
1276
+ document.getElementById("detail-panel-close")?.addEventListener("click", closeDetailPanel);
1277
+ document.addEventListener("keydown", (e) => {
1278
+ if (e.key === "Escape" && detailPanel.classList.contains("open")) {
1279
+ closeDetailPanel();
929
1280
  }
930
1281
  });
1282
+ document.getElementById("detail-tabs")?.addEventListener("click", (e) => {
1283
+ const btn = e.target.closest(".detail-tab");
1284
+ if (!btn) return;
1285
+ switchTab(btn.dataset.tab);
1286
+ });
1287
+ initPanelResize(detailPanel);
931
1288
  document.querySelectorAll(".card").forEach((card) => {
932
1289
  card.addEventListener("click", async (e) => {
933
1290
  if (e.defaultPrevented) return;
@@ -938,14 +1295,15 @@
938
1295
  openTaskDetail,
939
1296
  renderDetailPanel,
940
1297
  showUpdateWarning,
941
- getDetailTaskId
1298
+ getDetailTaskId,
1299
+ setActiveCard
942
1300
  });
943
1301
  registerGetDetailTaskId(getDetailTaskId);
944
1302
  }
945
1303
 
946
1304
  // src/board/client/filters.ts
947
1305
  function isFiltersActive() {
948
- return activeFilters.priorities.length > 0 || activeFilters.tagIds.length > 0 || activeFilters.assignee !== "";
1306
+ return activeFilters.priorities.length > 0 || activeFilters.tagIds.length > 0 || activeFilters.assignee !== "" || activeFilters.searchText !== "";
949
1307
  }
950
1308
  function applyFilters() {
951
1309
  const clearBtn = document.getElementById("filter-clear");
@@ -998,6 +1356,17 @@
998
1356
  applyFilters();
999
1357
  });
1000
1358
  });
1359
+ const searchInput = document.getElementById("filter-search");
1360
+ let searchTimer = null;
1361
+ if (searchInput) {
1362
+ searchInput.addEventListener("input", () => {
1363
+ if (searchTimer) clearTimeout(searchTimer);
1364
+ searchTimer = setTimeout(() => {
1365
+ activeFilters.searchText = searchInput.value.trim();
1366
+ applyFilters();
1367
+ }, 300);
1368
+ });
1369
+ }
1001
1370
  const assigneeInput = document.getElementById("filter-assignee");
1002
1371
  let assigneeTimer = null;
1003
1372
  if (assigneeInput) {
@@ -1015,7 +1384,9 @@
1015
1384
  activeFilters.tagIds = [];
1016
1385
  activeFilters.priorities = [];
1017
1386
  activeFilters.assignee = "";
1387
+ activeFilters.searchText = "";
1018
1388
  document.querySelectorAll(".filter-priority-btn").forEach((btn) => btn.classList.remove("active"));
1389
+ if (searchInput) searchInput.value = "";
1019
1390
  if (assigneeInput) assigneeInput.value = "";
1020
1391
  renderFilterTagPills();
1021
1392
  applyFilters();
@@ -1082,10 +1453,71 @@
1082
1453
  });
1083
1454
  }
1084
1455
 
1456
+ // src/board/client/darkMode.ts
1457
+ function applyTheme(preference) {
1458
+ if (preference === "dark") {
1459
+ document.documentElement.setAttribute("data-theme", "dark");
1460
+ } else if (preference === "light") {
1461
+ document.documentElement.setAttribute("data-theme", "light");
1462
+ } else {
1463
+ document.documentElement.removeAttribute("data-theme");
1464
+ }
1465
+ }
1466
+ async function persistThemeToServer(theme) {
1467
+ try {
1468
+ await fetch("/api/config", {
1469
+ method: "PUT",
1470
+ headers: { "Content-Type": "application/json" },
1471
+ body: JSON.stringify({ board: { theme } })
1472
+ });
1473
+ } catch {
1474
+ }
1475
+ }
1476
+ function getActivePreference() {
1477
+ const ssrTheme = document.documentElement.getAttribute("data-theme");
1478
+ if (ssrTheme === "dark" || ssrTheme === "light") return ssrTheme;
1479
+ return "system";
1480
+ }
1481
+ function updateCheckmarks(active) {
1482
+ const items = {
1483
+ dark: "burger-theme-dark",
1484
+ light: "burger-theme-light",
1485
+ system: "burger-theme-system"
1486
+ };
1487
+ for (const [pref, id] of Object.entries(items)) {
1488
+ const el = document.getElementById(id);
1489
+ if (!el) continue;
1490
+ if (pref === active) {
1491
+ if (!el.textContent?.startsWith("\u2713 ")) {
1492
+ el.textContent = "\u2713 " + el.textContent?.replace(/^\u2713 /, "");
1493
+ }
1494
+ } else {
1495
+ el.textContent = el.textContent?.replace(/^\u2713 /, "") ?? el.textContent;
1496
+ }
1497
+ }
1498
+ }
1499
+ function initDarkMode() {
1500
+ const activePreference = getActivePreference();
1501
+ updateCheckmarks(activePreference);
1502
+ document.getElementById("burger-theme-dark")?.addEventListener("click", () => {
1503
+ applyTheme("dark");
1504
+ updateCheckmarks("dark");
1505
+ void persistThemeToServer("dark");
1506
+ });
1507
+ document.getElementById("burger-theme-light")?.addEventListener("click", () => {
1508
+ applyTheme("light");
1509
+ updateCheckmarks("light");
1510
+ void persistThemeToServer("light");
1511
+ });
1512
+ document.getElementById("burger-theme-system")?.addEventListener("click", () => {
1513
+ applyTheme("system");
1514
+ updateCheckmarks("system");
1515
+ void persistThemeToServer("system");
1516
+ });
1517
+ }
1518
+
1085
1519
  // src/board/client/burgerMenu.ts
1086
- function initBurgerMenu() {
1087
- const burgerBtn = document.getElementById("burger-menu-btn");
1088
- const burgerDropdown = document.getElementById("burger-menu-dropdown");
1520
+ function initBurgerToggle(burgerBtn, burgerDropdown) {
1089
1521
  burgerBtn.addEventListener("click", (e) => {
1090
1522
  e.stopPropagation();
1091
1523
  burgerDropdown.classList.toggle("open");
@@ -1095,6 +1527,21 @@
1095
1527
  burgerDropdown.classList.remove("open");
1096
1528
  }
1097
1529
  });
1530
+ }
1531
+ async function executePurge() {
1532
+ try {
1533
+ const res = await fetch("/api/tasks/purge", {
1534
+ method: "POST",
1535
+ headers: { "Content-Type": "application/json" },
1536
+ body: JSON.stringify({})
1537
+ });
1538
+ if (res.ok) {
1539
+ await refreshBoardCards();
1540
+ }
1541
+ } catch {
1542
+ }
1543
+ }
1544
+ function initPurgeModal(burgerDropdown) {
1098
1545
  const purgeModal = document.getElementById("purge-confirm-modal");
1099
1546
  const purgeConfirmBtn = document.getElementById("purge-confirm-btn");
1100
1547
  const purgeCancelBtn = document.getElementById("purge-cancel-btn");
@@ -1107,32 +1554,12 @@
1107
1554
  purgeCancelBtn.addEventListener("click", () => {
1108
1555
  purgeModal.classList.remove("show");
1109
1556
  });
1110
- purgeConfirmBtn.addEventListener("click", async () => {
1111
- purgeConfirmBtn.disabled = true;
1112
- purgeConfirmBtn.textContent = "Purging...";
1113
- try {
1114
- const res = await fetch("/api/tasks/purge", {
1115
- method: "POST",
1116
- headers: { "Content-Type": "application/json" },
1117
- body: JSON.stringify({})
1118
- });
1119
- const data = await res.json();
1120
- if (res.ok) {
1121
- purgeResultEl.textContent = "Purged " + data.count + " task(s).";
1122
- setTimeout(() => {
1123
- purgeModal.classList.remove("show");
1124
- }, 1500);
1125
- location.reload();
1126
- } else {
1127
- purgeResultEl.textContent = "Error: " + (data.error || "Unknown error");
1128
- }
1129
- } catch {
1130
- purgeResultEl.textContent = "Failed to purge tasks.";
1131
- } finally {
1132
- purgeConfirmBtn.disabled = false;
1133
- purgeConfirmBtn.textContent = "Purge";
1134
- }
1557
+ purgeConfirmBtn.addEventListener("click", () => {
1558
+ purgeModal.classList.remove("show");
1559
+ void executePurge();
1135
1560
  });
1561
+ }
1562
+ function initVersionModal(burgerDropdown) {
1136
1563
  const versionModal = document.getElementById("version-info-modal");
1137
1564
  const versionCloseBtn = document.getElementById("version-info-close");
1138
1565
  const versionTextEl = document.getElementById("version-info-text");
@@ -1152,6 +1579,111 @@
1152
1579
  versionModal.classList.remove("show");
1153
1580
  });
1154
1581
  }
1582
+ function initExportModal(burgerDropdown) {
1583
+ document.getElementById("burger-export-tasks")?.addEventListener("click", () => {
1584
+ burgerDropdown.classList.remove("open");
1585
+ const a = document.createElement("a");
1586
+ a.href = "/api/export";
1587
+ a.download = "";
1588
+ document.body.appendChild(a);
1589
+ a.click();
1590
+ document.body.removeChild(a);
1591
+ });
1592
+ }
1593
+ function initImportModal(burgerDropdown) {
1594
+ const importModal = document.getElementById("import-modal");
1595
+ const importCancelBtn = document.getElementById("import-cancel-btn");
1596
+ const importConfirmBtn = document.getElementById("import-confirm-btn");
1597
+ const importResultEl = document.getElementById("import-result");
1598
+ const importDropZone = document.getElementById("import-drop-zone");
1599
+ const importFileInput = document.getElementById("import-file-input");
1600
+ if (!importModal || !importCancelBtn || !importConfirmBtn || !importResultEl || !importDropZone || !importFileInput) {
1601
+ return;
1602
+ }
1603
+ const safeImportModal = importModal;
1604
+ const safeImportCancelBtn = importCancelBtn;
1605
+ const safeImportConfirmBtn = importConfirmBtn;
1606
+ const safeImportResultEl = importResultEl;
1607
+ const safeImportDropZone = importDropZone;
1608
+ const safeImportFileInput = importFileInput;
1609
+ let selectedFile = null;
1610
+ function setFile(file) {
1611
+ selectedFile = file;
1612
+ safeImportResultEl.textContent = `Selected: ${file.name}`;
1613
+ safeImportResultEl.style.color = "#64748b";
1614
+ safeImportConfirmBtn.disabled = false;
1615
+ }
1616
+ document.getElementById("burger-import-tasks")?.addEventListener("click", () => {
1617
+ burgerDropdown.classList.remove("open");
1618
+ selectedFile = null;
1619
+ safeImportResultEl.textContent = "";
1620
+ safeImportConfirmBtn.disabled = true;
1621
+ safeImportFileInput.value = "";
1622
+ safeImportModal.classList.add("show");
1623
+ });
1624
+ safeImportCancelBtn.addEventListener("click", () => {
1625
+ safeImportModal.classList.remove("show");
1626
+ });
1627
+ safeImportFileInput.addEventListener("change", () => {
1628
+ const file = safeImportFileInput.files?.[0];
1629
+ if (file) setFile(file);
1630
+ });
1631
+ safeImportDropZone.addEventListener("dragover", (e) => {
1632
+ e.preventDefault();
1633
+ safeImportDropZone.style.borderColor = "#3b82f6";
1634
+ });
1635
+ safeImportDropZone.addEventListener("dragleave", () => {
1636
+ safeImportDropZone.style.borderColor = "#94a3b8";
1637
+ });
1638
+ safeImportDropZone.addEventListener("drop", (e) => {
1639
+ e.preventDefault();
1640
+ safeImportDropZone.style.borderColor = "#94a3b8";
1641
+ const file = e.dataTransfer?.files?.[0];
1642
+ if (file) setFile(file);
1643
+ });
1644
+ safeImportConfirmBtn.addEventListener("click", async () => {
1645
+ if (!selectedFile) return;
1646
+ safeImportConfirmBtn.disabled = true;
1647
+ safeImportConfirmBtn.textContent = "Importing...";
1648
+ try {
1649
+ const text = await selectedFile.text();
1650
+ const data = JSON.parse(text);
1651
+ const res = await fetch("/api/import", {
1652
+ method: "POST",
1653
+ headers: { "Content-Type": "application/json" },
1654
+ body: JSON.stringify(data)
1655
+ });
1656
+ const result = await res.json();
1657
+ if (res.ok) {
1658
+ safeImportResultEl.textContent = `Imported ${result.importedCount} task(s) successfully.`;
1659
+ safeImportResultEl.style.color = "#16a34a";
1660
+ setTimeout(() => {
1661
+ safeImportModal.classList.remove("show");
1662
+ location.reload();
1663
+ }, 1500);
1664
+ } else {
1665
+ safeImportResultEl.textContent = "Error: " + (result.error || "Unknown error");
1666
+ safeImportResultEl.style.color = "#dc2626";
1667
+ }
1668
+ } catch {
1669
+ safeImportResultEl.textContent = "Failed to import tasks. Invalid JSON file.";
1670
+ safeImportResultEl.style.color = "#dc2626";
1671
+ } finally {
1672
+ safeImportConfirmBtn.disabled = false;
1673
+ safeImportConfirmBtn.textContent = "Import";
1674
+ }
1675
+ });
1676
+ }
1677
+ function initBurgerMenu() {
1678
+ const burgerBtn = document.getElementById("burger-menu-btn");
1679
+ const burgerDropdown = document.getElementById("burger-menu-dropdown");
1680
+ initBurgerToggle(burgerBtn, burgerDropdown);
1681
+ initPurgeModal(burgerDropdown);
1682
+ initExportModal(burgerDropdown);
1683
+ initImportModal(burgerDropdown);
1684
+ initVersionModal(burgerDropdown);
1685
+ initDarkMode();
1686
+ }
1155
1687
 
1156
1688
  // src/board/client/main.ts
1157
1689
  initDragDrop();