Skip to main content

tablo_ui/components/primitives/
button.rs

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