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
package/src/cli/sync.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mdboard — Consistency checker
|
|
3
|
+
*
|
|
4
|
+
* Detects dangling references and inconsistencies in the project model.
|
|
5
|
+
* With --fix, auto-corrects fixable issues.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* mdboard sync Report issues
|
|
9
|
+
* mdboard sync --fix Auto-fix fixable issues
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const { buildModel } = require('./cli');
|
|
15
|
+
const { updateMarkdownFile } = require('../core/scanner');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Run consistency checks on the project model.
|
|
19
|
+
*
|
|
20
|
+
* @param {string} projectDir - Workspace root directory
|
|
21
|
+
* @param {{ fix?: boolean }} opts - Options
|
|
22
|
+
*/
|
|
23
|
+
function runSync(projectDir, opts) {
|
|
24
|
+
const fix = opts && opts.fix;
|
|
25
|
+
const { model, config, projectPath } = buildModel(projectDir);
|
|
26
|
+
|
|
27
|
+
const issues = [];
|
|
28
|
+
|
|
29
|
+
const taskIds = new Set(model.tasks.map(t => t.id));
|
|
30
|
+
const sprintIds = new Set(model.sprints.map(s => s.id));
|
|
31
|
+
const epicIds = new Set(model.epics.map(e => e.id));
|
|
32
|
+
const milestoneIds = new Set(model.milestones.map(m => m.id));
|
|
33
|
+
|
|
34
|
+
// 1. sprint.features[] references tasks that don't exist
|
|
35
|
+
for (const sprint of model.sprints) {
|
|
36
|
+
if (!sprint.features || !Array.isArray(sprint.features)) continue;
|
|
37
|
+
const missing = sprint.features.filter(f => !taskIds.has(f));
|
|
38
|
+
if (missing.length > 0) {
|
|
39
|
+
issues.push({
|
|
40
|
+
type: 'error',
|
|
41
|
+
fixable: true,
|
|
42
|
+
message: `Sprint ${sprint.id}: features[] references missing tasks: ${missing.join(', ')}`,
|
|
43
|
+
fix: () => {
|
|
44
|
+
const newFeatures = sprint.features.filter(f => taskIds.has(f));
|
|
45
|
+
updateMarkdownFile(projectPath, sprint._file, { features: newFeatures });
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 2. task.sprint references sprint that doesn't exist
|
|
52
|
+
for (const task of model.tasks) {
|
|
53
|
+
if (!task.sprint) continue;
|
|
54
|
+
if (!sprintIds.has(task.sprint)) {
|
|
55
|
+
issues.push({
|
|
56
|
+
type: 'error',
|
|
57
|
+
fixable: true,
|
|
58
|
+
message: `Task ${task.id}: sprint "${task.sprint}" does not exist`,
|
|
59
|
+
fix: () => {
|
|
60
|
+
updateMarkdownFile(projectPath, task._file, { sprint: null });
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 3. epic.dependencies[] references epics that don't exist
|
|
67
|
+
for (const epic of model.epics) {
|
|
68
|
+
if (!epic.dependencies || !Array.isArray(epic.dependencies)) continue;
|
|
69
|
+
const missing = epic.dependencies.filter(d => !epicIds.has(d));
|
|
70
|
+
if (missing.length > 0) {
|
|
71
|
+
issues.push({
|
|
72
|
+
type: 'error',
|
|
73
|
+
fixable: true,
|
|
74
|
+
message: `Epic ${epic.id}: dependencies[] references missing epics: ${missing.join(', ')}`,
|
|
75
|
+
fix: () => {
|
|
76
|
+
const newDeps = epic.dependencies.filter(d => epicIds.has(d));
|
|
77
|
+
updateMarkdownFile(projectPath, epic._file, { dependencies: newDeps });
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 4. milestone.tracks[] references milestones that don't exist
|
|
84
|
+
for (const ms of model.milestones) {
|
|
85
|
+
if (!ms.tracks || !Array.isArray(ms.tracks)) continue;
|
|
86
|
+
const missing = ms.tracks.filter(t => {
|
|
87
|
+
// tracks can reference source:MS-ID format
|
|
88
|
+
if (String(t).includes(':')) {
|
|
89
|
+
const parts = String(t).split(':');
|
|
90
|
+
const refId = parts[1];
|
|
91
|
+
return !model.milestones.some(m => m.id === t || m._originalId === refId || m.id === refId);
|
|
92
|
+
}
|
|
93
|
+
return !milestoneIds.has(t);
|
|
94
|
+
});
|
|
95
|
+
if (missing.length > 0) {
|
|
96
|
+
issues.push({
|
|
97
|
+
type: 'warning',
|
|
98
|
+
fixable: false,
|
|
99
|
+
message: `Milestone ${ms.id}: tracks[] references missing milestones: ${missing.join(', ')}`,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 5. Duplicate IDs in the same collection
|
|
105
|
+
const collections = [
|
|
106
|
+
{ name: 'tasks', items: model.tasks },
|
|
107
|
+
{ name: 'milestones', items: model.milestones },
|
|
108
|
+
{ name: 'epics', items: model.epics },
|
|
109
|
+
{ name: 'sprints', items: model.sprints },
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
for (const col of collections) {
|
|
113
|
+
const seen = new Map();
|
|
114
|
+
for (const item of col.items) {
|
|
115
|
+
if (!item.id) continue;
|
|
116
|
+
if (seen.has(item.id)) {
|
|
117
|
+
issues.push({
|
|
118
|
+
type: 'error',
|
|
119
|
+
fixable: false,
|
|
120
|
+
message: `Duplicate ${col.name.slice(0, -1)} ID "${item.id}": ${seen.get(item.id)} and ${item._file}`,
|
|
121
|
+
});
|
|
122
|
+
} else {
|
|
123
|
+
seen.set(item.id, item._file);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 6. Tasks under epic directory without epic README.md
|
|
129
|
+
const msDir = config.entities.milestone.dir;
|
|
130
|
+
const epicDir = config.entities.epic.dir;
|
|
131
|
+
const taskDir = config.entities.task.dir;
|
|
132
|
+
|
|
133
|
+
for (const task of model.tasks) {
|
|
134
|
+
if (!task._epic || !task._milestone) continue;
|
|
135
|
+
const epicReadmePath = path.join(projectPath, msDir, task._milestone, epicDir, task._epic, 'README.md');
|
|
136
|
+
if (!fs.existsSync(epicReadmePath)) {
|
|
137
|
+
issues.push({
|
|
138
|
+
type: 'warning',
|
|
139
|
+
fixable: false,
|
|
140
|
+
message: `Task ${task.id}: parent epic directory "${task._epic}" has no README.md`,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 7. Sprints under milestone without milestone README.md
|
|
146
|
+
for (const sprint of model.sprints) {
|
|
147
|
+
if (!sprint._milestone) continue;
|
|
148
|
+
const msReadmePath = path.join(projectPath, msDir, sprint._milestone, 'README.md');
|
|
149
|
+
if (!fs.existsSync(msReadmePath)) {
|
|
150
|
+
issues.push({
|
|
151
|
+
type: 'warning',
|
|
152
|
+
fixable: false,
|
|
153
|
+
message: `Sprint ${sprint.id}: parent milestone directory "${sprint._milestone}" has no README.md`,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Report
|
|
159
|
+
if (issues.length === 0) {
|
|
160
|
+
console.log('\n mdboard sync — No issues found.\n');
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const errors = issues.filter(i => i.type === 'error');
|
|
165
|
+
const warnings = issues.filter(i => i.type === 'warning');
|
|
166
|
+
const fixable = issues.filter(i => i.fixable);
|
|
167
|
+
|
|
168
|
+
console.log('');
|
|
169
|
+
for (const issue of issues) {
|
|
170
|
+
const prefix = issue.type === 'error' ? ' ERROR' : ' WARN ';
|
|
171
|
+
const suffix = issue.fixable ? ' [fixable]' : '';
|
|
172
|
+
console.log(`${prefix}: ${issue.message}${suffix}`);
|
|
173
|
+
}
|
|
174
|
+
console.log('');
|
|
175
|
+
|
|
176
|
+
if (fix && fixable.length > 0) {
|
|
177
|
+
for (const issue of fixable) {
|
|
178
|
+
issue.fix();
|
|
179
|
+
}
|
|
180
|
+
console.log(` mdboard sync --fix — Fixed ${fixable.length} issue${fixable.length > 1 ? 's' : ''}.`);
|
|
181
|
+
if (warnings.length > 0) {
|
|
182
|
+
console.log(` ${warnings.length} warning${warnings.length > 1 ? 's' : ''} remain (not auto-fixable).`);
|
|
183
|
+
}
|
|
184
|
+
console.log('');
|
|
185
|
+
} else {
|
|
186
|
+
console.log(` mdboard sync — ${issues.length} issue${issues.length > 1 ? 's' : ''} (${errors.length} error${errors.length > 1 ? 's' : ''}, ${warnings.length} warning${warnings.length > 1 ? 's' : ''}).${fixable.length > 0 ? ` ${fixable.length} fixable.` : ''}`);
|
|
187
|
+
if (fixable.length > 0) {
|
|
188
|
+
console.log(' Run `mdboard sync --fix` to auto-correct.');
|
|
189
|
+
}
|
|
190
|
+
console.log('');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
module.exports = { runSync };
|
package/src/cli/theme.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mdboard theme — Set or list available themes
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
|
|
9
|
+
const THEME_IDS = [
|
|
10
|
+
'default-dark',
|
|
11
|
+
'linear-dark', 'linear-light',
|
|
12
|
+
'jira-dark', 'jira-light',
|
|
13
|
+
'catppuccin-mocha', 'catppuccin-latte',
|
|
14
|
+
'ayu-dark', 'ayu-light',
|
|
15
|
+
'tokyo-night', 'tokyo-night-storm',
|
|
16
|
+
'notion-light', 'notion-dark',
|
|
17
|
+
'github-dark', 'github-light',
|
|
18
|
+
'dracula', 'nord',
|
|
19
|
+
'solarized-dark', 'solarized-light',
|
|
20
|
+
'one-dark',
|
|
21
|
+
'gruvbox-dark',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const THEME_NAMES = {
|
|
25
|
+
'default-dark': 'Default Dark',
|
|
26
|
+
'linear-dark': 'Linear Dark', 'linear-light': 'Linear Light',
|
|
27
|
+
'jira-dark': 'Jira Dark', 'jira-light': 'Jira Light',
|
|
28
|
+
'catppuccin-mocha': 'Catppuccin Mocha', 'catppuccin-latte': 'Catppuccin Latte',
|
|
29
|
+
'ayu-dark': 'Ayu Dark', 'ayu-light': 'Ayu Light',
|
|
30
|
+
'tokyo-night': 'Tokyo Night', 'tokyo-night-storm': 'Tokyo Night Storm',
|
|
31
|
+
'notion-light': 'Notion Light', 'notion-dark': 'Notion Dark',
|
|
32
|
+
'github-dark': 'GitHub Dark', 'github-light': 'GitHub Light',
|
|
33
|
+
'dracula': 'Dracula', 'nord': 'Nord',
|
|
34
|
+
'solarized-dark': 'Solarized Dark', 'solarized-light': 'Solarized Light',
|
|
35
|
+
'one-dark': 'One Dark',
|
|
36
|
+
'gruvbox-dark': 'Gruvbox Dark',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const THEME_VARS = {
|
|
40
|
+
'default-dark': {bg:'#0A0A0B',surface:'#141415',surface2:'#1A1A1C',border:'#232326',border2:'#2E2E33',text:'#E8E8ED',text2:'#8B8B93',text3:'#5A5A63',accent:'#5B6EF5','accent-dim':'rgba(91,110,245,.15)',success:'#2EA043','success-dim':'rgba(46,160,67,.15)',warning:'#D4A72C','warning-dim':'rgba(212,167,44,.15)',danger:'#DA3633','danger-dim':'rgba(218,54,51,.15)',purple:'#8B5CF6','purple-dim':'rgba(139,92,246,.15)'},
|
|
41
|
+
'linear-dark': {bg:'#101012',surface:'#1A1A1F',surface2:'#222228',border:'#2C2C35',border2:'#38384A',text:'#E2E2E9',text2:'#8E8E9E',text3:'#5C5C6E',accent:'#5E6AD2','accent-dim':'rgba(94,106,210,.15)',success:'#4DA771','success-dim':'rgba(77,167,113,.15)',warning:'#F2C94C','warning-dim':'rgba(242,201,76,.15)',danger:'#EB5757','danger-dim':'rgba(235,87,87,.15)',purple:'#BB87FC','purple-dim':'rgba(187,135,252,.15)'},
|
|
42
|
+
'linear-light': {bg:'#F9F9FB',surface:'#FFFFFF',surface2:'#F0F0F4',border:'#E0E0E6',border2:'#D0D0D8',text:'#1A1A2E',text2:'#6B6B80',text3:'#9090A0',accent:'#5E6AD2','accent-dim':'rgba(94,106,210,.1)',success:'#4DA771','success-dim':'rgba(77,167,113,.1)',warning:'#D4A017','warning-dim':'rgba(212,160,23,.1)',danger:'#EB5757','danger-dim':'rgba(235,87,87,.1)',purple:'#9B6DD7','purple-dim':'rgba(155,109,215,.1)'},
|
|
43
|
+
'jira-dark': {bg:'#0D1117',surface:'#161B22',surface2:'#1C2333',border:'#293040',border2:'#344050',text:'#DEE4EC',text2:'#8C96A5',text3:'#5A6577',accent:'#2684FF','accent-dim':'rgba(38,132,255,.15)',success:'#36B37E','success-dim':'rgba(54,179,126,.15)',warning:'#FFAB00','warning-dim':'rgba(255,171,0,.15)',danger:'#FF5630','danger-dim':'rgba(255,86,48,.15)',purple:'#6554C0','purple-dim':'rgba(101,84,192,.15)'},
|
|
44
|
+
'jira-light': {bg:'#F4F5F7',surface:'#FFFFFF',surface2:'#EBECF0',border:'#DFE1E6',border2:'#C1C7D0',text:'#172B4D',text2:'#6B778C',text3:'#97A0AF',accent:'#0052CC','accent-dim':'rgba(0,82,204,.1)',success:'#36B37E','success-dim':'rgba(54,179,126,.1)',warning:'#FF991F','warning-dim':'rgba(255,153,31,.1)',danger:'#FF5630','danger-dim':'rgba(255,86,48,.1)',purple:'#6554C0','purple-dim':'rgba(101,84,192,.1)'},
|
|
45
|
+
'catppuccin-mocha': {bg:'#1E1E2E',surface:'#262637',surface2:'#313244',border:'#3B3B52',border2:'#45475A',text:'#CDD6F4',text2:'#A6ADC8',text3:'#6C7086',accent:'#89B4FA','accent-dim':'rgba(137,180,250,.15)',success:'#A6E3A1','success-dim':'rgba(166,227,161,.15)',warning:'#F9E2AF','warning-dim':'rgba(249,226,175,.15)',danger:'#F38BA8','danger-dim':'rgba(243,139,168,.15)',purple:'#CBA6F7','purple-dim':'rgba(203,166,247,.15)'},
|
|
46
|
+
'catppuccin-latte': {bg:'#EFF1F5',surface:'#FFFFFF',surface2:'#E6E9EF',border:'#CCD0DA',border2:'#BCC0CC',text:'#4C4F69',text2:'#6C6F85',text3:'#9CA0B0',accent:'#1E66F5','accent-dim':'rgba(30,102,245,.1)',success:'#40A02B','success-dim':'rgba(64,160,43,.1)',warning:'#DF8E1D','warning-dim':'rgba(223,142,29,.1)',danger:'#D20F39','danger-dim':'rgba(210,15,57,.1)',purple:'#8839EF','purple-dim':'rgba(136,57,239,.1)'},
|
|
47
|
+
'ayu-dark': {bg:'#0B0E14',surface:'#0F1219',surface2:'#151920',border:'#1E222A',border2:'#272D38',text:'#BFBDB6',text2:'#6C7380',text3:'#4A5060',accent:'#E6B450','accent-dim':'rgba(230,180,80,.15)',success:'#7FD962','success-dim':'rgba(127,217,98,.15)',warning:'#FFB454','warning-dim':'rgba(255,180,84,.15)',danger:'#F07178','danger-dim':'rgba(240,113,120,.15)',purple:'#D2A6FF','purple-dim':'rgba(210,166,255,.15)'},
|
|
48
|
+
'ayu-light': {bg:'#F8F9FA',surface:'#FFFFFF',surface2:'#F0F1F2',border:'#E0E1E2',border2:'#D4D5D6',text:'#5C6166',text2:'#787B80',text3:'#ACB0B5',accent:'#FF9940','accent-dim':'rgba(255,153,64,.1)',success:'#6CBF43','success-dim':'rgba(108,191,67,.1)',warning:'#F2AE49','warning-dim':'rgba(242,174,73,.1)',danger:'#F07171','danger-dim':'rgba(240,113,113,.1)',purple:'#A37ACC','purple-dim':'rgba(163,122,204,.1)'},
|
|
49
|
+
'tokyo-night': {bg:'#1A1B26',surface:'#1F2335',surface2:'#24283B',border:'#2F3449',border2:'#3B4261',text:'#C0CAF5',text2:'#7982A9',text3:'#565F89',accent:'#7AA2F7','accent-dim':'rgba(122,162,247,.15)',success:'#9ECE6A','success-dim':'rgba(158,206,106,.15)',warning:'#E0AF68','warning-dim':'rgba(224,175,104,.15)',danger:'#F7768E','danger-dim':'rgba(247,118,142,.15)',purple:'#BB9AF7','purple-dim':'rgba(187,154,247,.15)'},
|
|
50
|
+
'tokyo-night-storm': {bg:'#24283B',surface:'#292E42',surface2:'#2F3549',border:'#3B4261',border2:'#464F78',text:'#C0CAF5',text2:'#7982A9',text3:'#565F89',accent:'#7AA2F7','accent-dim':'rgba(122,162,247,.15)',success:'#9ECE6A','success-dim':'rgba(158,206,106,.15)',warning:'#E0AF68','warning-dim':'rgba(224,175,104,.15)',danger:'#F7768E','danger-dim':'rgba(247,118,142,.15)',purple:'#BB9AF7','purple-dim':'rgba(187,154,247,.15)'},
|
|
51
|
+
'notion-light': {bg:'#F7F6F3',surface:'#FFFFFF',surface2:'#EEEEEC',border:'#E3E2DE',border2:'#D4D3CF',text:'#37352F',text2:'#6B6B6B',text3:'#9B9A97',accent:'#2383E2','accent-dim':'rgba(35,131,226,.1)',success:'#0F7B0F','success-dim':'rgba(15,123,15,.1)',warning:'#CB7B00','warning-dim':'rgba(203,123,0,.1)',danger:'#E03E3E','danger-dim':'rgba(224,62,62,.1)',purple:'#6940A5','purple-dim':'rgba(105,64,165,.1)'},
|
|
52
|
+
'notion-dark': {bg:'#191919',surface:'#202020',surface2:'#2B2B2B',border:'#363636',border2:'#444444',text:'#E3E2DE',text2:'#9B9B9B',text3:'#6B6B6B',accent:'#529CCA','accent-dim':'rgba(82,156,202,.15)',success:'#4DAB9A','success-dim':'rgba(77,171,154,.15)',warning:'#CB7B00','warning-dim':'rgba(203,123,0,.15)',danger:'#E03E3E','danger-dim':'rgba(224,62,62,.15)',purple:'#9A6DD7','purple-dim':'rgba(154,109,215,.15)'},
|
|
53
|
+
'github-dark': {bg:'#0D1117',surface:'#161B22',surface2:'#1C2128',border:'#30363D',border2:'#3D444D',text:'#E6EDF3',text2:'#8B949E',text3:'#6E7681',accent:'#58A6FF','accent-dim':'rgba(88,166,255,.15)',success:'#3FB950','success-dim':'rgba(63,185,80,.15)',warning:'#D29922','warning-dim':'rgba(210,153,34,.15)',danger:'#F85149','danger-dim':'rgba(248,81,73,.15)',purple:'#BC8CFF','purple-dim':'rgba(188,140,255,.15)'},
|
|
54
|
+
'github-light': {bg:'#F6F8FA',surface:'#FFFFFF',surface2:'#EAEEF2',border:'#D0D7DE',border2:'#BCC3CB',text:'#1F2328',text2:'#656D76',text3:'#8C959F',accent:'#0969DA','accent-dim':'rgba(9,105,218,.1)',success:'#1A7F37','success-dim':'rgba(26,127,55,.1)',warning:'#9A6700','warning-dim':'rgba(154,103,0,.1)',danger:'#CF222E','danger-dim':'rgba(207,34,46,.1)',purple:'#8250DF','purple-dim':'rgba(130,80,223,.1)'},
|
|
55
|
+
'dracula': {bg:'#1E1F29',surface:'#282A36',surface2:'#2E303E',border:'#3A3C4E',border2:'#44475A',text:'#F8F8F2',text2:'#ADB0C0',text3:'#6272A4',accent:'#BD93F9','accent-dim':'rgba(189,147,249,.15)',success:'#50FA7B','success-dim':'rgba(80,250,123,.15)',warning:'#F1FA8C','warning-dim':'rgba(241,250,140,.15)',danger:'#FF5555','danger-dim':'rgba(255,85,85,.15)',purple:'#FF79C6','purple-dim':'rgba(255,121,198,.15)'},
|
|
56
|
+
'nord': {bg:'#242933',surface:'#2E3440',surface2:'#353C4A',border:'#3B4252',border2:'#434C5E',text:'#ECEFF4',text2:'#A5B1C2',text3:'#697688',accent:'#88C0D0','accent-dim':'rgba(136,192,208,.15)',success:'#A3BE8C','success-dim':'rgba(163,190,140,.15)',warning:'#EBCB8B','warning-dim':'rgba(235,203,139,.15)',danger:'#BF616A','danger-dim':'rgba(191,97,106,.15)',purple:'#B48EAD','purple-dim':'rgba(180,142,173,.15)'},
|
|
57
|
+
'solarized-dark': {bg:'#002129',surface:'#002B36',surface2:'#073642',border:'#0A4050',border2:'#1A5060',text:'#93A1A1',text2:'#839496',text3:'#586E75',accent:'#268BD2','accent-dim':'rgba(38,139,210,.15)',success:'#859900','success-dim':'rgba(133,153,0,.15)',warning:'#B58900','warning-dim':'rgba(181,137,0,.15)',danger:'#DC322F','danger-dim':'rgba(220,50,47,.15)',purple:'#6C71C4','purple-dim':'rgba(108,113,196,.15)'},
|
|
58
|
+
'solarized-light': {bg:'#FDF6E3',surface:'#FFFFFF',surface2:'#EEE8D5',border:'#DDD6C1',border2:'#C9C2AD',text:'#586E75',text2:'#657B83',text3:'#93A1A1',accent:'#268BD2','accent-dim':'rgba(38,139,210,.1)',success:'#859900','success-dim':'rgba(133,153,0,.1)',warning:'#B58900','warning-dim':'rgba(181,137,0,.1)',danger:'#DC322F','danger-dim':'rgba(220,50,47,.1)',purple:'#6C71C4','purple-dim':'rgba(108,113,196,.1)'},
|
|
59
|
+
'one-dark': {bg:'#1E2127',surface:'#282C34',surface2:'#2C313C',border:'#3A3F4B',border2:'#4B5263',text:'#ABB2BF',text2:'#7F848E',text3:'#5C6370',accent:'#61AFEF','accent-dim':'rgba(97,175,239,.15)',success:'#98C379','success-dim':'rgba(152,195,121,.15)',warning:'#E5C07B','warning-dim':'rgba(229,192,123,.15)',danger:'#E06C75','danger-dim':'rgba(224,108,117,.15)',purple:'#C678DD','purple-dim':'rgba(198,120,221,.15)'},
|
|
60
|
+
'gruvbox-dark': {bg:'#1D2021',surface:'#282828',surface2:'#32302F',border:'#3C3836',border2:'#504945',text:'#EBDBB2',text2:'#A89984',text3:'#7C6F64',accent:'#FE8019','accent-dim':'rgba(254,128,25,.15)',success:'#B8BB26','success-dim':'rgba(184,187,38,.15)',warning:'#FABD2F','warning-dim':'rgba(250,189,47,.15)',danger:'#FB4934','danger-dim':'rgba(251,73,52,.15)',purple:'#D3869B','purple-dim':'rgba(211,134,155,.15)'},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
function generateThemeCss(themeId) {
|
|
64
|
+
const vars = THEME_VARS[themeId];
|
|
65
|
+
if (!vars) return '';
|
|
66
|
+
const name = THEME_NAMES[themeId] || themeId;
|
|
67
|
+
let css = '/* mdboard theme: ' + name + ' */\n:root {\n';
|
|
68
|
+
for (const key in vars) {
|
|
69
|
+
if (vars.hasOwnProperty(key)) {
|
|
70
|
+
css += ' --' + key + ': ' + vars[key] + ';\n';
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
css += '}\n';
|
|
74
|
+
return css;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function handleTheme(args) {
|
|
78
|
+
const themeName = args.filter(a => !a.startsWith('--'))[0];
|
|
79
|
+
const isProject = args.includes('--project');
|
|
80
|
+
|
|
81
|
+
if (!themeName) {
|
|
82
|
+
listThemes();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!THEME_IDS.includes(themeName)) {
|
|
87
|
+
console.error('\n Error: Unknown theme "' + themeName + '"');
|
|
88
|
+
console.log(' Run "mdboard theme" to see available themes.\n');
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const configPath = isProject
|
|
93
|
+
? path.join(process.cwd(), 'project', 'mdboard.json')
|
|
94
|
+
: path.join(os.homedir(), '.config', 'mdboard', 'mdboard.json');
|
|
95
|
+
|
|
96
|
+
const dir = path.dirname(configPath);
|
|
97
|
+
if (!fs.existsSync(dir)) {
|
|
98
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let config = {};
|
|
102
|
+
try {
|
|
103
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
104
|
+
} catch {
|
|
105
|
+
// file doesn't exist or is invalid, start fresh
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
config.theme = themeName;
|
|
109
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
110
|
+
|
|
111
|
+
// Also write CSS file
|
|
112
|
+
const css = generateThemeCss(themeName);
|
|
113
|
+
const cssPath = isProject
|
|
114
|
+
? path.join(process.cwd(), 'project', 'mdboard.css')
|
|
115
|
+
: path.join(os.homedir(), '.config', 'mdboard', 'mdboard.css');
|
|
116
|
+
fs.writeFileSync(cssPath, css, 'utf-8');
|
|
117
|
+
|
|
118
|
+
const scope = isProject ? 'project' : 'global';
|
|
119
|
+
console.log('\n Theme set to "' + THEME_NAMES[themeName] + '" (' + scope + ')');
|
|
120
|
+
console.log(' Config: ' + configPath);
|
|
121
|
+
console.log(' CSS: ' + cssPath + '\n');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function listThemes() {
|
|
125
|
+
const dark = THEME_IDS.filter(id => id.includes('dark') || ['catppuccin-mocha', 'tokyo-night', 'tokyo-night-storm', 'dracula', 'nord', 'one-dark', 'gruvbox-dark'].includes(id));
|
|
126
|
+
const light = THEME_IDS.filter(id => !dark.includes(id));
|
|
127
|
+
|
|
128
|
+
console.log('\n Available themes:\n');
|
|
129
|
+
console.log(' Dark:');
|
|
130
|
+
dark.forEach(function(id) {
|
|
131
|
+
console.log(' ' + id.padEnd(24) + THEME_NAMES[id]);
|
|
132
|
+
});
|
|
133
|
+
console.log('\n Light:');
|
|
134
|
+
light.forEach(function(id) {
|
|
135
|
+
console.log(' ' + id.padEnd(24) + THEME_NAMES[id]);
|
|
136
|
+
});
|
|
137
|
+
console.log('\n Usage:');
|
|
138
|
+
console.log(' mdboard theme <name> Set theme globally');
|
|
139
|
+
console.log(' mdboard theme <name> --project Set theme for current project\n');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
module.exports = { handleTheme };
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/* ══════════════════════════════════════════════════════════════
|
|
2
|
+
RENDER
|
|
3
|
+
══════════════════════════════════════════════════════════════ */
|
|
4
|
+
function renderAll() {
|
|
5
|
+
renderHeader(); renderBoardFilters(); renderBoard(); renderTableControls(); renderTableBody(); renderMsFilters(); renderMilestones(); renderMetrics();
|
|
6
|
+
// Render overview if active
|
|
7
|
+
var overviewView = document.getElementById('view-overview');
|
|
8
|
+
if (overviewView && overviewView.classList.contains('active')) {
|
|
9
|
+
renderOverview();
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function renderHeader() {
|
|
14
|
+
renderSidebarLogo();
|
|
15
|
+
|
|
16
|
+
var mw = document.getElementById('h-milestone-wrap');
|
|
17
|
+
var am = D.milestones.find(function(m) { return m.status === 'active'; });
|
|
18
|
+
if (am) {
|
|
19
|
+
mw.style.display = '';
|
|
20
|
+
document.getElementById('h-milestone-name').textContent = am.title || am.id || '';
|
|
21
|
+
document.getElementById('h-milestone-progress').style.width = (am.progress || 0) + '%';
|
|
22
|
+
} else { mw.style.display = 'none'; }
|
|
23
|
+
|
|
24
|
+
var sw = document.getElementById('h-sprint-wrap');
|
|
25
|
+
var as = D.sprints.find(function(s) { return s.status === 'active'; });
|
|
26
|
+
if (as) {
|
|
27
|
+
sw.style.display = '';
|
|
28
|
+
document.getElementById('h-sprint-name').textContent = as.goal || as.id || '';
|
|
29
|
+
var dr = daysRemaining(as.end_date);
|
|
30
|
+
document.getElementById('h-sprint-days').textContent = dr !== null ? (dr >= 0 ? dr + ' days left' : Math.abs(dr) + ' days over') : '';
|
|
31
|
+
} else { sw.style.display = 'none'; }
|
|
32
|
+
|
|
33
|
+
var taskPlural = ENTITY_NAMES.task ? ENTITY_NAMES.task.plural : 'Tasks';
|
|
34
|
+
var total = D.tasks.length;
|
|
35
|
+
var done = D.tasks.filter(function(f) { return f.status === COMPLETED_STATUS; }).length;
|
|
36
|
+
var inProgStatus = (D.config && D.config.statuses && D.config.statuses.task) ?
|
|
37
|
+
((D.config.statuses.task.find(function(s) { return s.icon === 'half-circle'; }) || {}).key || 'in-progress') : 'in-progress';
|
|
38
|
+
var inProg = D.tasks.filter(function(f) { return f.status === inProgStatus; }).length;
|
|
39
|
+
var vel = D.health && D.health.velocity != null ? D.health.velocity : (total ? Math.round(done / total * 100) : 0);
|
|
40
|
+
document.getElementById('h-stats').innerHTML =
|
|
41
|
+
'<div class="stat"><span class="stat-val">' + total + '</span><span class="stat-label">' + escHtml(taskPlural) + '</span></div>' +
|
|
42
|
+
'<div class="stat"><span class="stat-val" style="color:var(--success)">' + done + '</span><span class="stat-label">Done</span></div>' +
|
|
43
|
+
'<div class="stat"><span class="stat-val" style="color:var(--warning)">' + inProg + '</span><span class="stat-label">In Progress</span></div>' +
|
|
44
|
+
'<div class="stat"><span class="stat-val" style="color:var(--accent)">' + vel + '%</span><span class="stat-label">Velocity</span></div>';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* ══════════════════════════════════════════════════════════════
|
|
48
|
+
NAVIGATION
|
|
49
|
+
══════════════════════════════════════════════════════════════ */
|
|
50
|
+
function switchView(name) {
|
|
51
|
+
document.querySelectorAll('.view').forEach(function(v) { v.classList.remove('active'); });
|
|
52
|
+
document.querySelectorAll('#sidebar-nav a[data-view]').forEach(function(a) { a.classList.remove('active'); });
|
|
53
|
+
var t = document.getElementById('view-' + name);
|
|
54
|
+
if (t) t.classList.add('active');
|
|
55
|
+
var l = document.querySelector('#sidebar-nav a[data-view="' + name + '"]');
|
|
56
|
+
if (l) l.classList.add('active');
|
|
57
|
+
// Persist view selection
|
|
58
|
+
try { localStorage.setItem('mdboard-view', name); } catch(e) {}
|
|
59
|
+
// Trigger overview rendering when switching to it
|
|
60
|
+
if (name === 'overview' && D.loaded) {
|
|
61
|
+
renderOverview();
|
|
62
|
+
}
|
|
63
|
+
// Trigger notes rendering when switching to it
|
|
64
|
+
if (name === 'notes' && D.loaded) {
|
|
65
|
+
renderNotes();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
document.getElementById('sidebar-nav').addEventListener('click', function(e) {
|
|
70
|
+
var a = e.target.closest('a[data-view]');
|
|
71
|
+
if (!a) return;
|
|
72
|
+
e.preventDefault();
|
|
73
|
+
switchView(a.dataset.view);
|
|
74
|
+
window.location.hash = a.dataset.view;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
function handleHash() {
|
|
78
|
+
var hash = window.location.hash.replace('#', '');
|
|
79
|
+
if (!hash) {
|
|
80
|
+
try { hash = localStorage.getItem('mdboard-view') || ''; } catch(e) {}
|
|
81
|
+
}
|
|
82
|
+
hash = hash || 'board';
|
|
83
|
+
var valid = ['board','table','milestones','metrics','overview','notes'];
|
|
84
|
+
switchView(valid.indexOf(hash) !== -1 ? hash : 'board');
|
|
85
|
+
}
|
|
86
|
+
window.addEventListener('hashchange', handleHash);
|
|
87
|
+
|
|
88
|
+
/* ══════════════════════════════════════════════════════════════
|
|
89
|
+
SETTINGS PANEL
|
|
90
|
+
══════════════════════════════════════════════════════════════ */
|
|
91
|
+
var _settingsState = { selectedTheme: null, originalTheme: null };
|
|
92
|
+
|
|
93
|
+
function openSettings() {
|
|
94
|
+
var active = getActiveTheme();
|
|
95
|
+
_settingsState.originalTheme = active;
|
|
96
|
+
_settingsState.selectedTheme = null;
|
|
97
|
+
document.getElementById('settings-overlay').classList.add('open');
|
|
98
|
+
document.getElementById('settings-panel').classList.add('open');
|
|
99
|
+
var toggle = document.getElementById('theme-default-toggle');
|
|
100
|
+
if (toggle) toggle.checked = false;
|
|
101
|
+
renderThemeGrid();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function closeSettings() {
|
|
105
|
+
if (_settingsState.selectedTheme) {
|
|
106
|
+
revertThemePreview();
|
|
107
|
+
_settingsState.selectedTheme = null;
|
|
108
|
+
}
|
|
109
|
+
document.getElementById('settings-overlay').classList.remove('open');
|
|
110
|
+
document.getElementById('settings-panel').classList.remove('open');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function renderThemeGrid() {
|
|
114
|
+
var grid = document.getElementById('theme-grid');
|
|
115
|
+
var info = document.getElementById('settings-theme-info');
|
|
116
|
+
var active = getActiveTheme();
|
|
117
|
+
var selected = _settingsState.selectedTheme;
|
|
118
|
+
|
|
119
|
+
if (info) {
|
|
120
|
+
var displayId = selected || active;
|
|
121
|
+
var displayTheme = THEMES[displayId];
|
|
122
|
+
info.textContent = 'Active: ' + (THEMES[active] ? THEMES[active].name : active) +
|
|
123
|
+
(selected && selected !== active ? ' → Preview: ' + (displayTheme ? displayTheme.name : selected) : '');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
var html = '';
|
|
127
|
+
THEME_LIST.forEach(function(t) {
|
|
128
|
+
var theme = THEMES[t.id];
|
|
129
|
+
var v = theme.vars;
|
|
130
|
+
var isSelected = selected ? t.id === selected : t.id === active;
|
|
131
|
+
var cls = isSelected ? ' active' : '';
|
|
132
|
+
html += '<div class="theme-card' + cls + '" data-theme="' + t.id + '">' +
|
|
133
|
+
'<div class="theme-card-preview">' +
|
|
134
|
+
'<span style="background:' + v.bg + '"></span>' +
|
|
135
|
+
'<span style="background:' + v.surface + '"></span>' +
|
|
136
|
+
'<span style="background:' + v.accent + '"></span>' +
|
|
137
|
+
'<span style="background:' + v.success + '"></span>' +
|
|
138
|
+
'<span style="background:' + v.warning + '"></span>' +
|
|
139
|
+
'</div>' +
|
|
140
|
+
'<div class="theme-card-name">' + escHtml(theme.name) + '</div>' +
|
|
141
|
+
'<div class="theme-card-type">' + theme.type + '</div>' +
|
|
142
|
+
'</div>';
|
|
143
|
+
});
|
|
144
|
+
grid.innerHTML = html;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function saveTheme() {
|
|
148
|
+
var themeId = _settingsState.selectedTheme;
|
|
149
|
+
if (!themeId) {
|
|
150
|
+
closeSettings();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
var toggle = document.getElementById('theme-default-toggle');
|
|
154
|
+
var setDefault = toggle ? toggle.checked : false;
|
|
155
|
+
var css = generateThemeCss(themeId);
|
|
156
|
+
|
|
157
|
+
fetch('/api/theme', {
|
|
158
|
+
method: 'POST',
|
|
159
|
+
headers: { 'Content-Type': 'application/json' },
|
|
160
|
+
body: JSON.stringify({ themeId: themeId, css: css, setDefault: setDefault })
|
|
161
|
+
}).then(function(r) { return r.json(); }).then(function(data) {
|
|
162
|
+
if (data && data.ok) {
|
|
163
|
+
revertThemePreview();
|
|
164
|
+
reloadCustomCss();
|
|
165
|
+
D.config.theme = themeId;
|
|
166
|
+
_settingsState.selectedTheme = null;
|
|
167
|
+
showToast('Theme saved: ' + (THEMES[themeId] ? THEMES[themeId].name : themeId), 'success');
|
|
168
|
+
closeSettings();
|
|
169
|
+
} else {
|
|
170
|
+
showToast('Error: ' + (data.error || 'Unknown'), 'error');
|
|
171
|
+
}
|
|
172
|
+
}).catch(function(e) {
|
|
173
|
+
showToast('Error: ' + e.message, 'error');
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
document.getElementById('settings-toggle').addEventListener('click', function(e) {
|
|
178
|
+
e.preventDefault();
|
|
179
|
+
openSettings();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
document.getElementById('settings-close').addEventListener('click', closeSettings);
|
|
183
|
+
document.getElementById('settings-overlay').addEventListener('click', closeSettings);
|
|
184
|
+
|
|
185
|
+
document.getElementById('theme-grid').addEventListener('click', function(e) {
|
|
186
|
+
var card = e.target.closest('.theme-card');
|
|
187
|
+
if (!card) return;
|
|
188
|
+
var themeId = card.dataset.theme;
|
|
189
|
+
_settingsState.selectedTheme = themeId;
|
|
190
|
+
applyTheme(themeId);
|
|
191
|
+
renderThemeGrid();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
document.getElementById('settings-save').addEventListener('click', saveTheme);
|
|
195
|
+
|
|
196
|
+
/* ══════════════════════════════════════════════════════════════
|
|
197
|
+
INIT
|
|
198
|
+
══════════════════════════════════════════════════════════════ */
|
|
199
|
+
(async function() {
|
|
200
|
+
handleHash();
|
|
201
|
+
await loadAll();
|
|
202
|
+
initFromConfig();
|
|
203
|
+
|
|
204
|
+
// Sidebar logo always shows project name
|
|
205
|
+
renderSidebarLogo();
|
|
206
|
+
|
|
207
|
+
// Workspace detection: build source rail + restore last source
|
|
208
|
+
if (hasWorkspace()) {
|
|
209
|
+
// Create overview tab (hidden by default)
|
|
210
|
+
var overviewTab = document.querySelector('#sidebar-nav a[data-view="overview"]');
|
|
211
|
+
if (!overviewTab) {
|
|
212
|
+
var nav = document.getElementById('sidebar-nav');
|
|
213
|
+
var metricsLink = document.querySelector('#sidebar-nav a[data-view="metrics"]');
|
|
214
|
+
var overviewLink = document.createElement('a');
|
|
215
|
+
overviewLink.href = '#overview';
|
|
216
|
+
overviewLink.dataset.view = 'overview';
|
|
217
|
+
overviewLink.style.display = 'none';
|
|
218
|
+
overviewLink.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="9"/><path d="M12 3v18M3 12h18"/></svg><span>Overview</span>';
|
|
219
|
+
if (metricsLink && metricsLink.nextSibling) {
|
|
220
|
+
nav.insertBefore(overviewLink, metricsLink.nextSibling);
|
|
221
|
+
} else {
|
|
222
|
+
nav.appendChild(overviewLink);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Restore last source or default to overview
|
|
227
|
+
var savedSource = null;
|
|
228
|
+
try { savedSource = localStorage.getItem('mdboard-source'); } catch(e) {}
|
|
229
|
+
var validSource = savedSource && (savedSource === 'overview' || D.config.workspace.sources.some(function(s) { return s.name === savedSource; }));
|
|
230
|
+
var initialSource = validSource ? savedSource : 'overview';
|
|
231
|
+
|
|
232
|
+
buildSourceRail();
|
|
233
|
+
// switchSource skips if same as current, so set activeSource first then trigger
|
|
234
|
+
D.activeSource = null;
|
|
235
|
+
switchSource(initialSource);
|
|
236
|
+
// Wait for switchSource data reload before continuing
|
|
237
|
+
await loadAll();
|
|
238
|
+
initFromConfig();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
renderAll();
|
|
242
|
+
connectSSE();
|
|
243
|
+
|
|
244
|
+
// Auto-open history modal if no project
|
|
245
|
+
checkAutoOpenHistory();
|
|
246
|
+
|
|
247
|
+
// Keyboard shortcut: Ctrl/Cmd+K to open history modal
|
|
248
|
+
document.addEventListener('keydown', function(e) {
|
|
249
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
250
|
+
e.preventDefault();
|
|
251
|
+
openHistoryModal();
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Load AI suggestions for autocomplete
|
|
256
|
+
fetchJson('/api/ai-suggestions').then(function(data) {
|
|
257
|
+
D.aiSuggestions = data || { skills: [], agents: [], mcps: [], commands: [], context: [] };
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// Load overview links in background for reverse link display
|
|
261
|
+
if (hasWorkspace()) {
|
|
262
|
+
fetchJson('/api/overview/links').then(function(data) {
|
|
263
|
+
D.overviewLinks = data;
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
})();
|