Compare commits
No commits in common. "50e4b42fe48395ee2b888f3a91b33fdd9d29f317" and "0b8f3f300011581243885812cbcc46346460d18e" have entirely different histories.
50e4b42fe4
...
0b8f3f3000
35 changed files with 821 additions and 1647 deletions
|
|
@ -17,8 +17,7 @@ impl Extension for IntroFlex {
|
||||||
|
|
||||||
async fn intro_flex(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
async fn intro_flex(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
Page::new(request)
|
Page::new(request)
|
||||||
.with_assets(demo_box_styles())
|
.with_assets(AssetsOp::AddStyleSheet(demo_styles()))
|
||||||
.with_assets(demo_row_styles())
|
|
||||||
.with_child(
|
.with_child(
|
||||||
Intro::default()
|
Intro::default()
|
||||||
.with_opening(IntroOpening::Custom)
|
.with_opening(IntroOpening::Custom)
|
||||||
|
|
@ -479,37 +478,32 @@ fn other_block() -> Block {
|
||||||
|
|
||||||
// **< HELPERS >************************************************************************************
|
// **< HELPERS >************************************************************************************
|
||||||
|
|
||||||
// Aspecto fijo de las cajas de muestra.
|
// Aspecto fijo de las cajas y filas de muestra.
|
||||||
fn demo_box_styles() -> AssetsOp {
|
fn demo_styles() -> StyleSheet {
|
||||||
AssetsOp::add_responsive_styles(
|
StyleSheet::inline("intro-flex", |_| {
|
||||||
None,
|
util::indoc!(
|
||||||
"flex-demo-box",
|
r#"
|
||||||
[
|
.flex-demo-box {
|
||||||
("background-color", "#0d6efd"),
|
background-color: #0d6efd;
|
||||||
("color", "#fff"),
|
color: #fff;
|
||||||
("min-width", "3rem"),
|
min-width: 3rem;
|
||||||
("width", "auto"),
|
width: auto;
|
||||||
("max-width", "none"),
|
max-width: none;
|
||||||
("margin", "0"),
|
margin: 0;
|
||||||
("border-radius", "0.375rem"),
|
border-radius: 0.375rem;
|
||||||
("text-align", "center"),
|
text-align: center;
|
||||||
],
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
.flex-demo-row {
|
||||||
// Aspecto fijo de las filas de muestra.
|
background-color: #f1f3f5;
|
||||||
fn demo_row_styles() -> AssetsOp {
|
width: 100%;
|
||||||
AssetsOp::add_responsive_styles(
|
max-width: none;
|
||||||
None,
|
margin: 0 0 1.5rem;
|
||||||
"flex-demo-row",
|
padding: 0.75rem;
|
||||||
[
|
}
|
||||||
("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.
|
// Caja con fondo azul y relleno vertical configurable, para mostrar diferencias de altura.
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,7 @@ impl Extension for IntroResponsive {
|
||||||
|
|
||||||
async fn intro_responsive(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
async fn intro_responsive(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
Page::new(request)
|
Page::new(request)
|
||||||
.with_assets(demo_box_styles())
|
.with_assets(AssetsOp::AddStyleSheet(demo_styles()))
|
||||||
.with_assets(demo_row_styles())
|
|
||||||
.with_assets(demo_code_styles())
|
|
||||||
.with_child(
|
.with_child(
|
||||||
Intro::default()
|
Intro::default()
|
||||||
.with_opening(IntroOpening::Custom)
|
.with_opening(IntroOpening::Custom)
|
||||||
|
|
@ -191,42 +189,35 @@ fn gap_grow_block() -> Block {
|
||||||
|
|
||||||
// **< HELPERS >************************************************************************************
|
// **< HELPERS >************************************************************************************
|
||||||
|
|
||||||
// Aspecto fijo de las cajas de muestra.
|
// Aspecto fijo de las cajas y filas de muestra.
|
||||||
fn demo_box_styles() -> AssetsOp {
|
fn demo_styles() -> StyleSheet {
|
||||||
AssetsOp::add_responsive_styles(
|
StyleSheet::inline("intro-responsive", |_| {
|
||||||
None,
|
util::indoc!(
|
||||||
"flex-demo-box",
|
r#"
|
||||||
[
|
.flex-demo-box {
|
||||||
("background-color", "#0d6efd"),
|
background-color: #0d6efd;
|
||||||
("color", "#fff"),
|
color: #fff;
|
||||||
("min-width", "3rem"),
|
min-width: 3rem;
|
||||||
("width", "auto"),
|
width: auto;
|
||||||
("max-width", "none"),
|
max-width: none;
|
||||||
("margin", "0"),
|
margin: 0;
|
||||||
("border-radius", "0.375rem"),
|
border-radius: 0.375rem;
|
||||||
("text-align", "center"),
|
text-align: center;
|
||||||
],
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
.flex-demo-row {
|
||||||
// Aspecto fijo de las filas de muestra.
|
background-color: #f1f3f5;
|
||||||
fn demo_row_styles() -> AssetsOp {
|
width: 100%;
|
||||||
AssetsOp::add_responsive_styles(
|
max-width: none;
|
||||||
None,
|
margin: 0 0 1.5rem;
|
||||||
"flex-demo-row",
|
padding: 0.75rem;
|
||||||
[
|
|
||||||
("background-color", "#f1f3f5"),
|
|
||||||
("width", "100%"),
|
|
||||||
("max-width", "none"),
|
|
||||||
("margin", "0 0 1.5rem"),
|
|
||||||
("padding", "0.75rem"),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
code {
|
||||||
// Evita que los fragmentos de código largos desborden su contenedor.
|
overflow-wrap: anywhere;
|
||||||
fn demo_code_styles() -> AssetsOp {
|
}
|
||||||
AssetsOp::add_responsive_styles(None, "flex-demo-code", [("overflow-wrap", "anywhere")])
|
"#
|
||||||
|
)
|
||||||
|
.to_string()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Caja con fondo azul y relleno vertical configurable, para mostrar diferencias de altura.
|
// 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::with(move |cx| {
|
||||||
html! {
|
html! {
|
||||||
h3 { (title.using(cx)) }
|
h3 { (title.using(cx)) }
|
||||||
p { code class="flex-demo-code" { (code.using(cx)) } }
|
p { code { (code.using(cx)) } }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<ExtensionRef> {
|
|
||||||
vec![&pagetop_bootsier::Bootsier]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn configure_router(&self, router: Router) -> Router {
|
|
||||||
router.route("/", web::get(intro_spacing))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn intro_spacing(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
|
||||||
Page::new(request)
|
|
||||||
.with_assets(demo_box_styles())
|
|
||||||
.with_assets(demo_row_styles())
|
|
||||||
.with_assets(demo_code_styles())
|
|
||||||
.with_child(
|
|
||||||
Intro::default()
|
|
||||||
.with_opening(IntroOpening::Custom)
|
|
||||||
.with_title(Lc::n("PageTop"))
|
|
||||||
.with_slogan(Lc::t("spacing_slogan", &LOC))
|
|
||||||
.with_button(None::<(Lc, Route)>)
|
|
||||||
.with_child(Html::with(|cx| {
|
|
||||||
html! {
|
|
||||||
p class="intro-text-lead" {
|
|
||||||
(Lc::t("spacing_note", &LOC).using(cx))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
.with_child(padding_block())
|
|
||||||
.with_child(layout_block())
|
|
||||||
.with_child(responsive_block())
|
|
||||||
.with_child(combined_block()),
|
|
||||||
)
|
|
||||||
.render()
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
fn padding_block() -> Block {
|
|
||||||
let mut block = Block::new().with_title(Lc::t("spacing_block_title_padding", &LOC));
|
|
||||||
|
|
||||||
let padding_variants: [(&str, UnitValue, &str); 3] = [
|
|
||||||
(
|
|
||||||
"spacing_title_uniform_small",
|
|
||||||
UnitValue::RelRem(0.5),
|
|
||||||
"Padding::new().with_all(UnitValue::RelRem(0.5))",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"spacing_title_uniform_medium",
|
|
||||||
UnitValue::RelRem(1.0),
|
|
||||||
"Padding::new().with_all(UnitValue::RelRem(1.0))",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"spacing_title_uniform_large",
|
|
||||||
UnitValue::RelRem(2.0),
|
|
||||||
"Padding::new().with_all(UnitValue::RelRem(2.0))",
|
|
||||||
),
|
|
||||||
];
|
|
||||||
for (title_key, size, code) in padding_variants {
|
|
||||||
block = block
|
|
||||||
.with_child(caption(Lc::t(title_key, &LOC), Lc::n(code)))
|
|
||||||
.with_child(
|
|
||||||
demo_row(Flex::new().with_gap(flex::Gap::Both(UnitValue::RelRem(0.5)))).with_child(
|
|
||||||
demo_box(box_sample()).with_prop(Padding::new().with_all(size).into()),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
block
|
|
||||||
.with_child(caption(
|
|
||||||
Lc::t("spacing_title_sides", &LOC),
|
|
||||||
Lc::n(concat!(
|
|
||||||
"Padding::new()",
|
|
||||||
".with_top(UnitValue::RelRem(0.25))",
|
|
||||||
".with_end(UnitValue::RelRem(2.5))",
|
|
||||||
".with_bottom(UnitValue::RelRem(1.5))",
|
|
||||||
".with_start(UnitValue::RelRem(0.5))",
|
|
||||||
)),
|
|
||||||
))
|
|
||||||
.with_child(
|
|
||||||
demo_row(Flex::new()).with_child(
|
|
||||||
demo_box(box_sample()).with_prop(
|
|
||||||
Padding::new()
|
|
||||||
.with_top(UnitValue::RelRem(0.25))
|
|
||||||
.with_end(UnitValue::RelRem(2.5))
|
|
||||||
.with_bottom(UnitValue::RelRem(1.5))
|
|
||||||
.with_start(UnitValue::RelRem(0.5))
|
|
||||||
.into(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn layout_block() -> Block {
|
|
||||||
Block::new()
|
|
||||||
.with_title(Lc::t("spacing_block_title_layout", &LOC))
|
|
||||||
.with_child(caption(
|
|
||||||
Lc::t("spacing_title_margin_gap", &LOC),
|
|
||||||
Lc::n("Margin::new().with_x(UnitValue::RelRem(1.0))"),
|
|
||||||
))
|
|
||||||
.with_child(
|
|
||||||
demo_row(Flex::new())
|
|
||||||
.with_child(demo_box(box_sample()))
|
|
||||||
.with_child(
|
|
||||||
demo_box(box_sample())
|
|
||||||
.with_prop(Margin::new().with_x(UnitValue::RelRem(1.0)).into()),
|
|
||||||
)
|
|
||||||
.with_child(demo_box(box_sample())),
|
|
||||||
)
|
|
||||||
.with_child(caption(
|
|
||||||
Lc::t("spacing_title_margin_center", &LOC),
|
|
||||||
Lc::n("Margin::new().with_x(UnitValue::Auto)"),
|
|
||||||
))
|
|
||||||
.with_child(
|
|
||||||
demo_row(Flex::new()).with_child(
|
|
||||||
demo_box(box_sample())
|
|
||||||
.with_prop(Margin::new().with_x(UnitValue::Auto).into())
|
|
||||||
.with_prop(PropsOp::flex_item(
|
|
||||||
FlexItem::new().with_size(flex::ItemSize::Custom(UnitValue::RelRem(8.0))),
|
|
||||||
)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn responsive_block() -> Block {
|
|
||||||
Block::new()
|
|
||||||
.with_title(Lc::t("spacing_block_title_responsive", &LOC))
|
|
||||||
.with_child(caption(
|
|
||||||
Lc::t("spacing_title_responsive", &LOC),
|
|
||||||
Lc::n(concat!(
|
|
||||||
"Padding::new()",
|
|
||||||
".with_all(UnitValue::RelRem(0.5))",
|
|
||||||
".with_all_at(Breakpoint::Md, UnitValue::RelRem(2.0))",
|
|
||||||
".with_all_at(Breakpoint::Lg, UnitValue::RelRem(4.0))",
|
|
||||||
)),
|
|
||||||
))
|
|
||||||
.with_child(
|
|
||||||
demo_row(Flex::new()).with_child(
|
|
||||||
demo_box(box_sample()).with_prop(
|
|
||||||
Padding::new()
|
|
||||||
.with_all(UnitValue::RelRem(0.5))
|
|
||||||
.with_all_at(Breakpoint::Md, UnitValue::RelRem(2.0))
|
|
||||||
.with_all_at(Breakpoint::Lg, UnitValue::RelRem(4.0))
|
|
||||||
.into(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn combined_block() -> Block {
|
|
||||||
Block::new()
|
|
||||||
.with_title(Lc::t("spacing_block_title_combined", &LOC))
|
|
||||||
.with_child(caption(
|
|
||||||
Lc::t("spacing_title_combined", &LOC),
|
|
||||||
Lc::n(concat!(
|
|
||||||
"Container::new()",
|
|
||||||
".with_prop(Margin::new().with_y(UnitValue::RelRem(1.0)).into())",
|
|
||||||
".with_prop(Padding::new().with_all(UnitValue::RelRem(1.5)).into())",
|
|
||||||
)),
|
|
||||||
))
|
|
||||||
.with_child(
|
|
||||||
demo_row(Flex::new()).with_child(
|
|
||||||
Container::new()
|
|
||||||
.with_prop(PropsOp::add_classes("spacing-demo-box"))
|
|
||||||
.with_prop(Margin::new().with_y(UnitValue::RelRem(1.0)).into())
|
|
||||||
.with_prop(Padding::new().with_all(UnitValue::RelRem(1.5)).into())
|
|
||||||
.with_child(
|
|
||||||
Button::plain(Lc::t("spacing_box_card_button", &LOC))
|
|
||||||
.with_style(button::Style::Solid(Intent::Warning)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< HELPERS >************************************************************************************
|
|
||||||
|
|
||||||
// Caja de muestra sin relleno ni margen propios, para que sólo se vea el efecto de `Margin`/
|
|
||||||
// `Padding` aplicado en cada demostración.
|
|
||||||
fn demo_box_styles() -> AssetsOp {
|
|
||||||
AssetsOp::add_responsive_styles(
|
|
||||||
None,
|
|
||||||
"spacing-demo-box",
|
|
||||||
[
|
|
||||||
("background-color", "#0d6efd"),
|
|
||||||
("color", "#fff"),
|
|
||||||
("min-width", "3rem"),
|
|
||||||
("width", "auto"),
|
|
||||||
("max-width", "none"),
|
|
||||||
("margin", "0"),
|
|
||||||
("padding", "0"),
|
|
||||||
("border-radius", "0.375rem"),
|
|
||||||
("text-align", "center"),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Aspecto fijo de las filas de muestra.
|
|
||||||
fn demo_row_styles() -> AssetsOp {
|
|
||||||
AssetsOp::add_responsive_styles(
|
|
||||||
None,
|
|
||||||
"spacing-demo-row",
|
|
||||||
[
|
|
||||||
("background-color", "#f1f3f5"),
|
|
||||||
("width", "100%"),
|
|
||||||
("max-width", "none"),
|
|
||||||
("margin", "0 0 1.5rem"),
|
|
||||||
("padding", "0.75rem"),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Evita que los fragmentos de código largos desborden su contenedor.
|
|
||||||
fn demo_code_styles() -> AssetsOp {
|
|
||||||
AssetsOp::add_responsive_styles(None, "spacing-demo-code", [("overflow-wrap", "anywhere")])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Caja azul de muestra, sin relleno ni margen propios.
|
|
||||||
fn demo_box(label: Lc) -> Container {
|
|
||||||
Container::new()
|
|
||||||
.with_prop(PropsOp::add_classes("spacing-demo-box"))
|
|
||||||
.with_child(Html::with(move |cx| html! { (label.using(cx)) }))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Etiqueta genérica reutilizada en la mayoría de cajas de muestra.
|
|
||||||
fn box_sample() -> Lc {
|
|
||||||
Lc::t("spacing_box_sample", &LOC)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fila de demostración con fondo gris para visualizar los límites de cada caja.
|
|
||||||
fn demo_row(flex: Flex) -> Container {
|
|
||||||
Container::new()
|
|
||||||
.with_prop(PropsOp::add_classes("spacing-demo-row"))
|
|
||||||
.with_flex(flex)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Título y fragmento de código que introducen cada demostración.
|
|
||||||
fn caption(title: Lc, code: Lc) -> Html {
|
|
||||||
Html::with(move |cx| {
|
|
||||||
html! {
|
|
||||||
h3 { (title.using(cx)) }
|
|
||||||
p { code class="spacing-demo-code" { (code.using(cx)) } }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pagetop::main]
|
|
||||||
async fn main() -> std::io::Result<()> {
|
|
||||||
Application::prepare(&IntroSpacing).await.run().await
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -113,21 +113,21 @@ impl Extension for Aliner {
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Theme for Aliner {
|
impl Theme for Aliner {
|
||||||
fn before_render_page_body(&self, page: &mut Page) {
|
fn before_render_page_body(&self, page: &mut Page) {
|
||||||
page.alter_assets(
|
page.alter_assets(AssetsOp::AddStyleSheet(
|
||||||
StyleSheet::from("/pagetop/css/normalize.css")
|
StyleSheet::from("/pagetop/css/normalize.css")
|
||||||
.with_version("8.0.1")
|
.with_version("8.0.1")
|
||||||
.with_weight(-99),
|
.with_weight(-99),
|
||||||
)
|
))
|
||||||
.alter_assets(
|
.alter_assets(AssetsOp::AddStyleSheet(
|
||||||
StyleSheet::from("/pagetop/css/basic.css")
|
StyleSheet::from("/pagetop/css/basic.css")
|
||||||
.with_version(PAGETOP_VERSION)
|
.with_version(PAGETOP_VERSION)
|
||||||
.with_weight(-99),
|
.with_weight(-99),
|
||||||
)
|
))
|
||||||
.alter_assets(
|
.alter_assets(AssetsOp::AddStyleSheet(
|
||||||
StyleSheet::from("/aliner/css/styles.css")
|
StyleSheet::from("/aliner/css/styles.css")
|
||||||
.with_version(env!("CARGO_PKG_VERSION"))
|
.with_version(env!("CARGO_PKG_VERSION"))
|
||||||
.with_weight(-99),
|
.with_weight(-99),
|
||||||
)
|
))
|
||||||
.alter_child_in(
|
.alter_child_in(
|
||||||
&CoreRegions::Footer,
|
&CoreRegions::Footer,
|
||||||
ChildOp::AddIfEmpty(PoweredBy::new().into()),
|
ChildOp::AddIfEmpty(PoweredBy::new().into()),
|
||||||
|
|
|
||||||
|
|
@ -178,35 +178,37 @@ impl Theme for Bootsier {
|
||||||
|
|
||||||
// Las URLs de las fuentes deben coincidir exactamente con las declaradas en @font-face de
|
// 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.
|
// _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))
|
page.alter_assets(AssetsOp::AddPreload(
|
||||||
.alter_assets(
|
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),
|
Preload::font("/bootsier/fonts/bootsier.font.italic.woff2").with_weight(-99),
|
||||||
)
|
))
|
||||||
.alter_assets(
|
.alter_assets(AssetsOp::AddStyleSheet(
|
||||||
StyleSheet::from("/bootsier/css/bootsier.min.css")
|
StyleSheet::from("/bootsier/css/bootsier.min.css")
|
||||||
.with_version(ADMINLTE_VERSION)
|
.with_version(ADMINLTE_VERSION)
|
||||||
.with_weight(-99),
|
.with_weight(-99),
|
||||||
)
|
))
|
||||||
.alter_assets(
|
.alter_assets(AssetsOp::AddJavaScript(
|
||||||
JavaScript::defer("/bootsier/js/bootsier.bundle.min.js")
|
JavaScript::defer("/bootsier/js/bootsier.bundle.min.js")
|
||||||
.with_version(BOOTSTRAP_VERSION)
|
.with_version(BOOTSTRAP_VERSION)
|
||||||
.with_weight(-99),
|
.with_weight(-99),
|
||||||
)
|
))
|
||||||
.alter_assets(
|
.alter_assets(AssetsOp::AddJavaScript(
|
||||||
JavaScript::defer("/bootsier/js/bootsier.extended.min.js")
|
JavaScript::defer("/bootsier/js/bootsier.extended.min.js")
|
||||||
.with_version(ADMINLTE_VERSION)
|
.with_version(ADMINLTE_VERSION)
|
||||||
.with_weight(-99),
|
.with_weight(-99),
|
||||||
)
|
))
|
||||||
.alter_assets(
|
.alter_assets(AssetsOp::AddJavaScript(
|
||||||
JavaScript::defer("/bootsier/js/bootsier.dialog.min.js")
|
JavaScript::defer("/bootsier/js/bootsier.dialog.min.js")
|
||||||
.with_version(BOOTSTRAP_VERSION)
|
.with_version(BOOTSTRAP_VERSION)
|
||||||
.with_weight(-99),
|
.with_weight(-99),
|
||||||
)
|
))
|
||||||
.alter_assets(
|
.alter_assets(AssetsOp::AddJavaScript(
|
||||||
JavaScript::defer("/bootsier/js/bootsier.confirm.min.js")
|
JavaScript::defer("/bootsier/js/bootsier.confirm.min.js")
|
||||||
.with_version(BOOTSTRAP_VERSION)
|
.with_version(BOOTSTRAP_VERSION)
|
||||||
.with_weight(-99),
|
.with_weight(-99),
|
||||||
)
|
))
|
||||||
.alter_body_props(PropsOp::set("data-confirm-ok", confirm_ok))
|
.alter_body_props(PropsOp::set("data-confirm-ok", confirm_ok))
|
||||||
.alter_body_props(PropsOp::set("data-confirm-cancel", confirm_cancel))
|
.alter_body_props(PropsOp::set("data-confirm-cancel", confirm_cancel))
|
||||||
.alter_child_in(
|
.alter_child_in(
|
||||||
|
|
|
||||||
|
|
@ -41,11 +41,11 @@ async fn render_admin(cx: &mut Context) -> Markup {
|
||||||
cx.alter_body_props(PropsOp::add_classes(
|
cx.alter_body_props(PropsOp::add_classes(
|
||||||
"layout-fixed sidebar-expand-lg bg-body-tertiary",
|
"layout-fixed sidebar-expand-lg bg-body-tertiary",
|
||||||
));
|
));
|
||||||
cx.alter_assets(
|
cx.alter_assets(AssetsOp::AddJavaScript(
|
||||||
JavaScript::defer("/bootsier/js/bootsier.shell.min.js")
|
JavaScript::defer("/bootsier/js/bootsier.shell.min.js")
|
||||||
.with_version(ADMINLTE_VERSION)
|
.with_version(ADMINLTE_VERSION)
|
||||||
.with_weight(-88),
|
.with_weight(-88),
|
||||||
);
|
));
|
||||||
// `CoreRegions::Aside` es una región neutra del core: la usa `pagetop-admin` para su menú de
|
// `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
|
// 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.
|
// disponible para que cualquier extensión añada elementos propios a mano.
|
||||||
|
|
|
||||||
|
|
@ -21,3 +21,6 @@ pub use border::{Border, BorderColor};
|
||||||
|
|
||||||
mod rounded;
|
mod rounded;
|
||||||
pub use rounded::{Rounded, RoundedRadius};
|
pub use rounded::{Rounded, RoundedRadius};
|
||||||
|
|
||||||
|
mod layout;
|
||||||
|
pub use layout::{Margin, Padding};
|
||||||
|
|
|
||||||
211
extensions/pagetop-bootsier/src/theme/class/layout.rs
Normal file
211
extensions/pagetop-bootsier/src/theme/class/layout.rs
Normal file
|
|
@ -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<Margin> 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<Padding> for CowStr {
|
||||||
|
/// Permite pasar [`Padding`] directamente a [`PropsOp`].
|
||||||
|
fn from(val: Padding) -> Self {
|
||||||
|
val.to_class().into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,9 +4,12 @@ use pagetop::prelude::*;
|
||||||
|
|
||||||
/// Escala discreta de tamaños para clases utilitarias.
|
/// 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
|
/// [`Border`]: crate::theme::class::Border
|
||||||
|
/// [`Margin`]: crate::theme::class::Margin
|
||||||
|
/// [`Padding`]: crate::theme::class::Padding
|
||||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||||
pub enum ScaleSize {
|
pub enum ScaleSize {
|
||||||
/// Sin tamaño (no define ninguna clase).
|
/// Sin tamaño (no define ninguna clase).
|
||||||
|
|
@ -65,9 +68,11 @@ impl ScaleSize {
|
||||||
|
|
||||||
/// Lados sobre los que aplicar una clase utilitaria (respetando LTR/RTL).
|
/// 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
|
/// [`Border`]: crate::theme::class::Border
|
||||||
|
/// [`Margin`]: crate::theme::class::Margin
|
||||||
|
/// [`Padding`]: crate::theme::class::Padding
|
||||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||||
pub enum BoxSide {
|
pub enum BoxSide {
|
||||||
/// Todos los lados.
|
/// Todos los lados.
|
||||||
|
|
|
||||||
|
|
@ -154,5 +154,7 @@ impl Extension for Htmx {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_htmx_script(page: &mut Page) {
|
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"),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -114,9 +114,11 @@ impl Component for Intro {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
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 {
|
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#"
|
util::indoc!(r#"
|
||||||
try {
|
try {
|
||||||
const resp = await fetch("https://crates.io/api/v1/crates/pagetop");
|
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("LANGID", cx.langid().to_string().as_str())
|
||||||
.replace("LABEL", Lc::l("intro_release_label").using(cx).as_str())
|
.replace("LABEL", Lc::l("intro_release_label").using(cx).as_str())
|
||||||
));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(html! {
|
Ok(html! {
|
||||||
|
|
|
||||||
|
|
@ -14,26 +14,28 @@ impl Extension for Basic {
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Theme for Basic {
|
impl Theme for Basic {
|
||||||
fn before_render_page_body(&self, page: &mut Page) {
|
fn before_render_page_body(&self, page: &mut Page) {
|
||||||
page.alter_assets(
|
page.alter_assets(AssetsOp::AddStyleSheet(
|
||||||
StyleSheet::from("/pagetop/css/normalize.css")
|
StyleSheet::from("/pagetop/css/normalize.css")
|
||||||
.with_version("8.0.1")
|
.with_version("8.0.1")
|
||||||
.with_weight(-99),
|
.with_weight(-99),
|
||||||
)
|
))
|
||||||
.alter_assets(
|
.alter_assets(AssetsOp::AddStyleSheet(
|
||||||
StyleSheet::from("/pagetop/css/basic.min.css")
|
StyleSheet::from("/pagetop/css/basic.min.css")
|
||||||
.with_version(PAGETOP_VERSION)
|
.with_version(PAGETOP_VERSION)
|
||||||
.with_weight(-99),
|
.with_weight(-99),
|
||||||
)
|
))
|
||||||
.alter_assets(JavaScript::defer("/pagetop/js/basic.menu.min.js").with_version("4.4.0"))
|
.alter_assets(AssetsOp::AddJavaScript(
|
||||||
.alter_assets(
|
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),
|
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),
|
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),
|
JavaScript::defer("/pagetop/js/basic.dialog.min.js").with_version(PAGETOP_VERSION),
|
||||||
)
|
))
|
||||||
.alter_child_in(
|
.alter_child_in(
|
||||||
&CoreRegions::Footer,
|
&CoreRegions::Footer,
|
||||||
ChildOp::AddIfEmpty(PoweredBy::new().into()),
|
ChildOp::AddIfEmpty(PoweredBy::new().into()),
|
||||||
|
|
|
||||||
|
|
@ -2,28 +2,246 @@ use crate::auth::CurrentUser;
|
||||||
use crate::core::TypeInfo;
|
use crate::core::TypeInfo;
|
||||||
use crate::core::component::{ChildOp, Component, MessageLevel, StatusMessage};
|
use crate::core::component::{ChildOp, Component, MessageLevel, StatusMessage};
|
||||||
use crate::core::theme::all::DEFAULT_THEME;
|
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::core::theme::{RegionRef, TemplateRef, ThemeRef};
|
||||||
use crate::html::{Assets, Favicon, JavaScript, Preload, ResponsiveStyles, StyleSheet};
|
use crate::html::{Assets, Favicon, JavaScript, Preload, ResponsiveStyles, StyleSheet};
|
||||||
use crate::html::{Markup, Props, PropsOp, RoutePath, html};
|
use crate::html::{Markup, Props, PropsOp, RoutePath, html};
|
||||||
use crate::locale::Lc;
|
use crate::locale::Lc;
|
||||||
use crate::locale::{LangId, LanguageIdentifier, RequestLocale};
|
use crate::locale::{LangId, LanguageIdentifier, RequestLocale};
|
||||||
use crate::web::HttpRequest;
|
use crate::web::HttpRequest;
|
||||||
use crate::{builder_impl, util};
|
use crate::{CowStr, builder_impl, util};
|
||||||
|
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
use std::any::{Any, TypeId};
|
use std::any::{Any, TypeId};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
mod assets_op;
|
/// Operaciones para modificar recursos asociados al [`Context`] de un documento.
|
||||||
pub use assets_op::AssetsOp;
|
pub enum AssetsOp {
|
||||||
|
/// Define el *favicon* del documento. Sobrescribe cualquier valor anterior.
|
||||||
|
SetFavicon(Option<Favicon>),
|
||||||
|
/// Define el *favicon* solo si no se ha establecido previamente.
|
||||||
|
SetFaviconIfNone(Favicon),
|
||||||
|
|
||||||
mod error;
|
/// Añade un recurso para precarga al documento.
|
||||||
pub use error::ContextError;
|
AddPreload(Preload),
|
||||||
|
/// Elimina un recurso para precarga por su ruta.
|
||||||
|
RemovePreload(&'static str),
|
||||||
|
|
||||||
mod contextual;
|
/// Añade una hoja de estilos CSS al documento.
|
||||||
pub use contextual::Contextual;
|
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<Breakpoint>, 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<C: Contextual>(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<HttpRequest>) -> 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<T: Send + Sync + 'static>(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 `<body>`.
|
||||||
|
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<ChildOp>) -> 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<ChildOp>) -> 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<Markup, ErrorPage> {
|
||||||
|
/// 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::<String>("user_id").is_err());
|
||||||
|
/// ```
|
||||||
|
fn param<T: 'static>(&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<T: Clone + Default + 'static>(&self, key: &'static str) -> T {
|
||||||
|
self.param::<T>(key).ok().cloned().unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Devuelve el parámetro clonado o un **valor por defecto** si no existe.
|
||||||
|
fn param_or<T: Clone + 'static>(&self, key: &'static str, default: T) -> T {
|
||||||
|
self.param::<T>(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: Clone + 'static, F: FnOnce() -> T>(&self, key: &'static str, f: F) -> T {
|
||||||
|
self.param::<T>(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<StyleSheet>;
|
||||||
|
|
||||||
|
/// Devuelve los scripts JavaScript de los recursos del contexto.
|
||||||
|
fn javascripts(&self) -> &Assets<JavaScript>;
|
||||||
|
|
||||||
|
/// Devuelve los estilos *responsive* acumulados en el contexto.
|
||||||
|
fn responsive_styles(&self) -> &ResponsiveStyles;
|
||||||
|
|
||||||
|
/// Devuelve identificador, clases CSS, atributos HTML y valores extra del elemento `<body>`.
|
||||||
|
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.
|
/// Implementa un **contexto de renderizado** para un documento HTML.
|
||||||
///
|
///
|
||||||
|
|
@ -62,11 +280,11 @@ pub use contextual::Contextual;
|
||||||
/// // Establece el tema para renderizar.
|
/// // Establece el tema para renderizar.
|
||||||
/// .with_theme(&Aliner)
|
/// .with_theme(&Aliner)
|
||||||
/// // Asigna un favicon.
|
/// // 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.
|
/// // 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.
|
/// // 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.
|
/// // Añade un parámetro dinámico al contexto.
|
||||||
/// .with_param("user_id", 42);
|
/// .with_param("user_id", 42);
|
||||||
/// # cx }
|
/// # cx }
|
||||||
|
|
@ -368,8 +586,8 @@ impl Contextual for Context {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn with_assets(mut self, op: impl Into<AssetsOp>) -> Self {
|
fn with_assets(mut self, op: AssetsOp) -> Self {
|
||||||
match op.into() {
|
match op {
|
||||||
// Favicon.
|
// Favicon.
|
||||||
AssetsOp::SetFavicon(favicon) => {
|
AssetsOp::SetFavicon(favicon) => {
|
||||||
self.favicon = favicon;
|
self.favicon = favicon;
|
||||||
|
|
@ -405,9 +623,6 @@ impl Contextual for Context {
|
||||||
self.responsives
|
self.responsives
|
||||||
.add_style(breakpoint, classes, property, value);
|
.add_style(breakpoint, classes, property, value);
|
||||||
}
|
}
|
||||||
AssetsOp::AddResponsiveStyles(breakpoint, classes, styles) => {
|
|
||||||
self.responsives.add_styles(breakpoint, classes, styles);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<Favicon>),
|
|
||||||
/// 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<Breakpoint>, 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<Breakpoint>, 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<Option<Favicon>>) -> 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<Option<Breakpoint>>,
|
|
||||||
classes: impl Into<CowStr>,
|
|
||||||
property: impl Into<CowStr>,
|
|
||||||
value: impl Into<CowStr>,
|
|
||||||
) -> 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<Option<Breakpoint>>,
|
|
||||||
classes: impl Into<CowStr>,
|
|
||||||
styles: impl IntoIterator<Item = (impl Into<CowStr>, impl Into<CowStr>)>,
|
|
||||||
) -> Self {
|
|
||||||
Self::AddResponsiveStyles(
|
|
||||||
breakpoint.into(),
|
|
||||||
classes.into(),
|
|
||||||
styles
|
|
||||||
.into_iter()
|
|
||||||
.map(|(p, v)| (p.into(), v.into()))
|
|
||||||
.collect(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Favicon> 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<Preload> 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<StyleSheet> 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<JavaScript> for AssetsOp {
|
|
||||||
/// Convierte un script en [`AssetsOp::AddJavaScript`]. Ver la conversión equivalente para
|
|
||||||
/// [`Favicon`].
|
|
||||||
#[inline]
|
|
||||||
fn from(js: JavaScript) -> Self {
|
|
||||||
Self::AddJavaScript(js)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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<C: Contextual>(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<HttpRequest>) -> 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<T: Send + Sync + 'static>(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<AssetsOp>) -> Self;
|
|
||||||
|
|
||||||
/// Modifica identificador, clases CSS, atributos HTML o valores extra del elemento `<body>`.
|
|
||||||
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<ChildOp>) -> 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<ChildOp>) -> 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<Markup, ErrorPage> {
|
|
||||||
/// 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::<String>("user_id").is_err());
|
|
||||||
/// ```
|
|
||||||
fn param<T: 'static>(&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<T: Clone + Default + 'static>(&self, key: &'static str) -> T {
|
|
||||||
self.param::<T>(key).ok().cloned().unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve el parámetro clonado o un **valor por defecto** si no existe.
|
|
||||||
fn param_or<T: Clone + 'static>(&self, key: &'static str, default: T) -> T {
|
|
||||||
self.param::<T>(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: Clone + 'static, F: FnOnce() -> T>(&self, key: &'static str, f: F) -> T {
|
|
||||||
self.param::<T>(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<StyleSheet>;
|
|
||||||
|
|
||||||
/// Devuelve los scripts JavaScript de los recursos del contexto.
|
|
||||||
fn javascripts(&self) -> &Assets<JavaScript>;
|
|
||||||
|
|
||||||
/// Devuelve los estilos *responsive* acumulados en el contexto.
|
|
||||||
fn responsive_styles(&self) -> &ResponsiveStyles;
|
|
||||||
|
|
||||||
/// Devuelve identificador, clases CSS, atributos HTML y valores extra del elemento `<body>`.
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
@ -37,12 +37,6 @@ pub use unit::UnitValue;
|
||||||
|
|
||||||
// **< HTML LAYOUT >********************************************************************************
|
// **< HTML LAYOUT >********************************************************************************
|
||||||
|
|
||||||
mod responsive;
|
|
||||||
|
|
||||||
pub mod flex;
|
pub mod flex;
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
pub use flex::{Flex, FlexItem};
|
pub use flex::{Flex, FlexItem};
|
||||||
|
|
||||||
pub mod spacing;
|
|
||||||
#[doc(inline)]
|
|
||||||
pub use spacing::{Margin, Padding};
|
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,9 @@ type Entry = (Option<Breakpoint>, CowStr, Vec<(CowStr, CowStr)>);
|
||||||
/// El punto de corte es opcional, donde `None` declara una regla siempre activa, sin pasar por el
|
/// 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
|
/// 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
|
/// 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)]
|
#[derive(AutoDefault, Clone, Debug)]
|
||||||
pub struct ResponsiveStyles(Vec<Entry>);
|
pub struct ResponsiveStyles(Vec<Entry>);
|
||||||
|
|
||||||
|
|
@ -50,92 +50,48 @@ impl ResponsiveStyles {
|
||||||
value: impl AsRef<str>,
|
value: impl AsRef<str>,
|
||||||
) {
|
) {
|
||||||
let breakpoint = breakpoint.into();
|
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;
|
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<Option<Breakpoint>>,
|
|
||||||
classes: impl AsRef<str>,
|
|
||||||
styles: impl IntoIterator<Item = (impl AsRef<str>, impl AsRef<str>)>,
|
|
||||||
) {
|
|
||||||
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<CowStr> {
|
|
||||||
let classes = util::normalize_ascii(classes)?;
|
|
||||||
if classes.is_empty() {
|
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
|
let property_norm = property.as_ref().trim().to_ascii_lowercase();
|
||||||
// llamador. Aplica las mismas reglas que `add_style()`: normaliza `property`, descarta si ya
|
if property_norm.is_empty() {
|
||||||
// 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) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let Some(value) = util::non_blank(value) else {
|
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;
|
return;
|
||||||
};
|
};
|
||||||
styles.push((property.into(), value.to_string().into()));
|
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 >***************************************************************
|
// **< ResponsiveStyles GETTERS >***************************************************************
|
||||||
|
|
@ -149,7 +105,7 @@ impl ResponsiveStyles {
|
||||||
property: impl AsRef<str>,
|
property: impl AsRef<str>,
|
||||||
) -> Option<String> {
|
) -> Option<String> {
|
||||||
let styles = self.entry(breakpoint.into(), classes.as_ref())?;
|
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
|
styles
|
||||||
.iter()
|
.iter()
|
||||||
.find(|(k, _)| k.as_ref() == property)
|
.find(|(k, _)| k.as_ref() == property)
|
||||||
|
|
@ -241,17 +197,20 @@ impl ResponsiveStyles {
|
||||||
rules
|
rules
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normaliza `classes` y busca las declaraciones de la entrada correspondiente al punto de corte
|
// Normaliza `classes` igual que `add_style()` y busca las declaraciones de la entrada
|
||||||
// y las clases dados.
|
// correspondiente al punto de corte y las clases dados.
|
||||||
fn entry(
|
fn entry(
|
||||||
&self,
|
&self,
|
||||||
breakpoint: Option<Breakpoint>,
|
breakpoint: Option<Breakpoint>,
|
||||||
classes: &str,
|
classes: &str,
|
||||||
) -> Option<&Vec<(CowStr, CowStr)>> {
|
) -> Option<&Vec<(CowStr, CowStr)>> {
|
||||||
let classes = Self::normalize_classes(classes)?;
|
let classes = util::normalize_ascii(classes)?;
|
||||||
|
if classes.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
self.0
|
self.0
|
||||||
.iter()
|
.iter()
|
||||||
.find(|(bp, cls, _)| *bp == breakpoint && *cls == classes)
|
.find(|(bp, cls, _)| *bp == breakpoint && cls.as_ref() == classes.as_ref())
|
||||||
.map(|(_, _, styles)| styles)
|
.map(|(_, _, styles)| styles)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,24 @@
|
||||||
use crate::core::component::Context;
|
use crate::core::component::Context;
|
||||||
use crate::html::assets::Asset;
|
use crate::html::assets::Asset;
|
||||||
use crate::html::{Markup, html};
|
use crate::html::{Markup, PreEscaped, html};
|
||||||
use crate::{AutoDefault, CowStr, Weight, util};
|
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 `<style>`.
|
||||||
|
///
|
||||||
|
/// - [`From`] - Carga la hoja de estilos desde un archivo externo, insertándola mediante una
|
||||||
|
/// etiqueta `<link>` con `rel="stylesheet"`.
|
||||||
|
/// - [`Inline`] - Inserta directamente el contenido CSS dentro de una etiqueta `<style>`.
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
enum Source {
|
||||||
|
#[default]
|
||||||
|
From(CowStr),
|
||||||
|
/// `name`, `closure(&mut Context) -> String`.
|
||||||
|
Inline(CowStr, Box<dyn Fn(&mut Context) -> String + Send + Sync>),
|
||||||
|
}
|
||||||
|
|
||||||
/// Define el medio objetivo para una hoja de estilos.
|
/// Define el medio objetivo para una hoja de estilos.
|
||||||
///
|
///
|
||||||
/// Permite especificar en qué contexto se aplica el CSS, adaptándose a diferentes dispositivos o
|
/// Permite especificar en qué contexto se aplica el CSS, adaptándose a diferentes dispositivos o
|
||||||
|
|
@ -34,15 +50,9 @@ impl TargetMedia {
|
||||||
|
|
||||||
/// Define un recurso **StyleSheet** para incluir en un documento HTML.
|
/// Define un recurso **StyleSheet** para incluir en un documento HTML.
|
||||||
///
|
///
|
||||||
/// Este tipo permite incluir hojas de estilo CSS externas, con soporte para medios específicos
|
/// Este tipo permite incluir hojas de estilo CSS externas o embebidas, con soporte para medios
|
||||||
/// (`screen`, `print`, etc.) y [pesos](crate::Weight) que determinan el orden de inserción en el
|
/// específicos (`screen`, `print`, etc.) y [pesos](crate::Weight) que determinan el orden de
|
||||||
/// documento.
|
/// inserción en el documento.
|
||||||
///
|
|
||||||
/// Para declarar estilos embebidos en el documento (sin un archivo CSS externo), usar
|
|
||||||
/// [`AssetsOp::add_responsive_style()`](crate::core::component::AssetsOp::add_responsive_style) o
|
|
||||||
/// [`AssetsOp::add_responsive_styles()`](crate::core::component::AssetsOp::add_responsive_styles),
|
|
||||||
/// que asocian declaraciones de estilo `propiedad: valor` a una o varias clases CSS y las agrupan
|
|
||||||
/// en un único `<style>` en el `<head>` del documento.
|
|
||||||
///
|
///
|
||||||
/// > **Nota**
|
/// > **Nota**
|
||||||
/// > Las hojas de estilo CSS deben estar disponibles en el servidor web de la aplicación. Pueden
|
/// > Las hojas de estilo CSS deben estar disponibles en el servidor web de la aplicación. Pueden
|
||||||
|
|
@ -57,10 +67,18 @@ impl TargetMedia {
|
||||||
/// .with_version("2.0.1")
|
/// .with_version("2.0.1")
|
||||||
/// .for_media(TargetMedia::Screen)
|
/// .for_media(TargetMedia::Screen)
|
||||||
/// .with_weight(-10);
|
/// .with_weight(-10);
|
||||||
|
///
|
||||||
|
/// // Crea una hoja de estilos embebida en el documento HTML.
|
||||||
|
/// let embedded = StyleSheet::inline("custom_theme", |_| r#"
|
||||||
|
/// body {
|
||||||
|
/// background-color: #f5f5f5;
|
||||||
|
/// font-family: 'Segoe UI', sans-serif;
|
||||||
|
/// }
|
||||||
|
/// "#.to_string());
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(AutoDefault)]
|
#[derive(AutoDefault)]
|
||||||
pub struct StyleSheet {
|
pub struct StyleSheet {
|
||||||
path: CowStr, // Ruta del recurso CSS externo.
|
source: Source, // Fuente y modo de inclusión del CSS.
|
||||||
version: CowStr, // Versión del recurso para la caché del navegador.
|
version: CowStr, // Versión del recurso para la caché del navegador.
|
||||||
media: TargetMedia, // Medio objetivo para los estilos (`print`, `screen`, ...).
|
media: TargetMedia, // Medio objetivo para los estilos (`print`, `screen`, ...).
|
||||||
weight: Weight, // Peso que determina el orden.
|
weight: Weight, // Peso que determina el orden.
|
||||||
|
|
@ -72,7 +90,23 @@ impl StyleSheet {
|
||||||
/// Equivale a `<link rel="stylesheet" href="...">`.
|
/// Equivale a `<link rel="stylesheet" href="...">`.
|
||||||
pub fn from(path: impl Into<CowStr>) -> Self {
|
pub fn from(path: impl Into<CowStr>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
path: path.into(),
|
source: Source::From(path.into()),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Crea una hoja de estilos embebida directamente en el documento HTML.
|
||||||
|
///
|
||||||
|
/// Equivale a `<style>...</style>`. 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<F>(name: impl Into<CowStr>, f: F) -> Self
|
||||||
|
where
|
||||||
|
F: Fn(&mut Context) -> String + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
Self {
|
||||||
|
source: Source::Inline(name.into(), Box::new(f)),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -113,9 +147,14 @@ impl StyleSheet {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Asset for 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 {
|
fn name(&self) -> &str {
|
||||||
&self.path
|
match &self.source {
|
||||||
|
Source::From(path) => path,
|
||||||
|
Source::Inline(name, _) => name,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn weight(&self) -> Weight {
|
fn weight(&self) -> Weight {
|
||||||
|
|
@ -124,12 +163,17 @@ impl Asset for StyleSheet {
|
||||||
|
|
||||||
// **< StyleSheet RENDER >**********************************************************************
|
// **< StyleSheet RENDER >**********************************************************************
|
||||||
|
|
||||||
fn render(&self, _cx: &mut Context) -> Markup {
|
fn render(&self, cx: &mut Context) -> Markup {
|
||||||
html! {
|
match &self.source {
|
||||||
|
Source::From(path) => html! {
|
||||||
link
|
link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href=(util::join_pair!(&self.path, "?v=", &self.version))
|
href=(util::join_pair!(path, "?v=", &self.version))
|
||||||
media=[self.media.as_str()];
|
media=[self.media.as_str()];
|
||||||
|
},
|
||||||
|
Source::Inline(_, f) => html! {
|
||||||
|
style { (PreEscaped((f)(cx))) };
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,16 +16,20 @@
|
||||||
//! manera independiente a cualquier tema o framework CSS. El nombre interno de cada clase se deriva
|
//! 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
|
//! 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
|
//! 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 `<head>` del
|
//! vía [`AssetsOp::AddResponsiveStyle`] y se renderizan como reglas en el `<head>` del documento.
|
||||||
//! documento. Funciona igual conviva con quien conviva en la misma página, sin necesidad de
|
//! Funciona igual conviva con quien conviva en la misma página, sin necesidad de coordinar nombres
|
||||||
//! coordinar nombres de clase ni orden alguno en la carga de hojas de estilo.
|
//! 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
|
//! [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
|
//! [`PropsOp::flex_item()`]: crate::html::props::PropsOp::flex_item
|
||||||
//! [`Container`]: crate::base::component::Container
|
//! [`Container`]: crate::base::component::Container
|
||||||
//! [`Navbar`]: crate::base::component::Navbar
|
//! [`Navbar`]: crate::base::component::Navbar
|
||||||
|
|
||||||
|
use crate::CowStr;
|
||||||
|
use crate::core::component::{AssetsOp, Context, Contextual};
|
||||||
|
use crate::core::theme::BreakpointEntry;
|
||||||
|
|
||||||
mod props_container;
|
mod props_container;
|
||||||
pub use props_container::{Align, AlignContent, Behavior, ContentJustify, Direction, Gap};
|
pub use props_container::{Align, AlignContent, Behavior, ContentJustify, Direction, Gap};
|
||||||
|
|
||||||
|
|
@ -37,3 +41,83 @@ pub use container::Flex;
|
||||||
|
|
||||||
mod item;
|
mod item;
|
||||||
pub use item::FlexItem;
|
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<BreakpointEntry>,
|
||||||
|
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<T>` 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;
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ enum DisplayFlex {
|
||||||
///
|
///
|
||||||
/// Se resuelve como clases CSS generadas dinámicamente (`display`, `flex-direction`, `flex-wrap`,
|
/// Se resuelve como clases CSS generadas dinámicamente (`display`, `flex-direction`, `flex-wrap`,
|
||||||
/// `justify-content`, `align-items`, `align-content`, `gap`), registradas vía
|
/// `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
|
||||||
/// `<head>` del documento. Son propiedades nativas que no requieren interpretación por parte de los
|
/// `<head>` 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.
|
/// 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
|
/// regla generada en vez de duplicarla, y el nombre generado no coincide por accidente con clases
|
||||||
/// de terceros.
|
/// de terceros.
|
||||||
///
|
///
|
||||||
/// [`AssetsOp::add_responsive_style()`]: crate::core::component::AssetsOp::add_responsive_style
|
/// [`AssetsOp::AddResponsiveStyle`]: crate::core::component::AssetsOp::AddResponsiveStyle
|
||||||
/// [`ResponsiveStyles`]: crate::html::ResponsiveStyles
|
/// [`ResponsiveStyles`]: crate::html::ResponsiveStyles
|
||||||
///
|
///
|
||||||
/// # Ejemplo
|
/// # Ejemplo
|
||||||
|
|
@ -238,7 +238,7 @@ impl Flex {
|
||||||
return;
|
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 {
|
let (prefix, value) = match display {
|
||||||
DisplayFlex::Always
|
DisplayFlex::Always
|
||||||
|
|
|
||||||
|
|
@ -214,7 +214,7 @@ impl FlexItem {
|
||||||
/// cadenas intermedias.
|
/// cadenas intermedias.
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
pub(crate) fn apply(self, cx: &mut Context, classes: &mut String) {
|
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.grow, "_flex-item-grow_", "flex-grow");
|
||||||
apply!(cx, classes, self.shrink, "_flex-item-shrink_", "flex-shrink");
|
apply!(cx, classes, self.shrink, "_flex-item-shrink_", "flex-shrink");
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ use crate::core::component::Context;
|
||||||
use crate::html::flex::{Flex, FlexItem};
|
use crate::html::flex::{Flex, FlexItem};
|
||||||
use crate::html::maud::{Escaper, RenderAttrs};
|
use crate::html::maud::{Escaper, RenderAttrs};
|
||||||
use crate::html::props::{PropsError, PropsExtra, PropsOp};
|
use crate::html::props::{PropsError, PropsExtra, PropsOp};
|
||||||
use crate::html::spacing::{Margin, Padding};
|
|
||||||
use crate::{AutoDefault, CowStr, builder_impl, trace, util};
|
use crate::{AutoDefault, CowStr, builder_impl, trace, util};
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
@ -191,8 +190,6 @@ pub struct Props {
|
||||||
attrs: Vec<(CowStr, CowStr)>,
|
attrs: Vec<(CowStr, CowStr)>,
|
||||||
extras: HashMap<&'static str, PropsExtra>,
|
extras: HashMap<&'static str, PropsExtra>,
|
||||||
flex_item: FlexItem,
|
flex_item: FlexItem,
|
||||||
margin: Margin,
|
|
||||||
padding: Padding,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[builder_impl]
|
#[builder_impl]
|
||||||
|
|
@ -216,8 +213,7 @@ impl Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Modifica el identificador, las clases, los atributos o los valores extra según la operación
|
/// 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`,
|
/// indicada. El método recomendado para construir cada operación es usar los constructores de
|
||||||
/// `Padding`). El método recomendado para construir cada operación es usar los constructores de
|
|
||||||
/// [`PropsOp`].
|
/// [`PropsOp`].
|
||||||
pub fn with_prop(mut self, op: PropsOp) -> Self {
|
pub fn with_prop(mut self, op: PropsOp) -> Self {
|
||||||
match op {
|
match op {
|
||||||
|
|
@ -343,12 +339,6 @@ impl Props {
|
||||||
PropsOp::FlexItem(placement) => {
|
PropsOp::FlexItem(placement) => {
|
||||||
self.flex_item = self.flex_item.merge(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
|
self
|
||||||
}
|
}
|
||||||
|
|
@ -388,7 +378,7 @@ impl Props {
|
||||||
|
|
||||||
/// Devuelve el valor de la propiedad de estilo indicada, si existe.
|
/// Devuelve el valor de la propiedad de estilo indicada, si existe.
|
||||||
pub fn get_style(&self, property: impl AsRef<str>) -> Option<String> {
|
pub fn get_style(&self, property: impl AsRef<str>) -> Option<String> {
|
||||||
let property = util::normalize_property(property)?;
|
let property = property.as_ref().trim().to_ascii_lowercase();
|
||||||
self.styles
|
self.styles
|
||||||
.iter()
|
.iter()
|
||||||
.find(|(k, _)| k.as_ref() == property)
|
.find(|(k, _)| k.as_ref() == property)
|
||||||
|
|
@ -570,9 +560,9 @@ impl Props {
|
||||||
///
|
///
|
||||||
/// `Props` no implementa [`RenderAttrs`] directamente. Obliga a pasar siempre el `Context`
|
/// `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
|
/// 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
|
/// `&mut Context` porque aquí, en el momento de extraer los atributos, es donde se resuelve
|
||||||
/// [`FlexItem`], [`Margin`] y [`Padding`]: las clases que devuelven se añaden a las del propio
|
/// [`FlexItem`]: las clases que devuelve se añaden a las del propio componente al escribir el
|
||||||
/// componente al escribir el atributo `class`.
|
/// atributo `class`.
|
||||||
///
|
///
|
||||||
/// Si el propio elemento actúa además como contenedor [`Flex`], utiliza [`unpack_with_flex()`]
|
/// Si el propio elemento actúa además como contenedor [`Flex`], utiliza [`unpack_with_flex()`]
|
||||||
/// en su lugar.
|
/// en su lugar.
|
||||||
|
|
@ -588,14 +578,10 @@ impl Props {
|
||||||
/// [`html!`]: crate::html::html
|
/// [`html!`]: crate::html::html
|
||||||
/// [`Flex`]: crate::html::flex::Flex
|
/// [`Flex`]: crate::html::flex::Flex
|
||||||
/// [`FlexItem`]: crate::html::flex::FlexItem
|
/// [`FlexItem`]: crate::html::flex::FlexItem
|
||||||
/// [`Margin`]: crate::html::spacing::Margin
|
|
||||||
/// [`Padding`]: crate::html::spacing::Padding
|
|
||||||
/// [`unpack_with_flex()`]: Self::unpack_with_flex
|
/// [`unpack_with_flex()`]: Self::unpack_with_flex
|
||||||
pub fn unpack<'a>(&'a self, cx: &mut Context) -> impl RenderAttrs + 'a {
|
pub fn unpack<'a>(&'a self, cx: &mut Context) -> impl RenderAttrs + 'a {
|
||||||
let mut classes = String::new();
|
let mut classes = String::new();
|
||||||
self.flex_item.apply(cx, &mut classes);
|
self.flex_item.apply(cx, &mut classes);
|
||||||
self.margin.apply(cx, &mut classes);
|
|
||||||
self.padding.apply(cx, &mut classes);
|
|
||||||
PropsUnpack {
|
PropsUnpack {
|
||||||
props: self,
|
props: self,
|
||||||
classes,
|
classes,
|
||||||
|
|
@ -604,23 +590,20 @@ impl Props {
|
||||||
|
|
||||||
/// Igual que [`unpack()`], pero además resuelve `flex` con el posicionamiento [`Flex`].
|
/// Igual que [`unpack()`], pero además resuelve `flex` con el posicionamiento [`Flex`].
|
||||||
///
|
///
|
||||||
/// A diferencia de [`FlexItem`]/[`Margin`]/[`Padding`] (que se acumulan con sus respectivas
|
/// A diferencia de [`FlexItem`] (que se acumula con [`PropsOp::FlexItem`] porque cualquier
|
||||||
/// variantes de `PropsOp` porque cualquier componente ajeno puede necesitarlos sin tener un
|
/// componente ajeno puede necesitarlo sin tener un campo propio para ello), `Flex` sólo tiene
|
||||||
/// campo propio para ello), `Flex` sólo tiene sentido en los contenedores que ya declaran su
|
/// sentido en los contenedores que ya declaran su propio campo `flex: Flex` (`Container`,
|
||||||
/// propio campo `flex: Flex` (como `Container` o `Navbar`). Se les pasa aquí directamente, ya
|
/// `Navbar`...): se les pasa aquí directamente, ya resuelto (`self.flex()`), sin pasar por
|
||||||
/// resuelto (`self.flex()`), sin pasar por `PropsOp`.
|
/// `PropsOp`.
|
||||||
///
|
///
|
||||||
/// [`unpack()`]: Self::unpack
|
/// [`unpack()`]: Self::unpack
|
||||||
/// [`Flex`]: crate::html::flex::Flex
|
/// [`Flex`]: crate::html::flex::Flex
|
||||||
/// [`FlexItem`]: crate::html::flex::FlexItem
|
/// [`FlexItem`]: crate::html::flex::FlexItem
|
||||||
/// [`Margin`]: crate::html::spacing::Margin
|
/// [`PropsOp::FlexItem`]: crate::html::props::PropsOp::FlexItem
|
||||||
/// [`Padding`]: crate::html::spacing::Padding
|
|
||||||
pub fn unpack_with_flex<'a>(&'a self, cx: &mut Context, flex: Flex) -> impl RenderAttrs + 'a {
|
pub fn unpack_with_flex<'a>(&'a self, cx: &mut Context, flex: Flex) -> impl RenderAttrs + 'a {
|
||||||
let mut classes = String::new();
|
let mut classes = String::new();
|
||||||
flex.apply(cx, &mut classes);
|
flex.apply(cx, &mut classes);
|
||||||
self.flex_item.apply(cx, &mut classes);
|
self.flex_item.apply(cx, &mut classes);
|
||||||
self.margin.apply(cx, &mut classes);
|
|
||||||
self.padding.apply(cx, &mut classes);
|
|
||||||
PropsUnpack {
|
PropsUnpack {
|
||||||
props: self,
|
props: self,
|
||||||
classes,
|
classes,
|
||||||
|
|
@ -656,9 +639,10 @@ impl Props {
|
||||||
// la documentación de `PropsOp::AddStyle` sobre por qué los valores de estilo no se restringen
|
// la documentación de `PropsOp::AddStyle` sobre por qué los valores de estilo no se restringen
|
||||||
// a ASCII.
|
// a ASCII.
|
||||||
fn set_style(&mut self, property: &str, value: &str) {
|
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;
|
return;
|
||||||
};
|
};
|
||||||
|
let property = property.to_ascii_lowercase();
|
||||||
let Some(value) = util::non_blank(value) else {
|
let Some(value) = util::non_blank(value) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
@ -719,9 +703,8 @@ impl Props {
|
||||||
|
|
||||||
// Elimina la propiedad de estilo indicada, si existe.
|
// Elimina la propiedad de estilo indicada, si existe.
|
||||||
fn remove_style(&mut self, property: &str) {
|
fn remove_style(&mut self, property: &str) {
|
||||||
if let Some(property) = util::normalize_property(property) {
|
let property = property.trim().to_ascii_lowercase();
|
||||||
self.styles.retain(|(k, _)| k.as_ref() != property);
|
self.styles.retain(|(k, _)| k.as_ref() != property);
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -817,9 +800,8 @@ impl Props {
|
||||||
// **< PropsUnpack >********************************************************************************
|
// **< PropsUnpack >********************************************************************************
|
||||||
|
|
||||||
// Devuelto por `Props::unpack()`/`Props::unpack_with_flex()`. `classes` son las clases resueltas
|
// 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
|
// por `FlexItem::apply()`/`Flex::apply()` (desde el propio `unpack*()` usando el `&mut Context`),
|
||||||
// `unpack*()` usando el `&mut Context`), pendientes sólo de añadir a las del componente (ver
|
// pendientes sólo de añadir a las del componente (ver `Props::write_attrs()`).
|
||||||
// `Props::write_attrs()`).
|
|
||||||
struct PropsUnpack<'a> {
|
struct PropsUnpack<'a> {
|
||||||
props: &'a Props,
|
props: &'a Props,
|
||||||
classes: String,
|
classes: String,
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ use crate::CowStr;
|
||||||
use crate::core::TypeInfo;
|
use crate::core::TypeInfo;
|
||||||
use crate::html::flex::FlexItem;
|
use crate::html::flex::FlexItem;
|
||||||
use crate::html::props::extra::PropsExtra;
|
use crate::html::props::extra::PropsExtra;
|
||||||
use crate::html::spacing::{Margin, Padding};
|
|
||||||
|
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
@ -41,9 +40,8 @@ use std::sync::Arc;
|
||||||
/// se interpretan como si
|
/// se interpretan como si
|
||||||
/// fueran valores internos del componente para tomar decisiones durante el renderizado.
|
/// fueran valores internos del componente para tomar decisiones durante el renderizado.
|
||||||
///
|
///
|
||||||
/// [`FlexItem`](Self::FlexItem) aplica un posicionamiento Flexbox a nivel de ítem, mientras que
|
/// Finalmente, [`FlexItem`](Self::FlexItem) aplica un posicionamiento Flexbox a nivel de ítem sobre
|
||||||
/// [`Margin`](Self::Margin) y [`Padding`](Self::Padding) aplican márgenes y relleno interno; los
|
/// cualquier componente.
|
||||||
/// tres sobre cualquier componente.
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum PropsOp {
|
pub enum PropsOp {
|
||||||
/// Establece el identificador del componente normalizando el valor: recorta espacios, convierte
|
/// Establece el identificador del componente normalizando el valor: recorta espacios, convierte
|
||||||
|
|
@ -115,7 +113,7 @@ pub enum PropsOp {
|
||||||
/// Elimina el valor extra asociado a la clave indicada, si existe.
|
/// Elimina el valor extra asociado a la clave indicada, si existe.
|
||||||
RemoveExtra(&'static str),
|
RemoveExtra(&'static str),
|
||||||
/// Aplica un posicionamiento [`FlexItem`] a un componente particular en un contenedor [`Flex`].
|
/// 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
|
/// 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
|
/// propiedades de un ítem Flexbox tienen sentido sobre **cualquier** componente que pueda
|
||||||
|
|
@ -130,15 +128,6 @@ pub enum PropsOp {
|
||||||
/// [`Flex`]: crate::html::flex::Flex
|
/// [`Flex`]: crate::html::flex::Flex
|
||||||
/// [`Container::flex()`]: crate::base::component::Container::flex
|
/// [`Container::flex()`]: crate::base::component::Container::flex
|
||||||
FlexItem(FlexItem),
|
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 {
|
impl PropsOp {
|
||||||
|
|
@ -280,14 +269,4 @@ impl PropsOp {
|
||||||
pub fn flex_item(placement: FlexItem) -> Self {
|
pub fn flex_item(placement: FlexItem) -> Self {
|
||||||
Self::FlexItem(placement)
|
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 `<head>` 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<BreakpointEntry>,
|
|
||||||
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<T>` 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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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<UnitValue>,
|
|
||||||
/// Devuelve el margen inferior, por punto de corte.
|
|
||||||
#[getters(copy)]
|
|
||||||
bottom: Responsive<UnitValue>,
|
|
||||||
/// Devuelve el margen del lado lógico de inicio, por punto de corte.
|
|
||||||
#[getters(copy)]
|
|
||||||
start: Responsive<UnitValue>,
|
|
||||||
/// Devuelve el margen del lado lógico de fin, por punto de corte.
|
|
||||||
#[getters(copy)]
|
|
||||||
end: Responsive<UnitValue>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[builder_impl]
|
|
||||||
impl Margin {
|
|
||||||
/// Crea una configuración de margen sin ningún lado establecido.
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Margin BUILDER >*************************************************************************
|
|
||||||
|
|
||||||
/// Establece el margen superior.
|
|
||||||
pub fn with_top(mut self, value: UnitValue) -> Self {
|
|
||||||
self.top = self.top.set(value);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establece el margen superior, a partir del punto de corte indicado.
|
|
||||||
pub fn with_top_at(mut self, bp: Breakpoint, value: UnitValue) -> Self {
|
|
||||||
self.top = self.top.set_at(bp, value);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establece el margen inferior.
|
|
||||||
pub fn with_bottom(mut self, value: UnitValue) -> Self {
|
|
||||||
self.bottom = self.bottom.set(value);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establece el margen inferior, a partir del punto de corte indicado.
|
|
||||||
pub fn with_bottom_at(mut self, bp: Breakpoint, value: UnitValue) -> Self {
|
|
||||||
self.bottom = self.bottom.set_at(bp, value);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establece el margen del lado lógico de inicio (`margin-inline-start`).
|
|
||||||
pub fn with_start(mut self, value: UnitValue) -> Self {
|
|
||||||
self.start = self.start.set(value);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establece el margen del lado lógico de inicio (`margin-inline-start`), a partir del punto de
|
|
||||||
/// corte indicado.
|
|
||||||
pub fn with_start_at(mut self, bp: Breakpoint, value: UnitValue) -> Self {
|
|
||||||
self.start = self.start.set_at(bp, value);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establece el margen del lado lógico de fin (`margin-inline-end`).
|
|
||||||
pub fn with_end(mut self, value: UnitValue) -> Self {
|
|
||||||
self.end = self.end.set(value);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establece el margen del lado lógico de fin (`margin-inline-end`), a partir del punto de
|
|
||||||
/// corte indicado.
|
|
||||||
pub fn with_end_at(mut self, bp: Breakpoint, value: UnitValue) -> Self {
|
|
||||||
self.end = self.end.set_at(bp, value);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establece el mismo margen en ambos lados lógicos laterales (inicio y fin).
|
|
||||||
pub fn with_x(mut self, value: UnitValue) -> Self {
|
|
||||||
self.start = self.start.set(value);
|
|
||||||
self.end = self.end.set(value);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establece el mismo margen en ambos lados lógicos laterales (inicio y fin), a partir del
|
|
||||||
/// punto de corte indicado.
|
|
||||||
pub fn with_x_at(mut self, bp: Breakpoint, value: UnitValue) -> Self {
|
|
||||||
self.start = self.start.set_at(bp, value);
|
|
||||||
self.end = self.end.set_at(bp, value);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establece el mismo margen arriba y abajo.
|
|
||||||
pub fn with_y(mut self, value: UnitValue) -> Self {
|
|
||||||
self.top = self.top.set(value);
|
|
||||||
self.bottom = self.bottom.set(value);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establece el mismo margen arriba y abajo, a partir del punto de corte indicado.
|
|
||||||
pub fn with_y_at(mut self, bp: Breakpoint, value: UnitValue) -> Self {
|
|
||||||
self.top = self.top.set_at(bp, value);
|
|
||||||
self.bottom = self.bottom.set_at(bp, value);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establece el mismo margen en los cuatro lados.
|
|
||||||
pub fn with_all(mut self, value: UnitValue) -> Self {
|
|
||||||
self.top = self.top.set(value);
|
|
||||||
self.bottom = self.bottom.set(value);
|
|
||||||
self.start = self.start.set(value);
|
|
||||||
self.end = self.end.set(value);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establece el mismo margen en los cuatro lados, a partir del punto de corte indicado.
|
|
||||||
pub fn with_all_at(mut self, bp: Breakpoint, value: UnitValue) -> Self {
|
|
||||||
self.top = self.top.set_at(bp, value);
|
|
||||||
self.bottom = self.bottom.set_at(bp, value);
|
|
||||||
self.start = self.start.set_at(bp, value);
|
|
||||||
self.end = self.end.set_at(bp, value);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Margin {
|
|
||||||
/// Combina esta configuración con otra `Margin`, lado a lado y punto de corte a punto de corte.
|
|
||||||
/// Donde `margin` tenga un valor establecido, sustituye al de `self`, y donde no lo tenga, se
|
|
||||||
/// conserva el de `self`. Es el método que usa [`Props::with_prop()`] para que sucesivas
|
|
||||||
/// [`PropsOp::Margin`] sobre el mismo componente vayan completando lados concretos sin repetir
|
|
||||||
/// los ya establecidos.
|
|
||||||
///
|
|
||||||
/// [`Props::with_prop()`]: crate::html::Props::with_prop
|
|
||||||
/// [`PropsOp::Margin`]: crate::html::PropsOp::Margin
|
|
||||||
pub fn merge(mut self, margin: Margin) -> Self {
|
|
||||||
self.top = self.top.merge(margin.top);
|
|
||||||
self.bottom = self.bottom.merge(margin.bottom);
|
|
||||||
self.start = self.start.merge(margin.start);
|
|
||||||
self.end = self.end.merge(margin.end);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Aplica esta configuración como clases de utilidad responsive en el [`Context`], igual que
|
|
||||||
/// [`FlexItem::apply()`](crate::html::flex::FlexItem::apply). Un lado sin ningún valor
|
|
||||||
/// establecido, o con [`UnitValue::None`], no añade nada.
|
|
||||||
///
|
|
||||||
/// Las clases generadas se añaden a `classes`, separadas con un espacio de las que ya hubiera.
|
|
||||||
#[rustfmt::skip]
|
|
||||||
pub(crate) fn apply(self, cx: &mut Context, classes: &mut String) {
|
|
||||||
use crate::html::responsive::{apply, responsive_class, styles, value_to_token};
|
|
||||||
|
|
||||||
apply!(cx, classes, self.top, "_margin-top_", "margin-top", val);
|
|
||||||
apply!(cx, classes, self.bottom, "_margin-bottom_", "margin-bottom", val);
|
|
||||||
apply!(cx, classes, self.start, "_margin-start_", "margin-inline-start", val);
|
|
||||||
apply!(cx, classes, self.end, "_margin-end_", "margin-inline-end", val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Margin> for PropsOp {
|
|
||||||
fn from(margin: Margin) -> Self {
|
|
||||||
Self::margin(margin)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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<UnitValue>,
|
|
||||||
/// Devuelve el relleno interno inferior, por punto de corte.
|
|
||||||
#[getters(copy)]
|
|
||||||
bottom: Responsive<UnitValue>,
|
|
||||||
/// Devuelve el relleno interno del lado lógico de inicio, por punto de corte.
|
|
||||||
#[getters(copy)]
|
|
||||||
start: Responsive<UnitValue>,
|
|
||||||
/// Devuelve el relleno interno del lado lógico de fin, por punto de corte.
|
|
||||||
#[getters(copy)]
|
|
||||||
end: Responsive<UnitValue>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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<UnitValue>,
|
|
||||||
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<Padding> for PropsOp {
|
|
||||||
fn from(padding: Padding) -> Self {
|
|
||||||
Self::padding(padding)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -126,13 +126,6 @@ impl UnitValue {
|
||||||
pub const fn is_measurable(&self) -> bool {
|
pub const fn is_measurable(&self) -> bool {
|
||||||
!matches!(self, UnitValue::None | UnitValue::Auto)
|
!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.
|
/// Formatea la unidad como cadena CSS.
|
||||||
|
|
|
||||||
14
src/lib.rs
14
src/lib.rs
|
|
@ -109,15 +109,15 @@ use std::ops::Deref;
|
||||||
/// impl Theme for MyTheme {
|
/// impl Theme for MyTheme {
|
||||||
/// fn before_render_page_body(&self, page: &mut Page) {
|
/// fn before_render_page_body(&self, page: &mut Page) {
|
||||||
/// page
|
/// page
|
||||||
/// .alter_assets(
|
/// .alter_assets(AssetsOp::AddStyleSheet(
|
||||||
/// StyleSheet::from("/pagetop/css/normalize.css").with_version("8.0.1")
|
/// StyleSheet::from("/pagetop/css/normalize.css").with_version("8.0.1"),
|
||||||
/// )
|
/// ))
|
||||||
/// .alter_assets(
|
/// .alter_assets(AssetsOp::AddStyleSheet(
|
||||||
/// StyleSheet::from("/pagetop/css/basic.css").with_version(PAGETOP_VERSION),
|
/// 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")),
|
/// StyleSheet::from("/mytheme/styles.css").with_version(env!("CARGO_PKG_VERSION")),
|
||||||
/// );
|
/// ));
|
||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
|
||||||
|
|
@ -295,7 +295,7 @@ impl Contextual for Page {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn with_assets(mut self, op: impl Into<AssetsOp>) -> Self {
|
fn with_assets(mut self, op: AssetsOp) -> Self {
|
||||||
self.context.alter_assets(op);
|
self.context.alter_assets(op);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
|
||||||
14
src/util.rs
14
src/util.rs
|
|
@ -212,20 +212,6 @@ pub fn normalize_ascii(input: &str) -> Option<Cow<'_, str>> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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<str>) -> Option<String> {
|
|
||||||
non_blank(property.as_ref()).map(str::to_ascii_lowercase)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Recorta espacios, convierte una cadena vacía en `None` y normaliza el resto.
|
/// 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 `_`.
|
/// Convierte en un único token: en minúsculas y con cada espacio en blanco sustituido por `_`.
|
||||||
|
|
|
||||||
|
|
@ -105,75 +105,6 @@ async fn add_style_ignores_non_ascii_classes() {
|
||||||
assert!(r.is_empty());
|
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 >************************************************************************
|
// **< Class normalization >************************************************************************
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
|
|
@ -424,11 +355,11 @@ async fn render_has_no_line_breaks() {
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn context_add_responsive_style_feeds_responsives() {
|
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),
|
Some(Breakpoint::Md),
|
||||||
"col",
|
"col".into(),
|
||||||
"flex-basis",
|
"flex-basis".into(),
|
||||||
"50%",
|
"50%".into(),
|
||||||
));
|
));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
cx.responsive_styles().get_styles(Breakpoint::Md, "col"),
|
cx.responsive_styles().get_styles(Breakpoint::Md, "col"),
|
||||||
|
|
@ -439,30 +370,17 @@ async fn context_add_responsive_style_feeds_responsives() {
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn context_add_responsive_style_accumulates_across_calls() {
|
async fn context_add_responsive_style_accumulates_across_calls() {
|
||||||
let cx = Context::default()
|
let cx = Context::default()
|
||||||
.with_assets(AssetsOp::add_responsive_style(
|
.with_assets(AssetsOp::AddResponsiveStyle(
|
||||||
Some(Breakpoint::Md),
|
Some(Breakpoint::Md),
|
||||||
"col",
|
"col".into(),
|
||||||
"flex-basis",
|
"flex-basis".into(),
|
||||||
"50%",
|
"50%".into(),
|
||||||
))
|
))
|
||||||
.with_assets(AssetsOp::add_responsive_style(
|
.with_assets(AssetsOp::AddResponsiveStyle(
|
||||||
Some(Breakpoint::Md),
|
Some(Breakpoint::Md),
|
||||||
"col",
|
"col".into(),
|
||||||
"margin-inline-start",
|
"margin-inline-start".into(),
|
||||||
"0",
|
"0".into(),
|
||||||
));
|
|
||||||
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_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!(
|
assert_eq!(
|
||||||
cx.responsive_styles().get_styles(Breakpoint::Md, "col"),
|
cx.responsive_styles().get_styles(Breakpoint::Md, "col"),
|
||||||
|
|
@ -479,11 +397,11 @@ async fn context_default_has_no_responsive_styles() {
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn render_assets_includes_style_tag_with_responsive_styles() {
|
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),
|
Some(Breakpoint::Xs),
|
||||||
"col",
|
"col".into(),
|
||||||
"flex-basis",
|
"flex-basis".into(),
|
||||||
"100%",
|
"100%".into(),
|
||||||
));
|
));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
cx.render_assets().into_string(),
|
cx.render_assets().into_string(),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue