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;