agent-tasks 1.7.1 → 1.9.1

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 (68) hide show
  1. package/README.md +17 -15
  2. package/dist/domain/agent-bridge.d.ts.map +1 -1
  3. package/dist/domain/agent-bridge.js +22 -2
  4. package/dist/domain/agent-bridge.js.map +1 -1
  5. package/dist/domain/approvals.d.ts.map +1 -1
  6. package/dist/domain/approvals.js +4 -1
  7. package/dist/domain/approvals.js.map +1 -1
  8. package/dist/domain/cleanup.d.ts +1 -0
  9. package/dist/domain/cleanup.d.ts.map +1 -1
  10. package/dist/domain/cleanup.js +36 -4
  11. package/dist/domain/cleanup.js.map +1 -1
  12. package/dist/domain/comments.d.ts +1 -0
  13. package/dist/domain/comments.d.ts.map +1 -1
  14. package/dist/domain/comments.js +10 -0
  15. package/dist/domain/comments.js.map +1 -1
  16. package/dist/domain/rules.js +11 -10
  17. package/dist/domain/rules.js.map +1 -1
  18. package/dist/domain/task-validator.d.ts +9 -0
  19. package/dist/domain/task-validator.d.ts.map +1 -0
  20. package/dist/domain/task-validator.js +70 -0
  21. package/dist/domain/task-validator.js.map +1 -0
  22. package/dist/domain/tasks.d.ts +19 -9
  23. package/dist/domain/tasks.d.ts.map +1 -1
  24. package/dist/domain/tasks.js +242 -111
  25. package/dist/domain/tasks.js.map +1 -1
  26. package/dist/index.js +4 -2
  27. package/dist/index.js.map +1 -1
  28. package/dist/storage/database.d.ts.map +1 -1
  29. package/dist/storage/database.js +11 -3
  30. package/dist/storage/database.js.map +1 -1
  31. package/dist/transport/mcp-handlers.d.ts +31 -0
  32. package/dist/transport/mcp-handlers.d.ts.map +1 -0
  33. package/dist/transport/mcp-handlers.js +426 -0
  34. package/dist/transport/mcp-handlers.js.map +1 -0
  35. package/dist/transport/mcp.d.ts.map +1 -1
  36. package/dist/transport/mcp.js +207 -656
  37. package/dist/transport/mcp.js.map +1 -1
  38. package/dist/transport/rest.d.ts.map +1 -1
  39. package/dist/transport/rest.js +23 -7
  40. package/dist/transport/rest.js.map +1 -1
  41. package/dist/transport/ws.d.ts.map +1 -1
  42. package/dist/transport/ws.js +6 -4
  43. package/dist/transport/ws.js.map +1 -1
  44. package/dist/ui/app.js +186 -1608
  45. package/dist/ui/board.js +401 -0
  46. package/dist/ui/drag.js +143 -0
  47. package/dist/ui/index.html +5 -0
  48. package/dist/ui/inline-edit.js +242 -0
  49. package/dist/ui/panel.js +574 -0
  50. package/dist/ui/styles.css +109 -0
  51. package/dist/ui/ui-utils.js +323 -0
  52. package/package.json +1 -1
  53. package/dist/db.d.ts +0 -10
  54. package/dist/db.d.ts.map +0 -1
  55. package/dist/db.js +0 -112
  56. package/dist/db.js.map +0 -1
  57. package/dist/event-bus.d.ts +0 -10
  58. package/dist/event-bus.d.ts.map +0 -1
  59. package/dist/event-bus.js +0 -38
  60. package/dist/event-bus.js.map +0 -1
  61. package/dist/session.d.ts +0 -7
  62. package/dist/session.d.ts.map +0 -1
  63. package/dist/session.js +0 -11
  64. package/dist/session.js.map +0 -1
  65. package/dist/tasks.d.ts +0 -32
  66. package/dist/tasks.d.ts.map +0 -1
  67. package/dist/tasks.js +0 -410
  68. package/dist/tasks.js.map +0 -1
