(base): Añade componentes Nav y Navbar

Dropdown incorpora tamaño y estilo de botón configurables
(`button::Size`/`Style`), reutilizados también por Navbar. El tema Basic
sirve `basic.min.css` y añade el JS de inicialización de Navbar.
This commit is contained in:
Manuel Cillero 2026-08-28 18:24:43 +02:00
parent da959183f6
commit 35ae5bb6a8
22 changed files with 1695 additions and 280 deletions

View file

@ -47,6 +47,14 @@ pub use intro::{Intro, IntroOpening};
mod messages;
pub use messages::Messages;
pub mod nav;
#[doc(inline)]
pub use nav::Nav;
pub mod navbar;
#[doc(inline)]
pub use navbar::Navbar;
mod pager;
pub use pager::{Pager, PagerAlign, PagerVisibility};

View file

@ -31,6 +31,12 @@ pub struct Dropdown {
title: Lc,
/// Devuelve si el botón se desdobla (*split*) en botón de acción más *toggle*.
button_split: bool,
/// Devuelve el tamaño visual del botón.
#[getters(copy)]
button_size: button::Size,
/// Devuelve el estilo visual del botón.
#[getters(copy)]
button_style: button::Style,
/// Devuelve la lista de elementos del menú.
items: Children,
}
@ -65,15 +71,44 @@ impl Component for Dropdown {
});
}
// Sin tamaño ni estilo (caso más común), evita construir dinámicamente las clases.
let (button_classes, toggle_classes): (CowStr, CowStr) =
if matches!(self.button_size(), button::Size::None)
&& matches!(self.button_style(), button::Style::None)
{
(
CowStr::from("dropdown-button"),
CowStr::from("dropdown-toggle"),
)
} else {
use button::{Size, Style};
let size_class = match self.button_size() {
Size::None => "",
Size::Small => " button-sm",
Size::Large => " button-lg",
};
let style_class = match self.button_style() {
Style::None => String::new(),
Style::Solid(intent) => util::join!(" button-", intent.color(cx)),
Style::Outline(intent) => util::join!(" button-outline-", intent.color(cx)),
Style::Link => " button-link".to_string(),
};
(
util::join!("dropdown-button", size_class, &style_class).into(),
util::join!("dropdown-toggle", size_class, &style_class).into(),
)
};
let toggle_label = Lc::l("dropdown_toggle").using(cx);
Ok(html! {
div (self.props()) {
@if *self.button_split() {
button type="button" class="dropdown-button" { (&title) }
button type="button" class=(&button_classes) { (&title) }
button
type="button"
class="dropdown-toggle"
class=(&toggle_classes)
aria-haspopup="true"
aria-expanded="false"
{
@ -82,7 +117,7 @@ impl Component for Dropdown {
} @else {
button
type="button"
class="dropdown-toggle"
class=(&toggle_classes)
aria-haspopup="true"
aria-expanded="false"
{
@ -126,6 +161,20 @@ impl Dropdown {
self
}
/// Establece el tamaño visual del botón (usa [`button::Size::None`] para quitarlo).
#[builder_fn]
pub fn with_button_size(mut self, size: button::Size) -> Self {
self.button_size = size;
self
}
/// Establece el estilo visual del botón (usa [`button::Style::None`] para quitarlo).
#[builder_fn]
pub fn with_button_style(mut self, style: button::Style) -> Self {
self.button_style = style;
self
}
/// Añade un nuevo elemento al menú o modifica la lista de elementos del menú con una operación
/// [`TypedOp`].
///

10
src/base/component/nav.rs Normal file
View file

@ -0,0 +1,10 @@
//! Definiciones para crear menús de navegación planos ([`Nav`]) y sus elementos ([`Item`]).
mod props;
pub use props::Layout;
mod component;
pub use component::Nav;
mod item;
pub use item::{Item, ItemKind};

View file

@ -0,0 +1,113 @@
use crate::prelude::*;
/// Componente para crear un **menú de navegación plano**.
///
/// Presenta una lista de elementos [`nav::Item`](super::Item), donde alguno puede desplegar un
/// [`Dropdown`](super::super::Dropdown) embebido.
///
/// Si no contiene elementos, el componente **no se renderiza**.
///
/// # Ejemplo
///
/// ```rust,no_run
/// use pagetop::prelude::*;
///
/// let nav = nav::Nav::new()
/// .with_item(nav::Item::link(Lc::n("Home"), "/"))
/// .with_item(nav::Item::link_blank(Lc::n("External"), "https://docs.rs"))
/// .with_item(nav::Item::dropdown(
/// dropdown::Dropdown::new()
/// .with_title(Lc::n("Options"))
/// .with_item(dropdown::Item::link(Lc::n("Action"), "/action"))
/// .with_item(dropdown::Item::link(Lc::n("Another"), "/another")),
/// ))
/// .with_item(nav::Item::link_disabled(Lc::n("Disabled"), "#"));
/// ```
#[derive(AutoDefault, Clone, Debug, Getters)]
pub struct Nav {
/// Devuelve identificador, clases CSS, atributos HTML y valores extra del componente.
props: Props,
/// Devuelve la distribución y orientación seleccionada.
nav_layout: nav::Layout,
/// Devuelve la lista de elementos del menú.
items: Children,
}
#[async_trait]
impl Component for Nav {
fn new() -> Self {
Self::default()
}
fn id(&self) -> Option<String> {
self.props.get_id()
}
fn setup(&mut self, _cx: &Context) {
self.alter_prop(PropsOp::prepend_classes(match self.nav_layout() {
nav::Layout::Default => "nav",
nav::Layout::Start => "nav nav-start",
nav::Layout::Center => "nav nav-center",
nav::Layout::End => "nav nav-end",
nav::Layout::Vertical => "nav nav-vertical",
nav::Layout::Fill => "nav nav-fill",
nav::Layout::Justified => "nav nav-justified",
}));
}
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
let items = self.items().render(cx).await;
if items.is_empty() {
return Ok(html! {});
}
Ok(html! {
ul (self.props()) {
(items)
}
})
}
}
impl Nav {
// **< Nav BUILDER >****************************************************************************
/// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`.
#[builder_fn]
pub fn with_id(mut self, id: impl Into<CowStr>) -> Self {
self.props.alter_id(id);
self
}
/// Modifica identificador, clases CSS, atributos HTML o valores extra del componente.
#[builder_fn]
pub fn with_prop(mut self, op: PropsOp) -> Self {
self.props.alter_prop(op);
self
}
/// Selecciona la distribución y orientación del menú.
#[builder_fn]
pub fn with_layout(mut self, layout: nav::Layout) -> Self {
self.nav_layout = layout;
self
}
/// Añade un nuevo elemento al menú o modifica la lista de elementos del menú con una
/// operación [`TypedOp`].
///
/// # Ejemplo
///
/// ```rust,ignore
/// nav.with_item(nav::Item::link("Inicio", "/"));
/// nav.with_item(TypedOp::AddMany(vec![
/// nav::Item::link(...),
/// nav::Item::link_disabled(...),
/// ]));
/// ```
#[builder_fn]
pub fn with_item(mut self, op: impl Into<TypedOp<nav::Item>>) -> Self {
self.items.alter_child(op.into());
self
}
}

View file

@ -0,0 +1,291 @@
use crate::prelude::*;
// **< ItemKind >***********************************************************************************
/// Tipos de [`nav::Item`](super::Item) disponibles en un menú [`Nav`](super::Nav).
///
/// Define internamente la naturaleza del elemento y su comportamiento al mostrarse o interactuar
/// con él.
#[derive(AutoDefault, Clone, Debug)]
pub enum ItemKind {
/// Elemento vacío, no produce salida.
#[default]
Void,
/// Etiqueta sin comportamiento interactivo.
Label(Lc),
/// Elemento de navegación basado en una [`RoutePath`] dinámica resuelta por una [`Route`].
/// Opcionalmente, puede abrirse en una nueva ventana y estar inicialmente deshabilitado.
Link {
label: Lc,
route: Route,
blank: bool,
disabled: bool,
},
/// Contenido HTML arbitrario. El componente [`Html`] se renderiza tal cual como elemento del
/// menú, sin añadir ningún comportamiento de navegación adicional.
Html(Embed<Html>),
/// Elemento que despliega un menú [`Dropdown`](super::super::Dropdown).
Dropdown(Embed<Dropdown>),
}
impl ItemKind {
const ITEM: &str = "nav-item";
const DROPDOWN: &str = "nav-item dropdown";
/// Devuelve las clases base asociadas al tipo de elemento.
#[inline]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Void => "",
Self::Dropdown(_) => Self::DROPDOWN,
_ => Self::ITEM,
}
}
}
// **< Item >***************************************************************************************
/// Representa un **elemento individual** de un menú [`Nav`](super::Nav).
///
/// Cada instancia de [`nav::Item`](super::Item) se traduce en un componente visible que puede
/// comportarse como texto, enlace, contenido HTML o menú desplegable, según su [`ItemKind`].
///
/// Permite definir el identificador, las clases de estilo adicionales y el tipo de interacción
/// asociada, manteniendo una interfaz común para renderizar todos los elementos del menú.
#[derive(AutoDefault, Clone, Debug, Getters)]
pub struct Item {
/// Devuelve identificador, clases CSS, atributos HTML y valores extra del componente.
props: Props,
/// Devuelve el tipo de elemento representado.
item_kind: ItemKind,
/// Devuelve el valor de `active` forzado explícitamente, si lo hay.
///
/// Sin forzar (`None`), un [`ItemKind::Link`] se marca activo cuando su ruta coincide
/// exactamente con la del *request* actual (ver [`prepare()`](Component::prepare)). Forzarlo
/// permite otros criterios, como marcar activa una sección mientras la ruta actual sea
/// cualquiera de sus subrutas.
active_override: Option<bool>,
}
#[async_trait]
impl Component for Item {
fn new() -> Self {
Self::default()
}
fn id(&self) -> Option<String> {
self.props.get_id()
}
fn setup(&mut self, _cx: &Context) {
self.alter_prop(PropsOp::prepend_classes(self.item_kind().as_str()));
}
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
Ok(match self.item_kind() {
ItemKind::Void => html! {},
ItemKind::Label(label) => html! {
li (self.props()) {
span class="nav-link disabled" aria-disabled="true" {
(label.using(cx))
}
}
},
ItemKind::Link {
label,
route,
blank,
disabled,
} => {
let enabled = !*disabled;
// Deshabilitado, `href` no la usa: evita resolver la ruta para nada.
let route_link = enabled.then(|| route.resolve(cx));
let current_path = cx.request().map(|request| request.path());
let is_current = self
.active_override()
.copied()
.unwrap_or(enabled && route_link.as_ref().map(RoutePath::path) == current_path);
let active_class = if is_current { " active" } else { "" };
let disabled_class = if *disabled { " disabled" } else { "" };
let classes = util::join!("nav-link", active_class, disabled_class);
let target = (enabled && *blank).then_some("_blank");
let rel = (enabled && *blank).then_some("noopener noreferrer");
let aria_current = (enabled && is_current).then_some("page");
let aria_disabled = (*disabled).then_some("true");
html! {
li (self.props()) {
a
class=(classes)
href=[route_link]
target=[target]
rel=[rel]
aria-current=[aria_current]
aria-disabled=[aria_disabled]
{
(label.using(cx))
}
}
}
}
ItemKind::Html(html) => html! {
li (self.props()) {
(html.render(cx).await)
}
},
ItemKind::Dropdown(menu) => {
if let Some(dd) = menu.get() {
let items = dd.items().render(cx).await;
if items.is_empty() {
return Ok(html! {});
}
let title = dd.title().using(cx);
let title = if title.is_empty() {
Lc::l("dropdown_default_title").using(cx)
} else {
title
};
html! {
li (self.props()) {
a
class="nav-link dropdown-toggle"
href="#"
role="button"
aria-haspopup="true"
aria-expanded="false"
{
(title)
}
ul class="dropdown-menu" {
(items)
}
}
}
} else {
html! {}
}
}
})
}
}
impl Item {
/// Crea un elemento de tipo texto, mostrado sin interacción.
pub fn label(label: Lc) -> Self {
Self {
item_kind: ItemKind::Label(label),
..Default::default()
}
}
/// Crea un enlace para la navegación.
///
/// La ruta se obtiene invocando [`Route::resolve()`], que devuelve dinámicamente una
/// [`RoutePath`] en función del [`Context`]. El enlace se marca como `active` si la ruta
/// actual del *request* coincide con la ruta de destino (devuelta por `RoutePath::path`).
pub fn link(label: Lc, route: impl Into<Route>) -> Self {
Self {
item_kind: ItemKind::Link {
label,
route: route.into(),
blank: false,
disabled: false,
},
..Default::default()
}
}
/// Crea un enlace deshabilitado que no permite la interacción.
pub fn link_disabled(label: Lc, route: impl Into<Route>) -> Self {
Self {
item_kind: ItemKind::Link {
label,
route: route.into(),
blank: false,
disabled: true,
},
..Default::default()
}
}
/// Crea un enlace que se abre en una nueva ventana o pestaña.
pub fn link_blank(label: Lc, route: impl Into<Route>) -> Self {
Self {
item_kind: ItemKind::Link {
label,
route: route.into(),
blank: true,
disabled: false,
},
..Default::default()
}
}
/// Crea un enlace inicialmente deshabilitado que se abriría en una nueva ventana.
pub fn link_blank_disabled(label: Lc, route: impl Into<Route>) -> Self {
Self {
item_kind: ItemKind::Link {
label,
route: route.into(),
blank: true,
disabled: true,
},
..Default::default()
}
}
/// Crea un elemento con contenido HTML arbitrario.
///
/// El contenido se renderiza tal cual lo devuelve el componente [`Html`], dentro de un `<li>`
/// con las clases de navegación asociadas a [`Item`].
pub fn html(html: Html) -> Self {
Self {
item_kind: ItemKind::Html(Embed::with(html)),
..Default::default()
}
}
/// Crea un elemento de navegación que contiene un menú desplegable
/// [`Dropdown`](super::super::Dropdown).
///
/// Sólo se tienen en cuenta **el título** (si no existe, se asigna uno por defecto) y **la
/// lista de elementos** del [`Dropdown`](super::super::Dropdown); el resto de propiedades no
/// afectarán a su representación en [`Nav`](super::Nav).
pub fn dropdown(menu: Dropdown) -> Self {
Self {
item_kind: ItemKind::Dropdown(Embed::with(menu)),
..Default::default()
}
}
// **< Item BUILDER >***************************************************************************
/// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`.
#[builder_fn]
pub fn with_id(mut self, id: impl Into<CowStr>) -> Self {
self.props.alter_id(id);
self
}
/// Modifica identificador, clases CSS, atributos HTML o valores extra del componente.
#[builder_fn]
pub fn with_prop(mut self, op: PropsOp) -> Self {
self.props.alter_prop(op);
self
}
/// Fuerza si un [`ItemKind::Link`] se marca activo, o `None` para volver a la detección
/// automática por coincidencia exacta de ruta.
#[builder_fn]
pub fn with_active(mut self, active: impl Into<Option<bool>>) -> Self {
self.active_override = active.into();
self
}
}

View file

@ -0,0 +1,26 @@
use crate::prelude::*;
// **< Layout >*************************************************************************************
/// Distribución y orientación de un menú [`Nav`](super::Nav).
///
/// Las variantes son puramente semánticas: cada tema decide en su propio `setup()` qué clases CSS
/// les corresponden, según su propio vocabulario de estilos.
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum Layout {
/// Comportamiento por defecto, ancho definido por el contenido y sin alineación forzada.
#[default]
Default,
/// Alinea los elementos al inicio de la fila.
Start,
/// Centra horizontalmente los elementos.
Center,
/// Alinea los elementos al final de la fila.
End,
/// Apila los elementos en columna.
Vertical,
/// Los elementos se expanden para rellenar la fila.
Fill,
/// Todos los elementos ocupan el mismo ancho rellenando la fila.
Justified,
}

View file

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

View file

@ -0,0 +1,238 @@
use crate::prelude::*;
/// Componente para crear una **barra de navegación**.
///
/// Permite mostrar enlaces, menús desplegables ([`nav::Item::dropdown()`]) y una marca de
/// identidad, en distintas disposiciones controladas por [`navbar::Layout`].
///
/// Si no contiene elementos, el componente **no se renderiza**.
///
/// # Ejemplos
///
/// Barra **simple**, sólo con un menú horizontal:
///
/// ```rust,no_run
/// use pagetop::prelude::*;
///
/// let navbar = Navbar::simple()
/// .with_item(navbar::Item::nav(
/// Nav::new()
/// .with_item(nav::Item::link(Lc::n("Home"), "/"))
/// .with_item(nav::Item::link(Lc::n("About"), "/about"))
/// .with_item(nav::Item::link(Lc::n("Contact"), "/contact")),
/// ));
/// ```
///
/// Barra **colapsable**, con botón de despliegue:
///
/// ```rust,no_run
/// # use pagetop::prelude::*;
/// let navbar = Navbar::simple_toggle()
/// .with_item(navbar::Item::nav(
/// Nav::new()
/// .with_item(nav::Item::link(Lc::n("Home"), "/"))
/// .with_item(nav::Item::link_blank(Lc::n("Doc"), "https://docs.rs"))
/// .with_item(nav::Item::link(Lc::n("Support"), "/support")),
/// ));
/// ```
///
/// Barra con **marca de identidad** y menú, con menús desplegables:
///
/// ```rust,no_run
/// # use pagetop::prelude::*;
/// let brand = Brand::new()
/// .with_title(Lc::n("PageTop"))
/// .with_route(Route::from("/"));
///
/// let navbar = Navbar::brand_left(brand)
/// .with_item(navbar::Item::nav(
/// Nav::new()
/// .with_item(nav::Item::link(Lc::n("Home"), "/"))
/// .with_item(nav::Item::dropdown(
/// Dropdown::new()
/// .with_title(Lc::n("Tools"))
/// .with_item(dropdown::Item::link(Lc::n("Generator"), "/tools/gen"))
/// .with_item(dropdown::Item::link(Lc::n("Reports"), "/tools/reports")),
/// ))
/// .with_item(nav::Item::link_disabled(Lc::n("Disabled"), "#")),
/// ));
/// ```
///
/// Barra con **botón de despliegue** y **marca de identidad**, en ese orden:
///
/// ```rust,no_run
/// # use pagetop::prelude::*;
/// let brand = Brand::new()
/// .with_title(Lc::n("Intranet"))
/// .with_route(Route::from("/"));
///
/// let navbar = Navbar::brand_right(brand).with_item(navbar::Item::nav(
/// Nav::new()
/// .with_item(nav::Item::link(Lc::n("Dashboard"), "/dashboard"))
/// .with_item(nav::Item::link(Lc::n("Users"), "/users")),
/// ));
/// ```
///
/// [`nav::Item::dropdown()`]: super::super::nav::Item::dropdown
/// [`navbar::Layout`]: super::Layout
#[derive(AutoDefault, Clone, Debug, Getters)]
pub struct Navbar {
/// Devuelve identificador, clases CSS, atributos HTML y valores extra del componente.
props: Props,
/// Devuelve la disposición configurada para la barra de navegación.
layout: navbar::Layout,
/// Devuelve la lista de contenidos.
items: Children,
}
#[async_trait]
impl Component for Navbar {
fn new() -> Self {
Self::default()
}
fn id(&self) -> Option<String> {
self.props.get_id()
}
fn setup(&mut self, cx: &Context) {
// Asegura que la barra de navegación tiene un identificador único: lo necesita el botón de
// despliegue para referenciar el contenido colapsable con `aria-controls`.
self.alter_prop(PropsOp::ensure_id(cx.build_id::<Self>(1)));
self.alter_prop(PropsOp::prepend_classes("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() {
return Ok(html! {});
}
// `setup()` garantiza que habrá un `id` antes de renderizar.
let id = self.id().unwrap();
let id_content = util::join!(id, "-content");
Ok(html! {
nav (self.props()) {
@match self.layout() {
// Barra más sencilla: sólo contenido, siempre visible.
navbar::Layout::Simple => {
div class="navbar-content" { (items) }
},
// Barra sencilla que se puede contraer/expandir.
navbar::Layout::SimpleToggle => {
(button(cx, &id_content))
div id=(&id_content) class="navbar-content" { (items) }
},
// Barra con marca, siempre visible, sin botón.
navbar::Layout::SimpleBrandLeft(brand) => {
(brand.render(cx).await)
div class="navbar-content" { (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) class="navbar-content" { (items) }
},
// Barra con botón y marca, en ese orden.
navbar::Layout::BrandRight(brand) => {
(button(cx, &id_content))
div id=(&id_content) class="navbar-content" { (items) }
(brand.render(cx).await)
},
}
}
})
}
}
impl Navbar {
/// Crea una barra de navegación **simple**, sin marca y sin botón.
pub fn simple() -> Self {
Self::default().with_layout(navbar::Layout::Simple)
}
/// Crea una barra de navegación **simple pero colapsable**, con botón de despliegue.
pub fn simple_toggle() -> Self {
Self::default().with_layout(navbar::Layout::SimpleToggle)
}
/// Crea una barra de navegación **con marca de identidad**, siempre visible, sin botón.
pub fn simple_brand_left(brand: Brand) -> Self {
Self::default().with_layout(navbar::Layout::SimpleBrandLeft(Embed::with(brand)))
}
/// Crea una barra de navegación con **marca de identidad** y **botón de despliegue**, en ese
/// orden.
pub fn brand_left(brand: Brand) -> Self {
Self::default().with_layout(navbar::Layout::BrandLeft(Embed::with(brand)))
}
/// Crea una barra de navegación con **botón de despliegue** y **marca de identidad**, en ese
/// orden.
pub fn brand_right(brand: Brand) -> Self {
Self::default().with_layout(navbar::Layout::BrandRight(Embed::with(brand)))
}
// **< Navbar BUILDER >*************************************************************************
/// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`.
#[builder_fn]
pub fn with_id(mut self, id: impl Into<CowStr>) -> Self {
self.props.alter_id(id);
self
}
/// Modifica identificador, clases CSS, atributos HTML o valores extra del componente.
#[builder_fn]
pub fn with_prop(mut self, op: PropsOp) -> Self {
self.props.alter_prop(op);
self
}
/// Define el tipo de disposición que tendrá la barra de navegación.
#[builder_fn]
pub fn with_layout(mut self, layout: navbar::Layout) -> Self {
self.layout = layout;
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`].
///
/// # Ejemplo
///
/// ```rust,ignore
/// navbar.with_item(navbar::Item::nav(...));
/// navbar.with_item(TypedOp::AddMany(vec![
/// navbar::Item::nav(...),
/// navbar::Item::text(...),
/// ]));
/// ```
#[builder_fn]
pub fn with_item(mut self, op: impl Into<TypedOp<navbar::Item>>) -> Self {
self.items.alter_child(op.into());
self
}
}

View file

@ -0,0 +1,84 @@
use crate::prelude::*;
/// Elementos que puede contener una barra de navegación [`Navbar`](super::Navbar).
///
/// Cada variante determina qué se renderiza y cómo. Estos elementos se colocan **dentro del
/// contenido** de la barra (la parte colapsable), por lo que son independientes de la marca o del
/// botón que ya pueda definir el propio [`navbar::Layout`](super::Layout).
#[derive(AutoDefault, Clone, Debug)]
pub enum Item {
/// Sin contenido, no produce salida.
#[default]
Void,
/// Marca de identidad mostrada dentro del contenido de la barra de navegación.
///
/// Útil cuando el [`navbar::Layout`](super::Layout) no incluye marca, y se quiere incluir
/// dentro del área colapsable. Si el *layout* ya muestra una marca, esta variante no la
/// sustituye, sólo añade otra dentro del bloque de contenidos.
Brand(Embed<Brand>),
/// Representa un menú de navegación [`Nav`].
Nav(Embed<Nav>),
/// Representa un *texto localizado* libre.
Text(Lc),
}
#[async_trait]
impl Component for Item {
fn new() -> Self {
Self::default()
}
fn id(&self) -> Option<String> {
match self {
Self::Void => None,
Self::Brand(brand) => brand.id(),
Self::Nav(nav) => nav.id(),
Self::Text(_) => None,
}
}
fn setup(&mut self, _cx: &Context) {
if let Self::Nav(nav) = self
&& let Some(nav) = nav.get_mut()
{
// Se añade aquí, antes de que `nav.render(cx)` (en `prepare()`) dispare el ciclo de
// vida normal del `Nav` embebido y su propio `setup()` prepend las clases de `nav`/
// `nav::Layout`.
nav.alter_prop(PropsOp::prepend_classes("navbar-nav"));
}
}
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
Ok(match self {
Self::Void => html! {},
Self::Brand(brand) => html! { (brand.render(cx).await) },
Self::Nav(nav) => html! { (nav.render(cx).await) },
Self::Text(text) => html! {
span class="navbar-text" {
(text.using(cx))
}
},
})
}
}
impl Item {
/// Crea un elemento de tipo [`navbar::Brand`](super::super::Brand) para añadir en el contenido
/// de [`Navbar`](super::Navbar).
///
/// Pensado para barras colapsables donde se quiere que la marca aparezca en la zona
/// desplegable.
pub fn brand(brand: Brand) -> Self {
Self::Brand(Embed::with(brand))
}
/// Crea un elemento de tipo [`Nav`] para añadir al contenido de [`Navbar`](super::Navbar).
pub fn nav(item: Nav) -> Self {
Self::Nav(Embed::with(item))
}
/// Crea un elemento con un *texto localizado*, mostrado sin interacción.
pub fn text(item: Lc) -> Self {
Self::Text(item)
}
}

View file

@ -0,0 +1,32 @@
use crate::prelude::*;
// **< Layout >*************************************************************************************
/// Representa las distintas formas de presentación de una barra de navegación
/// [`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.
#[derive(AutoDefault, Clone, Debug)]
pub enum Layout {
/// Barra simple, sin marca de identidad y sin botón de despliegue.
///
/// La barra de navegación no se colapsa.
#[default]
Simple,
/// Barra simple, con botón de despliegue y sin marca de identidad.
SimpleToggle,
/// Barra simple, con marca de identidad y sin botón de despliegue.
///
/// La barra de navegación no se colapsa.
SimpleBrandLeft(Embed<Brand>),
/// Barra con marca de identidad y botón de despliegue, en ese orden.
BrandLeft(Embed<Brand>),
/// Barra con botón de despliegue y marca de identidad, en ese orden.
BrandRight(Embed<Brand>),
}

View file

@ -20,7 +20,7 @@ impl Theme for Basic {
.with_weight(-99),
))
.alter_assets(AssetsOp::AddStyleSheet(
StyleSheet::from("/pagetop/css/basic.css")
StyleSheet::from("/pagetop/css/basic.min.css")
.with_version(PAGETOP_VERSION)
.with_weight(-99),
))
@ -30,6 +30,9 @@ impl Theme for Basic {
.alter_assets(AssetsOp::AddJavaScript(
JavaScript::defer("/pagetop/js/basic.dropdown.min.js").with_version(PAGETOP_VERSION),
))
.alter_assets(AssetsOp::AddJavaScript(
JavaScript::defer("/pagetop/js/basic.navbar.init.js").with_version(PAGETOP_VERSION),
))
.alter_assets(AssetsOp::AddJavaScript(
JavaScript::defer("/pagetop/js/basic.dialog.min.js").with_version(PAGETOP_VERSION),
))