From c5ce5eb06d9d6eff156534798c89bb71132ad72e Mon Sep 17 00:00:00 2001 From: FrankZamora Date: Fri, 7 Nov 2025 09:55:56 -0600 Subject: [PATCH] [COMPONENTE 11] Implementar Sidebar TOC con ScrollSpy custom - Issue #121 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CREAR: template-parts/content-toc.php * Función PHP apu_generate_toc() para generar TOC automáticamente * Regex para buscar H2 con atributo ID * Solo se muestra en is_single() - MODIFICAR: single.php (sidebar) * Agregar div.sidebar-sticky wrapper * Integrar get_template_part('template-parts/content', 'toc') * Preparar espacio para CTA Box (componente 12) - MODIFICAR: style.css (+87 líneas) * 9 selectores CSS + 4 pseudo-elementos scrollbar * Sticky positioning (top: 85px) * Max-height calculado con calc() * Scroll interno con overflow-y: auto * Min-height: 0 (crítico para scroll en flexbox) * Border-left indicator (navy, NO naranja) * Scrollbar personalizado 6px (solo Webkit) - MODIFICAR: main.js (+66 líneas) * Función updateActiveSection() para ScrollSpy * Algoritmo SIMPLE: verifica si pasaste el top * Offset scroll: navbarHeight + 100 * Smooth scroll: navbarHeight + 40 * Selector .toc-container a (NO .toc-link) * NO tiene auto-scroll del TOC * NO actualiza URL con history.pushState() Características: ✅ Generación automática desde H2 con ID ✅ ScrollSpy custom (JavaScript vanilla, NO Bootstrap) ✅ Sticky positioning con flexbox ✅ Scroll interno solo en lista ✅ Border-left indicator activo ✅ Scrollbar delgado personalizado Selectores REALES usados: - .toc-container a (NO .toc-link) - .toc-container h4 (NO .toc-title) - .toc-container li (NO .toc-list li) 🎨 Generated with Claude Code Co-Authored-By: Claude --- .../themes/apus-theme/assets/css/style.css | 87 +++++++++++++++++++ .../themes/apus-theme/assets/js/main.js | 66 ++++++++++++++ wp-content/themes/apus-theme/single.php | 6 +- .../apus-theme/template-parts/content-toc.php | 65 ++++++++++++++ 4 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 wp-content/themes/apus-theme/template-parts/content-toc.php diff --git a/wp-content/themes/apus-theme/assets/css/style.css b/wp-content/themes/apus-theme/assets/css/style.css index f07439fd..38673ff7 100644 --- a/wp-content/themes/apus-theme/assets/css/style.css +++ b/wp-content/themes/apus-theme/assets/css/style.css @@ -918,3 +918,90 @@ img { .post-content a:hover { color: var(--color-orange-hover); } + +/* === SIDEBAR TOC === */ + +.sidebar-sticky { + position: sticky; + top: 85px; + display: flex; + flex-direction: column; +} + +.toc-container { + margin-bottom: 13px; + background: #ffffff; + border: 1px solid var(--color-neutral-100); + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + padding: 12px 16px; + max-height: calc(100vh - 71px - 10px - 250px - 15px - 15px); + display: flex; + flex-direction: column; +} + +.toc-container h4 { + color: var(--color-navy-primary); + padding-bottom: 8px; + border-bottom: 2px solid var(--color-neutral-100); + margin-bottom: 0.75rem; + font-weight: 600; + text-align: left; + font-size: 1rem; + font-style: normal; +} + +.toc-list { + overflow-y: auto; + padding-right: 0.5rem; + list-style: none; + flex: 1; + min-height: 0; +} + +.toc-container li { + margin-bottom: 0.15rem; +} + +.toc-container a { + display: block; + padding: 0.3rem 0.85rem; + color: var(--color-neutral-600); + text-decoration: none; + border-left: 3px solid transparent; + transition: all 0.3s ease; + border-radius: 4px; + font-size: 0.9rem; + line-height: 1.3; +} + +.toc-container a:hover { + background: var(--color-neutral-50); + border-left-color: var(--color-navy-primary); + color: var(--color-navy-primary); +} + +.toc-container a.active { + background: var(--color-neutral-50); + border-left-color: var(--color-navy-primary); + color: var(--color-navy-primary); + font-weight: 600; +} + +.toc-list::-webkit-scrollbar { + width: 6px; +} + +.toc-list::-webkit-scrollbar-track { + background: var(--color-neutral-50); + border-radius: 3px; +} + +.toc-list::-webkit-scrollbar-thumb { + background: var(--color-neutral-600); + border-radius: 3px; +} + +.toc-list::-webkit-scrollbar-thumb:hover { + background: var(--color-neutral-700); +} diff --git a/wp-content/themes/apus-theme/assets/js/main.js b/wp-content/themes/apus-theme/assets/js/main.js index d9fe1619..3cf92c1c 100644 --- a/wp-content/themes/apus-theme/assets/js/main.js +++ b/wp-content/themes/apus-theme/assets/js/main.js @@ -265,3 +265,69 @@ document.addEventListener('DOMContentLoaded', function() { }); console.log('%c APU México ', 'background: #1e3a5f; color: #FF8600; font-size: 16px; font-weight: bold; padding: 10px;'); + +// === SIDEBAR TOC - ScrollSpy === + +// Table of Contents - ScrollSpy +function updateActiveSection() { + const tocLinks = document.querySelectorAll('.toc-container a'); + if (!tocLinks.length) return; + + const navbar = document.querySelector('.navbar'); + const navbarHeight = navbar ? navbar.offsetHeight : 0; + + const sectionIds = Array.from(tocLinks).map(link => { + const href = link.getAttribute('href'); + return href ? href.substring(1) : null; + }).filter(id => id !== null); + + const sections = sectionIds.map(id => document.getElementById(id)).filter(el => el !== null); + const scrollPosition = window.scrollY + navbarHeight + 100; + + let activeSection = null; + + for (let i = 0; i < sections.length; i++) { + const section = sections[i]; + const sectionTop = section.offsetTop; + + if (scrollPosition >= sectionTop) { + activeSection = section.getAttribute('id'); + } else { + break; + } + } + + tocLinks.forEach(link => { + link.classList.remove('active'); + const href = link.getAttribute('href'); + if (href === '#' + activeSection) { + link.classList.add('active'); + } + }); +} + +// Smooth scroll for TOC links +document.addEventListener('DOMContentLoaded', function() { + document.querySelectorAll('.toc-container a').forEach(anchor => { + anchor.addEventListener('click', function (e) { + e.preventDefault(); + const targetId = this.getAttribute('href'); + const targetElement = document.querySelector(targetId); + + if (targetElement) { + const navbar = document.querySelector('.navbar'); + const navbarHeight = navbar ? navbar.offsetHeight : 0; + const offsetTop = targetElement.offsetTop - navbarHeight - 40; + + window.scrollTo({ + top: offsetTop, + behavior: 'smooth' + }); + } + }); + }); + + updateActiveSection(); +}); + +window.addEventListener('scroll', updateActiveSection); diff --git a/wp-content/themes/apus-theme/single.php b/wp-content/themes/apus-theme/single.php index efa7a207..524ad217 100644 --- a/wp-content/themes/apus-theme/single.php +++ b/wp-content/themes/apus-theme/single.php @@ -195,7 +195,11 @@ get_header();
- +
diff --git a/wp-content/themes/apus-theme/template-parts/content-toc.php b/wp-content/themes/apus-theme/template-parts/content-toc.php new file mode 100644 index 00000000..41a54c72 --- /dev/null +++ b/wp-content/themes/apus-theme/template-parts/content-toc.php @@ -0,0 +1,65 @@ +]*id=["']([^"']*) ["'][^>]*>(.*?) + preg_match_all('/]*id=["\']([^"\']*)["\'][^>]*>(.*?)<\/h2>/i', $content, $matches); + + // Si no hay H2 con ID, no mostrar TOC + if (empty($matches[1])) { + return ''; + } + + // Iniciar construcción del TOC + $toc = '
'; + $toc .= '

Tabla de Contenido

'; + $toc .= '
    '; + + // Iterar sobre cada H2 encontrado + foreach ($matches[1] as $index => $id) { + // Limpiar el título (eliminar tags HTML internos) + $title = strip_tags($matches[2][$index]); + + // Crear el elemento de la lista + $toc .= sprintf( + '
  1. %s
  2. ', + esc_attr($id), + esc_html($title) + ); + } + + $toc .= '
'; + $toc .= '
'; + + return $toc; +} + +// Obtener el contenido del post actual +global $post; +$post_content = $post->post_content; + +// Aplicar filtros de WordPress al contenido (shortcodes, etc.) +$post_content = apply_filters('the_content', $post_content); + +// Generar y mostrar el TOC +echo apu_generate_toc($post_content);