diff --git a/assets/images/certified_user_stamp.svg b/assets/images/certified_user_stamp.svg new file mode 100644 index 0000000000..7e65c4fc9d --- /dev/null +++ b/assets/images/certified_user_stamp.svg @@ -0,0 +1 @@ + diff --git a/assets/images/pro_trial_stamp.svg b/assets/images/pro_trial_stamp.svg new file mode 100644 index 0000000000..501de88a48 --- /dev/null +++ b/assets/images/pro_trial_stamp.svg @@ -0,0 +1 @@ + diff --git a/crates/agent_ui/src/ui/end_trial_upsell.rs b/crates/agent_ui/src/ui/end_trial_upsell.rs index 36770c2197..0d9751afec 100644 --- a/crates/agent_ui/src/ui/end_trial_upsell.rs +++ b/crates/agent_ui/src/ui/end_trial_upsell.rs @@ -1,9 +1,9 @@ use std::sync::Arc; -use ai_onboarding::{AgentPanelOnboardingCard, BulletItem}; +use ai_onboarding::{AgentPanelOnboardingCard, PlanDefinitions}; use client::zed_urls; use gpui::{AnyElement, App, IntoElement, RenderOnce, Window}; -use ui::{Divider, List, Tooltip, prelude::*}; +use ui::{Divider, Tooltip, prelude::*}; #[derive(IntoElement, RegisterComponent)] pub struct EndTrialUpsell { @@ -18,6 +18,8 @@ impl EndTrialUpsell { impl RenderOnce for EndTrialUpsell { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + let plan_definitions = PlanDefinitions; + let pro_section = v_flex() .gap_1() .child( @@ -31,13 +33,7 @@ impl RenderOnce for EndTrialUpsell { ) .child(Divider::horizontal()), ) - .child( - List::new() - .child(BulletItem::new("500 prompts with Claude models")) - .child(BulletItem::new( - "Unlimited edit predictions with Zeta, our open-source model", - )), - ) + .child(plan_definitions.pro_plan(false)) .child( Button::new("cta-button", "Upgrade to Zed Pro") .full_width() @@ -68,11 +64,7 @@ impl RenderOnce for EndTrialUpsell { ) .child(Divider::horizontal()), ) - .child( - List::new() - .child(BulletItem::new("50 prompts with the Claude models")) - .child(BulletItem::new("2,000 accepted edit predictions")), - ); + .child(plan_definitions.free_plan()); AgentPanelOnboardingCard::new() .child(Headline::new("Your Zed Pro Trial has expired")) diff --git a/crates/ai_onboarding/src/agent_api_keys_onboarding.rs b/crates/ai_onboarding/src/agent_api_keys_onboarding.rs index e86568fe7a..b55ad4c895 100644 --- a/crates/ai_onboarding/src/agent_api_keys_onboarding.rs +++ b/crates/ai_onboarding/src/agent_api_keys_onboarding.rs @@ -1,8 +1,6 @@ use gpui::{Action, IntoElement, ParentElement, RenderOnce, point}; use language_model::{LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID}; -use ui::{Divider, List, prelude::*}; - -use crate::BulletItem; +use ui::{Divider, List, ListBulletItem, prelude::*}; pub struct ApiKeysWithProviders { configured_providers: Vec<(IconName, SharedString)>, @@ -128,7 +126,7 @@ impl RenderOnce for ApiKeysWithoutProviders { ) .child(Divider::horizontal()), ) - .child(List::new().child(BulletItem::new( + .child(List::new().child(ListBulletItem::new( "Add your own keys to use AI without signing in.", ))) .child( diff --git a/crates/ai_onboarding/src/ai_onboarding.rs b/crates/ai_onboarding/src/ai_onboarding.rs index c252b65f20..9372a33fed 100644 --- a/crates/ai_onboarding/src/ai_onboarding.rs +++ b/crates/ai_onboarding/src/ai_onboarding.rs @@ -3,6 +3,7 @@ mod agent_panel_onboarding_card; mod agent_panel_onboarding_content; mod ai_upsell_card; mod edit_prediction_onboarding_content; +mod plan_definitions; mod young_account_banner; pub use agent_api_keys_onboarding::{ApiKeysWithProviders, ApiKeysWithoutProviders}; @@ -11,51 +12,14 @@ pub use agent_panel_onboarding_content::AgentPanelOnboarding; pub use ai_upsell_card::AiUpsellCard; use cloud_llm_client::Plan; pub use edit_prediction_onboarding_content::EditPredictionOnboarding; +pub use plan_definitions::PlanDefinitions; pub use young_account_banner::YoungAccountBanner; use std::sync::Arc; use client::{Client, UserStore, zed_urls}; -use gpui::{AnyElement, Entity, IntoElement, ParentElement, SharedString}; -use ui::{Divider, List, ListItem, RegisterComponent, TintColor, Tooltip, prelude::*}; - -#[derive(IntoElement)] -pub struct BulletItem { - label: SharedString, -} - -impl BulletItem { - pub fn new(label: impl Into) -> Self { - Self { - label: label.into(), - } - } -} - -impl RenderOnce for BulletItem { - fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement { - let line_height = 0.85 * window.line_height(); - - ListItem::new("list-item") - .selectable(false) - .child( - h_flex() - .w_full() - .min_w_0() - .gap_1() - .items_start() - .child( - h_flex().h(line_height).justify_center().child( - Icon::new(IconName::Dash) - .size(IconSize::XSmall) - .color(Color::Hidden), - ), - ) - .child(div().w_full().min_w_0().child(Label::new(self.label))), - ) - .into_any_element() - } -} +use gpui::{AnyElement, Entity, IntoElement, ParentElement}; +use ui::{Divider, RegisterComponent, TintColor, Tooltip, prelude::*}; #[derive(PartialEq)] pub enum SignInStatus { @@ -130,107 +94,6 @@ impl ZedAiOnboarding { self } - fn free_plan_definition(&self, cx: &mut App) -> impl IntoElement { - v_flex() - .mt_2() - .gap_1() - .child( - h_flex() - .gap_2() - .child( - Label::new("Free") - .size(LabelSize::Small) - .color(Color::Muted) - .buffer_font(cx), - ) - .child( - Label::new("(Current Plan)") - .size(LabelSize::Small) - .color(Color::Custom(cx.theme().colors().text_muted.opacity(0.6))) - .buffer_font(cx), - ) - .child(Divider::horizontal()), - ) - .child( - List::new() - .child(BulletItem::new("50 prompts per month with Claude models")) - .child(BulletItem::new( - "2,000 accepted edit predictions with Zeta, our open-source model", - )), - ) - } - - fn pro_trial_definition(&self) -> impl IntoElement { - List::new() - .child(BulletItem::new("150 prompts with Claude models")) - .child(BulletItem::new( - "Unlimited accepted edit predictions with Zeta, our open-source model", - )) - } - - fn pro_plan_definition(&self, cx: &mut App) -> impl IntoElement { - v_flex().mt_2().gap_1().map(|this| { - if self.account_too_young { - this.child( - h_flex() - .gap_2() - .child( - Label::new("Pro") - .size(LabelSize::Small) - .color(Color::Accent) - .buffer_font(cx), - ) - .child(Divider::horizontal()), - ) - .child( - List::new() - .child(BulletItem::new("500 prompts per month with Claude models")) - .child(BulletItem::new( - "Unlimited accepted edit predictions with Zeta, our open-source model", - )) - .child(BulletItem::new("$20 USD per month")), - ) - .child( - Button::new("pro", "Get Started") - .full_width() - .style(ButtonStyle::Tinted(ui::TintColor::Accent)) - .on_click(move |_, _window, cx| { - telemetry::event!("Upgrade To Pro Clicked", state = "young-account"); - cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx)) - }), - ) - } else { - this.child( - h_flex() - .gap_2() - .child( - Label::new("Pro Trial") - .size(LabelSize::Small) - .color(Color::Accent) - .buffer_font(cx), - ) - .child(Divider::horizontal()), - ) - .child( - List::new() - .child(self.pro_trial_definition()) - .child(BulletItem::new( - "Try it out for 14 days for free, no credit card required", - )), - ) - .child( - Button::new("pro", "Start Free Trial") - .full_width() - .style(ButtonStyle::Tinted(ui::TintColor::Accent)) - .on_click(move |_, _window, cx| { - telemetry::event!("Start Trial Clicked", state = "post-sign-in"); - cx.open_url(&zed_urls::start_trial_url(cx)) - }), - ) - } - }) - } - fn render_accept_terms_of_service(&self) -> AnyElement { v_flex() .gap_1() @@ -269,6 +132,7 @@ impl ZedAiOnboarding { fn render_sign_in_disclaimer(&self, _cx: &mut App) -> AnyElement { let signing_in = matches!(self.sign_in_status, SignInStatus::SigningIn); + let plan_definitions = PlanDefinitions; v_flex() .gap_1() @@ -278,7 +142,7 @@ impl ZedAiOnboarding { .color(Color::Muted) .mb_2(), ) - .child(self.pro_trial_definition()) + .child(plan_definitions.pro_plan(false)) .child( Button::new("sign_in", "Try Zed Pro for Free") .disabled(signing_in) @@ -297,43 +161,132 @@ impl ZedAiOnboarding { fn render_free_plan_state(&self, cx: &mut App) -> AnyElement { let young_account_banner = YoungAccountBanner; + let plan_definitions = PlanDefinitions; - v_flex() - .relative() - .gap_1() - .child(Headline::new("Welcome to Zed AI")) - .map(|this| { - if self.account_too_young { - this.child(young_account_banner) - } else { - this.child(self.free_plan_definition(cx)).when_some( - self.dismiss_onboarding.as_ref(), - |this, dismiss_callback| { - let callback = dismiss_callback.clone(); + if self.account_too_young { + v_flex() + .relative() + .max_w_full() + .gap_1() + .child(Headline::new("Welcome to Zed AI")) + .child(young_account_banner) + .child( + v_flex() + .mt_2() + .gap_1() + .child( + h_flex() + .gap_2() + .child( + Label::new("Pro") + .size(LabelSize::Small) + .color(Color::Accent) + .buffer_font(cx), + ) + .child(Divider::horizontal()), + ) + .child(plan_definitions.pro_plan(true)) + .child( + Button::new("pro", "Get Started") + .full_width() + .style(ButtonStyle::Tinted(ui::TintColor::Accent)) + .on_click(move |_, _window, cx| { + telemetry::event!( + "Upgrade To Pro Clicked", + state = "young-account" + ); + cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx)) + }), + ), + ) + .into_any_element() + } else { + v_flex() + .relative() + .gap_1() + .child(Headline::new("Welcome to Zed AI")) + .child( + v_flex() + .mt_2() + .gap_1() + .child( + h_flex() + .gap_2() + .child( + Label::new("Free") + .size(LabelSize::Small) + .color(Color::Muted) + .buffer_font(cx), + ) + .child( + Label::new("(Current Plan)") + .size(LabelSize::Small) + .color(Color::Custom( + cx.theme().colors().text_muted.opacity(0.6), + )) + .buffer_font(cx), + ) + .child(Divider::horizontal()), + ) + .child(plan_definitions.free_plan()), + ) + .when_some( + self.dismiss_onboarding.as_ref(), + |this, dismiss_callback| { + let callback = dismiss_callback.clone(); - this.child( - h_flex().absolute().top_0().right_0().child( - IconButton::new("dismiss_onboarding", IconName::Close) - .icon_size(IconSize::Small) - .tooltip(Tooltip::text("Dismiss")) - .on_click(move |_, window, cx| { - telemetry::event!( - "Banner Dismissed", - source = "AI Onboarding", - ); - callback(window, cx) - }), - ), - ) - }, - ) - } - }) - .child(self.pro_plan_definition(cx)) - .into_any_element() + this.child( + h_flex().absolute().top_0().right_0().child( + IconButton::new("dismiss_onboarding", IconName::Close) + .icon_size(IconSize::Small) + .tooltip(Tooltip::text("Dismiss")) + .on_click(move |_, window, cx| { + telemetry::event!( + "Banner Dismissed", + source = "AI Onboarding", + ); + callback(window, cx) + }), + ), + ) + }, + ) + .child( + v_flex() + .mt_2() + .gap_1() + .child( + h_flex() + .gap_2() + .child( + Label::new("Pro Trial") + .size(LabelSize::Small) + .color(Color::Accent) + .buffer_font(cx), + ) + .child(Divider::horizontal()), + ) + .child(plan_definitions.pro_trial(true)) + .child( + Button::new("pro", "Start Free Trial") + .full_width() + .style(ButtonStyle::Tinted(ui::TintColor::Accent)) + .on_click(move |_, _window, cx| { + telemetry::event!( + "Start Trial Clicked", + state = "post-sign-in" + ); + cx.open_url(&zed_urls::start_trial_url(cx)) + }), + ), + ) + .into_any_element() + } } fn render_trial_state(&self, _cx: &mut App) -> AnyElement { + let plan_definitions = PlanDefinitions; + v_flex() .relative() .gap_1() @@ -343,13 +296,7 @@ impl ZedAiOnboarding { .color(Color::Muted) .mb_2(), ) - .child( - List::new() - .child(BulletItem::new("150 prompts with Claude models")) - .child(BulletItem::new( - "Unlimited edit predictions with Zeta, our open-source model", - )), - ) + .child(plan_definitions.pro_trial(false)) .when_some( self.dismiss_onboarding.as_ref(), |this, dismiss_callback| { @@ -374,6 +321,8 @@ impl ZedAiOnboarding { } fn render_pro_plan_state(&self, _cx: &mut App) -> AnyElement { + let plan_definitions = PlanDefinitions; + v_flex() .gap_1() .child(Headline::new("Welcome to Zed Pro")) @@ -382,13 +331,7 @@ impl ZedAiOnboarding { .color(Color::Muted) .mb_2(), ) - .child( - List::new() - .child(BulletItem::new("500 prompts with Claude models")) - .child(BulletItem::new( - "Unlimited edit predictions with Zeta, our open-source model", - )), - ) + .child(plan_definitions.pro_plan(false)) .child( Button::new("pro", "Continue with Zed Pro") .full_width() @@ -450,8 +393,9 @@ impl Component for ZedAiOnboarding { Some( v_flex() - .p_4() .gap_4() + .items_center() + .max_w_4_5() .children(vec![ single_example( "Not Signed-in", @@ -462,8 +406,8 @@ impl Component for ZedAiOnboarding { onboarding(SignInStatus::SignedIn, false, None, false), ), single_example( - "Account too young", - onboarding(SignInStatus::SignedIn, false, None, true), + "Young Account", + onboarding(SignInStatus::SignedIn, true, None, true), ), single_example( "Free Plan", diff --git a/crates/ai_onboarding/src/ai_upsell_card.rs b/crates/ai_onboarding/src/ai_upsell_card.rs index 89a782a7c2..a3fea5dce3 100644 --- a/crates/ai_onboarding/src/ai_upsell_card.rs +++ b/crates/ai_onboarding/src/ai_upsell_card.rs @@ -1,11 +1,14 @@ -use std::sync::Arc; +use std::{sync::Arc, time::Duration}; use client::{Client, zed_urls}; use cloud_llm_client::Plan; -use gpui::{AnyElement, App, IntoElement, RenderOnce, Window}; -use ui::{Divider, List, Vector, VectorName, prelude::*}; +use gpui::{ + Animation, AnimationExt, AnyElement, App, IntoElement, RenderOnce, Transformation, Window, + percentage, +}; +use ui::{Divider, Vector, VectorName, prelude::*}; -use crate::{BulletItem, SignInStatus}; +use crate::{SignInStatus, plan_definitions::PlanDefinitions}; #[derive(IntoElement, RegisterComponent)] pub struct AiUpsellCard { @@ -36,6 +39,8 @@ impl AiUpsellCard { impl RenderOnce for AiUpsellCard { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + let plan_definitions = PlanDefinitions; + let pro_section = v_flex() .flex_grow() .w_full() @@ -51,13 +56,7 @@ impl RenderOnce for AiUpsellCard { ) .child(Divider::horizontal()), ) - .child( - List::new() - .child(BulletItem::new("500 prompts with Claude models")) - .child(BulletItem::new( - "Unlimited edit predictions with Zeta, our open-source model", - )), - ); + .child(plan_definitions.pro_plan(false)); let free_section = v_flex() .flex_grow() @@ -74,11 +73,7 @@ impl RenderOnce for AiUpsellCard { ) .child(Divider::horizontal()), ) - .child( - List::new() - .child(BulletItem::new("50 prompts with Claude models")) - .child(BulletItem::new("2,000 accepted edit predictions")), - ); + .child(plan_definitions.free_plan()); let grid_bg = h_flex().absolute().inset_0().w_full().h(px(240.)).child( Vector::new(VectorName::Grid, rems_from_px(500.), rems_from_px(240.)) @@ -101,44 +96,11 @@ impl RenderOnce for AiUpsellCard { ), )); - const DESCRIPTION: &str = "Zed offers a complete agentic experience, with robust editing and reviewing features to collaborate with AI."; + let description = PlanDefinitions::AI_DESCRIPTION; - let footer_buttons = match self.sign_in_status { - SignInStatus::SignedIn => v_flex() - .items_center() - .gap_1() - .child( - Button::new("sign_in", "Start 14-day Free Pro Trial") - .full_width() - .style(ButtonStyle::Tinted(ui::TintColor::Accent)) - .on_click(move |_, _window, cx| { - telemetry::event!("Start Trial Clicked", state = "post-sign-in"); - cx.open_url(&zed_urls::start_trial_url(cx)) - }) - .when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index)), - ) - .child( - Label::new("No credit card required") - .size(LabelSize::Small) - .color(Color::Muted), - ) - .into_any_element(), - _ => Button::new("sign_in", "Sign In") - .full_width() - .style(ButtonStyle::Tinted(ui::TintColor::Accent)) - .when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index)) - .on_click({ - let callback = self.sign_in.clone(); - move |_, window, cx| { - telemetry::event!("Start Trial Clicked", state = "pre-sign-in"); - callback(window, cx) - } - }) - .into_any_element(), - }; - - v_flex() + let card = v_flex() .relative() + .flex_grow() .p_4() .pt_3() .border_1() @@ -146,25 +108,129 @@ impl RenderOnce for AiUpsellCard { .rounded_lg() .overflow_hidden() .child(grid_bg) - .child(gradient_bg) - .child(Label::new("Try Zed AI").size(LabelSize::Large)) + .child(gradient_bg); + + let plans_section = h_flex() + .w_full() + .mt_1p5() + .mb_2p5() + .items_start() + .gap_6() + .child(free_section) + .child(pro_section); + + let footer_container = v_flex().items_center().gap_1(); + + let certified_user_stamp = div() + .absolute() + .top_2() + .right_2() + .size(rems_from_px(72.)) .child( - div() - .max_w_3_4() - .mb_2() - .child(Label::new(DESCRIPTION).color(Color::Muted)), - ) + Vector::new( + VectorName::CertifiedUserStamp, + rems_from_px(72.), + rems_from_px(72.), + ) + .color(Color::Custom(cx.theme().colors().text_accent.alpha(0.3))) + .with_animation( + "loading_stamp", + Animation::new(Duration::from_secs(10)).repeat(), + |this, delta| this.transform(Transformation::rotate(percentage(delta))), + ), + ); + + let pro_trial_stamp = div() + .absolute() + .top_2() + .right_2() + .size(rems_from_px(72.)) .child( - h_flex() - .w_full() - .mt_1p5() - .mb_2p5() - .items_start() - .gap_6() - .child(free_section) - .child(pro_section), - ) - .child(footer_buttons) + Vector::new( + VectorName::ProTrialStamp, + rems_from_px(72.), + rems_from_px(72.), + ) + .color(Color::Custom(cx.theme().colors().text.alpha(0.2))), + ); + + match self.sign_in_status { + SignInStatus::SignedIn => match self.user_plan { + None | Some(Plan::ZedFree) => card + .child(Label::new("Try Zed AI").size(LabelSize::Large)) + .child( + div() + .max_w_3_4() + .mb_2() + .child(Label::new(description).color(Color::Muted)), + ) + .child(plans_section) + .child( + footer_container + .child( + Button::new("start_trial", "Start 14-day Free Pro Trial") + .full_width() + .style(ButtonStyle::Tinted(ui::TintColor::Accent)) + .when_some(self.tab_index, |this, tab_index| { + this.tab_index(tab_index) + }) + .on_click(move |_, _window, cx| { + telemetry::event!( + "Start Trial Clicked", + state = "post-sign-in" + ); + cx.open_url(&zed_urls::start_trial_url(cx)) + }), + ) + .child( + Label::new("No credit card required") + .size(LabelSize::Small) + .color(Color::Muted), + ), + ), + Some(Plan::ZedProTrial) => card + .child(pro_trial_stamp) + .child(Label::new("You're in the Zed Pro Trial").size(LabelSize::Large)) + .child( + Label::new("Here's what you get for the next 14 days:") + .color(Color::Muted) + .mb_2(), + ) + .child(plan_definitions.pro_trial(false)), + Some(Plan::ZedPro) => card + .child(certified_user_stamp) + .child(Label::new("You're in the Zed Pro plan").size(LabelSize::Large)) + .child( + Label::new("Here's what you get:") + .color(Color::Muted) + .mb_2(), + ) + .child(plan_definitions.pro_plan(false)), + }, + // Signed Out State + _ => card + .child(Label::new("Try Zed AI").size(LabelSize::Large)) + .child( + div() + .max_w_3_4() + .mb_2() + .child(Label::new(description).color(Color::Muted)), + ) + .child(plans_section) + .child( + Button::new("sign_in", "Sign In") + .full_width() + .style(ButtonStyle::Tinted(ui::TintColor::Accent)) + .when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index)) + .on_click({ + let callback = self.sign_in.clone(); + move |_, window, cx| { + telemetry::event!("Start Trial Clicked", state = "pre-sign-in"); + callback(window, cx) + } + }), + ), + } } } @@ -188,7 +254,6 @@ impl Component for AiUpsellCard { fn preview(_window: &mut Window, _cx: &mut App) -> Option { Some( v_flex() - .p_4() .gap_4() .children(vec![example_group(vec![ single_example( @@ -202,11 +267,31 @@ impl Component for AiUpsellCard { .into_any_element(), ), single_example( - "Signed In State", + "Free Plan", AiUpsellCard { sign_in_status: SignInStatus::SignedIn, sign_in: Arc::new(|_, _| {}), - user_plan: None, + user_plan: Some(Plan::ZedFree), + tab_index: Some(1), + } + .into_any_element(), + ), + single_example( + "Pro Trial", + AiUpsellCard { + sign_in_status: SignInStatus::SignedIn, + sign_in: Arc::new(|_, _| {}), + user_plan: Some(Plan::ZedProTrial), + tab_index: Some(1), + } + .into_any_element(), + ), + single_example( + "Pro Plan", + AiUpsellCard { + sign_in_status: SignInStatus::SignedIn, + sign_in: Arc::new(|_, _| {}), + user_plan: Some(Plan::ZedPro), tab_index: Some(1), } .into_any_element(), diff --git a/crates/ai_onboarding/src/plan_definitions.rs b/crates/ai_onboarding/src/plan_definitions.rs new file mode 100644 index 0000000000..8d66f6c356 --- /dev/null +++ b/crates/ai_onboarding/src/plan_definitions.rs @@ -0,0 +1,39 @@ +use gpui::{IntoElement, ParentElement}; +use ui::{List, ListBulletItem, prelude::*}; + +/// Centralized definitions for Zed AI plans +pub struct PlanDefinitions; + +impl PlanDefinitions { + pub const AI_DESCRIPTION: &'static str = "Zed offers a complete agentic experience, with robust editing and reviewing features to collaborate with AI."; + + pub fn free_plan(&self) -> impl IntoElement { + List::new() + .child(ListBulletItem::new("50 prompts with Claude models")) + .child(ListBulletItem::new("2,000 accepted edit predictions")) + } + + pub fn pro_trial(&self, period: bool) -> impl IntoElement { + List::new() + .child(ListBulletItem::new("150 prompts with Claude models")) + .child(ListBulletItem::new( + "Unlimited edit predictions with Zeta, our open-source model", + )) + .when(period, |this| { + this.child(ListBulletItem::new( + "Try it out for 14 days for free, no credit card required", + )) + }) + } + + pub fn pro_plan(&self, price: bool) -> impl IntoElement { + List::new() + .child(ListBulletItem::new("500 prompts with Claude models")) + .child(ListBulletItem::new( + "Unlimited edit predictions with Zeta, our open-source model", + )) + .when(price, |this| { + this.child(ListBulletItem::new("$20 USD per month")) + }) + } +} diff --git a/crates/ai_onboarding/src/young_account_banner.rs b/crates/ai_onboarding/src/young_account_banner.rs index a43625a60e..54f563e4aa 100644 --- a/crates/ai_onboarding/src/young_account_banner.rs +++ b/crates/ai_onboarding/src/young_account_banner.rs @@ -15,6 +15,7 @@ impl RenderOnce for YoungAccountBanner { .child(YOUNG_ACCOUNT_DISCLAIMER); div() + .max_w_full() .my_1() .child(Banner::new().severity(ui::Severity::Warning).child(label)) } diff --git a/crates/ui/src/components/image.rs b/crates/ui/src/components/image.rs index 2deba68d88..18f804abe9 100644 --- a/crates/ui/src/components/image.rs +++ b/crates/ui/src/components/image.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use gpui::Transformation; use gpui::{App, IntoElement, Rems, RenderOnce, Size, Styled, Window, svg}; use serde::{Deserialize, Serialize}; use strum::{EnumIter, EnumString, IntoStaticStr}; @@ -12,11 +13,13 @@ use crate::prelude::*; )] #[strum(serialize_all = "snake_case")] pub enum VectorName { + AiGrid, + CertifiedUserStamp, + DebuggerGrid, + Grid, + ProTrialStamp, ZedLogo, ZedXCopilot, - Grid, - AiGrid, - DebuggerGrid, } impl VectorName { @@ -37,6 +40,7 @@ pub struct Vector { path: Arc, color: Color, size: Size, + transformation: Transformation, } impl Vector { @@ -46,6 +50,7 @@ impl Vector { path: vector.path(), color: Color::default(), size: Size { width, height }, + transformation: Transformation::default(), } } @@ -66,6 +71,11 @@ impl Vector { self.size = size; self } + + pub fn transform(mut self, transformation: Transformation) -> Self { + self.transformation = transformation; + self + } } impl RenderOnce for Vector { @@ -81,6 +91,7 @@ impl RenderOnce for Vector { .h(height) .path(self.path) .text_color(self.color.color(cx)) + .with_transformation(self.transformation) } } diff --git a/crates/ui/src/components/list.rs b/crates/ui/src/components/list.rs index 88650b6ae8..6876f290ce 100644 --- a/crates/ui/src/components/list.rs +++ b/crates/ui/src/components/list.rs @@ -1,10 +1,12 @@ mod list; +mod list_bullet_item; mod list_header; mod list_item; mod list_separator; mod list_sub_header; pub use list::*; +pub use list_bullet_item::*; pub use list_header::*; pub use list_item::*; pub use list_separator::*; diff --git a/crates/ui/src/components/list/list_bullet_item.rs b/crates/ui/src/components/list/list_bullet_item.rs new file mode 100644 index 0000000000..6e079d9f11 --- /dev/null +++ b/crates/ui/src/components/list/list_bullet_item.rs @@ -0,0 +1,40 @@ +use crate::{ListItem, prelude::*}; +use gpui::{IntoElement, ParentElement, SharedString}; + +#[derive(IntoElement)] +pub struct ListBulletItem { + label: SharedString, +} + +impl ListBulletItem { + pub fn new(label: impl Into) -> Self { + Self { + label: label.into(), + } + } +} + +impl RenderOnce for ListBulletItem { + fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement { + let line_height = 0.85 * window.line_height(); + + ListItem::new("list-item") + .selectable(false) + .child( + h_flex() + .w_full() + .min_w_0() + .gap_1() + .items_start() + .child( + h_flex().h(line_height).justify_center().child( + Icon::new(IconName::Dash) + .size(IconSize::XSmall) + .color(Color::Hidden), + ), + ) + .child(div().w_full().min_w_0().child(Label::new(self.label))), + ) + .into_any_element() + } +}