✨ (pagetop): Añade puntos de corte a Flex/FlexItem
- `Breakpoint` gana variante `Xxxl` y métodos `name()`/`min_width()`/ `resolved()`, para consultarse contra el tema activo. - `Theme::breakpoint_entry()` sustituye a `breakpoint_min_width()`: devuelve un `BreakpointEntry` con nombre y ancho mínimo. - Nuevo `Responsive<T>`, valor en cascada mobile-first por punto de corte. - Ejemplo `examples/intro-responsive.rs` con los patrones de uso.
This commit is contained in:
parent
8577ca8a59
commit
0b8f3f3000
22 changed files with 1257 additions and 359 deletions
|
|
@ -142,7 +142,7 @@ async fn form_controls(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
.with_child(
|
.with_child(
|
||||||
Container::new()
|
Container::new()
|
||||||
.with_flex(
|
.with_flex(
|
||||||
Flex::row()
|
Flex::new()
|
||||||
.with_wrap(flex::Behavior::Wrap)
|
.with_wrap(flex::Behavior::Wrap)
|
||||||
.with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))),
|
.with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
)
|
)
|
||||||
|
|
@ -262,7 +262,7 @@ async fn form_controls(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
.with_child(
|
.with_child(
|
||||||
Container::new()
|
Container::new()
|
||||||
.with_flex(
|
.with_flex(
|
||||||
Flex::row()
|
Flex::new()
|
||||||
.with_wrap(flex::Behavior::Wrap)
|
.with_wrap(flex::Behavior::Wrap)
|
||||||
.with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))),
|
.with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
)
|
)
|
||||||
|
|
@ -442,7 +442,7 @@ fn form_lists() -> Form {
|
||||||
.with_child(
|
.with_child(
|
||||||
Container::new()
|
Container::new()
|
||||||
.with_flex(
|
.with_flex(
|
||||||
Flex::row()
|
Flex::new()
|
||||||
.with_wrap(flex::Behavior::Wrap)
|
.with_wrap(flex::Behavior::Wrap)
|
||||||
.with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))),
|
.with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -40,21 +40,21 @@ fn direction_block() -> Block {
|
||||||
let mut block = Block::new().with_title(Lc::t("flex_block_title_direction", &LOC));
|
let mut block = Block::new().with_title(Lc::t("flex_block_title_direction", &LOC));
|
||||||
|
|
||||||
let direction_variants: [(&str, Flex, &str); 4] = [
|
let direction_variants: [(&str, Flex, &str); 4] = [
|
||||||
("flex_title_direction_row", Flex::row(), "Flex::row()"),
|
("flex_title_direction_row", Flex::new(), "Flex::new()"),
|
||||||
(
|
(
|
||||||
"flex_title_direction_row_reverse",
|
"flex_title_direction_row_reverse",
|
||||||
Flex::row().with_direction(flex::Direction::RowReverse),
|
Flex::new().with_direction(flex::Direction::RowReverse),
|
||||||
"Flex::row().with_direction(Direction::RowReverse)",
|
"Flex::new().with_direction(Direction::RowReverse)",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"flex_title_direction_column",
|
"flex_title_direction_column",
|
||||||
Flex::column(),
|
Flex::new().with_direction(flex::Direction::Column),
|
||||||
"Flex::column()",
|
"Flex::new().with_direction(Direction::Column)",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"flex_title_direction_column_reverse",
|
"flex_title_direction_column_reverse",
|
||||||
Flex::column().with_direction(flex::Direction::ColumnReverse),
|
Flex::new().with_direction(flex::Direction::ColumnReverse),
|
||||||
"Flex::column().with_direction(Direction::ColumnReverse)",
|
"Flex::new().with_direction(Direction::ColumnReverse)",
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
for (title_key, flex, code) in direction_variants {
|
for (title_key, flex, code) in direction_variants {
|
||||||
|
|
@ -77,32 +77,32 @@ fn justify_block() -> Block {
|
||||||
(
|
(
|
||||||
"flex_title_justify_start",
|
"flex_title_justify_start",
|
||||||
flex::ContentJustify::Start,
|
flex::ContentJustify::Start,
|
||||||
"Flex::row().with_justify(ContentJustify::Start)",
|
"Flex::new().with_justify(ContentJustify::Start)",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"flex_title_justify_center",
|
"flex_title_justify_center",
|
||||||
flex::ContentJustify::Center,
|
flex::ContentJustify::Center,
|
||||||
"Flex::row().with_justify(ContentJustify::Center)",
|
"Flex::new().with_justify(ContentJustify::Center)",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"flex_title_justify_end",
|
"flex_title_justify_end",
|
||||||
flex::ContentJustify::End,
|
flex::ContentJustify::End,
|
||||||
"Flex::row().with_justify(ContentJustify::End)",
|
"Flex::new().with_justify(ContentJustify::End)",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"flex_title_justify_between",
|
"flex_title_justify_between",
|
||||||
flex::ContentJustify::SpaceBetween,
|
flex::ContentJustify::SpaceBetween,
|
||||||
"Flex::row().with_justify(ContentJustify::SpaceBetween)",
|
"Flex::new().with_justify(ContentJustify::SpaceBetween)",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"flex_title_justify_around",
|
"flex_title_justify_around",
|
||||||
flex::ContentJustify::SpaceAround,
|
flex::ContentJustify::SpaceAround,
|
||||||
"Flex::row().with_justify(ContentJustify::SpaceAround)",
|
"Flex::new().with_justify(ContentJustify::SpaceAround)",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"flex_title_justify_evenly",
|
"flex_title_justify_evenly",
|
||||||
flex::ContentJustify::SpaceEvenly,
|
flex::ContentJustify::SpaceEvenly,
|
||||||
"Flex::row().with_justify(ContentJustify::SpaceEvenly)",
|
"Flex::new().with_justify(ContentJustify::SpaceEvenly)",
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
for (title_key, justify, code) in justify_variants {
|
for (title_key, justify, code) in justify_variants {
|
||||||
|
|
@ -110,7 +110,7 @@ fn justify_block() -> Block {
|
||||||
.with_child(caption(Lc::t(title_key, &LOC), Lc::n(code)))
|
.with_child(caption(Lc::t(title_key, &LOC), Lc::n(code)))
|
||||||
.with_child(
|
.with_child(
|
||||||
demo_row(
|
demo_row(
|
||||||
Flex::row()
|
Flex::new()
|
||||||
.with_justify(justify)
|
.with_justify(justify)
|
||||||
.with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))),
|
.with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
)
|
)
|
||||||
|
|
@ -129,22 +129,22 @@ fn align_block() -> Block {
|
||||||
(
|
(
|
||||||
"flex_title_align_start",
|
"flex_title_align_start",
|
||||||
flex::Align::Start,
|
flex::Align::Start,
|
||||||
"Flex::row().with_align(Align::Start)",
|
"Flex::new().with_align(Align::Start)",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"flex_title_align_center",
|
"flex_title_align_center",
|
||||||
flex::Align::Center,
|
flex::Align::Center,
|
||||||
"Flex::row().with_align(Align::Center)",
|
"Flex::new().with_align(Align::Center)",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"flex_title_align_end",
|
"flex_title_align_end",
|
||||||
flex::Align::End,
|
flex::Align::End,
|
||||||
"Flex::row().with_align(Align::End)",
|
"Flex::new().with_align(Align::End)",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"flex_title_align_stretch",
|
"flex_title_align_stretch",
|
||||||
flex::Align::Stretch,
|
flex::Align::Stretch,
|
||||||
"Flex::row().with_align(Align::Stretch)",
|
"Flex::new().with_align(Align::Stretch)",
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
for (title_key, align, code) in align_variants {
|
for (title_key, align, code) in align_variants {
|
||||||
|
|
@ -152,7 +152,7 @@ fn align_block() -> Block {
|
||||||
.with_child(caption(Lc::t(title_key, &LOC), Lc::n(code)))
|
.with_child(caption(Lc::t(title_key, &LOC), Lc::n(code)))
|
||||||
.with_child(
|
.with_child(
|
||||||
demo_row(
|
demo_row(
|
||||||
Flex::row()
|
Flex::new()
|
||||||
.with_align(align)
|
.with_align(align)
|
||||||
.with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))),
|
.with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
)
|
)
|
||||||
|
|
@ -164,11 +164,11 @@ fn align_block() -> Block {
|
||||||
block
|
block
|
||||||
.with_child(caption(
|
.with_child(caption(
|
||||||
Lc::t("flex_title_align_baseline", &LOC),
|
Lc::t("flex_title_align_baseline", &LOC),
|
||||||
Lc::n("Flex::row().with_align(Align::Baseline)"),
|
Lc::n("Flex::new().with_align(Align::Baseline)"),
|
||||||
))
|
))
|
||||||
.with_child(
|
.with_child(
|
||||||
demo_row(
|
demo_row(
|
||||||
Flex::row()
|
Flex::new()
|
||||||
.with_align(flex::Align::Baseline)
|
.with_align(flex::Align::Baseline)
|
||||||
.with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))),
|
.with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
)
|
)
|
||||||
|
|
@ -211,7 +211,7 @@ fn align_self_block() -> Block {
|
||||||
.with_child(caption(Lc::t(title_key, &LOC), Lc::n(code)))
|
.with_child(caption(Lc::t(title_key, &LOC), Lc::n(code)))
|
||||||
.with_child(
|
.with_child(
|
||||||
demo_row(
|
demo_row(
|
||||||
Flex::row()
|
Flex::new()
|
||||||
.with_align(flex::Align::Start)
|
.with_align(flex::Align::Start)
|
||||||
.with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))),
|
.with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
)
|
)
|
||||||
|
|
@ -229,7 +229,7 @@ fn align_self_block() -> Block {
|
||||||
))
|
))
|
||||||
.with_child(
|
.with_child(
|
||||||
demo_row(
|
demo_row(
|
||||||
Flex::row()
|
Flex::new()
|
||||||
.with_align(flex::Align::Start)
|
.with_align(flex::Align::Start)
|
||||||
.with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))),
|
.with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
)
|
)
|
||||||
|
|
@ -251,42 +251,42 @@ fn align_content_block() -> Block {
|
||||||
(
|
(
|
||||||
"flex_title_align_content_start",
|
"flex_title_align_content_start",
|
||||||
flex::AlignContent::Start,
|
flex::AlignContent::Start,
|
||||||
"Flex::row().with_wrap(Behavior::Wrap).with_align_content(AlignContent::Start)",
|
"Flex::new().with_wrap(Behavior::Wrap).with_align_content(AlignContent::Start)",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"flex_title_align_content_end",
|
"flex_title_align_content_end",
|
||||||
flex::AlignContent::End,
|
flex::AlignContent::End,
|
||||||
"Flex::row().with_wrap(Behavior::Wrap).with_align_content(AlignContent::End)",
|
"Flex::new().with_wrap(Behavior::Wrap).with_align_content(AlignContent::End)",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"flex_title_align_content_center",
|
"flex_title_align_content_center",
|
||||||
flex::AlignContent::Center,
|
flex::AlignContent::Center,
|
||||||
"Flex::row().with_wrap(Behavior::Wrap).with_align_content(AlignContent::Center)",
|
"Flex::new().with_wrap(Behavior::Wrap).with_align_content(AlignContent::Center)",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"flex_title_align_content_between",
|
"flex_title_align_content_between",
|
||||||
flex::AlignContent::SpaceBetween,
|
flex::AlignContent::SpaceBetween,
|
||||||
"Flex::row().with_wrap(Behavior::Wrap).with_align_content(AlignContent::SpaceBetween)",
|
"Flex::new().with_wrap(Behavior::Wrap).with_align_content(AlignContent::SpaceBetween)",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"flex_title_align_content_around",
|
"flex_title_align_content_around",
|
||||||
flex::AlignContent::SpaceAround,
|
flex::AlignContent::SpaceAround,
|
||||||
"Flex::row().with_wrap(Behavior::Wrap).with_align_content(AlignContent::SpaceAround)",
|
"Flex::new().with_wrap(Behavior::Wrap).with_align_content(AlignContent::SpaceAround)",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"flex_title_align_content_evenly",
|
"flex_title_align_content_evenly",
|
||||||
flex::AlignContent::SpaceEvenly,
|
flex::AlignContent::SpaceEvenly,
|
||||||
"Flex::row().with_wrap(Behavior::Wrap).with_align_content(AlignContent::SpaceEvenly)",
|
"Flex::new().with_wrap(Behavior::Wrap).with_align_content(AlignContent::SpaceEvenly)",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"flex_title_align_content_stretch",
|
"flex_title_align_content_stretch",
|
||||||
flex::AlignContent::Stretch,
|
flex::AlignContent::Stretch,
|
||||||
"Flex::row().with_wrap(Behavior::Wrap).with_align_content(AlignContent::Stretch)",
|
"Flex::new().with_wrap(Behavior::Wrap).with_align_content(AlignContent::Stretch)",
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
for (title_key, align_content, code) in align_content_variants {
|
for (title_key, align_content, code) in align_content_variants {
|
||||||
let mut row = demo_row(
|
let mut row = demo_row(
|
||||||
Flex::row()
|
Flex::new()
|
||||||
.with_wrap(flex::Behavior::Wrap)
|
.with_wrap(flex::Behavior::Wrap)
|
||||||
.with_align_content(align_content)
|
.with_align_content(align_content)
|
||||||
.with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))),
|
.with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
|
|
@ -314,7 +314,7 @@ fn grow_shrink_block() -> Block {
|
||||||
Lc::n("FlexItem::new().with_grow(flex::ItemGrow::Is1)"),
|
Lc::n("FlexItem::new().with_grow(flex::ItemGrow::Is1)"),
|
||||||
))
|
))
|
||||||
.with_child(
|
.with_child(
|
||||||
demo_row(Flex::row().with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))))
|
demo_row(Flex::new().with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))))
|
||||||
.with_child(demo_box(Lc::t("flex_box_fixed", &LOC)))
|
.with_child(demo_box(Lc::t("flex_box_fixed", &LOC)))
|
||||||
.with_child(
|
.with_child(
|
||||||
demo_box(Lc::t("flex_box_grows", &LOC)).with_prop(PropsOp::flex_item(
|
demo_box(Lc::t("flex_box_grows", &LOC)).with_prop(PropsOp::flex_item(
|
||||||
|
|
@ -328,7 +328,7 @@ fn grow_shrink_block() -> Block {
|
||||||
Lc::n("FlexItem::new().with_shrink(flex::ItemShrink::Is0)"),
|
Lc::n("FlexItem::new().with_shrink(flex::ItemShrink::Is0)"),
|
||||||
))
|
))
|
||||||
.with_child(
|
.with_child(
|
||||||
demo_row(Flex::row().with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))))
|
demo_row(Flex::new().with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))))
|
||||||
.with_prop(PropsOp::add_style("max-width", "31rem"))
|
.with_prop(PropsOp::add_style("max-width", "31rem"))
|
||||||
.with_child(
|
.with_child(
|
||||||
demo_box(flex_item("1")).with_prop(PropsOp::add_style("width", "10.5rem")),
|
demo_box(flex_item("1")).with_prop(PropsOp::add_style("width", "10.5rem")),
|
||||||
|
|
@ -355,14 +355,16 @@ fn other_block() -> Block {
|
||||||
Lc::n("FlexItem::push_end()"),
|
Lc::n("FlexItem::push_end()"),
|
||||||
))
|
))
|
||||||
.with_child(
|
.with_child(
|
||||||
demo_row(Flex::row().with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))))
|
demo_row(Flex::new().with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))))
|
||||||
.with_child(demo_box(Lc::t("flex_box_start_1", &LOC)))
|
.with_child(demo_box(Lc::t("flex_box_start_1", &LOC)))
|
||||||
.with_child(demo_box(Lc::t("flex_box_start_2", &LOC)))
|
.with_child(demo_box(Lc::t("flex_box_start_2", &LOC)))
|
||||||
.with_child(demo_box(Lc::t("flex_box_end", &LOC)).with_prop(FlexItem::push_end())),
|
.with_child(
|
||||||
|
demo_box(Lc::t("flex_box_end", &LOC)).with_prop(FlexItem::push_end().into()),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut wrap_row = demo_row(
|
let mut wrap_row = demo_row(
|
||||||
Flex::row()
|
Flex::new()
|
||||||
.with_wrap(flex::Behavior::Wrap)
|
.with_wrap(flex::Behavior::Wrap)
|
||||||
.with_align_content(flex::AlignContent::SpaceBetween)
|
.with_align_content(flex::AlignContent::SpaceBetween)
|
||||||
.with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))),
|
.with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
|
|
@ -379,7 +381,7 @@ fn other_block() -> Block {
|
||||||
.with_child(caption(
|
.with_child(caption(
|
||||||
Lc::t("flex_title_wrap", &LOC),
|
Lc::t("flex_title_wrap", &LOC),
|
||||||
Lc::n(concat!(
|
Lc::n(concat!(
|
||||||
"Flex::row()",
|
"Flex::new()",
|
||||||
".with_wrap(Behavior::Wrap)",
|
".with_wrap(Behavior::Wrap)",
|
||||||
".with_align_content(AlignContent::SpaceBetween)",
|
".with_align_content(AlignContent::SpaceBetween)",
|
||||||
)),
|
)),
|
||||||
|
|
@ -390,7 +392,7 @@ fn other_block() -> Block {
|
||||||
Lc::n("FlexItem::new().with_order(ItemOrder::First) / .with_order(ItemOrder::Last)"),
|
Lc::n("FlexItem::new().with_order(ItemOrder::First) / .with_order(ItemOrder::Last)"),
|
||||||
))
|
))
|
||||||
.with_child(
|
.with_child(
|
||||||
demo_row(Flex::row().with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))))
|
demo_row(Flex::new().with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))))
|
||||||
.with_child(demo_box(Lc::n("A")).with_prop(PropsOp::flex_item(
|
.with_child(demo_box(Lc::n("A")).with_prop(PropsOp::flex_item(
|
||||||
FlexItem::new().with_order(flex::ItemOrder::Last),
|
FlexItem::new().with_order(flex::ItemOrder::Last),
|
||||||
)))
|
)))
|
||||||
|
|
@ -403,20 +405,20 @@ fn other_block() -> Block {
|
||||||
)
|
)
|
||||||
.with_child(caption(
|
.with_child(caption(
|
||||||
Lc::t("flex_title_gap_none", &LOC),
|
Lc::t("flex_title_gap_none", &LOC),
|
||||||
Lc::n("Flex::row() (Gap::None por defecto)"),
|
Lc::n("Flex::new() (Gap::None por defecto)"),
|
||||||
))
|
))
|
||||||
.with_child(
|
.with_child(
|
||||||
demo_row(Flex::row())
|
demo_row(Flex::new())
|
||||||
.with_child(demo_box(flex_item("1")))
|
.with_child(demo_box(flex_item("1")))
|
||||||
.with_child(demo_box(flex_item("2")))
|
.with_child(demo_box(flex_item("2")))
|
||||||
.with_child(demo_box(flex_item("3"))),
|
.with_child(demo_box(flex_item("3"))),
|
||||||
)
|
)
|
||||||
.with_child(caption(
|
.with_child(caption(
|
||||||
Lc::t("flex_title_gap_some", &LOC),
|
Lc::t("flex_title_gap_some", &LOC),
|
||||||
Lc::n("Flex::row().with_gap(flex::Gap::Both(UnitValue::RelRem(1.5)))"),
|
Lc::n("Flex::new().with_gap(flex::Gap::Both(UnitValue::RelRem(1.5)))"),
|
||||||
))
|
))
|
||||||
.with_child(
|
.with_child(
|
||||||
demo_row(Flex::row().with_gap(flex::Gap::Both(UnitValue::RelRem(1.5))))
|
demo_row(Flex::new().with_gap(flex::Gap::Both(UnitValue::RelRem(1.5))))
|
||||||
.with_child(demo_box(flex_item("1")))
|
.with_child(demo_box(flex_item("1")))
|
||||||
.with_child(demo_box(flex_item("2")))
|
.with_child(demo_box(flex_item("2")))
|
||||||
.with_child(demo_box(flex_item("3"))),
|
.with_child(demo_box(flex_item("3"))),
|
||||||
|
|
@ -426,7 +428,7 @@ fn other_block() -> Block {
|
||||||
Lc::n("FlexItem::new().with_size(flex::ItemSize::Percent33)"),
|
Lc::n("FlexItem::new().with_size(flex::ItemSize::Percent33)"),
|
||||||
))
|
))
|
||||||
.with_child(
|
.with_child(
|
||||||
demo_row(Flex::row())
|
demo_row(Flex::new())
|
||||||
.with_child(demo_box(Lc::n("1/3")).with_prop(PropsOp::flex_item(
|
.with_child(demo_box(Lc::n("1/3")).with_prop(PropsOp::flex_item(
|
||||||
FlexItem::new().with_size(flex::ItemSize::Percent33),
|
FlexItem::new().with_size(flex::ItemSize::Percent33),
|
||||||
)))
|
)))
|
||||||
|
|
@ -446,7 +448,7 @@ fn other_block() -> Block {
|
||||||
)),
|
)),
|
||||||
))
|
))
|
||||||
.with_child(
|
.with_child(
|
||||||
demo_row(Flex::row()).with_child(
|
demo_row(Flex::new()).with_child(
|
||||||
demo_box(Lc::t("flex_box_half_centered", &LOC)).with_prop(PropsOp::flex_item(
|
demo_box(Lc::t("flex_box_half_centered", &LOC)).with_prop(PropsOp::flex_item(
|
||||||
FlexItem::new()
|
FlexItem::new()
|
||||||
.with_size(flex::ItemSize::Percent50)
|
.with_size(flex::ItemSize::Percent50)
|
||||||
|
|
@ -460,14 +462,16 @@ fn other_block() -> Block {
|
||||||
))
|
))
|
||||||
.with_child(
|
.with_child(
|
||||||
demo_row(
|
demo_row(
|
||||||
Flex::row()
|
Flex::new()
|
||||||
.with_align(flex::Align::Center)
|
.with_align(flex::Align::Center)
|
||||||
.with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))),
|
.with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
)
|
)
|
||||||
.with_child(demo_box(Lc::t("flex_box_file", &LOC)))
|
.with_child(demo_box(Lc::t("flex_box_file", &LOC)))
|
||||||
.with_child(demo_box(Lc::t("flex_box_edit", &LOC)))
|
.with_child(demo_box(Lc::t("flex_box_edit", &LOC)))
|
||||||
.with_child(demo_box(Lc::t("flex_box_view", &LOC)))
|
.with_child(demo_box(Lc::t("flex_box_view", &LOC)))
|
||||||
.with_child(demo_box(Lc::t("flex_box_profile", &LOC)).with_prop(FlexItem::push_end()))
|
.with_child(
|
||||||
|
demo_box(Lc::t("flex_box_profile", &LOC)).with_prop(FlexItem::push_end().into()),
|
||||||
|
)
|
||||||
.with_child(demo_box(Lc::t("flex_box_logout", &LOC))),
|
.with_child(demo_box(Lc::t("flex_box_logout", &LOC))),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
261
examples/intro-responsive.rs
Normal file
261
examples/intro-responsive.rs
Normal file
|
|
@ -0,0 +1,261 @@
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
include_locales!(LOC from "examples/locale");
|
||||||
|
|
||||||
|
struct IntroResponsive;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Extension for IntroResponsive {
|
||||||
|
fn dependencies(&self) -> Vec<ExtensionRef> {
|
||||||
|
vec![&pagetop_bootsier::Bootsier]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configure_router(&self, router: Router) -> Router {
|
||||||
|
router.route("/", web::get(intro_responsive))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn intro_responsive(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
|
Page::new(request)
|
||||||
|
.with_assets(AssetsOp::AddStyleSheet(demo_styles()))
|
||||||
|
.with_child(
|
||||||
|
Intro::default()
|
||||||
|
.with_opening(IntroOpening::Custom)
|
||||||
|
.with_title(Lc::n("PageTop"))
|
||||||
|
.with_slogan(Lc::t("responsive_slogan", &LOC))
|
||||||
|
.with_button(None::<(Lc, Route)>)
|
||||||
|
.with_child(Html::with(|cx| {
|
||||||
|
html! {
|
||||||
|
p class="intro-text-lead" {
|
||||||
|
(Lc::t("responsive_note", &LOC).using(cx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.with_child(activation_block())
|
||||||
|
.with_child(direction_block())
|
||||||
|
.with_child(grid_block())
|
||||||
|
.with_child(justify_align_block())
|
||||||
|
.with_child(order_block())
|
||||||
|
.with_child(gap_grow_block()),
|
||||||
|
)
|
||||||
|
.render()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn activation_block() -> Block {
|
||||||
|
Block::new()
|
||||||
|
.with_title(Lc::t("responsive_block_title_activation", &LOC))
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("responsive_title_activation", &LOC),
|
||||||
|
Lc::n("Flex::at(Breakpoint::Md)"),
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
demo_row(Flex::at(Breakpoint::Md).with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))))
|
||||||
|
.with_child(demo_box(Lc::t("responsive_box_nav_home", &LOC)))
|
||||||
|
.with_child(demo_box(Lc::t("responsive_box_nav_products", &LOC)))
|
||||||
|
.with_child(demo_box(Lc::t("responsive_box_nav_about", &LOC)))
|
||||||
|
.with_child(demo_box(Lc::t("responsive_box_nav_contact", &LOC))),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn direction_block() -> Block {
|
||||||
|
Block::new()
|
||||||
|
.with_title(Lc::t("responsive_block_title_direction", &LOC))
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("responsive_title_direction", &LOC),
|
||||||
|
Lc::n(concat!(
|
||||||
|
"Flex::new()",
|
||||||
|
".with_direction(Direction::Column)",
|
||||||
|
".with_direction_at(Breakpoint::Md, Direction::RowReverse)",
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
demo_row(
|
||||||
|
Flex::new()
|
||||||
|
.with_direction(flex::Direction::Column)
|
||||||
|
.with_direction_at(Breakpoint::Md, flex::Direction::RowReverse)
|
||||||
|
.with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
|
)
|
||||||
|
.with_child(sized_box(
|
||||||
|
Lc::t("responsive_box_image", &LOC),
|
||||||
|
"2.5rem 1rem",
|
||||||
|
))
|
||||||
|
.with_child(demo_box(Lc::t("responsive_box_text", &LOC))),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn grid_block() -> Block {
|
||||||
|
let mut row = demo_row(Flex::new().with_wrap(flex::Behavior::Wrap));
|
||||||
|
for n in 1..=6 {
|
||||||
|
row = row.with_child(
|
||||||
|
Container::new()
|
||||||
|
.with_prop(PropsOp::add_style("padding", "0.25rem"))
|
||||||
|
.with_prop(PropsOp::flex_item(
|
||||||
|
FlexItem::new()
|
||||||
|
.with_size(flex::ItemSize::Percent100)
|
||||||
|
.with_size_at(Breakpoint::Sm, flex::ItemSize::Percent50)
|
||||||
|
.with_size_at(Breakpoint::Md, flex::ItemSize::Percent33),
|
||||||
|
))
|
||||||
|
.with_child(demo_box(card_label(n))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Block::new()
|
||||||
|
.with_title(Lc::t("responsive_block_title_grid", &LOC))
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("responsive_title_grid", &LOC),
|
||||||
|
Lc::n(concat!(
|
||||||
|
"FlexItem::new()",
|
||||||
|
".with_size(ItemSize::Percent100)",
|
||||||
|
".with_size_at(Breakpoint::Sm, ItemSize::Percent50)",
|
||||||
|
".with_size_at(Breakpoint::Md, ItemSize::Percent33)",
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
.with_child(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn justify_align_block() -> Block {
|
||||||
|
Block::new()
|
||||||
|
.with_title(Lc::t("responsive_block_title_justify_align", &LOC))
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("responsive_title_justify_align", &LOC),
|
||||||
|
Lc::n(concat!(
|
||||||
|
"Flex::new()",
|
||||||
|
".with_justify(ContentJustify::Center)",
|
||||||
|
".with_justify_at(Breakpoint::Md, ContentJustify::SpaceBetween)",
|
||||||
|
".with_align(Align::Center)",
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
demo_row(
|
||||||
|
Flex::new()
|
||||||
|
.with_justify(flex::ContentJustify::Center)
|
||||||
|
.with_justify_at(Breakpoint::Md, flex::ContentJustify::SpaceBetween)
|
||||||
|
.with_align(flex::Align::Center)
|
||||||
|
.with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
|
)
|
||||||
|
.with_child(demo_box(Lc::t("responsive_box_logo", &LOC)))
|
||||||
|
.with_child(demo_box(Lc::t("responsive_box_menu", &LOC))),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn order_block() -> Block {
|
||||||
|
Block::new()
|
||||||
|
.with_title(Lc::t("responsive_block_title_order", &LOC))
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("responsive_title_order", &LOC),
|
||||||
|
Lc::n(concat!(
|
||||||
|
"Flex::at(Breakpoint::Lg)",
|
||||||
|
" + FlexItem::new().with_order_at(Breakpoint::Lg, ItemOrder::First)",
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
demo_row(Flex::at(Breakpoint::Lg).with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))))
|
||||||
|
.with_child(demo_box(Lc::t("responsive_box_content", &LOC)))
|
||||||
|
.with_child(demo_box(Lc::t("responsive_box_sidebar", &LOC)).with_prop(
|
||||||
|
PropsOp::flex_item(
|
||||||
|
FlexItem::new().with_order_at(Breakpoint::Lg, flex::ItemOrder::First),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gap_grow_block() -> Block {
|
||||||
|
Block::new()
|
||||||
|
.with_title(Lc::t("responsive_block_title_gap_grow", &LOC))
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("responsive_title_gap_grow", &LOC),
|
||||||
|
Lc::n(concat!(
|
||||||
|
"Flex::new()",
|
||||||
|
".with_gap(Gap::Both(RelRem(0.5)))",
|
||||||
|
".with_gap_at(Breakpoint::Md, Gap::Both(RelRem(1.5)))",
|
||||||
|
" + FlexItem::new().with_grow_at(Breakpoint::Md, ItemGrow::Is1)",
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
demo_row(
|
||||||
|
Flex::new()
|
||||||
|
.with_gap(flex::Gap::Both(UnitValue::RelRem(0.5)))
|
||||||
|
.with_gap_at(Breakpoint::Md, flex::Gap::Both(UnitValue::RelRem(1.5))),
|
||||||
|
)
|
||||||
|
.with_child(demo_box(Lc::t("responsive_box_file", &LOC)))
|
||||||
|
.with_child(demo_box(Lc::t("responsive_box_edit", &LOC)))
|
||||||
|
.with_child(demo_box(Lc::t("responsive_box_search", &LOC)).with_prop(
|
||||||
|
PropsOp::flex_item(
|
||||||
|
FlexItem::new().with_grow_at(Breakpoint::Md, flex::ItemGrow::Is1),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< HELPERS >************************************************************************************
|
||||||
|
|
||||||
|
// Aspecto fijo de las cajas y filas de muestra.
|
||||||
|
fn demo_styles() -> StyleSheet {
|
||||||
|
StyleSheet::inline("intro-responsive", |_| {
|
||||||
|
util::indoc!(
|
||||||
|
r#"
|
||||||
|
.flex-demo-box {
|
||||||
|
background-color: #0d6efd;
|
||||||
|
color: #fff;
|
||||||
|
min-width: 3rem;
|
||||||
|
width: auto;
|
||||||
|
max-width: none;
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.flex-demo-row {
|
||||||
|
background-color: #f1f3f5;
|
||||||
|
width: 100%;
|
||||||
|
max-width: none;
|
||||||
|
margin: 0 0 1.5rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
"#
|
||||||
|
)
|
||||||
|
.to_string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Caja con fondo azul y relleno vertical configurable, para mostrar diferencias de altura.
|
||||||
|
fn sized_box(label: Lc, padding: &'static str) -> Container {
|
||||||
|
Container::new()
|
||||||
|
.with_prop(PropsOp::add_classes("flex-demo-box"))
|
||||||
|
.with_prop(PropsOp::add_style("padding", padding))
|
||||||
|
.with_child(Html::with(move |cx| html! { (label.using(cx)) }))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Caja con el relleno vertical estandar del resto de ejemplos.
|
||||||
|
fn demo_box(label: Lc) -> Container {
|
||||||
|
sized_box(label, "0.5rem 1rem")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Etiqueta "Tarjeta N" para las cajas de la rejilla responsive.
|
||||||
|
fn card_label(n: usize) -> Lc {
|
||||||
|
Lc::t("responsive_box_card", &LOC).with_arg("n", n.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fila de demostracion con fondo gris para visualizar los limites del propio contenedor flex.
|
||||||
|
fn demo_row(flex: Flex) -> Container {
|
||||||
|
Container::new()
|
||||||
|
.with_prop(PropsOp::add_classes("flex-demo-row"))
|
||||||
|
.with_flex(flex)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Titulo y fragmento de codigo que introducen cada demostracion.
|
||||||
|
fn caption(title: Lc, code: Lc) -> Html {
|
||||||
|
Html::with(move |cx| {
|
||||||
|
html! {
|
||||||
|
h3 { (title.using(cx)) }
|
||||||
|
p { code { (code.using(cx)) } }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::main]
|
||||||
|
async fn main() -> std::io::Result<()> {
|
||||||
|
Application::prepare(&IntroResponsive).await.run().await
|
||||||
|
}
|
||||||
36
examples/locale/en-US/intro-responsive.ftl
Normal file
36
examples/locale/en-US/intro-responsive.ftl
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
responsive_slogan = Responsive behavior with Flex
|
||||||
|
responsive_note = Resize your browser window to see how the boxes below react. The breakpoints used are the usual Bootstrap ones: sm from 576px, md from 768px, lg from 992px.
|
||||||
|
|
||||||
|
responsive_block_title_activation = Flex activation at a breakpoint
|
||||||
|
responsive_block_title_direction = Direction change
|
||||||
|
responsive_block_title_grid = Column grid
|
||||||
|
responsive_block_title_justify_align = Justify and align
|
||||||
|
responsive_block_title_order = Visual order
|
||||||
|
responsive_block_title_gap_grow = Spacing and growth
|
||||||
|
|
||||||
|
responsive_title_activation = Stacked menu on mobile, in a row from md
|
||||||
|
responsive_title_direction = Image and text stacked on mobile; in a row, image on the right, from md
|
||||||
|
responsive_title_grid = From one column on mobile to three on desktop
|
||||||
|
responsive_title_justify_align = Centered header on mobile, spread out from md
|
||||||
|
responsive_title_order = Sidebar after the content on mobile, before it from lg
|
||||||
|
responsive_title_gap_grow = Growing spacing and a search box that only grows from md
|
||||||
|
|
||||||
|
responsive_box_nav_home = Home
|
||||||
|
responsive_box_nav_products = Products
|
||||||
|
responsive_box_nav_about = About
|
||||||
|
responsive_box_nav_contact = Contact
|
||||||
|
|
||||||
|
responsive_box_image = Image
|
||||||
|
responsive_box_text = Text
|
||||||
|
|
||||||
|
responsive_box_card = Card { $n }
|
||||||
|
|
||||||
|
responsive_box_logo = Logo
|
||||||
|
responsive_box_menu = Menu
|
||||||
|
|
||||||
|
responsive_box_content = Content
|
||||||
|
responsive_box_sidebar = Sidebar
|
||||||
|
|
||||||
|
responsive_box_file = File
|
||||||
|
responsive_box_edit = Edit
|
||||||
|
responsive_box_search = Search
|
||||||
36
examples/locale/es-ES/intro-responsive.ftl
Normal file
36
examples/locale/es-ES/intro-responsive.ftl
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
responsive_slogan = Comportamiento responsive con Flex
|
||||||
|
responsive_note = Cambia el ancho de la ventana del navegador para ver cómo reaccionan las siguientes cajas. Los puntos de corte usados son los habituales de Bootstrap: sm a partir de 576px, md a partir de 768px, lg a partir de 992px.
|
||||||
|
|
||||||
|
responsive_block_title_activation = Activación de Flex por punto de corte
|
||||||
|
responsive_block_title_direction = Cambio de dirección
|
||||||
|
responsive_block_title_grid = Rejilla de columnas
|
||||||
|
responsive_block_title_justify_align = Justificación y alineación
|
||||||
|
responsive_block_title_order = Orden visual
|
||||||
|
responsive_block_title_gap_grow = Espaciado y crecimiento
|
||||||
|
|
||||||
|
responsive_title_activation = Menú apilado en móvil, en fila a partir de md
|
||||||
|
responsive_title_direction = Imagen y texto apilados en móvil; en fila, con la imagen a la derecha, a partir de md
|
||||||
|
responsive_title_grid = De una columna en móvil a tres en escritorio
|
||||||
|
responsive_title_justify_align = Cabecera centrada en móvil, distribuida a partir de md
|
||||||
|
responsive_title_order = Barra lateral después del contenido en móvil, antes a partir de lg
|
||||||
|
responsive_title_gap_grow = Espaciado creciente y buscador que sólo crece a partir de md
|
||||||
|
|
||||||
|
responsive_box_nav_home = Inicio
|
||||||
|
responsive_box_nav_products = Productos
|
||||||
|
responsive_box_nav_about = Nosotros
|
||||||
|
responsive_box_nav_contact = Contacto
|
||||||
|
|
||||||
|
responsive_box_image = Imagen
|
||||||
|
responsive_box_text = Texto
|
||||||
|
|
||||||
|
responsive_box_card = Tarjeta { $n }
|
||||||
|
|
||||||
|
responsive_box_logo = Logotipo
|
||||||
|
responsive_box_menu = Menú
|
||||||
|
|
||||||
|
responsive_box_content = Contenido
|
||||||
|
responsive_box_sidebar = Barra lateral
|
||||||
|
|
||||||
|
responsive_box_file = Archivo
|
||||||
|
responsive_box_edit = Editar
|
||||||
|
responsive_box_search = Buscador
|
||||||
|
|
@ -83,7 +83,7 @@ impl Extension for SuperMenu {
|
||||||
.with_item(bs::navbar::Item::nav(
|
.with_item(bs::navbar::Item::nav(
|
||||||
bs::Nav::new()
|
bs::Nav::new()
|
||||||
// Empuja este menú (y lo que le siga) al extremo final de la barra.
|
// Empuja este menú (y lo que le siga) al extremo final de la barra.
|
||||||
.with_prop(FlexItem::push_end())
|
.with_prop(FlexItem::push_end().into())
|
||||||
.with_item(bs::nav::Item::link(
|
.with_item(bs::nav::Item::link(
|
||||||
Lc::t("menus_item_sign_up", &LOC),
|
Lc::t("menus_item_sign_up", &LOC),
|
||||||
"/auth/sign-up",
|
"/auth/sign-up",
|
||||||
|
|
|
||||||
|
|
@ -363,7 +363,7 @@ fn edit_actions(
|
||||||
|
|
||||||
let mut container = Container::new()
|
let mut container = Container::new()
|
||||||
.with_flex(
|
.with_flex(
|
||||||
Flex::row()
|
Flex::new()
|
||||||
.with_wrap(flex::Behavior::Wrap)
|
.with_wrap(flex::Behavior::Wrap)
|
||||||
.with_align(flex::Align::Center)
|
.with_align(flex::Align::Center)
|
||||||
.with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))),
|
.with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ pub struct Container {
|
||||||
kind: Kind,
|
kind: Kind,
|
||||||
/// Devuelve el posicionamiento Flexbox como contenedor, si tiene alguno.
|
/// Devuelve el posicionamiento Flexbox como contenedor, si tiene alguno.
|
||||||
#[getters(copy)]
|
#[getters(copy)]
|
||||||
flex: Option<Flex>,
|
flex: Flex,
|
||||||
/// Devuelve la lista de componentes (`children`) del contenedor.
|
/// Devuelve la lista de componentes (`children`) del contenedor.
|
||||||
children: Children,
|
children: Children,
|
||||||
}
|
}
|
||||||
|
|
@ -64,25 +64,20 @@ impl Component for Container {
|
||||||
self.props.get_id()
|
self.props.get_id()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup(&mut self, _cx: &mut Context) {
|
|
||||||
if let Some(flex) = self.flex() {
|
|
||||||
flex.apply_to(&mut self.props);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
let output = self.children().render(cx).await;
|
let output = self.children().render(cx).await;
|
||||||
if output.is_empty() {
|
if output.is_empty() {
|
||||||
return Ok(html! {});
|
return Ok(html! {});
|
||||||
}
|
}
|
||||||
|
let container_props = self.props().unpack_with_flex(cx, self.flex());
|
||||||
Ok(match self.kind() {
|
Ok(match self.kind() {
|
||||||
Kind::Default => html! { div (self.props().unpack(cx)) { (output) } },
|
Kind::Default => html! { div (container_props) { (output) } },
|
||||||
Kind::Main => html! { main (self.props().unpack(cx)) { (output) } },
|
Kind::Main => html! { main (container_props) { (output) } },
|
||||||
Kind::Header => html! { header (self.props().unpack(cx)) { (output) } },
|
Kind::Header => html! { header (container_props) { (output) } },
|
||||||
Kind::Footer => html! { footer (self.props().unpack(cx)) { (output) } },
|
Kind::Footer => html! { footer (container_props) { (output) } },
|
||||||
Kind::Section => html! { section (self.props().unpack(cx)) { (output) } },
|
Kind::Section => html! { section (container_props) { (output) } },
|
||||||
Kind::Article => html! { article (self.props().unpack(cx)) { (output) } },
|
Kind::Article => html! { article (container_props) { (output) } },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -145,7 +140,7 @@ impl Container {
|
||||||
|
|
||||||
/// Establece el posicionamiento Flexbox como contenedor (usa `None` para quitarlo).
|
/// Establece el posicionamiento Flexbox como contenedor (usa `None` para quitarlo).
|
||||||
pub fn with_flex(mut self, flex: impl Into<Option<Flex>>) -> Self {
|
pub fn with_flex(mut self, flex: impl Into<Option<Flex>>) -> Self {
|
||||||
self.flex = flex.into();
|
self.flex = self.flex.merge(flex);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ pub struct Navbar {
|
||||||
layout: navbar::Layout,
|
layout: navbar::Layout,
|
||||||
/// Devuelve el posicionamiento Flexbox como contenedor, si tiene alguno.
|
/// Devuelve el posicionamiento Flexbox como contenedor, si tiene alguno.
|
||||||
#[getters(copy)]
|
#[getters(copy)]
|
||||||
flex: Option<Flex>,
|
flex: Flex,
|
||||||
/// Devuelve la lista de contenidos.
|
/// Devuelve la lista de contenidos.
|
||||||
items: Children,
|
items: Children,
|
||||||
}
|
}
|
||||||
|
|
@ -106,21 +106,6 @@ impl Component for Navbar {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
// Botón de despliegue para el contenido colapsable de la barra.
|
|
||||||
fn button(cx: &mut Context, id_content: &str) -> Markup {
|
|
||||||
html! {
|
|
||||||
button
|
|
||||||
type="button"
|
|
||||||
class="navbar-toggle"
|
|
||||||
aria-expanded="false"
|
|
||||||
aria-controls=(id_content)
|
|
||||||
aria-label=[Lc::l("navbar_toggle").lookup(cx)]
|
|
||||||
{
|
|
||||||
span class="navbar-toggle-icon" {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Si no hay contenidos, no tiene sentido mostrar una barra vacía.
|
// Si no hay contenidos, no tiene sentido mostrar una barra vacía.
|
||||||
let items = self.items().render(cx).await;
|
let items = self.items().render(cx).await;
|
||||||
if items.is_empty() {
|
if items.is_empty() {
|
||||||
|
|
@ -131,44 +116,53 @@ impl Component for Navbar {
|
||||||
let id = self.id().unwrap();
|
let id = self.id().unwrap();
|
||||||
let id_content = util::join!(id, "-content");
|
let id_content = util::join!(id, "-content");
|
||||||
|
|
||||||
// Posicionamiento Flexbox opcional (no del `<nav>`, cuya estructura la fija `layout()`).
|
// Botón de despliegue para el contenido colapsable de la barra.
|
||||||
let mut content_props = Props::default();
|
let button = html! {
|
||||||
if let Some(flex) = self.flex() {
|
button
|
||||||
flex.apply_to(&mut content_props);
|
type="button"
|
||||||
|
class="navbar-toggle"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-controls=(&id_content)
|
||||||
|
aria-label=[Lc::l("navbar_toggle").lookup(cx)]
|
||||||
|
{
|
||||||
|
span class="navbar-toggle-icon" {}
|
||||||
}
|
}
|
||||||
content_props.alter_prop(PropsOp::prepend_classes("navbar-content"));
|
};
|
||||||
|
|
||||||
|
// Posicionamiento Flexbox opcional (no del `<nav>`, cuya estructura la fija `layout()`).
|
||||||
|
let content_props = Props::classes("navbar-content").with_id(id_content);
|
||||||
|
|
||||||
Ok(html! {
|
Ok(html! {
|
||||||
nav (self.props().unpack(cx)) {
|
nav (self.props().unpack(cx)) {
|
||||||
@match self.layout() {
|
@match self.layout() {
|
||||||
// Barra más sencilla: sólo contenido, siempre visible.
|
// Barra más sencilla: sólo contenido, siempre visible.
|
||||||
navbar::Layout::Simple => {
|
navbar::Layout::Simple => {
|
||||||
div (content_props.unpack(cx)) { (items) }
|
div (content_props.unpack_with_flex(cx, self.flex())) { (items) }
|
||||||
},
|
},
|
||||||
|
|
||||||
// Barra sencilla que se puede contraer/expandir.
|
// Barra sencilla que se puede contraer/expandir.
|
||||||
navbar::Layout::SimpleToggle => {
|
navbar::Layout::SimpleToggle => {
|
||||||
(button(cx, &id_content))
|
(button)
|
||||||
div id=(&id_content) (content_props.unpack(cx)) { (items) }
|
div (content_props.unpack_with_flex(cx, self.flex())) { (items) }
|
||||||
},
|
},
|
||||||
|
|
||||||
// Barra con marca, siempre visible, sin botón.
|
// Barra con marca, siempre visible, sin botón.
|
||||||
navbar::Layout::SimpleBrandLeft(brand) => {
|
navbar::Layout::SimpleBrandLeft(brand) => {
|
||||||
(brand.render(cx).await)
|
(brand.render(cx).await)
|
||||||
div (content_props.unpack(cx)) { (items) }
|
div (content_props.unpack_with_flex(cx, self.flex())) { (items) }
|
||||||
},
|
},
|
||||||
|
|
||||||
// Barra con marca y botón, en ese orden.
|
// Barra con marca y botón, en ese orden.
|
||||||
navbar::Layout::BrandLeft(brand) => {
|
navbar::Layout::BrandLeft(brand) => {
|
||||||
(brand.render(cx).await)
|
(brand.render(cx).await)
|
||||||
(button(cx, &id_content))
|
(button)
|
||||||
div id=(&id_content) (content_props.unpack(cx)) { (items) }
|
div (content_props.unpack_with_flex(cx, self.flex())) { (items) }
|
||||||
},
|
},
|
||||||
|
|
||||||
// Barra con botón y marca, en ese orden.
|
// Barra con botón y marca, en ese orden.
|
||||||
navbar::Layout::BrandRight(brand) => {
|
navbar::Layout::BrandRight(brand) => {
|
||||||
(button(cx, &id_content))
|
(button)
|
||||||
div id=(&id_content) (content_props.unpack(cx)) { (items) }
|
div (content_props.unpack_with_flex(cx, self.flex())) { (items) }
|
||||||
(brand.render(cx).await)
|
(brand.render(cx).await)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -231,7 +225,7 @@ impl Navbar {
|
||||||
/// No afecta a la posición de la marca ni del botón de despliegue, que quedan fijados con
|
/// No afecta a la posición de la marca ni del botón de despliegue, que quedan fijados con
|
||||||
/// [`with_layout()`](Self::with_layout).
|
/// [`with_layout()`](Self::with_layout).
|
||||||
pub fn with_flex(mut self, flex: impl Into<Option<Flex>>) -> Self {
|
pub fn with_flex(mut self, flex: impl Into<Option<Flex>>) -> Self {
|
||||||
self.flex = flex.into();
|
self.flex = self.flex.merge(flex);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ use crate::html::{Markup, Props, PropsOp, RoutePath, html};
|
||||||
use crate::locale::Lc;
|
use crate::locale::Lc;
|
||||||
use crate::locale::{LangId, LanguageIdentifier, RequestLocale};
|
use crate::locale::{LangId, LanguageIdentifier, RequestLocale};
|
||||||
use crate::web::HttpRequest;
|
use crate::web::HttpRequest;
|
||||||
use crate::{builder_impl, util};
|
use crate::{CowStr, builder_impl, util};
|
||||||
|
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
@ -42,7 +42,7 @@ pub enum AssetsOp {
|
||||||
/// Añade una declaración de estilo responsive (`property: value`) para las clases indicadas,
|
/// Añade una declaración de estilo responsive (`property: value`) para las clases indicadas,
|
||||||
/// dentro del punto de corte dado (`None` para una regla siempre activa). Ver
|
/// dentro del punto de corte dado (`None` para una regla siempre activa). Ver
|
||||||
/// [`ResponsiveStyles::add_style()`].
|
/// [`ResponsiveStyles::add_style()`].
|
||||||
AddResponsiveStyle(Option<Breakpoint>, &'static str, &'static str, &'static str),
|
AddResponsiveStyle(Option<Breakpoint>, CowStr, CowStr, CowStr),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Errores de acceso a parámetros dinámicos del contexto.
|
/// Errores de acceso a parámetros dinámicos del contexto.
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,11 @@
|
||||||
//! PageTop permite crear **temas hijo** que refinan el comportamiento de su tema padre,
|
//! PageTop permite crear **temas hijo** que refinan el comportamiento de su tema padre,
|
||||||
//! identificado por [`Theme::parent()`]. Un tema hijo hereda automáticamente todos los métodos del
|
//! identificado por [`Theme::parent()`]. Un tema hijo hereda automáticamente todos los métodos del
|
||||||
//! padre y puede sobrescribirlos selectivamente. Esta herencia determina qué implementación de sus
|
//! padre y puede sobrescribirlos selectivamente. Esta herencia determina qué implementación de sus
|
||||||
//! métodos se usa cuando el tema hijo no los sobrescribe (ya sea el renderizado del `<body>` o del
|
//! métodos se usa cuando el tema hijo no los sobrescribe, ya sea el renderizado del `<body>` o del
|
||||||
//! `<head>`, la definición de los recursos necesarios, la traducción de puntos de corte y colores
|
//! `<head>`, la definición de los recursos necesarios, la traducción de puntos de corte con
|
||||||
//! por intención vía [`Theme::breakpoint_min_width()`] y [`Theme::intent_color()`], la captura de
|
//! [`Theme::breakpoint_entry()`] y colores según intención vía [`Theme::intent_color()`], la
|
||||||
//! componentes para alterar su comportamiento usando [`Theme::setup_component()`] y
|
//! captura de componentes para alterar su comportamiento usando [`Theme::setup_component()`] y
|
||||||
//! [`Theme::render_component()`], las páginas de error, etc.).
|
//! [`Theme::render_component()`], las páginas de error, etc.
|
||||||
//!
|
//!
|
||||||
//! Un tema hijo puede ser a su vez padre de otro, basta declararlo cada vez en [`Theme::parent()`].
|
//! Un tema hijo puede ser a su vez padre de otro, basta declararlo cada vez en [`Theme::parent()`].
|
||||||
//! Como `parent()` se resuelve en tiempo de ejecución, PageTop no puede descartar en compilación
|
//! Como `parent()` se resuelve en tiempo de ejecución, PageTop no puede descartar en compilación
|
||||||
|
|
@ -78,13 +78,14 @@
|
||||||
//! variantes de plantilla. Para ajustarlo sin rehacer su marcado (añadir una clase, un
|
//! variantes de plantilla. Para ajustarlo sin rehacer su marcado (añadir una clase, un
|
||||||
//! atributo, etc.), se usa [`Theme::setup_component()`] en su lugar.
|
//! atributo, etc.), se usa [`Theme::setup_component()`] en su lugar.
|
||||||
//! 4. **Definir los anchos mínimos *mobile-first* para los puntos de corte** sobrescribiendo
|
//! 4. **Definir los anchos mínimos *mobile-first* para los puntos de corte** sobrescribiendo
|
||||||
//! [`Theme::breakpoint_min_width()`]. Por defecto, [`Breakpoint`] resuelve el ancho mínimo de
|
//! [`Theme::breakpoint_entry()`]. [`Breakpoint`] no define ningún ancho propio; la
|
||||||
//! cada variante (`Sm`, `Md`, etc.) como una cadena CSS ya formateada (p. ej. `"768px"`) que
|
//! implementación por defecto de este método resuelve el ancho mínimo de cada variante (`Sm`,
|
||||||
//! cada tema puede adaptar. Cuando se genera CSS *responsive* a partir de un [`Breakpoint`], se
|
//! `Md`, etc.) como una cadena CSS ya formateada (p. ej. `"768px"`), que cada tema puede
|
||||||
//! consulta el punto de corte a través de [`Breakpoint::min_width()`], listo para interpolar en
|
//! sobrescribir. Cuando se genera CSS *responsive* a partir de un [`Breakpoint`], se consulta el
|
||||||
//! un `@media (min-width: ...)` sin ningún cálculo adicional. Un tema sin diseño *responsive*
|
//! punto de corte a través de [`Breakpoint::min_width()`], listo para interpolar en un
|
||||||
//! puede traducir todas las variantes a `""` porque al ser *mobile-first*, un punto de corte sin
|
//! `@media (min-width: ...)` sin ningún cálculo adicional. Un tema sin diseño *responsive* puede
|
||||||
//! ancho real se aplicará siempre.
|
//! traducir todas las variantes a `""` porque al ser *mobile-first*, un punto de corte sin ancho
|
||||||
|
//! real se aplicará siempre.
|
||||||
//! 5. **Traducir [`Intent`] a la paleta de colores propia del tema** sobrescribiendo
|
//! 5. **Traducir [`Intent`] a la paleta de colores propia del tema** sobrescribiendo
|
||||||
//! [`Theme::intent_color()`]. Por defecto, este método devuelve el vocabulario semántico de
|
//! [`Theme::intent_color()`]. Por defecto, este método devuelve el vocabulario semántico de
|
||||||
//! [`Intent`] (`"primary"`, `"severe"`, etc.); un tema con su propio catálogo de colores (por
|
//! [`Intent`] (`"primary"`, `"severe"`, etc.); un tema con su propio catálogo de colores (por
|
||||||
|
|
@ -155,7 +156,7 @@ mod intent;
|
||||||
pub use intent::Intent;
|
pub use intent::Intent;
|
||||||
|
|
||||||
mod breakpoint;
|
mod breakpoint;
|
||||||
pub use breakpoint::Breakpoint;
|
pub use breakpoint::{Breakpoint, BreakpointEntry, Responsive};
|
||||||
|
|
||||||
mod layout;
|
mod layout;
|
||||||
pub use layout::{CoreRegions, RegionName, RegionRef};
|
pub use layout::{CoreRegions, RegionName, RegionRef};
|
||||||
|
|
|
||||||
|
|
@ -3,46 +3,182 @@ use crate::core::component::{Context, Contextual};
|
||||||
|
|
||||||
// **< Breakpoint >*********************************************************************************
|
// **< Breakpoint >*********************************************************************************
|
||||||
|
|
||||||
/// Puntos de corte *responsive*, *mobile-first* (aplican "a partir de" el ancho indicado).
|
/// Puntos de corte *responsive*, *mobile-first* (se aplican a partir del ancho indicado).
|
||||||
///
|
///
|
||||||
/// No define ningún valor en píxeles por sí mismo; cada tema decide a qué ancho corresponde cada
|
/// `Breakpoint` no define ningún valor en píxeles por sí mismo; cada tema decide qué nombre y a qué
|
||||||
/// variante en su sistema de diseño (ver [`Theme::breakpoint_min_width()`]).
|
/// ancho mínimo corresponde cada variante en su especificación (ver [`Theme::breakpoint_entry()`]).
|
||||||
///
|
///
|
||||||
/// [`Theme::breakpoint_min_width()`]: crate::core::theme::Theme::breakpoint_min_width
|
/// [`Theme::breakpoint_entry()`]: crate::core::theme::Theme::breakpoint_entry
|
||||||
#[derive(AutoDefault, Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
#[derive(AutoDefault, Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub enum Breakpoint {
|
pub enum Breakpoint {
|
||||||
/// Base *mobile-first*, equivale a "siempre".
|
/// Base *mobile-first*, equivale a "siempre aplica".
|
||||||
#[default]
|
#[default]
|
||||||
Xs,
|
Xs,
|
||||||
/// A partir del ancho donde un tema suele pasar de móvil a tableta.
|
/// Aplica a partir del ancho donde un tema suele pasar de móvil a tableta.
|
||||||
Sm,
|
Sm,
|
||||||
/// A partir del ancho donde un tema suele pasar a un escritorio pequeño.
|
/// Aplica a partir del ancho donde un tema suele pasar a un escritorio pequeño.
|
||||||
Md,
|
Md,
|
||||||
/// A partir del ancho donde un tema suele pasar a un escritorio normal.
|
/// Aplica a partir del ancho donde un tema suele pasar a un escritorio normal.
|
||||||
Lg,
|
Lg,
|
||||||
/// A partir del ancho donde un tema suele considerar el escritorio ancho.
|
/// Aplica a partir del ancho donde un tema suele considerar el escritorio ancho.
|
||||||
Xl,
|
Xl,
|
||||||
/// A partir del ancho donde un tema suele considerar el escritorio muy ancho.
|
/// Aplica a partir del ancho donde un tema suele considerar el escritorio muy ancho.
|
||||||
Xxl,
|
Xxl,
|
||||||
|
/// Aplica a partir del ancho donde un tema suele considerar el escritorio extra ancho.
|
||||||
|
Xxxl,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Breakpoint {
|
impl Breakpoint {
|
||||||
// Todas las variantes, en orden mobile-first (de Xs a Xxl).
|
// Todas las variantes, en orden mobile-first (de Xs a Xxxl).
|
||||||
pub(crate) const ALL: [Breakpoint; 6] = [
|
pub(crate) const ALL: [Breakpoint; 7] = [
|
||||||
Breakpoint::Xs,
|
Breakpoint::Xs,
|
||||||
Breakpoint::Sm,
|
Breakpoint::Sm,
|
||||||
Breakpoint::Md,
|
Breakpoint::Md,
|
||||||
Breakpoint::Lg,
|
Breakpoint::Lg,
|
||||||
Breakpoint::Xl,
|
Breakpoint::Xl,
|
||||||
Breakpoint::Xxl,
|
Breakpoint::Xxl,
|
||||||
|
Breakpoint::Xxxl,
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Ancho mínimo resuelto a través del tema activo del contexto actual, como valor CSS ya
|
/// Nombre del punto de corte resuelto a través del tema activo del contexto actual.
|
||||||
/// formateado (p. ej. `"768px"`), o `""` si la variante se aplica siempre, sin ancho real.
|
|
||||||
///
|
///
|
||||||
/// Atajo de [`Theme::breakpoint_min_width()`](crate::core::theme::Theme::breakpoint_min_width)
|
/// Depende de [`Context`] y puede cambiar entre temas. Es un atajo de acceso al campo `name` de
|
||||||
/// a través de [`Context::theme()`].
|
/// [`BreakpointEntry`] devuelto por [`Theme::breakpoint_entry()`] en el tema activo del
|
||||||
|
/// contexto ([`Context::theme()`]).
|
||||||
|
///
|
||||||
|
/// [`Theme::breakpoint_entry()`]: crate::core::theme::Theme::breakpoint_entry
|
||||||
|
#[inline]
|
||||||
|
pub fn name(&self, cx: &Context) -> &'static str {
|
||||||
|
cx.theme().breakpoint_entry(*self).name
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ancho mínimo resuelto para el punto de corte a través del tema activo del contexto actual,
|
||||||
|
/// como valor CSS ya formateado (p. ej. `"768px"`); o devuelve `""` si la variante se aplica
|
||||||
|
/// siempre, sin un ancho real asociado.
|
||||||
|
///
|
||||||
|
/// Normalmente se usará este método, aunque realmente es un atajo de acceso al campo
|
||||||
|
/// `min_width` de [`BreakpointEntry`] que devuelve [`Theme::breakpoint_entry()`] en el tema
|
||||||
|
/// activo del contexto ([`Context::theme()`]).
|
||||||
|
///
|
||||||
|
/// [`Theme::breakpoint_entry()`]: crate::core::theme::Theme::breakpoint_entry
|
||||||
|
#[inline]
|
||||||
pub fn min_width(&self, cx: &Context) -> &'static str {
|
pub fn min_width(&self, cx: &Context) -> &'static str {
|
||||||
cx.theme().breakpoint_min_width(*self)
|
cx.theme().breakpoint_entry(*self).min_width
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resuelve la variante en el tema activo del contexto ([`Context::theme()`]). Devuelve `None`
|
||||||
|
/// si el valor de `min_width` está vacío por lo que no representa ningún ancho mínimo real para
|
||||||
|
/// este tema. Devuelve el [`BreakpointEntry`] completo en caso contrario.
|
||||||
|
///
|
||||||
|
/// Permite decidir si una variante debe tratarse como incondicional (sin envolver en `@media`
|
||||||
|
/// y sin sufijo de punto de corte en nombres de clase), o como un punto de corte real. Se
|
||||||
|
/// devuelve el `BreakpointEntry` completo para poder obtener el [`name`](BreakpointEntry::name)
|
||||||
|
/// y el [`min_width`](BreakpointEntry::min_width) para el tema activo, sin tener que volver a
|
||||||
|
/// consultar el tema.
|
||||||
|
#[inline]
|
||||||
|
pub fn resolved(self, cx: &Context) -> Option<BreakpointEntry> {
|
||||||
|
let entry = cx.theme().breakpoint_entry(self);
|
||||||
|
if entry.min_width.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Breakpoint HELPERS >*********************************************************************
|
||||||
|
|
||||||
|
// Posición de esta variante en `Breakpoint::ALL`, para indexar `Responsive::values`. Válido
|
||||||
|
// porque el orden de declaración del enum coincide con `ALL` (de `Xs` a `Xxxl`).
|
||||||
|
fn index(self) -> usize {
|
||||||
|
self as usize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< BreakpointEntry >****************************************************************************
|
||||||
|
|
||||||
|
/// Punto de corte [`Breakpoint`] con su nombre y ancho mínimo *responsive* en un tema.
|
||||||
|
///
|
||||||
|
/// Ver [`Theme::breakpoint_entry()`](crate::core::theme::Theme::breakpoint_entry) para entender
|
||||||
|
/// cómo definir los puntos de corte en la implementación de un tema dado.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub struct BreakpointEntry {
|
||||||
|
/// Variante de la que procede esta entrada. Coincide siempre con el [`Breakpoint`] pasado a
|
||||||
|
/// [`Theme::breakpoint_entry()`](crate::core::theme::Theme::breakpoint_entry), incluso si el
|
||||||
|
/// tema delega en su padre. Se incluye para que el valor siga siendo identificable aunque se
|
||||||
|
/// conozca de partida.
|
||||||
|
pub breakpoint: Breakpoint,
|
||||||
|
/// Nombre del punto de corte según el tema (p. ej. `"md"` o `"tablet"` podrían ser nombres para
|
||||||
|
/// `Breakpoint::Md` en dos temas diferentes).
|
||||||
|
pub name: &'static str,
|
||||||
|
/// Ancho mínimo *responsive*, como valor CSS ya formateado, por ejemplo `"768px"`. Se usará una
|
||||||
|
/// cadena vacía `""` si la variante se aplica siempre (para cualquier ancho).
|
||||||
|
pub min_width: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Responsive >*********************************************************************************
|
||||||
|
|
||||||
|
/// Encapsula valores para cada punto de corte, aplicados en cascada *mobile-first*.
|
||||||
|
///
|
||||||
|
/// Guarda un valor opcional para cada variante de [`Breakpoint`]. No decide por sí mismo cómo se
|
||||||
|
/// interpreta cada punto de corte. Con [`by_breakpoint()`] se pueden devolver los valores
|
||||||
|
/// establecidos para cada variante, sin resolver ningún ancho ni consultar el tema activo. Traducir
|
||||||
|
/// eso a CSS, incluida la decisión de envolver en `@media` según [`Breakpoint::min_width()`], es
|
||||||
|
/// responsabilidad de quien consuma [`by_breakpoint()`], normalmente para acabar registrado en
|
||||||
|
/// [`ResponsiveStyles`].
|
||||||
|
///
|
||||||
|
/// Uso típico: los campos de [`Flex`]/[`FlexItem`] para el posicionamiento Flexbox de componentes.
|
||||||
|
///
|
||||||
|
/// [`by_breakpoint()`]: Self::by_breakpoint
|
||||||
|
/// [`ResponsiveStyles`]: crate::html::ResponsiveStyles
|
||||||
|
/// [`Flex`]: crate::html::Flex
|
||||||
|
/// [`FlexItem`]: crate::html::FlexItem
|
||||||
|
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct Responsive<T> {
|
||||||
|
values: [Option<T>; 7],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Copy> Responsive<T> {
|
||||||
|
// **< Responsive BUILDER >*********************************************************************
|
||||||
|
|
||||||
|
/// Establece el valor base (sin punto de corte, activo siempre).
|
||||||
|
pub fn set(mut self, value: T) -> Self {
|
||||||
|
self.values[0] = Some(value);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Establece el valor a partir del punto de corte indicado.
|
||||||
|
pub fn set_at(mut self, bp: Breakpoint, value: T) -> Self {
|
||||||
|
self.values[bp.index()] = Some(value);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Combina con otro `Responsive<T>`, punto de corte a punto de corte. Donde `other` tenga un
|
||||||
|
/// valor, sustituye al de `self`; donde no, se conserva el de `self`.
|
||||||
|
pub fn merge(mut self, other: Self) -> Self {
|
||||||
|
for (slot, value) in self.values.iter_mut().zip(other.values) {
|
||||||
|
if value.is_some() {
|
||||||
|
*slot = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Responsive GETTERS >*********************************************************************
|
||||||
|
|
||||||
|
/// Devuelve el valor establecido para el punto de corte exacto indicado, si existe.
|
||||||
|
pub fn get_at(&self, bp: Breakpoint) -> Option<T> {
|
||||||
|
self.values[bp.index()]
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Responsive HELPERS >*********************************************************************
|
||||||
|
|
||||||
|
/// Recorre los valores establecidos, en orden, como pares `(punto de corte, valor)`. Decidir si
|
||||||
|
/// su ancho mínimo resuelto la hace incondicional, el caso por defecto, es responsabilidad de
|
||||||
|
/// quien consuma este iterador, vía [`Breakpoint::resolved()`].
|
||||||
|
pub fn by_breakpoint(&self) -> impl Iterator<Item = (Breakpoint, T)> + '_ {
|
||||||
|
Breakpoint::ALL
|
||||||
|
.iter()
|
||||||
|
.zip(self.values.iter())
|
||||||
|
.filter_map(|(bp, value)| value.map(|value| (*bp, value)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ use crate::base::component::{Html, Intro, IntroOpening, layout};
|
||||||
use crate::core::component::{ChildOp, Component, ComponentError, ComponentRender};
|
use crate::core::component::{ChildOp, Component, ComponentError, ComponentRender};
|
||||||
use crate::core::component::{Context, Contextual};
|
use crate::core::component::{Context, Contextual};
|
||||||
use crate::core::extension::Extension;
|
use crate::core::extension::Extension;
|
||||||
use crate::core::theme::{Breakpoint, CoreRegions, Intent};
|
use crate::core::theme::{Breakpoint, BreakpointEntry, CoreRegions, Intent};
|
||||||
use crate::global;
|
use crate::global;
|
||||||
use crate::html::{Markup, html};
|
use crate::html::{Markup, html};
|
||||||
use crate::locale::Lc;
|
use crate::locale::Lc;
|
||||||
|
|
@ -65,31 +65,59 @@ pub trait Theme: Extension + Send + Sync {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Traduce un [`Breakpoint`] al punto de corte *responsive*, *mobile-first*, propio del tema.
|
/// Traduce un [`Breakpoint`] a su [`BreakpointEntry`] correspondiente, donde se asocia a cada
|
||||||
|
/// variante su nombre y ancho mínimo *responsive*, *mobile-first*, propios del tema.
|
||||||
///
|
///
|
||||||
/// `Breakpoint` no define ningún valor propio en píxeles. Será cada tema el que decida a qué
|
/// `Breakpoint` no define ningún nombre ni ancho mínimo propios. Será cada tema el que decida
|
||||||
/// ancho corresponde cada variante como valor CSS ya formateado (p. ej. `"768px"`), listo para
|
/// cómo se llama cada variante y a qué ancho corresponde (p. ej. `"768px"`) para aplicar en un
|
||||||
/// aplicar en un `@media (min-width: ...)` sin ningún cálculo adicional. La cadena vacía (`""`)
|
/// `@media (min-width: ...)` sin ningún cálculo adicional. La cadena vacía (`""`) en
|
||||||
/// indica que la variante no representa ningún ancho mínimo y se aplica siempre; es el caso de
|
/// `min_width` indica que la variante no representa ningún ancho mínimo y se aplica siempre.
|
||||||
/// `Xs`.
|
|
||||||
///
|
///
|
||||||
/// Normalmente, para resolver un ancho *responsive* no se llamará a este método directamente,
|
/// **Temas sin puntos de corte.** Devolver `""` como ancho para una variante no la deshabilita,
|
||||||
/// sino que se usará [`Breakpoint::min_width()`] a través de [`Context::theme()`].
|
/// de hecho la regla generada para ese punto de corte se sigue renderizando, pero sin incluirla
|
||||||
|
/// en un `@media` (ver [`ResponsiveStyles::render()`]). Por tanto, pasa a aplicarse siempre,
|
||||||
|
/// exactamente igual que si nunca se hubiera pedido ningún punto de corte. Un tema sin diseño
|
||||||
|
/// *responsive* puede traducir así todas las variantes a `""`; no por eso se vuelve un tema
|
||||||
|
/// "desktop-first", sino que cada punto de corte pedido pasa a aplicarse siempre, sin ninguna
|
||||||
|
/// condición de ancho.
|
||||||
///
|
///
|
||||||
|
/// **Temas con menos puntos de corte que variantes.** Dos variantes consecutivas que devuelvan
|
||||||
|
/// el mismo ancho no vacío se funden en la práctica: ambas generan un `@media (min-width: ...)`
|
||||||
|
/// idéntico, así que no hay forma de distinguir en CSS "a partir de `Md`" de "a partir de `Lg`"
|
||||||
|
/// si las dos resuelven, por ejemplo, a `"992px"`. Es la forma correcta de implementar menos
|
||||||
|
/// puntos de corte reales que las siete variantes de `Breakpoint`: repetir el mismo ancho en
|
||||||
|
/// las variantes consecutivas que no se quieran distinguir. Por ejemplo, un tema con tres
|
||||||
|
/// franjas reales (`Xs`, `Md`-`Lg` y `Xl`-`Xxl`-`Xxxl`) devolvería `""` para `Xs`, el mismo
|
||||||
|
/// ancho para `Md` y `Lg`, y otro ancho mayor, también repetido, para `Xl`, `Xxl` y `Xxxl`.
|
||||||
|
/// Para que el resultado siga siendo coherente, los anchos deben mantenerse no decrecientes en
|
||||||
|
/// el orden *mobile-first* (`Xs` a `Xxxl`); repetir un ancho en variantes no consecutivas, o no
|
||||||
|
/// ordenarlos de menor a mayor, produce puntos de corte confusos o contradictorios, aunque nada
|
||||||
|
/// en tiempo de compilación ni de ejecución lo impida.
|
||||||
|
///
|
||||||
|
/// Normalmente, para resolver el nombre o el ancho *responsive* de un punto de corte no se
|
||||||
|
/// llamará a este método directamente, sino que se usarán [`Breakpoint::name()`] y
|
||||||
|
/// [`Breakpoint::min_width()`], respectivamente, a través de [`Context::theme()`].
|
||||||
|
///
|
||||||
|
/// [`Breakpoint::name()`]: crate::core::theme::Breakpoint::name
|
||||||
/// [`Breakpoint::min_width()`]: crate::core::theme::Breakpoint::min_width
|
/// [`Breakpoint::min_width()`]: crate::core::theme::Breakpoint::min_width
|
||||||
/// [`Context::theme()`]: crate::core::component::Context::theme
|
/// [`Context::theme()`]: crate::core::component::Context::theme
|
||||||
|
/// [`ResponsiveStyles::render()`]: crate::html::ResponsiveStyles::render
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
fn breakpoint_min_width(&self, bp: Breakpoint) -> &'static str {
|
fn breakpoint_entry(&self, bp: Breakpoint) -> BreakpointEntry {
|
||||||
if let Some(parent) = self.parent() {
|
if let Some(parent) = self.parent() {
|
||||||
return parent.breakpoint_min_width(bp);
|
return parent.breakpoint_entry(bp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
use Breakpoint::*;
|
||||||
|
|
||||||
match bp {
|
match bp {
|
||||||
Breakpoint::Xs => "",
|
Xs => BreakpointEntry { breakpoint: Xs, name: "xs", min_width: "" },
|
||||||
Breakpoint::Sm => "576px",
|
Sm => BreakpointEntry { breakpoint: Sm, name: "sm", min_width: "576px" },
|
||||||
Breakpoint::Md => "768px",
|
Md => BreakpointEntry { breakpoint: Md, name: "md", min_width: "768px" },
|
||||||
Breakpoint::Lg => "992px",
|
Lg => BreakpointEntry { breakpoint: Lg, name: "lg", min_width: "992px" },
|
||||||
Breakpoint::Xl => "1200px",
|
Xl => BreakpointEntry { breakpoint: Xl, name: "xl", min_width: "1200px" },
|
||||||
Breakpoint::Xxl => "1400px",
|
Xxl => BreakpointEntry { breakpoint: Xxl, name: "xxl", min_width: "1400px" },
|
||||||
|
Xxxl => BreakpointEntry { breakpoint: Xxxl, name: "xxxl", min_width: "1920px" },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -142,13 +142,14 @@ impl ResponsiveStyles {
|
||||||
/// Renderiza las declaraciones acumuladas como texto CSS.
|
/// Renderiza las declaraciones acumuladas como texto CSS.
|
||||||
///
|
///
|
||||||
/// Emite primero las declaraciones sin punto de corte (`None`), siempre sin envoltorio. Luego
|
/// Emite primero las declaraciones sin punto de corte (`None`), siempre sin envoltorio. Luego
|
||||||
/// recorre los puntos de corte reales en orden *mobile-first* (`Xs` a `Xxl`, omitiendo los que
|
/// recorre los puntos de corte reales en orden *mobile-first* (`Xs` a `Xxxl`, omitiendo los que
|
||||||
/// no tengan ninguna declaración) y, para cada uno, agrupa las reglas de todas sus entradas
|
/// no tengan ninguna declaración) y, para cada uno, agrupa las reglas de todas sus entradas
|
||||||
/// (`.clases { propiedad: valor; ... }`). Si el ancho mínimo resuelto por el tema activo
|
/// (`.clases{propiedad:valor;...}`). Si el ancho mínimo resuelto por el tema activo
|
||||||
/// ([`Breakpoint::min_width()`]) es una cadena vacía, las reglas se emiten tal cual, sin punto
|
/// ([`Breakpoint::min_width()`]) es una cadena vacía, las reglas se emiten tal cual, sin punto
|
||||||
/// de corte real; en cualquier otro caso se envuelven en `@media (min-width: ...)`.
|
/// de corte real; en cualquier otro caso se envuelven en `@media(min-width:...)`.
|
||||||
///
|
///
|
||||||
/// El resultado no contiene saltos de línea.
|
/// El resultado no contiene espacios ni saltos de línea salvo los que pueda llevar el propio
|
||||||
|
/// valor de una declaración (p. ej. `font-family: "Segoe UI", sans-serif`).
|
||||||
pub fn render(&self, cx: &Context) -> Markup {
|
pub fn render(&self, cx: &Context) -> Markup {
|
||||||
let mut css = String::new();
|
let mut css = String::new();
|
||||||
|
|
||||||
|
|
@ -164,11 +165,11 @@ impl ResponsiveStyles {
|
||||||
css.push_str(&rules);
|
css.push_str(&rules);
|
||||||
} else {
|
} else {
|
||||||
css.push_str(&util::join!(
|
css.push_str(&util::join!(
|
||||||
"@media (min-width: ",
|
"@media(min-width:",
|
||||||
min_width,
|
min_width,
|
||||||
") { ",
|
"){",
|
||||||
rules,
|
rules,
|
||||||
" }"
|
"}"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -176,8 +177,8 @@ impl ResponsiveStyles {
|
||||||
html! { (PreEscaped(css)) }
|
html! { (PreEscaped(css)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construye, concatenadas y sin separador, las reglas CSS (`.clases { propiedad: valor; ... }`)
|
// Construye, concatenadas y sin separador, las reglas CSS (`.clases{propiedad:valor;...}`) de
|
||||||
// de todas las entradas del punto de corte indicado.
|
// todas las entradas del punto de corte indicado.
|
||||||
fn render_rules(&self, breakpoint: Option<Breakpoint>) -> String {
|
fn render_rules(&self, breakpoint: Option<Breakpoint>) -> String {
|
||||||
let mut rules = String::new();
|
let mut rules = String::new();
|
||||||
for (_, classes, styles) in self
|
for (_, classes, styles) in self
|
||||||
|
|
@ -188,10 +189,10 @@ impl ResponsiveStyles {
|
||||||
let selector = classes.replace(' ', ".");
|
let selector = classes.replace(' ', ".");
|
||||||
let declarations = styles
|
let declarations = styles
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(property, value)| util::join!(property.as_ref(), ": ", value.as_ref()))
|
.map(|(property, value)| util::join!(property.as_ref(), ":", value.as_ref()))
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join("; ");
|
.join(";");
|
||||||
rules.push_str(&util::join!(".", selector, " { ", declarations, " }"));
|
rules.push_str(&util::join!(".", selector, "{", declarations, "}"));
|
||||||
}
|
}
|
||||||
rules
|
rules
|
||||||
}
|
}
|
||||||
|
|
|
||||||
110
src/html/flex.rs
110
src/html/flex.rs
|
|
@ -5,24 +5,30 @@
|
||||||
//! ofrecen su propio `with_flex()`, como [`Container`] o [`Navbar`].
|
//! ofrecen su propio `with_flex()`, como [`Container`] o [`Navbar`].
|
||||||
//!
|
//!
|
||||||
//! [`FlexItem`] configura, en cambio, un único elemento en relación con el contenedor flex de su
|
//! [`FlexItem`] configura, en cambio, un único elemento en relación con el contenedor flex de su
|
||||||
//! padre (crecimiento, reducción, alineación individual, orden, ancho y desplazamiento). Al poder
|
//! padre (crecimiento, reducción, alineación individual, orden, ancho y desplazamiento). No tiene
|
||||||
//! acabar aplicándose sobre cualquier componente (no sólo los que ofrecen `with_flex()`), no tiene
|
//! un builder propio ya que puede acabar aplicándose sobre cualquier componente (no sólo los que
|
||||||
//! un builder propio: se aplica con [`PropsOp::flex_item()`] sobre el `with_prop()` que ya expone
|
//! ofrecen `with_flex()`). Por eso se aplica con [`PropsOp::flex_item()`] sobre el `with_prop()`
|
||||||
//! cualquier componente.
|
//! que normalmente ya expone cualquier componente.
|
||||||
//!
|
//!
|
||||||
//! # Un entorno autosuficiente
|
//! # Un entorno nativo autosuficiente
|
||||||
//!
|
//!
|
||||||
//! Toda la configuración de `Flex`/`FlexItem` se resuelve con estilos en línea (`style="..."`),
|
//! Toda la configuración de `Flex`/`FlexItem` se resuelve generando clases CSS dinámicamente y de
|
||||||
//! nunca como clases CSS (consulta el propio [`Flex`] para ver el porqué). Los estilos en línea
|
//! manera independiente a cualquier tema o framework CSS. El nombre interno de cada clase se deriva
|
||||||
//! tienen la especificidad más alta que existe en CSS, salvo `!important`, así que ningún framework
|
//! de la propiedad y el valor que representa, así que dos elementos con la misma configuración
|
||||||
//! CSS de terceros, ni el CSS de la propia aplicación, puede sobrescribirlo por accidente. Funciona
|
//! comparten la misma regla en vez de duplicarla. Las declaraciones correspondientes se registran
|
||||||
//! igual conviva con quien conviva en la misma página, sin necesidad de coordinar nombres de clase
|
//! vía [`AssetsOp::AddResponsiveStyle`] y se renderizan como reglas en el `<head>` del documento.
|
||||||
//! ni orden alguno en la carga de hojas de estilo.
|
//! Funciona igual conviva con quien conviva en la misma página, sin necesidad de coordinar nombres
|
||||||
|
//! de clase ni orden alguno en la carga de hojas de estilo.
|
||||||
//!
|
//!
|
||||||
//! [Flexbox]: https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Flexible_box_layout
|
//! [Flexbox]: https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Flexible_box_layout
|
||||||
|
//! [`AssetsOp::AddResponsiveStyle`]: crate::core::component::AssetsOp::AddResponsiveStyle
|
||||||
|
//! [`PropsOp::flex_item()`]: crate::html::props::PropsOp::flex_item
|
||||||
//! [`Container`]: crate::base::component::Container
|
//! [`Container`]: crate::base::component::Container
|
||||||
//! [`Navbar`]: crate::base::component::Navbar
|
//! [`Navbar`]: crate::base::component::Navbar
|
||||||
//! [`PropsOp::flex_item()`]: crate::html::props::PropsOp::flex_item
|
|
||||||
|
use crate::CowStr;
|
||||||
|
use crate::core::component::{AssetsOp, Context, Contextual};
|
||||||
|
use crate::core::theme::BreakpointEntry;
|
||||||
|
|
||||||
mod props_container;
|
mod props_container;
|
||||||
pub use props_container::{Align, AlignContent, Behavior, ContentJustify, Direction, Gap};
|
pub use props_container::{Align, AlignContent, Behavior, ContentJustify, Direction, Gap};
|
||||||
|
|
@ -35,3 +41,83 @@ pub use container::Flex;
|
||||||
|
|
||||||
mod item;
|
mod item;
|
||||||
pub use item::FlexItem;
|
pub use item::FlexItem;
|
||||||
|
|
||||||
|
// **< Flex / FlexItem PRIVATE >********************************************************************
|
||||||
|
|
||||||
|
// Sustituye, en un valor CSS ya resuelto, los únicos caracteres (`.`, `%`) que no podrían usarse
|
||||||
|
// como fragmento de un nombre de clase. Así, `"1.5rem"` sería `"1_5rem"` y `"33.3333%"` quedaría
|
||||||
|
// como `"33_3333pct"`.
|
||||||
|
fn value_to_token(value: &str) -> String {
|
||||||
|
value.replace('.', "_").replace('%', "pct")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Añade un estilo (`property: value`) al punto de corte indicado, y la clase a `classes`, separada
|
||||||
|
// con un espacio de las que ya hubiera. Recibe un `BreakpointEntry` ya resuelto (ver
|
||||||
|
// `Breakpoint::resolved()`) y extrae aquí el `Breakpoint` que `AddResponsiveStyle` necesita, que
|
||||||
|
// puede ser nulo si aplica siempre.
|
||||||
|
//
|
||||||
|
// La clase se copia al acumulador y se mueve al `AssetsOp`, sin clonarla. Recibirla como `CowStr`
|
||||||
|
// permite además que las clases fijas, las que no dependen de ningún punto de corte, lleguen como
|
||||||
|
// `&'static str` sin asignar memoria.
|
||||||
|
fn styles(
|
||||||
|
cx: &mut Context,
|
||||||
|
classes: &mut String,
|
||||||
|
entry: Option<BreakpointEntry>,
|
||||||
|
class: CowStr,
|
||||||
|
property: &'static str,
|
||||||
|
value: CowStr,
|
||||||
|
) {
|
||||||
|
if !classes.is_empty() {
|
||||||
|
classes.push(' ');
|
||||||
|
}
|
||||||
|
classes.push_str(&class);
|
||||||
|
|
||||||
|
cx.alter_assets(AssetsOp::AddResponsiveStyle(
|
||||||
|
entry.map(|e| e.breakpoint),
|
||||||
|
class,
|
||||||
|
property.into(),
|
||||||
|
value,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nombre de clase según el punto de corte: `prefix` ya incluye el guion bajo final antes del valor
|
||||||
|
// (p. ej. `"_flex-direction_"`), y `entry` añade su sufijo si aplica (`"_flex-direction_row_md_"`),
|
||||||
|
// ya resuelto para el tema activo (ver `Breakpoint::resolved()`).
|
||||||
|
macro_rules! responsive_class {
|
||||||
|
($prefix:expr, $token:expr, $entry:expr) => {
|
||||||
|
match $entry {
|
||||||
|
None => util::join!($prefix, $token, "_"),
|
||||||
|
Some(entry) => util::join!($prefix, $token, "_", entry.name, "_"),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
use responsive_class;
|
||||||
|
|
||||||
|
// Recorre las entradas para una propiedad `Responsive<T>` cuyo valor CSS es un único `T::value()`,
|
||||||
|
// generando y registrando (vía `styles()`) una clase por punto de corte con valor.
|
||||||
|
//
|
||||||
|
// La forma con el marcador final `val` es para propiedades cuyo valor puede contener `.`/`%` (como
|
||||||
|
// `ItemSize` o `ItemOffset` en `FlexItem`) y necesitan pasar por `value_to_token()`.
|
||||||
|
macro_rules! apply {
|
||||||
|
($cx:expr, $classes:expr, $field:expr, $prefix:literal, $property:literal) => {
|
||||||
|
for (bp, value) in $field.by_breakpoint() {
|
||||||
|
let value = value.value();
|
||||||
|
if !value.is_empty() {
|
||||||
|
let entry = bp.resolved($cx);
|
||||||
|
let class = responsive_class!($prefix, value, entry);
|
||||||
|
styles($cx, $classes, entry, class.into(), $property, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
($cx:expr, $classes:expr, $field:expr, $prefix:literal, $property:literal, val) => {
|
||||||
|
for (bp, value) in $field.by_breakpoint() {
|
||||||
|
let value = value.value();
|
||||||
|
if !value.is_empty() {
|
||||||
|
let entry = bp.resolved($cx);
|
||||||
|
let class = responsive_class!($prefix, value_to_token(&value), entry);
|
||||||
|
styles($cx, $classes, entry, class.into(), $property, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
use apply;
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,39 @@
|
||||||
use crate::html::flex::props_container::{
|
use crate::core::component::Context;
|
||||||
Align, AlignContent, Behavior, ContentJustify, Direction, Gap,
|
use crate::core::theme::{Breakpoint, Responsive};
|
||||||
};
|
use crate::html::flex::{Align, AlignContent, Behavior, ContentJustify, Direction, Gap};
|
||||||
use crate::html::props::{Props, PropsOp};
|
use crate::{AutoDefault, Getters, builder_impl, util};
|
||||||
use crate::{AutoDefault, Getters, builder_impl};
|
|
||||||
|
// **< DisplayFlex >********************************************************************************
|
||||||
|
|
||||||
|
// Modo de activación del posicionamiento Flexbox de un contenedor `Flex`. Detalle interno de
|
||||||
|
// implementación: la API pública sólo expone los constructores `Flex::new()`, `Flex::at()`,
|
||||||
|
// `Flex::inline()` e `Flex::inline_at()`, nunca esta variante directamente.
|
||||||
|
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||||
|
enum DisplayFlex {
|
||||||
|
#[default]
|
||||||
|
Always,
|
||||||
|
AlwaysInline,
|
||||||
|
At(Breakpoint),
|
||||||
|
InlineAt(Breakpoint),
|
||||||
|
}
|
||||||
|
|
||||||
// **< Flex >***************************************************************************************
|
// **< Flex >***************************************************************************************
|
||||||
|
|
||||||
/// Configuración para el posicionamiento Flexbox en un contenedor.
|
/// Configuración para el posicionamiento Flexbox en un contenedor.
|
||||||
///
|
///
|
||||||
/// Se resuelve con estilos en línea (`display`, `flex-direction`, `flex-wrap`, `justify-content`,
|
/// Se resuelve como clases CSS generadas dinámicamente (`display`, `flex-direction`, `flex-wrap`,
|
||||||
/// `align-items`, `align-content`, `gap`), nunca como clases CSS. Son propiedades estándar que no
|
/// `justify-content`, `align-items`, `align-content`, `gap`), registradas vía
|
||||||
/// requieren interpretación por parte de los temas, siempre funcionan igual, sin una sola
|
/// [`AssetsOp::AddResponsiveStyle`] en [`ResponsiveStyles`] y renderizadas como reglas en el
|
||||||
/// línea de CSS ni de código específico.
|
/// `<head>` del documento. Son propiedades nativas que no requieren interpretación por parte de los
|
||||||
|
/// temas, siempre funcionan igual, sin una sola línea de CSS ni de código específico.
|
||||||
///
|
///
|
||||||
/// Esto tiene además una consecuencia práctica; los estilos en línea tienen la especificidad más
|
/// El nombre de cada clase se deriva de la propiedad y el valor que representa (por ejemplo
|
||||||
/// alta que existe en CSS, salvo `!important`. Ningún *framework* CSS de terceros, ni el CSS de la
|
/// `_flex-direction_row_`), así que dos contenedores con la misma configuración comparten la misma
|
||||||
/// propia aplicación, puede sobrescribir por accidente lo que `Flex` aplica. Es un mecanismo
|
/// regla generada en vez de duplicarla, y el nombre generado no coincide por accidente con clases
|
||||||
/// autosuficiente que funciona igual conviva con quien conviva en la misma página, sin coordinar
|
/// de terceros.
|
||||||
/// nombres de clase ni orden de carga de hojas de estilo con nadie.
|
///
|
||||||
|
/// [`AssetsOp::AddResponsiveStyle`]: crate::core::component::AssetsOp::AddResponsiveStyle
|
||||||
|
/// [`ResponsiveStyles`]: crate::html::ResponsiveStyles
|
||||||
///
|
///
|
||||||
/// # Ejemplo
|
/// # Ejemplo
|
||||||
///
|
///
|
||||||
|
|
@ -26,7 +42,7 @@ use crate::{AutoDefault, Getters, builder_impl};
|
||||||
///
|
///
|
||||||
/// let actions = Container::new()
|
/// let actions = Container::new()
|
||||||
/// .with_flex(
|
/// .with_flex(
|
||||||
/// Flex::row()
|
/// Flex::new()
|
||||||
/// .with_justify(flex::ContentJustify::End)
|
/// .with_justify(flex::ContentJustify::End)
|
||||||
/// .with_align(flex::Align::Center)
|
/// .with_align(flex::Align::Center)
|
||||||
/// .with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))),
|
/// .with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
|
|
@ -36,38 +52,66 @@ use crate::{AutoDefault, Getters, builder_impl};
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq, Getters)]
|
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq, Getters)]
|
||||||
pub struct Flex {
|
pub struct Flex {
|
||||||
/// Devuelve la dirección del eje principal.
|
// Determina si esta configuración debe aplicarse (y con qué variante de `display`) o si
|
||||||
|
// `Flex` no está en absoluto configurado. `None` es el estado real de ausencia: lo que tiene
|
||||||
|
// un contenedor que nunca ha llamado a `with_flex()`. Sin getter público; `new()`, `at()`,
|
||||||
|
// `inline()` e `inline_at()` son la única forma de activarlo.
|
||||||
|
#[getters(skip)]
|
||||||
|
display: Option<DisplayFlex>,
|
||||||
|
/// Devuelve la dirección del eje principal por punto de corte.
|
||||||
#[getters(copy)]
|
#[getters(copy)]
|
||||||
direction: Direction,
|
direction: Responsive<Direction>,
|
||||||
/// Devuelve el comportamiento cuando los elementos no caben en una sola línea.
|
/// Devuelve el comportamiento cuando los elementos no caben en una sola línea, por punto de
|
||||||
|
/// corte.
|
||||||
#[getters(copy)]
|
#[getters(copy)]
|
||||||
wrap: Behavior,
|
wrap: Responsive<Behavior>,
|
||||||
/// Devuelve la alineación de los elementos en el eje principal.
|
/// Devuelve la alineación de los elementos en el eje principal, por punto de corte.
|
||||||
#[getters(copy)]
|
#[getters(copy)]
|
||||||
justify: ContentJustify,
|
justify: Responsive<ContentJustify>,
|
||||||
/// Devuelve la alineación de los elementos en el eje transversal.
|
/// Devuelve la alineación de los elementos en el eje transversal, por punto de corte.
|
||||||
#[getters(copy)]
|
#[getters(copy)]
|
||||||
align: Align,
|
align: Responsive<Align>,
|
||||||
/// Devuelve la alineación de las líneas cuando hay más de una.
|
/// Devuelve la alineación de las líneas cuando hay más de una, por punto de corte.
|
||||||
#[getters(copy)]
|
#[getters(copy)]
|
||||||
align_content: AlignContent,
|
align_content: Responsive<AlignContent>,
|
||||||
/// Devuelve el espaciado entre elementos.
|
/// Devuelve el espaciado entre elementos, por punto de corte.
|
||||||
#[getters(copy)]
|
#[getters(copy)]
|
||||||
gap: Gap,
|
gap: Responsive<Gap>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[builder_impl]
|
#[builder_impl]
|
||||||
impl Flex {
|
impl Flex {
|
||||||
/// Crea una configuración Flex para disponer los elementos en fila (comportamiento por
|
/// Define una configuración Flex con `display: flex`, sin punto de corte: se aplica siempre.
|
||||||
/// defecto).
|
pub fn new() -> Self {
|
||||||
pub fn row() -> Self {
|
Self {
|
||||||
Self::default()
|
display: Some(DisplayFlex::Always),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea una configuración Flex para disponer los elementos en columna.
|
/// Define una configuración Flex con `display: flex` que se aplica a partir del punto de corte
|
||||||
pub fn column() -> Self {
|
/// indicado.
|
||||||
|
pub fn at(bp: Breakpoint) -> Self {
|
||||||
Self {
|
Self {
|
||||||
direction: Direction::Column,
|
display: Some(DisplayFlex::At(bp)),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Define una configuración Flex con `display: inline-flex`, sin punto de corte: se aplica
|
||||||
|
/// siempre.
|
||||||
|
pub fn inline() -> Self {
|
||||||
|
Self {
|
||||||
|
display: Some(DisplayFlex::AlwaysInline),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Define una configuración Flex con `display: inline-flex` que se aplica a partir del punto de
|
||||||
|
/// corte indicado.
|
||||||
|
pub fn inline_at(bp: Breakpoint) -> Self {
|
||||||
|
Self {
|
||||||
|
display: Some(DisplayFlex::InlineAt(bp)),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -75,43 +119,106 @@ impl Flex {
|
||||||
// **< Flex BUILDER >***************************************************************************
|
// **< Flex BUILDER >***************************************************************************
|
||||||
|
|
||||||
/// Establece la dirección del eje principal.
|
/// Establece la dirección del eje principal.
|
||||||
pub fn with_direction(mut self, direction: Direction) -> Self {
|
pub fn with_direction(mut self, dir: Direction) -> Self {
|
||||||
self.direction = direction;
|
self.direction = self.direction.set(dir);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Establece la dirección del eje principal a partir del punto de corte indicado.
|
||||||
|
pub fn with_direction_at(mut self, bp: Breakpoint, dir: Direction) -> Self {
|
||||||
|
self.direction = self.direction.set_at(bp, dir);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Establece el comportamiento cuando los elementos no caben en una sola línea.
|
/// Establece el comportamiento cuando los elementos no caben en una sola línea.
|
||||||
pub fn with_wrap(mut self, wrap: Behavior) -> Self {
|
pub fn with_wrap(mut self, wrap: Behavior) -> Self {
|
||||||
self.wrap = wrap;
|
self.wrap = self.wrap.set(wrap);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Establece el comportamiento cuando los elementos no caben en una sola línea, a partir del
|
||||||
|
/// punto de corte indicado.
|
||||||
|
pub fn with_wrap_at(mut self, bp: Breakpoint, wrap: Behavior) -> Self {
|
||||||
|
self.wrap = self.wrap.set_at(bp, wrap);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Establece la alineación de los elementos en el eje principal.
|
/// Establece la alineación de los elementos en el eje principal.
|
||||||
pub fn with_justify(mut self, justify: ContentJustify) -> Self {
|
pub fn with_justify(mut self, justify: ContentJustify) -> Self {
|
||||||
self.justify = justify;
|
self.justify = self.justify.set(justify);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Establece la alineación de los elementos en el eje principal, a partir del punto de corte
|
||||||
|
/// indicado.
|
||||||
|
pub fn with_justify_at(mut self, bp: Breakpoint, justify: ContentJustify) -> Self {
|
||||||
|
self.justify = self.justify.set_at(bp, justify);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Establece la alineación de los elementos en el eje transversal.
|
/// Establece la alineación de los elementos en el eje transversal.
|
||||||
pub fn with_align(mut self, align: Align) -> Self {
|
pub fn with_align(mut self, align: Align) -> Self {
|
||||||
self.align = align;
|
self.align = self.align.set(align);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Establece la alineación de los elementos en el eje transversal, a partir del punto de corte
|
||||||
|
/// indicado.
|
||||||
|
pub fn with_align_at(mut self, bp: Breakpoint, align: Align) -> Self {
|
||||||
|
self.align = self.align.set_at(bp, align);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Establece la alineación de las líneas cuando hay más de una (ver [`AlignContent`]).
|
/// Establece la alineación de las líneas cuando hay más de una (ver [`AlignContent`]).
|
||||||
pub fn with_align_content(mut self, align_content: AlignContent) -> Self {
|
pub fn with_align_content(mut self, align_content: AlignContent) -> Self {
|
||||||
self.align_content = align_content;
|
self.align_content = self.align_content.set(align_content);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Establece la alineación de las líneas cuando hay más de una (ver [`AlignContent`]), a partir
|
||||||
|
/// del punto de corte indicado.
|
||||||
|
pub fn with_align_content_at(mut self, bp: Breakpoint, align_content: AlignContent) -> Self {
|
||||||
|
self.align_content = self.align_content.set_at(bp, align_content);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Establece el espaciado entre elementos.
|
/// Establece el espaciado entre elementos.
|
||||||
pub fn with_gap(mut self, gap: Gap) -> Self {
|
pub fn with_gap(mut self, gap: Gap) -> Self {
|
||||||
self.gap = gap;
|
self.gap = self.gap.set(gap);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Establece el espaciado entre elementos, a partir del punto de corte indicado.
|
||||||
|
pub fn with_gap_at(mut self, bp: Breakpoint, gap: Gap) -> Self {
|
||||||
|
self.gap = self.gap.set_at(bp, gap);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Flex {
|
impl Flex {
|
||||||
|
/// Combina esta configuración con otra `Flex`, campo a campo, o la resetea a los valores por
|
||||||
|
/// defecto si se pasa `None`.
|
||||||
|
///
|
||||||
|
/// Cada campo de `flex` que tenga un valor sustituye al correspondiente de `self`; los que
|
||||||
|
/// estén a `None` dejan intacto el valor ya presente en `self`. Así, sucesivas llamadas pueden
|
||||||
|
/// ir completando o sobrescribiendo campos concretos sin necesidad de repetir los ya
|
||||||
|
/// establecidos. Es el método recomendado para que un contenedor propio adopte `Flex` de forma
|
||||||
|
/// incremental (ver [`Container::with_flex()`](crate::base::component::Container::with_flex)
|
||||||
|
/// como referencia de uso).
|
||||||
|
pub fn merge(mut self, flex: impl Into<Option<Flex>>) -> Self {
|
||||||
|
let Some(flex) = flex.into() else {
|
||||||
|
return Flex::default();
|
||||||
|
};
|
||||||
|
self.display = flex.display.or(self.display);
|
||||||
|
self.direction = self.direction.merge(flex.direction);
|
||||||
|
self.wrap = self.wrap.merge(flex.wrap);
|
||||||
|
self.justify = self.justify.merge(flex.justify);
|
||||||
|
self.align = self.align.merge(flex.align);
|
||||||
|
self.align_content = self.align_content.merge(flex.align_content);
|
||||||
|
self.gap = self.gap.merge(flex.gap);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Aplica esta configuración a un [`Props`] como declaraciones de estilo en línea.
|
/// Aplica esta configuración a un [`Props`] como declaraciones de estilo en línea.
|
||||||
///
|
///
|
||||||
/// Es el método recomendado para que un componente adopte `Flex`: concentra en un único sitio
|
/// Es el método recomendado para que un componente adopte `Flex`: concentra en un único sitio
|
||||||
|
|
@ -119,19 +226,53 @@ impl Flex {
|
||||||
/// use. Precedente: [`Container`](crate::base::component::Container) lo aplica sobre su
|
/// use. Precedente: [`Container`](crate::base::component::Container) lo aplica sobre su
|
||||||
/// propio `Props`; [`Navbar`](crate::base::component::Navbar), sobre el `Props` de su área de
|
/// propio `Props`; [`Navbar`](crate::base::component::Navbar), sobre el `Props` de su área de
|
||||||
/// contenido.
|
/// contenido.
|
||||||
pub fn apply_to(self, props: &mut Props) {
|
///
|
||||||
props.alter_prop(PropsOp::add_style("display", "flex"));
|
/// Las clases generadas se añaden a `classes`, separadas con un espacio de las que ya hubiera,
|
||||||
for (property, value) in [
|
/// para poder compartir un único acumulador con [`FlexItem::apply()`](super::FlexItem::apply)
|
||||||
("flex-direction", self.direction.value()),
|
/// sin cadenas intermedias.
|
||||||
("flex-wrap", self.wrap.value()),
|
#[rustfmt::skip]
|
||||||
("justify-content", self.justify.value()),
|
pub(crate) fn apply(self, cx: &mut Context, classes: &mut String) {
|
||||||
("align-items", self.align.value()),
|
// Sin `display` no hay contenedor Flex: el resto de facetas (`flex-direction`, `gap`...)
|
||||||
("align-content", self.align_content.value()),
|
// no tienen ningún efecto en CSS sin `display: flex`/`inline-flex`, así que ni se generan.
|
||||||
] {
|
let Some(display) = self.display else {
|
||||||
props.alter_prop(PropsOp::add_style(property, value));
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{apply, responsive_class, styles, value_to_token};
|
||||||
|
|
||||||
|
let (prefix, value) = match display {
|
||||||
|
DisplayFlex::Always
|
||||||
|
| DisplayFlex::At(_) => ("_flex_", "flex"),
|
||||||
|
DisplayFlex::AlwaysInline
|
||||||
|
| DisplayFlex::InlineAt(_) => ("_inline-flex_", "inline-flex"),
|
||||||
|
};
|
||||||
|
let entry = match display {
|
||||||
|
DisplayFlex::At(bp) | DisplayFlex::InlineAt(bp) => bp.resolved(cx),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let class = match entry {
|
||||||
|
None => prefix.into(),
|
||||||
|
Some(entry) => util::join!(prefix, entry.name, "_").into(),
|
||||||
|
};
|
||||||
|
styles(cx, classes, entry, class, "display", value.into());
|
||||||
|
|
||||||
|
apply!(cx, classes, self.direction, "_flex-direction_", "flex-direction");
|
||||||
|
apply!(cx, classes, self.wrap, "_flex-wrap_", "flex-wrap");
|
||||||
|
apply!(cx, classes, self.justify, "_flex-justify_", "justify-content");
|
||||||
|
apply!(cx, classes, self.align, "_flex-align-items_", "align-items");
|
||||||
|
apply!(cx, classes, self.align_content, "_flex-align-content_", "align-content");
|
||||||
|
for (bp, gap) in self.gap.by_breakpoint() {
|
||||||
|
let entry = bp.resolved(cx);
|
||||||
|
for (property, value) in gap.styles().into_iter().flatten() {
|
||||||
|
// El prefijo de `gap` no es literal (depende de la propiedad), así que se compone
|
||||||
|
// aquí en un único `join!` en vez de pasar por `responsive_class!`.
|
||||||
|
let token = value_to_token(&value);
|
||||||
|
let class = match entry {
|
||||||
|
None => util::join!("_flex-", property, "_", token, "_"),
|
||||||
|
Some(e) => util::join!("_flex-", property, "_", token, "_", e.name, "_"),
|
||||||
|
};
|
||||||
|
styles(cx, classes, entry, class.into(), property, value);
|
||||||
}
|
}
|
||||||
for (property, value) in self.gap.styles() {
|
|
||||||
props.alter_prop(PropsOp::add_style(property, value));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,15 @@
|
||||||
use crate::html::flex::props_item::{
|
use crate::core::component::Context;
|
||||||
ItemAlign, ItemGrow, ItemOffset, ItemOrder, ItemShrink, ItemSize,
|
use crate::core::theme::{Breakpoint, Responsive};
|
||||||
};
|
use crate::html::PropsOp;
|
||||||
use crate::html::props::{Props, PropsOp};
|
use crate::html::flex::{ItemAlign, ItemGrow, ItemOffset, ItemOrder, ItemShrink, ItemSize};
|
||||||
use crate::{AutoDefault, Getters, builder_impl};
|
use crate::{AutoDefault, Getters, builder_impl, util};
|
||||||
|
|
||||||
// **< FlexItem >***********************************************************************************
|
|
||||||
|
|
||||||
/// Configuración de un elemento como ítem de un contenedor Flexbox.
|
/// Configuración de un elemento como ítem de un contenedor Flexbox.
|
||||||
///
|
///
|
||||||
/// A diferencia de [`Flex`](crate::html::flex::Flex), que configura el comportamiento Flexbox
|
/// A diferencia de [`Flex`](crate::html::flex::Flex), que configura el comportamiento Flexbox
|
||||||
/// global de un contenedor y sus hijos como grupo, `FlexItem` configura un único elemento en
|
/// global de un contenedor y sus hijos como grupo, `FlexItem` configura un único elemento en
|
||||||
/// relación con el contenedor flex padre: crecimiento ([`ItemGrow`]), reducción ([`ItemShrink`]),
|
/// relación con el contenedor flex padre: crecimiento ([`ItemGrow`]), reducción ([`ItemShrink`]),
|
||||||
/// alineación individual ([`ItemAlign`]), orden visual ([`ItemOrder`]), ancho ([`ItemSize`]) y
|
/// alineación individual ([`ItemAlign`]), orden visual ([`ItemOrder`]), tamaño ([`ItemSize`]) y
|
||||||
/// desplazamiento ([`ItemOffset`]).
|
/// desplazamiento ([`ItemOffset`]).
|
||||||
///
|
///
|
||||||
/// No tiene un builder dedicado en ningún componente. De hecho, no tendría sentido porque cualquier
|
/// No tiene un builder dedicado en ningún componente. De hecho, no tendría sentido porque cualquier
|
||||||
|
|
@ -20,7 +18,8 @@ use crate::{AutoDefault, Getters, builder_impl};
|
||||||
/// exponer cualquier componente.
|
/// exponer cualquier componente.
|
||||||
///
|
///
|
||||||
/// Con [`ItemSize`] y [`ItemOffset`] se pueden modelar rejillas de columnas fijas sobre Flexbox,
|
/// Con [`ItemSize`] y [`ItemOffset`] se pueden modelar rejillas de columnas fijas sobre Flexbox,
|
||||||
/// combinando un ancho en fracción del contenedor con un desplazamiento lateral cuando se necesite.
|
/// combinando un tamaño en fracción del contenedor con un desplazamiento lateral cuando se
|
||||||
|
/// necesite.
|
||||||
///
|
///
|
||||||
/// # Ejemplo
|
/// # Ejemplo
|
||||||
///
|
///
|
||||||
|
|
@ -28,7 +27,7 @@ use crate::{AutoDefault, Getters, builder_impl};
|
||||||
/// use pagetop::prelude::*;
|
/// use pagetop::prelude::*;
|
||||||
///
|
///
|
||||||
/// // Crece para ocupar el espacio sobrante, partiendo de ancho cero.
|
/// // Crece para ocupar el espacio sobrante, partiendo de ancho cero.
|
||||||
/// let title = Button::plain(Lc::n("Panel")).with_prop(PropsOp::flex_item(
|
/// let panel = Button::plain(Lc::n("Panel")).with_prop(PropsOp::flex_item(
|
||||||
/// FlexItem::new()
|
/// FlexItem::new()
|
||||||
/// .with_grow(flex::ItemGrow::Is1)
|
/// .with_grow(flex::ItemGrow::Is1)
|
||||||
/// .with_size(flex::ItemSize::Custom(UnitValue::Zero)),
|
/// .with_size(flex::ItemSize::Custom(UnitValue::Zero)),
|
||||||
|
|
@ -43,24 +42,24 @@ use crate::{AutoDefault, Getters, builder_impl};
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq, Getters)]
|
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq, Getters)]
|
||||||
pub struct FlexItem {
|
pub struct FlexItem {
|
||||||
/// Devuelve el factor de crecimiento.
|
/// Devuelve el factor de crecimiento, por punto de corte.
|
||||||
#[getters(copy)]
|
#[getters(copy)]
|
||||||
grow: ItemGrow,
|
grow: Responsive<ItemGrow>,
|
||||||
/// Devuelve el factor de reducción.
|
/// Devuelve el factor de reducción, por punto de corte.
|
||||||
#[getters(copy)]
|
#[getters(copy)]
|
||||||
shrink: ItemShrink,
|
shrink: Responsive<ItemShrink>,
|
||||||
/// Devuelve la alineación individual en el eje transversal.
|
/// Devuelve la alineación individual en el eje transversal, por punto de corte.
|
||||||
#[getters(copy)]
|
#[getters(copy)]
|
||||||
align_self: ItemAlign,
|
align_self: Responsive<ItemAlign>,
|
||||||
/// Devuelve la posición en el orden visual.
|
/// Devuelve la posición en el orden visual, por punto de corte.
|
||||||
#[getters(copy)]
|
#[getters(copy)]
|
||||||
order: ItemOrder,
|
order: Responsive<ItemOrder>,
|
||||||
/// Devuelve el ancho como fracción del contenedor.
|
/// Devuelve el tamaño como fracción del contenedor, por punto de corte.
|
||||||
#[getters(copy)]
|
#[getters(copy)]
|
||||||
size: ItemSize,
|
size: Responsive<ItemSize>,
|
||||||
/// Devuelve el desplazamiento respecto al inicio del contenedor.
|
/// Devuelve el desplazamiento respecto al inicio del contenedor, por punto de corte.
|
||||||
#[getters(copy)]
|
#[getters(copy)]
|
||||||
offset: ItemOffset,
|
offset: Responsive<ItemOffset>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[builder_impl]
|
#[builder_impl]
|
||||||
|
|
@ -70,80 +69,19 @@ impl FlexItem {
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
// **< FlexItem BUILDER >***********************************************************************
|
/// Crea una configuración de ítem que empuja el elemento, y los que le sigan, hacia el extremo
|
||||||
|
/// final de un contenedor flex en fila.
|
||||||
/// Establece el factor de crecimiento.
|
|
||||||
pub fn with_grow(mut self, grow: ItemGrow) -> Self {
|
|
||||||
self.grow = grow;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establece el factor de reducción.
|
|
||||||
pub fn with_shrink(mut self, shrink: ItemShrink) -> Self {
|
|
||||||
self.shrink = shrink;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establece la alineación individual en el eje transversal.
|
|
||||||
pub fn with_align_self(mut self, align_self: ItemAlign) -> Self {
|
|
||||||
self.align_self = align_self;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establece la posición en el orden visual.
|
|
||||||
pub fn with_order(mut self, order: ItemOrder) -> Self {
|
|
||||||
self.order = order;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establece el ancho como una fracción del contenedor (`flex-basis`). No fuerza
|
|
||||||
/// [`ItemShrink::Is0`](super::ItemShrink::Is0) por sí solo (consulta la documentación de
|
|
||||||
/// [`ItemSize`] antes de combinarlo con [`with_shrink()`](Self::with_shrink) porque con un
|
|
||||||
/// tamaño en porcentaje, forzar `ItemShrink::Is0` sólo es seguro si el contenedor no tiene
|
|
||||||
/// [`Gap`](super::Gap)).
|
|
||||||
pub fn with_size(mut self, size: ItemSize) -> Self {
|
|
||||||
self.size = size;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establece el desplazamiento respecto al inicio del contenedor (`margin-inline-start`). No
|
|
||||||
/// tiene relación con [`push_end()`](Self::push_end) aunque aplican la misma propiedad CSS para
|
|
||||||
/// casos de uso distintos.
|
|
||||||
pub fn with_offset(mut self, offset: ItemOffset) -> Self {
|
|
||||||
self.offset = offset;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FlexItem {
|
|
||||||
// Aplica esta configuración a un Props como declaraciones de estilo en línea.
|
|
||||||
pub(crate) fn apply_to(self, props: &mut Props) {
|
|
||||||
for (property, value) in [
|
|
||||||
("flex-grow", self.grow.value()),
|
|
||||||
("flex-shrink", self.shrink.value()),
|
|
||||||
("align-self", self.align_self.value()),
|
|
||||||
("order", self.order.value()),
|
|
||||||
("flex-basis", self.size.value()),
|
|
||||||
("margin-inline-start", self.offset.value()),
|
|
||||||
] {
|
|
||||||
props.alter_prop(PropsOp::add_style(property, value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Separa un elemento (y los que le sigan en el mismo eje principal) del resto, empujándolo
|
|
||||||
/// hacia el extremo final de un contenedor flex.
|
|
||||||
///
|
///
|
||||||
/// Se resuelve siempre como margen inicial automático (`margin-inline-start: auto`) en línea,
|
/// Aplica `margin-inline-start: auto`, un margen automático que absorbe todo el espacio libre
|
||||||
/// igual que el resto de facetas de `FlexItem`. Es el mecanismo estándar de Flexbox para, por
|
/// que quede antes del elemento en el eje de escritura. Con la dirección por defecto
|
||||||
/// ejemplo, separar dos menús dentro de una misma [`Navbar`](crate::base::component::Navbar)
|
/// ([`Direction::Row`](super::Direction::Row)) ese eje es el principal, de ahí el efecto de
|
||||||
/// -- uno pegado al inicio, el siguiente empujado al final -- sin que el contenedor necesite
|
/// empuje. En un contenedor en columna, en cambio, ese eje es el transversal: el margen ya no
|
||||||
/// conocer ninguna distinción entre sus elementos.
|
/// empuja nada, sólo desplaza ese elemento hacia el final de la línea (a la derecha si se
|
||||||
|
/// escribe de izquierda a derecha).
|
||||||
///
|
///
|
||||||
/// No forma parte de los campos de `FlexItem` (no se combina con `grow`/`shrink`/`align_self`/
|
/// Es el mecanismo estándar de Flexbox para, por ejemplo, separar dos menús dentro de un mismo
|
||||||
/// `order`/`size`/`offset` en una misma llamada): es una función asociada independiente porque
|
/// [`Navbar`](crate::base::component::Navbar) (uno pegado al inicio, el siguiente empujado al
|
||||||
/// resuelve un caso de uso completo por sí sola, con una sola línea, y vive aquí -- en vez de
|
/// final) sin que el contenedor necesite conocer ninguna distinción entre sus elementos.
|
||||||
/// como función suelta del módulo `flex` -- para dejar claro que es una operación de **ítem**,
|
|
||||||
/// no de contenedor.
|
|
||||||
///
|
///
|
||||||
/// # Ejemplo
|
/// # Ejemplo
|
||||||
///
|
///
|
||||||
|
|
@ -151,12 +89,139 @@ impl FlexItem {
|
||||||
/// use pagetop::prelude::*;
|
/// use pagetop::prelude::*;
|
||||||
///
|
///
|
||||||
/// let user_menu = Nav::new()
|
/// let user_menu = Nav::new()
|
||||||
/// .with_prop(FlexItem::push_end())
|
/// .with_prop(FlexItem::push_end().into())
|
||||||
/// .with_item(nav::Item::link(Lc::n("Profile"), "/profile"))
|
/// .with_item(nav::Item::link(Lc::n("Profile"), "/profile"))
|
||||||
/// .with_item(nav::Item::link(Lc::n("Sign out"), "/sign-out"));
|
/// .with_item(nav::Item::link(Lc::n("Sign out"), "/sign-out"));
|
||||||
/// ```
|
/// ```
|
||||||
pub fn push_end() -> PropsOp {
|
pub fn push_end() -> Self {
|
||||||
PropsOp::add_style("margin-inline-start", "auto")
|
Self::new().with_offset(ItemOffset::Auto)
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< FlexItem BUILDER >***********************************************************************
|
||||||
|
|
||||||
|
/// Establece el factor de crecimiento.
|
||||||
|
pub fn with_grow(mut self, grow: ItemGrow) -> Self {
|
||||||
|
self.grow = self.grow.set(grow);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Establece el factor de crecimiento, a partir del punto de corte indicado.
|
||||||
|
pub fn with_grow_at(mut self, bp: Breakpoint, grow: ItemGrow) -> Self {
|
||||||
|
self.grow = self.grow.set_at(bp, grow);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Establece el factor de reducción.
|
||||||
|
pub fn with_shrink(mut self, shrink: ItemShrink) -> Self {
|
||||||
|
self.shrink = self.shrink.set(shrink);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Establece el factor de reducción, a partir del punto de corte indicado.
|
||||||
|
pub fn with_shrink_at(mut self, bp: Breakpoint, shrink: ItemShrink) -> Self {
|
||||||
|
self.shrink = self.shrink.set_at(bp, shrink);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Establece la alineación individual en el eje transversal.
|
||||||
|
pub fn with_align_self(mut self, align_self: ItemAlign) -> Self {
|
||||||
|
self.align_self = self.align_self.set(align_self);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Establece la alineación individual en el eje transversal, a partir del punto de corte
|
||||||
|
/// indicado.
|
||||||
|
pub fn with_align_self_at(mut self, bp: Breakpoint, align_self: ItemAlign) -> Self {
|
||||||
|
self.align_self = self.align_self.set_at(bp, align_self);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Establece la posición en el orden visual.
|
||||||
|
pub fn with_order(mut self, order: ItemOrder) -> Self {
|
||||||
|
self.order = self.order.set(order);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Establece la posición en el orden visual, a partir del punto de corte indicado.
|
||||||
|
pub fn with_order_at(mut self, bp: Breakpoint, order: ItemOrder) -> Self {
|
||||||
|
self.order = self.order.set_at(bp, order);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Establece el tamaño como una fracción del contenedor (`flex-basis`). No fuerza
|
||||||
|
/// [`ItemShrink::Is0`](super::ItemShrink::Is0) por sí solo (consulta la documentación de
|
||||||
|
/// [`ItemSize`] antes de combinarlo con [`with_shrink()`](Self::with_shrink) porque con un
|
||||||
|
/// tamaño en porcentaje, forzar `ItemShrink::Is0` sólo es seguro si el contenedor no tiene
|
||||||
|
/// [`Gap`](super::Gap)).
|
||||||
|
pub fn with_size(mut self, size: ItemSize) -> Self {
|
||||||
|
self.size = self.size.set(size);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Establece el tamaño como una fracción del contenedor (`flex-basis`), a partir del punto de
|
||||||
|
/// corte indicado.
|
||||||
|
pub fn with_size_at(mut self, bp: Breakpoint, size: ItemSize) -> Self {
|
||||||
|
self.size = self.size.set_at(bp, size);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Establece el desplazamiento respecto al inicio del contenedor (`margin-inline-start`).
|
||||||
|
/// [`push_end()`](Self::push_end) fija este mismo campo a [`ItemOffset::Auto`]; combinar los
|
||||||
|
/// dos deja el que se aplique en último lugar.
|
||||||
|
pub fn with_offset(mut self, offset: ItemOffset) -> Self {
|
||||||
|
self.offset = self.offset.set(offset);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Establece el desplazamiento respecto al inicio del contenedor (`margin-inline-start`), a
|
||||||
|
/// partir del punto de corte indicado.
|
||||||
|
pub fn with_offset_at(mut self, bp: Breakpoint, offset: ItemOffset) -> Self {
|
||||||
|
self.offset = self.offset.set_at(bp, offset);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FlexItem {
|
||||||
|
/// Combina esta configuración con otra `FlexItem`, campo a campo.
|
||||||
|
///
|
||||||
|
/// La fusión llega al nivel de cada punto de corte; donde `item` tenga un valor establecido,
|
||||||
|
/// sustituye al de `self` y donde no lo tenga, se conserva el que ya hubiera. Por eso un `item`
|
||||||
|
/// que sólo establezca `with_size_at(Breakpoint::Lg, ...)` no borra el `with_size()` base que
|
||||||
|
/// `self` ya tuviera, sólo sustituye la entrada de ese punto de corte.
|
||||||
|
///
|
||||||
|
/// Es el método que usa [`Props::with_prop()`](crate::html::props::Props::with_prop) para que
|
||||||
|
/// sucesivas [`PropsOp::FlexItem`](crate::html::props::PropsOp::FlexItem) sobre el mismo
|
||||||
|
/// componente vayan completando campos concretos sin repetir los ya establecidos, en vez de
|
||||||
|
/// partir de cero en cada llamada.
|
||||||
|
pub fn merge(mut self, item: FlexItem) -> Self {
|
||||||
|
self.grow = self.grow.merge(item.grow);
|
||||||
|
self.shrink = self.shrink.merge(item.shrink);
|
||||||
|
self.align_self = self.align_self.merge(item.align_self);
|
||||||
|
self.order = self.order.merge(item.order);
|
||||||
|
self.size = self.size.merge(item.size);
|
||||||
|
self.offset = self.offset.merge(item.offset);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Aplica esta configuración como clases de utilidad responsive en el [`Context`], igual que
|
||||||
|
/// [`Flex::apply()`](super::Flex::apply): cada faceta con valor añade una declaración de
|
||||||
|
/// estilo (por punto de corte, si se ha establecido alguno) y su propia clase. Un campo sin
|
||||||
|
/// ningún valor establecido, o con un valor cuya variante es la "por defecto" del propio enum
|
||||||
|
/// (p. ej. `ItemGrow::Default`), no añade nada.
|
||||||
|
///
|
||||||
|
/// Las clases generadas se añaden a `classes`, separadas con un espacio de las que ya hubiera,
|
||||||
|
/// para poder compartir un único acumulador con [`Flex::apply()`](super::Flex::apply) sin
|
||||||
|
/// cadenas intermedias.
|
||||||
|
#[rustfmt::skip]
|
||||||
|
pub(crate) fn apply(self, cx: &mut Context, classes: &mut String) {
|
||||||
|
use super::{apply, responsive_class, styles, value_to_token};
|
||||||
|
|
||||||
|
apply!(cx, classes, self.grow, "_flex-item-grow_", "flex-grow");
|
||||||
|
apply!(cx, classes, self.shrink, "_flex-item-shrink_", "flex-shrink");
|
||||||
|
apply!(cx, classes, self.align_self, "_flex-item-align_", "align-self");
|
||||||
|
apply!(cx, classes, self.order, "_flex-item-order_", "order");
|
||||||
|
apply!(cx, classes, self.size, "_flex-item-basis_", "flex-basis", val);
|
||||||
|
apply!(cx, classes, self.offset, "_flex-item-offset_", "margin-inline-start", val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -207,28 +207,21 @@ pub enum Gap {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Gap {
|
impl Gap {
|
||||||
// Declaraciones de estilo (propiedad, valor) para este espaciado; vacío si no hay ninguna
|
// Declaraciones de estilo (propiedad, valor) para este espaciado; cada hueco a `None` si no hay
|
||||||
// medible (`UnitValue::None`/`UnitValue::Auto` no producen ningún estilo).
|
// ninguna medible (`UnitValue::None`/`UnitValue::Auto` no producen ningún estilo).
|
||||||
pub(super) fn styles(self) -> Vec<(&'static str, CowStr)> {
|
pub(super) fn styles(self) -> [Option<(&'static str, CowStr)>; 2] {
|
||||||
match self {
|
match self {
|
||||||
Self::None => Vec::new(),
|
Self::None => [None, None],
|
||||||
Self::Both(value) => {
|
Self::Both(value) => [Self::style("gap", value), None],
|
||||||
if value.is_measurable() {
|
Self::Distinct { row, column } => [
|
||||||
vec![("gap", value.into())]
|
Self::style("row-gap", row),
|
||||||
} else {
|
Self::style("column-gap", column),
|
||||||
Vec::new()
|
],
|
||||||
}
|
|
||||||
}
|
|
||||||
Self::Distinct { row, column } => {
|
|
||||||
let mut styles = Vec::new();
|
|
||||||
if row.is_measurable() {
|
|
||||||
styles.push(("row-gap", row.into()));
|
|
||||||
}
|
|
||||||
if column.is_measurable() {
|
|
||||||
styles.push(("column-gap", column.into()));
|
|
||||||
}
|
|
||||||
styles
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Declaración (propiedad, valor) para un valor medible, o `None` si no lo es.
|
||||||
|
fn style(property: &'static str, value: UnitValue) -> Option<(&'static str, CowStr)> {
|
||||||
|
value.is_measurable().then(|| (property, value.into()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,10 @@ pub enum ItemOffset {
|
||||||
/// Por defecto, sin desplazamiento (`margin-inline-start: 0` no explícito).
|
/// Por defecto, sin desplazamiento (`margin-inline-start: 0` no explícito).
|
||||||
#[default]
|
#[default]
|
||||||
None,
|
None,
|
||||||
|
/// Empuja el ítem, y los que le sigan, hacia el extremo final de un contenedor en fila
|
||||||
|
/// (`margin-inline-start: auto`). Ver [`FlexItem::push_end()`](super::FlexItem::push_end), que
|
||||||
|
/// detalla el comportamiento cuando el contenedor está en columna.
|
||||||
|
Auto,
|
||||||
/// Se desplaza el 10% del ancho del contenedor (`margin-inline-start: 10%`).
|
/// Se desplaza el 10% del ancho del contenedor (`margin-inline-start: 10%`).
|
||||||
Percent10,
|
Percent10,
|
||||||
/// Se desplaza el 20% del ancho del contenedor (`margin-inline-start: 20%`).
|
/// Se desplaza el 20% del ancho del contenedor (`margin-inline-start: 20%`).
|
||||||
|
|
@ -106,6 +110,7 @@ impl ItemOffset {
|
||||||
pub(super) fn value(self) -> CowStr {
|
pub(super) fn value(self) -> CowStr {
|
||||||
match self {
|
match self {
|
||||||
Self::None => "".into(),
|
Self::None => "".into(),
|
||||||
|
Self::Auto => "auto".into(),
|
||||||
Self::Percent10 => "10%".into(),
|
Self::Percent10 => "10%".into(),
|
||||||
Self::Percent20 => "20%".into(),
|
Self::Percent20 => "20%".into(),
|
||||||
Self::Percent25 => "25%".into(),
|
Self::Percent25 => "25%".into(),
|
||||||
|
|
@ -217,10 +222,14 @@ impl ItemShrink {
|
||||||
|
|
||||||
// **< ItemSize >***********************************************************************************
|
// **< ItemSize >***********************************************************************************
|
||||||
|
|
||||||
/// Ancho en [`FlexItem`](super::FlexItem) para un ítem como fracción del contenedor.
|
/// Tamaño en [`FlexItem`](super::FlexItem) para un ítem como fracción del contenedor.
|
||||||
|
///
|
||||||
|
/// Dimensiona el eje principal (`flex-basis`). Con la dirección por defecto
|
||||||
|
/// ([`Direction::Row`](super::Direction::Row)) fija el ancho, y en un contenedor en columna fija el
|
||||||
|
/// alto. El resto de esta documentación describe el caso en fila, que es el habitual.
|
||||||
///
|
///
|
||||||
/// Permite maquetar rejillas de columnas fijas. Un ítem con [`ItemSize::Percent33`] ocupa un tercio
|
/// Permite maquetar rejillas de columnas fijas. Un ítem con [`ItemSize::Percent33`] ocupa un tercio
|
||||||
/// del ancho del contenedor con independencia de su contenido.
|
/// del contenedor con independencia de su contenido.
|
||||||
///
|
///
|
||||||
/// # Cómo combinarlo con `Gap`
|
/// # Cómo combinarlo con `Gap`
|
||||||
///
|
///
|
||||||
|
|
@ -253,31 +262,31 @@ pub enum ItemSize {
|
||||||
/// Por defecto, el tamaño se calcula según el contenido (`flex-basis: auto` no explícito).
|
/// Por defecto, el tamaño se calcula según el contenido (`flex-basis: auto` no explícito).
|
||||||
#[default]
|
#[default]
|
||||||
Default,
|
Default,
|
||||||
/// Ocupa el 10% del ancho del contenedor (`flex-basis: 10%`).
|
/// Ocupa el 10% del contenedor (`flex-basis: 10%`).
|
||||||
Percent10,
|
Percent10,
|
||||||
/// Ocupa el 20% del ancho del contenedor (`flex-basis: 20%`).
|
/// Ocupa el 20% del contenedor (`flex-basis: 20%`).
|
||||||
Percent20,
|
Percent20,
|
||||||
/// Ocupa el 25% del ancho del contenedor (`flex-basis: 25%`).
|
/// Ocupa el 25% del contenedor (`flex-basis: 25%`).
|
||||||
Percent25,
|
Percent25,
|
||||||
/// Ocupa un tercio del ancho del contenedor (`flex-basis: 33.3333%`).
|
/// Ocupa un tercio del contenedor (`flex-basis: 33.3333%`).
|
||||||
Percent33,
|
Percent33,
|
||||||
/// Ocupa el 40% del ancho del contenedor (`flex-basis: 40%`).
|
/// Ocupa el 40% del contenedor (`flex-basis: 40%`).
|
||||||
Percent40,
|
Percent40,
|
||||||
/// Ocupa la mitad del ancho del contenedor (`flex-basis: 50%`).
|
/// Ocupa la mitad del contenedor (`flex-basis: 50%`).
|
||||||
Percent50,
|
Percent50,
|
||||||
/// Ocupa el 60% del ancho del contenedor (`flex-basis: 60%`).
|
/// Ocupa el 60% del contenedor (`flex-basis: 60%`).
|
||||||
Percent60,
|
Percent60,
|
||||||
/// Ocupa dos tercios del ancho del contenedor (`flex-basis: 66.6667%`).
|
/// Ocupa dos tercios del contenedor (`flex-basis: 66.6667%`).
|
||||||
Percent66,
|
Percent66,
|
||||||
/// Ocupa el 75% del ancho del contenedor (`flex-basis: 75%`).
|
/// Ocupa el 75% del contenedor (`flex-basis: 75%`).
|
||||||
Percent75,
|
Percent75,
|
||||||
/// Ocupa el 80% del ancho del contenedor (`flex-basis: 80%`).
|
/// Ocupa el 80% del contenedor (`flex-basis: 80%`).
|
||||||
Percent80,
|
Percent80,
|
||||||
/// Ocupa el 90% del ancho del contenedor (`flex-basis: 90%`).
|
/// Ocupa el 90% del contenedor (`flex-basis: 90%`).
|
||||||
Percent90,
|
Percent90,
|
||||||
/// Ocupa el 100% del ancho del contenedor (`flex-basis: 100%`).
|
/// Ocupa el 100% del contenedor (`flex-basis: 100%`).
|
||||||
Percent100,
|
Percent100,
|
||||||
/// Cualquier otro valor, incluidas unidades absolutas (p. ej. un ancho fijo en píxeles).
|
/// Cualquier otro valor, incluidas unidades absolutas (p. ej. un tamaño fijo en píxeles).
|
||||||
Custom(UnitValue),
|
Custom(UnitValue),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::core::TypeInfo;
|
use crate::core::TypeInfo;
|
||||||
use crate::core::component::Context;
|
use crate::core::component::Context;
|
||||||
|
use crate::html::flex::{Flex, FlexItem};
|
||||||
use crate::html::maud::{Escaper, RenderAttrs};
|
use crate::html::maud::{Escaper, RenderAttrs};
|
||||||
use crate::html::props::{PropsError, PropsExtra, PropsOp};
|
use crate::html::props::{PropsError, PropsExtra, PropsOp};
|
||||||
use crate::{AutoDefault, CowStr, builder_impl, trace, util};
|
use crate::{AutoDefault, CowStr, builder_impl, trace, util};
|
||||||
|
|
@ -188,6 +189,7 @@ pub struct Props {
|
||||||
styles: Vec<(CowStr, CowStr)>,
|
styles: Vec<(CowStr, CowStr)>,
|
||||||
attrs: Vec<(CowStr, CowStr)>,
|
attrs: Vec<(CowStr, CowStr)>,
|
||||||
extras: HashMap<&'static str, PropsExtra>,
|
extras: HashMap<&'static str, PropsExtra>,
|
||||||
|
flex_item: FlexItem,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[builder_impl]
|
#[builder_impl]
|
||||||
|
|
@ -335,7 +337,7 @@ impl Props {
|
||||||
self.extras.remove(key);
|
self.extras.remove(key);
|
||||||
}
|
}
|
||||||
PropsOp::FlexItem(placement) => {
|
PropsOp::FlexItem(placement) => {
|
||||||
placement.apply_to(self);
|
self.flex_item = self.flex_item.merge(placement);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self
|
self
|
||||||
|
|
@ -578,9 +580,11 @@ impl Props {
|
||||||
/// [`FlexItem`]: crate::html::flex::FlexItem
|
/// [`FlexItem`]: crate::html::flex::FlexItem
|
||||||
/// [`unpack_with_flex()`]: Self::unpack_with_flex
|
/// [`unpack_with_flex()`]: Self::unpack_with_flex
|
||||||
pub fn unpack<'a>(&'a self, cx: &mut Context) -> impl RenderAttrs + 'a {
|
pub fn unpack<'a>(&'a self, cx: &mut Context) -> impl RenderAttrs + 'a {
|
||||||
|
let mut classes = String::new();
|
||||||
|
self.flex_item.apply(cx, &mut classes);
|
||||||
PropsUnpack {
|
PropsUnpack {
|
||||||
props: self,
|
props: self,
|
||||||
classes: self.flex_item.apply(cx),
|
classes,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -597,9 +601,12 @@ impl Props {
|
||||||
/// [`FlexItem`]: crate::html::flex::FlexItem
|
/// [`FlexItem`]: crate::html::flex::FlexItem
|
||||||
/// [`PropsOp::FlexItem`]: crate::html::props::PropsOp::FlexItem
|
/// [`PropsOp::FlexItem`]: crate::html::props::PropsOp::FlexItem
|
||||||
pub fn unpack_with_flex<'a>(&'a self, cx: &mut Context, flex: Flex) -> impl RenderAttrs + 'a {
|
pub fn unpack_with_flex<'a>(&'a self, cx: &mut Context, flex: Flex) -> impl RenderAttrs + 'a {
|
||||||
|
let mut classes = String::new();
|
||||||
|
flex.apply(cx, &mut classes);
|
||||||
|
self.flex_item.apply(cx, &mut classes);
|
||||||
PropsUnpack {
|
PropsUnpack {
|
||||||
props: self,
|
props: self,
|
||||||
classes: util::join_pair!(flex.apply(cx), " ", self.flex_item.apply(cx)),
|
classes,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -143,3 +143,75 @@ async fn gap_none_adds_no_gap_style() {
|
||||||
assert!(!html.contains("gap"));
|
assert!(!html.contains("gap"));
|
||||||
assert!(!assets.contains("gap"));
|
assert!(!assets.contains("gap"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn inline_flex_uses_its_own_display_value() {
|
||||||
|
let mut cx = Context::default();
|
||||||
|
let mut container = Container::new()
|
||||||
|
.with_flex(Flex::inline())
|
||||||
|
.with_child(Lc::n("x"));
|
||||||
|
let html = container.render(&mut cx).await.into_string();
|
||||||
|
let assets = cx.render_assets().into_string();
|
||||||
|
|
||||||
|
assert!(html.contains(r#"class="_inline-flex_""#));
|
||||||
|
assert!(assets.contains("_inline-flex_{display:inline-flex}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn inline_flex_at_a_breakpoint_adds_its_suffix() {
|
||||||
|
let mut cx = Context::default();
|
||||||
|
let mut container = Container::new()
|
||||||
|
.with_flex(Flex::inline_at(Breakpoint::Lg))
|
||||||
|
.with_child(Lc::n("x"));
|
||||||
|
let html = container.render(&mut cx).await.into_string();
|
||||||
|
let assets = cx.render_assets().into_string();
|
||||||
|
|
||||||
|
assert!(html.contains(r#"class="_inline-flex_lg_""#));
|
||||||
|
assert!(assets.contains("@media(min-width:992px){._inline-flex_lg_{display:inline-flex}}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn flex_and_flex_item_classes_keep_their_order() {
|
||||||
|
let mut cx = Context::default();
|
||||||
|
let mut container = Container::new()
|
||||||
|
.with_prop(PropsOp::add_classes("own"))
|
||||||
|
.with_flex(Flex::new().with_direction(flex::Direction::Column))
|
||||||
|
.with_prop(PropsOp::flex_item(
|
||||||
|
FlexItem::new().with_grow(flex::ItemGrow::Is1),
|
||||||
|
))
|
||||||
|
.with_child(Lc::n("x"));
|
||||||
|
let html = container.render(&mut cx).await.into_string();
|
||||||
|
|
||||||
|
// The component's own classes come first, then those of `Flex`, then those of `FlexItem`,
|
||||||
|
// which share a single accumulator in `Props::unpack_with_flex()`.
|
||||||
|
assert!(html.contains(r#"class="own _flex_ _flex-direction_column_ _flex-item-grow_1_""#));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn flex_without_display_adds_no_class_of_its_own() {
|
||||||
|
let mut cx = Context::default();
|
||||||
|
let mut container = Container::new()
|
||||||
|
.with_prop(PropsOp::add_classes("own"))
|
||||||
|
.with_flex(Flex::default())
|
||||||
|
.with_child(Lc::n("x"));
|
||||||
|
let html = container.render(&mut cx).await.into_string();
|
||||||
|
|
||||||
|
assert!(html.contains(r#"class="own""#));
|
||||||
|
assert!(cx.render_assets().into_string().is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn breakpoints_add_their_suffix_and_wrap_rules_in_media_queries() {
|
||||||
|
let mut cx = Context::default();
|
||||||
|
let mut container = Container::new()
|
||||||
|
.with_flex(
|
||||||
|
Flex::at(Breakpoint::Md).with_direction_at(Breakpoint::Lg, flex::Direction::Column),
|
||||||
|
)
|
||||||
|
.with_child(Lc::n("x"));
|
||||||
|
let html = container.render(&mut cx).await.into_string();
|
||||||
|
let assets = cx.render_assets().into_string();
|
||||||
|
|
||||||
|
assert!(html.contains(r#"class="_flex_md_ _flex-direction_column_lg_""#));
|
||||||
|
assert!(assets.contains("@media(min-width:768px){._flex_md_{display:flex}}"));
|
||||||
|
assert!(assets.contains("@media(min-width:992px){._flex-direction_column_lg_"));
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -143,6 +143,39 @@ async fn combines_several_facets_in_one_call() {
|
||||||
assert!(assets.contains("_flex-item-offset_10pct_{margin-inline-start:10%}"));
|
assert!(assets.contains("_flex-item-offset_10pct_{margin-inline-start:10%}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn size_at_a_breakpoint_combines_token_and_suffix() {
|
||||||
|
let mut cx = Context::default();
|
||||||
|
let props = Props::default().with_prop(PropsOp::flex_item(
|
||||||
|
FlexItem::new().with_size_at(Breakpoint::Md, flex::ItemSize::Percent33),
|
||||||
|
));
|
||||||
|
let html = html! { span (props.unpack(&mut cx)) {} }.into_string();
|
||||||
|
let assets = cx.render_assets().into_string();
|
||||||
|
|
||||||
|
// Both the sanitized value (`33.3333%` -> `33_3333pct`) and the breakpoint suffix are part of
|
||||||
|
// the same class name, and the rule is wrapped in its media query.
|
||||||
|
assert!(html.contains(r#"class="_flex-item-basis_33_3333pct_md_""#));
|
||||||
|
assert!(assets.contains(
|
||||||
|
"@media(min-width:768px){._flex-item-basis_33_3333pct_md_{flex-basis:33.3333%}}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn base_and_breakpoint_values_generate_one_class_each() {
|
||||||
|
let mut cx = Context::default();
|
||||||
|
let props = Props::default().with_prop(PropsOp::flex_item(
|
||||||
|
FlexItem::new()
|
||||||
|
.with_grow(flex::ItemGrow::Default)
|
||||||
|
.with_size(flex::ItemSize::Percent50)
|
||||||
|
.with_size_at(Breakpoint::Lg, flex::ItemSize::Percent25),
|
||||||
|
));
|
||||||
|
let html = html! { span (props.unpack(&mut cx)) {} }.into_string();
|
||||||
|
|
||||||
|
// `ItemGrow::Default` resolves to an empty CSS value, so it adds no class at all.
|
||||||
|
assert!(html.contains(r#"class="_flex-item-basis_50pct_ _flex-item-basis_25pct_lg_""#));
|
||||||
|
assert!(!html.contains("_flex-item-grow_"));
|
||||||
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn from_flex_item_for_props_op() {
|
async fn from_flex_item_for_props_op() {
|
||||||
let mut cx = Context::default();
|
let mut cx = Context::default();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue