From 9c735febc1962a4f185747a49cb9ad53bd18e533 Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Sun, 20 Sep 2026 09:02:57 +0700 Subject: [PATCH] update sidebar --- crates/ui/src/button.rs | 6 --- crates/workspace/src/sidebar/mod.rs | 62 +++++++++++++++------------- crates/workspace/src/sidebar/tab.rs | 56 ++++++++++++++----------- crates/workspace/src/sidebar/tree.rs | 32 ++++++++++---- docs/concord-usage.md | 7 ++-- docs/sidebar-redesign-plan.md | 14 ++++--- 6 files changed, 104 insertions(+), 73 deletions(-) diff --git a/crates/ui/src/button.rs b/crates/ui/src/button.rs index 81d72f09..1add51af 100644 --- a/crates/ui/src/button.rs +++ b/crates/ui/src/button.rs @@ -116,26 +116,20 @@ pub trait ButtonVariants: Sized { #[allow(clippy::type_complexity)] pub struct Button { base: BaseButton, - icon: Option, label: Option, tooltip: Option, children: Vec, - variant: ButtonVariant, size: Size, - disabled: bool, loading: bool, - rounded: bool, compact: bool, caret: bool, indicator: bool, - on_click: Option>, on_hover: Option>, - tab_index: isize, tab_stop: bool, diff --git a/crates/workspace/src/sidebar/mod.rs b/crates/workspace/src/sidebar/mod.rs index 8929b577..b71f683c 100644 --- a/crates/workspace/src/sidebar/mod.rs +++ b/crates/workspace/src/sidebar/mod.rs @@ -255,35 +255,40 @@ fn rows_for(tab: SidebarTab, cx: &App) -> Vec { }]; } - let mut rows = vec![SidebarRow::Section { - label: "Communities".into(), - count: community_count, - }]; + let mut rows = Vec::new(); - rows.extend( - communities - .into_iter() - .map(|community| SidebarRow::Community { community }), - ); - rows.push(SidebarRow::Action { - label: "Show all communities".into(), - tab: SidebarTab::Communities, - }); + if !communities.is_empty() { + rows.push(SidebarRow::Section { + label: "Communities".into(), + count: community_count, + }); + rows.extend( + communities + .into_iter() + .map(|community| SidebarRow::Community { community }), + ); + rows.push(SidebarRow::Action { + label: "Show all communities".into(), + tab: SidebarTab::Communities, + }); + } - rows.push(SidebarRow::Section { - label: "Chats".into(), - count: rooms.len(), - }); - rows.extend( - rooms - .into_iter() - .take(5) - .map(|room| SidebarRow::Room { room }), - ); - rows.push(SidebarRow::Action { - label: "Show all chats".into(), - tab: SidebarTab::Chats, - }); + if !rooms.is_empty() { + rows.push(SidebarRow::Section { + label: "Chats".into(), + count: rooms.len(), + }); + rows.extend( + rooms + .into_iter() + .take(5) + .map(|room| SidebarRow::Room { room }), + ); + rows.push(SidebarRow::Action { + label: "Show all chats".into(), + tab: SidebarTab::Chats, + }); + } rows } @@ -400,6 +405,7 @@ fn render_rows(range: Range, rows: &[SidebarRow], cx: &Context) TreeRowKind::Action, label.clone(), ) + .icon(IconName::ArrowRight) .on_click(cx.listener(move |this, _event, _window, cx| { this.select_tab(tab, cx); })) @@ -590,7 +596,7 @@ impl Render for Sidebar { this.child( div() .absolute() - .bottom_12() + .bottom_16() .left_0() .h_9() .w_full() diff --git a/crates/workspace/src/sidebar/tab.rs b/crates/workspace/src/sidebar/tab.rs index 72f7502a..eda0d77b 100644 --- a/crates/workspace/src/sidebar/tab.rs +++ b/crates/workspace/src/sidebar/tab.rs @@ -1,7 +1,7 @@ use std::rc::Rc; use gpui::prelude::FluentBuilder; -use gpui::{App, IntoElement, ParentElement, RenderOnce, Styled, Window, div}; +use gpui::{App, InteractiveElement, IntoElement, ParentElement, RenderOnce, Styled, Window, div}; use theme::ActiveTheme; use ui::button::{Button, ButtonVariants}; use ui::{IconName, Selectable, h_flex}; @@ -85,29 +85,37 @@ impl RenderOnce for TabBar { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { let Self { active, on_select } = self; - div().absolute().bottom_2().left_0().w_full().px_2().child( - h_flex() - .w_full() - .p_1() - .gap_1() - .rounded(cx.theme().radius_lg) - .bg(cx.theme().elevated_surface_background) - .when(cx.theme().shadow, |this| this.shadow_md()) - .children(SidebarTab::ALL.into_iter().map(|tab| { - let on_select = on_select.clone(); + div() + .id("sidebar-tabs") + .absolute() + .bottom_3() + .left_0() + .w_full() + .px_4() + .child( + h_flex() + .w_full() + .p_1() + .gap_1() + .rounded_full() + .bg(cx.theme().background) + .when(cx.theme().shadow, |this| this.shadow_md()) + .children(SidebarTab::ALL.into_iter().map(|tab| { + let on_select = on_select.clone(); - Button::new(format!("tab-{}", tab.list_id())) - .icon(tab.icon()) - .ghost() - .flex_1() - .selected(tab == active) - .tooltip(tab.label()) - .on_click(move |_event, window, cx| { - if let Some(on_select) = on_select.as_ref() { - on_select(tab, window, cx); - } - }) - })), - ) + Button::new(format!("tab-{}", tab.list_id())) + .icon(tab.icon()) + .ghost() + .flex_1() + .rounded() + .selected(tab == active) + .tooltip(tab.label()) + .on_click(move |_event, window, cx| { + if let Some(on_select) = on_select.as_ref() { + on_select(tab, window, cx); + } + }) + })), + ) } } diff --git a/crates/workspace/src/sidebar/tree.rs b/crates/workspace/src/sidebar/tree.rs index a161512d..20323851 100644 --- a/crates/workspace/src/sidebar/tree.rs +++ b/crates/workspace/src/sidebar/tree.rs @@ -5,7 +5,7 @@ use community::Community; use gpui::prelude::FluentBuilder; use gpui::{ App, ClickEvent, ElementId, Entity, ImageSource, InteractiveElement, IntoElement, - ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window, div, + ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window, div, px, }; use settings::AppSettings; use theme::ActiveTheme; @@ -50,6 +50,7 @@ pub struct TreeRow { label: SharedString, avatar: Option, picture: Option, + icon: Option, count: Option, created_at: Option, selected: bool, @@ -69,6 +70,7 @@ impl TreeRow { label: label.into(), avatar: None, picture: None, + icon: None, count: None, created_at: None, selected: false, @@ -88,6 +90,12 @@ impl TreeRow { self } + /// Shows `icon` in the avatar slot when the row has no avatar or picture. + pub fn icon(mut self, icon: IconName) -> Self { + self.icon = Some(icon); + self + } + pub fn count(mut self, count: usize) -> Self { self.count = Some(count); self @@ -150,6 +158,18 @@ impl RenderOnce for TreeRow { } }; + let avatar = avatar.or_else(|| { + self.icon.map(|icon| { + h_flex() + .flex_shrink_0() + .w(px(20.)) + .justify_center() + .text_color(cx.theme().icon_muted) + .child(Icon::new(icon).small()) + .into_any_element() + }) + }); + h_flex() .id(self.id) .h_8() @@ -159,14 +179,12 @@ impl RenderOnce for TreeRow { .rounded(cx.theme().radius) .when(is_section, |this| { this.text_xs() - .text_color(cx.theme().text_muted) + .text_color(cx.theme().text_placeholder) .font_semibold() }) .when(is_room || is_community, |this| this.text_sm()) .when(is_action, |this| { - this.text_sm() - .font_medium() - .text_color(cx.theme().text_accent) + this.text_sm().text_color(cx.theme().text_muted) }) .when(is_hint, |this| { this.text_xs() @@ -197,11 +215,11 @@ impl RenderOnce for TreeRow { this.child(div().flex_shrink_0().font_normal().child(count.to_string())) }) .when_some(self.created_at, |this, created_at| { - this.child( + this.child(div().flex_1()).child( div() .flex_shrink_0() - .text_xs() .text_color(cx.theme().text_placeholder) + .text_xs() .child(created_at), ) }), diff --git a/docs/concord-usage.md b/docs/concord-usage.md index 5fecfbd8..ce285343 100644 --- a/docs/concord-usage.md +++ b/docs/concord-usage.md @@ -602,9 +602,10 @@ client.subscribe(filter).with_id(sub_id).await?; `crates/community`.** `concord` has no subscriptions, no `init`, and no `Entity`; `community::CommunityRegistry` owns one `Entity` per state document, subscribes when a community's plane set changes, and - re-folds on an inbound wrap. The sidebar observes the registry, logs - `CommunityEvent::Error` through `log::error!`, and its "New community" row opens - a name prompt that calls `CommunityRegistry::create`. `create` persists the + re-folds on an inbound wrap. The sidebar subscribes to the registry, surfaces + `CommunityEvent::Error` as a window notification, and its "New community" row + in the Communities tab dispatches `Command::NewCommunity`, whose name prompt + calls `CommunityRegistry::create`. `create` persists the genesis locally, publishes the wraps to the community's relays, and records the membership in the account's Community List — all best-effort, so a relay that is down warns without losing the community. Discovery diff --git a/docs/sidebar-redesign-plan.md b/docs/sidebar-redesign-plan.md index 795127f1..1087ca9b 100644 --- a/docs/sidebar-redesign-plan.md +++ b/docs/sidebar-redesign-plan.md @@ -205,7 +205,7 @@ Empty list shows the existing "No communities yet" hint. Each phase is independently reviewable and leaves the app runnable. -### Phase 1 — tab shell +### Phase 1 — tab shell — DONE Files: `crates/workspace/src/sidebar/mod.rs`, `crates/workspace/src/sidebar/tab.rs` (new), `sidebar/tree.rs`, @@ -228,7 +228,7 @@ Files: `crates/workspace/src/sidebar/mod.rs`, Validation: app runs signed in and signed out; chats and communities list and open as before; tab switching works; requests dot still clears. -### Phase 2 — Recents tab +### Phase 2 — Recents tab — DONE Files: `crates/settings/src/lib.rs`, `crates/workspace/src/sidebar/mod.rs`, `sidebar/tree.rs`. @@ -242,7 +242,7 @@ Files: `crates/settings/src/lib.rs`, `crates/workspace/src/sidebar/mod.rs`, Validation: `cargo test -p settings`; manually open communities, restart, and confirm the Recents order; confirm ≤3 / ≤5 rendering and both "Show all" rows. -### Phase 3 — tab actions +### Phase 3 — tab actions — DONE Files: `crates/workspace/src/dialogs/new_chat.rs` (new), `dialogs/new_community.rs` (new), `crates/workspace/src/dialogs/mod.rs`, @@ -264,7 +264,7 @@ Validation: create a chat from an npub and confirm the room opens; create a community and confirm it appears in the Communities tab and in Recents; requests/contacts/browse still dispatch. -### Phase 4 — onboarding sidebar +### Phase 4 — onboarding sidebar — DONE, except `Join now` Files: `crates/state/src/lib.rs`, `crates/workspace/src/sidebar/mod.rs`, `sidebar/onboarding.rs` (new), `crates/workspace/src/dialogs/create_identity.rs` @@ -291,7 +291,11 @@ Validation: with no stored credentials the sidebar shows onboarding and no modal; `Import identity` still signs in; `Join now` signs in with a fresh key; with bunker credentials the tabs appear without an onboarding flash. -### Phase 5 — polish and cleanup +**Deferred.** `dialogs/create_identity.rs` is not implemented, so `Join now` +renders without a click handler, and the §2.1 banner assets were skipped in +favor of a plain theme-colored background with the brand mark. + +### Phase 5 — polish and cleanup — DONE - Reposition the "Getting messages…" pill above the tab bar. - Empty states and counts for all three tabs; truncation rules (§3).