Add "Book Onboarding" action across the app (#20503)

Danilo Leal created

This PR adds a small UI touch-up to the welcome page so we can introduce
the "Book Onboarding" over there, as well as adding it to the user menu
(both in the signed in and signed out states). The actual URL these
buttons take to will still be updated to the correct destination.

<img width="700" alt="Screenshot 2024-11-12 at 12 45 27"
src="https://github.com/user-attachments/assets/9933bf94-f57a-43e2-8da3-bfbfd9fd24d0">

Release Notes:

- N/A

Change summary

assets/icons/blocks.svg                  |   1 
assets/icons/keyboard.svg                |   1 
assets/icons/phone_incoming.svg          |   1 
assets/icons/swatch_book.svg             |   1 
crates/title_bar/src/title_bar.rs        |  18 +
crates/ui/src/components/context_menu.rs |  15 
crates/ui/src/components/icon.rs         |   4 
crates/welcome/src/welcome.rs            | 306 ++++++++++++++++---------
8 files changed, 235 insertions(+), 112 deletions(-)

Detailed changes

assets/icons/blocks.svg πŸ”—

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-blocks"><rect width="7" height="7" x="14" y="3" rx="1"/><path d="M10 21V8a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1H3"/></svg>

assets/icons/keyboard.svg πŸ”—

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-keyboard"><path d="M10 8h.01"/><path d="M12 12h.01"/><path d="M14 8h.01"/><path d="M16 12h.01"/><path d="M18 8h.01"/><path d="M6 8h.01"/><path d="M7 16h10"/><path d="M8 12h.01"/><rect width="20" height="16" x="2" y="4" rx="2"/></svg>

assets/icons/phone_incoming.svg πŸ”—

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-phone-incoming"><polyline points="16 2 16 8 22 8"/><line x1="22" x2="16" y1="2" y2="8"/><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/></svg>

assets/icons/swatch_book.svg πŸ”—

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-swatch-book"><path d="M11 17a4 4 0 0 1-8 0V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2Z"/><path d="M16.7 13H19a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H7"/><path d="M 7 17h.01"/><path d="m11 8 2.3-2.3a2.4 2.4 0 0 1 3.404.004L18.6 7.6a2.4 2.4 0 0 1 .026 3.434L9.9 19.8"/></svg>

crates/title_bar/src/title_bar.rs πŸ”—

@@ -30,6 +30,7 @@ use ui::{
 use util::ResultExt;
 use vcs_menu::{BranchList, OpenRecent as ToggleVcsMenu};
 use workspace::{notifications::NotifyResultExt, Workspace};
+use zed_actions::OpenBrowser;
 
 #[cfg(feature = "stories")]
 pub use stories::*;
@@ -37,6 +38,8 @@ pub use stories::*;
 const MAX_PROJECT_NAME_LENGTH: usize = 40;
 const MAX_BRANCH_NAME_LENGTH: usize = 40;
 
+const BOOK_ONBOARDING: &str = "https://dub.sh/zed-onboarding";
+
 actions!(
     collab,
     [
@@ -580,6 +583,13 @@ impl TitleBar {
                         .action("Themes…", theme_selector::Toggle::default().boxed_clone())
                         .action("Extensions", extensions_ui::Extensions.boxed_clone())
                         .separator()
+                        .link(
+                            "Book Onboarding",
+                            OpenBrowser {
+                                url: BOOK_ONBOARDING.to_string(),
+                            }
+                            .boxed_clone(),
+                        )
                         .action("Sign Out", client::SignOut.boxed_clone())
                     })
                     .into()
@@ -608,6 +618,14 @@ impl TitleBar {
                             .action("Key Bindings", Box::new(zed_actions::OpenKeymap))
                             .action("Themes…", theme_selector::Toggle::default().boxed_clone())
                             .action("Extensions", extensions_ui::Extensions.boxed_clone())
+                            .separator()
+                            .link(
+                                "Book Onboarding",
+                                OpenBrowser {
+                                    url: BOOK_ONBOARDING.to_string(),
+                                }
+                                .boxed_clone(),
+                            )
                     })
                     .into()
                 })

crates/ui/src/components/context_menu.rs πŸ”—

