leptos_lucide_rs/
lib.rs

1//! # Leptos Lucide Icons
2//!
3//! A comprehensive Lucide icon library for Leptos with tree-shaking support.
4//!
5//! ## Features
6//!
7//! - 🌳 **Tree-shaking**: Only bundle the icons you actually use
8//! - âš¡ **Zero-cost**: All icon components are `#[inline(always)]` for maximum performance
9//! - 🦀 **Rust-friendly**: Generated component names follow Rust conventions
10//! - 🔒 **Type-safe**: Each icon is a separate typed component
11//! - 🎨 **Customizable**: Easy styling with CSS classes and inline styles
12//!
13//! ## Usage
14//!
15//! ```rust
16//! use leptos::prelude::*;
17//! use leptos_lucide_rs::*;
18//!
19//! #[component]
20//! pub fn MyComponent() -> impl IntoView {
21//!     view! {
22//!         <div>
23//!             <Home/>
24//!             <User/>
25//!             <Heart/>
26//!         </div>
27//!     }
28//! }
29//! ```
30//!
31//! ## Custom Styling
32//!
33//! ```rust
34//! use leptos::prelude::*;
35//! use leptos_lucide_rs::*;
36//!
37//! #[component]
38//! pub fn StyledIcons() -> impl IntoView {
39//!     view! {
40//!         <div>
41//!             // Using the macro for custom classes
42//!             // TODO:
43//!             // {icon!(Home, class = "text-blue-500 w-6 h-6")}
44//!
45//!             // Using the macro for custom sizes
46//!             // TODO:
47//!             // {icon!(User, size = "32px")}
48//!
49//!             // Direct component with custom attributes
50//!             <div class="icon-wrapper">
51//!                 <Search/>
52//!             </div>
53//!         </div>
54//!     }
55//! }
56//! ```
57
58use leptos::prelude::*;
59
60// Include the generated icons module
61#[cfg(leptos_lucide_generated)]
62include!(concat!(env!("OUT_DIR"), "/icons.rs"));
63
64// Fallback module when icons aren't generated yet (for IDE support)
65#[cfg(not(leptos_lucide_generated))]
66pub mod fallback {
67    use leptos::*;
68
69    /// Fallback Home icon for development
70    #[inline(always)]
71    #[allow(non_snake_case)]
72    pub fn Home() -> impl IntoView {
73        load_icon_fallback("home")
74    }
75
76    /// Fallback User icon for development
77    #[inline(always)]
78    #[allow(non_snake_case)]
79    pub fn User() -> impl IntoView {
80        load_icon_fallback("user")
81    }
82
83    /// Fallback Heart icon for development
84    #[inline(always)]
85    #[allow(non_snake_case)]
86    pub fn Heart() -> impl IntoView {
87        load_icon_fallback("heart")
88    }
89
90    /// Fallback Search icon for development
91    #[inline(always)]
92    #[allow(non_snake_case)]
93    pub fn Search() -> impl IntoView {
94        load_icon_fallback("search")
95    }
96
97    /// Fallback Star icon for development
98    #[inline(always)]
99    #[allow(non_snake_case)]
100    pub fn Star() -> impl IntoView {
101        load_icon_fallback("star")
102    }
103
104    /// Generic fallback icon loader using lucide-svg-rs
105    fn load_icon_fallback(name: &str) -> impl IntoView {
106        view! {
107            <svg
108                class="lucide-icon"
109                xmlns="http://www.w3.org/2000/svg"
110                width="24"
111                height="24"
112                viewBox="0 0 24 24"
113                fill="none"
114                stroke="currentColor"
115                stroke-width="2"
116                stroke-linecap="round"
117                stroke-linejoin="round"
118                data-lucide=name
119                inner_html=move || {
120                    let client = lucide_svg_rs::LucideClient::default();
121                    match client.get_icon_content(&format!("{}.svg", name)) {
122                        Ok(svg_content) => {
123                            // Extract content between <svg> tags
124                            if let (Some(start), Some(end)) = (svg_content.find('>'), svg_content.rfind("</svg>")) {
125                                if start < end {
126                                    return svg_content[start + 1..end].to_string();
127                                }
128                            }
129                            // Ultimate fallback if parsing fails
130                            r#"<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"></path>"#.to_string()
131                        }
132                        Err(_) => {
133                            // Ultimate fallback
134                            r#"<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"></path>"#.to_string()
135                        }
136                    }
137                }
138            >
139            </svg>
140        }
141    }
142
143    /// Dynamic icon loader (same as generated version)
144    pub fn load_icon(name: &str) -> impl IntoView {
145        load_icon_fallback(name)
146    }
147}
148
149// Re-export fallback icons when not generated
150#[cfg(not(leptos_lucide_generated))]
151pub use fallback::*;
152
153/// Re-export the load_icon function for dynamic loading
154#[cfg(not(leptos_lucide_generated))]
155pub use fallback::load_icon;
156
157/// Icon configuration for customizing appearance
158#[derive(Clone, Debug, Default)]
159pub struct IconConfig {
160    /// CSS class to apply to the icon
161    pub class: Option<String>,
162    /// Inline style string
163    pub style: Option<String>,
164    /// Icon size (width and height)
165    pub size: Option<String>,
166    /// Custom stroke width
167    pub stroke_width: Option<String>,
168    /// Custom stroke color
169    pub stroke: Option<String>,
170    /// Fill color
171    pub fill: Option<String>,
172}
173
174impl IconConfig {
175    /// Create a new icon configuration
176    pub fn new() -> Self {
177        Self::default()
178    }
179
180    /// Set the CSS class
181    pub fn class<S: Into<String>>(mut self, class: S) -> Self {
182        self.class = Some(class.into());
183        self
184    }
185
186    /// Set the inline style
187    pub fn style<S: Into<String>>(mut self, style: S) -> Self {
188        self.style = Some(style.into());
189        self
190    }
191
192    /// Set the icon size
193    pub fn size<S: Into<String>>(mut self, size: S) -> Self {
194        self.size = Some(size.into());
195        self
196    }
197
198    /// Set the stroke width
199    pub fn stroke_width<S: Into<String>>(mut self, width: S) -> Self {
200        self.stroke_width = Some(width.into());
201        self
202    }
203
204    /// Set the stroke color
205    pub fn stroke<S: Into<String>>(mut self, stroke: S) -> Self {
206        self.stroke = Some(stroke.into());
207        self
208    }
209
210    /// Set the fill color
211    pub fn fill<S: Into<String>>(mut self, fill: S) -> Self {
212        self.fill = Some(fill.into());
213        self
214    }
215}
216
217/// Helper component for rendering icons with configuration (enhanced version)
218#[component]
219pub fn Icon<F, V>(
220    /// The icon component function
221    icon: F,
222
223    /// Optional configuration
224    #[prop(optional)]
225    config: Option<IconConfig>,
226
227    /// Set wrapper element (defaults to "div")
228    #[prop(optional)]
229    wrapper: Option<String>,
230) -> leptos::prelude::AnyView
231where
232    F: Fn() -> V + 'static,
233    V: IntoView,
234{
235    let config = config.unwrap_or_default();
236    let wrapper_tag = wrapper.unwrap_or_else(|| "div".to_string());
237
238    let mut wrapper_class = "lucide-wrapper".to_string();
239    if let Some(ref class) = config.class {
240        wrapper_class.push(' ');
241        wrapper_class.push_str(class);
242    }
243
244    let mut wrapper_style = String::new();
245    if let Some(ref size) = config.size {
246        wrapper_style.push_str(&format!("width: {size}; height: {size};"));
247    }
248    if let Some(ref style) = config.style {
249        if !wrapper_style.is_empty() {
250            wrapper_style.push(' ');
251        }
252        wrapper_style.push_str(style);
253    }
254
255    // Create the wrapper with custom tag
256
257    match wrapper_tag.as_str() {
258        "span" => view! {
259            <span
260                class=wrapper_class.clone()
261                style=move || if wrapper_style.is_empty() {
262                    None
263                } else {
264                    Some(wrapper_style.clone())
265                }
266            >
267                {icon()}
268            </span>
269        }
270        .into_any(),
271
272        "button" => view! {
273            <button
274                class=wrapper_class.clone()
275                style=move || if wrapper_style.is_empty() {
276                    None
277                } else {
278                    Some(wrapper_style.clone())
279                }
280            >
281                {icon()}
282            </button>
283        }
284        .into_any(),
285
286        _ => view! {
287            <div
288                class=wrapper_class.clone()
289                style=move || if wrapper_style.is_empty() {
290                    None
291                } else {
292                    Some(wrapper_style.clone())
293                }
294            >
295                {icon()}
296            </div>
297        }
298        .into_any(),
299    }
300}
301
302// #[component]
303// pub fn Icon<F>(
304//     /// The icon component function
305//     icon: F,
306//     /// Optional configuration
307//     #[prop(optional)]
308//     config: Option<IconConfig>,
309//     /// Set wrapper element (defaults to "div")
310//     #[prop(optional)]
311//     wrapper: Option<String>,
312// ) -> impl IntoView
313// where
314//     F: Fn() -> impl IntoView + 'static,
315// {
316//     let config = config.unwrap_or_default();
317//     let wrapper_tag = wrapper.unwrap_or_else(|| "div".to_string());
318//
319//     let mut wrapper_class = "lucide-wrapper".to_string();
320//     if let Some(ref class) = config.class {
321//         wrapper_class.push(' ');
322//         wrapper_class.push_str(class);
323//     }
324//
325//     let mut wrapper_style = String::new();
326//     if let Some(ref size) = config.size {
327//         wrapper_style.push_str(&format!("width: {}; height: {};", size, size));
328//     }
329//     if let Some(ref style) = config.style {
330//         if !wrapper_style.is_empty() {
331//             wrapper_style.push(' ');
332//         }
333//         wrapper_style.push_str(style);
334//     }
335//
336//     // Create the wrapper with custom tag
337//     match wrapper_tag.as_str() {
338//         "span" => view! {
339//             <span
340//                 class=&wrapper_class
341//                 style=move || if wrapper_style.is_empty() { None } else { Some(wrapper_style.clone()) }
342//             >
343//                 {icon()}
344//             </span>
345//         }.into_view(),
346//         "button" => view! {
347//             <button
348//                 class=&wrapper_class
349//                 style=move || if wrapper_style.is_empty() { None } else { Some(wrapper_style.clone()) }
350//             >
351//                 {icon()}
352//             </button>
353//         }.into_view(),
354//         _ => view! {
355//             <div
356//                 class=&wrapper_class
357//                 style=move || if wrapper_style.is_empty() { None } else { Some(wrapper_style.clone()) }
358//             >
359//                 {icon()}
360//             </div>
361//         }.into_view(),
362//     }
363// }
364
365/// Utility macro for quick icon creation with builder pattern (enhanced)
366#[macro_export]
367macro_rules! icon {
368    ($icon:ident) => {
369        $icon()
370    };
371    ($icon:ident, $($method:ident($value:expr)),+ $(,)?) => {{
372        let config = leptos_lucide_rs::IconConfig::new()$(.$method($value))+;
373        leptos_lucide_rs::Icon(|| $icon(), Some(config), None)
374    }};
375    ($icon:ident, wrapper = $wrapper:expr, $($method:ident($value:expr)),+ $(,)?) => {{
376        let config = leptos_lucide_rs::IconConfig::new()$(.$method($value))+;
377        leptos_lucide_rs::Icon(|| $icon(), Some(config), Some($wrapper.to_string()))
378    }};
379}
380
381/// Simple macro to create dynamic icons by using the load_icon function
382#[macro_export]
383macro_rules! dynamic_icon {
384    ($name:expr) => {
385        #[cfg(leptos_lucide_generated)]
386        load_icon($name)
387
388        #[cfg(not(leptos_lucide_generated))]
389        fallback::load_icon($name)
390    };
391}
392
393/// Convenient alias for dynamic icons
394pub use dynamic_icon as dyn_icon;