@@ -29,20 +29,52 @@ public class CodeMindMapHtml
2929 margin: 0;
3030 padding: 0;
3131 }
32- /* Status indicator styles */
33- .node-completed {
34- opacity: 0.6;
32+ /* Root node: match the same padding-to-font ratio as level-1 nodes.
33+ Level-1 uses 8px/25px padding with 14px font (ratios 0.57v / 1.79h).
34+ Root has 25px font, so target: 14px vertical / 45px horizontal. */
35+ .map-container me-root me-tpc {
36+ padding: 14px 45px !important;
37+ }
38+
39+ /* Status indicator styles */
40+ /* Root (45px horizontal padding) and level-1 (25px padding) already have enough room for the icon */
41+ .map-container me-tpc {
42+ position: relative;
43+ }
44+ /* Level-2+ nodes: always reserve icon space so text never shifts when status changes. */
45+ .map-container me-children me-parent me-tpc {
46+ padding-left: 20px;
47+ box-sizing: border-box;
48+ }
49+ .map-container me-tpc[data-status=""completed""] {
3550 text-decoration: line-through;
3651 }
37- .node-in-progress::before {
38- content: '⟳ ';
39- color: #ff9800;
52+ .map-container me-tpc[data-status=""completed""] > .text {
53+ text-decoration: line-through !important;
54+ }
55+ .map-container me-tpc[data-status]::before {
56+ position: absolute;
57+ left: 2px; /* level-2+: within the 20px padding we add */
58+ top: 50%;
59+ transform: translateY(-50%);
60+ width: 14px;
61+ text-align: center;
4062 font-weight: bold;
4163 }
42- .node-completed::before {
43- content: '✓ ';
64+ /* Center icon in the existing left padding for root (45px) and level-1 (25px) */
65+ .map-container me-root me-tpc[data-status]::before {
66+ left: 16px; /* (45px - 14px) / 2 */
67+ }
68+ .map-container me-main > me-wrapper > me-parent > me-tpc[data-status]::before {
69+ left: 6px; /* (25px - 14px) / 2 */
70+ }
71+ .map-container me-tpc[data-status=""in-progress""]::before {
72+ content: '⟳';
73+ color: #ff9800;
74+ }
75+ .map-container me-tpc[data-status=""completed""]::before {
76+ content: '✓';
4477 color: #4caf50;
45- font-weight: bold;
4678 }
4779 </style>
4880</head>
@@ -53,12 +85,61 @@ public class CodeMindMapHtml
5385 import MindElixir from ""http://codemindmap.vsext/MindElixir.js"";
5486
5587 let mind, themeManager, lastSelectedNode;
88+ let linkDivDebounceTimer = null;
89+ let scheduleRafHandle = null;
90+ let scheduleTimerHandle = null;
5691
5792 function initMindMap() {
5893 const options = {
5994 el: '#map',
6095 allowUndo: true,
6196 toolBar: true,
97+ contextMenu: {
98+ extend: [
99+ {
100+ name: '⟳ In Progress',
101+ onclick: () => {
102+ const node = mind.currentNode?.nodeObj;
103+ if (!node) return;
104+ node.data = node.data || {};
105+ node.data.status = 'in-progress';
106+ updateNodeStatus(node);
107+ window.chrome.webview.postMessage({ action: 'mindMapOperation', operationName: 'updateNodeStatus' });
108+ const cm = document.querySelector('.map-container > .context-menu'); if (cm) cm.hidden = true;
109+ }
110+ },
111+ {
112+ name: '✓ Completed',
113+ onclick: () => {
114+ const node = mind.currentNode?.nodeObj;
115+ if (!node) return;
116+ node.data = node.data || {};
117+ node.data.status = 'completed';
118+ updateNodeStatus(node);
119+ window.chrome.webview.postMessage({ action: 'mindMapOperation', operationName: 'updateNodeStatus' });
120+ const cm = document.querySelector('.map-container > .context-menu'); if (cm) cm.hidden = true;
121+ }
122+ },
123+ {
124+ name: '✕ Clear Status',
125+ onclick: () => {
126+ const node = mind.currentNode?.nodeObj;
127+ if (!node) return;
128+ node.data = node.data || {};
129+ delete node.data.status;
130+ updateNodeStatus(node);
131+ window.chrome.webview.postMessage({ action: 'mindMapOperation', operationName: 'updateNodeStatus' });
132+ const cm = document.querySelector('.map-container > .context-menu'); if (cm) cm.hidden = true;
133+ }
134+ },
135+ ]
136+ },
137+ view: {
138+ beforeSelect(el, node) {
139+ mind.currentNode = node;
140+ return true;
141+ }
142+ }
62143 };
63144
64145 const LIGHT_THEME = {
@@ -195,23 +276,27 @@ function initMindMap() {
195276 topic: 'Space - Expand/collapse nodes',
196277 id: 'bd1bb2ac4bbab458',
197278 },
198- ],
199- },
200- {
201- topic: 'C - Toggle node status (Not Started, In Progress, Completed)',
202- id: 'bd1bb2ac4bbab460',
203- children: [
204279 {
205- topic: 'Press C to cycle through status states',
206- id: 'bd1bb2ac4bbab461',
207- },
208- {
209- topic: 'Completed nodes show ✓ and appear faded',
210- id: 'bd1bb2ac4bbab462',
211- },
212- {
213- topic: 'In Progress nodes show ⟳ in orange',
214- id: 'bd1bb2ac4bbab463',
280+ topic: 'c - Cycle node status: (none) → In Progress → Completed → (none)',
281+ id: 'bd1bb2ac4bbab460',
282+ children: [
283+ {
284+ topic: 'In Progress nodes show ⟳ in orange',
285+ id: 'bd1bb2ac4bbab461',
286+ },
287+ {
288+ topic: 'Completed nodes show ✓ with strikethrough text',
289+ id: 'bd1bb2ac4bbab462',
290+ },
291+ {
292+ topic: 'Right-click a node to set status directly from the context menu',
293+ id: 'bd1bb2ac4bbab463',
294+ },
295+ {
296+ topic: 'Status is saved automatically with the diagram',
297+ id: 'bd1bb2ac4bbab464',
298+ },
299+ ],
215300 },
216301 ],
217302 },
@@ -235,27 +320,60 @@ function initMindMap() {
235320 };
236321 });
237322
323+ scheduleApplyAllStatuses();
238324 // Helper function to update node visual status
239325 function updateNodeStatus(nodeObj) {
240326 if (!nodeObj || !nodeObj.id) return;
241327 const nodeElement = MindElixir.E(nodeObj.id);
242328 if (!nodeElement) return;
243-
329+
244330 const status = nodeObj.data?.status || null;
245- const domEl = nodeElement.getEl?.();
331+ const domEl = nodeElement.getEl?.() || nodeElement ;
246332 if (!domEl) return;
247-
248- // Remove all status classes
249- domEl.classList.remove('node-completed', 'node-in-progress');
250-
251- // Apply new status class
252- if (status === 'completed') {
253- domEl.classList.add('node-completed');
254- } else if (status === 'in-progress') {
255- domEl.classList.add('node-in-progress');
333+
334+ const topicEl = (() => {
335+ if (domEl.tagName === 'ME-TPC') return domEl;
336+ const byQuery = domEl.querySelector?.('me-tpc');
337+ if (byQuery) return byQuery;
338+ const byTag = domEl.getElementsByTagName?.('me-tpc')?.[0];
339+ if (byTag) return byTag;
340+ return domEl;
341+ })();
342+
343+ if (!topicEl) return;
344+
345+ if (!status) {
346+ topicEl.removeAttribute('data-status');
347+ return;
348+ }
349+
350+ topicEl.setAttribute('data-status', status);
351+ }
352+
353+ function applyAllStatuses() {
354+ const root = mind?.nodeData;
355+ if (!root) return;
356+ const stack = [root];
357+ while (stack.length > 0) {
358+ const node = stack.pop();
359+ if (!node) continue;
360+ updateNodeStatus(node);
361+ if (Array.isArray(node.children)) {
362+ for (const child of node.children) {
363+ stack.push(child);
364+ }
365+ }
256366 }
257367 }
258368
369+ function scheduleApplyAllStatuses() {
370+ if (!mind) return;
371+ if (scheduleRafHandle !== null) cancelAnimationFrame(scheduleRafHandle);
372+ if (scheduleTimerHandle !== null) clearTimeout(scheduleTimerHandle);
373+ scheduleRafHandle = requestAnimationFrame(() => { applyAllStatuses(); scheduleRafHandle = null; });
374+ scheduleTimerHandle = setTimeout(() => { applyAllStatuses(); scheduleTimerHandle = null; }, 50);
375+ }
376+
259377 mind.bus.addListener('selectNodes', nodes => {
260378 const node = nodes.at(-1);
261379 if (!node) return;
@@ -266,18 +384,27 @@ function updateNodeStatus(nodeObj) {
266384 nodeTopic: node.topic,
267385 nodeData: node.data,
268386 });
387+ scheduleApplyAllStatuses();
269388 });
270389
271390 mind.bus.addListener('selectNewNode', () => {
272391 // Mirror the same focus behavior for newly created nodes.
273392 mind.map?.focus();
274393 });
275394
276- mind.bus.addListener('operation', operation => {
277- window.chrome.webview.postMessage({
278- action: 'mindMapOperation',
279- operationName: operation.name,
280- });
395+ // Debounced linkDiv listener: MindElixir fires linkDiv after every layout pass.
396+ // Wait for 50ms of silence before applying statuses so we run after the final DOM state.
397+ mind.bus.addListener('linkDiv', () => {
398+ clearTimeout(linkDivDebounceTimer);
399+ linkDivDebounceTimer = setTimeout(applyAllStatuses, 50);
400+ });
401+
402+ mind.bus.addListener('operation', operation => {
403+ window.chrome.webview.postMessage({
404+ action: 'mindMapOperation',
405+ operationName: operation.name,
406+ });
407+ scheduleApplyAllStatuses();
281408 });
282409
283410 document.addEventListener('click', (e) => {
@@ -314,13 +441,15 @@ function updateNodeStatus(nodeObj) {
314441 }
315442 }
316443 else if ((e.key === 'c' || e.key === 'C') && !e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey) {
444+ // Skip if MindElixir's inline editor is open
445+ if (document.getElementById('input-box')) return;
317446 e.preventDefault();
318447 const currentNode = mind.currentNode?.nodeObj;
319448 if (!currentNode) return;
320449
321450 currentNode.data = currentNode.data || {};
322-
323- // Cycle through status : (none) -> in-progress -> completed -> (none)
451+
452+ // Cycle: (none) -> in-progress -> completed -> (none)
324453 const statuses = [null, 'in-progress', 'completed'];
325454 const currentStatus = currentNode.data.status || null;
326455 const currentIndex = statuses.indexOf(currentStatus);
@@ -330,9 +459,9 @@ function updateNodeStatus(nodeObj) {
330459 } else {
331460 currentNode.data.status = next;
332461 }
333-
334- // Update visual appearance
462+
335463 updateNodeStatus(currentNode);
464+ window.chrome.webview.postMessage({ action: 'mindMapOperation', operationName: 'updateNodeStatus' });
336465 }
337466 });
338467
@@ -438,10 +567,11 @@ function getThemeName(mindElixirData) {
438567 mind.clearHistory();
439568
440569 const dataThemeName = getThemeName(mindData);
441-
442570 if (dataThemeName != '' && themeManager.contains(dataThemeName) && dataThemeName != mind.theme?.name) {
443571 mind.changeTheme(themeManager.getTheme(dataThemeName));
444572 }
573+ // Statuses are applied via the debounced linkDiv bus listener
574+ // which fires after MindElixir's layout settles.
445575
446576 return { success: true, error: """" };
447577 } catch (e) {
0 commit comments