✨ (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

@ -1,7 +1,7 @@
//! Definiciones para crear barras de navegación ([`Navbar`]) y sus elementos ([`Item`]).
mod props;
pub use props::Layout;
pub use props::{Layout, Position};
mod component;
pub use component::Navbar;

View file

@ -88,6 +88,9 @@ pub struct Navbar {
#[default(Breakpoint::Md)]
#[getters(copy)]
expand: Breakpoint,
/// Devuelve la posición de la barra en el documento.
#[getters(copy)]
position: navbar::Position,
/// Devuelve la lista de contenidos.
items: Children,
}
@ -115,6 +118,11 @@ impl Component for Navbar {
};
self.alter_prop(PropsOp::prepend_classes(class));
self.alter_prop(PropsOp::prepend_classes("navbar"));
let position = self.position().as_str();
if !position.is_empty() {
self.alter_prop(PropsOp::add_classes(position));
}
}
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
@ -237,11 +245,23 @@ impl Navbar {
/// Por debajo de ese punto de corte, en las disposiciones con botón de despliegue el contenido
/// queda oculto tras el botón. Con [`Breakpoint::Xs`] la barra nunca colapsa. Por defecto es
/// [`Breakpoint::Md`].
///
/// Sólo tiene efecto en las disposiciones con botón de despliegue: en
/// [`navbar::Layout::Simple`] y [`navbar::Layout::SimpleBrandLeft`] no hay nada que colapsar y
/// la barra siempre se muestra en línea.
pub fn with_expand(mut self, bp: Breakpoint) -> Self {
self.expand = bp;
self
}
/// Define dónde se mostrará la barra de navegación dentro del documento.
///
/// Por defecto es [`navbar::Position::Static`], la barra fluye con el documento.
pub fn with_position(mut self, position: navbar::Position) -> Self {
self.position = position;
self
}
/// Añade un nuevo contenido a la barra de navegación o modifica la lista de contenidos de la
/// barra con una operación [`TypedOp`].
///

View file

@ -18,6 +18,8 @@ pub enum Item {
Brand(Embed<Brand>),
/// Representa un menú de navegación [`Nav`].
Nav(Embed<Nav>),
/// Representa un formulario [`Form`] (búsqueda, botones...), renderizado tal cual.
Form(Embed<Form>),
/// Representa un *texto localizado* libre.
Text(Lc),
}
@ -33,6 +35,7 @@ impl Component for Item {
Self::Void => None,
Self::Brand(brand) => brand.id(),
Self::Nav(nav) => nav.id(),
Self::Form(form) => form.id(),
Self::Text(_) => None,
}
}
@ -53,6 +56,7 @@ impl Component for Item {
Self::Void => html! {},
Self::Brand(brand) => html! { (brand.render(cx).await) },
Self::Nav(nav) => html! { (nav.render(cx).await) },
Self::Form(form) => html! { (form.render(cx).await) },
Self::Text(text) => html! {
span class="navbar-text" {
(text.using(cx))
@ -77,6 +81,27 @@ impl Item {
Self::Nav(Embed::with(item))
}
/// Crea un elemento de tipo [`Form`] para añadir al contenido de [`Navbar`](super::Navbar).
///
/// # Ejemplo
///
/// Formulario de búsqueda dentro de la barra:
///
/// ```rust,no_run
/// # use pagetop::prelude::*;
/// let search = Form::new().with_action("/search").with_child(
/// Flex::new()
/// .with_gap(align::Gap::Both(UnitValue::RelRem(0.5)))
/// .with_child(form::input::Field::search().with_name("q"))
/// .with_child(Button::submit(Lc::n("Search"))),
/// );
///
/// let navbar = Navbar::simple().with_item(navbar::Item::form(search));
/// ```
pub fn form(form: Form) -> Self {
Self::Form(Embed::with(form))
}
/// Crea un elemento con un *texto localizado*, mostrado sin interacción.
pub fn text(item: Lc) -> Self {
Self::Text(item)

View file

@ -6,8 +6,8 @@ use crate::prelude::*;
/// [`Navbar`](super::Navbar).
///
/// Sólo recoge las combinaciones de marca y botón de despliegue independientes de cualquier
/// framework CSS. Un tema puede definir su propia variante de disposición (posiciones fijas,
/// contenido en un panel lateral...) con su propio tipo, sin depender de éste.
/// framework CSS. Un tema puede definir su propia variante de disposición (contenido en un panel
/// lateral...) con su propio tipo, sin depender de éste.
#[derive(AutoDefault, Clone, Debug)]
pub enum Layout {
/// Barra simple, sin marca de identidad y sin botón de despliegue.
@ -30,3 +30,39 @@ pub enum Layout {
/// Barra con botón de despliegue y marca de identidad, en ese orden.
BrandRight(Embed<Brand>),
}
// **< Position >***********************************************************************************
/// Posición de una barra de navegación [`Navbar`](super::Navbar) en el documento.
///
/// Las variantes `Fixed*` sacan la barra del flujo del documento, por lo que pueden tapar el
/// contenido que queda debajo: hay que reservarle espacio.
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum Position {
/// Barra normal, fluye con el documento.
#[default]
Static,
/// Barra fijada en la parte superior, siempre visible.
FixedTop,
/// Barra fijada en la parte inferior, siempre visible.
FixedBottom,
/// Barra que se fija en la parte superior al hacer *scroll*.
StickyTop,
/// Barra que se fija en la parte inferior al hacer *scroll*.
StickyBottom,
}
impl Position {
/// Devuelve la clase CSS asociada a la posición, o una cadena vacía si es `Static`.
#[rustfmt::skip]
#[inline]
pub const fn as_str(self) -> &'static str {
match self {
Self::Static => "",
Self::FixedTop => "fixed-top",
Self::FixedBottom => "fixed-bottom",
Self::StickyTop => "sticky-top",
Self::StickyBottom => "sticky-bottom",
}
}
}