Agregar estructura completa del tema APUS con Bootstrap 5 y optimizaciones de rendimiento

Se implementa tema WordPress personalizado para Análisis de Precios Unitarios con funcionalidades avanzadas:
- Sistema de templates (front-page, single, archive, page, 404, search)
- Integración de Bootstrap 5.3.8 con estructura modular de assets
- Panel de opciones del tema con Customizer API
- Optimizaciones de rendimiento (Critical CSS, Image Optimization, Performance)
- Funcionalidades SEO y compatibilidad con Rank Math
- Sistema de posts relacionados y tabla de contenidos
- Badge de categorías y manejo de AdSense diferido
- Tipografías Google Fonts configurables
- Documentación completa del tema y guías de uso

🤖 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 09:31:47 -06:00
parent 12285bec3c
commit 7ba9080f57
67 changed files with 21825 additions and 0 deletions

View File

@@ -0,0 +1,216 @@
/**
* AdSense Delayed Loader
*
* This script delays the loading of Google AdSense until user interaction
* or a timeout occurs, improving initial page load performance.
*
* @package Apus_Theme
* @since 1.0.0
*/
(function() {
'use strict';
// Configuration
const CONFIG = {
timeout: 5000, // Fallback timeout in milliseconds
loadedClass: 'adsense-loaded',
debug: false // Set to true for console logging
};
// State
let adsenseLoaded = false;
let loadTimeout = null;
/**
* Log debug messages if debug mode is enabled
* @param {string} message - The message to log
*/
function debugLog(message) {
if (CONFIG.debug && typeof console !== 'undefined') {
console.log('[AdSense Loader] ' + message);
}
}
/**
* Load AdSense scripts and initialize ads
*/
function loadAdSense() {
// Prevent multiple loads
if (adsenseLoaded) {
debugLog('AdSense already loaded, skipping...');
return;
}
adsenseLoaded = true;
debugLog('Loading AdSense scripts...');
// Clear the timeout if it exists
if (loadTimeout) {
clearTimeout(loadTimeout);
loadTimeout = null;
}
// Remove event listeners to prevent multiple triggers
removeEventListeners();
// Load AdSense script tags
loadAdSenseScripts();
// Execute AdSense push scripts
executeAdSensePushScripts();
// Add loaded class to body
document.body.classList.add(CONFIG.loadedClass);
debugLog('AdSense loading complete');
}
/**
* Find and load all delayed AdSense script tags
*/
function loadAdSenseScripts() {
const delayedScripts = document.querySelectorAll('script[data-adsense-script]');
if (delayedScripts.length === 0) {
debugLog('No delayed AdSense scripts found');
return;
}
debugLog('Found ' + delayedScripts.length + ' delayed AdSense script(s)');
delayedScripts.forEach(function(oldScript) {
const newScript = document.createElement('script');
// Copy attributes
if (oldScript.src) {
newScript.src = oldScript.src;
}
// Set async attribute
newScript.async = true;
// Copy crossorigin if present
if (oldScript.getAttribute('crossorigin')) {
newScript.crossorigin = oldScript.getAttribute('crossorigin');
}
// Replace old script with new one
oldScript.parentNode.replaceChild(newScript, oldScript);
});
}
/**
* Execute delayed AdSense push scripts
*/
function executeAdSensePushScripts() {
const delayedPushScripts = document.querySelectorAll('script[data-adsense-push]');
if (delayedPushScripts.length === 0) {
debugLog('No delayed AdSense push scripts found');
return;
}
debugLog('Found ' + delayedPushScripts.length + ' delayed push script(s)');
// Initialize adsbygoogle array if it doesn't exist
window.adsbygoogle = window.adsbygoogle || [];
delayedPushScripts.forEach(function(oldScript) {
const scriptContent = oldScript.innerHTML;
// Create and execute new script
const newScript = document.createElement('script');
newScript.innerHTML = scriptContent;
newScript.type = 'text/javascript';
// Replace old script with new one
oldScript.parentNode.replaceChild(newScript, oldScript);
});
}
/**
* Event handler for user interactions
*/
function handleUserInteraction() {
debugLog('User interaction detected');
loadAdSense();
}
/**
* Remove all event listeners
*/
function removeEventListeners() {
window.removeEventListener('scroll', handleUserInteraction, { passive: true });
window.removeEventListener('mousemove', handleUserInteraction, { passive: true });
window.removeEventListener('touchstart', handleUserInteraction, { passive: true });
window.removeEventListener('click', handleUserInteraction, { passive: true });
window.removeEventListener('keydown', handleUserInteraction, { passive: true });
}
/**
* Add event listeners for user interactions
*/
function addEventListeners() {
debugLog('Adding event listeners for user interaction');
// Scroll event - load on first scroll
window.addEventListener('scroll', handleUserInteraction, { passive: true, once: true });
// Mouse movement - load when user moves mouse
window.addEventListener('mousemove', handleUserInteraction, { passive: true, once: true });
// Touch events - load on first touch (mobile)
window.addEventListener('touchstart', handleUserInteraction, { passive: true, once: true });
// Click events - load on first click
window.addEventListener('click', handleUserInteraction, { passive: true, once: true });
// Keyboard events - load on first key press
window.addEventListener('keydown', handleUserInteraction, { passive: true, once: true });
}
/**
* Set timeout fallback to load AdSense after specified time
*/
function setTimeoutFallback() {
debugLog('Setting timeout fallback (' + CONFIG.timeout + 'ms)');
loadTimeout = setTimeout(function() {
debugLog('Timeout reached, loading AdSense');
loadAdSense();
}, CONFIG.timeout);
}
/**
* Initialize the AdSense delayed loader
*/
function init() {
// Check if AdSense delay is enabled
if (!window.apusAdsenseDelayed) {
debugLog('AdSense delay not enabled');
return;
}
debugLog('Initializing AdSense delayed loader');
// Check if page is already interactive or complete
if (document.readyState === 'interactive' || document.readyState === 'complete') {
debugLog('Page already loaded, starting listeners');
addEventListeners();
setTimeoutFallback();
} else {
// Wait for DOM to be ready
debugLog('Waiting for DOMContentLoaded');
document.addEventListener('DOMContentLoaded', function() {
debugLog('DOMContentLoaded fired');
addEventListeners();
setTimeoutFallback();
});
}
}
// Start initialization
init();
})();

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,342 @@
/**
* Header Navigation JavaScript
*
* This file handles:
* - Mobile hamburger menu toggle
* - Sticky header behavior
* - Smooth scroll to anchors (optional)
* - Accessibility features (keyboard navigation, ARIA attributes)
* - Body scroll locking when mobile menu is open
*
* @package Apus_Theme
* @since 1.0.0
*/
(function() {
'use strict';
/**
* Initialize on DOM ready
*/
function init() {
setupMobileMenu();
setupStickyHeader();
setupSmoothScroll();
setupKeyboardNavigation();
}
/**
* Mobile Menu Functionality
*/
function setupMobileMenu() {
const mobileMenuToggle = document.getElementById('mobile-menu-toggle');
const mobileMenu = document.getElementById('mobile-menu');
const mobileMenuOverlay = document.getElementById('mobile-menu-overlay');
const mobileMenuClose = document.getElementById('mobile-menu-close');
if (!mobileMenuToggle || !mobileMenu || !mobileMenuOverlay) {
return;
}
// Open mobile menu
mobileMenuToggle.addEventListener('click', function() {
openMobileMenu();
});
// Close mobile menu via close button
if (mobileMenuClose) {
mobileMenuClose.addEventListener('click', function() {
closeMobileMenu();
});
}
// Close mobile menu via overlay click
mobileMenuOverlay.addEventListener('click', function() {
closeMobileMenu();
});
// Close mobile menu on Escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && mobileMenu.classList.contains('active')) {
closeMobileMenu();
mobileMenuToggle.focus();
}
});
// Close mobile menu when clicking a menu link
const mobileMenuLinks = mobileMenu.querySelectorAll('a');
mobileMenuLinks.forEach(function(link) {
link.addEventListener('click', function() {
closeMobileMenu();
});
});
// Handle window resize - close mobile menu if switching to desktop
let resizeTimer;
window.addEventListener('resize', function() {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(function() {
if (window.innerWidth >= 768 && mobileMenu.classList.contains('active')) {
closeMobileMenu();
}
}, 250);
});
}
/**
* Open mobile menu
*/
function openMobileMenu() {
const mobileMenuToggle = document.getElementById('mobile-menu-toggle');
const mobileMenu = document.getElementById('mobile-menu');
const mobileMenuOverlay = document.getElementById('mobile-menu-overlay');
// Add active classes
mobileMenu.classList.add('active');
mobileMenuOverlay.classList.add('active');
document.body.classList.add('mobile-menu-open');
// Update ARIA attributes
mobileMenuToggle.setAttribute('aria-expanded', 'true');
mobileMenu.setAttribute('aria-hidden', 'false');
mobileMenuOverlay.setAttribute('aria-hidden', 'false');
// Focus trap - focus first menu item
const firstMenuItem = mobileMenu.querySelector('a');
if (firstMenuItem) {
setTimeout(function() {
firstMenuItem.focus();
}, 300);
}
}
/**
* Close mobile menu
*/
function closeMobileMenu() {
const mobileMenuToggle = document.getElementById('mobile-menu-toggle');
const mobileMenu = document.getElementById('mobile-menu');
const mobileMenuOverlay = document.getElementById('mobile-menu-overlay');
// Remove active classes
mobileMenu.classList.remove('active');
mobileMenuOverlay.classList.remove('active');
document.body.classList.remove('mobile-menu-open');
// Update ARIA attributes
mobileMenuToggle.setAttribute('aria-expanded', 'false');
mobileMenu.setAttribute('aria-hidden', 'true');
mobileMenuOverlay.setAttribute('aria-hidden', 'true');
}
/**
* Sticky Header Behavior
*/
function setupStickyHeader() {
const header = document.getElementById('masthead');
if (!header) {
return;
}
let lastScrollTop = 0;
let scrollThreshold = 100;
window.addEventListener('scroll', function() {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
// Add/remove scrolled class based on scroll position
if (scrollTop > scrollThreshold) {
header.classList.add('scrolled');
} else {
header.classList.remove('scrolled');
}
lastScrollTop = scrollTop;
}, { passive: true });
}
/**
* Smooth Scroll to Anchors (Optional)
*/
function setupSmoothScroll() {
// Check if user prefers reduced motion
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (prefersReducedMotion) {
return;
}
// Get all anchor links
const anchorLinks = document.querySelectorAll('a[href^="#"]');
anchorLinks.forEach(function(link) {
link.addEventListener('click', function(e) {
const href = this.getAttribute('href');
// Skip if href is just "#"
if (href === '#') {
return;
}
const target = document.querySelector(href);
if (target) {
e.preventDefault();
// Get header height for offset
const header = document.getElementById('masthead');
const headerHeight = header ? header.offsetHeight : 0;
const targetPosition = target.getBoundingClientRect().top + window.pageYOffset - headerHeight - 20;
window.scrollTo({
top: targetPosition,
behavior: 'smooth'
});
// Update URL hash
if (history.pushState) {
history.pushState(null, null, href);
}
// Focus target element for accessibility
target.setAttribute('tabindex', '-1');
target.focus();
}
});
});
}
/**
* Keyboard Navigation for Menus
*/
function setupKeyboardNavigation() {
const menuItems = document.querySelectorAll('.primary-menu > li, .mobile-primary-menu > li');
menuItems.forEach(function(item) {
const link = item.querySelector('a');
const submenu = item.querySelector('.sub-menu');
if (!link || !submenu) {
return;
}
// Open submenu on Enter/Space
link.addEventListener('keydown', function(e) {
if (e.key === 'Enter' || e.key === ' ') {
if (submenu) {
e.preventDefault();
toggleSubmenu(item, submenu);
}
}
// Close submenu on Escape
if (e.key === 'Escape') {
closeSubmenu(item, submenu);
link.focus();
}
});
// Close submenu when focus leaves
const submenuLinks = submenu.querySelectorAll('a');
if (submenuLinks.length > 0) {
const lastSubmenuLink = submenuLinks[submenuLinks.length - 1];
lastSubmenuLink.addEventListener('keydown', function(e) {
if (e.key === 'Tab' && !e.shiftKey) {
closeSubmenu(item, submenu);
}
});
}
});
}
/**
* Toggle submenu visibility
*/
function toggleSubmenu(item, submenu) {
const isExpanded = item.classList.contains('submenu-open');
if (isExpanded) {
closeSubmenu(item, submenu);
} else {
openSubmenu(item, submenu);
}
}
/**
* Open submenu
*/
function openSubmenu(item, submenu) {
item.classList.add('submenu-open');
submenu.setAttribute('aria-hidden', 'false');
const firstLink = submenu.querySelector('a');
if (firstLink) {
firstLink.focus();
}
}
/**
* Close submenu
*/
function closeSubmenu(item, submenu) {
item.classList.remove('submenu-open');
submenu.setAttribute('aria-hidden', 'true');
}
/**
* Trap focus within mobile menu when open
*/
function setupFocusTrap() {
const mobileMenu = document.getElementById('mobile-menu');
if (!mobileMenu) {
return;
}
document.addEventListener('keydown', function(e) {
if (!mobileMenu.classList.contains('active')) {
return;
}
if (e.key === 'Tab') {
const focusableElements = mobileMenu.querySelectorAll(
'a, button, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
if (e.shiftKey) {
// Shift + Tab
if (document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
}
} else {
// Tab
if (document.activeElement === lastElement) {
e.preventDefault();
firstElement.focus();
}
}
}
});
}
/**
* Initialize focus trap
*/
setupFocusTrap();
/**
* Initialize when DOM is ready
*/
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();

View File

@@ -0,0 +1,216 @@
/**
* Table of Contents JavaScript
*
* Provides smooth scrolling and active link highlighting for the TOC.
* Pure vanilla JavaScript - no jQuery dependency.
*
* @package Apus_Theme
* @since 1.0.0
*/
(function() {
'use strict';
/**
* Initialize TOC functionality when DOM is ready
*/
function initTOC() {
const toc = document.querySelector('.apus-toc');
if (!toc) {
return; // No TOC on this page
}
initToggleButton();
initSmoothScroll();
initActiveHighlight();
}
/**
* Initialize toggle button functionality
*/
function initToggleButton() {
const toggleButton = document.querySelector('.apus-toc-toggle');
const tocList = document.querySelector('.apus-toc-list');
if (!toggleButton || !tocList) {
return;
}
toggleButton.addEventListener('click', function() {
const isExpanded = this.getAttribute('aria-expanded') === 'true';
this.setAttribute('aria-expanded', !isExpanded);
// Save state to localStorage
try {
localStorage.setItem('apus-toc-collapsed', isExpanded ? 'true' : 'false');
} catch (e) {
// localStorage might not be available
}
});
// Restore saved state
try {
const isCollapsed = localStorage.getItem('apus-toc-collapsed') === 'true';
if (isCollapsed) {
toggleButton.setAttribute('aria-expanded', 'false');
}
} catch (e) {
// localStorage might not be available
}
}
/**
* Initialize smooth scrolling for TOC links
*/
function initSmoothScroll() {
const tocLinks = document.querySelectorAll('.apus-toc-link');
tocLinks.forEach(function(link) {
link.addEventListener('click', function(e) {
e.preventDefault();
const targetId = this.getAttribute('href').substring(1);
const targetElement = document.getElementById(targetId);
if (!targetElement) {
return;
}
// Smooth scroll to target
targetElement.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
// Update URL without jumping
if (history.pushState) {
history.pushState(null, null, '#' + targetId);
} else {
window.location.hash = targetId;
}
// Update active state
updateActiveLink(this);
// Focus the target heading for accessibility
targetElement.setAttribute('tabindex', '-1');
targetElement.focus();
});
});
}
/**
* Initialize active link highlighting based on scroll position
*/
function initActiveHighlight() {
const tocLinks = document.querySelectorAll('.apus-toc-link');
const headings = Array.from(document.querySelectorAll('h2[id], h3[id]'));
if (headings.length === 0) {
return;
}
let ticking = false;
// Debounced scroll handler
function onScroll() {
if (!ticking) {
window.requestAnimationFrame(function() {
updateActiveOnScroll(headings, tocLinks);
ticking = false;
});
ticking = true;
}
}
window.addEventListener('scroll', onScroll, { passive: true });
// Initial update
updateActiveOnScroll(headings, tocLinks);
}
/**
* Update active link based on scroll position
*
* @param {Array} headings Array of heading elements
* @param {NodeList} tocLinks TOC link elements
*/
function updateActiveOnScroll(headings, tocLinks) {
const scrollPosition = window.scrollY + 100; // Offset for better UX
// Find the current heading
let currentHeading = null;
for (let i = headings.length - 1; i >= 0; i--) {
if (headings[i].offsetTop <= scrollPosition) {
currentHeading = headings[i];
break;
}
}
// If we're at the top, use the first heading
if (!currentHeading && scrollPosition < headings[0].offsetTop) {
currentHeading = headings[0];
}
// Update active class
tocLinks.forEach(function(link) {
if (currentHeading && link.getAttribute('href') === '#' + currentHeading.id) {
link.classList.add('active');
} else {
link.classList.remove('active');
}
});
}
/**
* Update active link when clicked
*
* @param {Element} clickedLink The clicked TOC link
*/
function updateActiveLink(clickedLink) {
const tocLinks = document.querySelectorAll('.apus-toc-link');
tocLinks.forEach(function(link) {
link.classList.remove('active');
});
clickedLink.classList.add('active');
}
/**
* Handle hash navigation on page load
*/
function handleHashOnLoad() {
if (!window.location.hash) {
return;
}
const targetId = window.location.hash.substring(1);
const targetElement = document.getElementById(targetId);
const tocLink = document.querySelector('.apus-toc-link[href="#' + targetId + '"]');
if (targetElement && tocLink) {
// Small delay to ensure page is fully loaded
setTimeout(function() {
targetElement.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
updateActiveLink(tocLink);
}, 100);
}
}
/**
* Initialize when DOM is ready
*/
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
initTOC();
handleHashOnLoad();
});
} else {
// DOM is already ready
initTOC();
handleHashOnLoad();
}
})();