(pagetop): Añade puntos de corte a Flex/FlexItem

- `Breakpoint` gana variante `Xxxl` y métodos `name()`/`min_width()`/
  `resolved()`, para consultarse contra el tema activo.
- `Theme::breakpoint_entry()` sustituye a `breakpoint_min_width()`:
  devuelve un `BreakpointEntry` con nombre y ancho mínimo.
- Nuevo `Responsive<T>`, valor en cascada mobile-first por punto de
  corte.
- Ejemplo `examples/intro-responsive.rs` con los patrones de uso.
This commit is contained in:
Manuel Cillero 2026-09-12 11:13:19 +02:00
parent 8577ca8a59
commit 0b8f3f3000
22 changed files with 1257 additions and 359 deletions

View file

@ -49,7 +49,7 @@ pub struct Container {
kind: Kind,
/// Devuelve el posicionamiento Flexbox como contenedor, si tiene alguno.
#[getters(copy)]
flex: Option<Flex>,
flex: Flex,
/// Devuelve la lista de componentes (`children`) del contenedor.
children: Children,
}
@ -64,25 +64,20 @@ impl Component for Container {
self.props.get_id()
}
fn setup(&mut self, _cx: &mut Context) {
if let Some(flex) = self.flex() {
flex.apply_to(&mut self.props);
}
}
#[rustfmt::skip]
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
let output = self.children().render(cx).await;
if output.is_empty() {
return Ok(html! {});
}
let container_props = self.props().unpack_with_flex(cx, self.flex());
Ok(match self.kind() {
Kind::Default => html! { div (self.props().unpack(cx)) { (output) } },
Kind::Main => html! { main (self.props().unpack(cx)) { (output) } },
Kind::Header => html! { header (self.props().unpack(cx)) { (output) } },
Kind::Footer => html! { footer (self.props().unpack(cx)) { (output) } },
Kind::Section => html! { section (self.props().unpack(cx)) { (output) } },
Kind::Article => html! { article (self.props().unpack(cx)) { (output) } },
Kind::Default => html! { div (container_props) { (output) } },
Kind::Main => html! { main (container_props) { (output) } },
Kind::Header => html! { header (container_props) { (output) } },
Kind::Footer => html! { footer (container_props) { (output) } },
Kind::Section => html! { section (container_props) { (output) } },
Kind::Article => html! { article (container_props) { (output) } },
})
}
}
@ -145,7 +140,7 @@ impl Container {
/// Establece el posicionamiento Flexbox como contenedor (usa `None` para quitarlo).
pub fn with_flex(mut self, flex: impl Into<Option<Flex>>) -> Self {
self.flex = flex.into();
self.flex = self.flex.merge(flex);
self
}

View file

@ -83,7 +83,7 @@ pub struct Navbar {
layout: navbar::Layout,
/// Devuelve el posicionamiento Flexbox como contenedor, si tiene alguno.
#[getters(copy)]
flex: Option<Flex>,
flex: Flex,
/// Devuelve la lista de contenidos.
items: Children,
}
@ -106,21 +106,6 @@ impl Component for Navbar {
}
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
// Botón de despliegue para el contenido colapsable de la barra.
fn button(cx: &mut Context, id_content: &str) -> Markup {
html! {
button
type="button"
class="navbar-toggle"
aria-expanded="false"
aria-controls=(id_content)
aria-label=[Lc::l("navbar_toggle").lookup(cx)]
{
span class="navbar-toggle-icon" {}
}
}
}
// Si no hay contenidos, no tiene sentido mostrar una barra vacía.
let items = self.items().render(cx).await;
if items.is_empty() {
@ -131,44 +116,53 @@ impl Component for Navbar {
let id = self.id().unwrap();
let id_content = util::join!(id, "-content");
// Botón de despliegue para el contenido colapsable de la barra.
let button = html! {
button
type="button"
class="navbar-toggle"
aria-expanded="false"
aria-controls=(&id_content)
aria-label=[Lc::l("navbar_toggle").lookup(cx)]
{
span class="navbar-toggle-icon" {}
}
};
// Posicionamiento Flexbox opcional (no del `<nav>`, cuya estructura la fija `layout()`).
let mut content_props = Props::default();
if let Some(flex) = self.flex() {
flex.apply_to(&mut content_props);
}
content_props.alter_prop(PropsOp::prepend_classes("navbar-content"));
let content_props = Props::classes("navbar-content").with_id(id_content);
Ok(html! {
nav (self.props().unpack(cx)) {
@match self.layout() {
// Barra más sencilla: sólo contenido, siempre visible.
navbar::Layout::Simple => {
div (content_props.unpack(cx)) { (items) }
div (content_props.unpack_with_flex(cx, self.flex())) { (items) }
},
// Barra sencilla que se puede contraer/expandir.
navbar::Layout::SimpleToggle => {
(button(cx, &id_content))
div id=(&id_content) (content_props.unpack(cx)) { (items) }
(button)
div (content_props.unpack_with_flex(cx, self.flex())) { (items) }
},
// Barra con marca, siempre visible, sin botón.
navbar::Layout::SimpleBrandLeft(brand) => {
(brand.render(cx).await)
div (content_props.unpack(cx)) { (items) }
div (content_props.unpack_with_flex(cx, self.flex())) { (items) }
},
// Barra con marca y botón, en ese orden.
navbar::Layout::BrandLeft(brand) => {
(brand.render(cx).await)
(button(cx, &id_content))
div id=(&id_content) (content_props.unpack(cx)) { (items) }
(button)
div (content_props.unpack_with_flex(cx, self.flex())) { (items) }
},
// Barra con botón y marca, en ese orden.
navbar::Layout::BrandRight(brand) => {
(button(cx, &id_content))
div id=(&id_content) (content_props.unpack(cx)) { (items) }
(button)
div (content_props.unpack_with_flex(cx, self.flex())) { (items) }
(brand.render(cx).await)
},
}
@ -231,7 +225,7 @@ impl Navbar {
/// No afecta a la posición de la marca ni del botón de despliegue, que quedan fijados con
/// [`with_layout()`](Self::with_layout).
pub fn with_flex(mut self, flex: impl Into<Option<Flex>>) -> Self {
self.flex = flex.into();
self.flex = self.flex.merge(flex);
self
}