diff --git a/docs/API-Reference/view/PanelView.md b/docs/API-Reference/view/PanelView.md index 7a4455bcc..544969a66 100644 --- a/docs/API-Reference/view/PanelView.md +++ b/docs/API-Reference/view/PanelView.md @@ -68,12 +68,6 @@ recomputeLayout callback from WorkspaceManager ## \_defaultPanelId : string \| null The default/quick-access panel ID -**Kind**: global variable - - -## \_$addBtn : jQueryObject -The "+" button inside the tab overflow area - **Kind**: global variable diff --git a/docs/API-Reference/view/WorkspaceManager.md b/docs/API-Reference/view/WorkspaceManager.md index 98a49cc6f..bb0c11ed1 100644 --- a/docs/API-Reference/view/WorkspaceManager.md +++ b/docs/API-Reference/view/WorkspaceManager.md @@ -190,7 +190,8 @@ Returns true if visible else false. ### view/WorkspaceManager.setPluginPanelWidth(width) Programmatically sets the plugin panel content width to the given value in pixels. The total toolbar width is adjusted to account for the plugin icons bar. -Width is clamped to respect panel minWidth and max size (75% of window). +If the requested width doesn't fit, the sidebar is progressively shrunk +(and collapsed if necessary) before clamping. No-op if no panel is currently visible. **Kind**: inner method of [view/WorkspaceManager](#module_view/WorkspaceManager) diff --git a/src/extensionsIntegrated/Phoenix-live-preview/live-preview.css b/src/extensionsIntegrated/Phoenix-live-preview/live-preview.css index ecb403319..8c72ac0fb 100644 --- a/src/extensionsIntegrated/Phoenix-live-preview/live-preview.css +++ b/src/extensionsIntegrated/Phoenix-live-preview/live-preview.css @@ -5,7 +5,7 @@ .live-preview-browser-btn { opacity: 0; visibility: hidden; - transition: opacity 1s, visibility 0s linear 1s; /* Fade-out effect */ + transition: opacity 1s, visibility 0s linear 1s; } #live-preview-plugin-toolbar { @@ -27,7 +27,7 @@ #panel-live-preview-frame, #panel-md-preview-frame { - background-color: transparent; + background-color: white; position: relative; } @@ -50,6 +50,127 @@ height: calc(100% - var(--toolbar-height)); } +.frame-container.responsive-viewport { + position: relative; + justify-content: center; + align-items: stretch; + background: #1a1a1e; +} + +.frame-container.responsive-viewport > div:first-child:not(.responsive-handle) { + display: none; +} + +.responsive-handle { + flex-shrink: 0; + z-index: 5; + background: transparent; + transition: background 0.15s; +} + +.responsive-handle:hover, +.responsive-handle.dragging { + background: rgba(255, 255, 255, 0.08); +} + +.responsive-handle-horizontal { + width: 10px; + cursor: ew-resize; + position: relative; +} + +.responsive-handle-horizontal::before, +.responsive-handle-horizontal::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 1px; + height: 16px; + border-radius: 0.5px; + background: rgba(255, 255, 255, 0.3); +} + +.responsive-handle-horizontal::before { + transform: translate(calc(-50% - 2px), -50%); +} + +.responsive-handle-horizontal::after { + transform: translate(calc(-50% + 2px), -50%); +} + +.responsive-handle-horizontal:hover::before, +.responsive-handle-horizontal:hover::after, +.responsive-handle-horizontal.dragging::before, +.responsive-handle-horizontal.dragging::after { + background: rgba(255, 255, 255, 0.7); +} + +.responsive-handle-left { + opacity: 0; + transition: opacity 0.2s; +} + +.responsive-handle-left:hover, +.responsive-handle-left.dragging { + opacity: 1; +} + +.responsive-handle-vertical { + position: absolute; + height: 10px; + cursor: ns-resize; + box-sizing: border-box; +} + +.responsive-handle-vertical::before, +.responsive-handle-vertical::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 16px; + height: 1px; + border-radius: 0.5px; + background: rgba(255, 255, 255, 0.3); +} + +.responsive-handle-vertical::before { + transform: translate(-50%, calc(-50% - 2px)); +} + +.responsive-handle-vertical::after { + transform: translate(-50%, calc(-50% + 2px)); +} + +.responsive-handle-vertical:hover::before, +.responsive-handle-vertical:hover::after, +.responsive-handle-vertical.dragging::before, +.responsive-handle-vertical.dragging::after { + background: rgba(255, 255, 255, 0.7); +} + +.responsive-dimension-label { + position: absolute; + transform: translateX(-50%); + z-index: 10; + display: none; + padding: 6px 16px; + border-radius: 6px; + background: rgba(0, 0, 0, 0.88); + border: 1px solid rgba(255, 255, 255, 0.18); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + color: #fff; + font-size: 15px; + font-weight: 600; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; + white-space: nowrap; + pointer-events: none; + letter-spacing: 0.5px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(255, 255, 255, 0.06); +} + .plugin-toolbar { height: var(--toolbar-height); color: #a0a0a0; @@ -61,10 +182,30 @@ .toolbar-button { background-color: transparent; - width:28px; + width: 28px; height: 22px; } +#live-preview-plugin-toolbar .btn-alt-quiet:hover, +#live-preview-plugin-toolbar .btn-alt-quiet:focus, +#live-preview-plugin-toolbar .btn-alt-quiet:active { + border-color: rgba(255, 255, 255, 0.1) !important; + box-shadow: none !important; +} + +#live-preview-plugin-toolbar .lp-device-size-icon:hover, +#live-preview-plugin-toolbar .lp-device-size-icon:focus, +#live-preview-plugin-toolbar .lp-device-size-icon:active { + border: none !important; +} + +#live-preview-plugin-toolbar .lp-device-size-dropdown-chevron:hover, +#live-preview-plugin-toolbar .lp-device-size-dropdown-chevron:focus, +#live-preview-plugin-toolbar .lp-device-size-dropdown-chevron:active { + border: none !important; + border-left: 1px solid rgba(255, 255, 255, 0.1) !important; +} + .open-icon { background: url("./images/sprites.svg#open-icon") no-repeat 72.5%; width: 30px; @@ -101,7 +242,7 @@ opacity: 0; color: #a0a0a0; visibility: hidden; - transition: opacity 1s, visibility 0s linear 1s; /* Fade-out effect */ + transition: opacity 1s, visibility 0s linear 1s; width: 30px; height: 22px; padding: 1px 6px; @@ -109,37 +250,92 @@ margin-top: 0; } -.lp-device-size-icon { - min-width: fit-content; +.lp-fit-to-panel-btn { + color: #a0a0a0; + flex-shrink: 0; + margin: 0 2px; + background: transparent; + border: 1px solid transparent; + box-shadow: none; + font-size: 12px; +} + +.lp-fit-to-panel-btn:hover, +.lp-fit-to-panel-btn:focus, +.lp-fit-to-panel-btn:active { + background: transparent !important; + border: 1px solid rgba(255, 255, 255, 0.1) !important; + box-shadow: none !important; +} + +.lp-device-size-btn-group { display: flex; align-items: center; + flex-shrink: 0; margin: 0 4px 0 3px; - cursor: pointer; - background: transparent; - box-shadow: none; border: 1px solid transparent; + border-radius: 3px; box-sizing: border-box; +} + +.lp-device-size-btn-group:hover { + border-color: rgba(255, 255, 255, 0.1); +} + +.lp-device-size-icon { + display: flex; + align-items: center; + justify-content: center; + width: 20px; + cursor: pointer; + background: transparent; + box-shadow: none !important; + border: none; color: #a0a0a0; - padding: 0 0.35em; + padding: 0; + margin: 0; } .lp-device-size-icon:hover, .lp-device-size-icon:focus, .lp-device-size-icon:active { background: transparent !important; - border: 1px solid rgba(255, 255, 255, 0.1) !important; + border: none !important; box-shadow: none !important; } -#deviceSizeBtn.btn-dropdown::after { - position: static; - margin-left: 5px; +.lp-device-size-dropdown-chevron { + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + background: transparent; + box-shadow: none !important; + border: none; + border-left: 1px solid transparent; + border-radius: 0 3px 3px 0; + color: #a0a0a0; + padding: 0 4px; + margin: 0; + height: 22px; + font-size: 10px; +} + +.lp-device-size-btn-group:hover .lp-device-size-dropdown-chevron { + border-left-color: rgba(255, 255, 255, 0.1); +} + +.lp-device-size-dropdown-chevron:hover, +.lp-device-size-dropdown-chevron:focus, +.lp-device-size-dropdown-chevron:active { + background: transparent !important; + border: none !important; + border-left: 1px solid rgba(255, 255, 255, 0.1) !important; + box-shadow: none !important; } .device-size-item-icon { margin-right: 6px; - width: 12px; - text-align: center; font-size: inherit; } @@ -152,17 +348,12 @@ .device-size-item-width { margin-left: 10px; - opacity: 0.5; -} - -.device-size-item-disabled { - opacity: 0.35; + color: #9a9a9a; + font-size: 12.5px; } .device-size-item-breakpoint-icon { margin-right: 6px; - width: 12px; - text-align: center; font-size: inherit; color: rgba(100, 180, 255, 0.8); } @@ -259,7 +450,6 @@ color: #fff; } -/* Persistent cursor-sync highlight on CM line corresponding to md viewer cursor */ .cm-cursor-sync-highlight { background-color: rgba(100, 150, 255, 0.18) !important; } diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 0bc4d69f2..5e797fc57 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -523,7 +523,11 @@ define({ "LIVE_DEV_IMAGE_FOLDER_DIALOG_HELP": "💡 Type folder path or leave empty to download in 'images' folder.", "LIVE_DEV_IMAGE_FOLDER_DIALOG_REMEMBER": "Don't ask again for this project", "AVAILABLE_IN_PRO_TITLE": "Available in Phoenix Pro", - "DEVICE_SIZE_LIMIT_MESSAGE": "Phoenix Pro lets you preview your page at the screen sizes defined in your CSS.", + "DEVICE_SIZE_LIMIT_MESSAGE": "See how your page looks on phones, tablets, desktops, and your CSS breakpoints. Upgrade to Phoenix Pro.", + "DEVICE_SIZE_NOT_ENOUGH_SPACE": "Not enough space for this screen size - try zooming out", + "DEVICE_SIZE_CYCLE_TOOLTIP": "Switch between mobile, tablet, and desktop", + "DEVICE_SIZE_DROPDOWN_TOOLTIP": "All device sizes", + "DEVICE_SIZE_FIT_TO_PANEL": "Fit to panel", "MD_EDIT_UPSELL_MESSAGE": "Write Markdown like a document. {APP_NAME} handles the formatting so you can stay focused on writing.", "IMAGE_UPLOADING": "Uploading", "IMAGE_UPLOAD_FAILED": "Failed to upload image", @@ -1557,7 +1561,7 @@ define({ "BOTTOM_PANEL_MINIMIZE": "Minimize Panel", "BOTTOM_PANEL_SHOW": "Show Bottom Panel", "BOTTOM_PANEL_HIDE_TOGGLE": "Hide Bottom Panel", - "BOTTOM_PANEL_DEFAULT_TITLE": "Tools", + "BOTTOM_PANEL_DEFAULT_TITLE": "Quick Access - Tools", "BOTTOM_PANEL_DEFAULT_HEADING": "Open a Panel", "BOTTOM_PANEL_OPEN_PANEL": "Open a Panel", "BOTTOM_PANEL_MAXIMIZE": "Maximize Panel", diff --git a/src/styles/Extn-BottomPanelTabs.less b/src/styles/Extn-BottomPanelTabs.less index f81bd6988..18fb29ec9 100644 --- a/src/styles/Extn-BottomPanelTabs.less +++ b/src/styles/Extn-BottomPanelTabs.less @@ -166,11 +166,12 @@ display: none; } -/* Default panel (Tools) tab: always show the icon alongside the title, - in both expanded and collapsed modes, active or inactive. */ +/* Pinned Quick Access tab: icon-only, always visible */ .bottom-panel-tab.bottom-panel-tab-default .bottom-panel-tab-icon { display: inline-flex; - margin-right: 0.4rem; + width: 1.1rem; + height: 1.1rem; + margin: 0; } .default-panel-btn .panel-titlebar-icon { @@ -237,8 +238,7 @@ /* Fix all collapsed tabs to the same width so the UI doesn't shake when switching active tab. The width matches the active tab's natural content (icon + close button) using fixed px since the - icon has a fixed size. The Tools button (.bottom-panel-add-btn) - is a separate element and keeps its natural "Tools" text width. */ + icon has a fixed size. */ .bottom-panel-tab { min-width: 63px; box-sizing: border-box; @@ -247,14 +247,9 @@ .bottom-panel-tab:not(.active) .bottom-panel-tab-close-btn { display: none; } - /* Default panel (Tools) tab: keep natural width and show its title - even in collapsed mode (other tabs collapse to icon-only). */ + /* Pinned Quick Access tab: icon-only, keeps its natural width */ .bottom-panel-tab.bottom-panel-tab-default { min-width: auto; - justify-content: flex-start; - } - .bottom-panel-tab.bottom-panel-tab-default .bottom-panel-tab-title { - display: inline; } } @@ -304,42 +299,18 @@ } } -.bottom-panel-add-btn { - display: flex; - align-items: center; +/* Pinned Quick Access tab: icon-only with separator */ +.bottom-panel-tab.bottom-panel-tab-default { + padding: 0 0.6rem; + min-width: auto; justify-content: center; - padding: 0 10px; - height: 2rem; - min-width: 70px; - line-height: 2rem; - cursor: pointer; - color: #888; - font-size: 0.82rem; - flex: 0 0 auto; - white-space: nowrap; - user-select: none; - -webkit-user-drag: none; - transition: color 0.12s ease, background-color 0.12s ease; - - img { - -webkit-user-drag: none; - pointer-events: none; - } + border-right: 1px solid rgba(0, 0, 0, 0.15); + background-color: transparent; .dark & { - color: #777; + border-right: 1px solid rgba(255, 255, 255, 0.1); + background-color: transparent; } - - &:hover { - background-color: #e0e0e0; - color: #333; - - .dark & { - background-color: #333; - color: #eee; - } - } - } .bottom-panel-tab-bar-actions { diff --git a/src/view/DefaultPanelView.js b/src/view/DefaultPanelView.js index 07fc9d91b..da4acd794 100644 --- a/src/view/DefaultPanelView.js +++ b/src/view/DefaultPanelView.js @@ -198,11 +198,9 @@ define(function (require, exports, module) { } }); - // Auto-hide when any other panel is shown; update drawer button state. + // Update drawer button state when panels are shown. PanelView.on(PanelView.EVENT_PANEL_SHOWN, function (event, panelID) { - if (panelID !== WorkspaceManager.DEFAULT_PANEL_ID) { - _panel.hide(); - } else { + if (panelID === WorkspaceManager.DEFAULT_PANEL_ID) { _updateButtonVisibility(); } $drawerBtn.toggleClass("selected-button", panelID === WorkspaceManager.DEFAULT_PANEL_ID); diff --git a/src/view/PanelView.js b/src/view/PanelView.js index 0c6f8e0dd..6e9dcf800 100644 --- a/src/view/PanelView.js +++ b/src/view/PanelView.js @@ -108,9 +108,6 @@ define(function (require, exports, module) { /** @type {string|null} The default/quick-access panel ID */ let _defaultPanelId = null; - /** @type {jQueryObject} The "+" button inside the tab overflow area */ - let _$addBtn = null; - // --- Tab helper functions --- /** @@ -149,7 +146,7 @@ define(function (require, exports, module) { */ function _buildTab(panel, isActive) { let title = panel._tabTitle || _getPanelTitle(panel.panelID, panel.$panel); - // Default panel (Tools) tab is not draggable — it's a fixed slot, not a user tab + // Default panel (Quick Access) tab is pinned — not draggable, not closable const isDefault = panel.panelID === _defaultPanelId; let $tab = $('
') .toggleClass('bottom-panel-tab-default', isDefault) @@ -162,8 +159,13 @@ define(function (require, exports, module) { $icon[0].style.maskImage = maskUrl; $icon[0].style.webkitMaskImage = maskUrl; $tab.append($icon); - $tab.append($('').text(title)); - $tab.append($('×').attr('title', Strings.CLOSE)); + if (isDefault) { + // Icon-only tab with tooltip + $tab.attr('title', title); + } else { + $tab.append($('').text(title)); + $tab.append($('×').attr('title', Strings.CLOSE)); + } return $tab; } @@ -171,10 +173,6 @@ 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) { @@ -185,11 +183,6 @@ 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(); } @@ -226,13 +219,7 @@ define(function (require, exports, module) { return; } let $tab = _buildTab(panel, panelId === _activeId); - // Insert before the Tools button so it stays at the end - if (_$addBtn && _$addBtn.parent().length) { - _$addBtn.before($tab); - } else { - _$tabsOverflow.append($tab); - } - _updateAddButtonVisibility(); + _$tabsOverflow.append($tab); _checkTabOverflow(); } @@ -247,7 +234,6 @@ define(function (require, exports, module) { return; } _$tabsOverflow.find('.bottom-panel-tab[data-panel-id="' + panelId + '"]').remove(); - _updateAddButtonVisibility(); _checkTabOverflow(); } @@ -291,8 +277,55 @@ define(function (require, exports, module) { _$tabBar.find(".bottom-panel-tab").removeClass("drag-target"); } + // Find the closest non-default, non-dragged tab to mouseX + function findNearestDropTarget(mouseX) { + let closest = null; + let closestDist = Infinity; + _$tabsOverflow.find(".bottom-panel-tab").each(function () { + if (this === draggedTab) { + return; + } + if ($(this).data("panel-id") === _defaultPanelId) { + return; + } + const rect = this.getBoundingClientRect(); + const dist = Math.min(Math.abs(mouseX - rect.left), Math.abs(mouseX - rect.right)); + if (dist < closestDist) { + closestDist = dist; + closest = this; + } + }); + return closest; + } + + // Perform the reorder in _openIds and rebuild the tab bar + function reorderTabs(targetEl, mouseX) { + if (!draggedTab || targetEl === draggedTab) { + return; + } + let draggedId = $(draggedTab).data("panel-id"); + let targetId = $(targetEl).data("panel-id"); + let fromIdx = _openIds.indexOf(draggedId); + let toIdx = _openIds.indexOf(targetId); + if (fromIdx === -1 || toIdx === -1) { + return; + } + const insertBefore = getDropPosition(targetEl, mouseX); + _openIds.splice(fromIdx, 1); + let newIdx = _openIds.indexOf(targetId); + if (!insertBefore) { + newIdx++; + } + // Never place a tab before the pinned Quick Access tab at index 0 + if (newIdx < 1 && _defaultPanelId && _openIds[0] === _defaultPanelId) { + newIdx = 1; + } + _openIds.splice(newIdx, 0, draggedId); + _updateBottomPanelTabBar(); + _updateActiveTabHighlight(); + } + _$tabBar.on("dragstart", ".bottom-panel-tab", function (e) { - // Default panel (Tools) tab is never draggable if ($(this).data("panel-id") === _defaultPanelId) { e.preventDefault(); return; @@ -311,7 +344,6 @@ define(function (require, exports, module) { if (!draggedTab || this === draggedTab) { return; } - // Don't allow dropping onto the default panel (Tools) tab if ($(this).data("panel-id") === _defaultPanelId) { return; } @@ -322,6 +354,30 @@ define(function (require, exports, module) { updateIndicator(this, getDropPosition(this, e.originalEvent.clientX)); }); + // Fallback dragover on the overflow container: handles gaps between + // tabs, empty space after the last tab, and the Quick Access tab area + // by snapping to the nearest valid drop target. + _$tabsOverflow.on("dragover", function (e) { + if (!draggedTab) { + return; + } + // Skip if already over a valid tab (the tab handler above covers it) + const $closestTab = $(e.target).closest(".bottom-panel-tab"); + if ($closestTab.length && $closestTab[0] !== draggedTab + && $closestTab.data("panel-id") !== _defaultPanelId) { + return; + } + const nearest = findNearestDropTarget(e.originalEvent.clientX); + if (!nearest) { + return; + } + e.preventDefault(); + e.originalEvent.dataTransfer.dropEffect = "move"; + _$tabBar.find(".bottom-panel-tab").removeClass("drag-target"); + $(nearest).addClass("drag-target"); + updateIndicator(nearest, getDropPosition(nearest, e.originalEvent.clientX)); + }); + _$tabBar.on("dragleave", ".bottom-panel-tab", function (e) { const related = e.originalEvent.relatedTarget; if (!$(this).is(related) && !$(this).has(related).length) { @@ -336,24 +392,23 @@ define(function (require, exports, module) { cleanup(); return; } - let draggedId = $(draggedTab).data("panel-id"); - let targetId = $(this).data("panel-id"); - let fromIdx = _openIds.indexOf(draggedId); - let toIdx = _openIds.indexOf(targetId); - if (fromIdx === -1 || toIdx === -1) { + reorderTabs(this, e.originalEvent.clientX); + cleanup(); + }); + + // Fallback drop on the overflow container + _$tabsOverflow.on("drop", function (e) { + if (!draggedTab) { cleanup(); return; } - const insertBefore = getDropPosition(this, e.originalEvent.clientX); - _openIds.splice(fromIdx, 1); - let newIdx = _openIds.indexOf(targetId); - if (!insertBefore) { - newIdx++; + const nearest = findNearestDropTarget(e.originalEvent.clientX); + if (nearest) { + e.preventDefault(); + e.stopPropagation(); + reorderTabs(nearest, e.originalEvent.clientX); } - _openIds.splice(newIdx, 0, draggedId); cleanup(); - _updateBottomPanelTabBar(); - _updateActiveTabHighlight(); }); } @@ -398,9 +453,13 @@ define(function (require, exports, module) { _$overflowBtn.toggle(stillOverflowing); } - // Show tooltip on hover only in collapsed mode (title text is hidden) + // Show tooltip on hover only in collapsed mode (title text is hidden). + // The pinned Quick Access tab always keeps its tooltip (icon-only). _$tabBar.find(".bottom-panel-tab").each(function () { const $tab = $(this); + if ($tab.data("panel-id") === _defaultPanelId) { + return; + } if (isOverflowing) { $tab.attr("title", $tab.find(".bottom-panel-tab-title").text()); } else { @@ -503,23 +562,6 @@ define(function (require, exports, module) { }); } - /** - * 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 - * clicking "+" would be a no-op) and shown otherwise. - * @private - */ - function _updateAddButtonVisibility() { - if (!_$addBtn) { - return; - } - if (_defaultPanelId && _activeId === _defaultPanelId) { - _$addBtn.hide(); - } else { - _$addBtn.show(); - } - } - /** * Switch the active tab to the given panel. Does not show/hide the container. * @param {string} panelId @@ -543,7 +585,6 @@ define(function (require, exports, module) { newPanel.$panel.addClass("active-bottom-panel"); } _updateActiveTabHighlight(); - _updateAddButtonVisibility(); } @@ -567,6 +608,12 @@ define(function (require, exports, module) { this._tabTitle = _getPanelTitle(id, $panel, title); this._options = options || {}; _panelMap[id] = this; + + // Quick Access panel is pinned: always at index 0, always in the tab bar + if (id === _defaultPanelId && _openIds.indexOf(id) === -1) { + _openIds.unshift(id); + _addTabToBar(id); + } } /** @@ -666,8 +713,13 @@ define(function (require, exports, module) { exports.trigger(EVENT_PANEL_SHOWN, panelId); return; } - // Not open: add to open set - _openIds.push(panelId); + // Not open: add to open set. + // Quick Access panel is always pinned at index 0. + if (panelId === _defaultPanelId) { + _openIds.unshift(panelId); + } else { + _openIds.push(panelId); + } // Show container if it was hidden if (!_$container.is(":visible")) { @@ -684,9 +736,26 @@ define(function (require, exports, module) { */ Panel.prototype.hide = function () { let panelId = this.panelID; + + // Quick Access panel is pinned — it stays in _openIds and the tab bar. + // Hiding it collapses the bottom panel container entirely. + if (panelId === _defaultPanelId) { + if (_activeId !== panelId) { + return; + } + this.$panel.removeClass("active-bottom-panel"); + _activeId = null; + _updateActiveTabHighlight(); + if (_$container) { + restoreIfMaximized(); + Resizer.hide(_$container[0]); + } + exports.trigger(EVENT_PANEL_HIDDEN, panelId); + return; + } + let idx = _openIds.indexOf(panelId); if (idx === -1) { - // Not open - no-op return; } @@ -700,10 +769,9 @@ define(function (require, exports, module) { if (wasActive && _openIds.length > 0) { let nextIdx = Math.min(idx, _openIds.length - 1); activatedId = _openIds[nextIdx]; - _activeId = null; // clear so _switchToTab runs + _activeId = null; _switchToTab(activatedId); } else if (wasActive) { - // No more tabs - hide the container _activeId = null; if (_$container) { restoreIfMaximized(); @@ -713,10 +781,8 @@ define(function (require, exports, module) { _removeTabFromBar(panelId); - // Always fire HIDDEN for the closed panel first exports.trigger(EVENT_PANEL_HIDDEN, panelId); - // Then fire SHOWN for the newly activated tab, if any if (activatedId) { exports.trigger(EVENT_PANEL_SHOWN, activatedId); } @@ -793,18 +859,11 @@ define(function (require, exports, module) { _recomputeLayout = recomputeLayoutFn; _defaultPanelId = defaultPanelId; - // Create the "Tools" button inside the scrollable tabs area. - _$addBtn = $('' - + '' - + Strings.BOTTOM_PANEL_DEFAULT_TITLE + ''); - _$tabsOverflow.append(_$addBtn); - // Tab bar click handlers _$tabBar.on("click", ".bottom-panel-tab-close-btn", function (e) { e.stopPropagation(); let panelId = $(this).closest(".bottom-panel-tab").data("panel-id"); - if (panelId) { + if (panelId && panelId !== _defaultPanelId) { let panel = _panelMap[panelId]; if (panel) { panel.requestClose(); @@ -836,14 +895,6 @@ define(function (require, exports, module) { _showOverflowMenu(); }); - // "+" button opens the default/quick-access panel - _$addBtn.on("click", function (e) { - e.stopPropagation(); - if (_defaultPanelId && _panelMap[_defaultPanelId]) { - _panelMap[_defaultPanelId].show(); - } - }); - // Hide-panel button collapses the container but keeps tabs intact. // Maximize state is preserved so the panel re-opens maximized. _$tabBar.on("click", ".bottom-panel-hide-btn", function (e) { @@ -860,9 +911,8 @@ define(function (require, exports, module) { }); // Double-click on empty tab bar area toggles maximize. - // Exclude tabs themselves, action buttons, and the add button. _$tabBar.on("dblclick", function (e) { - if ($(e.target).closest(".bottom-panel-tab, .bottom-panel-tab-close-btn, .bottom-panel-hide-btn, .bottom-panel-maximize-btn, .bottom-panel-add-btn").length) { + if ($(e.target).closest(".bottom-panel-tab, .bottom-panel-tab-close-btn, .bottom-panel-hide-btn, .bottom-panel-maximize-btn").length) { return; } _toggleMaximize(); @@ -990,12 +1040,12 @@ define(function (require, exports, module) { let $btn = _$tabBar.find(".bottom-panel-maximize-btn"); let $icon = $btn.find("i"); if (_isMaximized) { - $icon.removeClass("fa-regular fa-square") - .addClass("fa-regular fa-window-restore"); + $icon.removeClass("fa-solid fa-expand") + .addClass("fa-solid fa-compress"); $btn.attr("title", Strings.BOTTOM_PANEL_RESTORE); } else { - $icon.removeClass("fa-regular fa-window-restore") - .addClass("fa-regular fa-square"); + $icon.removeClass("fa-solid fa-compress") + .addClass("fa-solid fa-expand"); $btn.attr("title", Strings.BOTTOM_PANEL_MAXIMIZE); } } @@ -1088,7 +1138,12 @@ define(function (require, exports, module) { // Clear internal state BEFORE hiding the container so the // panelCollapsed handler sees an empty _openIds and doesn't // redundantly update the stacks. - _openIds = []; + // The Quick Access panel stays pinned at index 0. + if (_defaultPanelId) { + _openIds = [_defaultPanelId]; + } else { + _openIds = []; + } _activeId = null; if (_$container && _$container.is(":visible")) { @@ -1098,8 +1153,6 @@ define(function (require, exports, module) { _updateBottomPanelTabBar(); - // Fire one EVENT_PANEL_HIDDEN per panel for stack tracking. - // No intermediate EVENT_PANEL_SHOWN events are emitted. for (let i = 0; i < closedIds.length; i++) { exports.trigger(EVENT_PANEL_HIDDEN, closedIds[i]); } diff --git a/src/view/WorkspaceManager.js b/src/view/WorkspaceManager.js index 77b223997..de4ca2b8c 100644 --- a/src/view/WorkspaceManager.js +++ b/src/view/WorkspaceManager.js @@ -204,7 +204,7 @@ define(function (require, exports, module) { // Floor the toolbar's maxsize at its minimum width. Without the floor, a narrow // window with a wide sidebar can drive the cap below 10px, and Resizer's drag // logic would then squeeze the toolbar to zero and hide it. - var rawMax = Math.min(window.innerWidth * 0.75, window.innerWidth - sidebarWidth - 100); + var rawMax = window.innerWidth - sidebarWidth - 100; $mainToolbar.data("maxsize", Math.max(minToolbarWidth, rawMax)); } @@ -420,7 +420,7 @@ define(function (require, exports, module) { .attr('title', Strings.BOTTOM_PANEL_MINIMIZE) ); $tabBarActions.append( - $('') + $('') .attr('title', Strings.BOTTOM_PANEL_MAXIMIZE) ); $bottomPanelTabBar.append($bottomPanelTabsOverflow); @@ -570,7 +570,7 @@ define(function (require, exports, module) { let minToolbarWidth = (panelBeingShown.minWidth || 0) + pluginIconsBarWidth; let maxToolbarWidth = Math.max( minToolbarWidth, - Math.min(window.innerWidth * 0.75, window.innerWidth - sidebarWidth - 100) + window.innerWidth - sidebarWidth - 100 ); let currentWidth = $mainToolbar.width(); if (currentWidth > maxToolbarWidth || currentWidth < minToolbarWidth) { @@ -675,7 +675,8 @@ define(function (require, exports, module) { /** * Programmatically sets the plugin panel content width to the given value in pixels. * The total toolbar width is adjusted to account for the plugin icons bar. - * Width is clamped to respect panel minWidth and max size (75% of window). + * If the requested width doesn't fit, the sidebar is progressively shrunk + * (and collapsed if necessary) before clamping. * No-op if no panel is currently visible. * @param {number} width Desired content width in pixels */ @@ -686,11 +687,31 @@ define(function (require, exports, module) { var pluginIconsBarWidth = $pluginIconsBar.outerWidth(); var newToolbarWidth = width + pluginIconsBarWidth; - // Respect min/max constraints var minSize = currentlyShownPanel.minWidth || 0; var minToolbarWidth = minSize + pluginIconsBarWidth; var sidebarWidth = _getSidebarWidth(); - var maxToolbarWidth = Math.min(window.innerWidth * 0.75, window.innerWidth - sidebarWidth - 100); + var maxToolbarWidth = window.innerWidth - sidebarWidth - MIN_EDITOR_WIDTH; + + if (newToolbarWidth > maxToolbarWidth && sidebarWidth > 0) { + var $sb = $("#sidebar"); + var deficit = newToolbarWidth - maxToolbarWidth; + var newSidebarWidth = sidebarWidth - deficit; + + if (newSidebarWidth >= MIN_SIDEBAR_WIDTH) { + $sb.width(newSidebarWidth); + var resync = $sb.data("resyncSizer"); + if (typeof resync === "function") { + resync(); + } + $sb.trigger("panelResizeUpdate", [newSidebarWidth]); + $sb.trigger("panelResizeEnd", [newSidebarWidth]); + } else { + Resizer.hide($sb[0]); + } + sidebarWidth = _getSidebarWidth(); + maxToolbarWidth = window.innerWidth - sidebarWidth - MIN_EDITOR_WIDTH; + } + newToolbarWidth = Math.max(newToolbarWidth, minToolbarWidth); newToolbarWidth = Math.min(newToolbarWidth, maxToolbarWidth); diff --git a/test/spec/CentralControlBar-integ-test.js b/test/spec/CentralControlBar-integ-test.js index e2a10643d..104e3377f 100644 --- a/test/spec/CentralControlBar-integ-test.js +++ b/test/spec/CentralControlBar-integ-test.js @@ -887,30 +887,38 @@ define(function (require, exports, module) { expect(Math.abs(mtRect.right - testWindow.innerWidth)).toBeLessThan(2); }); - it("should, in normal mode, resize only the plugin-panel (sidebar untouched) and respect the 75%/sidebar clamp", async function () { + it("should, in normal mode, progressively shrink the sidebar when an oversized plugin-panel width is requested", async function () { await openLivePreview(); const iconsW = _$("#plugin-icons-bar").outerWidth(); const sidebarBefore = _$("#sidebar")[0].offsetWidth; - // Request a very wide panel — should be clamped against 75% window - // and (window - sidebar - 100). Either way sidebar must stay put. + // Request a very wide panel — the sidebar should shrink (or + // collapse) to make room, then the toolbar is clamped to + // (window - finalSidebarWidth - MIN_EDITOR_WIDTH). const requested = testWindow.innerWidth; // intentionally over-large WorkspaceManager.setPluginPanelWidth(requested); await awaits(0); - expect(_$("#sidebar")[0].offsetWidth).toBe(sidebarBefore); + // Sidebar must have shrunk or collapsed to accommodate the request. + const sidebarAfter = SidebarView.isVisible() ? _$("#sidebar")[0].offsetWidth : 0; + expect(sidebarAfter).toBeLessThan(sidebarBefore); const toolbar = _$("#main-toolbar").outerWidth(); - const maxAllowed = Math.min( - testWindow.innerWidth * 0.75, - testWindow.innerWidth - sidebarBefore - 100 - ); - // Toolbar must honour the clamp (+iconsBar = the WSM math). + // Toolbar is clamped to (window - sidebar - MIN_EDITOR_WIDTH). + const maxAllowed = testWindow.innerWidth - sidebarAfter - 100; expect(toolbar).toBeLessThanOrEqual(maxAllowed + 3); // And at minimum it's the icons-bar + LP's minWidth. const lp = livePanel(); const minToolbar = (lp && lp.minWidth ? lp.minWidth : 0) + iconsW; expect(toolbar).toBeGreaterThanOrEqual(minToolbar); + + // Restore sidebar for subsequent tests. + if (!SidebarView.isVisible()) { + SidebarView.show(); + await awaitsFor(function () { return SidebarView.isVisible(); }, + "sidebar to come back", 2000); + } + SidebarView.resize(200); }); }); diff --git a/tracking-repos.json b/tracking-repos.json index e555dba24..2f81c3ed3 100644 --- a/tracking-repos.json +++ b/tracking-repos.json @@ -1,5 +1,5 @@ { "phoenixPro": { - "commitID": "59e1d73841f94f546ad9b62a76217eb0a1e450e8" + "commitID": "0bb0ed293e70501363fa3cd9916319ea1dbf38bb" } }