Top/Bottom Navigation Buttons

Sleek top/bottom navigation buttons: click to jump, hover to scroll continuously

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name         Top/Bottom Navigation Buttons
// @namespace    https://openuserjs.org/
// @version      2.5.1
// @description  Sleek top/bottom navigation buttons: click to jump, hover to scroll continuously
// @author       r3dhack3r
// @license      MIT
// @match        *://*/*
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    // ─── Configuration ────────────────────────────────────────────────────────────

    const CFG = {
        scrollThresholdTop:    200,
        scrollThresholdBottom: 100,
        hoverScrollSpeed:      15,
        hoverScrollInterval:   16,
        clickScrollDuration:   800,
    };

    // ─── Styles ───────────────────────────────────────────────────────────────────

    const CSS = `
        :root {
            --nb-bg:           rgba(255, 255, 255, 0.90);
            --nb-bg-hover:     rgba(255, 255, 255, 1);
            --nb-bg-active:    rgba(240, 242, 245, 1);
            --nb-border:       rgba(0, 0, 0, 0.08);
            --nb-border-hover: rgba(0, 0, 0, 0.15);
            --nb-shadow:       0 2px 10px rgba(0,0,0,0.06), 0 1px 3px rgba(0,0,0,0.05);
            --nb-shadow-hover: 0 4px 20px rgba(0,0,0,0.10), 0 2px 5px rgba(0,0,0,0.06);
            --nb-icon:         #4b5563;
            --nb-icon-hover:   #111827;
            --nb-size:         40px;
            --nb-gap:          8px;
            --nb-radius:       10px;
        }

        @media (prefers-color-scheme: dark) {
            :root {
                --nb-bg:           rgba(30, 30, 36, 0.90);
                --nb-bg-hover:     rgba(42, 42, 50, 1);
                --nb-bg-active:    rgba(52, 52, 62, 1);
                --nb-border:       rgba(255,255,255,0.08);
                --nb-border-hover: rgba(255,255,255,0.15);
                --nb-shadow:       0 2px 10px rgba(0,0,0,0.30), 0 1px 3px rgba(0,0,0,0.20);
                --nb-shadow-hover: 0 4px 20px rgba(0,0,0,0.45), 0 2px 5px rgba(0,0,0,0.25);
                --nb-icon:         #9ca3af;
                --nb-icon-hover:   #f3f4f6;
            }
        }

        .nb-button {
            all: unset;
            position: fixed;
            right: 24px;
            width: var(--nb-size);
            height: var(--nb-size);
            background: var(--nb-bg);
            border: 1px solid var(--nb-border);
            border-radius: var(--nb-radius);
            box-shadow: var(--nb-shadow);
            backdrop-filter: blur(12px) saturate(1.4);
            -webkit-backdrop-filter: blur(12px) saturate(1.4);
            cursor: pointer;
            z-index: 2147483647;
            display: flex;
            align-items: center;
            justify-content: center;
            box-sizing: border-box;
            opacity: 0;
            visibility: hidden;
            pointer-events: none;
            transform: translateX(12px) scale(0.88);
            transition: all 0.25s cubic-bezier(0.22, 1, 0.36, 1);
        }

        .nb-button.nb-visible {
            opacity: 1;
            visibility: visible;
            pointer-events: auto;
            transform: translateX(0) scale(1);
        }

        .nb-button:hover {
            background: var(--nb-bg-hover);
            border-color: var(--nb-border-hover);
            box-shadow: var(--nb-shadow-hover);
            transform: translateX(0) scale(1.08);
        }

        .nb-button:active {
            transform: translateX(0) scale(0.94);
            background: var(--nb-bg-active);
            transition-duration: 0.08s;
        }

        .nb-button svg {
            width: 18px;
            height: 18px;
            fill: var(--nb-icon);
            transition: fill 0.2s ease;
            pointer-events: none !important;
        }

        .nb-button svg * {
            pointer-events: none !important;
        }

        .nb-button:hover svg {
            fill: var(--nb-icon-hover);
        }

        .nb-button.nb-scrolling svg {
            animation: nb-pulse 0.6s cubic-bezier(0.4, 0, 0.2, 1) infinite alternate;
        }

        #nb-top {
            top: calc(50% - var(--nb-size) - var(--nb-gap) / 2);
        }

        #nb-bottom {
            top: calc(50% + var(--nb-gap) / 2);
        }

        @keyframes nb-pulse {
            from { transform: translateY(0); }
            to { transform: translateY(-2px); }
        }
    `;

    // ─── Utilities ────────────────────────────────────────────────────────────────

    const getDocHeight = () => Math.max(
        document.body.scrollHeight,
        document.body.offsetHeight,
        document.documentElement.scrollHeight,
        document.documentElement.offsetHeight
    );

    const easeInOutCubic = (t) =>
        t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;

    // ─── Create SVG Icon ──────────────────────────────────────────────────────────

    function createSVGIcon(pathData) {
        const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        svg.setAttribute('viewBox', '0 0 24 24');
        svg.style.pointerEvents = 'none';

        const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
        path.setAttribute('d', pathData);
        path.style.pointerEvents = 'none';

        svg.appendChild(path);
        return svg;
    }

    // ─── Smooth Scroll (for clicks) ───────────────────────────────────────────────

    let smoothScrollRAF = null;

    function smoothScrollTo(targetY, duration = CFG.clickScrollDuration) {
        // Stop any ongoing smooth scroll
        if (smoothScrollRAF) {
            cancelAnimationFrame(smoothScrollRAF);
            smoothScrollRAF = null;
        }

        const startY = window.scrollY || window.pageYOffset;
        const distance = targetY - startY;

        // Already at target
        if (Math.abs(distance) < 1) return;

        const startTime = performance.now();

        function animate(currentTime) {
            const elapsed = currentTime - startTime;
            const progress = Math.min(elapsed / duration, 1);
            const eased = easeInOutCubic(progress);

            const newY = startY + distance * eased;
            window.scrollTo(0, newY);

            if (progress < 1) {
                smoothScrollRAF = requestAnimationFrame(animate);
            } else {
                smoothScrollRAF = null;
            }
        }

        smoothScrollRAF = requestAnimationFrame(animate);
    }

    // ─── Continuous Scroll State ──────────────────────────────────────────────────

    const scrollState = {
        top: { interval: null, checkInterval: null },
        bottom: { interval: null, checkInterval: null }
    };

    function startScroll(which, button) {
        const state = scrollState[which];
        const direction = which === 'top' ? 'up' : 'down';

        // Already running
        if (state.interval) return;

        button.classList.add('nb-scrolling');

        // Cancel smooth scroll if running
        if (smoothScrollRAF) {
            cancelAnimationFrame(smoothScrollRAF);
            smoothScrollRAF = null;
        }

        // Main scroll interval
        state.interval = setInterval(() => {
            const scrollY = window.scrollY;
            const docHeight = getDocHeight();
            const windowHeight = window.innerHeight;
            const maxScroll = docHeight - windowHeight;

            // Boundary checks
            if (direction === 'up' && scrollY <= 0) {
                stopScroll(which, button);
                return;
            }

            if (direction === 'down' && scrollY >= maxScroll - 1) {
                stopScroll(which, button);
                return;
            }

            // Perform scroll
            const delta = direction === 'up' ? -CFG.hoverScrollSpeed : CFG.hoverScrollSpeed;
            window.scrollBy(0, delta);
        }, CFG.hoverScrollInterval);

        // Position check interval - ensures scroll stops when mouse leaves
        state.checkInterval = setInterval(() => {
            const rect = button.getBoundingClientRect();
            const mouseX = window.nb_lastMouseX || 0;
            const mouseY = window.nb_lastMouseY || 0;

            const isOutside = (
                mouseX < rect.left ||
                mouseX > rect.right ||
                mouseY < rect.top ||
                mouseY > rect.bottom
            );

            if (isOutside) {
                stopScroll(which, button);
            }
        }, 50);
    }

    function stopScroll(which, button) {
        const state = scrollState[which];

        if (state.interval) {
            clearInterval(state.interval);
            state.interval = null;
        }

        if (state.checkInterval) {
            clearInterval(state.checkInterval);
            state.checkInterval = null;
        }

        button.classList.remove('nb-scrolling');
    }

    function stopAllScrolling(topButton, bottomButton) {
        stopScroll('top', topButton);
        stopScroll('bottom', bottomButton);
    }

    // ─── Track mouse position globally ────────────────────────────────────────────

    window.nb_lastMouseX = 0;
    window.nb_lastMouseY = 0;

    document.addEventListener('mousemove', (e) => {
        window.nb_lastMouseX = e.clientX;
        window.nb_lastMouseY = e.clientY;
    }, { passive: true, capture: true });

    // ─── Button Visibility ────────────────────────────────────────────────────────

    function updateButtonVisibility(topButton, bottomButton) {
        const scrollY = window.scrollY;
        const maxScroll = getDocHeight() - window.innerHeight;

        if (scrollY > CFG.scrollThresholdTop) {
            topButton.classList.add('nb-visible');
        } else {
            topButton.classList.remove('nb-visible');
        }

        if (scrollY < maxScroll - CFG.scrollThresholdBottom) {
            bottomButton.classList.add('nb-visible');
        } else {
            bottomButton.classList.remove('nb-visible');
        }
    }

    // ─── Event Handlers ───────────────────────────────────────────────────────────

    function attachEvents(topButton, bottomButton) {

        // Click handlers - FIXED: No stopPropagation, check if currently scrolling
        topButton.addEventListener('click', (e) => {
            e.preventDefault();
            // Stop hover scroll if running
            stopScroll('top', topButton);
            // Jump to top
            smoothScrollTo(0);
        });

        bottomButton.addEventListener('click', (e) => {
            e.preventDefault();
            // Stop hover scroll if running
            stopScroll('bottom', bottomButton);
            // Jump to bottom
            const target = getDocHeight() - window.innerHeight;
            smoothScrollTo(target);
        });

        // Hover handlers
        const startTop = () => startScroll('top', topButton);
        const stopTop = () => stopScroll('top', topButton);
        const startBottom = () => startScroll('bottom', bottomButton);
        const stopBottom = () => stopScroll('bottom', bottomButton);

        // Use both mouseenter/mouseleave and mouseover/mouseout for redundancy
        topButton.addEventListener('mouseenter', startTop, true);
        topButton.addEventListener('mouseover', startTop, true);
        topButton.addEventListener('mouseleave', stopTop, true);
        topButton.addEventListener('mouseout', stopTop, true);

        bottomButton.addEventListener('mouseenter', startBottom, true);
        bottomButton.addEventListener('mouseover', startBottom, true);
        bottomButton.addEventListener('mouseleave', stopBottom, true);
        bottomButton.addEventListener('mouseout', stopBottom, true);

        // Global safety nets
        document.addEventListener('mouseleave', () => {
            stopAllScrolling(topButton, bottomButton);
        }, true);

        window.addEventListener('blur', () => {
            stopAllScrolling(topButton, bottomButton);
        });

        // Scroll listener
        let rafPending = false;
        window.addEventListener('scroll', () => {
            if (!rafPending) {
                rafPending = true;
                requestAnimationFrame(() => {
                    updateButtonVisibility(topButton, bottomButton);
                    rafPending = false;
                });
            }
        }, { passive: true });

        // Resize listener
        window.addEventListener('resize', () => {
            updateButtonVisibility(topButton, bottomButton);
        }, { passive: true });

        // Keyboard shortcuts
        document.addEventListener('keydown', (e) => {
            if (e.ctrlKey || e.metaKey) {
                if (e.key === 'Home') {
                    e.preventDefault();
                    smoothScrollTo(0);
                }
                if (e.key === 'End') {
                    e.preventDefault();
                    smoothScrollTo(getDocHeight() - window.innerHeight);
                }
            }
        });
    }

    // ─── Initialize ───────────────────────────────────────────────────────────────

    function init() {
        if (document.getElementById('nb-top')) return;

        const styleEl = document.createElement('style');
        styleEl.id = 'nb-styles';
        styleEl.textContent = CSS;
        (document.head || document.documentElement).appendChild(styleEl);

        const topButton = document.createElement('button');
        topButton.id = 'nb-top';
        topButton.className = 'nb-button';
        topButton.setAttribute('aria-label', 'Scroll to top');
        topButton.setAttribute('title', 'Click: jump to top • Hover: scroll up');
        topButton.appendChild(createSVGIcon('M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z'));

        const bottomButton = document.createElement('button');
        bottomButton.id = 'nb-bottom';
        bottomButton.className = 'nb-button';
        bottomButton.setAttribute('aria-label', 'Scroll to bottom');
        bottomButton.setAttribute('title', 'Click: jump to bottom • Hover: scroll down');
        bottomButton.appendChild(createSVGIcon('M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z'));

        const root = document.body || document.documentElement;
        root.appendChild(topButton);
        root.appendChild(bottomButton);

        attachEvents(topButton, bottomButton);

        requestAnimationFrame(() => updateButtonVisibility(topButton, bottomButton));
    }

    if (document.body) {
        init();
    } else {
        const observer = new MutationObserver(() => {
            if (document.body) {
                observer.disconnect();
                init();
            }
        });
        observer.observe(document.documentElement, { childList: true });
    }

})();