diff --git a/.changeset/six-chefs-flash.md b/.changeset/six-chefs-flash.md
new file mode 100644
index 000000000000..3de93b983741
--- /dev/null
+++ b/.changeset/six-chefs-flash.md
@@ -0,0 +1,5 @@
+---
+'astro': patch
+---
+
+Add a new settings panel to the dev overlay
diff --git a/packages/astro/e2e/dev-overlay.test.js b/packages/astro/e2e/dev-overlay.test.js
index 3e8c6662c705..1a358487cb27 100644
--- a/packages/astro/e2e/dev-overlay.test.js
+++ b/packages/astro/e2e/dev-overlay.test.js
@@ -98,4 +98,23 @@ test.describe('Dev Overlay', () => {
await expect(auditHighlight).not.toBeVisible();
await expect(auditHighlightTooltip).not.toBeVisible();
});
+
+ test('can open Settings plugin', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+
+ const overlay = page.locator('astro-dev-overlay');
+ const pluginButton = overlay.locator('button[data-plugin-id="astro:settings"]');
+ await pluginButton.click();
+
+ const settingsPluginCanvas = overlay.locator(
+ 'astro-dev-overlay-plugin-canvas[data-plugin-id="astro:settings"]'
+ );
+ const settingsWindow = settingsPluginCanvas.locator('astro-dev-overlay-window');
+ await expect(settingsWindow).toHaveCount(1);
+ await expect(settingsWindow).toBeVisible();
+
+ // Toggle plugin off
+ await pluginButton.click();
+ await expect(settingsWindow).not.toBeVisible();
+ });
});
diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts
index faa2df03ebb9..e517a0424d8d 100644
--- a/packages/astro/src/@types/astro.ts
+++ b/packages/astro/src/@types/astro.ts
@@ -24,6 +24,7 @@ import type { AstroIntegrationLogger, Logger, LoggerLevel } from '../core/logger
import type { AstroDevOverlay, DevOverlayCanvas } from '../runtime/client/dev-overlay/overlay.js';
import type { DevOverlayHighlight } from '../runtime/client/dev-overlay/ui-library/highlight.js';
import type { Icon } from '../runtime/client/dev-overlay/ui-library/icons.js';
+import type { DevOverlayToggle } from '../runtime/client/dev-overlay/ui-library/toggle.js';
import type { DevOverlayTooltip } from '../runtime/client/dev-overlay/ui-library/tooltip.js';
import type { DevOverlayWindow } from '../runtime/client/dev-overlay/ui-library/window.js';
import type { AstroComponentFactory, AstroComponentInstance } from '../runtime/server/index.js';
@@ -2578,5 +2579,6 @@ declare global {
'astro-dev-overlay-plugin-canvas': DevOverlayCanvas;
'astro-dev-overlay-tooltip': DevOverlayTooltip;
'astro-dev-overlay-highlight': DevOverlayHighlight;
+ 'astro-dev-overlay-toggle': DevOverlayToggle;
}
}
diff --git a/packages/astro/src/runtime/client/dev-overlay/entrypoint.ts b/packages/astro/src/runtime/client/dev-overlay/entrypoint.ts
index 7336f9d06673..887449c3772c 100644
--- a/packages/astro/src/runtime/client/dev-overlay/entrypoint.ts
+++ b/packages/astro/src/runtime/client/dev-overlay/entrypoint.ts
@@ -1,5 +1,6 @@
import type { DevOverlayPlugin as DevOverlayPluginDefinition } from '../../../@types/astro.js';
import { type AstroDevOverlay, type DevOverlayPlugin } from './overlay.js';
+import { settings } from './settings.js';
let overlay: AstroDevOverlay;
@@ -9,22 +10,26 @@ document.addEventListener('DOMContentLoaded', async () => {
{ default: astroDevToolPlugin },
{ default: astroAuditPlugin },
{ default: astroXrayPlugin },
+ { default: astroSettingsPlugin },
{ AstroDevOverlay, DevOverlayCanvas },
{ DevOverlayCard },
{ DevOverlayHighlight },
{ DevOverlayTooltip },
{ DevOverlayWindow },
+ { DevOverlayToggle },
] = await Promise.all([
// @ts-expect-error
import('astro:dev-overlay'),
import('./plugins/astro.js'),
import('./plugins/audit.js'),
import('./plugins/xray.js'),
+ import('./plugins/settings.js'),
import('./overlay.js'),
import('./ui-library/card.js'),
import('./ui-library/highlight.js'),
import('./ui-library/tooltip.js'),
import('./ui-library/window.js'),
+ import('./ui-library/toggle.js'),
]);
// Register custom elements
@@ -34,6 +39,7 @@ document.addEventListener('DOMContentLoaded', async () => {
customElements.define('astro-dev-overlay-tooltip', DevOverlayTooltip);
customElements.define('astro-dev-overlay-highlight', DevOverlayHighlight);
customElements.define('astro-dev-overlay-card', DevOverlayCard);
+ customElements.define('astro-dev-overlay-toggle', DevOverlayToggle);
overlay = document.createElement('astro-dev-overlay');
@@ -60,7 +66,9 @@ document.addEventListener('DOMContentLoaded', async () => {
newState = evt.detail.state ?? true;
}
- target.querySelector('.notification')?.toggleAttribute('data-active', newState);
+ if (settings.config.showPluginNotifications === false) {
+ target.querySelector('.notification')?.toggleAttribute('data-active', newState);
+ }
});
eventTarget.addEventListener('toggle-plugin', async (evt) => {
@@ -77,8 +85,8 @@ document.addEventListener('DOMContentLoaded', async () => {
const customPluginsDefinitions = (await loadDevOverlayPlugins()) as DevOverlayPluginDefinition[];
const plugins: DevOverlayPlugin[] = [
- ...[astroDevToolPlugin, astroXrayPlugin, astroAuditPlugin].map((pluginDef) =>
- preparePlugin(pluginDef, true)
+ ...[astroDevToolPlugin, astroXrayPlugin, astroAuditPlugin, astroSettingsPlugin].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 e0ab02e481a7..70d95726920b 100644
--- a/packages/astro/src/runtime/client/dev-overlay/overlay.ts
+++ b/packages/astro/src/runtime/client/dev-overlay/overlay.ts
@@ -1,5 +1,6 @@
/* eslint-disable no-console */
import type { DevOverlayPlugin as DevOverlayPluginDefinition } from '../../../@types/astro.js';
+import { settings } from './settings.js';
import { getIconElement, isDefinedIcon, type Icon } from './ui-library/icons.js';
export type DevOverlayPlugin = DevOverlayPluginDefinition & {
@@ -235,14 +236,21 @@ export class AstroDevOverlay extends HTMLElement {
${this.plugins
- .filter((plugin) => plugin.builtIn)
+ .filter((plugin) => plugin.builtIn && plugin.id !== 'astro:settings')
.map((plugin) => this.getPluginTemplate(plugin))
.join('')}
+ ${
+ this.plugins.filter((plugin) => !plugin.builtIn).length > 0
+ ? `
${this.plugins
+ .filter((plugin) => !plugin.builtIn)
+ .map((plugin) => this.getPluginTemplate(plugin))
+ .join('')}`
+ : ''
+ }
- ${this.plugins
- .filter((plugin) => !plugin.builtIn)
- .map((plugin) => this.getPluginTemplate(plugin))
- .join('')}
+ ${this.getPluginTemplate(
+ this.plugins.find((plugin) => plugin.builtIn && plugin.id === 'astro:settings')!
+ )}
@@ -254,7 +262,8 @@ export class AstroDevOverlay extends HTMLElement {
// Create plugin canvases
this.plugins.forEach(async (plugin) => {
if (!this.hasBeenInitialized) {
- console.log(`Creating plugin canvas for ${plugin.id}`);
+ if (settings.config.verbose) console.log(`Creating plugin canvas for ${plugin.id}`);
+
const pluginCanvas = document.createElement('astro-dev-overlay-plugin-canvas');
pluginCanvas.dataset.pluginId = plugin.id;
this.shadowRoot?.append(pluginCanvas);
@@ -321,7 +330,7 @@ export class AstroDevOverlay extends HTMLElement {
if (this.isHidden()) {
this.hoverTimeout = window.setTimeout(() => {
this.toggleOverlay(true);
- }, this.HOVER_DELAY);
+ }, this.HOVER_DELAY + 200); // Slightly higher delay here to prevent users opening the overlay by accident
} else {
this.hoverTimeout = window.setTimeout(() => {
this.toggleMinimizeButton(true);
@@ -374,7 +383,8 @@ export class AstroDevOverlay extends HTMLElement {
const shadowRoot = this.getPluginCanvasById(plugin.id)!.shadowRoot!;
try {
- console.info(`Initializing plugin ${plugin.id}`);
+ if (settings.config.verbose) console.info(`Initializing plugin ${plugin.id}`);
+
await plugin.init?.(shadowRoot, plugin.eventTarget);
plugin.status = 'ready';
diff --git a/packages/astro/src/runtime/client/dev-overlay/plugins/astro.ts b/packages/astro/src/runtime/client/dev-overlay/plugins/astro.ts
index ea3b7f26fc1f..352a018e11e2 100644
--- a/packages/astro/src/runtime/client/dev-overlay/plugins/astro.ts
+++ b/packages/astro/src/runtime/client/dev-overlay/plugins/astro.ts
@@ -1,4 +1,5 @@
import type { DevOverlayPlugin } from '../../../../@types/astro.js';
+import { createWindowWithTransition, waitForTransition } from './utils/window.js';
export default {
id: 'astro',
@@ -10,38 +11,10 @@ export default {
document.addEventListener('astro:after-swap', createWindow);
function createWindow() {
- const style = document.createElement('style');
- style.textContent = `
- :host {
- opacity: 0;
- transition: opacity 0.15s ease-in-out;
- }
-
- :host([data-active]) {
- opacity: 1;
- }
-
- @media screen and (prefers-reduced-motion: no-preference) {
- :host astro-dev-overlay-window {
- transform: translateY(55px) translate(-50%, -50%);
- transition: transform 0.15s ease-in-out;
- transform-origin: center bottom;
- }
-
- :host([data-active]) astro-dev-overlay-window {
- transform: translateY(0) translate(-50%, -50%);
- }
- }
- `;
- canvas.append(style);
-
- const astroWindow = document.createElement('astro-dev-overlay-window');
-
- astroWindow.windowTitle = 'Astro';
- astroWindow.windowIcon = 'astro:logo';
-
- astroWindow.innerHTML = `
-
+ General
+ `,
+ settingsRows.flatMap((setting) => [
+ getElementForSettingAsString(setting),
+ document.createElement('hr'),
+ ])
+ );
+ canvas.append(window);
+
+ function getElementForSettingAsString(setting: SettingRow) {
+ const label = document.createElement('label');
+ label.classList.add('setting-row');
+ const section = document.createElement('section');
+ section.innerHTML = `${setting.name}
${setting.description}`;
+ label.append(section);
+
+ switch (setting.input) {
+ case 'checkbox': {
+ const astroToggle = document.createElement('astro-dev-overlay-toggle');
+ astroToggle.input.addEventListener('change', setting.changeEvent);
+ astroToggle.input.checked = settings.config[setting.settingKey];
+ label.append(astroToggle);
+ }
+ }
+
+ return label;
+ }
+ }
+ },
+ async beforeTogglingOff(canvas) {
+ return await waitForTransition(canvas);
+ },
+} satisfies DevOverlayPlugin;
diff --git a/packages/astro/src/runtime/client/dev-overlay/plugins/utils/window.ts b/packages/astro/src/runtime/client/dev-overlay/plugins/utils/window.ts
new file mode 100644
index 000000000000..04f09d6e6473
--- /dev/null
+++ b/packages/astro/src/runtime/client/dev-overlay/plugins/utils/window.ts
@@ -0,0 +1,56 @@
+import type { Icon } from '../../ui-library/icons.js';
+
+export function createWindowWithTransition(
+ title: string,
+ icon: Icon,
+ windowContent: string,
+ addedNodes: Node[] = []
+): DocumentFragment {
+ const fragment = document.createDocumentFragment();
+
+ const style = document.createElement('style');
+ style.textContent = `
+ :host {
+ opacity: 0;
+ transition: opacity 0.15s ease-in-out;
+ }
+
+ :host([data-active]) {
+ opacity: 1;
+ }
+
+ @media screen and (prefers-reduced-motion: no-preference) {
+ :host astro-dev-overlay-window {
+ transform: translateY(55px) translate(-50%, -50%);
+ transition: transform 0.15s ease-in-out;
+ transform-origin: center bottom;
+ }
+
+ :host([data-active]) astro-dev-overlay-window {
+ transform: translateY(0) translate(-50%, -50%);
+ }
+ }
+ `;
+ fragment.append(style);
+
+ const window = document.createElement('astro-dev-overlay-window');
+ window.windowTitle = title;
+ window.windowIcon = icon;
+ window.innerHTML = windowContent;
+
+ window.append(...addedNodes);
+
+ fragment.append(window);
+
+ return fragment;
+}
+
+export async function waitForTransition(canvas: ShadowRoot): Promise {
+ canvas.host?.removeAttribute('data-active');
+
+ await new Promise((resolve) => {
+ canvas.host.addEventListener('transitionend', resolve);
+ });
+
+ return true;
+}
diff --git a/packages/astro/src/runtime/client/dev-overlay/settings.ts b/packages/astro/src/runtime/client/dev-overlay/settings.ts
new file mode 100644
index 000000000000..a6c086d2c9f9
--- /dev/null
+++ b/packages/astro/src/runtime/client/dev-overlay/settings.ts
@@ -0,0 +1,32 @@
+export interface Settings {
+ showPluginNotifications: boolean;
+ verbose: boolean;
+}
+
+export const defaultSettings = {
+ showPluginNotifications: true,
+ verbose: false,
+} satisfies Settings;
+
+export const settings = getSettings();
+
+function getSettings() {
+ let _settings: Settings = { ...defaultSettings };
+ const overlaySettings = localStorage.getItem('astro:dev-overlay:settings');
+
+ if (overlaySettings) {
+ _settings = { ..._settings, ...JSON.parse(overlaySettings) };
+ }
+
+ function updateSetting(key: keyof Settings, value: Settings[typeof key]) {
+ _settings[key] = value;
+ localStorage.setItem('astro:dev-overlay:settings', JSON.stringify(_settings));
+ }
+
+ return {
+ get config() {
+ return _settings;
+ },
+ updateSetting,
+ };
+}
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 10f33cad114b..7a02369007e5 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
@@ -30,4 +30,5 @@ const icons = {
'',
'check-circle':
'',
+ gear: '',
} 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
new file mode 100644
index 000000000000..5ff21fd1837a
--- /dev/null
+++ b/packages/astro/src/runtime/client/dev-overlay/ui-library/toggle.ts
@@ -0,0 +1,52 @@
+export class DevOverlayToggle extends HTMLElement {
+ shadowRoot: ShadowRoot;
+ input: HTMLInputElement;
+
+ constructor() {
+ super();
+ this.shadowRoot = this.attachShadow({ mode: 'open' });
+
+ this.shadowRoot.innerHTML = `
+
+ `;
+
+ this.input = document.createElement('input');
+ }
+
+ connectedCallback() {
+ this.input.type = 'checkbox';
+ this.shadowRoot.append(this.input);
+ }
+}
diff --git a/packages/astro/src/runtime/client/dev-overlay/ui-library/window.ts b/packages/astro/src/runtime/client/dev-overlay/ui-library/window.ts
index 64bf580769d0..18b515429ad8 100644
--- a/packages/astro/src/runtime/client/dev-overlay/ui-library/window.ts
+++ b/packages/astro/src/runtime/client/dev-overlay/ui-library/window.ts
@@ -19,11 +19,12 @@ export class DevOverlayWindow extends HTMLElement {
this.shadowRoot.innerHTML = `
- ${this.windowIcon ? this.getElementForIcon(this.windowIcon) : ''}${this.windowTitle ?? ''}
+ ${this.windowIcon ? this.getElementForIcon(this.windowIcon) : ''}${
+ this.windowTitle ?? ''
+ }
`;