app

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

commit 78076d3dd6028098a64278712b7049706f698e6d
parent 289aa858ed7249e293620d220c42bb5da5dfa191
Author: triesap <tyson@radroots.org>
Date:   Sun, 19 Apr 2026 18:25:58 +0000

ui: add semantic text and form primitives

Diffstat:
Mcrates/launchers/desktop/src/window.rs | 618++++++++++++++++++++++++++++++++-----------------------------------------------
Mcrates/shared/ui/src/lib.rs | 11+++++++----
Mcrates/shared/ui/src/primitives.rs | 156+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
3 files changed, 405 insertions(+), 380 deletions(-)

diff --git a/crates/launchers/desktop/src/window.rs b/crates/launchers/desktop/src/window.rs @@ -33,14 +33,20 @@ use radroots_app_state::{ FarmSetupFlowStage, FarmWorkspaceStatus, HomeRoute, derive_product_publish_blockers, }; use radroots_app_ui::{ - APP_UI_THEME, AppCheckboxFieldSpec, AppSegmentButtonIconSpec as IconSegmentButtonSpec, - LabelValueRow, app_button_compact as action_button_compact, - app_button_icon as action_icon_button, app_button_primary as action_button_primary, + APP_UI_THEME, AppCheckboxFieldSpec, AppFormFieldSpec, + AppSegmentButtonIconSpec as IconSegmentButtonSpec, LabelValueRow, + app_button_compact as action_button_compact, app_button_icon as action_icon_button, + app_button_primary as action_button_primary, app_button_primary_disabled as action_button_primary_disabled, app_button_secondary as action_button, app_checkbox_field, 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_segment_button_icon as icon_segment_button, app_shared_label_text, app_shared_text, app_status_indicator as status_indicator, - app_surface_window as app_window_shell, label_value_list, utility_title_row, + 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; @@ -3065,7 +3071,7 @@ impl SettingsWindowView { .bg(rgb(APP_UI_THEME.foundation.surfaces.chrome_background)) .rounded(px( APP_UI_THEME - .layout + .shells .settings_account_sidebar_button_corner_radius_px, )) .p(px( @@ -3080,6 +3086,7 @@ impl SettingsWindowView { div() .text_size(px( APP_UI_THEME + .foundation .typography .settings_account_identity_text_px, )) @@ -3093,6 +3100,7 @@ impl SettingsWindowView { div() .text_size(px( APP_UI_THEME + .foundation .typography .settings_account_identity_text_px, )) @@ -3109,7 +3117,7 @@ impl SettingsWindowView { .w_full() .pt(px( APP_UI_THEME - .layout + .shells .settings_account_sidebar_footer_padding_top_px, )) .flex() @@ -3126,7 +3134,7 @@ impl SettingsWindowView { .justify_between() .gap(px( APP_UI_THEME - .layout + .shells .settings_account_sidebar_footer_button_gap_px, )) .child(action_button( @@ -3178,13 +3186,13 @@ impl SettingsWindowView { div() .size(px( APP_UI_THEME - .layout + .shells .settings_account_profile_avatar_size_px, )) .bg(rgb(APP_UI_THEME.foundation.surfaces.card_background)) .rounded(px( APP_UI_THEME - .layout + .shells .settings_account_profile_avatar_size_px / 2.0, )), @@ -3219,7 +3227,7 @@ impl SettingsWindowView { .items_center() .gap(px( APP_UI_THEME - .layout + .shells .settings_account_status_gap_px, )) .child(status_indicator(account_status_color)) @@ -3273,7 +3281,7 @@ impl SettingsWindowView { .items_center() .gap(px( APP_UI_THEME - .layout + .shells .settings_account_action_row_gap_px, )) .child( @@ -4146,9 +4154,12 @@ fn startup_home_shell( cx, )) .when_some(startup_notice, |this, error| { - this.child(startup_home_support_text( - error.to_owned(), - )) + this.child( + div() + .w_full() + .text_center() + .child(home_body_text(error.to_owned())), + ) }) .into_any_element(), StartupHomeSurface::IdentityChoice => div() @@ -4173,9 +4184,12 @@ fn startup_home_shell( cx, )) .when_some(startup_notice, |this, error| { - this.child(startup_home_support_text( - error.to_owned(), - )) + this.child( + div() + .w_full() + .text_center() + .child(home_body_text(error.to_owned())), + ) }) .into_any_element(), StartupHomeSurface::GenerateKeyStarting => div() @@ -4202,9 +4216,17 @@ fn startup_home_shell( ) .into_any_element() } - StartupHomeSurface::IssueCard => startup_home_card( - app_shared_text(AppTextKey::MetadataStartupIssue), - startup_home_body(runtime), + StartupHomeSurface::IssueCard => app_surface_card( + div() + .w_full() + .flex() + .flex_col() + .items_center() + .gap(px(APP_UI_THEME.shells.home_stack_gap_px)) + .child(app_heading_section(app_shared_text( + AppTextKey::MetadataStartupIssue, + ))) + .child(startup_home_body(runtime)), ) .into_any_element(), }), @@ -4222,11 +4244,8 @@ fn startup_home_title(surface: StartupHomeSurface) -> impl IntoElement { }; div() - .text_size(px(APP_UI_THEME.foundation.typography.startup_title_text_px)) - .font_weight(gpui::FontWeight::NORMAL) - .text_color(rgb(APP_UI_THEME.foundation.text.primary)) .text_center() - .child(app_shared_text(title_key)) + .child(app_heading_view(app_shared_text(title_key))) .with_animation( animation_id, Animation::new(Duration::from_millis(180)), @@ -4246,15 +4265,6 @@ fn startup_home_tagline() -> impl IntoElement { .child(app_shared_text(AppTextKey::HomeSetupTagline)) } -fn startup_home_support_text(body: impl Into<SharedString>) -> impl IntoElement { - div() - .text_size(px(APP_UI_THEME.foundation.typography.body_text_px)) - .line_height(relative(1.2)) - .text_color(rgb(APP_UI_THEME.foundation.text.secondary)) - .text_center() - .child(body.into()) -} - fn startup_signer_entry_surface( signer_entry: Option<&StartupSignerEntryState>, connect_state: &StartupSignerConnectState, @@ -4299,39 +4309,61 @@ fn startup_signer_entry_surface( ) }) .when_some(preview.as_ref().ok(), |this, preview| { - this.child(startup_home_card( - app_shared_text(AppTextKey::HomeSetupSignerReviewTitle), - label_value_list([ - LabelValueRow::new( - app_shared_text(AppTextKey::HomeSetupSignerSourceLabel), - preview.source_label.clone(), - ), - LabelValueRow::new( - app_shared_text(AppTextKey::HomeSetupSignerSignerLabel), - preview.signer_npub.clone(), - ), - LabelValueRow::new( - app_shared_text(AppTextKey::HomeSetupSignerRelaysLabel), - preview.relays_label.clone(), - ), - LabelValueRow::new( - app_shared_text(AppTextKey::HomeSetupSignerPermissionsLabel), - preview.permissions_label.clone(), - ), - ]), + this.child(app_surface_card( + div() + .w_full() + .flex() + .flex_col() + .items_center() + .gap(px(APP_UI_THEME.shells.home_stack_gap_px)) + .child(app_heading_section(app_shared_text( + AppTextKey::HomeSetupSignerReviewTitle, + ))) + .child(label_value_list([ + LabelValueRow::new( + app_shared_text(AppTextKey::HomeSetupSignerSourceLabel), + preview.source_label.clone(), + ), + LabelValueRow::new( + app_shared_text(AppTextKey::HomeSetupSignerSignerLabel), + preview.signer_npub.clone(), + ), + LabelValueRow::new( + app_shared_text(AppTextKey::HomeSetupSignerRelaysLabel), + preview.relays_label.clone(), + ), + LabelValueRow::new( + app_shared_text(AppTextKey::HomeSetupSignerPermissionsLabel), + preview.permissions_label.clone(), + ), + ])), )) }) .when_some(startup_signer_status_spec(connect_state), |this, status| { - this.child(startup_home_card( - app_shared_text(status.0), - status - .1 - .map(|body| startup_home_support_text(body).into_any_element()) - .unwrap_or_else(|| div().into_any_element()), + this.child(app_surface_card( + div() + .w_full() + .flex() + .flex_col() + .items_center() + .gap(px(APP_UI_THEME.shells.home_stack_gap_px)) + .child(app_heading_section(app_shared_text(status.0))) + .child( + status + .1 + .map(|body| { + div() + .w_full() + .text_center() + .child(home_body_text(body)) + .into_any_element() + }) + .unwrap_or_else(|| div().into_any_element()), + ), )) }) .when_some(parse_error, |this, error| { - this.child(startup_home_support_text(error)) + this.child(div().w_full().text_center().child(home_body_text(error))) }) .child(if submit_enabled { action_button_primary( @@ -4356,7 +4388,12 @@ fn startup_signer_entry_surface( cx, )) .when_some(startup_notice, |this, notice| { - this.child(startup_home_support_text(notice.to_owned())) + this.child( + div() + .w_full() + .text_center() + .child(home_body_text(notice.to_owned())), + ) }) } @@ -4499,47 +4536,13 @@ fn startup_text_button( ) } -fn startup_home_card(title: impl Into<SharedString>, body: impl IntoElement) -> impl IntoElement { - div() - .w_full() - .bg(rgb(APP_UI_THEME.foundation.surfaces.card_background)) - .rounded(px(APP_UI_THEME - .controls - .action_button - .sizing - .corner_radius_px)) - .child( - div() - .w_full() - .p(px(APP_UI_THEME.shells.home_card_padding_px)) - .flex() - .flex_col() - .items_center() - .gap(px(APP_UI_THEME.shells.home_stack_gap_px)) - .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.into()), - ) - .child(body), - ) -} - fn startup_home_body(runtime: &DesktopAppRuntimeSummary) -> impl IntoElement { let body = runtime .startup_issue .clone() .unwrap_or_else(|| app_shared_text(AppTextKey::HomeTodayEmptySetupBody).to_string()); - div() - .w_full() - .text_size(px(APP_UI_THEME.foundation.typography.body_text_px)) - .line_height(relative(1.2)) - .text_color(rgb(APP_UI_THEME.foundation.text.secondary)) - .text_center() - .child(body) + div().w_full().text_center().child(home_body_text(body)) } async fn connect_default_relay(relay_url: String) -> Result<RadrootsNostrClient, String> { @@ -4629,84 +4632,68 @@ fn home_sidebar( let selected_section = selected_farmer_section(runtime); let products_available = farmer_products_available(runtime); - div() - .h_full() - .w(px(APP_UI_THEME.shells.home_sidebar_width_px)) - .bg(rgb(APP_UI_THEME.foundation.surfaces.card_background)) - .p(px(APP_UI_THEME.shells.home_window_padding_px)) - .flex() - .flex_col() - .justify_between() - .child( - div() - .flex_1() - .flex() - .flex_col() - .justify_start() - .gap(px(APP_UI_THEME.shells.home_stack_gap_px)) - .child(home_sidebar_nav_button( - "home-nav-today", - AppTextKey::HomeNavToday, - selected_section == FarmerSection::Today, - on_select_today, - cx, - )) - .when(products_available, |this| { - this.child(home_sidebar_nav_button( - "home-nav-products", - AppTextKey::HomeNavProducts, - selected_section == FarmerSection::Products, - on_select_products, + app_surface_sidebar( + div() + .h_full() + .w(px(APP_UI_THEME.shells.home_sidebar_width_px)) + .p(px(APP_UI_THEME.shells.home_window_padding_px)) + .flex() + .flex_col() + .justify_between() + .child( + div() + .flex_1() + .flex() + .flex_col() + .justify_start() + .gap(px(APP_UI_THEME.shells.home_stack_gap_px)) + .child(home_sidebar_nav_button( + "home-nav-today", + AppTextKey::HomeNavToday, + selected_section == FarmerSection::Today, + on_select_today, cx, )) - }), - ) - .child( - div().child( - div() - .text_size(px(APP_UI_THEME.foundation.typography.body_text_px)) - .line_height(relative(1.2)) - .text_color(rgb(APP_UI_THEME.foundation.text.secondary)) - .when_some(home_saved_farm(runtime), |this, farm| { - this.child(farm.display_name.clone()) + .when(products_available, |this| { + this.child(home_sidebar_nav_button( + "home-nav-products", + AppTextKey::HomeNavProducts, + selected_section == FarmerSection::Products, + on_select_products, + cx, + )) }), + ) + .child( + div().child(div().when_some(home_saved_farm(runtime), |this, farm| { + this.child(home_body_text(farm.display_name.clone())) + })), ), - ) + ) } fn holding_home_sidebar(runtime: &DesktopAppRuntimeSummary) -> impl IntoElement { - div() - .h_full() - .w(px(APP_UI_THEME.shells.home_sidebar_width_px)) - .bg(rgb(APP_UI_THEME.foundation.surfaces.card_background)) - .p(px(APP_UI_THEME.shells.home_window_padding_px)) - .flex() - .flex_col() - .justify_between() - .child( - div() - .flex() - .flex_col() - .gap(px(APP_UI_THEME.shells.home_stack_gap_px)) - .child( - div() - .text_size(px(APP_UI_THEME.foundation.typography.body_text_px * 2.0)) - .font_weight(gpui::FontWeight::BOLD) - .text_color(rgb(APP_UI_THEME.foundation.text.primary)) - .child(app_shared_text(AppTextKey::HomeTodayTitle)), - ), - ) - .child( - div().child( + app_surface_sidebar( + div() + .h_full() + .w(px(APP_UI_THEME.shells.home_sidebar_width_px)) + .p(px(APP_UI_THEME.shells.home_window_padding_px)) + .flex() + .flex_col() + .justify_between() + .child( div() - .text_size(px(APP_UI_THEME.foundation.typography.body_text_px)) - .line_height(relative(1.2)) - .text_color(rgb(APP_UI_THEME.foundation.text.secondary)) - .when_some(home_saved_farm(runtime), |this, farm| { - this.child(farm.display_name.clone()) - }), + .flex() + .flex_col() + .gap(px(APP_UI_THEME.shells.home_stack_gap_px)) + .child(app_text_value(app_shared_text(AppTextKey::HomeTodayTitle))), + ) + .child( + div().child(div().when_some(home_saved_farm(runtime), |this, farm| { + this.child(home_body_text(farm.display_name.clone())) + })), ), - ) + ) } fn home_today_content( @@ -5275,10 +5262,9 @@ fn products_row_open_button( .active(selected_background.into()), ) .rounded(ButtonRounded::Size(px(APP_UI_THEME - .controls - .action_button - .sizing - .corner_radius_px))) + .foundation + .radii + .medium_px))) .flex_1() .min_w_0() .on_click(on_click) @@ -5375,7 +5361,7 @@ fn products_row_action_button( cx: &App, ) -> impl IntoElement { let sizing = APP_UI_THEME.components.app_button.sizing; - let colors = APP_UI_THEME.components.app_button.colors; + let colors = APP_UI_THEME.components.app_button.secondary_colors; Button::new(id) .custom( @@ -5415,11 +5401,7 @@ fn products_stock_editor_card( div() .w_full() .bg(rgb(APP_UI_THEME.foundation.surfaces.window_background)) - .rounded(px(APP_UI_THEME - .controls - .action_button - .sizing - .corner_radius_px)) + .rounded(px(APP_UI_THEME.foundation.radii.medium_px)) .p(px(16.0)) .flex() .flex_col() @@ -5664,7 +5646,9 @@ fn products_editor_text_field( .flex_col() .items_start() .gap(px(8.0)) - .child(home_farm_setup_field_label(field_label_key)) + .child(home_farm_setup_field_label(app_shared_text( + field_label_key, + ))) .child(app_text_input(input, false).w_full()) .when_some(validation_key, |this, validation_key| { this.child(home_body_text(app_shared_text(validation_key))) @@ -5685,9 +5669,9 @@ fn products_editor_status_section( .flex_col() .items_start() .gap(px(8.0)) - .child(home_farm_setup_field_label( + .child(home_farm_setup_field_label(app_shared_text( AppTextKey::ProductsEditorFieldStatus, - )) + ))) .child( div() .w_full() @@ -5741,9 +5725,9 @@ fn products_editor_publish_readiness_section( .flex_col() .items_start() .gap(px(8.0)) - .child(home_farm_setup_field_label( + .child(home_farm_setup_field_label(app_shared_text( AppTextKey::ProductsEditorPublishReadinessTitle, - )) + ))) .child(if blockers.is_empty() { home_body_text(app_shared_text(AppTextKey::ProductsEditorReady)).into_any_element() } else { @@ -6014,26 +5998,16 @@ fn home_farm_setup_text_field( input: &Entity<InputState>, blocker_key: Option<AppTextKey>, ) -> impl IntoElement { - div() - .w_full() - .flex() - .flex_col() - .items_start() - .gap(px(8.0)) - .child(home_farm_setup_section_label(section_key)) - .child( - div() - .w_full() - .flex() - .flex_col() - .items_start() - .gap(px(6.0)) - .child(home_farm_setup_field_label(field_label_key)) - .child(app_text_input(input, false).w_full().into_any_element()) - .when_some(blocker_key, |this, blocker_key| { - this.child(home_farm_setup_blocker(blocker_key)) - }), - ) + app_form_section( + app_shared_text(section_key), + app_form_field( + AppFormFieldSpec::new( + app_shared_text(field_label_key), + blocker_key.map(app_shared_text), + ), + app_text_input(input, false).w_full(), + ), + ) } fn home_farm_setup_order_method_section( @@ -6044,76 +6018,52 @@ fn home_farm_setup_order_method_section( on_shipping_change: impl Fn(&bool, &mut Window, &mut App) + 'static, cx: &App, ) -> impl IntoElement { - div() - .w_full() - .flex() - .flex_col() - .items_start() - .gap(px(8.0)) - .child(home_farm_setup_section_label( - AppTextKey::HomeFarmSetupSectionOrderMethods, - )) - .child( - div() - .w_full() - .flex() - .flex_col() - .items_start() - .gap(px(8.0)) - .child(app_checkbox_field( - AppCheckboxFieldSpec::new( - "home-farm-setup-pickup", - app_shared_text(AppTextKey::HomeFarmSetupOrderMethodPickup), - Option::<SharedString>::None, - ), - form.draft.order_methods.contains(&FarmOrderMethod::Pickup), - cx, - move |checked, window, cx| on_pickup_change(&checked, window, cx), - )) - .child(app_checkbox_field( - AppCheckboxFieldSpec::new( - "home-farm-setup-delivery", - app_shared_text(AppTextKey::HomeFarmSetupOrderMethodDelivery), - Option::<SharedString>::None, - ), - form.draft - .order_methods - .contains(&FarmOrderMethod::Delivery), - cx, - move |checked, window, cx| on_delivery_change(&checked, window, cx), - )) - .child(app_checkbox_field( - AppCheckboxFieldSpec::new( - "home-farm-setup-shipping", - app_shared_text(AppTextKey::HomeFarmSetupOrderMethodShipping), - Option::<SharedString>::None, - ), - form.draft - .order_methods - .contains(&FarmOrderMethod::Shipping), - cx, - 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)) - }), - ) -} - -fn home_farm_setup_section_label(key: AppTextKey) -> impl IntoElement { - div() - .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(key)) -} - -fn home_farm_setup_field_label(key: AppTextKey) -> impl IntoElement { - div() - .text_size(px(APP_UI_THEME.foundation.typography.body_text_px)) - .font_weight(gpui::FontWeight::MEDIUM) - .text_color(rgb(APP_UI_THEME.foundation.text.primary)) - .child(app_shared_text(key)) + app_form_section( + app_shared_text(AppTextKey::HomeFarmSetupSectionOrderMethods), + div() + .w_full() + .flex() + .flex_col() + .items_start() + .gap(px(8.0)) + .child(app_checkbox_field( + AppCheckboxFieldSpec::new( + "home-farm-setup-pickup", + app_shared_text(AppTextKey::HomeFarmSetupOrderMethodPickup), + Option::<SharedString>::None, + ), + form.draft.order_methods.contains(&FarmOrderMethod::Pickup), + cx, + move |checked, window, cx| on_pickup_change(&checked, window, cx), + )) + .child(app_checkbox_field( + AppCheckboxFieldSpec::new( + "home-farm-setup-delivery", + app_shared_text(AppTextKey::HomeFarmSetupOrderMethodDelivery), + Option::<SharedString>::None, + ), + form.draft + .order_methods + .contains(&FarmOrderMethod::Delivery), + cx, + move |checked, window, cx| on_delivery_change(&checked, window, cx), + )) + .child(app_checkbox_field( + AppCheckboxFieldSpec::new( + "home-farm-setup-shipping", + app_shared_text(AppTextKey::HomeFarmSetupOrderMethodShipping), + Option::<SharedString>::None, + ), + form.draft + .order_methods + .contains(&FarmOrderMethod::Shipping), + cx, + 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)) + }), + ) } fn home_farm_setup_blocker(key: AppTextKey) -> impl IntoElement { @@ -6147,13 +6097,11 @@ fn settings_panel_farm_context(runtime: &DesktopAppRuntimeSummary) -> Option<(St } fn settings_text_field(label_key: AppTextKey, input: &Entity<InputState>) -> impl IntoElement { - div() - .w_full() - .flex() - .flex_col() - .gap(px(6.0)) - .child(home_farm_setup_field_label(label_key)) - .child(app_text_input(input, false).w_full().into_any_element()) + app_form_input_text( + AppFormFieldSpec::new(app_shared_text(label_key), Option::<SharedString>::None), + input, + false, + ) } fn settings_pickup_location_title( @@ -6191,7 +6139,10 @@ fn settings_pickup_location_card( .items_center() .gap(px(8.0)) .child(if pickup_location.is_default { - settings_badge_text(AppTextKey::SettingsPickupLocationsDefaultBadge).into_any_element() + settings_badge_text(app_shared_text( + AppTextKey::SettingsPickupLocationsDefaultBadge, + )) + .into_any_element() } else { settings_dynamic_action_button( ("settings-farm-default-pickup", index), @@ -6218,11 +6169,7 @@ fn settings_pickup_location_card( div() .w_full() .bg(rgb(APP_UI_THEME.foundation.surfaces.chrome_background)) - .rounded(px(APP_UI_THEME - .controls - .action_button - .sizing - .corner_radius_px)) + .rounded(px(APP_UI_THEME.foundation.radii.medium_px)) .p(px(12.0)) .flex() .flex_col() @@ -6320,11 +6267,7 @@ fn settings_fulfillment_window_card( div() .w_full() .bg(rgb(APP_UI_THEME.foundation.surfaces.chrome_background)) - .rounded(px(APP_UI_THEME - .controls - .action_button - .sizing - .corner_radius_px)) + .rounded(px(APP_UI_THEME.foundation.radii.medium_px)) .p(px(12.0)) .flex() .flex_col() @@ -6362,24 +6305,18 @@ fn settings_fulfillment_window_card( AppTextKey::SettingsFulfillmentWindowsFieldLabel, &fulfillment_window.label_input, )) - .child( + .child(app_form_field( + AppFormFieldSpec::new( + app_shared_text(AppTextKey::SettingsFulfillmentWindowsFieldPickupLocation), + Option::<SharedString>::None, + ), div() .w_full() .flex() - .flex_col() - .gap(px(6.0)) - .child(home_farm_setup_field_label( - AppTextKey::SettingsFulfillmentWindowsFieldPickupLocation, - )) - .child( - div() - .w_full() - .flex() - .flex_wrap() - .gap(px(8.0)) - .children(pickup_location_options), - ), - ) + .flex_wrap() + .gap(px(8.0)) + .children(pickup_location_options), + )) .child(settings_text_field( AppTextKey::SettingsFulfillmentWindowsFieldStartsAt, &fulfillment_window.starts_at_input, @@ -6405,11 +6342,7 @@ fn settings_blackout_period_card( div() .w_full() .bg(rgb(APP_UI_THEME.foundation.surfaces.chrome_background)) - .rounded(px(APP_UI_THEME - .controls - .action_button - .sizing - .corner_radius_px)) + .rounded(px(APP_UI_THEME.foundation.radii.medium_px)) .p(px(12.0)) .flex() .flex_col() @@ -6502,14 +6435,6 @@ fn settings_timing_conflict_key(kind: FarmTimingConflictKind) -> AppTextKey { } } -fn settings_badge_text(key: AppTextKey) -> impl IntoElement { - div() - .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.accent)) - .child(app_shared_text(key)) -} - fn settings_dynamic_action_button( id: impl Into<gpui::ElementId>, label: impl Into<SharedString>, @@ -6521,7 +6446,7 @@ fn settings_dynamic_action_button( let colors = if is_primary { APP_UI_THEME.components.app_button.primary_colors } else { - APP_UI_THEME.components.app_button.colors + APP_UI_THEME.components.app_button.secondary_colors }; let hover_background = if colors.hover_changes_background { colors.hover_background @@ -6598,17 +6523,12 @@ fn settings_inventory_card(spec: SettingsInventorySectionSpec) -> impl IntoEleme } fn settings_inventory_field_row(key: AppTextKey) -> impl IntoElement { - div() - .w_full() - .bg(rgb(APP_UI_THEME.foundation.surfaces.chrome_background)) - .rounded(px(APP_UI_THEME - .controls - .action_button - .sizing - .corner_radius_px)) - .px(px(12.0)) - .py(px(10.0)) - .child(home_farm_setup_field_label(key)) + 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> { @@ -6647,42 +6567,6 @@ fn home_saved_farm_summary_card(runtime: &DesktopAppRuntimeSummary) -> Option<An ) } -fn home_card(title: impl Into<SharedString>, body: impl IntoElement) -> impl IntoElement { - div() - .w_full() - .bg(rgb(APP_UI_THEME.foundation.surfaces.card_background)) - .rounded(px(APP_UI_THEME - .controls - .action_button - .sizing - .corner_radius_px)) - .child( - div() - .w_full() - .p(px(APP_UI_THEME.shells.home_card_padding_px)) - .flex() - .flex_col() - .gap(px(APP_UI_THEME.shells.home_stack_gap_px)) - .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.into()), - ) - .child(body), - ) -} - -fn home_body_text(body: impl Into<SharedString>) -> impl IntoElement { - div() - .w_full() - .text_size(px(APP_UI_THEME.foundation.typography.body_text_px)) - .line_height(relative(1.2)) - .text_color(rgb(APP_UI_THEME.foundation.text.secondary)) - .child(body.into()) -} - fn home_status_row(status: &HomeStatusPresentation) -> impl IntoElement { div() .flex() @@ -6724,11 +6608,7 @@ fn home_summary_metric(label_key: AppTextKey, value: u32) -> impl IntoElement { .flex_1() .min_w_0() .bg(rgb(APP_UI_THEME.foundation.surfaces.window_background)) - .rounded(px(APP_UI_THEME - .controls - .action_button - .sizing - .corner_radius_px)) + .rounded(px(APP_UI_THEME.foundation.radii.medium_px)) .p(px(16.0)) .flex() .flex_col() diff --git a/crates/shared/ui/src/lib.rs b/crates/shared/ui/src/lib.rs @@ -5,10 +5,13 @@ mod text; mod theme; pub use primitives::{ - AppCheckboxFieldSpec, AppSegmentButtonIconSpec, LabelValueRow, app_button_compact, - app_button_icon, app_button_primary, app_button_primary_disabled, app_button_secondary, - app_checkbox_field, app_divider, app_input_text, app_segment_button_icon, app_status_indicator, - app_surface_card, app_surface_window, label_value_list, utility_title_row, + AppCheckboxFieldSpec, AppFormFieldSpec, AppSegmentButtonIconSpec, LabelValueRow, + app_button_compact, app_button_icon, app_button_primary, app_button_primary_disabled, + app_button_secondary, app_checkbox_field, app_divider, app_form_field, app_form_input_text, + app_form_section, app_heading_section, app_heading_view, app_input_text, + app_segment_button_icon, 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 @@ -53,6 +53,20 @@ impl AppCheckboxFieldSpec { } } +pub struct AppFormFieldSpec { + pub label: SharedString, + pub note: Option<SharedString>, +} + +impl AppFormFieldSpec { + pub fn new(label: impl Into<SharedString>, note: Option<impl Into<SharedString>>) -> Self { + Self { + label: label.into(), + note: note.map(Into::into), + } + } +} + #[derive(Clone, Debug, Eq, PartialEq)] pub struct LabelValueRow { pub label: SharedString, @@ -77,23 +91,41 @@ pub fn app_surface_window(background: u32, content: impl IntoElement) -> impl In .child(content) } +pub fn app_surface_sidebar(content: impl IntoElement) -> impl IntoElement { + div() + .h_full() + .bg(rgb(APP_UI_THEME.foundation.surfaces.card_background)) + .child(content) +} + +pub fn app_surface_panel(content: impl IntoElement) -> impl IntoElement { + div() + .w_full() + .bg(rgb(APP_UI_THEME.foundation.surfaces.chrome_background)) + .rounded(px(APP_UI_THEME.foundation.radii.medium_px)) + .child(content) +} + pub fn app_surface_card(content: impl IntoElement) -> impl IntoElement { div() .w_full() - .h_full() - .max_w(px(APP_UI_THEME.shells.home_card_max_width_px)) - .mx_auto() .bg(rgb(APP_UI_THEME.foundation.surfaces.card_background)) - .overflow_hidden() + .rounded(px(APP_UI_THEME.foundation.radii.medium_px)) .child( div() - .size_full() - .overflow_hidden() + .w_full() .p(px(APP_UI_THEME.shells.home_card_padding_px)) .child(content), ) } +pub fn app_surface_card_section( + title: impl Into<SharedString>, + body: impl IntoElement, +) -> impl IntoElement { + app_surface_card(app_form_section(title, body)) +} + pub fn app_divider() -> impl IntoElement { div() .w_full() @@ -101,6 +133,69 @@ pub fn app_divider() -> impl IntoElement { .bg(rgb(APP_UI_THEME.foundation.surfaces.divider)) } +pub fn app_heading_view(content: impl Into<SharedString>) -> impl IntoElement { + div() + .w_full() + .text_size(px(APP_UI_THEME.foundation.typography.startup_title_text_px)) + .font_weight(gpui::FontWeight::NORMAL) + .text_color(rgb(APP_UI_THEME.foundation.text.primary)) + .child(content.into()) +} + +pub fn app_heading_section(content: impl Into<SharedString>) -> impl IntoElement { + div() + .w_full() + .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(content.into()) +} + +pub fn app_text_body(content: impl Into<SharedString>) -> impl IntoElement { + div() + .w_full() + .text_size(px(APP_UI_THEME.foundation.typography.body_text_px)) + .line_height(relative(1.2)) + .text_color(rgb(APP_UI_THEME.foundation.text.primary)) + .child(content.into()) +} + +pub fn app_text_body_subtle(content: impl Into<SharedString>) -> impl IntoElement { + div() + .w_full() + .text_size(px(APP_UI_THEME.foundation.typography.body_text_px)) + .line_height(relative(1.2)) + .text_color(rgb(APP_UI_THEME.foundation.text.secondary)) + .child(content.into()) +} + +pub fn app_text_label(content: impl Into<SharedString>) -> impl IntoElement { + div() + .w_full() + .text_size(px(APP_UI_THEME.foundation.typography.body_text_px)) + .font_weight(gpui::FontWeight::MEDIUM) + .text_color(rgb(APP_UI_THEME.foundation.text.primary)) + .child(content.into()) +} + +pub fn app_text_value(content: impl Into<SharedString>) -> impl IntoElement { + div() + .w_full() + .text_size(px(APP_UI_THEME.foundation.typography.body_text_px * 2.0)) + .font_weight(gpui::FontWeight::BOLD) + .text_color(rgb(APP_UI_THEME.foundation.text.primary)) + .child(content.into()) +} + +pub fn app_text_badge(content: impl Into<SharedString>) -> impl IntoElement { + 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.accent)) + .child(content.into()) +} + pub fn utility_title_row(title: impl Into<SharedString>) -> impl IntoElement { div() .w_full() @@ -135,6 +230,42 @@ pub fn label_value_list(rows: impl IntoIterator<Item = LabelValueRow>) -> impl I .children(rows) } +pub fn app_form_section( + title: impl Into<SharedString>, + content: impl IntoElement, +) -> impl IntoElement { + div() + .w_full() + .flex() + .flex_col() + .items_start() + .gap(px(APP_UI_THEME.foundation.spacing.small_px)) + .child(app_heading_section(title)) + .child(content) +} + +pub fn app_form_field(spec: AppFormFieldSpec, field: impl IntoElement) -> impl IntoElement { + div() + .w_full() + .flex() + .flex_col() + .items_start() + .gap(px(APP_UI_THEME.foundation.spacing.tight_px)) + .child(app_text_label(spec.label)) + .child(field) + .when_some(spec.note, |this, note| { + this.child(app_text_body_subtle(note)) + }) +} + +pub fn app_form_input_text( + spec: AppFormFieldSpec, + input: &Entity<InputState>, + disabled: bool, +) -> impl IntoElement { + app_form_field(spec, app_input_text(input, disabled).w_full()) +} + fn app_checkbox( id: &'static str, checked: bool, @@ -508,7 +639,7 @@ fn app_button_disabled_colors(variant: AppButtonVariant) -> crate::AppButtonColo mod tests { use gpui_component::IconName; - use super::{AppCheckboxFieldSpec, AppSegmentButtonIconSpec}; + use super::{AppCheckboxFieldSpec, AppFormFieldSpec, AppSegmentButtonIconSpec}; #[test] fn icon_segment_spec_preserves_id_and_label() { @@ -529,4 +660,15 @@ mod tests { Some("Optional note") ); } + + #[test] + fn form_field_spec_preserves_optional_note() { + let spec = AppFormFieldSpec::new("Farm name", Some("Saved locally")); + + assert_eq!(spec.label.as_ref(), "Farm name"); + assert_eq!( + spec.note.as_ref().map(|note| note.as_ref()), + Some("Saved locally") + ); + } }