Skip to main content

islands_css/
variants.rs

1/// Declarative macro that reproduces the TypeScript `cva` (class-variance-authority) API in Rust.
2///
3/// Expands to:
4/// - `pub enum {Name}Variant { ... }` with one variant per key in the `variant:` block
5/// - `pub enum {Name}Size { ... }` with one variant per key in the `size:` block
6/// - `pub struct {Name}Variants { pub variant: {Name}Variant, pub size: {Name}Size }`
7/// - `impl Default for {Name}Variants` — uses the keys named in `defaults: { variant: X, size: Y }`
8/// - `pub struct {Name};` — a namespace type; never instantiated
9/// - `impl {Name} { pub fn class(v: {Name}Variants) -> String { ... } }`
10///
11/// Usage:
12/// ```ignore
13/// variants! {
14///     Button,
15///     base: "inline-flex items-center ...",
16///     variant: {
17///         default: "bg-primary text-primary-foreground",
18///         destructive: "bg-destructive ...",
19///     },
20///     size: {
21///         default: "h-10 px-4 py-2",
22///         sm: "h-9 rounded-md px-3",
23///     },
24///     defaults: { variant: default, size: default },
25/// }
26/// ```
27#[macro_export]
28macro_rules! variants {
29    (
30        $name:ident,
31        base: $base:literal,
32        variant: { $( $variant_key:ident : $variant_class:literal ),+ $(,)? },
33        size: { $( $size_key:ident : $size_class:literal ),+ $(,)? },
34        defaults: { variant: $default_variant:ident, size: $default_size:ident } $(,)?
35    ) => {
36        $crate::variants!(@paste
37            $name,
38            $base,
39            [ $( $variant_key : $variant_class ),+ ],
40            [ $( $size_key : $size_class ),+ ],
41            $default_variant,
42            $default_size
43        );
44    };
45
46    // Internal arm — uses paste! to concatenate idents
47    (@paste
48        $name:ident,
49        $base:literal,
50        [ $( $variant_key:ident : $variant_class:literal ),+ ],
51        [ $( $size_key:ident : $size_class:literal ),+ ],
52        $default_variant:ident,
53        $default_size:ident
54    ) => {
55        $crate::__paste! {
56            /// Variant dimension for this component.
57            #[allow(dead_code)]
58            pub enum [< $name Variant >] {
59                $( [< $variant_key:camel >] ),+
60            }
61
62            /// Size dimension for this component.
63            #[allow(dead_code)]
64            pub enum [< $name Size >] {
65                $( [< $size_key:camel >] ),+
66            }
67
68            /// Combined variants struct for this component.
69            #[allow(dead_code)]
70            pub struct [< $name Variants >] {
71                pub variant: [< $name Variant >],
72                pub size: [< $name Size >],
73            }
74
75            impl Default for [< $name Variants >] {
76                fn default() -> Self {
77                    Self {
78                        variant: [< $name Variant >]::[< $default_variant:camel >],
79                        size: [< $name Size >]::[< $default_size:camel >],
80                    }
81                }
82            }
83
84            /// Namespace type; never instantiated. Use `$name::class(variants)`.
85            #[allow(dead_code)]
86            pub struct $name;
87
88            impl $name {
89                /// Resolve the Tailwind class string for the given variant combination.
90                pub fn class(v: [< $name Variants >]) -> String {
91                    let variant_class = match v.variant {
92                        $( [< $name Variant >]::[< $variant_key:camel >] => $variant_class, )+
93                    };
94                    let size_class = match v.size {
95                        $( [< $name Size >]::[< $size_key:camel >] => $size_class, )+
96                    };
97                    $crate::cn(&[$base, variant_class, size_class])
98                }
99            }
100        }
101    };
102}
103
104// Re-export paste so callers of `variants!` don't need to depend on it separately.
105#[doc(hidden)]
106pub use paste::paste as __paste;