app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit 2b8e983acc9d9dab65c1eeb8e251474a1433e392
parent 0dc179d8d4c8c218dc94bb64c5eb9a09c2052f85
Author: triesap <tyson@radroots.org>
Date:   Sun,  7 Jun 2026 18:53:31 -0700

ui: add sidebar account menu

Diffstat:
Mcrates/desktop/src/source_guards.rs | 1+
Mcrates/desktop/src/window.rs | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Mcrates/ui/src/lib.rs | 17+++++++++--------
Mcrates/ui/src/primitives.rs | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 168 insertions(+), 14 deletions(-)

diff --git a/crates/desktop/src/source_guards.rs b/crates/desktop/src/source_guards.rs @@ -171,6 +171,7 @@ const ALLOWED_WINDOW_LITERALS: &[&str] = &[ "home-farm-setup-shipping", "home-farm-setup-start", "home-generate-key", + "home-sidebar-account-menu", "home-nav-orders", "home-nav-pack-day", "home-nav-products", diff --git a/crates/desktop/src/window.rs b/crates/desktop/src/window.rs @@ -41,7 +41,7 @@ use radroots_app_ui::{ app_button_primary_disabled as action_button_primary_disabled, app_button_primary_full_width as action_button_primary_full_width, app_button_secondary as action_button, app_button_secondary_disabled as action_button_disabled, - app_button_secondary_full_width as action_button_full_width, + app_button_secondary_full_width as action_button_full_width, app_button_sidebar_account_menu, app_button_square_dropdown_secondary as action_dropdown_button, app_button_text as text_button, app_checkbox_button as action_checkbox_button, app_checkbox_field, app_cluster, app_detail_row, app_divider as section_divider, app_focused_detail_view, app_focused_task_view, app_form_field, @@ -1972,6 +1972,22 @@ impl HomeView { } } + fn open_account_tab(&mut self, tab: AccountTab, cx: &mut Context<Self>) { + let route_changed = self.runtime.select_account(); + if route_changed { + self.products_stock_editor = None; + self.product_editor_form = None; + self.clear_focused_view(); + } + + let tab_changed = self.selected_account_tab != tab; + self.selected_account_tab = tab; + + if route_changed || tab_changed { + cx.notify(); + } + } + fn select_account_tab(&mut self, tab: AccountTab, cx: &mut Context<Self>) { if self.selected_account_tab != tab { self.selected_account_tab = tab; @@ -4193,6 +4209,10 @@ impl HomeView { }), cx.listener(|this, _, _, cx| this.open_orders(cx)), cx.listener(|this, _, _, cx| this.open_pack_day(None, cx)), + cx.listener(|this, _, _, cx| this.open_account_tab(AccountTab::Profile, cx)), + cx.listener(|this, _, _, cx| this.open_account_tab(AccountTab::FarmDetails, cx)), + cx.listener(|this, _, _, cx| this.open_account_tab(AccountTab::Preferences, cx)), + cx.listener(|this, _, _, cx| this.open_account_tab(AccountTab::Settings, cx)), cx, ) .into_any_element(), @@ -5526,6 +5546,10 @@ impl HomeView { }), cx.listener(|this, _, _, cx| this.open_orders(cx)), cx.listener(|this, _, _, cx| this.open_pack_day(None, cx)), + cx.listener(|this, _, _, cx| this.open_account_tab(AccountTab::Profile, cx)), + cx.listener(|this, _, _, cx| this.open_account_tab(AccountTab::FarmDetails, cx)), + cx.listener(|this, _, _, cx| this.open_account_tab(AccountTab::Preferences, cx)), + cx.listener(|this, _, _, cx| this.open_account_tab(AccountTab::Settings, cx)), cx, ) .into_any_element() @@ -13127,6 +13151,10 @@ fn home_sidebar( on_select_products: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, on_select_orders: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, on_select_pack_day: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, + on_select_account_profile: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, + on_select_account_farm_details: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, + on_select_account_preferences: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, + on_select_account_settings: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, cx: &App, ) -> impl IntoElement { let selected_section = selected_farmer_section(runtime); @@ -13138,6 +13166,10 @@ fn home_sidebar( let on_select_products = Arc::new(on_select_products); let on_select_orders = Arc::new(on_select_orders); let on_select_pack_day = Arc::new(on_select_pack_day); + let on_select_account_profile = Arc::new(on_select_account_profile); + let on_select_account_farm_details = Arc::new(on_select_account_farm_details); + let on_select_account_preferences = Arc::new(on_select_account_preferences); + let on_select_account_settings = Arc::new(on_select_account_settings); let mut navigation_elements = Vec::with_capacity(navigation_sections.len()); for section in navigation_sections { let element = match section { @@ -13211,11 +13243,60 @@ fn home_sidebar( .gap(px(APP_UI_THEME.shells.home_stack_gap_px)) .children(navigation_elements), ) - .child( - div().child(div().when_some(home_saved_farm(runtime), |this, farm| { - this.child(home_body_text(farm.display_name.clone())) - })), - ), + .when_some(home_saved_farm(runtime), |this, farm| { + this.child(home_sidebar_account_menu( + farm.display_name.clone(), + Arc::clone(&on_select_account_profile), + Arc::clone(&on_select_account_farm_details), + Arc::clone(&on_select_account_preferences), + Arc::clone(&on_select_account_settings), + cx, + )) + }), + ) +} + +fn home_sidebar_account_menu<ProfileAction, FarmDetailsAction, PreferencesAction, SettingsAction>( + label: impl Into<SharedString>, + on_select_profile: Arc<ProfileAction>, + on_select_farm_details: Arc<FarmDetailsAction>, + on_select_preferences: Arc<PreferencesAction>, + on_select_settings: Arc<SettingsAction>, + cx: &App, +) -> impl IntoElement +where + ProfileAction: Fn(&ClickEvent, &mut Window, &mut App) + 'static, + FarmDetailsAction: Fn(&ClickEvent, &mut Window, &mut App) + 'static, + PreferencesAction: Fn(&ClickEvent, &mut Window, &mut App) + 'static, + SettingsAction: Fn(&ClickEvent, &mut Window, &mut App) + 'static, +{ + app_button_sidebar_account_menu( + "home-sidebar-account-menu", + label, + move |menu, _, _| { + let on_select_profile = Arc::clone(&on_select_profile); + let on_select_farm_details = Arc::clone(&on_select_farm_details); + let on_select_preferences = Arc::clone(&on_select_preferences); + let on_select_settings = Arc::clone(&on_select_settings); + + menu.item( + PopupMenuItem::new(app_text(AccountTab::Profile.text_key())) + .on_click(move |event, window, app| on_select_profile(event, window, app)), + ) + .item( + PopupMenuItem::new(app_text(AccountTab::FarmDetails.text_key())) + .on_click(move |event, window, app| on_select_farm_details(event, window, app)), + ) + .item( + PopupMenuItem::new(app_text(AccountTab::Preferences.text_key())) + .on_click(move |event, window, app| on_select_preferences(event, window, app)), + ) + .item( + PopupMenuItem::new(app_text(AccountTab::Settings.text_key())) + .on_click(move |event, window, app| on_select_settings(event, window, app)), + ) + }, + cx, ) } diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs @@ -11,14 +11,15 @@ pub use primitives::{ app_button_icon, app_button_list_row, app_button_primary, app_button_primary_compact, app_button_primary_compact_disabled, app_button_primary_disabled, app_button_primary_full_width, app_button_secondary, app_button_secondary_disabled, - app_button_secondary_full_width, app_button_square_dropdown_secondary, app_button_text, - app_checkbox_button, app_checkbox_field, app_cluster, app_detail_row, app_divider, - app_focused_detail_view, app_focused_task_view, app_form_field, app_form_input_text, - app_form_section, app_heading_section, app_heading_view, app_input_text, app_pill_tabs, - app_scroll_panel, app_segment_button_icon, app_split_shell, app_stack_h, app_stack_v, - app_status_indicator, app_surface_card, app_surface_card_section, app_surface_panel, - app_surface_sidebar, app_surface_window, app_text_badge, app_text_body, app_text_body_subtle, - app_text_label, app_text_value, app_underline_tabs, label_value_list, utility_title_row, + app_button_secondary_full_width, app_button_sidebar_account_menu, + app_button_square_dropdown_secondary, app_button_text, app_checkbox_button, app_checkbox_field, + app_cluster, app_detail_row, app_divider, app_focused_detail_view, app_focused_task_view, + app_form_field, app_form_input_text, app_form_section, app_heading_section, app_heading_view, + app_input_text, app_pill_tabs, app_scroll_panel, app_segment_button_icon, app_split_shell, + app_stack_h, app_stack_v, app_status_indicator, app_surface_card, app_surface_card_section, + app_surface_panel, app_surface_sidebar, app_surface_window, app_text_badge, app_text_body, + app_text_body_subtle, app_text_label, app_text_value, app_underline_tabs, label_value_list, + utility_title_row, }; pub use text::{ SettingsPreferencesGeneralRowState, app_shared_label_text, app_shared_text, diff --git a/crates/ui/src/primitives.rs b/crates/ui/src/primitives.rs @@ -1012,6 +1012,77 @@ pub fn app_button_ellipsis_menu( .dropdown_menu_with_anchor(Corner::BottomRight, menu) } +pub fn app_button_sidebar_account_menu( + id: &'static str, + label: impl Into<SharedString>, + menu: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static, + cx: &App, +) -> impl IntoElement { + let label = label.into(); + let sizing = APP_UI_THEME.components.app_button.sizing; + let row = APP_UI_THEME.components.app_account_selector_row; + + Button::new(id) + .custom( + ButtonCustomVariant::new(cx) + .color(rgb(row.inactive_background).into()) + .foreground(rgb(APP_UI_THEME.foundation.text.secondary).into()) + .border(transparent_black()) + .hover(rgb(row.active_background).into()) + .active(rgb(row.active_background).into()), + ) + .w_full() + .h(px(APP_UI_THEME + .shells + .settings_account_sidebar_button_height_px)) + .rounded(ButtonRounded::Size(px(APP_UI_THEME + .shells + .settings_account_sidebar_button_corner_radius_px))) + .tab_stop(false) + .child( + div() + .size_full() + .px(px(APP_UI_THEME + .shells + .settings_account_sidebar_footer_button_gap_px)) + .flex() + .items_center() + .justify_between() + .gap(px(APP_UI_THEME + .shells + .settings_account_sidebar_button_gap_px)) + .child( + div() + .flex() + .items_center() + .min_w_0() + .gap(px(APP_UI_THEME + .shells + .settings_account_sidebar_button_gap_px)) + .child( + Icon::new(IconName::CircleUser) + .with_size(Size::Size(px(sizing.icon_size_px))) + .text_color(rgb(APP_UI_THEME.foundation.text.secondary)), + ) + .child( + div() + .min_w_0() + .truncate() + .text_size(px(APP_UI_THEME.foundation.typography.body_text_px)) + .font_weight(gpui::FontWeight::MEDIUM) + .text_color(rgb(APP_UI_THEME.foundation.text.secondary)) + .child(label), + ), + ) + .child( + Icon::new(IconName::ChevronsUpDown) + .with_size(Size::Size(px(sizing.icon_size_px))) + .text_color(rgb(APP_UI_THEME.foundation.text.secondary)), + ), + ) + .dropdown_menu_with_anchor(Corner::TopLeft, menu) +} + fn app_button_label( button: Button, label: SharedString,