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.
- package/bin.js +117 -59
- package/index.html +2161 -1579
- package/package.json +7 -5
- package/presets/kanban/api.json +91 -0
- package/presets/kanban/cli.json +69 -0
- package/presets/kanban/docs.json +29 -0
- package/presets/kanban/entities.json +128 -0
- package/presets/kanban/structure.json +15 -0
- package/presets/kanban/ui.json +86 -0
- package/presets/scrum/api.json +98 -0
- package/presets/scrum/cli.json +120 -0
- package/presets/scrum/docs.json +43 -0
- package/presets/scrum/entities.json +268 -0
- package/presets/scrum/structure.json +32 -0
- package/presets/scrum/ui.json +201 -0
- package/presets/shape-up/api.json +40 -0
- package/presets/shape-up/cli.json +44 -0
- package/presets/shape-up/docs.json +32 -0
- package/presets/shape-up/entities.json +140 -0
- package/presets/shape-up/structure.json +28 -0
- package/presets/shape-up/ui.json +114 -0
- package/src/cli/cli.js +186 -210
- package/src/cli/config.js +234 -0
- package/src/cli/init.js +128 -76
- package/src/cli/preset.js +849 -0
- package/src/cli/skill.js +417 -0
- package/src/cli/status.js +126 -96
- package/src/core/config.js +491 -38
- package/src/core/history.js +17 -1
- package/src/core/scanner.js +373 -463
- package/src/core/workspace.js +0 -15
- package/src/server/api.js +464 -741
- package/src/server/server.js +105 -130
- package/build.js +0 -44
- package/defaults.json +0 -43
- package/src/cli/sync.js +0 -194
- package/src/cli/theme.js +0 -142
- package/src/client/app.js +0 -266
- package/src/client/board.js +0 -157
- package/src/client/core.js +0 -331
- package/src/client/editor.js +0 -318
- package/src/client/history.js +0 -137
- package/src/client/metrics.js +0 -38
- package/src/client/milestones.js +0 -77
- package/src/client/notes.js +0 -183
- package/src/client/overview.js +0 -104
- package/src/client/panel.js +0 -637
- package/src/client/styles.css +0 -471
- package/src/client/table.js +0 -111
- package/src/client/template.html +0 -144
- package/src/client/themes.js +0 -261
- package/src/client/workspace.js +0 -164
- package/src/core/agent-scanner.js +0 -260
package/src/client/history.js
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
/* ══════════════════════════════════════════════════════════════
|
|
2
|
-
HISTORY — Project switch modal
|
|
3
|
-
══════════════════════════════════════════════════════════════ */
|
|
4
|
-
|
|
5
|
-
function openHistoryModal() {
|
|
6
|
-
var overlay = document.getElementById('history-overlay');
|
|
7
|
-
var modal = document.getElementById('history-modal');
|
|
8
|
-
if (!overlay || !modal) return;
|
|
9
|
-
|
|
10
|
-
overlay.classList.add('open');
|
|
11
|
-
modal.classList.add('open');
|
|
12
|
-
modal.innerHTML = '<div class="history-header"><h2>Switch Project</h2><button class="panel-close" id="history-close">×</button></div><div class="history-body"><div class="history-loading">Loading...</div></div>';
|
|
13
|
-
|
|
14
|
-
document.getElementById('history-close').onclick = closeHistoryModal;
|
|
15
|
-
overlay.onclick = closeHistoryModal;
|
|
16
|
-
|
|
17
|
-
fetchJson('/api/history').then(function(entries) {
|
|
18
|
-
renderHistoryList(entries || []);
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function closeHistoryModal() {
|
|
23
|
-
var overlay = document.getElementById('history-overlay');
|
|
24
|
-
var modal = document.getElementById('history-modal');
|
|
25
|
-
if (overlay) overlay.classList.remove('open');
|
|
26
|
-
if (modal) modal.classList.remove('open');
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function renderHistoryList(entries) {
|
|
30
|
-
var modal = document.getElementById('history-modal');
|
|
31
|
-
if (!modal) return;
|
|
32
|
-
|
|
33
|
-
var body = modal.querySelector('.history-body');
|
|
34
|
-
if (!body) return;
|
|
35
|
-
|
|
36
|
-
if (entries.length === 0) {
|
|
37
|
-
body.innerHTML = '<div class="history-empty">No projects in history yet.<br>Open mdboard from a project directory to add it.</div>';
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Sort: current first, then by lastOpened descending
|
|
42
|
-
entries.sort(function(a, b) {
|
|
43
|
-
if (a.isCurrent && !b.isCurrent) return -1;
|
|
44
|
-
if (!a.isCurrent && b.isCurrent) return 1;
|
|
45
|
-
return new Date(b.lastOpened) - new Date(a.lastOpened);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
var html = '';
|
|
49
|
-
for (var i = 0; i < entries.length; i++) {
|
|
50
|
-
var e = entries[i];
|
|
51
|
-
var initial = (e.name || 'P').charAt(0).toUpperCase();
|
|
52
|
-
var relTime = timeAgo(e.lastOpened);
|
|
53
|
-
var currentClass = e.isCurrent ? ' history-item-current' : '';
|
|
54
|
-
var currentBadge = e.isCurrent ? '<span class="history-current-badge">Current</span>' : '';
|
|
55
|
-
|
|
56
|
-
html += '<div class="history-item' + currentClass + '" data-path="' + escHtml(e.path) + '">' +
|
|
57
|
-
'<div class="history-item-icon">' + escHtml(initial) + '</div>' +
|
|
58
|
-
'<div class="history-item-info">' +
|
|
59
|
-
'<div class="history-item-name">' + escHtml(e.name) + currentBadge + '</div>' +
|
|
60
|
-
'<div class="history-item-path">' + escHtml(e.path) + '</div>' +
|
|
61
|
-
'</div>' +
|
|
62
|
-
'<div class="history-item-time">' + escHtml(relTime) + '</div>' +
|
|
63
|
-
'</div>';
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
body.innerHTML = html;
|
|
67
|
-
|
|
68
|
-
// Attach click handlers
|
|
69
|
-
var items = body.querySelectorAll('.history-item');
|
|
70
|
-
for (var j = 0; j < items.length; j++) {
|
|
71
|
-
items[j].addEventListener('click', function() {
|
|
72
|
-
var itemPath = this.getAttribute('data-path');
|
|
73
|
-
if (this.classList.contains('history-item-current')) return;
|
|
74
|
-
switchProject(itemPath);
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function switchProject(projectPath) {
|
|
80
|
-
var body = document.querySelector('#history-modal .history-body');
|
|
81
|
-
if (body) body.innerHTML = '<div class="history-loading">Switching project...</div>';
|
|
82
|
-
|
|
83
|
-
fetch('/api/history/switch', {
|
|
84
|
-
method: 'POST',
|
|
85
|
-
headers: { 'Content-Type': 'application/json' },
|
|
86
|
-
body: JSON.stringify({ path: projectPath }),
|
|
87
|
-
})
|
|
88
|
-
.then(function(r) { return r.json(); })
|
|
89
|
-
.then(function(data) {
|
|
90
|
-
if (data && data.ok) {
|
|
91
|
-
closeHistoryModal();
|
|
92
|
-
// Reload everything — the SSE event will also trigger for other tabs
|
|
93
|
-
D.activeSource = null;
|
|
94
|
-
D.loaded = false;
|
|
95
|
-
loadAll().then(function() {
|
|
96
|
-
initFromConfig();
|
|
97
|
-
if (hasWorkspace()) {
|
|
98
|
-
buildSourceRail();
|
|
99
|
-
D.activeSource = null;
|
|
100
|
-
switchSource('overview');
|
|
101
|
-
} else {
|
|
102
|
-
renderAll();
|
|
103
|
-
renderSidebarLogo();
|
|
104
|
-
// Hide source rail if no workspace
|
|
105
|
-
var rail = document.getElementById('source-rail');
|
|
106
|
-
if (rail) rail.innerHTML = '';
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
showToast('Switched to ' + (data.name || 'project'), 'success');
|
|
110
|
-
} else {
|
|
111
|
-
showToast('Error: ' + (data.error || 'Switch failed'), 'error');
|
|
112
|
-
closeHistoryModal();
|
|
113
|
-
}
|
|
114
|
-
})
|
|
115
|
-
.catch(function(err) {
|
|
116
|
-
showToast('Error: ' + err.message, 'error');
|
|
117
|
-
closeHistoryModal();
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function checkAutoOpenHistory() {
|
|
122
|
-
if (D.config && D.config.hasProject === false) {
|
|
123
|
-
openHistoryModal();
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function timeAgo(dateStr) {
|
|
128
|
-
if (!dateStr) return '';
|
|
129
|
-
var now = Date.now();
|
|
130
|
-
var then = new Date(dateStr).getTime();
|
|
131
|
-
var diff = Math.floor((now - then) / 1000);
|
|
132
|
-
if (diff < 60) return 'just now';
|
|
133
|
-
if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
|
|
134
|
-
if (diff < 86400) return Math.floor(diff / 3600) + 'h ago';
|
|
135
|
-
if (diff < 604800) return Math.floor(diff / 86400) + 'd ago';
|
|
136
|
-
return new Date(dateStr).toLocaleDateString();
|
|
137
|
-
}
|
package/src/client/metrics.js
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/* ══════════════════════════════════════════════════════════════
|
|
2
|
-
METRICS VIEW
|
|
3
|
-
══════════════════════════════════════════════════════════════ */
|
|
4
|
-
function renderMetrics() {
|
|
5
|
-
var c = document.getElementById('metrics-container');
|
|
6
|
-
var taskPlural = ENTITY_NAMES.task ? ENTITY_NAMES.task.plural : 'Tasks';
|
|
7
|
-
if (!D.loaded) { c.innerHTML = '<div class="metrics-grid">' + '<div class="skeleton skeleton-card" style="height:200px"></div>'.repeat(4) + '</div>'; return; }
|
|
8
|
-
|
|
9
|
-
var msProg = D.milestones.length ? D.milestones.map(function(ms) {
|
|
10
|
-
var pct = ms.progress || 0;
|
|
11
|
-
var clr = pct >= 100 ? 'var(--success)' : pct > 50 ? 'var(--warning)' : 'var(--accent)';
|
|
12
|
-
return '<div class="health-row"><div class="health-dot" style="background:' + clr + '"></div>' + milestoneIcon(ms.status) + '<div class="health-label">' + escHtml(ms.title || ms.id || '') + '</div><div class="health-val">' + pct + '%</div></div>';
|
|
13
|
-
}).join('') : '<div style="color:var(--text3);padding:8px 0">No milestones.</div>';
|
|
14
|
-
|
|
15
|
-
var statuses = {};
|
|
16
|
-
D.tasks.forEach(function(f) { var s = f.status || 'unknown'; statuses[s] = (statuses[s] || 0) + 1; });
|
|
17
|
-
var statusHtml = Object.keys(statuses).map(function(s) {
|
|
18
|
-
return '<div class="health-row">' + statusIcon(s) + '<div class="health-label" style="text-transform:capitalize">' + escHtml(STATUS_LABELS[s] || s) + '</div><div class="health-val">' + statuses[s] + '</div></div>';
|
|
19
|
-
}).join('') || '<div style="color:var(--text3);padding:8px 0">No data.</div>';
|
|
20
|
-
|
|
21
|
-
var priorities = {};
|
|
22
|
-
D.tasks.forEach(function(f) { var p = f.priority || 'none'; priorities[p] = (priorities[p] || 0) + 1; });
|
|
23
|
-
var priorityHtml = Object.keys(priorities).map(function(p) {
|
|
24
|
-
return '<div class="health-row">' + priorityIcon(p) + '<div class="health-label" style="text-transform:capitalize">' + escHtml(PRIORITY_LABELS[p] || p) + '</div><div class="health-val">' + priorities[p] + '</div></div>';
|
|
25
|
-
}).join('') || '<div style="color:var(--text3);padding:8px 0">No data.</div>';
|
|
26
|
-
|
|
27
|
-
var h = D.health || {};
|
|
28
|
-
var qualHtml = '<div class="health-row"><div class="health-dot" style="background:var(--text2)"></div><div class="health-label">Total ' + escHtml(taskPlural) + '</div><div class="health-val">' + (h.totalFeatures || D.tasks.length) + '</div></div>' +
|
|
29
|
-
'<div class="health-row"><div class="health-dot" style="background:var(--success)"></div><div class="health-label">Completed</div><div class="health-val">' + (h.completedFeatures || 0) + '</div></div>' +
|
|
30
|
-
'<div class="health-row"><div class="health-dot" style="background:var(--warning)"></div><div class="health-label">In Progress</div><div class="health-val">' + (h.inProgressFeatures || 0) + '</div></div>' +
|
|
31
|
-
'<div class="health-row"><div class="health-dot" style="background:var(--accent)"></div><div class="health-label">Avg Velocity</div><div class="health-val">' + (h.velocity != null ? h.velocity + '%' : 'N/A') + '</div></div>';
|
|
32
|
-
|
|
33
|
-
c.innerHTML = '<div class="metrics-grid">' +
|
|
34
|
-
'<div class="metric-card"><h3>Milestone Progress</h3>' + msProg + '</div>' +
|
|
35
|
-
'<div class="metric-card"><h3>Status Breakdown</h3>' + statusHtml + '</div>' +
|
|
36
|
-
'<div class="metric-card"><h3>Priority Breakdown</h3>' + priorityHtml + '</div>' +
|
|
37
|
-
'<div class="metric-card"><h3>Project Health</h3>' + qualHtml + '</div></div>';
|
|
38
|
-
}
|
package/src/client/milestones.js
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
/* ══════════════════════════════════════════════════════════════
|
|
2
|
-
MILESTONES VIEW
|
|
3
|
-
══════════════════════════════════════════════════════════════ */
|
|
4
|
-
var msFilter = { status: '' };
|
|
5
|
-
|
|
6
|
-
function renderMsFilters() {
|
|
7
|
-
var c = document.getElementById('ms-filters');
|
|
8
|
-
var msStatuses = (D.config && D.config.statuses && D.config.statuses.milestone) ?
|
|
9
|
-
D.config.statuses.milestone.map(function(s) { return s; }) :
|
|
10
|
-
[{key:'planned',label:'Planned'},{key:'active',label:'Active'},{key:'completed',label:'Completed'}];
|
|
11
|
-
|
|
12
|
-
c.innerHTML = '<label>Filter:</label>' +
|
|
13
|
-
'<select data-msf="status"><option value="">All Statuses</option>' +
|
|
14
|
-
msStatuses.map(function(s) {
|
|
15
|
-
return '<option value="' + s.key + '"' + (msFilter.status === s.key ? ' selected' : '') + '>' + escHtml(s.label) + '</option>';
|
|
16
|
-
}).join('') + '</select>';
|
|
17
|
-
c.querySelector('select[data-msf]').addEventListener('change', function(e) { msFilter.status = e.target.value; renderMilestones(); });
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function renderMilestones() {
|
|
21
|
-
var c = document.getElementById('milestones-container');
|
|
22
|
-
var taskPlural = ENTITY_NAMES.task ? ENTITY_NAMES.task.plural.toLowerCase() : 'tasks';
|
|
23
|
-
var msDir = ENTITY_NAMES.milestone ? ENTITY_NAMES.milestone.dir : 'milestones';
|
|
24
|
-
var msPlural = ENTITY_NAMES.milestone ? ENTITY_NAMES.milestone.plural.toLowerCase() : 'milestones';
|
|
25
|
-
if (!D.loaded) { c.innerHTML = '<div class="loading-container"><div class="skeleton skeleton-card" style="height:200px"></div></div>'; return; }
|
|
26
|
-
|
|
27
|
-
var milestones = D.milestones;
|
|
28
|
-
if (msFilter.status) milestones = milestones.filter(function(m) { return m.status === msFilter.status; });
|
|
29
|
-
|
|
30
|
-
if (!milestones.length) {
|
|
31
|
-
c.innerHTML = '<div class="empty"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/><line x1="4" y1="22" x2="4" y2="15"/></svg><p>' + (D.milestones.length ? 'No ' + escHtml(msPlural) + ' match filter.' : 'No ' + escHtml(msPlural) + ' yet. Create milestone directories under project/' + escHtml(msDir) + '/ to get started.') + '</p></div>';
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
c.innerHTML = milestones.map(function(ms) {
|
|
36
|
-
var pct = ms.progress || 0;
|
|
37
|
-
var fc = ms.featureCount || 0, cc = ms.completedCount || 0;
|
|
38
|
-
var msEpics = D.epics.filter(function(e) { return e.milestone === (ms.id || ms._dir); });
|
|
39
|
-
var epicCards = msEpics.length ? msEpics.map(function(e) {
|
|
40
|
-
var ep = e.progress || 0;
|
|
41
|
-
var ec = epicColor(e.id || e.title || '');
|
|
42
|
-
return '<div class="ms-epic" data-epic-id="' + escHtml(e.id || '') + '"><div class="epic-name"><span class="dot" style="background:' + ec + '"></span>' + escHtml(e.title || e.id || '') + '</div>' +
|
|
43
|
-
'<div class="epic-counts">' + (e.completedCount || 0) + ' / ' + (e.featureCount || 0) + ' ' + escHtml(taskPlural) + ' · ' + (e.totalPoints || 0) + ' pts</div>' +
|
|
44
|
-
'<div class="progress progress-accent"><div class="progress-fill" style="width:' + ep + '%;background:' + ec + '"></div></div>' +
|
|
45
|
-
'<div class="epic-meta">' +
|
|
46
|
-
(e.status ? '<span class="badge ' + badgeClass('badge', e.status) + '">' + statusIcon(e.status) + ' ' + escHtml(e.status) + '</span>' : '') +
|
|
47
|
-
(e.priority ? '<span class="badge ' + badgeClass('badge', e.priority) + '">' + priorityIcon(e.priority) + ' ' + escHtml(e.priority) + '</span>' : '') +
|
|
48
|
-
'</div></div>';
|
|
49
|
-
}).join('') : '<div style="color:var(--text3);font-size:.82rem">No epics yet.</div>';
|
|
50
|
-
|
|
51
|
-
return '<div class="ms-card" data-ms-id="' + escHtml(ms.id || '') + '">' +
|
|
52
|
-
'<div class="ms-header"><h2>' + milestoneIcon(ms.status) + ' ' + escHtml(ms.title || ms.id || '') + '</h2>' +
|
|
53
|
-
'<span class="badge ' + badgeClass('badge', ms.status) + '">' + escHtml(ms.status || '') + '</span>' +
|
|
54
|
-
(ms.deadline ? '<span class="ms-deadline">' + fmtDate(ms.deadline) + '</span>' : '') + '</div>' +
|
|
55
|
-
'<div class="ms-progress"><div class="progress-label"><span>' + cc + ' / ' + fc + ' ' + escHtml(taskPlural) + '</span><span>' + pct + '%</span></div>' +
|
|
56
|
-
'<div class="progress progress-lg progress-success"><div class="progress-fill" style="width:' + pct + '%"></div></div></div>' +
|
|
57
|
-
'<div class="ms-epics">' + epicCards + '</div></div>';
|
|
58
|
-
}).join('');
|
|
59
|
-
|
|
60
|
-
// Click handlers for milestones
|
|
61
|
-
c.querySelectorAll('.ms-card[data-ms-id]').forEach(function(card) {
|
|
62
|
-
card.addEventListener('click', function(e) {
|
|
63
|
-
if (e.target.closest('.ms-epic')) return;
|
|
64
|
-
var ms = D.milestones.find(function(m) { return m.id === card.dataset.msId; });
|
|
65
|
-
if (ms) openPanel('milestones', ms);
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
// Click handlers for epics
|
|
70
|
-
c.querySelectorAll('.ms-epic[data-epic-id]').forEach(function(card) {
|
|
71
|
-
card.addEventListener('click', function(e) {
|
|
72
|
-
e.stopPropagation();
|
|
73
|
-
var epic = D.epics.find(function(ep) { return ep.id === card.dataset.epicId; });
|
|
74
|
-
if (epic) openPanel('epics', epic);
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
}
|
package/src/client/notes.js
DELETED
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
/* ══════════════════════════════════════════════════════════════
|
|
2
|
-
NOTES VIEW — Notion-style split layout
|
|
3
|
-
══════════════════════════════════════════════════════════════ */
|
|
4
|
-
|
|
5
|
-
var notesState = { notes: [], activeId: null, editor: null, dirty: false };
|
|
6
|
-
|
|
7
|
-
function renderNotes() {
|
|
8
|
-
var container = document.getElementById('notes-container');
|
|
9
|
-
if (!container) return;
|
|
10
|
-
|
|
11
|
-
container.innerHTML =
|
|
12
|
-
'<div class="notes-layout">' +
|
|
13
|
-
'<div class="notes-sidebar">' +
|
|
14
|
-
'<div class="notes-toolbar">' +
|
|
15
|
-
'<input type="text" placeholder="Search..." id="notes-search">' +
|
|
16
|
-
'<button class="btn btn-sm btn-create" id="notes-new">+ New</button>' +
|
|
17
|
-
'</div>' +
|
|
18
|
-
'<div class="notes-list" id="notes-list"></div>' +
|
|
19
|
-
'</div>' +
|
|
20
|
-
'<div class="notes-editor" id="notes-editor-pane">' +
|
|
21
|
-
'<div class="notes-empty-state">' +
|
|
22
|
-
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" style="width:48px;height:48px;opacity:.3;margin-bottom:12px"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>' +
|
|
23
|
-
'<p>Select a note or create a new one</p>' +
|
|
24
|
-
'<p style="font-size:12px;margin-top:4px;color:var(--text3)">Use <kbd>/</kbd> for block commands</p>' +
|
|
25
|
-
'</div>' +
|
|
26
|
-
'</div>' +
|
|
27
|
-
'</div>';
|
|
28
|
-
|
|
29
|
-
loadNoteList();
|
|
30
|
-
|
|
31
|
-
document.getElementById('notes-new').addEventListener('click', createNewNote);
|
|
32
|
-
document.getElementById('notes-search').addEventListener('input', function() {
|
|
33
|
-
renderNoteList(this.value.toLowerCase());
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async function loadNoteList() {
|
|
38
|
-
var base = apiBase();
|
|
39
|
-
var notes = await fetchJson(base + '/notes');
|
|
40
|
-
notesState.notes = notes || [];
|
|
41
|
-
renderNoteList('');
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function renderNoteList(filter) {
|
|
45
|
-
var list = document.getElementById('notes-list');
|
|
46
|
-
if (!list) return;
|
|
47
|
-
|
|
48
|
-
var notes = notesState.notes;
|
|
49
|
-
if (filter) {
|
|
50
|
-
notes = notes.filter(function(n) {
|
|
51
|
-
return (n.title || '').toLowerCase().indexOf(filter) !== -1;
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
notes.sort(function(a, b) {
|
|
56
|
-
return (b.updated || b.created || '').localeCompare(a.updated || a.created || '');
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
if (notes.length === 0) {
|
|
60
|
-
list.innerHTML = '<div class="notes-list-empty">No notes yet</div>';
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
var html = '';
|
|
65
|
-
for (var i = 0; i < notes.length; i++) {
|
|
66
|
-
var n = notes[i];
|
|
67
|
-
var active = n.id === notesState.activeId ? ' active' : '';
|
|
68
|
-
html += '<div class="notes-list-item' + active + '" data-id="' + escHtml(n.id) + '">' +
|
|
69
|
-
'<div class="notes-list-title">' + escHtml(n.title || 'Untitled') + '</div>' +
|
|
70
|
-
'<div class="notes-list-date">' + escHtml(n.updated || n.created || '') + '</div>' +
|
|
71
|
-
'</div>';
|
|
72
|
-
}
|
|
73
|
-
list.innerHTML = html;
|
|
74
|
-
|
|
75
|
-
list.querySelectorAll('.notes-list-item').forEach(function(el) {
|
|
76
|
-
el.addEventListener('click', function() {
|
|
77
|
-
selectNote(el.dataset.id);
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
async function selectNote(id) {
|
|
83
|
-
if (notesState.editor) {
|
|
84
|
-
destroyEditor(notesState.editor);
|
|
85
|
-
notesState.editor = null;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
notesState.activeId = id;
|
|
89
|
-
renderNoteList(document.getElementById('notes-search') ? document.getElementById('notes-search').value.toLowerCase() : '');
|
|
90
|
-
|
|
91
|
-
var pane = document.getElementById('notes-editor-pane');
|
|
92
|
-
if (!pane) return;
|
|
93
|
-
|
|
94
|
-
pane.innerHTML = '<div class="notes-editor-loading">Loading...</div>';
|
|
95
|
-
|
|
96
|
-
var base = apiBase();
|
|
97
|
-
var note = await fetchJson(base + '/notes/' + encodeURIComponent(id));
|
|
98
|
-
if (!note) {
|
|
99
|
-
pane.innerHTML = '<div class="notes-empty-state"><p>Note not found</p></div>';
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
pane.innerHTML =
|
|
104
|
-
'<div class="notes-header-bar">' +
|
|
105
|
-
'<input type="text" class="notes-title-input" id="notes-title" placeholder="Untitled" value="' + escHtml(note.title || '') + '">' +
|
|
106
|
-
'<span class="notes-meta">' + escHtml(note.updated || note.created || '') + '</span>' +
|
|
107
|
-
'</div>' +
|
|
108
|
-
'<div class="notes-editorjs-wrap" id="notes-editorjs"></div>' +
|
|
109
|
-
'<div class="notes-editor-footer">' +
|
|
110
|
-
'<button class="btn btn-danger btn-sm" id="notes-delete">Delete</button>' +
|
|
111
|
-
'<span style="flex:1"></span>' +
|
|
112
|
-
'<button class="btn btn-primary" id="notes-save">Save</button>' +
|
|
113
|
-
'</div>';
|
|
114
|
-
|
|
115
|
-
notesState.editor = initEditor('notes-editorjs', note.content || '', {
|
|
116
|
-
placeholder: 'Type / for commands...',
|
|
117
|
-
autofocus: true,
|
|
118
|
-
onChange: function() { notesState.dirty = true; }
|
|
119
|
-
});
|
|
120
|
-
notesState.dirty = false;
|
|
121
|
-
|
|
122
|
-
document.getElementById('notes-save').addEventListener('click', saveCurrentNote);
|
|
123
|
-
document.getElementById('notes-delete').addEventListener('click', function() {
|
|
124
|
-
deleteCurrentNote(id);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
// Ctrl/Cmd+S to save
|
|
128
|
-
pane.addEventListener('keydown', function(e) {
|
|
129
|
-
if ((e.metaKey || e.ctrlKey) && e.key === 's') {
|
|
130
|
-
e.preventDefault();
|
|
131
|
-
saveCurrentNote();
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
async function saveCurrentNote() {
|
|
137
|
-
if (!notesState.activeId) return;
|
|
138
|
-
|
|
139
|
-
var titleEl = document.getElementById('notes-title');
|
|
140
|
-
var title = titleEl ? titleEl.value : '';
|
|
141
|
-
var content = notesState.editor ? await getEditorMarkdown(notesState.editor) : '';
|
|
142
|
-
|
|
143
|
-
var updates = {};
|
|
144
|
-
var current = notesState.notes.find(function(n) { return n.id === notesState.activeId; });
|
|
145
|
-
if (title && (!current || title !== current.title)) updates.title = title;
|
|
146
|
-
updates.content = content;
|
|
147
|
-
updates.updated = new Date().toISOString().split('T')[0];
|
|
148
|
-
|
|
149
|
-
var ok = await patchItem('notes', notesState.activeId, updates);
|
|
150
|
-
if (ok) {
|
|
151
|
-
notesState.dirty = false;
|
|
152
|
-
await loadNoteList();
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
async function createNewNote() {
|
|
157
|
-
var result = await createItem('notes', { title: 'Untitled', content: '' });
|
|
158
|
-
if (result && result.id) {
|
|
159
|
-
await loadNoteList();
|
|
160
|
-
selectNote(result.id);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
async function deleteCurrentNote(id) {
|
|
165
|
-
if (!confirm('Delete this note?')) return;
|
|
166
|
-
var ok = await deleteItem('notes', id);
|
|
167
|
-
if (ok) {
|
|
168
|
-
if (notesState.editor) {
|
|
169
|
-
destroyEditor(notesState.editor);
|
|
170
|
-
notesState.editor = null;
|
|
171
|
-
}
|
|
172
|
-
notesState.activeId = null;
|
|
173
|
-
await loadNoteList();
|
|
174
|
-
var pane = document.getElementById('notes-editor-pane');
|
|
175
|
-
if (pane) {
|
|
176
|
-
pane.innerHTML =
|
|
177
|
-
'<div class="notes-empty-state">' +
|
|
178
|
-
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" style="width:48px;height:48px;opacity:.3;margin-bottom:12px"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>' +
|
|
179
|
-
'<p>Select a note or create a new one</p>' +
|
|
180
|
-
'</div>';
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
package/src/client/overview.js
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
/* ══════════════════════════════════════════════════════════════
|
|
2
|
-
OVERVIEW VIEW
|
|
3
|
-
══════════════════════════════════════════════════════════════ */
|
|
4
|
-
async function renderOverview() {
|
|
5
|
-
var c = document.getElementById('overview-container');
|
|
6
|
-
if (!D.loaded) { c.innerHTML = '<div class="loading-container"><div class="skeleton skeleton-card" style="height:200px"></div></div>'; return; }
|
|
7
|
-
|
|
8
|
-
// Fetch overview data
|
|
9
|
-
var overviewMs = await fetchJson('/api/overview/milestones') || [];
|
|
10
|
-
var overviewLinks = await fetchJson('/api/overview/links');
|
|
11
|
-
var overviewMetrics = await fetchJson('/api/overview/metrics');
|
|
12
|
-
|
|
13
|
-
D.overviewLinks = overviewLinks;
|
|
14
|
-
|
|
15
|
-
var html = '<h2 style="margin-bottom:16px;font-size:16px;font-weight:700">Workspace Overview</h2>';
|
|
16
|
-
|
|
17
|
-
// Global Milestones
|
|
18
|
-
html += '<h3 style="font-size:13px;text-transform:uppercase;letter-spacing:.04em;color:var(--text2);font-weight:600;margin:16px 0 8px">Global Milestones</h3>';
|
|
19
|
-
if (overviewMs.length > 0) {
|
|
20
|
-
html += overviewMs.map(function(ms) {
|
|
21
|
-
var pct = ms.combinedProgress != null ? ms.combinedProgress : (ms.progress || 0);
|
|
22
|
-
var tracked = ms.tracked || [];
|
|
23
|
-
var trackedHtml = '';
|
|
24
|
-
if (tracked.length > 0) {
|
|
25
|
-
trackedHtml = '<div class="tracked-ms"><div class="tracked-ms-header">Tracked Sub-Milestones</div>' +
|
|
26
|
-
tracked.map(function(t) {
|
|
27
|
-
return '<div class="tracked-ms-item"><span class="dot" style="background:' + (t.sourceColor || 'var(--accent)') + '"></span>' +
|
|
28
|
-
'<span style="font-size:12px;flex:1">' + escHtml(t.title || t.id || '') + '</span>' +
|
|
29
|
-
'<div class="progress progress-accent" style="width:80px"><div class="progress-fill" style="width:' + t.progress + '%;background:' + (t.sourceColor || 'var(--accent)') + '"></div></div>' +
|
|
30
|
-
'<span class="tracked-ms-pct">' + t.progress + '%</span></div>';
|
|
31
|
-
}).join('') + '</div>';
|
|
32
|
-
}
|
|
33
|
-
return '<div class="ms-card">' +
|
|
34
|
-
'<div class="ms-header"><h2>' + milestoneIcon(ms.status) + ' ' + escHtml(ms.title || ms.id || '') + '</h2>' +
|
|
35
|
-
'<span class="badge ' + badgeClass('badge', ms.status) + '">' + escHtml(ms.status || '') + '</span>' +
|
|
36
|
-
(ms.deadline ? '<span class="ms-deadline">' + fmtDate(ms.deadline) + '</span>' : '') + '</div>' +
|
|
37
|
-
'<div class="ms-progress"><div class="progress-label"><span>' + (ms.completedCount || 0) + ' / ' + (ms.featureCount || 0) + ' tasks</span><span>' + pct + '%</span></div>' +
|
|
38
|
-
'<div class="progress progress-lg progress-success"><div class="progress-fill" style="width:' + pct + '%"></div></div></div>' +
|
|
39
|
-
trackedHtml + '</div>';
|
|
40
|
-
}).join('');
|
|
41
|
-
} else {
|
|
42
|
-
html += '<div style="color:var(--text3);padding:8px 0">No milestones found across sources.</div>';
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Cross-project links
|
|
46
|
-
if (overviewLinks && overviewLinks.links && overviewLinks.links.length > 0) {
|
|
47
|
-
html += '<h3 style="font-size:13px;text-transform:uppercase;letter-spacing:.04em;color:var(--text2);font-weight:600;margin:24px 0 8px">Cross-Project Links</h3>';
|
|
48
|
-
html += '<div class="metrics-grid">';
|
|
49
|
-
var linkGroups = {};
|
|
50
|
-
overviewLinks.links.forEach(function(l) {
|
|
51
|
-
var key = (l.fromSource || 'unknown');
|
|
52
|
-
if (!linkGroups[key]) linkGroups[key] = [];
|
|
53
|
-
linkGroups[key].push(l);
|
|
54
|
-
});
|
|
55
|
-
Object.keys(linkGroups).forEach(function(src) {
|
|
56
|
-
var links = linkGroups[src];
|
|
57
|
-
html += '<div class="metric-card"><h3>' + escHtml(src) + ' Links</h3>';
|
|
58
|
-
html += links.map(function(l) {
|
|
59
|
-
return '<div class="health-row"><span style="font-size:12px;font-family:var(--mono)">' + escHtml(l.from || '') + '</span>' +
|
|
60
|
-
'<span style="color:var(--text3);margin:0 4px">→</span>' +
|
|
61
|
-
'<span class="link-chip" data-link="' + escHtml(l.to || '') + '">' +
|
|
62
|
-
'<span class="link-chip-dot" style="background:var(--accent)"></span>' + escHtml(l.to || '') + '</span></div>';
|
|
63
|
-
}).join('');
|
|
64
|
-
html += '</div>';
|
|
65
|
-
});
|
|
66
|
-
html += '</div>';
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Source metrics
|
|
70
|
-
if (overviewMetrics && overviewMetrics.sources) {
|
|
71
|
-
html += '<h3 style="font-size:13px;text-transform:uppercase;letter-spacing:.04em;color:var(--text2);font-weight:600;margin:24px 0 8px">Source Metrics</h3>';
|
|
72
|
-
html += '<div class="metrics-grid">';
|
|
73
|
-
Object.keys(overviewMetrics.sources).forEach(function(key) {
|
|
74
|
-
var m = overviewMetrics.sources[key];
|
|
75
|
-
var pct = m.totalTasks > 0 ? Math.round((m.completedTasks / m.totalTasks) * 100) : 0;
|
|
76
|
-
html += '<div class="metric-card" style="border-left:3px solid ' + (m.color || 'var(--accent)') + '">' +
|
|
77
|
-
'<h3>' + escHtml(m.label || key) + '</h3>' +
|
|
78
|
-
'<div class="health-row"><div class="health-label">Tasks</div><div class="health-val">' + m.totalTasks + '</div></div>' +
|
|
79
|
-
'<div class="health-row"><div class="health-label">Completed</div><div class="health-val">' + m.completedTasks + '</div></div>' +
|
|
80
|
-
'<div class="health-row"><div class="health-label">Points</div><div class="health-val">' + m.totalPoints + '</div></div>' +
|
|
81
|
-
'<div class="progress progress-lg progress-success" style="margin-top:8px"><div class="progress-fill" style="width:' + pct + '%;background:' + (m.color || 'var(--success)') + '"></div></div>' +
|
|
82
|
-
'</div>';
|
|
83
|
-
});
|
|
84
|
-
html += '</div>';
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
c.innerHTML = html;
|
|
88
|
-
|
|
89
|
-
// Click handlers for link chips
|
|
90
|
-
c.querySelectorAll('.link-chip[data-link]').forEach(function(chip) {
|
|
91
|
-
chip.addEventListener('click', function() {
|
|
92
|
-
var ref = chip.dataset.link;
|
|
93
|
-
var parts = ref.split(':');
|
|
94
|
-
if (parts.length === 2) {
|
|
95
|
-
switchSource(parts[0]);
|
|
96
|
-
// After switch, try to find and open the item
|
|
97
|
-
setTimeout(function() {
|
|
98
|
-
var task = D.tasks.find(function(t) { return t.id === ref || t.id === parts[1]; });
|
|
99
|
-
if (task) openPanel('tasks', task);
|
|
100
|
-
}, 500);
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
}
|