✨ (pagetop): Añade with_expand() a Navbar

Navbar guarda el punto de corte a partir del cual deja de colapsar
(`Breakpoint::Md` por defecto) y añade la clase `navbar-expand-{nombre}`
o `navbar-expand` si no tiene ancho mínimo. El nombre lo resuelve el
tema activo con `Theme::breakpoint_entry()`.
This commit is contained in:
Manuel Cillero 2026-09-19 01:52:55 +02:00
parent 5bf28ef4f0
commit 9143f1eb26
10 changed files with 271 additions and 49 deletions

View file

@ -156,7 +156,7 @@ impl Theme for Bootsier {
Dropdown => |c| theme::bs::dropdown::setup(c),
Image => |c| theme::bs::image::setup(c),
Nav => |c| theme::bs::nav::setup(c),
Navbar => |c| theme::bs::navbar::setup(c, cx),
Navbar => |c| theme::bs::navbar::setup(c),
form::input::Field => |c| theme::bs::form::input::setup(c),
form::select::Field => |c| theme::bs::form::select::setup(c),
form::Textarea => |c| theme::bs::form::textarea::setup(c),

View file

@ -36,9 +36,9 @@ const EXTRA_WIDTH: &str = "bootsier.container.width";
pub trait ContainerBootsier {
/// Establece el comportamiento del ancho para el contenedor.
///
/// Determina si el contenedor aplica los anchos máximos predefinidos para cada punto de
/// ruptura, o si ocupa siempre el 100% del ancho disponible, o lo hace hasta un ancho máximo
/// explícito. Ver [`Width`] para las variantes disponibles.
/// Determina si el contenedor aplica los anchos máximos predefinidos para cada punto de corte,
/// o si ocupa siempre el 100% del ancho disponible, o lo hace hasta un ancho máximo explícito.
/// Ver [`Width`] para las variantes disponibles.
fn with_width(self, width: Width) -> Self;
}
@ -55,12 +55,12 @@ impl ContainerBootsier for Container {
/// Define cómo se comporta el ancho de un contenedor ([`Container`]).
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum Width {
/// Comportamiento por defecto, aplica los anchos máximos predefinidos para cada punto de
/// ruptura. Por debajo del menor punto de ruptura ocupa el 100% del ancho disponible.
/// Comportamiento por defecto, aplica los anchos máximos predefinidos para cada punto de corte.
/// Por debajo del menor punto de corte ocupa el 100% del ancho disponible.
#[default]
Default,
/// Aplica los anchos máximos predefinidos a partir del punto de ruptura indicado. Por debajo de
/// ese punto de ruptura ocupa el 100% del ancho disponible.
/// Aplica los anchos máximos predefinidos a partir del punto de corte indicado. Por debajo de
/// ese punto de corte ocupa el 100% del ancho disponible.
From(Breakpoint),
/// Ocupa el 100% del ancho disponible siempre.
Fluid,
@ -73,7 +73,7 @@ impl Width {
/// Añade la clase asociada al ancho del contenedor a la cadena de clases.
///
/// El nombre del punto de ruptura se resuelve en el tema activo de `cx`.
/// El nombre del punto de corte se resuelve en el tema activo de `cx`.
#[inline]
pub fn push_to(self, cx: &Context, classes: &mut String) {
match self {

View file

@ -112,28 +112,28 @@ impl Direction {
/// Alineación horizontal del menú desplegable [`Dropdown`](crate::theme::bs::Dropdown).
///
/// Permite alinear el menú al inicio o al final del botón (respetando LTR/RTL) y añadirle una
/// alineación diferente a partir de un punto de ruptura ([`Breakpoint`]).
/// alineación diferente a partir de un punto de corte ([`Breakpoint`]).
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum MenuAlign {
/// Alineación al inicio (comportamiento por defecto).
#[default]
Start,
/// Alineación al inicio a partir del punto de ruptura indicado.
/// Alineación al inicio a partir del punto de corte indicado.
StartAt(Breakpoint),
/// Alineación al inicio por defecto, y al final a partir de un punto de ruptura válido.
/// Alineación al inicio por defecto, y al final a partir de un punto de corte válido.
StartAndEnd(Breakpoint),
/// Alineación al final.
End,
/// Alineación al final a partir del punto de ruptura indicado.
/// Alineación al final a partir del punto de corte indicado.
EndAt(Breakpoint),
/// Alineación al final por defecto, y al inicio a partir de un punto de ruptura válido.
/// Alineación al final por defecto, y al inicio a partir de un punto de corte válido.
EndAndStart(Breakpoint),
}
impl MenuAlign {
/// Añade las clases de alineación a la cadena de clases (sin incluir la base `dropdown-menu`).
///
/// El nombre del punto de ruptura se resuelve en el tema activo de `cx`.
/// El nombre del punto de corte se resuelve en el tema activo de `cx`.
#[inline]
pub fn push_to(self, cx: &Context, classes: &mut String) {
const MENU: &str = "dropdown-menu";

View file

@ -8,16 +8,18 @@ const TOGGLE_OFFCANVAS: &str = "offcanvas";
const EXTRA_LAYOUT: &str = "bootsier.navbar.layout";
const EXTRA_POSITION: &str = "bootsier.navbar.position";
const EXTRA_EXPAND: &str = "bootsier.navbar.expand";
/// Extensión de Bootsier para [`Navbar`](crate::theme::bs::Navbar).
/// Extensión de Bootsier para [`Navbar`].
///
/// Permite mostrar enlaces, menús y una marca de identidad en distintas disposiciones (simples, con
/// botón de despliegue o dentro de un [`Offcanvas`](crate::theme::bs::Offcanvas)), controladas por
/// [`navbar::Layout`](crate::theme::bs::navbar::Layout). También puede fijarse en la parte superior
/// o inferior del documento mediante [`navbar::Position`](crate::theme::bs::navbar::Position), y
/// definir a partir de qué punto de ruptura deja de colapsar con
/// [`with_expand()`](Self::with_expand).
/// botón de despliegue o dentro de un [`Offcanvas`]), controladas por [`navbar::Layout`]. También
/// puede fijarse en la parte superior o inferior del documento mediante [`navbar::Position`]. El
/// punto de corte a partir del cual deja de colapsar se define con [`Navbar::with_expand()`].
///
/// [`Navbar`]: crate::theme::bs::Navbar
/// [`Offcanvas`]: crate::theme::bs::Offcanvas
/// [`navbar::Layout`]: crate::theme::bs::navbar::Layout
/// [`navbar::Position`]: crate::theme::bs::navbar::Position
///
/// # Ejemplos
///
@ -42,7 +44,6 @@ const EXTRA_EXPAND: &str = "bootsier.navbar.expand";
/// # use pagetop::prelude::*;
/// # use pagetop_bootsier::theme::*;
/// let navbar = bs::Navbar::simple_toggle()
/// .with_expand(Breakpoint::Md)
/// .with_item(bs::navbar::Item::nav(
/// bs::Nav::new()
/// .with_item(bs::nav::Item::link(Lc::n("Home"), "/"))
@ -151,9 +152,6 @@ pub trait NavbarBootsier {
/// Crea una barra de navegación con **marca de identidad** y contenido en **offcanvas**.
fn offcanvas_brand_right(brand: Brand, oc: bs::Offcanvas) -> Self;
/// Define a partir de qué punto de ruptura la barra de navegación deja de colapsar.
fn with_expand(self, bp: Breakpoint) -> Self;
/// Define dónde se mostrará la barra de navegación dentro del documento.
fn with_position(self, position: bs::navbar::Position) -> Self;
}
@ -187,11 +185,6 @@ impl NavbarBootsier for Navbar {
navbar
}
fn with_expand(mut self, bp: Breakpoint) -> Self {
self.alter_prop(PropsOp::set_extra(EXTRA_EXPAND, bp));
self
}
fn with_position(mut self, position: bs::navbar::Position) -> Self {
self.alter_prop(PropsOp::set_extra(EXTRA_POSITION, position));
self
@ -200,15 +193,11 @@ impl NavbarBootsier for Navbar {
// **< Navbar SETUP >*******************************************************************************
pub(crate) fn setup(navbar: &mut Navbar, cx: &Context) {
// Sin `with_expand()`, colapsa por debajo de 768px, igual que el tema Basic (que no tiene
// punto de ruptura configurable y siempre usa ese umbral, ver `static/css/basic.css`).
let expand = navbar.props().extra_or(EXTRA_EXPAND, Breakpoint::Md);
pub(crate) fn setup(navbar: &mut Navbar) {
let position = navbar
.props()
.extra_or(EXTRA_POSITION, bs::navbar::Position::default());
let mut classes = String::new();
push_breakpoint_class(cx, expand, &mut classes, "navbar-expand", "");
position.push_to(&mut classes);
if !classes.is_empty() {
navbar.alter_prop(PropsOp::add_classes(classes));

View file

@ -14,7 +14,7 @@ use crate::theme::*;
/// - Se puede configurar el borde de la ventana desde el que se desliza el panel
/// ([`with_placement()`](Self::with_placement)).
/// - Encabezado con título ([`with_title()`](Self::with_title)) y **botón de cierre** integrado.
/// - Puede cambiar su comportamiento a partir de un punto de ruptura
/// - Puede cambiar su comportamiento a partir de un punto de corte
/// ([`with_breakpoint()`](Self::with_breakpoint)).
/// - Asocia título y controles de accesibilidad a un identificador único y expone atributos
/// adecuados para lectores de pantalla y navegación por teclado.
@ -47,7 +47,7 @@ pub struct Offcanvas {
props: Props,
/// Devuelve el título del panel.
title: Lc,
/// Devuelve el punto de ruptura configurado para cambiar el comportamiento del panel.
/// Devuelve el punto de corte configurado para cambiar el comportamiento del panel.
breakpoint: Breakpoint,
/// Devuelve el comportamiento configurado para la capa de fondo.
backdrop: bs::offcanvas::Backdrop,
@ -112,7 +112,7 @@ impl Offcanvas {
self
}
/// Establece el punto de ruptura a partir del cual cambia el comportamiento del panel.
/// Establece el punto de corte a partir del cual cambia el comportamiento del panel.
///
/// - **Por debajo** de ese tamaño de pantalla, el componente actúa como panel deslizante
/// ([`Offcanvas`]).

View file

@ -1,7 +1,7 @@
//! Define clases para aplicar en componentes del tema.
//!
//! Incluyen puntos de ruptura, colores y niveles de opacidad, escalas de tamaño y lados, necesarios
//! para crear determinadas clases del tema:
//! Incluyen colores y niveles de opacidad, escalas de tamaño y lados, necesarios para crear
//! determinadas clases del tema:
//!
//! ```rust,no_run
//! use pagetop_bootsier::theme::*;

View file

@ -50,12 +50,22 @@ async fn navbar_expands_at_md_by_default_and_at_the_chosen_breakpoint() {
let mut navbar = bs::Navbar::simple().with_item(item());
let html = navbar.render(&mut cx()).await.into_string();
assert!(html.contains("navbar-expand-md"));
// The expansion class comes from the `Navbar` component only, so it is not duplicated.
assert_eq!(html.matches("navbar-expand").count(), 1);
let mut navbar = bs::Navbar::simple()
.with_expand(Breakpoint::Xxxl)
.with_item(item());
let html = navbar.render(&mut cx()).await.into_string();
assert!(html.contains("navbar-expand-xxl"));
// Bootstrap needs a plain `navbar-expand` to keep the bar always expanded.
let mut navbar = bs::Navbar::simple()
.with_expand(Breakpoint::Xs)
.with_item(item());
let html = navbar.render(&mut cx()).await.into_string();
assert!(html.contains("navbar-expand"));
assert!(!html.contains("navbar-expand-"));
}
#[pagetop::test]