// Delegate to the component root so navigation still works after HTML updates. function activate(event) { const target = event.target.closest('[data-path]'); if (!target || !element.contains(target)) return; if (event.type === 'keydown' && !['Enter', ' '].includes(event.key)) return; // Native buttons already synthesize a click on keyboard activation. if (event.type === 'keydown' && target.tagName.toLowerCase() === 'button') return; event.preventDefault(); trigger('click', {route: target.dataset.path}); } element.addEventListener('click', activate); element.addEventListener('keydown', activate); // A group and the flux arriving at it share a data-node id, so hovering either // one lights both. The ribbons are painted beneath every group, so no CSS // sibling rule can reach them from the group being hovered. let lit = ''; function light(uid) { if (uid === lit) return; lit = uid; element.querySelectorAll('.is-lit').forEach((el) => el.classList.remove('is-lit')); if (uid) element.querySelectorAll(`[data-node="${CSS.escape(uid)}"]`).forEach((el) => el.classList.add('is-lit')); } element.addEventListener('focusin', (event) => light(event.target.closest('[data-node]')?.dataset.node || '')); element.addEventListener('focusout', () => light('')); element.addEventListener('pointermove', (event) => { const node = event.target.closest('[data-detail]'); const tooltip = element.querySelector('.atlas-tooltip'); const atlas = element.querySelector('.atlas'); if (!tooltip || !atlas) return; if (!node || event.pointerType === 'touch') { tooltip.hidden = true; light(''); return; } light(node.dataset.node || ''); tooltip.textContent = node.dataset.detail; tooltip.hidden = false; const bounds = atlas.getBoundingClientRect(); tooltip.style.left = Math.max(8, Math.min(event.clientX - bounds.left + 15, bounds.width - 305)) + 'px'; tooltip.style.top = Math.max(8, Math.min(event.clientY - bounds.top + 18, bounds.height - tooltip.offsetHeight - 8)) + 'px'; }); element.addEventListener('pointerleave', () => { const tooltip = element.querySelector('.atlas-tooltip'); if (tooltip) tooltip.hidden = true; light(''); }); // Expanding adds a level below. The page scrolls, not the widget, so only nudge // it when the new bottom of the tree has fallen past the viewport. let shownRoute = element.querySelector('.life-tree')?.dataset.route; new MutationObserver(() => { const route = element.querySelector('.life-tree')?.dataset.route; if (!route || route === shownRoute) return; const grew = route.length > shownRoute.length; shownRoute = route; if (!grew) return; requestAnimationFrame(() => { const tree = element.querySelector('.life-tree'); if (!tree) return; const bottom = tree.getBoundingClientRect().bottom; if (bottom > window.innerHeight) { window.scrollBy({top: Math.min(bottom - window.innerHeight + 24, 400), behavior: 'smooth'}); } }); }).observe(element, {childList: true, subtree: true, attributes: true, attributeFilter: ['data-route']});