// Section dot navigation — labels always visible. // Visual motif: a vertical "spectrum bar" of light, with each section as a node on the bar. // The active node lights up like a lamp; the connector line behind reads as a wavelength gradient. function Remote({ active, variant, setVariant }) { const { locale, t } = useI18n(); // Detect whether the dot nav currently sits over a dark or light region of the page, // so we can flip the colors for legibility. The nav's vertical center is fixed at 50vh, // so we sample the element directly under that point and read its background luminance. const [theme, setTheme] = React.useState('dark'); const [sections, setSections] = React.useState(() => window.SoladuoNavigation.getRemoteSections(t)); React.useEffect(() => { const compute = () => { const x = window.innerWidth - 28 - 7; // approx node center const y = window.innerHeight / 2; const stack = document.elementsFromPoint(x, y) || []; // Explicit override: any ancestor with data-nav-theme wins over bg detection for (const el of stack) { if (el.closest('[aria-label="Section navigation"]')) continue; const themed = el.closest('[data-nav-theme]'); if (themed) { const t = themed.getAttribute('data-nav-theme'); if (t === 'dark' || t === 'light') { setTheme(t); return; } } break; } // Walk up the stack and find the first element with a non-transparent bg for (const el of stack) { if (el.closest('[aria-label="Section navigation"]')) continue; const cs = getComputedStyle(el); const bg = cs.backgroundColor; const m = bg.match(/rgba?\(([^)]+)\)/); if (!m) continue; const parts = m[1].split(',').map(s => parseFloat(s.trim())); const a = parts[3] === undefined ? 1 : parts[3]; if (a < 0.5) continue; const [r, g, b] = parts; // Perceived luminance const lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255; setTheme(lum < 0.55 ? 'dark' : 'light'); return; } }; compute(); let raf = 0; const onScroll = () => { cancelAnimationFrame(raf); raf = requestAnimationFrame(compute); }; window.addEventListener('scroll', onScroll, { passive: true }); window.addEventListener('resize', onScroll); // Re-sample periodically so Hero slide cross-fades (which don't fire scroll) update too const iv = setInterval(compute, 600); return () => { window.removeEventListener('scroll', onScroll); window.removeEventListener('resize', onScroll); cancelAnimationFrame(raf); clearInterval(iv); }; }, []); React.useEffect(() => { let alive = true; const fallbackSections = window.SoladuoNavigation.getRemoteSections(t); setSections(fallbackSections); window.SoladuoNavigation.loadRemoteSections(locale, t).then((items) => { if (alive) setSections(items); }); return () => { alive = false; }; }, [locale]); const onDark = theme === 'dark'; const labelActive = onDark ? 'var(--bone)' : 'var(--ink)'; const labelInactive = onDark ? 'rgba(244,238,228,0.55)' : 'rgba(22,19,16,0.45)'; const labelShadow = onDark ? '0 1px 2px rgba(0,0,0,0.35)' : 'none'; const dotInactiveBg = onDark ? 'rgba(244,238,228,0.55)' : 'rgba(22,19,16,0.40)'; const railGrad = onDark ? 'linear-gradient(180deg, rgba(248,200,120,0.65) 0%, rgba(244,238,228,0.55) 35%, rgba(244,238,228,0.42) 70%, rgba(244,238,228,0.30) 100%)' : 'linear-gradient(180deg, rgba(232,154,60,0.55) 0%, rgba(22,19,16,0.32) 35%, rgba(22,19,16,0.22) 70%, rgba(22,19,16,0.14) 100%)'; const activeIdx = Math.max(0, sections.findIndex(s => s.match.includes(active))); // 0..1 normalized progress along the spectrum const progress = sections.length > 1 ? activeIdx / (sections.length - 1) : 0; return ( ); } window.Remote = Remote;