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:
ZacharyZhang-NY committed 2026-09-27 15:05:06 -04:00
1 parent d4ba8f7037
commit fe1b00d414
20 files changed
+1662 -122

No files matched your search

+313
View File
@@ -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."))),
),
)
}
+25 -1
View File
@@ -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()
}