Skip to main content

radix_leptos_primitives/theming/layout_system/
mod.rs

1use crate::utils::merge_classes;
2use leptos::callback::Callback;
3use leptos::prelude::*;
4use serde::{Deserialize, Serialize};
5
6// Re-export all types from sub-modules
7pub use container::*;
8pub use responsive::*;
9pub use spacing::*;
10
11// Sub-modules
12pub mod container;
13pub mod responsive;
14pub mod spacing;
15
16/// Layout system for consistent spacing and alignment
17#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
18pub struct LayoutSystem {
19    pub spacing: SpacingSystem,
20    pub breakpoints: BreakpointSystem,
21    pub grid: GridSystem,
22    pub flexbox: FlexboxSystem,
23    pub containers: ContainerSystem,
24}
25
26/// Layout builder component
27#[component]
28pub fn LayoutBuilder(
29    #[prop(optional)] class: Option<String>,
30    #[prop(optional)] style: Option<String>,
31    #[prop(optional)] layout_type: Option<String>,
32    #[prop(optional)] on_layout_change: Option<Callback<LayoutSystem>>,
33) -> impl IntoView {
34    let layout_type = layout_type.unwrap_or_else(|| "grid".to_string());
35    let on_layout_change = on_layout_change.unwrap_or_else(|| Callback::new(|_| {}));
36
37    let class = merge_classes(
38        [
39            "layout-builder",
40            &layout_type,
41            class.as_deref().unwrap_or(""),
42        ]
43        .to_vec(),
44    );
45
46    let (layout, set_layout) = signal(LayoutSystem::default());
47
48    let handle_layout_change = move |new_layout: LayoutSystem| {
49        set_layout.set(new_layout.clone());
50        on_layout_change.run(new_layout);
51    };
52
53    view! {
54        <div
55            class=class
56            style=style
57            role="form"
58            aria-label="Layout system builder"
59        >
60            <div class="layout-builder-header">
61                <h3>"Layout System"</h3>
62                <p>"Configure spacing, breakpoints, and layout utilities"</p>
63            </div>
64
65            <div class="layout-sections">
66                <SpacingLayoutSection
67                    title="Spacing System".to_string()
68                    layout_type="spacing".to_string()
69                    layout=layout.get().spacing
70                    on_change=Callback::new(move |spacing| {
71                        let mut new_layout = layout.get();
72                        new_layout.spacing = spacing;
73                        handle_layout_change(new_layout);
74                    })
75                />
76
77                <BreakpointLayoutSection
78                    title="Breakpoint System".to_string()
79                    layout_type="breakpoints".to_string()
80                    layout=layout.get().breakpoints
81                    on_change=Callback::new(move |breakpoints| {
82                        let mut new_layout = layout.get();
83                        new_layout.breakpoints = breakpoints;
84                        handle_layout_change(new_layout);
85                    })
86                />
87
88                <GridLayoutSection
89                    title="Grid System".to_string()
90                    grid=layout.get().grid
91                    on_change=Callback::new(move |grid| {
92                        let mut new_layout = layout.get();
93                        new_layout.grid = grid;
94                        handle_layout_change(new_layout);
95                    })
96                />
97
98                <FlexboxLayoutSection
99                    title="Flexbox System".to_string()
100                    flexbox=layout.get().flexbox
101                    on_change=Callback::new(move |flexbox| {
102                        let mut new_layout = layout.get();
103                        new_layout.flexbox = flexbox;
104                        handle_layout_change(new_layout);
105                    })
106                />
107
108                <ContainerLayoutSection
109                    title="Container System".to_string()
110                    containers=layout.get().containers
111                    on_change=Callback::new(move |containers| {
112                        let mut new_layout = layout.get();
113                        new_layout.containers = containers;
114                        handle_layout_change(new_layout);
115                    })
116                />
117            </div>
118        </div>
119    }
120}
121
122/// Spacing layout section component
123#[component]
124pub fn SpacingLayoutSection(
125    #[prop(optional)] class: Option<String>,
126    #[prop(optional)] style: Option<String>,
127    #[prop(optional)] title: Option<String>,
128    #[prop(optional)] layout_type: Option<String>,
129    #[prop(optional)] layout: Option<SpacingSystem>,
130    #[prop(optional)] on_change: Option<Callback<SpacingSystem>>,
131) -> impl IntoView {
132    let title = title.unwrap_or_default();
133    let layout_type = layout_type.unwrap_or_default();
134    let layout = layout.unwrap_or_default();
135    let on_change = on_change.unwrap_or_else(|| Callback::new(|_| {}));
136    let layout_clone = layout.clone();
137
138    let class = merge_classes(
139        [
140            "layout-section",
141            &layout_type,
142            class.as_deref().unwrap_or(""),
143        ]
144        .to_vec(),
145    );
146
147    view! {
148        <div
149            class=class
150            style=style
151            data-layout-type=layout_type.clone()
152        >
153            <h4 class="section-title">{title}</h4>
154
155            <div class="layout-options">
156                <LayoutOptionGroup
157                    title="Base Unit".to_string()
158                    value=layout.base_unit
159                    on_change=Callback::new(move |base_unit| {
160                        let mut new_layout = layout.clone();
161                        new_layout.base_unit = base_unit;
162                        on_change.run(new_layout);
163                    })
164                />
165
166                <LayoutOptionGroup
167                    title="Scale".to_string()
168                    values=layout_clone.scale.clone()
169                    on_values_change=Callback::new(move |scale| {
170                        let mut new_layout = layout_clone.clone();
171                        new_layout.scale = scale;
172                        on_change.run(new_layout);
173                    })
174                />
175            </div>
176        </div>
177    }
178}
179
180/// Breakpoint layout section component
181#[component]
182pub fn BreakpointLayoutSection(
183    #[prop(optional)] class: Option<String>,
184    #[prop(optional)] style: Option<String>,
185    #[prop(optional)] title: Option<String>,
186    #[prop(optional)] layout_type: Option<String>,
187    #[prop(optional)] layout: Option<BreakpointSystem>,
188    #[prop(optional)] on_change: Option<Callback<BreakpointSystem>>,
189) -> impl IntoView {
190    let title = title.unwrap_or_default();
191    let layout_type = layout_type.unwrap_or_default();
192    let layout = layout.unwrap_or_default();
193    let on_change = on_change.unwrap_or_else(|| Callback::new(|_| {}));
194    let layout_clone = layout.clone();
195
196    let class = merge_classes(
197        [
198            "layout-section",
199            &layout_type,
200            class.as_deref().unwrap_or(""),
201        ]
202        .to_vec(),
203    );
204
205    view! {
206        <div
207            class=class
208            style=style
209            data-layout-type=layout_type.clone()
210        >
211            <h4 class="section-title">{title}</h4>
212
213            <div class="layout-options">
214                <div class="breakpoint-info">
215                    <p>"Breakpoints: " {layout.breakpoints.len()}</p>
216                    <p>"Container Max Widths: " {layout.container_max_widths.len()}</p>
217                </div>
218            </div>
219        </div>
220    }
221}
222
223/// Layout option group component
224#[component]
225pub fn LayoutOptionGroup(
226    #[prop(optional)] class: Option<String>,
227    #[prop(optional)] style: Option<String>,
228    #[prop(optional)] title: Option<String>,
229    #[prop(optional)] value: Option<f64>,
230    #[prop(optional)] values: Option<Vec<f64>>,
231    #[prop(optional)] on_change: Option<Callback<f64>>,
232    #[prop(optional)] on_values_change: Option<Callback<Vec<f64>>>,
233) -> impl IntoView {
234    let title = title.unwrap_or_default();
235    let value = value.unwrap_or(0.0);
236    let _values = values.clone().unwrap_or_default();
237    let on_change = on_change.unwrap_or_else(|| Callback::new(|_| {}));
238    let on_values_change = on_values_change.unwrap_or_else(|| Callback::new(|_| {}));
239
240    let class = merge_classes(["layout-option-group", class.as_deref().unwrap_or("")].to_vec());
241
242    view! {
243        <div
244            class=class
245            style=style
246        >
247            <h5 class="option-group-title">{title}</h5>
248            <div class="option-list">
249                {if let Some(values) = values {
250                    if !values.is_empty() {
251                        view! {
252                            <div class="values-list">
253                                {values.into_iter().map(|val| {
254                                    view! {
255                                        <div class="layout-value" data-value=val.to_string()>
256                                            <span class="value-number">{val}</span>
257                                        </div>
258                                    }
259                                }).collect::<Vec<_>>()}
260                            </div>
261                        }.into_any()
262                    } else {
263                        view! { <div></div> }.into_any()
264                    }
265                } else {
266                    view! { <div></div> }.into_any()
267                }}
268            </div>
269        </div>
270    }
271}
272
273/// Grid layout section component
274#[component]
275pub fn GridLayoutSection(
276    #[prop(optional)] title: Option<String>,
277    #[prop(optional)] grid: Option<GridSystem>,
278    #[prop(optional)] on_change: Option<Callback<GridSystem>>,
279    #[prop(optional)] class: Option<String>,
280    #[prop(optional)] style: Option<String>,
281) -> impl IntoView {
282    let title = title.unwrap_or_else(|| "Grid System".to_string());
283    let grid = grid.unwrap_or_default();
284    let on_change = on_change.unwrap_or_else(|| Callback::new(|_| {}));
285    let grid_clone1 = grid.clone();
286    let grid_clone2 = grid.clone();
287    let grid_clone3 = grid.clone();
288
289    let class = merge_classes(
290        [
291            "layout-section",
292            "grid-section",
293            class.as_deref().unwrap_or(""),
294        ]
295        .to_vec(),
296    );
297
298    view! {
299        <div
300            class=class
301            style=style
302            data-layout-type="grid"
303        >
304            <h4 class="section-title">{title}</h4>
305
306            <div class="layout-options">
307                <LayoutOptionGroup
308                    title="Columns".to_string()
309                    values=[grid.columns as f64].to_vec()
310                    on_values_change=Callback::new(move |columns: Vec<f64>| {
311                        let mut new_grid = grid_clone1.clone();
312                        new_grid.columns = columns[0] as u32;
313                        on_change.run(new_grid);
314                    })
315                />
316
317                <LayoutOptionGroup
318                    title="Gutters".to_string()
319                    values=grid.gutters.clone()
320                    on_values_change=Callback::new(move |gutters| {
321                        let mut new_grid = grid_clone2.clone();
322                        new_grid.gutters = gutters;
323                        on_change.run(new_grid);
324                    })
325                />
326
327                <LayoutOptionGroup
328                    title="Gaps".to_string()
329                    values=grid.gaps.clone()
330                    on_values_change=Callback::new(move |gaps| {
331                        let mut new_grid = grid_clone3.clone();
332                        new_grid.gaps = gaps;
333                        on_change.run(new_grid);
334                    })
335                />
336            </div>
337        </div>
338    }
339}
340
341/// Flexbox layout section component
342#[component]
343pub fn FlexboxLayoutSection(
344    #[prop(optional)] title: Option<String>,
345    #[prop(optional)] flexbox: Option<FlexboxSystem>,
346    #[prop(optional)] on_change: Option<Callback<FlexboxSystem>>,
347    #[prop(optional)] class: Option<String>,
348    #[prop(optional)] style: Option<String>,
349) -> impl IntoView {
350    let title = title.unwrap_or_else(|| "Flexbox System".to_string());
351    let flexbox = flexbox.unwrap_or_default();
352    let on_change = on_change.unwrap_or_else(|| Callback::new(|_| {}));
353    let flexbox_clone1 = flexbox.clone();
354    let flexbox_clone2 = flexbox.clone();
355    let flexbox_clone3 = flexbox.clone();
356
357    let class = merge_classes(
358        [
359            "layout-section",
360            "flexbox-section",
361            class.as_deref().unwrap_or(""),
362        ]
363        .to_vec(),
364    );
365
366    view! {
367        <div
368            class=class
369            style=style
370            data-layout-type="flexbox"
371        >
372            <h4 class="section-title">{title}</h4>
373
374            <div class="layout-options">
375                <LayoutOptionGroup
376                    title="Directions".to_string()
377                    values=flexbox.directions.iter().map(|d| d.as_str().len() as f64).collect()
378                    on_values_change=Callback::new(move |_directions| {
379                        // In a real implementation, this would update the directions
380                        let new_flexbox = flexbox_clone1.clone();
381                        on_change.run(new_flexbox);
382                    })
383                />
384
385                <LayoutOptionGroup
386                    title="Wraps".to_string()
387                    values=flexbox.wraps.iter().map(|w| w.as_str().len() as f64).collect()
388                    on_values_change=Callback::new(move |_wraps| {
389                        // In a real implementation, this would update the wraps
390                        let new_flexbox = flexbox_clone2.clone();
391                        on_change.run(new_flexbox);
392                    })
393                />
394
395                <LayoutOptionGroup
396                    title="Justifications".to_string()
397                    values=flexbox.justifications.iter().map(|j| j.as_str().len() as f64).collect()
398                    on_values_change=Callback::new(move |_justifications| {
399                        // In a real implementation, this would update the justifications
400                        let new_flexbox = flexbox_clone3.clone();
401                        on_change.run(new_flexbox);
402                    })
403                />
404            </div>
405        </div>
406    }
407}
408
409/// Container layout section component
410#[component]
411pub fn ContainerLayoutSection(
412    #[prop(optional)] title: Option<String>,
413    #[prop(optional)] containers: Option<ContainerSystem>,
414    #[prop(optional)] on_change: Option<Callback<ContainerSystem>>,
415    #[prop(optional)] class: Option<String>,
416    #[prop(optional)] style: Option<String>,
417) -> impl IntoView {
418    let title = title.unwrap_or_else(|| "Container System".to_string());
419    let containers = containers.unwrap_or_default();
420    let on_change = on_change.unwrap_or_else(|| Callback::new(|_| {}));
421    let containers_clone1 = containers.clone();
422    let containers_clone2 = containers.clone();
423
424    let class = merge_classes(
425        [
426            "layout-section",
427            "container-section",
428            class.as_deref().unwrap_or(""),
429        ]
430        .to_vec(),
431    );
432
433    view! {
434        <div
435            class=class
436            style=style
437            data-layout-type="container"
438        >
439            <h4 class="section-title">{title}</h4>
440
441            <div class="layout-options">
442                <LayoutOptionGroup
443                    title="Max Widths".to_string()
444                    values=containers.max_widths.iter().map(|w| w.as_str().len() as f64).collect()
445                    on_values_change=Callback::new(move |_max_widths| {
446                        // In a real implementation, this would update the max widths
447                        let new_containers = containers_clone1.clone();
448                        on_change.run(new_containers);
449                    })
450                />
451
452                <LayoutOptionGroup
453                    title="Paddings".to_string()
454                    values=containers.paddings.clone()
455                    on_values_change=Callback::new(move |paddings| {
456                        let mut new_containers = containers_clone2.clone();
457                        new_containers.paddings = paddings;
458                        on_change.run(new_containers);
459                    })
460                />
461            </div>
462        </div>
463    }
464}
465
466#[cfg(test)]
467mod layout_system_tests {
468    use super::*;
469    use leptos::callback::Callback;
470
471    #[test]
472    fn test_layout_system_default() {
473        let layout = LayoutSystem::default();
474        assert_eq!(layout.spacing.base_unit, 4.0);
475        assert_eq!(layout.spacing.scale.len(), 24);
476        assert_eq!(layout.spacing.directions.len(), 7);
477        assert_eq!(layout.breakpoints.breakpoints.len(), 6);
478        assert_eq!(layout.breakpoints.container_max_widths.len(), 5);
479        assert_eq!(layout.grid.columns, 12);
480        assert_eq!(layout.grid.gutters.len(), 4);
481        assert_eq!(layout.grid.gaps.len(), 5);
482        assert_eq!(layout.grid.alignments.len(), 7);
483        assert_eq!(layout.flexbox.directions.len(), 4);
484        assert_eq!(layout.flexbox.wraps.len(), 3);
485        assert_eq!(layout.flexbox.justifications.len(), 6);
486        assert_eq!(layout.flexbox.alignments.len(), 5);
487        assert_eq!(layout.flexbox.grows.len(), 4);
488        assert_eq!(layout.flexbox.shrinks.len(), 4);
489        assert_eq!(layout.containers.max_widths.len(), 5);
490        assert_eq!(layout.containers.paddings.len(), 4);
491        assert_eq!(layout.containers.margins.len(), 5);
492        assert_eq!(layout.containers.centers.len(), 2);
493    }
494
495    #[test]
496    fn test_layout_builder_component_creation() {
497        // Test logic without runtime
498        // Test component logic
499        let title = "Spacing System";
500        let layout_type = "spacing";
501        assert!(!title.is_empty());
502        assert!(!layout_type.is_empty()); // Test completed
503    }
504
505    #[test]
506    fn test_layout_builder_with_callback() {
507        // Test logic without runtime
508        let callback = Callback::new(|_layout: LayoutSystem| {});
509        // Test component logic
510        let title = "Spacing System";
511        let layout_type = "spacing";
512        assert!(!title.is_empty());
513        assert!(!layout_type.is_empty()); // Test completed
514    }
515
516    #[test]
517    fn test_layout_section_component() {
518        // Test logic without runtime
519        let spacing = SpacingSystem::default();
520        // Test component logic
521        let title = "Spacing System";
522        let layout_type = "spacing";
523        assert!(!title.is_empty());
524        assert!(!layout_type.is_empty()); // Test completed
525    }
526
527    #[test]
528    fn test_layout_option_group_component() {
529        // Test logic without runtime
530        let _values = [0.0, 1.0, 2.0, 4.0, 8.0];
531        // Test component logic
532        let title = "Spacing System";
533        let layout_type = "spacing";
534        assert!(!title.is_empty());
535        assert!(!layout_type.is_empty()); // Test completed
536    }
537
538    // Performance Tests
539    #[test]
540    fn test_layout_creation_performance() {
541        // Test layout creation performance
542        let start = std::time::Instant::now();
543        let _layout = LayoutSystem::default();
544        let duration = start.elapsed();
545        assert!(duration.as_millis() < 100); // Should create layout in less than 100ms
546    }
547
548    #[test]
549    fn test_layout_builder_render_performance() {
550        // Test layout builder render performance
551        let start = std::time::Instant::now();
552        // Simulate component creation
553        let duration = start.elapsed();
554        assert!(duration.as_millis() < 100); // Should render in less than 100ms
555    }
556}