✨ (base): Implementa Navbar con funcionalidad base

Añade `Navbar::with_position()` y `navbar::Position` al núcleo, con las
clases de Basic para barras fijas y pegajosas, y `navbar::Item::form()`
para mostrar un formulario dentro de la barra.

Las barras sin botón de despliegue (`Simple` y `SimpleBrandLeft`) ya no
colapsan: el punto de corte no les afecta y su menú se mantiene en
línea, con salto de línea si no cabe. En Bootsier usan `navbar-expand` y
`SimpleBrandLeft` agrupa los contenidos junto a la marca.
This commit is contained in:
Manuel Cillero 2026-09-21 16:27:44 +02:00
parent f8f023b513
commit c47724bb68
14 changed files with 1298 additions and 209 deletions

View file

@ -68,20 +68,20 @@ async fn navbar_expands_at_md_by_default_and_at_the_chosen_breakpoint() {
let item =
|| bs::navbar::Item::nav(bs::Nav::new().with_item(bs::nav::Item::link(Lc::n("Home"), "/")));
let mut navbar = bs::Navbar::simple().with_item(item());
let mut navbar = bs::Navbar::simple_toggle().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()
let mut navbar = bs::Navbar::simple_toggle()
.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()
let mut navbar = bs::Navbar::simple_toggle()
.with_expand(Breakpoint::Xs)
.with_item(item());
let html = navbar.render(&mut cx()).await.into_string();
@ -89,6 +89,37 @@ async fn navbar_expands_at_md_by_default_and_at_the_chosen_breakpoint() {
assert!(!html.contains("navbar-expand-"));
}
#[pagetop::test]
async fn navbar_without_toggle_ignores_the_breakpoint() {
let item =
|| bs::navbar::Item::nav(bs::Nav::new().with_item(bs::nav::Item::link(Lc::n("Home"), "/")));
let brand = || Brand::new().with_title(Lc::n("PageTop"));
// With nothing to collapse, Bootstrap would still stack the menu below the breakpoint.
let mut navbar = bs::Navbar::simple()
.with_expand(Breakpoint::Lg)
.with_item(item());
let html = navbar.render(&mut cx()).await.into_string();
assert!(html.contains("navbar-expand"));
assert!(!html.contains("navbar-expand-"));
let mut navbar = bs::Navbar::simple_brand_left(brand())
.with_expand(Breakpoint::Lg)
.with_item(item());
let html = navbar.render(&mut cx()).await.into_string();
assert!(html.contains("navbar-expand"));
assert!(!html.contains("navbar-expand-"));
// The menu stays next to the brand instead of being pushed to the opposite end.
assert!(html.contains(r#"<div class="navbar-collapse">"#));
// The layouts with a toggle keep their breakpoint.
let mut navbar = bs::Navbar::brand_left(brand())
.with_expand(Breakpoint::Lg)
.with_item(item());
let html = navbar.render(&mut cx()).await.into_string();
assert!(html.contains("navbar-expand-lg"));
}
#[pagetop::test]
async fn offcanvas_uses_either_the_plain_or_the_breakpoint_class() {
// Bootstrap's `.offcanvas` is always fixed, so it must not accompany `.offcanvas-{bp}`.