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 ?? '' + }


`;