Skip to main content

ferrox_front_ui/
components.rs

1//! # Glassmorphism Design System Components (`ferrox-front-ui::components`)
2//!
3//! `components` provides an enterprise-grade Glassmorphism component library:
4//! Buttons, Badges, Cards, Form Controls, Progress Bars, Spinners, Avatars, User Pills, and Alerts.
5
6use ferrox_front_core::dom::{div, span, button as dom_button, DomBuilder};
7
8/// Renders a Glassmorphism Card container
9pub fn card() -> DomBuilder {
10    div()
11        .attr("class", "ferrox-glass-card")
12        .attr("style", "background: var(--bg-glass); backdrop-filter: blur(12px); border: var(--border-glass); border-radius: 12px; padding: 1.25rem; box-shadow: 0 8px 32px rgba(0,0,0,0.37); display: flex; flex-direction: column;")
13}
14
15/// Renders a Card Header with title text
16pub fn card_header(title: &str) -> DomBuilder {
17    div()
18        .attr("style", "font-weight: 700; font-size: 1.15rem; margin-bottom: 1rem; border-bottom: var(--border-glass); padding-bottom: 0.6rem; color: var(--text-primary);")
19        .text(title)
20}
21
22/// Renders a Card Body content container
23pub fn card_body() -> DomBuilder {
24    div().attr("style", "flex: 1;")
25}
26
27/// Renders a Card Footer container
28pub fn card_footer() -> DomBuilder {
29    div().attr("style", "margin-top: 1rem; padding-top: 0.6rem; border-top: var(--border-glass); display: flex; gap: 8px; justify-content: flex-end;")
30}
31
32/// Renders a Design System Button with variants: "primary", "secondary", "outline", "glass", "danger", "ghost"
33pub fn button(text: &str, variant: &str) -> DomBuilder {
34    let (bg, color, border) = match variant {
35        "primary" => ("var(--btn-bg, linear-gradient(135deg, #7e22ce, #3b82f6))", "#ffffff", "none"),
36        "secondary" => ("rgba(255,255,255,0.1)", "var(--text-primary)", "var(--border-glass)"),
37        "danger" => ("linear-gradient(135deg, #dc2626, #991b1b)", "#ffffff", "none"),
38        "outline" => ("transparent", "var(--accent-cyan, #06b6d4)", "1px solid var(--accent-cyan, #06b6d4)"),
39        "ghost" => ("transparent", "var(--text-primary)", "none"),
40        _ => ("var(--bg-glass)", "var(--text-primary)", "var(--border-glass)"), // "glass"
41    };
42
43    let style = format!(
44        "background: {}; color: {}; border: {}; padding: 8px 16px; border-radius: 6px; font-weight: 600; font-size: 0.9rem; cursor: pointer; transition: all 0.2s ease; display: inline-flex; align-items: center; justify-content: center; gap: 6px;",
45        bg, color, border
46    );
47
48    dom_button().attr("class", &format!("ferrox-btn ferrox-btn-{}", variant)).attr("style", &style).text(text)
49}
50
51/// Renders a Status Badge pill: "primary", "success", "warning", "danger", "info"
52pub fn badge(text: &str, variant: &str) -> DomBuilder {
53    let (bg, color) = match variant {
54        "success" => ("rgba(16, 185, 129, 0.2)", "#34d399"),
55        "danger" => ("rgba(239, 68, 68, 0.2)", "#fca5a5"),
56        "warning" => ("rgba(245, 158, 11, 0.2)", "#fde047"),
57        "info" => ("rgba(14, 165, 233, 0.2)", "#7dd3fc"),
58        _ => ("rgba(147, 51, 234, 0.2)", "#c084fc"),
59    };
60
61    let style = format!(
62        "background: {}; color: {}; border: var(--border-glass); padding: 3px 10px; border-radius: 9999px; font-size: 0.78rem; font-weight: 700; display: inline-block;",
63        bg, color
64    );
65
66    span().attr("class", &format!("ferrox-badge ferrox-badge-{}", variant)).attr("style", &style).text(text)
67}
68
69/// Renders a Notification Alert banner: "success", "danger", "warning", "info"
70pub fn alert(message: &str, variant: &str) -> DomBuilder {
71    let (bg, color) = match variant {
72        "danger" => ("rgba(239, 68, 68, 0.15)", "#fca5a5"),
73        "success" => ("rgba(16, 185, 129, 0.15)", "#34d399"),
74        "warning" => ("rgba(245, 158, 11, 0.15)", "#fde047"),
75        _ => ("rgba(147, 51, 234, 0.15)", "#c084fc"),
76    };
77    
78    let style = format!(
79        "padding: 1rem; border-radius: 8px; background: {}; color: {}; border: var(--border-glass); margin-bottom: 1rem; font-weight: 500; font-size: 0.92rem;",
80        bg, color
81    );
82
83    div().attr("class", &format!("ferrox-alert ferrox-alert-{}", variant)).attr("style", &style).text(message)
84}
85
86/// Renders a Form Input field
87pub fn input(input_type: &str, placeholder: &str) -> DomBuilder {
88    let style = "width: 100%; padding: 10px 14px; border-radius: 6px; background: var(--bg-primary); color: var(--text-primary); border: var(--border-glass); font-size: 0.9rem; outline: none;";
89    DomBuilder::new("input")
90        .attr("type", input_type)
91        .attr("placeholder", placeholder)
92        .attr("class", "ferrox-input")
93        .attr("style", style)
94}
95
96/// Wraps a label and input element in a form group
97pub fn form_group(label_text: &str, input_element: DomBuilder) -> DomBuilder {
98    div()
99        .attr("class", "ferrox-form-group")
100        .attr("style", "margin-bottom: 1rem; display: flex; flex-direction: column; gap: 6px;")
101        .child(
102            DomBuilder::new("label")
103                .attr("style", "font-size: 0.85rem; font-weight: 600; color: var(--accent-cyan);")
104                .text(label_text)
105        )
106        .child(input_element)
107}
108
109/// Renders a Progress Bar (percentage 0.0 to 100.0)
110pub fn progress_bar(percentage: f64) -> DomBuilder {
111    let pct = percentage.clamp(0.0, 100.0);
112    div()
113        .attr("class", "ferrox-progress-track")
114        .attr("style", "width: 100%; height: 8px; background: rgba(255,255,255,0.1); border-radius: 9999px; overflow: hidden;")
115        .child(
116            div()
117                .attr("class", "ferrox-progress-fill")
118                .attr("style", &format!("width: {}%; height: 100%; background: var(--btn-bg, linear-gradient(135deg, #7e22ce, #3b82f6)); transition: width 0.3s ease;", pct))
119        )
120}
121
122/// Renders an Avatar circle with user initials
123pub fn avatar(initials: &str) -> DomBuilder {
124    div()
125        .attr("class", "ferrox-avatar")
126        .attr("style", "width: 38px; height: 38px; border-radius: 50%; background: var(--btn-bg); color: #fff; font-weight: 700; font-size: 0.88rem; display: inline-flex; align-items: center; justify-content: center; border: var(--border-glass);")
127        .text(initials)
128}
129
130/// Renders a User Pill component (Avatar + Name + Role)
131pub fn user_pill(name: &str, role: &str) -> DomBuilder {
132    let initials = name.split_whitespace().filter_map(|w| w.chars().next()).collect::<String>();
133    div()
134        .attr("class", "ferrox-user-pill")
135        .attr("style", "display: inline-flex; align-items: center; gap: 10px; padding: 4px 12px 4px 4px; background: var(--bg-glass); border: var(--border-glass); border-radius: 9999px;")
136        .child(avatar(&initials))
137        .child(
138            div()
139                .attr("style", "display: flex; flex-direction: column;")
140                .child(span().attr("style", "font-weight: 600; font-size: 0.85rem; color: var(--text-primary);").text(name))
141                .child(span().attr("style", "font-size: 0.72rem; opacity: 0.7; color: var(--accent-cyan);").text(role))
142        )
143}
144
145/// Renders a Loading Spinner
146pub fn spinner() -> DomBuilder {
147    div()
148        .attr("class", "ferrox-spinner")
149        .attr("style", "width: 24px; height: 24px; border: 3px solid rgba(255,255,255,0.2); border-top-color: var(--accent-cyan); border-radius: 50%; animation: ferrox-spin 0.8s linear infinite;")
150}
151
152/// Renders a multi-line Textarea input
153pub fn textarea(placeholder: &str, rows: u32) -> DomBuilder {
154    let style = "width: 100%; padding: 10px 14px; border-radius: 6px; background: var(--bg-primary); color: var(--text-primary); border: var(--border-glass); font-size: 0.9rem; outline: none; font-family: inherit; resize: vertical;";
155    DomBuilder::new("textarea")
156        .attr("rows", &rows.to_string())
157        .attr("placeholder", placeholder)
158        .attr("class", "ferrox-textarea")
159        .attr("style", style)
160}
161
162/// Renders a Select Dropdown input with options
163pub fn select(options: Vec<(&str, &str)>) -> DomBuilder {
164    let style = "width: 100%; padding: 10px 14px; border-radius: 6px; background: var(--bg-primary); color: var(--text-primary); border: var(--border-glass); font-size: 0.9rem; outline: none; cursor: pointer;";
165    let mut sel = DomBuilder::new("select")
166        .attr("class", "ferrox-select")
167        .attr("style", style);
168
169    for (val, label_text) in options {
170        let opt = DomBuilder::new("option")
171            .attr("value", val)
172            .text(label_text);
173        sel = sel.child(opt);
174    }
175    sel
176}
177
178/// Renders a Styled Checkbox input with label
179pub fn checkbox(label_text: &str, checked: bool) -> DomBuilder {
180    let mut cb = DomBuilder::new("input")
181        .attr("type", "checkbox")
182        .attr("style", "accent-color: var(--accent-cyan); width: 16px; height: 16px; cursor: pointer;");
183    if checked {
184        cb = cb.attr("checked", "true");
185    }
186
187    DomBuilder::new("label")
188        .attr("style", "display: inline-flex; align-items: center; gap: 8px; font-size: 0.88rem; cursor: pointer; color: var(--text-primary);")
189        .child(cb)
190        .child(span().text(label_text))
191}
192
193/// Renders a Radio Button input with label
194pub fn radio(name: &str, value: &str, label_text: &str, checked: bool) -> DomBuilder {
195    let mut rb = DomBuilder::new("input")
196        .attr("type", "radio")
197        .attr("name", name)
198        .attr("value", value)
199        .attr("style", "accent-color: var(--accent-cyan); width: 16px; height: 16px; cursor: pointer;");
200    if checked {
201        rb = rb.attr("checked", "true");
202    }
203
204    DomBuilder::new("label")
205        .attr("style", "display: inline-flex; align-items: center; gap: 8px; font-size: 0.88rem; cursor: pointer; color: var(--text-primary);")
206        .child(rb)
207        .child(span().text(label_text))
208}
209
210/// Renders a Toggle Switch input with label
211pub fn toggle_switch(label_text: &str, checked: bool) -> DomBuilder {
212    let bg = if checked { "var(--accent-cyan, #06b6d4)" } else { "rgba(255,255,255,0.2)" };
213    let translate = if checked { "transform: translateX(18px);" } else { "transform: translateX(0px);" };
214
215    let track = div()
216        .attr("style", &format!("width: 38px; height: 20px; border-radius: 9999px; background: {}; position: relative; transition: background 0.3s; padding: 2px; cursor: pointer;", bg))
217        .child(
218            div()
219                .attr("style", &format!("width: 16px; height: 16px; border-radius: 50%; background: #ffffff; transition: transform 0.3s; {}", translate))
220        );
221
222    div()
223        .attr("style", "display: inline-flex; align-items: center; gap: 10px; cursor: pointer;")
224        .child(track)
225        .child(span().attr("style", "font-size: 0.88rem; font-weight: 500;").text(label_text))
226}
227
228/// Renders a Range Slider input
229pub fn range_slider(min: i32, max: i32, value: i32) -> DomBuilder {
230    DomBuilder::new("input")
231        .attr("type", "range")
232        .attr("min", &min.to_string())
233        .attr("max", &max.to_string())
234        .attr("value", &value.to_string())
235        .attr("class", "ferrox-range")
236        .attr("style", "width: 100%; accent-color: var(--accent-cyan); cursor: pointer;")
237}
238
239/// Renders a Search Bar Input with Icon
240pub fn search_input(placeholder: &str) -> DomBuilder {
241    div()
242        .attr("style", "position: relative; width: 100%;")
243        .child(
244            DomBuilder::new("input")
245                .attr("type", "search")
246                .attr("placeholder", placeholder)
247                .attr("style", "width: 100%; padding: 10px 14px 10px 36px; border-radius: 6px; background: var(--bg-primary); color: var(--text-primary); border: var(--border-glass); font-size: 0.9rem; outline: none;")
248        )
249        .child(
250            span()
251                .attr("style", "position: absolute; left: 12px; top: 50%; transform: translateY(-50%); opacity: 0.6; font-size: 0.9rem;")
252                .text("🔍")
253        )
254}
255
256/// Renders a File Upload Dropzone
257pub fn file_upload(accept: &str) -> DomBuilder {
258    div()
259        .attr("style", "border: 2px dashed var(--border-glass); border-radius: 8px; padding: 1.5rem; text-align: center; background: rgba(0,0,0,0.2); cursor: pointer; display: flex; flex-direction: column; align-items: center; gap: 6px;")
260        .child(span().attr("style", "font-size: 1.5rem;").text("📁"))
261        .child(span().attr("style", "font-size: 0.88rem; font-weight: 600; color: var(--accent-cyan);").text("Click or drop files to upload"))
262        .child(span().attr("style", "font-size: 0.75rem; opacity: 0.6;").text(&format!("Supported formats: {}", accept)))
263        .child(
264            DomBuilder::new("input")
265                .attr("type", "file")
266                .attr("accept", accept)
267                .attr("style", "display: none;")
268        )
269}