From 8bf4df45138eecb69b324e2021cd89166df4fc15 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Wed, 14 Oct 2026 05:25:11 +0200 Subject: [PATCH] feat: add a notifications panel for plugin notifications and app errors --- .../__tests__/top-bar-plugin-actions.test.tsx | 7 +- components/layout/mobile-header.tsx | 26 +- components/layout/mv-top-bar.tsx | 2 + components/layout/navigation-rail.tsx | 3 + .../__tests__/notification-center.test.tsx | 121 ++++++++ .../notifications/notification-center.tsx | 262 ++++++++++++++++++ .../plugin-host-notifications.test.ts | 27 ++ lib/plugin-sandbox/host-api-notify.ts | 4 + locales/ar/common.json | 11 + locales/ca/common.json | 11 + locales/cs/common.json | 11 + locales/da/common.json | 11 + locales/de/common.json | 11 + locales/en/common.json | 11 + locales/es/common.json | 11 + locales/fa/common.json | 11 + locales/fr/common.json | 11 + locales/he/common.json | 11 + locales/hu/common.json | 11 + locales/it/common.json | 11 + locales/ja/common.json | 11 + locales/ko/common.json | 11 + locales/lv/common.json | 11 + locales/mn/common.json | 11 + locales/nb/common.json | 11 + locales/nl/common.json | 11 + locales/pl/common.json | 11 + locales/pt/common.json | 11 + locales/ro/common.json | 11 + locales/ru/common.json | 11 + locales/sk/common.json | 11 + locales/tr/common.json | 11 + locales/uk/common.json | 11 + locales/zh-TW/common.json | 11 + locales/zh/common.json | 11 + .../notification-center-store.test.ts | 70 +++++ stores/notification-center-store.ts | 114 ++++++++ stores/toast-store.ts | 5 + 38 files changed, 930 insertions(+), 8 deletions(-) create mode 100644 components/notifications/__tests__/notification-center.test.tsx create mode 100644 components/notifications/notification-center.tsx create mode 100644 stores/__tests__/notification-center-store.test.ts create mode 100644 stores/notification-center-store.ts diff --git a/components/layout/__tests__/top-bar-plugin-actions.test.tsx b/components/layout/__tests__/top-bar-plugin-actions.test.tsx index e3f09f61e..d4e4c48ae 100644 --- a/components/layout/__tests__/top-bar-plugin-actions.test.tsx +++ b/components/layout/__tests__/top-bar-plugin-actions.test.tsx @@ -56,8 +56,11 @@ describe('phone header', () => { render(); fireEvent.click(screen.getByRole('button', { name: 'more' })); - expect(screen.getAllByRole('menuitem').map((m) => m.textContent)).toEqual(['title']); - fireEvent.click(screen.getByRole('menuitem', { name: 'title' })); + // Notifications and the command palette (both labelled "title" here, as + // the translations are keys in tests); no plugin entries. + const items = screen.getAllByRole('menuitem'); + expect(items.map((m) => m.textContent)).toEqual(['title', 'title']); + fireEvent.click(items[1]); expect(opened).toHaveBeenCalled(); window.removeEventListener('bulwark:command-palette', opened); }); diff --git a/components/layout/mobile-header.tsx b/components/layout/mobile-header.tsx index 9fa7fbf64..3dc6db4cd 100644 --- a/components/layout/mobile-header.tsx +++ b/components/layout/mobile-header.tsx @@ -1,7 +1,7 @@ "use client"; import { useCallback, useEffect, useRef, useState } from "react"; -import { Menu, ArrowLeft, Plus, Search, X, MoreVertical, Terminal } from "@/components/icons"; +import { Menu, ArrowLeft, Plus, Search, X, MoreVertical, Terminal, Bell } from "@/components/icons"; import { Button } from "@/components/ui/button"; import { useUIStore } from "@/stores/ui-store"; import { useSettingsStore } from "@/stores/settings-store"; @@ -13,18 +13,22 @@ import { PluginActions } from "@/components/plugins/plugin-actions"; import { useMenuNavigation } from "@/hooks/use-menu-navigation"; import { ContextMenuItem } from "@/components/ui/context-menu"; import { openCommandPalette } from "@/lib/app-commands"; +import { NotificationSheet, formatBadgeCount, useUnreadNotificationCount } from "@/components/notifications/notification-center"; /** Plugin buttons in the top bar are not about any one item. */ const TOP_BAR_CONTEXT: Record = {}; /** - * The phone header's overflow menu: the command palette (no Ctrl+K on a - * phone) and what the desktop top bar shows as buttons (plugin - * top-bar-actions) and has no room for here. + * The phone header's overflow menu: the notifications (the desktop bell), the + * command palette (no Ctrl+K on a phone) and what the desktop top bar shows + * as buttons (plugin top-bar-actions) and has no room for here. */ function MobileHeaderMenu() { const t = useTranslations('top_bar'); const tPalette = useTranslations('command_palette'); + const tNotifications = useTranslations('notification_center'); + const unread = useUnreadNotificationCount(); + const [showNotifications, setShowNotifications] = useState(false); const [open, setOpen] = useState(false); const triggerRef = useRef(null); const close = useCallback(() => setOpen(false), []); @@ -47,12 +51,15 @@ function MobileHeaderMenu() { variant="ghost" size="icon" onClick={() => setOpen((v) => !v)} - className={cn("h-11 w-11", open && "bg-accent")} - aria-label={t('more')} + className={cn("relative h-11 w-11", open && "bg-accent")} + aria-label={unread > 0 ? `${t('more')} (${tNotifications('unread_count', { count: unread })})` : t('more')} aria-haspopup="menu" aria-expanded={open} > + {unread > 0 && ( +