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:
@@ -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}")),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user