sidebar: Add some design adjustments (#52832)

Danilo Leal created

- 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

Change summary

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(-)

Detailed changes

assets/icons/focus.svg 🔗

@@ -1,7 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8 9.5C8.82843 9.5 9.5 8.82843 9.5 8C9.5 7.17157 8.82843 6.5 8 6.5C7.17157 6.5 6.5 7.17157 6.5 8C6.5 8.82843 7.17157 9.5 8 9.5Z" fill="#C6CAD0"/>
-<path d="M2.25 4.80555V3.52777C2.25 3.18889 2.38462 2.86388 2.62425 2.62425C2.86388 2.38462 3.18889 2.25 3.52777 2.25H4.80555" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M11.1945 2.25H12.4722C12.8111 2.25 13.1361 2.38462 13.3758 2.62425C13.6154 2.86388 13.75 3.18889 13.75 3.52777V4.80555" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M13.75 11.1945V12.4722C13.75 12.8111 13.6154 13.1361 13.3758 13.3758C13.1361 13.6154 12.8111 13.75 12.4722 13.75H11.1945" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M4.80555 13.75H3.52777C3.18889 13.75 2.86388 13.6154 2.62425 13.3758C2.38462 13.1361 2.25 12.8111 2.25 12.4722V11.1945" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
-</svg>

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::<AgentPanel>(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::<AgentPanel>(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| {

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::<Vec<_>>()
                         .join("\n")
                         .into();
+
                     let worktree_tooltip_title = if self.worktrees.len() > 1 {
                         "Thread Running in Local Git Worktrees"
                     } else {

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| {