From 1111b29f3e098ed1200f51fe94389fe8e18a7f53 Mon Sep 17 00:00:00 2001 From: abose Date: Sat, 4 Apr 2026 12:01:31 +0530 Subject: [PATCH 1/5] feat: collapse overflowing app menu items into hamburger menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the titlebar is too narrow to fit all menu items on one row, overflow items are now hidden and a hamburger button (☰) appears. Hovering hamburger entries shows flyout submenus with full menu contents including nested sub-submenus. Also dismisses all menus, context menus and popups on window blur. --- src/command/Menus.js | 275 +++++++++++++++++++++ src/styles/brackets_patterns_override.less | 58 +++++ 2 files changed, 333 insertions(+) diff --git a/src/command/Menus.js b/src/command/Menus.js index 7c7151e814..7e4a605ea6 100644 --- a/src/command/Menus.js +++ b/src/command/Menus.js @@ -1762,6 +1762,268 @@ define(function (require, exports, module) { return cmenu; } + /** + * Hamburger menu: when the titlebar is too narrow to fit all menu items on one row, + * overflow items are hidden and a hamburger button appears with a dropdown listing them. + */ + function _initHamburgerMenu() { + const $menubar = $("#titlebar .nav"); + const $hamburger = $(``); + $menubar.append($hamburger); + const $hamburgerDropdown = $hamburger.find(".dropdown-menu"); + const $hamburgerToggle = $hamburger.find(".hamburger-toggle"); + let _activeSubmenuId = null; + + function _resetMenuItemStyles($menuItem) { + const menu = menuMap[$menuItem.attr("id")]; + if (menu) { + menu.closeSubMenu(); + } + $menuItem.removeClass("open").css({ + display: "none", + position: "", + visibility: "", + pointerEvents: "", + width: "", + height: "", + overflow: "" + }); + $menuItem.find("> .dropdown-menu").css({ + display: "", + visibility: "", + pointerEvents: "", + position: "", + top: "", + left: "", + margin: "" + }); + } + + function _closeHamburgerSubmenus() { + $hamburgerDropdown.find(".hamburger-submenu-open").removeClass("hamburger-submenu-open"); + // Reset the active flyout menu item + if (_activeSubmenuId) { + _resetMenuItemStyles($(`#${_activeSubmenuId}`)); + _activeSubmenuId = null; + } + // Safety: also reset any overflow menu items that might still have + // inline styles from a flyout that wasn't properly closed + $menubar.children("li.dropdown:not(.hamburger-menu)").each(function () { + const $item = $(this); + if ($item.css("display") !== "none" && $item.find("> .dropdown-menu").css("position") === "fixed") { + _resetMenuItemStyles($item); + } + }); + } + + function _closeHamburger() { + $hamburger.removeClass("hamburger-open"); + _closeHamburgerSubmenus(); + } + + // Wire up hamburger click to toggle the dropdown + $hamburgerToggle.on("click", function (e) { + e.preventDefault(); + e.stopPropagation(); + const wasOpen = $hamburger.hasClass("hamburger-open"); + closeAll(); + _closeHamburger(); + if (!wasOpen) { + $hamburger.addClass("hamburger-open"); + } + }); + + // Close hamburger when clicking outside + $(document).on("mousedown", function (e) { + if (!$hamburger.hasClass("hamburger-open")) { + return; + } + // Check if click is inside hamburger + if ($(e.target).closest("#hamburger-menu").length) { + return; + } + // Check if click is inside the active flyout menu + if (_activeSubmenuId && $(e.target).closest(`#${_activeSubmenuId}`).length) { + return; + } + // Check if click is inside any open context menu (sub-submenus + // live in #context-menu-bar, not inside the flyout menu) + if ($(e.target).closest("#context-menu-bar .open").length) { + return; + } + _closeHamburger(); + }); + + // Wire up hamburger toggle mouseenter like other menus + $hamburgerToggle.on("mouseenter", function () { + _closeAllSubMenus(); + const $this = $(this); + if ($('#titlebar, #titlebar *').is(':focus')) { + $this.addClass('selected').focus(); + } else { + $this.addClass('selected'); + } + }); + $hamburgerToggle.on("mouseleave", function () { + $(this).removeClass('selected'); + }); + + // Close hamburger when ESC is pressed + $(document).on("keydown", function (e) { + if (e.key === "Escape" && $hamburger.hasClass("hamburger-open")) { + _closeHamburger(); + e.stopPropagation(); + } + }); + + // Close hamburger when window loses focus + $(window).on("blur", _closeHamburger); + + // Close hamburger when a menu item in a flyout is clicked. + // Use setTimeout so the command executes before we hide the menu. + $menubar.on("click", ".dropdown:not(.hamburger-menu) .menuAnchor", function () { + setTimeout(_closeHamburger, 0); + }); + + // Also close on beforeExecuteCommand (e.g. keyboard shortcuts while open) + CommandManager.on("beforeExecuteCommand", function () { + _closeHamburger(); + }); + + let _updateScheduled = false; + + function _updateHamburgerMenu() { + _updateScheduled = false; + // Don't re-layout while a flyout submenu is active - showing the + // hidden menu li triggers ResizeObserver which would reset everything + if (_activeSubmenuId) { + return; + } + _closeHamburgerSubmenus(); + const $items = $menubar.children("li.dropdown:not(.hamburger-menu)"); + // First, show all items and hide hamburger to measure natural layout + $items.css({display: "", position: "", visibility: "", pointerEvents: ""}); + $hamburger.hide(); + $hamburgerDropdown.empty(); + + if ($items.length === 0) { + return; + } + + const firstItemTop = $items.first()[0].offsetTop; + let overflowStartIndex = -1; + + for (let i = 0; i < $items.length; i++) { + if ($items[i].offsetTop > firstItemTop) { + overflowStartIndex = i; + break; + } + } + + if (overflowStartIndex === -1) { + // Everything fits on one row + return; + } + + // Show hamburger, then re-check what fits with hamburger visible + $hamburger.css("display", ""); + + // Re-measure: with hamburger visible, even more items might overflow + for (let i = 0; i < $items.length; i++) { + if ($items[i].offsetTop > firstItemTop) { + overflowStartIndex = i; + break; + } + } + + function _openFlyout($entry, menuId) { + if (_activeSubmenuId && _activeSubmenuId !== menuId) { + _closeHamburgerSubmenus(); + } + $hamburgerDropdown.find(".hamburger-submenu-open").removeClass("hamburger-submenu-open"); + $entry.addClass("hamburger-submenu-open"); + _activeSubmenuId = menuId; + + const $menuItem = $(`#${menuId}`); + // Add 'open' class so sub-submenus (ContextMenus) can open properly. + // Keep the li itself invisible and out of flow. + $menuItem.addClass("open").css({ + display: "block", + position: "absolute", + visibility: "hidden", + pointerEvents: "none", + width: "0", + height: "0", + overflow: "visible" + }); + + const $realDropdown = $menuItem.find("> .dropdown-menu"); + const entryRect = $entry[0].getBoundingClientRect(); + const hamburgerRect = $hamburgerDropdown[0].getBoundingClientRect(); + let flyoutLeft = hamburgerRect.right - 2; + if (flyoutLeft + 250 > window.innerWidth) { + flyoutLeft = hamburgerRect.left - $realDropdown.outerWidth() + 2; + } + $realDropdown.css({ + display: "block", + visibility: "visible", + pointerEvents: "auto", + position: "fixed", + top: entryRect.top + "px", + left: flyoutLeft + "px", + margin: "0" + }); + } + + // Hide overflowing items and add them to hamburger dropdown as nested flyouts + for (let i = overflowStartIndex; i < $items.length; i++) { + const $item = $($items[i]); + const menuId = $item.attr("id"); + const menuName = $item.find(".dropdown-toggle").text(); + $item.css("display", "none"); + + const $entry = $(`
  • + + ${_.escape(menuName)} + + +
  • `); + + $entry.on("mouseenter", function () { + _openFlyout($(this), menuId); + }); + + $hamburgerDropdown.append($entry); + } + } + + function _scheduleUpdate() { + if (!_updateScheduled) { + _updateScheduled = true; + requestAnimationFrame(_updateHamburgerMenu); + } + } + + // Observe titlebar resizes + const titlebar = document.getElementById("titlebar"); + if (window.ResizeObserver) { + const resizeObserver = new ResizeObserver(_scheduleUpdate); + resizeObserver.observe(titlebar); + } + $(window).on("resize", _scheduleUpdate); + + // Also update when menus are added/removed + exports.on(EVENT_MENU_ADDED, _scheduleUpdate); + + // Initial check + _scheduleUpdate(); + } + AppInit.htmlReady(function () { $('#titlebar').on('focusin', function () { KeyBindingManager.addGlobalKeydownHook(menuKeyboardNavigationHandler); @@ -1769,6 +2031,19 @@ define(function (require, exports, module) { $('#titlebar').on('focusout', function () { KeyBindingManager.removeGlobalKeydownHook(menuKeyboardNavigationHandler); }); + _initHamburgerMenu(); + + // Close all menus, context menus, and popups when window loses focus + $(window).on("blur", function () { + closeAll(); + // Close all context menus (editor, file tree, working set, etc.) + _.forEach(contextMenuMap, function (contextMenu) { + if (contextMenu.isOpen()) { + contextMenu.close(); + } + }); + PopUpManager.closeAllPopups(); + }); }); EventDispatcher.makeEventDispatcher(exports); diff --git a/src/styles/brackets_patterns_override.less b/src/styles/brackets_patterns_override.less index 5e6085c579..fb053c4128 100644 --- a/src/styles/brackets_patterns_override.less +++ b/src/styles/brackets_patterns_override.less @@ -211,6 +211,64 @@ a:focus { animation: none; } } + .nav .hamburger-menu { + float: left; + .hamburger-toggle, .hamburger-toggle:hover, .hamburger-toggle:focus { + padding: @menubar-top-padding @menubar-h-padding @menubar-bottom-padding; + border: 1px solid transparent; + font-size: 14px; + color: fadeout(@bc-menu-text, 25%); + background: transparent; + cursor: default; + outline: none; + .dark & { + color: fadeout(@dark-bc-menu-text, 25%); + background: transparent; + } + } + .hamburger-toggle:hover, &.hamburger-open .hamburger-toggle { + color: @bc-menu-text; + background: @bc-bg-highlight; + .dark & { + color: @dark-bc-menu-text; + background: @dark-bc-bg-highlight; + } + } + .hamburger-dropdown { + display: none; + min-width: 120px; + text-align: left; + } + &.hamburger-open .hamburger-dropdown { + display: block; + } + .hamburger-submenu-item { + a { + display: flex; + align-items: center; + padding: 2px 10px 0 6px; + white-space: nowrap; + cursor: default; + text-align: left; + } + .hamburger-submenu-arrow { + margin-left: auto; + padding-left: 12px; + font-size: 10px; + opacity: 0.7; + } + &.hamburger-submenu-open > a, + a:hover { + background: @bc-bg-highlight; + color: @bc-menu-text; + .dark & { + background: @dark-bc-bg-highlight; + color: @dark-bc-menu-text; + } + } + } + } + .title { float: none; display: inline; // must be an inline for JS to measure text size From 6d77634634355eef3f0b05b8a8f030a155b6bf65 Mon Sep 17 00:00:00 2001 From: abose Date: Sat, 4 Apr 2026 12:41:50 +0530 Subject: [PATCH 2/5] test: add hamburger menu integration tests Tests cover: hamburger existence, visibility when menus fit/overflow, dropdown open/close on click, flyout submenu on hover, restore on expand, close on ESC key, command execution from flyout menu items, and command execution from flyout submenu items. Uses awaitsForCondition for robust async assertions. --- test/spec/Menu-integ-test.js | 269 ++++++++++++++++++++++++++++++++++- 1 file changed, 268 insertions(+), 1 deletion(-) diff --git a/test/spec/Menu-integ-test.js b/test/spec/Menu-integ-test.js index fdd5140909..6e8454c9da 100644 --- a/test/spec/Menu-integ-test.js +++ b/test/spec/Menu-integ-test.js @@ -19,7 +19,7 @@ * */ -/*global describe, it, expect, beforeAll, afterAll*/ +/*global describe, it, expect, beforeAll, afterAll, afterEach*/ define(function (require, exports, module) { @@ -1151,5 +1151,272 @@ define(function (require, exports, module) { expect(element.getElementsByClassName("forced-hidden").length).toBe(0); }); }); + + describe("Hamburger Menu", function () { + + function getHamburger() { + return testWindow.$("#hamburger-menu"); + } + + function getHamburgerDropdown() { + return testWindow.$("#hamburger-menu .hamburger-dropdown"); + } + + function getHiddenMenuItems() { + return testWindow.$("#titlebar .nav > li.dropdown:not(.hamburger-menu)").filter(function () { + return testWindow.$(this).css("display") === "none"; + }); + } + + function forceNarrowTitlebar() { + // Shrink the .content container so the titlebar becomes narrow + const $content = testWindow.$(".content"); + $content.css({"right": "", "width": "200px"}); + $content[0].offsetHeight; + } + + function restoreTitlebar() { + const $content = testWindow.$(".content"); + $content.css({"left": "", "right": "", "width": ""}); + $content[0].offsetHeight; + } + + function awaitsForCondition(conditionFn, message, timeout) { + timeout = timeout || 5000; + return new Promise(function (resolve, reject) { + const startTime = Date.now(); + function check() { + if (conditionFn()) { + resolve(); + } else if (Date.now() - startTime > timeout) { + reject(new Error("Timed out waiting for: " + (message || "condition"))); + } else { + testWindow.requestAnimationFrame(check); + } + } + check(); + }); + } + + afterEach(function () { + // Clean up: close hamburger properly (clears _activeSubmenuId) + const $hamburger = getHamburger(); + if ($hamburger.hasClass("hamburger-open")) { + $hamburger.find(".hamburger-toggle").click(); + } + Menus.closeAll(); + restoreTitlebar(); + }); + + it("should exist in the DOM", function () { + const $hamburger = getHamburger(); + expect($hamburger.length).toBe(1); + expect($hamburger.find(".hamburger-toggle").length).toBe(1); + expect($hamburger.find(".hamburger-dropdown").length).toBe(1); + }); + + it("should be hidden when all menus fit on one row", async function () { + // The test window may have extra test menus from prior tests. + // Verify with a very wide container that hamburger hides. + const $content = testWindow.$(".content"); + const $main = $content.parent(); + $main.css("width", "4000px"); + $content.css({"left": "0", "right": "0", "position": "relative", "width": "4000px"}); + $content[0].offsetHeight; + await awaitsForCondition(function () { + return getHamburger().is(":hidden"); + }, "hamburger to be hidden"); + $main.css("width", ""); + $content.css({"left": "", "right": "", "position": "", "width": ""}); + }); + + it("should appear when titlebar is too narrow", async function () { + forceNarrowTitlebar(); + await awaitsForCondition(function () { + return !getHamburger().is(":hidden"); + }, "hamburger to appear"); + const $hidden = getHiddenMenuItems(); + expect($hidden.length).toBeGreaterThan(0); + const $entries = getHamburgerDropdown().find(".hamburger-submenu-item"); + expect($entries.length).toBe($hidden.length); + }); + + it("should open dropdown on click", async function () { + forceNarrowTitlebar(); + await awaitsForCondition(function () { + return !getHamburger().is(":hidden"); + }, "hamburger to appear"); + const $hamburger = getHamburger(); + expect($hamburger.hasClass("hamburger-open")).toBe(false); + + $hamburger.find(".hamburger-toggle").click(); + expect($hamburger.hasClass("hamburger-open")).toBe(true); + + const $dropdown = getHamburgerDropdown(); + expect($dropdown.css("display")).toBe("block"); + }); + + it("should close dropdown on second click", async function () { + forceNarrowTitlebar(); + await awaitsForCondition(function () { + return !getHamburger().is(":hidden"); + }, "hamburger to appear"); + const $hamburger = getHamburger(); + const $toggle = $hamburger.find(".hamburger-toggle"); + + $toggle.click(); + expect($hamburger.hasClass("hamburger-open")).toBe(true); + + $toggle.click(); + expect($hamburger.hasClass("hamburger-open")).toBe(false); + }); + + it("should open flyout submenu on entry hover", async function () { + forceNarrowTitlebar(); + await awaitsForCondition(function () { + return !getHamburger().is(":hidden"); + }, "hamburger to appear"); + const $hamburger = getHamburger(); + $hamburger.find(".hamburger-toggle").click(); + + const $entries = getHamburgerDropdown().find(".hamburger-submenu-item"); + expect($entries.length).toBeGreaterThan(0); + + const $firstEntry = $entries.first(); + $firstEntry.trigger("mouseenter"); + + expect($firstEntry.hasClass("hamburger-submenu-open")).toBe(true); + + const menuId = $firstEntry.find("a").attr("data-menu-id"); + const $menuDropdown = testWindow.$("#" + menuId + " > .dropdown-menu"); + expect($menuDropdown.css("visibility")).toBe("visible"); + expect($menuDropdown.css("position")).toBe("fixed"); + }); + + it("should restore all menus when titlebar expands", async function () { + forceNarrowTitlebar(); + await awaitsForCondition(function () { + return !getHamburger().is(":hidden"); + }, "hamburger to appear"); + expect(getHiddenMenuItems().length).toBeGreaterThan(0); + + // Expand wide enough for all menus + restoreTitlebar(); + const $content = testWindow.$(".content"); + const $main = $content.parent(); + $main.css("width", "4000px"); + $content.css({"left": "0", "right": "0", "position": "relative", "width": "4000px"}); + $content[0].offsetHeight; + await awaitsForCondition(function () { + return getHamburger().is(":hidden"); + }, "hamburger to hide after expanding"); + expect(getHiddenMenuItems().length).toBe(0); + $main.css("width", ""); + $content.css({"left": "", "right": "", "position": "", "width": ""}); + }); + + it("should execute command when clicking a flyout menu item", async function () { + // Register a test command and add it to a menu + let commandExecuted = false; + CommandManager.register("Hamburger Test Command", "Menu-test.hamburgerCmd1", function () { + commandExecuted = true; + }); + const fileMenu = Menus.getMenu(Menus.AppMenuBar.FILE_MENU); + fileMenu.addMenuItem("Menu-test.hamburgerCmd1"); + + forceNarrowTitlebar(); + await awaitsForCondition(function () { + return !getHamburger().is(":hidden"); + }, "hamburger to appear"); + + // Open hamburger and hover File menu entry + const $hamburger = getHamburger(); + $hamburger.find(".hamburger-toggle").click(); + const $entries = getHamburgerDropdown().find(".hamburger-submenu-item"); + // Find the File menu entry + const $fileEntry = $entries.filter(function () { + return testWindow.$(this).find("a").attr("data-menu-id") === "file-menu"; + }); + expect($fileEntry.length).toBe(1); + $fileEntry.trigger("mouseenter"); + + // Click the test command in the flyout + const $menuItem = testWindow.$("#file-menu-Menu-test\\.hamburgerCmd1"); + expect($menuItem.length).toBe(1); + $menuItem.click(); + + await awaitsForCondition(function () { + return commandExecuted; + }, "command to be executed"); + await awaitsForCondition(function () { + return !$hamburger.hasClass("hamburger-open"); + }, "hamburger to close after command"); + }); + + it("should execute command when clicking a flyout submenu item", async function () { + // Register a command, create a submenu in the File menu + let subCommandExecuted = false; + CommandManager.register("Hamburger SubMenu Test", "Menu-test.hamburgerSubCmd1", function () { + subCommandExecuted = true; + }); + const fileMenu = Menus.getMenu(Menus.AppMenuBar.FILE_MENU); + const subMenu = fileMenu.addSubMenu("Hamburger Sub", "hamburger-test-submenu1"); + subMenu.addMenuItem("Menu-test.hamburgerSubCmd1"); + + forceNarrowTitlebar(); + await awaitsForCondition(function () { + return !getHamburger().is(":hidden"); + }, "hamburger to appear"); + + // Open hamburger and hover File menu entry + const $hamburger = getHamburger(); + $hamburger.find(".hamburger-toggle").click(); + const $entries = getHamburgerDropdown().find(".hamburger-submenu-item"); + const $fileEntry = $entries.filter(function () { + return testWindow.$(this).find("a").attr("data-menu-id") === "file-menu"; + }); + $fileEntry.trigger("mouseenter"); + + // Hover the submenu trigger to open it + const $subMenuTrigger = testWindow.$("#file-menu-hamburger-test-submenu1").closest("li"); + expect($subMenuTrigger.length).toBe(1); + $subMenuTrigger.trigger("mouseenter"); + + // Wait for submenu to open + await awaitsForCondition(function () { + return testWindow.$("#hamburger-test-submenu1").hasClass("open"); + }, "submenu to open"); + + // Click the command in the submenu + const $subItem = testWindow.$("#hamburger-test-submenu1-Menu-test\\.hamburgerSubCmd1"); + expect($subItem.length).toBe(1); + $subItem.closest("li").click(); + + await awaitsForCondition(function () { + return subCommandExecuted; + }, "submenu command to be executed"); + }); + + it("should close on ESC key", async function () { + forceNarrowTitlebar(); + await awaitsForCondition(function () { + return !getHamburger().is(":hidden"); + }, "hamburger to appear"); + const $hamburger = getHamburger(); + $hamburger.find(".hamburger-toggle").click(); + expect($hamburger.hasClass("hamburger-open")).toBe(true); + + // Dispatch ESC keydown event + const escEvent = new testWindow.KeyboardEvent("keydown", { + key: "Escape", + keyCode: KeyEvent.DOM_VK_ESCAPE, + bubbles: true, + cancelable: true + }); + testWindow.document.dispatchEvent(escEvent); + + expect($hamburger.hasClass("hamburger-open")).toBe(false); + }); + }); }); }); From 279716ae46e87d271fc7600fa645c9b4d9b318b9 Mon Sep 17 00:00:00 2001 From: abose Date: Sat, 4 Apr 2026 13:14:09 +0530 Subject: [PATCH 3/5] fix: hamburger menu positioning, hover transitions, and syntax fix - Position hamburger as last item in menu bar (after visible menus) - Right-align dropdown to hamburger button - Hide more items until hamburger fits on one row - Close hamburger when hovering a normal menu, and auto-open that menu - Auto-open hamburger when hovering from an open normal menu - Fix missing closing brace that broke module loading - Dismiss all menus and popups on window blur --- src/command/Menus.js | 39 ++++++++++++++++++---- src/styles/brackets_patterns_override.less | 3 ++ 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/command/Menus.js b/src/command/Menus.js index 7e4a605ea6..c9a21f8a7c 100644 --- a/src/command/Menus.js +++ b/src/command/Menus.js @@ -1369,10 +1369,9 @@ define(function (require, exports, module) { $menuDropdownToggle.parent().removeClass('open'); const menuID = $menuDropdownToggle.parent().get(0).id; const mainMenu = menuMap[menuID]; - const $dropdownToggles = $('#titlebar .dropdown-toggle'); + const $dropdownToggles = $('#titlebar .dropdown:not(.hamburger-menu):visible > .dropdown-toggle'); let currentIndex = $dropdownToggles.index($menuDropdownToggle); - currentIndex = event.key === KEY.ARROW_LEFT ? currentIndex - 1 : currentIndex + 1; - let nextIndex = currentIndex; + let nextIndex = event.key === KEY.ARROW_LEFT ? currentIndex - 1 : currentIndex + 1; if (nextIndex < 0) { nextIndex = 0; } else if (nextIndex >= $dropdownToggles.length) { @@ -1859,12 +1858,30 @@ define(function (require, exports, module) { _closeHamburger(); }); - // Wire up hamburger toggle mouseenter like other menus + // Close hamburger and open the hovered normal menu + $menubar.on("mouseenter", ".dropdown:not(.hamburger-menu) > .dropdown-toggle", function () { + if ($hamburger.hasClass("hamburger-open")) { + _closeHamburger(); + // Open the hovered menu and focus its toggle so keyboard nav works + const $toggle = $(this); + $toggle.parent().addClass("open"); + $toggle.focus(); + } + }); + + // Wire up hamburger toggle mouseenter like other menus. + // If the titlebar has focus (meaning a menu is already open), + // auto-open the hamburger on hover - matching normal menu behavior. $hamburgerToggle.on("mouseenter", function () { _closeAllSubMenus(); const $this = $(this); if ($('#titlebar, #titlebar *').is(':focus')) { + // Close any open normal menus first + closeAll(); $this.addClass('selected').focus(); + if (!$hamburger.hasClass("hamburger-open")) { + $hamburger.addClass("hamburger-open"); + } } else { $this.addClass('selected'); } @@ -1908,6 +1925,8 @@ define(function (require, exports, module) { const $items = $menubar.children("li.dropdown:not(.hamburger-menu)"); // First, show all items and hide hamburger to measure natural layout $items.css({display: "", position: "", visibility: "", pointerEvents: ""}); + // Ensure hamburger is always the last item in the menu bar + $menubar.append($hamburger); $hamburger.hide(); $hamburgerDropdown.empty(); @@ -1930,11 +1949,17 @@ define(function (require, exports, module) { return; } - // Show hamburger, then re-check what fits with hamburger visible + // Show hamburger, then keep hiding items from the end until + // both visible items and the hamburger fit on one row $hamburger.css("display", ""); - // Re-measure: with hamburger visible, even more items might overflow - for (let i = 0; i < $items.length; i++) { + while (overflowStartIndex > 0 && $hamburger[0].offsetTop > firstItemTop) { + overflowStartIndex--; + $($items[overflowStartIndex]).css("display", "none"); + } + + // Also check if any remaining items wrapped + for (let i = 0; i < overflowStartIndex; i++) { if ($items[i].offsetTop > firstItemTop) { overflowStartIndex = i; break; diff --git a/src/styles/brackets_patterns_override.less b/src/styles/brackets_patterns_override.less index fb053c4128..764fd3dc3d 100644 --- a/src/styles/brackets_patterns_override.less +++ b/src/styles/brackets_patterns_override.less @@ -234,10 +234,13 @@ a:focus { background: @dark-bc-bg-highlight; } } + position: relative; .hamburger-dropdown { display: none; min-width: 120px; text-align: left; + right: 0; + left: auto; } &.hamburger-open .hamburger-dropdown { display: block; From 30fbe8bfd6ee682971fc7eb1d6809555671dafcb Mon Sep 17 00:00:00 2001 From: abose Date: Sat, 4 Apr 2026 20:03:35 +0530 Subject: [PATCH 4/5] feat: add sidebar collapse/expand toggle button in titlebar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a « / » button before the File menu that toggles sidebar visibility. Shows context-aware tooltip (Hide/Show Sidebar) and updates icon direction on panelCollapsed/panelExpanded events. --- src/command/Menus.js | 29 ++++++++++++++++++++++ src/styles/brackets_patterns_override.less | 22 ++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/src/command/Menus.js b/src/command/Menus.js index c9a21f8a7c..18fb38ea21 100644 --- a/src/command/Menus.js +++ b/src/command/Menus.js @@ -1778,6 +1778,35 @@ define(function (require, exports, module) { const $hamburgerToggle = $hamburger.find(".hamburger-toggle"); let _activeSubmenuId = null; + // Sidebar collapse/expand toggle button before the File menu + const $sidebarToggle = $(``); + $menubar.prepend($sidebarToggle); + const $sidebarIcon = $sidebarToggle.find("a"); + + function _updateSidebarToggleIcon() { + const isVisible = $("#sidebar").is(":visible"); + if (isVisible) { + $sidebarIcon.html(''); + $sidebarIcon.attr("title", Strings.CMD_HIDE_SIDEBAR); + } else { + $sidebarIcon.html(''); + $sidebarIcon.attr("title", Strings.CMD_SHOW_SIDEBAR); + } + } + + $sidebarIcon.on("click", function (e) { + e.preventDefault(); + e.stopPropagation(); + CommandManager.execute(Commands.VIEW_HIDE_SIDEBAR); + }); + + $("#sidebar").on("panelCollapsed panelExpanded", _updateSidebarToggleIcon); + _updateSidebarToggleIcon(); + function _resetMenuItemStyles($menuItem) { const menu = menuMap[$menuItem.attr("id")]; if (menu) { diff --git a/src/styles/brackets_patterns_override.less b/src/styles/brackets_patterns_override.less index 764fd3dc3d..b1f19e0a26 100644 --- a/src/styles/brackets_patterns_override.less +++ b/src/styles/brackets_patterns_override.less @@ -211,6 +211,28 @@ a:focus { animation: none; } } + .nav .sidebar-toggle-btn { + float: left; + a { + display: inline-block; + padding: @menubar-top-padding 6px @menubar-bottom-padding; + border: 1px solid transparent; + font-size: @menu-item-font-size; + color: fadeout(@bc-menu-text, 50%); + cursor: default; + outline: none; + .dark & { + color: fadeout(@dark-bc-menu-text, 50%); + } + &:hover { + color: @bc-menu-text; + .dark & { + color: @dark-bc-menu-text; + } + } + } + } + .nav .hamburger-menu { float: left; .hamburger-toggle, .hamburger-toggle:hover, .hamburger-toggle:focus { From 4793c3d3307d39e99a5d2d3067fd1e23b6e43543 Mon Sep 17 00:00:00 2001 From: abose Date: Sat, 4 Apr 2026 20:58:37 +0530 Subject: [PATCH 5/5] fix: modal wrapper not removed when dialogs overlap in timing Each dialog now tracks its own wrapper reference instead of using $(".modal-wrapper:last").remove() which could remove the wrong wrapper when a closing animation overlaps with a new dialog opening. --- src/widgets/Dialogs.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/widgets/Dialogs.js b/src/widgets/Dialogs.js index 1eb8271519..82dc1beb8e 100644 --- a/src/widgets/Dialogs.js +++ b/src/widgets/Dialogs.js @@ -412,13 +412,14 @@ define(function (require, exports, module) { autoDismiss = true; } - $("body").append(""); + let $wrapper = $(""); + $("body").append($wrapper); let result = new $.Deferred(), promise = result.promise(), $dlg = $(template) .addClass("instance") - .appendTo(".modal-inner-wrapper:last"); + .appendTo($wrapper.find(".modal-inner-wrapper")); // Don't allow dialog to exceed viewport size setDialogMaxSize(); @@ -463,7 +464,7 @@ define(function (require, exports, module) { } //Remove wrapper - $(".modal-wrapper:last").remove(); + $wrapper.remove(); }).one("shown", function () { let $defaultOption = $dlg.find(".default-option"), $primaryBtn = $dlg.find(".primary:enabled"),