From 8331940e04f1dbd24c6487526ac3c5a7ab8a6366 Mon Sep 17 00:00:00 2001 From: Pluto Date: Tue, 17 Feb 2026 17:40:43 +0530 Subject: [PATCH 01/16] feat: tabbed bottom panel initial architecture --- .../NoDistractions/main.js | 19 +- src/styles/Extn-BottomPanelTabs.less | 155 +++++++++++ src/styles/brackets.less | 1 + src/utils/Resizer.js | 8 +- src/view/WorkspaceManager.js | 256 +++++++++++++++++- 5 files changed, 425 insertions(+), 14 deletions(-) create mode 100644 src/styles/Extn-BottomPanelTabs.less diff --git a/src/extensionsIntegrated/NoDistractions/main.js b/src/extensionsIntegrated/NoDistractions/main.js index e0da1645eb..17894b8c54 100644 --- a/src/extensionsIntegrated/NoDistractions/main.js +++ b/src/extensionsIntegrated/NoDistractions/main.js @@ -89,13 +89,20 @@ define(function (require, exports, module) { function _hidePanelsIfRequired() { var panelIDs = WorkspaceManager.getAllPanelIDs(); _previouslyOpenPanelIDs = []; - panelIDs.forEach(function (panelID) { - var panel = WorkspaceManager.getPanelForID(panelID); - if (panel && panel.isVisible()) { - panel.hide(); - _previouslyOpenPanelIDs.push(panelID); + // Loop until no visible panels remain. In a tabbed system, hiding the + // active tab may reveal the next tab, so we must iterate. + let hiddenSomething = true; + while (hiddenSomething) { + hiddenSomething = false; + for (let i = 0; i < panelIDs.length; i++) { + let panel = WorkspaceManager.getPanelForID(panelIDs[i]); + if (panel && panel.isVisible()) { + panel.hide(); + _previouslyOpenPanelIDs.push(panelIDs[i]); + hiddenSomething = true; + } } - }); + } } /** diff --git a/src/styles/Extn-BottomPanelTabs.less b/src/styles/Extn-BottomPanelTabs.less new file mode 100644 index 0000000000..f9475ecbca --- /dev/null +++ b/src/styles/Extn-BottomPanelTabs.less @@ -0,0 +1,155 @@ +/* + * GNU AGPL-3.0 License + * + * Copyright (c) 2021 - present core.ai . All rights reserved. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License + * for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://opensource.org/licenses/AGPL-3.0. + * + */ + +/* Bottom panel tab bar — switches between tabbed bottom panels */ + +#bottom-panel-container { + background-color: @bc-panel-bg; + border-top: 1px solid @bc-panel-border; + display: flex; + flex-direction: column; + + .dark & { + background-color: @dark-bc-panel-bg; + border-top: 1px solid @dark-bc-panel-border; + } + + .bottom-panel { + display: none !important; + flex: 1; + min-height: 0; + border-top: none; + height: auto !important; + + &.active-bottom-panel { + display: flex !important; + flex-direction: column; + } + } +} + +#bottom-panel-tab-bar { + display: flex; + align-items: center; + height: 28px; + min-height: 28px; + background-color: @bc-panel-bg-promoted; + border-bottom: @bc-panel-separator; + overflow-x: auto; + overflow-y: hidden; + user-select: none; + box-shadow: inset 0 1px 0 @bc-highlight-hard, 0 -1px 3px @bc-shadow-small; + + .dark & { + background-color: @dark-bc-panel-bg-promoted; + border-bottom: @dark-bc-panel-separator; + box-shadow: inset 0 1px 0 @dark-bc-highlight, 0 -1px 3px @dark-bc-shadow-small; + } + + /* Hide scrollbar but allow scrolling */ + &::-webkit-scrollbar { + display: none; + } + -ms-overflow-style: none; + scrollbar-width: none; +} + +.bottom-panel-tab { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 0 10px; + height: 100%; + cursor: pointer; + position: relative; + color: @bc-text-quiet; + font-size: 12px; + white-space: nowrap; + flex-shrink: 0; + transition: color 0.15s ease, background-color 0.15s ease; + + .dark & { + color: @dark-bc-text-quiet; + } + + &:hover { + color: @bc-text; + background-color: rgba(0, 0, 0, 0.04); + + .dark & { + color: @dark-bc-text; + background-color: rgba(255, 255, 255, 0.06); + } + } + + &.active { + color: @bc-text; + background-color: @bc-panel-bg; + + .dark & { + color: @dark-bc-text; + background-color: @dark-bc-panel-bg; + } + + &::after { + content: ""; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 2px; + background-color: @bc-primary-btn-bg; + + .dark & { + background-color: @dark-bc-primary-btn-bg; + } + } + } +} + +.bottom-panel-tab-title { + pointer-events: none; +} + +.bottom-panel-tab-close { + display: inline-flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + font-size: 14px; + line-height: 1; + border-radius: 3px; + opacity: 0; + transition: opacity 0.15s ease, background-color 0.15s ease; + + .bottom-panel-tab:hover & { + opacity: 0.6; + } + + &:hover { + opacity: 1 !important; + background-color: rgba(0, 0, 0, 0.1); + + .dark & { + background-color: rgba(255, 255, 255, 0.15); + } + } +} diff --git a/src/styles/brackets.less b/src/styles/brackets.less index a0f0c46faa..7cd694023f 100644 --- a/src/styles/brackets.less +++ b/src/styles/brackets.less @@ -47,6 +47,7 @@ @import "Extn-CustomSnippets.less"; @import "Extn-CollapseFolders.less"; @import "Extn-SidebarTabs.less"; +@import "Extn-BottomPanelTabs.less"; @import "Extn-AIChatPanel.less"; @import "UserProfile.less"; @import "phoenix-pro.less"; diff --git a/src/utils/Resizer.js b/src/utils/Resizer.js index 9ae4e29886..88caab5411 100644 --- a/src/utils/Resizer.js +++ b/src/utils/Resizer.js @@ -411,8 +411,9 @@ define(function (require, exports, module) { if(initialSize){ elementSize = elementPrefs.size || initialSize; } - if(elementSize minSize for bottom panels */ + let _panelMinSizes = {}; + + /** Fallback title map for panels that don't have a .toolbar .title element */ + const _KNOWN_PANEL_TITLES = { + "errors": "Problems", + "problems-panel": "Problems", + "search-results": "Search Results", + "find-in-files-results": "Search Results", + "references-panel": "References", + "git-panel": "Git", + "keyboard-shortcuts-panel": "Keyboard Shortcuts", + "custom-snippets-panel": "Custom Snippets", + "test-builder-panel": "Test Builder" + }; + /** * Calculates the available height for the full-size Editor (or the no-editor placeholder), @@ -226,6 +256,104 @@ define(function (require, exports, module) { }); } + // --- Bottom panel tab helpers --- + + /** + * Resolve the display title for a bottom panel tab. + * First looks for a .toolbar .title element within the panel DOM, + * then falls back to the _KNOWN_PANEL_TITLES map using the panel's DOM id. + * @param {string} id The panel registration ID + * @param {jQueryObject} $panel The panel's jQuery element + * @return {string} + * @private + */ + function _getPanelTitle(id, $panel) { + let $titleEl = $panel.find(".toolbar .title"); + if ($titleEl.length && $.trim($titleEl.text())) { + return $.trim($titleEl.text()); + } + let domId = $panel.attr("id") || ""; + if (_KNOWN_PANEL_TITLES[domId]) { + return _KNOWN_PANEL_TITLES[domId]; + } + if (_KNOWN_PANEL_TITLES[id]) { + return _KNOWN_PANEL_TITLES[id]; + } + // Last resort: humanize the id + let label = (domId || id).replace(/[-_]/g, " "); + return label.charAt(0).toUpperCase() + label.slice(1); + } + + /** + * Rebuild the tab bar DOM from _openBottomPanelIds. + * @private + */ + function _updateBottomPanelTabBar() { + if (!$bottomPanelTabBar) { + return; + } + $bottomPanelTabBar.empty(); + _openBottomPanelIds.forEach(function (panelId) { + let panel = panelIDMap[panelId]; + if (!panel) { + return; + } + let title = panel._tabTitle || _getPanelTitle(panelId, panel.$panel); + let isActive = (panelId === _activeBottomPanelId); + let $tab = $('
' + + '' + $("").text(title).html() + '' + + '×' + + '
'); + $bottomPanelTabBar.append($tab); + }); + } + + /** + * Update the container's dynamic minSize to match the active panel's minSize. + * @private + */ + function _updateContainerMinSize() { + if (!$bottomPanelContainer) { + return; + } + let activeMinSize = _panelMinSizes[_activeBottomPanelId] || 100; + // Add tab bar height (28px) to the panel's minSize + $bottomPanelContainer.data("currentMinSize", activeMinSize + 28); + } + + /** + * Switch the active tab to the given panel. Does not show/hide the container. + * @param {string} panelId + * @private + */ + function _switchToTab(panelId) { + if (_activeBottomPanelId === panelId) { + return; + } + // Remove active class from current + if (_activeBottomPanelId) { + let prevPanel = panelIDMap[_activeBottomPanelId]; + if (prevPanel) { + prevPanel.$panel.removeClass("active-bottom-panel"); + } + } + // Set new active + _activeBottomPanelId = panelId; + let newPanel = panelIDMap[panelId]; + if (newPanel) { + newPanel.$panel.addClass("active-bottom-panel"); + } + _updateContainerMinSize(); + _updateBottomPanelTabBar(); + } + + /** + * Returns a copy of the currently open bottom panel IDs in tab order. + * @return {string[]} + */ + function getOpenBottomPanelIDs() { + return _openBottomPanelIds.slice(); + } /** * Creates a new resizable panel beneath the editor area and above the status bar footer. Panel is initially invisible. @@ -238,16 +366,102 @@ define(function (require, exports, module) { * @return {!Panel} */ function createBottomPanel(id, $panel, minSize) { - $panel.insertBefore("#status-bar"); + // Insert panel into the tabbed container instead of before #status-bar + $bottomPanelContainer.append($panel); $panel.hide(); - updateResizeLimits(); // initialize panel's max size + updateResizeLimits(); + + // Store minSize for dynamic container minSize + _panelMinSizes[id] = minSize || 100; let bottomPanel = new PanelView.Panel($panel, id); panelIDMap[id] = bottomPanel; - Resizer.makeResizable($panel[0], Resizer.DIRECTION_VERTICAL, Resizer.POSITION_TOP, minSize, - false, undefined, true); - listenToResize($panel); + // Cache the tab title at creation time + bottomPanel._tabTitle = _getPanelTitle(id, $panel); + + // Do NOT call Resizer.makeResizable on individual panels. + // The container handles resizing. + + // --- Override show/hide/isVisible on the Panel instance --- + + bottomPanel.show = function () { + if (!this.canBeShown()) { + return; + } + let panelId = this.panelID; + let isOpen = _openBottomPanelIds.indexOf(panelId) !== -1; + let isActive = (_activeBottomPanelId === panelId); + + if (isOpen && isActive) { + // Already open and active - no-op + return; + } + if (isOpen && !isActive) { + // Open but not active - just switch tab + _switchToTab(panelId); + PanelView.trigger(PanelView.EVENT_PANEL_SHOWN, panelId); + triggerUpdateLayout(); + return; + } + // Not open: add to open set + _openBottomPanelIds.push(panelId); + + // Show container if it was hidden + if (!$bottomPanelContainer.is(":visible")) { + Resizer.show($bottomPanelContainer[0]); + } + + _switchToTab(panelId); + PanelView.trigger(PanelView.EVENT_PANEL_SHOWN, panelId); + triggerUpdateLayout(); + }; + + bottomPanel.hide = function () { + let panelId = this.panelID; + let idx = _openBottomPanelIds.indexOf(panelId); + if (idx === -1) { + // Not open - no-op + return; + } + + // Remove from open set + _openBottomPanelIds.splice(idx, 1); + this.$panel.removeClass("active-bottom-panel"); + + let wasActive = (_activeBottomPanelId === panelId); + + if (wasActive) { + if (_openBottomPanelIds.length > 0) { + // Activate the next tab (or previous if this was the last) + let nextIdx = Math.min(idx, _openBottomPanelIds.length - 1); + let nextId = _openBottomPanelIds[nextIdx]; + _activeBottomPanelId = null; // clear so _switchToTab runs + _switchToTab(nextId); + PanelView.trigger(PanelView.EVENT_PANEL_SHOWN, nextId); + } else { + // No more tabs - hide the container + _activeBottomPanelId = null; + Resizer.hide($bottomPanelContainer[0]); + _updateBottomPanelTabBar(); + } + } else { + _updateBottomPanelTabBar(); + } + + PanelView.trigger(PanelView.EVENT_PANEL_HIDDEN, panelId); + triggerUpdateLayout(); + }; + + bottomPanel.isVisible = function () { + return (_activeBottomPanelId === this.panelID) && + $bottomPanelContainer.is(":visible"); + }; + + bottomPanel.setTitle = function (newTitle) { + this._tabTitle = newTitle; + _updateBottomPanelTabBar(); + }; return bottomPanel; } @@ -324,6 +538,37 @@ define(function (require, exports, module) { $mainPluginPanel = $("#main-plugin-panel"); $pluginIconsBar = $("#plugin-icons-bar"); + // --- Create the bottom panel tabbed container --- + $bottomPanelContainer = $('
'); + $bottomPanelTabBar = $('
'); + $bottomPanelContainer.append($bottomPanelTabBar); + $bottomPanelContainer.insertBefore("#status-bar"); + $bottomPanelContainer.hide(); + + // Make the container resizable (not individual panels) + Resizer.makeResizable($bottomPanelContainer[0], Resizer.DIRECTION_VERTICAL, Resizer.POSITION_TOP, + 100, false, undefined, true); + listenToResize($bottomPanelContainer); + + // Tab bar click handlers + $bottomPanelTabBar.on("click", ".bottom-panel-tab-close", function (e) { + e.stopPropagation(); + let panelId = $(this).closest(".bottom-panel-tab").data("panel-id"); + let panel = panelIDMap[panelId]; + if (panel) { + panel.hide(); + } + }); + + $bottomPanelTabBar.on("click", ".bottom-panel-tab", function (e) { + let panelId = $(this).data("panel-id"); + if (panelId && panelId !== _activeBottomPanelId) { + _switchToTab(panelId); + PanelView.trigger(PanelView.EVENT_PANEL_SHOWN, panelId); + triggerUpdateLayout(); + } + }); + // Sidebar is a special case: it isn't a Panel, and is not created dynamically. Need to explicitly // listen for resize here. listenToResize($("#sidebar")); @@ -572,6 +817,7 @@ define(function (require, exports, module) { exports.recomputeLayout = recomputeLayout; exports.getAllPanelIDs = getAllPanelIDs; exports.getPanelForID = getPanelForID; + exports.getOpenBottomPanelIDs = getOpenBottomPanelIDs; exports.addEscapeKeyEventHandler = addEscapeKeyEventHandler; exports.removeEscapeKeyEventHandler = removeEscapeKeyEventHandler; exports._setMockDOM = _setMockDOM; From f5f72bd92d28c5e7297e4c3aedd20870fe4f08e7 Mon Sep 17 00:00:00 2001 From: Pluto Date: Tue, 17 Feb 2026 18:33:43 +0530 Subject: [PATCH 02/16] feat: improve bottom panel tab bar styling --- src/styles/Extn-BottomPanelTabs.less | 112 +++++++++++++++++---------- src/view/WorkspaceManager.js | 23 ++++-- 2 files changed, 85 insertions(+), 50 deletions(-) diff --git a/src/styles/Extn-BottomPanelTabs.less b/src/styles/Extn-BottomPanelTabs.less index f9475ecbca..5d48e5123d 100644 --- a/src/styles/Extn-BottomPanelTabs.less +++ b/src/styles/Extn-BottomPanelTabs.less @@ -18,7 +18,8 @@ * */ -/* Bottom panel tab bar — switches between tabbed bottom panels */ +/* Bottom panel tab bar — switches between tabbed bottom panels. + * Visual style mirrors the file tab bar (Extn-TabBar.less) for consistency. */ #bottom-panel-container { background-color: @bc-panel-bg; @@ -48,20 +49,24 @@ #bottom-panel-tab-bar { display: flex; align-items: center; - height: 28px; - min-height: 28px; - background-color: @bc-panel-bg-promoted; - border-bottom: @bc-panel-separator; - overflow-x: auto; - overflow-y: hidden; + height: 1.5rem; + min-height: 1.5rem; + background-color: #f5f5f5; + border-bottom: none; + overflow: hidden; user-select: none; - box-shadow: inset 0 1px 0 @bc-highlight-hard, 0 -1px 3px @bc-shadow-small; .dark & { - background-color: @dark-bc-panel-bg-promoted; - border-bottom: @dark-bc-panel-separator; - box-shadow: inset 0 1px 0 @dark-bc-highlight, 0 -1px 3px @dark-bc-shadow-small; + background-color: #1E1E1E; } +} + +.bottom-panel-tabs-overflow { + flex: 1; + display: flex; + overflow-x: auto; + overflow-y: hidden; + height: 100%; /* Hide scrollbar but allow scrolling */ &::-webkit-scrollbar { @@ -74,51 +79,54 @@ .bottom-panel-tab { display: inline-flex; align-items: center; - gap: 4px; - padding: 0 10px; + padding: 0 0.4rem 0 0.8rem; height: 100%; cursor: pointer; position: relative; - color: @bc-text-quiet; - font-size: 12px; + flex: 0 0 auto; + min-width: fit-content; + color: #555; + background-color: #f1f1f1; + border-right: 1px solid rgba(0, 0, 0, 0.05); + font-size: 0.8rem; + letter-spacing: 0.4px; white-space: nowrap; - flex-shrink: 0; - transition: color 0.15s ease, background-color 0.15s ease; + transition: color 0.12s ease-out, background-color 0.12s ease-out; .dark & { - color: @dark-bc-text-quiet; + color: #aaa; + background-color: #292929; + border-right: 1px solid rgba(255, 255, 255, 0.05); } &:hover { - color: @bc-text; - background-color: rgba(0, 0, 0, 0.04); + background-color: #e0e0e0; .dark & { - color: @dark-bc-text; - background-color: rgba(255, 255, 255, 0.06); + background-color: #3b3a3a; } } &.active { - color: @bc-text; - background-color: @bc-panel-bg; + color: #333; + background-color: #fff; .dark & { - color: @dark-bc-text; - background-color: @dark-bc-panel-bg; + color: #dedede; + background-color: #1D1F21; } &::after { content: ""; position: absolute; - bottom: 0; + top: 0; left: 0; right: 0; - height: 2px; - background-color: @bc-primary-btn-bg; + height: 0.1rem; + background-color: #0078D7; .dark & { - background-color: @dark-bc-primary-btn-bg; + background-color: #75BEFF; } } } @@ -128,28 +136,48 @@ pointer-events: none; } -.bottom-panel-tab-close { - display: inline-flex; - align-items: center; - justify-content: center; - width: 16px; - height: 16px; - font-size: 14px; - line-height: 1; +.bottom-panel-tab-close-btn { + margin-left: 0.4rem; border-radius: 3px; + cursor: pointer; + color: #999; + font-size: 1rem; + font-weight: 500; + padding: 0 3px; + line-height: 1; opacity: 0; - transition: opacity 0.15s ease, background-color 0.15s ease; + transition: opacity 0.12s ease, color 0.12s ease, background-color 0.12s ease; + + .dark & { + color: #666; + } .bottom-panel-tab:hover & { - opacity: 0.6; + opacity: 1; + color: #666; + + .dark & { + color: #888; + } + } + + .bottom-panel-tab.active & { + opacity: 1; + color: #666; + + .dark & { + color: #888; + } } &:hover { - opacity: 1 !important; + opacity: 1; + color: #333; background-color: rgba(0, 0, 0, 0.1); .dark & { - background-color: rgba(255, 255, 255, 0.15); + color: #fff; + background-color: rgba(255, 255, 255, 0.12); } } } diff --git a/src/view/WorkspaceManager.js b/src/view/WorkspaceManager.js index 204312a4f2..f57ff50109 100644 --- a/src/view/WorkspaceManager.js +++ b/src/view/WorkspaceManager.js @@ -293,6 +293,9 @@ define(function (require, exports, module) { return; } $bottomPanelTabBar.empty(); + + // Scrollable tabs area + let $tabsOverflow = $('
'); _openBottomPanelIds.forEach(function (panelId) { let panel = panelIDMap[panelId]; if (!panel) { @@ -302,10 +305,11 @@ define(function (require, exports, module) { let isActive = (panelId === _activeBottomPanelId); let $tab = $('
' + '' + $("").text(title).html() + '' + - '×' + + '×' + '
'); - $bottomPanelTabBar.append($tab); + $tabsOverflow.append($tab); }); + $bottomPanelTabBar.append($tabsOverflow); } /** @@ -317,8 +321,9 @@ define(function (require, exports, module) { return; } let activeMinSize = _panelMinSizes[_activeBottomPanelId] || 100; - // Add tab bar height (28px) to the panel's minSize - $bottomPanelContainer.data("currentMinSize", activeMinSize + 28); + // Add tab bar height to the panel's minSize + let tabBarHeight = $bottomPanelTabBar ? $bottomPanelTabBar.outerHeight() : 34; + $bottomPanelContainer.data("currentMinSize", activeMinSize + tabBarHeight); } /** @@ -551,12 +556,14 @@ define(function (require, exports, module) { listenToResize($bottomPanelContainer); // Tab bar click handlers - $bottomPanelTabBar.on("click", ".bottom-panel-tab-close", function (e) { + $bottomPanelTabBar.on("click", ".bottom-panel-tab-close-btn", function (e) { e.stopPropagation(); let panelId = $(this).closest(".bottom-panel-tab").data("panel-id"); - let panel = panelIDMap[panelId]; - if (panel) { - panel.hide(); + if (panelId) { + let panel = panelIDMap[panelId]; + if (panel) { + panel.hide(); + } } }); From f0807e4caa76d4aee68557dcb40d3f3ec986fb66 Mon Sep 17 00:00:00 2001 From: Pluto Date: Tue, 17 Feb 2026 18:45:33 +0530 Subject: [PATCH 03/16] feat: remove redundant box shadow color between panel and tabbar --- src/styles/Extn-BottomPanelTabs.less | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/styles/Extn-BottomPanelTabs.less b/src/styles/Extn-BottomPanelTabs.less index 5d48e5123d..19552c6fff 100644 --- a/src/styles/Extn-BottomPanelTabs.less +++ b/src/styles/Extn-BottomPanelTabs.less @@ -43,6 +43,10 @@ display: flex !important; flex-direction: column; } + + .toolbar { + box-shadow: none; + } } } From 72db91570ff48a3a00ce682f14fc2479ae5b8d99 Mon Sep 17 00:00:00 2001 From: Pluto Date: Tue, 17 Feb 2026 18:54:26 +0530 Subject: [PATCH 04/16] refactor: reduce height of active tab styling --- src/styles/Extn-BottomPanelTabs.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/styles/Extn-BottomPanelTabs.less b/src/styles/Extn-BottomPanelTabs.less index 19552c6fff..8aab496a7d 100644 --- a/src/styles/Extn-BottomPanelTabs.less +++ b/src/styles/Extn-BottomPanelTabs.less @@ -126,7 +126,7 @@ top: 0; left: 0; right: 0; - height: 0.1rem; + height: 0.07rem; background-color: #0078D7; .dark & { From b9a86889a6af02bb83c524209dabf5a947bb8c80 Mon Sep 17 00:00:00 2001 From: Pluto Date: Tue, 17 Feb 2026 21:36:59 +0530 Subject: [PATCH 05/16] feat: better support for bottom panel tab bar title --- .../default/DebugCommands/testBuilder.js | 4 +- src/extensions/default/Git/src/Panel.js | 2 +- .../CustomSnippets/main.js | 3 +- .../DisplayShortcuts/main.js | 3 +- src/features/FindReferencesManager.js | 3 +- src/language/CodeInspection.js | 2 +- src/nls/root/strings.js | 3 ++ src/search/FindInFilesUI.js | 3 +- src/search/SearchResultsView.js | 5 ++- src/view/WorkspaceManager.js | 41 +++++-------------- 10 files changed, 28 insertions(+), 41 deletions(-) diff --git a/src/extensions/default/DebugCommands/testBuilder.js b/src/extensions/default/DebugCommands/testBuilder.js index 38e58ddf0e..bfc9b434af 100644 --- a/src/extensions/default/DebugCommands/testBuilder.js +++ b/src/extensions/default/DebugCommands/testBuilder.js @@ -38,7 +38,7 @@ define(function (require, exports, module) { function toggleTestBuilder() { if(!$panel){ $panel = $(panelHTML); - builderPanel = WorkspaceManager.createBottomPanel("phcode-test-builder-panel", $panel, 100); + builderPanel = WorkspaceManager.createBottomPanel("phcode-test-builder-panel", $panel, 100, "Test Builder"); builderPanel.hide(); _setupPanel().then(()=>{ builderPanel.setVisible(!builderPanel.isVisible()); @@ -177,7 +177,7 @@ define(function (require, exports, module) { return; } $panel = $(panelHTML); - builderPanel = WorkspaceManager.createBottomPanel("phcode-test-builder-panel", $panel, 100); + builderPanel = WorkspaceManager.createBottomPanel("phcode-test-builder-panel", $panel, 100, "Test Builder"); builderPanel.hide(); _setupPanel(); }); diff --git a/src/extensions/default/Git/src/Panel.js b/src/extensions/default/Git/src/Panel.js index 02a34cfcdc..cc4aac6dcb 100644 --- a/src/extensions/default/Git/src/Panel.js +++ b/src/extensions/default/Git/src/Panel.js @@ -1240,7 +1240,7 @@ define(function (require, exports) { var $panelHtml = $(panelHtml); $panelHtml.find(".git-available, .git-not-available").hide(); - gitPanel = WorkspaceManager.createBottomPanel("main-git.panel", $panelHtml, 100); + gitPanel = WorkspaceManager.createBottomPanel("main-git.panel", $panelHtml, 100, Strings.GIT_PANEL_TITLE); $gitPanel = gitPanel.$panel; const resizeObserver = new ResizeObserver(_panelResized); resizeObserver.observe($gitPanel[0]); diff --git a/src/extensionsIntegrated/CustomSnippets/main.js b/src/extensionsIntegrated/CustomSnippets/main.js index 5e2b1b3d0f..6ca00fbfa2 100644 --- a/src/extensionsIntegrated/CustomSnippets/main.js +++ b/src/extensionsIntegrated/CustomSnippets/main.js @@ -58,7 +58,8 @@ define(function (require, exports, module) { * @private */ function _createPanel() { - customSnippetsPanel = WorkspaceManager.createBottomPanel(PANEL_ID, $snippetsPanel, PANEL_MIN_SIZE); + customSnippetsPanel = WorkspaceManager.createBottomPanel(PANEL_ID, $snippetsPanel, PANEL_MIN_SIZE, + Strings.CUSTOM_SNIPPETS_PANEL_TITLE); customSnippetsPanel.show(); // also register the handlers diff --git a/src/extensionsIntegrated/DisplayShortcuts/main.js b/src/extensionsIntegrated/DisplayShortcuts/main.js index 857ddf79ce..96b41cd769 100644 --- a/src/extensionsIntegrated/DisplayShortcuts/main.js +++ b/src/extensionsIntegrated/DisplayShortcuts/main.js @@ -478,7 +478,8 @@ define(function (require, exports, module) { // AppInit.htmlReady() has already executed before extensions are loaded // so, for now, we need to call this ourself - panel = WorkspaceManager.createBottomPanel(TOGGLE_SHORTCUTS_ID, $(s), 300); + panel = WorkspaceManager.createBottomPanel(TOGGLE_SHORTCUTS_ID, $(s), 300, + Strings.KEYBOARD_SHORTCUT_PANEL_TITLE); panel.hide(); $shortcutsPanel = $("#shortcuts-panel"); diff --git a/src/features/FindReferencesManager.js b/src/features/FindReferencesManager.js index 0c71269278..2cb213c871 100644 --- a/src/features/FindReferencesManager.js +++ b/src/features/FindReferencesManager.js @@ -194,7 +194,8 @@ define(function (require, exports, module) { searchModel, "reference-in-files-results", "reference-in-files.results", - "reference" + "reference", + Strings.REFERENCES_PANEL_TITLE ); if(_resultsView) { _resultsView diff --git a/src/language/CodeInspection.js b/src/language/CodeInspection.js index 8cbb15d89f..01a8352bda 100644 --- a/src/language/CodeInspection.js +++ b/src/language/CodeInspection.js @@ -1263,7 +1263,7 @@ define(function (require, exports, module) { Editor.registerGutter(CODE_INSPECTION_GUTTER, CODE_INSPECTION_GUTTER_PRIORITY); // Create bottom panel to list error details var panelHtml = Mustache.render(PanelTemplate, Strings); - problemsPanel = WorkspaceManager.createBottomPanel("errors", $(panelHtml), 100); + problemsPanel = WorkspaceManager.createBottomPanel("errors", $(panelHtml), 100, Strings.CMD_VIEW_TOGGLE_PROBLEMS); $problemsPanel = $("#problems-panel"); $fixAllBtn = $problemsPanel.find(".problems-fix-all-btn"); $fixAllBtn.click(()=>{ diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index cde5aeabfb..650a58bb7c 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -1249,6 +1249,8 @@ define({ "REFERENCES_IN_FILES": "references", "REFERENCE_IN_FILES": "reference", "REFERENCES_NO_RESULTS": "No References available for current cursor position", + "REFERENCES_PANEL_TITLE": "References", + "SEARCH_RESULTS_PANEL_TITLE": "Search Results", "CMD_FIND_DOCUMENT_SYMBOLS": "Find Document Symbols", "CMD_FIND_PROJECT_SYMBOLS": "Find Project Symbols", @@ -1396,6 +1398,7 @@ define({ "BUTTON_CANCEL": "Cancel", "CHECKOUT_COMMIT": "Checkout", "CHECKOUT_COMMIT_DETAIL": "Commit Message: {0}
Commit hash: {1}", + "GIT_PANEL_TITLE": "Git", "GIT_CLONE": "Clone", "BUTTON_CLOSE": "Close", "BUTTON_COMMIT": "Commit", diff --git a/src/search/FindInFilesUI.js b/src/search/FindInFilesUI.js index 6a02cd7e19..0aea171f61 100644 --- a/src/search/FindInFilesUI.js +++ b/src/search/FindInFilesUI.js @@ -536,7 +536,8 @@ define(function (require, exports, module) { // Initialize items dependent on HTML DOM AppInit.htmlReady(function () { var model = FindInFiles.searchModel; - _resultsView = new SearchResultsView(model, "find-in-files-results", "find-in-files.results"); + _resultsView = new SearchResultsView(model, "find-in-files-results", "find-in-files.results", + undefined, Strings.SEARCH_RESULTS_PANEL_TITLE); _resultsView .on("replaceBatch", function () { _finishReplaceBatch(model); diff --git a/src/search/SearchResultsView.js b/src/search/SearchResultsView.js index 54751a67e3..5557cf96d9 100644 --- a/src/search/SearchResultsView.js +++ b/src/search/SearchResultsView.js @@ -76,12 +76,13 @@ define(function (require, exports, module) { * @param {string} panelID The CSS ID to use for the panel. * @param {string} panelName The name to use for the panel, as passed to WorkspaceManager.createBottomPanel(). * @param {string} type type to identify if it is reference search or string match serach + * @param {string=} title Display title for the panel tab. */ - function SearchResultsView(model, panelID, panelName, type) { + function SearchResultsView(model, panelID, panelName, type, title) { const self = this; let panelHtml = Mustache.render(searchPanelTemplate, {panelID: panelID}); - this._panel = WorkspaceManager.createBottomPanel(panelName, $(panelHtml), 100); + this._panel = WorkspaceManager.createBottomPanel(panelName, $(panelHtml), 100, title); this._$summary = this._panel.$panel.find(".title"); this._$table = this._panel.$panel.find(".table-container"); this._$previewEditor = this._panel.$panel.find(".search-editor-preview"); diff --git a/src/view/WorkspaceManager.js b/src/view/WorkspaceManager.js index f57ff50109..e16282e837 100644 --- a/src/view/WorkspaceManager.js +++ b/src/view/WorkspaceManager.js @@ -140,18 +140,6 @@ define(function (require, exports, module) { /** @type {Object} Map of panelID -> minSize for bottom panels */ let _panelMinSizes = {}; - /** Fallback title map for panels that don't have a .toolbar .title element */ - const _KNOWN_PANEL_TITLES = { - "errors": "Problems", - "problems-panel": "Problems", - "search-results": "Search Results", - "find-in-files-results": "Search Results", - "references-panel": "References", - "git-panel": "Git", - "keyboard-shortcuts-panel": "Keyboard Shortcuts", - "custom-snippets-panel": "Custom Snippets", - "test-builder-panel": "Test Builder" - }; /** @@ -260,27 +248,17 @@ define(function (require, exports, module) { /** * Resolve the display title for a bottom panel tab. - * First looks for a .toolbar .title element within the panel DOM, - * then falls back to the _KNOWN_PANEL_TITLES map using the panel's DOM id. + * Uses the explicit title if provided, otherwise humanizes the panel id. * @param {string} id The panel registration ID - * @param {jQueryObject} $panel The panel's jQuery element + * @param {string=} title Explicit title passed to createBottomPanel * @return {string} * @private */ - function _getPanelTitle(id, $panel) { - let $titleEl = $panel.find(".toolbar .title"); - if ($titleEl.length && $.trim($titleEl.text())) { - return $.trim($titleEl.text()); + function _getPanelTitle(id, title) { + if (title) { + return title; } - let domId = $panel.attr("id") || ""; - if (_KNOWN_PANEL_TITLES[domId]) { - return _KNOWN_PANEL_TITLES[domId]; - } - if (_KNOWN_PANEL_TITLES[id]) { - return _KNOWN_PANEL_TITLES[id]; - } - // Last resort: humanize the id - let label = (domId || id).replace(/[-_]/g, " "); + let label = id.replace(new RegExp("[-_.]", "g"), " ").split(" ")[0]; return label.charAt(0).toUpperCase() + label.slice(1); } @@ -301,7 +279,7 @@ define(function (require, exports, module) { if (!panel) { return; } - let title = panel._tabTitle || _getPanelTitle(panelId, panel.$panel); + let title = panel._tabTitle || _getPanelTitle(panelId); let isActive = (panelId === _activeBottomPanelId); let $tab = $('
' + '' + $("").text(title).html() + '' + @@ -368,9 +346,10 @@ define(function (require, exports, module) { * @param {!jQueryObject} $panel DOM content to use as the panel. Need not be in the document yet. Must have an id * attribute, for use as a preferences key. * @param {number=} minSize Minimum height of panel in px. + * @param {string=} title Display title shown in the bottom panel tab bar. * @return {!Panel} */ - function createBottomPanel(id, $panel, minSize) { + function createBottomPanel(id, $panel, minSize, title) { // Insert panel into the tabbed container instead of before #status-bar $bottomPanelContainer.append($panel); $panel.hide(); @@ -383,7 +362,7 @@ define(function (require, exports, module) { panelIDMap[id] = bottomPanel; // Cache the tab title at creation time - bottomPanel._tabTitle = _getPanelTitle(id, $panel); + bottomPanel._tabTitle = _getPanelTitle(id, title); // Do NOT call Resizer.makeResizable on individual panels. // The container handles resizing. From 356f7b2b40f6d9b057489854a0f8feb282885c01 Mon Sep 17 00:00:00 2001 From: Pluto Date: Tue, 17 Feb 2026 21:51:26 +0530 Subject: [PATCH 06/16] feat: better support for tab naming using dom lookup --- src/view/WorkspaceManager.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/view/WorkspaceManager.js b/src/view/WorkspaceManager.js index e16282e837..823631cf23 100644 --- a/src/view/WorkspaceManager.js +++ b/src/view/WorkspaceManager.js @@ -248,16 +248,22 @@ define(function (require, exports, module) { /** * Resolve the display title for a bottom panel tab. - * Uses the explicit title if provided, otherwise humanizes the panel id. + * Uses the explicit title if provided, then checks for a .toolbar .title + * DOM element in the panel, and finally derives a name from the panel id. * @param {string} id The panel registration ID + * @param {jQueryObject} $panel The panel's jQuery element * @param {string=} title Explicit title passed to createBottomPanel * @return {string} * @private */ - function _getPanelTitle(id, title) { + function _getPanelTitle(id, $panel, title) { if (title) { return title; } + let $titleEl = $panel.find(".toolbar .title"); + if ($titleEl.length && $.trim($titleEl.text())) { + return $.trim($titleEl.text()); + } let label = id.replace(new RegExp("[-_.]", "g"), " ").split(" ")[0]; return label.charAt(0).toUpperCase() + label.slice(1); } @@ -279,7 +285,7 @@ define(function (require, exports, module) { if (!panel) { return; } - let title = panel._tabTitle || _getPanelTitle(panelId); + let title = panel._tabTitle || _getPanelTitle(panelId, panel.$panel); let isActive = (panelId === _activeBottomPanelId); let $tab = $('
' + '' + $("").text(title).html() + '' + @@ -362,7 +368,7 @@ define(function (require, exports, module) { panelIDMap[id] = bottomPanel; // Cache the tab title at creation time - bottomPanel._tabTitle = _getPanelTitle(id, title); + bottomPanel._tabTitle = _getPanelTitle(id, $panel, title); // Do NOT call Resizer.makeResizable on individual panels. // The container handles resizing. From 0ca722f3a82b88911e0420c1e0b4c0970949b4bf Mon Sep 17 00:00:00 2001 From: Pluto Date: Tue, 17 Feb 2026 22:19:23 +0530 Subject: [PATCH 07/16] feat: prevent full tab bar dom rebuild on panel switch --- src/view/WorkspaceManager.js | 54 ++++++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/src/view/WorkspaceManager.js b/src/view/WorkspaceManager.js index 823631cf23..fbc771eace 100644 --- a/src/view/WorkspaceManager.js +++ b/src/view/WorkspaceManager.js @@ -272,6 +272,11 @@ define(function (require, exports, module) { * Rebuild the tab bar DOM from _openBottomPanelIds. * @private */ + /** + * Full rebuild of the tab bar DOM from _openBottomPanelIds. + * Call this when tabs are added, removed, or renamed. + * @private + */ function _updateBottomPanelTabBar() { if (!$bottomPanelTabBar) { return; @@ -296,6 +301,24 @@ define(function (require, exports, module) { $bottomPanelTabBar.append($tabsOverflow); } + /** + * Swap the .active class on the tab bar without rebuilding the DOM. + * @private + */ + function _updateActiveTabHighlight() { + if (!$bottomPanelTabBar) { + return; + } + $bottomPanelTabBar.find(".bottom-panel-tab").each(function () { + let $tab = $(this); + if ($tab.data("panel-id") === _activeBottomPanelId) { + $tab.addClass("active"); + } else { + $tab.removeClass("active"); + } + }); + } + /** * Update the container's dynamic minSize to match the active panel's minSize. * @private @@ -333,7 +356,7 @@ define(function (require, exports, module) { newPanel.$panel.addClass("active-bottom-panel"); } _updateContainerMinSize(); - _updateBottomPanelTabBar(); + _updateActiveTabHighlight(); } /** @@ -403,6 +426,7 @@ define(function (require, exports, module) { } _switchToTab(panelId); + _updateBottomPanelTabBar(); PanelView.trigger(PanelView.EVENT_PANEL_SHOWN, panelId); triggerUpdateLayout(); }; @@ -421,23 +445,19 @@ define(function (require, exports, module) { let wasActive = (_activeBottomPanelId === panelId); - if (wasActive) { - if (_openBottomPanelIds.length > 0) { - // Activate the next tab (or previous if this was the last) - let nextIdx = Math.min(idx, _openBottomPanelIds.length - 1); - let nextId = _openBottomPanelIds[nextIdx]; - _activeBottomPanelId = null; // clear so _switchToTab runs - _switchToTab(nextId); - PanelView.trigger(PanelView.EVENT_PANEL_SHOWN, nextId); - } else { - // No more tabs - hide the container - _activeBottomPanelId = null; - Resizer.hide($bottomPanelContainer[0]); - _updateBottomPanelTabBar(); - } - } else { - _updateBottomPanelTabBar(); + // Tab was removed — rebuild tab bar, then activate next if needed + if (wasActive && _openBottomPanelIds.length > 0) { + let nextIdx = Math.min(idx, _openBottomPanelIds.length - 1); + let nextId = _openBottomPanelIds[nextIdx]; + _activeBottomPanelId = null; // clear so _switchToTab runs + _switchToTab(nextId); + PanelView.trigger(PanelView.EVENT_PANEL_SHOWN, nextId); + } else if (wasActive) { + // No more tabs - hide the container + _activeBottomPanelId = null; + Resizer.hide($bottomPanelContainer[0]); } + _updateBottomPanelTabBar(); PanelView.trigger(PanelView.EVENT_PANEL_HIDDEN, panelId); triggerUpdateLayout(); From 497897093a5d75b043fdda0d93072f166d889729 Mon Sep 17 00:00:00 2001 From: Pluto Date: Tue, 17 Feb 2026 23:19:13 +0530 Subject: [PATCH 08/16] feat: deprecate min size param from bottom panel --- src/utils/Resizer.js | 8 +++----- src/view/WorkspaceManager.js | 27 ++------------------------- 2 files changed, 5 insertions(+), 30 deletions(-) diff --git a/src/utils/Resizer.js b/src/utils/Resizer.js index 88caab5411..9ae4e29886 100644 --- a/src/utils/Resizer.js +++ b/src/utils/Resizer.js @@ -411,9 +411,8 @@ define(function (require, exports, module) { if(initialSize){ elementSize = elementPrefs.size || initialSize; } - let effectiveMinSize = $element.data("currentMinSize") || minSize; - if(elementSize minSize for bottom panels */ - let _panelMinSizes = {}; - - - /** * Calculates the available height for the full-size Editor (or the no-editor placeholder), * accounting for the current size of all visible panels, toolbar, & status bar. @@ -319,20 +314,6 @@ define(function (require, exports, module) { }); } - /** - * Update the container's dynamic minSize to match the active panel's minSize. - * @private - */ - function _updateContainerMinSize() { - if (!$bottomPanelContainer) { - return; - } - let activeMinSize = _panelMinSizes[_activeBottomPanelId] || 100; - // Add tab bar height to the panel's minSize - let tabBarHeight = $bottomPanelTabBar ? $bottomPanelTabBar.outerHeight() : 34; - $bottomPanelContainer.data("currentMinSize", activeMinSize + tabBarHeight); - } - /** * Switch the active tab to the given panel. Does not show/hide the container. * @param {string} panelId @@ -355,7 +336,6 @@ define(function (require, exports, module) { if (newPanel) { newPanel.$panel.addClass("active-bottom-panel"); } - _updateContainerMinSize(); _updateActiveTabHighlight(); } @@ -374,7 +354,7 @@ define(function (require, exports, module) { * @param {!string} id Unique id for this panel. Use package-style naming, e.g. "myextension.feature.panelname" * @param {!jQueryObject} $panel DOM content to use as the panel. Need not be in the document yet. Must have an id * attribute, for use as a preferences key. - * @param {number=} minSize Minimum height of panel in px. + * @param {number=} minSize @deprecated No longer used. Pass `undefined`. * @param {string=} title Display title shown in the bottom panel tab bar. * @return {!Panel} */ @@ -384,9 +364,6 @@ define(function (require, exports, module) { $panel.hide(); updateResizeLimits(); - // Store minSize for dynamic container minSize - _panelMinSizes[id] = minSize || 100; - let bottomPanel = new PanelView.Panel($panel, id); panelIDMap[id] = bottomPanel; @@ -557,7 +534,7 @@ define(function (require, exports, module) { // Make the container resizable (not individual panels) Resizer.makeResizable($bottomPanelContainer[0], Resizer.DIRECTION_VERTICAL, Resizer.POSITION_TOP, - 100, false, undefined, true); + 200, false, undefined, true); listenToResize($bottomPanelContainer); // Tab bar click handlers From c923b91d5e362e6bf79e38638f032224bd72efe7 Mon Sep 17 00:00:00 2001 From: Pluto Date: Tue, 17 Feb 2026 23:41:21 +0530 Subject: [PATCH 09/16] refactor: improve bottom panel tab bar styling --- src/styles/Extn-BottomPanelTabs.less | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/styles/Extn-BottomPanelTabs.less b/src/styles/Extn-BottomPanelTabs.less index 8aab496a7d..996cf096b0 100644 --- a/src/styles/Extn-BottomPanelTabs.less +++ b/src/styles/Extn-BottomPanelTabs.less @@ -53,8 +53,8 @@ #bottom-panel-tab-bar { display: flex; align-items: center; - height: 1.5rem; - min-height: 1.5rem; + height: 2rem; + min-height: 2rem; background-color: #f5f5f5; border-bottom: none; overflow: hidden; @@ -92,8 +92,9 @@ color: #555; background-color: #f1f1f1; border-right: 1px solid rgba(0, 0, 0, 0.05); - font-size: 0.8rem; - letter-spacing: 0.4px; + font-size: 1rem; + font-weight: 500; + letter-spacing: 0.5px; white-space: nowrap; transition: color 0.12s ease-out, background-color 0.12s ease-out; @@ -126,7 +127,7 @@ top: 0; left: 0; right: 0; - height: 0.07rem; + height: 0.1rem; background-color: #0078D7; .dark & { @@ -141,13 +142,13 @@ } .bottom-panel-tab-close-btn { - margin-left: 0.4rem; + margin-left: 0.55rem; border-radius: 3px; cursor: pointer; color: #999; - font-size: 1rem; + font-size: 1.25rem; font-weight: 500; - padding: 0 3px; + padding: 0 4px; line-height: 1; opacity: 0; transition: opacity 0.12s ease, color 0.12s ease, background-color 0.12s ease; From 5fd9c137c1ebfc2ff1f9957d6ac4363e81fb4c08 Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 18 Feb 2026 14:10:24 +0530 Subject: [PATCH 10/16] fix: redundant duplicated jsdoc --- src/view/WorkspaceManager.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/view/WorkspaceManager.js b/src/view/WorkspaceManager.js index 54a5f7c33c..515a0bd91a 100644 --- a/src/view/WorkspaceManager.js +++ b/src/view/WorkspaceManager.js @@ -263,10 +263,6 @@ define(function (require, exports, module) { return label.charAt(0).toUpperCase() + label.slice(1); } - /** - * Rebuild the tab bar DOM from _openBottomPanelIds. - * @private - */ /** * Full rebuild of the tab bar DOM from _openBottomPanelIds. * Call this when tabs are added, removed, or renamed. From 611b46ca580ad44a92d4986bebe60bb67aa3c3ef Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 18 Feb 2026 14:39:03 +0530 Subject: [PATCH 11/16] feat: add single close button that hides panel --- src/styles/Extn-BottomPanelTabs.less | 43 +++++++++++++++++++++++++++- src/view/WorkspaceManager.js | 39 +++++++++++++++++++------ 2 files changed, 73 insertions(+), 9 deletions(-) diff --git a/src/styles/Extn-BottomPanelTabs.less b/src/styles/Extn-BottomPanelTabs.less index 996cf096b0..d2843e22b7 100644 --- a/src/styles/Extn-BottomPanelTabs.less +++ b/src/styles/Extn-BottomPanelTabs.less @@ -46,6 +46,10 @@ .toolbar { box-shadow: none; + + .close { + display: none; + } } } } @@ -61,7 +65,7 @@ user-select: none; .dark & { - background-color: #1E1E1E; + background-color: #222222; } } @@ -186,3 +190,40 @@ } } } + +.bottom-panel-tab-bar-actions { + display: flex; + align-items: center; + height: 100%; + margin-left: auto; + padding: 0 0.25rem; + flex: 0 0 auto; +} + +.bottom-panel-close-all-btn { + display: flex; + align-items: center; + justify-content: center; + width: 1.6rem; + height: 1.6rem; + border-radius: 3px; + cursor: pointer; + color: #777; + font-size: 1.3rem; + line-height: 1; + transition: color 0.12s ease, background-color 0.12s ease; + + .dark & { + color: #777; + } + + &:hover { + color: #333; + background-color: rgba(0, 0, 0, 0.1); + + .dark & { + color: #fff; + background-color: rgba(255, 255, 255, 0.12); + } + } +} diff --git a/src/view/WorkspaceManager.js b/src/view/WorkspaceManager.js index 515a0bd91a..5683f4f84f 100644 --- a/src/view/WorkspaceManager.js +++ b/src/view/WorkspaceManager.js @@ -131,6 +131,9 @@ define(function (require, exports, module) { /** @type {jQueryObject} The tab bar inside the container */ let $bottomPanelTabBar; + /** @type {jQueryObject} Scrollable area holding the tab elements */ + let $bottomPanelTabsOverflow; + /** @type {string[]} Ordered list of currently open (tabbed) panel IDs */ let _openBottomPanelIds = []; @@ -269,13 +272,11 @@ define(function (require, exports, module) { * @private */ function _updateBottomPanelTabBar() { - if (!$bottomPanelTabBar) { + if (!$bottomPanelTabsOverflow) { return; } - $bottomPanelTabBar.empty(); + $bottomPanelTabsOverflow.empty(); - // Scrollable tabs area - let $tabsOverflow = $('
'); _openBottomPanelIds.forEach(function (panelId) { let panel = panelIDMap[panelId]; if (!panel) { @@ -287,9 +288,8 @@ define(function (require, exports, module) { '' + $("").text(title).html() + '' + '×' + '
'); - $tabsOverflow.append($tab); + $bottomPanelTabsOverflow.append($tab); }); - $bottomPanelTabBar.append($tabsOverflow); } /** @@ -380,12 +380,19 @@ define(function (require, exports, module) { let isActive = (_activeBottomPanelId === panelId); if (isOpen && isActive) { - // Already open and active - no-op + // Already open and active — just ensure container is visible + if (!$bottomPanelContainer.is(":visible")) { + Resizer.show($bottomPanelContainer[0]); + triggerUpdateLayout(); + } return; } if (isOpen && !isActive) { - // Open but not active - just switch tab + // Open but not active - switch tab and ensure container is visible _switchToTab(panelId); + if (!$bottomPanelContainer.is(":visible")) { + Resizer.show($bottomPanelContainer[0]); + } PanelView.trigger(PanelView.EVENT_PANEL_SHOWN, panelId); triggerUpdateLayout(); return; @@ -524,6 +531,13 @@ define(function (require, exports, module) { // --- Create the bottom panel tabbed container --- $bottomPanelContainer = $('
'); $bottomPanelTabBar = $('
'); + $bottomPanelTabsOverflow = $('
'); + let $tabBarActions = $('
'); + $tabBarActions.append( + '×' + ); + $bottomPanelTabBar.append($bottomPanelTabsOverflow); + $bottomPanelTabBar.append($tabBarActions); $bottomPanelContainer.append($bottomPanelTabBar); $bottomPanelContainer.insertBefore("#status-bar"); $bottomPanelContainer.hide(); @@ -554,6 +568,15 @@ define(function (require, exports, module) { } }); + // Close-panel button collapses the container but keeps tabs intact + $bottomPanelTabBar.on("click", ".bottom-panel-close-all-btn", function (e) { + e.stopPropagation(); + if ($bottomPanelContainer.is(":visible")) { + Resizer.hide($bottomPanelContainer[0]); + triggerUpdateLayout(); + } + }); + // Sidebar is a special case: it isn't a Panel, and is not created dynamically. Need to explicitly // listen for resize here. listenToResize($("#sidebar")); From 904c29ef53ce9533b40c8012fc7f770398f9762f Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 18 Feb 2026 16:50:36 +0530 Subject: [PATCH 12/16] =?UTF-8?q?feat:=20hide=20the=20bottom=20panel=20and?= =?UTF-8?q?=20don=E2=80=99t=20destroy=20any=20active=20ones?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/styles/Extn-BottomPanelTabs.less | 18 +++++++----------- src/view/WorkspaceManager.js | 6 +++--- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/styles/Extn-BottomPanelTabs.less b/src/styles/Extn-BottomPanelTabs.less index d2843e22b7..c5405f35ba 100644 --- a/src/styles/Extn-BottomPanelTabs.less +++ b/src/styles/Extn-BottomPanelTabs.less @@ -46,10 +46,6 @@ .toolbar { box-shadow: none; - - .close { - display: none; - } } } } @@ -200,29 +196,29 @@ flex: 0 0 auto; } -.bottom-panel-close-all-btn { +.bottom-panel-hide-btn { display: flex; align-items: center; justify-content: center; width: 1.6rem; height: 1.6rem; + margin-right: 0.3rem; border-radius: 3px; cursor: pointer; - color: #777; - font-size: 1.3rem; + color: #333; + font-size: 0.8rem; + -webkit-text-stroke: 0.4px; line-height: 1; - transition: color 0.12s ease, background-color 0.12s ease; + transition: background-color 0.12s ease; .dark & { - color: #777; + color: #fff; } &:hover { - color: #333; background-color: rgba(0, 0, 0, 0.1); .dark & { - color: #fff; background-color: rgba(255, 255, 255, 0.12); } } diff --git a/src/view/WorkspaceManager.js b/src/view/WorkspaceManager.js index 5683f4f84f..c2f75d4110 100644 --- a/src/view/WorkspaceManager.js +++ b/src/view/WorkspaceManager.js @@ -534,7 +534,7 @@ define(function (require, exports, module) { $bottomPanelTabsOverflow = $('
'); let $tabBarActions = $('
'); $tabBarActions.append( - '×' + '' ); $bottomPanelTabBar.append($bottomPanelTabsOverflow); $bottomPanelTabBar.append($tabBarActions); @@ -568,8 +568,8 @@ define(function (require, exports, module) { } }); - // Close-panel button collapses the container but keeps tabs intact - $bottomPanelTabBar.on("click", ".bottom-panel-close-all-btn", function (e) { + // Hide-panel button collapses the container but keeps tabs intact + $bottomPanelTabBar.on("click", ".bottom-panel-hide-btn", function (e) { e.stopPropagation(); if ($bottomPanelContainer.is(":visible")) { Resizer.hide($bottomPanelContainer[0]); From 132ebc0788c5a2a9c2d0cf9f738892de0c747915 Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 18 Feb 2026 20:48:20 +0530 Subject: [PATCH 13/16] feat: add button to toggle the bottom panel via status bar --- src/styles/brackets.less | 8 ++++++++ src/view/WorkspaceManager.js | 37 ++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/src/styles/brackets.less b/src/styles/brackets.less index 7cd694023f..fd0ed18932 100644 --- a/src/styles/brackets.less +++ b/src/styles/brackets.less @@ -648,6 +648,14 @@ a, img { animation: brightenFade 2s ease-in-out; } +#status-panel-toggle { + cursor: pointer; +} + +#status-panel-toggle.flash { + animation: brightenFade 2s ease-in-out; +} + @keyframes brightenFade { 0% { background-color: transparent; diff --git a/src/view/WorkspaceManager.js b/src/view/WorkspaceManager.js index c2f75d4110..fbd863d22c 100644 --- a/src/view/WorkspaceManager.js +++ b/src/view/WorkspaceManager.js @@ -40,6 +40,7 @@ define(function (require, exports, module) { EventDispatcher = require("utils/EventDispatcher"), KeyBindingManager = require("command/KeyBindingManager"), Resizer = require("utils/Resizer"), + AnimationUtils = require("utils/AnimationUtils"), PluginPanelView = require("view/PluginPanelView"), PanelView = require("view/PanelView"), EditorManager = require("editor/EditorManager"), @@ -134,6 +135,9 @@ define(function (require, exports, module) { /** @type {jQueryObject} Scrollable area holding the tab elements */ let $bottomPanelTabsOverflow; + /** @type {jQueryObject} Chevron toggle in the status bar */ + let $statusBarPanelToggle; + /** @type {string[]} Ordered list of currently open (tabbed) panel IDs */ let _openBottomPanelIds = []; @@ -542,11 +546,44 @@ define(function (require, exports, module) { $bottomPanelContainer.insertBefore("#status-bar"); $bottomPanelContainer.hide(); + // Create status bar chevron toggle for bottom panel + $statusBarPanelToggle = $( + '
' + + '' + + '
' + ); + $("#status-indicators").append($statusBarPanelToggle); + + $statusBarPanelToggle.on("click", function () { + if ($bottomPanelContainer.is(":visible")) { + Resizer.hide($bottomPanelContainer[0]); + triggerUpdateLayout(); + } else if (_openBottomPanelIds.length > 0) { + Resizer.show($bottomPanelContainer[0]); + triggerUpdateLayout(); + } else { + _showLastHiddenPanelIfPossible(); + } + }); + // Make the container resizable (not individual panels) Resizer.makeResizable($bottomPanelContainer[0], Resizer.DIRECTION_VERTICAL, Resizer.POSITION_TOP, 200, false, undefined, true); listenToResize($bottomPanelContainer); + $bottomPanelContainer.on("panelCollapsed", function () { + $statusBarPanelToggle.find("i") + .removeClass("fa-chevron-down") + .addClass("fa-chevron-up"); + AnimationUtils.animateUsingClass($statusBarPanelToggle[0], "flash", 1500); + }); + + $bottomPanelContainer.on("panelExpanded", function () { + $statusBarPanelToggle.find("i") + .removeClass("fa-chevron-up") + .addClass("fa-chevron-down"); + }); + // Tab bar click handlers $bottomPanelTabBar.on("click", ".bottom-panel-tab-close-btn", function (e) { e.stopPropagation(); From 0103442a231b859c523f86bf248bdf50ea317af8 Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 18 Feb 2026 21:33:29 +0530 Subject: [PATCH 14/16] feat: animate buttons when panel toggles its state via non status bar click --- src/styles/Extn-BottomPanelTabs.less | 6 +++--- src/styles/brackets.less | 2 +- src/view/WorkspaceManager.js | 18 +++++++++++++++--- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/styles/Extn-BottomPanelTabs.less b/src/styles/Extn-BottomPanelTabs.less index c5405f35ba..6ec4edc81e 100644 --- a/src/styles/Extn-BottomPanelTabs.less +++ b/src/styles/Extn-BottomPanelTabs.less @@ -205,14 +205,14 @@ margin-right: 0.3rem; border-radius: 3px; cursor: pointer; - color: #333; - font-size: 0.8rem; + color: #555; + font-size: 0.7rem; -webkit-text-stroke: 0.4px; line-height: 1; transition: background-color 0.12s ease; .dark & { - color: #fff; + color: #ccc; } &:hover { diff --git a/src/styles/brackets.less b/src/styles/brackets.less index fd0ed18932..500a374a68 100644 --- a/src/styles/brackets.less +++ b/src/styles/brackets.less @@ -653,7 +653,7 @@ a, img { } #status-panel-toggle.flash { - animation: brightenFade 2s ease-in-out; + animation: brightenFade 800ms ease-in-out; } @keyframes brightenFade { diff --git a/src/view/WorkspaceManager.js b/src/view/WorkspaceManager.js index fbd863d22c..ebdcdaa355 100644 --- a/src/view/WorkspaceManager.js +++ b/src/view/WorkspaceManager.js @@ -144,6 +144,9 @@ define(function (require, exports, module) { /** @type {string|null} The panel ID of the currently visible (active) tab */ let _activeBottomPanelId = null; + /** @type {boolean} True while the status bar toggle button is handling a click */ + let _statusBarToggleInProgress = false; + /** * Calculates the available height for the full-size Editor (or the no-editor placeholder), * accounting for the current size of all visible panels, toolbar, & status bar. @@ -548,13 +551,14 @@ define(function (require, exports, module) { // Create status bar chevron toggle for bottom panel $statusBarPanelToggle = $( - '
' + + '
' + '' + '
' ); - $("#status-indicators").append($statusBarPanelToggle); + $("#status-indicators").prepend($statusBarPanelToggle); $statusBarPanelToggle.on("click", function () { + _statusBarToggleInProgress = true; if ($bottomPanelContainer.is(":visible")) { Resizer.hide($bottomPanelContainer[0]); triggerUpdateLayout(); @@ -564,6 +568,7 @@ define(function (require, exports, module) { } else { _showLastHiddenPanelIfPossible(); } + _statusBarToggleInProgress = false; }); // Make the container resizable (not individual panels) @@ -575,13 +580,20 @@ define(function (require, exports, module) { $statusBarPanelToggle.find("i") .removeClass("fa-chevron-down") .addClass("fa-chevron-up"); - AnimationUtils.animateUsingClass($statusBarPanelToggle[0], "flash", 1500); + $statusBarPanelToggle.attr("title", "Show Bottom Panel"); + if (!_statusBarToggleInProgress) { + AnimationUtils.animateUsingClass($statusBarPanelToggle[0], "flash", 800); + } }); $bottomPanelContainer.on("panelExpanded", function () { $statusBarPanelToggle.find("i") .removeClass("fa-chevron-up") .addClass("fa-chevron-down"); + $statusBarPanelToggle.attr("title", "Hide Bottom Panel"); + if (!_statusBarToggleInProgress) { + AnimationUtils.animateUsingClass($statusBarPanelToggle[0], "flash", 800); + } }); // Tab bar click handlers From d09c6304d97f67363462a9eb195f524dab18a92e Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 18 Feb 2026 21:40:38 +0530 Subject: [PATCH 15/16] refactor: better styling for the hide panel button in bottom panel tab bar --- src/styles/Extn-BottomPanelTabs.less | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/styles/Extn-BottomPanelTabs.less b/src/styles/Extn-BottomPanelTabs.less index 6ec4edc81e..327f2d62e3 100644 --- a/src/styles/Extn-BottomPanelTabs.less +++ b/src/styles/Extn-BottomPanelTabs.less @@ -201,25 +201,27 @@ align-items: center; justify-content: center; width: 1.6rem; - height: 1.6rem; - margin-right: 0.3rem; + height: 1.4rem; + margin-right: 0.4rem; border-radius: 3px; cursor: pointer; - color: #555; + color: #666; font-size: 0.7rem; -webkit-text-stroke: 0.4px; line-height: 1; - transition: background-color 0.12s ease; + transition: color 0.12s ease, background-color 0.12s ease; .dark & { - color: #ccc; + color: #aaa; } &:hover { background-color: rgba(0, 0, 0, 0.1); + color: #333; .dark & { background-color: rgba(255, 255, 255, 0.12); + color: #eee; } } } From 1d8855cb84d8edd94c0d5f6bf4aaa0ff0d9d142e Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 18 Feb 2026 21:53:24 +0530 Subject: [PATCH 16/16] chore: localize the strings --- src/nls/root/strings.js | 3 +++ src/view/WorkspaceManager.js | 11 ++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 650a58bb7c..d1fd4bd702 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -1251,6 +1251,9 @@ define({ "REFERENCES_NO_RESULTS": "No References available for current cursor position", "REFERENCES_PANEL_TITLE": "References", "SEARCH_RESULTS_PANEL_TITLE": "Search Results", + "BOTTOM_PANEL_HIDE": "Hide Panel", + "BOTTOM_PANEL_SHOW": "Show Bottom Panel", + "BOTTOM_PANEL_HIDE_TOGGLE": "Hide Bottom Panel", "CMD_FIND_DOCUMENT_SYMBOLS": "Find Document Symbols", "CMD_FIND_PROJECT_SYMBOLS": "Find Project Symbols", diff --git a/src/view/WorkspaceManager.js b/src/view/WorkspaceManager.js index ebdcdaa355..8f2c594dcf 100644 --- a/src/view/WorkspaceManager.js +++ b/src/view/WorkspaceManager.js @@ -41,6 +41,7 @@ define(function (require, exports, module) { KeyBindingManager = require("command/KeyBindingManager"), Resizer = require("utils/Resizer"), AnimationUtils = require("utils/AnimationUtils"), + Strings = require("strings"), PluginPanelView = require("view/PluginPanelView"), PanelView = require("view/PanelView"), EditorManager = require("editor/EditorManager"), @@ -293,7 +294,7 @@ define(function (require, exports, module) { let isActive = (panelId === _activeBottomPanelId); let $tab = $('
' + '' + $("").text(title).html() + '' + - '×' + + '×' + '
'); $bottomPanelTabsOverflow.append($tab); }); @@ -541,7 +542,7 @@ define(function (require, exports, module) { $bottomPanelTabsOverflow = $('
'); let $tabBarActions = $('
'); $tabBarActions.append( - '' + '' ); $bottomPanelTabBar.append($bottomPanelTabsOverflow); $bottomPanelTabBar.append($tabBarActions); @@ -551,7 +552,7 @@ define(function (require, exports, module) { // Create status bar chevron toggle for bottom panel $statusBarPanelToggle = $( - '
' + + '
' + '' + '
' ); @@ -580,7 +581,7 @@ define(function (require, exports, module) { $statusBarPanelToggle.find("i") .removeClass("fa-chevron-down") .addClass("fa-chevron-up"); - $statusBarPanelToggle.attr("title", "Show Bottom Panel"); + $statusBarPanelToggle.attr("title", Strings.BOTTOM_PANEL_SHOW); if (!_statusBarToggleInProgress) { AnimationUtils.animateUsingClass($statusBarPanelToggle[0], "flash", 800); } @@ -590,7 +591,7 @@ define(function (require, exports, module) { $statusBarPanelToggle.find("i") .removeClass("fa-chevron-up") .addClass("fa-chevron-down"); - $statusBarPanelToggle.attr("title", "Hide Bottom Panel"); + $statusBarPanelToggle.attr("title", Strings.BOTTOM_PANEL_HIDE_TOGGLE); if (!_statusBarToggleInProgress) { AnimationUtils.animateUsingClass($statusBarPanelToggle[0], "flash", 800); }