app.js

I’ll build this as a strong RX Theme front-end inc/js/app.js file: modular, safe, WordPress-friendly, and full of optional features that only run when matching HTML exists.

My dear friend, create this file:

rx-theme/inc/js/app.js

Then enqueue it from functions.php using your theme script handle. This app.js is advanced, but safe: every feature checks whether the required HTML exists before running.

/*!
 * RX Theme - Main App JavaScript
 * File: inc/js/app.js
 * Theme: RX Theme
 * Author: RxHarun
 *
 * Purpose:
 * - Mobile menu
 * - Sticky header
 * - Scroll progress
 * - Back to top
 * - Dark mode
 * - Search overlay
 * - Dropdown menu accessibility
 * - Table of contents
 * - Lazy loading helpers
 * - Copy code button
 * - Accordion
 * - Tabs
 * - Modal
 * - Notice dismiss
 * - FAQ schema UI helpers
 * - Reading time
 * - Smooth scroll
 * - External link handling
 * - Form validation
 * - Comment form helpers
 * - AJAX-ready helpers
 */

(function () {
  'use strict';

  /**
   * RX Theme global namespace.
   */
  const RX = {
    version: '1.0.0',
    body: document.body,
    html: document.documentElement,
    win: window,
    doc: document,

    settings: {
      activeClass: 'is-active',
      openClass: 'is-open',
      hiddenClass: 'is-hidden',
      loadedClass: 'is-loaded',
      fixedClass: 'is-fixed',
      stickyClass: 'is-sticky',
      darkClass: 'rx-dark-mode',
      noScrollClass: 'rx-no-scroll',
      focusClass: 'rx-focus-visible',
      animationClass: 'rx-animate-in',
      reducedMotion: window.matchMedia('(prefers-reduced-motion: reduce)').matches
    },

    selectors: {
      siteHeader: '.rx-site-header',
      mobileToggle: '.rx-menu-toggle',
      mobileMenu: '.rx-mobile-menu',
      mainNavigation: '.rx-main-navigation',
      dropdownToggle: '.menu-item-has-children > a, .rx-dropdown-toggle',
      searchToggle: '.rx-search-toggle',
      searchOverlay: '.rx-search-overlay',
      searchClose: '.rx-search-close',
      darkToggle: '.rx-dark-toggle',
      scrollTop: '.rx-scroll-top',
      progressBar: '.rx-reading-progress',
      toc: '.rx-table-of-contents',
      content: '.entry-content, .rx-content, article',
      lazy: '[data-rx-lazy]',
      accordion: '[data-rx-accordion]',
      accordionTrigger: '[data-rx-accordion-trigger]',
      tabs: '[data-rx-tabs]',
      tabButton: '[data-rx-tab-button]',
      tabPanel: '[data-rx-tab-panel]',
      modalTrigger: '[data-rx-modal-open]',
      modalClose: '[data-rx-modal-close]',
      modal: '[data-rx-modal]',
      dismiss: '[data-rx-dismiss]',
      copyCode: 'pre code',
      readingTime: '[data-rx-reading-time]',
      smoothLink: 'a[href^="#"]',
      externalLinks: 'a[href^="http"]',
      form: 'form[data-rx-validate]',
      commentForm: '#commentform'
    }
  };

  /**
   * Utility helpers
   */
  RX.utils = {
    qs(selector, scope = document) {
      return scope.querySelector(selector);
    },

    qsa(selector, scope = document) {
      return Array.prototype.slice.call(scope.querySelectorAll(selector));
    },

    exists(selector, scope = document) {
      return !!scope.querySelector(selector);
    },

    on(element, event, handler, options) {
      if (!element) return;
      element.addEventListener(event, handler, options || false);
    },

    off(element, event, handler, options) {
      if (!element) return;
      element.removeEventListener(event, handler, options || false);
    },

    addClass(element, className) {
      if (element && className) element.classList.add(className);
    },

    removeClass(element, className) {
      if (element && className) element.classList.remove(className);
    },

    toggleClass(element, className, force) {
      if (element && className) element.classList.toggle(className, force);
    },

    hasClass(element, className) {
      return element && element.classList.contains(className);
    },

    attr(element, name, value) {
      if (!element) return null;
      if (typeof value === 'undefined') return element.getAttribute(name);
      element.setAttribute(name, value);
      return value;
    },

    removeAttr(element, name) {
      if (element) element.removeAttribute(name);
    },

    debounce(fn, delay = 200) {
      let timer;
      return function debounced(...args) {
        clearTimeout(timer);
        timer = setTimeout(() => fn.apply(this, args), delay);
      };
    },

    throttle(fn, limit = 100) {
      let waiting = false;
      return function throttled(...args) {
        if (!waiting) {
          fn.apply(this, args);
          waiting = true;
          setTimeout(() => {
            waiting = false;
          }, limit);
        }
      };
    },

    ready(callback) {
      if (document.readyState !== 'loading') {
        callback();
      } else {
        document.addEventListener('DOMContentLoaded', callback);
      }
    },

    isVisible(element) {
      if (!element) return false;
      return !!(
        element.offsetWidth ||
        element.offsetHeight ||
        element.getClientRects().length
      );
    },

    getFocusable(container) {
      if (!container) return [];

      const selectors = [
        'a[href]',
        'button:not([disabled])',
        'textarea:not([disabled])',
        'input:not([disabled])',
        'select:not([disabled])',
        '[tabindex]:not([tabindex="-1"])'
      ];

      return RX.utils.qsa(selectors.join(','), container).filter((el) => {
        return RX.utils.isVisible(el);
      });
    },

    lockScroll() {
      RX.body.classList.add(RX.settings.noScrollClass);
    },

    unlockScroll() {
      RX.body.classList.remove(RX.settings.noScrollClass);
    },

    storage: {
      get(key, fallback = null) {
        try {
          const value = localStorage.getItem(key);
          return value === null ? fallback : value;
        } catch (error) {
          return fallback;
        }
      },

      set(key, value) {
        try {
          localStorage.setItem(key, value);
        } catch (error) {
          return false;
        }
        return true;
      },

      remove(key) {
        try {
          localStorage.removeItem(key);
        } catch (error) {
          return false;
        }
        return true;
      }
    },

    createId(prefix = 'rx-id') {
      return `${prefix}-${Math.random().toString(36).slice(2, 10)}`;
    },

    escapeHTML(value) {
      const div = document.createElement('div');
      div.textContent = value;
      return div.innerHTML;
    }
  };

  /**
   * Mark JS active.
   */
  RX.jsEnabled = function () {
    RX.html.classList.remove('no-js');
    RX.html.classList.add('js');
    RX.body.classList.add('rx-js-ready');
  };

  /**
   * Mobile menu.
   *
   * Expected HTML:
   * <button class="rx-menu-toggle" aria-controls="rx-mobile-menu" aria-expanded="false">Menu</button>
   * <nav id="rx-mobile-menu" class="rx-mobile-menu"></nav>
   */
  RX.mobileMenu = function () {
    const toggle = RX.utils.qs(RX.selectors.mobileToggle);
    const menu = RX.utils.qs(RX.selectors.mobileMenu);

    if (!toggle || !menu) return;

    const openMenu = () => {
      toggle.setAttribute('aria-expanded', 'true');
      menu.classList.add(RX.settings.openClass);
      RX.body.classList.add('rx-menu-open');
      RX.utils.lockScroll();
    };

    const closeMenu = () => {
      toggle.setAttribute('aria-expanded', 'false');
      menu.classList.remove(RX.settings.openClass);
      RX.body.classList.remove('rx-menu-open');
      RX.utils.unlockScroll();
    };

    const toggleMenu = () => {
      const expanded = toggle.getAttribute('aria-expanded') === 'true';
      expanded ? closeMenu() : openMenu();
    };

    RX.utils.on(toggle, 'click', toggleMenu);

    RX.utils.on(document, 'keydown', function (event) {
      if (event.key === 'Escape') {
        closeMenu();
      }
    });

    RX.utils.on(document, 'click', function (event) {
      const isInside = menu.contains(event.target) || toggle.contains(event.target);
      if (!isInside && menu.classList.contains(RX.settings.openClass)) {
        closeMenu();
      }
    });

    RX.utils.on(window, 'resize', RX.utils.debounce(function () {
      if (window.innerWidth > 1024) {
        closeMenu();
      }
    }, 150));
  };

  /**
   * Dropdown menu accessibility.
   */
  RX.dropdownMenu = function () {
    const dropdownLinks = RX.utils.qsa(RX.selectors.dropdownToggle);

    if (!dropdownLinks.length) return;

    dropdownLinks.forEach((link) => {
      const parent = link.parentElement;
      const submenu = parent ? parent.querySelector('ul, .sub-menu') : null;

      if (!parent || !submenu) return;

      if (!link.id) {
        link.id = RX.utils.createId('rx-dropdown-link');
      }

      const button = document.createElement('button');
      button.className = 'rx-submenu-toggle';
      button.setAttribute('aria-expanded', 'false');
      button.setAttribute('aria-controls', submenu.id || RX.utils.createId('rx-submenu'));
      button.setAttribute('aria-label', 'Toggle submenu');
      button.innerHTML = '<span aria-hidden="true">+</span>';

      if (!submenu.id) {
        submenu.id = button.getAttribute('aria-controls');
      }

      parent.insertBefore(button, submenu);

      RX.utils.on(button, 'click', function () {
        const expanded = button.getAttribute('aria-expanded') === 'true';

        button.setAttribute('aria-expanded', String(!expanded));
        parent.classList.toggle(RX.settings.openClass, !expanded);
        submenu.classList.toggle(RX.settings.openClass, !expanded);
      });

      RX.utils.on(parent, 'mouseenter', function () {
        if (window.innerWidth > 1024) {
          parent.classList.add(RX.settings.openClass);
        }
      });

      RX.utils.on(parent, 'mouseleave', function () {
        if (window.innerWidth > 1024) {
          parent.classList.remove(RX.settings.openClass);
        }
      });
    });

    RX.utils.on(document, 'keydown', function (event) {
      if (event.key !== 'Escape') return;

      RX.utils.qsa('.menu-item-has-children.is-open').forEach((item) => {
        item.classList.remove(RX.settings.openClass);
        const button = item.querySelector('.rx-submenu-toggle');
        const submenu = item.querySelector('.sub-menu, ul');

        if (button) button.setAttribute('aria-expanded', 'false');
        if (submenu) submenu.classList.remove(RX.settings.openClass);
      });
    });
  };

  /**
   * Sticky header.
   */
  RX.stickyHeader = function () {
    const header = RX.utils.qs(RX.selectors.siteHeader);

    if (!header) return;

    let lastScroll = window.scrollY;

    const updateHeader = RX.utils.throttle(function () {
      const currentScroll = window.scrollY;

      header.classList.toggle(RX.settings.stickyClass, currentScroll > 20);
      header.classList.toggle('rx-scroll-down', currentScroll > lastScroll && currentScroll > 120);
      header.classList.toggle('rx-scroll-up', currentScroll < lastScroll);

      lastScroll = currentScroll <= 0 ? 0 : currentScroll;
    }, 80);

    updateHeader();
    RX.utils.on(window, 'scroll', updateHeader, { passive: true });
  };

  /**
   * Scroll reading progress bar.
   *
   * Expected:
   * <div class="rx-reading-progress"></div>
   */
  RX.readingProgress = function () {
    const bar = RX.utils.qs(RX.selectors.progressBar);

    if (!bar) return;

    const updateProgress = RX.utils.throttle(function () {
      const scrollTop = window.scrollY;
      const docHeight = document.documentElement.scrollHeight - window.innerHeight;
      const progress = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0;

      bar.style.width = `${Math.min(100, Math.max(0, progress))}%`;
      bar.setAttribute('aria-valuenow', String(Math.round(progress)));
    }, 50);

    bar.setAttribute('role', 'progressbar');
    bar.setAttribute('aria-valuemin', '0');
    bar.setAttribute('aria-valuemax', '100');

    updateProgress();
    RX.utils.on(window, 'scroll', updateProgress, { passive: true });
    RX.utils.on(window, 'resize', RX.utils.debounce(updateProgress, 150));
  };

  /**
   * Back to top button.
   *
   * Expected:
   * <button class="rx-scroll-top">Top</button>
   */
  RX.scrollTop = function () {
    const button = RX.utils.qs(RX.selectors.scrollTop);

    if (!button) return;

    const toggleButton = RX.utils.throttle(function () {
      button.classList.toggle(RX.settings.activeClass, window.scrollY > 400);
    }, 100);

    RX.utils.on(button, 'click', function () {
      window.scrollTo({
        top: 0,
        behavior: RX.settings.reducedMotion ? 'auto' : 'smooth'
      });
    });

    toggleButton();
    RX.utils.on(window, 'scroll', toggleButton, { passive: true });
  };

  /**
   * Dark mode.
   *
   * Expected:
   * <button class="rx-dark-toggle">Dark</button>
   */
  RX.darkMode = function () {
    const toggle = RX.utils.qs(RX.selectors.darkToggle);
    const storageKey = 'rx-theme-color-mode';

    const getSystemMode = () => {
      return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
    };

    const applyMode = (mode) => {
      const finalMode = mode === 'system' ? getSystemMode() : mode;

      RX.html.classList.toggle(RX.settings.darkClass, finalMode === 'dark');
      RX.html.setAttribute('data-rx-theme', finalMode);

      if (toggle) {
        toggle.setAttribute('aria-pressed', String(finalMode === 'dark'));
        toggle.setAttribute('data-current-mode', finalMode);
      }
    };

    const savedMode = RX.utils.storage.get(storageKey, 'system');
    applyMode(savedMode);

    if (!toggle) return;

    RX.utils.on(toggle, 'click', function () {
      const current = RX.utils.storage.get(storageKey, 'system');
      let next = 'dark';

      if (current === 'dark') {
        next = 'light';
      } else if (current === 'light') {
        next = 'system';
      }

      RX.utils.storage.set(storageKey, next);
      applyMode(next);
    });

    const media = window.matchMedia('(prefers-color-scheme: dark)');

    if (media && typeof media.addEventListener === 'function') {
      media.addEventListener('change', function () {
        if (RX.utils.storage.get(storageKey, 'system') === 'system') {
          applyMode('system');
        }
      });
    }
  };

  /**
   * Search overlay.
   *
   * Expected:
   * <button class="rx-search-toggle">Search</button>
   * <div class="rx-search-overlay">
   *   <button class="rx-search-close">Close</button>
   *   <input type="search">
   * </div>
   */
  RX.searchOverlay = function () {
    const toggles = RX.utils.qsa(RX.selectors.searchToggle);
    const overlay = RX.utils.qs(RX.selectors.searchOverlay);
    const close = RX.utils.qs(RX.selectors.searchClose, overlay || document);

    if (!toggles.length || !overlay) return;

    const input = overlay.querySelector('input[type="search"], input[type="text"]');

    const openSearch = () => {
      overlay.classList.add(RX.settings.openClass);
      overlay.setAttribute('aria-hidden', 'false');
      RX.body.classList.add('rx-search-open');
      RX.utils.lockScroll();

      setTimeout(() => {
        if (input) input.focus();
      }, 80);
    };

    const closeSearch = () => {
      overlay.classList.remove(RX.settings.openClass);
      overlay.setAttribute('aria-hidden', 'true');
      RX.body.classList.remove('rx-search-open');
      RX.utils.unlockScroll();
    };

    toggles.forEach((toggle) => {
      RX.utils.on(toggle, 'click', openSearch);
    });

    RX.utils.on(close, 'click', closeSearch);

    RX.utils.on(document, 'keydown', function (event) {
      if (event.key === 'Escape') {
        closeSearch();
      }

      if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'k') {
        event.preventDefault();
        openSearch();
      }
    });

    RX.utils.on(overlay, 'click', function (event) {
      if (event.target === overlay) {
        closeSearch();
      }
    });
  };

  /**
   * Smooth scroll for anchor links.
   */
  RX.smoothScroll = function () {
    const links = RX.utils.qsa(RX.selectors.smoothLink);

    if (!links.length) return;

    links.forEach((link) => {
      RX.utils.on(link, 'click', function (event) {
        const hash = link.getAttribute('href');

        if (!hash || hash === '#') return;

        const target = document.querySelector(hash);

        if (!target) return;

        event.preventDefault();

        target.scrollIntoView({
          behavior: RX.settings.reducedMotion ? 'auto' : 'smooth',
          block: 'start'
        });

        target.setAttribute('tabindex', '-1');
        target.focus({ preventScroll: true });

        history.pushState(null, '', hash);
      });
    });
  };

  /**
   * Table of contents generator.
   *
   * Expected:
   * <nav class="rx-table-of-contents"></nav>
   */
  RX.tableOfContents = function () {
    const toc = RX.utils.qs(RX.selectors.toc);
    const content = RX.utils.qs(RX.selectors.content);

    if (!toc || !content) return;

    const headings = RX.utils.qsa('h2, h3', content).filter((heading) => {
      return heading.textContent.trim().length > 0;
    });

    if (headings.length < 2) {
      toc.style.display = 'none';
      return;
    }

    const list = document.createElement('ol');
    list.className = 'rx-toc-list';

    headings.forEach((heading, index) => {
      if (!heading.id) {
        heading.id = `rx-heading-${index + 1}`;
      }

      const item = document.createElement('li');
      item.className = `rx-toc-item rx-toc-${heading.tagName.toLowerCase()}`;

      const link = document.createElement('a');
      link.href = `#${heading.id}`;
      link.textContent = heading.textContent.trim();

      item.appendChild(link);
      list.appendChild(item);
    });

    toc.innerHTML = '';
    toc.appendChild(list);

    const tocLinks = RX.utils.qsa('a', toc);

    if ('IntersectionObserver' in window) {
      const observer = new IntersectionObserver(
        function (entries) {
          entries.forEach((entry) => {
            if (!entry.isIntersecting) return;

            tocLinks.forEach((link) => link.classList.remove(RX.settings.activeClass));

            const active = toc.querySelector(`a[href="#${entry.target.id}"]`);
            if (active) active.classList.add(RX.settings.activeClass);
          });
        },
        {
          rootMargin: '-20% 0px -70% 0px',
          threshold: 0
        }
      );

      headings.forEach((heading) => observer.observe(heading));
    }
  };

  /**
   * Lazy loading helper.
   *
   * Expected:
   * <img data-rx-lazy data-src="image.jpg" alt="">
   */
  RX.lazyLoad = function () {
    const lazyItems = RX.utils.qsa(RX.selectors.lazy);

    if (!lazyItems.length) return;

    const loadItem = (item) => {
      const src = item.getAttribute('data-src');
      const srcset = item.getAttribute('data-srcset');
      const bg = item.getAttribute('data-bg');

      if (src) item.setAttribute('src', src);
      if (srcset) item.setAttribute('srcset', srcset);
      if (bg) item.style.backgroundImage = `url("${bg}")`;

      item.classList.add(RX.settings.loadedClass);
      item.removeAttribute('data-rx-lazy');
    };

    if ('IntersectionObserver' in window) {
      const observer = new IntersectionObserver(
        function (entries, obs) {
          entries.forEach((entry) => {
            if (!entry.isIntersecting) return;

            loadItem(entry.target);
            obs.unobserve(entry.target);
          });
        },
        {
          rootMargin: '200px 0px'
        }
      );

      lazyItems.forEach((item) => observer.observe(item));
    } else {
      lazyItems.forEach(loadItem);
    }
  };

  /**
   * Copy code button for code blocks.
   */
  RX.copyCode = function () {
    const codeBlocks = RX.utils.qsa(RX.selectors.copyCode);

    if (!codeBlocks.length || !navigator.clipboard) return;

    codeBlocks.forEach((code) => {
      const pre = code.closest('pre');

      if (!pre || pre.classList.contains('rx-copy-ready')) return;

      pre.classList.add('rx-copy-ready');

      const button = document.createElement('button');
      button.type = 'button';
      button.className = 'rx-copy-code-button';
      button.textContent = 'Copy';
      button.setAttribute('aria-label', 'Copy code');

      pre.appendChild(button);

      RX.utils.on(button, 'click', async function () {
        try {
          await navigator.clipboard.writeText(code.innerText);
          button.textContent = 'Copied';
          button.classList.add(RX.settings.activeClass);

          setTimeout(() => {
            button.textContent = 'Copy';
            button.classList.remove(RX.settings.activeClass);
          }, 1800);
        } catch (error) {
          button.textContent = 'Failed';

          setTimeout(() => {
            button.textContent = 'Copy';
          }, 1800);
        }
      });
    });
  };

  /**
   * Accordion.
   *
   * Expected:
   * <div data-rx-accordion>
   *   <button data-rx-accordion-trigger>Question</button>
   *   <div>Answer</div>
   * </div>
   */
  RX.accordion = function () {
    const accordions = RX.utils.qsa(RX.selectors.accordion);

    if (!accordions.length) return;

    accordions.forEach((accordion) => {
      const triggers = RX.utils.qsa(RX.selectors.accordionTrigger, accordion);

      triggers.forEach((trigger) => {
        const panel = trigger.nextElementSibling;

        if (!panel) return;

        const triggerId = trigger.id || RX.utils.createId('rx-accordion-trigger');
        const panelId = panel.id || RX.utils.createId('rx-accordion-panel');

        trigger.id = triggerId;
        panel.id = panelId;

        trigger.setAttribute('aria-controls', panelId);
        trigger.setAttribute('aria-expanded', 'false');

        panel.setAttribute('role', 'region');
        panel.setAttribute('aria-labelledby', triggerId);
        panel.hidden = true;

        RX.utils.on(trigger, 'click', function () {
          const expanded = trigger.getAttribute('aria-expanded') === 'true';
          const allowMultiple = accordion.getAttribute('data-rx-accordion') === 'multiple';

          if (!allowMultiple) {
            triggers.forEach((otherTrigger) => {
              const otherPanel = otherTrigger.nextElementSibling;

              otherTrigger.setAttribute('aria-expanded', 'false');
              otherTrigger.classList.remove(RX.settings.activeClass);

              if (otherPanel) {
                otherPanel.hidden = true;
                otherPanel.classList.remove(RX.settings.openClass);
              }
            });
          }

          trigger.setAttribute('aria-expanded', String(!expanded));
          trigger.classList.toggle(RX.settings.activeClass, !expanded);
          panel.hidden = expanded;
          panel.classList.toggle(RX.settings.openClass, !expanded);
        });
      });
    });
  };

  /**
   * Tabs.
   *
   * Expected:
   * <div data-rx-tabs>
   *   <button data-rx-tab-button data-tab="one">One</button>
   *   <div data-rx-tab-panel data-tab="one">Panel One</div>
   * </div>
   */
  RX.tabs = function () {
    const tabGroups = RX.utils.qsa(RX.selectors.tabs);

    if (!tabGroups.length) return;

    tabGroups.forEach((group) => {
      const buttons = RX.utils.qsa(RX.selectors.tabButton, group);
      const panels = RX.utils.qsa(RX.selectors.tabPanel, group);

      if (!buttons.length || !panels.length) return;

      group.setAttribute('role', 'tablist');

      buttons.forEach((button, index) => {
        const key = button.getAttribute('data-tab') || String(index);
        const panel = panels.find((item) => item.getAttribute('data-tab') === key) || panels[index];

        if (!panel) return;

        const buttonId = button.id || RX.utils.createId('rx-tab');
        const panelId = panel.id || RX.utils.createId('rx-tab-panel');

        button.id = buttonId;
        panel.id = panelId;

        button.setAttribute('role', 'tab');
        button.setAttribute('aria-controls', panelId);
        button.setAttribute('aria-selected', index === 0 ? 'true' : 'false');
        button.setAttribute('tabindex', index === 0 ? '0' : '-1');

        panel.setAttribute('role', 'tabpanel');
        panel.setAttribute('aria-labelledby', buttonId);
        panel.hidden = index !== 0;

        RX.utils.on(button, 'click', function () {
          buttons.forEach((btn) => {
            btn.setAttribute('aria-selected', 'false');
            btn.setAttribute('tabindex', '-1');
            btn.classList.remove(RX.settings.activeClass);
          });

          panels.forEach((p) => {
            p.hidden = true;
            p.classList.remove(RX.settings.activeClass);
          });

          button.setAttribute('aria-selected', 'true');
          button.setAttribute('tabindex', '0');
          button.classList.add(RX.settings.activeClass);

          panel.hidden = false;
          panel.classList.add(RX.settings.activeClass);
        });

        RX.utils.on(button, 'keydown', function (event) {
          const currentIndex = buttons.indexOf(button);
          let nextIndex = currentIndex;

          if (event.key === 'ArrowRight') nextIndex = (currentIndex + 1) % buttons.length;
          if (event.key === 'ArrowLeft') nextIndex = (currentIndex - 1 + buttons.length) % buttons.length;

          if (nextIndex !== currentIndex) {
            event.preventDefault();
            buttons[nextIndex].focus();
            buttons[nextIndex].click();
          }
        });
      });
    });
  };

  /**
   * Modal.
   *
   * Expected:
   * <button data-rx-modal-open="my-modal">Open</button>
   * <div data-rx-modal="my-modal">
   *   <button data-rx-modal-close>Close</button>
   * </div>
   */
  RX.modal = function () {
    const triggers = RX.utils.qsa(RX.selectors.modalTrigger);
    const modals = RX.utils.qsa(RX.selectors.modal);

    if (!triggers.length || !modals.length) return;

    let lastFocused = null;

    const getModal = (name) => {
      return modals.find((modal) => modal.getAttribute('data-rx-modal') === name);
    };

    const openModal = (modal) => {
      if (!modal) return;

      lastFocused = document.activeElement;

      modal.classList.add(RX.settings.openClass);
      modal.setAttribute('aria-hidden', 'false');
      RX.utils.lockScroll();

      const focusable = RX.utils.getFocusable(modal);
      if (focusable.length) {
        focusable[0].focus();
      }
    };

    const closeModal = (modal) => {
      if (!modal) return;

      modal.classList.remove(RX.settings.openClass);
      modal.setAttribute('aria-hidden', 'true');
      RX.utils.unlockScroll();

      if (lastFocused && typeof lastFocused.focus === 'function') {
        lastFocused.focus();
      }
    };

    modals.forEach((modal) => {
      modal.setAttribute('aria-hidden', 'true');
      modal.setAttribute('role', 'dialog');
      modal.setAttribute('aria-modal', 'true');

      RX.utils.qsa(RX.selectors.modalClose, modal).forEach((close) => {
        RX.utils.on(close, 'click', () => closeModal(modal));
      });

      RX.utils.on(modal, 'click', function (event) {
        if (event.target === modal) {
          closeModal(modal);
        }
      });

      RX.utils.on(modal, 'keydown', function (event) {
        if (event.key !== 'Tab') return;

        const focusable = RX.utils.getFocusable(modal);
        if (!focusable.length) return;

        const first = focusable[0];
        const last = focusable[focusable.length - 1];

        if (event.shiftKey && document.activeElement === first) {
          event.preventDefault();
          last.focus();
        } else if (!event.shiftKey && document.activeElement === last) {
          event.preventDefault();
          first.focus();
        }
      });
    });

    triggers.forEach((trigger) => {
      RX.utils.on(trigger, 'click', function () {
        const name = trigger.getAttribute('data-rx-modal-open');
        openModal(getModal(name));
      });
    });

    RX.utils.on(document, 'keydown', function (event) {
      if (event.key !== 'Escape') return;

      modals.forEach((modal) => {
        if (modal.classList.contains(RX.settings.openClass)) {
          closeModal(modal);
        }
      });
    });
  };

  /**
   * Dismissible notices.
   *
   * Expected:
   * <div class="notice">
   *   <button data-rx-dismiss>Dismiss</button>
   * </div>
   */
  RX.dismissible = function () {
    const buttons = RX.utils.qsa(RX.selectors.dismiss);

    if (!buttons.length) return;

    buttons.forEach((button) => {
      RX.utils.on(button, 'click', function () {
        const targetSelector = button.getAttribute('data-rx-dismiss');
        const target = targetSelector
          ? document.querySelector(targetSelector)
          : button.closest('.notice, .rx-notice, .rx-alert, .rx-banner');

        if (!target) return;

        target.classList.add(RX.settings.hiddenClass);

        const storageKey = target.getAttribute('data-rx-dismiss-key');

        if (storageKey) {
          RX.utils.storage.set(`rx-dismiss-${storageKey}`, '1');
        }
      });
    });

    RX.utils.qsa('[data-rx-dismiss-key]').forEach((item) => {
      const key = item.getAttribute('data-rx-dismiss-key');

      if (RX.utils.storage.get(`rx-dismiss-${key}`) === '1') {
        item.classList.add(RX.settings.hiddenClass);
      }
    });
  };

  /**
   * Reading time.
   *
   * Expected:
   * <span data-rx-reading-time></span>
   */
  RX.readingTime = function () {
    const output = RX.utils.qs(RX.selectors.readingTime);
    const content = RX.utils.qs(RX.selectors.content);

    if (!output || !content) return;

    const words = content.textContent.trim().split(/\s+/).filter(Boolean).length;
    const minutes = Math.max(1, Math.ceil(words / 220));

    output.textContent = `${minutes} min read`;
    output.setAttribute('datetime', `PT${minutes}M`);
  };

  /**
   * External links improvement.
   */
  RX.externalLinks = function () {
    const currentHost = window.location.hostname;
    const links = RX.utils.qsa(RX.selectors.externalLinks);

    if (!links.length) return;

    links.forEach((link) => {
      try {
        const url = new URL(link.href);

        if (url.hostname !== currentHost) {
          link.setAttribute('target', '_blank');
          link.setAttribute('rel', 'noopener noreferrer');

          if (!link.classList.contains('rx-no-external-icon')) {
            link.classList.add('rx-external-link');
          }
        }
      } catch (error) {
        // Invalid URL. Ignore safely.
      }
    });
  };

  /**
   * Image enhancements.
   */
  RX.imageEnhancements = function () {
    const images = RX.utils.qsa('img');

    if (!images.length) return;

    images.forEach((img) => {
      if (!img.hasAttribute('loading')) {
        img.setAttribute('loading', 'lazy');
      }

      if (!img.hasAttribute('decoding')) {
        img.setAttribute('decoding', 'async');
      }

      RX.utils.on(img, 'load', function () {
        img.classList.add('rx-image-loaded');
      });

      RX.utils.on(img, 'error', function () {
        img.classList.add('rx-image-error');
      });
    });
  };

  /**
   * Responsive tables.
   */
  RX.responsiveTables = function () {
    const content = RX.utils.qs(RX.selectors.content);

    if (!content) return;

    const tables = RX.utils.qsa('table', content);

    tables.forEach((table) => {
      if (table.closest('.rx-table-wrap')) return;

      const wrapper = document.createElement('div');
      wrapper.className = 'rx-table-wrap';
      wrapper.setAttribute('tabindex', '0');

      table.parentNode.insertBefore(wrapper, table);
      wrapper.appendChild(table);
    });
  };

  /**
   * Form validation.
   *
   * Expected:
   * <form data-rx-validate>
   */
  RX.formValidation = function () {
    const forms = RX.utils.qsa(RX.selectors.form);

    if (!forms.length) return;

    forms.forEach((form) => {
      RX.utils.on(form, 'submit', function (event) {
        let valid = true;

        RX.utils.qsa('[required]', form).forEach((field) => {
          const errorId = field.id ? `${field.id}-error` : RX.utils.createId('rx-field-error');
          let error = document.getElementById(errorId);

          if (!field.id) {
            field.id = RX.utils.createId('rx-field');
          }

          if (!error) {
            error = document.createElement('div');
            error.id = errorId;
            error.className = 'rx-field-error';
            error.setAttribute('role', 'alert');
            field.insertAdjacentElement('afterend', error);
          }

          if (!field.value.trim()) {
            valid = false;
            field.classList.add('rx-field-invalid');
            field.setAttribute('aria-invalid', 'true');
            field.setAttribute('aria-describedby', errorId);
            error.textContent = 'This field is required.';
          } else {
            field.classList.remove('rx-field-invalid');
            field.removeAttribute('aria-invalid');
            error.textContent = '';
          }
        });

        if (!valid) {
          event.preventDefault();

          const firstInvalid = form.querySelector('.rx-field-invalid');
          if (firstInvalid) firstInvalid.focus();
        }
      });
    });
  };

  /**
   * WordPress comment form enhancements.
   */
  RX.commentForm = function () {
    const form = RX.utils.qs(RX.selectors.commentForm);

    if (!form) return;

    const textarea = form.querySelector('textarea');

    if (textarea) {
      const counter = document.createElement('div');
      counter.className = 'rx-comment-counter';
      counter.setAttribute('aria-live', 'polite');

      textarea.insertAdjacentElement('afterend', counter);

      const updateCounter = () => {
        const count = textarea.value.trim().length;
        counter.textContent = `${count} characters`;
      };

      RX.utils.on(textarea, 'input', RX.utils.debounce(updateCounter, 100));
      updateCounter();
    }

    RX.utils.on(form, 'submit', function () {
      const submit = form.querySelector('[type="submit"]');

      if (submit) {
        submit.classList.add('rx-is-submitting');
        submit.setAttribute('aria-busy', 'true');
      }
    });
  };

  /**
   * Animate items on scroll.
   *
   * Expected:
   * Add class .rx-animate to elements.
   */
  RX.scrollAnimations = function () {
    const items = RX.utils.qsa('.rx-animate');

    if (!items.length) return;

    if (RX.settings.reducedMotion) {
      items.forEach((item) => item.classList.add(RX.settings.animationClass));
      return;
    }

    if ('IntersectionObserver' in window) {
      const observer = new IntersectionObserver(
        function (entries, obs) {
          entries.forEach((entry) => {
            if (!entry.isIntersecting) return;

            entry.target.classList.add(RX.settings.animationClass);
            obs.unobserve(entry.target);
          });
        },
        {
          threshold: 0.15
        }
      );

      items.forEach((item) => observer.observe(item));
    } else {
      items.forEach((item) => item.classList.add(RX.settings.animationClass));
    }
  };

  /**
   * Active section link highlight.
   *
   * Expected:
   * <a class="rx-section-link" href="#section-id">
   */
  RX.activeSectionLinks = function () {
    const links = RX.utils.qsa('.rx-section-link[href^="#"]');

    if (!links.length || !('IntersectionObserver' in window)) return;

    const targets = links
      .map((link) => {
        const id = link.getAttribute('href');
        return id ? document.querySelector(id) : null;
      })
      .filter(Boolean);

    if (!targets.length) return;

    const observer = new IntersectionObserver(
      function (entries) {
        entries.forEach((entry) => {
          if (!entry.isIntersecting) return;

          links.forEach((link) => link.classList.remove(RX.settings.activeClass));

          const active = links.find((link) => {
            return link.getAttribute('href') === `#${entry.target.id}`;
          });

          if (active) {
            active.classList.add(RX.settings.activeClass);
          }
        });
      },
      {
        rootMargin: '-30% 0px -60% 0px'
      }
    );

    targets.forEach((target) => observer.observe(target));
  };

  /**
   * Print button.
   *
   * Expected:
   * <button data-rx-print>Print</button>
   */
  RX.printButton = function () {
    const buttons = RX.utils.qsa('[data-rx-print]');

    if (!buttons.length) return;

    buttons.forEach((button) => {
      RX.utils.on(button, 'click', function () {
        window.print();
      });
    });
  };

  /**
   * Share buttons.
   *
   * Expected:
   * <button data-rx-share>Share</button>
   */
  RX.shareButton = function () {
    const buttons = RX.utils.qsa('[data-rx-share]');

    if (!buttons.length) return;

    buttons.forEach((button) => {
      RX.utils.on(button, 'click', async function () {
        const shareData = {
          title: document.title,
          text: button.getAttribute('data-rx-share-text') || document.title,
          url: button.getAttribute('data-rx-share-url') || window.location.href
        };

        if (navigator.share) {
          try {
            await navigator.share(shareData);
          } catch (error) {
            // User cancelled share. Ignore.
          }
        } else if (navigator.clipboard) {
          await navigator.clipboard.writeText(shareData.url);

          const original = button.textContent;
          button.textContent = 'Link copied';

          setTimeout(() => {
            button.textContent = original;
          }, 1600);
        }
      });
    });
  };

  /**
   * Font size controls.
   *
   * Expected:
   * <button data-rx-font="increase">A+</button>
   * <button data-rx-font="decrease">A-</button>
   * <button data-rx-font="reset">Reset</button>
   */
  RX.fontControls = function () {
    const buttons = RX.utils.qsa('[data-rx-font]');
    const storageKey = 'rx-font-scale';

    if (!buttons.length) return;

    const applyScale = (scale) => {
      RX.html.style.setProperty('--rx-font-scale', scale);
      RX.utils.storage.set(storageKey, String(scale));
    };

    let scale = parseFloat(RX.utils.storage.get(storageKey, '1'));

    if (Number.isNaN(scale)) scale = 1;

    applyScale(scale);

    buttons.forEach((button) => {
      RX.utils.on(button, 'click', function () {
        const action = button.getAttribute('data-rx-font');

        if (action === 'increase') scale = Math.min(1.3, scale + 0.05);
        if (action === 'decrease') scale = Math.max(0.85, scale - 0.05);
        if (action === 'reset') scale = 1;

        applyScale(Number(scale.toFixed(2)));
      });
    });
  };

  /**
   * Simple client-side post filter.
   *
   * Expected:
   * <input data-rx-filter-input>
   * <article data-rx-filter-item data-title="post title">
   */
  RX.postFilter = function () {
    const input = RX.utils.qs('[data-rx-filter-input]');
    const items = RX.utils.qsa('[data-rx-filter-item]');

    if (!input || !items.length) return;

    const filter = RX.utils.debounce(function () {
      const query = input.value.trim().toLowerCase();

      items.forEach((item) => {
        const title = (
          item.getAttribute('data-title') ||
          item.textContent ||
          ''
        ).toLowerCase();

        item.hidden = query.length > 0 && !title.includes(query);
      });
    }, 120);

    RX.utils.on(input, 'input', filter);
  };

  /**
   * Password visibility toggle.
   *
   * Expected:
   * <button data-rx-password-toggle="#password-field">Show</button>
   */
  RX.passwordToggle = function () {
    const buttons = RX.utils.qsa('[data-rx-password-toggle]');

    if (!buttons.length) return;

    buttons.forEach((button) => {
      RX.utils.on(button, 'click', function () {
        const selector = button.getAttribute('data-rx-password-toggle');
        const field = selector ? document.querySelector(selector) : null;

        if (!field) return;

        const isPassword = field.getAttribute('type') === 'password';

        field.setAttribute('type', isPassword ? 'text' : 'password');
        button.setAttribute('aria-pressed', String(isPassword));
        button.textContent = isPassword ? 'Hide' : 'Show';
      });
    });
  };

  /**
   * Auto year.
   *
   * Expected:
   * <span data-rx-year></span>
   */
  RX.autoYear = function () {
    const items = RX.utils.qsa('[data-rx-year]');

    if (!items.length) return;

    const year = new Date().getFullYear();

    items.forEach((item) => {
      item.textContent = String(year);
    });
  };

  /**
   * Last updated date formatter.
   *
   * Expected:
   * <time data-rx-format-date datetime="2026-05-14"></time>
   */
  RX.formatDates = function () {
    const dates = RX.utils.qsa('[data-rx-format-date]');

    if (!dates.length || !window.Intl) return;

    dates.forEach((dateEl) => {
      const raw = dateEl.getAttribute('datetime') || dateEl.textContent;
      const date = new Date(raw);

      if (Number.isNaN(date.getTime())) return;

      dateEl.textContent = new Intl.DateTimeFormat(undefined, {
        year: 'numeric',
        month: 'long',
        day: 'numeric'
      }).format(date);
    });
  };

  /**
   * Basic AJAX helper for WordPress.
   *
   * Requires localized object from PHP:
   *
   * wp_localize_script(
   *   'rx-theme-app',
   *   'rxTheme',
   *   array(
   *     'ajaxUrl' => admin_url('admin-ajax.php'),
   *     'nonce'   => wp_create_nonce('rx_theme_nonce')
   *   )
   * );
   */
  RX.ajax = {
    request(action, data = {}) {
      if (!window.rxTheme || !window.rxTheme.ajaxUrl) {
        return Promise.reject(new Error('rxTheme.ajaxUrl is missing.'));
      }

      const formData = new FormData();
      formData.append('action', action);

      if (window.rxTheme.nonce) {
        formData.append('nonce', window.rxTheme.nonce);
      }

      Object.keys(data).forEach((key) => {
        formData.append(key, data[key]);
      });

      return fetch(window.rxTheme.ajaxUrl, {
        method: 'POST',
        credentials: 'same-origin',
        body: formData
      }).then((response) => {
        if (!response.ok) {
          throw new Error('Network response failed.');
        }

        return response.json();
      });
    }
  };

  /**
   * AJAX load more posts.
   *
   * Expected:
   * <button data-rx-load-more data-page="1" data-max="5">Load More</button>
   * <div data-rx-load-target></div>
   */
  RX.loadMore = function () {
    const button = RX.utils.qs('[data-rx-load-more]');
    const target = RX.utils.qs('[data-rx-load-target]');

    if (!button || !target) return;

    RX.utils.on(button, 'click', function () {
      const page = parseInt(button.getAttribute('data-page') || '1', 10);
      const max = parseInt(button.getAttribute('data-max') || '1', 10);

      if (page >= max) {
        button.hidden = true;
        return;
      }

      button.disabled = true;
      button.classList.add('rx-loading');

      RX.ajax
        .request('rx_load_more_posts', {
          page: page + 1
        })
        .then((response) => {
          if (response && response.success && response.data && response.data.html) {
            target.insertAdjacentHTML('beforeend', response.data.html);
            button.setAttribute('data-page', String(page + 1));

            if (page + 1 >= max) {
              button.hidden = true;
            }
          }
        })
        .catch(() => {
          button.classList.add('rx-error');
        })
        .finally(() => {
          button.disabled = false;
          button.classList.remove('rx-loading');
        });
    });
  };

  /**
   * Newsletter/simple AJAX form.
   *
   * Expected:
   * <form data-rx-ajax-form data-action="rx_newsletter_submit">
   */
  RX.ajaxForm = function () {
    const forms = RX.utils.qsa('[data-rx-ajax-form]');

    if (!forms.length) return;

    forms.forEach((form) => {
      RX.utils.on(form, 'submit', function (event) {
        event.preventDefault();

        const action = form.getAttribute('data-action');
        const message = form.querySelector('[data-rx-form-message]');

        if (!action) return;

        const data = {};
        const formData = new FormData(form);

        formData.forEach((value, key) => {
          data[key] = value;
        });

        form.classList.add('rx-loading');

        RX.ajax
          .request(action, data)
          .then((response) => {
            if (message) {
              message.textContent =
                response && response.data && response.data.message
                  ? response.data.message
                  : 'Submitted successfully.';
            }

            if (response && response.success) {
              form.reset();
            }
          })
          .catch(() => {
            if (message) {
              message.textContent = 'Something went wrong. Please try again.';
            }
          })
          .finally(() => {
            form.classList.remove('rx-loading');
          });
      });
    });
  };

  /**
   * Medical article helper:
   * Converts glossary terms with data-rx-term into accessible tooltips.
   *
   * Expected:
   * <span data-rx-term="Simple explanation">Medical term</span>
   */
  RX.termTooltips = function () {
    const terms = RX.utils.qsa('[data-rx-term]');

    if (!terms.length) return;

    terms.forEach((term) => {
      const text = term.getAttribute('data-rx-term');

      if (!text) return;

      const tooltipId = RX.utils.createId('rx-term');

      term.setAttribute('tabindex', '0');
      term.setAttribute('aria-describedby', tooltipId);
      term.classList.add('rx-term');

      const tooltip = document.createElement('span');
      tooltip.id = tooltipId;
      tooltip.className = 'rx-term-tooltip';
      tooltip.setAttribute('role', 'tooltip');
      tooltip.textContent = text;

      term.appendChild(tooltip);
    });
  };

  /**
   * Medical article helper:
   * Citation copy button.
   *
   * Expected:
   * <button data-rx-copy-citation data-citation="Citation text">Copy citation</button>
   */
  RX.copyCitation = function () {
    const buttons = RX.utils.qsa('[data-rx-copy-citation]');

    if (!buttons.length || !navigator.clipboard) return;

    buttons.forEach((button) => {
      RX.utils.on(button, 'click', async function () {
        const citation = button.getAttribute('data-citation') || '';

        if (!citation) return;

        const oldText = button.textContent;

        try {
          await navigator.clipboard.writeText(citation);
          button.textContent = 'Citation copied';
        } catch (error) {
          button.textContent = 'Copy failed';
        }

        setTimeout(() => {
          button.textContent = oldText;
        }, 1600);
      });
    });
  };

  /**
   * Highlight search terms from URL.
   *
   * Example:
   * /?highlight=neutropenia
   */
  RX.highlightFromUrl = function () {
    const params = new URLSearchParams(window.location.search);
    const term = params.get('highlight');

    if (!term || term.length < 3) return;

    const content = RX.utils.qs(RX.selectors.content);

    if (!content) return;

    const safeTerm = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    const regex = new RegExp(`(${safeTerm})`, 'gi');

    RX.utils.qsa('p, li', content).forEach((node) => {
      if (node.children.length > 3) return;

      node.innerHTML = node.innerHTML.replace(regex, '<mark class="rx-highlight">$1</mark>');
    });
  };

  /**
   * Simple notice for offline/online state.
   */
  RX.networkStatus = function () {
    const notice = document.createElement('div');
    notice.className = 'rx-network-status';
    notice.setAttribute('role', 'status');
    notice.setAttribute('aria-live', 'polite');
    notice.hidden = true;

    document.body.appendChild(notice);

    const show = (message) => {
      notice.textContent = message;
      notice.hidden = false;

      setTimeout(() => {
        notice.hidden = true;
      }, 2500);
    };

    RX.utils.on(window, 'offline', function () {
      show('You are offline.');
      RX.body.classList.add('rx-is-offline');
    });

    RX.utils.on(window, 'online', function () {
      show('You are back online.');
      RX.body.classList.remove('rx-is-offline');
    });
  };

  /**
   * Keyboard focus style helper.
   */
  RX.focusVisible = function () {
    let usingKeyboard = false;

    RX.utils.on(document, 'keydown', function (event) {
      if (event.key === 'Tab') {
        usingKeyboard = true;
        RX.body.classList.add(RX.settings.focusClass);
      }
    });

    RX.utils.on(document, 'mousedown', function () {
      usingKeyboard = false;
      RX.body.classList.remove(RX.settings.focusClass);
    });

    RX.utils.on(document, 'focusin', function () {
      if (usingKeyboard) {
        RX.body.classList.add(RX.settings.focusClass);
      }
    });
  };

  /**
   * Prevent empty links from jumping.
   */
  RX.emptyLinks = function () {
    RX.utils.qsa('a[href="#"]').forEach((link) => {
      RX.utils.on(link, 'click', function (event) {
        event.preventDefault();
      });
    });
  };

  /**
   * Init all modules.
   */
  RX.init = function () {
    RX.jsEnabled();
    RX.focusVisible();
    RX.mobileMenu();
    RX.dropdownMenu();
    RX.stickyHeader();
    RX.readingProgress();
    RX.scrollTop();
    RX.darkMode();
    RX.searchOverlay();
    RX.smoothScroll();
    RX.tableOfContents();
    RX.lazyLoad();
    RX.copyCode();
    RX.accordion();
    RX.tabs();
    RX.modal();
    RX.dismissible();
    RX.readingTime();
    RX.externalLinks();
    RX.imageEnhancements();
    RX.responsiveTables();
    RX.formValidation();
    RX.commentForm();
    RX.scrollAnimations();
    RX.activeSectionLinks();
    RX.printButton();
    RX.shareButton();
    RX.fontControls();
    RX.postFilter();
    RX.passwordToggle();
    RX.autoYear();
    RX.formatDates();
    RX.loadMore();
    RX.ajaxForm();
    RX.termTooltips();
    RX.copyCitation();
    RX.highlightFromUrl();
    RX.networkStatus();
    RX.emptyLinks();

    RX.body.classList.add('rx-app-ready');

    document.dispatchEvent(
      new CustomEvent('rxThemeReady', {
        detail: {
          version: RX.version,
          app: RX
        }
      })
    );
  };

  /**
   * Start app.
   */
  RX.utils.ready(RX.init);

  /**
   * Expose safely for custom extensions.
   */
  window.RXTheme = RX;
})();

