From 070f7dbe1a283cc6d765f5fc8a8c3d78ba5ed425 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Thu, 7 Aug 2025 19:01:52 -0300 Subject: [PATCH] onboarding: Add fast-follow adjustments (#35814) Release Notes: - N/A --- assets/images/certified_user_stamp.svg | 1 - assets/images/pro_trial_stamp.svg | 2 +- assets/images/pro_user_stamp.svg | 1 + assets/keymaps/default-linux.json | 10 ++- assets/keymaps/default-macos.json | 10 ++- crates/ai_onboarding/src/ai_upsell_card.rs | 2 +- crates/onboarding/src/ai_setup_page.rs | 65 +++++++++++-------- crates/onboarding/src/basics_page.rs | 12 ++-- crates/onboarding/src/editing_page.rs | 17 +++-- crates/onboarding/src/onboarding.rs | 61 +++++++++-------- .../ui/src/components/button/toggle_button.rs | 44 ++++++++++++- crates/ui/src/components/image.rs | 2 +- 12 files changed, 155 insertions(+), 72 deletions(-) delete mode 100644 assets/images/certified_user_stamp.svg create mode 100644 assets/images/pro_user_stamp.svg diff --git a/assets/images/certified_user_stamp.svg b/assets/images/certified_user_stamp.svg deleted file mode 100644 index 7e65c4fc9d..0000000000 --- a/assets/images/certified_user_stamp.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/images/pro_trial_stamp.svg b/assets/images/pro_trial_stamp.svg index 501de88a48..a3f9095120 100644 --- a/assets/images/pro_trial_stamp.svg +++ b/assets/images/pro_trial_stamp.svg @@ -1 +1 @@ - + diff --git a/assets/images/pro_user_stamp.svg b/assets/images/pro_user_stamp.svg new file mode 100644 index 0000000000..d037a9e833 --- /dev/null +++ b/assets/images/pro_user_stamp.svg @@ -0,0 +1 @@ + diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 567580a9c6..c436b1a8fb 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -1103,6 +1103,13 @@ "ctrl-enter": "menu::Confirm" } }, + { + "context": "OnboardingAiConfigurationModal", + "use_key_equivalents": true, + "bindings": { + "escape": "menu::Cancel" + } + }, { "context": "Diagnostics", "use_key_equivalents": true, @@ -1179,7 +1186,8 @@ "ctrl-1": "onboarding::ActivateBasicsPage", "ctrl-2": "onboarding::ActivateEditingPage", "ctrl-3": "onboarding::ActivateAISetupPage", - "ctrl-escape": "onboarding::Finish" + "ctrl-escape": "onboarding::Finish", + "alt-tab": "onboarding::SignIn" } } ] diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 1c2ad3a006..960bac1479 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -1205,6 +1205,13 @@ "cmd-enter": "menu::Confirm" } }, + { + "context": "OnboardingAiConfigurationModal", + "use_key_equivalents": true, + "bindings": { + "escape": "menu::Cancel" + } + }, { "context": "Diagnostics", "use_key_equivalents": true, @@ -1281,7 +1288,8 @@ "cmd-1": "onboarding::ActivateBasicsPage", "cmd-2": "onboarding::ActivateEditingPage", "cmd-3": "onboarding::ActivateAISetupPage", - "cmd-escape": "onboarding::Finish" + "cmd-escape": "onboarding::Finish", + "alt-tab": "onboarding::SignIn" } } ] diff --git a/crates/ai_onboarding/src/ai_upsell_card.rs b/crates/ai_onboarding/src/ai_upsell_card.rs index 65d3866273..e9639ca075 100644 --- a/crates/ai_onboarding/src/ai_upsell_card.rs +++ b/crates/ai_onboarding/src/ai_upsell_card.rs @@ -137,7 +137,7 @@ impl RenderOnce for AiUpsellCard { .size(rems_from_px(72.)) .child( Vector::new( - VectorName::CertifiedUserStamp, + VectorName::ProUserStamp, rems_from_px(72.), rems_from_px(72.), ) diff --git a/crates/onboarding/src/ai_setup_page.rs b/crates/onboarding/src/ai_setup_page.rs index 6099745c40..00f2d5fc8b 100644 --- a/crates/onboarding/src/ai_setup_page.rs +++ b/crates/onboarding/src/ai_setup_page.rs @@ -1,7 +1,7 @@ use std::sync::Arc; -use ai_onboarding::{AiUpsellCard, SignInStatus}; -use client::UserStore; +use ai_onboarding::AiUpsellCard; +use client::{Client, UserStore}; use fs::Fs; use gpui::{ Action, AnyView, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity, @@ -12,8 +12,8 @@ use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageMod use project::DisableAiSettings; use settings::{Settings, update_settings_file}; use ui::{ - Badge, ButtonLike, Divider, Modal, ModalFooter, ModalHeader, Section, SwitchField, ToggleState, - prelude::*, tooltip_container, + Badge, ButtonLike, Divider, KeyBinding, Modal, ModalFooter, ModalHeader, Section, SwitchField, + ToggleState, prelude::*, tooltip_container, }; use util::ResultExt; use workspace::{ModalView, Workspace}; @@ -88,7 +88,7 @@ fn render_privacy_card(tab_index: &mut isize, disabled: bool, cx: &mut App) -> i h_flex() .gap_2() .justify_between() - .child(Label::new("We don't train models using your data")) + .child(Label::new("Privacy is the default for Zed")) .child( h_flex().gap_1().child(privacy_badge()).child( Button::new("learn_more", "Learn More") @@ -109,7 +109,7 @@ fn render_privacy_card(tab_index: &mut isize, disabled: bool, cx: &mut App) -> i ) .child( Label::new( - "Feel confident in the security and privacy of your projects using Zed.", + "Any use or storage of your data is with your explicit, single-use, opt-in consent.", ) .size(LabelSize::Small) .color(Color::Muted), @@ -240,6 +240,7 @@ fn render_llm_provider_card( pub(crate) fn render_ai_setup_page( workspace: WeakEntity, user_store: Entity, + client: Arc, window: &mut Window, cx: &mut App, ) -> impl IntoElement { @@ -283,15 +284,16 @@ pub(crate) fn render_ai_setup_page( v_flex() .mt_2() .gap_6() - .child(AiUpsellCard { - sign_in_status: SignInStatus::SignedIn, - sign_in: Arc::new(|_, _| {}), - account_too_young: user_store.read(cx).account_too_young(), - user_plan: user_store.read(cx).plan(), - tab_index: Some({ + .child({ + let mut ai_upsell_card = + AiUpsellCard::new(client, &user_store, user_store.read(cx).plan(), cx); + + ai_upsell_card.tab_index = Some({ tab_index += 1; tab_index - 1 - }), + }); + + ai_upsell_card }) .child(render_llm_provider_section( &mut tab_index, @@ -336,6 +338,10 @@ impl AiConfigurationModal { selected_provider, } } + + fn cancel(&mut self, _: &menu::Cancel, cx: &mut Context) { + cx.emit(DismissEvent); + } } impl ModalView for AiConfigurationModal {} @@ -349,11 +355,15 @@ impl Focusable for AiConfigurationModal { } impl Render for AiConfigurationModal { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() + .key_context("OnboardingAiConfigurationModal") .w(rems(34.)) .elevation_3(cx) .track_focus(&self.focus_handle) + .on_action( + cx.listener(|this, _: &menu::Cancel, _window, cx| this.cancel(&menu::Cancel, cx)), + ) .child( Modal::new("onboarding-ai-setup-modal", None) .header( @@ -368,18 +378,19 @@ impl Render for AiConfigurationModal { .section(Section::new().child(self.configuration_view.clone())) .footer( ModalFooter::new().end_slot( - h_flex() - .gap_1() - .child( - Button::new("onboarding-closing-cancel", "Cancel") - .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))), + Button::new("ai-onb-modal-Done", "Done") + .key_binding( + KeyBinding::for_action_in( + &menu::Cancel, + &self.focus_handle.clone(), + window, + cx, + ) + .map(|kb| kb.size(rems_from_px(12.))), ) - .child(Button::new("save-btn", "Done").on_click(cx.listener( - |_, _, window, cx| { - window.dispatch_action(menu::Confirm.boxed_clone(), cx); - cx.emit(DismissEvent); - }, - ))), + .on_click(cx.listener(|this, _event, _window, cx| { + this.cancel(&menu::Cancel, cx) + })), ), ), ) @@ -396,7 +407,7 @@ impl AiPrivacyTooltip { impl Render for AiPrivacyTooltip { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - const DESCRIPTION: &'static str = "One of Zed's most important principles is transparency. This is why we are and value open-source so much. And it wouldn't be any different with AI."; + const DESCRIPTION: &'static str = "We believe in opt-in data sharing as the default for building AI products, rather than opt-out. We'll only use or store your data if you affirmatively send it to us. "; tooltip_container(window, cx, move |this, _, _| { this.child( @@ -407,7 +418,7 @@ impl Render for AiPrivacyTooltip { .size(IconSize::Small) .color(Color::Muted), ) - .child(Label::new("Privacy Principle")), + .child(Label::new("Privacy First")), ) .child( div().max_w_64().child( diff --git a/crates/onboarding/src/basics_page.rs b/crates/onboarding/src/basics_page.rs index a4e4028051..a19a21fddf 100644 --- a/crates/onboarding/src/basics_page.rs +++ b/crates/onboarding/src/basics_page.rs @@ -201,12 +201,15 @@ fn render_telemetry_section(tab_index: &mut isize, cx: &App) -> impl IntoElement let fs = ::global(cx); v_flex() + .pt_6() .gap_4() + .border_t_1() + .border_color(cx.theme().colors().border_variant.opacity(0.5)) .child(Label::new("Telemetry").size(LabelSize::Large)) .child(SwitchField::new( "onboarding-telemetry-metrics", "Help Improve Zed", - Some("Sending anonymous usage data helps us build the right features and create the best experience.".into()), + Some("Anonymous usage data helps us build the right features and improve your experience.".into()), if TelemetrySettings::get_global(cx).metrics { ui::ToggleState::Selected } else { @@ -294,7 +297,7 @@ fn render_base_keymap_section(tab_index: &mut isize, cx: &mut App) -> impl IntoE ToggleButtonWithIcon::new("Emacs", IconName::EditorEmacs, |_, _, cx| { write_keymap_base(BaseKeymap::Emacs, cx); }), - ToggleButtonWithIcon::new("Cursor (Beta)", IconName::EditorCursor, |_, _, cx| { + ToggleButtonWithIcon::new("Cursor", IconName::EditorCursor, |_, _, cx| { write_keymap_base(BaseKeymap::Cursor, cx); }), ], @@ -326,10 +329,7 @@ fn render_vim_mode_switch(tab_index: &mut isize, cx: &mut App) -> impl IntoEleme SwitchField::new( "onboarding-vim-mode", "Vim Mode", - Some( - "Coming from Neovim? Zed's first-class implementation of Vim Mode has got your back." - .into(), - ), + Some("Coming from Neovim? Use our first-class implementation of Vim Mode.".into()), toggle_state, { let fs = ::global(cx); diff --git a/crates/onboarding/src/editing_page.rs b/crates/onboarding/src/editing_page.rs index a8f0265b6b..8b4293db0d 100644 --- a/crates/onboarding/src/editing_page.rs +++ b/crates/onboarding/src/editing_page.rs @@ -584,11 +584,15 @@ fn render_popular_settings_section( window: &mut Window, cx: &mut App, ) -> impl IntoElement { - const LIGATURE_TOOLTIP: &'static str = "Ligatures are when a font creates a special character out of combining two characters into one. For example, with ligatures turned on, =/= would become ≠."; + const LIGATURE_TOOLTIP: &'static str = + "Font ligatures combine two characters into one. For example, turning =/= into ≠."; v_flex() - .gap_5() - .child(Label::new("Popular Settings").size(LabelSize::Large).mt_8()) + .pt_6() + .gap_4() + .border_t_1() + .border_color(cx.theme().colors().border_variant.opacity(0.5)) + .child(Label::new("Popular Settings").size(LabelSize::Large)) .child(render_font_customization_section(tab_index, window, cx)) .child( SwitchField::new( @@ -683,7 +687,10 @@ fn render_popular_settings_section( [ ToggleButtonSimple::new("Auto", |_, _, cx| { write_show_mini_map(ShowMinimap::Auto, cx); - }), + }) + .tooltip(Tooltip::text( + "Show the minimap if the editor's scrollbar is visible.", + )), ToggleButtonSimple::new("Always", |_, _, cx| { write_show_mini_map(ShowMinimap::Always, cx); }), @@ -707,7 +714,7 @@ fn render_popular_settings_section( pub(crate) fn render_editing_page(window: &mut Window, cx: &mut App) -> impl IntoElement { let mut tab_index = 0; v_flex() - .gap_4() + .gap_6() .child(render_import_settings_section(&mut tab_index, cx)) .child(render_popular_settings_section(&mut tab_index, window, cx)) } diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index c4d2b6847c..98f61df97b 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -77,6 +77,8 @@ actions!( ActivateAISetupPage, /// Finish the onboarding process. Finish, + /// Sign in while in the onboarding flow. + SignIn ] ); @@ -376,6 +378,7 @@ impl Onboarding { cx, ) .map(|kb| kb.size(rems_from_px(12.))); + if ai_setup_page { this.child( ButtonLike::new("start_building") @@ -387,14 +390,7 @@ impl Onboarding { .w_full() .justify_between() .child(Label::new("Start Building")) - .child(keybinding.map_or_else( - || { - Icon::new(IconName::Check) - .size(IconSize::Small) - .into_any_element() - }, - IntoElement::into_any_element, - )), + .children(keybinding), ) .on_click(|_, window, cx| { window.dispatch_action(Finish.boxed_clone(), cx); @@ -409,11 +405,10 @@ impl Onboarding { .ml_1() .w_full() .justify_between() - .child(Label::new("Skip All")) - .child(keybinding.map_or_else( - || gpui::Empty.into_any_element(), - IntoElement::into_any_element, - )), + .child( + Label::new("Skip All").color(Color::Muted), + ) + .children(keybinding), ) .on_click(|_, window, cx| { window.dispatch_action(Finish.boxed_clone(), cx); @@ -435,23 +430,39 @@ impl Onboarding { Button::new("sign_in", "Sign In") .full_width() .style(ButtonStyle::Outlined) + .size(ButtonSize::Medium) + .key_binding( + KeyBinding::for_action_in(&SignIn, &self.focus_handle, window, cx) + .map(|kb| kb.size(rems_from_px(12.))), + ) .on_click(|_, window, cx| { - let client = Client::global(cx); - window - .spawn(cx, async move |cx| { - client - .sign_in_with_optional_connect(true, &cx) - .await - .notify_async_err(cx); - }) - .detach(); + window.dispatch_action(SignIn.boxed_clone(), cx); }) .into_any_element() }, ) } + fn on_finish(_: &Finish, _: &mut Window, cx: &mut App) { + go_to_welcome_page(cx); + } + + fn handle_sign_in(_: &SignIn, window: &mut Window, cx: &mut App) { + let client = Client::global(cx); + + window + .spawn(cx, async move |cx| { + client + .sign_in_with_optional_connect(true, &cx) + .await + .notify_async_err(cx); + }) + .detach(); + } + fn render_page(&mut self, window: &mut Window, cx: &mut Context) -> AnyElement { + let client = Client::global(cx); + match self.selected_page { SelectedPage::Basics => crate::basics_page::render_basics_page(cx).into_any_element(), SelectedPage::Editing => { @@ -460,16 +471,13 @@ impl Onboarding { SelectedPage::AiSetup => crate::ai_setup_page::render_ai_setup_page( self.workspace.clone(), self.user_store.clone(), + client, window, cx, ) .into_any_element(), } } - - fn on_finish(_: &Finish, _: &mut Window, cx: &mut App) { - go_to_welcome_page(cx); - } } impl Render for Onboarding { @@ -486,6 +494,7 @@ impl Render for Onboarding { .size_full() .bg(cx.theme().colors().editor_background) .on_action(Self::on_finish) + .on_action(Self::handle_sign_in) .on_action(cx.listener(|this, _: &ActivateBasicsPage, _, cx| { this.set_page(SelectedPage::Basics, cx); })) diff --git a/crates/ui/src/components/button/toggle_button.rs b/crates/ui/src/components/button/toggle_button.rs index 6fbf834667..91defa730b 100644 --- a/crates/ui/src/components/button/toggle_button.rs +++ b/crates/ui/src/components/button/toggle_button.rs @@ -1,6 +1,8 @@ +use std::rc::Rc; + use gpui::{AnyView, ClickEvent}; -use crate::{ButtonLike, ButtonLikeRounding, ElevationIndex, TintColor, prelude::*}; +use crate::{ButtonLike, ButtonLikeRounding, ElevationIndex, TintColor, Tooltip, prelude::*}; /// The position of a [`ToggleButton`] within a group of buttons. #[derive(Debug, PartialEq, Eq, Clone, Copy)] @@ -301,6 +303,7 @@ pub struct ButtonConfiguration { icon: Option, on_click: Box, selected: bool, + tooltip: Option AnyView>>, } mod private { @@ -315,6 +318,7 @@ pub struct ToggleButtonSimple { label: SharedString, on_click: Box, selected: bool, + tooltip: Option AnyView>>, } impl ToggleButtonSimple { @@ -326,6 +330,7 @@ impl ToggleButtonSimple { label: label.into(), on_click: Box::new(on_click), selected: false, + tooltip: None, } } @@ -333,6 +338,11 @@ impl ToggleButtonSimple { self.selected = selected; self } + + pub fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self { + self.tooltip = Some(Rc::new(tooltip)); + self + } } impl private::ToggleButtonStyle for ToggleButtonSimple {} @@ -344,6 +354,7 @@ impl ButtonBuilder for ToggleButtonSimple { icon: None, on_click: self.on_click, selected: self.selected, + tooltip: self.tooltip, } } } @@ -353,6 +364,7 @@ pub struct ToggleButtonWithIcon { icon: IconName, on_click: Box, selected: bool, + tooltip: Option AnyView>>, } impl ToggleButtonWithIcon { @@ -366,6 +378,7 @@ impl ToggleButtonWithIcon { icon, on_click: Box::new(on_click), selected: false, + tooltip: None, } } @@ -373,6 +386,11 @@ impl ToggleButtonWithIcon { self.selected = selected; self } + + pub fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self { + self.tooltip = Some(Rc::new(tooltip)); + self + } } impl private::ToggleButtonStyle for ToggleButtonWithIcon {} @@ -384,6 +402,7 @@ impl ButtonBuilder for ToggleButtonWithIcon { icon: Some(self.icon), on_click: self.on_click, selected: self.selected, + tooltip: self.tooltip, } } } @@ -486,11 +505,13 @@ impl RenderOnce icon, on_click, selected, + tooltip, } = button.into_configuration(); let entry_index = row_index * COLS + col_index; ButtonLike::new((self.group_name, entry_index)) + .rounding(None) .when_some(self.tab_index, |this, tab_index| { this.tab_index(tab_index + entry_index as isize) }) @@ -498,7 +519,6 @@ impl RenderOnce this.toggle_state(true) .selected_style(ButtonStyle::Tinted(TintColor::Accent)) }) - .rounding(None) .when(self.style == ToggleButtonGroupStyle::Filled, |button| { button.style(ButtonStyle::Filled) }) @@ -527,6 +547,9 @@ impl RenderOnce |this| this.color(Color::Accent), )), ) + .when_some(tooltip, |this, tooltip| { + this.tooltip(move |window, cx| tooltip(window, cx)) + }) .on_click(on_click) .into_any_element() }) @@ -920,6 +943,23 @@ impl Component ), ], )]) + .children(vec![single_example( + "With Tooltips", + ToggleButtonGroup::single_row( + "with_tooltips", + [ + ToggleButtonSimple::new("First", |_, _, _| {}) + .tooltip(Tooltip::text("This is a tooltip. Hello!")), + ToggleButtonSimple::new("Second", |_, _, _| {}) + .tooltip(Tooltip::text("This is a tooltip. Hey?")), + ToggleButtonSimple::new("Third", |_, _, _| {}) + .tooltip(Tooltip::text("This is a tooltip. Get out of here now!")), + ], + ) + .selected_index(1) + .button_width(rems_from_px(100.)) + .into_any_element(), + )]) .into_any_element(), ) } diff --git a/crates/ui/src/components/image.rs b/crates/ui/src/components/image.rs index 18f804abe9..09c3bbeb94 100644 --- a/crates/ui/src/components/image.rs +++ b/crates/ui/src/components/image.rs @@ -14,10 +14,10 @@ use crate::prelude::*; #[strum(serialize_all = "snake_case")] pub enum VectorName { AiGrid, - CertifiedUserStamp, DebuggerGrid, Grid, ProTrialStamp, + ProUserStamp, ZedLogo, ZedXCopilot, }