Overleaf Space Maximiser

(Requires Tampermonkey Legacy / MV2 on Chromium!) Auto-hide Overleaf top toolbar to maximise vertical space. Hover over that area to show it again. To optionally maximise horizontal space, you can optimise file tree/outline spacing and/or hide file outline to maximise horizontal space. Toggle with settings button in toolbar.

// ==UserScript==
// @name        Overleaf Space Maximiser
// @namespace   https://www.github.com/sjain882
// @author      sjain882 / shanie
// @match       https://www.overleaf.com/project/*
// @version     0.4.2
// @icon        https://www.google.com/s2/favicons?sz=64&domain=overleaf.com
// @description (Requires Tampermonkey Legacy / MV2 on Chromium!) Auto-hide Overleaf top toolbar to maximise vertical space. Hover over that area to show it again. To optionally maximise horizontal space, you can optimise file tree/outline spacing and/or hide file outline to maximise horizontal space. Toggle with settings button in toolbar.
// @homepageURL https://www.github.com/sjain882/Browser-Tweaks
// @supportURL  https://www.github.com/sjain882/Browser-Tweaks/issues
// @homepageURL https://www.github.com/sjain882/Browser-Tweaks/Userscripts
// @supportURL  https://www.github.com/sjain882/Browser-Tweaks/issues
// @require     https://openuserjs.org/src/libs/sizzle/GM_config.js
// @grant       GM_addStyle
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM.getValue
// @grant       GM.setValue
// @require     http://code.jquery.com/jquery-3.7.1.min.js
// @license MIT
// ==/UserScript==

/* globals $, GM_config */

