feat(topbar): aggregated notification bell
Adds a Bell icon button to the topbar that sums chatUnreadCount + queueCount and shows a single amber dot with the total. Click goes to Chat if there are unread messages, otherwise to Approvals — the scene most likely to be relevant. Per-tab badges on Approvals and Chat tabs stay (they answer 'where' specifically), but operators who just want 'do I have anything waiting' now get a single one-glance signal in the topbar. Title attr names the breakdown for accessibility. 31/31 vitest pass.
This commit is contained in:
+18
-1
@@ -18,7 +18,7 @@ import Documents from "./scenes/Documents";
|
|||||||
import CommandBar from "./components/CommandBar";
|
import CommandBar from "./components/CommandBar";
|
||||||
import Toaster from "./components/Toaster";
|
import Toaster from "./components/Toaster";
|
||||||
import Console from "./components/Console";
|
import Console from "./components/Console";
|
||||||
import { Cmd, Home, Layers, HistoryIcon, Pulse, Refresh, Branch, Cog, User, Sun, Moon, Bot } from "./components/icons";
|
import { Cmd, Home, Layers, HistoryIcon, Pulse, Refresh, Branch, Cog, User, Sun, Moon, Bot, Bell } from "./components/icons";
|
||||||
import { useBackendHealth } from "./lib/useBackendHealth";
|
import { useBackendHealth } from "./lib/useBackendHealth";
|
||||||
|
|
||||||
|
|
||||||
@@ -216,6 +216,23 @@ export default function App() {
|
|||||||
<Refresh size={12} /> Refresh
|
<Refresh size={12} /> Refresh
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
<button
|
||||||
|
className={`link-btn notif-btn${(chatUnreadCount + queueCount) > 0 ? " has-notif" : ""}`}
|
||||||
|
onClick={() => setScene(chatUnreadCount > 0 ? "chat" : "approvals")}
|
||||||
|
title={
|
||||||
|
chatUnreadCount + queueCount === 0
|
||||||
|
? "Nothing waiting for you"
|
||||||
|
: `${chatUnreadCount > 0 ? `${chatUnreadCount} unread message${chatUnreadCount === 1 ? "" : "s"}` : ""}${chatUnreadCount > 0 && queueCount > 0 ? " · " : ""}${queueCount > 0 ? `${queueCount} item${queueCount === 1 ? "" : "s"} in your queue` : ""}. Click to open ${chatUnreadCount > 0 ? "chat" : "approvals"}.`
|
||||||
|
}
|
||||||
|
aria-label={`Notifications: ${chatUnreadCount} unread, ${queueCount} queued`}
|
||||||
|
>
|
||||||
|
<Bell size={13} />
|
||||||
|
{(chatUnreadCount + queueCount) > 0 && (
|
||||||
|
<span className="notif-dot" aria-hidden>
|
||||||
|
{(chatUnreadCount + queueCount) > 99 ? "99+" : chatUnreadCount + queueCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`link-btn${consoleOpen ? " is-on" : ""}`}
|
className={`link-btn${consoleOpen ? " is-on" : ""}`}
|
||||||
onClick={() => setConsoleOpen(!consoleOpen)}
|
onClick={() => setConsoleOpen(!consoleOpen)}
|
||||||
|
|||||||
@@ -41,3 +41,4 @@ export const Layers = (p: P) => <Svg {...p}><path d="M12 3l9 5-9 5-9-5zM3 13l9 5
|
|||||||
export const HistoryIcon = (p: P) => <Svg {...p}><path d="M3 12a9 9 0 1 0 3-6.7L3 8M3 3v5h5M12 7v5l3 2" /></Svg>;
|
export const HistoryIcon = (p: P) => <Svg {...p}><path d="M3 12a9 9 0 1 0 3-6.7L3 8M3 3v5h5M12 7v5l3 2" /></Svg>;
|
||||||
export const Home = (p: P) => <Svg {...p}><path d="M4 11l8-7 8 7v9h-5v-6h-6v6H4z" /></Svg>;
|
export const Home = (p: P) => <Svg {...p}><path d="M4 11l8-7 8 7v9h-5v-6h-6v6H4z" /></Svg>;
|
||||||
export const ChevronRight = (p: P) => <Svg {...p}><path d="M9 18l6-6-6-6" /></Svg>;
|
export const ChevronRight = (p: P) => <Svg {...p}><path d="M9 18l6-6-6-6" /></Svg>;
|
||||||
|
export const Bell = (p: P) => <Svg {...p}><path d="M6 8a6 6 0 0 1 12 0c0 7 3 7 3 9H3c0-2 3-2 3-9M10 21a2 2 0 0 0 4 0" /></Svg>;
|
||||||
|
|||||||
@@ -2328,3 +2328,24 @@ select.studio-input { background: var(--bp-paper); }
|
|||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
.wizard-form-input:disabled { opacity: 0.7; }
|
.wizard-form-input:disabled { opacity: 0.7; }
|
||||||
|
|
||||||
|
/* Aggregated topbar notification bell */
|
||||||
|
.notif-btn { position: relative; display: inline-flex; align-items: center; gap: 4px; }
|
||||||
|
.notif-btn.has-notif { color: var(--bp-amber, #d97a00); }
|
||||||
|
.notif-dot {
|
||||||
|
position: absolute;
|
||||||
|
top: -3px;
|
||||||
|
right: -4px;
|
||||||
|
min-width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
padding: 0 4px;
|
||||||
|
border-radius: 7px;
|
||||||
|
background: var(--bp-amber, #d97a00);
|
||||||
|
color: #fff;
|
||||||
|
font-family: var(--bp-mono, monospace);
|
||||||
|
font-size: 9px;
|
||||||
|
line-height: 14px;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 700;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user