Surface unread counts on sidebar launcher rows

The design's Slack/Linear/Gmail rows show numeric badges (12, 3) that
real apps publish in their tab title prefix — "(12) Slack | …",
"(3) Inbox — Linear", "(99+) Gmail". Add a pure parser
ely_domain::parse_title_unread_count that recognises the leading
"(N)" pattern and surfaces it through BrowserTab::unread_count(); the
sidebar launcher row renders a glass pill badge whenever the count
is non-zero, capped at "99+" for very high counts.

No domain field, no fabrication: the badge appears only when a real
website publishes its own unread count via the title. Tests cover
canonical formats, non-prefixed titles, leading whitespace, and
non-numeric / overflow inputs.
This commit is contained in:
2026-05-09 19:25:31 -04:00
parent 3856c2ea70
commit 3903004ede
3 changed files with 70 additions and 1 deletions
@@ -220,6 +220,7 @@ impl ElyShell {
let host = tab.url().host().map(|host| host.to_string());
let title = tab.title().to_string();
let initial = title.chars().next().unwrap_or('?').to_string();
let unread = tab.unread_count();
let group_name = SharedString::from(format!("launcher-{}", tab.id().as_str()));
let close_id = SharedString::from(format!("launcher-close-{}", tab.id().as_str()));
@@ -252,6 +253,7 @@ impl ElyShell {
.text_color(rgb(text_color))
.child(title),
)
.when(unread > 0, |el| el.child(render_unread_badge(unread)))
.child(
div()
.id(close_id)
@@ -354,6 +356,24 @@ fn profile_initial(name: &str) -> String {
.to_string()
}
fn render_unread_badge(count: u32) -> impl IntoElement {
let label = if count > 99 {
"99+".to_string()
} else {
count.to_string()
};
div()
.px(px(6.0))
.py(px(1.0))
.rounded(px(999.0))
.bg(rgba(UNREAD_BADGE_BG))
.text_size(px(10.0))
.font_weight(gpui::FontWeight(500.0))
.text_color(rgb(colors::INK_3))
.child(label)
}
fn section_tabs_label(count: usize) -> impl IntoElement {
div()
.pt(px(12.0))
@@ -389,6 +409,7 @@ fn section_label(label: &'static str) -> impl IntoElement {
pub(crate) const ACTIVE_NAV_BG: u32 = 0xffffffd9;
pub(crate) const PANEL_BG: u32 = 0xffffffe0;
const UNREAD_BADGE_BG: u32 = 0x281e140f;
pub(crate) fn panel_shadow() -> Vec<BoxShadow> {
vec![