diff --git a/crates/onboarding_ui/src/components/callout_row.rs b/crates/onboarding_ui/src/components/callout_row.rs new file mode 100644 index 0000000000000000000000000000000000000000..2d7f600c0e4211c35ba112822be42ce46961c4d4 --- /dev/null +++ b/crates/onboarding_ui/src/components/callout_row.rs @@ -0,0 +1,86 @@ +use component::{example_group_with_title, single_example}; +use gpui::{AnyElement, App, IntoElement, RenderOnce, Window}; +use smallvec::SmallVec; +use ui::{Label, prelude::*}; + +#[derive(IntoElement, RegisterComponent)] +pub struct CalloutRow { + title: SharedString, + lines: SmallVec<[SharedString; 4]>, +} + +impl CalloutRow { + pub fn new(title: impl Into) -> Self { + Self { + title: title.into(), + lines: SmallVec::new(), + } + } + + pub fn line(mut self, line: impl Into) -> Self { + self.lines.push(line.into()); + self + } +} + +impl RenderOnce for CalloutRow { + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + div().px_2().child( + v_flex() + .p_3() + .gap_1() + .bg(cx.theme().colors().surface_background) + .border_1() + .border_color(cx.theme().colors().border_variant) + .rounded_md() + .child(Label::new(self.title).weight(gpui::FontWeight::MEDIUM)) + .children( + self.lines + .into_iter() + .map(|line| Label::new(line).size(LabelSize::Small).color(Color::Muted)), + ), + ) + } +} + +impl Component for CalloutRow { + fn scope() -> ComponentScope { + ComponentScope::Layout + } + + fn sort_name() -> &'static str { + "RowCallout" + } + + fn preview(_window: &mut Window, _cx: &mut App) -> Option { + let examples = example_group_with_title( + "CalloutRow Examples", + vec![ + single_example( + "Privacy Notice", + CalloutRow::new("We don't use your code to train AI models") + .line("You choose which providers you enable, and they have their own privacy policies.") + .line("Read more about our privacy practices in our Privacy Policy.") + .into_any_element(), + ), + single_example( + "Single Line", + CalloutRow::new("Important Notice") + .line("This is a single line of information.") + .into_any_element(), + ), + single_example( + "Multi Line", + CalloutRow::new("Getting Started") + .line("Welcome to Zed! Here are some things to know:") + .line("• Use Cmd+P to quickly open files") + .line("• Use Cmd+Shift+P to access the command palette") + .line("• Check out the documentation for more tips") + .into_any_element(), + ), + ], + ); + + Some(v_flex().p_4().gap_4().child(examples).into_any_element()) + } +} diff --git a/crates/onboarding_ui/src/components/checkbox_row.rs b/crates/onboarding_ui/src/components/checkbox_row.rs new file mode 100644 index 0000000000000000000000000000000000000000..a2e0957335adae502c29e92ef9fc64cdfb6c4d3d --- /dev/null +++ b/crates/onboarding_ui/src/components/checkbox_row.rs @@ -0,0 +1,134 @@ +use component::{example_group_with_title, single_example}; +use gpui::StatefulInteractiveElement as _; +use gpui::{AnyElement, App, ClickEvent, IntoElement, RenderOnce, Window}; +use ui::prelude::*; + +#[derive(IntoElement, RegisterComponent)] +pub struct CheckboxRow { + label: SharedString, + description: Option, + checked: bool, + on_click: Option>, +} + +impl CheckboxRow { + pub fn new(label: impl Into) -> Self { + Self { + label: label.into(), + description: None, + checked: false, + on_click: None, + } + } + + pub fn description(mut self, description: impl Into) -> Self { + self.description = Some(description.into()); + self + } + + pub fn checked(mut self, checked: bool) -> Self { + self.checked = checked; + self + } + + pub fn on_click(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self { + self.on_click = Some(Box::new(handler)); + self + } +} + +impl RenderOnce for CheckboxRow { + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { + let checked = self.checked; + let on_click = self.on_click; + + let checkbox = gpui::div() + .w_4() + .h_4() + .rounded_sm() + .border_1() + .border_color(cx.theme().colors().border) + .when(checked, |this| { + this.bg(cx.theme().colors().element_selected) + .border_color(cx.theme().colors().border_selected) + }) + .hover(|this| this.bg(cx.theme().colors().element_hover)) + .child(gpui::div().when(checked, |this| { + this.size_full() + .flex() + .items_center() + .justify_center() + .child(Icon::new(IconName::Check)) + })); + + let main_row = if let Some(on_click) = on_click { + gpui::div() + .id("checkbox-row") + .h_flex() + .gap_2() + .items_center() + .child(checkbox) + .child(Label::new(self.label)) + .cursor_pointer() + .on_click(move |_event, window, cx| on_click(window, cx)) + } else { + gpui::div() + .id("checkbox-row") + .h_flex() + .gap_2() + .items_center() + .child(checkbox) + .child(Label::new(self.label)) + }; + + v_flex() + .px_5() + .py_1() + .gap_1() + .child(main_row) + .when_some(self.description, |this, desc| { + this.child( + gpui::div() + .ml_6() + .child(Label::new(desc).size(LabelSize::Small).color(Color::Muted)), + ) + }) + } +} + +impl Component for CheckboxRow { + fn scope() -> ComponentScope { + ComponentScope::Layout + } + + fn sort_name() -> &'static str { + "RowCheckbox" + } + + fn preview(_window: &mut Window, _cx: &mut App) -> Option { + let examples = example_group_with_title( + "CheckboxRow Examples", + vec![ + single_example( + "Unchecked", + CheckboxRow::new("Enable Vim Mode").into_any_element(), + ), + single_example( + "Checked", + CheckboxRow::new("Send Crash Reports") + .checked(true) + .into_any_element(), + ), + single_example( + "With Description", + CheckboxRow::new("Send Telemetry") + .description("Help improve Zed by sending anonymous usage data") + .checked(true) + .into_any_element(), + ), + ], + ); + + Some(v_flex().p_4().gap_4().child(examples).into_any_element()) + } +} diff --git a/crates/onboarding_ui/src/components/header_row.rs b/crates/onboarding_ui/src/components/header_row.rs new file mode 100644 index 0000000000000000000000000000000000000000..e13d48f84df028fdb01c171435ffa314e2329109 --- /dev/null +++ b/crates/onboarding_ui/src/components/header_row.rs @@ -0,0 +1,78 @@ +use component::{example_group_with_title, single_example}; +use gpui::{AnyElement, App, IntoElement, RenderOnce, Window}; +use ui::{Label, prelude::*}; + +#[derive(IntoElement, RegisterComponent)] +pub struct HeaderRow { + label: SharedString, + end_slot: Option, +} + +impl HeaderRow { + pub fn new(label: impl Into) -> Self { + Self { + label: label.into(), + end_slot: None, + } + } + + pub fn end_slot(mut self, slot: impl IntoElement) -> Self { + self.end_slot = Some(slot.into_any_element()); + self + } +} + +impl RenderOnce for HeaderRow { + fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { + h_flex() + .h(px(32.)) + .w_full() + .px_5() + .justify_between() + .child(Label::new(self.label)) + .when_some(self.end_slot, |this, slot| this.child(slot)) + } +} + +impl Component for HeaderRow { + fn scope() -> ComponentScope { + ComponentScope::Layout + } + + fn sort_name() -> &'static str { + "RowHeader" + } + + fn preview(_window: &mut Window, _cx: &mut App) -> Option { + let examples = example_group_with_title( + "HeaderRow Examples", + vec![ + single_example( + "Simple Header", + HeaderRow::new("Pick a Theme").into_any_element(), + ), + single_example( + "Header with Button", + HeaderRow::new("Pick a Theme") + .end_slot( + Button::new("more_themes", "More Themes") + .style(ButtonStyle::Subtle) + .color(Color::Muted), + ) + .into_any_element(), + ), + single_example( + "Header with Icon Button", + HeaderRow::new("Settings") + .end_slot( + IconButton::new("refresh", IconName::RotateCw) + .style(ButtonStyle::Subtle), + ) + .into_any_element(), + ), + ], + ); + + Some(v_flex().p_4().gap_4().child(examples).into_any_element()) + } +} diff --git a/crates/onboarding_ui/src/components/mod.rs b/crates/onboarding_ui/src/components/mod.rs index 707b2d2ea41ce975f566caa313aba92b7db367ac..1685b826183a82f91b45937c7dadfc587dd55fe7 100644 --- a/crates/onboarding_ui/src/components/mod.rs +++ b/crates/onboarding_ui/src/components/mod.rs @@ -1 +1,5 @@ +mod callout_row; +mod checkbox_row; +mod header_row; mod selectable_tile; +mod selectable_tile_row; diff --git a/crates/onboarding_ui/src/components/selectable_tile_row.rs b/crates/onboarding_ui/src/components/selectable_tile_row.rs new file mode 100644 index 0000000000000000000000000000000000000000..f903e4a663ff8bcb021a4ea1554a19e58ef9a663 --- /dev/null +++ b/crates/onboarding_ui/src/components/selectable_tile_row.rs @@ -0,0 +1,124 @@ +use super::selectable_tile::SelectableTile; +use component::{example_group_with_title, single_example}; +use gpui::{ + AnyElement, App, IntoElement, RenderOnce, StatefulInteractiveElement, Window, prelude::*, +}; +use smallvec::SmallVec; +use ui::{Label, prelude::*}; + +#[derive(IntoElement, RegisterComponent)] +pub struct SelectableTileRow { + gap: Pixels, + tiles: SmallVec<[SelectableTile; 8]>, +} + +impl SelectableTileRow { + pub fn new() -> Self { + Self { + gap: px(12.), + tiles: SmallVec::new(), + } + } + + pub fn gap(mut self, gap: impl Into) -> Self { + self.gap = gap.into(); + self + } + + pub fn tile(mut self, tile: SelectableTile) -> Self { + self.tiles.push(tile); + self + } +} + +impl RenderOnce for SelectableTileRow { + fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { + h_flex().w_full().px_5().gap(self.gap).children(self.tiles) + } +} + +impl Component for SelectableTileRow { + fn scope() -> ComponentScope { + ComponentScope::Layout + } + + fn sort_name() -> &'static str { + "RowSelectableTile" + } + + fn preview(_window: &mut Window, _cx: &mut App) -> Option { + let examples = example_group_with_title( + "SelectableTileRow Examples", + vec![ + single_example( + "Theme Tiles", + SelectableTileRow::new() + .gap(px(12.)) + .tile( + SelectableTile::new("tile1", px(100.), px(80.)) + .selected(true) + .child( + div() + .size_full() + .bg(gpui::red()) + .flex() + .items_center() + .justify_center() + .child(Label::new("Dark")), + ), + ) + .tile( + SelectableTile::new("tile2", px(100.), px(80.)).child( + div() + .size_full() + .bg(gpui::green()) + .flex() + .items_center() + .justify_center() + .child(Label::new("Light")), + ), + ) + .tile( + SelectableTile::new("tile3", px(100.), px(80.)) + .parent_focused(true) + .child( + div() + .size_full() + .bg(gpui::blue()) + .flex() + .items_center() + .justify_center() + .child(Label::new("Auto")), + ), + ) + .into_any_element(), + ), + single_example( + "Icon Tiles", + SelectableTileRow::new() + .gap(px(8.)) + .tile( + SelectableTile::new("icon1", px(48.), px(48.)) + .selected(true) + .child(Icon::new(IconName::Code).size(IconSize::Medium)), + ) + .tile( + SelectableTile::new("icon2", px(48.), px(48.)) + .child(Icon::new(IconName::Terminal).size(IconSize::Medium)), + ) + .tile( + SelectableTile::new("icon3", px(48.), px(48.)) + .child(Icon::new(IconName::FileCode).size(IconSize::Medium)), + ) + .tile( + SelectableTile::new("icon4", px(48.), px(48.)) + .child(Icon::new(IconName::Settings).size(IconSize::Medium)), + ) + .into_any_element(), + ), + ], + ); + + Some(v_flex().p_4().gap_4().child(examples).into_any_element()) + } +}