diff --git a/examples/intro-flex.rs b/examples/intro-flex.rs index c51e5bb3..a5e54e30 100644 --- a/examples/intro-flex.rs +++ b/examples/intro-flex.rs @@ -17,8 +17,7 @@ impl Extension for IntroFlex { async fn intro_flex(request: HttpRequest) -> Result { Page::new(request) - .with_assets(demo_box_styles()) - .with_assets(demo_row_styles()) + .with_assets(AssetsOp::AddStyleSheet(demo_styles())) .with_child( Intro::default() .with_opening(IntroOpening::Custom) @@ -479,37 +478,32 @@ fn other_block() -> Block { // **< HELPERS >************************************************************************************ -// Aspecto fijo de las cajas de muestra. -fn demo_box_styles() -> AssetsOp { - AssetsOp::add_responsive_styles( - None, - "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"), - ], - ) -} - -// Aspecto fijo de las filas de muestra. -fn demo_row_styles() -> AssetsOp { - AssetsOp::add_responsive_styles( - None, - "flex-demo-row", - [ - ("background-color", "#f1f3f5"), - ("width", "100%"), - ("max-width", "none"), - ("margin", "0 0 1.5rem"), - ("padding", "0.75rem"), - ], - ) +// 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. diff --git a/examples/intro-responsive.rs b/examples/intro-responsive.rs index 782ecc1f..f01a7ebb 100644 --- a/examples/intro-responsive.rs +++ b/examples/intro-responsive.rs @@ -17,9 +17,7 @@ impl Extension for IntroResponsive { async fn intro_responsive(request: HttpRequest) -> Result { Page::new(request) - .with_assets(demo_box_styles()) - .with_assets(demo_row_styles()) - .with_assets(demo_code_styles()) + .with_assets(AssetsOp::AddStyleSheet(demo_styles())) .with_child( Intro::default() .with_opening(IntroOpening::Custom) @@ -191,42 +189,35 @@ fn gap_grow_block() -> Block { // **< HELPERS >************************************************************************************ -// Aspecto fijo de las cajas de muestra. -fn demo_box_styles() -> AssetsOp { - AssetsOp::add_responsive_styles( - None, - "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"), - ], - ) -} - -// Aspecto fijo de las filas de muestra. -fn demo_row_styles() -> AssetsOp { - AssetsOp::add_responsive_styles( - None, - "flex-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, "flex-demo-code", [("overflow-wrap", "anywhere")]) +// Aspecto fijo de las cajas y filas de muestra. +fn demo_styles() -> StyleSheet { + StyleSheet::inline("intro-responsive", |_| { + util::indoc!( + r#" + .flex-demo-box { + background-color: #0d6efd; + color: #fff; + min-width: 3rem; + width: auto; + max-width: none; + margin: 0; + border-radius: 0.375rem; + text-align: center; + } + .flex-demo-row { + background-color: #f1f3f5; + width: 100%; + max-width: none; + margin: 0 0 1.5rem; + padding: 0.75rem; + } + code { + overflow-wrap: anywhere; + } + "# + ) + .to_string() + }) } // Caja con fondo azul y relleno vertical configurable, para mostrar diferencias de altura. @@ -259,7 +250,7 @@ fn caption(title: Lc, code: Lc) -> Html { Html::with(move |cx| { html! { h3 { (title.using(cx)) } - p { code class="flex-demo-code" { (code.using(cx)) } } + p { code { (code.using(cx)) } } } }) } diff --git a/examples/intro-spacing.rs b/examples/intro-spacing.rs deleted file mode 100644 index b650208b..00000000 --- a/examples/intro-spacing.rs +++ /dev/null @@ -1,255 +0,0 @@ -use pagetop::prelude::*; - -include_locales!(LOC from "examples/locale"); - -struct IntroSpacing; - -#[async_trait] -impl Extension for IntroSpacing { - fn dependencies(&self) -> Vec { - vec![&pagetop_bootsier::Bootsier] - } - - fn configure_router(&self, router: Router) -> Router { - router.route("/", web::get(intro_spacing)) - } -} - -async fn intro_spacing(request: HttpRequest) -> Result { - 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 -} diff --git a/examples/locale/en-US/intro-spacing.ftl b/examples/locale/en-US/intro-spacing.ftl deleted file mode 100644 index 57792706..00000000 --- a/examples/locale/en-US/intro-spacing.ftl +++ /dev/null @@ -1,19 +0,0 @@ -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 diff --git a/examples/locale/es-ES/intro-spacing.ftl b/examples/locale/es-ES/intro-spacing.ftl deleted file mode 100644 index 9a8cf507..00000000 --- a/examples/locale/es-ES/intro-spacing.ftl +++ /dev/null @@ -1,19 +0,0 @@ -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 diff --git a/extensions/pagetop-aliner/src/lib.rs b/extensions/pagetop-aliner/src/lib.rs index fe7e1b19..29f43244 100644 --- a/extensions/pagetop-aliner/src/lib.rs +++ b/extensions/pagetop-aliner/src/lib.rs @@ -113,21 +113,21 @@ impl Extension for Aliner { #[async_trait] impl Theme for Aliner { fn before_render_page_body(&self, page: &mut Page) { - page.alter_assets( + page.alter_assets(AssetsOp::AddStyleSheet( StyleSheet::from("/pagetop/css/normalize.css") .with_version("8.0.1") .with_weight(-99), - ) - .alter_assets( + )) + .alter_assets(AssetsOp::AddStyleSheet( StyleSheet::from("/pagetop/css/basic.css") .with_version(PAGETOP_VERSION) .with_weight(-99), - ) - .alter_assets( + )) + .alter_assets(AssetsOp::AddStyleSheet( StyleSheet::from("/aliner/css/styles.css") .with_version(env!("CARGO_PKG_VERSION")) .with_weight(-99), - ) + )) .alter_child_in( &CoreRegions::Footer, ChildOp::AddIfEmpty(PoweredBy::new().into()), diff --git a/extensions/pagetop-bootsier/src/lib.rs b/extensions/pagetop-bootsier/src/lib.rs index 7991078d..8d38ae47 100644 --- a/extensions/pagetop-bootsier/src/lib.rs +++ b/extensions/pagetop-bootsier/src/lib.rs @@ -178,40 +178,42 @@ impl Theme for Bootsier { // Las URLs de las fuentes deben coincidir exactamente con las declaradas en @font-face de // _bootsier-custom.scss; cualquier discrepancia hace que el navegador descargue dos veces. - page.alter_assets(Preload::font("/bootsier/fonts/bootsier.font.woff2").with_weight(-99)) - .alter_assets( - Preload::font("/bootsier/fonts/bootsier.font.italic.woff2").with_weight(-99), - ) - .alter_assets( - StyleSheet::from("/bootsier/css/bootsier.min.css") - .with_version(ADMINLTE_VERSION) - .with_weight(-99), - ) - .alter_assets( - JavaScript::defer("/bootsier/js/bootsier.bundle.min.js") - .with_version(BOOTSTRAP_VERSION) - .with_weight(-99), - ) - .alter_assets( - JavaScript::defer("/bootsier/js/bootsier.extended.min.js") - .with_version(ADMINLTE_VERSION) - .with_weight(-99), - ) - .alter_assets( - JavaScript::defer("/bootsier/js/bootsier.dialog.min.js") - .with_version(BOOTSTRAP_VERSION) - .with_weight(-99), - ) - .alter_assets( - JavaScript::defer("/bootsier/js/bootsier.confirm.min.js") - .with_version(BOOTSTRAP_VERSION) - .with_weight(-99), - ) - .alter_body_props(PropsOp::set("data-confirm-ok", confirm_ok)) - .alter_body_props(PropsOp::set("data-confirm-cancel", confirm_cancel)) - .alter_child_in( - &CoreRegions::Footer, - ChildOp::AddIfEmpty(PoweredBy::new().into()), - ); + page.alter_assets(AssetsOp::AddPreload( + Preload::font("/bootsier/fonts/bootsier.font.woff2").with_weight(-99), + )) + .alter_assets(AssetsOp::AddPreload( + Preload::font("/bootsier/fonts/bootsier.font.italic.woff2").with_weight(-99), + )) + .alter_assets(AssetsOp::AddStyleSheet( + StyleSheet::from("/bootsier/css/bootsier.min.css") + .with_version(ADMINLTE_VERSION) + .with_weight(-99), + )) + .alter_assets(AssetsOp::AddJavaScript( + JavaScript::defer("/bootsier/js/bootsier.bundle.min.js") + .with_version(BOOTSTRAP_VERSION) + .with_weight(-99), + )) + .alter_assets(AssetsOp::AddJavaScript( + JavaScript::defer("/bootsier/js/bootsier.extended.min.js") + .with_version(ADMINLTE_VERSION) + .with_weight(-99), + )) + .alter_assets(AssetsOp::AddJavaScript( + JavaScript::defer("/bootsier/js/bootsier.dialog.min.js") + .with_version(BOOTSTRAP_VERSION) + .with_weight(-99), + )) + .alter_assets(AssetsOp::AddJavaScript( + JavaScript::defer("/bootsier/js/bootsier.confirm.min.js") + .with_version(BOOTSTRAP_VERSION) + .with_weight(-99), + )) + .alter_body_props(PropsOp::set("data-confirm-ok", confirm_ok)) + .alter_body_props(PropsOp::set("data-confirm-cancel", confirm_cancel)) + .alter_child_in( + &CoreRegions::Footer, + ChildOp::AddIfEmpty(PoweredBy::new().into()), + ); } } diff --git a/extensions/pagetop-bootsier/src/theme/bs/layout/template.rs b/extensions/pagetop-bootsier/src/theme/bs/layout/template.rs index 99a33065..b22d56e5 100644 --- a/extensions/pagetop-bootsier/src/theme/bs/layout/template.rs +++ b/extensions/pagetop-bootsier/src/theme/bs/layout/template.rs @@ -41,11 +41,11 @@ async fn render_admin(cx: &mut Context) -> Markup { cx.alter_body_props(PropsOp::add_classes( "layout-fixed sidebar-expand-lg bg-body-tertiary", )); - cx.alter_assets( + cx.alter_assets(AssetsOp::AddJavaScript( JavaScript::defer("/bootsier/js/bootsier.shell.min.js") .with_version(ADMINLTE_VERSION) .with_weight(-88), - ); + )); // `CoreRegions::Aside` es una región neutra del core: la usa `pagetop-admin` para su menú de // secciones sin que este tema tenga que depender de él. `BootsierRegions::Sidebar` sigue // disponible para que cualquier extensión añada elementos propios a mano. diff --git a/extensions/pagetop-bootsier/src/theme/class.rs b/extensions/pagetop-bootsier/src/theme/class.rs index ee51497b..44c56d5d 100644 --- a/extensions/pagetop-bootsier/src/theme/class.rs +++ b/extensions/pagetop-bootsier/src/theme/class.rs @@ -21,3 +21,6 @@ pub use border::{Border, BorderColor}; mod rounded; pub use rounded::{Rounded, RoundedRadius}; + +mod layout; +pub use layout::{Margin, Padding}; diff --git a/extensions/pagetop-bootsier/src/theme/class/layout.rs b/extensions/pagetop-bootsier/src/theme/class/layout.rs new file mode 100644 index 00000000..2e65ae15 --- /dev/null +++ b/extensions/pagetop-bootsier/src/theme/class/layout.rs @@ -0,0 +1,211 @@ +use pagetop::prelude::*; + +use crate::theme::{BoxSide, BreakPoint, ScaleSize}; + +// **< Margin >************************************************************************************* + +/// Clases para establecer **margin** por lado, tamaño y punto de ruptura. +/// +/// # Ejemplos +/// +/// ```rust +/// use pagetop_bootsier::theme::*; +/// +/// let m = class::Margin::with(BoxSide::Top, ScaleSize::Three); +/// assert_eq!(m.to_class(), "mt-3"); +/// +/// let m = class::Margin::with(BoxSide::Start, ScaleSize::Auto) +/// .with_breakpoint(BreakPoint::LG); +/// assert_eq!(m.to_class(), "ms-lg-auto"); +/// +/// let m = class::Margin::with(BoxSide::All, ScaleSize::None); +/// assert_eq!(m.to_class(), ""); +/// ``` +#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] +pub struct Margin { + side: BoxSide, + size: ScaleSize, + breakpoint: BreakPoint, +} + +impl Margin { + /// Crea un **margin** indicando lado(s) y tamaño. Por defecto no se aplica a ningún punto de + /// ruptura. + pub fn with(side: BoxSide, size: ScaleSize) -> Self { + Margin { + side, + size, + breakpoint: BreakPoint::None, + } + } + + // **< Margin BUILDER >************************************************************************* + + /// Establece el punto de ruptura a partir del cual se empieza a aplicar el **margin**. + pub fn with_breakpoint(mut self, breakpoint: BreakPoint) -> Self { + self.breakpoint = breakpoint; + self + } + + // **< Margin HELPERS >************************************************************************* + + // Devuelve el prefijo `m*` según el lado. + #[rustfmt::skip] + #[inline] + const fn side_prefix(&self) -> &'static str { + match self.side { + BoxSide::All => "m", + BoxSide::Top => "mt", + BoxSide::Bottom => "mb", + BoxSide::Start => "ms", + BoxSide::End => "me", + BoxSide::LeftAndRight => "mx", + BoxSide::TopAndBottom => "my", + } + } + + // Devuelve el sufijo del tamaño (`auto`, `0`..`5`), o `None` si no define clase. + #[rustfmt::skip] + #[inline] + const fn size_suffix(&self) -> Option<&'static str> { + match self.size { + ScaleSize::None => None, + ScaleSize::Auto => Some("auto"), + ScaleSize::Zero => Some("0"), + ScaleSize::One => Some("1"), + ScaleSize::Two => Some("2"), + ScaleSize::Three => Some("3"), + ScaleSize::Four => Some("4"), + ScaleSize::Five => Some("5"), + } + } + + /// Añade la clase de **margin** a la cadena de clases. + pub fn push_to(self, classes: &mut String) { + if let Some(size) = self.size_suffix() { + let side = self.side_prefix(); + self.breakpoint.push_to(classes, side, size); + } + } + + /// Devuelve la clase de *margin* como cadena (`"mt-3"`, `"ms-lg-auto"`, etc.). + /// + /// Si `size` es `ScaleSize::None`, devuelve `""`. + pub fn to_class(self) -> String { + let mut class = String::new(); + self.push_to(&mut class); + class + } +} + +impl From for CowStr { + /// Permite pasar [`Margin`] directamente a [`PropsOp`]. + fn from(val: Margin) -> Self { + val.to_class().into() + } +} + +// **< Padding >************************************************************************************ + +/// Clases para establecer **padding** por lado, tamaño y punto de ruptura. +/// +/// # Ejemplos +/// +/// ```rust +/// use pagetop_bootsier::theme::*; +/// +/// let p = class::Padding::with(BoxSide::LeftAndRight, ScaleSize::Two); +/// assert_eq!(p.to_class(), "px-2"); +/// +/// let p = class::Padding::with(BoxSide::End, ScaleSize::Four) +/// .with_breakpoint(BreakPoint::SM); +/// assert_eq!(p.to_class(), "pe-sm-4"); +/// +/// let p = class::Padding::with(BoxSide::All, ScaleSize::Auto); +/// assert_eq!(p.to_class(), ""); // `Auto` no aplica a padding. +/// ``` +#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] +pub struct Padding { + side: BoxSide, + size: ScaleSize, + breakpoint: BreakPoint, +} + +impl Padding { + /// Crea un **padding** indicando lado(s) y tamaño. Por defecto no se aplica a ningún punto de + /// ruptura. + pub fn with(side: BoxSide, size: ScaleSize) -> Self { + Padding { + side, + size, + breakpoint: BreakPoint::None, + } + } + + // **< Padding BUILDER >************************************************************************ + + /// Establece el punto de ruptura a partir del cual se empieza a aplicar el **padding**. + pub fn with_breakpoint(mut self, breakpoint: BreakPoint) -> Self { + self.breakpoint = breakpoint; + self + } + + // **< Padding HELPERS >************************************************************************ + + // Devuelve el prefijo `p*` según el lado. + #[rustfmt::skip] + #[inline] + const fn side_prefix(&self) -> &'static str { + match self.side { + BoxSide::All => "p", + BoxSide::Top => "pt", + BoxSide::Bottom => "pb", + BoxSide::Start => "ps", + BoxSide::End => "pe", + BoxSide::LeftAndRight => "px", + BoxSide::TopAndBottom => "py", + } + } + + // Devuelve el sufijo del tamaño (`0`..`5`), o None si no define clase. + // + // Nota: `ScaleSize::Auto` **no aplica** a *padding* => devuelve `None`. + #[rustfmt::skip] + #[inline] + const fn size_suffix(&self) -> Option<&'static str> { + match self.size { + ScaleSize::None => None, + ScaleSize::Auto => None, + ScaleSize::Zero => Some("0"), + ScaleSize::One => Some("1"), + ScaleSize::Two => Some("2"), + ScaleSize::Three => Some("3"), + ScaleSize::Four => Some("4"), + ScaleSize::Five => Some("5"), + } + } + + /// Añade la clase de **padding** a la cadena de clases. + pub fn push_to(self, classes: &mut String) { + if let Some(size) = self.size_suffix() { + let side = self.side_prefix(); + self.breakpoint.push_to(classes, side, size); + } + } + + /// Devuelve la clase de *padding* como cadena (`"px-2"`, `"pe-sm-4"`, etc.). + /// + /// Si `size` es `ScaleSize::None` o `ScaleSize::Auto`, devuelve `""`. + pub fn to_class(self) -> String { + let mut class = String::new(); + self.push_to(&mut class); + class + } +} + +impl From for CowStr { + /// Permite pasar [`Padding`] directamente a [`PropsOp`]. + fn from(val: Padding) -> Self { + val.to_class().into() + } +} diff --git a/extensions/pagetop-bootsier/src/theme/token/layout.rs b/extensions/pagetop-bootsier/src/theme/token/layout.rs index 450c8432..69401e5d 100644 --- a/extensions/pagetop-bootsier/src/theme/token/layout.rs +++ b/extensions/pagetop-bootsier/src/theme/token/layout.rs @@ -4,9 +4,12 @@ use pagetop::prelude::*; /// Escala discreta de tamaños para clases utilitarias. /// -/// Se usa como parámetro de tamaño para las clases de [`Border`]. +/// Se usa como parámetro de tamaño para las clases de [`Border`], [`Margin`] y [`Padding`]. La +/// variante `Auto` no aplica en `Padding`. /// /// [`Border`]: crate::theme::class::Border +/// [`Margin`]: crate::theme::class::Margin +/// [`Padding`]: crate::theme::class::Padding #[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] pub enum ScaleSize { /// Sin tamaño (no define ninguna clase). @@ -65,9 +68,11 @@ impl ScaleSize { /// Lados sobre los que aplicar una clase utilitaria (respetando LTR/RTL). /// -/// Se usa como selector de lado para las clases de [`Border`]. +/// Se usa como selector de lado para las clases de [`Border`], [`Margin`] y [`Padding`]. /// /// [`Border`]: crate::theme::class::Border +/// [`Margin`]: crate::theme::class::Margin +/// [`Padding`]: crate::theme::class::Padding #[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] pub enum BoxSide { /// Todos los lados. diff --git a/extensions/pagetop-htmx/src/lib.rs b/extensions/pagetop-htmx/src/lib.rs index 9404b49b..cdcfc83b 100644 --- a/extensions/pagetop-htmx/src/lib.rs +++ b/extensions/pagetop-htmx/src/lib.rs @@ -154,5 +154,7 @@ impl Extension for Htmx { } fn add_htmx_script(page: &mut Page) { - page.alter_assets(JavaScript::defer("/htmx/js/htmx.min.js").with_version("2.0.10")); + page.alter_assets(AssetsOp::AddJavaScript( + JavaScript::defer("/htmx/js/htmx.min.js").with_version("2.0.10"), + )); } diff --git a/src/base/component/intro.rs b/src/base/component/intro.rs index 00fa866b..9aaa3995 100644 --- a/src/base/component/intro.rs +++ b/src/base/component/intro.rs @@ -114,9 +114,11 @@ impl Component for Intro { } async fn prepare(&self, cx: &mut Context) -> Result { - cx.alter_assets(StyleSheet::from("/pagetop/css/intro.css").with_version(PAGETOP_VERSION)); + cx.alter_assets(AssetsOp::AddStyleSheet( + StyleSheet::from("/pagetop/css/intro.css").with_version(PAGETOP_VERSION), + )); if *self.opening() == IntroOpening::PageTop { - cx.alter_assets(JavaScript::on_load_async("intro-js", |cx| + cx.alter_assets(AssetsOp::AddJavaScript(JavaScript::on_load_async("intro-js", |cx| util::indoc!(r#" try { const resp = await fetch("https://crates.io/api/v1/crates/pagetop"); @@ -132,7 +134,7 @@ impl Component for Intro { "#) .replace("LANGID", cx.langid().to_string().as_str()) .replace("LABEL", Lc::l("intro_release_label").using(cx).as_str()) - )); + ))); } Ok(html! { diff --git a/src/base/theme/basic.rs b/src/base/theme/basic.rs index 7b1aa66d..8fedbf44 100644 --- a/src/base/theme/basic.rs +++ b/src/base/theme/basic.rs @@ -14,26 +14,28 @@ impl Extension for Basic { #[async_trait] impl Theme for Basic { fn before_render_page_body(&self, page: &mut Page) { - page.alter_assets( + page.alter_assets(AssetsOp::AddStyleSheet( StyleSheet::from("/pagetop/css/normalize.css") .with_version("8.0.1") .with_weight(-99), - ) - .alter_assets( + )) + .alter_assets(AssetsOp::AddStyleSheet( StyleSheet::from("/pagetop/css/basic.min.css") .with_version(PAGETOP_VERSION) .with_weight(-99), - ) - .alter_assets(JavaScript::defer("/pagetop/js/basic.menu.min.js").with_version("4.4.0")) - .alter_assets( + )) + .alter_assets(AssetsOp::AddJavaScript( + JavaScript::defer("/pagetop/js/basic.menu.min.js").with_version("4.4.0"), + )) + .alter_assets(AssetsOp::AddJavaScript( JavaScript::defer("/pagetop/js/basic.dropdown.min.js").with_version(PAGETOP_VERSION), - ) - .alter_assets( + )) + .alter_assets(AssetsOp::AddJavaScript( JavaScript::defer("/pagetop/js/basic.navbar.init.js").with_version(PAGETOP_VERSION), - ) - .alter_assets( + )) + .alter_assets(AssetsOp::AddJavaScript( JavaScript::defer("/pagetop/js/basic.dialog.min.js").with_version(PAGETOP_VERSION), - ) + )) .alter_child_in( &CoreRegions::Footer, ChildOp::AddIfEmpty(PoweredBy::new().into()), diff --git a/src/core/component/context.rs b/src/core/component/context.rs index 66ce4e08..a490cd26 100644 --- a/src/core/component/context.rs +++ b/src/core/component/context.rs @@ -2,28 +2,246 @@ use crate::auth::CurrentUser; use crate::core::TypeInfo; use crate::core::component::{ChildOp, Component, MessageLevel, StatusMessage}; use crate::core::theme::all::DEFAULT_THEME; -use crate::core::theme::{ChildrenInRegions, CoreRegions, CoreTemplates}; +use crate::core::theme::{Breakpoint, ChildrenInRegions, CoreRegions, CoreTemplates}; use crate::core::theme::{RegionRef, TemplateRef, ThemeRef}; use crate::html::{Assets, Favicon, JavaScript, Preload, ResponsiveStyles, StyleSheet}; use crate::html::{Markup, Props, PropsOp, RoutePath, html}; use crate::locale::Lc; use crate::locale::{LangId, LanguageIdentifier, RequestLocale}; use crate::web::HttpRequest; -use crate::{builder_impl, util}; +use crate::{CowStr, builder_impl, util}; use parking_lot::Mutex; +use thiserror::Error; use std::any::{Any, TypeId}; use std::collections::HashMap; -mod assets_op; -pub use assets_op::AssetsOp; +/// Operaciones para modificar recursos asociados al [`Context`] de un documento. +pub enum AssetsOp { + /// Define el *favicon* del documento. Sobrescribe cualquier valor anterior. + SetFavicon(Option), + /// Define el *favicon* solo si no se ha establecido previamente. + SetFaviconIfNone(Favicon), -mod error; -pub use error::ContextError; + /// Añade un recurso para precarga al documento. + AddPreload(Preload), + /// Elimina un recurso para precarga por su ruta. + RemovePreload(&'static str), -mod contextual; -pub use contextual::Contextual; + /// Añade una hoja de estilos CSS al documento. + AddStyleSheet(StyleSheet), + /// Elimina una hoja de estilos por su ruta o identificador. + RemoveStyleSheet(&'static str), + + /// Añade un script JavaScript al documento. + AddJavaScript(JavaScript), + /// Elimina un script por su ruta o identificador. + RemoveJavaScript(&'static str), + + /// Añade una declaración de estilo responsive (`property: value`) para las clases indicadas, + /// dentro del punto de corte dado (`None` para una regla siempre activa). Ver + /// [`ResponsiveStyles::add_style()`]. + AddResponsiveStyle(Option, CowStr, CowStr, CowStr), +} + +/// Errores de acceso a parámetros dinámicos del contexto. +#[derive(Debug, Error)] +pub enum ContextError { + /// La clave no existe. + #[error("parameter not found")] + ParamNotFound, + /// La clave existe, pero el valor guardado no coincide con el tipo solicitado. Incluye + /// nombre de la clave (`key`), tipo esperado (`expected`) y tipo realmente guardado (`saved`) + /// para facilitar el diagnóstico. + #[error("type mismatch for parameter \"{key}\": expected \"{expected}\", found \"{saved}\"")] + ParamTypeMismatch { + key: &'static str, + expected: &'static str, + saved: &'static str, + }, +} + +/// Interfaz para gestionar el **contexto de renderizado** de un documento HTML. +/// +/// `Contextual` extiende [`LangId`] para establecer el idioma del documento y añade métodos para: +/// +/// - Almacenar la **petición HTTP** de origen. +/// - Seleccionar la **plantilla** y el **tema** de renderizado. +/// - Administrar **recursos** del documento como el icono [`Favicon`], las hojas de estilo +/// [`StyleSheet`] o los scripts [`JavaScript`] mediante [`AssetsOp`]. +/// - Leer y mantener **parámetros dinámicos tipados** de contexto. +/// +/// Lo implementan, típicamente, estructuras que manejan el contexto de renderizado, como +/// [`Context`](crate::core::component::Context) o [`Page`](crate::response::Page). +/// +/// # Ejemplo +/// +/// ```rust,no_run +/// # use pagetop::prelude::*; +/// # use pagetop_aliner::Aliner; +/// fn prepare_context(cx: C) -> C { +/// cx.with_langid(&Locale::resolve("es-ES")) +/// .with_template(&CoreTemplates::Standard) +/// .with_theme(&Aliner) +/// .with_assets(AssetsOp::SetFavicon(Some(Favicon::new().with_icon("/favicon.ico")))) +/// .with_assets(AssetsOp::AddStyleSheet(StyleSheet::from("/css/app.css"))) +/// .with_assets(AssetsOp::AddJavaScript(JavaScript::defer("/js/app.js"))) +/// .with_param("user_id", 42_i32) +/// } +/// ``` +#[builder_impl] +pub trait Contextual: LangId { + // **< Contextual BUILDER >********************************************************************* + + /// Establece el idioma del documento. + fn with_langid(self, language: &impl LangId) -> Self; + + /// Almacena la petición HTTP de origen en el contexto. + /// + /// También recalcula el idioma ([`RequestLocale::from_request()`]) y + /// [`current_user()`](Self::current_user) a partir de la petición indicada, descartando + /// cualquier idioma forzado antes con [`with_langid()`](Self::with_langid) o el usuario ya + /// resuelto. Si necesitas forzar el idioma o el usuario, llama a `with_request()` primero en + /// la cadena de construcción, nunca después. + fn with_request(self, request: Option) -> Self; + + /// Especifica la plantilla para renderizar el documento. + fn with_template(self, template: TemplateRef) -> Self; + + /// Especifica el tema para renderizar el documento. + fn with_theme(self, theme: ThemeRef) -> Self; + + /// Añade o modifica un parámetro dinámico del contexto. + /// + /// El valor se almacena junto con el nombre de su tipo, lo que permite generar mensajes de + /// error precisos al recuperarlo con [`param`](Contextual::param) si el tipo solicitado no + /// coincide. + /// + /// # Ejemplo + /// + /// ```rust,no_run + /// # use pagetop::prelude::*; + /// let cx = Context::default() + /// .with_param("user_id", 42_i32) + /// .with_param("title", "Hello".to_string()) + /// .with_param("flags", vec!["a", "b"]); + /// ``` + fn with_param(self, key: &'static str, value: T) -> Self; + + /// Define los recursos del contexto usando [`AssetsOp`]. + fn with_assets(self, op: AssetsOp) -> Self; + + /// Modifica identificador, clases CSS, atributos HTML o valores extra del elemento ``. + fn with_body_props(self, op: PropsOp) -> Self; + + /// Añade un componente o aplica una operación [`ChildOp`] en la región por defecto del + /// documento. + fn with_child(self, op: impl Into) -> Self; + + /// Añade un componente o aplica una operación [`ChildOp`] en una región específica del + /// documento. + fn with_child_in(self, region: RegionRef, op: impl Into) -> Self; + + // **< Contextual GETTERS >********************************************************************* + + /// Devuelve una referencia a la petición HTTP asociada, si existe. + fn request(&self) -> Option<&HttpRequest>; + + /// Devuelve la identidad del usuario actual. + /// + /// Si ninguna extensión de autenticación ha inyectado un + /// [`CurrentUser`](crate::auth::CurrentUser) en las extensiones de la petición HTTP, devuelve + /// `&CurrentUser::Anonymous`. + /// + /// # Ejemplo + /// + /// ```rust,no_run + /// # use pagetop::prelude::*; + /// async fn greet(request: HttpRequest) -> Result { + /// let mut page = Page::new(request); + /// if page.current_user().is_authenticated() { + /// // Personalizar la página para el usuario autenticado. + /// } + /// page.render().await + /// } + /// ``` + fn current_user(&self) -> &CurrentUser; + + /// Devuelve la plantilla configurada para renderizar el documento. + fn template(&self) -> TemplateRef; + + /// Devuelve el tema que se usará para renderizar el documento. + fn theme(&self) -> ThemeRef; + + /// Recupera una *referencia tipada* al parámetro solicitado. + /// + /// Devuelve: + /// + /// - `Ok(&T)` si la clave existe y el tipo coincide. + /// - `Err(ContextError::ParamNotFound)` si la clave no existe. + /// - `Err(ContextError::ParamTypeMismatch)` si la clave existe pero el tipo no coincide. + /// + /// # Ejemplo + /// + /// ```rust + /// # use pagetop::prelude::*; + /// let cx = Context::default() + /// .with_param("user_id", 42_i32) + /// .with_param("title", "Hello".to_string()); + /// + /// let id: i32 = *cx.param("user_id").unwrap(); + /// let title: &String = cx.param("title").unwrap(); + /// + /// // Error de tipo: + /// assert!(cx.param::("user_id").is_err()); + /// ``` + fn param(&self, key: &'static str) -> Result<&T, ContextError>; + + /// Devuelve el parámetro clonado o el **valor por defecto del tipo** (`T::default()`). + fn param_or_default(&self, key: &'static str) -> T { + self.param::(key).ok().cloned().unwrap_or_default() + } + + /// Devuelve el parámetro clonado o un **valor por defecto** si no existe. + fn param_or(&self, key: &'static str, default: T) -> T { + self.param::(key).ok().cloned().unwrap_or(default) + } + + /// Devuelve el parámetro clonado o el **valor evaluado** por la función `f` si no existe. + fn param_or_else T>(&self, key: &'static str, f: F) -> T { + self.param::(key).ok().cloned().unwrap_or_else(f) + } + + /// Devuelve el Favicon de los recursos del contexto. + fn favicon(&self) -> Option<&Favicon>; + + /// Devuelve las hojas de estilo de los recursos del contexto. + fn stylesheets(&self) -> &Assets; + + /// Devuelve los scripts JavaScript de los recursos del contexto. + fn javascripts(&self) -> &Assets; + + /// Devuelve los estilos *responsive* acumulados en el contexto. + fn responsive_styles(&self) -> &ResponsiveStyles; + + /// Devuelve identificador, clases CSS, atributos HTML y valores extra del elemento ``. + fn body_props(&self) -> &Props; + + // **< Contextual HELPERS >********************************************************************* + + /// Elimina un parámetro del contexto. Devuelve `true` si la clave existía y se eliminó. + /// + /// # Ejemplo + /// + /// ```rust + /// # use pagetop::prelude::*; + /// let mut cx = Context::default().with_param("temp", 1u8); + /// assert!(cx.remove_param("temp")); + /// assert!(!cx.remove_param("temp")); // ya no existe + /// ``` + fn remove_param(&mut self, key: &'static str) -> bool; +} /// Implementa un **contexto de renderizado** para un documento HTML. /// @@ -62,11 +280,11 @@ pub use contextual::Contextual; /// // Establece el tema para renderizar. /// .with_theme(&Aliner) /// // Asigna un favicon. -/// .with_assets(Favicon::new().with_icon("/favicon.ico")) +/// .with_assets(AssetsOp::SetFavicon(Some(Favicon::new().with_icon("/favicon.ico")))) /// // Añade una hoja de estilo externa. -/// .with_assets(StyleSheet::from("/css/style.css")) +/// .with_assets(AssetsOp::AddStyleSheet(StyleSheet::from("/css/style.css"))) /// // Añade un script JavaScript. -/// .with_assets(JavaScript::defer("/js/main.js")) +/// .with_assets(AssetsOp::AddJavaScript(JavaScript::defer("/js/main.js"))) /// // Añade un parámetro dinámico al contexto. /// .with_param("user_id", 42); /// # cx } @@ -368,8 +586,8 @@ impl Contextual for Context { self } - fn with_assets(mut self, op: impl Into) -> Self { - match op.into() { + fn with_assets(mut self, op: AssetsOp) -> Self { + match op { // Favicon. AssetsOp::SetFavicon(favicon) => { self.favicon = favicon; @@ -405,9 +623,6 @@ impl Contextual for Context { self.responsives .add_style(breakpoint, classes, property, value); } - AssetsOp::AddResponsiveStyles(breakpoint, classes, styles) => { - self.responsives.add_styles(breakpoint, classes, styles); - } } self } diff --git a/src/core/component/context/assets_op.rs b/src/core/component/context/assets_op.rs deleted file mode 100644 index b0a59bdf..00000000 --- a/src/core/component/context/assets_op.rs +++ /dev/null @@ -1,169 +0,0 @@ -use crate::CowStr; -use crate::core::theme::Breakpoint; -use crate::html::{Favicon, JavaScript, Preload, StyleSheet}; - -/// Operaciones para modificar recursos asociados al [`Context`](super::Context) de un documento. -/// -/// [`Favicon`], [`Preload`], [`StyleSheet`] y [`JavaScript`] se convierten implícitamente en la -/// operación de añadir correspondiente (ver sus `impl From<...>` más abajo), por lo que no -/// necesitarían ningún constructor. Para el resto de operaciones, el método recomendado es recurrir -/// a los constructores asociados como [`remove_stylesheet()`], [`add_responsive_style()`], etc. -/// -/// [`remove_stylesheet()`]: Self::remove_stylesheet -/// [`add_responsive_style()`]: Self::add_responsive_style -pub enum AssetsOp { - /// Define el *favicon* del documento. Sobrescribe cualquier valor anterior. - SetFavicon(Option), - /// Define el *favicon* sólo si no se ha establecido previamente. - SetFaviconIfNone(Favicon), - - /// Añade un recurso para precarga al documento. - AddPreload(Preload), - /// Elimina un recurso para precarga por su ruta. - RemovePreload(&'static str), - - /// Añade una hoja de estilos CSS al documento. - AddStyleSheet(StyleSheet), - /// Elimina una hoja de estilos por su ruta. - RemoveStyleSheet(&'static str), - - /// Añade un script JavaScript al documento. - AddJavaScript(JavaScript), - /// Elimina un script por su ruta o identificador. - RemoveJavaScript(&'static str), - - /// Añade una declaración de estilo *responsive* (`property: value`) para las clases indicadas, - /// para un punto de corte dado (`None` para una regla siempre activa). Ver - /// [`ResponsiveStyles::add_style()`](crate::html::ResponsiveStyles::add_style). - AddResponsiveStyle(Option, CowStr, CowStr, CowStr), - /// Añade varias declaraciones de estilo *responsive* (`property: value`) para las clases - /// indicadas, para un punto de corte dado, en una única llamada. Ver - /// [`ResponsiveStyles::add_styles()`](crate::html::ResponsiveStyles::add_styles). - AddResponsiveStyles(Option, CowStr, Vec<(CowStr, CowStr)>), -} - -impl AssetsOp { - /// Crea la variante [`SetFavicon`](Self::SetFavicon) con el favicon indicado, o `None` para - /// eliminar cualquier favicon ya establecido. - pub fn set_favicon(favicon: impl Into>) -> Self { - Self::SetFavicon(favicon.into()) - } - - /// Crea la variante [`SetFaviconIfNone`](Self::SetFaviconIfNone) con el favicon indicado. - pub fn set_favicon_if_none(favicon: Favicon) -> Self { - Self::SetFaviconIfNone(favicon) - } - - /// Crea la variante [`AddPreload`](Self::AddPreload) con el recurso indicado. - pub fn add_preload(preload: Preload) -> Self { - Self::AddPreload(preload) - } - - /// Crea la variante [`RemovePreload`](Self::RemovePreload) para la ruta indicada. - pub fn remove_preload(path: &'static str) -> Self { - Self::RemovePreload(path) - } - - /// Crea la variante [`AddStyleSheet`](Self::AddStyleSheet) con la hoja de estilos indicada. - pub fn add_stylesheet(stylesheet: StyleSheet) -> Self { - Self::AddStyleSheet(stylesheet) - } - - /// Crea la variante [`RemoveStyleSheet`](Self::RemoveStyleSheet) para la ruta indicada. - pub fn remove_stylesheet(path: &'static str) -> Self { - Self::RemoveStyleSheet(path) - } - - /// Crea la variante [`AddJavaScript`](Self::AddJavaScript) con el script indicado. - pub fn add_javascript(js: JavaScript) -> Self { - Self::AddJavaScript(js) - } - - /// Crea la variante [`RemoveJavaScript`](Self::RemoveJavaScript) para la ruta o identificador - /// indicado. - pub fn remove_javascript(path: &'static str) -> Self { - Self::RemoveJavaScript(path) - } - - /// Crea la variante [`AddResponsiveStyle`](Self::AddResponsiveStyle) con la declaración de - /// estilo (`property: value`) indicada, para las clases y el punto de corte dados. - pub fn add_responsive_style( - breakpoint: impl Into>, - classes: impl Into, - property: impl Into, - value: impl Into, - ) -> Self { - Self::AddResponsiveStyle( - breakpoint.into(), - classes.into(), - property.into(), - value.into(), - ) - } - - /// Crea la variante [`AddResponsiveStyles`](Self::AddResponsiveStyles) con las declaraciones - /// de estilo (`property: value`) indicadas, para las clases y el punto de corte dados. - /// - /// ```rust,no_run - /// # use pagetop::prelude::*; - /// let op = AssetsOp::add_responsive_styles( - /// None, - /// "flex-demo-box", - /// [("background-color", "#0d6efd"), ("color", "#fff")], - /// ); - /// ``` - pub fn add_responsive_styles( - breakpoint: impl Into>, - classes: impl Into, - styles: impl IntoIterator, impl Into)>, - ) -> Self { - Self::AddResponsiveStyles( - breakpoint.into(), - classes.into(), - styles - .into_iter() - .map(|(p, v)| (p.into(), v.into())) - .collect(), - ) - } -} - -impl From for AssetsOp { - /// Convierte un favicon en [`AssetsOp::SetFavicon`] (lo sobrescribe siempre), permitiendo - /// pasarlo directamente a métodos como [`Contextual::with_assets`] sin envolverlo - /// explícitamente. Para establecerlo sólo si no hay uno ya definido, usar - /// [`AssetsOp::set_favicon_if_none()`] explícitamente. - /// - /// [`Contextual::with_assets`]: crate::core::component::Contextual::with_assets - #[inline] - fn from(favicon: Favicon) -> Self { - Self::SetFavicon(Some(favicon)) - } -} - -impl From for AssetsOp { - /// Convierte un recurso de precarga en [`AssetsOp::AddPreload`]. Ver la conversión - /// equivalente para [`Favicon`]. - #[inline] - fn from(preload: Preload) -> Self { - Self::AddPreload(preload) - } -} - -impl From for AssetsOp { - /// Convierte una hoja de estilos en [`AssetsOp::AddStyleSheet`]. Ver la conversión - /// equivalente para [`Favicon`]. - #[inline] - fn from(stylesheet: StyleSheet) -> Self { - Self::AddStyleSheet(stylesheet) - } -} - -impl From for AssetsOp { - /// Convierte un script en [`AssetsOp::AddJavaScript`]. Ver la conversión equivalente para - /// [`Favicon`]. - #[inline] - fn from(js: JavaScript) -> Self { - Self::AddJavaScript(js) - } -} diff --git a/src/core/component/context/contextual.rs b/src/core/component/context/contextual.rs deleted file mode 100644 index 3d436a81..00000000 --- a/src/core/component/context/contextual.rs +++ /dev/null @@ -1,195 +0,0 @@ -use super::{AssetsOp, ContextError}; - -use crate::auth::CurrentUser; -use crate::builder_impl; -use crate::core::component::ChildOp; -use crate::core::theme::{RegionRef, TemplateRef, ThemeRef}; -use crate::html::{Assets, Favicon, JavaScript, Props, PropsOp, ResponsiveStyles, StyleSheet}; -use crate::locale::LangId; -use crate::web::HttpRequest; - -/// Interfaz para gestionar el **contexto de renderizado** de un documento HTML. -/// -/// `Contextual` extiende [`LangId`] para establecer el idioma del documento y añade métodos para: -/// -/// - Almacenar la **petición HTTP** de origen. -/// - Seleccionar la **plantilla** y el **tema** de renderizado. -/// - Administrar **recursos** del documento como el icono [`Favicon`], las hojas de estilo -/// [`StyleSheet`] o los scripts [`JavaScript`], directamente o mediante una operación -/// [`AssetsOp`]. -/// - Leer y mantener **parámetros dinámicos tipados** de contexto. -/// -/// Lo implementan, típicamente, estructuras que manejan el contexto de renderizado, como -/// [`Context`](crate::core::component::Context) o [`Page`](crate::response::Page). -/// -/// # Ejemplo -/// -/// ```rust,no_run -/// # use pagetop::prelude::*; -/// # use pagetop_aliner::Aliner; -/// fn prepare_context(cx: C) -> C { -/// cx.with_langid(&Locale::resolve("es-ES")) -/// .with_template(&CoreTemplates::Standard) -/// .with_theme(&Aliner) -/// .with_assets(Favicon::new().with_icon("/favicon.ico")) -/// .with_assets(StyleSheet::from("/css/app.css")) -/// .with_assets(JavaScript::defer("/js/app.js")) -/// .with_param("user_id", 42_i32) -/// } -/// ``` -#[builder_impl] -pub trait Contextual: LangId { - // **< Contextual BUILDER >********************************************************************* - - /// Establece el idioma del documento. - fn with_langid(self, language: &impl LangId) -> Self; - - /// Almacena la petición HTTP de origen en el contexto. - /// - /// También recalcula el idioma ([`RequestLocale::from_request()`]) y - /// [`current_user()`](Self::current_user) a partir de la petición indicada, descartando - /// cualquier idioma forzado antes con [`with_langid()`](Self::with_langid) o el usuario ya - /// resuelto. Si necesitas forzar el idioma o el usuario, llama a `with_request()` primero en - /// la cadena de construcción, nunca después. - /// - /// [`RequestLocale::from_request()`]: crate::locale::RequestLocale::from_request - fn with_request(self, request: Option) -> Self; - - /// Especifica la plantilla para renderizar el documento. - fn with_template(self, template: TemplateRef) -> Self; - - /// Especifica el tema para renderizar el documento. - fn with_theme(self, theme: ThemeRef) -> Self; - - /// Añade o modifica un parámetro dinámico del contexto. - /// - /// El valor se almacena junto con el nombre de su tipo, lo que permite generar mensajes de - /// error precisos al recuperarlo con [`param`](Contextual::param) si el tipo solicitado no - /// coincide. - /// - /// # Ejemplo - /// - /// ```rust,no_run - /// # use pagetop::prelude::*; - /// let cx = Context::default() - /// .with_param("user_id", 42_i32) - /// .with_param("title", "Hello".to_string()) - /// .with_param("flags", vec!["a", "b"]); - /// ``` - fn with_param(self, key: &'static str, value: T) -> Self; - - /// Añade un recurso ([`Favicon`], [`StyleSheet`], [`JavaScript`] o - /// [`Preload`](crate::html::Preload)) directamente, o aplica una operación [`AssetsOp`] sobre - /// los recursos del contexto. - fn with_assets(self, op: impl Into) -> Self; - - /// Modifica identificador, clases CSS, atributos HTML o valores extra del elemento ``. - fn with_body_props(self, op: PropsOp) -> Self; - - /// Añade un componente o aplica una operación [`ChildOp`] en la región por defecto del - /// documento. - fn with_child(self, op: impl Into) -> Self; - - /// Añade un componente o aplica una operación [`ChildOp`] en una región específica del - /// documento. - fn with_child_in(self, region: RegionRef, op: impl Into) -> Self; - - // **< Contextual GETTERS >********************************************************************* - - /// Devuelve una referencia a la petición HTTP asociada, si existe. - fn request(&self) -> Option<&HttpRequest>; - - /// Devuelve la identidad del usuario actual. - /// - /// Si ninguna extensión de autenticación ha inyectado un - /// [`CurrentUser`](crate::auth::CurrentUser) en las extensiones de la petición HTTP, devuelve - /// `&CurrentUser::Anonymous`. - /// - /// # Ejemplo - /// - /// ```rust,no_run - /// # use pagetop::prelude::*; - /// async fn greet(request: HttpRequest) -> Result { - /// let mut page = Page::new(request); - /// if page.current_user().is_authenticated() { - /// // Personalizar la página para el usuario autenticado. - /// } - /// page.render().await - /// } - /// ``` - fn current_user(&self) -> &CurrentUser; - - /// Devuelve la plantilla configurada para renderizar el documento. - fn template(&self) -> TemplateRef; - - /// Devuelve el tema que se usará para renderizar el documento. - fn theme(&self) -> ThemeRef; - - /// Recupera una *referencia tipada* al parámetro solicitado. - /// - /// Devuelve: - /// - /// - `Ok(&T)` si la clave existe y el tipo coincide. - /// - `Err(ContextError::ParamNotFound)` si la clave no existe. - /// - `Err(ContextError::ParamTypeMismatch)` si la clave existe pero el tipo no coincide. - /// - /// # Ejemplo - /// - /// ```rust - /// # use pagetop::prelude::*; - /// let cx = Context::default() - /// .with_param("user_id", 42_i32) - /// .with_param("title", "Hello".to_string()); - /// - /// let id: i32 = *cx.param("user_id").unwrap(); - /// let title: &String = cx.param("title").unwrap(); - /// - /// // Error de tipo: - /// assert!(cx.param::("user_id").is_err()); - /// ``` - fn param(&self, key: &'static str) -> Result<&T, ContextError>; - - /// Devuelve el parámetro clonado o el **valor por defecto del tipo** (`T::default()`). - fn param_or_default(&self, key: &'static str) -> T { - self.param::(key).ok().cloned().unwrap_or_default() - } - - /// Devuelve el parámetro clonado o un **valor por defecto** si no existe. - fn param_or(&self, key: &'static str, default: T) -> T { - self.param::(key).ok().cloned().unwrap_or(default) - } - - /// Devuelve el parámetro clonado o el **valor evaluado** por la función `f` si no existe. - fn param_or_else T>(&self, key: &'static str, f: F) -> T { - self.param::(key).ok().cloned().unwrap_or_else(f) - } - - /// Devuelve el Favicon de los recursos del contexto. - fn favicon(&self) -> Option<&Favicon>; - - /// Devuelve las hojas de estilo de los recursos del contexto. - fn stylesheets(&self) -> &Assets; - - /// Devuelve los scripts JavaScript de los recursos del contexto. - fn javascripts(&self) -> &Assets; - - /// Devuelve los estilos *responsive* acumulados en el contexto. - fn responsive_styles(&self) -> &ResponsiveStyles; - - /// Devuelve identificador, clases CSS, atributos HTML y valores extra del elemento ``. - fn body_props(&self) -> &Props; - - // **< Contextual HELPERS >********************************************************************* - - /// Elimina un parámetro del contexto. Devuelve `true` si la clave existía y se eliminó. - /// - /// # Ejemplo - /// - /// ```rust - /// # use pagetop::prelude::*; - /// let mut cx = Context::default().with_param("temp", 1u8); - /// assert!(cx.remove_param("temp")); - /// assert!(!cx.remove_param("temp")); // ya no existe - /// ``` - fn remove_param(&mut self, key: &'static str) -> bool; -} diff --git a/src/core/component/context/error.rs b/src/core/component/context/error.rs deleted file mode 100644 index 15fcd070..00000000 --- a/src/core/component/context/error.rs +++ /dev/null @@ -1,18 +0,0 @@ -use thiserror::Error; - -/// Errores de acceso a parámetros dinámicos del contexto. -#[derive(Debug, Error)] -pub enum ContextError { - /// La clave no existe. - #[error("parameter not found")] - ParamNotFound, - /// La clave existe, pero el valor guardado no coincide con el tipo solicitado. Incluye - /// nombre de la clave (`key`), tipo esperado (`expected`) y tipo realmente guardado (`saved`) - /// para facilitar el diagnóstico. - #[error("type mismatch for parameter \"{key}\": expected \"{expected}\", found \"{saved}\"")] - ParamTypeMismatch { - key: &'static str, - expected: &'static str, - saved: &'static str, - }, -} diff --git a/src/html.rs b/src/html.rs index 80881899..25e03603 100644 --- a/src/html.rs +++ b/src/html.rs @@ -37,12 +37,6 @@ pub use unit::UnitValue; // **< HTML LAYOUT >******************************************************************************** -mod responsive; - pub mod flex; #[doc(inline)] pub use flex::{Flex, FlexItem}; - -pub mod spacing; -#[doc(inline)] -pub use spacing::{Margin, Padding}; diff --git a/src/html/assets/responsive.rs b/src/html/assets/responsive.rs index 7b78d14b..ef839823 100644 --- a/src/html/assets/responsive.rs +++ b/src/html/assets/responsive.rs @@ -19,9 +19,9 @@ type Entry = (Option, CowStr, Vec<(CowStr, CowStr)>); /// El punto de corte es opcional, donde `None` declara una regla siempre activa, sin pasar por el /// tema ni depender de que resuelva algún [`Breakpoint`]. No ordena ni combina las clases de dos /// llamadas que las declaren en distinto orden (por ejemplo, `"foo bar"` y `"bar foo"` generan dos -/// entradas distintas). La llamada es a través de [`AssetsOp::add_responsive_style()`]. +/// entradas distintas). La llamada es a través de [`AssetsOp::AddResponsiveStyle`]. /// -/// [`AssetsOp::add_responsive_style()`]: crate::core::component::AssetsOp::add_responsive_style +/// [`AssetsOp::AddResponsiveStyle`]: crate::core::component::AssetsOp::AddResponsiveStyle #[derive(AutoDefault, Clone, Debug)] pub struct ResponsiveStyles(Vec); @@ -50,92 +50,48 @@ impl ResponsiveStyles { value: impl AsRef, ) { let breakpoint = breakpoint.into(); - let Some(classes) = Self::normalize_classes(classes.as_ref()) else { + + let Some(classes) = util::normalize_ascii(classes.as_ref()) else { return; }; - - match self - .0 - .iter_mut() - .find(|(bp, cls, _)| *bp == breakpoint && *cls == classes) - { - Some((_, _, styles)) => Self::insert_style(styles, property.as_ref(), value.as_ref()), - None => { - let mut styles = Vec::new(); - Self::insert_style(&mut styles, property.as_ref(), value.as_ref()); - if !styles.is_empty() { - self.0.push((breakpoint, classes, styles)); - } - } - } - } - - /// Añade varias declaraciones de estilo (`property: value`) para las clases indicadas, dentro - /// del punto de corte dado, en una única llamada. - /// - /// Equivale a invocar [`add_style()`](Self::add_style) una vez por cada par `(property, - /// value)` de `styles`, con las mismas reglas de normalización y de descarte silencioso, pero - /// normalizando `classes` y localizando la entrada una sola vez para todo el lote. - pub fn add_styles( - &mut self, - breakpoint: impl Into>, - classes: impl AsRef, - styles: impl IntoIterator, impl AsRef)>, - ) { - let breakpoint = breakpoint.into(); - let Some(classes) = Self::normalize_classes(classes.as_ref()) else { - return; - }; - - match self - .0 - .iter_mut() - .find(|(bp, cls, _)| *bp == breakpoint && *cls == classes) - { - Some((_, _, existing)) => { - for (property, value) in styles { - Self::insert_style(existing, property.as_ref(), value.as_ref()); - } - } - None => { - let mut new_styles = Vec::new(); - for (property, value) in styles { - Self::insert_style(&mut new_styles, property.as_ref(), value.as_ref()); - } - if !new_styles.is_empty() { - self.0.push((breakpoint, classes, new_styles)); - } - } - } - } - - // Normaliza `classes` para usarla como clave de entrada. Devuelve `None` si contiene caracteres - // no ASCII o si el resultado queda vacío tras recortar espacios. Compartida por `add_style()`, - // `add_styles()` y `entry()`. - fn normalize_classes(classes: &str) -> Option { - let classes = util::normalize_ascii(classes)?; if classes.is_empty() { - return None; + return; } - Some(classes.into_owned().into()) - } + let classes: CowStr = classes.into_owned().into(); - // Inserta una declaración (`property: value`) en la lista de destino, ya localizada por el - // llamador. Aplica las mismas reglas que `add_style()`: normaliza `property`, descarta si ya - // existe una declaración para esa propiedad (sin normalizar `value`, el camino barato cuando - // muchos componentes comparten la misma clase utilitaria) y descarta si `property` o `value` - // quedan vacíos tras recortar espacios. - fn insert_style(styles: &mut Vec<(CowStr, CowStr)>, property: &str, value: &str) { - let Some(property) = util::normalize_property(property) else { - return; - }; - if styles.iter().any(|(k, _)| k.as_ref() == property) { + let property_norm = property.as_ref().trim().to_ascii_lowercase(); + if property_norm.is_empty() { return; } - let Some(value) = util::non_blank(value) else { - return; - }; - styles.push((property.into(), value.to_string().into())); + let property: CowStr = property_norm.into(); + + match self + .0 + .iter_mut() + .find(|(bp, cls, _)| *bp == breakpoint && *cls == classes) + { + Some((_, _, styles)) => { + // Ya declarada: se descarta sin normalizar `value`, el camino habitual (y que debe + // ser barato) cuando muchos componentes comparten la misma clase utilitaria. + if styles.iter().any(|(k, _)| *k == property) { + return; + } + let Some(value) = util::non_blank(value.as_ref()) else { + return; + }; + styles.push((property, value.to_string().into())); + } + None => { + let Some(value) = util::non_blank(value.as_ref()) else { + return; + }; + self.0.push(( + breakpoint, + classes, + vec![(property, value.to_string().into())], + )); + } + } } // **< ResponsiveStyles GETTERS >*************************************************************** @@ -149,7 +105,7 @@ impl ResponsiveStyles { property: impl AsRef, ) -> Option { let styles = self.entry(breakpoint.into(), classes.as_ref())?; - let property = util::normalize_property(property)?; + let property = property.as_ref().trim().to_ascii_lowercase(); styles .iter() .find(|(k, _)| k.as_ref() == property) @@ -241,17 +197,20 @@ impl ResponsiveStyles { rules } - // Normaliza `classes` y busca las declaraciones de la entrada correspondiente al punto de corte - // y las clases dados. + // Normaliza `classes` igual que `add_style()` y busca las declaraciones de la entrada + // correspondiente al punto de corte y las clases dados. fn entry( &self, breakpoint: Option, classes: &str, ) -> Option<&Vec<(CowStr, CowStr)>> { - let classes = Self::normalize_classes(classes)?; + let classes = util::normalize_ascii(classes)?; + if classes.is_empty() { + return None; + } self.0 .iter() - .find(|(bp, cls, _)| *bp == breakpoint && *cls == classes) + .find(|(bp, cls, _)| *bp == breakpoint && cls.as_ref() == classes.as_ref()) .map(|(_, _, styles)| styles) } } diff --git a/src/html/assets/stylesheet.rs b/src/html/assets/stylesheet.rs index 6872a4c2..35e547ac 100644 --- a/src/html/assets/stylesheet.rs +++ b/src/html/assets/stylesheet.rs @@ -1,8 +1,24 @@ use crate::core::component::Context; use crate::html::assets::Asset; -use crate::html::{Markup, html}; +use crate::html::{Markup, PreEscaped, html}; use crate::{AutoDefault, CowStr, Weight, util}; +/// Define el origen del recurso CSS y cómo se incluye en el documento. +/// +/// Los estilos pueden cargarse desde un archivo externo o estar embebidos directamente en una +/// etiqueta ``. El parámetro `name` se usa como identificador interno del + /// recurso. + /// + /// Un closure recibirá el [`Context`] por si se necesita durante el renderizado. + pub fn inline(name: impl Into, f: F) -> Self + where + F: Fn(&mut Context) -> String + Send + Sync + 'static, + { + Self { + source: Source::Inline(name.into(), Box::new(f)), ..Default::default() } } @@ -113,9 +147,14 @@ impl StyleSheet { } impl Asset for StyleSheet { - /// Devuelve la ruta del recurso, utilizada como clave única. + /// Devuelve el nombre del recurso, utilizado como clave única. + /// + /// Para hojas de estilos externas es la ruta del recurso; para las embebidas, un identificador. fn name(&self) -> &str { - &self.path + match &self.source { + Source::From(path) => path, + Source::Inline(name, _) => name, + } } fn weight(&self) -> Weight { @@ -124,12 +163,17 @@ impl Asset for StyleSheet { // **< StyleSheet RENDER >********************************************************************** - fn render(&self, _cx: &mut Context) -> Markup { - html! { - link - rel="stylesheet" - href=(util::join_pair!(&self.path, "?v=", &self.version)) - media=[self.media.as_str()]; + fn render(&self, cx: &mut Context) -> Markup { + match &self.source { + Source::From(path) => html! { + link + rel="stylesheet" + href=(util::join_pair!(path, "?v=", &self.version)) + media=[self.media.as_str()]; + }, + Source::Inline(_, f) => html! { + style { (PreEscaped((f)(cx))) }; + }, } } } diff --git a/src/html/flex.rs b/src/html/flex.rs index 6f4c1b5c..72adf0cb 100644 --- a/src/html/flex.rs +++ b/src/html/flex.rs @@ -16,16 +16,20 @@ //! manera independiente a cualquier tema o framework CSS. El nombre interno de cada clase se deriva //! de la propiedad y el valor que representa, así que dos elementos con la misma configuración //! comparten la misma regla en vez de duplicarla. Las declaraciones correspondientes se registran -//! vía [`AssetsOp::add_responsive_style()`] y se renderizan como reglas en el `` del -//! documento. 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. +//! vía [`AssetsOp::AddResponsiveStyle`] y se renderizan como reglas en el `` del documento. +//! 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 -//! [`AssetsOp::add_responsive_style()`]: crate::core::component::AssetsOp::add_responsive_style +//! [`AssetsOp::AddResponsiveStyle`]: crate::core::component::AssetsOp::AddResponsiveStyle //! [`PropsOp::flex_item()`]: crate::html::props::PropsOp::flex_item //! [`Container`]: crate::base::component::Container //! [`Navbar`]: crate::base::component::Navbar +use crate::CowStr; +use crate::core::component::{AssetsOp, Context, Contextual}; +use crate::core::theme::BreakpointEntry; + mod props_container; pub use props_container::{Align, AlignContent, Behavior, ContentJustify, Direction, Gap}; @@ -37,3 +41,83 @@ pub use container::Flex; mod item; pub use item::FlexItem; + +// **< Flex / FlexItem PRIVATE >******************************************************************** + +// Sustituye, en un valor CSS ya resuelto, los únicos caracteres (`.`, `%`) que no podrían usarse +// como fragmento de un nombre de clase. Así, `"1.5rem"` sería `"1_5rem"` y `"33.3333%"` quedaría +// como `"33_3333pct"`. +fn value_to_token(value: &str) -> String { + value.replace('.', "_").replace('%', "pct") +} + +// Añade un estilo (`property: value`) al punto de corte indicado, y la clase a `classes`, separada +// con un espacio de las que ya hubiera. Recibe un `BreakpointEntry` ya resuelto (ver +// `Breakpoint::resolved()`) y extrae aquí el `Breakpoint` que `AddResponsiveStyle` necesita, que +// puede ser nulo si aplica siempre. +// +// La clase se copia al acumulador y se mueve al `AssetsOp`, sin clonarla. Recibirla como `CowStr` +// permite además que las clases fijas, las que no dependen de ningún punto de corte, lleguen como +// `&'static str` sin asignar memoria. +fn styles( + cx: &mut Context, + classes: &mut String, + entry: Option, + class: CowStr, + property: &'static str, + value: CowStr, +) { + if !classes.is_empty() { + classes.push(' '); + } + classes.push_str(&class); + + cx.alter_assets(AssetsOp::AddResponsiveStyle( + entry.map(|e| e.breakpoint), + class, + property.into(), + value, + )); +} + +// Nombre de clase según el punto de corte: `prefix` ya incluye el guion bajo final antes del valor +// (p. ej. `"_flex-direction_"`), y `entry` añade su sufijo si aplica (`"_flex-direction_row_md_"`), +// ya resuelto para el tema activo (ver `Breakpoint::resolved()`). +macro_rules! responsive_class { + ($prefix:expr, $token:expr, $entry:expr) => { + match $entry { + None => util::join!($prefix, $token, "_"), + Some(entry) => util::join!($prefix, $token, "_", entry.name, "_"), + } + }; +} +use responsive_class; + +// Recorre las entradas para una propiedad `Responsive` cuyo valor CSS es un único `T::value()`, +// generando y registrando (vía `styles()`) una clase por punto de corte con valor. +// +// La forma con el marcador final `val` es para propiedades cuyo valor puede contener `.`/`%` (como +// `ItemSize` o `ItemOffset` en `FlexItem`) y necesitan pasar por `value_to_token()`. +macro_rules! apply { + ($cx:expr, $classes:expr, $field:expr, $prefix:literal, $property:literal) => { + for (bp, value) in $field.by_breakpoint() { + let value = value.value(); + if !value.is_empty() { + let entry = bp.resolved($cx); + let class = responsive_class!($prefix, value, entry); + styles($cx, $classes, entry, class.into(), $property, value); + } + } + }; + ($cx:expr, $classes:expr, $field:expr, $prefix:literal, $property:literal, val) => { + for (bp, value) in $field.by_breakpoint() { + let value = value.value(); + if !value.is_empty() { + let entry = bp.resolved($cx); + let class = responsive_class!($prefix, value_to_token(&value), entry); + styles($cx, $classes, entry, class.into(), $property, value); + } + } + }; +} +use apply; diff --git a/src/html/flex/container.rs b/src/html/flex/container.rs index 52d9ebd9..e9e7e1fc 100644 --- a/src/html/flex/container.rs +++ b/src/html/flex/container.rs @@ -23,7 +23,7 @@ enum DisplayFlex { /// /// Se resuelve como clases CSS generadas dinámicamente (`display`, `flex-direction`, `flex-wrap`, /// `justify-content`, `align-items`, `align-content`, `gap`), registradas vía -/// [`AssetsOp::add_responsive_style()`] en [`ResponsiveStyles`] y renderizadas como reglas en el +/// [`AssetsOp::AddResponsiveStyle`] en [`ResponsiveStyles`] y renderizadas como reglas en el /// `` del documento. Son propiedades nativas que no requieren interpretación por parte de los /// temas, siempre funcionan igual, sin una sola línea de CSS ni de código específico. /// @@ -32,7 +32,7 @@ enum DisplayFlex { /// regla generada en vez de duplicarla, y el nombre generado no coincide por accidente con clases /// de terceros. /// -/// [`AssetsOp::add_responsive_style()`]: crate::core::component::AssetsOp::add_responsive_style +/// [`AssetsOp::AddResponsiveStyle`]: crate::core::component::AssetsOp::AddResponsiveStyle /// [`ResponsiveStyles`]: crate::html::ResponsiveStyles /// /// # Ejemplo @@ -238,7 +238,7 @@ impl Flex { return; }; - use crate::html::responsive::{apply, responsive_class, styles, value_to_token}; + use super::{apply, responsive_class, styles, value_to_token}; let (prefix, value) = match display { DisplayFlex::Always diff --git a/src/html/flex/item.rs b/src/html/flex/item.rs index bec22bba..e512a4f6 100644 --- a/src/html/flex/item.rs +++ b/src/html/flex/item.rs @@ -214,7 +214,7 @@ impl FlexItem { /// cadenas intermedias. #[rustfmt::skip] pub(crate) fn apply(self, cx: &mut Context, classes: &mut String) { - use crate::html::responsive::{apply, responsive_class, styles, value_to_token}; + use super::{apply, responsive_class, styles, value_to_token}; apply!(cx, classes, self.grow, "_flex-item-grow_", "flex-grow"); apply!(cx, classes, self.shrink, "_flex-item-shrink_", "flex-shrink"); diff --git a/src/html/props/definition.rs b/src/html/props/definition.rs index 1c7d9091..fd2f1059 100644 --- a/src/html/props/definition.rs +++ b/src/html/props/definition.rs @@ -3,7 +3,6 @@ use crate::core::component::Context; use crate::html::flex::{Flex, FlexItem}; use crate::html::maud::{Escaper, RenderAttrs}; use crate::html::props::{PropsError, PropsExtra, PropsOp}; -use crate::html::spacing::{Margin, Padding}; use crate::{AutoDefault, CowStr, builder_impl, trace, util}; use std::collections::HashMap; @@ -191,8 +190,6 @@ pub struct Props { attrs: Vec<(CowStr, CowStr)>, extras: HashMap<&'static str, PropsExtra>, flex_item: FlexItem, - margin: Margin, - padding: Padding, } #[builder_impl] @@ -216,8 +213,7 @@ impl Props { } /// Modifica el identificador, las clases, los atributos o los valores extra según la operación - /// indicada, incluido el posicionamiento Flexbox y el espaciado (`FlexItem`, `Margin`, - /// `Padding`). El método recomendado para construir cada operación es usar los constructores de + /// indicada. El método recomendado para construir cada operación es usar los constructores de /// [`PropsOp`]. pub fn with_prop(mut self, op: PropsOp) -> Self { match op { @@ -343,12 +339,6 @@ impl Props { PropsOp::FlexItem(placement) => { self.flex_item = self.flex_item.merge(placement); } - PropsOp::Margin(margin) => { - self.margin = self.margin.merge(margin); - } - PropsOp::Padding(padding) => { - self.padding = self.padding.merge(padding); - } } self } @@ -388,7 +378,7 @@ impl Props { /// Devuelve el valor de la propiedad de estilo indicada, si existe. pub fn get_style(&self, property: impl AsRef) -> Option { - let property = util::normalize_property(property)?; + let property = property.as_ref().trim().to_ascii_lowercase(); self.styles .iter() .find(|(k, _)| k.as_ref() == property) @@ -570,9 +560,9 @@ impl Props { /// /// `Props` no implementa [`RenderAttrs`] directamente. Obliga a pasar siempre el `Context` /// vigente en el punto donde se renderiza, aunque no lo necesite ningún atributo propio. Recibe - /// `&mut Context` porque aquí, en el momento de extraer los atributos, es donde se resuelven - /// [`FlexItem`], [`Margin`] y [`Padding`]: las clases que devuelven se añaden a las del propio - /// componente al escribir el atributo `class`. + /// `&mut Context` porque aquí, en el momento de extraer los atributos, es donde se resuelve + /// [`FlexItem`]: las clases que devuelve se añaden a las del propio componente al escribir el + /// atributo `class`. /// /// Si el propio elemento actúa además como contenedor [`Flex`], utiliza [`unpack_with_flex()`] /// en su lugar. @@ -588,14 +578,10 @@ impl Props { /// [`html!`]: crate::html::html /// [`Flex`]: crate::html::flex::Flex /// [`FlexItem`]: crate::html::flex::FlexItem - /// [`Margin`]: crate::html::spacing::Margin - /// [`Padding`]: crate::html::spacing::Padding /// [`unpack_with_flex()`]: Self::unpack_with_flex pub fn unpack<'a>(&'a self, cx: &mut Context) -> impl RenderAttrs + 'a { let mut classes = String::new(); self.flex_item.apply(cx, &mut classes); - self.margin.apply(cx, &mut classes); - self.padding.apply(cx, &mut classes); PropsUnpack { props: self, classes, @@ -604,23 +590,20 @@ impl Props { /// Igual que [`unpack()`], pero además resuelve `flex` con el posicionamiento [`Flex`]. /// - /// A diferencia de [`FlexItem`]/[`Margin`]/[`Padding`] (que se acumulan con sus respectivas - /// variantes de `PropsOp` porque cualquier componente ajeno puede necesitarlos sin tener un - /// campo propio para ello), `Flex` sólo tiene sentido en los contenedores que ya declaran su - /// propio campo `flex: Flex` (como `Container` o `Navbar`). Se les pasa aquí directamente, ya - /// resuelto (`self.flex()`), sin pasar por `PropsOp`. + /// A diferencia de [`FlexItem`] (que se acumula con [`PropsOp::FlexItem`] porque cualquier + /// componente ajeno puede necesitarlo sin tener un campo propio para ello), `Flex` sólo tiene + /// sentido en los contenedores que ya declaran su propio campo `flex: Flex` (`Container`, + /// `Navbar`...): se les pasa aquí directamente, ya resuelto (`self.flex()`), sin pasar por + /// `PropsOp`. /// /// [`unpack()`]: Self::unpack /// [`Flex`]: crate::html::flex::Flex /// [`FlexItem`]: crate::html::flex::FlexItem - /// [`Margin`]: crate::html::spacing::Margin - /// [`Padding`]: crate::html::spacing::Padding + /// [`PropsOp::FlexItem`]: crate::html::props::PropsOp::FlexItem pub fn unpack_with_flex<'a>(&'a self, cx: &mut Context, flex: Flex) -> impl RenderAttrs + 'a { let mut classes = String::new(); flex.apply(cx, &mut classes); self.flex_item.apply(cx, &mut classes); - self.margin.apply(cx, &mut classes); - self.padding.apply(cx, &mut classes); PropsUnpack { props: self, classes, @@ -656,9 +639,10 @@ impl Props { // la documentación de `PropsOp::AddStyle` sobre por qué los valores de estilo no se restringen // a ASCII. fn set_style(&mut self, property: &str, value: &str) { - let Some(property) = util::normalize_property(property) else { + let Some(property) = util::non_blank(property) else { return; }; + let property = property.to_ascii_lowercase(); let Some(value) = util::non_blank(value) else { return; }; @@ -719,9 +703,8 @@ impl Props { // Elimina la propiedad de estilo indicada, si existe. fn remove_style(&mut self, property: &str) { - if let Some(property) = util::normalize_property(property) { - self.styles.retain(|(k, _)| k.as_ref() != property); - }; + let property = property.trim().to_ascii_lowercase(); + self.styles.retain(|(k, _)| k.as_ref() != property); } } @@ -817,9 +800,8 @@ impl Props { // **< PropsUnpack >******************************************************************************** // Devuelto por `Props::unpack()`/`Props::unpack_with_flex()`. `classes` son las clases resueltas -// por `Flex::apply()`/`FlexItem::apply()`/`Margin::apply()`/`Padding::apply()` (desde el propio -// `unpack*()` usando el `&mut Context`), pendientes sólo de añadir a las del componente (ver -// `Props::write_attrs()`). +// por `FlexItem::apply()`/`Flex::apply()` (desde el propio `unpack*()` usando el `&mut Context`), +// pendientes sólo de añadir a las del componente (ver `Props::write_attrs()`). struct PropsUnpack<'a> { props: &'a Props, classes: String, diff --git a/src/html/props/op.rs b/src/html/props/op.rs index 4f703162..d67c2e78 100644 --- a/src/html/props/op.rs +++ b/src/html/props/op.rs @@ -2,7 +2,6 @@ use crate::CowStr; use crate::core::TypeInfo; use crate::html::flex::FlexItem; use crate::html::props::extra::PropsExtra; -use crate::html::spacing::{Margin, Padding}; use std::any::Any; use std::sync::Arc; @@ -41,9 +40,8 @@ use std::sync::Arc; /// se interpretan como si /// fueran valores internos del componente para tomar decisiones durante el renderizado. /// -/// [`FlexItem`](Self::FlexItem) aplica un posicionamiento Flexbox a nivel de ítem, mientras que -/// [`Margin`](Self::Margin) y [`Padding`](Self::Padding) aplican márgenes y relleno interno; los -/// tres sobre cualquier componente. +/// Finalmente, [`FlexItem`](Self::FlexItem) aplica un posicionamiento Flexbox a nivel de ítem sobre +/// cualquier componente. #[derive(Clone, Debug)] pub enum PropsOp { /// Establece el identificador del componente normalizando el valor: recorta espacios, convierte @@ -115,7 +113,7 @@ pub enum PropsOp { /// Elimina el valor extra asociado a la clave indicada, si existe. RemoveExtra(&'static str), /// Aplica un posicionamiento [`FlexItem`] a un componente particular en un contenedor [`Flex`]. - /// Añade directamente sus estilos Flexbox al propio componente, generando clases CSS dinámicas. + /// 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 @@ -130,15 +128,6 @@ pub enum PropsOp { /// [`Flex`]: crate::html::flex::Flex /// [`Container::flex()`]: crate::base::component::Container::flex FlexItem(FlexItem), - /// Aplica un margen [`Margin`] a un componente particular. Añade directamente sus estilos al - /// propio componente, generando clases CSS dinámicas. - /// - /// Como [`FlexItem`](Self::FlexItem), existe como variante de `PropsOp` y no como método - /// builder de un componente, porque el margen tiene sentido sobre **cualquier** componente. - Margin(Margin), - /// Aplica un relleno interno [`Padding`] a un componente particular, siguiendo los mismos - /// criterios que [`Margin`](Self::Margin). - Padding(Padding), } impl PropsOp { @@ -280,14 +269,4 @@ impl PropsOp { pub fn flex_item(placement: FlexItem) -> Self { Self::FlexItem(placement) } - - /// Crea la variante [`Margin`](Self::Margin) con el margen indicado. - pub fn margin(margin: Margin) -> Self { - Self::Margin(margin) - } - - /// Crea la variante [`Padding`](Self::Padding) con el relleno interno indicado. - pub fn padding(padding: Padding) -> Self { - Self::Padding(padding) - } } diff --git a/src/html/responsive.rs b/src/html/responsive.rs deleted file mode 100644 index 4d0b8bfe..00000000 --- a/src/html/responsive.rs +++ /dev/null @@ -1,94 +0,0 @@ -//! Mecanismo interno compartido para resolver clases CSS nativas *responsive*. -//! -//! Usado por [`flex`] y [`spacing`]. Ambos módulos resuelven su configuración generando clases CSS -//! dinámicamente, de manera independiente a cualquier tema o framework CSS, registradas vía -//! [`AssetsOp::add_responsive_style()`] y renderizadas como reglas en el `` del documento. -//! El nombre interno de cada clase se deriva de la propiedad y el valor que representa, así que dos -//! elementos con la misma configuración comparten la misma regla en vez de duplicarla. -//! -//! [`flex`]: crate::html::flex -//! [`spacing`]: crate::html::spacing -//! [`AssetsOp::add_responsive_style()`]: crate::core::component::AssetsOp::add_responsive_style - -use crate::CowStr; -use crate::core::component::{AssetsOp, Context, Contextual}; -use crate::core::theme::BreakpointEntry; - -// Sustituye, en un valor CSS ya resuelto, los únicos caracteres (`.`, `%`) que no podrían usarse -// como fragmento de un nombre de clase. Así, `"1.5rem"` sería `"1_5rem"` y `"33.3333%"` quedaría -// como `"33_3333pct"`. -pub(crate) fn value_to_token(value: &str) -> String { - value.replace('.', "_").replace('%', "pct") -} - -// Añade un estilo (`property: value`) al punto de corte indicado, y la clase a `classes`, separada -// con un espacio de las que ya hubiera. Recibe un `BreakpointEntry` ya resuelto (ver -// `Breakpoint::resolved()`) y extrae aquí el `Breakpoint` que `AddResponsiveStyle` necesita, que -// puede ser nulo si aplica siempre. -// -// La clase se copia al acumulador y se mueve al `AssetsOp`, sin clonarla. Recibirla como `CowStr` -// permite además que las clases fijas, las que no dependen de ningún punto de corte, lleguen como -// `&'static str` sin asignar memoria. -pub(crate) fn styles( - cx: &mut Context, - classes: &mut String, - entry: Option, - class: CowStr, - property: &'static str, - value: CowStr, -) { - if !classes.is_empty() { - classes.push(' '); - } - classes.push_str(&class); - - cx.alter_assets(AssetsOp::add_responsive_style( - entry.map(|e| e.breakpoint), - class, - property, - value, - )); -} - -// Nombre de clase según el punto de corte: `prefix` ya incluye el guion bajo final antes del valor -// (p. ej. `"_flex-direction_"`), y `entry` añade su sufijo si aplica (`"_flex-direction_row_md_"`), -// ya resuelto para el tema activo (ver `Breakpoint::resolved()`). -macro_rules! responsive_class { - ($prefix:expr, $token:expr, $entry:expr) => { - match $entry { - None => util::join!($prefix, $token, "_"), - Some(entry) => util::join!($prefix, $token, "_", entry.name, "_"), - } - }; -} -pub(crate) use responsive_class; - -// Recorre las entradas para una propiedad `Responsive` cuyo valor CSS es un único `T::value()`, -// generando y registrando (vía `styles()`) una clase por punto de corte con valor. -// -// La forma con el marcador final `val` es para propiedades cuyo valor puede contener `.`/`%` (como -// `ItemSize`/`ItemOffset` en `FlexItem`, o `UnitValue` en `Margin`/`Padding`) y necesitan pasar por -// `value_to_token()`. -macro_rules! apply { - ($cx:expr, $classes:expr, $field:expr, $prefix:literal, $property:literal) => { - for (bp, value) in $field.by_breakpoint() { - let value = value.value(); - if !value.is_empty() { - let entry = bp.resolved($cx); - let class = responsive_class!($prefix, value, entry); - styles($cx, $classes, entry, class.into(), $property, value); - } - } - }; - ($cx:expr, $classes:expr, $field:expr, $prefix:literal, $property:literal, val) => { - for (bp, value) in $field.by_breakpoint() { - let value = value.value(); - if !value.is_empty() { - let entry = bp.resolved($cx); - let class = responsive_class!($prefix, value_to_token(&value), entry); - styles($cx, $classes, entry, class.into(), $property, value); - } - } - }; -} -pub(crate) use apply; diff --git a/src/html/spacing.rs b/src/html/spacing.rs deleted file mode 100644 index 4fb43451..00000000 --- a/src/html/spacing.rs +++ /dev/null @@ -1,30 +0,0 @@ -//! Márgenes y relleno nativos aplicados a componentes. -//! -//! [`Margin`] y [`Padding`] configuran márgenes externos y relleno interno por lado lógico y punto -//! de corte. Aplican sobre cualquier componente. Se usan igual que [`FlexItem`], con -//! [`PropsOp::margin()`] y [`PropsOp::padding()`] sobre el `with_prop()` que normalmente ya expone -//! cualquier componente. -//! -//! # Ejemplo -//! -//! ```rust,no_run -//! use pagetop::prelude::*; -//! -//! // Margen exterior arriba/abajo y relleno interno por los cuatro lados. -//! // `Margin` y `Padding` implementan `From` para `PropsOp`, así que `with_prop()` -//! // acepta `.into()` en vez de `PropsOp::margin()`/`PropsOp::padding()`. -//! let card = 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(Button::plain(Lc::n("Aceptar"))); -//! ``` -//! -//! [`FlexItem`]: crate::html::flex::FlexItem -//! [`PropsOp::margin()`]: crate::html::props::PropsOp::margin -//! [`PropsOp::padding()`]: crate::html::props::PropsOp::padding - -mod margin; -pub use margin::Margin; - -mod padding; -pub use padding::Padding; diff --git a/src/html/spacing/margin.rs b/src/html/spacing/margin.rs deleted file mode 100644 index a293302a..00000000 --- a/src/html/spacing/margin.rs +++ /dev/null @@ -1,191 +0,0 @@ -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, - /// Devuelve el margen inferior, por punto de corte. - #[getters(copy)] - bottom: Responsive, - /// Devuelve el margen del lado lógico de inicio, por punto de corte. - #[getters(copy)] - start: Responsive, - /// Devuelve el margen del lado lógico de fin, por punto de corte. - #[getters(copy)] - end: Responsive, -} - -#[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 for PropsOp { - fn from(margin: Margin) -> Self { - Self::margin(margin) - } -} diff --git a/src/html/spacing/padding.rs b/src/html/spacing/padding.rs deleted file mode 100644 index e0211cdc..00000000 --- a/src/html/spacing/padding.rs +++ /dev/null @@ -1,202 +0,0 @@ -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 relleno interno por lado lógico y punto de corte. -/// -/// Mismo mecanismo y criterio de uso que [`Margin`](super::Margin): no tiene relación con Flexbox, -/// y se aplica sobre cualquier componente vía [`PropsOp::padding()`] desde su `with_prop()`. -/// -/// A diferencia de `Margin`, [`UnitValue::Auto`] no tiene efecto en ningún lado ya que CSS no -/// admite `padding: auto`, así que un lado establecido a `Auto` se ignora como si no se hubiera -/// establecido. -/// -/// # Ejemplo -/// -/// ```rust,no_run -/// use pagetop::prelude::*; -/// -/// let card = Container::new().with_prop(PropsOp::padding( -/// Padding::new() -/// .with_all(UnitValue::RelRem(1.0)) -/// .with_bottom_at(Breakpoint::Md, UnitValue::RelRem(2.0)), -/// )); -/// ``` -#[derive(AutoDefault, Clone, Copy, Debug, PartialEq, Getters)] -pub struct Padding { - /// Devuelve el relleno interno superior, por punto de corte. - #[getters(copy)] - top: Responsive, - /// Devuelve el relleno interno inferior, por punto de corte. - #[getters(copy)] - bottom: Responsive, - /// Devuelve el relleno interno del lado lógico de inicio, por punto de corte. - #[getters(copy)] - start: Responsive, - /// Devuelve el relleno interno del lado lógico de fin, por punto de corte. - #[getters(copy)] - end: Responsive, -} - -#[builder_impl] -impl Padding { - /// Crea una configuración de relleno sin ningún lado establecido. - pub fn new() -> Self { - Self::default() - } - - // **< Padding BUILDER >************************************************************************ - - /// Establece el relleno interno superior. - pub fn with_top(mut self, value: UnitValue) -> Self { - self.top = self.top.set(value); - self - } - - /// Establece el relleno interno 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 relleno interno inferior. - pub fn with_bottom(mut self, value: UnitValue) -> Self { - self.bottom = self.bottom.set(value); - self - } - - /// Establece el relleno interno 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 relleno interno del lado lógico de inicio (`padding-inline-start`). - pub fn with_start(mut self, value: UnitValue) -> Self { - self.start = self.start.set(value); - self - } - - /// Establece el relleno interno del lado lógico de inicio (`padding-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 relleno interno del lado lógico de fin (`padding-inline-end`). - pub fn with_end(mut self, value: UnitValue) -> Self { - self.end = self.end.set(value); - self - } - - /// Establece el relleno interno del lado lógico de fin (`padding-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 relleno interno 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 relleno interno 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 relleno interno 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 relleno interno 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 relleno interno 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 relleno interno 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 Padding { - /// Combina esta configuración con otra `Padding`, lado a lado y punto de corte a punto de - /// corte; mismo criterio que [`Margin::merge()`](super::Margin::merge). - pub fn merge(mut self, padding: Padding) -> Self { - self.top = self.top.merge(padding.top); - self.bottom = self.bottom.merge(padding.bottom); - self.start = self.start.merge(padding.start); - self.end = self.end.merge(padding.end); - self - } - - /// Aplica esta configuración como clases de utilidad responsive en el [`Context`], igual que - /// [`Margin::apply()`](super::Margin::apply), salvo que aquí un lado con [`UnitValue::Auto`] se - /// descarta (ver la documentación de este tipo). - /// - /// 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) { - Self::apply_side(cx, classes, self.top, "_padding-top_", "padding-top"); - Self::apply_side(cx, classes, self.bottom, "_padding-bottom_", "padding-bottom"); - Self::apply_side(cx, classes, self.start, "_padding-start_", "padding-inline-start"); - Self::apply_side(cx, classes, self.end, "_padding-end_", "padding-inline-end"); - } - - // Aplica un único lado, descartando `UnitValue::Auto` porque CSS no admite `padding: auto`. - fn apply_side( - cx: &mut Context, - classes: &mut String, - field: Responsive, - prefix: &'static str, - property: &'static str, - ) { - use crate::html::responsive::{responsive_class, styles, value_to_token}; - - for (bp, value) in field.by_breakpoint() { - if value != UnitValue::Auto { - let value = value.value(); - if !value.is_empty() { - let entry = bp.resolved(cx); - let class = responsive_class!(prefix, value_to_token(&value), entry); - styles(cx, classes, entry, class.into(), property, value); - } - } - } - } -} - -impl From for PropsOp { - fn from(padding: Padding) -> Self { - Self::padding(padding) - } -} diff --git a/src/html/unit.rs b/src/html/unit.rs index 65bd7e39..8a8f0d0e 100644 --- a/src/html/unit.rs +++ b/src/html/unit.rs @@ -126,13 +126,6 @@ impl UnitValue { pub const fn is_measurable(&self) -> bool { !matches!(self, UnitValue::None | UnitValue::Auto) } - - // Convierte a un valor CSS ya formateado. Da a `UnitValue` la misma forma de acceso (`value()`) - // que ya usan propiedades de `html::flex` como `Direction`, `ItemSize`, `ItemOffset`, etc., - // para poder combinarse con su misma macro (`apply!`). - pub(crate) fn value(self) -> CowStr { - self.into() - } } /// Formatea la unidad como cadena CSS. diff --git a/src/lib.rs b/src/lib.rs index dbcea9d2..2d56ebcb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -109,15 +109,15 @@ use std::ops::Deref; /// impl Theme for MyTheme { /// fn before_render_page_body(&self, page: &mut Page) { /// page -/// .alter_assets( -/// StyleSheet::from("/pagetop/css/normalize.css").with_version("8.0.1") -/// ) -/// .alter_assets( +/// .alter_assets(AssetsOp::AddStyleSheet( +/// StyleSheet::from("/pagetop/css/normalize.css").with_version("8.0.1"), +/// )) +/// .alter_assets(AssetsOp::AddStyleSheet( /// StyleSheet::from("/pagetop/css/basic.css").with_version(PAGETOP_VERSION), -/// ) -/// .alter_assets( +/// )) +/// .alter_assets(AssetsOp::AddStyleSheet( /// StyleSheet::from("/mytheme/styles.css").with_version(env!("CARGO_PKG_VERSION")), -/// ); +/// )); /// } /// } /// ``` diff --git a/src/response/page.rs b/src/response/page.rs index 8685e13b..fde52ec2 100644 --- a/src/response/page.rs +++ b/src/response/page.rs @@ -295,7 +295,7 @@ impl Contextual for Page { self } - fn with_assets(mut self, op: impl Into) -> Self { + fn with_assets(mut self, op: AssetsOp) -> Self { self.context.alter_assets(op); self } diff --git a/src/util.rs b/src/util.rs index c6539ce4..ed3f9c1e 100644 --- a/src/util.rs +++ b/src/util.rs @@ -212,20 +212,6 @@ pub fn normalize_ascii(input: &str) -> Option> { } } -/// Recorta espacios y pasa a minúsculas el nombre de una propiedad, tratando el resultado vacío -/// como ausencia. -/// -/// # Ejemplo -/// -/// ```rust -/// # use pagetop::util; -/// assert_eq!(util::normalize_property(" Flex-Basis "), Some("flex-basis".to_string())); -/// assert_eq!(util::normalize_property(" "), None); -/// ``` -pub fn normalize_property(property: impl AsRef) -> Option { - non_blank(property.as_ref()).map(str::to_ascii_lowercase) -} - /// Recorta espacios, convierte una cadena vacía en `None` y normaliza el resto. /// /// Convierte en un único token: en minúsculas y con cada espacio en blanco sustituido por `_`. diff --git a/tests/html_responsives.rs b/tests/html_responsives.rs index 78c33387..49b2b774 100644 --- a/tests/html_responsives.rs +++ b/tests/html_responsives.rs @@ -105,75 +105,6 @@ async fn add_style_ignores_non_ascii_classes() { assert!(r.is_empty()); } -// **< ResponsiveStyles::add_styles >*************************************************************** - -#[pagetop::test] -async fn add_styles_adds_multiple_declarations_in_order() { - let mut r = ResponsiveStyles::new(); - r.add_styles( - Breakpoint::Md, - "col", - [("flex-basis", "50%"), ("margin-inline-start", "0")], - ); - assert_eq!( - r.get_styles(Breakpoint::Md, "col"), - Some("flex-basis: 50%; margin-inline-start: 0".to_string()) - ); -} - -#[pagetop::test] -async fn add_styles_merges_into_an_entry_already_created_by_add_style() { - // The batch must reuse the entry created by a prior add_style() call, not duplicate it. - let mut r = ResponsiveStyles::new(); - r.add_style(Breakpoint::Md, "col", "flex-basis", "50%"); - r.add_styles(Breakpoint::Md, "col", [("margin-inline-start", "0")]); - assert_eq!( - r.get_styles(Breakpoint::Md, "col"), - Some("flex-basis: 50%; margin-inline-start: 0".to_string()) - ); -} - -#[pagetop::test] -async fn add_styles_keeps_first_value_when_property_repeats_within_the_batch() { - // Same first-write-wins rule as add_style(), applied within a single batch. - let mut r = ResponsiveStyles::new(); - r.add_styles( - Breakpoint::Md, - "col", - [("flex-basis", "50%"), ("flex-basis", "33%")], - ); - assert_eq!( - r.get_styles(Breakpoint::Md, "col"), - Some("flex-basis: 50%".to_string()) - ); -} - -#[pagetop::test] -async fn add_styles_empty_batch_leaves_no_trace() { - // A batch that yields no declarations must not create an empty entry. - let none: Vec<(&str, &str)> = Vec::new(); - let mut r = ResponsiveStyles::new(); - r.add_styles(Breakpoint::Md, "col", none); - assert!(r.is_empty()); -} - -#[pagetop::test] -async fn add_styles_ignores_all_invalid_declarations_in_the_batch() { - // Same silent-discard rules as add_style(), so a batch with only invalid declarations must - // also leave no trace (no empty entry left behind). - let mut r = ResponsiveStyles::new(); - r.add_styles(Breakpoint::Md, "col", [("", "50%"), ("flex-basis", "")]); - assert!(r.is_empty()); -} - -#[pagetop::test] -async fn add_styles_ignores_empty_or_non_ascii_classes() { - let mut r = ResponsiveStyles::new(); - r.add_styles(Breakpoint::Md, "", [("flex-basis", "50%")]); - r.add_styles(Breakpoint::Md, "cañón", [("flex-basis", "50%")]); - assert!(r.is_empty()); -} - // **< Class normalization >************************************************************************ #[pagetop::test] @@ -424,11 +355,11 @@ async fn render_has_no_line_breaks() { #[pagetop::test] async fn context_add_responsive_style_feeds_responsives() { - let cx = Context::default().with_assets(AssetsOp::add_responsive_style( + let cx = Context::default().with_assets(AssetsOp::AddResponsiveStyle( Some(Breakpoint::Md), - "col", - "flex-basis", - "50%", + "col".into(), + "flex-basis".into(), + "50%".into(), )); assert_eq!( cx.responsive_styles().get_styles(Breakpoint::Md, "col"), @@ -439,17 +370,17 @@ async fn context_add_responsive_style_feeds_responsives() { #[pagetop::test] async fn context_add_responsive_style_accumulates_across_calls() { let cx = Context::default() - .with_assets(AssetsOp::add_responsive_style( + .with_assets(AssetsOp::AddResponsiveStyle( Some(Breakpoint::Md), - "col", - "flex-basis", - "50%", + "col".into(), + "flex-basis".into(), + "50%".into(), )) - .with_assets(AssetsOp::add_responsive_style( + .with_assets(AssetsOp::AddResponsiveStyle( Some(Breakpoint::Md), - "col", - "margin-inline-start", - "0", + "col".into(), + "margin-inline-start".into(), + "0".into(), )); assert_eq!( cx.responsive_styles().get_styles(Breakpoint::Md, "col"), @@ -457,19 +388,6 @@ async fn context_add_responsive_style_accumulates_across_calls() { ); } -#[pagetop::test] -async fn context_add_responsive_styles_feeds_responsives() { - let cx = Context::default().with_assets(AssetsOp::add_responsive_styles( - Some(Breakpoint::Md), - "col", - [("flex-basis", "50%"), ("margin-inline-start", "0")], - )); - assert_eq!( - cx.responsive_styles().get_styles(Breakpoint::Md, "col"), - Some("flex-basis: 50%; margin-inline-start: 0".to_string()) - ); -} - #[pagetop::test] async fn context_default_has_no_responsive_styles() { assert!(Context::default().responsive_styles().is_empty()); @@ -479,11 +397,11 @@ async fn context_default_has_no_responsive_styles() { #[pagetop::test] async fn render_assets_includes_style_tag_with_responsive_styles() { - let mut cx = Context::default().with_assets(AssetsOp::add_responsive_style( + let mut cx = Context::default().with_assets(AssetsOp::AddResponsiveStyle( Some(Breakpoint::Xs), - "col", - "flex-basis", - "100%", + "col".into(), + "flex-basis".into(), + "100%".into(), )); assert_eq!( cx.render_assets().into_string(),