T10b feedback: error views with 404, 403, maintenance and offline, error boundary, drawn results, confirmation card, saving, sync and connection lines, countdown and timer with rolling digits
This commit is contained in:
24 files changed
+1806
-173
No files matched your search
@@ -376,7 +376,8 @@ pub fn empties(cx: &mut App) -> impl IntoElement + use<> {
|
||||
.child(
|
||||
frame().child(
|
||||
EmptyState::new("empty-inbox", IconName::Inbox, "No messages yet")
|
||||
.body("When someone writes to you, it lands here."),
|
||||
.body("When someone writes to you, it lands here, newest first, until you archive it.")
|
||||
.action(Button::new("empty-compose", "Write a message")),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
mod messages;
|
||||
mod states;
|
||||
|
||||
use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div};
|
||||
|
||||
@@ -36,6 +37,23 @@ const SCRIPT: &[Step] = &[
|
||||
Step::Click("status"),
|
||||
Step::Wait(80),
|
||||
Step::Shot("status-rising"),
|
||||
Step::Click("boundary-break"),
|
||||
Step::Wait(700),
|
||||
Step::Shot("boundary"),
|
||||
Step::Click("boundary-break"),
|
||||
Step::Click("result-replay"),
|
||||
Step::Wait(120),
|
||||
Step::Shot("result-drawing"),
|
||||
Step::Wait(1000),
|
||||
Step::Shot("result"),
|
||||
Step::Click("saving-next"),
|
||||
Step::Click("sync-next"),
|
||||
Step::Click("connection-next"),
|
||||
Step::Wait(400),
|
||||
Step::Shot("statuses"),
|
||||
Step::Click("countdown-restart"),
|
||||
Step::Wait(1500),
|
||||
Step::Shot("clocks"),
|
||||
Step::Rest,
|
||||
];
|
||||
|
||||
@@ -48,5 +66,11 @@ fn render(window: &mut Window, cx: &mut App) -> AnyElement {
|
||||
.child(messages::callouts(cx))
|
||||
.child(messages::lines(window, cx))
|
||||
.child(messages::empties(cx))
|
||||
.child(states::errors(cx))
|
||||
.child(states::boundary(window, cx))
|
||||
.child(states::results(window, cx))
|
||||
.child(states::confirmations(window, cx))
|
||||
.child(states::statuses(window, cx))
|
||||
.child(states::clocks(window, cx))
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use ely_gpui_component::{
|
||||
buttons::{Button, ButtonVariant},
|
||||
feedback::{
|
||||
ConfirmationCard, ConnectionStatus, Countdown, ErrorBoundary, ErrorView, InlineMessage,
|
||||
ResultView, SaveState, SavingIndicator, SyncState, SyncStatus, Timer,
|
||||
},
|
||||
primitives::Severity,
|
||||
shell::Connectivity,
|
||||
theme::{ActiveTheme, Radius, TextSize},
|
||||
typography::Caption,
|
||||
};
|
||||
use gpui::{App, Div, IntoElement, ParentElement, Styled, Window, div, prelude::FluentBuilder};
|
||||
|
||||
use crate::{
|
||||
probe::probe,
|
||||
ui::{keep, row, section, set, specimen, specimens},
|
||||
};
|
||||
|
||||
fn frame(cx: &App) -> Div {
|
||||
let theme = cx.theme();
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.h_80()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.rounded(theme.radius(Radius::Lg))
|
||||
.border_1()
|
||||
.border_color(theme.colors.border)
|
||||
}
|
||||
|
||||
pub fn errors(cx: &mut App) -> impl IntoElement + use<> {
|
||||
section(
|
||||
"ErrorView / NotFound / NoPermission / Maintenance / Offline",
|
||||
"A view that could not show what was asked says what happened and offers a way on.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_3()
|
||||
.child(frame(cx).child(
|
||||
ErrorView::not_found("error-404").action(Button::new("error-home", "Go home")),
|
||||
))
|
||||
.child(
|
||||
frame(cx).child(
|
||||
ErrorView::no_permission("error-403")
|
||||
.action(Button::new("error-ask", "Request access").primary()),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_3()
|
||||
.child(frame(cx).child(ErrorView::maintenance("error-maintenance")))
|
||||
.child(frame(cx).child(
|
||||
ErrorView::offline("error-offline").action(Button::new("error-retry", "Try again")),
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn boundary(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let broken = keep("boundary-broken", || false, window, cx);
|
||||
let (now, toggle, retry) = (*broken.read(cx), broken.clone(), broken.clone());
|
||||
let theme = cx.theme();
|
||||
let content = if now {
|
||||
Err(anyhow!("the export service answered 503").context("Revenue, third quarter"))
|
||||
} else {
|
||||
Ok(div()
|
||||
.size_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.text_color(theme.colors.fg_muted)
|
||||
.child("Revenue, third quarter: 1.2 M"))
|
||||
};
|
||||
section(
|
||||
"ErrorBoundary",
|
||||
"Shows its content, or the error it holds, logged once. It renders a Result; it never catches a panic.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div().flex().child(
|
||||
frame(cx).child(
|
||||
ErrorBoundary::new("boundary", content)
|
||||
.on_retry(move |_, cx| set(&retry, false, cx)),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
row().child(probe(
|
||||
"boundary-break",
|
||||
Button::new("boundary-toggle", if now { "Fix it" } else { "Break it" })
|
||||
.on_click(move |_, _, cx| set(&toggle, !now, cx)),
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn results(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let turn = keep("result-turn", || 0usize, window, cx);
|
||||
let (now, replay) = (*turn.read(cx), turn.clone());
|
||||
section(
|
||||
"Result",
|
||||
"Where a task ended. The ring and its mark draw themselves once.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_3()
|
||||
.child(
|
||||
frame(cx).child(
|
||||
ResultView::success(("result-ok", now), "Payment sent")
|
||||
.body("€240.00 to Studio Ottanta. A receipt is on its way.")
|
||||
.action(Button::new("result-done", "Done").primary())
|
||||
.action(Button::new("result-receipt", "View receipt")),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
frame(cx).child(
|
||||
ResultView::failure(("result-no", now), "Payment declined")
|
||||
.body("Your bank declined the charge. No money moved.")
|
||||
.action(Button::new("result-again", "Try again")),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
row().child(probe(
|
||||
"result-replay",
|
||||
Button::new("result-replay", "Replay")
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.on_click(move |_, _, cx| set(&replay, now + 1, cx)),
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn confirmations(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let said = keep("confirm-said", || None::<&'static str>, window, cx);
|
||||
let theme = cx.theme();
|
||||
let answer = |what: &'static str| {
|
||||
let said = said.clone();
|
||||
move |_: &mut Window, cx: &mut App| set(&said, Some(what), cx)
|
||||
};
|
||||
let command = div()
|
||||
.px_3()
|
||||
.py_2()
|
||||
.rounded(theme.radius(Radius::Md))
|
||||
.bg(theme.colors.sunken)
|
||||
.font_family(theme.mono_family.clone())
|
||||
.text_size(theme.text_size(TextSize::Sm))
|
||||
.child("cargo clean && cargo build --release");
|
||||
section(
|
||||
"ConfirmationCard",
|
||||
"Asks in place before an action runs, with what it will do.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.w_128()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.child(
|
||||
ConfirmationCard::new("confirm-run", "Run this command?")
|
||||
.body("The agent wants to rebuild the project from scratch.")
|
||||
.confirm("Run")
|
||||
.on_confirm(answer("Ran the command"))
|
||||
.on_cancel(answer("Kept the build"))
|
||||
.child(command),
|
||||
)
|
||||
.child(
|
||||
ConfirmationCard::new("confirm-delete", "Delete 3 branches?")
|
||||
.body("feature/toasts, feature/menus and old/draft go for good.")
|
||||
.confirm("Delete")
|
||||
.destructive()
|
||||
.on_confirm(answer("Deleted 3 branches"))
|
||||
.on_cancel(answer("Kept the branches")),
|
||||
)
|
||||
.when_some(*said.read(cx), |cards, what| {
|
||||
cards.child(InlineMessage::new(Severity::Info, what))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn statuses(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
const SAVES: [SaveState; 3] = [SaveState::Saving, SaveState::Saved, SaveState::Failed];
|
||||
const SYNCS: [SyncState; 4] = [
|
||||
SyncState::Syncing(12),
|
||||
SyncState::Synced,
|
||||
SyncState::Paused,
|
||||
SyncState::Failed,
|
||||
];
|
||||
const LINKS: [Connectivity; 3] = [
|
||||
Connectivity::Online,
|
||||
Connectivity::Reconnecting,
|
||||
Connectivity::Offline,
|
||||
];
|
||||
let at = keep("status-states", || [0usize; 3], window, cx);
|
||||
let now = *at.read(cx);
|
||||
let next = |slot: usize, count: usize| {
|
||||
let at = at.clone();
|
||||
move |_: &gpui::ClickEvent, _: &mut Window, cx: &mut App| {
|
||||
let mut then = now;
|
||||
then[slot] = (now[slot] + 1) % count;
|
||||
set(&at, then, cx)
|
||||
}
|
||||
};
|
||||
let retry = at.clone();
|
||||
section(
|
||||
"SavingIndicator / SyncStatus / ConnectionStatus",
|
||||
"Quiet lines for work in the background. Each rises in when its state changes; sync arrows turn while they work.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
specimens()
|
||||
.child(specimen(
|
||||
"saving",
|
||||
row()
|
||||
.child(probe(
|
||||
"saving-next",
|
||||
Button::new("saving-next", "Next").on_click(next(0, SAVES.len())),
|
||||
))
|
||||
.child(
|
||||
SavingIndicator::new("saving", SAVES[now[0]]).on_retry(move |_, cx| {
|
||||
let mut then = now;
|
||||
then[0] = 0;
|
||||
set(&retry, then, cx)
|
||||
}),
|
||||
),
|
||||
cx,
|
||||
))
|
||||
.child(specimen(
|
||||
"sync",
|
||||
row()
|
||||
.child(probe(
|
||||
"sync-next",
|
||||
Button::new("sync-next", "Next").on_click(next(1, SYNCS.len())),
|
||||
))
|
||||
.child(SyncStatus::new("sync", SYNCS[now[1]])),
|
||||
cx,
|
||||
))
|
||||
.child(specimen(
|
||||
"connection",
|
||||
row()
|
||||
.child(probe(
|
||||
"connection-next",
|
||||
Button::new("connection-next", "Next").on_click(next(2, LINKS.len())),
|
||||
))
|
||||
.child(ConnectionStatus::new("connection", LINKS[now[2]])),
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn clocks(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let opened = keep("clock-opened", Instant::now, window, cx);
|
||||
let short = keep(
|
||||
"clock-short",
|
||||
|| Instant::now() + Duration::from_secs(10),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let stopped = keep("clock-stopped", || None::<Instant>, window, cx);
|
||||
let over = keep("clock-over", || false, window, cx);
|
||||
let (since, until, halt) = (*opened.read(cx), *short.read(cx), *stopped.read(cx));
|
||||
let (restart, toggle, done, reset) =
|
||||
(short.clone(), stopped.clone(), over.clone(), opened.clone());
|
||||
let reopen = over.clone();
|
||||
section(
|
||||
"Countdown / Timer",
|
||||
"Time left and time spent. Digits roll as they change.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
specimens()
|
||||
.child(specimen(
|
||||
"launch in",
|
||||
Countdown::new(
|
||||
"countdown-launch",
|
||||
since + Duration::from_secs(2 * 3_600 + 14 * 60 + 7),
|
||||
),
|
||||
cx,
|
||||
))
|
||||
.child(specimen(
|
||||
"ten seconds",
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_3()
|
||||
.child(
|
||||
Countdown::new("countdown-short", until)
|
||||
.on_done(move |_, cx| set(&done, true, cx)),
|
||||
)
|
||||
.child(probe(
|
||||
"countdown-restart",
|
||||
Button::new("countdown-restart", "Restart")
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.on_click(move |_, _, cx| {
|
||||
set(&restart, Instant::now() + Duration::from_secs(10), cx);
|
||||
set(&reopen, false, cx);
|
||||
}),
|
||||
)),
|
||||
cx,
|
||||
))
|
||||
.child(specimen(
|
||||
"on this page",
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_3()
|
||||
.child(Timer::new("timer", since).stopped(halt))
|
||||
.child(
|
||||
Button::new(
|
||||
"timer-toggle",
|
||||
if halt.is_some() { "Reset" } else { "Stop" },
|
||||
)
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.on_click(move |_, _, cx| match halt {
|
||||
Some(_) => {
|
||||
set(&reset, Instant::now(), cx);
|
||||
set(&toggle, None, cx);
|
||||
}
|
||||
None => set(&toggle, Some(Instant::now()), cx),
|
||||
}),
|
||||
),
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.when(*over.read(cx), |section| {
|
||||
section.child(Caption::new("The ten seconds ran out; on_done ran once."))
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user