(pagetop): Añade Margin y Padding nativos

- Nuevo módulo `html::spacing` con `Margin`/`Padding`: igual que
  `FlexItem`, se resuelven generando clases CSS dinámicamente y se
  aplican con `PropsOp::margin()`/`PropsOp::padding()` sobre cualquier
  componente.
- Elimina `Margin`/`Padding` de `pagetop-bootsier`.
- Añade el ejemplo `intro-spacing` con los patrones de uso típicos.
This commit is contained in:
Manuel Cillero 2026-09-13 00:48:11 +02:00
parent 64113aff09
commit 50e4b42fe4
17 changed files with 881 additions and 322 deletions

255
examples/intro-spacing.rs Normal file
View file

@ -0,0 +1,255 @@
use pagetop::prelude::*;
include_locales!(LOC from "examples/locale");
struct IntroSpacing;
#[async_trait]
impl Extension for IntroSpacing {
fn dependencies(&self) -> Vec<ExtensionRef> {
vec![&pagetop_bootsier::Bootsier]
}
fn configure_router(&self, router: Router) -> Router {
router.route("/", web::get(intro_spacing))
}
}
async fn intro_spacing(request: HttpRequest) -> Result<Markup, ErrorPage> {
Page::new(request)
.with_assets(demo_box_styles())
.with_assets(demo_row_styles())
.with_assets(demo_code_styles())
.with_child(
Intro::default()
.with_opening(IntroOpening::Custom)
.with_title(Lc::n("PageTop"))
.with_slogan(Lc::t("spacing_slogan", &LOC))
.with_button(None::<(Lc, Route)>)
.with_child(Html::with(|cx| {
html! {
p class="intro-text-lead" {
(Lc::t("spacing_note", &LOC).using(cx))
}
}
}))
.with_child(padding_block())
.with_child(layout_block())
.with_child(responsive_block())
.with_child(combined_block()),
)
.render()
.await
}
fn padding_block() -> Block {
let mut block = Block::new().with_title(Lc::t("spacing_block_title_padding", &LOC));
let padding_variants: [(&str, UnitValue, &str); 3] = [
(
"spacing_title_uniform_small",
UnitValue::RelRem(0.5),
"Padding::new().with_all(UnitValue::RelRem(0.5))",
),
(
"spacing_title_uniform_medium",
UnitValue::RelRem(1.0),
"Padding::new().with_all(UnitValue::RelRem(1.0))",
),
(
"spacing_title_uniform_large",
UnitValue::RelRem(2.0),
"Padding::new().with_all(UnitValue::RelRem(2.0))",
),
];
for (title_key, size, code) in padding_variants {
block = block
.with_child(caption(Lc::t(title_key, &LOC), Lc::n(code)))
.with_child(
demo_row(Flex::new().with_gap(flex::Gap::Both(UnitValue::RelRem(0.5)))).with_child(
demo_box(box_sample()).with_prop(Padding::new().with_all(size).into()),
),
);
}
block
.with_child(caption(
Lc::t("spacing_title_sides", &LOC),
Lc::n(concat!(
"Padding::new()",
".with_top(UnitValue::RelRem(0.25))",
".with_end(UnitValue::RelRem(2.5))",
".with_bottom(UnitValue::RelRem(1.5))",
".with_start(UnitValue::RelRem(0.5))",
)),
))
.with_child(
demo_row(Flex::new()).with_child(
demo_box(box_sample()).with_prop(
Padding::new()
.with_top(UnitValue::RelRem(0.25))
.with_end(UnitValue::RelRem(2.5))
.with_bottom(UnitValue::RelRem(1.5))
.with_start(UnitValue::RelRem(0.5))
.into(),
),
),
)
}
fn layout_block() -> Block {
Block::new()
.with_title(Lc::t("spacing_block_title_layout", &LOC))
.with_child(caption(
Lc::t("spacing_title_margin_gap", &LOC),
Lc::n("Margin::new().with_x(UnitValue::RelRem(1.0))"),
))
.with_child(
demo_row(Flex::new())
.with_child(demo_box(box_sample()))
.with_child(
demo_box(box_sample())
.with_prop(Margin::new().with_x(UnitValue::RelRem(1.0)).into()),
)
.with_child(demo_box(box_sample())),
)
.with_child(caption(
Lc::t("spacing_title_margin_center", &LOC),
Lc::n("Margin::new().with_x(UnitValue::Auto)"),
))
.with_child(
demo_row(Flex::new()).with_child(
demo_box(box_sample())
.with_prop(Margin::new().with_x(UnitValue::Auto).into())
.with_prop(PropsOp::flex_item(
FlexItem::new().with_size(flex::ItemSize::Custom(UnitValue::RelRem(8.0))),
)),
),
)
}
fn responsive_block() -> Block {
Block::new()
.with_title(Lc::t("spacing_block_title_responsive", &LOC))
.with_child(caption(
Lc::t("spacing_title_responsive", &LOC),
Lc::n(concat!(
"Padding::new()",
".with_all(UnitValue::RelRem(0.5))",
".with_all_at(Breakpoint::Md, UnitValue::RelRem(2.0))",
".with_all_at(Breakpoint::Lg, UnitValue::RelRem(4.0))",
)),
))
.with_child(
demo_row(Flex::new()).with_child(
demo_box(box_sample()).with_prop(
Padding::new()
.with_all(UnitValue::RelRem(0.5))
.with_all_at(Breakpoint::Md, UnitValue::RelRem(2.0))
.with_all_at(Breakpoint::Lg, UnitValue::RelRem(4.0))
.into(),
),
),
)
}
fn combined_block() -> Block {
Block::new()
.with_title(Lc::t("spacing_block_title_combined", &LOC))
.with_child(caption(
Lc::t("spacing_title_combined", &LOC),
Lc::n(concat!(
"Container::new()",
".with_prop(Margin::new().with_y(UnitValue::RelRem(1.0)).into())",
".with_prop(Padding::new().with_all(UnitValue::RelRem(1.5)).into())",
)),
))
.with_child(
demo_row(Flex::new()).with_child(
Container::new()
.with_prop(PropsOp::add_classes("spacing-demo-box"))
.with_prop(Margin::new().with_y(UnitValue::RelRem(1.0)).into())
.with_prop(Padding::new().with_all(UnitValue::RelRem(1.5)).into())
.with_child(
Button::plain(Lc::t("spacing_box_card_button", &LOC))
.with_style(button::Style::Solid(Intent::Warning)),
),
),
)
}
// **< HELPERS >************************************************************************************
// Caja de muestra sin relleno ni margen propios, para que sólo se vea el efecto de `Margin`/
// `Padding` aplicado en cada demostración.
fn demo_box_styles() -> AssetsOp {
AssetsOp::add_responsive_styles(
None,
"spacing-demo-box",
[
("background-color", "#0d6efd"),
("color", "#fff"),
("min-width", "3rem"),
("width", "auto"),
("max-width", "none"),
("margin", "0"),
("padding", "0"),
("border-radius", "0.375rem"),
("text-align", "center"),
],
)
}
// Aspecto fijo de las filas de muestra.
fn demo_row_styles() -> AssetsOp {
AssetsOp::add_responsive_styles(
None,
"spacing-demo-row",
[
("background-color", "#f1f3f5"),
("width", "100%"),
("max-width", "none"),
("margin", "0 0 1.5rem"),
("padding", "0.75rem"),
],
)
}
// Evita que los fragmentos de código largos desborden su contenedor.
fn demo_code_styles() -> AssetsOp {
AssetsOp::add_responsive_styles(None, "spacing-demo-code", [("overflow-wrap", "anywhere")])
}
// Caja azul de muestra, sin relleno ni margen propios.
fn demo_box(label: Lc) -> Container {
Container::new()
.with_prop(PropsOp::add_classes("spacing-demo-box"))
.with_child(Html::with(move |cx| html! { (label.using(cx)) }))
}
// Etiqueta genérica reutilizada en la mayoría de cajas de muestra.
fn box_sample() -> Lc {
Lc::t("spacing_box_sample", &LOC)
}
// Fila de demostración con fondo gris para visualizar los límites de cada caja.
fn demo_row(flex: Flex) -> Container {
Container::new()
.with_prop(PropsOp::add_classes("spacing-demo-row"))
.with_flex(flex)
}
// Título y fragmento de código que introducen cada demostración.
fn caption(title: Lc, code: Lc) -> Html {
Html::with(move |cx| {
html! {
h3 { (title.using(cx)) }
p { code class="spacing-demo-code" { (code.using(cx)) } }
}
})
}
#[pagetop::main]
async fn main() -> std::io::Result<()> {
Application::prepare(&IntroSpacing).await.run().await
}

