Grok Theme Toggle

Adds a Light/Dark mode toggle at the bottom left

Versión del día 25/06/2025. Echa un vistazo a la versión más reciente.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         Grok Theme Toggle
// @namespace    nisc
// @version      2025.06.24-A
// @description  Adds a Light/Dark mode toggle at the bottom left
// @homepageURL  https://github.com/nisc/grok-userscripts/
// @author       nisc
// @match        https://grok.com/*
// @icon         https://grok.com/images/favicon-light.png
// @run-at       document-end
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    const CONFIG = {
        THEME: {
            KEY: 'forcedTheme',
            MODES: {
                DARK: 'dark',
                LIGHT: 'light'
            },
            DEFAULT: 'dark',
            COLORS: {
                dark: { bg: 'var(--surface-l4, #3c3c3c)', text: 'var(--fg-primary, #fff)', border: 'var(--border-l1, #444)' },
                light: { bg: 'var(--surface-l4, #f8f8f8)', text: 'var(--fg-primary, #000)', border: 'var(--border-l1, #ddd)' }
            },
            ICONS: {
                dark: '🌕',
                light: '🌑'
            }
        },
        UI: {
            POSITION_MARGIN: 20,
            BUTTON_STYLES: {
                position: 'fixed',
                zIndex: '40',
                width: '36px',
                height: '36px',
                padding: '4px',
                border: '1px solid var(--border-color, #333)',
                borderRadius: '50%',
                cursor: 'pointer',
                fontSize: '12px',
                boxShadow: '0 1px 2px rgba(0,0,0,0.1)',
                transition: 'all 0.15s ease',
                userSelect: 'none',
                display: 'flex',
                alignItems: 'center',
                justifyContent: 'center'
            },
            LIGHT_MODE_STYLES: `
                .light .text-xs.font-semibold {
                    color: #000 !important;
                }
            `
        }
    };

    // Track current theme state
    let currentTheme = localStorage.getItem(CONFIG.THEME.KEY) || CONFIG.THEME.DEFAULT;

    /**
     * Applies theme to document root only if it differs from current state
     * Handles both class names and color-scheme property
     */
    function applyTheme(theme) {
        const htmlElement = document.documentElement;
        const isDark = htmlElement.classList.contains(CONFIG.THEME.MODES.DARK);
        const isLight = htmlElement.classList.contains(CONFIG.THEME.MODES.LIGHT);

        if (theme === CONFIG.THEME.MODES.DARK && !isDark) {
            htmlElement.classList.add(CONFIG.THEME.MODES.DARK);
            htmlElement.classList.remove(CONFIG.THEME.MODES.LIGHT);
            htmlElement.style.setProperty('color-scheme', CONFIG.THEME.MODES.DARK, 'important');
        } else if (theme === CONFIG.THEME.MODES.LIGHT && !isLight) {
            htmlElement.classList.add(CONFIG.THEME.MODES.LIGHT);
            htmlElement.classList.remove(CONFIG.THEME.MODES.DARK);
            htmlElement.style.setProperty('color-scheme', CONFIG.THEME.MODES.LIGHT, 'important');
        }
    }

    // Create and inject custom style element
    const styleElement = document.createElement('style');
    document.head.appendChild(styleElement);

    /**
     * Updates custom styles based on current theme
     * Primarily handles special cases for light mode
     */
    function updateStyles(theme) {
        styleElement.textContent = theme === CONFIG.THEME.MODES.LIGHT ? CONFIG.UI.LIGHT_MODE_STYLES : '';
    }

    /**
     * Toggles between light and dark themes
     * Updates localStorage, applies theme, and updates UI
     */
    function toggleTheme() {
        currentTheme = currentTheme === CONFIG.THEME.MODES.DARK ?
            CONFIG.THEME.MODES.LIGHT : CONFIG.THEME.MODES.DARK;
        localStorage.setItem(CONFIG.THEME.KEY, currentTheme);
        applyTheme(currentTheme);
        updateStyles(currentTheme);
        updateButtonAppearance();
    }

    /**
     * Updates toggle button appearance based on current theme
     * Changes icon, background, text, and border colors
     */
    function updateButtonAppearance() {
        const themeColors = CONFIG.THEME.COLORS[currentTheme];
        toggleButton.textContent = CONFIG.THEME.ICONS[currentTheme];
        toggleButton.style.backgroundColor = themeColors.bg;
        toggleButton.style.color = themeColors.text;
        toggleButton.style.borderColor = themeColors.border;
    }

    // Create and configure theme toggle button
    const toggleButton = document.createElement('button');
    Object.assign(toggleButton.style, CONFIG.UI.BUTTON_STYLES);
    updateButtonAppearance();

    // Add hover effects
    toggleButton.addEventListener('mouseenter', () => {
        toggleButton.style.transform = 'scale(1.1)';
        toggleButton.style.boxShadow = '0 6px 16px rgba(0,0,0,0.4)';
    });
    toggleButton.addEventListener('mouseleave', () => {
        toggleButton.style.transform = 'scale(1)';
        toggleButton.style.boxShadow = '0 4px 12px rgba(0,0,0,0.3)';
    });

    toggleButton.addEventListener('click', toggleTheme);
    document.body.appendChild(toggleButton);

    // Function to update position based on JX button location
    function updatePositionRelativeToJX() {
        const jxButton = document.querySelector('button[id^="radix-"] span span[class*="bg-surface-l4"]')?.closest('div.absolute');
        if (jxButton) {
            const jxRect = jxButton.getBoundingClientRect();
            const jxLeft = jxRect.left;
            const jxBottom = window.innerHeight - jxRect.bottom;

            // Position our button above JX button with 10px gap
            toggleButton.style.left = `${jxLeft + 2}px`; // Center align (36px vs 40px = 2px offset)
            toggleButton.style.bottom = `${jxBottom + jxRect.height + 10}px`;
        } else {
            // Fallback position if JX button not found
            toggleButton.style.left = '8px';
            toggleButton.style.bottom = '100px';
        }
    }

    // Initial positioning
    updatePositionRelativeToJX();

    // Initialize theme
    applyTheme(currentTheme);
    updateStyles(currentTheme);

    // Watch for external theme changes and maintain chosen theme
    const themeObserver = new MutationObserver(mutations => {
        mutations.forEach(mutation => {
            if (mutation.type === 'attributes' &&
                (mutation.attributeName === 'class' || mutation.attributeName === 'style')) {
                applyTheme(currentTheme);
            }
        });
    });

    themeObserver.observe(document.documentElement, {
        attributes: true,
        attributeFilter: ['class', 'style']
    });

    // Throttle position updates to prevent infinite loops
    let positionUpdateTimeout;
    function throttledPositionUpdate() {
        if (positionUpdateTimeout) return;
        positionUpdateTimeout = setTimeout(() => {
            updatePositionRelativeToJX();
            positionUpdateTimeout = null;
        }, 16); // ~60fps for smoother following
    }

    // Watch for JX button movement and follow it
    const positionObserver = new MutationObserver((mutations) => {
        // Only update if mutations don't involve our theme button
        const shouldUpdate = mutations.some(mutation => {
            return mutation.target !== toggleButton &&
                !toggleButton.contains(mutation.target);
        });

        if (shouldUpdate) {
            throttledPositionUpdate();
        }
    });

    // Observe for JX button changes with broader scope
    positionObserver.observe(document.body, {
        childList: true,
        subtree: true,
        attributes: true,
        attributeFilter: ['style', 'class']
    });

    // Also update position on window events
    window.addEventListener('resize', throttledPositionUpdate);
    window.addEventListener('scroll', throttledPositionUpdate, { passive: true });
})();