app

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

commit 1b659a51242b36a7d95c2014b5d07587388b1802
parent 40be6db8ff06ba14046e076c88b51c706388998e
Author: triesap <tyson@radroots.org>
Date:   Sun,  7 Jun 2026 13:04:10 -0700

ui: make account profile form interactive

Diffstat:
Mcrates/desktop/src/source_guards.rs | 6++++++
Mcrates/desktop/src/window.rs | 436+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Mcrates/i18n/src/keys.rs | 6++++++
Mcrates/ui/src/lib.rs | 18+++++++++---------
Mcrates/ui/src/primitives.rs | 40+++++++++++++++++++++++++++++++++++++++-
Mi18n/locales/en/messages.json | 6++++++
6 files changed, 369 insertions(+), 143 deletions(-)

diff --git a/crates/desktop/src/source_guards.rs b/crates/desktop/src/source_guards.rs @@ -376,8 +376,14 @@ const REQUIRED_WINDOW_COPY_KEYS: &[&str] = &[ "AppTextKey::AccountProfileEmailValue", "AppTextKey::AccountProfilePhoneValue", "AppTextKey::AccountProfileRoleValue", + "AppTextKey::AccountProfileRoleFarmManagerValue", + "AppTextKey::AccountProfileRoleTeamMemberValue", "AppTextKey::AccountProfileTimeZoneValue", + "AppTextKey::AccountProfileTimeZoneMountainValue", + "AppTextKey::AccountProfileTimeZoneEasternValue", "AppTextKey::AccountProfileLanguageValue", + "AppTextKey::AccountProfileLanguageFrenchValue", + "AppTextKey::AccountProfileLanguageSpanishValue", "AppTextKey::HomeSetupBackAction", "AppTextKey::HomeSetupBrowseMarketplaceAction", "AppTextKey::HomeSetupConnectSignerAction", diff --git a/crates/desktop/src/window.rs b/crates/desktop/src/window.rs @@ -5,7 +5,11 @@ use gpui::{ div, img, prelude::FluentBuilder, px, relative, rgb, size, transparent_black, }; use gpui_component::{ - Icon, IconName, Root, Sizable, input::InputEvent, input::InputState, menu::PopupMenuItem, + Icon, IconName, IndexPath, Root, Sizable, Size, + input::InputEvent, + input::InputState, + menu::PopupMenuItem, + select::{SearchableVec, Select, SelectState}, }; use radroots_app_i18n::{AppTextKey, app_text}; use radroots_app_remote_signer::{ @@ -33,7 +37,9 @@ use radroots_app_ui::{ app_button_compact as action_button_compact, app_button_list_row as list_row_button, app_button_primary as action_button_primary, 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_square_dropdown_secondary as action_dropdown_button, app_button_text as text_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, app_form_input_text, @@ -210,6 +216,95 @@ impl AccountTab { } } +type AccountProfileSelectState = SelectState<SearchableVec<SharedString>>; + +#[derive(Clone)] +struct AccountProfileFormState { + full_name_input: Entity<InputState>, + email_input: Entity<InputState>, + phone_input: Entity<InputState>, + role_select: Entity<AccountProfileSelectState>, + time_zone_select: Entity<AccountProfileSelectState>, + language_select: Entity<AccountProfileSelectState>, +} + +impl AccountProfileFormState { + fn new(window: &mut Window, cx: &mut Context<HomeView>) -> Self { + Self { + full_name_input: account_profile_input_state( + AppTextKey::AccountProfileFullNameValue, + window, + cx, + ), + email_input: account_profile_input_state( + AppTextKey::AccountProfileEmailValue, + window, + cx, + ), + phone_input: account_profile_input_state( + AppTextKey::AccountProfilePhoneValue, + window, + cx, + ), + role_select: account_profile_select_state( + &[ + AppTextKey::AccountProfileRoleValue, + AppTextKey::AccountProfileRoleFarmManagerValue, + AppTextKey::AccountProfileRoleTeamMemberValue, + ], + window, + cx, + ), + time_zone_select: account_profile_select_state( + &[ + AppTextKey::AccountProfileTimeZoneValue, + AppTextKey::AccountProfileTimeZoneMountainValue, + AppTextKey::AccountProfileTimeZoneEasternValue, + ], + window, + cx, + ), + language_select: account_profile_select_state( + &[ + AppTextKey::AccountProfileLanguageValue, + AppTextKey::AccountProfileLanguageFrenchValue, + AppTextKey::AccountProfileLanguageSpanishValue, + ], + window, + cx, + ), + } + } +} + +fn account_profile_input_state( + value_key: AppTextKey, + window: &mut Window, + cx: &mut Context<HomeView>, +) -> Entity<InputState> { + cx.new(|cx| InputState::new(window, cx).default_value(app_text(value_key))) +} + +fn account_profile_select_state( + value_keys: &[AppTextKey], + window: &mut Window, + cx: &mut Context<HomeView>, +) -> Entity<AccountProfileSelectState> { + let values = value_keys + .iter() + .copied() + .map(app_shared_text) + .collect::<Vec<_>>(); + cx.new(|cx| { + SelectState::new( + SearchableVec::new(values), + Some(IndexPath::default().row(0)), + window, + cx, + ) + }) +} + fn buyer_order_detail_focus_after_open( runtime_changed: bool, runtime: &DesktopAppRuntimeSummary, @@ -337,6 +432,7 @@ pub struct HomeView { product_editor_form: Option<ProductEditorFormState>, focused_view: Option<HomeFocusedView>, selected_account_tab: AccountTab, + account_profile_form: Option<AccountProfileFormState>, relay_client: Option<RadrootsNostrClient>, buyer_workspace_notice: Option<String>, } @@ -457,6 +553,7 @@ impl HomeView { product_editor_form: None, focused_view: None, selected_account_tab: AccountTab::default(), + account_profile_form: None, relay_client: None, buyer_workspace_notice: None, } @@ -4930,7 +5027,9 @@ impl Render for HomeView { cx, ) .into_any_element(), - HomeStage::AccountWorkspace => self.render_account_workspace(&runtime_summary, cx), + HomeStage::AccountWorkspace => { + self.render_account_workspace(&runtime_summary, window, cx) + } HomeStage::BuyerWorkspace => self.render_buyer_workspace(&runtime_summary, cx), HomeStage::FarmerWorkspace => self.render_farmer_workspace(&runtime_summary, cx), } @@ -4941,6 +5040,7 @@ impl HomeView { fn render_account_workspace( &mut self, runtime: &DesktopAppRuntimeSummary, + window: &mut Window, cx: &mut Context<Self>, ) -> AnyElement { let sidebar = if runtime.shell_projection.active_surface == ActiveSurface::Farmer { @@ -4990,18 +5090,31 @@ impl HomeView { "account-scroll", 0.0, None, - self.render_account_content(cx), + self.render_account_content(window, cx), )) .into_any_element(), ) .into_any_element() } - fn render_account_content(&mut self, cx: &mut Context<Self>) -> AnyElement { + fn render_account_content( + &mut self, + window: &mut Window, + cx: &mut Context<Self>, + ) -> AnyElement { let selected_tab = self.selected_account_tab; let tabs = AccountTab::ORDERED .into_iter() .map(|tab| AppUnderlineTabSpec::new(app_shared_text(tab.text_key()))); + let panel = match selected_tab { + AccountTab::Profile => { + let form = self.account_profile_form(window, cx).clone(); + account_profile_panel(&form, cx).into_any_element() + } + AccountTab::FarmDetails | AccountTab::Preferences | AccountTab::Security => { + account_placeholder_panel(selected_tab.panel_text_key()).into_any_element() + } + }; app_stack_v(APP_UI_THEME.shells.home_stack_gap_px) .w_full() @@ -5019,10 +5132,25 @@ impl HomeView { this.select_account_tab(AccountTab::from_index(*index), cx) }), )) - .child(account_panel(selected_tab, cx)), + .child(panel), ) .into_any_element() } + + fn account_profile_form( + &mut self, + window: &mut Window, + cx: &mut Context<Self>, + ) -> &AccountProfileFormState { + if self.account_profile_form.is_none() { + self.account_profile_form = Some(AccountProfileFormState::new(window, cx)); + } + + let Some(form) = self.account_profile_form.as_ref() else { + unreachable!(); + }; + form + } } #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -8653,8 +8781,10 @@ fn shared_shell_header( cx: &App, ) -> impl IntoElement { let can_enter_farmer_workspace = runtime.personal_projection.entry.can_enter_farmer_workspace; - let is_marketplace_active = - runtime.shell_projection.active_surface != radroots_app_view::ActiveSurface::Farmer; + let active_mode = shell_header_active_mode(runtime); + let is_account_active = active_mode == ShellHeaderActiveMode::Account; + let is_farm_active = active_mode == ShellHeaderActiveMode::Farm; + let is_marketplace_active = active_mode == ShellHeaderActiveMode::Marketplace; let farm_name = home_saved_farm(runtime).map(|farm| farm.display_name.clone()); let account_label = shell_account_label(runtime); @@ -8695,7 +8825,7 @@ fn shared_shell_header( shared_shell_mode_button( "shell-mode-farm", AppTextKey::HomeHeaderFarmMode, - !is_marketplace_active, + is_farm_active, on_select_farm, cx, ) @@ -8705,6 +8835,7 @@ fn shared_shell_header( .child(shell_account_entry( runtime, account_label, + is_account_active, on_open_account, cx, )), @@ -8712,6 +8843,26 @@ fn shared_shell_header( ) } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum ShellHeaderActiveMode { + Marketplace, + Farm, + Account, +} + +fn shell_header_active_mode(runtime: &DesktopAppRuntimeSummary) -> ShellHeaderActiveMode { + if matches!( + runtime.shell_projection.selected_section, + ShellSection::Account + ) { + ShellHeaderActiveMode::Account + } else if runtime.shell_projection.active_surface == ActiveSurface::Farmer { + ShellHeaderActiveMode::Farm + } else { + ShellHeaderActiveMode::Marketplace + } +} + fn shared_shell_mode_button( id: &'static str, key: AppTextKey, @@ -8725,20 +8876,28 @@ fn shared_shell_mode_button( fn shell_account_entry( runtime: &DesktopAppRuntimeSummary, account_label: String, + is_active: bool, on_open_account: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, cx: &App, ) -> AnyElement { if runtime.personal_projection.entry.state == PersonalEntryState::Guest { - action_button_compact( + choice_button( "shell-account-entry", app_shared_text(AppTextKey::HomeHeaderAccountSetupAction), + is_active, on_open_account, cx, ) .into_any_element() } else { - action_button_compact("shell-account-entry", account_label, on_open_account, cx) - .into_any_element() + choice_button( + "shell-account-entry", + account_label, + is_active, + on_open_account, + cx, + ) + .into_any_element() } } @@ -8784,15 +8943,6 @@ fn buyer_workspace_title_block(title_key: AppTextKey, body_key: AppTextKey) -> i ) } -fn account_panel(tab: AccountTab, cx: &mut Context<HomeView>) -> AnyElement { - match tab { - AccountTab::Profile => account_profile_panel(cx).into_any_element(), - AccountTab::FarmDetails | AccountTab::Preferences | AccountTab::Security => { - account_placeholder_panel(tab.panel_text_key()).into_any_element() - } - } -} - fn account_placeholder_panel(text_key: AppTextKey) -> impl IntoElement { div() .w_full() @@ -8805,7 +8955,10 @@ fn account_placeholder_panel(text_key: AppTextKey) -> impl IntoElement { .child(app_shared_text(text_key)) } -fn account_profile_panel(cx: &mut Context<HomeView>) -> impl IntoElement { +fn account_profile_panel( + form: &AccountProfileFormState, + cx: &mut Context<HomeView>, +) -> impl IntoElement { app_stack_v(APP_UI_THEME.shells.home_stack_gap_px) .w_full() .child( @@ -8818,10 +8971,13 @@ fn account_profile_panel(cx: &mut Context<HomeView>) -> impl IntoElement { AppTextKey::AccountProfilePersonalDetailsTitle, )), ) - .child(account_profile_details_card(cx)) + .child(account_profile_details_card(form, cx)) } -fn account_profile_details_card(cx: &mut Context<HomeView>) -> impl IntoElement { +fn account_profile_details_card( + form: &AccountProfileFormState, + cx: &mut Context<HomeView>, +) -> impl IntoElement { div() .w_full() .border_1() @@ -8837,85 +8993,79 @@ fn account_profile_details_card(cx: &mut Context<HomeView>) -> impl IntoElement .gap(px(APP_UI_THEME.shells.home_card_padding_px)) .child(account_profile_photo_actions(cx)) .child( - app_stack_v(APP_UI_THEME.shells.home_stack_gap_px) - .flex_1() - .min_w_0() - .child(account_profile_field_row( - account_profile_field( - AppTextKey::AccountProfileFullNameLabel, - AppTextKey::AccountProfileFullNameValue, - false, - ), - account_profile_field( - AppTextKey::AccountProfileEmailLabel, - AppTextKey::AccountProfileEmailValue, - false, - ), + account_profile_field_column() + .child(account_profile_input_field( + AppTextKey::AccountProfileFullNameLabel, + &form.full_name_input, )) - .child(account_profile_field_row( - account_profile_field( - AppTextKey::AccountProfilePhoneLabel, - AppTextKey::AccountProfilePhoneValue, - false, - ), - account_profile_field( - AppTextKey::AccountProfileRoleLabel, - AppTextKey::AccountProfileRoleValue, - true, - ), + .child(account_profile_input_field( + AppTextKey::AccountProfilePhoneLabel, + &form.phone_input, )) - .child(account_profile_field_row( - account_profile_field( - AppTextKey::AccountProfileTimeZoneLabel, - AppTextKey::AccountProfileTimeZoneValue, - true, - ), - account_profile_field( - AppTextKey::AccountProfileLanguageLabel, - AppTextKey::AccountProfileLanguageValue, - true, - ), + .child(account_profile_select_field( + AppTextKey::AccountProfileTimeZoneLabel, + &form.time_zone_select, + )), + ) + .child( + account_profile_field_column() + .child(account_profile_input_field( + AppTextKey::AccountProfileEmailLabel, + &form.email_input, + )) + .child(account_profile_select_field( + AppTextKey::AccountProfileRoleLabel, + &form.role_select, + )) + .child(account_profile_select_field( + AppTextKey::AccountProfileLanguageLabel, + &form.language_select, )), ), ) } fn account_profile_photo_actions(cx: &mut Context<HomeView>) -> impl IntoElement { - app_stack_v(8.0) - .w(px(190.0)) - .min_w(px(190.0)) + app_stack_v(10.0) + .flex_none() + .flex_basis(relative(0.4)) + .min_w(px(176.0)) .child( div() + .w_full() .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px)) .font_weight(gpui::FontWeight::SEMIBOLD) .text_color(rgb(APP_UI_THEME.foundation.text.secondary)) .child(app_shared_text(AppTextKey::AccountProfilePictureLabel)), ) .child( - div() - .size(px(72.0)) - .rounded(px(36.0)) - .border_1() - .border_color(rgb(APP_UI_THEME.foundation.surfaces.divider)) - .bg(rgb(APP_UI_THEME.foundation.surfaces.card_background)) - .flex() - .items_center() - .justify_center() - .child( - Icon::new(IconName::CircleUser) - .with_size(gpui_component::Size::Size(px(34.0))) - .text_color(rgb(APP_UI_THEME.foundation.text.secondary)), - ), + div().w_full().flex().justify_center().child( + div() + .size(px(72.0)) + .rounded(px(36.0)) + .border_1() + .border_color(rgb(APP_UI_THEME.foundation.surfaces.divider)) + .bg(rgb(APP_UI_THEME.foundation.surfaces.card_background)) + .flex() + .items_center() + .justify_center() + .child( + Icon::new(IconName::CircleUser) + .with_size(gpui_component::Size::Size(px(34.0))) + .text_color(rgb(APP_UI_THEME.foundation.text.secondary)), + ), + ), ) .child( - app_stack_h(8.0) - .child(action_button( + app_stack_v(8.0) + .w_full() + .child(action_button_primary_full_width( "account-profile-change-photo", app_shared_text(AppTextKey::AccountProfileChangePhotoAction), |_, _, _| {}, cx, )) - .child(text_button( + .child(action_button_full_width( "account-profile-remove-photo", app_shared_text(AppTextKey::AccountProfileRemovePhotoAction), |_, _, _| {}, @@ -8924,23 +9074,33 @@ fn account_profile_photo_actions(cx: &mut Context<HomeView>) -> impl IntoElement ) } -fn account_profile_field_row( - first: impl IntoElement, - second: impl IntoElement, -) -> impl IntoElement { - div() +fn account_profile_field_column() -> gpui::Div { + app_stack_v(APP_UI_THEME.shells.home_stack_gap_px) .w_full() - .flex() - .items_start() - .gap(px(APP_UI_THEME.shells.home_stack_gap_px)) - .child(div().flex_1().min_w_0().child(first)) - .child(div().flex_1().min_w_0().child(second)) + .flex_1() + .min_w_0() } -fn account_profile_field( +fn account_profile_input_field( label_key: AppTextKey, - value_key: AppTextKey, - selectable: bool, + input: &Entity<InputState>, +) -> impl IntoElement { + account_profile_labeled_control(label_key, app_text_input(input, false).w_full()) +} + +fn account_profile_select_field( + label_key: AppTextKey, + select: &Entity<AccountProfileSelectState>, +) -> impl IntoElement { + account_profile_labeled_control( + label_key, + Select::new(select).with_size(Size::Medium).w_full(), + ) +} + +fn account_profile_labeled_control( + label_key: AppTextKey, + control: impl IntoElement, ) -> impl IntoElement { app_stack_v(6.0) .w_full() @@ -8952,37 +9112,7 @@ fn account_profile_field( .text_color(rgb(APP_UI_THEME.foundation.text.secondary)) .child(app_shared_text(label_key)), ) - .child( - div() - .w_full() - .min_w_0() - .h(px(38.0)) - .border_1() - .border_color(rgb(APP_UI_THEME.foundation.surfaces.divider)) - .rounded(px(APP_UI_THEME.foundation.radii.medium_px)) - .bg(transparent_black()) - .px(px(12.0)) - .flex() - .items_center() - .justify_between() - .gap(px(8.0)) - .child( - div() - .flex_1() - .min_w_0() - .overflow_hidden() - .text_size(px(APP_UI_THEME.foundation.typography.body_text_px)) - .text_color(rgb(APP_UI_THEME.foundation.text.primary)) - .child(app_shared_text(value_key)), - ) - .when(selectable, |this| { - this.child( - Icon::new(IconName::ChevronDown) - .with_size(gpui_component::Size::Size(px(16.0))) - .text_color(rgb(APP_UI_THEME.foundation.text.secondary)), - ) - }), - ) + .child(control) } fn buyer_listings_feed( @@ -14502,14 +14632,14 @@ mod tests { PackDayHostHandoffStatusPresentation, PackDayPrintActionPresentation, PackDayPrintStatusPresentation, ReminderActionTarget, SETTINGS_FARM_PANEL_SECTIONS, SETTINGS_NAVIGATION_ORDER, SETTINGS_OPERATIONS_PANEL_SECTIONS, SettingsAutoFocusTarget, - SettingsInventorySectionSpec, SettingsPanelViewKey, StartupHomeSurface, - StartupSignerConnectState, abbreviated_npub, about_conflict_action_specs, - about_conflict_aggregate_text, about_conflict_detail_rows, about_conflict_review_body_key, - about_manual_refresh_enabled, about_runtime_rows, about_status_rows, account_display_name, - app_text, buyer_order_coordination_notice_forces_redraw, - buyer_order_detail_focus_after_open, buyer_orders_retry_action_visible, - buyer_receipt_issue_focus_after_submit, buyer_receipt_status_key, - farm_setup_onboarding_card_spec, farmer_home_farm_state, + SettingsInventorySectionSpec, SettingsPanelViewKey, ShellHeaderActiveMode, + StartupHomeSurface, StartupSignerConnectState, abbreviated_npub, + about_conflict_action_specs, about_conflict_aggregate_text, about_conflict_detail_rows, + about_conflict_review_body_key, about_manual_refresh_enabled, about_runtime_rows, + about_status_rows, account_display_name, app_text, + buyer_order_coordination_notice_forces_redraw, buyer_order_detail_focus_after_open, + buyer_orders_retry_action_visible, buyer_receipt_issue_focus_after_submit, + buyer_receipt_status_key, farm_setup_onboarding_card_spec, farmer_home_farm_state, farmer_order_detail_focus_after_open, farmer_pack_day_available, home_auto_focus_target, home_content_scroll_id, home_saved_farm, home_sidebar_navigation_sections, home_stage, home_window_launch_size_px, home_window_minimum_size_px, @@ -14522,8 +14652,8 @@ mod tests { parse_product_editor_price_input, presented_farmer_reminder, product_display_title, reminder_action_target, reminder_deadline_text, reminder_delivery_state_key, reminder_urgency_color, reminder_urgency_key, settings_auto_focus_target, - settings_preferences_general_row_state, startup_home_surface, startup_issue_summary_text, - startup_notice_text, startup_signer_preview_summary, + settings_preferences_general_row_state, shell_header_active_mode, startup_home_surface, + startup_issue_summary_text, startup_notice_text, startup_signer_preview_summary, startup_signer_preview_summary_for_connect_state, startup_signer_source_input_is_editable, startup_signer_status_spec, startup_signer_transport_failure_requires_notice, trade_agreement_status_key, trade_fulfillment_status_key, trade_inventory_status_key, @@ -15279,6 +15409,46 @@ mod tests { } #[test] + fn shell_header_active_mode_tracks_account_as_a_peer_selector() { + let mut runtime = summary( + HomeRoute::Personal, + TodayAgendaProjection::default(), + FarmSetupProjection::default(), + ); + runtime.shell_projection = AppShellProjection::new( + ActiveSurface::Personal, + ShellSection::Personal(PersonalSection::Browse), + ); + assert_eq!( + shell_header_active_mode(&runtime), + ShellHeaderActiveMode::Marketplace + ); + + runtime.shell_projection = AppShellProjection::new( + ActiveSurface::Farmer, + ShellSection::Farmer(FarmerSection::Today), + ); + assert_eq!( + shell_header_active_mode(&runtime), + ShellHeaderActiveMode::Farm + ); + + runtime.shell_projection = + AppShellProjection::new(ActiveSurface::Personal, ShellSection::Account); + assert_eq!( + shell_header_active_mode(&runtime), + ShellHeaderActiveMode::Account + ); + + runtime.shell_projection = + AppShellProjection::new(ActiveSurface::Farmer, ShellSection::Account); + assert_eq!( + shell_header_active_mode(&runtime), + ShellHeaderActiveMode::Account + ); + } + + #[test] fn home_auto_focus_target_tracks_startup_surface_contract() { assert_eq!( home_auto_focus_target( diff --git a/crates/i18n/src/keys.rs b/crates/i18n/src/keys.rs @@ -47,8 +47,14 @@ define_app_text_keys! { AccountProfileEmailValue => "account.profile.email.value", AccountProfilePhoneValue => "account.profile.phone.value", AccountProfileRoleValue => "account.profile.role.value", + AccountProfileRoleFarmManagerValue => "account.profile.role.farm_manager.value", + AccountProfileRoleTeamMemberValue => "account.profile.role.team_member.value", AccountProfileTimeZoneValue => "account.profile.time_zone.value", + AccountProfileTimeZoneMountainValue => "account.profile.time_zone.mountain.value", + AccountProfileTimeZoneEasternValue => "account.profile.time_zone.eastern.value", AccountProfileLanguageValue => "account.profile.language.value", + AccountProfileLanguageFrenchValue => "account.profile.language.french.value", + AccountProfileLanguageSpanishValue => "account.profile.language.spanish.value", HomeNavBrowse => "home.nav.browse", HomeNavSearch => "home.nav.search", HomeNavCart => "home.nav.cart", diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs @@ -8,15 +8,15 @@ pub use primitives::{ AppCheckboxFieldSpec, AppFormFieldSpec, AppIconButtonSpec, AppSegmentButtonIconSpec, AppUnderlineTabSpec, LabelValueRow, app_button_account_selector_row, app_button_card, app_button_choice, app_button_compact, app_button_icon, app_button_list_row, - app_button_primary, app_button_primary_disabled, app_button_secondary, - app_button_secondary_disabled, app_button_square_dropdown_secondary, app_button_text, - 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_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_primary, 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_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_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 @@ -722,6 +722,25 @@ pub fn app_button_secondary( ) } +pub fn app_button_secondary_full_width( + id: impl Into<ElementId>, + label: impl Into<SharedString>, + on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, + cx: &App, +) -> impl IntoElement { + app_button_label( + app_button_base(id, AppButtonVariant::Secondary, on_click, cx), + label.into(), + APP_UI_THEME + .components + .app_button + .sizing + .horizontal_padding_px, + AppButtonVariant::Secondary, + ) + .w_full() +} + pub fn app_button_secondary_disabled( id: impl Into<ElementId>, label: impl Into<SharedString>, @@ -757,6 +776,25 @@ pub fn app_button_primary( ) } +pub fn app_button_primary_full_width( + id: impl Into<ElementId>, + label: impl Into<SharedString>, + on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, + cx: &App, +) -> impl IntoElement { + app_button_label( + app_button_base(id, AppButtonVariant::Primary, on_click, cx), + label.into(), + APP_UI_THEME + .components + .app_button + .sizing + .horizontal_padding_px, + AppButtonVariant::Primary, + ) + .w_full() +} + pub fn app_button_primary_disabled( id: impl Into<ElementId>, label: impl Into<SharedString>, @@ -828,7 +866,7 @@ fn app_button_label( label: SharedString, horizontal_padding_px: f32, variant: AppButtonVariant, -) -> impl IntoElement { +) -> Button { let sizing = APP_UI_THEME.components.app_button.sizing; let colors = app_button_colors(variant); button.child( diff --git a/i18n/locales/en/messages.json b/i18n/locales/en/messages.json @@ -26,8 +26,14 @@ "account.profile.email.value": "avery@example.com", "account.profile.phone.value": "+1 250 555 0198", "account.profile.role.value": "Farm owner", + "account.profile.role.farm_manager.value": "Farm manager", + "account.profile.role.team_member.value": "Team member", "account.profile.time_zone.value": "Pacific Time", + "account.profile.time_zone.mountain.value": "Mountain Time", + "account.profile.time_zone.eastern.value": "Eastern Time", "account.profile.language.value": "English", + "account.profile.language.french.value": "French", + "account.profile.language.spanish.value": "Spanish", "home.nav.browse": "Browse", "home.nav.search": "Search", "home.nav.cart": "Cart",