From 88a233bbde468364e1807f728e7634f2b26b6b47 Mon Sep 17 00:00:00 2001 From: abose Date: Tue, 7 Apr 2026 18:16:00 +0530 Subject: [PATCH 1/2] feat: overflow dropdown for hidden panel tabs, tools button always visible Add chevron dropdown button using DropdownButton widget to access hidden tabs when tab bar overflows even after icon collapse. Tools button stays outside scrollable area, highlighted as active tab when selected. Scroll clicked/selected tabs into view. Guard drop handlers in ProjectManager and FileTreeView against non-file drags. --- src/project/FileTreeView.js | 10 +- src/project/ProjectManager.js | 10 +- src/styles/Extn-BottomPanelTabs.less | 59 +++++++++ src/view/PanelView.js | 171 +++++++++++++++++++++++---- 4 files changed, 221 insertions(+), 29 deletions(-) diff --git a/src/project/FileTreeView.js b/src/project/FileTreeView.js index 6e366f726..11992ff87 100644 --- a/src/project/FileTreeView.js +++ b/src/project/FileTreeView.js @@ -1213,8 +1213,14 @@ define(function (require, exports, module) { }, handleDrop: function(e) { - var data = JSON.parse(e.dataTransfer.getData("text")); - this.props.actions.moveItem(data.path, this.props.parentPath); + try { + var data = JSON.parse(e.dataTransfer.getData("text")); + if (data && data.path) { + this.props.actions.moveItem(data.path, this.props.parentPath); + } + } catch (err) { + console.error("FileTreeView: drop handler error:", err); + } e.stopPropagation(); }, diff --git a/src/project/ProjectManager.js b/src/project/ProjectManager.js index 87757e15a..b78a2d83c 100644 --- a/src/project/ProjectManager.js +++ b/src/project/ProjectManager.js @@ -2096,8 +2096,14 @@ define(function (require, exports, module) { // Add support for moving items to root directory $projectTreeContainer.on("drop", function(e) { - var data = JSON.parse(e.originalEvent.dataTransfer.getData("text")); - actionCreator.moveItem(data.path, getProjectRoot().fullPath); + try { + var data = JSON.parse(e.originalEvent.dataTransfer.getData("text")); + if (data && data.path) { + actionCreator.moveItem(data.path, getProjectRoot().fullPath); + } + } catch (err) { + console.error("ProjectManager: drop handler error:", err); + } e.stopPropagation(); }); diff --git a/src/styles/Extn-BottomPanelTabs.less b/src/styles/Extn-BottomPanelTabs.less index afe16fabf..8c5cc7fd3 100644 --- a/src/styles/Extn-BottomPanelTabs.less +++ b/src/styles/Extn-BottomPanelTabs.less @@ -180,6 +180,35 @@ img.panel-titlebar-icon { pointer-events: none; } +/* Overflow button: shown when tabs overflow even after collapsing to icons */ +.bottom-panel-overflow-btn { + display: flex; + align-items: center; + justify-content: center; + width: 1.9rem; + height: 2rem; + cursor: pointer; + color: #666; + font-size: 0.9rem; + flex: 0 0 auto; + transition: color 0.12s ease, background-color 0.12s ease; + + .dark & { + color: #aaa; + } + + &:hover { + background-color: #e0e0e0; + color: #333; + + .dark & { + background-color: #333; + color: #eee; + } + } + +} + /* Drag-and-drop tab reordering */ .bottom-panel-tab-dragging { opacity: 0.5; @@ -257,6 +286,11 @@ img.panel-titlebar-icon { align-items: center; justify-content: center; padding: 0 8px; + border-left: 1px solid rgba(0, 0, 0, 0.08); + + .dark & { + border-left: 1px solid rgba(255, 255, 255, 0.08); + } height: 2rem; line-height: 2rem; overflow: hidden; @@ -280,6 +314,31 @@ img.panel-titlebar-icon { color: #eee; } } + + &.active { + color: #333; + background-color: #fff; + position: relative; + + .dark & { + color: #dedede; + background-color: #1D1F21; + } + + &::after { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 0.1rem; + background-color: #0078D7; + + .dark & { + background-color: #75BEFF; + } + } + } } .bottom-panel-tab-bar-actions { diff --git a/src/view/PanelView.js b/src/view/PanelView.js index 3b3134454..e2ee9ba32 100644 --- a/src/view/PanelView.js +++ b/src/view/PanelView.js @@ -28,6 +28,7 @@ define(function (require, exports, module) { const EventDispatcher = require("utils/EventDispatcher"), PreferencesManager = require("preferences/PreferencesManager"), Resizer = require("utils/Resizer"), + DropdownButton = require("widgets/DropdownButton"), Strings = require("strings"); /** @@ -171,13 +172,13 @@ define(function (require, exports, module) { if (!_$tabsOverflow) { return; } - // Detach the add button before emptying to preserve its event handlers - if (_$addBtn) { - _$addBtn.detach(); - } _$tabsOverflow.empty(); _openIds.forEach(function (panelId) { + // Default panel uses the external Tools button, not a tab + if (panelId === _defaultPanelId) { + return; + } let panel = _panelMap[panelId]; if (!panel) { return; @@ -185,11 +186,7 @@ define(function (require, exports, module) { _$tabsOverflow.append(_buildTab(panel, panelId === _activeId)); }); - // Re-append the "+" button at the end (after all tabs) - if (_$addBtn) { - _$tabsOverflow.append(_$addBtn); - _updateAddButtonVisibility(); - } + _updateAddButtonVisibility(); _checkTabOverflow(); } @@ -221,18 +218,17 @@ define(function (require, exports, module) { if (!_$tabsOverflow) { return; } + // Default panel uses the external Tools button, not a tab + if (panelId === _defaultPanelId) { + _updateAddButtonVisibility(); + return; + } let panel = _panelMap[panelId]; if (!panel) { return; } let $tab = _buildTab(panel, panelId === _activeId); - - // Insert before the "+" button so it stays at the end - if (_$addBtn && _$addBtn.parent().length) { - _$addBtn.before($tab); - } else { - _$tabsOverflow.append($tab); - } + _$tabsOverflow.append($tab); _updateAddButtonVisibility(); _checkTabOverflow(); } @@ -247,6 +243,10 @@ define(function (require, exports, module) { if (!_$tabsOverflow) { return; } + if (panelId === _defaultPanelId) { + _updateAddButtonVisibility(); + return; + } _$tabsOverflow.find('.bottom-panel-tab[data-panel-id="' + panelId + '"]').remove(); _updateAddButtonVisibility(); _checkTabOverflow(); @@ -295,7 +295,7 @@ define(function (require, exports, module) { _$tabBar.on("dragstart", ".bottom-panel-tab", function (e) { draggedTab = this; e.originalEvent.dataTransfer.effectAllowed = "move"; - e.originalEvent.dataTransfer.setData("text/plain", "panel-tab"); + e.originalEvent.dataTransfer.setData("application/x-phoenix-panel-tab", "1"); $(this).addClass("bottom-panel-tab-dragging"); }); @@ -354,6 +354,9 @@ define(function (require, exports, module) { * Only collapses tabs that have an icon available. * @private */ + /** @type {jQueryObject} Overflow dropdown button */ + let _$overflowBtn = null; + function _checkTabOverflow() { if (!_$tabBar) { return; @@ -362,6 +365,16 @@ define(function (require, exports, module) { _$tabBar.removeClass("bottom-panel-tabs-collapsed"); const isOverflowing = _$tabsOverflow[0].scrollWidth > _$tabsOverflow[0].clientWidth; _$tabBar.toggleClass("bottom-panel-tabs-collapsed", isOverflowing); + + // Check if still overflowing after collapse + const stillOverflowing = isOverflowing && + _$tabsOverflow[0].scrollWidth > _$tabsOverflow[0].clientWidth; + + // Show/hide overflow button + if (_$overflowBtn) { + _$overflowBtn.toggle(stillOverflowing); + } + // Show tooltip on hover only in collapsed mode (title text is hidden) _$tabBar.find(".bottom-panel-tab").each(function () { const $tab = $(this); @@ -373,6 +386,105 @@ define(function (require, exports, module) { }); } + /** + * Get the list of hidden (not fully visible) panel tabs. + * @return {Array<{panelId: string, title: string}>} + * @private + */ + function _getHiddenTabs() { + const hidden = []; + const barRect = _$tabsOverflow[0].getBoundingClientRect(); + _$tabsOverflow.find(".bottom-panel-tab").each(function () { + const tabRect = this.getBoundingClientRect(); + const isVisible = tabRect.left >= barRect.left && + tabRect.right <= (barRect.right + 2); + if (!isVisible) { + const $tab = $(this); + hidden.push({ + panelId: $tab.data("panel-id"), + title: $tab.find(".bottom-panel-tab-title").text() + }); + } + }); + return hidden; + } + + /** @type {DropdownButton.DropdownButton} */ + let _overflowDropdown = null; + + /** + * Show a dropdown menu listing hidden panel tabs. + * Uses the same DropdownButton widget as the file tab bar overflow. + * @private + */ + function _showOverflowMenu() { + // If dropdown is already open, close it (toggle behavior) + if (_overflowDropdown) { + _overflowDropdown.closeDropdown(); + _overflowDropdown = null; + return; + } + + const hidden = _getHiddenTabs(); + if (!hidden.length) { + return; + } + + _overflowDropdown = new DropdownButton.DropdownButton("", hidden, function (item) { + const panel = _panelMap[item.panelId]; + let iconHtml = ""; + if (panel && panel._options) { + if (panel._options.iconClass) { + iconHtml = ''; + } else if (panel._options.iconSvg) { + iconHtml = ''; + } + } + const activeClass = item.panelId === _activeId ? ' style="font-weight:600"' : ''; + return { + html: '', + enabled: true + }; + }); + + _overflowDropdown.dropdownExtraClasses = "dropdown-overflow-menu"; + + // Position at the overflow button + const btnRect = _$overflowBtn[0].getBoundingClientRect(); + $("body").append(_overflowDropdown.$button); + _overflowDropdown.$button.css({ + position: "absolute", + left: btnRect.left + "px", + top: (btnRect.top - 2) + "px", + zIndex: 1000 + }); + + _overflowDropdown.showDropdown(); + + _overflowDropdown.on("select", function (e, item) { + const panel = _panelMap[item.panelId]; + if (panel) { + panel.show(); + // Scroll the newly active tab into view + const $tab = _$tabsOverflow.find('.bottom-panel-tab[data-panel-id="' + item.panelId + '"]'); + if ($tab.length) { + $tab[0].scrollIntoView({inline: "nearest"}); + } + } + }); + + // Clean up reference when dropdown closes + _overflowDropdown.on(DropdownButton.EVENT_DROPDOWN_CLOSED, function () { + if (_overflowDropdown) { + _overflowDropdown.$button.remove(); + _overflowDropdown = null; + } + }); + } + /** * Show or hide the "+" button based on whether the default panel is active. * The button is hidden when the default panel is the active tab (since @@ -383,11 +495,8 @@ define(function (require, exports, module) { if (!_$addBtn) { return; } - if (_defaultPanelId && _activeId === _defaultPanelId) { - _$addBtn.hide(); - } else { - _$addBtn.show(); - } + // Highlight the Tools button as active when the default panel is shown + _$addBtn.toggleClass("active", _defaultPanelId && _activeId === _defaultPanelId); } /** @@ -664,13 +773,13 @@ define(function (require, exports, module) { _recomputeLayout = recomputeLayoutFn; _defaultPanelId = defaultPanelId; - // Create the "Tools" button inside the tabs overflow area (after all tabs) - // This opens the default/quick-access panel when clicked. + // Create the "Tools" button outside the scrollable tabs area + // so it's always visible even when tabs overflow. _$addBtn = $('' + '' + Strings.BOTTOM_PANEL_DEFAULT_TITLE + ''); - _$tabsOverflow.append(_$addBtn); + _$tabBar.find(".bottom-panel-tab-bar-actions").before(_$addBtn); // Tab bar click handlers _$tabBar.on("click", ".bottom-panel-tab-close-btn", function (e) { @@ -692,10 +801,22 @@ define(function (require, exports, module) { panel.show(); } } + // Scroll clicked tab into view if partially hidden + this.scrollIntoView({inline: "nearest"}); }); _initDragAndDrop(); + // Overflow button for hidden tabs (inserted between tabs and action buttons) + _$overflowBtn = $('' + + ''); + _$overflowBtn.hide(); + _$tabBar.find(".bottom-panel-tab-bar-actions").before(_$overflowBtn); + _$overflowBtn.on("click", function (e) { + e.stopPropagation(); + _showOverflowMenu(); + }); + // "+" button opens the default/quick-access panel _$addBtn.on("click", function (e) { e.stopPropagation(); From 3b5528cbb8d1922f73c08c7216ee1bc7ad5edb64 Mon Sep 17 00:00:00 2001 From: abose Date: Tue, 7 Apr 2026 18:20:45 +0530 Subject: [PATCH 2/2] fix: move tools button back inside scrollable tab area Tools button is better placed inside the scrollable tabs area near the panels it launches. Restores original show/hide behavior when active. Overflow dropdown handles it when scrolled out of view. --- src/styles/Extn-BottomPanelTabs.less | 29 -------------------- src/view/PanelView.js | 40 +++++++++++++++------------- 2 files changed, 21 insertions(+), 48 deletions(-) diff --git a/src/styles/Extn-BottomPanelTabs.less b/src/styles/Extn-BottomPanelTabs.less index 8c5cc7fd3..c9dc05160 100644 --- a/src/styles/Extn-BottomPanelTabs.less +++ b/src/styles/Extn-BottomPanelTabs.less @@ -286,11 +286,6 @@ img.panel-titlebar-icon { align-items: center; justify-content: center; padding: 0 8px; - border-left: 1px solid rgba(0, 0, 0, 0.08); - - .dark & { - border-left: 1px solid rgba(255, 255, 255, 0.08); - } height: 2rem; line-height: 2rem; overflow: hidden; @@ -315,30 +310,6 @@ img.panel-titlebar-icon { } } - &.active { - color: #333; - background-color: #fff; - position: relative; - - .dark & { - color: #dedede; - background-color: #1D1F21; - } - - &::after { - content: ""; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 0.1rem; - background-color: #0078D7; - - .dark & { - background-color: #75BEFF; - } - } - } } .bottom-panel-tab-bar-actions { diff --git a/src/view/PanelView.js b/src/view/PanelView.js index e2ee9ba32..aeedd540e 100644 --- a/src/view/PanelView.js +++ b/src/view/PanelView.js @@ -172,13 +172,13 @@ define(function (require, exports, module) { if (!_$tabsOverflow) { return; } + // Detach the add button before emptying to preserve its event handlers + if (_$addBtn) { + _$addBtn.detach(); + } _$tabsOverflow.empty(); _openIds.forEach(function (panelId) { - // Default panel uses the external Tools button, not a tab - if (panelId === _defaultPanelId) { - return; - } let panel = _panelMap[panelId]; if (!panel) { return; @@ -186,6 +186,10 @@ define(function (require, exports, module) { _$tabsOverflow.append(_buildTab(panel, panelId === _activeId)); }); + // Re-append the Tools button at the end + if (_$addBtn) { + _$tabsOverflow.append(_$addBtn); + } _updateAddButtonVisibility(); _checkTabOverflow(); } @@ -218,17 +222,17 @@ define(function (require, exports, module) { if (!_$tabsOverflow) { return; } - // Default panel uses the external Tools button, not a tab - if (panelId === _defaultPanelId) { - _updateAddButtonVisibility(); - return; - } let panel = _panelMap[panelId]; if (!panel) { return; } let $tab = _buildTab(panel, panelId === _activeId); - _$tabsOverflow.append($tab); + // Insert before the Tools button so it stays at the end + if (_$addBtn && _$addBtn.parent().length) { + _$addBtn.before($tab); + } else { + _$tabsOverflow.append($tab); + } _updateAddButtonVisibility(); _checkTabOverflow(); } @@ -243,10 +247,6 @@ define(function (require, exports, module) { if (!_$tabsOverflow) { return; } - if (panelId === _defaultPanelId) { - _updateAddButtonVisibility(); - return; - } _$tabsOverflow.find('.bottom-panel-tab[data-panel-id="' + panelId + '"]').remove(); _updateAddButtonVisibility(); _checkTabOverflow(); @@ -495,8 +495,11 @@ define(function (require, exports, module) { if (!_$addBtn) { return; } - // Highlight the Tools button as active when the default panel is shown - _$addBtn.toggleClass("active", _defaultPanelId && _activeId === _defaultPanelId); + if (_defaultPanelId && _activeId === _defaultPanelId) { + _$addBtn.hide(); + } else { + _$addBtn.show(); + } } /** @@ -773,13 +776,12 @@ define(function (require, exports, module) { _recomputeLayout = recomputeLayoutFn; _defaultPanelId = defaultPanelId; - // Create the "Tools" button outside the scrollable tabs area - // so it's always visible even when tabs overflow. + // Create the "Tools" button inside the scrollable tabs area. _$addBtn = $('' + '' + Strings.BOTTOM_PANEL_DEFAULT_TITLE + ''); - _$tabBar.find(".bottom-panel-tab-bar-actions").before(_$addBtn); + _$tabsOverflow.append(_$addBtn); // Tab bar click handlers _$tabBar.on("click", ".bottom-panel-tab-close-btn", function (e) {