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}