mdboard 1.3.0 → 2.1.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 (53) hide show
  1. package/bin.js +117 -59
  2. package/index.html +2161 -1579
  3. package/package.json +7 -5
  4. package/presets/kanban/api.json +91 -0
  5. package/presets/kanban/cli.json +69 -0
  6. package/presets/kanban/docs.json +29 -0
  7. package/presets/kanban/entities.json +128 -0
  8. package/presets/kanban/structure.json +15 -0
  9. package/presets/kanban/ui.json +86 -0
  10. package/presets/scrum/api.json +98 -0
  11. package/presets/scrum/cli.json +120 -0
  12. package/presets/scrum/docs.json +43 -0
  13. package/presets/scrum/entities.json +268 -0
  14. package/presets/scrum/structure.json +32 -0
  15. package/presets/scrum/ui.json +201 -0
  16. package/presets/shape-up/api.json +40 -0
  17. package/presets/shape-up/cli.json +44 -0
  18. package/presets/shape-up/docs.json +32 -0
  19. package/presets/shape-up/entities.json +140 -0
  20. package/presets/shape-up/structure.json +28 -0
  21. package/presets/shape-up/ui.json +114 -0
  22. package/src/cli/cli.js +186 -210
  23. package/src/cli/config.js +234 -0
  24. package/src/cli/init.js +128 -76
  25. package/src/cli/preset.js +849 -0
  26. package/src/cli/skill.js +417 -0
  27. package/src/cli/status.js +126 -96
  28. package/src/core/config.js +491 -38
  29. package/src/core/history.js +17 -1
  30. package/src/core/scanner.js +373 -463
  31. package/src/core/workspace.js +0 -15
  32. package/src/server/api.js +464 -741
  33. package/src/server/server.js +105 -130
  34. package/build.js +0 -44
  35. package/defaults.json +0 -43
  36. package/src/cli/sync.js +0 -194
  37. package/src/cli/theme.js +0 -142
  38. package/src/client/app.js +0 -266
  39. package/src/client/board.js +0 -157
  40. package/src/client/core.js +0 -331
  41. package/src/client/editor.js +0 -318
  42. package/src/client/history.js +0 -137
  43. package/src/client/metrics.js +0 -38
  44. package/src/client/milestones.js +0 -77
  45. package/src/client/notes.js +0 -183
  46. package/src/client/overview.js +0 -104
  47. package/src/client/panel.js +0 -637
  48. package/src/client/styles.css +0 -471
  49. package/src/client/table.js +0 -111
  50. package/src/client/template.html +0 -144
  51. package/src/client/themes.js +0 -261
  52. package/src/client/workspace.js +0 -164
  53. package/src/core/agent-scanner.js +0 -260
