trekoon 0.2.8 → 0.3.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 (38) hide show
  1. package/README.md +14 -14
  2. package/docs/commands.md +9 -11
  3. package/docs/quickstart.md +10 -12
  4. package/package.json +23 -1
  5. package/src/board/assets/app.js +469 -1377
  6. package/src/board/assets/components/ClampedText.js +1 -1
  7. package/src/board/assets/components/Component.js +271 -0
  8. package/src/board/assets/components/ConfirmDialog.js +81 -0
  9. package/src/board/assets/components/EpicRow.js +43 -26
  10. package/src/board/assets/components/EpicsOverview.js +52 -11
  11. package/src/board/assets/components/Inspector.js +335 -0
  12. package/src/board/assets/components/Notice.js +87 -0
  13. package/src/board/assets/components/SubtaskModal.js +100 -0
  14. package/src/board/assets/components/TaskCard.js +82 -0
  15. package/src/board/assets/components/TaskModal.js +99 -0
  16. package/src/board/assets/components/TopBar.js +167 -0
  17. package/src/board/assets/components/Workspace.js +319 -0
  18. package/src/board/assets/components/assetMap.js +29 -14
  19. package/src/board/assets/components/helpers.js +261 -0
  20. package/src/board/assets/fonts/inter-latin.woff2 +0 -0
  21. package/src/board/assets/fonts/material-symbols-rounded.woff2 +0 -0
  22. package/src/board/assets/index.html +20 -57
  23. package/src/board/assets/main.js +2 -18
  24. package/src/board/assets/runtime/clipboard.js +34 -0
  25. package/src/board/assets/runtime/delegation.js +342 -0
  26. package/src/board/assets/state/actions.js +204 -16
  27. package/src/board/assets/state/api.js +201 -46
  28. package/src/board/assets/state/store.js +418 -117
  29. package/src/board/assets/state/url.js +184 -0
  30. package/src/board/assets/state/utils.js +222 -0
  31. package/src/board/assets/styles/board.css +933 -129
  32. package/src/board/assets/styles/fonts.css +22 -0
  33. package/src/board/routes.ts +15 -6
  34. package/src/board/server.ts +1 -0
  35. package/src/board/assets/components/AppShell.js +0 -17
  36. package/src/board/assets/components/BoardTopbar.js +0 -78
  37. package/src/board/assets/components/WorkspaceHeader.js +0 -70
  38. package/src/board/assets/utils/dom.js +0 -308