View file

@ -0,0 +1,19 @@
spacing_slogan = Native margin and padding with Margin and Padding
spacing_note = Margin and Padding have nothing to do with Flexbox: they apply to any component, whether its container uses Flexbox or not. They are combined with Flex here only to lay out the sample boxes in a row.
spacing_block_title_padding = Padding
spacing_block_title_layout = Layout
spacing_block_title_responsive = Padding at a breakpoint
spacing_block_title_combined = Margin and padding combined
spacing_title_uniform_small = Small padding on all four sides
spacing_title_uniform_medium = Medium padding on all four sides
spacing_title_uniform_large = Large padding on all four sides
spacing_title_sides = A different value per side, including the logical start/end sides
spacing_title_margin_gap = The middle box's margin pushes its neighbours away, without using Gap
spacing_title_margin_center = An automatic margin on both sides centers the box in the free space
spacing_title_responsive = Padding grows from md onwards, and again from lg
spacing_title_combined = Outer margin above/below and inner padding on all four sides, on a component with real content
spacing_box_sample = Content
spacing_box_card_button = Accept

View file

@ -0,0 +1,19 @@
spacing_slogan = Márgenes y relleno nativos con Margin y Padding
spacing_note = Margin y Padding no tienen relación con Flexbox: se aplican sobre cualquier componente, use o no Flexbox su contenedor. Aquí se combinan con Flex sólo para colocar las cajas de muestra en fila.
spacing_block_title_padding = Rellenos
spacing_block_title_layout = Composición
spacing_block_title_responsive = Relleno por punto de corte
spacing_block_title_combined = Margin y padding combinados
spacing_title_uniform_small = Relleno pequeño en los cuatro lados
spacing_title_uniform_medium = Relleno medio en los cuatro lados
spacing_title_uniform_large = Relleno grande en los cuatro lados
spacing_title_sides = Un valor distinto para cada lado, incluidos los lados lógicos de inicio y fin
spacing_title_margin_gap = El margen de la caja central empuja a sus vecinas, sin usar Gap
spacing_title_margin_center = Un margen automático en ambos lados centra la caja en el espacio libre
spacing_title_responsive = El relleno crece a partir de md, y de nuevo a partir de lg
spacing_title_combined = Margen exterior arriba/abajo y relleno interno por los cuatro lados, sobre un componente con contenido real
spacing_box_sample = Contenido
spacing_box_card_button = Aceptar