From 17e16652e49a38ebf37018e5a7d7656a8195fc24 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Fri, 4 Sep 2026 01:04:53 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20(pagetop):=20A=C3=B1ade=20Flex/Flex?= =?UTF-8?q?Item=20para=20usar=20Flexbox?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- examples/form-controls.rs | 39 +- examples/intro-flex.rs | 540 ++++++++++++++++++ examples/locale/en-US/intro-flex.ftl | 63 ++ examples/locale/es-ES/intro-flex.ftl | 63 ++ examples/navbar-menus.rs | 6 +- extensions/pagetop-bootsier/src/theme/bs.rs | 2 +- .../pagetop-bootsier/src/theme/bs/button.rs | 4 +- .../pagetop-user/src/handlers/admin/users.rs | 49 +- src/base/component.rs | 2 +- src/base/component/button.rs | 5 +- src/base/component/button/set.rs | 71 --- src/base/component/container.rs | 15 + src/base/component/dialog.rs | 3 +- src/base/component/navbar/component.rs | 29 +- src/html.rs | 6 + src/html/flex.rs | 37 ++ src/html/flex/container.rs | 137 +++++ src/html/flex/item.rs | 167 ++++++ src/html/flex/props_container.rs | 234 ++++++++ src/html/flex/props_item.rs | 304 ++++++++++ src/html/props.rs | 28 + src/html/unit.rs | 30 +- tests/component_button.rs | 55 -- tests/component_container.rs | 140 +++++ tests/component_navbar.rs | 68 +++ tests/html_props_flex_item.rs | 119 ++++ 26 files changed, 2023 insertions(+), 193 deletions(-) create mode 100644 examples/intro-flex.rs create mode 100644 examples/locale/en-US/intro-flex.ftl create mode 100644 examples/locale/es-ES/intro-flex.ftl delete mode 100644 src/base/component/button/set.rs create mode 100644 src/html/flex.rs create mode 100644 src/html/flex/container.rs create mode 100644 src/html/flex/item.rs create mode 100644 src/html/flex/props_container.rs create mode 100644 src/html/flex/props_item.rs create mode 100644 tests/component_container.rs create mode 100644 tests/component_navbar.rs create mode 100644 tests/html_props_flex_item.rs diff --git a/examples/form-controls.rs b/examples/form-controls.rs index 8273f207..350efd37 100644 --- a/examples/form-controls.rs +++ b/examples/form-controls.rs @@ -140,17 +140,22 @@ async fn form_controls(request: HttpRequest) -> Result { .with_child(form::Hidden::field("origin", "form-selections")) // Botonera de acciones. .with_child( - button::ButtonSet::new() - .with_button( + Container::new() + .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)) .with_style(button::Style::Solid(Intent::Primary)), ) - .with_button( + .with_child( Button::reset(Lc::t("btn_reset", &LOC)).with_style( button::Style::Outline(Intent::Neutral), ), ) - .with_button( + .with_child( Button::plain(Lc::t("btn_cancel", &LOC)) .with_style(button::Style::Link), ), @@ -255,17 +260,22 @@ async fn form_controls(request: HttpRequest) -> Result { .with_child(form::Hidden::field("origin", "form-text")) // Botonera de acciones. .with_child( - button::ButtonSet::new() - .with_button( + Container::new() + .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)) .with_style(button::Style::Solid(Intent::Primary)), ) - .with_button( + .with_child( Button::reset(Lc::t("btn_reset", &LOC)).with_style( button::Style::Outline(Intent::Neutral), ), ) - .with_button( + .with_child( Button::plain(Lc::t("btn_cancel", &LOC)) .with_style(button::Style::Link), ), @@ -430,16 +440,21 @@ fn form_lists() -> Form { .with_child(form::Hidden::field("origin", "form-lists")) // Botonera de acciones. .with_child( - button::ButtonSet::new() - .with_button( + Container::new() + .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)) .with_style(button::Style::Solid(Intent::Primary)), ) - .with_button( + .with_child( Button::reset(Lc::t("btn_reset", &LOC)) .with_style(button::Style::Outline(Intent::Neutral)), ) - .with_button( + .with_child( Button::plain(Lc::t("btn_cancel", &LOC)).with_style(button::Style::Link), ), ) diff --git a/examples/intro-flex.rs b/examples/intro-flex.rs new file mode 100644 index 00000000..656f9555 --- /dev/null +++ b/examples/intro-flex.rs @@ -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 { + vec![&pagetop_bootsier::Bootsier] + } + + fn configure_router(&self, router: Router) -> Router { + router.route("/", web::get(intro_flex)) + } +} + +async fn intro_flex(request: HttpRequest) -> Result { + 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) -> 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 +} diff --git a/examples/locale/en-US/intro-flex.ftl b/examples/locale/en-US/intro-flex.ftl new file mode 100644 index 00000000..d4f146b1 --- /dev/null +++ b/examples/locale/en-US/intro-flex.ftl @@ -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 diff --git a/examples/locale/es-ES/intro-flex.ftl b/examples/locale/es-ES/intro-flex.ftl new file mode 100644 index 00000000..312ebda0 --- /dev/null +++ b/examples/locale/es-ES/intro-flex.ftl @@ -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 diff --git a/examples/navbar-menus.rs b/examples/navbar-menus.rs index 56b0b202..eb2fecaf 100644 --- a/examples/navbar-menus.rs +++ b/examples/navbar-menus.rs @@ -82,10 +82,8 @@ impl Extension for SuperMenu { )) .with_item(bs::navbar::Item::nav( bs::Nav::new() - .with_prop(PropsOp::add_classes(class::Margin::with( - BoxSide::Start, - ScaleSize::Auto, - ))) + // Empuja este menú (y lo que le siga) al extremo final de la barra. + .with_prop(FlexItem::push_end()) .with_item(bs::nav::Item::link( Lc::t("menus_item_sign_up", &LOC), "/auth/sign-up", diff --git a/extensions/pagetop-bootsier/src/theme/bs.rs b/extensions/pagetop-bootsier/src/theme/bs.rs index 572934cd..0ce50d05 100644 --- a/extensions/pagetop-bootsier/src/theme/bs.rs +++ b/extensions/pagetop-bootsier/src/theme/bs.rs @@ -21,7 +21,7 @@ pub use pagetop::base::component::breadcrumb; // Button. pub mod button; -pub use button::{Button, ButtonBootsier, ButtonSet}; +pub use button::{Button, ButtonBootsier}; // Container. pub mod container; diff --git a/extensions/pagetop-bootsier/src/theme/bs/button.rs b/extensions/pagetop-bootsier/src/theme/bs/button.rs index 1a85ef19..18b80175 100644 --- a/extensions/pagetop-bootsier/src/theme/bs/button.rs +++ b/extensions/pagetop-bootsier/src/theme/bs/button.rs @@ -1,10 +1,10 @@ -//! Definiciones para crear botones ([`Button`]) y conjuntos de botones ([`ButtonSet`]). +//! Definiciones para crear botones ([`Button`]). use pagetop::prelude::*; 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_FULL_WIDTH: &str = "bootsier.button.full_width"; diff --git a/extensions/pagetop-user/src/handlers/admin/users.rs b/extensions/pagetop-user/src/handlers/admin/users.rs index b4704e57..a8edbb80 100644 --- a/extensions/pagetop-user/src/handlers/admin/users.rs +++ b/extensions/pagetop-user/src/handlers/admin/users.rs @@ -317,14 +317,12 @@ async fn render_user_edit( // pantalla siga sabiendo devolver al listado en el estado en que se dejó. // // "Guardar" (envía el `
` 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 -// tema los alinee con espaciado uniforme. Los botones de bloqueo/activación y de +// "Gestionar roles" y "Restablecer contraseña" son botones sueltos; bloqueo/activación y // 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`, -// así que no pueden ir dentro; conservan así el envío nativo sin JavaScript, mejorado con -// `hx-post`/`hx-confirm`. Todo se construye con componentes -- `Container`, `Form`, `Hidden`, -// `ButtonSet`, `Button` --, sin `html!` en bruto (ver PAGETOP.md, "Preferir componentes a `html!` -// en bruto"); la alineación en línea de los tres queda pendiente de una pasada posterior. +// `Form`, con un campo `Hidden` para el nuevo valor, para conservar el envío nativo sin JavaScript, +// mejorado con `hx-post`/`hx-confirm`. Todos son hijos directos del mismo `Container` con Flex, que +// los alinea en fila con espaciado uniforme sin que ninguno tenga que ser forzosamente un `Button` +// suelto (ver PAGETOP.md, "Preferir componentes a `html!` en bruto"). fn edit_actions( user_id: i32, status: UserStatus, @@ -350,21 +348,6 @@ fn edit_actions( 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 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() .with_action(status_action.clone()) .with_method(form::Method::Post) @@ -378,7 +361,27 @@ fn edit_actions( 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 { let mut admin_form = Form::new() diff --git a/src/base/component.rs b/src/base/component.rs index 9fb10b77..86409bda 100644 --- a/src/base/component.rs +++ b/src/base/component.rs @@ -17,7 +17,7 @@ pub use block::Block; pub mod button; #[doc(inline)] -pub use button::{Button, ButtonSet}; +pub use button::Button; pub mod container; #[doc(inline)] diff --git a/src/base/component/button.rs b/src/base/component/button.rs index 4bbb1787..758bf65a 100644 --- a/src/base/component/button.rs +++ b/src/base/component/button.rs @@ -1,10 +1,7 @@ -//! Definiciones para crear botones ([`Button`]) y conjuntos de botones ([`ButtonSet`]). +//! Definiciones para crear botones ([`Button`]). mod props; pub use props::{Kind, Size, Style}; mod component; pub use component::Button; - -mod set; -pub use set::ButtonSet; diff --git a/src/base/component/button/set.rs b/src/base/component/button/set.rs deleted file mode 100644 index 2a87ebd1..00000000 --- a/src/base/component/button/set.rs +++ /dev/null @@ -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 { - 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 { - 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) -> 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>) -> Self { - self.buttons.alter_child(op.into()); - self - } -} diff --git a/src/base/component/container.rs b/src/base/component/container.rs index 903a1d71..fff52183 100644 --- a/src/base/component/container.rs +++ b/src/base/component/container.rs @@ -47,6 +47,9 @@ pub struct Container { props: Props, /// Devuelve el tipo semántico del contenedor. kind: Kind, + /// Devuelve el posicionamiento Flexbox como contenedor, si tiene alguno. + #[getters(copy)] + flex: Option, /// Devuelve la lista de componentes (`children`) del contenedor. children: Children, } @@ -61,6 +64,12 @@ impl Component for Container { self.props.get_id() } + fn setup(&mut self, _cx: &Context) { + if let Some(flex) = self.flex() { + flex.apply_to(&mut self.props); + } + } + #[rustfmt::skip] async fn prepare(&self, cx: &mut Context) -> Result { let output = self.children().render(cx).await; @@ -134,6 +143,12 @@ impl Container { self } + /// Establece el posicionamiento Flexbox como contenedor (usa `None` para quitarlo). + pub fn with_flex(mut self, flex: impl Into>) -> Self { + self.flex = flex.into(); + self + } + /// Añade un nuevo componente al contenedor o modifica la lista de componentes (`children`) con /// una operación [`ChildOp`]. pub fn with_child(mut self, op: impl Into) -> Self { diff --git a/src/base/component/dialog.rs b/src/base/component/dialog.rs index e810562a..231a89fb 100644 --- a/src/base/component/dialog.rs +++ b/src/base/component/dialog.rs @@ -136,8 +136,7 @@ impl Dialog { /// 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 - /// (`dialog-footer`); por lo que no requiere un [`ButtonSet`](super::ButtonSet) para alinear - /// los botones, aunque puede usarse si se desea. + /// (`dialog-footer`); no requiere ninguna configuración adicional para alinear los botones. pub fn with_footer(mut self, op: impl Into) -> Self { self.footer.alter_child(op.into()); self diff --git a/src/base/component/navbar/component.rs b/src/base/component/navbar/component.rs index 85866751..8417b634 100644 --- a/src/base/component/navbar/component.rs +++ b/src/base/component/navbar/component.rs @@ -81,6 +81,9 @@ pub struct Navbar { props: Props, /// Devuelve la disposición configurada para la barra de navegación. layout: navbar::Layout, + /// Devuelve el posicionamiento Flexbox como contenedor, si tiene alguno. + #[getters(copy)] + flex: Option, /// Devuelve la lista de contenidos. items: Children, } @@ -128,37 +131,44 @@ impl Component for Navbar { let id = self.id().unwrap(); let id_content = util::join!(id, "-content"); + // Posicionamiento Flexbox opcional (no del `