Skip to main content

tablo_ui/components/primitives/
button.rs

1// SYNC: topcoat-ui-registry@0.11.0 sha256:1b40f815a96997b28f35b8f385f5849d86ca53ba47fb752a3e795bfc24c3a55d — do not hand-edit. Sync via `cargo xtask sync-topcoat-ui` (ADR-0007).
2use topcoat::{
3    Result,
4    view::{Attributes, Child, Class, StaticClass, View, class, component, view},
5};
6
7/// The visual style of a [`button`].
8///
9/// [`Default`] is `ButtonVariant::Primary`, used when no variant is given.
10#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
11#[allow(dead_code)]
12pub enum ButtonVariant {
13    /// The primary-filled button for the main action.
14    #[default]
15    Primary,
16    /// A muted, tinted fill for secondary actions.
17    Secondary,
18    /// A hairline-bordered button on the page background.
19    Outline,
20    /// No fill until hovered, for toolbars and inline actions.
21    Ghost,
22    /// A destructive-filled button for actions such as deleting data.
23    Destructive,
24}
25
26impl ButtonVariant {
27    /// Classes for the button variant and its interaction states.
28    ///
29    /// Each variant sets its own border color. Keep border colors out of the shared
30    /// base to avoid conflicting classes.
31    fn classes(self) -> StaticClass {
32        match self {
33            Self::Primary => class!(
34                "border-transparent bg-primary text-primary-foreground shadow-xs",
35                "hover:bg-primary/90 active:bg-primary/80",
36            ),
37            Self::Secondary => class!(
38                "border-transparent bg-foreground/5 text-foreground shadow-xs",
39                "hover:bg-foreground/10 active:bg-foreground/15",
40            ),
41            Self::Outline => class!(
42                "border-border text-foreground hover:bg-foreground/5",
43                "active:bg-foreground/10",
44            ),
45            Self::Ghost => class!(
46                "border-transparent text-foreground hover:bg-foreground/5 active:bg-foreground/10",
47            ),
48            Self::Destructive => class!(
49                "border-transparent bg-destructive text-destructive-foreground shadow-xs",
50                "hover:bg-destructive/90 active:bg-destructive/80",
51            ),
52        }
53    }
54}
55
56/// The size of a [`button`].
57///
58/// [`Default`] is `ButtonSize::Md`, used when no size is given.
59#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
60#[allow(dead_code)]
61pub enum ButtonSize {
62    /// A compact button.
63    Sm,
64    /// The standard button size.
65    #[default]
66    Md,
67    /// A prominent button.
68    Lg,
69    /// A square button sized for a single icon.
70    Icon,
71}
72
73impl ButtonSize {
74    /// Classes for the button dimensions. Text size stays the same across sizes.
75    fn classes(self) -> StaticClass {
76        match self {
77            Self::Sm => class!("h-8 gap-1.5 rounded-md px-3"),
78            Self::Md => class!("h-9 gap-2 rounded-lg px-4"),
79            Self::Lg => class!("h-10 gap-2 rounded-lg px-5"),
80            Self::Icon => class!("size-9 rounded-lg"),
81        }
82    }
83}
84
85/// Classes shared by button variants and sizes. A border reserves the same space in
86/// every variant.
87const BASE: StaticClass = class!(
88    "inline-flex shrink-0 items-center justify-center border",
89    "text-sm font-medium whitespace-nowrap transition-colors outline-none select-none",
90    "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
91    "focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50",
92);
93
94/// Builds the full class list for a button of the given `variant` and `size`.
95///
96/// Use it to give button styling to an element that is not a `<button>`, such
97/// as a link styled as a button:
98///
99/// ```ignore
100/// view! {
101///     <a href="/login" class=(button_variants(ButtonVariant::Outline, ButtonSize::Md))>
102///         "Sign in"
103///     </a>
104/// }
105/// ```
106#[must_use]
107pub fn button_variants(
108    variant: ButtonVariant,
109    size: ButtonSize,
110) -> Class<(StaticClass, StaticClass, StaticClass)> {
111    class!(BASE, variant.classes(), size.classes())
112}
113
114/// A styled button.
115///
116/// `variant` defaults to `Primary` and `size` to `Md`. Pass the content as children.
117/// `attrs` are forwarded to the `<button>`, with extra classes added to its classes.
118/// Use [`button_variants`] to apply the same styling to another element.
119///
120/// ```ignore
121/// view! {
122///     button(
123///         variant: ButtonVariant::Destructive,
124///         attrs: attributes! { type="submit" },
125///         "Delete"
126///     )
127/// }
128/// ```
129#[component]
130pub async fn button(
131    #[default] variant: ButtonVariant,
132    #[default] size: ButtonSize,
133    #[default] mut attrs: Attributes,
134    #[default] child: Child<'_>,
135) -> Result<impl View> {
136    Ok(view! {
137        <button
138            class=(class!(
139                BASE,
140                variant.classes(),
141                size.classes(),
142                attrs.remove("class"),
143            ))
144            (attrs)
145        >
146            (child)
147        </button>
148    })
149}