From a29f5c5982b2a08b232c74bb30d0019253bbe51f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Gagn=C3=A9?= Date: Sun, 8 Mar 2026 15:53:14 -0400 Subject: [PATCH 01/15] Add filter to hide completed tasks and their descendants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add 🙈 toolbar toggle button that hides/shows all completed nodes and their entire subtrees - Completed node + descendant DOM wrappers get .mm-node-hidden (display: none) when the filter is active; the class is removed when the filter is toggled off - For level-1 nodes the me-wrapper is hidden (no blank connector stub left behind); for deeper nodes me-parent is hidden - applyFilter() is called from applyAllStatuses() so the filter stays consistent after every status change or layout refresh - Button shows an active/pressed style (.mm-btn-active) while the filter is on - Added help-text node to the default diagram describing the button --- VS Code/src/extension.ts | 89 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/VS Code/src/extension.ts b/VS Code/src/extension.ts index 364c78a..fffca3e 100644 --- a/VS Code/src/extension.ts +++ b/VS Code/src/extension.ts @@ -790,6 +790,16 @@ export class CodeMindMapPanel { content: '✓'; color: #4caf50; } + + /* Filter: hide completed nodes and their descendants */ + .mm-node-hidden { + display: none !important; + } + /* Active state for toggle buttons */ + .mm-btn-active { + background: #555 !important; + box-shadow: inset 0 0 0 1px #888; + } @@ -830,6 +840,11 @@ export class CodeMindMapPanel { 🎨 Toggle Color Scheme + +
@@ -843,6 +858,7 @@ export class CodeMindMapPanel { let linkDivDebounceTimer = null; // debounce timer for the linkDiv bus event let scheduleRafHandle = null; let scheduleTimerHandle = null; + let hideCompleted = false; // filter: hide completed nodes and their descendants function initMindMap() { const options = { @@ -1068,6 +1084,10 @@ export class CodeMindMapPanel { }, ], }, + { + topic: '🙈 toolbar button — toggle hide/show all completed tasks and their descendants', + id: 'bd1bb2ac4bbab465', + }, ], }, ], @@ -1143,6 +1163,65 @@ export class CodeMindMapPanel { } // linkDiv is called by MindElixir itself after layout; we must not call it here // as that would create an infinite loop via the linkDiv bus listener + applyFilter(); + } + + // Returns the DOM element to hide/show for a given node (its wrapper, including children). + // For level-1 nodes (me-parent inside me-wrapper) we hide me-wrapper so no blank + // space remains from the connector stub. For deeper nodes we hide me-parent. + function getHideTargetEl(nodeObj) { + if (!nodeObj || !nodeObj.id) return null; + const nodeElement = MindElixir.E(nodeObj.id); + if (!nodeElement) return null; + const domEl = nodeElement.getEl?.() || nodeElement; + if (!domEl) return null; + + const topicEl = (() => { + if (domEl.tagName === 'ME-TPC') return domEl; + return domEl.querySelector?.('me-tpc') || + domEl.getElementsByTagName?.('me-tpc')?.[0] || + domEl; + })(); + + const meParent = topicEl.closest?.('me-parent'); + if (!meParent) return topicEl.parentElement; + const meParentParent = meParent.parentElement; + if (meParentParent && meParentParent.tagName?.toLowerCase() === 'me-wrapper') { + return meParentParent; + } + return meParent; + } + + // Walks all nodes and adds/removes .mm-node-hidden based on hideCompleted state. + // Descendants of a completed node are hidden even if they carry no status themselves. + function applyFilter() { + if (!mind) return; + const root = mind.nodeData; + if (!root) return; + + function processNode(nodeObj, ancestorCompleted) { + const isCompleted = nodeObj.data?.status === 'completed'; + const shouldHide = hideCompleted && (isCompleted || ancestorCompleted); + + if (nodeObj.id !== 'me-root') { + const wrapperEl = getHideTargetEl(nodeObj); + if (wrapperEl) { + if (shouldHide) { + wrapperEl.classList.add('mm-node-hidden'); + } else { + wrapperEl.classList.remove('mm-node-hidden'); + } + } + } + + if (Array.isArray(nodeObj.children)) { + for (const child of nodeObj.children) { + processNode(child, ancestorCompleted || isCompleted); + } + } + } + + processNode(root, false); } function scheduleApplyAllStatuses() { @@ -1458,6 +1537,16 @@ export class CodeMindMapPanel { }); } + // Hide Completed button + const hideCompletedBtn = document.getElementById('hideCompletedBtn'); + if (hideCompletedBtn) { + hideCompletedBtn.addEventListener('click', () => { + hideCompleted = !hideCompleted; + hideCompletedBtn.classList.toggle('mm-btn-active', hideCompleted); + scheduleApplyAllStatuses(); + }); + } + initMindMap(); } From ae79f68b5bf7b5b3584b19303e5a528ef58149d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Gagn=C3=A9?= Date: Sun, 8 Mar 2026 16:06:01 -0400 Subject: [PATCH 02/15] Fix hide-completed filter not working MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit applyFilter and getHideTargetEl were defined inside initMindMap(), so they were out of scope when the button handler in setupUI() called scheduleApplyAllStatuses() — causing a silent ReferenceError that prevented any hiding from happening. Fixes: - Move getHideTargetEl and applyFilter to module level (accessible from anywhere in the script) - Simplify getHideTargetEl: use document.getElementById(id) directly to get me-parent, then check if its parentElement is me-wrapper (one straightforward traversal, no intermediate me-tpc lookup) - In the button click handler call applyFilter() directly instead of scheduleApplyAllStatuses(), which was out of scope --- VS Code/src/extension.ts | 100 ++++++++++++++++----------------------- 1 file changed, 41 insertions(+), 59 deletions(-) diff --git a/VS Code/src/extension.ts b/VS Code/src/extension.ts index fffca3e..ae41d38 100644 --- a/VS Code/src/extension.ts +++ b/VS Code/src/extension.ts @@ -860,6 +860,46 @@ export class CodeMindMapPanel { let scheduleTimerHandle = null; let hideCompleted = false; // filter: hide completed nodes and their descendants + // Returns the DOM element to hide/show for a given node (its wrapper, including children). + // For any node, its me-parent element is fetched by ID; if its immediate parent is + // me-wrapper we hide the wrapper so no connector stub / margin is left behind. + function getHideTargetEl(nodeObj) { + if (!nodeObj || !nodeObj.id) return null; + const meParent = document.getElementById(nodeObj.id); + if (!meParent) return null; + const parent = meParent.parentElement; + if (parent && parent.tagName.toLowerCase() === 'me-wrapper') return parent; + return meParent; + } + + // Walks the full node tree and adds/removes .mm-node-hidden based on hideCompleted. + // Descendants of a completed node are hidden even if they carry no status themselves. + function applyFilter() { + if (!mind) return; + const root = mind.nodeData; + if (!root) return; + + function processNode(nodeObj, ancestorCompleted) { + const isCompleted = nodeObj.data?.status === 'completed'; + const shouldHide = hideCompleted && (isCompleted || ancestorCompleted); + + if (nodeObj.id !== 'me-root') { + const el = getHideTargetEl(nodeObj); + if (el) { + el.classList.toggle('mm-node-hidden', shouldHide); + } + } + + if (Array.isArray(nodeObj.children)) { + for (const child of nodeObj.children) { + processNode(child, ancestorCompleted || isCompleted); + } + } + } + + processNode(root, false); + } + function initMindMap() { const options = { el: '#map', @@ -1166,64 +1206,6 @@ export class CodeMindMapPanel { applyFilter(); } - // Returns the DOM element to hide/show for a given node (its wrapper, including children). - // For level-1 nodes (me-parent inside me-wrapper) we hide me-wrapper so no blank - // space remains from the connector stub. For deeper nodes we hide me-parent. - function getHideTargetEl(nodeObj) { - if (!nodeObj || !nodeObj.id) return null; - const nodeElement = MindElixir.E(nodeObj.id); - if (!nodeElement) return null; - const domEl = nodeElement.getEl?.() || nodeElement; - if (!domEl) return null; - - const topicEl = (() => { - if (domEl.tagName === 'ME-TPC') return domEl; - return domEl.querySelector?.('me-tpc') || - domEl.getElementsByTagName?.('me-tpc')?.[0] || - domEl; - })(); - - const meParent = topicEl.closest?.('me-parent'); - if (!meParent) return topicEl.parentElement; - const meParentParent = meParent.parentElement; - if (meParentParent && meParentParent.tagName?.toLowerCase() === 'me-wrapper') { - return meParentParent; - } - return meParent; - } - - // Walks all nodes and adds/removes .mm-node-hidden based on hideCompleted state. - // Descendants of a completed node are hidden even if they carry no status themselves. - function applyFilter() { - if (!mind) return; - const root = mind.nodeData; - if (!root) return; - - function processNode(nodeObj, ancestorCompleted) { - const isCompleted = nodeObj.data?.status === 'completed'; - const shouldHide = hideCompleted && (isCompleted || ancestorCompleted); - - if (nodeObj.id !== 'me-root') { - const wrapperEl = getHideTargetEl(nodeObj); - if (wrapperEl) { - if (shouldHide) { - wrapperEl.classList.add('mm-node-hidden'); - } else { - wrapperEl.classList.remove('mm-node-hidden'); - } - } - } - - if (Array.isArray(nodeObj.children)) { - for (const child of nodeObj.children) { - processNode(child, ancestorCompleted || isCompleted); - } - } - } - - processNode(root, false); - } - function scheduleApplyAllStatuses() { if (!mind) return; if (scheduleRafHandle !== null) cancelAnimationFrame(scheduleRafHandle); @@ -1543,7 +1525,7 @@ export class CodeMindMapPanel { hideCompletedBtn.addEventListener('click', () => { hideCompleted = !hideCompleted; hideCompletedBtn.classList.toggle('mm-btn-active', hideCompleted); - scheduleApplyAllStatuses(); + applyFilter(); }); } From 72e9de7e66173b0aa6fdf715b0d1bad885682b7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Gagn=C3=A9?= Date: Sun, 8 Mar 2026 19:01:05 -0400 Subject: [PATCH 03/15] Fix getHideTargetEl using wrong DOM selector document.getElementById(id) always returned null because MindElixir does not use the node id as the HTML element id. Instead it renders nodes as me-parent[data-nodeid="me"]. Use querySelector with the correct attribute selector. --- VS Code/src/extension.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/VS Code/src/extension.ts b/VS Code/src/extension.ts index ae41d38..8f8c5c6 100644 --- a/VS Code/src/extension.ts +++ b/VS Code/src/extension.ts @@ -861,11 +861,12 @@ export class CodeMindMapPanel { let hideCompleted = false; // filter: hide completed nodes and their descendants // Returns the DOM element to hide/show for a given node (its wrapper, including children). - // For any node, its me-parent element is fetched by ID; if its immediate parent is - // me-wrapper we hide the wrapper so no connector stub / margin is left behind. + // MindElixir renders nodes as me-parent[data-nodeid="me"]. + // For level-1 nodes whose me-parent is a direct child of me-wrapper we hide me-wrapper + // so the branch gap/margin disappears too. For deeper nodes we hide me-parent itself. function getHideTargetEl(nodeObj) { if (!nodeObj || !nodeObj.id) return null; - const meParent = document.getElementById(nodeObj.id); + const meParent = document.querySelector(`[data-nodeid="me${nodeObj.id}"]`); if (!meParent) return null; const parent = meParent.parentElement; if (parent && parent.tagName.toLowerCase() === 'me-wrapper') return parent; From 2a141957f427deaf2e613e59dca9c59efb9d53c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Gagn=C3=A9?= Date: Sun, 8 Mar 2026 19:01:25 -0400 Subject: [PATCH 04/15] Fix template literal conflict in querySelector The JS template literal inside the TS template string caused a parse error. Use string concatenation instead. --- VS Code/src/extension.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VS Code/src/extension.ts b/VS Code/src/extension.ts index 8f8c5c6..dc45101 100644 --- a/VS Code/src/extension.ts +++ b/VS Code/src/extension.ts @@ -866,7 +866,7 @@ export class CodeMindMapPanel { // so the branch gap/margin disappears too. For deeper nodes we hide me-parent itself. function getHideTargetEl(nodeObj) { if (!nodeObj || !nodeObj.id) return null; - const meParent = document.querySelector(`[data-nodeid="me${nodeObj.id}"]`); + const meParent = document.querySelector('[data-nodeid="me' + nodeObj.id + '"]'); if (!meParent) return null; const parent = meParent.parentElement; if (parent && parent.tagName.toLowerCase() === 'me-wrapper') return parent; From 01d5fe2a4bfc85d25eb5b5ad6b9ad1101656b595 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Gagn=C3=A9?= Date: Sun, 8 Mar 2026 19:26:25 -0400 Subject: [PATCH 05/15] Fix: hide SVG branch lines when filtering completed nodes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a node was hidden with display:none, the SVG connector lines drawn by MindElixir remained visible because they are rendered on separate SVG overlay elements, not inside the node's own DOM subtree. Fix: - After hiding/showing me-wrapper elements, applyFilter() now also syncs SVG path visibility: Main branches (root → level-1): The .lines SVG has one per me-main>me-wrapper in DOM order. Paths for hidden wrappers are set to display:none. Sub-branches (level-2+): Each level-1 me-wrapper owns a subLines SVG (its last child) that holds ALL branch paths for the entire subtree, in the same DFS pre-order used by MindElixir's internal Ie() function. collectSubLineOrder() mirrors that traversal so paths[i] maps to nodesInOrder[i], and paths for hidden nodes are hidden. - getHideTargetEl() simplified: every me-parent[data-nodeid] is always the first child of its own me-wrapper, so parentElement is always me-wrapper regardless of depth. - Button click also calls mind.linkDiv() so the layout recompacts around hidden nodes; the debounced applyAllStatuses → applyFilter then cleans up any stale paths drawn by that linkDiv pass. --- VS Code/src/extension.ts | 69 ++++++++++++++++++++++++++++++++-------- 1 file changed, 56 insertions(+), 13 deletions(-) diff --git a/VS Code/src/extension.ts b/VS Code/src/extension.ts index dc45101..67b97e5 100644 --- a/VS Code/src/extension.ts +++ b/VS Code/src/extension.ts @@ -860,35 +860,52 @@ export class CodeMindMapPanel { let scheduleTimerHandle = null; let hideCompleted = false; // filter: hide completed nodes and their descendants - // Returns the DOM element to hide/show for a given node (its wrapper, including children). - // MindElixir renders nodes as me-parent[data-nodeid="me"]. - // For level-1 nodes whose me-parent is a direct child of me-wrapper we hide me-wrapper - // so the branch gap/margin disappears too. For deeper nodes we hide me-parent itself. + // Every me-parent[data-nodeid] is the first child of its own me-wrapper. + // Hiding me-wrapper hides the node, all its descendants, and its subLines SVG. function getHideTargetEl(nodeObj) { if (!nodeObj || !nodeObj.id) return null; const meParent = document.querySelector('[data-nodeid="me' + nodeObj.id + '"]'); if (!meParent) return null; - const parent = meParent.parentElement; - if (parent && parent.tagName.toLowerCase() === 'me-wrapper') return parent; - return meParent; + return meParent.parentElement; // always me-wrapper + } + + // Mirrors MindElixir's Ie() DFS traversal so we can enumerate me-wrapper elements + // in the exact same order as the elements in a level-1 wrapper's subLines SVG. + // + // MindElixir DOM layout (after linkDiv has run): + // me-wrapper – one per node at every depth + // me-parent[data-nodeid] – children[0]: the node box + // me-tpc – children[0]: topic text + // me-epd – children[1]: expand button (only if has kids) + // me-children – children[1]: children container (only if expanded) + // me-wrapper ... – grandchildren, each following the same pattern + // – lastChild: all sub-branch paths for the subtree + function collectSubLineOrder(wrapper, results) { + const second = wrapper.children[1]; + if (!second || second.tagName.toLowerCase() !== 'me-children') return; + for (const cw of second.children) { // cw = child me-wrapper + results.push(cw); // Ie draws a path for each child, always + const epd = cw.children[0]?.children[1]; // me-parent.children[1] = me-epd + if (!epd || !epd.expanded) continue; // Ie skips recursion for collapsed/leaf + collectSubLineOrder(cw, results); + } } // Walks the full node tree and adds/removes .mm-node-hidden based on hideCompleted. - // Descendants of a completed node are hidden even if they carry no status themselves. + // Also syncs the SVG branch lines so connectors to hidden nodes are hidden too. function applyFilter() { if (!mind) return; const root = mind.nodeData; if (!root) return; + // 1. Show/hide node wrapper elements function processNode(nodeObj, ancestorCompleted) { const isCompleted = nodeObj.data?.status === 'completed'; const shouldHide = hideCompleted && (isCompleted || ancestorCompleted); if (nodeObj.id !== 'me-root') { const el = getHideTargetEl(nodeObj); - if (el) { - el.classList.toggle('mm-node-hidden', shouldHide); - } + if (el) el.classList.toggle('mm-node-hidden', shouldHide); } if (Array.isArray(nodeObj.children)) { @@ -897,8 +914,33 @@ export class CodeMindMapPanel { } } } - processNode(root, false); + + // 2. Sync SVG branch line visibility. + + // Main branches (root → each level-1 wrapper): one per me-wrapper, in order. + const linesEl = document.querySelector('.map-container .lines'); + if (linesEl) { + const wrappers = document.querySelectorAll('me-main > me-wrapper'); + const paths = linesEl.children; + for (let i = 0; i < wrappers.length && i < paths.length; i++) { + paths[i].style.display = wrappers[i].classList.contains('mm-node-hidden') ? 'none' : ''; + } + } + + // Sub-branches: each visible level-1 me-wrapper has a subLines SVG as its last + // child whose elements are in the same DFS order as collectSubLineOrder(). + for (const wrapper of document.querySelectorAll('me-main > me-wrapper')) { + if (wrapper.classList.contains('mm-node-hidden')) continue; // subLines already hidden + const lastEl = wrapper.lastElementChild; + if (!lastEl || lastEl.tagName.toLowerCase() !== 'svg') continue; // no subLines yet + const nodesInOrder = []; + collectSubLineOrder(wrapper, nodesInOrder); + const subPaths = lastEl.children; + for (let i = 0; i < nodesInOrder.length && i < subPaths.length; i++) { + subPaths[i].style.display = nodesInOrder[i].classList.contains('mm-node-hidden') ? 'none' : ''; + } + } } function initMindMap() { @@ -1526,7 +1568,8 @@ export class CodeMindMapPanel { hideCompletedBtn.addEventListener('click', () => { hideCompleted = !hideCompleted; hideCompletedBtn.classList.toggle('mm-btn-active', hideCompleted); - applyFilter(); + applyFilter(); // hide/show nodes and stale SVG paths immediately + if (mind) mind.linkDiv(); // recompute layout; debounced applyFilter will clean up new paths }); } From 13a3868f2438571421695f082e57f8f2f7c3ca23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Gagn=C3=A9?= Date: Sun, 8 Mar 2026 23:07:07 -0400 Subject: [PATCH 06/15] Fix: SVG branch lines still visible after filter (wrong element) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getHideTargetEl was querying [data-nodeid] which MindElixir sets on me-tpc, not me-parent. So .parentElement returned me-parent, while the SVG path-hiding code checks classList on me-wrapper — a mismatch that meant mm-node-hidden was never found on the queried wrappers. Fix: go up two levels (me-tpc → me-parent → me-wrapper) so the class lands on me-wrapper, matching what querySelectorAll('me-main>me-wrapper') returns for path index correlation. Also reorder button handler: call mind.linkDiv() before applyFilter() so paths are hidden after linkDiv redraws them, not before. --- VS Code/src/extension.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/VS Code/src/extension.ts b/VS Code/src/extension.ts index 67b97e5..ca4105e 100644 --- a/VS Code/src/extension.ts +++ b/VS Code/src/extension.ts @@ -862,11 +862,13 @@ export class CodeMindMapPanel { // Every me-parent[data-nodeid] is the first child of its own me-wrapper. // Hiding me-wrapper hides the node, all its descendants, and its subLines SVG. + // data-nodeid is set on me-tpc (not me-parent), so we must go up two levels: + // me-tpc → me-parent → me-wrapper function getHideTargetEl(nodeObj) { if (!nodeObj || !nodeObj.id) return null; - const meParent = document.querySelector('[data-nodeid="me' + nodeObj.id + '"]'); - if (!meParent) return null; - return meParent.parentElement; // always me-wrapper + const meTpc = document.querySelector('[data-nodeid="me' + nodeObj.id + '"]'); + if (!meTpc) return null; + return meTpc.parentElement?.parentElement ?? null; // me-tpc → me-parent → me-wrapper } // Mirrors MindElixir's Ie() DFS traversal so we can enumerate me-wrapper elements @@ -1568,8 +1570,8 @@ export class CodeMindMapPanel { hideCompletedBtn.addEventListener('click', () => { hideCompleted = !hideCompleted; hideCompletedBtn.classList.toggle('mm-btn-active', hideCompleted); - applyFilter(); // hide/show nodes and stale SVG paths immediately - if (mind) mind.linkDiv(); // recompute layout; debounced applyFilter will clean up new paths + if (mind) mind.linkDiv(); // recompute layout with hidden nodes + applyFilter(); // hide nodes and SVG paths after linkDiv has redrawn them }); } From e919b9a3f0eddbac878f109260763e7fb578a0f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Gagn=C3=A9?= Date: Sun, 8 Mar 2026 23:33:01 -0400 Subject: [PATCH 07/15] Fix: remove linkDiv() from button handler to prevent layout corruption MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Calling mind.linkDiv() while nodes are display:none causes linkDiv to query offsetLeft/offsetTop of hidden elements (which return 0), drawing all their SVG paths to (0,0). This permanently corrupts the layout even after filtering is turned off. Fix: just call applyFilter() — the existing paths drawn in the correct full layout are hidden/shown in place. No relayout, no corruption. --- VS Code/src/extension.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/VS Code/src/extension.ts b/VS Code/src/extension.ts index ca4105e..81b3881 100644 --- a/VS Code/src/extension.ts +++ b/VS Code/src/extension.ts @@ -1570,8 +1570,7 @@ export class CodeMindMapPanel { hideCompletedBtn.addEventListener('click', () => { hideCompleted = !hideCompleted; hideCompletedBtn.classList.toggle('mm-btn-active', hideCompleted); - if (mind) mind.linkDiv(); // recompute layout with hidden nodes - applyFilter(); // hide nodes and SVG paths after linkDiv has redrawn them + applyFilter(); }); } From 96086a07f0c9f347a62c69c57c5a1d474ac7306e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Gagn=C3=A9?= Date: Mon, 9 Mar 2026 00:21:19 -0400 Subject: [PATCH 08/15] Refactor: use mind.lines/mind.map refs and setAttribute for path hiding - Use mind.lines and mind.map.querySelectorAll (same references linkDiv uses internally) instead of document.querySelector/querySelectorAll. - Use subSvg.querySelectorAll('path') instead of .children so only elements are counted (avoids any stray non-path children). - Set both setAttribute('display') and style.display for maximum SVG compatibility with Chromium's rendering of SVG presentation attrs. --- VS Code/src/extension.ts | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/VS Code/src/extension.ts b/VS Code/src/extension.ts index 81b3881..7148535 100644 --- a/VS Code/src/extension.ts +++ b/VS Code/src/extension.ts @@ -919,28 +919,34 @@ export class CodeMindMapPanel { processNode(root, false); // 2. Sync SVG branch line visibility. + // Use mind.lines and mind.map (same references linkDiv uses) to avoid + // any selector-based mismatch. Use setAttribute('display') + style.display + // for maximum SVG compatibility. + function setPathDisplay(pathEl, hide) { + pathEl.setAttribute('display', hide ? 'none' : ''); + pathEl.style.display = hide ? 'none' : ''; + } // Main branches (root → each level-1 wrapper): one per me-wrapper, in order. - const linesEl = document.querySelector('.map-container .lines'); - if (linesEl) { - const wrappers = document.querySelectorAll('me-main > me-wrapper'); - const paths = linesEl.children; - for (let i = 0; i < wrappers.length && i < paths.length; i++) { - paths[i].style.display = wrappers[i].classList.contains('mm-node-hidden') ? 'none' : ''; + if (mind.lines) { + const l1Wrappers = mind.map.querySelectorAll('me-main > me-wrapper'); + const mainPaths = mind.lines.querySelectorAll('path'); + for (let i = 0; i < l1Wrappers.length && i < mainPaths.length; i++) { + setPathDisplay(mainPaths[i], l1Wrappers[i].classList.contains('mm-node-hidden')); } } // Sub-branches: each visible level-1 me-wrapper has a subLines SVG as its last // child whose elements are in the same DFS order as collectSubLineOrder(). - for (const wrapper of document.querySelectorAll('me-main > me-wrapper')) { + for (const wrapper of mind.map.querySelectorAll('me-main > me-wrapper')) { if (wrapper.classList.contains('mm-node-hidden')) continue; // subLines already hidden const lastEl = wrapper.lastElementChild; if (!lastEl || lastEl.tagName.toLowerCase() !== 'svg') continue; // no subLines yet const nodesInOrder = []; collectSubLineOrder(wrapper, nodesInOrder); - const subPaths = lastEl.children; + const subPaths = lastEl.querySelectorAll('path'); for (let i = 0; i < nodesInOrder.length && i < subPaths.length; i++) { - subPaths[i].style.display = nodesInOrder[i].classList.contains('mm-node-hidden') ? 'none' : ''; + setPathDisplay(subPaths[i], nodesInOrder[i].classList.contains('mm-node-hidden')); } } } From 986318b251c8e416695b698c84b42bc8a4d7c00d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Gagn=C3=A9?= Date: Mon, 9 Mar 2026 00:31:35 -0400 Subject: [PATCH 09/15] Fix: use visibility:hidden instead of display:none for filtered nodes display:none removes hidden nodes from layout flow, causing sibling nodes to shift position. SVG paths (drawn at fixed coordinates by linkDiv) then point to the old positions of the shifted nodes, causing the visible branches and labels to appear misaligned. visibility:hidden keeps all nodes in layout (no shift), so SVG path coordinates remain correct for all visible nodes. The hidden nodes are invisible but still occupy their original space, eliminating misalignment. --- VS Code/src/extension.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/VS Code/src/extension.ts b/VS Code/src/extension.ts index 7148535..733e7c6 100644 --- a/VS Code/src/extension.ts +++ b/VS Code/src/extension.ts @@ -791,9 +791,13 @@ export class CodeMindMapPanel { color: #4caf50; } - /* Filter: hide completed nodes and their descendants */ + /* Filter: hide completed nodes and their descendants. + NOTE: visibility:hidden (not display:none) is required so that + nodes stay in layout flow. display:none shifts sibling nodes, + which moves them away from the fixed SVG path coordinates and + causes visual misalignment of branches and labels. */ .mm-node-hidden { - display: none !important; + visibility: hidden !important; } /* Active state for toggle buttons */ .mm-btn-active { From 91a5fd5477a9711a0e97d5d5a8fd6b6af219e8c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Gagn=C3=A9?= Date: Mon, 9 Mar 2026 00:41:13 -0400 Subject: [PATCH 10/15] Feat: persist hide-completed filter state across sessions Restore hideCompleted from vscode.getState() before initMindMap() so the filter is active immediately on load (applyAllStatuses -> applyFilter picks it up). Save the new value to vscode.setState() on every toggle, spreading any other existing state keys so nothing is overwritten. --- VS Code/src/extension.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/VS Code/src/extension.ts b/VS Code/src/extension.ts index 733e7c6..528c0f9 100644 --- a/VS Code/src/extension.ts +++ b/VS Code/src/extension.ts @@ -862,7 +862,7 @@ export class CodeMindMapPanel { let linkDivDebounceTimer = null; // debounce timer for the linkDiv bus event let scheduleRafHandle = null; let scheduleTimerHandle = null; - let hideCompleted = false; // filter: hide completed nodes and their descendants + let hideCompleted = false; // filter: hide completed nodes and their descendants (persisted via vscode state) // Every me-parent[data-nodeid] is the first child of its own me-wrapper. // Hiding me-wrapper hides the node, all its descendants, and its subLines SVG. @@ -1577,9 +1577,15 @@ export class CodeMindMapPanel { // Hide Completed button const hideCompletedBtn = document.getElementById('hideCompletedBtn'); if (hideCompletedBtn) { + // Restore persisted filter state before initializing the mind map so + // applyAllStatuses() → applyFilter() picks up the correct value. + hideCompleted = !!(vscode.getState()?.hideCompleted); + hideCompletedBtn.classList.toggle('mm-btn-active', hideCompleted); + hideCompletedBtn.addEventListener('click', () => { hideCompleted = !hideCompleted; hideCompletedBtn.classList.toggle('mm-btn-active', hideCompleted); + vscode.setState({ ...(vscode.getState() || {}), hideCompleted }); applyFilter(); }); } From 112d725c5ec25bf62caf47e8fcfe18d9d0f3687f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Gagn=C3=A9?= Date: Mon, 9 Mar 2026 00:44:44 -0400 Subject: [PATCH 11/15] Fix: persist hide-completed filter via workspaceState (survives panel close) vscode.getState/setState only persists while the webview is alive (hide/show). When the panel is closed and reopened, a new webview is created and getState() returns null, so the previous approach lost the setting. Fix: store hideCompleted in ExtensionContext.workspaceState on the extension host, which survives panel close/reopen and VS Code restarts (workspace-scoped). - _getHtmlForWebview reads workspaceState and injects the value directly into the script as 'let hideCompleted = ' - no messaging or timing issues, the correct value is present from the very first applyFilter() call. - On toggle, webview posts setHideCompleted to extension which persists it. --- VS Code/src/extension.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/VS Code/src/extension.ts b/VS Code/src/extension.ts index 528c0f9..9b56a38 100644 --- a/VS Code/src/extension.ts +++ b/VS Code/src/extension.ts @@ -94,6 +94,7 @@ export class CodeMindMapPanel { public static CurrentPanel: CodeMindMapPanel | undefined; public static _context: vscode.ExtensionContext | undefined; public static readonly PANEL_OPEN_KEY = 'panelWasOpen'; + public static readonly HIDE_COMPLETED_KEY = 'hideCompleted'; private readonly _panel: vscode.WebviewPanel; private readonly _extensionUri: vscode.Uri; private _disposables: vscode.Disposable[] = []; @@ -405,6 +406,10 @@ export class CodeMindMapPanel { this.exportIfPathKnown(); break; + case 'setHideCompleted': + CodeMindMapPanel._context?.workspaceState.update(CodeMindMapPanel.HIDE_COMPLETED_KEY, message.value); + break; + case 'toggleColorScheme': this._panel.webview.postMessage({ action: 'toggleColorScheme' @@ -683,6 +688,7 @@ export class CodeMindMapPanel { const mindElixirUri = webview.asWebviewUri(mindElixirFileUri); const mindElixirStyleFileUri = vscode.Uri.joinPath(extensionUri, 'out', 'MindElixir', 'MindElixir.css'); const mindElixirStyleUri = webview.asWebviewUri(mindElixirStyleFileUri); + const initialHideCompleted = CodeMindMapPanel._context?.workspaceState.get(CodeMindMapPanel.HIDE_COMPLETED_KEY) ?? false; return ` @@ -862,7 +868,8 @@ export class CodeMindMapPanel { let linkDivDebounceTimer = null; // debounce timer for the linkDiv bus event let scheduleRafHandle = null; let scheduleTimerHandle = null; - let hideCompleted = false; // filter: hide completed nodes and their descendants (persisted via vscode state) + // Injected by extension host from workspaceState so the value survives panel close/reopen. + let hideCompleted = ${initialHideCompleted}; // Every me-parent[data-nodeid] is the first child of its own me-wrapper. // Hiding me-wrapper hides the node, all its descendants, and its subLines SVG. @@ -970,6 +977,7 @@ export class CodeMindMapPanel { node.data = node.data || {}; node.data.status = 'in-progress'; updateNodeStatus(node); + applyFilter(); vscode.postMessage({ action: 'mindMapOperation', operationName: 'updateNodeStatus' }); const cm = document.querySelector('.map-container > .context-menu'); if (cm) cm.hidden = true; } @@ -982,6 +990,7 @@ export class CodeMindMapPanel { node.data = node.data || {}; node.data.status = 'completed'; updateNodeStatus(node); + applyFilter(); vscode.postMessage({ action: 'mindMapOperation', operationName: 'updateNodeStatus' }); const cm = document.querySelector('.map-container > .context-menu'); if (cm) cm.hidden = true; } @@ -994,6 +1003,7 @@ export class CodeMindMapPanel { node.data = node.data || {}; delete node.data.status; updateNodeStatus(node); + applyFilter(); vscode.postMessage({ action: 'mindMapOperation', operationName: 'updateNodeStatus' }); const cm = document.querySelector('.map-container > .context-menu'); if (cm) cm.hidden = true; } @@ -1577,15 +1587,14 @@ export class CodeMindMapPanel { // Hide Completed button const hideCompletedBtn = document.getElementById('hideCompletedBtn'); if (hideCompletedBtn) { - // Restore persisted filter state before initializing the mind map so - // applyAllStatuses() → applyFilter() picks up the correct value. - hideCompleted = !!(vscode.getState()?.hideCompleted); + // Initial value is injected from workspaceState; sync the button appearance. hideCompletedBtn.classList.toggle('mm-btn-active', hideCompleted); hideCompletedBtn.addEventListener('click', () => { hideCompleted = !hideCompleted; hideCompletedBtn.classList.toggle('mm-btn-active', hideCompleted); - vscode.setState({ ...(vscode.getState() || {}), hideCompleted }); + // Persist to extension host workspaceState so it survives panel close/reopen. + vscode.postMessage({ action: 'setHideCompleted', value: hideCompleted }); applyFilter(); }); } From 5305b512909806efd12826bdb29d96f7f3a31e7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Gagn=C3=A9?= Date: Mon, 9 Mar 2026 00:50:36 -0400 Subject: [PATCH 12/15] Fix: apply filter synchronously after mind.init() to prevent flash of unfiltered content scheduleApplyAllStatuses() defers via RAF + 50ms, causing a visible frame of the full unfiltered graph before filtering is applied. Calling applyFilter() immediately after mind.init() runs before the first paint, so hidden nodes are never visible to the user when hideCompleted is active. --- VS Code/src/extension.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/VS Code/src/extension.ts b/VS Code/src/extension.ts index 9b56a38..2cf1b91 100644 --- a/VS Code/src/extension.ts +++ b/VS Code/src/extension.ts @@ -1205,6 +1205,10 @@ export class CodeMindMapPanel { mind = new MindElixir(options); mind.init(data); + // Apply filter immediately (synchronous, before first paint) so that + // when hideCompleted is true the hidden nodes are never seen by the user. + applyFilter(); + // Apply any import that arrived before mind was ready if (pendingImport !== null) { window.importData(pendingImport); From afa6e1705d99e70c00dd7b5888916c24dec7f64f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Gagn=C3=A9?= Date: Mon, 9 Mar 2026 00:52:14 -0400 Subject: [PATCH 13/15] Fix: apply filter synchronously in importData to prevent flash of unfiltered content mind.refresh() calls linkDiv() which fires the linkDiv bus event, but our listener debounces it by 50ms. During that window the full unfiltered diagram is visible. Calling applyFilter() immediately after mind.refresh() hides completed nodes before the browser paints the imported data. --- VS Code/src/extension.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/VS Code/src/extension.ts b/VS Code/src/extension.ts index 2cf1b91..447942d 100644 --- a/VS Code/src/extension.ts +++ b/VS Code/src/extension.ts @@ -1496,8 +1496,9 @@ export class CodeMindMapPanel { if (dataThemeName != '' && themeManager.contains(dataThemeName) && dataThemeName != mind.theme?.name) { mind.changeTheme(themeManager.getTheme(dataThemeName)); } - // Statuses are applied via the debounced linkDiv bus listener - // which fires after MindElixir's layout settles. + // Apply filter synchronously so completed nodes are hidden before the first + // paint after the data loads (the debounced linkDiv listener is too slow). + applyFilter(); return { success: true, error: '' }; From d88a46a1bb3f5117ab6a92114d2946ec8e0f3cce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Gagn=C3=A9?= Date: Mon, 9 Mar 2026 01:00:17 -0400 Subject: [PATCH 14/15] Fix: hide container until after applyFilter() to eliminate flash of unfiltered content Start #container with opacity:0. After mind.init() or mind.refresh() calls applyFilter() synchronously, set opacity:1 with a 150ms ease-in transition. The filter is applied while the diagram is invisible, so completed nodes are never seen by the user regardless of load timing. --- VS Code/src/extension.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/VS Code/src/extension.ts b/VS Code/src/extension.ts index 447942d..790e2e2 100644 --- a/VS Code/src/extension.ts +++ b/VS Code/src/extension.ts @@ -708,7 +708,9 @@ export class CodeMindMapPanel { #container { height: 100vh; display: flex; - flex-direction: column; + flex-direction: column; + opacity: 0; + transition: opacity 0.15s ease-in; } #map { width: 100%; @@ -1205,9 +1207,10 @@ export class CodeMindMapPanel { mind = new MindElixir(options); mind.init(data); - // Apply filter immediately (synchronous, before first paint) so that - // when hideCompleted is true the hidden nodes are never seen by the user. + // Apply filter synchronously (before first paint) then reveal the + // container so completed nodes are never visible to the user. applyFilter(); + document.getElementById('container').style.opacity = '1'; // Apply any import that arrived before mind was ready if (pendingImport !== null) { @@ -1387,6 +1390,7 @@ export class CodeMindMapPanel { // Update visual appearance updateNodeStatus(currentNode); + applyFilter(); // Trigger autosave vscode.postMessage({ action: 'mindMapOperation', operationName: 'updateNodeStatus' }); @@ -1496,9 +1500,10 @@ export class CodeMindMapPanel { if (dataThemeName != '' && themeManager.contains(dataThemeName) && dataThemeName != mind.theme?.name) { mind.changeTheme(themeManager.getTheme(dataThemeName)); } - // Apply filter synchronously so completed nodes are hidden before the first - // paint after the data loads (the debounced linkDiv listener is too slow). + // Apply filter synchronously then reveal; completed nodes are + // hidden before the browser paints the imported data. applyFilter(); + document.getElementById('container').style.opacity = '1'; return { success: true, error: '' }; From 1365d845e042717e4ac5da036e7a08166f7292a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Gagn=C3=A9?= Date: Mon, 9 Mar 2026 01:09:54 -0400 Subject: [PATCH 15/15] Port hide-completed filter to Visual Studio extension --- Visual Studio/CodeMindMap/CodeMindMapHtml.cs | 147 +++++++++++++++++++ 1 file changed, 147 insertions(+) diff --git a/Visual Studio/CodeMindMap/CodeMindMapHtml.cs b/Visual Studio/CodeMindMap/CodeMindMapHtml.cs index 6385c35..6e6bdf8 100644 --- a/Visual Studio/CodeMindMap/CodeMindMapHtml.cs +++ b/Visual Studio/CodeMindMap/CodeMindMapHtml.cs @@ -76,9 +76,40 @@ public class CodeMindMapHtml content: '✓'; color: #4caf50; } + + /* Filter: hide completed nodes and their descendants. + NOTE: visibility:hidden (not display:none) is required so that + nodes stay in layout flow. display:none shifts sibling nodes, + which moves them away from the fixed SVG path coordinates and + causes visual misalignment of branches and labels. */ + .mm-node-hidden { + visibility: hidden !important; + } + /* Active state for toggle button */ + .mm-btn-active { + background: #555 !important; + box-shadow: inset 0 0 0 1px #888; + } + #hideCompletedBtn { + position: fixed; + top: 8px; + right: 10px; + z-index: 1000; + padding: 5px 10px; + cursor: pointer; + background: rgba(50,50,50,0.85); + color: #ddd; + border: 1px solid #555; + border-radius: 4px; + font-size: 13px; + } + #hideCompletedBtn:hover { + background: rgba(70,70,70,0.95); + } +