Microsoft Loop Adjustable page width latest version (currently v1.0.0)

← Back to User Scripts

Script Content

// ==UserScript==
// @name         Microsoft Loop: Adjustable Width
// @namespace    https://www.timhilton.xyz/user-scripts
// @version      1.0.0
// @description  Adds drag handles either side of a Microsoft Loop page so you can make the centred content wider or narrower
// @author       Tim Hilton using Claude Code
// @match        https://loop.cloud.microsoft/*
// @grant        none
// @run-at       document-start
// ==/UserScript==

(function () {
    'use strict';

    const LOG_PREFIX = '[Microsoft Loop: Adjustable Width]';
    const STORAGE_KEY = 'tjh-loop-content-width';
    const MIN_WIDTH = 400;
    const FRAME_SELECTOR = '.scriptor-canvas-grid-layout > .scriptor-pageContainer > .scriptor-pageFrame';

    console.debug(`${LOG_PREFIX} Script initialized`);

    // Loop caps the page at max-width: var(--max-canvas-content-width, 920px) and centres it,
    // so setting the variable is enough to resize the content while keeping it centred
    const widthStyle = document.createElement('style');
    const baseStyle = document.createElement('style');
    baseStyle.textContent = `
        /* Loop sets 80px inline; a smaller gutter lets the content get closer to the edges */
        .scriptor-canvas-grid-layout > .scriptor-pageContainer {
            padding-left: 40px !important;
            padding-right: 40px !important;
        }
        .tjh-loop-width-handle {
            position: fixed;
            z-index: 1000;
            width: 8px;
            cursor: ew-resize;
            border-radius: 4px;
            opacity: 0;
            background: #8a8a8a;
            transition: opacity 0.15s;
            &:hover, &.dragging { opacity: 0.6; }
        }
    `;
    document.documentElement.append(baseStyle, widthStyle);

    function readStoredWidth() {
        try {
            return Number(localStorage.getItem(STORAGE_KEY)) || null;
        } catch {
            return null;
        }
    }

    function applyWidth(width) {
        widthStyle.textContent = width ? `:root { --max-canvas-content-width: ${width}px !important; }` : '';
    }

    applyWidth(readStoredWidth());

    let frame = null;
    const handles = ['left', 'right'].map((side) => {
        const handle = document.createElement('div');
        handle.className = 'tjh-loop-width-handle';
        handle.dataset.side = side;
        handle.title = 'Drag to change the page width. Double-click to reset.';
        handle.addEventListener('pointerdown', (e) => startDrag(e, handle));
        handle.addEventListener('dblclick', resetWidth);
        return handle;
    });

    function positionHandles() {
        if (!frame?.isConnected) {
            handles.forEach((h) => { h.style.display = 'none'; });
            return;
        }
        const frameRect = frame.getBoundingClientRect();
        const canvasRect = frame.closest('.scriptor-canvas-grid-layout').getBoundingClientRect();
        for (const handle of handles) {
            const x = handle.dataset.side === 'left' ? frameRect.left - 16 : frameRect.right + 8;
            Object.assign(handle.style, {
                display: 'block',
                left: `${x}px`,
                top: `${canvasRect.top}px`,
                height: `${canvasRect.height}px`,
            });
        }
    }

    function startDrag(e, handle) {
        e.preventDefault();
        handle.setPointerCapture(e.pointerId);
        handle.classList.add('dragging');
        const startX = e.clientX;
        const startWidth = frame.getBoundingClientRect().width;
        const container = frame.parentElement;
        const style = getComputedStyle(container);
        const maxWidth = container.clientWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight);
        const direction = handle.dataset.side === 'left' ? -1 : 1;
        console.debug(`${LOG_PREFIX} Drag started at width ${startWidth}px (max ${maxWidth}px)`);

        const onMove = (moveEvent) => {
            // Content is centred, so each edge moves by half the change in width
            const width = startWidth + 2 * direction * (moveEvent.clientX - startX);
            applyWidth(Math.round(Math.min(maxWidth, Math.max(MIN_WIDTH, width))));
            positionHandles();
        };
        const onUp = () => {
            handle.removeEventListener('pointermove', onMove);
            handle.classList.remove('dragging');
            const width = Math.round(frame.getBoundingClientRect().width);
            try {
                localStorage.setItem(STORAGE_KEY, width);
            } catch {
                console.debug(`${LOG_PREFIX} Could not save width`);
            }
            console.log(`${LOG_PREFIX} ✅ Width set to ${width}px`);
        };
        handle.addEventListener('pointermove', onMove);
        handle.addEventListener('pointerup', onUp, { once: true });
    }

    function resetWidth() {
        try {
            localStorage.removeItem(STORAGE_KEY);
        } catch {
            console.debug(`${LOG_PREFIX} Could not clear saved width`);
        }
        applyWidth(null);
        requestAnimationFrame(positionHandles);
        console.log(`${LOG_PREFIX} ✅ Width reset to Loop's default`);
    }

    const resizeObserver = new ResizeObserver(positionHandles);

    // ponytail: polls for the page frame, since Loop re-renders it when you switch pages; swap for a MutationObserver if 1s lag matters
    function checkFrame() {
        const current = document.querySelector(FRAME_SELECTOR);
        if (current !== frame) {
            console.debug(`${LOG_PREFIX} Page frame ${current ? 'found' : 'lost'}`);
            resizeObserver.disconnect();
            frame = current;
            if (frame) {
                resizeObserver.observe(frame);
                resizeObserver.observe(frame.parentElement);
            }
            positionHandles();
        }
        if (document.body && !handles[0].isConnected) {
            document.body.append(...handles);
        }
    }

    window.addEventListener('resize', positionHandles);
    setInterval(checkFrame, 1000);
    checkFrame();
})();