Makes the Ask Learn sidebar resizable and left TOC fixed width on learn.microsoft.com (only on >2K monitors)
// ==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();
})();