Learn Microsoft - Resizable Sidebars

Makes the Ask Learn sidebar resizable and left TOC fixed width on learn.microsoft.com (only on >2K monitors)

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

You will need to install an extension such as Tampermonkey to install this script.

Tendrás que 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.

Tendrás que instalar una extensión como Tampermonkey antes de poder 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)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

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

// ==UserScript==
// @name         Learn Microsoft - Resizable Sidebars
// @namespace    http://tampermonkey.net/
// @version      2.0
// @description  Makes the Ask Learn sidebar resizable and left TOC fixed width on learn.microsoft.com (only on >2K monitors)
// @author       You
// @match        https://learn.microsoft.com/*
// @grant        GM_addStyle
// @run-at       document-idle
// ==/UserScript==

(function () {
    'use strict';

    // ─── Resolution thresholds (CSS pixels = usable screen space) ──────
    const MIN_SCREEN_WIDTH  = 1920;
    const MIN_SCREEN_HEIGHT = 1080;

    // Right sidebar (Ask Learn) settings
    const MIN_WIDTH     = 250;
    const MAX_WIDTH     = 900;
    const DEFAULT_WIDTH = 400;
    const STORAGE_KEY   = 'askLearnSidebarWidth';
    const EDGE_SIZE     = 3;

    // Left sidebar (TOC) settings
    const LEFT_MIN_WIDTH     = 300;
    const LEFT_MAX_WIDTH     = 400;
    const LEFT_DEFAULT_WIDTH = 280;
    const LEFT_STORAGE_KEY   = 'leftTocSidebarWidth';

    // ─── State tracking ────────────────────────────────────────────────
    let isActive          = false;
    let styleElement      = null;
    let observer          = null;
    let lastSidebarState  = null;

    // ─── Resolution check (CSS pixels only — ignores DPR) ─────────────
    function isAbove2K() {
        const w = window.screen.width;
        const h = window.screen.height;

        console.log(`[Resizable Sidebars] screen: ${w}×${h}`);

        // Only activate when BOTH dimensions exceed the threshold
        return w > MIN_SCREEN_WIDTH && h > MIN_SCREEN_HEIGHT;
    }

    // ─── CSS injection / removal ───────────────────────────────────────
    function injectStyles() {
        if (styleElement) return;

        styleElement = document.createElement('style');
        styleElement.id = 'resizable-sidebar-styles';
        styleElement.textContent = `
            #layout-body-flyout.resizable-enabled {
                position: relative !important;
            }

            #layout-body-flyout.resizable-enabled > * {
                width: 100% !important;
                min-width: 0 !important;
                max-width: none !important;
            }

            .sidebar-resize-handle {
                position: absolute !important;
                left: 0;
                top: 0;
                width: ${EDGE_SIZE}px;
                height: 100%;
                background: transparent;
                cursor: col-resize;
                z-index: 999999;
                pointer-events: none;
            }

            .sidebar-resize-handle.active {
                background: rgba(0, 120, 212, 0.4);
                pointer-events: auto;
            }

            main.layout-body-main {
                display: flex !important;
            }

            #layout-body-menu {
                flex: 0 0 ${LEFT_DEFAULT_WIDTH}px !important;
                width: ${LEFT_DEFAULT_WIDTH}px !important;
                min-width: ${LEFT_DEFAULT_WIDTH}px !important;
                max-width: ${LEFT_DEFAULT_WIDTH}px !important;
            }

            .layout-body-main > div:not(#layout-body-menu):not(section) {
                flex: 1 1 auto !important;
            }

            #left-container,
            #left-container.width-full,
            div#left-container.left-container.width-full {
                width: 100% !important;
                max-width: 100% !important;
            }
        `;
        document.head.appendChild(styleElement);
    }

    function removeStyles() {
        if (styleElement) {
            styleElement.remove();
            styleElement = null;
        }
    }

    // ─── Width helpers ─────────────────────────────────────────────────
    function getSavedWidth() {
        const w = parseInt(localStorage.getItem(STORAGE_KEY), 10);
        return w >= MIN_WIDTH && w <= MAX_WIDTH ? w : DEFAULT_WIDTH;
    }

    function saveWidth(w) {
        localStorage.setItem(STORAGE_KEY, String(w));
    }

    function getLeftSavedWidth() {
        const w = parseInt(localStorage.getItem(LEFT_STORAGE_KEY), 10);
        return w >= LEFT_MIN_WIDTH && w <= LEFT_MAX_WIDTH ? w : LEFT_DEFAULT_WIDTH;
    }

    function setWidth(flyout, width) {
        flyout.style.setProperty('width',     `${width}px`, 'important');
        flyout.style.setProperty('min-width', `${width}px`, 'important');
        flyout.style.setProperty('max-width', `${width}px`, 'important');
    }

    function clearWidth(flyout) {
        flyout.style.removeProperty('width');
        flyout.style.removeProperty('min-width');
        flyout.style.removeProperty('max-width');
    }

    function updateBodyGrid(isOpen) {
        if (document.body.classList.contains('layout-body')) {
            document.body.style.setProperty(
                'grid-template-columns',
                isOpen ? '1fr 1500px' : '1fr 500px',
                'important'
            );
        }
    }

    function clearBodyGrid() {
        if (document.body.classList.contains('layout-body')) {
            document.body.style.removeProperty('grid-template-columns');
        }
    }

    function isSidebarOpen() {
        const flyout = document.getElementById('layout-body-flyout');
        if (!flyout) return false;
        const style = window.getComputedStyle(flyout);
        return style.display !== 'none' && flyout.offsetWidth > 0;
    }

    // ─── Left sidebar ──────────────────────────────────────────────────
    function initLeftSidebar() {
        const menuSection = document.getElementById('layout-body-menu');
        if (!menuSection || menuSection.dataset.leftInit) return;

        menuSection.dataset.leftInit = 'true';
        const width = getLeftSavedWidth();

        menuSection.style.cssText = `
            flex: 0 0 ${width}px !important;
            width: ${width}px !important;
            min-width: ${width}px !important;
            max-width: ${width}px !important;
        `;

        const leftContainer = document.getElementById('left-container');
        if (leftContainer) {
            leftContainer.style.cssText = `
                width: 100% !important;
                max-width: 100% !important;
            `;
        }
    }

    function teardownLeftSidebar() {
        const menuSection = document.getElementById('layout-body-menu');
        if (menuSection) {
            delete menuSection.dataset.leftInit;
            menuSection.style.cssText = '';
        }

        const leftContainer = document.getElementById('left-container');
        if (leftContainer) {
            leftContainer.style.cssText = '';
        }
    }

    // ─── Right sidebar (resizable) ────────────────────────────────────
    function initResizable() {
        const flyout = document.getElementById('layout-body-flyout');
        if (!flyout || flyout.classList.contains('resizable-enabled')) return;

        flyout.classList.add('resizable-enabled');

        const handle = document.createElement('div');
        handle.className = 'sidebar-resize-handle';
        flyout.appendChild(handle);

        setWidth(flyout, getSavedWidth());

        let isResizing = false;
        let startX     = 0;
        let startWidth = 0;

        flyout._resizeMouseMove = (e) => {
            const rect = flyout.getBoundingClientRect();
            const nearEdge = e.clientX >= rect.left && e.clientX <= rect.left + EDGE_SIZE;
            handle.style.pointerEvents = nearEdge || isResizing ? 'auto' : 'none';
        };
        flyout.addEventListener('mousemove', flyout._resizeMouseMove);

        handle._pointerDown = (e) => {
            if (e.button !== 0) return;
            isResizing = true;
            startX     = e.clientX;
            startWidth = flyout.getBoundingClientRect().width;
            handle.classList.add('active');
            handle.setPointerCapture(e.pointerId);
            e.preventDefault();
        };
        handle.addEventListener('pointerdown', handle._pointerDown);

        flyout._docPointerMove = (e) => {
            if (!isResizing) return;
            const deltaX = startX - e.clientX;
            let newWidth = Math.round(startWidth + deltaX);
            newWidth = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, newWidth));
            setWidth(flyout, newWidth);
        };
        document.addEventListener('pointermove', flyout._docPointerMove);

        flyout._docPointerUp = () => {
            if (!isResizing) return;
            isResizing = false;
            handle.classList.remove('active');
            handle.style.pointerEvents = 'none';
            saveWidth(Math.round(flyout.getBoundingClientRect().width));
        };
        document.addEventListener('pointerup', flyout._docPointerUp);
    }

    function teardownResizable() {
        const flyout = document.getElementById('layout-body-flyout');
        if (!flyout) return;

        if (flyout._resizeMouseMove) {
            flyout.removeEventListener('mousemove', flyout._resizeMouseMove);
        }
        if (flyout._docPointerMove) {
            document.removeEventListener('pointermove', flyout._docPointerMove);
        }
        if (flyout._docPointerUp) {
            document.removeEventListener('pointerup', flyout._docPointerUp);
        }

        const handle = flyout.querySelector('.sidebar-resize-handle');
        if (handle) handle.remove();

        flyout.classList.remove('resizable-enabled');
        clearWidth(flyout);
    }

    // ─── Sidebar state ────────────────────────────────────────────────
    function checkSidebarState() {
        const isOpen = isSidebarOpen();
        if (lastSidebarState !== isOpen) {
            lastSidebarState = isOpen;
            updateBodyGrid(isOpen);
        }
    }

    // ─── Activate / deactivate ─────────────────────────────────────────
    function activate() {
        if (isActive) return;
        isActive = true;
        console.log('[Resizable Sidebars] Activating – high-res monitor detected');

        injectStyles();
        initResizable();
        initLeftSidebar();
        checkSidebarState();

        observer = new MutationObserver(() => {
            initResizable();
            initLeftSidebar();
            checkSidebarState();
        });
        observer.observe(document.body, {
            childList: true,
            subtree: true,
            attributes: true,
            attributeFilter: ['style', 'class'],
        });
    }

    function deactivate() {
        if (!isActive) return;
        isActive = false;
        console.log('[Resizable Sidebars] Deactivating – monitor is ≤ threshold');

        if (observer) {
            observer.disconnect();
            observer = null;
        }

        teardownResizable();
        teardownLeftSidebar();
        removeStyles();
        clearBodyGrid();
        lastSidebarState = null;
    }

    // ─── Resolution change watcher ─────────────────────────────────────
    function evaluateResolution() {
        if (isAbove2K()) {
            activate();
        } else {
            deactivate();
        }
    }

    // Check on all events that can signal a monitor change
    window.addEventListener('resize',             evaluateResolution);
    window.addEventListener('focus',              evaluateResolution);
    document.addEventListener('visibilitychange', evaluateResolution);

    // Poll every 2s as a safety net (screen props don't always fire events)
    setInterval(evaluateResolution, 2000);

    // Initial check
    evaluateResolution();
})();