(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

191
src/html/spacing/margin.rs Normal file
View file

@ -0,0 +1,191 @@
use crate::core::component::Context;
use crate::core::theme::{Breakpoint, Responsive};
use crate::html::PropsOp;
use crate::html::unit::UnitValue;
use crate::{AutoDefault, Getters, builder_impl, util};
/// Configuración de márgenes externos por lado lógico y punto de corte.
///
/// No tiene relación con Flexbox. Se aplica sobre cualquier componente, con [`PropsOp::margin()`]
/// desde el `with_prop()` que suele exponer cualquier componente.
///
/// Cada lado admite cualquier [`UnitValue`], incluido [`UnitValue::Auto`] (por ejemplo, para
/// centrar un bloque con `margin-inline: auto`). Los lados lógicos `start`/`end` se traducen a
/// `margin-inline-start`/`margin-inline-end`, respetando LTR/RTL.
///
/// # Ejemplo
///
/// ```rust,no_run
/// use pagetop::prelude::*;
///
/// // Centra el bloque horizontalmente y añade espacio inferior.
/// let panel = Container::new().with_prop(PropsOp::margin(
/// Margin::new()
/// .with_x(UnitValue::Auto)
/// .with_bottom(UnitValue::RelRem(1.5)),
/// ));
/// ```
///
/// [`Flex`]: crate::html::flex::Flex
/// [`FlexItem`]: crate::html::flex::FlexItem
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq, Getters)]
pub struct Margin {
/// Devuelve el margen superior, por punto de corte.
#[getters(copy)]
top: Responsive<UnitValue>,
/// Devuelve el margen inferior, por punto de corte.
#[getters(copy)]
bottom: Responsive<UnitValue>,
/// Devuelve el margen del lado lógico de inicio, por punto de corte.
#[getters(copy)]
start: Responsive<UnitValue>,
/// Devuelve el margen del lado lógico de fin, por punto de corte.
#[getters(copy)]
end: Responsive<UnitValue>,
}
#[builder_impl]
impl Margin {
/// Crea una configuración de margen sin ningún lado establecido.
pub fn new() -> Self {
Self::default()
}
// **< Margin BUILDER >*************************************************************************
/// Establece el margen superior.
pub fn with_top(mut self, value: UnitValue) -> Self {
self.top = self.top.set(value);
self
}
/// Establece el margen superior, a partir del punto de corte indicado.
pub fn with_top_at(mut self, bp: Breakpoint, value: UnitValue) -> Self {
self.top = self.top.set_at(bp, value);
self
}
/// Establece el margen inferior.
pub fn with_bottom(mut self, value: UnitValue) -> Self {
self.bottom = self.bottom.set(value);
self
}
/// Establece el margen inferior, a partir del punto de corte indicado.
pub fn with_bottom_at(mut self, bp: Breakpoint, value: UnitValue) -> Self {
self.bottom = self.bottom.set_at(bp, value);
self
}
/// Establece el margen del lado lógico de inicio (`margin-inline-start`).
pub fn with_start(mut self, value: UnitValue) -> Self {
self.start = self.start.set(value);
self
}
/// Establece el margen del lado lógico de inicio (`margin-inline-start`), a partir del punto de
/// corte indicado.
pub fn with_start_at(mut self, bp: Breakpoint, value: UnitValue) -> Self {
self.start = self.start.set_at(bp, value);
self
}
/// Establece el margen del lado lógico de fin (`margin-inline-end`).
pub fn with_end(mut self, value: UnitValue) -> Self {
self.end = self.end.set(value);
self
}
/// Establece el margen del lado lógico de fin (`margin-inline-end`), a partir del punto de
/// corte indicado.
pub fn with_end_at(mut self, bp: Breakpoint, value: UnitValue) -> Self {
self.end = self.end.set_at(bp, value);
self
}
/// Establece el mismo margen en ambos lados lógicos laterales (inicio y fin).
pub fn with_x(mut self, value: UnitValue) -> Self {
self.start = self.start.set(value);
self.end = self.end.set(value);
self
}
/// Establece el mismo margen en ambos lados lógicos laterales (inicio y fin), a partir del
/// punto de corte indicado.
pub fn with_x_at(mut self, bp: Breakpoint, value: UnitValue) -> Self {
self.start = self.start.set_at(bp, value);
self.end = self.end.set_at(bp, value);
self
}
/// Establece el mismo margen arriba y abajo.
pub fn with_y(mut self, value: UnitValue) -> Self {
self.top = self.top.set(value);
self.bottom = self.bottom.set(value);
self
}
/// Establece el mismo margen arriba y abajo, a partir del punto de corte indicado.
pub fn with_y_at(mut self, bp: Breakpoint, value: UnitValue) -> Self {
self.top = self.top.set_at(bp, value);
self.bottom = self.bottom.set_at(bp, value);
self
}
/// Establece el mismo margen en los cuatro lados.
pub fn with_all(mut self, value: UnitValue) -> Self {
self.top = self.top.set(value);
self.bottom = self.bottom.set(value);
self.start = self.start.set(value);
self.end = self.end.set(value);
self
}
/// Establece el mismo margen en los cuatro lados, a partir del punto de corte indicado.
pub fn with_all_at(mut self, bp: Breakpoint, value: UnitValue) -> Self {
self.top = self.top.set_at(bp, value);
self.bottom = self.bottom.set_at(bp, value);
self.start = self.start.set_at(bp, value);
self.end = self.end.set_at(bp, value);
self
}
}
impl Margin {
/// Combina esta configuración con otra `Margin`, lado a lado y punto de corte a punto de corte.
/// Donde `margin` tenga un valor establecido, sustituye al de `self`, y donde no lo tenga, se
/// conserva el de `self`. Es el método que usa [`Props::with_prop()`] para que sucesivas
/// [`PropsOp::Margin`] sobre el mismo componente vayan completando lados concretos sin repetir
/// los ya establecidos.
///
/// [`Props::with_prop()`]: crate::html::Props::with_prop
/// [`PropsOp::Margin`]: crate::html::PropsOp::Margin
pub fn merge(mut self, margin: Margin) -> Self {
self.top = self.top.merge(margin.top);
self.bottom = self.bottom.merge(margin.bottom);
self.start = self.start.merge(margin.start);
self.end = self.end.merge(margin.end);
self
}
/// Aplica esta configuración como clases de utilidad responsive en el [`Context`], igual que
/// [`FlexItem::apply()`](crate::html::flex::FlexItem::apply). Un lado sin ningún valor
/// establecido, o con [`UnitValue::None`], no añade nada.
///
/// Las clases generadas se añaden a `classes`, separadas con un espacio de las que ya hubiera.
#[rustfmt::skip]
pub(crate) fn apply(self, cx: &mut Context, classes: &mut String) {
use crate::html::responsive::{apply, responsive_class, styles, value_to_token};
apply!(cx, classes, self.top, "_margin-top_", "margin-top", val);
apply!(cx, classes, self.bottom, "_margin-bottom_", "margin-bottom", val);
apply!(cx, classes, self.start, "_margin-start_", "margin-inline-start", val);
apply!(cx, classes, self.end, "_margin-end_", "margin-inline-end", val);
}
}
impl From<Margin> for PropsOp {
fn from(margin: Margin) -> Self {
Self::margin(margin)
}
}