[Notifications] Activity Center UI
>>> [!note] Migrated issue
<!-- Drupal.org comment -->
<!-- Migrated from issue #3580215. -->
Reported by: [justafish](https://www.drupal.org/user/161058)
Related to !840
>>>
<p>Build the Activity Center dropdown UI: a bell icon with badge count in the Topbar, a dropdown panel listing notifications with type-specific styling, read/unread indicators, and "Mark all as read" functionality. The entire feature is gated behind <code>drupalSettings.canvas.devMode</code>.</p>
<h2>Acceptance criteria</h2>
<h3>Bell icon and badge</h3>
<ul>
<li>A bell icon appears in the Topbar right column, before <code>UnpublishedChanges</code>.</li>
<li>The bell icon is only visible when <code>drupalSettings.canvas.devMode</code> is true.</li>
<li>A badge displays the count of all unread <code>info</code>, <code>error</code>, and <code>warning</code> notifications.</li>
<li>The badge is hidden when the count is 0.</li>
<li>Clicking the bell toggles the Activity Center dropdown.</li>
<li>When the Activity Center is opened and then closed, all unread <code>success</code> and <code>info</code> notifications are automatically marked as read upon close.</li>
</ul>
<h3>Activity Center dropdown</h3>
<ul>
<li>Dropdown panel appears below the bell icon.</li>
<li>Header displays "Activity Center" and a "Mark all as read" button.</li>
<li>"Mark all as read" calls <code>markNotificationsRead</code> with all unread notification IDs, immediately updating the badge count to 0.</li>
<li>Dropdown closes when clicking outside it.</li>
<li>An empty state is displayed when no notifications exist.</li>
</ul>
<h3>Notification cards</h3>
<ul>
<li>Each card displays: left colour border, type icon, title, message, timestamp, and optional action links.</li>
<li>Background colours by type: Warning: <code>#fefbe9</code> (amber/2); Error: <code>#feebec</code> (red/3); Info/Success/Progress: white.</li>
<li>Left border (4px) by type: Warning: <code>#E2A336</code> (amber/8); Error: <code>#E5484D</code> (red/9); Info/Progress: <code>#0090FF</code> (blue/9); Success: <code>#30A46C</code> (green/9).</li>
<li>Each type has a unique icon matching its border colour.</li>
<li>The processing icon spins continuously via CSS animation.</li>
<li>Action links appear as blue text, separated by <code>|</code>.</li>
<li>Clicking an action executes the action only — it does not auto-dismiss or mark the notification as read.</li>
<li>Timestamp displays in the bottom-right corner of the card.</li>
</ul>
<h3>Read/unread indicator</h3>
<ul>
<li><code>info</code>, <code>warning</code>, and <code>error</code> notifications display a read/unread indicator.</li>
<li><code>success</code> and <code>progress</code> notifications do NOT display the indicator.</li>
<li>Unread: filled blue dot. Read: outlined grey circle.</li>
<li>Clicking the indicator toggles read/unread state.</li>
</ul>
<h3>Sorting</h3>
<ul>
<li>Notifications are sorted: processing first, then unread errors, then unread warnings, then everything else chronologically.</li>
<li>When an error or warning is marked as read, it drops into chronological order.</li>
</ul>
<h3>Scrollable notification list</h3>
<ul>
<li>The notification list is scrollable within the dropdown panel.</li>
<li>The dropdown has a max height so it does not overflow the viewport (e.g. <code>min(400px, 60vh)</code>).</li>
<li>All notifications returned by the server (up to 25 plus any processing notifications) are rendered in the scrollable list — no client-side pagination.</li>
</ul>
<h2>Implementation plan</h2>
<h3>File structure</h3>
<p>New files under <code>ui/src/features/notifications/</code>:</p>
<ul>
<li><code>ActivityCenter.tsx</code> — dropdown panel</li>
<li><code>ActivityCenter.module.css</code> — scoped styles</li>
<li><code>NotificationCard.tsx</code> — individual notification card</li>
<li><code>NotificationCard.module.css</code> — card styles</li>
<li><code>NotificationBell.tsx</code> — bell icon + badge for Topbar</li>
<li><code>NotificationBell.module.css</code> — bell/badge styles</li>
</ul>
<h3>Topbar integration</h3>
<p>In <code>ui/src/components/topbar/Topbar.tsx</code>, add <code>NotificationBell</code> to the right column <code>&lt;Flex&gt;</code>, before <code>&lt;PreviewControls /&gt;</code>, conditionally rendered:</p>
<pre>
{getCanvasSettings()?.devMode && <NotificationBell />}
</pre><h3>NotificationBell component</h3>
<p>Use Radix <code>Popover</code> for the dropdown (consistent with existing Radix usage). On open, auto-mark <code>success</code> and <code>info</code> notifications as read. Badge count is computed from all unread <code>info</code>, <code>warning</code>, and <code>error</code> notifications. The open/closed state is stored in Redux via <code>setActivityCenterOpen</code>.</p>
<h3>CSS design tokens</h3>
<p>Use CSS custom properties matching the spec:</p>
<pre>
--notification-bg-warning: #fefbe9;
--notification-bg-error: #feebec;
--notification-border-warning: #E2A336;
--notification-border-error: #E5484D;
--notification-border-info: #0090FF;
--notification-border-success: #30A46C;
</pre><h2>Tests required</h2>
<h3>Component tests (Vitest + React Testing Library)</h3>
<p><code>NotificationBell.test.tsx</code>:</p>
<ul>
<li>Renders bell icon</li>
<li>Shows badge with correct count (all unread info/warning/error)</li>
<li>Hides badge when count is 0</li>
<li>Toggles Activity Center on click</li>
<li>Closing panel auto-marks unread success notifications as read</li>
<li>Closing panel auto-marks unread info notifications as read</li>
<li>Opening panel does NOT auto-mark success/info as read</li>
<li>Opening panel does NOT auto-mark warning/error as read</li>
</ul>
<p><code>ActivityCenter.test.tsx</code>:</p>
<ul>
<li>Renders "Activity Center" header</li>
<li>Renders notification cards</li>
<li>Shows empty state when no notifications</li>
<li>"Mark all as read" marks all unread notifications</li>
<li>Renders all notifications in a scrollable container</li>
</ul>
<p><code>NotificationCard.test.tsx</code>:</p>
<ul>
<li>Renders correct background for warning</li>
<li>Renders correct background for error</li>
<li>Renders correct left border colour</li>
<li>Shows read/unread dot for info/warning/error</li>
<li>Hides read/unread dot for success/processing</li>
<li>Clicking dot toggles read state</li>
<li>Renders action links separated by pipe</li>
<li>Renders timestamp</li>
<li>Processing icon has spin animation</li>
</ul>
<h3>Playwright E2E tests</h3>
<p>File: <code>tests/src/Playwright/notifications.spec.ts</code></p>
<p>Notifications are seeded via <code>drush php-eval</code> calls to <code>NotificationService::create()</code>. Tests should use <code>page.waitForResponse</code> or <code>expect.poll</code> to handle the async polling cycle rather than arbitrary timeouts. Tests that validate sorting should seed notifications with controlled timestamps to ensure deterministic order.</p>
<p>Test setup installs <code>canvas</code>, <code>canvas_dev_mode</code>, and <code>canvas_e2e_support</code> modules before the suite runs.</p>
<p>Test cases:</p>
<ul>
<li>Bell icon visible with devMode enabled</li>
<li>Badge count reflects all unread notifications</li>
<li>Badge hidden when no unread notifications exist</li>
<li>Activity Center opens on bell click</li>
<li>Activity Center closes on outside click</li>
<li>Notification cards render with correct styling per type</li>
<li>Processing notification shows spinner animation</li>
<li>Sorting: processing first, then unread errors, then rest</li>
<li>Read/unread dot toggle decrements badge count</li>
<li>Mark all as read clears badge and updates all dots</li>
<li>Auto-read on panel open: info and success marked as read, error remains unread</li>
<li>Key-based replacement: seeding a notification with an existing key removes the previous one</li>
<li>Scrollable list renders all seeded notifications</li>
<li>Empty state displayed when no notifications exist</li>
<li>Feature flag gating: bell icon absent when <code>canvas_dev_mode</code> is uninstalled</li>
</ul>
<h2>Manual testing steps</h2>
<ol>
<li>Enable <code>canvas_dev_mode</code>. Create several notifications via Drush (<code>info</code>, <code>warning</code>, <code>error</code>, <code>success</code>, <code>processing</code>). Open Canvas UI. Verify the bell icon appears and the badge count is correct.</li>
<li>Click the bell to open the Activity Center. Verify: notifications are sorted correctly (processing at top, then unread errors, unread warnings, then everything else); each card has the correct background colour and left border; the processing card icon is spinning; success and info notifications are automatically marked as read; warning and error notifications remain unread; badge count decrements accordingly.</li>
<li>Click the read/unread dot on a notification. Verify the dot changes appearance, the badge decrements, and if the notification was an unread error/warning it moves to the chronological section.</li>
<li>Click "Mark all as read". Verify all dots show read state and the badge disappears.</li>
<li>Create a warning notification with key <code>sync</code>, then create a processing notification with the same key. Verify only the processing notification is visible in the Activity Center.</li>
<li>Create more than 10 notifications. Verify all appear in the dropdown and the list is scrollable.</li>
<li>Disable <code>canvas_dev_mode</code> and reload. Verify the bell icon is no longer present.</li>
</ol>
issue