app

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

commit eeb3044562ea8e1d277e7557d0eee72b0e977661
parent 303f18a4d971485b9111c43d5ac3ceeaa83ea5d7
Author: triesap <tyson@radroots.org>
Date:   Sun, 19 Apr 2026 19:19:59 +0000

ui: move utility settings and farm rules onto shared ui

- add a shared detail-row primitive for the utility window account surface
- replace launcher-local settings form and action helpers with shared ui controls
- render farm, settings, and about panels through shared surfaces and layout primitives
- verify the mounted app lane with cargo test --manifest-path Cargo.toml -p radroots_app

Diffstat:
Mcrates/launchers/desktop/src/window.rs | 1009+++++++++++++++++++++++++++++++++++++++----------------------------------------
Mcrates/shared/ui/src/lib.rs | 12++++++------
Mcrates/shared/ui/src/primitives.rs | 19+++++++++++++++++++
3 files changed, 517 insertions(+), 523 deletions(-)

diff --git a/crates/launchers/desktop/src/window.rs b/crates/launchers/desktop/src/window.rs @@ -2,11 +2,10 @@ use gpui::{ Animation, AnimationExt, AnyElement, App, AppContext, Bounds, ClickEvent, Context, Entity, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Timer, Window, WindowBackgroundAppearance, WindowBounds, WindowOptions, div, - prelude::FluentBuilder, px, relative, rgb, size, transparent_black, + prelude::FluentBuilder, px, relative, rgb, size, }; use gpui_component::{ IconName, Root, - button::{Button, ButtonCustomVariant, ButtonRounded, ButtonVariants}, input::{InputEvent, InputState}, }; use radroots_app_i18n::AppTextKey; @@ -39,15 +38,16 @@ use radroots_app_ui::{ app_button_primary as action_button_primary, app_button_primary_disabled as action_button_primary_disabled, app_button_secondary as action_button, app_button_text as text_button, app_checkbox_field, - app_cluster, app_divider as section_divider, app_form_field, app_form_input_text, - app_form_section, app_heading_section, app_heading_view, app_input_text as app_text_input, - app_scroll_panel, app_segment_button_icon as icon_segment_button, app_shared_label_text, - app_shared_text, app_split_shell, app_stack_h, app_stack_v, - app_status_indicator as status_indicator, app_surface_card, - app_surface_card_section as home_card, app_surface_panel, app_surface_sidebar, - app_surface_window as app_window_shell, app_text_badge as settings_badge_text, - app_text_body_subtle as home_body_text, app_text_label as home_farm_setup_field_label, - app_text_value, label_value_list, utility_title_row, + app_cluster, app_detail_row, app_divider as section_divider, app_form_field, + app_form_input_text, app_form_section, app_heading_section, app_heading_view, + app_input_text as app_text_input, app_scroll_panel, + app_segment_button_icon as icon_segment_button, app_shared_label_text, app_shared_text, + app_split_shell, app_stack_h, app_stack_v, app_status_indicator as status_indicator, + app_surface_card, app_surface_card_section as home_card, app_surface_panel, + app_surface_sidebar, app_surface_window as app_window_shell, + app_text_badge as settings_badge_text, app_text_body_subtle as home_body_text, + app_text_label as home_farm_setup_field_label, app_text_value, label_value_list, + utility_title_row, }; use radroots_nostr::prelude::RadrootsNostrClient; use std::time::Duration; @@ -3069,14 +3069,12 @@ impl SettingsWindowView { div() .w_full() .bg(rgb(APP_UI_THEME.foundation.surfaces.chrome_background)) - .rounded(px( - APP_UI_THEME - .shells - .settings_account_sidebar_button_corner_radius_px, - )) - .p(px( - APP_UI_THEME.shells.settings_account_sidebar_button_padding_px, - )) + .rounded(px(APP_UI_THEME + .shells + .settings_account_sidebar_button_corner_radius_px)) + .p(px(APP_UI_THEME + .shells + .settings_account_sidebar_button_padding_px)) .child( div() .flex() @@ -3084,12 +3082,10 @@ impl SettingsWindowView { .gap(px(2.0)) .child( div() - .text_size(px( - APP_UI_THEME - .foundation - .typography - .settings_account_identity_text_px, - )) + .text_size(px(APP_UI_THEME + .foundation + .typography + .settings_account_identity_text_px)) .font_weight(gpui::FontWeight::MEDIUM) .text_color(rgb(APP_UI_THEME.foundation.text.primary)) .child(app_shared_text( @@ -3098,12 +3094,10 @@ impl SettingsWindowView { ) .child( div() - .text_size(px( - APP_UI_THEME - .foundation - .typography - .settings_account_identity_text_px, - )) + .text_size(px(APP_UI_THEME + .foundation + .typography + .settings_account_identity_text_px)) .text_color(rgb(APP_UI_THEME.foundation.text.secondary)) .line_height(relative(1.2)) .child(app_shared_text( @@ -3115,16 +3109,14 @@ impl SettingsWindowView { .child( div() .w_full() - .pt(px( - APP_UI_THEME - .shells - .settings_account_sidebar_footer_padding_top_px, - )) + .pt(px(APP_UI_THEME + .shells + .settings_account_sidebar_footer_padding_top_px)) .flex() .flex_col() - .gap(px( - APP_UI_THEME.shells.settings_account_sidebar_footer_row_gap_px, - )) + .gap(px(APP_UI_THEME + .shells + .settings_account_sidebar_footer_row_gap_px)) .child(section_divider()) .child( div() @@ -3132,11 +3124,9 @@ impl SettingsWindowView { .flex() .items_center() .justify_between() - .gap(px( - APP_UI_THEME - .shells - .settings_account_sidebar_footer_button_gap_px, - )) + .gap(px(APP_UI_THEME + .shells + .settings_account_sidebar_footer_button_gap_px)) .child(action_button( "account-add", app_shared_text(AppTextKey::SettingsAccountAddAction), @@ -3170,7 +3160,9 @@ impl SettingsWindowView { .child( div() .w_full() - .max_w(px(APP_UI_THEME.shells.settings_account_content_max_width_px)) + .max_w(px(APP_UI_THEME + .shells + .settings_account_content_max_width_px)) .flex() .flex_col() .items_start() @@ -3184,18 +3176,17 @@ impl SettingsWindowView { .gap(px(APP_UI_THEME.shells.settings_account_main_stack_gap_px)) .child( div() - .size(px( - APP_UI_THEME - .shells - .settings_account_profile_avatar_size_px, - )) - .bg(rgb(APP_UI_THEME.foundation.surfaces.card_background)) - .rounded(px( - APP_UI_THEME - .shells - .settings_account_profile_avatar_size_px - / 2.0, - )), + .size(px(APP_UI_THEME + .shells + .settings_account_profile_avatar_size_px)) + .bg(rgb(APP_UI_THEME + .foundation + .surfaces + .card_background)) + .rounded(px(APP_UI_THEME + .shells + .settings_account_profile_avatar_size_px + / 2.0)), ) .child( div() @@ -3213,49 +3204,60 @@ impl SettingsWindowView { .flex() .flex_col() .gap(px(APP_UI_THEME.shells.settings_account_detail_row_gap_px)) - .child(self.settings_account_detail_row( - AppTextKey::SettingsAccountProfileLabel, + .child(app_detail_row( + app_shared_label_text( + AppTextKey::SettingsAccountProfileLabel, + ), div() .text_size(px(detail_text_px)) .text_color(rgb(APP_UI_THEME.foundation.text.primary)) .child(app_shared_text(AppTextKey::ValueNone)), )) - .child(self.settings_account_detail_row( - AppTextKey::SettingsAccountStatusLabel, + .child(app_detail_row( + app_shared_label_text( + AppTextKey::SettingsAccountStatusLabel, + ), div() .flex() .items_center() - .gap(px( - APP_UI_THEME - .shells - .settings_account_status_gap_px, - )) + .gap(px(APP_UI_THEME + .shells + .settings_account_status_gap_px)) .child(status_indicator(account_status_color)) .child( div() .text_size(px(detail_text_px)) - .text_color(rgb(APP_UI_THEME.foundation.text.primary)) + .text_color(rgb(APP_UI_THEME + .foundation + .text + .primary)) .child(app_shared_text( AppTextKey::SettingsAccountStatusLoggedOut, )), ), )) - .child(self.settings_account_detail_row( - AppTextKey::SettingsAccountCustodyLabel, + .child(app_detail_row( + app_shared_label_text( + AppTextKey::SettingsAccountCustodyLabel, + ), div() .text_size(px(detail_text_px)) .text_color(rgb(APP_UI_THEME.foundation.text.primary)) .child(app_shared_text(AppTextKey::ValueNone)), )) - .child(self.settings_account_detail_row( - AppTextKey::SettingsAccountSurfaceLabel, + .child(app_detail_row( + app_shared_label_text( + AppTextKey::SettingsAccountSurfaceLabel, + ), div() .text_size(px(detail_text_px)) .text_color(rgb(APP_UI_THEME.foundation.text.primary)) .child(app_shared_text(AppTextKey::ValueNone)), )) - .child(self.settings_account_detail_row( - AppTextKey::SettingsAccountActivationLabel, + .child(app_detail_row( + app_shared_label_text( + AppTextKey::SettingsAccountActivationLabel, + ), div() .text_size(px(detail_text_px)) .text_color(rgb(APP_UI_THEME.foundation.text.primary)) @@ -3263,115 +3265,40 @@ impl SettingsWindowView { AppTextKey::SettingsAccountActivationInactive, )), )) - .child( - div() - .w_full() - .text_size(px(detail_text_px)) - .line_height(relative(1.2)) - .text_color(rgb(APP_UI_THEME.foundation.text.secondary)) - .child(app_shared_text( - AppTextKey::SettingsAccountNoSelectionBody, - )), - ) + .child(home_body_text(app_shared_text( + AppTextKey::SettingsAccountNoSelectionBody, + ))) .child( div() .w_full() .flex() .min_w_0() .items_center() - .gap(px( - APP_UI_THEME - .shells - .settings_account_action_row_gap_px, - )) - .child( - div().child(action_button( - "account-log-out", - app_shared_text( - AppTextKey::SettingsAccountLogOutAction, - ), - |_, _, _| {}, - cx, - )), - ) - .child( - div().child(action_button( - "account-open-workspace", - app_shared_text( - AppTextKey::SettingsAccountOpenWorkspaceAction, - ), - |_, _, _| {}, - cx, - )), - ), + .gap(px(APP_UI_THEME + .shells + .settings_account_action_row_gap_px)) + .child(div().child(action_button( + "account-log-out", + app_shared_text( + AppTextKey::SettingsAccountLogOutAction, + ), + |_, _, _| {}, + cx, + ))) + .child(div().child(action_button( + "account-open-workspace", + app_shared_text( + AppTextKey::SettingsAccountOpenWorkspaceAction, + ), + |_, _, _| {}, + cx, + ))), ), ), ), ) } - fn settings_account_detail_row( - &self, - label_key: AppTextKey, - value: impl IntoElement, - ) -> impl IntoElement { - div() - .w_full() - .flex() - .items_center() - .gap(px(APP_UI_THEME.shells.settings_account_detail_value_gap_px)) - .child( - div() - .text_size(px(APP_UI_THEME - .foundation - .typography - .settings_account_detail_text_px)) - .font_weight(gpui::FontWeight::SEMIBOLD) - .text_color(rgb(APP_UI_THEME.foundation.text.secondary)) - .child(app_shared_label_text(label_key)), - ) - .child(value) - } - - fn settings_checkbox_row( - &mut self, - id: &'static str, - checked: bool, - label_key: AppTextKey, - trailing_button_id: Option<&'static str>, - trailing_button_key: Option<AppTextKey>, - note_key: Option<AppTextKey>, - on_toggle: impl Fn(&bool, &mut Window, &mut gpui::App) + 'static, - cx: &mut Context<Self>, - ) -> impl IntoElement { - let note_text = note_key.map(app_shared_text); - - div().w_full().child( - div() - .w_full() - .flex() - .items_start() - .gap(px(APP_UI_THEME.shells.settings_account_detail_value_gap_px)) - .child(app_checkbox_field( - AppCheckboxFieldSpec::new(id, app_shared_text(label_key), note_text), - checked, - cx, - move |checked, window, cx| on_toggle(&checked, window, cx), - )) - .when_some( - trailing_button_id.zip(trailing_button_key), - |this, (button_id, button_key)| { - this.child(div().flex_none().child(action_button_compact( - button_id, - app_shared_text(button_key), - |_, _, _| {}, - cx, - ))) - }, - ), - ) - } - fn settings_panel(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { self.sync_farm_panel_state(window, cx); @@ -3415,26 +3342,46 @@ impl SettingsWindowView { cards.push( home_card( app_shared_text(AppTextKey::SettingsOperatingRulesSectionLabel), - div() + app_stack_v(12.0) .w_full() - .flex() - .flex_col() - .gap(px(12.0)) - .child(settings_text_field( - AppTextKey::SettingsOperatingRulesFieldPromiseLeadTime, + .child(app_form_input_text( + AppFormFieldSpec::new( + app_shared_text( + AppTextKey::SettingsOperatingRulesFieldPromiseLeadTime, + ), + Option::<SharedString>::None, + ), &form.operating_rules.promise_lead_hours_input, + false, )) - .child(settings_text_field( - AppTextKey::SettingsOperatingRulesFieldSubstitutionPolicy, + .child(app_form_input_text( + AppFormFieldSpec::new( + app_shared_text( + AppTextKey::SettingsOperatingRulesFieldSubstitutionPolicy, + ), + Option::<SharedString>::None, + ), &form.operating_rules.substitution_policy_input, + false, )) - .child(settings_text_field( - AppTextKey::SettingsOperatingRulesFieldMissedPickupPolicy, + .child(app_form_input_text( + AppFormFieldSpec::new( + app_shared_text( + AppTextKey::SettingsOperatingRulesFieldMissedPickupPolicy, + ), + Option::<SharedString>::None, + ), &form.operating_rules.missed_pickup_policy_input, + false, )) - .children(settings_validation_rows( - &evaluation.operating_rules_validation_keys, - )), + .children( + evaluation + .operating_rules_validation_keys + .iter() + .copied() + .map(|key| home_body_text(app_shared_text(key)).into_any_element()) + .collect::<Vec<_>>(), + ), ) .into_any_element(), ); @@ -3476,7 +3423,7 @@ impl SettingsWindowView { selected_pickup_location_id == pickup_location_id }); - settings_dynamic_action_button( + choice_button( ( "settings-fulfillment-window-pickup-location", index * 100 + pickup_index, @@ -3523,10 +3470,9 @@ impl SettingsWindowView { .collect::<Vec<_>>(), ) .child( - settings_dynamic_action_button( + action_button_compact( "settings-add-fulfillment-window", app_shared_text(AppTextKey::SettingsFulfillmentWindowsAddAction), - false, cx.listener(|this, _, window, cx| { this.add_fulfillment_window(window, cx) }), @@ -3576,10 +3522,9 @@ impl SettingsWindowView { .collect::<Vec<_>>(), ) .child( - settings_dynamic_action_button( + action_button_compact( "settings-add-blackout-period", app_shared_text(AppTextKey::SettingsBlackoutPeriodsAddAction), - false, cx.listener(|this, _, window, cx| { this.add_blackout_period(window, cx) }), @@ -3618,56 +3563,99 @@ impl SettingsWindowView { cards.push( home_card( app_shared_text(AppTextKey::SettingsGeneralSectionLabel), - div() + app_stack_v(16.0) .w_full() - .flex() - .flex_col() - .gap(px(16.0)) - .child(self.settings_checkbox_row( - "settings-allow-relay-connections", - general_allow_relay_connections, - AppTextKey::SettingsGeneralAllowRelayConnections, - None, - None, - None, - |_, _, _| {}, - cx, - )) - .child(self.settings_checkbox_row( - "settings-use-media-servers", - general_use_media_servers, - AppTextKey::SettingsGeneralUseMediaServers, - Some("settings-manage-media-servers"), - Some(AppTextKey::SettingsGeneralManageAction), - None, - |_, _, _| {}, - cx, - )) - .child(self.settings_checkbox_row( - "settings-use-nip05", - general_use_nip05, - AppTextKey::SettingsGeneralUseNip05, - None, - None, - Some(AppTextKey::SettingsGeneralUseNip05Note), - |_, _, _| {}, - cx, - )) - .child(self.settings_checkbox_row( - "settings-launch-at-login", - general_launch_at_login, - AppTextKey::SettingsGeneralLaunchAtLogin, - None, - None, - None, - |_, _, _| {}, - cx, - )), + .child( + div() + .w_full() + .flex() + .items_start() + .gap(px(APP_UI_THEME.shells.settings_account_detail_value_gap_px)) + .child(app_checkbox_field( + AppCheckboxFieldSpec::new( + "settings-allow-relay-connections", + app_shared_text( + AppTextKey::SettingsGeneralAllowRelayConnections, + ), + Option::<SharedString>::None, + ), + general_allow_relay_connections, + cx, + |_, _, _| {}, + )), + ) + .child( + div() + .w_full() + .flex() + .items_start() + .gap(px(APP_UI_THEME.shells.settings_account_detail_value_gap_px)) + .child(app_checkbox_field( + AppCheckboxFieldSpec::new( + "settings-use-media-servers", + app_shared_text(AppTextKey::SettingsGeneralUseMediaServers), + Option::<SharedString>::None, + ), + general_use_media_servers, + cx, + |_, _, _| {}, + )) + .child(div().flex_none().child(action_button_compact( + "settings-manage-media-servers", + app_shared_text(AppTextKey::SettingsGeneralManageAction), + |_, _, _| {}, + cx, + ))), + ) + .child( + div() + .w_full() + .flex() + .items_start() + .gap(px(APP_UI_THEME.shells.settings_account_detail_value_gap_px)) + .child(app_checkbox_field( + AppCheckboxFieldSpec::new( + "settings-use-nip05", + app_shared_text(AppTextKey::SettingsGeneralUseNip05), + Some(app_shared_text(AppTextKey::SettingsGeneralUseNip05Note)), + ), + general_use_nip05, + cx, + |_, _, _| {}, + )), + ) + .child( + div() + .w_full() + .flex() + .items_start() + .gap(px(APP_UI_THEME.shells.settings_account_detail_value_gap_px)) + .child(app_checkbox_field( + AppCheckboxFieldSpec::new( + "settings-launch-at-login", + app_shared_text(AppTextKey::SettingsGeneralLaunchAtLogin), + Option::<SharedString>::None, + ), + general_launch_at_login, + cx, + |_, _, _| {}, + )), + ), ) .into_any_element(), ); - settings_inventory_panel(AppTextKey::SettingsSettingsPanelBody, cards) + app_scroll_panel( + "settings-panel-scroll", + APP_UI_THEME.shells.settings_content_padding_px, + Some(560.0), + app_stack_v(APP_UI_THEME.shells.home_stack_gap_px) + .w_full() + .child(home_body_text(app_shared_text( + AppTextKey::SettingsSettingsPanelBody, + ))) + .children(cards), + ) } fn farm_panel(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { @@ -3683,7 +3671,17 @@ impl SettingsWindowView { ) .into_any_element(), ); - return settings_inventory_panel(AppTextKey::SettingsFarmPanelBody, cards); + return app_scroll_panel( + "settings-panel-scroll", + APP_UI_THEME.shells.settings_content_padding_px, + Some(560.0), + app_stack_v(APP_UI_THEME.shells.home_stack_gap_px) + .w_full() + .child(home_body_text(app_shared_text( + AppTextKey::SettingsFarmPanelBody, + ))) + .children(cards), + ); } let Some(form) = self.farm_panel_state.as_ref() else { @@ -3694,7 +3692,17 @@ impl SettingsWindowView { ) .into_any_element(), ); - return settings_inventory_panel(AppTextKey::SettingsFarmPanelBody, cards); + return app_scroll_panel( + "settings-panel-scroll", + APP_UI_THEME.shells.settings_content_padding_px, + Some(560.0), + app_stack_v(APP_UI_THEME.shells.home_stack_gap_px) + .w_full() + .child(home_body_text(app_shared_text( + AppTextKey::SettingsFarmPanelBody, + ))) + .children(cards), + ); }; let evaluation = form.evaluate(cx); @@ -3718,22 +3726,31 @@ impl SettingsWindowView { cards.push( home_card( app_shared_text(AppTextKey::HomeFarmSetupSectionFarm), - div() + app_stack_v(12.0) .w_full() - .flex() - .flex_col() - .gap(px(12.0)) - .child(settings_text_field( - AppTextKey::HomeFarmSetupFieldFarmName, + .child(app_form_input_text( + AppFormFieldSpec::new( + app_shared_text(AppTextKey::HomeFarmSetupFieldFarmName), + Option::<SharedString>::None, + ), &form.farm_name_input, + false, )) - .child(settings_text_field( - AppTextKey::SettingsFarmFieldTimezone, + .child(app_form_input_text( + AppFormFieldSpec::new( + app_shared_text(AppTextKey::SettingsFarmFieldTimezone), + Option::<SharedString>::None, + ), &form.timezone_input, + false, )) - .child(settings_text_field( - AppTextKey::SettingsFarmFieldCurrency, + .child(app_form_input_text( + AppFormFieldSpec::new( + app_shared_text(AppTextKey::SettingsFarmFieldCurrency), + Option::<SharedString>::None, + ), &form.currency_input, + false, )), ) .into_any_element(), @@ -3773,10 +3790,9 @@ impl SettingsWindowView { .collect::<Vec<_>>(), ) .child( - settings_dynamic_action_button( + action_button_compact( "settings-farm-add-pickup", app_shared_text(AppTextKey::SettingsPickupLocationsAddAction), - false, cx.listener(|this, _, window, cx| this.add_pickup_location(window, cx)), cx, ) @@ -3789,7 +3805,17 @@ impl SettingsWindowView { .into_any_element(), ); - settings_inventory_panel(AppTextKey::SettingsFarmPanelBody, cards) + app_scroll_panel( + "settings-panel-scroll", + APP_UI_THEME.shells.settings_content_padding_px, + Some(560.0), + app_stack_v(APP_UI_THEME.shells.home_stack_gap_px) + .w_full() + .child(home_body_text(app_shared_text( + AppTextKey::SettingsFarmPanelBody, + ))) + .children(cards), + ) } fn about_panel(&self) -> impl IntoElement { @@ -3804,36 +3830,24 @@ impl SettingsWindowView { app_stack_v(APP_UI_THEME.shells.settings_account_main_stack_gap_px) .w_full() .justify_between() - .text_size(px(APP_UI_THEME.foundation.typography.body_text_px)) - .text_color(rgb(APP_UI_THEME.foundation.text.secondary)) - .child(app_shared_text( + .child(home_body_text(app_shared_text( AppTextKey::SettingsAboutPlaceholderTopPrimary, - )) - .child(app_shared_text( + ))) + .child(home_body_text(app_shared_text( AppTextKey::SettingsAboutPlaceholderTopSecondary, - )) - .child(app_shared_text( + ))) + .child(home_body_text(app_shared_text( AppTextKey::SettingsAboutPlaceholderTopTertiary, - )), + ))), ) .child(section_divider()) - .child( - div() - .w_full() - .py_12() - .text_size(px(APP_UI_THEME.foundation.typography.body_text_px)) - .text_color(rgb(APP_UI_THEME.foundation.text.secondary)) - .child(app_shared_text(AppTextKey::SettingsAboutPlaceholderMiddle)), - ) + .child(div().w_full().py_12().child(home_body_text(app_shared_text( + AppTextKey::SettingsAboutPlaceholderMiddle, + )))) .child(section_divider()) - .child( - div() - .w_full() - .py_12() - .text_size(px(APP_UI_THEME.foundation.typography.body_text_px)) - .text_color(rgb(APP_UI_THEME.foundation.text.secondary)) - .child(app_shared_text(AppTextKey::SettingsAboutPlaceholderBottom)), - ), + .child(div().w_full().py_12().child(home_body_text(app_shared_text( + AppTextKey::SettingsAboutPlaceholderBottom, + )))), ) } @@ -5844,19 +5858,11 @@ fn home_farm_setup_order_method_section( move |checked, window, cx| on_shipping_change(&checked, window, cx), )) .when_some(blocker_key, |this, blocker_key| { - this.child(home_farm_setup_blocker(blocker_key)) + this.child(home_body_text(app_shared_text(blocker_key))) }), ) } -fn home_farm_setup_blocker(key: AppTextKey) -> impl IntoElement { - div() - .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px)) - .line_height(relative(1.2)) - .text_color(rgb(APP_UI_THEME.foundation.text.secondary)) - .child(app_shared_text(key)) -} - fn settings_panel_farm_context(runtime: &DesktopAppRuntimeSummary) -> Option<(String, FarmId)> { let account_id = runtime .settings_account_projection @@ -5879,14 +5885,6 @@ fn settings_panel_farm_context(runtime: &DesktopAppRuntimeSummary) -> Option<(St Some((account_id, farm_id)) } -fn settings_text_field(label_key: AppTextKey, input: &Entity<InputState>) -> impl IntoElement { - app_form_input_text( - AppFormFieldSpec::new(app_shared_text(label_key), Option::<SharedString>::None), - input, - false, - ) -} - fn settings_pickup_location_title( index: usize, pickup_location: &SettingsPickupLocationFormState, @@ -5927,10 +5925,9 @@ fn settings_pickup_location_card( )) .into_any_element() } else { - settings_dynamic_action_button( + action_button_compact( ("settings-farm-default-pickup", index), app_shared_text(AppTextKey::SettingsPickupLocationsMakeDefaultAction), - false, on_make_default, cx, ) @@ -5938,10 +5935,9 @@ fn settings_pickup_location_card( }) .when(pickup_location.can_remove, |this| { this.child( - settings_dynamic_action_button( + action_button_compact( ("settings-farm-remove-pickup", index), app_shared_text(AppTextKey::SettingsPickupLocationsRemoveAction), - false, on_remove, cx, ) @@ -5949,42 +5945,51 @@ fn settings_pickup_location_card( ) }); - div() - .w_full() - .bg(rgb(APP_UI_THEME.foundation.surfaces.chrome_background)) - .rounded(px(APP_UI_THEME.foundation.radii.medium_px)) - .p(px(12.0)) - .flex() - .flex_col() - .gap(px(10.0)) - .child( - div() - .w_full() - .flex() - .items_start() - .justify_between() - .gap(px(8.0)) - .child( - div() - .text_size(px(APP_UI_THEME.foundation.typography.body_text_px)) - .font_weight(gpui::FontWeight::SEMIBOLD) - .text_color(rgb(APP_UI_THEME.foundation.text.primary)) - .child(title), - ) - .child(action_row), - ) - .child(settings_text_field( - AppTextKey::SettingsPickupLocationsFieldLabel, - &pickup_location.label_input, - )) - .child(settings_text_field( - AppTextKey::SettingsPickupLocationsFieldAddress, - &pickup_location.address_input, - )) - .child(settings_text_field( - AppTextKey::SettingsPickupLocationsFieldDirections, - &pickup_location.directions_input, - )) + app_surface_panel( + app_stack_v(10.0) + .w_full() + .p(px(12.0)) + .child( + div() + .w_full() + .flex() + .items_start() + .justify_between() + .gap(px(8.0)) + .child( + div() + .text_size(px(APP_UI_THEME.foundation.typography.body_text_px)) + .font_weight(gpui::FontWeight::SEMIBOLD) + .text_color(rgb(APP_UI_THEME.foundation.text.primary)) + .child(title), + ) + .child(action_row), + ) + .child(app_form_input_text( + AppFormFieldSpec::new( + app_shared_text(AppTextKey::SettingsPickupLocationsFieldLabel), + Option::<SharedString>::None, + ), + &pickup_location.label_input, + false, + )) + .child(app_form_input_text( + AppFormFieldSpec::new( + app_shared_text(AppTextKey::SettingsPickupLocationsFieldAddress), + Option::<SharedString>::None, + ), + &pickup_location.address_input, + false, + )) + .child(app_form_input_text( + AppFormFieldSpec::new( + app_shared_text(AppTextKey::SettingsPickupLocationsFieldDirections), + Option::<SharedString>::None, + ), + &pickup_location.directions_input, + false, + )), + ) } fn settings_fulfillment_window_title( @@ -6031,14 +6036,6 @@ fn settings_blackout_period_title( } } -fn settings_validation_rows(keys: &[AppTextKey]) -> Vec<AnyElement> { - keys.iter() - .copied() - .map(home_farm_setup_blocker) - .map(IntoElement::into_any_element) - .collect() -} - fn settings_fulfillment_window_card( index: usize, fulfillment_window: &SettingsFulfillmentWindowFormState, @@ -6047,72 +6044,90 @@ fn settings_fulfillment_window_card( on_remove: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, cx: &App, ) -> impl IntoElement { - div() - .w_full() - .bg(rgb(APP_UI_THEME.foundation.surfaces.chrome_background)) - .rounded(px(APP_UI_THEME.foundation.radii.medium_px)) - .p(px(12.0)) - .flex() - .flex_col() - .gap(px(10.0)) - .child( - div() - .w_full() - .flex() - .items_start() - .justify_between() - .gap(px(8.0)) - .child( - div() - .text_size(px(APP_UI_THEME.foundation.typography.body_text_px)) - .font_weight(gpui::FontWeight::SEMIBOLD) - .text_color(rgb(APP_UI_THEME.foundation.text.primary)) - .child(settings_fulfillment_window_title( - index, - fulfillment_window, - cx, - )), - ) - .child( - settings_dynamic_action_button( - ("settings-remove-fulfillment-window", index), - app_shared_text(AppTextKey::SettingsFulfillmentWindowsRemoveAction), - false, - on_remove, - cx, + app_surface_panel( + app_stack_v(10.0) + .w_full() + .p(px(12.0)) + .child( + div() + .w_full() + .flex() + .items_start() + .justify_between() + .gap(px(8.0)) + .child( + div() + .text_size(px(APP_UI_THEME.foundation.typography.body_text_px)) + .font_weight(gpui::FontWeight::SEMIBOLD) + .text_color(rgb(APP_UI_THEME.foundation.text.primary)) + .child(settings_fulfillment_window_title( + index, + fulfillment_window, + cx, + )), ) - .into_any_element(), + .child( + action_button_compact( + ("settings-remove-fulfillment-window", index), + app_shared_text(AppTextKey::SettingsFulfillmentWindowsRemoveAction), + on_remove, + cx, + ) + .into_any_element(), + ), + ) + .child(app_form_input_text( + AppFormFieldSpec::new( + app_shared_text(AppTextKey::SettingsFulfillmentWindowsFieldLabel), + Option::<SharedString>::None, ), - ) - .child(settings_text_field( - AppTextKey::SettingsFulfillmentWindowsFieldLabel, - &fulfillment_window.label_input, - )) - .child(app_form_field( - AppFormFieldSpec::new( - app_shared_text(AppTextKey::SettingsFulfillmentWindowsFieldPickupLocation), - Option::<SharedString>::None, + &fulfillment_window.label_input, + false, + )) + .child(app_form_field( + AppFormFieldSpec::new( + app_shared_text(AppTextKey::SettingsFulfillmentWindowsFieldPickupLocation), + Option::<SharedString>::None, + ), + div() + .w_full() + .flex() + .flex_wrap() + .gap(px(8.0)) + .children(pickup_location_options), + )) + .child(app_form_input_text( + AppFormFieldSpec::new( + app_shared_text(AppTextKey::SettingsFulfillmentWindowsFieldStartsAt), + Option::<SharedString>::None, + ), + &fulfillment_window.starts_at_input, + false, + )) + .child(app_form_input_text( + AppFormFieldSpec::new( + app_shared_text(AppTextKey::SettingsFulfillmentWindowsFieldEndsAt), + Option::<SharedString>::None, + ), + &fulfillment_window.ends_at_input, + false, + )) + .child(app_form_input_text( + AppFormFieldSpec::new( + app_shared_text(AppTextKey::SettingsFulfillmentWindowsFieldOrderCutoff), + Option::<SharedString>::None, + ), + &fulfillment_window.order_cutoff_input, + false, + )) + .children( + validation_keys + .iter() + .copied() + .map(|key| home_body_text(app_shared_text(key)).into_any_element()) + .collect::<Vec<_>>(), ), - div() - .w_full() - .flex() - .flex_wrap() - .gap(px(8.0)) - .children(pickup_location_options), - )) - .child(settings_text_field( - AppTextKey::SettingsFulfillmentWindowsFieldStartsAt, - &fulfillment_window.starts_at_input, - )) - .child(settings_text_field( - AppTextKey::SettingsFulfillmentWindowsFieldEndsAt, - &fulfillment_window.ends_at_input, - )) - .child(settings_text_field( - AppTextKey::SettingsFulfillmentWindowsFieldOrderCutoff, - &fulfillment_window.order_cutoff_input, - )) - .children(settings_validation_rows(validation_keys)) + ) } fn settings_blackout_period_card( @@ -6122,52 +6137,66 @@ fn settings_blackout_period_card( on_remove: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, cx: &App, ) -> impl IntoElement { - div() - .w_full() - .bg(rgb(APP_UI_THEME.foundation.surfaces.chrome_background)) - .rounded(px(APP_UI_THEME.foundation.radii.medium_px)) - .p(px(12.0)) - .flex() - .flex_col() - .gap(px(10.0)) - .child( - div() - .w_full() - .flex() - .items_start() - .justify_between() - .gap(px(8.0)) - .child( - div() - .text_size(px(APP_UI_THEME.foundation.typography.body_text_px)) - .font_weight(gpui::FontWeight::SEMIBOLD) - .text_color(rgb(APP_UI_THEME.foundation.text.primary)) - .child(settings_blackout_period_title(index, blackout_period, cx)), - ) - .child( - settings_dynamic_action_button( - ("settings-remove-blackout-period", index), - app_shared_text(AppTextKey::SettingsBlackoutPeriodsRemoveAction), - false, - on_remove, - cx, + app_surface_panel( + app_stack_v(10.0) + .w_full() + .p(px(12.0)) + .child( + div() + .w_full() + .flex() + .items_start() + .justify_between() + .gap(px(8.0)) + .child( + div() + .text_size(px(APP_UI_THEME.foundation.typography.body_text_px)) + .font_weight(gpui::FontWeight::SEMIBOLD) + .text_color(rgb(APP_UI_THEME.foundation.text.primary)) + .child(settings_blackout_period_title(index, blackout_period, cx)), ) - .into_any_element(), + .child( + action_button_compact( + ("settings-remove-blackout-period", index), + app_shared_text(AppTextKey::SettingsBlackoutPeriodsRemoveAction), + on_remove, + cx, + ) + .into_any_element(), + ), + ) + .child(app_form_input_text( + AppFormFieldSpec::new( + app_shared_text(AppTextKey::SettingsBlackoutPeriodsFieldLabel), + Option::<SharedString>::None, ), - ) - .child(settings_text_field( - AppTextKey::SettingsBlackoutPeriodsFieldLabel, - &blackout_period.label_input, - )) - .child(settings_text_field( - AppTextKey::SettingsBlackoutPeriodsFieldStartsAt, - &blackout_period.starts_at_input, - )) - .child(settings_text_field( - AppTextKey::SettingsBlackoutPeriodsFieldEndsAt, - &blackout_period.ends_at_input, - )) - .children(settings_validation_rows(validation_keys)) + &blackout_period.label_input, + false, + )) + .child(app_form_input_text( + AppFormFieldSpec::new( + app_shared_text(AppTextKey::SettingsBlackoutPeriodsFieldStartsAt), + Option::<SharedString>::None, + ), + &blackout_period.starts_at_input, + false, + )) + .child(app_form_input_text( + AppFormFieldSpec::new( + app_shared_text(AppTextKey::SettingsBlackoutPeriodsFieldEndsAt), + Option::<SharedString>::None, + ), + &blackout_period.ends_at_input, + false, + )) + .children( + validation_keys + .iter() + .copied() + .map(|key| home_body_text(app_shared_text(key)).into_any_element()) + .collect::<Vec<_>>(), + ), + ) } fn settings_farm_readiness_rows(evaluation: &SettingsFarmRulesEvaluation) -> Vec<AnyElement> { @@ -6179,8 +6208,15 @@ fn settings_farm_readiness_rows(evaluation: &SettingsFarmRulesEvaluation) -> Vec readiness_keys .into_iter() - .map(settings_inventory_field_row) - .map(IntoElement::into_any_element) + .map(|key| { + app_surface_panel( + div() + .px(px(12.0)) + .py(px(10.0)) + .child(home_farm_setup_field_label(app_shared_text(key))), + ) + .into_any_element() + }) .collect() } @@ -6218,89 +6254,28 @@ fn settings_timing_conflict_key(kind: FarmTimingConflictKind) -> AppTextKey { } } -fn settings_dynamic_action_button( - id: impl Into<gpui::ElementId>, - label: impl Into<SharedString>, - is_primary: bool, - on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, - cx: &App, -) -> impl IntoElement { - let sizing = APP_UI_THEME.components.app_button.sizing; - let colors = if is_primary { - APP_UI_THEME.components.app_button.primary_colors - } else { - APP_UI_THEME.components.app_button.secondary_colors - }; - let hover_background = if colors.hover_changes_background { - colors.hover_background - } else { - colors.background - }; - let label = label.into(); - - Button::new(id) - .custom( - ButtonCustomVariant::new(cx) - .color(rgb(colors.background).into()) - .foreground(rgb(colors.foreground).into()) - .border(transparent_black()) - .hover(rgb(hover_background).into()) - .active(rgb(colors.active_background).into()), - ) - .rounded(ButtonRounded::Size(px(sizing.corner_radius_px))) - .h(px(sizing.height_px)) - .on_click(on_click) - .child( - div() - .h_full() - .flex() - .items_center() - .justify_center() - .px(px(sizing.compact_horizontal_padding_px)) - .text_size(px(sizing.label_size_px)) - .text_color(rgb(colors.foreground)) - .child(label), - ) -} - -fn settings_inventory_panel(intro_key: AppTextKey, cards: Vec<AnyElement>) -> impl IntoElement { - let content_max_width_px = 560.0; - - app_scroll_panel( - "settings-panel-scroll", - APP_UI_THEME.shells.settings_content_padding_px, - Some(content_max_width_px), - app_stack_v(APP_UI_THEME.shells.home_stack_gap_px) - .w_full() - .child(home_body_text(app_shared_text(intro_key))) - .children(cards), - ) -} - #[cfg(test)] fn settings_inventory_card(spec: SettingsInventorySectionSpec) -> impl IntoElement { home_card( app_shared_text(spec.title_key), - div().w_full().flex().flex_col().gap(px(8.0)).children( + app_stack_v(8.0).w_full().children( spec.field_keys .iter() .copied() - .map(settings_inventory_field_row) - .map(IntoElement::into_any_element) + .map(|key| { + app_surface_panel( + div() + .px(px(12.0)) + .py(px(10.0)) + .child(home_farm_setup_field_label(app_shared_text(key))), + ) + .into_any_element() + }) .collect::<Vec<_>>(), ), ) } -fn settings_inventory_field_row(key: AppTextKey) -> impl IntoElement { - app_surface_panel( - div() - .px(px(12.0)) - .py(px(10.0)) - .child(home_farm_setup_field_label(app_shared_text(key))), - ) -} - fn home_saved_farm_summary_card(runtime: &DesktopAppRuntimeSummary) -> Option<AnyElement> { let saved_farm = home_saved_farm(runtime)?; let location_or_service_area = if runtime diff --git a/crates/shared/ui/src/lib.rs b/crates/shared/ui/src/lib.rs @@ -8,12 +8,12 @@ pub use primitives::{ AppCheckboxFieldSpec, AppFormFieldSpec, AppSegmentButtonIconSpec, LabelValueRow, 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_text, - app_checkbox_field, app_cluster, app_divider, 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, label_value_list, utility_title_row, + app_checkbox_field, app_cluster, app_detail_row, app_divider, 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, label_value_list, utility_title_row, }; pub use text::{ app_shared_label_text, app_shared_text, runtime_metadata_rows, settings_about_status_rows, diff --git a/crates/shared/ui/src/primitives.rs b/crates/shared/ui/src/primitives.rs @@ -309,6 +309,25 @@ pub fn label_value_list(rows: impl IntoIterator<Item = LabelValueRow>) -> impl I .children(rows) } +pub fn app_detail_row(label: impl Into<SharedString>, value: impl IntoElement) -> impl IntoElement { + div() + .w_full() + .flex() + .items_center() + .gap(px(APP_UI_THEME.shells.settings_account_detail_value_gap_px)) + .child( + div() + .text_size(px(APP_UI_THEME + .foundation + .typography + .settings_account_detail_text_px)) + .font_weight(gpui::FontWeight::SEMIBOLD) + .text_color(rgb(APP_UI_THEME.foundation.text.secondary)) + .child(label.into()), + ) + .child(value) +} + pub fn app_form_section( title: impl Into<SharedString>, content: impl IntoElement,