Adjustable Width v1.0.0
Adds drag handles either side of a Microsoft Loop page so you can make the centred content wider or narrower, remembering the width you choose.
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();
})();