Onboarding · Help: help panel, cheatsheet, what's new, feedback, support, inline help
HelpPanel searches articles and opens one, its body in Markdown, with a way back; focus follows to All articles and back to the search, a link to help:<key> opens another, and Contact support and Keyboard shortcuts wait at its foot. KeyboardShortcutCheatsheet puts settings::KeyboardShortcutsList in a dialog with its search focused; WhatsNewDialog puts data_display::Changelog in one with Got it. FeedbackWidget asks how it is going with four faces and a note, and thanks. ContactSupport sends a topic and a message, spins while the owner sends, and empties the message with focus back in it. InlineHelp is a quiet line with Learn more. HelpTooltip moves to primitives, where FormLabel's help mark shows it; ToggleGroup keys its toggles by its own id. EmptyStateWithAction points to feedback::EmptyState.
This commit is contained in:
1 parent
d4ba8f7037
commit
fe1b00d414
20 files changed
+1662
-122
No files matched your search
@@ -0,0 +1,313 @@
|
||||
use ely_gpui_component::{
|
||||
buttons::{Button, ButtonVariant},
|
||||
data_display::{Change, Release},
|
||||
feedback::EmptyState,
|
||||
forms::{Choice, FormField, Input, TextInput},
|
||||
onboarding::{
|
||||
ContactSupport, FeedbackWidget, HelpArticle, HelpPanel, InlineHelp,
|
||||
KeyboardShortcutCheatsheet, WhatsNewDialog,
|
||||
},
|
||||
primitives::{HelpTooltip, IconName},
|
||||
settings::Shortcut,
|
||||
theme::{ActiveTheme, ControlSize, TextSize},
|
||||
};
|
||||
use gpui::{
|
||||
App, Entity, FontWeight, IntoElement, ParentElement, SharedString, Styled, Window, div,
|
||||
prelude::*, px,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
probe::probe,
|
||||
ui::{change, keep, section},
|
||||
};
|
||||
|
||||
/// The help demos: the article open, whether the panel shows, and which dialog is up.
|
||||
#[derive(Default)]
|
||||
struct Help {
|
||||
open: Option<SharedString>,
|
||||
hidden: bool,
|
||||
dialog: Option<Dialog>,
|
||||
sent: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum Dialog {
|
||||
Shortcuts,
|
||||
WhatsNew,
|
||||
}
|
||||
|
||||
fn field(
|
||||
key: &'static str,
|
||||
hint: &'static str,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<TextInput> {
|
||||
window.use_keyed_state(key, cx, move |window, cx| {
|
||||
TextInput::new(window, cx).placeholder(hint)
|
||||
})
|
||||
}
|
||||
|
||||
fn article(key: &str, title: &str, summary: &str, body: &str) -> HelpArticle {
|
||||
HelpArticle {
|
||||
key: key.to_string().into(),
|
||||
title: title.to_string().into(),
|
||||
summary: summary.to_string().into(),
|
||||
body: body.to_string().into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn articles() -> [HelpArticle; 4] {
|
||||
[
|
||||
article(
|
||||
"start",
|
||||
"Getting started",
|
||||
"Make a project and invite your team.",
|
||||
"Press **New project** in the sidebar, give it a name, and pick who can see it.\n\nThen [invite your team](help:team).",
|
||||
),
|
||||
article(
|
||||
"team",
|
||||
"Invite your team",
|
||||
"Bring people in by email.",
|
||||
"Open **Settings › Members** and press **Invite**. Each person gets a link that works for seven days.",
|
||||
),
|
||||
article(
|
||||
"billing",
|
||||
"Change billing",
|
||||
"Who can, and where.",
|
||||
"Only admins can change billing. Open **Settings › Billing** to change the plan or the card.",
|
||||
),
|
||||
article(
|
||||
"keys",
|
||||
"Work from the keyboard",
|
||||
"The shortcuts worth learning first.",
|
||||
"Press `cmd-k` to open anything, and `?` for every shortcut.",
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
fn shortcut(group: &str, name: &str, keys: &str) -> Shortcut {
|
||||
Shortcut {
|
||||
group: group.to_string().into(),
|
||||
name: name.to_string().into(),
|
||||
keys: keys.to_string().into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tips(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let state = keep("onboarding-help", Help::default, window, cx);
|
||||
let billing = field(
|
||||
"onboarding-billing-email",
|
||||
"billing@example.com",
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let theme = cx.theme();
|
||||
let more = state.clone();
|
||||
section(
|
||||
"HelpTooltip · InlineHelp · EmptyStateWithAction",
|
||||
"A help mark beside a name shows its tip on hover; FormLabel's help is one. A line of help sits where it is needed, and Learn more opens the article in the panel below. An empty state with an action is feedback::EmptyState.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_wrap()
|
||||
.items_start()
|
||||
.gap_6()
|
||||
.child(
|
||||
div()
|
||||
.w(px(320.))
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_4()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.child(div().font_weight(FontWeight::SEMIBOLD).child("Retention"))
|
||||
.child(HelpTooltip::new("onboarding-retention-help", "Deleted items stay 30 days before they are gone.")),
|
||||
)
|
||||
.child(
|
||||
FormField::new("onboarding-billing-field", "Billing email")
|
||||
.help("Invoices go here as well as to the owner.")
|
||||
.child(Input::new(&billing)),
|
||||
)
|
||||
.child(
|
||||
InlineHelp::new("onboarding-billing-hint", "Only admins can change billing.").on_more(move |_, cx| {
|
||||
change(&more, cx, |help| {
|
||||
help.open = Some("billing".into());
|
||||
help.hidden = false;
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div().w(px(320.)).text_size(theme.text_size(TextSize::Sm)).child(
|
||||
EmptyState::new("onboarding-empty", IconName::FolderOpen, "No projects yet")
|
||||
.body("Projects hold your work and who can see it.")
|
||||
.action(
|
||||
Button::new("onboarding-empty-new", "New project")
|
||||
.variant(ButtonVariant::Primary)
|
||||
.size(ControlSize::Sm)
|
||||
.on_click(|_, _, _| log::info!("gallery: new project")),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn panel(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let state = keep("onboarding-help", Help::default, window, cx);
|
||||
let search = field("onboarding-help-search", "Search help", window, cx);
|
||||
let (open, hidden) = (state.read(cx).open.clone(), state.read(cx).hidden);
|
||||
let theme = cx.theme();
|
||||
let [opener, keys, close, show] = [(); 4].map(|_| state.clone());
|
||||
let body = match hidden {
|
||||
true => Button::new("onboarding-help-show", "Show help")
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.size(ControlSize::Sm)
|
||||
.on_click(move |_, _, cx| change(&show, cx, |help| help.hidden = false))
|
||||
.into_any_element(),
|
||||
false => div()
|
||||
.h(px(460.))
|
||||
.rounded_lg()
|
||||
.border_1()
|
||||
.border_color(theme.colors.border)
|
||||
.bg(theme.colors.surface)
|
||||
.child(
|
||||
HelpPanel::new("onboarding-help-panel", articles(), &search, open)
|
||||
.on_open(move |key, _, cx| {
|
||||
let key = key.cloned();
|
||||
change(&opener, cx, |help| help.open = key)
|
||||
})
|
||||
.on_contact(|_, _| log::info!("gallery: contact support"))
|
||||
.on_shortcuts(move |_, cx| {
|
||||
change(&keys, cx, |help| help.dialog = Some(Dialog::Shortcuts))
|
||||
})
|
||||
.on_close(move |_, cx| change(&close, cx, |help| help.hidden = true)),
|
||||
)
|
||||
.into_any_element(),
|
||||
};
|
||||
section(
|
||||
"HelpPanel / DocsSidebar",
|
||||
"Help beside the work: a search over the articles, one article with the way back, and links at its foot. A link to help:<key> opens another article.",
|
||||
cx,
|
||||
)
|
||||
.child(probe("onboarding-help-panel", div().w(px(320.)).child(body)))
|
||||
}
|
||||
|
||||
pub fn dialogs(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let state = keep("onboarding-help", Help::default, window, cx);
|
||||
let search = field("onboarding-keys-search", "Search shortcuts", window, cx);
|
||||
let dialog = state.read(cx).dialog;
|
||||
let [keys, news, shut, shut_news] = [(); 4].map(|_| state.clone());
|
||||
let open = match dialog {
|
||||
Some(Dialog::Shortcuts) => Some(
|
||||
KeyboardShortcutCheatsheet::new(
|
||||
"onboarding-cheatsheet",
|
||||
[
|
||||
shortcut("General", "Open anything", "cmd-k"),
|
||||
shortcut("General", "Every shortcut", "?"),
|
||||
shortcut("General", "Settings", "cmd-,"),
|
||||
shortcut("Editing", "Undo", "cmd-z"),
|
||||
shortcut("Editing", "Redo", "cmd-shift-z"),
|
||||
shortcut("Editing", "Find", "cmd-f"),
|
||||
shortcut("View", "Split view", "cmd-\\"),
|
||||
shortcut("View", "Zoom in", "cmd-="),
|
||||
],
|
||||
&search,
|
||||
move |_, cx| change(&shut, cx, |help| help.dialog = None),
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
Some(Dialog::WhatsNew) => Some(
|
||||
WhatsNewDialog::new("onboarding-whats-new", move |_, cx| {
|
||||
change(&shut_news, cx, |help| help.dialog = None)
|
||||
})
|
||||
.release(
|
||||
Release::new("2.4", "Sep 25, 2026")
|
||||
.summary("Split view, and a calmer sidebar.")
|
||||
.change(Change::Added, "Split view: two files side by side.")
|
||||
.change(Change::Added, "Pin a view to the toolbar.")
|
||||
.change(Change::Fixed, "Search keeps its place after an edit."),
|
||||
)
|
||||
.release(
|
||||
Release::new("2.3", "Sep 9, 2026")
|
||||
.change(Change::Changed, "The sidebar folds with a narrow window.")
|
||||
.change(Change::Fixed, "Sync resumes after sleep."),
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
None => None,
|
||||
};
|
||||
section(
|
||||
"KeyboardShortcutCheatsheet · WhatsNewDialog",
|
||||
"Every shortcut in one dialog, its search taking focus as it opens; and what changed, newest first.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_wrap()
|
||||
.gap_3()
|
||||
.child(probe(
|
||||
"onboarding-shortcuts",
|
||||
Button::new("onboarding-shortcuts", "Keyboard shortcuts")
|
||||
.icon(IconName::Keyboard)
|
||||
.on_click(move |_, _, cx| change(&keys, cx, |help| help.dialog = Some(Dialog::Shortcuts))),
|
||||
))
|
||||
.child(probe(
|
||||
"onboarding-whats-new",
|
||||
Button::new("onboarding-whats-new", "What's new")
|
||||
.icon(IconName::Sparkles)
|
||||
.on_click(move |_, _, cx| change(&news, cx, |help| help.dialog = Some(Dialog::WhatsNew))),
|
||||
)),
|
||||
)
|
||||
.children(open)
|
||||
}
|
||||
|
||||
pub fn feedback(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let state = keep("onboarding-help", Help::default, window, cx);
|
||||
let sent = state.read(cx).sent;
|
||||
let done = state.clone();
|
||||
let topics = [
|
||||
Choice::new("billing", "Billing"),
|
||||
Choice::new("bug", "Something broke"),
|
||||
Choice::new("account", "My account"),
|
||||
Choice::new("other", "Something else"),
|
||||
];
|
||||
section(
|
||||
"FeedbackWidget · ContactSupport",
|
||||
"Four faces and a note, sent with one press; and a message to people, sent once it has a topic and words.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_wrap()
|
||||
.items_start()
|
||||
.gap_6()
|
||||
.child(probe(
|
||||
"onboarding-feedback",
|
||||
FeedbackWidget::new("onboarding-feedback")
|
||||
.on_send(|sentiment, note, _, _| log::info!("gallery: feedback {} {note}", sentiment.key())),
|
||||
))
|
||||
.child(
|
||||
div()
|
||||
.w(px(360.))
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.child(
|
||||
ContactSupport::new("onboarding-support", topics)
|
||||
.address("help@example.com")
|
||||
.on_send(move |topic, _, _, cx| {
|
||||
log::info!("gallery: support about {topic}");
|
||||
change(&done, cx, |help| help.sent = true)
|
||||
}),
|
||||
)
|
||||
.when(sent, |column| column.child(div().text_sm().child("Sent. We answer within a day."))),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
use gpui::{AnyElement, App, Window, div, prelude::*};
|
||||
|
||||
mod flows;
|
||||
mod help;
|
||||
|
||||
use super::Page;
|
||||
use crate::script::Step;
|
||||
@@ -9,7 +10,7 @@ pub const PAGE: Page = Page {
|
||||
number: 37,
|
||||
slug: "onboarding",
|
||||
title: "Onboarding & Help",
|
||||
summary: "A first run in steps, what is new called out, and first things to do.",
|
||||
summary: "A first run in steps, what is new called out, first things to do, and help where it is needed.",
|
||||
render,
|
||||
script: SCRIPT,
|
||||
};
|
||||
@@ -27,6 +28,25 @@ const SCRIPT: &[Step] = &[
|
||||
Step::Wait(200),
|
||||
Step::Shot("hotspot-tip"),
|
||||
Step::Key("escape"),
|
||||
Step::DownAt("onboarding-help-panel", 120.0, 124.0),
|
||||
Step::UpAt("onboarding-help-panel", 120.0, 124.0),
|
||||
Step::Wait(200),
|
||||
Step::Shot("help-article"),
|
||||
Step::DownAt("onboarding-shortcuts", 20.0, 16.0),
|
||||
Step::UpAt("onboarding-shortcuts", 20.0, 16.0),
|
||||
Step::Wait(300),
|
||||
Step::Shot("cheatsheet"),
|
||||
Step::Key("escape"),
|
||||
Step::DownAt("onboarding-whats-new", 20.0, 16.0),
|
||||
Step::UpAt("onboarding-whats-new", 20.0, 16.0),
|
||||
Step::Wait(300),
|
||||
Step::Shot("whats-new"),
|
||||
Step::Key("escape"),
|
||||
Step::DownAt("onboarding-feedback", 20.0, 16.0),
|
||||
Step::UpAt("onboarding-feedback", 20.0, 16.0),
|
||||
Step::Wait(200),
|
||||
Step::Shot("feedback"),
|
||||
Step::Key("escape"),
|
||||
];
|
||||
|
||||
fn render(window: &mut Window, cx: &mut App) -> AnyElement {
|
||||
@@ -34,5 +54,9 @@ fn render(window: &mut Window, cx: &mut App) -> AnyElement {
|
||||
.child(flows::wizard(window, cx))
|
||||
.child(flows::highlights(window, cx))
|
||||
.child(flows::checklist(window, cx))
|
||||
.child(help::tips(window, cx))
|
||||
.child(help::panel(window, cx))
|
||||
.child(help::dialogs(window, cx))
|
||||
.child(help::feedback(window, cx))
|
||||
.into_any_element()
|
||||
}
|
||||
Reference in new issue
Block a user