@@ -0,0 +1,342 @@
1
+ /**
2
+ * Event delegation system for the board runtime.
3
+ *
4
+ * Attaches a single listener per event type on the root element.
5
+ * Uses event.target.closest() to match data-attributes, so dynamically
6
+ * rendered content is handled automatically without rebinding.
7
+ *
8
+ * @param {HTMLElement} rootElement Mount root for delegated listeners.
9
+ * @param {object} actions Callback map the delegation dispatches into.
10
+ * @returns {() => void} Teardown function that removes every listener.
11
+ */
12
+ export function createDelegation(rootElement, actions) {
13
+ // ---------------------------------------------------------------------------
14
+ // Click delegation
15
+ // ---------------------------------------------------------------------------
16
+ function handleClick(event) {
17
+ const { target } = event;
18
+
19
+ // -- Destructive / mutation buttons (most specific first) -----------------
20
+
21
+ const deleteSubtaskEl = target.closest("[data-delete-subtask]");
22
+ if (deleteSubtaskEl) {
23
+ if (actions.isMutating()) return;
24
+ const subtaskId = deleteSubtaskEl.dataset.deleteSubtask;
25
+ if (subtaskId) actions.deleteSubtask(subtaskId, deleteSubtaskEl);
26
+ return;
27
+ }
28
+
29
+ const removeDependencyEl = target.closest("[data-remove-dependency-source]");
30
+ if (removeDependencyEl) {
31
+ if (actions.isMutating()) return;
32
+ const sourceId = removeDependencyEl.dataset.removeDependencySource;
33
+ const dependsOnId = removeDependencyEl.dataset.removeDependencyTarget;
34
+ actions.removeDependency(sourceId, dependsOnId, removeDependencyEl);
35
+ return;
36
+ }
37
+
38
+ // -- Disclosure openers ---------------------------------------------------
39
+
40
+ const openSubtaskEl = target.closest("[data-open-subtask]");
41
+ if (openSubtaskEl) {
42
+ actions.openSubtask(openSubtaskEl.dataset.openSubtask || null, openSubtaskEl);
43
+ return;
44
+ }
45
+
46
+ const copyEpicIdEl = target.closest("[data-copy-epic-id]");
47
+ if (copyEpicIdEl) {
48
+ actions.copyEpicId(copyEpicIdEl.dataset.copyEpicId || null);
49
+ return;
50
+ }
51
+
52
+ // -- Backdrop-style close handlers ----------------------------------------
53
+ // Only close when the click lands directly on the backdrop element itself,
54
+ // not on any child content rendered inside the overlay.
55
+
56
+ const closeSubtaskEl = target.closest("[data-close-subtask]");
57
+ if (closeSubtaskEl) {
58
+ if (
59
+ closeSubtaskEl.classList.contains("board-modal-backdrop") &&
60
+ target !== closeSubtaskEl
61
+ ) {
62
+ return;
63
+ }
64
+ actions.closeSubtask();
65
+ return;
66
+ }
67
+
68
+ const closeTaskEl = target.closest("[data-close-task]");
69
+ if (closeTaskEl) {
70
+ if (
71
+ closeTaskEl.classList.contains("board-task-modal-backdrop") &&
72
+ target !== closeTaskEl
73
+ ) {
74
+ return;
75
+ }
76
+ actions.closeTask();
77
+ return;
78
+ }
79
+
80
+ const closeConfirmEl = target.closest("[data-close-confirm]");
81
+ if (closeConfirmEl) {
82
+ if (
83
+ closeConfirmEl.classList.contains("board-confirm-backdrop") &&
84
+ target !== closeConfirmEl
85
+ ) {
86
+ return;
87
+ }
88
+ actions.cancelDelete();
89
+ return;
90
+ }
91
+
92
+ // -- Navigation -----------------------------------------------------------
93
+
94
+ const navEl = target.closest("[data-nav]");
95
+ if (navEl) {
96
+ if (navEl.dataset.nav === "epics") actions.showEpics();
97
+ return;
98
+ }
99
+
100
+ const navBoardEl = target.closest("[data-nav-board]");
101
+ if (navBoardEl) {
102
+ actions.showBoard();
103
+ return;
104
+ }
105
+
106
+ const navDetailEl = target.closest("[data-nav-detail]");
107
+ if (navDetailEl) {
108
+ actions.scrollToDetail();
109
+ return;
110
+ }
111
+
112
+ // -- Notes panel toggle ---------------------------------------------------
113
+
114
+ const toggleNotesEl = target.closest("[data-toggle-notes]");
115
+ if (toggleNotesEl) {
116
+ actions.toggleNotesPanel();
117
+ return;
118
+ }
119
+
120
+ // -- View switching -------------------------------------------------------
121
+
122
+ const viewEl = target.closest("[data-view]");
123
+ if (viewEl) {
124
+ actions.setView(viewEl.dataset.view);
125
+ return;
126
+ }
127
+
128
+ // -- Generic actions (toggle-theme, confirm-delete, cancel-delete) --------
129
+
130
+ const actionEl = target.closest("[data-action]");
131
+ if (actionEl) {
132
+ const action = actionEl.dataset.action;
133
+ if (action === "toggle-theme") actions.toggleTheme();
134
+ if (action === "confirm-delete") actions.confirmDelete();
135
+ if (action === "cancel-delete") actions.cancelDelete();
136
+ return;
137
+ }
138
+
139
+ // -- Epic selection -------------------------------------------------------
140
+
141
+ const openEpicEl = target.closest("[data-open-epic]");
142
+ if (openEpicEl) {
143
+ actions.openEpic(openEpicEl.dataset.openEpic || null);
144
+ return;
145
+ }
146
+
147
+ // -- Task selection (broadest — checked last) -----------------------------
148
+
149
+ const taskEl = target.closest("[data-task-id]");
150
+ if (taskEl) {
151
+ actions.selectTask(taskEl.dataset.taskId, taskEl);
152
+ return;
153
+ }
154
+ }
155
+
156
+ // ---------------------------------------------------------------------------
157
+ // Input delegation (#board-search-input)
158
+ // ---------------------------------------------------------------------------
159
+ function handleInput(event) {
160
+ if (event.target.id === "board-search-input") {
161
+ actions.updateSearch(event.target.value);
162
+ }
163
+ }
164
+
165
+ // ---------------------------------------------------------------------------
166
+ // Change delegation (#board-epic-select)
167
+ // ---------------------------------------------------------------------------
168
+ function handleChange(event) {
169
+ if (event.target.id === "board-epic-select") {
170
+ actions.selectEpic(event.target.value || null);
171
+ return;
172
+ }
173
+
174
+ const epicStatusForm = event.target.closest("[data-epic-status-form]");
175
+ if (epicStatusForm) {
176
+ if (actions.isMutating()) return;
177
+ actions.changeEpicStatus(epicStatusForm.dataset.epicStatusForm, event.target.value);
178
+ return;
179
+ }
180
+
181
+ const bulkStatusForm = event.target.closest("[data-bulk-status-form]");
182
+ if (bulkStatusForm) {
183
+ if (actions.isMutating()) return;
184
+ const newStatus = event.target.value;
185
+ if (newStatus) {
186
+ actions.bulkSetStatus(bulkStatusForm.dataset.bulkStatusForm, newStatus);
187
+ event.target.value = "";
188
+ }
189
+ return;
190
+ }
191
+ }
192
+
193
+ // ---------------------------------------------------------------------------
194
+ // Submit delegation (data-task-form, data-subtask-form, etc.)
195
+ // ---------------------------------------------------------------------------
196
+ function handleSubmit(event) {
197
+ const form = event.target;
198
+
199
+ const taskForm = form.closest("[data-task-form]");
200
+ if (taskForm) {
201
+ event.preventDefault();
202
+ if (actions.isMutating()) return;
203
+ actions.submitTaskForm(taskForm.dataset.taskForm, new FormData(taskForm));
204
+ return;
205
+ }
206
+
207
+ const subtaskForm = form.closest("[data-subtask-form]");
208
+ if (subtaskForm) {
209
+ event.preventDefault();
210
+ if (actions.isMutating()) return;
211
+ actions.submitSubtaskForm(
212
+ subtaskForm.dataset.subtaskForm,
213
+ new FormData(subtaskForm),
214
+ );
215
+ return;
216
+ }
217
+
218
+ const createSubtaskForm = form.closest("[data-create-subtask-form]");
219
+ if (createSubtaskForm) {
220
+ event.preventDefault();
221
+ if (actions.isMutating()) return;
222
+ actions.submitCreateSubtask(
223
+ createSubtaskForm.dataset.createSubtaskForm,
224
+ new FormData(createSubtaskForm),
225
+ );
226
+ return;
227
+ }
228
+
229
+ const dependencyForm = form.closest("[data-dependency-form]");
230
+ if (dependencyForm) {
231
+ event.preventDefault();
232
+ if (actions.isMutating()) return;
233
+ actions.addDependency(
234
+ dependencyForm.dataset.dependencyForm,
235
+ new FormData(dependencyForm),
236
+ );
237
+ return;
238
+ }
239
+ }
240
+
241
+ // ---------------------------------------------------------------------------
242
+ // Keyboard shortcuts (window-level)
243
+ // ---------------------------------------------------------------------------
244
+ function handleKeydown(event) {
245
+ if (event.defaultPrevented) {
246
+ return;
247
+ }
248
+ actions.handleKeydown(event);
249
+ }
250
+
251
+ function handleDelegatedKeydown(event) {
252
+ if (event.defaultPrevented) {
253
+ return;
254
+ }
255
+
256
+ const target = event.target instanceof Element ? event.target : null;
257
+ if (!target) {
258
+ return;
259
+ }
260
+
261
+ if (target.closest("[data-copy-epic-id]")) {
262
+ return;
263
+ }
264
+
265
+ const openEpicEl = target.closest("[data-open-epic]");
266
+ if (!openEpicEl) {
267
+ return;
268
+ }
269
+
270
+ if (event.key === "Enter" || event.key === " ") {
271
+ event.preventDefault();
272
+ actions.openEpic(openEpicEl.dataset.openEpic || null);
273
+ }
274
+ }
275
+
276
+ // ---------------------------------------------------------------------------
277
+ // Drag-and-drop delegation
278
+ // ---------------------------------------------------------------------------
279
+ function handleDragstart(event) {
280
+ const draggable = event.target.closest("[data-draggable-task]");
281
+ if (!draggable) return;
282
+
283
+ if (actions.isMutating()) {
284
+ event.preventDefault();
285
+ return;
286
+ }
287
+
288
+ const taskId = draggable.dataset.taskId;
289
+ if (!taskId) return;
290
+
291
+ event.dataTransfer?.setData("text/task-id", taskId);
292
+ event.dataTransfer?.setData("text/plain", taskId);
293
+ }
294
+
295
+ function handleDragover(event) {
296
+ if (event.target.closest("[data-drop-status]")) {
297
+ event.preventDefault();
298
+ }
299
+ }
300
+
301
+ function handleDrop(event) {
302
+ const column = event.target.closest("[data-drop-status]");
303
+ if (!column) return;
304
+
305
+ event.preventDefault();
306
+ if (actions.isMutating()) return;
307
+
308
+ const taskId =
309
+ event.dataTransfer?.getData("text/task-id") ||
310
+ event.dataTransfer?.getData("text/plain");
311
+ const nextStatus = column.dataset.dropStatus;
312
+ actions.dropTaskStatus(taskId, nextStatus);
313
+ }
314
+
315
+ // ---------------------------------------------------------------------------
316
+ // Attach
317
+ // ---------------------------------------------------------------------------
318
+ rootElement.addEventListener("click", handleClick);
319
+ rootElement.addEventListener("input", handleInput);
320
+ rootElement.addEventListener("change", handleChange);
321
+ rootElement.addEventListener("submit", handleSubmit);
322
+ rootElement.addEventListener("dragstart", handleDragstart);
323
+ rootElement.addEventListener("dragover", handleDragover);
324
+ rootElement.addEventListener("drop", handleDrop);
325
+ rootElement.addEventListener("keydown", handleDelegatedKeydown);
326
+ window.addEventListener("keydown", handleKeydown);
327
+
328
+ // ---------------------------------------------------------------------------
329
+ // Teardown
330
+ // ---------------------------------------------------------------------------
331
+ return function teardown() {
332
+ rootElement.removeEventListener("click", handleClick);
333
+ rootElement.removeEventListener("input", handleInput);
334
+ rootElement.removeEventListener("change", handleChange);
335
+ rootElement.removeEventListener("submit", handleSubmit);
336
+ rootElement.removeEventListener("dragstart", handleDragstart);
337
+ rootElement.removeEventListener("dragover", handleDragover);
338
+ rootElement.removeEventListener("drop", handleDrop);
339
+ rootElement.removeEventListener("keydown", handleDelegatedKeydown);
340
+ window.removeEventListener("keydown", handleKeydown);
341
+ };
342
+ }
@@ -1,3 +1,6 @@
1
+ import { copyTextToClipboard } from "../runtime/clipboard.js";
2
+ import { orderEpicsNewestFirst } from "./store.js";
3
+
1
4
  function cloneSnapshot(snapshot) {
2
5
  if (typeof structuredClone === "function") {
3
6
  return structuredClone(snapshot);
@@ -38,6 +41,40 @@ export function updateSubtaskInSnapshot(snapshot, subtaskId, updates, normalizeS
38
41
  return normalizeSnapshot(nextSnapshot);
39
42
  }
40
43
 
44
+ export function cascadeEpicStatusInSnapshot(snapshot, epicId, status, normalizeSnapshot) {
45
+ const nextSnapshot = cloneSnapshot(snapshot);
46
+ const epic = nextSnapshot.epics.find((candidate) => candidate.id === epicId);
47
+ if (!epic) {
48
+ return snapshot;
49
+ }
50
+
51
+ const updatedAt = Date.now();
52
+ epic.status = status;
53
+ epic.updatedAt = updatedAt;
54
+
55
+ const taskIds = new Set();
56
+ for (const task of nextSnapshot.tasks) {
57
+ if (task.epicId !== epicId) {
58
+ continue;
59
+ }
60
+
61
+ task.status = status;
62
+ task.updatedAt = updatedAt;
63
+ taskIds.add(task.id);
64
+ }
65
+
66
+ for (const subtask of nextSnapshot.subtasks) {
67
+ if (!taskIds.has(subtask.taskId)) {
68
+ continue;
69
+ }
70
+
71
+ subtask.status = status;
72
+ subtask.updatedAt = updatedAt;
73
+ }
74
+
75
+ return normalizeSnapshot(nextSnapshot);
76
+ }
77
+
41
78
  export function addDependencyInSnapshot(snapshot, sourceId, dependsOnId, normalizeSnapshot) {
42
79
  const nextSnapshot = cloneSnapshot(snapshot);
43
80
  const duplicate = normalizeArray(nextSnapshot.dependencies).some(
@@ -98,6 +135,8 @@ export function createBoardActions(options) {
98
135
  applyTheme,
99
136
  closeTopmostDisclosure,
100
137
  dismissSearch,
138
+ hasOpenOverlay,
139
+ closeActiveOverlay,
101
140
  focusSearch,
102
141
  focusTaskDetail,
103
142
  searchFocusKeys,
@@ -117,6 +156,80 @@ export function createBoardActions(options) {
117
156
  };
118
157
 
119
158
  let searchTimer = null;
159
+ let pendingSearchValue = null;
160
+ let copyFeedbackTimer = null;
161
+
162
+ const syncSearchInputToState = () => {
163
+ const input = document.querySelector("#board-search-input");
164
+ if (input instanceof HTMLInputElement) {
165
+ input.value = store.search;
166
+ }
167
+ };
168
+
169
+ const cancelPendingSearch = (options = {}) => {
170
+ const { syncInput = true } = options;
171
+ pendingSearchValue = null;
172
+ if (searchTimer !== null) {
173
+ clearTimeout(searchTimer);
174
+ searchTimer = null;
175
+ }
176
+ if (syncInput) {
177
+ syncSearchInputToState();
178
+ }
179
+ };
180
+
181
+ const focusSearchInput = () => {
182
+ const input = document.querySelector("#board-search-input");
183
+ if (input instanceof HTMLInputElement) {
184
+ input.focus({ preventScroll: true });
185
+ input.setSelectionRange(input.value.length, input.value.length);
186
+ }
187
+ };
188
+
189
+ const shouldRefocusSearchInput = () => document.activeElement?.id === "board-search-input";
190
+
191
+ const clearCopyFeedback = ({ rerenderBoard = true } = {}) => {
192
+ if (copyFeedbackTimer !== null) {
193
+ clearTimeout(copyFeedbackTimer);
194
+ copyFeedbackTimer = null;
195
+ }
196
+
197
+ if (!store.copyFeedback) {
198
+ return;
199
+ }
200
+
201
+ store.copyFeedback = null;
202
+ if (rerenderBoard) {
203
+ rerender();
204
+ }
205
+ };
206
+
207
+ const scheduleCopyFeedbackClear = (epicId) => {
208
+ if (copyFeedbackTimer !== null) {
209
+ clearTimeout(copyFeedbackTimer);
210
+ }
211
+
212
+ copyFeedbackTimer = setTimeout(() => {
213
+ copyFeedbackTimer = null;
214
+ if (store.copyFeedback?.epicId !== epicId) {
215
+ return;
216
+ }
217
+
218
+ store.copyFeedback = null;
219
+ rerender();
220
+ }, 1800);
221
+ };
222
+
223
+ const commitSearch = (nextSearch, options = {}) => {
224
+ const { focusInput = false } = options;
225
+ cancelPendingSearch({ syncInput: false });
226
+ syncState({ search: nextSearch });
227
+ persist();
228
+ rerender({ preserveFocus: false });
229
+ if (focusInput) {
230
+ focusSearchInput();
231
+ }
232
+ };
120
233
 
121
234
  return {
122
235
  toggleTheme() {
@@ -124,23 +237,26 @@ export function createBoardActions(options) {
124
237
  applyTheme(store.theme);
125
238
  rerender();
126
239
  },
240
+ toggleNotesPanel() {
241
+ store.notesPanelOpen = !store.notesPanelOpen;
242
+ persist();
243
+ rerender();
244
+ },
127
245
  updateSearch(value) {
128
- store.search = typeof value === "string" ? value : "";
129
- if (searchTimer !== null) {
130
- clearTimeout(searchTimer);
131
- }
246
+ const nextSearch = typeof value === "string" ? value : "";
247
+ cancelPendingSearch({ syncInput: false });
248
+ pendingSearchValue = nextSearch;
132
249
  searchTimer = setTimeout(() => {
133
- searchTimer = null;
134
- syncState({ search: store.search });
135
- persist();
136
- rerender({ preserveFocus: false });
137
- const input = document.querySelector("#board-search-input");
138
- if (input instanceof HTMLInputElement) {
139
- input.focus({ preventScroll: true });
140
- input.setSelectionRange(input.value.length, input.value.length);
250
+ if (pendingSearchValue !== nextSearch) {
251
+ return;
141
252
  }
253
+ commitSearch(nextSearch, { focusInput: shouldRefocusSearchInput() });
142
254
  }, 180);
143
255
  },
256
+ clearSearch() {
257
+ commitSearch("");
258
+ },
259
+ cancelPendingSearch,
144
260
  openEpic(epicId) {
145
261
  transition({
146
262
  screen: "tasks",
@@ -165,7 +281,11 @@ export function createBoardActions(options) {
165
281
  });
166
282
  },
167
283
  showBoard() {
168
- const fallbackEpicId = getBoardState().selectedEpicId || store.snapshot.epics[0]?.id || null;
284
+ const boardState = getBoardState();
285
+ const fallbackEpicId = boardState.selectedEpicId
286
+ || boardState.visibleEpics[0]?.id
287
+ || orderEpicsNewestFirst(store.snapshot.epics)[0]?.id
288
+ || null;
169
289
  if (!fallbackEpicId) {
170
290
  return;
171
291
  }
@@ -180,6 +300,37 @@ export function createBoardActions(options) {
180
300
  setView(view) {
181
301
  transition({ view });
182
302
  },
303
+ async copyEpicId(epicId) {
304
+ const normalizedEpicId = typeof epicId === "string" ? epicId.trim() : "";
305
+
306
+ if (!normalizedEpicId) {
307
+ clearCopyFeedback({ rerenderBoard: false });
308
+ store.notice = {
309
+ type: "error",
310
+ title: "Copy failed",
311
+ message: "Epic UUID is unavailable.",
312
+ };
313
+ rerender();
314
+ return;
315
+ }
316
+
317
+ try {
318
+ await copyTextToClipboard(normalizedEpicId);
319
+ store.copyFeedback = {
320
+ epicId: normalizedEpicId,
321
+ };
322
+ scheduleCopyFeedbackClear(normalizedEpicId);
323
+ } catch {
324
+ clearCopyFeedback({ rerenderBoard: false });
325
+ store.notice = {
326
+ type: "error",
327
+ title: "Copy failed",
328
+ message: "Could not copy the epic UUID.",
329
+ };
330
+ }
331
+
332
+ rerender();
333
+ },
183
334
  selectTask(taskId) {
184
335
  const task = getTaskById(taskId);
185
336
  if (!task) {
@@ -261,6 +412,20 @@ export function createBoardActions(options) {
261
412
  transition({ selectedTaskId: taskId }, { rerenderBoard: false });
262
413
  api.patchTask(taskId, { status: nextStatus }, (snapshot) => updateTaskInSnapshot(snapshot, taskId, { status: nextStatus }, normalizeSnapshot));
263
414
  },
415
+ changeEpicStatus(epicId, newStatus) {
416
+ const normalizedStatus = normalizeStatus(newStatus);
417
+ api.patchEpic(epicId, { status: normalizedStatus }, (snapshot) => {
418
+ const epic = snapshot.epics.find(e => e.id === epicId);
419
+ if (epic) epic.status = normalizedStatus;
420
+ return snapshot;
421
+ });
422
+ },
423
+ bulkSetStatus(epicId, newStatus) {
424
+ const normalizedStatus = normalizeStatus(newStatus);
425
+ api.cascadeEpicStatus(epicId, normalizedStatus, (snapshot) =>
426
+ cascadeEpicStatusInSnapshot(snapshot, epicId, normalizedStatus, normalizeSnapshot),
427
+ );
428
+ },
264
429
  handleKeydown(event) {
265
430
  const boardState = getBoardState();
266
431
  const activeElement = document.activeElement;
@@ -276,23 +441,42 @@ export function createBoardActions(options) {
276
441
  }
277
442
 
278
443
  if (event.key === "Escape") {
444
+ if (activeElement?.id === "board-search-input" && pendingSearchValue !== null) {
445
+ event.preventDefault();
446
+ activeElement.value = "";
447
+ this.clearSearch();
448
+ activeElement.blur();
449
+ return;
450
+ }
451
+
279
452
  if (closeTopmostDisclosure?.(boardState, activeElement)) {
280
453
  event.preventDefault();
281
454
  return;
282
455
  }
283
456
 
457
+ if (dismissSearch?.(boardState, activeElement)) {
458
+ event.preventDefault();
459
+ return;
460
+ }
461
+
462
+ if (hasOpenOverlay?.()) {
463
+ event.preventDefault();
464
+ closeActiveOverlay?.();
465
+ return;
466
+ }
467
+
284
468
  if (boardState.selectedSubtaskId) {
285
469
  event.preventDefault();
286
470
  this.closeSubtask();
287
471
  } else if (boardState.selectedTaskId) {
288
472
  event.preventDefault();
289
473
  this.closeTask();
290
- } else if (dismissSearch?.(boardState, activeElement)) {
291
- event.preventDefault();
292
- return;
293
474
  } else if (boardState.screen === "tasks") {
294
475
  event.preventDefault();
295
476
  this.showEpics();
477
+ } else if (store.copyFeedback) {
478
+ event.preventDefault();
479
+ clearCopyFeedback();
296
480
  } else if (store.notice) {
297
481
  event.preventDefault();
298
482
  store.notice = null;
@@ -301,6 +485,10 @@ export function createBoardActions(options) {
301
485
  return;
302
486
  }
303
487
 
488
+ if (hasOpenOverlay?.()) {
489
+ return;
490
+ }
491
+
304
492
  if (boardState.screen !== "tasks" || isTypingTarget || visibleTasks.length === 0) {
305
493
  return;
306
494
  }