mdboard 1.2.0 → 1.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.
- package/bin.js +44 -16
- package/build.js +44 -0
- package/index.html +1835 -216
- package/package.json +7 -10
- package/src/cli/cli.js +362 -0
- package/src/cli/init.js +123 -0
- package/src/cli/status.js +150 -0
- package/src/cli/sync.js +194 -0
- package/src/cli/theme.js +142 -0
- package/src/client/app.js +266 -0
- package/src/client/board.js +157 -0
- package/src/client/core.js +331 -0
- package/src/client/editor.js +318 -0
- package/src/client/history.js +137 -0
- package/src/client/metrics.js +38 -0
- package/src/client/milestones.js +77 -0
- package/src/client/notes.js +183 -0
- package/src/client/overview.js +104 -0
- package/src/client/panel.js +637 -0
- package/src/client/styles.css +471 -0
- package/src/client/table.js +111 -0
- package/src/client/template.html +144 -0
- package/src/client/themes.js +261 -0
- package/src/client/workspace.js +164 -0
- package/src/core/agent-scanner.js +260 -0
- package/{config.js → src/core/config.js} +27 -2
- package/src/core/history.js +130 -0
- package/{scanner.js → src/core/scanner.js} +141 -21
- package/{yaml.js → src/core/yaml.js} +5 -1
- package/{api.js → src/server/api.js} +150 -9
- package/{server.js → src/server/server.js} +105 -32
- package/{watcher.js → src/server/watcher.js} +40 -9
- package/init.js +0 -109
- /package/{workspace.js → src/core/workspace.js} +0 -0
|
@@ -0,0 +1,183 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
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
|
+
}
|