@jpgilldev / steid

steid/src/components/button.rs
5.3 KBRaw
1use topcoat::{
2 Result,
3 view::{Attributes, View, class, component, view},
4};
5
6/// The visual style of a [`button`].
7///
8/// [`Default`] is `ButtonVariant::Primary`, used when no variant is given.
9#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
10#[allow(dead_code)]
11pub enum ButtonVariant {
12 /// The primary-filled button for the main action.
13 #[default]
14 Primary,
15 /// A muted, tinted fill for secondary actions.
16 Secondary,
17 /// A hairline-bordered button on the page background.
18 Outline,
19 /// No fill until hovered, for toolbars and inline actions.
20 Ghost,
21 /// A destructive-filled button for actions such as deleting data.
22 Destructive,
23}
24
25impl ButtonVariant {
26 /// The Tailwind classes for this variant.
27 ///
28 /// Hover and press states apply the fill or foreground color at reduced
29 /// opacity, so they hold up in both color schemes without `dark:`
30 /// overrides. Every variant with a resting fill or border casts the
31 /// theme's control shadow; `Ghost` is flat until hovered, so it casts
32 /// none.
33 ///
34 /// Each variant sets its own border color rather than inheriting a
35 /// transparent one from [`BASE`]: with two border-color classes on the
36 /// same element, stylesheet order (not class order) would decide the
37 /// winner.
38 fn classes(self) -> &'static str {
39 match self {
40 Self::Primary => {
41 "border-transparent bg-primary text-primary-foreground shadow-xs \
42 hover:bg-primary/90 active:bg-primary/80"
43 }
44 Self::Secondary => {
45 "border-transparent bg-foreground/5 text-foreground shadow-xs \
46 hover:bg-foreground/10 active:bg-foreground/15"
47 }
48 Self::Outline => {
49 "border-border text-foreground shadow-xs hover:bg-foreground/5 \
50 active:bg-foreground/10"
51 }
52 Self::Ghost => {
53 "border-transparent text-foreground hover:bg-foreground/5 active:bg-foreground/10"
54 }
55 Self::Destructive => {
56 "border-transparent bg-destructive text-destructive-foreground shadow-xs \
57 hover:bg-destructive/90 active:bg-destructive/80"
58 }
59 }
60 }
61}
62
63/// The size of a [`button`].
64///
65/// [`Default`] is `ButtonSize::Md`, used when no size is given.
66#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
67#[allow(dead_code)]
68pub enum ButtonSize {
69 /// A compact button.
70 Sm,
71 /// The standard button size.
72 #[default]
73 Md,
74 /// A prominent button.
75 Lg,
76 /// A square button sized for a single icon.
77 Icon,
78}
79
80impl ButtonSize {
81 /// The Tailwind classes for this size.
82 ///
83 /// Each size sets a text size, which also scales any icons inside: the
84 /// `icon` component is `1em` square by default.
85 fn classes(self) -> &'static str {
86 match self {
87 Self::Sm => "h-8 gap-1.5 rounded-md px-3 text-xs",
88 Self::Md => "h-9 gap-2 rounded-lg px-4 text-sm",
89 Self::Lg => "h-10 gap-2 rounded-lg px-5 text-base",
90 Self::Icon => "size-9 rounded-lg text-base",
91 }
92 }
93}
94
95/// The classes shared by every button, regardless of variant or size.
96///
97/// Every button carries a border (colored per variant) so that the `Outline`
98/// variant, which only recolors it, does not change the button's dimensions.
99const BASE: &str = "inline-flex shrink-0 items-center justify-center border \
100 font-medium whitespace-nowrap transition-colors outline-none select-none \
101 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 \
102 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50";
103
104/// Builds the full class string for a button of the given `variant` and `size`.
105///
106/// Use it to give button styling to an element that is not a `<button>`, such
107/// as a link styled as a button:
108///
109/// ```ignore
110/// view! {
111/// <a href="/login" class=(button_variants(ButtonVariant::Outline, ButtonSize::Md))>
112/// "Sign in"
113/// </a>
114/// }
115/// ```
116#[must_use]
117pub fn button_variants(variant: ButtonVariant, size: ButtonSize) -> String {
118 format!("{BASE} {} {}", variant.classes(), size.classes())
119}
120
121/// A button component.
122///
123/// The `variant` and `size` parameters select the styling, defaulting to
124/// `Primary` and `Md`. The `attrs` (such as `class`, `type`, `disabled`, or
125/// event handlers) are forwarded to the underlying `<button>`; a `class` among
126/// them is appended to the computed classes. Child nodes become the button's
127/// content.
128///
129/// ```ignore
130/// view! {
131/// button(
132/// variant: ButtonVariant::Destructive,
133/// attrs: attributes! { type="submit" },
134/// "Delete"
135/// )
136/// }
137/// ```
138///
139/// To style a non-`<button>` element like a button, use [`button_variants`]
140/// directly.
141#[component]
142pub async fn button(
143 #[default] variant: ButtonVariant,
144 #[default] size: ButtonSize,
145 #[default] mut attrs: Attributes,
146 #[default] child: View,
147) -> Result {
148 view! {
149 <button
150 class=(class!(
151 BASE,
152 variant.classes(),
153 size.classes(),
154 attrs.remove("class"),
155 ))
156 (attrs)
157 >
158 (child)
159 </button>
160 }
161}