diff --git a/.changeset/hungry-needles-cough.md b/.changeset/hungry-needles-cough.md
new file mode 100644
index 000000000000..e7b6282d072a
--- /dev/null
+++ b/.changeset/hungry-needles-cough.md
@@ -0,0 +1,17 @@
+---
+"astro": minor
+---
+
+Adds support for emitting warning and info notifications from dev toolbar apps.
+
+When using the `toggle-notification` event, the severity can be specified through `detail.level`:
+
+```ts
+eventTarget.dispatchEvent(
+ new CustomEvent("toggle-notification", {
+ detail: {
+ level: "warning",
+ },
+ })
+);
+```
diff --git a/packages/astro/e2e/dev-toolbar.test.js b/packages/astro/e2e/dev-toolbar.test.js
index c3e5528fb10a..b2e6242b47cf 100644
--- a/packages/astro/e2e/dev-toolbar.test.js
+++ b/packages/astro/e2e/dev-toolbar.test.js
@@ -295,6 +295,20 @@ test.describe('Dev Toolbar', () => {
expect(clientRenderTime).not.toBe(null);
});
+ test('apps can show notifications', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+
+ const toolbar = page.locator('astro-dev-toolbar');
+ const appButton = toolbar.locator('button[data-app-id="my-plugin"]');
+ await appButton.click();
+
+ const customAppNotification = appButton.locator('.icon .notification');
+ await expect(customAppNotification).toHaveAttribute('data-active');
+ await expect(customAppNotification).toHaveAttribute('data-level', 'warning');
+
+ await expect(customAppNotification).toBeVisible();
+ });
+
test('can quit apps by clicking outside the window', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/'));
diff --git a/packages/astro/e2e/fixtures/dev-toolbar/custom-plugin.js b/packages/astro/e2e/fixtures/dev-toolbar/custom-plugin.js
index 42340b1ffa25..10bb7faff93a 100644
--- a/packages/astro/e2e/fixtures/dev-toolbar/custom-plugin.js
+++ b/packages/astro/e2e/fixtures/dev-toolbar/custom-plugin.js
@@ -2,7 +2,7 @@ export default {
id: 'my-plugin',
name: 'My Plugin',
icon: 'astro:logo',
- init(canvas) {
+ init(canvas, eventTarget) {
const astroWindow = document.createElement('astro-dev-toolbar-window');
const myButton = document.createElement('astro-dev-toolbar-button');
myButton.size = 'medium';
@@ -13,6 +13,14 @@ export default {
console.log('Clicked!');
});
+ eventTarget.dispatchEvent(
+ new CustomEvent("toggle-notification", {
+ detail: {
+ level: "warning",
+ },
+ })
+ );
+
astroWindow.appendChild(myButton);
canvas.appendChild(astroWindow);
diff --git a/packages/astro/src/runtime/client/dev-toolbar/entrypoint.ts b/packages/astro/src/runtime/client/dev-toolbar/entrypoint.ts
index 6a62b8416612..f115decf0676 100644
--- a/packages/astro/src/runtime/client/dev-toolbar/entrypoint.ts
+++ b/packages/astro/src/runtime/client/dev-toolbar/entrypoint.ts
@@ -62,30 +62,47 @@ document.addEventListener('DOMContentLoaded', async () => {
overlay = document.createElement('astro-dev-toolbar');
+ const notificationLevels = ['error', 'warning', 'info'] as const;
+ const notificationSVGs: Record<(typeof notificationLevels)[number], string> = {
+ error:
+ '',
+ warning:
+ '',
+ info: '',
+ } as const;
+
const prepareApp = (appDefinition: DevToolbarAppDefinition, builtIn: boolean): DevToolbarApp => {
const eventTarget = new EventTarget();
- const app = {
+ const app: DevToolbarApp = {
...appDefinition,
builtIn: builtIn,
active: false,
- status: 'loading' as const,
- notification: { state: false },
+ status: 'loading',
+ notification: { state: false, level: undefined },
eventTarget: eventTarget,
};
// Events apps can send to the overlay to update their status
eventTarget.addEventListener('toggle-notification', (evt) => {
+ if (!(evt instanceof CustomEvent)) return;
+
const target = overlay.shadowRoot?.querySelector(`[data-app-id="${app.id}"]`);
- if (!target) return;
+ const notificationElement = target?.querySelector('.notification');
+ if (!target || !notificationElement) return;
- let newState = true;
- if (evt instanceof CustomEvent) {
- newState = evt.detail.state ?? true;
- }
+ let newState = evt.detail.state ?? true;
+ let level = notificationLevels.includes(evt?.detail?.level)
+ ? (evt.detail.level as (typeof notificationLevels)[number])
+ : 'error';
app.notification.state = newState;
+ if (newState) app.notification.level = level;
- target.querySelector('.notification')?.toggleAttribute('data-active', newState);
+ notificationElement.toggleAttribute('data-active', newState);
+ if (newState) {
+ notificationElement.setAttribute('data-level', level);
+ notificationElement.innerHTML = notificationSVGs[level];
+ }
});
const onToggleApp = async (evt: Event) => {
@@ -137,12 +154,13 @@ document.addEventListener('DOMContentLoaded', async () => {
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;
+ right: -5px;
+ width: 12px;
+ height: 10px;
+ }
+
+ .notification svg {
+ display: block;
}
#dropdown:not([data-no-notification]) .notification[data-active] {
@@ -222,12 +240,33 @@ document.addEventListener('DOMContentLoaded', async () => {
app.eventTarget.addEventListener('toggle-notification', (evt) => {
if (!(evt instanceof CustomEvent)) return;
- notification.toggleAttribute('data-active', evt.detail.state ?? true);
+ let newState = evt.detail.state ?? true;
+ let level = notificationLevels.includes(evt?.detail?.level)
+ ? (evt.detail.level as (typeof notificationLevels)[number])
+ : 'error';
+
+ notification.toggleAttribute('data-active', newState);
+
+ if (newState) {
+ notification.setAttribute('data-level', level);
+ notification.innerHTML = notificationSVGs[level];
+ }
+
+ app.notification.state = newState;
+ if (newState) app.notification.level = level;
eventTarget.dispatchEvent(
new CustomEvent('toggle-notification', {
detail: {
state: hiddenApps.some((p) => p.notification.state === true),
+ level:
+ ['error', 'warning', 'info'].find((notificationLevel) =>
+ hiddenApps.some(
+ (p) =>
+ p.notification.state === true &&
+ p.notification.level === notificationLevel
+ )
+ ) ?? 'error',
},
})
);
diff --git a/packages/astro/src/runtime/client/dev-toolbar/toolbar.ts b/packages/astro/src/runtime/client/dev-toolbar/toolbar.ts
index 6ce2978c0b97..144f414ee3e5 100644
--- a/packages/astro/src/runtime/client/dev-toolbar/toolbar.ts
+++ b/packages/astro/src/runtime/client/dev-toolbar/toolbar.ts
@@ -9,6 +9,7 @@ export type DevToolbarApp = DevToolbarAppDefinition & {
status: 'ready' | 'loading' | 'error';
notification: {
state: boolean;
+ level?: 'error' | 'warning' | 'info';
};
eventTarget: EventTarget;
};
@@ -187,8 +188,8 @@ export class AstroDevToolbar extends HTMLElement {
}
}
- #dev-bar #bar-container .item.active .notification {
- border-color: rgba(71, 78, 94, 1);
+ #dev-bar #bar-container .item.active .notification rect, #dev-bar #bar-container .item.active .notification path {
+ stroke: rgba(71, 78, 94, 1);
}
#dev-bar .item .icon {
@@ -198,7 +199,7 @@ export class AstroDevToolbar extends HTMLElement {
user-select: none;
}
- #dev-bar .item svg {
+ #dev-bar .item .icon>svg {
width: 20px;
height: 20px;
display: block;
@@ -216,11 +217,12 @@ export class AstroDevToolbar extends HTMLElement {
position: absolute;
top: -4px;
right: -6px;
- width: 8px;
- height: 8px;
- border-radius: 9999px;
- border: 1px solid rgba(19, 21, 26, 1);
- background: #B33E66;
+ width: 10px;
+ height: 10px;
+ }
+
+ #dev-bar .item .notification svg {
+ display: block;
}
#dev-toolbar-root:not([data-no-notification]) #dev-bar .item .notification[data-active] {