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}