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/cli/theme.js
DELETED
|
@@ -1,142 +0,0 @@
|
|
|
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 };
|
package/src/client/app.js
DELETED
|
@@ -1,266 +0,0 @@
|
|
|
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
|
-
})();
|
package/src/client/board.js
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
/* ══════════════════════════════════════════════════════════════
|
|
2
|
-
BOARD VIEW
|
|
3
|
-
══════════════════════════════════════════════════════════════ */
|
|
4
|
-
var boardFilters = { search: '', priority: '', epic: '', milestone: '' };
|
|
5
|
-
|
|
6
|
-
function renderBoardFilters() {
|
|
7
|
-
var c = document.getElementById('board-filters');
|
|
8
|
-
var ep = [], ms = [];
|
|
9
|
-
D.tasks.forEach(function(f) {
|
|
10
|
-
if (f.epic && ep.indexOf(f.epic) === -1) ep.push(f.epic);
|
|
11
|
-
if (f.milestone && ms.indexOf(f.milestone) === -1) ms.push(f.milestone);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
var epicPlural = ENTITY_NAMES.epic ? ENTITY_NAMES.epic.plural : 'Epics';
|
|
15
|
-
var msPlural = ENTITY_NAMES.milestone ? ENTITY_NAMES.milestone.plural : 'Milestones';
|
|
16
|
-
|
|
17
|
-
function opts(arr, key, label) {
|
|
18
|
-
return '<select data-filter="' + key + '"><option value="">All ' + label + '</option>' +
|
|
19
|
-
arr.map(function(v) { return '<option value="' + escHtml(v) + '"' + (boardFilters[key] === v ? ' selected' : '') + '>' + escHtml(v) + '</option>'; }).join('') + '</select>';
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
var createBtn = isSourceWritable() ?
|
|
23
|
-
'<button class="btn btn-sm btn-create" id="board-create-btn">+ New ' + escHtml(ENTITY_NAMES.task ? ENTITY_NAMES.task.singular : 'Task') + '</button>' : '';
|
|
24
|
-
|
|
25
|
-
c.innerHTML = createBtn +
|
|
26
|
-
'<input type="text" data-filter="search" placeholder="Search cards..." value="' + escHtml(boardFilters.search) + '">' +
|
|
27
|
-
'<select data-filter="priority"><option value="">All Priorities</option>' +
|
|
28
|
-
PRIORITIES.map(function(p) { return '<option value="' + p + '"' + (boardFilters.priority === p ? ' selected' : '') + '>' + (PRIORITY_LABELS[p] || p) + '</option>'; }).join('') + '</select>' +
|
|
29
|
-
opts(ep, 'epic', epicPlural) + opts(ms, 'milestone', msPlural);
|
|
30
|
-
|
|
31
|
-
c.querySelectorAll('input[data-filter]').forEach(function(el) {
|
|
32
|
-
el.addEventListener('input', function() { boardFilters[el.dataset.filter] = el.value; renderBoard(); });
|
|
33
|
-
});
|
|
34
|
-
c.querySelectorAll('select[data-filter]').forEach(function(el) {
|
|
35
|
-
el.addEventListener('change', function() { boardFilters[el.dataset.filter] = el.value; renderBoard(); });
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
var btn = document.getElementById('board-create-btn');
|
|
39
|
-
if (btn) btn.addEventListener('click', function() { openCreateDialog('tasks'); });
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function getFilteredBoardTasks() {
|
|
43
|
-
var list = D.tasks.slice();
|
|
44
|
-
var f = boardFilters;
|
|
45
|
-
if (f.search) { var q = f.search.toLowerCase(); list = list.filter(function(x) { return (x.id || '').toLowerCase().indexOf(q) !== -1 || (x.title || '').toLowerCase().indexOf(q) !== -1; }); }
|
|
46
|
-
if (f.priority) list = list.filter(function(x) { return x.priority === f.priority; });
|
|
47
|
-
if (f.epic) list = list.filter(function(x) { return x.epic === f.epic; });
|
|
48
|
-
if (f.milestone) list = list.filter(function(x) { return x.milestone === f.milestone; });
|
|
49
|
-
return list;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function renderBoard() {
|
|
53
|
-
var sprint = D.sprints.find(function(s) { return s.status === 'active'; });
|
|
54
|
-
var sbar = document.getElementById('sprint-bar');
|
|
55
|
-
if (sprint) {
|
|
56
|
-
var planned = sprint.planned_points || 0;
|
|
57
|
-
var completed = sprint.completed_points || 0;
|
|
58
|
-
var pct = planned ? Math.round(completed / planned * 100) : 0;
|
|
59
|
-
sbar.style.display = '';
|
|
60
|
-
sbar.innerHTML =
|
|
61
|
-
'<div class="sprint-goal"><b>' + escHtml(sprint.id || '') + '</b>' + (sprint.goal ? ' — ' + escHtml(sprint.goal) : '') + '</div>' +
|
|
62
|
-
'<div style="width:160px"><div class="progress progress-accent"><div class="progress-fill" style="width:' + pct + '%"></div></div>' +
|
|
63
|
-
'<div style="font-size:11px;color:var(--text3);margin-top:2px;font-family:var(--mono)">' + completed + '/' + planned + ' pts</div></div>' +
|
|
64
|
-
'<div class="sprint-dates">' + fmtDate(sprint.start_date) + ' — ' + fmtDate(sprint.end_date) + '</div>';
|
|
65
|
-
} else { sbar.style.display = 'none'; }
|
|
66
|
-
|
|
67
|
-
var board = document.getElementById('board');
|
|
68
|
-
if (!D.loaded) {
|
|
69
|
-
board.innerHTML = BOARD_COLS.map(function() {
|
|
70
|
-
return '<div class="column"><div class="column-header"><div class="skeleton skeleton-line" style="width:80px;height:16px"></div></div><div class="column-body">' +
|
|
71
|
-
'<div class="skeleton skeleton-card"></div><div class="skeleton skeleton-card"></div></div></div>';
|
|
72
|
-
}).join('');
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
var filteredTasks = getFilteredBoardTasks();
|
|
77
|
-
var taskSingular = ENTITY_NAMES.task ? ENTITY_NAMES.task.singular.toLowerCase() : 'task';
|
|
78
|
-
|
|
79
|
-
if (!D.tasks.length) {
|
|
80
|
-
board.innerHTML = '<div class="empty"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="3"/><path d="M9 12h6M12 9v6"/></svg><p>No ' + escHtml(taskSingular) + 's yet. Create ' + escHtml(taskSingular) + ' files in your project/ directory to get started.</p></div>';
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
board.innerHTML = BOARD_COLS.map(function(status) {
|
|
85
|
-
var cards = filteredTasks.filter(function(f) { return f.status === status; });
|
|
86
|
-
return '<div class="column" data-status="' + status + '">' +
|
|
87
|
-
'<div class="column-header"><span class="col-title">' + statusIcon(status) + ' ' + (STATUS_LABELS[status] || status) + '</span><span class="col-count">' + cards.length + '</span></div>' +
|
|
88
|
-
'<div class="column-body">' + cards.map(renderCard).join('') + '</div></div>';
|
|
89
|
-
}).join('');
|
|
90
|
-
|
|
91
|
-
setupDragDrop();
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function renderCard(f) {
|
|
95
|
-
var ec = f.epic ? epicColor(f.epic) : '';
|
|
96
|
-
var assigned = Array.isArray(f.assigned) ? f.assigned.join(', ') : (f.assigned || '');
|
|
97
|
-
return '<div class="card" draggable="true" data-id="' + escHtml(f.id || '') + '" data-status="' + escHtml(f.status || '') + '">' +
|
|
98
|
-
'<div class="card-header">' +
|
|
99
|
-
'<span class="card-priority">' + priorityIcon(f.priority) + '</span>' +
|
|
100
|
-
'<span class="card-id">' + escHtml(f.id || '') + '</span>' +
|
|
101
|
-
'</div>' +
|
|
102
|
-
'<div class="card-title">' + escHtml(f.title || '') + '</div>' +
|
|
103
|
-
'<div class="card-meta">' +
|
|
104
|
-
(f.points != null ? '<span class="pill pill-points">' + f.points + ' pts</span>' : '') +
|
|
105
|
-
(f.epic ? '<span class="pill pill-epic" style="background:' + ec + '">' + escHtml(f.epic) + '</span>' : '') +
|
|
106
|
-
(f.ai && Object.keys(f.ai).length > 0 ? '<span class="pill pill-ai" title="AI properties">⚡</span>' : '') +
|
|
107
|
-
(assigned ? '<span class="card-assigned">' + escHtml(assigned) + '</span>' : '') +
|
|
108
|
-
'</div></div>';
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/* ── Drag & Drop ─────────────────────────────────────────── */
|
|
112
|
-
function setupDragDrop() {
|
|
113
|
-
document.querySelectorAll('.card[draggable]').forEach(function(card) {
|
|
114
|
-
card.addEventListener('dragstart', function(e) {
|
|
115
|
-
e.dataTransfer.setData('text/plain', card.dataset.id);
|
|
116
|
-
e.dataTransfer.effectAllowed = 'move';
|
|
117
|
-
card.classList.add('dragging');
|
|
118
|
-
});
|
|
119
|
-
card.addEventListener('dragend', function() {
|
|
120
|
-
card.classList.remove('dragging');
|
|
121
|
-
document.querySelectorAll('.column.drag-over').forEach(function(c) { c.classList.remove('drag-over'); });
|
|
122
|
-
});
|
|
123
|
-
card.addEventListener('click', function(e) {
|
|
124
|
-
if (e.defaultPrevented) return;
|
|
125
|
-
var task = D.tasks.find(function(f) { return f.id === card.dataset.id; });
|
|
126
|
-
if (task) openPanel('tasks', task);
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
document.querySelectorAll('.column').forEach(function(col) {
|
|
131
|
-
col.addEventListener('dragover', function(e) {
|
|
132
|
-
e.preventDefault();
|
|
133
|
-
e.dataTransfer.dropEffect = 'move';
|
|
134
|
-
col.classList.add('drag-over');
|
|
135
|
-
});
|
|
136
|
-
col.addEventListener('dragleave', function(e) {
|
|
137
|
-
if (e.target === col || !col.contains(e.relatedTarget)) {
|
|
138
|
-
col.classList.remove('drag-over');
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
col.addEventListener('drop', function(e) {
|
|
142
|
-
e.preventDefault();
|
|
143
|
-
col.classList.remove('drag-over');
|
|
144
|
-
var taskId = e.dataTransfer.getData('text/plain');
|
|
145
|
-
var newStatus = col.dataset.status;
|
|
146
|
-
if (!taskId || !newStatus) return;
|
|
147
|
-
|
|
148
|
-
// Optimistic update
|
|
149
|
-
var task = D.tasks.find(function(f) { return f.id === taskId; });
|
|
150
|
-
if (task && task.status !== newStatus) {
|
|
151
|
-
task.status = newStatus;
|
|
152
|
-
renderBoard();
|
|
153
|
-
patchItem('tasks', taskId, { status: newStatus });
|
|
154
|
-
}
|
|
155
|
-
});
|
|
156
|
-
});
|
|
157
|
-
}
|