Skip to main content

dioxus_ui_system/atoms/
box_component.rs

1//! Box atom component
2//!
3//! A foundational layout primitive that provides consistent spacing,
4//! borders, backgrounds, and flexbox utilities. Similar to a div but
5//! with theme-aware styling built-in.
6
7use crate::styles::Style;
8use crate::theme::tokens::Color;
9use crate::theme::use_style;
10use dioxus::prelude::*;
11
12/// Box display type
13#[derive(Default, Clone, PartialEq, Debug)]
14pub enum BoxDisplay {
15    /// Block display (default)
16    #[default]
17    Block,
18    /// Flex display
19    Flex,
20    /// Inline flex display
21    InlineFlex,
22    /// Inline block display
23    InlineBlock,
24    /// Grid display
25    Grid,
26    /// Inline display
27    Inline,
28    /// Hidden/none
29    None,
30}
31
32impl BoxDisplay {
33    #[allow(dead_code)]
34    fn as_str(&self) -> &'static str {
35        match self {
36            BoxDisplay::Block => "block",
37            BoxDisplay::Flex => "flex",
38            BoxDisplay::InlineFlex => "inline-flex",
39            BoxDisplay::InlineBlock => "inline-block",
40            BoxDisplay::Grid => "grid",
41            BoxDisplay::Inline => "inline",
42            BoxDisplay::None => "none",
43        }
44    }
45}
46
47/// Flex direction
48#[derive(Default, Clone, PartialEq, Debug)]
49pub enum FlexDirection {
50    /// Row direction (horizontal)
51    #[default]
52    Row,
53    /// Column direction (vertical)
54    Column,
55    /// Row reverse
56    RowReverse,
57    /// Column reverse
58    ColumnReverse,
59}
60
61impl FlexDirection {
62    fn as_str(&self) -> &'static str {
63        match self {
64            FlexDirection::Row => "row",
65            FlexDirection::Column => "column",
66            FlexDirection::RowReverse => "row-reverse",
67            FlexDirection::ColumnReverse => "column-reverse",
68        }
69    }
70}
71
72/// Flex wrap
73#[derive(Default, Clone, PartialEq, Debug)]
74pub enum FlexWrap {
75    /// No wrap (default)
76    #[default]
77    NoWrap,
78    /// Wrap to next line
79    Wrap,
80    /// Wrap reverse
81    WrapReverse,
82}
83
84impl FlexWrap {
85    fn as_str(&self) -> &'static str {
86        match self {
87            FlexWrap::NoWrap => "nowrap",
88            FlexWrap::Wrap => "wrap",
89            FlexWrap::WrapReverse => "wrap-reverse",
90        }
91    }
92}
93
94/// Justify content alignment
95#[derive(Default, Clone, PartialEq, Debug)]
96pub enum JustifyContent {
97    /// Start alignment (default)
98    #[default]
99    Start,
100    /// End alignment
101    End,
102    /// Center alignment
103    Center,
104    /// Space between
105    SpaceBetween,
106    /// Space around
107    SpaceAround,
108    /// Space evenly
109    SpaceEvenly,
110}
111
112impl JustifyContent {
113    fn as_str(&self) -> &'static str {
114        match self {
115            JustifyContent::Start => "flex-start",
116            JustifyContent::End => "flex-end",
117            JustifyContent::Center => "center",
118            JustifyContent::SpaceBetween => "space-between",
119            JustifyContent::SpaceAround => "space-around",
120            JustifyContent::SpaceEvenly => "space-evenly",
121        }
122    }
123}
124
125/// Align items
126#[derive(Default, Clone, PartialEq, Debug)]
127pub enum AlignItems {
128    /// Stretch (default)
129    #[default]
130    Stretch,
131    /// Start alignment
132    Start,
133    /// End alignment
134    End,
135    /// Center alignment
136    Center,
137    /// Baseline alignment
138    Baseline,
139}
140
141impl AlignItems {
142    fn as_str(&self) -> &'static str {
143        match self {
144            AlignItems::Stretch => "stretch",
145            AlignItems::Start => "flex-start",
146            AlignItems::End => "flex-end",
147            AlignItems::Center => "center",
148            AlignItems::Baseline => "baseline",
149        }
150    }
151}
152
153/// Spacing scale sizes
154#[derive(Default, Clone, PartialEq, Debug)]
155pub enum SpacingSize {
156    /// No spacing
157    None,
158    /// Extra small (4px)
159    Xs,
160    /// Small (8px)
161    #[default]
162    Sm,
163    /// Medium (16px)
164    Md,
165    /// Large (24px)
166    Lg,
167    /// Extra large (32px)
168    Xl,
169    /// Extra extra large (48px)
170    Xxl,
171}
172
173impl SpacingSize {
174    fn as_str(&self) -> &'static str {
175        match self {
176            SpacingSize::None => "none",
177            SpacingSize::Xs => "xs",
178            SpacingSize::Sm => "sm",
179            SpacingSize::Md => "md",
180            SpacingSize::Lg => "lg",
181            SpacingSize::Xl => "xl",
182            SpacingSize::Xxl => "xxl",
183        }
184    }
185}
186
187/// Border radius sizes
188#[derive(Default, Clone, PartialEq, Debug)]
189pub enum RadiusSize {
190    /// No radius
191    None,
192    /// Small radius (4px)
193    Sm,
194    /// Medium radius (8px)
195    #[default]
196    Md,
197    /// Large radius (12px)
198    Lg,
199    /// Extra large radius (16px)
200    Xl,
201    /// Full radius (circle/pill)
202    Full,
203}
204
205impl RadiusSize {
206    fn as_str(&self) -> &'static str {
207        match self {
208            RadiusSize::None => "none",
209            RadiusSize::Sm => "sm",
210            RadiusSize::Md => "md",
211            RadiusSize::Lg => "lg",
212            RadiusSize::Xl => "xl",
213            RadiusSize::Full => "full",
214        }
215    }
216}
217
218/// Shadow sizes
219#[derive(Default, Clone, PartialEq, Debug)]
220pub enum ShadowSize {
221    /// No shadow (default)
222    #[default]
223    None,
224    /// Small shadow
225    Sm,
226    /// Medium shadow
227    Md,
228    /// Large shadow
229    Lg,
230    /// Extra large shadow
231    Xl,
232    /// Inner shadow
233    Inner,
234}
235
236impl ShadowSize {
237    fn as_str(&self) -> &'static str {
238        match self {
239            ShadowSize::None => "none",
240            ShadowSize::Sm => "sm",
241            ShadowSize::Md => "md",
242            ShadowSize::Lg => "lg",
243            ShadowSize::Xl => "xl",
244            ShadowSize::Inner => "inner",
245        }
246    }
247}
248
249/// Background color options
250#[derive(Clone, PartialEq, Debug)]
251pub enum BackgroundColor {
252    /// Primary color
253    Primary,
254    /// Secondary color
255    Secondary,
256    /// Background color
257    Background,
258    /// Foreground color (text color as bg)
259    Foreground,
260    /// Muted color
261    Muted,
262    /// Accent color
263    Accent,
264    /// Card color
265    Card,
266    /// Popover color
267    Popover,
268    /// Destructive color
269    Destructive,
270    /// Success color
271    Success,
272    /// Warning color
273    Warning,
274    /// Transparent
275    Transparent,
276    /// Custom color
277    Custom(Color),
278}
279
280impl Default for BackgroundColor {
281    fn default() -> Self {
282        BackgroundColor::Transparent
283    }
284}
285
286/// Border width
287#[derive(Default, Clone, PartialEq, Debug)]
288pub enum BorderWidth {
289    /// No border
290    #[default]
291    None,
292    /// Thin border (1px)
293    Thin,
294    /// Medium border (2px)
295    Medium,
296    /// Thick border (4px)
297    Thick,
298}
299
300impl BorderWidth {
301    fn as_px(&self) -> u8 {
302        match self {
303            BorderWidth::None => 0,
304            BorderWidth::Thin => 1,
305            BorderWidth::Medium => 2,
306            BorderWidth::Thick => 4,
307        }
308    }
309}
310
311/// Overflow behavior
312#[derive(Default, Clone, PartialEq, Debug)]
313pub enum Overflow {
314    /// Visible (default)
315    #[default]
316    Visible,
317    /// Hidden
318    Hidden,
319    /// Scroll
320    Scroll,
321    /// Auto
322    Auto,
323}
324
325impl Overflow {
326    fn as_str(&self) -> &'static str {
327        match self {
328            Overflow::Visible => "visible",
329            Overflow::Hidden => "hidden",
330            Overflow::Scroll => "scroll",
331            Overflow::Auto => "auto",
332        }
333    }
334}
335
336/// Position type
337#[derive(Default, Clone, PartialEq, Debug)]
338pub enum Position {
339    /// Static (default)
340    #[default]
341    Static,
342    /// Relative
343    Relative,
344    /// Absolute
345    Absolute,
346    /// Fixed
347    Fixed,
348    /// Sticky
349    Sticky,
350}
351
352impl Position {
353    fn as_str(&self) -> &'static str {
354        match self {
355            Position::Static => "static",
356            Position::Relative => "relative",
357            Position::Absolute => "absolute",
358            Position::Fixed => "fixed",
359            Position::Sticky => "sticky",
360        }
361    }
362}
363
364/// Box component properties
365#[derive(Props, Clone, PartialEq)]
366pub struct BoxProps {
367    /// Box content
368    pub children: Element,
369    /// Display type
370    #[props(default)]
371    pub display: BoxDisplay,
372    /// Flex direction (when display is flex)
373    #[props(default)]
374    pub flex_direction: FlexDirection,
375    /// Flex wrap
376    #[props(default)]
377    pub flex_wrap: FlexWrap,
378    /// Justify content
379    #[props(default)]
380    pub justify_content: JustifyContent,
381    /// Align items
382    #[props(default)]
383    pub align_items: AlignItems,
384    /// Padding (all sides)
385    #[props(default)]
386    pub padding: SpacingSize,
387    /// Padding X (horizontal)
388    #[props(default)]
389    pub px: Option<SpacingSize>,
390    /// Padding Y (vertical)
391    #[props(default)]
392    pub py: Option<SpacingSize>,
393    /// Padding top
394    #[props(default)]
395    pub pt: Option<SpacingSize>,
396    /// Padding right
397    #[props(default)]
398    pub pr: Option<SpacingSize>,
399    /// Padding bottom
400    #[props(default)]
401    pub pb: Option<SpacingSize>,
402    /// Padding left
403    #[props(default)]
404    pub pl: Option<SpacingSize>,
405    /// Margin (all sides)
406    #[props(default)]
407    pub margin: SpacingSize,
408    /// Margin X (horizontal)
409    #[props(default)]
410    pub mx: Option<SpacingSize>,
411    /// Margin Y (vertical)
412    #[props(default)]
413    pub my: Option<SpacingSize>,
414    /// Margin top
415    #[props(default)]
416    pub mt: Option<SpacingSize>,
417    /// Margin right
418    #[props(default)]
419    pub mr: Option<SpacingSize>,
420    /// Margin bottom
421    #[props(default)]
422    pub mb: Option<SpacingSize>,
423    /// Margin left
424    #[props(default)]
425    pub ml: Option<SpacingSize>,
426    /// Gap between children (for flex/grid)
427    #[props(default)]
428    pub gap: SpacingSize,
429    /// Background color
430    #[props(default)]
431    pub background: BackgroundColor,
432    /// Border radius
433    #[props(default)]
434    pub border_radius: RadiusSize,
435    /// Border width
436    #[props(default)]
437    pub border: BorderWidth,
438    /// Border color (uses border color from theme by default)
439    #[props(default)]
440    pub border_color: Option<BackgroundColor>,
441    /// Box shadow
442    #[props(default)]
443    pub shadow: ShadowSize,
444    /// Width (e.g., "100%", "200px", "auto")
445    #[props(default)]
446    pub width: Option<String>,
447    /// Height (e.g., "100%", "200px", "auto")
448    #[props(default)]
449    pub height: Option<String>,
450    /// Minimum width
451    #[props(default)]
452    pub min_width: Option<String>,
453    /// Minimum height
454    #[props(default)]
455    pub min_height: Option<String>,
456    /// Maximum width
457    #[props(default)]
458    pub max_width: Option<String>,
459    /// Maximum height
460    #[props(default)]
461    pub max_height: Option<String>,
462    /// Overflow behavior
463    #[props(default)]
464    pub overflow: Overflow,
465    /// Position type
466    #[props(default)]
467    pub position: Position,
468    /// Top position (when position is not static)
469    #[props(default)]
470    pub top: Option<String>,
471    /// Right position
472    #[props(default)]
473    pub right: Option<String>,
474    /// Bottom position
475    #[props(default)]
476    pub bottom: Option<String>,
477    /// Left position
478    #[props(default)]
479    pub left: Option<String>,
480    /// Z-index
481    #[props(default)]
482    pub z_index: Option<i16>,
483    /// Opacity (0.0 - 1.0)
484    #[props(default)]
485    pub opacity: Option<f32>,
486    /// Cursor style
487    #[props(default)]
488    pub cursor: Option<String>,
489    /// Click handler
490    #[props(default)]
491    pub onclick: Option<EventHandler<MouseEvent>>,
492    /// Mouse enter handler
493    #[props(default)]
494    pub onmouseenter: Option<EventHandler<MouseEvent>>,
495    /// Mouse leave handler
496    #[props(default)]
497    pub onmouseleave: Option<EventHandler<MouseEvent>>,
498    /// Custom inline styles
499    #[props(default)]
500    pub style: Option<String>,
501    /// Custom class name
502    #[props(default)]
503    pub class: Option<String>,
504    /// Element ID
505    #[props(default)]
506    pub id: Option<String>,
507}
508
509/// Box atom component
510///
511/// A foundational layout primitive for building consistent UIs.
512///
513/// # Example
514/// ```rust,ignore
515/// use dioxus::prelude::*;
516/// use dioxus_ui_system::atoms::{Box, BoxDisplay, SpacingSize, BackgroundColor};
517///
518/// rsx! {
519///     Box {
520///         display: BoxDisplay::Flex,
521///         padding: SpacingSize::Md,
522///         background: BackgroundColor::Card,
523///         border_radius: RadiusSize::Md,
524///         "Content goes here"
525///     }
526/// }
527/// ```
528#[component]
529pub fn Box(props: BoxProps) -> Element {
530    let display = props.display.clone();
531    let flex_direction = props.flex_direction.clone();
532    let flex_wrap = props.flex_wrap.clone();
533    let justify_content = props.justify_content.clone();
534    let align_items = props.align_items.clone();
535    let padding = props.padding.clone();
536    let px = props.px.clone();
537    let py = props.py.clone();
538    let pt = props.pt.clone();
539    let pr = props.pr.clone();
540    let pb = props.pb.clone();
541    let pl = props.pl.clone();
542    let margin = props.margin.clone();
543    let mx = props.mx.clone();
544    let my = props.my.clone();
545    let mt = props.mt.clone();
546    let mr = props.mr.clone();
547    let mb = props.mb.clone();
548    let ml = props.ml.clone();
549    let gap = props.gap.clone();
550    let background = props.background.clone();
551    let border_radius = props.border_radius.clone();
552    let border = props.border.clone();
553    let border_color = props.border_color.clone();
554    let shadow = props.shadow.clone();
555    let overflow = props.overflow.clone();
556    let position = props.position.clone();
557
558    let style = use_style(move |t| {
559        let mut style = Style::new();
560
561        // Display
562        style = match display {
563            BoxDisplay::Block => style.block(),
564            BoxDisplay::Flex => style.flex(),
565            BoxDisplay::InlineFlex => style.inline_flex(),
566            BoxDisplay::InlineBlock => style.inline_block(),
567            BoxDisplay::Grid => style.grid(),
568            BoxDisplay::Inline => style,
569            BoxDisplay::None => style.hidden(),
570        };
571
572        // Flex properties (only apply if flex display)
573        if display == BoxDisplay::Flex || display == BoxDisplay::InlineFlex {
574            style = Style {
575                flex_direction: Some(flex_direction.as_str().into()),
576                ..style
577            };
578            style = Style {
579                flex_wrap: Some(flex_wrap.as_str().into()),
580                ..style
581            };
582            style = Style {
583                justify_content: Some(justify_content.as_str().into()),
584                ..style
585            };
586            style = Style {
587                align_items: Some(align_items.as_str().into()),
588                ..style
589            };
590        }
591
592        // Gap
593        if gap != SpacingSize::None
594            && (display == BoxDisplay::Flex
595                || display == BoxDisplay::InlineFlex
596                || display == BoxDisplay::Grid)
597        {
598            style = style.gap(&t.spacing, gap.as_str());
599        }
600
601        // Padding
602        if padding != SpacingSize::None {
603            style = style.p(&t.spacing, padding.as_str());
604        }
605        if let Some(px_size) = &px {
606            if *px_size != SpacingSize::None {
607                style = style.px(&t.spacing, px_size.as_str());
608            }
609        }
610        if let Some(py_size) = &py {
611            if *py_size != SpacingSize::None {
612                style = style.py(&t.spacing, py_size.as_str());
613            }
614        }
615        if let Some(pt_size) = &pt {
616            if *pt_size != SpacingSize::None {
617                style = style.pt(&t.spacing, pt_size.as_str());
618            }
619        }
620        if let Some(pr_size) = &pr {
621            if *pr_size != SpacingSize::None {
622                style = style.pr(&t.spacing, pr_size.as_str());
623            }
624        }
625        if let Some(pb_size) = &pb {
626            if *pb_size != SpacingSize::None {
627                style = style.pb(&t.spacing, pb_size.as_str());
628            }
629        }
630        if let Some(pl_size) = &pl {
631            if *pl_size != SpacingSize::None {
632                style = style.pl(&t.spacing, pl_size.as_str());
633            }
634        }
635
636        // Margin
637        if margin != SpacingSize::None {
638            style = style.m(&t.spacing, margin.as_str());
639        }
640        if let Some(mx_size) = &mx {
641            if *mx_size != SpacingSize::None {
642                style = style.mx(&t.spacing, mx_size.as_str());
643            }
644        }
645        if let Some(my_size) = &my {
646            if *my_size != SpacingSize::None {
647                style = style.my(&t.spacing, my_size.as_str());
648            }
649        }
650        if let Some(mt_size) = &mt {
651            if *mt_size != SpacingSize::None {
652                style = style.mt(&t.spacing, mt_size.as_str());
653            }
654        }
655        if let Some(mr_size) = &mr {
656            if *mr_size != SpacingSize::None {
657                style = style.mr(&t.spacing, mr_size.as_str());
658            }
659        }
660        if let Some(mb_size) = &mb {
661            if *mb_size != SpacingSize::None {
662                style = style.mb(&t.spacing, mb_size.as_str());
663            }
664        }
665        if let Some(ml_size) = &ml {
666            if *ml_size != SpacingSize::None {
667                style = style.ml(&t.spacing, ml_size.as_str());
668            }
669        }
670
671        // Background color
672        let bg_color = match &background {
673            BackgroundColor::Primary => t.colors.primary.clone(),
674            BackgroundColor::Secondary => t.colors.secondary.clone(),
675            BackgroundColor::Background => t.colors.background.clone(),
676            BackgroundColor::Foreground => t.colors.foreground.clone(),
677            BackgroundColor::Muted => t.colors.muted.clone(),
678            BackgroundColor::Accent => t.colors.accent.clone(),
679            BackgroundColor::Card => t.colors.card.clone(),
680            BackgroundColor::Popover => t.colors.popover.clone(),
681            BackgroundColor::Destructive => t.colors.destructive.clone(),
682            BackgroundColor::Success => t.colors.success.clone(),
683            BackgroundColor::Warning => t.colors.warning.clone(),
684            BackgroundColor::Transparent => Color::new_rgba(0, 0, 0, 0.0),
685            BackgroundColor::Custom(c) => c.clone(),
686        };
687        style = style.bg(&bg_color);
688
689        // Border radius
690        style = style.rounded(&t.radius, border_radius.as_str());
691
692        // Border
693        if border != BorderWidth::None {
694            let border_c = match &border_color {
695                Some(BackgroundColor::Primary) => t.colors.primary.clone(),
696                Some(BackgroundColor::Secondary) => t.colors.secondary.clone(),
697                Some(BackgroundColor::Background) => t.colors.background.clone(),
698                Some(BackgroundColor::Foreground) => t.colors.foreground.clone(),
699                Some(BackgroundColor::Muted) => t.colors.muted.clone(),
700                Some(BackgroundColor::Accent) => t.colors.accent.clone(),
701                Some(BackgroundColor::Card) => t.colors.card.clone(),
702                Some(BackgroundColor::Popover) => t.colors.popover.clone(),
703                Some(BackgroundColor::Destructive) => t.colors.destructive.clone(),
704                Some(BackgroundColor::Success) => t.colors.success.clone(),
705                Some(BackgroundColor::Warning) => t.colors.warning.clone(),
706                Some(BackgroundColor::Transparent) => Color::new_rgba(0, 0, 0, 0.0),
707                Some(BackgroundColor::Custom(c)) => c.clone(),
708                None => t.colors.border.clone(),
709            };
710            style = style.border(border.as_px(), &border_c);
711        }
712
713        // Shadow
714        if shadow != ShadowSize::None {
715            style = style.shadow_themed(&t, shadow.as_str());
716        }
717
718        // Overflow
719        style = Style {
720            overflow: Some(overflow.as_str().into()),
721            ..style
722        };
723
724        // Position
725        style = Style {
726            position: Some(position.as_str().into()),
727            ..style
728        };
729
730        // Opacity
731        if let Some(op) = props.opacity {
732            style = style.opacity(op.clamp(0.0, 1.0));
733        }
734
735        // Cursor
736        if let Some(c) = &props.cursor {
737            style = style.cursor(c);
738        }
739
740        style.build()
741    });
742
743    // Build additional styles string
744    let mut additional_styles = String::new();
745
746    // Width
747    if let Some(w) = &props.width {
748        additional_styles.push_str(&format!("width: {}; ", w));
749    }
750    // Height
751    if let Some(h) = &props.height {
752        additional_styles.push_str(&format!("height: {}; ", h));
753    }
754    // Min width
755    if let Some(mw) = &props.min_width {
756        additional_styles.push_str(&format!("min-width: {}; ", mw));
757    }
758    // Min height
759    if let Some(mh) = &props.min_height {
760        additional_styles.push_str(&format!("min-height: {}; ", mh));
761    }
762    // Max width
763    if let Some(mw) = &props.max_width {
764        additional_styles.push_str(&format!("max-width: {}; ", mw));
765    }
766    // Max height
767    if let Some(mh) = &props.max_height {
768        additional_styles.push_str(&format!("max-height: {}; ", mh));
769    }
770    // Position offsets
771    if let Some(top) = &props.top {
772        additional_styles.push_str(&format!("top: {}; ", top));
773    }
774    if let Some(right) = &props.right {
775        additional_styles.push_str(&format!("right: {}; ", right));
776    }
777    if let Some(bottom) = &props.bottom {
778        additional_styles.push_str(&format!("bottom: {}; ", bottom));
779    }
780    if let Some(left) = &props.left {
781        additional_styles.push_str(&format!("left: {}; ", left));
782    }
783    // Z-index
784    if let Some(z) = props.z_index {
785        additional_styles.push_str(&format!("z-index: {}; ", z));
786    }
787
788    // Combine styles
789    let final_style = if let Some(custom) = &props.style {
790        format!("{} {}{}", style(), additional_styles, custom)
791    } else {
792        format!("{} {}", style(), additional_styles)
793    };
794
795    let class = props.class.clone().unwrap_or_default();
796    let id = props.id.clone().unwrap_or_default();
797
798    rsx! {
799        div {
800            style: "{final_style}",
801            class: "{class}",
802            id: "{id}",
803            onclick: move |e| {
804                if let Some(handler) = &props.onclick {
805                    handler.call(e);
806                }
807            },
808            onmouseenter: move |e| {
809                if let Some(handler) = &props.onmouseenter {
810                    handler.call(e);
811                }
812            },
813            onmouseleave: move |e| {
814                if let Some(handler) = &props.onmouseleave {
815                    handler.call(e);
816                }
817            },
818            {props.children}
819        }
820    }
821}
822
823/// VStack component - Vertical stack layout
824///
825/// Convenience wrapper around Box with flex column layout.
826#[component]
827pub fn VStack(
828    children: Element,
829    #[props(default)] gap: SpacingSize,
830    #[props(default)] padding: SpacingSize,
831    #[props(default)] align: AlignItems,
832    #[props(default)] justify: JustifyContent,
833    #[props(default)] background: BackgroundColor,
834    #[props(default)] width: Option<String>,
835    #[props(default)] height: Option<String>,
836    #[props(default)] style: Option<String>,
837    #[props(default)] class: Option<String>,
838) -> Element {
839    rsx! {
840        Box {
841            display: BoxDisplay::Flex,
842            flex_direction: FlexDirection::Column,
843            align_items: align,
844            justify_content: justify,
845            gap: gap,
846            padding: padding,
847            background: background,
848            width: width,
849            height: height,
850            style: style,
851            class: class,
852            {children}
853        }
854    }
855}
856
857/// HStack component - Horizontal stack layout
858///
859/// Convenience wrapper around Box with flex row layout.
860#[component]
861pub fn HStack(
862    children: Element,
863    #[props(default)] gap: SpacingSize,
864    #[props(default)] padding: SpacingSize,
865    #[props(default)] align: AlignItems,
866    #[props(default)] justify: JustifyContent,
867    #[props(default)] background: BackgroundColor,
868    #[props(default)] width: Option<String>,
869    #[props(default)] height: Option<String>,
870    #[props(default)] style: Option<String>,
871    #[props(default)] class: Option<String>,
872) -> Element {
873    rsx! {
874        Box {
875            display: BoxDisplay::Flex,
876            flex_direction: FlexDirection::Row,
877            align_items: align,
878            justify_content: justify,
879            gap: gap,
880            padding: padding,
881            background: background,
882            width: width,
883            height: height,
884            style: style,
885            class: class,
886            {children}
887        }
888    }
889}
890
891/// Center component - Center content both vertically and horizontally
892///
893/// Convenience wrapper around Box with center alignment.
894#[component]
895pub fn Center(
896    children: Element,
897    #[props(default)] padding: SpacingSize,
898    #[props(default)] background: BackgroundColor,
899    #[props(default)] width: Option<String>,
900    #[props(default)] height: Option<String>,
901    #[props(default)] style: Option<String>,
902    #[props(default)] class: Option<String>,
903) -> Element {
904    rsx! {
905        Box {
906            display: BoxDisplay::Flex,
907            align_items: AlignItems::Center,
908            justify_content: JustifyContent::Center,
909            padding: padding,
910            background: background,
911            width: width,
912            height: height,
913            style: style,
914            class: class,
915            {children}
916        }
917    }
918}
919
920#[cfg(test)]
921mod tests {
922    use super::*;
923
924    #[test]
925    fn test_box_display_variants() {
926        assert_eq!(BoxDisplay::Flex.as_str(), "flex");
927        assert_eq!(BoxDisplay::Block.as_str(), "block");
928        assert_eq!(BoxDisplay::None.as_str(), "none");
929    }
930
931    #[test]
932    fn test_flex_direction_variants() {
933        assert_eq!(FlexDirection::Row.as_str(), "row");
934        assert_eq!(FlexDirection::Column.as_str(), "column");
935    }
936
937    #[test]
938    fn test_justify_content_variants() {
939        assert_eq!(JustifyContent::Center.as_str(), "center");
940        assert_eq!(JustifyContent::SpaceBetween.as_str(), "space-between");
941    }
942
943    #[test]
944    fn test_align_items_variants() {
945        assert_eq!(AlignItems::Center.as_str(), "center");
946        assert_eq!(AlignItems::Stretch.as_str(), "stretch");
947    }
948
949    #[test]
950    fn test_spacing_size_to_str() {
951        assert_eq!(SpacingSize::Md.as_str(), "md");
952        assert_eq!(SpacingSize::Lg.as_str(), "lg");
953    }
954
955    #[test]
956    fn test_border_width_to_px() {
957        assert_eq!(BorderWidth::None.as_px(), 0);
958        assert_eq!(BorderWidth::Thin.as_px(), 1);
959        assert_eq!(BorderWidth::Medium.as_px(), 2);
960        assert_eq!(BorderWidth::Thick.as_px(), 4);
961    }
962}