Skip to main content

stratum_components/layout/
stack.rs

1//! Stack, HStack, and VStack layout components.
2
3use crate::common::merge_classes;
4use stratum_core::render::RenderOutput;
5
6/// Spacing between stack items, mapped to Tailwind gap classes.
7#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
8pub enum StackSpacing {
9    None,
10    Xs,
11    Sm,
12    #[default]
13    Md,
14    Lg,
15    Xl,
16}
17
18impl StackSpacing {
19    pub fn gap_class(&self) -> &'static str {
20        match self {
21            StackSpacing::None => "gap-0",
22            StackSpacing::Xs => "gap-1",
23            StackSpacing::Sm => "gap-2",
24            StackSpacing::Md => "gap-4",
25            StackSpacing::Lg => "gap-6",
26            StackSpacing::Xl => "gap-8",
27        }
28    }
29}
30
31/// Alignment along the cross axis.
32#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
33pub enum StackAlign {
34    #[default]
35    Stretch,
36    Start,
37    Center,
38    End,
39    Baseline,
40}
41
42impl StackAlign {
43    pub fn class(&self) -> &'static str {
44        match self {
45            StackAlign::Stretch => "items-stretch",
46            StackAlign::Start => "items-start",
47            StackAlign::Center => "items-center",
48            StackAlign::End => "items-end",
49            StackAlign::Baseline => "items-baseline",
50        }
51    }
52}
53
54/// Justification along the main axis.
55#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
56pub enum StackJustify {
57    #[default]
58    Start,
59    Center,
60    End,
61    Between,
62    Around,
63    Evenly,
64}
65
66impl StackJustify {
67    pub fn class(&self) -> &'static str {
68        match self {
69            StackJustify::Start => "justify-start",
70            StackJustify::Center => "justify-center",
71            StackJustify::End => "justify-end",
72            StackJustify::Between => "justify-between",
73            StackJustify::Around => "justify-around",
74            StackJustify::Evenly => "justify-evenly",
75        }
76    }
77}
78
79/// Direction of the stack.
80#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
81pub enum StackDirection {
82    Horizontal,
83    #[default]
84    Vertical,
85}
86
87/// Properties for the Stack component.
88#[derive(Debug, Clone, PartialEq, Default)]
89pub struct StackProps {
90    pub direction: StackDirection,
91    pub spacing: StackSpacing,
92    pub align: StackAlign,
93    pub justify: StackJustify,
94    pub wrap: bool,
95    pub class: Option<String>,
96}
97
98pub struct Stack;
99
100impl Stack {
101    pub fn classes(props: &StackProps) -> String {
102        let dir = match props.direction {
103            StackDirection::Horizontal => "flex-row",
104            StackDirection::Vertical => "flex-col",
105        };
106        let wrap = if props.wrap { "flex-wrap" } else { "" };
107        let computed = format!(
108            "flex {} {} {} {} {}",
109            dir,
110            props.spacing.gap_class(),
111            props.align.class(),
112            props.justify.class(),
113            wrap,
114        );
115        merge_classes(&computed, &props.class)
116    }
117
118    pub fn render(props: &StackProps) -> RenderOutput {
119        RenderOutput::new()
120            .with_tag("div")
121            .with_class(Self::classes(props))
122    }
123}
124
125/// Horizontal stack convenience component.
126pub struct HStack;
127
128impl HStack {
129    pub fn classes(spacing: StackSpacing, align: StackAlign, class: &Option<String>) -> String {
130        let props = StackProps {
131            direction: StackDirection::Horizontal,
132            spacing,
133            align,
134            ..Default::default()
135        };
136        let mut cls = Stack::classes(&props);
137        if let Some(extra) = class {
138            cls = format!("{} {}", cls, extra);
139        }
140        cls
141    }
142
143    pub fn render(
144        spacing: StackSpacing,
145        align: StackAlign,
146        class: &Option<String>,
147    ) -> RenderOutput {
148        RenderOutput::new()
149            .with_tag("div")
150            .with_class(Self::classes(spacing, align, class))
151    }
152}
153
154/// Vertical stack convenience component.
155pub struct VStack;
156
157impl VStack {
158    pub fn classes(spacing: StackSpacing, align: StackAlign, class: &Option<String>) -> String {
159        let props = StackProps {
160            direction: StackDirection::Vertical,
161            spacing,
162            align,
163            ..Default::default()
164        };
165        let mut cls = Stack::classes(&props);
166        if let Some(extra) = class {
167            cls = format!("{} {}", cls, extra);
168        }
169        cls
170    }
171
172    pub fn render(
173        spacing: StackSpacing,
174        align: StackAlign,
175        class: &Option<String>,
176    ) -> RenderOutput {
177        RenderOutput::new()
178            .with_tag("div")
179            .with_class(Self::classes(spacing, align, class))
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186
187    #[test]
188    fn stack_default_is_vertical() {
189        let props = StackProps::default();
190        let classes = Stack::classes(&props);
191        assert!(classes.contains("flex-col"));
192        assert!(classes.contains("gap-4"));
193    }
194
195    #[test]
196    fn hstack_is_horizontal() {
197        let classes = HStack::classes(StackSpacing::Md, StackAlign::Center, &None);
198        assert!(classes.contains("flex-row"));
199        assert!(classes.contains("items-center"));
200    }
201
202    #[test]
203    fn vstack_is_vertical() {
204        let classes = VStack::classes(StackSpacing::Sm, StackAlign::Start, &None);
205        assert!(classes.contains("flex-col"));
206        assert!(classes.contains("gap-2"));
207    }
208
209    #[test]
210    fn stack_with_wrap() {
211        let props = StackProps {
212            wrap: true,
213            ..Default::default()
214        };
215        let classes = Stack::classes(&props);
216        assert!(classes.contains("flex-wrap"));
217    }
218
219    #[test]
220    fn stack_render_tag() {
221        let props = StackProps::default();
222        let output = Stack::render(&props);
223        assert_eq!(output.effective_tag(), "div");
224    }
225}