Grok Theme Toggle

Adds a Light/Dark mode toggle at the bottom left

Versão de: 25/06/2025. Veja: a última versão.

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey 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 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         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 });
})();