@@ -0,0 +1,401 @@
1
+ // =============================================================================
2
+ // agent-tasks — Board Module
3
+ //
4
+ // Kanban board rendering, column headers, gate indicators, card rendering,
5
+ // stats display.
6
+ // =============================================================================
7
+
8
+ window.TaskBoard = window.TaskBoard || {};
9
+
10
+ // ---- Constants ----
11
+
12
+ var STAGE_ICONS = {
13
+ backlog: 'inbox',
14
+ spec: 'description',
15
+ plan: 'map',
16
+ implement: 'code',
17
+ test: 'science',
18
+ review: 'rate_review',
19
+ done: 'check_circle',
20
+ cancelled: 'cancel',
21
+ };
22
+
23
+ var STAGE_EMPTY_MESSAGES = {
24
+ backlog: { icon: 'inbox', text: 'Nothing in backlog', cta: 'Add a task', ctaIcon: 'add' },
25
+ spec: {
26
+ icon: 'description',
27
+ text: 'No specs yet',
28
+ cta: 'Drag tasks here',
29
+ ctaIcon: 'drag_indicator',
30
+ },
31
+ plan: {
32
+ icon: 'map',
33
+ text: 'No plans in progress',
34
+ cta: 'Drag tasks here',
35
+ ctaIcon: 'drag_indicator',
36
+ },
37
+ implement: {
38
+ icon: 'code',
39
+ text: 'Nothing being built',
40
+ cta: 'Drag tasks here',
41
+ ctaIcon: 'drag_indicator',
42
+ },
43
+ test: {
44
+ icon: 'science',
45
+ text: 'Nothing to test',
46
+ cta: 'Drag tasks here',
47
+ ctaIcon: 'drag_indicator',
48
+ },
49
+ review: {
50
+ icon: 'rate_review',
51
+ text: 'Nothing in review',
52
+ cta: 'Drag tasks here',
53
+ ctaIcon: 'drag_indicator',
54
+ },
55
+ done: { icon: 'check_circle', text: 'No completed tasks', cta: '', ctaIcon: '' },
56
+ cancelled: { icon: 'cancel', text: 'No cancelled tasks', cta: '', ctaIcon: '' },
57
+ };
58
+
59
+ var WIP_WARNING = 5;
60
+ var WIP_DANGER = 8;
61
+ var CARDS_PER_PAGE = 20;
62
+
63
+ var _columnVisibleCounts = new Map();
64
+
65
+ // ---- Gate indicators ----
66
+
67
+ function renderGateIndicator(stage) {
68
+ var state = TaskBoard.state;
69
+ var esc = TaskBoard.esc;
70
+ let gates = null;
71
+ for (const proj of Object.keys(state.gateConfigs)) {
72
+ const gc = state.gateConfigs[proj];
73
+ if (gc?.gates?.[stage]) {
74
+ gates = gc.gates;
75
+ break;
76
+ }
77
+ }
78
+ if (!gates || !gates[stage]) return '';
79
+ const g = gates[stage];
80
+ const reqs = [];
81
+ if (g.require_artifacts?.length) {
82
+ for (const a of g.require_artifacts) reqs.push(esc(a));
83
+ }
84
+ if (g.require_min_artifacts)
85
+ reqs.push(g.require_min_artifacts + ' artifact' + (g.require_min_artifacts > 1 ? 's' : ''));
86
+ if (g.require_comment) reqs.push('comment');
87
+ if (g.require_approval) reqs.push('approval');
88
+ if (!reqs.length) return '';
89
+ return `<div class="gate-indicator" title="Gate: requires ${reqs.join(', ')} to advance"><span class="material-symbols-outlined">lock</span>${reqs.map((r) => `<span class="gate-req">${r}</span>`).join('')}</div>`;
90
+ }
91
+
92
+ // ---- Status indicator ----
93
+
94
+ function renderStatusIndicator(status) {
95
+ const icons = {
96
+ in_progress: 'pending',
97
+ completed: 'check_circle',
98
+ failed: 'cancel',
99
+ pending: 'radio_button_unchecked',
100
+ cancelled: 'block',
101
+ };
102
+ const icon = icons[status];
103
+ if (!icon) return '';
104
+ return `<span class="task-status-indicator status-${status}"><span class="material-symbols-outlined">${icon}</span></span>`;
105
+ }
106
+
107
+ // ---- Collaborators ----
108
+
109
+ function renderCollaborators(collabs) {
110
+ var esc = TaskBoard.esc;
111
+ if (!collabs || collabs.length === 0) return '';
112
+ const maxVisible = 3;
113
+ const visible = collabs.slice(0, maxVisible);
114
+ const overflow = collabs.length - maxVisible;
115
+ let html = '<div class="task-card-collabs">';
116
+ for (const c of visible) {
117
+ const initials = TaskBoard.avatarInitials(c.agent_id);
118
+ const color = TaskBoard.avatarColor(c.agent_id);
119
+ html += `<div class="collab-avatar" style="background:${color}" title="${esc(c.agent_id)} (${esc(c.role)})">${esc(initials)}</div>`;
120
+ }
121
+ if (overflow > 0) {
122
+ html += `<div class="collab-overflow" title="${collabs.length} collaborators">+${overflow}</div>`;
123
+ }
124
+ html += '</div>';
125
+ return html;
126
+ }
127
+
128
+ // ---- Card rendering ----
129
+
130
+ function renderCard(task, isBlocked, stage, index) {
131
+ var state = TaskBoard.state;
132
+ var esc = TaskBoard.esc;
133
+ var relativeTime = TaskBoard.relativeTime;
134
+ var renderAvatar = TaskBoard.renderAvatar;
135
+
136
+ const tags = [];
137
+
138
+ if (task.project) {
139
+ tags.push(`<span class="task-tag tag-project">${esc(task.project)}</span>`);
140
+ }
141
+ if (task.priority > 0) {
142
+ tags.push(`<span class="task-tag tag-priority">P${task.priority}</span>`);
143
+ }
144
+ const artCount = state.artifactCounts[task.id];
145
+ if (artCount) {
146
+ tags.push(`<span class="task-tag tag-artifacts">${artCount} art.</span>`);
147
+ }
148
+ const cmtCount = state.commentCounts[task.id];
149
+ if (cmtCount) {
150
+ tags.push(`<span class="task-tag tag-comments">${cmtCount} cmt.</span>`);
151
+ }
152
+ if (isBlocked) {
153
+ tags.push(`<span class="task-tag tag-blocked">blocked</span>`);
154
+ }
155
+
156
+ const progress = state.subtaskProgress[task.id];
157
+ let progressBar = '';
158
+ if (progress && progress.total > 0) {
159
+ const pct = Math.round((progress.done / progress.total) * 100);
160
+ tags.push(`<span class="task-tag tag-subtasks">${progress.done}/${progress.total}</span>`);
161
+ progressBar = `<div class="subtask-progress"><div class="subtask-progress-fill" style="width:${pct}%"></div></div>`;
162
+ }
163
+
164
+ const priorityClass =
165
+ task.priority >= 5 ? ' priority-high' : task.priority >= 3 ? ' priority-medium' : '';
166
+
167
+ const statusClass =
168
+ task.status === 'completed' || task.status === 'failed' || task.status === 'cancelled'
169
+ ? ` status-${task.status}`
170
+ : '';
171
+
172
+ const descPreview = task.description ? task.description.split('\n')[0].substring(0, 120) : '';
173
+
174
+ const timeAgo = relativeTime(task.updated_at);
175
+
176
+ const assigneeAvatar = task.assigned_to ? renderAvatar(task.assigned_to) : '';
177
+
178
+ const isActive = state.panelTaskId === task.id;
179
+ const activeClass = isActive ? ' active-card' : '';
180
+
181
+ const statusIndicator = renderStatusIndicator(task.status);
182
+
183
+ const collabs = state.collaborators[task.id] || [];
184
+ const collabHtml = renderCollaborators(collabs);
185
+
186
+ return `<div class="task-card${priorityClass}${statusClass}${activeClass}" tabindex="0" draggable="true"
187
+ data-task-id="${task.id}" data-stage="${esc(stage)}"
188
+ role="option"
189
+ style="animation-delay: ${index * 30}ms"
190
+ aria-label="Task #${task.id}: ${esc(task.title)}">
191
+ <div class="task-card-header">
192
+ <span class="task-card-id">#${task.id}${statusIndicator}</span>
193
+ ${timeAgo ? `<span class="task-card-time">${esc(timeAgo)}</span>` : ''}
194
+ </div>
195
+ <div class="task-card-title" data-action="edit-title" data-task-id="${task.id}">${esc(task.title)}</div>
196
+ ${descPreview ? `<div class="task-card-desc">${esc(descPreview)}</div>` : ''}
197
+ <div class="task-card-footer">
198
+ <div class="task-card-meta">${tags.join('')}</div>
199
+ ${assigneeAvatar ? `<div class="task-card-assignee" data-action="change-assignee" data-task-id="${task.id}">${assigneeAvatar}</div>` : ''}
200
+ </div>
201
+ ${collabHtml}
202
+ ${progressBar}
203
+ </div>`;
204
+ }
205
+
206
+ // ---- Board rendering ----
207
+
208
+ function renderBoard() {
209
+ var state = TaskBoard.state;
210
+ var esc = TaskBoard.esc;
211
+ var morph = TaskBoard.morph;
212
+ var getFilteredTasks = TaskBoard.getFilteredTasks;
213
+ var getBlockedTaskIds = TaskBoard.getBlockedTaskIds;
214
+
215
+ const board = document.getElementById('board');
216
+ const blocked = getBlockedTaskIds();
217
+ const filtered = getFilteredTasks();
218
+ const visibleStages = state.stages.filter((s) => s !== 'cancelled');
219
+
220
+ if (state.tasks.length === 0) {
221
+ morph(
222
+ board,
223
+ `<div class="board-empty">
224
+ <span class="material-symbols-outlined">view_kanban</span>
225
+ <h3>No tasks yet</h3>
226
+ <p>Create tasks via MCP tools (task_create) or the REST API (POST /api/tasks) to get started.</p>
227
+ <div class="empty-steps">
228
+ <div class="empty-step">
229
+ <span class="material-symbols-outlined">add_task</span>
230
+ <span>Create a task</span>
231
+ </div>
232
+ <div class="empty-step">
233
+ <span class="material-symbols-outlined">drag_indicator</span>
234
+ <span>Drag through stages</span>
235
+ </div>
236
+ <div class="empty-step">
237
+ <span class="material-symbols-outlined">check_circle</span>
238
+ <span>Complete the work</span>
239
+ </div>
240
+ </div>
241
+ </div>`,
242
+ );
243
+ return;
244
+ }
245
+
246
+ const byStage = {};
247
+ for (const s of state.stages) byStage[s] = [];
248
+ for (const t of filtered) {
249
+ if (byStage[t.stage]) byStage[t.stage].push(t);
250
+ else byStage[t.stage] = [t];
251
+ }
252
+
253
+ for (const s of Object.keys(byStage)) {
254
+ byStage[s].sort((a, b) => b.priority - a.priority);
255
+ }
256
+
257
+ const columnsToShow = [...visibleStages];
258
+ if (byStage['cancelled']?.length > 0 && !columnsToShow.includes('cancelled')) {
259
+ columnsToShow.push('cancelled');
260
+ }
261
+
262
+ morph(
263
+ board,
264
+ columnsToShow
265
+ .map((stage) => {
266
+ const tasks = byStage[stage] || [];
267
+ const isCollapsed = state.collapsedColumns.has(stage);
268
+ const colClass = isCollapsed ? 'kanban-column collapsed' : 'kanban-column';
269
+ const icon = STAGE_ICONS[stage] || 'label';
270
+
271
+ let countClass = 'column-count';
272
+ if (tasks.length >= WIP_DANGER) countClass += ' wip-danger';
273
+ else if (tasks.length >= WIP_WARNING) countClass += ' wip-warning';
274
+
275
+ const emptyMsg = STAGE_EMPTY_MESSAGES[stage] || {
276
+ icon: 'label',
277
+ text: 'No tasks',
278
+ cta: '',
279
+ };
280
+
281
+ var visibleCount = _columnVisibleCounts.get(stage) || CARDS_PER_PAGE;
282
+ var remaining = Math.max(0, tasks.length - visibleCount);
283
+
284
+ let bodyContent;
285
+ let showMoreHtml = '';
286
+ if (tasks.length === 0 && !isCollapsed) {
287
+ bodyContent = `<div class="column-empty">
288
+ <span class="material-symbols-outlined">${emptyMsg.icon}</span>
289
+ <div class="empty-text">${esc(emptyMsg.text)}</div>
290
+ ${emptyMsg.cta ? `<div class="empty-cta" data-action="add-task" data-stage="${esc(stage)}">${emptyMsg.ctaIcon ? `<span class="material-symbols-outlined">${emptyMsg.ctaIcon}</span>` : ''}${esc(emptyMsg.cta)}</div>` : ''}
291
+ </div>`;
292
+ } else {
293
+ var visibleTasks = tasks.slice(0, visibleCount);
294
+ bodyContent = visibleTasks
295
+ .map((t, i) => renderCard(t, blocked.has(t.id), stage, i))
296
+ .join('');
297
+ if (remaining > 0 && !isCollapsed) {
298
+ showMoreHtml = `<button class="column-show-more-btn" data-action="show-more" data-stage="${esc(stage)}">
299
+ <span class="material-symbols-outlined">expand_more</span> Show more (${remaining} remaining)
300
+ </button>`;
301
+ }
302
+ }
303
+
304
+ const gateHtml = renderGateIndicator(stage);
305
+
306
+ return `<div class="${colClass}" id="col-${esc(stage)}" data-stage="${esc(stage)}">
307
+ <div class="column-header" data-action="toggle-collapse" data-stage="${esc(stage)}">
308
+ <div class="column-header-left">
309
+ <span class="material-symbols-outlined">${icon}</span>
310
+ <h3>${esc(stage)}</h3>
311
+ </div>
312
+ <span class="${countClass}" aria-label="${tasks.length} tasks">${tasks.length}</span>
313
+ </div>${gateHtml}
314
+ <div class="column-body" role="listbox" aria-label="${esc(stage)} tasks">
315
+ ${bodyContent}
316
+ </div>
317
+ ${showMoreHtml}
318
+ ${
319
+ !isCollapsed
320
+ ? `<button class="column-add-btn" data-action="inline-create" data-stage="${esc(stage)}">
321
+ <span class="material-symbols-outlined">add</span> New task
322
+ </button>`
323
+ : ''
324
+ }
325
+ </div>`;
326
+ })
327
+ .join(''),
328
+ );
329
+
330
+ requestAnimationFrame(() => {
331
+ const cards = board.querySelectorAll('.task-card:not(.no-anim):not(.animated)');
332
+ cards.forEach((card, i) => {
333
+ card.classList.add('animated');
334
+ card.style.animationDelay = `${i * 30}ms`;
335
+ card.classList.add('animate-in');
336
+ });
337
+ });
338
+ }
339
+
340
+ // ---- Stats rendering ----
341
+
342
+ var _lastStatValues = {};
343
+
344
+ function renderStats() {
345
+ var state = TaskBoard.state;
346
+ var morph = TaskBoard.morph;
347
+
348
+ const total = state.tasks.length;
349
+ const active = state.tasks.filter((t) => t.status === 'in_progress').length;
350
+ const pending = state.tasks.filter((t) => t.status === 'pending').length;
351
+ const done = state.tasks.filter((t) => t.status === 'completed').length;
352
+
353
+ const statsEl = document.getElementById('stats');
354
+ const values = { total, active, pending, done };
355
+
356
+ morph(
357
+ statsEl,
358
+ `<span class="stat">Total <span class="stat-value" data-stat="total">${total}</span></span>` +
359
+ `<span class="stat">Active <span class="stat-value" data-stat="active">${active}</span></span>` +
360
+ `<span class="stat">Pending <span class="stat-value" data-stat="pending">${pending}</span></span>` +
361
+ `<span class="stat">Done <span class="stat-value" data-stat="done">${done}</span></span>`,
362
+ );
363
+
364
+ for (const key of Object.keys(values)) {
365
+ if (_lastStatValues[key] !== undefined && _lastStatValues[key] !== values[key]) {
366
+ const el = statsEl.querySelector(`[data-stat="${key}"]`);
367
+ if (el) {
368
+ el.classList.remove('pulse');
369
+ void el.offsetWidth;
370
+ el.classList.add('pulse');
371
+ }
372
+ }
373
+ }
374
+ _lastStatValues = values;
375
+ }
376
+
377
+ // ---- Column pagination ----
378
+
379
+ function showMoreCards(stage) {
380
+ var current = _columnVisibleCounts.get(stage) || CARDS_PER_PAGE;
381
+ _columnVisibleCounts.set(stage, current + CARDS_PER_PAGE);
382
+ }
383
+
384
+ function resetColumnVisibleCounts() {
385
+ _columnVisibleCounts.clear();
386
+ }
387
+
388
+ // ---- Register on namespace ----
389
+
390
+ TaskBoard.STAGE_ICONS = STAGE_ICONS;
391
+ TaskBoard.STAGE_EMPTY_MESSAGES = STAGE_EMPTY_MESSAGES;
392
+ TaskBoard.WIP_WARNING = WIP_WARNING;
393
+ TaskBoard.WIP_DANGER = WIP_DANGER;
394
+ TaskBoard.renderGateIndicator = renderGateIndicator;
395
+ TaskBoard.renderStatusIndicator = renderStatusIndicator;
396
+ TaskBoard.renderCollaborators = renderCollaborators;
397
+ TaskBoard.renderCard = renderCard;
398
+ TaskBoard.renderBoard = renderBoard;
399
+ TaskBoard.renderStats = renderStats;
400
+ TaskBoard.showMoreCards = showMoreCards;
401
+ TaskBoard.resetColumnVisibleCounts = resetColumnVisibleCounts;
@@ -0,0 +1,143 @@
1
+ // =============================================================================
2
+ // agent-tasks — Drag and Drop Module
3
+ //
4
+ // Drag-and-drop logic, auto-scroll during drag.
5
+ // =============================================================================
6
+
7
+ window.TaskBoard = window.TaskBoard || {};
8
+
9
+ var draggedTaskId = null;
10
+ var dragScrollInterval = null;
11
+ var _lastMouseX = 0;
12
+
13
+ function onDragStart(e, card) {
14
+ draggedTaskId = parseInt(card.dataset.taskId, 10);
15
+ e.dataTransfer.effectAllowed = 'move';
16
+ e.dataTransfer.setData('text/plain', String(draggedTaskId));
17
+
18
+ requestAnimationFrame(() => {
19
+ card.classList.add('dragging');
20
+ });
21
+
22
+ startDragAutoScroll();
23
+ }
24
+
25
+ function onDragEnd(e) {
26
+ const card = e.target.closest('.task-card');
27
+ if (card) card.classList.remove('dragging');
28
+ draggedTaskId = null;
29
+ stopDragAutoScroll();
30
+ document
31
+ .querySelectorAll('.kanban-column.drag-over')
32
+ .forEach((c) => c.classList.remove('drag-over'));
33
+ document.querySelectorAll('.drop-placeholder').forEach((p) => p.remove());
34
+ const board = document.getElementById('board');
35
+ board.classList.remove('drag-scroll-left', 'drag-scroll-right');
36
+ }
37
+
38
+ function onDragOver(e, col) {
39
+ e.preventDefault();
40
+ e.dataTransfer.dropEffect = 'move';
41
+ if (col && !col.classList.contains('drag-over')) {
42
+ document
43
+ .querySelectorAll('.kanban-column.drag-over')
44
+ .forEach((c) => c.classList.remove('drag-over'));
45
+ col.classList.add('drag-over');
46
+ }
47
+ }
48
+
49
+ function onDrop(e, col) {
50
+ var state = TaskBoard.state;
51
+ var showToast = TaskBoard.showToast;
52
+
53
+ e.preventDefault();
54
+ if (col) col.classList.remove('drag-over');
55
+ document.querySelectorAll('.drop-placeholder').forEach((p) => p.remove());
56
+
57
+ if (!draggedTaskId) return;
58
+ const targetStage = col.dataset.stage;
59
+ const task = state.tasks.find((t) => t.id === draggedTaskId);
60
+ if (!task || task.stage === targetStage) return;
61
+
62
+ fetch(`/api/tasks/${draggedTaskId}/stage`, {
63
+ method: 'PUT',
64
+ headers: { 'Content-Type': 'application/json' },
65
+ body: JSON.stringify({ stage: targetStage }),
66
+ })
67
+ .then((r) => r.json())
68
+ .then((result) => {
69
+ if (result.error) {
70
+ showToast('Move failed', result.error, 'error');
71
+ }
72
+ })
73
+ .catch(() => showToast('Move failed', 'Network error', 'error'));
74
+ }
75
+
76
+ function startDragAutoScroll() {
77
+ const board = document.getElementById('board');
78
+ dragScrollInterval = setInterval(() => {
79
+ if (!draggedTaskId) return;
80
+ const rect = board.getBoundingClientRect();
81
+ const mouseX = _lastMouseX;
82
+ const edgeSize = 80;
83
+
84
+ if (mouseX < rect.left + edgeSize) {
85
+ board.scrollLeft -= 8;
86
+ board.classList.add('drag-scroll-left');
87
+ } else {
88
+ board.classList.remove('drag-scroll-left');
89
+ }
90
+
91
+ if (mouseX > rect.right - edgeSize) {
92
+ board.scrollLeft += 8;
93
+ board.classList.add('drag-scroll-right');
94
+ } else {
95
+ board.classList.remove('drag-scroll-right');
96
+ }
97
+ }, 16);
98
+ }
99
+
100
+ function stopDragAutoScroll() {
101
+ clearInterval(dragScrollInterval);
102
+ dragScrollInterval = null;
103
+ }
104
+
105
+ // ---- Drag event wiring ----
106
+
107
+ function initDragEvents() {
108
+ var board = document.getElementById('board');
109
+
110
+ board.addEventListener('dragstart', (e) => {
111
+ const card = e.target.closest('.task-card[data-task-id]');
112
+ if (card) onDragStart(e, card);
113
+ });
114
+
115
+ board.addEventListener('dragend', (e) => {
116
+ onDragEnd(e);
117
+ });
118
+
119
+ board.addEventListener('dragover', (e) => {
120
+ const col = e.target.closest('.kanban-column');
121
+ if (col) onDragOver(e, col);
122
+ });
123
+
124
+ board.addEventListener('dragleave', (e) => {
125
+ const col = e.target.closest('.kanban-column');
126
+ if (col && !col.contains(e.relatedTarget)) {
127
+ col.classList.remove('drag-over');
128
+ }
129
+ });
130
+
131
+ board.addEventListener('drop', (e) => {
132
+ const col = e.target.closest('.kanban-column');
133
+ if (col) onDrop(e, col);
134
+ });
135
+
136
+ document.addEventListener('dragover', (e) => {
137
+ _lastMouseX = e.clientX;
138
+ });
139
+ }
140
+
141
+ // ---- Register on namespace ----
142
+
143
+ TaskBoard.initDragEvents = initDragEvents;
@@ -160,6 +160,11 @@
160
160
  />
161
161
  <script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11/build/highlight.min.js"></script>
162
162
  <script src="morphdom.min.js"></script>
163
+ <script src="ui-utils.js"></script>
164
+ <script src="board.js"></script>
165
+ <script src="panel.js"></script>
166
+ <script src="drag.js"></script>
167
+ <script src="inline-edit.js"></script>
163
168
  <script src="app.js"></script>
164
169
  </body>
165
170
  </html>