Add this in your functions.php:

function rx_theme_enqueue_scripts() {
    wp_enqueue_script(
        'rx-theme-app',
        get_template_directory_uri() . '/inc/js/app.js',
        array(),
        wp_get_theme()->get( 'Version' ),
        true
    );

    wp_localize_script(
        'rx-theme-app',
        'rxTheme',
        array(
            'ajaxUrl' => admin_url( 'admin-ajax.php' ),
            'nonce'   => wp_create_nonce( 'rx_theme_nonce' ),
            'homeUrl' => home_url( '/' ),
        )
    );
}
add_action( 'wp_enqueue_scripts', 'rx_theme_enqueue_scripts' );

For cache safety, you can use this better version:

function rx_theme_enqueue_scripts() {
    $app_js_path = get_template_directory() . '/inc/js/app.js';
    $app_js_uri  = get_template_directory_uri() . '/inc/js/app.js';

    wp_enqueue_script(
        'rx-theme-app',
        $app_js_uri,
        array(),
        file_exists( $app_js_path ) ? filemtime( $app_js_path ) : wp_get_theme()->get( 'Version' ),
        true
    );

    wp_localize_script(
        'rx-theme-app',
        'rxTheme',
        array(
            'ajaxUrl' => admin_url( 'admin-ajax.php' ),
            'nonce'   => wp_create_nonce( 'rx_theme_nonce' ),
            'homeUrl' => home_url( '/' ),
        )
    );
}
add_action( 'wp_enqueue_scripts', 'rx_theme_enqueue_scripts' );

This file is big, but it will not break the theme because each feature activates only when its matching class or data-rx-* attribute exists.

Leave a Reply

Your email address will not be published. Required fields are marked *