ironhtml_bootstrap/lib.rs
1//! # ironhtml-bootstrap
2//!
3//! Type-safe Bootstrap 5.3 components for Rust.
4//!
5//! This crate provides ergonomic, type-safe Bootstrap component generation
6//! that integrates seamlessly with the `ironhtml` ecosystem. Build
7//! Bootstrap UIs with Rust's compile-time guarantees.
8//!
9//! ## Examples
10//!
11//! Complete examples demonstrating the library's capabilities:
12//!
13//! - **[Landing Page](https://github.com/LeakIX/ironhtml/blob/main/crates/ironhtml-bootstrap/examples/landing_page.rs)** -
14//! A complete `SaaS` landing page with navbar, hero section, features, testimonials, and footer.
15//!
16//! - **[Wallet Dashboard](https://github.com/LeakIX/ironhtml/blob/main/crates/ironhtml-bootstrap/examples/wallet_dashboard.rs)** -
17//! Dynamic page generation based on Rust parameters, showing conditional rendering.
18//!
19//! - **[Bootstrap Docs](https://github.com/LeakIX/ironhtml/blob/main/crates/ironhtml-bootstrap/examples/bootstrap_docs.rs)** -
20//! A documentation site replica showcasing all Bootstrap components.
21//!
22//! Run examples with: `cargo run --example landing_page`
23//!
24//! ## Available Components
25//!
26//! | Module | Description |
27//! |--------|-------------|
28//! | [`accordion`] | Collapsible content panels |
29//! | [`alerts`] | Contextual feedback messages |
30//! | [`badge`] | Small count and labeling component |
31//! | [`breadcrumb`] | Navigation hierarchy indicator |
32//! | [`buttons`] | Button styles and variants |
33//! | [`cards`] | Flexible content containers |
34//! | [`carousel`] | Slideshow component |
35//! | [`close_button`] | Dismissal buttons |
36//! | [`collapse`] | Collapsible content |
37//! | [`dropdown`] | Dropdown menus |
38//! | [`grid`] | Bootstrap grid system |
39//! | [`list_group`] | Flexible list display |
40//! | [`modal`] | Dialog overlays |
41//! | [`navbar`] | Responsive navigation |
42//! | [`offcanvas`] | Sidebar panels |
43//! | [`pagination`] | Page navigation |
44//! | [`placeholder`] | Loading placeholders |
45//! | [`progress`] | Progress bars |
46//! | [`spinner`] | Loading indicators |
47//! | [`toast`] | Push notifications |
48//! | [`tooltip`] | Tooltips and popovers |
49//!
50//! ## Quick Start
51//!
52//! ```rust
53//! use ironhtml_bootstrap::*;
54//!
55//! // Create a simple button
56//! let button = buttons::btn(Color::Primary, "Click me");
57//!
58//! // Create an alert
59//! let warning = alerts::alert(Color::Warning, "This is a warning!");
60//!
61//! // Create a card
62//! let card = cards::card_simple(
63//! "Welcome",
64//! "Thanks for using ironhtml-bootstrap!",
65//! "Learn more",
66//! "#docs"
67//! );
68//! ```
69//!
70//! ## Reusable Components (React-like Pattern)
71//!
72//! Define custom components as functions, just like React components.
73//! This is the recommended pattern for building maintainable UIs.
74//!
75//! ```rust
76//! use ironhtml_bootstrap::*;
77//! use ironhtml::typed::Element;
78//! use ironhtml_elements::{Button, Div, A};
79//!
80//! // ============================================================
81//! // REUSABLE COMPONENT: Product Card
82//! // ============================================================
83//! // Define once, use everywhere. Just like React components!
84//!
85//! struct Product {
86//! name: String,
87//! description: String,
88//! price: f64,
89//! image_url: String,
90//! }
91//!
92//! fn product_card(product: &Product) -> Element<Div> {
93//! cards::card_with_image(
94//! &product.image_url,
95//! &product.name,
96//! &product.name,
97//! &product.description,
98//! )
99//! }
100//!
101//! // ============================================================
102//! // REUSABLE COMPONENT: Pricing Tier
103//! // ============================================================
104//!
105//! struct PricingTier {
106//! name: &'static str,
107//! price: &'static str,
108//! features: Vec<&'static str>,
109//! highlighted: bool,
110//! }
111//!
112//! fn pricing_card(tier: &PricingTier) -> Element<Div> {
113//! let color = if tier.highlighted { Color::Primary } else { Color::Light };
114//!
115//! cards::card_colored(color, |body| {
116//! use ironhtml_elements::{H3, H4, Ul, Li, P};
117//!
118//! body.child::<H3, _>(|h| h.class("card-title").text(tier.name))
119//! .child::<H4, _>(|h| h.text(tier.price))
120//! .child::<Ul, _>(|ul| {
121//! tier.features.iter().fold(ul.class("list-unstyled"), |ul, feature| {
122//! ul.child::<Li, _>(|li| li.text(*feature))
123//! })
124//! })
125//! .child::<Button, _>(|_| {
126//! buttons::btn(Color::Primary, "Choose Plan")
127//! })
128//! })
129//! }
130//!
131//! // ============================================================
132//! // USAGE: Compose components into a page
133//! // ============================================================
134//!
135//! fn render_product_catalog(products: &[Product]) -> Element<Div> {
136//! grid::container(|c| {
137//! c.child::<Div, _>(|_| {
138//! grid::row(|r| {
139//! products.iter().fold(r, |row, product| {
140//! row.child::<Div, _>(|_| {
141//! grid::col(4, |col| {
142//! col.child::<Div, _>(|_| product_card(product))
143//! })
144//! })
145//! })
146//! })
147//! })
148//! })
149//! }
150//! ```
151//!
152//! ## Building a Complete Page
153//!
154//! Here's how to build a complete landing page with navbar, hero section,
155//! features, and footer:
156//!
157//! ```rust
158//! use ironhtml_bootstrap::*;
159//! use ironhtml::typed::{Document, Element};
160//! use ironhtml_elements::*;
161//!
162//! // ============================================================
163//! // COMPONENT: Navigation Menu
164//! // ============================================================
165//!
166//! struct MenuItem {
167//! href: &'static str,
168//! label: &'static str,
169//! active: bool,
170//! }
171//!
172//! fn main_navbar(brand: &str, items: &[MenuItem]) -> Element<Nav> {
173//! navbar::navbar(brand, NavbarExpand::Lg, "mainNav", |ul| {
174//! items.iter().fold(ul, |ul, item| {
175//! ul.child::<Li, _>(|_| navbar::nav_item(item.href, item.label, item.active))
176//! })
177//! })
178//! }
179//!
180//! // ============================================================
181//! // COMPONENT: Hero Section
182//! // ============================================================
183//!
184//! fn hero_section(title: &str, subtitle: &str, cta_text: &str, cta_href: &str) -> Element<Div> {
185//! Element::<Div>::new()
186//! .class("bg-primary text-white py-5")
187//! .child::<Div, _>(|_| {
188//! grid::container(|c| {
189//! c.class("text-center")
190//! .child::<H1, _>(|h| h.class("display-4").text(title))
191//! .child::<P, _>(|p| p.class("lead").text(subtitle))
192//! .child::<A, _>(|a| {
193//! a.class("btn btn-light btn-lg")
194//! .attr("href", cta_href)
195//! .text(cta_text)
196//! })
197//! })
198//! })
199//! }
200//!
201//! // ============================================================
202//! // COMPONENT: Feature Card
203//! // ============================================================
204//!
205//! struct Feature {
206//! icon: &'static str, // Bootstrap icon class
207//! title: &'static str,
208//! description: &'static str,
209//! }
210//!
211//! fn feature_card(feature: &Feature) -> Element<Div> {
212//! cards::card(|body| {
213//! body.class("text-center")
214//! .child::<I, _>(|i| i.class(feature.icon).class("fs-1 text-primary mb-3"))
215//! .child::<H5, _>(|h| h.class("card-title").text(feature.title))
216//! .child::<P, _>(|p| p.class("card-text text-muted").text(feature.description))
217//! })
218//! }
219//!
220//! fn features_section(title: &str, features: &[Feature]) -> Element<Div> {
221//! Element::<Div>::new()
222//! .class("py-5")
223//! .child::<Div, _>(|_| {
224//! grid::container(|c| {
225//! c.child::<H2, _>(|h| h.class("text-center mb-5").text(title))
226//! .child::<Div, _>(|_| {
227//! grid::row_gutter(4, |r| {
228//! features.iter().fold(r, |row, feature| {
229//! row.child::<Div, _>(|_| {
230//! grid::col(4, |col| {
231//! col.child::<Div, _>(|_| feature_card(feature))
232//! })
233//! })
234//! })
235//! })
236//! })
237//! })
238//! })
239//! }
240//!
241//! // ============================================================
242//! // COMPONENT: Footer
243//! // ============================================================
244//!
245//! fn footer(copyright: &str) -> Element<Footer> {
246//! Element::<Footer>::new()
247//! .class("bg-dark text-white py-4 mt-5")
248//! .child::<Div, _>(|_| {
249//! grid::container(|c| {
250//! c.class("text-center")
251//! .child::<P, _>(|p| p.class("mb-0").text(copyright))
252//! })
253//! })
254//! }
255//!
256//! // ============================================================
257//! // COMPOSE: Full Landing Page
258//! // ============================================================
259//!
260//! fn landing_page() -> Document {
261//! let menu_items = vec![
262//! MenuItem { href: "/", label: "Home", active: true },
263//! MenuItem { href: "/features", label: "Features", active: false },
264//! MenuItem { href: "/pricing", label: "Pricing", active: false },
265//! MenuItem { href: "/contact", label: "Contact", active: false },
266//! ];
267//!
268//! let features = vec![
269//! Feature { icon: "bi bi-lightning", title: "Fast", description: "Blazing fast performance" },
270//! Feature { icon: "bi bi-shield", title: "Secure", description: "Enterprise-grade security" },
271//! Feature { icon: "bi bi-gear", title: "Flexible", description: "Highly customizable" },
272//! ];
273//!
274//! Document::new()
275//! .doctype()
276//! .root::<Html, _>(|html| {
277//! html.attr("lang", "en")
278//! .child::<Head, _>(|h| {
279//! h.child::<Meta, _>(|m| m.attr("charset", "UTF-8"))
280//! .child::<Title, _>(|t| t.text("My App"))
281//! .child::<Link, _>(|l| {
282//! l.attr("href", "https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css")
283//! .attr("rel", "stylesheet")
284//! })
285//! })
286//! .child::<Body, _>(|body| {
287//! body.child::<Nav, _>(|_| main_navbar("MyApp", &menu_items))
288//! .child::<Div, _>(|_| hero_section(
289//! "Build Amazing Apps",
290//! "The fastest way to create beautiful web applications",
291//! "Get Started",
292//! "#signup"
293//! ))
294//! .child::<Div, _>(|_| features_section("Features", &features))
295//! .child::<Footer, _>(|_| footer("© 2024 MyApp. All rights reserved."))
296//! })
297//! })
298//! }
299//! ```
300//!
301//! ## Dashboard Example
302//!
303//! ```rust
304//! use ironhtml_bootstrap::*;
305//! use ironhtml::typed::Element;
306//! use ironhtml_elements::*;
307//!
308//! // ============================================================
309//! // COMPONENT: Stat Card (reusable)
310//! // ============================================================
311//!
312//! fn stat_card(title: &str, value: &str, color: Color, trend: &str) -> Element<Div> {
313//! cards::card_border(color, |body| {
314//! body.child::<H6, _>(|h| h.class("text-muted").text(title))
315//! .child::<H2, _>(|h| h.class("mb-0").text(value))
316//! .child::<Small, _>(|s| s.class("text-success").text(trend))
317//! })
318//! }
319//!
320//! // ============================================================
321//! // COMPONENT: Activity Item (reusable)
322//! // ============================================================
323//!
324//! struct Activity {
325//! user: String,
326//! action: String,
327//! time: String,
328//! }
329//!
330//! fn activity_item(activity: &Activity) -> Element<Li> {
331//! Element::<Li>::new()
332//! .class("list-group-item d-flex justify-content-between")
333//! .child::<Div, _>(|d| {
334//! d.child::<Strong, _>(|s| s.text(&activity.user))
335//! .text(" ")
336//! .child::<Span, _>(|s| s.text(&activity.action))
337//! })
338//! .child::<Small, _>(|s| s.class("text-muted").text(&activity.time))
339//! }
340//!
341//! // ============================================================
342//! // COMPOSE: Dashboard Page
343//! // ============================================================
344//!
345//! fn dashboard() -> Element<Div> {
346//! let activities = vec![
347//! Activity { user: "John".into(), action: "created a new project".into(), time: "5m ago".into() },
348//! Activity { user: "Jane".into(), action: "pushed 3 commits".into(), time: "10m ago".into() },
349//! Activity { user: "Bob".into(), action: "deployed to production".into(), time: "1h ago".into() },
350//! ];
351//!
352//! grid::container(|c| {
353//! c.class("py-4")
354//! // Stats row
355//! .child::<Div, _>(|_| {
356//! grid::row_gutter(4, |r| {
357//! r.child::<Div, _>(|_| grid::col(3, |c| c.child::<Div, _>(|_| stat_card("Users", "1,234", Color::Primary, "+12%"))))
358//! .child::<Div, _>(|_| grid::col(3, |c| c.child::<Div, _>(|_| stat_card("Revenue", "$45K", Color::Success, "+8%"))))
359//! .child::<Div, _>(|_| grid::col(3, |c| c.child::<Div, _>(|_| stat_card("Orders", "567", Color::Info, "+23%"))))
360//! .child::<Div, _>(|_| grid::col(3, |c| c.child::<Div, _>(|_| stat_card("Tickets", "12", Color::Warning, "-5%"))))
361//! })
362//! })
363//! // Alert
364//! .child::<Div, _>(|_| {
365//! alerts::alert_dismissible(Color::Info, "Welcome to your dashboard!")
366//! })
367//! // Activity section
368//! .child::<Div, _>(|_| {
369//! grid::row(|r| {
370//! r.child::<Div, _>(|_| {
371//! grid::col(8, |c| {
372//! c.child::<Div, _>(|_| {
373//! cards::card(|body| {
374//! body.child::<H5, _>(|h| h.text("Recent Activity"))
375//! .child::<Ul, _>(|ul| {
376//! activities.iter().fold(ul.class("list-group list-group-flush"), |ul, act| {
377//! ul.child::<Li, _>(|_| activity_item(act))
378//! })
379//! })
380//! })
381//! })
382//! })
383//! })
384//! .child::<Div, _>(|_| {
385//! grid::col(4, |c| {
386//! c.child::<Div, _>(|_| {
387//! cards::card(|body| {
388//! body.child::<H5, _>(|h| h.text("Quick Actions"))
389//! .child::<Div, _>(|d| {
390//! d.class("d-grid gap-2")
391//! .child::<Button, _>(|_| buttons::btn(Color::Primary, "New Project"))
392//! .child::<Button, _>(|_| buttons::btn_outline(Color::Secondary, "View Reports"))
393//! .child::<Button, _>(|_| buttons::btn_outline(Color::Success, "Export Data"))
394//! })
395//! })
396//! })
397//! })
398//! })
399//! })
400//! })
401//! })
402//! }
403//! ```
404//!
405//! ## Features
406//!
407//! - **Type-safe**: All components are strongly typed
408//! - **Composable**: Build complex UIs from simple components
409//! - **Zero runtime overhead**: All abstractions compile away
410//! - **Bootstrap 5.3**: Full support for the latest Bootstrap
411//! - **No JavaScript required**: Pure HTML output (add Bootstrap JS separately if needed)
412//!
413//! ## Feature Flags
414//!
415//! - `v5` (default): Bootstrap 5.3 support
416
417#![no_std]
418
419extern crate alloc;
420
421pub mod accordion;
422pub mod alerts;
423pub mod badge;
424pub mod breadcrumb;
425pub mod buttons;
426pub mod cards;
427pub mod carousel;
428pub mod close_button;
429pub mod collapse;
430pub mod dropdown;
431pub mod grid;
432pub mod list_group;
433pub mod modal;
434pub mod navbar;
435pub mod offcanvas;
436pub mod pagination;
437pub mod placeholder;
438pub mod progress;
439pub mod spinner;
440pub mod toast;
441pub mod tooltip;
442mod types;
443
444pub use types::{Breakpoint, Color, NavbarExpand, Size};
445
446// Re-export ironhtml and ironhtml_elements for convenience
447pub use ironhtml;
448pub use ironhtml_elements;
449pub use ironhtml_macro;
450
451#[cfg(test)]
452mod tests {
453 use super::{Breakpoint, Color, NavbarExpand, Size};
454
455 #[test]
456 fn test_reexports() {
457 // Ensure types are accessible
458 let _ = Color::Primary;
459 let _ = Size::Large;
460 let _ = Breakpoint::Md;
461 let _ = NavbarExpand::Lg;
462 }
463}