@@ -1,7 +1,7 @@
 #![allow(missing_docs)]
 use crate::{
-    h_flex, prelude::*, utils::WithRemSize, v_flex, Icon, IconName, KeyBinding, Label, List,
-    ListItem, ListSeparator, ListSubHeader,
+    h_flex, prelude::*, utils::WithRemSize, v_flex, Icon, IconName, IconSize, KeyBinding, Label,
+    List, ListItem, ListSeparator, ListSubHeader,
 };
 use gpui::{
     px, Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView,
@@ -20,6 +20,7 @@ enum ContextMenuItem {
         toggle: Option<(IconPosition, bool)>,
         label: SharedString,
         icon: Option<IconName>,
+        icon_size: IconSize,
         handler: Rc<dyn Fn(Option<&FocusHandle>, &mut WindowContext)>,
         action: Option<Box<dyn Action>>,
         disabled: bool,
@@ -103,6 +104,7 @@ impl ContextMenu {
             label: label.into(),
             handler: Rc::new(move |_, cx| handler(cx)),
             icon: None,
+            icon_size: IconSize::Small,
             action,
             disabled: false,
         });
@@ -122,6 +124,7 @@ impl ContextMenu {
             label: label.into(),
             handler: Rc::new(move |_, cx| handler(cx)),
             icon: None,
+            icon_size: IconSize::Small,
             action,
             disabled: false,
         });
@@ -171,6 +174,7 @@ impl ContextMenu {
                 cx.dispatch_action(action.boxed_clone());
             }),
             icon: None,
+            icon_size: IconSize::Small,
             disabled: false,
         });
         self
@@ -193,6 +197,7 @@ impl ContextMenu {
                 cx.dispatch_action(action.boxed_clone());
             }),
             icon: None,
+            icon_size: IconSize::Small,
             disabled: true,
         });
         self
@@ -206,6 +211,7 @@ impl ContextMenu {
             action: Some(action.boxed_clone()),
             handler: Rc::new(move |_, cx| cx.dispatch_action(action.boxed_clone())),
             icon: Some(IconName::ArrowUpRight),
+            icon_size: IconSize::XSmall,
             disabled: false,
         });
         self
