diff --git a/.changeset/fresh-garlics-film.md b/.changeset/fresh-garlics-film.md new file mode 100644 index 000000000000..12a20ddf8548 --- /dev/null +++ b/.changeset/fresh-garlics-film.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +In the dev overlay, when there's too many plugins enabled at once, some of the plugins will now be hidden in a separate sub menu to avoid the bar becoming too long diff --git a/packages/astro/src/runtime/client/dev-overlay/entrypoint.ts b/packages/astro/src/runtime/client/dev-overlay/entrypoint.ts index 887449c3772c..65e50c98e03e 100644 --- a/packages/astro/src/runtime/client/dev-overlay/entrypoint.ts +++ b/packages/astro/src/runtime/client/dev-overlay/entrypoint.ts @@ -1,6 +1,8 @@ import type { DevOverlayPlugin as DevOverlayPluginDefinition } from '../../../@types/astro.js'; import { type AstroDevOverlay, type DevOverlayPlugin } from './overlay.js'; + import { settings } from './settings.js'; +import type { Icon } from './ui-library/icons.js'; let overlay: AstroDevOverlay; @@ -17,6 +19,7 @@ document.addEventListener('DOMContentLoaded', async () => { { DevOverlayTooltip }, { DevOverlayWindow }, { DevOverlayToggle }, + { getIconElement, isDefinedIcon }, ] = await Promise.all([ // @ts-expect-error import('astro:dev-overlay'), @@ -30,6 +33,7 @@ document.addEventListener('DOMContentLoaded', async () => { import('./ui-library/tooltip.js'), import('./ui-library/window.js'), import('./ui-library/toggle.js'), + import('./ui-library/icons.js'), ]); // Register custom elements @@ -53,6 +57,7 @@ document.addEventListener('DOMContentLoaded', async () => { builtIn: builtIn, active: false, status: 'loading' as const, + notification: { state: false }, eventTarget: eventTarget, }; @@ -66,7 +71,9 @@ document.addEventListener('DOMContentLoaded', async () => { newState = evt.detail.state ?? true; } - if (settings.config.showPluginNotifications === false) { + plugin.notification.state = newState; + + if (settings.config.disablePluginNotification === false) { target.querySelector('.notification')?.toggleAttribute('data-active', newState); } }); @@ -83,11 +90,171 @@ document.addEventListener('DOMContentLoaded', async () => { return plugin; }; + const astromorePlugin = { + id: 'astro:more', + name: 'More', + icon: 'dots-three', + init(canvas, eventTarget) { + const hiddenPlugins = plugins.filter((p) => !p.builtIn).slice(overlay.customPluginsToShow); + + createDropdown(); + + document.addEventListener('astro:after-swap', createDropdown); + + function createDropdown() { + const style = document.createElement('style'); + style.innerHTML = ` + #dropdown { + background: rgba(19, 21, 26, 1); + border: 1px solid rgba(52, 56, 65, 1); + border-radius: 12px; + box-shadow: 0px 0px 0px 0px rgba(19, 21, 26, 0.30), 0px 1px 2px 0px rgba(19, 21, 26, 0.29), 0px 4px 4px 0px rgba(19, 21, 26, 0.26), 0px 10px 6px 0px rgba(19, 21, 26, 0.15), 0px 17px 7px 0px rgba(19, 21, 26, 0.04), 0px 26px 7px 0px rgba(19, 21, 26, 0.01); + width: 180px; + padding: 8px; + z-index: 9999999999; + } + + .notification { + display: none; + position: absolute; + top: -4px; + right: -6px; + width: 8px; + height: 8px; + border-radius: 9999px; + border: 1px solid rgba(19, 21, 26, 1); + background: #B33E66; + } + + .notification[data-active] { + display: block; + } + + #dropdown button { + border: 0; + background: transparent; + color: white; + font-family: system-ui, sans-serif; + font-size: 16px; + line-height: 1.2; + white-space: nowrap; + text-decoration: none; + margin: 0; + display: flex; + align-items: center; + width: 100%; + padding: 8px; + border-radius: 8px; + } + + #dropdown button:hover, #dropdown button:focus-visible { + background: rgba(27, 30, 36, 1); + cursor: pointer; + } + + #dropdown button.active { + background: rgba(71, 78, 94, 1); + } + + #dropdown .icon { + position: relative; + height: 24px; + width: 24px; + margin-right: 0.5em; + } + + #dropdown .icon svg { + max-height: 100%; + max-width: 100%; + } + `; + canvas.append(style); + + const dropdown = document.createElement('div'); + dropdown.id = 'dropdown'; + + for (const plugin of hiddenPlugins) { + const buttonContainer = document.createElement('div'); + buttonContainer.classList.add('item'); + const button = document.createElement('button'); + button.setAttribute('data-plugin-id', plugin.id); + + const iconContainer = document.createElement('div'); + const iconElement = getPluginIcon(plugin.icon); + iconContainer.append(iconElement); + + const notification = document.createElement('div'); + notification.classList.add('notification'); + iconContainer.append(notification); + iconContainer.classList.add('icon'); + + button.append(iconContainer); + button.append(document.createTextNode(plugin.name)); + + button.addEventListener('click', () => { + overlay.togglePluginStatus(plugin); + }); + buttonContainer.append(button); + + dropdown.append(buttonContainer); + + eventTarget.addEventListener('plugin-toggled', positionDropdown); + window.addEventListener('resize', positionDropdown); + + plugin.eventTarget.addEventListener('toggle-notification', (evt) => { + if (!(evt instanceof CustomEvent)) return; + + if (settings.config.disablePluginNotification === false) { + notification.toggleAttribute('data-active', evt.detail.state ?? true); + } + + eventTarget.dispatchEvent( + new CustomEvent('toggle-notification', { + detail: { + state: hiddenPlugins.some((p) => p.notification.state === true), + }, + }) + ); + }); + } + + canvas.append(dropdown); + + function getPluginIcon(icon: Icon) { + if (isDefinedIcon(icon)) { + return getIconElement(icon)!; + } + + return icon; + } + + function positionDropdown() { + const moreButtonRect = overlay.shadowRoot + .querySelector('[data-plugin-id="astro:more"]') + ?.getBoundingClientRect(); + const dropdownRect = dropdown.getBoundingClientRect(); + + if (moreButtonRect && dropdownRect) { + dropdown.style.position = 'absolute'; + dropdown.style.top = `${moreButtonRect.top - dropdownRect.height - 12}px`; + dropdown.style.left = `${ + moreButtonRect.left + moreButtonRect.width - dropdownRect.width + }px`; + } + } + } + }, + } satisfies DevOverlayPluginDefinition; + const customPluginsDefinitions = (await loadDevOverlayPlugins()) as DevOverlayPluginDefinition[]; const plugins: DevOverlayPlugin[] = [ - ...[astroDevToolPlugin, astroXrayPlugin, astroAuditPlugin, astroSettingsPlugin].map( - (pluginDef) => preparePlugin(pluginDef, true) - ), + ...[ + astroDevToolPlugin, + astroXrayPlugin, + astroAuditPlugin, + astroSettingsPlugin, + astromorePlugin, + ].map((pluginDef) => preparePlugin(pluginDef, true)), ...customPluginsDefinitions.map((pluginDef) => preparePlugin(pluginDef, false)), ]; diff --git a/packages/astro/src/runtime/client/dev-overlay/overlay.ts b/packages/astro/src/runtime/client/dev-overlay/overlay.ts index 70d95726920b..900c3fb0fe60 100644 --- a/packages/astro/src/runtime/client/dev-overlay/overlay.ts +++ b/packages/astro/src/runtime/client/dev-overlay/overlay.ts @@ -7,6 +7,9 @@ export type DevOverlayPlugin = DevOverlayPluginDefinition & { builtIn: boolean; active: boolean; status: 'ready' | 'loading' | 'error'; + notification: { + state: boolean; + }; eventTarget: EventTarget; }; @@ -20,6 +23,7 @@ export class AstroDevOverlay extends HTMLElement { plugins: DevOverlayPlugin[] = []; HOVER_DELAY = 750; hasBeenInitialized = false; + customPluginsToShow = 3; constructor() { super(); @@ -164,8 +168,8 @@ export class AstroDevOverlay extends HTMLElement { #dev-bar .item .notification { display: none; position: absolute; - top: -2px; - right: 0; + top: -4px; + right: -6px; width: 8px; height: 8px; border-radius: 9999px; @@ -236,17 +240,27 @@ export class AstroDevOverlay extends HTMLElement {
${this.plugins - .filter((plugin) => plugin.builtIn && plugin.id !== 'astro:settings') + .filter( + (plugin) => plugin.builtIn && !['astro:settings', 'astro:more'].includes(plugin.id) + ) .map((plugin) => this.getPluginTemplate(plugin)) .join('')} ${ this.plugins.filter((plugin) => !plugin.builtIn).length > 0 ? `
${this.plugins .filter((plugin) => !plugin.builtIn) + .slice(0, this.customPluginsToShow) .map((plugin) => this.getPluginTemplate(plugin)) .join('')}` : '' } + ${ + this.plugins.filter((plugin) => !plugin.builtIn).length > this.customPluginsToShow + ? this.getPluginTemplate( + this.plugins.find((plugin) => plugin.builtIn && plugin.id === 'astro:more')! + ) + : '' + }
${this.getPluginTemplate( this.plugins.find((plugin) => plugin.builtIn && plugin.id === 'astro:settings')! @@ -438,9 +452,19 @@ export class AstroDevOverlay extends HTMLElement { } plugin.active = newStatus ?? !plugin.active; - const target = this.shadowRoot.querySelector(`[data-plugin-id="${plugin.id}"]`); - if (!target) return; - target.classList.toggle('active', plugin.active); + const mainBarButton = this.shadowRoot.querySelector(`[data-plugin-id="${plugin.id}"]`); + const moreBarButton = this.getPluginCanvasById('astro:more')?.shadowRoot?.querySelector( + `[data-plugin-id="${plugin.id}"]` + ); + + if (mainBarButton) { + mainBarButton.classList.toggle('active', plugin.active); + } + + if (moreBarButton) { + moreBarButton.classList.toggle('active', plugin.active); + } + pluginCanvas.style.display = plugin.active ? 'block' : 'none'; window.requestAnimationFrame(() => { diff --git a/packages/astro/src/runtime/client/dev-overlay/plugins/settings.ts b/packages/astro/src/runtime/client/dev-overlay/plugins/settings.ts index c4202c5c14e7..e0d3384463ef 100644 --- a/packages/astro/src/runtime/client/dev-overlay/plugins/settings.ts +++ b/packages/astro/src/runtime/client/dev-overlay/plugins/settings.ts @@ -15,10 +15,10 @@ const settingsRows = [ name: 'Disable notifications', description: 'Notification bubbles will not be shown when this is enabled.', input: 'checkbox', - settingKey: 'showPluginNotifications', + settingKey: 'disablePluginNotification', changeEvent: (evt: Event) => { if (evt.currentTarget instanceof HTMLInputElement) { - settings.updateSetting('showPluginNotifications', evt.currentTarget.checked); + settings.updateSetting('disablePluginNotification', evt.currentTarget.checked); } }, }, diff --git a/packages/astro/src/runtime/client/dev-overlay/settings.ts b/packages/astro/src/runtime/client/dev-overlay/settings.ts index a6c086d2c9f9..7ba12f2dbf6f 100644 --- a/packages/astro/src/runtime/client/dev-overlay/settings.ts +++ b/packages/astro/src/runtime/client/dev-overlay/settings.ts @@ -1,10 +1,10 @@ export interface Settings { - showPluginNotifications: boolean; + disablePluginNotification: boolean; verbose: boolean; } export const defaultSettings = { - showPluginNotifications: true, + disablePluginNotification: false, verbose: false, } satisfies Settings; diff --git a/packages/astro/src/runtime/client/dev-overlay/ui-library/icons.ts b/packages/astro/src/runtime/client/dev-overlay/ui-library/icons.ts index 7a02369007e5..d9445e44acf8 100644 --- a/packages/astro/src/runtime/client/dev-overlay/ui-library/icons.ts +++ b/packages/astro/src/runtime/client/dev-overlay/ui-library/icons.ts @@ -31,4 +31,6 @@ const icons = { 'check-circle': '', gear: '', + 'dots-three': + '', } as const; diff --git a/packages/astro/src/runtime/client/dev-overlay/ui-library/toggle.ts b/packages/astro/src/runtime/client/dev-overlay/ui-library/toggle.ts index 5ff21fd1837a..63dcba65e304 100644 --- a/packages/astro/src/runtime/client/dev-overlay/ui-library/toggle.ts +++ b/packages/astro/src/runtime/client/dev-overlay/ui-library/toggle.ts @@ -49,4 +49,12 @@ export class DevOverlayToggle extends HTMLElement { this.input.type = 'checkbox'; this.shadowRoot.append(this.input); } + + get value() { + return this.input.value; + } + + set value(val) { + this.input.value = val; + } }