diff --git a/.changeset/dry-eels-yell.md b/.changeset/dry-eels-yell.md new file mode 100644 index 000000000000..3b7a20f3a1d3 --- /dev/null +++ b/.changeset/dry-eels-yell.md @@ -0,0 +1,5 @@ +--- +"astro": minor +--- + +Adds a new dev toolbar settings option to change the horizontal placement of the dev toolbar on your screen: bottom left, bottom center, or bottom right. diff --git a/packages/astro/e2e/dev-toolbar.test.js b/packages/astro/e2e/dev-toolbar.test.js index ac8d7b4dff59..e7989b78befb 100644 --- a/packages/astro/e2e/dev-toolbar.test.js +++ b/packages/astro/e2e/dev-toolbar.test.js @@ -349,4 +349,37 @@ test.describe('Dev Toolbar', () => { await expect(appButton).not.toHaveClass('active'); } }); + + test('can adjust the placement', async ({ page, astro }) => { + await page.goto(astro.resolveUrl('/audit-no-warning')); + + const toolbar = page.locator('astro-dev-toolbar'); + const settingsAppButton = toolbar.locator('button[data-app-id="astro:settings"]'); + await settingsAppButton.click(); + + const settingsAppCanvas = toolbar.locator( + 'astro-dev-toolbar-app-canvas[data-app-id="astro:settings"]' + ); + const settingsWindow = settingsAppCanvas.locator('astro-dev-toolbar-window'); + await expect(settingsWindow).toBeVisible(); + + for (const placement of ['bottom-left', 'bottom-center', 'bottom-right']) { + const select = toolbar.getByRole('combobox'); + await expect(select).toBeVisible(); + await select.selectOption(placement); + + const toolbarRoot = toolbar.locator('#dev-toolbar-root'); + await expect(toolbarRoot).toHaveAttribute('data-placement', placement); + + for (const appId of ['astro:home', 'astro:xray', 'astro:settings']) { + const appButton = toolbar.locator(`button[data-app-id="${appId}"]`); + await appButton.click(); + + const appCanvas = toolbar.locator(`astro-dev-toolbar-app-canvas[data-app-id="${appId}"]`); + const appWindow = appCanvas.locator('astro-dev-toolbar-window'); + await expect(appWindow).toBeVisible(); + await expect(appWindow).toHaveJSProperty('placement', placement); + } + } + }); }); diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index e39689a25b75..7ebc76d2d26b 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -28,6 +28,7 @@ import type { DevToolbarCard, DevToolbarHighlight, DevToolbarIcon, + DevToolbarSelect, DevToolbarToggle, DevToolbarTooltip, DevToolbarWindow, @@ -2933,6 +2934,7 @@ declare global { 'astro-dev-toolbar-button': DevToolbarButton; 'astro-dev-toolbar-icon': DevToolbarIcon; 'astro-dev-toolbar-card': DevToolbarCard; + 'astro-dev-toolbar-select': DevToolbarSelect; // Deprecated names // TODO: Remove in Astro 5.0 diff --git a/packages/astro/src/runtime/client/dev-toolbar/apps/astro.ts b/packages/astro/src/runtime/client/dev-toolbar/apps/astro.ts index 430da8ea70ab..11e093234290 100644 --- a/packages/astro/src/runtime/client/dev-toolbar/apps/astro.ts +++ b/packages/astro/src/runtime/client/dev-toolbar/apps/astro.ts @@ -1,7 +1,11 @@ import type { DevToolbarApp, DevToolbarMetadata } from '../../../../@types/astro.js'; import { type Icon, isDefinedIcon } from '../ui-library/icons.js'; import { colorForIntegration, iconForIntegration } from './utils/icons.js'; -import { closeOnOutsideClick, createWindowElement } from './utils/window.js'; +import { + closeOnOutsideClick, + createWindowElement, + synchronizePlacementOnUpdate, +} from './utils/window.js'; const astroLogo = ''; @@ -45,6 +49,7 @@ export default { }); closeOnOutsideClick(eventTarget); + synchronizePlacementOnUpdate(eventTarget, canvas); function fetchIntegrationData() { fetch('https://astro.build/api/v1/dev-overlay/', { diff --git a/packages/astro/src/runtime/client/dev-toolbar/apps/settings.ts b/packages/astro/src/runtime/client/dev-toolbar/apps/settings.ts index d403fc191bdc..4f7dc14ce696 100644 --- a/packages/astro/src/runtime/client/dev-toolbar/apps/settings.ts +++ b/packages/astro/src/runtime/client/dev-toolbar/apps/settings.ts @@ -1,6 +1,11 @@ import type { DevToolbarApp } from '../../../../@types/astro.js'; import { type Settings, settings } from '../settings.js'; -import { closeOnOutsideClick, createWindowElement } from './utils/window.js'; +import { isValidPlacement, placements } from '../ui-library/window.js'; +import { + closeOnOutsideClick, + createWindowElement, + synchronizePlacementOnUpdate, +} from './utils/window.js'; interface SettingRow { name: string; @@ -43,6 +48,22 @@ const settingsRows = [ } }, }, + { + name: 'Placement', + description: 'Adjust the placement of the dev toolbar.', + input: 'select', + settingKey: 'placement', + changeEvent: (evt: Event) => { + if (evt.currentTarget instanceof HTMLSelectElement) { + const placement = evt.currentTarget.value; + if (isValidPlacement(placement)) { + document.querySelector('astro-dev-toolbar')?.setToolbarPlacement(placement); + settings.updateSetting('placement', placement); + settings.logger.verboseLog(`Placement set to ${placement}`); + } + } + }, + }, ] satisfies SettingRow[]; export default { @@ -55,6 +76,7 @@ export default { document.addEventListener('astro:after-swap', createSettingsWindow); closeOnOutsideClick(eventTarget); + synchronizePlacementOnUpdate(eventTarget, canvas); function createSettingsWindow() { const windowElement = createWindowElement( @@ -161,10 +183,26 @@ export default { case 'checkbox': { const astroToggle = document.createElement('astro-dev-toolbar-toggle'); astroToggle.input.addEventListener('change', setting.changeEvent); - astroToggle.input.checked = settings.config[setting.settingKey]; + astroToggle.input.checked = settings.config[setting.settingKey] as boolean; label.append(astroToggle); break; } + case 'select': { + const astroSelect = document.createElement('astro-dev-toolbar-select'); + placements.forEach((placement) => { + const option = document.createElement('option'); + option.setAttribute('value', placement); + if (placement === settings.config[setting.settingKey]) { + option.selected = true; + } + option.textContent = + `${placement.slice(0, 1).toUpperCase()}${placement.slice(1)}`.replace('-', ' '); + astroSelect.append(option); + }); + astroSelect.element.addEventListener('change', setting.changeEvent); + label.append(astroSelect); + break; + } default: break; } diff --git a/packages/astro/src/runtime/client/dev-toolbar/apps/utils/window.ts b/packages/astro/src/runtime/client/dev-toolbar/apps/utils/window.ts index 95882cf31198..fb76c2a9ac5f 100644 --- a/packages/astro/src/runtime/client/dev-toolbar/apps/utils/window.ts +++ b/packages/astro/src/runtime/client/dev-toolbar/apps/utils/window.ts @@ -1,6 +1,10 @@ -export function createWindowElement(content: string) { +import { settings } from '../../settings.js'; +import type { Placement } from '../../ui-library/window.js'; + +export function createWindowElement(content: string, placement = settings.config.placement) { const windowElement = document.createElement('astro-dev-toolbar-window'); windowElement.innerHTML = content; + windowElement.placement = placement; return windowElement; } @@ -30,3 +34,17 @@ export function closeOnOutsideClick( } }); } + +export function synchronizePlacementOnUpdate(eventTarget: EventTarget, canvas: ShadowRoot) { + eventTarget.addEventListener('placement-updated', (evt) => { + if (!(evt instanceof CustomEvent)) { + return; + } + const windowElement = canvas.querySelector('astro-dev-toolbar-window'); + if (!windowElement) { + return; + } + const event: CustomEvent<{ placement: Placement }> = evt; + windowElement.placement = event.detail.placement; + }); +} diff --git a/packages/astro/src/runtime/client/dev-toolbar/apps/xray.ts b/packages/astro/src/runtime/client/dev-toolbar/apps/xray.ts index 893f6f6b46fb..6dae6f6cacf6 100644 --- a/packages/astro/src/runtime/client/dev-toolbar/apps/xray.ts +++ b/packages/astro/src/runtime/client/dev-toolbar/apps/xray.ts @@ -7,7 +7,11 @@ import { getElementsPositionInDocument, positionHighlight, } from './utils/highlight.js'; -import { closeOnOutsideClick, createWindowElement } from './utils/window.js'; +import { + closeOnOutsideClick, + createWindowElement, + synchronizePlacementOnUpdate, +} from './utils/window.js'; const icon = ''; @@ -25,6 +29,7 @@ export default { document.addEventListener('astro:page-load', refreshIslandsOverlayPositions); closeOnOutsideClick(eventTarget); + synchronizePlacementOnUpdate(eventTarget, canvas); function addIslandsOverlay() { islandsOverlays.forEach(({ highlightElement }) => { diff --git a/packages/astro/src/runtime/client/dev-toolbar/entrypoint.ts b/packages/astro/src/runtime/client/dev-toolbar/entrypoint.ts index bb69254385f0..5a9f3536c13d 100644 --- a/packages/astro/src/runtime/client/dev-toolbar/entrypoint.ts +++ b/packages/astro/src/runtime/client/dev-toolbar/entrypoint.ts @@ -23,6 +23,7 @@ document.addEventListener('DOMContentLoaded', async () => { DevToolbarButton, DevToolbarBadge, DevToolbarIcon, + DevToolbarSelect, }, ] = await Promise.all([ loadDevToolbarApps() as DevToolbarAppDefinition[], @@ -45,6 +46,7 @@ document.addEventListener('DOMContentLoaded', async () => { customElements.define('astro-dev-toolbar-button', DevToolbarButton); customElements.define('astro-dev-toolbar-badge', DevToolbarBadge); customElements.define('astro-dev-toolbar-icon', DevToolbarIcon); + customElements.define('astro-dev-toolbar-select', DevToolbarSelect); // Add deprecated names // TODO: Remove in Astro 5.0 diff --git a/packages/astro/src/runtime/client/dev-toolbar/settings.ts b/packages/astro/src/runtime/client/dev-toolbar/settings.ts index d031b636da1d..34ab7b5c0526 100644 --- a/packages/astro/src/runtime/client/dev-toolbar/settings.ts +++ b/packages/astro/src/runtime/client/dev-toolbar/settings.ts @@ -1,11 +1,15 @@ +import type { Placement } from './ui-library/window.js'; + export interface Settings { disableAppNotification: boolean; verbose: boolean; + placement: Placement; } export const defaultSettings = { disableAppNotification: false, verbose: false, + placement: 'bottom-center', } satisfies Settings; export const settings = getSettings(); @@ -25,7 +29,7 @@ function getSettings() { _settings = { ..._settings, ...JSON.parse(toolbarSettings) }; } - function updateSetting(key: keyof Settings, value: Settings[typeof key]) { + function updateSetting(key: Key, value: Settings[Key]) { _settings[key] = value; localStorage.setItem('astro:dev-toolbar:settings', JSON.stringify(_settings)); } diff --git a/packages/astro/src/runtime/client/dev-toolbar/toolbar.ts b/packages/astro/src/runtime/client/dev-toolbar/toolbar.ts index cc14d7c53566..278bd0966723 100644 --- a/packages/astro/src/runtime/client/dev-toolbar/toolbar.ts +++ b/packages/astro/src/runtime/client/dev-toolbar/toolbar.ts @@ -2,6 +2,7 @@ import type { DevToolbarApp as DevToolbarAppDefinition } from '../../../@types/astro.js'; import { settings } from './settings.js'; import { type Icon, getIconElement, isDefinedIcon } from './ui-library/icons.js'; +import { type Placement } from './ui-library/window.js'; export type DevToolbarApp = DevToolbarAppDefinition & { builtIn: boolean; @@ -57,8 +58,6 @@ export class AstroDevToolbar extends HTMLElement { #dev-toolbar-root { position: fixed; bottom: 0px; - left: 50%; - transform: translate(-50%, 0%); z-index: 2000000010; display: flex; flex-direction: column; @@ -75,6 +74,17 @@ export class AstroDevToolbar extends HTMLElement { opacity: 0.2; } + #dev-toolbar-root[data-placement="bottom-left"] { + left: 16px; + } + #dev-toolbar-root[data-placement="bottom-center"] { + left: 50%; + transform: translateX(-50%); + } + #dev-toolbar-root[data-placement="bottom-right"] { + right: 16px; + } + #dev-bar-hitbox-above, #dev-bar-hitbox-below { width: 100%; @@ -246,9 +256,7 @@ export class AstroDevToolbar extends HTMLElement { width: 1px; } -
+
@@ -559,6 +567,19 @@ export class AstroDevToolbar extends HTMLElement { ?.querySelector('#dropdown') ?.toggleAttribute('data-no-notification', !newStatus); } + + setToolbarPlacement(newPlacement: Placement) { + this.devToolbarContainer?.setAttribute('data-placement', newPlacement); + this.apps.forEach((app) => { + app.eventTarget.dispatchEvent( + new CustomEvent('placement-updated', { + detail: { + placement: newPlacement, + }, + }) + ); + }); + } } export class DevToolbarCanvas extends HTMLElement { diff --git a/packages/astro/src/runtime/client/dev-toolbar/ui-library/index.ts b/packages/astro/src/runtime/client/dev-toolbar/ui-library/index.ts index c515dccfc721..7b1197ab70c2 100644 --- a/packages/astro/src/runtime/client/dev-toolbar/ui-library/index.ts +++ b/packages/astro/src/runtime/client/dev-toolbar/ui-library/index.ts @@ -3,6 +3,7 @@ export { DevToolbarButton } from './button.js'; export { DevToolbarCard } from './card.js'; export { DevToolbarHighlight } from './highlight.js'; export { DevToolbarIcon } from './icon.js'; +export { DevToolbarSelect } from './select.js'; export { DevToolbarToggle } from './toggle.js'; export { DevToolbarTooltip } from './tooltip.js'; export { DevToolbarWindow } from './window.js'; diff --git a/packages/astro/src/runtime/client/dev-toolbar/ui-library/select.ts b/packages/astro/src/runtime/client/dev-toolbar/ui-library/select.ts new file mode 100644 index 000000000000..ee56f3cb0796 --- /dev/null +++ b/packages/astro/src/runtime/client/dev-toolbar/ui-library/select.ts @@ -0,0 +1,108 @@ +import { settings } from '../settings.js'; + +const styles = ['purple', 'gray', 'red', 'green', 'yellow', 'blue'] as const; + +type SelectStyle = (typeof styles)[number]; + +export class DevToolbarSelect extends HTMLElement { + shadowRoot: ShadowRoot; + element: HTMLSelectElement; + _selectStyle: SelectStyle = 'gray'; + + get selectStyle() { + return this._selectStyle; + } + set selectStyle(value) { + if (!styles.includes(value)) { + settings.logger.error(`Invalid style: ${value}, expected one of ${styles.join(', ')}.`); + return; + } + this._selectStyle = value; + this.updateStyle(); + } + + static observedAttributes = ['select-style']; + + constructor() { + super(); + this.shadowRoot = this.attachShadow({ mode: 'open' }); + this.shadowRoot.innerHTML = ` + + + + `; + this.element = document.createElement('select'); + this.shadowRoot.addEventListener('slotchange', (event) => { + if (event.target instanceof HTMLSlotElement) { + // Manually add slotted elements to