Rebuild sidebar nav to match design's launcher + tabs structure

The previous sidebar listed FAVORITES / PINNED / SPACES / TABS / ARCHIVE
sections side-by-side. The design opens with a single Home anchor row,
then promotes favorites and pinned tabs as Arc-style launcher rows
(brand glyph + label, no section label between Home and the launchers),
then a TABS · {count} section with a + New Tab row at the end.

Spaces are no longer listed as nav rows because the workspace picker
already cycles through them; archived tabs move out of the sidebar (the
ely://archive route still exposes them). The footer is split into a
Settings row + a profile row that opens ely://settings/profiles. Every
launcher tab now resolves its icon through brand_glyph::glyph_for_host,
so figma.com/github.com/etc surfaces show the design's stylized glyphs.
This commit is contained in:
2026-05-09 18:38:24 -04:00
parent 7762d500c9
commit cd6d8dea32
+143 -133
View File
@@ -1,6 +1,6 @@
use ely_browser_core::BrowserSnapshot; use ely_browser_core::BrowserSnapshot;
use ely_design_system::{colors, spacing}; use ely_design_system::{colors, spacing};
use ely_domain::{ArchivedTab, BrowserTab, Profile, Space}; use ely_domain::BrowserTab;
use gpui::{ use gpui::{
AnyElement, BoxShadow, Context, InteractiveElement, IntoElement, ParentElement, SharedString, AnyElement, BoxShadow, Context, InteractiveElement, IntoElement, ParentElement, SharedString,
StatefulInteractiveElement, Styled, div, hsla, linear_color_stop, linear_gradient, point, StatefulInteractiveElement, Styled, div, hsla, linear_color_stop, linear_gradient, point,
@@ -9,8 +9,7 @@ use gpui::{
use gpui_component::{IconName, StyledExt, scroll::ScrollableElement}; use gpui_component::{IconName, StyledExt, scroll::ScrollableElement};
use crate::shell::ElyShell; use crate::shell::ElyShell;
use crate::shell::archive_labels::archive_detail_label; use crate::shell::chrome::{render_glyph_for, render_sidebar_header};
use crate::shell::chrome::render_sidebar_header;
impl ElyShell { impl ElyShell {
pub(crate) fn render_expanded_sidebar( pub(crate) fn render_expanded_sidebar(
@@ -36,60 +35,97 @@ impl ElyShell {
.p(px(10.0)) .p(px(10.0))
.flex() .flex()
.flex_col() .flex_col()
.gap(px(6.0)) .gap(px(2.0))
.child(section_label("FAVORITES")) .child(self.render_home_anchor_row(snapshot, cx))
.children(snapshot.favorites.iter().map(|tab| { .children(snapshot.favorites.iter().map(|tab| {
self.render_favorite_row( self.render_launcher_row(
tab, tab,
&snapshot.profiles,
tab.id() == &snapshot.active_tab_id, tab.id() == &snapshot.active_tab_id,
cx, cx,
) )
})) }))
.child(section_label("PINNED")) .child(section_label("PINNED"))
.children(snapshot.pinned_tabs.iter().map(|tab| { .children(snapshot.pinned_tabs.iter().map(|tab| {
self.render_pinned_row( self.render_launcher_row(
tab, tab,
&snapshot.profiles,
tab.id() == &snapshot.active_tab_id, tab.id() == &snapshot.active_tab_id,
cx, cx,
) )
})) }))
.child(section_label("SPACES")) .child(section_tabs_label(snapshot.tabs.len()))
.children(snapshot.spaces.iter().map(|space| {
self.render_space_row(
space,
space.id() == &snapshot.active_space_id,
cx,
)
}))
.child(section_label("TABS"))
.children(self.render_sidebar_tab_rows(snapshot, cx)) .children(self.render_sidebar_tab_rows(snapshot, cx))
.child(section_label("ARCHIVE")) .child(self.render_new_tab_row(cx)),
.children(
snapshot
.archived_tabs
.iter()
.rev()
.map(|archived_tab| {
self.render_archived_row(archived_tab, snapshot, cx)
}),
),
) )
.child(self.render_sidebar_footer(snapshot)) .child(self.render_sidebar_footer(snapshot, cx))
.into_any_element() .into_any_element()
} }
fn render_sidebar_footer(&self, snapshot: &BrowserSnapshot) -> AnyElement { fn render_sidebar_footer(
&mut self,
snapshot: &BrowserSnapshot,
cx: &mut Context<Self>,
) -> AnyElement {
div() div()
.px(px(14.0)) .px(px(10.0))
.py(px(12.0)) .py(px(8.0))
.flex() .flex()
.items_center() .flex_col()
.gap_2() .gap(px(2.0))
.flex_shrink_0() .flex_shrink_0()
.border_t_1() .border_t_1()
.border_color(rgba(colors::DIVIDER)) .border_color(rgba(colors::DIVIDER))
.child(self.render_settings_row(cx))
.child(self.render_profile_row(snapshot, cx))
.into_any_element()
}
fn render_settings_row(&mut self, cx: &mut Context<Self>) -> AnyElement {
div()
.id(SharedString::from("nav-settings"))
.rounded(px(spacing::RADIUS_NAV))
.px(px(10.0))
.py(px(7.0))
.gap(px(10.0))
.flex()
.items_center()
.text_size(px(13.0))
.text_color(rgb(colors::INK_2))
.cursor_pointer()
.hover(|style| style.bg(rgba(ACTIVE_NAV_BG)).text_color(rgb(colors::INK)))
.active(|style| style.opacity(0.82))
.on_click(cx.listener(|shell, _, window, cx| {
shell.open_internal_tab("ely://settings", window, cx);
}))
.child(
div()
.text_color(rgb(colors::INK_3))
.child(IconName::Settings),
)
.child("Settings")
.into_any_element()
}
fn render_profile_row(
&mut self,
snapshot: &BrowserSnapshot,
cx: &mut Context<Self>,
) -> AnyElement {
let profile_name = snapshot.active_profile_name.clone();
div()
.id(SharedString::from("nav-profile"))
.rounded(px(spacing::RADIUS_NAV))
.px(px(10.0))
.py(px(6.0))
.gap(px(10.0))
.flex()
.items_center()
.cursor_pointer()
.hover(|style| style.bg(rgba(ACTIVE_NAV_BG)))
.active(|style| style.opacity(0.82))
.on_click(cx.listener(|shell, _, window, cx| {
shell.open_internal_tab("ely://settings/profiles", window, cx);
}))
.child( .child(
div() div()
.size(px(26.0)) .size(px(26.0))
@@ -107,31 +143,44 @@ impl ElyShell {
.text_size(px(10.0)) .text_size(px(10.0))
.font_semibold() .font_semibold()
.text_color(rgb(0xffffff)) .text_color(rgb(0xffffff))
.child(profile_initial(&snapshot.active_profile_name)), .child(profile_initial(&profile_name)),
), ),
) )
.child( .child(
div() div()
.flex_1()
.min_w_0()
.truncate()
.text_size(px(13.0)) .text_size(px(13.0))
.font_weight(gpui::FontWeight(500.0)) .font_weight(gpui::FontWeight(500.0))
.text_color(rgb(colors::INK_2)) .text_color(rgb(colors::INK_2))
.child(snapshot.active_profile_name.clone()), .child(profile_name),
)
.child(
div()
.text_color(rgb(colors::INK_4))
.child(IconName::ChevronDown),
) )
.into_any_element() .into_any_element()
} }
fn render_space_row( fn render_home_anchor_row(
&mut self, &mut self,
space: &Space, snapshot: &BrowserSnapshot,
active: bool,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> AnyElement { ) -> AnyElement {
let space_id = space.id().clone(); let active_tab = snapshot
.tabs
.iter()
.find(|tab| tab.id() == &snapshot.active_tab_id);
let active = active_tab
.map(|tab| tab.url().as_str() == "ely://new-tab")
.unwrap_or(false);
let bg_color = if active { ACTIVE_NAV_BG } else { 0x00000000 }; let bg_color = if active { ACTIVE_NAV_BG } else { 0x00000000 };
let text_color = if active { colors::INK } else { colors::INK_2 }; let text_color = if active { colors::INK } else { colors::INK_2 };
div() div()
.id(SharedString::from(format!("space-{}", space.id().as_str()))) .id(SharedString::from("nav-home"))
.rounded(px(spacing::RADIUS_NAV)) .rounded(px(spacing::RADIUS_NAV))
.px(px(10.0)) .px(px(10.0))
.py(px(7.0)) .py(px(7.0))
@@ -143,52 +192,33 @@ impl ElyShell {
.active(|style| style.opacity(0.82)) .active(|style| style.opacity(0.82))
.bg(rgba(bg_color)) .bg(rgba(bg_color))
.when(active, |el| el.shadow(soft_shadow())) .when(active, |el| el.shadow(soft_shadow()))
.on_click(cx.listener(move |shell, _, window, cx| { .on_click(cx.listener(|shell, _, window, cx| {
shell.select_space(&space_id, window, cx); shell.open_internal_tab("ely://new-tab", window, cx);
})) }))
.child(render_workspace_tile(space)) .child(div().text_color(rgb(colors::INK_3)).child(IconName::Frame))
.child( .child(
div() div()
.flex_1()
.text_size(px(13.0)) .text_size(px(13.0))
.font_weight(gpui::FontWeight(500.0)) .font_weight(gpui::FontWeight(500.0))
.text_color(rgb(text_color)) .text_color(rgb(text_color))
.child(space.name().to_string()), .child("Home"),
) )
.into_any_element() .into_any_element()
} }
fn render_favorite_row( fn render_launcher_row(
&mut self, &mut self,
tab: &BrowserTab, tab: &BrowserTab,
profiles: &[Profile],
active: bool, active: bool,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> AnyElement {
self.render_nav_row(tab, profiles, active, IconName::Star, colors::ACCENT, cx)
}
fn render_pinned_row(
&mut self,
tab: &BrowserTab,
profiles: &[Profile],
active: bool,
cx: &mut Context<Self>,
) -> AnyElement {
self.render_nav_row(tab, profiles, active, IconName::Asterisk, colors::INK_3, cx)
}
fn render_nav_row(
&mut self,
tab: &BrowserTab,
_profiles: &[Profile],
active: bool,
icon: IconName,
icon_color: u32,
cx: &mut Context<Self>,
) -> AnyElement { ) -> AnyElement {
let tab_id = tab.id().clone(); let tab_id = tab.id().clone();
let bg_color = if active { ACTIVE_NAV_BG } else { 0x00000000 }; let bg_color = if active { ACTIVE_NAV_BG } else { 0x00000000 };
let text_color = if active { colors::INK } else { colors::INK_2 }; let text_color = if active { colors::INK } else { colors::INK_2 };
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();
div() div()
.id(SharedString::from(format!("nav-{}", tab.id().as_str()))) .id(SharedString::from(format!("nav-{}", tab.id().as_str())))
@@ -206,19 +236,47 @@ impl ElyShell {
.on_click(cx.listener(move |shell, _, window, cx| { .on_click(cx.listener(move |shell, _, window, cx| {
shell.select_tab(&tab_id, window, cx); shell.select_tab(&tab_id, window, cx);
})) }))
.child(div().text_color(rgb(icon_color)).child(icon)) .child(render_glyph_for(host.as_deref(), &initial, 18.0))
.child( .child(
div() div()
.flex_1()
.min_w_0() .min_w_0()
.overflow_hidden() .overflow_hidden()
.truncate()
.text_size(px(13.0)) .text_size(px(13.0))
.font_weight(gpui::FontWeight(500.0)) .font_weight(gpui::FontWeight(500.0))
.text_color(rgb(text_color)) .text_color(rgb(text_color))
.child(tab.title().to_string()), .child(title),
) )
.into_any_element() .into_any_element()
} }
fn render_new_tab_row(&mut self, cx: &mut Context<Self>) -> AnyElement {
div()
.id(SharedString::from("nav-new-tab"))
.rounded(px(spacing::RADIUS_NAV))
.px(px(10.0))
.py(px(7.0))
.gap(px(10.0))
.flex()
.items_center()
.text_color(rgb(colors::INK_3))
.text_size(px(13.0))
.cursor_pointer()
.hover(|style| style.bg(rgba(ACTIVE_NAV_BG)).text_color(rgb(colors::INK)))
.active(|style| style.opacity(0.82))
.on_click(cx.listener(|shell, _, window, cx| {
shell.open_new_tab(window, cx);
}))
.child(
div()
.text_color(rgb(colors::INK_4))
.child(IconName::Plus),
)
.child("New Tab")
.into_any_element()
}
pub(crate) fn render_tab_row( pub(crate) fn render_tab_row(
&mut self, &mut self,
tab: &BrowserTab, tab: &BrowserTab,
@@ -263,55 +321,6 @@ impl ElyShell {
.into_any_element() .into_any_element()
} }
fn render_archived_row(
&mut self,
archived_tab: &ArchivedTab,
snapshot: &BrowserSnapshot,
cx: &mut Context<Self>,
) -> AnyElement {
let tab = archived_tab.tab();
let tab_id = tab.id().clone();
let detail = archive_detail_label(archived_tab, &snapshot.spaces, &snapshot.profiles);
div()
.id(SharedString::from(format!("archived-{}", tab.id().as_str())))
.rounded(px(spacing::RADIUS_NAV))
.px(px(10.0))
.py(px(7.0))
.gap(px(10.0))
.flex()
.items_center()
.cursor_pointer()
.hover(|style| style.bg(rgba(ACTIVE_NAV_BG)))
.active(|style| style.opacity(0.82))
.on_click(cx.listener(move |shell, _, window, cx| {
shell.restore_archived_tab(&tab_id, window, cx);
}))
.child(div().text_color(rgb(colors::INK_4)).child(IconName::Undo2))
.child(
div()
.min_w_0()
.flex()
.flex_col()
.gap_1()
.child(
div()
.text_size(px(13.0))
.font_weight(gpui::FontWeight(500.0))
.text_color(rgb(colors::INK_2))
.overflow_hidden()
.child(tab.title().to_string()),
)
.child(
div()
.text_size(px(11.0))
.text_color(rgb(colors::INK_4))
.overflow_hidden()
.child(detail),
),
)
.into_any_element()
}
} }
fn profile_initial(name: &str) -> String { fn profile_initial(name: &str) -> String {
@@ -322,24 +331,25 @@ fn profile_initial(name: &str) -> String {
.to_string() .to_string()
} }
fn render_workspace_tile(space: &Space) -> impl IntoElement { fn section_tabs_label(count: usize) -> impl IntoElement {
div() div()
.size(px(32.0)) .pt(px(12.0))
.rounded(px(9.0)) .pb(px(4.0))
.bg(linear_gradient( .px(px(10.0))
135.0,
linear_color_stop(hsla(0., 0., 1., 1.0), 0.0),
linear_color_stop(hsla(20.0 / 360.0, 0.6, 0.94, 1.0), 1.0),
))
.shadow(soft_shadow())
.flex() .flex()
.items_center() .items_center()
.justify_center() .gap(px(6.0))
.child( .child(
div() div()
.text_size(px(14.0)) .text_color(rgb(colors::INK_4))
.text_color(rgb(colors::ACCENT)) .child(IconName::Frame),
.child(space.icon().to_string()), )
.child(
div()
.text_size(px(10.5))
.font_weight(gpui::FontWeight(500.0))
.text_color(rgb(colors::INK_4))
.child(format!("TABS · {count}")),
) )
} }