@@ -393,6 +399,7 @@ impl Render for ContextMenu {
                                     label,
                                     handler,
                                     icon,
+                                    icon_size,
                                     action,
                                     disabled,
                                 } => {
@@ -403,12 +410,12 @@ impl Render for ContextMenu {
                                     } else {
                                         Color::Default
                                     };
-                                    let label_element = if let Some(icon) = icon {
+                                    let label_element = if let Some(icon_name) = icon {
                                         h_flex()
                                             .gap_1()
                                             .child(Label::new(label.clone()).color(color))
                                             .child(
-                                                Icon::new(*icon).size(IconSize::Small).color(color),
+                                                Icon::new(*icon_name).size(*icon_size).color(color),
                                             )
                                             .into_any_element()
                                     } else {

crates/ui/src/components/icon.rs πŸ”—

@@ -135,6 +135,7 @@ pub enum IconName {
     BellDot,
     BellOff,
     BellRing,
+    Blocks,
     Bolt,
     Book,
     BookCopy,
@@ -202,6 +203,7 @@ pub enum IconName {
     Indicator,
     IndicatorX,
     InlayHint,
+    Keyboard,
     Library,
     LineHeight,
     Link,
@@ -227,6 +229,7 @@ pub enum IconName {
     PocketKnife,
     Public,
     PullRequest,
+    PhoneIncoming,
     Quote,
     RefreshTitle,
     Regex,
@@ -269,6 +272,7 @@ pub enum IconName {
     SupermavenDisabled,
     SupermavenError,
     SupermavenInit,
+    SwatchBook,
     Tab,
     Terminal,
     Trash,

crates/welcome/src/welcome.rs πŸ”—

@@ -26,6 +26,7 @@ actions!(welcome, [ResetHints]);
 
 pub const FIRST_OPEN: &str = "first_open";
 pub const DOCS_URL: &str = "https://zed.dev/docs/";
+const BOOK_ONBOARDING: &str = "https://dub.sh/zed-onboarding";
 
 pub fn init(cx: &mut AppContext) {
     BaseKeymap::register(cx);
@@ -75,125 +76,207 @@ impl Render for WelcomePage {
             .track_focus(&self.focus_handle(cx))
             .child(
                 v_flex()
-                    .w_80()
-                    .gap_6()
+                    .gap_8()
                     .mx_auto()
-                    .child(
-                        svg()
-                            .path("icons/logo_96.svg")
-                            .text_color(cx.theme().colors().icon_disabled)
-                            .w(px(80.))
-                            .h(px(80.))
-                            .mx_auto(),
-                    )
                     .child(
                         v_flex()
-                            .gap_2()
+                            .w_full()
                             .child(
-                                Button::new("choose-theme", "Choose Theme")
-                                    .full_width()
-                                    .on_click(cx.listener(|this, _, cx| {
-                                        this.telemetry.report_app_event(
-                                            "welcome page: change theme".to_string(),
-                                        );
-                                        this.workspace
-                                            .update(cx, |workspace, cx| {
-                                                theme_selector::toggle(
-                                                    workspace,
-                                                    &Default::default(),
-                                                    cx,
-                                                )
-                                            })
-                                            .ok();
-                                    })),
+                                svg()
+                                    .path("icons/logo_96.svg")
+                                    .text_color(cx.theme().colors().icon_disabled)
+                                    .w(px(40.))
+                                    .h(px(40.))
+                                    .mx_auto()
+                                    .mb_4(),
                             )
                             .child(
-                                Button::new("choose-keymap", "Choose Keymap")
-                                    .full_width()
-                                    .on_click(cx.listener(|this, _, cx| {
-                                        this.telemetry.report_app_event(
-                                            "welcome page: change keymap".to_string(),
-                                        );
-                                        this.workspace
-                                            .update(cx, |workspace, cx| {
-                                                base_keymap_picker::toggle(
-                                                    workspace,
-                                                    &Default::default(),
-                                                    cx,
-                                                )
-                                            })
-                                            .ok();
-                                    })),
+                                h_flex()
+                                    .w_full()
+                                    .justify_center()
+                                    .child(Headline::new("Welcome to Zed")),
                             )
                             .child(
-                                Button::new("edit settings", "Edit Settings")
-                                    .full_width()
-                                    .on_click(cx.listener(|this, _, cx| {
-                                        this.telemetry.report_app_event(
-                                            "welcome page: edit settings".to_string(),
-                                        );
-                                        cx.dispatch_action(Box::new(zed_actions::OpenSettings));
-                                    })),
-                            )
-                            .child(Button::new("view docs", "View Docs").full_width().on_click(
-                                cx.listener(|this, _, cx| {
-                                    this.telemetry
-                                        .report_app_event("welcome page: view docs".to_string());
-                                    cx.open_url(DOCS_URL);
-                                }),
-                            )),
+                                h_flex().w_full().justify_center().child(
+                                    Label::new("The editor for what's next")
+                                        .color(Color::Muted)
+                                        .italic(true),
+                                ),
+                            ),
                     )
                     .child(
-                        v_flex()
-                            .gap_2()
-                            .when(cfg!(target_os = "macos"), |el| {
-                                el.child(
-                                    Button::new("install-cli", "Install the CLI")
-                                        .full_width()
-                                        .on_click(cx.listener(|this, _, cx| {
-                                            this.telemetry.report_app_event(
-                                                "welcome page: install cli".to_string(),
-                                            );
-                                            cx.app_mut()
-                                                .spawn(|cx| async move {
-                                                    install_cli::install_cli(&cx).await
-                                                })
-                                                .detach_and_log_err(cx);
-                                        })),
-                                )
-                            })
+                        h_flex()
+                            .items_start()
+                            .gap_8()
                             .child(
-                                Button::new("sign-in-to-copilot", "Sign in to GitHub Copilot")
-                                    .full_width()
-                                    .on_click(cx.listener(|this, _, cx| {
-                                        this.telemetry.report_app_event(
-                                            "welcome page: sign in to copilot".to_string(),
-                                        );
-                                        inline_completion_button::initiate_sign_in(cx);
-                                    })),
+                                v_flex()
+                                    .gap_2()
+                                    .pr_8()
+                                    .border_r_1()
+                                    .border_color(cx.theme().colors().border_variant)
+                                    .child(
+                                        self.section_label(cx).child(
+                                            Label::new("Get Started")
+                                                .size(LabelSize::XSmall)
+                                                .color(Color::Muted),
+                                        ),
+                                    )
+                                    .child(
+                                        Button::new("choose-theme", "Choose a Theme")
+                                            .icon(IconName::SwatchBook)
+                                            .icon_size(IconSize::XSmall)
+                                            .icon_color(Color::Muted)
+                                            .icon_position(IconPosition::Start)
+                                            .on_click(cx.listener(|this, _, cx| {
+                                                this.telemetry.report_app_event(
+                                                    "welcome page: change theme".to_string(),
+                                                );
+                                                this.workspace
+                                                    .update(cx, |workspace, cx| {
+                                                        theme_selector::toggle(
+                                                            workspace,
+                                                            &Default::default(),
+                                                            cx,
+                                                        )
+                                                    })
+                                                    .ok();
+                                            })),
+                                    )
+                                    .child(
+                                        Button::new("choose-keymap", "Choose a Keymap")
+                                            .icon(IconName::Keyboard)
+                                            .icon_size(IconSize::XSmall)
+                                            .icon_color(Color::Muted)
+                                            .icon_position(IconPosition::Start)
+                                            .on_click(cx.listener(|this, _, cx| {
+                                                this.telemetry.report_app_event(
+                                                    "welcome page: change keymap".to_string(),
+                                                );
+                                                this.workspace
+                                                    .update(cx, |workspace, cx| {
+                                                        base_keymap_picker::toggle(
+                                                            workspace,
+                                                            &Default::default(),
+                                                            cx,
+                                                        )
+                                                    })
+                                                    .ok();
+                                            })),
+                                    )
+                                    .child(
+                                        Button::new(
+                                            "sign-in-to-copilot",
+                                            "Sign in to GitHub Copilot",
+                                        )
+                                        .icon(IconName::Copilot)
+                                        .icon_size(IconSize::XSmall)
+                                        .icon_color(Color::Muted)
+                                        .icon_position(IconPosition::Start)
+                                        .on_click(
+                                            cx.listener(|this, _, cx| {
+                                                this.telemetry.report_app_event(
+                                                    "welcome page: sign in to copilot".to_string(),
+                                                );
+                                                inline_completion_button::initiate_sign_in(cx);
+                                            }),
+                                        ),
+                                    )
+                                    .child(
+                                        Button::new("edit settings", "Edit Settings")
+                                            .icon(IconName::Settings)
+                                            .icon_size(IconSize::XSmall)
+                                            .icon_color(Color::Muted)
+                                            .icon_position(IconPosition::Start)
+                                            .on_click(cx.listener(|this, _, cx| {
+                                                this.telemetry.report_app_event(
+                                                    "welcome page: edit settings".to_string(),
+                                                );
+                                                cx.dispatch_action(Box::new(
+                                                    zed_actions::OpenSettings,
+                                                ));
+                                            })),
+                                    ),
                             )
                             .child(
-                                Button::new("explore extensions", "Explore extensions")
-                                    .full_width()
-                                    .on_click(cx.listener(|this, _, cx| {
-                                        this.telemetry.report_app_event(
-                                            "welcome page: open extensions".to_string(),
-                                        );
-                                        cx.dispatch_action(Box::new(extensions_ui::Extensions));
-                                    })),
+                                v_flex()
+                                    .gap_2()
+                                    .child(
+                                        self.section_label(cx).child(
+                                            Label::new("Resources")
+                                                .size(LabelSize::XSmall)
+                                                .color(Color::Muted),
+                                        ),
+                                    )
+                                    .when(cfg!(target_os = "macos"), |el| {
+                                        el.child(
+                                            Button::new("install-cli", "Install the CLI")
+                                                .icon(IconName::Terminal)
+                                                .icon_size(IconSize::XSmall)
+                                                .icon_color(Color::Muted)
+                                                .icon_position(IconPosition::Start)
+                                                .on_click(cx.listener(|this, _, cx| {
+                                                    this.telemetry.report_app_event(
+                                                        "welcome page: install cli".to_string(),
+                                                    );
+                                                    cx.app_mut()
+                                                        .spawn(|cx| async move {
+                                                            install_cli::install_cli(&cx).await
+                                                        })
+                                                        .detach_and_log_err(cx);
+                                                })),
+                                        )
+                                    })
+                                    .child(
+                                        Button::new("view-docs", "View Documentation")
+                                            .icon(IconName::FileCode)
+                                            .icon_size(IconSize::XSmall)
+                                            .icon_color(Color::Muted)
+                                            .icon_position(IconPosition::Start)
+                                            .on_click(cx.listener(|this, _, cx| {
+                                                this.telemetry.report_app_event(
+                                                    "welcome page: view docs".to_string(),
+                                                );
+                                                cx.open_url(DOCS_URL);
+                                            })),
+                                    )
+                                    .child(
+                                        Button::new("explore-extensions", "Explore Extensions")
+                                            .icon(IconName::Blocks)
+                                            .icon_size(IconSize::XSmall)
+                                            .icon_color(Color::Muted)
+                                            .icon_position(IconPosition::Start)
+                                            .on_click(cx.listener(|this, _, cx| {
+                                                this.telemetry.report_app_event(
+                                                    "welcome page: open extensions".to_string(),
+                                                );
+                                                cx.dispatch_action(Box::new(
+                                                    extensions_ui::Extensions,
+                                                ));
+                                            })),
+                                    )
+                                    .child(
+                                        Button::new("book-onboarding", "Book Onboarding")
+                                            .icon(IconName::PhoneIncoming)
+                                            .icon_size(IconSize::XSmall)
+                                            .icon_color(Color::Muted)
+                                            .icon_position(IconPosition::Start)
+                                            .on_click(cx.listener(|_, _, cx| {
+                                                cx.open_url(BOOK_ONBOARDING);
+                                            })),
+                                    ),
                             ),
                     )
                     .child(
                         v_flex()
                             .p_3()
                             .gap_2()
-                            .bg(cx.theme().colors().elevated_surface_background)
+                            .bg(cx.theme().colors().element_background)
                             .border_1()
-                            .border_color(cx.theme().colors().border)
+                            .border_color(cx.theme().colors().border_variant)
                             .rounded_md()
                             .child(CheckboxWithLabel::new(
                                 "enable-vim",
-                                Label::new("Enable vim mode"),
+                                Label::new("Enable Vim Mode"),
                                 if VimModeSetting::get_global(cx).0 {
                                     ui::Selection::Selected
                                 } else {
@@ -210,25 +293,25 @@ impl Render for WelcomePage {
                                 }),
                             ))
                             .child(CheckboxWithLabel::new(
-                                "enable-telemetry",
-                                Label::new("Send anonymous usage data"),
-                                if TelemetrySettings::get_global(cx).metrics {
+                                "enable-crash",
+                                Label::new("Send Crash Reports"),
+                                if TelemetrySettings::get_global(cx).diagnostics {
                                     ui::Selection::Selected
                                 } else {
                                     ui::Selection::Unselected
                                 },
                                 cx.listener(move |this, selection, cx| {
                                     this.telemetry.report_app_event(
-                                        "welcome page: toggle metric telemetry".to_string(),
+                                        "welcome page: toggle diagnostic telemetry".to_string(),
                                     );
                                     this.update_settings::<TelemetrySettings>(selection, cx, {
                                         let telemetry = this.telemetry.clone();
 
                                         move |settings, value| {
-                                            settings.metrics = Some(value);
+                                            settings.diagnostics = Some(value);
 
                                             telemetry.report_setting_event(
-                                                "metric telemetry",
+                                                "diagnostic telemetry",
                                                 value.to_string(),
                                             );
                                         }
@@ -236,25 +319,25 @@ impl Render for WelcomePage {
                                 }),
                             ))
                             .child(CheckboxWithLabel::new(
-                                "enable-crash",
-                                Label::new("Send crash reports"),
-                                if TelemetrySettings::get_global(cx).diagnostics {
+                                "enable-telemetry",
+                                Label::new("Send Telemetry"),
+                                if TelemetrySettings::get_global(cx).metrics {
                                     ui::Selection::Selected
                                 } else {
                                     ui::Selection::Unselected
                                 },
                                 cx.listener(move |this, selection, cx| {
                                     this.telemetry.report_app_event(
-                                        "welcome page: toggle diagnostic telemetry".to_string(),
+                                        "welcome page: toggle metric telemetry".to_string(),
                                     );
                                     this.update_settings::<TelemetrySettings>(selection, cx, {
                                         let telemetry = this.telemetry.clone();
 
                                         move |settings, value| {
-                                            settings.diagnostics = Some(value);
+                                            settings.metrics = Some(value);
 
                                             telemetry.report_setting_event(
-                                                "diagnostic telemetry",
+                                                "metric telemetry",
                                                 value.to_string(),
                                             );
                                         }
@@ -287,6 +370,13 @@ impl WelcomePage {
         this
     }
 
+    fn section_label(&self, cx: &WindowContext) -> Div {
+        div()
+            .pl_1()
+            .font_buffer(cx)
+            .text_color(Color::Muted.color(cx))
+    }
+
     fn update_settings<T: Settings>(
         &mut self,
         selection: &Selection,