stratum_components/layout/
stack.rs1use crate::common::merge_classes;
4use stratum_core::render::RenderOutput;
5
6#[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#[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#[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#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
81pub enum StackDirection {
82 Horizontal,
83 #[default]
84 Vertical,
85}
86
87#[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
125pub 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
154pub 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}