Skip to main content

hypersig_style/
pattern.rs

1//! Layouts used often enough to name, each a function that returns a [`Style`].
2//!
3//! Each pattern is a starting point. Add to it with the property methods, or
4//! merge it into another style with [`Style::merge`], and a later value for
5//! the same property wins.
6//!
7//! ```
8//! use hypersig_style::pattern::{center, hstack, stack};
9//! use hypersig_style::value::{JustifyContent, rem};
10//!
11//! let toolbar = hstack(rem(0.5)).justify_content(JustifyContent::SpaceBetween);
12//! let column = stack(rem(1));
13//! let badge = center();
14//! # let _ = (toolbar, column, badge);
15//! ```
16
17use crate::PseudoElement;
18use crate::style::Style;
19use crate::value::{
20    AlignItems, BorderTopStyle, Color, Display, FlexDirection, FlexWrap, Gap, JustifyContent,
21    Length, MarginInline, MaxWidth, Overflow, PaddingInline, Position, WhiteSpace, pct, px, quoted,
22    raw,
23};
24
25/// Children in a column, `gap` apart.
26#[must_use]
27pub fn stack(gap: impl Into<Gap>) -> Style {
28    Style::new()
29        .display(Display::Flex)
30        .flex_direction(FlexDirection::Column)
31        .gap(gap)
32}
33
34/// Children in a column, centered across it, `gap` apart.
35#[must_use]
36pub fn vstack(gap: impl Into<Gap>) -> Style {
37    stack(gap).align_items(AlignItems::Center)
38}
39
40/// Children in a row, centered on its line, `gap` apart.
41#[must_use]
42pub fn hstack(gap: impl Into<Gap>) -> Style {
43    Style::new()
44        .display(Display::Flex)
45        .flex_direction(FlexDirection::Row)
46        .align_items(AlignItems::Center)
47        .gap(gap)
48}
49
50/// Children in rows that wrap onto new lines, `gap` apart.
51#[must_use]
52pub fn wrap(gap: impl Into<Gap>) -> Style {
53    Style::new()
54        .display(Display::Flex)
55        .flex_wrap(FlexWrap::Wrap)
56        .gap(gap)
57}
58
59/// Children centered on both axes.
60#[must_use]
61pub fn center() -> Style {
62    Style::new()
63        .display(Display::Flex)
64        .align_items(AlignItems::Center)
65        .justify_content(JustifyContent::Center)
66}
67
68/// An empty flex child that takes the free space, pushing its siblings apart.
69#[must_use]
70pub fn spacer() -> Style {
71    Style::new().flex_grow(1).flex_shrink(1).flex_basis(px(0))
72}
73
74/// A grid of `columns` equal columns, `gap` apart.
75///
76/// Each column is `minmax(0, 1fr)`, so long content wraps inside its column
77/// rather than widening it.
78#[must_use]
79pub fn grid(columns: u16, gap: impl Into<Gap>) -> Style {
80    Style::new()
81        .display(Display::Grid)
82        .grid_template_columns(raw(format!("repeat({columns}, minmax(0, 1fr))")))
83        .gap(gap)
84}
85
86/// A grid child spanning `columns` columns.
87#[must_use]
88pub fn grid_item(columns: u16) -> Style {
89    Style::new().grid_column(raw(format!("span {columns} / span {columns}")))
90}
91
92/// A block no wider than `max`, centered, with `padding` on both sides.
93#[must_use]
94pub fn container(max: impl Into<MaxWidth>, padding: impl Into<PaddingInline>) -> Style {
95    Style::new()
96        .max_width(max)
97        .margin_inline(MarginInline::Auto)
98        .padding_inline(padding)
99}
100
101/// A `size` square that centers its children.
102#[must_use]
103pub fn square(size: Length) -> Style {
104    center().width(size.clone()).height(size).flex_shrink(0)
105}
106
107/// A circle `size` across that centers its children.
108#[must_use]
109pub fn circle(size: Length) -> Style {
110    square(size).border_radius(pct(50))
111}
112
113/// A box whose height follows its width at `ratio` (width over height), with
114/// its content clipped to it.
115#[must_use]
116pub fn aspect_ratio(ratio: f64) -> Style {
117    Style::new()
118        .position(Position::Relative)
119        .aspect_ratio(ratio)
120        .overflow(Overflow::Hidden)
121}
122
123/// A line across the box, `width` thick.
124#[must_use]
125pub fn divider(width: Length, color: Color) -> Style {
126    Style::new()
127        .border_top_width(width)
128        .border_top_style(BorderTopStyle::Solid)
129        .border_top_color(color)
130}
131
132/// Hidden from sight but read by screen readers, as the `sr-only` utility of
133/// Tailwind CSS and Panda CSS's `visuallyHidden` pattern write it.
134#[must_use]
135pub fn visually_hidden() -> Style {
136    Style::new()
137        .position(Position::Absolute)
138        .width(px(1))
139        .height(px(1))
140        .padding(px(0))
141        .margin(px(-1))
142        .overflow(Overflow::Hidden)
143        .clip_path(raw("inset(50%)"))
144        .white_space(WhiteSpace::Nowrap)
145        .border_width(px(0))
146}
147
148/// Makes a link cover the nearest positioned ancestor, so a whole card is
149/// clickable. Put [`aspect_ratio`] or `position: relative` on the card.
150#[must_use]
151pub fn link_overlay() -> Style {
152    Style::new().pseudo(
153        PseudoElement::Before,
154        Style::new()
155            .content(quoted(""))
156            .position(Position::Absolute)
157            .inset(px(0)),
158    )
159}
160
161#[cfg(test)]
162mod tests {
163    use super::{circle, grid, link_overlay, stack};
164    use crate::Sheet;
165    use crate::value::rem;
166
167    #[test]
168    fn writes_the_declarations_of_a_pattern() {
169        assert_eq!(
170            stack(rem(1)).class_names().collect::<Vec<_>>(),
171            ["display_flex", "flex-direction_column", "gap_1rem"]
172        );
173        assert!(
174            circle(rem(2))
175                .class_names()
176                .any(|name| name == "border-radius_50%")
177        );
178        let mut sheet = Sheet::new();
179        sheet.add(&grid(3, rem(1)));
180        assert!(
181            sheet
182                .to_css()
183                .contains("{grid-template-columns:repeat(3, minmax(0, 1fr))}")
184        );
185    }
186
187    #[test]
188    fn puts_the_overlay_on_a_pseudo_element() {
189        let mut sheet = Sheet::new();
190        sheet.add(&link_overlay());
191        let css = sheet.to_css();
192        assert!(css.contains("::before{content:\"\"}"));
193        assert!(css.contains("::before{inset:0}"));
194    }
195}