diff --git a/src/extension/_locales/de/messages.json b/src/extension/_locales/de/messages.json
index c93e0f78..266a0c26 100644
--- a/src/extension/_locales/de/messages.json
+++ b/src/extension/_locales/de/messages.json
@@ -317,6 +317,9 @@
"dark_mode": {
"message": "Dunkles Design"
},
+ "drag_to_reposition": {
+ "message": "Zum Neupositionieren ziehen"
+ },
"delete": {
"message": "Löschen"
},
diff --git a/src/extension/_locales/en/messages.json b/src/extension/_locales/en/messages.json
index 72203c46..aceca29f 100644
--- a/src/extension/_locales/en/messages.json
+++ b/src/extension/_locales/en/messages.json
@@ -256,6 +256,9 @@
"dark_mode": {
"message": "Dark theme"
},
+ "drag_to_reposition": {
+ "message": "Drag to reposition"
+ },
"delete": {
"message": "Delete"
},
diff --git a/src/extension/_locales/es/messages.json b/src/extension/_locales/es/messages.json
index fdd38de5..dad39d1f 100644
--- a/src/extension/_locales/es/messages.json
+++ b/src/extension/_locales/es/messages.json
@@ -317,6 +317,9 @@
"dark_mode": {
"message": "Tema oscuro"
},
+ "drag_to_reposition": {
+ "message": "Arrastrar para cambiar la posición"
+ },
"delete": {
"message": "Eliminar"
},
diff --git a/src/extension/_locales/fr/messages.json b/src/extension/_locales/fr/messages.json
index 6d826d78..c403bb65 100644
--- a/src/extension/_locales/fr/messages.json
+++ b/src/extension/_locales/fr/messages.json
@@ -317,6 +317,9 @@
"dark_mode": {
"message": "Thème sombre"
},
+ "drag_to_reposition": {
+ "message": "Faire glisser pour repositionner"
+ },
"delete": {
"message": "Supprimer"
},
diff --git a/src/extension/_locales/it/messages.json b/src/extension/_locales/it/messages.json
index 3b1eec6d..cb456f42 100644
--- a/src/extension/_locales/it/messages.json
+++ b/src/extension/_locales/it/messages.json
@@ -317,6 +317,9 @@
"dark_mode": {
"message": "Tema scuro"
},
+ "drag_to_reposition": {
+ "message": "Trascina per riposizionare"
+ },
"delete": {
"message": "Elimina"
},
diff --git a/src/extension/_locales/ja/messages.json b/src/extension/_locales/ja/messages.json
index 828aee09..24b7e048 100644
--- a/src/extension/_locales/ja/messages.json
+++ b/src/extension/_locales/ja/messages.json
@@ -317,6 +317,9 @@
"dark_mode": {
"message": "ダークテーマ"
},
+ "drag_to_reposition": {
+ "message": "ドラッグして位置を変更"
+ },
"delete": {
"message": "削除"
},
diff --git a/src/extension/_locales/ko/messages.json b/src/extension/_locales/ko/messages.json
index 18fd7fe9..c797bfb7 100644
--- a/src/extension/_locales/ko/messages.json
+++ b/src/extension/_locales/ko/messages.json
@@ -317,6 +317,9 @@
"dark_mode": {
"message": "어두운 테마"
},
+ "drag_to_reposition": {
+ "message": "재배치하려면 드래그"
+ },
"delete": {
"message": "삭제"
},
diff --git a/src/extension/_locales/pt_BR/messages.json b/src/extension/_locales/pt_BR/messages.json
index 902b6069..43a8ab13 100644
--- a/src/extension/_locales/pt_BR/messages.json
+++ b/src/extension/_locales/pt_BR/messages.json
@@ -317,6 +317,9 @@
"dark_mode": {
"message": "Tema escuro"
},
+ "drag_to_reposition": {
+ "message": "Arraste para reposicionar"
+ },
"delete": {
"message": "Excluir"
},
diff --git a/src/extension/_locales/zh_CN/messages.json b/src/extension/_locales/zh_CN/messages.json
index 520faa79..d69c81bc 100644
--- a/src/extension/_locales/zh_CN/messages.json
+++ b/src/extension/_locales/zh_CN/messages.json
@@ -317,6 +317,9 @@
"dark_mode": {
"message": "深色主题"
},
+ "drag_to_reposition": {
+ "message": "拖动以重新定位"
+ },
"delete": {
"message": "删除"
},
diff --git a/src/extension/_locales/zh_TW/messages.json b/src/extension/_locales/zh_TW/messages.json
index c2812d9c..1ed4028f 100644
--- a/src/extension/_locales/zh_TW/messages.json
+++ b/src/extension/_locales/zh_TW/messages.json
@@ -317,6 +317,9 @@
"dark_mode": {
"message": "深色主題"
},
+ "drag_to_reposition": {
+ "message": "拖曳以重新定位"
+ },
"delete": {
"message": "刪除"
},
diff --git a/src/extension/app/aem-sidekick.css.js b/src/extension/app/aem-sidekick.css.js
index 56fc9c03..441bc6bc 100644
--- a/src/extension/app/aem-sidekick.css.js
+++ b/src/extension/app/aem-sidekick.css.js
@@ -66,8 +66,8 @@ export const style = css`
@media (max-width: 800px) {
plugin-action-bar {
min-width: unset;
- width: 100%;
- max-width: unset;
+ width: 100vw;
+ max-width: 100vw;
bottom: 0;
}
}
diff --git a/src/extension/app/components/action-bar/action-bar.js b/src/extension/app/components/action-bar/action-bar.js
index 09a436ea..27dd44fe 100644
--- a/src/extension/app/components/action-bar/action-bar.js
+++ b/src/extension/app/components/action-bar/action-bar.js
@@ -19,6 +19,7 @@ export class ActionBar extends LitElement {
.action-bar {
display: flex;
border-radius: var(--spectrum2-sidekick-border-radius);
+ clip-path: border-box;
color: var(--spectrum2-sidekick-color));
background-color: var(--spectrum2-sidekick-background);
border: 1px solid var(--spectrum2-sidekick-border-color);
@@ -59,6 +60,7 @@ export class ActionBar extends LitElement {
.action-bar {
border-radius: 0;
}
+ }
`;
render() {
diff --git a/src/extension/app/components/plugin/env-switcher/env-switcher.js b/src/extension/app/components/plugin/env-switcher/env-switcher.js
index 6905424c..094ad006 100644
--- a/src/extension/app/components/plugin/env-switcher/env-switcher.js
+++ b/src/extension/app/components/plugin/env-switcher/env-switcher.js
@@ -376,6 +376,6 @@ export class EnvironmentSwitcher extends ConnectedElement {
}
render() {
- return html``;
+ return html``;
}
}
diff --git a/src/extension/app/components/plugin/plugin-action-bar.css.js b/src/extension/app/components/plugin/plugin-action-bar.css.js
index a89c852d..98787599 100644
--- a/src/extension/app/components/plugin/plugin-action-bar.css.js
+++ b/src/extension/app/components/plugin/plugin-action-bar.css.js
@@ -15,6 +15,11 @@
import { css } from 'lit';
export const style = css`
+ :host([dragging="true"]) {
+ user-select: none;
+ opacity: 0.95;
+ }
+
action-bar > div.action-group {
display: flex;
padding: 12px;
@@ -38,7 +43,9 @@ export const style = css`
flex-shrink: 0;
}
-
+ action-bar > div.plugin-menu-container.hidden {
+ display: none;
+ }
action-bar > div.badge-plugins-container {
position: absolute;
@@ -55,16 +62,48 @@ export const style = css`
}
action-bar .logo {
- padding: 12px;
- width: 32px;
+ width: 42px;
height: 32px;
+ padding: 12px 12px 12px 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0;
+ }
+
+ action-bar .logo > svg {
+ width: 24px;
+ }
+
+ action-bar .logo .drag-handle {
display: flex;
align-items: center;
justify-content: center;
+ height: 100%;
+ cursor: grab;
+ flex-shrink: 0;
+ user-select: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ padding: 0 8px;
+ }
+
+ action-bar .logo .drag-handle .drag-bar {
+ width: 2px;
+ height: 16px;
+ background-color: var(--spectrum2-sidekick-color);
+ opacity: 0.2;
+ border-radius: 2px;
+ transition: opacity 0.2s;
+ }
+
+ action-bar .logo .drag-handle:hover .drag-bar {
+ opacity: 0.5;
}
- action-bar .logo > svg{
- width: 56px;
+ action-bar .logo .drag-handle:active {
+ cursor: grabbing;
}
#plugin-menu {
@@ -126,8 +165,6 @@ export const style = css`
width: 37px;
flex-shrink: 0;
cursor: pointer;
- border-top-right-radius: var(--spectrum2-sidekick-border-radius);
- border-bottom-right-radius: var(--spectrum2-sidekick-border-radius);
background-color: var(--spectrum2-sidekick-background-close);
backdrop-filter: var(--sidekick-backdrop-filter);
}
@@ -161,16 +198,15 @@ export const style = css`
color: var(--spectrum-global-color-gray-600);
}
- @media (max-width: 500px) {
- #properties {
- display: none;
+ @media (max-width: 800px) {
+ action-bar .logo {
+ width: 32px;
+ min-width: 32px;
+ padding-left: 12px;
}
- }
- @media (max-width: 800px) {
- action-bar .close-button {
- border-top-right-radius: 0;
- border-bottom-right-radius: 0;
+ action-bar .logo .drag-handle {
+ display: none;
}
}
`;
diff --git a/src/extension/app/components/plugin/plugin-action-bar.js b/src/extension/app/components/plugin/plugin-action-bar.js
index 946e6034..dc871461 100644
--- a/src/extension/app/components/plugin/plugin-action-bar.js
+++ b/src/extension/app/components/plugin/plugin-action-bar.js
@@ -13,7 +13,9 @@
/* eslint-disable max-len */
import { html } from 'lit';
-import { customElement, queryAll, queryAsync } from 'lit/decorators.js';
+import {
+ customElement, queryAll, queryAsync, state,
+} from 'lit/decorators.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { reaction } from 'mobx';
import {
@@ -36,19 +38,16 @@ import { getConfig } from '../../../config.js';
*/
/**
- * The gap between plugins in the plugin group
- */
-const PLUGIN_GROUP_GAP = 8;
-
-/**
- * The maximum width of the action bar
+ * The maximum width of the action bar (from CSS))
+ * @type {number}
*/
const ACTION_BAR_MAX_WIDTH = 800;
/**
- * The threshold for overflow
+ * The gap between plugins in the plugin group (from CSS)
+ * @type {number}
*/
-const OVERFLOW_THRESHOLD = -10;
+const PLUGIN_GAP = 8;
@customElement('plugin-action-bar')
export class PluginActionBar extends ConnectedElement {
@@ -65,10 +64,10 @@ export class PluginActionBar extends ConnectedElement {
visiblePlugins = [];
/**
- * The plugins visible in the action bar.
+ * All pinned plugins (to be distributed to the action bar/menu)
* @type {Plugin[]}
*/
- barPlugins = [];
+ pinnedPlugins = [];
/**
* The plugins folded into the action menu.
@@ -77,10 +76,10 @@ export class PluginActionBar extends ConnectedElement {
menuPlugins = [];
/**
- * The plugins temporarily folded into the action menu.
+ * The badge plugins.
* @type {Plugin[]}
*/
- transientPlugins = [];
+ badgePlugins = [];
/**
* The current width of the action bar.
@@ -88,12 +87,86 @@ export class PluginActionBar extends ConnectedElement {
*/
actionBarWidth = 0;
+ /**
+ * The last window width we calculated for
+ * @type {number}
+ */
+ lastWindowWidth = 0;
+
+ /**
+ * Last plugin count (to detect plugin visibility changes)
+ * @type {number}
+ */
+ lastPluginCount = 0;
+
+ /**
+ * Whether plugin distribution is currently running
+ * @type {boolean}
+ */
+ isDistributing = false;
+
+ /**
+ * Timeout ID for post-resize plugin distribution check
+ * @type {number|null}
+ */
+ resizeDistributingTimeout = null;
+
/**
* The configured projects
* @type {Array}
*/
projects = [];
+ /**
+ * Whether the action bar is currently being dragged
+ * @type {boolean}
+ */
+ isDragging = false;
+
+ /**
+ * The initial X coordinate when drag starts
+ * @type {number}
+ */
+ dragStartX = 0;
+
+ /**
+ * The initial Y coordinate when drag starts
+ * @type {number}
+ */
+ dragStartY = 0;
+
+ /**
+ * The initial left position (translateX) when drag starts
+ * @type {number}
+ */
+ initialLeft = 0;
+
+ /**
+ * The initial bottom position when drag starts
+ * @type {number}
+ */
+ initialBottom = 0;
+
+ /**
+ * Throttle timer for window resize handler
+ * @type {number|null}
+ */
+ resizeThrottle = null;
+
+ /**
+ * The plugins visible in the action bar.
+ * @type {Plugin[]}
+ */
+ @state()
+ accessor barPlugins = [];
+
+ /**
+ * The plugins temporarily folded into the action menu.
+ * @type {Plugin[]}
+ */
+ @state()
+ accessor transientPlugins = [];
+
@queryAsync('action-bar')
accessor actionBar;
@@ -112,28 +185,8 @@ export class PluginActionBar extends ConnectedElement {
@queryAsync('.close-button')
accessor closeButton;
- /**
- * Set up the bar and menu plugins in this environment and updates the component.
- */
- setupPlugins() {
- this.transientPlugins = [];
-
- this.visiblePlugins = [
- ...Object.values(this.appStore.corePlugins),
- ...Object.values(this.appStore.customPlugins),
- ].filter((plugin) => plugin.isVisible());
-
- this.barPlugins = this.visiblePlugins
- .filter((plugin) => plugin.isPinned() && !plugin.isBadge());
-
- this.menuPlugins = this.visiblePlugins
- .filter((plugin) => !plugin.isPinned() && !plugin.isBadge());
-
- this.badgePlugins = this.visiblePlugins
- .filter((plugin) => plugin.isBadge());
-
- this.requestUpdate();
- }
+ @queryAsync('.drag-handle')
+ accessor dragHandle;
async connectedCallback() {
super.connectedCallback();
@@ -191,13 +244,237 @@ export class PluginActionBar extends ConnectedElement {
}
});
+ window.addEventListener('resize', this.onWindowResize);
+
this.requestUpdate();
}
disconnectedCallback() {
super.disconnectedCallback();
this.removeEventListener('click', this.onClick);
+ window.removeEventListener('resize', this.onWindowResize);
EventBus.instance.removeEventListener(EVENTS.CLOSE_POPOVER);
+
+ // Clear any pending throttled resize
+ if (this.resizeThrottle) {
+ window.clearTimeout(this.resizeThrottle);
+ this.resizeThrottle = null;
+ }
+
+ // Clear any pending post-resize check
+ if (this.resizeDistributingTimeout) {
+ window.clearTimeout(this.resizeDistributingTimeout);
+ this.resizeDistributingTimeout = null;
+ }
+ }
+
+ /**
+ * Set up the bar and menu plugins in this environment and updates the component.
+ */
+ setupPlugins() {
+ this.visiblePlugins = [
+ ...Object.values(this.appStore.corePlugins),
+ ...Object.values(this.appStore.customPlugins),
+ ].filter((plugin) => plugin.isVisible());
+
+ // Store all pinned plugins (before distribution)
+ this.pinnedPlugins = this.visiblePlugins
+ .filter((plugin) => plugin.isPinned() && !plugin.isBadge());
+
+ // Non-pinned plugins go directly to menu (not subject to distribution)
+ this.menuPlugins = this.visiblePlugins
+ .filter((plugin) => !plugin.isPinned() && !plugin.isBadge());
+
+ this.badgePlugins = this.visiblePlugins
+ .filter((plugin) => plugin.isBadge());
+
+ this.requestUpdate();
+ }
+
+ /**
+ * Handle window resize
+ */
+ onWindowResize = () => {
+ // Throttle resize handling to avoid performance issues
+ if (this.resizeThrottle) {
+ return;
+ }
+
+ this.resizeThrottle = window.setTimeout(() => {
+ if (this.hasAttribute('style')) {
+ if (window.innerWidth <= ACTION_BAR_MAX_WIDTH) {
+ // Below 800px, remove custom positioning
+ this.removeAttribute('style');
+ } else {
+ // Restrict custom positioning to viewport
+ this.constrainToViewport();
+ }
+ }
+ this.resizeThrottle = null;
+ }, 150);
+
+ // Trigger plugin redistribution with resize flag
+ this.distributePlugins(true);
+ };
+
+ /**
+ * Prevent selection during drag
+ * @param {Event} e The event
+ */
+ preventSelection = (e) => {
+ if (this.isDragging) {
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ };
+
+ /**
+ * Handle mouse leaving viewport during drag
+ */
+ onMouseLeave = () => {
+ if (this.isDragging) {
+ this.onDragEnd();
+ }
+ };
+
+ /**
+ * Handle drag move
+ * @param {MouseEvent} e The mouse event
+ */
+ onDragMove = (e) => {
+ if (!this.isDragging) return;
+
+ e.preventDefault();
+
+ // Calculate delta
+ const deltaX = e.clientX - this.dragStartX;
+ const deltaY = this.dragStartY - e.clientY; // Inverted because bottom increases upward
+
+ // Calculate new position
+ const newLeft = this.initialLeft + deltaX;
+ const newBottom = this.initialBottom + deltaY;
+
+ // Apply new position
+ this.style.left = '50%';
+ this.style.transform = `translate(${newLeft}px, 0px)`;
+ this.style.bottom = `${newBottom}px`;
+ };
+
+ /**
+ * Handle drag end
+ */
+ onDragEnd = (e) => {
+ if (!this.isDragging) return;
+
+ if (e) {
+ e.preventDefault();
+ }
+
+ this.isDragging = false;
+
+ // Reset drag start positions
+ this.dragStartX = 0;
+ this.dragStartY = 0;
+
+ // Remove event listeners
+ window.removeEventListener('mousemove', this.onDragMove, false);
+ window.removeEventListener('mouseup', this.onDragEnd, false);
+ window.removeEventListener('selectstart', this.preventSelection, true);
+ window.removeEventListener('mouseleave', this.onMouseLeave, false);
+
+ // Remove dragging attribute
+ this.removeAttribute('dragging');
+
+ // Constrain to viewport
+ this.constrainToViewport();
+ };
+
+ /**
+ * Handle drag start
+ * @param {MouseEvent} e The mouse event
+ */
+ onDragStart = (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ this.isDragging = true;
+
+ this.appStore.sampleRUM('click', { source: 'sidekick', target: 'sidekick-dragged' });
+
+ // Store initial mouse position
+ this.dragStartX = e.clientX;
+ this.dragStartY = e.clientY;
+
+ // Get current position
+ const computedStyle = window.getComputedStyle(this);
+ const { transform, bottom } = computedStyle;
+
+ // Parse current position from transform and bottom
+ if (transform && transform !== 'none') {
+ const matrix = new DOMMatrixReadOnly(transform);
+ this.initialLeft = matrix.m41; // translateX value
+ } else {
+ this.initialLeft = 0;
+ }
+
+ this.initialBottom = parseInt(bottom, 10);
+
+ // Add move and up listeners
+ window.addEventListener('mousemove', this.onDragMove, false);
+ window.addEventListener('mouseup', this.onDragEnd, false);
+ window.addEventListener('selectstart', this.preventSelection, true);
+ window.addEventListener('mouseleave', this.onMouseLeave, false);
+
+ // Set dragging attribute for CSS styling
+ this.setAttribute('dragging', 'true');
+ };
+
+ /**
+ * Constrain the element to viewport bounds
+ */
+ constrainToViewport() {
+ const rect = this.getBoundingClientRect();
+ const viewportWidth = window.innerWidth;
+ const viewportHeight = window.innerHeight;
+
+ // Get current values
+ const computedStyle = window.getComputedStyle(this);
+ const { transform, bottom } = computedStyle;
+
+ let translateX = 0;
+ if (transform && transform !== 'none') {
+ const matrix = new DOMMatrixReadOnly(transform);
+ translateX = matrix.m41;
+ }
+
+ const currentBottom = parseInt(this.style.bottom || bottom, 10) || 0;
+
+ // Check horizontal bounds
+ let newTranslateX = translateX;
+ let newLeft = '50%'; // Default for too far left
+ if (rect.left < 0) {
+ // Too far left
+ newTranslateX = translateX - rect.left;
+ } else if (rect.right > viewportWidth) {
+ // Too far right
+ newTranslateX = translateX - (rect.right - viewportWidth);
+ newLeft = '49%'; // Account for scrollbar
+ }
+
+ // Check vertical bounds
+ let newBottom = currentBottom;
+ if (rect.top < 0) {
+ // Too far up - decrease bottom to move element down
+ newBottom = Math.max(0, currentBottom + rect.top);
+ } else if (rect.bottom > viewportHeight) {
+ // Too far down - increase bottom to move element up
+ newBottom = currentBottom + (rect.bottom - viewportHeight);
+ }
+
+ // Apply constrained position
+ this.style.left = newLeft;
+ this.style.transform = `translate(${newTranslateX}px, 0px)`;
+ this.style.bottom = `${newBottom}px`;
}
/**
@@ -212,92 +489,116 @@ export class PluginActionBar extends ConnectedElement {
return width + padding * 2;
};
- async checkOverflow() {
+ /**
+ * Distribute plugins between bar and menu based on available space.
+ * @param {boolean} resizing - Whether this is called during a resize event
+ * @param {boolean} checkAfterResize - Whether this is a post-resize verification check
+ */
+ async distributePlugins(resizing = false, checkAfterResize = false) {
+ // Prevent parallel execution
+ if (this.isDistributing) {
+ return;
+ }
+
+ // Wait for DOM elements to be ready
if (this.actionGroups.length < 3) {
- // wait for all action groups to be rendered
return;
}
- const [logoContainer, closeButton] = await Promise.all([this.logoContainer, this.closeButton]);
-
- // Action bar styles
- const barWidth = parseInt(window.getComputedStyle(this).width, 10);
- const logoWidth = this.getTotalWidth(logoContainer);
- const closeButtonWidth = parseInt(window.getComputedStyle(closeButton).width, 10);
-
- const [pluginGroup, pluginMenu, systemGroup] = this.actionGroups;
-
- // Left plugin container styles
- const pluginGroupStyles = window.getComputedStyle(pluginGroup);
- const pluginGroupPadding = parseInt(pluginGroupStyles.padding, 10);
- const pluginGroupWidth = parseInt(pluginGroupStyles.width, 10);
- const pluginGroupWidthIncludingPadding = pluginGroupWidth + (pluginGroupPadding * 2);
-
- const pluginMenuWidth = this.getTotalWidth(pluginMenu);
- const systemWidth = this.getTotalWidth(systemGroup);
-
- // Combined width of system plugins, plugin menu, and close button
- const rightWidth = pluginMenuWidth + systemWidth + closeButtonWidth;
-
- // Combined width of left logo and plugin group
- const leftWidth = pluginGroupWidthIncludingPadding + logoWidth;
-
- // Total free space in the bar, minus 3px to account for the dividers
- const totalFreeSpace = barWidth - leftWidth - rightWidth - 3;
-
- // If there's not enough space for the plugins in the bar, move the last plugin to the menu
- // We check against -10 to avoid endless loop caused after a plugin is added to back to the bar
- if (totalFreeSpace <= OVERFLOW_THRESHOLD && this.barPlugins.length > 1) {
- this.transientPlugins.unshift(this.barPlugins.pop());
- this.requestUpdate();
- // If we don't need to remove a plugin and if the action bar is less than 800px wide,
- // the logic must adjust to check if there's space for the next plugin
- } else if (window.innerWidth < ACTION_BAR_MAX_WIDTH) {
- const nextPlugin = this.transientPlugins[0];
- if (nextPlugin) {
- const children = Array.from(this.actionGroups[0].children);
-
- let childrenWidth = children.reduce((acc, child) => acc + child.clientWidth, 0);
- // Account for the gap between the plugins
- childrenWidth += (children.length - 1) * PLUGIN_GROUP_GAP;
-
- // Calculate the hypothetical new width of the plugins group if we added the next plugin
- const nextPluginWidth = nextPlugin.getEstimatedWidth();
- const newWidth = nextPluginWidth + childrenWidth + PLUGIN_GROUP_GAP;
-
- // If the new width is less than the plugin group width, add the plugin to the bar
- if (newWidth < pluginGroupWidth) {
- this.barPlugins.push(this.transientPlugins.shift());
- this.requestUpdate();
+ // Only recalculate if window width or plugin count changed
+ const currentWindowWidth = window.innerWidth;
+ const currentPluginCount = this.pinnedPlugins.length;
+ if (currentWindowWidth === this.lastWindowWidth
+ && currentPluginCount === this.lastPluginCount) {
+ return;
+ }
+
+ this.isDistributing = true;
+
+ try {
+ // Schedule a post-resize check if we're resizing but not already in a check
+ if (resizing && !checkAfterResize) {
+ // Clear any pending post-resize check
+ if (this.resizeDistributingTimeout) {
+ window.clearTimeout(this.resizeDistributingTimeout);
}
+ // Schedule a new check after UI has settled
+ this.resizeDistributingTimeout = window.setTimeout(() => {
+ this.resizeDistributingTimeout = null;
+ this.distributePlugins(true, true);
+ }, 150);
}
- } else {
- // If the action bar is wider than 800px, and we don't need to remove a plugin, check if there's space for the next plugin
- const extraSpace = ACTION_BAR_MAX_WIDTH - barWidth;
- if (this.transientPlugins.length > 0) {
- const nextPlugin = this.transientPlugins[0];
- if (nextPlugin) {
- const nextPluginWidth = nextPlugin.getEstimatedWidth();
- if (nextPluginWidth < extraSpace) {
- this.barPlugins.push(this.transientPlugins.shift());
- this.requestUpdate();
- }
+
+ this.lastWindowWidth = currentWindowWidth;
+ this.lastPluginCount = currentPluginCount;
+
+ // Determine maximum bar width based on window size
+ const maxBarWidth = currentWindowWidth > ACTION_BAR_MAX_WIDTH
+ ? ACTION_BAR_MAX_WIDTH
+ : currentWindowWidth;
+
+ // Get fixed width elements
+ const [logoContainer, closeButton] = await Promise.all([
+ this.logoContainer,
+ this.closeButton,
+ ]);
+
+ // Get actual rendered widths
+ const logoWidth = parseInt(window.getComputedStyle(logoContainer).width, 10);
+ const closeButtonWidth = parseInt(window.getComputedStyle(closeButton).width, 10);
+
+ // Get system plugins width and plugin menu button width
+ const [pluginGroup, pluginMenu, systemGroup] = this.actionGroups;
+ const pluginGroupPadding = parseInt(window.getComputedStyle(pluginGroup).padding, 10) * 2;
+ const pluginMenuWidth = this.getTotalWidth(pluginMenu);
+ const systemWidth = this.getTotalWidth(systemGroup);
+
+ // Calculate fixed widths (always reserve space for plugin menu even when hidden)
+ const fixedWidth = logoWidth + pluginGroupPadding + 3 + systemWidth + pluginMenuWidth + closeButtonWidth;
+
+ // Calculate available space for plugins with safety margin
+ // Safety margin accounts for estimation errors (estimated vs actual rendered widths)
+ const safetyMargin = 16;
+ const availableWidth = maxBarWidth - fixedWidth - safetyMargin;
+
+ // Distribute plugins
+ const fittingPlugins = [];
+ const overflowPlugins = [];
+ let accumulatedWidth = 0;
+ let hasOverflowed = false; // Track if we've started overflowing
+
+ // Iterate through plugins in original order to preserve sequence
+ for (const plugin of this.pinnedPlugins) {
+ const pluginWidth = plugin.getEstimatedWidth();
+ const gapWidth = fittingPlugins.length > 0 ? PLUGIN_GAP : 0;
+ const requiredWidth = accumulatedWidth + pluginWidth + gapWidth;
+
+ // Always keep env-switcher in the bar, regardless of space
+ const isEnvSwitcher = plugin.id === 'env-switcher';
+ const fitsInBar = requiredWidth <= availableWidth;
+
+ // Add to bar if: it's env-switcher OR (it fits AND nothing has overflowed yet)
+ if (isEnvSwitcher || (fitsInBar && !hasOverflowed)) {
+ fittingPlugins.push(plugin);
+ accumulatedWidth = requiredWidth;
+ } else {
+ // Plugin overflows - add to menu and mark that we've started overflowing
+ overflowPlugins.push(plugin);
+ hasOverflowed = true;
}
}
- }
- this.actionBarWidth = barWidth;
- }
-
- firstUpdated() {
- window.addEventListener('resize', () => {
- this.checkOverflow();
- });
+ // Update reactive arrays with new distribution
+ this.barPlugins = [...fittingPlugins];
+ this.transientPlugins = [...overflowPlugins];
+ } finally {
+ this.isDistributing = false;
+ }
}
async updated() {
await this.updateComplete;
- this.checkOverflow();
+ await this.distributePlugins();
}
// istanbul ignore next 7
@@ -342,6 +643,9 @@ export class PluginActionBar extends ConnectedElement {
return html`
@@ -387,8 +691,10 @@ export class PluginActionBar extends ConnectedElement {
return html``;
}
+ const isHidden = this.menuPlugins.length === 0 && this.transientPlugins.length === 0;
+
return html`
-
+
${this.transientPlugins.length > 0 || this.menuPlugins.length > 0 ? html`