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.
@@ -0,0 +1,157 @@
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 ? ' &mdash; ' + 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) + ' &mdash; ' + 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">&#9889;</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
+ }
@@ -0,0 +1,331 @@
1
+ /* ══════════════════════════════════════════════════════════════
2
+ ICON SHAPES — SVG generators by icon name
3
+ ══════════════════════════════════════════════════════════════ */
4
+ var ICON_SHAPES = {
5
+ 'dashed-circle': function(c) { return '<svg class="icon" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="' + c + '" stroke-width="1.5" stroke-dasharray="3 2"/></svg>'; },
6
+ 'circle': function(c) { return '<svg class="icon" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="' + c + '" stroke-width="1.5"/></svg>'; },
7
+ 'half-circle': function(c) { return '<svg class="icon" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="' + c + '" stroke-width="1.5"/><path d="M8 2A6 6 0 0 1 8 14Z" fill="' + c + '"/></svg>'; },
8
+ 'three-quarter-circle': function(c) { return '<svg class="icon" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="' + c + '" stroke-width="1.5"/><path d="M8 2A6 6 0 1 1 2 8L8 8Z" fill="' + c + '"/></svg>'; },
9
+ 'check-circle': function(c) { return '<svg class="icon" viewBox="0 0 16 16"><circle cx="8" cy="8" r="7" fill="' + c + '"/><path d="M5 8.5L7 10.5L11 5.5" stroke="#fff" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>'; },
10
+ 'x-circle': function(c) { return '<svg class="icon" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="' + c + '" stroke-width="1.5"/><path d="M5.5 5.5L10.5 10.5M10.5 5.5L5.5 10.5" stroke="' + c + '" stroke-width="1.5" stroke-linecap="round"/></svg>'; },
11
+ 'slash-circle': function(c) { return '<svg class="icon" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="' + c + '" stroke-width="1.5"/><path d="M4.5 11.5L11.5 4.5" stroke="' + c + '" stroke-width="1.5" stroke-linecap="round"/></svg>'; }
12
+ };
13
+
14
+ /* Config-driven maps — populated by initFromConfig() */
15
+ var STATUS_ICON_MAP = {};
16
+ var STATUS_COLOR_MAP = {};
17
+ var ENTITY_NAMES = {};
18
+
19
+ function statusIcon(status) {
20
+ var entry = STATUS_ICON_MAP[status];
21
+ if (entry) {
22
+ var fn = ICON_SHAPES[entry.icon];
23
+ if (fn) return fn(entry.color);
24
+ }
25
+ // Fallback: dashed circle
26
+ return ICON_SHAPES['dashed-circle']('#5A5A63');
27
+ }
28
+
29
+ function priorityIcon(priority) {
30
+ var c = '#3A3A40', n = 0;
31
+ if (D.config && D.config.priorities) {
32
+ var entry = D.config.priorities.find(function(p) { return p.key === priority; });
33
+ if (entry) { c = entry.color; n = entry.bars; }
34
+ } else {
35
+ switch (priority) {
36
+ case 'urgent': c = '#F97316'; n = 4; break;
37
+ case 'high': c = '#F97316'; n = 3; break;
38
+ case 'medium': c = '#D4A72C'; n = 2; break;
39
+ case 'low': c = '#5B6EF5'; n = 1; break;
40
+ }
41
+ }
42
+ var bars = '';
43
+ for (var i = 0; i < 4; i++) {
44
+ var filled = i < n;
45
+ var h = 3 + i * 3;
46
+ var y = 14 - h;
47
+ bars += '<rect x="' + (1.5 + i * 3.5) + '" y="' + y + '" width="2" height="' + h + '" rx=".5" fill="' + (filled ? c : '#2E2E33') + '"/>';
48
+ }
49
+ return '<svg class="icon" viewBox="0 0 16 16">' + bars + '</svg>';
50
+ }
51
+
52
+ function milestoneIcon(status) {
53
+ var msStatuses = D.config && D.config.statuses ? D.config.statuses.milestone : null;
54
+ var entry = msStatuses ? msStatuses.find(function(s) { return s.key === status; }) : null;
55
+ var c = entry ? entry.color : '#8B8B93';
56
+
57
+ switch (status) {
58
+ case 'active':
59
+ return '<svg class="icon" viewBox="0 0 16 16"><path d="M8 1L15 8L8 15L1 8Z" fill="none" stroke="' + c + '" stroke-width="1.5"/><path d="M8 5L11 8L8 11L5 8Z" fill="' + c + '"/></svg>';
60
+ case 'completed':
61
+ return '<svg class="icon" viewBox="0 0 16 16"><path d="M8 1L15 8L8 15L1 8Z" fill="' + c + '"/><path d="M5.5 8L7 9.5L10.5 6" stroke="#fff" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>';
62
+ default:
63
+ return '<svg class="icon" viewBox="0 0 16 16"><path d="M8 1L15 8L8 15L1 8Z" fill="none" stroke="' + c + '" stroke-width="1.5"/></svg>';
64
+ }
65
+ }
66
+
67
+ /* ══════════════════════════════════════════════════════════════
68
+ DATA STORE
69
+ ══════════════════════════════════════════════════════════════ */
70
+ var D = {
71
+ config: null, project: null, milestones: [], epics: [], tasks: [],
72
+ sprints: [], allSprints: [], metrics: null, health: null, loaded: false,
73
+ sources: [], activeSource: null, overviewLinks: null, notes: [],
74
+ aiSuggestions: null, theme: null
75
+ };
76
+
77
+ /* ── Helpers ─────────────────────────────────────────────── */
78
+ function escHtml(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
79
+
80
+ function epicColor(name) {
81
+ var colors = ['#5B6EF5','#8B5CF6','#EC4899','#F59E0B','#10B981','#06B6D4','#F97316','#6366F1'];
82
+ var hash = 0;
83
+ for (var i = 0; i < name.length; i++) hash = name.charCodeAt(i) + ((hash << 5) - hash);
84
+ return colors[Math.abs(hash) % colors.length];
85
+ }
86
+
87
+ function badgeClass(prefix, value) {
88
+ if (!value) return prefix + '-backlog';
89
+ return prefix + '-' + value.toLowerCase().replace(/\s+/g, '-');
90
+ }
91
+
92
+ function fmtDate(d) { return d ? String(d).substring(0, 10) : ''; }
93
+
94
+ function daysRemaining(endDate) {
95
+ if (!endDate) return null;
96
+ var end = new Date(endDate); var now = new Date();
97
+ return Math.ceil((end - now) / 86400000);
98
+ }
99
+
100
+ var STATUSES = ['backlog','todo','in-progress','in-review','done','blocked','cancelled'];
101
+ var PRIORITIES = ['urgent','high','medium','low'];
102
+ var STATUS_LABELS = {backlog:'Backlog',todo:'Todo','in-progress':'In Progress','in-review':'In Review',done:'Done',blocked:'Blocked',cancelled:'Cancelled'};
103
+ var PRIORITY_LABELS = {urgent:'Urgent',high:'High',medium:'Medium',low:'Low'};
104
+ var BOARD_COLS = ['backlog','todo','in-progress','in-review','done'];
105
+ var COMPLETED_STATUS = 'done';
106
+
107
+ /* ── Toast ───────────────────────────────────────────────── */
108
+ function showToast(msg, type) {
109
+ var el = document.createElement('div');
110
+ el.className = 'toast toast-' + (type || 'success');
111
+ el.textContent = msg;
112
+ document.body.appendChild(el);
113
+ setTimeout(function() { el.remove(); }, 3000);
114
+ }
115
+
116
+ /* ── Data Fetching ───────────────────────────────────────── */
117
+ async function fetchJson(url) {
118
+ try { var r = await fetch(url); return r.ok ? await r.json() : null; }
119
+ catch { return null; }
120
+ }
121
+
122
+ async function patchItem(type, id, updates) {
123
+ try {
124
+ var base = apiBase();
125
+ var r = await fetch(base + '/' + type + '/' + encodeURIComponent(id), {
126
+ method: 'PATCH',
127
+ headers: { 'Content-Type': 'application/json' },
128
+ body: JSON.stringify(updates),
129
+ });
130
+ var data = await r.json();
131
+ if (data && data.ok) {
132
+ showToast('Saved successfully', 'success');
133
+ return true;
134
+ } else {
135
+ showToast('Error: ' + (data.error || 'Unknown'), 'error');
136
+ return false;
137
+ }
138
+ } catch (e) {
139
+ showToast('Error: ' + e.message, 'error');
140
+ return false;
141
+ }
142
+ }
143
+
144
+ /* ── CRUD API helpers ─────────────────────────────────────── */
145
+ function apiBase() {
146
+ if (D.activeSource) return '/api/sources/' + encodeURIComponent(D.activeSource);
147
+ return '/api';
148
+ }
149
+
150
+ async function createItem(collection, data) {
151
+ try {
152
+ var url = apiBase() + '/' + collection;
153
+ var r = await fetch(url, {
154
+ method: 'POST',
155
+ headers: { 'Content-Type': 'application/json' },
156
+ body: JSON.stringify(data),
157
+ });
158
+ var result = await r.json();
159
+ if (result && result.ok) {
160
+ showToast('Created ' + (result.id || ''), 'success');
161
+ return result;
162
+ } else {
163
+ showToast('Error: ' + (result.error || 'Unknown'), 'error');
164
+ return null;
165
+ }
166
+ } catch (e) {
167
+ showToast('Error: ' + e.message, 'error');
168
+ return null;
169
+ }
170
+ }
171
+
172
+ async function deleteItem(collection, id) {
173
+ try {
174
+ var url = apiBase() + '/' + collection + '/' + encodeURIComponent(id);
175
+ var r = await fetch(url, { method: 'DELETE' });
176
+ var result = await r.json();
177
+ if (result && result.ok) {
178
+ showToast('Archived successfully', 'success');
179
+ return true;
180
+ } else {
181
+ showToast('Error: ' + (result.error || 'Unknown'), 'error');
182
+ return false;
183
+ }
184
+ } catch (e) {
185
+ showToast('Error: ' + e.message, 'error');
186
+ return false;
187
+ }
188
+ }
189
+
190
+ /* ── Data Loading ────────────────────────────────────────── */
191
+ async function loadAll() {
192
+ var base = apiBase();
193
+ var results = await Promise.all([
194
+ fetchJson(base + '/project'), fetchJson(base + '/milestones'), fetchJson(base + '/epics'),
195
+ fetchJson(base + '/tasks'), fetchJson(base + '/sprint'), fetchJson(base + '/metrics'),
196
+ fetchJson(base + '/health'), fetchJson(base + '/sprints'), fetchJson('/api/config'),
197
+ fetchJson('/api/sources'), fetchJson(base + '/notes')
198
+ ]);
199
+ D.project = results[0]; D.milestones = results[1] || []; D.epics = results[2] || [];
200
+ D.tasks = results[3] || [];
201
+ if (results[4]) D.sprints = [results[4]]; else D.sprints = [];
202
+ D.metrics = results[5]; D.health = results[6];
203
+ D.allSprints = results[7] || [];
204
+ D.config = results[8];
205
+ D.sources = results[9] || [];
206
+ D.notes = results[10] || [];
207
+ D.loaded = true;
208
+ }
209
+
210
+ function refreshData() {
211
+ loadAll().then(function() {
212
+ initFromConfig();
213
+ renderAll();
214
+ if (hasWorkspace()) buildSourceRail();
215
+ });
216
+ }
217
+
218
+ function hasWorkspace() {
219
+ return D.config && D.config.workspace && D.config.workspace.sources && D.config.workspace.sources.length > 0;
220
+ }
221
+
222
+ /* ── Config-driven initialization ────────────────────────── */
223
+ function initFromConfig() {
224
+ if (!D.config) return;
225
+
226
+ var cfg = D.config;
227
+
228
+ // Derive entity names
229
+ ENTITY_NAMES = cfg.entities || {};
230
+
231
+ // Derive statuses, labels, board columns
232
+ if (cfg.statuses && cfg.statuses.task) {
233
+ STATUSES = cfg.statuses.task.map(function(s) { return s.key; });
234
+ STATUS_LABELS = {};
235
+ cfg.statuses.task.forEach(function(s) { STATUS_LABELS[s.key] = s.label; });
236
+ }
237
+
238
+ // Build icon/color maps for all status types
239
+ STATUS_ICON_MAP = {};
240
+ STATUS_COLOR_MAP = {};
241
+ if (cfg.statuses) {
242
+ Object.keys(cfg.statuses).forEach(function(entityType) {
243
+ cfg.statuses[entityType].forEach(function(s) {
244
+ if (!STATUS_ICON_MAP[s.key]) {
245
+ STATUS_ICON_MAP[s.key] = { icon: s.icon || 'dashed-circle', color: s.color || '#5A5A63' };
246
+ STATUS_COLOR_MAP[s.key] = s.color || '#5A5A63';
247
+ }
248
+ });
249
+ });
250
+ }
251
+
252
+ // Derive priorities
253
+ if (cfg.priorities) {
254
+ PRIORITIES = cfg.priorities.map(function(p) { return p.key; });
255
+ PRIORITY_LABELS = {};
256
+ cfg.priorities.forEach(function(p) { PRIORITY_LABELS[p.key] = p.label; });
257
+ }
258
+
259
+ // Board columns
260
+ if (cfg.boardColumns) BOARD_COLS = cfg.boardColumns;
261
+
262
+ // Completed status
263
+ if (cfg.completedStatus) COMPLETED_STATUS = cfg.completedStatus;
264
+
265
+ generateDynamicStyles();
266
+ }
267
+
268
+ function generateDynamicStyles() {
269
+ var css = '';
270
+ if (D.config && D.config.statuses && D.config.statuses.task) {
271
+ D.config.statuses.task.forEach(function(s) {
272
+ var key = s.key;
273
+ var color = s.color;
274
+ css += '.badge-' + key + '{background:' + color + '20;color:' + color + '}\n';
275
+ css += '.card[data-status="' + key + '"]{border-left-color:' + color + '}\n';
276
+ });
277
+ }
278
+ // Milestone/epic/sprint statuses
279
+ ['milestone', 'epic', 'sprint'].forEach(function(entityType) {
280
+ if (D.config && D.config.statuses && D.config.statuses[entityType]) {
281
+ D.config.statuses[entityType].forEach(function(s) {
282
+ css += '.badge-' + s.key + '{background:' + s.color + '20;color:' + s.color + '}\n';
283
+ });
284
+ }
285
+ });
286
+ // Priority badges
287
+ if (D.config && D.config.priorities) {
288
+ D.config.priorities.forEach(function(p) {
289
+ css += '.badge-' + p.key + '{background:' + p.color + '20;color:' + p.color + '}\n';
290
+ });
291
+ }
292
+ var styleEl = document.getElementById('dynamic-styles');
293
+ if (styleEl) styleEl.textContent = css;
294
+ }
295
+
296
+ /* ══════════════════════════════════════════════════════════════
297
+ THEME ENGINE
298
+ ══════════════════════════════════════════════════════════════ */
299
+ function generateThemeCss(themeId) {
300
+ var theme = THEMES[themeId];
301
+ if (!theme) return '';
302
+ var css = '/* mdboard theme: ' + theme.name + ' */\n:root {\n';
303
+ for (var key in theme.vars) {
304
+ if (theme.vars.hasOwnProperty(key)) {
305
+ css += ' --' + key + ': ' + theme.vars[key] + ';\n';
306
+ }
307
+ }
308
+ css += '}\n';
309
+ return css;
310
+ }
311
+
312
+ function applyTheme(themeId) {
313
+ var theme = THEMES[themeId];
314
+ if (!theme) return;
315
+ var el = document.getElementById('theme-styles');
316
+ if (el) el.textContent = generateThemeCss(themeId);
317
+ }
318
+
319
+ function revertThemePreview() {
320
+ var el = document.getElementById('theme-styles');
321
+ if (el) el.textContent = '';
322
+ }
323
+
324
+ function reloadCustomCss() {
325
+ var link = document.getElementById('custom-theme');
326
+ if (link) link.href = '/mdboard.css?t=' + Date.now();
327
+ }
328
+
329
+ function getActiveTheme() {
330
+ return D.config && D.config.theme ? D.config.theme : 'default-dark';
331
+ }