radix_leptos_primitives/theming/layout_system/
mod.rs1use crate::utils::merge_classes;
2use leptos::callback::Callback;
3use leptos::prelude::*;
4use serde::{Deserialize, Serialize};
5
6pub use container::*;
8pub use responsive::*;
9pub use spacing::*;
10
11pub mod container;
13pub mod responsive;
14pub mod spacing;
15
16#[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#[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#[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#[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#[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#[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#[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 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 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 let new_flexbox = flexbox_clone3.clone();
401 on_change.run(new_flexbox);
402 })
403 />
404 </div>
405 </div>
406 }
407}
408
409#[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 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 let title = "Spacing System";
500 let layout_type = "spacing";
501 assert!(!title.is_empty());
502 assert!(!layout_type.is_empty()); }
504
505 #[test]
506 fn test_layout_builder_with_callback() {
507 let callback = Callback::new(|_layout: LayoutSystem| {});
509 let title = "Spacing System";
511 let layout_type = "spacing";
512 assert!(!title.is_empty());
513 assert!(!layout_type.is_empty()); }
515
516 #[test]
517 fn test_layout_section_component() {
518 let spacing = SpacingSystem::default();
520 let title = "Spacing System";
522 let layout_type = "spacing";
523 assert!(!title.is_empty());
524 assert!(!layout_type.is_empty()); }
526
527 #[test]
528 fn test_layout_option_group_component() {
529 let _values = [0.0, 1.0, 2.0, 4.0, 8.0];
531 let title = "Spacing System";
533 let layout_type = "spacing";
534 assert!(!title.is_empty());
535 assert!(!layout_type.is_empty()); }
537
538 #[test]
540 fn test_layout_creation_performance() {
541 let start = std::time::Instant::now();
543 let _layout = LayoutSystem::default();
544 let duration = start.elapsed();
545 assert!(duration.as_millis() < 100); }
547
548 #[test]
549 fn test_layout_builder_render_performance() {
550 let start = std::time::Instant::now();
552 let duration = start.elapsed();
554 assert!(duration.as_millis() < 100); }
556}