您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds a Light/Dark mode toggle at the bottom left
// ==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 }); })();