orbital_core_components/flex/flex.rs
1use leptos::prelude::*;
2use orbital_base_components::BaseFlex;
3use orbital_macros::component_doc;
4
5pub use orbital_base_components::{FlexAlign, FlexGap, FlexJustify, FlexWrap, SpacingInset};
6
7/// Flexbox layout container for arranging children in a row or column.
8///
9/// The canonical one-dimensional layout primitive. [`Stack`](crate::Stack) and [`Space`](crate::Space) are convenience wrappers with opinionated defaults.
10///
11/// Orbital-only props beyond basic flex direction and gap: **`wrap`**, **`fill`** (height 100%), **`full_width`**, and token-based **`padding`** / **`margin`** via [`SpacingInset`].
12///
13/// # When to use
14///
15/// - Full control over direction, wrap, inline, fill, and inset padding - Toolbars and form rows that need alignment along both axes - Inline clusters beside text or other inline content (`inline=true`) - When [`Stack`](crate::Stack) (even-gap column) or [`Space`](crate::Space) (space-between) defaults do not fit
16///
17/// # Usage
18///
19/// 1. Choose direction: default row, or `vertical=true` for a column stack. 2. Set `gap` to a [`FlexGap`] preset or custom size — avoid margin hacks. 3. Tune `align` (cross-axis) and `justify` (main-axis) for centering or distribution. 4. Set `inline=true` when the flex container should sit in flowing text or inline UI.
20///
21/// # Best Practices
22///
23/// ## Do's
24///
25/// * Use `gap` instead of margin hacks between items * Set `vertical` for stacked form fields or list actions * Pair with `align` / `justify` for centering and distribution * Use `FlexGap::Size` or `FlexGap::WH` when presets do not match your spacing rhythm * Put borders, fixed heights, and backgrounds on a native wrapper `div`; use `Flex` for direction, gap, and alignment * Use `wrap`, `fill`, and `full_width` props instead of inline flex CSS on `Flex` * Use [`SpacingInset`] with [`SpacingHorizontal`] / [`SpacingVertical`] for theme-aware padding and margin * Prefer [`Stack`](crate::Stack) for simple even-gap vertical sections — `Stack` defaults to column + full-width * Prefer [`Space`](crate::Space) for opposite-edge distribution — `Space` defaults to space-between + full-width
26///
27/// ## Don'ts
28///
29/// * Do not use Flex for two-dimensional page grids — prefer [`Grid`](crate::Grid) * Avoid nesting many Flex containers when a single grid suffices * Do not reach for Flex when [`Stack`](crate::Stack) or [`Space`](crate::Space) defaults already match your layout
30///
31/// # Layout primitives
32///
33/// When `Flex` is not the right fit:
34///
35/// - **Even gap between every sibling on one axis** — [`Stack`](crate::Stack). - **Opposite edges / space-between on one axis** — [`Space`](crate::Space). - **Full flex control** (wrap, inline, fill, inset padding) — `Flex` (this component). - **Fixed column count with span/offset per cell** — [`Grid`](crate::Grid) + [`GridItem`](crate::GridItem). - **Fluid card tiles that reflow by viewport width** — [AutoGrid](/auto-grid) in the orbital crate. - **Single node with padding/surface tokens, no sibling gaps** — [`Box`](crate::Box). - **Page max-width centering inside the shell** — [Container](/container) in the orbital crate. - **Doc-style content + sticky aside rail** — [`ContentWithAside`](crate::ContentWithAside). - **Application shell** (header, sidebar, main) — [`Layout`](crate::Layout).
36///
37/// **Spacing vocabulary:** [`FlexGap`] presets for `Flex`, `Stack`, and `Space`; pixel gaps on [`Grid`](crate::Grid); [`SpacingSize`](/auto-grid) on AutoGrid. Pick the token type each component expects.
38///
39/// # Examples
40///
41/// ## Default
42/// Horizontal flex row with medium gap between items—the baseline for toolbars, button groups, and inline control rows.
43/// <!-- default -->
44/// <!-- preview -->
45/// ```rust
46/// use crate::DemoBox;
47/// view! {
48/// <div data-testid="flex-preview">
49/// <Flex gap=FlexGap::Medium>
50/// <DemoBox data_testid="flex-item-a">"Item A"</DemoBox>
51/// <DemoBox data_testid="flex-item-b">"Item B"</DemoBox>
52/// </Flex>
53/// </div>
54/// }
55/// ```
56///
57/// ## Vertical stack
58/// Column direction stacks children for form fields, settings sections, and vertically listed actions.
59/// <!-- preview -->
60/// ```rust
61/// use crate::DemoBox;
62/// view! {
63/// <div data-testid="flex-vertical">
64/// <Flex vertical=true gap=FlexGap::Small>
65/// <DemoBox data_testid="flex-stack-1">"First"</DemoBox>
66/// <DemoBox data_testid="flex-stack-2">"Second"</DemoBox>
67/// </Flex>
68/// </div>
69/// }
70/// ```
71///
72/// ## Inline flex
73/// `inline-flex` keeps the container in the text flow so compact clusters sit beside surrounding copy without breaking the line.
74/// <!-- preview -->
75/// ```rust
76/// view! {
77/// <div data-testid="flex-inline">
78/// <span>"Before "</span>
79/// <Flex inline=true gap=FlexGap::Small>
80/// <span data-testid="inline-a" style="padding: var(--orb-space-inline-sm); border: 1px dashed var(--orb-color-border-default); border-radius: var(--orb-radius-md);">"A"</span>
81/// <span data-testid="inline-b" style="padding: var(--orb-space-inline-sm); border: 1px dashed var(--orb-color-border-default); border-radius: var(--orb-radius-md);">"B"</span>
82/// </Flex>
83/// <span>" after"</span>
84/// </div>
85/// }
86/// ```
87///
88/// ## Gap matrix
89/// Compare Small, Medium, and Large presets plus custom `Size` and `WH` values so spacing stays consistent without margin hacks.
90/// <!-- preview -->
91/// ```rust
92/// use crate::DemoBox;
93/// view! {
94/// <div data-testid="flex-gap-matrix">
95/// <Flex vertical=true gap=FlexGap::Medium>
96/// <Flex gap=FlexGap::Small>
97/// <DemoBox data_testid="gap-small-a">"Small"</DemoBox>
98/// <DemoBox data_testid="gap-small-b">"Small"</DemoBox>
99/// </Flex>
100/// <Flex gap=FlexGap::Medium>
101/// <DemoBox data_testid="gap-medium-a">"Medium"</DemoBox>
102/// <DemoBox data_testid="gap-medium-b">"Medium"</DemoBox>
103/// </Flex>
104/// <Flex gap=FlexGap::Large>
105/// <DemoBox data_testid="gap-large-a">"Large"</DemoBox>
106/// <DemoBox data_testid="gap-large-b">"Large"</DemoBox>
107/// </Flex>
108/// <Flex gap=FlexGap::Size(20)>
109/// <DemoBox data_testid="gap-size-a">"Size(20)"</DemoBox>
110/// <DemoBox data_testid="gap-size-b">"Size(20)"</DemoBox>
111/// </Flex>
112/// <Flex gap=FlexGap::WH(8, 24)>
113/// <DemoBox data_testid="gap-wh-a">"WH(8,24)"</DemoBox>
114/// <DemoBox data_testid="gap-wh-b">"WH(8,24)"</DemoBox>
115/// </Flex>
116/// </Flex>
117/// </div>
118/// }
119/// ```
120///
121/// ## Centered content
122/// Center on both axes when a single block should sit in the middle of a fixed-height region (empty states, compact panels).
123/// <!-- preview -->
124/// ```rust
125/// use crate::DemoBox;
126/// view! {
127/// <div data-testid="flex-centered" style="width: 100%; max-width: 560px; height: 160px; border: 1px dashed var(--orb-color-border-default); border-radius: var(--orb-radius-md);">
128/// <Flex
129/// fill=true
130/// full_width=true
131/// justify=FlexJustify::Center
132/// align=FlexAlign::Center
133/// >
134/// <DemoBox data_testid="flex-center-label">"Centered"</DemoBox>
135/// </Flex>
136/// </div>
137/// }
138/// ```
139///
140/// ## Align (cross-axis)
141/// Start, Center, and End alignment along the cross axis when row height exceeds item height.
142/// <!-- preview -->
143/// ```rust
144/// use crate::{DemoBox, Flex, FlexAlign, FlexGap};
145/// view! {
146/// <div data-testid="flex-align" style="height: 120px;">
147/// <Flex vertical=true gap=FlexGap::Medium>
148/// <Flex align=FlexAlign::Start gap=FlexGap::Small>
149/// <DemoBox height="24px">"Start"</DemoBox>
150/// <DemoBox height="48px">"Start"</DemoBox>
151/// </Flex>
152/// <Flex align=FlexAlign::Center gap=FlexGap::Small>
153/// <DemoBox height="24px">"Center"</DemoBox>
154/// <DemoBox height="48px">"Center"</DemoBox>
155/// </Flex>
156/// <Flex align=FlexAlign::End gap=FlexGap::Small>
157/// <DemoBox height="24px">"End"</DemoBox>
158/// <DemoBox height="48px">"End"</DemoBox>
159/// </Flex>
160/// </Flex>
161/// </div>
162/// }
163/// ```
164///
165/// ## Justify (main-axis)
166/// Start, Center, End, and SpaceBetween distribute items along the main axis—SpaceBetween is common for footer action bars.
167/// <!-- preview -->
168/// ```rust
169/// use crate::{DemoBox, Flex, FlexGap, FlexJustify};
170/// view! {
171/// <div data-testid="flex-justify">
172/// <Flex vertical=true gap=FlexGap::Medium>
173/// <Flex justify=FlexJustify::Start gap=FlexGap::Small full_width=true>
174/// <DemoBox>"Start"</DemoBox>
175/// <DemoBox>"Start"</DemoBox>
176/// </Flex>
177/// <Flex justify=FlexJustify::Center gap=FlexGap::Small full_width=true>
178/// <DemoBox>"Center"</DemoBox>
179/// <DemoBox>"Center"</DemoBox>
180/// </Flex>
181/// <Flex justify=FlexJustify::End gap=FlexGap::Small full_width=true>
182/// <DemoBox>"End"</DemoBox>
183/// <DemoBox>"End"</DemoBox>
184/// </Flex>
185/// <Flex justify=FlexJustify::SpaceBetween gap=FlexGap::Small full_width=true>
186/// <DemoBox>"Between"</DemoBox>
187/// <DemoBox>"Between"</DemoBox>
188/// </Flex>
189/// </Flex>
190/// </div>
191/// }
192/// ```
193#[component_doc(
194 category = "Layout",
195 preview_slug = "flex",
196 preview_label = "Flex",
197 preview_icon = icondata::AiColumnWidthOutlined,
198)]
199#[component]
200pub fn Flex(
201 /// Optional CSS class names merged onto the flex container.
202 #[prop(optional, into)]
203 class: MaybeProp<String>,
204 /// Spacing between flex items. Presets: `Small`, `Medium` (default), `Large`; or custom via `Size(px)` / `WH(row_px, col_px)`.
205 #[prop(optional)]
206 gap: FlexGap,
207 /// When `true`, lays out children in a column (`flex-direction: column`).
208 #[prop(optional)]
209 vertical: bool,
210 /// When `true`, uses `display: inline-flex` so the container flows inline.
211 #[prop(optional, into)]
212 inline: Signal<bool>,
213 /// Cross-axis alignment (`align-items`).
214 #[prop(optional, into)]
215 align: MaybeProp<FlexAlign>,
216 /// Main-axis distribution (`justify-content`).
217 #[prop(optional, into)]
218 justify: MaybeProp<FlexJustify>,
219 /// Whether flex items wrap onto multiple lines.
220 #[prop(optional, default = FlexWrap::NoWrap)]
221 wrap: FlexWrap,
222 /// When `true`, the container fills the height of its parent (`height: 100%`).
223 #[prop(optional, default = false)]
224 fill: bool,
225 /// When `true`, the container spans the full width of its parent.
226 #[prop(optional, default = false)]
227 full_width: bool,
228 /// Theme-aware padding using Orbital spacing tokens.
229 #[prop(optional, into)]
230 padding: MaybeProp<SpacingInset>,
231 /// Theme-aware margin using Orbital spacing tokens.
232 #[prop(optional, into)]
233 margin: MaybeProp<SpacingInset>,
234 /// Flex item children.
235 children: Children,
236) -> impl IntoView {
237 view! {
238 <BaseFlex
239 class=class
240 gap=gap
241 vertical=vertical
242 inline=inline
243 align=align
244 justify=justify
245 wrap=wrap
246 fill=fill
247 full_width=full_width
248 padding=padding
249 margin=margin
250 >
251 {children()}
252 </BaseFlex>
253 }
254}