From 9503795c6c816d4e3fae687cfd1d2621e2ac6d0d Mon Sep 17 00:00:00 2001
From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
Date: Tue, 31 Mar 2026 12:42:24 -0300
Subject: [PATCH] sidebar: Add some design adjustments (#52832)
- Adjust thread item and gradient fade colors for themes that define
transparent colors for the tokens we use on them
- Make the entire project header clickable area activate the workspace
instead of collapsing the group. The chevron is now an icon button that
does that, which makes it consistent with the collab panel and settings
UI.
Release Notes:
- N/A
---
assets/icons/focus.svg | 7 --
crates/icons/src/icons.rs | 1 -
crates/sidebar/src/sidebar.rs | 94 +++++++++++-----------
crates/ui/src/components/ai/thread_item.rs | 15 ++--
crates/ui/src/components/gradient_fade.rs | 14 ++--
5 files changed, 64 insertions(+), 67 deletions(-)
delete mode 100644 assets/icons/focus.svg
diff --git a/assets/icons/focus.svg b/assets/icons/focus.svg
deleted file mode 100644
index 9003e437ce..0000000000
--- a/assets/icons/focus.svg
+++ /dev/null
@@ -1,7 +0,0 @@
-
diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs
index 89932125c1..acec738030 100644
--- a/crates/icons/src/icons.rs
+++ b/crates/icons/src/icons.rs
@@ -131,7 +131,6 @@ pub enum IconName {
FileTree,
Filter,
Flame,
- Focus,
Folder,
FolderOpen,
FolderPlus,
diff --git a/crates/sidebar/src/sidebar.rs b/crates/sidebar/src/sidebar.rs
index a0f6e88c2b..31c796b200 100644
--- a/crates/sidebar/src/sidebar.rs
+++ b/crates/sidebar/src/sidebar.rs
@@ -1281,13 +1281,14 @@ impl Sidebar {
) -> AnyElement {
let id_prefix = if is_sticky { "sticky-" } else { "" };
let id = SharedString::from(format!("{id_prefix}project-header-{ix}"));
+ let disclosure_id = SharedString::from(format!("disclosure-{ix}"));
let group_name = SharedString::from(format!("{id_prefix}header-group-{ix}"));
let is_collapsed = self.collapsed_groups.contains(path_list);
- let disclosure_icon = if is_collapsed {
- IconName::ChevronRight
+ let (disclosure_icon, disclosure_tooltip) = if is_collapsed {
+ (IconName::ChevronRight, "Expand Project")
} else {
- IconName::ChevronDown
+ (IconName::ChevronDown, "Collapse Project")
};
let has_new_thread_entry = self
@@ -1325,8 +1326,8 @@ impl Sidebar {
.group(&group_name)
.h(Tab::content_height(cx))
.w_full()
- .pl_1p5()
- .pr_1()
+ .pl(px(5.))
+ .pr_1p5()
.border_1()
.map(|this| {
if is_selected {
@@ -1339,16 +1340,21 @@ impl Sidebar {
.hover(|s| s.bg(hover_color))
.child(
h_flex()
+ .when(!is_active, |this| this.cursor_pointer())
.relative()
.min_w_0()
.w_full()
- .gap_1p5()
+ .gap(px(5.))
.child(
- h_flex().size_4().flex_none().justify_center().child(
- Icon::new(disclosure_icon)
- .size(IconSize::Small)
- .color(Color::Custom(cx.theme().colors().icon_muted.opacity(0.5))),
- ),
+ IconButton::new(disclosure_id, disclosure_icon)
+ .shape(ui::IconButtonShape::Square)
+ .icon_size(IconSize::Small)
+ .icon_color(Color::Custom(cx.theme().colors().icon_muted.opacity(0.5)))
+ .tooltip(Tooltip::text(disclosure_tooltip))
+ .on_click(cx.listener(move |this, _, window, cx| {
+ this.selection = None;
+ this.toggle_collapse(&path_list_for_toggle, window, cx);
+ })),
)
.child(label)
.when_some(
@@ -1425,39 +1431,6 @@ impl Sidebar {
})),
)
})
- .when(!is_active, |this| {
- this.child(
- IconButton::new(
- SharedString::from(format!(
- "{id_prefix}project-header-open-workspace-{ix}",
- )),
- IconName::Focus,
- )
- .icon_size(IconSize::Small)
- .icon_color(Color::Muted)
- .tooltip(Tooltip::text("Activate Workspace"))
- .on_click(cx.listener({
- move |this, _, window, cx| {
- this.active_entry =
- Some(ActiveEntry::Draft(workspace_for_open.clone()));
- if let Some(multi_workspace) = this.multi_workspace.upgrade() {
- multi_workspace.update(cx, |multi_workspace, cx| {
- multi_workspace.activate(
- workspace_for_open.clone(),
- window,
- cx,
- );
- });
- }
- if AgentPanel::is_visible(&workspace_for_open, cx) {
- workspace_for_open.update(cx, |workspace, cx| {
- workspace.focus_panel::(window, cx);
- });
- }
- }
- })),
- )
- })
.when(show_new_thread_button, |this| {
this.child(
IconButton::new(
@@ -1483,10 +1456,29 @@ impl Sidebar {
)
})
})
- .on_click(cx.listener(move |this, _, window, cx| {
- this.selection = None;
- this.toggle_collapse(&path_list_for_toggle, window, cx);
- }))
+ .when(!is_active, |this| {
+ this.tooltip(Tooltip::text("Activate Workspace"))
+ .on_click(cx.listener({
+ move |this, _, window, cx| {
+ this.active_entry =
+ Some(ActiveEntry::Draft(workspace_for_open.clone()));
+ if let Some(multi_workspace) = this.multi_workspace.upgrade() {
+ multi_workspace.update(cx, |multi_workspace, cx| {
+ multi_workspace.activate(
+ workspace_for_open.clone(),
+ window,
+ cx,
+ );
+ });
+ }
+ if AgentPanel::is_visible(&workspace_for_open, cx) {
+ workspace_for_open.update(cx, |workspace, cx| {
+ workspace.focus_panel::(window, cx);
+ });
+ }
+ }
+ }))
+ })
.into_any_element()
}
@@ -2779,6 +2771,11 @@ impl Sidebar {
let id = SharedString::from(format!("thread-entry-{}", ix));
+ let color = cx.theme().colors();
+ let sidebar_bg = color
+ .title_bar_background
+ .blend(color.panel_background.opacity(0.32));
+
let timestamp = format_history_entry_timestamp(
self.thread_last_message_sent_or_queued
.get(&thread.metadata.session_id)
@@ -2788,6 +2785,7 @@ impl Sidebar {
);
ThreadItem::new(id, title)
+ .base_bg(sidebar_bg)
.icon(thread.icon)
.status(thread.status)
.when_some(thread.icon_from_external_svg.clone(), |this, svg| {
diff --git a/crates/ui/src/components/ai/thread_item.rs b/crates/ui/src/components/ai/thread_item.rs
index e03dce5fe2..d6b5f56e0a 100644
--- a/crates/ui/src/components/ai/thread_item.rs
+++ b/crates/ui/src/components/ai/thread_item.rs
@@ -218,21 +218,23 @@ impl RenderOnce for ThreadItem {
let color = cx.theme().colors();
let sidebar_base_bg = color
.title_bar_background
- .blend(color.panel_background.opacity(0.2));
+ .blend(color.panel_background.opacity(0.32));
- let base_bg = self.base_bg.unwrap_or(sidebar_base_bg);
+ let raw_bg = self.base_bg.unwrap_or(sidebar_base_bg);
+ let apparent_bg = color.background.blend(raw_bg);
let base_bg = if self.selected {
- color.element_active
+ apparent_bg.blend(color.element_active)
} else {
- base_bg
+ apparent_bg
};
let hover_color = color
.element_active
.blend(color.element_background.opacity(0.2));
+ let hover_bg = apparent_bg.blend(hover_color);
- let gradient_overlay = GradientFade::new(base_bg, hover_color, hover_color)
+ let gradient_overlay = GradientFade::new(base_bg, hover_bg, hover_bg)
.width(px(64.0))
.right(px(-10.0))
.gradient_stop(0.75)
@@ -399,7 +401,7 @@ impl RenderOnce for ThreadItem {
.child(gradient_overlay)
.when(self.hovered, |this| {
this.when_some(self.action_slot, |this, slot| {
- let overlay = GradientFade::new(base_bg, hover_color, hover_color)
+ let overlay = GradientFade::new(base_bg, hover_bg, hover_bg)
.width(px(64.0))
.right(px(6.))
.gradient_stop(0.75)
@@ -432,6 +434,7 @@ impl RenderOnce for ThreadItem {
.collect::>()
.join("\n")
.into();
+
let worktree_tooltip_title = if self.worktrees.len() > 1 {
"Thread Running in Local Git Worktrees"
} else {
diff --git a/crates/ui/src/components/gradient_fade.rs b/crates/ui/src/components/gradient_fade.rs
index 2173fdf06e..8a982695ec 100644
--- a/crates/ui/src/components/gradient_fade.rs
+++ b/crates/ui/src/components/gradient_fade.rs
@@ -49,10 +49,14 @@ impl GradientFade {
}
impl RenderOnce for GradientFade {
- fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
+ fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let stop = self.gradient_stop;
- let hover_bg = self.hover_bg;
- let active_bg = self.active_bg;
+
+ // Best-effort to flatten potentially-transparent colors to opaque ones.
+ let app_bg = cx.theme().colors().background;
+ let base_bg = app_bg.blend(self.base_bg);
+ let hover_bg = app_bg.blend(self.hover_bg);
+ let active_bg = app_bg.blend(self.active_bg);
div()
.id("gradient_fade")
@@ -63,8 +67,8 @@ impl RenderOnce for GradientFade {
.h_full()
.bg(linear_gradient(
90.,
- linear_color_stop(self.base_bg, stop),
- linear_color_stop(self.base_bg.opacity(0.0), 0.),
+ linear_color_stop(base_bg, stop),
+ linear_color_stop(base_bg.opacity(0.0), 0.),
))
.when_some(self.group_name.clone(), |element, group_name| {
element.group_hover(group_name, move |s| {