@@ -1,637 +0,0 @@
1
- /* ══════════════════════════════════════════════════════════════
2
- CRUD HELPERS
3
- ══════════════════════════════════════════════════════════════ */
4
- function isSourceWritable() {
5
- if (!hasWorkspace() || !D.activeSource) return true;
6
- var src = D.config.workspace.sources.find(function(s) { return s.name === D.activeSource; });
7
- return src ? !src.readonly : true;
8
- }
9
-
10
- function openCreateDialog(collection) {
11
- var item = { _isNew: true };
12
-
13
- if (collection === 'tasks') {
14
- var ms = D.milestones.length > 0 ? (D.milestones[0].id || D.milestones[0]._dir || '') : '';
15
- var ep = D.epics.length > 0 ? (D.epics[0]._dir || D.epics[0].id || '') : '';
16
- item.title = '';
17
- item.status = 'backlog';
18
- item.priority = '';
19
- item.points = null;
20
- item.assigned = '';
21
- item.sprint = '';
22
- item.milestone = ms;
23
- item.epic = ep;
24
- item.content = '';
25
- } else if (collection === 'milestones') {
26
- item.title = '';
27
- item.status = 'planned';
28
- item.deadline = '';
29
- item.content = '';
30
- } else if (collection === 'epics') {
31
- var ms = D.milestones.length > 0 ? (D.milestones[0]._dir || D.milestones[0].id || '') : '';
32
- item.title = '';
33
- item.status = 'active';
34
- item.priority = '';
35
- item.milestone = ms;
36
- item.content = '';
37
- }
38
-
39
- panelState = { open: true, type: collection, item: item, isCreate: true, editor: null };
40
- renderPanel();
41
- document.getElementById('detail-panel').classList.add('open');
42
- document.getElementById('panel-overlay').classList.add('open');
43
- }
44
-
45
- /* ══════════════════════════════════════════════════════════════
46
- DETAIL PANEL — Notion-style: form top + editor bottom
47
- ══════════════════════════════════════════════════════════════ */
48
- var panelState = { open: false, type: null, item: null, isCreate: false, editor: null };
49
-
50
- function openPanel(type, item) {
51
- panelState = { open: true, type: type, item: JSON.parse(JSON.stringify(item)), isCreate: false, editor: null };
52
- renderPanel();
53
- document.getElementById('detail-panel').classList.add('open');
54
- document.getElementById('panel-overlay').classList.add('open');
55
- }
56
-
57
- function closePanel() {
58
- if (panelState.editor) { destroyEditor(panelState.editor); panelState.editor = null; }
59
- panelState = { open: false, type: null, item: null, editor: null };
60
- document.getElementById('detail-panel').classList.remove('open');
61
- document.getElementById('panel-overlay').classList.remove('open');
62
- }
63
-
64
- function renderPanel() {
65
- var panel = document.getElementById('detail-panel');
66
- var t = panelState.type;
67
- var item = panelState.item;
68
- var isCreate = panelState.isCreate;
69
- if (!item) return;
70
-
71
- var isReadonly = !isCreate && item.readonly;
72
-
73
- var typeLabel;
74
- if (t === 'tasks') typeLabel = ENTITY_NAMES.task ? ENTITY_NAMES.task.singular : 'Task';
75
- else if (t === 'epics') typeLabel = ENTITY_NAMES.epic ? ENTITY_NAMES.epic.singular : 'Epic';
76
- else typeLabel = ENTITY_NAMES.milestone ? ENTITY_NAMES.milestone.singular : 'Milestone';
77
-
78
- // ── Header ──
79
- var html = '<div class="panel-header">' +
80
- '<span class="panel-type">' + escHtml(isCreate ? 'New ' + typeLabel : typeLabel) + '</span>' +
81
- '<span class="panel-item-id">' + escHtml(isCreate ? '' : (item.id || '')) + '</span>';
82
-
83
- if (item.source && item.sourceColor) {
84
- html += '<span class="pill" style="background:' + item.sourceColor + '20;color:' + item.sourceColor + ';font-size:10px">' + escHtml(item.sourceLabel || item.source) + '</span>';
85
- }
86
-
87
- html += '<button class="panel-close" id="panel-close-btn">&times;</button></div>';
88
-
89
- // ── Title (large, Notion-style) ──
90
- html += '<div class="panel-title-wrap">' +
91
- '<input type="text" id="p-title" class="panel-title-input" value="' + escHtml(item.title || '') + '" placeholder="Untitled"' + (isReadonly ? ' disabled' : '') + '>' +
92
- '</div>';
93
-
94
- // ── Properties (compact inline) ──
95
- html += '<div class="panel-properties">';
96
-
97
- // Properties toggle header
98
- html += '<div class="prop-row prop-row-toggle" id="props-toggle"><span class="prop-label" style="font-weight:600"><span class="ai-toggle-arrow" id="props-arrow">&#9654;</span> Properties</span><div class="prop-value"></div></div>';
99
- html += '<div class="props-fields-body" id="props-fields-body" style="display:none">';
100
-
101
- // Status
102
- var statusOptions, statusLabels;
103
- if (t === 'tasks' && D.config && D.config.statuses && D.config.statuses.task) {
104
- statusOptions = D.config.statuses.task.map(function(s) { return s.key; });
105
- statusLabels = {};
106
- D.config.statuses.task.forEach(function(s) { statusLabels[s.key] = s.label; });
107
- } else if (t === 'epics' && D.config && D.config.statuses && D.config.statuses.epic) {
108
- statusOptions = D.config.statuses.epic.map(function(s) { return s.key; });
109
- statusLabels = {};
110
- D.config.statuses.epic.forEach(function(s) { statusLabels[s.key] = s.label; });
111
- } else if (t === 'milestones' && D.config && D.config.statuses && D.config.statuses.milestone) {
112
- statusOptions = D.config.statuses.milestone.map(function(s) { return s.key; });
113
- statusLabels = {};
114
- D.config.statuses.milestone.forEach(function(s) { statusLabels[s.key] = s.label; });
115
- } else if (t === 'tasks') {
116
- statusOptions = STATUSES;
117
- statusLabels = STATUS_LABELS;
118
- } else {
119
- statusOptions = ['planned', 'active', 'completed', 'cancelled'];
120
- statusLabels = { planned: 'Planned', active: 'Active', completed: 'Completed', cancelled: 'Cancelled' };
121
- }
122
-
123
- html += '<div class="prop-row"><span class="prop-label">Status</span><div class="prop-value"><span id="p-status-icon">' + statusIcon(item.status) + '</span>' +
124
- '<select id="p-status"' + (isReadonly ? ' disabled' : '') + '>' + statusOptions.map(function(s) {
125
- return '<option value="' + s + '"' + (item.status === s ? ' selected' : '') + '>' + (statusLabels[s] || s) + '</option>';
126
- }).join('') + '</select></div></div>';
127
-
128
- if (t === 'tasks' || t === 'epics') {
129
- html += '<div class="prop-row"><span class="prop-label">Priority</span><div class="prop-value"><span id="p-priority-icon">' + priorityIcon(item.priority) + '</span>' +
130
- '<select id="p-priority"' + (isReadonly ? ' disabled' : '') + '><option value="">None</option>' + PRIORITIES.map(function(p) {
131
- return '<option value="' + p + '"' + (item.priority === p ? ' selected' : '') + '>' + (PRIORITY_LABELS[p] || p) + '</option>';
132
- }).join('') + '</select></div></div>';
133
- }
134
-
135
- if (t === 'tasks') {
136
- html += '<div class="prop-row"><span class="prop-label">Points</span><div class="prop-value"><input type="number" id="p-points" min="0" max="100" value="' + (item.points != null ? item.points : '') + '" placeholder="0"' + (isReadonly ? ' disabled' : '') + '></div></div>';
137
-
138
- var assignedVal = Array.isArray(item.assigned) ? item.assigned.join(', ') : (item.assigned || '');
139
- html += '<div class="prop-row"><span class="prop-label">Assigned</span><div class="prop-value"><input type="text" id="p-assigned" value="' + escHtml(assignedVal) + '" placeholder="Unassigned"' + (isReadonly ? ' disabled' : '') + '></div></div>';
140
-
141
- html += '<div class="prop-row"><span class="prop-label">Sprint</span><div class="prop-value"><select id="p-sprint"' + (isReadonly ? ' disabled' : '') + '><option value="">None</option>' +
142
- D.allSprints.map(function(s) { return '<option value="' + escHtml(s.id || '') + '"' + (item.sprint === s.id ? ' selected' : '') + '>' + escHtml(s.id || '') + '</option>'; }).join('') +
143
- '</select></div></div>';
144
-
145
- if (isCreate) {
146
- html += '<div class="prop-row"><span class="prop-label">Milestone</span><div class="prop-value"><select id="p-milestone">' +
147
- D.milestones.map(function(m) { return '<option value="' + escHtml(m._dir || m.id || '') + '"' + (item.milestone === (m._dir || m.id) ? ' selected' : '') + '>' + escHtml(m.title || m.id || '') + '</option>'; }).join('') +
148
- '</select></div></div>';
149
- html += '<div class="prop-row"><span class="prop-label">Epic</span><div class="prop-value"><select id="p-epic-select">' +
150
- D.epics.map(function(e) { return '<option value="' + escHtml(e._dir || e.id || '') + '"' + (item.epic === (e._dir || e.id) ? ' selected' : '') + '>' + escHtml(e.title || e.id || '') + '</option>'; }).join('') +
151
- '</select></div></div>';
152
- } else {
153
- html += '<div class="prop-row"><span class="prop-label">Epic</span><div class="prop-value"><span class="prop-text">' + escHtml(item.epic || 'None') + '</span></div></div>';
154
- }
155
- }
156
-
157
- if (t === 'milestones') {
158
- html += '<div class="prop-row"><span class="prop-label">Deadline</span><div class="prop-value"><input type="date" id="p-deadline" value="' + fmtDate(item.deadline) + '"' + (isReadonly ? ' disabled' : '') + '></div></div>';
159
- }
160
-
161
- if (isCreate && (t === 'epics' || t === 'milestones')) {
162
- if (t === 'epics') {
163
- html += '<div class="prop-row"><span class="prop-label">Milestone</span><div class="prop-value"><select id="p-milestone">' +
164
- D.milestones.map(function(m) { return '<option value="' + escHtml(m._dir || m.id || '') + '">' + escHtml(m.title || m.id || '') + '</option>'; }).join('') +
165
- '</select></div></div>';
166
- }
167
- }
168
-
169
- // Links
170
- if (t === 'tasks' && !isCreate && item.links && Array.isArray(item.links) && item.links.length > 0) {
171
- html += '<div class="prop-row"><span class="prop-label">Links</span><div class="prop-value"><div class="link-chips">';
172
- item.links.forEach(function(link) {
173
- var parts = String(link).split(':');
174
- var srcName = parts.length === 2 ? parts[0] : null;
175
- var srcColor = 'var(--accent)';
176
- if (srcName && D.config && D.config.workspace && D.config.workspace.sources) {
177
- var src = D.config.workspace.sources.find(function(s) { return s.name === srcName; });
178
- if (src && src.color) srcColor = src.color;
179
- }
180
- html += '<span class="link-chip" data-link="' + escHtml(String(link)) + '">' +
181
- '<span class="link-chip-dot" style="background:' + srcColor + '"></span>' +
182
- escHtml(String(link)) + '</span>';
183
- });
184
- html += '</div></div></div>';
185
- }
186
-
187
- // Reverse links
188
- if (t === 'tasks' && !isCreate && D.overviewLinks && D.overviewLinks.reverseLinks && item.id) {
189
- var reverseRefs = D.overviewLinks.reverseLinks[item.id] || [];
190
- if (reverseRefs.length > 0) {
191
- html += '<div class="prop-row"><span class="prop-label">Referenced By</span><div class="prop-value"><div class="link-chips">';
192
- reverseRefs.forEach(function(ref) {
193
- var srcColor = ref.sourceColor || 'var(--accent)';
194
- html += '<span class="link-chip" data-link="' + escHtml(ref.from || '') + '">' +
195
- '<span class="link-chip-dot" style="background:' + srcColor + '"></span>' +
196
- escHtml(ref.from || '') + '</span>';
197
- });
198
- html += '</div></div></div>';
199
- }
200
- }
201
-
202
- html += '</div>'; // close props-fields-body
203
-
204
- // ── AI Properties — integrated as prop-rows ──
205
- var aiData = item.ai || {};
206
- var aiOwnData = item.aiOwn || {};
207
- var aiFieldDefs = [
208
- { key: 'skills', label: 'Skills', icon: '<svg class="icon icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>' },
209
- { key: 'agents', label: 'Agents', icon: '<svg class="icon icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="10" rx="2"/><circle cx="12" cy="5" r="3"/><circle cx="9" cy="16" r="1" fill="currentColor"/><circle cx="15" cy="16" r="1" fill="currentColor"/></svg>' },
210
- { key: 'mcps', label: 'MCPs', icon: '<svg class="icon icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16v16H4z"/><path d="M9 9h6v6H9z"/><path d="M9 1v3M15 1v3M9 20v3M15 20v3M1 9h3M1 15h3M20 9h3M20 15h3"/></svg>' },
211
- { key: 'commands', label: 'Commands', icon: '<svg class="icon icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>' },
212
- { key: 'context', label: 'Context', icon: '<svg class="icon icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>' }
213
- ];
214
- var aiTotalCount = 0;
215
- for (var ai = 0; ai < aiFieldDefs.length; ai++) {
216
- var arr = aiData[aiFieldDefs[ai].key];
217
- if (arr) aiTotalCount += arr.length;
218
- }
219
- var showAiSection = aiTotalCount > 0 || !isReadonly;
220
- if (showAiSection) {
221
- // AI divider row — clickable toggle
222
- html += '<div class="prop-row prop-row-divider prop-row-toggle" id="ai-toggle"><span class="prop-label" style="font-weight:600"><span class="ai-toggle-arrow" id="ai-arrow">&#9654;</span> AI</span><div class="prop-value">';
223
- if (aiTotalCount > 0) html += '<span class="pill" style="font-size:10px;padding:1px 6px">' + aiTotalCount + '</span>';
224
- html += '</div></div>';
225
- html += '<div class="ai-fields-body" id="ai-fields-body" style="display:none">';
226
- for (var ag = 0; ag < aiFieldDefs.length; ag++) {
227
- var def = aiFieldDefs[ag];
228
- var resolved = aiData[def.key] || [];
229
- var own = aiOwnData[def.key] || [];
230
- var inherited = resolved.filter(function(v) { return own.indexOf(v) === -1; });
231
- if (isReadonly && resolved.length === 0) continue;
232
- html += '<div class="prop-row"><span class="prop-label">' + def.icon + ' ' + escHtml(def.label) + '</span><div class="prop-value">';
233
- if (isReadonly) {
234
- // Readonly: just show tags
235
- for (var ci = 0; ci < resolved.length; ci++) {
236
- var isOwn = own.indexOf(resolved[ci]) !== -1;
237
- html += '<span class="ai-tag' + (isOwn ? ' ai-tag-own' : ' ai-tag-inherited') + '">' + escHtml(resolved[ci]) + '</span>';
238
- }
239
- } else {
240
- // Editable: tag input container
241
- html += '<div class="ai-tag-input" data-ai-field="' + def.key + '">';
242
- // Inherited tags (dashed, no remove)
243
- for (var ii = 0; ii < inherited.length; ii++) {
244
- html += '<span class="ai-tag ai-tag-inherited">' + escHtml(inherited[ii]) + '</span>';
245
- }
246
- // Own tags (accent, with remove button)
247
- for (var oi = 0; oi < own.length; oi++) {
248
- html += '<span class="ai-tag ai-tag-own"><span class="ai-tag-val">' + escHtml(own[oi]) + '</span><button class="ai-tag-remove" type="button">&times;</button></span>';
249
- }
250
- // Text input for adding
251
- html += '<input type="text" class="ai-tag-text" placeholder="Add...">';
252
- // Autocomplete dropdown
253
- html += '<div class="ai-autocomplete"></div>';
254
- html += '</div>';
255
- }
256
- html += '</div></div>';
257
- }
258
- html += '</div>'; // close ai-fields-body
259
- }
260
-
261
- html += '</div>'; // close panel-properties
262
-
263
- // ── Content Editor (fills remaining space) ──
264
- if (isReadonly) {
265
- html += '<div class="panel-content-area"><div id="p-content-editor" class="panel-editor-zone md-rendered">' + simpleMarkdownRender(item.content || '') + '</div></div>';
266
- } else {
267
- html += '<div class="panel-content-area"><div id="p-content-editor" class="panel-editor-zone"></div></div>';
268
- }
269
-
270
- // ── Footer ──
271
- html += '<div class="panel-footer">';
272
- if (!isCreate && !isReadonly && isSourceWritable()) {
273
- html += '<button class="btn btn-danger btn-sm" id="panel-archive-btn">Archive</button>';
274
- }
275
- html += '<span style="flex:1"></span>';
276
- html += '<button class="btn" id="panel-cancel-btn">Cancel</button>';
277
- if (!isReadonly) {
278
- html += '<button class="btn btn-primary" id="panel-save-btn">' + (isCreate ? 'Create' : 'Save') + '</button>';
279
- }
280
- html += '</div>';
281
-
282
- panel.innerHTML = html;
283
-
284
- // Initialize Editor.js for content (if not readonly)
285
- if (!isReadonly) {
286
- panelState.editor = initEditor('p-content-editor', item.content || '', {
287
- placeholder: 'Type / for commands, or start writing...',
288
- minHeight: 0,
289
- });
290
- }
291
-
292
- // Event listeners
293
- document.getElementById('panel-close-btn').addEventListener('click', closePanel);
294
- document.getElementById('panel-cancel-btn').addEventListener('click', closePanel);
295
- var saveBtn = document.getElementById('panel-save-btn');
296
- if (saveBtn) saveBtn.addEventListener('click', isCreate ? saveCreatePanel : savePanel);
297
-
298
- var archiveBtn = document.getElementById('panel-archive-btn');
299
- if (archiveBtn) {
300
- archiveBtn.addEventListener('click', function() {
301
- if (!confirm('Archive this ' + typeLabel.toLowerCase() + '?')) return;
302
- deleteItem(t, item.id).then(function(ok) {
303
- if (ok) { closePanel(); refreshData(); }
304
- });
305
- });
306
- }
307
-
308
- // Properties section toggle
309
- var propsToggle = document.getElementById('props-toggle');
310
- if (propsToggle) {
311
- propsToggle.addEventListener('click', function() {
312
- var body = document.getElementById('props-fields-body');
313
- var arrow = document.getElementById('props-arrow');
314
- if (body.style.display === 'none') {
315
- body.style.display = '';
316
- arrow.innerHTML = '&#9660;';
317
- } else {
318
- body.style.display = 'none';
319
- arrow.innerHTML = '&#9654;';
320
- }
321
- });
322
- }
323
-
324
- // AI section toggle
325
- var aiToggle = document.getElementById('ai-toggle');
326
- if (aiToggle) {
327
- aiToggle.addEventListener('click', function() {
328
- var body = document.getElementById('ai-fields-body');
329
- var arrow = document.getElementById('ai-arrow');
330
- if (body.style.display === 'none') {
331
- body.style.display = '';
332
- arrow.innerHTML = '&#9660;';
333
- } else {
334
- body.style.display = 'none';
335
- arrow.innerHTML = '&#9654;';
336
- }
337
- });
338
- }
339
-
340
- // AI tag input event listeners
341
- panel.querySelectorAll('.ai-tag-input').forEach(function(container) {
342
- var field = container.dataset.aiField;
343
- var textInput = container.querySelector('.ai-tag-text');
344
- var dropdown = container.querySelector('.ai-autocomplete');
345
- if (!textInput || !dropdown) return;
346
-
347
- function getOwnValues() {
348
- var vals = [];
349
- container.querySelectorAll('.ai-tag-own .ai-tag-val').forEach(function(el) {
350
- vals.push(el.textContent);
351
- });
352
- return vals;
353
- }
354
-
355
- function getAllValues() {
356
- var vals = [];
357
- container.querySelectorAll('.ai-tag').forEach(function(el) {
358
- var valEl = el.querySelector('.ai-tag-val');
359
- vals.push(valEl ? valEl.textContent : el.textContent);
360
- });
361
- return vals;
362
- }
363
-
364
- function addTag(value) {
365
- var v = value.trim();
366
- if (!v) return;
367
- var existing = getAllValues();
368
- if (existing.indexOf(v) !== -1) return;
369
- var tag = document.createElement('span');
370
- tag.className = 'ai-tag ai-tag-own';
371
- tag.innerHTML = '<span class="ai-tag-val">' + escHtml(v) + '</span><button class="ai-tag-remove" type="button">&times;</button>';
372
- tag.querySelector('.ai-tag-remove').addEventListener('click', function() { tag.remove(); });
373
- container.insertBefore(tag, textInput);
374
- textInput.value = '';
375
- closeDropdown();
376
- }
377
-
378
- function closeDropdown() {
379
- dropdown.classList.remove('open');
380
- dropdown.innerHTML = '';
381
- }
382
-
383
- function showDropdown() {
384
- var suggestions = D.aiSuggestions ? (D.aiSuggestions[field] || []) : [];
385
- var present = getAllValues();
386
- var query = textInput.value.trim().toLowerCase();
387
- var filtered = suggestions.filter(function(s) {
388
- return present.indexOf(s) === -1 && (!query || s.toLowerCase().indexOf(query) !== -1);
389
- });
390
- if (filtered.length === 0) { closeDropdown(); return; }
391
- dropdown.innerHTML = '';
392
- filtered.forEach(function(s) {
393
- var item = document.createElement('div');
394
- item.className = 'ai-autocomplete-item';
395
- item.textContent = s;
396
- item.addEventListener('mousedown', function(e) {
397
- e.preventDefault();
398
- addTag(s);
399
- });
400
- dropdown.appendChild(item);
401
- });
402
- dropdown.classList.add('open');
403
- }
404
-
405
- // Click on × to remove own tag
406
- container.querySelectorAll('.ai-tag-remove').forEach(function(btn) {
407
- btn.addEventListener('click', function() { btn.closest('.ai-tag').remove(); });
408
- });
409
-
410
- // Click on container focuses input
411
- container.addEventListener('click', function(e) {
412
- if (e.target === container) textInput.focus();
413
- });
414
-
415
- // Input typing → show autocomplete
416
- textInput.addEventListener('input', showDropdown);
417
- textInput.addEventListener('focus', showDropdown);
418
-
419
- // Enter → add tag
420
- textInput.addEventListener('keydown', function(e) {
421
- if (e.key === 'Enter') {
422
- e.preventDefault();
423
- if (textInput.value.trim()) addTag(textInput.value);
424
- } else if (e.key === 'Backspace' && textInput.value === '') {
425
- // Remove last own tag
426
- var ownTags = container.querySelectorAll('.ai-tag-own');
427
- if (ownTags.length > 0) ownTags[ownTags.length - 1].remove();
428
- } else if (e.key === 'Escape') {
429
- closeDropdown();
430
- textInput.blur();
431
- }
432
- });
433
-
434
- // Close dropdown on blur
435
- textInput.addEventListener('blur', function() {
436
- setTimeout(closeDropdown, 150);
437
- });
438
- });
439
-
440
- // Live icon updates
441
- var statusSel = document.getElementById('p-status');
442
- if (statusSel) {
443
- statusSel.addEventListener('change', function() {
444
- var iconEl = document.getElementById('p-status-icon');
445
- if (iconEl) iconEl.innerHTML = statusIcon(statusSel.value);
446
- });
447
- }
448
- var prioritySel = document.getElementById('p-priority');
449
- if (prioritySel) {
450
- prioritySel.addEventListener('change', function() {
451
- var iconEl = document.getElementById('p-priority-icon');
452
- if (iconEl) iconEl.innerHTML = priorityIcon(prioritySel.value);
453
- });
454
- }
455
-
456
- // Ctrl/Cmd+S to save
457
- panel.addEventListener('keydown', function(e) {
458
- if ((e.metaKey || e.ctrlKey) && e.key === 's') {
459
- e.preventDefault();
460
- if (isCreate) saveCreatePanel();
461
- else savePanel();
462
- }
463
- });
464
-
465
- // Link chip click handlers
466
- panel.querySelectorAll('.link-chip[data-link]').forEach(function(chip) {
467
- chip.addEventListener('click', function() {
468
- var ref = chip.dataset.link;
469
- var parts = ref.split(':');
470
- if (parts.length === 2 && hasWorkspace()) {
471
- closePanel();
472
- switchSource(parts[0]);
473
- setTimeout(function() {
474
- var task = D.tasks.find(function(t) { return t.id === ref || t.id === parts[1]; });
475
- if (task) openPanel('tasks', task);
476
- }, 500);
477
- }
478
- });
479
- });
480
- }
481
-
482
- async function savePanel() {
483
- var t = panelState.type;
484
- var item = panelState.item;
485
- if (!t || !item) return;
486
-
487
- var updates = {};
488
-
489
- var titleEl = document.getElementById('p-title');
490
- if (titleEl && titleEl.value !== (item.title || '')) updates.title = titleEl.value;
491
-
492
- var statusEl = document.getElementById('p-status');
493
- if (statusEl && statusEl.value !== (item.status || '')) updates.status = statusEl.value;
494
-
495
- if (t === 'tasks' || t === 'epics') {
496
- var priorityEl = document.getElementById('p-priority');
497
- if (priorityEl && priorityEl.value !== (item.priority || '')) updates.priority = priorityEl.value || null;
498
- }
499
-
500
- if (t === 'tasks') {
501
- var pointsEl = document.getElementById('p-points');
502
- if (pointsEl) {
503
- var pv = pointsEl.value ? Number(pointsEl.value) : null;
504
- if (pv !== item.points) updates.points = pv;
505
- }
506
-
507
- var assignedEl = document.getElementById('p-assigned');
508
- if (assignedEl) {
509
- var av = assignedEl.value.trim();
510
- var origAssigned = Array.isArray(item.assigned) ? item.assigned.join(', ') : (item.assigned || '');
511
- if (av !== origAssigned) updates.assigned = av || null;
512
- }
513
-
514
- var sprintEl = document.getElementById('p-sprint');
515
- if (sprintEl && sprintEl.value !== (item.sprint || '')) updates.sprint = sprintEl.value || null;
516
- }
517
-
518
- if (t === 'milestones') {
519
- var deadlineEl = document.getElementById('p-deadline');
520
- if (deadlineEl && deadlineEl.value !== fmtDate(item.deadline)) updates.deadline = deadlineEl.value || null;
521
- }
522
-
523
- if (panelState.editor) {
524
- var newContent = await getEditorMarkdown(panelState.editor);
525
- if (newContent !== (item.content || '')) updates.content = newContent;
526
- }
527
-
528
- // AI properties — read from tag inputs
529
- var aiTagInputs = document.querySelectorAll('.ai-tag-input[data-ai-field]');
530
- if (aiTagInputs.length > 0) {
531
- var aiObj = {};
532
- aiTagInputs.forEach(function(container) {
533
- var field = container.dataset.aiField;
534
- var vals = [];
535
- container.querySelectorAll('.ai-tag-own .ai-tag-val').forEach(function(el) {
536
- var v = el.textContent.trim();
537
- if (v) vals.push(v);
538
- });
539
- if (vals.length > 0) aiObj[field] = vals;
540
- });
541
- if (Object.keys(aiObj).length > 0) {
542
- updates.ai = aiObj;
543
- } else if (item.aiOwn && Object.keys(item.aiOwn).length > 0) {
544
- updates.ai = null;
545
- }
546
- }
547
-
548
- if (Object.keys(updates).length === 0) {
549
- showToast('No changes to save', 'success');
550
- closePanel();
551
- return;
552
- }
553
-
554
- var ok = await patchItem(t, item.id, updates);
555
- if (ok) {
556
- closePanel();
557
- refreshData();
558
- }
559
- }
560
-
561
- async function saveCreatePanel() {
562
- var t = panelState.type;
563
- if (!t) return;
564
-
565
- var data = {};
566
-
567
- var titleEl = document.getElementById('p-title');
568
- if (titleEl) data.title = titleEl.value || 'Untitled';
569
-
570
- var statusEl = document.getElementById('p-status');
571
- if (statusEl) data.status = statusEl.value;
572
-
573
- if (t === 'tasks' || t === 'epics') {
574
- var priorityEl = document.getElementById('p-priority');
575
- if (priorityEl && priorityEl.value) data.priority = priorityEl.value;
576
- }
577
-
578
- if (t === 'tasks') {
579
- var pointsEl = document.getElementById('p-points');
580
- if (pointsEl && pointsEl.value) data.points = Number(pointsEl.value);
581
-
582
- var assignedEl = document.getElementById('p-assigned');
583
- if (assignedEl && assignedEl.value.trim()) data.assigned = assignedEl.value.trim();
584
-
585
- var sprintEl = document.getElementById('p-sprint');
586
- if (sprintEl && sprintEl.value) data.sprint = sprintEl.value;
587
-
588
- var milestoneEl = document.getElementById('p-milestone');
589
- if (milestoneEl) data.milestone = milestoneEl.value;
590
-
591
- var epicEl = document.getElementById('p-epic-select');
592
- if (epicEl) data.epic = epicEl.value;
593
- }
594
-
595
- if (t === 'epics' || t === 'milestones') {
596
- var milestoneEl = document.getElementById('p-milestone');
597
- if (milestoneEl) data.milestone = milestoneEl.value;
598
- }
599
-
600
- if (t === 'milestones') {
601
- var deadlineEl = document.getElementById('p-deadline');
602
- if (deadlineEl && deadlineEl.value) data.deadline = deadlineEl.value;
603
- }
604
-
605
- if (panelState.editor) {
606
- var editorContent = await getEditorMarkdown(panelState.editor);
607
- if (editorContent) data.content = editorContent;
608
- }
609
-
610
- // AI properties — read from tag inputs
611
- var aiTagInputs = document.querySelectorAll('.ai-tag-input[data-ai-field]');
612
- if (aiTagInputs.length > 0) {
613
- var aiObj = {};
614
- aiTagInputs.forEach(function(container) {
615
- var field = container.dataset.aiField;
616
- var vals = [];
617
- container.querySelectorAll('.ai-tag-own .ai-tag-val').forEach(function(el) {
618
- var v = el.textContent.trim();
619
- if (v) vals.push(v);
620
- });
621
- if (vals.length > 0) aiObj[field] = vals;
622
- });
623
- if (Object.keys(aiObj).length > 0) data.ai = aiObj;
624
- }
625
-
626
- var result = await createItem(t, data);
627
- if (result) {
628
- closePanel();
629
- refreshData();
630
- }
631
- }
632
-
633
- // Close panel on overlay click or Escape
634
- document.getElementById('panel-overlay').addEventListener('click', closePanel);
635
- document.addEventListener('keydown', function(e) {
636
- if (e.key === 'Escape' && panelState.open) closePanel();
637
- });