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;