@@ -28,6 +28,7 @@ define(function (require, exports, module) {
2828 const EventDispatcher = require ( "utils/EventDispatcher" ) ,
2929 PreferencesManager = require ( "preferences/PreferencesManager" ) ,
3030 Resizer = require ( "utils/Resizer" ) ,
31+ DropdownButton = require ( "widgets/DropdownButton" ) ,
3132 Strings = require ( "strings" ) ;
3233
3334 /**
@@ -171,25 +172,21 @@ define(function (require, exports, module) {
171172 if ( ! _$tabsOverflow ) {
172173 return ;
173174 }
174- // Detach the add button before emptying to preserve its event handlers
175- if ( _$addBtn ) {
176- _$addBtn . detach ( ) ;
177- }
178175 _$tabsOverflow . empty ( ) ;
179176
180177 _openIds . forEach ( function ( panelId ) {
178+ // Default panel uses the external Tools button, not a tab
179+ if ( panelId === _defaultPanelId ) {
180+ return ;
181+ }
181182 let panel = _panelMap [ panelId ] ;
182183 if ( ! panel ) {
183184 return ;
184185 }
185186 _$tabsOverflow . append ( _buildTab ( panel , panelId === _activeId ) ) ;
186187 } ) ;
187188
188- // Re-append the "+" button at the end (after all tabs)
189- if ( _$addBtn ) {
190- _$tabsOverflow . append ( _$addBtn ) ;
191- _updateAddButtonVisibility ( ) ;
192- }
189+ _updateAddButtonVisibility ( ) ;
193190 _checkTabOverflow ( ) ;
194191 }
195192
@@ -221,18 +218,17 @@ define(function (require, exports, module) {
221218 if ( ! _$tabsOverflow ) {
222219 return ;
223220 }
221+ // Default panel uses the external Tools button, not a tab
222+ if ( panelId === _defaultPanelId ) {
223+ _updateAddButtonVisibility ( ) ;
224+ return ;
225+ }
224226 let panel = _panelMap [ panelId ] ;
225227 if ( ! panel ) {
226228 return ;
227229 }
228230 let $tab = _buildTab ( panel , panelId === _activeId ) ;
229-
230- // Insert before the "+" button so it stays at the end
231- if ( _$addBtn && _$addBtn . parent ( ) . length ) {
232- _$addBtn . before ( $tab ) ;
233- } else {
234- _$tabsOverflow . append ( $tab ) ;
235- }
231+ _$tabsOverflow . append ( $tab ) ;
236232 _updateAddButtonVisibility ( ) ;
237233 _checkTabOverflow ( ) ;
238234 }
@@ -247,6 +243,10 @@ define(function (require, exports, module) {
247243 if ( ! _$tabsOverflow ) {
248244 return ;
249245 }
246+ if ( panelId === _defaultPanelId ) {
247+ _updateAddButtonVisibility ( ) ;
248+ return ;
249+ }
250250 _$tabsOverflow . find ( '.bottom-panel-tab[data-panel-id="' + panelId + '"]' ) . remove ( ) ;
251251 _updateAddButtonVisibility ( ) ;
252252 _checkTabOverflow ( ) ;
@@ -295,7 +295,7 @@ define(function (require, exports, module) {
295295 _$tabBar . on ( "dragstart" , ".bottom-panel-tab" , function ( e ) {
296296 draggedTab = this ;
297297 e . originalEvent . dataTransfer . effectAllowed = "move" ;
298- e . originalEvent . dataTransfer . setData ( "text/plain " , "panel-tab " ) ;
298+ e . originalEvent . dataTransfer . setData ( "application/x-phoenix-panel-tab " , "1 " ) ;
299299 $ ( this ) . addClass ( "bottom-panel-tab-dragging" ) ;
300300 } ) ;
301301
@@ -354,6 +354,9 @@ define(function (require, exports, module) {
354354 * Only collapses tabs that have an icon available.
355355 * @private
356356 */
357+ /** @type {jQueryObject } Overflow dropdown button */
358+ let _$overflowBtn = null ;
359+
357360 function _checkTabOverflow ( ) {
358361 if ( ! _$tabBar ) {
359362 return ;
@@ -362,6 +365,16 @@ define(function (require, exports, module) {
362365 _$tabBar . removeClass ( "bottom-panel-tabs-collapsed" ) ;
363366 const isOverflowing = _$tabsOverflow [ 0 ] . scrollWidth > _$tabsOverflow [ 0 ] . clientWidth ;
364367 _$tabBar . toggleClass ( "bottom-panel-tabs-collapsed" , isOverflowing ) ;
368+
369+ // Check if still overflowing after collapse
370+ const stillOverflowing = isOverflowing &&
371+ _$tabsOverflow [ 0 ] . scrollWidth > _$tabsOverflow [ 0 ] . clientWidth ;
372+
373+ // Show/hide overflow button
374+ if ( _$overflowBtn ) {
375+ _$overflowBtn . toggle ( stillOverflowing ) ;
376+ }
377+
365378 // Show tooltip on hover only in collapsed mode (title text is hidden)
366379 _$tabBar . find ( ".bottom-panel-tab" ) . each ( function ( ) {
367380 const $tab = $ ( this ) ;
@@ -373,6 +386,105 @@ define(function (require, exports, module) {
373386 } ) ;
374387 }
375388
389+ /**
390+ * Get the list of hidden (not fully visible) panel tabs.
391+ * @return {Array<{panelId: string, title: string}> }
392+ * @private
393+ */
394+ function _getHiddenTabs ( ) {
395+ const hidden = [ ] ;
396+ const barRect = _$tabsOverflow [ 0 ] . getBoundingClientRect ( ) ;
397+ _$tabsOverflow . find ( ".bottom-panel-tab" ) . each ( function ( ) {
398+ const tabRect = this . getBoundingClientRect ( ) ;
399+ const isVisible = tabRect . left >= barRect . left &&
400+ tabRect . right <= ( barRect . right + 2 ) ;
401+ if ( ! isVisible ) {
402+ const $tab = $ ( this ) ;
403+ hidden . push ( {
404+ panelId : $tab . data ( "panel-id" ) ,
405+ title : $tab . find ( ".bottom-panel-tab-title" ) . text ( )
406+ } ) ;
407+ }
408+ } ) ;
409+ return hidden ;
410+ }
411+
412+ /** @type {DropdownButton.DropdownButton } */
413+ let _overflowDropdown = null ;
414+
415+ /**
416+ * Show a dropdown menu listing hidden panel tabs.
417+ * Uses the same DropdownButton widget as the file tab bar overflow.
418+ * @private
419+ */
420+ function _showOverflowMenu ( ) {
421+ // If dropdown is already open, close it (toggle behavior)
422+ if ( _overflowDropdown ) {
423+ _overflowDropdown . closeDropdown ( ) ;
424+ _overflowDropdown = null ;
425+ return ;
426+ }
427+
428+ const hidden = _getHiddenTabs ( ) ;
429+ if ( ! hidden . length ) {
430+ return ;
431+ }
432+
433+ _overflowDropdown = new DropdownButton . DropdownButton ( "" , hidden , function ( item ) {
434+ const panel = _panelMap [ item . panelId ] ;
435+ let iconHtml = "" ;
436+ if ( panel && panel . _options ) {
437+ if ( panel . _options . iconClass ) {
438+ iconHtml = '<i class="panel-titlebar-icon ' + panel . _options . iconClass
439+ + '" style="margin-right:6px"></i>' ;
440+ } else if ( panel . _options . iconSvg ) {
441+ iconHtml = '<img class="panel-titlebar-icon" src="' + panel . _options . iconSvg
442+ + '" style="width:14px;height:14px;margin-right:6px;vertical-align:middle">' ;
443+ }
444+ }
445+ const activeClass = item . panelId === _activeId ? ' style="font-weight:600"' : '' ;
446+ return {
447+ html : '<div class="dropdown-tab-item"' + activeClass + '>'
448+ + iconHtml + '<span>' + item . title + '</span></div>' ,
449+ enabled : true
450+ } ;
451+ } ) ;
452+
453+ _overflowDropdown . dropdownExtraClasses = "dropdown-overflow-menu" ;
454+
455+ // Position at the overflow button
456+ const btnRect = _$overflowBtn [ 0 ] . getBoundingClientRect ( ) ;
457+ $ ( "body" ) . append ( _overflowDropdown . $button ) ;
458+ _overflowDropdown . $button . css ( {
459+ position : "absolute" ,
460+ left : btnRect . left + "px" ,
461+ top : ( btnRect . top - 2 ) + "px" ,
462+ zIndex : 1000
463+ } ) ;
464+
465+ _overflowDropdown . showDropdown ( ) ;
466+
467+ _overflowDropdown . on ( "select" , function ( e , item ) {
468+ const panel = _panelMap [ item . panelId ] ;
469+ if ( panel ) {
470+ panel . show ( ) ;
471+ // Scroll the newly active tab into view
472+ const $tab = _$tabsOverflow . find ( '.bottom-panel-tab[data-panel-id="' + item . panelId + '"]' ) ;
473+ if ( $tab . length ) {
474+ $tab [ 0 ] . scrollIntoView ( { inline : "nearest" } ) ;
475+ }
476+ }
477+ } ) ;
478+
479+ // Clean up reference when dropdown closes
480+ _overflowDropdown . on ( DropdownButton . EVENT_DROPDOWN_CLOSED , function ( ) {
481+ if ( _overflowDropdown ) {
482+ _overflowDropdown . $button . remove ( ) ;
483+ _overflowDropdown = null ;
484+ }
485+ } ) ;
486+ }
487+
376488 /**
377489 * Show or hide the "+" button based on whether the default panel is active.
378490 * The button is hidden when the default panel is the active tab (since
@@ -383,11 +495,8 @@ define(function (require, exports, module) {
383495 if ( ! _$addBtn ) {
384496 return ;
385497 }
386- if ( _defaultPanelId && _activeId === _defaultPanelId ) {
387- _$addBtn . hide ( ) ;
388- } else {
389- _$addBtn . show ( ) ;
390- }
498+ // Highlight the Tools button as active when the default panel is shown
499+ _$addBtn . toggleClass ( "active" , _defaultPanelId && _activeId === _defaultPanelId ) ;
391500 }
392501
393502 /**
@@ -664,13 +773,13 @@ define(function (require, exports, module) {
664773 _recomputeLayout = recomputeLayoutFn ;
665774 _defaultPanelId = defaultPanelId ;
666775
667- // Create the "Tools" button inside the tabs overflow area (after all tabs)
668- // This opens the default/quick-access panel when clicked .
776+ // Create the "Tools" button outside the scrollable tabs area
777+ // so it's always visible even when tabs overflow .
669778 _$addBtn = $ ( '<span class="bottom-panel-add-btn" title="' + Strings . BOTTOM_PANEL_DEFAULT_TITLE + '">'
670779 + '<img class="app-drawer-tab-icon" src="styles/images/app-drawer.svg"'
671780 + ' style="width:12px;height:12px;vertical-align:middle;margin-right:4px">'
672781 + Strings . BOTTOM_PANEL_DEFAULT_TITLE + '</span>' ) ;
673- _$tabsOverflow . append ( _$addBtn ) ;
782+ _$tabBar . find ( ".bottom-panel-tab-bar-actions" ) . before ( _$addBtn ) ;
674783
675784 // Tab bar click handlers
676785 _$tabBar . on ( "click" , ".bottom-panel-tab-close-btn" , function ( e ) {
@@ -692,10 +801,22 @@ define(function (require, exports, module) {
692801 panel . show ( ) ;
693802 }
694803 }
804+ // Scroll clicked tab into view if partially hidden
805+ this . scrollIntoView ( { inline : "nearest" } ) ;
695806 } ) ;
696807
697808 _initDragAndDrop ( ) ;
698809
810+ // Overflow button for hidden tabs (inserted between tabs and action buttons)
811+ _$overflowBtn = $ ( '<span class="bottom-panel-overflow-btn" title="' + Strings . TABBAR_SHOW_HIDDEN_TABS + '">'
812+ + '<i class="fa-solid fa-chevron-down"></i></span>' ) ;
813+ _$overflowBtn . hide ( ) ;
814+ _$tabBar . find ( ".bottom-panel-tab-bar-actions" ) . before ( _$overflowBtn ) ;
815+ _$overflowBtn . on ( "click" , function ( e ) {
816+ e . stopPropagation ( ) ;
817+ _showOverflowMenu ( ) ;
818+ } ) ;
819+
699820 // "+" button opens the default/quick-access panel
700821 _$addBtn . on ( "click" , function ( e ) {
701822 e . stopPropagation ( ) ;
0 commit comments