A Meter's name takes the free width and ends in an ellipsis while its count stays whole, so a long quota name keeps "9 of 12 seats" inside a 280px box. `expiry` says why it refuses a date, and Expires reads "There is no month 13." apart from "This date has passed.". Tests now cover an American Express card's fifteen digits and four-digit code, the address emptying once an invitation goes, a quota past its limit, and a prompt with benefits standing as a paywall and one without as a line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
144 lines
4.5 KiB
Rust
144 lines
4.5 KiB
Rust
use std::rc::Rc;
|
|
|
|
use gpui::{
|
|
App, ElementId, FontWeight, InteractiveElement, IntoElement, ParentElement, RenderOnce,
|
|
SharedString, Styled, Window, div,
|
|
};
|
|
|
|
use super::login::Run;
|
|
use crate::{
|
|
buttons::{Button, ButtonVariant},
|
|
layout::Card,
|
|
primitives::{Icon, IconName},
|
|
theme::{ActiveTheme, IconSize, TextSize},
|
|
};
|
|
|
|
/// A way past a limit: why, what the next plan brings, and Upgrade. With benefits it stands as a paywall; without, as one line.
|
|
#[derive(IntoElement)]
|
|
pub struct UpgradePrompt {
|
|
id: ElementId,
|
|
title: SharedString,
|
|
body: Option<SharedString>,
|
|
benefits: Vec<SharedString>,
|
|
on_upgrade: Option<Run>,
|
|
on_dismiss: Option<Run>,
|
|
}
|
|
|
|
impl UpgradePrompt {
|
|
pub fn new(id: impl Into<ElementId>, title: impl Into<SharedString>) -> Self {
|
|
Self {
|
|
id: id.into(),
|
|
title: title.into(),
|
|
body: None,
|
|
benefits: Vec::new(),
|
|
on_upgrade: None,
|
|
on_dismiss: None,
|
|
}
|
|
}
|
|
|
|
pub fn body(mut self, body: impl Into<SharedString>) -> Self {
|
|
self.body = Some(body.into());
|
|
self
|
|
}
|
|
|
|
/// Something the next plan brings; any makes this a paywall.
|
|
pub fn benefit(mut self, benefit: impl Into<SharedString>) -> Self {
|
|
self.benefits.push(benefit.into());
|
|
self
|
|
}
|
|
|
|
pub fn on_upgrade(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
|
|
self.on_upgrade = Some(Rc::new(handler));
|
|
self
|
|
}
|
|
|
|
/// Shows "Not now".
|
|
pub fn on_dismiss(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
|
|
self.on_dismiss = Some(Rc::new(handler));
|
|
self
|
|
}
|
|
}
|
|
|
|
impl RenderOnce for UpgradePrompt {
|
|
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
|
let id = self.id;
|
|
let upgrade = self
|
|
.on_upgrade
|
|
.unwrap_or_else(|| panic!("upgrade prompt {id:?} has no on_upgrade"));
|
|
let theme = cx.theme();
|
|
let wall = !self.benefits.is_empty();
|
|
let actions = div()
|
|
.flex()
|
|
.flex_wrap()
|
|
.gap_2()
|
|
.child(
|
|
Button::new((id.clone(), "upgrade"), "Upgrade")
|
|
.variant(ButtonVariant::Primary)
|
|
.on_click(move |_, window, cx| {
|
|
log::info!("upgrade prompt: upgrade");
|
|
upgrade(window, cx);
|
|
}),
|
|
)
|
|
.children(self.on_dismiss.map(|run| {
|
|
Button::new((id.clone(), "dismiss"), "Not now")
|
|
.variant(ButtonVariant::Ghost)
|
|
.on_click(move |_, window, cx| run(window, cx))
|
|
}));
|
|
let words = div()
|
|
.flex_1()
|
|
.min_w(theme.label_width())
|
|
.child(div().font_weight(FontWeight::SEMIBOLD).child(self.title))
|
|
.children(
|
|
self.body
|
|
.map(|body| div().text_color(theme.colors.fg_muted).child(body)),
|
|
);
|
|
let benefits = self.benefits.into_iter().map(|benefit| {
|
|
div()
|
|
.flex()
|
|
.items_center()
|
|
.gap_2()
|
|
.child(
|
|
Icon::new(IconName::Check)
|
|
.size(IconSize::Sm)
|
|
.color(theme.colors.success),
|
|
)
|
|
.child(div().flex_1().min_w_0().child(benefit))
|
|
});
|
|
let body = match wall {
|
|
true => div()
|
|
.flex()
|
|
.flex_col()
|
|
.gap_4()
|
|
.child(
|
|
div()
|
|
.flex()
|
|
.items_start()
|
|
.gap_3()
|
|
.child(
|
|
Icon::new(IconName::Lock)
|
|
.size(IconSize::Md)
|
|
.color(theme.colors.fg_muted),
|
|
)
|
|
.child(words),
|
|
)
|
|
.child(
|
|
div()
|
|
.debug_selector(|| "paywall-benefits".into())
|
|
.flex()
|
|
.flex_col()
|
|
.gap_1p5()
|
|
.children(benefits),
|
|
)
|
|
.child(actions),
|
|
false => div()
|
|
.flex()
|
|
.flex_wrap()
|
|
.items_center()
|
|
.gap_3()
|
|
.child(words)
|
|
.child(actions),
|
|
};
|
|
Card::new().child(div().text_size(theme.text_size(TextSize::Sm)).child(body))
|
|
}
|
|
}
|