(function () {
  "use strict";

  var $jqueryOverleafUserScript = jQuery.noConflict();
  // $j is now an alias to the jQuery function; creating the new alias is optional.

  var setIntervalFileTree;

  let gmc = new GM_config({
    id: "OverleafMaximiserConfig", // The id used for this instance of GM_config
    title: "Settings (WARNING: Saving will reload page!)", // Panel Title

    // Fields object
    fields: {

      // This is the id of the field
      HIDE_FILE_OUTLINE: {
        label: "Hide File Outline", // Appears next to field
        type: "checkbox", // Makes this setting a checkbox
        default: true, // Default value if user doesn't change it
      },

      OPTIMISE_FILE_TREE_SPACING: {
        label: "Optimise File Tree Spacing", 
        type: "checkbox", 
        default: true, 
      },

      FILE_TREE_FONT_SIZE: {
        label: "File Tree Font Size", 
        type: "text",
        default: "8", 
      },

      OPTIMISE_FILE_OUTLINE_SPACING: {
        label: "Optimise File Outline Spacing", 
        type: "checkbox", 
        default: true, 
      },

      FILE_OUTLINE_FONT_SIZE: {
        label: "File Outline Font Size", 
        type: "text", 
        default: "8", 
      }
    },

    events: {
      init: function () {
        // runs after initialization completes
        // override saved value
        this.set(
          "HIDE_FILE_OUTLINE",
          localStorage.getItem("ls_HIDE_FILE_OUTLINE") === "true"
        );
        this.set(
          "OPTIMISE_FILE_TREE_SPACING",
          localStorage.getItem("ls_OPTIMISE_FILE_TREE_SPACING") === "true"
        );
        this.set(
          "FILE_TREE_FONT_SIZE",
          localStorage.getItem("ls_FILE_TREE_FONT_SIZE") || "8"
        );
        this.set(
          "OPTIMISE_FILE_OUTLINE_SPACING",
          localStorage.getItem("ls_OPTIMISE_FILE_OUTLINE_SPACING") === "true"
        );
        this.set(
          "FILE_OUTLINE_FONT_SIZE",
          localStorage.getItem("ls_FILE_OUTLINE_FONT_SIZE") || "8"
        );

        optimiseFileTree();
        optimiseFileOutline();
        hideFileOutline();
        // gmc.open();
      },
      save: function () {
        localStorage.setItem(
          "ls_HIDE_FILE_OUTLINE",
          gmc.get("HIDE_FILE_OUTLINE")
        );
        localStorage.setItem(
          "ls_OPTIMISE_FILE_TREE_SPACING",
          gmc.get("OPTIMISE_FILE_TREE_SPACING")
        );
        localStorage.setItem(
          "ls_FILE_TREE_FONT_SIZE",
          gmc.get("FILE_TREE_FONT_SIZE")
        );
        localStorage.setItem(
          "ls_OPTIMISE_FILE_OUTLINE_SPACING",
          gmc.get("OPTIMISE_FILE_OUTLINE_SPACING")
        );
        localStorage.setItem(
          "ls_FILE_OUTLINE_FONT_SIZE",
          gmc.get("FILE_OUTLINE_FONT_SIZE")
        );
        optimiseFileTree();
        hideFileOutline();
        optimiseFileOutline();
        window.location.reload();
      },
    },
  });

  GM_addStyle(`
    nav.toolbar.toolbar-header {
      transition: transform 0.25s ease-in-out, opacity 0.25s ease-in-out;
      opacity: 1;
      z-index: 1000;
    }
    nav.toolbar.toolbar-header.toolbar-hidden {
      transform: translateY(-100%);
      opacity: 0;
      pointer-events: none;
      position: absolute !important; /* take it out of flex flow */
      top: 0;
      left: 0;
      right: 0;
    }

    .ide-react-body {
      flex: 1 1 auto !important; /* always expand to fill */
      transition: all 0.25s ease-in-out;
    }

  `);

  function makeButton() {
    const a = document.createElement("a");
    a.className =
      "d-inline-grid toolbar-header-upgrade-prompt btn btn-primary btn-sm";
    a.setAttribute("tabindex", "0");
    a.setAttribute("href", "#"); // prevents navigation
    a.setAttribute("role", "button");
    a.dataset.osmSettings = "1"; // marker so we don't duplicate
    const span = document.createElement("span");
    span.className = "button-content";
    span.setAttribute("aria-hidden", "false");
    span.textContent = "Overleaf Space Maximiser";
    a.appendChild(span);

    a.addEventListener("click", (ev) => {
      ev.preventDefault();
      try {
        gmc.open(); // open the GM_config UI from inside the userscript scope
      } catch (err) {
        console.error("Failed to open GM_config:", err);
        alert("Could not open settings UI — see console for details.");
      }
    });

    return a;
  }

  function insertIntoContainer(container) {
    if (!container) return false;
    // avoid duplicates
    if (container.querySelector('a[data-osm-settings="1"]')) return true;
    const btn = makeButton();

    // insert before the first "Upgrade" button if present, otherwise append
    const firstUpgrade = container.querySelector(
      "a.toolbar-header-upgrade-prompt"
    );
    if (firstUpgrade) container.insertBefore(btn, firstUpgrade);
    else container.appendChild(btn);

    return true;
  }

  function tryFindAndInsert() {
    // prefer targeting the nav by aria-label so it's less brittle
    const nav =
      document.querySelector('nav[aria-label="Project actions"]') ||
      document.querySelector("nav.toolbar.toolbar-header") ||
      document.querySelector("nav.toolbar-header") ||
      document.querySelector("nav.toolbar");
    if (!nav) return false;

    // direct-child selector to match your pasted markup:
    const container =
      nav.querySelector("div.d-flex.align-items-center") ||
      nav.querySelector(".d-flex.align-items-center");
    return insertIntoContainer(container);
  }

  // try immediately (in case element already present)
  if (!tryFindAndInsert()) {
    // element not found yet — observe DOM until the toolbar appears
    const mo = new MutationObserver((mutations, observer) => {
      if (tryFindAndInsert()) {
        observer.disconnect();
      }
    });
    mo.observe(document.documentElement || document.body, {
      childList: true,
      subtree: true,
    });
    // also set a fallback timeout to stop observing after 30s
    setTimeout(() => mo.disconnect(), 30000);
  }

  function optimiseFileTree() {
    if (gmc.get("OPTIMISE_FILE_TREE_SPACING")) {
      GM_addStyle(`
        #panel-file-tree > div > div.file-tree-inner {
          font-size: ${gmc.get("FILE_TREE_FONT_SIZE")}pt !important;
        }

        .item-name-button {
          padding-right: 0 !important;
        }
      `);
    }
  }

  function optimiseFileOutline() {
    if (gmc.get("OPTIMISE_FILE_OUTLINE_SPACING")) {
      GM_addStyle(`
        .outline-pane {
          font-size: ${gmc.get("FILE_OUTLINE_FONT_SIZE")}pt !important;
        }
      `);
    }
  }

  function collapsePanels() {
    const separator = document.querySelector(
      '[role="separator"][aria-controls="panel-file-tree"]'
    );
    if (!separator) return;

    // Force aria-valuenow to 0
    separator.setAttribute("aria-valuenow", "0");

    // Collapse the file tree panel
    const fileTreePanel = document.querySelector("#panel-file-tree");
    if (fileTreePanel) {
      fileTreePanel.style.flexBasis = "0px";
      fileTreePanel.style.height = "0px";
      fileTreePanel.style.minHeight = "0px";
      fileTreePanel.style.overflow = "hidden";
    }

    // Expand remaining panels if needed
    const otherPanels = document.querySelectorAll(
      '[data-panel-group-id=":r3:"] > div'
    );
    otherPanels.forEach((panel) => {
      if (panel !== separator && panel.id !== "panel-file-tree") {
        panel.style.flexGrow = "1";
      }
    });
  }

  function hideFileOutline() {
    if (gmc.get("HIDE_FILE_OUTLINE")) {
      GM_addStyle(`

            .outline-pane {
              position: absolute !important;
              width: 0 !important;
              height: 0 !important;
              overflow: hidden !important;
            }

            .outline-container {
              position: absolute !important;
              width: 0 !important;
              height: 0 !important;
              overflow: hidden !important;
            }

            .vertical-resize-handle {
              position: absolute !important;
              width: 0 !important;
              height: 0 !important;
              overflow: hidden !important;
            }

            .file-tree
            {
              height: 100% !important;
            }
            `);

      // Reapply every 500ms to override layout JS
      setIntervalFileTree = setInterval(collapsePanels, 500);
    }
  }

  function setupToolbarHider(toolbar) {
    if (!toolbar) return;

    let visible = false;

    // invisible hover zone at top
    const hoverZone = document.createElement("div");
    hoverZone.style.position = "fixed";
    hoverZone.style.top = "0";
    hoverZone.style.left = "0";
    hoverZone.style.width = "100%";
    hoverZone.style.height = "8px";
    hoverZone.style.zIndex = "2000";
    hoverZone.style.background = "transparent";
    document.body.appendChild(hoverZone);

    function showToolbar() {
      if (visible) return;
      toolbar.classList.remove("toolbar-hidden");
      toolbar.style.position = "relative"; // put back into flex flow
      visible = true;
    }

    function hideToolbar() {
      if (!visible) return;
      toolbar.classList.add("toolbar-hidden");
      visible = false;
    }

    // start hidden
    hideToolbar();

    hoverZone.addEventListener("mouseenter", showToolbar);
    toolbar.addEventListener("mouseleave", (ev) => {
      const rt = ev.relatedTarget;
      if (rt && (rt === hoverZone || hoverZone.contains(rt))) return;
      hideToolbar();
    });
  }

  const observer = new MutationObserver((_, obs) => {
    const toolbar = document.querySelector("nav.toolbar.toolbar-header");
    if (toolbar) {
      setupToolbarHider(toolbar);
      obs.disconnect();
    }
  });
  observer.observe(document.body, { childList: true, subtree: true });
})();