diff --git a/.changeset/tidy-moles-brush.md b/.changeset/tidy-moles-brush.md new file mode 100644 index 000000000000..46163cef68b4 --- /dev/null +++ b/.changeset/tidy-moles-brush.md @@ -0,0 +1,20 @@ +--- +'astro': minor +--- + +Adds optional `placement` config option for the dev toolbar. + +You can now configure the default toolbar position (`'bottom-left'`, `'bottom-center'`, or `'bottom-right'`) via `devToolbar.placement` in your Astro config. This option is helpful for sites with UI elements (chat widgets, cookie banners) that are consistently obscured by the toolbar in the dev environment. + +You can set a project default that is consistent across environments (e.g. dev machines, browser instances, team members): + +```js +// astro.config.mjs +export default defineConfig({ + devToolbar: { + placement: 'bottom-left', + }, +}); +``` + +User preferences from the toolbar UI (stored in `localStorage`) still take priority, so this setting can be overridden in individual situations as necessary. diff --git a/packages/astro/src/core/config/schemas/base.ts b/packages/astro/src/core/config/schemas/base.ts index c2b4a42ef992..485f3948ea4d 100644 --- a/packages/astro/src/core/config/schemas/base.ts +++ b/packages/astro/src/core/config/schemas/base.ts @@ -293,6 +293,7 @@ export const AstroConfigSchema = z.object({ devToolbar: z .object({ enabled: z.boolean().default(ASTRO_CONFIG_DEFAULTS.devToolbar.enabled), + placement: z.enum(['bottom-left', 'bottom-center', 'bottom-right']).optional(), }) .default(ASTRO_CONFIG_DEFAULTS.devToolbar), markdown: z diff --git a/packages/astro/src/runtime/client/dev-toolbar/settings.ts b/packages/astro/src/runtime/client/dev-toolbar/settings.ts index 1f4ce78e6e02..996e94d61129 100644 --- a/packages/astro/src/runtime/client/dev-toolbar/settings.ts +++ b/packages/astro/src/runtime/client/dev-toolbar/settings.ts @@ -1,4 +1,5 @@ -import type { Placement } from './ui-library/window.js'; +import type { DevToolbarMetadata } from '../../../types/public/toolbar.js'; +import { isValidPlacement, type Placement } from './ui-library/window.js'; export interface Settings { disableAppNotification: boolean; @@ -15,7 +16,16 @@ export const defaultSettings = { export const settings = getSettings(); function getSettings() { + // 1. Start with hardcoded defaults let _settings: Settings = { ...defaultSettings }; + + // 2. Override with config placement (if provided) + const configPlacement = (globalThis as DevToolbarMetadata).__astro_dev_toolbar__?.placement; + if (configPlacement && isValidPlacement(configPlacement)) { + _settings.placement = configPlacement; + } + + // 3. Override with localStorage (preserves user's UI choice) const toolbarSettings = localStorage.getItem('astro:dev-toolbar:settings'); if (toolbarSettings) { diff --git a/packages/astro/src/types/public/config.ts b/packages/astro/src/types/public/config.ts index 4961027c1f27..caeed6522661 100644 --- a/packages/astro/src/types/public/config.ts +++ b/packages/astro/src/types/public/config.ts @@ -1171,6 +1171,19 @@ export interface AstroUserConfig< * This option is scoped to the entire project, to only disable the toolbar for yourself, run `npm run astro preferences disable devToolbar`. To disable the toolbar for all your Astro projects, run `npm run astro preferences disable devToolbar --global`. */ enabled: boolean; + + /** + * @docs + * @name devToolbar.placement + * @version 5.17.0 + * @type {'bottom-left' | 'bottom-center' | 'bottom-right'} + * @default `'bottom-center'` + * @description + * The default placement of the Astro Dev Toolbar on the screen. + * + * The placement of the toolbar can still be changed via the toolbar settings UI. Once changed, the user's preference is saved in `localStorage` and overrides this configuration value. + */ + placement?: 'bottom-left' | 'bottom-center' | 'bottom-right'; }; /** diff --git a/packages/astro/src/types/public/toolbar.ts b/packages/astro/src/types/public/toolbar.ts index 21b1db6b72cd..fce192d3682b 100644 --- a/packages/astro/src/types/public/toolbar.ts +++ b/packages/astro/src/types/public/toolbar.ts @@ -61,6 +61,8 @@ export type DevToolbarApp = { // An app that has been loaded and as such contain all of its properties export type ResolvedDevToolbarApp = DevToolbarAppMeta & DevToolbarApp; +export type DevToolbarPlacement = 'bottom-left' | 'bottom-center' | 'bottom-right'; + export type DevToolbarMetadata = Window & typeof globalThis & { __astro_dev_toolbar__: { @@ -68,5 +70,6 @@ export type DevToolbarMetadata = Window & version: string; latestAstroVersion: string | undefined; debugInfo: string; + placement?: DevToolbarPlacement; }; }; diff --git a/packages/astro/src/vite-plugin-astro-server/pipeline.ts b/packages/astro/src/vite-plugin-astro-server/pipeline.ts index 7d95a11b2d7c..50200d7146cd 100644 --- a/packages/astro/src/vite-plugin-astro-server/pipeline.ts +++ b/packages/astro/src/vite-plugin-astro-server/pipeline.ts @@ -101,6 +101,7 @@ export class DevPipeline extends Pipeline { // enabled, it would nice to request the debug info through import.meta.hot // when the button is click to defer execution as much as possible debugInfo: await this.getDebugInfo(), + placement: settings.config.devToolbar.placement, }; // Additional data for the dev overlay diff --git a/packages/astro/test/units/config/config-validate.test.js b/packages/astro/test/units/config/config-validate.test.js index 6dc9555cb281..b84e0a7c4dda 100644 --- a/packages/astro/test/units/config/config-validate.test.js +++ b/packages/astro/test/units/config/config-validate.test.js @@ -589,4 +589,29 @@ describe('Config Validation', () => { ); }); }); + + describe('devToolbar', () => { + it('should allow valid placement values', async () => { + for (const placement of ['bottom-left', 'bottom-center', 'bottom-right']) { + const result = await validateConfig({ + devToolbar: { placement }, + }); + assert.equal(result.devToolbar.placement, placement); + } + }); + + it('should allow omitting placement (optional)', async () => { + const result = await validateConfig({ + devToolbar: { enabled: true }, + }); + assert.equal(result.devToolbar.placement, undefined); + }); + + it('should reject invalid placement values', async () => { + const configError = await validateConfig({ + devToolbar: { placement: 'top-left' }, + }).catch((err) => err); + assert.equal(configError instanceof z.ZodError, true); + }); + }); });