✨ (pagetop): Añade Flex/FlexItem para usar Flexbox
Container y Navbar lo adoptan vía `with_flex()`/`PropsOp::flex_item()`. Y se elimina `ButtonSet` porque su funcionalidad queda cubierta por este mecanismo más general. Incluye el ejemplo `examples/intro-flex.rs` con los patrones de uso.
This commit is contained in:
parent
4e4fdf7b10
commit
17e16652e4
26 changed files with 2023 additions and 193 deletions
|
|
@ -140,17 +140,22 @@ async fn form_controls(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
.with_child(form::Hidden::field("origin", "form-selections"))
|
.with_child(form::Hidden::field("origin", "form-selections"))
|
||||||
// Botonera de acciones.
|
// Botonera de acciones.
|
||||||
.with_child(
|
.with_child(
|
||||||
button::ButtonSet::new()
|
Container::new()
|
||||||
.with_button(
|
.with_flex(
|
||||||
|
Flex::row()
|
||||||
|
.with_wrap(flex::Behavior::Wrap)
|
||||||
|
.with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
Button::submit(Lc::t("btn_submit", &LOC))
|
Button::submit(Lc::t("btn_submit", &LOC))
|
||||||
.with_style(button::Style::Solid(Intent::Primary)),
|
.with_style(button::Style::Solid(Intent::Primary)),
|
||||||
)
|
)
|
||||||
.with_button(
|
.with_child(
|
||||||
Button::reset(Lc::t("btn_reset", &LOC)).with_style(
|
Button::reset(Lc::t("btn_reset", &LOC)).with_style(
|
||||||
button::Style::Outline(Intent::Neutral),
|
button::Style::Outline(Intent::Neutral),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.with_button(
|
.with_child(
|
||||||
Button::plain(Lc::t("btn_cancel", &LOC))
|
Button::plain(Lc::t("btn_cancel", &LOC))
|
||||||
.with_style(button::Style::Link),
|
.with_style(button::Style::Link),
|
||||||
),
|
),
|
||||||
|
|
@ -255,17 +260,22 @@ async fn form_controls(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
.with_child(form::Hidden::field("origin", "form-text"))
|
.with_child(form::Hidden::field("origin", "form-text"))
|
||||||
// Botonera de acciones.
|
// Botonera de acciones.
|
||||||
.with_child(
|
.with_child(
|
||||||
button::ButtonSet::new()
|
Container::new()
|
||||||
.with_button(
|
.with_flex(
|
||||||
|
Flex::row()
|
||||||
|
.with_wrap(flex::Behavior::Wrap)
|
||||||
|
.with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
Button::submit(Lc::t("btn_submit", &LOC))
|
Button::submit(Lc::t("btn_submit", &LOC))
|
||||||
.with_style(button::Style::Solid(Intent::Primary)),
|
.with_style(button::Style::Solid(Intent::Primary)),
|
||||||
)
|
)
|
||||||
.with_button(
|
.with_child(
|
||||||
Button::reset(Lc::t("btn_reset", &LOC)).with_style(
|
Button::reset(Lc::t("btn_reset", &LOC)).with_style(
|
||||||
button::Style::Outline(Intent::Neutral),
|
button::Style::Outline(Intent::Neutral),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.with_button(
|
.with_child(
|
||||||
Button::plain(Lc::t("btn_cancel", &LOC))
|
Button::plain(Lc::t("btn_cancel", &LOC))
|
||||||
.with_style(button::Style::Link),
|
.with_style(button::Style::Link),
|
||||||
),
|
),
|
||||||
|
|
@ -430,16 +440,21 @@ fn form_lists() -> Form {
|
||||||
.with_child(form::Hidden::field("origin", "form-lists"))
|
.with_child(form::Hidden::field("origin", "form-lists"))
|
||||||
// Botonera de acciones.
|
// Botonera de acciones.
|
||||||
.with_child(
|
.with_child(
|
||||||
button::ButtonSet::new()
|
Container::new()
|
||||||
.with_button(
|
.with_flex(
|
||||||
|
Flex::row()
|
||||||
|
.with_wrap(flex::Behavior::Wrap)
|
||||||
|
.with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
Button::submit(Lc::t("btn_submit", &LOC))
|
Button::submit(Lc::t("btn_submit", &LOC))
|
||||||
.with_style(button::Style::Solid(Intent::Primary)),
|
.with_style(button::Style::Solid(Intent::Primary)),
|
||||||
)
|
)
|
||||||
.with_button(
|
.with_child(
|
||||||
Button::reset(Lc::t("btn_reset", &LOC))
|
Button::reset(Lc::t("btn_reset", &LOC))
|
||||||
.with_style(button::Style::Outline(Intent::Neutral)),
|
.with_style(button::Style::Outline(Intent::Neutral)),
|
||||||
)
|
)
|
||||||
.with_button(
|
.with_child(
|
||||||
Button::plain(Lc::t("btn_cancel", &LOC)).with_style(button::Style::Link),
|
Button::plain(Lc::t("btn_cancel", &LOC)).with_style(button::Style::Link),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
540
examples/intro-flex.rs
Normal file
540
examples/intro-flex.rs
Normal file
|
|
@ -0,0 +1,540 @@
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
include_locales!(LOC from "examples/locale");
|
||||||
|
|
||||||
|
struct IntroFlex;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Extension for IntroFlex {
|
||||||
|
fn dependencies(&self) -> Vec<ExtensionRef> {
|
||||||
|
vec![&pagetop_bootsier::Bootsier]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configure_router(&self, router: Router) -> Router {
|
||||||
|
router.route("/", web::get(intro_flex))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn intro_flex(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("flex_slogan", &LOC))
|
||||||
|
.with_button(None::<(Lc, Route)>)
|
||||||
|
.with_child(direction_block())
|
||||||
|
.with_child(justify_block())
|
||||||
|
.with_child(align_block())
|
||||||
|
.with_child(align_self_block())
|
||||||
|
.with_child(align_content_block())
|
||||||
|
.with_child(grow_shrink_block())
|
||||||
|
.with_child(other_block()),
|
||||||
|
)
|
||||||
|
.render()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn direction_block() -> Block {
|
||||||
|
let mut block = Block::new().with_title(Lc::t("flex_block_title_direction", &LOC));
|
||||||
|
|
||||||
|
let direction_variants: [(&str, Flex, &str); 4] = [
|
||||||
|
("flex_title_direction_row", Flex::row(), "Flex::row()"),
|
||||||
|
(
|
||||||
|
"flex_title_direction_row_reverse",
|
||||||
|
Flex::row().with_direction(flex::Direction::RowReverse),
|
||||||
|
"Flex::row().with_direction(Direction::RowReverse)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"flex_title_direction_column",
|
||||||
|
Flex::column(),
|
||||||
|
"Flex::column()",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"flex_title_direction_column_reverse",
|
||||||
|
Flex::column().with_direction(flex::Direction::ColumnReverse),
|
||||||
|
"Flex::column().with_direction(Direction::ColumnReverse)",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
for (title_key, flex, code) in direction_variants {
|
||||||
|
block = block
|
||||||
|
.with_child(caption(Lc::t(title_key, &LOC), code))
|
||||||
|
.with_child(
|
||||||
|
demo_row(flex)
|
||||||
|
.with_child(demo_box(flex_item("1")))
|
||||||
|
.with_child(demo_box(flex_item("2")))
|
||||||
|
.with_child(demo_box(flex_item("3"))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
block
|
||||||
|
}
|
||||||
|
|
||||||
|
fn justify_block() -> Block {
|
||||||
|
let mut block = Block::new().with_title(Lc::t("flex_block_title_justify", &LOC));
|
||||||
|
|
||||||
|
let justify_variants: [(&str, flex::ContentJustify, &str); 6] = [
|
||||||
|
(
|
||||||
|
"flex_title_justify_start",
|
||||||
|
flex::ContentJustify::Start,
|
||||||
|
"Flex::row().with_justify(ContentJustify::Start)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"flex_title_justify_center",
|
||||||
|
flex::ContentJustify::Center,
|
||||||
|
"Flex::row().with_justify(ContentJustify::Center)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"flex_title_justify_end",
|
||||||
|
flex::ContentJustify::End,
|
||||||
|
"Flex::row().with_justify(ContentJustify::End)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"flex_title_justify_between",
|
||||||
|
flex::ContentJustify::SpaceBetween,
|
||||||
|
"Flex::row().with_justify(ContentJustify::SpaceBetween)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"flex_title_justify_around",
|
||||||
|
flex::ContentJustify::SpaceAround,
|
||||||
|
"Flex::row().with_justify(ContentJustify::SpaceAround)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"flex_title_justify_evenly",
|
||||||
|
flex::ContentJustify::SpaceEvenly,
|
||||||
|
"Flex::row().with_justify(ContentJustify::SpaceEvenly)",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
for (title_key, justify, code) in justify_variants {
|
||||||
|
block = block
|
||||||
|
.with_child(caption(Lc::t(title_key, &LOC), code))
|
||||||
|
.with_child(
|
||||||
|
demo_row(
|
||||||
|
Flex::row()
|
||||||
|
.with_justify(justify)
|
||||||
|
.with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
|
)
|
||||||
|
.with_child(demo_box(flex_item("1")))
|
||||||
|
.with_child(demo_box(flex_item("2")))
|
||||||
|
.with_child(demo_box(flex_item("3"))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
block
|
||||||
|
}
|
||||||
|
|
||||||
|
fn align_block() -> Block {
|
||||||
|
let mut block = Block::new().with_title(Lc::t("flex_block_title_align", &LOC));
|
||||||
|
|
||||||
|
let align_variants: [(&str, flex::Align, &str); 4] = [
|
||||||
|
(
|
||||||
|
"flex_title_align_start",
|
||||||
|
flex::Align::Start,
|
||||||
|
"Flex::row().with_align(Align::Start)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"flex_title_align_center",
|
||||||
|
flex::Align::Center,
|
||||||
|
"Flex::row().with_align(Align::Center)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"flex_title_align_end",
|
||||||
|
flex::Align::End,
|
||||||
|
"Flex::row().with_align(Align::End)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"flex_title_align_stretch",
|
||||||
|
flex::Align::Stretch,
|
||||||
|
"Flex::row().with_align(Align::Stretch)",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
for (title_key, align, code) in align_variants {
|
||||||
|
block = block
|
||||||
|
.with_child(caption(Lc::t(title_key, &LOC), code))
|
||||||
|
.with_child(
|
||||||
|
demo_row(
|
||||||
|
Flex::row()
|
||||||
|
.with_align(align)
|
||||||
|
.with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
|
)
|
||||||
|
.with_child(sized_box(Lc::t("flex_box_tall", &LOC), "2.5rem 1rem"))
|
||||||
|
.with_child(demo_box(Lc::t("flex_box_medium", &LOC)))
|
||||||
|
.with_child(sized_box(Lc::t("flex_box_short", &LOC), "0.15rem 1rem")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
block
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("flex_title_align_baseline", &LOC),
|
||||||
|
"Flex::row().with_align(Align::Baseline)",
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
demo_row(
|
||||||
|
Flex::row()
|
||||||
|
.with_align(flex::Align::Baseline)
|
||||||
|
.with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
sized_box(Lc::t("flex_box_tall", &LOC), "2.5rem 1rem")
|
||||||
|
.with_prop(PropsOp::add_style("font-size", "1.75rem")),
|
||||||
|
)
|
||||||
|
.with_child(demo_box(Lc::t("flex_box_medium", &LOC)))
|
||||||
|
.with_child(sized_box(Lc::t("flex_box_short", &LOC), "0.15rem 1rem")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn align_self_block() -> Block {
|
||||||
|
let mut block = Block::new().with_title(Lc::t("flex_block_title_align_self", &LOC));
|
||||||
|
|
||||||
|
let align_self_variants: [(&str, flex::ItemAlign, &str); 4] = [
|
||||||
|
(
|
||||||
|
"flex_title_align_self_start",
|
||||||
|
flex::ItemAlign::Start,
|
||||||
|
"FlexItem::new().with_align_self(flex::ItemAlign::Start)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"flex_title_align_self_end",
|
||||||
|
flex::ItemAlign::End,
|
||||||
|
"FlexItem::new().with_align_self(flex::ItemAlign::End)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"flex_title_align_self_center",
|
||||||
|
flex::ItemAlign::Center,
|
||||||
|
"FlexItem::new().with_align_self(flex::ItemAlign::Center)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"flex_title_align_self_stretch",
|
||||||
|
flex::ItemAlign::Stretch,
|
||||||
|
"FlexItem::new().with_align_self(flex::ItemAlign::Stretch)",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
for (title_key, align_self, code) in align_self_variants {
|
||||||
|
block = block
|
||||||
|
.with_child(caption(Lc::t(title_key, &LOC), code))
|
||||||
|
.with_child(
|
||||||
|
demo_row(
|
||||||
|
Flex::row()
|
||||||
|
.with_align(flex::Align::Start)
|
||||||
|
.with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
|
)
|
||||||
|
.with_child(sized_box(Lc::t("flex_box_tall", &LOC), "2.5rem 1rem"))
|
||||||
|
.with_child(demo_box(flex_item("1")).with_prop(PropsOp::flex_item(
|
||||||
|
FlexItem::new().with_align_self(align_self),
|
||||||
|
)))
|
||||||
|
.with_child(sized_box(Lc::t("flex_box_tall", &LOC), "2.5rem 1rem")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
block
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("flex_title_align_self_baseline", &LOC),
|
||||||
|
"FlexItem::new().with_align_self(flex::ItemAlign::Baseline)",
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
demo_row(
|
||||||
|
Flex::row()
|
||||||
|
.with_align(flex::Align::Start)
|
||||||
|
.with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
sized_box(Lc::t("flex_box_tall", &LOC), "2.5rem 1rem")
|
||||||
|
.with_prop(PropsOp::add_style("font-size", "1.75rem")),
|
||||||
|
)
|
||||||
|
.with_child(demo_box(flex_item("1")).with_prop(PropsOp::flex_item(
|
||||||
|
FlexItem::new().with_align_self(flex::ItemAlign::Baseline),
|
||||||
|
)))
|
||||||
|
.with_child(sized_box(Lc::t("flex_box_tall", &LOC), "2.5rem 1rem")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn align_content_block() -> Block {
|
||||||
|
let mut block = Block::new().with_title(Lc::t("flex_block_title_align_content", &LOC));
|
||||||
|
|
||||||
|
let align_content_variants: [(&str, flex::AlignContent, &str); 7] = [
|
||||||
|
(
|
||||||
|
"flex_title_align_content_start",
|
||||||
|
flex::AlignContent::Start,
|
||||||
|
"Flex::row().with_wrap(Behavior::Wrap).with_align_content(AlignContent::Start)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"flex_title_align_content_end",
|
||||||
|
flex::AlignContent::End,
|
||||||
|
"Flex::row().with_wrap(Behavior::Wrap).with_align_content(AlignContent::End)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"flex_title_align_content_center",
|
||||||
|
flex::AlignContent::Center,
|
||||||
|
"Flex::row().with_wrap(Behavior::Wrap).with_align_content(AlignContent::Center)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"flex_title_align_content_between",
|
||||||
|
flex::AlignContent::SpaceBetween,
|
||||||
|
"Flex::row().with_wrap(Behavior::Wrap).with_align_content(AlignContent::SpaceBetween)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"flex_title_align_content_around",
|
||||||
|
flex::AlignContent::SpaceAround,
|
||||||
|
"Flex::row().with_wrap(Behavior::Wrap).with_align_content(AlignContent::SpaceAround)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"flex_title_align_content_evenly",
|
||||||
|
flex::AlignContent::SpaceEvenly,
|
||||||
|
"Flex::row().with_wrap(Behavior::Wrap).with_align_content(AlignContent::SpaceEvenly)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"flex_title_align_content_stretch",
|
||||||
|
flex::AlignContent::Stretch,
|
||||||
|
"Flex::row().with_wrap(Behavior::Wrap).with_align_content(AlignContent::Stretch)",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
for (title_key, align_content, code) in align_content_variants {
|
||||||
|
let mut row = demo_row(
|
||||||
|
Flex::row()
|
||||||
|
.with_wrap(flex::Behavior::Wrap)
|
||||||
|
.with_align_content(align_content)
|
||||||
|
.with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
|
)
|
||||||
|
.with_prop(PropsOp::add_style("max-width", "21rem"))
|
||||||
|
.with_prop(PropsOp::add_style("min-height", "11rem"));
|
||||||
|
for label in ["1", "2", "3", "4"] {
|
||||||
|
row = row.with_child(
|
||||||
|
sized_box(flex_item(label), "0.5rem 1rem")
|
||||||
|
.with_prop(PropsOp::add_style("width", "9rem")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
block = block
|
||||||
|
.with_child(caption(Lc::t(title_key, &LOC), code))
|
||||||
|
.with_child(row);
|
||||||
|
}
|
||||||
|
block
|
||||||
|
}
|
||||||
|
|
||||||
|
fn grow_shrink_block() -> Block {
|
||||||
|
Block::new()
|
||||||
|
.with_title(Lc::t("flex_block_title_grow_shrink", &LOC))
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("flex_title_grow", &LOC),
|
||||||
|
"FlexItem::new().with_grow(flex::ItemGrow::Is1)",
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
demo_row(Flex::row().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_grows", &LOC)).with_prop(PropsOp::flex_item(
|
||||||
|
FlexItem::new().with_grow(flex::ItemGrow::Is1),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.with_child(demo_box(Lc::t("flex_box_fixed", &LOC))),
|
||||||
|
)
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("flex_title_shrink", &LOC),
|
||||||
|
"FlexItem::new().with_shrink(flex::ItemShrink::Is0)",
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
demo_row(Flex::row().with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))))
|
||||||
|
.with_prop(PropsOp::add_style("max-width", "31rem"))
|
||||||
|
.with_child(
|
||||||
|
demo_box(flex_item("1")).with_prop(PropsOp::add_style("width", "10.5rem")),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
demo_box(flex_item("2"))
|
||||||
|
.with_prop(PropsOp::add_style("width", "10.5rem"))
|
||||||
|
.with_prop(PropsOp::flex_item(
|
||||||
|
FlexItem::new().with_shrink(flex::ItemShrink::Is0),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
demo_box(flex_item("3")).with_prop(PropsOp::add_style("width", "10.5rem")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn other_block() -> Block {
|
||||||
|
let mut block = Block::new().with_title(Lc::t("flex_block_title_other", &LOC));
|
||||||
|
|
||||||
|
block = block
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("flex_title_push_end", &LOC),
|
||||||
|
"FlexItem::push_end()",
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
demo_row(Flex::row().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_2", &LOC)))
|
||||||
|
.with_child(demo_box(Lc::t("flex_box_end", &LOC)).with_prop(FlexItem::push_end())),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut wrap_row = demo_row(
|
||||||
|
Flex::row()
|
||||||
|
.with_wrap(flex::Behavior::Wrap)
|
||||||
|
.with_align_content(flex::AlignContent::SpaceBetween)
|
||||||
|
.with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
|
)
|
||||||
|
.with_prop(PropsOp::add_style("max-width", "20rem"))
|
||||||
|
.with_prop(PropsOp::add_style("min-height", "11rem"));
|
||||||
|
for label in ["1", "2", "3", "4", "5", "6", "7", "8"] {
|
||||||
|
wrap_row = wrap_row.with_child(
|
||||||
|
sized_box(flex_item(label), "0.5rem 1rem")
|
||||||
|
.with_prop(PropsOp::add_style("width", "4rem")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
block
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("flex_title_wrap", &LOC),
|
||||||
|
"Flex::row().with_wrap(Behavior::Wrap).with_align_content(AlignContent::SpaceBetween)",
|
||||||
|
))
|
||||||
|
.with_child(wrap_row)
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("flex_title_order", &LOC),
|
||||||
|
"FlexItem::new().with_order(ItemOrder::First) / .with_order(ItemOrder::Last)",
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
demo_row(Flex::row().with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))))
|
||||||
|
.with_child(demo_box(Lc::n("A")).with_prop(PropsOp::flex_item(
|
||||||
|
FlexItem::new().with_order(flex::ItemOrder::Last),
|
||||||
|
)))
|
||||||
|
.with_child(demo_box(Lc::n("B")))
|
||||||
|
.with_child(demo_box(Lc::n("C")))
|
||||||
|
.with_child(demo_box(Lc::n("D")))
|
||||||
|
.with_child(demo_box(Lc::n("E")).with_prop(PropsOp::flex_item(
|
||||||
|
FlexItem::new().with_order(flex::ItemOrder::First),
|
||||||
|
))),
|
||||||
|
)
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("flex_title_gap_none", &LOC),
|
||||||
|
"Flex::row() (Gap::None por defecto)",
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
demo_row(Flex::row())
|
||||||
|
.with_child(demo_box(flex_item("1")))
|
||||||
|
.with_child(demo_box(flex_item("2")))
|
||||||
|
.with_child(demo_box(flex_item("3"))),
|
||||||
|
)
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("flex_title_gap_some", &LOC),
|
||||||
|
"Flex::row().with_gap(flex::Gap::Both(UnitValue::RelRem(1.5)))",
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
demo_row(Flex::row().with_gap(flex::Gap::Both(UnitValue::RelRem(1.5))))
|
||||||
|
.with_child(demo_box(flex_item("1")))
|
||||||
|
.with_child(demo_box(flex_item("2")))
|
||||||
|
.with_child(demo_box(flex_item("3"))),
|
||||||
|
)
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("flex_title_grid_thirds", &LOC),
|
||||||
|
"FlexItem::new().with_size(flex::ItemSize::Percent33)",
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
demo_row(Flex::row())
|
||||||
|
.with_child(demo_box(Lc::n("1/3")).with_prop(PropsOp::flex_item(
|
||||||
|
FlexItem::new().with_size(flex::ItemSize::Percent33),
|
||||||
|
)))
|
||||||
|
.with_child(demo_box(Lc::n("1/3")).with_prop(PropsOp::flex_item(
|
||||||
|
FlexItem::new().with_size(flex::ItemSize::Percent33),
|
||||||
|
)))
|
||||||
|
.with_child(demo_box(Lc::n("1/3")).with_prop(PropsOp::flex_item(
|
||||||
|
FlexItem::new().with_size(flex::ItemSize::Percent33),
|
||||||
|
))),
|
||||||
|
)
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("flex_title_grid_offset", &LOC),
|
||||||
|
"FlexItem::new().with_size(ItemSize::Percent50).with_offset(ItemOffset::Percent25)",
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
demo_row(Flex::row()).with_child(
|
||||||
|
demo_box(Lc::t("flex_box_half_centered", &LOC)).with_prop(PropsOp::flex_item(
|
||||||
|
FlexItem::new()
|
||||||
|
.with_size(flex::ItemSize::Percent50)
|
||||||
|
.with_offset(flex::ItemOffset::Percent25),
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("flex_title_toolbar", &LOC),
|
||||||
|
"Container con Flex anidado dentro de otro Container con Flex, y push_end()",
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
demo_row(Flex::row().with_align(flex::Align::Center))
|
||||||
|
.with_child(
|
||||||
|
Container::new()
|
||||||
|
.with_flex(Flex::row().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_edit", &LOC)))
|
||||||
|
.with_child(demo_box(Lc::t("flex_box_view", &LOC))),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
Container::new()
|
||||||
|
.with_prop(FlexItem::push_end())
|
||||||
|
.with_flex(Flex::row().with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))))
|
||||||
|
.with_child(demo_box(Lc::t("flex_box_profile", &LOC)))
|
||||||
|
.with_child(demo_box(Lc::t("flex_box_logout", &LOC))),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< HELPERS >************************************************************************************
|
||||||
|
|
||||||
|
// Aspecto fijo de las cajas y filas de muestra.
|
||||||
|
fn demo_styles() -> StyleSheet {
|
||||||
|
StyleSheet::inline("intro-flex", |_| {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
"#
|
||||||
|
)
|
||||||
|
.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 "Flex item N" para las cajas que solo se distinguen por su posicion.
|
||||||
|
fn flex_item(n: impl Into<CowStr>) -> Lc {
|
||||||
|
Lc::t("flex_item_label", &LOC).with_arg("n", n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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: &'static str) -> Html {
|
||||||
|
Html::with(move |cx| {
|
||||||
|
html! {
|
||||||
|
h3 { (title.using(cx)) }
|
||||||
|
p { code { (code) } }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::main]
|
||||||
|
async fn main() -> std::io::Result<()> {
|
||||||
|
Application::prepare(&IntroFlex).await.run().await
|
||||||
|
}
|
||||||
63
examples/locale/en-US/intro-flex.ftl
Normal file
63
examples/locale/en-US/intro-flex.ftl
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
flex_slogan = Flexbox positioning
|
||||||
|
flex_block_title_direction = Direction
|
||||||
|
flex_block_title_justify = Justify content
|
||||||
|
flex_block_title_align = Align items
|
||||||
|
flex_block_title_align_self = Align self
|
||||||
|
flex_block_title_align_content = Align content
|
||||||
|
flex_block_title_grow_shrink = Grow and shrink
|
||||||
|
flex_block_title_other = Other examples
|
||||||
|
|
||||||
|
flex_title_direction_row = Row
|
||||||
|
flex_title_direction_row_reverse = Row reverse
|
||||||
|
flex_title_direction_column = Column
|
||||||
|
flex_title_direction_column_reverse = Column reverse
|
||||||
|
flex_title_justify_start = Justify content: start
|
||||||
|
flex_title_justify_center = Justify content: center
|
||||||
|
flex_title_justify_end = Justify content: end
|
||||||
|
flex_title_justify_between = Justify content: space between
|
||||||
|
flex_title_justify_around = Justify content: space around
|
||||||
|
flex_title_justify_evenly = Justify content: space evenly
|
||||||
|
flex_title_align_start = Align items: start
|
||||||
|
flex_title_align_center = Align items: center
|
||||||
|
flex_title_align_end = Align items: end
|
||||||
|
flex_title_align_stretch = Align items: stretch
|
||||||
|
flex_title_align_baseline = Align items: baseline
|
||||||
|
flex_title_align_self_start = Align self: start
|
||||||
|
flex_title_align_self_end = Align self: end
|
||||||
|
flex_title_align_self_center = Align self: center
|
||||||
|
flex_title_align_self_stretch = Align self: stretch
|
||||||
|
flex_title_align_self_baseline = Align self: baseline
|
||||||
|
flex_title_align_content_start = Align content: start
|
||||||
|
flex_title_align_content_end = Align content: end
|
||||||
|
flex_title_align_content_center = Align content: center
|
||||||
|
flex_title_align_content_between = Align content: space between
|
||||||
|
flex_title_align_content_around = Align content: space around
|
||||||
|
flex_title_align_content_evenly = Align content: space evenly
|
||||||
|
flex_title_align_content_stretch = Align content: stretch
|
||||||
|
flex_title_grow = Growth
|
||||||
|
flex_title_shrink = Shrink
|
||||||
|
flex_title_push_end = Automatic margin
|
||||||
|
flex_title_wrap = Line wrapping across multiple rows
|
||||||
|
flex_title_order = Visual order
|
||||||
|
flex_title_gap_none = Without spacing
|
||||||
|
flex_title_gap_some = With spacing
|
||||||
|
flex_title_grid_thirds = Column grid: three thirds
|
||||||
|
flex_title_grid_offset = Column grid: centered column with offset
|
||||||
|
flex_title_toolbar = Composite structure: toolbar
|
||||||
|
|
||||||
|
flex_item_label = Flex item { $n }
|
||||||
|
|
||||||
|
flex_box_tall = Tall
|
||||||
|
flex_box_medium = Medium
|
||||||
|
flex_box_short = Short
|
||||||
|
flex_box_fixed = Fixed
|
||||||
|
flex_box_grows = Grows to fill the space
|
||||||
|
flex_box_start_1 = Start 1
|
||||||
|
flex_box_start_2 = Start 2
|
||||||
|
flex_box_end = End
|
||||||
|
flex_box_half_centered = Half, centered
|
||||||
|
flex_box_file = File
|
||||||
|
flex_box_edit = Edit
|
||||||
|
flex_box_view = View
|
||||||
|
flex_box_profile = Profile
|
||||||
|
flex_box_logout = Sign out
|
||||||
63
examples/locale/es-ES/intro-flex.ftl
Normal file
63
examples/locale/es-ES/intro-flex.ftl
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
flex_slogan = Posicionamiento con Flexbox
|
||||||
|
flex_block_title_direction = Dirección
|
||||||
|
flex_block_title_justify = Justificación de contenido
|
||||||
|
flex_block_title_align = Alineación de elementos
|
||||||
|
flex_block_title_align_self = Alineación individual
|
||||||
|
flex_block_title_align_content = Alineación de contenido
|
||||||
|
flex_block_title_grow_shrink = Crecimiento y reducción
|
||||||
|
flex_block_title_other = Otros ejemplos
|
||||||
|
|
||||||
|
flex_title_direction_row = Fila
|
||||||
|
flex_title_direction_row_reverse = Fila invertida
|
||||||
|
flex_title_direction_column = Columna
|
||||||
|
flex_title_direction_column_reverse = Columna invertida
|
||||||
|
flex_title_justify_start = Justificación: inicio
|
||||||
|
flex_title_justify_center = Justificación: centro
|
||||||
|
flex_title_justify_end = Justificación: final
|
||||||
|
flex_title_justify_between = Justificación: espacio entre elementos
|
||||||
|
flex_title_justify_around = Justificación: espacio alrededor de cada elemento
|
||||||
|
flex_title_justify_evenly = Justificación: espacio repartido a partes iguales
|
||||||
|
flex_title_align_start = Alineación: inicio
|
||||||
|
flex_title_align_center = Alineación: centro
|
||||||
|
flex_title_align_end = Alineación: final
|
||||||
|
flex_title_align_stretch = Alineación: estirar
|
||||||
|
flex_title_align_baseline = Alineación: línea base
|
||||||
|
flex_title_align_self_start = Alineación individual: inicio
|
||||||
|
flex_title_align_self_end = Alineación individual: final
|
||||||
|
flex_title_align_self_center = Alineación individual: centro
|
||||||
|
flex_title_align_self_stretch = Alineación individual: estirar
|
||||||
|
flex_title_align_self_baseline = Alineación individual: línea base
|
||||||
|
flex_title_align_content_start = Alineación de contenido: inicio
|
||||||
|
flex_title_align_content_end = Alineación de contenido: final
|
||||||
|
flex_title_align_content_center = Alineación de contenido: centro
|
||||||
|
flex_title_align_content_between = Alineación de contenido: espacio entre líneas
|
||||||
|
flex_title_align_content_around = Alineación de contenido: espacio alrededor de cada línea
|
||||||
|
flex_title_align_content_evenly = Alineación de contenido: espacio repartido a partes iguales
|
||||||
|
flex_title_align_content_stretch = Alineación de contenido: estirar
|
||||||
|
flex_title_grow = Crecimiento
|
||||||
|
flex_title_shrink = Reducción
|
||||||
|
flex_title_push_end = Margen automático
|
||||||
|
flex_title_wrap = Ajuste de línea con múltiples filas
|
||||||
|
flex_title_order = Orden visual
|
||||||
|
flex_title_gap_none = Sin espaciado
|
||||||
|
flex_title_gap_some = Con espaciado
|
||||||
|
flex_title_grid_thirds = Rejilla de columnas: tres tercios
|
||||||
|
flex_title_grid_offset = Rejilla de columnas: columna centrada con desplazamiento
|
||||||
|
flex_title_toolbar = Estructura compuesta: barra de herramientas
|
||||||
|
|
||||||
|
flex_item_label = Flex ítem { $n }
|
||||||
|
|
||||||
|
flex_box_tall = Alto
|
||||||
|
flex_box_medium = Medio
|
||||||
|
flex_box_short = Bajo
|
||||||
|
flex_box_fixed = Fijo
|
||||||
|
flex_box_grows = Crece para llenar el espacio
|
||||||
|
flex_box_start_1 = Inicio 1
|
||||||
|
flex_box_start_2 = Inicio 2
|
||||||
|
flex_box_end = Final
|
||||||
|
flex_box_half_centered = Mitad, centrada
|
||||||
|
flex_box_file = Archivo
|
||||||
|
flex_box_edit = Editar
|
||||||
|
flex_box_view = Ver
|
||||||
|
flex_box_profile = Perfil
|
||||||
|
flex_box_logout = Salir
|
||||||
|
|
@ -82,10 +82,8 @@ impl Extension for SuperMenu {
|
||||||
))
|
))
|
||||||
.with_item(bs::navbar::Item::nav(
|
.with_item(bs::navbar::Item::nav(
|
||||||
bs::Nav::new()
|
bs::Nav::new()
|
||||||
.with_prop(PropsOp::add_classes(class::Margin::with(
|
// Empuja este menú (y lo que le siga) al extremo final de la barra.
|
||||||
BoxSide::Start,
|
.with_prop(FlexItem::push_end())
|
||||||
ScaleSize::Auto,
|
|
||||||
)))
|
|
||||||
.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",
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ pub use pagetop::base::component::breadcrumb;
|
||||||
|
|
||||||
// Button.
|
// Button.
|
||||||
pub mod button;
|
pub mod button;
|
||||||
pub use button::{Button, ButtonBootsier, ButtonSet};
|
pub use button::{Button, ButtonBootsier};
|
||||||
|
|
||||||
// Container.
|
// Container.
|
||||||
pub mod container;
|
pub mod container;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
//! Definiciones para crear botones ([`Button`]) y conjuntos de botones ([`ButtonSet`]).
|
//! Definiciones para crear botones ([`Button`]).
|
||||||
|
|
||||||
use pagetop::prelude::*;
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
use crate::theme::BootsierColors;
|
use crate::theme::BootsierColors;
|
||||||
|
|
||||||
pub use pagetop::base::component::button::{Button, ButtonSet, Kind, Size, Style};
|
pub use pagetop::base::component::button::{Button, Kind, Size, Style};
|
||||||
|
|
||||||
const EXTRA_ACTIVE: &str = "bootsier.button.active";
|
const EXTRA_ACTIVE: &str = "bootsier.button.active";
|
||||||
const EXTRA_FULL_WIDTH: &str = "bootsier.button.full_width";
|
const EXTRA_FULL_WIDTH: &str = "bootsier.button.full_width";
|
||||||
|
|
|
||||||
|
|
@ -317,14 +317,12 @@ async fn render_user_edit(
|
||||||
// pantalla siga sabiendo devolver al listado en el estado en que se dejó.
|
// pantalla siga sabiendo devolver al listado en el estado en que se dejó.
|
||||||
//
|
//
|
||||||
// "Guardar" (envía el `<form>` de `UserForm` vía el atributo `form`, ver `USER_ADMIN_FORM_ID`),
|
// "Guardar" (envía el `<form>` de `UserForm` vía el atributo `form`, ver `USER_ADMIN_FORM_ID`),
|
||||||
// "Gestionar roles" y "Restablecer contraseña" se agrupan en un `button::ButtonSet` para que el
|
// "Gestionar roles" y "Restablecer contraseña" son botones sueltos; bloqueo/activación y
|
||||||
// tema los alinee con espaciado uniforme. Los botones de bloqueo/activación y de
|
|
||||||
// concesión/revocación de admin (este último sólo si `can_toggle_admin`) van cada uno en su propio
|
// concesión/revocación de admin (este último sólo si `can_toggle_admin`) van cada uno en su propio
|
||||||
// `Form`, con un campo `Hidden` para el nuevo valor: `ButtonSet` sólo admite componentes `Button`,
|
// `Form`, con un campo `Hidden` para el nuevo valor, para conservar el envío nativo sin JavaScript,
|
||||||
// así que no pueden ir dentro; conservan así el envío nativo sin JavaScript, mejorado con
|
// mejorado con `hx-post`/`hx-confirm`. Todos son hijos directos del mismo `Container` con Flex, que
|
||||||
// `hx-post`/`hx-confirm`. Todo se construye con componentes -- `Container`, `Form`, `Hidden`,
|
// los alinea en fila con espaciado uniforme sin que ninguno tenga que ser forzosamente un `Button`
|
||||||
// `ButtonSet`, `Button` --, sin `html!` en bruto (ver PAGETOP.md, "Preferir componentes a `html!`
|
// suelto (ver PAGETOP.md, "Preferir componentes a `html!` en bruto").
|
||||||
// en bruto"); la alineación en línea de los tres queda pendiente de una pasada posterior.
|
|
||||||
fn edit_actions(
|
fn edit_actions(
|
||||||
user_id: i32,
|
user_id: i32,
|
||||||
status: UserStatus,
|
status: UserStatus,
|
||||||
|
|
@ -350,21 +348,6 @@ fn edit_actions(
|
||||||
waypoint.append_to(cx.route(format!("{ADMIN_USERS_PATH}/{user_id}/status")));
|
waypoint.append_to(cx.route(format!("{ADMIN_USERS_PATH}/{user_id}/status")));
|
||||||
let admin_action = waypoint.append_to(cx.route(format!("{ADMIN_USERS_PATH}/{user_id}/admin")));
|
let admin_action = waypoint.append_to(cx.route(format!("{ADMIN_USERS_PATH}/{user_id}/admin")));
|
||||||
|
|
||||||
let buttons = button::ButtonSet::new()
|
|
||||||
.with_button(
|
|
||||||
Button::submit(Lc::t("btn-save", &LOCALES_USER))
|
|
||||||
.with_style(button::Style::Solid(Intent::Primary))
|
|
||||||
.with_prop(PropsOp::set("form", USER_ADMIN_FORM_ID)),
|
|
||||||
)
|
|
||||||
.with_button(
|
|
||||||
Button::anchor(Lc::t("btn-manage-roles", &LOCALES_USER), roles_href)
|
|
||||||
.with_style(button::Style::Solid(Intent::Neutral)),
|
|
||||||
)
|
|
||||||
.with_button(
|
|
||||||
Button::anchor(Lc::t("btn-reset-password", &LOCALES_USER), password_href)
|
|
||||||
.with_style(button::Style::Solid(Intent::Neutral)),
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut status_form = Form::new()
|
let mut status_form = Form::new()
|
||||||
.with_action(status_action.clone())
|
.with_action(status_action.clone())
|
||||||
.with_method(form::Method::Post)
|
.with_method(form::Method::Post)
|
||||||
|
|
@ -378,7 +361,27 @@ fn edit_actions(
|
||||||
status_form = status_form.with_prop(PropsOp::set(hx::CONFIRM, confirm));
|
status_form = status_form.with_prop(PropsOp::set(hx::CONFIRM, confirm));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut container = Container::new().with_child(buttons).with_child(status_form);
|
let mut container = Container::new()
|
||||||
|
.with_flex(
|
||||||
|
Flex::row()
|
||||||
|
.with_wrap(flex::Behavior::Wrap)
|
||||||
|
.with_align(flex::Align::Center)
|
||||||
|
.with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
Button::submit(Lc::t("btn-save", &LOCALES_USER))
|
||||||
|
.with_style(button::Style::Solid(Intent::Primary))
|
||||||
|
.with_prop(PropsOp::set("form", USER_ADMIN_FORM_ID)),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
Button::anchor(Lc::t("btn-manage-roles", &LOCALES_USER), roles_href)
|
||||||
|
.with_style(button::Style::Solid(Intent::Neutral)),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
Button::anchor(Lc::t("btn-reset-password", &LOCALES_USER), password_href)
|
||||||
|
.with_style(button::Style::Solid(Intent::Neutral)),
|
||||||
|
)
|
||||||
|
.with_child(status_form);
|
||||||
|
|
||||||
if can_toggle_admin {
|
if can_toggle_admin {
|
||||||
let mut admin_form = Form::new()
|
let mut admin_form = Form::new()
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ pub use block::Block;
|
||||||
|
|
||||||
pub mod button;
|
pub mod button;
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
pub use button::{Button, ButtonSet};
|
pub use button::Button;
|
||||||
|
|
||||||
pub mod container;
|
pub mod container;
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
//! Definiciones para crear botones ([`Button`]) y conjuntos de botones ([`ButtonSet`]).
|
//! Definiciones para crear botones ([`Button`]).
|
||||||
|
|
||||||
mod props;
|
mod props;
|
||||||
pub use props::{Kind, Size, Style};
|
pub use props::{Kind, Size, Style};
|
||||||
|
|
||||||
mod component;
|
mod component;
|
||||||
pub use component::Button;
|
pub use component::Button;
|
||||||
|
|
||||||
mod set;
|
|
||||||
pub use set::ButtonSet;
|
|
||||||
|
|
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
use crate::prelude::*;
|
|
||||||
|
|
||||||
/// Componente para mostrar un **conjunto de botones**.
|
|
||||||
///
|
|
||||||
/// Envuelve los botones en un contenedor que cada tema estiliza para separarlos visualmente. Sólo
|
|
||||||
/// admite componentes [`Button`].
|
|
||||||
///
|
|
||||||
/// # Ejemplo
|
|
||||||
///
|
|
||||||
/// ```rust,no_run
|
|
||||||
/// use pagetop::prelude::*;
|
|
||||||
///
|
|
||||||
/// let actions = button::ButtonSet::new()
|
|
||||||
/// .with_button(Button::submit(Lc::n("Save")))
|
|
||||||
/// .with_button(Button::plain(Lc::n("Cancel")));
|
|
||||||
/// ```
|
|
||||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
|
||||||
pub struct ButtonSet {
|
|
||||||
/// Devuelve identificador, clases CSS, atributos HTML y valores extra del componente.
|
|
||||||
props: Props,
|
|
||||||
/// Devuelve los botones del conjunto.
|
|
||||||
buttons: Children,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Component for ButtonSet {
|
|
||||||
fn new() -> Self {
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn id(&self) -> Option<String> {
|
|
||||||
self.props.get_id()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn setup(&mut self, _cx: &Context) {
|
|
||||||
self.alter_prop(PropsOp::prepend_classes("button-set"));
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
|
||||||
let buttons = self.buttons().render(cx).await;
|
|
||||||
if buttons.is_empty() {
|
|
||||||
return Ok(html! {});
|
|
||||||
}
|
|
||||||
Ok(html! {
|
|
||||||
div (self.props()) { (buttons) }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[builder_impl]
|
|
||||||
impl ButtonSet {
|
|
||||||
// **< ButtonSet BUILDER >*************************************************************************
|
|
||||||
|
|
||||||
/// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`.
|
|
||||||
pub fn with_id(mut self, id: impl Into<CowStr>) -> Self {
|
|
||||||
self.props.alter_id(id);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Modifica identificador, clases CSS, atributos HTML o valores extra del componente.
|
|
||||||
pub fn with_prop(mut self, op: PropsOp) -> Self {
|
|
||||||
self.props.alter_prop(op);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Añade un botón al conjunto, o modifica su lista de botones con una operación [`TypedOp`].
|
|
||||||
pub fn with_button(mut self, op: impl Into<TypedOp<Button>>) -> Self {
|
|
||||||
self.buttons.alter_child(op.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -47,6 +47,9 @@ pub struct Container {
|
||||||
props: Props,
|
props: Props,
|
||||||
/// Devuelve el tipo semántico del contenedor.
|
/// Devuelve el tipo semántico del contenedor.
|
||||||
kind: Kind,
|
kind: Kind,
|
||||||
|
/// Devuelve el posicionamiento Flexbox como contenedor, si tiene alguno.
|
||||||
|
#[getters(copy)]
|
||||||
|
flex: Option<Flex>,
|
||||||
/// Devuelve la lista de componentes (`children`) del contenedor.
|
/// Devuelve la lista de componentes (`children`) del contenedor.
|
||||||
children: Children,
|
children: Children,
|
||||||
}
|
}
|
||||||
|
|
@ -61,6 +64,12 @@ impl Component for Container {
|
||||||
self.props.get_id()
|
self.props.get_id()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn setup(&mut self, _cx: &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;
|
||||||
|
|
@ -134,6 +143,12 @@ impl Container {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Establece el posicionamiento Flexbox como contenedor (usa `None` para quitarlo).
|
||||||
|
pub fn with_flex(mut self, flex: impl Into<Option<Flex>>) -> Self {
|
||||||
|
self.flex = flex.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Añade un nuevo componente al contenedor o modifica la lista de componentes (`children`) con
|
/// Añade un nuevo componente al contenedor o modifica la lista de componentes (`children`) con
|
||||||
/// una operación [`ChildOp`].
|
/// una operación [`ChildOp`].
|
||||||
pub fn with_child(mut self, op: impl Into<ChildOp>) -> Self {
|
pub fn with_child(mut self, op: impl Into<ChildOp>) -> Self {
|
||||||
|
|
|
||||||
|
|
@ -136,8 +136,7 @@ impl Dialog {
|
||||||
/// lista de componentes (`children`) del pie con una operación [`ChildOp`].
|
/// lista de componentes (`children`) del pie con una operación [`ChildOp`].
|
||||||
///
|
///
|
||||||
/// El pie ya se maqueta en fila y alineado a la derecha por su propia clase CSS
|
/// El pie ya se maqueta en fila y alineado a la derecha por su propia clase CSS
|
||||||
/// (`dialog-footer`); por lo que no requiere un [`ButtonSet`](super::ButtonSet) para alinear
|
/// (`dialog-footer`); no requiere ninguna configuración adicional para alinear los botones.
|
||||||
/// los botones, aunque puede usarse si se desea.
|
|
||||||
pub fn with_footer(mut self, op: impl Into<ChildOp>) -> Self {
|
pub fn with_footer(mut self, op: impl Into<ChildOp>) -> Self {
|
||||||
self.footer.alter_child(op.into());
|
self.footer.alter_child(op.into());
|
||||||
self
|
self
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,9 @@ pub struct Navbar {
|
||||||
props: Props,
|
props: Props,
|
||||||
/// Devuelve la disposición configurada para la barra de navegación.
|
/// Devuelve la disposición configurada para la barra de navegación.
|
||||||
layout: navbar::Layout,
|
layout: navbar::Layout,
|
||||||
|
/// Devuelve el posicionamiento Flexbox como contenedor, si tiene alguno.
|
||||||
|
#[getters(copy)]
|
||||||
|
flex: Option<Flex>,
|
||||||
/// Devuelve la lista de contenidos.
|
/// Devuelve la lista de contenidos.
|
||||||
items: Children,
|
items: Children,
|
||||||
}
|
}
|
||||||
|
|
@ -128,37 +131,44 @@ 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()`).
|
||||||
|
let mut content_props = Props::default();
|
||||||
|
if let Some(flex) = self.flex() {
|
||||||
|
flex.apply_to(&mut content_props);
|
||||||
|
}
|
||||||
|
content_props.alter_prop(PropsOp::prepend_classes("navbar-content"));
|
||||||
|
|
||||||
Ok(html! {
|
Ok(html! {
|
||||||
nav (self.props()) {
|
nav (self.props()) {
|
||||||
@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 class="navbar-content" { (items) }
|
div (content_props) { (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(cx, &id_content))
|
||||||
div id=(&id_content) class="navbar-content" { (items) }
|
div id=(&id_content) (content_props) { (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 class="navbar-content" { (items) }
|
div (content_props) { (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(cx, &id_content))
|
||||||
div id=(&id_content) class="navbar-content" { (items) }
|
div id=(&id_content) (content_props) { (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(cx, &id_content))
|
||||||
div id=(&id_content) class="navbar-content" { (items) }
|
div id=(&id_content) (content_props) { (items) }
|
||||||
(brand.render(cx).await)
|
(brand.render(cx).await)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -216,6 +226,15 @@ impl Navbar {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Establece el posicionamiento Flexbox como contenedor (usa `None` para quitarlo).
|
||||||
|
///
|
||||||
|
/// No afecta a la posición de la marca ni del botón de despliegue, que quedan fijados con
|
||||||
|
/// [`with_layout()`](Self::with_layout).
|
||||||
|
pub fn with_flex(mut self, flex: impl Into<Option<Flex>>) -> Self {
|
||||||
|
self.flex = flex.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Añade un nuevo contenido a la barra de navegación o modifica la lista de contenidos de la
|
/// Añade un nuevo contenido a la barra de navegación o modifica la lista de contenidos de la
|
||||||
/// barra con una operación [`TypedOp`].
|
/// barra con una operación [`TypedOp`].
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -33,3 +33,9 @@ pub use props::{Props, PropsError, PropsExtra, PropsOp};
|
||||||
|
|
||||||
mod unit;
|
mod unit;
|
||||||
pub use unit::UnitValue;
|
pub use unit::UnitValue;
|
||||||
|
|
||||||
|
// **< HTML LAYOUT >********************************************************************************
|
||||||
|
|
||||||
|
pub mod flex;
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use flex::{Flex, FlexItem};
|
||||||
|
|
|
||||||
37
src/html/flex.rs
Normal file
37
src/html/flex.rs
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
//! Definiciones para el posicionamiento de componentes con [Flexbox].
|
||||||
|
//!
|
||||||
|
//! [`Flex`] configura un contenedor y sus hijos como un grupo sobre el que se aplican propiedades
|
||||||
|
//! de presentación (dirección, ajuste de línea, alineación, espaciado). Lo usan componentes que
|
||||||
|
//! 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
|
||||||
|
//! padre (crecimiento, reducción, alineación individual, orden, ancho y desplazamiento). Al poder
|
||||||
|
//! acabar aplicándose sobre cualquier componente (no sólo los que ofrecen `with_flex()`), no tiene
|
||||||
|
//! un builder propio: se aplica con [`PropsOp::flex_item()`] sobre el `with_prop()` que ya expone
|
||||||
|
//! cualquier componente.
|
||||||
|
//!
|
||||||
|
//! # Un entorno autosuficiente
|
||||||
|
//!
|
||||||
|
//! Toda la configuración de `Flex`/`FlexItem` se resuelve con estilos en línea (`style="..."`),
|
||||||
|
//! nunca como clases CSS (consulta el propio [`Flex`] para ver el porqué). Los estilos en línea
|
||||||
|
//! tienen la especificidad más alta que existe en CSS, salvo `!important`, así que ningún framework
|
||||||
|
//! CSS de terceros, ni el CSS de la propia aplicación, puede sobrescribirlo por accidente. 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
|
||||||
|
//! [`Container`]: crate::base::component::Container
|
||||||
|
//! [`Navbar`]: crate::base::component::Navbar
|
||||||
|
//! [`PropsOp::flex_item()`]: crate::html::props::PropsOp::flex_item
|
||||||
|
|
||||||
|
mod props_container;
|
||||||
|
pub use props_container::{Align, AlignContent, Behavior, ContentJustify, Direction, Gap};
|
||||||
|
|
||||||
|
mod props_item;
|
||||||
|
pub use props_item::{ItemAlign, ItemGrow, ItemOffset, ItemOrder, ItemShrink, ItemSize};
|
||||||
|
|
||||||
|
mod container;
|
||||||
|
pub use container::Flex;
|
||||||
|
|
||||||
|
mod item;
|
||||||
|
pub use item::FlexItem;
|
||||||
137
src/html/flex/container.rs
Normal file
137
src/html/flex/container.rs
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
use crate::html::flex::props_container::{
|
||||||
|
Align, AlignContent, Behavior, ContentJustify, Direction, Gap,
|
||||||
|
};
|
||||||
|
use crate::html::props::{Props, PropsOp};
|
||||||
|
use crate::{AutoDefault, Getters, builder_impl};
|
||||||
|
|
||||||
|
// **< Flex >***************************************************************************************
|
||||||
|
|
||||||
|
/// Configuración para el posicionamiento Flexbox en un contenedor.
|
||||||
|
///
|
||||||
|
/// Se resuelve con estilos en línea (`display`, `flex-direction`, `flex-wrap`, `justify-content`,
|
||||||
|
/// `align-items`, `align-content`, `gap`), nunca como clases CSS. Son propiedades estándar 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
|
||||||
|
/// alta que existe en CSS, salvo `!important`. Ningún *framework* CSS de terceros, ni el CSS de la
|
||||||
|
/// propia aplicación, puede sobrescribir por accidente lo que `Flex` aplica. Es un mecanismo
|
||||||
|
/// autosuficiente que funciona igual conviva con quien conviva en la misma página, sin coordinar
|
||||||
|
/// nombres de clase ni orden de carga de hojas de estilo con nadie.
|
||||||
|
///
|
||||||
|
/// # Ejemplo
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// use pagetop::prelude::*;
|
||||||
|
///
|
||||||
|
/// let actions = Container::new()
|
||||||
|
/// .with_flex(
|
||||||
|
/// Flex::row()
|
||||||
|
/// .with_justify(flex::ContentJustify::End)
|
||||||
|
/// .with_align(flex::Align::Center)
|
||||||
|
/// .with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
|
/// )
|
||||||
|
/// .with_child(Button::submit(Lc::n("Save")))
|
||||||
|
/// .with_child(Button::plain(Lc::n("Cancel")));
|
||||||
|
/// ```
|
||||||
|
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq, Getters)]
|
||||||
|
pub struct Flex {
|
||||||
|
/// Devuelve la dirección del eje principal.
|
||||||
|
#[getters(copy)]
|
||||||
|
direction: Direction,
|
||||||
|
/// Devuelve el comportamiento cuando los elementos no caben en una sola línea.
|
||||||
|
#[getters(copy)]
|
||||||
|
wrap: Behavior,
|
||||||
|
/// Devuelve la alineación de los elementos en el eje principal.
|
||||||
|
#[getters(copy)]
|
||||||
|
justify: ContentJustify,
|
||||||
|
/// Devuelve la alineación de los elementos en el eje transversal.
|
||||||
|
#[getters(copy)]
|
||||||
|
align: Align,
|
||||||
|
/// Devuelve la alineación de las líneas cuando hay más de una.
|
||||||
|
#[getters(copy)]
|
||||||
|
align_content: AlignContent,
|
||||||
|
/// Devuelve el espaciado entre elementos.
|
||||||
|
#[getters(copy)]
|
||||||
|
gap: Gap,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[builder_impl]
|
||||||
|
impl Flex {
|
||||||
|
/// Crea una configuración Flex para disponer los elementos en fila (comportamiento por
|
||||||
|
/// defecto).
|
||||||
|
pub fn row() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Crea una configuración Flex para disponer los elementos en columna.
|
||||||
|
pub fn column() -> Self {
|
||||||
|
Self {
|
||||||
|
direction: Direction::Column,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Flex BUILDER >***************************************************************************
|
||||||
|
|
||||||
|
/// Establece la dirección del eje principal.
|
||||||
|
pub fn with_direction(mut self, direction: Direction) -> Self {
|
||||||
|
self.direction = direction;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Establece el comportamiento cuando los elementos no caben en una sola línea.
|
||||||
|
pub fn with_wrap(mut self, wrap: Behavior) -> Self {
|
||||||
|
self.wrap = wrap;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Establece la alineación de los elementos en el eje principal.
|
||||||
|
pub fn with_justify(mut self, justify: ContentJustify) -> Self {
|
||||||
|
self.justify = justify;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Establece la alineación de los elementos en el eje transversal.
|
||||||
|
pub fn with_align(mut self, align: Align) -> Self {
|
||||||
|
self.align = align;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
||||||
|
self.align_content = align_content;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Establece el espaciado entre elementos.
|
||||||
|
pub fn with_gap(mut self, gap: Gap) -> Self {
|
||||||
|
self.gap = gap;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Flex {
|
||||||
|
/// 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
|
||||||
|
/// la traducción de la configuración a estilos, para no repetirla en cada componente que la
|
||||||
|
/// 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
|
||||||
|
/// contenido.
|
||||||
|
pub fn apply_to(self, props: &mut Props) {
|
||||||
|
props.alter_prop(PropsOp::add_style("display", "flex"));
|
||||||
|
for (property, value) in [
|
||||||
|
("flex-direction", self.direction.value()),
|
||||||
|
("flex-wrap", self.wrap.value()),
|
||||||
|
("justify-content", self.justify.value()),
|
||||||
|
("align-items", self.align.value()),
|
||||||
|
("align-content", self.align_content.value()),
|
||||||
|
] {
|
||||||
|
props.alter_prop(PropsOp::add_style(property, value));
|
||||||
|
}
|
||||||
|
for (property, value) in self.gap.styles() {
|
||||||
|
props.alter_prop(PropsOp::add_style(property, value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
167
src/html/flex/item.rs
Normal file
167
src/html/flex/item.rs
Normal file
|
|
@ -0,0 +1,167 @@
|
||||||
|
use crate::html::flex::props_item::{
|
||||||
|
ItemAlign, ItemGrow, ItemOffset, ItemOrder, ItemShrink, ItemSize,
|
||||||
|
};
|
||||||
|
use crate::html::props::{Props, PropsOp};
|
||||||
|
use crate::{AutoDefault, Getters, builder_impl};
|
||||||
|
|
||||||
|
// **< FlexItem >***********************************************************************************
|
||||||
|
|
||||||
|
/// Configuración de un elemento como ítem de un contenedor 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
|
||||||
|
/// relación con el contenedor flex padre: crecimiento ([`ItemGrow`]), reducción ([`ItemShrink`]),
|
||||||
|
/// alineación individual ([`ItemAlign`]), orden visual ([`ItemOrder`]), ancho ([`ItemSize`]) y
|
||||||
|
/// desplazamiento ([`ItemOffset`]).
|
||||||
|
///
|
||||||
|
/// No tiene un builder dedicado en ningún componente. De hecho, no tendría sentido porque cualquier
|
||||||
|
/// componente puede acabar siendo hijo de un contenedor flex, y ninguno debería necesitar un campo
|
||||||
|
/// propio para esto. Se aplica con [`PropsOp::flex_item()`] sobre el `with_prop()` que suele
|
||||||
|
/// exponer cualquier componente.
|
||||||
|
///
|
||||||
|
/// 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.
|
||||||
|
///
|
||||||
|
/// # Ejemplo
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// use pagetop::prelude::*;
|
||||||
|
///
|
||||||
|
/// // Crece para ocupar el espacio sobrante, partiendo de ancho cero.
|
||||||
|
/// let title = Button::plain(Lc::n("Panel")).with_prop(PropsOp::flex_item(
|
||||||
|
/// FlexItem::new()
|
||||||
|
/// .with_grow(flex::ItemGrow::Is1)
|
||||||
|
/// .with_size(flex::ItemSize::Custom(UnitValue::Zero)),
|
||||||
|
/// ));
|
||||||
|
///
|
||||||
|
/// // Ocupa un tercio del ancho del contenedor, desplazado otro tercio desde el inicio.
|
||||||
|
/// let column = Container::new().with_prop(PropsOp::flex_item(
|
||||||
|
/// FlexItem::new()
|
||||||
|
/// .with_size(flex::ItemSize::Percent33)
|
||||||
|
/// .with_offset(flex::ItemOffset::Percent33),
|
||||||
|
/// ));
|
||||||
|
/// ```
|
||||||
|
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq, Getters)]
|
||||||
|
pub struct FlexItem {
|
||||||
|
/// Devuelve el factor de crecimiento.
|
||||||
|
#[getters(copy)]
|
||||||
|
grow: ItemGrow,
|
||||||
|
/// Devuelve el factor de reducción.
|
||||||
|
#[getters(copy)]
|
||||||
|
shrink: ItemShrink,
|
||||||
|
/// Devuelve la alineación individual en el eje transversal.
|
||||||
|
#[getters(copy)]
|
||||||
|
align_self: ItemAlign,
|
||||||
|
/// Devuelve la posición en el orden visual.
|
||||||
|
#[getters(copy)]
|
||||||
|
order: ItemOrder,
|
||||||
|
/// Devuelve el ancho como fracción del contenedor.
|
||||||
|
#[getters(copy)]
|
||||||
|
size: ItemSize,
|
||||||
|
/// Devuelve el desplazamiento respecto al inicio del contenedor.
|
||||||
|
#[getters(copy)]
|
||||||
|
offset: ItemOffset,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[builder_impl]
|
||||||
|
impl FlexItem {
|
||||||
|
/// Crea una configuración de ítem con todos los valores por defecto.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< FlexItem BUILDER >***********************************************************************
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
/// igual que el resto de facetas de `FlexItem`. Es el mecanismo estándar de Flexbox para, por
|
||||||
|
/// ejemplo, separar dos menús dentro de una misma [`Navbar`](crate::base::component::Navbar)
|
||||||
|
/// -- uno pegado al inicio, el siguiente empujado al final -- sin que el contenedor necesite
|
||||||
|
/// conocer ninguna distinción entre sus elementos.
|
||||||
|
///
|
||||||
|
/// No forma parte de los campos de `FlexItem` (no se combina con `grow`/`shrink`/`align_self`/
|
||||||
|
/// `order`/`size`/`offset` en una misma llamada): es una función asociada independiente porque
|
||||||
|
/// resuelve un caso de uso completo por sí sola, con una sola línea, y vive aquí -- en vez de
|
||||||
|
/// como función suelta del módulo `flex` -- para dejar claro que es una operación de **ítem**,
|
||||||
|
/// no de contenedor.
|
||||||
|
///
|
||||||
|
/// # Ejemplo
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// use pagetop::prelude::*;
|
||||||
|
///
|
||||||
|
/// let user_menu = Nav::new()
|
||||||
|
/// .with_prop(FlexItem::push_end())
|
||||||
|
/// .with_item(nav::Item::link(Lc::n("Profile"), "/profile"))
|
||||||
|
/// .with_item(nav::Item::link(Lc::n("Sign out"), "/sign-out"));
|
||||||
|
/// ```
|
||||||
|
pub fn push_end() -> PropsOp {
|
||||||
|
PropsOp::add_style("margin-inline-start", "auto")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<FlexItem> for PropsOp {
|
||||||
|
fn from(item: FlexItem) -> Self {
|
||||||
|
Self::flex_item(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
234
src/html/flex/props_container.rs
Normal file
234
src/html/flex/props_container.rs
Normal file
|
|
@ -0,0 +1,234 @@
|
||||||
|
//! Enums semánticos que configuran [`Flex`](super::Flex), a nivel de contenedor.
|
||||||
|
|
||||||
|
use crate::html::unit::UnitValue;
|
||||||
|
use crate::{AutoDefault, CowStr};
|
||||||
|
|
||||||
|
// **< Align >**************************************************************************************
|
||||||
|
|
||||||
|
/// Alineación de los elementos en el eje transversal de un contenedor [`Flex`](super::Flex).
|
||||||
|
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum Align {
|
||||||
|
/// Por defecto (`align-items: normal` no explícito), mismo efecto que [`Align::Stretch`], salvo
|
||||||
|
/// que el elemento tenga su propio tamaño.
|
||||||
|
#[default]
|
||||||
|
Default,
|
||||||
|
/// Alinea los elementos al inicio del eje transversal (`align-items: flex-start`).
|
||||||
|
Start,
|
||||||
|
/// Alinea los elementos al final del eje transversal (`align-items: flex-end`).
|
||||||
|
End,
|
||||||
|
/// Centra los elementos en el eje transversal (`align-items: center`).
|
||||||
|
Center,
|
||||||
|
/// Alinea los elementos por su línea base de texto (`align-items: baseline`).
|
||||||
|
Baseline,
|
||||||
|
/// Estira los elementos para ocupar todo el eje transversal (`align-items: stretch`).
|
||||||
|
Stretch,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Align {
|
||||||
|
// Devuelve el valor CSS de `align-items`, o "" para el valor por defecto.
|
||||||
|
pub(super) fn value(self) -> CowStr {
|
||||||
|
match self {
|
||||||
|
Self::Default => "".into(),
|
||||||
|
Self::Start => "flex-start".into(),
|
||||||
|
Self::End => "flex-end".into(),
|
||||||
|
Self::Center => "center".into(),
|
||||||
|
Self::Baseline => "baseline".into(),
|
||||||
|
Self::Stretch => "stretch".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< AlignContent >*******************************************************************************
|
||||||
|
|
||||||
|
/// Alineación de varias líneas en un contenedor [`Flex`](super::Flex).
|
||||||
|
///
|
||||||
|
/// Sólo tiene efecto si el contenedor usa [`Behavior::Wrap`] o [`Behavior::WrapReverse`] y genera
|
||||||
|
/// más de una línea de elementos.
|
||||||
|
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum AlignContent {
|
||||||
|
/// Por defecto (`align-content: normal` no explícito), como en [`AlignContent::Stretch`], las
|
||||||
|
/// líneas se estiran para ocupar el espacio sobrante del eje transversal, sin efecto visible si
|
||||||
|
/// el contenedor no tiene ningún espacio sobrante que repartir (p. ej. una altura `auto`
|
||||||
|
/// ajustada al contenido).
|
||||||
|
#[default]
|
||||||
|
Default,
|
||||||
|
/// Alinea las líneas al inicio del eje transversal (`align-content: flex-start`).
|
||||||
|
Start,
|
||||||
|
/// Alinea las líneas al final del eje transversal (`align-content: flex-end`).
|
||||||
|
End,
|
||||||
|
/// Centra las líneas en el eje transversal (`align-content: center`).
|
||||||
|
Center,
|
||||||
|
/// Reparte el espacio sobrante entre las líneas (`align-content: space-between`).
|
||||||
|
SpaceBetween,
|
||||||
|
/// Reparte el espacio sobrante alrededor de cada línea (`align-content: space-around`).
|
||||||
|
SpaceAround,
|
||||||
|
/// Reparte el espacio sobrante en partes iguales, incluidos los extremos
|
||||||
|
/// (`align-content: space-evenly`).
|
||||||
|
SpaceEvenly,
|
||||||
|
/// Estira las líneas para ocupar todo el eje transversal (`align-content: stretch`).
|
||||||
|
Stretch,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AlignContent {
|
||||||
|
// Devuelve el valor CSS de `align-content`, o "" para el valor por defecto.
|
||||||
|
pub(super) fn value(self) -> CowStr {
|
||||||
|
match self {
|
||||||
|
Self::Default => "".into(),
|
||||||
|
Self::Start => "flex-start".into(),
|
||||||
|
Self::End => "flex-end".into(),
|
||||||
|
Self::Center => "center".into(),
|
||||||
|
Self::SpaceBetween => "space-between".into(),
|
||||||
|
Self::SpaceAround => "space-around".into(),
|
||||||
|
Self::SpaceEvenly => "space-evenly".into(),
|
||||||
|
Self::Stretch => "stretch".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Behavior >***********************************************************************************
|
||||||
|
|
||||||
|
/// Comportamiento de los elementos si no caben en una línea del contenedor [`Flex`](super::Flex).
|
||||||
|
///
|
||||||
|
/// Si el contenedor aplica [`Gap`] y un [`ItemSize`](super::ItemSize) porcentual en los hijos,
|
||||||
|
/// entonces usar [`Behavior::Wrap`] en vez de [`Behavior::NoWrap`] (su valor por defecto) puede
|
||||||
|
/// provocar saltos de línea prematuros. En la sección "Cómo combinarlo con `Gap`" de `ItemSize`
|
||||||
|
/// se explica el porqué.
|
||||||
|
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum Behavior {
|
||||||
|
/// Por defecto, no se dividen en varias líneas: se comprimen o desbordan (`flex-wrap: nowrap`
|
||||||
|
/// no explícito).
|
||||||
|
#[default]
|
||||||
|
NoWrap,
|
||||||
|
/// Se dividen en varias líneas cuando no caben en una sola (`flex-wrap: wrap`).
|
||||||
|
Wrap,
|
||||||
|
/// Igual que [`Behavior::Wrap`], pero las líneas se apilan en orden inverso
|
||||||
|
/// (`flex-wrap: wrap-reverse`).
|
||||||
|
WrapReverse,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Behavior {
|
||||||
|
// Devuelve el valor CSS de `flex-wrap`, o "" para el valor por defecto.
|
||||||
|
pub(super) fn value(self) -> CowStr {
|
||||||
|
match self {
|
||||||
|
Self::NoWrap => "".into(),
|
||||||
|
Self::Wrap => "wrap".into(),
|
||||||
|
Self::WrapReverse => "wrap-reverse".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< ContentJustify >*****************************************************************************
|
||||||
|
|
||||||
|
/// Alineación de los elementos en el eje principal de un contenedor [`Flex`](super::Flex).
|
||||||
|
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum ContentJustify {
|
||||||
|
/// Por defecto, el navegador no fuerza ninguna alineación (`justify-content: normal` no
|
||||||
|
/// explícito).
|
||||||
|
#[default]
|
||||||
|
Default,
|
||||||
|
/// Alinea los elementos al inicio del eje principal (`justify-content: flex-start`).
|
||||||
|
Start,
|
||||||
|
/// Alinea los elementos al final del eje principal (`justify-content: flex-end`).
|
||||||
|
End,
|
||||||
|
/// Centra los elementos en el eje principal (`justify-content: center`).
|
||||||
|
Center,
|
||||||
|
/// Reparte el espacio sobrante entre los elementos (`justify-content: space-between`).
|
||||||
|
SpaceBetween,
|
||||||
|
/// Reparte el espacio sobrante alrededor de cada elemento (`justify-content: space-around`).
|
||||||
|
SpaceAround,
|
||||||
|
/// Reparte el espacio sobrante en partes iguales, incluidos los extremos
|
||||||
|
/// (`justify-content: space-evenly`).
|
||||||
|
SpaceEvenly,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContentJustify {
|
||||||
|
// Devuelve el valor CSS de `justify-content`, o "" para el valor por defecto.
|
||||||
|
pub(super) fn value(self) -> CowStr {
|
||||||
|
match self {
|
||||||
|
Self::Default => "".into(),
|
||||||
|
Self::Start => "flex-start".into(),
|
||||||
|
Self::End => "flex-end".into(),
|
||||||
|
Self::Center => "center".into(),
|
||||||
|
Self::SpaceBetween => "space-between".into(),
|
||||||
|
Self::SpaceAround => "space-around".into(),
|
||||||
|
Self::SpaceEvenly => "space-evenly".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Direction >**********************************************************************************
|
||||||
|
|
||||||
|
/// Dirección del eje principal de un contenedor [`Flex`](super::Flex).
|
||||||
|
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum Direction {
|
||||||
|
/// Por defecto, los elementos se disponen en fila, de izquierda a derecha
|
||||||
|
/// (`flex-direction: row` no explícito).
|
||||||
|
#[default]
|
||||||
|
Row,
|
||||||
|
/// Los elementos se disponen en fila, de derecha a izquierda (`flex-direction: row-reverse`).
|
||||||
|
RowReverse,
|
||||||
|
/// Los elementos se disponen en columna, de arriba abajo (`flex-direction: column`).
|
||||||
|
Column,
|
||||||
|
/// Los elementos se disponen en columna, de abajo arriba (`flex-direction: column-reverse`).
|
||||||
|
ColumnReverse,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Direction {
|
||||||
|
// Devuelve el valor CSS de `flex-direction`, o "" para el valor por defecto.
|
||||||
|
pub(super) fn value(self) -> CowStr {
|
||||||
|
match self {
|
||||||
|
Self::Row => "".into(),
|
||||||
|
Self::RowReverse => "row-reverse".into(),
|
||||||
|
Self::Column => "column".into(),
|
||||||
|
Self::ColumnReverse => "column-reverse".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Gap >****************************************************************************************
|
||||||
|
|
||||||
|
/// Espaciado entre los elementos de un contenedor [`Flex`](super::Flex).
|
||||||
|
///
|
||||||
|
/// Es un valor continuo, no una utilidad predefinida: se resuelve siempre como estilo
|
||||||
|
/// `gap`/`row-gap`/`column-gap` en línea, igual que el resto de facetas de
|
||||||
|
/// [`Flex`](super::Flex)/[`FlexItem`](super::FlexItem).
|
||||||
|
///
|
||||||
|
/// Si se combina con un [`ItemSize`](super::ItemSize) porcentual sobre los hijos, la sección "Cómo
|
||||||
|
/// combinarlo con `Gap`" de `ItemSize` explica cómo evitar que el hueco desborde el contenedor.
|
||||||
|
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum Gap {
|
||||||
|
/// Por defecto, no hay espaciado (`gap: normal` no explícito).
|
||||||
|
#[default]
|
||||||
|
None,
|
||||||
|
/// Mismo espaciado entre filas y columnas.
|
||||||
|
Both(UnitValue),
|
||||||
|
/// Espaciado distinto entre filas y columnas.
|
||||||
|
Distinct { row: UnitValue, column: UnitValue },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Gap {
|
||||||
|
// Declaraciones de estilo (propiedad, valor) para este espaciado; vacío si no hay ninguna
|
||||||
|
// medible (`UnitValue::None`/`UnitValue::Auto` no producen ningún estilo).
|
||||||
|
pub(super) fn styles(self) -> Vec<(&'static str, CowStr)> {
|
||||||
|
match self {
|
||||||
|
Self::None => Vec::new(),
|
||||||
|
Self::Both(value) => {
|
||||||
|
if value.is_measurable() {
|
||||||
|
vec![("gap", value.into())]
|
||||||
|
} else {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
304
src/html/flex/props_item.rs
Normal file
304
src/html/flex/props_item.rs
Normal file
|
|
@ -0,0 +1,304 @@
|
||||||
|
//! Enums semánticos que configuran [`FlexItem`](super::FlexItem), a nivel de ítem.
|
||||||
|
|
||||||
|
use crate::html::unit::UnitValue;
|
||||||
|
use crate::{AutoDefault, CowStr};
|
||||||
|
|
||||||
|
// **< ItemAlign >**********************************************************************************
|
||||||
|
|
||||||
|
/// Alineación en [`FlexItem`](super::FlexItem) para un ítem, sobrescribiendo la del contenedor.
|
||||||
|
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum ItemAlign {
|
||||||
|
/// Por defecto, hereda la alineación del contenedor (`align-self: auto` no explícito).
|
||||||
|
#[default]
|
||||||
|
Default,
|
||||||
|
/// Alinea el ítem al inicio del eje transversal (`align-self: flex-start`).
|
||||||
|
Start,
|
||||||
|
/// Alinea el ítem al final del eje transversal (`align-self: flex-end`).
|
||||||
|
End,
|
||||||
|
/// Centra el ítem en el eje transversal (`align-self: center`).
|
||||||
|
Center,
|
||||||
|
/// Alinea el ítem por su línea base de texto (`align-self: baseline`).
|
||||||
|
Baseline,
|
||||||
|
/// Estira el ítem para ocupar todo el eje transversal (`align-self: stretch`).
|
||||||
|
Stretch,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ItemAlign {
|
||||||
|
// Devuelve el valor CSS de `align-self`, o "" para el valor por defecto.
|
||||||
|
pub(super) fn value(self) -> CowStr {
|
||||||
|
match self {
|
||||||
|
Self::Default => "".into(),
|
||||||
|
Self::Start => "flex-start".into(),
|
||||||
|
Self::End => "flex-end".into(),
|
||||||
|
Self::Center => "center".into(),
|
||||||
|
Self::Baseline => "baseline".into(),
|
||||||
|
Self::Stretch => "stretch".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< ItemGrow >***********************************************************************************
|
||||||
|
|
||||||
|
/// Factor de crecimiento en [`FlexItem`](super::FlexItem) para un ítem dentro de un contenedor.
|
||||||
|
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum ItemGrow {
|
||||||
|
/// Por defecto, no crece más allá de su tamaño base (`flex-grow: 0` no explícito).
|
||||||
|
#[default]
|
||||||
|
Default,
|
||||||
|
/// Crece para ocupar el espacio sobrante (`flex-grow: 1`).
|
||||||
|
Is1,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ItemGrow {
|
||||||
|
// Devuelve el valor CSS de `flex-grow`, o "" para el valor por defecto.
|
||||||
|
pub(super) fn value(self) -> CowStr {
|
||||||
|
match self {
|
||||||
|
Self::Default => "".into(),
|
||||||
|
Self::Is1 => "1".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< ItemOffset >*********************************************************************************
|
||||||
|
|
||||||
|
/// Desplazamiento en [`FlexItem`](super::FlexItem) para un ítem respecto al inicio del contenedor.
|
||||||
|
///
|
||||||
|
/// Junto con [`ItemSize`], permite maquetar rejillas de columnas fijas sobre Flexbox. Un ítem con
|
||||||
|
/// [`ItemOffset::Percent33`] deja libre el primer tercio del contenedor antes de empezar. No tiene
|
||||||
|
/// relación con [`FlexItem::push_end()`](super::FlexItem::push_end). Ambos aplican la misma
|
||||||
|
/// propiedad CSS (`margin-inline-start`), pero para casos de uso distintos (un desplazamiento fijo
|
||||||
|
/// en fracción del contenedor, frente a "ocupa todo el espacio sobrante"); combinarlos no tiene
|
||||||
|
/// sentido, y si se aplican los dos, gana el último que se llame.
|
||||||
|
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum ItemOffset {
|
||||||
|
/// Por defecto, sin desplazamiento (`margin-inline-start: 0` no explícito).
|
||||||
|
#[default]
|
||||||
|
None,
|
||||||
|
/// Se desplaza el 10% del ancho del contenedor (`margin-inline-start: 10%`).
|
||||||
|
Percent10,
|
||||||
|
/// Se desplaza el 20% del ancho del contenedor (`margin-inline-start: 20%`).
|
||||||
|
Percent20,
|
||||||
|
/// Se desplaza el 25% del ancho del contenedor (`margin-inline-start: 25%`).
|
||||||
|
Percent25,
|
||||||
|
/// Se desplaza un tercio del ancho del contenedor (`margin-inline-start: 33.3333%`).
|
||||||
|
Percent33,
|
||||||
|
/// Se desplaza el 40% del ancho del contenedor (`margin-inline-start: 40%`).
|
||||||
|
Percent40,
|
||||||
|
/// Se desplaza la mitad del ancho del contenedor (`margin-inline-start: 50%`).
|
||||||
|
Percent50,
|
||||||
|
/// Se desplaza el 60% del ancho del contenedor (`margin-inline-start: 60%`).
|
||||||
|
Percent60,
|
||||||
|
/// Se desplaza dos tercios del ancho del contenedor (`margin-inline-start: 66.6667%`).
|
||||||
|
Percent66,
|
||||||
|
/// Se desplaza el 75% del ancho del contenedor (`margin-inline-start: 75%`).
|
||||||
|
Percent75,
|
||||||
|
/// Se desplaza el 80% del ancho del contenedor (`margin-inline-start: 80%`).
|
||||||
|
Percent80,
|
||||||
|
/// Se desplaza el 90% del ancho del contenedor (`margin-inline-start: 90%`).
|
||||||
|
Percent90,
|
||||||
|
/// Cualquier otro valor, incluidas unidades absolutas (p. ej. un desplazamiento fijo en
|
||||||
|
/// píxeles).
|
||||||
|
Custom(UnitValue),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ItemOffset {
|
||||||
|
// Devuelve el valor CSS de `margin-inline-start`, o cadena vacía para el valor por defecto.
|
||||||
|
pub(super) fn value(self) -> CowStr {
|
||||||
|
match self {
|
||||||
|
Self::None => "".into(),
|
||||||
|
Self::Percent10 => "10%".into(),
|
||||||
|
Self::Percent20 => "20%".into(),
|
||||||
|
Self::Percent25 => "25%".into(),
|
||||||
|
Self::Percent33 => "33.3333%".into(),
|
||||||
|
Self::Percent40 => "40%".into(),
|
||||||
|
Self::Percent50 => "50%".into(),
|
||||||
|
Self::Percent60 => "60%".into(),
|
||||||
|
Self::Percent66 => "66.6667%".into(),
|
||||||
|
Self::Percent75 => "75%".into(),
|
||||||
|
Self::Percent80 => "80%".into(),
|
||||||
|
Self::Percent90 => "90%".into(),
|
||||||
|
Self::Custom(value) => value.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< ItemOrder >**********************************************************************************
|
||||||
|
|
||||||
|
/// Posición en [`FlexItem`](super::FlexItem) para un ítem en el orden visual.
|
||||||
|
///
|
||||||
|
/// # Accesibilidad
|
||||||
|
///
|
||||||
|
/// Con `ItemOrder` se cambia únicamente el **orden visual**, no el orden del documento que siguen
|
||||||
|
/// la navegación por tabulador y los lectores de pantalla. Al reordenar con `ItemOrder` se puede
|
||||||
|
/// desalinear lo que se ve en pantalla de lo que se lee o se recorre con teclado, sin ningún aviso
|
||||||
|
/// del navegador.
|
||||||
|
///
|
||||||
|
/// Por eso se recomienda usar únicamente en reordenaciones puramente cosméticas, donde ese
|
||||||
|
/// desajuste no importe (p. ej. dos bloques intercambiables sin relación de lectura entre sí). Si
|
||||||
|
/// el orden tiene significado real, cambia el orden en el propio documento en lugar de maquillarlo
|
||||||
|
/// con `ItemOrder`.
|
||||||
|
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum ItemOrder {
|
||||||
|
/// Por defecto, el orden visual coincide con el del documento (`order: 0` no explícito).
|
||||||
|
#[default]
|
||||||
|
Default,
|
||||||
|
/// Se muestra antes que cualquier ítem, incluidos los que usan [`Self::Custom`]
|
||||||
|
/// (`order: -129`).
|
||||||
|
First,
|
||||||
|
/// Se muestra después de cualquier ítem, incluidos los que usan [`Self::Custom`]
|
||||||
|
/// (`order: 128`).
|
||||||
|
Last,
|
||||||
|
/// Posición `1` en el orden visual (`order: 1`).
|
||||||
|
Is1,
|
||||||
|
/// Posición `2` en el orden visual (`order: 2`).
|
||||||
|
Is2,
|
||||||
|
/// Posición `3` en el orden visual (`order: 3`).
|
||||||
|
Is3,
|
||||||
|
/// Posición `4` en el orden visual (`order: 4`).
|
||||||
|
Is4,
|
||||||
|
/// Posición `5` en el orden visual (`order: 5`).
|
||||||
|
Is5,
|
||||||
|
/// Cualquier otra posición no cubierta por `Default` (posición `0`) ni `Is1`..`Is5`.
|
||||||
|
Custom(i8),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ItemOrder {
|
||||||
|
// Devuelve el valor CSS de `order`, o "" para el valor por defecto. `First`/`Last` usan el
|
||||||
|
// primer entero fuera del rango de `Custom` (`i8::MIN - 1` / `i8::MAX + 1`), para quedar
|
||||||
|
// siempre antes o después de cualquier valor que éste pueda representar.
|
||||||
|
pub(super) fn value(self) -> CowStr {
|
||||||
|
match self {
|
||||||
|
Self::Default => "".into(),
|
||||||
|
Self::First => "-129".into(),
|
||||||
|
Self::Last => "128".into(),
|
||||||
|
Self::Is1 => "1".into(),
|
||||||
|
Self::Is2 => "2".into(),
|
||||||
|
Self::Is3 => "3".into(),
|
||||||
|
Self::Is4 => "4".into(),
|
||||||
|
Self::Is5 => "5".into(),
|
||||||
|
Self::Custom(value) => value.to_string().into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< ItemShrink >*********************************************************************************
|
||||||
|
|
||||||
|
/// Factor de reducción en [`FlexItem`](super::FlexItem) para un ítem dentro de un contenedor.
|
||||||
|
///
|
||||||
|
/// # Cuándo usar `Is0`
|
||||||
|
///
|
||||||
|
/// Para un tamaño fijo ([`ItemSize::Custom`]) es la opción natural. Un icono, un avatar o una barra
|
||||||
|
/// lateral con un ancho fijo definido por diseño no debe deformarse si falta espacio, que sea otro
|
||||||
|
/// elemento el que ceda (uno con [`ItemGrow::Is1`] y contenido que sí admita reajuste, como texto),
|
||||||
|
/// no éste.
|
||||||
|
///
|
||||||
|
/// Con [`ItemSize`] en porcentaje, `Is0` es seguro si el contenedor no tiene [`Gap`](super::Gap)
|
||||||
|
/// (sin `gap` no hay nada que compensar). Pero **si el contenedor tiene `Gap`, no combines `Is0`
|
||||||
|
/// con un tamaño porcentual** porque desactivas la única pieza (el reparto del espacio negativo
|
||||||
|
/// entre elementos) que compensa el hueco por ti. La explicación completa está en [`ItemSize`].
|
||||||
|
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum ItemShrink {
|
||||||
|
/// Por defecto, puede encoger si hace falta (`flex-shrink: 1` no explícito).
|
||||||
|
#[default]
|
||||||
|
Default,
|
||||||
|
/// No encoge nunca, aunque no quepa en el contenedor (`flex-shrink: 0`).
|
||||||
|
Is0,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ItemShrink {
|
||||||
|
// Devuelve el valor CSS de `flex-shrink`, o "" para el valor por defecto.
|
||||||
|
pub(super) fn value(self) -> CowStr {
|
||||||
|
match self {
|
||||||
|
Self::Default => "".into(),
|
||||||
|
Self::Is0 => "0".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< ItemSize >***********************************************************************************
|
||||||
|
|
||||||
|
/// Ancho en [`FlexItem`](super::FlexItem) para un ítem como fracción del contenedor.
|
||||||
|
///
|
||||||
|
/// Permite maquetar rejillas de columnas fijas. Un ítem con [`ItemSize::Percent33`] ocupa un tercio
|
||||||
|
/// del ancho del contenedor con independencia de su contenido.
|
||||||
|
///
|
||||||
|
/// # Cómo combinarlo con `Gap`
|
||||||
|
///
|
||||||
|
/// Un porcentaje se resuelve contra el ancho del contenedor sin contar el espacio que va a ocupar
|
||||||
|
/// el [`Gap`](super::Gap). Es una limitación del propio CSS, porque `flex-basis` en porcentaje usa
|
||||||
|
/// la misma regla de resolución que cualquier `width: %`. Si los porcentajes de una fila suman el
|
||||||
|
/// 100% (una rejilla completa, el caso habitual), el hueco que añade `gap` sobra respecto al ancho
|
||||||
|
/// del contenedor.
|
||||||
|
///
|
||||||
|
/// Ese sobrante se compensa solo, sin ningún ajuste manual, siempre que:
|
||||||
|
///
|
||||||
|
/// - **No se fuerce [`ItemShrink::Is0`]** en los ítems de esa fila. Déjalos en su valor por
|
||||||
|
/// defecto, [`ItemShrink::Default`](super::ItemShrink::Default). El reparto por defecto del
|
||||||
|
/// espacio negativo entre elementos, proporcional al tamaño de partida de cada uno, reproduce
|
||||||
|
/// exactamente el resultado de restar el `gap` antes de repartir. Forzar `ItemShrink::Is0`
|
||||||
|
/// desactiva esa compensación y el hueco sobrante pasa a desbordar de verdad.
|
||||||
|
/// - **El contenedor use [`Behavior::NoWrap`](super::Behavior::NoWrap)** (su valor por defecto).
|
||||||
|
/// Con [`Behavior::Wrap`](super::Behavior::Wrap) el navegador decide si rompe la línea a partir
|
||||||
|
/// de los tamaños *antes* de aplicar el `shrink`, así que una fila que encajaría perfectamente en
|
||||||
|
/// una sola línea puede saltar de línea antes de que la compensación llegue a actuar. Combinar
|
||||||
|
/// `ItemSize` porcentual, `Gap` y ajuste de línea sigue siendo el caso sin resolver porque no hay
|
||||||
|
/// compensación automática posible cuando distintas líneas acaban con un número distinto de
|
||||||
|
/// elementos.
|
||||||
|
///
|
||||||
|
/// Con un tamaño fijo ([`ItemSize::Custom`]) ninguna de estas condiciones aplica: un `gap` nunca
|
||||||
|
/// sorprende a un tamaño que no dependía de un porcentaje del contenedor, así que ahí
|
||||||
|
/// `ItemShrink::Is0` es siempre seguro (ver [`ItemShrink`]).
|
||||||
|
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum ItemSize {
|
||||||
|
/// Por defecto, el tamaño se calcula según el contenido (`flex-basis: auto` no explícito).
|
||||||
|
#[default]
|
||||||
|
Default,
|
||||||
|
/// Ocupa el 10% del ancho del contenedor (`flex-basis: 10%`).
|
||||||
|
Percent10,
|
||||||
|
/// Ocupa el 20% del ancho del contenedor (`flex-basis: 20%`).
|
||||||
|
Percent20,
|
||||||
|
/// Ocupa el 25% del ancho del contenedor (`flex-basis: 25%`).
|
||||||
|
Percent25,
|
||||||
|
/// Ocupa un tercio del ancho del contenedor (`flex-basis: 33.3333%`).
|
||||||
|
Percent33,
|
||||||
|
/// Ocupa el 40% del ancho del contenedor (`flex-basis: 40%`).
|
||||||
|
Percent40,
|
||||||
|
/// Ocupa la mitad del ancho del contenedor (`flex-basis: 50%`).
|
||||||
|
Percent50,
|
||||||
|
/// Ocupa el 60% del ancho del contenedor (`flex-basis: 60%`).
|
||||||
|
Percent60,
|
||||||
|
/// Ocupa dos tercios del ancho del contenedor (`flex-basis: 66.6667%`).
|
||||||
|
Percent66,
|
||||||
|
/// Ocupa el 75% del ancho del contenedor (`flex-basis: 75%`).
|
||||||
|
Percent75,
|
||||||
|
/// Ocupa el 80% del ancho del contenedor (`flex-basis: 80%`).
|
||||||
|
Percent80,
|
||||||
|
/// Ocupa el 90% del ancho del contenedor (`flex-basis: 90%`).
|
||||||
|
Percent90,
|
||||||
|
/// Ocupa el 100% del ancho del contenedor (`flex-basis: 100%`).
|
||||||
|
Percent100,
|
||||||
|
/// Cualquier otro valor, incluidas unidades absolutas (p. ej. un ancho fijo en píxeles).
|
||||||
|
Custom(UnitValue),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ItemSize {
|
||||||
|
// Devuelve el valor CSS de `flex-basis`, o "" para el valor por defecto.
|
||||||
|
pub(super) fn value(self) -> CowStr {
|
||||||
|
match self {
|
||||||
|
Self::Default => "".into(),
|
||||||
|
Self::Percent10 => "10%".into(),
|
||||||
|
Self::Percent20 => "20%".into(),
|
||||||
|
Self::Percent25 => "25%".into(),
|
||||||
|
Self::Percent33 => "33.3333%".into(),
|
||||||
|
Self::Percent40 => "40%".into(),
|
||||||
|
Self::Percent50 => "50%".into(),
|
||||||
|
Self::Percent60 => "60%".into(),
|
||||||
|
Self::Percent66 => "66.6667%".into(),
|
||||||
|
Self::Percent75 => "75%".into(),
|
||||||
|
Self::Percent80 => "80%".into(),
|
||||||
|
Self::Percent90 => "90%".into(),
|
||||||
|
Self::Percent100 => "100%".into(),
|
||||||
|
Self::Custom(value) => value.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::core::TypeInfo;
|
use crate::core::TypeInfo;
|
||||||
|
use crate::html::flex::FlexItem;
|
||||||
use crate::html::maud::{Escaper, RenderAttrs};
|
use crate::html::maud::{Escaper, RenderAttrs};
|
||||||
use crate::{AutoDefault, CowStr, builder_impl, trace, util};
|
use crate::{AutoDefault, CowStr, builder_impl, trace, util};
|
||||||
|
|
||||||
|
|
@ -89,6 +90,9 @@ pub enum PropsError {
|
||||||
/// estructura de un componente ya definido, temas y extensiones pueden definir un trait con nuevos
|
/// estructura de un componente ya definido, temas y extensiones pueden definir un trait con nuevos
|
||||||
/// métodos que leen y escriben valores extra en [`Props`]. Esos valores se interpretan como si
|
/// métodos que leen y escriben valores extra en [`Props`]. Esos valores se interpretan como si
|
||||||
/// fueran valores internos del componente para tomar decisiones durante el renderizado.
|
/// fueran valores internos del componente para tomar decisiones durante el renderizado.
|
||||||
|
///
|
||||||
|
/// Finalmente, [`FlexItem`](Self::FlexItem) aplica un posicionamiento Flexbox a nivel de ítem sobre
|
||||||
|
/// cualquier componente.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum PropsOp {
|
pub enum PropsOp {
|
||||||
/// Establece el identificador del componente normalizando el valor: recorta espacios, convierte
|
/// Establece el identificador del componente normalizando el valor: recorta espacios, convierte
|
||||||
|
|
@ -159,6 +163,22 @@ pub enum PropsOp {
|
||||||
SetExtra(&'static str, PropsExtra),
|
SetExtra(&'static str, PropsExtra),
|
||||||
/// Elimina el valor extra asociado a la clave indicada, si existe.
|
/// Elimina el valor extra asociado a la clave indicada, si existe.
|
||||||
RemoveExtra(&'static str),
|
RemoveExtra(&'static str),
|
||||||
|
/// Aplica un posicionamiento [`FlexItem`] a un componente particular en un contenedor [`Flex`].
|
||||||
|
/// Añade directamente sus estilos Flexbox al propio componente, sin usar clases CSS.
|
||||||
|
///
|
||||||
|
/// Existe como variante de `PropsOp`, y no como método builder de un componente, porque las
|
||||||
|
/// propiedades de un ítem Flexbox tienen sentido sobre **cualquier** componente que pueda
|
||||||
|
/// añadirse como hijo de un contenedor Flex (por ejemplo `Button`, `Nav`, un componente de
|
||||||
|
/// terceros, incluso otro componente que sea, a su vez, un contenedor Flex para sus propios
|
||||||
|
/// hijos).
|
||||||
|
///
|
||||||
|
/// No existe una variante equivalente `PropsOp::Flex` para el componente contenedor. No hace
|
||||||
|
/// falta porque los componentes contenedores, como `Container` o `Navbar`, ofrecen su propio
|
||||||
|
/// `with_flex()` tipado y con introspección (p. ej. [`Container::flex()`]).
|
||||||
|
///
|
||||||
|
/// [`Flex`]: crate::html::flex::Flex
|
||||||
|
/// [`Container::flex()`]: crate::base::component::Container::flex
|
||||||
|
FlexItem(FlexItem),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PropsOp {
|
impl PropsOp {
|
||||||
|
|
@ -295,6 +315,11 @@ impl PropsOp {
|
||||||
pub fn remove_extra(key: &'static str) -> Self {
|
pub fn remove_extra(key: &'static str) -> Self {
|
||||||
Self::RemoveExtra(key)
|
Self::RemoveExtra(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Crea la variante [`FlexItem`](Self::FlexItem) con el posicionamiento indicado.
|
||||||
|
pub fn flex_item(placement: FlexItem) -> Self {
|
||||||
|
Self::FlexItem(placement)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// **< Props >**************************************************************************************
|
// **< Props >**************************************************************************************
|
||||||
|
|
@ -627,6 +652,9 @@ impl Props {
|
||||||
PropsOp::RemoveExtra(key) => {
|
PropsOp::RemoveExtra(key) => {
|
||||||
self.extras.remove(key);
|
self.extras.remove(key);
|
||||||
}
|
}
|
||||||
|
PropsOp::FlexItem(placement) => {
|
||||||
|
placement.apply_to(self);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::{AutoDefault, util};
|
use crate::{AutoDefault, CowStr, util};
|
||||||
|
|
||||||
use serde::{Deserialize, Deserializer};
|
use serde::{Deserialize, Deserializer};
|
||||||
|
|
||||||
|
|
@ -42,14 +42,12 @@ use std::str::FromStr;
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
/// use std::str::FromStr;
|
/// assert_eq!(Ok(UnitValue::Px(16)), "16px".parse());
|
||||||
///
|
/// assert_eq!(Ok(UnitValue::RelRem(1.25)), "1.25rem".parse());
|
||||||
/// assert_eq!(UnitValue::from_str("16px").unwrap(), UnitValue::Px(16));
|
/// assert_eq!(Ok(UnitValue::RelPct(33.0)), "33%".parse());
|
||||||
/// assert_eq!(UnitValue::from_str("1.25rem").unwrap(), UnitValue::RelRem(1.25));
|
/// assert_eq!(Ok(UnitValue::Auto), "auto".parse());
|
||||||
/// assert_eq!(UnitValue::from_str("33%").unwrap(), UnitValue::RelPct(33.0));
|
/// assert_eq!(Ok(UnitValue::None), "".parse());
|
||||||
/// assert_eq!(UnitValue::from_str("auto").unwrap(), UnitValue::Auto);
|
/// assert_eq!(Ok(UnitValue::Zero), "0".parse());
|
||||||
/// assert_eq!(UnitValue::from_str("").unwrap(), UnitValue::None);
|
|
||||||
/// assert_eq!(UnitValue::from_str("0").unwrap(), UnitValue::Zero);
|
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// # Notas
|
/// # Notas
|
||||||
|
|
@ -165,6 +163,14 @@ impl fmt::Display for UnitValue {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<UnitValue> for CowStr {
|
||||||
|
/// Delega en `Display`; siempre produce un `Cow::Owned`, porque `to_string()` reserva un
|
||||||
|
/// `String` nuevo con independencia del contenido.
|
||||||
|
fn from(value: UnitValue) -> Self {
|
||||||
|
value.to_string().into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Convierte una cadena a [`UnitValue`] siguiendo una gramática CSS acotada.
|
/// Convierte una cadena a [`UnitValue`] siguiendo una gramática CSS acotada.
|
||||||
///
|
///
|
||||||
/// # Acepta
|
/// # Acepta
|
||||||
|
|
@ -182,10 +188,8 @@ impl fmt::Display for UnitValue {
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
/// use std::str::FromStr;
|
/// assert_eq!("12px".parse(), Ok(UnitValue::Px(12)));
|
||||||
///
|
/// assert!("12".parse::<UnitValue>().is_err());
|
||||||
/// assert_eq!(UnitValue::from_str("12px").unwrap(), UnitValue::Px(12));
|
|
||||||
/// assert!(UnitValue::from_str("12").is_err());
|
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// # Errores de interpretación
|
/// # Errores de interpretación
|
||||||
|
|
|
||||||
|
|
@ -142,58 +142,3 @@ async fn disabled_anchor_omits_href_and_sets_aria_disabled() {
|
||||||
assert!(html.contains(r#"aria-disabled="true""#));
|
assert!(html.contains(r#"aria-disabled="true""#));
|
||||||
assert!(html.contains(r#"tabindex="-1""#));
|
assert!(html.contains(r#"tabindex="-1""#));
|
||||||
}
|
}
|
||||||
|
|
||||||
// **< ButtonSet >**********************************************************************************
|
|
||||||
|
|
||||||
#[pagetop::test]
|
|
||||||
async fn button_set_is_not_rendered_when_empty() {
|
|
||||||
let mut set = button::ButtonSet::new();
|
|
||||||
let html = set.render(&mut Context::default()).await;
|
|
||||||
|
|
||||||
assert!(html.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pagetop::test]
|
|
||||||
async fn button_set_wraps_buttons_in_button_set_class() {
|
|
||||||
let mut set = button::ButtonSet::new().with_button(Button::submit(Lc::n("Save")));
|
|
||||||
let html = set.render(&mut Context::default()).await.into_string();
|
|
||||||
|
|
||||||
assert!(html.contains("button-set"));
|
|
||||||
assert!(html.contains("Save"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pagetop::test]
|
|
||||||
async fn button_set_renders_buttons_in_insertion_order() {
|
|
||||||
let mut set = button::ButtonSet::new()
|
|
||||||
.with_button(Button::submit(Lc::n("First")))
|
|
||||||
.with_button(Button::plain(Lc::n("Second")));
|
|
||||||
let html = set.render(&mut Context::default()).await.into_string();
|
|
||||||
|
|
||||||
assert!(html.find("First").unwrap() < html.find("Second").unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pagetop::test]
|
|
||||||
async fn button_set_add_many_appends_all_buttons() {
|
|
||||||
let mut set = button::ButtonSet::new().with_button(TypedOp::AddMany(vec![
|
|
||||||
Button::submit(Lc::n("Save")),
|
|
||||||
Button::reset(Lc::n("Reset")),
|
|
||||||
Button::plain(Lc::n("Cancel")),
|
|
||||||
]));
|
|
||||||
let html = set.render(&mut Context::default()).await.into_string();
|
|
||||||
|
|
||||||
assert!(html.contains("Save"));
|
|
||||||
assert!(html.contains("Reset"));
|
|
||||||
assert!(html.contains("Cancel"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pagetop::test]
|
|
||||||
async fn button_set_remove_by_id_drops_matching_button() {
|
|
||||||
let mut set = button::ButtonSet::new()
|
|
||||||
.with_button(Button::submit(Lc::n("Save")).with_id("save-button"))
|
|
||||||
.with_button(Button::plain(Lc::n("Cancel")))
|
|
||||||
.with_button(TypedOp::RemoveById("save-button"));
|
|
||||||
let html = set.render(&mut Context::default()).await.into_string();
|
|
||||||
|
|
||||||
assert!(!html.contains("Save"));
|
|
||||||
assert!(html.contains("Cancel"));
|
|
||||||
}
|
|
||||||
|
|
|
||||||
140
tests/component_container.rs
Normal file
140
tests/component_container.rs
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
// **< Container >**********************************************************************************
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn is_not_rendered_when_empty() {
|
||||||
|
let mut container = Container::new();
|
||||||
|
let html = container.render(&mut Context::default()).await;
|
||||||
|
|
||||||
|
assert!(html.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn default_kind_renders_a_div_element() {
|
||||||
|
let mut container = Container::new().with_child(Lc::n("x"));
|
||||||
|
let html = container
|
||||||
|
.render(&mut Context::default())
|
||||||
|
.await
|
||||||
|
.into_string();
|
||||||
|
|
||||||
|
assert!(html.starts_with("<div"));
|
||||||
|
assert!(html.ends_with("</div>"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn main_kind_renders_a_main_element() {
|
||||||
|
let mut container = Container::main().with_child(Lc::n("x"));
|
||||||
|
let html = container
|
||||||
|
.render(&mut Context::default())
|
||||||
|
.await
|
||||||
|
.into_string();
|
||||||
|
|
||||||
|
assert!(html.starts_with("<main"));
|
||||||
|
assert!(html.ends_with("</main>"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Container + Flex >***************************************************************************
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn without_flex_no_style_attribute_is_added() {
|
||||||
|
let mut container = Container::new().with_child(Lc::n("x"));
|
||||||
|
let html = container
|
||||||
|
.render(&mut Context::default())
|
||||||
|
.await
|
||||||
|
.into_string();
|
||||||
|
|
||||||
|
assert!(!html.contains("style="));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn default_flex_adds_only_display_flex() {
|
||||||
|
let mut container = Container::new()
|
||||||
|
.with_flex(Flex::row())
|
||||||
|
.with_child(Lc::n("x"));
|
||||||
|
let html = container
|
||||||
|
.render(&mut Context::default())
|
||||||
|
.await
|
||||||
|
.into_string();
|
||||||
|
|
||||||
|
assert!(html.contains(r#"style="display: flex""#));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn column_direction_adds_flex_direction_style() {
|
||||||
|
let mut container = Container::new()
|
||||||
|
.with_flex(Flex::column())
|
||||||
|
.with_child(Lc::n("x"));
|
||||||
|
let html = container
|
||||||
|
.render(&mut Context::default())
|
||||||
|
.await
|
||||||
|
.into_string();
|
||||||
|
|
||||||
|
assert!(html.contains("display: flex"));
|
||||||
|
assert!(html.contains("flex-direction: column"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn wrap_justify_and_align_add_their_matching_styles() {
|
||||||
|
let mut container = Container::new()
|
||||||
|
.with_flex(
|
||||||
|
Flex::row()
|
||||||
|
.with_wrap(flex::Behavior::Wrap)
|
||||||
|
.with_justify(flex::ContentJustify::Center)
|
||||||
|
.with_align(flex::Align::Center)
|
||||||
|
.with_align_content(flex::AlignContent::SpaceBetween),
|
||||||
|
)
|
||||||
|
.with_child(Lc::n("x"));
|
||||||
|
let html = container
|
||||||
|
.render(&mut Context::default())
|
||||||
|
.await
|
||||||
|
.into_string();
|
||||||
|
|
||||||
|
assert!(html.contains("flex-wrap: wrap"));
|
||||||
|
assert!(html.contains("justify-content: center"));
|
||||||
|
assert!(html.contains("align-items: center"));
|
||||||
|
assert!(html.contains("align-content: space-between"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn gap_both_adds_a_single_gap_style() {
|
||||||
|
let mut container = Container::new()
|
||||||
|
.with_flex(Flex::row().with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))))
|
||||||
|
.with_child(Lc::n("x"));
|
||||||
|
let html = container
|
||||||
|
.render(&mut Context::default())
|
||||||
|
.await
|
||||||
|
.into_string();
|
||||||
|
|
||||||
|
assert!(html.contains("gap: 0.5rem"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn gap_distinct_adds_row_and_column_gap_styles() {
|
||||||
|
let mut container = Container::new()
|
||||||
|
.with_flex(Flex::row().with_gap(flex::Gap::Distinct {
|
||||||
|
row: UnitValue::Px(4),
|
||||||
|
column: UnitValue::Px(8),
|
||||||
|
}))
|
||||||
|
.with_child(Lc::n("x"));
|
||||||
|
let html = container
|
||||||
|
.render(&mut Context::default())
|
||||||
|
.await
|
||||||
|
.into_string();
|
||||||
|
|
||||||
|
assert!(html.contains("row-gap: 4px"));
|
||||||
|
assert!(html.contains("column-gap: 8px"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn gap_none_adds_no_gap_style() {
|
||||||
|
let mut container = Container::new()
|
||||||
|
.with_flex(Flex::row())
|
||||||
|
.with_child(Lc::n("x"));
|
||||||
|
let html = container
|
||||||
|
.render(&mut Context::default())
|
||||||
|
.await
|
||||||
|
.into_string();
|
||||||
|
|
||||||
|
assert!(!html.contains("gap:"));
|
||||||
|
}
|
||||||
68
tests/component_navbar.rs
Normal file
68
tests/component_navbar.rs
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
fn one_link_nav() -> Nav {
|
||||||
|
Nav::new().with_item(nav::Item::link(Lc::n("Home"), "/"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Navbar >*************************************************************************************
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn is_not_rendered_when_empty() {
|
||||||
|
let mut navbar = Navbar::simple();
|
||||||
|
let html = navbar.render(&mut Context::default()).await;
|
||||||
|
|
||||||
|
assert!(html.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn nav_root_class_is_unaffected_by_content_flex() {
|
||||||
|
let mut navbar = Navbar::simple()
|
||||||
|
.with_flex(Flex::row().with_justify(flex::ContentJustify::End))
|
||||||
|
.with_item(navbar::Item::nav(one_link_nav()));
|
||||||
|
let html = navbar.render(&mut Context::default()).await.into_string();
|
||||||
|
|
||||||
|
assert!(html.contains(r#"class="navbar""#));
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Navbar + Flex (content area) >***************************************************************
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn without_flex_content_area_has_no_style_attribute() {
|
||||||
|
let mut navbar = Navbar::simple().with_item(navbar::Item::nav(one_link_nav()));
|
||||||
|
let html = navbar.render(&mut Context::default()).await.into_string();
|
||||||
|
|
||||||
|
assert!(html.contains(r#"class="navbar-content""#));
|
||||||
|
assert!(!html.contains("style="));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn flex_adds_its_styles_to_the_content_area() {
|
||||||
|
let mut navbar = Navbar::simple()
|
||||||
|
.with_flex(Flex::row().with_justify(flex::ContentJustify::End))
|
||||||
|
.with_item(navbar::Item::nav(one_link_nav()));
|
||||||
|
let html = navbar.render(&mut Context::default()).await.into_string();
|
||||||
|
|
||||||
|
assert!(html.contains(r#"class="navbar-content""#));
|
||||||
|
assert!(html.contains("display: flex"));
|
||||||
|
assert!(html.contains("justify-content: flex-end"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn flex_gap_adds_a_style_to_the_content_area() {
|
||||||
|
let mut navbar = Navbar::simple()
|
||||||
|
.with_flex(Flex::row().with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))))
|
||||||
|
.with_item(navbar::Item::nav(one_link_nav()));
|
||||||
|
let html = navbar.render(&mut Context::default()).await.into_string();
|
||||||
|
|
||||||
|
assert!(html.contains("gap: 0.5rem"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Navbar + FlexItem::push_end >****************************************************************
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn push_end_adds_an_automatic_start_margin() {
|
||||||
|
let mut nav = one_link_nav().with_prop(FlexItem::push_end());
|
||||||
|
let html = nav.render(&mut Context::default()).await.into_string();
|
||||||
|
|
||||||
|
assert!(html.contains(r#"style="margin-inline-start: auto""#));
|
||||||
|
}
|
||||||
119
tests/html_props_flex_item.rs
Normal file
119
tests/html_props_flex_item.rs
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn default_flex_item_adds_nothing() {
|
||||||
|
let props = Props::default().with_prop(PropsOp::flex_item(FlexItem::new()));
|
||||||
|
|
||||||
|
assert_eq!(props.get_classes(), None);
|
||||||
|
assert_eq!(props.get_styles(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn grow_adds_flex_grow_style() {
|
||||||
|
let props = Props::default().with_prop(PropsOp::flex_item(
|
||||||
|
FlexItem::new().with_grow(flex::ItemGrow::Is1),
|
||||||
|
));
|
||||||
|
|
||||||
|
assert_eq!(props.get_styles(), Some("flex-grow: 1".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn shrink_adds_flex_shrink_style() {
|
||||||
|
let props = Props::default().with_prop(PropsOp::flex_item(
|
||||||
|
FlexItem::new().with_shrink(flex::ItemShrink::Is0),
|
||||||
|
));
|
||||||
|
|
||||||
|
assert_eq!(props.get_styles(), Some("flex-shrink: 0".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn align_self_adds_matching_style() {
|
||||||
|
let props = Props::default().with_prop(PropsOp::flex_item(
|
||||||
|
FlexItem::new().with_align_self(flex::ItemAlign::Center),
|
||||||
|
));
|
||||||
|
|
||||||
|
assert_eq!(props.get_styles(), Some("align-self: center".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn order_adds_matching_style() {
|
||||||
|
let props = Props::default().with_prop(PropsOp::flex_item(
|
||||||
|
FlexItem::new().with_order(flex::ItemOrder::First),
|
||||||
|
));
|
||||||
|
|
||||||
|
assert_eq!(props.get_styles(), Some("order: -129".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn size_percent_adds_flex_basis_style() {
|
||||||
|
let props = Props::default().with_prop(PropsOp::flex_item(
|
||||||
|
FlexItem::new().with_size(flex::ItemSize::Percent33),
|
||||||
|
));
|
||||||
|
|
||||||
|
assert_eq!(props.get_styles(), Some("flex-basis: 33.3333%".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn size_custom_adds_flex_basis_style() {
|
||||||
|
let props = Props::default().with_prop(PropsOp::flex_item(
|
||||||
|
FlexItem::new().with_size(flex::ItemSize::Custom(UnitValue::Zero)),
|
||||||
|
));
|
||||||
|
|
||||||
|
assert_eq!(props.get_classes(), None);
|
||||||
|
assert_eq!(props.get_styles(), Some("flex-basis: 0".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn offset_percent_adds_margin_inline_start_style() {
|
||||||
|
let props = Props::default().with_prop(PropsOp::flex_item(
|
||||||
|
FlexItem::new().with_offset(flex::ItemOffset::Percent33),
|
||||||
|
));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
props.get_styles(),
|
||||||
|
Some("margin-inline-start: 33.3333%".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn offset_custom_adds_margin_inline_start_style() {
|
||||||
|
let props = Props::default().with_prop(PropsOp::flex_item(
|
||||||
|
FlexItem::new().with_offset(flex::ItemOffset::Custom(UnitValue::Px(16))),
|
||||||
|
));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
props.get_styles(),
|
||||||
|
Some("margin-inline-start: 16px".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn combines_several_facets_in_one_call() {
|
||||||
|
let props = Props::default().with_prop(PropsOp::flex_item(
|
||||||
|
FlexItem::new()
|
||||||
|
.with_grow(flex::ItemGrow::Is1)
|
||||||
|
.with_shrink(flex::ItemShrink::Is0)
|
||||||
|
.with_align_self(flex::ItemAlign::Start)
|
||||||
|
.with_order(flex::ItemOrder::Is2)
|
||||||
|
.with_size(flex::ItemSize::Custom(UnitValue::Zero))
|
||||||
|
.with_offset(flex::ItemOffset::Percent10),
|
||||||
|
));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
props.get_styles(),
|
||||||
|
Some(
|
||||||
|
"flex-grow: 1; flex-shrink: 0; align-self: flex-start; order: 2; flex-basis: 0; \
|
||||||
|
margin-inline-start: 10%"
|
||||||
|
.to_string()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert_eq!(props.get_classes(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn from_flex_item_for_props_op() {
|
||||||
|
let item = FlexItem::new().with_grow(flex::ItemGrow::Is1);
|
||||||
|
let props = Props::default().with_prop(item.into());
|
||||||
|
|
||||||
|
assert_eq!(props.get_styles(), Some("flex-grow: 1".to_string()));
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue