Implementar navbar sticky con Bootstrap 5 y animaciones - Issue #7

Implementación completa de navbar sticky con menú hamburguesa responsive según especificaciones del template del cliente:

**Archivos Modificados:**
- header.php: Reescritura completa con navbar Bootstrap 5, sticky positioning, y responsive hamburger menu
- functions.php: Agregado require para nav-walker.php
- inc/enqueue-scripts.php: Agregado enqueue de custom-style.css y main.js

**Archivos Creados:**
- assets/css/custom-style.css: Estilos navbar con animaciones (gradient underline, dropdown slideDown, etc.)
- assets/js/main.js: JavaScript para scroll effect, active menu highlight, y mobile auto-close
- inc/nav-walker.php: Bootstrap 5 Nav Walker para dropdowns WordPress

**Características:**
 Navbar sticky con transición de sombra al hacer scroll
 Gradient underline animation en hover de nav-links
 Dropdown menus con animación slideDown
 Menú hamburguesa responsive (< 991px)
 Auto-close mobile menu al hacer click en enlaces
 Active menu item highlighting
 Smooth scroll para anchor links
 Skip link para accesibilidad
 Compatible con WordPress menu system

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
FrankZamora
2025-11-04 16:27:54 -06:00
parent 928e543215
commit 5440f23512
6 changed files with 712 additions and 93 deletions

View File

@@ -0,0 +1,276 @@
/**
* Custom Styles - APUS Theme
*
* Estilos personalizados según el template del cliente.
* Incluye: Navbar sticky, animaciones, y componentes específicos.
*
* @package Apus_Theme
* @since 1.0.0
*/
/* ============================================
NAVBAR STICKY CON ANIMACIONES
============================================ */
.navbar {
position: sticky;
top: 0;
z-index: 1030;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(0,0,0,0.08);
}
.navbar.scrolled {
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
background-color: #fff !important;
}
/* Gradient underline animation en hover */
.nav-link {
position: relative;
transition: all 0.3s ease;
padding: 0.5rem 1rem !important;
font-weight: 500;
}
.nav-link::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%) scaleX(0);
width: 80%;
height: 2px;
background: linear-gradient(90deg, #0d6efd, #0dcaf0);
transition: transform 0.3s ease;
}
.nav-link:hover {
color: #0d6efd !important;
background-color: rgba(13, 110, 253, 0.05);
border-radius: 4px;
transform: translateY(-2px);
}
.nav-link:hover::after {
transform: translateX(-50%) scaleX(1);
}
/* Active nav link */
.nav-link.active,
.nav-item.current-menu-item > .nav-link {
color: #0d6efd !important;
font-weight: 600;
}
.nav-link.active::after,
.nav-item.current-menu-item > .nav-link::after {
transform: translateX(-50%) scaleX(1);
}
/* Dropdown animations */
.dropdown-menu {
border: none;
box-shadow: 0 8px 24px rgba(0,0,0,0.12);
border-radius: 8px;
animation: slideDown 0.3s ease;
margin-top: 0.5rem;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.dropdown-item {
padding: 0.75rem 1.5rem;
transition: all 0.2s ease;
font-weight: 400;
}
.dropdown-item:hover,
.dropdown-item:focus {
background: linear-gradient(90deg, rgba(13, 110, 253, 0.1), rgba(13, 202, 240, 0.1));
color: #0d6efd;
transform: translateX(5px);
}
.dropdown-item.active {
background-color: rgba(13, 110, 253, 0.1);
color: #0d6efd;
}
/* Navbar Brand */
.navbar-brand {
font-weight: 700;
font-size: 1.5rem;
color: #1a1a1a;
transition: all 0.3s ease;
}
.navbar-brand:hover {
color: #0d6efd;
transform: scale(1.05);
}
/* Navbar Toggler (Hamburger) */
.navbar-toggler {
border: 2px solid rgba(0, 0, 0, 0.1);
padding: 0.5rem 0.75rem;
transition: all 0.3s ease;
}
.navbar-toggler:hover {
border-color: #0d6efd;
background-color: rgba(13, 110, 253, 0.05);
}
.navbar-toggler:focus {
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
}
/* Mobile Menu Styles */
@media (max-width: 991px) {
.navbar-collapse {
margin-top: 1rem;
padding: 1rem 0;
border-top: 1px solid rgba(0, 0, 0, 0.1);
}
.nav-link {
padding: 0.75rem 1rem !important;
}
.nav-link::after {
display: none;
}
.dropdown-menu {
border: none;
box-shadow: none;
animation: none;
background-color: rgba(0, 0, 0, 0.02);
margin-left: 1rem;
padding: 0.5rem 0;
}
.dropdown-item {
padding: 0.5rem 1rem;
}
.dropdown-item:hover {
transform: none;
}
}
/* ============================================
SKIP LINK (Accesibilidad)
============================================ */
.skip-link.screen-reader-text {
position: absolute;
left: -9999px;
top: 2.5em;
z-index: 100000;
padding: 1em 1.5em;
background-color: #0d6efd;
color: #fff;
text-decoration: none;
font-size: 0.875rem;
font-weight: 600;
border-radius: 4px;
}
.skip-link.screen-reader-text:focus {
left: 6px;
outline: 3px solid rgba(13, 110, 253, 0.5);
outline-offset: 2px;
}
/* ============================================
SITE CONTENT SPACING
============================================ */
.site-content {
margin-top: 2rem;
margin-bottom: 2rem;
}
@media (max-width: 767px) {
.site-content {
margin-top: 1rem;
margin-bottom: 1rem;
}
}
/* ============================================
WORDPRESS SPECIFIC CLASSES
============================================ */
/* WordPress Menu Classes */
.menu-item {
position: relative;
}
.menu-item-has-children > .nav-link {
padding-right: 1.5rem !important;
}
/* Submenu Indicator (si se usan íconos) */
.menu-item-has-children > .nav-link::before {
content: '';
position: absolute;
right: 0.5rem;
top: 50%;
transform: translateY(-50%);
width: 0;
height: 0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 5px solid currentColor;
opacity: 0.6;
}
/* ============================================
UTILITY CLASSES
============================================ */
/* Screen Reader Only Text */
.screen-reader-text {
border: 0;
clip: rect(1px, 1px, 1px, 1px);
clip-path: inset(50%);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
word-wrap: normal !important;
}
.screen-reader-text:focus {
background-color: #f1f1f1;
border-radius: 3px;
box-shadow: 0 0 2px 2px rgba(0, 0, 0, 0.6);
clip: auto !important;
clip-path: none;
color: #21759b;
display: block;
font-size: 0.875rem;
font-weight: 700;
height: auto;
left: 5px;
line-height: normal;
padding: 15px 23px 14px;
text-decoration: none;
top: 5px;
width: auto;
z-index: 100000;
}

View File

@@ -0,0 +1,144 @@
/**
* Main JavaScript - APUS Theme
*
* Funcionalidades principales del tema según template del cliente.
* Incluye: Navbar sticky scroll effect y animaciones.
*
* @package Apus_Theme
* @since 1.0.0
*/
(function() {
'use strict';
/**
* Navbar Scroll Effect
* Añade clase 'scrolled' al navbar cuando se hace scroll > 50px
*/
function initNavbarScrollEffect() {
const navbar = document.querySelector('.navbar');
if (!navbar) {
return;
}
// Optimización con throttle para mejor performance
let ticking = false;
function updateNavbar() {
if (window.scrollY > 50) {
navbar.classList.add('scrolled');
} else {
navbar.classList.remove('scrolled');
}
ticking = false;
}
window.addEventListener('scroll', function() {
if (!ticking) {
window.requestAnimationFrame(updateNavbar);
ticking = true;
}
});
// Ejecutar una vez al cargar por si la página ya tiene scroll
updateNavbar();
}
/**
* Highlight Active Menu Item
* Marca el item del menú correspondiente a la página actual
*/
function highlightActiveMenuItem() {
const currentUrl = window.location.href;
const navLinks = document.querySelectorAll('.navbar-nav .nav-link');
navLinks.forEach(function(link) {
// Remover active de todos
link.classList.remove('active');
// Agregar active si coincide URL
if (link.href === currentUrl) {
link.classList.add('active');
}
});
}
/**
* Mobile Menu Close on Link Click
* Cierra el menú móvil automáticamente al hacer click en un enlace
*/
function initMobileMenuAutoClose() {
const navbarToggler = document.querySelector('.navbar-toggler');
const navbarCollapse = document.querySelector('.navbar-collapse');
const navLinks = document.querySelectorAll('.navbar-nav .nav-link');
if (!navbarToggler || !navbarCollapse) {
return;
}
navLinks.forEach(function(link) {
link.addEventListener('click', function() {
// Solo en móvil (cuando el toggler es visible)
if (window.getComputedStyle(navbarToggler).display !== 'none') {
const bsCollapse = bootstrap.Collapse.getInstance(navbarCollapse);
if (bsCollapse) {
bsCollapse.hide();
}
}
});
});
}
/**
* Smooth Scroll for Anchor Links
* Scroll suave para enlaces ancla (#)
*/
function initSmoothScroll() {
const anchorLinks = document.querySelectorAll('a[href^="#"]');
anchorLinks.forEach(function(link) {
link.addEventListener('click', function(e) {
const targetId = this.getAttribute('href');
// Ignorar enlaces # vacíos o solo #
if (targetId === '#' || targetId === '') {
return;
}
const targetElement = document.querySelector(targetId);
if (targetElement) {
e.preventDefault();
// Offset por el navbar sticky
const navbarHeight = document.querySelector('.navbar').offsetHeight;
const targetPosition = targetElement.getBoundingClientRect().top + window.pageYOffset - navbarHeight - 20;
window.scrollTo({
top: targetPosition,
behavior: 'smooth'
});
}
});
});
}
/**
* Initialize all functions when DOM is ready
*/
function init() {
initNavbarScrollEffect();
highlightActiveMenuItem();
initMobileMenuAutoClose();
initSmoothScroll();
}
// DOM Ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();

View File

@@ -166,6 +166,11 @@ if (is_admin()) {
}
}
// Bootstrap Nav Walker
if (file_exists(get_template_directory() . '/inc/nav-walker.php')) {
require_once get_template_directory() . '/inc/nav-walker.php';
}
// Bootstrap and Script Enqueuing
if (file_exists(get_template_directory() . '/inc/enqueue-scripts.php')) {
require_once get_template_directory() . '/inc/enqueue-scripts.php';

View File

@@ -1,9 +1,9 @@
<?php
/**
* The header template file
* The header for our theme
*
* This template displays the site header including the opening HTML tags,
* the site navigation, and the header content.
* Navbar sticky con Bootstrap 5.3.2 y animaciones según template del cliente.
* Incluye: sticky positioning, gradient animations, responsive hamburger menu.
*
* @package Apus_Theme
* @since 1.0.0
@@ -22,111 +22,77 @@
<body <?php body_class(); ?>>
<?php wp_body_open(); ?>
<!-- Skip to main content link for accessibility -->
<!-- Skip to main content link para accesibilidad -->
<a class="skip-link screen-reader-text" href="#main-content">
<?php esc_html_e( 'Skip to content', 'apus-theme' ); ?>
</a>
<div id="page" class="site">
<!-- Sticky Header -->
<header id="masthead" class="site-header" role="banner">
<div class="header-inner container">
<!-- Navbar Sticky con Bootstrap 5 -->
<nav class="navbar navbar-expand-lg navbar-light bg-white py-3 border-bottom">
<div class="container">
<!-- Site Branding / Logo -->
<div class="site-branding">
<!-- Logo / Site Title -->
<a class="navbar-brand" href="<?php echo esc_url( home_url( '/' ) ); ?>">
<?php
if ( has_custom_logo() ) :
if ( has_custom_logo() ) {
the_custom_logo();
else :
} else {
?>
<div class="site-identity">
<?php if ( is_front_page() && is_home() ) : ?>
<h1 class="site-title">
<a href="<?php echo esc_url( home_url( '/' ) ); ?>" rel="home">
<?php bloginfo( 'name' ); ?>
</a>
</h1>
<?php else : ?>
<p class="site-title">
<a href="<?php echo esc_url( home_url( '/' ) ); ?>" rel="home">
<?php bloginfo( 'name' ); ?>
</a>
</p>
<?php endif; ?>
<?php
$description = get_bloginfo( 'description', 'display' );
if ( $description || is_customize_preview() ) :
?>
<p class="site-description"><?php echo esc_html( $description ); ?></p>
<?php endif; ?>
</div>
<b><?php bloginfo( 'name' ); ?></b>
<?php
endif;
}
?>
</div><!-- .site-branding -->
</a>
<!-- Desktop Navigation -->
<nav id="site-navigation" class="main-navigation desktop-nav" role="navigation" aria-label="<?php esc_attr_e( 'Primary Navigation', 'apus-theme' ); ?>">
<!-- Hamburger Toggle Button (Bootstrap 5) -->
<button class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent"
aria-expanded="false"
aria-label="<?php esc_attr_e( 'Toggle navigation', 'apus-theme' ); ?>">
<span class="navbar-toggler-icon"></span>
</button>
<!-- Collapsible Menu -->
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<?php
wp_nav_menu(
array(
'theme_location' => 'primary',
'menu_id' => 'primary-menu',
'menu_class' => 'primary-menu',
'container' => false,
'fallback_cb' => false,
)
);
if ( has_nav_menu( 'primary' ) ) {
wp_nav_menu(
array(
'theme_location' => 'primary',
'container' => false,
'menu_class' => 'navbar-nav ms-auto mb-2 mb-lg-0',
'fallback_cb' => false,
'depth' => 2,
'walker' => new WP_Bootstrap_Navwalker(),
)
);
} else {
// Fallback si no hay menú asignado
?>
<ul class="navbar-nav ms-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="<?php echo esc_url( home_url( '/' ) ); ?>">
<?php esc_html_e( 'Home', 'apus-theme' ); ?>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="<?php echo esc_url( get_post_type_archive_link( 'post' ) ); ?>">
<?php esc_html_e( 'Blog', 'apus-theme' ); ?>
</a>
</li>
</ul>
<?php
}
?>
</nav><!-- #site-navigation -->
</div>
<!-- Mobile Menu Toggle (Hamburger) -->
<button
id="mobile-menu-toggle"
class="mobile-menu-toggle"
aria-controls="mobile-menu"
aria-expanded="false"
aria-label="<?php esc_attr_e( 'Toggle mobile menu', 'apus-theme' ); ?>"
>
<span class="hamburger-icon" aria-hidden="true">
<span class="line"></span>
<span class="line"></span>
<span class="line"></span>
</span>
<span class="screen-reader-text"><?php esc_html_e( 'Menu', 'apus-theme' ); ?></span>
</button>
</div><!-- .header-inner -->
</header><!-- #masthead -->
<!-- Mobile Menu Overlay -->
<div id="mobile-menu-overlay" class="mobile-menu-overlay" aria-hidden="true"></div>
<!-- Mobile Navigation -->
<nav id="mobile-menu" class="mobile-menu" role="navigation" aria-label="<?php esc_attr_e( 'Mobile Navigation', 'apus-theme' ); ?>" aria-hidden="true">
<div class="mobile-menu-header">
<span class="mobile-menu-title"><?php esc_html_e( 'Menu', 'apus-theme' ); ?></span>
<button
id="mobile-menu-close"
class="mobile-menu-close"
aria-label="<?php esc_attr_e( 'Close menu', 'apus-theme' ); ?>"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<?php
wp_nav_menu(
array(
'theme_location' => 'primary',
'menu_id' => 'mobile-primary-menu',
'menu_class' => 'mobile-primary-menu',
'container' => false,
'fallback_cb' => false,
)
);
?>
</nav><!-- #mobile-menu -->
</div><!-- .container -->
</nav><!-- .navbar -->
<!-- Main Content Area -->
<div id="content" class="site-content">

View File

@@ -86,6 +86,34 @@ function apus_enqueue_header() {
add_action('wp_enqueue_scripts', 'apus_enqueue_header', 10);
/**
* Enqueue custom styles and main JavaScript
*/
function apus_enqueue_custom_assets() {
// Custom Styles - navbar animations and theme components
wp_enqueue_style(
'apus-custom-style',
get_template_directory_uri() . '/assets/css/custom-style.css',
array('apus-bootstrap'),
'1.0.0',
'all'
);
// Main JavaScript - navbar scroll effects and interactions
wp_enqueue_script(
'apus-main-js',
get_template_directory_uri() . '/assets/js/main.js',
array('apus-bootstrap-js'),
'1.0.0',
array(
'in_footer' => true,
'strategy' => 'defer',
)
);
}
add_action('wp_enqueue_scripts', 'apus_enqueue_custom_assets', 11);
/**
* Enqueue footer styles
*/

View File

@@ -0,0 +1,200 @@
<?php
/**
* Bootstrap 5 Nav Walker
*
* Custom Walker para wp_nav_menu() compatible con Bootstrap 5.
* Genera markup correcto para navbar, dropdowns y menús responsive.
*
* @package Apus_Theme
* @since 1.0.0
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
/**
* Class WP_Bootstrap_Navwalker
*
* Bootstrap 5 compatible navigation menu walker
*/
class WP_Bootstrap_Navwalker extends Walker_Nav_Menu {
/**
* Starts the list before the elements are added.
*
* @param string $output Used to append additional content (passed by reference).
* @param int $depth Depth of menu item. Used for padding.
* @param stdClass $args An object of wp_nav_menu() arguments.
*/
public function start_lvl(&$output, $depth = 0, $args = null) {
if (isset($args->item_spacing) && 'discard' === $args->item_spacing) {
$t = '';
$n = '';
} else {
$t = "\t";
$n = "\n";
}
$indent = str_repeat($t, $depth);
// Dropdown menu classes
$classes = array('dropdown-menu');
$class_names = join(' ', apply_filters('nav_menu_submenu_css_class', $classes, $args, $depth));
$class_names = $class_names ? ' class="' . esc_attr($class_names) . '"' : '';
$output .= "{$n}{$indent}<ul$class_names>{$n}";
}
/**
* Starts the element output.
*
* @param string $output Used to append additional content (passed by reference).
* @param WP_Post $item Menu item data object.
* @param int $depth Depth of menu item. Used for padding.
* @param stdClass $args An object of wp_nav_menu() arguments.
* @param int $id Current item ID.
*/
public function start_el(&$output, $item, $depth = 0, $args = null, $id = 0) {
if (isset($args->item_spacing) && 'discard' === $args->item_spacing) {
$t = '';
$n = '';
} else {
$t = "\t";
$n = "\n";
}
$indent = ($depth) ? str_repeat($t, $depth) : '';
$classes = empty($item->classes) ? array() : (array) $item->classes;
$classes[] = 'menu-item-' . $item->ID;
// Add Bootstrap classes based on depth
if ($depth === 0) {
$classes[] = 'nav-item';
}
// Check if menu item has children
$has_children = in_array('menu-item-has-children', $classes);
if ($has_children && $depth === 0) {
$classes[] = 'dropdown';
}
$class_names = join(' ', apply_filters('nav_menu_css_class', array_filter($classes), $item, $args, $depth));
$class_names = $class_names ? ' class="' . esc_attr($class_names) . '"' : '';
$id = apply_filters('nav_menu_item_id', 'menu-item-' . $item->ID, $item, $args, $depth);
$id = $id ? ' id="' . esc_attr($id) . '"' : '';
// Output <li>
if ($depth === 0) {
$output .= $indent . '<li' . $id . $class_names . '>';
} else {
$output .= $indent . '<li' . $class_names . '>';
}
// Link attributes
$atts = array();
$atts['title'] = !empty($item->attr_title) ? $item->attr_title : '';
$atts['target'] = !empty($item->target) ? $item->target : '';
$atts['rel'] = !empty($item->xfn) ? $item->xfn : '';
$atts['href'] = !empty($item->url) ? $item->url : '';
// Add Bootstrap nav-link class for depth 0
if ($depth === 0) {
$atts['class'] = 'nav-link';
} else {
$atts['class'] = 'dropdown-item';
}
// Add dropdown-toggle class and attributes for parent items
if ($has_children && $depth === 0) {
$atts['class'] .= ' dropdown-toggle';
$atts['data-bs-toggle'] = 'dropdown';
$atts['aria-expanded'] = 'false';
$atts['role'] = 'button';
}
// Add active class for current menu item
if (in_array('current-menu-item', $classes) || in_array('current-menu-parent', $classes)) {
$atts['class'] .= ' active';
$atts['aria-current'] = 'page';
}
$atts = apply_filters('nav_menu_link_attributes', $atts, $item, $args, $depth);
$attributes = '';
foreach ($atts as $attr => $value) {
if (!empty($value)) {
$value = ('href' === $attr) ? esc_url($value) : esc_attr($value);
$attributes .= ' ' . $attr . '="' . $value . '"';
}
}
$title = apply_filters('the_title', $item->title, $item->ID);
$title = apply_filters('nav_menu_item_title', $title, $item, $args, $depth);
// Build the link
$item_output = $args->before;
$item_output .= '<a' . $attributes . '>';
$item_output .= $args->link_before . $title . $args->link_after;
$item_output .= '</a>';
$item_output .= $args->after;
$output .= apply_filters('walker_nav_menu_start_el', $item_output, $item, $depth, $args);
}
/**
* Traverse elements to create list from elements.
*
* Display one element if the element doesn't have any children otherwise,
* display the element and its children. Will only traverse up to the max
* depth and no ignore elements under that depth. It is possible to set the
* max depth to include all depths, see walk() method.
*
* This method should not be called directly, use the walk() method instead.
*
* @param object $element Data object.
* @param array $children_elements List of elements to continue traversing (passed by reference).
* @param int $max_depth Max depth to traverse.
* @param int $depth Depth of current element.
* @param array $args An array of arguments.
* @param string $output Used to append additional content (passed by reference).
*/
public function display_element($element, &$children_elements, $max_depth, $depth, $args, &$output) {
if (!$element) {
return;
}
$id_field = $this->db_fields['id'];
// Display this element
if (is_object($args[0])) {
$args[0]->has_children = !empty($children_elements[$element->$id_field]);
}
parent::display_element($element, $children_elements, $max_depth, $depth, $args, $output);
}
/**
* Menu Fallback
*
* If this function is assigned to the wp_nav_menu's fallback_cb option
* and a menu has not been assigned to the theme location in the WordPress
* menu manager the function will display a basic menu of all published pages.
*
* @param array $args passed from the wp_nav_menu function.
*/
public static function fallback($args) {
if (current_user_can('edit_theme_options')) {
echo '<ul class="' . esc_attr($args['menu_class']) . '">';
echo '<li class="nav-item">';
echo '<a class="nav-link" href="' . esc_url(admin_url('nav-menus.php')) . '">';
esc_html_e('Crear un menú', 'apus-theme');
echo '</a>';
echo '</li>';
echo '</ul>';
}
}
}