Scrollbar Customizer

Fix sites w/white scroll bars on a light theme, vice versa!

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Scrollbar Customizer
// @namespace    https://greasyfork.org/en/users/922168-mark-zinzow
// @version      11.1
// @description  Fix sites w/white scroll bars on a light theme, vice versa!
// @author       Mark Zinzow
// @match        *://*/*
// @exclude      *://chromewebstore.google.com/*
// @exclude      chrome://*
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @run-at       document-start
// @license MIT
// ==/UserScript==
/* jshint esversion: 9 */
/* eslint-disable no-multi-spaces */

(function() {
    'use strict';

    // ====================================================================
    // 1. GLOBAL SETTINGS & SITE CONFIG
    // ====================================================================
    const DEFAULT_CONFIG = {
        enabled: false, // Default is OFF, so no green bars on fresh sites!
        width: 16,
        outlineWidth: 2,
        outlineColor: '#00FF00',
        thumbColor: 'transparent',
        trackColor: 'transparent'
    };

    const currentDomain = window.location.hostname;
    
    let storage = GM_getValue('scrollbarConfig_v11', {});
    let siteConfig = storage[currentDomain] || { ...DEFAULT_CONFIG };
    let globalTheme = GM_getValue('globalUITheme', 'dark');

    // ====================================================================
    // 2. CSS ENGINE (Scoped)
    // ====================================================================
    // FIX: We preface every rule with [data-usb-enabled="true"]
    // This guarantees the CSS is dormant until JS activates it.

    const cssSkeleton = `
        :root {
            /* Variables still exist but do nothing unless used */
            --usb-width: 16px;
            --usb-outline-width: 2px;
            --usb-outline-color: #00FF00;
            --usb-thumb-color: transparent;
            --usb-track-color: transparent;
        }

        /* FIREFOX & STANDARD: Only apply if enabled */
        [data-usb-enabled="true"] * {
            scrollbar-width: auto !important; 
            scrollbar-color: var(--usb-outline-color) var(--usb-track-color) !important;
        }

        /* WEBKIT: Chrome, Edge, Safari */
        
        /* Main Window */
        [data-usb-enabled="true"] ::-webkit-scrollbar {
            display: block !important;
            width: var(--usb-width) !important;
            height: var(--usb-width) !important;
            background: var(--usb-track-color) !important;
        }
        
        /* Internal Divs */
        [data-usb-enabled="true"] *::-webkit-scrollbar {
            display: block !important;
            width: var(--usb-width) !important;
            height: var(--usb-width) !important;
            background: var(--usb-track-color) !important;
        }

        /* Track */
        [data-usb-enabled="true"] ::-webkit-scrollbar-track, 
        [data-usb-enabled="true"] *::-webkit-scrollbar-track {
            background: var(--usb-track-color) !important;
        }

        /* Thumb */
        [data-usb-enabled="true"] ::-webkit-scrollbar-thumb, 
        [data-usb-enabled="true"] *::-webkit-scrollbar-thumb {
            background-color: var(--usb-thumb-color) !important;
            background-clip: padding-box !important;
            border: var(--usb-outline-width) solid var(--usb-outline-color) !important;
            border-radius: 0px !important;
        }

        /* Hover */
        [data-usb-enabled="true"] ::-webkit-scrollbar-thumb:hover, 
        [data-usb-enabled="true"] *::-webkit-scrollbar-thumb:hover {
            background-color: var(--usb-outline-color) !important; 
            border-color: var(--usb-outline-color) !important;
        }

        /* Corner */
        [data-usb-enabled="true"] ::-webkit-scrollbar-corner, 
        [data-usb-enabled="true"] *::-webkit-scrollbar-corner {
            background: var(--usb-track-color) !important;
        }
    `;

    GM_addStyle(cssSkeleton);

    function updatePageVariables(config) {
        const root = document.documentElement;
        
        // 1. The Master Switch (Fixes the Bug)
        // If disabled, we remove the attribute, and the CSS above stops matching.
        if (config.enabled) {
            root.setAttribute('data-usb-enabled', 'true');
        } else {
            root.removeAttribute('data-usb-enabled');
            return; // Stop here, no need to update variables if disabled
        }

        // 2. Update Variables
        root.style.setProperty('--usb-width', config.width + 'px');
        root.style.setProperty('--usb-outline-width', config.outlineWidth + 'px');
        root.style.setProperty('--usb-outline-color', config.outlineColor);
        root.style.setProperty('--usb-thumb-color', config.thumbColor);
        root.style.setProperty('--usb-track-color', config.trackColor);
    }

    // Apply immediately on load
    updatePageVariables(siteConfig);

    function saveSiteConfig(newConfig) {
        siteConfig = newConfig;
        storage[currentDomain] = siteConfig;
        GM_setValue('scrollbarConfig_v11', storage);
        updatePageVariables(siteConfig);
    }

    // ====================================================================
    // 3. MENU "PANIC BUTTONS"
    // ====================================================================
    
    GM_registerMenuCommand("⚡ Force: Black on White", () => {
        saveSiteConfig({
            enabled: true,
            width: 24,
            outlineWidth: 0,
            outlineColor: '#000000',
            thumbColor: '#000000',
            trackColor: '#FFFFFF'
        });
    });

    GM_registerMenuCommand("⚡ Force: White on Black", () => {
        saveSiteConfig({
            enabled: true,
            width: 24,
            outlineWidth: 0,
            outlineColor: '#FFFFFF',
            thumbColor: '#FFFFFF',
            trackColor: '#000000'
        });
    });

    GM_registerMenuCommand("⚙️ Customize Scrollbars (UI)", createUI);

    // ====================================================================
    // 4. UI BUILDER
    // ====================================================================
    function createEl(tag, styles = {}, parent = null) {
        const el = document.createElement(tag);
        for (const [key, value] of Object.entries(styles)) {
            el.style[key] = value;
        }
        if (parent) parent.appendChild(el);
        return el;
    }

    const PALETTE_COLORS = ['#000000', '#FFFFFF', '#808080', '#C0C0C0', '#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#00FFFF', '#FF00FF'];

    function createControlGroup(labelText, value, parent, isColor = false, isTransparentSupported = false) {
        const wrapper = createEl('div', { marginBottom: '12px', paddingBottom: '5px', borderBottom: '1px solid var(--border)' }, parent);
        const header = createEl('div', { display: 'flex', justifyContent: 'space-between', marginBottom: '5px' }, wrapper);
        createEl('span', { fontSize: '12px', color: 'var(--text-dim)' }, header).textContent = labelText;

        let input, chkTransparent;

        if (isColor) {
            const controls = createEl('div', { display: 'flex', gap: '8px', alignItems: 'center' }, header);
            input = createEl('input', { width: '40px', height: '25px', border: '1px solid #777', cursor: 'pointer', padding: '0', background: 'none' }, controls);
            input.type = 'color';
            input.title = "Open System Picker (Eye Dropper)";
            input.value = (value === 'transparent') ? '#000000' : value;

            if (isTransparentSupported) {
                const lbl = createEl('label', { fontSize: '10px', display: 'flex', alignItems: 'center', gap: '3px', color: 'var(--text)' }, controls);
                chkTransparent = createEl('input', {}, lbl);
                chkTransparent.type = 'checkbox';
                chkTransparent.checked = (value === 'transparent');
                createEl('span', {}, lbl).textContent = 'None';
                const toggle = () => { input.disabled = chkTransparent.checked; input.style.opacity = chkTransparent.checked ? '0.3' : '1'; };
                chkTransparent.addEventListener('change', toggle);
                toggle();
            }
            const palContainer = createEl('div', { display: 'flex', gap: '4px', marginBottom: '8px', flexWrap: 'wrap' }, wrapper);
            PALETTE_COLORS.forEach(color => {
                const swatch = createEl('div', { width: '18px', height: '18px', background: color, border: '1px solid #555', cursor: 'pointer' }, palContainer);
                swatch.onclick = () => {
                    input.value = color;
                    if (chkTransparent) { chkTransparent.checked = false; input.disabled = false; input.style.opacity = '1'; }
                    input.dispatchEvent(new Event('input'));
                };
            });
        } else {
            input = createEl('input', { width: '50px', background: 'var(--input-bg)', color: 'var(--text)', border: '1px solid #777', padding: '2px' }, header);
            input.type = 'number';
            input.value = value;
        }
        return { input, chkTransparent };
    }

    let uiContainer = null;

    function createUI() {
        if (uiContainer) { uiContainer.remove(); uiContainer = null; return; }

        const host = createEl('div', { position: 'fixed', top: '10px', right: '10px', zIndex: '2147483647', fontFamily: 'sans-serif' });
        document.body.appendChild(host);
        uiContainer = host;
        const shadow = host.attachShadow({ mode: 'open' });

        const themeStyles = globalTheme === 'dark' 
            ? { bg: '#1a1a1a', text: '#ffffff', textDim: '#cccccc', inputBg: '#333', border: '#444' }
            : { bg: '#f4f4f4', text: '#000000', textDim: '#333333', inputBg: '#ffffff', border: '#cccccc' };

        const panel = createEl('div', { 
            background: themeStyles.bg, color: themeStyles.text, 
            padding: '15px', border: `2px solid ${themeStyles.border}`, borderRadius: '8px', width: '280px', 
            boxShadow: '0 10px 40px rgba(0,0,0,0.5)',
            '--text': themeStyles.text, '--text-dim': themeStyles.textDim, '--input-bg': themeStyles.inputBg, '--border': themeStyles.border 
        }, shadow);

        const headRow = createEl('div', { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '10px', borderBottom: `1px solid ${themeStyles.border}`, paddingBottom: '5px' }, panel);
        createEl('h3', { margin: '0', fontSize: '14px', textTransform: 'uppercase', color: 'var(--text-dim)' }, headRow).textContent = 'Scrollbar Config';
        
        const btnTheme = createEl('button', { background: 'none', border: 'none', cursor: 'pointer', fontSize: '16px' }, headRow);
        btnTheme.textContent = globalTheme === 'dark' ? '☀️' : '🌙';
        btnTheme.title = "Switch Panel Theme";
        btnTheme.onclick = () => {
            globalTheme = globalTheme === 'dark' ? 'light' : 'dark';
            GM_setValue('globalUITheme', globalTheme);
            host.remove(); uiContainer = null; createUI();
        };

        const presetGrid = createEl('div', { display: 'flex', gap: '8px', marginBottom: '15px', justifyContent: 'space-between' }, panel);
        const presets = [
            { name: 'Outline', w: 16, out: 2, outC: '#00FF00', thC: 'transparent', trC: 'transparent' },
            { name: 'Neon',    w: 20, out: 0, outC: '#FFFF00', thC: '#FFFF00',     trC: '#000000' },
            { name: 'Classic', w: 16, out: 1, outC: '#808080', thC: '#C0C0C0',     trC: '#FFFFFF' },
            { name: 'BnW',     w: 18, out: 0, outC: '#000000', thC: '#000000',     trC: '#FFFFFF' },
            { name: 'WnB',     w: 18, out: 0, outC: '#FFFFFF', thC: '#FFFFFF',     trC: '#000000' },
        ];

        presets.forEach(p => {
            const btn = createEl('button', { flex: '1', height: '35px', padding: '0', background: '#333', border: '1px solid #555', cursor: 'pointer', borderRadius: '4px', position: 'relative', overflow: 'hidden' }, presetGrid);
            btn.title = p.name;
            createEl('div', { width: '100%', height: '100%', background: '#444' }, btn);
            const track = createEl('div', { position: 'absolute', top: '2px', bottom: '2px', right: '4px', width: '10px', background: (p.trC === 'transparent') ? 'none' : p.trC, border: (p.trC === 'transparent') ? '1px dashed #666' : 'none' }, btn);
            createEl('div', { position: 'absolute', top: '20%', height: '40%', left: '0', right: '0', backgroundColor: (p.thC === 'transparent') ? 'transparent' : p.thC, border: `${p.out}px solid ${p.outC}`, boxSizing: 'border-box' }, track);
            
            btn.onclick = () => {
                inpEnable.checked = true;
                ctrlWidth.input.value = p.w;
                ctrlOutlineW.input.value = p.out;
                ctrlOutlineC.input.value = p.outC;
                if (p.thC === 'transparent') { ctrlThumbC.chkTransparent.checked = true; ctrlThumbC.input.disabled = true; } 
                else { ctrlThumbC.chkTransparent.checked = false; ctrlThumbC.input.disabled = false; ctrlThumbC.input.value = p.thC; }
                if (p.trC === 'transparent') { ctrlTrackC.chkTransparent.checked = true; ctrlTrackC.input.disabled = true; } 
                else { ctrlTrackC.chkTransparent.checked = false; ctrlTrackC.input.disabled = false; ctrlTrackC.input.value = p.trC; }
                updateLive();
            };
        });

        const rowEnable = createEl('div', { marginBottom: '15px', display: 'flex', alignItems: 'center', gap: '10px', color: 'var(--text)' }, panel);
        const inpEnable = createEl('input', { transform: 'scale(1.2)' }, rowEnable);
        inpEnable.type = 'checkbox';
        inpEnable.checked = siteConfig.enabled;
        createEl('span', { fontWeight: 'bold' }, rowEnable).textContent = 'Enable Custom Scrollbar';

        const ctrlWidth = createControlGroup('Total Width (px)', siteConfig.width, panel);
        const ctrlOutlineW = createControlGroup('Outline Thickness (px)', siteConfig.outlineWidth, panel);
        const ctrlOutlineC = createControlGroup('Outline Color', siteConfig.outlineColor, panel, true, false);
        const ctrlThumbC = createControlGroup('Thumb Fill', siteConfig.thumbColor, panel, true, true);
        const ctrlTrackC = createControlGroup('Track Background', siteConfig.trackColor, panel, true, true);

        const btnSave = createEl('button', { width: '100%', padding: '10px', marginTop: '10px', background: '#00d26a', border: 'none', borderRadius: '4px', fontWeight: 'bold', cursor: 'pointer', color: '#000' }, panel);
        btnSave.textContent = 'Save Configuration';

        function getValues() {
            return {
                enabled: inpEnable.checked,
                width: ctrlWidth.input.value,
                outlineWidth: ctrlOutlineW.input.value,
                outlineColor: ctrlOutlineC.input.value,
                thumbColor: ctrlThumbC.chkTransparent.checked ? 'transparent' : ctrlThumbC.input.value,
                trackColor: ctrlTrackC.chkTransparent.checked ? 'transparent' : ctrlTrackC.input.value
            };
        }
        function updateLive() { updatePageVariables(getValues()); }

        panel.addEventListener('input', updateLive);
        panel.addEventListener('change', updateLive);
        btnSave.addEventListener('click', () => { saveSiteConfig(getValues()); host.remove(); uiContainer = null; });
    }
})();